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
ISP - Your interfaces seem to serve a single purpose ...
IMO returning IEnumerable<T> from an API is a violation of "L", as running GetEnumerator() a second time (or foreach, or Count()) behaves differently for different kinds of IEnumerable<T>s. It may fail, run forever, or have expensive side-effects (like running a database query).
Instead commit to returning an in-memory collection, and ...
I think instead of creating an unknown amount of task using PLinq would make it simpler and have control over the number of task inflight/created
var result = model.Materials
.WithDegreeOfParallelism(8) // whatever you want or leave it out
.Select(material => _materials.Get(...
As requested, I moved my comments into an answer.
Do not add the IMapper directly to the DataReader because you then give the DataReader 2 purposes (breaking S):Reading data and mapping it to the DTO.
Your DataReader is used to read the data. DTOs are created just before sending it over the wire.
If your GetUsers() is within a controller you should inject ...
It is fine to refresh the token on every app start.
It will also prevent potential hijacked tokens to be usefull for a longer time, when the token is refreshed everytime the user starts the app and the old token becomes invalid.
There can be two improvements in above code block initially as below:
Method should be async, this will help your application in terms of performance for parallel requests
In order to delete the large file, one should always prefer to download the file in chunks, again we are talking about parallel programming.