در نسخه‌های جدید زیرساخت DNTFrameworkCore، امکان مشخص کردن چندین فیلد برای تولید کد منحصر به فرد مهیا شده است. برای مثال:
services.AddEFCore<NumberingDbContext>()
    .WithTrackingHook<long>()
    .WithNumberingHook(options =>
    {
        options[typeof(NumberingTestEntity)] = new[]
        {
            new NumberedEntityOption
            {
                FieldName = nameof(NumberingTestEntity.Number),
                Prefix = "Prefix-",
                Start = 100,
                IncrementBy = 5
            },
            new NumberedEntityOption
            {
                FieldName = nameof(NumberingTestEntity.NumberBasedOnBranchId),
                Start = 10,
                IncrementBy = 10,
                Fields = new[] {nameof(NumberingTestEntity.BranchId)}
            },
            new NumberedEntityOption
            {
                FieldName = nameof(NumberingTestEntity.NumberBasedOnBranchIdDateTime),
                Fields = new[]
                {
                    nameof(NumberingTestEntity.BranchId),
                    nameof(NumberingTestEntity.DateTime)
                }
            },
            new NumberedEntityOption
            {
                FieldName = nameof(NumberingTestEntity.NumberBasedOnBranchIdCreatedDateTime),
                Fields = new[]
                {
                    nameof(NumberingTestEntity.BranchId),
                    EFCoreShadow.CreatedDateTime
                }
            }
        };
    });
از این پس مقداردهی FieldName اجباری می‌باشد و موجودیت مورد نظر برای وارد شدن به PreInsertNumberedEntityHook نیاز است واسط INumberedEntity را پیاده‌سازی کرده باشد؛ این واسط برای  برچسب گذاری استفاده شده است.
همانطور که در مطلب جاری اشاره شد، در اینجا از یک قفل منطقی برروی یک منبع مجازی (همان lockKey مطلب جاری ) با استفاده از  sp_getapplock ایجاد می‌شود که این اطمینان را به ما می‌دهد که تا قبل از اتمام تراکنش جاری، درخواست دیگری وارد قسمت قفل شده با lockKey مذکور نخواهد شد. حال اگر درخواست ثبت یک سند حسابداری به NumberingPreInsertHook مذکور برسد، قفل متناظر با موجودیت سند حسابداری و با توجه فیلدهای ریست مشخص شده، ایجاد خواهد شد و وقفه‌ای در ثبت سایر موجودیت‌ها ایجاد نخواهد شد. 
یک آزمون مرتبط با درخواست‌های همزمان برای ثبت یک موجودیت، در اینجا  قابل بررسی می‌باشد.
‫۳ سال و ۵ ماه قبل، چهارشنبه ۲۷ اسفند ۱۳۹۹، ساعت ۰۳:۳۴
جهت اطلاع
نسخه جدید این قالب منطبق با نسخه 5.1.0 زیرساخت DNTFrameworkCore، منتشر شد.
آدرس مخزن جدید قالب جاری: 
https://github.com/rabbal/DNTFrameworkCore/tree/master/templates/aspnet-core-api
‫۳ سال و ۵ ماه قبل، شنبه ۲۳ اسفند ۱۳۹۹، ساعت ۲۱:۰۶
این سری مطالب ادامه دار نخواهد بود و به عنوان مکمل منابع موجود در اینترنت،  ویدئو مربوط به وبینار Web Components از طریق لینک زیر در دسترس می‌باشد که مباحث زیر را شامل می‌شود:
  • چرا Web Components؟
  • وضعیت پشتیبانی و به کار گیری Web Components
  • توسعه چند کامپوننت نمونه با استفاده از Web Components
  • بررسی ابزارهای جانبی
  • استفاده به شکل Framework agnostic 
مجموعه‌ای از اعتبارسنج‌های سفارشی Cross Field
buildForm(){
 this.form = _fb.group({
 field1: [null, Validators.compose([Validators.required, CorssFieldValidators.greaterThan('field2', 'فیلد دو')])],
 field2: [null,  CorssFieldValidators.requiredIf('field1', 20)],
 password: [null, Validators.required],
 confirmPassword: [null, Validators.compose([Validators.required, CorssFieldValidators.equal('password')])]
 })
}


export class CrossFieldValidators {

