مطالب
اعمال کنترل دسترسی پویا در پروژه‌های ASP.NET Core با استفاده از AuthorizationPolicyProvider سفارشی

در مطلب «سفارشی سازی ASP.NET Core Identity - قسمت پنجم - سیاست‌های دسترسی پویا» به طور مفصل به قضیه کنترل دسترسی پویا در ASP.NET Core Identity پرداخته شده‌است؛ در این مطلب روش دیگری را بررسی خواهیم کرد.

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

services.AddAuthorization(options =>
{
    options.AddPolicy("View Projects", 
        policy => policy.RequireClaim(CustomClaimTypes.Permission, "projects.view"));
});
با یک ClaimType مشخص برای دسترسی‌ها، یک سیاست جدید را تعریف کرده و برای استفاده از آن نیز همانند قبل به شکل زیر می‌توان عمل کرد:
[Authorize("View Projects")]
public IActionResult Index(int siteId)
{
    return View();
}
روشی یکپارچه و بدون نیاز به کوچکترین سفارشی سازی؛ ولی در مقیاس بزرگ تعریف سیاست‌ها برای تک تک دسترسی‌های مورد نیاز، قطعا آزار دهنده خواهد بود. خبر خوب اینکه زیرساخت احراز هویت و کنترل دسترسی در ASP.NET Core مکانیزمی برای خودکار کردن فرآیند تعریف options.AddPolicy‌ها در کلاس آغازین برنامه، ارائه داده‌است که دقیقا یکی از موارد استفاده‌ی آن، راه حلی می‌باشد برای همین مشکلی که مطرح شد.
Using a large range of policies (for different room numbers or ages, for example), so it doesn’t make sense to add each individual authorization policy with an AuthorizationOptions.AddPolicy call. 


کار با پیاده سازی واسط IAuthorizationPolicyProvider شروع می‌شود؛ یا شاید ارث بری از DefaultAuthorizationPolicyProvider رجیستر شده‌ی در سیستم DI و توسعه آن هم کافی باشد.

public class AuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider
{
    public AuthorizationPolicyProvider(IOptions<AuthorizationOptions> options)
        : base(options)
    {
    }

    public override Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
    {
        if (!policyName.StartsWith(PermissionAuthorizeAttribute.PolicyPrefix, StringComparison.OrdinalIgnoreCase))
        {
            return base.GetPolicyAsync(policyName);
        }

        var permissionNames = policyName.Substring(PermissionAuthorizeAttribute.PolicyPrefix.Length).Split(',');

        var policy = new AuthorizationPolicyBuilder()
            .RequireClaim(CustomClaimTypes.Permission, permissionNames)
            .Build();

        return Task.FromResult(policy);
    }
}

متد GetPolicyAsync موظف به یافتن و بازگشت یک Policy ثبت شده می‌باشد؛ با این حال می‌توان با بازنویسی آن و با استفاده از وهله‌ای از AuthorizationPolicyBuilder، فرآیند تعریف سیاست درخواست شده را که احتمالا در تنظیمات آغازین پروژه تعریف نشده و پیشوند مدنظر را نیز دارد، خوکار کرد. در اینجا امکان ترکیب کردن چندین دسترسی را هم خواهیم داشت که برای این منظور می‌توان دسترسی‌های مختلف را به صورت comma separated به سیستم معرفی کرد. 

نکته‌ی مهم در تکه کد بالا مربوط است به PolicyPrefix که با استفاده از آن مشخص کرده‌ایم که برای هر سیاست درخواستی، این فرآیند را طی نکند و موجب اختلال در سیستم نشود. 


پس از پیاده سازی واسط مطرح شده، لازم است این پیاده سازی جدید را به سیستم DI هم معرفی کنید:

services.AddSingleton<IAuthorizationPolicyProvider, AuthorizationPolicyProvider>();


خوب، تا اینجا فرآیند تعریف سیاست‌ها به صورت خودکار انجام شد. در ادامه نیاز است با تعریف یک فیلتر Authorization، بتوان لیست دسترسی‌های مورد نظر برای اکشنی خاص را نیز مشخص کرد تا در متد GetPolicyAsync فوق، کار ثبت خودکار سیاست دسترسی متناظر با آن‌را توسط فراخوانی متد policyBuilder.RequireClaim، انجام دهد تا دیگر نیازی به تعریف دستی و جداگانه‌ی آن، در کلاس آغازین برنامه نباشد. برای این منظور به شکل زیر عمل خواهیم کرد:

    public class PermissionAuthorizeAttribute : AuthorizeAttribute
    {
        internal const string PolicyPrefix = "PERMISSION:";

        /// <summary>
        /// Creates a new instance of <see cref="AuthorizeAttribute"/> class.
        /// </summary>
        /// <param name="permissions">A list of permissions to authorize</param>
        public PermissionAuthorizeAttribute(params string[] permissions)
        {
            Policy = $"{PolicyPrefix}{string.Join(",", permissions)}";
        }
    }

همانطور که مشخص می‌باشد، رشته PERMISSION به عنوان پیشوند رشته تولیدی از لیست اسامی دسترسی‌ها، استفاده شده‌است و در پراپرتی Policy قرار داده شده‌است. این بار برای کنترل دسترسی می‌توان به شکل زیر عمل کرد:

[PermissionAuthorize(PermissionNames.Projects_View)]
public IActionResult Get(FilteredQueryModel query)
{
   //...
}
[PermissionAuthorize(PermissionNames.Projects_Create)]
public IActionResult Post(ProjectModel model)
{
   //...
}

برای مثال در اولین فراخوانی فیلتر PermissionAuthorize فوق، مقدار ثابت PermissionNames.Projects_View به عنوان یک Policy جدید به متد GetPolicyAsync کلاس AuthorizationPolicyProvider سفارشی ما ارسال می‌شود. چون دارای پیشوند «:PERMISSION» است، مورد پردازش قرار گرفته و توسط متد policyBuilder.RequireClaim به صورت خودکار به سیستم معرفی و ثبت خواهد شد.


همچنین راه حل مطرح شده برای مدیریت دسترسی‌های پویا، در gist به اشتراک گذاشته شده «موجودیت‌های مرتبط با مدیریت دسترسی‌های پویا» را نیز مد نظر قرار دهید.

نظرات مطالب
شروع به کار با EF Core 1.0 - قسمت 5 - استراتژهای تعیین کلید اصلی جداول و ایندکس‌ها
ساده شدن امکان تعریف ایندکس‌ها با Attributes از EF-Core 5x

ویژگی جدید Index که در اسمبلی Microsoft.EntityFrameworkCore.Abstractions واقع شده‌است، امکان تعریف انواع و اقسام ایندکس‌ها را میسر می‌کند. این ویژگی باید به خود کلاس اعمال شود و نه تک تک خواص. چند مثال:
الف) تعریف ایندکس بر روی خاصیت Url یک کلاس
[Index(nameof(Url))]
public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }
}
که امکان تعریف نام سفارشی آن نیز میسر است:
[Index(nameof(Url), Name = "Index_Url")]

ب) ایندکس‌های ترکیبی
[Index(nameof(FirstName), nameof(LastName))]
public class Person
{
    public int PersonId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

ج) ایندکس‌های منحصربفرد
[Index(nameof(Url), IsUnique = true)]
public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }
}
مطالب
وی‍‍ژگی های پیشرفته ی AutoMapper - قسمت اول
در پست قبلی مقدمه‌ای داشتیم بر AutoMapper؛ مثالی که در اون پست عنوان شد ساده‌ترین و پرکاربردترین روش استفاده از AutoMapper هست بنام Flattening که در واقع از یک شیء کل به یک شیء کوچکتر می‌رسیم.
همانطور که در قسمت اول گفتم AutoMapper کارش رو بر اساس قراردادها انجام میده یا همون Convention Base. یکی از قردادهای AutoMapper، نگاشت براساس نام اعضای اون شی هست؛ مثلا در مثال قبلی FirstName در مبداء، به خاصیتی با همین نام نگاشت شد و ...

Projection
روش استفاده بعدی که به اون Projection (معنی فارسی خوب برا Projection چیه ؟) میگن برای مواقعی هست که اعضای یک شیء در مبداء، همتایی در مقصد ندارد (از نظر نام) و در واقع می‌خواهیم یک شیء رو به یک شیء دیگه تغییر شکل بدیم.
مدل زیر رو در نظر بگیرید:
  public class Person
    {
        public int Id { get; set; }

        public string FirstName { get; set; }       

        public string LastName { get; set; }

    }

  که قرار است به مدل PersonDTO نگاشت بشه.
  public class PersonDTO
    {
        public int Id { get; set; }

        public string Name { get; set; }

        public string Family { get; set; }

public string FullName { get; set; }

        public string Email { get; set; }
    }
همنطور که می‌بینید در مقصد خاصیت‌هایی داریم که در مبداء همتایی ندارند. برای نگاشت چنین اشیایی، از متد ForMember  استفاده و نگاشت‌های شی‌های موردنظر رو Custom می‌کنیم.
 Mapper.CreateMap<Person, PersonDTO>().ForMember(des => des.Name, op => op.MapFrom(src => src.FirstName)).
                ForMember(des => des.FullName, op => op.MapFrom(src => src.FirstName + " " + src.LastName)).ForMember(
                    des => des.Email, op => op.Ignore());
متد ForMember  از یک Action Delegate  برای کانفیگ کردن هر عضو استفاده میکنه. در مثال بالا ما از MapFrom برای اعمال نگاشت Custom  استفاده کردیم.
AutoMapper.IMappingExpression<TSource,TDestination>.ForMember(System.Linq.Expressions.Expression<System.Func<TDestination,object>>, System.Action<AutoMapper.IMemberConfigurationExpression<TSource>>)

نکته:برای تست کردن اینکه آیا نگاشت ما Exception  داره یا نه میتوان از متد زیر استفاده کرد.
Mapper.AssertConfigurationIsValid();

