مطالب
معرفی Async Parallel.ForEach در دات نت 6
عموما زمانیکه می‌خواهیم تمام وظایف مدنظر، به صورت موازی اجرا شوند، آن‌ها را Task.WhenAll می‌کنیم. برای مثال 10 هزار درخواست HTTP را به صورت وظایفی، WhenAll می‌کنیم و ... در این حالت ... سرور ریموت، IP شما را خواهد بست! چون کنترلی بر روی تعداد وظیفه‌ی در حالت اجرای موازی وجود ندارد و یک چنین عملی، شبیه به یک حمله‌ی DDOS عمل می‌کند! برای مدیریت بهتر یک چنین مواردی، در دات نت 6 متدهای Parallel.ForEachAsync ارائه شده‌اند تا دیگر نیازی به استفاده از راه‌حل‌های ثالثی که عموما آنچنان بهینه هم نیستند، نباشد.
public static Task ForEachAsync<TSource>(IEnumerable<TSource> source, Func<TSource, CancellationToken, ValueTask> body)
public static Task ForEachAsync<TSource>(IEnumerable<TSource> source, CancellationToken cancellationToken, Func<TSource, CancellationToken, ValueTask> body)
public static Task ForEachAsync<TSource>(IEnumerable<TSource> source, ParallelOptions parallelOptions, Func<TSource, CancellationToken, ValueTask> body)
public static Task ForEachAsync<TSource>(IAsyncEnumerable<TSource> source, Func<TSource, CancellationToken, ValueTask> body)
public static Task ForEachAsync<TSource>(IAsyncEnumerable<TSource> source, CancellationToken cancellationToken, Func<TSource, CancellationToken, ValueTask> body)
public static Task ForEachAsync<TSource>(IAsyncEnumerable<TSource> source, ParallelOptions parallelOptions, Func<TSource, CancellationToken, ValueTask> body)
این مجموعه متدها از ValueTaskها بجای Taskها استفاده می‌کند تا سربار ایجاد Taskها در حلقه‌ها کاهش یابد. همچنین در اینجا degree of parallelism به صورت پیش‌فرض به تعداد هسته‌های سی‌پی تنظیم شده‌است (Environment.ProcessorCount)؛ چون عموما توسعه دهنده‌ها نمی‌دانند که چه عددی را باید برای آن انتخاب کنند. هر چند امکان تنظیم دستی آن‌ها هم وجود دارد (یکی از مهم‌ترین مشکلات کار با WhenAll).

یک مثال: در اینجا می‌خواهیم به صورت موازی، مشخصات کاربرانی از Github را توسط HttpClient دریافت کنیم. هر بار هم فقط می‌خواهیم سه وظیفه اجرا شوند و نه بیشتر
using System.Net.Http.Headers;
using System.Net.Http.Json;
 
var userHandlers = new []  { "users/VahidN", "users/shanselman", "users/jaredpar", "users/davidfowl" };
 
using HttpClient client = new()
{
    BaseAddress = new Uri("https://api.github.com"),
};
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("DotNet", "6"));
 
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = 3 };
 
await Parallel.ForEachAsync(userHandlers, parallelOptions, async (uri, token) =>
{
    var user = await client.GetFromJsonAsync<GitHubUser>(uri, token); 
    Console.WriteLine($"Name: {user.Name}\nBio: {user.Bio}\n");
});
 
public class GitHubUser
{
    public string Name { get; set; }
    public string  Bio { get; set; }
}
در این مثال، نمونه‌ای از کارکرد متد جدید Parallel.ForEachAsync را مشاهده می‌کنید که اینبار، MaxDegreeOfParallelism آن قابل تنظیم است. یعنی با تنظیم فوق، هربار فقط سه وظیفه به صورت موازی اجرا خواهند شد. البته تنظیم آن به منهای یک، همان حالت WhenAll را سبب خواهد شد؛ یعنی محدودیتی وجود نخواهد داشت.
متد Parallel.ForEachAsync، آرایه‌ای را که باید بر روی آن کار کند، دریافت می‌کند. سپس تنظیمات اجرای موازی آن‌ها را هم مشخص می‌کنیم. در ادامه آن‌ها را در دسته‌های مشخصی، به صورت موازی بر اساس منطقی که مشخص می‌کنیم، اجرا خواهد کرد.


وضعیت امکان اجرای موازی متدهای async همزمان، تا پیش از دات نت 6

<List<T به همراه متد الحاقی ForEach است که می‌تواند یک <Action<T را بر روی المان‌های این لیست، اجرا کند و ... عموما زمانیکه به وظایف async می‌رسیم، به اشتباه مورد استفاده قرار می‌گیرد:
customers.ForEach(c => SendEmailAsync(c));
مثال فوق، با اجرای حلقه‌ی زیر تفاوتی ندارد:
foreach(var c in customers)
{
    SendEmailAsync(c); // the return task is ignored
}
یعنی یک عملیات async، بدون await فراخوانی شده‌است و تا پایان عملیات مدنظر، صبر نخواهد شد. حداقل مشکل آن این است که اگر در این بین استثنایی رخ دهد، هیچگاه متوجه آن نخواهید شد و حتی می‌تواند کل پروسه‌ی برنامه را خاتمه دهد. شاید عنوان کنید که می‌شود این مشکل را به صورت زیر حل کرد:
customers.ForEach(async c => await SendEmailAsync(c));
اما ... این روش هم تفاوتی با قبل ندارد. از این لحاظ که متد ForEach یک <Action<T را دریافت می‌کند که خروجی آن void است. یعنی در نهایت با راه حل دوم، فقط یک async void ایجاد می‌شود که باز هم قابلیت صبر کردن تا پایان عملیات را ندارد. نکته‌ی مهم اینجا است که اجرای موازی آن‌ها توسط متد Parallel.ForEach نیز دقیقا همین مشکل را دارد.
تنها راه حل پذیرفته‌ی شده‌ی چنین عمل async ای، فراخوانی آن‌ها به صورت متداول زیر و بدون استفاده از متد ForEach است:
foreach(var c in customers)
{
   await SendEmailAsync(c);
}
و یا Task.WhenAll کردن آن‌ها، با علم به این موضوع که MaxDegreeOfParallelism آن قابل کنترل نیست (حداقل به صورت استاندارد و بدون نیاز به کتابخانه‌های جانبی). برای مثال بجای نوشتن:
foreach(var o in orders)
{
    await ProcessOrderAsync(o);
}
می‌توان آن‌را به صورت زیر درآورد:
var tasks = orders.Select(o => ProcessOrderAsync(o)).ToList();
await Task.WhenAll(tasks);
در این حالت عملیات ProcessOrderAsync را تبدیل به لیستی از وظایف مدنظر کرده و به متد Task.WhenAll ارسال می‌کنیم تا به صورت موازی اجرا شوند. اما ... اگر 10 هزار Task وجود داشته باشند، کنترلی بر روی تعداد وظایف در حال اجرای موازی وجود نخواهد داشت و این مورد نه تنها سبب بالا رفتن کارآیی نخواهد شد، بلکه می‌تواند سرور را هم با اخلال پردازشی، به علت کمبود منابع در دسترس مواجه کند.

دات نت 6، هم کنترل MaxDegreeOfParallelism را میسر کرده‌است و هم اینکه اینبار نگارش async واقعی Parallel.ForEachAsync را ارائه داده‌است تا دیگر همانند حالت قبلی Parallel.ForEach، به async void‌ها و مشکلات مرتبط با آن‌ها نرسیم.
بازخوردهای دوره
افزونه‌ای برای کپسوله سازی نکات ارسال یک فرم ASP.NET MVC به سرور توسط jQuery Ajax
با سلام خدمت آقای نصیری 
با تشکر از مطلب کاربردی جاری ، یک سوال داشتم چطور می‌شه با این روش validation‌های سمت سرور رو به کاربر نمایش بدهیم به طور مثال اگه در اینجا
[Required(ErrorMessage = "(*)"), DisplayName("نام")]
        [MaxLength(3,ErrorMessage="لطفا بیشتر از 3 کاراکتر وارد ننمایید")]
        public string Name { set; get; }

