4
  • 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?

0

2 Answers 2

4

Indirection

There is a significant difference between an entity and a viewmodel. You've already pointed one out: entities have no inherent requirements of linked entities to exist/be loaded, but in this case your viewmodel does.
Since your viewmodel is the one who causes this requirement to exist, putting it in the viewmodel is precisely the place it should be enshrined.

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

This is the age-old mantra of bad practice justifications. I want to cut this corner because I it's a waste of time. Or, more aptly put, anything that takes longer than the shortest thing I can think of is therefore a needless waste of time.

Not only should you take the time and effort to separately declare the structure of an entity and a viewmodel, regardless of any similarities they may share; but you also seem to have skipped one or two extra layers of indirection, i.e. the database DTO and business DTO.

This is where we get to differing opinions on where the bar is set. Some advocate that you always need all layers. Some advocate a more contextual approach. But I think you'll get a near unanimous agreement that entities and viewmodels are the absolute barebones requirements for indirection. I would personally suggest having at least one additional layer of indirection in there, but that's a contextual inference on my part about the scope and complexity of your application.

if the View Model class simply embeds Entity Types (which is fine when the View Model is not being used in a <form>!)

I have no idea where you got this idea, I have never heard of this argument before, and I disagree with it wholly.

Whether or not your data ends up in a form is completely irrelevant. Any data you send to a browser gets sent as HTML (since you're talking about <form>) and inherently loses all connection to the entity that was the source of the data.

If the user ends up making a second request, this time containing some form data that just happens to be the same as the entity you used for the first request, that's a completely separate request, with its own scope, and no bearing on the entity from the first event.

which doesn't "scale" when an application could have hundreds of different views all with similar requirements

We've now hit on the second mantra of bad practice justification. I could decide to not cut this corner, but there are many corners ahead, and not cutting any corner is a waste of time.

Every class exists because it has its own defined purpose. Different purpose? Different class. Same purpose? Same class.

Note the difference between a class' purpose and its structure. Two classes may exist with the exact same class structure, but each serving a different purpose (DTOs are a common example here). Overzealous refactoring may lead you to condense these two classes together as their structure is the same, but that is a bad practice mistake. Each class serves its own purpose and therefore each class has its own reason to exist.

The size of an application is not justification for cutting corners. There is not a single good practice rule that has a "unless you have a lot of classes" kind of exception to it.

Just imagine if you find out that the contractor building your house skipped parts of the building plan "because they have a lot of houses to build". The building plan is to be followed, regardless of how many building plans exist.
"I had to do many" is just not a valid argument. But often, we only care about that when we personally suffer from the corner-cutting, rather than when we're the one doing it for our own benefit (i.e. less work).

An example based on the scenario above would involve a new type struct CustomerWithLoadedOrders

You've basically just reinvented the DTO.

It's interesting to see that you first balk at the thought of having to develop a separate type (think of the work it will cause), and then solve the problem by... developing a separate type.

Credit where credit is due, you have written a great question here. You've elaborated on all the considerations and your thought process. You've done your due diligence and I would definitely not accuse you of not thinking things through.

But your eloquence means you're clever, and clever is dangerous in the field of clean coding. Clean code is boring, but boring is good. Being clever can become your undoing, when it leads you to try and cleverly work around a commonly accepted feature, somehow end up doing it anyway (just with a different name), and then not realize that you've done the same thing you were avoiding, and it's now needlessly more complex because its atypical and therefore harder to understand.

Take this from me, someone who is infinitely attracted to clever solutions, that the unsexy truth is that good practice and cleverness rarely go hand in hand. There's a reason why Code Golf has zero overlap with any good practice principle.


Immutability

And this immutable View Model for a page

class CustomerOrderPageViewModel : IPageViewModel
{
    public Customer CustomerSubject { get; }
}

If Customer is mutable, then given its class definition, CustomerOrderPageViewModel is also mutable, since you can do things like this:

var model = new CustomerOrderPageViewModel(new Customer());

model.CustomerSubject.Name = "I can totally change this";

Immutability can only be built on immutability. An interesting discussion appears when you consider if Customer had not been publically accessible from the viewmodel, e.g.:

class CustomerOrderPageViewModel : IPageViewModel
{
    private readonly Customer customer;
    public Customer CustomerName => this.customer.Name;
}

Is this immutable? You cannot alter a CustomerOrderPageViewModel instance, so it must be immutable, right?

However, it's still possible to indirectly alter the data contained in this CustomerOrderPageViewModel instance:

var customer = new Customer() { Name = "Original" };

var model = new CustomerOrderPageViewModel(customer);
var modelName_1 = model.CustomerName;

customer.Name = "Different";
var modelName_2 = model.CustomerName;

var isImmutable = modelName_1 == modelName_2; // false

If the same property of the same instance can yield different values, then the object is by definition mutable.

This answer explores the issue better than I can, but the gist of it is that the only way to have an immutable class that is based on a mutable reference type (i.e. the entity) is to (a) obviously not expose any mutable types as properties and (b) copy all the needed data in the constructor, so that any future changes made to the instance of the mutable reference type no longer cause changes to the content of your immutable instance.

Note that when you do actually implement the indirection described above, and your viewmodels and entities are separate, then you can in fact achieve immutability easily here. I just wanted to point out that what you call immutability is not in fact immutability, because otherwise you are liable to repeat that mistake even if you were to add a layer of indirection.

2

DAL in my MVC?!

First of all, I would like to comment on the usage of DAL entities as types in your UI.

Personally, I don't like it: your UI suddenly has to care about the internal organization of your data.

For example, you expose a CustomerId field to your UI. This is a purely service field: its only purpose is to make your database capable of telling objects apart. It doesn't even have any real meaning behind it. So now your UI has to be intimately familiar with elements of your DB implementation that have nothing to do with business logic.

Eager Loading

That aside, I also want to point out that your Customer.Orders is not marked as virtual, so it shouldn't use lazy loading unless you are doing some external work to make it happen. The collection should either not be loaded at all, or it should be explicitly included via eager loading (Include).

Failure to Deliver

Now, as for how View or View Model should interpret an empty collection: is it such a big issue? Does it happen so often, that you need to handle it?

Even if it is, I am not sure what you intend your View to do about it.

Let's say your View/View Model can tell that the data retrieval was indeed botched and the empty collection is the result of that.

So what?

How is this any concern of a View or a View Model? Are they going to halt execution of UI and demand a second trip to the database?

Even if you consider logging an error, it shouldn't really be something that View or View Model do. It is up to Controller to check the outcome of data loading—as you say yourself the View Model must be fully-populated by the time Controller is done.

Solution

I suggest separating your ViewModel-class from your Entities and then using something like an Automapper to connect the two. It is what its for, after all.

Load your collections eagerly in your controller. If data loading does result in failure so often (and doesn't throw exceptions left, right, and center before quietly giving an empty collection to your controller) then you should probably include a count tracker as a separate column/field you request.

Don't think of it as duplicating data. Consider it an optimization. After all, it is a common practice for mostly read-oriented databases to do away with data purity for the sake of performance.

1
  • While I agree with the overall answer, CustomerId is maybe not the best example here. Any form that updates customer data is obviously going to need to know the ID of the customer it tries to update.
    – Flater
    Commented Jan 12, 2021 at 10:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.