نکته
: همیشه موقع نگاشت، اعضای شیء مقصد برای AutoMapper  مهمن؛ مثلا در مثال بالا خاصیت LastName در مبداء به هیچ عضوی در مقصد نگاشت نشده (به صورت مستقیم) و این تولید Exception  نمیکنه ولی برعکس اون باعث تولید Exception میشه؛ مثلا اگه خاصیت Email  رو که در مبداء همتایی نداره رو کانفیگ نکنیم، باعث تولید Exception میشه.
 
نحوه استفاده
var person = new Person
                {
                    Id = 1,
                    FirstName = "Mohammad",
                    LastName = "Saheb",
                    
                };
var personDTO = Mapper.Map<Person, PersonDTO>(person);
خروجی به شکل زیر میشه


Collection
بعد از نوشتن کانفیگ نگاشت‌ها، بدون نیاز به تنظیمات خاصی میتونیم مجموعه ای از شی‌های مبداء رو به مقصد نگاشت کنیم. مجموعه‌های پشتیبانی شده به شرح زیرن.
IEnumerable, IEnumerable<T>, ICollection, ICollection<T>, IList, IList<T>, List<T>.
و البته آرایه ها.
مثال
var persons = new[]
                {
                    new Person { Id = 1, FirstName = "Mohammad", LastName = "Saheb" },
                    new Person { Id = 2, FirstName = "Babak", LastName = "Saheb" }
                };

var personDTOs = Mapper.Map<Person[], List<PersonDTO>>(persons);
خروجی به شکل زیر میشه

Nested Mappings
برای نگاشت کلاس‌های تو در تو از این روش استفاده می‌کنیم و ...
فرض کنید در کلاس Person خاصیتی از نوع Address داریم و در کلاس PersonDTO  خاصیتی از نوع AddressDTO.
public class Address
    {
        public string Ad1 { get; set; }
        public string Ad2 { get; set; }
    }


public class AddressDTO
    {
        public string Ad1 { get; set; }
        public string Ad2 { get; set; }
    }
برای نگاشت‌هایی از این دست باید تنظیمات نگاشت مربوط به نوع‌های تودرتو را صریحا معین کنیم.
Mapper.CreateMap<Address, AddressDTO>();
نکته: هرچند منطقی‌تر بنظر میرسه که تعریف نگاشت‌های داخلی ابتدا بیاد، ولی فرقی نمیکنه که تعریف این نگاشت قبل یا بعد از نگاشت اصلی باشه.
ادامه دارد...
مطالب
پراپرتی سفارشی در EF Database First
فرض کنید در روش EF Database First می‌خواهید فیلدی به مدل اضافه شود که در دیتابیس وجود ندارد، درواقع فیلدی محاسباتی به مدل اضافه کنید.راه حل چیست؟ اولین روشی که ممکن است به ذهن برسد این است که به کلاس مدل که از جدول دیتابیس ساخته شده، فیلدی محاسباتی اضافه می‌کنیم.  
public class Person
{
    public string FullName {
        get
        {
            return FirstName + " " + LastName;
        }
    }
}
 به نظر راه حل درستی می‌رسد، اما مشکل این روش چیست؟
 اگر مدل برنامه از روی دیتابیس بروزشود، چه اتفاقی می‌افتد؟
خب قاعدتا این فیلد محاسباتی از دست می‌رود و باید دوباره آن را به کلاس مدل جدید و بروز شده اضافه کنیم. که به نظر راه حل منطقی و خوبی نمیرسد. در چنین مواقعی می‌توان به جای اینکه این پراپرتی را به کلاس تولید شده از روی دیتابیس اضافه کرد، این فیلد را به یک Partial Class از کلاس تولید شده که درهمان پروژه و فضای نام کلاس تولید شده از دیتابیس قراردارد، اضافه کرد. به این ترتیب با هر بار بروز شدن مدل از روی دیتابیس، این فیلد از بین نمی‌رود. 
public partial class Person
{
    public string FirstName {get; set;}
    public string LastName {get; set; }
}
public partial class Person
{
    public string FullName {
        get
        {
            return FirstName + " " + LastName;
        }
    }
}

اما این روش محدودیت هایی نیز دارد :
1. همه قسمت‌های Partial Class باید در یک اسمبلی باشند.
2. پراپرتی‌های درون Partial Class در دیتابیس ذخیره نمی‌شوند.
3. ونیز این پراپرتی‌ها در عبارات Linq قابل استفاده نیستند.چون عبارت Linq در نهایت به یک رشته SQL تبدیل شده و در دیتابیس اجرا می‌شود.البته با فرض این که دیتاپرووایدر، SQL  باشد.
 
مطالب
مقدمه ای بر CQRS و Event Sourcing
به صورت عام، functionality اکثر پروژه‌های نرم افزاری تجاری خلاصه میشود به مخفف معروفی به نام CRUD، که object‌ها را میسازیم، آن‌ها را میخوانیم و تغییر میدهیم.
اپلیکیشن‌های طراحی شده بدین صورت، قابلیت خوانایی بالایی خواهند داشت و دیاگرام طراحی آنها چیزی شبیه به تصویر زیر میباشد

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

زمانیکه نیاز‌های پروژه روز به روز افزورده و پیچیده‌تر میشود، مدل CRUD بصورت پیوسته از ارزشش کاسته میشود و از آن سادگی اولیه‌ی در درک و خوانایی آن دور خواهد شد.

ذات CQRS بر آن است که شما مدل‌های مختلفی را برای خواندن و نوشتن دیتا داشته باشید. الگوی آن چیزی شبیه به تصویر زیر است

چیزی که در این روش مشهود است این میباشد که برنامه نویسان باید قسمت‌های Command و Query را به صورت جداگانه طراحی نمایند.
CQRS این قابلیت را به شما میدهد که interface و Datastore و حتی بطور کامل Technology مجزایی در قسمت‌های CQ داشته باشید.

Event Sourcing
قسمت دوم و متمایز، معماری Event Sourcing یا ES میباشد که بصورت کوتاه، ES یک روش متفاوت برای Data storage میباشد.
اکثرا ما از Datastore‌هایی که مدلی از دیتا را انعکاس می‌دهند استفاده میکنیم. به مثال ساده‌ی زیر توجه کنید

ES از برنامه نویسان میخواهد که مدل سنتی CRUD را فراموش کرده و بجای آن تغییراتی را که روی دیتا صورت گرفته، نیز درج نمایند. اینکار به وسیله‌ی یک دیتابیس Append-only انجام میشود که به نام Event Store شناخته میشود.

در این معماری ما همه‌ی تغییرات روی دیتا را به صورت Serialize Event ذخیره میکنیم که میتواند دوباره در هر زمانی اجرا شده و current state هر objectی را در اختیار بگذارد.

این روش به ما کمک بزرگی میکند تا وضعیت یک object را در گذشته به راحتی پیدا کنیم و از آن میتوان به غیر از فوایدی که دارد، به عنوان یک Logger نیز استفاده نمود. به دلیل اینکه جزء به جزء تغییرات بر روی state سیستم، در آن ثبت شده است. از آنجاییکه دیتا بصورت serialize ذخیره میشود، بارگزاری آن نیز با سرعت بالایی انجام خواهد شد.

پس بصورت خلاصه در معماری Command Query Responsibility Segregation ابتدا باید به این موضوع توجه داشت که قسمت‌های Read و Write نرم افزار به صورت مجزایی طراحی میشوند و Event Sourcing شامل تغییراتی که روی data انجام شده است، میباشد و به‌صورت Serialize شده ذخیره میشود. ما تنها به یک دیتابیس و یک جدول برای نمایش event store نیازمندیم (بستگی به نیازتان میتوان تعداد آن را نیز بیشتر نمود و همچنین حتما لزومی ندارد که از دیتابیس‌های رابطه‌ای استفاده شود؛ بصورت مثال پیاده سازی این قسمت را میتوان با استفاده از Redis که دیتابیسی غیر رابطه‌ای و باسرعت میباشد استفاده نمود).
برای شروع کار (نه پیاده سازی کامل) باید با قسمت‌های مختلف طراحی در این معماری آشنا شویم:
Domain Object
نکته: SimpleCqrs فریم ورکی برای پیاده سازی معماری CQRS , ES میباشد که برای ساده‌تر شدن کار، از آن استفاده شده است (شما حتی میتوانید پیاده سازی خود را داشته باشید)
مدل Movie از کلاسی به نام AggregateRoot ارث بری کرده‌است که توسط SimpleCQRS پیاده سازی شده‌است و یک guid key در آن تعبیه شده است (Aggregate root از مباحث Domain Driven برگرفته شده است و آشنایی با آن کمک شایانی به درک عمیق‌تر روی این مباحث مینماید).
public class Movie : AggregateRoot  
{
    public string Title { get; set; }
    public DateTime ReleaseDate { get; set; }
    public int RunningTimeMinutes { get; set; }

    public Movie() { }

    public Movie(Guid movieId, string title, DateTime releaseDate, int runningTimeMinutes)
    {
        //پیاده سازی خواهد شد
    }
}
توجه: SimpleCQRS فقط پیاده سازی guid برای کلید مربوط به هر مدل را پیاده سازی نموده است؛ بنابراین کلید مدل نمیتواند integer باشد.

Commands
command دستوراتی است که توسط end user فراخوانی میشود که باعث تغییرات خواهد شد. وقتی اپلیکیشن یک command را دریافت مینماید، command handler به پردازش آن برای فهمیدن خواسته کاربر میپردازد و پس از آن event مربوطه را برای اجرای آن وظیفه‌ی خاص صدا میزند.
همه‌ی command‌ها تغییراتی بر روی state جاری خواهند داشت. در نتیجه دیتا‌های ذخیره شده درون دیتابیس تغییرات خواهند کرد. هر commandی که تغییری بر روی State سیستم نداشته باشد، یک دستور غلط محسوب شده و باید در سمت query‌ها آن را پیاده سازی نمود.
در نتیجه Commnad‌ها دستوراتی هستند که از طرف کاربر برای تغییرات بر روی دیتا‌های ذخیره شده، ارسال میشوند.
فرض کنید Domain Objectی برای Movie تعریف کرده‌ایم و میخواهیم دستور اضافه کردن فیلم را پیاده سازی نماییم
public class CreateMovieCommand : ICommand  
{
    public string Title { get; set; }
    public DateTime ReleaseDate { get; set; }
    public int RunningTimeMinutes { get; set; }
    public CreateMovieCommand(string title, DateTime releaseDate, int runningTime)
    {
        Title = title;
        ReleaseDate = releaseDate;
        RunningTimeMinutes = runningTime;
    }
}
توجه: ICommand از طریق SimpleCQRS اضافه شده‌است.

