بازخوردهای پروژه‌ها
خطای sql?
ببخشید برای بازخورد قبلیم جایی برای ویرایش پیدا نکرم.
کد کامل کلاس من به این صورته:
 class SQLiteDataReaderPdfReport
    {
        public IPdfReportData CreatePdfReport()
        {
           
                return new PdfReport().DocumentPreferences(doc =>
                {
                    doc.RunDirection(PdfRunDirection.RightToLeft);
                    doc.Orientation(PageOrientation.Portrait);
                    doc.PageSize(PdfPageSize.A4);
                    doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" });
                })
                .DefaultFonts(fonts =>
                {
                    fonts.Path(System.AppDomain.CurrentDomain.BaseDirectory + "\\fonts\\BMitra.ttf",
                                      Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
                })
                .PagesFooter(footer =>
                {
                    footer.DefaultFooter(DateTime.Now.ToString("MM/dd/yyyy"));
                })
                .PagesHeader(header =>
                {
                    header.DefaultHeader(defaultHeader =>
                    {
                        defaultHeader.RunDirection(PdfRunDirection.RightToLeft);
                        defaultHeader.ImagePath(System.AppDomain.CurrentDomain.BaseDirectory + "\\Images\\s.jpg");
                        defaultHeader.Message("لیست دانشجویان");
                    });
                })
                .MainTableTemplate(template =>
                {
                    template.BasicTemplate(BasicTemplate.SilverTemplate);
                })
                .MainTablePreferences(table =>
                {
                    table.ColumnsWidthsType(TableColumnWidthType.Relative);
                    table.NumberOfDataRowsPerPage(5);
                })
                .MainTableDataSource(dataSource =>
                {
                    dataSource.GenericDataReader(
                        providerName: "System.Data.SQLite",
                        connectionString: "Data Source=" + System.AppDomain.CurrentDomain.BaseDirectory + "\\data\\database.sqlite",
                        sql: @"SELECT id,name,family,mark
                               FROM Student
                                WHERE id='1'",
                             

                        parametersValues: new object[] { 10 }
                    );
                })

                .MainTableColumns(columns =>
                {
                    columns.AddColumn(column =>
                    {
                        column.PropertyName("id");
                        column.IsRowNumber(true);
                        column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                        column.IsVisible(true);
                        column.Order(0);
                        column.Width(2);
                        column.HeaderCell("کد");
                    });


                    columns.AddColumn(column =>
                    {
                        column.PropertyName("name");
                        column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                        column.IsVisible(true);
                        column.Order(1);
                        column.Width(2);
                        column.HeaderCell("نام");
                    });

                    columns.AddColumn(column =>
                    {
                        column.PropertyName("family");
                        column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                        column.IsVisible(true);
                        column.Order(2);
                        column.Width(2);
                        column.HeaderCell("نام خانوادگی");
                    });

                    columns.AddColumn(column =>
                    {
                        column.PropertyName("mark");
                        column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                        column.IsVisible(true);
                        column.Order(3);
                        column.Width(2);
                        column.HeaderCell("نمره");
                    });
                })
                .MainTableEvents(events =>
                {
                    events.DataSourceIsEmpty(message: "There is no data available to display.");
                })
                .Export(export =>
                {
                    export.ToExcel();
                })
                .Generate(data => data.AsPdfFile(System.AppDomain.CurrentDomain.BaseDirectory + "\\Pdf\\RptSqlDataReaderSample.pdf"));
           
        }
        
    }
}
به خاطر توجهی که کردید سپاسگزارم!
نظرات مطالب
Blazor 5x - قسمت دهم - مبانی Blazor - بخش 7 - مسیریابی
بهبود قسمت «ارسال کوئری استرینگ‌ها به کامپوننت‌های مختلف » در Blazor 6x

در Blazor 6x دیگر نیازی نیست تا با استفاده از «HttpUtility.ParseQueryString»، کار پردازش دستی کوئری استرینگ‌های رسیده، صورت گیرد. برای نمونه فرض کنید Url رسیده، چنین شکلی را دارد:
/search?filter=some+stuff&page=3&assignee=User1&assignee=User2
برای دسترسی به مقادیر کوئری استرینگ‌های آدرس فوق، اینبار کافی است به صورت زیر عمل کنیم:
@code {
    [Parameter]
    [SupplyParameterFromQuery]
    public string Filter { get; set; }

    [Parameter]
    [SupplyParameterFromQuery]
    public int? Page { get; set; }

    [Parameter]
    [SupplyParameterFromQuery(Name = "assignee")]
    public string[] Assignees { get; set; }
}
یعنی ذکر دو ویژگی [Parameter, SupplyParameterFromQuery] بر روی یک خاصیت عمومی کامپوننت، آن‌را آماده‌ی دریافت مقادیر کوئری استرینگ‌های متناظری می‌کند. اگر می‌خواهید نام پارامتر، با نام کوئری استرینگ یکی نباشد (حالت پیش‌فرض)، از خاصیت Name این ویژگی می‌توان استفاده کرد.

نکته: در اینجا مقادیری مانند ذیل قابل قبول هستند:
String, bool, DateTime, decimal, double, float, Guid, int, long
و یا حالت Nullable آن‌ها و یا آرایه‌ای آن‌ها.

امکان ساخت Urlهایی به همراه کوئری استرینگ‌ها جهت هدایت ساده‌تر به آن‌ها

به Blazor 6x، متدهای الحاقی UriWithQueryParameter نیز اضافه شده‌اند و کار آن‌ها، افزودن ساده‌تر کوئری‌استرینگ‌ها به Urlها است که نمونه‌ای از آن به صورت زیر است:
var actualUri = NavigationManager.GetUriWithQueryParameters(new Dictionary<string, object>
        {
            ["full name"] = "John Doe", // Single value
            ["ping"] = new int?[] { 35, 16, null, 87, 240 }
        });
که چنین Url ای را تولید می‌کند (نمونه‌ای از روش تعریف کوئری استرینگ‌های آرایه‌ای):
host/?full%20name=John%20Doe&ping=35&ping=16&ping=87&ping=240
مثال‌های بیشتر آن‌را در اینجا می‌توانید مشاهده کنید.
مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت دهم- ذخیره سازی اطلاعات کاربران IDP در بانک اطلاعاتی
تا اینجا تمام قسمت‌های این سری، برای اساس اطلاعات یک کلاس Config استاتیک تشکیل شده‌ی در حافظه ارائه شدند. این روش برای دمو و توضیح مفاهیم پایه‌ی IdentityServer بسیار مفید است؛ اما برای دنیای واقعی خیر. بنابراین در ادامه می‌خواهیم این قسمت را با اطلاعات ذخیره شده‌ی در بانک اطلاعاتی تعویض کنیم. یک روش مدیریت آن، نصب ASP.NET Core Identity دقیقا داخل همان پروژه‌ی IDP است. در این حالت کدهای ASP.NET Core Identity مایکروسافت، کار مدیریت کاربران IDP را انجام می‌دهند. روش دیگر اینکار را که در اینجا بررسی خواهیم کرد، تغییر کدهای Quick Start UI اضافه شده‌ی در «قسمت چهارم - نصب و راه اندازی IdentityServer»، جهت پذیرفتن مدیریت کاربران مبتنی بر بانک اطلاعاتی تهیه شده‌ی توسط خودمان است. مزیت آن آشنا شدن بیشتر با کدهای Quick Start UI و درک زیرساخت آن است.


تکمیل ساختار پروژه‌ی IDP

تا اینجا برای IDP، یک پروژه‌ی خالی وب را ایجاد و به مرور، آن‌را تکمیل کردیم. اما اکنون نیاز است پشتیبانی از بانک اطلاعاتی را نیز به آن اضافه کنیم. برای این منظور چهار پروژه‌ی Class library کمکی را نیز به Solution آن اضافه می‌کنیم:


