Изменения REST Hypermedia URI в зависимости от контекста в веб-API (HATEOAS)

Я работаю над новой спокойной службой веб-API asp.net и провел некоторое время с некоторыми курсами Pluralsight по этому вопросу. Один из лучших глубоко погружается в дизайн и реализацию гипермедиа (HATEOAS).

Я следил за реализацией в видео, так как она была очень простой, и, будучи новичком в mvc/web api, было действительно полезно увидеть, как она работает от начала до конца.

Однако, как только я начал немного углубляться в свою реализацию, использование UrlHelper() для вычисления ссылки для возврата начало разваливаться.

В приведенном ниже коде у меня есть простой Get(), который возвращает набор определенных ресурсов, а затем Get(int id), который позволяет вернуть отдельный ресурс.

Все результаты проходят через ModelFactory, который преобразует мои POCO для возврата результатов и обратно при публикации, исправлении и размещении.

Я пытался сделать это более изощренным способом, позволив ModelFactory обрабатывать всю логику создания ссылок, поскольку она создается с использованием объекта Request.

Теперь я знаю, что мог бы решить все это, просто обрабатывая генерацию/включение ссылок прямо внутри моих методов, и, возможно, это ответ, но мне было любопытно, как другие справляются с этим.

Моя цель:

1) В наборы результатов (т. е. коллекции результатов, возвращаемых функцией «Get()»), чтобы включить общее количество элементов, общее количество страниц, следующую и предыдущую страницы по мере необходимости. Я реализовал собственный конвертер json, чтобы сбрасывать пустые ссылки на землю. Например, я не печатаю «prevPage», когда вы находитесь на первой странице. Это работает сегодня.

2) В отдельные результаты (т. е. результат, возвращаемый «Get (id)»), чтобы включить ссылки на себя, включить rel, метод, который представляет ссылка, и независимо от того, является ли он шаблонным. Это работает сегодня.

Что сломано:

Как вы увидите в выводе ниже, две вещи «неправильные». Когда вы смотрите на ссылку «POST» для нового отдельного элемента, URL-адрес правильный. Это потому, что я удаляю последнюю часть URI (отбрасывая идентификатор ресурса). Однако при возврате набора результатов URI для «POST» теперь неверен. Это связано с тем, что маршрут не включал идентификатор отдельного ресурса, поскольку был вызван «Get ()», а не «Get (id)».

Опять же, реализация может быть изменена, чтобы создавать разные ссылки в зависимости от того, какой метод был выбран, вытягивая их из фабрики в контроллер, но мне хотелось бы верить, что я просто упускаю что-то очевидное.

Любые указатели для этого новичка на маршрутизацию и веб-API?

Получить контроллер()

[HttpGet]
    public IHttpActionResult Get(int pageSize = 50, int page = 0)
    {
        if (pageSize == 0)
        {
            pageSize = 50;
        }

        var links = new List<LinkModel>();

        var baseQuery = _deliverableService.Query().Select();
        var totalCount = baseQuery.Count();
        var totalPages = Math.Ceiling((double) totalCount / pageSize);

        var helper = new UrlHelper(Request);
        if (page > 0)
        {
            links.Add(TheModelFactory.CreateLink(helper.Link("Deliverables",
                new
                {
                    pageSize,
                    page = page - 1
                }),
                "prevPage"));
        }
        if (page < totalPages - 1)
        {
            links.Add(TheModelFactory.CreateLink(helper.Link("Deliverables",
                new
                {
                    pageSize,
                    page = page + 1
                }),
                "nextPage"));
        }

        var results = baseQuery
            .Skip(page * pageSize)
            .Take(pageSize)
            .Select(p => TheModelFactory.Create(p))
            .ToList();

        return Ok(new DeliverableResultSet
                  {
                      TotalCount = totalCount,
                      TotalPages = totalPages,
                      Links = links,
                      Results = results
                  }
            );
    }

Контроллер Get(id)

        [HttpGet]
    public IHttpActionResult GetById(int id)
    {
        var entity = _deliverableService.Find(id);

        if (entity == null)
        {
            return NotFound();
        }

        return Ok(TheModelFactory.Create(entity));
    }

Создание фабрики моделей()

 public DeliverableModel Create(Deliverable deliverable)
    {
        return new DeliverableModel
               {
                   Links = new List<LinkModel>
                           {
                               CreateLink(_urlHelper.Link("deliverables",
                                   new
                                   {
                                       id = deliverable.Id
                                   }),
                                   "self"),
                                   CreateLink(_urlHelper.Link("deliverables",
                                   new
                                   {
                                       id = deliverable.Id
                                   }),
                                   "update", "PUT"),
                                   CreateLink(_urlHelper.Link("deliverables",
                                   new
                                   {
                                       id = deliverable.Id
                                   }),
                                   "delete", "DELETE"),
                               CreateLink(GetParentUri() , "new", "POST")
                           },
                   Description = deliverable.Description,
                   Name = deliverable.Name,
                   Id = deliverable.Id
               };
    }

