使用 AngularJs、ASP.NET MVC、Web API 和 EntityFramework 构建 NLayered 单页 Web 应用程序






4.92/5 (240投票s)
使用 AngularJs、ASP.NET MVC、Web API、EntityFramework 和 ASP.NET Boilerplate 构建一个 NLayered、本地化、结构良好的单页 Web 应用程序
- 从 Github 仓库获取源代码
目录
- 引言
- 从样板模板创建应用程序
- 创建实体
- 创建 DbContext
- 创建数据库迁移
- 定义存储库
- 实现存储库
- 构建应用程序服务
- 构建 Web API 服务
- 开发 SPA
- 本地化
- 单元测试
- 摘要
- 文章历史
- 参考文献
引言
在本文中,我将向您展示如何使用以下工具从头开始开发一个单页 Web 应用程序(SPA)
- ASP.NET MVC 和 ASP.NET Web API 作为 Web 框架
- Angularjs 作为 SPA 框架
- EntityFramework 作为 ORM(对象关系映射)框架
- Castle Windsor 作为依赖注入框架
- Twitter Bootstrap 作为 HTML/CSS 框架
- Log4Net 用于日志记录,AutoMapper 用于对象到对象的映射
- 以及 ASP.NET Boilerplate 作为启动模板和应用程序框架
ASP.NET Boilerplate [1] 是一个开源应用程序框架,它结合了所有这些框架和库,让您可以轻松开始开发应用程序。它为我们提供了开发应用程序的最佳实践基础设施。它自然支持依赖注入、领域驱动设计和分层架构。示例应用程序还实现了验证、异常处理、本地化和响应式设计。
从 Boilerplate 模板创建应用程序
ASP.NET Boilerplate 通过提供模板来节省我们启动新应用程序的时间,这些模板组合并配置了构建企业级 Web 应用程序的最佳工具。
让我们访问 aspnetboilerplate.com/Templates 以从模板构建我们的应用程序...
在这里,我选择了ASP.NET MVC 5.x,带有AngularJs和EntityFramework的SPA(单页应用程序)。我还为我的项目名称输入了SimpleTaskSystem
。我不想包含身份验证选项,以获得最简单的项目模板。它创建并下载了我的解决方案。
解决方案中包含五个项目。Core 项目用于域(业务)层,Application 项目用于应用程序层,WebApi 项目用于实现 Web API 控制器,Web 项目用于表示层,最后是EntityFramework 项目用于EntityFramework
实现。
注意:如果您下载本文的示例解决方案,您将在解决方案中看到 7 个项目。我改进了模板,使其支持同一应用程序的 NHibernate 和 Durandal。如果您对 NHibernate 或 Durandal 不感兴趣,只需忽略这两个项目。
创建实体
我正在创建一个简单的应用程序来创建任务并将这些任务分配给人员。因此,我需要Task
和Person
实体。
Task
实体简单地定义了一个Description
、CreationTime
和一个Task
的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; }
}
ASP.NET Boilerplate 提供了定义Id
属性的Entity
类。我的实体继承自这个Entity
类。由于我继承自Entity<long>
,所以Task
类具有 long 类型的Id
。Person
类具有 int 类型的Id
。由于int
是默认的主键类型,所以我没有指定它。
我在Core
项目中定义了实体,因为实体是域/业务层的一部分。
创建 DbContext
正如您所知,EntityFramework
与DbContext
类一起工作。我们应该首先定义它。ASP.NET Boilerplate 模板为我们创建了一个DbContext
模板。我只为Task
和Person
添加了IDbSet
。这是我的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
连接string
。定义如下
<add name="Default" connectionString="Server=localhost;
Database=SimpleTaskSystem; Trusted_Connection=True;" providerName="System.Data.SqlClient" />
创建数据库迁移
我们将使用EntityFramework
的代码优先迁移来创建和维护数据库模式。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"}
);
}
}
在Seed
方法中,我添加了四个人作为初始数据。现在,我将创建初始迁移。我打开了程序包管理器控制台并输入了以下命令
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
此命令运行迁移,创建数据库并为我们填充初始数据
当我们更改Entity
类时,我们可以使用Add-Migration
命令轻松创建新的迁移类,并使用Update-Database
命令更新数据库。要了解有关数据库迁移的更多信息,请参阅 entity framework 的文档。
定义存储库
在领域驱动设计中,存储库用于实现特定于数据库的代码。ASP.NET Boilerplate 使用泛型IRepository 接口
为每个实体自动创建存储库。IRepository
定义了select
、insert
、update
、delete
以及其他一些通用方法
我们可以根据需要扩展这些存储库。我将扩展它来创建一个Task
存储库。由于我想将interface
与实现分离,所以我首先声明存储库的interface
。这是Task
存储库interface
public interface ITaskRepository : IRepository<Task, long>
{
List<Task> GetAllWithPeople(int? assignedPersonId, TaskState? state);
}
它继承了 ASP.NET Boilerplate 的泛型IRepository interface
。因此,ITaskRepository
本身定义了所有这些默认方法。它还可以添加自己的方法,如我定义的GetAllWithPeople(...)
。
由于默认方法对我来说足够了,因此无需为Person
创建存储库。ASP.NET Boilerplate 提供了一种在不创建存储库类的情况下注入泛型存储库的方法。我们将在“构建应用程序服务”部分的TaskAppService
类中看到它。
我在Core
项目中定义了存储库接口,因为它们是域/业务层的一部分。
实现存储库
我们应该实现上面定义的ITaskRepository interface
。我正在EntityFramework
项目中实现存储库。因此,域层完全独立于EntityFramework
。
当我们创建项目模板时,ASP.NET Boilerplate 在我们的项目中定义了一个用于存储库的泛型基类:SimpleTaskSystemRepositoryBase
。拥有这样一个基类是一个好习惯,因为我们可以稍后为我们的存储库添加一些通用方法。您可以在代码中看到这个类的定义。我只是为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
。
GetAllWithPeople
是我们获取已分配人员(预取)并可选地按某些条件过滤的任务的特定方法。我们可以自由使用 Context(EF 的DBContext
)对象和数据库。ASP.NET Boilerplate 为我们管理数据库连接、事务、创建和处置DbContext
(有关更多信息,请参阅 文档)。
构建应用程序服务
应用程序服务用于通过提供外观样式的方法来分离表示层和域层。我在项目中的Application
程序集中定义了应用程序服务。首先,我定义任务应用程序服务的interface
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 中依赖注入的更多信息,请参阅 文档)。
请注意,我们通过注入IRepository<Person>
来使用PersonRepository
。ASP.NET Boilerplate 会自动为我们的实体创建存储库。如果IRepository
的默认方法对我们来说足够,我们不必创建存储库类。
应用程序服务方法使用数据传输对象(DTO)。这是一个最佳实践,我强烈建议使用此模式。但只要您能处理将实体暴露给表示层带来的问题,您就不必这样做。
在GetTasks
方法中,我使用了之前实现的GetAllWithPeople
方法。它返回一个List<Task>
,但我需要向表示层返回一个List<TaskDto>
。AutoMapper
在此处帮助我们自动将Task
对象转换为TaskDto
对象。GetTasksInput
和GetTasksOutput
是为GetTasks
方法定义的特殊 DTO。
在UpdateTask
方法中,我从数据库检索了Task
(使用IRepository
的Get
方法)并更改了Task
的属性。请注意,我甚至没有调用存储库的Update
方法。ASP.NET Boilerplate 实现了UnitOfWork模式。因此,应用程序服务方法中的所有更改都构成一个工作单元(原子性的),并在方法结束时自动应用于数据库。
在CreateTask
方法中,我只是创建了一个新的Task
并使用存储库的Insert
方法将其插入数据库。
ASP.NET Boilerplate 的ApplicationService
类包含一些属性,可以使开发应用程序服务更容易。例如,它定义了一个Logger
属性用于日志记录。因此,我们从ApplicationService
派生了TaskAppService
并在这里使用了它的Logger
属性。继承自这个类是可选的,但必须实现IApplicationService
(请注意ITaskAppService
继承自IApplicationService
)。
验证
ASP.NET Boilerplate 会自动验证应用程序服务方法的输入。CreateTask
方法以CreateTaskInput
作为参数
public class CreateTaskInput
{
public int? AssignedPersonId { get; set; }
[Required]
public string Description { get; set; }
}
这里,Description
标记为Required
。您可以在此处使用任何 Data Annotation 属性。如果您想进行一些自定义验证,可以像我在UpdateTaskInput
中实现的那样实现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
方法是我们编写自定义验证代码的地方。
处理异常
请注意,我们没有处理任何异常。ASP.NET Boilerplate 会自动处理异常、日志记录并将适当的错误消息返回给客户端。此外,在客户端,它会处理这些错误消息并显示给用户。实际上,这对于 ASP.NET MVC 和 Web API 控制器操作都是如此。由于我们将使用 Web API 公开TaskAppService
,因此我们无需处理异常。有关详细信息,请参阅 异常处理文档。
构建 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
的interface
,并为每个应用程序服务类创建一个 Web API 控制器。有替代的语法可以进行精细控制。我们将看到如何通过 AJAX 调用这些服务。
开发 SPA
我将实现一个单页 Web 应用程序作为我项目的用户界面。AngularJs(由 Google 开发)是最流行的 SPA 框架之一(可能是最顶级的)。
ASP.NET Boilerplate 提供了一个模板,可以轻松地开始使用 AngularJs。该模板有两个页面(主页和关于页),页面之间具有流畅的过渡。它使用 TwitterBootstrap作为 HTML/CSS 框架(因此是响应式的)。它还通过 ASP.NET Boilerplate 的本地化系统被本地化为英语和土耳其语(您可以轻松添加其他语言或删除其中一种)。
我们首先更改模板的路由。ASP.NET Boilerplate 模板使用AngularUI-Router
,这是 AngularJs 的事实标准路由器。它提供了基于状态的路由模型。我们将有两个视图:任务列表和新任务。因此,我们将更改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是用于配置和启动我们的 SPA 的主 JavaScript 文件。请注意,我们正在使用cshtml 文件作为视图!通常,AngularJs 使用 HTML 文件作为视图。ASP.NET Boilerplate 使使用 cshtml 文件成为可能。因此,我们将拥有razor引擎来生成 HTML 的强大功能。
ASP.NET Boilerplate 提供了一个基础设施来创建和显示应用程序中的菜单。它允许在 C# 中定义菜单,并在 C# 和 JavaScript 中使用相同的菜单。有关创建菜单,请参阅SimpleTaskSystemNavigationProvider
类;有关以 Angular 方式显示菜单,请参阅header.js/header.cshtml。
首先,我正在为任务列表视图创建一个 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( //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
'。这是我的约定(用于可扩展的代码库),但您可以简单地将其命名为 'ListController
'。AngularJs 也使用依赖注入。我们在这里注入 '$scope
' 和 'abp.services.tasksystem.task
'。前者是 Angular 的作用域变量,后者是我们之前在“构建 Web API 服务”部分构建的ITaskAppService
的自动创建的 JavaScript 服务代理。
ASP.NET Boilerplate 提供了一个基础设施,可以在服务器和客户端使用相同的 本地化文本(有关详细信息,请参阅其文档)。
vm.taks
是将在视图中显示的待办事项列表。vm.refreshTasks
方法通过使用taskService
获取待办事项来填充此数组。当selectedTaskState
更改时(通过$scope.$watch
观察),它会被调用。
正如您所见,调用应用程序服务方法非常简单明了!这是 ASP.NET Boilerplate 的一项功能。它生成 Web API 层和与该 Web API 层通信的 JavaScript 代理层。因此,我们调用应用程序服务方法就像调用一个简单的 JavaScript 方法一样。它与 AngularJs 完全集成(使用 Angular 的$http
服务)。
让我们来看看任务列表的视图端
<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
属性(在第一行)将控制器绑定到视图。@L("TaskList")
获取“任务列表”的本地化文本(在渲染 HTML 时在服务器端工作)。这在它是一个cshtml文件的情况下是可行的。
ng-model
将下拉列表框和 JavaScript 变量绑定在一起。当变量更改时,下拉列表框会更新。当下拉列表框更改时,变量会更新。这是 AngularJs 的双向绑定。
ng-repeat
是 Angular 的另一个“指令”,用于为数组中的每个值渲染相同的 HTML。当数组更改时(例如添加一项),它会自动反映到视图。这是 AngularJs 的另一个强大功能。
注意:添加 JavaScript 文件(例如,“任务列表”控制器)时,应将其添加到您的页面中。这可以通过将其添加到模板中的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 项目的单元测试和集成测试。请参阅文章: 使用 xUnit、Entity Framework、Effort 和 ASP.NET Boilerplate 进行 C# 单元测试。
摘要
在本文中,我演示了如何使用 ASP.NET Boilerplate 开发一个 NLayered ASP.NET MVC Web 应用程序,该应用程序具有 SPA 和响应式用户界面,因为它简化了使用最佳实践开发此类应用程序的过程并节省了我们的时间。有关更多信息,请使用以下链接
- 官方网站和文档: aspnetboilerplate.com
- 官方论坛: forum.aspnetboilerplate.com
- Github 仓库: github.com/aspnetboilerplate
- 在 Twitter 上关注: @aspboilerplate
- 企业启动模板: aspnetzero.com
文章历史
- 2018-02-18:将示例项目和文章升级到 ABP v3.4
- 2016-10-26:将示例项目升级到 ABP v1.0
- 2016-07-19:更新了 ABP v0.10 的文章和示例项目
- 2015-06-08:将文章和示例项目更新到 ABP v0.6.3.1
- 2015-02-20:添加了单元测试文章链接并更新了示例项目
- 2015-01-05:将示例项目更新到 ABP v0.5
- 2014-11-03:将文章和示例项目更新到 ABP v0.4.1
- 2014-09-08:将文章和示例项目更新到 ABP v0.3.2
- 2014-08-17:将示例项目更新到 ABP v0.3.1.2
- 2014-07-22:将示例项目更新到 ABP v0.3.0.1
- 2014-07-11:添加了“启用迁移”命令的屏幕截图
- 2014-07-08:更新了示例项目和文章
- 2014-07-01:文章首次发布
参考文献
[1] ASP.NET Boilerplate 官方网站: http://www.aspnetboilerplate.com