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

ABP可以与任何ORM框架协同工作,它内置了对EntityFramework的集成支持。本文将介绍如何在ABP中使用EntityFramework。本文假定你已经初步掌握了EntityFramework。

译者注:怎么才算初步掌握了EntityFramework呢?译者认为应当懂得使用Code First模式进行CRUD。

9.1.1 Nuget包

在ABP中要使用EntityFramework作为ORM框架的话,需要到Nuget上下载一个名为Abp.EntityFramework的包。比较好的做法是:新建一个独立的程序集(dll),然后在这个程序集中调用这个包和EntityFramework。

ABP官方提供的模板程序就是这样做的。模板程序的下载方法详见《ABP系列之2、ABP入门教程》

9.1.2 创建DbContext

要使用EntityFramework,首先需要定义一个DbContext类。下面是一个DbContex类的示例:

public class SimpleTaskSystemDbContext : AbpDbContext
{
    public virtual IDbSet<Person> People { get; set; }
    public virtual IDbSet<Task> Tasks { get; set; }

    public SimpleTaskSystemDbContext()
        : base("Default")
    {

    }
    
    public SimpleTaskSystemDbContext(string nameOrConnectionString)
        : base(nameOrConnectionString)
    {

    }

    public SimpleTaskSystemDbContext(DbConnection existingConnection)
        : base(existingConnection, false)
    {

    }

    public SimpleTaskSystemDbContext(DbConnection existingConnection, bool contextOwnsConnection)
        : base(existingConnection, contextOwnsConnection)
    {

    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<Person>().ToTable("StsPeople");
        modelBuilder.Entity<Task>().ToTable("StsTasks").HasOptional(t => t.AssignedPerson);
    }
}

上面是一个常规的ABP的DbContext,它还需要遵循下列规则:

  • 它派生自 AbpDbContext 而不是DbContext。

  • 如上面示例所示:它应该有构造函数(构造函数的参数名字也应该相同):

    • Default 构造函数传入 "Default" 给基类作为连接字符串。所以,这表示在web.config/app.config文件中有一个名字为 "Default" 的连接字符串。该构造函数不是被ABP使用,它是被迁移工具EF命令行使用的,如:update-database。

    • 在运行时ABP通过构造函数取得 nameOrConnectionString 来传入连接名或连接字符串。

    • 在单元测试的时候使用构造函数来取得 existingConnection,而不是直接由ABP使用。

    • DbContextEfTransactionStrategy 被使用的时候(请查阅事务管理章节),ABP会通过构造函数取得 existingConnection和contextOwnsConnection,并且在单数据库多dbcontext场景下会共享连接和事务。

EntityFramework可以使用约定的方式来映射实体和数据表。除非你想进行自定义映射,否则你甚至不需要做任何配置。在上例中,我们将实体映射到了不同的表中。默认情况下(按照约定优先于配置的原则,会默认采用约定的方式),Task实体会映射到Tasks表,但在这里我们将它映射到了StsTasks表。相比起使用Data Annotation模式来进行自定义映射,我更喜欢使用Fluent API模式。当然,你可以选择你所喜欢的模式,这里并没有特别的限制。

9.1.3 仓储

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

默认仓储

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

//应用程序中的所有仓储的基类
public class SimpleTaskSystemRepositoryBase<TEntity, TPrimaryKey> : EfRepositoryBase<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)
    {
    }
    
    //别在这里添加任何方法,请将方法添加到上面那个类,因为该类继承了上面的泛型类
}

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

你指定的 DbContext 默认都是使用 EfRepositoryBase 来实现仓储功能的,例如:上面示例 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。

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

Repository 最佳实践

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

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

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

9.1.4 事务管理

ABP已有内置的工作单元来管理数据库连接和事务。EF有不同的事务管理方法。ABP默认使用 TransactionScope 包裹的方式,对于DbContext事务API也有内置的扩展。如果你想切换到DbContext事务API,你可以在模块的PreInitialize方法中配置它;如下所示:

Configuration.ReplaceService<IEfTransactionStrategy, DbContextEfTransactionStrategy>(DependencyLifeStyle.Transient);

记住引用 using Abp.Configuration.Startup 命名空间,来使用 ReplaceService 扩展方法。

还有,如同上面DbContext章节所描述的一样,你的DbContext 应该有构造函数

9.1.5 数据存储

由于ABP已经内置的集成了EF,它能很好的支持EF的数据存储功能。在启动模板中默认设计是使用Sql Server,但是你也可以修改它来使用其它数据存储。

例如,如果你想使用MySQL,请参考该[文档](9.4ABP基础设施层-集成Entity Framework MySql.md);