ABP示例程序-使用AngularJs,ASP.NET MVC,Web API和EntityFramework创建N层的单页面Web应用
Posted RAINAI
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ABP示例程序-使用AngularJs,ASP.NET MVC,Web API和EntityFramework创建N层的单页面Web应用相关的知识,希望对你有一定的参考价值。
本片文章翻译自ABP在CodeProject上的一个简单示例程序,网站上的程序是用ABP之前的版本创建的,模板创建界面及工程文档有所改变,本文基于最新的模板创建。通过这个简单的示例可以对ABP有个更深入的了解,每个工程里应该写什么样的代码,代码如何组织以及ABP是如何在工程中发挥作用的。
源文档地址:https://www.codeproject.com/Articles/791740/Using-AngularJs-ASP-NET-MVC-Web-API-and-EntityFram
源码可以下载文档中的示例代码,也可以下载我使用最新模板创建的示例工程,github地址:https://github.com/YSmileX/SimpleTaskSystem0726
使用AngularJs,ASP.NET MVC,Web API和EntityFramework创建N层的单页面Web应用
在本文中,将展示给你如何使用下面的工具从头到尾发布一个单页面Web应用(SPA):
- ASP.NET MVC和ASP.NET Web API作为Web框架。
- Angularjs作为SPA框架
- EntityFramework作为ORM(Object-Relational Mapping)框架。
- Castle Windsor作为依赖注入框架。
- Twitter Bootstrap作为html/CSS框架。
- 日志使用Log4Net,对象到对象映射使用AutoMapper。
- ASP.NET Boilerplate作为启动模板和应用框架。
ABP是一个开源的应用框架,它结合了这些所有的框架和类库可以很容易的发布你的应用。它使用最佳实践提供给我们一个基础设施来发布应用。它天生支持依赖注入、领域驱动和分层架构。示例应用还实现了校验、异常处理、本地化和响应式设计。
ABP提供了模板来节省我们创建一个新应用的时间,模板中包含并配置了最好的工具来构建企业级别的Web引用。
让我们到aspnetboilerplate.com/Templates来从模板构建我们的应用:
这里我们选择ASP.NET MVC 5.X标签页,然后选择SPA(Sigle Page Application) with AngularJs,ORM选择EntityFramework。工程名称中输入SimpleTaskSystem。点击“Create my project!”按钮就会创建并下载我们的解决方案。
在解决方案中包含5个工程。Core工程为领域(业务)层,Application工程为应用层,WebApi工程实现Web Api控制器,Web工程为展示层,EntityFramework工程实现Entityframework。
注意:如果你从本文中下载示例解决方案,解决方案中会有7个工程。我实现了NHibernate和Durandal的支持。如果你对NHibernate或Durandal不感兴趣,可以忽略这两个工程。
我将创建一个简单的应用,这个应用可以创建tasks并把这些tasks分配给people。所以我需要Task和Person实体。
Task实体简单定义了Description,CreationTime和State。它还有一个对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实体更简单,仅定义了person的Name:
public class Person : Entity { public virtual string Name { get; set; } }
ABP提供了Entity类,它定义了Id属性。我从这个实体类派生实体。因为我从Entity<long>派生,所以Task类有一个long类型的Id。Person类有一个int类型的Id。因为int为默认的主键类型,我没有指定它。
我在Core工程中定义实体,因为实体为领域/业务层的一部分。
如你所知,EntityFramework需要DbContext类。我们首先定义它。ABP模板为我们创建了一个DbContext。我仅仅需要为Task和Person添加IDbSets。这是我的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中的Default连接字符串。定义如下所示:
<add name="Default" connectionString="Server=localhost; Database=SimpleTaskSystem; Trusted_Connection=True;" providerName="System.Data.SqlClient" />
我们使用EntityFramework的Code First迁移来创建和维护数据库模式。ABP模板默认启用迁移并添加了一个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"} ); } }
在Seed方法中,我添加了4个people作为初始化数据。现在,将创建初始迁移。打开包管理控制台并键入下面的命令:我创建的工程名称为SimpleTaskSystem0726,2017.7.26从ABP官网模板创建
Add-Migration "InitalCreate"命令创建了一个名为InitialCreate的类,如下所示:
public partial class InitialCreate : DbMigration { public override void Up() { CreateTable( "dbo.People", c => new { Id = c.Int(nullable: false, identity: true), Name = c.String(), }) .PrimaryKey(t => t.Id); CreateTable( "dbo.Tasks", 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.People", t => t.AssignedPersonId) .Index(t => t.AssignedPersonId); } public override void Down() { DropForeignKey("dbo.Tasks", "AssignedPersonId", "dbo.People"); DropIndex("dbo.Tasks", new[] { "AssignedPersonId" }); DropTable("dbo.Tasks"); DropTable("dbo.People"); } }
我们创建了创建数据所需要的类,但是还没有创建数据库。运行下面的指令创建数据库:
PM> Update-Database
这个命令会运行迁移,创建数据库并创建初始数据:
当我们改变实体类时,可以使用Add-Migration命令创建新的迁移类,Update-Database命令更新数据库。要学习更多源于数据库迁移的知识,参见framework的文档。
在领域驱动设计中,仓储用于实现特定数据库的代码。ABP使用泛型IRepository接口自动为每一个实体创建了一个仓储。IRepository为select,insert,update,delete还有其他一些定义了共同的方法:
我们可以基于需求扩展这些仓储。我将扩展它来创建一个Task仓储。我想接口与实现分离开,首先创建仓储接口。这里是Task仓储接口:
public interface ITaskRepository : IRepository<Task, long> { List<Task> GetAllWithPeople(int? assignedPersonId, TaskState? state); }
它扩展了ABP的泛型IRepository接口。所以,ITaskRepository默认继承了所有这些方法的定义。它也可以添加自己的方法,如我定义了GetAllWithPeople(...)。
没有必要为Person创建一个仓储,因为默认的方法已经足够使用。ABP提供了不用创建仓储类注入泛型仓储的方式。我们将在“构建应用服务”部分的TaskAppService类中见到它。
我在Core工程中定义了仓储接口,因为它们是领域/业务层的一部分。
我们应该实现上面定义的ITaskRepository接口。我在EntityFramework工程中实现仓储。这样,领域层完全独立于EntityFramework。
当我们创建工程模板时,ABP在我们的工程中为仓储定义了一个泛型基类:SimpleTaskSystemRepositoryBase。创建这样一个基类是好的实践,这样我们可以以后为我们的仓储添加共同的方法。你可以在代码中看见这个类的定义。我派生它来实现TaskRepository:
public class TaskRepository : SimpleTaskSystemRepositoryBase<Task, long>, ITaskRepository { public List<Task> GetAllWithPeople(int? assignedPersonId, TaskState? state) { //在仓储方法中,我们不需要处理创建/释放DBConnections,DbContext和transactions,ABP会处理。 var query = GetAll(); //GetAll() returns IQueryable<T>, 所以我们基于它查询. //var query = Context.Tasks.AsQueryable(); //我们也可以直接使用 EF\'s DbContext对象. //var query = Table.AsQueryable(); //另一个选择: 我们可以直接使用‘Table’属性取代‘Context.Tasks’,他们是一致的 //添加 Where 条件... 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) //在同一个查询里包含assiged person .ToList(); } }
TaskRepository派生自SimpleTaskSystemRepositoryBase并实现了我们定义的ITaskRepository。
GetAllWithPeople是我们特定的方法来获取tasks,方法中包含AssignedPerson(预获取)并可以根据一些条件选择性的过滤。我们可以在仓储中自由使用Context(EF`s DBContext)对象和数据库。ABP为我们管理数据库连接,事务,创建并释放DbContext(参见文档了解更多信息)。
应用服务通过提供外观方法来隔离展示层和领域层。我在工程的Application程序集中定义应用服务。首先,为task应用服务定义接口:
public interface ITaskAppService : IApplicationService { GetTasksOutput GetTasks(GetTasksInput input); void UpdateTask(UpdateTaskInput input); void CreateTask(CreateTaskInput input); }
ITaskAppService扩展了IApplicationService。这样,ABP自动为这个类提供一些特征(如依赖注入和校验)。现在,让我们实现ITaskAppService:
public class TaskAppService : ApplicationService, ITaskAppService { //在构造函数中使用构造函数注入设置这些成员 private readonly ITaskRepository _taskRepository; private readonly IRepository<Person> _personRepository; /// <summary> /// 在构造函数中,我们可以获取需要的类/接口。他们自动被依赖注入系统初始化。 /// </summary> /// <param name="taskRepository"></param> /// <param name="personRepository"></param> public TaskAppService(ITaskRepository taskRepository, IRepository<Person> personRepository) { _taskRepository = taskRepository; _personRepository = personRepository; } public GetTasksOutput GetTasks(GetTasksInput input) { //调用task仓储的GetAllWithPeople方法 var tasks = _taskRepository.GetAllWithPeople(input.AssignedPersonId, input.State); //使用AutoMapper自动将List<Task>转换为List<TaskDto> return new GetTasksOutput { Tasks = Mapper.Map<List<TaskDto>>(tasks) }; } public void UpdateTask(UpdateTaskInput input) { //我们可以使用Logger,它在应用服务基类中定义 Logger.Info("Updating a task for input:" + input); //使用仓储的标准方法Get通过给定的id重新获取task实体 var task = _taskRepository.Get(input.TaskId); //更新重新获取的task实体的属性 if (input.State.HasValue) { task.State = input.State.Value; } if (input.AssignedPersonId.HasValue) { task.AssignedPerson = _personRepository.Load(input.AssignedPersonId.Value); } //我们不需要调用仓储的Update方法。因为应用服务方法默认为一个工作单元。 //当工作单元结束时(没有任何异常),ABP自动保存所有更改。 } public void CreateTask(CreateTaskInput input) { //我们可以使用Logger,它在应用服务基类中定义 Logger.Info("Creating a task for input:" + input); //使用给定的input的属性创建一个新的Task var task = new Tasks.Task {Description = input.Description}; if (input.AssignedPersonId.HasValue) { task.AssignedPersonId = input.AssignedPersonId.Value; } //使用仓储标准Insert方法保存实体 _taskRepository.Insert(task); } }
TaskAppService使用仓储来操作数据库。它在构造函数中通过构造函数注入模式获取引用。ABP天生实现了依赖注入,所以我们可以自由使用构造函数注入或属性注入(参见ABP文档中的依赖注入)。
注意我们通过注入IRepository<Person>来使用PersonRepository。ABP自动为实体创建仓储。如果IRepository默认的方法对我们已足够,我们不需要再创建仓储类。
服务方法使用数据传输对象(DTOs)工作。这是一个最佳实践,我建议使用这种模式。但是,如果你可以处理在展示层暴露实体的问题就可以不使用它。
在GetTasks方法中,我使用了之前实现的GetAllWithPeople方法。它返回List<Task>但是我需要返回List<TaskDto>给展示层。AutoMapper帮助我们自动转换Task对象为TaskDto对象。GetTasksInput和GetTasksOut是为GetTasks方法定义的特定DTOs。
在UpdateTask方法中,我从数据库重新获取Task(使用IRepository的Get方法)并更新Task的属性。注意,我没有调用仓储的Update方法。ABP实现了工作单元模式。所以,应用服务方法中的所有更改为一个工作单元(原子的),在方法结束的时候自动应用到数据库。
在CreateTask方法中,我简单创建了一个新的Task,使用IRepository的Insert方法插入到数据库。
ABP的ApplicationService类有一些属性可以简化发布应用服务。例如,它定义了Logger属性从来记录日志。所以,我们从ApplicationService派生TaskAppService并使用它的Logger属性。可以选择性的使用这个类但是必须实现IApplicationService(注意ITaskAppService扩展了IApplicationService)。
ABP自动校验应用服务方法的输入。CreateTask方法使用CreateTaskInput作为参数:
public class CreateTaskInput { public int? AssignedPersonId { get; set; } [Required] public string Description { get; set; } }
这里,Description标记为Required。你可以使用任何的数据标记特性。如果你想创建自定义校验,可以实现ICustomValidate接口:
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方法里编写自定义校验代码。
注意我们不需要处理任何异常。ABP自动处理异常,记录并返回一个恰当的错误信息到客户端。在客户端处理这些错误信息并显示给用户。实际上,这也适用于ASP.NET MVC和Web API控制器actions。因为我们将使用Web API暴露TaskAppService,我们不需要处理异常。参见异常处理文档了解更多详情。
我想将我的应用服务暴露给远程客户端。这样,我的AngularJs应用可以很容易的使用AJAX调用这些服务方法。
ABP提供了一种自动暴露应用服务作为ASP.NET Web API的方法。我仅仅使用DynamiApicControllerBuilder,如下所示:
DynamicApiControllerBuilder .ForAll<IApplicationService>(Assembly.GetAssembly(typeof (SimpleTaskSystemApplicationModule)), "tasksystem") .Build();
对于这个示例,ABP在应用层程序集中查找所有集成IApplicationService的接口并为每一个应用服务方法创建一个Web api控制器。还有可选的语法可以实现更好的控制。我们将见到如何通过AJAX调用这些服务。
我将实现一个单页面应用,作为工程的用户接口。AngularJs(Google出品)是使用最广泛的SPA框架之一。
ABP提供了一个轻松使用AngularJs的模板。这个模板有两个pages(Home和About),可以平滑的在这两个页面之间切换。使用Twitter BootStrap作为HTML/CSS框架(因此,它是响应式的)。它还使用ABP的本地化系统(你可以简单的添加其他语言或移除其中一个)本地化为English和Turkish。
我们首先更改模板的路由。ABP模板使用AngularUI-Router,AngularJs的de-facto标准路由。它基于路由模式提供状态。我们将有两个视图: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文件作为视图。ABP使的可以使用cshtml文件。因此我们可以使用razor引擎生成HTML。
ABP提供了一个基础设施来创建和显示菜单。它允许在C#中定义菜单,可以同时在C#和javascript中使用。创建菜单参见SimpleTaskSystemNavigationProvider类,使用angular方式显示菜单参见header.js/header.cshtml。
首先,为task list视图创建一个Angular控制器:
(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( //直到getTasks完成之前设置整个页忙碌 null, taskService.getTasks({ //从javascript中直接调用服务方法 state: $scope.selectedTaskState > 0 ? $scope.selectedTaskState : null }).success(function(data) {//注意,如果angular版本高于1.4,success需改为then vm.tasks = data.tasks; }) ); }; vm.changeTaskState = function(task) { var newState; if (task.state == 1) { newState = 2; //完成 } else { newState = 1; //活动的 } taskService.updateTask({ taskId: task.id, state: newState }).success(function() {//注意,如果angular版本高于1.4,success需改为then task.state = newState; abp.notify.info(vm.localize(\'TaskUpdatedMessage\')); }); }; vm.getTaskCountText = function() { return abp.utils.formatString(vm.localize(\'Xtasks\'), vm.tasks.length); }; } ]); })();
控制器的名称定义为\'sts.views.taks.list\'。这是我的习惯(为了代码可扩展)但是你可以简化命名为\'ListController\'。AngularJs也可以使用依赖注入。这里我们注入了\'$scope\'和\'abp.services.tasksystem.task\'。第一个为Angular的scope变量,第二个为自动为ITaskAppService(我们在\'构建Web API\'部分创建的它)创建的javascript服务代理。
ABP提供了基础设施来在服务端和客户端使用相同的本地化文本(参见文档了解更多详情)。
vm.tasks为tasks的列表,将在视图中显示。vm.refreshTasks方法通过使用taskService获取任务填充这个数组。当selectedTaskState改变时调用它(使用$scope.$watch监视)。
如你所见,调用一个应用服务方法非常简单直接。这是ABP的一个特征。它生成Web API层和Javascript代理。因此,我们调用应用服务方法如同调用一个简单的javascript方法。它与AngularJs(使用Angular的$http服务)完全集成。
让我们看看task list视图编码:
<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>
以上是关于ABP示例程序-使用AngularJs,ASP.NET MVC,Web API和EntityFramework创建N层的单页面Web应用的主要内容,如果未能解决你的问题,请参考以下文章