از MaxLength و یا یک سری Att‌های خاصی استفاده بشه چطور میشه پیغام "لطفا بیشتر از 3 کاراکتر وارد ننمایید" را به کاربر نشان داد ، در حالت فعلی فقط پیغام خطایی رخ داده ظاهر می‌شه 
با تشکر
نظرات اشتراک‌ها
معرفی کتابخانه‌ی DNTPersianUtils.Core
با سلام 
موقع استفاده از dt.ToGregorianDateTime خطای
Cannot implicitly convert type 'System.DateTime?' to 'System.DateTime'. An explicit conversion exists
میگیره (  مدل:      public string UserBirthday { get; set; }  ) 
چطوری میتونم حلش کنم ؟  
و اینکه برای تبدیل یکسری روزها قبلا به مشکل برمیخوردیم مثل تاریخ‌های 02/29 یا 02/30. برای این تاریخ‌ها مشکلی بوجود نمیاد؟
برای ثبت تاریخ در دیتابیس ، شمسی وارد بشه بهتره یا میلادی؟
نظرات مطالب
روش‌هایی برای بهبود تجربه‌ی کاربری صفحات لاگین و ثبت نام
وجود ندارد به معنای عدم امکان افزودن آن‌ها نیست. تمام کامپوننت‌های استاندارد Blazor به همراه خاصیت زیر هم هستند:
[Parameter(CaptureUnmatchedValues = true)] 
public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; }
با تعریف پارامتر از نوع CaptureUnmatchedValues = true، تمام ویژگی‌های اضافه شده که به صورت پارامتر عمومی کامپوننت نیستند، به عنوان Unmatched Values تفسیر شده و مورد استفاده قرار می‌گیرند. یعنی همینقدر که ویژگی "autocomplete="new-password را به تعریف کامپوننت اضافه کردید (و هر مورد مشابه دیگری را)، یک Unmatched Value است و به صورت خودکار در حین رندر نهایی، در المان اضافه شده‌ی به صفحه، ظاهر می‌شود.
نظرات مطالب
فعال سازی قسمت ارسال فایل و تصویر ویرایشگر آنلاین RedActor در ASP.NET MVC
با سلام.
من خاصیتی از ویومدل را بصورت زیر تعریف کردم:
[Required(ErrorMessageResourceType = typeof(ValidationErrorsResource),
            ErrorMessageResourceName = ResourceKeys.ValidationErrorsResource.RequiredField)]
        [MaxLength]
        [DisplayName("متن")]
        [AllowHtml]
        public string FullDescription { get; set; }
در ویو هم بصورت زیر:
<div>متن: </div>
                <div>@Html.TextAreaFor(p => p.FullDescription)</div>
                <div>@Html.ValidationMessageFor(p => p.FullDescription)</div>

$('#FullDescription').redactor({
            autoformat: false,
            convertDivs: false
        });
ولی وقتی کاربر حتی چیزی وارد نمیکند در دیتابیس مقدار  "<p><br></p> " ذخیره می‌شود و همین باعث میشود Required کار نکند و پیغامی به کاربر نمایش نمی‌دهد.
ایراد کارم از کجاست؟ باتشکر.
نظرات مطالب
انجام کارهای زمانبندی شده در برنامه‌های ASP.NET توسط DNT Scheduler
زمانی که وظیفه ای میخواهد اجرا شود همزمان با آن کنترلر Home هم فراخوانی می‌شود
این درخواست هایی هست که لاگ VS قسمت Output ثبت می‌شود

Application Insights Telemetry (unconfigured): {"name":"Microsoft.ApplicationInsights.Dev.Metric","time":"2019-01-08T11:23:01.0000000Z","tags":{"ai.cloud.roleInstance":"BAHARI-PC","ai.internal.sdkVersion":"m-agg2:2.8.1-22898"},"data":{"baseType":"MetricData","baseData":{"ver":2,"metrics":[{"name":"Dependency duration","kind":"Aggregation","value":4143.777,"count":1,"min":4143.777,"max":4143.777,"stdDev":0}],"properties":{"_MS.MetricId":"dependencies/duration","_MS.IsAutocollected":"True","_MS.AggregationIntervalMs":"60000","Dependency.Type":"Http","DeveloperMode":"true","Dependency.Success":"True"}}}}
Application Insights Telemetry (unconfigured): {"name":"Microsoft.ApplicationInsights.Dev.Metric","time":"2019-01-08T11:23:01.0000000Z","tags":{"ai.cloud.roleInstance":"BAHARI-PC","ai.internal.sdkVersion":"m-agg2:2.8.1-22898"},"data":{"baseType":"MetricData","baseData":{"ver":2,"metrics":[{"name":"Server response time","kind":"Aggregation","value":4136.7653,"count":1,"min":4136.7653,"max":4136.7653,"stdDev":0}],"properties":{"_MS.MetricId":"requests/duration","_MS.IsAutocollected":"True","_MS.AggregationIntervalMs":"60000","DeveloperMode":"true","Request.Success":"True"}}}}
Application Insights Telemetry (unconfigured): {"name":"Microsoft.ApplicationInsights.Dev.Metric","time":"2019-01-08T11:23:01.0000000Z","tags":{"ai.cloud.roleInstance":"BAHARI-PC","ai.internal.sdkVersion":"m-agg2:2.8.1-22898"},"data":{"baseType":"MetricData","baseData":{"ver":2,"metrics":[{"name":"Dependency duration","kind":"Aggregation","value":3972.6929,"count":3,"min":12.8297,"max":2687.0447,"stdDev":1092.34881420812}],"properties":{"_MS.MetricId":"dependencies/duration","_MS.IsAutocollected":"True","_MS.AggregationIntervalMs":"60000","Dependency.Type":"SQL","DeveloperMode":"true","Dependency.Success":"True"}}}}
این هم نمونه ای از وظیفه:
#region Using

using Autofac;
using CHK.ServiceLayer.Interfaces;
using DNTScheduler;
using System;

#endregion

namespace CHK.Web.Scheduler
{
    public class InventoryTask : ScheduledTaskTemplate
    {
        #region Properties

        public override int Order => 1;
        public override string Name => "Inventory-DiscountCoupon-GiftCard";
        public IContainer Container { get; set; }

        #endregion

        #region Methods

        public override bool RunAt(DateTime utcNow)
        {
            if (IsShuttingDown || Pause) return false;
            var currentDateTime = utcNow.AddHours(3.5);
            return (currentDateTime.Minute % 15 == 0 && currentDateTime.Second > 5 && currentDateTime.Second < 15);
        }

        public override void Run()
        {
            if (IsShuttingDown || Pause) return;
            Pause = true;
            using (var scope = Container.BeginLifetimeScope())
            {
                var schedulerService = scope.Resolve<ISchedulerService>();
                schedulerService.UpdateInventory();
            }
            Pause = false;
        }

        #endregion
    }
}

