Skip to content

Entity Framework Core Integration

  • Do define a separated DbContext interface 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 DbContext that inherits from IEfCoreDbContext.
  • Do add a ConnectionStringName attribute to the DbContext interface.
  • Do add DbSet<TEntity> properties to the DbContext interface for only aggregate roots.
  • Do not define set; for the properties in this interface.

Example:

[ConnectionStringName(ProjectPlanningDbProperties.ConnectionStringName)]
public interface IProjectPlanningDbContext : IEfCoreDbContext
{
    public DbSet<BaseModel> BaseModels { get; }

    public DbSet<Project> Projects { get; }
}

DbContext class

  • Do inherit the DbContext from the AbpDbContext<TDbContext> class.
  • Do add a ConnectionStringName attribute to the DbContext class.
  • Do implement the corresponding interface for the DbContext class. Example:
[ConnectionStringName(ProjectPlanningDbProperties.ConnectionStringName)]
public class ProjectPlanningDbContext : AbpDbContext<ProjectPlanningDbContext>, IProjectPlanningDbContext
{
    public DbSet<BaseModel> BaseModels { get; set; }

    public DbSet<Project> Projects { get; set; }

    public ProjectPlanningDbContext(DbContextOptions<ProjectPlanningDbContext> options)
        : base(options)
    {

    }

    // code omitted for brevity
}

Table Prefix and Schema

  • Do add static TablePrefix and Schema properties to the DbContext class in domain layer. Example:
public static class ProjectPlanningDbProperties
{
    public static string DbTablePrefix { get; set; } = "ProjectPlanning";

    public static string DbSchema { get; set; } = null;

    public const string ConnectionStringName = "ProjectPlanning";
}
  • Do always use a short TablePrefix value for a module to create unique table names in a shared database. Abp table prefix is reserved for ABP core modules.
  • Do set Schema to null as default.

Model Mapping

  • Do explicitly configure all entities by overriding the OnModelCreating method of the DbContext. Example:
protected override void OnModelCreating(ModelBuilder builder)
{
    base.OnModelCreating(builder);

    builder.ConfigureProjectPlanning();
}
  • Do not configure model directly in the OnModelCreating method. Instead, create an extension method for ModelBuilder. Use Configure*ModuleName* as the method name. Example:
