0

I am implementing clean architecture using the existing database, with scaffolding command I have generated the POCO entities in the Infrastructure layer and as well as manually created the entities in the domain layer to map them later.

in the Application layer, I have the generic interface repository with a few standard operations.

public interface IRepository<T> where T : class
 {
     Task<IReadOnlyList<T>> GetAllAsync();
     Task<T> GetByIdAsync(int id);
     Task<T> AddAsync(T entity);
     Task UpdateAsync(T entity);
     Task DeleteAsync(T entity);
 }

As per the principles of Clean-Architecture, I am implementing it in the Infrastructure layer.

public class Repository<T> : IRepository<T> where T : class
    {

        protected readonly MyDBContext _MyDBContext;
        public Repository( MyDBContext mydbContext)
        {
            _MyDBContext= mydbContext;
        }
        public async Task<T> AddAsync(T entity)
        {
            await _MyDBContext.Set<T>().AddAsync(entity);
            await _MyDBContext.SaveChangesAsync();
            return entity;
        }

-----
----

I am using a Mediator pattern with CQRS, when I try to save the user from the API layer I will end up with the below exception.

System.InvalidOperationException: Cannot create a DbSet for 'ABC.Domain.Entities.User' because this type is not included in the model for the context. However, the model contains an entity type with the same name in a different namespace: 'ABC.Infrastructure.Models.User'.

It will be resolved if I can able to map the domain entity to the infrastructure entity in the above Repository implementation.

In the above implementation, the T is the ABC.Domain.Entities.User, not the ABC.Infrastructure.Models.User.

I can't pass the ABC.Infrastructure.Models.User from the Application layer ( because I can't add a reference to the Infrastructure layer in Application layer) due to the rule Clean Architecture all dependencies flow inwards and Core has no dependency on any other layer.

Please help me to map the incoming domain entity with the infrastructure entity in the above repository implementation so that I can use these general methods for other entity operations as well.

Check my skeleton repo.

https://gitlab.com/mail2mdilyas/workcontinent/-/blob/master/Work.Continent.Infrastructure/Repositories/Base/Repository.cs

In the above class, the "AddAsync" operation is in the generic repository (Repository.cs) and can be used for different insert operations with different domain entities in the future. And here I won't be knowing what is T :

public class Repository : IRepository where T : class

please advise me of the generic way to find and map the incoming domain entity with the data entity.

Mohd Ilyas
  • 31
  • 2
  • 1
    That "generic repository" antipattern is anything but clean. Your `Add` will actually execute 20 DELETEs and 43 UPDATEs on any number of entities. A DbSet is already a single-entity Repository, a DbContext is a multi-entity Unit of Work. When you call `SaveChanges` in your "Add" you actually persist all changes in all entities tracked by this specific Unit of Work – Panagiotis Kanavos Jan 27 '22 at 10:22
  • Create and use a proper DbContext first, then consider if you even need something more. Patterns and architectures are meant to solve *specific* problems, not used "just in case". They have to justify their cost. Besides, `Clean Architecture` is just a marketing name used by a specific author, used to structure their book, only in the .NET space. It's not a generally accepted way of building services or applications. Book architectures never work in practice, simply because a book has to cover *everything* even when most apps typically encounter only 10% of problems – Panagiotis Kanavos Jan 27 '22 at 10:51
  • Trying to apply a book architecture to your application is similar to an architecture student trying to apply their entire textbook to ... a cottage. I'm sure a cottage doesn't need roundabouts, bridges, arches and multi-level underground parkings, even though the textbook mentions them. And the book's samples and "framework" are little better than balsa models. Once again, they're meant to illustrate the book's points, not to be used in every situation – Panagiotis Kanavos Jan 27 '22 at 10:54
  • A repository should not serve as a Mediator. A mediator and CQRS involve a significant overhead cost in a system where there is a specific and valuable reason to introduce a complete separation of domain and *multiple* implementations of business implementation acting as consumers in unison with one another. As a pattern for architecture within a single application is is 100% overkill and leads to far, far more problems than you might imagine it would solve. K.I.S.S. 'cuz Y.A.G.N.I. – Steve Py Jan 27 '22 at 12:17
  • @Mohd Ilyas did you managed to get the mapping working? – user7849697 Jan 20 '23 at 21:54

1 Answers1

0

I'd say you have two different ways to handle this. You could either just map the domain model to the table structures and let your configuration files on the infrastructure layer do the mapping for you. That's the way I prefer to do it nowadays and it's an easy adjustment in your codebase. So, for your entity configuration of modelBuilder.Entity<Professional> - Professional class should be the one coming from your domain instead of infrastructure. Ardalis is also using this approach if you take a look at his sample again.