  static greaterThanEqual(controlName: string, label: string): ValidatorFn {
    return this.is(controlName, label, 'greaterThanEqual');
  }

  static greaterThan(controlName: string, label: string): ValidatorFn {
    return this.is(controlName, label, 'greaterThan');
  }

  static lessThan(controlName: string, label: string): ValidatorFn {
    return this.is(controlName, label, 'lessThan');
  }

  static lessThanEqual(controlName: string, label: string): ValidatorFn {
    return this.is(controlName, label, 'lessThanEqual');
  }

  static equal(controlName: string, label: string): ValidatorFn {
    return this.is(controlName, label, 'equal');
  }

  static requiredIf(
    controlName: string,
    value: any
  ): ValidatorFn {
    let subscribed = false;
    const validatorFn = (control: AbstractControl): { [key: string]: any } => {
      if (!control.root || !(control.root as FormGroup).controls) return null;

      const dependentControl = control.root.get(controlName);
      if (!subscribed) {
        subscribed = true;
        dependentControl.valueChanges.subscribe(() => {
          control.updateValueAndValidity();
          control.markAsTouched();
        });
      }

      const requiredFn = Validators.required;
      return dependentControl.value === value ? requiredFn(control) : null;
    };
    return validatorFn;
  }

  private static is(
    controlName: string,
    label: string,
    operator:
      | 'equal'
      | 'greaterThan'
      | 'greaterThanEqual'
      | 'lessThan'
      | 'lessThanEqual'
  ): ValidatorFn {
    let subscribed = false;
    const validatorFn = (control: AbstractControl): { [key: string]: any } => {
      const value = control.value;
      if (value === null) return null;

      if (!control.root || !(control.root as FormGroup).controls) return null;

      const dependentControl = control.root.get(controlName);
      if (!subscribed) {
        subscribed = true;
        dependentControl.valueChanges.subscribe(() => {
          control.updateValueAndValidity({ emitEvent: operator === 'equal' });
          control.markAsTouched();
        });
      }

      const dependentValue = dependentControl.value;
      if (dependentValue === null) return null;

      let isInvalid = false;
      switch (operator) {
        case 'equal':
          isInvalid = dependentValue !== value;
          break;
        case 'greaterThan':
          isInvalid = parseFloat(dependentValue) >= parseFloat(value);
          break;
        case 'greaterThanEqual':
          isInvalid = parseFloat(dependentValue) > parseFloat(value);
          break;
        case 'lessThan':
          isInvalid = parseFloat(dependentValue) <= parseFloat(value);
          break;
        case 'lessThanEqual':
          isInvalid = parseFloat(dependentValue) < parseFloat(value);
          break;
        default:
          throw 'invalid operator!';
      }
      return isInvalid ? { [operator]: { label } } : null;
    };

    return validatorFn;
  }
}


‫۴ سال و ۲ ماه قبل، چهارشنبه ۴ تیر ۱۳۹۹، ساعت ۱۳:۳۸
بهتر است سه قسمت اول مرتبط با دوره «بررسی مفاهیم معکوس سازی وابستگی‌ها و ابزارهای مرتبط با آن» را مطالعه کنید.
1) Dependency inversion principle یا DIP (اصل معکوس سازی وابستگی‌ها)
DIP یکی از اصول طراحی نرم افزار است و D آن همان D معروف SOLID است (اصول پذیرفته شده شیءگرایی).
2) Inversion of Control یا IOC (معکوس سازی کنترل)
الگویی است که نحوه پیاده سازی DIP را بیان می‌کند.
3) Dependency injection یا DI (تزریق وابستگی‌ها)
یکی از روش‌های پیاده سازی IOC است.
4) IOC container
به فریم ورک‌هایی که کار DI را انجام می‌دهند گفته می‌شود.  
نکته تکمیلی
در راستای تکمیل مطلب جاری و مطلب «پیاده سازی Conventional UI در ASP.NET MVC» برای رسیدن به یک قالب مشخص و جلوگیری از تکرار، می‌توان به شکل زیر عمل کرد:
1- انتقال قسمت‌های مشترک فرم‌ها به یک پارشال‌ویو به عنوان Layout فرم‌ها
//_EntityFormLayout.cshtml

