I am trying to change the JSON parser in my web API project.
I have followed the following tutorials:
https://learn.microsoft.com/en-us/aspnet/core/web-api/advanced/custom-formatters?view=aspnetcore-2.2
https://www.youtube.com/watch?v=tNzgXjqqIjI
https://weblog.west-wind.com/posts/2012/Mar/09/Using-an-alternate-JSON-Serializer-in-ASPNET-Web-API
I now have the following code:
public class MyJsonFormatter : MediaTypeFormatter
{
public MyJsonFormatter()
{
base.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
}
public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext, CancellationToken cancellationToken)
{
return null;
}
public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext)
{
return null;
}
public override void SetDefaultContentHeaders(Type type, HttpContentHeaders headers, MediaTypeHeaderValue mediaType)
{
base.SetDefaultContentHeaders(type, headers, mediaType);
}
public override bool CanReadType(Type type)
{
return true;
}
public override bool CanWriteType(Type type)
{
return true;
}
public override MediaTypeFormatter GetPerRequestFormatterInstance(Type type, HttpRequestMessage request, MediaTypeHeaderValue mediaType)
{
return base.GetPerRequestFormatterInstance(type, request, mediaType);
}
public override Task<object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
{
return null;
}
public override Task<object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger, CancellationToken cancellationToken)
{
return null;
}
public override IRequiredMemberSelector RequiredMemberSelector { get => base.RequiredMemberSelector; set => base.RequiredMemberSelector = value; }
}
public static void Register(HttpConfiguration config)
{
///Other stuff...
GlobalConfiguration.Configuration.Formatters.Clear();
GlobalConfiguration.Configuration.Formatters.Insert(0, new MyJsonFormatter());
}
My issue is that whatever I do, JSON gets parsed and it seems to ignore my code - I can throw exceptions in the read or write methods and nothing will happen, break points do not get hit etc.
I know this formatter is being added as only the content types in my class are visible and if I set CanReadType to return false then nothing gets parsed.
My question is, how can I make the code execute my overrides?
Update how the formatter is registered
public static class WebApiConfig {
public static void Register(HttpConfiguration config) {
// Other stuff...
var jsonFormatter = new MyJsonFormatter();
config.Formatters.Clear();
config.Formatters.Insert(0, jsonFormatter);
//...
}
}
Making sure the suggested syntax is followed in Startup or where ever the application is started.
// configure Web Api
GlobalConfiguration.Configure(WebApiConfig.Register);
There is also the process of content negotiation as suggested in the following article
Supporting only JSON in ASP.NET Web API – the right way
Adapting it to your example, it would look like
public class JsonContentNegotiator : IContentNegotiator {
MediaTypeHeaderValue mediaType = MediaTypeHeaderValue.Parse("application/json;charset=utf-8");
private readonly MyJsonFormatter _jsonFormatter;
public JsonContentNegotiator(MyJsonFormatter formatter) {
_jsonFormatter = formatter;
}
public ContentNegotiationResult Negotiate(Type type, HttpRequestMessage request, IEnumerable<MediaTypeFormatter> formatters) {
var result = new ContentNegotiationResult(_jsonFormatter, mediaType);
return result;
}
}
And registered against your HttpConfiguration
var jsonFormatter = new MyJsonFormatter();
config.Formatters.Clear();
config.Formatters.Insert(0, jsonFormatter);
//update content negotiation
config.Services.Replace(typeof(IContentNegotiator), new JsonContentNegotiator(jsonFormatter));
Finally one piece of information to note is that the framework did tightly couple its JSON formatting to its own JsonMediaTypeFormatter
/// <summary>
/// Gets the <see cref="MediaTypeFormatter"/> to use for Json.
/// </summary>
public JsonMediaTypeFormatter JsonFormatter
{
get { return Items.OfType<JsonMediaTypeFormatter>().FirstOrDefault(); }
}
Reference source
So depending on how much of the pipeline actually depends on the existence of an instance of JsonMediaTypeFormatter, it would probably affect JSON related formatting.
If it is in fact a problem then my suggestion would be to derive from JsonMediaTypeFormatter and override its members as needed.
public class MyJsonFormatter : JsonMediaTypeFormatter {
//... removed for brevity
}
But that might bring with it, its own problems depending on what that base class is coupled to.
You need to register your formatter in the startup config.
Related
Normally, I'd just do in my controller action:
return Content(System.Net.HttpStatusCode.InternalServerError,
new MyCustomObject("An error has occurred processing your request.", // Custom object, serialised to JSON automatically by the web api service
ex.ToString()));`
However the Content method exists on the controller. The ExceptionHandler I made has this:
public override void Handle(ExceptionHandlerContext context)
{
context.Result = ???;
The type of context.Result is IHttpActionResult, so what I need to do is create one and stick it in there. I can't find any constructors or similar that will allow me to create an IHttpActionResult outside of a controller. Is there an easy way?
I thing for custom responses you should probably implement your own http action result:
public override void Handle(ExceptionHandlerContext context)
{
context.Result = new HttpContentResult(new { }, context.Request);
}
public class HttpContentResult : IHttpActionResult
{
private readonly object content;
private readonly HttpRequestMessage requestMessage;
public HttpContentResult(object content, HttpRequestMessage requestMessage)
{
this.content = content;
this.requestMessage = requestMessage;
}
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
var httpContentResponse = new HttpResponseMessage(HttpStatusCode.BadRequest);
var httpContent = new StringContent(content);
//... [customize http contetnt properties]
httpContentResponse.Content = httpContent;
httpContentResponse.RequestMessage = this.requestMessage;
//... [customize another http response properties]
return Task.FromResult(httpContentResponse);
}
}
I'm experiencing strange behavior with a custom MediaTypeFormatter, but only with an Owin Self-hosted WebApi.
With a standard .NET WebApi project hosted in IIS, the same formatter works fine.
Stepping through the program, CanWriteType in the Formatter is getting called multiple times. The exception logger is firing with an exception of Cannot access a closed stream.
Any insight would be very helpful! Removing the async nature of this formatter results in it working fine so It's mostly likely some weird threading issue. I'd like to stay with an asynchronous formatter and not use the BufferedMediaTypeFormatter if at all possible.
Stack Trace:
at System.IO.__Error.StreamIsClosed()
at System.IO.MemoryStream.Seek(Int64 offset, SeekOrigin loc)
at System.Net.Http.HttpContent.<>c__DisplayClass21_0.<LoadIntoBufferAsync>b__0(Task copyTask)
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Web.Http.Owin.HttpMessageHandlerAdapter.<BufferResponseContentAsync>d__13.MoveNext()
Startup.cs:
namespace ConsoleApplication1
{
public class TraceExceptionLogger : ExceptionLogger
{
public override void Log(ExceptionLoggerContext context)
{
Trace.TraceError(context.ExceptionContext.Exception.ToString());
}
}
public class Startup
{
// This code configures Web API. The Startup class is specified as a type
// parameter in the WebApp.Start method.
public void Configuration(IAppBuilder appBuilder)
{
// Configure Web API for self-host.
HttpConfiguration config = new HttpConfiguration();
// Add in a simple exception tracer so we can see what is causing the 500 Internal Server Error
config.Services.Add(typeof(IExceptionLogger), new TraceExceptionLogger());
// Add in the custom formatter
config.Formatters.Add(new JsonFhirAsyncFormatter());
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.Formatters.Add(new JsonFhirAsyncFormatter());
appBuilder.UseWebApi(config);
}
}
}
Formatter:
namespace ConsoleApplication1
{
public class JsonFhirAsyncFormatter : MediaTypeFormatter
{
public JsonFhirAsyncFormatter()
{
SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json+fhir"));
}
public override bool CanWriteType(Type type)
{
return true;
}
public override bool CanReadType(Type type)
{
return false;
}
public override async Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext)
{
using (StreamWriter streamwriter = new StreamWriter(writeStream))
{
await streamwriter.WriteAsync("{\"test\":\"test\"}");
}
}
}
}
Controller:
namespace ConsoleApplication1.Controllers
{
[RoutePrefix("test")]
public class TestController : ApiController
{
[HttpGet]
[Route("")]
public async Task<string> Test()
{
// Eventually this will using an await method
return "test";
}
}
}
Program.cs:
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
WebApp.Start<Startup>("http://localhost:9000");
Console.ReadLine();
}
}
}
Maybe you could try this:
public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext)
{
using (StreamWriter streamwriter = new StreamWriter(writeStream))
{
return streamwriter.WriteAsync("{\"test\":\"test\"}");
}
}
This is then not activating the Async stuff, and allows the Async to occur when needed.
However in my server (sqlonfhir) this function is implemented as
public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext)
{
StreamWriter writer = new StreamWriter(writeStream);
JsonWriter jsonwriter = new JsonTextWriter(writer);
if (type == typeof(OperationOutcome))
{
Resource resource = (Resource)value;
FhirSerializer.SerializeResource(resource, jsonwriter);
}
else if (typeof(Resource).IsAssignableFrom(type))
{
if (value != null)
{
Resource r = value as Resource;
SummaryType st = SummaryType.False;
if (r.UserData.ContainsKey("summary-type"))
st = (SummaryType)r.UserData["summary-type"];
FhirSerializer.SerializeResource(r, jsonwriter, st);
}
}
writer.Flush();
return Task.FromResult<object>(null); // Task.CompletedTask // When we update to .net 4.6;
}
(And yes, I do my unit testing using OWIN self testing with this code too)
I have a simple MediaTypeFormatter like so:
public class SomeFormatter : MediaTypeFormatter
{
public override bool CanReadType(Type type)
{
return type == typeof(SomeRequest);
}
public override bool CanWriteType(Type type)
{
return type == typeof(SomeResponse);
}
public override Task<object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
{
return Task.Factory.StartNew(() =>
{
using (readStream)
{
return (object)new SomeRequest();
}
});
}
public override Task<object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger,
CancellationToken cancellationToken)
{
// ReSharper disable once MethodSupportsCancellation
return ReadFromStreamAsync(type, readStream, content, formatterLogger);
}
public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext)
{
return Task.Factory.StartNew(() =>
{
for (var i = 0; i < 255; i++)
{
writeStream.WriteByte((byte)i);
}
});
}
}
It is wired in WebApiConfig like so:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
config.Formatters.Clear();
config.Formatters.Add(new SomeFormatter());
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
and a Web API controller :
public class SomeController : ApiController
{
public SomeResponse Get(SomeRequest request)
{
return new SomeResponse();
}
}
yet when I test the controller with a GET (from a browser) I get a null request. The CanReadType fires and returns true but then none of the ReadFromStreamAsync overloads fire.
What could be wrong?
It was the content-type header (or lack of).
Although the formatter was inquired if it is able to deserialize this Type, it failed the next check, namely to see if it supports the content-type supplied, or in case it was not supplied, application/octet-stream.
All that was needed was this:
public class SomeFormatter : MediaTypeFormatter
{
public SomeFormatter()
{
SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/octet-stream"));
}
...
}
When calling HttpClient's extension method PostAsXmlAsync, it ignores the XmlRootAttribute on the class. Is this behaviour a bug?
Test
[Serializable]
[XmlRoot("record")]
class Account
{
[XmlElement("account-id")]
public int ID { get; set }
}
var client = new HttpClient();
await client.PostAsXmlAsync(url, new Account())
Looking at the source code of PostAsXmlAsync, we can see that it uses XmlMediaTypeFormatter which internally uses DataContractSerializer and not XmlSerializer. The former doesn't respect the XmlRootAttribute:
public static Task<HttpResponseMessage> PostAsXmlAsync<T>(this HttpClient client, Uri requestUri, T value, CancellationToken cancellationToken)
{
return client.PostAsync(requestUri, value, new XmlMediaTypeFormatter(),
cancellationToken);
}
In order to achieve what you need, you can create a your own custom extension method which explicitly specifies to use XmlSerializer:
public static class HttpExtensions
{
public static Task<HttpResponseMessage> PostAsXmlWithSerializerAsync<T>(this HttpClient client, Uri requestUri, T value, CancellationToken cancellationToken)
{
return client.PostAsync(requestUri, value,
new XmlMediaTypeFormatter { UseXmlSerializer = true },
cancellationToken);
}
}
I have a self-hosted WebApi application with a custom MediaTypeFormatter
Depending on the "name" parameter (Or thereby part of the URL), the application should format the request body to varying types.
Here's the action
// http://localhost/api/fire/test/
// Route: "api/fire/{name}",
public HttpResponseMessage Post([FromUri] string name, object data)
{
// Snip
}
Here's the custom MediaTypeFormatter.ReadFromStreamAsync
public override Task<object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
{
var name = "test"; // TODO this should come from the current request
var formatter = _httpSelfHostConfiguration.Formatters.JsonFormatter;
if (name.Equals("test", StringComparison.InvariantCultureIgnoreCase))
{
return formatter.ReadFromStreamAsync(typeof(SomeType), readStream, content, formatterLogger);
}
else
{
return formatter.ReadFromStreamAsync(typeof(OtherType), readStream, content, formatterLogger);
}
}
Here is one way you can do this. Have a message handler read the request and add a content header like this.
public class TypeDecidingHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
// Inspect the request here and determine the type to be used
request.Content.Headers.Add("X-Type", "SomeType");
return await base.SendAsync(request, cancellationToken);
}
}
Then, you can read this header from the formatter inside ReadFromStreamAsync.
public override Task<object> ReadFromStreamAsync(
Type type, Stream readStream,
HttpContent content,
IFormatterLogger formatterLogger)
{
string typeName = content.Headers.GetValues("X-Type").First();
// rest of the code based on typeName
}