مطالب
شروع به کار با DNTFrameworkCore - قسمت 3 - پیاده‌سازی سرویس‌های موجودیت‌ها
در قسمت قبل سناریوهای مختلف مرتبط با طراحی موجودیت‌های سیستم را بررسی کردیم. در این قسمت به طراحی DTO‌های متناظر با موجودیت‌ها به همراه اعتبارسنج‌های مرتبط و در نهایت به پیاده سازی سرویس‌های CRUD آنها خواهیم پرداخت. 
قراردادها، مفاهیم و نکات اولیه
  1. برخلاف بسیاری از طراحی‌های موجود، بر فراز هر موجودیت اصلی (منظور AggregateRoot) باید یک DTO که از این پس با عنوان Model از آنها یاد خواهیم کرد، تعریف شود. 
  2. هیچ تراکنشی برای موجودیت‌های فرعی یا همان Detailها نخواهیم داشت. این موجودیت‌ها در تراکنش موجودیت اصلی مرتبط به آن مدیریت خواهند شد.
  3. هر Commandای که قرار است مرتبط با یک موجودیت اصلی در سیستم انجام پذیرد، باید از منطق تجاری آن موجودیت عبور کند و نباید با دور زدن منطق تجاری، از طرق مختلف تغییراتی بر آن موجودیت اعمال شود. (موضوع مهمی که در ادامه مطلب جاری تشریح خواهد شد)
  4. ویوهای مختلفی از یک موجودیت می‌توان انتظار داشت که ویو پیش‌فرض آن در CrudService تدارک دیده شده است. برای سایر موارد نیاز است در سرویس مرتبط، متدهای Read مختلفی را پیاده‌سازی کنید.
  5. با اعمال اصل CQS، متدهای ثبت و ویرایش در کلاس سرویس پایه CRUD، بعد از انجام عملیات مربوطه، Id و RowVersion مدل ورودی و هچنین Id و TrackingState موجودیت‌های فرعی وابسته، مقداردهی خواهند شد و نیاز به انجام یک Query دیگر و بازگشت آن به عنوان خروجی متدها نبوده است. به همین دلیل خروجی این متدها صرفا Result ای می‌باشد که نشان از امکان Failure بودن انجام آنها می‌باشد که با اصل مذکور در تضاد نمی‌باشد.
  6. ورودی متدهای Read شما که در اکثر موارد نیاز به مهیا کردن خروجی صفحه‌بندی شده دارند، باید از نوع PagedQueryModel و یا اگر همچنین نیاز به جستجوی پویا براساس فیلدهایی موجود در ReadModel مرتبط دارید، باید از نوع FilteredPagedQueryModel باشد. متدهای الحاقی برای اعمال خودکار این صفحه‌‌بندی و جستجوی پویا در نظر گرفته شده است. همچنین خروجی آنها در اکثر موارد از نوع IPagedQueryResult خواهد بود. اگر نیاز است تا جستجوی خاصی داشته باشید که خصوصیتی متناظر با آن فیلد در مدل Read وجود ندارد، لازم است تا از این QueryModel‌های مطرح شده، ارث‌بری کرده و خصوصیت اضافی مدنظر خود را تعریف کنید. بدیهی است که اعمال جستجوی این موارد خاص به عهده توسعه دهنده می‌باشد.
  7. عملیات ثبت، ویرایش و حذف، برای کار بر روی لیستی از وهله‌های Model، طراحی شده‌اند. این موضوع در بسیاری از دومین‌ها قابلیت مورد توجهی می‌باشد. 
  8. رخداد متناظر با عملیات CUD مرتبط با هر موجودیت اصلی، به عنوان یکسری نقاط قابل گسترش (Extensibility Point) در اختیار سایر بخش‌های سیستم می‌باشد. این رخدادها درون تراکنش جاری Raise خواهند شد؛ از این جهت امکان اعمال یکسری Rule جدید از سمت سایر موءلفه‌های سیستم موجود می‌باشد.
  9. برخلاف بسیاری از طراحی‌های موجود، قصد ایجاد لایه انتزاعی برفراز EF Core  به منظور رسیدن به Persistence Ignorance را ندارم. بنابراین امروز بسته DNTFrameworkCore.EntityFramework آن آماده می‌باشد. اگر توسعه دهنده‌ای قصد یکپارچه کردن این زیرساخت را با سایر ORMها یا Micro ORMها داشته باشد، می‌تواند Pull Request خود را ارسال کند.
  10. خبر خوب اینکه هیچ وابستگی به AutoMapper به منظور نگاشت مابین موجودیت‌ها و مدل‌های متناظر آنها، در این زیرساخت وجود ندارد. با پیاده سازی متدهای MapToModel و MapToEntity می‌توانید از کتابخانه Mapper مورد نظر خودتان استفاده کنید؛ یا به صورت دستی این کار را انجام دهید. بعد از چند سال استفاده از AutoMapper، این روزها خیلی اعتقادی به استفاده از آن ندارم.
  11. هیچ وابستگی به FluentValidation به منظور اعتبارسنجی ورودی متدها یا پیاده‌سازی قواعد تجاری، در این زیرساخت وجود ندارد. شما امکان استفاده از Attributeهای اعتبارسنجی توکار، پیاده سازی IValidatableObject توسط مدل یا در موارد خاص به منظور پیاده سازی قواعد تجاری پیچیده، پیاده سازی IModelValidator را دارید. با این حال برای یکپارچگی با این کتابخانه محبوب، می‌توانید بسته نیوگت DNTFrameworkCore.FluentValidation را نصب کرده و استفاده کنید.
  12. با اعمال الگوی Template Method در پیاده سازی سرویس CRUD پایه، از طریق تعدادی متد با پیشوندهای Before و After متناظر با عملیات CUD می‌توانید در فرآیند انجام آنها نیز دخالت داشته باشید؛ به عنوان مثال: BeforeEditAsync یا AfterCreateAsync
  13. باتوجه به اینکه در فرآیند انجام متدهای CUD، یکسری Event هم Raise خواهند شد و همچنین در خیلی از موراد شاید نیاز به فراخوانی SaveChange مرتبط با UnitOfWork جاری باشد، لذا مطمئن‌ترین راه حل برای این قضیه و حفظ ثبات سیستم، همان استفاده از تراکنش محیطی می‌باشد. از این جهت متدهای مذکور با TransactionAttribute نیز تزئین شده‌اند که برای فعال سازی این مکانیزم نیاز است تا TransactionInterceptor مربوطه را به سیستم معرفی کنید.
  14. ValidationInterceptor موجود در زیرساخت، در صورتیکه خروجی متد از نوع Result باشد، خطاهای ممکن را در قالب یک شی Result بازگشت خواهد داد؛ در غیر این صورت یک استثنای ValidationException پرتاب می‌شود که این مورد هم توسط GlobalExceptionFilter مدیریت خواهند شد و در قالب یک BadRequest به کلاینت ارسال خواهد شد.
  15. در سناریوهای Master-Detail، قرارداد این است که Detailها به همراه Master متناظر واکشی خواهند شد و در زمان ثبت و یا ویرایش هم همه آنها به همراه Master متناظر خود به سرور ارسال خواهند شد. 
نکته مهم:  همانطور که اشاره شد، در سناریوهای Master-Detail باید تمامی Detailها به سمت سرور ارسال شوند. در این صورت سناریویی را در نظر بگیرید که قرار است کاربر در front-office سیستم امکان حذف یک قلم از اقلام فاکتور را داشته باشد؛ این درحالی است که در back-office و در منطق تجاری اصلی، ما جایی برای حذف یک تک قلم ندیده‌ایم و کلا منطق و قواعد تجاری حاکم بر فاکتور را زیر سوال می‌برد. چرا که ممکن است یکسری قواعد تجاری متناسب با دومین، بر روی لیست اقلام یک فاکتور در زمان ذخیره سازی وجود داشته باشند که با حذف یک تک قلم از یک مسیر فرعی، کل فاکتور را در حالت نامعتبری برای ذخیره سازی‌های بعدی قرار دهد. در این موارد باید API شما یک DTO سفارشی را دریافت کند که شامل شناسه قلم فاکتور و شناسه فاکتور می‌باشد. سپس با استفاده از شناسه فاکتور و سرویس متناظر، آن را واکشی کرده و از لیست قلم‌های InvoiceModel، آن قلم را با TrackingState.Deleted علامت‌گذاری کنید. همچنین باید توجه داشته باشید که برروی فیلدهای موجود در جداول مرتبط با موجودیت‌های Detail، محدودیت‌های دیتابیسی از جمله Unique Constraint و ... را اعمال نکنید؛ مگر اینکه میدانید و دقیقا مطمئن باشید عملیات حذف اقلام، قبل از عملیات ثبت اقلام جدید رخ می‎دهد (این موضوع نیاز به توضیح و شبیه سازی شرایط خاص آن را دارد که در صورت نیاز می‌توان در مطلب جدایی به آن پرداخت).
‌پیاده سازی و بررسی تعدادی سرویس فرضی
برای شروع لازم است بسته‌های نیوگت زیر را نصب کنید:
PM> Install-Package DNTFrameworkCore -Version 1.0.0
PM> Install-Package DNTFrameworkCore.EntityFramework -Version 1.0.0

مثال اول: پیاده‌سازی سرویس یک موجودیت ساده بدون نیاز به ReadModel 
گام اول: طراحی Model متناظر
[LocalizationResource(Name = "SharedResource", Location = "DNTFrameworkCore.TestAPI")]
public class BlogModel : MasterModel<int>, IValidatableObject
{
    public string Title { get; set; }

