Separating Domain and Persistence Logic in .NET Entity Framework 7
INTRODUCTION
Ever felt that your ORM is limiting you in designing domain models in a way that is forcing you to use data types that best fit your database but not necessarily your usage or that you have to mix database property decorators with your business domain model?
Well, we all have.
Using an ORM, in this case Entity Framework, brings many benefits like being able to design and access a database in the same programming language as the rest of your application so you don’t have to deal with raw SQL for most cases. Most ORM guides show examples that use a lot of ORM specific conventions which speed up and make things easier to get going but that approach introduces new problems later on which more complex applications have to handle and that is mixing of concerns.
If a class represents a business model and at the same time explains how it should persist its data and interact with the database, that’s mixing concerns and its a problem because it forces you to create dumb anemic models which have little to any logic, allows invalid states to exist and to be persisted — which is opposite of good practices of maintainable and reusable code and object oriented programming in general actually.
HOW TO UNTANGLE DOMAIN AND PERSISTENCE LOGIC
The best way to separate those two concerns while still utilizing ORM features is to remove any persistence logic (data annotations, conventions) from domain models and use Entity Framework fluent API.
Fluent API allows you to teach your ORM how to persist and load your models in a completely isolated configuration manner without needing to follow any limiting conventions. This allows for a nice clean implementation of Value Objects from DDD or Guarded Keys to improve the type safety of ID properties or simple but quality things for example mapping an enum whose values are integers to be written in the database to their textual representation for an easier read.
EXAMPLE #1
I have this FileType domain model which I want to be able to persist in a database as some sort of a lookup table.
By default Enums in C# are saved as an integer value but I like it more as a string since it’s much easier to read and know what’s-what that way. To achieve this I create a configuration class for this domain model that looks like this:
With this I can have full control of how FileType class will be saved and read from/to a database while my domain model stays clean of any persistence logic. Enum configuration starts on line 15.
EXAMPLE #2
FileNode represents a single file in a hierarchical file system.
The Title property is a value object which adds many benefits including keeping the object always in a valid state with data validation and parsing. Now that’s awesome but by default EF won’t know how to save the FileNodeTitle struct to the database. So this is how we teach him:
To apply model configuration you simple have to specify it in DbContext’s OnModelCreating method:
ONE LITTLE TIP – Database first approach
Writing fluent api code can be a little hard when configuring mapping of complex domain models because of all syntax and special methods that come with its use.
In order to avoid learning all of it, what I like to do is use the Entity Framework CLI command method called scaffold.
command: dotnet ef scaffold {host} {provider}
example:
dotnet ef dbcontext scaffold “Server=(localdb)\mssqllocaldb;Database=Blogging;Trusted_Connection=True;” Microsoft.EntityFrameworkCore.SqlServer
What it does is generates all the configuration (as seen above) you need to get the same database models (tables and columns) in your code as in the given database server.
I like to move the scaffold output in the following folder structure:
CONCLUSION
What is simple is not always easy.
Keeping domain models clean of any persistence logic is an advanced technique to be done properly and it also makes the code more complex, so there is a price to pay, but what you get for it is a much more readable, maintainable and less error prone codebase.
So as for a lot of things the decision whether a project needs this or not is subjective but generally speaking if a simple CRUD or an MVP application will probably start serving its purpose earlier with just following ORM’s conventions but for any bigger long-term projects this is definitely something to consider practicing from the beginning.