نظرات مطالب
C# 7.1 - Tuple Name Inference
یکی از ویژگی‌های جالب Tuple اینه که میشه در بدنه For نوع داده‌ی متفاوتی قرار داد
public static  void Main() 
{
        var li = Enumerable.Range(1, 10).ToList();
        var sb = new StringBuilder();
        //for (int i=0,j=1;;) 
//در اینجا میشه نوع‌های متفاوتی تعریف کرد
        for ( (int i, bool first) = (0, true); i < li.Count; i++, first = false)
        {
            if (!first)
                sb.Append(", ");
            
            sb.Append(li[i]);
        }
        
        Console.WriteLine(sb.ToString());
}
مطالب
راهکار لاگ متناسب با Cloud و On-Premise
Application Insights راهکار ارائه شده توسط Microsoft است که در سه بخش به ما کمک می‌کند تا سیستم لاگ مؤثر و کارآمدی داشته باشیم:

۱- متدهای پایه Log که به صورت دستی فراخوانی می‌شوند، مانند TrackEvent برای ثبت یک رویداد بیزینسی که این متدها فراتر از متدهای معمول loggerهای متداول هستند.

۲- به صورت خودکار، Application Insights خطاهای سیستم را لاگ نموده و همچنین در زمان کار کردن با Http Client، دیتابیس و سایر Dependencyها، میزان طول کشیدن آنها را به همراه آدرس Request یا متن Sql Command و سایر اطلاعات مفید را نیز ذخیره می‌کند که این خود منجر به جمع شدن دیتایی بسیار ارزشمند در سیستم می‌شود.
البته اگر یک Dependency به صورت خودکار شناسایی نشود، مانند Redis، شما می‌توانید خودتان با متد TrackDependency اطلاعات آن‌را به AppInsights بدهید.

۳- داشبورد App Insights در Azure این امکان را می‌دهد که به سریع‌ترین شکل ممکن در لاگ‌ها جستجو نمود و برای مثال تمامی کارهای انجام شده توسط یک کاربر خاص را به صورت یک‌جا مشاهده و بررسی کرد.
فرضا اگر کاربر درخواست گرفتن خروجی Excel از لیست محصولات را داشته و این ۱ ثانیه طول کشیده، چقدر آن در انتظار دیتابیس بوده و ...
به علاوه از Power BI نیز می‌توانید برای بیرون کشیدن نکات مهم استفاده کنید.

البته شاید App Insights برای کسانی که Azure Account نداشته باشند، مناسب به نظر نرسد، ولی اگر راهکاری برای ذخیره سازی On-Premise اطلاعات لاگ شده وجود داشته باشد چه؟ مثلا اطلاعات آن را در Elastic موجود در سرورهای شرکت، داخل ایران ذخیره نمود، بدون الزام به این‌که حتی آن سرور دسترسی به اینترنت داشته باشد.

بله، این امکان وجود دارد و با کمک  Microsoft Diagnostics EventFlow می‌توان اطلاعات App Insights را در هر جایی از جمله Elastic ذخیره نمود و بدین طریق از عمده مزایای App Insights بدون داشتن Azure Account بهره مند شد.

برای این منظور به شکل زیر عمل کنید: (آموزش برای ASP.NET Core 3.1 بوده، ولی برای سایر پروژه‌ها نیز قابل استفاده است)

۱- ابتدا Application Insights را به پروژه خود اضافه کنید.بدین منظور لازم است Packageهای Microsoft.Extensions.Logging.ApplicationInsights و Microsoft.ApplicationInsights.AspNetCore را نصب کنید. 

۲- در Program.cs بعد از
Host.CreateDefaultBuilder(args)
کد زیر را قرار دهید
.ConfigureLogging(loggingBuilder  =>
{
    loggingBuilder.ClearProviders();
    loggingBuilder.AddApplicationInsights(); 
})
این باعث می‌شود تا Loggerهای پیش فرض Console و Debug حذف شوند و البته اگر کتابخانه 3rd party ای از Microsoft.Extensions.Logging استفاده کرده باشد، اطلاعات لاگ آن به AppInsights نیز داده شود و در نهایت شما با کمک Microsoft Diagnostics EventFlow، آن اطلاعات را در Elastic و Console و سایر Outputها خواهید داشت.

۳- اگر جایی قصد لاگ کردن یک Event را دارید، یا در مثال استفاده از Redis میخواهید اطلاعات زمان طول کشیدن رفت و برگشت به Redis را لاگ کنید یا یک try/catch دارید که در catch آن خطا را مجدد throw نمی‌کنید، ولی قصد لاگ کردن exception را دارید، ابتدا TelemetryClient را inject نموده و از متدهای آن مانند TrackException استفاده کنید.
توجه داشته باشید که اگر از ILogger ارائه شده توسط MS.Ext.Logging استفاده کنید نیز کار خواهد کرد.

۴- پکیج Microsoft.Diagnostics.EventFlow.Inputs.ApplicationInsights را در پروژه نصب کنید و سپس از بین Output‌های معرفی شده نیز یکی را انتخاب و پکیج آن را نیز نصب کنید. شما می‌توانید دیتایی را که AppInsights به صورت خودکار جمع نموده را + دیتای ارائه شده توسط خودتان را به Elastic، Splunk و ... بفرستید.
ما در این مثال برای سادگی Std Out - Console Output را انتخاب می‌کنیم و پکیج Microsoft.Diagnostics.EventFlow.Outputs.StdOutput را نصب می‌کنیم.

۵- فایل eventFlowConfig.json را به پروژه اضافه کنید و موارد زیر را در آن قرار دهید:
 {
  "inputs": [
    {
      "type": "ApplicationInsights"
    }
  ],
  "outputs": [
    {
      "type": "StdOutput" // console output
    }
  ],
  "schemaVersion": "2016-08-11"
}  
در این فایل، inputs شامل ApplicationInsights بوده و البته موارد جالبی چون PerformanceCounters را نیز میتوانید در آن بگنجانید و outputs بسته به نیاز شما برابر با Elastic و ... است که در این صورت باید اطلاعات اتصال به Elastic شامل آدرس سرور و ... را نیز ارائه کنید. توجه داشته باشید که ما در حال استفاده از ApplicationInsights SDK هستیم، به عنوان کتابخانه Logging و در نهایت دیتا نه به ApplicationInsights در سرورهای Microsoft Azure، که به output یا outputهایی که در فایل eventFlowConfig.json می‌گوییم ارسال می‌شود.

