📜 ⬆️ ⬇️

Breeze.js + Entity Framework + Angular.js = easy work with database entities directly from the browser



Some time ago, in the open spaces of the network, I ran into an interesting library of Breeze.js . The first thought that came to mind when looking at it: "Yes, it's like the Entity Framework for the browser." In search of information and reviews of other users, of course, first of all I searched for an article on Habré, but I didn’t find it, so I decided to write, in the hope that it would also be useful to someone. The article is written as a tutorial for creating a project based on Breeze.js, Angular.js, ASP.NET Web API and the Entity Framework.

The analogy with the Entity Framework arose because the main work with the library is done using the EntityManager class, it stores the data model, implements their retrieval, monitors changes and allows them to be saved, a little bit and resembles DbContext. In addition, it provides a number of interesting features, such as validation on the client and server side, caching data in the browser, exporting data to be stored in temporary storage (for example, when communication is lost) and importing this data for later synchronization of changes with the server.

The client part of the library implements the work according to the OData protocol, therefore you are not required to use any specific libraries to implement backend. But the Breeze.js team also provides libraries for quickly creating services based on the following technologies:

In this article, we will create a backend using WebApi + EF.
')
In order for EntityManager to work, it needs the metadata of the entities with which you plan to work, their relationships and validation rules (if you only plan to request data, then you can do without metadata). They can be imported from a special object on the client , formed by calling the appropriate methods on the client , or you can use the easiest way to obtain metadata from the DbContext or ObjectContext Entity Framework using the EFContextProvider <> class. At the same time, you need to understand that the entire data scheme becomes available on the client, and if you don’t want to fully disclose the scheme and use DTO to access the data, you will have to create a special context that will only serve to simplify the generation of the metadata you need.

Perhaps it will be clearer to go directly to the practice. In our example, we will use the Entity Framework to access the database, the WebApi controller with the special attribute BreezeController as backend, Angular.js, as the basis of the frontend, and of course Breeze.js, for accessing data from the browser. We will not use ASP.NET MVC, since we will build the markup in Angular.js in the browser, it will also take care of the routing. Moreover, ASP.NET vNext is not far off, it will be much easier to switch to it if we use only WebApi, which is not tied to IIS, unlike MVC.

So let's go. We launch Visual Studio 2013 of any edition and create a new empty ASP.NET project. We go into NuGet and install the following packages and their dependencies: EntityFramework , Breeze Client and Server - Javascript client with ASP.NET Web API 2 and Entity Framework 6 (this is a package assembled from several we need + it will pull WebAPI), : Breeze Angular Service , Angular JS , so where can it be without Bootstrap , and you can add angular-loading-bar for beauty? Next, go to the Updates tab and click Update All. So we got all the latest versions of the packages we need. Here we have installed everything you need from NuGet, but for my taste, it’s much more convenient to install all frontend libraries with Bower , if you are interested in learning more about how to use Npm, Bower, Gulp and Grunt in Visual Studio, here we are translation of an article on this topic.

Model


Our app will be a shopping list, where all products will be categorized to consider working with links. To begin with, let's create data model classes using the Code First approach and put them in the Models folder (this name is used by agreement, but in practice you can put it anywhere):

The ListItem class will represent an item in our list.
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 Category is the category the item belongs to.
 public class Category { public int Id { get; set; } public String Name { get; set; } public List<ListItem> ListItems { get; set; } } 

Create DbContext
  public class ShoppingListDbContext: DbContext { public DbSet<ListItem> ListItems { get; set; } public DbSet<Category> Categories { get; set; } } 

We enable automatic migrations so that the Entity Framework creates a database for us, and then adjusts its structure to the model when it changes. To do this, go to Tools -> NuGet Package Manager -> Package Manager Console and enter the command:
Enable-Migrations -EnableAutomaticMigrations

We have a project folder Migrations, and in it the class Configuration.


To apply this configuration to our context, you can create a static constructor for it.
 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; } } } 

Now when you first use the context, the Entity Framework will take care of creating the database, if it does not exist, or adding the missing tables, columns, relationships, if required. There are no connection strings in our application, so Entity will create the SQL Server Compact database in the App_Data folder, or if you have SQL Express installed on your machine, the database will be created on it.

Breeze Controller


Next, create a WebApi controller that will give and save our data. To do this, create the Controllers folder (by agreement, or to any other), click on it with the right button Add -> Controller and select Web API Controller - Empty, call it, for example, 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); } } } 

Let's sort this code in more detail. To work with Breeze, its developers advise to create only one controller per database, as they say:
"One controller to rule them all ..."

This is the usual ApiController with the BreezeControllerAttribute attribute, which performs a number of settings for us.

At the very beginning, we created an instance of EFContextProvider, with us it performs three tasks:

We return the metadata using the Metadata method; this is the path that the client will look for. All the entities with which we want to work are returned, as IQueryable <>, in the corresponding methods, one for each. You can restrict operations with one or another entity using the BreezeQueryableAttribute attribute:
 [BreezeQueryable(AllowedQueryOptions= AllowedQueryOptions.Skip | AllowedQueryOptions.Top)] 

It is inherited from QueryableAttribute , and to use and it is possible absolutely similarly.

Let's also pay attention to the only SaveChanges method, it accepts, perhaps, a familiar JObject that contains all current changes, and EFContextProvider performs validation and saves all changes to the database using our DbContext.

Customer


Before working with javascript, html, css, less in Visual Studio 2013, I highly recommend (if you have not done so already) to install Update 4, and after updating go to Tools -> Extensions and Updates -> Online Extension Web Essentials 2013 for Update will be available 4 , including the Express version. Update 4 and this wonderful plugin adds a huge amount of convenient tools for the web developer, really must-have, more information about all the functions can be found on the Official Website of developers. Visual Studio also supports intellisense for javascript, for this you just need to add the _references.js file to the scripts folder and add links to your .js files in it so that the studio can index them. So Web Essentials can do it for you, it will create a file with the autosync parameter, and then the studio will keep it up to date, for this you just need to right-click on the folder Scripts -> Add -> _references.js Intellisense file, in my look, autocompletion is much more convenient


Let's start creating the client part. We will store the scripts and markup of the application in the app folder. Create a file with the application module app.module.js,
 (function () { 'use strict'; angular.module('app', ['ngRoute', 'breeze.angular', 'angular-loading-bar']); })(); 

We declared the module of our application, the first parameter is the name of the module, the second is the enumeration of dependencies of other modules that are used in ours. Here we indicate the dependence on the breeze.angular module, it has the breeze service . In principle, you can do without it, and then access the window.breeze object, it works in exactly the same way, except for one thing: the breeze service uses $ q and $ http angular.js, and can initiate a digest, and is therefore preferable. angular-loading-bar is a cute loading indicator that works out of the box without any settings, you just need to add a dependency, it will clearly show us when breeze loads data from the server. ngRoute is a standard module angular.js, responsible for routing, create a file with the path settings 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: '/' }); } })(); 

We will analyze in detail. Here in the appeal to angular.module there is no second parameter, it means that we are not creating a new module, but taking a link to an existing one. Then we call its config method, to which we pass the configuration function. The config functions add the $ inject property, assign it an array with a list of dependencies that angular will pass to the function in the input parameters. Here we have requested the $ routeProvider provider, it is used to configure the application paths. The when method sets the correspondence between the address and the markup file on the server. That is, for our only address "/", the markup from the file '/app/shoppingList/shoppingList.html' will be loaded inside the tag with the ng-view directive. The otherwise method allows you to customize the behavior, if the address does not match any of the routes specified with when, in our case, a redirect will occur to the address "/".

For view with our shopping list, create in the / app / shoppingList folder the shoppingList.controller.js controller.
 (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'; } } })(); 

Here we have indicated a dependency on the breeze service, which I mentioned above, and will work with it. First of all, I’ll draw your attention to the line breeze.NamingConvention.camelCase.setAsDefault () - this is how we make breeze re-name all the properties of objects in camelCase, because it is much more familiar to work in JavaScript. Next, create an EntityManager object — it allows us to query, create, delete entities, monitor their changes and send them to the server. In the constructor, we pass the address of our DbController controller. For addresses of controllers with the BreezeController attribute, the default prefix is ​​"/ breeze /". Then we create EntityQuery queries, transfer the name of the controller's method to the constructor, which returns the required entity. Then, using the using method, we specify the request for its EntityManager (instead, we could use the EntityManager's executeQuery method when querying). Following we used the expand method, it tells the server that we want to load more and all ListItem of each category, access to them can be obtained through the listItems navigation property.

The refreshData function uses the EntityQuery where method to add filter criteria for the listItemsQuery query if the category vm.filterCategory is selected. We get all the ListItem for which 'Category.Id' is vm.filterCategory.id (the filterByCategory function sets it). The second where parameter is passed one of the breeze.FilterQueryOp values ​​is an enumeration that contains all valid filtering operators. For more complex filtering conditions, the where method accepts an object of the Predicate class, which can contain several conditions. Next, the EntityQuery execute method is used to load the data. It executes a request to the controller method and returns promise. When the request is completed, we write the result in the controller's categories property to display it in the markup. With the help of expand, we loaded not only categories but all the elements of the list, respectively, there is no need to query them again over the network, so we followed the executeLocally method to query the cached data, assigning the result to the vm.listItems property.