@inherits EntityFormRazorPage<dynamic>
@{
    Layout = null;
}
<div class="modal-header">
    <h4 class="modal-title" asp-if="IsNew">Create New @EntityDisplayName</h4>
    <h4 class="modal-title" asp-if="!IsNew">Edit @EntityDisplayName</h4>
    <button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<form asp-action="@(IsNew ? CreateActionName : EditActionName)" asp-modal-form="@FormId">
    <div class="modal-body">
        <input type="hidden" name="continue-editing" value="true" asp-permission="@EditPermission"/>
        <input asp-for="@Version" type="hidden"/>
        <input asp-for="@Id" type="hidden"/>
        @RenderBody()
    </div>
    <div class="modal-footer">

        <a class="btn btn-light btn-circle" asp-modal-delete-link asp-model-id="@Id" asp-modal-toggle="false"
           asp-action="@DeleteActionName" asp-if="!IsNew" asp-permission="@DeletePermission"
           title="Delete Role">
            <i class="fa fa-trash text-danger"></i>
        </a>

        <a class="btn btn-light btn-circle" title="Refresh Role" asp-if="!IsNew" asp-modal-link asp-modal-toggle="false"
           asp-action="@EditActionName" asp-route-id="@Id">
            <i class="fa fa-repeat"></i>
        </a>
        <a class="btn btn-light btn-circle mr-auto" title="New Role" asp-modal-link asp-modal-toggle="false"
           asp-permission="@CreatePermission"
           asp-action="@CreateActionName">
            <i class="fa fa-plus"></i>
        </a>
        <button type="button" class="btn btn-light" data-dismiss="modal">
            <i class="fa fa-ban"></i>&nbsp; Cancel
        </button>
        <button type="submit" class="btn btn-outline-primary">
            <i class="fa fa-save"></i>&nbsp;Save Changes
        </button>
    </div>
</form>

با توجه به اینکه مدل متناظر با یک ویو در Layout آن نیز قابل دسترس می‌باشد. بدین ترتیب امکان دسترسی به خصوصیاتی مانند Id و Version یا متد IsNew وجود دارد؛ این خصوصیات در کلاس MasterModel به عنوان پایه مدل/DTO/ویومدل‌های ثبت/ویرایش، تعریف شده‌اند.
قراداد ما استفاده از همان مدل/DTO‌ها به عنوان ویومدل می‌باشد که در سناریوهای خاص پیشنهاد شد که از مدلی با نام موجودیت + کلمه ModalViewModel یا FormViewModel استفاده شود. برای انتقال سایر دیتا و متادیتای مورد نیاز برای ساخت فرم می‌توان از ViewBag و ViewData پس از امکان تعریف ویومدل پایه (دارای خصوصیات مورد نیاز Layout) که در این طراحی ممکن نیست، استفاده کرد. 
2- طراحی یک EntityFormRazorPage پایه
برای رسیدن به کدی با خوانایی بالا کلاسی را به عنوان پایه ویو‌های فرم‌ها و پارشال‌ویو EntityFormLayout، به شکل زیر طراحی می‌کنیم. در اینجا فرم ما یکسری خصوصیات موجود در کلاس پایه خود را مقداردهی خواهد کرد و در ادامه به دلیل ذخیره شدن این اطلاعات در ViewData، در Layout نیز قابل دسترس خواهند بود. 
    public abstract class EntityFormRazorPage<T> : RazorPage<T>
    {
        protected string EntityName
        {
            get => ViewData[nameof(EntityName)].ToString();
            set => ViewData[nameof(EntityName)] = value;
        }

        protected string EntityDisplayName
        {
            get => ViewData[nameof(EntityDisplayName)].ToString();
            set => ViewData[nameof(EntityDisplayName)] = value;
        }

        protected string DeletePermission
        {
            get => ViewData[nameof(DeletePermission)].ToString();
            set => ViewData[nameof(DeletePermission)] = value;
        }

        protected string CreatePermission
        {
            get => ViewData[nameof(CreatePermission)].ToString();
            set => ViewData[nameof(CreatePermission)] = value;
        }

        protected string EditPermission
        {
            get => ViewData[nameof(EditPermission)].ToString();
            set => ViewData[nameof(EditPermission)] = value;
        }

        protected string CreateActionName
        {
            get => ViewData.TryGetValue(nameof(CreateActionName), out var value) ? value.ToString() : "Create";
            set => ViewData[nameof(CreateActionName)] = value;
        }

        protected string EditActionName
        {
            get => ViewData.TryGetValue(nameof(EditActionName), out var value) ? value.ToString() : "Edit";
            set => ViewData[nameof(EditActionName)] = value;
        }

        protected string DeleteActionName
        {
            get => ViewData.TryGetValue(nameof(DeleteActionName), out var value) ? value.ToString() : "Delete";
            set => ViewData[nameof(DeleteActionName)] = value;
        }

        protected string FormId => $"{EntityName}Form";
        protected bool IsNew => (Model as dynamic).IsNew();
        protected string Id => (Model as dynamic).Id.ToString(CultureInfo.InvariantCulture);
        protected byte[] Version => (Model as dynamic).Version;
    }
