مطالب
آزمون واحد Entity Framework به کمک چارچوب تقلید
در باب ضرورت نوشتن کدهای تست پذیر، توسعه کلاس‌های کوچک تک مسئولیتی و اهمیت تزریق وابستگی‌ها بارها و بارها بحث شده و مطلب نوشته شده است. این روز‌ها کم پیش میاید که نرم افزاری توسعه داده شود و از پایگاه داده به جهت ذخیره و بازیابی داده‌ها استفاده نکند. با گسترش و رواج ORM ها، نوشتن کدهای دسترسی به داده‌ها سهولت یافته است و استفاده از ORM در لایه‌ی سرویس که نگهدارنده‌ی منطق تجاری برنامه است، امری اجتناب ناپذیر می‌باشد. 
در این مطلب نحوه‌ی نوشتن آزمون واحد برای کلاس سرویسی که وابسته به DbContext می‌باشد، به همراه محدودیت‌ها شرح داده می‌شود.
ابتدا یک روش که که در آن مستقیما از DbContext در سرویس استفاده شده را بررسی میکنیم. در مثال زیر کلاس ProductService وظیفه‌ی برگرداندن لیست کالاها را به ترتیب نام دارد. در آن DbContext مستقیما وهله سازی شده و از آن جهت انجام تراکنش‌های دیتابیس کمک گرفته شده است:
    public class ProductService
    {
        public IEnumerable<Product> GetOrderedProducts()
        {
            using (var ctx = new Entites())
            {
                return ctx.Products.OrderBy(x => x.Name).ToList();
            }
        }
    }

برای این کلاس نمی‌توان Unit Test نوشت چرا که یک وابستگی به شی DbContext دارد و این وابستگی مستقیما درون متد GetOrderedProducts  نمونه سازی شده است. در مطالب پیشین شرح داده شد که برای تست پذیر کردن کدها باید این وابستگی‌ها را از بیرون، در اختیار کلاس مورد نظر قرار داد.
برای نوشتن تست برای کلاس ProductService حداقل دو روش در اختیار است:
- نوشتن Integration Test:
یعنی کلاس جاری را به همین شکل نگاه داریم و در تست، مستقیما به یک پایگاه داده که به منظور تست فراهم شده وصل شویم. برای سهولت مدیریت پایگاه داده می‌توان عمل درج را در یک Transaction قرار داد و پس از پایان یافتن تست Transaction را RollBack کرد. این روش مورد بحث مطلب جاری نمی‌باشد، لطفا برای آشنایی این دو مطلب را مطالعه بفرمایید:
- بهره جستن از تزریق وابستگی و نوشتن Unit Test که وابستگی به دیتابیس ندارد
یکی از قانون‌های یک آزمون واحد این است که وابستگی به منابع خارجی مثل پایگاه داده نداشته باشد. این مطلب نحوه‌ی صحیح پیاده سازی الگوی Unit of Work را شرح داده است. بعد از پیاده سازی Unit Of Work، کلاس DbContext به شرح زیر می‌شود. همانطور که مشاهده می‌کنید، اکنون DbContext یک Interface را پیاده سازی کرده است.
    public interface IUnitOfWork
    {
        IDbSet<TEntity> Set<TEntity>() where TEntity : class;
        int SaveAllChanges();
    }

    public class Entites : DbContext, IUnitOfWork
    {
        public virtual DbSet<Product> Products { get; set; }  // This is virtual because Moq needs to override the behaviour 

        public new virtual IDbSet<TEntity> Set<TEntity>() where TEntity : class   // This is virtual because Moq needs to override the behaviour 
        {
            return base.Set<TEntity>();
        }

        public int SaveAllChanges()
        {
            return base.SaveChanges();
        }
    }
در این حالت می‌توان به جای وهله سازی مستقیم DbContext در ProductService آن را خارج از کلاس سرویس در اختیار استفاده کننده قرار داد:
    public class ProductService
    {
        private readonly IDbSet<Product> _products;
        private readonly IUnitOfWork _uow;
        public ProductService(IUnitOfWork uow)
        {
            _uow = uow;
            _products = _uow.Set<Product>();
        }
     public IEnumerable<Product> GetOrderedProducts()
        {
            return _products.OrderBy(x => x.Name).ToList();
        }
    }
همانطور که مشاهده می‌کنید، الان IUnitOfWork به کلاس سرویس تزریق شده و در متدها، خبری از وهله سازی یک وابستگی (DbContext) نمی‌باشد.
اکنون برای تست این سرویس می‌توان پیاده سازی دیگری را از IUnitOfWork انجام داد و در کدهای تست به سرویس مورد نظر تزریق کرد. برای سهولت این امر قصد داریم از moq به عنوان  چارچوب تقلید (Mocking framework) استفاده کنیم. برای  نصب moq  می توان از  بسته‌ی نیوگت آن بهره جست. پیشتر  مطلبی  در رابطه با چارچوب‌های تقلید در سایت نوشته شده است.
با توجه به اینکه PoductService به دیتابیس وابستگی دارد، مقصود این است که این وابستگی با ایجاد یک نمونه‌ی mock از IUnitOfWork حذف شود. برای این منظور در سازنده‌ی کلاس، تعدادی کالای درون حافظه ایجاد شده و به صورت IQueryable جایگزین DbSet شده است.
اگر به تعریف کلاس Entities که همان DbContext می‌باشد دقت کنید، مشاهده می‌شود که Products و تابع Set، هر دو به صورت Virtual تعریف شده اند. برای تغییر رفتار DbContext نیاز است در آزمون واحد، این دو با داده‌های درون حافظه کار کنند و رفتار آنها قرار است عوض شود. این تغییر رفتار از طریق چند ریختی (Polymorphism) خواهد بود.
کلاس تست در نهایت اینگونه تعریف می‌شود:
   [TestFixture]
    public class ProductServiceTest
    {
        private readonly ProductService _productService;
        public ProductServiceTest()
        {
            IQueryable<Product> data = GetRoadNetworks().AsQueryable();
            var mockSet = new Mock<DbSet<Product>>();
            mockSet.As<IQueryable<Product>>().Setup(m => m.Provider).Returns(data.Provider);
            mockSet.As<IQueryable<Product>>().Setup(m => m.Expression).Returns(data.Expression);
            mockSet.As<IQueryable<Product>>().Setup(m => m.ElementType).Returns(data.ElementType);
            mockSet.As<IQueryable<Product>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());
            var context = new Mock<Entites>();
            context.Setup(c => c.Products).Returns(mockSet.Object);
            context.Setup(m => m.Set<Product>()).Returns(mockSet.Object);
            _productService = new ProductService(context.Object);
        }
        private IEnumerable<Product> GetRoadNetworks()
        {
            return new List<Product>
            {
                new Product
                {
                    Id = 1,
                    Name = "A"
                },
                new Product
                {
                    Id = 2,
                    Name = "B"
                },
                new Product
                {
                    Id = 3,
                    Name = "C"
                }
            };
        }
        [Test]
        public void GetOrderedProductTest()
        {
            IEnumerable<Product> products = _productService.GetOrderedProducts();
            List<string> names = products.Select(x => x.Name).ToList();
            var expected = new List<string> {"A", "B", "C"};
            CollectionAssert.AreEqual(names, expected);
        }
    }
همانطور که مشاهده می‌شود، در سازنده‌ی کلاس تست، یک منبع داده‌ی درون حافظه‌ای به صورت IQueryable تولید شده و پیاده سازی‌های تقلیدی از DbContext به همراه تابع Set و همچنین DbSet کالا‌ها به کمک Moq ایجاد گردیده و در اختیار ProductService قرار داده شده است.
در نهایت، در یک تست تلاش شده است تا منطق متد GerOrderedProducts مورد آزمون قرار گیرد.
محدودیت این روش:
با اینکه LINQ یک روش و سینتکس یکتا برای دسترسی به منابع داده‌ای مختلف را محیا می‌کند، اما این الزامی برای یکسان بودن نتایج، هنگام استفاده از Provider‌های مختلف LINQ نمی‌باشد. در تست نوشته شده از LINQ To Objects برای کوئری گرفتن از منبع داده استفاده شده است؛ در صورتیکه در برنامه‌ی اصلی از LINQ To Entities استفاده می‌شود و الزامی نیست که یک کوئری LINQ در دو Provider متفاوت یک رفتار را داشته باشد.
این نکته در قسمت Limitations of EF in-memory test doubles این مطلب هم شرح داده شده است.
در نهایت این پرسش به وجود می‌آید که با وجود محدودیت ذکر شده، از این روش استفاده شود یا خیر؟ پاسخ این پرسش، بسته به هر سناریو، متفاوت است.
به عنوان نمونه اگر در یک سناریو داده‌ها با یک کوئری نه چندان پیچیده از منبع داده ای گرفته می‌شود و اعمال دیگری دیگری روی نتیجه‌ی کوئری درون حافظه انجام می‌شود می‌توان این روش را قابل اعتماد قلمداد کرد.
برای مطالعه‌ی بیشتر مطالب متعددی در سایت در رابطه با تزریق وابستگی و آزمون‌های واحد نوشته شده است.
مطالب
ASP.NET MVC #23

اجرای برنامه‌های ASP.NET MVC توسط نگارش‌های متفاوت IIS

تا اینجا برای اجرای برنامه‌های ASP.NET MVC از وب سرور توکار VS.NET استفاده شد که صرفا جهت آزمایش برنامه‌ها طراحی شده است. تا این تاریخ سه رده از وب سرورهای مایکروسافت ارائه شده‌اند که برای نصب ASP.NET MVC می‌توانند مورد استفاده قرار گیرند و هر کدام هم نکته‌های خاص خودشان را دارند که در ادامه به بررسی آن‌ها خواهیم پرداخت.


اجرای برنامه‌های ASP.NET MVC بر روی IIS 5.x ویندوز XP

پس از ایجاد یک دایرکتوری مجازی بر روی پوشه یک برنامه ASP.NET MVC و سعی در اجرای برنامه، بلافاصله پیغام خطای HTTP 403 forbidden مشاهده می‌شود.
اولین کاری که برای رفع این مساله باید صورت گیرد، کلیک راست بر روی نام دایرکتوری مجازی در کنسول IIS، انتخاب گزینه خواص و سپس مراجعه به برگه «ASP.NET» آن است. در اینجا شماره نگارش دات نت فریم ورک مورد استفاده را به 4 تغییر دهید (برای نمونه ASP.NET MVC 3.0 مبتنی بر دات نت فریم ورک 4 است).
بعد از این تغییر، بازهم موفق به اجرای برنامه‌های ASP.NET MVC بر روی IIS 5.x نخواهیم شد؛ چون در آن زمان مفاهیم مسیریابی و Routing که اصل و پایه ASP.NET MVC هستند وجود خارجی نداشتند. این نگارش از IIS به صورت پیش فرض تنها قادر به پردازش درخواست‌های رسیده‌ای که به یک فایل فیزیکی بر روی سرور اشاره می‌کند، می‌باشد (یعنی مشکلی با اجرای برنامه‌های ASP.NET Web forms ندارد).
برای رفع این مشکل، مجددا بر روی نام دایرکتوری مجازی برنامه در کنسول IIS کلیک راست کرده و گزینه خواص را انتخاب کنید. در صفحه ظاهر شده، در برگه «Virtual directory» آن، بر روی دکمه «Configuration» کلیک نمائید. در صفحه باز شده مجددا بر روی دکمه «Add» کلیک کنید.
در صفحه باز شده، مسیر Executable را C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll وارد کرده و Extension را به .* (دات هرچی) تنظیم کنید. همین مقدار تنظیم، برای اجرای برنامه‌های ASP.NET MVC بر روی IIS 5.x ویندوز XP کفایت می‌کند.

کاری که در اینجا انجام شده است، نگاشت تمام درخواست‌های رسیده صرفنظر از پسوند فایل‌ها، به موتور ASP.NET می‌باشد. به صورت پیش فرض در IIS 5.x درخواست‌ها تنها بر اساس پسوند فایل‌ها پردازش می‌شوند. مثلا اگر فایل درخواستی aspx است، درخواست رسیده به aspnet_isapi.dll یاد شده هدایت خواهد شد. اگر پسوند فایل php است به isapi مخصوص آن (در صورت نصب) هدایت می‌گردد و به همین ترتیب برای سایر سیستم‌های دیگر. زمانیکه Extension به «دات هرچی» و Executable به aspnet_isapi.dll دات نت 4 تنظیم می‌شود، دایرکتوری مجازی تنظیم شده تنها جهت سرویس دهی به یک برنامه ASP.NET عمل خواهد کرد و تمام درخواست‌های رسیده به آن، به موتور اجرایی ASP.NET هدایت می‌شوند.

