A while back, I started looking at ways to port EF6 code, that uses a lot of the API hooks and such, to Entity Framework Core. Over the past week, I managed to port a large scale application that was using EF6 to utilize EF Core. Here are some observations/tips in continuing with my Dotnet Core porting.
Mapping is a different in EF Core. The first, and most painful, thing I found that was removed is EntityTypeConfiguration<TEntity>. This is a very helpful class that you can wrap any TEntity’s mapping within. When all of your mappings are defined within this object type, EF 6.x provided a convenience method to load all of those configurations at build time via an “AddFromAssembly” method. Well, ALLLLL of that is gone in EF Core. It’s not too terribly difficult to create your own convenience methods to do this, though.
For example, here’s an implementation of EntityTypeConfiguration.
using Csoki.Core.Utilities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; namespace aspnet_core_web.Data { internal static class ModelBuilderExtensions { private static List<Type> GetEntityConfigurations(this Assembly assembly, string filterNamespace = "") { var types = assembly .GetTypes() .Where(x => { var ti = x.GetTypeInfo(); return !ti.IsAbstract && ti.GetInterfaces().Any(y => y.GetTypeInfo().IsGenericType && y.GetGenericTypeDefinition() == typeof(IDbEntityConfiguration<>)) && (string.IsNullOrWhiteSpace(filterNamespace) || x.Namespace.ToLowerTrim() == filterNamespace.ToLowerTrim()); }) .ToList(); return types; } public static void AddConfiguration<TEntity>( this ModelBuilder modelBuilder, DbEntityConfiguration<TEntity> dbEntityConfiguration) where TEntity : class { modelBuilder.Entity<TEntity>(dbEntityConfiguration.Configure); } /// <summary> /// Specify a type in an assembly that has DbConfigurations you want to add /// </summary> /// <typeparam name="TType"></typeparam> /// <param name="modelBuilder"></param> public static void AddAssemblyConfiguration<TType>(this ModelBuilder modelBuilder, string filterNamespace = "") where TType : class { var assembly = typeof(TType).GetTypeInfo().Assembly; var entityConfigs = assembly.GetEntityConfigurations(filterNamespace); foreach (var entityConfig in entityConfigs.Select(Activator.CreateInstance).Cast<IDbEntityConfiguration>()) { entityConfig.Configure(modelBuilder); } } /// <summary> /// Specify a type in an assembly that has DbConfigurations you want to add /// </summary> /// <typeparam name="TType"></typeparam> /// <param name="modelBuilder"></param> public static void AddAssemblyConfiguration(this ModelBuilder modelBuilder, Assembly assembly, string filterNamespace = "") { var entityConfigs = assembly.GetEntityConfigurations(filterNamespace); foreach (var entityConfig in entityConfigs.Select(Activator.CreateInstance).Cast<IDbEntityConfiguration>()) { entityConfig.Configure(modelBuilder); } } } public interface IDbEntityConfiguration { void Configure(ModelBuilder modelBuilder); } public interface IDbEntityConfiguration<TEntity> : IDbEntityConfiguration where TEntity : class { void Configure(EntityTypeBuilder<TEntity> modelBuilder); } internal abstract class DbEntityConfiguration<TEntity> : IDbEntityConfiguration<TEntity> where TEntity : class { public abstract void Configure(EntityTypeBuilder<TEntity> entity); public void Configure(ModelBuilder modelBuilder) { Configure(modelBuilder.Entity<TEntity>()); } } }
A typical usage would then look like this:
internal class AppLogModelConfiguration : DbEntityConfiguration<AppLogModel> { public override void Configure(EntityTypeBuilder<AppLogModel> entity) { entity.ToTable("Log4Net", "ApplicationLogging"); entity.HasKey(x => x.Id); entity.Property(p => p.Id).HasColumnName("Id").ValueGeneratedOnAdd(); } }
And in the DbContext modelBuilder, the extension method can be used:
public class AppLogContext : BaseContext { public AppLogContext(DbContextOptions<AppLogContext> options, ICurrentUserService currentUserService) : base(options, currentUserService) { } public DbSet<AppLogModel> AppLogModels { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { var assembly = typeof(AppLogContext).GetTypeInfo().Assembly; modelBuilder.AddAssemblyConfiguration<AppLogContext>(typeof(AppLogModelConfiguration).Namespace); } }
You’ll notice that I’m passing the namespace that has the configuration objects. This is only if you have multiple contexts within the same assembly.
If you recognize the context above, you may realize it’s from a previous post in which I showed how to implement an ILogger using the Log4Net schema and EF Core. An interesting side note is that one must be careful when define the DbContext scope for a DbContext that is used for an ILogger. By default, EF Core will attempt to log the context’s creation. This creates an infinite loop. Also, by default, DbContext’s are scoped to the request, but need to be transisent. Within the ConfigureServices startup method, these options can be controlled directly.
// Logging context must be transient.. services.AddDbContext<AppLogContext>(options => { options.UseSqlServer(Configuration.GetConnectionString(loggingConnStr)); options.UseLoggerFactory(null); }, ServiceLifetime.Transient);
There are some other interesting tidbits that I’ll discuss in a future blog post.