-1

My Domain layer contains below model:

public class ApiResource
{
    public bool Enabled { get; set; } = true;
    public string Name { get; set; }
    public string ClientId { get; set; }
    public IEnumerable<string> HandledScopes { get; set; } = Array.Empty<string>();
}

Later, the Infrastructure layer uses EntityFramework Core. And it's entity for it:

public class ApiResource
{
    public int Id { get; set; }
    public bool Enabled { get; set; }
    public string Name { get; set; }
    public Client Client { get; set; }
    public int ClientId { get; set; }
    public ICollection<Scope> Scopes { get; set; }
}

Notice that the string ClientId from model is different from int ClientId in entity. The second property (in entity) is foreign key to Client, whereas the string ClientId is client identifier in "logical sense", used in business logic. They are not the same.

The problem is that if I want to e.g. create new record in database, then I have to convert my model to entity. (At some IRepository in or something like it in Infrastructure layer). But I cant do it properly, because the model doesn't know about primary keys. So every time I want to create entity, I have to SELECT existing relationships from database and then add them to entity instance that I want to create.

Another problem is that if I wouldn't have some identifier in model (in Domain layer definition), then I even wouldn't know which entity shall I e.g. update.

But that's not the end of problems this approach causes. If I want to select e.g. only Name from very complex object, I still must have SELECT whole entity from database. Only thing I can do is to define new methods e.g. in IRepository for every use case. So if I want to get name, I would have method FindName etc. It doesn't seem good.

So how would you design Domain models to be easy implementable and flexible in different databases (relational and no relational), using ORM's like EF or any other? Could you provide any examples?

10
  • 3
    I think the premise of the question is based on some confusion or misunderstanding - DDD is about domain modelling so does not touch upon database design at all; Relational Database design should focus on Normalisation - stackoverflow.com/questions/246701/… Furthermore, ORMs such as EntityFramework also do not have any relationship whatsoever with DDD, as their purpose is to provide abstraction around Data Access and persistence, so they are not tools for representing your domain model. May 5 at 16:22
  • But DDD touch models. It says that Domain layer have to contain models that will be used in logic. But later (in my case) it creates some problems I described in question. So I think I'm doing something wrong.
    – Szyszka947
    May 5 at 16:30
  • 5
    DDD focuses on domain entities, not on ORM models nor DB tables (a "database entity" is a table, which is completely unrelated to the DDD concept of a "Domain Entity"). I would not typically expect your domain entities to neatly map onto your ORM or database tables because these are totally different things - the simple answer is that mapping between a database persistence model and domain model generally involves work and you simply have to write code to translate between the two. In many cases the overall shape/structure of a domain model may look completely different to the database. May 5 at 16:31
  • 2
    No, absolutely not - that would be one of the reasons for separating your domain model away from your database model; if domain models have different fields and representations than your database it's a matter of designing queries which only retrieve only the data you need as part of translations between the domain model and database model. If you only need a single value from the database then there would be no reason to retrieve a whole record. May 5 at 21:23
  • 1
    If I want to select e.g. only Name from very complex object I still must have SELECT whole entity DDD is all about this. You no longer can mutilate data and access to it arbitrarily. You have to get it from the domain model (in memory model) an it should be always be in a valid state. So, in DDD, there's not such a thing as I want to fetch 1/4 of a domain model only. For the APP the only source of trust is the domain layer, not the data access layer
    – Laiv
    May 6 at 10:08

3 Answers 3

1

Notice that the string ClientId from model is different from int ClientId in entity. The second property (in entity) is foreign key to Client, whereas the string ClientId is client identifier in "logical sense", used in business logic. They are not the same.

The easier would be to have a single ID in the domain. It's fine to have IDs in the domain layer for persistence purposes. It's simple and practical.

Now, let's assume you can't change to a single ID and you need to deal with both. Ask yourself: Why are they different? Where do the int and the string come from? Who is responsible for each ID? Who generates each ID? Is the int an auto-incremental number generated by the DB? If yes, you are probably leaking implementation details you might want to keep hidden. But how can you handle tuples of IDs and hide the values? With abstractions, for example, ClientID.

The abstraction can hide both values and makes them accessible only to those layers they are affine. No code will be coupled to the int or the string value, but the DAL which is aware of both.

class ClientID implements StoreId, DomainId{
  private final int storeId;
  private final String domainId;
  public ClientID (int storeId) { ... }
  public ClientID (String domainId) { ... }
  public ClientID (int storeId, String domainId) { ... }
  public boolean equalsTo(ClientId otherId){ ... }
  //interface segregation
  public int getStoreId() { ... }
  public String getDomainId(){ ... }
}