بدیهی است تنظیمات فوق تنها به یک دایرکتوری مجازی اعمال شدند. اگر نیاز باشد تا بر روی تمام سایت‌ها تاثیر گذار شود، اینبار در کنسول IIS 5.x بر روی «Default web site» کلیک راست کرده و گزینه خواص را انتخاب کنید. در صفحه باز شده به برگه «Home directory» مراجعه کرده و مراحل ذکر شده را تکرار کنید.

مشکل! این روش بهینه نیست.
روش فوق خوبه، کار می‌کنه، اما بهینه نیست؛ از این جهت که «نگاشت تمام درخواست‌ها به موتور ASP.NET» یعنی پروسه پردازش درخواست یک فایل تصویری، js یا css هم باید از فیلتر موتور ASP.NET عبور کند که ضروری نیست.
برای رفع این مشکل، توصیه شده است که سیستم مسیریابی ASP.NET MVC را در IIS 5.x «پسوند دار» کنید. به این نحو که با مراجعه به فایل Global.asax.cs، تعاریف مسیریابی را به نحو زیر ویرایش کنید:

public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add(
new Route("{controller}.aspx/{action}/{id}", new MvcRouteHandler())
{
Defaults = new RouteValueDictionary(new
{
controller = "Home",
action = "Index",
id = UrlParameter.Optional
})
});



اینبار برای مثال مسیر http://localhost/MyMvcApp/home.aspx/index به علت داشتن پسوند aspx وارد موتور پردازشی ASP.NET خواهد شد. البته در این حالت URL های تمیز ASP.NET MVC را از دست خواهیم داد و مدام باید دقت داشت که مسیرهای کنترلرها حتما باید به aspx ختم شوند. ضمنا با این تنظیم، دیگر نیازی به تغییر تعاریف نگاشت‌ها در کنسول مدیریتی IIS، نخواهد بود.


اجرای برنامه‌های ASP.NET MVC بر روی IIS 6.x ویندوز سرور 2003

تمام نکات عنوان شده جهت IIS 5.x در IIS 6.x نیز صادق هستند. به علاوه برای اجرای برنامه‌های ASP.NET بر روی IIS 6.x باید به دو نکته مهم دیگر نیز دقت داشت:
الف) ASP.NET 4 به صورت پیش فرض در IIS 6.x غیرفعال است که باید با مراجعه به قسمت Web Services Extensions در کنسول مدیریتی IIS، آن‌را از حالت prohibited خارج کرد.
ب) در هر Application pool تنها از یک نگارش دات نت فریم ورک می‌توان استفاده کرد. برای مثال اگر هم اکنون AppPool1 مشغول سرویس دهی به یک سایت ASP.NET 3.5 است، از آن نمی‌توانید جهت اجرای برنامه‌های ASP.NET MVC 3 به بعد استفاده کنید. زیرا برای مثال ASP.NET MVC 3 مبتنی بر دات نت فریم ورک 4 است. به همین جهت حتما نیاز است تا یک Application pool مجزا را برای برنامه‌های دات نت 4 در IIS 6 اضافه نمائید و سپس در تنظیمات سایت، از این Application pool جدید استفاده نمائید.
البته روش صحیح و اصولی کار با IIS از نگارش 6 به بعد هم مطابق شرحی است که عنوان شد. برای دستیابی به بهترین کارآیی و امنیت بیشتر، بهتر است به ازای هر سایت، از یک Application pool مجزا استفاده نمائید.

اطلاعات تکمیلی:
نکات نصب برنامه‌های ASP.NET 4.0 بر روی IIS 6
مروری بر تاریخچه محدودیت حافظه مصرفی برنامه‌های ASP.NET در IIS



اجرای برنامه‌های ASP.NET MVC بر روی IIS 7.x ویندوز 7 و ویندوز سرور 2008

اگر برنامه ASP.NET MVC در IIS 7.x در حالت یکپارچه (integrated mode) اجرا شود، بدون نیاز به هیچگونه تغییری در تنظیمات سرور یا برنامه، بدون مشکل قابل اجرا خواهد بود. بدیهی است در اینجا نیز بهتر است به ازای هر برنامه، یک Application pool مجزا را ایجاد کرد.
اما در حالت classic (که برای برنامه‌های جدید توصیه نمی‌شود) نیاز است همان مراحل IIS 5,x تکرار شود. البته اینبار مسیر زیر را باید طی کرد تا به صفحه افزودن نگاشت‌ها رسید:
Right-click on a web site -> Properties -> Home Directory tab -> click on the Configuration button -> Mappings tab



نکته‌ای مهم در تمام نگارش‌های IIS

ترتیب نصب دات نت فریم ورک 4 و IIS مهم است. اگر ابتدا IIS نصب شود و سپس دات نت فریم ورک 4، به صورت خودکار، کار نگاشت اطلاعات ASP.NET به IIS صورت خواهد گرفت.
اگر ابتدا دات نت فریم ورک 4 نصب شود و سپس IIS، برای مثال دیگر از برگه ASP.NET در IIS 6.x خبری نخواهد بود. برای رفع این مشکل دستور زیر را در خط فرمان اجرا کنید:

C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319\aspnet_regiis.exe /i

به این ترتیب، اطلاعات مرتبط با موتور ASP.NET مجددا به تنظیمات IIS اضافه خواهند شد.


مطالب
نگاهی به هویت سنجی کاربران در ASP.NET MVC 5
در مقاله پیش رو، سعی شده‌است به شکلی تقریبا عملی، کلیاتی در مورد Authentication در MVC5 توضیح داده شود. هدف روشن شدن ابهامات اولیه در هویت سنجی MVC5 و حل شدن مشکلات اولیه برای ایجاد یک پروژه است.
در MVC 4 برای دسترسی به جداول مرتبط با اعتبار سنجی (مثلا لیست کاربران) مجبور به استفاده از متدهای از پیش تعریف شده‌ی رفرنس‌هایی که برای آن نوع اعتبار سنجی وجود داشت، بودیم. راه حلی نیز برای دسترسی بهتر وجود داشت و آن هم ساختن مدل‌های مشابه آن جدول‌ها و اضافه کردن چند خط کد به برنامه بود. با اینکار دسترسی ساده به Roles و Users برای تغییر و اضافه کردن محتوای آنها ممکن می‌شد. در لینک زیر توضیحاتی در مورد روش اینکار وجود دارد.
 در MVC5 داستان کمی فرق کرده است. برای درک موضوع پروژه ای بسازید و حالت پیش فرض آن را تغییر ندهید و آن را اجرا کنید و ثبت نام را انجام دهید، بلافاصله تصویر زیر در دیتابیس نمایان خواهد شد.

دقت کنید بعد از ایجاد پروژه در MVC5 دو پکیج بصورت اتوماتیک از طریق Nuget به پروژه شما اضافه میشود:
 Microsoft.AspNet.Identity.Core
Microsoft.AspNet.Identity.EntityFrameWork
عامل اصلی تغییرات جدید، همین دو پکیج فوق است.
 اولین پکیج شامل اینترفیس‌های IUser و IRole است که شامل فیلدهای مرتبط با این دو می‌باشد. همچنین اینترفیسی به نام IUserStore وجود دارد که چندین متد داشته و وظیفه اصلی هر نوع اضافه و حذف کردن یا تغییر در کاربران، بر دوش آن است.
 دومین پکیج هم وظیفه پیاده سازی آن‌چیزی را دارد که در پکیج اول معرفی شده است. کلاس‌های موجود در این پکیج ابزارهایی برای ارتباط EntityFramework با دیتابیس هستند.
اما از مقدمات فوق که بگذریم برای درک بهتر رفتار با دیتابیس یک مثال را پیاده سازی خواهیم کرد.

 فرض کنید میخواهیم چنین ارتباطی را بین سه جدول در دیتابیس برقرار کنیم، فقط به منظور یادآوری، توجه کنید که جدول ASPNetUsers جدولی است که به شکل اتوماتیک پیش از این تولید شد و ما قرار است به کمک یک جدول واسط (AuthorProduct) آن را به جدول Product مرتبط سازیم تا مشخص شود هر کتاب (به عنوان محصول) به کدام کاربر (به عنوان نویسنده) مرتبط است.
 بعد از اینکه مدل‌های مربوط به برنامه خود را ساختیم، اولا نیاز به ساخت کلاس کانتکست نداریم چون خود MVC5 کلاس کانتکست را دارد؛ ثانیا نیاز به ایجاد مدل برای جداول اعتبارسنجی نیست، چون کلاسی برای فیلدهای اضافی ما که علاقمندیم به جدول Users اضافه شود، از پیش تعیین گردیده است.

دو کلاسی که با فلش علامت گذاری شده اند، تنها فایل‌های موجود در پوشه مدل، بعد از ایجاد یک پروژه هستند. فایل IdentityModel را به عنوان فایل کانتکست خواهیم شناخت (چون یکی از کلاسهایش Context است). همانطور که پیش از این گفتیم با وجود این فایل نیازی به ایجاد یک کلاس مشتق شده از DbContext نیست. همانطور که در کد زیر میبینید این فایل دارای دو کلاس است:
namespace MyShop.Models
{
    // You can add profile data for the user by adding more properties to your ApplicationUser class, please visit http://go.microsoft.com/fwlink/?LinkID=317594 to learn more.
    public class ApplicationUser : IdentityUser
    {
    }

    public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public ApplicationDbContext()
            : base("DefaultConnection")
        {
        }
      
}
کلاس اول همان کلاسی است که اگر به آن پراپرتی اضافه کنیم، بطور اتوماتیک آن پراپرتی به جدول ASPNetUsers در دیتابیس اضافه می‌شود و دیگر نگران فیلدهای نداشته‌ی جدول کاربران ASP.NET نخواهیم بود. مثلا در کد زیر چند عنوان به این جدول اضافه کرده ایم.
namespace MyShop.Models
{
    // You can add profile data for the user by adding more properties to your ApplicationUser class, please visit http://go.microsoft.com/fwlink/?LinkID=317594 to learn more.
    public class ApplicationUser : IdentityUser
    {
        [Display(Name = "نام انگلیسی")]
        public string EnglishName { get; set; }

        [Display(Name = "نام سیستمی")]
        public string NameInSystem { get; set; }

        [Display(Name = "نام فارسی")]
        public string PersianName { get; set; }

        [Required]
        [DataType(DataType.EmailAddress)]
        [Display(Name = "آدرس ایمیل")]
        public string Email { get; set; }
     }
}
کلاس دوم نیز محل معرفی مدلها به منظور ایجاد در دیتابیس است. به ازای هر مدل یک جدول در دیتابیس خواهیم داشت. مثلا در شکل فوق سه پراپرتی به جدول کاربران اضافه میشود. دقت داشته باشید با اینکه هیچ مدلی برای جدول کاربران نساخته ایم اما کلاس ApplicatioUsers کلاسی است که به ما امکان دسترسی به مقادیر این جدول را می‌دهد(دسترسی به معنای اضافه و حذف وتغییر مقادیر این جدول است) (در MVC4 به کمک کلاس membership کارهای مشابهی انجام میدادیم)
 در ساختن مدل هایمان نیز اگر نیاز به ارتباط با جدول کاربران باشد، از همین کلاس فوق استفاده میکنیم. کلاس واسط(مدل واسط) بین AspNetUsers و Product در کد زیر زیر نشان داده شده است :
namespace MyShop.Models
{
    public class AuthorProduct
    {
        [Key]
        public int AuthorProductId { get; set; }
       /* public int UserId { get; set; }*/

        [Display(Name = "User")]
        public string ApplicationUserId { get; set; }

        public int ProductID { get; set; }

        public virtual Product Product { get; set; }
    
        public virtual ApplicationUser ApplicationUser { get; set; }
    }
}
همانطور که مشاهده میکنید، به راحتی ارتباط را برقرار کردیم و برای برقراری این ارتباط از کلاس ApplicationUser استفاده کردیم. پراپرتی ApplicationUserId نیز فیلد ارتباطی ما با جدول کاربران است. جدول product هم نکته خاصی ندارد و به شکل زیر مدل خواهد شد.
namespace MyShop.Models
{
    [DisplayName("محصول")]
    [DisplayPluralName("محصولات")]
    public class Product
    {
        [Key]
        public int ProductID { get; set; }

        [Display(Name = "گروه محصول")]
        [Required(ErrorMessage = "لطفا {0} را وارد کنید")]
        public int ProductGroupID { get; set; }

