1
votes

Is there a way to simplify when we have multiple required levels of routing fro each and every method?

I have a hypothetical WebAPI project that i'm using for a generic look at the problem. It gives us movies from some source.

public class MovieController : ApiController
{
    // GET api/<controller>
    public IEnumerable<Movie> Get()
    {
        return MoviesDB.All();
    }

    // GET api/<controller>/5
    public Movie Get(int id)
    {
        return MoviesDB.ThisSpecificOne(id);
    }

    // POST api/<controller>
    public void Post([FromBody]Movie value)
    {
    }

    // PUT api/<controller>/5
    public void Put(int id, [FromBody]Movie value)
    {
    }

    // DELETE api/<controller>/5
    public void Delete(int id)
    {
    }
}

But lets say for some stupid reason movies are stored by Genre. So you need genre + id combo.

I'm assuming this is how you would do it

config.Routes.MapHttpRoute(
    name: "MoviesWithGenre",
    routeTemplate: "api/{controller}/{genre}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

public class MovieController : ApiController
{
    // GET api/<controller>/<genre>
    public IEnumerable<Movie> Get(string genre)
    {
        return MoviesDB.All(genre);
    }

    // GET api/<controller>/<genre>/5
    public Movie Get((string genre, int id)
    {
        return MoviesDB.ThisSpecificOne(string genre, id);
    }

    // POST api/<controller>/<genre>
    public void Post(string genre, [FromBody]Movie value)
    {
    }

    // PUT api/<controller>/<genre>/5
    public void Put(string genre, int id, [FromBody]Movie value)
    {
    }

    // DELETE api/<controller>/<genre>/5
    public void Delete(string genre, int id)
    {
    }
}

So now MySite.Com/api/movie/horror/12345 might return a movie but I needed to add the optional parameter in every method. Now I find out they are stored by year as well.

config.Routes.MapHttpRoute(
    name: "MoviesWithGenreAndYear",
    routeTemplate: "api/{controller}/{genre}/{year}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

public class MovieController : ApiController
{
    // GET api/<controller>/<genre>/<year>
    public IEnumerable<Movie> Get(string genre, int year)
    {
        return MoviesDB.All(string genre, int year);
    }

    // GET api/<controller>/<genre>/<year>/5
    public Movie Get(string genre, int year, int id)
    {
        return MoviesDB.ThisSpecificOne(string genre, int year, id);
    }

    // POST api/<controller>/<genre>/<year>
    public void Post(string genre, int year, [FromBody]Movie value)
    {
    }

    // PUT api/<controller>/<genre>/<year>/5
    public void Put(string genre, int year, int id, [FromBody]Movie value)
    {
    }

    // DELETE api/<controller>/<genre>/<year>/5
    public void Delete(string genre, int year, int id)
    {
    }
}

This all works fine but with each new layer you would need to add a new parameter to each and every method. That doesn't feel very DRY

Could I inject these layers into the constructor instead of the methods themselves.

Perhaps I would like to initialize the controller differently based on these layers, so I would have a different repo based on genre and/or year or something like that.

Is there a solution for this?

3

3 Answers

2
votes

Did you consider using OData? Web Api has support for OData baked in, with it you could write your queries as url's: e.g. ?$filter=Genre eq 'horror'. If, for some reason or other you, don't want your data returned as OData but would like the query syntax of OData then you could:

  1. use Linq To QueryString: this lib gives you an extension method to IQueryable that parses the query string and applies the query to any IQueryable

  2. transform ODataQueryOptions into a query into your database (see this MSDN article for an example that translates the query into HQL)

0
votes

Is moving the optional parameters into the query string a viable option?

e.g. GET api/movie?genre=horror&year=2014

This would simplify your route and controller to:

config.Routes.MapHttpRoute(
    name: "Movies",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

public class MovieController : ApiController
{
    // GET api/<controller>?genre=<genre>&year=<year>
    public IEnumerable<Movie> Get(string genre = null, int? year = null)
    {
        return MoviesDB.All(string genre, int year);
    }

    // GET api/<controller>/5?genre=<genre>&year=<year>
    public Movie Get(int id, string genre = null, int? year = null)
    {
        return MoviesDB.ThisSpecificOne(string genre, int year, id);
    }

    // POST api/<controller>?genre=<genre>&year=<year>
    public void Post([FromBody]Movie value, string genre = null, int? year = null)
    {
    }

    // PUT api/<controller>/5?genre=<genre>&year=<year>
    public void Put(int id, [FromBody]Movie value, string genre = null, int? year = null)
    {
    }

    // DELETE api/<controller>/5?genre=<genre>&year=<year>
    public void Delete(int id, string genre = null, int? year = null)
    {
    }
}
0
votes

If you really mean the movies are stored by genre and the year, then I do believe your not so dry solution is actually correct. This does lead me to question the purpose of constructing such multi part identifier, as for instance the movie used in your example, surely the year and genre are just meta information about the movie, but not parts of the identifier. More generally speaking I'd really argue whether a composite identifier of more than two, or at least more than three parts is ever a good fit for any peace of software. A surrogate key would ease the development pain in such scenario.

Also discussing your concern about the DRY:ness of the repetition, I'd argue that as the primary key structure of an object seldom changes, it is not a really big issue. Even more so as a change in the primary key will thus always be a change breaking all backward compatibility.

As a gimmick, you could create a new class that contains the complex ID as such:

public class MovieId
{
    public int Id { get; set; }
    public int Yead { get; set; }
    public string Genre { get; set; }
}

And then make the controller methods as such:

    public Movie Get( [FromBody]MovieId id )
    {
        return MoviesDB.ThisSpecificOne( id );
    }

This works and now the code adheres well to DRY principle. The problem is, the complex type has to be a body parameter, so the query string will not be pretty and self explanatory anymore and you'll have to be creative with the routes to distinguish the different get methods.

Moving into a business layer or a DDD layer, this kind of composite key as a value object is a very common scenario and as the query string is of no concern there, it's actually a very viable and recommended solution.