Привет, Хабр! Недавно, пришлось работать на проекте с внешним API. Работал, я, к слову, всегда либо с простым REST, либо с GET/POST only запросами, но в этом нужно было работать с API Timetta. Он использует OData и что же это такое? REST vs OData Схема Типы данных Примитивные EntityType ComplexType EnumType Collection EntitySet Запросы $select $filter $expand $orderby $top, $skip $count Функции, действия Обновление/добавление ресурсов Добавление Обновление Удаление Подытожим Напоследок Инструменты В то время как REST - набор архитектурных правил создания хорошего API, OData - это уже веб-протокол, собравший в себя "лучшие архитектурные практики": defines a set of best practices for building and consuming RESTful APIs (как написано на официальном сайте). Сам протокол очень большой, поэтому я затрону наиболее практически-значимые аспекты. Каждая система использующая OData должна описать свою схему данных. По ней можно понять все: какие сущности есть в системе, какие операции над ними можно производить. Схема может описывается в формате XML или JSON. Для получения схемы нужно сделать запрос по адресу:  <root>/$metadata Где <root> - корень сервиса OData. Примеры дальше будут предполагать, что мы делаем запросы из этого <root>. Для Timetta этот адрес такой:  https://api.timetta.com/odata/$metadata Примеры дальше будут с использованием XML схем. Примитивные Протокол определяет ряд встроенных типов данных. Все имеют префикс "Edm". Например:  Edm.Boolean Edm.String Edm.Int32 Edm.Int16 Edm.Stream Edm.Date Edm.Byte Edm.Decimal Edm.Binary EntityType EntityType похож на сущность из DDD: у него есть как состояние, так и свой ID (в схеме указывается отдельно). В схеме состоит из элементов Property - поля со скалярными данными. Например, строка или число. Имеет атрибуты:  Name - название поля (обязателен) Type - тип поля (обязателен) Nullable - может ли быть null NavigationProperty - поле, которое ссылается на другую сущность Name - название (обязателен) Type - тип (обязателен) ReferentialConstraint - "как" мы ссылаемся Property - название поля в ССЫЛАЮЩЕМСЯ типе ReferencedProperty - название поля в типе, на который ССЫЛАЕМСЯ Key - элемент, определяющий первичный ключ сущности. Значения могут быть только примитивными типами или перечислением и не может равняться null.  Name - название поля, которое является ключом Alias - псеводним для ключа. Например, если ключ - поле вложенного типа. Пример, для сущности TimeSheet, представляющей рабочий график проекта (табель) за некоторый период времени. Объяснение: Поле "id" - первичый ключ Имеет несколько полей с примитивными типами. Например: dueDate - до какой даты нужно закончить dateFrom - когда начинать проект dateTo - когда оканчивать проект approvalInstanceId - Id документа (ресурса), который сигнализирует о том, что работа согласована (если не согласован, то значение - null, нет атрибута Nullable, что по умолчанию означает поддержку null) Некоторые из которых могут принимать значение null:  dueDate - мы еще не знаем дедлайн approvalInstanceId - работу еще могли не согласовать Имеет навигационные свойства. Например: approvalInstance - документ согласования timeSheetLines - потраченные часы  createdBy - кем создан план работ Где: approvalInstance можно найти по занчению поля approvalInstanceId родителя и сопоставление по полю id искомой сущности timeSheetLines - "слабая" сущность. Для нее не нужен Id, время жизни привязано к самому родителю ComplexType ComplexType похож на Value type из DDD - не имеет первичного ключа, сравнение по значению полей. Может состоять из тех же элементов, что и EntityType за исключением Key. Пример, для объекта с 2 полями, который используется для отметок времени Объяснение: Тип DateHours является сложным (ComplexType) Состоит из 2 полей: date - дата (Type="Edm.Date"), за которую идет отсчет. hours - время, представляемое дробным числом (Type="Edm.Decimal") Где: date - не может быть null, т.к. мы должны знать дату hours - может быть null, т.к. время может быть просто не проставлено/указано EnumType EnumType - обычный тип перечисления. Особенность в том, как передается значение в параметры - сначала пишется полное имя Enum, затем в кавычках его значение. Для данного примера, чтобы передать PlanningMethod.Manual, нужно написать PlanningMethod'Manual'. Атрибуты: Name - название перечисления (обязательно) UnderlyingType - тип, которое определяет используемое значение (по умолчанию, Edm.Int32) IsFlags - является ли перечисление флагом. Чтобы исползовать в UnderlyingType должно быть проставлено "true" Для определения занчений - элемент Member: Name - название элемента Value - значение элемента Объяснение: Тип PlanningMethod является типом перечисления (EnumType) Может принимать значения: Manual, определяемое значением 0 FrontLoad, определяемое значением 1 RemainingCapacity, определяемое значением 2 Evenly, определяемое значением 3 P.S. В общем случае такой вид записи применяется ко всем типам и имеет вид: ПолныйТипСущности'Значение'. Например, для даты - date'2022-07-01' Collection Тип коллекции является отдельным. Определяется как Collection(ПолноеНазваниеТипа). Например, Collection(WP.TimeSheet).  Возвращаясь к типу TimeSheet. В нем присутствуют 5 коллекций*: timeSheetLines timeAllocations lineApprovals approvalRecors timeOffRequests *Тип коллекции может быть и у Property, не только NavigationProperty EntitySet У нас есть сущности, сложные типы, перечисления и т.д. Но где это хранить? Для этого нам нужна коллекция.EntitySet - это top-level коллекция доступная всем. Внутри нее хранятся какие-либо сущности. Соответственно, для нее обязательны имя и тип: Name="SampleName" EntityType="SampleType", соответственно. По умолчанию, все обычные поля (Property) коллекции включаются в вывод. Если у типа есть навигационные свойства, то их должны указать в EntitySet, иначе их существование не гарантируется. Здесь: Name="TimeSheets" - название коллекции. Доступ к ней через это слово: https://app.timetta.com/TimeSheets  EntityType="WP.TimeSheet" - тип сущностей используемый в коллекции (был выше). WP - пространство имен.  У коллекции есть несколько навигационных свойств. Свойства - те же, что и у самой сущности. Например: user template modifiedBy Но некоторые (total, lineApprovements) отсутствуют. Есть коллекция, значит есть и единственный элемент. Для получения нужно использовать ID сущности, который указан в схеме. Указывать в круглых скобках после названии коллекции. Например, для получения TimeSheet то его ID нужно сделать запрос: /TimeSheets(00000000-0000-0000-0000-000000000000) На мой взгляд, что примечательного в OData - язык запросов. Можно делать запросы к ресурсам прямо в строке запроса. Для запросов есть свой специальный язык. По фукнционалу он очень похож на SQL. Сам запрос передается в строке запросов (query string) URL. Например: /TimeSheets?$select=id,dateFrom,dateTo&$filter=approval&$expand=createdBy($select=name)&$count=true Вместе с каждым запросом возвращаются специальные поля с метаданными. Они могут быть полезны, например, для получения мета информации о запрашиваемой сущности. Дальше на них акцент делаться не будет. Поэтому, многие запросы OData транслируются в SQL. У этого языка много ключевых слов, но рассмотрим основные. Для лучшего погружения представим ситуацию: нам нужно получить отчеты. $select $select позволяет нам выбрать только нужные поля.  Пример: нам нужно получить только отрезки времени отсчета таймшитов и имя того, кто его  создал. Тогда следующий запрос: /TimeSheets?$select=dateFrom,dateTo,id $filter Прекрасно. Мы получили данные. Но что если какие-то табеля не были согласованы? Нужно убрать такие: /TimeSheets?$select=dateFrom,dateTo,id&$filter=approvalInstanceId ne null Какой-то табель не был согласован! Хорошо, что мы обнаружили это. Сам $filter принимает в себя логическое выражение. Доступные логические операции (bash-like): eq (equal), ==: name eq 'За ноябрь' ne (not equal), !=: approvalInstanceId ne null gt (greater than), >: dueDate gt '2022-03-15' ge (greater or equal), >=:  dateFrom ge '2021-01-01' lt (less than), <: age lt 18 le (less or equal), <=: ttl le 0 Эти выражения можно комбинировать с помощью скобок и:  and (логическое 'и'): (name eq 'За ноябрь') and (dateFrom ge '2021-01-01') or (логическое 'или'): (dateFrom ge '2021-01-01') or (approvalInstanceId ne null) not (отрицание): not isActive Также есть и булевы литералы: true, false $expand Прекрасно. Мы отфильтровали данные и выбрали только те поля, которые нужны. Но теперь в отчете нужно получить и имя человека создавшего табель. Можно сделать несколько запросов: сначала массив табелей, затем для каждого - запрос на информацию о пользователе. Но это лишнее. Можно ведь сделать просто - добавить $expand: /TimeSheets?$select=dateFrom,dateTo,id&$filter=approvalInstanceId ne null&$expand=user($select=name) Здесь продемонстрированы сразу 2 фичи: Возможность вставить сущность из навигационного свойства (у типа TimeSheet есть навигационное свойство user типа WP.User) Сделать подзапросы: здесь из всех возможных свойств нам нужно только имя. Для подзапросов есть ограничения - ключевые слова разделяются точкой с запятой, иначе парсинг строки запроса будет неверным. Например: /TimeSheets?$select=dateFrom,dateTo,id&$filter=approvalInstanceId ne null&$expand=user($select=name;$expand=schedules) - дает нам не только имя пользователя, но и список его рабочих графиков. Если бы вместо точки с запятой был амперсанд, то получилось бы 2 параметра: $expand=user($select=name и $expand=schedules) $orderby Вроде бы все прекрасно. Данные получаются, но мы хотим получать упорядоченные данные сразу. Нам поможет ключевое слово $orderby. Представим, что мы хотим сортировать табели сначала по дате начала по убыванию, а затем по дате конца по возрастанию (получим самые  близкие к нам по дате, короткие по длительности): /TimeSheets?$select=dateFrom,dateTo,id&$filter=approvalInstanceId ne null&$expand=user($select=name)&$orderby=dateFrom desc, dateTo asc  По умолчанию используется сортировка по возрастанию - asc $top, $skip Вот проблема! У нас слишком много данных. Как бы нам ограничить их прием? Для этого можно указать какое количество мы хотим принять с помощью $top. Тогда нам вернется не больше заданного количества: /TimeSheets?$select=dateFrom,dateTo,id&$filter=approvalInstanceId ne null&$expand=user($select=name)&$orderby=dateFrom desc, dateTo asc$top=5 А что если нужно еще и пропускать некоторое количество (для пагинации, например)? Тогда используем $skip: /TimeSheets?$select=dateFrom,dateTo,id&$filter=approvalInstanceId ne null&$expand=user($select=name)&$orderby=dateFrom desc, dateTo asc&$top=5&$skip=10 $count Мы можем получить наши данные - прекрасно. А если я хочу знать только количество удовлетворяющих критерию? Или для пагинации? Нужно посчитать общее количество элементов. Здесь нам поможет $count. Он принимает булево значение: true - вернуть общее количество, false - не возвращать (по умолчанию) /TimeSheets?$select=dateFrom,dateTo,id&$filter=approvalInstanceId ne null&$expand=user($select=name)&$orderby=dateFrom desc, dateTo asc&$count=true Вот здесь начинается самое интересное - функции и действия (Functions, Actions). Теперь мы хотим получить табель за текущий период. Можно получить все табеля и отфильтровать их - сделать сложный запрос, а потом отфильтровать еще и результат. Это лишнее. Не проще ли использовать функцию: /TimeSheets/Current И все! OData определяет функции (Function) и действия (Action) Function - это операция над ресурсами, которая обязательно возвращает значение и не имеет сторонних эффектов. Action - это операция, которая может изменить значение PS. похоже на CQRS Как же ими пользоваться? Для начала: операции могут быть привязанными и нет - требуется ли сущность для выполнения операции. Описание операции состоит из  Функция из примера имеет следующее пределение: Что это все значит: Name="Current" - название функции, которую будем вызывать IsBound="true" - функция привязана к конкретному типу. Т.е. вызвать ее из произвольного места нельзя  "bindingParameter" - особый параметр означающий к какому типу функция применяется. Здесь применяется к типу Collection(WP.TimeSheet)(чем и является /TimeSheets) Возвращает тип TimeSheet Если функция не принимает ничего, то скобок нет. Видели. А если функция принимает параметры? Тогда они указываются в элементе Parameter: Эта функция принимает дополнительные параметры: userId, from, to (в данном случае они обязательные - Nullable="false"). Передача параметров - как вызов функции, в скобках, причем все параметры именованные и вставляются через запятую (как в Python). Пример: /Schedules/GetUserSchedule(userId=00000000-0000-0000-0000-00000000,from=2022-01-01,to=2022-02-01) - получение расписания для пользователя за весь январь Что по Action? Разница в том, что действие может модифицировать данные и может не возвращать данные. Например: Это действие устанавливает тип роли пользователя применяемой по умолчанию. Логично, что оно может модифицировать, а может и нет (если это тип уже был по умолчанию). Также он ничего не возвращает (разве что статусный код по которму и будет понятен результат). Как вызывать: /Roles(00000000-0000-0000-0000-00000000)/SetAsDefault Это привязанное действие и привязано оно к типу WP.Role, а значит к единственному элементу, а не к целой коллекции как было в предыдущем примере. Пример действия, который что-то возвращает: Для операций модификаций ресурсов используются HTTP методы: POST, PATCH, PUT, DELETE Создание Создание сущности = добавление в коллекцию. Для этого нужно сделать POST запрос с адресом коллекции и передать необходимые для создания параметры. Например, для создания нового департамента нужно сделать такой POST запрос: POST /Departments И в ответ получим: Обновление Для обновления используются 2 HTTP метода: PUT, PATCH (последний предпочтительнее). Если бы мы хотели обновить название только что созданного департамента на "Лучший департамент", то сделали такой запрос: PATCH /Departments(11111111-1111-1111-1111-111111111111) И в ответ - 204 No Content При повторном запросе: Название действительно изменилось. Также изменилось и поле "rowVersion" - для предотвращение параллельного обновления. Но говорилось еще и о PUT. При использовании PUT нам нужно передавать ВСЮ сущность. Даже те поля, которые не обновляются (за исключением тех над которыми не имеем власти, например, rowVersion или modified). Тоже самое обновление, но с помощью PUT: PUT /Departments(11111111-1111-1111-1111-111111111111) Удаление И для удаления остался последний метод - DELETE. Удаление через ID сущности. Удалим же наш департамент: DELETE /Departments(11111111-1111-1111-1111-111111111111) В ответ получим - 204 No Content. И при обращении по этому же ID получаем Not Found. OData - мощный веб-фреймворк, ядром которого является управление ресурсами.  Для функционирования использует возможности HTTP: HTTP методы, HTTP заголовки, строки запроса, URL. Сервис, использующий OData, определяет свою схему. По ней можно понять какую функциональность данный сервис предоставляет: Типы данных: Перечисления, Enum Сложные типы, ComplexType Сущности, EntityType Их атрибуты Свойства (и свойства свойств (Nullability, MaxLength) Навигационные свойства Специфичные для данного типа атрибуты: ключ для сущности, значения для перечисления Коллекции: их название, доступ, содержащийся в них тип данных Предопределенные функции и действия (Function, Action) Сам протокол определяет SQL подобный язык запросов (передается в строке запроса) и позволяет управлять получаемым контентом с помощью ключевых слов: $select - в выводе только указанные поля. (~ SELECT) $filter - вывести ресурсы, удовлетворяющие предикату (~ WHERE) $expand - включить другие ресурсы, на который ссылается (~JOIN) $orderby - сортировка по полю. (~ORDER BY) $top - ограничить вывод максимальным количеством. (~TAKE) $skip - пропустить какое-то количество. (~SKIP, OFFSET) $count - дополнительно подсчитать общее число (или удовлетворяющих предикату) сущностей. (~COUNT) OData очень большой фреймворк. Одна статья не может покрыть его полностью. Но целью этого поста стало простое ознакомление с наиболее используемой (по мнению автора) функциональностью. Многие темы не были покрыты, как например ключевое слово запроса $compute или лямбда операций any/all. Если вы хотите исследовать эту тему дальше, то вот некоторый список ссылок от куда можно стартовать: https://www.odata.org - сайт по OData. Здесь можно найти спецификацию, туториалы для новичков и продвинутых, полезные инструменты и так далее. https://docs.microsoft.com/en-us/odata - раздел об OData созданный Microsoft. Здесь много туториалов как по самому OData, так и по инструментам связанным с ним. Много уделяется фреймворку Microsoft.OData. https://services.odata.org/ - сервис, для обучения/тестирования/просто попробовать OData. Как пользоваться для Read-Only Service: Устанавливаю базовый URL https://services.odata.org/V3/OData/OData.svc/  Мне удобнее получать результат в виде JSON. Поэтому выставляю заголовок Accept: application/json Получаю схему OData: https://services.odata.org/V3/OData/OData.svc/$metadata  Изучаю полученную схему и делаю запросы. Для получения списка всех продуктов по схеме нужно делаю такой запрос: https://services.odata.org/V3/OData/OData.svc/Products Read-Only Northwind пользоваться по аналогии Если нужно потренироваться и в модификации, то есть Read-Write. Для этого нужен ваш секретный ключ. Получить его можно, перейдя по ссылке Browse the Full Access (Read-Write) Service на указанном сайте. В браузере ваша строка заменится на строку с ключом. Либо, эту строка указывается в полученной схеме. Сам вид строки: https://services.odata.org/V3/(S(<секретный ключ>))/OData/OData.svc. А в запросах на создание (POST) нужно также указывать тип ресурса, который хотим создать в поле "odata.type" (там используется наследование, про которое не было рассказано). Например, для создания нового Product сделать вот такой запрос: POST https://services.odata.org/V3/(S(j5lmqrfbgk1st4mmgrva1jtg))/OData/OData.svc/Products Полезные инструменты: Расширение VS Code для работы с OData. Веб-приложение для визуализации и исследования OData, по ее схеме. Использование: в верхней части в поисковике выберите вариант 'Metadata URL'. Вбейте в поисковик URL и нажмите 'Get Details'.  Триггером написания статьи было, то что автор не нашел "краш курсов" по OData и пришлось собирать знания по кусочкам. Если статья дала вам хороший старт, то значит проделанное было не зря. Бэкэнд разработчик, но для ПМ могу быть кем угодно