<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Alexey</title>
    <description>The latest articles on Forem by Alexey (@alexdevfx).</description>
    <link>https://forem.com/alexdevfx</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F966356%2F1e594f81-57a6-4a0c-8012-1398b95cdfed.jpg</url>
      <title>Forem: Alexey</title>
      <link>https://forem.com/alexdevfx</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/alexdevfx"/>
    <language>en</language>
    <item>
      <title>ASP.NET Refactoring: Map Domain Results into HTTP Responses</title>
      <dc:creator>Alexey</dc:creator>
      <pubDate>Sun, 27 Jul 2025 08:43:45 +0000</pubDate>
      <link>https://forem.com/alexdevfx/aspnet-refactoring-map-domain-results-into-http-responses-3j7i</link>
      <guid>https://forem.com/alexdevfx/aspnet-refactoring-map-domain-results-into-http-responses-3j7i</guid>
      <description>&lt;p&gt;This article proposes a clean approach to make API endpoint code more maintainable by avoiding repeated failure-handling code. We’ll explore different ways to control flow and map domain results to HTTP responses using the Result pattern.&lt;/p&gt;




&lt;p&gt;In Web API development, beyond handling success scenarios, you must handle failures and return appropriate HTTP status codes. Let’s consider the following controller example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ApiController]
public class PaymentsController(IPaymentService paymentService): ControllerBase
{
 [HttpPost("payments/new")]
 public async Task&amp;lt;IActionResult&amp;gt; NewPayment(NewPaymentInput model)
 {
  CreatedPayment payment = paymentService.Create(model.Map&amp;lt;NewPaymentDto&amp;gt;());
  return Ok(payment);
 }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the &lt;em&gt;PaymentService&lt;/em&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class PaymentService: IPaymentService
{
  public CreatedPayment Create(NewPaymentDto input)
  {
      // New payment processing
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Common Anti-Pattern: Exception-Based Error Handling
&lt;/h3&gt;

&lt;p&gt;How should we handle the “unhappy path” inside PaymentService? Often, developers raise custom exceptions and catch them at the middleware level, wrapping them into appropriate API responses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public CreatedPayment Create(NewPaymentDto input)
  {
      CreatedPayment paymentResult = new();

      TransactionResult transactionResult = transactionService.Create(input.User, input.Amount);

      if(!transactionResult.Success)
      {
         throw new PaymentException("Transaction creation failure");
      }

      return paymentResult;
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not a good idea because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Performance Cost&lt;/strong&gt;: Throwing exceptions is expensive. The runtime must create stack traces and unwind the method stack appropriately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code Complexity&lt;/strong&gt;: Calling code must wrap operations in try-catch blocks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implicit Behavior&lt;/strong&gt;: The method signature doesn’t indicate that exceptions might be thrown, requiring documentation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note: Exceptions should handle truly exceptional cases — scenarios where you don’t know how to process the error, such as a lost connection to an external service.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution: The Result Pattern
&lt;/h3&gt;

&lt;p&gt;We can improve this code by implementing the Result pattern using a simple wrapper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public record ServiceResult&amp;lt;TData&amp;gt;(TData Data, string? Error)
{
 public bool Success =&amp;gt; string.IsNullOrEmpty(Error);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this approach, the service code becomes&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public ServiceResult&amp;lt;CreatedPayment?&amp;gt; Create(NewPaymentDto input)
  {
      CreatedPayment paymentResult = new();

      TransactionResult transactionResult = transactionService.Create(input.User, input.Amount);

      if(!transactionResult.Success)
      {
         return new ServiceResult(null, "Transaction creation failure");
      }

      return new ServiceResult(paymentResult, null);
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks better. We can return the same model in the controller method now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[HttpPost("payments/new")]
 public IActionResult NewPayment(NewPaymentInput model)
 {
  ServiceResult&amp;lt;CreatedPayment?&amp;gt; result = paymentService.Create(model.Map&amp;lt;NewPaymentDto&amp;gt;());

  return Ok(result);
 }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, this approach has drawbacks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data Exposure&lt;/strong&gt;: Internal DTO ServiceResult is exposed to external clients&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code Pollution&lt;/strong&gt;: Handling error results with different HTTP status codes requires many if-then blocks in every method. What if we need to return different HTTP status codes in a response depending on the error case? Let’s say in the case the user doesn’t have enough money in an account.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Enhanced Result Pattern with Error Codes
&lt;/h3&gt;

&lt;p&gt;To handle different HTTP status codes based on error types, let’s enhance our approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public record ServiceResult&amp;lt;TData&amp;gt;(TData Data, Error? Error)
{
 public bool Success =&amp;gt; Error != null;
}

public record Error(string Message, int Code);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public ServiceResult&amp;lt;CreatedPayment?&amp;gt; Create(NewPaymentDto input)
  {
      CreatedPayment paymentResult = new();

      decimal funds = accountService.GetFunds(input.User);
      if(funds &amp;lt; input.Amount)
      {
         return new ServiceResult(null, new Error("Insufficient funds amount. Please deposit additional funds.", ErrorCode.Account));
      }

      TransactionResult transactionResult = transactionService.Create(input.User, input.Amount);
      if(!transactionResult.Success)
      {
         return new ServiceResult(null, new Error("Transaction creation failure", ErrorCode.ExternalService));
      }   

      return new ServiceResult(paymentResult, null);
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[HttpPost("payments/new")]
 public IActionResult NewPayment(NewPaymentInput model)
 {
  ServiceResult&amp;lt;CreatedPayment?&amp;gt; result = paymentService.Create(model.Map&amp;lt;NewPaymentDto&amp;gt;());

  if(!result.Success)
  {
    if(result.Error.Code == ErrorCode.Account)
      return BadRequest(result.Error);

    if(result.Error.Code == ErrorCode.ExternalService)
     return Problem(result.Error, statusCode: 500);
  }

  return Ok(payment);
 }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Messy? Let’s get rid of this. We have different ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We can raise custom exceptions still.&lt;/li&gt;
&lt;li&gt;Extract base class with method that handles ServiceResult and returns appropriate response.&lt;/li&gt;
&lt;li&gt;Use &lt;a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-9.0" rel="noopener noreferrer"&gt;middleware&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Solution 1: Base Controller Approach
&lt;/h3&gt;

&lt;p&gt;Exceptions — expensive and not implicit as I pointed out above. Let’s try to write a handler method and extract it into a base class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class BaseApiController: ControllerBase
{
  public IActionResult HandleServiceResult&amp;lt;T&amp;gt;(ServiceResult&amp;lt;T&amp;gt; result)
   {
    if (!result.Success)
      {
       return new ObjectResult(new ApiError { Message = result.Error.Message });)
       {
        StatusCode = result.Error?.Code switch
        {
         ErrorCode.Account=&amp;gt; StatusCodes.Status400BadRequest,
         ErrorCode.Unauthorized=&amp;gt; StatusCodes.Status401Unauthorized,
         ErrorCode.NotFound =&amp;gt; StatusCodes.Status404NotFound,
         ErrorCode.ExternalService=&amp;gt; StatusCodes.Status500InternalServerError,
         _ =&amp;gt; StatusCodes.Status500InternalServerError
        }
       };
      }

      return new ObjectResult(result.Data)
       {
        StatusCode = StatusCodes.Status200OK
       };     
   }
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So finally we can use it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ApiController]
public class PaymentsController(IPaymentService paymentService): BaseApiController
{
 [HttpPost("payments/new")]
 public IActionResult NewPayment(NewPaymentInput model)
 {
  ServiceResult&amp;lt;CreatedPayment?&amp;gt; result = paymentService.Create(model.Map&amp;lt;NewPaymentDto&amp;gt;());
  return HandleServiceResult(payment);
 }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better. Isn't it? But this solution has cons — a base class with a handler method makes dependence on the single base class and forces us to wrap every return result into a call of the handler.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution 2: Action Filter Approach
&lt;/h3&gt;

&lt;p&gt;Let’s consider middlewares. &lt;a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-9.0#create-a-middleware-pipeline-with-webapplication" rel="noopener noreferrer"&gt;As we remember&lt;/a&gt;, the ASP.NET request pipeline consists of a sequence of request delegates, called one after the other. A middleware is called twice: for an incoming request and response. We would rather not process every request and every response—overhead. Here is where &lt;a href="https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-9.0" rel="noopener noreferrer"&gt;action filters&lt;/a&gt; are coming in. They are part of the common request pipeline and allow you to run custom code before or after specific steps in the request pipeline. There are many types of action filters. We need IResultFilter. This interface contains 2 methods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;OnResultExecuting&lt;/em&gt; — called before the action result executes&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;OnResultExecuted&lt;/em&gt; — called after the action result executes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We have to send a processed response to API client, so we have to implement OnResultExecuting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class ServiceResultFilterAttribute: Attribute, IResultFilter
{
 public void OnResultExecuting(ResultExecutingContext context)
 {
  if (context.Result is ObjectResult result)
  {
   if (result.Value is IServiceResult serviceResult)
   {
    if (!serviceResult.Success)
    {
     context.Result = new ObjectResult(serviceResult.Value)
     {
         StatusCode = result.Error?.Code switch
         {
          ErrorCode.Account=&amp;gt; StatusCodes.Status400BadRequest,
          ErrorCode.Unauthorized=&amp;gt; StatusCodes.Status401Unauthorized,
          ErrorCode.NotFound =&amp;gt; StatusCodes.Status404NotFound,
          ErrorCode.ExternalService=&amp;gt; StatusCodes.Status500InternalServerError,
          _ =&amp;gt; StatusCodes.Status500InternalServerError
         }
      }
    };
   }
   else
   {
    context.Result = new ObjectResult(serviceResult.Value)
    {
     StatusCode = StatusCodes.Status200OK
    };
   }
  }
 }

 public void OnResultExecuted(ResultExecutedContext context)
 {
 }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I made it an attribute to apply to a controller or controller’s method. So we can make the payment method cleaner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ApiController]
[ServiceResultFilter]
public class PaymentsController(IPaymentService paymentService): BaseApiController
{
 [HttpPost("payments/new")]
 public ServiceResult&amp;lt;CreatedPayment?&amp;gt; NewPayment(NewPaymentInput model)
 {
  return paymentService.Create(model.Map&amp;lt;NewPaymentDto&amp;gt;());
 }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also, we can apply the filter globally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;builder.Services.AddControllersWithViews(options =&amp;gt;
{
    options.Filters.Add&amp;lt;ServiceResultFilter&amp;gt;();
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I added the marker interface &lt;em&gt;IServiceResult&lt;/em&gt; to distinguish when controller methods return &lt;em&gt;ServiceResult&lt;/em&gt; objects instead of &lt;em&gt;IActionResult&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public record ServiceResult&amp;lt;TData&amp;gt;(TData Data, Error? Error): IServiceResult
{
 public bool Success =&amp;gt; Error != null;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So what about Minimal API, is it possible to apply this approach? — Yes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class ServiceResultFilter: IEndpointFilter
{
 public ValueTask&amp;lt;object?&amp;gt; InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
 {
  // Invoke the handler
  object? result = await next(context);

  // Handle endpoint result
  if (result is IServiceResult serviceResult)
  {
   if (!serviceResult.Success)
   {
    return result.Error?.Code switch
    {
     ErrorCode.Account =&amp;gt; Results.BadRequest(result.Error.Message),
     ErrorCode.Unauthorized =&amp;gt; Results.Unauthorized(),
     ErrorCode.NotFound =&amp;gt; Results.NotFound(result.Error.Message),
     ErrorCode.ExternalService =&amp;gt; Results.InternalServerError(result.Error.Message),
     _ =&amp;gt; Results.InternalServerError(result.Error.Message)
    };
   }
  }

  return Results.Ok(result.Data);
 }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And applying to the endpoint&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app.MapPost("/payments/new", async (
    NewPaymentInput request,
    IPaymentService paymentService) =&amp;gt;
{
    return paymentService.Create(request.Map&amp;lt;NewPaymentDto&amp;gt;());
})
.AddEndpointFilter&amp;lt;ServiceResultFilter&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So we successfully implemented Result pattern and made the controller’s code simpler by moving mapping logic to the Result filter making the code cleaner and testable.&lt;/p&gt;




&lt;h3&gt;
  
  
  Alternative Approaches
&lt;/h3&gt;

&lt;p&gt;Other approaches worth considering include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Extension methods — &lt;a href="https://www.milanjovanovic.tech/blog/functional-error-handling-in-dotnet-with-the-result-pattern" rel="noopener noreferrer"&gt;as discussed by Milan Jovanović&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Custom middleware — for global handling&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://matteland.medium.com/oneof-discriminated-unions-in-c-132e534bda99" rel="noopener noreferrer"&gt;OneOf library&lt;/a&gt; — for discriminated union types&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;In this article, we explored refactoring API endpoints by:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Implementing the Result pattern to unify domain results and separate known error handling from truly exceptional cases&lt;/li&gt;
&lt;li&gt;Extracting mapping logic from controllers/endpoints to result filters, improving code readability and testability&lt;/li&gt;
&lt;li&gt;Providing clean, maintainable solutions that work with both traditional controllers and Minimal APIs&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>aspnet</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
