مطالب
بررسی دقیق عملکرد AutoMapper
همانطور که اطلاع دارید، AutoMapper ابزاری برای نگاشت خودکار بین Model و Dto می‌باشد؛ که به صورت نادرست تصور کاهش سرعت در استفاده کردن از آن، بین توسعه دهندگان جا افتاده‌است. در این مقاله قصد داریم به صورت دقیق، به بررسی سرعت عملکرد استفاده از AutoMapper و مقایسه آن با نگاشت دستی بپردازیم.
کد‌های کامل این قسمت را میتوانید از اینجا clone کرده و شخصا تست نمایید.

ابتدا یک پروژه‌ی Console Application را ساخته و AutoMapper را به همراه Ef6، نصب مینماییم. سپس دو کلاس جدید را به نام‌های User و Address به صورت زیر در پوشه‌ی Models مینویسیم.
using System.Collections.Generic;

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

        public string Name { get; set; }

        public ICollection<Address> Addresses { get; set; }
    }
}
using System.ComponentModel.DataAnnotations.Schema;

namespace AutoMapperComparison.Models
{
    public class Address
    {
        public int Id { get; set; }

        public double? Code { get; set; }

        public string Title { get; set; }

        public int UserId { get; set; }

        [ForeignKey(nameof(UserId))]
        public virtual User User { get; set; }
    }
}
بدیهی است که این دو مدل با همدیگر رابطه‌ی 1 به چند دارند. حال کافیست AppDbContext خود را به صورت زیر تعریف نماییم.
نکته: در متد Seed، برای ثبت رکورد‌های اولیه، از BulkInsert استفاده شده است (باید پکیج BulkInsert را نیز نصب نمایید)
using EntityFramework.BulkInsert.Extensions;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;

namespace AutoMapperComparison.Models
{
    public class AppDbContextInitializer : DropCreateDatabaseAlways<AppDbContext>
    {
        protected override void Seed(AppDbContext context)
        {
            User user = context.Users.Add(new User { Name = "Test" });

            context.SaveChanges();

            List<Address> addresses = new List<Address>();

            for (int i = 0; i < 500000; i++)
            {
                addresses.Add(new Address { Id = i, Code = 1, Title = "Test", UserId = user.Id });
            }

            context.BulkInsert(addresses);

            base.Seed(context);
        }
    }

    public class AppDbContext : DbContext
    {
        static AppDbContext()
        {
            Database.SetInitializer(new AppDbContextInitializer());

            //Database.SetInitializer<AppDbContext>(null);
        }

        public AppDbContext()
            : base(new SqlConnection(@"Data Source=.;Initial Catalog=AppDbContext;Integrated Security=True"), contextOwnsConnection: true)
        {
            Configuration.AutoDetectChangesEnabled = false;
            Configuration.EnsureTransactionsForFunctionsAndCommands = false;
            Configuration.LazyLoadingEnabled = false;
            Configuration.ProxyCreationEnabled = false;
            Configuration.ValidateOnSaveEnabled = false;
            Configuration.UseDatabaseNullSemantics = false;
        }

        public DbSet<User> Users { get; set; }

        public DbSet<Address> Addresses { get; set; }
    }
}
 برای اینکه مقایسه انجام شده دقیق باشد، تمامی Configuration‌های اضافی را نیز غیر فعال نموده‌ام.
فقط نیاز داریم یک Dto را برای Address نیز تعریف کنیم؛ چون قرار است نگاشت از Model به Dto از روی Address و AddressDto انجام شود.
کلاس AddressDto را به صورت زیر ایجاد میکنیم:
namespace AutoMapperComparison.Models
{
    public class AddressDto
    {
        public int Id { get; set; }

        public double? Code { get; set; }

        public string Title { get; set; }

        public int UserId { get; set; }

        public string UserName { get; set; }
    }
}
قرار است به صورت خودکار از طریق AutoMapper و همچنین به صورت دستی، نگاشت از Model به Dto مربوطه انجام شود.
 حال نیاز است فایل Program.cs را باز کرده و تغییرات زیر را اعمل نماییم:
using AutoMapper;
using AutoMapper.QueryableExtensions;
using AutoMapperComparison.Models;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace AutoMapperComparison
{
    public class Program
    {
        public static void Main()
        {
            Mapper.Initialize(cfg =>
            {
                cfg.CreateMap<Address, AddressDto>();
            });

            Console.WriteLine($"Create Db {DateTimeOffset.UtcNow}");
            using (AppDbContext db = new AppDbContext())
            {
                db.Database.Initialize(force: true);
                db.Database.ExecuteSqlCommand("DBCC DROPCLEANBUFFERS"); //Removes all clean buffers from the buffer pool, and columnstore objects from the columnstore object pool
                Console.WriteLine(db.Addresses.ProjectTo<AddressDto>());
                Console.WriteLine(db.Addresses.Select(add => new AddressDto { Id = add.Id, Code = add.Code, Title = add.Title, UserId = add.UserId, UserName = add.User.Name }));
            }

            Console.WriteLine($"Normal Select {DateTimeOffset.UtcNow}");
            using (AppDbContext db = new AppDbContext())
            {
                db.Database.ExecuteSqlCommand("DBCC DROPCLEANBUFFERS");
                Stopwatch watch = Stopwatch.StartNew();
                List<AddressDto> addresses = db.Addresses.AsNoTracking().Select(add => new AddressDto { Id = add.Id, Code = add.Code, Title = add.Title, UserId = add.UserId, UserName = add.User.Name }).ToList();
                List<AddressDto> addresses2 = db.Addresses.AsNoTracking().Select(add => new AddressDto { Id = add.Id, Code = add.Code, Title = add.Title, UserId = add.UserId, UserName = add.User.Name }).ToList();
                watch.Stop();
                Console.WriteLine($"{watch.ElapsedMilliseconds} {addresses.Count} {addresses2.Count}");
            }

            Console.WriteLine($"AutoMapper Exec {DateTimeOffset.UtcNow}");
            using (AppDbContext db = new AppDbContext())
            {
                db.Database.ExecuteSqlCommand("DBCC DROPCLEANBUFFERS");
                Stopwatch watch = Stopwatch.StartNew();
                List<AddressDto> addresses = db.Addresses.AsNoTracking().ProjectTo<AddressDto>().ToList();
                List<AddressDto> addresses2 = db.Addresses.AsNoTracking().ProjectTo<AddressDto>().ToList();
                watch.Stop();
                Console.WriteLine($"{watch.ElapsedMilliseconds} {addresses.Count} {addresses2.Count}");
            }

            Console.ReadKey();
        }
    }
}
نکته: از دستور DBCC DROPCLEANBUFFERS جهت خالی کردن بافر sql server برای رسیدن به نتیجه‌ی هرچه دقیق‌تر استفاده شده است.
بعد از اجرا کردن، ابتدا بررسی میکنیم که کوئری اجرا شده‌ی دو نوع مختلف، هیچ تفاوتی با هم نداشته باشند.


حال نتایج بدست آمده، در قسمت پایین‌تر آن نمایان میشود:


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

در سه آزمایش دیگر به صورت متوالی نتیجه‌ی زیر بدست آمد:

Normal   AutoMapper
 2451  2378
 2120  2111
 2202  2124

اگر این مقدار جزئی از تفاوت بین دو نوع مختلف آزمایش را مورد نظر نگیریم، میتوان گفت که هر دو روش نتیجه‌ی کاملا یکسانی خواهند داشت. فقط با استفاده از AutoMapper کد‌های کمتری نوشته شده‌است!

اما دلیل چیست؟ از آنجایی که ProjectTo از Dto به Model انجام شده و Lambda Expressionی که به سمت Entity Framework فرستاده شده‌است با روش Normal کاملا برابر است و بقیه‌ی عملیات توسط EF انجام میشود، با قاطعیت میتوان گفت که هر دو روش ذکر شده از نظر Performance کاملا یکسان خواهند بود.

نکته: البته به این موضوع باید توجه شود که اگر همین آزمایش را بطور مثال با استفاده از یک Listی از رکورد‌های درون Memory ساخته شده توسط خودمان انجام دهیم، آن موقع نتیجه‌ی یکسانی نخواهیم داشت، به دلیل اینکه EFی دیگر وجود نخواهد داشت که مسئولیت بازگشت داده‌ها را بر عهده بگیرد. از آنجائیکه اکثر کارهایی که توقع داریم AutoMapper برای ما انجام دهد، توسط ORM بازگشت داده میشود، پس میتوان گفت نکته‌ی فوق تقریبا در دنیای واقعی رخ نخواهد داد و باعث مشکل نخواهد شد.

مطالب
کنترل عمومی فایل‌های آپلودی در ASP.NET MVC
در مطلب «محدود کردن کاربر‌ها به آپلود فایل‌هایی خاص در ASP.NET MVC» تصمیم گیری بر اساس یک لیست سفید صورت می‌گیرد. برای مثال کاربران فقط قرار است تصویرهایی از نوع png یا jpg را ارسال کنند. اکنون نیاز است حالت کلی‌تری را درنظر بگیریم که کاربر قرار است هر نوع فایل دلخواهی را ارسال کند. در اینجا نباید امکان آپلود هر نوع فایلی، خصوصا فایل‌های اجرایی ASP.NET یا هر نوع موتور اجرایی دیگری که ممکن است روی سرور نصب باشد (مثلا PHP)، وجود داشته باشد. برای این منظور فیلتر دیگری به نام AllowUploadSafeFiles طراحی شده است که سورس آن‌را در ذیل مشاهده می‌کنید:
using System;
using System.Linq;
using System.Collections.Generic;
using System.IO;
using System.Web.Mvc;

namespace SecurityModule
{
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
    public sealed class AllowUploadSafeFilesAttribute : ActionFilterAttribute
    {
        static readonly IList<string> ExtToFilter = new List<string> { 
            ".aspx", ".asax", ".asp", ".ashx", ".asmx", ".axd", ".master", ".svc", ".php" ,        
            ".php3" , ".php4", ".ph3", ".ph4", ".php4", ".ph5", ".sphp", ".cfm", ".ps", ".stm",
            ".htaccess", ".htpasswd", ".php5", ".phtml", ".cgi", ".pl", ".plx", ".py", ".rb", ".sh", ".jsp",
            ".cshtml", ".vbhtml", ".swf" , ".xap", ".asptxt"
        };

        static readonly IList<string> NameToFilter = new List<string> { 
           "web.config" , "htaccess" , "htpasswd", "web~1.con"
        };

        static bool canUpload(string fileName)
        {
            if (string.IsNullOrWhiteSpace(fileName))
                return false;

            fileName = fileName.ToLowerInvariant();
            var name = Path.GetFileName(fileName);
            var ext = Path.GetExtension(fileName);

            if (string.IsNullOrWhiteSpace(name))
                throw new InvalidOperationException("Uploaded file should have a name.");

            return !ExtToFilter.Contains(ext) &&
                   !NameToFilter.Contains(name) &&
                   !NameToFilter.Contains(ext) &&
                   //for "file.asp;.jpg" files
                   ExtToFilter.All(item => !name.Contains(item));
        }

        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var files = filterContext.HttpContext.Request.Files;
            foreach (string file in files)
            {
                var postedFile = files[file];
                if (postedFile == null || postedFile.ContentLength == 0) continue;

                if (!canUpload(postedFile.FileName))
                    throw new InvalidOperationException(string.Format("You are not allowed to upload {0} file.", Path.GetFileName(postedFile.FileName)));
            }

