Entity Framework Core Integration Best Practices
See Entity Framework Core Integration document for the basics of the EF Core integration.
- Do define a separated
DbContextinterface and class for each module. - Do not rely on lazy loading on the application development.
- Do not enable lazy loading for the
DbContext.
DbContext Interface
- Do define an interface for the
DbContextthat inherits fromIEfCoreDbContext. - Do add a
ConnectionStringNameattribute to theDbContextinterface. - Do add
DbSet<TEntity>properties to theDbContextinterface for only aggregate roots. Example:
[ConnectionStringName("AbpIdentity")]
public interface IIdentityDbContext : IEfCoreDbContext
{
DbSet<IdentityUser> Users { get; }
DbSet<IdentityRole> Roles { get; }
}
- Do not define
set;for the properties in this interface.
DbContext class
- Do inherit the
DbContextfrom theAbpDbContext<TDbContext>class. - Do add a
ConnectionStringNameattribute to theDbContextclass. - Do implement the corresponding
interfacefor theDbContextclass. Example:
[ConnectionStringName("AbpIdentity")]
public class IdentityDbContext : AbpDbContext<IdentityDbContext>, IIdentityDbContext
{
public DbSet<IdentityUser> Users { get; set; }
public DbSet<IdentityRole> Roles { get; set; }
public IdentityDbContext(DbContextOptions<IdentityDbContext> options)
: base(options)
{
}
//code omitted for brevity
}
Table Prefix and Schema
- Do add static
TablePrefixandSchemaproperties to theDbContextclass. Set default value from a constant. Example:
public static string TablePrefix { get; set; } = AbpIdentityConsts.DefaultDbTablePrefix;
public static string Schema { get; set; } = AbpIdentityConsts.DefaultDbSchema;
- Do always use a short
TablePrefixvalue for a module to create unique table names in a shared database.Abptable prefix is reserved for ABP core modules. - Do set
Schematonullas default.
Model Mapping
- Do explicitly configure all entities by overriding the
OnModelCreatingmethod of theDbContext. Example:
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.ConfigureIdentity();
}
- Do not configure model directly in the
OnModelCreatingmethod. Instead, create an extension method forModelBuilder. Use ConfigureModuleName as the method name. Example:
public static class IdentityDbContextModelBuilderExtensions
{
public static void ConfigureIdentity([NotNull] this ModelBuilder builder)
{
Check.NotNull(builder, nameof(builder));
builder.Entity<IdentityUser>(b =>
{
b.ToTable(AbpIdentityDbProperties.DbTablePrefix + "Users", AbpIdentityDbProperties.DbSchema);
b.ConfigureByConvention();
//code omitted for brevity
});
builder.Entity<IdentityUserClaim>(b =>
{
b.ToTable(AbpIdentityDbProperties.DbTablePrefix + "UserClaims", AbpIdentityDbProperties.DbSchema);
b.ConfigureByConvention();
//code omitted for brevity
});
//code omitted for brevity
}
}
- Do call
b.ConfigureByConvention();for each entity mapping (as shown above).
Repository Implementation
- Do inherit the repository from the
EfCoreRepository<TDbContext, TEntity, TKey>class and implement the corresponding repository interface. Example:
public class EfCoreIdentityUserRepository
: EfCoreRepository<IIdentityDbContext, IdentityUser, Guid>, IIdentityUserRepository
{
public EfCoreIdentityUserRepository(
IDbContextProvider<IIdentityDbContext> dbContextProvider)
: base(dbContextProvider)
{
}
}
- Do use the
DbContextinterface as the generic parameter, not the class. - Do pass the
cancellationTokento EF Core using theGetCancellationTokenhelper method. Example:
public virtual async Task<IdentityUser> FindByNormalizedUserNameAsync(
string normalizedUserName,
bool includeDetails = true,
CancellationToken cancellationToken = default)
{
return await (await GetDbSetAsync())
.IncludeDetails(includeDetails)
.FirstOrDefaultAsync(
u => u.NormalizedUserName == normalizedUserName,
GetCancellationToken(cancellationToken)
);
}
GetCancellationToken fallbacks to the ICancellationTokenProvider.Token to obtain the cancellation token if it is not provided by the caller code.
- Do create a
IncludeDetailsextension method for theIQueryable<TEntity>for each aggregate root which has sub collections. Example:
public static IQueryable<IdentityUser> IncludeDetails(
this IQueryable<IdentityUser> queryable,
bool include = true)
{
if (!include)
{
return queryable;
}
return queryable
.Include(x => x.Roles)
.Include(x => x.Logins)
.Include(x => x.Claims)
.Include(x => x.Tokens);
}
- Do use the
IncludeDetailsextension method in the repository methods just like used in the example code above (seeFindByNormalizedUserNameAsync).
- Do override
WithDetailsmethod of the repository for aggregates root which have sub collections. Example:
public override async Task<IQueryable<IdentityUser>> WithDetailsAsync()
{
// Uses the extension method defined above
return (await GetQueryableAsync()).IncludeDetails();
}
Module Class
- Do define a module class for the Entity Framework Core integration package.
- Do add
DbContextto theIServiceCollectionusing theAddAbpDbContext<TDbContext>method. - Do add implemented repositories to the options for the
AddAbpDbContext<TDbContext>method. Example:
[DependsOn(
typeof(AbpIdentityDomainModule),
typeof(AbpEntityFrameworkCoreModule)
)]
public class AbpIdentityEntityFrameworkCoreModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAbpDbContext<IdentityDbContext>(options =>
{
options.AddRepository<IdentityUser, EfCoreIdentityUserRepository>();
options.AddRepository<IdentityRole, EfCoreIdentityRoleRepository>();
});
}
}