しばらく前、ネットワークの広大さの中で、興味深いBreeze.jsライブラリに出会いました。 最初に思いついたのは、「はい、ブラウザのEntity Frameworkと同じです」ということです。 もちろん、他のユーザーからの情報とフィードバックを探して、私が最初に探したのはHabréの記事でしたが、見つけられなかったので、それが誰かにも役立つことを期待して書きました。 この記事は、Breeze.js、Angular.js、ASP.NET Web API、およびEntity Frameworkに基づいてプロジェクトを作成するチュートリアルとして書かれています。
ライブラリでの主な作業はEntityManagerクラスを使用して行われ、データモデルを格納し、受信を実装し、変更を監視し、DbContextに似た変更を保存できるため、Entity Frameworkと類似しています。 さらに、クライアント側とサーバー側での検証、ブラウザーでのデータのキャッシュ、一時的なストレージへのデータのエクスポート(接続が失われたときなど)、およびサーバーとの変更のその後の同期のためにこのデータをインポートするなど、多くの興味深い機能を提供します。
ライブラリのクライアント部分はODataプロトコルに従って作業を実装するため、特定のライブラリを使用してバックエンドを実装する必要はありません。 ただし、Breeze.jsチームは、次のテクノロジに基づいてサービスを迅速に作成するためのライブラリも提供しています。
- ASP.NET Web API + Entity Framework
- ASP.NET Web API + NHibernate (これまでのベータ版)
- Node.js + MongoDb (ベータ版)
この記事では、WebApi + EFを使用してバックエンドを作成します。
作業のために、EntityManagerには、使用する予定のエンティティ、それらの関係、および検証ルールのメタデータが必要です(データのみを要求する場合は、メタデータなしで実行できます)。 クライアントの特別なオブジェクトからインポートするか、クライアントの適切なメソッドを呼び出して生成するか、最も簡単な方法を使用できます。これは、EFContextProvider <>クラスを使用してDbContextまたはObjectContext Entity Frameworkからメタデータを取得することです。 同時に、データスキーム全体がクライアント上で利用可能になることを理解する必要があります。スキームを完全に開き、それを使用してDTOデータにアクセスしたくない場合は、必要なメタデータの形成を簡素化するだけの役割を果たす特別なコンテキストを作成する必要があります。
おそらく、すぐに練習に行く方が明らかになるでしょう。 この例では、Entity Frameworkを使用してデータベースにアクセスし、バックエンドとして特別な属性BreezeControllerを持つWebApiコントローラー、フロントエンドの基礎としてAngular.js、そしてもちろんBreeze.jsを使用してブラウザーからデータにアクセスします。 ASP.NET MVCは使用しません。Angular.jsブラウザーでマークアップを作成するため、ルーティングも処理します。 さらに、 ASP.NET vNextもそう遠くありません。MVCとは異なり、IISに関連付けられていないWebApiのみを使用すると、移行がはるかに容易になります。
行きましょう。 任意のエディションのVisual Studio 2013を起動し、新しい空のASP.NETプロジェクトを作成します。 NuGetに移動して、次のパッケージとその依存関係をインストールします: EntityFramework 、 Breeze Clientおよび Server-ASP.NET Web API 2およびEntity Framework 6を備えたJavascriptクライアント (これは必要ないくつかから組み立てられたパッケージであり、WebAPIをプルします)、 Breeze Labs :Breeze Angular Service 、 Angular JS 、したがってBootstrapがなくても、美のために角度ローディングバーを追加できます。 次に、[更新]タブに移動し、[すべて更新]をクリックします。 そこで、必要なパッケージの最新バージョンをすべて入手しました。 ここでは、NuGetから必要なものをすべてインストールしましたが、Visual StudioでNpm、Bower、Gulp、Gruntを便利に使用する方法について詳しく知りたい場合は、 Bowerを使用してすべてのフロントエンドライブラリをインストールする方がはるかに便利です。このトピックに関する記事の翻訳 。
モデル
私たちのアプリケーションは買い物リストになり、すべての製品がカテゴリに分類され、リンクの操作を検討します。 最初に、 コードファーストアプローチを使用してデータモデルクラスを作成し、Modelsフォルダーに配置します(この名前は慣例により使用されますが、実際にはどこにでも配置できます)。
ListItemクラスはリストアイテムを表します。
public class ListItem { public int Id { get; set; } public String Name { get; set; } public Boolean IsBought { get; set; } public int CategoryId { get; set; } public Category Category { get; set; } }
Categoryクラスは、アイテムが属するカテゴリです。
public class Category { public int Id { get; set; } public String Name { get; set; } public List<ListItem> ListItems { get; set; } }
DbContextを作成する
public class ShoppingListDbContext: DbContext { public DbSet<ListItem> ListItems { get; set; } public DbSet<Category> Categories { get; set; } }
自動移行を有効にして、Entity Frameworkがデータベースを作成し、モデルの変更時にその構造をモデルに合わせます。 これを行うには、ツール-> NuGetパッケージマネージャー->パッケージマネージャーコンソールに移動し、次のコマンドを入力します。
Enable-Migrations -EnableAutomaticMigrations
このプロジェクトでは、Migrationsフォルダーが表示され、Configurationクラスがその中にあります。
この構成をコンテキストに適用するには、静的コンストラクターを作成します。
using BreezeJsDemo.Migrations; using System; using System.Collections.Generic; using System.Data.Entity; using System.Linq; using System.Web; namespace BreezeJsDemo.Model { public class ShoppingListDbContext: DbContext { static ShoppingListDbContext() { Database.SetInitializer<ShoppingListDbContext>(new MigrateDatabaseToLatestVersion<ShoppingListDbContext, Configuration>()); } public DbSet<ListItem> ListItems { get; set; } public DbSet<Category> Categories { get; set; } } }
これで、初めてコンテキストを使用するときに、Entity Frameworkは、指定されたデータベースが存在しない場合は作成し、必要に応じて不足しているテーブル、列、リレーションシップを追加します。 アプリケーションには単一の接続文字列がないため、EntityはApp_DataフォルダーにSQL Server Compactデータベースを作成します。SQLExpressがマシンにインストールされている場合は、データベースが作成されます。
ブリーズコントローラー
次に、データを送信および保存するWebApiコントローラーを作成します。 これを行うには、(同意またはその他の方法で)Controllersフォルダーを作成し、右クリックして[追加]-> [コントローラー]を選択し、[Web APIコントローラー-空]を選択して、DbControllerなどの名前を付けます。
using Breeze.ContextProvider; using Breeze.ContextProvider.EF6; using Breeze.WebApi2; using BreezeJsDemo.Model; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Web.Http; namespace BreezeJsDemo.Controllers { [BreezeController] public class DbController : ApiController { private EFContextProvider<ShoppingListDbContext> _contextProvider = new EFContextProvider<ShoppingListDbContext>(); public String Metadata () { return _contextProvider.Metadata(); } [HttpGet] public IQueryable<ListItem> ListItems() { return _contextProvider.Context.ListItems; } [HttpGet] public IQueryable<Category> Categories() { return _contextProvider.Context.Categories; } [HttpPost] public SaveResult SaveChanges(JObject saveBundle) { return _contextProvider.SaveChanges(saveBundle); } } }
このコードをさらに詳しく分析してみましょう。 Breezeを使用するには、データベースごとに1つのコントローラーのみを作成することをお勧めします。
「すべてを支配する1つのコントローラー...」
これは、BreezeControllerAttribute属性を持つ通常のApiControllerであり、多くの設定を実行します。
最初に、EFContextProviderのインスタンスを作成し、3つのタスクを実行します。
- DbContextのインスタンスを作成します
- クライアントが必要とする形式でメタデータを取得するのに役立ちます
- データ保持リクエストを処理します
メタデータメソッドを使用してメタデータを返します。これは、クライアントが検索する方法です。 IQueryable <>として、対応するすべてのエンティティを、それぞれに対応するメソッドで返します。 BreezeQueryableAttribute属性を使用して、エンティティでの操作を制限できます。
[BreezeQueryable(AllowedQueryOptions= AllowedQueryOptions.Skip | AllowedQueryOptions.Top)]
QueryableAttributeを継承し、まったく同じ方法で使用できます 。
また、唯一のSaveChangesメソッドにも注意を払い、現在のすべての変更を含む使い慣れたJObjectを受け入れ、 EFContextProviderはDbContextを使用してデータベースへのすべての変更を検証して保存します。
お客様
Visual Studio 2013でjavascript、html、cssなどを使用する前に、Update 4をインストールすることを強くお勧めします(まだ行っていない場合)。更新後、ツール->拡張機能と更新-> Online Extension Web Essentials 2013 for Updateが利用可能になります4 (Expressバージョンを含む)。 Update 4とこの素晴らしいプラグインは、Web開発者にとって非常に多くの便利なツールを追加します。本当に必要なものであり、開発者の公式Webサイトですべての機能の詳細を読むことができます。 Visual Studioは、javascriptのインテリセンスもサポートしています。そのためには、_references.jsファイルをスクリプトフォルダーに追加し、その中の.jsファイルにリンクを追加して、スタジオがインデックスを作成できるようにするだけです。 したがって、Web Essentialsはこれを行うことができます。autosyncパラメーターを使用してファイルを作成し、スタジオはそれを最新の状態に保ち、[スクリプト]-> [追加]-> _references.js Intellisenseファイルフォルダーをクリックします見て、オートコンプリートの方がずっと便利です
クライアント部分の作成を始めましょう。 アプリケーションのスクリプトとマークアップは、アプリフォルダーに保存されます。 アプリケーションモジュールapp.module.jsを使用してファイルを作成し、
(function () { 'use strict'; angular.module('app', ['ngRoute', 'breeze.angular', 'angular-loading-bar']); })();
アプリケーションのモジュールを発表しました。最初のパラメーターはモジュールの名前、2番目は私たちのモジュールで使用されている他のモジュールの依存関係の列挙です。 ここでは、breeze.angularモジュールへの依存関係を示しました。このモジュールにはbreezeサービスが含まれています。 原則として、あなたはそれなしで行うことができ、その後window.breezeオブジェクトにアクセスしますが、1つのことを除いてまったく同じように動作します:breezeサービスは$ qと$ httpangle.jsを使用し、ダイジェストをトリガーできるため、望ましいです。 angle-loading-bar-設定なしですぐに動作する素敵なロードインジケーター。依存関係を追加するだけで、ブリーズがサーバーからデータをロードするときに明確に表示されます。 ngRouteはルーティングを行う標準のangle.jsモジュールであり、パス設定app.config.routes.jsでファイルを作成します。
(function () { 'use strict'; angular.module('app').config(config); config.$inject = ['$routeProvider']; function config($routeProvider) { $routeProvider. when('/', { templateUrl: '/app/shoppingList/shoppingList.html' }). otherwise({ redirectTo: '/' }); } })();
詳細に分析します。 ここでは、angular.moduleの呼び出しには2番目のパラメーターはありません。これは、新しいモジュールを作成するのではなく、既存のモジュールへのリンクを取ることを意味します。 次に、その上でconfigメソッドを呼び出し、そこに設定関数を渡します。 config関数に$ injectプロパティを追加します。入力パラメーターで関数が関数に渡す依存関係のリストを配列に割り当てます。 ここでは、プロバイダー$ routeProviderをリクエストしました。これは、アプリケーションパスの構成に使用されます。 whenメソッドは、アドレスとサーバー上のマークアップファイルとの対応を設定します。 つまり、単一のアドレス「/」に対して、ファイル「/app/shoppingList/shoppingList.html」からのマークアップがng-viewディレクティブでタグにロードされます。 それ以外の方法では、アドレスがwhenで指定されたルートのいずれとも一致しない場合に動作を設定できます。この場合、アドレス「/」へのリダイレクトが発生します。
買い物リストのあるビューの場合、/ app / shoppingListフォルダーにコントローラーshoppingList.controller.jsを作成します。
(function () { 'use strict'; angular.module('app').controller('ShoppingListController', ShoppingListController); ShoppingListController.$inject = ['$scope', 'breeze']; function ShoppingListController($scope, breeze) { var vm = this; vm.newItem = {}; vm.refreshData = refreshData; vm.isItemExists = isItemExists; vm.saveChanges = saveChanges; vm.rejectChanges = rejectChanges; vm.hasChanges = hasChanges; vm.addNewItem = addNewItem; vm.deleteItem = deleteItem; vm.filterByCategory = filterByCategory; breeze.NamingConvention.camelCase.setAsDefault(); var manager = new breeze.EntityManager("breeze/db"); var categoriesQuery = new breeze.EntityQuery("Categories").using(manager).expand("listItems"); var listItemsQuery = new breeze.EntityQuery("ListItems").using(manager); activate(); function activate() { refreshData(); $scope.$watch('vm.filterCategory', function (a, b) { if (a !== b) { refreshData(); } }); } function refreshData() { var query = listItemsQuery; if (vm.filterCategory) { query = query.where('category.id', breeze.FilterQueryOp.Equals, vm.filterCategory.id); } categoriesQuery.execute() .then( function (data) { vm.categories = data.results; }) .then( function () { vm.listItems = query.executeLocally(); } ); } function filterByCategory(cat) { if (vm.filterCategory && vm.filterCategory.name === cat.name) { vm.filterCategory = undefined; } else { vm.filterCategory = cat; } } function saveChanges() { manager.saveChanges(); } function rejectChanges() { manager.rejectChanges(); } function hasChanges() { return manager.hasChanges(); } function addNewItem() { var category = vm.categories.filter(function (x) { return x.name === vm.newItem.category; }); if (category.length === 0) { category = manager.createEntity('Category', { name: vm.newItem.category }); vm.categories.push(category); } else { category = category[0]; } var item = manager.createEntity('ListItem', { name: vm.newItem.name, category: category, isBought: false }); vm.listItems.push(item); vm.newItem = {}; } function deleteItem(item) { item.entityAspect.setDeleted(); } function isItemExists(x) { return x.entityAspect.entityState.name !== 'Deleted' && x.entityAspect.entityState.name !== 'Detached'; } } })();
ここでは、前述のブリーズサービスへの依存を示しました。 まず、行breeze.NamingConvention.camelCase.setAsDefault()に注目します。これは、JavaScriptに慣れているため、キャメルケース内のオブジェクトのすべてのプロパティの名前を簡単にやり直す方法です。 次に、 EntityManagerオブジェクトを作成します。これにより、エンティティのクエリ、作成、削除、変更の監視、サーバーへの送信が可能になります。 DbControllerコントローラーのアドレスをコンストラクターに渡します。 BreezeController属性を持つコントローラーアドレスの場合、デフォルトのプレフィックスは「/ breeze /」です。 次に、EntityQueryクエリを作成し、コントローラーメソッドの名前をコンストラクターに渡します。コンストラクターは目的のエンティティーを返します。 次に、usingメソッドを使用して、リクエストのEntityManagerを指定します(代わりに、リクエスト時にexecuteQueryの EntityManagerメソッドを使用できます)。 次に、expandメソッドを使用しました。これは、各カテゴリのすべてのListItemもロードすることをサーバーに指示し、ナビゲーションプロパティlistItemsを介してそれらにアクセスできます。
refreshData関数は、vm.filterCategoryカテゴリが選択されている場合、EntityQuery whereメソッドを使用してlistItemsQueryクエリのフィルター条件を追加します。 「Category.Id」がvm.filterCategory.idと等しいすべてのListItemを取得します(filterByCategory関数が設定します)。 breeze.FilterQueryOp値の1つが渡される 2番目のパラメーターは、すべての有効なフィルター演算子を含む列挙です。 より複雑なフィルタリング条件の場合、whereメソッドは、いくつかの条件を含むことができるPredicateクラスのオブジェクトを受け入れます。 次に、EntityQuery 実行メソッドを使用してデータをロードし、コントローラーメソッドへの要求を実行してプロミスを返します。 リクエストの最後で、結果をコントローラーのcategoriesプロパティに書き込み、マークアップで表示します。 展開を使用して、カテゴリだけでなくリストのすべての要素もロードしました。したがって、ネットワーク経由でそれらを再度要求する必要はありません。そのため、 executeLocallyメソッドを使用してキャッシュからデータをクエリし、結果をvm.listItemsプロパティに割り当てます。
EntityQueryには、次のような場所以外にも多くの便利なメソッドが含まれています。
orderBy / orderByDesc(n)-プロパティnによるソートを設定します
select( 'a、b、c、... n')-エンティティ全体ではなく、プロパティa、b、c ... nのみを含む投影を選択できます
take / top-ページネーションに非常に便利な最初のnエントリを選択します
skip(n)-n個のエントリをスキップします。takeとの組み合わせで最適です
inlineCount-skip / take(top)を使用すると、レコードの総数も返されます
executeLocally-ネットワークを使用せずにキャッシュへのリクエストを行います
noTracking-breezeは、エンティティではなく、単純なJavaScriptオブジェクトの形式で結果を返します(EntityManagerは変更を追跡しません)
その他いくつか...
上記のように、EntityManagerはエンティティのすべての変更、削除、追加を追跡します。 その後、すべての変更をサーバーに送信して、データベースに保存できます。 これにはsaveChangesメソッドが使用され、非同期に呼び出され、promiseを返します。 hasChangesメソッドを使用して、変更が発生したかどうかを調べます。 一般に、各エンティティには、データを含む_backingStoreプロパティがあり、entityAspect-オブジェクトをBreezeエンティティとして表すプロパティとメソッド(オブジェクトステータス、検証、元の値など)が含まれます。 entityAspect.originalValuesプロパティには、変更されたすべてのプロパティの元の値のリストが表示されます。 そして、 entityAspect.entityStateプロパティには現在のステータスが含まれています。 entityAspect.entityStateで変更されたエンティティのステータスは「変更済み」になり、変更されていないエンティティのステータスは「変更なし」になります。 ステータスもあります:削除済み(オブジェクト削除済み)、追加済み(新しいオブジェクト)および分離済み(オブジェクトはEntityManagerから「切り離され」、変更は追跡されません。そのようなオブジェクトはattachEntityメソッドを使用してマネージャーに「接続」できます)。 EntityManagerでは、rejectChangesメソッドを使用して発生したすべての変更を取り消すこともできます。
次に、新しいリストアイテムを追加するaddNewItem関数について考えます。 まず、vm.categoriesで新しいリストアイテムのカテゴリを名前で検索し、そのようなカテゴリがまだない場合は、EntityManagerメソッドcreateEntityを使用して作成し、作成されたエンティティ(またはEntityType)のタイプの名前を最初のパラメーターとして渡し、オブジェクトを2番目のパラメーターとして渡します作成されたオブジェクトのプロパティ値が含まれます。 さらに2つのパラメーターを指定することもできます。EntityState-作成されたオブジェクトのステータスを設定し、 MergeStrategy-同じキーを持つエンティティが既に存在する場合の競合解決戦略を設定します。 次に、同じ方法で新しいListItemを追加します。
削除ボタンをクリックすると、listItem関数が呼び出されます。 その中で、エンティティのentityAspect.setDeleted()メソッドを使用し、そのステータスを「削除済み」に設定してから、saveChangesが呼び出されると、データベース内のレコードが削除されます。
関数isItemExistsもあります。この関数は、既に削除されたアイテムを表示しないようにリストをフィルタリングするために使用されます。
マークアップに進み、index.htmlファイルをプロジェクトルートに追加します。次のようになります。
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title> </title> <link href="Content/bootstrap.css" rel="stylesheet" /> <link href="Content/loading-bar.min.css" rel="stylesheet" /> </head> <body ng-app="app"> <div ng-view></div> <script src="Scripts/jquery-2.1.1.js"></script> <script src="Scripts/bootstrap.js"></script> <script src="Scripts/angular.js"></script> <script src="Scripts/angular-route.js"></script> <script src="Scripts/loading-bar.min.js"></script> <script src="Scripts/breeze.debug.js"></script> <script src="Scripts/breeze.angular.js"></script> <script src="app/app.module.js"></script> <script src="app/app.config.routes.js"></script> <script src="app/shoppingList/shoppingList.controller.js"></script> <!-- , , , , GET , bundle, RequireJS.--> </body> </html>
ng-app属性に注意する価値があります。これは、アプリケーションのAngularルート要素を示すために使用されます。多くの場合、これにはhtml要素とbody要素が使用されます。 属性ng-view-現在のルートのtemplateUrlパスにあるファイルからのマークアップがロードされる要素を示します。
唯一のルート「/」では、ファイル「/app/shoppingList/shoppingList.html」になります
<div class="container" ng-controller="ShoppingListController as vm"> <nav class="navbar navbar-default"> <ul class="navbar-nav nav"> <li ng-if="vm.hasChanges()"><a ng-click="vm.saveChanges()"><span class="glyphicon glyphicon-thumbs-up"></span> </a></li> <li ng-if="vm.hasChanges()"><a ng-click="vm.rejectChanges()"><span class="glyphicon glyphicon-thumbs-down"></span> </a></li> <li><a ng-click="vm.refreshData()"><span class="glyphicon glyphicon-refresh"></span> </a></li> </ul> </nav> <h1> </h1> <div ng-if="vm.categories.length>0"> <h4> </h4> <ul class="nav nav-pills"> <li ng-repeat="cat in vm.categories" ng-class="{active:vm.filterCategory===cat}" ng-if="cat.listItems.length>0"> <a ng-click="vm.filterByCategory(cat)">{{cat.name}} ({{cat.listItems.length}})</a> </li> </ul> </div> <table class="table table-striped"> <tbody> <tr> <td></td> <td><input class="form-control" ng-model="vm.newItem.category" placeholder="" /></td> <td><input class="form-control" ng-model="vm.newItem.name" placeholder="" /></td> <td><button class="btn btn-success btn-sm" type="button" ng-click="vm.addNewItem()"><span class="glyphicon glyphicon-plus"></span></button></td> </tr> <tr ng-repeat="item in vm.listItems | filter: vm.isItemExists | orderBy:'isBought'"> <td><input type="checkbox" ng-model="item.isBought"> </td> <td>{{item.category.name}}</td> <td>{{item.name}}</td> <td><button class="btn btn-danger" type="button" ng-click="vm.deleteItem(item)"><span class="glyphicon glyphicon-trash"></span></button></td> </tr> </tbody> </table> </div>
ng-controller属性は、コントローラーをマークアップ要素に「アタッチ」します。 ここでは、「 controller as 」という構文を使用しました。つまり、「ShoppingListController vm」を指定してエイリアスvmをコントローラーに割り当て、マークアップで、ドットを介してコントローラーのプロパティにアクセスできます。たとえば、vm.listItems(最初にvar vm = this;-これは、同じ方法でプロパティにアクセスするコントローラのコードで、便宜上行われました。 Angular.jsの多くのチュートリアルでは、わずかに異なるアプローチが使用され、値が$スコープオブジェクトのプロパティに割り当てられ、コントローラーの名前のみがng-controllerに書き込まれ、マークアップでこれらのプロパティに名前で簡単にアクセスできます。例:{{newItem.name }}しかし、ある日、別のコントローラー内でコントローラーを使用する必要があり、両方が同じ名前のプロパティを持っている場合、親コントローラーのプロパティにアクセスするには、「$ parent。$ parent.property」のような構造を記述する必要があります彼に目を向ける それはそれを構文«としてのコントローラを»使用するためのルールを取ることは理にかなっていますので、ニックネームです。
次に、「Save Changes」、「Discard Changes」、「Refresh」というボタンが付いたトップメニューが表示されます。最初の2つはng-ifで非表示にし、変更がなければng-clickで対応する機能をボタンに割り当てます。
次に、ng-repeatを使用してカテゴリのリストを描画します。ng-class= "{active:vm.filterCategory === cat}"ディレクティブは、条件vm.filterCategory === catが満たされた場合、つまり選択されたカテゴリに色を付ける場合、要素をアクティブクラスに設定します 次に、買い物リストを含むテーブルを表示します。最初の行は[追加]ボタンを含む名前とカテゴリの入力フィールドになり、リストは直接ng-repeat = "item in vm.listItems | フィルター:vm.isItemExists | ここでは、既存の要素のみを表示するためにvm.isItemExists関数で指定されたフィルターフィルターを使用しました。orderByフィルターは、isBoughtプロパティの値でリストを並べ替えて、購入したすべてのアイテムを下に移動します。
おわりに
おそらくこれが初めて(もう少し)伝えたかったことです。 私は夏に記事を書き始めましたが、時間が足りなかったため、今しか書きませんでした。 そして、この半年間、breeze.jsの使用を停止していません。 並べ替え、検索、ページネーションのグリッドが必要な場所にアプリケーションを実装することは、特に高速で便利です。 編集は、特に検証がEntityFrameworkモデルの検証属性を超えない場合、2つの方法で実行されます。
落とし穴のうち、これまでのところ、中間クラスのない多対多モデルの接続をサポートしていないことに気づきました(EntityFrameworkが中間テーブルを「考え出す」場合)、もちろん、すべてが中間クラスで問題ありません。
新しいプロジェクトを作成する時間がなくても、簡単に試してみたいという要望がある場合- 既製のソリューションへのリンクです。
PS私は初めて執筆しているので、コンテンツ/デザイン/プレゼンテーションのスタイルに関するコメントと、もしあれば、将来の記事への提案について、読者にコメントをお願いします。
PPS次の記事-Breeze Server- 属性を使用してオブジェクトへのアクセスを区別する