When .Net Core came out, the old OData ASP.NET Web API version was incompatible with the new platform. This fatal flaw allowed me to create my own implementation of OData on the .Net Core platform. As a result of the creative rethinking of the previous implementation, it came to be understood that she suffered from an over-complicated design with a large number of unnecessary abstractions. The idea was to create an easy-to-use library that requires minimal coding. I present to you OdataToEntity, a library for creating OData services without writing code, you only need a data access context. As a result, to simplify the design of api, it was decided not to use interfaces in the code, and the library has full test coverage. To reduce external dependencies, the library is decoupled from HTTP, which allows you to implement OData on top of any transport. This engineering marvel is built under Framework 4.6.1 or .Net Core 2.0 and uses Microsoft.OData.Core 7.4. The following data contexts are supported:
How it works
The main idea of ββthe project is to translate OData requests into an expression tree, which is then transmitted to the corresponding data access adapter.
To isolate the library from various ORM APIs, the OdataToEntity.Db.OeDataAdapter abstract class "data access adapter" is used. Each context implements its descendant from this class (Ef6: OeEf6DataAdapter, EfCore: OeEfCoreDataAdapter, Linq2Db: OeLinq2DbDataAdapter).
The data model is based on the OData Entity Data Model (EDM) description model of the data provided by your service. The EDM model is required by the ODataLib library to parse the query string. If user entities are tagged with attributes (System.ComponentModel.DataAnnotations), then the model can be built in a universal way suitable for all data providers.
//Create adapter data access, where OrderContext your DbContext var dataAdapter = new OeEfCoreDataAdapter<Model.OrderContext>(); //Build OData Edm Model EdmModel edmModel = dataAdapter.BuildEdmModel();
If the Entity Framework context is used and the Fluent API is used to describe the entities (without using attributes):
Entity Framework Core
//Create adapter data access, where OrderContext your DbContext var dataAdapter = new OeEfCoreDataAdapter<Model.OrderContext>(); //Build OData Edm Model EdmModel edmModel = dataAdapter.BuildEdmModelFromEfCoreModel();
Entity Framework 6
//Create adapter data access, where OrderEf6Context your DbContext var dataAdapter = new OeEf6DataAdapter<OrderEf6Context>(); //Build OData Edm Model EdmModel edmModel = dataAdapter.BuildEdmModelFromEf6Model();
Create a model from multiple data contexts
//Create referenced data adapter var refDataAdapter = new OeEfCoreDataAdapter<Model.Order2Context>(); //Build referenced Edm Model EdmModel refModel = refDataAdapter.BuildEdmModel(); //Create root data adapter var rootDataAdapter = new OeEfCoreDataAdapter<Model.OrderContext>(); //Build root Edm Model EdmModel rootModel = rootDataAdapter.BuildEdmModel(refModel);
The library can be used to read and edit data.
In read mode, the OData request is submitted to the library, it is parsed with the help of Microsoft.OData.Core (ODataLib) into the ODataLib representation, which is translated into the usual expression tree. The query is parameterized (i.e., the replacement of constant expressions with variables) and then transferred to the data access adapter. The adapter translates the general expression tree into a more specific, applicable in this data context. Creates a context that makes a query to the database, and the resulting entities are serialized in an OData JSON format.
In the edit mode, the entities of the model serialized in the OData JSON format are served as input to the library. Using ODataLib, it is deserialized into the essence of the data model, which are added to the data access context and stored in the database. Fields computed on the database side are returned to the client. Supported "batch change set" - batch add, delete, change entities. Editing tables describing tree data structures (self-referencing table). For Linq2Db, a data context similar to the DbContext Entity Framework was implemented, allowing you to edit the object graph.
Supported query types
Supported Features
Usage example
The following data model is used in tests and examples.
public sealed class Category { public ICollection<Category> Children { get; set; } public int Id { get; set; } [Required] public String Name { get; set; } public Category Parent { get; set; } public int? ParentId { get; set; } } public sealed class Customer { public String Address { get; set; } [InverseProperty(nameof(Order.AltCustomer))] public ICollection<Order> AltOrders { get; set; } [Key, Column(Order = 0), Required] public String Country { get; set; } [Key, Column(Order = 1)] public int Id { get; set; } [Required] public String Name { get; set; } [InverseProperty(nameof(Order.Customer))] public ICollection<Order> Orders { get; set; } public Sex? Sex { get; set; } } public sealed class Order { [ForeignKey("AltCustomerCountry,AltCustomerId")] public Customer AltCustomer { get; set; } public String AltCustomerCountry { get; set; } public int? AltCustomerId { get; set; } [ForeignKey("CustomerCountry,CustomerId")] public Customer Customer { get; set; } public String CustomerCountry { get; set; } public int CustomerId { get; set; } public DateTimeOffset? Date { get; set; } [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } public ICollection<OrderItem> Items { get; set; } [Required] public String Name { get; set; } public OrderStatus Status { get; set; } } public sealed class OrderItem { public int? Count { get; set; } [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } public Order Order { get; set; } public int OrderId { get; set; } public Decimal? Price { get; set; } [Required] public String Product { get; set; } } public enum OrderStatus { Unknown, Processing, Shipped, Delivering, Cancelled } public enum Sex { Male, Female } public sealed class OrderContext : DbContext { public DbSet<Category> Categories { get; set; } public DbSet<Customer> Customers { get; set; } public DbSet<Order> Orders { get; set; } public DbSet<OrderItem> OrderItems { get; set; } [Description("dbo.GetOrders")] public IEnumerable<Order> GetOrders(int? id, String name, OrderStatus? status) => throw new NotImplementedException(); public void ResetDb() => throw new NotImplementedException(); }
An example of executing an OData request consists of only five lines:
//Create adapter data access, where OrderContext your DbContext var dataAdapter = new OeEfCoreDataAdapter<Model.OrderContext>(); //Create query parser var parser = new OeParser(new Uri("http://dummy"), dataAdapter, dataAdapter.BuildEdmModel()); //Query var uri = new Uri("http://dummy/Orders?$select=Name"); //The result of the query var response = new MemoryStream(); //Execute query await parser.ExecuteGetAsync(uri, OeRequestHeaders.JsonDefault, response, CancellationToken.None);
An example of saving new entities in the database also consists of five lines:
string batch = @" --batch_6263d2a1-1ddc-4b02-a1c1-7031cfa93691 Content-Type: multipart/mixed; boundary=changeset_e9a0e344-4133-4677-9be8-1d0006e40bb6 --changeset_e9a0e344-4133-4677-9be8-1d0006e40bb6 Content-Type: application/http Content-Transfer-Encoding: binary Content-ID: 1 POST http://dummy/Customers HTTP/1.1 OData-Version: 4.0 OData-MaxVersion: 4.0 Content-Type: application/json;odata.metadata=minimal Accept: application/json;odata.metadata=minimal Accept-Charset: UTF-8 User-Agent: Microsoft ADO.NET Data Services {""@odata.type"":""#OdataToEntity.Test.Model.Customer"",""Address"":""Moscow"",""Id"":1,""Name"":""Ivan"",""Sex@odata.type"":""#OdataToEntity.Test.Model.Sex"",""Sex"":""Male""} --changeset_e9a0e344-4133-4677-9be8-1d0006e40bb6-- --batch_6263d2a1-1ddc-4b02-a1c1-7031cfa93691-- "; //Create adapter data access, where OrderContext your DbContext var dataAdapter = new OeEfCoreDataAdapter<Model.OrderContext>(); //Create query parser var parser = new OeParser(new Uri("http://dummy"), dataAdapter, dataAdapter.BuildEdmModel()); //Serialized entities in JSON UTF8 format var request = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(batch)); //The result of the query var response = new MemoryStream(); //Execute query await parser.ExecuteBatchAsync(request, response, CancellationToken.None);
An example of the stored procedure
//Create adapter data access, where OrderContext your DbContext var dataAdapter = new OeEfCoreDataAdapter<Model.OrderContext>(); //Create query parser var parser = new OeParser(new Uri("http://dummy"), dataAdapter, dataAdapter.BuildEdmModel()); //The result of the stored procedure var response = new MemoryStream(); //Execute sored procedure await parser.ExecuteGetAsync(new Uri("http://dummy/GetOrders(name='Order 1',id=1,status=null)"), OeRequestHeaders.JsonDefault, response, CancellationToken.None);
To set a procedure name other than the method name in c #, you can use the attribute
[Description("dbo.GetOrders")] public IEnumerable<Order> GetOrders(int? id, String name, OrderStatus? status) => throw new NotImplementedException();
Other examples can be found in the test folder.
Paginated data sampling
Server-Driven Paging allows you to get a partial set of data whose size is set using the OeRequestHeaders.SetMaxPageSize method (int maxPageSize) . The server returns the data and a link to the next part in the @ odata.nextLink annotation, where the beginning of the next part of the data is recorded with the $ skiptoken tag. If the query returns data where in the sort the column participates allowing for NULL values ββin the database (the Required attribute is not specified), you must set the OeDataAdapter.IsDatabaseNullHighestValue property for SQLite, MySql, Sql Server to false, for PostgreSql, Oracle to true.
//Create adapter data access, where OrderContext your DbContext var dataAdapter = new OeEfCoreDataAdapter<Model.OrderContext>(Model.OrderContext.CreateOptions()) { IsDatabaseNullHighestValue = true //PostgreSql }; //Create query parser var parser = new OeParser(new Uri("http://dummy"), dataAdapter, dataAdapter.BuildEdmModel()); //Query var uri = new Uri("http://dummy/Orders?$select=Name&$orderby=Date"); //Set max page size OeRequestHeaders requestHeaders = OeRequestHeaders.JsonDefault.SetMaxPageSize(10); //The result of the query var response = new MemoryStream(); //Execute query await parser.ExecuteGetAsync(uri, requestHeaders, response, CancellationToken.None);
If you want to get links, rather than real one-to-many navigation properties data, you must call the OeRequestHeaders.SetNavigationNextLink (true) method.
//Query var uri = new Uri("http://dummy/Orders?$expand=Items"); //Set max page size, to-many navigation properties OeRequestHeaders requestHeaders = OeRequestHeaders.JsonDefault.SetMaxPageSize(10).SetNavigationNextLink(true); //The result of the query var response = new MemoryStream(); //Execute query await parser.ExecuteGetAsync(uri, requestHeaders, response, CancellationToken.None);
Entity Framework Core features
For the Entity Framework Core provider, there is the ability to cache queries, which in existing tests allows us to raise the sample rate up to two times. The cache key is a parsed OData query with remote constant values, and the value is the delegate that accepts the data context and returns the result of the query. This allows you to exclude the stage of building the data query itself (IQueryable). To use this feature, use the OeEfCoreDataAdapter constructor (DbContextOptions options, Db.OeQueryCache queryCache) .
To use the DbContext object pool ( DbContextPool ), create an instance of OeEfCoreDataAdapter via the constructor with the DbContextOptions parameter
//Create adapter data access, where OrderContext your DbContext var dataAdapter = new OeEfCoreDataAdapter<Model.OrderContext>(Model.OrderContext.CreateOptions());
Source code structure
The source code is divided into two parts: in the source folder, the library itself and assemblies of access to various data sources, in the test folder, tests and code samples.
Solution files in the sln folder.
The library itself is located in the source / OdataEntity project .
Adapter to the context of the Entity Framework 6.2 source / OdataToEntity.Ef6 .
Adapter to context Entity Framework Core source / OdataToEntity.EfCore .
Adapter to context Linq2Db source / OdataToEntity.Linq2Db .
Routing and base controller classes for Asp .Net Core Mvc source / OdataToEntity.AspNetCore
Tests:
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
Examples of requests can be viewed in tests.
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 ast maxus, price with min as min, max 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 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
Examples;
HTTP service test / OdataToEntityCore.Asp / OdataToEntity.Test.AspServer
HTTP Mvc service test / OdataToEntityCore.Asp / OdataToEntity.Test.AspMvcServer
Microsoft.OData.Client client for the HTTP test / OdataToEntityCore.Asp / OdataToEntity.Test.AspClient service
Microsoft.OData.Client client and WCF server source / OdataToEntity.Test.Wcf
An example of a Wcf service contract that works with Microsoft.OData.Client
[ServiceContract] public interface IOdataWcf { [OperationContract] Task<Stream> Get(String query, String acceptHeader); [OperationContract] Task<OdataWcfPostResponse> Post(OdataWcfPostRequest request); } [MessageContract] public sealed class OdataWcfPostRequest { [MessageHeader] public String ContentType { get; set; } [MessageBodyMember] public Stream RequestStream { get; set; } } [MessageContract] public sealed class OdataWcfPostResponse { [MessageBodyMember] public Stream ResponseStream { get; set; } }
SQL Server Database Creation Script for test \ OdataToEntity.Test.EfCore.SqlServer \ script.sql tests
PostgreSql database creation script for test \ OdataToEntity.Test.EfCore.PostgreSql \ script.sql tests
β Source Code
β Nuget packages
Source: https://habr.com/ru/post/319996/
All Articles