            base.OnActionExecuting(filterContext);
        }
    }
}
در این فیلتر، یک سری پسوند خطرناک مانند aspx، asp و امثال آن فیلتر می‌شوند و اجازه آپلود نخواهند یافت. همچنین فایل‌هایی مانند web.config یا نام داسی آن معادل web~1.con نیز فرصت آپلود نخواهد یافت.
استفاده از این فیلتر سفارشی به نحو زیر است:
[AllowUploadSafeFiles]
public ActionResult UploadFile(HttpPostedFileBase file)
مطالب
رمزنگاری و رمزگشایی خودکار خواص مدل‌ها در ASP.NET Core
فرض کنید قصد دارید خاصیت Id مدل مورد استفاده‌ی در یک View را رمزنگاری کنید تا در سمت کلاینت به سادگی قابل تغییر نباشد. همچنین این Id زمانیکه به سمت سرور ارسال شد، به صورت خودکار رمزگشایی شود و بدون نیاز به تغییرات خاصی در کدهای متداول اکشن متدها، اطلاعات نهایی آن قابل استفاده باشند. برای این منظور در ASP.NET Core می‌توان یک Action Result رمزنگاری کننده و یک Model binder رمزگشایی کننده را طراحی کرد.


نیاز به علامتگذاری خواصی که باید رمزنگاری شوند

می‌خواهیم خاصیت یا خاصیت‌های مشخصی، از یک مدل را رمزنگاری شده به سمت کلاینت ارسال کنیم. به همین جهت ویژگی خالی زیر را به پروژه اضافه می‌کنیم تا از آن تنها جهت علامتگذاری این نوع خواص، استفاده کنیم:
using System;

namespace EncryptedModelBinder.Utils
{
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class EncryptedFieldAttribute : Attribute { }
}


رمزنگاری خودکار مدل خروجی از یک اکشن متد

در ادامه کدهای کامل یک ResultFilter را مشاهده می‌کنید که مدل ارسالی به سمت کلاینت را یافته و سپس خواصی از آن‌را که با ویژگی EncryptedField مزین شده‌اند، به صورت خودکار رمزنگاری می‌کند:
namespace EncryptedModelBinder.Utils
{
    public class EncryptedFieldResultFilter : ResultFilterAttribute
    {
        private readonly IProtectionProviderService _protectionProviderService;
        private readonly ILogger<EncryptedFieldResultFilter> _logger;
        private readonly ConcurrentDictionary<Type, bool> _modelsWithEncryptedFieldAttributes = new ConcurrentDictionary<Type, bool>();

        public EncryptedFieldResultFilter(
            IProtectionProviderService protectionProviderService,
            ILogger<EncryptedFieldResultFilter> logger)
        {
            _protectionProviderService = protectionProviderService;
            _logger = logger;
        }

        public override void OnResultExecuting(ResultExecutingContext context)
        {
            var model = context.Result switch
            {
                PageResult pageResult => pageResult.Model, // For Razor pages
                ViewResult viewResult => viewResult.Model, // For MVC Views
                ObjectResult objectResult => objectResult.Value, // For Web API results
                _ => null
            };

            if (model is null)
            {
                return;
            }

            if (typeof(IEnumerable).IsAssignableFrom(model.GetType()))
            {
                foreach (var item in model as IEnumerable)
                {
                    encryptProperties(item);
                }
            }
            else
            {
                encryptProperties(model);
            }
        }

        private void encryptProperties(object model)
        {
            var modelType = model.GetType();
            if (_modelsWithEncryptedFieldAttributes.TryGetValue(modelType, out var hasEncryptedFieldAttribute)
                && !hasEncryptedFieldAttribute)
            {
                return;
            }

            foreach (var property in modelType.GetProperties())
            {
                var attribute = property.GetCustomAttributes(typeof(EncryptedFieldAttribute), false).FirstOrDefault();
                if (attribute == null)
                {
                    continue;
                }

                hasEncryptedFieldAttribute = true;

                var value = property.GetValue(model);
                if (value is null)
                {
                    continue;
                }

                if (value.GetType() != typeof(string))
                {
                    _logger.LogWarning($"[EncryptedField] should be applied to `string` proprties, But type of `{property.DeclaringType}.{property.Name}` is `{property.PropertyType}`.");
                    continue;
                }

                var encryptedData = _protectionProviderService.Encrypt(value.ToString());
                property.SetValue(model, encryptedData);
            }

            _modelsWithEncryptedFieldAttributes.TryAdd(modelType, hasEncryptedFieldAttribute);
        }
    }
}
توضیحات:
- در اینجا برای رمزنگاری از IProtectionProviderService استفاده شده‌است که در بسته‌ی DNTCommon.Web.Core تعریف شده‌است. این سرویس در پشت صحنه از سیستم Data Protection استفاده می‌کند.
- سپس رخ‌داد OnResultExecuting، بازنویسی شده‌است تا بتوان به مدل ارسالی به سمت کلاینت، پیش از ارسال نهایی آن، دسترسی یافت.
- context.Result می‌تواند از نوع PageResult صفحات Razor باشد و یا از نوع ViewResult مدل‌های متداول Viewهای پروژه‌های MVC و یا از نوع ObjectResult که مرتبط است به پروژه‌های Web Api بدون هیچ نوع View سمت سروری. هر کدام از این نوع‌ها، دارای خاصیت مدل هستند که در اینجا قصد بررسی آن‌را داریم.
- پس از مشخص شدن شیء Model، اکنون حلقه‌ای را بر روی خواص آن تشکیل داده و خواصی را که دارای ویژگی EncryptedFieldAttribute هستند، یافته و آن‌ها را رمزنگاری می‌کنیم.

روش اعمال این فیلتر باید به صورت سراسری باشد:
namespace EncryptedModelBinder
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDNTCommonWeb();
            services.AddControllersWithViews(options =>
            {
                options.Filters.Add(typeof(EncryptedFieldResultFilter));
            });
        }
از این پس مدل‌های تمام خروجی‌های ارسالی به سمت کلاینت، بررسی شده و در صورت لزوم، خواص آن‌ها رمزنگاری می‌شود.


رمزگشایی خودکار مدل دریافتی از سمت کلاینت

تا اینجا موفق شدیم خواص ویژه‌ای از مدل‌ها را رمزنگاری کنیم. مرحله‌ی بعد، رمزگشایی خودکار این اطلاعات در سمت سرور است. به همین جهت نیاز داریم تا در سیستم Model Binding پیش‌فرض ASP.NET Core مداخله کرده و منطق سفارشی خود را تزریق کنیم. بنابراین در ابتدا یک IModelBinderProvider سفارشی را تهیه می‌کنیم تا در صورتیکه خاصیت جاری در حال بررسی توسط سیستم Model Binding دارای ویژگی EncryptedFieldAttribute بود، از EncryptedFieldModelBinder برای پردازش آن استفاده کند:
namespace EncryptedModelBinder.Utils
{
    public class EncryptedFieldModelBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (context.Metadata.IsComplexType)
            {
                return null;
            }

            var propName = context.Metadata.PropertyName;
            if (string.IsNullOrWhiteSpace(propName))
            {
                return null;
            }

            var propInfo = context.Metadata.ContainerType.GetProperty(propName);
            if (propInfo == null)
            {
                return null;
            }

            var attribute = propInfo.GetCustomAttributes(typeof(EncryptedFieldAttribute), false).FirstOrDefault();
            if (attribute == null)
            {
                return null;
            }

            return new BinderTypeModelBinder(typeof(EncryptedFieldModelBinder));
        }
    }
}
که این EncryptedFieldModelBinder به صورت زیر تعریف می‌شود:
namespace EncryptedModelBinder.Utils
{
    public class EncryptedFieldModelBinder : IModelBinder
    {
        private readonly IProtectionProviderService _protectionProviderService;

        public EncryptedFieldModelBinder(IProtectionProviderService protectionProviderService)
        {
            _protectionProviderService = protectionProviderService;
        }

        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
            {
                throw new ArgumentNullException(nameof(bindingContext));
            }

            var logger = bindingContext.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>();
            var fallbackBinder = new SimpleTypeModelBinder(bindingContext.ModelType, logger);
            var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
            if (valueProviderResult == ValueProviderResult.None)
            {
                return fallbackBinder.BindModelAsync(bindingContext);
            }

            bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);

            var valueAsString = valueProviderResult.FirstValue;
            if (string.IsNullOrWhiteSpace(valueAsString))
            {
                return fallbackBinder.BindModelAsync(bindingContext);
            }

            var decryptedResult = _protectionProviderService.Decrypt(valueAsString);
            bindingContext.Result = ModelBindingResult.Success(decryptedResult);
            return Task.CompletedTask;
        }
    }
}
در اینجا مقدار ارسالی به سمت سرور به صورت یک رشته دریافت شده و سپس رمزگشایی می‌شود و بجای مقدار فعلی خاصیت، مورد استفاده قرار می‌گیرد. به این ترتیب دیگر نیازی به تغییر کدهای اکشن متدها برای رمزگشایی اطلاعات نیست.

پس از این تعاریف نیاز است EncryptedFieldModelBinderProvider را به صورت زیر به سیستم معرفی کرد:
namespace EncryptedModelBinder
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDNTCommonWeb();
            services.AddControllersWithViews(options =>
            {
                options.ModelBinderProviders.Insert(0, new EncryptedFieldModelBinderProvider());
                options.Filters.Add(typeof(EncryptedFieldResultFilter));
            });
        }


یک مثال

فرض کنید مدل‌های زیر تعریف شده‌اند:
namespace EncryptedModelBinder.Models
{
    public class ProductInputModel
    {
        [EncryptedField]
        public string Id { get; set; }

        [EncryptedField]
        public int Price { get; set; }

        public string Name { get; set; }
    }
}

namespace EncryptedModelBinder.Models
{
    public class ProductViewModel
    {
        [EncryptedField]
        public string Id { get; set; }

        [EncryptedField]
        public int Price { get; set; }

        public string Name { get; set; }
    }
}
که بعضی از خواص آن‌ها با ویژگی EncryptedField مزین شده‌اند.
اکنون کنترلر زیر زمانیکه رندر شود، View متناظر با اکشن متد Index آن، یکسری لینک را به اکشن متد Details، جهت مشاهده‌ی جزئیات محصول، تولید می‌کند. همچنین اکشن متد Products آن هم فقط یک خروجی JSON را به همراه دارد:
namespace EncryptedModelBinder.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            var model = getProducts();
            return View(model);
        }

        public ActionResult<string> Details(ProductInputModel model)
        {
            return model.Id;
        }

        public ActionResult<List<ProductViewModel>> Products()
        {
            return getProducts();
        }

        private static List<ProductViewModel> getProducts()
        {
            return new List<ProductViewModel>
            {
                new ProductViewModel { Id = "1", Name = "Product 1"},
                new ProductViewModel { Id = "2", Name = "Product 2"},
                new ProductViewModel { Id = "3", Name = "Product 3"}
            };
        }
    }
}
کدهای View اکشن متد Index به صورت زیر است:
@model List<ProductViewModel>

