Občas je potrebné cez REST-ové API poslať súbor (napríklad PDF, obrázok, ZIP archív,…), alebo iné binárne dáta. Ak sú malé (desiatky bajtov max. jednotky kB) je stále vhodné použiť štandardnú JSON serializáciu, no pri väčších objemoch dát je vhodné ich posielať ako binárne dáta a nie ako base64 string v JSON-e, lebo sa tým šetrí pamäť, buffering a umožňuje na strane serveru použiť streamové spracovanie.
Tento návod funguje pre ASP.NET Core 3.1 a vyšší s použitím NSwag knižnice.
Na takýto prenos dát myslí aj špecifikácia OpenAPI (Swagger 3) - https://swagger.io/specification/, ktorá hovorí, že sa majú prenášať s content-type application/octet-stream
ako binárny string.
(Pozor: Swagger verzie 2 to neumožňoval, podporoval len upload ako form data s content-type multipart/form-data
- https://swagger.io/docs/specification/2-0/file-upload/).
Pre nahranie (upload) binárnych dát s klienta na server je potrebné určiť konzumovaný content-type a doplniť InputFormater do ASP.NET Core.
Stačí oanotovať akciu API kontroleru nasledovne:
[HttpPost]
[Consumes("application/octet-stream")]
[ProducesResponseType(typeof(void), 200)]
public IActionResult Upload([FromBody] Stream content)
{
// …
return this.Ok();
}
No to pre správne fungovanie je potrebné pridať vlastný InputFormater pre kontroleri:
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Net.Http.Headers;
using System.IO;
public class RawRequestBodyFormatter : InputFormatter
{
public RawRequestBodyFormatter()
{
this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/octet-stream"));
}
public override bool CanRead(InputFormatterContext context)
{
if (context == null) throw new ArgumentNullException(nameof(context));
return context.ModelType == typeof(Stream);
}
public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
Microsoft.AspNetCore.Http.HttpRequest request = context.HttpContext.Request;
if (context.ModelType == typeof(Stream))
{
return await InputFormatterResult.SuccessAsync(request.Body);
}
return await InputFormatterResult.FailureAsync();
}
}
A v triede Startup.cs
do inicializácie kontrolerov (AddControllers
, AddControllersWithViews
,…) zaradiť tento InputFormater:
services.AddControllers(options =>
{
options.InputFormatters.Insert(0, new RawRequestBodyFormatter());
});
Následne aj Sagger UI zobrazuje upload tlačidlo pre túto akciu:
Samotné stiahnutie (download) binárnych dát v ASP.NET Core funguje. No treba pedant vlastný atribút aby to bolo jasné OpenAPI a Swagger UI.
[HttpGet("{id}")]
[ProducesBinaryString(200)]
[Produces("application/octet-stream")]
public async ValueTask<IActionResult> GetDocumentContent(int id)
{
Stream stream = await this.GetContentStreamById(id);
return this.Ok(stream);
}
Je potrebné upraviť response body v OpenAPI špecifikácii, na to sa použije nasledujúci atribút:
[AttributeUsage(AttributeTargets.Method)]
internal class ProducesBinaryStringAttribute : OpenApiOperationProcessorAttribute
{
public ProducesBinaryStringAttribute(int statusCode)
:base(typeof(ProducesBinaryStringOperationProcessor), statusCode)
{
}
}
internal class ProducesBinaryStringOperationProcessor : IOperationProcessor
{
private readonly int statusCode;
public ProducesBinaryStringOperationProcessor(int statusCode)
{
this.statusCode = statusCode;
}
public bool Process(OperationProcessorContext context)
{
string defaultMimeType = context.OperationDescription.Operation.Produces.FirstOrDefault() ?? "application/octet-stream";
NSwag.OpenApiResponse response = new NSwag.OpenApiResponse();
NSwag.OpenApiMediaType mediaType = new NSwag.OpenApiMediaType()
{
Schema = new NJsonSchema.JsonSchema()
{
Type = NJsonSchema.JsonObjectType.String,
Format = "binary"
}
};
response.Content.Add(defaultMimeType, mediaType);
context.OperationDescription.Operation.Responses.Add(this.statusCode.ToString(), response);
return true;
}
}
Pri posielaní binárnych dát na API kontroler sa dá podobne prestupovať aj ku bajtovému poľu alebo stringu, záleží ako sa budú dáta ďalej spracovávať.
No treba myslieť na to, že hoci tento postup dodržuje OpenAPI špecifikáciu, tak nie každý generátor dokáže vygenerovať správneho klienta (stávalo sa mi, že niektoré pre Rust sa pokúšali binárny obsah najskôr serializovať a až potom poslať). V takomto prípade je ale veľmi jednoduché chybnú metódu dopísať ručne.