۶- در Program.cs متد Main را به شکل زیر در بیاورید:
using (var pipeline = DiagnosticPipelineFactory.CreatePipeline("eventFlowConfig.json"))
{
    CreateHostBuilder(args, pipeline)
        .Build()
        .Run();
}  
و CreateHostBuilder نیز به شکل زیر باشد:
public static IHostBuilder CreateHostBuilder(string[] args, DiagnosticPipeline pipeline) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureServices(services => services.AddSingleton<ITelemetryProcessorFactory>(sp => new EventFlowTelemetryProcessorFactory(pipeline)))
        .ConfigureLogging(logginBuilder =>
        {
            logginBuilder.ClearProviders();
            loggingBuilder.AddApplicationInsights(); 
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });  

همه چیز آماده است. هم اکنون اگر Azure Account داشته باشید، می‌توانید با دادن instrumentationKey در appsetting.json از داشبورد فوق العاده ApplicationInsights استفاده کنید و اگر نه هم در سرورهای داخلی خودتان Splunk و ... را راه اندازی و در فایل eventFlowConfig.json، در قسمت outputs، اطلاعات آدرس آنها را بدهید و لاگ‌های مفصل و کاربردی ای که به صورت خودکار جمع آوری شده را به همراه اطلاعاتی که خودتان دستی ارائه کرده اید، یکجا تحویل بگیرید.

لینک پروژه در GitHub که حاوی مثال Elastic است.
نظرات اشتراک‌ها
تنظیم history مرورگر و رعایت اصول دکمه back در حین اعمال Ajax ایی
اگه بخواهیم چند تا پارامتر  شبه هم و با هم map کنیم باید چیکار کنیم . 
Path.map("#!/page(/:page)(/:SortBy)(/:Order)(/:PageSize)(/:MinPrice)(/:MaxPrice)").to(function () {
//var sort=this.params['SortBy'];
});

یعنی حالتی رو در نظر بگیرید که hash به اینصورت باشه
#!/Category-Electronic-Devices/Category-Mobile/Category-Mobile-Phone/Type-201/Type-202/ 
یعنی چند تا Type داشته باشیم
نظرات مطالب
صفحه بندی و مرتب سازی خودکار اطلاعات به کمک jqGrid در ASP.NET MVC
در برگه‌ی  network، بررسی کنید که دقیقا چه اطلاعاتی با چه فرمتی و خصوصا چه Content-Type ای ارسال شدند. همین موارد را برای response هم بررسی کنید. اگر هدرهای accept و یا Content-Type اشتباه تنظیم شده باشند، ممکن است web api شما خروجی با فرمت xml تولید کند. یا حتی ممکن است خروجی خالی دریافت کرده باشید که به xml تفسیر شده. یا حتی اگر Content-Type خروجی تنظیم نشده باشد، ممکن است این خطای xml parsing error را دریافت کنید (حتی اگر خروجی JSON به نظر برسد).
نظرات مطالب
مروری بر کاربردهای Action و Func - قسمت سوم
گاهی از اوقات حین کار با نوع‌های جنریک نیاز دارید که مثلا null بازگشت بدید. در این حالت کامپایلر شما را با خطای Cannot convert null to type parameter T متوقف می‌کند. به همین جهت مرسوم است در این حالت از default T استفاده شود که مقدار پیش فرض نوع را برمی‌گرداند. اگر reference type باشد (مثل کلاس‌ها) این مقدار پیش فرض null خواهد بود؛ اگر value type باشد مانند int صفر بازگشت داده می‌شود.
مطالب
GraphQL Mutations در ASP.NET Core ( عملیات POST, PUT, DELETE )
Mutation‌ها در GraphQL، اکشن‌های می‌باشند که با استفاده از آن‌ها Add ، Delete و Update را انجام می‌دهیم. تا کنون ما query ‌های بازیابی اطلاعات را اجرا کرده‌ایم. در این قسمت می‌خواهیم در رابطه با Mutation داده‌ها صحبت کنیم.


Input Types and Schema Enhancing for the GraphQL Mutations 

کار را با ایجاد کردن یک کلاس جدید به نام OwnerInputType در پوشه Types، شروع می‌کنیم:
public class OwnerInputType : InputObjectGraphType
{
    public OwnerInputType()
    {
        Name = "ownerInput";
        Field<NonNullGraphType<StringGraphType>>("name");
        Field<NonNullGraphType<StringGraphType>>("address");
    }
}
این همان typeی می‌باشد که قرار است به عنوان یک آرگومان برای Mutation از طرف کلاینت ارسال شود. این کلاس از InputObjectGraphType مشتق شده است؛ نه از ObjectGraphType همانند Typeهای تعریف شده قبل.
در سازنده کلاس، خصوصیت Name را مقدار دهی و دو فیلد را ایجاد می‌کنیم. همانطور که می‌بینیم دو خصوصیت Id و Accounts را نداریم؛ به دلیل اینکه نیازی به آن‌ها نیست. 
اگر قسمت قبل را دنبال کرده باشید متوجه هستیم برای ایجاد کردن query ‌ها مجبور بودیم که یک کلاس را به نام AppQuery ایجاد کنیم. برای Mutation هم همین گونه‌است. یک کلاس به نام AppMutation را در پوشه GraphQLQueries ایجاد می‌کنیم:
public class AppMutation : ObjectGraphType
{
    public AppMutation()
    {
    }
}

در نهایت نیاز است کلاس AppSchema را باز کرده و خصوصیت Mutation را به آن اضافه می‌کنیم:
public class AppSchema : Schema
{
    public AppSchema(IDependencyResolver resolver)
        :base(resolver)
    {
        Query = resolver.Resolve<AppQuery>();
        Mutation = resolver.Resolve<AppMutation>();
    }
}
اکنون همه چیز آماده است تا تعدادی Mutation، در پروژه ایجاد کنیم. 

Create Mutation 
  در ابتدا واسط IOwnerRepository  و کلاس OwnerRepository را همانند زیر ویرایش می‌کنیم (اضافه کردن متد CreateOwner):
public interface IOwnerRepository
{
   ...
    Owner CreateOwner(Owner owner);
}

