Поддержка действий 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; }
}

Но мы не хотим, чтобы клиенты размещали ProductRating объект в коллекции Ratings. Интуитивно понятно, что оценка связана с коллекцией Products, и клиенту нужно только опубликовать значение оценки.

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

Действия имеют побочные эффекты на сервере. По этой причине они вызываются с помощью HTTP-запросов POST. Действия могут иметь параметры и типы возвращаемых значений, описанные в метаданных службы. Клиент отправляет параметры в тексте запроса, а сервер отправляет возвращаемое значение в тексте ответа. Чтобы вызвать действие "Оценить продукт", клиент отправляет 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. Метод имеет два параметра:

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

Если вы используете соглашения о маршрутизации по умолчанию, параметр ключа должен иметь имя "key". Также важно включить атрибут [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 добавьте действие в свойство Collection сущности.

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>

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

{
  "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.

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