Использование $select, $expand и $value в веб-API ASP.NET 2 OData

Майк Уосон

Обзор и примеры кода для параметров $expand, $select и $value в веб-API OData 2 для ASP.NET 4.x. Эти параметры позволяют клиенту управлять представлением, которое он возвращает с сервера.

  • $expand приводит к тому, что связанные сущности включаются непосредственно в ответ.
  • $select выбирает подмножество свойств для включения в ответ.
  • $value получает необработанное значение свойства.

Пример схемы

В этой статье я буду использовать службу OData, которая определяет три сущности: Product, Supplier и Category. Каждый продукт имеет одну категорию и один поставщик.

Схема, показывающая пример схемы для службы данных O, определение продуктов, поставщиков и категорий в качестве сущностей.

Ниже приведены классы C#, определяющие модели сущностей:

public class Supplier
{
    [Key]
    public string Key {get; set; }
    public string Name { get; set; }
}
public class Category
{
    public int ID { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Product> Products { get; set; }
}

public class Product
{
    public int ID { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }

    [ForeignKey("Category")]
    public int CategoryId { get; set; }
    public Category Category { get; set; }

    [ForeignKey("Supplier")]
    public string SupplierId { get; set; }
    public virtual Supplier Supplier { get; set; }
}

Обратите внимание, что Product класс определяет свойства навигации для Supplier и Category. Класс Category определяет свойство навигации для продуктов в каждой категории.

Чтобы создать конечную точку OData для этой схемы, используйте шаблон Visual Studio 2013, как описано в статье "Создание конечной точки OData в ASP.NET веб-API". Добавьте отдельные контроллеры для продукта, категории и поставщика.

Включение $expand и $select

В Visual Studio 2013 шаблон OData веб-API создает контроллер, который автоматически поддерживает $expand и $select. Для справки ниже приведены требования к поддержке $expand и $select в контроллере.

Для коллекций метод контроллера Get должен возвращать IQueryable.

[Queryable]
public IQueryable<Category> GetCategories()
{
    return db.Categories;
}

Для отдельных сущностей возвращается объект SingleResult<T>, где T — это IQueryable , содержащий ноль или одну сущность.

[Queryable]
public SingleResult<Category> GetCategory([FromODataUri] int key)
{
    return SingleResult.Create(db.Categories.Where(c => c.ID == key));
}

Кроме того, украшайте Get методы атрибутом [Queryable] , как показано в предыдущих фрагментах кода. Кроме того, вызовите EnableQuerySupport в объекте HttpConfiguration при запуске. (Дополнительные сведения см. в разделе "Включение параметров запроса OData".)

Использование $expand

При запросе сущности или коллекции OData ответ по умолчанию не включает связанные сущности. Например, ниже приведен ответ по умолчанию для набора сущностей "Категории":

{
  "odata.metadata":"http://localhost/odata/$metadata#Categories",
  "value":[
    {"ID":1,"Name":"Apparel"},
    {"ID":2,"Name":"Toys"}
  ]
}

Как видно, ответ не содержит никаких продуктов, даже если сущность категории имеет ссылку навигации "Продукты". Однако клиент может использовать $expand для получения списка продуктов для каждой категории. Параметр $expand включается в строку запроса:

GET http://localhost/odata/Categories?$expand=Products

Теперь сервер будет включать продукты для каждой категории в соответствие с категориями. Ниже приведены полезные данные ответа:

{
  "odata.metadata":"http://localhost/odata/$metadata#Categories",
  "value":[
    {
      "Products":[
        {"ID":1,"Name":"Hat","Price":"15.00","CategoryId":1,"SupplierId":"CTSO"},
        {"ID":2,"Name":"Scarf","Price":"12.00","CategoryId":1,"SupplierId":"CTSO"},
        {"ID":3,"Name":"Socks","Price":"5.00","CategoryId":1,"SupplierId":"FBRK"}
      ],
      "ID":1,
      "Name":"Apparel"
    },
    {
      "Products":[
        {"ID":4,"Name":"Yo-yo","Price":"4.95","CategoryId":2,"SupplierId":"WING"},
        {"ID":5,"Name":"Puzzle","Price":"8.00","CategoryId":2,"SupplierId":"WING"}
      ],
      "ID":2,
      "Name":"Toys"
    }
  ]
}

Обратите внимание, что каждая запись в массиве value содержит список продуктов.

Параметр $expand принимает разделенный запятыми список свойств навигации для расширения. Следующий запрос расширяет как категорию, так и поставщиков для продукта.

GET http://localhost/odata/Products(1)?$expand=Category,Supplier

Ниже приведен текст ответа:

{
  "odata.metadata":"http://localhost/odata/$metadata#Products/@Element",
  "Category": {"ID":1,"Name":"Apparel"},
  "Supplier":{"Key":"CTSO","Name":"Contoso, Ltd."},
  "ID":1,
  "Name":"Hat",
  "Price":"15.00",
  "CategoryId":1,
  "SupplierId":"CTSO"
}

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

GET http://localhost/odata/Categories(1)?$expand=Products/Supplier

Ниже приведен текст ответа:

{
  "odata.metadata":"http://localhost/odata/$metadata#Categories/@Element",
  "Products":[
    {
      "Supplier":{"Key":"CTSO","Name":"Contoso, Ltd."},
      "ID":1,"Name":"Hat","Price":"15.00","CategoryId":1,"SupplierId":"CTSO"
    },
    {
      "Supplier":{"Key":"CTSO","Name":"Contoso, Ltd."},
      "ID":2,"Name":"Scarf","Price":"12.00","CategoryId":1,"SupplierId":"CTSO"
    },{
      "Supplier":{
        "Key":"FBRK","Name":"Fabrikam, Inc."
      },"ID":3,"Name":"Socks","Price":"5.00","CategoryId":1,"SupplierId":"FBRK"
    }
  ],"ID":1,"Name":"Apparel"
}

По умолчанию веб-API ограничивает максимальную глубину расширения до 2. Это препятствует клиенту отправлять сложные запросы, например $expand=Orders/OrderDetails/Product/Supplier/Region, которые могут быть неэффективными для запроса и создания больших ответов. Чтобы переопределить значение по умолчанию, задайте свойство MaxExpansionDepth в атрибуте [Queryable] .

[Queryable(MaxExpansionDepth=4)]
public IQueryable<Category> GetCategories()
{
    return db.Categories;
}

Дополнительные сведения о параметре $expand см. в разделе Expand System Query Option ($expand) официальной документации по OData.

Использование $select

Параметр $select указывает подмножество свойств для включения в текст ответа. Например, чтобы получить только имя и цену каждого продукта, используйте следующий запрос:

GET http://localhost/odata/Products?$select=Price,Name

Ниже приведен текст ответа:

{
  "odata.metadata":"http://localhost/odata/$metadata#Products&$select=Price,Name",
  "value":[
    {"Price":"15.00","Name":"Hat"},
    {"Price":"12.00","Name":"Scarf"},
    {"Price":"5.00","Name":"Socks"},
    {"Price":"4.95","Name":"Yo-yo"},
    {"Price":"8.00","Name":"Puzzle"}
  ]
}

Вы можете объединить $select и $expand в одном запросе. Обязательно включите свойство "расширенное" в параметр $select. Например, следующий запрос получает имя продукта и поставщика.

GET http://localhost/odata/Products?$select=Name,Supplier&$expand=Supplier

Ниже приведен текст ответа:

{
  "odata.metadata":"http://localhost/odata/$metadata#Products&$select=Name,Supplier",
  "value":[
    {
      "Supplier":{"Key":"CTSO","Name":"Contoso, Ltd."},
      "Name":"Hat"
    },
    {
      "Supplier":{"Key":"CTSO","Name":"Contoso, Ltd."},
      "Name":"Scarf"
    },
    {
      "Supplier":{"Key":"FBRK","Name":"Fabrikam, Inc."},
      "Name":"Socks"
    },
    {
      "Supplier":{"Key":"WING","Name":"Wingtip Toys"},
      "Name":"Yo-yo"
    },
    {
      "Supplier":{"Key":"WING","Name":"Wingtip Toys"},
      "Name":"Puzzle"
   }
  ]
}

Вы также можете выбрать свойства в развернутом свойстве. Следующий запрос подробно описывает продукты, выбирая названия категории и продукта.

GET http://localhost/odata/Categories?$expand=Products&$select=Name,Products/Name

Ниже приведен текст ответа:

{
  "odata.metadata":"http://localhost/odata/$metadata#Categories&$select=Name,Products/Name",
  "value":[ 
    {
      "Products":[ {"Name":"Hat"},{"Name":"Scarf"},{"Name":"Socks"} ],
      "Name":"Apparel"
    },
    {
      "Products":[ {"Name":"Yo-yo"},{"Name":"Puzzle"} ],
      "Name":"Toys"
    }
  ]
}

Дополнительные сведения о параметре $select см. в официальной документации по OData в разделе "Выбор параметра системного запроса" ($select).

Получение отдельных свойств сущности ($value)

Существует два способа для клиента OData получить отдельное свойство из сущности. Клиент может получить значение в формате OData или получить необработанное значение свойства.

Следующий запрос получает свойство в формате OData.

GET http://localhost/odata/Products(1)/Name

Ниже приведен пример ответа в формате JSON:

HTTP/1.1 200 OK
Content-Type: application/json; odata=minimalmetadata; streaming=true; charset=utf-8
DataServiceVersion: 3.0
Content-Length: 90

{
  "odata.metadata":"http://localhost:14239/odata/$metadata#Edm.String",
  "value":"Hat"
}

Чтобы получить необработанное значение свойства, добавьте $value к URI:

GET http://localhost/odata/Products(1)/Name/$value

Вот ответ. Обратите внимание, что тип контента — text/plain, а не JSON.

HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
DataServiceVersion: 3.0
Content-Length: 3

Hat

Чтобы поддерживать эти запросы в контроллере OData, добавьте метод с именем GetProperty, где Property находится имя свойства. Например, метод для получения свойства Name будет называться GetName. Метод должен возвращать значение этого свойства:

public async Task<IHttpActionResult> GetName(int key)
{
    Product product = await db.Products.FindAsync(key);
    if (product == null)
    {
        return NotFound();
    }
    return Ok(product.Name);
}