<h3>Home</h3>

<ul>
    @foreach (var item in Model)
    {
        <li><a asp-action="Details" asp-route-id="@item.Id">@item.Name</a></li>
    }
</ul>
در ادامه اگر برنامه را اجرا کنیم، می‌توان مشاهده کرد که تمام asp-route-id‌ها که به خاصیت ویژه‌ی Id اشاره می‌کنند، به صورت خودکار رمزنگاری شده‌اند:


و اگر یکی از لینک‌ها را درخواست کنیم، خروجی model.Id، به صورت معمولی و رمزگشایی شده‌ای مشاهده می‌شود (این خروجی یک رشته‌است که هیچ ویژگی خاصی به آن اعمال نشده‌است. به همین جهت، اینبار این خروجی معمولی مشاهده می‌شود). هدف از اکشن متد Details، نمایش رمزگشایی خودکار اطلاعات است.


و یا اگر اکشن متدی که همانند اکشن متدهای Web API، فقط یک شیء JSON را باز می‌گرداند، فراخوانی کنیم نیز می‌توان به خروجی رمزنگاری شده‌ی زیر رسید:



کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: EncryptedModelBinder.zip
مطالب
ایندکس منحصر به فرد با استفاده از Data Annotation در EF Code First
در حال حاضر امکان خاصی برای ایجاد ایندکس منحصر به فرد در EF First Code وجود ندارد, برای این کار راه‌های زیادی وجود دارد مانند پست قبلی آقای نصیری, در این آموزش از Data Annotation و یا همان Attribute  هایی که بالای Property‌های مدل‌ها قرار می‌دهیم, مانند کد زیر : 
public class User
    {
        public int Id { get; set; }

        [Unique]
        public string Email { get; set; }

        [Unique("MyUniqueIndex",UniqueIndexOrder.ASC)]
        public string Username { get; set; }

        [Unique(UniqueIndexOrder.DESC)]
        public string PersonalCode{ get; set; }

        public string Password { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }

همانطور که در کد بالا می‌بینید با استفاده از Attribute Unique ایندکس منحصر به فرد آن در دیتابیس ساخته خواهد شد.
ابتدا یک کلاس برای Attribute Unique به صورت زیر ایحاد کنید : 
using System;

namespace SampleUniqueIndex
{
    [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
    public class UniqueAttribute : Attribute
    {
        public UniqueAttribute(UniqueIndexOrder order = UniqueIndexOrder.ASC) {
            Order = order;
        }
        public UniqueAttribute(string indexName,UniqueIndexOrder order = UniqueIndexOrder.ASC)
        {
            IndexName = indexName;
            Order = order;
        }
        public string IndexName { get; private set; }
        public UniqueIndexOrder Order { get; set; }
    }

    public enum UniqueIndexOrder
    {
        ASC,
        DESC
    }
}
در کد بالا یک Enum برای مرتب سازی ایندکس به دو صورت صعودی و نزولی قرار دارد, همانند کد ابتدای آموزش که مشاهده می‌کنید امکان تعریف این Attribute به سه صورت امکان دارد که به صورت زیر می‌باشد:
1. ایجاد Attribute بدون هیچ پارامتری که در این صورت نام ایندکس با استفاده از نام جدول و آن فیلد ساخته خواهد شد :  IX_Unique_TableName_FieldName و مرتب ساری آن به صورت صعودی می‌باشد.
2.نامی برای ایندکس انتخاب کنید تا با آن نام در دیتابیس ذخبره شود, در این حالت مرتب سازی آن هم به صورت صعودی می‌باشد.
3. در حالت سوم شما ضمن وارد کردن نام ایندکس مرتب سازی آن را نیز وارد می‌کنید.
بعد از کلاس Attribute حالا نوبت به کلاس اصلی میرسد که به صورت زیر می‌باشد:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Metadata.Edm;
using System.Linq;
using System.Reflection;

namespace SampleUniqueIndex
{
    public static class DbContextExtention
    {
        private static BindingFlags PublicInstance = BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy;

        public static void ExecuteUniqueIndexes(this DbContext context)
        {
            var tables = GetTables(context);
            var query = "";
            foreach (var dbSet in GetDbSets(context))
            {
                var entityType = dbSet.PropertyType.GetGenericArguments().First();
                var table = tables[entityType.Name];
                var currentIndexes = GetCurrentUniqueIndexes(context, table.TableName);
                foreach (var uniqueProp in GetUniqueProperties(context, entityType, table))
                {
                    var indexName = string.IsNullOrWhiteSpace(uniqueProp.IndexName) ?
                        "IX_Unique_" + uniqueProp.TableName + "_" + uniqueProp.FieldName :
                        uniqueProp.IndexName;

                    if (!currentIndexes.Contains(indexName))
                    {
                        query += "ALTER TABLE [" + table.TableSchema + "].[" + table.TableName + "] ADD CONSTRAINT [" + indexName + "] UNIQUE ([" + uniqueProp.FieldName + "] " + uniqueProp.Order + "); ";
                    }
                    else
                    {
                        currentIndexes.Remove(indexName);
                    }
                }
                foreach (var index in currentIndexes)
                {
                    query += "ALTER TABLE [" + table.TableSchema + "].[" + table.TableName + "] DROP CONSTRAINT " + index + "; ";
                }
            }

            if (query.Length > 0)
                context.Database.ExecuteSqlCommand(query);
        }

        private static List<string> GetCurrentUniqueIndexes(DbContext context, string tableName)
        {
            var sql = "SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS where table_name = '"
                      + tableName + "' and CONSTRAINT_TYPE = 'UNIQUE'";
            var result = context.Database.SqlQuery<string>(sql).ToList();
            return result;
        }
        private static IEnumerable<PropertyDescriptor> GetDbSets(DbContext context)
        {
            foreach (PropertyDescriptor prop in TypeDescriptor.GetProperties(context))
            {
                var notMapped = prop.GetType().GetCustomAttributes(typeof(NotMappedAttribute),true);
                if (prop.PropertyType.Name == typeof(DbSet<>).Name && notMapped.Length == 0)
                    yield return prop;
            }
        }
        private static List<UniqueProperty> GetUniqueProperties(DbContext context, Type entity, TableInfo tableInfo)
        {
            var indexedProperties = new List<UniqueProperty>();
            var properties = entity.GetProperties(PublicInstance);
            var tableName = tableInfo.TableName;
            foreach (var prop in properties)
            {
                if (!prop.PropertyType.IsValueType && prop.PropertyType != typeof(string)) continue;

                UniqueAttribute[] uniqueAttributes = (UniqueAttribute[])prop.GetCustomAttributes(typeof(UniqueAttribute), true);
                NotMappedAttribute[] notMappedAttributes = (NotMappedAttribute[])prop.GetCustomAttributes(typeof(NotMappedAttribute), true);
                if (uniqueAttributes.Length > 0 && notMappedAttributes.Length == 0)
                {
                    var fieldName = GetFieldName(context, entity, prop.Name);
                    if (fieldName != null)
                    {
                        indexedProperties.Add(new UniqueProperty
                        {
                            TableName = tableName,
                            IndexName = uniqueAttributes[0].IndexName,
                            FieldName = fieldName,
                            Order = uniqueAttributes[0].Order.ToString()
                        });
                    }
                }
            }
            return indexedProperties;
        }
        private static Dictionary<string, TableInfo> GetTables(DbContext context)
        {
            var tablesInfo = new Dictionary<string, TableInfo>();
            var metadata = ((IObjectContextAdapter)context).ObjectContext.MetadataWorkspace;
            var tables = metadata.GetItemCollection(DataSpace.SSpace)
              .GetItems<EntityContainer>()
              .Single()
              .BaseEntitySets
              .OfType<EntitySet>()
              .Where(s => !s.MetadataProperties.Contains("Type")
                || s.MetadataProperties["Type"].ToString() == "Tables");
            foreach (var table in tables)
            {
                var tableName = table.MetadataProperties.Contains("Table")
                    && table.MetadataProperties["Table"].Value != null
                  ? table.MetadataProperties["Table"].Value.ToString()
                  : table.Name;
                var tableSchema = table.MetadataProperties["Schema"].Value.ToString();
                tablesInfo.Add(table.Name, new TableInfo
                {
                    EntityName = table.Name,
                    TableName = tableName,
                    TableSchema = tableSchema,
                });
            }

            return tablesInfo;
        }
        public static string GetFieldName(DbContext context, Type entityModel, string propertyName)
        {
            var metadata = ((IObjectContextAdapter)context).ObjectContext.MetadataWorkspace;
            var osMembers = metadata.GetItem<EntityType>(entityModel.FullName, DataSpace.OSpace).Properties;
            var ssMebers = metadata.GetItem<EntityType>("CodeFirstDatabaseSchema." + entityModel.Name, DataSpace.SSpace).Properties;
            
            if (!osMembers.Contains(propertyName)) return null;

            var index = osMembers.IndexOf(osMembers[propertyName]);
            return ssMebers[index].Name;
        }

        internal class UniqueProperty
        {
            public string TableName { get; set; }
            public string FieldName { get; set; }
            public string IndexName { get; set; }
            public string Order { get; set; }
        }
        internal class TableInfo
        {
            public string EntityName { get; set; }
            public string TableName { get; set; }
            public string TableSchema { get; set; }
        }
    }
}
در کد بالا با استفاده از Extension Method برای کلاس DbContext یک متد با نام ExecuteUniqueIndexes  ایجاد می‌کنیم تا برای ایجاد ایندکس‌ها در دیتابیس از آن استفاده کنیم.
روند اجرای کلاس بالا به صورت زیر می‌باشد:
در ابتدای متد ()ExecuteUniqueIndexes  :
 public static void ExecuteUniqueIndexes(this DbContext context)
        {
            var tables = GetTables(context);
            ...
        }
با استفاده از متد ()GetTables ما تمام جداول ساخته توسط دیتایس توسط DbContext را گرفنه:
        private static Dictionary<string, TableInfo> GetTables(DbContext context)
        {
            var tablesInfo = new Dictionary<string, TableInfo>();
            var metadata = ((IObjectContextAdapter)context).ObjectContext.MetadataWorkspace;
            var tables = metadata.GetItemCollection(DataSpace.SSpace)
              .GetItems<EntityContainer>()
              .Single()
              .BaseEntitySets
              .OfType<EntitySet>()
              .Where(s => !s.MetadataProperties.Contains("Type")
                || s.MetadataProperties["Type"].ToString() == "Tables");
            foreach (var table in tables)
            {
                var tableName = table.MetadataProperties.Contains("Table")
                    && table.MetadataProperties["Table"].Value != null
                  ? table.MetadataProperties["Table"].Value.ToString()
                  : table.Name;
                var tableSchema = table.MetadataProperties["Schema"].Value.ToString();
                tablesInfo.Add(table.Name, new TableInfo
                {
                    EntityName = table.Name,
                    TableName = tableName,
                    TableSchema = tableSchema,
                });
            }

            return tablesInfo;
        }
با استفاده از این طریق چنانچه کاربر نام دیگری برای هر جدول در نظر بگیرد مشکلی ایجاد نمی‌شود و همینطور Schema جدول نیز گرفته می‌شود, سه مشخصه نام مدل و نام جدول و Schema جدول در کلاس TableInfo قرار داده می‌شود و در انتها تمام جداول در یک Collection قرار داده میشوند و به عنوان خروجی متد استفاده می‌شوند.
بعد از آنکه نام جداول متناظر با نام مدل آنها را در اختیار داریم نوبت به گرفتن تمام DbSet‌ها در DbContext می‌باشد که با استفاده از متد ()GetDbSets :
public static void ExecuteUniqueIndexes(this DbContext context)
        {
            var tables = GetTables(context);
            var query = "";
            foreach (var dbSet in GetDbSets(context))
            {
            ....
        }
در این متد چنانچه Property دارای Attribute NotMapped باشد در لیست خروجی متد قرار داده نمی‌شود. 
سپس داخل چرخه DbSet‌ها نوبت به گرفتن ایندکس‌های موجود با استفاده از متد ()GetCurrentUniqueIndexes برای این مدل می‌باشد تا از ایجاد دوباره آن جلوگیری شود و البته اگر ایندکس هایی را در مدل تعربف نکرده باشید از دیتابیس حذف شوند.
        public static void ExecuteUniqueIndexes(this DbContext context)
        {
            ...
            foreach (var dbSet in GetDbSets(context))
            {
                var entityType = dbSet.PropertyType.GetGenericArguments().First();
                var table = tables[entityType.Name];
                var currentIndexes = GetCurrentUniqueIndexes(context, table.TableName);
            }
        }
بعد از آن نوبت به گرفتن Property‌های دارای Attribute Unique می‌باشد که این کار نیز با استفاده از متد ()GetUniqueProperties انجام خواهد شد.
در متد ()GetUniqueProperties چند شرط بررسی خواهد شد از جمله اینکه Property از نوع Value Type باشد و نه یک کلاس سپس Attribute NotMapped را نداشته باشد و بعد از آن می‌بایست نام متناظر با آن Property را در دیتابیس به دست بیاریم برای این کار از متد ()GetFieldName استفاده می‌کنیم:
        public static string GetFieldName(DbContext context, Type entityModel, string propertyName)
        {
            var metadata = ((IObjectContextAdapter)context).ObjectContext.MetadataWorkspace;
            var osMembers = metadata.GetItem<EntityType>(entityModel.FullName, DataSpace.OSpace).Properties;
            var ssMebers = metadata.GetItem<EntityType>("CodeFirstDatabaseSchema." + entityModel.Name, DataSpace.SSpace).Properties;
            
            if (!osMembers.Contains(propertyName)) return null;

            var index = osMembers.IndexOf(osMembers[propertyName]);
            return ssMebers[index].Name;
        }
برای این کار با استفاده از MetadataWorkspace در DbContext دو لیست SSpace و OSpace استفاده می‌کنیم که در ادامه در مورد این گونه لیست ها بیشتر توضیح می‌دهیم , سپس با استفاده از Member‌های این دو لیست و ایندکس‌های متناظر در این دو لیست نام متناظر با Property را در دیتابیس پیدا خواهیم کرد, البته یک نکته مهم هست چنانچه برای فیلد‌های دیتابیس OrderColumn قرار داده باشید دو لیست Member‌ها از نظر ایندکس متناظر متفاوت خواهند شد پس در نتیجه ایندکس به اشتباه برروی یک فیلد دیگر اعمال خواهد شد.
لیست‌ها در MetadataWorkspace:
1. CSpace : این لیست شامل آبجکت‌های Conceptual از مدل‌های شما می‌باشد تا برای Mapping دیتابیس با مدل‌های شما مانند مبدلی این بین عمل کند.
2. OSpace : این لیست شامل آبجکت‌های مدل‌های شما می‌باشد.
3. SSpace : این لیست نیز شامل آبجکت‌های مربوط به دیتابیس از مدل‌های شما می‌باشد
4. CSSpace : این لیست شامل تنظیمات Mapping بین دو لیست SSpace و CSpace می‌باشد.
5. OCSpace : این لیست شامل تنظیمات Mapping بین دو لیست OSpace و CSpace می‌باشد.
روند Mapping مدل‌های شما از OSpace شروع شده و به SSpace ختم میشود که سه لیست دیگز شامل تنظیماتی برای این کار می‌باشند.
و حالا در متد اصلی ()ExecuteUniqueIndexes ما کوئری مورد نیاز برای ساخت ایندکس‌ها را ساخته ایم.

حال برای استفاده از متد()ExecuteUniqueIndexes می‌بایست در متد Seed آن را صدا بزنیم تا کار ساخت ایندکس‌ها شروع شود، مانند کد زیر:
protected override void Seed(myDbContext context)
        {
            //  This method will be called after migrating to the latest version.

            //  You can use the DbSet<T>.AddOrUpdate() helper extension method 
            //  to avoid creating duplicate seed data. E.g.
            //
            //    context.People.AddOrUpdate(
            //      p => p.FullName,
            //      new Person { FullName = "Andrew Peters" },
            //      new Person { FullName = "Brice Lambson" },
            //      new Person { FullName = "Rowan Miller" }
            //    );
            //
            context.ExecuteUniqueIndexes();
        }
چند نکته برای ایجاد ایندکس منحصر به فرد وجود دارد که در زیر به آنها اشاره می‌کنیم:
1. فیلد‌های متنی باید حداکثر تا 350 کاراکتر باشند تا ایندکس اعمال شود.
2. همانطور که بالاتر اشاره شد برای فیلد‌های دیتابیس OrderColumn اعمال نکنید که علت آن در بالا توضیح داده شد

دانلود فایل پروژه:
Sample_UniqueIndex.zip
مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 18 - کار با ASP.NET Web API
در ASP.NET Core، برخلاف نگارش‌های قبلی ASP.NET که ASP.NET Web API مجزای از ASP.NET MVC و همچنین وب فرم‌ها ارائه شده بود، اکنون جزئی از ASP.NET MVC است و با آن یکپارچه می‌باشد. بنابراین پیشنیازهای راه اندازی Web API با ASP.NET Core شامل سه مورد ذیل هستند که پیشتر آن‌ها را بررسی کردیم:
الف) فعال سازی ارائه‌ی فایل‌های استاتیک
ب) فعال سازی ASP.NET MVC
ج) آشنایی با تغییرات مسیریابی

