I am working on a (self-hosted) WebApi application using Visual Studio 2012 targetting .Net 4+ and with an eye on moving to VS2013 as soon as possible to take advantage of MVC 5 and WebApi 2.0.
I need to prefix all outgoing Uri's with a string that is sent as a query parameter on the incoming request. The challenge is to do this without any specific code in controllers, models or views/viewmodels. And if at all possible, I would also like to stay away from using an action filter that would need to use reflection/recursion to work through all properties of the outgoing response, though it would be fine to use a result action filter to make the url prefix available to the serializer.
What I have come with so far is a DelegatingHandler to get the url prefix to use from the request's query parameters so it can be added as a property to some request/controller context object; and with a JsonConverter to add the desired prefix to all Uri type properties in responses.
What I am left with is getting the Url prefix specified in the request's parameters to the convertor. The serializer that is passed to the JsonConvertor does have a Context property, but I can't see if and how that is related to the request context.
I can think of a number of approaches to solve this, but keep run into "lack of knowledge" walls on how MVC/WebApi carries request (context) information around the pipeline.
Delegating Handler:
class UrlPrefixHandler : DelegatingHandler
{
private string GetUrlPrefixValue(HttpRequestMessage request)
{
var queryStrings = request.GetQueryNameValuePairs();
if (queryStrings == null)
return null;
var match = queryStrings.FirstOrDefault(kv => string.Compare(kv.Key, "url_prefix", true) == 0);
if (string.IsNullOrEmpty(match.Value))
return null;
return match.Value;
}
async protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
string urlPrefix = GetUrlPrefixValue(request);
// TODO : How do I get this to the serializer on a per request basis?
// and do this without placing requirements on controllers/models/views?
HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
return response;
}
}
Json Converter:
class UriPrefixConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(Uri).IsAssignableFrom(objectType);
}
public override bool CanRead
{
get { return false; }
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException("Unnecessary because CanRead is false. The type will skip the converter.");
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
JObject result = new JObject();
//serializer.
Uri u = (Uri)value as Uri;
// TODO : Getting the prefix from the request's context somehow
Uri prefixed = new Uri("/uriPrefix" + u.OriginalString, UriKind.Relative);
writer.WriteValue(prefixed.OriginalString);
}
Related
I am looking for a way to catch an exception thrown from a custome Newtonsoft's JsonConverter.
I created the following custom converter. JsonConverter attribute in Config class uses it. Config class is used to post a config object and used for Web API POST method (I'm using .NET Core 3.1).
The converter works fine but when an exception is thrown, the middleware that handles exceptions does not catch it. For instance, I expected that the middleware would catch MissingConfigTypeException when type is null in the HTTP request body, but the Func in appBuilder.Run() never gets called. Any exceptions thrown from the converter are never caught by the middleware.
Because the exception is not processed by the middleware, the API method returns http status code 500 with no HTTP response body. I want to return 400 with my custom error message.
My goals are (I need to achieve both):
Return http 400 error instead of 500 and
Return my custom error in HTTP response body (Error object. See the middleware below)
I wonder if there is a way to catch an exception somehow (using the middleware or otherwise) or modify HTTP response body (I'll have to be able to identify that a particular error occurred so I can modify the response body only when the error occurs)
Note: I don't want to use ModelState in my controller action method (don't want to add some sort of error checking code for each method).
Update
The middleware can catch exceptions thrown from controller action methods.
My custom converter:
public class ConfigJsonConverter : JsonConverter
{
public override object ReadJson(
JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
...
var jObject = JObject.Load(reader);
if (jObject == null) throw new InvalidConfigException();
var type = jObject["type"] ?? jObject["Type"];
if (type == null) throw new MissingConfigTypeException();
var target = CreateConfig(jObject);
serializer.Populate(jObject.CreateReader(), target);
return target;
}
private static Config CreateConfig(JObject jObject)
{
var type = (string)jObject.GetValue("type", StringComparison.OrdinalIgnoreCase);
if (Enum.TryParse<ConfigType>(type, true, out var configType))
{
switch (configType)
{
case ConfigType.TypeA:
return new ConfigA();
case ConfigType.TypeB:
return new ConfigB();
}
}
throw new UnsupportedConfigTypeException(type, jObject);
}
Config class:
[JsonConverter(typeof(ConfigJsonConverter))]
public abstract class Config {...}
public class ConfigA : Config {...}
The middleware:
// This is called in startup.cs
public static IApplicationBuilder UseCustomExceptionHandler(this IApplicationBuilder application)
{
return application.UseExceptionHandler(appBuilder => appBuilder.Run(async context =>
{
var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
var exception = exceptionHandlerPathFeature.Error;
Error error;
switch (exception)
{
case InvalidConfigException typedException:
error = new Error
{
Code = StatusCodes.Status400BadRequest,
Message = typedException.Message
};
break;
case MissingConfigTypeException typedException:
error = new Error
{
Code = StatusCodes.Status400BadRequest,
Message = typedException.Message
};
break;
.....
}
var result = JsonConvert.SerializeObject(error);
context.Response.ContentType = "application/json";
context.Response.StatusCode = error.Code;
await context.Response.WriteAsync(result);
}));
}
Update:
Startup.cs
public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger)
{
if (EnableHttps)
app.UseHsts();
...
app
.UseForwardedHeaders()
.UsePathBase(appConfig.BasePath);
if (EnableHttps)
app.UseHttpsRedirection();
app
.UseRouting()
.UseEndpoints(endpoints =>
{
endpoints.MapHealthChecks("/health");
endpoints.MapControllers();
})
.UseCustomExceptionHandler(logger);
Try adding your UseCustomExceptionHandler before setting up endpoints and routing:
app
.UseCustomExceptionHandler(logger)
.UseRouting()
.UseEndpoints(endpoints =>
{
endpoints.MapHealthChecks("/health");
endpoints.MapControllers();
});
Also based on docs exception handling usually is set up one of the first in pipeline, even before app.UseHsts().
I have a model object that I send to the browser and gets sent back to me. I want that ID value in that object to be encrypted. I created a custom JsonConverter to encrypt the string and then decrypt it.
public class SecretItem
{
[JsonConverter(typeof(EncryptedIdConverter))]
public string Id { get; set; }
public string Name { get; set; }
}
This is my EncryptedIdConverter class
class EncryptedIdConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
string encryptedValue = (string)value;
if (!string.IsNullOrWhiteSpace(encryptedValue))
encryptedValue = Encryption.EncryptString(encryptedValue);
serializer.Serialize(writer, encryptedValue);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
string decryptedString = (string)reader.Value;
if (!string.IsNullOrWhiteSpace(decryptedString))
decryptedString = Encryption.DecryptString(decryptedString);
return decryptedString;
}
public override bool CanConvert(Type objectType)
{
return typeof(string).IsAssignableFrom(objectType);
}
}
If I try calling the JsonConvert.Serialization functions, everything works correctly.
JsonConvert.SerializeObject(secretItem)
JsonConvert.DeserializeObject<SecretItem>([JSON secretItem]);
When I return the HttpActionResult Ok(secretItem)... the browser also gets the encrypted Id string.
However, when I POST the data back to my controller, my webapi method is not getting a decrypted property. It skips the JsonConverter.
public async Task<IHttpActionResult> Post(SecretItem secretItem)
{
// Not decrypted
var decryptedId = secretItem.Id;
}
Why would the deserialize logic not be working the same as the serialize logic in my WebAPI? I don't even know where to start debugging that.
We are using Newtonsoft.Json 10.0.0.0, MVC5, .NET Framework 4.6.1.
It turns out the code is working correctly. The problem was that on the POST that was being tested, the content-type wasn't set to "application/json". So, it didn't use the JsonNetFormatter and therefore skipped the converter.
Once I set the contentType, everything works!
How is your Global.asax
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
GlobalConfiguration.Configure(WebApiConfig.Register);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
it should work
maybe you need a TypeConverter
Model binding
When Web API calls a method on a controller, it must set values for the parameters, a process called binding
This is called Model binding
Post(SecretItem secretItem)
Model binding use a TypeConverter
JSON Serialization
This is called JSON Serialization
HttpActionResult Ok(secretItem)
JSON Serialization use a JsonConverter
Documentation
Parameter Binding in ASP.NET Web API | Microsoft Docs
JSON and XML Serialization in ASP.NET Web API | Microsoft Docs
more
asp.net mvc - C# WebAPI: Set default JSON serializer to NewtonSoft JSON - Stack Overflow
c# - Setting the default JSON serializer in ASP.NET MVC - Stack Overflow
c# - How to use Json.NET for JSON modelbinding in an MVC5 project? - Stack Overflow
Based on some value in the request (header or in the url) I want to change the serialization of my DTO objects.
Why? Well I've applied the [JsonProperty("A")] to my DTO's but depending on the client (website or mobile app) it want to use that property or not.
I started with
services
.AddMvc()
.AddJsonOptions(opt =>
{
#if DEBUG
opt.SerializerSettings.ContractResolver = new NoJsonPropertyNameContractResolver();
#endif
}
So while debugging I get JSON with full propertynames. I use the JsonProperty attribute to shorten the response JSON, which works fine with the mobile app (Xamarin) which deserialize back to the same DTO's.
But now I have a website which uses the the same API to get data via jQuery, but in there I want to deal with the full property names of the DTO's, not the name given in the JsonProperty attribute.
Website and WebApi are on the same server so it's no problem if the response is a little bigger.
I started with a middleware class to react on a customer header value, which works, but now I don't know how to get to the JSON SerializerSettings. Searched the web but cannot find it.
While searching I've read about InputFormatters and OutputFormatters, and also content negotiation, but I don't know which direction I must go.
I don't want to deploy the same API twice with different settings.
I'am able to change things like the routesconfig if that would help.
Update
Not only the JSON response had to be serialized in 2 different ways, also the deserializing had to be done in 2 different ways.
Here are two options:
1. Manual formatting
Options you set by services.AddMvc().AddJsonOptions() are registered in DI and you can inject it into your controllers and services:
public HomeController(IOptions<MvcJsonOptions> optionsAccessor)
{
JsonSerializerSettings jsonSettings = optionsAccessor.Value.SerializerSettings;
}
To per-request override these serialization settings, you could use Json method or create JsonResult instance:
public IActionResult Get()
{
return Json(data, new JsonSerializerSettings());
return new JsonResult(data, new JsonSerializerSettings());
}
2. Result filter to replace JSON output
public class ModifyResultFilter : IAsyncResultFilter
{
public ModifyResultFilter(IOptions<MvcJsonOptions> optionsAccessor)
{
_globalSettings = optionsAccessor.Value.SerializerSettings;
}
public async Task OnResultExecutionAsync(
ResultExecutingContext context,
ResultExecutionDelegate next)
{
var originResult = context.Result as JsonResult;
context.Result = new JsonResult(originResult.Value, customSettings);
await next();
}
}
Use it on action/controller:
[ServiceFilter(typeof(ModifyResultFilter ))]
public IActionResult Index() {}
Or create a custom attribute as described in documentation:
[ModifyResultAttribute]
public IActionResult Index() {}
Don't forget to register the filter in DI.
Thanks for the comments and answers. I found a solution with Input and outputformatters. With thanks to http://rovani.net/Explicit-Model-Constructor/ to point me in the right direction.
I've created my own input and outputformatters, which inherit from JsonInputFormatter to keep as much functionality the same.
In the constructor I set the supported mediatype (used some that looks like the existing one for JSON).
Also must override CreateJsonSerializer to set the ContractResolver to the desired one (could implement singleton).
Must do it this way, because changing the serializerSettings in the constructor would change the serializersettings for all input/outputformatters, meaning the default JSON formatters will also use the new contract resolver.
Also doing it this way means you can setup some default JSON options via AddMvc().AddJsonOption()
Example inputformatter, outputformatter uses the same principle:
static MediaTypeHeaderValue protoMediaType = MediaTypeHeaderValue.Parse("application/jsonfull");
public JsonFullInputFormatter(ILogger logger, JsonSerializerSettings serializerSettings, ArrayPool<char> charPool, ObjectPoolProvider objectPoolProvider)
: base(logger, serializerSettings, charPool, objectPoolProvider)
{
this.SupportedMediaTypes.Clear();
this.SupportedMediaTypes.Add(protoMediaType);
}
protected override JsonSerializer CreateJsonSerializer()
{
var serializer = base.CreateJsonSerializer();
serializer.ContractResolver = new NoJsonPropertyNameContractResolver();
return serializer;
}
As per the mentioned URL above the setup class:
public class YourMvcOptionsSetup : IConfigureOptions<MvcOptions>
{
private readonly ILoggerFactory _loggerFactory;
private readonly JsonSerializerSettings _jsonSerializerSettings;
private readonly ArrayPool<char> _charPool;
private readonly ObjectPoolProvider _objectPoolProvider;
public YourMvcOptionsSetup(ILoggerFactory loggerFactory, IOptions<MvcJsonOptions> jsonOptions, ArrayPool<char> charPool, ObjectPoolProvider objectPoolProvider)
{
//Validate parameters and set fields
}
public void Configure(MvcOptions options)
{
var jsonFullInputFormatter = new JsonFullInputFormatter(
_loggerFactory.CreateLogger<JsonFullInputFormatter>(),
_jsonSerializerSettings,
_charPool,
_objectPoolProvider
);
options.InputFormatters.Add(jsonFullInputFormatter);
options.OutputFormatters.Add(new JsonFullOutputFormatter(
_jsonSerializerSettings,
_charPool
));
}
And then an extension method to register it:
public static class MvcBuilderExtensions
{
public static IMvcBuilder AddJsonFullFormatters(this IMvcBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
ServiceDescriptor descriptor = ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, YourMvcOptionsSetup>();
builder.Services.TryAddEnumerable(descriptor);
return builder;
}
}
Call it in ConfigureServices:
services.AddMvc(config =>
{
config.RespectBrowserAcceptHeader = true; // To use the JsonFullFormatters if clients asks about it via Accept Header
})
.AddJsonFullFormatters() //Add our own JSON Formatters
.AddJsonOptions(opt =>
{
//Set up some default options all JSON formatters must use (if any)
});
Now our Xamarin App can access the webapi and receive JSON with (short) property names set via JsonProperty attribute.
And in the website we can get the full JSON property names by adding an Accept (get calls) and ContentType (post/put calls) header. Which we do once via jQuery's $.ajaxSetup(.
$.ajaxSetup({
contentType: "application/jsonfull; charset=utf-8",
headers: { 'Accept': 'application/jsonfull' }
});
All,
I am trying to modify the payload of incoming object via the web API. Currently I'm using a custom formatter which inherits from JsonMediaTypeFormatter and overrides the relevant methods.
Looks like this:
public override async Task<object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger,
CancellationToken cancellationToken)
{
object obj = await base.ReadFromStreamAsync(type, readStream, content, formatterLogger, cancellationToken);
TrySetEventNo(obj, GetEventNo());
return obj;
}
private void TrySetEventNo(object content, long eventNo)
{
if (content is EventModelBase)
{
EventModelBase eventBase = (EventModelBase)content;
eventBase.EventNo = eventNo;
}
}
I'm using this to track every object that comes through the API.
Before all of this happens, I have a MessageHandler which is creating an event number and adding it to Request.Properties.
Trying to get the event number in the formatter which was created previously in the MessageHandler is proving difficult. Access HttpContext.Current.Items["MS_HttpRequestMessage"].Properties seems to be a different request as it does not contain the event number.
I've two questions:
Am I doing this the correctly or is there a better way?
If I am taking the correct approach, how to I get the correct Request to extract the event number?
Thanks
I've found a solution, instead of attempting to do this inside a formatter I'm now using an ActionFilterAttribute.
overriding OnActionExecuting(HttpActionContext actionContext) and enumerating action actionContext.ActionArguments.
The complete solution looks like so:
public class SetModelEventNumberActionFilter : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
foreach (KeyValuePair<string, object> actionArgument in actionContext.ActionArguments)
{
TrySetEventNo(actionArgument.Value, GetEventNo(actionContext));
}
base.OnActionExecuting(actionContext);
}
private void TrySetEventNo(object content, long eventNo)
{
if (content is EventPivotRequestMessageBase)
{
EventPivotRequestMessageBase eventBase = (EventPivotRequestMessageBase)content;
eventBase.EventNo = eventNo;
}
}
private long GetEventNo(HttpActionContext actionContext)
{
long eventNo = (long)actionContext.Request.Properties[Constant.EVENT_ID];
return eventNo;
}
}
I am writing a media type formatter for HTML to automatically generate a Razor view based on an html request from the user. I am doing this for use inside a SelfHosted service. I need to detect what controller/action was requested to allow me to pick the view to render into.
public class RazorHtmlMediaTypeFormatter : MediaTypeFormatter
{
public RazorHtmlMediaTypeFormatter()
{
SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html"));
}
public override bool CanWriteType(Type type)
{
return true;
}
public override bool CanReadType(Type type)
{
return false;
}
public override Task WriteToStreamAsync(Type type, object value, Stream stream, HttpContentHeaders contentHeaders, System.Net.TransportContext transportContext)
{
return Task.Factory.StartNew(() =>
{
var view = Razor.Resolve(String.Format("{0}.{1}.cshtml", something.Controller, something.Action), value);
byte[] buf = System.Text.Encoding.Default.GetBytes(view.Run(new ExecuteContext()));
stream.Write(buf, 0, buf.Length);
stream.Flush();
});
}
}
Why not wrapping your returned objects in Metadata<T>?
I.e. return, instead of MyCustomObject, Metadata<MyCustomObject>. As Metadata properties, you can set controller name and action. Then in the formatter, just decouple the Metadata and your custom object, and serialize just that custom object.
I blogged about this approach here - http://www.strathweb.com/2012/06/extending-your-asp-net-web-api-responses-with-useful-metadata/. While the purpose of the article is a bit different, I am sure you can relate it to your needs.
Edit: or if you are OK with a small hack, use a custom filter and headers:
public override void OnActionExecuting(HttpActionContext actionContext)
{
actionContext.Response.Headers.Add("controller", actionContext.ActionDescriptor.ControllerDescriptor.ControllerName);
actionContext.Response.Headers.Add("action", actionContext.ActionDescriptor.ActionName;);
base.OnActionExecuting(actionContext);
}
then just read it from the headers in the formatter, and remove the header entries so that they don't get sent to the client.
Web API Contrib has a working RazorViewFormatter in here.