    [MaxLength(50, ErrorMessage = "Maximum length is 50")]
    public string Url { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (Title == "BlogTitle")
        {
            yield return new ValidationResult("IValidatableObject Message", new[] {nameof(Title)});
        }
    }
}
مدل متناظر با موجودیت‌های اصلی باید از کلاس جنریک MasterModel ارث‌بری کرده باشد. همانطور که ملاحظه می‌کنید، برای نشان دادن مکانیزم اعتبارسنجی، از DataAnnotationها و IValidatableObject استفاده شده‌است. LocalizationResource برای مشخص کردن نام و محل فایل Resource متناظر برای خواندن پیغام‌های اعتبارسنجی استفاده می‌شود. این مورد برای سناریوهای ماژولار و کامپوننت محور بیشتر می‌تواند مدنظر باشد. 
گام دوم: پیاده‌سازی اعتبارسنج مستقل
در صورت نیاز به اعتبارسنجی پیچیده برای مدل متناظر، می‌توانید با استفاده از دو روش زیر به این هدف برسید:
1- استفاده از کتابخانه DNTFrameworkCore.FluentValidation
public class BlogValidator : FluentModelValidator<BlogModel>
{
    public BlogValidator(IMessageLocalizer localizer)
    {
        RuleFor(b => b.Title).NotEmpty()
            .WithMessage(localizer["Blog.Fields.Title.Required"]);
    }
}
2- پیاده‌سازی IModelValidator یا ارث‌بری از کلاس ModelValidator پایه
public class BlogValidator : ModelValidator<BlogkModel>
{
    public override IEnumerable<ModelValidationResult> Validate(BlogModel model)
    {
        yield return new ModelValidationResult(nameof(BlogkModel.Title), "Validation from IModelValidator");
    }
}

گام سوم: پیاده‌سازی سرویس متناظر
public interface IBlogService : ICrudService<int, BlogModel>
{
}
پیاده سازی واسط بالا
public class BlogService : CrudService<Blog, int, BlogModel>, IBlogService
{
    public BlogService(CrudServiceDependency dependency) : base(dependency)
    {
    }

    protected override IQueryable<BlogModel> BuildReadQuery(FilteredPagedQueryModel model)
    {
        return EntitySet.AsNoTracking().Select(b => new BlogModel
            {Id = b.Id, RowVersion = b.RowVersion, Url = b.Url, Title = b.Title});
    }

    protected override Blog MapToEntity(BlogModel model)
    {
        return new Blog
        {
            Id = model.Id,
            RowVersion = model.RowVersion,
            Url = model.Url,
            Title = model.Title,
            NormalizedTitle = model.Title.ToUpperInvariant() //todo: normalize based on your requirement 
        };
    }

    protected override BlogModel MapToModel(Blog entity)
    {
        return new BlogModel
        {
            Id = entity.Id,
            RowVersion = entity.RowVersion,
            Url = entity.Url,
            Title = entity.Title
        };
    }
}
برای این چنین موجودیت‌هایی، بازنویسی همین 3 متد کفایت می‌کند؛ دو متد MapToModel و MapToEntity برای نگاشت مابین مدل و موجودیت مورد نظر و متد BuildReadQuery نیز برای تعیین نحوه ساخت کوئری ReadPagedListAsync پیش‌فرض موجود در CrudService به عنوان متد Read پیش‌فرض این موجودیت. باکمترین مقدار کدنویسی و با کیفیت قابل قبول، عملیات CRUD یک موجودیت ساده، تکمیل شد. 
مثال دوم: پیاده سازی سرویس یک موجودیت ساده با ReadModel و  FilteredPagedQueryModel متمایز
گام اول: طراحی Model متناظر
[LocalizationResource(Name = "SharedResource", Location = "DNTFrameworkCore.TestAPI")]
public class TaskModel : MasterModel<int>, IValidatableObject
{
    public string Title { get; set; }

    [MaxLength(50, ErrorMessage = "Validation from DataAnnotations")]
    public string Number { get; set; }

    public string Description { get; set; }
    public TaskState State { get; set; } = TaskState.Todo;

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (Title == "IValidatableObject")
        {
            yield return new ValidationResult("Validation from IValidatableObject");
        }
    }
}
public class TaskReadModel : MasterModel<int>
{
    public string Title { get; set; }
    public string Number { get; set; }
    public TaskState State { get; set; } = TaskState.Todo;
    public DateTimeOffset CreationDateTime { get; set; }
    public string CreatorUserDisplayName { get; set; }
}
به عنوان مثال خصوصیاتی برای نمایش داریم که در زمان ثبت و ویرایش، انتظار دریافت آنها را از کاربر نیز نداریم. 
گام دوم: پیاده‌سازی اعتبارسنج  مستقل 
public class TaskValidator : ModelValidator<TaskModel>
{
    public override IEnumerable<ModelValidationResult> Validate(TaskModel model)
    {
        if (!Enum.IsDefined(typeof(TaskState), model.State))
        {
            yield return new ModelValidationResult(nameof(TaskModel.State), "Validation from IModelValidator");
        }
    }
}
 گام سوم: پیاده‌سازی سرویس متناظر
public interface ITaskService : ICrudService<int, TaskReadModel, TaskModel, TaskFilteredPagedQueryModel>
{
}
همانطور که ملاحظه می‌کنید، از ICrudService استفاده شده است که امکان تعیین نوع پارامتر جنریک TReadModel و TFilteredPagedQueryModel را هم دارد.
مدل جستجو و صفحه‌بندی سفارشی 
public class TaskFilteredPagedQueryModel : FilteredPagedQueryModel
{
    public TaskState? State { get; set; }
}


پیاده سازی واسط ITaskService با استفاده از AutoMapper

public class TaskService : CrudService<Task, int, TaskReadModel, TaskModel, TaskFilteredPagedQueryModel>,
  ITaskService
{
    private readonly IMapper _mapper;

    public TaskService(CrudServiceDependency dependency, IMapper mapper) : base(dependency)
    {
        _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
    }

    protected override IQueryable<TaskReadModel> BuildReadQuery(TaskFilteredPagedQueryModel model)
    {
        return EntitySet.AsNoTracking()
                    .WhereIf(model.State.HasValue, t => t.State == model.State)
                    .ProjectTo<TaskReadModel>(_mapper.ConfigurationProvider);
    }

    protected override Task MapToEntity(TaskModel model)
    {
        return _mapper.Map<Task>(model);
    }

    protected override TaskModel MapToModel(Task entity)
    {
        return _mapper.Map<TaskModel>(entity);
    }
}

به عنوان مثال در کلاس بالا برای نگاشت مابین مدل و موجودیت، از واسط IMapper کتابخانه AutoMapper استفاده شده‌است و همچنین عملیات جستجوی سفارشی در همان متد BuildReadQuery برای تولید کوئری متد Read پیش‌فرض، قابل ملاحظه می‌باشد.

مثال سوم: پیاده‌سازی سرویس یک موجودیت اصلی به همراه تعدادی موجودیت فرعی وابسته (سناریوهای Master-Detail) 

گام اول: طراحی Modelهای متناظر

    public class UserModel : MasterModel
    {
        public string UserName { get; set; }
        public string DisplayName { get; set; }
        public string Password { get; set; }
        public bool IsActive { get; set; }
        public ICollection<UserRoleModel> Roles { get; set; } = new HashSet<UserRoleModel>();
        public ICollection<PermissionModel> Permissions { get; set; } = new HashSet<PermissionModel>();
        public ICollection<PermissionModel> IgnoredPermissions { get; set; } = new HashSet<PermissionModel>();
    }

مدل بالا متناظر است با موجودیت کاربر سیستم، که به یکسری گروه کاربری متصل می‌باشد و همچنین دارای یکسری دسترسی مستقیم بوده و یا یکسری دسترسی از او گرفته شده‌است. مدل‌های Detail نیز از قرارداد خاصی پیروی خواهند کرد که در ادامه مشاهده خواهیم کرد.

public class PermissionModel : DetailModel<int>
{
    public string Name { get; set; }
}

به عنوان مثال PermissionModel بالا از DetailModel جنریک‌ای ارث‌بری کرده است که دارای Id و TrackingState نیز می‌باشد. 

public class UserRoleModel : DetailModel<int>
{
    public long RoleId { get; set; }
}

شاید در نگاه اول برای گروه‌های کاربری یک کاربر کافی بود تا یک لیست ساده از long را از کلاینت دریافت کنیم. در این صورت نیاز است تا برای تمام موجودیت‎های سیستم که چنین شرایط مشابهی را دارند، عملیات ثبت، ویرایش و حذف متناظر با تک تک Detailها را دستی مدیریت کنید. روش فعلی خصوصا برای سناریوهای منفصل به مانند پروژه‌های تحت وب، پیشنهاد می‌شود.

گام دوم: پیاده سازی اعتبارسنج مستقل

