بعد از انتشار
قسمت 6 به عنوان آخرین قسمت مرتبط با تفکر مبتنیبر CRUD (CRUD-based thinking) قصد دارم پشتیبانی از طراحی Application Layer مبتنیبر CQRS را نیز به این زیرساخت اضافه کنم. در این مطلب تغییرات حاصل از طراحی مجدد و بازسازی انجام شده در نسخه جدید را مرور خواهیم کرد.
تغییرات کتابخانه DNTFrameworkCore
1- واسطهای مورد استفاده جهت ردیابی موجودیتها :
public interface ICreationTracking
{
DateTime CreatedDateTime { get; set; }
}
public interface IModificationTracking
{
DateTime? ModifiedDateTime { get; set; }
}
علاوه بر تغییر نام و نوع داده خصوصیتهای تاریخ ایجاد و ویرایش، سایر خصوصیات به صورت خواص سایهای در کتابخانه DNTFrameworkCore.EFCore مدیریت خواهند شد.
public interface IHasRowIntegrity
{
string Hash { get; set; }
}
public interface IHasRowVersion
{
byte[] Version { get; set; }
}
3- ارثبری از کلاس AggregateRoot در سناریوهای CRUD و در زمان استفاده از CrudService هیچ ضرورتی ندارد و صرفا برای پشتیبانی از طراحی مبتنیبر DDD کاربرد خواهد داشت. اگر قصد طراحی یک Rich Domain Model را دارید و رویکرد DDD را دنبال میکنید، با استفاده از کلاس پایه AggregateRoot امکان مدیریت DomainEventهای مرتبط با یک Aggregate را خواهید داشت.
public abstract class AggregateRoot<TKey> : Entity<TKey>, IAggregateRoot
where TKey : IEquatable<TKey>
{
private readonly List<IDomainEvent> _events = new List<IDomainEvent>();
public IReadOnlyCollection<IDomainEvent> Events => _events.AsReadOnly();
protected virtual void AddDomainEvent(IDomainEvent newEvent)
{
_events.Add(newEvent);
}
public virtual void ClearEvents()
{
_events.Clear();
}
}
4- امکان Publish رخدادهای مرتبط با یک AggregateRoot به IEventBus اضافه شده است:
public static class EventBusExtensions
{
public static Task TriggerAsync(this IEventBus bus, IEnumerable<IDomainEvent> events)
{
var tasks = events.Select(async domainEvent => await bus.TriggerAsync(domainEvent));
return Task.WhenAll(tasks);
}
public static async Task PublishAsync(this IEventBus bus, IAggregateRoot aggregateRoot)
{
await bus.TriggerAsync(aggregateRoot.Events);
aggregateRoot.ClearEvents();
}
}
5- واسط IDbSeed به IDbSetup تغییر نام پیدا کرده است.
6- اضافه شدن یک سرویس برای ذخیرهسازی اطلاعات به صورت Key/Value در بانک اطلاعاتی:
public interface IKeyValueService : IApplicationService
{
Task SetValueAsync(string key, string value);
Task<Maybe<string>> LoadValueAsync(string key);
Task<bool> IsTamperedAsync(string key);
}
public class KeyValue : Entity, IModificationTracking, ICreationTracking, IHasRowIntegrity
{
public string Key { get; set; }
[Encrypted] public string Value { get; set; }
public string Hash { get; set; }
public DateTime CreatedDateTime { get; set; }
public DateTime? ModifiedDateTime { get; set; }
}
7- AuthorizationProvider حذف شده و جمع آوری دسترسیهای سیستم به عهده خود استفاده کننده از این زیرساخت میباشد.
8- اضافه شدن امکان Exception Mapping و همچنین سفارشی سازی پیغامهای خطای عمومی:
public class ExceptionOptions
{
public List<ExceptionMapItem> Mappings { get; } = new List<ExceptionMapItem>();
[Required] public string DbException { get; set; }
[Required] public string DbConcurrencyException { get; set; }
[Required] public string InternalServerIssue { get; set; }
public bool TryFindMapping(DbException dbException, out ExceptionMapItem mapping)
{
mapping = null;
var words = new HashSet<string>(Regex.Split(dbException.ToStringFormat(), @"\W"));
var mappingItem = Mappings.FirstOrDefault(a => a.Keywords.IsProperSubsetOf(words));
if (mappingItem == null)
{
return false;
}
mapping = mappingItem;
return true;
}
}
و روش استفاده از آن را در پروژه DNTFrameworkCore.TestAPI میتوانید مشاهده کنید. برای معرفی نگاشتها، میتوان به شکل زیر در فایل appsetting.json عمل کرد:
"Exception": {
"Mappings": [
{
"Message": "به دلیل وجود اطلاعات وابسته امکان حذف وجود ندارد",
"Keywords": [
"DELETE",
"REFERENCE"
]
},
{
"Message": "یک تسک با این عنوان قبلا در سیستم ثبت شده است",
"MemberName": "Title",
"Keywords": [
"Task",
"UIX_Task_NormalizedTitle"
]
}
],
"DbException": "امکان ذخیرهسازی اطلاعات وجود ندارد؛ دوباره تلاش نمائید",
"DbConcurrencyException": "اطلاعات توسط کاربری دیگر در شبکه تغییر کرده است",
"InternalServerIssue": "متأسفانه مشکلی در فرآیند انجام درخواست شما پیش آمده است!"
}
8- اطلاعات مرتبط با مستأجر جاری در سناریوهای چند مستأجری از واسط IUserSession حذف شده و به واسط ITenantSession منتقل شده است. نوع داده خصوصیت UserId به String تغییر پیدا کرده و بر اساس نیاز میتوان به شکل زیر از آن استفاده کرد:
_session.UserId
_session.UserId<long>()
_session.UserId<int>()
_session.UserId<Guid>()
علاوه بر آن خصوصیت ImpersonatorUserId که میتواند حاوی UserId کاربری باشد که در نقش کاربر دیگری در سناریوهای Impersonation وارد سیستم شده است؛ این مورد در سیستم Logging مبتنیبر فایل سیستم و بانک اطلاعاتی موجود در این زیرساخت، ثبت و نگهداری میشود.
9- لیست ClaimTypeهای مورد استفاده در این زیرساخت:
public static class UserClaimTypes
{
public const string UserName = ClaimTypes.Name;
public const string UserId = ClaimTypes.NameIdentifier;
public const string SerialNumber = ClaimTypes.SerialNumber;
public const string Role = ClaimTypes.Role;
public const string DisplayName = nameof(DisplayName);
public const string BranchId = nameof(BranchId);
public const string BranchName = nameof(BranchName);
public const string IsHeadOffice = nameof(IsHeadOffice);
public const string TenantId = nameof(TenantId);
public const string TenantName = nameof(TenantName);
public const string IsHeadTenant = nameof(IsHeadTenant);
public const string Permission = nameof(Permission);
public const string PackedPermission = nameof(PackedPermission);
public const string ImpersonatorUserId = nameof(ImpersonatorUserId);
public const string ImpersonatorTenantId = nameof(ImpersonatorTenantId);
}
از خصوصیات Branch* برای سناریوهای چند شعبهای میتوان استفاده کرد که در این صورت اگر یکی از شعب به عنوان دفتر مرکزی در نظر گرفته شود باید Claimای با نام IsHeadOffice با مقدار true از زمان ورود به سیستم برای کاربران آن شعبه در نظر گرفته شود.
خصوصیات Tenant* برای سناریوهای چند مستأجری در نظر گرفته شده است که اگرطراحی مورد نظرتان به نحوی باشد که بخش مدیریت مستأجرهای سیستم در همان سیستم پیادهسازی شده باشد یا به تعبیری سیستم Host و Tenant یکی باشند، میتوان Claimای با نام IsHeadTenant با مقدار true در زمان ورود به سیستم برای کاربران Host (مستأجر اصلی) در نظر گرفته شود.
10- مکانیزم Logging مبتنیبر فایل سیستم:
/// <summary>
/// Adds a file logger named 'File' to the factory.
/// </summary>
/// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param>
public static ILoggingBuilder AddFile(this ILoggingBuilder builder)
{
builder.Services.AddSingleton<ILoggerProvider, FileLoggerProvider>();
return builder;
}
/// <summary>
/// Adds a file logger named 'File' to the factory.
/// </summary>
/// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param>
/// <param name="configure">Configure an instance of the <see cref="FileLoggerOptions" /> to set logging options</param>
public static ILoggingBuilder AddFile(this ILoggingBuilder builder, Action<FileLoggerOptions> configure)
{
builder.AddFile();
builder.Services.Configure(configure);
return builder;
}
11- امکان TenantResolution برای شناسایی مستأجر جاری سیستم:
public interface ITenantResolutionStrategy
{
string TenantId();
}
public interface ITenantStore
{
Task<Tenant> FindTenantAsync(string tenantId);
}
از این واسطها در میان افزار TenantResolutionMiddleware موجود در کتابخانه DNTFrameworkCore.Web.Tenancy استفاده شده است. و همچنین جهت دسترسی به اطلاعات مستأجر جاری سیستم میتوان واسط زیر را تزریق و استفاده کرد:
public interface ITenantSession : IScopedDependency
{
/// <summary>
/// Gets current TenantId or null.
/// This TenantId should be the TenantId of the <see cref="IUserSession.UserId" />.
/// It can be null if given <see cref="IUserSession.UserId" /> is a head-tenant user or no user logged in.
/// </summary>
string TenantId { get; }
/// <summary>
/// Gets current TenantName or null.
/// This TenantName should be the TenantName of the <see cref="IUserSession.UserId" />.
/// It can be null if given <see cref="IUserSession.UserId" /> is a head-tenant user or no user logged in.
/// </summary>
string TenantName { get; }
/// <summary>
/// Represents current tenant is head-tenant.
/// </summary>
bool IsHeadTenant { get; }
/// <summary>
/// TenantId of the impersonator.
/// This is filled if a user with <see cref="IUserSession.ImpersonatorUserId" /> performing actions behalf of the
/// <see cref="IUserSession.UserId" />.
/// </summary>
string ImpersonatorTenantId { get; }
}
12- استفاده از SystemTime و IClock برای افزایش تستپذیری سناریوهای درگیر با DateTime:
public static class SystemTime
{
public static Func<DateTime> Now = () => DateTime.UtcNow;
public static Func<DateTime, DateTime> Normalize = (dateTime) =>
DateTime.SpecifyKind(dateTime, DateTimeKind.Utc);
}
public interface IClock : ITransientDependency
{
DateTime Now { get; }
DateTime Normalize(DateTime dateTime);
}
internal sealed class Clock : IClock
{
public DateTime Now => SystemTime.Now();
public DateTime Normalize(DateTime dateTime)
{
return SystemTime.Normalize(dateTime);
}
}
13- تغییر واسط عمومی کلاس Result:
public class Result
{
private static readonly Result _ok = new Result(false, string.Empty);
private readonly List<ValidationFailure> _failures;
protected Result(bool failed, string message) : this(failed, message,
Enumerable.Empty<ValidationFailure>())
{
Failed = failed;
Message = message;
}
protected Result(bool failed, string message, IEnumerable<ValidationFailure> failures)
{
Failed = failed;
Message = message;
_failures = failures.ToList();
}
public bool Failed { get; }
public string Message { get; }
public IEnumerable<ValidationFailure> Failures => _failures.AsReadOnly();
[DebuggerStepThrough]
public static Result Ok() => _ok;
[DebuggerStepThrough]
public static Result Fail(string message)
{
return new Result(true, message);
}
//...
}
روش معرفی سرویسهای مرتبط با کتابخانه DNTFrameworkCore
services.AddFramework()
.WithModelValidation()
.WithFluentValidation()
.WithMemoryCache()
.WithSecurityService()
.WithBackgroundTaskQueue()
.WithRandomNumber();
متد WithFluentValidation یک متد الحاقی برای FrameworkBuilder میباشد که در کتابخانه DNTFrameworkCore.FluentValidation تعریف شده است.
تغییرات کتابخانه DNTFrameworkCore.EFCore
1- اگر از CrudService پایه موجود استفاده میکنید، محدودیت ارثبری از TrackableEntity از موجودیت اصلی برداشته شده است. همچنین همانطور که در نظرات مطالب قبلی در قالب نکته تکمیلی اشاره شد، متد MapToEntity به نحوی تغییر کرد که پاسخگوی اکثر نیازها باشد.
2- امکان تنظیم ModifiedProperties برای موجودیتهای وابسته در سناریوهایی با موجودیتهای وابسته Master-Detail نیز مهیا شده است.
public abstract class TrackableEntity<TKey> : Entity<TKey>, ITrackable where TKey : IEquatable<TKey>
{
[NotMapped] public TrackingState TrackingState { get; set; }
[NotMapped] public ICollection<string> ModifiedProperties { get; set; }
}
public static class ConfigurationBuilderExtensions
{
public static IConfigurationBuilder AddEFCore(this IConfigurationBuilder builder,
IServiceProvider provider)
{
return builder.Add(new EFConfigurationSource(provider));
}
}
4- واسط IHookEngine حذف شده و سازنده کلاس پایه DbContextCore لیستی از IHook را به عنوان پارامتر میپذیرد:
protected DbContextCore(DbContextOptions options, IEnumerable<IHook> hooks) : base(options)
{
_hooks = hooks ?? throw new ArgumentNullException(nameof(hooks));
}
همچنین امکان IgnoreHook برای غیرفعال کردن یک Hook خاص با استفاده از نام آن مهیا شده است:
public void IgnoreHook(string hookName)
{
_ignoredHookList.Add(hookName);
}
امکان پیاده سازی Hook سفارشی را برای سناریوهای خاص هم با پیاده سازی واسط IHook و یا با ارثبری از کلاسهای پایه موجود در زیرساخت، خواهید داشت. به عنوان مثال:
internal sealed class RowIntegrityHook : PostActionHook<IHasRowIntegrity>
{
public override string Name => HookNames.RowIntegrity;
public override int Order => int.MaxValue;
public override EntityState HookState => EntityState.Unchanged;
protected override void Hook(IHasRowIntegrity entity, HookEntityMetadata metadata, IUnitOfWork uow)
{
metadata.Entry.Property(EFCore.Hash).CurrentValue = uow.EntityHash(entity);
}
}
در بازطراحی انجام شده، دسترسی به وهله جاری DbContext هم از طریق واسط IUnitOfWork مهیا شده است.
5- متد EntityHash به واسط IUnitOfWork اضافه شده است که امکان محاسبه هش مرتبط با یک رکورد از یک موجودیت خاص را مهیا میکند؛ همچنین امکان تغییر الگوریتم و سفارشی سازی آن را به شکل زیر خواهید داشت:
//DbContextCore : IUnitOfWork
public string EntityHash<TEntity>(TEntity entity) where TEntity : class
{
var row = Entry(entity).ToDictionary(p => p.Metadata.Name != EFCore.Hash &&
!p.Metadata.ValueGenerated.HasFlag(ValueGenerated.OnUpdate) &&
!p.Metadata.IsShadowProperty());
return EntityHash<TEntity>(row);
}
protected virtual string EntityHash<TEntity>(Dictionary<string, object> row) where TEntity : class
{
var json = JsonConvert.SerializeObject(row, Formatting.Indented);
using (var hashAlgorithm = SHA256.Create())
{
var byteValue = Encoding.UTF8.GetBytes(json);
var byteHash = hashAlgorithm.ComputeHash(byteValue);
return Convert.ToBase64String(byteHash);
}
}
همچنین از طریق متدهای الحاقی زیر که مرتبط با واسط IUnitOfWork میباشند، امکان دسترسی به رکوردهای دستکاری شده را خواهید داشت:
IsTamperedAsync
HasTamperedEntryAsync
TamperedEntryListAsync
6- همانطور که اشاره شد، خواص سایهای مرتبط با سیستم ردیابی موجودیتها نیز به شکل زیر تغییر نام پیدا کردهاند:
public const string CreatedDateTime = nameof(ICreationTracking.CreatedDateTime);
public const string CreatedByUserId = nameof(CreatedByUserId);
public const string CreatedByBrowserName = nameof(CreatedByBrowserName);
public const string CreatedByIP = nameof(CreatedByIP);
public const string ModifiedDateTime = nameof(IModificationTracking.ModifiedDateTime);
public const string ModifiedByUserId = nameof(ModifiedByUserId);
public const string ModifiedByBrowserName = nameof(ModifiedByBrowserName);
public const string ModifiedByIP = nameof(ModifiedByIP);
7- یک تبدیلگر سفارشی برای ذخیره سازی اشیا به صورت JSON اضافه شده است که برگرفته از کتابخانه
Innofactor.EfCoreJsonValueConverter میباشد.
8- دو متد الحاقی زیر برای نرمالسازی خصوصیات تاریخ از نوع DateTime و خصوصیات عددی از نوع Decimal به ModelBuilder اضافه شدهاند:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.NormalizeDateTime();
modelBuilder.NormalizeDecimalPrecision(precision: 20, scale: 6);
base.OnModelCreating(modelBuilder);
}
9- متد MigrateDbContext به این کتابخانه منتقل شده است:
MigrateDbContext<TContext>(this IHost host)
متد Seed واسط IDbSetup در صورت معرفی یک پیادهسازی از آن به سیستم تزریق وابستگیها، در بدنه این متد فراخوانی خواهد شد.
روش معرفی سرویسهای مرتبط با کتابخانه DNTFrameworkCore.EFCore
services.AddEFCore<ProjectDbContext>()
.WithTrackingHook<long>()
.WithDeletedEntityHook()
.WithRowIntegrityHook()
.WithNumberingHook(options =>
{
options.NumberedEntityMap[typeof(Task)] = new NumberedEntityOption
{
Prefix = "Task",
FieldNames = new[] {nameof(Task.BranchId)}
};
});
همانطور که عنوان شد، محدودیت نوع خصوصیات CreatedByUserId و ModifiedByUserId برداشته شده است و از طریق متد WithTrackingHook قابل تنظیم میباشد.
تغییرات کتابخانه DNTFrameworkCore.Web.Tenancy
فعلا امکان شناسایی مستأجر جاری و دسترسی به اطلاعات آن از طریق واسط ITenantSession در دسترس میباشد؛ همچنین امکان تغییر و تعیین رشته اتصال به بانک اطلاعاتی هر مستأجر از طریق متد UseConnectionString واسط IUnitOfWork فراهم میباشد.
services.AddTenancy()
.WithTenantSession()
.WithStore<InMemoryTenantStore>()
.WithResolutionStrategy<HostResolutionStrategy>();
سایر کتابخانهها تغییرات خاصی نداشتند و صرفا نحوه معرفی سرویسهای آنها ممکن است تغییر کند و یا وابستگیهای آنها به آخرین نسخه موجود ارتقاء داده شده باشند که در پروژه DNTFrameworkCore.TestAPI اعمال شدهاند.
لیست بستههای نیوگت نسخه ۴.۵.۳
PM> Install-Package DNTFrameworkCore
PM> Install-Package DNTFrameworkCore.EFCore
PM> Install-Package DNTFrameworkCore.EFCore.SqlServer
PM> Install-Package DNTFrameworkCore.Web
PM> Install-Package DNTFrameworkCore.FluentValidation
PM> Install-Package DNTFrameworkCore.Web.Tenancy
PM> Install-Package DNTFrameworkCore.Licensing