Command Handler
بعد از اینکه Command مورد نیاز نوشته شد، حال احتیاج به پیاده سازی CommandHandler مربوطه که دستور متناظر را پردازش میکند، داریم.
public class CreateMovieCommandHandler : CommandHandler<CreateMovieCommand>  
{
    protected IDomainRepository _repository;

    public CreateMovieCommandHandler(IDomainRepository repository)
    {
        _repository = repository;
    }

    public override void Handle(CreateMovieCommand command)  
    {
        var movie = new Domain.Movie(Guid.NewGuid(), command.Title, 
    command.ReleaseDate, command.RunningTimeMinutes);

        _repository.Save(movie);
    }
}
Command Handler باید از کلاس جنریک <CommandHandler<T ارث بری نماید و T باید از نوع Command در نظر گرفته شود و همچنین IDomainRepository اینترفیسی است که توسط SimpleCQRS تعریف شده‌است و ما احتیاجی به پیاده سازی آن نداریم (در قسمت‌های بعدی پیکربندی آن را انجام میدهیم).
برای رسیدگی کردن به دستور مربوطه احتیاج به override کردن متد Handle میباشد.
کار اساسی توسط متد Save انجام میشود که همه‌ی event‌های pending شده توسط Domain Object را گرفته و آنها را به Event Store میفرستد.

Events
event‌ها تغییراتی هستند بر روی State جاری سیستم که توسط کاربر به وسیله‌ی Commandها فراخوانی میشوند.
رویداد‌ها serialize میشوند و درون Event Store ذخیره میشوند؛ بنابراین میتوان فراخوانی آنها را در هر لحظه انجام داد.
هر تعداد Event میتواند توسط یک دستور raise شود.
ساخت یک Event:
قبلا دستوری را برای ساخت یک movie نوشتیم و حال احتیاج به event مربوطه را داریم:
public class MovieCreatedEvent : DomainEvent  
{
    public Guid MovieId
    {
        get { return AggregateRootId; }
        set { AggregateRootId = value;}
    }

    public string Title { get; set; }
    public DateTime ReleaseDate { get; set; }
    public int RunningTimeMinutes { get; set; }

    public MovieCreatedEvent(Guid movieId, string title, DateTime releaseDate, int runningTime)
    {
        MovieId = movieId;
        Title = title;
        ReleaseDate = releaseDate;
        RunningTimeMinutes = runningTime;
    }
}
فراموش نکنید که این کلاس آبجکتی خواهد بود که Serialize شده و در دیتابیس ذخیره خواهد شد. باید همه‌ی پراپرتی‌های لازم که با استفاده از این Event ممکن است تغییر کنند را شامل شود (بدیهی است که این پراپرتی‌ها از Domain Object گرفته میشود).
public class Movie : AggregateRoot  
{
    public string Title { get; set; }
    public DateTime ReleaseDate { get; set; }
    public int RunningTimeMinutes { get; set; }

    public Movie(Guid movieId, string title, DateTime releaseDate, int runningTimeMinutes)
    {
        Apply(new MovieCreatedEvent(Guid.NewGuid(), title, releaseDate, runningTimeMinutes));
    }
}
به Aggregate فوق که در اوایل بحث صحبت شده‌است دقت کنید. حال متد Apply باعث میشود که event مربوطه درون بخش لوکال aggregate root ذخیره شود. بنابراین بعدا میتواند به صورت فیزیکی درون Event Store ذخیره شود.

Event Handler
هر Event Handler  میتواند تعداد زیادی از IHandleDomainEvents ‌ها را پیاده سازی نماید. حال متد Handle این اینترفیس را پیاده سازی نمودیم. 
public class MovieEventHandler : IHandleDomainEvents<MovieCreatedEvent>  
{
    public void Handle(MovieCreatedEvent createdEvent)
    {
        using (MoviesContext entities = new MoviesContext())
        {
            entities.Movies.Add(new Movie()
            {
                Id = createdEvent.AggregateRootId,
                Title = createdEvent.Title,
                ReleaseDate = createdEvent.ReleaseDate,
                RunningTimeMinutes = createdEvent.RunningTimeMinutes
            });

            entities.SaveChanges();
        }
    }
}
مثلا در این قسمت با استفاده از ORM، شیء مورد نظر به صورت فیزیکی درون دیتابیس ذخیره میشود.
در قسمت آخر نیازمندیم که تغییرات زیر را به Movie اضافه نماییم.
درون Doamin Objectی که قبلا تعریف کرده بودیم متدی را به صورت زیر پیاده سازی مینماییم
protected void OnMovieCreated(MovieCreatedEvent domainEvent)
        {
            Id = domainEvent.AggregateRootId;
            Title = domainEvent.Title;
            ReleaseDate = domainEvent.ReleaseDate;
            RunningTimeMinutes = domainEvent.RunningTimeMinutes;
        }
باعث میشود پس از فراخوانی شدن Event، تغییرات صورت گرفته‌ی بر state سیستم، بر روی Domain Object اعمال شود و آن را بروزرسانی نماید. این متد دقیقا بصورت اتوماتیک وقتی که event مربوطه raise میشود، فراخوانی میشود.
پس از ترکیب CQRS و ES معماری اولیه‌ی سیستم چیزی شبیه به دیاگرام زیر خواهد بود (بسته به سناریوهای خاص میتواند سفارشی سازی شود)

خلاصه:
کاربر دستوری را از طریق برنامه به سیستم ارسال مینماید.
command مربوطه دریافت میشود و به روی Command Bus قرار داده میشود.
Command Handler وظیفه‌ی تفسیر کردن Command مربوطه را به عهده میگیرد و به وسیله‌ی Domain object آن event مورد نظر فراخوانی خواهد شد و باعث میشود domain object بروزرسانی گردد.
Event همان objectی است که باید به صورت serialize شده درون append only database ذخیره شود.
Event handler رویداد مربوطه را گرفته و بصورت فیزیکی مقادیر مورد نظر را در دیتابیس ذخیره مینماید.

Query
از آنجاییکه قسمت Read، در سیستم به صورت CQRS طراحی میشود، به راحتی میتوان query‌ها را optimize کرده و به صورت مثال به جای استفاده از ORM‌های معمول بطور مستقیم Stored Procedure فراخوانی کرده، تا جای ممکن کیفیت query‌ها بهترین حالت ممکن باشند. در حالیکه در مدل CRUD بهینه کردن بخش read بسیار پیچیده و بعضا غیر ممکن میباشد.

مزایای استفاده از این مدل
Distributed Systems Capabilities
یکی از مهمترین مزیت‌های این مدل تسهیم گسترش پذیری سیستم بر روی ماشین‌های فیزیکی مختلف از طریق messaging pattern میباشد.
High Availability
از آنجایی که سیستم توزیع پذیر طراحی شده‌است، هر قسمت از آن میتواند بدون توجه به fail شدن قسمت‌های دیگر به کار خود ادامه دهد.
Reduce Complexity
در domain‌های پیچیده طراحی و پیاده سازی objectهایی که مسئول دو قسمت read و write هستند، میتواند کار را بیش از حد پیچیده کرده و در این صورت چون business logic و read logic در هم ترکیب میشوند، مدیریت کردن موارد multiple user, shared data, performance, transactions, consistency سخت و سخت‌تر میشود.
Facilitates Building Task-based UI
وقتی شما به پیاده سازی الگوی CQRS میپردازید، اصولا هر عملی که توسط End user از طریق ui ارسال میشود، معادل command مربوط به آن وجود دارد. به همین جهت میتوان عملیات لازم برای اجرای یک پروسه را بصورت واضحی درک کرد.
 Maintenance And Flexibility
هر چند پیاده سازی این مدل سخت خواهد بود، اما در ابعاد وسیع‌تر به دلیل اینکه هر قسمت به صورت مجزایی طراحی شده و اینکه دستورات و رویداد‌ها به صورت تفکیک شده پیاده سازی شده‌اند، همچنین وجود ES، قابلیت زیادی به debug سیستم می‌دهد.
نکته: ES مدل مورد قبولی برای اکثر معماری‌های نوین سیستم‌های نرم افزاری امروزی میباشد و فقط مختص به CQRS نمیباشد. بطور مثال در معماری Microservices به وفور از Event Sourcing استفاده میشود.

مشکلات استفاده از این مدل
  • ذاتا پیاده سازی این مدل سخت و دشوار است و از آنجاییکه سادگی در پیاده سازی سیستم‌های نرم افزاری، یک اصل مهم محسوب میشود، بنابراین استفاده از این مدل محدود میشود به سیستم‌های نرم افزاری که مزیت‌های گفته شده در قسمت فوق برایشان حیاتی محسوب شود.
  • برای پیاده سازی سیستمی با این مدل احتیاج به تیم توسعه‌ای است که با مفاهیم آن کاملا آشنا باشد.
  • هر چند امروزه فضای فیزیکی برای ذخیره سازی دیتا ارزان محسوب میشود، اما به هر حال استفاده از این مدل به همراه ES، حجم زیادی از Disk space را خواهد گرفت.
  • همانطور که دیدید برای پیاده سازی یک Insert ساده، حجم زیادی کد نوشته شده‌است. بنابراین تولید اینگونه نرم افزار‌ها به زمان بیشتری نیاز دارد.