public class UserValidator : FluentModelValidator<UserModel>
{
    private readonly IUnitOfWork _uow;

    public UserValidator(IUnitOfWork uow, IMessageLocalizer localizer)
    {
        _uow = uow ?? throw new ArgumentNullException(nameof(uow));

        RuleFor(m => m.DisplayName).NotEmpty()
            .WithMessage(localizer["User.Fields.DisplayName.Required"])
            .MinimumLength(3)
            .WithMessage(localizer["User.Fields.DisplayName.MinimumLength"])
            .MaximumLength(User.MaxDisplayNameLength)
            .WithMessage(localizer["User.Fields.DisplayName.MaximumLength"])
            .Matches(@"^[\u0600-\u06FF,\u0590-\u05FF,0-9\s]*$")
            .WithMessage(localizer["User.Fields.DisplayName.RegularExpression"])
            .DependentRules(() =>
            {
                RuleFor(m => m).Must(model =>
                     !CheckDuplicateDisplayName(model.DisplayName, model.Id))
                    .WithMessage(localizer["User.Fields.DisplayName.Unique"])
                    .OverridePropertyName(nameof(UserModel.DisplayName));
            });

        RuleFor(m => m.UserName).NotEmpty()
            .WithMessage(localizer["User.Fields.UserName.Required"])
            .MinimumLength(3)
            .WithMessage(localizer["User.Fields.UserName.MinimumLength"])
            .MaximumLength(User.MaxUserNameLength)
            .WithMessage(localizer["User.Fields.UserName.MaximumLength"])
            .Matches("^[a-zA-Z0-9_]*$")
            .WithMessage(localizer["User.Fields.UserName.RegularExpression"])
            .DependentRules(() =>
            {
                RuleFor(m => m).Must(model =>
                     !CheckDuplicateUserName(model.UserName, model.Id))
                    .WithMessage(localizer["User.Fields.UserName.Unique"])
                    .OverridePropertyName(nameof(UserModel.UserName));
            });

        RuleFor(m => m.Password).NotEmpty()
            .WithMessage(localizer["User.Fields.Password.Required"])
            .When(m => m.IsNew, ApplyConditionTo.CurrentValidator)
            .MinimumLength(6)
            .WithMessage(localizer["User.Fields.Password.MinimumLength"])
            .MaximumLength(User.MaxPasswordLength)
            .WithMessage(localizer["User.Fields.Password.MaximumLength"]);

        RuleFor(m => m).Must(model => !CheckDuplicateRoles(model))
            .WithMessage(localizer["User.Fields.Roles.Unique"])
            .When(m => m.Roles != null && m.Roles.Any(r => !r.IsDeleted));
    }

    private bool CheckDuplicateUserName(string userName, long id)
    {
        var normalizedUserName = userName.ToUpperInvariant();
        return _uow.Set<User>().Any(u => u.NormalizedUserName == normalizedUserName && u.Id != id);
    }

    private bool CheckDuplicateDisplayName(string displayName, long id)
    {
        var normalizedDisplayName = displayName.NormalizePersianTitle();
        return _uow.Set<User>().Any(u => u.NormalizedDisplayName == normalizedDisplayName && u.Id != id);
    }

    private bool CheckDuplicateRoles(UserModel model)
    {
        var roles = model.Roles.Where(a => !a.IsDeleted);
        return roles.GroupBy(r => r.RoleId).Any(r => r.Count() > 1);
    }
}

به عنوان مثال در این اعتبارسنج بالا، قواعدی از جمله بررسی تکراری بودن نام‌کاربری و از این دست اعتبارسنجی‌ها نیز انجام شده است. نکته حائز اهمیت آن متد CheckDuplicateRoles می‌باشد:

private bool CheckDuplicateRoles(UserModel model)
{
    var roles = model.Roles.Where(a => !a.IsDeleted);
    return roles.GroupBy(r => r.RoleId).Any(r => r.Count() > 1);
}

با توجه به «نکته مهم» ابتدای بحث، model.Roles، شامل تمام گروه‌های کاربری متصل شده به کاربر می‌باشند که در این لیست برخی از آنها با TrackingState.Deleted، برخی دیگر با TrackingState.Added و ... علامت‌گذاری شده‌اند. لذا برای بررسی یکتایی و عدم تکرار در این سناریوها نیاز به اجری پرس‌و‌جویی بر روی دیتابیس نمی‌باشد. بدین منظور، با اعمال یک شرط، گروه‌های حذف شده را از بررسی خارج کرده‌ایم؛ چرا که آنها بعد از عبور از منطق تجاری، حذف خواهند شد. 


گام سوم: پیاده‌سازی سرویس متناظر

public interface IUserService : ICrudService<long, UserReadModel, UserModel>
{
}
public class UserService : CrudService<User, long, UserReadModel, UserModel>, IUserService
{
    private readonly IUserManager _manager;

    public UserService(CrudServiceDependency dependency, IUserManager manager) : base(dependency)
    {
        _manager = manager ?? throw new ArgumentNullException(nameof(manager));
    }

    protected override IQueryable<User> BuildFindQuery()
    {
        return base.BuildFindQuery()
            .Include(u => u.Roles)
            .Include(u => u.Permissions);
    }

    protected override IQueryable<UserReadModel> BuildReadQuery(FilteredPagedQueryModel model)
    {
        return EntitySet.AsNoTracking().Select(u => new UserReadModel
        {
            Id = u.Id,
            RowVersion = u.RowVersion,
            IsActive = u.IsActive,
            UserName = u.UserName,
            DisplayName = u.DisplayName,
            LastLoggedInDateTime = u.LastLoggedInDateTime
        });
    }

    protected override User MapToEntity(UserModel model)
    {
        return new User
        {
            Id = model.Id,
            RowVersion = model.RowVersion,
            IsActive = model.IsActive,
            DisplayName = model.DisplayName,
            UserName = model.UserName,
            NormalizedUserName = model.UserName.ToUpperInvariant(),
            NormalizedDisplayName = model.DisplayName.NormalizePersianTitle(),
            Roles = model.Roles.Select(r => new UserRole
                {Id = r.Id, RoleId = r.RoleId, TrackingState = r.TrackingState}).ToList(),
            Permissions = model.Permissions.Select(p => new UserPermission
            {
                Id = p.Id,
                TrackingState = p.TrackingState,
                IsGranted = true,
                Name = p.Name
            }).Union(model.IgnoredPermissions.Select(p => new UserPermission
            {
                Id = p.Id,
                TrackingState = p.TrackingState,
                IsGranted = false,
                Name = p.Name
            })).ToList()
        };
    }

    protected override UserModel MapToModel(User entity)
    {
        return new UserModel
        {
            Id = entity.Id,
            RowVersion = entity.RowVersion,
            IsActive = entity.IsActive,
            DisplayName = entity.DisplayName,
            UserName = entity.UserName,
            Roles = entity.Roles.Select(r => new UserRoleModel
                {Id = r.Id, RoleId = r.RoleId, TrackingState = r.TrackingState}).ToList(),
            Permissions = entity.Permissions.Where(p => p.IsGranted).Select(p => new PermissionModel
            {
                Id = p.Id,
                TrackingState = p.TrackingState,
                Name = p.Name
            }).ToList(),
            IgnoredPermissions = entity.Permissions.Where(p => !p.IsGranted).Select(p => new PermissionModel
            {
                Id = p.Id,
                TrackingState = p.TrackingState,
                Name = p.Name
            }).ToList()
        };
    }

    protected override Task BeforeSaveAsync(IReadOnlyList<User> entities, List<UserModel> models)
    {
        ApplyPasswordHash(entities, models);
        ApplySerialNumber(entities, models);
        return base.BeforeSaveAsync(entities, models);
    }

    private void ApplySerialNumber(IEnumerable<User> entities, IReadOnlyList<UserModel> models)
    {
        var i = 0;
        foreach (var entity in entities)
        {
            var model = models[i++];

            if (model.IsNew || !model.IsActive || !model.Password.IsEmpty() ||
                model.Roles.Any(a => a.IsNew || a.IsDeleted) ||
                model.IgnoredPermissions.Any(p => p.IsDeleted || p.IsNew) ||
                model.Permissions.Any(p => p.IsDeleted || p.IsNew))
            {
                entity.SerialNumber = _manager.NewSerialNumber();
            }
            else
            {
                //prevent include SerialNumber in update query
                UnitOfWork.Entry(entity).Property(a => a.SerialNumber).IsModified = false;
            }
        }
    }

