ABP 基础设施层 - 集成 Entity Framework Core

9.3.1 简介

Abp.EntityFrameworkCore nuget package 被用来集成到EF Core ORM框架. 在安装这个包以后,我们应该在模块类 AbpEntityFrameworkCoreModule 上添加 DependsOn 特性。

9.3.2 DbContext

EF Core要求有个派生自DbContext的类。在ABP中,我们应该使其派生自 AbpDbContext,如下所示:

public class MyDbContext : AbpDbContext
{
    public DbSet<Product> Products { get; set; }

    public MyDbContext(DbContextOptions<MyDbContext> options)
        : base(options)
    {
    }
}

如上所示,构造函数应该取得 DbContextOptions<T>。参数的名字必须是 options。由于ABP提供了匿名对象参数,所以更改它是不可能的。

9.3.3 Configuration

In Startup Class

我们可以在 ConfigureServices 方法中,使用 AddApbDbContext,如下所示:

public IServiceProvider ConfigureServices(IServiceCollection services)
{
    ...
    
    services.AddDbContext<MyDbContext>(options =>
    {
        options.UseSqlServer(Configuration.GetConnectionString("Default"));
    });

    ...
}

对于非web项目,Startup类是不存在的。在这种情况下,我们可以在模块类中使用 Configuration.Modules.AbpEfCore().AddDbContext 来配置DbContext。如下所示:

Configuration.Modules.AbpEfCore().AddDbContext<MyDbContext>(options =>
{
    options.DbContextOptions.UseSqlServer(options.ConnectionString);
});

我们使用给定的连接字符串并使用Sql Server作为数据库提供器。通常 options.ConnectionString 的值就是 default连接字符串。但是ABP使用 IConnectionStringResolver 来确定。所以,这个行为方式是可以改变的并且连接字符串可以动态的切换。每当DbContext被实例化的时候,这个动作会传递给 AddAbpDbContext 方法。所以,你有机会可以返还不同条件下的连接字符串。

那么,哪里可以设置默认的连接字符串呢?

In Module PreInitialize

你可以在模块的 PreInitialize 方法中来设置它,如下所示:

public class MyEfCoreAppModule : AbpModule
{
    public override void PreInitialize()
    {
        Configuration.DefaultNameOrConnectionString = GetConnectionString("Default");
        ...
    }
}

所以,你可以定义GetConnectionString方法从配置文件中取得连接字符串(通常是从appsettings.json)。

9.3.4 仓储

仓储被用来从更高层抽象数据访问的。详细请看仓储文档

默认仓储

Abp.EntityFrameworkCore 为所有在DbContext中已定义的实体默认实现了仓储。你没必要创建仓储类来使用预定义的仓储方法。例如:

public class PersonAppService : IPersonAppService
{
    private readonly IRepository<Person> _personRepository;

    public PersonAppService(IRepository<Person> personRepository)
    {
        _personRepository = personRepository;
    }

    public void CreatePerson(CreatePersonInput input)
    {        
        person = new Person { Name = input.Name, EmailAddress = input.EmailAddress };

        _personRepository.Insert(person);
    }
}

IRepository<Person>PersonAppService 的构造函数中被注入,并且使用了 Insert 方法。 使用这种方式,你可以很容易的注入 IRepository<TEntity>或者IRepository<TEntity, TPrimaryKey> 并且使用其中预定义的方法。查看所有的预定义方法请查询仓储文档

自定义仓储

如果你对标准的仓储方法不满意,你可以对你的实体创建自定义仓储。

特定于应用的基础仓储类

ABP提供了一个基础类:EfCoreRepositoryBase 使用它你可以很容易实现仓储类。为了实现 IRepository 接口,你可以使你的仓储类派生自该类。但是最好的方式是创建你自己的基类来扩展 EfRepositoryBase 类。因此,你可以轻松的添加共享/通用方法给你的仓储类。下面是SimpleTaskSystem应用对所有仓储的基类实现示例:

//应用程序中的所有仓储的基类
public class SimpleTaskSystemRepositoryBase<TEntity, TPrimaryKey> : EfCoreRepositoryBase<SimpleTaskSystemDbContext, TEntity, TPrimaryKey>
    where TEntity : class, IEntity<TPrimaryKey>
{
    public SimpleTaskSystemRepositoryBase(IDbContextProvider<SimpleTaskSystemDbContext> dbContextProvider)
        : base(dbContextProvider)
    {
    }

    //为所有仓储添加通用方法
}


