Search code examples
c#entity-frameworkentity-framework-coreoptimistic-concurrencyrowversion

Configure IsConcurrencyToken(value) with annotations in EF Core


I want to be able to use an attribute on a Row Version property of a model to enable or disable Optimistic Concurrency in the OnModelCreating(ModelBuilder modelBuilder) method of my DbContext in EF Core 7.0.0. I was able to make it work in .NET Framework 4.8 but I need to do it in an EF Core project as well.

I have an interface for entities which have a Version property called IVersionedEntity and I want to use a custom attribute where I can define if I want to enable or disable optimistic concurrency.

The attribute:

[AttributeUsage(AttributeTargets.Property, Inherited = true)]
public class LockAttribute : Attribute
{
    public bool IsEnabled { get; private set; }

    public LockAttribute(bool isEnabled = true)
    {
        IsEnabled = isEnabled;
    }
}

And I know I can achieve the enable-disable behavior inside the OnModelCreating(DbModelBuilder modelBuilder) method with this extension method in ASP.NET (.NET Framework):

public static void ConfigureLocking(this DbModelBuilder modelBuilder)
{
    modelBuilder.Types<IVersionedEntity>().Configure(configuration =>
    {
        var versionPropertyInfo = configuration.ClrType.GetProperty("Version");
        var lockAttribute = versionPropertyInfo.GetCustomAttribute<LockAttribute>(true);
        if (lockAttribute != null)
        {
            configuration.Property(entity => entity.Version).IsRowVersion().IsConcurrencyToken(lockAttribute.IsEnabled);
        }
    });
}

My problem is, that the ModelBuilder class inside EF Core is quite different and I do not know how to configure something like this using that class.


Solution

  • In EF Core 7.0+ it can be done relatively easy with a custom Model building convention. In fact all predefined EF Core data annotations are handled with convention classes similar to this:

    public class LockAttributeConvention : PropertyAttributeConventionBase<LockAttribute>
    {
        public LockAttributeConvention(ProviderConventionSetBuilderDependencies dependencies) : base(dependencies) { }
    
        protected override void ProcessPropertyAdded(IConventionPropertyBuilder propertyBuilder, LockAttribute attribute, MemberInfo clrMember, IConventionContext context)
        {
            if (attribute.IsEnabled)
            {
                propertyBuilder.ValueGenerated(ValueGenerated.OnAddOrUpdate, fromDataAnnotation: true);
                propertyBuilder.IsConcurrencyToken(true, fromDataAnnotation: true);
            }
        }
    }
    

    Helper method for registering it:

    
    public static ModelConfigurationBuilder AddLockAttributeSupport(this ModelConfigurationBuilder configurationBuilder)
    {
        configurationBuilder.Conventions.Add(sp => new LockAttributeConvention(
            sp.GetRequiredService<ProviderConventionSetBuilderDependencies>()));
        return configurationBuilder;
    }
    

    and finally call it from ConfigureConventions override:

    configurationBuilder.AddLockAttributeSupport();