و مابقی آن صرفا یک سری نکات تکمیلی هستند که در ادامه آن‌ها را بررسی خواهیم کرد.


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

در اینجا همانند مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 9 - بررسی تغییرات مسیریابی»، می‌توان در صورت نیاز، مسیریابی کلی کنترلر را توسط ویژگی Route بازنویسی کرد و برای مثال درخواست‌های آن‌را محدود به درخواست‌هایی کرد که با api/ شروع شوند:
[Route("api/[controller]")] // http://localhost:7742/api/test
public class TestController : Controller
{
    private readonly ILogger<TestController> _logger;
 
    public TestController(ILogger<TestController> logger)
    {
        _logger = logger;
    }
[controller] هم در اینجا یک توکن پیش فرض است که با نام کنترلر جاری یا همان Test، به صورت خودکار جایگزین می‌شود.
در مورد سرویس ثبت وقایع نیز در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 17 - بررسی فریم ورک Logging» بحث کردیم و از آن می‌توان برای ثبت استثناءهای رخ داده استفاده کرد.


یک کنترلر ، اما با قابلیت‌های متعدد

همانطور که ملاحظه می‌کنید، اینبار کلاس پایه‌ی این کنترلر Test، همان Controller متداول ASP.NET MVC ذکر شده‌است و نه Api Controller سابق. تمام قابلیت‌های موجود در این‌دو توسط همان Controller ارائه می‌شوند.


هنوز پیش فرض‌های سابق Web API برقرار هستند

در مثال ذیل که به نظر یک کنترلر ASP.NET MVC است،
- هنوز متد Get مربوط به Web API که به صورت پیش فرض به درخواست‌های Get ختم شده‌ی به نام کنترلر پاسخ می‌دهد، برقرار است (متد IEnumerable<string> Get). برای مثال اگر شخصی در مرورگر، آدرس http://localhost:7742/api/test را درخواست دهد، متد Get اجرا می‌شود.
- در اینجا می‌توان نوع خروجی متد را دقیقا از همان نوع اشیاء مدنظر، تعیین کرد؛ برای نمونه تعریف  <IEnumerable<string در مثال زیر.
- مهم نیست که از return Json استفاده کنید و یا خروجی را مستقیما با فرمت <IEnumerable<string ارائه دهید.
- اگر نیاز به کنترل بیشتری بر روی HTTP Response Status بازگشتی داشتید، می‌توانید از متدهایی مانند return Ok و یا return BadRequest در صورت بروز مشکلی استفاده نمائید. برای مثال در متد IActionResult GetEpisodes2، استثنای فرضی حاصل، ابتدا توسط سرویس ثبت وقایع ذخیره شده و در آخر یک BadRequest بازگشت داده می‌شود.
- تمام مسیریابی‌ها را توسط ویژگی Route و یا نوع‌های درخواستی مانند HttpGet، می‌توان بازنویسی کرد؛ مانند مسیر /api/path1
- امکان محدود ساختن نوع پارامترهای دریافتی همانند متد Get(int page) ذیل، توسط ویژگی‌های مسیریابی وجود دارد.
[Route("api/[controller]")] // http://localhost:7742/api/test
public class TestController : Controller
{
    private readonly ILogger<TestController> _logger;
 
    public TestController(ILogger<TestController> logger)
    {
        _logger = logger;
    }
 
    [HttpGet]
    public IEnumerable<string> Get() // http://localhost:7742/api/test
    {
        return new [] { "value1", "value2" };
    }
 
    [HttpGet("{page:int}")]
    public IActionResult Get(int page) // http://localhost:7742/api/test/1
    {
        return Json(new[] { "value3", "value4" });
    }
 
    [HttpGet("/api/path1")]
    public IActionResult GetEpisodes1() // http://localhost:7742/api/path1
    {
        return Json(new[] { "value5", "value6" });
    }
 
