It's important to understand your application in terms of it's architecture. I think your confusion is likely being caused by a lack of understanding as to why applications are structured the way they are. Starting with the basics, a traditional "layered" architecture, will look something like:
View -> Application -> Domain -> Persistence
The idea being that each layer is completely decoupled from the layers above it, such that the flow of dependencies in "downward". The management of dependencies and coupling is the most critical concern of any architecture! What the above accomplishes is the ability to "swap" out any layer in a way that we know what other pieces of the application may have to be changed as a result (the layer(s) directly above). With this powerful idea in mind, it makes sense to structure your dependencies in a way where:
Volatile Modules -> Stable Modules
Because your Domain
is likely the least volatile module (almost certainly not going to be swapped out), we can use the DIP (dependency inversion principal) to "fix" the layered architecture above to:
View -> Application -> Domain <- Persistence
Now we have reached the architecture you are describing in your application. Now let's go through each layer and figure out for what each should be responsible.
Your View
layer is where your controllers live. Each Controller
simply mediates the flow of data (and control) towards your Application
layer. Importantly, your controllers don't contain business logic. They only contain logic necessary to transform input from the outside world into concepts your Application
layer can understand, and transform the output from your Application
layer into concepts the outside world can understand. If we "swapped" out your View
layer (or added another one) from a web interface to command line interface, you would need new controllers. The inputs/outputs to the View
would necessarily be different.
The Application
layer is used to carryout use-cases for your users. It does this by coordinating your Domain
. Again, it is important that this layer does not contain business logic. For example, for changing a password:
class ChangePasswordCommand
{
private string newPassword;
public string UserId { get; set; }
public string CurrentPassword { get; set; }
public string NewPassword
{
get { return newPassword; }
set {
if( value.Length > 50 ) {
throw new System.ArgumentException("New password must be less than 50 characters");
}
newPassword = value;
}
}
}
class ChangePasswordCommandHandler
{
private IUserRepository userRepository;
handle( ChangePasswordCommand cmd )
{
userId = cmd.UserId;
newPassword = cmd.NewPassword;
currentPassword = cmd.CurrentPassword;
user = this.userRepository.FindById(userId);
// this may raise a UserChangedPasswordDomainEvent
// or throw a PasswordIsNotDifferentException
user.changePassword(newPassword, currentPassword);
this.userRepository.save(user)
}
}
Notice there is no "logic" occurring here that pertains to the actual changing of a password. The invariant that the ChangePasswordCommand
is protecting really has nothing to do with your business logic. That is, your CEO probably isn't worried about the length of a Users
password. The password length invariant is a technical detail of the system (say your backing database field is VARCHAR(50) or something), and we want to "fail fast".
The command handler, itself, simply takes some input that it understands (ChangePasswordCommand
), and coordinates your domain model to carryout the command. Things like transaction management should also occur in your Application
layer. Think of every use-case as a 3-step problem:
- Get pieces of domain model into memory that are needed to carryout use-case.
- Coordinate domain model (change state)
2.5. Handle/bubble exceptions
- Persist domain model
It's also worth noting that the dependency on IUserRepository
. This interface is defined in your Domain
layer, but is implemented in your Persistence
layer (this is the DI we added).
Following the flow of control, we reach your Persistence
layer. This is where your ORM exists (and only here!). For example the UserRepository.FindById
might look like:
public User FindById( string userId )
{
// 1. get UserEntity
// 2. map UserEntity to User
// 3. return User
}
There is really no need for some extra DTO or anything unless you intend on using some library for mapping which requires a certain format. Again, the idea here is that this layer can be completely replaced and NO OTHER part of your application would even know the difference. The only vectors of change for this layer are when you change/update your ORM or the Domain
.
Finally, we get you your Domain
. This is where everything is happening. This is where we make sure that 2 passwords cannot be the same, and mutate the domain model accordingly. Not too much explanation necessary here.
Lastly, I'd like to make a note here that ALL OF THE ABOVE is really only necessary in the context of WRITING to a system. You can read from it however you want (there is no danger of an invalid state during a read). If you need user permissions, just get them. Don't over-complicate it. Get the data you need to your View
however you want, BUT make sure you only write data through the proper channels (your Domain
). This is the idea of CQRS.
You are free to create a separate read model project, but there is no concept of, or need for, a Domain
in this project so it will end up being quite redundant. Best to just define some read services to organize it if you wish. I can understand the confusion of some Controller
methods (reads) using one project, and other methods (writes) using another, but a View
needs both.