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 MVCASP.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。所以我需要TaskPerson实体。

  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工程中定义实体,因为实体为领域/业务层的一部分。

创建DbContext

   如你所知,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,我们不需要处理异常。参见异常处理文档了解更多详情。

构建Web API服务

   我想将我的应用服务暴露给远程客户端。这样,我的AngularJs应用可以很容易的使用AJAX调用这些服务方法。

  ABP提供了一种自动暴露应用服务作为ASP.NET Web API的方法。我仅仅使用DynamiApicControllerBuilder,如下所示:

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

  对于这个示例,ABP在应用层程序集中查找所有集成IApplicationService的接口并为每一个应用服务方法创建一个Web api控制器。还有可选的语法可以实现更好的控制。我们将见到如何通过AJAX调用这些服务。

发布SPA

  我将实现一个单页面应用,作为工程的用户接口。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应用的主要内容,如果未能解决你的问题,请参考以下文章

在基于AngularJs架构的ABP项目中使用UEditor

ABP官方文档翻译 7.2 Hangfire集成

ABP初始化

ABP框架系列学习模块系统之示例开发

使用abp快速搭建web项目

[Abp vNext 源码分析] - 19. 多租户