This article investigates all aspects of injecting services into entity classes and explains how to use Entity Framework 7.0 IMaterializationInterceptor to implement property-injection pattern while getting entities from database.
Introduction
Dependency injection is a widely-used pattern of obtaining references to other services from our classes. It is a built-in feature when you develop ASP.NET Core applications. In this article, I will explain why we may need to have references to other services in an entity class and how we can implement Entity Framework Core's new IMaterializationInterceptor
interface to provide these services to the entities using the standard dependency injection system.
I will use the ABP Framework to implement the application code.
The Problem
While developing applications based on Domain-Driven Design (DDD) patterns, we typically write our business code inside application services, domain services and entities. Since the application and domain service instances are created by the dependency injection system, they can inject services into their constructors.
Here is an example domain service that injects a repository into its constructor:
public class ProductManager : DomainService
{
private readonly IRepository<Product, Guid> _productRepository;
public ProductManager(IRepository<Product, Guid> productRepository)
{
_productRepository = productRepository;
}
}
ProductManager
can then use the _productRepository
object in its methods to perform its business logic. In the following example, ChangeCodeAsync
method is used to change a product's code (the ProductCode
property) by ensuring uniqueness of product codes in the system:
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;
}
}
Here, the ProductManager
forces the rule "product code must be unique". Let's see the Product
entity class too:
public class Product : AuditedAggregateRoot<Guid>
{
public string ProductCode { get; internal set; }
public string Name { get; private set; }
private Product()
{
}
public Product(string productCode, string name)
{
ProductCode = Check.NotNullOrWhiteSpace(productCode, nameof(productCode));
Name = Check.NotNullOrWhiteSpace(name, nameof(name));
}
}
You see that the ProductCode
property's setter is internal
, which makes possible to set it from the ProductManager
class as shown before.
This design has a problem: We had to make the ProductCode
setter internal
. Now, any developer may forget to use the ProductManager.ChangeCodeAsync
method, and can directly set the ProductCode
on the entity. So, we can't completely force the "product code must be unique" rule.
It would be better to move the ChangeCodeAsync
method into the Product
class and make the ProductCode
property's 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;
}
if (await _productRepository.AnyAsync(x => x.ProductCode == newProductCode))
{
throw new ApplicationException
("Product code is already used: " + newProductCode);
}
ProductCode = newProductCode;
}
}
With that design, there is no way to set the ProductCode
without applying the rule "product code must be unique". Great! But we have a problem: An entity class cannot inject dependencies into its constructor, because an entity is not created using the dependency injection system. There are two common points of creating an entity:
- We can create an entity in our application code, using the standard
new
keyword, like var product = new Product(...);
. - Entity Framework (and any other ORM / database provider) creates entities after getting them from the database. They typically use the empty (default) constructor of the entity to create it, then sets the properties coming from the database query.
So, how can we use the product repository in the Product.ChangeCodeAsync
method? If we forget the dependency injection system, we would think of adding the repository as a parameter to the ChangeCodeAsync
method and delegate the responsibility of obtaining the service reference to the caller of that method:
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;
}
However, that design would make it hard to use the ChangeCodeAsync
method, and also exposes its internal dependencies to outside. If we need another dependency in the ChangeCodeAsync
method later, we should add another parameter, which will affect all the application code that uses the ChangeCodeAsync
method. I think that's not reasonable. The next section offers a better and a more generic solution to the problem.
The Solution
First of all, we can introduce an interface that should be implemented by the entity classes which need to use services in their methods:
public interface IInjectServiceProvider
{
ICachedServiceProvider ServiceProvider { get; set; }
}
ICachedServiceProvider
is a service that is provided by the ABP Framework. It extends the standard IServiceProvider
, but caches the resolved services. Basically, it internally resolves a service only a single time, even if you resolve the service from it multiple times. The ICachedServiceProvider
service itself is a scoped service, means it is created only once in a scope. We can use it to optimize the service resolution, however, the standard IServiceProvider
would work as expected.
Next, we can implement the IInjectServiceProvider
for our Product
entity:
public class Product : AuditedAggregateRoot<Guid>, IInjectServiceProvider
{
public ICachedServiceProvider ServiceProvider { get; set; }
}
I will explain how to set the ServiceProvider
property later, but first see how to use it in our Product.ChangeCodeAsync
method. Here is the final Product
class:
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()
{
}
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;
}
}
The ChangeCodeAsync
method gets the product repository from the ServiceProvider
and uses it to check if there is another product with the given newProductCode
value.
Now, let's explain how to set the ServiceProvider
value...
Entity Framework Core Configuration
Entity Framework 7.0 introduces the IMaterializationInterceptor
interceptor that allows us to manipulate an entity object just after the entity object is created as a result of database query.
We can write the following interceptor that sets the ServiceProvider
property of an entity, if it implements the IInjectServiceProvider
interface:
public class ServiceProviderInterceptor : IMaterializationInterceptor
{
public object InitializedInstance(
MaterializationInterceptionData materializationData,
object instance)
{
if (instance is IInjectServiceProvider entity)
{
entity.ServiceProvider = materializationData
.Context
.GetService<ICachedServiceProvider>();
}
return instance;
}
}
Lifetime of the resolved services are tied to the lifetime of the related DbContext
instance. So, you don't need to care if the resolved dependencies are disposed. ABP's unit of work system already disposes the DbContext
instance when the unit of work is completed.
Once we defined such an interceptor, we should configure our DbContext
class to use it. You can do it by overriding the OnConfiguring
method in your DbContext
class:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder.AddInterceptors(new ServiceProviderInterceptor());
}
Finally, you should ignore the ServiceProvider
property in your entity mapping configuration in your DbContext
(because we don't want to map it to a database table field):
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<Product>(b =>
{
b.Ignore(x => x.ServiceProvider);
});
}
That's all. From now, EF Core will set the ServiceProvider
property for you.
Manually Creating Entities
While EF Core seamlessly set the ServiceProvider
property while getting entities from database, you should still set it manually while creating new entities yourself.
Example: Set ServiceProvider
property while creating a new Product entity:
public async Task CreateAsync(CreateProductDto input)
{
var product = new Product(input.ProductCode, input.Name)
{
ServiceProvider = _cachedServiceProvider
};
await _productRepository.InsertAsync(product);
}
Here, you may think that it is not necessary to set the ServiceProvider
, because we haven't used the ChangeCodeAsync
method. You are definitely right; It is not needed in this example, because it is clear to see the entity object is not used between the entity creation and saving it to the database. However, if you call a method of the entity, or pass it to another service before inserting into the database, you may not know if the ServiceProvider
will be needed. So, you should carefully use it.
Basically, I've introduced the problem and the solution. In the next section, I will explain some limitations of that design and some of my other thoughts.
Discussions
In this section, I will first discuss a slightly different way of obtaining services. Then I will explain limitations and problems of injecting services into entities.
Why Injected a Service Provider, but Not the Services?
As an obvious question, you may ask why we've property-injected a service provider object, then resolved the services manually. Can't we directly property-inject our dependencies?
Example: Property-inject the IRepository<Product, Guid>
service:
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;
}
}
Now, we don't need to implement the IInjectServiceProvider
interface and manually resolve the IRepository<Product, Guid>
object from the ServiceProvider
. You see that the ChangeCodeAsync
method is much simpler now.
So, how to set ProductRepository
? For the EF Core interceptor part, you can somehow get all public properties of the entity via reflection. Then, for each property, check if such a service does exist, and set it from the dependency injection system if available. Surely, that will be less performant, but will work if you can truly implement. On the other hand, it would be extra hard to set all the dependencies of the entity while manually creating it using the new
keyword. So, personally, I wouldn't recommend that approach.
Limitations
One important limitation is that you cannot use the services inside your entity's constructor code. Ideally, the constructor of the Product
class should check if the product code is already used before. See the following constructor:
public Product(string productCode, string name)
{
ProductCode = Check.NotNullOrWhiteSpace(productCode, nameof(productCode));
Name = Check.NotNullOrWhiteSpace(name, nameof(name));
}
It is not possible to use the product repository here, because;
- The services are property-injected. That means they will be set after the object creation has completed.
- Even if the service is available, it won't be truly possible to call async code in a constructor. You know constructors cannot be async in C#, but the repository and other service methods are generally designed as async.
So, if you want to force the "product code must be unique" rule, you should create an async domain service method (like ProductManager.CreateAsync(...)
) and always use it to create products (you can make the Product
class constructor internal
to not allow to use it in the application layer).
Design Problems
Beside the technical limitations, coupling your entities to external services is generally considered as a bad design. It makes your entities over-complicated, hard to test, and generally leads to take too much responsibility over the time.
Conclusion
In this article, I tried to investigate all aspects of injecting services into entity classes. I explained how to use Entity Framework 7.0 IMaterializationInterceptor
to implement property-injection pattern while getting entities from database.
Injecting services into entities seems a certain way of forcing some business rules in your entities. However, because of the current technical limitations, design issues and usage difficulties, I don't suggest to depend on services in your entities. Instead, create domain services when you need to implement a business rule that depends on external services and entities.
The Source Code
- You can find the full source code of the example application here.
- You can see this pull request for the changes I've done after creating the application.
History
- 6th January, 2023: Initial post