项目
版本

Entity Framework Core 集成

本文档解释了如何将 EF Core 作为 ORM 提供程序集成到基于 ABP 的应用程序中以及如何进行配置。

安装

Volo.Abp.EntityFrameworkCore 是用于 EF Core 集成的主要 NuGet 包。将其安装到您的项目(对于分层应用程序,安装到您的数据/基础设施层):

abp add-package Volo.Abp.EntityFrameworkCore

如果您尚未安装,首先需要安装 ABP CLI。关于其他安装选项,请参阅 包描述页面。

注意:或者,您可以直接下载已预装 EF Core 的 启动模板。

选择数据库管理系统

Entity Framework Core 支持多种数据库管理系统(查看所有)。ABP 和本文档不依赖任何特定的 DBMS。如果您正在创建 可重用的应用程序模块,请避免依赖特定的 DBMS 包。但是,在最终应用程序中,您最终需要选择一个 DBMS。

请参阅 为 Entity Framework Core 切换到其他 DBMS 文档,了解如何切换 DBMS。

创建 DbContext

您的 DbContext 类应派生自 AbpDbContext<T>,如下所示:

using Microsoft.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore;

namespace MyCompany.MyProject
{
    public class MyDbContext : AbpDbContext<MyDbContext>
    {
        //...在此处定义您的 DbSet 属性

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

关于 EF Core Fluent 映射

应用程序启动模板 已配置为使用 EF Core Fluent 配置 API 将您的实体映射到数据库表。

在您的实体属性上,您仍然可以使用 数据注解特性(如 [Required]),而 ABP 文档通常遵循 Fluent 映射 API 方法。这取决于您。

ABP 有一些 基实体类 和 约定(请参阅 实体文档),并提供了一些有用的 扩展方法 来配置从基实体类继承的属性。

ConfigureByConvention 方法

ConfigureByConvention() 是主要的扩展方法,它为您的实体 配置所有基础属性和约定。因此,在您的 Fluent 映射代码中为所有实体调用此方法是一种 最佳实践。

示例:假设您有一个派生自 AggregateRoot<Guid> 基类的 Book 实体:

public class Book : AuditedAggregateRoot<Guid>
{
    public string Name { get; set; }
}

您可以在 DbContext 中重写 OnModelCreating 方法,并按如下所示配置映射:

protected override void OnModelCreating(ModelBuilder builder)
{
    //始终调用基类方法
    base.OnModelCreating(builder);

    builder.Entity<Book>(b =>
    {
        b.ToTable("Books");

        //配置基础属性
        b.ConfigureByConvention();

        //配置其他属性(如果您使用 Fluent API)
        b.Property(x => x.Name).IsRequired().HasMaxLength(128);
    });
}
  • 在这里调用 b.ConfigureByConvention() 对于正确 配置基础属性 非常重要。
  • 您可以在此配置 Name 属性,也可以使用 数据注解特性(请参阅 EF Core 文档)。

虽然有许多扩展方法来配置您的基础属性,但 ConfigureByConvention() 会在必要时内部调用它们。因此,调用它就足够了。

配置连接字符串选择

如果您的应用程序中有多个数据库,您可以使用 [ConnectionStringName] 特性为 DbContext 配置连接字符串名称。示例:

[ConnectionStringName("MySecondConnString")]
public class MyDbContext : AbpDbContext<MyDbContext>
{

}

如果不配置,则使用 Default 连接字符串。如果配置了特定的连接字符串名称,但在应用程序配置中未定义此连接字符串名称,则它将回退到 Default 连接字符串(更多信息请参阅 连接字符串文档)。

AbpDbContextOptions

AbpDbContextOptions 用于配置 DbContext 选项。当您使用 ABP 的应用程序启动模板创建新解决方案时,您将看到简单的配置(在 EntityFrameworkCore 集成项目的模块类中),如下所示:

Configure<AbpDbContextOptions>(options =>
{
    options.UseSqlServer();
});

该配置将应用程序所有 DbContext 的默认 DBMS 设置为 SQL Server。该配置是一种简写形式,也可以用以下代码块完成:

Configure<AbpDbContextOptions>(options =>
{
    options.Configure(opts =>
    {
        opts.UseSqlServer();
    });
});

options.Configure(...) 方法有更多配置选项。例如,您可以按如下方式设置 DbContextOptions(EF Core 的本地选项):

Configure<AbpDbContextOptions>(options =>
{
    options.Configure(opts =>
    {
        opts.DbContextOptions.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
    });
});

为 DbContext 的 ConfigureConventions 和 OnModelCreating 方法添加操作,如下所示:

Configure<AbpDbContextOptions>(options =>
{
    options.ConfigureDefaultConvention((dbContext, builder) =>
    {
        // 此操作会为所有 DbContext 的 ConfigureConventions 方法调用。
    });

    options.ConfigureConventions<YourDbContext>((dbContext, builder) =>
    {
        // 此操作会为特定 DbContext 的 ConfigureConventions 方法调用。
    });

    options.ConfigureDefaultOnModelCreating((dbContext, builder) =>
    {
        // 此操作会为所有 DbContext 的 OnModelCreating 方法调用。
    });

    options.ConfigureOnModelCreating<YourDbContext>((dbContext, builder) =>
    {
        // 此操作会为特定 DbContext 的 OnModelCreating 方法调用。
    });
});

如果您只有一个 DbContext,或者有多个 DbContext 但希望为所有使用相同的 DBMS 和配置,您可以保持原样。但是,如果您需要为特定的 DbContext 配置不同的 DBMS 或自定义配置,可以按如下方式指定:

Configure<AbpDbContextOptions>(options =>
{
    // 所有 DbContext 的默认配置
    options.Configure(opts =>
    {
        opts.UseSqlServer();
    });

    // 为特定 DbContext 自定义配置
    options.Configure<MyOtherDbContext>(opts =>
    {
        opts.UseMySQL();
    });
});

请参阅 为 Entity Framework Core 切换到其他 DBMS 文档,了解如何配置 DBMS。

将 DbContext 注册到依赖注入

在您的模块中使用 AddAbpDbContext 方法将您的 DbContext 类注册到 依赖注入 系统。

using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.Modularity;

namespace MyCompany.MyProject
{
    [DependsOn(typeof(AbpEntityFrameworkCoreModule))]
    public class MyModule : AbpModule
    {
        public override void ConfigureServices(ServiceConfigurationContext context)
        {
            context.Services.AddAbpDbContext<MyDbContext>();

            //...
        }
    }
}

添加默认仓储

ABP 可以自动为 DbContext 中的实体创建默认的 通用仓储。只需在注册时使用 AddDefaultRepositories() 选项:

services.AddAbpDbContext<MyDbContext>(options =>
{
    options.AddDefaultRepositories();
});

默认情况下,这将为每个 聚合根实体(派生自 AggregateRoot 的类)创建一个仓储。如果您也想为其他实体创建仓储,则将 includeAllEntities 设置为 true:

services.AddAbpDbContext<MyDbContext>(options =>
{
    options.AddDefaultRepositories(includeAllEntities: true);
});

然后,您可以在服务中注入和使用 IRepository<TEntity, TPrimaryKey>。假设您有一个主键为 Guid 的 Book 实体:

public class Book : AggregateRoot<Guid>
{
    public string Name { get; set; }

