二、领域层

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’);

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