public class OwnerRepository : IOwnerRepository
{
   ...
    public Owner CreateOwner(Owner owner)
     {
         owner.Id = Guid.NewGuid();
         _context.Add(owner);
         _context.SaveChanges();
         return owner;
     }
}

متد CreateOwner یک شیء Owner جدید ایجاد شده را بازگشت می‌دهد. سپس در کلاس AppMutation: 
public class AppMutation : ObjectGraphType
{
   // Add
    public AppMutation(IOwnerRepository repository)
    {  
        Field<OwnerType>(
            "createOwner",
            arguments: new QueryArguments(new QueryArgument<NonNullGraphType<OwnerInputType>> { Name = "owner" }),
            resolve: context =>
            {
                var owner = context.GetArgument<Owner>("owner");
                return repository.CreateOwner(owner);
            }
        );
    }
}
 در ابتدا یک فیلد را به منظور بازگشت دادن شیء OwnerType ایجاد می‌کنیم. در بخش نام، createOwner را وارد می‌کنیم و در بخش arguments، یک آرگومان از نوع OwnerInputType را داریم (نشان دهنده ورودی‌ها می‌باشند) و در نهایت در بخش resolve، که قرار است متد CreateOwner را از IOwnerRepository اجرا کند. 
  اکنون پروژه را اجرا کرده و سپس درخواست را در UI.Playground به صورت زیر ارسال کنید:
mutation($owner:ownerInput!){
  createOwner(owner:$owner){
    id,
    name,
    address
  }
}
سپس در قسمت Query Variables : 
{
  "owner":{
    "name":"Abolfazl-Roshanzamir",
    "address":"Address - User 4"
  }
}
و در نهایت، نتیجه اجرا، در بخش سمت راست تصویر زیر قابل مشاهده‌است:
 


بجای کلمه کلیدی query، از کلمه کلیدی mutation برای mutations‌ها استفاده می‌کنیم. این تنها یک تفاوت جدید است.

Update Mutation  

در ابتدا واسط IOwnerRepository و کلاس OwnerRepository را مطابق زیر ویرایش می‌کنیم: 
public interface IOwnerRepository
{
    ...
    Owner UpdateOwner(Owner dbOwner, Owner owner);
}

public class OwnerRepository : IOwnerRepository
{
   ...
   public Owner UpdateOwner(Owner dbOwner, Owner owner)
   {
         dbOwner.Name = owner.Name;
         dbOwner.Address = owner.Address;

         _context.SaveChanges();
         return dbOwner;
     }
}

در نهایت همانند بخش (Create Mutation) یک فیلد را در سازنده کلاس AppMutation برای Update، اضافه می‌کنیم:
    public class AppMutation : ObjectGraphType
    {
        public AppMutation(IOwnerRepository repository)
        {
            ...
            // Update
            Field<OwnerType>(
                "updateOwner",
                arguments: new QueryArguments(
                    new QueryArgument<NonNullGraphType<OwnerInputType>> { Name = "owner" },
                    new QueryArgument<NonNullGraphType<IdGraphType>> { Name = "ownerId" }),
                resolve: context =>
                {
                    var owner = context.GetArgument<Owner>("owner");
                    var ownerId = context.GetArgument<Guid>("ownerId");

                    var dbOwner = repository.GetById(ownerId);
                    if (dbOwner == null)
                    {
                        context.Errors.Add(new ExecutionError("Couldn't find owner in db."));
                        return null;
                    }

                    return repository.UpdateOwner(dbOwner, owner);
                }
            );

        }
    }

  اکنون پروژه را اجرا کرده و سپس درخواست را در UI.Playground به صورت زیر ارسال کنید:
mutation($owner:ownerInput!,$ownerId:ID!){
  updateOwner(owner:$owner,ownerId:$ownerId){
    id,
    name,
    address
  }
}

سپس در قسمت Query Variables:
{
  "owner":{
    "name":"Andy Madaidan",
    "address":"Address - User 1"
  },
  "ownerId": "53270061-3ba1-4aa6-b937-1f6bc57d04d2"
}


Delete Mutation   
همان الگوی Create / Update را دوباره دنبال می‌کنیم. در ابتدا واسط IOwnerRepository و کلاس OwnerRepository را همانند زیر ویرایش می‌کنیم:
public interface IOwnerRepository
{
   ...
    void DeleteOwner(Owner owner);
}

public class OwnerRepository : IOwnerRepository
{
   ...
   public void DeleteOwner(Owner owner)
   {
        _context.Remove(owner);
        _context.SaveChanges();
    }
}

و آخرین کاری که نیاز است انجام شود، ویرایش کلاس AppMutation می‌باشد:
 public class AppMutation : ObjectGraphType
    {
        public AppMutation(IOwnerRepository repository)
        {
            ...
           
            //Delete
            Field<StringGraphType>(
                "deleteOwner",
                arguments: new QueryArguments(new QueryArgument<NonNullGraphType<IdGraphType>> { Name = "ownerId" }),
                resolve: context =>
                {
                    var ownerId = context.GetArgument<Guid>("ownerId");
                    var owner = repository.GetById(ownerId);
                    if (owner == null)
                    {
                        context.Errors.Add(new ExecutionError("Couldn't find owner in db."));
                        return null;
                    }
            
                    repository.DeleteOwner(owner);
                    return $"The owner with the id: {ownerId} has been successfully deleted from db.";
                }
            );
        }
    }

 اکنون پروژه را اجرا کنید و سپس درخواست را در UI.Playground به صورت زیر ارسال کنید:
mutation($ownerId:ID!){
  deleteOwner(ownerId:$ownerId)
}

سپس در قسمت Query Variables : 
{
  "ownerId": "6f513773-be46-4001-8adc-2e7f17d52d83"
}
 نتیجه اجرا در بخش سمت راست تصویر زیر قابل مشاهده است : 


نتیجه گیری
اکنون ما می‌دانیم که چگونه از Input Type ها برای Mutation ها استفاده کنیم. چگونه mutation action های متفاوتی را ایجاد کنیم و هم چنین چگونه در خواست‌های  mutation را در سمت کلاینت ایجاد کنیم.

کد‌های کامل مربوط به این قسمت را از اینجا دریافت کنید: ASPCoreGraphQL_3.rar

