How can I wrap Web API responses(in .net core) for consistency?

后端 未结 4 1469
误落风尘
误落风尘 2020-12-14 18:27

I need to return a consistent response with a similar structure returned for all requests. In the previous .NET web api, I was able to achieve this using DelegatingHandler (

相关标签:
4条回答
  • 2020-12-14 19:09

    This is an old question but maybe this will help others.

    In AspNetCore 2(not sure if it applies to previous versions) you can add a Custom OutputFormatter. Below is an implementation using the builtin JsonOutputFormatter.

    Note that this wasn't tested thoroughly and I'm not 100% that changing the context is ok. I looked in the aspnet source code and it didn't seem to matter but I might be wrong.

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

    and then register it in your Startup class

    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));
    }
    
    0 讨论(0)
  • 2020-12-14 19:12

    I created a middleware to wrap the response for consistency. I also created an extension method to IApplicationBuilder for convenience when registering this middleware. So in Startup.cs, register middleware :

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        //code removed for brevity.
        ...
        app.UseResponseWrapper();
    
        //code removed for brevity.
        ...
    }
    

    And here's the middleware code:

    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;
            }
        }
    }
    
    0 讨论(0)
  • 2020-12-14 19:18

    For those looking for a modern solution, you can now use AutoWrapper for this.

    It's very easy to use; just add the following to your Startup.cs file:

    app.UseApiResponseAndExceptionWrapper();
    
    0 讨论(0)
  • 2020-12-14 19:22

    I can see at least two options to accomplish this.

    Firstly, if you want to add this wrapper to all api in the project, you can do this by implementing middleware in the startup.cs part of your project. This is done by adding an app.Use just before the app.UseMvc in the "Configure" function in a similar way as follows:

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

    The other option you have is to intercept the call in a controller. This can be done by overriding the OnActionExecuted function in the controller. Something similar to the following:

        public override void OnActionExecuted(ActionExecutedContext context)
        {
            // 
            // add code to update the context.Result as needed.
            //
    
            base.OnActionExecuted(context);
        }
    
    0 讨论(0)
提交回复
热议问题