Sometimes we don't need to over-engineer applications by introducing a ton of layers. I recently had the chance to rewrite a small MVC application as a .NET Core REST API and followed the following approach:
The application pulls data from an existing SQL Server database and from existing Stored Procedures. So, my repo looks like this:
As you can see, I'm overriding the OnModelCreating method where I manually map the properties of my Item model to column names in SQL Server.
Ok cool, so then on top of that I have a repository whose only job it is to retrieve 1 or more of these entities:
I then have a service layer which receives the entities. Ideally, you could use AutoMapper here to do object mapping, but it just so happened my project did not require it and I don't like to over-engineer. But anyway, the job of the service layer is to retrieve the entities and prepare the objects to be sent out by the API. It looks something like this:
As you can see, the service gets the entities from the repository, performs additional validation and returns an ApiResult. Shoutout to my friend Eric for teaching me this pattern. Anyway, as you can see, if the repo returns no items, the service returns an ApiResult with ResultCode.Empty and when there are items found, it returns an ApiResult with the items list.
If you look at the ApiResult class below, you'll notice that it has 2 constructors. The first constructor takes in an ApiResultCode, so in the case above where there were no items returned by the repository, we send a ResultCode.Empty to let consumers of our API know that there were no items to be retrieved. The second constructor takes in a generic type and automatically sets the ResultCode to ResultCode.Success. In the service example above, when we got items from the repository, we returned them via this second constructor.
In case you're wondering what the API response from this API Result looks like, it would look something like this (except that the data property would be a serialized JSON of your service payload):
Even neater, you could do operations on this ApiResult on the C# side of things as well. For example, in the following example, I have created a base controller class has a generic ApiResponse() method that I can use in my other controller methods. Because, why would I repeat the same logic over and over and over?
Lastly, I encapsulate all of the endpoint parameter validation logic through the use of IValidatableObject, which I've covered on my other post.