Entity Framework Core and LINQ to Entities in Depth (8) Optimistic Concurrency

[LINQ via C# series]

[Entity Framework Core (EF Core) series]

[Entity Framework (EF) series]

Conflicts can occur if the same data is read and changed concurrently. Generally, there are 2 concurrency control approaches:

  • Pessimistic concurrency: one database client can lock the data being accessed, in order to prevent other database clients to change that same data concurrently.
  • Optimistic concurrency: Data is not locked in the database for client to CRUD. Any database client is allowed to read and change any data concurrently. As a result, concurrency conflicts can happen. This is how EF/Core work with database.

To demonstrate the behavior of EF/Core for concurrency, the following DbReaderWriter type is defined as database CRUD client:

internal partial class DbReaderWriter : IDisposable
{
    private readonly DbContext context;

    internal DbReaderWriter(DbContext context) => this.context = context;

    internal TEntity Read<TEntity>(params object[] keys) where TEntity : class => 
        this.context.Set<TEntity>().Find(keys);

    internal int Write(Action change)
    {
        change();
        return this.context.SaveChanges();
    }

    internal DbSet<TEntity> Set<TEntity>() where TEntity : class => this.context.Set<TEntity>();

    public void Dispose() => this.context.Dispose();
}

Multiple DbReaderWriter instances can be be used to read and write data concurrently. For example:

internal static partial class Concurrency
{
    internal static void NoCheck(
        DbReaderWriter readerWriter1, DbReaderWriter readerWriter2, DbReaderWriter readerWriter3)
    {
        int id = 1;
        ProductCategory categoryCopy1 = readerWriter1.Read<ProductCategory>(id);
        ProductCategory categoryCopy2 = readerWriter2.Read<ProductCategory>(id);

        readerWriter1.Write(() => categoryCopy1.Name = nameof(readerWriter1));
        // exec sp_executesql N'SET NOCOUNT ON;
        // UPDATE [Production].[ProductCategory] SET [Name] = @p0
        // WHERE [ProductCategoryID] = @p1;
        // SELECT @@ROWCOUNT;
        // ',N'@p1 int,@p0 nvarchar(50)',@p1=1,@p0=N'readerWriter1'
        readerWriter2.Write(() => categoryCopy2.Name = nameof(readerWriter2)); // Last client wins.
        // exec sp_executesql N'SET NOCOUNT ON;
        // UPDATE [Production].[ProductCategory] SET [Name] = @p0
        // WHERE [ProductCategoryID] = @p1;
        // SELECT @@ROWCOUNT;
        // ',N'@p1 int,@p0 nvarchar(50)',@p1=1,@p0=N'readerWriter2'

        ProductCategory category3 = readerWriter3.Read<ProductCategory>(id);
        category3.Name.WriteLine(); // readerWriter2
    }
}

In this example, multiple DbReaderWriter instances read and write data concurrently:

  1. readerWriter1 reads category “Bikes”
  2. readerWriter2 reads category “Bikes”. These 2 entities are independent because they are are from different DbContext instances.
  3. readerWriter1 updates category’s name from “Bikes” to “readerWriter1”. As previously discussed, by default EF/Core locate the category with its primary key.
  4. In database, this category’s name is no longer “Bikes”
  5. readerWriter2 updates category’s name from “Bikes” to “readerWriter2”. It locates the category with its primary key as well. The primary key is unchanged, so the same category can be located and the name can be changed.
  6. So later when readerWriter3 reads the entity with the same primary key, the category entity’s Name is “readerWriter2”.

Detect Concurrency conflicts

Concurrency conflicts can be detected by checking entities’ property values besides primary keys. To required EF/Core to check a certain property, just add a System.ComponentModel.DataAnnotations.ConcurrencyCheckAttribute to it. Remember when defining ProductPhoto entity, its ModifiedDate has a [ConcurrencyCheck] attribute:

public partial class ProductPhoto
{
    [ConcurrencyCheck]
    public DateTime ModifiedDate { get; set; }
}

This property is also called the concurrency token. When EF/Core translate changes of a photo, ModifiedDate property is checked along with the primary key to locate the photo:

internal static void ConcurrencyCheck(DbReaderWriter readerWriter1, DbReaderWriter readerWriter2)
{
    int id = 1;
    ProductPhoto photoCopy1 = readerWriter1.Read<ProductPhoto>(id);
    ProductPhoto photoCopy2 = readerWriter2.Read<ProductPhoto>(id);

    readerWriter1.Write(() =>
    {
        photoCopy1.LargePhotoFileName = nameof(readerWriter1);
        photoCopy1.ModifiedDate = DateTime.Now;
    });
    // exec sp_executesql N'SET NOCOUNT ON;
    // UPDATE [Production].[ProductPhoto] SET [LargePhotoFileName] = @p0, [ModifiedDate] = @p1
    // WHERE [ProductPhotoID] = @p2 AND [ModifiedDate] = @p3;
    // SELECT @@ROWCOUNT;
    // ',N'@p2 int,@p0 nvarchar(50),@p1 datetime2(7),@p3 datetime2(7)',@p2=1,@p0=N'readerWriter1',@p1='2017-01-25 22:04:25.9292433',@p3='2008-04-30 00:00:00'
    readerWriter2.Write(() =>
    {
        photoCopy2.LargePhotoFileName = nameof(readerWriter2);
        photoCopy2.ModifiedDate = DateTime.Now;
    });
    // exec sp_executesql N'SET NOCOUNT ON;
    // UPDATE [Production].[ProductPhoto] SET [LargePhotoFileName] = @p0, [ModifiedDate] = @p1
    // WHERE [ProductPhotoID] = @p2 AND [ModifiedDate] = @p3;
    // SELECT @@ROWCOUNT;
    // ',N'@p2 int,@p0 nvarchar(50),@p1 datetime2(7),@p3 datetime2(7)',@p2=1,@p0=N'readerWriter2',@p1='2017-01-25 22:04:59.1792263',@p3='2008-04-30 00:00:00'
}

In the translated SQL statement, the WHERE clause contains primary key and the original concurrency token. The following is how EF/Core check the concurrency conflicts:

  1. readerWriter1 reads photo with primary key 1, and modified date “2008-04-30 00:00:00”
  2. readerWriter2 reads the same photo with primary key 1, and modified date “2008-04-30 00:00:00”
  3. readerWriter1 locates the photo with primary key and original modified date, and update its large photo file name and modified date.
  4. In database the photo’s modified date is no longer the original value “2008-04-30 00:00:00”
  5. readerWriter2 tries to locate the photo with primary key and original modified date. However the provided modified date is outdated. EF/Core detect that 0 row is updated by the translated SQL, and throws DbUpdateConcurrencyException: Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.

Another option for concurrency check is System.ComponentModel.DataAnnotations.TimestampAttribute. It can only be used for a byte[] property, which is mapped from a rowversion (timestamp) column. For SQL database, these 2 terms, rowversion and timestamp, are the same thing. timestamp is just a synonym of rowversion data type. A row’s non-nullable rowversion column is a 8 bytes (binary(8)) counter maintained by database, its value increases for each change of the row.

Microsoft’s AdventureWorks sample database does not have such a rowversion column, so create one for the Production.Product table:

ALTER TABLE [Production].[Product] ADD [RowVersion] rowversion NOT NULL
GO

Then define the mapping property for Product entity:

public partial class Product
{
    [DatabaseGenerated(DatabaseGeneratedOption.Computed)]
    [Timestamp]
    public byte[] RowVersion { get; set; }

    [NotMapped]
    public string RowVersionString =>
        $"0x{BitConverter.ToUInt64(this.RowVersion.Reverse().ToArray(), 0).ToString("X16")}";
}

Now RowVersion property is the concurrency token. Regarding database automatically increases the RowVersion value, Rowversion also has the [DatabaseGenerated(DatabaseGeneratedOption.Computed)] attribute. The other RowVersionString property returns a readable representation of the byte array returned by RowVersion. It is not a part of the object-relational mapping, so it has a [NotMapped] attribute. The following example updates and and deletes the same product concurrently:

internal static void RowVersion(DbReaderWriter readerWriter1, DbReaderWriter readerWriter2)
{
    int id = 995;
    Product productCopy1 = readerWriter1.Read<Product>(id);
    productCopy1.RowVersionString.WriteLine(); // 0x0000000000000803

    Product productCopy2 = readerWriter2.Read<Product>(id);
    productCopy2.RowVersionString.WriteLine(); // 0x0000000000000803

    readerWriter1.Write(() => productCopy1.Name = nameof(readerWriter1));
    // exec sp_executesql N'SET NOCOUNT ON;
    // UPDATE [Production].[Product] SET [Name] = @p0
    // WHERE [ProductID] = @p1 AND [RowVersion] = @p2;
    // SELECT [RowVersion]
    // FROM [Production].[Product]
    // WHERE @@ROWCOUNT = 1 AND [ProductID] = @p1;
    // ',N'@p1 int,@p0 nvarchar(50),@p2 varbinary(8)',@p1=995,@p0=N'readerWriter1',@p2=0x0000000000000803
    productCopy1.RowVersionString.WriteLine(); // 0x00000000000324B1
    readerWriter2.Write(() => readerWriter2.Set<Product>().Remove(productCopy2));
    // exec sp_executesql N'SET NOCOUNT ON;
    // DELETE FROM [Production].[Product]
    // WHERE [ProductID] = @p0 AND [RowVersion] = @p1;
    // SELECT @@ROWCOUNT;
    // ',N'@p0 int,@p1 varbinary(8)',@p0=995,@p1=0x0000000000000803
}

When updating and deleting photo entities, its auto generated RowVersion property value is checked too. So this is how it works:

  1. readerWriter1 reads product with primary key 995 and row version 0x0000000000000803
  2. readerWriter2 reads product with the same primary key 995 and row version 0x0000000000000803
  3. readerWriter1 locates the photo with primary key and original row version, and update its name. Database automatically increases the photo’s row version. Since the row version is specified as [DatabaseGenerated(DatabaseGeneratedOption.Computed)], EF/Core also locate the photo with the primary key to query the increased row version, and update the entity at client side.
  4. In database the product’s row version is no longer 0x0000000000000803.
  5. Then readerWriter2 tries to locate the product with primary key and original row version, and delete it. No product can be found with outdated row version, EF/Core detect that 0 row is deleted, and throws DbUpdateConcurrencyException.

Resolve concurrency conflicts

DbUpdateConcurrencyException is thrown when SaveChanges detects concurrency conflict:

namespace Microsoft.EntityFrameworkCore
{
    public class DbUpdateException : Exception
    {
        public virtual IReadOnlyList<EntityEntry> Entries { get; }

        // Other members.
    }

    public class DbUpdateConcurrencyException : DbUpdateException
    {
        // Members.
    }
}

Inherited from DbUpdateException, DbUpdateConcurrencyException has an Entries property. Entries returns a sequence of EntityEntry instances, representing the conflicting entities’ tracking information. The basic idea of resolving concurrency conflicts, is to handle DbUpdateConcurrencyException and retry SaveChanges:

internal partial class DbReaderWriter
{
    internal int Write(Action change, Action<DbUpdateConcurrencyException> handleException, int retryCount = 3)
    {
        change();
        for (int retry = 1; retry < retryCount; retry++)
        {
            try
            {
                return this.context.SaveChanges();
            }
            catch (DbUpdateConcurrencyException exception)
            {
                handleException(exception);
            }
        }
        return this.context.SaveChanges();
    }
}

In the above Write overload, if SaveChanges throws DbUpdateConcurrencyException, the handleException function is called. This function is expected to handle the exception and resolve the conflicts properly. Then SaveChanges is called again. If the last retry of SaveChanges still throws DbUpdateConcurrencyException, the exception is thrown to the caller.

Retain database values (database wins)

Similar to previous examples, the following example has multiple DbReaderWriter instances to update a product concurrently:

internal static void UpdateProduct(
    DbReaderWriter readerWriter1, DbReaderWriter readerWriter2, DbReaderWriter readerWriter3,
    Action<EntityEntry> resolveConflicts)
{
    int id = 950;
    Product productCopy1 = readerWriter1.Read<Product>(id);
    Product productCopy2 = readerWriter2.Read<Product>(id);

    readerWriter1.Write(() =>
    {
        productCopy1.Name = nameof(readerWriter1);
        productCopy1.ListPrice = 100.0000M;
    });
    readerWriter2.Write(
        change: () =>
        {
            productCopy2.Name = nameof(readerWriter2);
            productCopy2.ProductSubcategoryID = 1;
        },
        handleException: exception =>
        {
            EntityEntry tracking = exception.Entries.Single();
            Product original = (Product)tracking.OriginalValues.ToObject();
            Product current = (Product)tracking.CurrentValues.ToObject();
            Product database = productCopy1; // Values saved in database.
            $"Original:  ({original.Name},   {original.ListPrice}, {original.ProductSubcategoryID}, {original.RowVersionString})"
                        .WriteLine();
            $"Database:  ({database.Name}, {database.ListPrice}, {database.ProductSubcategoryID}, {database.RowVersionString})"
                .WriteLine();
            $"Update to: ({current.Name}, {current.ListPrice}, {current.ProductSubcategoryID})"
                .WriteLine();

            resolveConflicts(tracking);
        });

    Product resolved = readerWriter3.Read<Product>(id);
    $"Resolved:  ({resolved.Name}, {resolved.ListPrice}, {resolved.ProductSubcategoryID}, {resolved.RowVersionString})"
        .WriteLine();
}

This is how it works with concurrency conflicts:

  1. readerWriter1 reads product with primary key 950, and RowVersion 0x00000000000007D1
  2. readerWriter2 reads product with the same primary key 950, and RowVersion 0x00000000000007D1
  3. readerWriter1 locates product with primary key and original RowVersion 0x00000000000007D1, and updates product’s name and  list price. Database automatically increases the product’s row version
  4. In database the product’s row version is no longer 0x00000000000007D1.
  5. readerWriter2 tries to locate product with primary key and original RowVersion, and update product’s name and subcategory.
  6. readerWriter2 fails to update product, because it cannot locate the product with original RowVersion 0x00000000000007D1. Again, no product can be found with outdated row version, DbUpdateConcurrencyException is thrown.

As a result, the handleException function specified for readWriter2 is called, it retrieves the conflicting product’s tracking information from DbUpdateConcurrencyException.Entries, and logs these information:

  • product’s original property values read by readerWriter2 before the changes
  • product’s property values in database at this moment, which are already updated readerWriter1
  • product’s current property values after changes, which readerWriter2 fails to save to database.

Then handleException calls resolveConflicts function to actually resolve the conflict. Then readerWriter2 retries to save the product changes again. This time, SaveChanges should succeed, because there is no conflicts anymore (In this example, there are only 2 database clients reading/writing data concurrently. In reality, the concurrency can be higher, an appropriate retry count or retry strategy should be specified.). Eventually, readerWriter3 reads the product from database, verify its property values.

There are several options to implement the resolveConflicts function to resolves the conflicts. One simple option, called “database wins”, is to simply give up the client update, and let database retain whatever values it has for that entity. This seems to be easy to just catch DbUpdateConcurrencyException and do nothing, then database naturally wins, and retains its values:

internal partial class DbReaderWriter
{
    internal int WriteDatabaseWins(Action change)
    {
        change();
        try
        {
            return this.context.SaveChanges();
        }
        catch (DbUpdateConcurrencyException)
        {
            return 0; // this.context is in a corrupted state.
        }
    }
}

However, this way leaves the DbContext, the conflicting entity, and the entity’s tracking information in a corrupted state. For the caller, since the change saving is done, the entity’s property values should be in sync with database values, but the values are actually out of sync and still conflicting. Also, the entity has a tracking state Modified after change saving is done. So the safe approach is to reload and refresh the entity’s values and tracking information:

internal static void DatabaseWins(
    DbReaderWriter readerWriter1, DbReaderWriter readerWriter2, DbReaderWriter readerWriter3)
{
    UpdateProduct(readerWriter1, readerWriter2, readerWriter3, resolveConflicts: tracking =>
    {
        tracking.State.WriteLine(); // Modified
        tracking.Property(nameof(Product.Name)).IsModified.WriteLine(); // True
        tracking.Property(nameof(Product.ListPrice)).IsModified.WriteLine(); // False
        tracking.Property(nameof(Product.ProductSubcategoryID)).IsModified.WriteLine(); // True

        tracking.Reload(); // Execute query.

        tracking.State.WriteLine(); // Unchanged
        tracking.Property(nameof(Product.Name)).IsModified.WriteLine(); // False
        tracking.Property(nameof(Product.ListPrice)).IsModified.WriteLine(); // False
        tracking.Property(nameof(Product.ProductSubcategoryID)).IsModified.WriteLine(); // False
    });
    // Original:  (ML Crankset,   256.4900, 8, 0x00000000000007D1)
    // Database:  (readerWriter1, 100.0000, 8, 0x0000000000036335)
    // Update to: (readerWriter2, 256.4900, 1)
    // Resolved:  (readerWriter1, 100.0000, 8, 0x0000000000036335)
}

UpdateProduct is called with a resolveConflicts function, which resolves the conflict by calling Reload method on the EntityEntry instance representing the conflicting product’s tracking information:

  1. EntityEntry.Reload executes a SELECT statement to read the product’s property values from database, then refresh the product entity and all tracking information. The product’s property values, the tracked original property values before changes, the tracked current property values after changes, are all refreshed to the queried database values. The entity tracking state is also refreshed to Unchanged.
  2. At this moment, product has the same tracked original values and current values, as if it is just initially read from database, without changes.
  3. When DbReaderWriter.Write’s retry logic calls SaveChanges again, no changed entity is detected. SaveChanges succeeds without executing any SQL, and returns 0. As expected, readerWriter2 does not update any value to database, and all values in database are retained.

Later, when readerWriter3 reads the product again, product has all values updated by readerWrtier1.

Overwrite database values (client wins)

Another simple option, called “client wins”, is to disregard values in database, and overwrite them with whatever data submitted from client.

internal static void ClientWins(
    DbReaderWriter readerWriter1, DbReaderWriter readerWriter2, DbReaderWriter readerWriter3)
{
    UpdateProduct(readerWriter1, readerWriter2, readerWriter3, resolveConflicts: tracking =>
    {
        PropertyValues databaseValues = tracking.GetDatabaseValues();
        // Refresh original values, which go to WHERE clause of UPDATE statement.
        tracking.OriginalValues.SetValues(databaseValues);

        tracking.State.WriteLine(); // Modified
        tracking.Property(nameof(Product.Name)).IsModified.WriteLine(); // True
        tracking.Property(nameof(Product.ListPrice)).IsModified.WriteLine(); // True
        tracking.Property(nameof(Product.ProductSubcategoryID)).IsModified.WriteLine(); // True
    });
    // Original:  (ML Crankset,   256.4900, 8, 0x00000000000007D1)
    // Database:  (readerWriter1, 100.0000, 8, 0x0000000000036336)
    // Update to: (readerWriter2, 256.4900, 1)
    // Resolved:  (readerWriter2, 256.4900, 1, 0x0000000000036337)
}

The same conflict is resolved differently:

  1. EntityEntry.GetDatabaseValues executes a SELECT statement to read the product’s property values from database, including the updated row version. This call does not impact the product values or tracking information.
  2. Manually set the tracked original property values to the queried database values. The entity tracking state is still Changed. The original property values become all different from tracked current property values. So all product properties are tracked as modified.
  3. At this moment, the product has tracked original values updated, and keeps all tracked current values, as if it is read from database after readerWriter1 updates the name and list price, and then have all properties values changed.
  4. When DbReaderWriter.Write’s retry logic calls SaveChanges again, product changes are detected to submit. So EF/Core translate the product change to a UPDATE statement. In the SET clause, since there are 3 properties tracked as modified, 3 columns are set. In the WHERE clause, to locate the product, the tracked original row version has been set to the updated value from database. This time product can be located, and all 3 properties are updated. SaveChanges succeeds and returns 1. As expected, readerWriter2 updates all value to database.

Later, when readerWriter3 reads the product again, product has all values updated by readerWrter2.

Merge with database values

A more complex but useful option, is to merge the client values and database values. For each property:

  • If original value is different from database value, which means database value is already updated by other concurrent client, then give up updating this property, and retain the database value
  • If original value is the same as database value, which means no concurrency conflict for this property, then process normally to submit the change
internal static void MergeClientAndDatabase(
    DbReaderWriter readerWriter1, DbReaderWriter readerWriter2, DbReaderWriter readerWriter3)
{
    UpdateProduct(readerWriter1, readerWriter2, readerWriter3, resolveConflicts: tracking =>
    {
        PropertyValues databaseValues = tracking.GetDatabaseValues(); // Execute query.
        PropertyValues originalValues = tracking.OriginalValues.Clone();
        // Refresh original values, which go to WHERE clause.
        tracking.OriginalValues.SetValues(databaseValues);
        // If database has an different value for a property, then retain the database value.
#if EF
        databaseValues.PropertyNames // Navigation properties are not included.
            .Where(property => !object.Equals(originalValues[property], databaseValues[property]))
            .ForEach(property => tracking.Property(property).IsModified = false);
#else
        databaseValues.Properties // Navigation properties are not included.
            .Where(property => !object.Equals(originalValues[property.Name], databaseValues[property.Name]))
            .ForEach(property => tracking.Property(property.Name).IsModified = false);
#endif
        tracking.State.WriteLine(); // Modified
        tracking.Property(nameof(Product.Name)).IsModified.WriteLine(); // False
        tracking.Property(nameof(Product.ListPrice)).IsModified.WriteLine(); // False
        tracking.Property(nameof(Product.ProductSubcategoryID)).IsModified.WriteLine(); // True
    });
    // Original:  (ML Crankset,   256.4900, 8, 0x00000000000007D1)
    // Database:  (readerWriter1, 100.0000, 8, 0x0000000000036338)
    // Update to: (readerWriter2, 256.4900, 1)
    // Resolved:  (readerWriter1, 100.0000, 1, 0x0000000000036339)
}

With this approach:

  1. Again, EntityEntry.GetDatabaseValues executes a SELECT statement to read the product’s property values from database, including the updated row version.
  2. Backup tracked original values, then refresh conflict.OriginalValues to the database values, so that these values can go to the translated WHERE clause. Again, the entity tracking state is still Changed. The original property values become all different from tracked current property values. So all product values are tracked as modified and should go to SET clause.
  3. For each property, if the backed original value is different from the database value, it means this property is changed by other client and there is concurrency conflict. In this case, revert this property’s tracking status to unmodified. The name and list price are reverted.
  4. At this moment, the product has tracked original values updated, and only keeps tracked current value of subcategory, as if it is read from database after readerWriter1 updates the name and list price, and then only have subcategory changed, which has no conflict.
  5. When DbReaderWriter.Write’s retry logic calls SaveChanges again, product changes are detected to submit. Here only subcategory is updated to database. SaveChanges succeeds and returns 1. As expected, readerWriter2 only updates value without conflict, the other conflicted values are retained.

Later, when readerWriter3 reads the product, product has name and list price values updated by readerWrtier1, and has subcategory updated by readerWriter2.

Save changes with concurrency conflict handling

Similar to above DbReaderWriter.Write method, a general SaveChanges extension method for DbContext can be defined to handle concurrency conflicts and apply simple retry logic:

public static partial class DbContextExtensions
{
    public static int SaveChanges(
        this DbContext context, Action<IEnumerable<EntityEntry>> resolveConflicts, int retryCount = 3)
    {
        if (retryCount <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(retryCount));
        }

        for (int retry = 1; retry < retryCount; retry++)
        {
            try
            {
                return context.SaveChanges();
            }
            catch (DbUpdateConcurrencyException exception) when (retry < retryCount)
            {
                resolveConflicts(exception.Entries);
            }
        }
        return context.SaveChanges();
    }
}