مطالب
Blazor 5x - قسمت 26 - برنامه‌ی Blazor WASM - ایجاد و تنظیمات اولیه
در قسمت قبل، پایه‌ی Web API و سرویس‌های سمت سرور برنامه‌ی کلاینت Blazor WASM این سری را آماده کردیم. این برنامه‌ی سمت کلاینت، قرار است توسط عموم کاربران آن جهت رزرو کردن اتاق‌های هتل فرضی مثال این سری، مورد استفاده قرار گیرد. پیش از این نیز یک برنامه‌ی Blazor Server را تهیه کردیم که کار آن صرفا محدود است به مسائل مدیریتی هتل؛ مانند تعریف اتاق‌ها و امکانات رفاهی آن.


ایجاد یک پروژه‌ی جدید Blazor WASM

برای تکمیل پیاده سازی قسمت سمت کلاینت پروژه‌ی این سری، نیاز به یک پروژه‌ی جدید Blazor WASM را داریم که می‌توان آن‌را با اجرای دستور dotnet new blazorwasm  در یک پوشه‌ی خالی، ایجاد کرد. کدهای این پروژه را می‌توانید در پوشه‌ی HotelManagement\BlazorWasm\BlazorWasm.Client فایل پیوستی انتهای بحث مشاهده کنید.


افزودن فایل‌های جاوااسکریپتی مورد نیاز

شبیه به کاری که در مطلب «Blazor 5x - قسمت یازدهم - مبانی Blazor - بخش 8 - کار با جاوا اسکریپت» انجام دادیم، در اینجا هم قصد افزودن یکسری کتابخانه‌ی جاوااسکریپتی و CSS ای را داریم که توسط LibMan آن‌ها را مدیریت خواهیم کرد.
- بنابراین در ابتدا به پوشه‌ی BlazorWasm.Client\wwwroot\css وارد شده و پوشه‌های پیش‌فرض bootstrap و open-iconic آن‌را حذف می‌کنیم؛ چون تحت مدیریت هیچ package manager ای نیستند و در این حالت، مدیریت به روز رسانی و یا بازیابی آن‌ها به صورت خودکار میسر نیست.
- سپس فایل wwwroot\css\app.css را هم ویرایش کرده و سطر زیر را از ابتدای آن حذف می‌کنیم:
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');
- اکنون دستورات زیر را در ریشه‌ی پروژه‌ی WASM، اجرا می‌کنیم تا کتابخانه‌های مدنظر ما، تحت مدیریت libman، در پوشه‌ی wwwroot/lib نصب شوند:
dotnet tool update -g Microsoft.Web.LibraryManager.Cli
libman init
libman install bootstrap --provider unpkg --destination wwwroot/lib/bootstrap
libman install open-iconic --provider unpkg --destination wwwroot/lib/open-iconic
libman install jquery --provider unpkg --destination wwwroot/lib/jquery
libman install toastr --provider unpkg --destination wwwroot/lib/toastr
این دستورات همچنین فایل libman.json متناظری را نیز جهت اجرای دستور libman restore برای دفعات آتی، تولید می‌کند.

- بعد از نصب بسته‌های ذکر شده، فایل wwwroot\index.html را به صورت زیر به روز رسانی می‌کنیم تا به مسیرهای جدید بسته‌های CSS و JS نصب شده، اشاره کند:
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
    />
    <title>BlazorWasm.Client</title>
    <base href="/" />

    <link href="lib/toastr/build/toastr.min.css" rel="stylesheet" />
    <link
      href="lib/open-iconic/font/css/open-iconic-bootstrap.min.css"
      rel="stylesheet"
    />
    <link href="lib/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
    <link href="css/app.css" rel="stylesheet" />
    <link href="BlazorWasm.Client.styles.css" rel="stylesheet" />
  </head>

  <body>
    <div id="app">Loading...</div>

    <div id="blazor-error-ui">
      An unhandled error has occurred.
      <a href="" class="reload">Reload</a>
      <a class="dismiss">🗙</a>
    </div>

    <script src="lib/jquery/dist/jquery.min.js"></script>
    <script src="lib/toastr/build/toastr.min.js"></script>
    <script src="js/common.js"></script>
    <script src="_framework/blazor.webassembly.js"></script>
  </body>
</html>
مداخل فایل‌های css را در قسمت head و فایل‌های js را پیش از بسته شدن تگ body تعریف می‌کنیم. در اینجا نیازی به ذکر پوشه‌ی آغازین wwwroot نیست؛ چون base href تعریف شده، به این پوشه اشاره می‌کند.

- محتویات فایل wwwroot\css\app.css را هم به صورت زیر تغییر می‌دهیم تا یک spinner و شیوه نامه‌های نمایش تصاویر، به آن اضافه شوند:
.valid.modified:not([type="checkbox"]) {
  outline: 1px solid #26b050;
}

.invalid {
  outline: 1px solid red;
}

.validation-message {
  color: red;
}

#blazor-error-ui {
  background: lightyellow;
  bottom: 0;
  box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
  display: none;
  left: 0;
  padding: 0.6rem 1.25rem 0.7rem 1.25rem;
  position: fixed;
  width: 100%;
  z-index: 1000;
}

#blazor-error-ui .dismiss {
  cursor: pointer;
  position: absolute;
  right: 0.75rem;
  top: 0.5rem;
}

.spinner {
  border: 16px solid silver !important;
  border-top: 16px solid #337ab7 !important;
  border-radius: 50% !important;
  width: 80px !important;
  height: 80px !important;
  animation: spin 700ms linear infinite !important;
  top: 50% !important;
  left: 50% !important;
  transform: translate(-50%, -50%);
  position: absolute !important;
}

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }

  100% {
    transform: rotate(360deg);
  }
}

.room-image {
  display: block;
  width: 100%;
  height: 150px;
  background-size: cover !important;
  border: 3px solid green;
  position: relative;
}

.room-image-title {
  position: absolute;
  top: 0;
  right: 0;
  background-color: green;
  color: white;
  padding: 0px 6px;
  display: inline-block;
}
- همچنین فایل جدید wwwroot\js\common.js را که در قسمت 11 این سری ایجاد کردیم، به پروژه‌ی جاری نیز با محتوای زیر اضافه می‌کنیم تا سبب سهولت دسترسی به toastr شود:
window.ShowToastr = (type, message) => {
  if (type === "success") {
    toastr.success(message, "Operation Successful", { timeOut: 10000 });
  }
  if (type === "error") {
    toastr.error(message, "Operation Failed", { timeOut: 10000 });
  }
};

