After what I've felt is a sufficient amount of research, I have made my first attempt at a domain driven design MVC project. I would love for you guys to tear it apart and give me pointers to make it better.
The actual "data tier" has 2 projects, MyProject.Data
and MyProjects.Model
. I wont go into detail on the Model project, it's quite literally just POCOs for Entity Framework Code First and the DBContext
. For this example I'll be using my user_account
entity.
MyProject.Data
is effectively a repository layer, I have a base repository and interface that will expose commonly used EF operations:
public interface IBaseRepository<T> : IDisposable where T : class
{
IQueryable<T> GetAll();
IQueryable<T> FindBy(Expression<Func<T, bool>> predicate);
void Add(T entity);
void Delete(T entity);
void Edit(T entity);
void Save();
}
public abstract class BaseRepository<C, T> :
IBaseRepository<T> where T : class where C : DbContext, new()
{
private C _entities = new C();
protected C Context
{
get { return _entities; }
set { _entities = value; }
}
public virtual IQueryable<T> GetAll()
{
IQueryable<T> query = _entities.Set<T>();
return query;
}
public IQueryable<T> FindBy(System.Linq.Expressions.Expression<Func<T, bool>> predicate)
{
IQueryable<T> query = _entities.Set<T>().Where(predicate);
return query;
}
public virtual void Add(T entity)
{
_entities.Set<T>().Add(entity);
}
public virtual void Save()
{
_entities.SaveChanges();
}
etc....
}
And then I have a concrete class/interface combo specifically for each entity. Here's UserRepository.cs
:
public interface IUserRepository : IBaseRepository<user_account>
{
void CreatePasswordReset();
}
public class UserRepository : BaseRepository<DBContext, user_account>, IUserRepository
{
public void CreatePasswordReset();
}
This allows the Data project to expose an interface for each entity and will by default have basic CRUD operations available. More operations for the repository can be added on using its specific repository, like CreatePasswordReset
.
Next up is MyProject.Domain, aka my service layer. I feel that this layer could definitely use some pointers and refining. It feels repetitive. This layer contains a Models
folder that is client friendly and will act as an intermediary between any clients and the repository. Here's an example of MyProject.Domain.Models.User.cs
:
public class User
{
public int Id { get; set; }
[MaxLength(20)]
[Required]
[DisplayName("First Name")]
public string FirstName { get; set; }
[MaxLength(30)]
[Required]
[DisplayName("Last Name")]
public string LastName { get; set; }
[MaxLength(50)]
[DisplayName("Address Line 1")]
public string Address1 { get; set; }
public string FullName => $"{FirstName} {LastName}";
}
The second functionality of the Domain layer is a little more in depth, but similar to the repository layer. Each Model gets it's own Interface/Concrete class that will perform operations on it's respective repository and return any data in the friendly, ready to be consumed format that the Model offers. Here's the MyProject.Domain.Services.(I)UserService.cs
example:
public interface IUserService
{
IEnumerable<User> GetAll();
User GetOne(int id);
}
public class UserService : IUserService
{
private IUserRepository _userRepository { get; set; }
public UserService(IUserRepository userRepository)
{
if (userRepository == null) throw new ArgumentNullException(nameof(userRepository));
_userRepository = userRepository;
}
public IEnumerable<User> GetAll()
{
var users = new List<User>();
var usersList = _userRepository.GetAll();
foreach(var user in usersList)
{
users.Add(Convert(user));
}
return users;
}
public User GetOne(int id)
{
return Convert(_userRepository.GetOne(id));
}
#region Conversion
private User Convert(user_account userAccount)
{
if (userAccount == null) return new user_account();
return new User
{
FirstName = userAccount.first_name,
LastName = userAccount.last_name,
Id = userAccount.user_id
};
}
#endregion
}
The idea here is kind of the same as the repository layer, we expose the interface and do the conversions and operations in the concrete class. Most articles/blogs/whatever I've read recommend using an interface for your service/domain layer, but I'm having a very hard time understanding what benefits are gained here with the interfaces versus just creating a new instance of the UserService
and running with it.
This has gotten so long and tiring that I wont go into great depth of the MVC project unless it's requested. Basically Unity dependency injection is used to bind the interfaces to the concrete classes, and the services are used in the controllers exactly like the repositories are used in the services. The controllers know absolutely nothing about the repository layer, it is 100% decoupled. The ViewModels on the MVC project are still used, but they're created specific for each view, they may contain a reference to the Domain.Model
classes, and some select SelectListItem
shenanigans.
This whole project has been a huge learning curve for me, can't wait to get some feedback.