    public BookType Type { get; set; }
}

(BookType 在这里是一个简单的 enum,并不重要)并且您想在 领域服务 中创建一个新的 Book 实体:

public class BookManager : DomainService
{
    private readonly IRepository<Book, Guid> _bookRepository;

    //将默认仓储注入到构造函数
    public BookManager(IRepository<Book, Guid> bookRepository)
    {
        _bookRepository = bookRepository;
    }

    public async Task<Book> CreateBook(string name, BookType type)
    {
        Check.NotNullOrWhiteSpace(name, nameof(name));

        var book = new Book
        {
            Id = GuidGenerator.Create(),
            Name = name,
            Type = type
        };

        //使用标准仓储方法
        await _bookRepository.InsertAsync(book);

        return book;
    }
}

此示例使用 InsertAsync 方法将新实体插入数据库。

添加自定义仓储

默认通用仓储在大多数情况下足够强大(因为它们实现了 IQueryable)。但是,您可能需要创建自定义仓储以添加自己的仓储方法。假设您想按类型删除所有书籍。

建议为您的自定义仓储定义一个接口:

public interface IBookRepository : IRepository<Book, Guid>
{
    Task DeleteBooksByType(BookType type);
}

您通常希望从 IRepository 派生以继承标准仓储方法(但您不必这样做)。仓储接口在分层应用程序的领域层中定义。它们在数据/基础设施层(启动模板 中的 EntityFrameworkCore 项目)中实现。

IBookRepository 接口的示例实现:

public class BookRepository
    : EfCoreRepository<BookStoreDbContext, Book, Guid>, IBookRepository
{
    public BookRepository(IDbContextProvider<BookStoreDbContext> dbContextProvider)
        : base(dbContextProvider)
    {
    }

    public async Task DeleteBooksByType(BookType type)
    {
        var dbContext = await GetDbContextAsync();
        await dbContext.Database.ExecuteSqlRawAsync(
            $"DELETE FROM Books WHERE Type = {(int)type}"
        );
    }
}

现在,可以在需要时 注入 IBookRepository 并使用 DeleteBooksByType 方法。

重写默认通用仓储

即使您创建了自定义仓储,您仍然可以注入默认的通用仓储(对于此示例为 IRepository<Book, Guid>)。默认仓储实现不会使用您创建的类。

如果希望用您的自定义仓储替换默认仓储实现,请在 AddAbpDbContext 选项内进行操作:

context.Services.AddAbpDbContext<BookStoreDbContext>(options =>
{
    options.AddDefaultRepositories();

    //替换 IRepository<Book, Guid>
    options.AddRepository<Book, BookRepository>();
});

当您想要 重写基础仓储方法 以自定义它时,这尤其重要。例如,您可能希望重写 DeleteAsync 方法以更高效的方式删除特定实体:

public async override Task DeleteAsync(
    Guid id,
    bool autoSave = false,
    CancellationToken cancellationToken = default)
{
    //TODO: 删除方法的自定义实现
}

加载相关实体

假设您有一个包含多个 OrderLine 的 Order,并且 OrderLine 具有到 Order 的导航属性:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Volo.Abp.Auditing;
using Volo.Abp.Domain.Entities;

namespace MyCrm
{
    public class Order : AggregateRoot<Guid>, IHasCreationTime
    {
        public Guid CustomerId { get; set; }
        public DateTime CreationTime { get; set; }