ModelFactory CreateLink()

public LinkModel CreateLink(string href, string rel, string method = "GET", bool isTemplated = false)
    {
        return new LinkModel
               {
                   Href = href,
                   Rel = rel,
                   Method = method,
                   IsTemplated = isTemplated
               };
    }

Результат Get()

{
totalCount: 10,
totalPages: 4,
links: [{
    href: "https://localhost/Test.API/api/deliverables?pageSize=2&page=1",
    rel: "nextPage"
}],
results: [{
    links: [{
        href: "https://localhost/Test.API/api/deliverables/2",
        rel: "self"
    },
    {
        href: "https://localhost/Test.API/api/deliverables/2",
        rel: "update",
        method: "PUT"
    },
    {
        href: "https://localhost/Test.API/api/deliverables/2",
        rel: "delete",
        method: "DELETE"
    },
    {
        href: "https://localhost/Test.API/api/",
        rel: "new",
        method: "POST"
    }],
    name: "Deliverable1",
    description: "",
    id: 2
},
{
    links: [{
        href: "https://localhost/Test.API/api/deliverables/3",
        rel: "self"
    },
    {
        href: "https://localhost/Test.API/api/deliverables/3",
        rel: "update",
        method: "PUT"
    },
    {
        href: "https://localhost/Test.API/api/deliverables/3",
        rel: "delete",
        method: "DELETE"
    },
    {
        href: "https://localhost/Test.API/api/",
        rel: "new",
        method: "POST"
    }],
    name: "Deliverable2",
    description: "",
    id: 3
}]

}

Результат Get(id)

{
links: [{
    href: "https://localhost/Test.API/api/deliverables/2",
    rel: "self"
},
{
    href: "https://localhost/Test.API/api/deliverables/2",
    rel: "update",
    method: "PUT"
},
{
    href: "https://localhost/Test.API/api/deliverables/2",
    rel: "delete",
    method: "DELETE"
},
{
    href: "https://localhost/Test.API/api/deliverables/",
    rel: "new",
    method: "POST"
}],
name: "Deliverable2",
description: "",
id: 2

}

Обновление 1

В пятницу я нашел и начал реализовывать решение, описанное здесь: http://benfoster.io/blog/generating-hypermedia-links-in-aspnet-web-api. Решение Бена очень хорошо продумано и позволяет мне поддерживать мои модели (хранящиеся в общедоступной библиотеке для использования в других решениях .NET (например, RestSharp)) и позволяет мне использовать AutoMapper вместо реализации моей собственной ModelFactory. AutoMapper потерпел неудачу, когда мне нужно было работать с контекстными данными (такими как запрос). Поскольку моя реализация HATEOAS была перенесена в MessageHandler, AutoMapper снова становится приемлемым вариантом.


person James Legan    schedule 18.04.2014    source источник


Ответы (2)


Я расширил решение Бена (ссылка ниже), и оно отвечает всем моим требованиям. Я считаю, что «обогащение» возвращаемого результата в обработчиках необходимыми данными HATEOAS — это путь. Единственный раз, когда мне нужно установить ссылки непосредственно вне обработчика, это когда я занимаюсь такими вещами, как пейджинг, где только контроллер имеет необходимую информацию для принятия решения о том, как должны выглядеть ссылки. В этот момент я просто добавляю ссылку на коллекцию в моей модели, которая передается обработчику, где можно добавить еще больше ссылок.

http://benfoster.io/blog/generating-hypermedia-links-in-aspnet-web-api

person James Legan    schedule 24.04.2014

Я расширил подход Бена, используя ASP.NET Core. Мой подход использует ResultFilter, где ответ украшен ссылками. Конструктор ссылок (обогатитель) используется для каждой модели, поддерживающей гипермедиа-ссылки. Поскольку официального стандарта форматирования ссылок не существует, используются определения Paypal. Пожалуйста, проверьте мой блог Создание ссылок Hypermedia для ASP.NET Core Web API

person Sotiris    schedule 04.09.2017
comment
Ссылка на решение приветствуется, но убедитесь, что ваш ответ полезен и без нее: добавьте контекст вокруг ссылки, чтобы другие пользователи иметь некоторое представление о том, что это такое и почему оно там, а затем процитировать наиболее релевантную часть страницы, на которую вы ссылаетесь, в случае, если целевая страница недоступна. Ответы, которые представляют собой не более чем ссылку, могут быть удалены. - person g00glen00b; 04.09.2017