        [Display(Name = "مدت زمان")]
        public string Duration { get; set; }

   
        [Display(Name = "نام تهیه کننده")]
        public string Producer { get; set; }

        [Display(Name = "عنوان محصول")]
        [Required(ErrorMessage = "لطفا {0} را وارد کنید")]
        public string ProductTitle { get; set; }

        [StringLength(200)]
        [Display(Name = "کلید واژه")]
        public string MetaKeyword { get; set; }

        [StringLength(200)]
        [Display(Name = "توضیح")]
        public string MetaDescription { get; set; }

        [Display(Name = "شرح محصول")]
        [UIHint("RichText")]
        [AllowHtml]
        public string ProductDescription { get; set; }

        [Display(Name = "قیمت محصول")]
        [DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:#,0 ریال}")]
        [UIHint("Integer")]
        [Required(ErrorMessage = "لطفا {0} را وارد کنید")]
        public int ProductPrice { get; set; }
        [Display(Name = "تاریخ ثبت محصول")]

        public DateTime? RegisterDate { get; set; }

    }
}
به این ترتیب هم ارتباطات را برقرار کرده‌ایم و هم از ساختن یک UserProfile اضافی خلاص شدیم.
برای پر کردن مقادیر اولیه نیز به راحتی از seed موجود در Configuration.cs مربوط به migration استفاده میکنیم. نمونه‌ی اینکار در کد زیر موجود است:
protected override void Seed(MyShop.Models.ApplicationDbContext context)
        {
            context.Users.AddOrUpdate(u => u.Id,
                      new ApplicationUser() {  Id = "1",EnglishName = "MortezaDalil", PersianName = "مرتضی دلیل", UserDescription = "توضیح در مورد مرتضی", Email = "mm@mm.com", Phone = "2323", Address = "test", NationalCode = "2222222222", ZipCode = "2222222222" },
                            new ApplicationUser() { Id = "2", EnglishName = "MarhamatZeinali", PersianName = "محسن احمدی", UserDescription = "توضیح در مورد محسن", Email = "mm@mm.com", Phone = "2323", Address = "test", NationalCode = "2222222222", ZipCode = "2222222222" },
                            new ApplicationUser() { Id = "3", EnglishName = "MahdiMilani", PersianName = "مهدی محمدی", UserDescription = "توضیح در مورد مهدی", Email = "mm@mm.com", Phone = "2323", Address = "test", NationalCode = "2222222222", ZipCode = "2222222222" },
                            new ApplicationUser() { Id = "4", EnglishName = "Babak", PersianName = "بابک", UserDescription = "کاربر معمولی بدون توضیح", Email = "mm@mm.com", Phone = "2323", Address = "test", NationalCode = "2222222222", ZipCode = "2222222222" }
                     
                        );


            context.AuthorProducts.AddOrUpdate(u => u.AuthorProductId,
              new AuthorProduct() { AuthorProductId = 1, ProductID = 1, ApplicationUserId = "2" },
              new AuthorProduct() { AuthorProductId = 2, ProductID = 2, ApplicationUserId = "1" },
              new AuthorProduct() { AuthorProductId = 3, ProductID = 3, ApplicationUserId = "3" }

          );
 می‌توانیم از کلاس‌های خود Identity برای انجام روش فوق استفاده کنیم؛ فرض کنید بخواهیم یک کاربر به نام admin و با نقش admin به سیستم اضافه کنیم.
            if (!context.Users.Where(u => u.UserName == "Admin").Any())
            {
                var roleStore = new RoleStore<IdentityRole>(context);
                var rolemanager = new RoleManager<IdentityRole>(roleStore);

                var userstore = new UserStore<ApplicationUser>(context);
                var usermanager = new UserManager<ApplicationUser>(userstore);
                
                var user = new ApplicationUser {UserName = "Admin"};
                
                usermanager.Create(user, "121212");
                rolemanager.Create(new IdentityRole {Name = "admin"});
                
                usermanager.AddToRole(user.Id, "admin");
            }
   در عبارت شرطی موجود کد فوق، ابتدا چک کردیم که چنین یوزری در دیتابیس نباشد، سپس از کلاس RoleStore که پیاده سازی شده‌ی اینترفیس IRoleStore است استفاده کردیم. سازنده این کلاس به کانتکست نیاز دارد؛ پس به آن context را به عنوان ورودی می‌دهیم. در خط بعد، کلاس rolemanager را داریم که بخشی از پکیج Core است و پیش از این درباره اش توضیح دادیم ( یکی از دو رفرنسی که خوبخود به پروژه اضافه میشوند) و از ویژگی‌های Identity است. به آن آبجکتی که از RoleStore ساختیم را پاس میدهیم و خود کلاس میداند چه چیز را کجا ذخیره کند.
برای ایجاد کاربر نیز همین روند را انجام می‌دهیم. سپس یک آبجکت به نام user را از روی کلاس ApplicationUser میسازیم. برای آن پسورد 121212 سِت میکنیم و نقش ادمین را به آن نسبت میدهیم. این روش قابل تسری به تمامی بخش‌های برنامه شماست. میتوانید عملیات کنترل و مدیریت اکانت را نیز به همین شکل انجام دهید. ساخت کاربر و لاگین کردن یا مدیریت پسورد نیز به همین شکل قابل انجام است.
 بعد از آپدیت دیتابیس تغییرات را مشاهده خواهیم کرد. 
مطالب
اتریبیوت اختصاصی برای قفل کردن یک اکشن جهت جلوگیری از تداخلات درخواست‌های همزمان

در کتابخانه‌ی Microsoft AspNetCore Identity میتوان با این کد، فیلد Email را منحصر به‌فرد کرد:

//Program.cs file
builder.Services.AddIdentity<User, Role>(options =>
{
    options.User.RequireUniqueEmail = true;
}).AddEntityFrameworkStores<DatabaseContext>();

برنامه را اجرا و درخواست‌ها را یکی یکی به سمت سرور ارسال میکنیم و اگر ایمیل تکراری باشد به ما خطا میده و میگه: "ایمیل تکراری است".

ولی مشکل اینجاست که کد بالا فیلد Email رو داخل دیتابیس منحصر به‌فرد نمیکنه و فقط از سمت نرم افزار بررسی تکراری بودن ایمیل رو انجام میده. حالا اگه ما با استفاده از نرم افزارهای "تست برنامه‌های وب" مثل Apache JMeter تعداد زیادی درخواست را به سمت برنامه‌مان ارسال کنیم و بعد رکوردهای داخل جدول کاربران را نگاه کنیم، با وجود اینکه داخل نرم افزارمان پراپرتی Email را منحصر به‌فرد کرده‌ایم، ولی چندین رکورد، با یک ایمیل مشابه در داخل جدول User وجود خواهد داشت.

برای تست این سناریو، برنامه Apache JMeter را از این لینک دانلود می‌کنیم (در بخش Binaries فایل zip رو دانلود می کنیم).

نکته: داشتن jdk ورژن 8 به بالا پیش نیاز است. برای اینکه بدونید ورژن جاوای سیستمتون چنده، داخل cmd دستور java -version رو صادر کنید.

اگه تمایل به نصب، یا به روز رسانی jdk را داشتید، میتونید از این لینک استفاده کنید و بسته به سیستم عاملتون، یکی از تب‌های Windows, macOS یا Linux رو انتخاب کنید و فایل مورد نظر رو دانلود کنید (برای Windows فایل x64 Compressed Archive رو دانلود و نصب میکنیم).

حالا فایل دانلود شده JMeter رو استخراج میکنیم، وارد پوشه‌ی bin میشیم و فایل jmeter.bat رو اجرا میکنیم تا برنامه‌ی JMeter اجرا بشه.

قبل از اینکه وارد برنامه JMeter بشیم، کدهای برنامه رو بررسی می‌کنیم.

موجودیت کاربر:

public class User : IdentityUser<int>;

ویوو مدل ساخت کاربر:

public class UserViewModel
{
    public string UserName { get; set; } = null!;

    public string Email { get; set; } = null!;

    public string Password { get; set; } = null!;
}

کنترلر ساخت کاربر:

[ApiController]
[Route("/api/[controller]")]
public class UserController(UserManager<User> userManager) : Controller
{
    [HttpPost]
    public async Task<IActionResult> Add(UserViewModel model)
    {
        var user = new User
        {
            UserName = model.UserName,
            Email = model.Email
        };
        var result = await userManager.CreateAsync(user, model.Password);
        if (result.Succeeded)
        {
            return Ok();
        }
        return BadRequest(result.Errors);
    }
}

حالا وارد برنامه JMeter میشیم و اولین کاری که باید انجام بدیم این است که مشخص کنیم چند درخواست را در چند ثانیه قرار است ارسال کنیم. برای اینکار در برنامه JMeter روی TestPlan کلیک راست میکنیم و بعد:

Add -> Threads (Users) -> Thread Group

حالا باید بر روی Thread Group کلیک کنیم و بعد در بخش Number of threads (users) تعداد درخواست‌هایی را که قرار است به سمت سرور ارسال کنیم، مشخص کنیم؛ برای مثال عدد 100.

گزینه Ramp-up period (seconds) برای اینه که مشخص کنیم این 100 درخواست قرار است در چند ثانیه ارسال شوند که آن را روی 0.1 ثانیه قرار می‌دهیم تا درخواست‌ها را با سرعت بسیار زیاد ارسال کند.

الان باید مشخص کنیم چه دیتایی قرار است به سمت سرور ارسال شود:

برای اینکار باید یک Http Request اضافه کنیم. برای این منظور روی Thread Group که از قبل ایجاد کردیم، کلیک راست میکنیم و بعد:

Add -> Sampler -> Http Request

حالا روی Http Request کلیک میکنیم و متد ارسال درخواست رو که روی Get هست، به Post تغییر میدیم و بعد Path رو هم به آدرسی که قراره دیتا رو بهش ارسال کنیم، تغییر میدهیم:

https://localhost:7091/api/User

حالا پایینتر Body Data رو انتخاب میکنیم و دیتایی رو که قراره به سمت سرور ارسال کنیم، در قالب Json وارد میکنیم:

{
  "UserName": "payam${__Random(1000, 9999999)}",
  "Email": "payam@gmail.com",
  "Password": "123456aA@"
}

چون بخش UserName در پایگاه داده منحصر به‌فرد است، با این دستور:

${__Random(1000, 9999999)}

یک عدد Random رو به UserName اضافه میکنیم که دچار خطا نشیم.

حالا فقط باید یک Header رو هم به درخواستمون اضافه کنیم، برای اینکار روی Http Request که از قبل ایجاد کردیم، کلیک راست میکنیم و بعد:

Add -> Config Element -> Http Header Manager

حالا روی دکمه‌ی Add در پایین صفحه کلیک میکنیم و این Header رو اضافه میکنیم:

Name: Content-Type
Value: application/json

همچنین میتونیم یک View result رو هم اضافه کنیم تا وضعیت تمامی درخواست‌های ارسال شده رو مشاهده کنیم. برای اینکار روی Http Request که از قبل ایجاد کردیم، کلیک راست میکنیم و بعد:

Add -> Listener -> View Results Tree

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

File -> Open

حالا بر روی دکمه‌ی سبز رنگ Play در Toolbar بالا کلیک میکنیم تا تمامی درخواست ها را به سمت سرور ارسال کنه و همچنین میتونیم از طریق View result tree ببینیم که چند درخواست موفقیت آمیز و چند درخواست ناموفق انجام شده‌است.

حالا اگر وارد پایگاه داده بشیم، میبینیم که چندین رکورد، با Email یکسان، در جدول User وجود داره:

در حالیکه ایمیل رو در تنظیمات کتابخانه Microsoft AspNetCore Identity به صورت Unique تعریف کرده‌ایم:

//Program.cs file
builder.Services.AddIdentity<User, Role>(options =>
{
    options.User.RequireUniqueEmail = true;
}).AddEntityFrameworkStores<DatabaseContext>();

دلیل این مشکل این است که درخواست‌ها در قالب یک صف، یک به یک اجرا نمیشوند؛ بلکه به صورت همزمان فریم ورک ASP.NET Core برای بالا بردن سرعت اجرای درخواست‌ها از تمامی Thread هایی که در اختیارش هست استفاده می‌کند و در چندین Thread جداگانه، درخواست‌هایی رو به کنترلر User میفرسته و در نتیجه، در یک زمان مشابه، چندین درخواست ارسال میشه که آیا یک ایمیل برای مثال با مقدار payam@yahoo.com وجود داره یا خیر و در تمامی درخواست‌ها چون همزمان انجام شده، جواب خیر است. یعنی ایمیل تکراری با آن مقدار، در پایگاه داده وجود ندارد و تمامی درخواست‌هایی که همزمان به سرور رسیده‌اند، کاربر جدید را با ایمیل مشابهی ایجاد می‌کنند.

این مشکل را میتوان حتی در سایت‌های فروش بلیط نیز پیدا کرد؛ یعنی چند نفر یک صندلی را رزرو کرده‌اند و همزمان وارد درگاه پرداخت شده و هزینه‌ایی را برای آن پرداخت میکنند. اگر آن درخواست‌ها را وارد صف نکنیم، امکان دارد که یک صندلی را به چند نفر بفروشیم. این سناریو برای زمانی است که در پایگاه داده، فیلد‌ها را Unique تعریف نکرده باشیم. هر چند که اگر فیلدها را نیز Unique تعریف کرده باشیم تا یک صندلی را به چند نفر نفروشیم، در آن صورت هم برنامه دچار خطای 500 خواهد شد. پس بهتر است که حتی در زمان‌هایی هم که فیلدها را Unique تعریف میکنیم، باز هم از ورود چند درخواست همزمان به اکشن رزرو صندلی جلوگیری کنیم.

راه حل

برای حل این مشکل میتوان از Lock statement استفاده کرد که این راه حل نیز یک مشکل دارد که در ادامه به آن اشاره خواهم کرد.

Lock statement به ما این امکان رو میده تا اگر بخشی از کد ما در یک Thread در حال اجرا شدن است، Thread دیگری به آن بخش از کد، دسترسی نداشته باشد و منتظر بماند تا آن Thread کارش با کد ما تموم شود و بعد Thread جدید بتونه کد مارو اجرا کنه.

نحوه استفاده از Lock statement هم بسیار ساده‌است:

public class TestClass
{
    private static readonly object _lock1 = new();

    public void Method1()
    {
        lock (_lock1)
        {
            // Body
        }
    }
}

حالا باید کدهای خودمون رو در بخش Body اضافه کنیم تا دیگر چندین Thread به صورت همزمان، کدهای ما رو اجرا نکنند.

اما یک مشکل وجود داره و آن این است که ما نمیتوانیم در Lock statement، از کلمه کلیدی await استفاده کنیم؛ در حالیکه برای ساخت User جدید باید از await استفاده کنیم:

var result = await userManager.CreateAsync(user, model.Password);

برای حل این مشکل میتوان از کلاس SemaphoreSlim بجای کلمه‌ی کلیدی lock استفاده کرد:

[ApiController]
[Route("/api/[controller]")]
public class UserController(UserManager<User> userManager) : Controller
{
    private static readonly SemaphoreSlim Semaphore = new (initialCount: 1, maxCount: 1);

    [HttpPost]
    public async Task<IActionResult> Add(UserViewModel model)
    {
        var user = new User
        {
            UserName = model.UserName,
            Email = model.Email
        };

        // Acquire the semaphore
        await Semaphore.WaitAsync();
        try
        {
            // Perform user creation
            var result = await userManager.CreateAsync(user, model.Password);
            if (result.Succeeded)
            {
                return Ok();
            }
            return BadRequest(result.Errors);
        }
        finally
        {
            // Release the semaphore
            Semaphore.Release();
        }
    }
}

این کلاس نیز مانند lock عمل میکند، ولی توانایی‌های بیشتری را در اختیار ما قرار میدهد؛ برای مثال میتوان تعیین کرد که همزمان چند ترد میتوانند به این کد دسترسی داشته باشند؛ در حالیکه در lock statement فقط یک Thread میتوانست به کد دسترسی داشته باشد. مزیت دیگر کلاس SemaphoreSlim این است که میتوان برای اجرای کدمان Timeout در نظر گرفت تا از بلاک شدن نامحدود Thread جلوگیری کنیم.

با فراخوانی await semaphore.WaitAsync، دسترسی کد ما توسط سایر Thread ها محدود و با فراخوانی Release، کد ما توسط سایر Thread ها قابل دسترسی می‌شود.

مشکل قفل کردن Thread ها

هنگام قفل کردن Thread ها، مشکلی وجود دارد و آن این است که اگر برنامه‌ی ما روی چندین سرور مختلف اجرا شود، این روش جوابگو نخواهد بود؛ چون قفل کردن Thread روی یک سرور تاثیری در سایر سرورها جهت محدود کردن دسترسی به کد ما ندارد. اما به صورت کلی میتوان از این روش برای بخش‌هایی خاص از برنامه‌هایمان استفاده کنیم.

پیاده سازی با کمک الگوی AOP

برای اینکه کارمون راحت تر بشه، میتونیم کدهای بالا رو به یک Attribute انتقال بدیم و از اون Attribute در بالای اکشن‌هامون استفاده کنیم تا کل عملیات اکشن‌هامونو رو در یک Thread قفل کنیم:

[AttributeUsage(AttributeTargets.Method)]
public class SemaphoreLockAttribute : Attribute, IAsyncActionFilter
{
    private static readonly SemaphoreSlim Semaphore = new (1, 1);

    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        // Acquire the semaphore
        await Semaphore.WaitAsync();
        try
        {
            // Proceed with the action
            await next();
        }
        finally
        {
            // Release the semaphore
            Semaphore.Release();
        }
    }
}

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

[HttpPost]
[SemaphoreLock]
public async Task<IActionResult> Add(UserViewModel model)
{
    var user = new User
    {
        UserName = model.UserName,
        Email = model.Email
    };

    var result = await userManager.CreateAsync(user, model.Password);
    if (result.Succeeded)
    {
        return Ok();
    }
    return BadRequest(result.Errors);
}
مطالب
افزودن ASP.NET Identity به یک پروژه Web Forms
  • با نصب و اجرای Visual Studio 2013 Express for Web یا Visual Studio 2013 شروع کنید.
  • یک پروژه جدید بسازید (از صفحه شروع یا منوی فایل)
  • گزینه #Visual C و سپس ASP.NET Web Application را انتخاب کنید. نام پروژه را به "WebFormsIdentity" تغییر داده و OK کنید.

