[譯]ABP框架使用AngularJs,ASP.NET MVC,Web API和EntityFramework構建N層架構的SPA應用程序

本文演示ABP框架如何使用AngularJs,ASP.NET MVC,Web API 和EntityFramework構建基於N層架構的多語言SPA應用程序

 

 

Simple Task System Screenshot
演示程序截圖如上所示.javascript

內容摘要

介紹

在這篇文章, 我將基於如下框架演示如何開發單頁面的(SPA) 應用程序 :html

  • ASP.NET MVC 和 ASP.NET Web API  web站點的基礎框架.
  • Angularjs  SPA 框架.
  • EntityFramework  ORM (Object-Relational Mapping) 框架
  • Castle Windsor – 依賴注入框架.
  • Twitter Bootstrap – 前端框架.
  • Log4Net 來記錄日誌, AutoMapper 實體對象映射.
  • 和 ASP.NET Boilerplate 做爲應用程序模板框架.

ASP.NET Boilerplate [1] 是一個開源的應用程序框架,它包含了一些經常使用的組件讓您可以快速簡單的開發應用. 它集成一些經常使用的基礎框架. 好比依賴注入領域驅動設計 和分層架構. 本應用演示ABP如何實現驗證,異常處理,本地化響應式設計.前端

使用 boilerplate 模板建立程序

ASP.NET Boilerplate給咱們提供了一個很是好的構建企業應用的模板,以便節約咱們構建應用程序的時間。
在www.aspnetboilerplate.com/Templates目錄,咱們可使用模板建立應用。java

 

Create template by ASP.NET Boilerplate

這裏我選擇 SPA(單頁面程序)使用AngularJs EntityFramework框架. 而後輸入項目名稱SimpleTaskSystem.來建立和下載應用模版.下載的模版解決方案包含5個項目. Core 項目是領域 (業務) 層, Application 項目是應用層, WebApi 項目實現了 Web Api 控制器, Web 項目是展現層,最後EntityFramework 項目實現了EntityFramework框架.git

Note: 若是您下載本文演示實例, 你會看到解決方案有7個項目. 我把NHibernate和Durandal都放到本演示中.若是你對NHibernate,Durandal不感興趣的話,能夠忽略它們.github

建立實體對象

我將建立一個簡單的應用程序來演示任務和分配任務的人. 因此我須要建立Task實體對象和Person實體對象.web

Task實體對象簡單的定義了一些描述:CreationTime和Task的狀態. 它一樣包含了Person(AssignedPerson)的關聯引用:數據庫

 

public class Task : Entity<long> { [ForeignKey("AssignedPersonId")] public virtual Person AssignedPerson{ get; set; } public virtual int? AssignedPersonId { get; set; } public virtual string Description { get; set; } public virtual DateTime CreationTime { get; set; } public virtual TaskState State { get; set; } public Task() { CreationTime = DateTime.Now; State = TaskState.Active; } }

 

 

Person實體對象簡單的定義下Name:c#

public class Person : Entity { public virtual string Name { get; set; } }

ASP.NET Boilerplate 給 Entity 類定義了 Id 屬性. 從Entity 類派生實體對象將繼承Id屬性. Task 類從 Entity<long>派生將包含 long 類型的ID. Person 類包含 int 類型的ID. 由於int是默認的主鍵類型, 這裏我不須要特殊指定.api

我在這個 Core 項目裏添加實體對象由於實體對象是屬於領域/業務層的.

建立 DbContext

衆所周知, EntityFramework使用DbContext 類工做. 咱們首先得定義它. ASP.NET Boilerplate建立了一個DbContext模板給咱們. 咱們只須要添加 IDbSets給 Task and Person. 完整的 DbContext 類以下:

public class SimpleTaskSystemDbContext : AbpDbContext { public virtual IDbSet<Task> Tasks { get; set; } public virtual IDbSet<Person> People { get; set; } public SimpleTaskSystemDbContext(): base("Default") { } public SimpleTaskSystemDbContext(string nameOrConnectionString): base(nameOrConnectionString) { } }

咱們還須要在web.config添加默認的鏈接字符串. 以下:

<add name="Default" connectionString="Server=localhost; Database=SimpleTaskSystem; Trusted_Connection=True;" providerName="System.Data.SqlClient" />

建立數據庫遷移

咱們將使用EntityFramework的Code First模式來遷移和建立數據庫. ASP.NET Boilerplate模板默認支持簽約但須要咱們添加以下的Configuration 類:

internalinternal sealed class Configuration : DbMigrationsConfiguration<SimpleTaskSystem.EntityFramework.SimpleTaskSystemDbContext> { public Configuration() { AutomaticMigrationsEnabled = false; } protected override void Seed(SimpleTaskSystem.EntityFramework.SimpleTaskSystemDbContext context) { context.People.AddOrUpdate( p => p.Name, new Person {Name = "Isaac Asimov"}, new Person {Name = "Thomas More"}, new Person {Name = "George Orwell"}, new Person {Name = "Douglas Adams"} ); } }

另外一種方法, 是在初始化的是添加4個Person. 我將建立初始遷移.打開包管理控臺程序並輸入如下命令:

Visual studio Package manager console

Add-Migration 「InitialCreate」 命令建立 InitialCreate 類以下:

public partial class InitialCreate : DbMigration { public override void Up() { CreateTable( "dbo.StsPeople", c => new { Id = c.Int(nullable: false, identity: true), Name = c.String(), }) .PrimaryKey(t => t.Id); CreateTable( "dbo.StsTasks", c => new { Id = c.Long(nullable: false, identity: true), AssignedPersonId = c.Int(), Description = c.String(), CreationTime = c.DateTime(nullable: false), State = c.Byte(nullable: false), }) .PrimaryKey(t => t.Id) .ForeignKey("dbo.StsPeople", t => t.AssignedPersonId) .Index(t => t.AssignedPersonId); } public override void Down() { DropForeignKey("dbo.StsTasks", "AssignedPersonId", "dbo.StsPeople"); DropIndex("dbo.StsTasks", new[] { "AssignedPersonId" }); DropTable("dbo.StsTasks"); DropTable("dbo.StsPeople"); } }

咱們已經建立了數據庫類, 可是還沒建立那數據庫. 下面來建立數據庫,命令以下:

PM> Update-Database

這個命令幫咱們建立好了數據庫並填充了初始數據:

Database created by EntityFramework Migrations

當咱們修改實體類時, 咱們能夠經過 Add-Migration 命令很容易的建立遷移類。須要更新數據庫的時候則能夠經過 Update-Database 命令. 關於更多的數據庫遷移, 能夠查看 entity framework的官方文檔.

定義庫

在領域驅動設計中, repositories 用於實現特定的代碼. ASP.NET Boilerplate 使用 IRepository 接口給每一個實體自動的建立 repository . IRepository 定義了經常使用的方法如 select, insert, update, delete 等,更多以下:

IRepository interface

咱們還能夠根據咱們的須要來擴展 repository . 若是須要單獨的實現接口的話,首先須要繼承 repositories 接口. Task repository 接口以下:

public interface ITaskRepository : IRepository<Task, long> { List<Task> GetAllWithPeople(int? assignedPersonId, TaskState? state); }

這繼承了ASP.NET Boilerplate 的 IRepository 接口. ITaskRepository 默認定義了這些方法. 咱們也能夠添加本身的方法 GetAllWithPeople(…).

這裏不須要再爲Person建立 repository 了,由於默認的方法已經足夠. ASP.NET Boilerplate 提供了通用的 repositories 而不須要建立 repository 類. 在’構建應用程序服務層’ 章節中的TaskAppService 類中將演示這些..

repository 接口被定義在Core 項目中由於它們是屬於領域/業務層的.

實現庫

咱們須要實現上述的 ITaskRepository 接口. 咱們在EntityFramework 項目實現 repositories. 所以,領域層徹底獨立於 EntityFramework.

當咱們建立項目模板, ASP.NET Boilerplate 在項目中爲 repositories 定義了一些基本類: SimpleTaskSystemRepositoryBase. 這是一個很是好的方式來添加基本類,由於咱們能夠爲repositories稍後添加方法. 你能夠看下面代碼定義的這個類.定義TaskRepository 從它派生:

public class TaskRepository : SimpleTaskSystemRepositoryBase<Task, long>, ITaskRepository { public List<Task> GetAllWithPeople(int? assignedPersonId, TaskState? state) { //In repository methods, we do not deal with create/dispose DB connections, DbContexes and transactions. ABP handles it. var query = GetAll(); //GetAll() returns IQueryable<T>, so we can query over it. //var query = Context.Tasks.AsQueryable();  //Alternatively, we can directly use EF's DbContext object. //var query = Table.AsQueryable();  //Another alternative: We can directly use 'Table' property instead of 'Context.Tasks', they are identical. //Add some Where conditions... if (assignedPersonId.HasValue) { query = query.Where(task => task.AssignedPerson.Id == assignedPersonId.Value); } if (state.HasValue) { query = query.Where(task => task.State == state); } return query .OrderByDescending(task => task.CreationTime) .Include(task => task.AssignedPerson) //Include assigned person in a single query .ToList(); } }

上述代碼 TaskRepository 派生自 SimpleTaskSystemRepositoryBase 並實現了 ITaskRepository.

構建應用程序服務層

應用程序服務層被用於分離表示層和領域層並提供一些界面的樣式方法. 在 Application 組件中定義應用程序服務. 首先定義 task 應用程序服務的接口:

public interface ITaskAppService : IApplicationService { GetTasksOutput GetTasks(GetTasksInput input); void UpdateTask(UpdateTaskInput input); void CreateTask(CreateTaskInput input); }

ITaskAppService繼承自IApplicationService. 所以ASP.NET Boilerplate自動的提供了一些類的特性(像依賴注入和驗證).如今咱們來實現ITaskAppService:

public class TaskAppService : ApplicationService, ITaskAppService { //These members set in constructor using constructor injection. private readonly ITaskRepository _taskRepository; private readonly IRepository<Person> _personRepository; /// <summary>  ///In constructor, we can get needed classes/interfaces. ///They are sent here by dependency injection system automatically. /// </summary>  public TaskAppService(ITaskRepository taskRepository, IRepository<Person> personRepository) { _taskRepository = taskRepository; _personRepository = personRepository; } public GetTasksOutput GetTasks(GetTasksInput input) { //Called specific GetAllWithPeople method of task repository. var tasks = _taskRepository.GetAllWithPeople(input.AssignedPersonId, input.State); //Used AutoMapper to automatically convert List<Task> to List<TaskDto>. return new GetTasksOutput { Tasks = Mapper.Map<List<TaskDto>>(tasks) }; } public void UpdateTask(UpdateTaskInput input) { //We can use Logger, it's defined in ApplicationService base class. Logger.Info("Updating a task for input: " + input); //Retrieving a task entity with given id using standard Get method of repositories. var task = _taskRepository.Get(input.TaskId); //Updating changed properties of the retrieved task entity. if (input.State.HasValue) { task.State = input.State.Value; } if (input.AssignedPersonId.HasValue) { task.AssignedPerson = _personRepository.Load(input.AssignedPersonId.Value); } //We even do not call Update method of the repository. //Because an application service method is a 'unit of work' scope as default. //ABP automatically saves all changes when a 'unit of work' scope ends (without any exception). } public void CreateTask(CreateTaskInput input) { //We can use Logger, it's defined in ApplicationService class.  Logger.Info("Creating a task for input: " + input); //Creating a new Task entity with given input's properties var task = new Task { Description = input.Description }; if (input.AssignedPersonId.HasValue) { task.AssignedPersonId = input.AssignedPersonId.Value; } //Saving entity with standard Insert method of repositories. _taskRepository.Insert(task); } }

TaskAppService 使用倉儲來操做數據庫. 它過在構造函數中注入倉儲. ASP.NET Boilerplate實現了依賴注入, 因此咱們能夠自由的使用構造函數注入和屬性注入 (更多的依賴注入章節查看 ASP.NET Boilerplate 文檔).

注意:咱們使用PersonRepository來注入IRepository<Person>. ASP.NET Boilerplate會自動給咱們的實體建立庫. 若是默認的庫接口夠用的話,咱們就不須要在從新定義實體庫類型了.

應用服務層的方法使用了Data Transfer Objects (DTOs)協議. 這是一個很是好的方式,我一樣建議你們這麼作. 可是你也不必非這麼作不可,若是你能處理你的問題並傳輸到展現層。.

GetTasks方法中,咱們使用GetAllWithPeople方法,它返回List<Task>類型, 但我可能須要返回一個List<TaskDto>給展示層. 這時候AutoMapper自動的幫助咱們把TaskDto對象轉換爲Task對象.GetTasksInput和GetTasksOutput是特殊的DTOs被定義在GetTasks方法中.

UpdateTask方法中,我從數據庫返回Task(使用IRepository的Get方法)並更新Task屬性.注意我並無調用reponsitory的Update方法. ASP.NET Boilerplate實現工做單元模式. 因此,在應用服務層的全部改變都是以工做單元形式,並最後自動保存到數據庫中.

CreateTask方法中,使用IRepository的Insert方法建立新Task到數據庫.

ASP.NET Boilerplate的ApplicationService類提供一些屬性來簡化開發應用服務.例如,它定義了Logger給日誌. 你也能夠本身實現,但要繼承IApplicationService接口(注意:ITaskAppService繼承自IApplicationService).

驗證

ASP.NET Boilerplate在服務層方法輸入的參數. CreateTask method getsCreateTaskInput as parameter:

public class CreateTaskInput { public int? AssignedPersonId { get; set; } [Required] public string Description { get; set; } }

這裏的Description標記是必須輸入的意思. 這裏你可使用任何的Data Annotation屬性. 若是你須要使用自定義屬性, 你能夠繼承ICustomValidate 接口來實現UpdateTaskInput:

public class UpdateTaskInput : ICustomValidate { [Range(1, long.MaxValue)] public long TaskId { get; set; } public int? AssignedPersonId { get; set; } public TaskState? State { get; set; } public void AddValidationErrors(List<ValidationResult> results) { if (AssignedPersonId == null && State == null) { results.Add(new ValidationResult("Both of AssignedPersonId and State can not be null in order to update a Task!", new[] { "AssignedPersonId", "State" })); } } public override string ToString() { return string.Format("[UpdateTask > TaskId = {0}, AssignedPersonId = {1}, State = {2}]", TaskId, AssignedPersonId, State); } }

你能夠替換AddValidationErrors方法裏面的內容來自定義錯誤代碼.

處理異常

注意咱們沒有作任何的異常處理. ASP.NET Boilerplate 自動的處理了異常, 日誌和返回友好的錯誤信息給客戶端. 客戶端僅需處理錯誤信息並顯示給用戶.  異常處理文檔查看.

構建Web API服務

我把應用服務層暴露給遠程客戶端,以便AngularJs可以簡單的使用AJAX調用.

ASP.NET Boilerplate使用自動的方法來提供應該程序服務,即ASP.NET Web API.如DynamicApiControllerBuilder:

DynamicApiControllerBuilder
    .ForAll<IApplicationService>(Assembly.GetAssembly(typeof (SimpleTaskSystemApplicationModule)), "tasksystem") .Build();

在本段代碼中, ASP.NET Boilerplate找全部繼承自IApplicationService接口的方法並建立web api controller給每一個應用程序服務類. 這裏還有幾種其餘的查找control的方法.咱們將在如何使用AJAX調用服務中看到.

開發SPA

我要在項目中實現一個單頁面的web應用程序. AngularJs(Google開發的)是一個最流行的最火的SPA框架.

ASP.NET Boilerplate提供了一個模版來簡單的使用AngularJs.這個模版有兩個頁面(Home 和About)能平滑過分. 使用了Twitter的Bootstrap前端框架.ASP.NET Boilerplate它默認定義了English和Turkish兩種本地語言(你能夠簡單的添加刪除語言).

咱們先改變路由模版. ASP.NET Boilerplate模版使用AngularUI-Router, 這其實是標準的AngularJs路由.它提供了路由狀態模式. 咱們有兩個views: task list 和 new task. 因此咱們將在app.js中來作以下的定義:

app.config([ '$stateProvider', '$urlRouterProvider', function ($stateProvider, $urlRouterProvider) { $urlRouterProvider.otherwise('/'); $stateProvider .state('tasklist', { url: '/', templateUrl: '/App/Main/views/task/list.cshtml', menu: 'TaskList' //Matches to name of 'TaskList' menu in SimpleTaskSystemNavigationProvider }) .state('newtask', { url: '/new', templateUrl: '/App/Main/views/task/new.cshtml', menu: 'NewTask' //Matches to name of 'NewTask' menu in SimpleTaskSystemNavigationProvider }); } ]);

app.js是javascript的入口文件,用來配置SPA的啓動. 注意這裏使用的是cshtml示圖文件! 通常狀況下, AngularJs的顯示頁面是html. 而ASP.NET Boilerplate使用cshtml文件. 所以強大的razor引擎將會把cshtml生成HTML.

ASP.NET Boilerplate提供了基礎框架來建立和顯示菜單.能夠用c#或javascript語言來定義菜單. SimpleTaskSystemNavigationProvider類來建立菜單 ,header.js/header.cshtml用來顯示菜單.

首先咱們建立一個Angular controllertask列表頁面:

(function() { var app = angular.module('app'); var controllerId = 'sts.views.task.list'; app.controller(controllerId, [ '$scope', 'abp.services.tasksystem.task', function($scope, taskService) { var vm = this; vm.localize = abp.localization.getSource('SimpleTaskSystem'); vm.tasks = []; $scope.selectedTaskState = 0; $scope.$watch('selectedTaskState', function(value) { vm.refreshTasks(); }); vm.refreshTasks = function() { abp.ui.setBusy( //Set whole page busy until getTasks complete null, taskService.getTasks({ //Call application service method directly from javascript state: $scope.selectedTaskState > 0 ? $scope.selectedTaskState : null }).success(function(data) { vm.tasks = data.tasks; }) ); }; vm.changeTaskState = function(task) { var newState; if (task.state == 1) { newState = 2; //Completed } else { newState = 1; //Active } taskService.updateTask({ taskId: task.id, state: newState }).success(function() { task.state = newState; abp.notify.info(vm.localize('TaskUpdatedMessage')); }); }; vm.getTaskCountText = function() { return abp.utils.formatString(vm.localize('Xtasks'), vm.tasks.length); }; } ]); })();

我定義了名爲’sts.views.task.list‘的控制器. 這是個人命名習慣(for scalable code-base)但你也能夠簡單的命名爲’ListController’. AngularJs也使用依賴注入. 咱們這裏注入’$scope‘和’abp.services.tasksystem.task‘.首先是Angular的scope變量再次是ITaskAppService自動建立的的javascript服務代理(咱們在’Build Web API services’以前就建立了).

ASP.NET Boilerplate 提供了基礎設施本地語言文件用於服務端和客戶端. 

vm.taks是頁面的任務列表.vm.refreshTasks方法內執行了taskService獲取task的數據集合.這是在selectedTaskState修改的時候被調用(查看執行使用$scope.$watch).

正如你所看到的,調用應用服務的方法是如此的簡單!這就是ASP.NET Boilerplate的特性.它生成了Web API層和Javascript代理層.所以咱們調用應用服務層像調用javascript方法同樣. 它徹底集成進了AngularJs (使用Angular的$http service).

咱們來看看任務列表的頁面:

<div class="panel panel-default" ng-controller="sts.views.task.list as vm"> <div class="panel-heading" style="position: relative;"> <div class="row"> <!-- Title --> <h3 class="panel-title col-xs-6"> @L("TaskList") - <span>{{vm.getTaskCountText()}}</span> </h3> <!-- Task state combobox --> <div class="col-xs-6 text-right"> <select ng-model="selectedTaskState"> <option value="0">@L("AllTasks")</option> <option value="1">@L("ActiveTasks")</option> <option value="2">@L("CompletedTasks")</option> </select> </div> </div> </div> <!-- Task list --> <ul class="list-group" ng-repeat="task in vm.tasks"> <div class="list-group-item"> <span class="task-state-icon glyphicon" ng-click="vm.changeTaskState(task)" ng-class="{'glyphicon-minus': task.state == 1, 'glyphicon-ok': task.state == 2}"></span> <span ng-class="{'task-description-active': task.state == 1, 'task-description-completed': task.state == 2 }">{{task.description}}</span> <br /> <span ng-show="task.assignedPersonId > 0"><span class="task-assignedto">{{task.assignedPersonName}}</span> </span> <span class="task-creationtime">{{task.creationTime}}</span> </div> </ul> </div>

ng-controller 屬性(在第一行) 綁定頁面的controller. @L(「TaskList」) 獲取」task list」的本地語言文本(服務器端解析html的時候執行). 由於這是個cshtml文件.

ng-model 綁定combobox和javascript變量. 當變量值改變combobox就會被更新.當改變combobox變量就會被更新. 這是AngularJs的雙向綁定.

ng-repeat 是Angular的另外一個指令用於循環集合裏面的值. 當集合改變(例如增長值),它會自動更新界面. 這是AngularJs的另外一個強大特性.

注意: 當你應該在頁面添加javascript文件 (例如, 添加’task list’控制器)時.能夠改爲在添加Home\Index.cshtml模版時添加.

本地化

ASP.NET Boilerplate提供了靈活健壯的本地化系統.你可使用XML文件或者資源文件作爲本地化的數據源.你也能夠自定義數據源.更多信息能夠查看文檔. 本示例使用XML文件演示(在web應用項目的Localization文件夾裏):

<?xml version="1.0" encoding="utf-8" ?> <localizationDictionary culture="en"> <texts> <text name="TaskSystem" value="Task System" /> <text name="TaskList" value="Task List" /> <text name="NewTask" value="New Task" /> <text name="Xtasks" value="{0} tasks" /> <text name="AllTasks" value="All tasks" /> <text name="ActiveTasks" value="Active tasks" /> <text name="CompletedTasks" value="Completed tasks" /> <text name="TaskDescription" value="Task description" /> <text name="EnterDescriptionHere" value="Task description" /> <text name="AssignTo" value="Assign to" /> <text name="SelectPerson" value="Select person" /> <text name="CreateTheTask" value="Create the task" /> <text name="TaskUpdatedMessage" value="Task has been successfully updated." /> <text name="TaskCreatedMessage" value="Task {0} has been created successfully." /> </texts> </localizationDictionary>

使用單元測試

ASP.NET Boilerplate 是可測試的. 個人另外一篇文章展現瞭如何使用ABP 基本項目集成集成單元測試. 查看文章: Unit testing in C# using xUnit, Entity Framework, Effort and ASP.NET Boilerplate.

摘要

在這篇文章, 我闡述瞭如何在 ASP.NET MVC web 應用中開發N層架構的SPA應用. ASP.NET Boilerplate 使用很是好的方式而且如此簡單的建立了應用. 下面的連接能夠得到更多信息:

文章歷史

  • 2016-10-26: Upgraded sample project to ABP v1.0.
  • 2016-07-19: Updated article and sample project for ABP v0.10.
  • 2015-06-08: Updated article and sample project for ABP v0.6.3.1.
  • 2015-02-20: Added link to unit test article and updated the sample project
  • 2015-01-05: Updated sample project for ABP v0.5.
  • 2014-11-03: Updated article and sample project for ABP v0.4.1.
  • 2014-09-08: Updated article and sample project for ABP v0.3.2.
  • 2014-08-17: Updated sample project to ABP v0.3.1.2.
  • 2014-07-22: Updated sample project to ABP v0.3.0.1.
  • 2014-07-11: Added screenshot of ‘Enable-Migrations’ command.
  • 2014-07-08: Updated sample project and article.
  • 2014-07-01: First publish of the article.

引用

[1] ASP.NET Boilerplate 官網: http://www.aspnetboilerplate.com

相關文章
相關標籤/搜索