二、领域层
10,实体
11,值对象
12,仓储
13,领域服务
14,规格模式
15,工作单元
16,事件总线
17,数据过滤器
三、应用层
18,应用服务
19,数据传输对象
20,验证数据传输对象
21,授权
22,功能管理
23,审计日志
四、分布式服务层
24,ASP.NET Web API Controllers
25,动态Webapi层
26,OData整合
27,Swagger UI 整合
10,实体
实体具有Id并存储在数据库中, 实体通常映射到关系数据库的表。
1,审计接口
①当Entity被插入到实现该接口的数据库中时,ASP.NET Boilerplate会自动将CreationTime设置为当前时间。
public interface IHasCreationTime
{
DateTime CreationTime { get; set; }
}
②ASP.NET Boilerplate在保存新实体时自动将CreatorUserId设置为当前用户的id
public interface ICreationAudited : IHasCreationTime
{
long? CreatorUserId { get; set; }
}
③编辑时间,编辑人员
public interface IHasModificationTime
{
DateTime? LastModificationTime { get; set; }
}
public interface IModificationAudited : IHasModificationTime
{
long? LastModifierUserId { get; set; }
}
④如果要实现所有审计属性,可以直接实现IAudited接口:
public interface IAudited : ICreationAudited, IModificationAudited
{
}
⑤可以直接继承AuditedEntity审计类
注意:ASP.NET Boilerplate从ABP Session获取当前用户的Id。
⑥软删除
public interface ISoftDelete
{
bool IsDeleted { get; set; }
}
public interface IDeletionAudited : ISoftDelete
{
long? DeleterUserId { get; set; }
DateTime? DeletionTime { get; set; }
}
⑦所有审计接口
public interface IFullAudited : IAudited, IDeletionAudited
{
}
⑧直接使用所有审计类FullAuditedEntity
2,继承IExtendableObject接口存储json字段
public class Person : Entity, IExtendableObject
{
public string Name { get; set; }
public string ExtensionData { get; set; }
public Person(string name)
{
Name = name;
}
}
var person = new Person("John");
//存入数据库中的值:{"CustomData":{"Value1":42,"Value2":"forty-two"},"RandomValue":178}
person.SetData("RandomValue", RandomHelper.GetRandom(1, 1000));
person.SetData("CustomData", new MyCustomObject { Value1 = 42, Value2 = "forty-two" });
var randomValue = person.GetData<int>("RandomValue");
var customData = person.GetData<MyCustomObject>("CustomData");
十一、值对象
与实体相反,实体拥有身份标识(id),而值对象没有。例如地址(这是一个经典的Value Object)类,如果两个地址有相同的国家/地区,城市,街道号等等,它们被认为是相同的地址。
public class Address : ValueObject<Address>
{
public Guid CityId { get; private set; } //A reference to a City entity.
public string Street { get; private set; }
public int Number { get; private set; }
public Address(Guid cityId, string street, int number)
{
CityId = cityId;
Street = street;
Number = number;
}
}
值对象基类覆盖了相等运算符(和其他相关的运算符和方法)来比较两个值对象
var address1 = new Address(new Guid("21C67A65-ED5A-4512-AA29-66308FAAB5AF"), "Baris Manco Street", 42);
var address2 = new Address(new Guid("21C67A65-ED5A-4512-AA29-66308FAAB5AF"), "Baris Manco Street", 42);
Assert.Equal(address1, address2);
Assert.Equal(address1.GetHashCode(), address2.GetHashCode());
Assert.True(address1 == address2);
Assert.False(address1 != address2);
十二、仓储
是领域层与数据访问层的中介。每个实体(或聚合根)对应一个仓储
在领域层中定义仓储接口,在基础设施层实现
1,自定义仓储接口
public interface IPersonRepository : IRepository<Person>
{
}
public interface IPersonRepository : IRepository<Person, long>
{
}
2,基类仓储接口方法
①获取单个实体
TEntity Get(TPrimaryKey id);
Task<TEntity> GetAsync(TPrimaryKey id);
TEntity Single(Expression<Func<TEntity, bool>> predicate);
Task<TEntity> SingleAsync(Expression<Func<TEntity, bool>> predicate);
TEntity FirstOrDefault(TPrimaryKey id);
Task<TEntity> FirstOrDefaultAsync(TPrimaryKey id);
TEntity FirstOrDefault(Expression<Func<TEntity, bool>> predicate);
Task<TEntity> FirstOrDefaultAsync(Expression<Func<TEntity, bool>> predicate);
TEntity Load(TPrimaryKey id);//不会从数据库中检索实体,而是延迟加载。
(它在NHibernate中实现。 如果ORM提供程序未实现,Load方法与Get方法完全相同)
Get方法用于获取具有给定主键(Id)的实体。 如果数据库中没有给定Id的实体,它将抛出异常。 Single方法与Get类似,但需要一个表达式而不是Id。 所以,您可以编写一个lambda表达式来获取一个实体。 示例用法:
var person = _personRepository.Get(42);
var person = _personRepository.Single(p => p.Name == "John");
//请注意,如果没有给定条件的实体或有多个实体,Single方法将抛出异常。
②获取实体列表
List<TEntity> GetAllList();
Task<List<TEntity>> GetAllListAsync();
List<TEntity> GetAllList(Expression<Func<TEntity, bool>> predicate);
Task<List<TEntity>> GetAllListAsync(Expression<Func<TEntity, bool>> predicate);
IQueryable<TEntity> GetAll();
GetAllList用于从数据库检索所有实体。 过载可用于过滤实体。 例子:
var allPeople = _personRepository.GetAllList();
var somePeople = _personRepository.GetAllList(person => person.IsActive && person.Age > 42);
GetAll返回IQueryable <T>。
所以,你可以添加Linq方法。 例子:
//Example 1
var query = from person in _personRepository.GetAll()
where person.IsActive
orderby person.Name
select person;
var people = query.ToList();
//Example 2:
List<Person> personList2 = _personRepository.GetAll().Where(p => p.Name.Contains("H")).OrderBy(p => p.Name).Skip(40).Take(20).ToList();
③Insert
TEntity Insert(TEntity entity);
Task<TEntity> InsertAsync(TEntity entity);
TPrimaryKey InsertAndGetId(TEntity entity);//方法返回新插入实体的ID
Task<TPrimaryKey> InsertAndGetIdAsync(TEntity entity);
TEntity InsertOrUpdate(TEntity entity);//通过检查其Id值来插入或更新给定实体
Task<TEntity> InsertOrUpdateAsync(TEntity entity);
TPrimaryKey InsertOrUpdateAndGetId(TEntity entity);//在插入或更新后返回实体的ID
Task<TPrimaryKey> InsertOrUpdateAndGetIdAsync(TEntity entity);
④Update
TEntity Update(TEntity entity);
Task<TEntity> UpdateAsync(TEntity entity);
大多数情况下,您不需要显式调用Update方法,因为在工作单元完成后,工作单元会自动保存所有更改
⑤Delete
void Delete(TEntity entity);
Task DeleteAsync(TEntity entity);
void Delete(TPrimaryKey id);
Task DeleteAsync(TPrimaryKey id);
void Delete(Expression<Func<TEntity, bool>> predicate);
Task DeleteAsync(Expression<Func<TEntity, bool>> predicate);
⑥ASP.NET Boilerplate支持异步编程模型
。 所以,存储库方法有Async
版本。 这里,使用异步模型的示例应用程序服务方法:
public class PersonAppService : AbpWpfDemoAppServiceBase, IPersonAppService
{
private readonly IRepository<Person> _personRepository;
public PersonAppService(IRepository<Person> personRepository)
{
_personRepository = personRepository;
}
public async Task<GetPeopleOutput> GetAllPeople()
{
var people = await _personRepository.GetAllListAsync();
return new GetPeopleOutput
{
People = Mapper.Map<List<PersonDto>>(people)
};
}
}
自定义存储库方法不应包含业务逻辑或应用程序逻辑。 它应该只是执行与数据有关的或orm特定的任务。
十三、领域服务
领域服务(或DDD中的服务)用于执行领域操作和业务规则。Eric Evans描述了一个好的服务应该具备下面三个特征:
和领域概念相关的操作不是一个实体或者值对象的本质部分。
该接口是在领域模型的其他元素来定义的。
操作是无状态的。
1,例子(假如我们有一个任务系统,并且将任务分配给一个人时,我们有业务规则):
我们在这里有两个业务规则:
①任务应处于活动状态,以将其分配给新的人员。
②一个人最多可以有3个活动任务。
public interface ITaskManager : IDomainService
{
void AssignTaskToPerson(Task task, Person person);//将任务分配给人
}
public class TaskManager : DomainService, ITaskManager
{
public const int MaxActiveTaskCountForAPerson = 3;
private readonly ITaskRepository _taskRepository;
public TaskManager(ITaskRepository taskRepository)
{
_taskRepository = taskRepository;
}
public void AssignTaskToPerson(Task task, Person person)
{
if (task.AssignedPersonId == person.Id)
{
return;
}
if (task.State != TaskState.Active)
{
//认为这个一个应用程序错误
throw new ApplicationException("Can not assign a task to a person when task is not active!");
}
if (HasPersonMaximumAssignedTask(person))
{
//向用户展示错误
throw new UserFriendlyException(L("MaxPersonTaskLimitMessage", person.Name));
}
task.AssignedPersonId = person.Id;
}
private bool HasPersonMaximumAssignedTask(Person person)
{
var assignedTaskCount = _taskRepository.Count(t => t.State == TaskState.Active && t.AssignedPersonId == person.Id);
return assignedTaskCount >= MaxActiveTaskCountForAPerson;
}
}
public class TaskAppService : ApplicationService, ITaskAppService
{
private readonly IRepository<Task, long> _taskRepository;
private readonly IRepository<Person> _personRepository;
private readonly ITaskManager _taskManager;
public TaskAppService(IRepository<Task, long> taskRepository, IRepository<Person> personRepository, ITaskManager taskManager)
{
_taskRepository = taskRepository;
_personRepository = personRepository;
_taskManager = taskManager;
}
public void AssignTaskToPerson(AssignTaskToPersonInput input)
{
var task = _taskRepository.Get(input.TaskId);
var person = _personRepository.Get(input.PersonId);
_taskManager.AssignTaskToPerson(task, person);
}
}
2,强制使用领域服务
将任务实体设计成这样:
public class Task : Entity<long>
{
public virtual int? AssignedPersonId { get; protected set; }
//...other members and codes of Task entity
public void AssignToPerson(Person person, ITaskPolicy taskPolicy)
{
taskPolicy.CheckIfCanAssignTaskToPerson(this, person);
AssignedPersonId = person.Id;
}
}
我们将AssignedPersonId的setter更改为protected。 所以,这个Task实体类不能被修改。 添加了一个AssignToPerson方法,该方法接受人员和任务策略。 CheckIfCanAssignTaskToPerson方法检查它是否是一个有效的赋值,如果没有,则抛出正确的异常(这里的实现不重要)。 那么应用服务方式就是这样的:
public void AssignTaskToPerson(AssignTaskToPersonInput input)
{
var task = _taskRepository.Get(input.TaskId);
var person = _personRepository.Get(input.PersonId);
task.AssignToPerson(person, _taskPolicy);
}
十四、规格模式
规格模式是一种特定的软件设计模式。通过使用布尔逻辑将业务规则链接在一起可以重组业务规则。在实际中,它主要用于为实体或其他业务对象定义可重用的过滤器
Abp定义了规范接口,和实现。使用时只需要继承Specification
public interface ISpecification<T>
{
bool IsSatisfiedBy(T obj);
Expression<Func<T, bool>> ToExpression();
}
//拥有100,000美元余额的客户被认为是PREMIUM客户
public class PremiumCustomerSpecification : Specification<Customer>
{
public override Expression<Func<Customer, bool>> ToExpression()
{
return (customer) => (customer.Balance >= 100000);
}
}
//参数规范示例。
public class CustomerRegistrationYearSpecification : Specification<Customer>
{
public int Year { get; }
public CustomerRegistrationYearSpecification(int year)
{
Year = year;
}
public override Expression<Func<Customer, bool>> ToExpression()
{
return (customer) => (customer.CreationYear == Year);
}
}
public class CustomerManager
{
private readonly IRepository<Customer> _customerRepository;
public CustomerManager(IRepository<Customer> customerRepository)
{
_customerRepository = customerRepository;
}
public int GetCustomerCount(ISpecification<Customer> spec)
{
return _customerRepository.Count(spec.ToExpression());
}
}
count = customerManager.GetCustomerCount(new PremiumCustomerSpecification());
count = customerManager.GetCustomerCount(new CustomerRegistrationYearSpecification(2017));
var count = customerManager.GetCustomerCount(new PremiumCustomerSpecification().And(new CustomerRegistrationYearSpecification(2017)));
我们甚至可以从现有规范中创建一个新的规范类:
public class NewPremiumCustomersSpecification : AndSpecification<Customer>
{
public NewPremiumCustomersSpecification()
: base(new PremiumCustomerSpecification(), new CustomerRegistrationYearSpecification(2017))
{
}
}
AndSpecification类是Specification类的一个子类,只有当两个规范都满足时才能满足。使用如下
var count = customerManager.GetCustomerCount(new NewPremiumCustomersSpecification());
一般不需要使用规范类,可直接使用lambda表达式
var count = _customerRepository.Count(c => c.Balance > 100000 && c.CreationYear == 2017);
十五、工作单元
如果工作方法单元调用另一个工作单元方法,则使用相同的连接和事务。
第一个进入方法管理连接和事务,其他的使用它。
应用服务方法默认是工作单元。
方法开始启动事务,方法结束提交事务。
如果发生异常则回滚。
这样应用服务方法中的所有数据库操作都将变为原子(工作单元)
1,显示使用工作单元
①在方法上引用[UnitOfWork]特性。如果是应用服务方法,则不需要应用此特性
②使用IUnitOfWorkManager
public class MyService
{
private readonly IUnitOfWorkManager _unitOfWorkManager;
private readonly IPersonRepository _personRepository;
private readonly IStatisticsRepository _statisticsRepository;
public MyService(IUnitOfWorkManager unitOfWorkManager, IPersonRepository personRepository, IStatisticsRepository statisticsRepository)
{
_unitOfWorkManager = unitOfWorkManager;
_personRepository = personRepository;
_statisticsRepository = statisticsRepository;
}
public void CreatePerson(CreatePersonInput input)
{
var person = new Person { Name = input.Name, EmailAddress = input.EmailAddress };
using (var unitOfWork = _unitOfWorkManager.Begin())
{
_personRepository.Insert(person);
_statisticsRepository.IncrementPeopleCount();
unitOfWork.Complete();
}
}
}
2,禁用工作单元
[UnitOfWork(IsDisabled = true)]
public virtual void RemoveFriendship(RemoveFriendshipInput input)
{
_friendshipRepository.Delete(input.Id);
}
3,禁用事务功能
[UnitOfWork(isTransactional: false)]
public GetTasksOutput GetTasks(GetTasksInput input)
{
var tasks = _taskRepository.GetAllWithPeople(input.AssignedPersonId, input.State);
return new GetTasksOutput
{
Tasks = Mapper.Map<List<TaskDto>>(tasks)
};
}
4,自动保存更改
如果一个方法是工作单元,ASP.NET Boilerplate会自动在方法结束时保存所有更改。 假设我们需要更新一个人的名字的方法:
[UnitOfWork]
public void UpdateName(UpdateNameInput input)
{
var person = _personRepository.Get(input.PersonId);
person.Name = input.NewName;
}
5,更改工作单元配置
①通常在PreInitialize方法中完成
public class SimpleTaskSystemCoreModule : AbpModule
{
public override void PreInitialize()
{
Configuration.UnitOfWork.IsolationLevel = IsolationLevel.ReadCommitted;
Configuration.UnitOfWork.Timeout = TimeSpan.FromMinutes(30);
}
//...other module methods
}
6,如果访问工作单元
①如果您的类派生自某些特定的基类(ApplicationService,DomainService,AbpController,AbpApiController …等),则可以直接使用CurrentUnitOfWork属性。
②您可以将IUnitOfWorkManager注入任何类并使用IUnitOfWorkManager.Current属性。
6,活动
工作单位已完成,失败和处理事件。 您可以注册这些事件并执行所需的操作。 例如,您可能希望在当前工作单元成功完成时运行一些代码。 例:
public void CreateTask(CreateTaskInput input)
{
var task = new Task { Description = input.Description };
if (input.AssignedPersonId.HasValue)
{
task.AssignedPersonId = input.AssignedPersonId.Value;
_unitOfWorkManager.Current.Completed += (sender, args) => { /* TODO: Send email to assigned person */ };
}
_taskRepository.Insert(task);
}
十六、事件总线
1,两种方式使用事件总线
①依赖注入IEventBus(属性注入比构造函数注入更适合于注入事件总线)
public class TaskAppService : ApplicationService
{
public IEventBus EventBus { get; set; }
public TaskAppService()
{
EventBus = NullEventBus.Instance;
//NullEventBus实现空对象模式。 当你调用它的方法时,它什么都不做
}
}
②获取默认实例(不建议直接使用EventBus.Default,因为它使得单元测试变得更加困难。)
如果不能注入,可以直接使用EventBus.Default
EventBus.Default.Trigger(…); //trigger an event
2,定义事件( EventData类定义EventSource(哪个对象触发事件)和EventTime(触发时)属性)
在触发事件之前,应首先定义事件。 事件由派生自EventData的类表示。 假设我们要在任务完成时触发事件:
public class TaskCompletedEventData : EventData
{
public int TaskId { get; set; }
}
3,预定义事件
①AbpHandledExceptionData:任何异常时触发此事件
②实体变更
还有用于实体更改的通用事件数据类:
EntityCreatingEventData <TEntity>,EntityCreatedEventData <TEntity>,EntityUpdatingEventData <TEntity>,EntityUpdatedEventData <TEntity>,EntityDeletingEventData <TEntity>和EntityDeletedEventData <TEntity>。 另外还有EntityChangingEventData <TEntity>和EntityChangedEventData <TEntity>。 可以插入,更新或删除更改。
“ing”:保存之前
“ed”:保存之后
4,触发事件
public class TaskAppService : ApplicationService
{
public IEventBus EventBus { get; set; }
public TaskAppService()
{
EventBus = NullEventBus.Instance;
}
public void CompleteTask(CompleteTaskInput input)
{
//TODO: complete the task on database...
EventBus.Trigger(new TaskCompletedEventData {TaskId = 42});
}
}
EventBus.Trigger(new TaskCompletedEventData { TaskId = 42 }); //明确地声明泛型参数
EventBus.Trigger(this, new TaskCompletedEventData { TaskId = 42 }); //将“事件源”设置为“this”
EventBus.Trigger(typeof(TaskCompletedEventData), this, new TaskCompletedEventData { TaskId = 42 }); //调用非泛型版本(第一个参数是事件类的类型)
public class ActivityWriter : IEventHandler, ITransientDependency
{
public void HandleEvent(TaskCompletedEventData eventData)
{
WriteActivity("A task is completed by id = " + eventData.TaskId);
}
}
5,处理多个事件
public class ActivityWriter :
IEventHandler<TaskCompletedEventData>,
IEventHandler<TaskCreatedEventData>,
ITransientDependency
{
public void HandleEvent(TaskCompletedEventData eventData)
{
//TODO: handle the event...
}
public void HandleEvent(TaskCreatedEventData eventData)
{
//TODO: handle the event...
}
}
十七、数据过滤器
1,ISoftDelete软删除接口
public class Person : Entity, ISoftDelete
{
public virtual string Name { get; set; }
public virtual bool IsDeleted { get; set; }
}
使用IRepository.Delete方法时将IsDeleted属性设置为true。_personRepository.GetAllList()不会查询出软删除的数据
注:如果您实现IDeletionAudited(扩展了ISoftDelete),则删除时间和删除用户标识也由ASP.NET Boilerplate自动设置。
2, IMustHaveTenant
public class Product : Entity, IMustHaveTenant
{
public int TenantId { get; set; }
public string Name { get; set; }
}
IMustHaveTenant定义TenantId来区分不同的租户实体。 ASP.NET Boilerplate默认使用IAbpSession获取当前TenantId,并自动过滤当前租户的查询。
如果当前用户未登录到系统,或者当前用户是主机用户(主机用户是可管理租户和租户数据的上级用户),ASP.NET Boilerplate将自动禁用IMustHaveTenant过滤器。 因此,所有租户的所有数据都可以被检索到应用程序
3,IMayHaveTenant(没有IMustHaveTenant常用)
public class Role : Entity, IMayHaveTenant
{
public int? TenantId { get; set; }
public string RoleName { get; set; }
}
空值表示这是主机实体,非空值表示由租户拥有的该实体,其ID为TenantId
4,禁用过滤器
var people1 = _personRepository.GetAllList();//访问未删除的
using (_unitOfWorkManager.Current.DisableFilter(AbpDataFilters.SoftDelete))
{
var people2 = _personRepository.GetAllList(); //访问所有的
}
var people3 = _personRepository.GetAllList();//访问未删除的
5,全局禁用过滤器
如果需要,可以全局禁用预定义的过滤器。 例如,要全局禁用软删除过滤器,请将此代码添加到模块的PreInitialize方法中:
Configuration.UnitOfWork.OverrideFilter(AbpDataFilters.SoftDelete, false);
6,自定义过滤器
①定义过滤字段
public interface IHasPerson
{
int PersonId { get; set; }
}
②实体实现接口
public class Phone : Entity, IHasPerson
{
[ForeignKey(“PersonId”)]
public virtual Person Person { get; set; }
public virtual int PersonId { get; set; }
public virtual string Number { get; set; }
}
③定义过滤器
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Filter("PersonFilter", (IHasPerson entity, int personId) => entity.PersonId == personId, 0);
}
“PersonFilter”是此处过滤器的唯一名称。 第二个参数定义了过滤器接口和personId过滤器参数(如果过滤器不是参数,则不需要),最后一个参数是personId的默认值。
最后,我们必须在本模块的PreInitialize方法中向ASP.NET Boilerplate的工作单元注册此过滤器:
Configuration.UnitOfWork.RegisterFilter(“PersonFilter”, false);
第一个参数是我们之前定义的唯一的名称。 第二个参数表示默认情况下是启用还是禁用此过滤器
using (CurrentUnitOfWork.EnableFilter(“PersonFilter”))
{
using(CurrentUnitOfWork.SetFilterParameter(“PersonFilter”, “personId”, 42))
{
var phones = _phoneRepository.GetAllList();
//…
}
}
我们可以从某个来源获取personId,而不是静态编码。 以上示例是参数化过滤器。 滤波器可以有零个或多个参数。 如果没有参数,则不需要设置过滤器参数值。 此外,如果默认情况下启用,则不需要手动启用它(当然,我们可以禁用它)。
十八、应用服务
1,CrudAppService和AsyncCrudAppService类
如果您需要创建一个应用程序服务,该服务将为特定实体创建“创建”,“更新”,“删除”,“获取”GetAll方法,则可以从CrudAppService继承(如果要创建异步方法,则可以继承AsyncCrudAppService)类来创建它。 CrudAppService基类是通用的,它将相关的实体和DTO类型作为通用参数,并且是可扩展的,允许您在需要自定义时重写功能。
①实体类
public class Task : Entity, IHasCreationTime
{
public string Title { get; set; }
public string Description { get; set; }
public DateTime CreationTime { get; set; }
public TaskState State { get; set; }
public Person AssignedPerson { get; set; }
public Guid? AssignedPersonId { get; set; }
public Task()
{
CreationTime = Clock.Now;
State = TaskState.Open;
}
}
②创建DTO
[AutoMap(typeof(Task))]
public class TaskDto : EntityDto, IHasCreationTime
{
public string Title { get; set; }
public string Description { get; set; }
public DateTime CreationTime { get; set; }
public TaskState State { get; set; }
public Guid? AssignedPersonId { get; set; }
public string AssignedPersonName { get; set; }
}
③应用服务
public class TaskAppService : AsyncCrudAppService<Task, TaskDto>,ITaskAppService
{ public TaskAppService(IRepository repository) : base(repository) { } }
④应用服务接口
public interface ITaskAppService : IAsyncCrudAppService
{
}
2,自定义CURD应用服务
1,查询
PagedAndSortedResultRequestDto,它提供可选的排序和分页参数。可以定义派生类过滤
public class GetAllTasksInput : PagedAndSortedResultRequestDto
{
public TaskState? State { get; set; }
}
现在,我们应该更改TaskAppService以应用自定义过滤器
public class TaskAppService : AsyncCrudAppService<Task, TaskDto, int, GetAllTasksInput>
{
public TaskAppService(IRepository repository)
: base(repository)
{
}
protected override IQueryable<Task> CreateFilteredQuery(GetAllTasksInput input)
{
return base.CreateFilteredQuery(input)
.WhereIf(input.State.HasValue, t => t.State == input.State.Value);
}
}
2,创建和更新
创建一个CreateTaskInput类
[AutoMapTo(typeof(Task))]
public class CreateTaskInput
{
[Required]
[MaxLength(Task.MaxTitleLength)]
public string Title { get; set; }
[MaxLength(Task.MaxDescriptionLength)]
public string Description { get; set; }
public Guid? AssignedPersonId { get; set; }
}
并创建一个UpdateTaskInput类
[AutoMapTo(typeof(Task))]
public class UpdateTaskInput : CreateTaskInput, IEntityDto
{
public int Id { get; set; }
public TaskState State { get; set; }
}
现在,我们可以将这些DTO类作为AsyncCrudAppService类的通用参数,如下所示:
public class TaskAppService : AsyncCrudAppService<Task, TaskDto, int, GetAllTasksInput, CreateTaskInput, UpdateTaskInput>
{
public TaskAppService(IRepository repository)
: base(repository)
{
}
protected override IQueryable<Task> CreateFilteredQuery(GetAllTasksInput input)
{
return base.CreateFilteredQuery(input)
.WhereIf(input.State.HasValue, t => t.State == input.State.Value);
}
}
3,CURD权限
您可能需要授权您的CRUD方法。 您可以设置预定义的权限属性:GetPermissionName,GetAllPermissionName,CreatePermissionName,UpdatePermissionName和DeletePermissionName。 如果您设置它们,基本CRUD类将自动检查权限。 您可以在构造函数中设置它,如下所示:
public class TaskAppService : AsyncCrudAppService<Task, TaskDto>
{
public TaskAppService(IRepository repository)
: base(repository)
{
CreatePermissionName = “MyTaskCreationPermission”;
}
}
或者,您可以覆盖适当的权限检查方法来手动检查权限:CheckGetPermission(),CheckGetAllPermission(),CheckCreatePermission(),CheckUpdatePermission(),CheckDeletePermission()。 默认情况下,它们都调用具有相关权限名称的CheckPermission(…)方法,该方法只需调用IPermissionChecker.Authorize(…)方法即可。
十九、数据传输对象
1,帮助接口和类
ILimitedResultRequest定义MaxResultCount属性。 因此,您可以在输入的DTO中实现它,以便对限制结果集进行标准化。
IPagedResultRequest通过添加SkipCount扩展ILimitedResultRequest。 所以,我们可以在SearchPeopleInput中实现这个接口进行分页:
public class SearchPeopleInput : IPagedResultRequest
{
[StringLength(40, MinimumLength = 1)]
public string SearchedName { get; set; }
public int MaxResultCount { get; set; }
public int SkipCount { get; set; }
}
二十、验证数据传输对象
1,使用数据注解(System.ComponentModel.DataAnnotations)
public class CreateTaskInput
{
public int? AssignedPersonId { get; set; }
[Required]
public string Description { get; set; }
}
2,自定义验证
public class CreateTaskInput : ICustomValidate
{
public int? AssignedPersonId { get; set; }
public bool SendEmailToAssignedPerson { get; set; }
[Required]
public string Description { get; set; }
public void AddValidationErrors(CustomValidatationContext context)
{
if (SendEmailToAssignedPerson && (!AssignedPersonId.HasValue || AssignedPersonId.Value <= 0))
{
context.Results.Add(new ValidationResult("AssignedPersonId must be set if SendEmailToAssignedPerson is true!"));
}
}
}
3,Normalize方法在验证之后调用(并在调用方法之前)
public class GetTasksInput : IShouldNormalize
{
public string Sorting { get; set; }
public void Normalize()
{
if (string.IsNullOrWhiteSpace(Sorting))
{
Sorting = "Name ASC";
}
}
}
二十一、授权
1,定义权限
①不同的模块可以有不同的权限。 一个模块应该创建一个派生自AuthorizationProvider的类来定义它的权限
public class MyAuthorizationProvider : AuthorizationProvider
{
public override void SetPermissions(IPermissionDefinitionContext context)
{
var administration = context.CreatePermission(“Administration”);
var userManagement = administration.CreateChildPermission("Administration.UserManagement");
userManagement.CreateChildPermission("Administration.UserManagement.CreateUser");
var roleManagement = administration.CreateChildPermission("Administration.RoleManagement");
}
}
IPermissionDefinitionContext属性定义:
Name:一个系统内的唯一名称, 最好为权限名称
Display name:一个可本地化的字符串,可以在UI中稍后显示权限
Description:一个可本地化的字符串,可用于显示权限的定义,稍后在UI中
MultiTenancySides:对于多租户申请,租户或主机可以使用许可。 这是一个Flags枚举,因此可以在双方使用权限。
featureDependency:可用于声明对功能的依赖。 因此,仅当满足特征依赖性时,才允许该权限。 它等待一个对象实现IFeatureDependency。 默认实现是SimpleFeatureDependency类。 示例用法:new SimpleFeatureDependency(“MyFeatureName”)
②权限可以具有父权限和子级权限。 虽然这并不影响权限检查,但可能有助于在UI中分组权限。
③创建授权提供者后,我们应该在我们的模块的PreInitialize方法中注册它:
Configuration.Authorization.Providers.Add();
2,检查权限
①使用AbpAuthorize特性
[AbpAuthorize(“Administration.UserManagement.CreateUser”)]
public void CreateUser(CreateUserInput input)
{
//如果未授予“Administration.UserManagement.CreateUser”权限,用户将无法执行此方法。
}
AbpAuthorize属性还会检查当前用户是否已登录(使用IAbpSession.UserId)。 所以,如果我们为一个方法声明一个AbpAuthorize,它只检查登录:
[AbpAuthorize]
public void SomeMethod(SomeMethodInput input)
{
//如果用户无法登录,则无法执行此方法。
}
②Abp授权属性说明
ASP.NET Boilerplate使用动态方法截取功能进行授权。 所以方法使用AbpAuthorize属性有一些限制。
不能用于私有方法。
不能用于静态方法。
不能用于非注入类的方法(我们必须使用依赖注入)。
此外
如果方法通过接口调用(如通过接口使用的应用程序服务),可以将其用于任何公共方法。
如果直接从类引用(如ASP.NET MVC或Web API控制器)调用,则该方法应该是虚拟的。
如果保护方法应该是虚拟的。
注意:有四种类型的授权属性:
在应用服务(应用层)中,我们使用Abp.Authorization.AbpAuthorize属性。
在MVC控制器(Web层)中,我们使用Abp.Web.Mvc.Authorization.AbpMvcAuthorize属性。
在ASP.NET Web API中,我们使用Abp.WebApi.Authorization.AbpApiAuthorize属性。
在ASP.NET Core中,我们使用Abp.AspNetCore.Mvc.Authorization.AbpMvcAuthorize属性。
③禁止授权
您可以通过将AbpAllowAnonymous属性添加到应用程序服务来禁用方法/类的授权。 对MVC,Web API和ASP.NET核心控制器使用AllowAnonymous,这些框架是这些框架的本机属性。
④使用IPermissionChecker
虽然AbpAuthorize属性在大多数情况下足够好,但是必须有一些情况需要检查方法体中的权限。 我们可以注入和使用IPermissionChecker,如下例所示:
public void CreateUser(CreateOrUpdateUserInput input)
{
if (!PermissionChecker.IsGranted(“Administration.UserManagement.CreateUser”))
{
throw new AbpAuthorizationException(“您无权创建用户!”);
}
//如果用户未授予“Administration.UserManagement.CreateUser”权限,则无法达到此目的。
}
当然,您可以编写任何逻辑,因为IsGranted只返回true或false(也有Async版本)。 如果您只是检查权限并抛出如上所示的异常,则可以使用Authorize方法:
public void CreateUser(CreateOrUpdateUserInput input)
{
PermissionChecker.Authorize(“Administration.UserManagement.CreateUser”);
//如果用户未授予“Administration.UserManagement.CreateUser”权限,则无法达到此目的。
}
由于授权被广泛使用,ApplicationService和一些常见的基类注入并定义了PermissionChecker属性。 因此,可以在不注入应用程序服务类的情况下使用权限检查器。
⑤Razor 视图
基本视图类定义IsGranted方法来检查当前用户是否具有权限。 因此,我们可以有条件地呈现视图。 例:
@if (IsGranted(“Administration.UserManagement.CreateUser”))
{
@L(“CreateNewUser”)
}
客户端(Javascript)
在客户端,我们可以使用在abp.auth命名空间中定义的API。 在大多数情况下,我们需要检查当前用户是否具有特定权限(具有权限名称)。 例:
abp.auth.isGranted(‘Administration.UserManagement.CreateUser’);
您还可以使用abp.auth.grantedPermissions获取所有授予的权限,或者使用abp.auth.allPermissions获取应用程序中的所有可用权限名称。 在运行时检查其他人的abp.auth命名空间。
二十二、功能管理
大多数SaaS(多租户)应用程序都有具有不同功能的版本(包)。 因此,他们可以向租户(客户)提供不同的价格和功能选项。
1,定义功能
检查前应定义特征。 模块可以通过从FeatureProvider类派生来定义自己的特征。 在这里,一个非常简单的功能提供者定义了3个功能:
public class AppFeatureProvider : FeatureProvider
{
public override void SetFeatures(IFeatureDefinitionContext context)
{
//识别功能的唯一名称(作为字符串).默认值
var sampleBooleanFeature = context.Create(“SampleBooleanFeature”, defaultValue: “false”);
sampleBooleanFeature.CreateChildFeature(“SampleNumericFeature”, defaultValue: “10”);
context.Create(“SampleSelectionFeature”, defaultValue: “B”);
}
}
创建功能提供者后,我们应该在模块的PreInitialize方法中注册,如下所示:
Configuration.Features.Providers.Add();
其他功能属性
虽然需要唯一的名称和默认值属性,但是有一些可选的属性用于详细控制。
Scope:FeatureScopes枚举中的值。 它可以是版本(如果此功能只能为版本级设置),租户(如果此功能只能为租户级别设置)或全部(如果此功能可以为版本和租户设置,租户设置覆盖其版本的 设置)。 默认值为全部。
DisplayName:一个可本地化的字符串,用于向用户显示该功能的名称。
Description:一个可本地化的字符串,用于向用户显示该功能的详细说明。
InputType:功能的UI输入类型。 这可以定义,然后可以在创建自动功能屏幕时使用。
Attributes:键值对的任意自定义词典可以与特征相关。
public class AppFeatureProvider : FeatureProvider
{
public override void SetFeatures(IFeatureDefinitionContext context)
{
var sampleBooleanFeature = context.Create(
AppFeatures.SampleBooleanFeature,
defaultValue: “false”,
displayName: L(“Sample boolean feature”),
inputType: new CheckboxInputType()
);
sampleBooleanFeature.CreateChildFeature(
AppFeatures.SampleNumericFeature,
defaultValue: "10",
displayName: L("Sample numeric feature"),
inputType: new SingleLineStringInputType(new NumericValueValidator(1, 1000000))
);
context.Create(
AppFeatures.SampleSelectionFeature,
defaultValue: "B",
displayName: L("Sample selection feature"),
inputType: new ComboboxInputType(
new StaticLocalizableComboboxItemSource(
new LocalizableComboboxItem("A", L("Selection A")),
new LocalizableComboboxItem("B", L("Selection B")),
new LocalizableComboboxItem("C", L("Selection C"))
)
)
);
}
private static ILocalizableString L(string name)
{
return new LocalizableString(name, AbpZeroTemplateConsts.LocalizationSourceName);
}
}
功能层次结构
如示例功能提供者所示,功能可以具有子功能。 父功能通常定义为布尔特征。 子功能仅在启用父级时才可用。 ASP.NET Boilerplate不执行但建议这一点。 应用程序应该照顾它。
2,检查功能
我们定义一个功能来检查应用程序中的值,以允许或阻止每个租户的某些应用程序功能。 有不同的检查方式。
①使用RequiresFeature属性
[RequiresFeature(“ExportToExcel”)]
public async Task GetReportToExcel(…)
{
…
}
只有对当前租户启用了“ExportToExcel”功能(当前租户从IAbpSession获取),才执行此方法。 如果未启用,则会自动抛出AbpAuthorizationException异常。
当然,RequiresFeature属性应该用于布尔类型的功能。 否则,你可能会得到异常。
②RequiresFeature属性注释
ASP.NET Boilerplate使用动态方法截取功能进行功能检查。 所以,方法使用RequiresFeature属性有一些限制。
不能用于私有方法。
不能用于静态方法。
不能用于非注入类的方法(我们必须使用依赖注入)。
此外
如果方法通过接口调用(如通过接口使用的应用程序服务),可以将其用于任何公共方法。
如果直接从类引用(如ASP.NET MVC或Web API控制器)调用,则该方法应该是虚拟的。
如果保护方法应该是虚拟的。
③使用IFeatureChecker
我们可以注入和使用IFeatureChecker手动检查功能(它自动注入并可直接用于应用程序服务,MVC和Web API控制器)。
public async Task GetReportToExcel(…)
{
if (await FeatureChecker.IsEnabledAsync(“ExportToExcel”))
{
throw new AbpAuthorizationException(“您没有此功能:ExportToExcel”);
}
...
}
如果您只想检查一个功能并抛出异常,如示例所示,您只需使用CheckEnabled方法即可。
④获取功能的值
var createdTaskCountInThisMonth = GetCreatedTaskCountInThisMonth();
if (createdTaskCountInThisMonth >= FeatureChecker.GetValue(“MaxTaskCreationLimitPerMonth”).To())
{
throw new AbpAuthorizationException(“你超过本月的任务创建限制”);
}
FeatureChecker方法也覆盖了指定tenantId的工作功能,不仅适用于当前的tenantId。
⑤客户端
在客户端(javascript)中,我们可以使用abp.features命名空间来获取当前的功能值。
var isEnabled = abp.features.isEnabled(‘SampleBooleanFeature’);
var value = abp.features.getValue(‘SampleNumericFeature’);