  • در دیالوگ جدید ASP.NET گزینه Empty را انتخاب کنید.

دقت کنید که دکمه Change Authentication غیرفعال است و هیچ پشتیبانی ای برای احراز هویت در این قالب پروژه وجود ندارد.


افزودن پکیج‌های ASP.NET Identity به پروژه

روی نام پروژه کلیک راست کنید و گزینه Manage NuGet Packages را انتخاب کنید. در قسمت جستجوی دیالوگ باز شده عبارت "Identity.E" را وارد کرده و  این پکیج را نصب کنید.

دقت کنید که نصب کردن این پکیج وابستگی‌ها را نیز بصورت خودکار نصب می‌کند: Entity Framework و ASP.NET Idenity Core.


افزودن فرم‌های وب لازم برای ثبت نام کاربران

یک فرم وب جدید بسازید.

در دیالوگ باز شده نام فرم را به Register تغییر داده و تایید کنید.

فایل ایجاد شده جدید را باز کرده و کد Markup آن را با قطعه کد زیر جایگزین کنید.

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Register.aspx.cs" Inherits="WebFormsIdentity.Register" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body style="
    <form id="form1" runat="server">
    <div>
        <h4 style="Register a new user</h4>
        <hr />
        <p>
            <asp:Literal runat="server" ID="StatusMessage" />
        </p>                
        <div style="margin-bottom:10px">
            <asp:Label runat="server" AssociatedControlID="UserName">User name</asp:Label>
            <div>
                <asp:TextBox runat="server" ID="UserName" />                
            </div>
        </div>
        <div style="margin-bottom:10px">
            <asp:Label runat="server" AssociatedControlID="Password">Password</asp:Label>
            <div>
                <asp:TextBox runat="server" ID="Password" TextMode="Password" />                
            </div>
        </div>
        <div style="margin-bottom:10px">
            <asp:Label runat="server" AssociatedControlID="ConfirmPassword">Confirm password</asp:Label>
            <div>
                <asp:TextBox runat="server" ID="ConfirmPassword" TextMode="Password" />                
            </div>
        </div>
        <div>
            <div>
                <asp:Button runat="server" OnClick="CreateUser_Click" Text="Register" />
            </div>
        </div>
    </div>
    </form>
</body>
</html>

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

فایل کد این فرم را باز کرده و محتویات آن را با قطعه کد زیر جایگزین کنید.

using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using System;
using System.Linq;

namespace WebFormsIdentity
{
   public partial class Register : System.Web.UI.Page
   {
      protected void CreateUser_Click(object sender, EventArgs e)
      {
         // Default UserStore constructor uses the default connection string named: DefaultConnection
         var userStore = new UserStore<IdentityUser>();
         var manager = new UserManager<IdentityUser>(userStore);

         var user = new IdentityUser() { UserName = UserName.Text };
         IdentityResult result = manager.Create(user, Password.Text);

         if (result.Succeeded)
         {
            StatusMessage.Text = string.Format("User {0} was created successfully!", user.UserName);
         }
         else
         {
            StatusMessage.Text = result.Errors.FirstOrDefault();
         }
      }
   }
}

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

کلاس IdentityUser پیاده سازی پیش فرض EntityFramework از قرارداد IUser است. قرارداد IUser تعریفات حداقلی یک کاربر در ASP.NET Identity Core را در بر می‌گیرد.

کلاس UserStore پیاده سازی پیش فرض EF از یک فروشگاه کاربر (user store) است. این کلاس چند قرارداد اساسی ASP.NET Identity Core را پیاده سازی می‌کند: IUserStore, IUserLoginStore, IUserClaimStore و IUserRoleStore.

کلاس UserManager دسترسی به API‌های مربوط به کاربران را فراهم می‌کند. این کلاس تمامی تغییرات را بصورت خودکار در UserStore ذخیره می‌کند.

کلاس IdentityResult نتیجه یک عملیات هویتی را معرفی می‌کند (identity operations).

پوشه App_Data را به پروژه خود اضافه کنید.

فایل Web.config پروژه را باز کنید و رشته اتصال جدیدی برای دیتابیس اطلاعات کاربران اضافه کنید. این دیتابیس در زمان اجرا (runtime) بصورت خودکار توسط EF ساخته می‌شود. این رشته اتصال شبیه به رشته اتصالی است که هنگام ایجاد پروژه بصورت خودکار برای شما تنظیم می‌شود.

<?xml version="1.0" encoding="utf-8"?>
<!--
  For more information on how to configure your ASP.NET application, please visit
  http://go.microsoft.com/fwlink/?LinkId=169433
  -->
<configuration>
  <configSections>
    <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
  </configSections>
   <connectionStrings>
      <add name="DefaultConnection" connectionString="Data Source=(LocalDb)\v11.0;AttachDbFilename=|DataDirectory|\WebFormsIdentity.mdf;Initial Catalog=WebFormsIdentity;Integrated Security=True"
            providerName="System.Data.SqlClient" />
   </connectionStrings>
  <system.web>
    <compilation debug="true" targetFramework="4.5" />
    <httpRuntime targetFramework="4.5" />
  </system.web>
  <entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
      <parameters>
        <parameter value="v11.0" />
      </parameters>
    </defaultConnectionFactory>
    <providers>
      <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
    </providers>
  </entityFramework>
</configuration>

همانطور که مشاهده می‌کنید نام این رشته اتصال DefaultConnection است.

روی فایل Register.aspx کلیک راست کنید و گزینه Set As Start Page را انتخاب کنید. اپلیکیشن خود را با کلیدهای ترکیبی Ctrl + F5 اجرا کنید که تمام پروژه را کامپایل نیز خواهد کرد. یک نام کاربری و کلمه عبور وارد کنید و روی Register کلیک کنید.

ASP.NET Identity از اعتبارسنجی نیز پشتیبانی می‌کند، مثلا در این مرحله می‌توانید از اعتبارسنج هایی که توسط ASP.NET Identity Core عرضه می‌شوند برای کنترل رفتار فیلد‌های نام کاربری و کلمه عبور استفاده کنید. اعتبارسنج پیش فرض کاربران (User) که UserValidator نام دارد خاصیتی با نام AllowOnlyAlphanumericUserNames دارد که مقدار پیش فرضش هم true است. اعتبارسنج پیش فرض کلمه عبور (MinimumLengthValidator) اطمینان حاصل می‌کند که کلمه عبور حداقل 6 کاراکتر باشد. این اعتبارسنج‌ها بصورت property‌ها در کلاس UserManager تعریف شده اند و می‌توانید آنها را overwrite کنید و اعتبارسنجی سفارشی خود را پیاده کنید. از آنجا که الگوی دیتابیس سیستم عضویت توسط Entity Framework مدیریت می‌شود، روی الگوی دیتابیس کنترل کامل دارید، پس از Data Annotations نیز می‌توانید استفاده کنید.


تایید دیتابیس LocalDbIdentity که توسط EF ساخته می‌شود

از منوی View گزینه Server Explorer را انتخاب کنید.

گره (DefaultConnection (WebFormsIdentity و سپس Tables را باز کنید. روی جدول AspNetUsers کلیک راست کرده و Show Table Data را انتخاب کنید.


پیکربندی اپلیکیشن برای استفاده از احراز هویت OWIN

تا این مرحله ما تنها امکان ایجاد حساب‌های کاربری را فراهم کرده ایم. حال نیاز داریم امکان احراز هویت کاربران برای ورود آنها به سایت را فراهم کنیم. ASP.NET Identity برای احراز هویت مبتنی بر فرم (forms authentication) از OWIN Authentication استفاده می‌کند. OWIN Cookie Authentication مکانیزمی برای احراز هویت کاربران بر اساس cookie‌ها و claim‌ها است (claims-based). این مکانیزم می‌تواند توسط Entity Framework روی OWIN یا IIS استفاده شود.
با چنین مدلی، می‌توانیم از پکیج‌های احراز هویت خود در فریم ورک‌های مختلفی استفاده کنیم، مانند ASP.NET MVC و ASP.NET Web Forms. برای اطلاعات بیشتر درباره پروژه Katana و نحوه اجرای آن بصورت Host Agnostic به لینک Getting Started with the Katana Project مراجعه کنید.


نصب پکیج‌های احراز هویت روی پروژه

روی نام پروژه خود کلیک راست کرده و Manage NuGet Packages را انتخاب کنید. در قسمت جستجوی دیالوگ باز شده عبارت "Identity.Owin" را وارد کنید و این پکیج را نصب کنید.

به دنبال پکیجی با نام Microsoft.Owin.Host.SystemWeb بگردید و آن را نیز نصب کنید.

پکیج Microsoft.Aspnet.Identity.Owin حاوی یک سری کلاس Owin Extension است و امکان مدیریت و پیکربندی OWIN Authentication در پکیج‌های ASP.NET Identity Core را فراهم می‌کند.

پکیج Microsoft.Owin.Host.SystemWeb حاوی یک سرور OWIN است که اجرای اپلیکیشن‌های مبتنی بر OWIN را روی IIS و با استفاده از ASP.NET Request Pipeline ممکن می‌سازد. برای اطلاعات بیشتر به OWIN Middleware in the IIS integrated pipeline مراجعه کنید.


افزودن کلاس‌های پیکربندی Startup و Authentication

روی پروژه خود کلیک راست کرده و گزینه Add و سپس Add New Item را انتخاب کنید. در قسمت جستجوی دیالوگ باز شده عبارت "owin" را وارد کنید. نام کلاس را "Startup" تعیین کرده و تایید کنید.

فایل Startup.cs را باز کنید و قطعه کد زیر را با محتویات آن جایگزین کنید تا احراز هویت OWIN Cookie Authentication پیکربندی شود.

using Microsoft.AspNet.Identity;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Owin;

[assembly: OwinStartup(typeof(WebFormsIdentity.Startup))]

namespace WebFormsIdentity
{
   public class Startup
   {
      public void Configuration(IAppBuilder app)
      {
         // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=316888
         app.UseCookieAuthentication(new CookieAuthenticationOptions
         {
            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            LoginPath = new PathString("/Login")
         });
      }
   }
}

این کلاس حاوی خاصیت OwinAttribute است که کلاس راه انداز OWIN را نشانه گذاری می‌کند. هر اپلیکیشن OWIN یک کلاس راه انداز (startup) دارد که توسط آن می‌توانید کامپوننت‌های application pipeline را مشخص کنید. برای اطلاعات بیشتر درباره این مدل، به OWIN Startup Class Detection مراجعه فرمایید.


افزودن فرم‌های وب برای ثبت نام و ورود کاربران

فایل Register.cs را باز کنید و قطعه کد زیر را وارد کنید. این قسمت پس از ثبت نام موفقیت آمیز کاربر را به سایت وارد می‌کند.
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.Owin.Security;
using System;
using System.Linq;
using System.Web;

namespace WebFormsIdentity
{
   public partial class Register : System.Web.UI.Page
   {
      protected void CreateUser_Click(object sender, EventArgs e)
      {
         // Default UserStore constructor uses the default connection string named: DefaultConnection
         var userStore = new UserStore<IdentityUser>();
         var manager = new UserManager<IdentityUser>(userStore);
         var user = new IdentityUser() { UserName = UserName.Text };

         IdentityResult result = manager.Create(user, Password.Text);

         if (result.Succeeded)
         {
            var authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
            var userIdentity = manager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie);
            authenticationManager.SignIn(new AuthenticationProperties() { }, userIdentity);
            Response.Redirect("~/Login.aspx");
         }
         else
         {
            StatusMessage.Text = result.Errors.FirstOrDefault();
         }
      }
   }
}
از آنجا که ASP.NET Identity و OWIN Cookie Authentication هر دو مبتنی بر Claims هستند، فریم ورک از برنامه نویس اپلیکیشن انتظار دارد تا برای کاربر یک آبجکت از نوع ClaimsIdentity تولید کند. این آبجکت تمام اطلاعات اختیارات کاربر را در بر می‌گیرد، مثلا اینکه کاربر به چه نقش هایی تعلق دارد. همچنین در این مرحله می‌توانید اختیارات (Claims) جدیدی به کاربر اضافه کنید.
شما با استفاده از AuthenticationManager که متعلق به OWIN است می‌توانید کاربر را به سایت وارد کنید. برای این کار شما متد SignIn را فراخوانی می‌کنید و آبجکتی از نوع ClaimsIdentity را به آن پاس می‌دهید. این کد کاربر را به سایت وارد می‌کند و یک کوکی برای او می‌سازد. این فراخوانی معادل همان FormAuthentication.SetAuthCookie است که توسط ماژول FormsAuthentication استفاده می‌شود.
روی پروژه خود کلیک راست کرده، فرم وب جدیدی با نام Login بسازید.

فایل Login.aspx را باز کنید و کد Markup آن را مانند قطعه کد زیر تغییر دهید.

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Login.aspx.cs" Inherits="WebFormsIdentity.Login" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
   <title></title>
</head>
<body style="font-family: Arial, Helvetica, sans-serif; font-size: small">
   <form id="form1" runat="server">
      <div>
         <h4 style="font-size: medium">Log In</h4>
         <hr />
         <asp:PlaceHolder runat="server" ID="LoginStatus" Visible="false">
            <p>
               <asp:Literal runat="server" ID="StatusText" />
            </p>
         </asp:PlaceHolder>
         <asp:PlaceHolder runat="server" ID="LoginForm" Visible="false">
            <div style="margin-bottom: 10px">
               <asp:Label runat="server" AssociatedControlID="UserName">User name</asp:Label>
               <div>
                  <asp:TextBox runat="server" ID="UserName" />
               </div>
            </div>
            <div style="margin-bottom: 10px">
               <asp:Label runat="server" AssociatedControlID="Password">Password</asp:Label>
               <div>
                  <asp:TextBox runat="server" ID="Password" TextMode="Password" />
               </div>
            </div>
            <div style="margin-bottom: 10px">
               <div>
                  <asp:Button runat="server" OnClick="SignIn" Text="Log in" />
               </div>
            </div>
         </asp:PlaceHolder>
         <asp:PlaceHolder runat="server" ID="LogoutButton" Visible="false">
            <div>
               <div>
                  <asp:Button runat="server" OnClick="SignOut" Text="Log out" />
               </div>
            </div>
         </asp:PlaceHolder>
      </div>
   </form>
</body>
</html>

محتوای فایل Login.aspx.cs را نیز مانند لیست زیر تغییر دهید.

using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.Owin.Security;
using System;
using System.Web;
using System.Web.UI.WebControls;

namespace WebFormsIdentity
{
   public partial class Login : System.Web.UI.Page
   {
      protected void Page_Load(object sender, EventArgs e)
      {
         if (!IsPostBack)
         {
            if (User.Identity.IsAuthenticated)
            {
               StatusText.Text = string.Format("Hello {0}!", User.Identity.GetUserName());
               LoginStatus.Visible = true;
               LogoutButton.Visible = true;
            }
            else
            {
               LoginForm.Visible = true;
            }
         }
      }

      protected void SignIn(object sender, EventArgs e)
      {
         var userStore = new UserStore<IdentityUser>();
         var userManager = new UserManager<IdentityUser>(userStore);
         var user = userManager.Find(UserName.Text, Password.Text);

         if (user != null)
         {
            var authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
            var userIdentity = userManager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie);

            authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = false }, userIdentity);
            Response.Redirect("~/Login.aspx");
         }
         else
         {
            StatusText.Text = "Invalid username or password.";
            LoginStatus.Visible = true;
         }
      }