- در قسمت 11، در بخش «کاهش کدهای تکراری فراخوانی متدهای جاوا اسکریپتی با تعریف متدهای الحاقی» آن، کلاس JSRuntimeExtensions را تعریف کردیم که سبب کاهش تکرار کدهای استفاده از تابع ShowToastr می‌شود. این فایل‌را در پروژه‌ی BlazorServer.App\Utils\JSRuntimeExtensions.cs این سری نیز استفاده کردیم. یا می‌توان مجددا آن‌را به پروژه‌ی جاری کپی کرد؛ یا آن‌را در یک پروژه‌ی اشتراکی قرار داد. برای مثال اگر آن‌را به پوشه‌ی BlazorWasm.Client\Utils کپی کردیم، نیاز است فضای نام آن‌را اصلاح کرده و سپس آن‌را به انتهای فایل BlazorWasm.Client\_Imports.razor نیز اضافه کنیم تا در تمام کامپوننت‌های برنامه قابل استفاده شود:
@using BlazorWasm.Client.Utils


تغییر و ساده سازی منوی برنامه‌ی کلاینت

در برنامه‌ی کلاینت جاری دیگر نمی‌خواهیم منوی پیش‌فرض سمت چپ صفحه را شاهد باشیم. به همین جهت ابتدا فایل Shared\MainLayout.razor را به صورت زیر ساده می‌کنیم:
@inherits LayoutComponentBase

<NavMenu />
<div>
  @Body
</div>
سپس محتوای فایل Shared\NavMenu.razor را نیز حذف کرده و با تعاریف زیر جایگزین می‌کنیم:
<nav class="navbar navbar-expand-sm navbar-dark bg-dark p-0">
    <a class="navbar-brand mx-4" href="#">Navbar</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse"
            data-target="#navbarSupportedContent"
            aria-controls="navbarSupportedContent"
            aria-expanded="false"
            aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse pr-2" id="navbarSupportedContent">
        <ul class="navbar-nav mr-auto"></ul>
        <ul class="my-0 navbar-nav">
            <li class="nav-item p-0">
                <NavLink class="nav-link" href="registration">
                    <span class="p-2">
                        Register
                    </span>
                </NavLink>
            </li>
            <li class="nav-item p-0">
                <NavLink class="nav-link" href="login">
                    <span class="p-2">
                        Login
                    </span>
                </NavLink>
            </li>
        </ul>
    </div>
</nav>
تا اینجا اگر برنامه‌ی سمت کلاینت را اجرا کنیم، شکل زیر را پیدا کرده که به همراه یک navbar افقی قرار گرفته‌ی در بالای صفحه است؛ به همراه دو لینک به قسمت‌های ثبت‌نام و لاگین:



تغییر محتوای صفحه‌ی آغازین برنامه


صفحه‌ی ابتدایی برنامه، یعنی کامپوننت Pages\Index.razor را نیز به صورت زیر تغییر می‌دهیم:
@page "/"

<form>
    <div class="row p-0 mx-0 mt-4">
        <div class="col-12 col-md-5  offset-md-1 pl-2  pr-2 pr-md-0">
            <div class="form-group">
                <label>Check In Date</label>
                <input type="text" class="form-control" />
            </div>
        </div>
        <div class="col-8 col-md-3 pl-2 pr-2">
            <div class="form-group">
                <label>No. of nights</label>
                <select class="form-control">
                    @for (var i = 1; i <= 10; i++)
                    {
                        <option value="@i">@i</option>
                    }
                </select>
            </div>
        </div>
        <div class="col-4 col-md-2 p-0 pr-2">
            <div class="form-group">
                <label>&nbsp;</label>
                <input type="submit" value="Go" class="btn btn-success btn-block" />
            </div>
        </div>
    </div>
</form>
در اینجا فرمی تعریف شده که تاریخ ورود و رزرو اتاقی را مشخص می‌کند؛ به همراه دراپ‌داونی برای انتخاب تعداد شب‌های اقامت مدنظر.


تعریف View Model رابط کاربری Pages\Index.razor

پس از تعریف محتوای ثابت برنامه، اکنون نوبت به پویا سازی آن است. به همین جهت نیاز است مدلی را برای صفحه‌ی آغازین برنامه تعریف کرد تا بتوان فرم آن‌را به این مدل متصل کرد. این مدل چون مختص به برنامه‌ی کلاینت است، آن‌را در پوشه‌ی جدید Models\ViewModels ایجاد می‌کنیم:
using System;

namespace BlazorWasm.Client.Models.ViewModels
{
    public class HomeVM
    {
        public DateTime StartDate { get; set; } = DateTime.Now;

        public DateTime EndDate { get; set; }

        public int NoOfNights { get; set; } = 1;
    }
}
در اینجا EndDate، یک خاصیت محاسباتی است که بر اساس تاریخ شروع و تعداد شب‌های انتخابی، قابل محاسبه‌است.
پس از این تعریف، بهتر است فضای نام آن‌را نیز به فایل BlazorWasm.Client\_Imports.razor افزود، تا کار با آن در کامپوننت‌های برنامه، ساده‌تر شود:
using BlazorWasm.Client.Models.ViewModels
اکنون می‌توان فرم Pages\Index.razor را به مدل فوق متصل کرد که شامل این تغییرات است:
- ابتدا فیلدی که ارائه کننده‌ی شیء ViewModel فرم است را تعریف می‌کنیم:
@code{
    HomeVM HomeModel = new HomeVM();
}
- سپس بجای یک form ساده، از EditForm اشاره کننده‌ی به این فیلد، استفاده خواهیم کرد:
<EditForm Model="HomeModel">
 // ...
</EditForm>
- در آخر بجای input معمولی، از کامپوننت InputDate متصل به HomeModel.StartDate :
<InputDate min="@DateTime.Now.ToString("yyyy-MM-dd")"
           @bind-Value="HomeModel.StartDate"
           type="text"
           class="form-control" />
و بجای select معمولی، از نمونه‌ی متصل شده‌ی به HomeModel.NoOfNights استفاده می‌کنیم:
<select @bind="HomeModel.NoOfNights">