    [HttpGet("/api/path2")]
    public IActionResult GetEpisodes2() // http://localhost:7742/api/path2
    {
        try
        {
            // get data from the DB ...
            return Ok(new[] { "value7", "value8" });
        }
        catch (Exception ex)
        {
            _logger.LogError("Failed to get data from the API", ex);
            return BadRequest();
        }
    } 
}
بنابراین در اینجا اگر می‌خواهید یک کنترلر ASP.NET Web API 2.x را به ASP.NET Core 1.0 ارتقاء دهید، تمام متدهای Get و Put و امثال آن هنوز معتبر هستند و مانند سابق عمل می‌کنند:
    [Route("api/[controller]")]
    public class ValuesController : Controller
    {
        // GET: api/values
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new string[] { "value1", "value2" };
        }
// GET api/values/5
        [HttpGet("{id}")]
        public string Get(int id)
        {
            return "value";
        }
// POST api/values
        [HttpPost]
        public void Post([FromBody]string value)
        {
        }
// PUT api/values/5
        [HttpPut("{id}")]
        public void Put(int id, [FromBody]string value)
        {
        }
// DELETE api/values/5
        [HttpDelete("{id}")]
        public void Delete(int id)
        {
        }
    }
}
در مورد ویژگی FromBody در ادامه بیشتر بحث خواهد شد.

یک نکته: اگر می‌خواهید خروجی Web API شما همواره JSON باشد، می‌توانید ویژگی جدید Produces را به شکل ذیل به کلاس کنترلر اعمال کنید:
 [Produces("application/json")]
[Route("api/[controller]")] // http://localhost:7742/api/test
public class TestController : Controller


تغییرات Model binding پیش فرض، برای پشتیبانی از ASP.NET MVC و ASP.NET Web API

فرض کنید مدل زیر را به برنامه اضافه کرده‌اید:
namespace Core1RtmEmptyTest.Models
{
    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int Age { get; set; }
    }
}
و همچنین قصد دارید اطلاعات آن‌را از کاربر توسط یک عملیات POST دریافت کرده و به شکل JSON نمایش دهید:
using Core1RtmEmptyTest.Models;
using Microsoft.AspNetCore.Mvc;
 
namespace Core1RtmEmptyTest.Controllers
{
    public class PersonController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }
 
        [HttpPost]
        public IActionResult Index(Person person)
        {
            return Json(person);
        }
    }
}
برای اینکار، از jQuery به نحو ذیل استفاده می‌کنیم (از این جهت که بیشتر ارسال‌های به سرور جهت کار با Web API نیز Ajax ایی هستند):
@section scripts
{
    <script type="text/javascript">
        $(function () {
            $.ajax({
                type: 'POST',
                url: '/Person/Index',
                dataType: 'json',
                contentType: 'application/json; charset=utf-8',
                data: JSON.stringify({
                    FirstName: 'F1',
                    LastName: 'L1',
                    Age: 23
                }),
                success: function (result) {
                    console.log('Data received: ');
                    console.log(result);
                }
            });
        });
    </script>
}


همانطور که مشاهده می‌کنید، اگر در ابتدای این متد یک break-point قرار دهیم، اطلاعاتی را از سمت کاربر دریافت نکرده‌است و مقادیر دریافتی نال هستند.
این مورد یکی از مهم‌ترین تغییرات Model binding این نگارش از ASP.NET MVC با نگارش‌های قبلی آن است. در اینجا اشیاء پیچیده از request body دریافت و bind نمی‌شوند و باید به نحو ذیل، محل دریافت و تفسیر آن‌ها را دقیقا مشخص کرد:
 public IActionResult Index([FromBody]Person person)
زمانیکه ویژگی FromBody را مشخص می‌کنیم، آنگاه اطلاعات دریافتی از request body دریافتی، به شیء Person نگاشت خواهند شد.


نکته‌ی مهم: حتی اگر FromBody را ذکر کنید ولی از JSON.stringify در سمت کاربر استفاده نکنید، باز هم نال دریافت خواهید کرد. بنابراین در این نگارش ذکر JSON.stringify نیز الزامی است.


حالت‌های دیگر تغییرات Model Binding در ASP.NET Core

تا اینجا مشخص شد که اگر یک درخواست Ajax ایی را به سمت سرور یک برنامه‌ی ASP.NET Core ارسال کنیم، به صورت پیش فرض به اشیاء پیچیده‌ی سمت سرور bind نمی‌شود و باید حتما ویژگی FromBody را نیز مشخص کرد تا اطلاعات را از request body واکشی کند (محل دریافت اطلاعات پیش فرض آن نامشخص است).
یک سؤال: اگر به سمت یک چنین اکشن متدی، اطلاعات فرمی را به حالت معمول ارسال کنیم، چه اتفاقی رخ خواهد داد؟
ارسال اطلاعات فرم‌ها به سرور، همواره شامل دو تغییر ذیل است:
  var dataType = 'application/x-www-form-urlencoded; charset=utf-8';
 var data = $('form').serialize();
اطلاعات فرم سریالایز می‌شوند و data type مخصوصی هم برای آن‌ها تنظیم خواهد شد. در این حالت، ارسال یک چنین اطلاعاتی به سمت اکشن متد فوق، با خطای 415 unsupported media type متوقف می‌شود. برای رفع این مشکل باید از ویژگی دیگری به نام FromForm استفاده کرد:
 [HttpPost]
public IActionResult Index([FromForm]Person person)
حالت‌های دیگر ممکن را در تصویر ذیل ملاحظه می‌کنید:


علت این مساله نیز بالا رفتن میزان امنیت سیستم است. در نگارش‌های قبلی، تمام مکان‌ها و حالت‌های میسر جستجو می‌شوند و اگر یکی از آن‌ها قابلیت تطابق با خواص شیء مدنظر را داشته باشد، کار binding به پایان می‌رسد. اما در اینجا با مشخص شدن محل دقیق منبع اطلاعات، دیگر سایر حالات جستجو نشده و سطح حمله کاهش پیدا می‌کند.
در اینجا باید مشخص کرد که دقیقا اطلاعاتی که قرار است به یک شیء پیچیده Bind شوند، آیا از یک Form تامین می‌شوند، یا از Body و یا از هدر، کوئری استرینگ، مسیریابی و یا حتی از یک سرویس.
تمام این حالت‌ها مشخص هستند (برای مثال دریافت اطلاعات از هدر درخواست HTTP و انتساب آن‌ها به خواص متناظری در شیء مشخص شده)، منهای FromService آن که به نحو ذیل عمل می‌کند:
در این حالت می‌توان در سازنده‌ی کلاس مدل خود، سرویسی را تزریق کرد و توسط آن خاصیتی را مقدار دهی نمود:
public class ProductModel
{
    public ProductModel(IProductService prodService)
    {
        Value = prodService.Get(productId);
    }
    public IProduct Value { get; private set; }
}
این تزریق وابستگی‌ها برای اینکه تکمیل شود، نیاز به ویژگی FromServices خواهد داشت:
 public async Task<IActionResult> GetProduct([FromServices]ProductModel product)
{
}
وجود ویژگی FromServices به این معنا است که سرویس‌های مدل یاد شده را از تنظیمات ابتدایی IoC Container خود خوانده و سپس در اختیار مدل جاری قرار بده. به این ترتیب حتی تزریق وابستگی‌ها در مدل‌های برنامه هم میسر می‌شود.


تغییر تنظیمات اولیه‌ی خروجی‌های ASP.NET Web API

در اینجا حالت ارائه‌ی خروجی XML به صورت پیش فرض فعال نیست. اگر علاقمند به افزودن آن نیز باشید، نحوه‌ی کار را در متد ConfigureServices کلاس آغازین برنامه در کدهای ذیل مشاهده می‌کنید:
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options =>
    {
        options.FormatterMappings.SetMediaTypeMappingForFormat("xml", new MediaTypeHeaderValue("application/xml")); 
 
    }).AddJsonOptions(options =>
    {
        options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
        options.SerializerSettings.DefaultValueHandling = DefaultValueHandling.Include;
        options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
    });
همچنین اگر خواستید تنظیمات ابتدایی JSON.NET را تغییر داده و برای مثال خروجی JSON تولیدی را camel case کنید، این‌کار را توسط متد AddJsonOptions به نحو فوق می‌توان انجام داد.
نظرات مطالب
پشتیبانی توکار از انجام کارهای پس‌زمینه در ASP.NET Core 2x
نکته تکمیلی: معادل  HostingEnvironment.QueueBackgroundWorkItem  در ASP.NET Core
public interface IBackgroundTaskQueue : ISingletonDependency
{
    void QueueBackgroundWorkItem(Func<CancellationToken, IServiceProvider, Task> workItem);

    Task<Func<CancellationToken, IServiceProvider, Task>> DequeueAsync(
        CancellationToken cancellationToken);
}
با تزریق این IBackgroundTaskQueue و استفاده از متد QueueBackgoundWorkItem، امکان در صف قرار دادن یک وظیفه جدید را خواهید داشت. 
پیاده سازی واسط IBackgroundTaskQueue
internal class BackgroundTaskQueue : IBackgroundTaskQueue
{
    private readonly ConcurrentQueue<Func<CancellationToken, IServiceProvider, Task>> _workItems =
        new ConcurrentQueue<Func<CancellationToken, IServiceProvider, Task>>();

    private readonly SemaphoreSlim _signal = new SemaphoreSlim(0);

    public void QueueBackgroundWorkItem(
        Func<CancellationToken, IServiceProvider, Task> workItem)
    {
        if (workItem == null)
        {
            throw new ArgumentNullException(nameof(workItem));
        }

        _workItems.Enqueue(workItem);
        _signal.Release();
    }

    public async Task<Func<CancellationToken, IServiceProvider, Task>> DequeueAsync(
        CancellationToken cancellationToken)
    {
        await _signal.WaitAsync(cancellationToken);
        _workItems.TryDequeue(out var workItem);

        return workItem;
    }
}
در زمان ثبت و معرفی یک کار پس‌زمینه، داخل صفی با رعایت مباحث همزمانی و تحت عنوان ‎_workItems قرار خواهد گرفت. متد DequeueAsync نیز توسط HostedService پیاده سازی شده در ادامه، استفاده شده و به ترتیب وظایف ثبت شده را اجرا خواهد کرد.
پیاده سازی یک QueuedHostedService 
public class QueuedHostedService : BackgroundService
{
    private readonly IServiceScopeFactory _factory;
    private readonly ILogger _logger;
    private readonly IBackgroundTaskQueue _queue;

    public QueuedHostedService(
        IBackgroundTaskQueue queue,
        IServiceScopeFactory factory,
        ILoggerFactory loggerFactory)
    {
        _factory = factory ?? throw new ArgumentNullException(nameof(factory));
        _queue = queue ?? throw new ArgumentNullException(nameof(queue));
        _logger = loggerFactory.CreateLogger<QueuedHostedService>();
    }


    protected override async Task ExecuteAsync(CancellationToken cancellationToken)