To apply custom retry logic, Microsoft provides EnterpriseLibrary.TransientFaultHandling NuGet package (Exception Handling Application Block) for .NET Framework. It has been ported to .NET Core for this tutorial, as EnterpriseLibrary.TransientFaultHandling.Core NuGet package. can be used. With this library, a SaveChanges overload with customizable retry logic can be easily defined:

public class TransientDetection<TException> : ITransientErrorDetectionStrategy
    where TException : Exception
{
    public bool IsTransient(Exception ex) => ex is TException;
}

public static partial class DbContextExtensions
{
    public static int SaveChanges(
        this DbContext context, Action<IEnumerable<EntityEntry>> resolveConflicts, RetryStrategy retryStrategy)
    {
        RetryPolicy retryPolicy = new RetryPolicy(
            errorDetectionStrategy: new TransientDetection<DbUpdateConcurrencyException>(),
            retryStrategy: retryStrategy);
        retryPolicy.Retrying += (sender, e) =>
            resolveConflicts(((DbUpdateConcurrencyException)e.LastException).Entries);
        return retryPolicy.ExecuteAction(context.SaveChanges);
    }
}

Here Microsoft.Practices.EnterpriseLibrary.TransientFaultHandling.ITransientErrorDetectionStrategy is the contract to detect each exception, and determine whether the exception is transient and the operation should be retried. Microsoft.Practices.EnterpriseLibrary.TransientFaultHandling.RetryStrategy is the contract of retry logic. Then Microsoft.Practices.EnterpriseLibrary.TransientFaultHandling.RetryPolicy executes the operation with the specified exception detection, exception handling, and retry logic.

