Как можно объединить ответы Web API (в ядре .net) для согласованности?
Мне нужно вернуть согласованный ответ с аналогичной структурой, возвращенной для всех запросов. В предыдущем .NET web api я смог добиться этого, используя DelegatingHandler (MessageHandlers). Объект, который я хочу вернуть, будет инкапсулирован в элемент "Результат". Таким образом, в этом виде структура будет отвечать json-ответ:
Пример 1:
{
"RequestId":"some-guid-abcd-1234",
"StatusCode":200,
"Result":
{
"Id":42,
"Todo":"Do Hello World"
}
}
Пример 2:
{
"RequestId":"some-guid-abcd-1235",
"StatusCode":200,
"Result":
{
[
{
"Id":42,
"Todo":"Print Hello World"
},
{
"Id":43,
"Todo":"Print Thank you"
}
]
}
}
В ядре .NET, похоже, мне нужно сделать это через промежуточное программное обеспечение. Я попытался, но я не вижу лучшего способа извлечь контент, например, как в предыдущем веб-API, когда вы можете вызвать HttpResponseMessage.TryGetContentValue
, чтобы получить контент и обернуть его в глобальную/общую модель ответа.
Как я могу добиться того же в ядре .NET?
Ответы
Ответ 1
Я создал промежуточное ПО, чтобы обернуть ответ для согласованности. Я также создал метод расширения для IApplicationBuilder для удобства при регистрации этого промежуточного программного обеспечения. Итак, в Startup.cs зарегистрируйте промежуточное ПО:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
//code removed for brevity.
...
app.UseResponseWrapper();
//code removed for brevity.
...
}
И вот код промежуточного кода:
using System;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
namespace RegistrationWeb.Middleware
{
public class ResponseWrapper
{
private readonly RequestDelegate _next;
public ResponseWrapper(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
var currentBody = context.Response.Body;
using (var memoryStream = new MemoryStream())
{
//set the current response to the memorystream.
context.Response.Body = memoryStream;
await _next(context);
//reset the body
context.Response.Body = currentBody;
memoryStream.Seek(0, SeekOrigin.Begin);
var readToEnd = new StreamReader(memoryStream).ReadToEnd();
var objResult = JsonConvert.DeserializeObject(readToEnd);
var result = CommonApiResponse.Create((HttpStatusCode)context.Response.StatusCode, objResult, null);
await context.Response.WriteAsync(JsonConvert.SerializeObject(result));
}
}
}
public static class ResponseWrapperExtensions
{
public static IApplicationBuilder UseResponseWrapper(this IApplicationBuilder builder)
{
return builder.UseMiddleware<ResponseWrapper>();
}
}
public class CommonApiResponse
{
public static CommonApiResponse Create(HttpStatusCode statusCode, object result = null, string errorMessage = null)
{
return new CommonApiResponse(statusCode, result, errorMessage);
}
public string Version => "1.2.3";
public int StatusCode { get; set; }
public string RequestId { get; }
public string ErrorMessage { get; set; }
public object Result { get; set; }
protected CommonApiResponse(HttpStatusCode statusCode, object result = null, string errorMessage = null)
{
RequestId = Guid.NewGuid().ToString();
StatusCode = (int)statusCode;
Result = result;
ErrorMessage = errorMessage;
}
}
}
Ответ 2
Я могу увидеть как минимум два варианта для этого.
Во-первых, если вы хотите добавить эту оболочку ко всем api в проект, вы можете сделать это, внедрив промежуточное ПО в часть startup.cs вашего проекта. Это делается путем добавления app.Use
непосредственно перед app.UseMvc
в функции "Настроить" следующим образом:
app.Use(async (http, next) =>
{
//remember previous body
var currentBody = http.Response.Body;
using (var memoryStream = new MemoryStream())
{
//set the current response to the memorystream.
http.Response.Body = memoryStream;
await next();
string requestId = Guid.NewGuid().ToString();
//reset the body as it gets replace due to https://github.com/aspnet/KestrelHttpServer/issues/940
http.Response.Body = currentBody;
memoryStream.Seek(0, SeekOrigin.Begin);
//build our content wrappter.
var content = new StringBuilder();
content.AppendLine("{");
content.AppendLine(" \"RequestId\":\"" + requestId + "\",");
content.AppendLine(" \"StatusCode\":" + http.Response.StatusCode + ",");
content.AppendLine(" \"Result\":");
//add the original content.
content.AppendLine(new StreamReader(memoryStream).ReadToEnd());
content.AppendLine("}");
await http.Response.WriteAsync(content.ToString());
}
});
Другой вариант, который у вас есть, - перехватить вызов в контроллере. Это можно сделать, переопределив функцию OnActionExecuted
в контроллере. Что-то похожее на следующее:
public override void OnActionExecuted(ActionExecutedContext context)
{
//
// add code to update the context.Result as needed.
//
base.OnActionExecuted(context);
}
Ответ 3
Это старый вопрос, но, возможно, это поможет другим.
В AspNetCore 2 (не уверен, что он применяется к предыдущим версиям) вы можете добавить пользовательский OutputFormatter
. Ниже приведена реализация с использованием встроенного JsonOutputFormatter
.
Примечание, что это не было проверено полностью, и я не на 100%, что изменение контекста в порядке. Я посмотрел в исходном коде aspnet, и это, похоже, не имело значения, но я могу ошибаться.
public class CustomJsonOutputFormatter : JsonOutputFormatter
{
public CustomJsonOutputFormatter(JsonSerializerSettings serializerSettings, ArrayPool<char> charPool)
: base(serializerSettings, charPool)
{ }
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
{
if (context.HttpContext.Response.StatusCode == (int)HttpStatusCode.OK)
{
var @object = new ApiResponse { Data = context.Object };
var newContext = new OutputFormatterWriteContext(context.HttpContext, context.WriterFactory, typeof(ApiResponse), @object);
newContext.ContentType = context.ContentType;
newContext.ContentTypeIsServerDefined = context.ContentTypeIsServerDefined;
return base.WriteResponseBodyAsync(newContext, selectedEncoding);
}
return base.WriteResponseBodyAsync(context, selectedEncoding);
}
}
а затем зарегистрируйте его в своем классе запуска
public void ConfigureServices(IServiceCollection services)
{
var jsonSettings = new JsonSerializerSettings
{
NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore,
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
options.OutputFormatters.RemoveType<JsonOutputFormatter>();
options.OutputFormatters.Add(new WrappedJsonOutputFormatter(jsonSettings, ArrayPool<char>.Shared));
}