Поддержка действий OData в веб-API ASP.NET 2

Майк Уосон

Скачивание завершенного проекта

В OData действия — это способ добавить функции на стороне сервера, которые не могут быть легко определены как операции CRUD над сущностями. Некоторые способы использования действий включают:

  • Реализация сложных транзакций.
  • Одновременное управление несколькими сущностями.
  • Разрешение обновлений только для определенных свойств сущности.
  • Отправка информации, которая не определена в сущности, на сервер.

Версии программного обеспечения, используемые в руководстве

  • Веб-API 2
  • OData версии 3
  • Entity Framework 6

Пример: оценка продукта

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

Ниже приведена модель, которую можно использовать для представления рейтингов в Entity Framework:

public class ProductRating
{
    public int ID { get; set; }

    [ForeignKey("Product")]
    public int ProductID { get; set; }
    public virtual Product Product { get; set; }  // Navigation property

    public int Rating { get; set; }
}

Но мы не хотим, чтобы клиенты размещали объект POST ProductRating в коллекцию "Оценки". Интуитивно, оценка связана с коллекцией Продуктов, и клиенту нужно только опубликовать значение рейтинга.

Поэтому вместо использования обычных операций CRUD мы определяем действие, которое клиент может вызвать в продукте. В терминологии OData действие привязано к сущностям Product.

Действия имеют побочные эффекты на сервере. По этой причине они вызываются с помощью HTTP-запросов POST. Действия могут иметь параметры и типы возвращаемых данных, описанные в метаданных службы. Клиент отправляет параметры в тексте запроса, а сервер отправляет возвращаемое значение в тексте ответа. Чтобы вызвать действие "Rate Product", клиент отправляет POST в URI, как показано ниже:

http://localhost/odata/Products(1)/RateProduct

Данные в запросе POST — это просто оценка продукта:

{"Rating":2}

Объявление действия в модели данных сущностей

В конфигурации веб-API добавьте действие в модель данных сущности (EDM):

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
        builder.EntitySet<Product>("Products");
        builder.EntitySet<Supplier>("Suppliers");
        builder.EntitySet<ProductRating>("Ratings");

        // New code: Add an action to the EDM, and define the parameter and return type.
        ActionConfiguration rateProduct = builder.Entity<Product>().Action("RateProduct");
        rateProduct.Parameter<int>("Rating");
        rateProduct.Returns<double>();

        config.Routes.MapODataRoute("odata", "odata", builder.GetEdmModel());
    }
}

Этот код определяет RateProduct как действие, которое может выполняться в сущностях Product. Он также объявляет, что действие принимает параметр int с именем "Rating" и возвращает значение int .

Добавление действия в контроллер

Действие RateProduct привязано к сущностям Product. Чтобы реализовать действие, добавьте метод с именем RateProduct контроллера Products:

[HttpPost]
public async Task<IHttpActionResult> RateProduct([FromODataUri] int key, ODataActionParameters parameters)
{
    if (!ModelState.IsValid)
    {
        return BadRequest();
    }

    int rating = (int)parameters["Rating"];

    Product product = await db.Products.FindAsync(key);
    if (product == null)
    {
        return NotFound();
    }

    product.Ratings.Add(new ProductRating() { Rating = rating });
    db.SaveChanges();

    double average = product.Ratings.Average(x => x.Rating);

    return Ok(average);
}

Обратите внимание, что имя метода соответствует имени действия в EDM. Метод имеет два параметра:

  • ключ: ключ продукта для оценки.
  • параметры: словарь значений параметров действия.

При использовании соглашений маршрутизации по умолчанию ключевой параметр должен называться "ключ". Также важно включить атрибут [FromOdataUri] , как показано ниже. Этот атрибут сообщает веб-API использовать правила синтаксиса OData при анализе ключа из URI запроса.

Используйте словарь параметров для получения параметров действия:

if (!ModelState.IsValid)
{
    return BadRequest();
}
int rating = (int)parameters["Rating"];

Если клиент отправляет параметры действия в правильном формате, значение ModelState.IsValid равно true. В этом случае можно использовать словарь ODataActionParameters для получения значений параметров. В этом примере RateProduct действие принимает один параметр с именем "Rating".

Метаданные действия

Чтобы просмотреть метаданные службы, отправьте запрос GET в /odata/$metadata. Ниже приведена часть метаданных, объявляющих RateProduct действие:

<FunctionImport Name="RateProduct" m:IsAlwaysBindable="true" IsBindable="true" ReturnType="Edm.Double">
  <Parameter Name="bindingParameter" Type="ProductService.Models.Product"/>
  <Parameter Name="Rating" Nullable="false" Type="Edm.Int32"/>
</FunctionImport>

Элемент FunctionImport объявляет действие. Большинство полей являются самообъяснительными, но два стоит отметить:

  • IsBindable означает, что действие может быть выполнено на целевой сущности хотя бы в некоторых случаях.
  • IsAlwaysBindable означает, что действие всегда может быть вызвано на целевой сущности.