- DNT.IDP.DomainClasses
در این پروژه، کلاس‌های متناظر با موجودیت‌های جداول مرتبط با اطلاعات کاربران قرار می‌گیرند.
- DNT.IDP.DataLayer
این پروژه Context برنامه و Migrations آن‌را تشکیل می‌دهد. همچنین به همراه تنظیمات و Seed اولیه‌ی اطلاعات بانک اطلاعاتی نیز می‌باشد.
رشته‌ی اتصالی آن نیز در فایل DNT.IDP\appsettings.json ذخیره شده‌است.
- DNT.IDP.Common
الگوریتم هش کردن اطلاعات، در این پروژه‌ی مشترک بین چند پروژه‌ی دیگر قرار گرفته‌است. از آن جهت هش کردن کلمات عبور، در دو پروژه‌ی DataLayer و همچنین Services استفاده می‌کنیم.
- DNT.IDP.Services
کلاس سرویس کاربران که با استفاده از DataLayer با بانک اطلاعاتی ارتباط برقرار می‌کند، در این پروژه قرار گرفته‌است.


ساختار بانک اطلاعاتی کاربران IdentityServer

در اینجا ساختار بانک اطلاعاتی کاربران IdentityServer، بر اساس جداول کاربران و Claims آن‌ها تشکیل می‌شود:
namespace DNT.IDP.DomainClasses
{
    public class User
    {
        [Key]
        [MaxLength(50)]       
        public string SubjectId { get; set; }
    
        [MaxLength(100)]
        [Required]
        public string Username { get; set; }

        [MaxLength(100)]
        public string Password { get; set; }

        [Required]
        public bool IsActive { get; set; }

        public ICollection<UserClaim> UserClaims { get; set; }

        public ICollection<UserLogin> UserLogins { get; set; }
    }
}
در اینجا SubjectId همان Id کاربر، در سطح IDP است. این خاصیت به صورت یک کلید خارجی در جداول UserClaims و UserLogins نیز بکار می‌رود.
ساختار Claims او نیز به صورت زیر تعریف می‌شود که با تعریف یک Claim استاندارد، سازگاری دارد:
namespace DNT.IDP.DomainClasses
{
    public class UserClaim
    {         
        public int Id { get; set; }

        [MaxLength(50)]
        [Required]
        public string SubjectId { get; set; }
        
        public User User { get; set; }

        [Required]
        [MaxLength(250)]
        public string ClaimType { get; set; }

        [Required]
        [MaxLength(250)]
        public string ClaimValue { get; set; }
    }
}
همچنین کاربر می‌توان تعدادی لاگین نیز داشته باشد:
namespace DNT.IDP.DomainClasses
{
    public class UserLogin
    {
        public int Id { get; set; }

        [MaxLength(50)]
        [Required]
        public string SubjectId { get; set; }
        
        public User User { get; set; }

        [Required]
        [MaxLength(250)]
        public string LoginProvider { get; set; }

        [Required]
        [MaxLength(250)]
        public string ProviderKey { get; set; }
    }
}
هدف از آن، یکپارچه سازی سیستم، با IDPهای ثالث مانند گوگل، توئیتر و امثال آن‌ها است.

در پروژه‌ی DNT.IDP.DataLayer در پوشه‌ی Configurations آن، کلاس‌های UserConfiguration و UserClaimConfiguration را مشاهده می‌کنید که حاوی اطلاعات اولیه‌ای برای تشکیل User 1 و User 2 به همراه Claims آن‌ها هستند. این اطلاعات را دقیقا از فایل استاتیک ‍Config که در قسمت‌های قبل تکمیل کردیم، به این دو کلاس جدید IEntityTypeConfiguration منتقل کرده‌ایم تا به این ترتیب متد GetUsers فایل استاتیک Config را با نمونه‌ی دیتابیسی آن جایگزین کنیم.
سرویسی که از طریق Context برنامه با بانک اطلاعاتی ارتباط برقرار می‌کند، چنین ساختاری را دارد:
    public interface IUsersService
    {
        Task<bool> AreUserCredentialsValidAsync(string username, string password);
        Task<User> GetUserByEmailAsync(string email);
        Task<User> GetUserByProviderAsync(string loginProvider, string providerKey);
        Task<User> GetUserBySubjectIdAsync(string subjectId);
        Task<User> GetUserByUsernameAsync(string username);
        Task<IEnumerable<UserClaim>> GetUserClaimsBySubjectIdAsync(string subjectId);
        Task<IEnumerable<UserLogin>> GetUserLoginsBySubjectIdAsync(string subjectId);
        Task<bool> IsUserActiveAsync(string subjectId);
        Task AddUserAsync(User user);
        Task AddUserLoginAsync(string subjectId, string loginProvider, string providerKey);
        Task AddUserClaimAsync(string subjectId, string claimType, string claimValue);
    }
که توسط آن امکان دسترسی به یک کاربر، اطلاعات Claims او و افزودن رکوردهایی جدید وجود دارد.
تنظیمات نهایی این سرویس‌ها و Context برنامه نیز در فایل DNT.IDP\Startup.cs جهت معرفی به سیستم تزریق وابستگی‌ها، صورت گرفته‌اند. همچنین در اینجا متد initializeDb را نیز مشاهده می‌کنید که با فراخوانی متد context.Database.Migrate، تمام کلاس‌های Migrations پروژه‌ی DataLayer را به صورت خودکار به بانک اطلاعاتی اعمال می‌کند.


غیرفعال کردن صفحه‌ی Consent در Quick Start UI

در «قسمت چهارم - نصب و راه اندازی IdentityServer» فایل‌های Quick Start UI را به پروژه‌ی IDP اضافه کردیم. در ادامه می‌خواهیم قدم به قدم این پروژه را تغییر دهیم.
در صفحه‌ی Consent در Quick Start UI، لیست scopes درخواستی برنامه‌ی کلاینت ذکر شده و سپس کاربر انتخاب می‌کند که کدامیک از آن‌ها، باید به برنامه‌ی کلاینت ارائه شوند. این صفحه، برای سناریوی ما که تمام برنامه‌های کلاینت توسط ما توسعه یافته‌اند، بی‌معنا است و صرفا برای کلاینت‌های ثالثی که قرار است از IDP ما استفاده کنند، معنا پیدا می‌کند. برای غیرفعال کردن آن کافی است به فایل استاتیک Config مراجعه کرده و خاصیت RequireConsent کلاینت مدنظر را به false تنظیم کرد.


تغییر نام پوشه‌ی Quickstart و سپس اصلاح فضای نام پیش‌فرض کنترلرهای آن

در حال حاضر کدهای کنترلرهای Quick Start UI داخل پوشه‌ی Quickstart برنامه‌ی IDP قرار گرفته‌اند. با توجه به اینکه قصد داریم این کدها را تغییر دهیم و همچنین این پوشه در اساس، همان پوشه‌ی استاندارد Controllers است، ابتدا نام این پوشه را به Controllers تغییر داده و سپس در تمام کنترلرهای ذیل آن، فضای نام پیش‌فرض IdentityServer4.Quickstart.UI را نیز به فضای نام متناسبی با پوشه بندی پروژه‌ی جاری تغییر می‌دهیم. برای مثال کنترلر Account واقع در پوشه‌ی Account، اینبار دارای فضای نام DNT.IDP.Controllers.Account خواهد شد و به همین ترتیب برای مابقی کنترل‌ها عمل می‌کنیم.
پس از این تغییرات، عبارات using موجود در Viewها را نیز باید تغییر دهید تا برنامه در زمان اجرا به مشکلی برنخورد. البته ASP.NET Core 2.1 در زمان کامپایل برنامه، تمام Viewهای آن‌را نیز کامپایل می‌کند و اگر خطایی در آن‌ها وجود داشته باشد، امکان بررسی و رفع آن‌ها پیش از اجرای برنامه، میسر است.
و یا می‌توان جهت سهولت کار، فایل DNT.IDP\Views\_ViewImports.cshtml را جهت معرفی این فضاهای نام جدید ویرایش کرد تا نیازی به تغییر Viewها نباشد:
@using DNT.IDP.Controllers.Account;
@using DNT.IDP.Controllers.Consent;
@using DNT.IDP.Controllers.Grants;
@using DNT.IDP.Controllers.Home;
@using DNT.IDP.Controllers.Diagnostics;
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers


تعامل با IdentityServer از طریق کدهای سفارشی

پس از تشکیل «ساختار بانک اطلاعاتی کاربران IdentityServer» و همچنین تهیه سرویس‌های متناظری جهت کار با آن، اکنون نیاز است مطمئن شویم IdentityServer از این بانک اطلاعاتی برای دریافت اطلاعات کاربران خود استفاده می‌کند.
در حال حاضر، با استفاده از متد الحاقی AddTestUsers معرفی شده‌ی در فایل DNT.IDP\Startup.cs، اطلاعات کاربران درون حافظه‌ای برنامه را از متد ()Config.GetUsers دریافت می‌کنیم.
بنابراین اولین قدم، بررسی ساختار متد AddTestUsers است. برای این منظور به مخزن کد IdentityServer4 مراجعه کرده و کدهای متد الحاقی AddTestUsers را بررسی می‌کنیم:
 public static class IdentityServerBuilderExtensions
 {
        public static IIdentityServerBuilder AddTestUsers(this IIdentityServerBuilder builder, List<TestUser> users)
        {
            builder.Services.AddSingleton(new TestUserStore(users));
            builder.AddProfileService<TestUserProfileService>();
            builder.AddResourceOwnerValidator<TestUserResourceOwnerPasswordValidator>();

            return builder;
        }
}
- ابتدا یک TestUserStore را به صورت Singleton ثبت کرده‌است.
- سپس سرویس پروفایل کاربران را اضافه کرده‌است. این سرویس با پیاده سازی اینترفیس IProfileService تهیه می‌شود. کار آن اتصال یک User Store سفارشی به سرویس کاربران و دریافت اطلاعات پروفایل آن‌ها مانند Claims است.
- در آخر TestUserResourceOwnerPasswordValidator، کار اعتبارسنجی کلمه‌ی عبور و نام کاربری را در صورت استفاده‌ی از Flow ویژه‌ای به نام ResourceOwner که استفاده‌ی از آن توصیه نمی‌شود (ROBC Flow)، انجام می‌دهد.

برای جایگزین کردن AddTestUsers، کلاس جدید IdentityServerBuilderExtensions را در ریشه‌ی پروژه‌ی IDP با محتوای ذیل اضافه می‌کنیم:
using DNT.IDP.Services;
using Microsoft.Extensions.DependencyInjection;

namespace DNT.IDP
{
    public static class IdentityServerBuilderExtensions
    {
        public static IIdentityServerBuilder AddCustomUserStore(this IIdentityServerBuilder builder)
        {
            // builder.Services.AddScoped<IUsersService, UsersService>();
            builder.AddProfileService<CustomUserProfileService>();
            return builder;
        }
    }
}
در اینجا ابتدا IUsersService سفارشی برنامه معرفی شده‌است که User Store سفارشی برنامه است. البته چون UsersService ما با بانک اطلاعاتی کار می‌کند، نباید به صورت Singleton ثبت شود و باید در پایان هر درخواست به صورت خودکار Dispose گردد. به همین جهت طول عمر آن Scoped تعریف شده‌است. در کل ضرورتی به ذکر این سطر نیست؛ چون پیشتر کار ثبت IUsersService در کلاس Startup برنامه انجام شده‌است.
سپس یک ProfileService سفارشی را ثبت کرده‌ایم. این سرویس، با پیاده سازی IProfileService به صورت زیر پیاده سازی می‌شود:
namespace DNT.IDP.Services
{
    public class CustomUserProfileService : IProfileService
    {
        private readonly IUsersService _usersService;

        public CustomUserProfileService(IUsersService usersService)
        {
            _usersService = usersService;
        }

        public async Task GetProfileDataAsync(ProfileDataRequestContext context)
        {
            var subjectId = context.Subject.GetSubjectId();
            var claimsForUser = await _usersService.GetUserClaimsBySubjectIdAsync(subjectId);
            context.IssuedClaims = claimsForUser.Select(c => new Claim(c.ClaimType, c.ClaimValue)).ToList();
        }

        public async Task IsActiveAsync(IsActiveContext context)
        {
            var subjectId = context.Subject.GetSubjectId();
            context.IsActive = await _usersService.IsUserActiveAsync(subjectId);
        }
    }
}
سرویس پروفایل، توسط سرویس کاربران برنامه که در ابتدای مطلب آن‌را تهیه کردیم، امکان دسترسی به اطلاعات پروفایل کاربران را مانند Claims او، پیدا می‌کند.
در متدهای آن، ابتدا subjectId و یا همان Id منحصربفرد کاربر جاری سیستم، دریافت شده و سپس بر اساس آن می‌توان از usersService، جهت دریافت اطلاعات مختلف کاربر، کوئری گرفت و نتیجه را در خواص context جاری، برای استفاده‌های بعدی، ذخیره کرد.

اکنون به کلاس src\IDP\DNT.IDP\Startup.cs مراجعه کرده و متد AddTestUsers را با AddCustomUserStore جایگزین می‌کنیم:
namespace DNT.IDP
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddIdentityServer()
             .AddDeveloperSigningCredential()
             .AddCustomUserStore()
             .AddInMemoryIdentityResources(Config.GetIdentityResources())
             .AddInMemoryApiResources(Config.GetApiResources())
             .AddInMemoryClients(Config.GetClients());
تا اینجا فقط این سرویس‌های جدید را ثبت کرده‌ایم، اما هنوز کار خاصی را انجام نمی‌دهند و باید از آن‌ها در برنامه استفاده کرد.


اتصال IdentityServer به User Store سفارشی

در ادامه، سازنده‌ی کنترلر DNT.IDP\Quickstart\Account\AccountController.cs را بررسی می‌کنیم:
        public AccountController(
            IIdentityServerInteractionService interaction,
            IClientStore clientStore,
            IAuthenticationSchemeProvider schemeProvider,
            IEventService events,
            TestUserStore users = null)
        {
            _users = users ?? new TestUserStore(TestUsers.Users);

            _interaction = interaction;
            _clientStore = clientStore;
            _schemeProvider = schemeProvider;
            _events = events;
        }
- سرویس توکار IIdentityServerInteractionService، کار تعامل برنامه با IdentityServer4‌  را انجام می‌دهد.
- IClientStore پیاده سازی محل ذخیره سازی اطلاعات کلاینت‌ها را ارائه می‌دهد که در حال حاضر توسط متد استاتیک Config در اختیار آن قرار می‌گیرد.
- IEventService رخ‌دادهایی مانند لاگین موفقیت آمیز یک کاربر را گزارش می‌دهد.
- در آخر، TestUserStore تزریق شده‌است که می‌خواهیم آن‌را با User Store سفارشی خودمان جایگزین کنیم.  بنابراین در ابتدا TestUserStore را با UserStore سفارشی خودمان جایگزین می‌کنیم:
        private readonly TestUserStore _users;
        private readonly IUsersService _usersService;
        public AccountController(
    // ...
            IUsersService usersService)
        {
            _usersService = usersService;
    // ...
        }
