ASP.NET CORE Minimal API - Content Negotiation
Primárny cieľ ASP.NET Core Minimal API je priniesť priamočiarý, jednoduchý a hlavne veľmi výkonný framework na tvorbu API. Miesto toho aby prinášal features, ktoré umožnia pokryť každý možný scenár použitia (na rozdiel od MVC) je navrhnutý s ohľadom na určité špecifické patterny. Jedným z týchto prípadov je aj to, že Minimal API konzumuje a produkuje iba JSON
formát (Nepodporuje teda ani Content Negotiation ktorý sa v prípade MVC rieši pomocou Output formatters).
Sú ale situácie, keď naozaj potrebujete buď iný typ výstupu (napríklad štandardný XML
), prípadne potrebujete mať proces serializácie viac pod kontrolou a zároveň chcete využiť jednoduchosť a silu Minimal API.
V tomto článku sa pokúsim ukázať ako je možné si vytvoriť vlastnú podporu pre Content Negotiation v Minimal API.
⚠️ Je to síce funkčná implementácia, ale nepokrýva všetky scenáre a je to len základná ukážka toho ako by mohla takáto podpora vyzerať.
Celé riešenie je založené na vlastnej implementácií pre IResult
. Viac info v dokumentácií.
IResponseNegotiator
Definujme si najskôr rozhranie, ktorého implementácia bude zodpovedná za serializovanie odpovede.
public interface IResponseNegotiator
{
string ContentType { get; }
bool CanHandle(MediaTypeHeaderValue accept);
Task Handle<TResult>(HttpContext httpContext, TResult result, CancellationToken cancellationToken);
}
Implementácia pre JSON
Pre JSON môžeme využiť priamo Ok<TResult>
result, ktorý je súčasťou Minimal API.
public class JsonNegotiator : IResponseNegotiator
{
public string ContentType => MediaTypeNames.Application.Json;
public bool CanHandle(MediaTypeHeaderValue accept)
=> accept.MediaType == ContentType;
public Task Handle<TResult>(HttpContext httpContext, TResult result, CancellationToken cancellationToken)
{
// 👇 Use original Ok<TResult> type for JSON serialization
return TypedResults.Ok(result).ExecuteAsync(httpContext);
}
}
Implementácia pre XML
Pre XML môžeme vytvoriť vlastnú implementáciu, ktorá bude využívať napríklad DataContractSerializer
.
public class XmlNegotiator : IResponseNegotiator
{
public string ContentType => MediaTypeNames.Application.Xml;
public bool CanHandle(MediaTypeHeaderValue accept)
=> accept.MediaType == ContentType;
public async Task Handle<TResult>(HttpContext httpContext, TResult result, CancellationToken cancellationToken)
{
httpContext.Response.ContentType = ContentType;
// 👇 Use DataContractSerializer for XML serialization
using var stream = new FileBufferingWriteStream();
using var streamWriter = new StreamWriter(stream);
var serializer = new DataContractSerializer(result.GetType());
serializer.WriteObject(stream, result);
await stream.DrainBufferAsync(httpContext.Response.Body, cancellationToken);
}
}
Registrovanie
Bohužiaľ vzhľadom na fungovanie IEndpointMetadataProvider
a spôsobu ako funguje serializácia priamo v Minimal API sa mi nepodarilo nájsť elegantný spôsob ako využiť DI kontajner (tých neelegantných by zopár bolo 😊).
Preto som sa uchýlil k vlastnému registrátoru pre negotiatorov.
public static class ContentNegotiationProvider
{
private static readonly List<IResponseNegotiator> _negotiators = [];
// 👇 Internal list of negotiators
internal static IReadOnlyList<IResponseNegotiator> Negotiators => _negotiators;
// 👇 Add negotiator to the list
public static void AddNegotiator<TNegotiator>()
where TNegotiator : IResponseNegotiator, new()
{
_negotiators.Add(new TNegotiator());
}
}
Registrujeme negotiatorov v Program.cs
alebo v Startup.cs
.
ContentNegotiationProvider.AddNegotiator<JsonNegotiator>();
ContentNegotiationProvider.AddNegotiator<XmlNegotiator>();
ContentNegotiationResult<TResult>
public class ContentNegotiationResult<TResult>(TResult result)
: IResult, IEndpointMetadataProvider, IStatusCodeHttpResult, IValueHttpResult
{
private readonly TResult _result = result;
// ...
public Task ExecuteAsync(HttpContext httpContext)
{
if (_result == null)
{
httpContext.Response.StatusCode = StatusCodes.Status204NoContent;
return Task.CompletedTask;
}
// 👇 Get negotiator based on Accept header
var negotiator = GetNegotiator(httpContext);
if (negotiator == null)
{
httpContext.Response.StatusCode = StatusCodes.Status406NotAcceptable;
return Task.CompletedTask;
}
// 👇 Set status code
httpContext.Response.StatusCode = StatusCode;
// 👇 Handle the result
return negotiator.Handle(httpContext, _result, httpContext.RequestAborted);
}
private static IResponseNegotiator? GetNegotiator(HttpContext httpContext)
{
var accept = httpContext.Request.GetTypedHeaders().Accept;
// 👇 Get negotiator based on Accept header (use ContentNegotiationProvider)
return ContentNegotiationProvider.Negotiators.FirstOrDefault(n =>
{
return accept.Any(a => n.CanHandle(a));
});
}
//...
}
Aby dokumentácia bola pekne vygenerovaná a obsahovala informácie o možných formátoch, môžeme implementovať IEndpointMetadataProvider
.
static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
// 👇 Add produces response type metadata
builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status200OK, typeof(TResult),
ContentNegotiationProvider.Negotiators.Select(n => n.ContentType).ToArray()));
}
Helper method
Vytvorme si statický helper pre jednoduchšie použitie.
public static class Negotiation
{
public static ContentNegotiationResult<T> Negotiate<T>(T result)
=> new(result);
}
Použitie
app.MapGet("/products", () =>
{
// 👇 Use Negotiation
return Negotiation.Negotiate(new List<Product>() { new(1, "Product 1", 100) });
});
app.MapGet("/products/{id}", GetProduct);
static Results<ContentNegotiationResult<Product>, NotFound> GetProduct(int id)
{
if (id == 1)
{
// 👇 Use Negotiation
return Negotiation.Negotiate(new Product(1, "Product 1", 100));
}
else
{
return TypedResults.NotFound();
}
}
A je to ✅.
### GET products as XML
GET http://localhost:5210/product/
Accept: application/xml
### Response
<ArrayOfProduct xmlns="http://schemas.datacontract.org/2004/07/" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<Product>
<Id>1</Id>
<Name>Product 1</Name>
<Price>100</Price>
</Product>
</ArrayOfProduct>
⚠️ Toto riešenie je len základný návrh a nepokrýva všetky možné scenáre.
Napríklad takto ako je to spravené to môžete použiť len pre200 OK
odpovede (dá sa to ale rozšíriť).
Celý príklad je na GitHub.