12

following code in asp.net web API worked fine but doesn't work in Asp.net core.

Endpoint api/devices?query={"deviceName":"example"}

[HttpGet]
public Device ([FromUri] string deviceName)
{        
        var device = context.Computers.Where(x => x.deviceName == deviceName);
        return device;
}

[FromUri] attribute is not present asp.net core web API, and I tried to use following , but no success.

[HttpGet]
public Device  Get([FromQuery] string  deviceName)
{
    return repo.GetDeviceByName(deviceName);
}
3
  • 1
    Seems like non-functional code. The parameter is called query and not deviceName. deviceName is a property of some json-like query parameter. For get you should only use query parameters and for post transport the data inside the request body. If you really want this non-standard way (which isn't tied to ASP.NET Core at all) to still work, you'll need to write your own model binder Commented Jun 17, 2017 at 19:27
  • After wrestling with this myself for some time, I realised in the end that I needed to do a POST instead of a GET even though I want a value back. The data being sent, although not persisted, it's still affecting the state of the API. You need to think about the design of your RESTful interface - that's the key to this. Commented Oct 6, 2019 at 21:49
  • I think tseng is saying is to change it to Get([FromQuery] string query) Commented Nov 1, 2019 at 17:40

2 Answers 2

13

Unfortunately there is no way to bind JSON in a GET query like you have there. What you are looking for is to use a custom model binder to tell ASP.net Core how you want to bind.

First, you want to build your model for your JSON object.

public class MyCustomModel
{
    public string DeviceName { get; set; }
}

Next you need to build your model binder. A simple example is given below but you would obviously want other checks around if it can be converted, Try/Catch blocks etc. Essentially a model binder tells ASP.net Core how a model should be bound. You might also run into TypeConverters which are given a type, how can I change this to another type during model binding. For now let's just use modelbinders.

public class MyViewModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var jsonString = bindingContext.ActionContext.HttpContext.Request.Query["query"];
        MyCustomModel result = JsonConvert.DeserializeObject<MyCustomModel>(jsonString);

        bindingContext.Result = ModelBindingResult.Success(result);
        return Task.CompletedTask;
    }
}

So all we are doing is taking the query string and deserializing it to our model.

Next we build a provider. A provider is what tells ASP.net core which modelbinder to use. In our case it's simple, if the model type is our custom type, then use our custom binder.

public class MyViewModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context.Metadata.ModelType == typeof(MyCustomModel))
            return new MyViewModelBinder();

        return null;
    }
}

And the final piece of the puzzle. In our startup.cs, we find where we add MVC services and we insert our model binder to the front of the list. This is important. If we just add our modelbinder to the list, another model binder might think it should be used instead (First in first served), so we might not ever make it to ours. So be sure to insert it at the start.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(config => config.ModelBinderProviders.Insert(0, new MyViewModelBinderProvider()));
}

Now we just create an action where we read the data, no attributes required.

[HttpGet]
public void Get(MyCustomModel model)
{

}

Further reading :

Sign up to request clarification or add additional context in comments.

4 Comments

Thank you. It really helped. If I have two Get methods in the api controller, one gets all the devices and other Get method takes the devicename. I have errors on having same Get methods.
If they are both accepting the same payload (The entire JSON object), then you should only need one right?
The draw back to this is now for every custom model binder you create, you have to make sure your json can deserialize it as well, It might be a better Idea to use a json value provider, and have the default model vinder kick in afterwards, That way you can also get error information on individual properties
Wow. That's a lot of work to avoid just calling the JSON deserializer directly in the controller.
4

I came along this question because I have a similar problem. In my scenario it was more convinient to implement ModelBinderAttribute:

public class FromUriJsonAttribute : ModelBinderAttribute
{
    public FromUriJsonAttribute()
    {
        BinderType = typeof(JsonBinder);
    }

    public FromUriJsonAttribute(string paramName)
        : this()
    {
        Name = paramName;
    }
}

As in @MindingData answer the binder is needed:

public class JsonBinder : IModelBinder
{
    public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
    {
        try
        {
            var name = bindingContext.ModelName;
            var json = actionContext.Request.GetQueryNameValuePairs().FirstOrDefault(x => x.Key == name).Value;
            var targetType = bindingContext.ModelType;
            var model = Newtonsoft.Json.JsonConvert.DeserializeObject(json, targetType);
            bindingContext.Model = model;
            return true;
        }
        catch
        {
        }
        return false;
    }
}

And the usage:

[Route("{id}/items")]
public Item GetItem(string id, [FromUriJson] Filter filter)
{
    //logic
}  

or:

[Route("{id}/items")]
public Item GetItem(string id, [FromUriJson("queryStringKey")] Filter filter)
{
    //logic
} 

It can be useful if you want to have Json formatted query string parameter only in some endpoints and/or don't want to mess with default IServiceCollection configuration by inserting a new IModelBinderProvider implementation.

3 Comments

did you have to register the ModelBinder or Attribute anywhere (like in a provider)? Because in my case the ModelBinder does not get activated
@BoukeVersteegh No, I only use the attribute in controllers methods when I need it. The ModelBinderAttribute has BinderType set to JsonBinder. In my case BindModel() is called right before controllers action.
It may be worth noting that this approach triggers GitHub's security alert of 'Deserialization of untrusted data'. Reference: owasp.org/www-community/vulnerabilities/…

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.