Help with clean architecture

I wondered how I could return the DTO without breaking anything. Here is the code from the service where I access the entity.
public async Task<IReadOnlyList<Todo>> GetAllTodosAsync()
{
var result = await _repository.GetAllAsync();
if (result is not null)
return result;
else
throw new Exception("List is null");
}
public async Task<IReadOnlyList<Todo>> GetAllTodosAsync()
{
var result = await _repository.GetAllAsync();
if (result is not null)
return result;
else
throw new Exception("List is null");
}
And here in the controller
[HttpGet]
public async Task<ActionResult<IReadOnlyList<Todo>>> GetAll()
{
var todos = await _todoService.GetAllTodosAsync();
return Ok(todos);
}
[HttpGet]
public async Task<ActionResult<IReadOnlyList<Todo>>> GetAll()
{
var todos = await _todoService.GetAllTodosAsync();
return Ok(todos);
}
I could create a mapper right here in the controller, but is that a good approach? If I'm doing anything, it's purely because I don't like the way it glows in the swager. Here: and the DTO that should replace the Todo entity itself looks virtually identical to it, but the point is that I don't think it's a good idea to expose the entity.
public record TodoResponseDto(...)
public record TodoResponseDto(...)
No description
2 Replies
Sossenbinder
Sossenbinder2w ago
If you need an API model / dto to map to, I'd totally do that in the controller That's definitely the right layer for that Where else should it fit
kurumi
kurumi2w ago
In clean architecture each layer has it's own abstractions and models. And it is recommended to map each model from layer to layer For example if you have a ToDo model in application layer, the abstraction to retrive it (some repository interface) and some use case flow
// Application layer
public record ToDo(Guid Id, string Task);

public interface IToDoRepository
{
Task<ToDo?> GetToDoById(Guid todoId, CancellationToken cancelationToken);
}

public record GetToDoByIdRequest(Guid ToDoId);

public class RetriveToDoRequestHander(IToDoRepository toDoRepository, ILogger<RetriveToDoRequestHander> logger) : IRequestHandler<GetToDoByIdRequest, ToDo>
{
private readonly IToDoRepository _toDoRepository = toDoRepository;
private readonly ILogger<RetriveToDoRequestHander> _logger = logger;

public async Task<ToDo?> Handle(GetToDoByIdRequest request, CancellationToken cancellationToken)
{
var toDo = await _toDoRepository.GetToDoById(request.ToDoId, cancellationToken);
if (toDo is null)
{
// just logging if there is no ToDo with this Id.
// you can do what ever you want here, it's just a very dummy example.
_logger.LogInformation("No ToDo with {ToDoId}", request.ToDoId);
}

return toDo;
}
// Application layer
public record ToDo(Guid Id, string Task);

public interface IToDoRepository
{
Task<ToDo?> GetToDoById(Guid todoId, CancellationToken cancelationToken);
}

public record GetToDoByIdRequest(Guid ToDoId);

public class RetriveToDoRequestHander(IToDoRepository toDoRepository, ILogger<RetriveToDoRequestHander> logger) : IRequestHandler<GetToDoByIdRequest, ToDo>
{
private readonly IToDoRepository _toDoRepository = toDoRepository;
private readonly ILogger<RetriveToDoRequestHander> _logger = logger;

public async Task<ToDo?> Handle(GetToDoByIdRequest request, CancellationToken cancellationToken)
{
var toDo = await _toDoRepository.GetToDoById(request.ToDoId, cancellationToken);
if (toDo is null)
{
// just logging if there is no ToDo with this Id.
// you can do what ever you want here, it's just a very dummy example.
_logger.LogInformation("No ToDo with {ToDoId}", request.ToDoId);
}

return toDo;
}
here as you see I am getting a ToDo model from abstraction IToDoRepository which defined in same application layer. How you implement this abstraction is the problem of infrastructure layer. You can store it in your database not the same ToDo as previously, but with some additional props if you wish to. These two layers separated and only infrastructure responsible for mapping to application
// Infrastructure layer
public record ToDoEntity(Guid Id, string Task, DateTimeOffset CreatedAt)
{
public ToDo MapToApplication()
{
return new ToDo(Id, Task);
}
}

// I assume you are using EFCore
public class ToDoDbContext(DbContextOptions<ToDoDbContext> options) : DbContext<ToDoDbContext>(options)
{
public DbSet<ToDoEntity> ToDoes { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);

// some configuration here
}
}

public class ToDoRepository(ToDoDbContext dbContext) : IToDoRepository // we implement application layer abstraction here
{
private readonly ToDoDbContext _dbContext = dbContext;

public Task<ToDo?> GetToDoById(Guid todoId, CancellationToken cancellationToken)
{
var toDoEntity = _dbContext.ToDoes.FindAsync(toDoId, cancellationToken);
return toDoEntity.MapToApplication();
}
}
// Infrastructure layer
public record ToDoEntity(Guid Id, string Task, DateTimeOffset CreatedAt)
{
public ToDo MapToApplication()
{
return new ToDo(Id, Task);
}
}

// I assume you are using EFCore
public class ToDoDbContext(DbContextOptions<ToDoDbContext> options) : DbContext<ToDoDbContext>(options)
{
public DbSet<ToDoEntity> ToDoes { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);

// some configuration here
}
}

public class ToDoRepository(ToDoDbContext dbContext) : IToDoRepository // we implement application layer abstraction here
{
private readonly ToDoDbContext _dbContext = dbContext;

public Task<ToDo?> GetToDoById(Guid todoId, CancellationToken cancellationToken)
{
var toDoEntity = _dbContext.ToDoes.FindAsync(toDoId, cancellationToken);
return toDoEntity.MapToApplication();
}
}
Also my code lacks of Domain and Presentation layers abstraction, but the idea that mapping should be only one way. Deeper layers don't know anything about previous layers and also clean architecture is not very good with EFCore, you gonna end up recreating entire EFCore on your infrastructure layer with repositories, since DbSet<T> is already the one I personally like to create a Map method (or extension method) on model itself, so it can produce deeper layer model. And this should be done on your controller

Did you find this page helpful?