نظرات مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایل‌های config
یک نکته: طراحی کانفیگ برای یک کتابخانه
فرض کنید قصد دارید برای کتابخانه‌ای که در حال طراحی هستید، تنظیماتی را از استفاده کننده دریافت کنید؛ مثلا از طریق کلاسی شبیه به تعریف SmtpConfig مطلب فوق.
برای اینکار، ابتدا یک متد الحاقی را تعریف می‌کنیم که IServiceCollection را به همراه SmtpConfig دریافت کند:
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
 
namespace SampleLib
{
  public static class MyServiceCollectionExtensions
  {
   public static void AddMySampleLib(this IServiceCollection services, SmtpConfig configureOptions)
   {
    services.TryAddSingleton(Options.Create(configureOptions));
   }
  }
}
سپس کار معرفی این کلاس تنظیمات به سیستم IOptions ، از طریق ثبت سرویس آن توسط متد Options.Create صورت می‌گیرد.
استفاده کنند، تنها نیاز دارد تا این تنظیمات را به صورت ذیل در کلاس آغازین برنامه وارد کند:
public void ConfigureServices(IServiceCollection services)
{
   services.AddMySampleLib(new SmtpConfig { /*...*/ });
و در آخر در کدهای کتابخانه‌ی در حال طراحی، با تزریق <IOptions<SmtpConfig به سازنده‌ی کلاس‌ها، می‌توان به تنظیمات ثبت شده، دسترسی پیدا کرد.
مطالب
تنظیمات کش توزیع شده‌ی مبتنی بر SQL Server در ASP.NET Core
ASP.NET Core به همراه زیر ساختی‌است جهت خارج کردن امکانات Caching درون حافظه‌ای آن از سرور جاری و انتقال آن به سرورهای دیگر جهت کاهش بار سرور و برنامه. این کش توزیع شده را می‌توان به عنوان زیرساختی برای مدیریت سشن‌ها، مدیریت اطلاعات کش و همچنین مدیریت کوکی‌های حجیم ASP.NET Core Identity نیز بکار گرفت. برای مثال بجای ارسال یک کوکی حجیم بالای 5 کیلوبایت به کلاینت، فقط ID رمزنگاری شده‌ی آن‌را ارسال کرد و اصل کوکی را در داخل دیتابیس ذخیره و بازیابی نمود. این مساله هم مقیاس پذیری برنامه را افزایش خواهد داد و هم امنیت آن‌را با عدم ارسال اصل محتوای کوکی به سمت کلاینت‌ها و یا ذخیره سازی اطلاعات سشن‌ها در بانک اطلاعاتی، مشکلات راه اندازی مجدد برنامه را به طور کامل برطرف می‌کنند و در این حالت بازیابی Application pool و یا کرش برنامه و یا ری استارت شدن کل سرور، سبب از بین رفتن سشن‌های کاربران نخواهند شد. بنابراین آشنایی با نحوه‌ی راه اندازی این امکانات، حداقل از بعد امنیتی بسیار مفید هستند؛ حتی اگر سرور ذخیره کننده‌ی این اطلاعات، همان سرور و بانک اطلاعاتی اصلی برنامه باشند.


پیشنیازهای کار با کش توزیع شده‌ی مبتنی بر SQL Server

برای کار با کش توزیع شده‌ی با قابلیت ذخیره سازی در یک بانک اطلاعاتی SQL Server، نیاز است دو بسته‌ی ذیل را به فایل project.json برنامه اضافه کرد:
{
    "dependencies": {
        "Microsoft.Extensions.Caching.SqlServer": "1.1.0"
    },
    "tools": {
        "Microsoft.Extensions.Caching.SqlConfig.Tools": "1.1.0-preview4-final"
    }
}
وابستگی که در قسمت dependencies ذکر شده‌است، کلاس‌های اصلی کار با کش توزیع شده را به برنامه اضافه می‌کند. ذکر وابستگی قسمت tools، اختیاری است و کار آن، ایجاد جدول مورد نیاز برای ذخیره سازی اطلاعات، در یک بانک اطلاعاتی SQL Server می‌باشد.


ایجاد جدول ذخیره سازی اطلاعات کش توزیع شده به کمک ابزار sql-cache

پس از افزودن و بازیابی ارجاعات فوق، با استفاده از خط فرمان، به پوشه‌ی جاری برنامه وارد شده و دستور ذیل را صادر کنید:
 dotnet sql-cache create "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=sql_cache;Integrated Security=True;" "dbo" "AppSqlCache"
توضیحات:
- در اینجا می‌توان هر نوع رشته‌ی اتصالی معتبری را به انواع و اقسام بانک‌های SQL Server ذکر کرد. برای نمونه در مثال فوق این رشته‌ی اتصالی به یک بانک اطلاعاتی از پیش ایجاد شده‌ی LocalDB اشاره می‌کند. نام دلخواه این بانک اطلاعاتی در اینجا sql_cache ذکر گردیده و نام دلخواه جدولی که قرار است این اطلاعات را ثبت کند AppSqlCache تنظیم شده‌است و dbo، نام اسکیمای جدول است:


در اینجا تصویر ساختار جدولی را که توسط ابزار dotnet sql-cache ایجاد شده‌است، مشاهده می‌کنید. اگر خواستید این جدول را خودتان دستی ایجاد کنید، یک چنین کوئری را باید بر روی دیتابیس مدنظرتان اجرا نمائید:
CREATE TABLE AppSqlCache (
    Id                         NVARCHAR (449)  COLLATE SQL_Latin1_General_CP1_CS_AS NOT NULL,
    Value                      VARBINARY (MAX) NOT NULL,
    ExpiresAtTime              DATETIMEOFFSET  NOT NULL,
    SlidingExpirationInSeconds BIGINT          NULL,
    AbsoluteExpiration         DATETIMEOFFSET  NULL,
    CONSTRAINT pk_Id PRIMARY KEY (Id)
);

CREATE NONCLUSTERED INDEX Index_ExpiresAtTime
    ON AppSqlCache(ExpiresAtTime);


ایجاد جدول ذخیره سازی اطلاعات کش توزیع شده به کمک ابزار Migrations در EF Core

زیر ساخت کش توزیع شده‌ی مبتنی بر SQL Server هیچگونه وابستگی به EF Core ندارد و تمام اجزای آن توسط Async ADO.NET نوشته شده‌اند. اما اگر خواستید قسمت ایجاد جدول مورد نیاز آن‌را به ابزار Migrations در EF Core واگذار کنید، روش کار به صورت زیر است:
- ابتدا یک کلاس دلخواه جدید را با محتوای ذیل ایجاد کنید:
    public class AppSqlCache
    {
        public string Id { get; set; }
        public byte[] Value { get; set; }
        public DateTimeOffset ExpiresAtTime { get; set; }
        public long? SlidingExpirationInSeconds { get; set; }
        public DateTimeOffset? AbsoluteExpiration { get; set; }
    }
- سپس تنظیمات ایجاد جدول متناظر با آن را به نحو ذیل تنظیم نمائید:
    public class MyDBDataContext : DbContext
    {
        public virtual DbSet<AppSqlCache> AppSqlCache { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<AppSqlCache>(entity =>
            {
                entity.ToTable(name: "AppSqlCache", schema: "dbo");
                entity.HasIndex(e => e.ExpiresAtTime).HasName("Index_ExpiresAtTime");
                entity.Property(e => e.Id).HasMaxLength(449);
                entity.Property(e => e.Value).IsRequired();
            });
        }
    }
به این ترتیب این جدول جدید به صورت خودکار در کنار سایر جداول برنامه ایجاد خواهند شد.
البته این مورد به شرطی است که بخواهید از یک دیتابیس، هم برای برنامه و هم برای ذخیره سازی اطلاعات کش استفاده کنید.


معرفی تنظیمات رشته‌ی اتصالی و نام جدول ذخیره سازی اطلاعات کش به برنامه

پس از ایجاد جدول مورد نیاز جهت ذخیره سازی اطلاعات کش، اکنون نیاز است این اطلاعات را به برنامه معرفی کرد. برای این منظور به کلاس آغازین برنامه مراجعه کرده و متد الحاقی AddDistributedSqlServerCache را بر روی مجموعه‌ی سرویس‌های موجود فراخوانی کنید؛ تا سرویس‌های این کش توزیع شده نیز به برنامه معرفی شوند:
public void ConfigureServices(IServiceCollection services)
{
    services.AddDistributedSqlServerCache(options =>
    {
        options.ConnectionString = @"Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=sql_cache;Integrated Security=True;";
        options.SchemaName = "dbo";
        options.TableName = "AppSqlCache";
    });
باتوجه به توزیع شده بودن این کش، هیچ الزامی ندارد که ConnectionString ذکر شده‌ی در اینجا با رشته‌ی اتصالی مورد استفاده‌ی توسط EF Core یکی باشد (هرچند مشکلی هم ندارد).


آزمایش کش توزیع شده‌ی تنظیمی با فعال سازی سشن‌ها

سشن‌ها را همانند نکات ذکر شده‌ی در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 16 - کار با Sessions» فعال کنید و سپس مقداری را در آن بنویسید:
public IActionResult Index()
{
   HttpContext.Session.SetString("User", "VahidN");
   return Json(true);
}

public IActionResult About()
{
   var userContent = HttpContext.Session.GetString("User");
   return Json(userContent);
}
اکنون از جدول AppSqlCache کوئری بگیرید:


همانطور که مشاهده می‌کنید، سیستم سشن اینبار بجای حافظه، به صورت خودکار از جدول بانک اطلاعاتی SQL Server تنظیم شده‌، برای ذخیره سازی اطلاعات خود استفاده کرده‌است.


کار با کش توزیع شده از طریق برنامه نویسی

همانطور که در مقدمه‌ی بحث نیز عنوان شد، استفاده‌ی از زیر ساخت کش توزیع شده منحصر به استفاده‌ی از آن جهت ذخیره سازی اطلاعات سشن‌ها نیست و از آن می‌توان جهت انواع و اقسام سناریوهای مختلف مورد نیاز استفاده کرد. در این حالت روش دسترسی به این زیر ساخت، از طریق اینترفیس IDistributedCache است. زمانیکه متد AddDistributedSqlServerCache را فراخوانی می‌کنیم، در حقیقت کار ثبت یک چنین سرویسی به صورت خودکار انجام خواهد شد:
 services.Add(ServiceDescriptor.Singleton<IDistributedCache, SqlServerCache>());
به عبارتی کلاس SqlServerCache به صورت singleton به مجموعه‌ی سرویس‌های برنامه اضافه شده‌است و برای دسترسی به آن تنها کافی است اینترفیس IDistributedCache را به کنترلرها و یا سرویس‌های برنامه تزریق و از امکانات آن استفاده کنیم.

در اینجا یک نمونه از این تزریق وابستگی و سپس استفاده‌ی از متدهای Set و Get اینترفیس IDistributedCache را مشاهده می‌کنید:
using System;
using System.Text;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Distributed;
 
namespace Core1RtmEmptyTest.Controllers
{
    public class CacheTestController : Controller
    {
        readonly IDistributedCache _cache;
        public CacheTestController(IDistributedCache cache)
        {
            _cache = cache;
        }
 
        public IActionResult SetCacheData()
        {
            var time = DateTime.Now.ToLocalTime().ToString();
            var cacheOptions = new DistributedCacheEntryOptions
            {
                AbsoluteExpiration = DateTime.Now.AddYears(1)
 
            };
            _cache.Set("Time", Encoding.UTF8.GetBytes(time), cacheOptions);
            return View();
        }
 
        public IActionResult GetCacheData()
        {
            var time = Encoding.UTF8.GetString(_cache.Get("Time"));
            ViewBag.data = time;
            return View();
        }
 
        public bool RemoveCacheData()
        {
            _cache.Remove("Time");
            return true;
        }
    }
}
در ابتدای بحث که ساختار جدول ذخیره سازی اطلاعات کش را بررسی کردیم، فیلد value آن یک چنین نوعی را دارد:
  Value  VARBINARY (MAX) NOT NULL,
که در سمت کدهای دات نتی، به شکل آرایه‌ای از بایت‌ها قابل بیان است.
  public byte[] Value { get; set; }
به همین جهت متد Set آن مقدار مدنظر را به صورت آرایه‌ای از بایت‌ها قبول می‌کند.
در این حالت اگر برنامه را اجرا و مسیر http://localhost:7742/CacheTest/SetCacheData را فراخوانی کنیم، اطلاعات ذخیره شده‌ی با کلید Test را می‌توان در بانک اطلاعاتی مشاهده کرد:



Tag helper مخصوص کش توزیع شده

در ASP.NET Core، می‌توان از یک Tag Helper جدید به نام distributed-cache برای کش سمت سرور توزیع شده‌ی محتوای قسمتی از یک View به نحو ذیل استفاده کرد:
<distributed-cache name="MyCacheItem2" expires-sliding="TimeSpan.FromMinutes(30)">
    <p>From distributed-cache</p>
    @DateTime.Now.ToString()
</distributed-cache>
که اطلاعات آن در بانک اطلاعاتی به نحو ذیل ذخیره می‌شود:


در اینجا name به صورت هش شده به صورت کلید کش مورد استفاده قرار می‌گیرد. سپس محتوای تگ distributed-cache رندر شده، تبدیل به آرایه‌ای از بایت‌ها گردیده و در بانک اطلاعاتی ذخیره می‌گردد.
ذکر name در اینجا اجباری است و باید دقت داشت که چون به عنوان کلید بازیابی کش مورد استفاده قرار خواهد گرفت، نباید به اشتباه در قسمت‌های دیگر برنامه با همین نام وارد شود. در غیر اینصورت دو قسمتی که name یکسانی داشته باشند، یک محتوا را نمایش خواهند داد.
مطالب
استثنای Sequence contains no elements در حین استفاده از LINQ

در ابتدا مثال‌های زیر را در نظر بگیرید:

using System;
using System.Collections.Generic;
using System.Linq;

namespace testWinForms87
{
public class Data
{
public int id { get; set; }
public string name { get; set; }
}

class CLinqTests
{
public static int TestGetListMin1()
{
var lst = new List<Data>
{
new Data{ id=1, name="id1"},
new Data{ id=2, name="id2"},
new Data{ id=3, name="name3"}
};

return (from c in lst
where c.name.Contains("id")
select c.id).Min();
}

public static int TestGetListMin2()
{
var lst = new List<Data>();

return (from c in lst
where c.name.Contains("id")
select c.id).Min();
}
}
}
در متد TestGetListMin1 قصد داریم کوچکترین آی دی رکوردهایی را که نام آن‌ها حاوی id است، از لیست تشکیل شده از کلاس Data بدست آوریم (همانطور که مشخص است سه رکورد از نوع Data در لیست lst ما قرار گرفته‌اند).
محاسبات آن کار می‌کند و مشکلی هم ندارد. اما همیشه در دنیای واقعی همه چیز قرار نیست به این خوبی پیش برود. ممکن است همانند متد TestGetListMin2 ، لیست ما خالی باشد (برای مثال از دیتابیس، رکوردی مطابق شرایط کوئری‌های قبلی بازگشت داده نشده باشد). در این حالت هنگام فراخوانی متد Min ، استثنای Sequence contains no elements رخ خواهد داد و همانطور که در مباحث defensive programming عنوان شد، وظیفه‌ی ما این نیست که خودرو را به دیوار کوبیده (یا منتظر شویم تا کوبیده شود) و سپس به فکر چاره بیفتیم که خوب، عجب! مشکلی رخ داده است!
اکنون چه باید کرد؟ حداقل یک مرحله بررسی اینکه آیا کوئری ما حاوی رکوردی می‌باشد یا خیر باید به این متد اضافه شود (به صورت زیر):

public static int TestGetListMin3()
{
var lst = new List<Data>();
var query = from c in lst
where c.name.Contains("id")
select c.id;

if (query.Any())
return query.Min();
else
return -1;
}
البته می‌شد اگر هیچ رکوردی بازگشت داده نمی‌شد، یک استثنای سفارشی را ایجاد کرد، اما به شخصه ترجیح می‌دهم عدد منهای یک را بر گردانم (چون می‌دانم رکوردهای من عدد مثبت هستند و اگر حاصل منفی شد نیازی به ادامه‌ی پروسه نیست).

شبیه به این مورد در هنگام استفاده از تابع Single مربوط به LINQ نیز ممکن است رخ دهد (تولید استثنای ذکر شده) اما در اینجا مایکروسافت تابع SingleOrDefault را نیز پیش بینی کرده است. در این حالت اگر کوئری ما رکوردی را برنگرداند، SingleOrDefault مقدار نال را برگشت داده و استثنایی رخ نخواهد داد (نمونه‌ی دیگر آن متدهای First و FirstOrDefault هستند).
در مورد متدهای Min و Max ، متدهای MinOrDefault یا MaxOrDefault در دات نت فریم ورک وجود ندارند. می‌توان این نقیصه را با استفاده از extension methods برطرف کرد.

using System;
using System.Collections.Generic;
using System.Linq;

public static class LinqExtensions
{
public static T MinOrDefault<T>(this IEnumerable<T> source, T defaultValue)
{
if (source.Any<T>())
return source.Min<T>();

return defaultValue;
}

public static T MaxOrDefault<T>(this IEnumerable<T> source, T defaultValue)
{
if (source.Any<T>())
return source.Max<T>();

return defaultValue;
}
}
اکنون با استفاده از extension methods فوق، کد ما به صورت زیر تغییر خواهد کرد:

public static int TestGetListMin4()
{
var lst = new List<Data>();
return (from c in lst
where c.name.Contains("id")
select c.id).MinOrDefault(-1);
}

نظرات مطالب
EF Code First #12
به این ترتیب باید پیاده سازی بشه. یک حالت عمومی است و به کلاس و شیء خاصی گره نخورده:
using System;
using System.ComponentModel.DataAnnotations;
namespace Test
{
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class CompareAttribute : ValidationAttribute
    {
        public CompareAttribute(string originalProperty, string confirmProperty)
        {
            OriginalProperty = originalProperty;
            ConfirmProperty = confirmProperty;
        }
        public string ConfirmProperty { get; private set; }
        public string OriginalProperty { get; private set; }
        protected override ValidationResult IsValid(object value, ValidationContext ctx)
        {
            if (value == null)
                return new ValidationResult("لطفا فیلدها را تکمیل نمائید");
            var confirmProperty = ctx.ObjectType.GetProperty(ConfirmProperty);
            if (confirmProperty == null)
                throw new InvalidOperationException(string.Format("لطفا فیلد {0} را تعریف نمائید", ConfirmProperty));
            var confirmValue = confirmProperty.GetValue(ctx.ObjectInstance, null) as string;
            if (string.IsNullOrWhiteSpace(confirmValue))
                return new ValidationResult(string.Format("لطفا فیلد {0} را تکمیل نمائید", ConfirmProperty));
            var originalProperty = ctx.ObjectType.GetProperty(OriginalProperty);
            if (originalProperty == null)
                throw new InvalidOperationException(string.Format("لطفا فیلد {0} را تعریف نمائید", OriginalProperty));
            var originalValue = originalProperty.GetValue(ctx.ObjectInstance, null) as string;
            if (string.IsNullOrWhiteSpace(originalValue))
                return new ValidationResult(string.Format("لطفا فیلد {0} را تکمیل نمائید", OriginalProperty));
            return originalValue == confirmValue ? ValidationResult.Success : new ValidationResult("مقادیر وارد شده یکسان نیستند");
        }
    }
}

مطالب
بررسی تصویر امنیتی (Captcha) سایت - قسمت دوم

در ادامه بررسی تصویر امنیتی سایت مواردی که باید پیاده سازی شود برای مورد اول میتوان کلاس زیر را در نظر گرفت که متدهایی برای تولید اعداد بصورت تصادفی در بین بازه معرفی شده را بازگشت میدهد:


// RandomGenerator.cs
using System;
using System.Security.Cryptography;

namespace PersianCaptchaHandler
{
    public class RandomGenerator
    {
        private static readonly byte[] Randb = new byte[4];
        private static readonly RNGCryptoServiceProvider Rand = new RNGCryptoServiceProvider();

        public static int Next()
        {
            Rand.GetBytes(Randb);
            var value = BitConverter.ToInt32(Randb, 0);
            if (value < 0) value = -value;
            return value;
        }
        public static int Next(int max)
        {
            Rand.GetBytes(Randb);
            var value = BitConverter.ToInt32(Randb, 0);
            value = value%(max + 1);
            if (value < 0) value = -value;
            return value;
        }
        public static int Next(int min, int max)
        {
            var value = Next(max - min) + min;
            return value;
        }
    }
}
و برای تبدیل عدد تصادفی بدست آمده به متن نیز از این کلاس میتوان استفاده کرد که به طرز ساده ای این کار را انجام میدهد:
// NumberToString.cs
namespace PersianCaptchaHandler
{
    public class NumberToString
    {

        #region Fields
        private static readonly string[] Yakan = new[] { "صفر", "یک", "دو", "سه", "چهار", "پنج", "شش", "هفت", "هشت", "نه" };
        private static readonly string[] Dahgan = new[] { "", "", "بیست", "سی", "چهل", "پنجاه", "شصت", "هفتاد", "هشتاد", "نود" };
        private static readonly string[] Dahyek = new [] { "ده", "یازده", "دوازده", "سیزده", "چهارده", "پانزده", "شانزده", "هفده", "هجده", "نوزده" };
        private static readonly string[] Sadgan = new [] { "", "یکصد", "دوصد", "سیصد", "چهارصد", "پانصد", "ششصد", "هفتصد", "هشتصد", "نهصد" };
        private static readonly string[] Basex = new [] { "", "هزار", "میلیون", "میلیارد", "تریلیون" };
        #endregion

        private static string Getnum3(int num3)
        {
            var s = "";
            var d12 = num3 % 100;
            var d3 = num3 / 100;
            if (d3 != 0)
                s = Sadgan[d3] + " و ";
            if ((d12 >= 10) && (d12 <= 19))
            {
                s = s + Dahyek[d12 - 10];
            }
            else
            {
                var d2 = d12 / 10;
                if (d2 != 0)
                    s = s + Dahgan[d2] + " و ";
                var d1 = d12 % 10;
                if (d1 != 0)
                    s = s + Yakan[d1] + " و ";
                s = s.Substring(0, s.Length - 3);
            }
            return s;
        }

        public static string ConvertIntNumberToFarsiAlphabatic(string snum)
        {
            var stotal = "";
            if (snum == "0") return Yakan[0];

            snum = snum.PadLeft(((snum.Length - 1) / 3 + 1) * 3, '0');
            var l = snum.Length / 3 - 1;
            for (var i = 0; i <= l; i++)
            {
                var b = int.Parse(snum.Substring(i * 3, 3));
                if (b != 0)
                    stotal = stotal + Getnum3(b) + " " + Basex[l - i] + " و ";
            }
            stotal = stotal.Substring(0, stotal.Length - 3);
            return stotal;
        }
    }
}
و برای کد کردن و دیکد کردن یعنی موارد سوم و چهارم مقاله قبلی، متن بدست آمده را که بعنوان قسمتی از آدرس تصویر در ادامه آدرس هندلر معرفی شده می‌آید تبدیل به یک string بی معنی برای بازدیدکننده میکند:

using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

namespace PersianCaptchaHandler
{
    public class Encryptor
    {
        #region constraints
        private static string Password { get { return "Mehdi"; } }
        private static string Salt { get { return "Payervand"; } }
        #endregion

        public static string Encrypt(string clearText)
        {
            // Turn text to bytes
            var clearBytes = Encoding.Unicode.GetBytes(clearText);

            var pdb = new PasswordDeriveBytes(Password, Encoding.Unicode.GetBytes(Salt));

            var ms = new MemoryStream();
            var alg = Rijndael.Create();

            alg.Key = pdb.GetBytes(32);
            alg.IV = pdb.GetBytes(16);

            var cs = new CryptoStream(ms, alg.CreateEncryptor(), CryptoStreamMode.Write);

            cs.Write(clearBytes, 0, clearBytes.Length);
            cs.Close();

            var encryptedData = ms.ToArray();

            return Convert.ToBase64String(encryptedData);
        }
        public static string Decrypt(string cipherText)
        {
            // Convert text to byte
            var cipherBytes = Convert.FromBase64String(cipherText);

            var pdb = new PasswordDeriveBytes(Password, Encoding.Unicode.GetBytes(Salt));

            var ms = new MemoryStream();
            var alg = Rijndael.Create();

            alg.Key = pdb.GetBytes(32);
            alg.IV = pdb.GetBytes(16);

            var cs = new CryptoStream(ms, alg.CreateDecryptor(), CryptoStreamMode.Write);

            cs.Write(cipherBytes, 0, cipherBytes.Length);
            cs.Close();

            var decryptedData = ms.ToArray();

            return Encoding.Unicode.GetString(decryptedData);
        }
    }
}


و نیز برای اعتبار سنجی عدد دریافتی از کاربر میتوان از عبارت با قاعده زیر استفاده کرد:

// Utils.cs
using System.Text.RegularExpressions;

namespace PersianCaptchaHandler
{
    public class Utils
    {
        private static readonly Regex NumberMatch = new Regex(@"^([0-9]*|\d*\.\d{1}?\d*)$", RegexOptions.Compiled);
        public static bool IsNumber(string number2Match)
        {
            return NumberMatch.IsMatch(number2Match);
        }
    }
}
برای استفاده نیز کافیست که هندلر مربوط به این کتابخانه را بطریق زیر در وب کانفیگ رجیستر کرد:

<add verb="GET" path="/captcha/" type="PersianCaptchaHandler.CaptchaHandler, PersianCaptchaHandler, Version=1.0.0.0, Culture=neutral"  />
و در صفحه مورد نظرتان بطریق زیر میتوان از یک تگ تصویر برای نمایش تصویر تولیدی و از یک فیلد مخفی برای نگهداری مقدار کد شده معادل عدد تولیدی که در هنگام مقایسه با عدد ورودی کاربر مورد نیاز است استفاده شود:

<!-- ASPX -->
<dl>
    <dt>تصویر امنیتی</dt>
    <dd>
        <asp:Image ID="imgCaptchaText" runat="server" AlternateText="CaptchaImage" />
        <asp:HiddenField ID="hfCaptchaText" runat="server" />
        <asp:ImageButton ID="btnRefreshCaptcha" runat="server" ImageUrl="/img/refresh.png"
            OnClick="btnRefreshCaptcha_Click" />
        <br />
        <asp:TextBox ID="txtCaptcha" runat="server" AutoCompleteType="Disabled"></asp:TextBox>
    </dd>
    <dd>
        <asp:Button ID="btnSubmit" runat="server" Text="ثبت" OnClick="btnSubmit_Click" />
    </dd>
</dl>
<asp:Label ID="lblMessage" runat="server"></asp:Label>
در زمان لود صفحه، تصویر امنیتی مقدار دهی میشود و در زمان ورود عدد توسط کاربر با توجه به اینکه کاربر حتما باید عدد وارد کند با عبارت با قاعده این اعتبار سنجی انجام میشود:

        protected void Page_Load(object sender, EventArgs e)
        {
            if (!Page.IsPostBack)
                SetCaptcha();
        }

        private void SetCaptcha()
        {
            lblMessage.Text =
            txtCaptcha.Text = string.Empty;

            var newNumber =
                RandomGenerator.Next(100, 999)
                ;

            var farsiAlphabatic = NumberToString.ConvertIntNumberToFarsiAlphabatic(newNumber.ToString());

            hfCaptchaText.Value =
                HttpUtility
                .UrlEncode(
                    Encryptor.Encrypt(
                        farsiAlphabatic
                    )
                );

            txtCaptcha.Text = string.Empty;
            imgCaptchaText.ImageUrl =
                "/captcha/?text=" + hfCaptchaText.Value;
        }
و بعد از ورود عدد از سمت کاربر از متد تبدیل به حروف استفاده کرده و این مقدار تولیدی با مقدار فیلد مخفی مقایسه میشود:
        private string GetCaptcha()
        {
            var farsiAlphabatic = NumberToString.ConvertIntNumberToFarsiAlphabatic(txtCaptcha.Text);

            var encryptedString =
                HttpUtility
                .UrlEncode(
                    Encryptor.Encrypt(
                        farsiAlphabatic
                    )
                );

            return encryptedString;
        }

        private bool ValidateUserInputForLogin()
        {
            if (!Utils.IsNumber(txtCaptcha.Text))
            {
                lblMessage.Text = "تصویر امنیتی را بطور صحیح وارد نکرده اید";
                return false;
            }

            var strGetCaptcha =
                GetCaptcha();

            var strDecodedVAlue =
                hfCaptchaText.Value;

            if (strDecodedVAlue != strGetCaptcha)
            {
                lblMessage.Text = "کلمه امنیتی اشتباه است";
                SetCaptcha();
                return false;
            }
            return true;
        }

        protected void btnSubmit_Click(object sender, EventArgs e)
        {
            if (!ValidateUserInputForLogin()) return;
            lblMessage.Text = "کلمه امنیتی درست است";
        }

        protected void btnRefreshCaptcha_Click(object sender, ImageClickEventArgs e)
        {
            SetCaptcha();
        }
 در آخر این پروژه در کدپلکس قرار داده شده، و مشتاق نظرات و پیشنهادات شما هستم و نیز نمونه مثال بالا ضمیمه شده است
مطالب
استفاده از Fluent Validation در برنامه‌های ASP.NET Core - قسمت پنجم - اعتبارسنجی تنظیمات آغازین برنامه
در برنامه‌های ASP.NET Core، امکان دریافت تنظیمات برنامه از منابع مختلفی مانند فایل‌های JSON وجود دارد که در نگارش‌های اخیر آن، امکان اعتبارسنجی اطلاعات آن‌ها به صورت توکار نیز اضافه شده‌است؛ مانند:
services.AddOptions<BearerTokensOptions>()
           .Bind(configuration.GetSection("BearerTokens"))
           .Validate(bearerTokens =>
          {
                 return bearerTokens.AccessTokenExpirationMinutes < bearerTokens.RefreshTokenExpirationMinutes;
          }, "RefreshTokenExpirationMinutes is less than AccessTokenExpirationMinutes. Obtaining new tokens using the refresh token should happen only if the access token has expired.");
اما این امکان در مقایسه با امکاناتی که FluentValidation در اختیار ما قرار می‌دهد، بسیار ابتدایی به نظر می‌رسد. به همین جهت در این قسمت قصد داریم امکانات اعتبارسنجی کتابخانه‌ی FluentValidation را در حین آغاز برنامه، جهت تعیین اعتبار اطلاعات فایل کانفیگ آن، مورد استفاده قرار دهیم.


معرفی تنظیمات برنامه

فرض کنید فایل appsettings.json برنامه یک چنین محتوایی را دارد:
{
  "ApiSettings": {
    "AllowedEndpoints": [
      {
        "Name": "Service 1",
        "Timeout": 30,
        "Url": "http://service1.site.com"
      },
      {
        "Name": "Service 2",
        "Timeout": 10,
        "Url": "https://service2.site.com"
      }
    ]
  }
}

ایجاد مدل‌های معادل تنظیمات JSON برنامه

بر اساس تعاریف JSON فوق، می‌توان به مدل‌های زیر رسید:
using System;
using System.Collections.Generic;

namespace FluentValidationSample.Models
{
    public class AllowedEndpoint
    {
        public string Name { get; set; }
        public int Timeout { get; set; }
        public Uri Url { get; set; }
    }

    public class ApiSettings
    {
        public IEnumerable<AllowedEndpoint> AllowedEndpoints { get; set; }
    }
}
که نحوه‌ی معرفی آن به سیستم تزریق وابستگی‌های برنامه به صورت زیر است:
namespace FluentValidationSample.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<ApiSettings>(Configuration.GetSection(nameof(ApiSettings)));
و پس از آن در هر قسمتی از برنامه با تزریق <IOptions<ApiSettings می‌توان به اطلاعات تنظیمات برنامه دسترسی یافت.


تعریف شرط‌های اعتبارسنجی مدل‌های تنظیمات برنامه

پس از مدلسازی تنظیمات برنامه و همچنین اتصال آن به <IOptions<ApiSettings، اکنون می‌خواهیم این مدل‌ها، شرایط زیر را برآورده کنند:
- باید مدخل ApiSettings در فایل تنظیمات برنامه وجود خارجی داشته باشد.
- می‌خواهیم AllowedEndpoint‌ها نامدار بوده و هر نام نیز منحصربفرد باشد.
- مقادیر timeout‌ها باید بین 1 و 90 تعریف شده باشند.
- تمام URLها باید منحصربفرد باشند.
- تمام URLها باید HTTPS باشند.

برای این منظور می‌توان تنظیمات زیر را توسط Fluent Validation تعریف کرد:
using System;
using System.Linq;
using FluentValidation;
using FluentValidationSample.Models;

namespace FluentValidationSample.ModelsValidations
{
    public class ApiSettingsValidator : AbstractValidator<ApiSettings>
    {
        public ApiSettingsValidator()
        {
            RuleFor(apiSetting => apiSetting).NotNull()
            .WithMessage("مدخل ApiSettings تعریف نشده‌است.");

            RuleFor(apiSetting => apiSetting.AllowedEndpoints).NotNull().NotEmpty()
            .WithMessage("مدخل AllowedEndpoints تعریف نشده‌است.");

            When(apiSetting => apiSetting.AllowedEndpoints != null,
            () =>
                {
                    RuleFor(apiSetting => apiSetting.AllowedEndpoints)
                        .Must(endpoints => endpoints.GroupBy(endpoint => endpoint.Name).Count() == endpoints.Count())
                        .WithMessage("نام‌های سرویس‌ها باید منحصربفرد باشند.");

                    RuleFor(apiSetting => apiSetting.AllowedEndpoints)
                        .Must(endpoints => !endpoints.Any(endpoint => endpoint.Timeout > 90 || endpoint.Timeout < 1))
                        .WithMessage("مقدار timeout باید بین 1 و 90 باشد");

                    RuleFor(apiSetting => apiSetting.AllowedEndpoints)
                        .Must(endpoints => endpoints.GroupBy(endpoint => endpoint.Url.ToString().ToLower()).Count() == endpoints.Count())
                        .WithMessage("آدرس‌های سرویس‌ها باید منحصربفرد باشند.");

                    RuleFor(apiSetting => apiSetting.AllowedEndpoints)
                        .Must(endpoints => endpoints.All(endpoint => endpoint.Url.Scheme.Equals("https", StringComparison.CurrentCultureIgnoreCase)))
                        .WithMessage("تمام آدرس‌ها باید HTTPS باشند.");
                });
        }
    }
}
که در اینجا نکات زیر قابل ملاحظه هستند:
- چگونه می‌توان از تعریف و وجود یک مدخل فایل JSON، اطمینان حاصل کرد (اعمال RuleFor به کل مدل).
- چگونه می‌توان اگر مدخلی تعریف شده بود، آنگاه برای آن اعتبارسنجی خاصی را تعریف کرد (متد When).
- چگونه می‌توان شرایط سفارشی خاصی را مانند بررسی منحصربفرد بودن‌ها، بررسی کرد (متد Must).


یکپارچه کردن اعتبارسنجی کتابخانه‌ی FluentValidation با اعتبارسنجی توکار مدل‌های تنظیمات برنامه توسط ASP.NET Core

در ابتدای بحث، امکان تعریف متد Validate را که از نگارش ASP.NET Core 2.2 اضافه شده‌است، مشاهده کردید:
services.AddOptions<BearerTokensOptions>()
           .Bind(configuration.GetSection("BearerTokens"))
           .Validate(bearerTokens =>
          {
                 return bearerTokens.AccessTokenExpirationMinutes < bearerTokens.RefreshTokenExpirationMinutes;
          }, "RefreshTokenExpirationMinutes is less than AccessTokenExpirationMinutes. Obtaining new tokens using the refresh token should happen only if the access token has expired.");
می‌توان این متد را با پیاده سازی اینترفیس توکار IValidateOptions نیز به سیستم ارائه داد:
namespace Microsoft.Extensions.Options
{
    public interface IValidateOptions<TOptions> where TOptions : class
    {
        ValidateOptionsResult Validate(string name, TOptions options);
    }
}
و اگر سرویس پیاده سازی کننده‌ی آن‌را با طول عمر Transient به سیستم اضافه کردیم، به صورت خودکار جهت اعتبارسنجی TOptions، مورد استفاده قرار خواهد گرفت. TOptions در این مثال همان ApiSettings است.
در ادامه یک نمونه پیاده سازی جنریک IValidateOptions استاندارد ASP.NET Core را مشاهده می‌کنید:
using System.Linq;
using FluentValidation;
using Microsoft.Extensions.Options;

namespace FluentValidationSample.ModelsValidations
{
    public class AppConfigValidator<TOptions> : IValidateOptions<TOptions> where TOptions : class
    {
        private readonly IValidator<TOptions> _validator;

        public AppConfigValidator(IValidator<TOptions> validator)
        {
            _validator = validator;
        }

        public ValidateOptionsResult Validate(string name, TOptions options)
        {
            if (options is null)
            {
                return ValidateOptionsResult.Fail("Configuration object is null.");
            }

            var validationResult = _validator.Validate(options);
            return validationResult.IsValid
                ? ValidateOptionsResult.Success
                : ValidateOptionsResult.Fail(validationResult.Errors.Select(error => error.ToString()));
        }
    }
}
همانطور که در قسمت دوم این سری این نیز بررسی کردیم، یکی از روش‌های اجرای اعتبارسنجی‌های FluentValidation، کار با اینترفیس IValidator آن است که در اینجا به سازنده‌ی این کلاس تزریق شده‌است. سپس در متد Validate این سرویس، با فراخوانی آن، کار اعتبارسنجی وهله‌ی دریافتی options صورت گرفته و اگر خطایی وجود داشته باشد، بازگشت داده می‌شود.
در آخر روش معرفی آن به سیستم به صورت زیر است:
namespace FluentValidationSample.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<ApiSettings>(Configuration.GetSection(nameof(ApiSettings)));
            services.AddTransient<IValidateOptions<ApiSettings>, AppConfigValidator<ApiSettings>>();
به این ترتیب هرگاه در برنامه یک چنین تعریفی را داشته باشیم که از طریق IOptions، تنظیمات برنامه را دریافت می‌کند:
namespace FluentValidationSample.Web.Controllers
{
    public class HomeController : Controller
    {
        private readonly IUsersService _usersService;
        private readonly ApiSettings _apiSettings;

        public HomeController(IUsersService usersService, IOptions<ApiSettings> apiSettings)
        {
            _usersService = usersService;
            _apiSettings = apiSettings.Value;
        }
اگر در سیستم یک <IValidateOptions<ApiSettings متناظر با <IOptions<ApiSettings ثبت شده باشد (مانند تنظیمات متد ConfigureServices فوق)، هرگاه که فراخوانی apiSettings.Value صورت گیرد، قبل از هرکاری متد Validate سرویس پیاده سازی کننده‌ی IValidateOptions متناظر، فراخوانی شده و اگر خطای اعتبارسنجی وجود داشته باشد، به صورت یک استثناء بازگشت داده می‌شود؛ مانند:
An unhandled exception occurred while processing the request.
OptionsValidationException: تمام آدرس‌ها باید HTTPS باشند.


کدهای کامل این سری را تا این قسمت از اینجا می‌توانید دریافت کنید: FluentValidationSample-part05.zip
مطالب
GraphQL Mutations در ASP.NET Core ( عملیات POST, PUT, DELETE )
Mutation‌ها در GraphQL، اکشن‌های می‌باشند که با استفاده از آن‌ها Add ، Delete و Update را انجام می‌دهیم. تا کنون ما query ‌های بازیابی اطلاعات را اجرا کرده‌ایم. در این قسمت می‌خواهیم در رابطه با Mutation داده‌ها صحبت کنیم.


Input Types and Schema Enhancing for the GraphQL Mutations 

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

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

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

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

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


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

Update Mutation  

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

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

         _context.SaveChanges();
         return dbOwner;
     }
}

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

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

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

        }
    }

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

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


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

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

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

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

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


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

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

