In Entity Framework 6 and/or Entity Framework Core 3+, the code-first types generated by the scaffolding (or other code-generation tools, my preference is this T4 script) are mutable classes that do not expose their
Entry<T>
state.In web-applications today, current practice is to load the required entities inside the Controller's Action and compose a View Model object, then pass the View Model off to the View. When the Controller's Action method returns the
DbContext
is disposed, so the View Model must be fully-populated and thus cannot depend on any lazy-loading to render the View.- (btw, this question is not concerned with Model Binding or anything like that)
The problem is that if the View Model class simply embeds Entity Types (which is fine when the View Model is not being used in a
<form>
!) the View has no way of knowing what data is "loaded" and what it isn't - and there's also no easy way in C# to use the type-system to express these kinds of invariants about an object's state.
For example, consider these mutable entity types (in .NET 4.8 - so we can't use nullable-reference-types):
namespace Contoso.Data
{
class Customer
{
public Int32 CustomerId { get; set; }
public String Name { get; set; }
public ICollection<Order> Orders { get; set; }
}
class Order
{
public Int32 OrderId { get; set; }
public Int32 CustomerId { get; set; }
public Customer Customer { get; set; }
}
}
And this immutable View Model for a page which is a read-only view of a Customer's Orders:
namespace Contoso.Web
{
class CustomerOrderPageViewModel : IPageViewModel
{
String IPageViewModel.PageTitle => this.CustomerSubject.Name + "'s Orders";
public CustomerOrderPageViewModel( Customer c )
{
this.CustomerSubject = c ?? throw new ArgumentNullException(nameof(c));
if( c.Orders is null ) throw new ArgumentException( "Orders is null." )
}
public Customer CustomerSubject { get; }
}
}
And this straightforward Razor view's fragment:
<table>
<tbody>
@foreach( Order order in this.Model.Customer.Orders ) {
<tr>
<td>@order.OrderId</td>
</tr>
}
</tbody>
</table>
The view expects that CustomerOrderPageViewModel.CustomerSubject
contains a Customer
with a loaded Orders
collection (not to mention not-null
too). It is not possible for the View to statically express its expectation that the collection is loaded (this would be possible with C# Code Contracts, but that's dead in the water right now - I'd love to see it come back, however).
The "solution" is to move the Orders
collection to the CustomerOrderPageViewModel
so it at least that expresses the expectation that Orders
is available - and as CustomerOrderPageViewModel
is immutable and the .Orders
collection is only set by the constructor that makes it "safe" as far as I'm concerned - but this introduces a lot of time wasting by essentially copying the data's structural definition from the Entity type to the View-Model type - which doesn't "scale" when an application could have hundreds of different views all with similar requirements. My job is in the business of automating away tedious things - so I'd rather not have tedious things to do myself!
Another problem is that the View (and View-Model) has no way of knowing if an empty CustomerSubject.Orders
collection represents a either a collection that hasn't been loaded yet (e.g. due to a bug/missing-step in the entity loading code) - or if it did actually load the collection but the Customer has not made any orders.
So I've been thinking about how we could use immutable types in C#/.NET to express invariants, and how as I'm already using a very customizable T4 template to scaffold entity types I can extend that T4 to generate state invariant types to represent the shape of loaded data.
An example based on the scenario above would involve a new type struct CustomerWithLoadedOrders
, like so (it's a struct
because invariants shouldn't participate in an inheritance hierarchy, they're immutable, and we should avoid GC heap allocations anyway):
struct CustomerWithLoadedOrders
{
public static implicit operator CustomerWithLoadedOrders( ( Customer c, IReadOnlyList<Order> o ) t ) => new CustomerWithLoadedOrders( t.c, t.o );
public static implicit operator Customer( CustomerWithLoadedOrders self ) => self.Customer;
public static implicit operator IReadOnlyList<Order>( CustomerWithLoadedOrders self ) => self.Orders;
public CustomerWithLoadedOrders( Customer c, IReadOnlyList<Order> loadedOrders )
{
this.Customer = c ?? throw new ArgumentNullException(nameof(c));
this.LoadedOrders = loadedOrders ?? throw new ArgumentNullException(nameof(loadedOrders));
}
public Customer Customer { get; }
public IReadOnlyList<Order> LoadedOrders { get; }
}
So then I'd have an extension-method on the DbContext
:
public static async Task<CustomerWithLoadedOrders> LoadCustomerWithOrdersAsync( this MyDbContext db, Int32 customerId )
{
Customer cus = await db.Customers.SingleAsync( c => c.CustomerId == customerId ).ConfigureAwait(false);
await db.Entry( cus ).Collection( c => c.Orders ).LoadAsync().ConfigureAwait(false);
return ( cus, cus.Orders ); // or just `new CustomerWithLoadedOrders( cus, cus.Orders )`
}
This looks tediuous to write my hand, but with T4 invariant types can be easily generated for each loadable member of every entity type, and T4 can also be used to generate combinations of invariants together, think of it as a worse-than-poor-man's implementation of an ADT product type (with most of the fiddly syntax pain removed and compatibility with entity types maintained with implicit conversion).
...and this works for the simpler cases involving single types or collections-of, but eventually it results in massive code-bloat (even if it is generated code) when needing to define invariants for the state of a any moderately-sized object-graph: for example, a Customer
, all their Orders
, as well as all Products
in all of those Orders
- and it quickly spirals from there. An additional pain-point is that for batch loading or customized Linq queries we can't use extension methods like LoadCustomerWithOrdersAsync
because those load single entities - so we need to rely on runtime assertions which defeats the point of using a static-type system to encode invariants in our program.
Have you run into this problem, and how did you come to a solution?