REPR Pattern - For C# developers

September 2 2024

 

Many thanks to the sponsors who make it possible for this newsletter to be free for readers.

 

• Thanks to the VS Code extension by Postman, you can now test your API directly within your code editor.

Explore it here.  
 

 
 

Background

 
 

Controllers in .NET projects have never been the best solution for exposing the endpoints of your API.

 

Why?

 

Controllers become bloated very quickly.

 

The reason for this is that you end up with many controllers that have disparate methods that are not cohesive.

 

Today I will explain to you a great solution to this, in the form of the REPR design pattern.

 

We will go through:

 

1. What is the REPR design pattern
2. Replacement of the controller with REPR pattern
3. REPR pattern with FastEndpoints
4. Cons
5. Where to use it?
6. Conclusion

 

But did you know that there is something better that is also easier to implement?

 

Have you heard of Refit?

 

Let's see what it's all about.

 
 

What is the REPR design pattern?

 
 

The Request, Endpoint, Response (REPR) pattern is a modern architectural approach often used in web development to design robust, scalable, and maintainable APIs.

 

This pattern emphasizes the clear separation of concerns between handling requests, defining endpoints, and structuring responses.

 

In .NET 8, implementing the REPR pattern allows developers to create cleaner codebases, enhance API performance, and improve user experience.

 

What is the REPR Pattern in practice?

 

The REPR pattern breaks down API interaction into three distinct parts:

 

1. Request: The client's input data or action that initiates a process.

 

2. Endpoint: The server-side function or method that processes the request.

 

3. Response: The output or result returned to the client after processing the request.

 

By structuring APIs this way, each component is specialized and can be modified independently, making the system easier to maintain and evolve.

 

 
 

Replacement of the controller with REPR pattern

 
 

Let's see a simple example with a User controller that has one method/endpoint to create a new user.

 

If you were to use controllers, it would look like this:

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly IUserService _userService;

    public UsersController(IUserService userService)
    {
        _userService = userService;
    }

    // POST api/users
    [HttpPost]
    public async Task<ActionResult<UserDto>> CreateUser([FromBody] CreateUserDto createUserDto)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        var createdUser = await _userService.CreateUserAsync(createUserDto);
        return CreatedAtAction(nameof(GetUserById), new { id = createdUser.Id }, createdUser);
    }
}
This controller structure is typical in many .NET applications and serves as a standard for managing RESTful API endpoints.

 

However, as I pointed out earlier, adopting patterns like REPR can further enhance the organization, scalability, and maintainability of your application.

 

Let’s explore how to implement the REPR pattern in a .NET 8 application step-by-step.

 

1. Define the Request

 

The request object represents the data sent from the client to the server. It typically contains all the necessary information for the server to process the request, such as parameters, headers, and body content.

public class CreateUserRequest
{
    public string Username { get; set; };
    public string Email { get; set; };
    public string Password { get; set; };
}

 

Here, the CreateUserRequest class encapsulates the data required to create a new user.

 

2. Create the Endpoint

 

The endpoint is a server-side method or function that handles the incoming request. It contains the logic to process the request data, interact with services or databases, and prepare the response.

[Produces(MediaTypeNames.Application.Json)]
[Consumes(MediaTypeNames.Application.Json)]
[Route("api/[controller]")]
[ApiController]
public class CreateUserController : ControllerBase
{
    [HttpPost(Name = "CreateUser")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public async Task<IActionResult> CreateCustomerAsync(CreateUserRequest createUserRequest)
    {
        var result = await _userService.CreateUserAsync(request);
        return result.Success ? Ok(result.Data) : BadRequest(result.ErrorMessage);
    }
}
3. Design the Response

 

The response object defines the structure of the data sent back to the client after the request has been processed. It provides feedback on the action, such as success or failure, along with any relevant data or error messages.

public class ApiResponse<T>
{
    public bool Success { get; set; };
    public T Data { get; set; };
    public string ErrorMessage { get; set; };

    public static ApiResponse<T> SuccessResponse(T data) => new ApiResponse<T> { Success = true, Data = data };
    public static ApiResponse<T> ErrorResponse(string errorMessage) => new ApiResponse<T> { Success = false, ErrorMessage = errorMessage };
}

 

This ApiResponse class is a generic response object that can handle various data types (T) and provide standardized success and error messages.

 

4. Integrate the REPR Components

 

Combining these components ensures a seamless flow from request to response. The endpoint processes the request, and based on the logic, it generates a response using the defined response object.

[Produces(MediaTypeNames.Application.Json)]
[Consumes(MediaTypeNames.Application.Json)]
[Route("api/[controller]")]
[ApiController]
public class CreateUserController : ControllerBase
{
    [HttpPost(Name = "CreateUser")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public async Task<IActionResult> CreateCustomerAsync(CreateUserRequest createUserRequest)
    {
        var result = await _userService.CreateUserAsync(request);
        if (result.Success)
        {
            return Ok(ApiResponse<User>.SuccessResponse(result.Data));
        }
        else
        {
            return BadRequest(ApiResponse<string>.ErrorResponse(result.ErrorMessage));
        }
    }
}

 

This complete endpoint demonstrates how the REPR pattern is fully implemented:

 

- It receives a CreateUserRequest.
- Validates the request data.
- Uses a service to process the creation.
- Returns a standardized response using the ApiResponse class.

 
 

REPR pattern with FastEndpoints

 
 

FastEndpoints is a library for .NET that facilitates the use of the REPR pattern by providing a framework to define APIs in a more streamlined way compared to traditional ASP.NET Core controllers.

 

It promotes a minimalistic approach to defining endpoints and handling requests and responses.

 

It fits perfectly for the implementation of the REPR pattern, and we will see that now:

using FastEndpoints;

public class CreateUserEndpoint : Endpoint<CreateUserRequest, CreateUserResponse>
{
    public override void Configure()
    {
        Post("/api/users/create");
        AllowAnonymous();
    }