    {
        _logger.LogInformation("Queued Hosted Service is starting.");

        while (!cancellationToken.IsCancellationRequested)
        {
            var workItem = await _queue.DequeueAsync(cancellationToken);

            try
            {
                using (var scope = _factory.CreateScope())
                {
                    await workItem(cancellationToken, scope.ServiceProvider);
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex,
                    $"Error occurred executing {nameof(workItem)}.");
            }
        }

        _logger.LogInformation("Queued Hosted Service is stopping.");
    }
}
این امکان قرار است به صورت آزمایشی به نسخه ASP.NET Core 3.0 اضافه شود. برای استفاده از آن کافی است QueuedHostedService را به سیستم DI معرفی کرده به شکل زیر عمل کنید:
public class InvoiceService : IInvoiceService
{
   private readonly IBackgroundTaskQueue _queue;
   
   public InvoiceService(IBackgroundTaskQueue queue)
   {
     _queue = queue ?? throw new ArgumentNullException(nameof(queue));
   }
   
   public Print(InvoiceModel model)
   {
      _queue.QueueBackgroundWorkItem((token, provider)=>
      {
      //todo: print
      return Task.Task.CompletedTask;
      })
   }
}

نظرات مطالب
ASP.NET MVC #13
public class MyViewModel
{
    [Required]
    public string CategoryId { get; set; }

    public IEnumerable<Category> Categories { get; set; }
}

Controller:

public ActionResult Index()
{
   var model = new MyViewModel
   {
       Categories = GetCategories()
   }
   return View(model);
}

View:

@Html.DropDownListFor(
    x => x.CategoryId, 
    new SelectList(Model.Categories, "ID", "CategoryName"), 
    "-- Please select a category --"
)
@Html.ValidationMessageFor(x => x.CategoryId)
مطالب
آشنایی با Refactoring - قسمت 1

کارهای سورس باز قابل توجهی از برنامه نویس‌های ایرانی یافت نمی‌شوند؛ عموما کارهای ارائه شده در حد یک سری مثال یا کتابخانه‌های کوچک است و در همین حد. یا گاهی هم انگشت شمار پروژه‌هایی کامل. مثل یک وب سایت یا یک برنامه نصفه نیمه دبیرخانه و امثال آن. این‌ها هم خوب است از دیدگاه به اشتراک گذاری اطلاعات، ایده‌ها و هم ... یک مزیت دیگر را هم دارد و آن این است که بتوان کیفیت عمومی کد نویسی را حدس زد.
اگر کیفیت کدها رو بررسی ‌کنید به یک نتیجه‌ی کلی خواهید رسید: "عموم برنامه نویس‌های ایرانی (حداقل این‌هایی که چند عدد کار سورس باز به اشتراک گذاشته‌اند) با مفهومی به نام Refactoring هیچگونه آشنایی ندارند". مثلا یک برنامه‌ی WinForm تهیه کرده‌اند و کل سورس برنامه همان چند عدد فرم برنامه است و هر فرم بالای 3000 سطر کد دارد. دوستان عزیز! به این می‌گویند «فاجعه‌ای به نام کدنویسی!» صاحب اول و آخر این نوع کدها خودتان هستید! شاید به همین جهت باشد که عمده‌ی پروژه‌های سورس باز پس از اینکه برنامه نویس اصلی از توسعه‌ی آن دست می‌کشد، «می‌میرند». چون کسی جرات نمی‌کند به این کدها دست بزند. مشخص نیست الان این قسمت را که تغییر دادم، کجای برنامه به هم ریخت. تستی ندارند. ساختاری را نمی‌توان از آن‌ها دریافت. منطق قسمت‌های مختلف برنامه از هم جدا نشده است. برنامه یک فرم است با چند هزار سطر کد در یک فایل! کار شما شبیه به کد اسمبلی چند هزار سطری حاصل از decompile یک برنامه که نباید باشد!
به همین جهت قصد دارم یک سری «ساده» Refactoring را در این سایت ارائه دهم. روی سادگی هم تاکید کردم، چون اگر عموم برنامه نویس‌ها با همین موارد به ظاهر ساده آشنایی داشتند، کیفیت کد نویسی بهتری را می‌شد در نتایج عمومی شده، شاهد بود.
این مورد در راستای نظر سنجی انجام شده هم هست؛ درخواست مقالات خالص سی شارپ در صدر آمار فعلی قرار دارد.



Refactoring چیست؟

Refactoring به معنای بهبود پیوسته کیفیت کدهای نوشته شده در طی زمان است؛ بدون ایجاد تغییری در عملکرد اصلی برنامه. به این ترتیب به کدهایی دست خواهیم یافت که قابلیت آزمون پذیری بهتری داشته، در مقابل تغییرات مقاوم و شکننده نیستند و همچنین امکان به اشتراک گذاری قسمت‌هایی از آن‌ها در پروژه‌های دیگر نیز میسر می‌شود.


قسمت اول - مجموعه‌ها را کپسوله کنید

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

namespace Refactoring.Day1.EncapsulateCollection
{
public class OrderItem
{
public int Id { set; get; }
public string Name { set; get; }
public int Amount { set; get; }
}
}

using System.Collections.Generic;

namespace Refactoring.Day1.EncapsulateCollection
{
public class Orders
{
public List<OrderItem> OrderItems { set; get; }
}
}

نکته اول: هر کلاس باید در داخل یک فایل جدا قرار گیرد. «لطفا» یک فایل درست نکنید با 50 کلاس داخل آن. البته اگر باز هم یک فایل باشد که بتوان 50 کلاس را داخل آن مشاهده کرد که چقدر هم عالی! نه اینکه یک فایل باشد تا بعدا 50 کلاس را با Refactoring از داخل آن بیرون کشید!

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

Error, Certainty 95, for Do Not Expose Generic Lists

بله. لیست‌های جنریک را نباید به همین شکل در اختیار مصرف کننده قرار داد؛ چون به این صورت هر کاری را می‌توانند با آن انجام دهند، مثلا کل آن را تعویض کنند، بدون اینکه کلاس تعریف کننده آن از این تغییرات مطلع شود.
پیشنهاد FxCop این است که بجای List از Collection یا IList و موارد مشابه استفاده شود. اگر اینکار را انجام دهیم اینبار به خطای زیر خواهیم رسید:

Warning, Certainty 75, for Collection Properties Should Be ReadOnly

FxCop پیشنهاد می‌دهد که مجموعه تعریف شده باید فقط خواندنی باشد.

چکار باید کرد؟
بجای استفاده از List جهت ارائه مجموعه‌ها، از IEnumerable استفاده کنید و اینبار متدهای Add و Remove اشیاء به آن‌را به صورت دستی تعریف نمائید تا بتوان از تغییرات انجام شده بر روی مجموعه ارائه شده، در کلاس اصلی آن مطلع شد و امکان تعویض کلی آن‌را از مصرف کننده گرفت. برای مثال:

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

namespace Refactoring.Day1.EncapsulateCollection
{
public class Orders
{
private int _orderTotal;
private List<OrderItem> _orderItems;

public IEnumerable<OrderItem> OrderItems
{
get { return _orderItems; }
}

public void AddOrderItem(OrderItem orderItem)
{
_orderTotal += orderItem.Amount;
_orderItems.Add(orderItem);
}

public void RemoveOrderItem(OrderItem orderItem)
{
var order = _orderItems.Find(o => o == orderItem);
if (order == null) return;

_orderTotal -= orderItem.Amount;
_orderItems.Remove(orderItem);
}
}
}


اکنون اگر برنامه را مجددا با fxCop آنالیز کنیم، دو خطای ذکر شده دیگر وجود نخواهند داشت. اگر این تغییرات صورت نمی‌گرفت، امکان داشتن فیلد _orderTotal غیر معتبری در کلاس Orders به شدت بالا می‌رفت. زیرا مصرف کننده مجموعه OrderItems می‌توانست به سادگی آیتمی را به آن اضافه یا از آن حذف کند، بدون اینکه کلاس Orders از آن مطلع شود یا اینکه بتواند عکس العمل خاصی را بروز دهد.


مطالب
EF Code First #8

ادامه بحث بررسی جزئیات نحوه نگاشت کلاس‌ها به جداول، توسط EF Code first


استفاده از Viewهای SQL Server در EF Code first

از Viewها عموما همانند یک جدول فقط خواندنی استفاده می‌شود. بنابراین نحوه نگاشت اطلاعات یک کلاس به یک View دقیقا همانند نحوه نگاشت اطلاعات یک کلاس به یک جدول است و تمام نکاتی که تا کنون بررسی شدند، در اینجا نیز صادق است. اما ...
الف) بر اساس تنظیمات توکار EF Code first، نام مفرد کلاس‌ها، حین نگاشت به جداول، تبدیل به اسم جمع می‌شوند. بنابراین اگر View ما در سمت بانک اطلاعاتی چنین تعریفی دارد:
Create VIEW EmployeesView
AS
SELECT id,
FirstName
FROM Employees

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

using System.ComponentModel.DataAnnotations;

namespace EF_Sample04.Models
{
[Table("EmployeesView")]
public class EmployeesView
{
public int Id { set; get; }
public string FirstName { set; get; }
}
}

در اینجا به کمک ویژگی Table، نام دقیق این View را در بانک اطلاعاتی مشخص کرده‌ایم. به این ترتیب تنظیمات توکار EF بازنویسی خواهد شد و دیگر به دنبال EmployeesViews نخواهد گشت؛ یا جدول متناظر با آن‌را به صورت خودکار ایجاد نخواهد کرد.
ب) View شما نیاز است دارای یک فیلد Primary key نیز باشد.
ج) اگر از مهاجرت خودکار توسط MigrateDatabaseToLatestVersion استفاده کنیم، پیغام خطای زیر را دریافت خواهیم کرد:

There is already an object named 'EmployeesView' in the database.

علت این است که هنوز جدول dbo.__MigrationHistory از وجود آن مطلع نشده است، زیرا یک View، خارج از برنامه و در سمت بانک اطلاعاتی اضافه می‌شود.
برای حل این مشکل می‌توان همانطور که در قسمت‌های قبل نیز عنوان شد، EF را طوری تنظیم کرد تا کاری با بانک اطلاعاتی نداشته باشد:

Database.SetInitializer<Sample04Context>(null);

به این ترتیب EmployeesView در همین لحظه قابل استفاده است.
و یا به حالت امن مهاجرت دستی سوئیچ کنید:
Add-Migration Init -IgnoreChanges
Update-Database

پارامتر IgnoreChanges سبب می‌شود تا متدهای Up و Down کلاس مهاجرت تولید شده، خالی باشد. یعنی زمانیکه دستور Update-Database انجام می‌شود، نه Viewایی دراپ خواهد شد و نه جدول اضافه‌ای ایجاد می‌گردد. فقط جدول dbo.__MigrationHistory به روز می‌شود که هدف اصلی ما نیز همین است.
همچنین در این حالت کنترل کاملی بر روی کلاس‌های Up و Down وجود دارد. می‌توان CreateTable اضافی را به سادگی از این کلاس‌ها حذف کرد.