    private void ApplyPasswordHash(IEnumerable<User> entities, IReadOnlyList<UserModel> models)
    {
        var i = 0;
        foreach (var entity in entities)
        {
            var model = models[i++];
            if (model.IsNew || !model.Password.IsEmpty())
            {
                entity.PasswordHash = _manager.HashPassword(model.Password);
            }
            else
            {
                //prevent include PasswordHash in update query
                UnitOfWork.Entry(entity).Property(a => a.PasswordHash).IsModified = false;
            }
        }
    }
}

در سناریوهای Master-Detail نیاز است متد دیگری تحت عنوان BuildFindQuery را نیز بازنویسی کنید. این متد برای بقیه حالات نیاز به بازنویسی نداشت؛ چرا که یک تک موجودیت واکشی می‌شد و خبری از موجودیت‌های Detail نبود. در اینجا لازم است تا روش تولید کوئری FindAsyn رو بازنویسی کنیم تا جزئیات دیگری را نیز واکشی کنیم. به عنوان مثال در اینجا Roles و Permissions کاربر نیز Include شده‌اند.

نکته: بازنویسی BuildFindQuery را شاید بتوان با روش‌های دیگری هم مانند تزئین موجودیت‌های وابسته با یک DetailOfAttribute و مشخص کردن نوع موجودیت اصلی، نیز جایگزین کرد.

متدهای MapToModel و MapToEntity هم به مانند قبل پیاده‌سازی شده‌اند. موضوع دیگری که در برخی از سناریوها پیش خواهد آمد، مربوط است به خصوصیتی که در زمان ثبت ضروری می‌باشد، ولی در زمان ویرایش اگر مقدار داشت باید با اطلاعات موجود در دیتابیس جایگزین شود؛ مانند Password و SerialNumber در موجودیت کاربر. برای این حالت می‌توان از متد BeforeSaveAsync بهره برد؛ به عنوان مثال برای SerialNumber:

private void ApplySerialNumber(IEnumerable<User> entities, IReadOnlyList<UserModel> models)
{
    var i = 0;
    foreach (var entity in entities)
    {
        var model = models[i++];

        if (model.IsNew || !model.IsActive || !model.Password.IsEmpty() ||
            model.Roles.Any(a => a.IsNew || a.IsDeleted) ||
            model.IgnoredPermissions.Any(p => p.IsDeleted || p.IsNew) ||
            model.Permissions.Any(p => p.IsDeleted || p.IsNew))
        {
            entity.SerialNumber = _manager.NewSerialNumber();
        }
        else
        {
            //prevent include SerialNumber in update query
            UnitOfWork.Entry(entity).Property(a => a.SerialNumber).IsModified = false;
        }
    }
}

در اینجا ابتدا بررسی شده‌است که اگر کاربر، جدید می‌باشد، غیرفعال شده است، کلمه عبور او تغییر داده شده است و یا تغییراتی در دسترسی‌ها و گروه‌های کاربری او وجود دارد، یک SerialNumber جدید ایجاد کند. در غیر این صورت با توجه به اینکه برای عملیات ویرایش، به صورت منفصل عمل می‌کنیم، نیاز است تا به شکل بالا، از قید این فیلد در کوئری ویرایش، جلوگیری کنیم. 

نکته: متد BeforeSaveAsync دقیقا بعد از ردیابی شدن وهله‌های موجودیت توسط Context برنامه و دقیقا قبل از UnitOfWork.SaveChange فراخوانی خواهد شد.


برای بررسی بیشتر، پیشنهاد می‌کنم پروژه DNTFrameworkCore.TestAPI موجود در مخزن این زیرساخت را بازبینی کنید.
مطالب
ASP.NET MVC #10

آشنایی با روش‌های مختلف ارسال اطلاعات یک درخواست به کنترلر

تا اینجا با روش‌های مختلف ارسال اطلاعات از یک کنترلر به View متناظر آن آشنا شدیم. اما حالت عکس آن چطور؟ مثلا در ASP.NET Web forms، دوبار بر روی یک دکمه کلیک می‌کردیم و در روال رویدادگردان کلیک آن، همانند برنامه‌های ویندوزی، دسترسی به اطلاعات اشیاء قرار گرفته بر روی فرم را داشتیم. در ASP.NET MVC که کلا مفهوم Events را حذف کرده و وب را همانگونه که هست ارائه می‌دهد و به علاوه کنترلرهای آن، ارجاع مستقیمی را به هیچکدام از اشیاء بصری در خود ندارند (برای مثال کنترلر و متدی در آن نمی‌دانند که الان بر روی View آن، یک گرید قرار دارد یا یک دکمه یا اصلا هیچی)، چگونه می‌توان اطلاعاتی را از کاربر دریافت کرد؟
در اینجا حداقل سه روش برای دریافت اطلاعات از کاربر وجود دارد:
الف) استفاده از اشیاء Context مانند HttpContext، Request، RouteData و غیره
ب) به کمک پارامترهای اکشن متدها
ج) با استفاده از ویژگی جدیدی به نام Data Model Binding

یک مثال کاربردی
قصد داریم یک صفحه لاگین ساده را طراحی کنیم تا بتوانیم هر سه حالت ذکر شده فوق را در عمل بررسی نمائیم. بحث HTML Helpers استاندارد ASP.NET MVC را هم که در قسمت قبل شروع کردیم، لابلای توضیحات قسمت جاری و قسمت‌های بعدی با مثال‌های کاربردی دنبال خواهند شد.
بنابراین یک پروژه جدید خالی ASP.NET MVC را شروع کرده و مدلی را به نام Account با محتوای زیر به پوشه Models برنامه اضافه کنید:

namespace MvcApplication6.Models
{
public class Account
{
public string Name { get; set; }
public string Password { get; set; }
}
}

یک کنترلر جدید را هم به نام LoginController به پوشه کنترلرهای برنامه اضافه کنید. بر روی متد Index پیش فرض آن کلیک راست نمائید و یک View خالی را اضافه نمائید.
در ادامه به فایل Global.asax.cs مراجعه کرده و نام کنترلر پیش‌فرض را به Login تغییر دهید تا به محض شروع برنامه در VS.NET، صفحه لاگین ظاهر شود.
کدهای کامل کنترلر لاگین را در ادامه ملاحظه می‌کنید:

using System.Web.Mvc;
using MvcApplication6.Models;

namespace MvcApplication6.Controllers
{
public class LoginController : Controller
{
[HttpGet]
public ActionResult Index()
{
return View(); //Shows the login page
}

[HttpPost]
public ActionResult LoginResult()
{
string name = Request.Form["name"];
string password = Request.Form["password"];

if (name == "Vahid" && password == "123")
ViewBag.Message = "Succeeded";
else
ViewBag.Message = "Failed";

return View("Result");
}

[HttpPost]
[ActionName("LoginResultWithParams")]
public ActionResult LoginResult(string name, string password)
{
if (name == "Vahid" && password == "123")
ViewBag.Message = "Succeeded";
else
ViewBag.Message = "Failed";

return View("Result");
}

[HttpPost]
public ActionResult Login(Account account)
{
if (account.Name == "Vahid" && account.Password == "123")
ViewBag.Message = "Succeeded";
else
ViewBag.Message = "Failed";

return View("Result");
}
}
}

همچنین Viewهای متناظر با این کنترلر هم به شرح زیر هستند:
فایل index.cshtml به نحو زیر تعریف خواهد شد:

@model MvcApplication6.Models.Account
@{
ViewBag.Title = "Index";
}
<h2>
Login</h2>
@using (Html.BeginForm(actionName: "LoginResult", controllerName: "Login"))
{
<fieldset>
<legend>Test LoginResult()</legend>
<p>
Name: @Html.TextBoxFor(m => m.Name)</p>
<p>
Password: @Html.PasswordFor(m => m.Password)</p>
<input type="submit" value="Login" />
</fieldset>
}
@using (Html.BeginForm(actionName: "LoginResultWithParams", controllerName: "Login"))
{
<fieldset>
<legend>Test LoginResult(string name, string password)</legend>
<p>
Name: @Html.TextBoxFor(m => m.Name)</p>
<p>
Password: @Html.PasswordFor(m => m.Password)</p>
<input type="submit" value="Login" />
</fieldset>
}
@using (Html.BeginForm(actionName: "Login", controllerName: "Login"))
{
<fieldset>
<legend>Test Login(Account acc)</legend>
<p>
Name: @Html.TextBoxFor(m => m.Name)</p>
<p>
Password: @Html.PasswordFor(m => m.Password)</p>
<input type="submit" value="Login" />
</fieldset>
}

