DDD
30th July 2021
by Christopher Dresel

Keep navigation properties of type IReadOnlyCollection<T> small and simple with SpatialFocus.EFLazyLoading.Fody.

About this series

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 issue

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:

  • You have to add ILazyLoader or Action<object, string?> to your constructor and store it into a field
  • For each collection property you need an additional intermediate property which triggers lazy loading
  • (Almost) All accesses to the backing field must be done via this intermediate property

This 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.

The solution

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:

  • Adds a field private readonly Action<object, string>? lazyLoader to your entity
  • Overloads existing constructors with an additional Action<object, string>? lazyLoader parameter
  • For every ReadOnlyCollection or IReadOnlyCollection it finds the corresponding backing field (propertyname, _propertyname, m_propertyname)
  • For every access (including methods & properties) to this backing field (except the navigation property itself), it adds the corresponding this.lazyLoader?.Invoke statement

This 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.

The result

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.

Summary

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.