      protected void SignOut(object sender, EventArgs e)
      {
         var authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
         authenticationManager.SignOut();
         Response.Redirect("~/Login.aspx");
      }
   }
}
  • متد Page_Load حالا وضعیت کاربر جاری را بررسی می‌کند و بر اساس وضعیت Context.User.Identity.IsAuthenticated تصمیم گیری می‌کند. 
نمایش نام کاربر جاری: فریم ورک ASP.NET Identity روی  System.Security.Principal.Identity  متدهایی نوشته است که به شما امکان دریافت نام و شناسه کاربر جاری را می‌دهد. این متدها در اسمبلی Microsoft.AspNet.Identity.Core وجود دارند. این متدها جایگزین  HttpContext.User.Identity.Name  هستند.
  • متد SignIn
این متد، متد CreateUser_Click را که پیشتر بصورت خودکار ایجاد شده جایگزین می‌کند و پس از ایجاد موفقیت آمیز حساب کاربری، کاربر جاری را به سایت وارد می‌کند. فریم ورک OWIN متدهایی روی System.Web.HttpContext افزوده است که به شما این امکان را می‌دهند که یک ارجاع از نوع IOwinContext بگیرید. این متد‌ها در اسمبلی Microsoft.Owin.Host.SystemWeb وجود دارند. کلاس OwinContext خاصیتی از نوع IAuthenticationManager دارد که امکانات احراز هویت موجود برای درخواست جاری را معرفی می‌کند.
  • پروژه را با Ctrl + F5 اجرا کنید و کاربر جدیدی بسازید. پس از وارد کردن نام کاربری و کلمه عبور و کلیک کردن دکمه Register باید بصورت خودکار به سایت وارد شوید و نام خود را مشاهده کنید.

  • همانطور که مشاهده می‌کنید در این مرحله حساب کاربری جدید ایجاد شده و به سایت وارد شده اید. روی Log out کلیک کنید تا از سایت خارج شوید. پس از آن باید به صفحه ورود هدایت شوید.
  • حالا یک نام کاربری یا کلمه عبور نامعتبر وارد کنید و روی Log in کلیک کنید. 
متد UserManager.Find مقدار null بر می‌گرداند، بنابراین پیام خطای "Invalid username or password" نمایش داده خواهد شد.

مطالب
اجرای Stored Procedure با چند نوع مقدار برگشتی توسط EF CodeFirst
فرض کنید Stored Procedure ی با چند مقدار برگشتی را می‌خواهیم در EF CodeFirst مورد استفاده قرار دهیم. برای مثال Stored Procedure زیر را در نظر بگیرید:
CREATE PROCEDURE [dbo].[GetAllBlogsAndPosts]
AS
    SELECT * FROM dbo.Blogs
    SELECT * FROM dbo.Posts
Stord Procedure  ی که توسط این دستور ساخته می‌شود تمام رکوردهای جدول Blogs و تمامی رکوردهای جدول Posts را واکشی کرده و به عنوان خروجی برمیگرداند (دو خروجی متفاوت). روش فراخوانی و استفاده از داده‌های این StoredProcedure در EF CodeFirst به صورت زیر است :
تعریف دو کلاس مدل Blog و Post به ترتیب  برای نگهداری اطلاعات وبلاگ‌ها و پست‌ها در زیر آمده است. در ادامه نیز تعریف کلاس BloggingContext را مشاهده می‌کنید.

public class Blog
    {
        public int BlogId { get; set; }
        public string Name { get; set; }

        public virtual List<Post> Posts { get; set; }
    }

    public class Post
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }

        public int BlogId { get; set; }
        public virtual Blog Blog { get; set; }
    }

    public class BloggingContext : DbContext
    {
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }
    }


 
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Objects;

