12

Currently for every GET I have to manually create a query object from the route parameters.

Is it possible to bind directly to a query object instead?

So, instead of :

[Route("{id:int}")]
public Book Get(int id) {

    var query = new GetBookByIdQuery {
        Id = id
    };

    // execute query and return result
}

I could do this:

[Route("{id:int}")]
public Book Get(GetBookByIdQuery query) {
    // execute query and return result
}

Where GetBookByIdQuery looks like:

public class GetBookByIdQuery {
    public int Id { get; set;}
}
abatishchev
  • 98,240
  • 88
  • 296
  • 433
kimsagro
  • 15,513
  • 17
  • 54
  • 69
  • I can't understand why you want to waste your time on this. What's the problem to use the id instead of GetBookByIdQuery? Can you say a little more about the goal or the usage of this proposal? – Alberto León Aug 02 '15 at 08:12
  • 4
    Huh, why is this a waste of time? For posts in WebApi, you would bind directly to a strongly typed model rather than binding to each individual field and then populating the strongly typed model yourself. Why should a GET be any different. Right now I have to take each bound parameter and create my strongly typed query object rather than just binding to that object in the first place – kimsagro Aug 02 '15 at 20:32
  • Your approach will break the REST standard. You respond to a requested resource id, not a model. REST is well defined about how to GET, PUT, POST, PATCH and DELETE and ASP.NET Web API 2 is build to do REST applications. Perhaps you're an innovator. Then you need to justify better your approach. I did similar things in ASP.NET MVC with model binders. You can find it for Web API here http://www.asp.net/web-api/overview/formats-and-model-binding/parameter-binding-in-aspnet-web-api I strongly dislike your GET proposal, but I hope this could help you – Alberto León Aug 02 '15 at 22:01
  • 5
    I'm confused.. I'm not breaking any standards. You are still requesting the resource with an Id, the url does not change, the route with it's constraints does not change. I'm just asking if their is a binding shortcut, nothing more. My question is simply can you bind directly to a model rather than binding to each individual route parameter – kimsagro Aug 02 '15 at 22:19

3 Answers3

11

to read a complex type from the URI, [FromUri] can be used

    [Route("{id:int}")]
    public Book Get([FromUri] GetBookByIdQuery query) {

        // execute query and return result
    }

if you request api/values/2 then id property of query object will be 2;

Mustafa ASAN
  • 3,747
  • 2
  • 23
  • 34
  • why downvoting this?what is problem with this? – Mustafa ASAN Dec 18 '15 at 09:13
  • This only works for me if there are values in the query string for the complex type. Then it will take both the route id and the query string values. – Shane Courtrille Nov 02 '16 at 19:44
  • What happens if `GetBookIdQuery` also has values in the body? Ideally, It would build the object based on the body first, and then try to set values on it based on the route template parameters. – crush Feb 14 '19 at 17:43
  • In AspNet Core the [FromUri]-attribute is split into separate attributes for route and querystring: [FromRoute] and [FromQuery]. In the example above [FromRoute] would be correct. Also notice that even if it works, Swashbuckle (Swagger UI) might be confused when binding route-parameters to property of complex object. – toralux Jul 09 '19 at 14:34
3

The answer is to define you own HttpParameterBinding.

Here is the example I've made.

First I've created my CustomParameterBinding

public class CustomParameterBinding : HttpParameterBinding
{
    public CustomParameterBinding( HttpParameterDescriptor p ) : base( p ) { }

    public override System.Threading.Tasks.Task ExecuteBindingAsync( System.Web.Http.Metadata.ModelMetadataProvider metadataProvider, HttpActionContext actionContext, System.Threading.CancellationToken cancellationToken )
    {

        // Do your custom logic here
        var id = int.Parse( actionContext.Request.RequestUri.Segments.Last() );

        // Set transformed value
        SetValue( actionContext, string.Format( "This is formatted ID value:{0}", id ) );

        var tsc = new TaskCompletionSource<object>();
        tsc.SetResult(null );
        return tsc.Task;
    }
}

The next step is to create custom attribute to decorate parameter:

public class CustomParameterBindingAttribute : ParameterBindingAttribute
{
    public override HttpParameterBinding GetBinding( HttpParameterDescriptor parameter )
    {
        return new CustomParameterBinding( parameter );
    }
}

And finally now controller looks like:

public class ValuesController : ApiController
{
    // GET api/values/5
    [Route( "api/values/{id}" )]
    public string Get([CustomParameterBinding] string id )
    {
        return id;
    }       
}

So now when I call http://localhost:xxxx/api/values/5

I get: "This is formatted ID value:5"

Enes
  • 1,115
  • 7
  • 12
0

You can use the class as the parameter, but as id is no longer a parameter in the definition of the method, it cannot be included in the Route.

public class LibraryController : ApiController
{
    [HttpGet]  
    public Book Get(GetBookByIdQuery query)
    {
        // Process query... & return
    }
}

You can call it with the link:

http://localhost:54556/api/Library?id=12
MaRoBet
  • 309
  • 4
  • 8