I'm failing to get the Dependency Injection working for the following Newtonsoft JsonConverter in .NET Core 3.1.
I want to use it at the attribute level only, not at a global level. So, it should be executed only when the designated attribute(s) from a certain class(es).
JsonConverter:
public class HelloWorldCustomConverter : JsonConverter<string>
{
private readonly IMyService _myService;
public HelloWorldCustomConverter(IMyService myService)
{
_myService = myService;
}
public override bool CanRead => false;
public override string ReadJson(JsonReader reader, Type objectType, string existingValue, bool hasExistingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override void WriteJson(JsonWriter writer, string value, JsonSerializer serializer)
{
// append a value using the injected service
writer.WriteValue($"{value}-{myService.GetValue()}");
}
}
Usage:
public class MyClass
{
public string Title { get; set; }
[JsonConverter(typeof(HelloWorldCustomConverter))]
public string Details { get; set; }
}
It's .NET Core 3.1 and Newtonsoft.json version 13.0.1.
I appreciate any help, thanks.
Edit 1
I checked lots of answers from StackOverflow but none worked for me so far. Most of them are rather out-dated or has something missing to get it working. Few of them which I checked already and it didn't work for me:
Edit 2
I tried the post suggested as a duplicate reference but it doesn't work in my case.
I tried spinning my head around and various other options but no luck.
One of the suggested work around from James (dated: 2108), didn't work.
Ref: https://github.com/JamesNK/Newtonsoft.Json/issues/1910
You can try something like
public class JsonOptions : IConfigureOptions<MvcJsonOptions>
{
IHttpContextAccessor _accessor;
public JsonOptions(IHttpContextAccessor accessor)
{
_accessor = accessor;
}
public virtual void Configure(MvcJsonOptions options)
{
options.SerializerSettings.Converters.Add(new MyCustomConverter(_accessor));
}
}
Register it in your startup
services.AddSingleton<IConfigureOptions<MvcJsonOptions>, JsonOptions>()
(can't remember if IHttpContextAccessor is registered by default so you may need to register that one as well)
Then in your Read/WriteJson methods use _accessor.HttpContext to access the context of the request
Judging from the comments in Thomas' blog post you have tried his approach. Regardless whether you managed to get it to work or not, I'm posting the solution to the issue I ran into, when I tried to implement Thomas' hack - maybe this will help someone else.
In my setup the custom JsonConverter is actually not instantiated directly by the MVC framework, but indirectly via Newtonsoft's JToken.ToObject(), which creates a JsonSerializer with default JsonSerializerSettings. Further down ToObject()'s call chain, my custom JsonConverter is instantiated with these default settings.
TL;DR; To make Thomas' hack work, I needed to change IConfigureOptions<MvcNewtonsoftJsonOptions>.Configure()'s implementation to:
public void Configure(MvcNewtonsoftJsonOptions options)
{
JsonConvert.DefaultSettings = () =>
{
var settings = new JsonSerializerSettings();
settings.Converters.Add(new ServiceProviderDummyConverter(_httpContextAccessor, _serviceProvider));
return settings;
};
}
Here's how it worked for me:
Use a ContractResolver. (I was using Converter in my case).
Custom ContractResolver. Change the logic as per your need.
using HelloWorld.Attributes;
using HelloWorld.Helpers;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System;
using System.Reflection;
namespace HelloWorld.Serializers
{
public class MyCustomContractResolver : CamelCasePropertyNamesContractResolver
{
private readonly IServiceProvider _serviceProvider;
public MyCustomContractResolver(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);
// this condition is specific to my case, just to showcase how I'm accessing value from HTTP Context
if (Attribute.IsDefined(member, typeof(MyCustomAttribute),true))
{
if (property.PropertyType == typeof(string))
{
PropertyInfo propertyInfo = member as PropertyInfo;
// access required services here
var contextAccessor = _serviceProvider.GetRequiredService<IHttpContextAccessor>();
var customHelper = _serviceProvider.GetRequiredService<ICustomHelper>();
var attribute = (MyCustomAttribute)member.GetCustomAttribute(typeof(MyCustomAttributeAttribute));
property.ValueProvider = new StringValueProvider(propertyInfo, customHelper, contextAccessor, attribute);
}
}
return property;
}
public class StringValueProvider : IValueProvider
{
private PropertyInfo _targetProperty;
private readonly ICustomHelper _customHelper;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly MyCustomAttribute _attribute;
public StringValueProvider(
PropertyInfo targetProperty,
ICustomHelper customHelper,
IHttpContextAccessor httpContextAccessor,
MyCustomAttribute attribute)
{
_targetProperty = targetProperty;
_customHelper = customHelper;
_httpContextAccessor = httpContextAccessor;
_attribute = attribute;
}
// SetValue gets called by Json.Net during deserialization.
// The value parameter has the original value read from the JSON;
// target is the object on which to set the value.
public void SetValue(object target, object value)
{
_targetProperty.SetValue(target, value);
}
// GetValue is called by Json.Net during serialization.
// The target parameter has the object from which to read the value;
// the return value is what gets written to the JSON
public object GetValue(object target)
{
object value = _targetProperty.GetValue(target);
var userId = _httpContextAccessor.HttpContext.Request.Headers["UserId"].ToString();
return value == null ? value : _customHelper.SetGreetingsTextForUser(value.ToString(),userId, _attribute.UserRole);
}
}
}
}
MvcNewtonsoftJsonOptionsWrapper with serviceProvider injection
using HelloWorld.Serializers;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using System;
namespace HelloWorld.Extensions
{
public class MvcNewtonsoftJsonOptionsWrapper : IConfigureOptions<MvcNewtonsoftJsonOptions>
{
IServiceProvider ServiceProvider;
public MvcNewtonsoftJsonOptionsWrapper(IServiceProvider serviceProvider)
{
this.ServiceProvider = serviceProvider;
}
public void Configure(MvcNewtonsoftJsonOptions options)
{
options.SerializerSettings.ContractResolver = new MyCustomContractResolver(ServiceProvider);
}
}
}
ServiceCollection Extension to register the ContractResolver:
public static class ServiceCollectionExtensions
{
public static IServiceCollection MyCustomContractResolver(this IServiceCollection services)
{
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<IConfigureOptions<MvcNewtonsoftJsonOptions>, MvcNewtonsoftJsonOptionsWrapper>();
return services;
}
}
In Startup.cs file register the ContractResolver in DI:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddAppendSasTokenContractResolver();
...
}
That's all. Let me know if that works for you!
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