C# n-layer entity changes not reflected in DTO after insert

118 views Asked by At

Sorry about the title, I couldn't figure out a suitable description.

I have four layers:

  • Core Layer : Contains DTO's, interfaces for services and repositories.
  • Business Layer : Contains "services" that handle business logic.
  • Data Access Layer : Contains repositories that handle database access and conversion of entities to DTO's.
  • Presentation Layer : UI stuff

I've run into an issue that I don't know how to resolve best. I am asynchronously adding an entity to the database, like so:

// The AdministrationRate has an ID property, Entity Framework does treat the property as an Identity and does increment it.
var adminRate = new AdministrationRate() {FosterChildID = fosterChild.ID};
await adminRateService.AddAdministrationRate(adminRate);

AdministrationRateService:

public async Task AddAdministrationRate(AdministrationRate administrationRate) => await repo.AddAdministrationRate(administrationRate);

AdministrationRateRepository:

 //I use a generic repository to avoid code repition.
 //Notice the DTO to entity conversion. ID is not set in the conversion.
 public async Task AddAdministrationRate(AdministrationRate administrationRate) => await Add(administrationRate.ToEntity());

Repository:

public async Task Add(TEntity entity)
{
    using (var db = new DatabaseContext())
    {
        db.Entry(entity).State = EntityState.Added;
        await db.SaveChangesAsync();
    }
}

And that does work, the problem is that after it has been added the newly generated id of the entity has not been reflected in the DTO (DTO.ID = 0).

I can't possibly return the updated entity because it would require me to convert the entity to a DTO in my presentation layer to keep it async all the way.

I also can't return just the ID because of the generic repository.

I really don't want to get rid of the generic repository as it is quite useful, but I can't really see another way to do it securely without altering the database (which I can't do).

So, what do I do?

2

There are 2 answers

0
Starwop On BEST ANSWER

Answer: I found two solutions to this issue, the latter being the one I'm actually going with.


Solution 1:

I figured out that I could make the Add method return the ID of an entity, by marking the Add method of the generic repository virtual, then overriding it in one of the specific repositories and finally catch the result from the calling code. This would of course mean more code, but also that I would not be converting from entities to DTO's in the wrong layer (Not that that was an option).


Solution 2:

Apparently the await keyword returns the result type of a task asynchronously, instead of blocking it like result, so I was able to convert and return a DTO from my repository:

AdministrationRateRepository:

public async Task<AdministrationRate> AddAdministrationRate(AdministrationRate administrationRate) => (await Add(administrationRate.ToEntity())).ToModel();

I would like if someone could comment wether I need to make my ToEntity() and ToModel() methods async, and how that would be done.

EDIT: Oh, didn't notice someone had posted. I'll have a look at your suggestions Akos, thanks.

0
Akos Nagy On

Here's a couple of ideas that I'd try. Not sure if any of them suits your needs, but might get you started (or start a discussion at least :) ):

1; Introduce an interface for your DTOs, like IDto. Then you can change the generic repository to return Task<IDto> instead of Task. Convert the entity to the concrete Dto inside the Add method, return through the IDto reference, then convert it back in the service.

2; If you have a generic repository like IRepository<TEntity>, or Repository<TEntity>, then you might try introducing another parameter to the generic repository and make it an IRepository<TEntity,TDto>. This way you can keep the generic nature of your architecture, but also return Task<Dto> from the Add method.

3; You can go even further: create an IDto<TEntity> interface. And then you can have a User entity and a UserDto:IDto<User>. Then you can change the return type of the Add method to Task<IDto<TEntity>>. Or, again, use the two-parametered repository IRepository<TEntity,TDto>, and add constraints, so that TDto must implement IDto<TEntity>.