An EF Lazy Loading Fody plugin.
A Fody plugin to inject EF lazy-loader and load statements for readonly collections.
If you are going for a clean DDD implementation, you might want to encapsulate access to navigation collection properties and expose those as readonly collections only. If you now reference your backing field within your entity, there is no chance for lazy loading to kick in. For this to work you would have to pollute your entity with EF infrastructure knowledge, as described in this issue. This Fody plugin deals exactly with this problem and does the heavy lifting described in the issue mentioned.
See also Fody usage.
Install the SpatialFocus.EFLazyLoading.Fody NuGet package and update the Fody NuGet package:
PM> Install-Package Fody
PM> Install-Package SpatialFocus.EFLazyLoading.Fody
The Install-Package Fody
is required since NuGet always defaults to the oldest, and most buggy, version of any dependency.
Add <SpatialFocus.EFLazyLoading/>
to FodyWeavers.xml
<Weavers>
<SpatialFocus.EFLazyLoading/>
</Weavers>
This fody plugin performs the following actions:
- 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
orIReadOnlyCollection
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
Before code:
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);
}
What gets compiled
public class CustomerWeaved
{
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);
}
}
Classes with an already exsting lazyloader
field or any constructor with a lazyloader
named parameter will be skipped. Only classes with ReadOnlyCollection<T>
or IReadOnlyCollection<T>
navigation properties will be weaved. The generic type of these collections must be class (excluding strings, enums and value types) to be considered for weaving.
These config options are configured by modifying the SpatialFocus.EFLazyLoading
node in FodyWeavers.xml
A list of namespaces to exclude.
Can take two forms.
As an element with items delimited by a newline.
<SpatialFocus.EFLazyLoading>
<ExcludeNamespaces>
Foo
Bar
</ExcludeNamespaces>
</SpatialFocus.EFLazyLoading>
Or as a attribute with items delimited by a pipe |
.
<SpatialFocus.EFLazyLoading ExcludeNamespaces='Foo|Bar'/>
A list of namespaces to include.
Can take two forms.
As an element with items delimited by a newline.
<SpatialFocus.EFLazyLoading>
<IncludeNamespaces>
Foo
Bar
</IncludeNamespaces>
</SpatialFocus.EFLazyLoading>
Or as a attribute with items delimited by a pipe |
.
<SpatialFocus.EFLazyLoading IncludeNamespaces='Foo|Bar'/>
Use *
at the beginning or at the end of an in- or exclude for wildcard matching.
To include the namespace and all sub-namespaces, simply define it like this:
<SpatialFocus.EFLazyLoading>
<IncludeNamespaces>
Foo
Foo.*
</IncludeNamespaces>
</SpatialFocus.EFLazyLoading>
You can combine excludes and includes, excludes overrule the includes if both match.
Made with ❤️ by Spatial Focus