فعلا فیلد TestUserStore را نیز سطح کلاس جاری باقی نگه می‌داریم. از این جهت که قسمت‌های لاگین خارجی سیستم (استفاده از گوگل، توئیتر و ...) هنوز از آن استفاده می‌کنند و آن‌را در قسمتی دیگر تغییر خواهیم داد.
پس از معرفی فیلد usersService_، اکنون در قسمت زیر از آن استفاده می‌کنیم:
در اکشن متد لاگین، جهت بررسی صحت نام کاربری و کلمه‌ی عبور و همچنین یافتن کاربر متناظر با آن:
        public async Task<IActionResult> Login(LoginInputModel model, string button)
        {
    //...
            if (ModelState.IsValid)
            {
                if (await _usersService.AreUserCredentialsValidAsync(model.Username, model.Password))
                {
                    var user = await _usersService.GetUserByUsernameAsync(model.Username);
تا همینجا برنامه را کامپایل کرده و اجرا کنید. پس از لاگین در آدرس https://localhost:5001/Gallery/IdentityInformation، هنوز اطلاعات User Claims کاربر وارد شده‌ی به سیستم نمایش داده می‌شوند که بیانگر صحت عملکرد CustomUserProfileService است.


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

پس از اتصال قسمت login برنامه‌ی IDP به بانک اطلاعاتی، اکنون می‌خواهیم امکان ثبت کاربران را نیز به آن اضافه کنیم.
این قسمت شامل تغییرات ذیل است:
الف) اضافه شدن RegisterUserViewModel
این ViewModel که فیلدهای فرم ثبت‌نام را تشکیل می‌دهد، ابتدا با نام کاربری و کلمه‌ی عبور شروع می‌شود:
    public class RegisterUserViewModel
    {
        // credentials       
        [MaxLength(100)]
        public string Username { get; set; }

        [MaxLength(100)]
        public string Password { get; set; }
سپس سایر خواصی که در اینجا اضافه می‌شوند:
    public class RegisterUserViewModel
    {
   // ...

        // claims 
        [Required]
        [MaxLength(100)]
        public string Firstname { get; set; }

        [Required]
        [MaxLength(100)]
        public string Lastname { get; set; }

        [Required]
        [MaxLength(150)]
        public string Email { get; set; }

        [Required]
        [MaxLength(200)]
        public string Address { get; set; }

        [Required]
        [MaxLength(2)]
        public string Country { get; set; }
در کنترلر UserRegistrationController، تبدیل به UserClaims شده و در جدول مخصوص آن ذخیره خواهند شد.
ب) افزودن UserRegistrationController
این کنترلر، RegisterUserViewModel را دریافت کرده و سپس بر اساس آن، شیء User ابتدای بحث را تشکیل می‌دهد. ابتدا نام کاربری و کلمه‌ی عبور را در جدول کاربران ثبت می‌کند و سپس سایر خواص این ViewModel را در جدول UserClaims:
varuserToCreate=newUser
{
  Password=model.Password.GetSha256Hash(),
  Username=model.Username,
  IsActive=true
};
userToCreate.UserClaims.Add(newUserClaim("country",model.Country));
userToCreate.UserClaims.Add(newUserClaim("address",model.Address));
userToCreate.UserClaims.Add(newUserClaim("given_name",model.Firstname));
userToCreate.UserClaims.Add(newUserClaim("family_name",model.Lastname));
userToCreate.UserClaims.Add(newUserClaim("email",model.Email));
userToCreate.UserClaims.Add(newUserClaim("subscriptionlevel","FreeUser"));
ج) افزودن RegisterUser.cshtml
این فایل، view متناظر با ViewModel فوق را ارائه می‌دهد که توسط آن، کاربری می‌تواند اطلاعات خود را ثبت کرده و وارد سیستم شود.
د) اصلاح فایل ViewImports.cshtml_ جهت تعریف فضای نام UserRegistration
در RegisterUser.cshtml از RegisterUserViewModel استفاده می‌شود. به همین جهت بهتر است فضای نام آن‌را به ViewImports اضافه کرد.
ه) افزودن لینک ثبت نام به صفحه‌ی لاگین در Login.cshtml
این لینک دقیقا در ذیل چک‌باکس Remember My Login اضافه شده‌است.


اکنون اگر برنامه را اجرا کنیم، ابتدا مشاهده می‌کنیم که صفحه‌ی لاگین به همراه لینک ثبت نام ظاهر می‌شود:


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


برای آزمایش، کاربری را ثبت کنید. پس از ثبت اطلاعات، بلافاصله وارد سیستم خواهید شد. البته چون در اینجا subscriptionlevel به FreeUser تنظیم شده‌است، این کاربر یکسری از لینک‌های برنامه‌ی MVC Client را به علت نداشتن دسترسی، مشاهده نخواهد کرد.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشه‌ی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشه‌ی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آن‌را اجرا کنید تا برنامه‌ی IDP راه اندازی شود.
- در آخر به پوشه‌ی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحه‌ی login نام کاربری را User 1 و کلمه‌ی عبور آن‌را password وارد کنید.
بازخوردهای دوره
متدهای async تقلبی
در این مثال عملیات تبدیل sync به async درست انجام شده ؟
کد sync :
        public string AESEncrypt(string Text)
        {
            byte[] dataToEncrypt = Encoding.UTF8.GetBytes(Text);
            byte[] key = Convert.FromBase64String(AES_Key);
            byte[] iv = Convert.FromBase64String(AES_Iv);

            using (var aes = new AesManaged())
            {
                aes.Mode = CipherMode.CBC;
                aes.Padding = PaddingMode.PKCS7;
                aes.Key = key;
                aes.IV = iv;
                using (var ms = new MemoryStream())
                {
                    using (var cs = new CryptoStream(ms, aes.CreateEncryptor(), CryptoStreamMode.Write))
                    {
                        cs.Write(dataToEncrypt, 0, dataToEncrypt.Length);
                        cs.FlushFinalBlock();
                        return Convert.ToBase64String(ms.ToArray());
                    }
                }
            }
        }
کد async :
        public async Task<string> AESEncryptAsync(string Text)
        {
            byte[] dataToEncrypt = Encoding.UTF8.GetBytes(Text);
            byte[] key = Convert.FromBase64String(AES_Key);
            byte[] iv = Convert.FromBase64String(AES_Iv);

            using (var aes = new AesManaged())
            {
                aes.Mode = CipherMode.CBC;
                aes.Padding = PaddingMode.PKCS7;
                aes.Key = key;
                aes.IV = iv;
                using (var ms = new MemoryStream())
                {
                    using (var cs = new CryptoStream(ms, aes.CreateEncryptor(), CryptoStreamMode.Write))
                    {
                        await cs.WriteAsync(dataToEncrypt, 0, dataToEncrypt.Length);
                        cs.FlushFinalBlock();
                        return Convert.ToBase64String(ms.ToArray());
                    }
                }
            }
        }
من این دو متد رو از نظر "زمان اجرا" ، "ترد مشغول" ، "هسته cpu مشغول" در asp.net mvc تست گرفتم {1000 بار فرخوانی} .
در این تست متد async  هر بار زمان بیشتری برای محاسبه برد (که خوب چون ترد جدید باز میکنه طبیعی هست) و در ترد‌ها و هسته‌های مختلف cpu اجرا میشد ، که این هم طبیعی هست ، طرف نظر از این که طبق مقاله حاضر کلا اینجا async نباید اجرا بشه چون خارج از مرز سیستم نیست ولی عملیات async کامل انجام میشه (فکر میکنم).
اما متد sync که بررسی شد ، خروجی به صورتی بود که آن هم هر بار در ترد و هسته مختلف سی پی یو اجرا میشد ! سوال دلیل چیست ؟
مگر تفاوت sync و async  دقیقا به اجرای آنها نیست ؟ پس چرا در هر دو حالت ما اجرا‌های یکسانی داریم ؟
ممنونم.
نظرات مطالب
راه اندازی StimulSoft Report در ASP.NET MVC
در سمت سرور کلاس‌هایی به صورت زیر می‌توانید داشته باشید:
public class PlanModel
{
    public int RId { get; set; }
    public string Day { get; set; }
    public string Plan { get; set; }
    public List<RecordModel> Records { get; set; }
}