بنابراین باید در انتخاب معماری سیستم بسیار دقت شود؛ هر چند که این مدل برای سیستم‌های بزرگ و پیچیده خیلی کارآمد محسوب میشود و باعث یک Domain object غنی ، History Tracking، شفافیت در مشکلات Concurrency و همچنین Scalability و غیره خواهد شد، اما پیدا کردن برنامه نویسانی با داشتن درک عمیق روی این مباحث کمی سخت به نظر میرسد.
در قسمت بعدی بصورت کامل به پیاده سازی این الگو در یک اپلیکشن دات نتی خواهیم پرداخت.
مطالب
ASP.NET MVC #13

اعتبار سنجی اطلاعات ورودی در فرم‌های ASP.NET MVC

زمانیکه شروع به دریافت اطلاعات از کاربران کردیم، نیاز خواهد بود تا اعتبار اطلاعات ورودی را نیز ارزیابی کنیم. در ASP.NET MVC، به کمک یک سری متادیتا، نحوه‌ی اعتبار سنجی، تعریف شده و سپس فریم ورک بر اساس این ویژگی‌ها، به صورت خودکار اعتبار اطلاعات انتساب داده شده به خواص یک مدل را در سمت کلاینت و همچنین در سمت سرور بررسی می‌نماید.
این ویژگی‌ها در اسمبلی System.ComponentModel.DataAnnotations.dll قرار دارند که به صورت پیش فرض در هر پروژه جدید ASP.NET MVC لحاظ می‌شود.

یک مثال کاربردی

مدل زیر را به پوشه مدل‌های یک پروژه جدید خالی ASP.NET MVC اضافه کنید:

using System;
using System.ComponentModel.DataAnnotations;

namespace MvcApplication9.Models
{
public class Customer
{
public int Id { set; get; }

[Required(ErrorMessage = "Name is required.")]
[StringLength(50)]
public string Name { set; get; }

[Display(Name = "Email address")]
[Required(ErrorMessage = "Email address is required.")]
[RegularExpression(@"\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*",
ErrorMessage = "Please enter a valid email address.")]
public string Email { set; get; }

[Range(0, 10)]
[Required(ErrorMessage = "Rating is required.")]
public double Rating { set; get; }

[Display(Name = "Start date")]
[Required(ErrorMessage = "Start date is required.")]
public DateTime StartDate { set; get; }
}
}

سپس کنترلر جدید زیر را نیز به برنامه اضافه نمائید:
using System.Web.Mvc;
using MvcApplication9.Models;

namespace MvcApplication9.Controllers
{
public class CustomerController : Controller
{
[HttpGet]
public ActionResult Create()
{
var customer = new Customer();
return View(customer);
}

[HttpPost]
public ActionResult Create(Customer customer)
{
if (this.ModelState.IsValid)
{
//todo: save data
return Redirect("/");
}
return View(customer);
}
}
}

بر روی متد Create کلیک راست کرده و گزینه Add view را انتخاب کنید. در صفحه باز شده، گزینه Create a strongly typed view را انتخاب کرده و مدل را Customer انتخاب کنید. همچنین قالب Scaffolding را نیز بر روی Create قرار دهید.

توضیحات تکمیلی

همانطور که در مدل برنامه ملاحظه می‌نمائید، به کمک یک سری متادیتا یا اصطلاحا data annotations، تعاریف اعتبار سنجی، به همراه عبارات خطایی که باید به کاربر نمایش داده شوند، مشخص شده است. ویژگی Required مشخص می‌کند که کاربر مجبور است این فیلد را تکمیل کند. به کمک ویژگی StringLength، حداکثر تعداد حروف قابل قبول مشخص می‌شود. با استفاده از ویژگی RegularExpression، مقدار وارد شده با الگوی عبارت باقاعده مشخص گردیده، مقایسه شده و در صورت عدم تطابق، پیغام خطایی به کاربر نمایش داده خواهد شد. به کمک ویژگی Range، بازه اطلاعات قابل قبول، مشخص می‌گردد.
ویژگی دیگری نیز به نام System.Web.Mvc.Compare مهیا است که برای مقایسه بین مقادیر دو خاصیت کاربرد دارد. برای مثال در یک فرم ثبت نام، عموما از کاربر درخواست می‌شود که کلمه عبورش را دوبار وارد کند. ویژگی Compare در یک چنین مثالی کاربرد خواهد داشت.
در مورد جزئیات کنترلر تعریف شده در قسمت 11 مفصل توضیح داده شد. برای مثال خاصیت this.ModelState.IsValid مشخص می‌کند که آیا کارmodel binding موفق بوده یا خیر و همچنین اعتبار سنجی‌های تعریف شده نیز در اینجا تاثیر داده می‌شوند. بنابراین بررسی آن پیش از ذخیره سازی اطلاعات ضروری است.
در حالت HttpGet صفحه ورود اطلاعات به کاربر نمایش داده خواهد شد و در حالت HttpPost، اطلاعات وارد شده دریافت می‌گردد. اگر دست آخر، ModelState معتبر نبود، همان اطلاعات نادرست وارد شده به کاربر مجددا نمایش داده خواهد شد تا فرم پاک نشود و بتواند آن‌ها را اصلاح کند.
برنامه را اجرا کنید. با مراجعه به مسیر http://localhost/customer/create، صفحه ورود اطلاعات کاربر نمایش داده خواهد شد. در اینجا برای مثال در قسمت ورود اطلاعات آدرس ایمیل، مقدار abc را وارد کنید. بلافاصله خطای اعتبار سنجی عدم اعتبار مقدار ورودی نمایش داده می‌شود. یعنی فریم ورک، اعتبار سنجی سمت کاربر را نیز به صورت خودکار مهیا کرده است.
اگر علاقمند باشید که صرفا جهت آزمایش، اعتبار سنجی سمت کاربر را غیرفعال کنید، به فایل web.config برنامه مراجعه کرده و تنظیم زیر را تغییر دهید:

<appSettings>
<add key="ClientValidationEnabled" value="true"/>

البته این تنظیم تاثیر سراسری دارد. اگر قصد داشته باشیم که این تنظیم را تنها به یک view خاص اعمال کنیم، می‌توان از متد زیر کمک گرفت:

@{ Html.EnableClientValidation(false); }

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

نحوه تعریف عناصر مرتبط با اعتبار سنجی در Viewهای برنامه نیز به شکل زیر است:

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

@using (Html.BeginForm()) {
@Html.ValidationSummary(true)
<fieldset>
<legend>Customer</legend>

<div class="editor-label">
@Html.LabelFor(model => model.Name)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Name)
@Html.ValidationMessageFor(model => model.Name)
</div>

همانطور که ملاحظه می‌کنید به صورت پیش فرض از jQuery validator در سمت کلاینت استفاده شده است. فایل jquery.validate.unobtrusive متعلق به تیم ASP.NET MVC است و کار آن وفق دادن سیستم موجود، با jQuery validator می‌باشد (validation adapter). در نگارش‌های قبلی، از کتابخانه‌های اعتبار سنجی مایکروسافت استفاده شده بود، اما از نگارش سه به بعد، jQuery به عنوان کتابخانه برگزیده مطرح است.
Unobtrusive همچنین در اینجا به معنای مجزا سازی کدهای جاوا اسکریپتی، از سورس HTML صفحه و استفاده از ویژگی‌های data-* مرتبط با HTML5 برای معرفی اطلاعات مورد نیاز اعتبار سنجی است:
<input data-val="true" data-val-required="The Birthday field is required." id="Birthday" name="Birthday" type="text" value="" />

اگر خواستید این مساله را بررسی کنید، فایل web.config قرار گرفته در ریشه اصلی برنامه را باز کنید. در آنجا مقدار UnobtrusiveJavaScriptEnabled را false کرده و بار دیگر برنامه را اجرا کنید. در این حالت کلیه کدهای اعتبار سنجی، به داخل سورس View رندر شده، تزریق می‌شوند و مجزا از آن نخواهند بود.
نحوه‌ی تعریف این اسکریپت‌ها نیز جالب توجه است. متد Url.Content، یک متد سمت سرور می‌باشد که در زمان اجرای برنامه، مسیر نسبی وارد شده را بر اساس ساختار سایت اصلاح می‌کند. حرف ~ بکارگرفته شده، در ASP.NET به معنای ریشه سایت است. بنابراین مسیر نسبی تعریف شده از ریشه سایت شروع و تفسیر می‌شود.
اگر از این متد استفاده نکنیم، مجبور خواهیم شد که مسیرهای نسبی را به شکل زیر تعریف کنیم:

<script src="../../Scripts/customvaildation.js" type="text/javascript"></script>

در این حالت بسته به محل قرارگیری صفحات و همچنین برنامه در سایت، ممکن است آدرس فوق صحیح باشد یا خیر. اما استفاده از متد Url.Content، کار مسیریابی نهایی را خودکار می‌کند.
البته اگر به فایل Views/Shared/_Layout.cshtml، مراجعه کنید، تعریف و الحاق کتابخانه اصلی jQuery در آنجا انجام شده است. بنابراین می‌توان این دو تعریف دیگر مرتبط با اعتبار سنجی را به آن فایل هم منتقل کرد تا همه‌جا در دسترس باشند.
توسط متد Html.ValidationSummary، خطاهای اعتبار سنجی مدل که به صورت دستی اضافه شده باشند نمایش داده می‌شود. این مورد در قسمت 11 توضیح داده شد (چون پارامتر آن true وارد شده، فقط خطاهای سطح مدل را نمایش می‌دهد).
متد Html.ValidationMessageFor، با توجه به متادیتای یک خاصیت و همچنین استثناهای صادر شده حین model binding خطایی را به کاربر نمایش خواهد داد.



اعتبار سنجی سفارشی

ویژگی‌های اعتبار سنجی از پیش تعریف شده، پر کاربردترین‌ها هستند؛ اما کافی نیستند. برای مثال در مدل فوق، StartDate نباید کمتر از سال 2000 وارد شود و همچنین در آینده هم نباید باشد. این موارد اعتبار سنجی سفارشی را چگونه باید با فریم ورک، یکپارچه کرد؟
حداقل دو روش برای حل این مساله وجود دارد:
الف) نوشتن یک ویژگی اعتبار سنجی سفارشی
ب) پیاده سازی اینترفیس IValidatableObject


