I want to add global JsonSerializer options to use ReferenceHandler.Preserve, i can't configure my blazor server App to use it as a global setting for all json Serializers.
i used
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.ReferenceHandler = ReferenceHandler.Preserve;
options.SerializerOptions.PropertyNameCaseInsensitive = true;
});
builder.Services.AddRazorPages().AddJsonOptions(options =>
{
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve;
options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
});
builder.Services.Configure<JsonOptions>(o =>
{
o.SerializerOptions.ReferenceHandler = ReferenceHandler.Preserve;
o.SerializerOptions.PropertyNameCaseInsensitive = true;
});
none of them works as expected the options doesn't change from the defaults and i keep getting the same exception: "The JSON value could not be converted to"
using the same options at each request works
var options = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve, PropertyNameCaseInsensitive = true };
var httpClient = _httpFactory.CreateClient("API");
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _tokenProvider.JwtToken);
var result = await httpClient.GetFromJsonAsync<List<Manufacturer>>("manufacturer", options);
but i want to define the options for all requests without explicitly writing them each time.
Instead of Named Client you could use Typed Client (or even generated with NSwag or Refit) and handle JSON formatting options inside this typed API client.
E.g. NSwag API clients generator has an option to generate UpdateJsonSerializerSettings method which you can define in the base class for type dAPI client like:
internal class BaseClient
{
protected static void UpdateJsonSerializerSettings(JsonSerializerOptions settings) => settings.ConfigureDefaults();
}
// generated API client:
[System.CodeDom.Compiler.GeneratedCode("NSwag", "13.16.1.0 (NJsonSchema v10.7.2.0 (Newtonsoft.Json v13.0.0.0))")]
internal partial class SomeClient : BaseClient, ISomeClient
{
private System.Net.Http.HttpClient _httpClient;
private System.Lazy<System.Text.Json.JsonSerializerOptions> _settings;
public ActionsClient(System.Net.Http.HttpClient httpClient)
{
_httpClient = httpClient;
_settings = new System.Lazy<System.Text.Json.JsonSerializerOptions>(CreateSerializerSettings);
}
private System.Text.Json.JsonSerializerOptions CreateSerializerSettings()
{
var settings = new System.Text.Json.JsonSerializerOptions();
UpdateJsonSerializerSettings(settings);
return settings;
}
... the rest of generated code has omitted
}
// Extension which configures the default JSON settings (as an example):
public static class JsonExtensions
{
private static JsonSerializerOptions GetDefaultOptions() => new() { WriteIndented = true };
public static JsonSerializerOptions ConfigureDefaults(this JsonSerializerOptions? settings)
{
settings ??= GetDefaultOptions();
settings.PropertyNameCaseInsensitive = true;
settings.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
settings.NumberHandling = JsonNumberHandling.AllowReadingFromString;
settings.Converters.Add(new JsonStringEnumConverter());
return settings;
}
}
Then you register on DI your ISomeClient typed client like this:
builder.Services
.AddScoped<MyLovelyAuthorizationMessageHandler>()
.AddHttpClient<ISomeClient, SomeClient>(httpClient =>
{
httpClient.BaseAddress = new Uri(apiSettings.WebApiBaseAddress);
// ...etc.
}).AddHttpMessageHandler<MyLovelyAuthorizationMessageHandler>();
And then - inject ISomeClient where you need it, and call methods with typed DTOs keeping all JSON serialization/deserialization magic under the carpet.
[Inject] private ISomeClient Client {get; set;} = default!;
Documentation: about Typed Clients
Related
I am trying to write a Blazor app that uses client secret credentials to get an access token for the API. I wanted to encapsulate it in such a way that it handles the token fetching and refreshing behind the scenes. To achieve this, I created the following inherited class which uses IdentityModel Nuget package:
public class MPSHttpClient : HttpClient
{
private readonly IConfiguration Configuration;
private readonly TokenProvider Tokens;
private readonly ILogger Logger;
public MPSHttpClient(IConfiguration configuration, TokenProvider tokens, ILogger logger)
{
Configuration = configuration;
Tokens = tokens;
Logger = logger;
}
public async Task<bool> RefreshTokens()
{
if (Tokens.RefreshToken == null)
return false;
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync(Configuration["Settings:Authority"]);
if (disco.IsError) throw new Exception(disco.Error);
var result = await client.RequestRefreshTokenAsync(new RefreshTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = Configuration["Settings:ClientID"],
RefreshToken = Tokens.RefreshToken
});
Logger.LogInformation("Refresh Token Result {0}", result.IsError);
if (result.IsError)
{
Logger.LogError("Error: {0)", result.ErrorDescription);
return false;
}
Tokens.RefreshToken = result.RefreshToken;
Tokens.AccessToken = result.AccessToken;
Logger.LogInformation("Access Token: {0}", result.AccessToken);
Logger.LogInformation("Refresh Token: {0}" , result.RefreshToken);
return true;
}
public async Task<bool> CheckTokens()
{
if (await RefreshTokens())
return true;
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync(Configuration["Settings:Authority"]);
if (disco.IsError) throw new Exception(disco.Error);
var result = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = Configuration["Settings:ClientID"],
ClientSecret = Configuration["Settings:ClientSecret"]
});
if (result.IsError)
{
//Log("Error: " + result.Error);
return false;
}
Tokens.AccessToken = result.AccessToken;
Tokens.RefreshToken = result.RefreshToken;
return true;
}
public new async Task<HttpResponseMessage> GetAsync(string requestUri)
{
DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", Tokens.AccessToken);
var response = await base.GetAsync(requestUri);
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
if (await CheckTokens())
{
DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", Tokens.AccessToken);
response = await base.GetAsync(requestUri);
}
}
return response;
}
}
The idea is to keep from having to write a bunch of redundant code to try the API, then request/refresh the token if you are unauthorized. I tried it at first using extension methods to HttpClient, but there was no good way to inject the Configuration into a static class.
So my Service code is written as this:
public interface IEngineListService
{
Task<IEnumerable<EngineList>> GetEngineList();
}
public class EngineListService : IEngineListService
{
private readonly MPSHttpClient _httpClient;
public EngineListService(MPSHttpClient httpClient)
{
_httpClient = httpClient;
}
async Task<IEnumerable<EngineList>> IEngineListService.GetEngineList()
{
return await JsonSerializer.DeserializeAsync<IEnumerable<EngineList>>
(await _httpClient.GetStreamAsync($"api/EngineLists"), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
}
}
Everything compiles great. In my Startup, I have the following code:
services.AddScoped<TokenProvider>();
services.AddHttpClient<IEngineListService, EngineListService>(client =>
{
client.BaseAddress = new Uri(Configuration["Settings:ApiAddress"]);
});
Just to be complete, Token Provider looks like this:
public class TokenProvider
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
}
When I run the App, it complains that it can't find a suitable constructor for EngineListService in the call to services.AddHttpClient. Is there a way to pass AddHttpClient an actual instance of the IEngineListService. Any other way I might be able to achieve this?
Thanks,
Jim
I think that EngineListService should not be registered as a HttpClient in services and instead you should register MPSHttpClient.
This follows the "Typed Client" example in the documentation and uses IHttpClientFactory behind the scenes.
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-requests#typed-clients
When you use services.AddHttpClient the constructor needs a HttpClient parameter. That is how the HttpClientFactory initializes the HttpClient and then passes it into your service ready to go.
You can change your MPSHttpClient to not inherit HttpClient and instead add a HttpClient parameter to the constructor. You could also have it implement an interface like IMPSHttpClient
public class MPSHttpClient
{
public MPSHttpClient(HttpClient httpClient, IConfiguration configuration, TokenProvider tokens, ILogger logger)
{
HttpClient = httpClient;
Configuration = configuration;
Tokens = tokens;
Logger = logger;
}
}
You must remove these lines from MPSHttpClient and use the injected client.
// remove this
var client = new HttpClient();
In Startup add
services.AddHttpClient<MPSHttpClient>(client =>
{
// add any configuration
client.BaseAddress = new Uri(Configuration["Settings:ApiAddress"]);
});
Change EngineListService to a normal service registration as it is not a HttpClient
services.AddScoped<IEngineListService, EngineListService>()
Special thanks to #pinkfloydx33 for helping me solve this. This link that he shared https://blog.joaograssi.com/typed-httpclient-with-messagehandler-getting-accesstokens-from-identityserver/ was everything I needed. The trick was that there exists a class called DelegatingHandler that you can inherit and override the OnSendAsync method and do all of your token-checking there before sending it to the final HttpHandler. So my new MPSHttpClient class is as so:
public class MPSHttpClient : DelegatingHandler
{
private readonly IConfiguration Configuration;
private readonly TokenProvider Tokens;
private readonly ILogger<MPSHttpClient> Logger;
private readonly HttpClient client;
public MPSHttpClient(HttpClient httpClient, IConfiguration configuration, TokenProvider tokens, ILogger<MPSHttpClient> logger)
{
Configuration = configuration;
Tokens = tokens;
Logger = logger;
client = httpClient;
}
public async Task<bool> CheckTokens()
{
var disco = await client.GetDiscoveryDocumentAsync(Configuration["Settings:Authority"]);
if (disco.IsError) throw new Exception(disco.Error);
var result = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = Configuration["Settings:ClientID"],
ClientSecret = Configuration["Settings:ClientSecret"]
});
if (result.IsError)
{
//Log("Error: " + result.Error);
return false;
}
Tokens.AccessToken = result.AccessToken;
Tokens.RefreshToken = result.RefreshToken;
return true;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.SetBearerToken(Tokens.AccessToken);
var response = await base.SendAsync(request, cancellationToken);
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
if (await CheckTokens())
{
request.SetBearerToken(Tokens.AccessToken);
response = await base.SendAsync(request, cancellationToken);
}
}
return response;
}
}
The big changes here are the inheritance and I used DI to obtain the HttpClient much like #Rosco mentioned. I had tried to override OnGetAsync in my original version. When inheriting from DelegatingHandler, all you have to override is OnSendAsync. This will handle all of your get, put, post, and deletes from your HttpContext all in one method.
My EngineList Service is written as if there were no tokens to be considered, which was my original goal:
public interface IEngineListService
{
Task<IEnumerable<EngineList>> GetEngineList();
}
public class EngineListService : IEngineListService
{
private readonly HttpClient _httpClient;
public EngineListService(HttpClient httpClient)
{
_httpClient = httpClient;
}
async Task<IEnumerable<EngineList>> IEngineListService.GetEngineList()
{
return await JsonSerializer.DeserializeAsync<IEnumerable<EngineList>>
(await _httpClient.GetStreamAsync($"api/EngineLists"), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
}
}
The Token Provider stayed the same. I plan to add expirations and such to it, but it works as is:
public class TokenProvider
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
}
The ConfigureServices code changed just a bit:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddScoped<TokenProvider>();
services.AddTransient<MPSHttpClient>();
services.AddHttpClient<IEngineListService, EngineListService>(client =>
{
client.BaseAddress = new Uri(Configuration["Settings:ApiAddress"]);
}).AddHttpMessageHandler<MPSHttpClient>();
...
}
You instantiate MPSHttpClient as Transient, then reference it with the AddHttpMessageHandler call attached to the AddHttpClient call. I know this is different than how others implement HttpClients, but I learned this method of creating client services from a Pluralsight video and have been using it for everything. I create a separate Service for each entity in the database. If say I wanted to do tires, I would add the following to ConfigureServices:
services.AddHttpClient<ITireListService, TireListService>(client =>
{
client.BaseAddress = new Uri(Configuration["Settings:ApiAddress"]);
}).AddHttpMessageHandler<MPSHttpClient>();
It will use the same DelegatingHandler so I can just keep adding services for each entity type while no longer worrying about tokens. Thanks to everyone that responded.
Thanks,
Jim
I have a controller that I use for a third party API, which uses a snake case naming convention. The rest of my controllers are used for my own app, which uses a camelcase JSON convention. I'd like to automatically deserialize and serialize my models from/to snake case for the API in that one controller. This question explains how to use a snake case naming policy for JSON in the entire app, but is there a way that I can specify to use the naming policy only for that single controller?
I've seen Change the JSON serialization settings of a single ASP.NET Core controller which suggests using an ActionFilter, but that only helps for ensuring that outgoing JSON is serialized properly. How can I get the incoming JSON deserialized to my models properly as well? I know that I can use [JsonPropertyName] on the model property names but I'd prefer to be able to set something at the controller level, not at the model level.
The solution on the shared link in your question is OK for serialization (implemented by IOutputFormatter) although we may have another approach by extending it via other extensibility points.
Here I would like to focus on the missing direction (the deserializing direction which is implemented by IInputFormatter). You can implement a custom IModelBinder but it requires you to reimplement the BodyModelBinder and BodyModelBinderProvider which is not easy. Unless you accept to clone all the source code of them and modify the way you want. That's not very friendly to maintainability and getting up-to-date to what changed by the framework.
After researching through the source code, I've found that it's not easy to find a point where you can customize the deserializing behavior based on different controllers (or actions). Basically the default implementation uses a one-time init IInputFormatter for json (default by JsonInputFormatter for asp.net core < 3.0). That in chain will share one instance of JsonSerializerSettings. In your scenario, actually you need multiple instances of that settings (for each controller or action). The easiest point I think is to customize an IInputFormatter (extending the default JsonInputFormatter). It becomes more complicated when the default implementation uses ObjectPool for the instance of JsonSerializer (which is associated with a JsonSerializerSettings). To follow that style of pooling the objects (for better performance), you need a list of object pools (we will use a dictionary here) instead of just one object pool for the shared JsonSerializer as well as the associated JsonSerializerSettings (as implemented by the default JsonInputFormatter).
The point here is to based on the current InputFormatterContext, you need to build the corresponding JsonSerializerSettings as well as the JsonSerializer to be used. That sounds simple but once it comes to a full implementation (with fairly complete design), the code is not short at all. I've designed it into multiple classes. If you really want to see it working, just be patient to copy the code carefully (of course reading it through to understand is recommended). Here's all the code:
public abstract class ContextAwareSerializerJsonInputFormatter : JsonInputFormatter
{
public ContextAwareSerializerJsonInputFormatter(ILogger logger,
JsonSerializerSettings serializerSettings,
ArrayPool<char> charPool, ObjectPoolProvider objectPoolProvider, MvcOptions options, MvcJsonOptions jsonOptions) : base(logger, serializerSettings, charPool, objectPoolProvider, options, jsonOptions)
{
PoolProvider = objectPoolProvider;
}
readonly AsyncLocal<InputFormatterContext> _currentContextAsyncLocal = new AsyncLocal<InputFormatterContext>();
readonly AsyncLocal<ActionContext> _currentActionAsyncLocal = new AsyncLocal<ActionContext>();
protected InputFormatterContext CurrentContext => _currentContextAsyncLocal.Value;
protected ActionContext CurrentAction => _currentActionAsyncLocal.Value;
protected ObjectPoolProvider PoolProvider { get; }
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
{
_currentContextAsyncLocal.Value = context;
_currentActionAsyncLocal.Value = context.HttpContext.RequestServices.GetRequiredService<IActionContextAccessor>().ActionContext;
return base.ReadRequestBodyAsync(context, encoding);
}
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
_currentContextAsyncLocal.Value = context;
_currentActionAsyncLocal.Value = context.HttpContext.RequestServices.GetRequiredService<IActionContextAccessor>().ActionContext;
return base.ReadRequestBodyAsync(context);
}
protected virtual JsonSerializer CreateJsonSerializer(InputFormatterContext context) => null;
protected override JsonSerializer CreateJsonSerializer()
{
var context = CurrentContext;
return (context == null ? null : CreateJsonSerializer(context)) ?? base.CreateJsonSerializer();
}
}
public abstract class ContextAwareMultiPooledSerializerJsonInputFormatter : ContextAwareSerializerJsonInputFormatter
{
public ContextAwareMultiPooledSerializerJsonInputFormatter(ILogger logger, JsonSerializerSettings serializerSettings, ArrayPool<char> charPool, ObjectPoolProvider objectPoolProvider, MvcOptions options, MvcJsonOptions jsonOptions)
: base(logger, serializerSettings, charPool, objectPoolProvider, options, jsonOptions)
{
}
readonly IDictionary<object, ObjectPool<JsonSerializer>> _serializerPools = new ConcurrentDictionary<object, ObjectPool<JsonSerializer>>();
readonly AsyncLocal<object> _currentPoolKeyAsyncLocal = new AsyncLocal<object>();
protected object CurrentPoolKey => _currentPoolKeyAsyncLocal.Value;
protected abstract object GetSerializerPoolKey(InputFormatterContext context);
protected override JsonSerializer CreateJsonSerializer(InputFormatterContext context)
{
object poolKey = GetSerializerPoolKey(context) ?? "";
if(!_serializerPools.TryGetValue(poolKey, out var pool))
{
//clone the settings
var serializerSettings = new JsonSerializerSettings();
foreach(var prop in typeof(JsonSerializerSettings).GetProperties().Where(e => e.CanWrite))
{
prop.SetValue(serializerSettings, prop.GetValue(SerializerSettings));
}
ConfigureSerializerSettings(serializerSettings, poolKey, context);
pool = PoolProvider.Create(new JsonSerializerPooledPolicy(serializerSettings));
_serializerPools[poolKey] = pool;
}
_currentPoolKeyAsyncLocal.Value = poolKey;
return pool.Get();
}
protected override void ReleaseJsonSerializer(JsonSerializer serializer)
{
if(_serializerPools.TryGetValue(CurrentPoolKey ?? "", out var pool))
{
pool.Return(serializer);
}
}
protected virtual void ConfigureSerializerSettings(JsonSerializerSettings serializerSettings, object poolKey, InputFormatterContext context) { }
}
//there is a similar class like this implemented by the framework
//but it's a pity that it's internal
//So we define our own class here (which is exactly the same from the source code)
//It's quite simple like this
public class JsonSerializerPooledPolicy : IPooledObjectPolicy<JsonSerializer>
{
private readonly JsonSerializerSettings _serializerSettings;
public JsonSerializerPooledPolicy(JsonSerializerSettings serializerSettings)
{
_serializerSettings = serializerSettings;
}
public JsonSerializer Create() => JsonSerializer.Create(_serializerSettings);
public bool Return(JsonSerializer serializer) => true;
}
public class ControllerBasedJsonInputFormatter : ContextAwareMultiPooledSerializerJsonInputFormatter,
IControllerBasedJsonSerializerSettingsBuilder
{
public ControllerBasedJsonInputFormatter(ILogger logger, JsonSerializerSettings serializerSettings, ArrayPool<char> charPool, ObjectPoolProvider objectPoolProvider, MvcOptions options, MvcJsonOptions jsonOptions) : base(logger, serializerSettings, charPool, objectPoolProvider, options, jsonOptions)
{
}
readonly IDictionary<object, Action<JsonSerializerSettings>> _configureSerializerSettings
= new Dictionary<object, Action<JsonSerializerSettings>>();
readonly HashSet<object> _beingAppliedConfigurationKeys = new HashSet<object>();
protected override object GetSerializerPoolKey(InputFormatterContext context)
{
var routeValues = context.HttpContext.GetRouteData()?.Values;
var controllerName = routeValues == null ? null : routeValues["controller"]?.ToString();
if(controllerName != null && _configureSerializerSettings.ContainsKey(controllerName))
{
return controllerName;
}
var actionContext = CurrentAction;
if (actionContext != null && actionContext.ActionDescriptor is ControllerActionDescriptor actionDesc)
{
foreach (var attr in actionDesc.MethodInfo.GetCustomAttributes(true)
.Concat(actionDesc.ControllerTypeInfo.GetCustomAttributes(true)))
{
var key = attr.GetType();
if (_configureSerializerSettings.ContainsKey(key))
{
return key;
}
}
}
return null;
}
public IControllerBasedJsonSerializerSettingsBuilder ForControllers(params string[] controllerNames)
{
foreach(var controllerName in controllerNames ?? Enumerable.Empty<string>())
{
_beingAppliedConfigurationKeys.Add((controllerName ?? "").ToLowerInvariant());
}
return this;
}
public IControllerBasedJsonSerializerSettingsBuilder ForControllersWithAttribute<T>()
{
_beingAppliedConfigurationKeys.Add(typeof(T));
return this;
}
public IControllerBasedJsonSerializerSettingsBuilder ForActionsWithAttribute<T>()
{
_beingAppliedConfigurationKeys.Add(typeof(T));
return this;
}
ControllerBasedJsonInputFormatter IControllerBasedJsonSerializerSettingsBuilder.WithSerializerSettingsConfigurer(Action<JsonSerializerSettings> configurer)
{
if (configurer == null) throw new ArgumentNullException(nameof(configurer));
foreach(var key in _beingAppliedConfigurationKeys)
{
_configureSerializerSettings[key] = configurer;
}
_beingAppliedConfigurationKeys.Clear();
return this;
}
protected override void ConfigureSerializerSettings(JsonSerializerSettings serializerSettings, object poolKey, InputFormatterContext context)
{
if (_configureSerializerSettings.TryGetValue(poolKey, out var configurer))
{
configurer.Invoke(serializerSettings);
}
}
}
public interface IControllerBasedJsonSerializerSettingsBuilder
{
ControllerBasedJsonInputFormatter WithSerializerSettingsConfigurer(Action<JsonSerializerSettings> configurer);
IControllerBasedJsonSerializerSettingsBuilder ForControllers(params string[] controllerNames);
IControllerBasedJsonSerializerSettingsBuilder ForControllersWithAttribute<T>();
IControllerBasedJsonSerializerSettingsBuilder ForActionsWithAttribute<T>();
}
To help conveniently configure the services to replace the default JsonInputFormatter, we have the following code:
public class ControllerBasedJsonInputFormatterMvcOptionsSetup : IConfigureOptions<MvcOptions>
{
private readonly ILoggerFactory _loggerFactory;
private readonly MvcJsonOptions _jsonOptions;
private readonly ArrayPool<char> _charPool;
private readonly ObjectPoolProvider _objectPoolProvider;
public ControllerBasedJsonInputFormatterMvcOptionsSetup(
ILoggerFactory loggerFactory,
IOptions<MvcJsonOptions> jsonOptions,
ArrayPool<char> charPool,
ObjectPoolProvider objectPoolProvider)
{
if (loggerFactory == null)
{
throw new ArgumentNullException(nameof(loggerFactory));
}
if (jsonOptions == null)
{
throw new ArgumentNullException(nameof(jsonOptions));
}
if (charPool == null)
{
throw new ArgumentNullException(nameof(charPool));
}
if (objectPoolProvider == null)
{
throw new ArgumentNullException(nameof(objectPoolProvider));
}
_loggerFactory = loggerFactory;
_jsonOptions = jsonOptions.Value;
_charPool = charPool;
_objectPoolProvider = objectPoolProvider;
}
public void Configure(MvcOptions options)
{
//remove the default
options.InputFormatters.RemoveType<JsonInputFormatter>();
//add our own
var jsonInputLogger = _loggerFactory.CreateLogger<ControllerBasedJsonInputFormatter>();
options.InputFormatters.Add(new ControllerBasedJsonInputFormatter(
jsonInputLogger,
_jsonOptions.SerializerSettings,
_charPool,
_objectPoolProvider,
options,
_jsonOptions));
}
}
public static class ControllerBasedJsonInputFormatterServiceCollectionExtensions
{
public static IServiceCollection AddControllerBasedJsonInputFormatter(this IServiceCollection services,
Action<ControllerBasedJsonInputFormatter> configureFormatter)
{
if(configureFormatter == null)
{
throw new ArgumentNullException(nameof(configureFormatter));
}
services.TryAddSingleton<IActionContextAccessor, ActionContextAccessor>();
return services.ConfigureOptions<ControllerBasedJsonInputFormatterMvcOptionsSetup>()
.PostConfigure<MvcOptions>(o => {
var jsonInputFormatter = o.InputFormatters.OfType<ControllerBasedJsonInputFormatter>().FirstOrDefault();
if(jsonInputFormatter != null)
{
configureFormatter(jsonInputFormatter);
}
});
}
}
//This attribute is used as a marker to decorate any controllers
//or actions that you want to apply your custom input formatter
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class UseSnakeCaseJsonInputFormatterAttribute : Attribute
{
}
Finally here's a sample configuration code:
//inside Startup.ConfigureServices
services.AddControllerBasedJsonInputFormatter(formatter => {
formatter.ForControllersWithAttribute<UseSnakeCaseJsonInputFormatterAttribute>()
.ForActionsWithAttribute<UseSnakeCaseJsonInputFormatterAttribute>()
.WithSerializerSettingsConfigurer(settings => {
var contractResolver = settings.ContractResolver as DefaultContractResolver ?? new DefaultContractResolver();
contractResolver.NamingStrategy = new SnakeCaseNamingStrategy();
settings.ContractResolver = contractResolver;
});
});
Now you can use the marker attribute UseSnakeCaseJsonInputFormatterAttribute on any controllers (or action methods) that you want to apply the snake-case json input formatter, like this:
[UseSnakeCaseJsonInputFormatter]
public class YourController : Controller {
//...
}
Note that the code above uses asp.net core 2.2, for asp.net core 3.0+, you can replace the JsonInputFormatter with NewtonsoftJsonInputFormatter and MvcJsonOptions with MvcNewtonsoftJsonOptions.
Warning: this doesn't work
I've been trying to create an attribute which would re-read the body of the request and return a bad result:
public class OnlyValidParametersAttribute : ActionFilterAttribute
{
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
if (context.HttpContext.Request.ContentType.Contains("application/json"))
{
context.HttpContext.Request.EnableBuffering();
StreamReader stream = new StreamReader(context.HttpContext.Request.Body);
string body = await stream.ReadToEndAsync(); // Always set as "".
try
{
JsonConvert.DeserializeObject(
body,
context.ActionDescriptor.Parameters.FirstOrDefault()!.ParameterType,
new JsonSerializerSettings
{
MissingMemberHandling = MissingMemberHandling.Error,
}
);
}
catch (MissingMemberException e) // Unsure if this is the right exception to catch.
{
context.Result = new UnprocessableEntityResult();
}
base.OnActionExecuting(context);
}
await next();
}
}
However, in my .NET Core 3.1 application, body is always an empty string.
I'd return Content with json serialised with desired settings:
var serializeOptions = new JsonSerializerOptions
{
...
};
return Content(JsonSerializer.Serialize(data, options), "application/json");
For multiple methods I'd create a helper method.
I have a .net core 3.1 application. I use the library json.net (newtonsoft) to serialize or deserialize the json . This is the app settings for newtonsoft :
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(options =>
{
options.SuppressAsyncSuffixInActionNames = false;
}).AddNewtonsoftJson(options =>
{
options.SerializerSettings.DateTimeZoneHandling = Newtonsoft.Json.DateTimeZoneHandling.Local;
options.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
options.SerializerSettings.Converters.Add(new GuidJsonConverter());
});
I've put this line to ignore null json value on deserialization :
options.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
But I remark that it ignores also null value for serialization (when use Json method of the class Microsoft.AspNetCore.Mvc.Controller), but I don't want this behavior.
Is there a way to specify differents value of NullValueHandling for serialization and for deserialization ?
Finally I opt for this solution:
I made a BaseController class which inherits from Microsoft.AspNetCore.Mvc.Controller. I have inherited each of my controllers from this BaseController class.
In this class, I override the Microsoft.AspNetCore.Mvc.Controller.Json method :
public class BaseController : Controller
{
private readonly JsonSerializerSettings _jsonSerializerSettings;
public BaseController(IServiceProvider services)
{
IOptions<MvcNewtonsoftJsonOptions> newtonsoftOptions = services.GetService<IOptions<MvcNewtonsoftJsonOptions>>();
_jsonSerializerSettings = newtonsoftOptions.Value.SerializerSettings;
_jsonSerializerSettings.NullValueHandling = NullValueHandling.Include;
}
public override JsonResult Json(object data)
{
return Json(data, _jsonSerializerSettings);
}
Thanks to IOptions<MvcNewtonsoftJsonOptions> I was able to recover the serializer settings initialized in the startup.
EDIT
I remarks that the change of value _jsonSerializerSettings.NullValueHandling = NullValueHandling.Include; change also the init serializer settings.
So I've made an extensions method to copy all data of serializer settings in aim to update just the new settings :
public CustomerAccountController(IServiceProvider services)
{
IOptions<MvcNewtonsoftJsonOptions> newtonsoftOptions = services.GetService<IOptions<MvcNewtonsoftJsonOptions>>();
_jsonSerializerSettings = newtonsoftOptions.Value.SerializerSettings.CloneJsonSerializerSettings();
_jsonSerializerSettings.NullValueHandling = NullValueHandling.Include;
}
public static JsonSerializerSettings CloneJsonSerializerSettings(this JsonSerializerSettings settings)
{
JsonSerializerSettings cloneSettings = new JsonSerializerSettings();
cloneSettings.StringEscapeHandling = settings.StringEscapeHandling;
cloneSettings.FloatParseHandling = settings.FloatParseHandling;
cloneSettings.FloatFormatHandling = settings.FloatFormatHandling;
cloneSettings.DateParseHandling = settings.DateParseHandling;
cloneSettings.DateTimeZoneHandling = settings.DateTimeZoneHandling;
cloneSettings.DateFormatHandling = settings.DateFormatHandling;
cloneSettings.Formatting = settings.Formatting;
cloneSettings.MaxDepth = settings.MaxDepth;
cloneSettings.DateFormatString = settings.DateFormatString;
cloneSettings.Context = settings.Context;
cloneSettings.Error = settings.Error;
cloneSettings.SerializationBinder = settings.SerializationBinder;
cloneSettings.Binder = settings.Binder;
cloneSettings.TraceWriter = settings.TraceWriter;
cloneSettings.Culture = settings.Culture;
cloneSettings.ReferenceResolverProvider = settings.ReferenceResolverProvider;
cloneSettings.EqualityComparer = settings.EqualityComparer;
cloneSettings.ContractResolver = settings.ContractResolver;
cloneSettings.ConstructorHandling = settings.ConstructorHandling;
cloneSettings.TypeNameAssemblyFormatHandling = settings.TypeNameAssemblyFormatHandling;
cloneSettings.TypeNameAssemblyFormat = settings.TypeNameAssemblyFormat;
cloneSettings.MetadataPropertyHandling = settings.MetadataPropertyHandling;
cloneSettings.TypeNameHandling = settings.TypeNameHandling;
cloneSettings.PreserveReferencesHandling = settings.PreserveReferencesHandling;
cloneSettings.Converters = settings.Converters;
cloneSettings.DefaultValueHandling = settings.DefaultValueHandling;
cloneSettings.NullValueHandling = settings.NullValueHandling;
cloneSettings.ObjectCreationHandling = settings.ObjectCreationHandling;
cloneSettings.MissingMemberHandling = settings.MissingMemberHandling;
cloneSettings.ReferenceLoopHandling = settings.ReferenceLoopHandling;
cloneSettings.ReferenceResolver = settings.ReferenceResolver;
cloneSettings.CheckAdditionalContent = settings.CheckAdditionalContent;
return cloneSettings;
}
This is not a question but a case study that was tried on by me where questions have not been asked. In case anyone else tries this kind of idiotic unit testing in the future, these are my findings:
While trying to implement eager validation, as it is not supported currently by .NET Core 3.1, but as the documentation states at the bottom of the section https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-3.1#options-post-configuration:
Eager validation (fail fast at startup) is under consideration for a future release.
You cannot test programmatically the lazy validation from accessing the option in question if you've implemented custom eager validation.
This is what I did:
Created config class
public class TestOptions : IValidateObject // for eager validation config
{
[Required]
public string Prop { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (string.IsNullOrEmpty(this.Prop))
yield return new ValidationResult($"{nameof(this.Prop)} is null or empty.");
}
}
Added the configuration in my lib that I'm testing:
public static void AddConfigWithValidation(this IServiceCollection services, Action<TestOptions> options)
{
var opt = new TestOptions();
options(opt);
// eager validation
var validationErrors = opt.Validate(new ValidationContext(opt)).ToList();
if (validationErrors.Any())
throw new ApplicationException($"Found {validationErrors.Count} configuration error(s): {string.Join(',', validationErrors)}");
// lazy validation with validate data annotations from IOptions
services.AddOptions<TestOptions>()
.Configure(o =>
{
o.Prop = opt.Prop
})
.ValidateDataAnnotations();
}
And the test looks like this
public class MethodTesting
{
private readonly IServiceCollection _serviceCollection;
public MethodTesting()
{
_serviceCollection = new ServiceCollection();
}
// this works as it should
[Fact]
public void ServiceCollection_Eager_Validation()
{
var opt = new TestOptions { Prop = string.Empty };
Assert.Throws<ApplicationException>(() => _serviceCollection.AddConfigWithValidation(o =>
{
o.Prop = opt.Prop
});
}
// this does not work
[Fact]
public void ServiceCollection_Lazy_Validation_Mock_Api_Start()
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("settings.json", optional: false, reloadOnChange: true);
_configuration = builder.Build();
var opt = _configuration.GetSection(nameof(TestOptions)).Get<TestOptions>();
_serviceCollection.AddConfigWithValidation(o =>
{
o.Prop = opt.Prop
});
// try to mock a disposable object, sort of how the API works on subsequent calls
using (var sb = _serviceCollection.BuildServiceProvider())
{
var firstValue = sb.GetRequiredService<IOptionsSnapshot<TestOptions>>().Value;
firstValue.Should().BeEquivalentTo(opt);
}
// edit the json file programmatically, trying to trigger a new IOptionsSnapshot<>
var path = $"{Directory.GetCurrentDirectory()}\\settings.json";
var jsonString = File.ReadAllText(path);
var concreteObject = Newtonsoft.Json.JsonConvert.DeserializeObject<TestObject>(jsonString);
concreteObject.TestObject.Prop = string.Empty;
File.WriteAllText(path, Newtonsoft.Json.JsonConvert.SerializeObject(concreteObject));
using (var sb = _serviceCollection.BuildServiceProvider())
{
// this does not work, as the snapshot is still identical to the first time it is pulled
Assert.Throws<OptionsValidationException>(() => _serviceCollection.BuildServiceProvider().GetRequiredService<IOptionsSnapshot<TestOptions>>().Value);
}
}
// this does not work as well
[Fact]
public void ServiceCollection_Lazy_Validation_Mock_Api_Start_With_Direct_Prop_Assignation()
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("settings.json", optional: false, reloadOnChange: true);
_configuration = builder.Build();
var opt = _configuration.GetSection(nameof(TestOptions)).Get<TestOptions>();
_serviceCollection.AddConfigWithValidation(o =>
{
o.Prop = opt.Prop
});
using (var sb = _serviceCollection.BuildServiceProvider())
{
var firstValue = sb.GetRequiredService<IOptionsSnapshot<TestOptions>>().Value;
firstValue.Should().BeEquivalentTo(opt);
}
var prop = _configuration["TestOptions:Prop"];
_configuration["TestOptions:Prop"] = string.Empty;
// this returns a new value
var otherProp = _configuration["TestOptions:Prop"];
using (var sb = _serviceCollection.BuildServiceProvider())
{
// this does not work, the snapshot is not yet modified, however, calling _configuration.GetSection(nameof(TestOptions)).Get<TestOptions>(); does return the new TestOptions.
Assert.Throws<OptionsValidationException>(() => _serviceCollection.BuildServiceProvider().GetRequiredService<IOptionsSnapshot<TestOptions>>().Value);
}
}
public class TestObject
{
public TestOptions TestOptions { get; set; }
}
My settings.json looked like:
{
"TestOptions": {
"Prop": "something"
}
}
A solution to get this up and running as a test, is to add an optional parameter or an overloaded method with an optional parameter that enforces or not eager validation and test that the lazy validation works properly when the eager is deactivated.
Please note that this is not perfect, but a method of test for people who want to test how the eager and lazy validation can be tested when the options provided are from a source that gets updated but the apps do not get restarted.
If you have suggestions, questions or want to discuss on the subject at hand, feel free to use the comment section
Looks like I found something that can satisfy the lazy validation parable that has eager validation on top of it. Please note that IValidatableObject vs IValidateOptions for eager validation does not make a difference, so please use whatever fits you best!
The solution:
public static void AddConfigWithValidation(this IServiceCollection services, IConfiguration config)
{
// lazy validation
services.Configure<TestOptions>(config.GetSection(nameof(TestOptions))).AddOptions<TestOptions>().ValidateDataAnnotations();
var model = config.GetSection(nameof(TestOptions)).Get<TestOptions>();
// eager validation
var validationErrors = model.Validate(new ValidationContext(model)).ToList();
if (validationErrors.Any())
throw new ApplicationException($"Found {validationErrors.Count} configuration error(s): {string.Join(',', validationErrors)}");
}
And in test method:
[Fact]
public void ServiceCollection_Lazy_Validation_Mock_Api_Start()
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("settings.json", optional: false, reloadOnChange: true);
_configuration = builder.Build();
var opt = _configuration.GetSection(nameof(TestOptions)).Get<TestOptions>();
_serviceCollection.AddConfigWithValidation(_configuration);
var firstValue = _serviceCollection.BuildServiceProvider().GetRequiredService<IOptionsSnapshot<TestOptions>>().Value;
firstValue.Should().BeEquivalentTo(opt);
// edit the json file programmatically, trying to trigger a new IOptionsSnapshot<>
var path = $"{Directory.GetCurrentDirectory()}\\settings.json";
var jsonString = File.ReadAllText(path);
var concreteObject = Newtonsoft.Json.JsonConvert.DeserializeObject<TestObject>(jsonString);
concreteObject.TestObject.Prop = string.Empty;
File.WriteAllText(path, Newtonsoft.Json.JsonConvert.SerializeObject(concreteObject));
_configuration = builder.Build(); // rebuild the config builder
System.Threading.Thread.Sleep(1000); // let it propagate the change
// error is thrown, lazy validation is triggered.
Assert.Throws<OptionsValidationException>(() => _serviceCollection.BuildServiceProvider().GetRequiredService<IOptionsSnapshot<TestOptions>>().Value);
}
This now works correctly and the lazy validation is triggered.
Please note that I have tried to mimic their implementation for IConfiguration listening to change but it did not work.
For eager validation, I stumbled across this post on github (can't take any credit for it, but it seems to do the trick)
I use as follows...
public static IServiceCollection AddOptionsWithEagerValidation<TOptions, TOptionsValidator>(this IServiceCollection services,
Action<TOptions> configAction,
ILogger<ServiceCollection>? logger = default)
where TOptions : class, new()
where TOptionsValidator : class, IValidator, new()
{
services
.AddOptions<TOptions>()
.Configure(configAction)
.Validate(x =>
{
return ValidateConfigurationOptions<TOptions, TOptionsValidator>(x, logger);
})
.ValidateEagerly();
return services;
}
I do some custom stuff during Configure and then perform my own validation using Fluent Validation in Validate. ValidateEagerly causes the IStatupFilter to validate the options early.
I have Asp.Net Core WebApi. I am making Http requests according to HttpClientFactory pattern. Here is my sample code:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddHttpClient<IMyInterface, MyService>();
...
}
public class MyService: IMyInterface
{
private readonly HttpClient _client;
public MyService(HttpClient client)
{
_client = client;
}
public async Task CallHttpEndpoint()
{
var request = new HttpRequestMessage(HttpMethod.Get, "www.customUrl.com");
var response = await _client.SendAsync(request);
...
}
}
I want to implement sending requests through dynamic proxy. This basically means that I might need to change proxy with each request. As for now I find out 2 approuces, non of which seems good to me:
1.Have a static proxy like this:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddHttpClient<IMyInterface, MyService>().ConfigurePrimaryHttpMessageHandler(() =>
{
return new HttpClientHandler
{
Proxy = new WebProxy("http://127.0.0.1:8888"),
UseProxy = true
};
});
...
}
But I can only have single proxy per service in this approach.
2.Dispose HttpClient with each request:
HttpClientHandler handler = new HttpClientHandler()
{
Proxy = new WebProxy("http://127.0.0.1:8888"),
UseProxy = true,
};
using(var client = new HttpClient(handler))
{
var request = new HttpRequestMessage(HttpMethod.Get, "www.customUrl.com");
var response = await client.SendAsync(request);
...
}
But in this way I violate HttpClientFactory pattern and it might cause issues to application performance as stated in following article
Is there a third way where I could change proxy dinamically without re-creating HttpClient?
There is no way to change the any of the properties of HttpClientHandler or to assign a new version of HttpClientHandler to an existing HttpClient after it is instantiated. As such, it is then impossible to have a dynamic proxy for a particular HttpClient: you can only specify one proxy.
The correct way to achieve this is to use named clients, instead, and define a client for each proxy endpoint. Then, you'll need to inject IHttpClientFactory and pick one of the proxies to use, requesting the named client that implements that.
services.AddHttpClient("MyServiceProxy1").ConfigurePrimaryHttpMessageHandler(() =>
{
return new HttpClientHandler
{
Proxy = new WebProxy("http://127.0.0.1:8888"),
UseProxy = true
};
});
services.AddHttpClient("MyServiceProxy2").ConfigurePrimaryHttpMessageHandler(() =>
{
return new HttpClientHandler
{
Proxy = new WebProxy("http://127.0.0.1:8889"),
UseProxy = true
};
});
...
Then:
public class MyService : IMyInterface
{
private readonly HttpClient _client;
public MyService(IHttpClientFactory httpClientFactory)
{
_client = httpClientFactory.CreateClient("MyServiceProxy1");
}
public async Task CallHttpEndpoint()
{
var request = new HttpRequestMessage(HttpMethod.Get, "www.customUrl.com");
var response = await _client.SendAsync(request);
...
}
}
I can do that by inheriting from HttpClientHandler:
public class ProxyHttpHandler : HttpClientHandler
{
private int currentProxyIndex = 0;
private ProxyOptions proxyOptions;
public ProxyHttpHandler(IOptions<ProxyOptions> options)
{
proxyOptions = options != null ? options.Value : throw new ArgumentNullException(nameof(options));
UseProxy = true;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var proxy = proxyOptions.Proxies[currentProxyIndex];
var proxyResolver = new WebProxy(proxy.Host, proxy.Port)
{
Credentials = proxy.Credentials
};
Proxy = proxyResolver;
currentProxyIndex++;
if(currentProxyIndex >= proxyOptions.Proxies.Count)
currentProxyIndex = 0;
return base.SendAsync(request, cancellationToken);
}
}
Then I register my ProxyHttpHandler and ProxyOptions in IoC:
public IForksCoreConfigurationBuilder ConfigureProxy(Action<ProxyOptions> options)
{
Services.AddOptions<ProxyOptions>().Configure(options);
Services.AddTransient<ProxyHttpHandler>();
Services.AddHttpClient<IService, MyService>()
.ConfigurePrimaryHttpMessageHandler<ProxyHttpHandler>();
return this;
}