JSONP implementation of ASP.NET Core Web API under .NET 5.0

Recently, I was doing an upgrade from .NET Framework to .NET 5.0, and an important problem I encountered was the problem of cross-domain requests. Because the old project mainly uses JSONP to implement cross-domain requests, so in order to ensure support for the old system, we still need to implement support for JSONP. However, during the actual upgrade process, I found that there are very few existing articles about the implementation of JSONP under .NET 5.0. After trying a few articles, I found that they can only be applied to .NET Core 3.1 and previous versions, while No longer available for the .NET 5.0 Framework. Therefore, after consulting the official documents and constantly trying, I found a method, and I record and share it here. I also hope that some big guys can give me suggestions or better implementation methods.

Write in front

  1. Microsoft recommends (CORS) to enable cross-origin request ASP.NET Core in .NET 5.0. If you don’t have to use JSONP, you can consider using Cross-Origin Resource Sharing (CORS) to achieve cross-domain.
  2. For .NET Core 3.1 and earlier versions, or using the CORS method, you can directly refer to the article: Cross-domain solutions in Asp.Net Core (Cors, jsonp transformation, chrome configuration)

Implementing JSONP with a custom formatter

This article refers to the custom formatter implementation in the official document ASP.NET Core Web API

1. Create a JSONP formatter class

First, we need to create a JSONP formatter class JsonpMediaTypeFormatter, which is derived from TextOutputFormatter.

public class JsonpMediaTypeFormatter : TextOutputFormatter
{<!-- -->
    public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
    {<!-- -->
        throw new NotImplementedException();
    }
}

2. Specify a valid media type and encoding in the constructor

We need to specify a valid media type and encoding in the constructor of JsonpMediaTypeFormatter.

private static readonly MediaTypeHeaderValue JsonType = new MediaTypeHeaderValue("application/json");
private static readonly MediaTypeHeaderValue JsType = new MediaTypeHeaderValue("application/javascript");
private static readonly MediaTypeHeaderValue TextType = new MediaTypeHeaderValue("text/javascript");

public JsonpMediaTypeFormatter()
{<!-- -->
    SupportedMediaTypes. Add(JsonType);
    SupportedMediaTypes. Add(JsType);
    SupportedMediaTypes. Add(TextType);

    SupportedEncodings. Add(Encoding. UTF8);
    SupportedEncodings. Add(Encoding. Unicode);
}

3. Specify the conditions for using the JSONP format

We need to override the CanWriteType method to specify when to start the JSONP format to process the returned data. Usually, when we use GET request and the number of callback is not empty, we implement the response to JSONP request.

public override bool CanWriteResult(OutputFormatterCanWriteContext context)
{<!-- -->
    if (context. HttpContext. Request. Method != HttpMethods. Get)
    {<!-- -->
        return false;
    }

    string callback = HttpUtility.UrlDecode((string)context.HttpContext.Request.Query["callback"]);
    return !string.IsNullOrEmpty(callback);
}

4. Transform the data result to realize the JSONP format

Finally, you only need to rewrite the WriteResponseBodyAsync method to return the corresponding JSONP format data according to the callback parameter.

public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
{<!-- -->
    string callback = HttpUtility.UrlDecode((string)context.HttpContext.Request.Query["callback"]);

    var buffer = new StringBuilder();
    buffer. Append(callback);
    buffer.Append('(');
    buffer.Append(JsonSerializer.Serialize(context.Object));
    buffer.Append(')');

    await context.HttpContext.Response.WriteAsync(buffer.ToString(), selectedEncoding);
}

5. Register JSONP formatter class

At this time, it is still the last step to use the JSONP format normally. We need to register the JsonpMediaTypeFormatter we just created in ConfigureServices.

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{<!-- -->
    services. AddControllers(options =>
    {<!-- -->
        options.OutputFormatters.Insert(0, new JsonpMediaTypeFormatter());
    });
}

6. Finally

So far, our Web API can support JSONP requests, and finally attach the complete JsonpMediaTypeFormatter code.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Net.Http.Headers;
using System. Text;
using System. Text. Json;
using System. Threading. Tasks;
using System. Web;

namespace Formatter
{<!-- -->
    public class JsonpMediaTypeFormatter : TextOutputFormatter
    {<!-- -->
        private static readonly MediaTypeHeaderValue JsonType = new MediaTypeHeaderValue("application/json");
        private static readonly MediaTypeHeaderValue JsType = new MediaTypeHeaderValue("application/javascript");
        private static readonly MediaTypeHeaderValue TextType = new MediaTypeHeaderValue("text/javascript");

        public JsonpMediaTypeFormatter()
        {<!-- -->
            SupportedMediaTypes. Add(JsonType);
            SupportedMediaTypes. Add(JsType);
            SupportedMediaTypes. Add(TextType);

            SupportedEncodings. Add(Encoding. UTF8);
            SupportedEncodings. Add(Encoding. Unicode);
        }

        public override bool CanWriteResult(OutputFormatterCanWriteContext context)
        {<!-- -->
            if (context. HttpContext. Request. Method != HttpMethods. Get)
            {<!-- -->
                return false;
            }

            string callback = HttpUtility.UrlDecode((string)context.HttpContext.Request.Query["callback"]);
            return !string.IsNullOrEmpty(callback);
        }

        public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
        {<!-- -->
            string callback = HttpUtility.UrlDecode((string)context.HttpContext.Request.Query["callback"]);

            var buffer = new StringBuilder();
            buffer. Append(callback);
            buffer.Append('(');
            buffer.Append(JsonSerializer.Serialize(context.Object));
            buffer.Append(')');

            await context.HttpContext.Response.WriteAsync(buffer.ToString(), selectedEncoding);
        }
    }
}

Attachment: failure experience

At first I was trying to take the filter ActionFilterAttribute by overriding
OnActionExecuted method to achieve the adjustment of the data results. However, in the actual test, it was found that the following error would occur after passing in the callback parameter, so the idea of implementing JSONP in this way was abandoned.

fail: Microsoft.AspNetCore.Server.Kestrel[13]
Connection id “0HM8UGOM59TCA”, Request id “0HM8UGOM59TCA:00000006”: An unhandled exception was thrown by the application.
System.InvalidOperationException: Headers are read-only, response has already started.
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.ThrowHeadersReadOnlyException()
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.Microsoft.AspNetCore.Http.IHeaderDictionary.set_Item(String key, StringValues value)
at Microsoft.AspNetCore.Http.DefaultHttpResponse.set_ContentType(String value)
at Microsoft.AspNetCore.Mvc.Formatters.OutputFormatter.WriteResponseHeaders(OutputFormatterWriteContext context)
at Microsoft.AspNetCore.Mvc.Formatters.TextOutputFormatter.WriteAsync(OutputFormatterWriteContext context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor.ExecuteAsyncCore(ActionContext context, ObjectResult result, Type objectType, Object value)
at Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor.ExecuteAsync(ActionContext context, ObjectResult result)
at Microsoft.AspNetCore.Mvc.ObjectResult.ExecuteResultAsync(ActionContext context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeResultAsync(IActionResult result)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State & amp; next, Scope & amp; scope, Object & amp; state, Boolean & amp; isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeNextResultFilterAsyncTFilter,TFilterAsync
– End of stack trace from previous location –
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State & amp; next, Scope & amp; scope, Object & amp; state, Boolean & amp; isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeResultFilters()
– End of stack trace from previous location –
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)