ASP.NET Core 8 API: How to implement REPR Pattern Endpoints in ASP.NET Core 8 API

ASP.NET Core, is one of the best technologies for building APIs. In the modern world customer focused applications, the design and implementation of APIs is mandatory for fast data communications. The data can be in JSON format or even may be in the form of Binary Stream. In ASP.NET Core 6 onwards we have been introduced with Minimal APIs. The Minimal API eliminates the need for creating controllers. Yes, use of the controller approach is based on the typical MVC design approach. The MVC Controllers involves Views, ViewModels, etc. But in APIs, we use DTOs. Hence, the controller's approach does not really have any advantages because it has tendency to become bloated with numerous dependencies as we increase the number of endpoints, this also make the maintenance as a challenge. The Minimal APIs are the best. The Minimal API approach offers an endpoint that is directly mapped with the HTTP request type, and it provides a highly cohesive approach of dependency management along with request and response objects.      

Why can we consider using REPR Pattern?

In ASP.NET Core 6 onwards, the Minimal APIs are proven as the game changer by offering the simplicity. But a point to be noted here is that we need to plan on how many endpoints should we define in the API? Since the API is the face of the application, it should be stable and handle incoming requests from multiple client applications. To improve the stability and maintainability, we must make sure that an endpoint should remain unchanged and should have capability of handling all types of HTTP requests (GET, POST, PUT, DELETE, etc.) for any type of objects that is either requested from API or posted in the HTTP request body. This is where we need to think of implementing Request-Endpoint-Response (REPR) pattern. The REPR pattern outlines API endpoints using Request, Endpoint, and a Response core element. This is highly suitable for Minimal APIs and also with the Command-Query-Responsibility-Segregation (CQRS) pattern. The Figure 1 will provide an idea of REPR pattern implementation.



Figure 1: REPR Implementation

The Implementation

Let's start an implementation of the REPR using ASP.NET Core 8 Minimal APIs. We will be using Entity Framework Core (EF Core), Mediator, Fluent Validator, and Mapper.

We will be using SQL Server database for performing Database Operations. Use the scripts shown in Listing 1 to create database and tables

Create Database EShoppingDB

GO

USE [EShoppingDB]

GO

CREATE TABLE [dbo].[Categories](

[CategoryUniqueId] [int] IDENTITY(1,1) NOT NULL,

[CategoryId] [nvarchar](max) NOT NULL,

[CategoryName] [nvarchar](max) NOT NULL,

[BasePrice] [int] NOT NULL,

 CONSTRAINT [PK_Categories] PRIMARY KEY CLUSTERED (

[CategoryUniqueId] ASC

)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, 

IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, 

OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]

) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

 

CREATE TABLE [dbo].[Products](

[ProductUniqueId] [int] IDENTITY(1,1) NOT NULL,

[ProductId] [nvarchar](max) NULL,

[ProductName] [nvarchar](max) NOT NULL,

[Manufacturer] [nvarchar](max) NOT NULL,

[Price] [int] NOT NULL,

[CategoryUniqueId] [int] NOT NULL,

CONSTRAINT [PK_Products] PRIMARY KEY CLUSTERED (

[ProductUniqueId] ASC

)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, 

ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF)

 ON [PRIMARY]) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

GO


ALTER TABLE [dbo].[Products] WITH CHECK ADD CONSTRAINT 

[FK_Products_Categories_CategoryUniqueId] 

FOREIGN KEY([CategoryUniqueId])

REFERENCES [dbo].[Categories] ([CategoryUniqueId])


ON DELETE CASCADE

GO

ALTER TABLE [dbo].[Products] CHECK CONSTRAINT 

[FK_Products_Categories_CategoryUniqueId]

GO

Listing 1: Database and Tables

Step 1: Open Visual Studio 2022 and create a new ASP.NET Core API project, name this project as REPR_API. In this project add packages to use Entity Framework Core, Mapper, Mediator, Fluent Validator, etc. as shown in Figure 2.



Figure 2: Required Packages for the Application

Step 2: To generate data access layer using Entity Framework Core, we will use the database first approach as shown in the following command

dotnet ef dbcontext scaffold "Data Source=.\SqlExpress;Initial Catalog=EShoppingDB;Integrated Security=SSPI;TrustServerCertificate=True" Microsoft.EntityFrameworkCore.SqlServer -o Models

