I am currently practising using the SOLID principles in C#/.NET
I have made a little example, but i am not sure if i have followed it correct. Its a simple example where i have an API controller that calls a method to get a list of users from an database and returned as DTOs.
To use the method i need to submit an IUserReader that is mainly doing the reading operation from the database and then a IMapper object is needed that will handle the mapping from database entity to DTO.
I made it that way to be available to switch out the IUserReader and IMapper so i can fit the query and mapping to match my requirements for a specific API call. Lets say an app is using the API to get users, the app will only need some details of the user, so we use a implementation that only selects the required information and maps it correct. We also have an backend to view the users, where we want all the user details from the database, here we use another implementation for that.
Here is my code example, i would like to know if it is done correct.
Data readers
public interface IDataReader<T>
{
IEnumerable<T> Read();
}
public abstract class DataReader<T> : IDataReader<T>
{
private protected IDataContext _context;
public abstract IEnumerable<T> Read();
public DataReader(IDataContext context)
{
_context = context;
}
}
public interface IUserDataReader : IDataReader<IUserEntity>
{
}
//First implementation of user reader
public class UserDataReader : DataReader<IUserEntity>, IUserDataReader
{
public UserDataReader(IDataContext context) : base(context) { }
public override IEnumerable<IUserEntity> Read()
{
return _context.Users.Where(x => x.IsActive).OrderBy(x => x.Name).ToList();
}
}
//Secound implementation of user reader
public class UserGridDataReader : DataReader<IUserEntity>, IUserDataReader
{
public UserGridDataReader(IDataContext context) : base(context) { }
public override IEnumerable<IUserEntity> Read()
{
return _context.Users.OrderBy(x => x.Name).ToList();
}
}
Now the mappers:
public interface IMapper<I, O>
{
O Map(I item);
}
public interface IUserMapper : IMapper<IUserEntity, UserDTO>
{
}
public class UserMapper : IUserMapper
{
public UserDTO Map(IUserEntity item)
{
return new FullUserDTO
{
Name = item.Name,
Email = item.Email
};
}
}
The API then calls this class and method to read:
public class UserReaderService
{
private IUserDataReader _reader;
private IUserMapper _mapper;
public UserReaderService(IUserDataReader reader, IUserMapper mapper)
{
_reader = reader;
_mapper = mapper;
}
public IEnumerable<UserDTO> Read()
{
IEnumerable<IUserEntity> userData = _reader.Read();
IEnumerable<UserDTO> users = userData.Select(x => _mapper.Map(x));
return users;
}
}
API method:
public void GetUsers()
{
IDataContext context = new DataContext();
IUserDataReader userDataReader = new UserDataReader(context);
IUserMapper mapper = new UserMapper();
UserReaderService ur = new UserReaderService(userDataReader, mapper);
ur.Read();
}
Is that correctly using SOLID principles and generally abstraction? Could i throw the IMapper dirrectly into the DataReader and make the mapping directly in the linq query instead of first returning the data result and then use mapping or would it break the SOLID principles?
IMapper
directly to theDataReader
because you then give theDataReader
2 functions (breaking S), reading data and mapping it to the DTO. yourDataReader
is used to read the data. DTOs are created just before sending it over the wire. If yourGetUsers()
is within a controller you should injectIDataContext
,IUserDataReader
,IUserMapper
using DI via the controller constructor. \$\endgroup\$UserGridDataReader : DataReader<IUserEntity>, IUserGridDataReader
whereIUserGridDataReader
will inherit fromIUserDataReader
whereIUserGridDataReader
has the required implementation for eg.ReadUserNamesOnly()
. (the O part) \$\endgroup\$