使用 Entity Framework Core 7.0 向实体注入服务依赖






4.71/5 (10投票s)
如何将服务依赖项注入到 EFCore 7 的实体中
引言
依赖注入是一种广泛使用的模式,用于从类中获取其他服务的引用。这是开发 ASP.NET Core 应用程序的内置功能。在本文中,我将解释为什么我们可能需要在实体类中引用其他服务,以及如何实现 Entity Framework Core 新的 IMaterializationInterceptor
接口,以使用标准的依赖注入系统为实体提供这些服务。
我将使用 ABP Framework 来实现应用程序代码。
问题
在开发基于 领域驱动设计 (DDD) 模式的应用程序时,我们通常将业务代码编写在 应用程序服务、领域服务 和 实体 中。由于应用程序和服务实例是由依赖注入系统创建的,因此它们可以在构造函数中注入服务。
这是一个将存储库注入其构造函数的领域服务示例
public class ProductManager : DomainService
{
private readonly IRepository<Product, Guid> _productRepository;
public ProductManager(IRepository<Product, Guid> productRepository)
{
_productRepository = productRepository;
}
//...
}
ProductManager
随后可以在其方法中使用 _productRepository
对象来执行其业务逻辑。在以下示例中,ChangeCodeAsync
方法用于更改产品的代码(ProductCode
属性),同时确保系统中的产品代码唯一性。
public class ProductManager : DomainService
{
private readonly IRepository<Product, Guid> _productRepository;
public ProductManager(IRepository<Product, Guid> productRepository)
{
_productRepository = productRepository;
}
public async Task ChangeCodeAsync(Product product, string newProductCode)
{
Check.NotNull(product, nameof(product));
Check.NotNullOrWhiteSpace(newProductCode, nameof(newProductCode));
if (product.ProductCode == newProductCode)
{
return;
}
if (await _productRepository.AnyAsync(x => x.ProductCode == newProductCode))
{
throw new ApplicationException(
"Product code is already used: " + newProductCode);
}
product.ProductCode = newProductCode;
}
}
在此,ProductManager
强制执行“产品代码必须唯一”的规则。我们来看看 Product
实体类。
public class Product : AuditedAggregateRoot<Guid>
{
public string ProductCode { get; internal set; }
public string Name { get; private set; }
private Product()
{
/* This constructor is used by EF Core while
getting the Product from database */
}
/* Primary constructor that should be used in the application code */
public Product(string productCode, string name)
{
ProductCode = Check.NotNullOrWhiteSpace(productCode, nameof(productCode));
Name = Check.NotNullOrWhiteSpace(name, nameof(name));
}
}
您可以看到 ProductCode
属性的 setter 是 internal
,这使得像上面所示那样从 ProductManager
类中设置它成为可能。
这种设计有一个问题:我们不得不将 ProductCode
的 setter 设置为 internal
。现在,任何开发人员都可能忘记使用 ProductManager.ChangeCodeAsync
方法,并直接在实体上设置 ProductCode
。因此,我们无法完全强制执行“产品代码必须唯一”的规则。
将 ChangeCodeAsync
方法移到 Product
类中,并将 ProductCode
属性的 setter 设置为 private
会更好。
public class Product : AuditedAggregateRoot<Guid>
{
public string ProductCode { get; private set; }
public string Name { get; private set; }
// ...
public async Task ChangeCodeAsync(string newProductCode)
{
Check.NotNullOrWhiteSpace(newProductCode, nameof(newProductCode));
if (newProductCode == ProductCode)
{
return;
}
/* ??? HOW TO INJECT THE PRODUCT REPOSITORY HERE ??? */
if (await _productRepository.AnyAsync(x => x.ProductCode == newProductCode))
{
throw new ApplicationException
("Product code is already used: " + newProductCode);
}
ProductCode = newProductCode;
}
}
采用这种设计,无法在不应用“产品代码必须唯一”规则的情况下设置 ProductCode
。太棒了!但我们遇到了一个问题:实体类无法在其构造函数中注入依赖项,因为实体不是使用依赖注入系统创建的。创建实体的两个常见方式是:
- 我们可以使用标准的
new
关键字在应用程序代码中创建实体,例如var product = new Product(...);
。 - Entity Framework(以及任何其他 ORM/数据库提供程序)在从数据库获取实体后创建实体。它们通常使用实体的空(默认)构造函数来创建它,然后设置来自数据库查询的属性。
那么,我们如何在 Product.ChangeCodeAsync
方法中使用产品存储库呢?如果我们忘记了依赖注入系统,我们会想到将存储库添加为 ChangeCodeAsync
方法的参数,并将获取服务引用的责任委托给该方法的调用者。
public async Task ChangeCodeAsync(
IRepository<Product, Guid> productRepository, string newProductCode)
{
Check.NotNull(productRepository, nameof(productRepository));
Check.NotNullOrWhiteSpace(newProductCode, nameof(newProductCode));
if (newProductCode == ProductCode)
{
return;
}
if (await productRepository.AnyAsync(x => x.ProductCode == newProductCode))
{
throw new ApplicationException(
"Product code is already used: " + newProductCode);
}
ProductCode = newProductCode;
}
但是,这种设计将使 ChangeCodeAsync
方法难以使用,并且还会将其内部依赖项暴露给外部。如果以后在 ChangeCodeAsync
方法中需要另一个依赖项,我们就必须添加另一个参数,这将影响所有使用 ChangeCodeAsync
方法的应用程序代码。我认为这是不合理的。下一节将提供一个更好、更通用的解决方案。
解决方案
首先,我们可以引入一个接口,需要使用服务的方法的实体类应实现该接口。
public interface IInjectServiceProvider
{
ICachedServiceProvider ServiceProvider { get; set; }
}
ICachedServiceProvider
是 ABP Framework 提供的服务。它扩展了标准的 IServiceProvider
,但缓存了已解析的服务。本质上,它内部只解析一次服务,即使您多次从它那里解析服务。ICachedServiceProvider
服务本身是一个作用域服务,意味着它在一个作用域中只创建一次。我们可以使用它来优化服务解析,但是,标准的 IServiceProvider
也能正常工作。
接下来,我们可以为 Product
实体实现 IInjectServiceProvider
。
public class Product : AuditedAggregateRoot<Guid>, IInjectServiceProvider
{
public ICachedServiceProvider ServiceProvider { get; set; }
//...
}
我将在稍后解释如何设置 ServiceProvider
属性,但首先看一下如何在 Product.ChangeCodeAsync
方法中使用它。这是最终的 Product
类。
public class Product : AuditedAggregateRoot<Guid>, IInjectServiceProvider
{
public string ProductCode { get; internal set; }
public string Name { get; private set; }
public ICachedServiceProvider ServiceProvider { get; set; }
private Product()
{
/* This constructor is used by EF Core while
getting the Product from database */
}
/* Primary constructor that should be used in the application code */
public Product(string productCode, string name)
{
ProductCode = Check.NotNullOrWhiteSpace(productCode, nameof(productCode));
Name = Check.NotNullOrWhiteSpace(name, nameof(name));
}
public async Task ChangeCodeAsync(string newProductCode)
{
Check.NotNullOrWhiteSpace(newProductCode, nameof(newProductCode));
if (newProductCode == ProductCode)
{
return;
}
var productRepository = ServiceProvider
.GetRequiredService<IRepository<Product, Guid>>();
if (await productRepository.AnyAsync(x => x.ProductCode == newProductCode))
{
throw new ApplicationException
("Product code is already used: " + newProductCode);
}
ProductCode = newProductCode;
}
}
ChangeCodeAsync
方法从 ServiceProvider
获取产品存储库,并使用它来检查是否还有其他产品具有给定的 newProductCode
值。
现在,我们来解释如何设置 ServiceProvider
值……
Entity Framework Core 配置
Entity Framework 7.0 引入了 IMaterializationInterceptor
拦截器,该拦截器允许我们在实体对象作为数据库查询结果被创建后立即对其进行操作。
我们可以编写以下拦截器,如果实体实现了 IInjectServiceProvider
接口,则设置实体的 ServiceProvider
属性。
public class ServiceProviderInterceptor : IMaterializationInterceptor
{
public object InitializedInstance(
MaterializationInterceptionData materializationData,
object instance)
{
if (instance is IInjectServiceProvider entity)
{
entity.ServiceProvider = materializationData
.Context
.GetService<ICachedServiceProvider>();
}
return instance;
}
}
解析服务的生命周期与相关 DbContext
实例的生命周期相关联。因此,您无需担心解析的依赖项是否被释放。ABP 的 工作单元 系统在工作单元完成后会自动释放 DbContext
实例。
一旦我们定义了这样的拦截器,我们就应该配置我们的 DbContext
类来使用它。您可以通过在 DbContext
类中重写 OnConfiguring
方法来实现这一点。
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder.AddInterceptors(new ServiceProviderInterceptor());
}
最后,您应该在 DbContext
的实体映射配置中忽略 ServiceProvider
属性(因为我们不想将其映射到数据库表字段)。
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// ...
builder.Entity<Product>(b =>
{
// ...
/* We should ignore the ServiceProvider on mapping! */
b.Ignore(x => x.ServiceProvider);
});
}
就是这样。从现在开始,EF Core 将为您设置 ServiceProvider
属性。
手动创建实体
虽然 EF Core 在从数据库获取实体时会无缝地设置 ServiceProvider
属性,但您在自己创建新实体时仍需要手动设置它。
示例:创建新的 Product 实体时设置 ServiceProvider
属性
public async Task CreateAsync(CreateProductDto input)
{
var product = new Product(input.ProductCode, input.Name)
{
ServiceProvider = _cachedServiceProvider
};
await _productRepository.InsertAsync(product);
}
在这里,您可能会认为设置 ServiceProvider
不是必需的,因为我们没有使用 ChangeCodeAsync
方法。您完全正确;在此示例中不需要,因为可以清楚地看到实体对象在创建实体和将其保存到数据库之间没有被使用。但是,如果您在将实体插入数据库之前调用其方法,或将其传递给其他服务,您可能不知道是否需要 ServiceProvider
。因此,您应该谨慎使用它。
基本上,我介绍了问题和解决方案。在下一节中,我将解释该设计的一些局限性和我的一些其他想法。
讨论
在本节中,我将首先讨论一种获取服务的略有不同的方法。然后,我将解释将服务注入实体中的局限性和问题。
为什么注入服务提供程序,而不是服务本身?
作为一个显而易见的问题,您可能会问为什么我们属性注入了一个服务提供程序对象,然后手动解析了服务。我们不能直接属性注入我们的依赖项吗?
示例:属性注入 IRepository<Product, Guid>
服务
public class Product : AuditedAggregateRoot<Guid>
{
// ...
public IRepository<Product, Guid> ProductRepository { get; set; }
public async Task ChangeCodeAsync(string newProductCode)
{
Check.NotNullOrWhiteSpace(newProductCode, nameof(newProductCode));
if (newProductCode == ProductCode)
{
return;
}
if (await ProductRepository.AnyAsync(x => x.ProductCode == newProductCode))
{
throw new ApplicationException
("Product code is already used: " + newProductCode);
}
ProductCode = newProductCode;
}
}
现在,我们无需实现 IInjectServiceProvider
接口并手动从 ServiceProvider
解析 IRepository<Product, Guid>
对象。您会看到 ChangeCodeAsync
方法现在简单多了。
那么,如何设置 ProductRepository
呢?对于 EF Core 拦截器部分,您可以通过反射以某种方式获取实体的所有公共属性。然后,对于每个属性,检查是否存在这样的服务,如果存在,则从依赖注入系统设置它。当然,这性能会稍差一些,但如果真的可以实现的话,它会起作用。另一方面,在使用 new
关键字手动创建实体时,设置实体所有依赖项会更加困难。所以,我个人不推荐这种方法。
限制
一个重要的局限性是您无法在实体构造函数代码中使用服务。理想情况下,Product
类的构造函数应检查产品代码是否已被使用。请看下面的构造函数。
public Product(string productCode, string name)
{
ProductCode = Check.NotNullOrWhiteSpace(productCode, nameof(productCode));
Name = Check.NotNullOrWhiteSpace(name, nameof(name));
/* Can not check if product code is already used by another product? */
}
这里不可能使用产品存储库,因为:
- 服务是属性注入的。这意味着它们将在对象创建完成后设置。
- 即使服务可用,在构造函数中调用异步代码也并非真正可行。您知道 C# 中的构造函数不能是异步的,但是存储库和其他服务方法通常设计为异步的。
因此,如果您想强制执行“产品代码必须唯一”的规则,您应该创建一个异步领域服务方法(例如 ProductManager.CreateAsync(...)
),并始终使用它来创建产品(您可以将 Product
类的构造函数设置为 internal
,以防止在应用程序层使用它)。
设计问题
除了技术限制之外,将实体与外部服务耦合通常被认为是一种不良设计。它会使您的实体过于复杂,难以测试,并且通常会导致随着时间的推移承担过多的责任。
结论
在本文中,我试图探讨将服务注入实体类的各个方面。我解释了如何使用 Entity Framework 7.0 的 IMaterializationInterceptor
在从数据库获取实体时实现属性注入模式。
将服务注入实体似乎是强制执行实体中某些业务规则的一种确定方法。但是,由于当前的技术限制、设计问题和使用困难,我不建议在实体中依赖服务。取而代之的是,在需要实现依赖于外部服务和实体的业务规则时创建领域服务。
源代码
- 您可以在 这里 找到示例应用程序的完整源代码。
- 您可以在 这个 pull request 中看到我在创建应用程序后所做的更改。
历史
- 2023 年 1 月 6 日:初始帖子