نظرات مطالب
EF Code First #12
با سلام 
من در حین اجرای نمونه کد‌های این مقاله در بخش MVC به خطای Value Cannot be null در کلاس
 public class StructureMapControllerFactory : DefaultControllerFactory
    {
        protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
        {
            return ObjectFactory.GetInstance(controllerType) as Controller;
        }
    }
مواجه شدم که با اضافه کردن : 
 routes.IgnoreRoute("{*favicon}", new { favicon = @"(.*/)?favicon.ico(/.*)?" });
به متد Register Route برطرف شد.
بازخوردهای دوره
انتقال خودکار Data Annotations از مدل‌ها به ViewModelهای ASP.NET MVC به کمک AutoMapper
روشی دیگر جهت انتقال Data Annotations از Model به ViewModel

در ASP MVC  با MetadataTypeAttribute .
 روش کار به این صورت است که کلاس ViewModel را به Attribute (MetadataType ) مزین می‌کنیم که این Attribute  در سازنده خود تایپ Model را دریافت می‌کند و همچنین  در فضای نامی زیر قرار دارد 
System.ComponentModel.DataAnnotations 
    public class Student
    {
        public int Id { get; set; }

        [Required(ErrorMessage = "نام ضروری است")]
        [Display(Name = "نام")]
        public string Name { get; set; }
    }

