ABP框架学习

 

1、整体与公共结构
  1,ABP配置javascript

  2,多租户css

  3,ABP Sessionhtml

  4,缓存 java

  5,日志jquery

  6,设置管理 angularjs

  7,Timing web

  8,ABPMapperajax

  9,发送电子邮件 数据库

2、领域层编程

  10,实体

  11,值对象

  12,仓储

  13,领域服务 

  14,规格模式

  15,工做单元

  16,事件总线 

  17,数据过滤器

3、应用层

  18,应用服务 

  19,数据传输对象

  20,验证数据传输对象

  21,受权

  22,功能管理

  23,审计日志

4、分布式服务层

  24,ASP.NET Web API Controllers

  25,动态Webapi层

  26,OData整合

  27,Swagger UI 整合 

5、展现层

  28,ASP.NET MVC

  29,本地化

  30,导航

  31,嵌入式资源文件

  32,ABP-JavaScript API

  33,CSRF / XSRF保护

6、后台服务

  34,后台工做(Jobs)和工做者(Workers)

  35,Hangfire集成

  36,Quartz 集成

7、实时服务 

  37,通知系统

  38,SignalR集成

 8、ORM

  39,EntityFramework集成

  40,NHibernate集成

  41,Dapper使用

 

1、ABP配置

//将全部异常发送到客户端
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;

2、多租户

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接口,由于它更难维护租户和租主共享的代码。

 

3、ABP Session

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
        }
    }
}

 

4、缓存

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"/>

不一样的数据库标识对于在同一服务器中建立不一样的密钥空间(隔离缓存)颇有用。

 

5、日志

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...');

 

6、设置管理

一个设置通常是存储在数据库(或其余源)的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是否发生了变化。

 

7、Timing

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)。

 

8、ABPMapper

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

 

9、发送电子邮件

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

10、实体

实体具备Id并存储在数据库中, 实体一般映射到关系数据库的表。

1,审计接口

①当Entity被插入到实现该接口的数据库中时,ASP.NET Boilerplate会自动将CreationTime设置为当前时间。

public interface IHasCreationTime
{
    DateTime CreationTime { get; set; }
}

②ASP.NET Boilerplate在保存新实体时自动将CreatorUserId设置为当前用户的id

public interface ICreationAudited : IHasCreationTime
{
    long? CreatorUserId { get; set; }
}

③编辑时间,编辑人员

public interface IHasModificationTime
{
    DateTime? LastModificationTime { get; set; }
}

public interface IModificationAudited : IHasModificationTime
{
    long? LastModifierUserId { get; set; }
}

④若是要实现全部审计属性,能够直接实现IAudited接口:

public interface IAudited : ICreationAudited, IModificationAudited
{

}

⑤能够直接继承AuditedEntity审计类

注意:ASP.NET Boilerplate从ABP Session获取当前用户的Id。

⑥软删除

public interface ISoftDelete
{
    bool IsDeleted { get; set; }
}
public interface IDeletionAudited : ISoftDelete
{
    long? DeleterUserId { get; set; }

    DateTime? DeletionTime { get; set; }
}

⑦全部审计接口

public interface IFullAudited : IAudited, IDeletionAudited
{

}

⑧直接使用全部审计类FullAuditedEntity 

2,继承IExtendableObject接口存储json字段

public class Person : Entity, IExtendableObject
{
    public string Name { get; set; }

    public string ExtensionData { get; set; }

    public Person(string name)
    {
        Name = name;
    }
}
var person = new Person("John");
//存入数据库中的值:{"CustomData":{"Value1":42,"Value2":"forty-two"},"RandomValue":178}
person.SetData("RandomValue", RandomHelper.GetRandom(1, 1000)); 
person.SetData(
"CustomData", new MyCustomObject { Value1 = 42, Value2 = "forty-two" });
var randomValue = person.GetData<int>("RandomValue");
var customData = person.GetData<MyCustomObject>("CustomData");

 

11、值对象

与实体相反,实体拥有身份标识(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);

12、仓储

是领域层与数据访问层的中介。每一个实体(或聚合根)对应一个仓储

在领域层中定义仓储接口,在基础设施层实现

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特定的任务。

  

十3、领域服务

领域服务(或DDD中的服务)用于执行领域操做和业务规则。Eric Evans描述了一个好的服务应该具有下面三个特征:

  1. 和领域概念相关的操做不是一个实体或者值对象的本质部分。
  2. 该接口是在领域模型的其余元素来定义的。
  3. 操做是无状态的。

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

 

十4、规格模式

规格模式是一种特定的软件设计模式。经过使用布尔逻辑将业务规则连接在一块儿能够重组业务规则。在实际中,它主要用于为实体或其余业务对象定义可重用的过滤器

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

 

十5、工做单元

若是工做方法单元调用另外一个工做单元方法,则使用相同的链接和事务。 第一个进入方法管理链接和事务,其余的使用它。

应用服务方法默认是工做单元。方法开始启动事务,方法结束提交事务。若是发生异常则回滚。这样应用服务方法中的全部数据库操做都将变为原子(工做单元)

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

 

十6、事件总线

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...
    }
}

 

十7、数据过滤器

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,而不是静态编码。 以上示例是参数化过滤器。 滤波器能够有零个或多个参数。 若是没有参数,则不须要设置过滤器参数值。 此外,若是默认状况下启用,则不须要手动启用它(固然,咱们能够禁用它)。

 

十8、应用服务

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(...)方法便可。

 

十9、数据传输对象

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; }
}

 

二10、验证数据传输对象

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";
        }
    }
}

 

二11、受权

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命名空间。

 

二12、功能管理

大多数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,深度研究

1、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表示基于版本

2、案例分析

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(默认配置的值)

 

二十3、审计日志

基本上保存的字段是:相关租户信息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的一个或一个属性。 所以,您能够隐藏审计日志中的敏感数据,例如密码。

 

二十4、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>)进行归一化。

 

二十5、动态Webapi层

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>

 

二十6、OData整合

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>
元数据

 

二十7、Swagger UI 整合

....使用启用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,测试

 

二十8、ASP.NET MVC

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对象。

 

二十9、本地化

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可用于获取全部可用语言和当前语言的列表。

 

三10、导航

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来获取应用程序的主菜单。 所以,咱们能够在客户端建立菜单。

 

三11、嵌入式资源文件

在一个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方法。

 

三十3、CSRF / XSRF保护

“跨站点请求伪造(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集成是现成可用的。

 

三十4、后台工做(Jobs)和工做者(Workers)

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文档讲解了一些其余的方法。

 

三十5、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应该在您的启动类中的认证中间件以后调用(多是最后一行)。 不然,受权老是失败。

 

三十6、Quartz 集成

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!");
    }
}  

 

三十7、通知系统

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

 

三十8、SignalR集成

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文档。

 

三十9、EntityFramework集成

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部分中所述的构造函数。

 

四10、NHibernate集成

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)
    {
    }

    //任务存储库的具体方法
}

 

四11、Dapper使用

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");
    }
}
相关文章
相关标签/搜索