Integrating with ASP.NET Core - ulfbou/Zentient.Results GitHub Wiki
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!
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 andtry-catch
blocks within the controller.
The core pattern involves:
- Your application service returns an
IResult<T>
orIResult
. - Your controller/endpoint method receives this
IResult
. - Based on
result.IsSuccess
orresult.IsFailure
, you return the appropriateIActionResult
(e.g.,Ok()
,NotFound()
,BadRequest()
). - For failures, you can map
result.Status.Code
to the HTTP status and includeresult.Errors
in the response body for detailed context.
// 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; } }
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 });
}
}
}
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();
}
}
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 itsCategory
,Code
,Message
, andData
.
// 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.
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.
- Chaining and Composing Operations: Learn how to build complex business logic flows using Zentient.Results.
-
Structured Error Handling with
ErrorInfo
andErrorCategory
: A deeper dive into how errors are represented. -
Managing Operation Status with
IResultStatus
andResultStatuses
: Understand how status codes align with HTTP. - Visit the Zentient.Results GitHub Repository for updates and source code.