Reputation: 53
Currently I'm trying to migrate legacy application to some api's using Clean Architecture. Until now I was able to go through changes, but every time I encounter a DTO I cannot understand how to place it in the clean architecture. By DTO, I am referring to: Object containing multiple properties from domain entities combined. I'm using DTO's because the database is still in "legacy format" but the api must expose diferent formats of responses across multiple systems.
Let's say I have the following structure:
Domain:
public class EntityA
{
public string Prop1{get; set;}
}
public class EntityB
{
public string Prop2{get; set;}
}
Then I have a interface to a Service as follow:
public interface IService
{
}
In the application layer (Use Cases) I have the implementation of the services described in the Domain and the DTO itself:
public class DTO
{
public string Prop1{get; set;}
public string Prop2{get; set;}
}
public class Service : IService
{
public IEnumerable<DTO> RetrieveDto()
{
return new DTO()//Construct DTO....
}
}
And here my issue is starting.
I need to modify the domain service interface to return the DTO. This is generating a circular reference and I don't think is ok to be done.
I tried to create an abstract DTO class in the domain and inherit from it to avoid the reference from Domain to Application. But I'm not pretty sure this should be a solution because DTO's are just object that store data, I don't have anything in that abstract class.
Currently, the mapper and the DTO are placed in the Application because from the application I access the Infrastructure for repositories and here is where I map the entity to a DTO.
So my question is: Do I understand something wrong here? Where should be DTO places correctly?
Thank you!
Upvotes: 2
Views: 9158
Reputation: 53
So after couple of weeks of trying to understand this I can say the following.
This is one of the diagram presented by uncle bob in a lecture. The delivery mechanism (which in an API is the controller) is talking with the interactor (Use case / Service) through the boundary (Interface). This means that the Request/Response must be specific to a use-case.
Please correct me if I understood wrong :)
Thank you!
Upvotes: 0
Reputation: 123
Domain Layer:
Models - contains concrete classes with properties and behaviors relevant to domain. They don't depend on anything they are the core to domain layer itself.
Services - domain services are concrete classes which contain business rules which does not fit within a domain model.
Events - contain only domain event POCO.
DTO - contains only interfaces for each entities and value object. Should be implemented in persistence layer as data models.
Factories - contains interfaces and its implementation which accepts DTO interface to create a new instance of domain aggregate root. Would be utilized in persistence layer and application services in application layer.
Application Layer:
Repositories - interfaces for persisting and fetch domain aggregate root object.
Integration Events - contains concrete POCO clases and interfaces for handling integration event. Use for propagating domain state change to other applications. Example: To other micro services or workser service via event bus.
Event Bus - interfaces for implementing event bus.
Services - they are application services which contains use cases. They orchestrate the interfaces available within domain layer and its own layer, for accomplishing the use case. They return DTO interface type to outside layers for queries. Controllers in webapi or mvc application will consume these services.
DTO - interfaces for returning data to outer world.
Mappers - contains interfaces to map a domain object to DTO object and vice versa. Would be utilized in application layer, presentation/api layer and implemented in infrastructure layer.
Domain Layer - Models:
namespace Acme.Core.Domain.Identity.Models.AccountAggregate
{
public class Account : Aggregate<Account, AccountId>
{
public Account(AccountId id, string userName, string normalizedUserName, string passwordHash, string concurrencyStamp, string securityStamp, string email, string normalizedEmail, bool emailConfirmed, string phoneNumber, bool phoneNumberConfirmed, bool twoFactorEnabled, DateTimeOffset? lockoutEnd, bool lockoutEnabled, int accessFailedCount, AccountStatus status, List<RoleId> roles, List<AccountClaim> accountClaims, List<AccountLogin> accountLogins, List<AccountToken> accountTokens) : base(id)
{
UserName = Guard.Against.NullOrWhiteSpace(userName, nameof(userName));
NormalizedUserName = Guard.Against.NullOrWhiteSpace(normalizedUserName, nameof(normalizedUserName));
PasswordHash = Guard.Against.NullOrWhiteSpace(passwordHash, nameof(passwordHash));
ConcurrencyStamp = concurrencyStamp;
SecurityStamp = securityStamp;
Email = Guard.Against.NullOrWhiteSpace(email, nameof(email));
NormalizedEmail = Guard.Against.NullOrWhiteSpace(normalizedEmail, nameof(normalizedEmail));
EmailConfirmed = emailConfirmed;
PhoneNumber = phoneNumber;
PhoneNumberConfirmed = phoneNumberConfirmed;
TwoFactorEnabled = twoFactorEnabled;
LockoutEnd = lockoutEnd;
LockoutEnabled = lockoutEnabled;
AccessFailedCount = accessFailedCount;
Status = Guard.Against.Null(status, nameof(status));
_roles = Guard.Against.Null(roles, nameof(roles));
_accountClaims = accountClaims;
_accountLogins = accountLogins;
_accountTokens = accountTokens;
}
public string UserName { get; private set; }
public string NormalizedUserName { get; private set; }
public string PasswordHash { get; private set; }
public string ConcurrencyStamp { get; private set; }
public string SecurityStamp { get; private set; }
public string Email { get; private set; }
public string NormalizedEmail { get; private set; }
public bool EmailConfirmed { get; private set; }
public string PhoneNumber { get; private set; }
public bool PhoneNumberConfirmed { get; private set; }
public bool TwoFactorEnabled { get; private set; }
public DateTimeOffset? LockoutEnd { get; private set; }
public bool LockoutEnabled { get; private set; }
public int AccessFailedCount { get; private set; }
public AccountStatus Status { get; private set; }
private List<RoleId> _roles;
public IReadOnlyCollection<RoleId> Roles
{
get
{
return _roles;
}
}
private List<AccountClaim> _accountClaims;
public IReadOnlyCollection<AccountClaim> AccountClaims
{
get
{
return _accountClaims;
}
}
private List<AccountLogin> _accountLogins;
public IReadOnlyCollection<AccountLogin> AccountLogins
{
get
{
return _accountLogins;
}
}
private List<AccountToken> _accountTokens;
public IReadOnlyCollection<AccountToken> AccountTokens
{
get
{
return _accountTokens;
}
}
public void AddRole(long roleId)
{
var role = _roles.Where(x => x.GetValue().Equals(roleId)).FirstOrDefault();
if (role == null)
{
_roles.Add(new RoleId(roleId));
}
}
public void RemoveRole(long roleId)
{
var role = _roles.Where(x => x.GetValue().Equals(roleId)).FirstOrDefault();
if (role == null)
{
_roles.Remove(role);
}
}
public void ActivateAccount()
{
Status = AccountStatus.Active;
}
public void BanAccount()
{
Status = AccountStatus.Banned;
}
public void CloseAccount()
{
Status = AccountStatus.Closed;
}
public void LockAccount()
{
Status = AccountStatus.Locked;
}
public void NewAccount()
{
Status = AccountStatus.New;
}
}
}
Domain Layer - DTO:
namespace Acme.Core.Domain.Identity.DTO
{
public interface IAccountDto
{
long Id { get; set; }
string UserName { get; set; }
string NormalizedUserName { get; set; }
string PasswordHash { get; set; }
string ConcurrencyStamp { get; set; }
string SecurityStamp { get; set; }
string Email { get; set; }
string NormalizedEmail { get; set; }
bool EmailConfirmed { get; set; }
string PhoneNumber { get; set; }
bool PhoneNumberConfirmed { get; set; }
bool TwoFactorEnabled { get; set; }
DateTimeOffset? LockoutEnd { get; set; }
bool LockoutEnabled { get; set; }
int AccessFailedCount { get; set; }
int StatusId { get; set; }
ICollection<long> Roles { get; set; }
ICollection<IAccountClaimDto> Claims { get; set; }
ICollection<IAccountLoginDto> Logins { get; set; }
ICollection<IAccountTokenDto> Tokens { get; set; }
}
}
Domain Layer - Factories:
namespace Acme.Core.Domain.Identity.Factories
{
public interface IAccountFactory
{
Account Create(IAccountDto dto);
AccountId Create(long id);
}
}
namespace Acme.Core.Domain.Identity.Factories
{
public class AccountFactory : IAccountFactory
{
private readonly IAccountClaimFactory _accountClaimFactory;
private readonly IAccountLoginFactory _accountLoginFactory;
private readonly IAccountTokenFactory _accountTokenFactory;
private readonly IRoleFactory _roleFactory;
public AccountFactory(IAccountClaimFactory accountClaimFactory, IAccountLoginFactory accountLoginFactory, IAccountTokenFactory accountTokenFactory, IRoleFactory roleFactory)
{
_accountClaimFactory = Guard.Against.Null(accountClaimFactory, nameof(accountClaimFactory));
_accountLoginFactory = Guard.Against.Null(accountLoginFactory, nameof(accountLoginFactory));
_accountTokenFactory = Guard.Against.Null(accountTokenFactory, nameof(accountTokenFactory));
_roleFactory = Guard.Against.Null(roleFactory, nameof(roleFactory));
}
public Account Create(IAccountDto dto)
{
AccountId aggregateId = Create(dto.Id);
AccountStatus status;
if (dto.StatusId.Equals(0))
{
status = AccountStatus.New;
}
else
{
status = AccountStatus.FromValue<AccountStatus>(dto.StatusId);
}
List<RoleId> roles = new List<RoleId>();
foreach (long roleid in dto.Roles)
{
roles.Add(_roleFactory.Create(roleid));
}
List<AccountClaim> accountClaims = new List<AccountClaim>();
foreach (var claim in dto.Claims)
{
accountClaims.Add(_accountClaimFactory.Create(claim));
}
List<AccountLogin> accountLogins = new List<AccountLogin>();
foreach (var login in dto.Logins)
{
accountLogins.Add(_accountLoginFactory.Create(login));
}
List<AccountToken> accountTokens = new List<AccountToken>();
foreach (var token in dto.Tokens)
{
accountTokens.Add(_accountTokenFactory.Create(token));
}
return new Account(aggregateId, dto.UserName, dto.NormalizedUserName, dto.PasswordHash, dto.ConcurrencyStamp, dto.SecurityStamp, dto.Email, dto.NormalizedEmail, dto.EmailConfirmed, dto.PhoneNumber, dto.PhoneNumberConfirmed, dto.TwoFactorEnabled, dto.LockoutEnd, dto.LockoutEnabled, dto.AccessFailedCount, status, roles, accountClaims, accountLogins, accountTokens);
}
public AccountId Create(long id)
{
return new AccountId(id);
}
}
}
Application Layer - Repositories:
namespace Acme.Core.Application.Identity.Repositories
{
public interface IAccountRepo : ICreateRepository<Account>, IReadRepository<Account, AccountId>, IUpdateRepository<Account>
{
}
}
Application Layer - Integration Events:
namespace Acme.Core.Application.Identity.IntegrationEvents.Events
{
public record AccountCreatedIntegrationEvent : IntegrationEvent
{
public AccountCreatedIntegrationEvent(string accountName, string emailAddress, string token)
{
AccountName = accountName;
EmailAddress = emailAddress;
Token = token;
}
public string AccountName { get; }
public string EmailAddress { get; }
public string Token { get; }
}
}
Application Layer - Application Services:
namespace Acme.Core.Application.Identity.Services
{
public interface IAccountService
{
Task<bool> RegisterAsync(IAccountDto dto);
Task<bool> ActivateAsync(string emailAddress, string activationCode);
Task<bool> RecoverUsernameAsync(string emailAddress);
Task<bool> ResetPasswordAsync(string emailAddress);
Task<bool> ChangePasswordAsync(string username, string currentPassword, string newPassword);
Task<bool> CloseAccountAsync(string username);
Task<bool> LockAccountAsync(string username);
Task<bool> BanAccountAsync(string username);
Task<bool> GenerateTokenForExistingEmailAddressAsync(string username);
Task<bool> ChangeEmailAddressAsync(string username, string activationCode, string newEmailAddress);
Task<bool> ActivateNewEmailAddressAsync(string emailaddress, string activationCode);
Task<bool> GenerateTokenForPhoneNumberAsync(string username, string phoneNumber);
Task<bool> ChangePhoneNumberAsync(string username, string phoneNumber, string activationCode);
}
}
Upvotes: 2
Reputation: 195
DTOs are Data Transfer Objects. They should be used when there is a network call involved because they are lightweight. Entities can be heavy and contain domain logic which may not be necessary to be transmitted over a network. DTOs are used to only pass data without exposing your domain entities. Say for example, when your API returns a response to a client app, make use of a DTO.
Since your domain service will be in the Domain layer, you can directly make use of your entities. I don't think this is a right use case for a DTO. You are correct to place your DTOs and their mappings in the Application layer. The Domain layer should never directly communicate with the outside world.
Upvotes: 0
Reputation: 176
Most popular architecture style : Layer 1
Upvotes: -1
Reputation: 6060
I think it is not accurate that you see the DTO so separate from the entities. After all, if your use case needs to return this data structure, it belongs to / under the use case.
Side note: I also dislike the term "dto" as this does not specify anything out of the ordinary. (Nearly all objects contain data and are transferred) But on to your use case: I would rename the DTO to "UseCaseXResponse" and then put it next to the other entities. All the entites would then consist of some input-oriented ones, some output-oriented ones and maybe also some general purpose ones. The logic, how to convert the input ones to the output ones is in the use case class.
If you feel that this data agglomeration has no place in your business logic, then you need to expose other entites to the outer layer and use that outer layer to aggregate the response into a dto.
Upvotes: -1
Reputation: 6801
You must define the DTO in the domain layer, along with the service's interface. The DTO is essentially part of the service's interface definition and it doesn't make any sense without it.
When you think about implementations of that service, in outer layers, those implementations would all share the ability to return that DTO type, despite different internal implementations.
The domain layer methods that depend on the service's defined interface also depend on the definded DTO as the service's method(s) return type.
Something like:
public class Domain.DTO
{
public string Prop1{get; set;}
public string Prop2{get; set;}
}
public interface Domain.IService
{
DTO DoSomething();
}
public class Domain.EntityA
{
public string Prop1{get; set;}
// Super-contrived, but you get the point...
public void DoSomethingWith(IService service)
{
// regardless of the actual implementation,
// I know result will always be DTO-shaped
var result = service.DoSomething();
}
}
public class Application.ServiceOne : Domain.IService
{
public Domain.DTO DoSomething()
{
// Do something one way, but providing the agreed DTO
}
}
public class Application.ServiceTwo : Domain.IService
{
public Domain.DTO DoSomething()
{
// Do something another way, but still providing the agreed DTO
}
}
This maintains all dependencies travelling inwards as promoted by the architecture.
Upvotes: -1