Keep navigation properties of type IReadOnlyCollection<T> small and simple with SpatialFocus.EFLazyLoading.Fody.
Keep navigation properties of type IReadOnlyCollection<T> small and simple with SpatialFocus.EFLazyLoading.Fody.
When designing a clean DDD solution, many times you need to pollute your domain with infrastructure code in order to get Entity Framework (EF) working. This blog series will look at some of these issues and how to resolve them.
The IReadOnlyCollection<T>
interface comes in handy when you want to prevent direct modification of a collection property - which you want to if you aim for DDD purity. It's not so easy to integrate this into Entity Framework though, especially in combination with lazy loading. If you then reference your backing field within your entity (e.g. to add/remove an item or clear the collection), there is no chance for lazy loading to kick in. See this suggestion by one of the EF Core maintainers on how we could implement this for our car sample:
public class CarHolder
{
private readonly Action<object, string?>? lazyLoader;
private List<Car> cars = new();
protected CarHolder(Action<object, string?> lazyLoader)
{
this.lazyLoader = lazyLoader;
}
public virtual IReadOnlyCollection<Car> Cars => InternalCars.AsReadOnly();
public int CarsCount => InternalCars.Count;
protected List<Car> InternalCars => this.lazyLoader.Load(this, ref this.cars, nameof(CarHolder.Cars));
public void AddCar(Car car) => InternalCars.Add(car);
}
There are a few issues with this approach:
ILazyLoader
or Action<object, string?>
to your constructor and store it into a fieldThis is quite cumbersome and painful to do. Also this adds a lot of infrastructure logic to our domain which we want to avoid as part of this series.
This is not something for the average Joe but challenges like this are ideal candidates for IL weaving (if you want to spend some time :)). If you do not know what IL weaving is, there is plenty of information out there.
We did spend some time and came up with our EFLazyLoading.Fody NuGet package.
What it does is best explained by this little example:
public class Customer
{
private readonly List<Order> orders = new();
public Customer(string name)
{
Name = name;
}
public int NumberOfOrders => this.orders.Count;
public virtual IReadOnlyCollection<Order> Orders => this.orders.AsReadOnly();
public int Id { get; protected set; }
public string Name { get; protected set; }
public void AddOrder(Order order) => this.orders.Add(order);
public void ClearOrders() => this.orders.Clear();
public void RemoveOrder(Order order) => this.orders.Remove(order);
}
This is how I would model my domain if I could ignore any EF or lazy loading pitfalls. EFLazyLoading.Fody adds all the boilerplate which is necessarily to let this work as expected:
private readonly Action<object, string>? lazyLoader
to your entityAction<object, string>? lazyLoader
parameterReadOnlyCollection
or IReadOnlyCollection
it finds the corresponding backing field (propertyname
, _propertyname
, m_propertyname
)this.lazyLoader?.Invoke
statementThis is how the class would look like after the weaving:
public class Customer
{
private readonly Action<object, string>? lazyLoader;
private readonly List<Order> orders = new();
public CustomerWeaved(string name)
{
Name = name;
}
// For every constructor a constructor overload with Action<object, string> lazyLoader will be added,
// and the original constructor will be called
// See https://docs.microsoft.com/en-us/ef/core/querying/related-data/lazy#lazy-loading-without-proxies
protected CustomerWeaved(string name, Action<object, string> lazyLoader) : this(name)
{
this.lazyLoader = lazyLoader;
}
public int NumberOfOrders
{
get
{
this.lazyLoader?.Invoke(this, "Orders");
return this.orders.Count;
}
}
// Access via navigation property will trigger default lazy loading behaviour
public virtual IReadOnlyCollection<Order> Orders => this.orders.AsReadOnly();
public int Id { get; protected set; }
public string Name { get; protected set; }
public void AddOrder(Order order)
{
this.lazyLoader?.Invoke(this, "Orders");
this.orders.Add(order);
}
public void ClearOrders()
{
this.lazyLoader?.Invoke(this, "Orders");
this.orders.Clear();
}
public void RemoveOrder(Order order)
{
this.lazyLoader?.Invoke(this, "Orders");
this.orders.Remove(order);
}
}
If you want to check the internals, the most relevant code can be found here.
Instead of polluting our domain entities with a lot of EF and lazy loading workarounds, we end up with a clean domain entity class and slim and clear functions:
public class CarHolder
{
private readonly List<Car> cars = new();
public IReadOnlyCollection<Car> Cars => this.cars.AsReadOnly();
public int CarsCount => this.cars.Count;
public void AddCar(Car car) => this.cars.Add(car);
}
The EFLazyLoading.Fody might not cover all use cases, so if you find something not working feel free to open an issue in the Github repository.
The part 5 concludes our series on DDD and clean-code development. We have looked at some of the most obvious issues, where infrastructure-related code spoils our domain model. We described the issues, the potential solutions and showed the packages that help to make your domain model cleaner, simpler and less error prone. Hopefully you enjoyed reading about it and can make use of some or all of the concepts in one of your next projects.