Как можно объединить ответы 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));
}