Home > Mobile >  Reading a value from a Query parameter name containing a colon (:)
Reading a value from a Query parameter name containing a colon (:)

Time:11-18

I got a request to create a new REST API in a .NET application, but I have no idea how to implement one of the parameters.

I got a Swagger definition and the parameter is defined as followed:

Screenshot of Swagger definition

If it would just be eventCreatedDateTime=2021-04-01T14:12:56 01:00 no problem, but it's getting the part between the colon and the equals sign I have no idea how to get.

Basically, I could get eventCreatedDateTime:gte=2021-04-01T14:12:56 01:00as a querystring parameter and I have to read the gte part and also be able to validate if it's one of the allowed suffixes. The suffix isn't mandatory, so eventCreatedDateTime=2021-04-01T14:12:56 01:00 should be valid as well.

For clarification, this is a querystring parameter, so part of the URL. e.g. https://example.com/api/mycontroller?param1=value&param2=value&eventCreatedDateTime:gte=2021-04-01T14:12:56 01:00&param4=value

Any idea how to do this in .NET?

CodePudding user response:

For this, I will use a custom type like :

public class EventCreatedDateTime
{
    public string Operator { get; set; }
    public string Value { get; set; }
}

Next I will create a custom model binder :

public class EventCreatedDateTimeModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if(context.Metadata.ModelType == typeof(EventCreatedDateTime))
        {
            return new EventCreatedDateTimeModelBinder();
        }
        return null;
    }
}

public class EventCreatedDateTimeModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        foreach(var kvp in bindingContext.HttpContext.Request.Query)
        {
            if (kvp.Key.StartsWith("eventCreatedDateTime:"))
            {
                bindingContext.Result = ModelBindingResult.Success(
                    new EventCreatedDateTime {
                        Operator = kvp.Key.Substring("eventCreatedDateTime:".Length),
                        Value = kvp.Value.First()
                });
            }
        }
        return Task.CompletedTask;
    }
}

That I add in Startup :

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers(options => 
            options.ModelBinderProviders.Insert(0, new EventCreatedDateTimeModelBinderProvider())
        );
        ...
    }
}

Then the action is :

[HttpGet]
public IActionResult Get(
    string param1,
    string param2,
    EventCreatedDateTime eventCreatedDateTime)
{...}

CodePudding user response:

See vernou's reponse for the .NET Core way to do it. My environment was still Framework, so here's the solution for that.

My custom type is a bit different, with a DateTime and an enumerator property, this can off course be usable in Core as well:

public enum Operator
{
    Equals,
    GreaterThenEquals,
    GreaterThen,
    LesserThenEquals,
    LesserThen
}
public class DateTimeFilter
{
    public DateTime? Date { get; set; }
    public Operator Operator { get; set; }
}

The custom model binder is a bit different in Framework:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.ModelBinding;
using System.Web.Http.ModelBinding.Binders;

namespace CustomModelBinders
{
    public class DateTimeFilterModelBinderProvider : ModelBinderProvider
    {
        private CollectionModelBinderProvider originalProvider = null;

        public DateTimeFilterModelBinderProvider(CollectionModelBinderProvider originalProvider)
        {
            this.originalProvider = originalProvider;
        }

        public override IModelBinder GetBinder(HttpConfiguration configuration, Type modelType)
        {
            IModelBinder originalBinder = originalProvider.GetBinder(configuration, modelType);

            if (originalBinder != null && modelType == typeof(DateTimeFilter))
            {
                return new DateTimeFilterModelBinder();
            }
            return null;
        }
    }

    public class DateTimeFilterModelBinder : IModelBinder
    {
        public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
        {
            if (bindingContext.ModelType != typeof(DateTimeFilter))
            {
                return false;
            }
            //Get's the correct key/value from the querystring based on your receiving paramter name.
            //note: you can't use [FromUri(Name = "customName")] with the custom binder so the have to match (partially)
            var query = actionContext.Request.Properties["MS_QueryNameValuePairs"] as ICollection<KeyValuePair<string, string>>;
            KeyValuePair<string, string> kvp = query.First(q => q.Key.Contains(bindingContext.ModelName));
            if (kvp.Key.Contains(":"))
            {
                bindingContext.Model =
                    new DateTimeFilter
                    {
                        Operator = ConvertOperator(kvp.Key.Substring(kvp.Key.IndexOf(":") 1)),
                        Date = ConvertDate(kvp.Value)
                    };
            }
            else
            {
                bindingContext.Model =
                    new DateTimeFilter
                    {
                        Operator = Operator.Equals,
                        Date = ConvertDate(kvp.Value)
                    };
            }
            return true;
        }

        private DateTime? ConvertDate(string str)
        {
            DateTime result;
            DateTimeOffset resultOffset;
            if (DateTime.TryParse(str, out result))
                return result;
            //Apparently the   gets converted into a space, so we need to revert that to have a valid offset
            else if (DateTimeOffset.TryParse(str.Replace(' ', ' '), out resultOffset))
                return resultOffset.ToLocalTime().DateTime;
            else
                return null;
        }

        private Operator ConvertOperator(string str)
        {
            switch (str.ToLowerInvariant())
            {
                case "gte": return Operator.GreaterThenEquals;
                case "gt": return Operator.GreaterThen;
                case "lte": return Operator.LesserThenEquals;
                case "lt": return Operator.LesserThen;
                case "eq": return Operator.Equals;
                default: throw new ArgumentException("Invalid operator");
            }
        }
    }
}

The Conversion methods are perfectly fine to usein a Core application

No startup in Framework, the parameter has to be coupled to the binder with an atrbute:

[HttpGet]
public IHttpActionResult Get(string param1 = null, string param2 = null, [ModelBinder(typeof(DateTimeFilterModelBinder))] DateTimeFilter eventCreatedDateTime = null, string param3 = null)
{
     //Do Logic
}

The above works as expected for eventCreatedDateTime=2021-04-01T14:12:56 01:00 enter image description here

And for example eventCreatedDateTime:gte=2021-04-01T14:12:56 01:00 enter image description here

  • Related