[译]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

相关文章
相关标签/搜索