從架構角度來講,ApplicationService究竟應該如何定位,一種說法是直接對應用例UseCase, 也就是直接對應UI, 這個UI是廣義的,不僅僅是瀏覽器的頁面,也包括API調用。還是從我曾經踩過的一個坑說起吧:
public class ProductImportService : AdvancedAsyncCrudAppService<Product, ProductDto, PagedResultRequestDto> , IProductImportService { ......public ProductImportService(...... ) : base(productRepository) { ...... } //[MyIgnoreApiAttribute] //[DisableValidation] //[DisableAuditing] private void SaveProductAndTestSizeHead( RawData rawData ) { ...... } //[DisableValidation] //[DisableAuditing] private void SaveStandardSizeValue( RawData rawData) { ...... } //[DisableValidation] //[DisableAuditing] private void SaveTestSizeInfo( RawData rawData) { ...... } private void SaveTestSizeValue( RawData rawData) { List<TestSizeValue> newTestSizeValues = rawData.TestSizeValues; var firstEntity = newTestSizeValues.FirstOrDefault(); if (firstEntity == null) { return; } var dbExistEntities = _testSizeValueValueRepository.GetAllIncluding( os => os.TestSizeInfo , os => os.TestSizeInfo.TestSizeHead , os => os.StandardSizeValue ) .Where(os => os.TestSizeInfo.TestSizeHead.Id == rawData.TestSizeHead.Id && os.IsAim == firstEntity.IsAim ).ToList(); FilterValues(newTestSizeValues, dbExistEntities, out List<TestSizeValue> newEnities, out List<TestSizeValue> updateEnities); foreach( var entity in newEnities) { entity.TestSizeInfo = rawData.TestSizeInfos.FirstOrDefault(tsi => tsi.IsSame(entity.TestSizeInfo)); entity.StandardSizeValue = rawData.StandardSizeValues.FirstOrDefault(ssv => ssv.IsSame(entity.StandardSizeValue)); } _testSizeValueValueRepository.BulkInsert(newEnities); // 批量插入新的 foreach( var updateEntity in updateEnities) { _testSizeValueValueRepository.Update(updateEntity); // 修改已存在的 } rawData.TestSizeValues = newTestSizeValues; } private void FilterValues(List<TestSizeValue> testSizeValues, List<TestSizeValue> dbExistEntities, out List<TestSizeValue> newEnities, out List<TestSizeValue> updateEnities) { ...... } public RawData Save(RawData rawData) { SaveProductAndTestSizeHead( rawData); SaveStandardSizeValue( rawData); SaveTestSizeInfo( rawData); SaveTestSizeValue( rawData); return rawData; }
這是一個從Excel文件中導入數據的場景,每個文件的數據是個矩陣,有50多列,有30多行,數據有50x30=1500個左右,導入場景性能是個關鍵因素,因為它決定了單位時間內能處理多少個Excel文件,調試時發現每個文件的處理時間是90秒左右,首先想到的方案是改用批量插入,改善到10秒左右,再也沒法改善了。於是在各個地方加了時間計算,終於發現問題出在哪里了,其實瓶頸並不在數據庫操作,而是在方法執行前,也就是ABP攔截器里消耗的時間,這個攔截器就是Audit Logging : User, browser, IP address, calling service, method, parameters, calling time, execution duration and some other informations are automatically saved for each request based on conventions and configurations. 審計全記錄,最耗時的是記錄 parameters,每次記錄都要序列化(用的是Json),如果是大數據庫的化,這塊是非常非常耗時的!后來仔細研究ABP源碼,其實很簡單
public sealed class AbpKernelModule : AbpModule { public override void PreInitialize() { IocManager.AddConventionalRegistrar(new BasicConventionalRegistrar()); IocManager.Register<IScopedIocResolver, ScopedIocResolver>(DependencyLifeStyle.Transient); ValidationInterceptorRegistrar.Initialize(IocManager); AuditingInterceptorRegistrar.Initialize(IocManager); UnitOfWorkRegistrar.Initialize(IocManager); AuthorizationInterceptorRegistrar.Initialize(IocManager); Configuration.Auditing.Selectors.Add( new NamedTypeSelector( "Abp.ApplicationServices", type => typeof(IApplicationService).IsAssignableFrom(type) ) ); ...... } ...... }
ABP是通過攔截器的方式,注入了代碼(功能),ValidationInterceptor 驗證攔截器、AuditingInterceptor 審計攔截器、AuthorizationInterceptor 認證攔截器,AuditingInterceptor 審計攔截器會攔截所有ApplicationServices
Configuration.Auditing.Selectors.Add( new NamedTypeSelector( "Abp.ApplicationServices", type => typeof(IApplicationService).IsAssignableFrom(type) ) );
AuditingInterceptor 審計攔截器,有與其配套的Attribute,來實現申明式Enable/Disable
namespace Abp.Auditing { internal class AuditingInterceptor : IInterceptor { ......public void Intercept(IInvocation invocation) { if (AbpCrossCuttingConcerns.IsApplied(invocation.InvocationTarget, AbpCrossCuttingConcerns.Auditing)) { invocation.Proceed(); return; } if (!_auditingHelper.ShouldSaveAudit(invocation.MethodInvocationTarget)) { invocation.Proceed(); return; } var auditInfo = _auditingHelper.CreateAuditInfo(invocation.MethodInvocationTarget, invocation.Arguments); if (AsyncHelper.IsAsyncMethod(invocation.Method)) { PerformAsyncAuditing(invocation, auditInfo); } else { PerformSyncAuditing(invocation, auditInfo); } } ...... } }
public class AuditingHelper : IAuditingHelper, ITransientDependency { ......public bool ShouldSaveAudit(MethodInfo methodInfo, bool defaultValue = false) { if (!_configuration.IsEnabled) { return false; } if (!_configuration.IsEnabledForAnonymousUsers && (AbpSession?.UserId == null)) { return false; } if (methodInfo == null) { return false; } if (!methodInfo.IsPublic) { return false; } if (methodInfo.IsDefined(typeof(AuditedAttribute), true)) { return true; } if (methodInfo.IsDefined(typeof(DisableAuditingAttribute), true)) { return false; } var classType = methodInfo.DeclaringType; if (classType != null) { if (classType.IsDefined(typeof(AuditedAttribute), true)) { return true; } if (classType.IsDefined(typeof(DisableAuditingAttribute), true)) { return false; } if (_configuration.Selectors.Any(selector => selector.Predicate(classType))) { return true; } } return defaultValue; }
所以我的,第一個解決方案是:
[DisableValidation]
[DisableAuditing]
但經過仔細分析,其實在導入這個場景中,Save保存數據到DB, 其實不是UseCase用例,而Import才是UseCase, Save只是Import的一個步驟; Import 的第一步是Parse解析Excel文件,第二步才是Save; 因此Save不應該作為ApplicationService(比較重的服務,ABP會自動注入很多關切),上策應該把Save作為DomainServie(輕量級服務,ABP不會自動注入很多東西),以下是Excel導入Save的最終解決方案:
public class ProductImportService :DomainService , IProductImportService { ...... public ProductImportService(...... ) : base(productRepository) { ...... } //[MyIgnoreApiAttribute] //[DisableValidation] //[DisableAuditing] private void SaveProductAndTestSizeHead( RawData rawData ) { ...... } //[DisableValidation] //[DisableAuditing] private void SaveStandardSizeValue( RawData rawData) { ...... } //[DisableValidation] //[DisableAuditing] private void SaveTestSizeInfo( RawData rawData) { ...... } private void SaveTestSizeValue( RawData rawData) { ...... } private void FilterValues(List<TestSizeValue> testSizeValues, List<TestSizeValue> dbExistEntities, out List<TestSizeValue> newEnities, out List<TestSizeValue> updateEnities) { ...... } public RawData Save(RawData rawData) { SaveProductAndTestSizeHead( rawData); SaveStandardSizeValue( rawData); SaveTestSizeInfo( rawData); SaveTestSizeValue( rawData); return rawData; }
總結,ApplicationService 很強大,但也要合適的使用,分清ApplicationService和DomainServie的適合場景,也許是ABP或DDD的一個重要的架構選擇!