تعریف Local Storage سمت کلاینت

در ادامه می‌خواهیم اگر کاربری زمان شروع رزرو اتاقی را به همراه تعداد شب مدنظر، انتخاب کرد، با کلیک بر روی دکمه‌ی Go، به یک صفحه‌ی مشاهده‌ی جزئیات منتقل شود. بنابراین نیاز داریم تا اطلاعات انتخابی کاربر را به نحوی ذخیره سازی کنیم. برای یک چنین سناریوی سمت کلاینتی، می‌توان از local storage استاندارد مرورگرها استفاده کرد که امکان کار آفلاین با برنامه را نیز فراهم می‌کند.
برای این منظور کتابخانه‌ای به نام Blazored.LocalStorage طراحی شده‌است که پس از نصب آن توسط دستور زیر:
dotnet add package Blazored.LocalStorage
نیاز است سرویس‌های آن‌را به سیستم تزریق وابستگی‌های برنامه اضافه کرد. در برنامه‌های Blazor Server، اینکار را در فایل Startup برنامه انجام می‌دادیم؛ اما در اینجا، سرویس‌ها در فایل Program.cs تعریف می‌شوند:
namespace BlazorWasm.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            // ...
            builder.Services.AddBlazoredLocalStorage();
            // ...
        }
    }
}
پس از این تعاریف می‌توان از سرویس ILocalStorageService آن در کامپوننت‌های برنامه استفاده کرد. البته جهت سهولت استفاده‌ی از این سرویس بهتر است فضای نام آن‌را به فایل BlazorWasm.Client\_Imports.razor افزود:
@using Blazored.LocalStorage
اکنون برای استفاده از آن به کامپوننت Pages\Index.razor مراجعه کرده و سرویس‌های ILocalStorageService و IJSRuntime را به کامپوننت تزریق می‌کنیم:
@page "/"

@inject ILocalStorageService LocalStorage
@inject IJSRuntime JsRuntime

<EditForm Model="HomeModel" OnValidSubmit="SaveInitialData">
همچنین متدی را هم برای مدیریت رویداد OnValidSubmit تعریف خواهیم کرد:
@code{
    HomeVM HomeModel = new HomeVM();

    private async Task SaveInitialData()
    {
        try
        {
            HomeModel.EndDate = HomeModel.StartDate.AddDays(HomeModel.NoOfNights);
            await LocalStorage.SetItemAsync("InitialRoomBookingInfo", HomeModel);
        }
        catch (Exception e)
        {
            await JsRuntime.ToastrError(e.Message);
        }
    }
}
در اینجا با استفاده از متد SetItemAsync و ذکر یک کلید دلخواه، اطلاعات مدل فرم را در local storage مرورگر ذخیره کرده‌ایم. همچنین اگر خطایی هم رخ دهد توسط ToastrError نمایش داده خواهد شد.
برای مثال اگر تاریخ و عددی را انتخاب کنیم، نتیجه‌ی حاصل از کلیک بر روی دکمه‌ی Go را می‌توان در قسمت Local storage مرورگر جاری مشاهده کرد:


البته با توجه به اینکه می‌خواهیم از کلید InitialRoomBookingInfo در سایر کامپوننت‌های برنامه نیز استفاده کنیم، بهتر است آن‌را به یک پروژه‌ی مشترک مانند BlazorServer.Common که پیشتر نام نقش‌هایی مانند Admin را در آن تعریف کردیم، منتقل کنیم:
namespace BlazorServer.Common
{
    public static class ConstantKeys
    {
        public const string LocalInitialBooking = "InitialRoomBookingInfo";
    }
}
سپس باید ارجاعی به آن پروژه را افزوده:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
  <ItemGroup>
    <ProjectReference Include="..\..\BlazorServer\BlazorServer.Common\BlazorServer.Common.csproj" />
  </ItemGroup>
</Project>
همچنین فضای نام آن‌را نیز به فایل BlazorWasm.Client\_Imports.razor اضافه می‌کنیم:
@using BlazorServer.Common
اکنون می‌توان از کلید ثابت تعریف شده‌ی مشترک، استفاده کرد:
await LocalStorage.SetItemAsync(ConstantKeys.LocalInitialBooking, HomeModel);

در آخر قصد داریم با کلیک بر روی Go، به یک صفحه‌ی جدید مانند نمایش لیست اتاق‌ها هدایت شویم. به همین جهت کامپوننت جدید Pages\HotelRooms\HotelRooms.razor را ایجاد می‌کنیم:
@page "/hotel/rooms"

<h3>HotelRooms</h3>

@code {

}
سپس در کامپوننت Pages\Index.razor با استفاده از سرویس NavigationManager، کار هدایت خودکار کاربر را به این کامپوننت جدید انجام خواهیم داد:
@page "/"

@inject ILocalStorageService LocalStorage
@inject IJSRuntime JsRuntime
@inject NavigationManager NavigationManager


@code{
    HomeVM HomeModel = new HomeVM();

    private async Task SaveInitialData()
    {
        try
        {
            HomeModel.EndDate = HomeModel.StartDate.AddDays(HomeModel.NoOfNights);
            await LocalStorage.SetItemAsync(ConstantKeys.LocalInitialBooking, HomeModel);
            NavigationManager.NavigateTo("hotel/rooms");
        }
        catch (Exception e)
        {
            await JsRuntime.ToastrError(e.Message);
        }
    }
}


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-26.zip
مطالب
شروع به کار با DNTFrameworkCore - قسمت 7 - ارتقاء به نسخه ‭4.5.x
بعد از انتشار قسمت 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 مدیریت خواهند شد. 
2. با اضافه شدن واسط IHasRowIntegrity برای پشتیبانی از امکان تشخیص اصالت ردیف‌های یک بانک اطلاعاتی با استفاده از EF Core، خصوصیت RowVersion به Version تغییر نام پیدا کرد.
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; }
}
3-  امکان ذخیره سازی تنظیمات برنامه‌های ASP.NET Core در یک بانک اطلاعاتی با استفاده از EF ، اضافه شده است که از همان موجودیت KeyValue برای نگهداری مقادیر استفاده می‌کند:
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>();
app.UseTenancy();