As discussed above, to resolve a concurrency conflict, the entity and its tracking information need to be refreshed. So the more specific SaveChanges overloads can be implemented by applying refresh for each conflict:

public enum RefreshConflict
{
    StoreWins,

    ClientWins,

    MergeClientAndStore
}

public static partial class DbContextExtensions
{
    public static int SaveChanges(this DbContext context, RefreshConflict refreshMode, int retryCount = 3)
    {
        if (retryCount <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(retryCount));
        }

        return context.SaveChanges(
            conflicts => conflicts.ForEach(tracking => tracking.Refresh(refreshMode)), retryCount);
    }

    public static int SaveChanges(
        this DbContext context, RefreshConflict refreshMode, RetryStrategy retryStrategy) =>
            context.SaveChanges(
                conflicts => conflicts.ForEach(tracking => tracking.Refresh(refreshMode)), retryStrategy);
}

A RefreshConflict enumeration has to be defined with 3 members to represent the 3 options discussed above: database wins, client wind, merge client and database.. And here the Refresh method is an extension method for EntityEntry:

public static EntityEntry Refresh(this EntityEntry tracking, RefreshConflict refreshMode)
{
    switch (refreshMode)
    {
        case RefreshConflict.StoreWins:
        {
            // When entity is already deleted in database, Reload sets tracking state to Detached.
            // When entity is already updated in database, Reload sets tracking state to Unchanged.
            tracking.Reload(); // Execute SELECT.
            // Hereafter, SaveChanges ignores this entity.
            break;
        }
        case RefreshConflict.ClientWins:
        {
            PropertyValues databaseValues = tracking.GetDatabaseValues(); // Execute SELECT.
            if (databaseValues == null)
            {
                // When entity is already deleted in database, there is nothing for client to win against.
                // Manually set tracking state to Detached.
                tracking.State = EntityState.Detached;
                // Hereafter, SaveChanges ignores this entity.
            }
            else
            {
                // When entity is already updated in database, refresh original values, which go to in WHERE clause.
                tracking.OriginalValues.SetValues(databaseValues);
                // Hereafter, SaveChanges executes UPDATE/DELETE for this entity, with refreshed values in WHERE clause.
            }
            break;
        }
        case RefreshConflict.MergeClientAndStore:
        {
            PropertyValues databaseValues = tracking.GetDatabaseValues(); // Execute SELECT.
            if (databaseValues == null)
            {
                // When entity is already deleted in database, there is nothing for client to merge with.
                // Manually set tracking state to Detached.
                tracking.State = EntityState.Detached;
                // Hereafter, SaveChanges ignores this entity.
            }
            else
            {
                // When entity is already updated, refresh original values, which go to WHERE clause.
                PropertyValues originalValues = tracking.OriginalValues.Clone();
                tracking.OriginalValues.SetValues(databaseValues);
                // If database has an different value for a property, then retain the database value.
#if EF
                databaseValues.PropertyNames // Navigation properties are not included.
                    .Where(property => !object.Equals(originalValues[property], databaseValues[property]))
                    .ForEach(property => tracking.Property(property).IsModified = false);
#else
                databaseValues.Properties // Navigation properties are not included.
                    .Where(property => !object.Equals(originalValues[property.Name], databaseValues[property.Name]))
                    .ForEach(property => tracking.Property(property.Name).IsModified = false);
#endif
                // Hereafter, SaveChanges executes UPDATE/DELETE for this entity, with refreshed values in WHERE clause.
            }
            break;
        }
    }
    return tracking;
}