ضمن اینکه باید دقت داشت یکی از اهداف کار با ORMs، فراهم شدن امکان استفاده از بانک‌های اطلاعاتی مختلف، بدون اعمال تغییری در کدهای برنامه می‌باشد (فقط تغییر کانکشن استرینگ، به علاوه تعیین Provider جدید، باید جهت این مهاجرت کفایت کند). بنابراین اگر از View استفاده می‌کنید، این برنامه به SQL Server گره خواهد خورد و دیگر از سایر بانک‌های اطلاعاتی که از این مفهوم پشتیبانی نمی‌کنند، نمی‌توان به سادگی استفاده کرد.



استفاده از فیلدهای XML اس کیوال سرور

در حال حاضر پشتیبانی توکاری توسط EF Code first از فیلدهای ویژه XML اس کیوال سرور وجود ندارد؛ اما استفاده از آن‌ها با رعایت چند نکته ساده، به نحو زیر است:

using System.ComponentModel.DataAnnotations;
using System.Xml.Linq;

namespace EF_Sample04.Models
{
public class MyXMLTable
{
public int Id { get; set; }

[Column(TypeName = "xml")]
public string XmlValue { get; set; }

[NotMapped]
public XElement XmlValueWrapper
{
get { return XElement.Parse(XmlValue); }
set { XmlValue = value.ToString(); }
}
}
}


در اینجا توسط TypeName ویژگی Column، نوع توکار xml مشخص شده است. این فیلد در طرف کدهای کلاس‌های برنامه، به صورت string تعریف می‌شود. سپس اگر نیاز بود به این خاصیت توسط LINQ to XML دسترسی یافت، می‌توان یک فیلد محاسباتی را همانند خاصیت XmlValueWrapper فوق تعریف کرد. نکته‌ دیگری را که باید به آن دقت داشت، استفاده از ویژگی NotMapped می‌باشد، تا EF سعی نکند خاصیتی از نوع XElement را (یک CLR Property) به بانک اطلاعاتی نگاشت کند.

و همچنین اگر علاقمند هستید که این قابلیت به صورت توکار اضافه شود، می‌توانید اینجا رای دهید!



نحوه تعریف Composite keys در EF Code first

کلاس نوع فعالیت زیر را درنظر بگیرید:

namespace EF_Sample04.Models
{
public class ActivityType
{
public int UserId { set; get; }
public int ActivityID { get; set; }
}
}

در جدول متناظر با این کلاس، نباید دو رکورد تکراری حاوی شماره کاربری و شماره فعالیت یکسانی باهم وجود داشته باشند. بنابراین بهتر است بر روی این دو فیلد، یک کلید ترکیبی تعریف کرد:

using System.Data.Entity.ModelConfiguration;
using EF_Sample04.Models;

namespace EF_Sample04.Mappings
{
public class ActivityTypeConfig : EntityTypeConfiguration<ActivityType>
{
public ActivityTypeConfig()
{
this.HasKey(x => new { x.ActivityID, x.UserId });
}
}
}

در اینجا نحوه معرفی بیش از یک کلید را در متد HasKey ملاحظه می‌کنید.

یک نکته:
اینبار اگر سعی کنیم مثلا از متد db.ActivityTypes.Find با یک پارامتر استفاده کنیم، پیغام خطای «The number of primary key values passed must match number of primary key values defined on the entity» را دریافت خواهیم کرد. برای رفع آن باید هر دو کلید، در این متد قید شوند:

var activity1 = db.ActivityTypes.Find(4, 1);

ترتیب آن‌ها هم بر اساس ترتیبی که در کلاس ActivityTypeConfig، ذکر شده است، مشخص می‌گردد. بنابراین در این مثال، اولین پارامتر متد Find، به ActivityID اشاره می‌کند و دومین پارامتر به UserId.


بررسی نحوه تعریف نگاشت جداول خود ارجاع دهنده (Self Referencing Entity)

سناریوهای کاربردی بسیاری را جهت جداول خود ارجاع دهنده می‌توان متصور شد و عموما تمام آن‌ها برای مدل سازی اطلاعات چند سطحی کاربرد دارند. برای مثال یک کارمند را درنظر بگیرید. مدیر این شخص هم یک کارمند است. مسئول این مدیر هم یک کارمند است و الی آخر. نمونه دیگر آن، طراحی منوهای چند سطحی هستند و یا یک مشتری را درنظر بگیرید. مشتری دیگری که توسط این مشتری معرفی شده است نیز یک مشتری است. این مشتری نیز می‌تواند یک مشتری دیگر را به شما معرفی کند و این سلسله مراتب به همین ترتیب می‌تواند ادامه پیدا کند.
در طراحی بانک‌های اطلاعاتی، برای ایجاد یک چنین جداولی، یک کلید خارجی را که به کلید اصلی همان جدول اشاره می‌کند، ایجاد خواهند کرد؛ اما در EF Code first چطور؟

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; }
}
}

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

using System.Data.Entity.ModelConfiguration;
using EF_Sample04.Models;

namespace EF_Sample04.Mappings
{
public class EmployeeConfig : EntityTypeConfiguration<Employee>
{
public EmployeeConfig()
{
this.HasOptional(x => x.Manager)
.WithMany()
//.HasForeignKey(x => x.ManagerID)
.WillCascadeOnDelete(false);
}
}
}

با توجه به اینکه یک کارمند می‌تواند مسئولی نداشته باشد (خودش مدیر ارشد است)، به کمک متد HasOptional مشخص کرده‌ایم که فیلد Manager_Id را که می‌خواهی به این کلاس اضافه کنی باید نال پذیر باشد. توسط متد WithMany طرف دیگر رابطه مشخص شده است.
اگر نیاز بود فیلد Manager_Id اضافه شده نام دیگری داشته باشد، یک خاصیت nullable مانند ManagerID را که در کلاس Employee مشاهده می‌کنید،‌ اضافه نمائید. سپس در طرف تعاریف نگاشت‌ها به کمک متد HasForeignKey، باید صریحا عنوان کرد که این خاصیت، همان کلید خارجی است. از این نکته در سایر حالات تعاریف نگاشت‌ها نیز می‌توان استفاده کرد، خصوصا اگر از یک بانک اطلاعاتی موجود قرار است استفاده شود و از نام‌های دیگری بجز نام‌های پیش فرض EF استفاده کرده است.




مثال‌های این سری رو از این آدرس هم می‌تونید دریافت کنید: (^)

مطالب
breeze js به همراه ایجاد سایت آگهی قسمت دوم
نصب: پکیج‌های متنوعی از breeze وجود دارند. برای ما بسته‌ی زیر بهترین انتخاب می‌باشد. با نصب پکیج زیر، breeze در سمت سرور و کلاینت، به همراه ASP.NET Web API 2.2 and Entity Framework 6 نصب می‌شود:
Install-Package Breeze.WebApi2.EF6
بعد از نصب، دو فایل جاوا اسکریپتی به پروژه اضافه میشوند: breeze.debug.js  فایل اصلی breeze می‌باشد که از Backbone و Knockout پشتیبانی می‌کند و breeze.min.js که فایل فشرده شده breeze.debug میباشد. برای بهتر کار کردن با angularjs و breezejs، کتابخانه‌ی Breeze.Angular را  نصب نمایید. یکی از مواردی که این سرویس برای ما انجام می‌دهد،interceptor ایی را برای درخواست‌های http فعال می‌کند. یکی از موارد استفاده‌ی آن، ارسال token امنیتی، قبل از درخواست‌های breeze به کنترلر میباشد:
var instance = breeze.config.initializeAdapterInstance("ajax", "angular");
instance.setHttp($http);
Install-Package Breeze.Angular
 قلب تپنده‌ی breezejs در کلاینت EntityManager است که نقش data context را در کلاینت، بازی می‌کند. به برخی از خصوصیات آن می‌پردازیم:
  var manager = new breeze.EntityManager({  
  dataService: dataService,          
  metadataStore: metadataStore,                     
  saveOptions: new breeze.SaveOptions({    allowConcurrentSaves: true, tag: [{}] })   
                 });

var dataService = new breeze.DataService({  
serviceName: "/breeze/"+ "Automobile",             
hasServerMetadata: false,
namingConvention: breeze.NamingConvention.camelCase        
});
var metadataStore = new breeze.MetadataStore({});

- serviceName: نام سرویس دهنده یا کنترلر سمت سرور میباشد. درمورد کنترلر سمت سرور کمی جلوتر بحث می‌کنیم.
- metadataStore: اطلاعاتی را در مورد تمام آبجکت‌ها (جداول دیتابیس) می‌دهد. مثل نام فیلدها، نوع فیلدها و...
برای کار با متادیتا دو راه وجود دارد:
1- متا دیتا را خودتان در سمت کلاینت ایجاد نمایید:
var myMetadataStore = new breeze.MetadataStore();
myMetadataStore.addEntityType({...});
یا برای اضافه کردن فیلد شهر به جدول customer:
 var customer = function () {
                    this.City = "";
                };
myMetadataStore.registerEntityTypeCtor("Customer", customer);
2- اطلاعات  را از سرور دریافت  نمایید. در این صورت  کنترلر شما باید دارای متد Metadata باشد. بنابراین کنترلی را در سرور به نام Automobile و با محتویات زیر ایجاد نمایید. همانطور که مشاهده می‌کنید، این کنترلر از ApiController مشتق شده است که تفاوت خاصی با Api‌‌های دیگر ندارد و تنها به BreezeController مزین شده است. این attribute به NET WebApi  کمک میکند که فیلترینگ و مرتب سازی با فرمت oData را فراهم کند و همچنین درک صحیح فرمت json را نیز به کنترلر می‌دهد.
EFContextProvider: کامپوننتی که تعامل بین کنترلر breeze با Entity Framework را ساده‌تر می‌کند و در واقع یک  wrapper بر روی دیتاکانتکس یا آبجکت کانتکس می‌باشد. یکی از وظایف آن  ارسال متا دیتا، برای کلاینت‌های breeze است.
[BreezeController]
public class AutomobileController : ApiController
    {
        readonly EFContextProvider<ApplicationDbContext> _contextProvider =
        new EFContextProvider<ApplicationDbContext>();
        [HttpGet]
        public string Metadata()
        {
            return _contextProvider.Metadata();
        }
        [HttpGet]
        public IQueryable<Customer> Customers() {
           return _contextProvider.Context.Customers;
        }

        [System.Web.Http.HttpPost]
        public SaveResult SaveChanges(JObject saveBundle)
        {
           _contextProvider.BeforeSaveEntitiesDelegate = BeforeSaveEntities;
           _contextProvider.AfterSaveEntitiesDelegate = afterSaveEntities;
            return _contextProvider.SaveChanges(saveBundle);
        }
protected Dictionary<Type, List<EntityInfo>> BeforeSaveEntities(Dictionary<Type, List<EntityInfo>> saveMap)
        {
        }
private void afterSaveEntities(Dictionary<Type, List<EntityInfo>> saveMap, List<KeyMapping> keyMappings)
        {
        }
    }
در اینجا متدی مانند Customers، از طریق کلاینت‌های breeze قابل دسترسی می‌باشد.

