【问题标题】:How can I wrap Web API responses(in .net core) for consistency?如何包装 Web API 响应(在 .net 核心中)以保持一致性?
【发布时间】:2017-04-06 13:03:18
【问题描述】:

我需要返回一个一致的响应,所有请求都返回一个类似的结构。在之前的 .NET web api 中,我可以使用 DelegatingHandler (MessageHandlers) 来实现这一点。我要返回的对象将被封装在 Result 元素中。所以基本上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 核心中,看起来我需要通过中间件来执行此操作。我试过了,但是当您可以调用 HttpResponseMessage.TryGetContentValue 来获取内容并将其包装在全局/通用响应模型中时,我没有看到更好的方法来提取内容,就像在以前的 Web API 中那样。

如何在 .NET core 中实现同样的功能?

【问题讨论】:

  • 是的,中间件是一个控制点。您需要缓冲正文,重新解析它,更新它,然后发送结果。 MVC 还可能具有响应过滤器,可让您在序列化之前更改操作结果。
  • 您在项目中使用什么架构?如果您有 n 层逻辑,则不必将对象包装在 Web 项目中,您可以在业务层或此类层中执行此操作。如果您想在执行操作后包装您的结果,那么中间件是一个选项,我不知道其他任何东西。
  • @kizilsu 在n层架构中,业务层包含丰富的领域模型。因此在 api 层中,它需要映射到您想要公开给 api 的“哑”/view/dto 模型。映射后需要设置为一致响应模型中的Result属性/字段。

标签: c# asp.net-core asp.net-core-mvc


【解决方案1】:

对于那些正在寻找现代统包解决方案的人,您现在可以使用AutoWrapper

它非常易于使用;只需将以下内容添加到您的 Startup.cs 文件中:

app.UseApiResponseAndExceptionWrapper();

【讨论】:

    【解决方案2】:

    这是一个老问题,但也许这会对其他人有所帮助。

    在 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);
        }
    }
    

    然后在你的 Startup 类中注册它

    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));
    }
    

    【讨论】:

      【解决方案3】:

      我创建了一个中间件来包装响应以保持一致性。为了方便注册这个中间件,我还为 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;
              }
          }
      }
      

      【讨论】:

      • 你能用errormessage展示你的代码吗?
      • @FatihErol 检查!但仍然不完整,发生异常时没有实现。
      • 如果你像BadRequest("error message")一样使用它,它会在JsonConvert.DeserializeObject(readToEnd);中断你可以像return BadRequest(new NotImplementedException());一样使用它
      【解决方案4】:

      我至少可以看到两个选项来完成此任务。

      首先,如果您想将此包装器添加到项目中的所有 api,您可以通过在项目的 startup.cs 部分中实现中间件来实现。这是通过在“配置”函数中的 app.UseMvc 之前添加一个 app.Use 来完成的,方法如下:

      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);
          }
      

      【讨论】:

      • 我以前看过这个解决方案,但我一直在寻找一个更干净的解决方案,避免使用MemoryStream。在 .NET Framework 中,我可以使用 HttpResponse 消息的 TryGetContentValue 来做到这一点。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-04-07
      • 1970-01-01
      • 2019-10-25
      • 2020-06-24
      • 2012-11-18
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多