        public ICollection<OrderLine> Lines { get; set; } //子集合

        public Order()
        {
            Lines = new Collection<OrderLine>();
        }
    }

    public class OrderLine : Entity<Guid>
    {
        public Order Order { get; set; } //导航属性
        public Guid OrderId { get; set; }

        public Guid ProductId { get; set; }
        public int Count { get; set; }
        public double UnitPrice { get; set; }
    }
}

并按如下所示定义数据库映射:

builder.Entity<Order>(b =>
{
    b.ToTable("Orders");
    b.ConfigureByConvention();

    //定义关系
    b.HasMany(x => x.Lines)
        .WithOne(x => x.Order)
        .HasForeignKey(x => x.OrderId)
        .IsRequired();
});

builder.Entity<OrderLine>(b =>
{
    b.ToTable("OrderLines");
    b.ConfigureByConvention();
});

当查询 Order 时,您可能希望在单个查询中 包含 所有 OrderLine,或者可能希望稍后按需 加载它们。

实际上,这些与 ABP 没有直接关系。您可以参阅 EF Core 文档 了解所有细节。本节将涵盖与 ABP 相关的一些主题。

预先加载 / 加载明细

当您希望在查询实体时加载相关实体,有不同的选项。

Repository.WithDetails

IRepository.WithDetailsAsync(...) 可用于通过包含一个关系集合/属性来获取 IQueryable<T>。

示例:获取订单及其明细行

using System;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Domain.Services;

namespace AbpDemo.Orders
{
    public class OrderManager : DomainService
    {
        private readonly IRepository<Order, Guid> _orderRepository;