- saveOptions: نحوه‌ی چگونگی برخورد با ذخیره کردن اطلاعات را مشخص می‌کند. با ذخیره سازی تغییرات، متد SaveChanges سمت سرور فراخوانی می‌شود. در breeze می‌توان به قبل و بعد از ذخیره سازی اطلاعات دسترسی داشت. یکی از موارد رایج کاربرد آن، اعمال چک کردن دسترسی‌ها، قبل از ذخیره سازی می‌باشد.
برای ذخیره سازی تغییرات:
manger.saveChanges().then(function success() {
                    }, function failer(e) {
                    });
برای نادیده گرفتن تغییرات:
manger.rejectChanges()

کوئری:
بعد از تعریف Entity Manger می‌توانیم کوئری خود را اجرا نماییم. کوئری ما شامل گرفتن اطلاعات از جدول Customer، با مرتب سازی بر روی فیلد آیدی می‌باشد و با اجرا کردن کوئری می‌توانیم موفقیت یا عدم موفقیت آن‌را بررسی نماییم. 
   var query = breeze.EntityQuery
            .from("Customer")   
            .orderBy("Id");
   var result= manager.executeQuery(query);
   result.then(querySucceeded)
    .fail(queryFailed);

   query = query.where("Id", "==", 1)
با نوشتن Predicate تکی یا ترکیب آنها نیز می‌توان شرط‌های پیچیده‌تری را ایجاد کرد:
var predicate = new breeze.Predicate("Id", "==", false);
query = query.where(predicate)

var p1 = new breeze.Predicate("IsArchived", "==", false);
var p2 = breeze.Predicate("IsDone", "==", false); 
var predicate = p1.and(p2);
query = query.where(predicate).orderBy("Id")  
در اینجا خروجی مشابه زیر برای کنترلر ارسال میشود:
?$filter=IsArchived eq false&IsDone eq false  &$orderby=Id

اعتبارسنجی
:اعتبارسنجی در breeze، هم در سمت کلاینت و هم در سمت سرور امکان پذیر می‌باشد که در مثالی، در قسمت بعدی، validator سفارشی خودمان را خواهیم ساخت و به entity مورد نظر اعمال خواهیم کرد.
breeze دارای یک سری Validator در سطح پراپرتی‌ها است:
- برای انواع اقسام dataType ها مانند Int,string,..
- برای نیازهای رایجی چون: emailAddress,creditCard,maxLength,phone,regularExpression,required,url 
هم چنین در breeze امکان تغییر دادن اعتبارسنجی‌های پیش فرض نیز وجود دارند. برای مثال برای اینکه در فیلدهای required بتوان متن خالی هم وارد کرد، از دستور زیر می‌توان استفاده کرد:
breeze.Validator.required({ allowEmptyStrings: true });

ردیابی تغییرات
: هر آیتم Entity دارای EntityAspect است که وضعیت آن‌را مشخص می‌کند و می‌تواند یکی از وضعیت‌های Added،Modified،Deleted،Detached،Unchanged باشد. با مشخص کردن حالت هر آیتم، با فراخوانی SaveChanges تغییرات بر روی دیتابیس اعمال می‌گردد.
ایجاد آیتم جدید:
manager.createEntity('Customer', jsonValue);
 ویرایش اطلاعات:
manager.createEntity("Customer", jsonValue, breeze.EntityState.Modified, breeze.MergeStrategy.OverwriteChanges)
 حذف اطلاعات:
manager.createEntity("Customer", item, breeze.EntityState.Deleted)

برای اشنایی بیشتر با امکانات Breeze، قصد داریم یک سایت ایجاد آگهی را راه اندازی کنیم. پیش نیازهای ضروری این بخش typescript ،angularjs ،requirejs هستند. قصد داریم سایتی را برای آگهی‌های خرید و فروش خودرو، مشابه با سایت باما ایجاد نماییم:

امکانات این سایت:
- ثبت نام کاربران 
- ثبت آگهی توسط کاربران 
- ایجاد برچسب‌های آگهی‌ها 
- امتیاز دهی به آگهی‌ها
- جستجوی آگهی‌ها
- و....
ابتدا نصب پکیج‌های زیر 
Install-Package angularjs
Install-Package angularjs.TypeScript.DefinitelyTyped

Install-Package bootstrap
Install-Package bootstrap.TypeScript.DefinitelyTyped

Install-Package jQuery
Install-Package jquery.TypeScript.DefinitelyTyped

Install-Package RequireJS
Install-Package requirejs.TypeScript.DefinitelyTyped

bower install angularAMD

مدلهای برنامه:
ایجاد کلاس BaseEntity 
 public  class BaseEntity
    {
        public int Id { get; set; }
        public bool Status { get; set; }
        public DateTime CreatedDateTime { get; set; }
    }
ایجاد جدول آگهی
    public class Ad : BaseEntity
    {
        public string Title { get; set; }
        public float Price { get; set; }
        public double Rating { get; set; }
        public int? RatingNumber { get; set; }
        public string UserId { get; set; }
        public DateTime ModifieDateTime { get; set; }
        public string Description { get; set; }
        public virtual ICollection<Comment> Comments { get; set; }
        public virtual IdentityUser User { get; set; }
        public virtual ICollection<AdLabel> Labels { get; set; }
        public virtual ICollection<AdMedia> Medias { get; set; }
    }
ایجاد جدول برچسب 
public class Label 
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public int? ParentId { get; set; }
        public virtual Label Parent { get; set; }
        public virtual ICollection<Label> Items { get; set; }
    }
ایجاد جدول مدیا
 public class Media 
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string MimeType { get; set; }
    }
ایجاد جدول واسط برچسب‌های آگهی
  public class AdLabel
    {
        public int Id { get; set; }
        public virtual Ad Ad { get; set; }
        public virtual Label Label { get; set; }
        [Index("IX_AdLabel", 1, IsUnique = true)]
        public int AdId { get; set; }
        [Index("IX_AdLabel", 2, IsUnique = true)]
        public int LabelId { get; set; }
        public string Value { get; set; }
    }
ایجاد جدول واسط مدیا‌های مرتبط با آگهی
 public class AdMedia
    {
        public int Id { get; set; }
        public virtual Ad Ad { get; set; }
        public virtual Media Media { get; set; }
        [Index("IX_AdMedia", 1, IsUnique = true)]
        public int AdId { get; set; }
        [Index("IX_AdMedia", 2, IsUnique = true)]
        public int MediaId { get; set; }
    }
ایجاد جدول کامنت‌ها
  public class Comment : BaseEntity
    {
        public string Body { get; set; }
        public double Rating { get; set; }
        public int? RatingNumber { get; set; }
        public string EntityName { get; set; }
        public string UserId { get; set; }
        public int? ParentId { get; set; }
        public int? AdId { get; set; }
        public virtual Comment Parent { get; set; }
        public virtual Ad Ad { get; set; }
        public virtual ICollection<Comment> Items { get; set; }
        public virtual IdentityUser User { get; set; }
    }
ایجاد جدول اعضاء
public class Customer:BaseEntity
    {
        public string UserId { get; set; }
        public virtual string DisplayName { get; set; }
        public virtual string BirthDay { get; set; }
        public string City { get; set; }
        public string Address { get; set; }
        public int? MediaId { get; set; }
        public bool? NewsLetterSubscription { get; set; }
        public string PhoneNumber { get; set; }
        public virtual IdentityUser User { get; set; }
        public virtual Media Media { get; set; }
    }
ایجاد جدول امتیاز دهی به آگهی‌ها
public class Rating 
    {
      public int Id { get; set; }
       public string UserId { get; set; }
       public Double Rate { get; set; }
       public string EntityName { get; set; }
       public int DestinationId { get; set; }
    }

اضافه کردن مدلهای برنامه به ApplicationDbContext 
 public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public ApplicationDbContext()
            : base("DefaultConnection", throwIfV1Schema: false)
        {
        }
        public DbSet<Ad> Ads { get; set; }
        public DbSet<AdLabel> AdLabels { get; set; }
        public DbSet<AdMedia> AdMedias { get; set; }
        public DbSet<Comment> Comments { get; set; }
        public DbSet<Label> Labels { get; set; }
        public DbSet<Media> Medias { get; set; }
        public static ApplicationDbContext Create()
        {
            return new ApplicationDbContext();
        }
    }

لود کردن فایل main.js در فایل layout.cshtml ترجیحا در انتهای body
    <script src="~/Scripts/require.js" data-main="/app/main"></script>
RequireJS  کتابخانه‌ی جاوااسکریپتی برای بارگزاری فایل‌ها در صورت نیاز می‌باشد. تنها کاری که ما باید انجام بدهیم این است که کدهای خود را داخل module‌ها قرار دهیم (در فایل‌های جداگانه) و RequireJS در صورت نیاز آنها را load خواهد کرد. همچنین RequireJS وابستگی بین module‌ها را نیز مدیریت می‌کند.

ایجاد فایل main.ts 
path: مسیر فایل‌های جاوا اسکریپتی
shim: وابستگی‌های فایل‌ها(ماژول ها) و export کردن آنها را مشخص می‌کند.
requirejs.config({
    paths: {
        "app": "app",
        "angularAmd":"/Scripts/angularAmd",
        "angular": "/Scripts/angular",
        "bootstrap": "/Scripts/bootstrap",
        "angularRoute": "/Scripts/angular-route",
        "jquery": "/Scripts/jquery-2.2.2",
    },
    waitSeconds: 0,
    shim: {
        "angular": { exports: "angular" },
        "angularRoute": { deps: ["angular"] },
        "bootstrap": { deps: ["jquery"] },
        "app": {
            deps: ["bootstrap","angularRoute"]
        }
    }
});
require(["app"]);

ایجاد فایل app.ts: کارهایی که در فایل app انجام داده‌ایم:
ایجاد کنترلر SecurityCtrl و اعمال آن به تگ body
<body ng-controller="SecurityCtrl">
...
</body>
ایجاد ماژول AdApps و قرار دادن کلاس SecurityCtrl در آن. از این به بعد برای مدیریت بهتر، تمام کدهای خود را درون ماژول‌ها قرار می‌دهیم. 
"use strict";
module AdApps {
    class SecurityCtrl {
        private $scope: Interfaces.IAdvertismentScope;
        constructor($scope: Interfaces.IAdvertismentScope) {
           // security check
      this.$scope = $scope;
        }
    }
 define(["angularAmd", "angular"], (angularAmd, ng) => {
   angularAmd = angularAmd.__proto__;
        var app = ng.module("AngularTypeScript", ['ngRoute']);
        var viewPath = "app/views/";
        var controllerPath = "app/controller/";
        app.config(['$routeProvider', $routeProvider => {
                $routeProvider
                    .when("/", angularAmd.route({
                        templateUrl: viewPath + "home.html",
                        controllerUrl: controllerPath + "home .js"
                    }))
                    .otherwise({ redirectTo: '/' });
            }
        ]);
        app.controller('SecurityCtrl', ['$scope', SecurityCtrl]);
        return angularAmd.bootstrap(app);
 })}