In asp.net core 3.1, using the new System.Text.Json, I am trying to use a custom JsonConverter on an appsettings section. Manually serializing/deserializing respects the converter just fine, but reading from appSettings via Options pattern does not. Here's what I have:
The JsonConverter. For simplicity, this one just converts a string value to uppercase:
public class UpperConverter : JsonConverter<string>
{
public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
reader.GetString().ToUpper();
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) =>
writer.WriteStringValue(value == null ? "" : value.ToUpper());
}
The appsettings class, declaring the converter on a string property:
public class MyOptions
{
public const string Name = "MyOptions";
[JsonConverter(typeof(UpperConverter))]
public string MyString { get; set; }
}
The Startup.cs changes to prepare everything:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(new UpperConverter());
});
services.Configure<MyOptions>(Configuration.GetSection(MyOptions.Name));
}
When I inject an IOptions<MyOptions>
into the HomeController, it reads a lowercase value. If I manually do JsonSerializer.Deserialize<MyOptions>("{\"MyString\":\"lowercase_manual\"}")
, I get an uppercase string. Even when I remove Startup declarations of JsonSerializerOptions.
Does anyone know how to get the appsettings / options pattern to respect the JsonConverter? Do I have to declare the JsonSerializerOptions somewhere else? Thanks.
It's important to understand that the options pattern is implemented as two separate steps: reading data from a configuration source, then binding that data to strongly-typed objects.
The read step is implemented by various configuration providers, only one of which is JSON. You might expect that the JSON provider would respect your JsonConverter
, but this step only performs minimal transformation of its configuration data into a generic format that the next step can accept.
The binding step, then, would seem to be the place that would care about JsonConverter
. But this step is intentionally completely agnostic of any specific configuration provider, because it simply receives data in a generic format from the providers of which it purposefully knows nothing about. Therefore, it won't care about a JSON-specific converter.
It will, however, care about more generic converters to handle its generic data, and fortunately .NET already has infrastructure for this built in: type converters. These have been in .NET since almost the beginning, and while they're old they're perfectly simple, serviceable, and indeed ideal for this specific scenario.
A full example of how to implement a type converter is out of scope for this answer, but the essentials are that you derive from TypeConverter
, override the appropriate methods, and decorate the class you want to be converted with a TypeConverterAttribute
pointing back to your TypeConverter
implementation. Then it should all Just Work™.
The caveat with the example you've provided is that you aren't actually trying to convert anything, you're trying to transform a string, and obviously a TypeConverter
won't be invoked since the source value from the configuration providers is a string, while the destination type on your options class is also a string.
What you can do instead is create a new class that wraps a string to force it to uppercase:
public class UppercaseString
{
public string Value { get; }
public UppercaseString(string str)
{
Value = str.ToUpper();
}
public static implicit operator string(UppercaseString upper)
=> upper.Value;
}
then change your options class to use that wrapper:
public class MyOptions
{
public const string Name = "MyOptions";
public UppercaseString MyString { get; set; }
}
and finally, implement a TypeConverter
that converts from string
to UppercaseString
.
Note the definition of implicit operator string
- this allows you to use an UppercaseString
anywhere a standard string
is expected, so that you don't have to change your code that references MyOptions.MyString
to MyOptions.MyString.Value
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With