و فایل result.cshtml هم محتوای زیر را دارد:

@{
ViewBag.Title = "Result";
}
<fieldset>
<legend>Login Result</legend>
<p>
@ViewBag.Message</p>
</fieldset>

توضیحاتی در مورد View لاگین برنامه:
در View صفحه لاگین سه فرم را مشاهده می‌کنید. در برنامه‌های ASP.NET Web forms در هر صفحه، تنها یک فرم را می‌توان تعریف کرد؛ اما در ASP.NET MVC این محدودیت برداشته شده است.
تعریف یک فرم هم با متد کمکی Html.BeginForm انجام می‌شود. در اینجا برای مثال می‌شود یک فرم را به کنترلری خاص و متدی مشخص در آن نگاشت نمائیم.
از عبارت using هم برای درج خودکار تگ بسته شدن فرم، در حین dispose شیء MvcForm کمک گرفته شده است.
برای نمونه خروجی HTML اولین فرم تعریف شده به صورت زیر است:

<form action="/Login/LoginResult" method="post">   
<fieldset>
<legend>Test LoginResult()</legend>
<p>
Name: <input id="Name" name="Name" type="text" value="" /></p>
<p>
Password: <input id="Password" name="Password" type="password" /></p>
<input type="submit" value="Login" />
</fieldset>
</form>

توسط متدهای کمکی Html.TextBoxFor و Html.PasswordFor یک TextBox و یک PasswordBox به صفحه اضافه می‌شوند، اما این For آن‌ها و همچنین lambda expression ایی که بکارگرفته شده برای چیست؟
متدهای کمکی Html.TextBox و Html.Password از نگارش‌های اولیه ASP.NET MVC وجود داشتند. این متدها نام خاصیت‌ها و پارامترهایی را که قرار است به آن‌ها بایند شوند، به صورت رشته می‌پذیرند. اما با توجه به اینکه در اینجا می‌توان یک strongly typed view را تعریف کرد،‌ تیم ASP.NET MVC بهتر دیده است که این رشته‌ها را حذف کرده و از قابلیتی به نام Static reflection استفاده کند (^ و ^).

با این توضیحات، اطلاعات سه فرم تعریف شده در View لاگین برنامه، به سه متد متفاوت قرار گرفته در کنترلری به نام Login ارسال خواهند شد. همچنین با توجه به مشخص بودن نوع model که در ابتدای فایل تعریف شده، خاصیت‌هایی را که قرار است اطلاعات ارسالی به آن‌ها بایند شوند نیز به نحو strongly typed تعریف شده‌اند و تحت نظر کامپایلر خواهند بود.


توضیحاتی در مورد نحوه عملکرد کنترلر لاگین برنامه:

در این کنترلر صرفنظر از محتوای متدهای آن‌ها، دو نکته جدید را می‌توان مشاهده کرد. استفاده از ویژگی‌های HttpPost، HttpGet و ActionName. در اینجا به کمک ویژگی‌های HttpGet و HttpPost در مورد نحوه دسترسی به این متدها، محدودیت قائل شده‌ایم. به این معنا که تنها در حالت Post است که متد LoginResult در دسترس خواهد بود و اگر شخصی نام این متدها را مستقیما در مرورگر وارد کند (یا همان HttpGet پیش فرض که نیازی هم به ذکر صریح آن نیست)، با پیغام «یافت نشد» مواجه می‌گردد.
البته در نگارش‌های اولیه ASP.NET MVC از ویژگی دیگری به نام AcceptVerbs برای مشخص سازی نوع محدودیت فراخوانی یک اکشن متد استفاده می‌شد که هنوز هم معتبر است. برای مثال:

[AcceptVerbs(HttpVerbs.Get)]

یک نکته امنیتی:
همیشه متدهای Delete خود را به HttpPost محدود کنید. به این علت که ممکن است در طی مثلا یک ایمیل، آدرسی به شکل http://localhost/blog/delete/10 برای شما ارسال شود و همچنین سشن کار با قسمت مدیریتی بلاگ شما نیز در همان حال فعال باشد. URL ایی به این شکل، در حالت پیش فرض، محدودیت اجرایی HttpGet را دارد. بنابراین احتمال اجرا شدن آن بالا است. اما زمانیکه متد delete را به HttpPost محدود کردید، دیگر این نوع حملات جواب نخواهند داد و حتما نیاز خواهد بود تا اطلاعاتی به سرور Post شود و نه یک Get ساده (مثلا کلیک بر روی یک لینک معمولی)، کار حذف را انجام دهد.


توسط ActionName می‌توان نام دیگری را صرفنظر از نام متد تعریف شده در کنترلر، به آن متد انتساب داد که توسط فریم ورک در حین پردازش نهایی مورد استفاده قرار خواهد گرفت. برای مثال در اینجا به متد LoginResult دوم، نام LoginResultWithParams را انتساب داده‌ایم که در فرم دوم تعریف شده در View لاگین برنامه مورد استفاده قرار گرفته است.
وجود این ActionName هم در مثال فوق ضروری است. از آنجائیکه دو متد هم نام را معرفی کرده‌ایم و فریم ورک نمی‌داند که کدامیک را باید پردازش کند. در این حالت (بدون وجود ActionName معرفی شده)، برنامه با خطای زیر مواجه می‌گردد:

The current request for action 'LoginResult' on controller type 'LoginController' is ambiguous between the following action methods:
System.Web.Mvc.ActionResult LoginResult() on type MvcApplication6.Controllers.LoginController
System.Web.Mvc.ActionResult LoginResult(System.String, System.String) on type MvcApplication6.Controllers.LoginController

برای اینکه بتوانید نحوه نگاشت فرم‌ها به متدها را بهتر درک کنید، بر روی چهار return View موجود در کنترلر لاگین برنامه، چهار breakpoint را تعریف کنید. سپس برنامه را در حالت دیباگ اجرا نمائید و تک تک فرم‌ها را یکبار با کلیک بر روی دکمه لاگین، به سرور ارسال نمائید.


بررسی سه روش دریافت اطلاعات از کاربر در ASP.NET MVC

الف) استفاده از اشیاء Context

در ویژوال استودیو، در کنترلر لاگین برنامه، بر روی کلمه Controller کلیک راست کرده و گزینه Go to definition را انتخاب کنید. در اینجا بهتر می‌توان به خواصی که در یک کنترلر به آن‌ها دسترسی داریم، نگاهی انداخت:

public HttpContextBase HttpContext { get; }
public HttpRequestBase Request { get; }
public HttpResponseBase Response { get; }
public RouteData RouteData { get; }

در بین این خواص و اشیاء مهیا، Request و RouteData بیشتر مد نظر ما هستند. در مورد RouteData در قسمت ششم این سری، توضیحاتی ارائه شد. اگر مجددا Go to definition مربوط به HttpRequestBase خاصیت Request را بررسی کنیم، موارد ذیل جالب توجه خواهند بود:

public virtual NameValueCollection QueryString { get; } // GET variables
public NameValueCollection Form { get; } // POST variables
public HttpCookieCollection Cookies { get; }
public NameValueCollection Headers { get; }
public string HttpMethod { get; }

توسط خاصیت Form شیء Request می‌توان به مقادیر ارسالی به سرور در یک کنترلر دسترسی یافت که نمونه‌ای از آن‌را در اولین متد LoginResult می‌توانید مشاهده کنید. این روش در ASP.NET Web forms هم کار می‌کند. جهت اطلاع این روش با ASP کلاسیک دهه نود هم سازگار است!
البته این روش آنچنان مرسوم نیست؛ چون NameValueCollection مورد استفاده، ایندکسی عددی یا رشته‌ای را می‌پذیرد که هر دو با پیشرفت‌هایی که در زبان‌های دات نتی صورت گرفته‌اند، دیگر آنچنان مطلوب و روش مرجح به حساب نمی‌آیند. اما ... هنوز هم قابل استفاده است.
به علاوه اگر دقت کرده باشید در اینجا HttpContextBase داریم بجای HttpContext. تمام این کلاس‌های پایه هم به جهت سهولت انجام آزمون‌های واحد در ASP.NET MVC ایجاد شده‌اند. کار کردن مستقیم با HttpContext مشکل بوده و نیاز به شبیه سازی فرآیندهای رخ داده در یک وب سرور را دارد. اما این کلاس‌های پایه جدید، مشکلات یاد شده را به همراه ندارند.