public class RecordModel
{
    public int RId { get; set; }
    public string Help { get; set; }
}
که لیستی از PlanModel پس از فراهم کردن اطلاعاتش،  به گزارش ارسال می‌کنید.
در فایل mrt گزارش خود نیز در قسمت Business Objects دو Business Object تولید می‌کنید، که مدل دوم (از نوع RecordModel) به عنوان فرزند مدل اول (از نوع PlanModel) است.

برای نمایش اطلاعات این دو مدل هم، به دو Data band احتیاج دارید.

 نکته‌ای که در اینجا مهم است باید پراپرتی Master Component، بند دوم برابر بند اول مقداردهی شود.
نمونه فایل: MultiBusinessObject.mrt 
مطالب
کامپایل پویای کد در دات نت

در دات نت فریم ورک امکان کامپایل پویای یک قطعه کد دریافت شده از یک رشته، توسط فضای نام CodeDom مهیا است که قدرت قابل توجهی را در اختیار برنامه نویس قرار می‌دهد.

مثال یک:
رشته زیر را کامپایل کرده و تبدیل به یک فایل exe کنید:

string source =
@"
namespace Foo
{
public class Bar
{
static void Main(string[] args)
{
Bar.SayHello();
}

public static void SayHello()
{
System.Console.WriteLine(""Hello World"");
}
}
}
";
روش انجام کار به همراه توضیحات مربوطه به صورت کامنت:

using System;
using System.Collections.Generic;
//دو فضای نامی که برای این منظور اضافه شده‌اند
using Microsoft.CSharp;
using System.CodeDom.Compiler;

