[TinyERP: 企业应用程序的 SPA] 管理员工 - 第 2 部分





5.00/5 (2投票s)
[TinyERP: 企业应用程序的 SPA] 管理员工 - 第 2 部分
概述
在本文中,我们将实现“管理员工”功能的API端,并使用CQRS模式创建默认员工。
对于API端
如“[TinyERP:企业应用SPA]概述”中所述,我们已经为API创建了项目。让我们在Visual Studio中打开它,并添加一个名为“TinyERP.HRM
”的新“类库”项目。
请记住删除新项目中不必要的cs文件,并将“TinyERP.HRM
”添加到“TinyERP.Api
”作为引用。
将新的 StaffHandler.cs 添加到 TinyERP.HRM\Api 中。
namespace TinyERP.HRM.Api
{
using Common.DI;
using Query;
using Search.Share;
using Share.Staff;
using System.Web.Http;
using TinyERP.Common.MVC;
using TinyERP.Common.MVC.Attributes;
[RoutePrefix("api/hrm/staffs")]
public class StaffHandler: BaseApiController
{
[Route("")]
[HttpGet()]
[ResponseWrapper()]
public ISearchResult<StaffListItem> GetStaffs() {
IStaffQuery query = IoC.Container.Resolve<IStaffQuery>();
return query.Search<StaffListItem>();
}
}
}
这是WebAPI
中的普通ApiController
,有几点值得注意:
RoutePrefix
是“api/hrm/staffs”。Staff
是系统中的资源,因此所有与Staff
相关的请求都将调用此uri
。有关更多信息,请参阅“RESTful Web 服务”。StaffHandler
继承自BaseApiController
,后者在TinyERP.Common
中定义。我们需要从nuget安装此包。更多信息请参阅 https://nuget.net.cn/packages/TinyERP.Common- 我们对大多数API方法都使用了
ResponseWrapper
属性。 - 当我们想根据某些条件搜索数据时,使用
ISearchResult
。例如,使用名字、电子邮件搜索员工。此接口在TinyERP.Search.Share
中定义。请从nuget添加此包。参见TinyERP.Search.Share - 由于此功能我们使用了CQRS模式,所以
IStaffQuery
仅用于从只读数据库获取数据。 - IoC也定义在
TinyERP.Common
中。无需初始化此容器。
让我们继续添加 IStaffQuery.cs。
namespace TinyERP.HRM.Query
{
using TinyERP.Common.Data;
using TinyERP.Search.Share;
internal interface IStaffQuery : IBaseQueryRepository<TinyERP.HRM.Query.Entities.StaffSummary>
{
ISearchResult<TResult> Search<TResult>();
}
}
这很简单,只需继承自IBaseQueryRepository
。我们将从MongoDB
中的StaffSummary
集合获取数据,这就是为什么我们需要在泛型声明中指定这个类。
以及IStaffQuery
的实现
namespace TinyERP.HRM.Query
{
using Common.Data;
using TinyERP.HRM.Query.Entities;
using Search.Share;
using System.Linq;
using System.Collections.Generic;
using Common.Extensions;
internal class StaffQuery : BaseQueryRepository<StaffSummary>, IStaffQuery
{
public StaffQuery() : base() { }
public StaffQuery(IUnitOfWork uow) : base(uow.Context) { }
public ISearchResult<TResult> Search<TResult>()
{
IList<TResult> items = this.DbSet.AsQueryable().ToList().Cast<StaffSummary, TResult>();
ISearchResult<TResult> result = new SearchResult<TResult>(items, items.Count);
return result;
}
}
}
StaffQuery
也继承自BaseQueryRepository
。- 有两个必需的构造函数,第一个用于读取,第二个用于在CQRS模式的读取端更新数据。
BaseQueryRepository
中有一个可用的DbSet
作为属性,我们可以用它来读取、更新或删除相应的数据。在这种情况下,我们只能使用DbSet
来处理StaffSummary
。只需将其转换为IQueryable
并使用LINQ获取数据。- Cast:这是将
A
类转换为B
类的扩展。在此示例中,它将集合从StaffSummary
类型转换为StaffListItem
类型。
我们还需要使用 IBootstrapper<ITaskArgument>
接口将 IStaffQuery
映射到 StaffQuery
。
namespace TinyERP.HRM.Query
{
using TinyERP.Common.DI;
using TinyERP.Common.Tasks;
public class Bootstrap:BaseTask<ITaskArgument>, IBootstrapper<ITaskArgument>
{
public Bootstrap():base(Common.ApplicationType.All){}
public override void Execute(ITaskArgument context)
{
if (!this.IsValid(context.Type)) { return; }
IBaseContainer container = context.Context as IBaseContainer;
container.RegisterTransient<IStaffQuery, StaffQuery>();
}
}
}
我建议我们应该对大多数接口使用瞬态(transient)。这将减少运行时的内存量。
让我们定义StaffSummary
类,这是mongodb服务器中的一个集合。
namespace TinyERP.HRM.Query.Entities
{
using Common.MVC.Attributes;
using Context;
using System;
using TinyERP.Common;
[DbContext(Use = typeof(IHRMQueryContext))]
internal class StaffSummary: AggregateSummaryEntity
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Department { get; set; }
public StaffSummary(Guid aggregateId):base(aggregateId){}
}
}
有一些有趣的要点
DbContext
属性:描述我们想要使用的数据库上下文。这对于在一个数据库中有很多实体(表)的情况很有用,这样我们就可以将其分解成多个更小的数据库。我们将在“扩展您的存储库”一文中再次提及。StaffSummary
被视为“员工域”的聚合根。因此它需要继承自AggregateSummaryEntity
(来自 TinyERP.Common 包)。- 聚合根必须有一个带
GUID
值的构造函数。这是写数据库上相应对象的ID。
以及 IHRMQueryContext.cs
namespace TinyERP.HRM.Context
{
using TinyERP.Common.Data;
public interface IHRMQueryContext:IDbContext
{
}
}
在此接口中,我们只需继承自IDbContext
。
好的,目前,我们可以从读取数据库获取员工列表并返回给客户端。
最后,我们需要在 TinyERP.Api/config/configuration.debug.config 中配置 IHRMQueryContext
的连接字符串。在 aggregates 部分,添加:
<add name="TinyERP.HRM.Context.IHRMQueryContext"
repoType="MongoDb" connectionStringName="DefaultMongoDb"></add>
并将其添加到数据库部分
<add
name="DefaultMongoDb"
database="TinyERP"
server="localhost"
port="27017"
userName=""
password=""
ssl="false"
dbType="MongoDb"
default="true"
></add>
通过上述配置,我们告诉系统,IHRMQueryContext
将连接到MongoDB,并使用如下所述的DefaultMongoDb
连接字符串。
我们运行TinyERP.Api
并调用GetStaffs
方法,结果如下所示:
让我稍作解释
status
:这决定了请求是成功还是失败。我们通常使用200(OK)、400(Bad Request)、500(InternalServerError)。Errors
:这将包含验证错误的列表,例如:登录请求中的“无效用户名或密码”。Data
:如果状态为200,这就是服务器的响应。
目前,由于mongodb服务器中没有数据,我们在data
属性中接收到空值。
我想,现在我们应该创建一些员工作为初始化数据。
添加新员工
现在,我们将在“TinyERP.HRM\Share\Tasks”文件夹中创建CreateDefaultStaff
。
namespace TinyERP.HRM.Share.Task
{
using Command.Staff;
using Common.Command;
using TinyERP.Common.Tasks;
public class CreateDefaultStaff: BaseTask<ITaskArgument>,
TinyERP.Common.Tasks.IApplicationReadyTask<ITaskArgument>
{
public CreateDefaultStaff():base(Common.ApplicationType.All){}
public override void Execute(ITaskArgument context)
{
if (!this.IsValid(context.Type)) { return; }
CreateStaffRequest request =
new CreateStaffRequest("Tu", "Tran", "contact@tranthanhtu.vn");
ICommandHandlerStrategy commandHandler =
CommandHandlerStrategyFactory.Create<TinyERP.HRM.Aggregate.Staff>();
CreateStaffResponse response =
commandHandler.Execute<CreateStaffRequest, CreateStaffResponse>(request);
this.Logger.Info("New staff (id: {0}) was created", response.Id);
}
}
}
- 创建
CreateStaffRequest
并调用Execute
方法很简单。系统会将此请求重定向到必要的地址。 - 在应用程序的生命周期中有许多阶段,我们可以在这些阶段注入自定义任务。当所有必要的配置完成后,将调用此任务。
CreateStaffRequest
和CreateStaffResponse
的内容如下:
namespace TinyERP.HRM.Command.Staff
{
using TinyERP.Common.Command;
public class CreateStaffRequest: IBaseCommand
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public CreateStaffRequest(string firstName, string lastName, string email)
{
this.FirstName = firstName;
this.LastName = lastName;
this.Email = email;
}
}
}
namespace TinyERP.HRM.Command.Staff
{
using System;
class CreateStaffResponse
{
public Guid Id { get; set; }
}
}
我们可以看到它很简单。创建一个包含三个字段的请求:名字、姓氏和电子邮件,并收到新创建员工的ID。
我们需要为这个请求注册处理程序,创建命令文件夹并添加这个类。
using TinyERP.Common.Command;
using TinyERP.Common.DI;
using TinyERP.Common.Tasks;
using TinyERP.HRM.Command.Staff;
namespace TinyERP.HRM.Command
{
public class Bootstrap: BaseTask<ITaskArgument>, IBootstrapper<ITaskArgument>
{
public Bootstrap():base(Common.ApplicationType.All){}
public override void Execute(ITaskArgument arg)
{
if (!this.IsValid(arg.Type)) { return; }
IBaseContainer container = arg.Context as IBaseContainer;
container.RegisterTransient<IBaseCommandHandler
<CreateStaffRequest, CreateStaffResponse>, StaffCommandHandler>();
}
}
}
这意味着CreateStaffRequest
请求将被重定向到StaffCommandHandler
类。
namespace TinyERP.HRM.Command
{
using System;
using TinyERP.Common.Command;
using Staff;
using Common.Helpers;
using Common.Validation;
using Common.Data;
using Repository;
using Common.DI;
internal class StaffCommandHandler : BaseCommandHandler, IStaffCommandHandler
{
public CreateStaffResponse Handle(CreateStaffRequest command)
{
this.Validate(command);
using (IUnitOfWork uow = this.CreateUnitOfWork<TinyERP.HRM.Aggregate.Staff>()) {
TinyERP.HRM.Aggregate.Staff staff = new Aggregate.Staff();
staff.UpdateBasicInfo(command);
IStaffRepository repository = IoC.Container.Resolve<IStaffRepository>(uow);
repository.Add(staff);
uow.Commit();
staff.PublishEvents();
return ObjectHelper.Cast<CreateStaffResponse>(staff);
}
}
private void Validate(CreateStaffRequest command)
{
IValidationException validator = ValidationHelper.Validate(command);
// and other business validations here, such as: unit first + last name, unit email, ....
validator.ThrowIfError();
}
}
}
我认为代码很直观,只需定义由IStaffCommandHandler
声明的Handle
方法即可。
namespace TinyERP.HRM.Command
{
using TinyERP.Common.Command;
using TinyERP.HRM.Command.Staff;
internal interface IStaffCommandHandler:
IBaseCommandHandler<CreateStaffRequest, CreateStaffResponse>
{
}
}
我们应该调用aggregate
对象的所有适当方法来执行必要的动作(例如:本例中的更新名字)。
aggregate
的每次更改都会引发新的事件。然后CQRS模式的读取端将订阅这些事件并相应地更新数据。
因此,对于 Staff.cs
namespace TinyERP.HRM.Aggregate
{
using TinyERP.Common.Aggregate;
using Command.Staff;
using Event;
using Common.MVC.Attributes;
using Context;
[DbContext(Use = typeof(IHRMContext))]
internal class Staff: BaseAggregateRoot
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public Staff()
{
this.AddEvent(new OnStaffCreated(this.Id));
}
internal void UpdateBasicInfo(CreateStaffRequest command)
{
this.FirstName = command.FirstName;
this.LastName = command.LastName;
this.Email = command.Email;
this.AddEvent(new OnStaffBasicInforChanged
(this.Id, this.FirstName, this.LastName, this.Email));
}
}
}
以及 StaffEventHandler.cs
namespace TinyERP.HRM.Event
{
using System;
using Common.Data;
using Common.DI;
using Query;
using Query.Entities;
using TinyERP.Common.Event;
internal class StaffEventHandler : BaseEventHandler, IStaffEventHandler
{
public void Execute(OnStaffBasicInforChanged ev)
{
using (IUnitOfWork uow = this.CreateUnitOfWork<StaffSummary>())
{
IStaffQuery query = IoC.Container.Resolve<IStaffQuery>(uow);
StaffSummary staff = query.GetByAggregateId(ev.StaffId.ToString());
staff.FirstName = ev.FirstName;
staff.LastName = ev.LastName;
staff.Email = ev.Email;
query.Update(staff);
uow.Commit();
}
}
public void Execute(OnStaffCreated ev)
{
using (IUnitOfWork uow = this.CreateUnitOfWork<StaffSummary>())
{
StaffSummary summary = new StaffSummary(ev.StaffId);
IStaffQuery query = IoC.Container.Resolve<IStaffQuery>(uow);
query.Add(summary);
uow.Commit();
}
}
}
}
在StaffEventHandler
中,我们接收更改(由StaffCommandHandler
引发)并更新到读取数据库(本例中是mongodb)。
我们还需要为IHRMContext
进行配置,类似于IHRMQueryContext
。
在聚合部分(在 config/configuration.debug.config 中),添加
<add name="TinyERP.HRM.Context.IHRMContext" repoType="MSSQL"
connectionStringName="DefaultMSSQL"></add>
并将其添加到数据库部分
<add
name="DefaultMSSQL"
database="TinyERP"
server=".\SqlExpress"
port="0"
userName="sa"
password="123456"
dbType="MSSQL"
></add>
再次尝试编译并运行应用程序,您可以看到MSSQL和MongoDB中都添加了新的记录。
好的,所以,再次调用“api/hrm/staffs”
当前HRM项目的结构是
让我们进入最后一步来完成这篇文章。
在客户端打开 staffService.ts 并将 uri
更新为 api
。
export class StaffService extends BaseService implements IStaffService{
public getStaffs():Promise{
let uri="https://:56622/api/hrm/staffs";
let iconnector: IConnector = window.ioc.resolve(IoCNames.IConnector);
return iconnector.get(uri);
}
}
再次编译并运行客户端。我们可以看到staffs
列表显示在UI上。
“管理员工”流程的概述如下:
到目前为止,我们可以做到
- 使用
IApplicationReadyTask
接口创建新员工。 - 创建并发送
CreateStaffRequest
。 - 使用
IBootstrapper
接口注册命令处理程序/事件处理程序。 - 定义
StaffCommandHandler
和IStaffCommandHandler
来处理CreateStaffRequest
。 - 定义
StaffEventHandler
和IStaffEventHandler
来处理“员工领域”的相应事件。 - 使用
IStaffQuery
获取Staffs
列表。
我们将在后续的“修订管理员工”文章中继续阐明许多问题。
有关本部分的参考源代码,请查看 https://github.com/tranthanhtu0vn/TinyERP(分支:feature/manage_staff)。
本系列其他文章
感谢您的阅读。