Implementing Missing Features in Entity Framework Core – Part 4: Conventions

Conventions

This will be the fourth in a series of posts about bringing the features that were present in Entity Framework pre-Core into EF Core. The others are:

  • Part 1: Introduction, Find, Getting an Entity’s Id Programmatically, Reload, Local, Evict
  • Part 2: Explicit Loading
  • Part 3: Validations


Conventions are a mechanism by which we do not have to configure specific aspects of our mappings over and over again. We just accept how they will be configured, of course, we can override them if we need, but in most cases, we’re safe.

In EF 6.x we had a number of built-in conventions (which we could remove) but we also had the ability to add our own. In Entity Framework Core 1, this capability hasn’t been implemented, or, rather, it is there, but hidden under the surface.

A lot has changed. As of Entity Framework Core, conventions are specific to the provider in use, which makes perfect sense. But then we have several kinds of conventions. Just to give you an idea, the ConventionSet class exposes 15 convention collections! This is the place where we can register our own conventions, but there are two problems:

  • The current ConventionSet instance is not publicly accessible, so we need to use reflection to get hold of it;
  • We don’t want to add a new “blank” instance of ConventionSet because this way we would lose all of the conventions that would have been injected by the provider.


So, the solution I came up with had to use reflection to get the current convention set and allow adding new conventions, which is not ideal. Let’s see the code.

We need an extension method to get hold of the ConventionSet from the ModelBuilder and allow adding a convention, in the form of a IModelConvention:

public static class ModelBuilderExtensions
{
    public static ModelBuilder AddConvention(this ModelBuilder modelBuilder, IModelConvention convention)
    {
        var imb = modelBuilder.GetInfrastructure();
        var cd = imb.Metadata.ConventionDispatcher;
        var cs = cd.GetType().GetField("_conventionSet", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(cd) as ConventionSet;
 
        cs.ModelBuiltConventions.Add(convention);
 
        return modelBuilder;
    }
 
    public static ModelBuilder AddConvention<TConvention>(this ModelBuilder modelBuilder) where TConvention : IModelConvention, new()
    {
        return modelBuilder.AddConvention(new TConvention());
    }
}

Feel free to cache the _conventionSet field somewhere, but that is not the point of this post.

You can see that we are adding the new convention to the ModelBuiltConventions collection, basically, because this is the only collection that Entity Framework Core will go through during the OnModelCreating method, all of the other collections will have been processed before it. We’re cool with that.

Let’s see two examples of sample conventions, first, one to set the maximum length of strings, where it hasn’t been explicitly set:

public sealed class DefaultStringLengthConvention : IModelConvention
{
    internal const int DefaultStringLength = 50;
    internal const string MaxLengthAnnotation = "MaxLength";
 
    private readonly int _defaultStringLength;
 
    public DefaultStringLengthConvention(int defaultStringLength = DefaultStringLength)
    {
        this._defaultStringLength = defaultStringLength;
    }
 
    public InternalModelBuilder Apply(InternalModelBuilder modelBuilder)
    {
        foreach (var entity in modelBuilder.Metadata.GetEntityTypes())
        {
            foreach (var property in entity.GetProperties())
            {
                if (property.ClrType == typeof(string))
                {
                    if (property.FindAnnotation(MaxLengthAnnotation) == null)
                    {
                        property.AddAnnotation(MaxLengthAnnotation, this._defaultStringLength);
                    }
                }
            }
        }
 
        return modelBuilder;
    }
}

Easy, easy, hey? We go through all of the mapped entities, then through all of their properties of type string, check if the maximum length annotation is present, and, if not, add a new one. Only one method, with access to all mapped entities.

Another example, turning off the table pluralization. As you know, as of EF RC2, table names of DbSet properties exposed in the DbContext are pluralized. We can get rid of this behavior if we explicitly set the name to something, in this case, it will be the entity type name:

public sealed class SingularizeTableNameConvention : IModelConvention
{
    public InternalModelBuilder Apply(InternalModelBuilder modelBuilder)
    {
        foreach (var entity in modelBuilder.Metadata.GetEntityTypes())
        {
            if (entity.FindAnnotation(RelationalAnnotationNames.TableName) == null)
            {
                entity.Relational().TableName = entity.Name.Split('.').Last();
            }
        }
 
        return modelBuilder;
    }
}

Now, some new extension methods come handy:

public static ModelBuilder UseDefaultStringLength(this ModelBuilder modelBuilder, int defaultStringLength = DefaultStringLengthConvention.DefaultStringLength)
{
    modelBuilder.AddConvention(new DefaultStringLengthConvention(defaultStringLength));
 
    return modelBuilder;
}
 
public static ModelBuilder UseSingularTableNames(this ModelBuilder modelBuilder)
{
    modelBuilder.AddConvention<SingularizeTableNameConvention>();
 
    return modelBuilder;
}

And that’s it! The way to apply these conventions (one or both) is during OnModelCreating:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .UseDefaultStringLength()
        .UseSingularTableNames();
 
    base.OnModelCreating(modelBuilder);
}

You can now reuse these conventions in all of your context classes very easily. Hope you enjoy it! Two final remarks about this solution:

  • The reflection bit is a problem, because things may change in the future. Hopefully Microsoft will give us a workaround;
  • There is no easy way to remove built-in (or provider-injected) conventions, because are applied before OnModelCreating, but I think this is not a big problem, since we can change them afterwards, as we’ve seen.


Stay tuned for more!

                             

6 Comments

Add a Comment

As it will appear on the website

Not displayed

Your website