namespace compilerTest
{
class Program
{
static void compileIt1()
{
//سورس کد ما جهت کامپایل
string source =
@"
namespace Foo
{
public class Bar
{
static void Main(string[] args)
{
Bar.SayHello();
}

public static void SayHello()
{
System.Console.WriteLine(""Hello World"");
}
}
}
";

//تعیین نگارش کامپایلر مورد استفاده
Dictionary<string, string> providerOptions = new Dictionary<string, string>
{
{"CompilerVersion", "v3.5"}
};
//تعیین اینکه کد ما سی شارپ است
CSharpCodeProvider provider = new CSharpCodeProvider(providerOptions);

//تعیین اینکه خروجی یک فایل اجرایی است بعلاوه مشخص سازی محل ذخیره سازی فایل نهایی
CompilerParameters compilerParams = new CompilerParameters
{
OutputAssembly = "D:\\Foo.EXE",
GenerateExecutable = true
};

//عملیات کامپایل در اینجا صورت می‌گیرد
CompilerResults results = provider.CompileAssemblyFromSource(compilerParams, source);

//اگر خطایی وجود داشته باشد نمایش داده خواهد شد
Console.WriteLine("Number of Errors: {0}", results.Errors.Count);
foreach (CompilerError err in results.Errors)
{
Console.WriteLine("ERROR {0}", err.ErrorText);
}
}

static void Main(string[] args)
{
compileIt1();

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
مثال 2:
کد مورد نظر را به صورت یک فایل dll کامپایل کنید.
برای این منظور تمامی مراحل مانند قبل است فقط GenerateExecutable ذکر شده به false تنظیم شده و نام خروجی نیز به foo.dll باید تنظیم شود.


مثال 3:
کد مورد نظر را در حافظه کامپایل کرده (خروجی dll یا exe نمی‌خواهیم)، سپس متد SayHello آن را به صورت پویا فراخوانی نموده و خروجی را نمایش دهید.
در این حالت روش کار همانند مثال 1 است با این تفاوت که GenerateInMemory = true و GenerateExecutable = false تنظیم می‌شوند. همچنین جهت دسترسی به متد کلاس ذکر شده،‌ از قابلیت‌های ریفلکشن موجود در دات نت فریم ورک استفاده خواهد شد.

using System;
using System.Collections.Generic;
using Microsoft.CSharp;
using System.CodeDom.Compiler;
using System.Reflection;

namespace compilerTest
{
class Program
{
static void compileIt2()
{
//سورس کد ما جهت کامپایل
string source =
@"
namespace Foo
{
public class Bar
{
static void Main(string[] args)
{
Bar.SayHello();
}

public static void SayHello()
{
System.Console.WriteLine(""Hello World"");
}
}
}
";

//تعیین نگارش کامپایلر مورد استفاده
Dictionary<string, string> providerOptions = new Dictionary<string, string>
{
{"CompilerVersion", "v3.5"}
};
//تعیین اینکه کد ما سی شارپ است
CSharpCodeProvider provider = new CSharpCodeProvider(providerOptions);

//نحوه تعیین مشخص سازی کامپایل در حافظه
CompilerParameters compilerParams = new CompilerParameters
{
GenerateInMemory = true,
GenerateExecutable = false
};

//عملیات کامپایل در اینجا صورت می‌گیرد
CompilerResults results = provider.CompileAssemblyFromSource(compilerParams, source);

// اگر خطایی در کامپایل وجود نداشت متد دلخواه را فراخوانی می‌کنیم
if (results.Errors.Count == 0)
{
//استفاده از ریفلکشن برای دسترسی به متد و فراخوانی آن
Type type = results.CompiledAssembly.GetType("Foo.Bar");
MethodInfo method = type.GetMethod("SayHello");
method.Invoke(null, null);
}
}


static void Main(string[] args)
{
compileIt2();

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
نکته: نحوه‌ی استفاده از اسمبلی‌های دیگر در رشته سورس کد خود
مثال:
اگر رشته سورس ما به صورت زیر بوده و از اسمبلی System.Drawing.Dll نیز کمک گرفته باشد،‌

string source =
@"
namespace Foo
{

public class Bar
{
static void Main(string[] args)
{
Bar.SayHello();
}

public static void SayHello()
{
System.Console.WriteLine(""Hello World"");
var r = new System.Drawing.Rectangle(0,0,100,100);
System.Console.WriteLine(r);
}
}
}
";
هنگام کامپایل آن توسط روش مثال یک، با خطای زیر مواجه خواهیم شد.

Number of Errors: 1
ERROR The type or namespace name 'Drawing' does not exist in the namespace 'System' (are you missing an assembly reference?)

برای رفع این مشکل و معرفی این اسمبلی،‌ سطر زیر باید پس از تعریف compilerParams اضافه شود.

compilerParams.ReferencedAssemblies.Add("System.Drawing.Dll");
اکنون کد کامپایل شده و مشکلی نخواهد داشت.
نمونه‌ای دیگر از این دست، استفاده از LINQ می‌باشد. در این حالت اسمبلی System.Core.Dll نیز به روش ذکر شده باید معرفی گردد تا مشکلی در کامپایل کد رخ ندهد.


کاربردها:
1- استفاده در ابزارهای تولید کد (برای مثال در برنامه Linqer از این قابلیت استفاده می‌شود)
2- استفاده‌های امنیتی (ایجاد روش‌های تولید یک سریال به صورت پویا و کامپایل پویای کد مربوطه در حافظه‌ای محافظت شده)
3- استفاده جهت مقاصد محاسباتی پیشرفته
4- دادن اجازه‌ی کد نویسی به کاربران برنامه‌ی خود (شبیه به سیستم‌های ماکرو و اسکریپت نویسی موجود)
و ...

مطالب
EF Code First #5

در قسمت قبل خاصیت AutomaticMigrationsEnabled را در کلاس Configuration به true تنظیم کردیم. به این ترتیب، عملیات ساده شده، اما یک سری از قابلیت‌های ردیابی تغییرات را از دست خواهیم داد و این عملیات،‌ صرفا یک عملیات رو به جلو خواهد بود.
اگر AutomaticMigrationsEnabled را مجددا به false تنظیم کنیم و هربار به کمک دستوارت Add-Migration و Update-Database تغییرات مدل‌ها را به بانک اطلاعاتی اعمال نمائیم، علاوه بر تشکیل تاریخچه این تغییرات در برنامه، امکان بازگشت به عقب و لغو تغییرات صورت گرفته نیز مهیا می‌گردد.

هدف قرار دادن مرحله‌ای خاص یا لغو آن

به همان پروژه قسمت قبل مراجعه نمائید. در کلاس Configuration آن، خاصیت AutomaticMigrationsEnabled را به false تنظیم کنید. سپس یک خاصیت جدید را به کلاس Project اضافه نموده و برنامه را اجرا نمائید. بلافاصله خطای زیر را دریافت خواهیم کرد:

Unable to update database to match the current model because there are pending changes and 
automatic migration is disabled. Either write the pending model changes to a code-based migration
or enable automatic migration. Set DbMigrationsConfiguration.AutomaticMigrationsEnabled to true
to enable automatic migration.

EF تشخیص داده است که کلاس مدل برنامه، با بانک اطلاعاتی تطابق ندارد و همچنین ویژگی مهاجرت خودکار نیز فعال نیست. بنابراین اعمال code-based migration را توصیه کرده است.
برای این منظور به کنسول پاورشل NuGet مراجعه نمائید (منوی Tools در ویژوال استودیو، گزینه‌ Library package manager آن و سپس انتخاب گزینه package manager console). در ادامه فرمان add-m را نوشته و دکمه tab را فشار دهید. یک منوی Auto Complete ظاهر خواهد شد که از آن‌ می‌توان فرمان add-migration را انتخاب نمود. در اینجا یک نام را هم نیاز است وارد کرد؛ برای مثال:

Add-Migration AddSomeProp2ToProject

به این ترتیب کلاس زیر را به صورت خودکار تولید خواهد کرد:

namespace EF_Sample02.Migrations
{
using System.Data.Entity.Migrations;

public partial class AddSomeProp2ToProject : DbMigration
{
public override void Up()
{
AddColumn("Projects", "SomeProp", c => c.String());
AddColumn("Projects", "SomeProp2", c => c.String());
}

public override void Down()
{
DropColumn("Projects", "SomeProp2");
DropColumn("Projects", "SomeProp");
}
}
}

مدل‌های برنامه را با بانک اطلاعاتی تطابق داده و دریافته است که هنوز دو خاصیت در اینجا به بانک اطلاعاتی اضافه نشده‌اند.
از متد Up برای اعمال تغییرات و از متد Down برای بازگشت به قبل استفاده می‌گردد. نام فایل این کلاس هم طبق معمول چیزی است شبیه به timeStamp_AddSomeProp2ToProject.cs .

در ادامه نیاز است این تغییرات به بانک اطلاعاتی اعمال شوند. به همین منظور دستور زیر را در کنسول پاورشل وارد نمائید:

Update-Database -Verbose

پارامتر Verbose آن سبب خواهد شد تا جزئیات عملیات به صورت مفصل گزارش داده شود که شامل دستورات ALTER TABLE نیز هست:

Using NuGet project 'EF_Sample02'.
Using StartUp project 'EF_Sample02'.
Target database is: 'testdb2012' (DataSource: (local), Provider: System.Data.SqlClient, Origin: Configuration).
Applying explicit migrations: [201205061835024_AddSomeProp2ToProject].
Applying explicit migration: 201205061835024_AddSomeProp2ToProject.
ALTER TABLE [Projects] ADD [SomeProp] [nvarchar](max)
ALTER TABLE [Projects] ADD [SomeProp2] [nvarchar](max)
[Inserting migration history record]

اکنون مجددا یک خاصیت دیگر را مثلا به نام public string SomeProp3، به کلاس Project اضافه نمائید.
سپس همین روال باید مجددا تکرار شود. دستورات زیر را در کنسول پاورشل NuGet اجرا نمائید:

Add-Migration AddSomeProp3ToProject
Update-Database -Verbose

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

در ادامه برای مثال به این نتیجه رسیده‌ایم که نیازی به خاصیت public string SomeProp3 اضافه شده، نبوده است. روش متداول، باز هم مانند سابق است. ابتدا خاصیت را از کلاس Project حذف خواهیم کرد و سپس دو دستور Add-Migration و Update-Database را اجرا خواهیم نمود.
اما با توجه به اینکه مهاجرت خودکار را غیرفعال کرده‌ایم و هربار با فراخوانی دستور Add-Migration یک کلاس جدید، با متدهای Up و Down به پروژه، جهت نگهداری سوابق عملیات اضافه می‌شوند، می‌توان دستور Update-Database را جهت فراخوانی متد Down صرفا یک مرحله موجود نیز فراخوانی نمود.

نکته:
اگر علاقمند باشید که راهنمای مفصل پارامترهای دستور Update-Database را مشاهده کنید، تنها کافی است دستور زیر را در کنسول پاورشل اجرا نمائید:

get-help update-database -detailed

به عنوان نمونه اگر در حین فراخوانی دستور Update-Database احتمال از دست رفتن اطلاعات باشد، عملیات متوقف می‌شود. برای وادار کردن پروسه به انجام تغییرات بر روی بانک اطلاعاتی می‌توان از پارامتر Force در اینجا استفاده کرد.

در ادامه برای اینکه دستور Update-Database تنها یک مرحله مشخص را که سابقه آن در برنامه موجود است، هدف قرار دهد، باید از پارامتر TargetMigration به همراه نام کلاس مرتبط استفاده کرد:

Update-Database -TargetMigration:"AddSomeProp2ToProject" -Verbose

اگر دقت کرده باشید در اینجا AddSomeProp2ToProject بجای AddSomeProp3ToProject بکارگرفته شده است. اگر یک مرحله قبل را هدف قرار دهیم، متد Down را اجرا خواهد کرد:

Using NuGet project 'EF_Sample02'.
Using StartUp project 'EF_Sample02'.
Target database is: 'testdb2012' (DataSource: (local), Provider: System.Data.SqlClient, Origin: Configuration).
Reverting migrations: [201205061845485_AddSomeProp3ToProject].
Reverting explicit migration: 201205061845485_AddSomeProp3ToProject.
DECLARE @var0 nvarchar(128)
SELECT @var0 = name
FROM sys.default_constraints
WHERE parent_object_id = object_id(N'Projects')
AND col_name(parent_object_id, parent_column_id) = 'SomeProp3';
IF @var0 IS NOT NULL
EXECUTE('ALTER TABLE [Projects] DROP CONSTRAINT ' + @var0)
ALTER TABLE [Projects] DROP COLUMN [SomeProp3]
[Deleting migration history record]

همانطور که ملاحظه می‌کنید در اینجا عملیات حذف ستون SomeProp3 انجام شده است. البته این خاصیت به صورت خودکار از کدهای برنامه (کلاس Project در این مثال) حذف نمی‌شود و فرض بر این است که پیشتر اینکار را انجام داده‌اید.


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

تمام کلاس‌های خودکار مهاجرت تولید شده توسط پاورشل، از کلاس DbMigration ارث بری می‌کنند. در این کلاس امکانات قابل توجهی مانند AddColumn، AddForeignKey، AddPrimaryKey، AlterColumn، CreateIndex و امثال آن وجود دارند که در تمام کلاس‌های مشتق شده از آن، قابل استفاده هستند. حتی متد Sql نیز در آن پیش بینی شده است که در صورت نیاز به اجرای دستوارت خام SQL، می‌توان از آن استفاده کرد.
برای مثال فرض کنید مجددا همان خاصیت public string SomeProp3 را به کلاس Project اضافه کرد‌ه‌ایم. اما اینبار نیاز است حین تشکیل این فیلد در بانک اطلاعاتی، یک مقدار پیش فرض نیز برای آن درنظر گرفته شود که در صورت نال بودن مقدار خاصیت آن در برنامه، به صورت خودکار توسط بانک اطلاعاتی مقدار دهی گردد:

namespace EF_Sample02.Migrations
{
using System.Data.Entity.Migrations;

public partial class AddSomeProp3ToProject : DbMigration
{
public override void Up()
{
AddColumn("Projects", "SomeProp3", c => c.String(defaultValue: "some data"));
Sql("Update Projects set SomeProp3=N'some data'");
}

public override void Down()
{
DropColumn("Projects", "SomeProp3");
}
}
}

متد String در اینجا چنین امضایی دارد:

public ColumnModel String(bool? nullable = null, int? maxLength = null, bool? fixedLength = null, 
bool? isMaxLength = null, bool? unicode = null, string defaultValue = null, string defaultValueSql = null,
string name = null, string storeType = null)

که برای نمونه در اینجا پارامتر defaultValue آن‌را در کلاس AddSomeProp3ToProject مقدار دهی کرده‌ایم.
برای اعمال این تغییرات تنها کافی است دستور Update-Database -Verbose اجرا گردد. اینبار خروجی SQL اجرا شده آن به نحو زیر است که شامل مقدار پیش فرض نیز شده است:

ALTER TABLE [Projects] ADD [SomeProp3] [nvarchar](max) DEFAULT 'some data'

تعیین مقدار پیش فرض، زمانیکه یک فیلد not null تعریف شده‌است نیز می‌تواند مفید باشد. همچنین در اینجا امکان اجرای دستورات مستقیم SQL نیز وجود دارد که نمونه‌ای از آن‌را در متد Up فوق مشاهده می‌کنید.


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

در قسمت‌های قبل با متد Seed که به همراه آغاز کننده‌های بانک اطلاعاتی EF ارائه شده‌اند، جهت افزودن رکوردهای اولیه و پیش فرض به بانک اطلاعاتی آشنا شدید. در اینجا نیز با تحریف متد Seed در کلاس Configuration،‌ چنین امری میسر است:

namespace EF_Sample02.Migrations
{
using System;
using System.Data.Entity.Migrations;

internal sealed class Configuration : DbMigrationsConfiguration<EF_Sample02.Sample2Context>
{
public Configuration()
{
this.AutomaticMigrationsEnabled = false;
this.AutomaticMigrationDataLossAllowed = true;
}

protected override void Seed(EF_Sample02.Sample2Context context)
{
context.Users.AddOrUpdate(
a => a.Name,
new Models.User { Name = "Vahid", AddDate = DateTime.Now },
new Models.User { Name = "Test", AddDate = DateTime.Now });
}
}
}

متد AddOrUpdate در EF 4.3 اضافه شده است. این متد ابتدا بررسی می‌کند که آیا رکورد مورد نظر در بانک اطلاعاتی وجود دارد یا خیر. اگر خیر، آن‌را اضافه خواهد کرد در غیراینصورت، نمونه موجود را به روز رسانی می‌کند. اولین پارامتر آن، identifierExpression نام دارد. توسط آن مشخص می‌شود که بر اساس چه خاصیتی باید در مورد update یا add تصمیم‌گیری شود. دراینجا اگر نیاز به ذکر بیش از یک خاصیت وجود داشت، از anonymously type object می‌توان کمک گرفت new { p.Name, p.LastName } .


تولید اسکریپت به روز رسانی بانک اطلاعاتی

بهترین کار و امن‌ترین روش حین انجام این نوع به روز رسانی‌ها، تهیه اسکریپت SQL فرامینی است که باید بر روی بانک اطلاعاتی اجرا شوند. سپس می‌توان این دستورات و اسکریپت نهایی را دستی هم اجرا کرد (که روش متداول‌تری است در محیط کاری).
برای اینکار تنها کافی است دستور زیر را در کنسول پاورشل اجرا نمائیم:
Update-Database -Verbose -Script

پس از اجرای این دستور، یک فایل اسکریپت با پسوند sql تولید شده و بلافاصله در ویژوال استودیو جهت مرور نیز گشوده خواهد شد. برای نمونه محتوای آن برای افزودن خاصیت جدید SomeProp5 به صورت زیر است:

ALTER TABLE [Projects] ADD [SomeProp5] [nvarchar](max)
INSERT INTO [__MigrationHistory] ([MigrationId], [CreatedOn], [Model], [ProductVersion]) VALUES
('201205060852004_AutomaticMigration', '2012-05-06T08:52:00.937Z', 0x1F8B0800000............ '4.3.1')

همانطور که ملاحظه می‌کنید، در یک مرحله، جدول پروژه‌ها را به روز خواهد کرد و در مرحله بعد، سابقه آن‌را در جدول __MigrationHistory ثبت می‌کند.

یک نکته:
اگر دستور فوق را بر روی برنامه‌ای که با بانک اطلاعاتی هماهنگ است اجرا کنیم، خروجی را مشاهده نخواهیم کرد. برای این منظور می‌توان مرحله خاصی را توسط پارامتر SourceMigration هدف گیری کرد:

Update-Database -Verbose -Script -SourceMigration:"stepName"




استفاده از DB Migrations در عمل

البته این یک روش پیشنهادی و امن است:
الف) در ابتدای اجرا برنامه، پارامتر ورودی متد System.Data.Entity.Database.SetInitializer را به نال تنظیم کنید تا برنامه تغییری را بر روی بانک اطلاعاتی اعمال نکند.
ب) توسط دستور enable-migrations،‌ فایل‌های اولیه DB Migration را ایجاد کنید. پیش فرض‌های آن را نیز تغییر ندهید.
ج) هر بار که کلاس‌های مدل‌ برنامه تغییر کردند و پس از آن نیاز به به روز رسانی ساختار بانک اطلاعاتی وجود داشت دو دستور زیر را اجرا کنید:
Add-Migration AddSomePropToProject
Update-Database -Verbose -Script

به این ترتیب سابقه تغییرات در برنامه نگهداری شده و همچنین بدون اجرای دستورات بر روی بانک اطلاعاتی، اسکریپت نهایی اعمال تغییرات تولید می‌گردد.
د) اسکریپت تولید شده را بررسی کرده و پس از تائید و افزودن به سورس کنترل، به صورت دستی بر روی بانک اطلاعاتی اجرا کنید (مثلا توسط management studio).


نظرات مطالب
انجام کارهای زمانبندی شده در برنامه‌های ASP.NET توسط DNT Scheduler
زمانی که وظیفه ای میخواهد اجرا شود همزمان با آن کنترلر Home هم فراخوانی می‌شود
این درخواست هایی هست که لاگ VS قسمت Output ثبت می‌شود

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

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

#endregion

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

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

