Skip to main content

Base Domain Type

Here are notes about the base domain type, its usage, and design choices.

NOTE: Some of these are restrictions of the C# compiler, that makes it necessary to include some boilerplate in domain objects.

Design Requirements

Here are design requirements for our domain base:

  • Derived type instances shall be fully-formed at construction
  • Support persistence by EF and Dapper
  • Support persistence by In-Memory backends
  • Derived type instances must be reference-able at construction
  • Include a protected constructor that accepts an Id
  • Include a parameter-less, protected constructor
  • Include creation and modified timestamp
  • Include a setter for the Modified time

The above requirements come from these constraints:

Be Fully Formed

We want domain entities to be fully-formed at construction.
This is so that our domain logic doesn't have to save an instance to a database, before it can be used or referenced.

Instead, the domain logic can treat any new instance or copy of one, to be usable in logic, without being first saved to a backend.

What this allows us to do, is to generate instances, remotely, and disconnected from a database.
And, to save those instances to the backend, as the domain logic sees fit.
Meaning: A disconnected client may create objects and work with them, while there is no connection to the bakend.

Support EF and Dapper

These two technologies require the presence of protected constructors, so that instances can be hydrated from storage, via reflection.

This means that we require a parameter-less, protected constructor in the base type.

As well. This means that the derived type also requires the presence of a paramter-less, protected constructor.
See this for details: 

 

This is required, so that instances of derived type can be fully referenced before saved to a backend.
It also means that 

 



Our domain base needs to support an In-Memory backend, that doesn't have entity change tracking.
This means that our base type need to set its own creation timestamp at construction.
It also means that the base type needs a setter for its modified timestamp, so any domain logic can update it.

Our domain base needs to include audit timestamping of when created and modified.

Our domain base needs to include identity.



  • Instances shall be considered "alive" and "exists" at construction.
    This means that an instance shall have an identity and meaningful timestamps and properties at conception.
  • Identity shall be set at construction.
    This means that the instance shall include an Id, that can be fully referenced before it is persisted to a backend.
    This allows for instances to be created, remotely and disconnected from a backend.

 


C# Compiler Problem

When the C# compiler encounters a type that derives from a base, it inspects the derived type for any constructors.

If the derived type includes a constructor, regardless of its visibility (public, protected, private), any constructors that are in the base class, will be hidden and excluded, unless explicitly called.

What makes this a problem is: EF and Dapper both require a parameter-less, protected constructor, in order to properly hydrate instances from storage.

And, EF and Dapper will have problems if they cannot identify a parameter-less, protected constructor to call.

So, our domain base type requires a parameter-less, protected constructor that effectively does nothing.

Example Base Type

Here's what a simple base domain type would look like.

    /// <summary>
    /// Base type for all domain models.
    /// Derive from this, to have a persistable domain type.
    /// This is purposely made abstract, so it's well known that the base type will not be persisted.
    /// EF will not try to map to it, directly, will not create a table for it, nor persist it directly.
    /// </summary>
    abstract public class DomainObject_v1
    {
        public Guid Id { get; protected set; }

        /// <summary>
        /// Is nullable, for DB compatibility and to allow for detached instances.
        /// Will be set when added as new object.
        /// </summary>
        public DateTimeOffset? CreationDateUTC { get; protected set; }

        /// <summary>
        /// Is nullable, for DB compatibility and to allow for detached instances.
        /// Will be set when persisted (via add or update).
        /// </summary>
        public DateTimeOffset? ModifiedDateUTC { get; protected set; }

        /// <summary>
        /// Protected parameterless constructor for EF + Dapper hydration.
        /// NOTE:   If your derived type includes any constructor, at all, then this parameter-less constructor will be hidden and not visible to EF or Dapper.
        ///         So, you will need to include a parameter-less, protected constructor in your derived type, to allow this one to be visible, IF your derived type has at least one constructor of its own.
        /// </summary>
        protected DomainObject_v1() { }

        /// <summary>
        /// Optional domain constructor.
        /// Add 
        /// </summary>
        /// <param name="id"></param>
        protected DomainObject_v1(Guid id)
        {
            Id = id;
            CreationDateUTC = DateTimeOffset.UtcNow;
        }

        /// <summary>
        /// For EF, this will be called, automatically, on SaveChanges().
        /// For non-EF, call this method when your logic updates the instance.
        /// </summary>
        public void TouchModified() => ModifiedDateUTC = DateTimeOffset.UtcNow;
    }

The base type includes a few design details, explained here.

Parameter-less Protected Constructor

The base type includes a parameter-less, protected constructor.
This is needed in the base type, so that constructors of the derived type can chain to it.
This is a compiler requirement, not an EF or Dapper requirement.

But indirectly, EF and Dapper require a parameter-less, protected constructor in the derived type.
And, that needs something to chain to, on the base, making this important.

Protected Constructor with Id Parameter

The base type includes a protected constructor that accepts an Id.

This is NOT an EF or Dapper requirement.

It is present, so that derived types can supply an Id when needed.
This would be used where domain objects can be created, remotely or disconnected from the backend, where the instance must be fully alive and reference-able without having, yet, been saved to a backend.

Id Property

This is our primary key of the object in storage.

It is how we find the instances for retrieval.

NOTE: The Id is ALWAYS accepted at construction.
Meaning: The constructor of your derived type, needs to pass the id it receives, or create an id value.

The identity (Id) is given to the base type for this reason:

  • So that instances can be considered "alive" and "exist", and be fully created (having an Id) 

or when an instance is saved to the backend.

It is also set in the instance, when hydrated from storage, via reflection.

Creation Timestamps

This property is defined on the base type, so that every derived type has tracking of when instances were created.

It's an interesting design choice, of when to set this:

  • We could set it at construction, if we consider the instance "live" for domain consistency.
  • We could set it when saved to the backend, to indicate when the instance was considered usable by any read-model or other logic.

We take a hybrid approach, that satisfies several use cases.
And in doing so, we follow these rules, for the creation time property:

  • We set the creation time at instance construction.
    This ensures that an instance is "alive" or "exists" before commit.
    And, it allows us to track audit metadata.
    This also allows for object types to function with an In-Memory backend, that has no logic to set creation timestamps during persistence.
  • We also set the creation time, when a new instance is saved to the backend.
    This is done by an override of SaveChanges(), in our DbContext type.
    This is action is only done if the object's creation time is NULL.
    And, is done to enforce correctness at persistence time.

Modified Timestamps

This property is defined on the base type, so that every derived type has tracking of when instances were updated.

It's an interesting design choice, of when to set this:

  • We could set it by a domain-logic Setter.
  • We could set it when the updated instance is saved to the backend, to indicate when the update was saved.

We take a hybrid approach, that satisfies a few use cases.
And in doing so, we follow these rules, for the modified time property:

  • We leave the modified time as NULL at instance construction.
  • We include a setter, called TouchModified(), to update the modified time when the object is updated.
    This can be called by any domain logic that mutates the instance.
    And, it allows for the domain logic to fully define when the change occurs, as an "alive" instance.
    And, it allows us to fully track audit metadata.
    This also allows for object types to function with an In-Memory backend, that has no logic to set modified timestamps during persistence.
  • We also set the modified time, when an updated instance is saved to the backend.
    This is done by an override of SaveChanges(), in our DbContext type.
    This is action is done if the change tracking logic identifies the instance as updated.

Example Derived Type