演示程序截图如上所示.
内容摘要
- 介绍
- 使用 boilerplate 模板创建程序
- 创建实体对象
- 创建DbContext
- 创建数据库迁移
- 定义库
- 实现库
- 构建应用程序服务
- 构建 Web API 服务
- 开发SPA
- 本地化
- 单元测试
- 摘要
- 文章历史
- 参考
介绍
在这篇文章, 我将基于以下框架演示如何开发单页面的(SPA) 应用程序 :
- 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目录,我们可以使用模板创建应用。
这里我选择 SPA(单页面程序)使用AngularJs , EntityFramework框架. 然后输入项目名称SimpleTaskSystem.来创建和下载应用模版.下载的模版解决方案包含5个项目. Core 项目是领域 (业务) 层, Application 项目是应用层, WebApi 项目实现了 Web Api 控制器, Web 项目是展示层,最后EntityFramework 项目实现了EntityFramework框架.
Note: 如果您下载本文演示实例, 你会看到解决方案有7个项目. 我把NHibernate和Durandal都放到本演示中.如果你对NHibernate,Durandal不感兴趣的话,可以忽略它们.
创建实体对象
我将创建一个简单的应用程序来演示任务和分配任务的人. 所以我需要创建Task实体对象和Person实体对象.
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:
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是默认的主键类型, 这里我不需要特殊指定.
我在这个 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. 我将创建初始迁移.打开包管理控台程序并输入以下命令:
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
这个命令帮我们创建好了数据库并填充了初始数据:
当我们修改实体类时, 我们可以通过 Add-Migration 命令很容易的创建迁移类。需要更新数据库的时候则可以通过 Update-Database 命令. 关于更多的数据库迁移, 可以查看 entity framework的官方文档.
定义库
在领域驱动设计中, repositories 用于实现特定的代码. ASP.NET Boilerplate 使用 IRepository 接口给每个实体自动的创建 repository . IRepository 定义了常用的方法如 select, insert, update, delete 等,更多如下:
我们还可以根据我们的需要来扩展 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(