public static class ProjectPlanningDbContextModelCreatingExtensions
{
    public static void ConfigureProjectPlanning(this ModelBuilder builder)
    {
        Check.NotNull(builder, nameof(builder));

        builder.Entity<Project>(b =>
        {
             b.ToTable(ProjectPlanningDbProperties.DbTablePrefix + "Projects", ProjectPlanningDbProperties.DbSchema);
             b.ConfigureByConvention();
             b.Property(x => x.TenantId).HasColumnName(nameof(Project.TenantId));
             b.Property(x => x.Name).HasMaxLength(ProjectConsts.MaxNameLength).HasColumnName(nameof(Project.Name)).IsRequired();
             b.Property(x => x.Description).HasMaxLength(ProjectConsts.MaxDescriptionLength).HasColumnName(nameof(Project.Description));
             b.Property(x => x.DefinitionId).HasColumnName(nameof(Project.DefinitionId)).IsRequired();
             b.Property(x => x.InstanceId).HasColumnName(nameof(Project.InstanceId));
             b.Property(x => x.Priority).HasColumnName(nameof(Project.Priority)).IsRequired();
             b.Property(x => x.CurrentState).HasColumnName(nameof(Project.CurrentState)).IsRequired();
             b.Property(x => x.Version).HasColumnName(nameof(Project.Version)).IsRequired();
             b.Property(x => x.IsClosed).HasColumnName(nameof(Project.IsClosed)).IsRequired();
             b.Property(x => x.StartDate).HasColumnName(nameof(Project.StartDate)).IsRequired();
             b.Property(x => x.EndDate).HasColumnName(nameof(Project.EndDate));
             b.Property(x => x.CommissionDate).HasColumnName(nameof(Project.CommissionDate));
             b.Property(x => x.InServiceDate).HasColumnName(nameof(Project.InServiceDate));
             // ValueObject : Edges
             b.OwnsMany(x => x.Edges, e =>
             {
                 e.ToTable(ProjectPlanningDbProperties.DbTablePrefix + "Edges", ProjectPlanningDbProperties.DbSchema);
                 e.Property<int>("Id");
                 e.HasKey("Id");
             });
             // Conversions
             b.Property(x => x.SuccessorIds).HasColumnName(nameof(Project.SuccessorIds)).HasConversion(
                      v => JsonConvert.SerializeObject(v),
                      v => JsonConvert.DeserializeObject<List<Guid>>(v),
                      new ValueComparer<IReadOnlyList<Guid>>(
                         (c1, c2) => c1.SequenceEqual(c2),
                         c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
                         c => c.ToList()));
             b.Property(x => x.PredecessorIds).HasColumnName(nameof(Project.PredecessorIds)).HasConversion(
                      v => JsonConvert.SerializeObject(v),
                      v => JsonConvert.DeserializeObject<List<Guid>>(v),
                      new ValueComparer<IReadOnlyList<Guid>>(
                         (c1, c2) => c1.SequenceEqual(c2),
                         c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
                         c => c.ToList()));
             // Relations
             b.HasOne<BaseModel>().WithMany().HasForeignKey(x => x.BaseModelId).IsRequired();
             b.HasOne<RequiredChange>().WithMany().HasForeignKey(x => x.RequiredChangesId);
             // Index
             b.HasIndex(x => new { x.Name });
            });        

            //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 EfCoreProjectRepository : EfCoreRepository<IProjectPlanningDbContext, Project, Guid>, IProjectRepository
{
    public EfCoreProjectRepository(IDbContextProvider<IProjectPlanningDbContext> dbContextProvider)
        : base(dbContextProvider)
    {

    }
}
  • Do use the DbContext interface as the generic parameter, not the class.
  • Do pass the cancellationToken to EF Core using the GetCancellationToken helper method. Example:
public virtual async Task<Project> FindByNameAsync(
    string name, 
    bool includeDetails = true,
    CancellationToken cancellationToken = default)
{
    return await (await GetDbSetAsync())
        .IncludeDetails(includeDetails)
        .FirstOrDefaultAsync(
            u => u.name == name,
            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 IncludeDetails extension method for the IQueryable<TEntity> for each aggregate root which has sub collections. Example:
public static IQueryable<Project> IncludeDetails(
    this IQueryable<Project> queryable,
    bool include = true)
{
    if (!include)
    {
        return queryable;
    }

    return queryable
        .Include(x => x.Changes)
        .Include(x => x.Requirements);
}
  • Do use the IncludeDetails extension method in the repository methods just like used in the example code above (see FindByNameAsync).

  • Do override WithDetails method of the repository for aggregates root which have sub collections. Example:

public override async Task<IQueryable<Project>> WithDetailsAsync()
{
    // Uses the extension method defined above
    return (await GetQueryableAsync()).IncludeDetails();
}
  • Do create a ApplyFilter for the query methods. Example:
protected virtual IQueryable<Project> ApplyFilter(
    IQueryable<Project> query,
    string filterText = null,
    string name = null,
    string description = null,
    Guid? definitionId = null,
    Guid? instanceId = null,
    Priority? priority = null,
    string currentState = null,
    int? versionMin = null,
    int? versionMax = null,
    bool? isClosed = null,
    DateTime? startDateMin = null,
    DateTime? startDateMax = null,
    DateTime? endDateMin = null,
    DateTime? endDateMax = null,
    DateTime? commissionDateMin = null,
    DateTime? commissionDateMax = null,
    DateTime? inServiceDateMin = null,
    DateTime? inServiceDateMax = null,
    Guid? ownerId = null,
    Guid? organizationUnitId = null,
    Guid? creatorId = null,
    DateTime? creationTimeMin = null,
    DateTime? creationTimeMax = null,
    Guid? lastModifierId = null,
    DateTime? lastModificationTimeMin = null,
    DateTime? lastModificationTimeMax = null)
{
    return query
        .WhereIf(!string.IsNullOrWhiteSpace(filterText), p => p.Name.Contains(filterText) || p.Description.Contains(filterText))
        .WhereIf(!string.IsNullOrWhiteSpace(name), p => p.Name.Contains(name))
        .WhereIf(!string.IsNullOrWhiteSpace(description), p => p.Description.Contains(description))
        .WhereIf(definitionId.HasValue, p => p.DefinitionId == definitionId)
        .WhereIf(instanceId.HasValue, p => p.InstanceId == instanceId)
        .WhereIf(priority.HasValue, p => p.Priority == priority)
        .WhereIf(!string.IsNullOrWhiteSpace(currentState), p => p.CurrentState.Contains(currentState))
        .WhereIf(versionMin.HasValue, p => p.Version >= versionMin.Value)
        .WhereIf(versionMax.HasValue, p => p.Version <= versionMax.Value)
        .WhereIf(isClosed.HasValue, p => p.IsClosed == isClosed)
        .WhereIf(startDateMin.HasValue, p => p.StartDate >= startDateMin.Value)
        .WhereIf(startDateMax.HasValue, p => p.StartDate <= startDateMax.Value)
        .WhereIf(endDateMin.HasValue, p => p.EndDate >= endDateMin.Value)
        .WhereIf(endDateMax.HasValue, p => p.EndDate <= endDateMax.Value)
        .WhereIf(commissionDateMin.HasValue, p => p.CommissionDate >= commissionDateMin.Value)
        .WhereIf(commissionDateMax.HasValue, p => p.CommissionDate <= commissionDateMax.Value)
        .WhereIf(inServiceDateMin.HasValue, p => p.InServiceDate >= inServiceDateMin.Value)
        .WhereIf(inServiceDateMax.HasValue, p => p.InServiceDate <= inServiceDateMax.Value)
        .WhereIf(ownerId.HasValue, p => p.OwnerId == ownerId)
        .WhereIf(organizationUnitId.HasValue, p => p.OrganizationUnitId == organizationUnitId)
        .WhereIf(creatorId.HasValue, p => p.CreatorId == creatorId)
        .WhereIf(creationTimeMin.HasValue, p => p.CreationTime >= creationTimeMin.Value)
        .WhereIf(creationTimeMax.HasValue, p => p.CreationTime <= creationTimeMax.Value)
        .WhereIf(lastModifierId.HasValue, p => p.LastModifierId == lastModifierId)
        .WhereIf(lastModificationTimeMin.HasValue, p => p.LastModificationTime >= lastModificationTimeMin.Value)
        .WhereIf(lastModificationTimeMax.HasValue, p => p.LastModificationTime <= lastModificationTimeMax.Value);
}
  • Do use the ApplyFilter method in the repository methods for paged results
public async Task<List<Project>> GetListAsync(
  string filterText = null,
  string name = null,
  string description = null,
  Guid? definitionId = null,
  Guid? instanceId = null,
  Priority? priority = null,
  string currentState = null,
  int? versionMin = null,
  int? versionMax = null,
  bool? isClosed = null,
  DateTime? startDateMin = null,
  DateTime? startDateMax = null,
  DateTime? endDateMin = null,
  DateTime? endDateMax = null,
  DateTime? commissionDateMin = null,
  DateTime? commissionDateMax = null,
  DateTime? inServiceDateMin = null,
  DateTime? inServiceDateMax = null,
  Guid? ownerId = null,
  Guid? organizationUnitId = null,
  Guid? creatorId = null,
  DateTime? creationTimeMin = null,
  DateTime? creationTimeMax = null,
  Guid? lastModifierId = null,
  DateTime? lastModificationTimeMin = null,
  DateTime? lastModificationTimeMax = null,
  string sorting = null,
  int maxResultCount = 100,
  int skipCount = 0,
  CancellationToken cancellationToken = default)
{
  var query = ApplyFilter(
      query: await GetQueryableAsync(),
      filterText: filterText,
      name: name,
      description: description,
      definitionId: definitionId,
      instanceId: instanceId,
      priority: priority,
      currentState: currentState,
      versionMin: versionMin,
      versionMax: versionMax,
      isClosed: isClosed,
      startDateMin: startDateMin,
      startDateMax: startDateMax,
      endDateMin: endDateMin,
      endDateMax: endDateMax,
      commissionDateMin: commissionDateMin,
      commissionDateMax: commissionDateMax,
      inServiceDateMin: inServiceDateMin,
      inServiceDateMax: inServiceDateMax,
      ownerId: ownerId,
      organizationUnitId: organizationUnitId,
      creatorId: creatorId,
      creationTimeMin: creationTimeMin,
      creationTimeMax: creationTimeMax,
      lastModifierId: lastModifierId,
      lastModificationTimeMin: lastModificationTimeMin,
      lastModificationTimeMax: lastModificationTimeMax
  );

  return await query
      .OrderBy(string.IsNullOrWhiteSpace(sorting) ? ProjectConsts.GetDefaultSorting(false) : sorting)
      .PageBy(skipCount, maxResultCount)
      .ToListAsync(cancellationToken);
}
  • Do create a GetCount method for total filter result. Example:
public async Task<long> GetCountAsync(
    string filterText = null,
    string name = null,
    string description = null,
    Guid? definitionId = null,
    Guid? instanceId = null,
    Priority? priority = null,
    string currentState = null,
    int? versionMin = null,
    int? versionMax = null,
    bool? isClosed = null,
    DateTime? startDateMin = null,
    DateTime? startDateMax = null,
    DateTime? endDateMin = null,
    DateTime? endDateMax = null,
    DateTime? commissionDateMin = null,
    DateTime? commissionDateMax = null,
    DateTime? inServiceDateMin = null,
    DateTime? inServiceDateMax = null,
    Guid? ownerId = null,
    Guid? organizationUnitId = null,
    Guid? creatorId = null,
    DateTime? creationTimeMin = null,
    DateTime? creationTimeMax = null,
    Guid? lastModifierId = null,
    DateTime? lastModificationTimeMin = null,
    DateTime? lastModificationTimeMax = null,
    CancellationToken cancellationToken = default)
{
    var query = ApplyFilter(
        query: await GetQueryableAsync(),
        filterText: filterText,
        name: name,
        description: description,
        definitionId: definitionId,
        instanceId: instanceId,
        priority: priority,
        currentState: currentState,
        versionMin: versionMin,
        versionMax: versionMax,
        isClosed: isClosed,
        startDateMin: startDateMin,
        startDateMax: startDateMax,
        endDateMin: endDateMin,
        endDateMax: endDateMax,
        commissionDateMin: commissionDateMin,
        commissionDateMax: commissionDateMax,
        inServiceDateMin: inServiceDateMin,
        inServiceDateMax: inServiceDateMax,
        ownerId: ownerId,
        organizationUnitId: organizationUnitId,
        creatorId: creatorId,
        creationTimeMin: creationTimeMin,
        creationTimeMax: creationTimeMax,
        lastModifierId: lastModifierId,
        lastModificationTimeMin: lastModificationTimeMin,
        lastModificationTimeMax: lastModificationTimeMax
    );

    return await query.LongCountAsync(GetCancellationToken(cancellationToken));
}

Do define GetWithNavigation repository methods if there is relation between two or more aggregate

The related **Composite Class*:

public class ProjectWithNavigationProperties
{
    public Project Project { get; set; }

    // Navigations
    public BaseModel BaseModel { get; set; }
}

Implementation:

public virtual async Task<ProjectWithNavigationProperties> GetWithNavigationPropertiesAsync(
    Guid id,
    CancellationToken cancellationToken = default)
{
    var dbContext = await GetDbContextAsync();

    return (await GetDbSetAsync()).Where(x => x.Id == id)
        .Select(project => new ProjectWithNavigationProperties
        {
            Project = project,
            BaseModel = dbContext.BaseModels.FirstOrDefault(b => b.Id == project.BaseModelId),
        }).FirstOrDefault();
}

Follow same approach if there is listing required. Define GetListWithNavigation repository methods, create ApplyFilter as shown above. In this case we have to customize GetQueryableAsync() method.

  • Do define a module GetQueryForNavigationPropertiesAsync() method in the repository
protected virtual async Task<IQueryable<ProjectWithNavigationProperties>> GetQueryForNavigationPropertiesAsync()
{
  var dbContext = await GetDbContextAsync();

  return from project in await GetDbSetAsync()
         join baseModel in dbContext.BaseModels on project.BaseModelId equals baseModel.Id into baseModels
         from baseModel in baseModels.DefaultIfEmpty()
         join requiredChange in dbContext.RequiredChanges on project.RequiredChangesId equals requiredChange.Id into requiredChanges

         select new ProjectWithNavigationProperties
         {
             Project = project,
             BaseModel = baseModel,
         };
}

Module Class

  • Do define a module class for the Entity Framework Core integration package.
  • Do add DbContext to the IServiceCollection using the AddAbpDbContext<TDbContext> method.
  • Do add implemented repositories to the options for the AddAbpDbContext<TDbContext> method. Example:
[DependsOn(
    typeof(ProjectPlanningDomainModule),
    typeof(PSSXOrganizationUnitsEntityFrameworkCoreModule),
    typeof(PSSXUsersEntityFrameworkCoreModule),
    typeof(AbpEntityFrameworkCoreModule)
)]
public class ProjectPlanningEntityFrameworkCoreModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        context.Services.AddAbpDbContext<ProjectPlanningDbContext>(options =>
        {
            // Business related
            options.AddRepository<BaseModel, EfCoreBaseModelRepository>();
            options.AddRepository<Project, EfCoreProjectRepository>();
            // User related
            options.AddRepository<ProjectPlanningUser, EfCoreProjectPlanningUserRepository>();
            options.AddRepository<ProjectPlanningOrganizationUnit, EfCoreProjectPlanningOrganizationUnitRepository>();
        });
    }