This command will generate the DbContext class and Model classes named Category and Product in the Models folder.  In the Models folder add a new class file named EntityBase.cs. In this class file we will add code for the EntityBase class. This will be an abstract class which we will be using as a base class for the Category and Product classes so that we can use them as a common object while using the reflection to read the actual model object for performing Read/Write operations. The code for the EntityBase class is shown in Listing 2


using System.Text.Json.Serialization;

namespace REPR_API.Models
{
    public  class EntityBase
    {
        [JsonConstructor]
        public EntityBase()
        {
            
        }
    }
}

Listing 2: The EntityBase class

Modify the code of the Product and Category class and set the EntityBase class as base class for Category and Product class as shown in Listing 3


public partial class Category : EntityBase
{
   /* Some code is removed */
}

public partial class Product : EntityBase
{
    /* Some code is removed */
}

Listing 3: The Category and Product class modified with the EntityBase as the base class

In the Models folder, add a new class file named ResponseObject.cs. In this class file, add the code for the ResponeObject generic class. We need this class to send a common standard schema in the response from the API. The code for the ResponseObject class is shown in Listing 4


 public class ResponseObject<T> where T: EntityBase
 {
     public int StatusCode { get; set; }
     public string? Message { get; set; }
     public IEnumerable<T> Records { get; set; } = Enumerable.Empty<T>();
     public T? Record { get; set; }    
 }

Listing 4: The ResponseObject class

The ResponseObject class is a generic class. The generic parameter will be Model class which we have generated in the Step 2.  In this class, we have property like Records and Record, these properties represent collection and single object in the response object.
  

Creating Data Access Service for Performing CRUD Operations

Step 3: In the project add a new folder named Services. In this folder, add a new interface file named IDataService.cs. In this file lets add an interface that will declare methods to perform Read/Write operations as shown in Listing 5.


 public interface IDataService<TEntity, in TPk> where TEntity : EntityBase
 {
     Task<ResponseObject<TEntity>> GetAsync();
     Task<ResponseObject<TEntity>> GetByIdAsync(TPk id);
     Task<ResponseObject<TEntity>> CreateAsync(TEntity entity);
     Task<ResponseObject<TEntity>> UpdateAsync(TPk id, TEntity entity);
     Task<ResponseObject<TEntity>> DeleteAsync(TPk id);
 }

Listing 5: The Service Interface

Let's implement this interface in the CategoryService and ProductService classes for CRUD operations. The code in Listing 6 shows code for CategoryService class, you can implement the similar code for ProductService. The CategoryService and ProductService classes are constructor injected by the EShoppingDbContext class. The CRUD Operations are performed using this class.


using Microsoft.EntityFrameworkCore;
using REPR_API.Models;