As an alternative, you could make the implementation of a repository on the infrastructure layer aware of two different models and do the mapping within the implementation of your repository itself. You'll have to pass two different generic types to the Repository<TDomainEntity, TDbModel> and declare it correctly during DI registration. However, the repository interface itself must accept domain entities only because you shouldn't leak database models outside of your infrastructure layer.

public class Repository<TDomainEntity, TDbModel>
    : IRepository<TDomainEntity>
    where TDomainEntity : class
    where TDbModel : class
{
    protected readonly MyDBContext _dbContext;
    protected readonly IMapper _mapper;
    
    public Repository(MyDBContext dbContext, IMapper mapper)
    {
        _dbContext= dbContext;
        _mapper = mapper;
    }
        
    public async Task<TDomainEntity> AddAsync(TDomainEntity entity)
    {
        TDbModel model = _mapper.Map<TDbModel>(entity);
        await _dbContext.Set<TDbModel>().AddAsync(model);
        await _dbContext.SaveChangesAsync();
        return entity;
    }
}

I'd say there are multiple cons to doing it like this. You have an unnecessary layer of mapping since you can manipulate the mapping of your domain entity to the database table through your model configuration. This also increases the number of changes you have to make if any new requirement comes up in your model. You'll also have to work on some reflection magic to match your domain entity and database model based on naming convention or something or do a bunch of registrations manually.

msmolcic
  • 6,407
  • 8
  • 32
  • 56
  • The big, unsolvable bug is using this "repository" in the first place. `SaveChanges` will persist all changes in all entities. This class is simply broken, as a DbSet is already a Repository, and a DbContext is a multi-entity Unit of Work. Trying to shoehorn that low-level "repository" over the high-level Unit-of-Work will always break down one way or another. – Panagiotis Kanavos Jan 27 '22 at 10:25
  • @PanagiotisKanavos I understand your point, but the repository pattern in this scenario has the purpose of hiding the dependency on any 3rd party dependency such as Entity Framework from the application layer and keeping it only as infrastructure layer dependency. If you inject `DbContext` to the application layer you're tight coupling it to the Entity Framework. – msmolcic Jan 27 '22 at 10:31
  • @PanagiotisKanavos However, I agree with your other comment that this specific implementation of the repository pattern is bad. Saving changes after each add/update/remove action is insanity. What I usually do when I'm trying to hide the 3rd party dependencies of ORM is an abstraction of `UnitOfWork` through `IUnitOfWork` interface which exposes different entities that are attached to the same context underneath with the explicit methods to save changes and begin/commit transaction. – msmolcic Jan 27 '22 at 10:32
  • Attempting to hide the fact that your domain uses EF is akin to telling your chef to prepare meals wearing a straight jacket... You might find some exceptional specimens that can actually pull it off, but at the end of the day you really need to ask: "WHY?!" Generic repositories with methods like that just cripple the performance you can achieve with EF's IQueryable and Linq capabilities, either that or you're left writing very complex code to try and reinvent the wheel which is ultimately dependent om consumers knowing/obeying EF's working behind the scenes anyways. – Steve Py Jan 27 '22 at 12:21
  • @StevePy I'm not asking you to do it this way, it's a free choice. I was just explaining the reason behind the decision. :) There's a situation where it's perfectly fine to inject DbContext into your API controller and "get s**t done". – msmolcic Jan 27 '22 at 13:00
  • @msmolcic thanks for answering to my question. As you said there are cons of using the 2nd solution and I too feel it would be hectic to maintain 2 same type of entities in both domain and infrastructure layer. If I understand correctly your first solution, you have advised to replace the domain entity with model builder. In this case, what will be the use of Infra layer entities which are generated as a part of scaffolding from database. – Mohd Ilyas Jan 29 '22 at 07:54
  • (--continue @msmolcic) Can I totally remove the infra structure entities as I exactly created the same set of entities in the domain layer and I will use it with model builder. But, my question is doesn't it brake the clean architecture rules and how healthy it will be in future for scalability and maintenance ? – Mohd Ilyas Jan 29 '22 at 07:58
  • @MohdIlyas I'd say yes, you can remove infrastructure entities completely. I haven't had any scalability/maintenance issues with it so far. If you find an issue with it, I'm sure you'll also find a way to cross that bridge in some way once you get there. – msmolcic Feb 02 '22 at 11:22