ب) استفاده از پارامترهای اکشن متدها

نکته‌ای در مورد نامگذاری پارامترهای یک اکشن متد به صورت توکار اعمال می‌شود که باید به آن دقت داشت:
اگر نام یک پارامتر، با نام کلید یکی از رکوردهای موجود در مجموعه‌های زیر یکی باشد، آنگاه به صورت خودکار اطلاعات دریافتی به این پارامتر نگاشت خواهد شد (پارامتر هم نام، به صورت خودکار مقدار دهی می‌شود). این مجموعه‌ها شامل موارد زیرهستند:

Request.Form
Request.QueryString
Request.Files
RouteData.Values

برای نمونه در متدی که با نام LoginResultWithParams مشخص شده، چون نام‌های دو پارامتر آن، با نام‌های بکارگرفته شده در Html.TextBoxFor و Html.PasswordFor یکی هستند، با مقادیر ارسالی آن‌ها مقدار دهی شده و سپس در متد قابل استفاده خواهند بود. در پشت صحنه هم از همان رکوردهای موجود در Request.Form (یا سایر موارد ذکر شده)، استفاده می‌شود. در اینجا هر رکورد مثلا مجموعه Request.Form، کلیدی مساوی نام ارسالی به سرور را داشته و مقدار آن هم، مقداری است که کاربر وارد کرده است.
اگر همانندی یافت نشد، آن پارامتر با نال مقدار دهی می‌گردد. بنابراین اگر برای مثال یک پارامتر از نوع int را معرفی کرده باشید و چون نوع int، نال نمی‌پذیرد، یک استثناء بروز خواهد کرد. برای حل این مشکل هم می‌توان از Nullable types استفاده نمود (مثلا بجای int id نوشت int? id تا مشکلی جهت انتساب مقدار نال وجود نداشته باشد).
همچنین باید دقت داشت که این بررسی تطابق‌های بین نام عناصر HTML و نام پارامترهای متدها، case insensitive است و به کوچکی و بزرگی حروف حساس نیست. برای مثال، پارامتر معرفی شده در متد LoginResult مساوی string name است، اما نام خاصیت تعریف شده در کلاس Account مساوی Name بود.


ج) استفاده از ویژگی جدیدی به نام Data Model Binding

در ASP.NET MVC چون می‌توان با یک Strongly typed view کار کرد، خود فریم ورک این قابلیت را دارد که اطلاعات ارسالی یکی فرم را به صورت خودکار به یک وهله از یک شیء نگاشت کند. در اینجا model binder وارد عمل می‌شود، مقادیر ارسالی را استخراج کرده (اطلاعات دریافتی از Form یا کوئری استرینگ‌ها یا اطلاعات مسیریابی و غیره) و به خاصیت‌های یک شیء نگاشت می‌کند. بدیهی است در اینجا این خواص باید عمومی باشند و هم نام عناصر HTML ارسالی به سرور. همچنین model binder پیش فرض ASP.NET MVC را نیز می‌توان کاملا تعویض کرد و محدود به استفاده از model binder توکار آن نیستیم.
وجود این Model binder، کار با ORMها را بسیار لذت بخش می‌کند؛ از آنجائیکه خود فریم ورک ASP.NET MVC می‌تواند عناصر شیءایی را که قرار است به بانک اطلاعاتی اضافه شود، یا در آن به روز شود، به صورت خودکار ایجاد کرده یا به روز رسانی نماید.
نحوه کار با model binder را در متد Login کنترلر فوق می‌توانید مشاهده کنید. بر روی return View آن یک breakpoint قرار دهید. فرم سوم را به سرور ارسال کنید و سپس در VS.NET خواص شیء ساخته شده را در حین دیباگ برنامه، بررسی نمائید.
بنابراین تفاوتی نمی‌کند که از چندین پارامتر استفاده کنید یا اینکه کلا یک شیء را به عنوان پارامتر معرفی نمائید. فریم ورک سعی می‌کند اندکی هوش به خرج داده و مقادیر ارسالی به سرور را به پارامترهای تعریفی، حتی به خواص اشیاء این پارامترهای تعریف شده، نگاشت کند.

در ASP.NET MVC سه نوع Model binder وجود دارند:
1) Model binder پیش فرض که توضیحات آن به همراه مثالی ارائه شد.
2) Form collection model binder که در ادامه توضیحات آن‌را مشاهده خواهید نمود.
3) HTTP posted file base model binder که توضیحات آن به قسمت بعدی موکول می‌شود.

یک نکته:
اولین متد LoginResult کنترلر را به نحو زیر نیز می‌توان بازنویسی کرد:
[HttpPost]
[ActionName("LoginResultWithFormCollection")]
public ActionResult LoginResult(FormCollection collection)
{
string name = collection["name"];
string password = collection["password"];

if (name == "Vahid" && password == "123")
ViewBag.Message = "Succeeded";
else
ViewBag.Message = "Failed";

return View("Result");
}

در اینجا FormCollection به صورت خودکار بر اساس مقادیر ارسالی به سرور توسط فریم ورک تشکیل می‌شود (FormCollection هم یک نوع model binder ساده است) و اساسا یک NameValueCollection می‌باشد.
بدیهی است در این حالت باید نگاشت مقادیر دریافتی، به متغیرهای متناظر با آن‌ها، دستی انجام شود (مانند مثال فوق) یا اینکه می‌توان از متد UpdateModel کلاس Controller هم استفاده کرد:

[HttpPost]
public ActionResult LoginResultUpdateFormCollection(FormCollection collection)
{
var account = new Account();
this.UpdateModel(account, collection.ToValueProvider());

if (account.Name == "Vahid" && account.Password == "123")
ViewBag.Message = "Succeeded";
else
ViewBag.Message = "Failed";

return View("Result");
}

متد توکار UpdateModel، به صورت خودکار اطلاعات FormCollection دریافتی را به شیء مورد نظر، نگاشت می‌کند.
همچنین باید عنوان کرد که متد UpdateModel، در پشت صحنه از اطلاعات Model binder پیش فرض و هر نوع Model binder سفارشی که ایجاد کنیم استفاده می‌کند. به این ترتیب زمانیکه از این متد استفاده می‌کنیم، اصلا نیازی به استفاده از FormCollection نیست و متد بدون آرگومان زیر هم به خوبی کار خواهد کرد:

[HttpPost]
public ActionResult LoginResultUpdateModel()
{
var account = new Account();
this.UpdateModel(account);

if (account.Name == "Vahid" && account.Password == "123")
ViewBag.Message = "Succeeded";
else
ViewBag.Message = "Failed";

return View("Result");
}

استفاده از model binderها همینجا به پایان نمی‌رسد. نکات تکمیلی آن‌ها در قسمت بعدی بررسی خواهند شد.

نظرات مطالب
ارتقاء به HTTP Client در Angular 4.3
یک نکته‌ی تکمیلی: روش ارسال form-urlencoded به سرور، بجای JSON

doRefreshToken(refreshToken: string): Observable<any> {
  const body = new HttpParams()
    .set('grant_type', "refresh_token")
    .set('refresh_token', refreshToken);

  return this.http.post('/login',
    body.toString(),
    {
      headers: new HttpHeaders()
        .set('Content-Type', 'application/x-www-form-urlencoded')
    }
  );
}
نظرات مطالب
مباحث تکمیلی مدل‌های خود ارجاع دهنده در EF Code first
به دلیل اینکه دیگر در EF Core متدهای HasOptional و WillCascadeOnDelete را نداریم قسمت ModelBuilder به شکل زیر تغییر می‌کند.
modelBuilder.Entity<BlogComment>()
                        .HasOne(x => x.Reply)
                        .WithMany(x => x.Children)
                        .HasForeignKey(x => x.ReplyId)
                        .OnDelete(DeleteBehavior.SetNull);
فقط بایستی navigation‌های مربوطه هم نال پذیر باشد یعنی:
  public virtual BlogComment? Reply { set; get; }
public int? ReplyId { get; set; }
public ICollection<BlogComment>? Children { get; set; }