The problem is that if I want to e.g. create new record in database, then I have to convert my model to entity.

Yes. This is what DDD is all about. Different models for different scopes. Domain vs Storage. It's also a trade-off in hexagonal architectures. A high degree of decoupling involves mapping somewhere.

But I can't do it properly because the model doesn't know about the primary keys

But it knows the domainId. Use the string value to find the entity.

If I wouldn't have some identifier in the model (domain)

Why would you shoot yourself in the foot? Don't do that. In DDD entities are identifiable elements of the domain, so the concept ID is already present.

If I want to select e.g. only Name from a very complex object,

DDD doesn't work this way. You fetch everything to keep data consistent in memory too. There's no such thing as "I want to load or update 1/4" of the data".

You neither change data directly on the DB, you first change it in memory and when data is consistent and in a valid state, you persist it as a whole. Or you make deltas; that's an implementation detail specific to the DAL.

So how would you design Domain models to be easily implementable and flexible in different databases

Giving up on the idea that a single model can be a one-fit-all solution and being selective with the ORM.

1

If I want to select e.g. only Name from very complex object, I still must have SELECT whole entity from database. Only thing I can do is to define new methods e.g. in IRepository for every use case. So if I want to get name, I would have method FindName etc. It doesn't seem good.

If you want to paint a wall, you have two options: a tiny brush or a paint roller. A tiny brush is fantastic when you need to do a lot of custom wall features, but it takes ages to paint an entire wall. A paint roller is not great at small custom features, but it significantly decreases how long it takes to paint a wall.

Custom tailored repository end points for custom tailored use cases are the equivalent of the tiny brush. While it gives you great control over exactly how much data you transmit, it significantly increases both the time to develop something and how long any future maintenance on it will require.

Generally speaking, given hardware improvements over the last few decades, the more significant bottleneck for cost of development is the work effort required, not the runtime performance. Therefore, the better approach here is the paint roller. Yeah, you might sometimes load a bit more data than you actually need, but the effort of tailoring all your calls is likely going to be the bigger bottleneck.

Exceptions to the above argument exist, where you are squeezing for every inch of performance. But this kind of squeezing is counterproductive for a run-of-the-mill data application.

Secondly, your repository is providing endpoints for other domains, which means that it's not defining itself. In DDD, the domain very much owns itself and shouldn't be subject to other domains' whims.

Both of these points lead to what DDD very much promotes: the domain defines itself, and the consumers just deal with the domain as it presents itself. Generally, you'll find that the domain then spends no time subdividing its aggregate into individual data packets. It's not going to serve you only a name, or only an ID. It's going to give you the resource entity as a whole, and you can just take from it what you want.

The simplicity of only serving whole entities tends to far outweigh the performance drawbacks from not being able to omit certain columns from your query retrieval.


Don't succumb to dogma!

DDD, just like any other paradigm, is a general guideline, not a dogma. It's perfectly fine to deviate from DDD purism when it matters to you.

What's important here is that you are made aware of the cost of doing so, i.e. you need to consider the maintenance effort of having custom repo methods that are tailored to specific customers, and you have to weigh it against the concrete performance concern of loading more data than you need.

If your analysis shows that you do in fact benefit from doing so, then by all means you should do it. DDD is not intended to cover every possible use case perfectly, it's just a good baseline to start off of, and your expertise should then be used to make informed decisions on when it makes sense for you to deviate from the baseline.

0

I suggest that your problem is more with “How to work effectively with EntityFrameworkCore” than how to do DDD, and you will possibly get better help with that on SO.

With EF Core you should not need a duplicate entity in the Infrastructure layer. The only Infrastructure you need is the concrete database (and, a matching IDesignTimeDbContextFactory so that the cli tooling can work)

Instead, configure EF Core to work directly with your domain entity. EFCore has excellent support for configuring exactly how the database table for an entity gets created and/or you can can create the database yourself and let EFCore magically work out how to map.

Here is my minimal example code for getting EF Core to 'just work' with your Entity. I've assumed you want a table for HandledScope because you can't easily put a string array into a single column.

Nuget dependencies after creating a Net7.0 XUnit test project:

dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package TestBase
using System.Diagnostics;
using EFCoreAndDomainModels.Application;
using EFCoreAndDomainModels.Domain;
using EFCoreAndDomainModels.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using TestBase;
using Xunit.Abstractions;

