代表深层资源树的最佳方法?

时间:2019-01-31 15:37:52

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

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

例如,我想代表我的资源与此类似:

  • 房屋
    • {住所地址}
      • 架构
        • 屋顶
        • 侧墙
      • 房间
        • 生活
          • windows
        • 用餐
          • windows
        • 厨房
          • windows
        • 卧室
          • [0]
            • windows
          • [1]
            • windows

以下是可以完成的一种方法。 什么是更好的方法?

[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

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

Microsoft似乎将人们推向了一个非常扁平的层次结构。例如:

  • ~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;

1 个答案:

答案 0 :(得分: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属性可用并可以相应地使用它,而不必着重于自己的特定行为。