تعریف یک ویژگی اعتبار سنجی سفارشی

using System;
using System.ComponentModel.DataAnnotations;

namespace MvcApplication9.CustomValidators
{
public class MyDateValidator : ValidationAttribute
{
public int MinYear { set; get; }

public override bool IsValid(object value)
{
if (value == null) return false;

var date = (DateTime)value;
if (date > DateTime.Now || date < new DateTime(MinYear, 1, 1))
return false;

return true;
}
}
}

برای نوشتن یک ویژگی اعتبار سنجی سفارشی، با ارث بری از کلاس ValidationAttribute شروع می‌کنیم. سپس باید متد IsValid آن‌را تحریف کنیم. اگر این متد false برگرداند به معنای شکست اعتبار سنجی می‌باشد.
در ادامه برای بکارگیری آن خواهیم داشت:
[Display(Name = "Start date")]
[Required(ErrorMessage = "Start date is required.")]
[MyDateValidator(MinYear = 2000,
ErrorMessage = "Please enter a valid date.")]
public DateTime StartDate { set; get; }

اکنون مجددا برنامه را اجرا نمائید. اگر تاریخ غیرمعتبری وارد شود، اعتبار سنجی سمت سرور رخ داده و سپس نتیجه به کاربر نمایش داده می‌شود.


اعتبار سنجی سفارشی به کمک پیاده سازی اینترفیس IValidatableObject

یک سؤال: اگر اعتبار سنجی ما پیچیده‌تر باشد چطور؟ مثلا نیاز باشد مقادیر دریافتی چندین خاصیت با هم مقایسه شده و سپس بر این اساس تصمیم گیری شود. برای حل این مشکل می‌توان از اینترفیس IValidatableObject کمک گرفت. در این حالت مدل تعریف شده باید اینترفیس یاد شده را پیاده سازی نماید. برای مثال:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using MvcApplication9.CustomValidators;

namespace MvcApplication9.Models
{
public class Customer : IValidatableObject
{
//... same as before

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var fields = new[] { "StartDate" };
if (StartDate > DateTime.Now || StartDate < new DateTime(2000, 1, 1))
yield return new ValidationResult("Please enter a valid date.", fields);

if (Rating > 4 && StartDate < new DateTime(2003, 1, 1))
yield return new ValidationResult("Accepted date should be greater than 2003", fields);
}
}
}

در اینجا در متد Validate، فرصت خواهیم داشت تا به مقادیر کلیه خواص تعریف شده در مدل دسترسی پیدا کرده و بر این اساس اعتبار سنجی بهتری را انجام دهیم. اگر اطلاعات وارد شده مطابق منطق مورد نظر نباشند، کافی است توسط yield return new ValidationResult، یک پیغام را به همراه فیلدهایی که باید این پیغام را نمایش دهند، بازگردانیم.
به این نوع مدل‌ها، self validating models هم گفته می‌شود.


یک نکته:

از MVC3 به بعد، حین کار با ValidationAttribute، امکان تحریف متد IsValid به همراه پارامتری از نوع ValidationContext نیز وجود دارد. به این ترتیب می‌توان به اطلاعات سایر خواص نیز دست یافت. البته در این حالت نیاز به استفاده از Reflection خواهد بود و پیاده سازی IValidatableObject، طبیعی‌تر به نظر می‌رسد:

protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var info = validationContext.ObjectType.GetProperty("Rating");
//...
return ValidationResult.Success;
}




فعال سازی سمت کلاینت اعتبار سنجی‌های سفارشی

اعتبار سنجی‌های سفارشی تولید شده تا به اینجا، تنها سمت سرور است که فعال می‌شوند. به عبارتی باید یکبار اطلاعات به سرور ارسال شده و در بازگشت، نتیجه عملیات به کاربر نمایش داده خواهد شد. اما ویژگی‌های توکاری مانند Required و Range و امثال آن، علاوه بر سمت سرور، سمت کاربر هم فعال هستند و اگر جاوا اسکریپت در مرورگر کاربر غیرفعال نشده باشد، نیازی به ارسال اطلاعات یک فرم به سرور جهت اعتبار سنجی اولیه، نخواهد بود.
در اینجا باید سه مرحله برای پیاده سازی اعتبار سنجی سمت کلاینت طی شود:
الف) ویژگی سفارشی اعتبار سنجی تعریف شده باید اینترفیس IClientValidatable را پیاده سازی کند.
ب) سپس باید متد jQuery validation متناظر را پیاده سازی کرد.
ج) و همچنین مانند تیم ASP.NET MVC، باید unobtrusive adapter خود را نیز پیاده سازی کنیم. به این ترتیب متادیتای ASP.NET MVC به فرمتی که افزونه jQuery validator آن‌را درک می‌کند، وفق داده خواهد شد.

در ادامه، تکمیل کلاس سفارشی MyDateValidator را ادامه خواهیم داد:
using System;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
using System.Collections.Generic;

namespace MvcApplication9.CustomValidators
{
public class MyDateValidator : ValidationAttribute, IClientValidatable
{
// ... same as before

public IEnumerable<ModelClientValidationRule> GetClientValidationRules(
ModelMetadata metadata,
ControllerContext context)
{
var rule = new ModelClientValidationRule
{
ValidationType = "mydatevalidator",
ErrorMessage = FormatErrorMessage(metadata.GetDisplayName())
};
yield return rule;
}
}
}

در اینجا نحوه پیاده سازی اینترفیس IClientValidatable را ملاحظه می‌نمائید. ValidationType، نام متدی خواهد بود که در سمت کلاینت، کار بررسی اعتبار داده‌ها را به عهده خواهد گرفت.
سپس برای مثال یک فایل جدید به نام customvaildation.js به پوشه اسکریپت‌های برنامه با محتوای زیر اضافه خواهیم کرد:

/// <reference path="jquery-1.5.1-vsdoc.js" />
/// <reference path="jquery.validate-vsdoc.js" />
/// <reference path="jquery.validate.unobtrusive.js" />

jQuery.validator.addMethod("mydatevalidator",
function (value, element, param) {
return Date.parse(value) < new Date();
});

jQuery.validator.unobtrusive.adapters.addBool("mydatevalidator");

توسط referenceهایی که مشاهده می‌کنید، intellisense جی‌کوئری در VS.NET فعال می‌شود.
سپس به کمک متد jQuery.validator.addMethod، همان مقدار ValidationType پیشین را معرفی و در ادامه بر اساس مقدار value دریافتی، تصمیم گیری خواهیم کرد. اگر خروجی false باشد، به معنای شکست اعتبار سنجی است.
همچنین توسط متد jQuery.validator.unobtrusive.adapters.addBool، این متد جدید را به مجموعه وفق دهنده‌ها اضافه می‌کنیم.
و در آخر این فایل جدید باید به View مورد نظر یا فایل master page سیستم اضافه شود:

<script src="@Url.Content("~/Scripts/customvaildation.js")" type="text/javascript"></script>




تغییر رنگ و ظاهر پیغام‌های اعتبار سنجی

اگر از رنگ پیش فرض قرمز پیغام‌های اعتبار سنجی خرسند نیستید، باید اندکی CSS سایت را ویرایش کرد که شامل اعمال تغییرات به موارد ذیل خواهد شد:

1. .field-validation-error
2. .field-validation-valid
3. .input-validation-error
4. .input-validation-valid
5. .validation-summary-errors
6. .validation-summary-valid




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

فرض کنید مدل‌های برنامه شما به کمک یک code generator تولید می‌شوند. در این حالت هرگونه ویژگی اضافی تعریف شده در این کلاس‌ها پس از تولید مجدد کدها از دست خواهند رفت. به همین منظور امکان تعریف مجزای متادیتاها نیز پیش بینی شده است:

[MetadataType(typeof(CustomerMetadata))]
public partial class Customer
{
class CustomerMetadata
{

}
}

