Когда вышел .Net Core, старая версии OData ASP.NET Web API оказалась несовместимой с новой платформой. Этот фатальный недостаток позволил мне создать свою реализацию OData на платформе .Net Core. В результате творческого переосмысления предыдущей реализации пришло понимание, что она страдала от переусложненного дизайна с большим количеством ненужных абстракций. Возникла идея создать простую в использовании, требующую минимального кодирования библиотеку. Представляю вашему вниманию OdataToEntity, библиотеку создания OData сервисов без написания кода, нужен только контекст доступа к данным. Как следствие, для упрощения дизайна api было принято решение не использовать в коде интерфейсы, при этом библиотека имеет полное покрытие тестами. Для уменьшения внешних зависимостей библиотека отвязана от HTTP, что позволяет реализовать OData поверх любого транспорта. Это чудо инженерной мысли собирается под Framework 4.8 или .Net Core 3.1 и использует Microsoft.OData.Core 7.6. Поддерживаются следующие контексты данных: Как это работает Основная идея проекта — трансляция OData запросов в дерево выражений, которое потом передается к соответствующему адаптеру доступа к данным. Для изоляции библиотеки от различных ORM API используется абстрактный класс "адаптер доступа к данным" OdataToEntity.Db.OeDataAdapter. Каждый контекст реализует своего наследника от этого класса (Ef6: OeEf6DataAdapter, EfCore: OeEfCoreDataAdapter, Linq2Db: OeLinq2DbDataAdapter). По модели данных строится OData Entity Data Model (EDM) модель описания данных, предоставляемых вашим сервисом. EDM модель необходима библиотеке ODataLib для разбора строки запроса. Если сущности пользователя размечены атрибутами (System.ComponentModel.DataAnnotations), то модель можно построить универсальным способом, подходящим для всех поставщиков данных. Если используется контекст Entity Framework и для описания сущностей используется "Fluent API" (без использования атрибутов):
Entity Framework Core Entity Framework 6 Создать модель из нескольких контекстов данных Библиотека может использоваться для чтения и редактирования данных.
В режиме чтения на вход библиотеке подается OData запрос, он c помощью Microsoft.OData.Core (ODataLib) парсится в представление ODataLib, которое транслируется в обычное дерево выражений. Выполняется параметризация запроса (т.е. замена константных выражений на переменные) и затем передается в адаптер доступа к данным. Адаптер транслирует общее древо выражений в более конкретное, применимое в данном контексте данных. Создает контекст, который выполняет запрос к базе данных, и полученные сущности сериализуются в OData JSON формате.
В режиме редактирования на вход библиотеке подаются сущности модели, сериализованные в OData JSON формате. С помощью ODataLib десериализуется в сущности модели данных, которые добавляются в контекст доступа к данным и сохраняются в базе данных. Поля, вычисляемые на стороне базы данных, возвращаются клиенту. Поддерживается "batch change set" — пакетное добавление, удаление, изменение сущностей. Редактирование таблиц, описывающих древовидные структуры данных (self-referencing table). Для Linq2Db был реализован контекст данных подобный DbContext Entity Framework, позволяющий редактировать граф объектов. Поддерживаемые виды запросов Поддерживаемые функции Пример использования В тестах и примерах используется следующая модель данных Пример выполнения OData запроса состоит всего из пяти строчек: Пример сохранения новых сущностей в базе данных также состоит из пяти строчек:  Пример выполнения хранимой процедуры  Для задания имени процедуры, отличного от имени метода в c#, можно использовать атрибут Другие примеры можно найти в папке test Постраничная выборка данных
Server-Driven Paging позволяет получить частичный набор данных размер которого устанавливается через метод OeRequestHeaders.SetMaxPageSize(int maxPageSize). Сервер возвращает данные и ссылку на следующею часть в аннотации @odata.nextLink, где меткой $skiptoken записано начало следующей части данных. Если запрос возвращает данные где в сортировке участвует столбец допускающий значения NULL в базе данных (не указан атрибут Required), необходимо установить свойство OeDataAdapter.IsDatabaseNullHighestValue для SQLite, MySql, Sql Server в false, для PostgreSql, Oracle в true. Если необходимо получить ссылки, а не реальные данные навигационных свойств один ко многим, необходимо вызывать метод OeRequestHeaders.SetNavigationNextLink(true). Возможности специфичные для Entity Framework Core
Для провайдера Entity Framework Core существует возможность кэширования запросов, что на существующих тестах позволяет поднять скорость выборки до двух раз. Ключом кэша выступает разобранный OData запрос с удаленными константными значениями, а значением — делегат, принимающий контекст данных и возвращающий результат запроса. Это позволяет исключить стадию построения самого запроса к данным (IQueryable). Для использования этой возможности используйте конструктор OeEfCoreDataAdapter(DbContextOptions options, Db.OeQueryCache queryCache).
Для использование пула объектов DbContext (DbContextPool) создайте экземпляр OeEfCoreDataAdapter через конструктор с параметром DbContextOptions Структура исходного кода
Исходный код разделен на две части: в папке source — сама библиотека и сборки доступа к различным источникам данных, в папке test — тесты и примеры кода.
Файлы солюшенов в папке sln.
Сама библиотека находится в проекте source/OdataEntity.
Адаптер к контексту Entity Framework 6.2 source/OdataToEntity.Ef6.
Адаптер к контексту Entity Framework Core source/OdataToEntity.EfCore.
Адаптер к контексту Linq2Db source/OdataToEntity.Linq2Db.
Роутинг и базовые классы контроллеров для Asp .Net Core Mvc source/OdataToEntity.AspNetCore  Тесты:
Entity Framework Core in-memory database test/OdataToEntity.Test
Entity Framework Core Sql Server test/OdataToEntity.Test.EfCore.SqlServer
Entity Framework Core PostgreSql test/OdataToEntity.Test.EfCore.PostgreSql
Entity Framework 6 Sql Server test/OdataToEntity.Test.Ef6.SqlServer
Linq2Db Sql Server test/OdataToEntity.Test.Linq2Db Примеры запросов можно посмотреть в тестах  OrderItems?$apply=filter(Order/Status eq OdataToEntity.Test.Model.OrderStatus'Processing')
Orders?$apply=filter(Status eq OdataToEntity.Test.Model.OrderStatus'Unknown')/groupby((Name), aggregate(Id with countdistinct as cnt))
OrderItems?$apply=groupby((Product))
OrderItems?$apply=groupby((OrderId, Order/Status), aggregate(Price with average as avg, Product with countdistinct as dcnt, Price with max as max, Order/Status with max as max_status, Price with min as min, Price with sum as sum, $count as cnt))
OrderItems?$apply=groupby((OrderId), aggregate(Price with sum as sum))/filter(OrderId eq 2 and sum ge 4)
OrderItems?$apply=groupby((OrderId), aggregate(Price with sum as sum))&$filter=OrderId eq 2
OrderItems?$apply=groupby((OrderId, Order/Name))/filter(OrderId eq 1 and Order/Name eq 'Order 1')
OrderItems?$apply=groupby((OrderId), aggregate(Price mul Count with sum as sum))
OrderItems?$apply=groupby((OrderId, Order/Name))&$orderby=OrderId desc, Order/Name
OrderItems?$apply=groupby((OrderId, Order/Name))&$orderby=OrderId desc, Order/Name&$skip=1&$top=1
OrderItems?$apply=groupby((OrderId))&$orderby=OrderId&$skip=1
OrderItems?$apply=groupby((OrderId))&$top=1
OrderItems?$apply=groupby((OrderId), aggregate(substring(Product, 0, 10) with countdistinct as dcnt, $count as cnt))/filter(dcnt ne cnt)
Orders/$count
Orders?$expand=Customer,Items&$orderby=Id
Orders?$expand=AltCustomer,Customer,Items&$select=AltCustomerCountry,AltCustomerId,CustomerCountry,CustomerId,Date,Id,Name,Status&$orderby=Id
Customers?$expand=AltOrders($expand=Items($filter=contains(Product,'unknown'))),Orders($expand=Items($filter=contains(Product,'unknown')))
Customers?$expand=AltOrders($expand=Items),Orders($expand=Items)
OrderItems?$expand=Order($expand=AltCustomer,Customer)&$orderby=Id
Customers?$expand=Orders($expand=Items($orderby=Id desc))
Customers?$orderby=Id&$skip=1&$top=3&$expand=AltOrders($expand=Items($top=1)),Orders($expand=Items($top=1))
Customers?$expand=Orders($filter=Status eq OdataToEntity.Test.Model.OrderStatus'Processing')
Customers?$expand=AltOrders,Orders
Customers?$expand=Orders($select=AltCustomerCountry,AltCustomerId,CustomerCountry,CustomerId,Date,Id,Name,Status)
Orders?$expand=*&$orderby=Id
Orders?$filter=Items/all(d:d/Price ge 2.1)
Orders?$filter=Items/any(d:d/Count gt 2)
Orders?$filter=Status eq OdataToEntity.Test.Model.OrderStatus'Unknown'&$apply=groupby((Name), aggregate(Id with countdistinct as cnt))
Orders?$filter=Items/$count gt 2
Orders?$filter=Date ge 2016-07-04T19:10:10.8237573%2B03:00
Orders?$filter=year(Date) eq 2016 and month(Date) gt 3 and day(Date) lt 20
Orders?$filter=Date eq null
OrderItems?$filter=Price gt 2
OrderItems?$filter=Price eq null
Customers?$filter=Sex eq OdataToEntity.Test.Model.Sex'Female'
Customers?$filter=Sex eq null
Customers?$filter=Sex ne null and Address ne null
Customers?$filter=Sex eq null and Address ne null
Customers?$filter=Sex eq null and Address eq null
OrderItems?$filter=Count ge 2
OrderItems?$filter=Count eq null
OrderItems?$filter=Order/Customer/Name eq 'Ivan'
Customers?$filter=Address eq 'Tula'
Customers?$filter=concat(concat(Name,' hello'),' world') eq 'Ivan hello world'
Customers?$filter=contains(Name, 'sh')
Customers?$filter=endswith(Name, 'asha')
Customers?$filter=length(Name) eq 5
Customers?$filter=indexof(Name, 'asha') eq 1
Customers?$filter=startswith(Name, 'S')
Customers?$filter=substring(Name, 1, 1) eq substring(Name, 4)
Customers?$filter=tolower(Name) eq 'sasha'
Customers?$filter=toupper(Name) eq 'SASHA'
Customers?$filter=trim(concat(Name, ' ')) eq trim(Name)
Customers(Country='RU',Id=1)
Orders(1)?$expand=Customer,Items
Orders(1)/Items?$filter=Count ge 2
OrderItems(1)/Order/Customer
OrderItems(1)/Order?$apply=groupby((CustomerId), aggregate(Status with min as min))
Orders(1)/Items?$orderby=Count,Price
OrderItems?$orderby=Id desc,Count desc,Price desc
OrderItems?$orderby=Order/Customer/Sex desc,Order/Customer/Name,Id desc
Orders?$filter=AltCustomerId eq 3 and CustomerId eq 4 and ((year(Date) eq 2016 and month(Date) gt 11 and day(Date) lt 20) or Date eq null) and contains(Name,'unknown') and Status eq OdataToEntity.Test.Model.OrderStatus'Unknown'
&$expand=Items($filter=(Count eq 0 or Count eq null) and (Price eq 0 or Price eq null) and (contains(Product,'unknown') or contains(Product,'null')) and OrderId gt -1 and Id ne 1)
Orders?$select=AltCustomer,AltCustomerId,Customer,CustomerId,Date,Id,Items,Name,Status&$orderby=Id
Orders?$select=Name
Customers
Customers?$orderby=Id&$top=3&$skip=2
Orders?$expand=Items&$count=true&$top=1
OrderItems?$filter=OrderId eq 1&$count=true&$top=1 Примеры;
HTTP сервис test/OdataToEntityCore.Asp/OdataToEntity.Test.AspServer
HTTP Mvc сервис test/OdataToEntityCore.Asp/OdataToEntity.Test.AspMvcServer
Клиент Microsoft.OData.Client для HTTP сервиса test/OdataToEntityCore.Asp/OdataToEntity.Test.AspClient
Клиента Microsoft.OData.Client и WCF сервера source/OdataToEntity.Test.Wcf Пример контракта Wcf сервиса, который работает с Microsoft.OData.Client Скрипт создания базы SQL Server для тестов test\OdataToEntity.Test.EfCore.SqlServer\script.sql
Скрипт создания базы PostgreSql для тестов test\OdataToEntity.Test.EfCore.PostgreSql\script.sql → Исходный код
→ Nuget пакеты