Разница заключается в том, что некоторые действия всегда доступны для клиентов, но другие действия могут зависеть от состояния объекта. Например, предположим, что вы определяете действие "Покупка". Вы можете приобрести только товар, который находится на складе. Если элемент находится вне запасов, клиент не может вызвать это действие.

При определении EDM метод Action создает всегда привязываемое действие:

builder.Entity<Product>().Action("RateProduct"); // Always bindable

Далее в этом разделе я поговорим о не всегда привязанных действиях (также называемых временными действиями).

Вызов действия

Теперь давайте посмотрим, как клиент вызовет это действие. Предположим, клиент хочет дать оценку 2 продукту с идентификатором = 4. Ниже приведен пример сообщения запроса с использованием формата JSON для текста запроса:

POST http://localhost/odata/Products(4)/RateProduct HTTP/1.1
Content-Type: application/json
Content-Length: 12

{"Rating":2}

Ниже приведено ответное сообщение:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
DataServiceVersion: 3.0
Date: Tue, 22 Oct 2013 19:04:00 GMT
Content-Length: 89

{
  "odata.metadata":"http://localhost:21900/odata/$metadata#Edm.Double","value":2.75
}

Привязка действия к набору сущностей

В предыдущем примере действие привязано к одной сущности: клиент оценивает один продукт. Вы также можете привязать действие к коллекции сущностей. Просто внесите следующие изменения:

В EDM добавьте действие в свойство Коллекция сущности.

var rateAllProducts = builder.Entity<Product>().Collection.Action("RateAllProducts");

В методе контроллера опустите параметр ключа .

[HttpPost]
public int RateAllProducts(ODataActionParameters parameters)
{
    // ....
}

Теперь клиент вызывает действие в наборе сущностей Products:

http://localhost/odata/Products/RateAllProducts

Действия с параметрами коллекции

Действия могут иметь параметры, которые принимают коллекцию значений. В EDM используйте CollectionParameter<T> для объявления параметра.

rateAllProducts.CollectionParameter<int>("Ratings");

Это объявляет параметр с именем "Ratings", который принимает коллекцию значений int . В методе контроллера вы по-прежнему получаете значение параметра из объекта ODataActionParameters, но теперь значение является значением int< iCollection>:

[HttpPost]
public void RateAllProducts(ODataActionParameters parameters)
{
    if (!ModelState.IsValid)
    {
        throw new HttpResponseException(HttpStatusCode.BadRequest);
    }

    var ratings = parameters["Ratings"] as ICollection<int>; 

    // ...
}

Временные действия

В примере RateProduct пользователи всегда могут оценить продукт, поэтому действие всегда доступно. Но некоторые действия зависят от состояния сущности. Например, в службе аренды видео действие "CheckOut" не всегда доступно. (Это зависит от того, доступна ли копия этого видео.) Этот тип действия называется временным действием.

В метаданных службы временное действие имеет IsAlwaysBindable равно false. Это фактически значение по умолчанию, поэтому метаданные будут выглядеть следующим образом:

<FunctionImport Name="CheckOut" IsBindable="true">
    <Parameter Name="bindingParameter" Type="ProductsService.Models.Product" />
</FunctionImport>

Вот почему это важно: если действие является временным, сервер должен сообщить клиенту, когда действие доступно. Это делается путем включения ссылки на действие в сущность. Ниже приведен пример сущности 'Фильм':

{
  "odata.metadata":"http://localhost:17916/odata/$metadata#Movies/@Element",
  "#CheckOut":{ "target":"http://localhost:17916/odata/Movies(1)/CheckOut" },
  "ID":1,"Title":"Sudden Danger 3","Year":2012,"Genre":"Action"
}

Свойство "#CheckOut" содержит ссылку на действие CheckOut. Если действие недоступно, сервер опустит ссылку.

Чтобы объявить временное действие в EDM, вызовите метод TransientAction :

var checkoutAction = builder.Entity<Movie>().TransientAction("CheckOut");

Кроме того, необходимо указать функцию, которая возвращает ссылку действия для данной сущности. Задайте эту функцию путем вызова HasActionLink. Вы можете написать функцию как лямбда-выражение:

checkoutAction.HasActionLink(ctx =>
{
    var movie = ctx.EntityInstance as Movie;
    if (movie.IsAvailable) {
        return new Uri(ctx.Url.ODataLink(
            new EntitySetPathSegment(ctx.EntitySet), 
            new KeyValuePathSegment(movie.ID.ToString()),
            new ActionPathSegment(checkoutAction.Name)));
    }
    else
    {
        return null;
    }
}, followsConventions: true);

Если действие доступно, лямбда-выражение возвращает ссылку на действие. Сериализатор OData включает эту ссылку при сериализации сущности. Если действие недоступно, функция возвращается null.

Дополнительные ресурсы