Domain-Driven Design Entity Mapping Strategies
Image by Gerd Altmann from Pixabay
Introduction
Previously, I discussed a cross-stack methodology for persisting exception information from deep within the server-side application to the UI app running in the user’s browser. In this story, we’re going to circle back and discuss the importance of keeping a separation between various entity types within a DDD application, the challenges that presents, and elegant mapping strategies we can use to make it work. First, however, I’m going to briefly touch on some key enhancements to the C# language that were released with version 9, and how those influence our architecture from a Domain-Driven Design perspective. Alongside the blog entry in which I laid out my Clean DDD architectural template in detail, this is one of the most important that I’ve written to date, so I hope you enjoy it and find the knowledge from this to be useful.
C# 9 Enhancements — The F#-ification Continues…
Having been programming F# for nearly a decade now, I find it amusing, though at times unnerving, how more and more features from F# end up in each new release of C#. Sometimes these features smoothly transition over, though (as with switch expressions and their F# counterpart, match expressions) this is not always the case. I’ll definitely comment more in the future. However, suffice it to say, the new features of C# 9 will impact our DDD implementation.
Records as Value Objects
I plan to go into more detail in a later blog entry on the nuances of C# records, and how they square up against F# records, which have been canonical in that language since the beginning. However, at this time I will simply state that, for the purposes of the demo application, I chose to reimplement the Value Object classes as records. Please note that this is at odds with the recommendations of other DDD authorities that I follow, such as Vladimir Khorikov, who gives good reasons in this blog entry why records are NOT well-suited to Value Object implementations.
Once I figured out how the new record types work in C#, the carrot was simply too tempting for me to resist, knowing that I could turn this…
public class Address : ValueObject
{
public string City { get; }
public State State { get; }
public string Street1 { get; }
public string Street2 { get; }
public ZipCode Zip { get; } public Address(string street1, string city, State state, ZipCode zip, string street2 = null)
{
Street1 = street1 ?? throw new ArgumentNullException(nameof(street1));
City = city ?? throw new ArgumentNullException(nameof(city));
State = state ?? throw new ArgumentNullException(nameof(state));
Zip = zip ?? throw new ArgumentNullException(nameof(zip));
Street2 = string.IsNullOrWhiteSpace(street2) ? "" : street2;
} public override string ToString()
{
var apt = !string.IsNullOrWhiteSpace(Street2) ? " " + Street2 : "";
return $"{Street1}{apt}, {City}, {State} {Zip}";
} protected override IEnumerable<object> GetAtomicValues()
{
yield return Street1;
yield return Street2;
yield return City;
yield return State;
yield return Zip;
}
}
… into this:
public record Address(string Street1, string City, State State, ZipCode ZipCode, string? Street2 = default) : IValueObject
{
public override string ToString()
{
var apt = !string.IsNullOrWhiteSpace(Street2) ? " " + Street2 : "";
return $"{Street1}{apt}, {City}, {State} {ZipCode}";
}
}
If this turns out to be a misguided decision, then we can always pivot back to a base class implementation, like what was in place earlier. That’s the beauty of software architecture, as opposed to real life, building architecture — in the software world, the “bricks” don’t cost anything. You must simply be mindful of your time, and if you screw up, learn from your mistake and then change course quickly.
Init-Only Setters
Another C# 9 feature that was released alongside records is init-only setters. In my opinion, this feature has been needed for a while, and once I started using it, it felt like a breath of fresh air. Why? Because it allows us to more elegantly declare Domain entity types that are inherently immutable, without needing to lean on constructors for property initialization.
Let’s examine the code of a previous version of one of our Domain entity classes, which I personally find kind of smelly.
public class PaidTimeOffPolicy : DomainEntity<PaidTimeOffPolicy>
{
public bool AllowsUnlimitedPto { get; private set; }
public int EmployeeLevel { get; private set; }
public bool IsDefaultForEmployeeLevel { get; private set; }
public decimal? MaxPtoHours { get; private set; }
public string Name { get; private set; }
public decimal? PtoAccrualRate { get; private set; } public PaidTimeOffPolicy()
{ } public PaidTimeOffPolicy(bool allowsUnlimitedPto, int employeeLevel, bool isDefaultForEmployeeLevel, decimal? maxPtoHours, string name, decimal? ptoAccruralRate)
{
if (employeeLevel < 1)
{
throw new ArgumentOutOfRangeException(nameof(employeeLevel));
}
if (!allowsUnlimitedPto && (maxPtoHours == null || ptoAccruralRate == null))
{
throw new PaidTimeOffException($"Invalid PTO policy. If policy does not specify unlimited hours, then you must specify both max PTO hours and PTO accrual rate.");
}
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentNullException(nameof(name));
}
AllowsUnlimitedPto = allowsUnlimitedPto;
EmployeeLevel = employeeLevel;
MaxPtoHours = allowsUnlimitedPto ? null : maxPtoHours;
PtoAccrualRate = allowsUnlimitedPto ? null : ptoAccruralRate;
Name = name;
IsDefaultForEmployeeLevel = isDefaultForEmployeeLevel;
}
}
What stands out here?
- It has an empty constructor, which is needed to allow mapping tools like Mapster to do their job.
- It has six parameters in the main constructor, which comes close to violating the “Rule of Seven” (I believe I read about that in Code Complete 2, though I’m not 100% sure.)
- It’s doing copious validation in the main constructor, which may or may not be a good practice. At the very least, that validation won’t be executed if an instance of this class is creating using Mapster, or otherwise by calling the empty constructor. Of course, this betrays another design smell, which is failing to consolidate the validation into a single method and call it from multiple places, but I digress…
- It can be moderately to severely hard to maintain, and unit tests might start breaking all over the place, depending on how complex the business logic becomes in the future.
If only there were a way to use the property initialization syntax that we all have known and loved since version 4, or whenever, combined with immutability… wait, now there is!
Thank goodness for init-only setters.
public class PaidTimeOffPolicy : DomainEntity<PaidTimeOffPolicy>
{
public bool AllowsUnlimitedPto { get; init; }
public int EmployeeLevel { get; init; }
public bool IsDefaultForEmployeeLevel { get; init; }
public decimal? MaxPtoHours { get; init; }
public string Name { get; init; } = default!;
public decimal? PtoAccrualRate { get; init; } public override void ValidateAggregate()
{
base.ValidateAggregate();
if (EmployeeLevel < 1)
{
throw new PaidTimeOffException("Invalid employee level for PTO policy.");
}
if (!AllowsUnlimitedPto && (MaxPtoHours == null || PtoAccrualRate == null))
{
throw new PaidTimeOffException($"Invalid PTO policy. If policy does not specify unlimited hours, then you must specify both max PTO hours and PTO accrual rate.");
}
if (string.IsNullOrWhiteSpace(Name))
{
throw new PaidTimeOffException("Invalid PTO policy name.");
}
}
}
Instantiating new Domain entities is now this simple, and the code is more maintainable because we’re not concerned about argument positioning, etc.
var p = new PaidTimeOffPolicy
{
AllowsUnlimitedPto = true,
EmployeeLevel = 6,
IsDefaultForEmployeeLevel = true,
MaxPtoHours = 55.0m,
Name = "HELLO",
PtoAccrualRate = 10.0M,
Id = 67
};
Immutability is Key
Make this into another one of your mantras: “immutability is key.” Note that in my interpretation of Domain-Driven Design (Clean DDD-Jacobs???) this is ONLY applicable to the Domain layer. Why? Because it is in the Domain layer that we are most concerned with logical code correctness, data validation, and business rules being faithfully implemented in a manner which is intelligible, scalable, and maintainable… and mutability is anathema to all the above.
So, let’s reiterate our intent in how we apply this to the two main types of Domain classes:
Value Objects
These are immutable reference types which have value semantics and structural equality comparison operations. In other words, they look and behave like classic .NET structs, but they are reference types under the hood. If their constituent properties are equal, then for all intents and purposes two separate Value Objects of the same type may be considered equal. 80% of the time we can probably get away with implementing them using the new C# 9 record types, and for the outliers we may choose to fall back on a ValueObject base class implementation.
Domain Entities
These are immutable reference types which have identity semantics and reference equality comparison operations. This means that two Employee objects may have entirely identical property values — First Name, Last Name, SSN — however, we may NOT assume that they philosophically represent the same real-life person (fun fact: in the United States, Social Security numbers are not guaranteed to be unique). As a matter of fact, two Domain entities may represent the same real-life entity (for example, a person) at different points in time, and should therefore be considered distinct for the purposes of our application! Each Domain entity instance has a unique identifier, typically borrowed from the corresponding Persistence entity, which can be used to determine whether two different objects represent the same thing. Combined with a fluent syntax that looks something like this…
employee.WithManager(manager);
… it becomes possible to create complex aggregates inside workflows, while maintaining the safety of immutability.
Fortunately, the new C# features — records and init-only setters — which make the above possible, also seem to play nice with mapping tools and other reflection-based techniques, like the nifty DomainEntity
base class helper method you see below (please note that this technique is not future-proof and may break with future versions of the framework):
public TEntity ReflectionCloneWith<TProperty>(Expression<Func<TEntity, TProperty>> expr, TProperty value)
{
var prop = expr.GetPropertyInfoFromLambda();
var e = CreateShallowCopy();
prop.SetValue(e, value);
return e;
}
All of this brings us to my main point, and the crux of this blog entry. Why use separate, immutable Domain entity types? Why mapping? Why is all this even necessary? I’m glad you asked.
The Design’s Greatest Strength Is Also It’s Greatest Weakness
Without getting too far ahead of ourselves — and I will go into this in more detail in the next blog entry — a typical high-level workflow for a complex update operation, such as updating employee information or submitting a PTO request, will look something like the following:
In a nutshell, we are passing through multiple layers, thereby transforming multiple entity type aggregates in both directions, just to perform a single operation. Furthermore, we may not assume that analogous entity types from each layer may be similar enough to each other that some mapping or translation process isn’t necessary, or that such operations may be trivialized.
If you’ve followed along with my previous blog entries, then you’ll know that I advocate keeping a clear demarcation between Domain entities and other types of entities in the stack, such as Persistence entities and View Models. This means that when we declare our Persistence entities using Entity Framework, or some other way, we do NOT treat those entities in any way as Domain entities. Note that this flies in the face of the guidance provided by many other experts. However, I’ve both built and maintained countless systems which did not abide by this practice, and every time it’s led to tears and frustration. Again, we’re talking about line-of-business applications, not CRUD. Our intent is to maintain architectural loose coupling between the layers of the system, which promotes scalability and maintainability.
(I want to mention that in the demo application these Clean DDD workflows are implemented as CQRS Commands/Queries. In the context of this blog entry, I’m not going to assume that you are using CQRS. It’s not a requirement in order to build a Clean DDD solution, and there are other ways you might choose to orchestrate your Domain logic. However, the same principle applies.)
We’ve established a mandate — make Domain entities the focus of the application and do not reuse them for any other purpose, such as View Models or Persistence entities — yet this is all on paper. In practice it’s actually harder than you might think. By trying to solve one problem, we’ve introduced more complexity into our application. I warned you before that this is perhaps the biggest drawback to my Clean DDD approach. However, it’s possible to minimize the pain of the added complexity, and that’s exactly what I’m going to show you next.
Slaying the Hydra
Like the Lernaean Hydra from Greek mythology, often in the software profession we find that in attempting to solve one problem, we end up creating one or more additional problems before coming to a final, workable solution. This is a great example.
Our ultimate goal is to be able to freely map from View Models to Domain entities to Persistence entities, or any permutation thereof, with minimal headache and without creating any hacky solutions that we might regret later as the solution evolves. I’m going to demonstrate how I solved that problem, and the various secondary problems which arose along the way, so that you can confidently overcome analogous obstacles in your own coding endeavors. Let’s begin.
Problem #1: Manually Mapping Between Entities Is Tedious
This sucks.
var employee = new Employee
{
HomeAddress = new Address(employeeViewModel.Address1, employeeViewModel.City, new State(employeeViewModel.State), new ZipCode(employeeViewModel.ZipCode)),
FirstName = employeeViewModel.FirstName,
LastName = employeeViewModel.LastName,
...
};
What you see above is the naïve approach, and you should avoid it like the plague. Remember: your job as a software professional is to automate solutions to difficult problems, not punch repetitive lines of code into your IDE or cut and paste blocks of logic without a thought as to how difficult it’ll be to maintain. You should strive to think abstractly and whenever possible, make the computer do the repetitive work. Enter, mapper classes.
Solution #1: Create Well-Defined Mapper Classes Using a Robust Toolkit Under the Hood
At this point many of you are saying “just use AutoMapper or Mapster directly inside your workflows and be done with it!” Not so fast. As the above code illustrates, mapping between fundamentally different entity types, even something as “simple” as an Employee, gets messy quickly. You end up with an overgrowth of complex configurations which may or may not be neatly maintained in a single location, and now your line-of-business code is tightly coupled to a third-party framework. Great, you just poisoned the well you drink from.
Instead, what we will do to solve this problem is create well-defined mapping interfaces, and corresponding mapper classes which implement them. We then use Dependency Injection to provide the mappers to our workflows.
The workflows don’t care what’s behind those interfaces. You could be using Mapster, or AutoMapper, or some home brewed reflection-based approach. You could be manually setting properties or using some combination of the above. It doesn’t matter. There is an implicit code contract in place stating that the workflows will receive a mapper class instance that will do its job — map one entity type to another. This is the essence of Dependency Injection/Inversion of Control. As a knock-on benefit, the code is easier to understand and vastly more unit testable. Such an implementation may look like this:
public class EmployeeViewModelToDomainEntityMapper : IEmployeeViewModelToDomainEntityMapper
{
protected TypeAdapterConfig Config { get; init; } = new TypeAdapterConfig(); public EmployeeViewModelToDomainEntityMapper()
{
Configure(
Config.NewConfig<EmployeeViewModel, Employee>()
.Map(dest => dest.HomeAddress, src => new Address(src.Address1, src.City, new State(src.State, ""), new ZipCode(src.ZipCode), src.Address2))
.Map(dest => dest.Salary, src => new Money(src.Salary, Currency.GetByCode(src.CurrencyCode)))
// Do NOT deep copy object graphs, with the exception of Value Objects (which have value semantics)
.IgnoreMember((member, side) => side == MemberSide.Destination && !(member.Type.IsValueType || typeof(IValueObject).IsAssignableFrom(member.Type) || member.Type == typeof(string)))
);
PerformAdditionalInitialization();
} public Employee Map(EmployeeViewModel source) => source.Adapt<Employee>(Config); public void Map(EmployeeViewModel source, Employee destination) => source.Adapt(destination, typeof(EmployeeViewModel), typeof(Employee), Config);
}
As you can see, we are not constrained by any third-party tool. We have the capability to use Mapster under the hood, which is obviously what we will choose to do most of the time for productivity’s sake. However, we don’t have to. The details of the mapping process are encapsulated behind the interface.
We inject the mapper into our CQRS Command handler like so:
public AddOrUpdateEmployeeCommandHandler(
IApplicationWriteDbContext context,
IApplicationWriteDbFacade facade,
IEmployeeViewModelToDomainEntityMapper employeeVmToDomainEntityMapper
)
{
this.context = context ?? throw new ArgumentNullException(nameof(context));
this.facade = facade ?? throw new ArgumentNullException(nameof(facade));
this.employeeVmToDomainEntityMapper = employeeVmToDomainEntityMapper ?? throw new ArgumentNullException(nameof(employeeVmToDomainEntityMapper));
}
And we use it like this:
public async Task<EmployeeViewModel> Handle(AddOrUpdateEmployeeCommand request, CancellationToken cancellationToken)
{
// APPLICATION LAYER
var employeeViewModel = request.Employee;
... other code
// DOMAIN LAYER
var employee = employeeVmToDomainEntityMapper.Map(employeeViewModel);
...
Seems like a fairly good fix, right? No, there’s a problem. We now must create a separate mapper class to represent each entity relationship, and we are copying over a ton of boilerplate for each one we create. If we want to map from PTO policy View Models to Domain entities, here we go again…
public class PaidTimeOffPolicyViewModelToDomainEntityMapper : IPaidTimeOffPolicyViewModelToDomainEntityMapper
{
protected TypeAdapterConfig Config { get; init; } = new TypeAdapterConfig(); public PaidTimeOffPolicyViewModelToDomainEntityMapper()
{
Configure(
Config.NewConfig<PaidTimeOffPolicyViewModel, PaidTimeOffPolicy>()
...
We now have a different problem on our hands.
Problem #2: Repetitious Mapping Code (Violation of DRY)
The above code clearly violates the Don’t Repeat Yourself (DRY) principle. For a solution with just one or two View Model to Domain entity relationships, maybe this would be fine. However, that’s not realistic, and this will not scale. The obvious solution is to fall back on those basic concepts we learned as junior developers — inheritance and encapsulation.
Solution #2: Create an Abstract Generic Base Class and Strongly Typed Generic Interfaces to Facilitate Encapsulation
The approach to fix this problem is to:
- Maximally reuse code across the mapper classes and
- Avoid having to create superfluous base classes to represent each different kind of relationship (VM->Domain entity; VM->Persistence entity; Domain entity->Persistence entity, etc.)
We fulfill the first criteria by creating a mapper base class, in combination with the Template Method design pattern; the second, using generics.
When combined together, we end up with an abstract generic MapperBase<TSource, TDestination>
class, from which we will derive all mapper class implementations. That base class looks like this:
public abstract class MapperBase
{
}public abstract class MapperBase<TSource, TDestination> : MapperBase where TSource : notnull where TDestination : notnull
{
protected TypeAdapterConfig Config { get; init; } = new TypeAdapterConfig(); protected MapperBase()
{
Configure(
Config.NewConfig<TSource, TDestination>()
// Do NOT deep copy object graphs, with the exception of Value Objects (which have value semantics)
.IgnoreMember((member, side) => side == MemberSide.Destination && !(member.Type.IsValueType || typeof(IValueObject).IsAssignableFrom(member.Type) || member.Type == typeof(string)))
);
PerformAdditionalInitialization();
} public virtual TDestination Map(TSource source) => source.Adapt<TDestination>(Config); public virtual void Map(TSource source, TDestination destination) => source.Adapt(destination, typeof(TSource), typeof(TDestination), Config); public virtual string ToScript(TSource source) => source.BuildAdapter().CreateMapExpression<TDestination>().ToScript(); protected virtual TypeAdapterSetter<TSource, TDestination> Configure(TypeAdapterSetter<TSource, TDestination> typeAdapterSetter) => typeAdapterSetter; protected virtual void PerformAdditionalInitialization()
{
}
}
This is great! There’s no longer a need to copy over boilerplate code across mapper implementations — it’s all encapsulated inside the base class. Along the way, we’ve created generic interfaces to represent all the possible kinds of mapping relationships that will probably come into play, which you can see listed below:
IDbEntityToDomainEntityMapper<TSource, TDestination>
IDbEntityToValueObjectMapper<TSource, TDestination>
IDbEntityToViewModelMapper<TSource, TDestination>
IDomainEntityToDbEntityMapper<TSource, TDestination>
IDomainEntityToViewModelMapper<TSource, TDestination>
IViewModelToDbEntityMapper<TSource, TDestination>
IViewModelToDomainEntityMapper<TSource, TDestination>
IViewModelToValueObjectMapper<TSource, TDestination>
Peeking inside one of them, you’ll see that we’re using generic constraints so that the compiler enforces validity of the relationship (the implicit code contract).
public interface IDomainEntityToViewModelMapper<TDomainEntity, TViewModel> where TDomainEntity : IDomainEntity where TViewModel : IViewModel
{
TViewModel Map(TDomainEntity source); void Map(TDomainEntity source, TViewModel destination);
}
This way, someone can’t start working on the system and create a “Domain entity to View Model mapper” which actually maps from Domain entities to Persistence entities, or something like that. This is all simply good coding practice.
Creating a new mapper class, free from repetitive boilerplate, is as easy as this:
public class EmployeeDomainEntityToViewModelMapper : MapperBase<Employee, EmployeeViewModel>, IDomainEntityToViewModelMapper<Employee, EmployeeViewModel> { }
Any particular mappers that have special requirements are free to override the virtual members of the base class.
All is not well, though…
Guess what? Requirements keep pouring in from the business, and now we’ve had to create all of these mapper classes:
AddOrUpdateEmployeeDomainEntityToViewModelMapper
AddOrUpdateEmployeeViewModelToDbEntityMapper
AddOrUpdateEmployeeViewModelToDomainEntityMapper
AddressDbEntityToValueObjectMapper
AddressViewModelToValueObjectMapper
CustomerViewModelToDbEntityMapper
EmployeeDbEntityToDomainEntityMapper
EmployeeDomainEntityToDbEntityMapper
EmployeeDomainEntityToViewModelMapper
EmployeeViewModelToDomainEntityMapper
PaidTimeOffPolicyDbEntityToDomainEntityMapper
PaidTimeOffPolicyDomainEntityToDbEntityMapper
PaidTimeOffRequestDbEntityToDomainEntityMapper
PaidTimeOffRequestDbEntityToViewModelMapper
PaidTimeOffRequestDomainEntityToDbEntityMapper
PaidTimeOffRequestDomainEntityToViewModelMapper
TenantViewModelToDbEntityMapper
ValidateRequestedPaidTimeOffHoursViewModelToDomainEntityMapper
Ahhhhh!!!!!!!!
Problem #3: Need to Manually Create New Mapper Classes for Every Entity Relationship
We’re no longer creating horrible boilerplate code by hand, which is nice, but we’re still back in the boat of having to create a new class every time there’s a new entity mapping requirement. This is now tedious in another way, and it gets worse as the application grows and grows. Is there some magical programming wizardry that can save us from this predicament?
Maybe there is.
Solution #3: Use Auto-Generation Code (Meta-Programming) Alongside Partial Classes to Minimize the Pain of Mapper Class Creation
Manually creating a new mapper class for every new entity relationship is a massive pain. Fortunately, we can mitigate it by using a classic meta-programming technique, which is writing code that generates code.
Back in the day, we used to use T4 Templates to do this sort of thing. However, I eventually found them to be clunky and they didn’t work really well with multiple input files. The fact of the matter is, this isn’t hard to do in your language of choice, and that’s exactly what we’ve done with the demo application. If you download the source code off GitHub, you can see that I created a console application in F# called JDS.OrgManager.MapperGeneratorConsoleApp.
I’m not going to post all of it up here because it doesn’t matter that much and frankly it’s a bunch of spaghetti code. But it gets the job done. The sample below shows how we use reflection to scan through generated assemblies, searching for classes that implement the various tag interfaces that indicate what kind of entities they are.
let assignableTypes = [ typeof<IDomainEntity>; typeof<IViewModel>; typeof<IDbEntity>; typeof<IValueObject> ]
let allTypes =
Directory.EnumerateFiles(".", "*.dll")
|> Seq.filter (fun name -> name.TrimStart('.', '\\').StartsWith("JDS"))
|> Seq.map Assembly.LoadFrom
|> Seq.map (fun a -> a.GetTypes())
|> Seq.concat
|> Seq.filter (fun t -> assignableTypes |> List.exists (fun t' -> t'.IsAssignableFrom(t)))
|> Seq.map (fun t -> { Name = t.Name; Type = t; AssignableType = assignableTypes |> List.find (fun t' -> t'.IsAssignableFrom(t)) })
|> List.ofSeq
In case you haven’t figured it out by now, I’m a huge fan of F#. The functional paradigm is incredibly powerful, and F# owns when it comes to tasks like this. Anyway, here is a small sample of the input file to the program.
CustomerViewModel->CustomerEntity
Employee->EmployeeEntity
EmployeeEntity->Employee
EmployeeViewModel->Employee
Employee->EmployeeViewModel
As you can see, it’s just a text file that specifies the relationships we want to generate. After we run it, it spits out a C# file that looks kind of like this:
public partial class AddressViewModelToValueObjectMapper : MapperBase<IAddressViewModel, Address>, IViewModelToValueObjectMapper<IAddressViewModel, Address>
{ }
public partial class CustomerViewModelToDbEntityMapper : MapperBase<CustomerViewModel, CustomerEntity>, IViewModelToDbEntityMapper<CustomerViewModel, CustomerEntity>
{ }
public partial class EmployeeDbEntityToDomainEntityMapper : MapperBase<EmployeeEntity, Employee>, IDbEntityToDomainEntityMapper<EmployeeEntity, Employee>
{ }
public partial class EmployeeDomainEntityToDbEntityMapper : MapperBase<Employee, EmployeeEntity>, IDomainEntityToDbEntityMapper<Employee, EmployeeEntity>
{ }
This is a prime example of when it’s okay to break our own rules. In this case, we are putting multiple class declarations into a single source file, something I’d never do under ordinary circumstances. You’ll notice that these are declared as partial classes. Partial class implementations are an extremely useful feature that has been around since C# 2. Their purpose, as you can see here, is to allow developers to split class declarations across multiple files. In this way, we can extend auto-generated code without worrying about our customizations getting wiped out the next time the generator runs. Obviously, there’s no one-size-fits-all-solution and we cannot be certain that the MapperBase
default implementation will always fit our needs in every situation. So, we’ve left ourselves an out.
Here’s the new, customized code for the Employee View Model to Domain entity mapper class that we saw earlier. Again, this is code that I manually wrote to augment the auto-generated code from the mapper generator console app.
public partial class EmployeeViewModelToDomainEntityMapper
{
private readonly IViewModelToValueObjectMapper<IAddressViewModel, Address> addressMapper; public EmployeeViewModelToDomainEntityMapper(IViewModelToValueObjectMapper<IAddressViewModel, Address> addressMapper) => this.addressMapper = addressMapper ?? throw new ArgumentNullException(nameof(addressMapper)); protected override TypeAdapterSetter<EmployeeViewModel, Employee> Configure(TypeAdapterSetter<EmployeeViewModel, Employee> typeAdapterSetter)
=> base.Configure(typeAdapterSetter)
.Map(dest => dest.HomeAddress, src => addressMapper.Map(src))
.Map(dest => dest.Salary, src => new Money(src.Salary, Currency.GetByCode(src.CurrencyCode)));
}
Now, it seems like we have the best of all worlds. We have combined the power of automation with the flexibility to handle special cases, like what you see above. We have finally slayed the hydra and solved our mapping problem entirely, right?
Wrong. Now there’s another issue.
Problem #4: Mapper Class Explosion
This is the final problem we have yet to solve (I promise). Scenarios like this are common in software solutions that have been built in the Object-Oriented paradigm (OOP), as is the case with C#. This is especially true when using sophisticated design patterns to compensate for inherent shortcomings of the language and/or framework. An overabundance of classes tends to occur because that is simply the natural tendency of OOP languages. In our case, we have made it so easy to generate mapping code that we now have a proliferation of mapper classes in our solution. This is problematic because A. it becomes onerous to inject mapper classes into workflows that are even moderately complex, as you can see from the constructor declaration below…
public AddOrUpdateEmployeeCommandHandler(
IApplicationWriteDbContext context,
IApplicationWriteDbFacade facade,
IViewModelToDomainEntityMapper<AddOrUpdateEmployeeViewModel, Employee> employeeVmToDomainEntityMapper,
IViewModelToDbEntityMapper<AddOrUpdateEmployeeViewModel, EmployeeEntity> employeeVmToDbEntityMapper,
IDomainEntityToViewModelMapper<Employee, AddOrUpdateEmployeeViewModel> employeeDomainEntityToViewModelMapper,
IDomainEntityToDbEntityMapper<Employee, EmployeeEntity> employeeDomainToDbEntityMapper,
IDbEntityToDomainEntityMapper<EmployeeEntity, Employee> employeeDbEntityToDomainEntityMapper,
IDbEntityToDomainEntityMapper<PaidTimeOffPolicyEntity, PaidTimeOffPolicy> ptoPolicyDbEntityToDomainEntityMapper
)
{
this.context = context ?? throw new ArgumentNullException(nameof(context));
this.facade = facade ?? throw new ArgumentNullException(nameof(facade));
this.employeeVmToDbEntityMapper = employeeVmToDbEntityMapper ?? throw new ArgumentNullException(nameof(employeeVmToDbEntityMapper));
this.employeeVmToDomainEntityMapper = employeeVmToDomainEntityMapper ?? throw new ArgumentNullException(nameof(employeeVmToDomainEntityMapper));
this.employeeDomainEntityToViewModelMapper = employeeDomainEntityToViewModelMapper ?? throw new ArgumentNullException(nameof(employeeDomainEntityToViewModelMapper));
this.employeeDomainToDbEntityMapper = employeeDomainToDbEntityMapper ?? throw new ArgumentNullException(nameof(employeeDomainToDbEntityMapper));
this.employeeDbEntityToDomainEntityMapper = employeeDbEntityToDomainEntityMapper ?? throw new ArgumentNullException(nameof(employeeDbEntityToDomainEntityMapper));
this.ptoPolicyDbEntityToDomainEntityMapper = ptoPolicyDbEntityToDomainEntityMapper ?? throw new ArgumentNullException(nameof(ptoPolicyDbEntityToDomainEntityMapper));
}
… and B. the registration code for our IOC container will quickly balloon out of control, as you can see from the short sample below:
public static class DependencyInjectionExtensions
{
public static IServiceCollection AddApplicationLayer(this IServiceCollection services, bool addValidation = false, bool addRequestLogging = false, bool useReadThroughCachingForQueries = false)
{
services
.AddSingleton<IDomainEntityToDbEntityMapper<Employee, EmployeeEntity>, EmployeeDomainToDbEntityMapper>()
.AddSingleton<IDomainEntityToDbEntityMapper<PaidTimeOffPolicy, PaidTimeOffPolicyEntity>, PaidTimeOffPolicyDomainToDbEntityMapper>()
.AddSingleton<IViewModelToDomainEntityMapper<RegisterOrUpdateEmployeeCommand, Employee>, RegisterOrUpdateEmployeeDomainEntityMapper>()
.AddSingleton<IViewModelToDbEntityMapper<TenantViewModel, TenantEntity>, TenantViewModelToDbEntityMapper>();
...
There are now too many interconnected pieces, and it can become easy to forget to register one of the mappers, in which case the application will throw an exception at run time when it is unable to resolve the proper implementation for the injected interface.
Fortunately, there’s an elegant solution.
Solution #4: Use Registration by Convention, and Create a Universal Mapper Factory
The first part of the solution to our problem involves implementing a pattern which is a good idea regardless of the situation at hand, and that is incorporating Registration by Convention into the application. Simply put, this means using a toolkit to automatically register classes into the IOC container rather than manually writing the registration code, which is error-prone and tedious. In the demo application I’ve chosen to use a package called Scrutor to get the job done. The result is what you see below.
public static class DependencyInjectionExtensions
{
public static IServiceCollection AddApplicationLayer(this IServiceCollection services, bool addValidation = false, bool addRequestLogging = false, bool useReadThroughCachingForQueries = false)
{
services.Scan(scan =>
scan
.FromCallingAssembly()
.AddClasses(classes => classes.AssignableTo(typeof(IDbEntityToViewModelMapper<,>))).AsImplementedInterfaces().WithSingletonLifetime()
.AddClasses(classes => classes.AssignableTo(typeof(IDbEntityToDomainEntityMapper<,>))).AsImplementedInterfaces().WithSingletonLifetime()
.AddClasses(classes => classes.AssignableTo(typeof(IDbEntityToValueObjectMapper<,>))).AsImplementedInterfaces().WithSingletonLifetime()
.AddClasses(classes => classes.AssignableTo(typeof(IDomainEntityToDbEntityMapper<,>))).AsImplementedInterfaces().WithSingletonLifetime()
.AddClasses(classes => classes.AssignableTo(typeof(IDomainEntityToViewModelMapper<,>))).AsImplementedInterfaces().WithSingletonLifetime()
.AddClasses(classes => classes.AssignableTo(typeof(IViewModelToDbEntityMapper<,>))).AsImplementedInterfaces().WithSingletonLifetime()
.AddClasses(classes => classes.AssignableTo(typeof(IViewModelToDomainEntityMapper<,>))).AsImplementedInterfaces().WithSingletonLifetime()
.AddClasses(classes => classes.AssignableTo(typeof(IViewModelToValueObjectMapper<,>))).AsImplementedInterfaces().WithSingletonLifetime()
);
In a nutshell, we are simply telling it to find all concrete implementations of the open generic interfaces specified and register each as a singleton. It is fast, efficient, and makes our task way easier.
The second part of our solution is intended to make the actual usage of dependency injection easier inside the high-level workflows. As you’ll recall from the code sample earlier in this section, we are injecting six different mappers into the AddOrUpdateEmployee
command handler class. Six! And this is just to perform a moderately complex operation: adding a new employee to a tenant and running some business validation rules. Is there an easier way? Absolutely. The answer is to create a universal mapper factory class, which encapsulates the IOC container and uses a little reflection mojo to make it more intuitive to use.
In brief, the universal mapper factory, which I’ve named simply ModelMapper
, exposes a number of methods of the form:
TDestinationType Map___To___(TSourceType source)
OR
void Map___To___(TSourceType source, TDestinationType existing)
When one of these methods is called, the appropriate mapper type is resolved from the IOC container, cached in a ConcurrentDictionary
(to improve performance on subsequent calls), and then the underlying Map()
method is invoked on the mapper object. You can see the ModelMapper
source code below. It’s a little verbose, but I think it’s important, so I’ve presented it in its entirety.
public class ModelMapper : IModelMapper
{
private readonly IServiceProvider serviceProvider; private ConcurrentDictionary<Type, object> mapperLookup = new ConcurrentDictionary<Type, dynamic>(); public ModelMapper(IServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
} public TDomainEntity MapDbEntityToDomainEntity<TDbEntity, TDomainEntity>(TDbEntity dbEntity)
where TDbEntity : IDbEntity
where TDomainEntity : IDomainEntity => GetFactoryMethodInstance<TDbEntity, TDomainEntity>().Invoke(dbEntity); public TValueObject MapDbEntityToValueObject<TDbEntity, TValueObject>(TDbEntity dbEntity)
where TDbEntity : IDbEntity
where TValueObject : IValueObject => GetFactoryMethodInstance<TDbEntity, TValueObject>().Invoke(dbEntity); public TViewModel MapDbEntityToViewModel<TDbEntity, TViewModel>(TDbEntity dbEntity)
where TDbEntity : IDbEntity
where TViewModel : IViewModel => GetFactoryMethodInstance<TDbEntity, TViewModel>().Invoke(dbEntity); public void MapDbEntityToViewModel<TDbEntity, TViewModel>(TDbEntity dbEntity, TViewModel existing)
where TDbEntity : IDbEntity
where TViewModel : IViewModel => GetVoidMethodInstance<TDbEntity, TViewModel>().Invoke(dbEntity, existing); public TDbEntity MapDomainEntityToDbEntity<TDomainEntity, TDbEntity>(TDomainEntity domainEntity)
where TDomainEntity : IDomainEntity
where TDbEntity : IDbEntity => GetFactoryMethodInstance<TDomainEntity, TDbEntity>().Invoke(domainEntity); public void MapDomainEntityToDbEntity<TDomainEntity, TDbEntity>(TDomainEntity domainEntity, TDbEntity existing)
where TDomainEntity : IDomainEntity
where TDbEntity : IDbEntity => GetVoidMethodInstance<TDomainEntity, TDbEntity>().Invoke(domainEntity, existing); public TViewModel MapDomainEntityToViewModel<TDomainEntity, TViewModel>(TDomainEntity domainEntity)
where TDomainEntity : IDomainEntity
where TViewModel : IViewModel => GetFactoryMethodInstance<TDomainEntity, TViewModel>().Invoke(domainEntity); public void MapDomainEntityToViewModel<TDomainEntity, TViewModel>(TDomainEntity domainEntity, TViewModel existing)
where TDomainEntity : IDomainEntity
where TViewModel : IViewModel => GetVoidMethodInstance<TDomainEntity, TViewModel>().Invoke(domainEntity, existing); public TDbEntity MapViewModelToDbEntity<TViewModel, TDbEntity>(TViewModel viewModel)
where TViewModel : IViewModel
where TDbEntity : IDbEntity => GetFactoryMethodInstance<TViewModel, TDbEntity>().Invoke(viewModel); public void MapViewModelToDbEntity<TViewModel, TDbEntity>(TViewModel viewModel, TDbEntity existing)
where TViewModel : IViewModel
where TDbEntity : IDbEntity => GetVoidMethodInstance<TViewModel, TDbEntity>().Invoke(viewModel, existing); public TDomainEntity MapViewModelToDomainEntity<TViewModel, TDomainEntity>(TViewModel viewModel)
where TViewModel : IViewModel
where TDomainEntity : IDomainEntity => GetFactoryMethodInstance<TViewModel, TDomainEntity>().Invoke(viewModel); public TValueObject MapViewModelToValueObject<TViewModel, TValueObject>(TViewModel viewModel)
where TViewModel : IViewModel
where TValueObject : IValueObject => GetFactoryMethodInstance<TViewModel, TValueObject>().Invoke(viewModel); private Func<TSource, TTarget> GetFactoryMethodInstance<TSource, TTarget>()
{
var mapper = GetMapper<TSource, TTarget>();
return (Func<TSource, TTarget>)Delegate.CreateDelegate(typeof(Func<TSource, TTarget>), mapper, "Map");
} private object GetMapper<TSource, TTarget>()
{
var mapperType = (typeof(TSource), typeof(TTarget)) switch
{
(Type source, Type target) when typeof(IDbEntity).IsAssignableFrom(source) && typeof(IDomainEntity).IsAssignableFrom(target) =>
typeof(IDbEntityToDomainEntityMapper<,>).MakeGenericType(source, target),
(Type source, Type target) when typeof(IDbEntity).IsAssignableFrom(source) && typeof(IValueObject).IsAssignableFrom(target) =>
typeof(IDbEntityToValueObjectMapper<,>).MakeGenericType(source, target),
(Type source, Type target) when typeof(IDbEntity).IsAssignableFrom(source) && typeof(IViewModel).IsAssignableFrom(target) =>
typeof(IDbEntityToViewModelMapper<,>).MakeGenericType(source, target),
(Type source, Type target) when typeof(IDomainEntity).IsAssignableFrom(source) && typeof(IDbEntity).IsAssignableFrom(target) =>
typeof(IDomainEntityToDbEntityMapper<,>).MakeGenericType(source, target),
(Type source, Type target) when typeof(IDomainEntity).IsAssignableFrom(source) && typeof(IViewModel).IsAssignableFrom(target) =>
typeof(IDomainEntityToViewModelMapper<,>).MakeGenericType(source, target),
(Type source, Type target) when typeof(IViewModel).IsAssignableFrom(source) && typeof(IDbEntity).IsAssignableFrom(target) =>
typeof(IViewModelToDbEntityMapper<,>).MakeGenericType(source, target),
(Type source, Type target) when typeof(IViewModel).IsAssignableFrom(source) && typeof(IDomainEntity).IsAssignableFrom(target) =>
typeof(IViewModelToDomainEntityMapper<,>).MakeGenericType(source, target),
(Type source, Type target) when typeof(IViewModel).IsAssignableFrom(source) && typeof(IValueObject).IsAssignableFrom(target) =>
typeof(IViewModelToValueObjectMapper<,>).MakeGenericType(source, target),
{ } => throw new ApplicationLayerException($"Could not get instance of mapper class for source type {typeof(TSource).Name} and destination type {typeof(TTarget).Name}.")
}; return mapperLookup.GetOrAdd(mapperType, serviceProvider.GetRequiredService(mapperType));
} private Action<TSource, TTarget> GetVoidMethodInstance<TSource, TTarget>()
{
var mapper = GetMapper<TSource, TTarget>();
return (Action<TSource, TTarget>)Delegate.CreateDelegate(typeof(Action<TSource, TTarget>), mapper, "Map");
}
}
Now, instead of having to inject in multiple mapper types into workflows that need them, we can simplify our constructor to this:
public class AddOrUpdateEmployeeCommandHandler : IRequestHandler<AddOrUpdateEmployeeCommand, AddOrUpdateEmployeeViewModel>
{
private readonly IApplicationWriteDbContext context;
private readonly IApplicationWriteDbFacade facade;
private readonly IModelMapper mapper;
public AddOrUpdateEmployeeCommandHandler(
IApplicationWriteDbContext context,
IApplicationWriteDbFacade facade,
IModelMapper mapper
)
{
this.context = context ?? throw new ArgumentNullException(nameof(context));
this.facade = facade ?? throw new ArgumentNullException(nameof(facade));
this.mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
}
Using it is as simple as:
mapper.MapViewModelToDbEntity(employeeViewModel, employeeEntity);
or
var employee = mapper.MapDbEntityToDomainEntity<EmployeeEntity, Employee>(employeeEntity);
You can review the entire AddOrUpdateEmployee
workflow for the demo solution on GitHub, here.
At last, we have slayed the hydra of complexity. We developed a means by which we can easily map from View Models to Domain entities to Persistence entities, or any permutation thereof, which allows us to meet our mandate of keeping entity types decoupled. The groundwork is in place for our system to scale elegantly and efficiently, while keeping the code reasonably maintainable.
Conclusion
We started out be re-examining Domain entities and Value Objects from a Domain-Driven Design perspective, and then saw that new C# 9 language features such as records and init-only setters can help simplify the design of these classes. Next, I laid out the case for keeping different entity types — View Models, Domain entities, and Persistence entities — distinct and decoupled, the primary reason being that they serve orthogonal purposes, and we want our application to scale. After that, I discussed the complexity that this design decision introduces, and outlined a four-step process for mitigating that complexity, culminating in the creation of an elegant mapping infrastructure that we can use to transform entities of one type to those of another.
In the next blog entry, we’ll explore how we can use this new mapping infrastructure to create various kinds of DDD workflow patterns, giving us the power and flexibility to implement virtually any requirement the business team throws at as. It’s starting to get interesting…
Experts / Authorities / Resources
Vladimir Khorikov
Steve McConnell
MSDN Official Docs
Wikipedia
- Hydra (mythical beast)
Mapping Tools
This is entry #5 in the Scaffold Your Clean DDD Web App Series