        #endregion

        #region Methods

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

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

        #endregion
    }
}

مطالب
پیاده سازی CQRS توسط MediatR - قسمت سوم
در قسمت قبلی روش استفاده از IRequest و IRequestHandler را در MediatR که نقش پیاده سازی Command/Query را در CQRS بر عهده دارند، بررسی کردیم. کدهای این قسمت در این ریپازیتوری به‌روزرسانی شده و قابل دسترسی است.

Fluent Validation


Command ما که نقش ایجاد یک مشتری را داشت ( CreateCustomerCommand )، هیچ Validation ای برای اعتبار سنجی مقادیر ورودی از سمت کاربر را ندارد و کاربر با هر مقادیری میتواند این Command را فراخوانی کند. در این قسمت با استفاده از کتابخانه Fluent Validation امکان اعتبار سنجی را به Command‌های خود اضافه میکنیم.

در ابتدا با استفاده از دستور زیر ، این کتابخانه را داخل پروژه خود نصب میکنیم:
Install-Package FluentValidation.AspNetCore

بعد از افزودن این کتابخانه، باید آن را داخل DI Container خود Register کنیم:
services.AddMvc()
    .AddFluentValidation(cfg => cfg.RegisterValidatorsFromAssemblyContaining<Startup>());

کلاس جدیدی با نام CreateCustomerCommandValidator ایجاد میکنیم و از کلاس AbstractValidator مربوط به Fluent Validation ارث بری میکنیم تا منطق اعتبارسنجی برای CreateCustomerCommand را داخل آن تعریف نماییم :
public class CreateCustomerCommandValidator : AbstractValidator<CreateCustomerCommand>
{
    public CreateCustomerCommandValidator()
    {
        RuleFor(customer => customer.FirstName).NotEmpty();
        RuleFor(customer => customer.LastName).NotEmpty();
    }
}
همانطور که میبینید در اینجا خالی نبودن Firstname و Lastname ورودی از سمت کاربر را چک کرده‌ایم. Fluent Validation دارای متدهای زیادی برای اعتبارسنجی است که لیست آن‌ها را در اینجا میتوانید ببینید. همچنین درصورت نیاز میتوانید Validator‌های سفارشی خود را طبق نمونه‌ها ایجاد کنید.

