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.
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.
Note that a slightly different argument applies to non-HTML formats, e.g. a WPF app; but it's the same conclusion in the end. Here, at the end of a rendering a view the entity does not necessarily descope yet (because it can and most likely will be kept in memory). However, the only way in which your rendered view impacts the entity is if you set up a binding. And it makes no sense to set up the binding for the content of an immutable object. If it's immutable, then you wouldn't be able to reflect any changes made to the data anyway.
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? I've heard argument either way. You cannot alter a CustomerOrderPageViewModel
instance. However, it's still possible to indirectly alter the data contained in this CustomerOrderPageViewModel
instance:
var customer = new Customer();
var model = new CustomerOrderPageViewModel(customer);
var modelName_1 = model.CustomerName;
customer.Name = "Different";
var modelName_2 = model.CustomerName;
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 you are liable to repeat that mistake even if you were to add a layer of indirection.