namespace EFCoreAndDomainModels
{

namespace Domain
{
    public record ApiResource
    {
        public bool Enabled { get; set; } = true;
        public string Name { get; set; }
        public string ClientId { get; set; }
        public IEnumerable<HandledScope> HandledScopes { get; set; } = Array.Empty<HandledScope>();
    }

    public record HandledScope(string ScopeName)
    {
        public static implicit operator HandledScope(string str) => new HandledScope(str);
        public static implicit operator string(HandledScope scope) => scope.ScopeName;
    }
}

namespace Application
{
    public class QueryApiResources
    {
        readonly ApplicationDb database;

        public IList<ApiResource> GetForClient(string clientId)
            => database.ApiResources.Where(r => r.ClientId == clientId).ToList();
        
        public QueryApiResources(ApplicationDb database) => this.database = database;
    }

    public interface ApplicationDb
    {
        public DbSet<ApiResource> ApiResources { get; set; }
    }
    
}
namespace Infrastructure
{
    public class SqliteApplicationDb : DbContext, ApplicationDb
    {
        public SqliteApplicationDb(DbContextOptionsBuilder<SqliteApplicationDb> optionsBuilderOptions)
            : base(optionsBuilderOptions.Options) { }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.Entity<ApiResource>().HasKey(r => r.Name);
            modelBuilder.Entity<HandledScope>().HasKey(r => r.ScopeName);
        }
        public DbSet<ApiResource> ApiResources { get; set; }
    }
    
    public class SqliteApplicationDbFactory : IDesignTimeDbContextFactory<SqliteApplicationDb>
    {
        public SqliteApplicationDb CreateDbContext(params string[] args)
        {
            Debug.Assert(args?.Length>0 && args[0]?.Length>0, "Required argument: filepath for the sqlite db.");
            var path = args[0];
            try { Path.GetFullPath(path);} catch(Exception){throw new ArgumentOutOfRangeException("Not a valid path for a file: "+args[0].Substring(0,999));}
            //
            var optionsBuilder = new DbContextOptionsBuilder<SqliteApplicationDb>();
            optionsBuilder.UseSqlite($"Data Source={path}");
            
            return new SqliteApplicationDb(optionsBuilder);
        }
    }

    public record Config
    {
        public string DbPath { get; set; }

        public static Config ForTest = new Config
        {
            DbPath = "./IntegrationTest.db"
        };
    }
}

namespace Tests
{

    public class IntegrationTestQueryApiResources
    {
        readonly ITestOutputHelper _out;

        readonly ApiResource FakeApiResource = new ApiResource
        {
            Enabled = true,
            ClientId = "testid",
            HandledScopes = new HandledScope[] { "testscope1" },
            Name = "testresource"
        };

        Config TestConfig = Config.ForTest;
        SqliteApplicationDb testDb;

        [Fact]
        public void QueryApiResources_CanGetByClientId()
        {
            //Arrange
            GivenEmptyTestDb();
            GivenApiResourceInDb(FakeApiResource);

            //Act & Assert
            var uut = new QueryApiResources(testDb);
            
            var result= uut.GetForClient(FakeApiResource.ClientId)
                .ShouldBeOfLength(1)
                .First()
                .ShouldEqualByValue(FakeApiResource);
            
            //Debug
            _out.WriteLine(result.ToString());
        }

        void GivenApiResourceInDb(ApiResource resource)
        {
            testDb.ApiResources.Add(resource);
            testDb.SaveChanges();
        }

        void GivenEmptyTestDb()
        {
            if (File.Exists(TestConfig.DbPath)) { File.Delete(TestConfig.DbPath); }

            testDb = new SqliteApplicationDbFactory().CreateDbContext(TestConfig.DbPath);
            testDb.Database.GetMigrations()
                .ShouldNotBeEmpty(@"
    You must FIRST create at least one database migration from the commandline with for example:
 
    dotnet ef migrations add InitialCreate --context SqliteApplicationDb -- ./IntegrationTest.db

    Inspect the Db creation sql script with

    dotnet ef migrations script --context SqliteApplicationDb -- ./IntegrationTest.db > sqlitedbscript.sql  

    See https://learn.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs=netcore-cli#create-the-database"
                );
            testDb.Database.Migrate();
        }
        
        public IntegrationTestQueryApiResources(ITestOutputHelper @out) => _out = @out;
    }

}
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.