public partial class Customer : IValidatableObject
{


حالت کلی روش انجام آن هم به شکلی است که ملاحظه می‌کنید. کلاس اصلی، به صورت partial معرفی خواهد شد. سپس کلاس partial دیگری نیز به همین نام که در برگیرنده یک کلاس داخلی دیگر برای تعاریف متادیتا است، به پروژه اضافه می‌گردد. به کمک ویژگی MetadataType، کلاسی که قرار است ویژگی‌های خواص از آن خوانده شود، معرفی می‌گردد. موارد عنوان شده، شکل کلی این پیاده سازی است. برای نمونه اگر با WCF RIA Services کار کرده باشید، از این روش زیاد استفاده می‌شود. کلاس خصوصی تو در توی تعریف شده صرفا وظیفه ارائه متادیتاهای تعریف شده را به فریم ورک خواهد داشت و هیچ کاربرد دیگری ندارد.
در ادامه کلیه خواص کلاس Customer به همراه متادیتای آن‌ها باید به کلاس CustomerMetadata منتقل شوند. اکنون می‌توان تمام متادیتای کلاس اصلی Customer را حذف کرد.



اعتبار سنجی از راه دور (remote validation)

فرض کنید شخصی مشغول به پر کردن فرم ثبت نام، در سایت شما است. پس از اینکه نام کاربری دلخواه خود را وارد کرد و مثلا به فیلد ورود کلمه عبور رسید، در همین حال و بدون ارسال کل صفحه به سرور، به او پیغام دهیم که نام کاربری وارد شده، هم اکنون توسط شخص دیگری در حال استفاده است. این مکانیزم از ASP.NET MVC3 به بعد تحت عنوان Remote validation در دسترس است و یک درخواست Ajaxایی خودکار را به سرور ارسال خواهد کرد و نتیجه نهایی را به کاربر نمایش می‌دهد؛ کارهایی که به سادگی توسط کدهای جاوا اسکریپتی قابل مدیریت نیستند و نیاز به تعامل با سرور، در این بین وجود دارد. پیاده سازی آن هم به نحو زیر است:
برای مثال خاصیت Name را در مدل برنامه به نحو زیر تغییر دهید:

[Required(ErrorMessage = "Name is required.")]
[StringLength(50)]
[System.Web.Mvc.Remote(action: "CheckUserNameAndEmail",
controller: "Customer",
AdditionalFields = "Email",
HttpMethod = "POST",
ErrorMessage = "Username is not available.")]
public string Name { set; get; }

سپس متد زیر را نیز به کنترلر Customer اضافه کنید:

[HttpPost]
[OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
public ActionResult CheckUserNameAndEmail(string name, string email)
{
if (name.ToLowerInvariant() == "vahid") return Json(false);
if (email.ToLowerInvariant() == "name@site.com") return Json(false);
//...
return Json(true);
}


توضیحات:
توسط ویژگی System.Web.Mvc.Remote، نام کنترلر و متدی که در آن قرار است به صورت خودکار توسط jQuery Ajax فراخوانی شود، مشخص خواهند شد. همچنین اگر نیاز بود فیلدهای دیگری نیز به این متد کنترلر ارسال شوند، می‌توان آن‌ها را توسط خاصیت AdditionalFields، مشخص کرد.
سپس در کدهای کنترلر مشخص شده، متدی با پارامترهای خاصیت مورد نظر و فیلدهای اضافی دیگر، تعریف می‌شود. در اینجا فرصت خواهیم داشت تا برای مثال پس از بررسی بانک اطلاعاتی، خروجی Json ایی را بازگردانیم. return Json false به معنای شکست اعتبار سنجی است.
توسط ویژگی OutputCache، از کش شدن نتیجه درخواست‌های Ajaxایی جلوگیری کرده‌ایم. همچنین نوع درخواست هم جهت امنیت بیشتر، به HttpPost محدود شده است.
تمام کاری که باید انجام شود همین مقدار است و مابقی مسایل مرتبط با اعمال و پیاده سازی آن خودکار است.


استفاده از مکانیزم اعتبار سنجی مبتنی برمتادیتا در خارج از ASP.Net MVC

مباحثی را که در این قسمت ملاحظه نمودید، منحصر به ASP.NET MVC نیستند. برای نمونه توسط متد الحاقی زیر نیز می‌توان یک مدل را مثلا در یک برنامه کنسول هم اعتبار سنجی کرد. بدیهی است در این حالت نیاز خواهد بود تا ارجاعی را به اسمبلی System.ComponentModel.DataAnnotations، به برنامه اضافه کنیم و تمام عملیات هم دستی است و فریم ورک ویژه‌ای هم وجود ندارد تا یک سری از کارها را به صورت خودکار انجام دهد.

using System.ComponentModel.DataAnnotations;

namespace MvcApplication9.Helper
{
public static class ValidationHelper
{
public static bool TryValidateObject(this object instance)
{
return Validator.TryValidateObject(instance, new ValidationContext(instance, null, null), null);
}
}
}



نظرات مطالب
EF Code First #8
سلام در قسمت Self Referencing Entity اگر بخواهیم کلید خارجی نام دیگری داشته باشد طبق گفته شما که عمل کردم خطای Sequence contains no elements را میدهد.
using System.Collections.Generic;
 
namespace EF_Sample04.Models
{
    public class Employee
    {
        public int Id { set; get; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
 
        public int? ManagerID { get; set; }
        public virtual Employee Manager { get; set; }       
    }
}
using EF_Sample04.Models;
using System.Data.Entity.ModelConfiguration; 
namespace EF_Sample04.Mappings
{
    public class EmployeeConfig : EntityTypeConfiguration<Employee>
    {
        public EmployeeConfig()
        {
            this.HasOptional(x => x.Manager)
                .WithMany()
                .HasForeignKey(x => x.ManagerID)
                .WillCascadeOnDelete(false);
        }
    }
}
مطالب
چندین Submit در یک Html Form و انتساب Action های مجزا به هر یک از Submit ها در MVC
تا به حال به این نکته برخورد کردید که برای یک فرم Html نیاز به چندین Submit داشته باشید که هر کدوم یک Action مجزا داشته باشن و یک کار متفاوت انجام بدن ؟
برای مثال فرمی داریم که داده‌های وارد شده در ان باید به دو صورت برای یک کاربر ارسال بشن یا از طریق پیامک یا از طریق ایمیل (این فقط یک مثال پیش فرض هست) و .... در حالت عادی ما در یک فرم نمیتونیم دو عدد Submit  داشته باشیم که هر کدوم به یک Action جدا بسط داده بشه خب راه حل چیه ؟ شاید با خودتون بگید خب دو input از نوع radio قرار میدیم و در یک اکشن کنترل میکنیم که کدوم یکی انتخاب شده و عملیات رو با اون معیار انجام میدیم ... به نظرتون زیباتر نیست برای هر عملیات که ممکن باشه هر کدوم کاملا روال کاری متفاوتی داشته باشه یک Action وجود داشته باشه ؟ در این صورت خوانایی کد خیلی بالاتر میره و Unit Test هر Action کاملا مشخص هست که قراره چه فرایندی رو مورد تست قرار بده و مجبور نیستیم چندین حالت رو با عبارات شرطی از هم جدا کنیم و همه چی قاطی بشه با هم ... من در کل با امکاناتی که C# و MVC در اختیارم قرار میده حاظر نیستم تن به کد نویسی به صورت کلاسیک و قاطی پاتی بدم سعی میکنم با مطالعه‌ی سورس MVC بهترین حالت رو انتخاب کنم شما چطور ؟ معلومه که همه همینو میخوان پس بریم سر اصل مطلب .
قطعه کد Html و Razor ساده‌ی زیر رو در نظر بگیرید برای View :
@model Models.MyModel
@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <title>ViewPage1</title>
</head>
<body>
    <div>
        @using (Html.BeginForm("SendMessage", "Home", FormMethod.Post))
        {

            @Html.LabelFor(x => x.Name); 
            @Html.TextBoxFor(x => x.Name);

            <input type="submit" value="ارسال توسط پیامک" name="Send_sms" />
            <input type="submit" value="ارسال توسط ایمیل" name="Send_email" />
        }
    </div>
</body>
</html>

خب ما دو تا Submit داریم . یکم اگه شیطنت کنید و مقادیر ارسال شده بعد از submit این فرم رو توسط ابزارهای مانیتورینگ بررسی کنید میبینید که روی هر کدوم از Submit‌ها که کلیک میشه داده ای با نام اون که در خاصیت name اون و مقدار موجود در value اون همراه اون فرم به سرور ارسال میشه و اون یکی Submit از این اتفاق بی نصیب میمونه ... خب ما هم استفاده‌ی لازم رو از این موضوع شیرین میبریم و با یک تکنیک تهاجمی از این موضوع برای رسیدن به هدفمون استفاده میکنیم .

این هم کلاس Model ماست :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel;

namespace Models
{
    public class MyModel
    {
        [DisplayName("نام خود را وارد کنید :")]
        public string Name { get; set; }
    }
}
 و اما یک نکته‌ی دیگه . توجه داشته باشید که ما در قسمت View نام Action رو در فرم, SendMessage مشخص کردیم . ولی ... اصلا در واقع همچین اکشنی وجود نداره ! پس چرا ما همچین نامی رو واسه اکشن فرم گذاشتیم !؟
دلیل اینه که ما قصد داریم با یک ActionNameSelectorAttribute درخواست کاربر رو شکار کنیم و اون رو به اکشن دلخواه ارجاع بدیم ... جالبه نه ؟ ولی چه جوری ... کلاس زیر رو بهش دقت مضاعف کنید و در پروژتون ایجادش کنید :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Reflection;

namespace ActionHandlers
{
    public class SendMessageHandlerAttribute : ActionNameSelectorAttribute
    {
        public override bool IsValidName(ControllerContext controllerContext, string actionName, MethodInfo methodInfo)
        {
            if (actionName.Equals(methodInfo.Name, StringComparison.InvariantCultureIgnoreCase))
                return true;

            if (!actionName.Equals("SendMessage", StringComparison.InvariantCultureIgnoreCase))
                return false;

            var request = controllerContext.RequestContext.HttpContext.Request;
            return request[methodInfo.Name] != null;
        }
    }
}
 خب حالا بخش Controller رو بهش دقت کنید که ما در اون دو اکشن رو با نام هایی که برای هر Submit مشخص کردیم مینویسیم و ActionNameSelectorAttribute نوشته شده رو به اونها بسط میدیم. 
    [SendMessageHandler ]
        [HttpPost]
        public ContentResult Send_sms(MyModel mdl)
        {
            /// Do something ...
           return string.Empty ;
        }

        [SendMessageHandler ]
        [HttpPost]
        public ContentResult Send_email(MyModel mdl)
        {
           /// Do something ...
          return  string.Empty; 
        }
 خب حالا بعد از کلیک بر روی هر Submit اکشن متناظر با اون اجرا میشه . بعد از ارسال درخواست به سرور MVC در بین اکشن‌های موجود در Controller مشخص شده به دنبال اکشن معین شده میگرده و وقتی به اکشن‌های ما میرسه میبینه عجب ! اون دوتا ActionNameSelectorAttribute سفارشی دارن پس میره ببینه چه خبره اونجا که ما با یک حرکت تهاجمی بررسی میکنیم که اگه  نام اکشن مشخص شده در فرم با نام اکشن در حال بررسی مساوی بود که همینو اجرا کن ( یعنی ما میتونی اکشنی با نام SendMessage هم داشته باشیم ) . اگه نام اکشن مشخص شده در فرم اون نامی نبود که ما میخوایم که کلا بیخیال هندل کردن اکشن میشیم میزاریم خود MVC تصمیم بگیره . و در اخر بررسی میکنیم که ایا در درخواست جاری مقداری با نام اکشن در حال بررسی وجود داره !؟ اگه داشت یعنی همون Submit که ما میخوایم وصل بشه به این اکشن کلیک شده پس اکشن در حال بررسی رو بسط میدیم به درخواست ارسال شده ... به همین سادگی ...

پیروز و موفق باشید . 
مطالب
کار با Kendo UI DataSource
Kendo UI DataSource جهت تامین داده‌های سمت کلاینت ویجت‌های مختلف KendoUI طراحی شده‌است و به عنوان یک اینترفیس استاندارد قابل استفاده توسط تمام کنترل‌های داده‌ای Kendo UI کاربرد دارد. Kendo UI DataSource امکان کار با منابع داده محلی، مانند اشیاء و آرایه‌های جاوا اسکریپتی و همچنین منابع تامین شده از راه دور، مانند JSON، JSONP و XML را دارد. به علاوه توسط آن می‌توان اعمال ثبت، ویرایش و حذف اطلاعات، به همراه صفحه بندی، گروه بندی و مرتب سازی داده‌ها را کنترل کرد.


استفاده از منابع داده محلی
در ادامه مثالی را از نحوه‌ی استفاده از یک منبع داده محلی جاوا اسکریپتی، مشاهده می‌کنید:
    <script type="text/javascript">
        $(function () {
            var cars = [
                { "Year": 2000, "Make": "Hyundai", "Model": "Elantra" },
                { "Year": 2001, "Make": "Hyundai", "Model": "Sonata" },
                { "Year": 2002, "Make": "Toyota", "Model": "Corolla" },
                { "Year": 2003, "Make": "Toyota", "Model": "Yaris" },
                { "Year": 2004, "Make": "Honda", "Model": "CRV" },
                { "Year": 2005, "Make": "Honda", "Model": "Accord" },
                { "Year": 2000, "Make": "Honda", "Model": "Accord" },
                { "Year": 2002, "Make": "Kia", "Model": "Sedona" },
                { "Year": 2004, "Make": "Fiat", "Model": "One" },
                { "Year": 2005, "Make": "BMW", "Model": "M3" },
                { "Year": 2008, "Make": "BMW", "Model": "X5" }
            ];

            var carsDataSource = new kendo.data.DataSource({
                data: cars
            });

            carsDataSource.read();
            alert(carsDataSource.total());
        });
    </script>
در اینجا cars آرایه‌ای از اشیاء جاوا اسکریپتی بیانگر ساختار یک خودرو است. سپس برای معرفی آن به Kendo UI، کار با مقدار دهی خاصیت data مربوط به new kendo.data.DataSource شروع می‌شود.
ذکر new kendo.data.DataSource به تنهایی به معنای مقدار دهی اولیه است و در این حالت منبع داده مورد نظر، استفاده نخواهد شد. برای مثال اگر متد total آن‌را جهت یافتن تعداد عناصر موجود در آن فراخوانی کنید، صفر را بازگشت می‌دهد. برای شروع به کار با آن، نیاز است ابتدا متد read را بر روی این منبع داده مقدار دهی شده، فراخوانی کرد.


استفاده از منابع داده راه دور

در برنامه‌های کاربردی، عموما نیاز است تا منبع داده را از یک وب سرور تامین کرد. در اینجا نحوه‌ی خواندن اطلاعات JSON بازگشت داده شده از جستجوی توئیتر را مشاهده می‌کنید:
    <script type="text/javascript">
        $(function () {
            var twitterDataSource = new kendo.data.DataSource({
                transport: {
                    read: {
                        url: "http://search.twitter.com/search.json",
                        dataType: "jsonp",
                        contentType: 'application/json; charset=utf-8',
                        type: 'GET',
                        data: { q: "#kendoui" }
                    },
                    schema: { data: "results" }
                },
                error: function (e) {
                    alert(e.errorThrown.stack);
                }
            });
        });
    </script>
در قسمت transport، جزئیات تبادل اطلاعات با سرور راه دور مشخص می‌شود؛ برای مثال url ارائه دهنده‌ی سرویس، dataType بیانگر نوع داده مورد انتظار و data کار مقدار دهی پارامتر مورد انتظار توسط سرویس توئیتر را انجام می‌دهد. در اینجا چون صرفا عملیات خواندن اطلاعات صورت می‌گیرد، خاصیت read مقدار دهی شده‌است.
در قسمت schema مشخص می‌کنیم که اطلاعات JSON بازگشت داده شده توسط توئیتر، در فیلد results آن قرار دارد.


کار با منابع داده OData

علاوه بر فرمت‌های یاد شده، Kendo UI DataSource امکان کار با اطلاعاتی از نوع OData را نیز دارا است که تنظیمات ابتدایی آن به صورت ذیل است:
    <script type="text/javascript">
            var moviesDataSource = new kendo.data.DataSource({
                type: "odata",
                transport: {
                    read: "http://demos.kendoui.com/service/Northwind.svc/Orders"
                },
                error: function (e) {
                    alert(e.errorThrown.stack);
                }
            });
        });
    </script>
همانطور که ملاحظه می‌کنید، تنظیمات ابتدایی آن اندکی با حالت remote data پیشین متفاوت است. در اینجا ابتدا نوع داده‌ی بازگشتی مشخص می‌شود و در قسمت transport، خاصیت read آن، آدرس سرویس را دریافت می‌کند.


یک مثال: دریافت اطلاعات از ASP.NET Web API

یک پروژه‌ی جدید ASP.NET را آغاز کنید. تفاوتی نمی‌کند که Web forms باشد یا MVC؛ از این جهت که مباحث Web API در هر دو یکسان است.
سپس یک کنترلر جدید Web API را به نام ProductsController با محتوای زیر ایجاد کنید:
using System.Collections.Generic;
using System.Web.Http;

namespace KendoUI02
{
    public class Product
    {
        public int Id { set; get; }
        public string Name { set; get; }
    }