namespace Sproc.Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var db = new BloggingContext())
            {
                 db.Database.Initialize(force: false);
               
                var cmd = db.Database.Connection.CreateCommand();
                cmd.CommandText = "[dbo].[GetAllBlogsAndPosts]";

                try
                {
                    // اجرای پروسیجر
                    db.Database.Connection.Open();
                    var reader = cmd.ExecuteReader();

                    // خواند رکوردهای blogs
                    var blogs = ((IObjectContextAdapter)db)
                        .ObjectContext
                        .Translate<Blog>(reader, "Blogs", MergeOption.AppendOnly);

                    foreach (var item in blogs)
                    {
                        Console.WriteLine(item.Name);
                    }

                    // پرش به نتایج بعدی (همان Posts)
                    reader.NextResult();
                    var posts = ((IObjectContextAdapter)db)
                        .ObjectContext
                        .Translate<Post>(reader, "Posts", MergeOption.AppendOnly);

                    foreach (var item in posts)
                    {
                        Console.WriteLine(item.Title);
                    }
                }
                finally
                {
                    db.Database.Connection.Close();
                }
            }
        }
    }
در کدهای بالا ابتدا یک Connection به بانک اطلاعاتی باز می‌شود:
 db.Database.Connection.Open();
و پس از آن نوبت به اجرای Stored Procedure می‌رسد:
 
var reader = cmd.ExecuteReader();
در کد بالا پس از اجرای Stored Procudure نتایج بدست آمده در یک reader ذخیره می‌شود. شئ reader از نوع DBDataReader می‌باشد. پس از اجرای Stored Procedure و دریافت نتایج و ذخیره سازی در شئی reader ، نوبت به جداسازی رکوردها می‌رسد. همانطور که در تعریف Stored procedure مشخص است این Stored Procedure دارای دو نوع خروجی از نوع‌های Blog و Post می‌باشد و این دو نوع باید از هم جدا شوند.برای انجام این کار از متد Translate شئی Context استفاده می‌شود. این متد قابلیت کپی کردن نتایج موجود از یک شئی DBDataReader به یک شئی از نوع مدل را دارد. برای مثال :
 
var blogs = ((IObjectContextAdapter)db)             
           .ObjectContext
           .Translate<Blog>(reader, "Blogs", MergeOption.AppendOnly);
در کدهای بالا تمامی رکوردهایی از نوع Blog از شئی reader خوانده شده و پس از تبدیل به نوع Blog درون شئی Blogs ذخیره می‌شود.
پس از آن توسط حلقه foreach محتویات Blogs پیمایش شده و مقدار موجود در  فیلد  Name نمایش داده می‌شود.
  foreach (var item in blogs)
  {
             Console.WriteLine(item.Name);
  }
با توجه به اینکه حاصل اجرای این Stored Procedure دو خروجی متفاوت بوده است ، پس از پیمایش رکوردهای Blogs باید به سراغ نتایج بعدی که همان رکوردهای Post می‌باشد برویم. برای اینکار از متد NextResult شئی reader استفاده می‌شود:
 
reader.NextResult();
در ادامه برای خواندن رکوردهایی از نوع Post نیز به همان روشی که برای Blog انجام شد عمل می‌شود.
مطالب
PowerShell 7.x - قسمت هشتم - ماژول‌ها
توسط ماژول‌ها میتوانیم یک مجموعه از دستورات را گروه‌بندی کنیم و تحت عنوان یک پکیج ارائه دهیم که برای دیگران نیز قابل استفاده باشند. برای ایجاد یک ماژول کافی است اسکریپت‌های خود را درون یک فایل با پسوند psm1 قرار دهیم؛ به این فایل اصطلاحاً root module گفته میشود. در واقع میتوان گفت ماژول‌ها یک روش مناسب برای به اشتراک‌گذاری اسکریپت‌ها میباشند. تا اینجا با کمک پروفایل‌ها توانستیم امکان استفاده مجدد از توابع و اسکریپت‌ها را داشته باشیم؛ ماژول‌ها نیز یک روش دیگر برای بارگذاری اسکریپت‌ها درون شل هستند. زمانیکه شل را باز میکنیم PowerShell به صورت خودکار یکسری مسیر را برای بارگذاری ماژول‌ها اسکن میکند. توسط متغیر env:PSModulePath$ میتوانیم لیست این مسیرها را ببینیم:  
PS /> $env:PSModulePath -Split ":"

/Users/sirwanafifi/.local/share/powershell/Modules
/usr/local/share/powershell/Modules
/usr/local/microsoft/powershell/7/Modules
همانطور که عنوان شد برای ایجاد یک ماژول کافی است اسکریپت‌های خود را داخل یک فایل با پسوند psm1 ذخیره کنیم. به عنوان مثال میتوانیم تابع Get-PingReply را درون یک فایل با نام PingModule.psm1 ذخیره و سپس توسط دستور Import-Module ماژول را ایمپورت کنیم:  
PS /> Import-Module ./PingModule.psm1
سپس توسط دستور Get-Module PingModule میتوانیم جزئیات ماژول ایمپورت شده را مشاهده نمائیم: 
PS /> Get-Module PingModule

ModuleType Version    PreRelease Name                                ExportedCommands
---------- -------    ---------- ----                                ----------------
Script     0.0                   PingModule                          Get-PingReply
به صورت پیش‌فرض تمام توابع درون اسکریپت export خواهند شد. اگر ExportedCommands خالی باشد به این معنا است که ماژول به درستی ایمپورت نشده‌است. به عنوان مثال اگر سعی کنید فایل قبل را با پسوند ps1 به عنوان ماژول ایمپورت کنید. خطایی هنگام ایمپورت کردن مشاهده نخواهید کرد و قسمت ExportedCommands خالی خواهد بود. در این‌حالت نیز امکان استفاده از تابع درون اسکریپت را خواهیم داشت؛ اما هیچ تضمینی نیست که به صورت یک ماژول به درستی عمل کند. بنابراین بهتر است ماژول‌هایی که ایجاد میکنیم حتماً پسوند psm1 داشته باشند.
PS /> Import-Module ./PingModule.ps1
PS /> Get-Module PingModule

ModuleType Version    PreRelease Name                                ExportedCommands
---------- -------    ---------- ----                                ----------------
Script     0.0                   PingModule
ممکن است بخواهیم یکسری توابع را به صورت private تعریف کنیم و فقط تعداد محدودی از توابع به صورت public باشند. در این حالت میتوانیم درون فایل psm1 با کمک دستور Export-ModuleMember اینکار را انجام دهیم: 
Function Get-PingReply {
    // as before
}

Function Get-PrivateFunction {
    Write-Debug 'This is a private function'
}

Export-ModuleMember -Function @(
    'Get-PingReply'
)
اضافه کردن متادیتا به ماژول‌ها
برای هر ماژول میتوانیم با کمک module manifest یکسری متادیتا به ماژول اضافه کنیم. این متادیتا درون یک فایل با پسوند psd1 که محتویات آن در واقع یک Hashtable است ذخیره میشود. برای ایجاد فایل manifest میتوانیم از دستور New-ModuleManifest استفاده کنیم. به عنوان مثال برای ایجاد فایل manifest برای ماژول PingModule.psm1 اینگونه عمل خواهیم کرد:  
$moduleSettings = @{
    Path                 = './PingModule.psd1'
    Description          = 'A module to ping a remote system'
    RootModule           = 'PingModule.psm1'
    ModuleVersion        = '1.0.0'
    FunctionsToExport    = @(
        'Get-PingReply'
    )
    PowerShellVersion    = '5.1'
    CompatiblePSEditions = @(
        'Core'
        'Desktop'
    )
}
New-ModuleManifest @moduleSettings
تنها پراپرتی موردنیاز برای ایجاد module manifest پراپرتی Path میباشد. این پراپرتی به فایلی که متادیتا قرار است درون آن ایجاد شود اشاره دارد. همانطور که مشاهده میکنید یکسری پراپرتی دیگر نیز اضافه کرده‌ایم و توسط splatted hash table (با کمک @) به دستور New-ModuleManifest ارسال کرده‌ایم. به این معنا که کلیدها و مقادیر درون hash table به جای اینکه یکجا به عنوان یک آبجکت به دستور موردنظر ارسال شوند، به صورت جدا پاس داده شده‌اند. اما اگر از $ استفاده میکردیم: 
$moduleSettings = @{
    Author = 'John Doe'
    Description = 'This is a sample module'
}

New-ModuleManifest $moduleSettings
با خطای زیر مواجه میشدیم: 
New-ModuleManifest : A parameter cannot be found that matches parameter name 'Author'.
در نهایت فایل manifest در مسیر تعیین شده ایجاد خواهد شد: 
#
# Module manifest for module 'PingModule'
#
# Generated by: sirwanafifi
#
# Generated on: 01/01/2023
#

@{

    # Script module or binary module file associated with this manifest.
    RootModule           = './PingModule.psm1'

    # Version number of this module.
    ModuleVersion        = '1.0.0'

    # Supported PSEditions
    CompatiblePSEditions = 'Core', 'Desktop'

    # ID used to uniquely identify this module
    GUID                 = '3f8561fc-c004-4c8e-b2fc-4a4191504131'

    # Author of this module
    Author               = 'sirwanafifi'

    # Company or vendor of this module
    CompanyName          = 'Unknown'

    # Copyright statement for this module
    Copyright            = '(c) sirwanafifi. All rights reserved.'

    # Description of the functionality provided by this module
    Description          = 'A module to ping a remote system'

    # Minimum version of the PowerShell engine required by this module
    PowerShellVersion    = '5.1'

    # Name of the PowerShell host required by this module
    # PowerShellHostName = ''

    # Minimum version of the PowerShell host required by this module
    # PowerShellHostVersion = ''

    # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
    # DotNetFrameworkVersion = ''

    # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
    # ClrVersion = ''

    # Processor architecture (None, X86, Amd64) required by this module
    # ProcessorArchitecture = ''

    # Modules that must be imported into the global environment prior to importing this module
    # RequiredModules = @()

    # Assemblies that must be loaded prior to importing this module
    # RequiredAssemblies = @()

    # Script files (.ps1) that are run in the caller's environment prior to importing this module.
    # ScriptsToProcess = @()

    # Type files (.ps1xml) to be loaded when importing this module
    # TypesToProcess = @()

    # Format files (.ps1xml) to be loaded when importing this module
    # FormatsToProcess = @()

    # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
    # NestedModules = @()

    # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
    FunctionsToExport    = 'Get-PingReply'

    # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
    CmdletsToExport      = '*'

    # Variables to export from this module
    VariablesToExport    = '*'

    # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
    AliasesToExport      = '*'

    # DSC resources to export from this module
    # DscResourcesToExport = @()

    # List of all modules packaged with this module
    # ModuleList = @()

    # List of all files packaged with this module
    # FileList = @()

    # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
    PrivateData          = @{

        PSData = @{

            # Tags applied to this module. These help with module discovery in online galleries.
            # Tags = @()

            # A URL to the license for this module.
            # LicenseUri = ''

            # A URL to the main website for this project.
            # ProjectUri = ''

            # A URL to an icon representing this module.
            # IconUri = ''

            # ReleaseNotes of this module
            # ReleaseNotes = ''

            # Prerelease string of this module
            # Prerelease = ''

            # Flag to indicate whether the module requires explicit user acceptance for install/update/save
            # RequireLicenseAcceptance = $false

            # External dependent modules of this module
            # ExternalModuleDependencies = @()

        } # End of PSData hashtable

    } # End of PrivateData hashtable

    # HelpInfo URI of this module
    # HelpInfoURI = ''

    # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix.
    # DefaultCommandPrefix = ''

}
هر ماژول باید یک آی‌دی منحصر به فرد داشته باشد که به صورت Guid توسط یک پراپرتی با همین نام تعیین میشود. برای هر پراپرتی درون این فایل توضیحی به صورت کامنت نوشته شده است؛ اما برای دیدن جزئیات کامل میتوانید به اینجا مراجعه نمائید. در اینجا RootModule به PingModule.psm1 تنظیم شده است. تنظیم این پراپرتی نحوه نمایش module type را در خروجی Get-Module مشخص میکند. این مقدار اگر به فایل ماژول (psm1) اشاره کند، نوع ماژول script در نظر گرفته میشود، اگر به یک DLL اشاره کند به binary و در نهایت اگر به یک فایلی با پسوند دیگری اشاره کند manifest در نظر گرفته میشود. همچنین درون فایل فوق یکسری پراپرتی مانند CmdletsToExport, VariablesToExport, AliasesToExport به صورت خودکار تنظیم شده‌اند. در نهایت برای تست صحت فایل میتوانیم از دستور Test-ModuleManifest استفاده کنیم: 
PS /> Test-ModuleManifest ./PingModule.psd1   