اگر برنامه را اجرا و CreateCustomerCommand را با مقادیر خالی فراخوانی کنیم، خواهید دید که بلافاصله با چنین خطایی مواجه خواهید شد که نشان میدهد Fluent Validation بدرستی وظیفه اعتبارسنجی ورودی‌ها را انجام داده است:
Error: Bad Request

{
  "LastName": [
    "'Last Name' must not be empty."
  ],
  "FirstName": [
    "'First Name' must not be empty."
  ]
}

* نکته : تمامی اعتبارسنجی‌های سطحی ( Superficial Validation ) مانند خالی نبودن مقادیر، اعتبارسنجی تاریخ‌ها، اعتبارسنجی ایمیل و ... باید قبل از Handle شدن Command‌ها صورت گیرد و در صورت ناموفق بودن اعتبارسنجی، نباید وارد متد Handle در Command‌ها شویم. ( Fail Fast Principle )

Events

Observer Pattern

فرض کنید میخواهیم در صورت موفقیت آمیز بودن ثبت نام یک مشتری، برای او ایمیلی ارسال کنیم. فرستادن ایمیل وظیفه CreateCustomerCommand نیست و در صورت افزودن منطق ارسال ایمیل به Command، اصل SRP را نقض کرده‌ایم.

برای حل این مشکل میتوانیم از Event‌ها استفاده کنیم. Event‌ها خبری را به Subscriber‌ های خود میدهند. در فریمورک MediatR، ارسال و Handle کردن Event‌‌ها، با دو interface صورت میگیرد: INotification و INotificationHandler

بر خلاف Command‌ها که فقط یک Handler میتوانند داشته باشند، Event ها میتوانند چندین Handler داشته باشند. مزیت داشتن چند Subscriber برای Event‌ها این است که شما علاوه بر اینکه میتوانید Subscriber ای داشته باشید که وظیفه ارسال Email برای مشتری را بر عهده داشته باشد، Subscriber دیگری داشته باشید که اطلاعات مشتری جدید را Log کند.

ابتدا کلاس CustomerCreatedEvent را ایجاد و از INotification ارث بری میکنیم. این کلاس منتشر کننده یک اتفاق است که آن را Producer مینامند :
public class CustomerCreatedEvent : INotification
{
    public CustomerCreatedEvent(string firstName, string lastName, DateTime registrationDate)
    {
        FirstName = firstName;
        LastName = lastName;
        RegistrationDate = registrationDate;
    }

    public string FirstName { get; }

    public string LastName { get; }

    public DateTime RegistrationDate { get; }
}

سپس دو Handler برای این Event مینویسیم. Handler اول وظیفه ارسال ایمیل را بر عهده خواهد داشت :
public class CustomerCreatedEmailSenderHandler : INotificationHandler<CustomerCreatedEvent>
{
    public Task Handle(CustomerCreatedEvent notification, CancellationToken cancellationToken)
    {
        // IMessageSender.Send($"Welcome {notification.FirstName} {notification.LastName} !");
        return Task.CompletedTask;
    }
}

و Handler دوم، وظیفه Log کردن اطلاعات مشتری ثبت شده را بر عهده خواهد داشت:
public class CustomerCreatedLoggerHandler : INotificationHandler<CustomerCreatedEvent>
{
    readonly ILogger<CustomerCreatedLoggerHandler> _logger;

    public CustomerCreatedLoggerHandler(ILogger<CustomerCreatedLoggerHandler> logger)
    {
        _logger = logger;
    }

    public Task Handle(CustomerCreatedEvent notification, CancellationToken cancellationToken)
    {
        _logger.LogInformation($"New customer has been created at {notification.RegistrationDate}: {notification.FirstName} {notification.LastName}");

        return Task.CompletedTask;
    }
}

در نهایت کافیست داخل CreateCustomerCommandHandler که در قسمت قبل آن را ایجاد کردیم، متد Handle را ویرایش و با استفاده از متد Publish موجود در اینترفیس IMediator، این Event را Raise کنیم :
public class CreateCustomerCommandHandler : IRequestHandler<CreateCustomerCommand, CustomerDto>
{
    readonly ApplicationDbContext _context;
    readonly IMapper _mapper;
    readonly IMediator _mediator;

    public CreateCustomerCommandHandler(ApplicationDbContext context,
        IMapper mapper,
        IMediator mediator)
    {
        _context = context;
        _mapper = mapper;
        _mediator = mediator;
    }

    public async Task<CustomerDto> Handle(CreateCustomerCommand createCustomerCommand, CancellationToken cancellationToken)
    {
        Customer customer = _mapper.Map<Customer>(createCustomerCommand);

        await _context.Customers.AddAsync(customer, cancellationToken);
        await _context.SaveChangesAsync(cancellationToken);

        // Raising Event ...
        await _mediator.Publish(new CustomerCreatedEvent(customer.FirstName, customer.LastName, customer.RegistrationDate), cancellationToken);

        return _mapper.Map<CustomerDto>(customer);
    }
}

برنامه را اجرا و روی دو NotificationHandler خود Breakpoint قرار دهید. اگر api/Customers را برای ایجاد یک مشتری جدید فراخوانی کنید، بعد از ثبت نام موفق مشتری، خواهید دید که هر دو Handler شما Raise میشوند و اطلاعات مشتری را که با LogHandler خود داخل Console لاگ کردیم، خواهیم دید:
info: MediatrTutorial.Features.Customer.Events.CustomerCreated.CustomerCreatedLoggerHandler[0]
      New customer has been created at 2/1/2019 11:40:48 PM: Moien Tajik


* نکته : در این قسمت از آموزش برای Log کردن اطلاعات از یک Notification استفاده کردیم. اگر تعداد Command‌های ما در برنامه بیشتر شوند، به ازای هر Command مجبور به ایجاد یک Notification و NotificationHandler خواهیم بود که منطق کار آن‌ها بسیار شبیه به یک دیگر است.

در مقاله بعدی با استفاده از Behaviors موجود در MediatR که AOP را پیاده سازی میکند، این موارد تکراری را از بین خواهیم برد.
اشتراک‌ها
کتابخانه drum

Drum is a little library for building URIs to ASP.NET Web API actions, using direct routes and lambda expressions. It provides an alternative to the UrlHelper class. Instead of requiring a route name and a set of name-value pairs, Drum allows the creation of URIs using actions invocations.

// using UrlHelper 
var uri1 = _urlHelper.Link("GetPaged", new { page = 0, count = 10 });

// using UriMaker
var uri2 = _uriMaker.UriFor(c => c.GetPaged(0, 10));

where GetPaged is a Web API controller action

[RoutePrefix("api/UriMakerTests/resources")]
public class ResourceController : ApiController
{
    [Route("", Name="GetPaged")]
    public HttpResponseMessage GetPaged(int page, int count) {...}

    ...
}
کتابخانه drum