        public OrderManager(IRepository<Order, Guid> orderRepository)
        {
            _orderRepository = orderRepository;
        }

        public async Task TestWithDetails(Guid id)
        {
            //通过包含子集合获取 IQueryable<T>
            var queryable = await _orderRepository.WithDetailsAsync(x => x.Lines);

            //应用额外的 LINQ 扩展方法
            var query = queryable.Where(x => x.Id == id);

            //执行查询并获取结果
            var order = await AsyncExecuter.FirstOrDefaultAsync(query);
        }
    }
}

AsyncExecuter 用于在不依赖 EF Core 的情况下执行异步 LINQ 扩展。如果您在项目中添加了 EF Core NuGet 包引用,那么可以直接使用 await query.FirstOrDefaultAsync()。但是,这样您的领域层就依赖于 EF Core。更多信息请参阅 仓储文档。

示例:获取订单列表及其明细行

public async Task TestWithDetails()
{
    //通过包含子集合获取 IQueryable<T>
    var queryable = await _orderRepository.WithDetailsAsync(x => x.Lines);

    //执行查询并获取结果
    var orders = await AsyncExecuter.ToListAsync(queryable);
}

如果需要包含多个导航属性或集合,WithDetailsAsync 方法可以接受多个表达式参数。

DefaultWithDetailsFunc

如果未向 WithDetailsAsync 方法传递任何表达式,则它将使用您提供的 DefaultWithDetailsFunc 选项来包含所有明细。

您可以在 EntityFrameworkCore 项目的 模块 的 ConfigureServices 方法中为实体配置 DefaultWithDetailsFunc。

示例:在查询 Order 时包含 Lines

Configure<AbpEntityOptions>(options =>
{
    options.Entity<Order>(orderOptions =>
    {
        orderOptions.DefaultWithDetailsFunc = query => query.Include(o => o.Lines);
    });
});

您可以完全使用 EF Core API,因为此配置位于 EF Core 集成项目中。

然后,您就可以使用不带任何参数的 WithDetails 方法:

public async Task TestWithDetails()
{
    //通过包含所有子集合获取 IQueryable<T>
    var queryable = await _orderRepository.WithDetailsAsync();

    //执行查询并获取结果
    var orders = await AsyncExecuter.ToListAsync(queryable);
}

WithDetailsAsync() 执行您设置为 DefaultWithDetailsFunc 的表达式。

Repository Get/Find 方法

一些标准的 仓储 方法有可选的 includeDetails 参数:

  • GetAsync 和 FindAsync 获取 includeDetails,默认值为 true。
  • GetListAsync 和 GetPagedListAsync 获取 includeDetails,默认值为 false。

这意味着,默认情况下,返回 单个实体 的方法会 包含明细,而返回列表的方法默认 不包含明细。您可以显式传递 includeDetails 来改变此行为。

这些方法使用上面解释的 DefaultWithDetailsFunc 选项。

示例:获取包含明细的订单

public async Task TestWithDetails(Guid id)
{
    var order = await _orderRepository.GetAsync(id);
}

示例:获取不包含明细的订单

public async Task TestWithoutDetails(Guid id)
{
    var order = await _orderRepository.GetAsync(id, includeDetails: false);
}

示例:获取包含明细的实体列表

public async Task TestWithDetails()
{
    var orders = await _orderRepository.GetListAsync(includeDetails: true);
}

替代方案

仓储模式试图封装 EF Core,因此您的选项有限。如果需要高级场景,可以遵循以下选项之一:

  • 创建自定义仓储方法并使用完整的 EF Core API。
  • 从您的项目引用 Volo.Abp.EntityFrameworkCore 包。通过这种方式,您可以直接在代码中使用 Include 和 ThenInclude。

另请参阅 EF Core 的 预先加载文档。

显式 / 延迟加载

如果在查询实体时未包含关系,但后来需要访问导航属性或集合,您有不同的选项。

EnsurePropertyLoadedAsync / EnsureCollectionLoadedAsync

仓储提供 EnsurePropertyLoadedAsync 和 EnsureCollectionLoadedAsync 扩展方法来 显式加载 导航属性或子集合。

示例:在需要时加载订单的明细行

public async Task TestWithDetails(Guid id)
{
    var order = await _orderRepository.GetAsync(id, includeDetails: false);
    //此时 order.Lines 为空

    await _orderRepository.EnsureCollectionLoadedAsync(order, x => x.Lines);
    //现在 order.Lines 已填充
}

如果属性或集合已加载,EnsurePropertyLoadedAsync 和 EnsureCollectionLoadedAsync 方法不执行任何操作。因此,多次调用没有问题。

另请参阅 EF Core 的 显式加载文档。

使用代理的延迟加载

在某些情况下,显式加载可能不可行,特别是当您没有对 Repository 或 DbContext 的引用时。延迟加载是 EF Core 的一个功能,当您首次访问相关属性/集合时加载它们。

要启用延迟加载:

  1. 将 Microsoft.EntityFrameworkCore.Proxies 包安装到您的项目中(通常安装到 EF Core 集成项目)
  2. 为您的 DbContext 配置 UseLazyLoadingProxies(在您的 EF Core 项目中模块的 ConfigureServices 方法中)。示例:
Configure<AbpDbContextOptions>(options =>
{
    options.PreConfigure<MyCrmDbContext>(opts =>
    {
        opts.DbContextOptions.UseLazyLoadingProxies(); //启用延迟加载
    });

    options.UseSqlServer();
});
  1. 使您的导航属性和集合变为 virtual。示例:
public virtual ICollection<OrderLine> Lines { get; set; } //virtual 集合
public virtual Order Order { get; set; } //virtual 导航属性

一旦启用延迟加载并配置好您的实体,您就可以自由访问导航属性和集合:

public async Task TestWithDetails(Guid id)
{
    var order = await _orderRepository.GetAsync(id);
    //此时 order.Lines 为空

    var lines = order.Lines;
    //order.Lines 已填充(延迟加载)
}

每当您访问属性/集合时,EF Core 会自动执行额外的查询以从数据库加载属性/集合。

应谨慎使用延迟加载,因为在某些特定情况下可能会导致性能问题。

另请参阅 EF Core 的 延迟加载文档。

只读仓储

ABP 提供只读 仓储 接口(IReadOnlyRepository<...> 或 IReadOnlyBasicRepository<...>)来明确表明您的目的是查询数据,而不是更改它。如果是这样,您可以将这些接口注入到您的服务中。

Entity Framework Core 只读仓储实现使用了 EF Core 的无跟踪功能。这意味着从仓储返回的实体不会被 EF Core 更改跟踪器 跟踪,因为预计您不会更新从只读仓储查询的实体。如果需要跟踪实体,仍然可以在 LINQ 表达式上使用 AsTracking() 扩展方法,或在仓储对象上使用 EnableTracking() 扩展方法(请参阅本文档中的 启用/禁用更改跟踪 部分)。

仅当使用只读仓储接口之一(IReadOnlyRepository<...> 或 IReadOnlyBasicRepository<...>)注入仓储对象时,此行为才有效。如果您注入了标准仓储(例如 IRepository<...>),然后将其转换为只读仓储接口,则此行为无效。

启用/禁用更改跟踪

除了只读仓储之外,ABP 还允许手动控制查询对象的更改跟踪行为。请参阅 仓储文档 中的 启用/禁用更改跟踪 部分,了解如何使用它。

访问 EF Core API

在大多数情况下,您希望将 EF Core API 隐藏在仓储后面(这是仓储模式的主要目的)。但是,如果您想通过仓储访问 DbContext 实例,可以使用 GetDbContext() 或 GetDbSet() 扩展方法。示例:

public async Task TestAsync()
{
    var dbContext = await _orderRepository.GetDbContextAsync();
    var dbSet = await _orderRepository.GetDbSetAsync();
    //var dbSet = dbContext.Set<Order>(); //备选方案,当您拥有 DbContext 时
}
  • GetDbContextAsync 返回的是 DbContext 引用,而不是 BookStoreDbContext。如果需要,可以强制转换。但在大多数情况下不需要。

重要:您必须从想要访问 DbContext 的项目引用 Volo.Abp.EntityFrameworkCore 包。这破坏了封装,但这就是您在这种情况下想要的。

额外属性与对象扩展管理器

额外属性系统允许您向实现 IHasExtraProperties 接口的实体设置/获取动态属性。当您将 应用程序模块 作为包引用,并希望向其定义的实体添加自定义属性时,这尤其有用。

默认情况下,实体的所有额外属性都作为单个 JSON 对象存储在数据库中。

实体扩展系统允许您将所需的额外属性存储在相关数据库表的单独字段中。有关额外属性和实体扩展系统的更多信息,请参阅以下文档:

本节仅解释 ObjectExtensionManager 与 EF Core 相关的用法。

ObjectExtensionManager.Instance

ObjectExtensionManager 实现了单例模式,因此您需要使用静态的 ObjectExtensionManager.Instance 来执行所有操作。

MapEfCoreProperty

MapEfCoreProperty 是一个快捷扩展方法,用于为实体定义扩展属性并将其映射到数据库。

示例:向 IdentityRole 实体添加 Title 属性(数据库字段):

ObjectExtensionManager.Instance
    .MapEfCoreProperty<IdentityRole, string>(
        "Title",
        (entityBuilder, propertyBuilder) =>
        {
            propertyBuilder.HasMaxLength(64);
        }
    );

MapEfCoreEntity

MapEfCoreEntity 是一个快捷扩展方法,用于配置 Entity。

示例:设置 IdentityRole 实体的 Name 的最大长度:

ObjectExtensionManager.Instance
    .MapEfCoreEntity<IdentityRole>(builder =>
    {
        builder.As<EntityTypeBuilder<IdentityRole>>().Property(x => x.Name).HasMaxLength(200);
    });

MapEfCoreDbContext

MapEfCoreDbContext 是一个快捷扩展方法,用于配置 DbContext。

示例:设置 IdentityDbContext 的 IdentityRole 实体的 Name 的最大长度:

ObjectExtensionManager.Instance.MapEfCoreDbContext<IdentityDbContext>(b =>
{
    b.Entity<IdentityRole>().Property(x => x.Name).HasMaxLength(200);
});

如果相关模块实现了此功能(如下所述),则新属性将添加到模型中或 DbContext/Entity 配置发生更改。然后,您需要运行标准的 Add-Migration 和 Update-Database 命令来更新数据库以添加新字段。

MapEfCoreProperty、MapEfCoreEntity 和 MapEfCoreDbContext 方法必须在相关 DbContext 使用之前调用。它是一个静态方法。最佳方式是在应用程序中尽早使用它。应用程序启动模板中有一个 YourProjectNameEfCoreEntityExtensionMappings 类,可以安全地在此类中使用此方法。

ConfigureEfCoreEntity、ApplyObjectExtensionMappings 和 TryConfigureObjectExtensions

如果您正在构建可重用模块,并希望允许应用程序开发人员向您的实体添加属性,则可以在实体映射中使用 ConfigureEfCoreEntity、ApplyObjectExtensionMappings 和 TryConfigureObjectExtensions 扩展方法。

示例:

public static class QADbContextModelCreatingExtensions
{
    public static void ConfigureQA(
        this ModelBuilder builder,
        Action<QAModelBuilderConfigurationOptions> optionsAction = null)
    {
        Check.NotNull(builder, nameof(builder));

        var options = new QAModelBuilderConfigurationOptions(
            QADatabaseDbProperties.DbTablePrefix,
            QADatabaseDbProperties.DbSchema
        );

        optionsAction?.Invoke(options);

        builder.Entity<QA_Question>(b =>
        {
            b.ToTable(options.TablePrefix + "Questions", options.Schema);
            b.ConfigureByConvention();
            //...

            //在 buildAction 末尾调用此方法。
            b.ApplyObjectExtensionMappings();
        });

        //...

        //在 ConfigureQA 末尾调用此方法。
        builder.TryConfigureObjectExtensions<QADbContext>();
    }
}

如果调用 ConfigureByConvention() 扩展方法(如此示例中的 b.ConfigureByConvention()),ABP 会在内部调用 ConfigureObjectExtensions 和 ConfigureEfCoreEntity 方法。使用 ConfigureByConvention() 方法是一种 最佳实践,因为它还会按约定配置基础属性的数据库映射。

对象扩展 功能需要 更改跟踪,这意味着对于具有 扩展属性(MapEfCoreProperty) 的实体,不能使用只读仓储。请参阅 仓储文档 了解更改跟踪行为。

更多信息请参阅上面的 ConfigureByConvention 方法 部分。

访问额外属性(影子属性)

存储在数据库单独字段中的额外属性称为 影子属性。这些属性未在实体类中定义,但属于 EF Core 模型的一部分,可以在 LINQ 查询中使用 EF.Property 静态方法引用:

var query = (await GetQueryableAsync()).Where(x => EF.Property<string>(x, "Title") == "MyTitle");

更多信息请参阅 EF Core 影子和索引器属性文档。

高级主题

控制多租户

如果您的解决方案是 多租户 的,租户可能拥有 独立的数据库,您的解决方案中有 多个 DbContext 类,并且您的某些 DbContext 类应该 仅从宿主端可用,建议在 DbContext 类上添加 [IgnoreMultiTenancy] 特性。在这种情况下,ABP 保证相关的 DbContext 始终使用宿主 连接字符串,即使您处于租户上下文中。

示例:

[IgnoreMultiTenancy]
public class MyDbContext : AbpDbContext<MyDbContext>
{
    ...
}

如果 DbContext 中的任何实体可以保存在租户数据库中,请不要使用 [IgnoreMultiTenancy] 特性。

当您使用仓储时,ABP 已经对未实现 IMultiTenant 接口的实体使用宿主数据库。因此,如果您使用仓储来处理数据库,大多数时候不需要 [IgnoreMultiTenancy] 特性。

设置默认仓储类

默认通用仓储默认由 EfCoreRepository 类实现。您可以创建自己的实现,并将其用于所有默认仓储实现。

首先,按如下方式定义您的默认仓储类:

public class MyRepositoryBase<TEntity>
    : EfCoreRepository<BookStoreDbContext, TEntity>
      where TEntity : class, IEntity
{
    public MyRepositoryBase(IDbContextProvider<BookStoreDbContext> dbContextProvider)
        : base(dbContextProvider)
    {
    }
}

public class MyRepositoryBase<TEntity, TKey>
    : EfCoreRepository<BookStoreDbContext, TEntity, TKey>
    where TEntity : class, IEntity<TKey>
{
    public MyRepositoryBase(IDbContextProvider<BookStoreDbContext> dbContextProvider)
        : base(dbContextProvider)
    {
    }
}

第一个适用于 具有复合键的实体,第二个适用于具有单个主键的实体。

建议继承 EfCoreRepository 类并在需要时重写方法。否则,您将不得不手动实现所有标准仓储方法。

现在,您可以使用 SetDefaultRepositoryClasses 选项:

context.Services.AddAbpDbContext<BookStoreDbContext>(options =>
{
    options.SetDefaultRepositoryClasses(
        typeof(MyRepositoryBase<,>),
        typeof(MyRepositoryBase<>)
    );

    //...
});

为默认仓储设置基础 DbContext 类或接口

如果您的 DbContext 继承自另一个 DbContext 或实现了一个接口,您可以使用该基类或接口作为默认仓储的 DbContext。示例:

public interface IBookStoreDbContext : IEfCoreDbContext
{
    DbSet<Book> Books { get; }
}

IBookStoreDbContext 由 BookStoreDbContext 类实现。然后您可以使用 AddDefaultRepositories 的泛型重载:

context.Services.AddAbpDbContext<BookStoreDbContext>(options =>
{
    options.AddDefaultRepositories<IBookStoreDbContext>();
    //...
});

现在,您的自定义 BookRepository 也可以使用 IBookStoreDbContext 接口:

public class BookRepository : EfCoreRepository<IBookStoreDbContext, Book, Guid>, IBookRepository
{
    //...
}

为 DbContext 使用接口的一个优点是它可以通过另一个实现替换。

替换其他 DbContext

一旦您正确定义并使用 DbContext 的接口,那么任何其他实现都可以使用以下方式替换它:

ReplaceDbContext 特性

[ReplaceDbContext(typeof(IBookStoreDbContext))]
public class OtherDbContext : AbpDbContext<OtherDbContext>, IBookStoreDbContext
{
    //...
}

ReplaceDbContext 选项

context.Services.AddAbpDbContext<OtherDbContext>(options =>
{
    //...
    options.ReplaceDbContext<IBookStoreDbContext>();
});

在此示例中,OtherDbContext 实现了 IBookStoreDbContext。此功能允许您在开发时拥有多个 DbContext(每个模块一个),但在运行时拥有单个 DbContext(实现所有 DbContext 的所有接口)。

多租户替换

也可以根据 多租户 端替换 DbContext。ReplaceDbContext 特性和 ReplaceDbContext 方法可以接受一个 MultiTenancySides 选项,默认值为 MultiTenancySides.Both。

示例: 仅针对租户替换 DbContext,使用 ReplaceDbContext 特性

[ReplaceDbContext(typeof(IBookStoreDbContext), MultiTenancySides.Tenant)]

示例: 仅针对宿主端替换 DbContext,使用 ReplaceDbContext 方法

options.ReplaceDbContext<IBookStoreDbContext>(MultiTenancySides.Host);

拆分查询

为了获得更好的性能,ABP 默认全局启用 拆分查询。您可以根据需要进行更改。

示例

Configure<AbpDbContextOptions>(options =>
{
    options.UseSqlServer(optionsBuilder =>
    {
        optionsBuilder.UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery);
    });
});

自定义批量操作

如果您有更好的逻辑或使用外部库进行批量操作,可以通过实现 IEfCoreBulkOperationProvider 来重写该逻辑。

  • 您可以使用以下示例模板:
public class MyCustomEfCoreBulkOperationProvider
    : IEfCoreBulkOperationProvider, ITransientDependency
{
    public async Task DeleteManyAsync<TDbContext, TEntity>(
        IEfCoreRepository<TEntity> repository,
        IEnumerable<TEntity> entities,
        bool autoSave,
        CancellationToken cancellationToken)
        where TDbContext : IEfCoreDbContext
        where TEntity : class, IEntity
    {
        // 在此处实现您的逻辑。
    }

    public async Task InsertManyAsync<TDbContext, TEntity>(
        IEfCoreRepository<TEntity> repository,
        IEnumerable<TEntity> entities,
        bool autoSave,
        CancellationToken cancellationToken)
        where TDbContext : IEfCoreDbContext
        where TEntity : class, IEntity
    {
        // 在此处实现您的逻辑。
    }

    public async Task UpdateManyAsync<TDbContext, TEntity>(
        IEfCoreRepository<TEntity> repository,
        IEnumerable<TEntity> entities,
        bool autoSave,
        CancellationToken cancellationToken)
        where TDbContext : IEfCoreDbContext
        where TEntity : class, IEntity
    {
        // 在此处实现您的逻辑。
    }
}

另请参阅

在本文档中