    public class ProductsController : ApiController
    {
        public IEnumerable<Product> Get()
        {
            var products = new List<Product>();
            for (var i = 1; i <= 100; i++)
            {
                products.Add(new Product { Id = i, Name = "Product " + i });
            }
            return products;
        }
    }
}
در این مثال، هدف صرفا ارائه یک خروجی ساده JSON از طرف سرور است.
در ادامه نیاز است تعریف مسیریابی ذیل نیز به فایل Global.asax.cs برنامه اضافه شود تا بتوان به آدرس api/products در سایت، دسترسی یافت:
using System;
using System.Web.Http;
using System.Web.Routing;

namespace KendoUI02
{
    public class Global : System.Web.HttpApplication
    {
        protected void Application_Start(object sender, EventArgs e)
        {
            RouteTable.Routes.MapHttpRoute(
               name: "DefaultApi",
               routeTemplate: "api/{controller}/{id}",
               defaults: new { id = RouteParameter.Optional }
               );
        }
    }
}

در ادامه فایلی را به نام Index.html (یا در یک View و یا یک فایل aspx دلخواه)، محتوای ذیل را اضافه کنید:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title>Kendo UI: Implemeting the Grid</title>

    <link href="styles/kendo.common.min.css" rel="stylesheet" type="text/css" />
    <link href="styles/kendo.default.min.css" rel="stylesheet" type="text/css" />
    <script src="js/jquery.min.js" type="text/javascript"></script>
    <script src="js/kendo.all.min.js" type="text/javascript"></script>
</head>
<body>

    <div id="report-grid"></div>
    <script type="text/javascript">
        $(function () {
            var productsDataSource = new kendo.data.DataSource({
                transport: {
                    read: {
                        url: "api/products",
                        dataType: "json",
                        contentType: 'application/json; charset=utf-8',
                        type: 'GET'
                    }
                },
                error: function (e) {
                    alert(e.errorThrown.stack);
                },
                pageSize: 5,
                sort: { field: "Id", dir: "desc" }
            });

            $("#report-grid").kendoGrid({
                dataSource: productsDataSource,
                autoBind: true,
                scrollable: false,
                pageable: true,
                sortable: true,
                columns: [
                    { field: "Id", title: "#" },
                    { field: "Name", title: "Product" }
                ]
            });
        });
    </script>
</body>
</html>
- ابتدا فایل‌های اسکریپت و CSS مورد نیاز Kendo UI اضافه شده‌اند.
- گرید صفحه، در محل div ایی با id مساوی report-grid تشکیل خواهد شد.
- سپس DataSource ایی که به آدرس api/products اشاره می‌کند، تعریف شده و در آخر productsDataSource را توسط یک kendoGrid نمایش داده‌ایم.
- نحوه‌ی تعریف productsDataSource، در قسمت استفاده از منابع داده راه دور ابتدای بحث توضیح داده شد. در اینجا فقط دو خاصیت pageSize و sort نیز به آن اضافه شده‌اند. این دو خاصیت بر روی نحوه‌ی نمایش گرید نهایی تاثیر گذار هستند. pageSize تعداد رکورد هر صفحه را مشخص می‌کند و sort نحوه‌ی مرتب سازی را بر اساس فیلد Id و در حالت نزولی قرار می‌دهد.
- در ادامه، ابتدایی‌ترین حالت کار با kendoGrid را ملاحظه می‌کنید.
- تنظیم dataSource و autoBind: true (حالت پیش فرض)، سبب خواهند شد تا به صورت خودکار، اطلاعات JSON از مسیر api/products خوانده شوند.
- سه خاصیت بعدی صفحه بندی و مرتب سازی خودکار ستون‌ها را فعال می‌کنند.
- در آخر هم دو ستون گرید، بر اساس نام‌های خواص کلاس Product تعریف شده‌اند.


سورس کامل این قسمت را از اینجا می‌توانید دریافت کنید:
KendoUI02.zip
 
مطالب
تولید هدرهای Content Security Policy توسط ASP.NET Core برای برنامه‌های Angular
پیشتر مطلب «افزودن هدرهای Content Security Policy به برنامه‌های ASP.NET» را در این سایت مطالعه کرده‌اید. در اینجا قصد داریم معادل آن‌را برای ASP.NET Core تهیه کرده و همچنین نکات مرتبط با برنامه‌های Angular را نیز در آن لحاظ کنیم.


تهیه میان افزار افزودن هدرهای Content Security Policy

کدهای کامل این میان افزار را در ادامه مشاهده می‌کنید:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;

namespace AngularTemplateDrivenFormsLab.Utils
{
    public class ContentSecurityPolicyMiddleware
    {
        private readonly RequestDelegate _next;

