Custom Configuration Binder for Property
Asked Answered
I

3

12

I'm using Configuration Binding in an ASP.NET Core 1.1 solution. Basically, I have some simple code for the binding in my ConfigureServices Startup section that looks like this:

services.AddSingleton(Configuration.GetSection("SettingsSection").Get<SettingsClass>());

The wrinkle is that my class as an int property that is normally bound to an int value in the configuration file, but could be bound instead to the string "disabled". Under the hood, I want the property to get a value of -1 if it is bound to the string "disabled".

It can be more complicated than this, but I'm simplifying for the sake of brevity.

My question is this: How do I provide a custom binder/converter for that overrides the configuration binding for a specific property in SettingsClass so that when doing a string conversion it will convert "disabled" to -1, rather than throwing an exception that "disabled" can't be converted to an Int32?

Improvident answered 2/2, 2017 at 16:15 Comment(0)
I
16

It appears that since the ConfigurationBinder uses the type's TypeDescriptor to get the converter, the only way for me to do what I'm trying to do is to implement a custom type converter and insert it into the TypeDescriptor for the class I'm converting to (in this case Int32).

So, basically, add this before configuration happens:

TypeDescriptor.AddAttributes(typeof(int), new TypeConverterAttribute(typeof(MyCustomIntConverter)));

Where MyCustomIntConverter looks something like this:

public class MyCustomIntConverter  : Int32Converter
{
    public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
    {
        if (value != null && value is string)
        {
            string stringValue = value as string;
            if(stringValue == "disabled")
            {
                return -1;
            }
        }
        return base.ConvertFrom(context, culture, value);
    }
}

Seems like overkill, as now "disabled" will always convery to -1 for Int32 everwhere in the application. If anyone knows a less invasive way to do this, please let me know.

Improvident answered 2/2, 2017 at 17:23 Comment(1)
"value != null && value is string" is redundant. Either "value is string" or even "value is string stringValue " in newer c# (i know, this post is old but maybe it helps to new ppl coming here)Slice
C
6

I just stumbled upon the same problem recently and came up with slightly different solution.

My idea was to use default binding mechanism. In my case I wanted to get new instance of HashSet having values stored in proper array format inside my database. I created a class I'm binding my configuration to with one private property named as in my configuration and one public property, which uses the private one to create me an instance of HashSet. It looks a little like this:

// settings.json
{
    option: {
        ids:[1,2,3],
    }
}

and the class

public class Options
{
    public HashSet<int> TrueIds
    {
        get
        {
            return RestrictedCategoryIds?.ToHashSet();
        }
    }

    private int[] Ids{ get; set; }
}

Then you can use BindNonPublicProperties to ensure that binder will fill your private property.

// Startup.cs
services.Configure<Options>(Configuration, c => c.BindNonPublicProperties = true);

You say that in your case this might be not as simple as converting "disabled" into -1, but maybe my idea will inspire you to solve this one in different way.

Coleslaw answered 23/11, 2018 at 15:10 Comment(0)
D
0

I found another solution which is not necessarily better but it gives you a way to put it in an extension method.

private const string SectionName = "example";

public static void UseCustomizedSettings(this IServiceCollection services) =>
    services
        .AddOptions<CustomizedSettings>()
        .Configure<IConfiguration>((settings, configuration) =>
        {
            configuration
                .GetRequiredSection(SectionName)
                .Bind(settings);

            // TODO move to shared project extension method
            var numberFlag = configuration
                .GetRequiredSection(SectionName)
                .GetValue<object?>(nameof(CustomizedSettings.NumberFlag));
            settings
                .NumberFlag = ParseNumberOrDisabled(numberFlag)!;
        });

private static int ParseNumberOrDisabled(object? numberFlag) {
  // Conversion logic here
}

Now you can just move everything beneeth the DODO into an extension method in a shared project and use it like this:

public static void UseCustomizedSettings(this IServiceCollection services) =>
    services
        .AddOptions<CustomizedSettings>()
        .Configure<IConfiguration>((settings, configuration) =>
        {
            configuration
                .GetRequiredSection(SectionName)
                .Bind(settings);
            configuration
                .GetRequiredSection(SectionName)
                .BindNumberFlag(settings, s => s.NumberFlag);
        });

Perhaps you can give it some more descriptive name than BindNumberFlag, communicating what it's actually meant to do, so anyone reading this config binding can immediately see what's happening.

Disarticulate answered 15/8, 2023 at 15:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.