ABP框架学习
一、总体与公共结构
1,ABP配置
二、领域层
三、应用层
四、分布式服务层
24,ASP.NET Web API Controllers
五、展示层
六、后台服务
七、实时服务
八、ORM
//将所有异常发送到客户端 Configuration.Modules.AbpWebCommon().SendAllExceptionsToClients = true;
1,自定义配置
①创建配置类
public class CustomModule { public bool IsEnable { get; set; } }
②定义Configuration扩展
public static class ModuleConfigrationExt { public static CustomModule GetCustomModule(this IModuleConfigurations moduleConfigurations) { return moduleConfigurations.AbpConfiguration.Get<CustomModule>(); } }
③使用自定义配置
//注册CustomModule配置类 IocManager.Register<CustomModule>(); //设置CustomModule配置 Configuration.Modules.GetCustomModule().IsEnable = true;
1,在core层开启多租户
Configuration.MultiTenancy.IsEnabled = true;
2,主机与租户
主机:拥有自己的用户,角色,权限,设置的客户,并使用与其他租户完全隔离的应用程序。 多租户申请将有一个或多个租户。 如果这是CRM应用程序,不同的租户也有自己的帐户,联系人,产品和订单。 所以,当我们说一个'租户用户'时,我们的意思是租户拥有的用户。
租户:主机是单例(有一个主机)。 主持人负责创建和管理租户。 所以,“主机用户”是较高层次的,独立于所有租户,可以控制他们。
3,abp定义IAbpSession接口获取UserId和TenantId
如果UserId和TenantId都为空,则当前用户未登录到系统。 所以,我们不知道是主机用户还是租户用户。 在这种情况下,用户无法访问授权的内容。
如果UserId不为null并且TenantId为null,那么我们可以知道当前用户是主机用户。
如果UserId不为null,并且TenantId不为空,我们可以知道当前用户是租户用户。
如果UserId为null但TenantId不为空,那意味着我们可以知道当前的租户,但是当前的请求没有被授权(用户没有登录)
4,数据过滤器
如果实体实现的是IMustHaveTenant接口,且AbpSession.TenantId为null的时候(即主机用户),获取到的数据是所有租户的,除非你自己显式进行过滤。而在IMayHaveTenant情况下,AbpSession.TenantId为null获取到的是主机用户的数据。
①IMustHaveTenant 接口:该接口通过定义TenantId属性来区分不同租户的实体
public class Product : Entity, IMustHaveTenant { public int TenantId { get; set; } public string Name { get; set; } //...其他属性 }
②IMayHaveTenant接口:我们可能需要在租户和租户之间共享一个实体类型。因此,一个实体可能会被一个租户或租主拥有
public class Role : Entity, IMayHaveTenant { public int? TenantId { get; set; } public string RoleName { get; set; } //...其他属性 }
IMayHaveTenant不像IMustHaveTenant一样常用。比如,一个Product类可以不实现IMayHaveTenant接口,因为Product和实际的应用功能相关,和管理租户不相干。因此,要小心使用IMayHaveTenant接口,因为它更难维护租户和租主共享的代码。
1,AbpSession定义了一些关键属性:
UserId:当前用户的ID,如果没有当前用户,则为null。 如果通话代码被授权,则不能为空。
TenantId:当前租户的ID,如果没有当前租户(如果用户未登录或他是主机用户),则为null。
ImpersonatorUserId:当前会话由其他用户模拟时,模拟人员的身份。 如果这不是模拟登录,它为null。
ImpersonatorTenantId:假冒用户的租户的身份,如果当前会话由其他用户模拟。 如果这不是模拟登录,它为null。
MultiTenancySide:可能是主机或租户。
2,覆盖当前会话值
public class MyService { private readonly IAbpSession _session; public MyService(IAbpSession session) { _session = session; } public void Test() { using (_session.Use(42, null)) { var tenantId = _session.TenantId; //42 var userId = _session.UserId; //null } } }
1,我们可以注入它并使用它来获取缓存
public class TestAppService : ApplicationService { private readonly ICacheManager _cacheManager; public TestAppService(ICacheManager cacheManager) { _cacheManager = cacheManager; } public Item GetItem(int id) { //Try to get from cache return _cacheManager .GetCache("MyCache") .Get(id.ToString(), () => GetFromDatabase(id)) as Item; } public Item GetFromDatabase(int id) { //... retrieve item from database } }
2,配置缓存有效期
//Configuration for all caches Configuration.Caching.ConfigureAll(cache => { cache.DefaultSlidingExpireTime = TimeSpan.FromHours(2); }); //Configuration for a specific cache Configuration.Caching.Configure("MyCache", cache => { cache.DefaultSlidingExpireTime = TimeSpan.FromHours(8); });
默认缓存超时时间为60分钟。该代码应该放在您的模块的PreInitialize方法中。 有了这样一个代码,MyCache将有8个小时的过期时间,而所有其他缓存将有2个小时。
3,实体缓存(例如:根据人员id查询人员信息缓存)
①在dto中定义缓存类
[AutoMapFrom(typeof(Person))] public class PersonCacheItem { public string Name { get; set; } }
②在application层中定义实体缓存接口
public interface IPersonCache : IEntityCache<PersonCacheItem> { }
③实现缓存接口
public class PersonCache : EntityCache<Person, PersonCacheItem>, IPersonCache, ITransientDependency { public PersonCache(ICacheManager cacheManager, IRepository<Person> repository) : base(cacheManager, repository) { } }
④应用层使用
public class MyPersonService : ITransientDependency { private readonly IPersonCache _personCache; public MyPersonService(IPersonCache personCache) { _personCache = personCache; } public string GetPersonNameById(int id) { return _personCache[id].Name; //替代: _personCache.Get(id).Name; } }
4,使用Redis缓存
①在Web项目中Nuget引用Abp.RedisCache
②设置Module
using Abp.Runtime.Caching.Redis; namespace MyProject.AbpZeroTemplate.Web { [DependsOn( //...other module dependencies typeof(AbpRedisCacheModule))] public class MyProjectWebModule : AbpModule { public override void PreInitialize() { //...other configurations Configuration.Caching.UseRedis(); } //...other code } }
③Abp.Redis Cache包使用“localhost”作为连接字符串作为默认值。 您可以将连接字符串添加到您的配置文件中以覆盖它。 例:
<add name="Abp.Redis.Cache" connectionString="localhost"/>
④此外,您可以将appSettings的设置添加到Redis的数据库ID中。 例:
<add key="Abp.Redis.Cache.DatabaseId" value="2"/>
不同的数据库标识对于在同一服务器中创建不同的密钥空间(隔离缓存)很有用。
1,在Web层NuGet安装:Abp.Castle.Log4Net
2,在Web层的Global.asax注入logger
public class MvcApplication : AbpWebApplication<SimpleWebModule> { protected override void Session_Start(object sender, EventArgs e) { IocManager.Instance.IocContainer.AddFacility<LoggingFacility>( f => f.UseAbpLog4Net().WithConfig("log4net.config")); base.Session_Start(sender, e); } }
3,在Web层添加log4net配置文件(log4net.config)
<?xml version="1.0" encoding="utf-8" ?> <log4net> <appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender" > <file value="App_Data/Logs/Logs.txt" /> <appendToFile value="true" /> <rollingStyle value="Size" /> <maxSizeRollBackups value="10" /> <maximumFileSize value="10000KB" /> <staticLogFileName value="true" /> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%-5level %date [%-5.5thread] %-40.40logger - %message%newline" /> </layout> </appender> <root> <appender-ref ref="RollingFileAppender" /> <level value="DEBUG" /> </root> <logger name="NHibernate"> <level value="WARN" /> </logger> </log4net>
4,在application层直接直接使用Logger
public async Task<ListResultDto<EmployeeListDto>> GetEmpAll() { Logger.Info("查询员工信息"); var emps = await _employeeRepository.GetAllListAsync(); return new ListResultDto<EmployeeListDto>(emps.MapTo<List<EmployeeListDto>>()); }
5,客户端
abp.log.warn('a sample log message...');
一个设置一般是存储在数据库(或其他源)的name-value字符串对。我们可以将非字符串的值转换成字符串。
使用前必须定义一个设置。 ASP.NET Boilerplate设计为模块化。 所以,不同的模块可以有不同的设置
1,定义设置
public class MySettingProvider : SettingProvider { public override IEnumerable<SettingDefinition> GetSettingDefinitions(SettingDefinitionProviderContext context) { return new[] { new SettingDefinition( "SmtpServerAddress", "127.0.0.1" ), new SettingDefinition( "PassiveUsersCanNotLogin", "true", scopes: SettingScopes.Application | SettingScopes.Tenant ), new SettingDefinition( "SiteColorPreference", "red", scopes: SettingScopes.User, isVisibleToClients: true ) }; } }
参数说明:
Name(必填):名称
Default Name:默认值,该值可以为空或空字符串
Scopes:
Application:应用范围设置用于用户/租户独立设置。 例如,我们可以定义一个名为“SmtpServerAddress”的设置,以便在发送电子邮件时获取服务器的IP地址。 如果此设置具有单个值(不根据用户进行更改),那么我们可以将其定义为应用程序范围。
Tenant:如果应用程序是多租户,我们可以定义特定于租户的设置。
User:我们可以使用用户作用域设置来存储/获取每个用户特定设置的值。
Display Name:可用于在UI中稍后显示设置名称的本地化字符串。
Description:一个可本地化的字符串,可用于稍后在UI中显示设置描述。
Group:可用于分组设置。 这只是用于UI,不用于设置管理。
IsVisibleToClients:设置为true,使客户端可以使用设置。
2,创建setting provider后,我们应该在本模块的PreIntialize方法中注册它:
Configuration.Settings.Providers.Add<MySettingProvider>();
由于ISettingManager被广泛使用,一些特殊的基类(如ApplicationService,DomainService和AbpController)有一个名为SettingManager的属性。 如果我们从这些类派生,不需要明确地注入它们。
3,获取设置
①服务器端:
//获取布尔值(异步调用) var value1 = await SettingManager.GetSettingValueAsync<bool>("PassiveUsersCanNotLogin"); //获取字符串值(同步调用) var value2 = SettingManager.GetSettingValue("SmtpServerAddress");
②客户端:
当定义一个setting时,如果将IsVisibleToClients设置为true,那么可以使用javascript在客户端获得当前的值。abp.setting命名空间定义了一些用得到的函数和对象。例如:
var currentColor = abp.setting.get("SiteColorPreference");
也有getInt和 getBoolean 方法。你可以使用abp.setting.values获得所有的值。注意:如果在服务端更改了一个setting,那么如果页面没有更新,setting没有重新加载或者通过代码手动更新的话,那么客户端就不知道该setting是否发生了变化。
1,Clock类静态属性介绍
Now:根据当前提供商获取当前时间
Kind:获取当前提供者的DateTimeKind
SupportsMultipleTimezone:获取一个值,表示当前提供程序可以用于需要多个时区的应用程序
Normalize:对当前提供商的给定DateTime进行规范化/转换
DateTime now = Clock.Now;
2,三种内置的clock
ClockProviders.Unspecified (UnspecifiedClockProvider):默认的clock。就像DateTime.Now一样
ClockProviders.Utc (UtcClockProvider):Normalize方法将给定的datetime转换为utc datetime,并将其设置为DateTimeKind.UTC。 它支持多个时区。
ClockProviders.Local (LocalClockProvider):在本地计算机的时间工作。 Normalize方法将给定的datetime转换为本地datetime,并将其设置为DateTimeKind.Local
您可以设置Clock.Provider以使用不同的时钟提供程序:
Clock.Provider = ClockProviders.Utc;
这通常在应用程序开始时完成(适用于Web应用程序中的Application_Start)。
1,使用AbpAutoMapper
①模块使用AbpAutoMapper需要引用依赖
namespace SimpleDemo.Application { [DependsOn(typeof(SimpleDemoCoreModule), typeof(AbpAutoMapperModule))] public class SimpleDemoApplicationModule:AbpModule { public override void Initialize() { IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly()); } } }
②使用AutoMapFrom 、AutoMapTo 两个方向的映射
[AutoMapTo(typeof(User))] public class CreateUserInput { public string Name { get; set; } public string Surname { get; set; } public string EmailAddress { get; set; } public string Password { get; set; } }
[AutoMapFrom(typeof(User))] public class CreateUserOutput { public string Name { get; set; } public string Surname { get; set; } public string EmailAddress { get; set; } public string Password { get; set; } }
2,自定义映射
在某些情况下,简单的映射可能不合适。 例如,两个类的属性名可能有些不同,或者您可能希望在映射期间忽略某些属性
假设我们要忽略映射的密码,并且用户具有电子邮件地址的电子邮件属性。 我们可以定义映射,如下所示:
[DependsOn(typeof(AbpAutoMapperModule))] public class MyModule : AbpModule { public override void PreInitialize() { Configuration.Modules.AbpAutoMapper().Configurators.Add(config => { config.CreateMap<CreateUserInput, User>() .ForMember(u => u.Password, options => options.Ignore()) .ForMember(u => u.Email, options => options.MapFrom(input => input.EmailAddress)); }); } }
1,Abp.Net.Mail.EmailSettingNames类中定义为常量字符串。 他们的价值观和描述:
Abp.Net.Mail.DefaultFromAddress:在发送电子邮件时不指定发件人时,用作发件人电子邮件地址(如上面的示例所示)。
Abp.Net.Mail.DefaultFromDisplayName:在发送电子邮件时不指定发件人时,用作发件人显示名称(如上面的示例所示)。
Abp.Net.Mail.Smtp.Host:SMTP服务器的IP /域(默认值:127.0.0.1)。
Abp.Net.Mail.Smtp.Port:SMTP服务器的端口(默认值:25)。
Abp.Net.Mail.Smtp.UserName:用户名,如果SMTP服务器需要身份验证。
Abp.Net.Mail.Smtp.Password:密码,如果SMTP服务器需要身份验证。
Abp.Net.Mail.Smtp.Domain:域名用户名,如果SMTP服务器需要身份验证。
Abp.Net.Mail.Smtp.EnableSsl:一个值表示SMTP服务器使用SSL(“true”或“false”),默认值为“false”)。
Abp.Net.Mail.Smtp.UseDefaultCredentials:True,使用默认凭据而不是提供的用户名和密码(“true”或“false”)。默认值为“true”)。
2,使用qq邮箱发送邮件
①需要在qq邮箱中开启smtp服务
②配置设置
namespace Demo.Core.Email { public class AppSettingProvider:SettingProvider { public override IEnumerable<SettingDefinition> GetSettingDefinitions(SettingDefinitionProviderContext context) { return new[] { //EnableSsl一定设置为true new SettingDefinition( EmailSettingNames.Smtp.EnableSsl,"true",L("SmtpEnableSsl"),scopes:SettingScopes.Application|SettingScopes.Tenant), //UseDefaultCredentials一定设置为false new SettingDefinition( EmailSettingNames.Smtp.UseDefaultCredentials,"false",L("SmtpUseDefaultCredentials"),scopes:SettingScopes.Application|SettingScopes.Tenant), new SettingDefinition( EmailSettingNames.Smtp.Host,"smtp.qq.com",L("SmtpHost"),scopes:SettingScopes.Application|SettingScopes.Tenant), new SettingDefinition( EmailSettingNames.Smtp.Port,"25",L("SmtpPort"),scopes:SettingScopes.Application|SettingScopes.Tenant), new SettingDefinition( EmailSettingNames.Smtp.UserName,"962410314@qq.com",L("SmtpUserName"),scopes:SettingScopes.Application|SettingScopes.Tenant), //Password使用授权码 new SettingDefinition( EmailSettingNames.Smtp.Password,"aaa",L("SmtpPassword"),scopes:SettingScopes.Application|SettingScopes.Tenant), //这个EmailSettingNames.Smtp.Domain设置后老是报错,索性直接注销了 //new SettingDefinition( EmailSettingNames.Smtp.Domain,"962410314@qq.com",L("SmtpDomain"),scopes:SettingScopes.Application|SettingScopes.Tenant), new SettingDefinition( EmailSettingNames.DefaultFromAddress,"962410314@qq.com",L("SmtpDefaultFromAddress"),scopes:SettingScopes.Application|SettingScopes.Tenant), new SettingDefinition( EmailSettingNames.DefaultFromDisplayName,"默认",L("SmtpDefaultFromDisplayName"),scopes:SettingScopes.Application|SettingScopes.Tenant), }; } private static LocalizableString L(string name) { return new LocalizableString(name, AbpConsts.LocalizationSourceName); } } }
③在PreInitialize中注册
namespace Demo.Core { public class DemoCoreModule:AbpModule { public override void PreInitialize() { Configuration.Settings.Providers.Add<AppSettingProvider>(); } public override void Initialize() { IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly()); } } }
④发送邮件
namespace Demo.Core.Employee { public class EmployeeManager:IDomainService { private readonly IEmailSender _emailSender; public EmployeeManager(IEmailSender emailSender) { _emailSender = emailSender; } public void send() { _emailSender.Send(to: "qq962410314@163.com", subject: "测试", body: "测试", isBodyHtml: false); } } }
案例下载:http://pan.baidu.com/s/1dF5wp9J
实体具有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<T>
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<TaskCompletedEventData>(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<TaskCompletedEventData>, 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<Task> repository) : base(repository) { } }
④应用服务接口
public interface ITaskAppService : IAsyncCrudAppService<TaskDto> { }
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<Task> 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<Task> 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<Task> 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<MyAuthorizationProvider>();
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")) { <button id="CreateNewUserButton" class="btn btn-primary"><i class="fa fa-plus"></i> @L("CreateNewUser")</button> }
客户端(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<AppFeatureProvider>();
其他功能属性
虽然需要唯一的名称和默认值属性,但是有一些可选的属性用于详细控制。
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<FileDto> GetReportToExcel(...) { ... }
只有对当前租户启用了“ExportToExcel”功能(当前租户从IAbpSession获取),才执行此方法。 如果未启用,则会自动抛出AbpAuthorizationException异常。
当然,RequiresFeature属性应该用于布尔类型的功能。 否则,你可能会得到异常。
②RequiresFeature属性注释
ASP.NET Boilerplate使用动态方法截取功能进行功能检查。 所以,方法使用RequiresFeature属性有一些限制。
不能用于私有方法。
不能用于静态方法。
不能用于非注入类的方法(我们必须使用依赖注入)。
此外
如果方法通过接口调用(如通过接口使用的应用程序服务),可以将其用于任何公共方法。
如果直接从类引用(如ASP.NET MVC或Web API控制器)调用,则该方法应该是虚拟的。
如果保护方法应该是虚拟的。
③使用IFeatureChecker
我们可以注入和使用IFeatureChecker手动检查功能(它自动注入并可直接用于应用程序服务,MVC和Web API控制器)。
public async Task<FileDto> GetReportToExcel(...) { if (await FeatureChecker.IsEnabledAsync("ExportToExcel")) { throw new AbpAuthorizationException("您没有此功能:ExportToExcel"); } ... }
如果您只想检查一个功能并抛出异常,如示例所示,您只需使用CheckEnabled方法即可。
④获取功能的值
var createdTaskCountInThisMonth = GetCreatedTaskCountInThisMonth(); if (createdTaskCountInThisMonth >= FeatureChecker.GetValue("MaxTaskCreationLimitPerMonth").To<int>()) { throw new AbpAuthorizationException("你超过本月的任务创建限制"); }
FeatureChecker方法也覆盖了指定tenantId的工作功能,不仅适用于当前的tenantId。
⑤客户端
在客户端(javascript)中,我们可以使用abp.features命名空间来获取当前的功能值。
var isEnabled = abp.features.isEnabled('SampleBooleanFeature'); var value = abp.features.getValue('SampleNumericFeature');
3,深度研究
一、AbpFeatures表分析
1,Name:
名称,必须先配置FeatureProvider,此名称一定在FeatureProvider配置中存在。唯一
2,Value:
①值。支持两种类型(bool类型和值类型)
②bool类型使用FeatureChecker.CheckEnabled("")获取,不存咋抛异常
③值类型使用FeatureChecker.GetValue("").To<int>()获取,不存咋抛异常
3,EditionId:
①版本关联,使用EditionFeatureSetting类型创建
②使用_abpEditionManager.SetFeatureValueAsync()设置基于版本的功能
4,TenantId:
①租户关联,使用TenantFeatureSetting类型创建
②使用TenantManager.SetFeatureValue()设置基于租户的功能
5,Discriminator:
①标识
②值为TenantFeatureSetting表示基于租户
③值为EditionFeatureSetting表示基于版本
二、案例分析
FeatureProvider配置
AbpFeatures表
权限配置
1,租户1没有Product权限(当前Product权限为菜单权限),所以租户1在页面上不会显示出Product菜单权限
2,租户2 FeatureChecker.GetValue("Count").To<int>() 值为20
3,租户3 FeatureChecker.GetValue("Count").To<int>() 值为30
4,租户1 FeatureChecker.GetValue("Count").To<int>() 值为10(默认配置的值)
基本上保存的字段是:相关租户信息tenant id,、user id、服务名称(被调用方法的类)、方法名称、执行参数(序列化为json)、执行时间、执行持续时间(毫秒)、客户端ip地址、客户端计算机名称和异常(如果方法抛出异常)
1,配置
要配置审核,可以在模块的PreInitialize方法中使用Configuration.Auditing属性。 默认情况下启用审计。 您可以禁用它,如下所示。
public class MyModule : AbpModule { public override void PreInitialize() { Configuration.Auditing.IsEnabled = false; } //... }
这里列出了审核配置属性:
- IsEnabled:用于完全启用/禁用审计系统。 默认值:true。
- IsEnabledForAnonymousUsers:如果设置为true,则对于未登录系统的用户,还会保存审核日志。 默认值:false。
- Selectors: 用于选择其他类来保存审核日志。
选择器是选择其他类型以保存审核日志的谓词列表。 选择器具有唯一的名称和谓词。 此列表中唯一的默认选择器用于选择应用程序服务类。 定义如下:
Configuration.Auditing.Selectors.Add( new NamedTypeSelector( "Abp.ApplicationServices", type => typeof (IApplicationService).IsAssignableFrom(type) ) );
您可以在您的模块的PreInitialize方法中添加选择器。 另外,如果不想保存应用程序服务的审核日志,可以通过名称删除上面的选择器。 这就是为什么它有一个唯一的名称(使用简单的LINQ在选择器中找到选择器,如果需要的话)。
注意:除了标准审核配置外,MVC和ASP.NET Core模块还定义了启用/禁用审计日志记录的配置。
2,按属性启用/禁用
虽然您可以通过配置选择审核类,但可以对单个类使用“已审核”和“禁用属性”,也就是单一方法。 一个例子:
[Audited] public class MyClass { public void MyMethod1(int a) { //... } [DisableAuditing] public void MyMethod2(string b) { //... } public void MyMethod3(int a, int b) { //... } }
除了MyMethod2,MyClass的所有方法都被审计,因为它被明确禁用。 经审计的属性可用于仅为所需方法保存审核的方法。
DisableAuditing也可以用于DTO的一个或一个属性。 因此,您可以隐藏审计日志中的敏感数据,例如密码。
二十四、ASP.NET Web API Controllers
1,AbpApiController基类
public class UsersController : AbpApiController { }
2,本地化
AbpApiController定义了L方法,使本地化更容易。 例:
public class UsersController : AbpApiController { public UsersController() { LocalizationSourceName = "MySourceName"; } public UserDto Get(long id) { var helloWorldText = L("HelloWorld"); //... } }
您应该设置LocalizationSourceName使L方法工作。 您可以将其设置在您自己的基本api控制器类中,以便不对每个api控制器重复。
3,其他
您还可以使用预先注入的AbpSession,EventBus,PermissionManager,PermissionChecker,SettingManager,FeatureManager,FeatureChecker,LocalizationManager,Logger,CurrentUnitOfWork基础属性等。
4,过滤器
①审计日志
AbpApiAuditFilter用于集成审计日志系统。 它默认将所有请求记录到所有操作(如果未禁用审核)。 您可以使用Audited和DisableAuditing属性控制审计日志记录,以执行操作和控制器。
②授权
您可以为您的api控制器或操作使用AbpApiAuthorize属性,以防止未经授权的用户使用您的控制器和操作。 例:
public class UsersController : AbpApiController { [AbpApiAuthorize("MyPermissionName")] public UserDto Get(long id) { //... } }
您可以为操作或控制器定义AllowAnonymous属性以抑制身份验证/授权。 AbpApiController还将IsGranted方法定义为在代码中检查权限的快捷方式。
③防伪过滤器
AbpAntiForgeryApiFilter用于自动保护来自CSRF / XSRF攻击的POST,PUT和DELETE请求的ASP.NET Web API操作(包括动态Web api)
④工作单位
AbpApiUowFilter用于集成到工作单元系统。 在动作执行之前,它会自动开始一个新的工作单元,并在动作执行之后完成工作单元(如果没有异常被抛出)。
您可以使用UnitOfWork属性来控制操作的UOW的行为。 您还可以使用启动配置来更改所有操作的默认工作属性。
⑤结果缓存
ASP.NET Boilerplate为Web API请求的响应添加了Cache-Control头(无缓存,无存储)。 因此,它甚至阻止浏览器缓存GET请求。 可以通过配置禁用此行为。
⑥验证
AbpApiValidationFilter会自动检查ModelState.IsValid,如果该操作无效,可以防止执行该操作。 此外,实现验证文档中描述的输入DTO验证。
⑦模型绑定
AbpApiDateTimeBinder用于使用Clock.Normalize方法对DateTime(和Nullable <DateTime>)进行归一化。
1,ForAll Method
DynamicApiControllerBuilper提供了一种在一个调用中为所有应用程序服务构建Web api控制器的方法
Configuration.Modules.AbpWebApi().DynamicApiControllerBuilder .ForAll<IApplicationService>(Assembly.GetAssembly(typeof(SimpleTaskSystemApplicationModule)), "tasksystem") .Build();
对于此配置,服务将是“/api/services/tasksystem/task”和“/api/services/tasksystem/person”。 要计算服务名称:Service和AppService后缀和I前缀被删除(用于接口)
2,覆盖ForAll
我们可以在ForAll方法之后覆盖配置。 例:
Configuration.Modules.AbpWebApi().DynamicApiControllerBuilder .ForAll<IApplicationService>(Assembly.GetAssembly(typeof(SimpleTaskSystemApplicationModule)), "tasksystem") .Build(); Configuration.Modules.AbpWebApi().DynamicApiControllerBuilder .For<ITaskAppService>("tasksystem/task") .ForMethod("CreateTask").DontCreateAction().Build();
在此代码中,我们为程序集中的所有应用程序服务创建了动态Web API控制器。 然后覆盖单个应用程序服务(ITaskAppService)的配置来忽略CreateTask方法。
3,ForMethods
使用ForAll方法时,可以使用ForMethods方法来更好地调整每种方法。 例:
Configuration.Modules.AbpWebApi().DynamicApiControllerBuilder .ForAll<IApplicationService>(Assembly.GetExecutingAssembly(), "app") .ForMethods(builder => { if (builder.Method.IsDefined(typeof(MyIgnoreApiAttribute))) { builder.DontCreate = true; } }) .Build();
在此示例中,我使用自定义属性(MyIgnoreApiAttribute)来检查所有方法,并且不为使用该属性标记的方法创建动态Web api控制器操作。
4,Http动词
默认情况下,所有方法都创建为POST
5,WithVerb Method
Configuration.Modules.AbpWebApi().DynamicApiControllerBuilder .For<ITaskAppService>("tasksystem/task") .ForMethod("GetTasks").WithVerb(HttpVerb.Get) .Build();
6,Http特性
public interface ITaskAppService : IApplicationService { [HttpGet] GetTasksOutput GetTasks(GetTasksInput input); [HttpPut] void UpdateTask(UpdateTaskInput input); [HttpPost] void CreateTask(CreateTaskInput input); }
7,WithConventionalVerbs 命名公约
Configuration.Modules.AbpWebApi().DynamicApiControllerBuilder .ForAll<IApplicationService>(Assembly.GetAssembly(typeof(SimpleTaskSystemApplicationModule)), "tasksystem") .WithConventionalVerbs() .Build();
- Get: 如果方法名以“Get”开头,则使用
- Put:如果方法名称以“Put”或“Update”开头,则使用
- Delete: 如果方法名称以“Delete”或“Remove”开头
- Post: 如果方法名称以“Post”,“Create”或“Insert”开头,则使用
- Patch: 如果方法名称以“Patch”开头,则使用
- 否则,Post被用作默认的HTTP动词
8,远程服务属性
您还可以使用RemoteService属性进行任何接口或方法定义来启用/禁用(IsEnabled)动态API或API资源管理器设置(IsMetadataEnabled)。
9,动态Javascript代理
abp.services.tasksystem.task.getTasks({ state: 1 }).done(function (result) { //use result.tasks here... });
Javascript代理是动态创建的。 在使用之前,您应该将动态脚本添加到页面中:
<script src="/api/AbpServiceProxies/GetAll" type="text/javascript"></script>
10,AJAX参数
您可能希望将自定义ajax参数传递给代理方法。 你可以将它们作为第二个参数传递下面所示:
abp.services.tasksystem.task.createTask({ assignedPersonId: 3, description: 'a new task description...' },{ //覆盖jQuery的ajax参数 async: false, timeout: 30000 }).done(function () { abp.notify.success('successfully created a task!'); });
jQuery.ajax的所有参数在这里是有效的。
除了标准的jQuery.ajax参数之外,您可以向AJAX选项添加abpHandleError:false,以便在错误情况下禁用自动消息显示。
11,单一服务脚本
'/api/AbpServiceProxies/GetAll'在一个文件中生成所有服务代理。 您还可以使用'/api/AbpServiceProxies/Get?name=serviceName'生成单个服务代理,并将脚本包含在页面中,如下所示:
<script src="/api/AbpServiceProxies/Get?name=tasksystem/task" type="text/javascript"></script>
12,Angular整合
(function() { angular.module('app').controller('TaskListController', [ '$scope', 'abp.services.tasksystem.task', function($scope, taskService) { var vm = this; vm.tasks = []; taskService.getTasks({ state: 0 }).success(function(result) { vm.tasks = result.tasks; }); } ]); })();
为了能够使用自动生成的服务,您应该在页面中包含所需的脚本:
<script src="~/Abp/Framework/scripts/libs/angularjs/abp.ng.js"></script> <script src="~/api/AbpServiceProxies/GetAll?type=angular"></script>
OData被定义为“odata.org中允许以简单和标准的方式创建和消费可查询和可互操作的RESTful API的开放式协议”
1,安装包
Install-Package Abp.Web.Api.OData
2,设置模块依赖
配置您的实体
OData需要声明可用作OData资源的实体。 我们应该在我们模块的PreInitialize方法中执行此操作,如下所示:
[DependsOn(typeof(AbpWebApiODataModule))] public class MyProjectWebApiModule : AbpModule { public override void PreInitialize() { var builder = Configuration.Modules.AbpWebApiOData().ODataModelBuilder; //在这里配置您的实体 builder.EntitySet<Person>("Persons"); } ... }
在这里,我们得到了ODataModelBuilder引用,并设置了Person实体。 您可以使用EntitySet添加类似的其他实体
3,创建控制器
Abp.Web.Api.OData nuget软件包包括AbpODataEntityController基类(扩展标准ODataController),以便更轻松地创建控制器。 为Person实体创建OData端点的示例:
public class PersonsController : AbpODataEntityController<Person> { public PersonsController(IRepository<Person> repository) : base(repository) { } }
这很容易 AbpODataEntityController的所有方法都是虚拟的。 这意味着您可以覆盖Get,Post,Put,Patch,Delete和其他操作,并添加您自己的逻辑。
4,配置
Abp.Web.Api.OData自动调用HttpConfiguration.MapODataServiceRoute方法与常规配置。 如果需要,您可以设置Configuration.Modules.AbpWebApiOData()。MapAction自己映射OData路由。
5,例子
在这里,一些例子请求到上面定义的控制器。 假设应用程序在http://localhost:61842上工作。 我们将展示一些基础知识。 由于OData是标准协议,您可以轻松地在网络上找到更多高级示例。
①获取所有列表
请求:GET http://localhost:61842/odata/Persons
响应:
{ "@odata.context":"http://localhost:61842/odata/$metadata#Persons","value":[ { "Name":"Douglas Adams","IsDeleted":false,"DeleterUserId":null,"DeletionTime":null,"LastModificationTime":null,"LastModifierUserId":null,"CreationTime":"2015-11-07T20:12:39.363+03:00","CreatorUserId":null,"Id":1 },{ "Name":"John Nash","IsDeleted":false,"DeleterUserId":null,"DeletionTime":null,"LastModificationTime":null,"LastModifierUserId":null,"CreationTime":"2015-11-07T20:12:39.363+03:00","CreatorUserId":null,"Id":2 } ] }
②获取单个实体(where id=2)
请求:GET http://localhost:61842/odata/Persons(2)
响应:
{ "@odata.context":"http://localhost:61842/odata/$metadata#Persons/$entity","Name":"John Nash","IsDeleted":false,"DeleterUserId":null,"DeletionTime":null,"LastModificationTime":null,"LastModifierUserId":null,"CreationTime":"2015-11-07T20:12:39.363+03:00","CreatorUserId":null,"Id":2 }
③获取具有导航属性的单个实体(获得Id = 1的人,包括他的电话号码)
请求:GET http://localhost:61842/odata/Persons(1)?$expand=Phones
响应:
{ "@odata.context":"http://localhost:61842/odata/$metadata#Persons/$entity","Name":"Douglas Adams","IsDeleted":false,"DeleterUserId":null,"DeletionTime":null,"LastModificationTime":null,"LastModifierUserId":null,"CreationTime":"2015-11-07T20:12:39.363+03:00","CreatorUserId":null,"Id":1,"Phones":[ { "PersonId":1,"Type":"Mobile","Number":"4242424242","CreationTime":"2015-11-07T20:12:39.363+03:00","CreatorUserId":null,"Id":1 },{ "PersonId":1,"Type":"Mobile","Number":"2424242424","CreationTime":"2015-11-07T20:12:39.363+03:00","CreatorUserId":null,"Id":2 } ] }
④更高级的查询包括过滤,排序和获取前2个结果
请求:GET http://localhost:61842/odata/Persons?$filter=Name eq 'Douglas Adams'&$orderby=CreationTime&$top=2
响应:
{ "@odata.context":"http://localhost:61842/odata/$metadata#Persons","value":[ { "Name":"Douglas Adams","IsDeleted":false,"DeleterUserId":null,"DeletionTime":null,"LastModificationTime":null,"LastModifierUserId":null,"CreationTime":"2015-11-07T20:12:39.363+03:00","CreatorUserId":null,"Id":1 },{ "Name":"Douglas Adams","IsDeleted":false,"DeleterUserId":null,"DeletionTime":null,"LastModificationTime":null,"LastModifierUserId":null,"CreationTime":"2016-01-12T20:29:03+02:00","CreatorUserId":null,"Id":3 } ] }
⑤创建新实体
请求(这里,“Content-Type”头是“application / json”):
POST http://localhost:61842/odata/Persons { Name: "Galileo Galilei" }
响应:
{ "@odata.context": "http://localhost:61842/odata/$metadata#Persons/$entity", "Name": "Galileo Galilei", "IsDeleted": false, "DeleterUserId": null, "DeletionTime": null, "LastModificationTime": null, "LastModifierUserId": null, "CreationTime": "2016-01-12T20:36:04.1628263+02:00", "CreatorUserId": null, "Id": 4 }
⑥获取元数据(元数据用于调查服务)
请求:GET http://localhost:61842/odata/$metadata
响应:
<?xml version="1.0" encoding="utf-8"?> <edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx"> <edmx:DataServices> <Schema Namespace="AbpODataDemo.People" xmlns="http://docs.oasis-open.org/odata/ns/edm"> <EntityType Name="Person"> <Key> <PropertyRef Name="Id" /> </Key> <Property Name="Name" Type="Edm.String" Nullable="false" /> <Property Name="IsDeleted" Type="Edm.Boolean" Nullable="false" /> <Property Name="DeleterUserId" Type="Edm.Int64" /> <Property Name="DeletionTime" Type="Edm.DateTimeOffset" /> <Property Name="LastModificationTime" Type="Edm.DateTimeOffset" /> <Property Name="LastModifierUserId" Type="Edm.Int64" /> <Property Name="CreationTime" Type="Edm.DateTimeOffset" Nullable="false" /> <Property Name="CreatorUserId" Type="Edm.Int64" /> <Property Name="Id" Type="Edm.Int32" Nullable="false" /> <NavigationProperty Name="Phones" Type="Collection(AbpODataDemo.People.Phone)" /> </EntityType> <EntityType Name="Phone"> <Key> <PropertyRef Name="Id" /> </Key> <Property Name="PersonId" Type="Edm.Int32" /> <Property Name="Type" Type="AbpODataDemo.People.PhoneType" Nullable="false" /> <Property Name="Number" Type="Edm.String" Nullable="false" /> <Property Name="CreationTime" Type="Edm.DateTimeOffset" Nullable="false" /> <Property Name="CreatorUserId" Type="Edm.Int64" /> <Property Name="Id" Type="Edm.Int32" Nullable="false" /> <NavigationProperty Name="Person" Type="AbpODataDemo.People.Person"> <ReferentialConstraint Property="PersonId" ReferencedProperty="Id" /> </NavigationProperty> </EntityType> <EnumType Name="PhoneType"> <Member Name="Unknown" Value="0" /> <Member Name="Mobile" Value="1" /> <Member Name="Home" Value="2" /> <Member Name="Office" Value="3" /> </EnumType> </Schema> <Schema Namespace="Default" xmlns="http://docs.oasis-open.org/odata/ns/edm"> <EntityContainer Name="Container"> <EntitySet Name="Persons" EntityType="AbpODataDemo.People.Person" /> </EntityContainer> </Schema> </edmx:DataServices> </edmx:Edmx>
....使用启用Swagger的API,您可以获得交互式文档,客户端SDK生成和可发现性。
1,ASP.NET Core
①安装Nuget软件包
将Swashbuckle.AspNetCore nuget软件包安装到您的Web项目中
②配置
将Swagger的配置代码添加到Startup.cs的ConfigureServices方法中
public IServiceProvider ConfigureServices(IServiceCollection services) { //your other code... services.AddSwaggerGen(options => { options.SwaggerDoc("v1", new Info { Title = "AbpZeroTemplate API", Version = "v1" }); options.DocInclusionPredicate((docName, description) => true); }); //your other code... }
然后,将以下代码添加到Startup.cs的Configure方法中以使用Swagger
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { //your other code... app.UseSwagger(); //Enable middleware to serve swagger - ui assets(HTML, JS, CSS etc.) app.UseSwaggerUI(options => { options.SwaggerEndpoint("/swagger/v1/swagger.json", "AbpZeroTemplate API V1"); }); //URL: /swagger //your other code... }
就这样。 您可以浏览“/swagger”下的swagger ui。
2,ASP.NET 5.x
注意,swagger只能在webapi层使用。我在web层使用失败
①安装Nuget软件包
将Swashbuckle.Core nuget软件包安装到WebApi项目(或Web项目)。
②配置
public class SwaggerIntegrationDemoWebApiModule : AbpModule { public override void Initialize() { //your other code... ConfigureSwaggerUi(); } private void ConfigureSwaggerUi() { Configuration.Modules.AbpWebApi().HttpConfiguration .EnableSwagger(c => { c.SingleApiVersion("v1", "SwaggerIntegrationDemo.WebApi"); c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First()); }) .EnableSwaggerUi(c => { c.InjectJavaScript(Assembly.GetAssembly(typeof(AbpProjectNameWebApiModule)), "AbpCompanyName.AbpProjectName.Api.Scripts.Swagger-Custom.js"); }); } }
注意,在配置swagger ui时,我们注入一个名为“Swagger-Custom.js”的javascript文件。 此脚本文件用于在swagger ui上测试api服务时向请求中添加CSRF令牌。 您还需要将此文件作为嵌入式资源添加到WebApi项目中,并在注入时使用InjectJavaScript方法中的逻辑名称。
Swagger-Custom.js的内容在这里:
var getCookieValue = function(key) { var equalities = document.cookie.split('; '); for (var i = 0; i < equalities.length; i++) { if (!equalities[i]) { continue; } var splitted = equalities[i].split('='); if (splitted.length !== 2) { continue; } if (decodeURIComponent(splitted[0]) === key) { return decodeURIComponent(splitted[1] || ''); } } return null; }; var csrfCookie = getCookieValue("XSRF-TOKEN"); var csrfCookieAuth = new SwaggerClient.ApiKeyAuthorization("X-XSRF-TOKEN", csrfCookie, "header"); swaggerUi.api.clientAuthorizations.add("X-XSRF-TOKEN", csrfCookieAuth);
3,测试
1,ASP.NET MVC Controllers
①,mvc控制器继承AbpController
public class HomeController : AbpController { public HomeController() { //您应该设置LocalizationSourceName使L方法工作。 您可以将其设置在您自己的基本控制器类中,以便不对每个控制器重复。 LocalizationSourceName = "MySourceName"; } public ActionResult Index() { var helloWorldText = L("HelloWorld"); return View(); } }
②,授权
您可以为控制器或操作使用AbpMvcAuthorize属性,以防止未经授权的用户使用您的控制器和操作。 例:
public class HomeController : AbpController { [AbpMvcAuthorize("MyPermissionName")] public ActionResult Index() { return View(); } }
2,ASP.NET MVC Views
①AbpWebViewPage基类
ASP.NET Boilerplate还提供了AbpWebViewPage,它定义了一些有用的属性和方法。 如果您使用启动模板创建了项目,则所有视图都将自动从该基类继承。
②AbpWebViewPage定义L方法进行本地化,IsGranted方法授权,IsFeatureEnabled和GetFeatureValue方法进行功能管理等。
3,处理异常
①启用错误处理
要启用ASP.NET MVC控制器的错误处理,必须为ASP.NET MVC应用程序启用customErrors模式。
<customErrors mode="On" />//如果您不想在本地计算机中处理错误,它也可以是“RemoteOnly”。 请注意,这仅适用于ASP.NET MVC控制器,不需要Web API控制器。
②非AJAX请求
如果请求不是AJAX,则会显示错误页面。
public ActionResult Index() { throw new Exception("A sample exception message..."); }
当然,这个异常可以被另一个从这个动作调用的方法抛出。 ASP.NET Boilerplate处理这个异常,记录它并显示'Error.cshtml'视图。 您可以自定义此视图以显示错误。 示例错误视图(ASP.NET Boilerplate模板中的默认错误视图):
UserFriendlyException
UserFriendlyException是一种特殊类型的异常,直接显示给用户。 见下面的示例:
public ActionResult Index() { throw new UserFriendlyException("Ooppps! There is a problem!", "You are trying to see a product that is deleted..."); }
错误模型
ASP.NET Boilerplate将ErrorViewModel对象作为模型传递给“错误”视图:
public class ErrorViewModel
{
public AbpErrorInfo ErrorInfo { get; set; }
public Exception Exception { get; set; }
}
ErrorInfo包含有关可以向用户显示的错误的详细信息。 异常对象是抛出的异常。 如果需要,您可以查看并显示其他信息。 例如,如果是AbpValidationException,我们可以显示验证错误:
③AJAX请求
如果MVC操作的返回类型是JsonResult(或异步操作的Task JsonResult),则ASP.NET Boilerplate会向客户端返回异常的JSON对象。 示例返回对象的错误:
{ "targetUrl": null, "result": null, "success": false, "error": { "message": "An internal error occured during your request!", "details": "..." }, "unAuthorizedRequest": false }
④异常事件
当ASP.NET Boilerplare处理任何异常时,它会触发可以注册的AbpHandledExceptionData事件(有关事件总线的更多信息,请参见eventbus文档)。 例:
public class MyExceptionHandler : IEventHandler<AbpHandledExceptionData>, ITransientDependency { public void HandleEvent(AbpHandledExceptionData eventData) { //TODO: Check eventData.Exception! } }
如果将此示例类放入应用程序(通常在您的Web项目中),将为ASP.NET Boilerplate处理的所有异常调用HandleEvent方法。 因此,您可以详细调查Exception对象。
1,应用语言,首先申明支持哪些语, 这在模块的PreInitialize方法中完成
Configuration.Localization.Languages.Add(new LanguageInfo("en", "English", "famfamfam-flag-england", true)); Configuration.Localization.Languages.Add(new LanguageInfo("tr", "Türkçe", "famfamfam-flag-tr"));
在服务器端,您可以注入并使用ILocalizationManager。 在客户端,您可以使用abp.localization javascript API来获取所有可用语言和当前语言的列表。 famfamfam-flag-england(和tr)只是一个CSS类,您可以根据需要进行更改。 然后你可以在UI上使用它来显示相关的标志。
2,本地化来源
本地化文本可以存储在不同的来源。 即使您可以在同一应用程序中使用多个源(如果您有多个模块,每个模块可以定义一个独立的本地化源,或者一个模块可以定义多个源)。 ILocalizationSource接口应由本地化源实现。 然后它被注册到ASP.NET Boilerplate的本地化配置。
每个本地化源必须具有唯一的源名称。 有如下定义的预定义的本地化源类型(xml文件、json文件、资源文件等)。
①XML文件
本地化文本可以存储在XML文件中。 XML文件的内容是这样的:
<?xml version="1.0" encoding="utf-8" ?> <localizationDictionary culture="en"> <texts> <text name="TaskSystem" value="Task System" /> <text name="TaskList" value="Task List" /> <text name="NewTask" value="New Task" /> <text name="Xtasks" value="{0} tasks" /> <text name="CompletedTasks" value="Completed tasks" /> <text name="EmailWelcomeMessage">Hi, Welcome to Simple Task System! This is a sample email content.</text> </texts> </localizationDictionary>
XML文件必须是unicode(utf-8)。 culture =“en”表示此XML文件包含英文文本。 对于文本节点; name属性用于标识文本。 您可以使用值属性或内部文本(如最后一个)来设置本地化文本的值。 我们为每种语言创建一个分离的XML文件,如下所示:
SimpleTaskSystem是这里的源名称,SimpleTaskSystem.xml定义了默认语言。 当请求文本时,ASP.NET Boilerplate从当前语言的XML文件获取文本(使用Thread.CurrentThread.CurrentUICulture查找当前语言)。 如果当前语言不存在,则会从默认语言的XML文件获取文本。
注册XML本地化源,XML文件可以存储在文件系统中,也可以嵌入到程序集中。
对于文件系统存储的XML,我们可以注册一个XML定位源,如下所示(这在模块的PreInitialize事件中完成):
Configuration.Localization.Sources.Add( new DictionaryBasedLocalizationSource( "SimpleTaskSystem", new XmlFileLocalizationDictionaryProvider( HttpContext.Current.Server.MapPath("~/Localization/SimpleTaskSystem") ) ) );
对于嵌入式XML文件,我们应该将所有本地化XML文件标记为嵌入式资源(选择XML文件,打开属性窗口(F4)并将Build Action作为嵌入式资源更改)。 然后我们可以注册本地化源,如下所示:
Configuration.Localization.Sources.Add( new DictionaryBasedLocalizationSource( "SimpleTaskSystem", new XmlEmbeddedFileLocalizationDictionaryProvider( Assembly.GetExecutingAssembly(), "MyCompany.MyProject.Localization.Sources" ) ) );
XmlEmbeddedFileLocalizationDictionaryProvider获取一个包含XML文件的程序集(GetExecutingAssembly简称为当前程序集)和XML文件的命名空间(命名空间+ XML文件的文件夹层次结构)。
注意:当添加语言后缀到嵌入式XML文件时,不要使用像MySource.tr.xml这样的点符号,而是使用像MySource-tr.xml这样的破折号,因为点符号在找到资源时会导致命名空间的问题。
②json文件
{ "culture": "en", "texts": { "TaskSystem": "Task system", "Xtasks": "{0} tasks" } }
JSON文件应该是unicode(utf-8)。 文化:“en”声明此JSON文件包含英文文本。 我们为每种语言创建一个分隔的JSON文件,如下所示:
MySourceName是源名,MySourceName.json定义了默认语言。 它类似于XML文件。
注册JSON本地化源,JSON文件可以存储在文件系统中,也可以嵌入到程序集中。
文件系统存储的JSONs,我们可以注册一个JSON本地化源码,如下所示(这在模块的PreInitialize事件中完成):
Configuration.Localization.Sources.Add( new DictionaryBasedLocalizationSource( "MySourceName", new JsonFileLocalizationDictionaryProvider( HttpContext.Current.Server.MapPath("~/Localization/MySourceName") ) ) );
对于嵌入式JSON文件,我们应将所有本地化JSON文件标记为嵌入式资源(选择JSON文件,打开属性窗口(F4),并将Build Action作为嵌入式资源更改)。 然后我们可以注册本地化源,如下所示:
Configuration.Localization.Sources.Add( new DictionaryBasedLocalizationSource( "MySourceName", new JsonEmbeddedFileLocalizationDictionaryProvider( Assembly.GetExecutingAssembly(), "MyCompany.MyProject.Localization.Sources" ) ) );
JsonEmbeddedFileLocalizationDictionaryProvider获取一个包含JSON文件的程序集(GetExecutingAssembly简称为当前程序集)和JSON文件的命名空间(命名空间是计算的程序集名称和JSON文件的文件夹层次结构)。
注意:在嵌入式JSON文件中添加语言后缀时,请勿使用像MySource.tr.json这样的点符号,而是使用像MySource-tr.json这样的破折号,因为点符号在找到资源时会导致命名空间问题。
③资源文件
本地化文本也可以存储在.NET的资源文件中。 我们可以为每种语言创建资源文件,如下所示(右键单击项目,选择添加新项目,然后查找资源文件)。
MyTexts.resx包含默认语言文本,MyTexts.tr.resx包含土耳其语的文本。 当我们打开MyTexts.resx时,我们可以看到所有的文本:
在这种情况下,ASP.NET Boilerplate使用.NET的内置资源管理器进行本地化。 您应该为资源配置本地化源:
Configuration.Localization.Sources.Add( new ResourceFileLocalizationSource( "MySource", MyTexts.ResourceManager ));
源的唯一名称是MySource,而MyTexts.ResourceManager是用于获取本地化文本的资源管理器的引用。 这在模块的PreInitialize事件中完成(有关更多信息,请参阅模块系统)。
3,获取本地化文本
①在服务器端,我们可以注入ILocalizationManager并使用它的GetString方法。
var s1 = _localizationManager.GetString("SimpleTaskSystem", "NewTask");
GetString方法根据当前线程的UI文化从本地化源获取字符串。 如果没有找到,它将回退到默认语言。
②为了不重复源的名字,你可以先拿到源,然后得到从源字符串:
var source = _localizationManager.GetSource("SimpleTaskSystem"); var s1 = source.GetString("NewTask");
③在mvc控制器
public class HomeController : SimpleTaskSystemControllerBase { public ActionResult Index() { var helloWorldText = L("HelloWorld"); return View(); } }
public abstract class SimpleTaskSystemControllerBase : AbpController { protected SimpleTaskSystemControllerBase() { LocalizationSourceName = "SimpleTaskSystem"; } }
L方法用于本地化字符串。 当然,您必须提供源名称。 它在SimpleTaskSystemControllerBase中完成。注意它是从AbpController派生的。 因此,您可以使用L方法轻松本地化文本。
④在MVC视图,同样的L方法也存在于视图中:
<div> <form id="NewTaskForm" role="form"> <div class="form-group"> <label for="TaskDescription">@L("TaskDescription")</label> <textarea id="TaskDescription" data-bind="value: task.description" class="form-control" rows="3" placeholder="@L("EnterDescriptionHere")" required></textarea> </div> <div class="form-group"> <label for="TaskAssignedPerson">@L("AssignTo")</label> <select id="TaskAssignedPerson" data-bind="options: people, optionsText: 'name', optionsValue: 'id', value: task.assignedPersonId, optionsCaption: '@L("SelectPerson")'" class="form-control"></select> </div> <button data-bind="click: saveTask" type="submit" class="btn btn-primary">@L("CreateTheTask")</button> </form> </div>
为了使这项工作,您应该从设置源名称的基类派生您的视图:
public abstract class SimpleTaskSystemWebViewPageBase : SimpleTaskSystemWebViewPageBase<dynamic> { } public abstract class SimpleTaskSystemWebViewPageBase<TModel> : AbpWebViewPage<TModel> { protected SimpleTaskSystemWebViewPageBase() { LocalizationSourceName = "SimpleTaskSystem"; } }
并在web.config中设置此视图基类:
<pages pageBaseType="SimpleTaskSystem.Web.Views.SimpleTaskSystemWebViewPageBase">
⑤在Javascript中
ASP.NET Boilerplate也可以使用同样的本地化文本也是JavaScript代码。 首先,您应该在页面中添加动态ABP脚本:
<script src="/AbpScripts/GetScripts" type="text/javascript"></script>
ASP.NET Boilerplate自动生成需要的JavaScript代码,以便在客户端获取本地化的文本。 然后,您可以轻松地获取javascript中的本地化文本,如下所示:
var s1 = abp.localization.localize('NewTask', 'SimpleTaskSystem');
NewTask是文本名,SimpleTaskSystem是源名。 不要重复源名称,您可以先获取源代码,然后获取文本:
var source = abp.localization.getSource('SimpleTaskSystem'); var s1 = source('NewTask');
格式参数,本地化方法也可以获得附加的格式参数。 例:
abp.localization.localize('RoleDeleteWarningMessage', 'MySource', 'Admin'); //如果使用getSource得到源,则如上所示 source('RoleDeleteWarningMessage', 'Admin');
如果RoleDeleteWarningMessage ='Role {0}将被删除',那么本地化的文本将被“Role Admin将被删除”。
默认本地化源,您可以设置默认的本地化源,并使用没有源名称的abp.localization.localize方法。
abp.localization.defaultSourceName = 'SimpleTaskSystem'; var s1 = abp.localization.localize('NewTask');
defaultSourceName是全局的,一次只能运行一个源。
4,扩展本地化源
ASP.NET Boilerplate还定义了一些本地化源。 例如,Abp.Web nuget软件包将一个名为“AbpWeb”的本地化源定义为嵌入式XML文件:
默认(英文)XML文件如下(仅显示前两个文本):
<?xml version="1.0" encoding="utf-8" ?> <localizationDictionary culture="en"> <texts> <text name="InternalServerError" value="An internal error occurred during your request!" /> <text name="ValidationError" value="Your request is not valid!" /> ... </texts> </localizationDictionary>
为了扩展AbpWeb源,我们可以定义XML文件。 假设我们只想改变InternalServerError文本。 我们可以定义一个XML文件,如下所示:
<?xml version="1.0" encoding="utf-8" ?> <localizationDictionary culture="en"> <texts> <text name="InternalServerError" value="Sorry :( It seems there is a problem. Let us to solve it and please try again later." /> </texts> </localizationDictionary>
然后我们可以在我们的模块的PreInitialize方法上注册它:
Configuration.Localization.Sources.Extensions.Add( new LocalizationSourceExtensionInfo("AbpWeb", new XmlFileLocalizationDictionaryProvider( HttpContext.Current.Server.MapPath("~/Localization/AbpWebExtensions") ) ) );
5,获取语言
ILanguageManager可用于获取所有可用语言和当前语言的列表。
1,创建菜单
应用程序可能由不同的模块组成,每个模块都可以拥有自己的菜单项。 要定义菜单项,我们需要创建一个派生自NavigationProvider的类。
假设我们有一个主菜单,如下所示:
这里,管理菜单项有两个子菜单项。 示例导航提供程序类创建这样的菜单可以如下所示:
public class SimpleTaskSystemNavigationProvider : NavigationProvider { public override void SetNavigation(INavigationProviderContext context) { context.Manager.MainMenu .AddItem( new MenuItemDefinition( "Tasks", new LocalizableString("Tasks", "SimpleTaskSystem"), url: "/Tasks", icon: "fa fa-tasks" ) ).AddItem( new MenuItemDefinition( "Reports", new LocalizableString("Reports", "SimpleTaskSystem"), url: "/Reports", icon: "fa fa-bar-chart" ) ).AddItem( new MenuItemDefinition( "Administration", new LocalizableString("Administration", "SimpleTaskSystem"), icon: "fa fa-cogs" ).AddItem( new MenuItemDefinition( "UserManagement", new LocalizableString("UserManagement", "SimpleTaskSystem"), url: "/Administration/Users", icon: "fa fa-users", requiredPermissionName: "SimpleTaskSystem.Permissions.UserManagement" ) ).AddItem( new MenuItemDefinition( "RoleManagement", new LocalizableString("RoleManagement", "SimpleTaskSystem"), url: "/Administration/Roles", icon: "fa fa-star", requiredPermissionName: "SimpleTaskSystem.Permissions.RoleManagement" ) ) ); } }
MenuItemDefinition基本上可以有唯一的名称,可本地化的显示名称,url和图标。 也,
2,注册导航
创建导航提供程序后,我们应该在我们的模块的PreInitialize事件上注册到ASP.NET Boilerplate配置:
Configuration.Navigation.Providers.Add<SimpleTaskSystemNavigationProvider>();
3,显示菜单
IUserNavigationManager可以被注入并用于获取菜单项并显示给用户。 因此,我们可以在服务器端创建菜单。
ASP.NET Boilerplate自动生成一个JavaScript API来获取客户端的菜单和项目。 abp.nav命名空间下的方法和对象可以用于此目的。 例如,可以使用abp.nav.menus.MainMenu来获取应用程序的主菜单。 因此,我们可以在客户端创建菜单。
在一个web应用中,有供客户端使用的javascript,css,xml等文件。它们一般是作为分离的文件被添加到web项目中并发布。有时,我们需要将这些文件打包到一个程序集(类库项目,一个dll文件)中,作为内嵌资源散布到程序集中。ABP提供了一个基础设施使得这个很容易实现。
1,创建嵌入式文件
我们首先应该创建一个资源文件并把它标记为内嵌的资源。任何程序集都可以包含内嵌的资源文件。假设我们有一个叫做“Abp.Zero.Web.UI.Metronic.dll”程序集,而且它包含了javascript,css,和图片文件:
①xproj / project.json格式
假设我们有一个名为EmbeddedPlugIn的项目,如下所示:
我们想要使这些文件在一个web应用中可用,首先,我们应该将想要暴露的文件标记为内嵌的资源。在这里,我选择了 metronic.js文件,右键打开属性面板(快捷键是F4)。
选中你想在web应用中使用的所有文件,将生成操作(build action)的属性值选为内嵌的 资源。
2,暴露内嵌文件
ABP使得暴露这些内嵌资源很容易,只需要一行代码:
WebResourceHelper.ExposeEmbeddedResources("AbpZero/Metronic", Assembly.GetExecutingAssembly(), "Abp.Zero.Web.UI.Metronic");
这行代码一般放在模块的Initialize方法中,下面解释一下这些参数:
- 第一个参数为这些文件定义了根文件夹,它匹配了根命名空间。
- 第二个参数定义了包含这些文件的程序集。本例中,我传入了包含这行代码的程序集。但你也可以传入任何包含内嵌资源的程序集。
- 最后一个参数定义了这些文件在程序集的根命名空间。它是“默认的命名空间”加上“文件夹名”。默认的命名空间一般和程序集的名字是相同的,但是在程序集的属性中进行更改。这里 ,默认的命名空间是Abp(已经更改了),因此Metronic文件夹的命名空间是“Abp.Zero.Web.UI.Metronic”。
3,使用内嵌文件
可以直接使用内嵌的资源:
<script type="text/javascript" src="~/AbpZero/Metronic/assets/global/scripts/metronic.js"></script>
ABP知道这是一个内嵌的资源,因而可以从之前暴露的dll中获得文件。此外,还可以在razor视图中使用HtmlHelper的扩展方法IncludeScript:
@Html.IncludeScript("~/AbpZero/Metronic/assets/global/scripts/metronic.js")
这会产生下面的代码:
<script src="/AbpZero/Metronic/assets/global/scripts/metronic.js?v=635438748506909100" type="text/javascript"></script>
唯一的不同就是参数v=635438748506909100。这样做的好处是阻止了浏览器端脚本的失败缓存。该值只有当你的dll重新生成(实际上是文件的最后写入时间)的时候才会改变,而且如果该值改变了,浏览器就不会缓存这个文件了。因此,建议使用IncludeScript方式。这对于非嵌入的物理资源也是有效的。对应于css文件,也存在相应的IncludeStyle方法。
“跨站点请求伪造(CSRF)”是一种当恶意网站,电子邮件,博客,即时消息或程序导致用户的Web浏览器对用户所在的受信任站点执行不必要的操作时发生的攻击类型 目前认证的“(OWASP)。
1,ASP.NET MVC
①ABP做了下面这些事情来客服上面的困难:
- actions会被自动保护(通过AbpAntiForgeryMvcFilter)。自动保护可以应对大多数情况。当然,可以使用DisableAbpAntiForgeryTokenValidation特性为任何action和Controller关闭自动保护,也可以使用 ValidateAbpAntiForgeryToken特性打开。
- 除了HTML的表单域,AbpAntiForgeryMvcFilter也会检查请求头中的token。因此,可以很容易对ajax请求使用反伪造token保护。
- 在js中可以使用abp.security.antiForgery.getToken()函数获得token。
- 为所有的ajax请求头部自动添加反伪造token。
②在Layout视图中添加以下代码:
@{
SetAntiForgeryCookie();
}
这样,所有使用了这个布局页的页面都会包含这句代码了,该方法定义在ABP视图基类中,它会创建和设置正确的token cookie,使得在js端可以工作。如果有多个Layout的话,需要为每个布局添加上面的代码。
对于ASP.NET MVC 应用,只需要做这么多,所有的ajax请求都会自动工作。但是对于HTML 表单仍然需要使用** @Html.AntiForgeryToken()** HTML帮助方法,因为表单不是通过Ajax提交的,但是不需要在相应的action上使用ValidateAbpAntiForgeryToken 特性了。
③配置
XSRF默认是打开的,也可以在模块的PreInitialize方法中关闭或配置,如下:
Configuration.Modules.AbpWeb().AntiForgery.IsEnabled = false;
也可以使用Configuration.Modules.AbpWebCommon().AntiForgery对象配置token和cookie名称。
2,ASP.NET WEB API
ASP.NET Web API不包括反伪造机制,ABP为ASP.NET Web API Controllers提供了基础设施来添加CSRF保护,并且是完全自动化的。
①ASP.NET MVC客户端
如果在MVC项目中使用了Web API,那么不需要额外的配置。只要Ajax请求是从一个配置的MVC应用中发出的,即使你的Web API层自宿主在其它进程中,也不需要配置。
②其他客户端
如果你的客户端是其它类型的应用(比如,一个独立的angularjs应用,它不能像之前描述的那样使用SetAntiForgeryCookie()方法),那么你应该提供一种设置反伪造token cookie的方法。一种可能的实现方式是像下面那样创建一个api控制器:
using System.Net.Http; using Abp.Web.Security.AntiForgery; using Abp.WebApi.Controllers; namespace AngularForgeryDemo.Controllers { public class AntiForgeryController : AbpApiController { private readonly IAbpAntiForgeryManager _antiForgeryManager; public AntiForgeryController(IAbpAntiForgeryManager antiForgeryManager) { _antiForgeryManager = antiForgeryManager; } public HttpResponseMessage GetTokenCookie() { var response = new HttpResponseMessage(); _antiForgeryManager.SetCookie(response.Headers); return response; } } }
然后就可以从客户端调用这个action来设置cookie了。
3,客户端类
①jQuery
abp.jquery.js中定义了一个ajax拦截器,它可以将反伪造请求token添加到每个请求的请求头中,它会从abp.security.antiForgery.getToken()函数中获得token。
②Angularjs
Angularjs会将反伪造token自动添加到所有的ajax请求中,请点击链接查看Angularjs的XSRF保护一节。ABP默认使用了相同的cookie和header名称。因此,Angularjs集成是现成可用的。
1,后台工作
后台作业用于将某些任务排列在后台以排队和持久的方式执行。 您可能需要后台工作,原因有几个。 一些例子:
- 执行长时间运行的任务。比如,一个用户按了“report”按钮来启动一个长时间运行的报告工作,点击了这个按钮你不可能让用户一直处于等待状态,所以你应该将这个工作(job)添加到 队列(queue)中,然后,当这项工作完成时,通过邮件将报告结果发送给该用户。
- 创建重复尝试(re-trying)和持续的任务来保证代码将会 成功执行。比如,你可以在后台工作中发送邮件以克服 临时失败,并 保证邮件最终能够发送出去。因此,当发送邮件的时候用户不需要等待。
①创建后台工作
我们可以通过继承BackgroundJob <TArgs>类或直接实现IBackgroundJob <TArgs>接口来创建一个后台作业类。这是最简单的后台工作:
public class TestJob : BackgroundJob<int>, ITransientDependency { public override void Execute(int number) { Logger.Debug(number.ToString()); } }
后台作业定义了一个Execute方法获取一个输入参数。 参数类型定义为通用类参数,如示例所示。
后台作业必须注册到依赖注入。 实现ITransientDependency是最简单的方法。
我们来定义一个更现实的工作,在后台队列中发送电子邮件:
public class SimpleSendEmailJob : BackgroundJob<SimpleSendEmailJobArgs>, ITransientDependency { private readonly IRepository<User, long> _userRepository; private readonly IEmailSender _emailSender; public SimpleSendEmailJob(IRepository<User, long> userRepository, IEmailSender emailSender) { _userRepository = userRepository; _emailSender = emailSender; } [UnitOfWork] public override void Execute(SimpleSendEmailJobArgs args) { var senderUser = _userRepository.Get(args.SenderUserId); var targetUser = _userRepository.Get(args.TargetUserId); _emailSender.Send(senderUser.EmailAddress, targetUser.EmailAddress, args.Subject, args.Body); } }
我们注入了user仓储(为了获得用户信息)和email发送者(发送邮件的服务),然后简单地发送了该邮件。SimpleSendEmailJobArgs是该工作的参数,它定义如下:
[Serializable] public class SimpleSendEmailJobArgs { public long SenderUserId { get; set; } public long TargetUserId { get; set; } public string Subject { get; set; } public string Body { get; set; } }
工作参数应该是serializable(可序列化),因为要将它 序列化并存储到数据库中。虽然ABP默认的后台工作管理者使用了JSON序列化(它不需要[Serializable]特性),但是最好定义 [Serializable]特性,因为我们将来可能会转换到其他使用二进制序列化的工作管理者。
记住,要保持你的参数简单,不要在参数中包含实体或者其他非可序列化的对象。正如上面的例子演示的那样,我们只存储了实体的 Id,然后在该工作的内部从仓储中获得该实体。
②添加新工作到队列
定义后台作业后,我们可以注入并使用IBackgroundJobManager将作业添加到队列中。 参见上面定义的TestJob的示例:
[AbpAuthorize] public class MyEmailAppService : ApplicationService, IMyEmailAppService { private readonly IBackgroundJobManager _backgroundJobManager; public MyEmailAppService(IBackgroundJobManager backgroundJobManager) { _backgroundJobManager = backgroundJobManager; } public async Task SendEmail(SendEmailInput input) { await _backgroundJobManager.EnqueueAsync<SimpleSendEmailJob, SimpleSendEmailJobArgs>( new SimpleSendEmailJobArgs { Subject = input.Subject, Body = input.Body, SenderUserId = AbpSession.GetUserId(), TargetUserId = input.TargetUserId }); } }
Enqueu (或 EnqueueAsync)方法还有其他的参数,比如 priority和 delay(优先级和延迟)。
③IBackgroundJobManager默认实现
IBackgroundJobManager默认是由BackgroundJobManager实现的。它可以被其他的后台工作提供者替代(看后面的Hangfire集成)。关于默认的BackgroundJobManager一些信息如下:
- 它是一个在单线程中以FIFO(First In First Out)工作的简单工作队列,使用 IBackgroundJobStore来持久化工作。
- 它会重复尝试执行工作,直到工作成功执行(不会抛出任何异常)或者超时。默认的超时是一个工作2天。
- 当成功执行后,它会从存储(数据库)中删除该工作。如果超时了,就会将该工作设置为 abandoned(废弃的),并保留在数据库中。
- 在重复尝试一个工作之间会增加等待时间。第一次重试时等待1分钟,第二次等待2分钟,第三次等待4分钟等等。
- 在固定的时间间隔轮询工作的存储。查询工作时先按优先级排序,再按尝试次数排序。
后台工作存储
默认的BackgroundJobManager需要一个数据存储来保存、获得工作。如果你没有实现IBackgroundJobStore,那么它会使用 InMemoryBackgroundJobStore,它不会将工作持久化到数据库中。你可以简单地实现它来存储工作到数据库或者你可以使用module-zero,它已经实现了IBackgroundJobStore。
如果你正在使用第三方的工作管理者(像Hangfire),那么不需要实现IBackgroundJobStore。
④配置
您可以在模块的PreInitialize方法中使用Configuration.BackgroundJobs来配置后台作业系统。
关闭工作执行功能
你可能想关闭应用程序的后台工作执行:
public class MyProjectWebModule : AbpModule { public override void PreInitialize() { Configuration.BackgroundJobs.IsJobExecutionEnabled = false; } //... }
2,后台工作者
后台工作者不同于后台工作。它们是运行在应用后台的简单独立线程。一般来说,它们会定期地执行一些任务。比如:
- 后台工作者可以定期运行来删除旧的日志。
- 后台工作者可以定期运行来确定不活跃的用户,并给他们发送邮件以使他们返回你的应用。
①创建后台工作者
要创建后台工作者,我们应该实现IBackgroundWorker接口。除此之外,我们可以基于需求从 BackgroundWorkerBase或者 PeriodicBackgroundWorkerBase继承。
假设一个用户在过去30天内没有登录到该应用,那我们想要让Ta的状态为passive。看下面的代码:
public class MakeInactiveUsersPassiveWorker : PeriodicBackgroundWorkerBase, ISingletonDependency { private readonly IRepository<User, long> _userRepository; public MakeInactiveUsersPassiveWorker(AbpTimer timer, IRepository<User, long> userRepository) : base(timer) { _userRepository = userRepository; Timer.Period = 5000; //5 seconds (good for tests, but normally will be more) } [UnitOfWork] protected override void DoWork() { using (CurrentUnitOfWork.DisableFilter(AbpDataFilters.MayHaveTenant)) { var oneMonthAgo = Clock.Now.Subtract(TimeSpan.FromDays(30)); var inactiveUsers = _userRepository.GetAllList(u => u.IsActive && ((u.LastLoginTime < oneMonthAgo && u.LastLoginTime != null) || (u.CreationTime < oneMonthAgo && u.LastLoginTime == null)) ); foreach (var inactiveUser in inactiveUsers) { inactiveUser.IsActive = false; Logger.Info(inactiveUser + " made passive since he/she did not login in last 30 days."); } CurrentUnitOfWork.SaveChanges(); } } }
这是现实中的代码,并且在具有module-zero模块的ABP中直接有效。
- 如果你从PeriodicBackgroundWorkerBase 类继承(如这个例子),那么你应该实现 DoWork方法来执行定期运行的代码。
- 如果从BackgroundWorkerBase继承或直接实现了 IBackgroundWorker,那么你要重写或者实现Start, Stop 和 WaitToStop方法。Start和Stop方法应该是 非阻塞的(non-blocking),WaitToStop方法应该等待工作者完成当前重要的工作。
②注册后台工作者
创建一个后台工作者后,我们应该把它添加到IBackgroundWorkerManager,通常放在模块的PostInitialize方法中:
public class MyProjectWebModule : AbpModule { //... public override void PostInitialize() { var workManager = IocManager.Resolve<IBackgroundWorkerManager>(); workManager.Add(IocManager.Resolve<MakeInactiveUsersPassiveWorker>()); } }
虽然一般我们将工作者添加到PostInitialize方法中,但是没有强制要求。你可以在任何地方注入IBackgroundWorkerManager,在运行时添加工作者。
当应用要关闭时,IBackgroundWorkerManager会停止并释放所有注册的工作者。
③后台工作者生命周期
后台工作者一般实现为单例的,但是没有严格限制。如果你需要相同工作者类的多个实例,那么可以使它成为transient(每次使用时创建),然后给IBackgroundWorkerManager添加多个实例。在这种情况下,你的工作者很可能会参数化(比如,你有单个LogCleaner类,但是两个LogCleaner工作者实例会监视并清除不同的log文件夹)。
3,让你的应用程序一直运行
只有当你的应用运行时,后台工作和工作者才会工作。如果一个Asp.Net 应用长时间没有执行请求,那么它默认会关闭(shutdown)。如果你想让后台工作一直在web应用中执行(这是默认行为),那么你要确保web应用配置成了总是运行。否则,后台工作只有在应用使用时才会执行。
有很多技术来实现这个目的。最简单的方法是从外部应用定期向你的web应用发送请求。这样,你可以检查web应用是否开启并且处于运行状态。Hangfire文档讲解了一些其他的方法。
Hangfire是一个综合的后台工作管理者。你可以将Hangfire集成到ABP中,这样就可以不使用默认的后台工作管理者了。但你仍然可以为Hangfire使用相同的后台工作API。这样,你的代码就独立于Hangfire了,但是,如果你喜欢的话,也可以直接使用 Hangfire的API。
1,ASP.NET MVC 5.x 集成
Abp.HangFire nuget包用于ASP.NET MVC 5.x项目:
Install-Package Abp.HangFire
然后,您可以为Hangfire安装任何存储。 最常见的是SQL Server存储(请参阅Hangfire.SqlServer nuget软件包)。 安装这些nuget软件包后,您可以将项目配置为使用Hangfire,如下所示:
[DependsOn(typeof (AbpHangfireModule))] public class MyProjectWebModule : AbpModule { public override void PreInitialize() { Configuration.BackgroundJobs.UseHangfire(configuration => { configuration.GlobalConfiguration.UseSqlServerStorage("Default"); }); } //... }
我们添加了AbpHangfireModule作为依赖,并使用Configuration.BackgroundJobs.UseHangfire方法启用和配置Hangfire(“Default”是web.config中的连接字符串)。
Hangfire在数据库中需要模式创建权限,因为它在第一次运行时创建自己的模式和表。
仪表板授权
Hagfire可以显示仪表板页面,以实时查看所有后台作业的状态。 您可以按照其说明文档中的说明进行配置。 默认情况下,此仪表板页面适用于所有用户,未经授权。 您可以使用Abp.HangFire包中定义的AbpHangfireAuthorizationFilter类将其集成到ABP的授权系统中。 示例配置:
app.UseHangfireDashboard("/hangfire", new DashboardOptions { Authorization = new[] { new AbpHangfireAuthorizationFilter() } });
这将检查当前用户是否已登录到应用程序。 如果你想要一个额外的权限,你可以传入它的构造函数:
app.UseHangfireDashboard("/hangfire", new DashboardOptions { Authorization = new[] { new AbpHangfireAuthorizationFilter("MyHangFireDashboardPermissionName") } });
注意:UseHangfireDashboard应该在您的启动类中的认证中间件之后调用(可能是最后一行)。 否则,授权总是失败。
Quartz是一个全功能的开源作业调度系统,可以从最小的应用程序到大型企业系统使用。 Abp.Quartz包简单地将Quartz集成到ASP.NET Boilerplate。
ASP.NET Boilerplate具有内置的持久性后台作业队列和后台工作系统。 如果您对后台工作人员有高级调度要求,Quartz可以是一个很好的选择。 此外,Hangfire可以成为持久后台作业队列的好选择。
1,安装
将Abp.Quartz nuget软件包安装到您的项目中,并将一个DependsOn属性添加到您的AbpQuartzModule模块中:
[DependsOn(typeof (AbpQuartzModule))] public class YourModule : AbpModule { //... }
要创建新作业,您可以实现Quartz的IJob界面,也可以从具有一些帮助属性/方法的JobBase类(在Abp.Quartz包中定义)派生(用于记录和本地化)。 一个简单的工作类如下所示:
public class MyLogJob : JobBase, ITransientDependency { public override void Execute(IJobExecutionContext context) { Logger.Info("Executed MyLogJob :)"); } }
我们只是执行Execute方法来写一个日志。 您可以看到更多的Quartz的文档。
2,调度工作
IQuartzScheduleJobManager用于调度作业。 您可以将其注入到您的类(或者您可以在您的模块的PostInitialize方法中解析并使用它)来计划作业。 调度作业的示例控制器:
public class HomeController : AbpController { private readonly IQuartzScheduleJobManager _jobManager; public HomeController(IQuartzScheduleJobManager jobManager) { _jobManager = jobManager; } public async Task<ActionResult> ScheduleJob() { await _jobManager.ScheduleAsync<MyLogJob>( job => { job.WithIdentity("MyLogJobIdentity", "MyGroup") .WithDescription("A job to simply write logs."); }, trigger => { trigger.StartNow() .WithSimpleSchedule(schedule => { schedule.RepeatForever() .WithIntervalInSeconds(5) .Build(); }); }); return Content("OK, scheduled!"); } }
1,介绍
通知(Notification)用于告知用户系统中的特定事件。ABP提供了基于实时通知基础设施的发布订阅模型(pub/sub)。
①发送模型
给用户发送通知有两种方式:
- 首先用户订阅特定的通知类型,然后我们发布这种类型的通知,这种类型的通知会传递给所有已经订阅的用户。这就是发布订阅(pub/sub)模型。
- 直接给目标用户发送通知。
②通知类型
通知类型也有两种:
- 一般通知:是任意类型的通知。“如果一个用户给我发送了添加好友的请求就通知我”是这种通知类型的一个例子。
- 实体通知:和特定的实体相关联。“如果一个用户在 这张图片上评论那么通知我”是基于实体的通知,因为它和特定的实体相关联。用户可能想获得某些图片的通知而不是所有的图片通知。
③通知数据
一个通知一般都会包括一个通知数据。比如,“如果一个用户给我发送了添加好友的请求就通知我”,这个通知可能有两个数据属性:发送者用户名(那哪一个用户发送的这个添加好友的请求)和请求内容(该用户在这个请求中写的东西)。很明显,该通知数据类型紧耦合于通知类型。不同的通知类型有不同的数据类型。
- 通知数据是可选的。某些通知可能不需要数据。一些预定义的通知数据类型可能对于大多数情况够用了。 MessageNotificationData可以用于简单的信息, LocalizableMessageNotificationData可以用于本地化的,带参数的通知信息。
④通知安全
通知的安全性有5个级别,它们定义在NotificationSeverity枚举类中,分别是 Info,Success,Warn,Error和Fatal。默认值是Info。
2,订阅通知
INotificationSubscriptionManager提供API来订阅通知。 例子:
public class MyService : ITransientDependency { private readonly INotificationSubscriptionManager _notificationSubscriptionManager; public MyService(INotificationSubscriptionManager notificationSubscriptionManager) { _notificationSubscriptionManager = notificationSubscriptionManager; } //订阅一般通知(发送好友请求) public async Task Subscribe_SentFrendshipRequest(int? tenantId, long userId) { await _notificationSubscriptionManager.SubscribeAsync(new UserIdentifier(tenantId, userId), "SentFrendshipRequest"); } //订阅实体通知(评论图片) public async Task Subscribe_CommentPhoto(int? tenantId, long userId, Guid photoId) { await _notificationSubscriptionManager.SubscribeAsync(new UserIdentifier(tenantId, userId), "CommentPhoto", new EntityIdentifier(typeof(Photo), photoId)); } }
首先,我们注入了INotificationSubscriptionManager。第一个方法订阅了一个一般通知。当某人发送了一个添加好友的请求时,用户想得到通知。第二个方法订阅了一个和特定实体(Photo)相关的通知。如果任何人对这个特定的图片进行了评论,那么用户就会收到通知。
每一个通知应该有唯一的名字(比如例子中的SentFrendshipRequest和 CommentPhoto)。
INotificationSubscriptionManager还有很多方法来管理通知,比如UnsubscribeAsync, IsSubscribedAsync, GetSubscriptionsAsync等方法。
3,发布通知
INotification Publisher用于发布通知。 例子:
public class MyService : ITransientDependency { private readonly INotificationPublisher _notiticationPublisher; public MyService(INotificationPublisher notiticationPublisher) { _notiticationPublisher = notiticationPublisher; } //给特定的用户发送一个一般通知 public async Task Publish_SentFrendshipRequest(string senderUserName, string friendshipMessage, long targetUserId) { await _notiticationPublisher.PublishAsync("SentFrendshipRequest", new SentFrendshipRequestNotificationData(senderUserName, friendshipMessage), userIds: new[] { targetUserId }); } //给特定的用户发送一个实体通知 public async Task Publish_CommentPhoto(string commenterUserName, string comment, Guid photoId, long photoOwnerUserId) { await _notiticationPublisher.PublishAsync("CommentPhoto", new CommentPhotoNotificationData(commenterUserName, comment), new EntityIdentifier(typeof(Photo), photoId), userIds: new[] { photoOwnerUserId }); } //给具特定严重级别程度的所有订阅用户发送一个一般通知 public async Task Publish_LowDisk(int remainingDiskInMb) { //例如 "LowDiskWarningMessage"的英文内容 -> "Attention! Only {remainingDiskInMb} MBs left on the disk!" var data = new LocalizableMessageNotificationData(new LocalizableString("LowDiskWarningMessage", "MyLocalizationSourceName")); data["remainingDiskInMb"] = remainingDiskInMb; await _notiticationPublisher.PublishAsync("System.LowDisk", data, severity: NotificationSeverity.Warn); } }
在第一个例子中,我们向单个用户发布了一个通知。 SentFrendshipRequestNotificationData应该从NotificationData派生如下:
[Serializable] public class SentFrendshipRequestNotificationData : NotificationData { public string SenderUserName { get; set; } public string FriendshipMessage { get; set; } public SentFrendshipRequestNotificationData(string senderUserName, string friendshipMessage) { SenderUserName = senderUserName; FriendshipMessage = friendshipMessage; } }
在第二个例子中,我们向特定的用户发送了一个特定实体的通知。通知数据类CommentPhotoNotificationData一般不需要serializble(因为默认使用了JSON序列化)。但是建议把它标记为serializable,因为你可能需要在应用之间移动通知,也可能在未来使用二进制的序列。此外,正如之前声明的那样,通知数据是可选的,而且对于所有的通知可能不是必须的。
注意:如果我们对特定的用户发布了一个通知,那么他们不需要订阅那些通知。
在第三个例子中,我们没有定义一个专用的通知数据类。相反,我们直接使用了内置的具有基于字典数据的LocalizableMessageNotificationData,并且以 Warn发布了通知。LocalizableMessageNotificationData可以存储基于字典的任意数据(这对于自定义的通知数据类也是成立的,因为它们也从 NotificationData类继承)。在本地化时我们使用了“ remaingDiskInMb”作为参数。本地化信息可以包含这些参数(比如例子中的"Attention! Only {remainingDiskInMb} MBs left on the disk!")
4,用户通知管理者
IUserNotificationManager用于管理用户的通知,它有 get,update或delete用户通知的方法。你可以为你的应用程序准备一个通知列表页面。
5,实时通知
虽然可以使用IUserNotificationManager来查询通知,但我们通常希望将实时通知推送到客户端。
通知系统使用IRealTimeNotifier向用户发送实时通知。 这可以用任何类型的实时通信系统来实现。 它使用SignalR在一个分开的包中实现。 启动模板已经安装了SignalR。 有关详细信息,请参阅SignalR集成文档。
注意:通知系统在后台作业中异步调用IRealTimeNotifier。 因此,可能会以较小的延迟发送通知。
①客户端
当收到实时通知时,ASP.NET Boilerplate会触发客户端的全局事件。 您可以注册以获取通知:
abp.event.on('abp.notifications.received', function (userNotification) { console.log(userNotification); });
每个收到的实时通知触发abp.notifications.received事件。 您可以注册到此事件,如上所示,以获得通知。 有关事件的更多信息,请参阅JavaScript事件总线文档。 “System.LowDisk”示例传入通知JSON示例:
{ "userId": 2, "state": 0, "notification": { "notificationName": "System.LowDisk", "data": { "message": { "sourceName": "MyLocalizationSourceName", "name": "LowDiskWarningMessage" }, "type": "Abp.Notifications.LocalizableMessageNotificationData", "properties": { "remainingDiskInMb": "42" } }, "entityType": null, "entityTypeName": null, "entityId": null, "severity": 0, "creationTime": "2016-02-09T17:03:32.13", "id": "0263d581-3d8a-476b-8e16-4f6a6f10a632" }, "id": "4a546baf-bf17-4924-b993-32e420a8d468" }
在这个对象中
- userId:当前的用户Id。一般来说不需要这个,因为你知道当前的用户。
- state: UserNotificationState枚举的值。0:未读,1:已读。
- notification:通知细节。
- notificationName:通知的唯一名称。(当发布该通知时使用相同的值)
- data:通知数据。在本例中,我们使用了LocalizableMessageNotificationData (正如之前的例子中发布的)。
- message:本地的信息通知。我们可以使用 sourceName和 name来本地化UI上的信息。
- type:通知数据类型。全类型名称,包括命名空间。当处理该通知数据时我们可以检查该类型。
- properties:基于字典的自定义属性。
- entityType, entityTypeName和entityId:和通知相关的实体的信息。
- severity: NotificationSeverity枚举的值。0: Info, 1: Success, 2: Warn, 3: Error, 4: Fatal。
- creationTime:该通知创建的时间。
- id:通知的Id。
- id:用户的通知id。
当然,你不会只记录通知。 您可以使用通知数据向用户显示通知信息。 例:
abp.event.on('abp.notifications.received', function (userNotification) { if (userNotification.notification.data.type === 'Abp.Notifications.LocalizableMessageNotificationData') { var localizedText = abp.localization.localize( userNotification.notification.data.message.name, userNotification.notification.data.message.sourceName ); $.each(userNotification.notification.data.properties, function (key, value) { localizedText = localizedText.replace('{' + key + '}', value); }); alert('New localized notification: ' + localizedText); } else if (userNotification.notification.data.type === 'Abp.Notifications.MessageNotificationData') { alert('New simple notification: ' + userNotification.notification.data.message); } });
它适用于内置的通知数据类型(LocalizableMessageNotificationData和MessageNotificationData)。 如果您有自定义通知数据类型,那么您应该注册这样的数据格式化程序:
abp.notifications.messageFormatters['MyProject.MyNotificationDataType'] = function(userNotification) { return ...; //格式和返回信息 };
6,通知存储
通知系统使用了INotificationStore来持久化通知。为了使通知系统合适地工作,应该实现这个接口。你可以自己实现或者使用module-zero(它已经实现了该接口)。7,
7,通知定义
在使用之前你不必定义一个通知。你无需定义任何类型的通知名就可以使用它们。但是,定义通知名可能会给你带来额外的好处。比如,你以后要研究应用程序中所有的通知,在这种情况下,我们可以给我们的模块定义一个通知提供器(notification provider ),如下所示:
public class MyAppNotificationProvider : NotificationProvider { public override void SetNotifications(INotificationDefinitionContext context) { context.Manager.Add( new NotificationDefinition( "App.NewUserRegistered", displayName: new LocalizableString("NewUserRegisteredNotificationDefinition", "MyLocalizationSourceName"), permissionDependency: new SimplePermissionDependency("App.Pages.UserManagement") ) ); } }
"App.NewUserRegistered"是该通知的唯一名称。我们定义了一个本地的 displayName(然后当在UI上订阅了该通知时,我们可以显示该通知)。最后,我们声明了只有拥有了"App.Pages.UserManagement"权限的用户才可以使用该通知。
也有一些其他的参数,你可以在代码中研究。对于一个通知定义,只有通知名称是必须的。
当定义了这么一个通知提供器之后,我们应该在模块的PreInitialize事件中进行注册,如下所示:
public class AbpZeroTemplateCoreModule : AbpModule { public override void PreInitialize() { Configuration.Notifications.Providers.Add<MyAppNotificationProvider>(); } //... }
最后,要获得通知定义,在应用程序中注入并使用INotificationDefinitionManager。
Abp.Web.SignalR nuget软件包可以方便地在基于ASP.NET Boilerplate的应用程序中使用SignalR
1,安装
①服务器端
将Abp.Web.SignalR nuget包安装到您的项目(通常到您的Web层),并向您的模块添加依赖关系:
[DependsOn(typeof(AbpWebSignalRModule))] public class YourProjectWebModule : AbpModule { //... }
然后按照惯例,在OWIN启动类中使用MapSignalR方法:
[assembly: OwinStartup(typeof(Startup))] namespace MyProject.Web { public class Startup { public void Configuration(IAppBuilder app) { app.MapSignalR(); //... } } }
注意:Abp.Web.SignalR只依赖于Microsoft.AspNet.SignalR.Core包。 因此,如果以前没有安装,您还需要将Microsoft.AspNet.SignalR包安装到您的Web项目中(有关更多信息,请参阅SignalR文档)。
②客户端
应该将abp.signalr.js脚本包含在页面中。 它位于Abp.Web.Resources包(它已经安装在启动模板中)
<script src="~/signalr/hubs"></script> <script src="~/Abp/Framework/scripts/libs/abp.signalr.js"></script>
就这样。 SignalR已正确配置并集成到您的项目中。
2,建立连接
当您的页面中包含abp.signalr.js时,ASP.NET Boilerplate会自动连接到服务器(从客户端)。 这通常很好。 但可能有些情况你不想要它。 您可以在包括abp.signalr.js之前添加这些行以禁用自动连接:
<script> abp.signalr = abp.signalr || {}; abp.signalr.autoConnect = false; </script>
在这种情况下,您可以在需要连接到服务器时手动调用abp.signalr.connect()函数。
如果abp.signalr.autoConnect为true,ASP.NET Boilerplate也会自动重新连接到客户端(从客户端)。
当客户端连接到服务器时,会触发“abp.signalr.connected”全局事件。 您可以注册到此事件,以便在连接成功建立时采取行动。 有关客户端事件的更多信息,请参阅JavaScript事件总线文档。
3,内置功能
您可以在应用程序中使用SignalR的所有功能。 另外,Abp.Web.SignalR包实现了一些内置的功能。
①通知(Notification)
Abp.Web.SignalR包实现IRealTimeNotifier向客户端发送实时通知(见通知系统)。 因此,用户可以获得实时推送通知。
②在线客户端
ASP.NET Boilerplate提供IOnlineClientManager来获取有关在线用户的信息(注入IOnlineClientManager并使用GetByUserIdOrNull,GetAllClients,IsOnline方法)。 IOnlineClientManager需要通信基础设施才能正常工作。 Abp.Web.SignalR包提供了基础架构。 因此,如果安装了SignalR,您可以在应用程序的任何层中注入和使用IOnlineClientManager。
③PascalCase与camelCase
Abp.Web.SignalR包覆盖SignalR的默认ContractResolver,以便在序列化时使用CamelCasePropertyNamesContractResolver。 因此,我们可以让类在服务器上具有PascalCase属性,并将它们作为camelCase在客户端上发送/接收对象(因为camelCase是javascript中的首选符号)。 如果您想在某些程序集中忽略此类,那么可以将这些程序集添加到AbpSignalRContractResolver.IgnoredAssemblies列表中。
4,您的SignalR代码
Abp.Web.SignalR包也简化了SignalR代码。 假设我们要添加一个Hub到我们的应用程序:
public class MyChatHub : Hub, ITransientDependency { public IAbpSession AbpSession { get; set; } public ILogger Logger { get; set; } public MyChatHub() { AbpSession = NullAbpSession.Instance; Logger = NullLogger.Instance; } public void SendMessage(string message) { Clients.All.getMessage(string.Format("User {0}: {1}", AbpSession.UserId, message)); } public async override Task OnConnected() { await base.OnConnected(); Logger.Debug("A client connected to MyChatHub: " + Context.ConnectionId); } public async override Task OnDisconnected(bool stopCalled) { await base.OnDisconnected(stopCalled); Logger.Debug("A client disconnected from MyChatHub: " + Context.ConnectionId); } }
我们实现了ITransientDependency接口,简单地将我们的集线器注册到依赖注入系统(您可以根据需要进行单例化)。 我们注入session和logger。
SendMessage是我们的集线器的一种方法,可以被客户端使用。 我们在这个方法中调用所有客户端的getMessage函数。 我们可以使用AbpSession来获取当前用户ID(如果用户登录)。 我们还覆盖了OnConnected和OnDisconnected,这只是为了演示,实际上并不需要。
这里,客户端的JavaScript代码使用我们的集线器发送/接收消息。
var chatHub = $.connection.myChatHub; chatHub.client.getMessage = function (message) { //注册传入消息 console.log('received message: ' + message); }; abp.event.on('abp.signalr.connected', function() { //注册连接事件 chatHub.server.sendMessage("Hi everybody, I'm connected to the chat!"); //向服务器发送消息 });
然后我们可以随时使用chatHub发送消息到服务器。 有关SignalR的详细信息,请参阅SignalR文档。
1,Nuget包
Abp.EntityFramework
2,DbContext
如您所知,要使用EntityFramework,您应该为应用程序定义一个DbContext类。 示例DbContext如下所示:
public class SimpleTaskSystemDbContext : AbpDbContext { public virtual IDbSet<Person> People { get; set; } public virtual IDbSet<Task> Tasks { get; set; } public SimpleTaskSystemDbContext() : base("Default") { } public SimpleTaskSystemDbContext(string nameOrConnectionString) : base(nameOrConnectionString) { } public SimpleTaskSystemDbContext(DbConnection existingConnection) : base(existingConnection, false) { } public SimpleTaskSystemDbContext(DbConnection existingConnection, bool contextOwnsConnection) : base(existingConnection, contextOwnsConnection) { } protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity<Person>().ToTable("StsPeople"); modelBuilder.Entity<Task>().ToTable("StsTasks").HasOptional(t => t.AssignedPerson); } }
除了以下规则,它是一个常规的DbContext类:
①它来自AbpDbContext而不是DbContext。
②它应该有上面的示例的构造函数(构造函数参数名也应该相同)。 说明:
默认构造器将“Default”传递给基类作为连接字符串。 因此,它希望在web.config / app.config文件中使用一个名为“Default”的连接字符串。 这个构造函数不被ABP使用,而是由EF命令行迁移工具命令(如update-database)使用。
构造函数获取nameOrConnectionString由ABP用于在运行时传递连接名称或字符串。
构造函数获取existingConnection可以用于单元测试,而不是ABP直接使用。
构造函数获取existingConnection,并且在使用DbContextEfTransactionStrategy时,ABP在单个数据库多个dbcontext场景中使用contextOwnsConnection来共享相同的connection和transaction()(参见下面的事务管理部分)。
3,仓储
仓储库用于从较高层抽象数据访问
①默认仓储库
Abp.EntityFramework为您在DbContext中定义的所有实体实现默认存储库。 您不必创建存储库类以使用预定义的存储库方法。 例:
public class PersonAppService : IPersonAppService { private readonly IRepository<Person> _personRepository; public PersonAppService(IRepository<Person> personRepository) { _personRepository = personRepository; } public void CreatePerson(CreatePersonInput input) { person = new Person { Name = input.Name, EmailAddress = input.EmailAddress }; _personRepository.Insert(person); } }
②应用程序特定基础仓储库类
ASP.NET Boilerplate提供了一个基类EfRepositoryBase来轻松实现存储库。 要实现IRepository接口,您只需从该类派生您的存储库。 但是最好创建自己的扩展EfRepositoryBase的基类。 因此,您可以将共享/常用方法添加到存储库中,或轻松覆盖现有方法。 一个示例基类全部用于SimpleTaskSystem应用程序的存储库:
//我的应用程序中所有存储库的基类 public class SimpleTaskSystemRepositoryBase<TEntity, TPrimaryKey> : EfRepositoryBase<SimpleTaskSystemDbContext, TEntity, TPrimaryKey> where TEntity : class, IEntity<TPrimaryKey> { public SimpleTaskSystemRepositoryBase(IDbContextProvider<SimpleTaskSystemDbContext> dbContextProvider) : base(dbContextProvider) { } //为所有存储库添加常用方法 } //具有整数ID的实体的快捷方式 public class SimpleTaskSystemRepositoryBase<TEntity> : SimpleTaskSystemRepositoryBase<TEntity, int> where TEntity : class, IEntity<int> { public SimpleTaskSystemRepositoryBase(IDbContextProvider<SimpleTaskSystemDbContext> dbContextProvider) : base(dbContextProvider) { } //不要在这里添加任何方法,添加到上面的类(因为这个类继承它) }
请注意,我们继承自EfRepositoryBase <SimpleTaskSystemDbContext,TEntity,TPrimaryKey>。 这声明ASP.NET Boilerplate在我们的存储库中使用SimpleTaskSystemDbContext。
默认情况下,您使用EfRepositoryBase实现给定DbContext(此示例中为SimpleTaskSystemDbContext)的所有存储库。 您可以将AutoRepositoryTypes属性添加到DbContext中,将其替换为您自己的存储库库存储库类,如下所示:
[AutoRepositoryTypes( typeof(IRepository<>), typeof(IRepository<,>), typeof(SimpleTaskSystemEfRepositoryBase<>), typeof(SimpleTaskSystemEfRepositoryBase<,>) )] public class SimpleTaskSystemDbContext : AbpDbContext { ... }
③自定义存储库示例
要实现自定义存储库,只需从上面创建的应用程序特定的基础存储库类派生。
public interface ITaskRepository : IRepository<Task, long> { List<Task> GetAllWithPeople(int? assignedPersonId, TaskState? state); } public class TaskRepository : SimpleTaskSystemRepositoryBase<Task, long>, ITaskRepository { public TaskRepository(IDbContextProvider<SimpleTaskSystemDbContext> dbContextProvider) : base(dbContextProvider) { } public List<Task> GetAllWithPeople(int? assignedPersonId, TaskState? state) { var query = GetAll(); if (assignedPersonId.HasValue) { query = query.Where(task => task.AssignedPerson.Id == assignedPersonId.Value); } if (state.HasValue) { query = query.Where(task => task.State == state); } return query .OrderByDescending(task => task.CreationTime) .Include(task => task.AssignedPerson) .ToList(); } }
我们首先定义了ITaskRepository,然后实现它。 GetAll()返回IQueryable <Task>,然后我们可以使用给定的参数添加一些Where过滤器。 最后我们可以调用ToList()来获取任务列表。
您还可以使用存储库方法中的Context对象来访问DbContext并直接使用Entity Framework API。
④仓储库最佳实现
尽可能使用默认存储库
始终为您的应用程序创建自定义存储库的存储库基类,如上所述。
仓储接口在领域层定义,仓储实现在EntityFramework层
⑤事务管理
ASP.NET Boilerplate具有内置的工作系统单元来管理数据库连接和事务。 实体框架有不同的事务管理方法。 ASP.NET Boilerplate默认使用环境TransactionScope方法,但也具有DbContext事务API的内置实现。 如果要切换到DbContext事务API,可以在模块的PreInitialize方法中进行配置:
Configuration.ReplaceService<IEfTransactionStrategy, DbContextEfTransactionStrategy>(DependencyLifeStyle.Transient);
记得添加“using Abp.Configuration.Startup;” 到你的代码文件可以使用ReplaceService泛型扩展方法。
此外,您的DbContext应具有本文档DbContext部分中所述的构造函数。
1,Nuget包
安装Abp.NHibernate
2,配置
要开始使用NHibernate,您应该在模块的PreInitialize中进行配置。
[DependsOn(typeof(AbpNHibernateModule))] public class SimpleTaskSystemDataModule : AbpModule { public override void PreInitialize() { var connStr = ConfigurationManager.ConnectionStrings["Default"].ConnectionString; Configuration.Modules.AbpNHibernate().FluentConfiguration .Database(MsSqlConfiguration.MsSql2008.ConnectionString(connStr)) .Mappings(m => m.FluentMappings.AddFromAssembly(Assembly.GetExecutingAssembly())); } public override void Initialize() { IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly()); } }
3,映射实体
在上面的示例配置中,我们使用当前程序集中的所有映射类进行流畅映射。 示例映射类可以如下所示:
public class TaskMap : EntityMap<Task> { public TaskMap() : base("TeTasks") { References(x => x.AssignedUser).Column("AssignedUserId").LazyLoad(); Map(x => x.Title).Not.Nullable(); Map(x => x.Description).Nullable(); Map(x => x.Priority).CustomType<TaskPriority>().Not.Nullable(); Map(x => x.Privacy).CustomType<TaskPrivacy>().Not.Nullable(); Map(x => x.State).CustomType<TaskState>().Not.Nullable(); } }
EntityMap是一个ASP.NET Boilerplate类,它扩展了ClassMap <T>,自动映射Id属性并在构造函数中获取表名。 所以,我从它导出,并使用FluentNHibernate映射其他属性。 当然,您可以直接从ClassMap中导出,您可以使用FluentNHibernate的完整API,您可以使用NHibernate的其他映射技术(如映射XML文件)。
4,仓储
①默认实现
Abp.NHibernate包实现了应用程序中实体的默认存储库。 您不必为实体创建存储库类,只需使用预定义的存储库方法。 例:
public class PersonAppService : IPersonAppService { private readonly IRepository<Person> _personRepository; public PersonAppService(IRepository<Person> personRepository) { _personRepository = personRepository; } public void CreatePerson(CreatePersonInput input) { person = new Person { Name = input.Name, EmailAddress = input.EmailAddress }; _personRepository.Insert(person); } }
②自定义仓储
如果要添加一些自定义方法,应首先将其添加到存储库接口(作为最佳实践),然后在存储库类中实现。 ASP.NET Boilerplate提供了一个基类NhRepositoryBase来轻松实现存储库。 要实现IRepository接口,您只需从该类派生您的存储库。
public interface ITaskRepository : IRepository<Task, long> { List<Task> GetAllWithPeople(int? assignedPersonId, TaskState? state); } public class TaskRepository : NhRepositoryBase<Task, long>, ITaskRepository { public TaskRepository(ISessionProvider sessionProvider) : base(sessionProvider) { } public List<Task> GetAllWithPeople(int? assignedPersonId, TaskState? state) { var query = GetAll(); if (assignedPersonId.HasValue) { query = query.Where(task => task.AssignedPerson.Id == assignedPersonId.Value); } if (state.HasValue) { query = query.Where(task => task.State == state); } return query .OrderByDescending(task => task.CreationTime) .Fetch(task => task.AssignedPerson) .ToList(); } }
GetAll()返回IQueryable <Task>,然后我们可以使用给定的参数添加一些Where过滤器。 最后我们可以调用ToList()来获取任务列表。
您还可以使用存储库方法中的Session对象来使用NHibernate的完整API。
③应用程序特定的基本存储库类
虽然您可以从ASP.NET Boilerplate的NhRepositoryBase派生您的存储库,但更好的做法是创建自己的扩展NhRepositoryBase的基类。 因此,您可以轻松地将共享/常用方法添加到您的存储库。 例:
//我的应用程序中所有存储库的基类 public abstract class MyRepositoryBase<TEntity, TPrimaryKey> : NhRepositoryBase<TEntity, TPrimaryKey> where TEntity : class, IEntity<TPrimaryKey> { protected MyRepositoryBase(ISessionProvider sessionProvider) : base(sessionProvider) { } //为所有存储库添加常用方法 } //具有整数ID的实体的快捷方式 public abstract class MyRepositoryBase<TEntity> : MyRepositoryBase<TEntity, int> where TEntity : class, IEntity<int> { protected MyRepositoryBase(ISessionProvider sessionProvider) : base(sessionProvider) { } //不要在这里添加任何方法,添加上面的类(因为它继承它) } public class TaskRepository : MyRepositoryBase<Task>, ITaskRepository { public TaskRepository(ISessionProvider sessionProvider) : base(sessionProvider) { } //任务存储库的具体方法 }
1,安装
nuget Abp.Dapper
2,模块注册
[DependsOn( typeof(AbpEntityFrameworkCoreModule), typeof(AbpDapperModule) )] public class MyModule : AbpModule { public override void Initialize() { IocManager.RegisterAssemblyByConvention(typeof(SampleApplicationModule).GetAssembly()); } }
请注意,AbpDapperModule依赖关系应该晚于EF Core依赖项。
3,实体到表映射
您可以配置映射。 例如,Person类映射到以下示例中的Persons表:
public class PersonMapper : ClassMapper<Person> { public PersonMapper() { Table("Persons"); Map(x => x.Roles).Ignore(); AutoMap(); } }
您应该设置包含映射器类的程序集。 例:
[DependsOn( typeof(AbpEntityFrameworkModule), typeof(AbpDapperModule) )] public class MyModule : AbpModule { public override void Initialize() { IocManager.RegisterAssemblyByConvention(typeof(SampleApplicationModule).GetAssembly()); DapperExtensions.SetMappingAssemblies(new List<Assembly> { typeof(MyModule).GetAssembly() }); } }
4,用法
注册AbpDapperModule后,您可以使用Generic IDapperRepository接口(而不是标准IRepository)来注入dapper存储库。
public class SomeApplicationService : ITransientDependency { private readonly IDapperRepository<Person> _personDapperRepository; private readonly IRepository<Person> _personRepository; public SomeApplicationService( IRepository<Person> personRepository, IDapperRepository<Person> personDapperRepository) { _personRepository = personRepository; _personDapperRepository = personDapperRepository; } public void DoSomeStuff() { var people = _personDapperRepository.Query("select * from Persons"); } }