        public ContentSecurityPolicyMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public Task Invoke(HttpContext context)
        {
            context.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN");
            context.Response.Headers.Add("X-Xss-Protection", "1; mode=block");
            context.Response.Headers.Add("X-Content-Type-Options", "nosniff");

            string[] csp =
            {
              "default-src 'self'",
              "style-src 'self' 'unsafe-inline'",
              "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
              "font-src 'self'",
              "img-src 'self' data:",
              "connect-src 'self'",
              "media-src 'self'",
              "object-src 'self'",
              "report-uri /api/CspReport/Log" //TODO: Add api/CspReport/Log
            };
            context.Response.Headers.Add("Content-Security-Policy", string.Join("; ", csp));
            return _next(context);
        }
    }

    public static class ContentSecurityPolicyMiddlewareExtensions
    {
        /// <summary>
        /// Make sure you add this code BEFORE app.UseStaticFiles();,
        /// otherwise the headers will not be applied to your static files.
        /// </summary>
        public static IApplicationBuilder UseContentSecurityPolicy(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<ContentSecurityPolicyMiddleware>();
        }
    }
}
که نحوه‌ی استفاده از آن در کلاس آغازین برنامه به صورت ذیل خواهد بود:
public void Configure(IApplicationBuilder app)
{
   app.UseContentSecurityPolicy();

توضیحات تکمیلی

افزودن X-Frame-Options
 context.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN");
از هدر X-FRAME-OPTIONS، جهت منع نمایش و رندر سایت جاری، در iframeهای سایت‌های دیگر استفاده می‌شود. ذکر مقدار SAMEORIGIN آن، به معنای مجاز تلقی کردن دومین جاری برنامه است.


افزودن X-Xss-Protection
 context.Response.Headers.Add("X-Xss-Protection", "1; mode=block");
تقریبا تمام مرورگرهای امروزی قابلیت تشخیص حملات XSS را توسط static analysis توکار خود دارند. این هدر، آنالیز اجباری XSS را فعال کرده و همچنین تنظیم حالت آن به block، نمایش و رندر قسمت مشکل‌دار را به طور کامل غیرفعال می‌کند.


افزودن X-Content-Type-Options
 context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
وجود این هدر سبب می‌شود تا مرورگر، حدس‌زدن نوع فایل‌ها، درخواست‌ها و محتوا را کنار گذاشته و صرفا به content-type ارسالی توسط سرور اکتفا کند. به این ترتیب برای مثال امکان لینک کردن یک فایل غیرجاوا اسکریپتی و اجرای آن به صورت کدهای جاوا اسکریپت، چون توسط تگ script ذکر شده‌است، غیرفعال می‌شود. در غیراینصورت مرورگر هرچیزی را که توسط تگ script به صفحه لینک شده باشد، صرف نظر از content-type واقعی آن، اجرا خواهد کرد.


افزودن Content-Security-Policy
string[] csp =
            {
              "default-src 'self'",
              "style-src 'self' 'unsafe-inline'",
              "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
              "font-src 'self'",
              "img-src 'self' data:",
              "connect-src 'self'",
              "media-src 'self'",
              "object-src 'self'",
              "report-uri /api/CspReport/Log" //TODO: Add api/CspReport/Log
            };
context.Response.Headers.Add("Content-Security-Policy", string.Join("; ", csp));
وجود این هدر، تزریق کدها و منابع را از دومین‌های دیگر غیرممکن می‌کند. برای مثال ذکر self در اینجا به معنای مجاز بودن الصاق و اجرای اسکریپت‌ها، شیوه‌نامه‌ها، تصاویر و اشیاء، صرفا از طریق دومین جاری برنامه است و هرگونه منبعی که از دومین‌های دیگر به برنامه تزریق شود، قابلیت اجرایی و یا نمایشی نخواهد داشت.

در اینجا ذکر unsafe-inline و unsafe-eval را مشاهده می‌کنید. برنامه‌های Angular به همراه شیوه‌نامه‌های inline و یا بکارگیری متد eval در مواردی خاص هستند. اگر این دو گزینه ذکر و فعال نشوند، در کنسول developer مرورگر، خطای بلاک شدن آن‌ها را مشاهده کرده و همچنین برنامه از کار خواهد افتاد.

یک نکته: با فعالسازی گزینه‌ی aot-- در حین ساخت برنامه، می‌توان unsafe-eval را نیز حذف کرد.


استفاده از فایل web.config برای تعریف SameSite Cookies

یکی از پیشنهادهای اخیر ارائه شده‌ی جهت مقابله‌ی با حملات CSRF و XSRF، قابلیتی است به نام  Same-Site Cookies. به این ترتیب مرورگر، کوکی سایت جاری را به همراه یک درخواست ارسال آن به سایت دیگر، پیوست نمی‌کند (کاری که هم اکنون با درخواست‌های Cross-Site صورت می‌گیرد). برای رفع این مشکل، با این پیشنهاد امنیتی جدید، تنها کافی است SameSite، به انتهای کوکی اضافه شود:
 Set-Cookie: sess=abc123; path=/; SameSite

نگارش‌های بعدی ASP.NET Core، ویژگی SameSite را نیز به عنوان CookieOptions لحاظ کرده‌اند. همچنین یک سری از کوکی‌های خودکار تولیدی توسط آن مانند کوکی‌های anti-forgery به صورت خودکار با این ویژگی تولید می‌شوند.
اما مدیریت این مورد برای اعمال سراسری آن، با کدنویسی میسر نیست (مگر اینکه مانند نگارش‌های بعدی ASP.NET Core پشتیبانی توکاری از آن صورت گیرد). به همین جهت می‌توان از ماژول URL rewrite مربوط به IIS برای افزودن ویژگی SameSite به تمام کوکی‌های تولید شده‌ی توسط سایت، کمک گرفت. برای این منظور تنها کافی است فایل web.config را ویرایش کرده و موارد ذیل را به آن اضافه کنید:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <rewrite>
      <outboundRules>
        <clear />
        <!-- https://scotthelme.co.uk/csrf-is-dead/ -->
        <rule name="Add SameSite" preCondition="No SameSite">
          <match serverVariable="RESPONSE_Set_Cookie" pattern=".*" negate="false" />
          <action type="Rewrite" value="{R:0}; SameSite=lax" />
          <conditions></conditions>
        </rule>
        <preConditions>
          <preCondition name="No SameSite">
            <add input="{RESPONSE_Set_Cookie}" pattern="." />
            <add input="{RESPONSE_Set_Cookie}" pattern="; SameSite=lax" negate="true" />
          </preCondition>
        </preConditions>
      </outboundRules>
    </rewrite>
  </system.webServer>
</configuration>


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

اگر به هدر Content-Security-Policy دقت کنید، گزینه‌ی آخر آن، ذکر اکشن متدی در سمت سرور است:
   "report-uri /api/CspReport/Log" //TODO: Add api/CspReport/Log
با تنظیم این مورد، می‌توان موارد بلاک شده را در سمت سرور لاگ کرد. اما این اطلاعات ارسالی به سمت سرور، فرمت خاصی را دارند:
{
  "csp-report": {
    "document-uri": "http://localhost:5000/untypedSha",
    "referrer": "",
    "violated-directive": "script-src",
    "effective-directive": "script-src",
    "original-policy": "default-src 'self'; style-src 'self'; script-src 'self'; font-src 'self'; img-src 'self' data:; connect-src 'self'; media-src 'self'; object-src 'self'; report-uri /api/Home/CspReport",
    "disposition": "enforce",
    "blocked-uri": "eval",
    "line-number": 21,
    "column-number": 8,
    "source-file": "http://localhost:5000/scripts.bundle.js",
    "status-code": 200,
    "script-sample": ""
  }
}
به همین جهت ابتدا نیاز است توسط JsonProperty کتابخانه‌ی JSON.NET، معادل این خواص را تولید کرد:
    class CspPost
    {
        [JsonProperty("csp-report")]
        public CspReport CspReport { get; set; }
    }

    class CspReport
    {
        [JsonProperty("document-uri")]
        public string DocumentUri { get; set; }

        [JsonProperty("referrer")]
        public string Referrer { get; set; }

        [JsonProperty("violated-directive")]
        public string ViolatedDirective { get; set; }

        [JsonProperty("effective-directive")]
        public string EffectiveDirective { get; set; }

        [JsonProperty("original-policy")]
        public string OriginalPolicy { get; set; }

        [JsonProperty("disposition")]
        public string Disposition { get; set; }

        [JsonProperty("blocked-uri")]
        public string BlockedUri { get; set; }

        [JsonProperty("line-number")]
        public int LineNumber { get; set; }

        [JsonProperty("column-number")]
        public int ColumnNumber { get; set; }

        [JsonProperty("source-file")]
        public string SourceFile { get; set; }

        [JsonProperty("status-code")]
        public string StatusCode { get; set; }

        [JsonProperty("script-sample")]
        public string ScriptSample { get; set; }
    }
اکنون می‌توان بدنه‌ی درخواست را استخراج و سپس به این شیء ویژه نگاشت کرد:
namespace AngularTemplateDrivenFormsLab.Controllers
{
    [Route("api/[controller]")]
    public class CspReportController : Controller
    {
        [HttpPost("[action]")]
        [IgnoreAntiforgeryToken]
        public async Task<IActionResult> Log()
        {
            CspPost cspPost;
            using (var bodyReader = new StreamReader(this.HttpContext.Request.Body))
            {
                var body = await bodyReader.ReadToEndAsync().ConfigureAwait(false);
                this.HttpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body));
                cspPost = JsonConvert.DeserializeObject<CspPost>(body);
            }

            //TODO: log cspPost

            return Ok();
        }
    }
}
در اینجا نحوه‌ی استخراج Request.Body را به صورت خام را مشاهده می‌کنید. سپس توسط متد DeserializeObject کتابخانه‌ی JSON.NET، این رشته به شیء CspPost نگاشت شده‌است.


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