Open daiplusplus opened 5 years ago
I like your thinking here. An alarm bell rings for this class
// Common mutable properties
class CommonProduct
{
String Name { get; set; }
DateTime Created { get; set; }
String Manufacturer { get; set; }
}
// The "New" class is not a superclass or subclass of the main mutable entity, but is a cousin, to avoid inadvertent use of the wrong type by relying on languages' built-in covariance/contravariance handling
class NewProduct : CommonProduct
{
}
class Product : CommonProduct
{
Int32 ProductId { get; } // Immutable, as IDENTITY columns cannot be changed
}
The NewProduct
still has no ProductId
and therefore cannot be configured, however Product
can be configured with HasKey(x => x.ProductId);
. I suspect NewProduct would cause a problem as EF expects there to be a key column defined.
Great idea though, and I like it. It would be a huge help if you could have a small hand-crafted working example I could take a look at. No rush though, as I'll only get to pick this up after launch :-)
Thanks.
Background:
When designing with a functional mindset, object states tend to be immutable, with the object's design only exposing operations and data suitable for that particular state.
When objects are immutable, then their methods that put them into a new state would emit a new immutable object with those new operations available:
This is as opposed-to the old-school of OOP design where objects were mutable (not just of their trivial data-members (getter/setter scalar properties) but also what they represent, where you might have this design:
This
State
class is harder to reason about because theY
property exists and can be used by programs beforeNextState
has been called, for example - and consumers can still callNextState()
twice on the same object (granted, they'll get an exception by doing so) - whereas in the immutable example above, there isn't aGetNextState()
method onStateB
, which makes it impossible.Now - thinking about how this applies to Entity Framework - I know the idea of only working with immutable entities is a bad idea - but at the same time I don't like how the same entity class is used to represent:
IDENTITY
column values and database-computed values)SaveChanges
) - or when usingAsNoTracking()
.UPDATE
orDELETE
operation.In this case, we have 4 different states where the state objects should have different interfaces to prevent meaningless operations.
So consider a typical Orders and Products database:
Ordinarily in EF6 and EFCore, we'd get these classes (the keyword
public
omitted for brevity):But there are issues with these class designs:
IDENTITY
primary-key values are mutable, even though in most RDBMS, anIDENTITY
column is immutable.IDENTITY
(or other database-generated value) value, but those properties are still exposed - which means their consumers need to know that if their value is zero or some other magic value (-1
?) that they're "new" entities.UPDATE
andDELETE
, consumers have no way of knowing which properties are significant or not - which is why EF has theEntry<T>
type which allows access to metadata about the state of each property member.AsNoTracking()
or that we don't callSaveChanges
in the sameDbContext
.I'd like to forward some ideas for making programming with EF entities "safer" which can be achieved simply by tweaking this T4 code-generator rather than having to change anything internal to EF - while still keeping entity types as mutable classes for most cases:
Proposal 1: Separate
New
andExtant
entity subclasses:Using the same Orders and Products database example as above, the code-generator could produce separate entity classes for "new" and "extant" entities.
A "new" entity class would not have any properties for values that are generated by the database - whereas an "extant" entity class does. Note that applications are still free to create a new instance of an "extant" entity object in-memory and then attach it to a
DbContext
to use as a "partial" entity as they can do today.So we'd have:
DbContext
'sDbSet<Product>.Add
would then be modified like so:Add
only acceptsNewProduct
, so applications cannot accidentally try to add an extant Product.Add
returns aTask<Product>
that resolves to the fully valid extantProduct
instance onceSaveChanges
orSaveChangesAsync
is called, like so:(I appreciate this is more verbose than the current code, but I think the added safety makes it worthwhile)
Meanwhile the
DbSet<Product>.Attach
method remains the same:Proposal 2A: Adding interfaces for read-only views of objects:
Following the example above, we add a new interface:
Note that the interface is named
IReadOnlyProduct
because it provides a read-only view of a mutable object (so technically another thread could concurrently mutate the underlyingProduct
object which would mess things up for the consumer of theIReadOnlyProduct
- but it does mean that library authors don't need to worry about code accidentally mutating a mutable objects. It's also keeping with convention of otherIReadOnly...
types in the .NET standard libraries.But as the mutable object is still returned, consumers could perform a valid cast and then mutate the object - but that's clearly intentional, whereas this post is more about preventing unintentional behaviour (i.e. bugs) through the use of appropriate types.
Proposal 2B: Truly immutable entity types:
A more stringent version of the above, this requires separate classes be defined (as immutable and mutable representations of the same aggregate cannot be derived from each other.
The EF
DbContext
'sDbSet<T>
'sAsNoTracking()
extension method would be modified to return the immutable type (or just returnIReadOnly...
interfaces, which the immutable class implements anyway). UsingAsNoTracking()
has demonstrable performance benefits and returning an immutable object would help programmers understand whySaveChanges()
doesn't work when changing the properties ofAsNoTracking()
entity objects.Proposal 3: Read-only
DbContext
interface.Somewhat related - I'd like it if the
DbContext
class only offered read-only operations at first, and that to perform operations that would be applied to the backing-store (i.e.SaveChanges
/SaveChangesAsync
) you would need a separateDbContext
instance - or some kid of "unit-of-work" class.I'm asking for this because when using ASP.NET's dependency-injection to inject a HTTP request-scoped
DbContext
instance, it's easy to forget how expensiveSaveChanges
calls are after pulling-in loads of entities but having only modified a few entities.Something like this:
Would be this instead:
Proposal 4: Entity key structs:
Now this proposal is not concerned with any functional programming or concepts, but a little request that's been in my head for a while.
As C# does not support "strong" type-aliasing (only syntactical weak type-aliasing
using This = That;
) we need to define our own types.Using a separate strong type for each entity's key means we can avoid bugs where entity keys are confused - for example:
I propose that each code-gen'd entity with a primary-key defined have a primary-key type defined and added to the entity type's interface.
or for composite keys:
This would work with the earlier proposals where the immutable
Order.OrderId
property is now an instance ofOrderId
(the type) and the underlyingInt32
values exposed only by also-immutable properties ofOrderId
....and the
db.Products.SingleOrDefault( p => o.ProductId == orderId );
line would give a compiler error becauseclass ProductId
isn't comparable toclass OrderId
.