0

The project I'm working on deals with quite complex business rules, so I'm trying to apply DDD. Unfortunately, I have to work with a legacy database I cannot get rid of, and I'm having trouble keeping a clean Domain Design.

Lets say some Entity, has some ValueType as primary key, which is required. This could be designed in DDD like the following:

public class Entity
{
    public Entity(ValueType key)
    {
        Key = key;
    }

    public ValueType Key { get; }
}

Now, lets say this key is actually stored as a string representation, which can be parsed to construct the ValueType. I could do something like this, to make it work with Entity Framework:

public class Entity
{
    private Entity()
    {
        //Private empty ctor for EF
    }

    public Entity(ValueType key)
    {
        StoredKey = key.ToString();
    }

    public ValueType Key => ValueType.Parse(StoredKey);

    //DB representation of the key, setter for EF
    private string StoredKey { get; set; }
}

This way, I feel I'm kind of polluting my Domain Design with storage concerns. For what the Domain cares, the Entity could be persisted just in memory, so this string internal representation feels weird.

This is a very simple scenario to show an example, but things can actually get really worse. I would like to know if there is any way to achieve persistance ignorance in the model with this simple example, so I can start thinking later about how to design more complex scenarios.

  • You can directly map the [backing field](https://learn.microsoft.com/en-us/ef/core/modeling/backing-field#fluent-api) and [owned entities](https://blogs.msdn.microsoft.com/dotnet/2017/08/14/announcing-entity-framework-core-2-0/), which is good enough for small or simple domains though. If you have really complex ones, don't use ORM (except for storing events when using Event Sourcing - best-combined with cqrs) – Tseng Jan 16 '18 at 12:21
  • 1
    Also its completely okay to use a legacy database layout. You can use it for the "read-model" in cqrs, whereas write model and mutation of the entity happens somewhere else and the read-store is updated by processing events emitted by the command queries – Tseng Jan 16 '18 at 12:27
  • @Tseng those two features of EF Core seem useful, but I don't think they would help in every scenario (think of a DateTime stored as a string representation in a specific TimeZone and locale format...). I don't know anything about cqrs, but what you say about it kind of makes sense in this scenario, so I'll investigate. – Daniel García Rubio Jan 16 '18 at 12:37
  • 2
    Check out [CQRS.nu](http://cqrs.nu) for a summary. It adds a bit more initial complexity to your domain, but will make your life easier on in later stages – Tseng Jan 16 '18 at 12:49
  • Possible duplicate: https://stackoverflow.com/q/18109547/861716 – Gert Arnold Jan 16 '18 at 14:01

3 Answers3

2

The domain model doesn't need to follow the entity framework structure. What you can do is to create 2 types of models. One pure domain models and when passing it to the repository to persist it transform it into entity framework model. And when fetching the model you can do the inverse transformation.

Mohamed Bouallegue
  • 1,334
  • 15
  • 19
  • I have thought of this alternative, but I think it kind of breaks the unit of work and repository patterns in some way: If I fetch my business entity from the repository and make some changes to it, then entity framework is unaware of this changes, because the EF model is no longer there. This means I can't just call SaveChanges() in my Unit of Work (aka, DbContext). I would have to use the Repository first like Update(entity), to create the EF model and make EF aware of the changes, and then SaveChanges(). Is this approach good enough, or am I missing something? – Daniel García Rubio Jan 16 '18 at 10:07
  • 1
    Good or bad, this is the *only* approach. EF (Core) entity model is a *store* (persistence) model and with few exceptions follows the database table/relationship structure. In entity model you are forced to follow the EF rules. Any attempt to apply domain/business rules to that model will just cause you troubles. – Ivan Stoev Jan 16 '18 at 11:23
1

You can achieve persistance ignorance in this instance. Your instincts are right, get rid of all persistance concerns from your domain model, move them entirely within your dal where they belong.

DB.sql:

  create table entity {
    id  nvarchar(50)    not null primary key,
    fields  nvarchar(max)  /*Look mum, NoSql inside sql! (please dont do this) */
  }

Domain.dll:

  class Entity {
    /*optional - you are going to need some way of 'restoring' a persisted domain entity - how you do this is up to your own conventions */
    public Entity(ValueType key, ValueObjects.EntityAttributes attributes) {Key=key;Attributes=attributes;}
    public ValueType Key { get; }
    public ValueObjects.EntityAttributes Attributes { get; }
    /* domain functions below */
  }
  IEntityRepository { 
    public Update(Domain.Entity enity);
    public Fetch(ValueType Key);
  }

now ALL persistance work can go in your DAL, includeing the translation. I havent done EF in a while so treat the below as sudo code only.

DAL (EF):

  /* this class lives in your DAL, and can be private, no other project needs to know about this class */
  class Entity :{
    public string EntityId {get;set;}
    public string Fields {get;set;}
  } 
  class EntityRepository : BaseRepository, Domain.IEntityRepository {
    public EntityRepository(DBContext context) {
      base.Context = context;      
    }
    public Domain.Entity Fetch(ValueType key) {
      string id = key.ToString();
      var efEntity = base.Context.Entitys.SingleOrDefault(e => e.Id == id);
      return MapToDomain(efEntity);
    }
    /*Note: Handle mapping as you want, this is for example only*/
    private Domain.Entity MapToDomain(EF.Entity efEntity) {
      if (efEntity==null) return null;
      return new Domain.Entity(
        ValueType.Parse(efEntity.Id),
        SomeSerializer.Deserialize<ValueObjects.EntityAttributes>(efEntity.Fields) /*every time you do this, a puppy hurts its paw*/
      );
    }
    public Domain.Entity Update(Domain.Entity domainEntity) {
      string id = key.ToString();
      var efEntity = MapToEf(domainEntity);
      base.Context.Entities.Attach(efEntity);
      base.Context.Entity(efEntity).State=EntityState.Modified;
      base.Context.SaveChanges();
    }
    private Domain.Entity MapToEf(Domain.Entity domainEntity) {
      return new EF.Entity(
        Id = domainEntity.Key.ToString(),
        Fields = SomeSerializer.Serialize(domainEntity.Attributes) /*stahp!*/
      );
    }
  }

The takeaway thing here is that you are going to need to do Mapping of some sort. This all but unavoidable unless your domain is realy simple and your ORM is super fancy, but even then I would recommend keeping your ORM models seperate to your Domain models because they solving 2 different problems (ORMS are providing a code version of your database model, DDD are providing a code version of you Business Models). If you are compromising your Domain Model (ie, making properties public set ) to cater for your DAL then step back and re evaluate. Obviously compromise where appropriate but realise this means you are introducing (implied) dependancies across your application layers.

You next quetion in realtion to performance (but mapping is so slow) was answered by Constantin Galbenu, have seperate 'read' models and reposistories for lists, searches. Do you really need to pull back 1000's of business models just to populate a search result list (and then have the tempation to add properties of no concern to the business model because 'the search page needs this one bit of data for the finaince people'). You should only be pulling out our domain model when you are doing some sort of business action, otherwise some nice anemica read only views are your friend.

Joe
  • 1,327
  • 1
  • 10
  • 19
  • Thanks for the thorough answer, this makes a lot of sense. It's a pity that I have to keep separate entities, because EF works much better when your DB design is your domain design. I guess this is unavoidable unless you're doing CRUD on a fairly well designed database. – Daniel García Rubio Jan 17 '18 at 08:34
0

As many suggested in the comments, CQRS is a good choice for complex business rules. It has the great advantage that you can have different models for each side (write/command and read/query). In this way you separate the concerns. This is also very good because the business logic for the write side differs from the read side's but enough with the advantages of CQRS.

...Unfortunately, I have to work with a legacy database I cannot get rid of...

Your new Write model, the Aggregate, will be responsible for handling commands. This means that the legacy model will be relieved of this responsibility; it will be used only for queries. And to keep it up-to-date you can create a LegacyReadModelUpdater that is subscribed to all Domain events generated by the new Aggregate and it will project them to the old model in an eventually consistent manner.

Constantin Galbenu
  • 16,951
  • 3
  • 38
  • 54