譯者注:必須要下源碼,作者文章中的代碼比較飄逸,會跳過部分聲明,你可能會發現很多類沒有定義,要去源碼找來看。當然,我會在本文中盡量補充完整,使得這個入門級別的文章更加“入門”
項目截屏
介紹
ASP.NET Boilerplate 是一個開源框架它整合了所有上面提到的框架或類庫來使你的開發更容易。它提供了一個很好的開發應用的基礎。它原生支持依賴注入,領域驅動設計和分層體系結構。這個簡單的應用同樣實現了 數據驗證,異常處理,本地化和響應式設計。
在這篇文章中,我會來展示如何使用下面的工具來開發一款 單頁應用 Single-Page Web Application (SPA)
- ASP.NET MVC 和 ASP.NET Web API 作為網站框架
- Angularjs 作為 SPA 框架
- EntityFramework 作為對象關系映射 ORM (Object-Relational Mapping) 框架
- Castle Windsor 作為依賴注入框架
- Twitter Bootstrap 作為 HTML/CSS 框架
- Log4Net 做登錄(logging), AutoMapper 做對象映射 (object-to-object mapping).
- 最后 ASP.NET Boilerplate 作為模板和項目框架
通過ABP模板創建應用
ABP整合并配置好了企業級網站開發用最好的工具,為我們節省了大量時間。
讓我們打開 aspnetboilerplate.com/Templates 來創建我們的應用
譯者注,現在ABP模板下面多出一個 module zero 是用來創建權限以及用戶的,勾和不勾與本文沒關系。另外源碼中的項目結構也和現在下載的模板有所出入,還多出NHibernate or Durandal。無視就好
點擊按鈕后輸入驗證碼 并開始下載。
不要急著關掉,你需要按照網頁提示去創建你的本地sqlserver數據庫
就是剛才你給項目起的名字,當然你也可以翻過來修改連接字符串來指定數據庫
用之前配置好的 VS2015打開下載的項目,可能會出現如圖情況,點擊確認。具體配置方法
加載完成后,右鍵點擊項目,然后還原nuget包
把web項目設置為啟動項目,然后點擊F5運行就能看到網站歡迎頁面
創建實體
我會創建一個簡單應用來給一些人發一些任務,所以我需要 Task 任務 和 Person 人 2個實體。(注:實體作為業務的一部分,創建在core領域層并單獨擁有文件夾,后續還要存放倉庫接口和關聯文件,如圖)
Task 任務,簡單定義了一個描述,創建時間和任務的狀態。同時也含有一個 被指派人的引用(注:有人發現了,沒有定義主鍵,因為他們都繼承自Entity 會默認加一個 int 類型 的字段 叫 Id.這里 Task任務通過泛型修改了id類型為 long)
(注:文件夾默認要用復數來取名,單復數同型的單詞比如person,文件夾不要取一樣的名字,會導致命名空間和類名重名,無法使用)
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 人,簡單定義一下名字
public class Person : Entity
{
public virtual string Name { get; set; }
}
創建 DbContext
眾所周知,EF 需要DbContext來工作.我們需要先定義他,ABP模板已經為我們創建了一個DbContext模板,我們只需要在里面添加 IDbSet 就行了
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)
{
}
}
他使用 Default 連接字符串 , 配置在webconfig中
<add name="Default" connectionString="Server=localhost; Database=SimpleTaskSystem; Trusted_Connection=True;" providerName="System.Data.SqlClient" />
創建migration
我們使用EF的code first migrations來升級數據庫.ABP模板已經默認打開了migrations,并且有下面這個配置類
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個人 來初始化數據,然后打開 包控制臺Package Manager Console,選擇 EF項目,輸入下面的命令
Add-Migration "InitialCreate"
然后
Update-Database
注:可以用tab"自動完成",比如輸入update 然后 tab
當我們修改我們的數據實體時,可以很輕松的通過migration來完成數據庫升級,想要了解更多?看entity framework的文檔
定義倉儲接口
在領域驅動設計 (DDD)中,倉儲被用來實現具體的數據庫訪問代碼 .ABP 為每一個數據實體創建了一個自動化的通用倉儲IRepository接口,IRepository 已經定義了一些常用怎刪改查方法,如圖
注:當你聲明的倉儲繼承于IRepository,就會自動擁有這些方法
如果需要,我們可以擴展這些倉儲,我會繼承他來創建一個Task倉儲,由于我想要分離接口和實現,所以我先在這里創建接口(注:添加在核心領域層 Tasks文件夾中)
public interface ITaskRepository : IRepository<Task, long>
{
List<Task> GetAllWithPeople(int? assignedPersonId, TaskState? state);
}
它繼承于通用的ABP IRepository.所以他可以使用所有IRepository中定義的方法,并且我們可以添加我們自己的方法 GetAllWithPeople(...).
不需要為person創建倉儲,因為默認方法就已經夠用了.ABP提供了一種注入通用倉儲的方法而不需要單獨去創建倉儲類.我們會在后面的章節(創建應用服務)中看到
我會把倉儲接口定義在核心層,因為他是領域/業務的一部分
實現倉儲
我們需要實現上面定義過的 ITaskRepository 接口.我會在EF層實現倉儲,這樣 領域層就完全和數據分離了
當我們創建模板的時候,ABP會默認創建一個倉儲類 SimpleTaskSystemRepositoryBase.擁有這樣一個基礎類是非常好的,我們可以在日后為我們的倉儲添加自己想要的通用方法.下面是我實現 TaskRepository 的代碼
public class TaskRepository : SimpleTaskSystemRepositoryBase<Task, long>, ITaskRepository
{
public TaskRepository(IDbContextProvider<ABPSimpleTaskTestDbContext> dbContextProvider) : base(dbContextProvider)
{
}
public List<Task> GetAllWithPeople(int? assignedPersonId, TaskState? state)
{
//在倉儲方法中,我們不需要去定義數據庫連接 DbContext 和數據事務
var query = GetAll(); //GetAll() 返回 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) //一起查詢被分配任務的人
.ToList();
}
}
(注:刪掉 using System.Threading.Tasks;)
TaskRepository 繼承于 SimpleTaskSystemRepositoryBase 并且實現了我們剛才定義的 ITaskRepository 接口
GetAllWithPeople 是我們定義的 可以添加條件并拿到關聯實體 的查詢任務的方法.另外, 我們可以自由的在倉儲中使用 Context (EF的 DBContext) 和 數據庫 . ABP會為我們管理數據庫連接,事務,創建和回收DbContext. (從 文檔 了解更多)
創建應用服務
應用服務 通過提供 外觀樣式方法 從 領域層 中分離出 演示層 .我會定義應用服務在應用程序集(Application)中.首先,我會定義一個task的應用服務接口
注:原文在這里跳過了很重要的內容,Dto的相關介紹,譯者這里靠自己的理解補上
和其他層一樣,為tasks創建一個文件夾.并且創建好Dtos文件夾 Dto簡介
譯者目前無法完全表述清楚這些文件的意思,所以從源碼中復制一下吧,畢竟我們這次的目的是ABP入門
創建ITaskAppService 接口
public interface ITaskAppService : IApplicationService
{
GetTasksOutput GetTasks(GetTasksInput input);
void UpdateTask(UpdateTaskInput input);
void CreateTask(CreateTaskInput input);
}
ITaskAppService 繼承于 IApplicationService. 這樣, ASP.NET Boilerplate 可以自動為這個類提供一些特征 (比如依賴注入和數據驗證). 現在,讓我們來實現 ITaskAppService:
/// <summary>
/// 實現 <see cref="ITaskAppService"/> 來執行task相關的應用服務功能
///
/// 繼承 <see cref="ApplicationService"/>.
/// <see cref="ApplicationService"/> 包括一些常用的應用服務 (比如登陸和本地化).
/// </summary>
public class TaskAppService : ApplicationService, ITaskAppService
{
//這些在構造函數中的成員使用了構造函數注入
private readonly ITaskRepository _taskRepository;
private readonly IRepository<Person> _personRepository;
/// <summary>
/// 在構造函數中,我們可以獲得所需要的類和接口
/// 他們被依賴注入系統自動送到這里
/// </summary>
public TaskAppService(ITaskRepository taskRepository, IRepository<Person> personRepository)
{
_taskRepository = taskRepository;
_personRepository = personRepository;
}
public GetTasksOutput GetTasks(GetTasksInput input)
{
//調用特殊的倉儲方法 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),它已經在基礎類(ApplicationService)里定義過了
Logger.Info("Updating a task for input: " + input);
//通過id獲取實體可以用倉儲中標準的Get方法
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),它已經在基礎類(ApplicationService)里定義過了
Logger.Info("Creating a task for input: " + input);
//用收到的input里的值新建一個task
var task = new Task { Description = input.Description };
if (input.AssignedPersonId.HasValue)
{
task.AssignedPerson = _personRepository.Load(input.AssignedPersonId.Value);
}
//使用倉儲中標準餓Insert方法來保存task
_taskRepository.Insert(task);
}
}
注:這里用到了automapper,而這個東西是需要配置的,到源文件里去找到下面2個文件,進行配置
static class DtoMappings
{
public static void Map(IMapperConfigurationExpression mapper)
{
mapper.CreateMap<Task, TaskDto>();
}
}
[DependsOn(typeof(ABPSimpleTaskTestCoreModule), typeof(AbpAutoMapperModule))]
public class ABPSimpleTaskTestApplicationModule : AbpModule
{
public override void Initialize()
{
IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly());
//為了使用automapper我們必須在這里先聲明
Configuration.Modules.AbpAutoMapper().Configurators.Add(mapper =>
{
DtoMappings.Map(mapper);
});
}
}
TaskAppService 使用倉儲來進行數據庫操作. 它通過 構造函數注入(constructor injection) 的方式,在他的構造函數中提供了引用. ASP.NET Boilerplate 原生實現了依賴注入, 所以我們可以自由使用構造注入或者屬性注入 (了解更多 ASP.NET Boilerplate 中的依賴注入 文檔).
注意, 我們通過注入IRepository<Person>來使用 PersonRepository. ASP.NET Boilerplate 就會自動為我們創建倉儲. 如果 IRepository 中的默認方法夠我們使用, 那我們就沒有必要去創建倉儲類.
應用服務方法需要用到數據傳輸對象 -- Data Transfer Objects (DTOs). 這是非常好的,而且我很建議使用這樣的模式. 但是你也不一定要照做,只要你能解決這些暴露實體到演示層的問題
在 GetTasks 方法中, 我使用了之前實現過的 GetAllWithPeople 方法 。 它返回了一個 List<Task> ,但是我希望返回 List<TaskDto> 到演示層. AutoMapper 幫助我們自動轉換 Task 對象到 TaskDto 對象. GetTasksInput 和 GetTasksOutput 是特別為 GetTasks 方法而定義的數據傳輸對象 (DTOs) .
在 UpdateTask 方法中, 我從數據庫獲得 Task (使用IRepository's 的 Get 方法) 并且修改了 Task的值. 注意,你沒有調用過倉儲中的 Update 方法 . ASP.NET Boilerplate 實現了 UnitOfWork 模式. 所以, 所有在一個應用服務里的修改就是一個 unit of work (atomic原子的) 并且在方法的最后自動提交到數據庫.
在 CreateTask 方法,我簡單地創建了一個 Task 并且使用 IRepository自帶的 Insert 方法添加到數據庫 .
ASP.NET Boilerplate 中的 ApplicationService 類 有一些功能來幫助我們更容易地開發應用.比如, 它定義的 Logger 來做日志功能. 所以, 我們可以在這里讓 TaskAppService 繼承 ApplicationService 并且使用它的 Logger 日志功能. 我們任然可以選擇性地去繼承這個類,只要你實現 IApplicationService 接口(注意 ITaskAppService接口 繼承自 IApplicationService接口).
驗證
ABP自動在 application service 驗證 inputs .CreateTask 方法獲取了 CreateTaskInput 作為參數
public class CreateTaskInput
{
public int? AssignedPersonId { get; set; }
[Required]
public string Description { get; set; }
}
這里 Description 被標記成 [Required] 如果你想使用一些常規驗證,你可以使用 Data Annotation attributes中所有的驗證.你可以實現接口 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 方法 可以寫你自己的驗證方法代碼
異常處理
有人注意到了,我們還沒有處理過任何異常,ABP自動為我們處理異常,記錄日志并給客戶端返回一個適當的錯誤.在客戶端可以看到錯誤信息.事實上,我這個是為了ASP.NET MVC and Web API Controller actions 準備的,由于我們會用WEBAPI來暴露 TaskAppService 所以我們不需要處理異常,可以看 exception handling 文檔 獲得更多信息
構建web api services
我想要暴露我的服務到遠程客戶端,這樣,我的 AngularJs 就可以使用ajax輕松調用他們.
ABP提供一個自動化方式來 把 服務方法 轉換成webapi .我只要使用 DynamicApiControllerBuilder 就像下面:
DynamicApiControllerBuilder
.ForAll<IApplicationService>(Assembly.GetAssembly(typeof (SimpleTaskSystemApplicationModule)), "tasksystem")
.Build();
在這個例子中,ABP 在 Application 層的應用程序中 找到了所有繼承 IApplicationService 的 接口 ,并且為每一個 application service class 創建了一個 web api controller.There are alternative syntaxes for fine control. 我們接下來會看到如何使用ajax來調用這些服務
開發 SPA
我會實現一個SPA 來作為我項目中的用戶界面 ,Angularjs 是使用最廣的 SPA框架
ABP提供了一個模板 可以很輕松地使用 AngularJs . 這個模板有2個頁面 Home 和 About .Bootstrap 作為 HTML/CSS 框架 同事本地化加入了 英語和 土耳其語 在 ABP的本地化系統中 (你可以很輕松地添加一個新語言或者移除它)
我們首先來修改路由,ABP使用 AngularUI-Router the de-facto standard router of AngularJs ,It provides state based routing modal,我們有2個 視圖 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 是主要的 js文件 用來啟動和設置我們的SPA,需要注意的是 我們正在使用cshtml 文件來作為視圖,通常情況下,html文件會被作為AngularJs的視圖 .ABP經過處理 使得cshtml也能使用AngularJs.這樣 我們就能使用razor來構成我們的html了
ABP基礎插件 來創建和顯示菜單,他允許我們在C#中定義菜單但可以同時在C#和JS中使用,看 **SimpleTaskSystemNavigationProvider ** 類 它創建了菜單 ,看 header.js/header.cshtml 通過angular 來顯示菜單
接下來 首先我要 為了 task list 視圖 創建一個 Angular controller :
(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(); //當selectedTaskState改變時調用
});
vm.refreshTasks = function() {
abp.ui.setBusy( //設置頁面為忙碌 直到 getTasks 方法完成
null,
taskService.getTasks({ //直接從 javascript 調用 application service 方法
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的 局部變量,后者是一個自動創建的 ITaskAppService 的js服務代理(我們之前在 構建web api services 中設置過)
ABP提供了一些基礎插件讓 服務器和客戶端 來使用 統一的 localization 本地化文本
vm.tasks 是一個 tasks的集合 會被現實到視圖中 vm.refreshTasks 方法 會調用 taskService 來填充這個集合,他會在 selectedTaskState改變時被調用 (用 $scope.$watch 來監聽 )
正如你所見,調用一個服務器方法 是非常容易且直接了當的 . 這就是ABP的特色,他生成 web api 層 和 與之通行的 js 代理層 ,這樣 我們就能夠像調用js方法一樣調用服務端方法了.而且它完全集成在 AngularJs 中 (使用了 Angular's $http service)
讓我們接著來看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>
第一行的 ng-controller 屬性 綁定了 視圖的控制器 @L("TaskList") 為 "task list" 提供了本地化的文字翻譯 (在服務端生成html時),這全歸功于它是一個cshtml文件
ng-model綁定了 下拉框的變量,當變量改變的時候 下拉框自動改變 同樣,當下拉框改變的時候 變量也會被保存,這個是 AngularJs 實現的雙向綁定
ng-repeat
是另外一個 Angular 的指令 用來循環集合 生成html ,當 集合改變的時候 (比如添加了一個內容) 它會自動 反射到視圖上,這個是AngularJs另外一個強大的功能
注意 當你添加一個 js文件 ,你需要添加它到你的頁面中
本地化
ABP提供了一個靈活且強大的 本地化系統 你可以使用xml文件 或者 Resource 文件 來作為 數據源 你可以定制 本地化數據源 看 documentation 獲得更多,我使用XML文件(在web項目中 的 Localization 文件夾)