ViewModel
    [MetadataType(typeof(Student))]
    public class StudentViewModel
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

در ASP MVC Core  هم روش کار به همین صورت است بجای MetadataType از ModelMetadataType  استفاده کنید .
نظرات مطالب
EF Code First #12
- پیاده سازی متد SaveChanges در کلاس پایه DbContext که توسط تیم EF ارائه شده، انجام شده و وجود دارد.
- ذکر یک متد در اینترفیس (یک قرار داد) به جهت امکان استفاده از آن است. شما نهایتا با متدهای تعریف شده در طراحی IUnitOfWork در لایه سرویس قرار است کار کنید نه مستقیما با کلاس مشتق شده از DbContext.
زمانیکه می‌نویسید:
public class MyContext : DbContext, IUnitOfWork
چند کار با هم انجام می‌شود:
MyContext به صورت خودکار امکان دسترسی به متد SaveChanges موجود در DbContext را پیدا می‌کند. کتابخانه StructureMap می‌تونه زمانیکه نیازی به یک وهله پیاده ساز IUnitOfWork بود، از MyContext استفاده کنه. همچنین چون الان SaveChanges با امضایی که در اینترفیس IUnitOfWork وجود دارد در کلاس MyContext هم قابل دسترسی است، نیازی به پیاده سازی مجدد آن نیست.