    public override async Task HandleAsync(CreateUserRequest req, CancellationToken ct)
    {
        // Example: Validate input data
        if (string.IsNullOrEmpty(req.Username) || string.IsNullOrEmpty(req.Email) || string.IsNullOrEmpty(req.Password))
        {
            await SendAsync(new CreateUserResponse
            {
                Success = false,
                Message = "Invalid input data. Please provide all required fields."
            });
            return;
        }

        // Example: Simulate user creation logic (e.g., save to database and hash password)
        // Here, we're simulating a successful user creation with a hardcoded user ID
        int newUserId = 123; // Replace this with actual logic to save the user and retrieve the ID

        // Example: Assume user creation was successful
        await SendAsync(new CreateUserResponse
        {
            Success = true,
            Message = "User created successfully.",
            UserId = newUserId
        });
    }
}

 

Explanation:

 

Endpoint Configuration:

 

- Routes("/api/users/create"): Defines the endpoint's route.
- AllowAnonymous(): Allows the endpoint to be accessed without authentication, suitable for user registration or creation scenarios.

 

Request Handling:

 

The HandleAsync method processes the incoming CreateUserRequest.
It includes basic validation to ensure that all required fields are provided. If validation fails, it returns a response indicating the error.

 

Benefits of Using FastEndpoints with the REPR Pattern

 

1. Reduced Boilerplate: FastEndpoints minimizes the amount of boilerplate code compared to traditional controllers, focusing more on the core business logic.

 

2. Clear Separation of Concerns: By following the REPR pattern, each part of the process (request handling, endpoint logic, response generation) is distinct, making the code more maintainable and easier to understand.

 

3. Scalability: This modular approach makes it easier to scale your application. New endpoints can be added without affecting existing ones, and changes to business logic are isolated to specific endpoints.

 

4. Testability: With a clear separation of concerns, each component of the REPR pattern (Request, Endpoint, Response) can be individually tested, ensuring a more reliable and maintainable codebase.

 
 

Cons of using REPR Pattern

 
 

Nothing in this world is perfect, and neither is REPR.
I personally encountered 2 problems, for which of course there is a solution:
1. Swagger problem

Every Endpoint, and consequently each Controller, will be displayed individually in the Swagger documentation. Thankfully, there's a way to manage this. By utilizing Tags in the SwaggerOperation attribute, we can organize them into groups. Below is a code snippet demonstrating how to do this:


[SwaggerOperation(
    Tags = new[] { "UserEndpoints" }
)]

 

This will group all the endpoints with same tag together in Swagger document.

 

2. Developers can add aditional ActionMethod

 

Solution: Write Architecture tests.

 
 

Where to use it?

 
 

The REPR pattern is commonly applied in scenarios like CQRS, where distinct endpoints are designated for commands and queries, ensuring clear separation of responsibilities.

 

Another example is the vertical slice architecture, where the application is organized into distinct segments or slices, each tailored to specific functionality and use cases, promoting modularity and focus within the codebase.

 
 

Conclusion

 
 

The Request, Endpoint, Response (REPR) pattern is a powerful approach for building APIs that emphasizes modularity, maintainability, and clarity.

 

By separating each part of the request-handling process into distinct components: request, endpoint, and response - the REPR pattern makes it easier to develop, test, and maintain complex applications.

 

It's easy to replace controllers with the REPR pattern.

 

From my experience, the advice is to use FastEndpoints considering the performance it offers compared to all other solutions.

 

The problems you may encounter listed above can be easily solved.

 

Use pattern in Vertical Slice Architecture but also in all other architectures, if you use CQRS for example.

 

That's all from me for today. Make a coffee and try REPR.

Join 13,250+ subscribers to improve your .NET Knowledge.

There are 3 ways I can help you:

Design Patterns Simplified ebook

Go-to resource for understanding the core concepts of design patterns without the overwhelming complexity. In this concise and affordable ebook, I've distilled the essence of design patterns into an easy-to-digest format. It is a Beginner level. Check out it here.


Sponsorship

Promote yourself to 13,250+ subscribers by sponsoring this newsletter.


Join TheCodeMan.net Newsletter

Every Monday morning, I share 1 actionable tip on C#, .NET & Arcitecture topic, that you can use right away.


Subscribe to
TheCodeMan.net

Subscribe to the TheCodeMan.net and be among the 13,250+ subscribers gaining practical tips and resources to enhance your .NET expertise.