【问题标题】:Best way to represent deep tree of resources?表示深层资源树的最佳方式?
【发布时间】:2019-06-25 02:56:14
【问题描述】:

是否有一种简单的方法可以重用路由“段”以及用于验证和使用段中参数的控制器逻辑?

例如,我想像这样表示我的资源:

  • 房屋
    • {住宅地址}
      • 架构
        • 屋顶
        • 壁板
      • 房间
        • 生活
          • 窗户
        • 餐饮
          • 窗户
        • 厨房
          • 窗户
        • 卧室
          • [0]
            • 窗户
          • [1]
            • 窗户

以下是一种可行的方法。 有什么更好的方法?

[Route("[controller]")]
[ApiController]
class HousesController : ControllerBase
{
    [HttpGet]
    public IEnumerable<House> Get() => _housesRepo.GetAll();

    [HttpGet("{houseId}")]
    public ActionResult<House> Get(string houseId)
    {
        if (!_housesRepo.TryGetHouse(houseId, out var house))
            return NotFound("Bad house ID");
    }
}

class RoomsController : ControllerBase
{
    [HttpGet("~houses/{houseId}/rooms/bedrooms/{bedroomId}")]
    public ActionResult<Room> GetBedroom(string houseId, int bedroomId)
    {
        if (!_housesRepo.TryGetHouse(houseId, out var house))
            return NotFound("Bad house ID");
        var bedrooms = house.Bedrooms;
        if (bedroomId < 0 || bedroomId >= bedrooms.Length)
            return NotFound("Bad bedroom ID");
        return bedrooms[bedroomId];
    }

    [HttpGet("~houses/{houseId}/rooms/{roomClass}")]
    public ActionResult<Room> GetRoom(string houseId, string roomClass)
    {
        if (!_housesRepo.TryGetHouse(houseId, out var house))
            return NotFound("Bad house ID");
        var roomsByClass = house.RoomsByClass;
        if (!roomsByClass.TryGetValue(roomClass, out var room))
            return NotFound("Bad room class");
        return room;
    }
}

class ArchitectureController : ControllerBase
{
    [HttpGet("~houses/{houseId}/architecture")]
    public ActionResult<string[]> Get(string houseId)
    {
        if (!_housesRepo.TryGetHouse(houseId, out _))
            return NotFound("Bad house ID");
        return new [] { "roof", "siding" };
    }

    [HttpGet("~houses/{houseId}/architecture/roof")]
    public ActionResult<Roof> GetRoof(string houseId)
    {
        if (!_housesRepo.TryGetHouse(houseId, out var house))
            return NotFound("Bad house ID");
        return house.Architecture.Roof;
    }

    [HttpGet("~houses/{houseId}/architecture/siding")]
    public ActionResult<Siding> GetSiding(string houseId)
    {
        if (!_housesRepo.TryGetHouse(houseId, out var house))
            return NotFound("Bad house ID");
        return house.Architecture.Siding;
    }
}

但请注意有多少重复:

  • ~houses/{houseId}路段
  • 验证houseId 并检索关联的House

随着资源变得越来越复杂,重复只会变得更糟。

微软似乎在推动人们拥有一个非常扁平的层次结构。例如:

  • ~houses/{houseId}
  • ~architecture/{architectureId}
  • ~rooms/{roomId}

但这只会隐藏问题。例如,Room 仅作为House 的组成部分才有意义,因此{roomId} 还必须包含有关房屋ID 的信息。然后我需要解析roomId 以提取houseId 以验证和提取关联的House 资源。

如果我能有这个就好了:

[Segment("HOUSE")]
[HttpGet("houses/{houseId}")]
public ActionResult<House> GetHouse(string houseId) => _housesRepo
    .TryGetValue(houseId, out var house)
    ? new ActionResult<House>(house)
    : NotFound("Bad house ID");

[HttpGet("[HOUSE]/architecture")]
public string[] GetArchitectureCategories(House house) => new [] { "roof", "siding" };

[HttpGet("[HOUSE]/architecture/roof")]
public Roof GetRoof(House house) => house.Architecture.Roof;

[HttpGet("[HOUSE]/architecture/siding")]
public Siding GetSiding(House house) => house.Architecture.Siding;

[Segment("BEDROOM")]
[HttpGet("[HOUSE]/rooms/bedrooms/{bedroomId}")]
public Room GetBedroom(House house, int bedroomId) => house.Bedrooms[bedroomId];

[Segment("ROOM")]
[HttpGet("[HOUSE]/rooms/{roomClass}")]
public ActionResult<Room> GetRoom(House house, string roomClass) => house
    .RoomsByClass
    .TryGetValue(roomClass, out var room)
    ? new ActionResult<Room>(room)
    : NotFound("Bad room class");

这将很容易建立。例如,我可以快速添加门窗控制器:

[HttpGet("[ROOM|BEDROOM]/doors")]
public Door[] GetDoors(Room room) => room.Doors;

[HttpGet("[ROOM|BEDROOM]/windows")]
public Window[] GetWindows(Room room) => room.Windows;

【问题讨论】:

    标签: rest asp.net-core asp.net-core-webapi


    【解决方案1】:

    正如您所说,这是不可能的,但仍然可以实现。首先,您希望依赖应用于控制器类本身的路​​由前缀。换句话说,而不是:

    [HttpGet("~houses/{houseId}/rooms/bedrooms/{bedroomId}")]
    public ActionResult<Room> GetBedroom(string houseId, int bedroomId)
    

    用途:

    [Route("houses/{houseId}/rooms")]
    public class RoomsController : ControllerBase
    {
        [HttpGet("bedrooms/{bedroomId}")]
        public ActionResult<Room> GetBedroom(int bedroomId)
    

    然后,还要注意我在那里取出了houseId 参数。这个逻辑可以移出到OnActionExecutionAsync的覆盖中:

    private House House { get; set; }
    
    public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        if (context.RouteData.Values.TryGetValue("houseId", out var houseId))
        {
            House = await _context.Set<House>().FindAsync(houseId);
            if (House == null)
                context.Result = NotFound();
        }
    
        await base.OnActionExecutionAsync(context, next);
    }
    

    这看起来有点复杂,但所做的只是从路径中拉出houseId 参数并尝试找到相关联的房子。如果没有找到,则立即返回 404,否则,控制器上的 House 属性将设置为该房屋。因此,您在此控制器中的每个操作现在都可以依赖可用的 House 属性并可以相应地使用它,而只需关注自己的特定行为。

    【讨论】:

    • 太棒了。细节(比如参数名称以及如何将参数转化为对象)可以封装抽象掉——我喜欢!
    • 基于使用FindAsync,传递houseId 的内容无关紧要。例如,如果有人尝试了/houses/foo/rooms/bedrooms/1,它应该只会导致 404。但是,您可能仍想为您的参数添加路由约束:house/{houseId:int}/roomsbedrooms/{bedroomId:int}
    猜你喜欢
    • 2010-10-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多