EF already provides a System.Data.Entity.Core.Objects.RefreshMode enumeration, but it only has 2 members: StoreWins and ClientWins.

This Refresh extension method covers the update conflicts discussed above, as well as deletion conflicts. Now the these SaveChanges extension methods can be used to manage concurrency conflicts easily. For example:

internal static void SaveChanges(AdventureWorks adventureWorks1, AdventureWorks adventureWorks2)
{
    int id = 950;
    Product productCopy1 = adventureWorks1.Products.Find(id);
    Product productCopy2 = adventureWorks2.Products.Find(id);

    productCopy1.Name = nameof(adventureWorks1);
    productCopy1.ListPrice = 100;
    adventureWorks1.SaveChanges();

    productCopy2.Name = nameof(adventureWorks2);
    productCopy2.ProductSubcategoryID = 1;
    adventureWorks2.SaveChanges(RefreshConflict.MergeClientAndStore);
}

85 Comments

  • By far the most complete detailed explanation of Concurrency handling with EF that I have read. Thank you!

  • C# Linq to SQL vs Entity Framework .Net Core

    I decided to start learning something about .Net Core and Entity Framework for Core. Since most of our apps rely heavily on data access, I decided to start with the Entity Framework for .Net Core. After creating a working app, I decided to run the same the same query using Linq to SQL and test the response time. The database was local (running on the same machine). The EF code was created based on the following article… Data Points - Run EF Core on Both .NET Framework and .NET Core (https://msdn.microsoft.com/en-us/magazine/mt742867.aspx).

    Linq to Sql vs Entity Famework Core – Speed Test

    Linq to SQL in .net Library and called from .Net Console App Entity Framework Core in a Core Library called same .Net Console App

    The query transfers table fields to a DTO object

    Records in Table: 1,906,643; Records returned in Query: 78; Linq to SQL: 3 Seconds; Entity Framework Core: 19 Seconds

    Similar Query for Both Linq to SQL and EF Core…

    public IEnumerable<PackageDataDTO> GetAll(string Building, string Status="All")
    {
    using (var context = new PackageDataEF.PackageDataContext(_options))
    {
    var Lst = from Dbo in context.PackageData
    where Dbo.BuildingNumber.Equals(Building)
    && (Dbo.Status.Equals("All") || Dbo.Status.Equals(Status))
    select new PackageDataDTO
    {
    RecordGuid = Dbo.RecordGuid,
    //TimeStamp = Dbo.TimeStamp,
    BuildingNumber = Dbo.BuildingNumber,
    UnitNumber = Dbo.UnitNumber,
    TrackingNumber = Dbo.TrackingNumber,
    Carrier = Dbo.Carrier,
    PackageType = Dbo.PackageType,
    PackageLocation = Dbo.PackageLocation,
    StorageArea = Dbo.StorageArea,
    DateOfReceiptUTC = Dbo.DateOfReceiptUTC,
    DateLastChangeUTC = Dbo.DateLastChangeUTC,
    Status = Dbo.Status,
    ResidentId = Dbo.ResidentId,
    NotificationBy = Dbo.NotificationBy,
    CallSid = Dbo.CallSid,
    Comments = Dbo.Comments,
    DeliveredTo = Dbo.DeliveredTo,
    DeliveredBy = Dbo.DeliveredBy,
    DeliveryNotes = Dbo.DeliveryNotes,
    DeliveryConfirmation = Dbo.DeliveryConfirmation,
    DelayedNotification = Dbo.DelayedNotification,
    GroupedNotification = Dbo.GroupedNotification,
    ButtonOnReceipt = Dbo.ButtonOnReceipt,
    };
    return Lst.ToList();
    }
    }
    Is there really that much of a speed difference or am I doing something wrong?

    Thanks in advance for any guidance.



  • [url=https://www.amazonprimefirestick.com]fff[/url]

    <strong><a href="https://www.amazonprimefirestick.com">f</a></strong>

  • I like your blog post. Keep on writing this type of great stuff. I make sure to follow up on your blog in the future.

  • I read the above post and get some useful knowledge about the framework. Thanks for sharing the nice information.

  • thank you for this site

  • thank you for sharing this post

  • قیمت درب داخلی ساختمان

  • thankk you for this site

  • thank you

  • بهترین نوع در بضد سرقت

  • thank you for sharing this post

  • در نظر گرفتن هزینه سم زدایی به روش Urod یکی از موضوعات مهم در سم زدایی Urodاست که باید مطرح شود. زیرا انجام سم زدایی به روش Urod در هر مرکزی یکسان نیست و متفاوت خواهد بود.مقاله ای که قرار است ارائه شود در رابطه با سم زدایی Urod یا سم زدایی فوق سریع است. احتمالا شما هم جزء کسانی هستید که سم زدایی Urod را برای سم زدایی در نظر گرفته اید یا مشتاق هستید که اطلاعات جامع تری را در مورد سم زدایی Urod به دست بیاورید، پس با ما همراه باشید.راه های مختلفی برای درمان اعتیاد موجود است. راه هایی که سر و ته شان به درمان اعتیاد ختم شده و قرار است به وسیله همین راه ها و شیوه ها معتاد از دام اعتیاد بیرون کشید.

  • ما در این مقاله به شما کمک کردیم تا انتخاب درست برای دکتر کاشت مو خود داشته باشد.هدف از ارائه این مقاله معرفی ۱۰ تا از بهترین دکترهای کاشت مو می باشد تا شما اطلاعات لازم را کسب کنید.

  • امروزه بسیاری از خانم ها برای زیباتر دیده شدن چهره شان ، به دنبال راه های مختلف می روند . مژه ها باعث زیبا تر دیده شدن چهره افراد نیز می شوند . چه بسا اگر مژه های پر پشت و بلندتری داشته باشید . در این مقاله شما را برای داشتن مژه های پر پشت و بلند راهنمایی خواهیم کرد .

  • کمپ ترک اعتیاد رایگان یک نوع مرکز ترک اعتیاد است که خدمات و سرویس های خود را به معتادانی که از نظر مالی بسیار ضعیف هستند و به صورت کارتون خواب در سطح شهر پراکنده شده اند قرار خواهند داد.

  • کاشت مو مانند هر عمل زیبایی دارای مراحل مختلف است که بهتر است قبل از انجام آن کمی با مراحل کاشت آشنا شوید تا با اطلاعات دقیق تری برای انجام این کار اقدام کنید. این روزها بسیاری از خانم ها و آقایان با مشکل کم پشتی مو و طاسی سر مواجه هستند و تصمیم می‌گیرند کاشت مو را انجام دهند.

  • اگر قرار به انتخاب کمپ ترک اعتیاد باشد، باید اطلاعات بالایی در این زمینه داشته باشیم. اطلاعاتی که بتوانند به ما در انتخاب درست کمپ اعتیاد کمک کنند. ما در این مقاله بررسی کلی‌ای روی کار مراکز ترک اعتیاد داری و نگرانی‎های موجود در این زمینه را بررسی خواهیم کرد. اگر می‌خواهید هر چه بیشتر با کمپ ترک اعتیاد آشنا شوید، با ما همراه باشید.

  • خریدار انواع لپتاپ و پرینتر دست دوم و نو در منزل و محل کار شما حضور سریع و پرداخت نقد و کلرت به کارت در محل

  • تاکسی vip با کیفیت عالی قیمت مناسب آرامش و امنیت را برای مسافران خود به ارمغان می آورد، ما هر روزه هفته ۲۴ ساعته خدمتگذار شما عزیزان هستیم. از مزایای تاکسی vip می توان به حضور رانندگان مجرب و با سابقه شرکت تاکسیرانی، احساس راحتی و آرامش و استفاده از اتومبیلهای با ضریب امنیت و کیفیت بالا اشاره کرد.

  • درآموزش تعمیرات برد های الکترونیکی به شما نحوه تعمیرات انواع بردهای لوازم خانگی، تعمیرات بردهای صنعتی، تعمیرات برد پکیج، تعمیرات برد کولر گازی، تعمیرات برد اینورتر و ... آموزش داده خواهد شد.
    https://fannipuyan.com/electronic-boards-repair-training/

  • <a href="https://tahvienovin.com/">شرکت تهویه نوین ایرانیان</a> با بهره گیری از کادری مجرب و حرفه ای، متشکل از مهندسین با تجربه و نیروهای متخصص بر آن است تا در مسیر تحقق مشتری مداری گامهایی مؤثرتر بردارد. در این راستا با ارائه محصولاتی با کیفیت، عملکردی مطلوب، هزینه ای بهینه و نیز خدمات پس از فروش، در پی جلب رضایت مشتریان گرامی است.

  • https://ma-study.blogspot.com/

  • فن کویل یک وسیله چند منظوره است که مبدل هوا بوده و گرما و سرمای هوا را با توجه به خواسته ما انجام می دهد. در واقع می توان گفت فن کویل میدیا یک مبدل گرمایشی بسیار قوی است که در کنار فن کویل می تواند تبدیل به یک مبدل سرمایشی کارآمد شود. این دستگاه در هوای سرد موجب گرمای محیط و در هوای گرم هوای خنک را در محیط تولید می کند. می توان گفت انواع فن ها به کانال وصل نمی شوند و هوا را به صورت مستقیم وارد فضا می کنند.

  • خرید فن کویل میدیا از نمایندگی

  • خرید صندلی اداری از نمایندگی

  • The gap between sports media coverage has been reviewed around free advertising space online. Sports fans who want to watch EPL broadcasts, international football broadcasts, volleyball broadcasts and radio broadcasts can watch sports 24 hours a day, 365 days a year thanks to sports broadcasts which provide sports schedules and information disseminated. You can watch sports videos on the Internet using your smartphone, tablet or PC.

  • خرید گیم تایم 60 روزه از جت گیم

    اگر به دنبال این هستید که یک گیم تایم 60 روزه را خریداری کنید برای بازی world of warcraft خود می توانید به فروشگاه جت گیم مراجعه کنید. یکی از ویژگی های این فروشگاه آنی بودن آن است. پس از پرداخت قیمت کد محصول به شما در سریع ترین مدت زمان تحویل داده می شود. در حال حاضر مزیت فروشگاه جت گیم همین است که نسبت به فروشگاه های دیگر سریع تر است. و با کادری مجرب و با پشتیبانی محصولات ارائه شده به کاربران با مناسب ترین قیمت در حال فعالیت می باشد.

    بهترین راه برای اکتیو کردن گیم تایم 60 روزه
    راحت ترین راه و بهترین راه برای فعال کردن گیم تایم ارائه به کلاینت بتل نت است. بعد از اینکه شما گیم تایم 60 روزه را از جت گیم خریداری کنید به شما یک کد ارسال می شود. شما باید این کد را در کلاینت بتل نت بخش Rededm a Code وارد کنید تا گیم تایم 60 روزه برای شما فعال شود. اما راه دیگر شما برای اکتیو کردن گیم تایم مراجعه به سایت بتل نت است.

    ارتباط گیم تایم به شدولند
    از همان روز اولی که شدولند به دنیای world of warcraft آمد گیم تایم نیز ارائه شد. می توان گفت که اصلی ترین هدف ارتباط گیم تایم به شدولند جلوگیری از چیت زدن است. چرا که برای اینکه شما بتوانید گیم تایم را بازی کنید باید هزینه زیادی را پرداخت کنید. از طرفی دیگر قوی کردن سرور ها است. بعد از به وجود آمدن سرور های گیم تایم سرور های بازی خود وارکرافت نیز قوی تر شده است.




    سخن آخر خرید گیم تایم 60 روزه
    جمع بندی که می توان از این مطلب داشته باشیم این است که شما می توانید برای خرید گیم تایم 60 روزه از فروشگاه جت گیم آن را خریداری کنید. گیم تایم 60 روزه دارای سرور اروپا و آمریکا است که بهتر است سرور گیم تایم شما با شدولند شما یکی باشد تا از لحاظ پینگی مشکلی را به وجود نیاورد. امیدوارم مطالب برای علاقمندان این گیم جذاب مفید قرار گرفته باشه با تشکر.

  • Two-month gametime popularity:
    As mentioned above, 60-day gametime has been more popular than other gametime for a few months. This is because it has both the right time and the right price. The reason world of warcraft players use this type of game time is time. Because 60 days of game time is an average game time and most people use the days of this game time. One of the advantages of this game time over other game times is its length of time.

    Types of game time regions
    In general, the two-month game time is made up of 2 regions, Europe and America. But an important argument is that it is recommended to get a gametime regimen that is compatible with your Shodland region. If you are looking for our advice, we recommend that you buy the European region. Because it is close to Middle East servers and you usually get a better ping. More products from Jet Game site

  • گیم تایم 60 روزه در حال حاضر تنها گیم تایمی است که از طرف کمپانی blizzard برای بازیکنان گیم ، ورد اف وارکرافت ارائه شده است. در گذشته گیم تایم هایی مانند 30 روزه و 180 روزه هم موجود بود اما به دلیل سیاست های جدید این کمپانی و خط مشی که در نظر گرفته است، تنها گیم تایمی که در حال حاضر امکان فراهم کردن آن برای گیمر های عزیز، گیم تایم 60 روزه می باشد. در ادامه توضیحات جالبی در مورد گیم تایم برای شما جمع آوری کرده ایم که خواندنشان خالی از لطف نیست.

    کاربرد گیم تایم دو ماهه

    در حال حاضر گیم تایم 2 ماهه در تمامی زمینه های world of warcraft کاربرد دارد. اما اگر می خواهید که یک سری تجربه های جذاب و جدید را تجربه کنید باید این گیم تایم را خریداری کنید. این تجربه ها عبارتند از:
    استفاده از اکسپنشن های جدید
    بازی در مپ های جدید
    لول آپ به سبک جدید
    تغییر در شکل بازی
    تهیه از سایت جت گیم
    حمایت کنید لطفا

  • Popularity of Gametime for two months:
    As mentioned above, the 60-day game time has been more popular than other game times for several months. This is because it has both the right time and the right price. The reason why World of Warcraft players use this type of game time is the duration. Because the game time of 60 days is an average game time and most people use the days of this game time. One advantage that this game time has over other game times is its duration.

    All kinds of game time regions
    In general, the two-month game time is made from 2 regions, Europe and America. But an important point is that it is recommended to get a region of Gametime that is compatible with your Shadowland region. If you are looking for our advice, we recommend you to buy Region Europe. Because it is close to Middle East servers and usually you get better ping.
    Prepared from the Jet Game website

  • The 60-day game time is currently the only game time provided by the blizzard company for the players of the game, Word of Warcraft. In the past, game times such as 30 days and 180 days were also available, but due to the new policies of this company and the policy it has considered, the only game time that is currently available for dear gamers is Game Time 60. It is fasting. In the following, we have collected interesting explanations about Game Time for you, which are worth reading.

    Game time application for two months

    Currently, 2-month game time is used in all areas of World of Warcraft. But if you want to experience a series of interesting and new experiences, you should buy this game time. These experiences include:
    Using new extensions
    Play on new maps
    Lollup in a new style
    Change in the shape of the game
    Produced from the Jet Game website. Please support the Jet Game website.

  • گیم تایم 60 روزه در حال حاضر تنها گیم تایمی است که از طرف کمپانی blizzard برای بازیکنان گیم ، ورد اف وارکرافت ارائه شده است. در گذشته گیم تایم هایی مانند 30 روزه و 180 روزه هم موجود بود اما به دلیل سیاست های جدید این کمپانی و خط مشی که در نظر گرفته است، تنها گیم تایمی که در حال حاضر امکان فراهم کردن آن برای گیمر های عزیز، گیم تایم 60 روزه می باشد. در ادامه توضیحات جالبی در مورد گیم تایم برای شما جمع آوری کرده ایم که خواندنشان خالی از لطف نیست.

    کاربرد گیم تایم دو ماهه

    در حال حاضر گیم تایم 2 ماهه در تمامی زمینه های world of warcraft کاربرد دارد. اما اگر می خواهید که یک سری تجربه های جذاب و جدید را تجربه کنید باید این گیم تایم را خریداری کنید. این تجربه ها عبارتند از:
    استفاده از اکسپنشن های جدید
    بازی در مپ های جدید
    لول آپ به سبک جدید
    تغییر در شکل بازی

  • I came to this site with the introduction of a friend around me and I was very impressed when I found your writing. I'll come back often after bookmarking! Keonhacai

  • خرید بازی دراگون فلایت جت گیم  سری بازی ورلد آف وارکرافت یکی از قدیمی ترین گیم هایی است که هم از نظر محبوبیت و هم از نظر شکل بازی نزدیک به دو دهه است که با ارائه انواع بسته های الحاقی برای دوستداران این گیم سرپا است و به کار خود ادامه می دهد .
    ورلد آف وارکرافت توسط شرکت بلیزارد ارائه شده و بدلیل سبک بازی و گرافیک بالا در سرتاسر جهان طرفداران زیادی را به خود جذب کرده است.
    این بازی محبوب دارای انواع بسته های الحاقی میباشد که جدید ترین آن که به تازگی رونمائی شده و در حال حاضر صرفا امکان تهیه پیش فروش آن فراهم میباشد دراگون فلایت است
    این بازی که از نظر سبک بازی با سایر نسخه ها متفاوت بوده و جذابیت خاص خود را دارد که در ادامه به آن می پردازیم . همچنین برای تهیه نسخه های این گیم جذاب می توانید به سایت جت گیم مراجعه نمائید. در ادامه بیشتر در مورد بازی و سیستم مورد نیاز بازی می پردازیم
    سایت جت گیم

  • The assignment submission period was over and I was nervous, and I am very happy to see your post just in time and it was a great help. Thank you ! Leave your blog address below. Please visit me anytime.

  • Let our writers take care of your headaches and enjoy stress free days for weeks just for $12.99 per page for an essay. Also, you don`t have to worry about plagiarism

  • She rubs it on your own body in a way that provides you with the most amazing sexual pleasure. She lets her naked body rub, and glide across every part of your body to provide you with sensual sensations. For more info visit here: - https://www.bellaspa.in

  • People used to travel abroad for high end massage. Imagine the kind of money one had to invest but with Body to body massage in your city, you can experience the same in your own city. One such spa is sweety massage who is known for rendering some of the best quality massage. Visit https://www.spasweety.com/

  • The woman to man body massage is a popular trend in young people. Most middle-aged people also look for it. The soft gesture and body touch is the ultimate way to avail the fun. Females are good at massage and blessed with soft hands. Our girls are also a great way to get the high-grade female to male massage in Hyderabad . Some of the reasons to connect with our service are as follows:

  • We are Hyderabad's most popular spa and have carved out a place for ourselves as a female to male spa. We have our spa at several places around Hyderabad, so no matter where you are in the city, you will always be able to access Izspa. In a word, we give all forms of massages by the top therapists, from mind to body relaxation.

  • Rose is an erotic masseuse with many years of experience in the ancient Thai massaging technic. Her clients have never had a reason to doubt her. She keeps them sensually alive, United with their emotions and relieved from their stress.

  • Body-to-body massage centers with excellent services are hard to come by these days, a wise man once said that money well spent is money spent on self. That’s why we at Lisha body spa are here to bring you the ultimate in body-to-body massage. We are the body-to-body spa near you.

  • Nuru Massage and Erotic Body Massage in Bangalore Will Give You a Unique Experience, All for the Reasons That Make You Relaxed. Best Massage Therapy by Our Experts Including Body To Body Massage, Female To Male Massage. We Take Care of Every Detail and Strive to Provide an Amazing Experience from When They Step in Until Their Leave with Happiness! It Begins with Our Massage Service Which Reduces Tiredness. Sore Muscles Relieve Tension and Enhance Blood Circulation So People Feel Much Better Than Before... but Stay There’s More!!!

  • I didn't expect to receive any new news from here. Everything looks different but interesting in itself.

  • B2B massage in Chennai is the most sensational massage service offered by the massage therapist since the body massage provider often follows the demands of the client when conducting body massage. For more details visit: https://www.vipbodyspa.com/

  • When my mind doesn’t push my dreams or when my strength does not support my will I directly go for a Nuru massage in Chennai. Body massage is the drug for my health and my mind. I feel so rejuvenated, energized, and full of spirit. And in Chennai flip spa is the best spa provider. Visit http://www.flipbodyspa.com

  • The Best with Our Massage Care and Hair Dressing at Our massage near me. We are always there for you. Our Young girls are having greater endurance, and are experiencing more strains. Enjoy our Body massage in Bangalore. For more info visit here: - https://www.ramyabodyspa.com/

  • Stellen Sie sicher, dass Sie Ihre Anmeldeinformationen <a href="https://o2guide.website/">webmail o2 login</a> korrekt eingeben. Überprüfen Sie Ihre O2 Mail-Adresse und Ihr Passwort, um Tippfehler zu vermeiden

  • Sutra massage is dedicated to providing no.1 massage with best therapist and you will get to experience bliss. come and enjoy your life.

  • B2B massage in Chennai is the most sensational massage service offered by the massage therapist since the body massage provider often follows the demands of the client when conducting body massage.
    https://bodymassageinchennai.in/

  • Awesome Post

  • Disney Plus is a combination of films and series from Marvel, Star Wars, Pixar, and the National Geographic channel.

  • really amazing how do you think It gave me a new perspective.

  • I recommend seeking out licensed and certified massage therapists or spas in your area. They will have trained professionals flip body spa provide various massage techniques suited to your needs and preferences.

  • Halotherapy (Salt Room) Spa: Utilizes salt-infused rooms to promote respiratory health and relaxation.

  • Prestige Park Grove is the Best Townships in Bangalore city for current date close to the airport within 15 mins we can reach railway stations

  • Just desire to say your article is as astonishing. The clarity for your submit is simply excellent and i
    could assume you're knowledgeable in this subject.
    Well along with your permission allow me to seize
    your RSS feed to keep up to date with drawing close post. Thanks
    a million and please continue the rewarding work.


  • I've been struggling with my writing assignments lately, and finding quality help has been a challenge. It's reassuring to come across a company like British Assignment Help, which provides reliable UK assignment help. They understand the unique demands of British academic standards, and their assistance has been invaluable in enhancing my writing skills. It's refreshing to have a trusted partner in my academic journey. Writing just got a whole lot easier!

  • No matter how many times I want to read an article Your story will be one of my thoughts. that i must read

  • As I am looking at your writing, <a href="https://images.google.jo/url?sa=t&url=https%3A%2F%2Fwww.mtclean.blog/">safetoto</a> I regret being unable to do outdoor activities due to Corona 19, and I miss my old daily life. If you also miss the daily life of those days, would you please visit my site once? My site is a site where I post about photos and daily life when I was free.

  • Looking at this article, I miss the time when I didn't wear a mask. <a href="https://images.google.je/url?sa=t&url=https%3A%2F%2Fwww.mtclean.blog/">casino online</a> Hopefully this corona will end soon. My blog is a blog that mainly posts pictures of daily life before Corona and landscapes at that time. If you want to remember that time again, please visit us.

  • I have been looking for articles on these topics for a long time. <a href="https://images.google.it/url?sa=t&url=https%3A%2F%2Fwww.mtclean.blog/">totosite</a> I don't know how grateful you are for posting on this topic. Thank you for the numerous articles on this site, I will subscribe to those links in my bookmarks and visit them often. Have a nice day

  • I've been searching for hours on this topic and finally found your post. <a href="https://images.google.is/url?sa=t&url=https%3A%2F%2Fwww.mtclean.blog/">baccarat online</a>, I have read your post and I am very impressed. We prefer your opinion and will visit this site frequently to refer to your opinion. When would you like to visit my site?

  • JNANABHUMI AP provides all the latest educational updates and many more. The main concept or our aim behind this website has been the will to provide resources with full information on each topic which can be accessed through the Internet. https://jnanabhumiap.in/ To ensure that every reader gets what is important and worthy about the topic they search and link to hear from us.

  • want to see you your article is good i want to talk to you. I want to talk to you and tell you that I love the article you wrote.

  • Excellently written article, doubts all bloggers offered the identical content since you, the internet has to be far better place.

  • Your article is impressive. Thank you for putting these thoughts into writing.

  • great article,i would be happy if you also checked out my website
    <a href="https://radinphysio.com/">کلینیک فیزیوتراپی رادین</a>

  • great article,i would be happy if you also checked out my website
    <a href="https://drghasemnejad.com/">دندانپزشکی دکتر قاسم نژاد</a>


  • great article,we offer laminet,veneer and other dental services in our website.

    [url= https://drghasemnejad.com] دندانپزشکی دکتر قاسم نژاد[/url]

  • Looking to <a href="https://neejacs.com/difc-company-setup/"> DIFC Company SetUp </a>? Our expert consultants offer top-notch services for business setup consultants in Dubai, licensing, visas and more.

  • Concurrency conflicts can be detected by checking entities’ property values besides primary keys. To required EF/Core to check a certain property, just add a System.ComponentModel.DataAnnotations.ConcurrencyCheckAttribute to it. Remember when defining ProductPhoto entity, its ModifiedDate has a [ConcurrencyCheck] attribute:

  • Your article not only provided valuable information but also sparked introspection and reflection on my part.

  • I think this is a real great article post

  • Hello, I enjoy reading through your post. I wanted to write a little comment to support you.

  • Say, you got a nice blog.Really looking forward to read more. Awesome.

  • I just stumbled upon your blog and wanted to say that I really enjoy browsing your blog posts.

  • its actually awesome paragraph, I have got much clear idea concerning from this piece of writing.

  • Your posts are always well-timed and relevant.

  • Very efficiently written information. It will be beneficial to anybody who utilizes it, including me. Keep up the good work. For sure i will check out more posts. This site seems to get a good amount of visitors.

  • Excellent website you have here, so much cool information!..

  • I have read a few of the articles on your website now, and I really like your style.

  • Thanks for publishing such a unique and great article.


  • great article,we offer laminet,veneer and other dental services in our website.

Add a Comment

As it will appear on the website

Not displayed

Your website