//对于那些有int类型Id的实体的仓储的快速实现方式
public class SimpleTaskSystemRepositoryBase<TEntity> : SimpleTaskSystemRepositoryBase<TEntity, int>
    where TEntity : class, IEntity<int>
{
    public SimpleTaskSystemRepositoryBase(IDbContextProvider<SimpleTaskSystemDbContext> dbContextProvider)
        : base(dbContextProvider)
    {
    }
    
    //别在这里添加任何方法,请将方法添加到上面那个类,因为该类被上面类继承
}

注意:我们的仓储类都是继承自 EfCoreRepositoryBase<SimpleTaskSystemDbContext, TEntity, TPrimaryKey>。这说明ABP在仓储中使用了 SimpleTaskSystemDbContext

你指定的 DbContext 默认都是使用 EfCoreRepositoryBase 来实现仓储功能的,例如:上面示例 SimpleTaskSystemDbContext。你可以用你自己的仓储基类来替换默认的仓储基类 EfRepositoryBase。这需要你将 AutoRepositoryTypes 特性添加到你的 DbContext 上,如下所示:

[AutoRepositoryTypes(
    typeof(IRepository<>),
    typeof(IRepository<,>),
    typeof(SimpleTaskSystemEfRepositoryBase<>),
    typeof(SimpleTaskSystemEfRepositoryBase<,>)
)]
public class SimpleTaskSystemDbContext : AbpDbContext
{
    ...
}
自定义仓储示例

为了实现一个自定义的仓储,你应该使你创建的应用的仓储派生自上面所说的特定于应用的基础仓储类。

假设我们有一个任务实体且任务能够被分配给某个人并且任务有自己的状态(如:new,assigned,completed 等等)。我们可能需要写一个自定义的方法来取得任务列表并且需要使用某些条件来执行数据过滤,如:分配人,任务状态 来过滤。 如下所示:

public interface ITaskRepository : IRepository<Task, long>
{
    List<Task> GetAllWithPeople(int? assignedPersonId, TaskState? state);
}

public class TaskRepository : SimpleTaskSystemRepositoryBase<Task, long>, ITaskRepository
{
    public TaskRepository(IDbContextProvider<SimpleTaskSystemDbContext> dbContextProvider)
        : base(dbContextProvider)
    {
    }

    public List<Task> GetAllWithPeople(int? assignedPersonId, TaskState? state)
    {
        var query = GetAll();

        if (assignedPersonId.HasValue)
        {
            query = query.Where(task => task.AssignedPerson.Id == assignedPersonId.Value);
        }

        if (state.HasValue)
        {
            query = query.Where(task => task.State == state);
        }

        return query
            .OrderByDescending(task => task.CreationTime)
            .Include(task => task.AssignedPerson)
            .ToList();
    }
}

首先我们定义了 ITaskRepository 接口然后实现它。GetAll() 方法返回 IQueryable<Task>,然后我们可以使用给定的参数添加一些 Where 来过滤数据。最后我们可以调用 ToList() 方法来获取任务列表。

你也可以在仓储的方法中使用 Context 对象取得DbContext然后直接使用EF的API。

注意:在领域层定义自定义仓储接口,然后在 EntityFrameworkCore 项目中实现该接口。因此,你可以从任何项目中注入该接口并不需要引用EF Core。

替换默认仓储

如上所示,即使你创建了一个 TaskRepository,在任何类中可以注入且使用它 IRepository<Task, long>。在大多数情况下,这不会发生任何问题。但是,如果你想在你自定义的仓储中重写基类的方法;例如:你已经在自定义仓储中重写了基类的Delete方法,并实现一些自定义的行为。如果某个类注入了 IRepository<Task, long> 并且使用了默认的仓储来删除某个任务,那么该自定义的行为将不会执行。为了克服这个问题,你可以用自定义仓储来替换掉默认仓储;如下所示:

Configuration.ReplaceService<IRepository<Task, Guid>>(() =>
{
    IocManager.IocContainer.Register(
        Component.For<IRepository<Task, Guid>, ITaskRepository, TaskRepository>()
            .ImplementedBy<TaskRepository>()
            .LifestyleTransient()
    );
});

我们为 IRepository<Task, Guid>,ITaskRepository,TaskRepository 注册了 TaskRepository;所以,它们中任意一个可以被注入且使用 TaskRepository

Repository 最佳实践

  • 在任何可能的地方使用默认仓储。即使你的实体有自定义仓储,你也能使用默认仓储(如果你使用了标准的仓储方法)。

  • 如同上面所述:对自定义的仓储创建 仓储基类

  • 在领域层中定义自定义仓储接口(.Core 项目是在启动模板),如果你从你的领域层抽象出EF Core, 那么请在 .EntityFrameworkCore 项目中实现自定义仓储类。