3- تنظیم خصوصیات موجود در کلاس پایه
برای این منظور لازم است کلاس پایه را با دایرکتیو inherits مشخص کرده و سپس کار تنظیم Layout و سایر خصوصیات مورد نیاز را انجام دهید:
//_BlogPartial.cshtml

@inherits EntityFormRazorPage<BlogModel>
@{
    Layout = "_EntityFormLayout";
    EntityName = "Blog";
    DeletePermission = PermissionNames.Blogs_Delete;
    CreatePermission = PermissionNames.Blogs_Create;
    EditPermission = PermissionNames.Blogs_Edit;
    EntityDisplayName = "Blog";
}

4 - فرم ثبت و ویرایش متناظر با یک موجودیت
//_BlogPartial.cshtml

@inherits EntityFormRazorPage<BlogModel>
@{
    Layout = "_EntityFormLayout";
    ...
}

<div class="form-group row">
    <div class="col col-md-8">
        <label asp-for="Title" class="col-form-label text-md-left"></label>
        <input asp-for="Title" autocomplete="off" class="form-control"/>
        <span asp-validation-for="Title" class="text-danger"></span>
    </div>
</div>
<div class="form-group row">
    <div class="col">
        <label asp-for="Url" class="col-form-label text-md-left"></label>
        <input asp-for="Url" class="form-control" type="url"/>
        <span asp-validation-for="Url" class="text-danger"></span>
    </div>
</div>

و یا اگر از EditorTemplates استفاده می‌کنید:
//_BlogPartial.cshtml

@inherits EntityFormRazorPage<BlogModel>
@{
    Layout = "_EntityFormLayout";
    EntityName = "Blog";
    DeletePermission = PermissionNames.Blogs_Delete;
    CreatePermission = PermissionNames.Blogs_Create;
    EditPermission = PermissionNames.Blogs_Edit;
    EntityDisplayName = "Blog";
}

@Html.EditorForModel()

پ.ن: از همین روش برای ساخت لیست‌های یکدست متناظر با موجودیت‌ها نیز می‌توان ایده گرفت؛ همچنین امکان تعریف و تنظیم Layout‌های متناسب با شرایط مختلف نیز در این حالت به راحتی ممکن است. در ادامه اگر در سیستم متادیتای غنی متناظر با موجودیت‌ها وجود داشته باشد، چه بسا صرفا با مشخص کردن نام موجودیت به باقی خصوصیات تنظیم شده در کد بالا دسترسی داشته باشیم. 

‫۴ سال و ۷ ماه قبل، جمعه ۴ بهمن ۱۳۹۸، ساعت ۰۱:۰۷
با تشکر از شما؛ قابلیت RowIntegrity به زیرساخت DNTFrameworkCore اضافه شد.

public interface IHasRowIntegrity
{
    string Hash { get; set; }
}
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);
    }
}
//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);
    }
}

با توجه به محدودیت‌هایی (منفصل بودن اشیاء از کانتکست) که در استفاده از TrackGraph در زیرساخت وجود داشت، در اینجا از خواص سایه‌ای چشم پوشی شده است.
روش فعال‌سازی آن نیز در نسخه‌های جدید به شکل زیر می‌باشد:
services.AddEFCore<ProjectDbContext>()
    .WithTrackingHook<long>()
    .WithDeletedEntityHook()
    .WithRowLevelSecurityHook<long>()
    .WithRowIntegrityHook()
    .WithNumberingHook(options =>
    {
        options.NumberedEntityMap[typeof(Task)] = new NumberedEntityOption
        {
            Prefix = "Task",
            FieldNames = new[] {nameof(Task.BranchId)}
        };
    });