C# Am I using proper SOLID principles? 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&lt;T&gt;
{
    IEnumerable&lt;T&gt; Read();
}

public abstract class DataReader&lt;T&gt; : IDataReader&lt;T&gt;
{
    private protected IDataContext _context;
    public abstract IEnumerable&lt;T&gt; Read();


    public DataReader(IDataContext context) 
    {
        _context = context;
    }
}

public interface IUserDataReader : IDataReader&lt;IUserEntity&gt;
{
}

//First implementation of user reader
public class UserDataReader : DataReader&lt;IUserEntity&gt;, IUserDataReader
{

    public UserDataReader(IDataContext context) : base(context) { }

    public override IEnumerable&lt;IUserEntity&gt; Read()
    {
        return _context.Users.Where(x =&gt; x.IsActive).OrderBy(x =&gt; x.Name).ToList();
    }
}


//Secound implementation of user reader
public class UserGridDataReader : DataReader&lt;IUserEntity&gt;, IUserDataReader
{

    public UserGridDataReader(IDataContext context) : base(context) { }

    public override IEnumerable&lt;IUserEntity&gt; Read()
    {
        return _context.Users.OrderBy(x =&gt; x.Name).ToList();
    }
}

Now the mappers:
public interface IMapper&lt;I, O&gt;
{
    O Map(I item);
}

public interface IUserMapper : IMapper&lt;IUserEntity, UserDTO&gt;
{

}

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&lt;UserDTO&gt; Read()
    {
        IEnumerable&lt;IUserEntity&gt; userData = _reader.Read();

        IEnumerable&lt;UserDTO&gt; users = userData.Select(x =&gt; _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?
I suppose the simplest thing is to go through the principles:

SRP - Cannot see any 'too big' things in your example
OCP - The example is small enough and you are not using class
inheritance that I cannot think of how this would apply
LSP - You only have one implementation of each interface so hard to
say
ISP - Your interfaces seem to serve a single purpose that might serve
the needs of several users - so no complaints there
DIP - You have pushed the more variable 'decision' making out,
keeping the API more abstract which seems reasonable.

Much of this, of course, depends on your specific aims and requirements. some thoughts:

How likely is it that the mapper and reader will vary independently?
Are you going to get any reuse out of all the different readers and mappers (actual not future-proofing)
Will each reader have a mapper in practice, in which case your comment about combining the mapper and reader is worth more thought.
Do you really need an interface for the reader, it seems like you are just wrapping what could be a function/lambda
What is the motivation in restricting the outbound data?
I appreciate this may just be an exercise, but it's good practice to think if you need to do this kind of restriction

It can be difficult to have a good instinct for principles like SOLID on toy problems due to the lack of realistic constraints and compromises.