65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2018 年 11 月 12 日

Apache

5分钟阅读

viewsIcon

10452

[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 方法很简单。系统会将此请求重定向到必要的地址。
  • 在应用程序的生命周期中有许多阶段,我们可以在这些阶段注入自定义任务。当所有必要的配置完成后,将调用此任务。

CreateStaffRequestCreateStaffResponse的内容如下:

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接口注册命令处理程序/事件处理程序。
  • 定义StaffCommandHandlerIStaffCommandHandler来处理CreateStaffRequest
  • 定义StaffEventHandlerIStaffEventHandler来处理“员工领域”的相应事件。
  • 使用IStaffQuery获取Staffs列表。

我们将在后续的“修订管理员工”文章中继续阐明许多问题。

有关本部分的参考源代码,请查看 https://github.com/tranthanhtu0vn/TinyERP(分支:feature/manage_staff)。

 

本系列其他文章

感谢您的阅读。

© . All rights reserved.