1

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

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Sunny Sharma
  • 4,688
  • 5
  • 35
  • 73
  • There are more than a few questions and answers on this, Did none of them work for you? – TheGeneral Oct 11 '21 at 02:06
  • @TheGeneral - yes, I did. updated my question with some of the refs. – Sunny Sharma Oct 11 '21 at 02:18
  • [This answer](https://stackoverflow.com/a/53295770/3181933) with the appropriate change to `MvcNewtonsoftJsonOptions` from `MvcJsonOptions` didn't work for you? – ProgrammingLlama Oct 11 '21 at 02:21
  • @Llama - it didn't. Apparently "MvcJsonOptions" isn't available beyond ".net core 2.2" https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcjsonoptions?view=aspnetcore-2.2&viewFallbackFrom=aspnetcore-3.1 – Sunny Sharma Oct 11 '21 at 02:28
  • Quoting my previous comment: _"with the appropriate change to `MvcNewtonsoftJsonOptions` **from** `MvcJsonOptions`"_ (why do you think I remarked on this?) – ProgrammingLlama Oct 11 '21 at 02:49
  • Does the linked duplicate answer your question sufficiently? Because injecting a run-time dependency via a constructor argument *into converters applied via metadata* doesn't seem to be supported by Json.NET out of the box... – dbc Oct 11 '21 at 18:22
  • ... Checking the source code, [`DefaultContractResolver.SetPropertySettingsFromAttributes()`](https://github.com/JamesNK/Newtonsoft.Json/blob/52e257ee57899296d81a868b32300f0b3cfeacbe/Src/Newtonsoft.Json/Serialization/DefaultContractResolver.cs#L1611) calls [`JsonTypeReflector.GetJsonConverter(object attributeProvider)`](https://github.com/JamesNK/Newtonsoft.Json/blob/52e257ee57899296d81a868b32300f0b3cfeacbe/Src/Newtonsoft.Json/Serialization/JsonTypeReflector.cs#L181) which does not have any option to pass in some run-time argument such as `IMyService myService`. – dbc Oct 11 '21 at 18:24
  • you're right @dbc, it didn't work for me, and that was the point asking a separate question on this. – Sunny Sharma Oct 12 '21 at 07:48
  • 1
    As an alternative, this might work for you: [Pass additional data to JsonConverter](https://stackoverflow.com/q/53193503/3744182). – dbc Oct 12 '21 at 07:52
  • @SunnySharma did you find a solution? – wodzu Feb 21 '22 at 18:05
  • @wodzu - I've added my answer. Please see if that's helpful. thanks – Sunny Sharma Feb 22 '22 at 18:06
  • 1
    @SunnySharma I needed to inject a dependency into my custom `JsonConverter` implementation, which cannot have a non-default constructor as it would cause exception otherwise. Your answer in turn uses a custom `ContractResolver`, which apparently can have a non-default constructor - So our individual problems were a bit different. I posted my answer below. It is based on Thomas' hack, but with a slight modification to handle my specific setup. – wodzu Feb 23 '22 at 07:23

3 Answers3

1

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!

Sunny Sharma
  • 4,688
  • 5
  • 35
  • 73
0

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;
    };
}
wodzu
  • 3,004
  • 3
  • 25
  • 41
-4

IMHO there's something substantially wrong with what you're trying to achieve. Serializing the result from your controller method should not contain any logic whatsoever.

Even more, your api should allow content negotiation, where Accept: application/json gives you a json string, and Accept: application/xml gives you an xml string.

Instead you should leverage on Dependency Injection, where you inject MyService in your other service and call myResult.whatever = myService.GetValue() there.

Pieterjan
  • 2,738
  • 4
  • 28
  • 55