uxp uxp - 1 month ago 20
ASP.NET (C#) Question

ASP.NET Deserialize Form POST with unusual key names into proper DataContract model?

I'm somewhat new to C# and ASP.NET, and I've been tasked with replacing an application with a new ASP.NET Core MVC API service. I also have a legacy application that needs to POST some data into it, but I have no control over it's code and cannot change it's behavior. It wants to send data as

application/x-www-form-urlencoded
, and with uppercased parameter names containing "invalid" characters when mapped back to a POCO data contract object, such as
.
, or names starting with numbers.

An example request from the legacy app might look like:

POST /something HTTP/1.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 42

USERNAME=somebody&ACCOUNT=123&APP.SESSID=acbd18db4cc2f85cedef654fccc4a4d8&1STCOLOR=BLUE


Since I cannot, and somewhat don't want, to create a DataContract model like this invalid code, I'm not sure how to proceed.

class InputModel
{
public string USERNAME { get; set; }
public int ACCOUNT { get; set; }
public string APP.SESSID { get; set; }
public string 1STCOLOR { get; set; }
}


Preferably, I'd like to humanize the data model member names where possible, (eg,
APP.SESSID
to
ApplicationSessionID
), and be able to bind the legacy parameter names to the humanized member names on deserialization of the form data, or during model binding, or something.

I just don't know where to look or what to search for in the ASP.NET Core code to help me determine what I should be doing to override the default form deserialization behavior. Can anyone with experience here help? Thanks for your time!

uxp uxp
Answer

I think I figured it out. I first had to create a ValueProvider object. This is what I came up with.

public class MyValueProvider : BindingSourceValueProvider, IValueProvider
{
    private IEnumerable<KeyValuePair<string, string>> _keyMap;
    private readonly CultureInfo _culture;
    private PrefixContainer _prefixContainer;
    private IFormCollection _values;

    public MyValueProvider(BindingSource form, IFormCollection values, CultureInfo culture) : this(form, values, null, culture) { }

    public MyValueProvider(BindingSource form, IFormCollection values, IEnumerable<KeyValuePair<string, string>> keyMap, CultureInfo culture) : base(form)
    {
        if (form == null)
        {
            throw new ArgumentNullException(nameof(form));
        }

        if (values == null)
        {
            throw new ArgumentNullException(nameof(values));
        }


        _values = values;
        _culture = culture;
        _keyMap = keyMap;
    }

    public CultureInfo Culture
    {
        get { return _culture; }
    }

    protected PrefixContainer PrefixContainer
    {
        get
        {
            if (_prefixContainer == null)
            {
                _prefixContainer = new PrefixContainer(_values.Keys);
            }
            return _prefixContainer;
        }
    }

    public override bool ContainsPrefix(string prefix)
    {
        return PrefixContainer.ContainsPrefix(prefix);
    }

    public virtual IDictionary<string, string> GetKeysFromPrefix(string prefix)
    {
        if (prefix == null)
        {
            throw new ArgumentNullException(nameof(prefix));
        }

        return PrefixContainer.GetKeysFromPrefix(prefix);
    }

    public override ValueProviderResult GetValue(string key)
    {
        if (key == null)
        {
            throw new ArgumentNullException(nameof(key));
        }

        var pair = _keyMap.FirstOrDefault(map => map.Key.EndsWith(key));
        if (pair.Value == null)
        {
            return ValueProviderResult.None;
        }
        var values = _values[pair.Value];
        if (values.Count == 0)
        {
            return ValueProviderResult.None;
        }

        return new ValueProviderResult(values, Culture);
    }
}

and then a Factory for that value provider.

public class MyValueProviderFactory : IValueProviderFactory
{
    private readonly IEnumerable<KeyValuePair<string,string>> keyMap;

    public MyValueProviderFactory(IConfigurationSection section)
    {
        keyMap = section.AsEnumerable();
    }

    public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (context.ActionContext.HttpContext.Request.HasFormContentType)
        {
            return AddValueProviderAsync(context, keyMap);
        }

        return TaskCache.CompletedTask;
    }

    private static async Task AddValueProviderAsync(ValueProviderFactoryContext context, IEnumerable<KeyValuePair<string, string>> keyMap)
    {
        var request = context.ActionContext.HttpContext.Request;
        var valueProvider = new MyValueProvider(
                BindingSource.Form,
                await request.ReadFormAsync(),
                keyMap,
                CultureInfo.CurrentCulture);

        context.ValueProviders.Add(valueProvider);
    }
}

And then finally, in my Startup class, I load a section of my appsettings.json configuration as the key map. This is just a JSON object with the original key name as the JSON property, and the new key name as the value. I pass this as a bunch of key-values to my ValueProviderFactory, and then I insert this value provider factory into the front of the list of factories provided by the system.

public class Startup
{
    public Startup(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("appsettings.json")
            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true)
            .AddEnvironmentVariables();
        Configuration = builder.Build();
    }

    public IConfigurationRoot Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc(options =>
        {
            options.ValueProviderFactories.Insert(0, new MyValueProviderFactory(Configuration.GetSection("MyValueProviderKeyMap")));
        });
    }
}

At the end, it's pretty straightforward. It just took me a bit to figure out the process.