ABP框架概念

二、领域层

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

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

是刘彦宏吖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值