ABP框架使用(版本3.3.1)
1.Refers
EntityFramework
https://docs.microsoft.com/zh-cn/ef/core/modeling/entity-properties?tabs=data-annotations
https://entityframework.net/articles/carloscds-ef6-stored-procedure
abp sample
https://github.com/abpframework/abp-samples
2.DbMigrator
Remove-migration -force
add-migration initial
update-database
3.配置数据库表前缀
https://www.cnblogs.com/yiluomyt/p/10350524.html
https://www.bookstack.cn/read/abp-3.0-zh/dfae9fb778e87934.md#aasbho
基础模块(如身份, 租户管理 和 审计日志)使用 Abp 前缀, 其他的模块使用自己的前缀. 如Identity Server 模块使用前缀 IdentityServer.
修改Volo.Abp.IdentityServer.AbpIdentityServerDbProperties.DbTablePrefix 的方法如果用update-database命令是不起作用的
启动Acme.BookStore.DbMigrator可以生效
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public class BookStoreDomainModule : AbpModule { public override void PreConfigureServices(ServiceConfigurationContext context) { Volo.Abp.AuditLogging.AbpAuditLoggingDbProperties.DbTablePrefix = "test_" ; Volo.Abp.IdentityServer.AbpIdentityServerDbProperties.DbTablePrefix = "mid_" ; BookStoreDomainObjectExtensions.Configure(); } public override void ConfigureServices(ServiceConfigurationContext context) { Configure<AbpMultiTenancyOptions>(options => { options.IsEnabled = MultiTenancyConsts.IsEnabled; }); } } |
Option1 . 修改基础模块的表前缀,可以新起一个 BackgroundJobsDbContextModelCreatingExtensions
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | public static class BackgroundJobsDbContextModelCreatingExtensions { public static void ConfigureBackgroundJobs( this ModelBuilder builder, Action<BackgroundJobsModelBuilderConfigurationOptions> optionsAction = null ) { var options = new BackgroundJobsModelBuilderConfigurationOptions( BackgroundJobsDbProperties.DbTablePrefix, BackgroundJobsDbProperties.DbSchema ); optionsAction?.Invoke(options); builder.Entity<BackgroundJobRecord>(b => { b.ToTable( "bg_" + "BackgroundJobs" , options.Schema); b.ConfigureCreationTime(); b.ConfigureExtraProperties(); b.Property(x => x.JobName) .IsRequired() .HasMaxLength(BackgroundJobRecordConsts.MaxJobNameLength); //... }); } } |
Optioin2 . 修改应用程序的模块更改数据库表前缀,在表的类前面加上表属性 [Table("Author")] ,在BookStoreDbContextModelCreatingExtensions.cs 类里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | var entityTypes = builder.Model.GetEntityTypes().ToList(); // 设置自定义表前缀 foreach ( var entityType in entityTypes) { if (entityType.ClrType .GetCustomAttributes( typeof (TableAttribute), true ) .FirstOrDefault() is TableAttribute table) { // 如果你的表名就是实体类型名的话,可以修改为如下形式,就不必给出[table]的Name参数 // string tableName = tablePrefix + entityType.ClrType.Name; // 如若有其他需求也可在此进行修改 string tableName = "TESTY" + table.Name; builder.Entity(entityType.ClrType) .ToTable(tableName); } } |
Option3 . 最简单的方法是在 BookStoreMigrationsDbContext 指定表前缀
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | protected override void OnModelCreating(ModelBuilder builder) { base .OnModelCreating(builder); /* Include modules to your migration db context */ builder.ConfigurePermissionManagement(options => { options.TablePrefix = "cc" ; }); builder.ConfigureSettingManagement(options => { options.TablePrefix = "dd" ; }); builder.ConfigureBackgroundJobs(options => { options.TablePrefix = "ee" ; }); builder.ConfigureAuditLogging(options => { options.TablePrefix = "ff" ; }); // builder.ConfigureIdentity(); builder.ConfigureIdentity(options => { options.TablePrefix = "gg" ; }); builder.ConfigureIdentityServer(options => { options.TablePrefix = "hh" ; }); builder.ConfigureFeatureManagement(options => { options.TablePrefix = "aa" ; }); builder.ConfigureTenantManagement(options => { options.TablePrefix = "bb" ; // options.Schema = ""; }); /* Configure your own tables/entities inside the ConfigureBookStore method */ builder.ConfigureBookStore(); } |
改变了表的schema后,生成的sql会报错,在domain层加了类AbpIdentityServerDbProperties ,但不起作用,需在DomainModule上指定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public override void ConfigureServices(ServiceConfigurationContext context) { Configure<AbpMultiTenancyOptions>(options => { options.IsEnabled = MultiTenancyConsts.IsEnabled; }); #if DEBUG context.Services.Replace(ServiceDescriptor.Singleton<IEmailSender, NullEmailSender>()); #endif AbpIdentityServerDbProperties.DbSchema = "ids" ; AbpIdentityDbProperties.DbSchema = "id" ; } |
4.Swagger小绿锁
Bearer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | private void ConfigureSwaggerServices(IServiceCollection services) { services.AddSwaggerGen( options => { options.SwaggerDoc( "v1" , new OpenApiInfo { Title = "BookStore API" , Version = "v1" }); options.DocInclusionPredicate((docName, description) => true ); options.CustomSchemaIds(type => type.FullName); options.AddSecurityDefinition( "Bearer" , new OpenApiSecurityScheme() { Name = "Authorization" , Scheme = "bearer" , Description = "Specify the authorization token." , In = ParameterLocation.Header, Type = SecuritySchemeType.Http, }); options.AddSecurityRequirement( new OpenApiSecurityRequirement() { { new OpenApiSecurityScheme { Reference = new OpenApiReference {Type = ReferenceType.SecurityScheme, Id = "Bearer" } }, new string [] { } }, }); } ); } |
token 值
1 2 3 4 5 6 7 8 9 10 | { "nbf" : 1606551733, "exp" : 1638087733, "iss" : "https://localhost:44356" , "aud" : "BookStore" , "client_id" : "BookStore_App" , "scope" : [ "BookStore" ] } |
bearerAuth
options.AddSecurityDefinition("bearerAuth", new Microsoft.OpenApi.Models.OpenApiSecurityScheme() { Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"", Name = "Authorization", In = Microsoft.OpenApi.Models.ParameterLocation.Header, Type = Microsoft.OpenApi.Models.SecuritySchemeType.OAuth2, Flows = new Microsoft.OpenApi.Models.OpenApiOAuthFlows() { Password = new Microsoft.OpenApi.Models.OpenApiOAuthFlow() { TokenUrl = new Uri("https://localhost:44356/connect/token") } } });
token 值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | { "nbf" : 1606550841, "exp" : 1638086841, "iss" : "https://localhost:44356" , "aud" : "BookStore" , "client_id" : "BookStore_App" , "sub" : "bfc83aa4-0278-4f4b-656e-39f912477096" , "auth_time" : 1606550840, "idp" : "local" , "phone_number_verified" : "False" , "email" : "bookstore@test.com" , "email_verified" : [ "False" , false ], "name" : "bookstore" , "scope" : [ "address" , "email" , "openid" , "phone" , "profile" , "role" , "BookStore" , "offline_access" ], "amr" : [ "pwd" ] } |
5.自动API控制器
配置
基本配置很简单. 只需配置AbpAspNetCoreMvcOptions并使用ConventionalControllers.Create方法,如下所示:
此示例代码配置包含类BookStoreApplicationModule的程序集中的所有应用程序服务.下图显示了Swagger UI上的API内容.
例子
一些示例方法名称和按约定生成的相应路由:
服务方法名称 | HTTP Method | 路由 |
---|---|---|
GetAsync(Guid id) | GET | /api/app/book/{id} |
GetListAsync() | GET | /api/app/book |
CreateAsync(CreateBookDto input) | POST | /api/app/book |
UpdateAsync(Guid id, UpdateBookDto input) | PUT | /api/app/book/{id} |
DeleteAsync(Guid id) | DELETE | /api/app/book/{id} |
GetEditorsAsync(Guid id) | GET | /api/app/book/{id}/editors |
CreateEditorAsync(Guid id, BookEditorCreateDto input) | POST | /api/app/book/{id}/editor |
6. 扩展属性
Option1 :在类AppUser里面,扩展属性加在表AbpUsers
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | public class AppUser : FullAuditedAggregateRoot<Guid>, IUser { #region Base properties /* These properties are shared with the IdentityUser entity of the Identity module. * Do not change these properties through this class. Instead, use Identity module * services (like IdentityUserManager) to change them. * So, this properties are designed as read only! */ public virtual Guid? TenantId { get ; private set ; } public virtual string UserName { get ; private set ; } public virtual string Name { get ; private set ; } public virtual string Surname { get ; private set ; } public virtual string Email { get ; private set ; } public virtual bool EmailConfirmed { get ; private set ; } public virtual string PhoneNumber { get ; private set ; } public virtual bool PhoneNumberConfirmed { get ; private set ; } public string MyProperty { get ; set ; } #endregion /* Add your own properties here. Example: * * public string MyProperty { get; set; } * * If you add a property and using the EF Core, remember these; * * 1. Update BookStoreDbContext.OnModelCreating * to configure the mapping for your new property * 2. Update BookStoreEfCoreEntityExtensionMappings to extend the IdentityUser entity * and add your new property to the migration. * 3. Use the Add-Migration to add a new database migration. * 4. Run the .DbMigrator project (or use the Update-Database command) to apply * schema change to the database. */ private AppUser() { } } |
Option2 :
https://iter01.com/522920.html
BookStoreEfCoreEntityExtensionMappings
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner(); public static void Configure() { BookStoreModulePropertyConfigurator.Configure(); OneTimeRunner.Run(() => { /* You can configure entity extension properties for the * entities defined in the used modules. * * The properties defined here becomes table fields. * If you want to use the ExtraProperties dictionary of the entity * instead of creating a new field, then define the property in the * BookStoreDomainObjectExtensions class. * * Example: * * ObjectExtensionManager.Instance * .MapEfCoreProperty<IdentityUser, string>( * "MyProperty", * b => b.HasMaxLength(128) * ); * * See the documentation for more: * https://docs.abp.io/en/abp/latest/Customizing-Application-Modules-Extending-Entities */ ObjectExtensionManager.Instance .MapEfCoreProperty<IdentityUser, string >( "MyProperty" , (entityBuilder, propertyBuilder) => { propertyBuilder.HasMaxLength(32); } ); }); } } |
Acme.BookStore.Application.Contracts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | public static class BookStoreDtoExtensions { private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner(); public static void Configure() { OneTimeRunner.Run(() => { /* You can add extension properties to DTOs * defined in the depended modules. * * Example: * * ObjectExtensionManager.Instance * .AddOrUpdateProperty<IdentityRoleDto, string>("Title"); * * See the documentation for more: * https://docs.abp.io/en/abp/latest/Object-Extensions */ ObjectExtensionManager.Instance /* .AddOrUpdateProperty<string>( new[] { typeof(IdentityUserDto), typeof(IdentityUserCreateDto), typeof(IdentityUserUpdateDto), typeof(ProfileDto), typeof(UpdateProfileDto) }, "MyProperty" ) */ .AddOrUpdateProperty< string >( new [] { typeof (IdentityRoleDto), typeof (IdentityRoleCreateDto), typeof (IdentityRoleUpdateDto) }, "MyProperty" ); }); } } |
7.Overriding Identity Services
https://github.com/abpframework/abp/blob/dev/docs/en/Customizing-Application-Modules-Overriding-Services.md
Example: Overriding a Domain Service
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | [Dependency(ReplaceServices = true )] [ExposeServices( typeof (IdentityUserManager))] public class MyIdentityUserManager : IdentityUserManager { public MyIdentityUserManager( IdentityUserStore store, IIdentityRoleRepository roleRepository, IIdentityUserRepository userRepository, IOptions<IdentityOptions> optionsAccessor, IPasswordHasher<IdentityUser> passwordHasher, IEnumerable<IUserValidator<IdentityUser>> userValidators, IEnumerable<IPasswordValidator<IdentityUser>> passwordValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, ILogger<IdentityUserManager> logger, ICancellationTokenProvider cancellationTokenProvider) : base (store, roleRepository, userRepository, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger, cancellationTokenProvider) { } public async override Task<IdentityResult> CreateAsync(IdentityUser user) { if (user.PhoneNumber.IsNullOrWhiteSpace()) { throw new AbpValidationException( "Phone number is required for new users!" , new List<ValidationResult> { new ValidationResult( "Phone number can not be empty!" , new []{ "PhoneNumber" } ) } ); } return await base .CreateAsync(user); } } |
/api/account/register
Volo.Abp.Account.AccountController
/api/account/login
Volo.Abp.Account.Web.Areas.Account.Controllers.AccountController
8.事件总线
https://www.cnblogs.com/whuanle/p/13679991.html
9.实体历史:ABP 提供了一个基础设施,可以自动的记录所有实体以及属性的变更历史。默认开启,一般应用不重要可以在预初始化PreInitialize 方法中禁用他Configuration.EntityHistory.IsEnabled = false;
Entity History : 会记录在表 AbpEntityChanges 和 AbpEntityPropertyChanges
[Audited]
public class MyEntity : Entity
{
public string MyProperty1 { get; set; }
[DisableAuditing]
public int MyProperty2 { get; set; }
public long MyProperty3 { get; set; }
}
10.调试
所有官方的 ABP nuget packages 都开启了Sourcelink。这就是说你可以在你的项目中很方便的调试 Abp. nuget packages。为了开启该功能,你需要像下面一样来设置你的 Visual Studio (2017+) 调试选项。
一旦你开启了该功能,你可以进入(F11)ABP的源代码。
11.AsyncCrudAppService?
12. SwaggerUI InjectJavaScript
1 2 3 4 5 | .EnableSwaggerUi( "apis/{*assetPath}" , b => { //对js进行了拓展 b.InjectJavaScript(Assembly.GetExecutingAssembly(), "YoYoCMS.PhoneBook.SwaggerUi.scripts.swagger.js" ); }); |
13.禁用具体某个接口的审计功能
1 2 3 | [DisableAuditing] //屏蔽这个AppService的审计功能 [AbpAuthorize(AppPermissions.Pages_Administration_AuditLogs)] public class AuditLogAppService : GHITAssetAppServiceBase, IAuditLogAppService |
14.复合主键
1 2 3 4 5 | [Key, Column(Order = 2)] public int CityId{ get ; set ; } [Key, Column(Order = 3)] public int companyId{ get ; set ; } |
15. abphelper 的使用
%USERPROFILE%\.dotnet\tools\abphelper generate crud "Item" -d "D:\projects\github\csharp\_abp\DRS" --skip-db-migrations --skip-ui --skip-view-model --skip-localization --migration-project-name "DRS.DbMigrator.csproj"
当在Domain项目下创建了类Item后,abphelper 自动生成的crud代码如下
16. 定时任务AsyncPeriodicBackgroundWorkerBase 参照
abp-dev\abp\modules\identityserver\src\Volo.Abp.IdentityServer.Domain\Volo\Abp\IdentityServer\Tokens\TokenCleanupBackgroundWorker.cs
17. LocalEventBus : EntityChangedEventData 参照
abp-dev\abp\modules\identityserver\src\Volo.Abp.IdentityServer.Domain\Volo\Abp\IdentityServer\AllowedCorsOriginsCacheItemInvalidator.cs
18. 类继承Entity(复合主键类)需要实现Equals方法和GetKeys,在EFCore层用HasKey 参照
abp-dev\abp\modules\identityserver\src\Volo.Abp.IdentityServer.Domain\Volo\Abp\IdentityServer\ApiResources\ApiResourceProperty.cs
abp-dev\abp\modules\identityserver\src\Volo.Abp.IdentityServer.EntityFrameworkCore\Volo\Abp\IdentityServer\EntityFrameworkCore\IdentityServerDbContextModelCreatingExtensions.cs
19.同时需要用EfCore和MongoDB,connection string会被覆盖掉,需要实现 DefaultConnectionStringResolver
同一个entity只能定义EfCore或者MongoDB的DBContext,不能同时为EfCore和MongoDB定义不同的IRepositories,背后似乎是用entity name来做注入(看名字后面是否跟Interface名字匹配),对于EntityChangedEventData,也是基于Entity,而不是IRepositories
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | using Microsoft.Extensions.Options; using System; using System.Collections.Generic; using System.Text; using Volo.Abp.Data; using Volo.Abp.DependencyInjection; namespace MyAPP.MongoDb { [Dependency(ReplaceServices = true )] public class MyAPPMongoDBConnectionStringResolver : DefaultConnectionStringResolver { public MyAPPMongoDBConnectionStringResolver(IOptionsSnapshot<AbpDbConnectionOptions> options) : base (options) { } public override string Resolve( string connectionStringName = null ) { var connStrName = "MyAPPMongoDb" ; if (connectionStringName == connStrName && ! string .IsNullOrWhiteSpace(Options.ConnectionStrings[connStrName])) { return Options.ConnectionStrings[connStrName]; } //Get default value return Options.ConnectionStrings[ "Default" ]; } } } |
20.UserManager
abp-dev\abp\modules\account\src\Volo.Abp.Account.Application\Volo\Abp\Account\AccountAppService.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | public async Task<IActionResult> OnPostAsync( string returnUrl = null ) { returnUrl = returnUrl ?? Url.Content( "~/" ); if (ModelState.IsValid) { var user = new IdentityUser { UserName = Input.Email, Email = Input.Email }; var result = await _userManager.CreateAsync(user, Input.Password); //创建账户 if (result.Succeeded) { _logger.LogInformation( "User created a new account with password." ); var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); //生成邮箱验证码 var callbackUrl = Url.Page( //生成验证的回调地址 "/Account/ConfirmEmail" , pageHandler: null , values: new { userId = user.Id, code = code }, protocol: Request.Scheme); await _emailSender.SendEmailAsync(Input.Email, "Confirm your email" , //发送邮箱验证邮件 $ "Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>." ); await _signInManager.SignInAsync(user, isPersistent: false ); //登录 return LocalRedirect(returnUrl); } foreach ( var error in result.Errors) { ModelState.AddModelError( string .Empty, error.Description); } } // If we got this far, something failed, redisplay form return Page(); } |
ExternalLoginInfo
1 2 3 4 5 6 7 8 | if (!userLoginAlreadyExists) { (await UserManager.AddLoginAsync(user, new UserLoginInfo( externalLoginInfo.LoginProvider, externalLoginInfo.ProviderKey, externalLoginInfo.ProviderDisplayName ))).CheckErrors(); } |
21.[ApiExplorerSettings(IgnoreApi = true)]
abp-dev\abp\modules\account\src\Volo.Abp.Account.Web\Areas\Account\Controllers
22.Autofac.Core.Registration.ComponentNotRegisteredException: The requested service '...MyPermissionDataSeedContributor' has not been registered
can manually add to the DI system or implement the ITransientDependency
interface.
1 | context.Services.AddTransient<MyPermissionDataSeedContributor>(); |
23.项目分离
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public override void Initialize() { IocManager.RegisterAssemblyByConvention( typeof (DMWebHostModule).GetAssembly()); var partManager = IocManager.Resolve<ApplicationPartManager>(); //分离类库里的任意类。 var type = typeof (BIApiController); var assembly = type.Assembly; //判断是否存在 if (!partManager.ApplicationParts.Any(o => o.Name == type.Namespace)) { //添加分离类库的程序集 partManager.ApplicationParts.Add( new AssemblyPart(assembly)); } } } |
24. WithDetails() 可以将sub entity也取出来
You can configure DefaultWithDetailsFunc
for an entity in the ConfigureServices
method of your module in your EntityFrameworkCore
project.
1 2 3 4 5 6 7 | Configure<AbpEntityOptions>(options => { options.Entity<Order>(orderOptions => { orderOptions.DefaultWithDetailsFunc = query => query.Include(o => o.Lines); }); }); |
Then you can use the WithDetails
without any parameter:
1 2 3 4 5 | public async Task TestWithDetails() { var query = _orderRepository.WithDetails(); var orders = await AsyncExecuter.ToListAsync(query); } |
Get list of entities with details
var orders = await _orderRepository.GetListAsync(includeDetails: true);
// var list = _repository.WithDetails(i => i.ItemTiers, j => j.ItemClass).ToList();
如果sub entity里又有entity
1 2 3 4 | options.Entity<TestEntity>(orderOptions => { orderOptions.DefaultWithDetailsFunc = query => query.Include(o => o.SubTestEntity).ThenInclude(x=>x.SubSubTestEntity); }); |
25.批量插入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public class TagRepository : EfCoreRepository<MeowvBlogDbContext, Tag, int >, ITagRepository { public TagRepository(IDbContextProvider<MeowvBlogDbContext> dbContextProvider) : base (dbContextProvider) { } /// <summary> /// 批量插入 /// </summary> /// <param name="tags"></param> /// <returns></returns> public async Task BulkInsertAsync(IEnumerable<Tag> tags) { await DbContext.Set<Tag>().AddRangeAsync(tags); await DbContext.SaveChangesAsync(); } } |
26.Override CreateFilteredQuery
and GetEntityById
in your AppService:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | public class MyAppService : CrudAppService<ParentEntity, ParentEntityDto>, IMyAppService { public MyAppService(IRepository<ParentEntity> repository) : base (repository) { } protected override IQueryable<ParentEntity> CreateFilteredQuery(PagedAndSortedResultRequestDto input) { return Repository.GetAllIncluding(p => p.ChildEntity); } protected override ParentEntity GetEntityById( int id) { var entity = Repository.GetAllIncluding(p => p.ChildEntity).FirstOrDefault(p => p.Id == id); if (entity == null ) { throw new EntityNotFoundException( typeof (ParentEntity), id); } return entity; } } |
Override Create API
1 2 3 4 | public async override Task<ItemDto> CreateAsync(CreateUpdateItemDto input) { return await base .CreateAsync(input); } |
重写排序函数
1 2 3 4 | protected override IQueryable<AuditLog> ApplySorting(IQueryable<AuditLog> query, AuditLogPagedDto input) { return base .ApplySorting(query, input).OrderByDescending(s => s.ExecutionTime); //时间降序 } |
27.扩展 ICurrentUser,登录时增加Claim
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | [Dependency(ReplaceServices = true )] [ExposeServices( typeof (AbpUserClaimsPrincipalFactory))] // 替换旧的AbpUserClaimsPrincipalFactory public class MyUserClaimsPrincipalFactory : AbpUserClaimsPrincipalFactory, IScopedDependency { public MainUserClaimsPrincipalFactory(UserManager<IdentityUser> userManager, RoleManager<IdentityRole> roleManager, IOptions<IdentityOptions> options) : base (userManager, roleManager, options) { } public override async Task<ClaimsPrincipal> CreateAsync(IdentityUser user) { var principal = await base .CreateAsync(user); var identityPrincipal = principal.Identities.First(); /// add custom claim /// identityPrincipal.AddClaim(new Claim(XXX, XXX)); return principal; } } |
28 . Post时候总是报Bad Request 400错误,而Get没问题
[ERR] The required antiforgery cookie ".AspNetCore.Antiforgery.jc6jICwMZA8" is not present.
[INF] Authorization failed for the request at filter 'Volo.Abp.AspNetCore.Mvc.AntiForgery.AbpAutoValidateAntiforgeryTokenAuthorizationFilter'.
解决方法,在 ConfigureServices 禁止AbpAntiForgeryOptions
https://support.abp.io/QA/Questions/595/The-required-antiforgery-cookie-AspNetCoreAntiforgerydXGKnviEebk-is-not-present
https://github.com/abpframework/abp/pull/5864
1 2 3 4 5 | Configure<AbpAntiForgeryOptions>(options => { options.AutoValidate = false ; }); |
29.DynamicEntityPropertyValueManagerExtensions?
30.IAvoidDuplicateCrossCuttingConcerns ?
31. [RemoteService(false)] 和 [DisableAuditing] , 表 AbpAuditLogs 和 AbpAuditLogActions
不想公布某些特殊的接口访问,那么我们可以通过标记 [RemoteService(false)] 进行屏蔽,这样在Web API层就不会公布对应的接口了
默认的一般应用服务层和接口,都是会进行审计记录写入的,如果我们需要屏蔽某些应用服务层或者接口,不进行审计信息的记录,那么需要使用特性标记[DisableAuditing]来管理。
32.API返回创建实体的名字
Entity 和 EntityDto 继承 FullAuditedAggregateRootWithUser<Guid,AppUser>
1 2 3 4 | public class Item : FullAuditedAggregateRootWithUser<Guid,AppUser> { public string Name { get ; set ; } }<br><br> |
1 2 3 4 | public class ItemDto : FullAuditedEntityWithUserDto<Guid, AppUserDto> { public string Name { get ; set ; }<br> } |
1 |
1 | 增加 AutoMapperProfile |
1 | CreateMap<AppUserDto, AppUser>().ReverseMap(); |
默认返回 Creator
1 2 3 4 5 6 7 8 9 | Configure<AbpEntityOptions>(options => { options.Entity<Item>(orderOptions => { orderOptions.DefaultWithDetailsFunc = query => query.Include(o=>o.Creator); }); }); |
注意:这里千万不能改 AbpIdentityDbProperties.DbTablePrefix , 不然就linkage不到 AbpUsers
继承类的时候用 AppUser, 但用户数据是保存在 AbpUsers 里,两个entity share同样的properties
builder.Entity<AppUser>(b => { b.ToTable(AbpIdentityDbProperties.DbTablePrefix + "Users"); //Sharing the same table "AbpUsers" with the IdentityUser b.ConfigureByConvention(); b.ConfigureAbpUser(); /* Configure mappings for your additional properties * Also see the DRSEfCoreEntityExtensionMappings class */ });
33 . PermissionCheck & AuthorizationHandlerContext
abp\modules\blogging\src\Volo.Blogging.Application\Volo\Blogging\Comments\CommentAuthorizationHandler.cs
34. File upload IFileAppService
abp\modules\blogging\src\Volo.Blogging.Application\Volo\Blogging\Files\FileAppService.cs
35. AbpBlobStoringOptions
abp\modules\blob-storing-database\src\Volo.Abp.BlobStoring.Database.Domain\Volo\Abp\BlobStoring\Database\BlobStoringDatabaseDomainModule.cs
36.日志级别
1 2 3 4 5 6 | public class MyException : Exception, IHasLogLevel { public LogLevel LogLevel { get ; set ; } = LogLevel.Warning; //... } |
37. SoftDelete
1 2 3 4 5 6 7 8 | var people1 = _personRepository.GetAllList(); using (_unitOfWorkManager.Current.DisableFilter(AbpDataFilters.SoftDelete)) { var people2 = _personRepository.GetAllList(); } var people3 = _personRepository.GetAllList(); |
设想这样的场景,当user删除后,entity对应的creator和lastModifier就找不到对应的record,只能返回空值。
需要单独为AppUser不启用SoftDelete的feature,可以override DbContext的CreateFilterExpression
protected override Expression<Func<TEntity, bool>> CreateFilterExpression<TEntity>() { //var expression = base.CreateFilterExpression<TEntity>(); Expression<Func<TEntity, bool>> expression = null; if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity))) { if (typeof(TEntity).Name != typeof(AppUser).Name) { expression = e => !IsSoftDeleteFilterEnabled || !EF.Property<bool>(e, "IsDeleted"); } } if (typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity))) { Expression<Func<TEntity, bool>> multiTenantFilter = e => !IsMultiTenantFilterEnabled || EF.Property<Guid>(e, "TenantId") == CurrentTenantId; expression = expression == null ? multiTenantFilter : CombineExpressions(expression, multiTenantFilter); } if (typeof(IActiveObject).IsAssignableFrom(typeof(TEntity))) { Expression<Func<TEntity, bool>> isActiveFilter = e => !IsActiveFilterEnabled || EF.Property<bool>(e, "Active"); expression = expression == null ? isActiveFilter : CombineExpressions(expression, isActiveFilter); } return expression; }
38.Setting Filter Parameters
CurrentUnitOfWork.SetFilterParameter("PersonFilter", "personId", 42);
39.ExtensibleObject
Dictionary<string, object> ExtraProperties { get; }
user.SetProperty("Title", "My Title");
40.Public Const
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public static class IdentityPermissions { public const string GroupName = "AbpIdentity" ; public static class Roles { public const string Default = GroupName + ".Roles" ; public const string Create = Default + ".Create" ; public const string Update = Default + ".Update" ; public const string Delete = Default + ".Delete" ; public const string ManagePermissions = Default + ".ManagePermissions" ; } public static string [] GetAll() { return ReflectionHelper.GetPublicConstantsRecursively( typeof (IdentityPermissions)); } } |
41.Timezone
Abp.Timing.TimeZone
42. 为了在create entity的时候,把LastModifierId也一起赋值,Override CrudAppService
将service 继承自 MyCrudAppServiceCrudAppService
[Dependency(ReplaceServices = true)] public abstract class MyCrudAppServiceCrudAppService<TEntity, TEntityDto, TKey, TGetListInput, TCreateInput, TUpdateInput> : CrudAppService<TEntity, TEntityDto, TEntityDto, TKey, TGetListInput, TCreateInput, TUpdateInput>, ITransientDependency where TEntity : class, IEntity<TKey> where TEntityDto : IEntityDto<TKey> { protected MyCrudAppServiceCrudAppService(IRepository<TEntity, TKey> repository) : base(repository) { } public override async Task<TEntityDto> CreateAsync(TCreateInput input) { await CheckCreatePolicyAsync(); var entity = await MapToEntityAsync(input); TryToSetTenantId(entity); await Repository.InsertAsync(entity, autoSave: true); TryToSetLastModifierId(entity); return await MapToGetOutputDtoAsync(entity); } protected virtual void TryToSetLastModifierId(TEntity entity) { var propertyInfo = entity.GetType().GetProperty("LastModifierId"); if (propertyInfo == null || propertyInfo.GetSetMethod(true) == null) { return; } propertyInfo.SetValue(entity, CurrentUser.Id); } }
43. Run Test Project的时候如果entity有继承 FullAuditedAggregateRootWithUser<Guid,AppUser> 就会报错
Microsoft.EntityFrameworkCore.DbUpdateException : An error occurred while updating the entries. See the inner exception for details.
---- Microsoft.Data.Sqlite.SqliteException : SQLite Error 19: 'FOREIGN KEY constraint failed'.
这是因为默认会加foreign key到表appuser,但由于实际用到的是abpusers(IdentityUser),所以外键约束导致出错
解决办法是在
namespace DRS.EntityFrameworkCore 里配置ignore Creator/LastModifier/Deleter
1 2 3 4 5 6 7 8 9 10 11 | builder.Entity<Item>(b => { b.ToTable(DRSConsts.DbTablePrefix + "Tests" , DRSConsts.DbSchema); b.ConfigureByConvention(); b.Ignore(i => i.Creator); b.Ignore(i => i.LastModifier); b.Ignore(i => i.Deleter); /* Configure more properties here */ }); |
44.隐藏API
https://support.abp.io/QA/Questions/264/How-to-hide-an-endpoint-from-Swagger
private void ConfigureSwaggerServices(IServiceCollection services) { services.AddSwaggerGen( options => { options.SwaggerDoc("v1", new OpenApiInfo {Title = "MyProjectName API", Version = "v1"}); options.DocInclusionPredicate((docName, description) => true); options.CustomSchemaIds(type => type.FullName); options.DocumentFilter<HideOrganizationUnitsFilter>(); //<-------- added this ----------- } ); }
class HideOrganizationUnitsFilter : IDocumentFilter { private const string pathToHide = "/identity/organization-units"; public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) { var organizationUnitPaths = swaggerDoc .Paths .Where(pathItem => pathItem.Key.Contains(pathToHide, StringComparison.OrdinalIgnoreCase)) .ToList(); foreach (var item in organizationUnitPaths) { swaggerDoc.Paths.Remove(item.Key); } } }
45.string encrypt
abp-dev\abp\framework\test\Volo.Abp.Security.Tests\Volo\Abp\Security\Encryption\StringEncryptionService_Tests.cs
public void Should_Enrypt_And_Decrpyt_With_Default_Options(string plainText) { _stringEncryptionService .Decrypt(_stringEncryptionService.Encrypt(plainText)) .ShouldBe(plainText); }
46.定时清理AuditLog
public class DeleteOldAuditLogsWorker : AsyncPeriodicBackgroundWorkerBase, ISingletonDependency { private readonly IRepository<AuditLog> _auditLogRepository; public DeleteOldAuditLogsWorker(AbpTimer timer, IServiceScopeFactory serviceScopeFactory, IRepository<AuditLog> auditLogRepository) : base( timer, serviceScopeFactory) { _auditLogRepository = auditLogRepository; Timer.Period = 5000; } [UnitOfWork] protected async override Task DoWorkAsync(PeriodicBackgroundWorkerContext workerContext) { Logger.LogInformation("---------------- DeleteOldAuditLogsWorker 正在工作 ----------------"); var dt = new DateTime(2021,2,2); await _auditLogRepository.DeleteAsync(log => log.ExecutionTime <= dt); } }
public override void OnApplicationInitialization(ApplicationInitializationContext context) { context.ServiceProvider .GetRequiredService<IBackgroundWorkerManager>() .Add( context.ServiceProvider.GetRequiredService<DeleteOldAuditLogsWorker>() ); }
这里用到的 AsyncPeriodicBackgroundWorkerBase 跟abp的 BackgroundJobs 不是一回事
后台作业与后台工作者的区别是,前者主要用于某些耗时较长的任务,而不想阻塞用户的时候所使用。后者主要用于周期性的执行某些任务,从 “工作者” 的名字可以看出来,就是一个个工人,而且他们每个工人都拥有单独的后台线程。如果是多台机器,就会导致执行多次。
BackgroundJobs 的用法是继承 BackgroundJob<WriteToConsoleGreenJobArgs> , 可以用IBackgroundJobManager调用,也可以在数据库中插入一条数据调用
执行成功的话数据会清除,不成功就留在数据库中 IsAbandoned 为1
1 2 | insert into [AbpBackgroundJobs](id,IsAbandoned,JobName,JobArgs,CreationTime,NextTryTime ) values (newid(),0, 'GreenJob' , '{"Value":"test args"}' ,getdate(), '2021-03-06 15:03' ) |
WriteToConsoleGreenJob
public class WriteToConsoleGreenJob : BackgroundJob<WriteToConsoleGreenJobArgs>, ITransientDependency { public override void Execute(WriteToConsoleGreenJobArgs args) { if (RandomHelper.GetRandom(0, 100) < 70) { //throw new ApplicationException("A sample exception from the WriteToConsoleGreenJob!"); } lock (Console.Out) { var oldColor = Console.ForegroundColor; Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine(); Console.WriteLine($"############### WriteToConsoleGreenJob: {args.Value} - {args.Time:HH:mm:ss} ###############"); Console.WriteLine(); Logger.LogInformation("WriteToConsoleGreenJob"); Console.ForegroundColor = oldColor; } } }
WriteToConsoleGreenJobArgs
[BackgroundJobName("GreenJob")] public class WriteToConsoleGreenJobArgs { public string Value { get; set; } public DateTime Time { get; set; } public WriteToConsoleGreenJobArgs() { Time = DateTime.Now; } }
SampleJobCreator
public class SampleJobCreator : ITransientDependency { private readonly IBackgroundJobManager _backgroundJobManager; public SampleJobCreator(IBackgroundJobManager backgroundJobManager) { _backgroundJobManager = backgroundJobManager; } public void CreateJobs() { AsyncHelper.RunSync(CreateJobsAsync); } public async Task CreateJobsAsync() { await _backgroundJobManager.EnqueueAsync(new WriteToConsoleGreenJobArgs { Value = "test 1 (green)" }); await _backgroundJobManager.EnqueueAsync(new WriteToConsoleGreenJobArgs { Value = "test 2 (green)" }); await _backgroundJobManager.EnqueueAsync(new WriteToConsoleYellowJobArgs { Value = "test 1 (yellow)" }); await _backgroundJobManager.EnqueueAsync(new WriteToConsoleYellowJobArgs { Value = "test 2 (yellow)" }); } }
47.数据过滤
https://docs.abp.io/zh-Hans/abp/latest/Data-Filtering
- 添加
IsActiveFilterEnabled
属性用于检查是否启用了IIsActive
. 内部使用了之前介绍到的IDataFilter
服务. - 重写
ShouldFilterEntity
和CreateFilterExpression
方法检查给定实体是否实现IIsActive
接口,在必要时组合表达式.
48.获取DbContext的全部entity
// DbContextHelper public static IEnumerable<Type> GetEntityTypes(Type dbContextType) { return from property in dbContextType.GetTypeInfo().GetProperties(BindingFlags.Public | BindingFlags.Instance) where ReflectionHelper.IsAssignableToGenericType(property.PropertyType, typeof(DbSet<>)) && typeof(IEntity).IsAssignableFrom(property.PropertyType.GenericTypeArguments[0]) select property.PropertyType.GenericTypeArguments[0]; }
var entityTypes = GetEntityTypes(typeof(DRSDbContext)); foreach(Type entityType in entityTypes) { if (entityType.IsAssignableFrom(typeof(FullAuditedAggregateRoot<Guid>))) { //EfCoreRepositoryRegistrar.GetRepositoryType var repositoryType = typeof(EfCoreRepository<,,>).MakeGenericType(typeof(DRSDbContext), entityType, typeof(Guid)); //repositoryType.GetConstructor var method = repositoryType .MakeGenericType(entityType) .GetMethod( nameof(WithDetails) ); //var result = method.Invoke(repositoryObject,method); } }
49.数据过滤 Data Filtering
https://docs.abp.io/en/abp/2.3/Data-Filtering
按照文档加上Expression在DbContext后,可以在MyCrudAppService里手动enable给每个entity的AppService启动过滤功能
abp-dev\abp\framework\src\Volo.Abp.Data\Volo\Abp\Data\IDataFilter.cs
GetExpression()的写法无效,也不知为什么
//public static Expression<Func<TEntity, bool>> GetExpression() // //where TEntity : IEntity<TKey> //{ // var lambdaParam = Expression.Parameter(typeof(TEntity)); // var leftExpression = Expression.PropertyOrField(lambdaParam, "IsDeleted"); // // var idValue = Convert.ChangeType(id, typeof(TKey)); // Expression<Func<object>> closure = () => false; // idValue; // var rightExpression = Expression.Convert(closure.Body, leftExpression.Type); // var lambdaBody = Expression.Equal(leftExpression, rightExpression); // return Expression.Lambda<Func<TEntity, bool>>(lambdaBody, lambdaParam); //} public async Task PostActive() { // Expression<Func<TEntity, bool>> expression = GetExpression(); using (GetFilter<IActiveObject>().Enable()) { var entities = Repository.WithDetails(); } await Task.CompletedTask; } private IDataFilter<TFilter> GetFilter<TFilter>() where TFilter : class { var _filters = ServiceProvider.GetRequiredService<IDataFilter<IActiveObject>>() as IDataFilter<TFilter>; return _filters; }
50.并发锁
abp-dev\abp\framework\src\Volo.Abp.Caching\Volo\Abp\Caching\DistributedCache.cs
SyncSemaphore = new SemaphoreSlim(1, 1);
https://cloud.tencent.com/developer/article/1641967
51.BulkInsert
1 2 3 4 5 | public async Task BulkInsertAsync(IEnumerable<Item> items) { await DbContext.Set<Item>().AddRangeAsync(items); await DbContext.SaveChangesAsync(); } |
52.加密
IStringEncryptionService
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)