namespace REPR_API.Services
{
    public class CategoryDataService : IDataService<Category, int>
    {
        EshoppingDbContext ctx;
        ResponseObject<Category> response;
        public CategoryDataService(EshoppingDbContext ctx)
        {
            this.ctx = ctx;
            response = new ResponseObject<Category>();
        }
        async Task<ResponseObject<Category>> IDataService<Category, int>.CreateAsync(Category entity)
        {
            try
            {
                var result = await ctx.Categories.AddAsync(entity);
                await ctx.SaveChangesAsync();
                response.Record = result.Entity;
                response.StatusCode = 200;
                response.Message = "New Record is created suiccessfully";
                return response;
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        async Task<ResponseObject<Category>> IDataService<Category, int>.DeleteAsync(int id)
        {
            try
            {
                var entity = await ctx.Categories.FindAsync(id);
                if (entity == null)
                    throw new Exception($"Record by Id= {id} is not found");

                ctx.Categories.Remove(entity);
                await ctx.SaveChangesAsync();
                response.Record = entity;
                response.StatusCode = 200;
                response.Message = "Record deleted successfuly";
                return response;
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        async Task<ResponseObject<Category>> IDataService<Category, int>.GetAsync()
        {
            try
            {
                var result = await ctx.Categories.ToListAsync();
                response.Records = result;
                response.StatusCode = 200;
                response.Message = "Records read successfuly";
                return response;
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        async Task<ResponseObject<Category>> IDataService<Category, int>.GetByIdAsync(int id)
        {
            try
            {
                var entity = await ctx.Categories.FindAsync(id);
                if (entity == null)
                    throw new Exception($"Record by Id= {id} is not found");
                response.Record = entity;
                response.StatusCode = 200;
                response.Message = "Record found successfuly";
                return response;
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        async Task<ResponseObject<Category>> IDataService<Category, int>.UpdateAsync(int id, Category entity)
        {
            try
            {
                ctx.Entry<Category>(entity).State = EntityState.Modified;
                await ctx.SaveChangesAsync();
                response.Record = entity;
                response.StatusCode = 200;
                response.Message = "Record updates successfuly";
                return response;
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }
    }
}

Listing 6: The CatgoryService class

The Validator code for dynamically evaluating Fluent Model Validations  

Step 4: In the project, add a new folder named Validators. In this folder, add a class file named CategoryValidator.cs to define validation rules on the Category class. Similarly, we will also add class file named ProductValidator.cs to add rules for Product model class. We will use the Fluent Validator to define validators. The code for these two classes is shown in Listing 7.


public class CategoryValidator: AbstractValidator<Category>
{
    public CategoryValidator() 
    {
        RuleFor(p => p.CategoryId).NotEmpty();
        RuleFor(p => p.CategoryName).NotEmpty();
        RuleFor(p => p.BasePrice).GreaterThan(0);
    }
}

 public class ProductValiator : AbstractValidator<Product>
 {
     public ProductValiator()
     {
         RuleFor(p => p.ProductId).NotEmpty();
         RuleFor(p => p.ProductName).NotEmpty();
         RuleFor(p=> p.Manufacturer).NotEmpty();
         RuleFor(p=>p.Price).GreaterThan(0);
         RuleFor(p=>p.CategoryUniqueId).NotNull();
         RuleFor(p => p.CategoryUniqueId).GreaterThan(0);
     }
 }

Listing 7: The CategoryValidator and ProductValidator classes

We need this validation to make sure that the data received for write operation will be validated.  Now the most important step here is to make sure that the validations are executed while performing write operations. But since we are implementing REPR pattern for loading Models dynamically, we will be validating the model class dynamically using reflection, so we need to write a method for validation method by invoking the Validate() method of the Fluent Validator using reflection as shown in the Listing 8


namespace REPR_API.Validators
{
    public static class ModelValidator
    {
        /// <summary>
        /// Method to validate the Model 
        /// </summary>
        /// <param name="validatorTypeName"></param>
        /// <param name="entityInstance"></param>
        /// <returns></returns>
        public static FluentValidation.Results.ValidationResult IsValid(string validatorTypeName, object entityInstance)
        { 
            
            // Vaidator Type Instance
            var validatorType = Type.GetType(validatorTypeName);
            var validatorInstance = Activator.CreateInstance(validatorType);

            // Get methods from the type

            Type validType = validatorInstance.GetType();
            // Read only Validate Methods
            var validateMethods = validType.GetMethods().Where(m => m.Name.StartsWith("Validate")).ToArray();

            string name = validateMethods[0].Name;

            ////Valiate the Entity, Get the method that accepts the ENtity Object as Input Parameter
            var validateMethod = validatorInstance.GetType().GetMethod(validateMethods[0].Name, new Type[] { entityInstance.GetType() });

            var validationResult = (FluentValidation.Results.ValidationResult)validateMethod.Invoke(validatorInstance, new[] { entityInstance });

             
            return validationResult;
        }

    }
}

Listing 8: Dynamically invoking the Validate() method for performing validations

Carefully read and understand the code in Listing 8, we have implemented the IsValid() method that accepts string and object type parameters.  The string parameter will be the name of the class using which an instance will be created. This class will be the validator class created in Listing 7. Since the CategoryValidator and ProductValidator classes are derived from the AbstractValidator class they have access to the Validate() method. The code in Listing 8, reads the Validate() method. Since there are more than one Validate() methods present in the class the 0th index Validate() method is invokes using the reflection and entityinstance object, the actual model object, is passed to it so that the validator is loaded, and validation rules are evaluated. This is one time effort that we need to take to make sure that the hardcoding of the model class validator can be avoided.     

The Mediator with Query and Command

In this step, we will implement the CQRS pattern so that we will isolate the data access services from endpoints. you can visit this link to understand the CQRS pattern implementation in ASP.NET Core.      

Step 5: In the project, add a new folder named Queries. In this folder add a sub-folders named CategoryQuery and ProductQuery. In CategoryQuery folder, add a new class file named GetCategoriesQuery.cs. In this file, we will add code for MediatR request to manage the request to read categories. The Listing 9 shows the code for GetCategoriesQuery.


 public class GetCategoriesQuery:IRequest<ResponseObject<Category>>
 {
 }

Listing 9: GetCatgoriesQuery class 

Similarly, the queries for GetCategoryByIdQuery, GetProductsQuery, and GetProductById classes can be created. To understand the Mediator pattern in ASP.NET Core, you can visit this link. You can also download the code for this article to see the code.

In the project, add folder named Commands. In this folder, add two new subfolders named CategoryCommand and ProductCommand. In the CategoryCommand folder, add a new class file named CreateCategoryCommands.cs. In this class file, add the code for CreateCategoryCommand class as shown in Listing 10.

 public class CreateCategoryCommand: IRequest<ResponseObject<Category>>
 {
     public Category? Category { get; set; }

     public CreateCategoryCommand(Category cat)
     {
         Category = cat;   
     }
 }


Listing 10: The CreateCategoryCommand  

The code in Listing 10 shows the command to process the request for the received Model object. Similarly, we can create classes for UpdateCategoryCommand, DeleteCategoryCommand, CreateProductCommand, UpdateProductCommand, and DeleteProductCommand.  

Let's implement Handlers for handling Query and Command Requests created in Listing 9 and 10. In the project, add a new folder named Handlers. In this folder, add a subfolder named CategoryHandlers. In this folder, add a new class file GetCategoriesHandler.cs. In this class file, add the code as shown in Listing 11


using MediatR;
using REPR_API.Models;
using REPR_API.Queries.CategoryQuery;
using REPR_API.Services;

namespace REPR_API.Handlers.CategoryHandlers
{
    public class GetCategoriesHandler : IRequestHandler<GetCategoriesQuery, ResponseObject<Category>>
    {

        IDataService<Category, int> service;

        private readonly IServiceScopeFactory _serviceScopeFactory;

        public GetCategoriesHandler(IServiceScopeFactory serviceScopeFactory)
        {
            _serviceScopeFactory = serviceScopeFactory;

        }
        public async Task<ResponseObject<Category>> Handle(GetCategoriesQuery request, CancellationToken cancellationToken)
        {
            using (var scope = _serviceScopeFactory.CreateScope())
            {
               var catServ =  scope.ServiceProvider.GetService<IDataService<Category,int>>();
                return await catServ.GetAsync();
            }
            
        }
    }
}

Listing 11: The GetCategriesHandler

The GetCategoriesHandler class is constructor injected using the IServiceScropeFactory interface. This will be used to read the service instance registered in the dependency container of the ASP.NET Core. This interface has the CreateScope() method using which we can access the ServiceProvider object property that has the GetService() method to extract the exact match instance type from the dependency container. In the current code listing we will be extracting the instance type that implements IDataService interface that performs CRUD Operations based in the Model Type parameter passed to it. Currently, we are using the IDataService type for Category model class. Similarly, we can add code for handlers to perform Create, Update, and Delete operations for Category and Product Models. You can download the code for it from the GitHub link provided at the end of the article.

Registering Mediator Handlers in the ASP.NET Core Dependency Container 

This is one of the important steps that we need to follow while using Mediator pattern in ASP.NET Core. In this step we will register all Mediator objects like Commands, Queries, and Handlers in the ASP.NET Core Dependency container. 

Step 6: In the Handlers folder, add a new class file named Dependencies.cs. In this class file we will add code for Dependencies class. In this class we will add a code for RegisterRequestHandlers() method, this will be an extension method to IServiceCollection using which we will register Mediator Objects in Dependency Container. The code for the class is shown in Listing 12


public static class Dependencies
{
    public static IServiceCollection RegisterRequestHandlers(
        this IServiceCollection services)
    {
        return services
            .AddMediatR(cfg => 
            {
                cfg.RegisterServicesFromAssembly(typeof(Dependencies).Assembly);
            });
    }
}

Listing 12: The Dependency class

In the code in Listing 12, the RegisterServicesFromAssembly() Method is used to register various Handlers from the current assembly in Dependency Container.   

The Request Endpoint Response (REPR) Implementation

In this section we will implement the REPR Pattern in ASP.NET Core 8 using Minimal APIs. Before implementing the actual endpoints, we need to implement add code for reading HTTP Request body so that the HTTP POST and PUT request data will be extracted and mapped with the Model object. Let's do it.

Step 7: In the project, add a new folder named REPRInfra. In this folder add three sub-folders named, RequestExtensions, EndpointMapper, EndpointExtensions, and Endpoints. In the RequestExtensions folder, add a new class file named HttpRequestExtension.cs. In this file, we will add code for HttpRequestExtension class that will contain an extension method named ReadAsStringAsync() for the Stream class. This method will read the stream contents using StreamReader as a String. This method is needed to transform the Stream data into string that we further convert into JSON. Listing 13 shows the code for this class.


 public static class HttpRequestExtension
 {
     public static async Task<string> ReadAsStringAsync(this Stream requestBody)
     {
         using StreamReader reader = new(requestBody);
         var bodyAsString = await reader.ReadToEndAsync();
         return bodyAsString;
     }
 }

Listing 13: HttpRequestExtension class

Step 8: In the EndpointMapper folder, add a new interface file named IEndpointMapper.cs. In this method interface we will define a method named MapAPIEndpoint(). This method will accept the IEndpointRouteBuilder interface as a parameter. This interface parameter represents a contract for route builder in the ASP.NET Core application. This specifies a route for the application so that incoming requests are mapped with the route. The code for IEndpointMapper is shown in Listing 14


public interface IEndPointMapper
{
    void MapAPIEndpoint(IEndpointRouteBuilder app);
}

Listing 14: The IEndpointMapper interface

Step 9: This is the heart step for the REPR Pattern. Since we will be using common endpoints for all route mapper, we need to implement a logic where we need to extract all services registered in the current application's assembly and assigned them to IEndpointMapper that we have declared in step 8. Since all Endpoint type classes (which we will be adding in next steps), these service objects will be assigned and available to them so that when the HTTP request is received and mapped these services are available while processing requests, this logic is written in an extension method for IServiceCollection in the class ApiEndpointsExtension shown in Listing 15.  In this class, we also need to add an extension method WebApplication class. This method is named as MapAPIEndpoints. This method also accepts the RouteGroupBuilder as an input parameter. This class represents a builder for defining group of endpoints with a common prefix. The MapAPIEndpoints() method is used to map the request and execute the request for endpoint in HTTP pipeline. The code is shown in Listing 15.


using Microsoft.Extensions.DependencyInjection.Extensions;
using REPR_API.REPRInfra.EndpointMapper;
using System.Reflection;

namespace REPR_API.REPRInfra.EndpointExtensions
{
    public static class ApiEndpointsExtension
    {
        public static IServiceCollection AddAPIEndpoints(this IServiceCollection services, Assembly assembly)
        {
            // Get All Se`rvices 
            ServiceDescriptor[] serviceDescriptors = assembly
                .DefinedTypes
                .Where(type => type is { IsAbstract: false, IsInterface: false } &&
                               type.IsAssignableTo(typeof(IEndPointMapper)))
                .Select(type => ServiceDescriptor.Transient(typeof(IEndPointMapper), type))
                .ToArray();

            services.TryAddEnumerable(serviceDescriptors);

            return services;
        }
        /// <summary>
        /// Map All Endpoints to Route in the HTTP Pipeline to Execute when the 
        /// HTTP Request is received 
        /// RouteGroupBuilder: This is used to define a Group of Endpoints with a common prefix
        /// </summary>
        /// <param name="app"></param>
        /// <param name="routeGroupBuilder"></param>
        /// <returns></returns>
        public static IApplicationBuilder MapAPIEndpoints(this WebApplication app, RouteGroupBuilder? routeGroupBuilder = null)
        {
            IEnumerable<IEndPointMapper> endpoints = app.Services.GetRequiredService<IEnumerable<IEndPointMapper>>();
           
            IEndpointRouteBuilder builder = routeGroupBuilder is null ? app : routeGroupBuilder;

            foreach (IEndPointMapper endpoint in endpoints)
            {
                // Map Endpoints
                endpoint.MapAPIEndpoint(builder);
            }

            return app;
        }
    }
}

Listing 15: The ApiEndpointsExtension  class                      

Step 10: Let's modify the appsettings.json by adding keys for API definitions with Modes, Validator, Get, Post, Put, Delete actions constants as shown in Listing 16.

 "API": {
   "Category": {
     "Model": "REPR_API.Models.Category,REPR_API",
     "Validator": "REPR_API.Validators.CategoryValidator,REPR_API",
     "Get": "REPR_API.Queries.CategoryQuery.GetCategoriesQuery,REPR_API",
     "GetById": "REPR_API.Queries.CategoryQuery.GetCategoryByIdQuery,REPR_API",
     "Post": "REPR_API.Commands.CategoryCommand.CreateCategoryCommand,REPR_API",
     "Put": "REPR_API.Commands.CategoryCommand.UpdateCategoryCommand,REPR_API",
     "Delete": "REPR_API.Commands.CategoryCommand.DeleteCategoryCommand,REPR_API"
   },
   "Product": {
     "Model": "REPR_API.Models.Product,REPR_API",
     "Validator": "REPR_API.Validators.ProductValiator,REPR_API",
     "Get": "REPR_API.Queries.ProductQuery.GetProductsQuery,REPR_API",
     "GetById": "REPR_API.Queries.ProductQuery.GetProductById,REPR_API",
     "Post": "REPR_API.Commands.ProductCommand.CreateProductCommand,REPR_API",
     "Put": "REPR_API.Commands.ProductCommand.UpdateProductCommand,REPR_API",
     "Delete": "REPR_API.Commands.ProductCommand.DeleteProductCommand,REPR_API"
   }
 }


Listing 16: The appsettings.json API definitions

Why do we need this? The reason is since we are creating endpoints those will be used for various models we will be providing model name, validator, commands, and queries to these endpoints so that using the reflection required objects will be loaded while processing the HTTP requests.

Creating Endpoints

Step 11: In the Endpoints folder, add a class file named Get.cs. In this class file we will add code for Get class. This class will implement IEndPointMapper interface and will implement its MapAPIEndpoint() method. The Get class will be constructor injected with IMediator and IConfioguration interfaces. In the MapAPIEndpoint() method the minimal endpoint for HTTP Get request will be implemented. This endpoint code will read API Key from the appsettings.json file as created in Listing 16.  From the configuration the value for Get key will be read. This value is the name of the Query class e.g. if the Model passed to Get endpoint is Category then the value for the Get key from the configuration is GetCategoriesQuery class (please refer Listing 16). The code in the Get endpoint will create an instance of GetCategoriesQuery class using Activator. Once the instance is created then using the mediator the call is made to query the data. This is how the general code is implemented for GET endpoint which will be used for any model provided that details for Command, Query, etc. are added in appsettings.json file. The code for the Get class is shown in Listing 17.

using MediatR;
using REPR_API.Models;
using REPR_API.Queries.CategoryQuery;
using REPR_API.Queries.ProductQuery;
using REPR_API.REPRInfra.EndpointMapper;
using REPR_API.Services;


namespace REPR_API.Endpoints;

public class Get : IEndPointMapper
{
    IMediator mediator;
    IConfiguration configuration;
    
    public Get(IMediator mediator,IConfiguration configuration)
    {
        this.mediator = mediator;
        this.configuration = configuration;
    }
    public  void MapAPIEndpoint(IEndpointRouteBuilder app)
    {
        app.MapGet("get/{model}",async(HttpContext ctx, string model) => {

            // Read the Get request Command Value from appsettings.json
            var typeName = configuration[$"API:{model}:Get"];
            // Read the Command Type Name
            var type = Type.GetType(typeName);
            
            object? queryInstance = Activator.CreateInstance(type);
            if (queryInstance != null)
            {
                var resultResponse = await mediator.Send(queryInstance);
                return Results.Ok(resultResponse);
            }
            return null;
           
        });
    }
}


Listing 17: The Get class 

Similarly, in Endpoints folder, add a new class file named Post.cs. In this file we will add code for Post class. Like the Get class, this class implements the IEndpointMapper intarfe and its method. This class is also constructor injected with IMediator and IConfiguration interfaces. The code for this class is shown in Listing 18.


using MediatR;
using REPR_API.Commands.CategoryCommand;
using REPR_API.Commands.ProductCommand;
using REPR_API.Models;
using REPR_API.REPRInfra.EndpointMapper;
using REPR_API.REPRInfra.RequestExtensions;
using REPR_API.Validators;
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using System.Text.Json;
 

namespace REPR_API.Endpoints;

public class Post : IEndPointMapper
{
    IMediator mediator;
    IConfiguration configuration;
    public Post(IMediator mediator, IConfiguration configuration)
    {
        this.mediator = mediator;
        this.configuration = configuration;

    }

    public void MapAPIEndpoint(IEndpointRouteBuilder app)
    {
        app.MapPost("post/{model}", async (HttpContext ctx, string model, EntityBase data) =>
        {
            if (String.IsNullOrEmpty(model))
               return Results.BadRequest("Model Value missing");

            ctx.Request.EnableBuffering();
            ctx.Request.Body.Position = 0;
            string requestBody = string.Empty;
            
            requestBody = await ctx.Request.Body.ReadAsStringAsync();
            if (requestBody != null || requestBody?.Length != 0)
            {
                try
                {
                    // Read the Post request Command Value from appsettings.json
                    var typeName = configuration[$"API:{model}:Post"];
                    // Read the Command Type Name
                    var type = Type.GetType(typeName);
                    // Read the Model ENtity Name
                    var entityTypeName = configuration[$"API:{model}:Model"];
                    // Read the Model Entity Type
                    var entityType = Type.GetType(entityTypeName);


                    // Store data from Body into the Entity Object and create its instance
                    object? entityInstance = JsonSerializer.Deserialize(requestBody, entityType);
                    // Create an Instance of Command
                    object? commandInstance = Activator.CreateInstance(type, entityInstance);

                    // Read the Validator class from the appsettings file 
                    var validatorTypeName = configuration[$"API:{model}:Validator"];
                    var validationResult = ModelValidator.IsValid(validatorTypeName, entityInstance);

                    if (validationResult.IsValid)
                    {
                        var resultResponse = await mediator.Send(commandInstance);
                        return Results.Ok(resultResponse);
                    }
                    else
                    {
                        return Results.BadRequest(validationResult.Errors.Select(e=>e.ErrorMessage).ToList());
                    }
                }
                catch (Exception ex)
                {

                    throw ex;
                }

            }
            return null;
        });
    }


}

Listing 18: The Post class 

As shown in the Post class, the MapAPIEndpoint() method contains code for HTTP Post request. In the POST request reads the models class from the HTTP request body and further it reads the suitable class name from the configuration file and then creates an instance of the Command class to further the HTTP POST request. This code also performs the validation by reading the validator class name from the configuration. Similarly Listing 19 shows the code for Put and Delete endpoint. We add this code by adding Put.cs and Delete.cs class files in Endpoints folder.


using MediatR;
using REPR_API.Models;
using REPR_API.REPRInfra.EndpointMapper;
using REPR_API.REPRInfra.RequestExtensions;
using REPR_API.Validators;
using System.Text.Json;

namespace REPR_API.Endpoints;

public class Put : IEndPointMapper
{

    IMediator mediator;
    IConfiguration configuration;
    public Put(IMediator mediator, IConfiguration configuration)
    {
        this.mediator = mediator;
        this.configuration = configuration;

    }
    public void MapAPIEndpoint(IEndpointRouteBuilder app)
    {
        app.MapPut("put/{model}/{id}", async (HttpContext ctx, string model,int id, EntityBase data) =>
        {
            if (String.IsNullOrEmpty(model))
                return Results.BadRequest("Model Value missing");

            ctx.Request.EnableBuffering();
            ctx.Request.Body.Position = 0;
            string requestBody = string.Empty;
             

            requestBody = await ctx.Request.Body.ReadAsStringAsync();
            if (requestBody != null || requestBody.Length != 0)
            {
                try
                {

                    // Read the Post request Command Value from appsettings.json
                    var typeName = configuration[$"API:{model}:Put"];
                    // Read the Command Type Name
                    var type = Type.GetType(typeName);
                    // Read the Model ENtity Name
                    var entityTypeName = configuration[$"API:{model}:Model"];
                    // Read the Model Entity Type
                    var entityType = Type.GetType(entityTypeName);
                    // Store data from Body into the Entity Object and create its instance
                    object? entityInstance = JsonSerializer.Deserialize(requestBody, entityType);
                    // Create an Instance of Command
                    object? commandInstance = Activator.CreateInstance(type, entityInstance);


                    // Read the Validator class from the appsettings file 
                    var validatorTypeName = configuration[$"API:{model}:Validator"];
                    var validationResult = ModelValidator.IsValid(validatorTypeName, entityInstance);

                    if (validationResult.IsValid)
                    {
                        var resultResponse = await mediator.Send(commandInstance);
                        return Results.Ok(resultResponse);
                    }
                    else
                    {
                        return Results.BadRequest(validationResult.Errors.Select(e => e.ErrorMessage).ToList());
                    }
                }
                catch (Exception ex)
                {

                    throw;
                }
                
            }
            return null;
        });
    }
}


using MediatR;
using REPR_API.REPRInfra.EndpointMapper;

namespace REPR_API.Endpoints;

public class Delete : IEndPointMapper
{

    IMediator mediator;
    IConfiguration configuration;
    public Delete(IMediator mediator, IConfiguration configuration)
    {
        this.mediator = mediator;
        this.configuration = configuration;

    }
    public void MapAPIEndpoint(IEndpointRouteBuilder app)
    {
        app.MapDelete("delete/{model}/{id}", async (string model, int id) => {
            // Read the Post request Command Value from appsettings.json
            var typeName = configuration[$"API:{model}:Delete"];
            // Read the Command Type Name
            var type = Type.GetType(typeName);
            object? commandInstance = Activator.CreateInstance(type,id);
            var resultResponse = await mediator.Send(commandInstance);
            return Results.Ok(resultResponse);
        });
    }
}

Listing 19: Put and Delete classes

Finally, we need to perform register Mediator, Services, Handles, etc. in the Dependency Container. Specially we will also be adding the endpoints using MapAPIEndpoints() method and the request buffering in the HTTP Pipeline. The code in Listing 20 shows all details.


using Microsoft.EntityFrameworkCore;
using REPR_API.Handlers.HandlerRegistrations;
using REPR_API.Models;

using REPR_API.REPRInfra.EndpointExtensions;
using REPR_API.Services;
using System.Reflection;

// For JSON Seriaization Policy
using JsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions;


var builder = WebApplication.CreateBuilder(args);
//1. Register Mapper

builder.Services.AddAutoMapper(typeof(Program));
builder.Services.AddDbContext<EshoppingDbContext>(options =>
{
    options.UseSqlServer(builder.Configuration.GetConnectionString("AppDbConnection"));
});

//2. Register all MediatR Handlers
builder.Services.RegisterRequestHandlers();
// 3. The Json Options for Serializatrion
builder.Services.Configure<JsonOptions>(options =>
{
    options.SerializerOptions.PropertyNamingPolicy = null;
});
// 4. Register Services 
builder.Services.AddTransient<IDataService<Category, int>, CategoryDataService>();
builder.Services.AddTransient<IDataService<Product, int>, ProductDataService>();

// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddAPIEndpoints(Assembly.GetExecutingAssembly());

var app = builder.Build();

//5. Map Endpoint with 'api/' as a Prefix 

app.MapAPIEndpoints(app
    .MapGroup("api/"));

// 6. To Read the HTTP Body
app.Use((context, next) =>
{
    context.Request.EnableBuffering();
    return next();
});

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.Run();

Listing 20: The Registration details 

This is how we can implement the REPR pattern in ASP.NET Core. Important point to be kept in mind that we must define the models and CQRS pattern for the implementation.


Run the application, the swagger page will be loaded as shown in Figure 3



Figure 3: The Swagger page 

Figure 3 shows the model and id parameters, this means that we can make get request by including the model class name. This will use required Query and Command classes to perform read and write operations. Make HTTP GET request using Category model and the Category data will be responded as shown in Figure 4.



Figure 4: The Category response  

Similarly, the other requests can be tested. The code for this article can be downloaded from this link.

Conclusion: The REPR Pattern is used to develop maintainable API code. This will help the API development with extensibility. If the reflection is used properly then the dependency across Domain Layers and API Layers can be implemented easily.

Popular posts from this blog

Uploading Excel File to ASP.NET Core 6 application to save data from Excel to SQL Server Database

ASP.NET Core 6: Downloading Files from the Server

ASP.NET Core 6: Using Entity Framework Core with Oracle Database with Code-First Approach