.Net Coreが登場したとき、OData ASP.NET Web APIの古いバージョンは新しいプラットフォームと互換性がありませんでした。 この致命的な欠陥により、.Net Coreプラットフォームで独自のOData実装を作成できました。 以前の実装を創造的に再考した結果、私は彼女が多くの不必要な抽象化を伴う過度に複雑なデザインに苦しんでいることに気付きました。 このアイデアは、最小限のコーディングを必要とする使いやすいライブラリを作成するために生まれました。 コードを記述せずにODataサービスを作成するためのライブラリOdataToEntityを紹介します。データアクセスコンテキストのみが必要です。 その結果、API設計を簡素化するために、ライブラリが完全なテストカバレッジを提供する一方で、コードでインターフェイスを使用しないことが決定されました。 外部の依存関係を減らすため、ライブラリはHTTPから解放され、トランスポートの上にODataを実装できます。 このエンジニアリングマーベルは、Framework 4.8または.Net Core 3.0で構築され、Microsoft.OData.Core 7.6を使用します。 次のデータコンテキストがサポートされています。
- Entity Framework 6.3
- Entity Framework Core 3.0
- Linq2db
仕組み
プロジェクトの主なアイデアは、ODataリクエストを式ツリーに変換し、それを対応するデータアクセスアダプターに転送することです。
ライブラリをさまざまなORM APIから分離するには、OdataToEntity.Db.OeDataAdapter抽象データアクセスアダプタークラスを使用します。 各コンテキストは、このクラスの子孫を実装します(Ef6:OeEf6DataAdapter、EfCore:OeEfCoreDataAdapter、Linq2Db:OeLinq2DbDataAdapter)。
データモデルに基づいて、ODataエンティティデータモデル(EDM)モデルを使用して、サービスによって提供されるデータを記述します。 ODataLibライブラリでは、クエリ文字列を解析するためにEDMモデルが必要です。 ユーザーエンティティが属性(System.ComponentModel.DataAnnotations)でマークされている場合、すべてのデータプロバイダーに適した普遍的な方法でモデルを構築できます。
//Create adapter data access, where OrderContext your DbContext var dataAdapter = new OeEfCoreDataAdapter<Model.OrderContext>(); //Build OData Edm Model EdmModel edmModel = dataAdapter.BuildEdmModel();
Entity Frameworkコンテキストを使用し、Fluent APIを使用してエンティティを記述する場合(属性を使用せずに)
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 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);
このライブラリは、データの読み取りと編集に使用できます。
読み取りモードでは、OData要求はライブラリ入力に送信され、Microsoft.OData.Core(ODataLib)を使用してODataLibビューに解析され、正規表現ツリーに変換されます。 要求はパラメーター化され(つまり、定数式が変数に置き換えられます)、データアクセスアダプターに渡されます。 アダプターは、一般的な式ツリーを、このコンテキストで適用可能なより具体的なデータに変換します。 データベースクエリを実行するコンテキストを作成し、結果のエンティティをOData JSON形式でシリアル化します。
編集モードでは、OData JSON形式でシリアル化されたモデルエンティティがライブラリ入力に送られます。 ODataLibを使用すると、データアクセスコンテキストに追加され、データベースに保存されているデータモデルが逆シリアル化されます。 データベース側で計算されたフィールドはクライアントに返されます。 サポートされている「バッチ変更セット」-エンティティのバッチの追加、削除、変更。 ツリーのようなデータ構造を記述するテーブル(自己参照テーブル)の編集。 Linq2Dbでは、DbContext Entity Frameworkに類似したデータコンテキストが実装され、オブジェクトのグラフを編集できるようになりました。
サポートされているリクエストタイプ
- $適用(フィルター、groupby、集計(平均、カウント、仮想プロパティ$カウント、countdistinct、最大、最小、合計))
- $カウント
- $フィルター
- $ orderby
- $選択
- $スキップ
- $トップ
- 計算する
- $ skiptoken
- ラムダ
- ラムダすべて
サポートされている機能
- キャスト
- 天井
- 連結
- 含む
- 日
- で終わる
- 床
- 小数秒
- 時
- indexof
- 長さ
- 分
- 月
- 丸い
- 第二
- 始まる
- 部分文字列
- 低い
- タッパー
- トリム
- 年
使用例
テストと例では、次のデータモデルが使用されます。
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(); }
ODataクエリの実行例は、5行のみで構成されています。
//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);
データベースに新しいエンティティを保存する例も5行で構成されています。
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);
ストアドプロシージャの実行例
//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);
c#でメソッド名以外のプロシージャ名を指定するには、属性を使用できます
[Description("dbo.GetOrders")] public IEnumerable<Order> GetOrders(int? id, String name, OrderStatus? status) => throw new NotImplementedException();
他の例は、 テストフォルダーにあります。
ページ分割されたデータ
サーバー駆動のページングを使用すると、 OeRequestHeaders.SetMaxPageSize(int maxPageSize)メソッドを使用してサイズが設定されている部分的なデータセットを取得できます。 サーバーは、データと@ odata.nextLinkアノテーションの次の部分へのリンクを返します。$ skiptokenラベルは次のデータ部分の始まりを示します。 クエリがデータベースでNULL許容列を許可するデータを返す場合(Required属性が指定されていない場合)、SQLite、MySql、 Sql ServerのOeDataAdapter.IsDatabaseNullHighestValueプロパティをfalseに、PostgreSqlのOracleを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);
実際の1 対多のナビゲーションプロパティデータではなく、リンクを取得する必要がある場合は、 OeRequestHeaders.SetNavigationNextLink(true)メソッドを呼び出す必要があります。
//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固有の機能
プロバイダーEntity Framework Coreの場合、要求をキャッシュする可能性があります。既存のテストでは、サンプリングレートを最大2倍に増やすことができます。 キャッシュキーは定数値が削除された解析済みODataリクエストであり、値はデータコンテキストを取得してクエリ結果を返すデリゲートです。 これにより、データクエリ自体(IQueryable)を構築する段階がなくなります。 この機能を使用するには、 OeEfCoreDataAdapter(DbContextOptionsオプション、Db.OeQueryCache queryCache)コンストラクターを使用します。
DbContextオブジェクトプール( DbContextPool )を使用するには、コンストラクターでDbContextOptionsパラメーターを使用してOeEfCoreDataAdapterインスタンスを作成します
//Create adapter data access, where OrderContext your DbContext var dataAdapter = new OeEfCoreDataAdapter<Model.OrderContext>(Model.OrderContext.CreateOptions());
ソースコード構造
ソースコードは2つの部分に分かれています。 ソースフォルダー内-ライブラリ自体と、 テストフォルダー内のさまざまなデータソースのアセンブリへのアクセス-テストとコード例。
slnフォルダー内のソリューションファイル。
ライブラリ自体はsource / OdataEntityプロジェクトにあります。
Entity Framework 6.2 source / OdataToEntity.Ef6コンテキストへのアダプター。
Entity Framework Core ソース/OdataToEntity.EfCoreコンテキストへのアダプター。
Linq2Dbコンテキストソース/ OdataToEntity.Linq2Dbへのアダプター。
Asp .Net Core Mvc source / OdataToEntity.AspNetCoreのルーティングおよびベースコントローラークラス
テスト:
Entity Framework Coreのメモリ内データベーステスト/ OdataToEntity.Test
Entity Framework Core Sql Server テスト/ OdataToEntity.Test.EfCore.SqlServer
Entity Framework Core PostgreSql テスト/ OdataToEntity.Test.EfCore.PostgreSql
Entity Framework 6 Sql Server テスト/ OdataToEntity.Test.Ef6.SqlServer
Linq2Db SQL Server テスト/ 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(countdistinct as cnt))
OrderItems?$ Apply = groupby((製品))
OrderItems?$ Apply = groupby((OrderId、Order / Status)、aggregate(価格は平均が平均、製品がcountdistinctがdcnt、価格が最大が最大、注文/状態が最大がmax_status、価格が最小が最小、価格sumをsum、$をcntとしてカウント)
OrderItems?$ Apply = groupby((OrderId)、aggregate(sum as sum as sum))/ filter(OrderId eq 2 and sum ge 4)
OrderItems?$ Apply = groupby((OrderId)、aggregate(sum as sum as sum))&$ filter = OrderId eq 2
OrderItems?$ Apply = groupby((OrderId、Order / Name))/ filter(OrderId eq 1および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 count as cnt))/ filter(dcnt ne cnt)
注文/ $カウント
Orders?$ Expand = Customer、Items&$ orderby = Id
Orders?$ Expand = AltCustomer、Customer、Items&$ select = AltCustomerCountry、AltCustomerId、CustomerCountry、CustomerId、Date、Id、Name、Status&$ orderby = Id
顧客?$ Expand = AltOrders($ expand = Items($ filter = contains(Product、 'unknown'))))、Orders($ expand = Items($ filter = contains(Product、 'unknown')))
顧客?$ Expand = AltOrders($ expand = Items)、Orders($ expand = Items)
OrderItems?$ Expand = Order($ expand = AltCustomer、Customer)&$ orderby = Id
顧客?$ Expand = Orders($ expand = Items($ orderby = Id desc))
顧客?$ Orderby = Id&$ skip = 1&$ top = 3&$ expand = AltOrders($ expand = Items($ top = 1))、Orders($ expand = Items($ top = 1))
顧客?$ Expand = Orders($ filter = Status eq OdataToEntity.Test.Model.OrderStatus'Processing ')
顧客?$ Expand = AltOrders、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(countdistinct as cnt))
Orders?$ Filter = Items / $ count gt 2
Orders?$ Filter = Date ge 2016-07-04T19:10:10.8237573%2B03:00
注文?$ 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
顧客?$ Filter = Sex eq OdataToEntity.Test.Model.Sex'Female '
顧客?$ Filter = Sex eq null
顧客?$ Filter = Sex ne nullおよびAddress ne null
顧客?$ Filter = Sex eq nullおよびAddress ne null
顧客?$ Filter = Sex eq nullおよびAddress eq null
OrderItems?$ Filter = Count ge 2
OrderItems?$ Filter = Count eq null
OrderItems?$ Filter = Order / Customer / Name eq 'Ivan'
顧客?$ Filter = Address eq 'Tula'
顧客?$ Filter = concat(concat(Name、 'hello')、 'world')eq 'Ivan hello world'
顧客?$ Filter = contains(Name、 'sh')
顧客?$ Filter = endswith(名前、「asha」)
顧客?$フィルター=長さ(名前)eq 5
顧客?$ Filter = indexof(Name、 'asha')eq 1
顧客?$ Filter = startswith(Name、 'S')
顧客?$ Filter = substring(Name、1、1)eq substring(Name、4)
顧客?$ Filter = tolower(Name)eq 'sasha'
顧客?$ Filter = toupper(Name)eq 'SASHA'
顧客?$ Filter = trim(concat(Name、 ''))eq trim(Name)
顧客(国= 'RU'、ID = 1)
Orders(1)?$ Expand = Customer、Items
Orders(1)/ Items?$ Filter = Count ge 2
OrderItems(1)/注文/顧客
OrderItems(1)/ Order?$ Apply = groupby((CustomerId)、aggregate(min as min as min))
注文(1)/アイテム?$ Orderby =カウント、価格
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')およびステータス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およびId ne 1)
Orders?$ Select = AltCustomer、AltCustomerId、Customer、CustomerId、Date、Id、Items、Name、Status&$ orderby = Id
注文$選択=名前
お客さま
顧客?$ Orderby = Id&$ top = 3&$ skip = 2
注文?$エキスパンド=アイテム&$カウント= true&$トップ= 1
OrderItems?$ Filter = OrderId eq 1&$ count = true&$ top = 1
例;
HTTPサービステスト/ OdataToEntityCore.Asp / OdataToEntity.Test.AspServer
HTTP Mvcサービステスト/ OdataToEntityCore.Asp / OdataToEntity.Test.AspMvcServer
HTTPサービステスト用Microsoft.OData.Clientクライアント/ OdataToEntityCore.Asp / OdataToEntity.Test.AspClient
Microsoft.OData.ClientクライアントおよびWCFサーバーソース/ OdataToEntity.Test.Wcf
Microsoft.OData.Clientと連携するWcfサービスコントラクトの例
[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データベースを作成するスクリプトtest \ OdataToEntity.Test.EfCore.SqlServer \ script.sql
テスト用のPostgreSqlデータベースを作成するスクリプトtest \ OdataToEntity.Test.EfCore.PostgreSql \ script.sql
→ ソースコード
→ Nugetパッケージ