September 2 2024
Sponsored
⢠Thanks to the VS Code extension by Postman, you can now test your API directly within your code editor.
Explore it here.
Many thanks to the sponsors who make it possible for this newsletter to be free for readers. Become a sponsor.
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.
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.
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.
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.
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); } }
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
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:
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.
Nothing in this world is perfect, and neither is REPR.
I personally encountered 2 problems, for which of course there is a solution:
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.
Solution: Write Architecture tests.
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.
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.
1. Design Patterns that Deliver
This isnāt just another design patterns book. Dive into real-world examples and practical solutions to real problems in real applications.Check out it here.
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.
Every Monday morning, I share 1 actionable tip on C#, .NET & Arcitecture topic, that you can use right away.
Promote yourself to 20,000+ subscribers by sponsoring this newsletter.
Join 20,000+ subscribers to improve your .NET Knowledge.
Subscribe to the TheCodeMan.net and be among the 20,000+ subscribers gaining practical tips and resources to enhance your .NET expertise.