EntityQuery contains many more useful methods besides where, for example:
orderBy / orderByDesc (n) - sets the sorting by the property n
select ('a, b, c, ... n') - allows you to choose not an entity as a whole, but a projection containing only the properties a, b and c ... n
take / top - selects the first n records, very convenient for pagination
skip (n) - skip n records, great in combination with take
inlineCount - when using skip / take (top) it also returns the total number of records
executeLocally - makes a query in the cache, without using the network
noTracking - forces breeze to return the result as simple javascript objects, not entities (EntityManager will not track changes)
and a few others ...

As mentioned above, EntityManager tracks all changes, deletions, and additions of entities. Then, all changes can be sent to the server to be saved in the database. The saveChanges method is used for this , it is called asynchronously and returns a promise. You can find out if any changes were made using the hasChanges method. In general, each entity has the _backingStore property, which contains data, and entityAspect contains properties and methods that represent an object, like a Breeze entity (object status, validation, original values, etc.). In the entityAspect.originalValues ​​property, we will see a list of the initial values ​​of all the changed properties. And the entityAspect.entityState property contains the current status. Entities that have changes in entityAspect.entityState will have the status “Modified”, and those that have not changed will have the status “Unchanged”. There are also statuses: Deleted (object removed), Added (new object) and Detached (object “detached” from EntityManager, no change tracking occurs, such an object can then be “attached” to any manager using the attachEntity method). EntityManager also allows you to undo all changes that have occurred using the rejectChanges method.

Now let's look at the addNewItem function, with which we add a new list item. First, we search by name for the category of a new list item in vm.categories, and if we don’t have such a category, create it with the EntityManager method, createEntity , pass the type name of the created entity (or EntityType object) as the first parameter, which contains the property values ​​of the object being created. You can also specify two more parameters: EntityState - sets the status of the created object and MergeStrategy - conflict resolution strategy, in cases where an entity with such a key already exists. Then add the new ListItem in the same way.

When you click on the delete button of the list item, the deleteItem function will be called. In it, we use the entityAspect.setDeleted () entity method, it sets its status to 'Deleted', and then, when saveChanges is called, the record in the database will be deleted.

We also have the isItemExists function, it will be used to filter the list, so as not to show items that have already been deleted.

Let's go to the markup, add the index.html file to the project root, it will look like this:
 <!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> 

Here you should pay attention to the ng-app attribute - it is used to specify the Angular root element for our application, often using the html and body elements for this. The ng-view attribute specifies the element into which the markup will be loaded from the file located along the templateUrl path of the current route.

For our only route, '/' this will be the file '/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> 


The ng-controller attribute “attaches” the controller to the markup element. Here we used the syntax " controller as ", that is, specifying "ShoppingListController as vm" we assigned the controller alias vm, and then in the markup we can access the controller properties through a period, for example vm.listItems (in the controller code we wrote var vm = in the first line this; - this was done for convenience, in the code of the controller to access the properties as well). Many tutorials on Angular.js use a slightly different approach, values ​​are assigned to properties of the $ scope object, and only the controller name is written to ng-controller, and then in the markup these properties can be accessed simply by name, for example: {{newItem.name }}, but once a moment comes when you need to use a controller inside another controller, and they both have properties with the same name, then to access the property of the parent controller you have to write constructions like "$ parent. $ parent.property", instead of turn to him s nickname, so it makes sense to take it a rule to use the syntax «controller as».

Next comes the top menu with the buttons “Save Changes”, “Undo Changes”, “Refresh”, we hide the first two with ng-if if there are no changes, and with the help of ng-click we assign buttons to the corresponding functions.

Then we draw a list of categories using ng-repeat, the directive ng-class = "{active: vm.filterCategory === cat}" sets the element to the class active if the condition vm.filterCategory === cat is met, that is, tint the selected category. Next we will display a table with our shopping list, the first line will be the input fields of the name and category with the Add button, and then the list will go directly ng-repeat = “item in vm.listItems | filter: vm.isItemExists | orderBy: 'isBought' ”, here we used the filter filter, which was given the vm.isItemExists function to display only existing items, the orderBy filter sorts the list by the values ​​of the isBought property so that all purchased items are moved down.

Conclusion


Perhaps this and all that I wanted to tell for the first time (even a little more). I started writing the article in the summer, but due to lack of time I finished it just now. And during this half year, we have not stopped using breeze.js. It is especially fast and convenient to implement applications on it, where you need a grid with sorting, searching and pagination. Also, editing is done in two accounts, especially if validation does not go beyond the validation attributes on EntityFramework models.

Of the pitfalls, so far, we only noticed that breeze does not support the connection of many-to-many models without an intermediate class (when EntityFramework “thinks out” the intermediate table for us), with the intermediate class, of course, everything is fine.

For those who do not have time to create a new project, but there is a desire to experiment with the breeze - a link to a ready-made solution .

PS I am writing for the first time, so I ask the reader to take a minute and comment in the comments on the content / design / style of presentation and, if there is, suggestions for future articles.

PPS The following article - Breeze Server - we delimit access to objects using attributes

Source: https://habr.com/ru/post/246441/


All Articles