سایر کتابخانه‌ها تغییرات خاصی نداشتند و صرفا نحوه معرفی سرویس‌های آنها ممکن است تغییر کند و یا وابستگی‌های آنها به آخرین نسخه موجود ارتقاء داده شده باشند که در پروژه 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
مطالب
اعتبارسنجی سفارشی سمت کاربر و سمت سرور در jqGrid
همانطور که در مطلب «فعال سازی و پردازش صفحات پویای افزودن، ویرایش و حذف رکوردهای jqGrid در ASP.NET MVC» نیز ذکر شد، خاصیت editrules یک ستون، برای مباحث اعتبارسنجی اطلاعات ورودی توسط کاربر پیش بینی شده‌است. برای مثال اگر required: true در آن تنظیم شود، کاربر مجبور به تکمیل این سلول خاص خواهد بود. در اینجا خواصی مانند number و integer از نوع bool هستند، minValue و maxValue از نوع عددی، email, url, date, time از نوع bool و custom قابل تنظیم است. در ادامه نحوه‌ی اعمال اعتبارسنجی‌های سفارشی سمت سرور و همچنین سمت کلاینت را بررسی خواهیم کرد.


مدل برنامه و نیازمندی‌های اعتبارسنجی آن

namespace jqGrid08.Models
{
    public class User
    {
        public int Id { set; get; }

        public string Name { set; get; }

        public string Email { set; get; }

        public string Password { set; get; }

        public string SiteUrl { set; get; }
    }
}
مدل کاربر فوق را در نظر بگیرید. در حین ورود اطلاعات نیاز است:
- نام کاربر به صورت اجباری وارد شود و همچنین بین 3 تا 40 حرف باشد.
- همچنین نام کاربر نباید بر اساس اطلاعات موجود در بانک اطلاعاتی، تکراری وارد شود.
- ورود ایمیل شخص اجباری است؛ به علاوه فرمت آن نیز باید با یک ایمیل واقعی تطابق داشته باشد.
- ایمیل وارد شده‌ی یک کاربر جدید نیز نباید تکراری بوده و پیشتر توسط کاربر دیگری وارد شده باشد.
- ورود کلمه‌ی عبور در حالت ثبت اطلاعات اجباری است؛ اما در حالت ویرایش اطلاعات خیر (از کلمه‌ی عبور موجود در این حالت استفاده خواهد شد).
- ورود آدرس سایت کاربر اجباری بوده و همچنین فرمت آدرس وارد شده نیز باید معتبر باشد.


اعتبار سنجی سمت سرور و سمت کلاینت نام کاربر

                colModel: [
                    {
                        name: '@(StronglyTyped.PropertyName<User>(x => x.Name))',
                        index: '@(StronglyTyped.PropertyName<User>(x => x.Name))',
                        align: 'right', width: 150,
                        editable: true, edittype: 'text',
                        editoptions: {
                            maxlength: 40
                        },
                        editrules: {
                            required: true,
                            custom: true,
                            custom_func: function (value, colname) {
                                if (!value)
                                    return [false, "لطفا نامی را وارد کنید"];

                                if (value.length < 3 || value.length > 40)
                                    return [false, colname + " باید بین 3 تا 40 حرف باشد"];
                                else
                                    return [true, ""];
                            }
                        }
                    },
                ],
با تنظیم required: true، کار تنظیم ورود اجباری نام کاربر به پایان می‌رسد. اما نیاز است این نام بین 3 تا 40 حرف باشد. بنابراین نیاز است سمت کاربر بتوان اطلاعات وارد شده توسط کاربر را دریافت کرده و سپس طول آن‌را بررسی نمود. این‌کار، توسط مقدار دهی خاصیت custom به true و سپس تعریف متدی برای custom_func قابل انجام است.
خروجی این متد یک آرایه دو عضوی است. اگر عضو اول آن true باشد، یعنی اعتبارسنجی موفقیت آمیز بوده‌است؛ اگر خیر، عضو دوم آرایه، پیامی است که به کاربر نمایش داده خواهد شد.
تا اینجا کار اعتبارسنجی سمت کاربر به پایان می‌رسد. اما نیاز است در سمت سرور نیز بررسی شود که آیا نام وارد شده تکراری است یا خیر. برای این منظور تنها کافی است رویداد afterSubmit حالت‌های Add و Edit را بررسی کنیم:
            $('#list').jqGrid({
                // ...
            }).navGrid(
                '#pager',
                //enabling buttons
                { add: true, del: true, edit: true, search: false },
                //edit option
                {
                    afterSubmit: showServerSideErrors
                },
                //add options
                {
                    afterSubmit: showServerSideErrors
                },
                //delete options
                {
                });
        });

        function showServerSideErrors(response, postdata) {
            var result = $.parseJSON(response.responseText);
            if (result.success === false) {
                //نمایش خطای اعتبار سنجی سمت سرور پس از ویرایش یا افزودن
                //و همچنین جلوگیری از ثبت نهایی فرم
                return [false, result.message, result.id];
            }

            return [true, "", result.id];
        }
شیء response، حاوی اطلاعات بازگشت داده شده از طرف سرور است. برای مثال یک چنین خروجی JSON ایی را در حالت‌های شکست اعتبارسنجی بازگشت می‌دهیم:
        [HttpPost]
        public ActionResult AddUser(User postData)
        {
            //todo: Add user to repository
            if (postData == null)
                return Json(new { success = false, message = "اطلاعات دریافتی خالی است" }, JsonRequestBehavior.AllowGet);

            if (_usersInMemoryDataSource.Any(
                    user => user.Name.Equals(postData.Name, StringComparison.InvariantCultureIgnoreCase)))
            {
                return Json(new { success = false, message = "نام کاربر تکراری است" }, JsonRequestBehavior.AllowGet);
            }

            if (_usersInMemoryDataSource.Any(
                    user => user.Email.Equals(postData.Email, StringComparison.InvariantCultureIgnoreCase)))
            {
                return Json(new { success = false, message = "آدرس ایمیل کاربر تکراری است" }, JsonRequestBehavior.AllowGet);
            }

            postData.Id = _usersInMemoryDataSource.LastOrDefault() == null ? 1 : _usersInMemoryDataSource.Last().Id + 1;
            _usersInMemoryDataSource.Add(postData);

            return Json(new { id = postData.Id, success = true }, JsonRequestBehavior.AllowGet);
        }
