Integrating with ASP.NET Core - ulfbou/Zentient.Results GitHub Wiki

Integrating with ASP.NET Core

Zentient.Results brings clarity and consistency to your ASP.NET Core APIs by providing explicit outcome handling for your service operations. This guide demonstrates how to manually integrate Zentient.Results into your controllers and minimal API endpoints.

Important Note: This guide focuses on manual integration. A dedicated NuGet package, Zentient.Results.AspNetCore, is planned for release. It will offer more streamlined and opinionated integration patterns, including automatic result-to-action-result mapping, global error handling, and more. Stay tuned for its release for a more automated experience!


1. The Benefit in ASP.NET Core

By returning IResult<T> or IResult from your application services, your API controllers can:

  • Clearly Understand Outcomes: The controller method immediately knows if the operation succeeded or failed, and with what details.
  • Generate Consistent API Responses: Map Zentient.Results statuses and errors to standard HTTP status codes and structured JSON error bodies.
  • Simplify Controller Logic: Reduce nested if statements and try-catch blocks within the controller.

2. Basic Controller/Endpoint Integration Pattern

The core pattern involves:

  1. Your application service returns an IResult<T> or IResult.
  2. Your controller/endpoint method receives this IResult.
  3. Based on result.IsSuccess or result.IsFailure, you return the appropriate IActionResult (e.g., Ok(), NotFound(), BadRequest()).
  4. For failures, you can map result.Status.Code to the HTTP status and include result.Errors in the response body for detailed context.

Example Setup (Service Layer)

// Example Service Layer (from previous wiki pages)
using Zentient.Results;
using System;

public class ProductService
{
    public IResult<Product> GetProductById(Guid id)
    {
        if (id == Guid.Empty)
        {
            return Result<Product>.BadRequest(
                new ErrorInfo(ErrorCategory.Validation, "InvalidProductId", "Product ID cannot be empty.")
            );
        }
        if (id == new Guid("00000000-0000-0000-0000-000000000001"))
        {
            return Result<Product>.Success(new Product { Id = id, Name = "Laptop Pro" });
        }
        return Result<Product>.NotFound(
            new ErrorInfo(ErrorCategory.NotFound, "ProductNotFound", $"Product with ID '{id}' not found.")
        );
    }

    public IResult DeleteProduct(Guid id)
    {
        if (id == Guid.Empty)
        {
            return Result.BadRequest(
                new ErrorInfo(ErrorCategory.Validation, "InvalidProductId", "Product ID cannot be empty for deletion.")
            );
        }
        if (id == new Guid("00000000-0000-0000-0000-000000000002"))
        {
            // Simulate successful deletion
            Console.WriteLine($"Product {id} deleted.");
            return Result.Success(ResultStatuses.NoContent); // 204 No Content
        }
        return Result.NotFound(
            new ErrorInfo(ErrorCategory.NotFound, "ProductNotFound", $"Product with ID '{id}' not found for deletion.")
        );
    }
}

public class Product { public Guid Id { get; set; } public string Name { get; set; } }

3. Integrating in a Controller

using Microsoft.AspNetCore.Mvc;
using System;
using Zentient.Results; // Make sure to include Zentient.Results namespace

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly ProductService _productService;

    public ProductsController(ProductService productService) // Inject your service
    {
        _productService = productService;
    }

    // GET api/products/{id}
    [HttpGet("{id}")]
    public IActionResult GetProduct(Guid id)
    {
        IResult<Product> result = _productService.GetProductById(id);

        if (result.IsSuccess)
        {
            // For success, return 200 OK with the value
            return Ok(result.Value);
        }
        else
        {
            // For failure, map the result status and errors to an appropriate HTTP response
            return StatusCode(result.Status.Code, new { errors = result.Errors, message = result.Error });
        }
    }

    // DELETE api/products/{id}
    [HttpDelete("{id}")]
    public IActionResult DeleteProduct(Guid id)
    {
        IResult result = _productService.DeleteProduct(id);

        if (result.IsSuccess)
        {
            // For success (no content returned), return 204 No Content
            return NoContent();
        }
        else
        {
            // For failure, map the result status and errors
            // You can use a custom error response structure for consistency
            return StatusCode(result.Status.Code, new { errors = result.Errors, message = result.Error });
        }
    }
}

4. Integrating in Minimal APIs

The pattern remains similar for Minimal APIs introduced in .NET 6+.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using System;
using Zentient.Results; // Make sure to include Zentient.Results namespace

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        // Register your ProductService
        builder.Services.AddSingleton<ProductService>();

        var app = builder.Build();

        // GET /products/{id}
        app.MapGet("/products/{id}", (Guid id, ProductService productService) =>
        {
            IResult<Product> result = productService.GetProductById(id);

            if (result.IsSuccess)
            {
                return Results.Ok(result.Value);
            }
            else
            {
                // For failures, use Results.StatusCode or Results.Json with a custom status code
                return Results.Json(new { errors = result.Errors, message = result.Error }, statusCode: result.Status.Code);
            }
        });

        // DELETE /products/{id}
        app.MapDelete("/products/{id}", (Guid id, ProductService productService) =>
        {
            IResult result = productService.DeleteProduct(id);

            if (result.IsSuccess)
            {
                return Results.NoContent();
            }
            else
            {
                return Results.Json(new { errors = result.Errors, message = result.Error }, statusCode: result.Status.Code);
            }
        });

        app.Run();
    }
}

5. Structured Error Responses for APIs

When returning a failure, it's a good practice to provide a consistent, structured error response body to your API consumers. ErrorInfo provides all the necessary details.

A common pattern is to use a structure similar to RFC 7807 Problem Details or a custom JSON format that includes:

  • A high-level message.
  • A list of specific errors (from result.Errors), each with its Category, Code, Message, and Data.
// Example JSON error response
{
  "message": "One or more validation errors occurred.",
  "errors": [
    {
      "category": "Validation",
      "code": "InvalidProductId",
      "message": "Product ID cannot be empty."
    },
    {
      "category": "NotFound",
      "code": "ProductNotFound",
      "message": "Product with ID '00000000-0000-0000-0000-000000000003' not found."
    }
  ]
}

The manual examples above demonstrate how to construct this JSON structure directly. The upcoming Zentient.Results.AspNetCore package will likely provide utilities or middleware to automate this mapping for you.


6. What the Zentient.Results.AspNetCore Package Will Offer

While the manual approach is effective, the dedicated Zentient.Results.AspNetCore package will simplify integration significantly by providing:

  • Automatic Result-to-Action-Result Mapping: Eliminates the need for manual if/else checks in every controller action.
  • Configurable Error Response Formats: Easily customize how ErrorInfo collections are serialized into API error bodies.
  • Global Error Handling Middleware: Centralized handling of ResultException or other unhandled result failures.
  • Dependency Injection Extensions: Streamlined setup for integrating Zentient.Results components.

We encourage you to stay tuned for the release of Zentient.Results.AspNetCore for a more streamlined developer experience in ASP.NET Core applications.


Next Steps

⚠️ **GitHub.com Fallback** ⚠️