One of the things I find confusing about developing Entity Framework Core libraries is that using fluent-style configuration quickly leads to a gigantic override of the DbContext.OnModelCreating() method (I prefer fluent-style configuration because it makes me think about exactly what I’m trying to do, and gives me feedback right away on many common mistakes).
Today I was hit by a little brainstorm which may be of use to others. Fair warning, this hasn’t been tested much; think of it as a working conceptual concept.
The idea is to flag POCO classes that are database entities with an attribute which ties them to a configuration class, whose one method, Configure(), gets called via an extension method which scans all the entity classes in whatever assembly defines them.
Here’s the attribute used to mark the entity classes:
[ AttributeUsage( AttributeTargets.Class, AllowMultiple = false, Inherited = false ) ]
public class EntityConfigurationAttribute : Attribute
{
private readonly Type _configType;
public EntityConfigurationAttribute( Type configType )
{
_configType = configType ?? throw new NullReferenceException( nameof(configType) );
if( !typeof(IEntityConfiguration).IsAssignableFrom( configType ) )
throw new ArgumentException(
$"Database entity configuration type is not {nameof(IEntityConfiguration)}" );
}
public IEntityConfiguration GetConfigurator() =>
(IEntityConfiguration) Activator.CreateInstance( _configType );
}
The interface definitions are pretty simple:
public interface IEntityConfiguration
{
void Configure( ModelBuilder builder );
}
public interface IEntityConfiguration<TEntity> : IEntityConfiguration
where TEntity : class
{
}
The marked lines show the interface that a configurator class needs to implement. It’s just a marker interface (i.e., doesn’t specify any required methods).
The abstract class which forms the basis for configurators is also pretty simple:
public abstract class EntityConfigurator<TEntity> : IEntityConfiguration<TEntity>
where TEntity: class
{
protected abstract void Configure( EntityTypeBuilder<TEntity> builder );
void IEntityConfiguration.Configure( ModelBuilder builder )
{
Configure( builder.Entity<TEntity>() );
}
}
The highlighted line just is meant to show that what gets passed to the protected abstract method you have to implement isn’t a ModelBuilder, but a type-specific EntityTypeBuilder<>. That helps protect you from configuring the wrong entity in your configurator class.
Here’s an example of a POCO class with its corresponding configurator class:
[EntityConfiguration(typeof(AssemblyDbConfigurator))]
public class AssemblyDb
{
public int ID { get; set; }
public string Name { get; set; }
public Version Version { get; set; }
public string RootNamespace { get; set; }
public string Authors { get; set; }
public string Company { get; set; }
public string Description { get; set; }
public string Copyright { get; set; }
public int ActiveFrameworkID { get; set; }
public Framework ActiveFramework { get; set; }
public List<AssemblyFramework> TargetFrameworks { get; set; }
public List<AssemblyNamespace> AssemblyNamespaces { get; set; }
public List<NamedType> NamedTypes { get; set; }
}
internal class AssemblyDbConfigurator : EntityConfigurator<AssemblyDb>
{
protected override void Configure( EntityTypeBuilder<AssemblyDb> builder )
{
builder.HasOne<Framework>(x => x.ActiveFramework);
builder.Property(x => x.Version)
.HasConversion(
x => x.ToString(),
y => Version.Parse(y)
);
builder.HasMany(x => x.TargetFrameworks)
.WithOne(x => x.AssemblyDb);
builder.HasMany(x => x.AssemblyNamespaces)
.WithOne(x => x.AssemblyDb);
}
}
I make my configurator classes internal because I define them in the same assembly where I define my POCO entity classes.
The glue that ties this all together is an extension method:
public static class EntityConfigurationExtensions
{
public static void ConfigureEntities( this ModelBuilder modelBuilder, Assembly assemblyToScan )
{
if( modelBuilder == null || assemblyToScan == null )
return;
// scan current assembly for types decorated with EntityConfigurationAttribute
foreach( var entityType in assemblyToScan.DefinedTypes
.Where( t => ((MemberInfo) t).GetCustomAttribute<EntityConfigurationAttribute>() != null ) )
{
entityType.GetCustomAttribute<EntityConfigurationAttribute>()
.GetConfigurator()
.Configure( modelBuilder );
}
}
}
The extension method is called like this:
protected override void OnModelCreating( ModelBuilder modelBuilder )
{
base.OnModelCreating( modelBuilder );
modelBuilder.ConfigureEntities(this.GetType().Assembly);
}
And that’s it. If you have your POCO classes defined in a different assembly you’d just pass in a reference to the correct assembly.