در سمت کلاینت در روال رویدادگردان afterSubmit می‌توان با آنالیز response و سپس استخراج فیلدهای JSON آن، وضعیت success و همچنین پیام‌های بازگشت داده شده را بررسی کرد.
خروجی روال رویدادگردان afterSubmit نیز بسیار شبیه است به حالت اعتبارسنجی سفارشی یک ستون. اگر عضو اول آرایه بازگشت داده شده توسط آن false باشد، یعنی اعتبارسنجی سمت سرور، با شکست مواجه شده و در این حالت از عضو دوم آرایه برای نمایش پیام خطای بازگشت داده شده از طرف سرور استفاده خواهد شد.



اعتبار سنجی ایمیل کاربر

                colModel: [
                    {
                        name: '@(StronglyTyped.PropertyName<User>(x => x.Email))',
                        index: '@(StronglyTyped.PropertyName<User>(x => x.Email))',
                        align: 'center', width: 150,
                        editable: true, edittype: 'text',
                        editoptions: {
                            maxlength: 250,
                            dir: 'ltr'
                        },
                        editrules: {
                            required: true,
                            email: true
                        },
                        formatter: 'email'
                    },
                ],
با تنظیم required: true، کار تنظیم ورود اجباری ایمیل کاربر به پایان می‌رسد. همچنین با تنظیم email: true، به صورت خودکار فرمت ایمیل وارد شده نیز بررسی می‌شود.
مطابق نیازمندی‌های اعتبارسنجی پروژه، ایمیل وارد شده نیز نباید تکراری باشد. این مورد نیز توسط خروجی روال رویدادگردان afterSubmit که پیشتر توضیح داده شده، مدیریت می‌شود.



اعتبار سنجی کلمه عبور کاربر

                colModel: [
                    {
                        name: '@(StronglyTyped.PropertyName<User>(x => x.Password))',
                        index: '@(StronglyTyped.PropertyName<User>(x => x.Password))',
                        align: 'center', width: 70,
                        editable: true, edittype: 'password',
                        editoptions: {
                            maxlength: 10,
                            dir: 'ltr'
                        },
                        editrules: {
                            //required: true ---> در این حالت خاص قابل استفاده نیست
                            //در حالت ویرایش رکورد، ورود کلمه عبور اختیاری است
                            //در حالت افزودن رکورد، ورود کلمه عبور اجباری است
                        }
                    },
                ],
حالت بررسی اعتبارسنجی کلمه‌ی عبور در اینجا، حالت ویژه‌ای است. نیاز است در حالت ثبت اطلاعات اجباری باشد اما در حالت ویرایش خیر. بنابراین نمی‌توان از خاصیت required: true استفاده کرد؛ چون به هر دو حالت ویرایش و ثبت اطلاعات به صورت یکسان اعمال می‌شود.
برای این منظور تنها کافی است از روال رویدادگردان beforeSubmit استفاده کرد:
            $('#list').jqGrid({
                // ...
            }).navGrid(
                '#pager',
                //enabling buttons
                { add: true, del: true, edit: true, search: false },
                //edit option
                {
                    /*, beforeSubmit: function (posdata, obj) {
                        //در حالت ویرایش رکورد، ورود کلمه عبور اختیاری است
                        return [true, ""];
                    }*/
                },
                //add options
                {
                    beforeSubmit: function (postdata, obj) {
                        //در حالت افزودن رکورد، ورود کلمه عبور اجباری است
                        if (postdata.Password == null || postdata.Password == "" || postdata.Password == undefined)
                            return [false, "لطفا کلمه عبور را وارد کنید"];
                        return [true, ""];
                    }
                },
                //delete options
                {
                });
        });
چون می‌خواهیم تنها حالت Add را تحت کنترل قرار دهیم، رویدادگردان beforeSubmit آن‌را مقدار دهی کرده‌ایم. توسط postdata کلیه اطلاعات قابل ارسال به سرور به صورت یک شیء جاوا اسکریپتی یا JSON در اختیار ما است. سپس با بررسی برای مثال postdata.Password می‌توان در مورد مقدار کلمه‌ی عبور تصمیم گیری کرد. در اینجا نیز خروجی متد باید یک آرایه دو عضوی باشد تا در صورت false بودن اولین عضو آن، پیام سفارشی اعتبارسنجی خاصی را بتوان به کاربر نمایش داد.



اعتبار سنجی آدرس سایت کاربر

                colModel: [
                    {
                        name: '@(StronglyTyped.PropertyName<User>(x => x.SiteUrl))',
                        index: '@(StronglyTyped.PropertyName<User>(x => x.SiteUrl))',
                        align: 'center', width: 150,
                        editable: true, edittype: 'text',
                        editoptions: {
                            maxlength: 1000,
                            dir: 'ltr'
                        },
                        editrules: {
                            required: true,
                            url: true
                        },
                        formatter: function (cellvalue, options, rowObject) {
                            return "<a href='" + cellvalue + "' >" + cellvalue + "</a>";
                        },
                        unformat: function (cellvalue, options, cell) {
                            return $('a', cell).attr('href');
                        }
                    },
                ],
با تنظیم required: true، کار تنظیم ورود اجباری آدرس سایت کاربر به پایان می‌رسد. همچنین با تنظیم url: true، به صورت خودکار فرمت URL وارد شده نیز بررسی می‌شود.



کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید
jqGrid08.zip
نظرات مطالب
تولید هدرهای Content Security Policy توسط ASP.NET Core برای برنامه‌های Angular
یک نکته‌ی تکمیلی: پردازش صحیح X-Content-Type-Options در کروم 65

اگر هدر X-Content-Type-Options را به nosniff تنظیم کرده باشید، کروم 65 از اجرای فایل‌های اسکریپت با خطای زیر سر باز می‌زند:
Refused to execute script from '<URL>' because its MIME type ('') is not executable, and strict MIME type checking is enabled.
چون تنظیم پیش فرض app.UseStaticFiles، هیچ نوع content-type ایی را نمی‌شناسد.
برای رفع آن باید به صورت زیر content-type صحیحی را به فایل‌های اسکریپت تولیدی نسبت داد:
var provider = new FileExtensionContentTypeProvider();
app.UseStaticFiles(new StaticFileOptions
{
    ContentTypeProvider = provider
});