【问题标题】:Dry method for webapi with multiple routing levels多路由级别的webapi干法
【发布时间】:2014-03-05 10:08:13
【问题描述】:

当我们对每个方法都有多个所需的路由级别时,有没有办法简化?

我有一个假设的 WebAPI 项目,我正在使用它来对问题进行一般性的研究。它为我们提供了一些来源的电影。

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)
    {
    }
}

但是让我们说一些愚蠢的原因电影是按流派存储的。所以你需要流派 + id 组合。

我假设你会这样做

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)
    {
    }
}

所以现在MySite.Com/api/movie/horror/12345 可能会返回一部电影,但我需要在每个方法中添加可选参数。现在我发现它们也是按年份存储的。

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)
    {
    }
}

这一切都很好,但是对于每个新层,您都需要为每个方法添加一个新参数。感觉不是很DRY

我能否将这些层注入构造函数而不是方法本身。

也许我想根据这些层以不同的方式初始化控制器,所以我会根据流派和/或年份或类似的东西有一个不同的 repo。

有解决办法吗?

【问题讨论】:

    标签: c# asp.net-web-api dry asp.net-web-api-routing asp.net-web-api2


    【解决方案1】:

    您是否考虑过使用 OData? Web Api 支持内置的 OData,您可以使用它将查询编写为 url:例如?$filter=流派 eq '恐怖'。如果出于某种原因或其他原因,您不希望您的数据以 OData 形式返回,但希望使用 OData 的查询语法,那么您可以:

    1. 使用Linq To QueryString:这个库为您提供了一个 IQueryable 的扩展方法,它解析查询字符串并将查询应用于任何 IQueryable

    2. 将 ODataQueryOptions 转换为对数据库的查询(有关将查询转换为 HQL 的示例,请参阅 this MSDN article

    【讨论】:

    • 我没有足够的声誉来添加超过 2 个链接。您可以在 asp.net/web-api/overview/odata-support-in-aspnet-web-api 找到有关 OData 支持的信息
    • 这很整洁。我支持你,但我也对关于参数不是可选的实际路由 url 的答案感兴趣,当你想限制搜索时,OData 解决方案似乎效果更好。请注意,我不是在构建电影服务,我只是将其用作示例。我在问,如果您必须将每次调用限制为特定的子集/数据库等,该怎么办?
    • 可能有点繁重的解决方案,但您可以实现 IHttpControllerSelector 和 IHttpActionSelector:这两个让您可以控制对给定 HttpRequestMessage 执行哪个控制器的操作
    【解决方案2】:

    将可选参数移动到查询字符串中是否可行?

    例如GET api/movie?genre=horror&amp;year=2014

    这会将您的路由和控制器简化为:

    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)
        {
        }
    }
    

    【讨论】:

    • 我怀疑正如问题特别提到的那样“电影是按类型存储的。所以你需要流派+ID组合。”,这实际上意味着它们是主键的一部分(在 RDBMS 中)或其他对象的身份。因此,我真的看不出它们怎么可能是可选的。
    【解决方案3】:

    如果您的意思是电影是按类型和年份存储的,那么我相信您的不那么枯燥的解决方案实际上是正确的。这确实让我质疑构建这种多部分标识符的目的,例如您的示例中使用的电影,当然年份和流派只是关于电影的元信息,而不是标识符的一部分。更一般地说,我真的会争论两个以上或至少三个以上部分的复合标识符是否适合任何软件和平。代理键可以减轻这种情况下的开发痛苦。

    还讨论了您对重复的 DRY:ness 的担忧,我认为由于对象的主键结构很少更改,因此这并不是一个真正的大问题。更重要的是,主键的更改将始终是破坏所有向后兼容性的更改。

    作为一个噱头,您可以创建一个包含复杂 ID 的新类:

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

    然后让控制器方法如下:

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

    这行得通,现在代码很好地遵守了 DRY 原则。问题是,复杂类型必须是 body 参数,因此查询字符串将不再漂亮和不言自明,您必须创造性地使用路由来区分不同的 get 方法。

    移动到业务层或DDD层,这种复合键作为值对象是一种非常常见的场景,因为查询字符串在那里并不重要,它实际上是一个非常可行和推荐的解决方案。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-02-09
      • 2019-11-16
      • 1970-01-01
      • 2016-06-28
      • 2013-03-11
      • 1970-01-01
      • 2015-02-12
      • 2017-06-26
      相关资源
      最近更新 更多