ModuleType Version    PreRelease Name                                ExportedCommands
---------- -------    ---------- ----                                ----------------
Script     1.0.0                 PingModule                          Get-PingReply
پابلیش ماژول
در نهایت توسط Publish-Module میتوانیم ماژول موردنظرمان را درون یک مخزن PowerShell پابلیش کنیم. رایج‌ترین مخزن برای انتشار پکیج‌های PowerShell یک فید نیوگت (مانند PowerShell Gallery) است. همچنین میتوانیم یک مخزن لوکال نیز برای پابلیش پکیج‌ها نیز ایجاد کنیم و خروجی نهایی را به صورت یک فایل با پسوند nupkg با دیگران به اشتراک بگذاریم. برای اینکار ابتدا نیاز است یک مخزن لوکال را به PowerShell معرفی کنیم: 
PS /> Register-PSRepository -Name 'PSLocal' `                       
>>     -SourceLocation "$(Resolve-Path $RepoPath)" `
>>     -PublishLocation "$(Resolve-Path $RepoPath)" `
>>     -InstallationPolicy 'Trusted'
در کد فوق میتوانید مقدار متغیر RepoPath را به محل مدنظر روی سیستم‌تان تنظیم کنید. ساختار ماژولی که میخواهیم پابلیش کنیم نیز اینچنین خواهد بود: 
ProjectRoot 
| -- PingModule
     | -- PingModule.psd1 
     | -- PingModule.psm1
در ادامه برای پابلیش ماژول فوق درون ProjectRoot دستور Publish-Module را اینگونه اجرا خواهیم کرد: 
PS /> Publish-Module -Path ./PingModule/ -Repository PSLocal
خروجی دستور فوق یک فایل با پسوند nupkg در مسیر مخزنی است که معرفی کردیم. همچنین با کمک دستور Find-Module میتوانیم ماژول موردنظر را لیست کنیم: 
PS /> Find-Module -Name PingModule -Repository PSLocal

Version              Name                                Repository           Description
-------              ----                                ----------           -----------
0.0.1                PingModule                          PSLocal              Get-PingReply is a.
برای نصب ماژول روی یک سیستم دیگر نیز ابتدا باید مطمئن شوید که سیستم مقصد، مخزن لوکال را تعریف کرده باشد و در نهایت توسط دستور Install-Module میتوانیم ماژول موردنظر را نصب کنیم: 
PS /> Install-Module -Name PingModule -Repository PSLocal -Scope CurrentUser
همچنین امکان پابلیش کردن ورژن‌های متفاوت را نیز خواهیم داشت: 
ProjectRoot 
| -- PingModule
     | -- 1.0.0
          | -- PingModule.psd1 
          | -- PingModule.psm1
     | -- 1.0.1
          | -- PingModule.psd1 
          | -- PingModule.psm1
برای پابلیش کردن هر کدام از ماژول‌های فوق باید به ازای هر کدام یکبار دستور Publish-Module را اجرا کنیم: 
PS /> Publish-Module -Path ./PingModule/1.0.0 -Repository PSLocal
PS /> Publish-Module -Path ./PingModule/1.0.1 -Repository PSLocal
اکنون حین نصب ماژول میتوانیم ورژن را نیز تعیین کنیم؛ در غیراینصورت آخرین ورژن یعنی 1.0.1 نصب خواهد شد: 
PS /> Install-Module -Name PingModule -Repository PSLocal -Scope CurrentUser
PS /> Get-InstalledModule -Name PingModule                                                                            
Version              Name                                Repository           Description
-------              ----                                ----------           -----------
1.0.1                PingModule                          PSLocal              Get-PingReply is a.
ساختار مناسب برای ایجاد ماژول‌ها
برای توسعه ماژول‌های PowerShell توصیه میشود که از ساختار Multi-file layout استفاده شود به این معنا که بخش‌های مختلف پروژه به قسمت‌های کوچک‌تر و قابل‌نگهداری تقسیم شوند: 
ProjectRoot 
| -- PingModule
     | -- 1.0.0
          | -- Public/
          | -- Private/
          | -- PingModule.psd1
          | -- PingModule.psm1
در اینحالت باید اسکریپت‌ها و فایل‌های موردنیاز را توسط dot sourcing درون فایل ماژول بارگذاری کنیم: 
$ScriptList = Get-ChildItem -Path $PSScriptRoot/Public/*.ps1 -Filter *.ps1

foreach ($Script in $ScriptList) {
    . $Script.FullName
}

$ScriptList = Get-ChildItem -Path $PSScriptRoot/Private/*.ps1 -Filter *.ps1

foreach ($Script in $ScriptList) {
    . $Script.FullName
}
یک مثال عملی
در ادامه میخواهیم یک ماژول تهیه کنیم که قابلیت امضاء روی PDF را با کمک کتابخانه iTextSharp.LGPLv2.Core انجام دهیم و به شکل زیر برای کاربر قابل استفاده باشد: 
PS /> Set-PDFSingature -PdfToSign "./sample_invoice.pdf" -SignatureImage "./sample_signature.jpg"
بنابراین ساختار پروژه را اینگونه ایجاد خواهیم کرد: 
ProjectRoot 
| -- SignPdf
     | -- 1.0.0
          | -- Public/
               | -- dependencies/
                    | -- BouncyCastle.Crypto.dll
                    | -- System.Drawing.Common.dll
                    | -- Microsoft.Win32.SystemEvents.dll
                    | -- iTextSharp.LGPLv2.Core.dll
               | -- Set-PDFSingature.ps1
          | -- SignPdf.psd1
          | -- SignPdf.psm1
برای استفاده از پکیج ذکر شده نیاز خواهد بود که Dllهای موردنیاز را نیز به عنوان وابستگی به پروژه اضافه کنیم (محتویات پوشه dependencies) سپس درون فایل ماژول (SignPdf.psm1) همانطور که عنوان شد میبایست اسکریپت‌ها درون پوشه Public را بارگذاری کنیم و همچنین درون تابعی که قرار است Export شود را نیز تعیین کرده‌ایم (Set-PDFSingature) 
$ScriptList = Get-ChildItem -Path $PSScriptRoot/Public/*.ps1 -Filter *.ps1

foreach ($Script in $ScriptList) {
    . $Script.FullName
}

Export-ModuleMember -Function Set-PDFSingature
در ادامه درون فایل Set-PDFSignature پیاده‌سازی را انجام خواهیم داد: 
using namespace iTextSharp.text
using namespace iTextSharp.text.pdf
using namespace System.IO

Function Set-PDFSingature {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateScript({
                if (Test-Path ([Path]::Join($(Get-Location), $_))) {
                    return $true
                }
                else {
                    throw "Signature image not found"
                }
                if ($_.EndsWith('.pdf')) {
                    return $true
                }
                else {
                    throw "File extension must be .pdf"
                }
            })]
        [string]$PdfToSign,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateScript({
                if (Test-Path ([Path]::Join($(Get-Location), $_))) {
                    return $true
                }
                else {
                    throw "Signature image not found"
                }
                if ($_.EndsWith('.png') -or $_.EndsWith('.jpg')) {
                    return $true
                }
                else {
                    throw "File extension must be .png or .jpg"
                }
            })]
        [string]$SignatureImage,
        [Parameter(Mandatory = $false, ValueFromPipeline = $true)]
        [int]$XPos = 130,
        [Parameter(Mandatory = $false, ValueFromPipeline = $true)]
        [int]$YPos = 50
    )
    Try {
        Add-Type -Path "$PSScriptRoot/dependencies/*.dll"
        $pdf = [PdfReader]::new("$(Get-Location)/$PdfToSign")
        $fs = [FileStream]::new("$(Get-Location)/$PdfToSign-signed.pdf", 
            [FileMode]::Create)
        $stamper = [PdfStamper]::new($pdf, $fs)
        $stamper.AcroFields.AddSubstitutionFont([BaseFont]::CreateFont())
        $content = $stamper.GetOverContent(1)
        $width = $pdf.GetPageSize(1).Width
        $image = [Image]::GetInstance("$(Get-Location)/$SignatureImage")
        $image.SetAbsolutePosition($width - $XPos, $YPos)
        $image.ScaleAbsolute(100, 30)
        $content.AddImage($image)
        $stamper.Close()
        $pdf.Close()
        $fs.Dispose()
    }
    Catch {
        Write-Host "Error: $($_.Exception.Message)"
    }
}
روال قرار دادن یک امضاء بر روی یک فایل PDF قبلاً در سایت توضیح داده شده است. کدهای فوق در واقع معادل PowerShell همان کدهای موجود در سایت هستند و نکته خاصی ندارند. در نهایت میتوانیم ماژول تهیه شده را روی مخزن موردنظر پابلیش کنیم: 
PS /> Publish-Module -Path ./SignPdf/1.0.0 -Repository PSLocal
برای نصب ماژول میتوانیم از دستور Install-Module استفاده کنیم: 
PS /> Install-Module -Name SignPdf -Repository PSLocal -Scope CurrentUser
در نهایت برای استفاده از ماژول ایجاد شده میتوانیم اینگونه عمل کنیم: 
PS /> Set-PDFSingature -PdfToSign "./sample_invoice.pdf" -SignatureImage "./sample_signature.jpg"
خروجی نیز فایل امضاء شده خواهد بود: 

کدهای ماژول را میتوانید از اینجا دانلود کنید. 

نظرات مطالب
Angular CLI - قسمت پنجم - ساخت و توزیع برنامه
یک نکته‌ی تکمیلی
 دستور ng serve باعث اجرای برنامه می‌شود که پورت پیش فرض آن 4200 می‌باشد. در صورتیکه قصد تغییر پورت را داشته باشیم، همانطور که در بالا هم عنوان شده باید از سوئیچ port--  استفاده کنیم. در این شرایط در هر بار اجرای برنامه باید ng serve --port 4800 را وارد کنیم. برای اینکه پورت پیش فرض را به صورت دائمی تغییر دهیم، باید فایل angular.json را ویرایش کنیم. در اینجا آبجکت serve را یافته و کلید port را با value مثلا 4800 به options اضافه می‌کنیم: 
"serve": {
  "options": {
      "browserTarget": "AngularPort:build",
      "port": 4800
   }
} 
 در این حالت، بدون نیاز به ذکر شماره پورت، در هر بار اجرا ( ng serve --port 4800 )، تنها با دستور ng serve، برنامه بر روی پورت 4800 در دسترس خواهد بود.
مطالب
SQL Antipattern #1
در این سلسله نوشتار قصد دارم ترجمه و خلاصه چندین فصل از کتاب ارزشمند ( SQL Antipatterns: Avoiding the Pitfalls of Database Programming (Pragmatic Programmers که حاصل تلاش گروه IT موسسه هدایت فرهیختگان جوان می‌باشد، را ارائه نمایم.

بخش اول : Jaywalking  


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

در این صورت، اگر پایگاه داده را به نحوی تغییر دهیم که شناسه‌ی حساب کاربران را در لیستی که با کاما از یکدیگر جدا شده‌اند ذخیره نماییم، خیلی ساده‌تر به نظر می‌رسد نسبت به اینکه بصورت جداگانه آنها را ثبت نماییم.

برنامه نویسان معمولا برای جلوگیری از ایجاد جدول واسطی [1] که رابطه‌های چند به چند زیادی دارد از یک لیست که با کاما داده‌هایش از هم جدا شده‌اند، استفاده می‌کنند. بدین جهت اسم این بخش jaywalking ,antipatten می‌باشد، زیرا jaywalking نیز عملیاتی است که از تقاطع جلوگیری می‌کند.

1.1  هدف:  ذخیره کردن چندین صفت  

طراحی کردن جدولی که ستون آن فقط یک مقدار دارد، بسیار ساده و آسان می‌باشد. شما می‌توانید نوع داده‌ایی که متعلق به ستون می‌باشد را انتخاب نمایید. مثلا از نوع (int,date…)

چگونه می‌توانیم مجموعه‌ایی از مقادیری که به یکدیگر مرتبط هستند را در یک ستون ذخیره نماییم ؟

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

CREATE TABLE Products (
product_id SERIAL PRIMARY KEY,
product_name VARCHAR(1000),
account_id BIGINT UNSIGNED,
-- . . .
FOREIGN KEY (account_id) REFERENCES Accounts(account_id)
);

INSERT INTO Products (product_id, product_name, account_id)
VALUES (DEFAULT, 'Visual TurboBuilder' , 12);

2.1 Antipattern : قالب بندی لیست هایی که با کاما از یکدیگر جدا شده اند 
برای اینکه تغییرات در پایگاه داده را به حداقل برسانید، می‌توانید نوع شناسه‌ی کاربر را از نوع varchar قرار دهید. در این صورت می‌توانید چندین شناسه‌ی کاربر که با کاما از یکدیگر جدا شده‌اند را در یک ستون ذخیره نمایید. پس در جدول محصولات، شناسه‌ی کاربران را از نوع varchar قرار می‌دهیم.  
CREATE TABLE Products (
product_id SERIAL PRIMARY KEY,
product_name VARCHAR(1000),
account_id VARCHAR(100), -- comma-separated list
-- . . .
);

INSERT INTO Products (product_id, product_name, account_id)
VALUES (DEFAULT, 'Visual TurboBuilder' , '12,34' );

 اکنون مشکلات کارایی و جامعیت داده‌ها را در این راه حل پیشنهادی بررسی می‌نماییم . 

بدست آوردن محصولاتی برای یک کاربر خاص

بدلیل اینکه تمامی شناسه‌ی کاربران که بصورت کلید خارجی جدول Products می‌باشند به صورت رشته در یک فیلد ذخیره شده‌اند و حالت ایندکس بودن آنها از دست رفته است، بدست آوردن محصولاتی برای یک کاربر خاص سخت می‌باشد. به عنوان مثال بدست آوردن محصولاتی که کاربری با شناسه‌ی 12 خریداری نموده به‌صورت زیر می‌باشد: 

SELECT * FROM Products WHERE account_id REGEXP '[[:<:]]12[[:>:]]' ;
بدست آوردن کاربرانی که یک محصول خاص خریداری نموده اند
 Join کردن account_id با Accounts table  یعنی جدول مرجع نا‌مناسب و غیر معمول می‌باشد. مساله مهمی که وجود دارد این است که زمانیکه بدین صورت شناسه‌ی کاربران را ذخیره می‌نماییم، ایندکس بودن آنها از بین می‌رود. کد زیر کاربرانی که محصولی با شناسه‌ی 123 خریداری کرده‌اند را محاسبه می‌کند.
SELECT * FROM Products AS p JOIN Accounts AS a
ON p.account_id REGEXP '[[:<:]]' || a.account_id || '[[:>:]]'
WHERE p.product_id = 123;

  ایجاد توابع تجمیعی [2]
مبنای چنین توابعی از قبیل sum(), count(), avg() بدین صورت می‌باشد که بر روی گروهی از سطرها اعمال می‌شوند. با این حال با کمک ترفندهایی می‌توان برخی از این توابع را تولید نماییم هر چند برای همه آن‌ها این مساله صادق نمی‌باشد. اگر بخواهیم تعداد کاربران برای هر محصول را بدست بیاوریم ابتدا باید طول لیست شناسه‌ی کاربران را محاسبه کنیم، سپس کاما را از این لیست حذف کرده و طول جدید را از طول قبلی کم کرده و با یک جمع کنیم. نمونه کد آن در زیر آورده شده است:   
SELECT product_id, LENGTH(account_id) - LENGTH(REPLACE(account_id, ',' , '' )) + 1
AS contacts_per_product
FROM Products;

ویرایش کاربرانی که یک محصول خاص خریداری نموده‌اند
ما به راحتی می‌توانیم یک شناسه‌ی جدید را به انتهای این رشته اضافه نماییم؛ فقط مرتب بودن آن را بهم میریزیم. در نمونه اسکریپت زیر گفته شده که در جدول محصولات برای محصولی با شناسه‌ی 123 بعد از کاما یک کاربر با شناسه‌ی 56 درج شود:
UPDATE Products
SET account_id = account_id || ',' || 56
WHERE product_id = 123;
برای حذف یک کاربر از لیست کافیست دو دستور sql را اجرا کرد: اولی برای fetch کردن شناسه‌ی کاربر از لیست و بعدی برای ذخیره کردن لیست ویرایش شده. بطور مثال تمامی افرادی که محصولی با شناسه‌ی 123 را خریداری کرده‌اند، از جدول محصولات انتخاب می‌کنیم. برای حذف کاربری با شناسه‌ی 34 از لیست کاربران، بر حسب کاما مقادیر لیست را در آرایه می‌ریزیم، شناسه‌ی مورد نظر را جستجو می‌کنیم و آن را حذف می‌کنیم. دوباره مقادیر درون آرایه را بصورت رشته درمیاوریم و جدول محصولات را با لیست جدید کاربران برای محصولی با شناسه‌ی 123 بروز‌رسانی می‌کنیم.
<?php
$stmt = $pdo->query(
"SELECT account_id FROM Products WHERE product_id = 123");
$row = $stmt->fetch();
$contact_list = $row['account_id' ];


// change list in PHP code
$value_to_remove = "34";
$contact_list = split(",", $contact_list);
$key_to_remove = array_search($value_to_remove, $contact_list);
unset($contact_list[$key_to_remove]);
$contact_list = join(",", $contact_list);
$stmt = $pdo->prepare(
"UPDATE Products SET account_id = ?
WHERE product_id = 123");
$stmt->execute(array($contact_list));

 اعتبارسنجی شناسه‌ی  محصولات 
به دلیل آنکه نوع فیلد account_id،varchar  می‌باشد احتمال این وجود دارد داده‌ای نامعتبر وارد نماییم چون پایگاه داده‌ها هیچ عکس العملی یا خطایی را نشان نمی‌دهد، فقط از لحاظ معنایی دچار مشکل می‌شویم. در نمونه زیر banana یک داده‌ی غیر معتبر می‌باشد و ارتباطی با شناسه‌ی کاربران ندارد. 
INSERT INTO Products (product_id, product_name, account_id)
VALUES (DEFAULT, 'Visual TurboBuilder' , '12,34,banana' );

انتخاب کردن کاراکتر جداکننده 
نکته قابل توجه این است که کاراکتری که بعنوان جد‌اکننده در نظر می‌گیریم باید در هیچکدام از داده‌های ورودی ما امکان بودنش وجود نداشته باشد .

محدودیت طول لیست 
در زمان طراحی جدول، برای هر یک از فیلد‌ها باید حداکثر طولی را قرار دهیم. به عنوان نمونه ما اگر (varchar(30 در نظر بگیریم بسته به تعداد کاراکتر‌هایی که داده‌های ورودی ما دارند می‌توانیم جدول را پر‌نماییم. اسکریپت زیر شناسه‌ی کاربرانی که محصولی با شناسه‌ی 123 را خریده‌اند، ویرایش می‌کند. با این تفاوت که با توجه به محدودیت لیست، در نمونه‌ی اولی شناسه‌ی کاربران دو کاراکتری بوده و 10داده بعنوان ورودی پذیرفته است در حالیکه در نمونه‌ی دوم بخاطر اینکه شناسه‌ی کاربران شش کاراکتری می‌باشد فقط 4 شناسه می‌توانیم وارد نماییم.
UPDATE Products SET account_id = '10,14,18,22,26,30,34,38,42,46'
WHERE product_id = 123;  
UPDATE Products SET account_id = '101418,222630,343842,467790'
WHERE product_id = 123;

3.1 موارد تشخیص این Antipattern:
سؤالات زیر نشان می‌دهند که Jaywalking antipattern  مورد استفاده قرار گرفته است:
• حداکثر پذیرش این لیست برای داده ورودی چه میزان است؟
• چه کاراکتری هرگز در داده‌های ورودی این لیست ظاهر نمی‌شود؟
4.1  مواردی که استفاده از این Antipattern مجاز است:
دی نرمال کردن کارایی را افزایش می‌دهد. ذخیره کردن شناسه‌ها در یک لیست که با کاما از یکدیگر جدا شده‌اند نمونه بارزی از دی نرمال کردن می‌باشد. ما زمانی می‌توانیم از این مدل استفاده نماییم که به جدا کردن شناسه‌ها از هم نیاز نداشته باشیم. به همین ترتیب، اگر در برنامه، شما یک لیست را از یک منبع دیگر با این فرمت دریافت نمایید، به سادگی آن را در پایگاه داده خود به همان فرمت بصورت کامل می‌توانید ذخیره و بازیابی نمایید و احتیاجی به مقادیر جداگانه ندارید. درهنگام استفاده از پایگاه داده‌های دی‌نرمال دقت کنید. برای شروع از پایگاه داده نرمال استفاده کنید زیرا به کدهای برنامه شما امکان انعطاف بیشتری می‌دهد و کمک می‌کند تا پایگاه داده‌ها تمامیت داده‌(Data integrity) را حفظ کند.
5.1  راه حل: استفاده کردن از جدول واسط
در این روش شناسه‌ی کاربران را در جدول جداگانهایی که هر کدام از آنها یک سطر را به خود اختصاص داده اند، ذخیره می‌نماییم.
CREATE TABLE Contacts ( product_id BIGINT UNSIGNED NOT NULL,
account_id BIGINT UNSIGNED NOT NULL,
PRIMARY KEY (product_id, account_id),
FOREIGN KEY (product_id) REFERENCES Products(product_id),
FOREIGN KEY (account_id) REFERENCES Accounts(account_id)
);

جدول Contacts یک جدول رابطه ایی بین جداول Products,Accounts

جدول Contacts یک جدول رابطه ایی بین جداول Products,Accounts 


 بدست آوردن محصولات برای کاربران و موارد مربوط به آن 

ما براحتی می‌توانیم تمامی محصولاتی که مختص به یک کاربر هستند را بدست آوریم. در این شیوه خاصیت ایندکس بودن شناسه‌ی کاربران حفظ می‌شود به همین دلیل queryهای آن برای خواندن و بهینه کردن راحت‌تر می‌باشند. در این روش به کاراکتری برای جدا کردن ورودی‌ها از یکدیگر نیاز نداریم چون هر کدام از آنها در یک سطر جداگانه ثبت می‌شوند. برای ویرایش کردن کاربرانی که یک محصول را خریداری نموده اند، کافیست یک سطر از جدول واسط را اضافه یا حذف نماییم. درنمونه کد زیر، ابتدا در جدول Contacts کاربری با شناسه‌ی 34 که محصولی با شناسه‌ی 456 را خریداری کرده، درج شده است و در خط بعد عملیات حذف با شرط آنکه شناسه‌ی کاربر و محصول به ترتیب 34،456 باشد روی جدول Contacts اعمال شده است.

INSERT INTO Contacts (product_id, account_id) VALUES (456, 34);

DELETE FROM Contacts WHERE product_id = 456 AND account_id = 34;

ایجاد توابع تجمیعی

به عنوان نمونه در مثال زیر براحتی ما می‌توانیم تعداد محصولات در هر حساب کاربری را بدست آوریم:

SELECT account_id, COUNT(*) AS products_per_account
FROM Contacts
GROUP BY account_id;

اعتبارسنجی شناسه  محصولات 

از آنجاییکه مقادیری که در جدول قرار دارند کلید خارجی می‌باشند میتوان صحت اعتبار آنها را بررسی نمود. بعنوان مثال Contacts.account_id به Account.account_id  اشاره می‌کند. در ضمن برای هر فیلد نوع آن را می‌توان مشخص کرد تا فقط همان نوع داده را بپذیرد.

محدودیت طول لیست 

نسبت به روش قبلی تقریبا در این حالت محدودیتی برای تعداد کاراکتر‌های ورودی نداریم.


مزیت‌های دیگر استفاده از جدول واسط

کارایی روش دوم بهتر از حالت قبلی می‌باشد چون ایندکس بودن شناسه‌ها حفظ شده است. همچنین براحتی میتوانیم فیلدی را به این جدول اضافه نماییم مثلا (time, date… ) 

  


[1] Intersection Table  
[2] Aggregate  
نظرات مطالب
اعمال تزریق وابستگی‌ها به مثال رسمی ASP.NET Identity
با سلام؛ من همین روش رو رفتم و توی پروژه م اضافه کردم. اما وقتی میخوام Add-Migration بزنم ، به چنین اروری برمی خوردم:
One or more validation errors were detected during model generation:
DataLayer.Context.CustomUserRole: : EntityType 'CustomUserRole' has no key defined. Define the key for this EntityType.
DataLayer.Context.CustomUserLogin: : EntityType 'CustomUserLogin' has no key defined. Define the key for this EntityType.
CustomUserRoles: EntityType: EntitySet 'CustomUserRoles' is based on type 'CustomUserRole' that has no keys defined.
CustomUserLogins: EntityType: EntitySet 'CustomUserLogins' is based on type 'CustomUserLogin' that has no keys defined.
آیا ممکنه بخاطر آپدیت کردن پکیج‌های مربوطه باشه؟