مطالب
سفارشی سازی ASP.NET Core Identity - قسمت سوم - نرمال سازها و اعتبارسنج‌ها
چندی قبل مطلب «نرمال سازی اطلاعات کاربران در حین ثبت نام» را در سایت جاری مطالعه کردید. پیاده سازی یک چنین قابلیتی به صورت توکار در ASP.NET Core Identity پیش بینی شده‌است. همچنین تمام اعتبارسنج‌های نام‌های کاربران، کلمات عبور آن‌ها، ایمیل‌های آن‌ها و غیره را نیز می‌توان سفارشی سازی کرد و بجای سرویس‌های پیش‌فرض آن‌ها معرفی و جایگزین نمود.


سفارشی سازی نرمال سازها

اگر به طراحی جداول ASP.NET Core Identity دقت کنید، تعدادی فیلد اضافی حاوی کلمه‌ی Normalized را هم مشاهده خواهید کرد. برای مثال:


در جدول کاربران، فیلدهای Email و UserName به همراه دو فیلد اضافه‌ی NormalizedEmail و NormalizedUserName وجود دارند.
مقدار دهی و مدیریت این فیلدهای ویژه به صورت خودکار توسط کلاسی به نام UpperInvariantLookupNormalizer صورت می‌گیرد:
 public class UpperInvariantLookupNormalizer : ILookupNormalizer
این کلاس اینترفیس ILookupNormalizer را پیاده سازی کرده و تنها کاری را که انجام می‌دهد، تبدیل نام کاربر، نام نقش‌ها و یا ایمیل کاربر به حالت upper case آن است. اما هدف اصلی از آن چیست؟
همانطور که در مطلب «نرمال سازی اطلاعات کاربران در حین ثبت نام» نیز عنوان شد، برای مثال ایمیل‌های جی‌میل را می‌توان با چندین حالت مختلف ثبت کرد و یک کاربر به این صورت می‌تواند شرط یکتا بودن آدرس ایمیل‌های تنظیم شده‌ی در کلاس IdentityServicesRegistry را دور بزند:
 identityOptionsUser.RequireUniqueEmail = true;
به همین جهت برای سفارشی سازی آن کلاس CustomNormalizer با سفارشی سازی UpperInvariantLookupNormalizer پیاده سازی شده‌است.
چون تنها یک اینترفیس ILookupNormalizer وجود دارد، باید بر اساس محتوای کلیدی که به آن ارسال می‌شود:
   public override string Normalize(string key)
تصمیم‌گیری کرد که آیا ایمیل است یا خیر. چون از این نرمال کننده هم برای ایمیل‌ها و هم برای نام‌ها استفاده می‌شود. سپس می‌توان منطق‌های سفارشی خود مانند حذف نقطه‌های اضافی ایمیل‌ها و یا حذف کاراکترهای اضافی اعمالی به نام‌های کاربری را اعمال کرد.
پس از تدارک کلاس CustomNormalizer، تنها کاری را که باید در جهت معرفی و جایگرینی آن انجام داد، تغییر ذیل در کلاس IdentityServicesRegistry است:
services.AddScoped<ILookupNormalizer, CustomNormalizer>();
services.AddScoped<UpperInvariantLookupNormalizer, CustomNormalizer>();
یکبار CustomNormalizer را به عنوان پیاده سازی کننده‌ی ILookupNormalizer معرفی کرده‌ایم. همچنین یکبار هم سرویس توکار UpperInvariantLookupNormalizer را به سرویس سفارشی خودمان هدایت کرده‌ایم. به این ترتیب مطمئن خواهیم شد که همواره از CustomNormalizer ما استفاده خواهد شد.
بنابراین دیگر نیازی نیست تا در حین ثبت‌نام نسبت به تمیزسازی ایمیل‌ها و یا نام‌های کاربری اقدام کنیم. سرویس ILookupNormalizer در پشت صحنه به صورت خودکار در تمام مراحل ثبت نام و به روز رسانی‌ها توسط ASP.NET Core Identity استفاده می‌شود.


سفارشی سازی UserValidator

ASP.NET Core Identity به همراه یک سرویس توکار اعتبارسنج کاربران است که با پیاده سازی اینترفیس IUserValidator ارائه شده‌است:
 public class UserValidator<TUser> : IUserValidator<TUser> where TUser : class
این سرویس پیش‌فرض و توکار، تنظیمات Options.User.RequireUniqueEmail، Options.User.AllowedUserNameCharacters و امثال آن‌را در مورد نام‌های کاربری و ایمیل‌ها بررسی می‌کند (تنظیم شده‌ی در متد setUserOptions کلاس IdentityServicesRegistry).
بنابراین اگر قصد تهیه‌ی یک IUserValidator جدید را داشته باشیم، از تمام تنظیمات و بررسی‌های پیش فرض سرویس توکار UserValidator فوق محروم می‌شویم. به همین جهت برای سفارشی سازی این سرویس، از خود کلاس UserValidator ارث بری کرده و سپس base.ValidateAsync آن‌را فراخوانی می‌کنیم. با این‌کار سبب خواهیم شد تا تمام اعتبارسنجی‌های پیش‌فرض ASP.NET Core Identity اعمال شده و پس از آن منطق‌های سفارشی اعتبارسنجی خود را که در کلاس CustomUserValidator‌ قابل مشاهده هستند، اضافه می‌کنیم.
        public override async Task<IdentityResult> ValidateAsync(UserManager<User> manager, User user)
        {
            // First use the built-in validator
            var result = await base.ValidateAsync(manager, user).ConfigureAwait(false);
            var errors = result.Succeeded ? new List<IdentityError>() : result.Errors.ToList();

            // Extending the built-in validator
            validateEmail(user, errors);
            validateUserName(user, errors);

            return !errors.Any() ? IdentityResult.Success : IdentityResult.Failed(errors.ToArray());
        }
در اینجا برای مثال در متد validateEmail سفارشی تهیه شده، لیست یک سری fake email provider اضافه شده‌اند (مدخل EmailsBanList در فایل appsettings.json برنامه) تا کاربران نتوانند از آن‌ها جهت ثبت‌نام استفاده کنند و یا در متد validateUserName سفارشی، اگر نام کاربری برای مثال عددی وارد شده بود، یک new IdentityError بازگشت داده می‌شود.

پس از تدارک کلاس CustomUserValidator، تنها کاری را که باید در جهت معرفی و جایگرینی آن انجام داد، تغییر ذیل در کلاس IdentityServicesRegistry است:
 services.AddScoped<IUserValidator<User>, CustomUserValidator>();
services.AddScoped<UserValidator<User>, CustomUserValidator>();
یکبار CustomUserValidator را به عنوان پیاده سازی کننده‌ی IUserValidator معرفی کرده‌ایم. همچنین یکبار هم سرویس توکار UserValidator را به سرویس سفارشی خودمان هدایت کرده‌ایم. به این ترتیب مطمئن خواهیم شد که همواره از CustomUserValidator ما استفاده خواهد شد (حتی اگر UserValidator اصلی از سیستم تزریق وابستگی‌ها درخواست شود).


سفارشی سازی PasswordValidator

مراحل سفارشی سازی اعتبارسنج کلمات عبور نیز همانند تهیه‌ی CustomUserValidator فوق است.
ASP.NET Core Identity به همراه یک سرویس توکار اعتبارسنج کلمات عبور کاربران است که با پیاده سازی اینترفیس IPasswordValidator ارائه شده‌است:
 public class PasswordValidator<TUser> : IPasswordValidator<TUser> where TUser : class
در این کلاس، از اطلاعات متد setPasswordOptions کلاس IdentityServicesRegistry
        private static void setPasswordOptions(PasswordOptions identityOptionsPassword, SiteSettings siteSettings)
        {
            identityOptionsPassword.RequireDigit = siteSettings.PasswordOptions.RequireDigit;
            identityOptionsPassword.RequireLowercase = siteSettings.PasswordOptions.RequireLowercase;
            identityOptionsPassword.RequireNonAlphanumeric = siteSettings.PasswordOptions.RequireNonAlphanumeric;
            identityOptionsPassword.RequireUppercase = siteSettings.PasswordOptions.RequireUppercase;
            identityOptionsPassword.RequiredLength = siteSettings.PasswordOptions.RequiredLength;
        }
که از فایل appsettings.json و مدخل PasswordOptions آن تامین می‌شود:
"PasswordOptions": {
   "RequireDigit": false,
   "RequiredLength": 6,
   "RequireLowercase": false,
   "RequireNonAlphanumeric": false,
   "RequireUppercase": false
},
جهت اعتبارسنجی کلمات عبور وارد شده‌ی توسط کاربران در حین ثبت نام و یا به روز رسانی اطلاعات خود، استفاده می‌شود.

بنابراین در اینجا نیز ارائه‌ی یک پیاده سازی خام از IPasswordValidator سبب خواهد شد تا تمام اعتبارسنجی‌های توکار کلاس PasswordValidator اصلی را از دست بدهیم. به همین جهت کار را با ارث بری از همین کلاس توکار شروع کرده و ابتدا متد base.ValidateAsync آن‌را فراخوانی می‌کنیم تا مطمئن شویم، مدخل PasswordOptions تنظیمات یاد شده، حتما پردازش خواهند شد. سپس منطق سفارشی خود را اعمال می‌کنیم.
برای مثال در کلاس CustomPasswordValidator تهیه شده، به مدخل PasswordsBanList فایل appsettings.json مراجعه شده و کاربران را از انتخاب کلمات عبوری به شدت ساده، منع می‌کند.

پس از تدارک کلاس CustomPasswordValidator‌، تنها کاری را که باید در جهت معرفی و جایگرینی آن انجام داد، تغییر ذیل در کلاس IdentityServicesRegistry است:
services.AddScoped<IPasswordValidator<User>, CustomPasswordValidator>();
services.AddScoped<PasswordValidator<User>, CustomPasswordValidator>();
یکبار CustomPasswordValidator را به عنوان پیاده سازی کننده‌ی IPasswordValidator معرفی کرده‌ایم. همچنین یکبار هم سرویس توکار PasswordValidator را به سرویس سفارشی خودمان هدایت کرده‌ایم. به این ترتیب مطمئن خواهیم شد که همواره از CustomPasswordValidator ما استفاده خواهد شد (حتی اگر PasswordValidator اصلی از سیستم تزریق وابستگی‌ها درخواست شود).


پردازش نتایج اعتبارسنج‌ها

این اعتبارسنج‌ها در خروجی‌های IdentityResult تمام متدهای ASP.NET Core Identity ظاهر می‌شوند. بنابراین فراخوانی ساده‌ی UpdateUserAsync اشتباه است و حتما باید خروجی آن‌را جهت پردازش IdentityResult آن بررسی کرد. به همین جهت تعدادی متد الحاقی به کلاس IdentityExtensions اضافه شده‌اند تا کارکردن با IdentityResult را ساده‌تر کنند.
   public static void AddErrorsFromResult(this ModelStateDictionary modelStat, IdentityResult result)
متد AddErrorsFromResult خطاهای حاصل از عملیات ASP.NET Core Identity را به ModelState جاری اضافه می‌کند. به این ترتیب می‌توان این خطاها را به کاربر در Viewهای برنامه و در قسمت اعتبارسنجی مدل آن نمایش داد.

   public static string DumpErrors(this IdentityResult result, bool useHtmlNewLine = false)
و یا متد DumpErrors تمام خطاهای موجود در IdentityResult  را تبدیل به یک رشته می‌کند. برای مثال می‌توان این رشته را در Remote validationها مورد استفاده قرار داد.
استفاده‌ی از این متدهای الحاقی را در کنترلرهای برنامه می‌توانید مشاهده کنید.


استفاده‌ی از اعتبارسنج‌ها جهت انجام Remote validation

اگر به RegisterController دقت کنید، اکشن متدهای ValidateUsername و ValidatePassword قابل مشاهده هستند:
  [AjaxOnly, HttpPost, ValidateAntiForgeryToken]
  [ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]
  public async Task<IActionResult> ValidateUsername(string username, string email)

  [AjaxOnly, HttpPost, ValidateAntiForgeryToken]
  [ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]
  public async Task<IActionResult> ValidatePassword(string password, string username)
این اکشن متدها توسط سرویس‌های
IPasswordValidator<User> passwordValidator,
IUserValidator<User> userValidator,
تزریق شده‌ی به سازنده‌ی کلاس، پیاده سازی شده‌اند. در مورد تامین آن‌ها و سفارشی سازی آن‌ها نیز پیشتر بحث شد. این اینترفیس‌ها دقیقا همان وهله‌های CustomUserValidator و CustomPasswordValidator را در اختیار ما قرار می‌دهند. تنها کاری را که باید انجام دهیم، فراخوانی متد ValidateAsync آن‌ها است. این متد یک خروجی از نوع IdentityResult را دارد. به همین جهت متد DumpErrors را برای پردازش این نتیجه تدارک دیدیم.
به این ترتیب کاربران در حین ثبت نام، راهنمای بهتری را جهت انتخاب کلمات عبور و نام کاربری مشاهده خواهند کرد و این بررسی‌ها نیز Ajax ایی هستند و پیش از ارسال فرم نهایی به سرور اتفاق می‌افتند.

برای فعالسازی Remote validation، علاوه بر ثبت اسکریپت‌های Ajax ایی، خواص کلاس RegisterViewModel نیز از ویژگی Remote استفاده می‌کنند:
  [Required(ErrorMessage = "(*)")]
  [Display(Name = "نام کاربری")]
  [Remote("ValidateUsername", "Register",
AdditionalFields = nameof(Email) + "," + ViewModelConstants.AntiForgeryToken, HttpMethod = "POST")]
  [RegularExpression("^[a-zA-Z_]*$", ErrorMessage = "لطفا تنها از حروف انگلیسی استفاده نمائید")]
  public string Username { get; set; }

یک نکته: برای اینکه Remote Validation را به همراه ValidateAntiForgeryToken استفاده کنیم، تنها کافی است نام فیلد مخفی آن‌را به لیست AdditionalFields به نحوی که مشاهده می‌کنید، اضافه کنیم.


کدهای کامل این سری را در مخزن کد DNT Identity می‌توانید ملاحظه کنید.
مطالب
آشنایی با Jaeger
 در سال‌های اخیر، معماری میکروسرویس، یکی از محبوب‌ترین روش‌ها برای طراحی نرم‌افزار بوده‌است. جهت بهبود کارآیی، رفع خطا، درک  عملکرد سیستم در محیط عملیاتی و  نمایش چگونگی فراخوانی سرویس‌ها توسط یکدیگر می‌توانیم از ابزار‌های distributed tracing استفاده کنیم. ابزارهای متنوعی برای این منظور وجود دارند، اما بطور کلی همه با روش مشابهی کار می‌کنند. اطلاعات مربوط به فعالیت‌هایی مثل فراخوانی سرویس و مراجعه به دیتابیس که درون میکروسرویس رخ می‌دهد، در یک span  ذخیره می‌شوند. Span‌های جداگانه توسط شناسه‌ای یکتا به هم مرتبط می‌شوند و به عنوان یک trace نمایش داده می‌شوند. با استفاده از این trace‌ها، مجموعه‌ای از اطلاعات مثل تاریخ شروع و پایان هر درخواست و هر فعالیت را در اختیار داریم. 


جهت گرفتن دیتای مربوط به هر span، درون هر میکروسرویس می‌توانیم از پروژه‌های متن باز OpenTracing  و یا  OpenTelemetry استفاده کنیم. کتابخانه OpenTracing.Contrib.NetCore پیاده سازی OpenTracing در دات نت می‌باشد و می‌تواند  فعالیت‌های مربوط به ASP.NET Core، Entity Framework Core System.Net.Http (HttpClient)، System.Data.SqlClient و Microsoft.Data.SqlClient را دریافت و به tracer  ارسال کند. 

برای پیاده سازی distributed tracing، می‌توانیم از ابزار متن باز و محبوب Jaeger (با تلفظ یِگِر)  که ابتدا توسط شرکت Uber منتشر شد، استفاده کنیم. نحوه کارکرد Jaeger بصورت زیر می‌باشد:




ساده‌ترین روش  برای راه‌اندازی Jager، استفاده از داکر ایمیج All in one که شامل ماژول های agent ، collector،  query  و ui  است. پورت 6831 مربوط به agent  و پورت 16686 مربوط به ui می‌باشد. برای جزئیات مربوط به ماژول‌های مختلف از این لینک استفاده کنید.

docker run -d -p 6831:6831/udp -p 6832:6832/udp -p 14268:14268 -p 14250:14250 -p 16686:16686 -p 5778:5778  
--name jaeger jaegertracing/all-in-one:latest

بعد از اجرای دستور بالا، اطلاعات مربوط به سرویس‌ها و trace ها  در ماژول Jager UI  با آدرس http://localhost:16686 قابل مشاهده است. 

جهت استفاده از Jaeger از پروژه تستی که شامل دو سرویس User و Gateway می‌باشد، استفاده می‌کنیم. در سرویس User، متد AddUser در صورت عدم وجود کاربر در دیتابیس، اطلاعات کاربر از گیت‌هاب را دریافت و در دیتابیس ذخیره می‌کند. سرویس Gateway از Ocelot برای مسیردهی درخواست‌ها استفاده می‌کند. برای آشنایی با ocelot‌ این پست را  مطالعه نمایید. 


    public async Task<ApiResult<Models.User>> AddUserAsync(string username)
        {

            var result = new ApiResult<Models.User>();
            
            var user = await _applicationDbContext.Users.FirstOrDefaultAsync(x => x.Login == username);

            if (user is null)
            {
                try
                {
                    var url = string.Format(_appConfig.Github.ProfileUrl, username);
                    var apiResult = await _httpClient.GetStringAsync(url);
                    var userDto = JsonSerializer.Deserialize<UserDto>(apiResult);
                    user = _mapper.Map<Models.User>(userDto);
                    await _applicationDbContext.Users.AddAsync(user);
                    await _applicationDbContext.SaveChangesAsync();
                    result.Result = user;
                    result.Message = "User successfully Created";
                    return result;
                }
                catch (Exception e)
                {
                    result.Message = "User not found";
                    return result;
                }
            }

            result.Message = "User already exist";
            result.Result = user;

            return result;

        }


برای ثبت Trace مربوط به درخواست‌ها در Jaeger ، بعد از نصب  پکیج‌های Jaeger و OpenTracing.Contrib.NetCore در هر دو سرویس، در کانفیگ هریک از سرویس‌ها مورد زیر را اضافه می‌کنیم:

"JaegerConfig": {
    "Host": "localhost",
    "Port": 6831,
    "IsEnabled": true,
  "SamplingRate": 0.5
  }


و برای اضافه شدن tracer به برنامه از متد الحاقی زیر استفاده می‌کنیم:

 public static class Extensions
    {
        public static void AddJaeger(this IServiceCollection services, IConfiguration configuration)
        {
            var config = configuration.GetSection("JaegerConfig").Get<JaegerConfig>();
            
            if (!(config?.IsEnabled ?? false))
                return;

            if (string.IsNullOrEmpty(config?.Host))
                throw new Exception("invalid JaegerConfig");

            services.AddSingleton<ITracer>(serviceProvider =>
            {
                string serviceName = Assembly.GetEntryAssembly()?.GetName().Name;

                ILoggerFactory loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();

                var sampler = new ProbabilisticSampler(config.SamplingRate); 

                var reporter = new RemoteReporter.Builder()
                    .WithLoggerFactory(loggerFactory)
                    .WithSender(new UdpSender(config.Host, config.Port, 0))
                    .WithFlushInterval(TimeSpan.FromSeconds(15))
                    .WithMaxQueueSize(300)
                    .Build();
                
                ITracer tracer = new Tracer.Builder(serviceName)
                    .WithLoggerFactory(loggerFactory)
                    .WithSampler(sampler)
                    .WithReporter(reporter)
                    .Build();

                GlobalTracer.Register(tracer);

                return tracer;
            });

            services.AddOpenTracing();
        }
    }


برای ثبت trace‌ها استراتژی‌های متفاوتی وجود دارد. در اینجا از ProbabilisticSampler استفاده شده‌است که در سازنده‌ی آن می‌توان درصد ثبت Trace‌ها را مقدار دهی کرد. در نهایت این متد الحاقی را در Startup اضافه می‌کنیم:

builder.Services.AddJaeger(builder.Configuration);


بعد از اجرای پروژه و فراخوانی https://localhost:6000/gateway/Users/Add ، سرویس Gateway، درخواست را به سرویس User ارسال می‌کند و این سرویس‌ها در  Jaeger UI  قابل مشاهده هستند.




جهت مشاهده trace ‌ها ، سرویس مورد نظر را انتخاب و روی Find Traces کلیک کنید. با کلیک روی Trace مورد نظر، جزئیات فعالیت هایی مثل فراخوانی سرویس و مراجعه به دیتابیس قابل مشاهده است. 


برای اضافه کردن لاگ سفارشی به یک span، می‌توان از اینترفیس ITracer  استفاده کرد:

        private readonly IUserService _userService;
        private readonly ITracer _tracer;

        public UsersController(IUserService userService, ITracer tracer)
        {
            _userService = userService;
            _tracer = tracer;
        }
        [HttpPost]
        public async Task<ActionResult> AddUser(AddUserDto model)
        {
            var actionName = ControllerContext.ActionDescriptor.DisplayName;
            
            using var scope = _tracer.BuildSpan(actionName).StartActive(true);
            
            scope.Span.Log($"Add user log username: {model.Username}");
            
            return Ok(await _userService.AddUserAsync(model.Username));
        }  



کدهای مربوط به این مطلب در اینجا قابل دسترسی است. 

مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت پنجم - پیاده سازی ورود و خروج از سیستم
پس از راه اندازی IdentityServer، نوبت به امن سازی برنامه‌ی Mvc Client توسط آن می‌رسد و اولین قسمت آن، ورود به سیستم و خروج از آن می‌باشد.


بررسی اجزای Hybrid Flow

در قسمت سوم در حین «انتخاب OpenID Connect Flow مناسب برای یک برنامه‌ی کلاینت از نوع ASP.NET Core» به این نتیجه رسیدیم که Flow مناسب یک برنامه‌ی Mvc Client از نوع Hybrid است. در اینجا هر Flow، شروع به ارسال درخواستی به سمت Authorization Endpoint می‌کند؛ با یک چنین قالبی:
https://idpHostAddress/connect/authorize? 
client_id=imagegalleryclient 
&redirect_uri=https://clientapphostaddress/signin-oidcoidc 
&scope=openid profile 
&response_type=code id_token 
&response_mode=form_post
&nonce=63626...n2eNMxA0
- در سطر اول، Authorization Endpoint مشخص شده‌است. این آدرس از discovery endpoint که یک نمونه تصویر محتوای آن‌را در قسمت قبل مشاهده کردید، استخراج می‌شود.
- سپس client_id جهت تعیین برنامه‌ای که درخواست را ارسال می‌کند، ذکر شده‌است؛ از این جهت که یک IDP جهت کار با چندین نوع کلاینت مختلف طراحی شده‌است.
- redirect_uri همان Redirect Endpoint است که در سطح برنامه‌ی کلاینت تنظیم می‌شود.
- در مورد scope در قسمت قبل در حین راه اندازی IdentityServer توضیح دادیم. در اینجا برنامه‌ی کلاینت، درخواست scopeهای openid و profile را داده‌است. به این معنا که نیاز دارد تا Id کاربر وارد شده‌ی به سیستم و همچنین Claims منتسب به او را در اختیار داشته باشد.
- response_type نیز به code id_token تنظیم شده‌است. توسط response_type، نوع Flow مورد استفاده مشخص می‌شود. ذکر code به معنای بکارگیری Authorization code flow است. ذکر id_token و یا id_token token هر دو به معنای استفاده‌ی از implicit flow است. اما برای مشخص سازی Hybrid flow یکی از سه مقدار code id_token و یا code token و یا code id_token token با هم ذکر می‌شوند:


- در اینجا response_mode مشخص می‌کند که اطلاعات بازگشتی از سمت IDP که توسط response_type مشخص شده‌اند، با چه قالبی به سمت کلاینت بازگشت داده شوند که می‌تواند از طریق Form POST و یا URI باشد.


در Hybrid flow با response_type از نوع code id_token، ابتدا کلاینت یک درخواست Authentication را به Authorization Endpoint ارسال می‌کند (با همان قالب URL فوق). سپس در سطح IDP، کاربر برای مثال با ارائه‌ی کلمه‌ی عبور و نام کاربری، تعیین اعتبار می‌شود. همچنین در اینجا IDP ممکن است رضایت کاربر را از دسترسی به اطلاعات پروفایل او نیز سؤال بپرسد (تحت عنوان مفهوم Consent). سپس IDP توسط یک Redirection و یا Form POST، اطلاعات authorization code و identity token را به سمت برنامه‌ی کلاینت ارسال می‌کند. این همان اطلاعات مرتبط با response_type ای است که درخواست کرد‌ه‌ایم. سپس برنامه‌ی کلاینت این اطلاعات را تعیین اعتبار کرده و در صورت موفقیت آمیز بودن این عملیات، اکنون درخواست تولید توکن هویت را به token endpoint ارسال می‌کند. برای این منظور کلاینت سه مشخصه‌ی authorization code ،client-id و client-secret را به سمت token endpoint ارسال می‌کند. در پاسخ یک identity token را دریافت می‌کنیم. در اینجا مجددا این توکن تعیین اعتبار شده و سپس Id کاربر را از آن استخراج می‌کند که در برنامه‌ی کلاینت قابل استفاده خواهد بود. این مراحل را در تصویر زیر می‌توانید ملاحظه کنید.
البته اگر دقت کرده باشید، یک identity token در همان ابتدای کار از Authorization Endpoint دریافت می‌شود. اما چرا از آن استفاده نمی‌کنیم؟ علت اینجا است که token endpoint نیاز به اعتبارسنجی client را نیز دارد. به این ترتیب یک لایه‌ی امنیتی دیگر نیز در اینجا بکار گرفته می‌شود. همچنین access token و refresh token نیز از همین token endpoint قابل دریافت هستند.




تنظیم IdentityServer جهت انجام عملیات ورود به سیستم بر اساس جزئیات Hybrid Flow

برای افزودن قسمت لاگین به برنامه‌ی MVC قسمت دوم، نیاز است تغییراتی را در برنامه‌ی کلاینت و همچنین IDP اعمال کنیم. برای این منظور کلاس Config پروژه‌ی IDP را که در قسمت قبل ایجاد کردیم، به صورت زیر تکمیل می‌کنیم:
namespace DNT.IDP
{
    public static class Config
    {
        public static IEnumerable<Client> GetClients()
        {
            return new List<Client>
            {
                new Client
                {
                    ClientName = "Image Gallery",
                    ClientId = "imagegalleryclient",
                    AllowedGrantTypes = GrantTypes.Hybrid,
                    RedirectUris = new List<string>
                    {
                        "https://localhost:5001/signin-oidc"
                    },
                    PostLogoutRedirectUris = new List<string>
                    {
                        "https://localhost:5001/signout-callback-oidc"
                    },
                    AllowedScopes =
                    {
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile
                    },
                    ClientSecrets =
                    {
                        new Secret("secret".Sha256())
                    }
                }
             };
        }
    }
}
در اینجا بجای بازگشت لیست خالی کلاینت‌ها، یک کلاینت جدید را تعریف و تکمیل کرده‌ایم.
- ابتدا نام کلاینت را مشخص می‌کنیم. این نام و عنوان، در صفحه‌ی لاگین و Consent (رضایت دسترسی به اطلاعات پروفایل کاربر)، ظاهر می‌شود.
- همچنین نیاز است یک Id دلخواه را نیز برای آن مشخص کنیم؛ مانند imagegalleryclient در اینجا.
- AllowedGrantTypes را نیز به Hybrid Flow تنظیم کرده‌ایم. علت آن‌را در قسمت سوم این سری بررسی کردیم.
- با توجه به اینکه Hybrid Flow از Redirectها استفاده می‌کند و اطلاعات نهایی را به کلاینت از طریق Redirection ارسال می‌کند، به همین جهت آدرس RedirectUris را به آدرس برنامه‌ی Mvc Client تنظیم کرده‌ایم (که در اینجا بر روی پورت 5001 کار می‌کند). قسمت signin-oidc آن‌را در ادامه تکمیل خواهیم کرد.
- در قسمت AllowedScopes، لیست scopeهای مجاز قابل دسترسی توسط این کلاینت مشخص شده‌اند که شامل دسترسی به ID کاربر و Claims آن است.
- به ClientSecrets نیز جهت client authenticating نیاز داریم.


تنظیم برنامه‌ی MVC Client جهت انجام عملیات ورود به سیستم بر اساس جزئیات Hybrid Flow

برای افزودن قسمت لاگین به سیستم، کلاس آغازین پروژه‌ی MVC Client را به نحو زیر تکمیل می‌کنیم:
namespace ImageGallery.MvcClient.WebApp
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(options =>
            {
                options.DefaultScheme = "Cookies";
                options.DefaultChallengeScheme = "oidc";
            }).AddCookie("Cookies")
              .AddOpenIdConnect("oidc", options =>
              {
                  options.SignInScheme = "Cookies";
                  options.Authority = "https://localhost:6001";
                  options.ClientId = "imagegalleryclient";
                  options.ResponseType = "code id_token";
                  //options.CallbackPath = new PathString("...")
                  //options.SignedOutCallbackPath = new PathString("...")
                  options.Scope.Add("openid");
                  options.Scope.Add("profile");
                  options.SaveTokens = true;
                  options.ClientSecret = "secret";
                  options.GetClaimsFromUserInfoEndpoint = true;
              });
این قسمت تنظیمات، سمت کلاینت OpenID Connect Flow را مدیریت می‌کند.

- ابتدا با فراخوانی AddAuthentication، کار تنظیمات میان‌افزار استاندارد Authentication برنامه‌های ASP.NET Core انجام می‌شود. در اینجا DefaultScheme آن به Cookies تنظیم شده‌است تا عملیات Sign-in و Sign-out سمت کلاینت را میسر کند. سپس DefaultChallengeScheme به oidc تنظیم شده‌است. این مقدار با Scheme ای که در ادامه آن‌را تنظیم خواهیم کرد، تطابق دارد.

- سپس متد AddCookie فراخوانی شده‌است که authentication-Scheme را به عنوان پارامتر قبول می‌کند. به این ترتیب cookie based authentication در برنامه میسر می‌شود. پس از اعتبارسنجی توکن هویت دریافتی و تبدیل آن به Claims Identity، در یک کوکی رمزنگاری شده برای استفاده‌های بعدی ذخیره می‌شود.

- در آخر تنظیمات پروتکل OpenID Connect را ملاحظه می‌کنید. به این ترتیب مراحل اعتبارسنجی توسط این پروتکل در اینجا که Hybrid flow است، پشتیبانی خواهد شد.  اینجا است که کار درخواست Authorization، دریافت و اعتبارسنجی توکن هویت صورت می‌گیرد. اولین پارامتر آن authentication-Scheme است که به oidc تنظیم شده‌است. به این ترتیب اگر قسمتی از برنامه نیاز به Authentication داشته باشد، OpenID Connect به صورت پیش‌فرض مورد استفاده قرار می‌گیرد. به همین جهت DefaultChallengeScheme را نیز به oidc تنظیم کردیم. در اینجا SignInScheme به Cookies تنظیم شده‌است که با DefaultScheme اعتبارسنجی تطابق دارد. به این ترتیب نتیجه‌ی موفقیت آمیز عملیات اعتبارسنجی در یک کوکی رمزنگاری شده ذخیره خواهد شد. مقدار خاصیت Authority به آدرس IDP تنظیم می‌شود که بر روی پورت 6001 قرار دارد. تنظیم این مسیر سبب خواهد شد تا این میان‌افزار سمت کلاینت، به discovery endpoint دسترسی یافته و بتواند مقادیر سایر endpoints برنامه‌ی IDP را به صورت خودکار دریافت و استفاده کند. سپس ClientId تنظیم شده‌است که باید با مقدار تنظیم شده‌ی آن در سمت IDP یکی باشد و همچنین مقدار ClientSecret در اینجا نیز باید با ClientSecrets سمت IDP یکی باشد. ResponseType تنظیم شده‌ی در اینجا با AllowedGrantTypes سمت IDP تطابق دارد که از نوع Hybrid است. سپس دو scope درخواستی توسط این برنامه‌ی کلاینت که openid و profile هستند در اینجا اضافه شده‌اند. به این ترتیب می‌توان به مقادیر Id کاربر و claims او دسترسی داشت. مقدار CallbackPath در اینجا به RedirectUris سمت IDP اشاره می‌کند که مقدار پیش‌فرض آن همان signin-oidc است. با تنظیم SaveTokens به true امکان استفاده‌ی مجدد از آن‌ها را میسر می‌کند.

پس از تکمیل قسمت ConfigureServices و انجام تنظیمات میان‌افزار اعتبارسنجی، نیاز است این میان‌افزار را نیز به برنامه افزود که توسط متد UseAuthentication انجام می‌شود:
namespace ImageGallery.MvcClient.WebApp
{
    public class Startup
    {
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseAuthentication();

پس از این تنظیمات، با اعمال ویژگی Authorize، دسترسی به کنترلر گالری برنامه‌ی MVC Client را صرفا محدود به کاربران وارد شده‌ی به سیستم می‌کنیم:
namespace ImageGallery.MvcClient.WebApp.Controllers
{
    [Authorize]
    public class GalleryController : Controller
    {
    // .... 
   
        public async Task WriteOutIdentityInformation()
        {
            var identityToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.IdToken);
            Debug.WriteLine($"Identity token: {identityToken}");

            foreach (var claim in User.Claims)
            {
                Debug.WriteLine($"Claim type: {claim.Type} - Claim value: {claim.Value}");
            }
        }
در اینجا علاوه بر اعمال فیلتر Authorize به کل اکشن متدهای این کنترلر، یک اکشن متد جدید دیگر را نیز به انتهای آن اضافه کرده‌ایم تا صرفا جهت دیباگ برنامه، اطلاعات دریافتی از IDP را در Debug Window، برای بررسی بیشتر درج کند. البته این روش با Debug Window مخصوص Visual Studio کار می‌کند. اگر می‌خواهید آن‌را در صفحه‌ی کنسول dotnet run مشاهده کنید، بجای Debug باید از ILogger استفاده کرد.

فراخوانی متد GetTokenAsync با پارامتر IdToken، همان Identity token دریافتی از IDP را بازگشت می‌دهد. این توکن با تنظیم SaveTokens به true در تنظیمات AddOpenIdConnect که پیشتر انجام دادیم، قابل استخراج از کوکی اعتبارسنجی برنامه شده‌است.
این متد را در ابتدای اکشن متد Index فراخوانی می‌کنیم:
        public async Task<IActionResult> Index()
        {
            await WriteOutIdentityInformation();
            // ....


اجرای برنامه جهت آزمایش تنظیمات انجام شده

برای اجرای برنامه:
- ابتدا به پوشه‌ی 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 را درخواست کنید:


در این حالت چون فیلتر Authorize به کل اکشن متدهای کنترلر گالری اعمال شده، میان‌افزار Authentication که در فایل آغازین برنامه‌ی کلاینت MVC تنظیم شده‌است، وارد عمل شده و کاربر را به صفحه‌ی لاگین سمت IDP هدایت می‌کند (شماره پورت آن 6001 است). لاگ این اعمال را هم در برگه‌ی network مرورگر می‌تواند مشاهده کنید.

در اینجا نام کاربری و کلمه‌ی عبور اولین کاربر تعریف شده‌ی در فایل Config.cs برنامه‌ی IDP را که User 1 و password است، وارد می‌کنیم. پس از آن صفحه‌ی Consent ظاهر می‌شود:


در اینجا از کاربر سؤال می‌پرسد که آیا به برنامه‌ی کلاینت اجازه می‌دهید تا به Id و اطلاعات پروفایل و یا همان Claims شما دسترسی پیدا کند؟
فعلا گزینه‌ی remember my design را انتخاب نکنید تا همواره بتوان این صفحه را در دفعات بعدی نیز مشاهده کرد. سپس بر روی گزینه‌ی Yes, Allow کلیک کنید.
اکنون به صورت خودکار به سمت برنامه‌ی MVC Client هدایت شده و می‌توانیم اطلاعات صفحه‌ی اول سایت را کاملا مشاهده کنیم (چون کاربر اعتبارسنجی شده‌است، از فیلتر Authorize رد خواهد شد).


همچنین در اینجا اطلاعات زیادی نیز جهت دیباگ برنامه لاگ می‌شوند که در آینده جهت عیب یابی آن می‌توانند بسیار مفید باشند:


با دنبال کردن این لاگ می‌توانید مراحل Hybrid Flow را مرحله به مرحله با مشاهده‌ی ریز جزئیات آن بررسی کنید. این مراحل به صورت خودکار توسط میان‌افزار Authentication انجام می‌شوند و در نهایت اطلاعات توکن‌های دریافتی به صورت خودکار در اختیار برنامه برای استفاده قرار می‌گیرند. یعنی هم اکنون کوکی رمزنگاری شده‌ی اطلاعات اعتبارسنجی کاربر در دسترس است و به اطلاعات آن می‌توان توسط شیء this.User، در اکشن متدهای برنامه‌ی MVC، دسترسی داشت.


تنظیم برنامه‌ی MVC Client جهت انجام عملیات خروج از سیستم

ابتدا نیاز است یک لینک خروج از سیستم را به برنامه‌ی کلاینت اضافه کنیم. برای این منظور به فایل Views\Shared\_Layout.cshtml مراجعه کرده و لینک logout را در صورت IsAuthenticated بودن کاربر جاری وارد شده‌ی به سیستم، نمایش می‌دهیم:
<div class="navbar-collapse collapse">
    <ul class="nav navbar-nav">
        <li><a asp-area="" asp-controller="Gallery" asp-action="Index">Home</a></li>
        <li><a asp-area="" asp-controller="Gallery" asp-action="AddImage">Add an image</a></li>
        @if (User.Identity.IsAuthenticated)
        {
            <li><a asp-area="" asp-controller="Gallery" asp-action="Logout">Logout</a></li>
        }
    </ul>
</div>


شیء this.User، هم در اکشن متدها و هم در Viewهای برنامه، جهت دسترسی به اطلاعات کاربر اعتبارسنجی شده، در دسترس است.
این لینک به اکشن متد Logout، در کنترلر گالری اشاره می‌کند که آن‌را به صورت زیر تکمیل خواهیم کرد:
namespace ImageGallery.MvcClient.WebApp.Controllers
{
    [Authorize]
    public class GalleryController : Controller
    {
        public async Task Logout()
        {
            // Clears the  local cookie ("Cookies" must match the name of the scheme)
            await HttpContext.SignOutAsync("Cookies");
            await HttpContext.SignOutAsync("oidc");
        }
در اینجا ابتدا کوکی Authentication حذف می‌شود. نامی که در اینجا انتخاب می‌شود باید با نام scheme انتخابی مرتبط در فایل آغازین برنامه یکی باشد.
سپس نیاز است از برنامه‌ی IDP نیز logout شویم. به همین جهت سطر دوم SignOutAsync با پارامتر oidc را مشاهده می‌کنید. بدون وجود این سطر، کاربر فقط از برنامه‌ی کلاینت logout می‌شود؛ اما اگر به IDP مجددا هدایت شود، مشاهده خواهد کرد که در آن سمت، هنوز نام کاربری او توسط IDP شناسایی می‌شود.


بهبود تجربه‌ی کاربری Logout

پس از logout، بدون انجام یکسری از تنظیمات، کاربر مجددا به برنامه‌ی کلاینت به صورت خودکار هدایت نخواهد شد و در همان سمت IDP متوقف می‌شد. برای بهبود این وضعیت و بازگشت مجدد به برنامه‌ی کلاینت، اینکار را یا توسط مقدار دهی خاصیت SignedOutCallbackPath مربوط به متد AddOpenIdConnect می‌توان انجام داد و یا بهتر است مقدار پیش‌فرض آن‌را به تنظیمات IDP نسبت داد که پیشتر در تنظیمات متد GetClients آن‌را ذکر کرده بودیم:
PostLogoutRedirectUris = new List<string>
{
     "https://localhost:5001/signout-callback-oidc"
},
با وجود این تنظیم، اکنون IDP می‌داند که پس از logout، چه آدرسی را باید به کاربر جهت بازگشت به سیستم قبلی ارائه دهد:


البته هنوز یک مرحله‌ی انتخاب و کلیک بر روی لینک بازگشت وجود دارد. برای حذف آن و خودکار کردن Redirect نهایی آن، می‌توان کدهای IdentityServer4.Quickstart.UI را که در قسمت قبل به برنامه‌ی IDP اضافه کردیم، اندکی تغییر دهیم. برای این منظور فایل src\IDP\DNT.IDP\Quickstart\Account\AccountOptions.cs را گشوده و سپس فیلد AutomaticRedirectAfterSignOut را که false است، به true تغییر دهید.

 
تنظیمات بازگشت Claims کاربر به برنامه‌ی کلاینت

به صورت پیش‌فرض، Identity Server اطلاعات Claims کاربر را ارسال نمی‌کند و Identity token صرفا به همراه اطلاعات Id کاربر است. برای تنظیم آن می‌توان در سمت تنظیمات IDP، در متد GetClients، زمانیکه new Client صورت می‌گیرد، خاصیت AlwaysIncludeUserClaimsInIdToken هر کلاینت را به true تنظیم کرد؛ اما ایده خوبی نیست. Identity token از طریق Authorization endpoint دریافت می‌شود. در اینجا اگر این اطلاعات از طریق URI دریافت شود و Claims به Identity token افزوده شوند، به مشکل بیش از حد طولانی شدن URL نهایی خواهیم رسید و ممکن است از طرف وب سرور یک چنین درخواستی برگشت بخورد. به همین جهت به صورت پیش‌فرض اطلاعات Claims به Identity token اضافه نمی‌شوند.
در اینجا برای دریافت Claims، یک endpoint دیگر در IDP به نام UserInfo endpoint درنظر گرفته شده‌است. در این حالت برنامه‌ی کلاینت، مقدار Access token دریافتی را که به همراه اطلاعات scopes متناظر با Claims است، به سمت UserInfo endpoint ارسال می‌کند. باید دقت داشت زمانیکه Identity token دوم از Token endpoint دریافت می‌شود (تصویر ابتدای بحث)، به همراه آن یک Access token نیز صادر و ارسال می‌گردد. اینجا است که میان‌افزار oidc، این توکن دسترسی را به سمت UserInfo endpoint ارسال می‌کند تا user claims را دریافت کند:


در تنظیمات سمت کلاینت AddOpenIdConnect، درخواست openid و profile، یعنی درخواست Id کاربر و Claims آن وجود دارند:
options.Scope.Add("openid");
options.Scope.Add("profile");
برای بازگشت آن‌ها به سمت کلاینت، درخواست دریافت claims از UserInfo Endpoint را در سمت کلاینت تنظیم می‌کنیم:
options.GetClaimsFromUserInfoEndpoint = true;
همین اندازه تنظیم میان‌افزار oidc، برای انجام خودکار کل گردش کاری یاد شده کافی است.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشه‌ی 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 وارد کنید.
مطالب
شروع به کار با EF Core 1.0 - قسمت 2 - به روز رسانی ساختار بانک اطلاعاتی
پس از برپایی تنظیمات اولیه‌ی کار با EF Core در ASP.NET Core، اکنون نوبت به تبدیل کلاس Person، به جدول معادل آن در بانک اطلاعاتی برنامه است. در EF Core نیز همانند EF Code First 6.x، برای انجام یک چنین اعمالی از مفهومی به نام Migrations استفاده می‌شود که در ادامه به آن خواهیم پرداخت.


پیشنیازهای کار با EF Core Migrations

در قسمت قبل در حین بررسی «برپایی تنظیمات اولیه‌ی EF Core 1.0 در یک برنامه‌ی ASP.NET Core 1.0»، چهار مدخل جدید را به فایل project.json برنامه اضافه کردیم. مدخل جدید Microsoft.EntityFrameworkCore.Tools که به قسمت tools آن اضافه شد، پیشنیاز اصلی کار با EF Core Migrations است.


بررسی ابزارهای خط فرمان EF Core و تشکیل ساختار بانک اطلاعاتی بر اساس کلاس‌های برنامه

پس از تکمیل پیشنیازهای کار با EF Core، از طریق خط فرمان به پوشه‌ی جاری پروژه وارد شده و دستور dotnet ef را صادر کنید.
یک نکته: در ویندوز اگر در پوشه‌ای، کلید shift را نگه دارید و در آن پوشه کلیک راست کنید، در منوی باز شده، گزینه‌ی جدیدی را به نام Open command window here مشاهده خواهید کرد که می‌تواند به سرعت خط فرمان را از پوشه‌ی جاری شروع کند.

خروجی صدور فرمان dotnet ef را در ذیل مشاهده می‌کنید:
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef
                     _/\__
               ---==/    \\
         ___  ___   |.    \|\
        | __|| __|  |  )   \\\
        | _| | _|   \_/ |  //|\\
        |___||_|       /   \\\/\\
Entity Framework .NET Core CLI Commands 1.0.0-preview2-21431
Usage: dotnet ef [options] [command]
Options:
  -h|--help                      Show help information
  -v|--verbose                   Enable verbose output
  --version                      Show version information
  --assembly <ASSEMBLY>          The assembly file to load.
  --startup-assembly <ASSEMBLY>  The assembly file containing the startup class.
  --data-dir <DIR>               The folder used as the data directory (defaults to current working directory).
  --project-dir <DIR>            The folder used as the project directory (defaults to current working directory).
  --content-root-path <DIR>      The folder used as the content root path for the application (defaults to application base directory).
  --root-namespace <NAMESPACE>   The root namespace of the target project (defaults to the project assembly name).
Commands:
  database    Commands to manage your database
  dbcontext   Commands to manage your DbContext types
  migrations  Commands to manage your migrations
Use "dotnet ef [command] --help" for more information about a command.
در قسمت Commands آن در انتهای لیست، از فرمان migrations آن استفاده خواهیم کرد. برای این منظور در همین پوشه‌ی جاری، دستور ذیل را صادر کنید:
 D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef migrations add InitialDatabase
دستورات migrations با dotnet ef migrations شروع شده و سپس یک سری پارامتر را دریافت می‌کنند برای مثال در اینجا سوئیچ add، به همراه یک نام دلخواه ذکر شده‌است (نام این مرحله را InitialDatabase گذاشته‌ایم). پس از فراخوانی این دستور، اگر به Solution explorer مراجعه کنید، پوشه‌ی جدید Migrations، قابل مشاهده است:


نام دلخواه InitialDatabase را در انتهای نام فایل 13950526050417_InitialDatabase مشاهده می‌کنید.
اگر قصد حذف این مرحله را داشته باشیم، می‌توان دستور dotnet ef migrations remove را مجددا صادر کرد.

فایل 13950526050417_InitialDatabase به همراه کلاسی است که در آن دو متد Up و Down قابل مشاهده هستند. متد Up نحوه‌ی ایجاد جدول جدیدی را از کلاس Person بیان می‌کند و متد Down نحوه‌ی Drop این جدول را پیاده سازی کرده‌است.
فایل ApplicationDbContextModelSnapshot.cs دارای کلاسی است که خلاصه‌ای از تعاریف موجودیت‌های ذکر شده‌ی در DB Context برنامه را به همراه دارد و تفسیر آن‌ها را از دیدگاه  EF در اینجا می‌توان مشاهده کرد.

پس از مرحله‌ی افزودن migrations، نوبت به اعمال آن به بانک اطلاعاتی است. تا اینجا EF تنها متدهای Up و Down مربوط به ساخت و حذف ساختار جداول را ایجاد کرده‌است. اما هنوز آن‌ها را به بانک اطلاعاتی برنامه اعمال نکرده‌است. برای اینکار در پوشه‌ی جاری دستور ذیل را صادر کنید:
 D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef database update
Applying migration '13950526050417_InitialDatabase'.
Done.
همانطور که ملاحظه می‌کنید، دستور dotnet ef database update سبب اعمال اطلاعات فایل 13950526050417_InitialDatabase به بانک اطلاعاتی شده‌است.
اکنون اگر به لیست بانک‌های اطلاعاتی مراجعه کنیم، بانک اطلاعاتی جدید TestDbCore2016 را به همراه جدول متناظر کلاس Person می‌توان مشاهده کرد:


در اینجا جدول دیگری به نام __EFMigrationsHistory نیز قابل مشاهده‌است که کار آن ذخیره سازی وضعیت فعلی Migrations در بانک اطلاعاتی، جهت مقایسه‌های آتی است. این جدول صرفا توسط ابزارهای EF استفاده می‌شود و نباید به صورت مستقیم تغییری در آن ایجاد کنید.


مقدار دهی اولیه‌ی جداول بانک‌های اطلاعاتی در EF Core

در همین حالت اگر کنترلر TestDB مطرح شده‌ی در انتهای بحث قسمت قبل را اجرا کنیم، به این استثناء خواهیم رسید:


این تصویر بدین معنا است که کار Migrations موفقیت آمیز بوده‌است و اینبار امکان اتصال و کار با بانک اطلاعاتی وجود دارد، اما این جدول حاوی اطلاعات اولیه‌ای برای نمایش نیست.
در نگارش قبلی EF Code First، امکانات Migrations به همراه یک متد Seed نیز بود که توسط آن کار مقدار دهی اولیه‌ی جداول را می‌توان انجام داد (زمانیکه جدولی ایجاد می‌شود، در همان هنگام، چند رکورد خاص نیز به آن اضافه شوند. برای مثال به جدول کاربران، رکورد اولین کاربر یا همان Admin اضافه شود). این متد در EF Core 1.0 وجود ندارد.
برای این منظور کلاس جدیدی را به نام ApplicationDbContextSeedData به همان پوشه‌ی جدید Migrations اضافه کنید؛ با این محتوا:
using System.Collections.Generic;
using System.Linq;
using Core1RtmEmptyTest.Entities;
using Microsoft.Extensions.DependencyInjection;

namespace Core1RtmEmptyTest.Migrations
{
    public static class ApplicationDbContextSeedData
    {
        public static void SeedData(this IServiceScopeFactory scopeFactory)
        {
            using (var serviceScope = scopeFactory.CreateScope())
            {
                var context = serviceScope.ServiceProvider.GetService<ApplicationDbContext>();
                if (!context.Persons.Any())
                {
                    var persons = new List<Person>
                    {
                        new Person
                        {
                            FirstName = "Admin",
                            LastName = "User"
                        }
                    };
                    context.AddRange(persons);
                    context.SaveChanges();
                }
            }
        }
    }
}
و سپس نحوه‌ی فراخوانی آن در متد Configure کلاس آغازین برنامه به صورت زیر است:
public void Configure(IServiceScopeFactory scopeFactory)
{
    scopeFactory.SeedData();
به همراه این تغییر در نحوه‌ی معرفی Db Context برنامه:
public void ConfigureServices(IServiceCollection services)
{
   services.AddDbContext<ApplicationDbContext>(ServiceLifetime.Scoped);
توضیحات:
- برای پیاده سازی الگوی واحد کار، اولین قدم، مشخص سازی طول عمر Db Context برنامه است. برای اینکه تنها یک Context در طول یک درخواست وهله سازی شود، نیاز است به نحو صریحی طول عمر آن‌را به حالت Scoped تنظیم کرد. متد AddDbContext دارای پارامتری است که این طول عمر را دریافت می‌کند. بنابراین در اینجا ServiceLifetime.Scoped ذکر شده‌است. همچنین در این مثال از نمونه‌ای که IConfigurationRoot به سازنده‌ی کلاس ApplicationDbContext تزریق شده (نکته‌ی انتهای بحث قسمت قبل)، استفاده شده‌است. به همین جهت تنظیمات options آن‌را ملاحظه نمی‌کنید.
- مرحله‌ی بعد نحوه‌ی دسترسی به این سرویس ثبت شده در یک کلاس static دارای متدی الحاقی است. در اینجا دیگر دسترسی مستقیمی به تزریق وابستگی‌ها نداریم و باید کار را با  IServiceScopeFactory شروع کنیم. در اینجا می‌توانیم به صورت دستی یک Scope را ایجاد کرده، سپس توسط ServiceProvider آن، به سرویس ApplicationDbContext دسترسی پیدا کنیم و در ادامه از آن به نحو متداولی استفاده نمائیم. IServiceScopeFactory جزو سرویس‌های توکار ASP.NET Core است و در صورت ذکر آن به عنوان پارامتر جدیدی در متد Configure، به صورت خودکار وهله سازی شده و در اختیار ما قرار می‌گیرد.
- نکته‌ی مهمی که در اینجا بکار رفته‌است، ایجاد Scope و dispose خودکار آن توسط عبارت using است. باید دقت داشت که ایجاد Scope و تخریب آن به صورت خودکار در ابتدا و انتهای درخواست‌ها توسط ASP.NET Core انجام می‌شود. اما چون شروع کار ما از متد Configure است، در اینجا خارج از Scope قرار داریم و باید مدیریت ایجاد و تخریب آن‌را به صورت دستی انجام دهیم که نمونه‌ای از آن‌را در متد SeedData کلاس ApplicationDbContextSeedData ملاحظه می‌کنید. در اینجا Scope ایی ایجاد شده‌است. سپس داده‌های اولیه‌ی مدنظر به بانک اطلاعاتی اضافه گردیده و در آخر این Scope تخریب شده‌است.
- اگر کار ایجاد و تخریب scope، به نحوی که مشخص شده‌است انجام نگیرد، طول عمر درخواستی خارج از Scope، همواره Singleton خواهد بود. چون خارج از طول عمر درخواست جاری قرار داریم و هنوز کار به سرویس دهی درخواست‌ها نرسیده‌است. بنابراین مدیریت Scopeها هنوز شروع نشده‌است و باید به صورت دستی انجام شود.

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


که به معنای کار کردن متد SeedData و ثبت اطلاعات اولیه‌ای در بانک اطلاعاتی است.


اعمال تغییرات به مدل‌های برنامه و به روز رسانی ساختار بانک اطلاعاتی

فرض کنید به کلاس Person قسمت قبل، خاصیت Age را هم اضافه کرده‌ایم:
namespace Core1RtmEmptyTest.Entities
{
    public class Person
    {
        public int PersonId { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }

        public int Age { get; set; }
    }
}
در این حالت اگر برنامه را اجرا کنیم، به استثنای ذیل خواهیم رسید:
 An unhandled exception occurred while processing the request.
SqlException: Invalid column name 'Age'.
برای رفع این مشکل نیاز است مجددا مراحل Migrations را اجرا کرد:
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef migrations add v2
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef database update
در اینجا همان دستورات قبل را مجددا اجرا می‌کنیم. با این تفاوت که اینبار نام دلخواه این مرحله را مثلا v2، به معنای نگارش دوم وارد کرده‌ایم.
با اجرا این دستورات، فایل جدید 13950526073248_v2 به پوشه‌ی Migrations اضافه می‌شود. این فایل حاوی نحوه‌ی به روز رسانی بانک اطلاعاتی، بر اساس خاصیت جدید Age است. سپس با اجرای دستور dotnet ef database update، کار به روز رسانی بانک اطلاعاتی بر اساس مرحله‌ی v2 انجام می‌شود.


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


تهیه اسکریپت تغییرات بجای اعمال تغییرات توسط ابزارهای EF

شاید علاقمند باشید که پیش از اعمال تغییرات به بانک اطلاعاتی، یک اسکریپت SQL از آن تهیه کنید (جهت مطالعه و یا اعمال دستی آن توسط خودتان). برای اینکار می‌توانید دستور ذیل را در پوشه‌ی جاری پروژه اجرا کنید:
 D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef migrations script -o v2.sql
در این حالت اسکریپت SQL تغییرات، در فایلی به نام v2.sql، در ریشه‌ی جاری پروژه تولید می‌شود.


تغییرات ساختار جدول __EFMigrationsHistory در EF Core 1.0


در EF 6.x، ساختار اطلاعات جدول نگهداری تاریخچه‌ی تغییرات، بسیار پیچیده بود و شامل رشته‌ای gzip شده‌ی حاوی یک snapshot از کل ساختار دیتابیس در هر مرحله‌ی migration بود. در این نگارش، این snapshot حذف شده‌است و بجای آن فایل ApplicationDbContextModelSnapshot.cs را مشاهده می‌کنید (تنها یک snapshot به ازای کل context برنامه). همچنین در اینجا کاملا مشخص است که چه مراحلی به بانک اطلاعاتی اعمال شده‌اند و دیگر خبری از رشته‌ی gzip شده‌ی قبلی نیست (تصویر فوق).

در شکل زیر ساختار قبلی این جدول را در EF 6.x مشاهده می‌کنید. در EF 6.x حتی فضای نام کلاس‌های موجودیت‌های برنامه هم مهم هستند و در صورت تغییر، مشکل ایجاد می‌شود:



مهاجرت خودکار از EF Core حذف شده‌است

در EF 6.x در کنار کلاس Db Context یک کلاس Configuration هم وجود داشت که برای مثال امکان چنین تعریفی در آن میسر هست:
public Configuration()
{
   AutomaticMigrationsEnabled = true;
}
کار آن مهاجرت خودکار اطلاعات context به بانک اطلاعاتی بود؛ بدون نیازی به استفاده از دستورات خط فرمان مرتبط. تمام این موارد از EF Core حذف شده‌اند و علت آن‌را می‌توانید در توضیحات یکی از اعضای تیم EF Core در اینجا مطالعه کنید و خلاصه‌ی آن به این شرح است:
با حذف مهاجرت خودکار:
- دیگر نیازی نیست تا model snapshots در بانک اطلاعاتی ذخیره شوند (همان ساده شدن ساختار جدول ذخیره سازی تاریخچه‌ی مهاجرت‌های فوق).
- در حالت افزودن یک مرحله‌ی مهاجرت، دیگر نیازی به کوئری گرفتن از بانک اطلاعاتی نخواهد بود (سرعت بیشتر).
- می‌توان چندین مرحله‌ی مهاجرت را افزود بدون اینکه الزاما مجبور به اعمال آن‌ها به بانک اطلاعاتی باشیم.
- کاهش کدهای مدیریت ساختار بانک اطلاعاتی.
- تیم‌ها برای یکی کردن تغییرات خود مشکلی نخواهند داشت چون دیگر snapshot مدل‌ها در جدول __EFMigrationsHistory ذخیره نمی‌شود.

بنابراین در EF Core می‌توان مهاجرت v1 را اضافه کرد. سپس تغییراتی را در کدها اعمال کرد. در ادامه مهاجرت v2 را تولید کرد و در آخر کار اعمال یکجای این‌ها را به بانک اطلاعاتی انجام داد.

هرچند در اینجا اگر می‌خواهید مرحله‌ی اجرای دستور dotnet ef database update را حذف کنید، می‌توانید از کدهای ذیل نیز استفاده نمائید:
using Core1RtmEmptyTest.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace Core1RtmEmptyTest.Migrations
{
    public static class DbInitialization
    {
        public static void Initialize(this IServiceScopeFactory scopeFactory)
        {
            using (var serviceScope = scopeFactory.CreateScope())
            {
                var context = serviceScope.ServiceProvider.GetService<ApplicationDbContext>();
                // Applies any pending migrations for the context to the database.
                // Will create the database if it does not already exist.
                context.Database.Migrate();
            }
        }
    }
}
روش فراخوانی آن نیز همانند روش فراخوانی متد SeedData است که پیشتر بحث شد.
کار متد Migrate، ایجاد بانک اطلاعاتی در صورت عدم وجود و سپس اعمال تمام مراحل migration ایی است که در جدول __EFMigrationsHistory ثبت نشده‌اند (دقیقا همان کار دستور dotnet ef database update را انجام می‌دهد).


تفاوت متد Database.EnsureCreated با متد Database.Migrate

اگر به متدهای context.Database دقت کنید، یکی از آن‌ها EnsureCreated نام دارد. این متد نیز سبب تولید بانک اطلاعاتی بر اساس ساختار Context برنامه می‌شود. اما هدف آن صرفا استفاده‌ی از آن در آزمون‌های واحد سریع است. از این جهت که جدول __EFMigrationsHistory را تولید نمی‌کند (برخلاف متد Migrate). بنابراین بجز آزمون‌های واحد، در جای دیگری از آن استفاده نکنید چون به دلیل عدم تولید جدول __EFMigrationsHistory توسط آن، قابلیت استفاده‌ی از بانک اطلاعاتی تولید شده‌ی توسط آن با امکانات migrations وجود ندارد. در پایان آزمون واحد نیز می‌توان از متد EnsureDeleted برای حذف این بانک اطلاعاتی موقتی استفاده کرد.



در قسمت بعد، مطالب تکمیلی مهاجرت‌ها را بررسی خواهیم کرد. برای مثال چگونه می‌توان کلاس‌های موجودیت‌ها را به اسمبلی‌های دیگری منتقل کرد.
مطالب
آشنایی با ساختار IIS قسمت دوازدهم
پیکربندی قسمت لاگ‌ها، میتواند برای یک سرور و یا وب سایت خاص از طریق فایل کانفیگ یا از طریق خود IIS انجام گیرد. برای اینکه به بیشتر این قابلیت‌ها در IIS دسترسی داشت، باید یکی از نسخه‌های ویندوز سرور 2012 و ویندوز 8 را نصب کرده باشید. لاگ‌ها به ثبت خطاها و درخواست‌های HTTP می‌پردازند و با تحلیل آن‌ها میتوان عملیات بهینه سازی را بر روی سرو اجرا کرد. تمامی ثبت لاگ‌ها توسط Http.sys انجام می‌گیرد.

نحوه‌ی ذخیره سازی لاگها
در این بخش نحوه‌ی ذخیره سازی و فرمت ذخیره‌ی لاگ‌ها را در دو سطح سایت و سرور به طور جداگانه بررسی می‌کنیم. در IIS ماژول Logging را باز کنید و در لیست One log file per می‌توانید مشخص کنید که لاگ‌ها در چه سطحی اجرا شوند. اگر گزینه‌ی server باشد، تمامی خطاها و درخواست‌های رسیده به سرور در یک فایل لاگ ثبت می‌شوند. ولی اگر سطح سایت باشد، برای هر سایت بر روی IIS لاگ‌ها، جداگانه بررسی می‌شوند. به طور پیش فرض سطح سایت انتخاب شده است.

سطح سایت
موقعی‌که در لیست، سایت را انتخاب کنید، در لیست format می‌توانید تعیین کنید که لاگ‌ها به چه صورتی باید ذخیره شوند. مواردی که در این حالت لیست می‌شوند گزینه‌های W3C,IIS,NCSA,Custom می‌باشند که در زیر یکایک آن‌ها را بررسی می‌کنیم:

فرمت IIS: این فرمت توسط مایکروسافت ارائه شده و در این حالت لاگ‌های همه‌ی وب سایت‌ها ذخیره می‌شوند. به این فرمت  Fixed ASCII Based Text نیز می‌گویند؛ چرا که اجازه‌ی خصوصی سازی ندارد و نمی‌توانید بگویید چه فیلدهایی در لاگ قرار داشته باشند. لاگ فایل‌های این فرمت با ، (کاما) از هم جدا می‌شوند و مقدار زمانی که برای هر فیلد ثبت می‌شود، به صورت محلی local Time می‌باشد.
فیلدهایی که در لاگ این نوع فرمت خواهند آمد، به شرح زیر است:
  • Client IP address 
  • User name 
  • Date 
  • Time 
  • Service and instance 
  • Server name 
  • Server IP address 
  • Time taken 
  • Client bytes sent 
  • Server bytes sent 
  • (Service status code (A value of 200 indicates that the request was fulfilled successfully 
  • (Windows status code (A value of 0 indicates that the request was fulfilled successfully. 
  • Request type 
  • Target of operation 
  • (Parameters (the parameters that are passed to a script 
احتمال این وجود دارد که بعضی از فیلدها در بعضی رکوردها، شامل اطلاعاتی نباشند که به جای مقدار آن علامت -  ثبت می‌گردد و برای کاراکترهایی که قابل نمایش نیستند یا کاراکتر نمایشی ندارند، از علامت + استفاده می‌شود. دلیل اینکار هم این است که ممکن است یک کاربر مهاجم، به ارسال اطلاعات کلیدهای کنترلی چون Carriage return اختصارا CR یا Line Feed به اختصار LF کند، که باعث شکسته شدن خط لاگ فایل می‌شود و در نتیجه از استاندارد خارج خواهد شد و هنگام خواندن آن هم با خطا روبرو می‌شویم؛ در نتیجه با جایگزینی چنین کاراکترهایی با + از این اتفاق جلوگیری می‌شود.
شکل زیر نمونه ای از یک خط لاگ در این فرمت است:
192.168.114.201, -, 03/20/01, 7:55:20, W3SVC2, SERVER, 172.21.13.45, 4502, 163, 3223, 200, 0, GET, /DeptLogo.gif, -,
 نام فیلد نوع حالت مقداردهی  توضیح اتفاقات افتاده
 Client IP address   192.168.114.201   آی پی کلاینت
 User name   -  کاربر ناشناس است
 Date  03/20/01   تاریخ فعالیت
 Time  7:55:20  ساعت فعالیت 
 Service and instance  W3SVC2  لاگی که مربوط به سایت خاصی می‌شود به صورت  #W3SVC  نمایش داده می‌شود که علامت # شماره سایت می‌باشد که در اینجا این لاگ مربوط به سایت شماره 2 است 
 Server name   SERVER   نام سرور
 Server IP   172.21.13.45   آی پی سرور
 Time taken  4502   چقدر انجام عملیات این درخواست به طول انجامیده است که بر حسب میلی ثانیه است.
 Client bytes sent   163   تعداد بایت هایی که از طرف کلاینت به سرور ارسال شده است
 Server bytes sent   3223   تعداد بایت هایی که از طرف سرور به سمت کلاینت ارسال شده است
 Service status code   200   درخواست کاملا موفقیت آمیز بوده است
 Windows status code  0  درخواست کاملا موفقیت آمیز بوده است
 Request type  GET   نوع درخواست کاربر
 Target of operation   /DeptLogo.gif   کاربر قصد دانلود یک فایل تصویری GIF داشته است که نامش Deptlogo است
 Parameters   -  پارامتری ارسال نشده است

فرمت NCSA:
 این فرمت توسط مرکز علمی کاربردهای ابرمحاسباتی National Center for Supercomputing Applications ایجاد شده و دقیقا مانند قبلی نمیتوان در آن نوع فیلدها را مشخص کرد و برای جدا سازی، از فاصله space استفاده می‌کند و ثبت مقدار زمان در آن هم به صورت محلی و هم UTC می‌باشد.
این فیلدها در لاگ آن نمایش داده می‌شوند:
  • Remote host address 
  • (Remote log name (This value is always a hyphen 
  • User name 
  • Date, time, and Greenwich mean time (GMT) offset 
  • Request and protocol version 
  • (Service status code (A value of 200 indicates that the request was fulfilled successfully   
  • Bytes sen 

نمونه ای از یک لاگ ثبت شده:
172.21.13.45 - Microsoft\JohnDoe [08/Apr/2001:17:39:04 -0800] "GET /scripts/iisadmin/ism.dll?http/serv HTTP/1.0" 200 3401
نام فیلد  مقدار ثبت شده  توضیح اتفاق افتاده 
 Remote host address  172.21.13.45  آی پی کلاینت
 Remote log name  - نامی وجود ندارد 
 User name  Microsoft\JohnDoe  نام کاربری
 Date, time, and GMT offset  [08/Apr/2001:17:39:04 -0800]  تاریخ و ساعت فعالیت به صورت محلی که 8 ساعت از مبدا گرینویچ بیشتر است
 Request and protocol version  GET /scripts/iisadmin/ism.dll?http/serv HTTP/1.0  کاربر با متد GET و Http نسخه‌ی یک، درخواست فایل ism.dll را کرده است.
 Service status code  200  عملیات کاملا موفقیت آمیز بود.
 Bytes sent  3401  تعداد بایت‌های ارسال شده به سمت کاربر

امنیت در برابر کاربران مهاجم مانند همان فرمت قبلی صورت گرفته است.


فرمت W3C: توسط W3C توسط کنسرسیوم جهانی وب ارائه شده است و یک فرمت customizable ASCII text-based است. به این معنی که میتوان فیلدهایی که در گزارش نهایی می‌آید را خودتان مشخص کنید، که برای اینکار در کنار لیست، دکمه‌ی Select وجود دارد که میتوانید هر کدام از فیلد‌هایی را که خواستید، انتخاب کنید تا به ترتیب در خط لاگ ظاهر شوند. تاریخ ثبت به صورت UTC است.

نام فیلد   توضیح به طور پیش فرض انتخاب شده است 
 Date  تاریخ رخ دادن فعالیت  بله
 Time  ساعت زخ دادن فعالیت بر اساس UTC  بله
 Client IP Address آی پی کلاینت  بله
 User Name نام کاربری که هویت آن تایید شده و در صورتی که هویت تایید شده نباشد و کاربر ناشناس باشد، جای آن - قرار می‌گیرد   بله
 Service Name and Instance Number  نام و شماره سایتی که درخواست در آن صورت گرفته است  خیر
 Server Name  نام سروری که لاگ روی آن ثبت می‌شود  خیر
 Server IP Address  آی پی سرور که لاگ روی آن ثبت می‌شود  بله
 Server Port  شمره پورتی که سرویس مورد نظر روی آن پورت اعمال می‌شود.  بله
 Method  متد درخواست مثل GET  بله
 URI Stem   هدف درخواست یا Target مثل index.htm  بله
 URI Query  کوئری ارسال شده برای صفحات داینامیک  بله
 HTTP Status  کد وضیعینی HTTP status  بله
 Win32 Status  کد وضعیتی ویندوز  خیر
 Bytes Sent  تعداد بایت‌های ارسال شده به سمت کلاینت  خیر
 Bytes Received  تعداد بایت‌های دریافت شده از سمت کلاینت  خیر
 Time Taken  زمان به طول انجامیدن درخواست بر حسب میلی ثانیه  خیر
 Protocol Version درخواست با چه نسخه‌ای از پروتکل http یا ftp ارسال شده است  خیر
 Host  اگر در هدر درخواست ارسالی این گزینه بوده باشد، نوشته خواهد شد.  خیر
 User Agent  اطلاعات را از هدر درخواست می‌گیرد.  بله
 Cookie اگر کوکی رد و بدل شده باشد، محتویات کوکی ارسالی یا دریافت شده  خیر
 Referrer  کاربر از چه سایتی به سمت سایت ما آمده است.  خیر
 Protocol Substatus 
 در صورت رخ دادن خطا در IIS ، کد خطا بازگردانده میشود. در IIS به منظور امنیت بیشتر و کاهش حملات، محتوای خطاهای رخ داده در IIS به صورت متنی نمایش داده نمی‌شوند و شامل کد خطایی به اسم Substatus Code هستند تا مدیران شبکه با ردیابی لاگ‌ها پی به دلیل خطا و درخواست‌های ناموفق ببرند. برای مثال Error 404.2 به این معنی است که فایل درخواستی به دلیل قوانین محدود کنده، قفل شده و قابل ارائه نیست. ولی هکر تنها با خطای 404 یعنی وجود نداشتن فایل روبرو می‌شود. در حالت substatus code، کد شماره 2 را هم خواهید داشت که در لاگ ثبت می‌شود.
هر شخصی که در سرور توانایی دسترسی به لاگ‌ها را داشته باشد، می‌تواند کد دوم خطا را نیز مشاهده کند. برای مثال مدیر سرور متوجه میشود که یکی از فایل‌های مورد نظر به کاربران، خطای 404 نمایش میدهد و با بررسی لاگ‌ها متوجه می‌شود که کد خطا 404.9 هست. از آنجا که ما همه‌ی کدها را حفظ نیستیم به این صفحه رجوع می‌کنیم و متوجه میشویم تعداد کاربرانی که برای این فایل، اتصال connection ایجاد کرده‌اند بیش از مقدار مجاز است و مدیر میتواند این وضع را کنترل کند. برای مثال تعداد اتصالات مجاز را نامحدود unlimited تعیین کند.
 بله
حروف - و + برای موارد بالا هم صدق می‌کند. در ضمن گزینه‌های زیر در حالتی که درخواست از پروتکل FTP باشد مقداری نخواهند گرفت:
  • uri-query
  • host
  • (User-Agent)
  • Cookie
  • Referrer
  • substatus

گزینه Custom
 : موقعی که شما این گزینه را انتخاب کنید ماژول logging غیرفعال خواهد شد. زیرا این امکان در IIS قابل پیکربندی نیست و نوشتن ماژول آن بر عهده شما خواهد بود؛ با استفاده از اینترفیس های ILogPlugin ، ILogPluginEx و  ILogUIPlugin  آن را پیاده سازی کنید.

ذخیره اطلاعات به انکدینگ UTF-8 و موضوع امنیت
در صورتی که شما از سایتی با زبانی غیر از انگلیسی و لاتین و فراتر از ANSI  استفاده می‌کنید، این گزینه حتما باید انتخاب شده باشد تا درخواست را بهتر لاگ کند. حتی برای وب سایت‌های انگلیسی زبان هم انتخاب این گزینه بسیار خوب است؛ چرا که اگر به سمت سرور کاراکترهای خاصی در URL ارسال شوند، نمی‌تواند با کدپیج موجود آن‌ها را درست تبدیل کند.

ادامه‌ی تنظیمات
موارد بعدی که در تنظیمات لاگ‌ها کاملا مشخص و واضح است، عملیات زمان بندی  است که برای ساخت یک فایل لاگ جدید به کار می‌رود؛ برای مثال هر ساعت یک لاگ فایل جدید بسازد و فعالیت‌های موجود در هر ساعت در یک لاگ ذخیره می‌شوند.
گزینه‌ی بعدی حداکثر حجم هر فایل لاگ است که به صورت بایت مشخص می‌شود. اگر مقداری که تعیین میکنید کمتر از 1048576 بایت باشد، خودش به طور پیش فرض همان 1048576 بایت را در نظر خواهد گرفت.
گزینه بعدی do not create a new logfile بدین معناست که همه‌ی لاگ‌ها در یک فایل ذخیره می‌شوند و فایل جدیدی برای لاگ‌ها ایجاد نمی‌شود.
گزینه آخری به اسم use local time for filenaming and rollover است که اگر انتخاب شود، نامگذاری هر فایل لاگ بر اساس زمان محلی ساخت فایل لاگ خواهد بود. در صورتیکه انتخاب نشود، نامگذاری با زمان  UTC درج خواهد شد.

سطح سرور
لاگ‌ها فقط در سمت سرور انجام می‌گیرد و لاگ هر سایت در یک فایل لاگ ثبت می‌شود. اگر بخواهید لاگ‌ها را در سطح سرور انجام دهید، گزینه‌ی binary هم اضافه خواهد شد.
Binary: در این گزینه دیگر از قالب بندی یا فرمت بندی لاگ‌ها خبری نیست و لاگ هر وب سایت به صورت اختصاصی صورت نمی‌گیرد. عملیات ذخیره سازی و ثبت هر لاگ می‌تواند از منابع یک سرور از قبیل حافظه و CPU و ... استفاده کند و اگر تعداد این وب سایت‌ها بالا باشد، باقی روش‌ها باعث فشار به سرور می‌شوند. برای همین ایجاد یک فایل خام از لاگ‌ها در این مواقع می‌تواند راهگشا باشد. برای همه یک فایل لاگ ایجاد شده  و بدون قالب بندی ذخیره می‌کند. پسوند این نوع لاگ‌ها ibl است که مخفف Internet Binary Log می‌باشد. دلیل این تغییر پسوند این است که اطمینان کسب شود کاربر، با برنامه‌های متنی چون notepad یا امثال آن که به Text Utilities معروفند فایل را باز نمی‌کند. برای خواندن این فایل‌های میتوان از برنامه‌ی Log parser استفاده کرد. پروتکل‌های FTP,NNTP و SMTP در این حالت لاگشان ثبت نمی‌شود.
مطالب
نحوه‌ی مشاهده‌ی خروجی SQL تولید شده توسط WCF RIA Services

این روزها با وجود ORMs ، کوئری SQL‌ نوشتن شبیه به دورانی شده که با وجود زبان‌های سطح بالا، عده‌ای علاقمند هستند با استفاده از زبان اسمبلی برنامه نویسی کنند! WCF RIA Services به صورت پیش فرض از entity framework استفاده می‌کند (هر چند می‌توان از سایر ORMs هم استفاده کرد)، بنابراین عنوان صحیح‌تر بحث این خواهد بود: چگونه خروجی SQL تولید شده توسط Entity framework را بررسی کنیم؟

الف) استفاده از SQL Server profiler
اولین برنامه‌ای که از سال‌ها قبل، حتی پیش از ظهور ORMs وجود داشته، برنامه‌ی SQL server profiler است، که عموما در مسیر ذیل قابل دستیابی است:
Start Menu->Programs->Microsoft SQL Server 2008->Performance Tools->SQL Server profiler



نکته مهم:
حین کار با SQL Server profiler ، ممکن است انبوهی از کوئری‌های دیگر مثلا مرتبط با SQL Server agent یا reporting services و غیره نیز لاگ شوند. اما الان ما تنها به کوئری‌های برنامه‌ی خود نیاز داریم. برای این منظور به کانکشن استرینگ خود، گزینه‌ی Application Name=My Application Name را نیز اضافه کنید:

<connectionStrings>
<add name="dmEntities" connectionString="metadata=res://*/Models.dmDataModel.csdl|res://*/Models.dmDataModel.ssdl|res://*/Models.dmDataModel.msl;provider=System.Data.SqlClient;provider connection string=&quot;Data Source=(local);Initial Catalog=dm;Integrated Security=True;Application Name=My Application Name;MultipleActiveResultSets=True&quot;" providerName="System.Data.EntityClient" />
</connectionStrings>

اکنون اگر برنامه را با پروفایلر مورد بررسی قرار دهید خروجی به صورت زیر خواهد بود:



برای فیلتر کردن Application Name مورد نظر، در ابتدای کار که یک سشن جدید را آغاز می‌کنید به برگه‌ی events selection مراجعه کرده و بر روی دکمه‌ی column filter کلیک کنید. گزینه‌ی application name را در صفحه‌ی باز شده انتخاب نموده و در قسمت Like آن مطابق تصویر زیر ، نام برنامه‌ی خود را وارد نمائید:




ب) استفاده از IntelliTrace در VS.NET 2010
برنامه را در حالت دیباگ در VS.NET 2010 اجرا کنید. در هر لحظه‌ای می‌توان روی گزینه‌ی Break all کلیک کرد و خروجی SQL تولید شده را نیز علاوه بر اطلاعات دیگر مشاهده نمود:




ج) استفاده از برنامه‌ی حرفه‌ای entity framework profiler
این برنامه از هر دو مورد قبل کاملتر بوده و اساسا برای لاگ کردن کوئری‌ها، مدت زمان اجرا، گزارشگیری از وضعیت برنامه، کدامیک از کوئری‌ها سنگین‌تر هستند، حتی از طریق کدام متد فراخوانی شده‌اند، ارائه‌ی گزارشات و راهنمایی‌هایی در مورد چگونگی بهبود کارآیی برنامه‌ی تهیه شده و امثال آن کاربرد دارد.



استفاده از آن هم بسیار ساده است. ابتدا ارجاعی را به اسمبلی HibernatingRhinos.Profiler.Appender.v4.0 به پروژه‌ی ASP.NET خود اضافه کنید (همان پروژه‌ی هوست مربوط به WCF RIA Service ما). سپس به فایل Global.asax.cs برنامه مراجعه کرده و یک سطر ذیل را اضافه کنید:

protected void Application_Start(object sender, EventArgs e)
{
HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize();
}

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

نظرات مطالب
میان‌افزار جدید Authorization در ASP.NET Core 3.0
خطای 500، یعنی internal server error، یعنی بروز استثنایی در کدهای شما (و این مورد نیاز به بررسی دقیقی دارد). در مطلب «بررسی خطاهای ممکن در حین راه اندازی اولیه برنامه‌های ASP.NET Core در IIS» دو روش لاگ کردن آن‌ها ذکر شده‌اند. همچنین روش‌های دیگری هم برای لاگ کردن خطاها توسط «فریم ورک Logging» وجود دارد. به علاوه گاهی از اوقات بررسی محتوای response بازگشتی از سرور هم مفید است؛ یک نمونه. نکته‌ی «شبیه سازی customErrors در نگارش‌های دیگر ASP.NET» هم مفید است.
- در کل زمانیکه خطای 500 internal server error را دریافت می‌کنید، اگر برنامه را در حالت dotnet run اجرا کرده باشید، تمام خطاهای مرتبط، در پنجره‌ی کنسولی که باز است، لاگ می‌شوند. اگر از ویژوال استودیو استفاده می‌کنید، همین خروجی، در پنجره‌ی دیباگ آن هم درج می‌شود. مرور این خطاهای سمت سرور، برای رفع مشکل الزامی است. همچنین احتمال دارد خروجی خطاهای سمت سرور، در قسمت مشاهده‌ی محتوای response، در برگه‌ی ابزارهای توسعه دهندگان مرورگر هم ظاهر شود. آن‌را هم بررسی کنید. 
مطالب
پَرباد - آموزش پیاده‌سازی پرداخت آنلاین در دات نت - آموزش پیشرفته
در قسمت قبل، با تنظیمات پَرباد آشنا شدیم. در این مقاله قصد داریم سایر امکانات قابل استفاده را آموزش دهیم.

آنچه شما در این مقاله یاد خواهید گرفت:

  • ایجاد صورت حساب پرداخت با استفاده از InvoiceBuilder
  • درگاه مجازی
  • استفاده از پروکسی
  • توکن پرداخت
  • تزریق وابستگی
  • Logging


ایجاد صورت حساب با استفاده از InvoiceBuilder

InvoiceBuilder به شما کمک می‌کند تا یک صورت حساب را جهت پرداخت آماده کنید.
مثال زیر را در نظر بگیرید:
var result = _onlinePayment.Request(Gateways.Mellat, 123, 25000, "http://www.mywebsite.com/foo/bar/");
همانطور که مشخص است، در این مثال یک صورت حساب با شماره رهگیری ۱۲۳ به مبلغ ۲۵۰۰۰ با یک آدرس بازگشتی به درگاه بانک ملت درخواست داده می‌شود.

اما همین دستور را با کمک InvoiceBuilder نیز می‌توان ایجاد کرد.
نمونه مثال بالا با استفاده از InvoiceBuilder
var result = _onlinePayment.Request(invoice =>
{
    invoice
          .SetTrackingNumber(123)
          .SetAmount(25000)
          .SetCallbackUrl("http://www.mywebsite.com/foo/bar/")
          .UseGateway(Gateways.Mellat);
});

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

در زیر نمونه‌هایی از کارایی آن را بررسی می‌کنیم.

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

تولید اتوماتیک کد رهگیری به صورت افزایشی

در این روش، کد رهگیری (TrackingNumber) که مورد نیاز درگاه‌های بانکی است، به صورت اتوماتیک در هنگام ایجاد درخواست پرداخت، توسط پَرباد تولید می‌شود.
var result = _onlinePayment.Request(invoice =>
{
    invoice
          .UseAutoIncrementTrackingNumber()
          .SetAmount(25000)
          .SetCallbackUrl("http://www.mywebsite.com/foo/bar/")
          .UseGateway(Gateways.Mellat);
});
کد تولید شده، به صورت افزایشی است. در واقع در هر درخواست پرداخت جدید، یک کد رهگیری تولید می‌شود که یک واحد از کد تولید شده‌ی قبلی بیشتر است. 

شما همچنین می‌توانید مقدار اولیه این عدد را جهت شروع تولید، در پارامتر متد تعیین کنید. این مقدار همچنین در قسمت تنظیمات پَرباد نیز توسط متد ConfigureAutoTrackingNumber قابل تنظیم است.

تولید اتوماتیک کد رهگیری به صورت تصادفی

var result = _onlinePayment.Request(invoice =>
{
    invoice
          .UseAutoRandomTrackingNumber()
          .SetAmount(25000)
          .SetCallbackUrl("http://www.mywebsite.com/foo/bar/")
          .UseGateway(Gateways.Mellat);
});
در این روش کد رهگیری، به صورت تصادفی در محدوده Int64 توسط پَرباد تولید خواهد شد. کد‌های تولید شده در این روش تقریبا ٪۹۹.۹ غیر تکراری هستند. اما اگر به تمیز بودن کدهای تولید شده اهمیت می‌دهید، بهتر است از روش AutoIncrement که بالاتر توضیح داده شد، استفاده کنید.

ایجاد یک تولید کننده کد رهگیری توسط شما

اگر بنا به دلایلی قصد دارید خودتان نیز یک منبع تولید کد رهگیری را ایجاد کنید، می‌توانید به روش زیر عمل کنید.
public class MyTrackingNumberProvider : ITrackingNumberProvider
{
    public Task<long> ProvideAsync(CancellationToken cancellationToken = new CancellationToken())
    {
        // تولید و برگشت کد در اینجا
    }
}

نکته ۱: شما همچنین می‌توانید در منبع خود، از تزریق وابستگی‌ها نیز استفاده کنید. بدیهی است سرویسی را که تزریق می‌کنید، باید از قبل توسط سیستم تزریق وابستگی‌های اپلیکیشن شما، ثبت شده باشد.
نکته ۲؛ شما همچنین می‌توانید بدون ایجاد هیچ منبعی، به راحتی از متد SetTrackingNumber در InvoiceBuilder (که بالاتر توضیح داده شده) استفاده کنید.

سپس در هنگام ایجاد درخواست پرداخت به روش زیر از منبع خود استفاده کنید:
var result = _onlinePayment.Request(invoice =>
{
    invoice
          .UseTrackingNumberProvider<MyTrackingNumberProvider>()
          //  یا 
          .UseTrackingNumberProvider(new MyTrackingNumberProvider())
          //  یا 
          .UseTrackingNumberProvider(services => new MyTrackingNumberProvider())
});
همانطور که می‌بینید، متد‌های مختلفی جهت استفاده از منبع مورد نظر شما موجود است.

صورت حساب سفارشی برای امکانات اختصاصی درگاه‌های بانکی

درگاه‌های بانکی علاوه بر سرویس‌های پرداخت عادی، امکانات مختلفی دیگری را نیز ارائه می‌کنند. برای مثال بانک ملت دارای سرویسی به نام "باشگاه مشتریان بانک ملت" است که امکان واریز مبلغ را از حساب مشتری، به چندین حساب مختلف می‌دهد.
نکته مهم: همانطور که می‌دانید قبل از استفاده از این گونه سرویس‌ها، کلیه شماره حساب‌هایی که قصد واریز مبلغ به آنها را دارید، باید از قبل (در هنگام عقد قرارداد با بانک) به بانک مورد نظر داده شده باشد. در غیر اینصورت امکان استفاده از این گونه سرویس‌ها را ندارید.
نکته: در هنگام نوشتار این مقاله، این سرویس هنوز در پَرباد آماده نیست و در حال توسعه است.


درگاه مجازی

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

ابتدا در قسمت تنظیمات پَرباد،  آدرس مورد نظر برای درگاه مجازی را مانند کد زیر مشخص می‌کنیم:
services.AddParbad()
        .ConfigureGateways(gateways =>
        {
            gateways
               .AddParbadVirtual()
               .WithOptions(options => options.GatewayPath = "/MyVirtualGateway");
        });


در مثال بالا، درگاه مجازی توسط آدرس داده شده در دسترس خواهد بود. توجه داشته باشید که این آدرس حتما باید با یک ( / ) آغاز شده باشد.
سپس درگاه مجازی را در اپلیکیشن خود ثبت می‌کنید:
ASP.NET CORE (Startup.cs)
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseMvc(routes =>
    {
        routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
    });

    // ثبت درگاه مجازی
    app.UseParbadVirtualGateway();
}

ASP.NET WebForms, ASP.NET MVC (Startup.cs)
public void Configuration(IAppBuilder app)
{
    var parbad = ParbadBuilder.CreateDefaultBuilder()
        .ConfigureGateways(gateways =>
        {
            gateways
               .AddParbadVirtual()
               .WithOptions(options => options.GatewayPath = "/MyVirtualGateway");
        })
        .Build();

    app.UseParbadVirtualGateway(parbad.Services);
}


تنظیمات درگاه مجازی تا اینجا به پایان رسیده و فقط در هنگام ایجاد درخواست پرداخت، از میان درگاه‌ها، درگاه مجازی پَرباد را انتخاب کنید.
var result = _onlinePayment.Request(Gateways.ParbadVirtualGateway, 123, 25000, "http://www.mywebsite.com/foo/bar/");

و در نهایت به درگاه مجازی هدایت خواهید شد:

نمونه پروژه‌های کامل را در انتهای مقاله می‌توانید مشاهده کنید.

استفاده از پروکسی

با توجه به تغییرات اخیر در بانکداری کشور، احتمال آن وجود دارد که در آینده، بعضی از درگاه‌های بانکی فقط به IP‌های داخل کشور سرویس دهی کنند. اگر وب سایت شما بنا به دلایلی در سروری خارج از کشور میزبانی می‌شود، شما جهت استفاده از درگاه‌های بانکی، نیاز به یک سرور در داخل کشور دارید که نقش پروکسی را برای شما بازی کند. پَرباد این امکان را برای شما محیا کرده و کافیست اطلاعات پروکسی سرور خود را به شکل زیر برای درگاه بانکی مورد نظر ثبت کنید.
services.AddParbad()
        .ConfigureGateways(gateways =>
        {
            gateways
               .AddMellat()
               .WithOptions(options => 
               {
                    options.TerminalId = 123;
                    options.UserName = "abc";
                    options.UserPassword = "xyz;
               )
               .WithProxy(new Uri("Proxy Server URL"), "UserName", "Password");
        });
در مثال بالا، درگاه بانک ملت (صرفا جهت مثال) با یک پروکسی تنظیم شده است. برای سایر درگاه‌های بانکی، فرمت تنظیمات، کاملا مشابه مثال بالا است.

توکن پرداخت

پَرباد جهت شناسایی و تبادل اطلاعات پرداخت با خارج از سیستم خود و بانک‌ها، از یک توکن به ازاء هر پرداخت استفاده می‌کند. به عبارت دیگر، به ازاء هر درخواست پرداخت یک توکن تولید می‌شود. به این صورت، درخواست‌های پرداخت، غیر قابل دستکاری و غیر قابل حدس زدن توسط کاربران می‌شود.
نکته: پَرباد از یک تولید کننده پیش فرض توکن استفاده می‌کند و شما نیازی به انجام هیچگونه تنظیماتی ندارید. تولید کننده پیش فرض، از یک GUID در Query String استفاده می‌کند.
اگر قصد دارید روش مورد نظر خود را برای تولید توکن جهت شناسایی یک پرداخت پیاده‌سازی کنید، می‌توانید به روش زیر عمل کنید:
ابتدا تولید کننده توکن را تعریف کنید.
public class MyTokenProvider : IPaymentTokenProvider
{
    public Task<string> ProvideTokenAsync(Invoice invoice, CancellationToken cancellationToken = new CancellationToken())
    {
        // تولید و برگرداندن توکن در اینجا
    }

    public Task<string> RetrieveTokenAsync(CancellationToken cancellationToken = new CancellationToken())
    {
        // خواندن و برگرداندن توکن در اینجا
    }
}

نکته: شما همچنین می‌توانید از تزریق وابستگی‌ها نیز استفاده کنید. بدیهی است که در اینصورت باید سرویسی که تزریق می‌کنید، از قبل در سیستم تزریق وابستگی‌های اپلیکیشن شما ثبت شده باشد.
سپس تولید کننده توکن خود را در تنظیمات به پَرباد معرفی کنید:
services.AddParbad()
        .ConfigurePaymentToken(builder => builder.AddPaymentTokenProvider<MyTokenProvider>(ServiceLifetime.Transient));

ServiceLifetime، تایین کننده طول عمر سرویس شما است.
به این صورت پَرباد، شناسایی و ردیابی یک صورت حساب را با استفاده از تولید کننده توکن شما انجام خواهد داد.


تزریق وابستگی

همانطور که قبلا در مقاله آموزش تنظیمات نیز گفته شد، پَرباد به صورت توکار، از تزریق وابستگی استاندارد مایکروسافت استفاده می‌کند. بنابراین اگر اپلیکیشن شما نیز از تزریق وابستگی مشابهی استفاده می‌کند، نیازی به خواندن و یاد گرفتن این بخش ندارید و به راحتی می‌توانید از اینترفیس IOnlinePayment در هر کجا که نیاز داشتید جهت عملیات پرداخت استفاده کنید.
اما در صورتیکه در اپلیکیشن خود از تزریق وابستگی دیگری ( مانند Autofac ) استفاده می‌کنید، باید این دو سیستم را با یکدیگر هماهنگ کنید. خوشبختانه تمام کتابخانه‌های معروف تزریق وابستگی ( مانند Autofac )  از قبل این کار را برای شما محیا کرده‌اند و شما فقط نیاز به افزودن چند خط کد به اپلیکیشن فعلی خود را دارید.
جهت فهم بهتر و آموزش عملی، یک اپلیکیشن کامل ASP.NET MVC برای شما تهیه شده که از Autofac جهت تزریق وابستگی استفاده می‌کند. در این پروژه خواهید دید چگونه به راحتی پَرباد و Autofac را با یکدیگر هماهنگ کرده و هچنین اینترفیس IOnlinePayment را درون کنترلر تزریق می‌کنیم.
لینک پروژه‌ها در انتهای همین مقاله قابل مشاهده هستند.


Logging
لاگ کردن در پَرباد توسط سیستم استاندارد مایکروسافت انجام می‌شود. این بدان معنی است که شما امکان استفاده از کتابخانه‌های Logging بسیار زیادی را دارید. نحوه استفاده و تنظیم کتابخانه‌های معروف Logging در وب سایت آنها آورده شده و به راحتی می‌توانید آنها را با لاگ مایکروسافت تطابق دهید.

نمونه پروژه‌ها
مقاله‌های مرتبط
مطالب
پیاده سازی برنامه‌های چند مستاجری در ASP.NET Core

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

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

 در معماری Multi-Tenancy، چندین کاربر می‌توانند از یک نمونه (Single Instance) از اپلیکیشن نرم‌افزاری استفاده کنند. یعنی این نمونه روی سرور اجرا می‌شود و به چندین کاربر سرویس می‌دهد. هر کاربر را یک Tenant می‌نامیم. می‌توان به Tenantها امکان تغییر و شخصی‌سازی بخشی از اپلیکیشن را داد؛ مثلا امکان تغییر رنگ رابط کاربری و یا قوانین کسب‌وکار، اما آنها نمی‌توانند کدهای اپلیکیشن را شخصی‌سازی کنند.

بدون داشتن دانش کافی، پیاده سازی معماری multi tenant می‌تواند تبدیل یه یک چالش بزرگ شود. مخصوصا در نسخه‌ی قبلی ASP.NET که یکپارچه نبودن فریم ورک‌های مختلف می‌توانست باعث ایجاد چندین پیاده سازی مختلف در برنامه شود. موضوع وقتی پیچیده‌تر می‌شد که شما قصد داشتید در یک برنامه چندین فریم ورک مختلف مثل SignalR, MVC, Web API را مورد استفاده قرار دهید.

خوشبختانه اوضاع با وجود OWIN بهتر شده و ما در این مطلب قصد استفاده از یک تولکیت را به نام SaasKit، برای پیاده سازی این معماری در ASP.NET Core داریم. هدف از این toolkit، ساده‌تر کردن هر چه بیشتر ساخت برنامه‌های SaaS (Software as a Service) هست. با استفاده از OWIN ما قادریم که بدون در نظر گرفتن فریم ورک مورد استفاده، رفتار مورد نظر خودمان را مستقیما در یک چرخه درخواست HTTP پیاده سازی کنیم و البته به لطف طراحی خاص ASP.NET Core 1.0 و استفاده از میان افزار‌هایی مشابه OWIN در برنامه، کار ما با SaasKit باز هم راحت‌تر خواهد بود.

شروع کار 

یک پروژه ASP.NET Core جدید را ایجاد کنید و سپس ارجاعی را به فضای نام SaasKit.Multitenancy  (موجود در Nuget) بدهید. 
PM> Install-Package SaasKit.Multitenancy
بعد از اینکار ما باید به SaasKit اطلاع دهیم که چطور مستاجر‌های ما را شناسایی کند.

شناسایی مستاجر (tenant) 

اولین جنبه در معماری multi-tenant، شناسایی مستاجر بر اساس اطلاعات درخواست جاری می‌باشد که می‌تواند از hostname ، کاربر جاری یا یک HTTP header باشد.
ابتدا به تعریف کلاس مستاجر می‌پردازیم: 
    public class AppTenant
    {
        public string Name { get; set; }
        public string[] Hostnames { get; set; }
    }
سپس از طریق پیاده سازی اینترفیس ITenantResolver  و نوشتن یک tenant resolver به SaasKit اطلاع می‌دهیم که چطور مستاجر جاری را بر اساس اطلاعات درخواست جاری شناسایی کند و در صورتیکه موفق به شناسایی شود، یک وهله از نوع <TenantContext<TTenant را بازگشت خواهد داد. 
    public class AppTenantResolver : ITenantResolver<AppTenant>
    {
        IEnumerable<AppTenant> tenants = new List<AppTenant>(new[]
        {
            new AppTenant {
                Name = "Tenant 1",
                Hostnames = new[] { "localhost:6000", "localhost:6001" }
            },
            new AppTenant {
                Name = "Tenant 2",
                Hostnames = new[] { "localhost:6002" }
            }
        });
        public async Task<TenantContext<AppTenant>> ResolveAsync(HttpContext context)
        {
            TenantContext<AppTenant> tenantContext = null;
            var tenant = tenants.FirstOrDefault(t =>
                t.Hostnames.Any(h => h.Equals(context.Request.Host.Value.ToLower())));
            if (tenant != null)
            {
                tenantContext = new TenantContext<AppTenant>(tenant);
            }
            return tenantContext;
        }
    }
در نظر داشته باشید که اینجا ما اطلاعات مستاجر را از روی hostname استخراج کردیم؛ اما از آنجا که شما به شیء HttpContext دسترسی کاملی دارید، می‌توانید از هر چیزی که مایل باشید استفاده کنید؛ مثل URL، اطلاعات کاربر، هدر‌های HTTP و غیره. در اینجا فعلا مشخصات مستاجر‌های خودمان را در کد نوشتیم. اما شما می‌توانید در برنامه خودتان این اطلاعات را از فایل تنظیمات برنامه و یا یک بانک اطلاعاتی دریافت کنید.
 

سیم کشی کردن 

بعد از پیاده سازی این اینترفیس نوبت به سیم کشی‌های SaasKit میرسد. من در اینجا سعی کردم که مثل الگوی برنامه‌های ASP.NET Core عمل کنم. ابتدا نیاز داریم که وابستگی‌های SaasKit را ثبت کنیم. فایل startups.cs  را باز کنید و کدهای زیر را در متد ConfigureServices اضافه نمایید: 
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMultitenancy<AppTenant, AppTenantResolver>();
    }
سپس باید میان افزار SaasKit را ثبت کنیم. کدهای زیر را به متد Configure اضافه کنید:
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        // after .UseStaticFiles()
        app.UseMultitenancy<AppTenant>();
        // before .UseMvc()
    }


دریافت مستاجر جاری 

حالا هر جا که نیاز به وهله‌ای از شیء مستاجر جاری داشتید، می‌توانید به روش زیر عمل کنید: 
    public class HomeController : Controller
    {
        private AppTenant tenant;
        public HomeController(AppTenant tenant)
        {
            this.tenant = tenant;
        }
    }
به عنوان مثال قصد داریم نام مستاجر را در عنوان سایت نمایش دهیم. برای اینکار ما از قابلیت جدید MVC Core یعنی تزریق سرویس‌ها به View استفاده خواهیم کرد.  در فایل Layout.cshtml_ تکه کد زیر را به بالای صفحه اضافه کنید:
    @inject AppTenant Tenant;
این کد، AppTenant را برای ما در تمامی View‌ها از طریق شی Tenant قابل دسترس می‌کند. حالا می‌توانیم در View خود از جزییات مستاجر به شکل زیر استفاده کنیم: 
    <a asp-controller="Home" asp-action="Index">@Tenant.Name</a>


اجرای نمونه مثال 

فایل project.json را باز کنید و مقدار web را به شکل زیر مقدار دهی کنید: (در اینجا برای سایت خود 3 آدرس را نگاشت کردیم) 
    "commands": {
      "web": "Microsoft.AspNet.Server.Kestrel --server.urls=http://localhost:6000;http://localhost:6001;http://localhost:6002",
    },
سپس کنسول را در محل ریشه پروژه باز نموده و دستور زیر را اجرا کنید: 
    dotnet run
حال اگر در مرورگر خود آدرس http://localhost:6000 را وارد کنیم، مستاجر 1 را خواهیم دید:


و اگر آدرس http://localhost:6002 را وارد کنیم، مستاجر 2 را مشاهده می‌کنیم:


قابل پیکربندی کردن مستاجر ها 

از آنجائیکه نوشتن مشخصات مستاجر‌ها در کد زیاد جالب نیست، برای همین تصمیم داریم که این مشخصات را با استفاده از قابلیت‌های ASP.NET Core از فایل appsettings.json دریافت کنیم. تنظیمات مستاجر‌ها را مطابق اطلاعات زیر به این فایل اضافه کنید:

    "Multitenancy": {
      "Tenants": [
        {
          "Name": "Tenant 1",
          "Hostnames": [
            "localhost:6000",
            "localhost:6001"
          ]
        },
        {
          "Name": "Tenant 2",
          "Hostnames": [
            "localhost:6002"
          ]
        }
      ]
    }
سپس کلاسی را که بیانگر تنظیمات چند مستاجری باشد، می‌نویسیم: 
    public class MultitenancyOptions
    {
        public Collection<AppTenant> Tenants { get; set; }
    }
حالا نیاز داریم که به برنامه اعلام کنیم تا تنظیمات مورد نیاز خود را از فایل appsettings.json بخواند. کد زیر را به ConfigureServices اضافه کنید: 
    services.Configure<MultitenancyOptions>(Configuration.GetSection("Multitenancy"));
سپس کدهای resolver خود را جهت دریافت اطلاعات از MultitenancyOptions مطابق زیر تغییر می‌دهیم: 
    public class AppTenantResolver : ITenantResolver<AppTenant>
    {
        private readonly IEnumerable<AppTenant> tenants;
        public AppTenantResolver(IOptions<MultitenancyOptions> options)
        {
            this.tenants = options.Value.Tenants;
        }
        public async Task<TenantContext<AppTenant>> ResolveAsync(HttpContext context)
        {
            TenantContext<AppTenant> tenantContext = null;
            var tenant = tenants.FirstOrDefault(t => 
                t.Hostnames.Any(h => h.Equals(context.Request.Host.Value.ToLower())));
            if (tenant != null)
            {
                tenantContext = new TenantContext<AppTenant>(tenant);
            }
            return Task.FromResult(tenantContext);
        }
    }
برنامه را یکبار re-build کرده و اجرا کنید . 


در آخر 

اولین قدم در پیاده سازی یک معماری multi-tenant، تصمیم گیری درباره این موضوع است که شما چطور مستاجر خود را شناسایی کنید. به محض این شناسایی شما می‌توانید عملیات‌های بعدی خود را مثل تفکیک بخشی از برنامه، فیلتر کردن داده‌ای، نمایش یک view خاص برای هر مستاجر و یا بازنویسی قسمت‌های مختلف برنامه بر اساس هر مستاجر، انجام دهید.

_ سورس مثال بالا در گیت هاب قابل دریافت می‌باشد.

_ منبع: اینجا  

نظرات مطالب
پشتیبانی توکار از انجام کارهای پس‌زمینه در ASP.NET Core 2x
ارتقاء به NET Core 3.0.: پشتیبانی از ایجاد سرویس‌های پس‌زمینه

یکی از تغییرات مهم قالب ایجاد پروژه‌های ASP.NET Core 3.0، تغییر فایل program.cs آن است که در آن از یک Generic Host بجای روش قبلی Web Host، استفاده شده‌است. علت آن فراهم آوردن امکان استفاده‌ی از قابلیت‌هایی مانند تزریق وابستگی‌ها، logging، تنظیمات برنامه و غیره، در برنامه‌های غیر وب نیز می‌باشد. یکی از این انواع برنامه‌ها، سرویس‌های پس‌زمینه‌ی غیر HTTP هستند. به این ترتیب می‌توان برنامه‌ای شبیه به یک برنامه‌ی وب ASP.NET Core را ایجاد کرد که تنها کارش اجرای سرویس‌های غیر وبی است؛ اما به تمام امکانات و زیر ساخت‌های ASP.NET Core دسترسی دارد.
برای ایجاد این نوع برنامه‌ها در NET Core 3x. می‌توانید دستور زیر را در پوشه‌ی خالی که ایجاد کرده‌اید، اجرا کنید:
dotnet new worker
ساختار برنامه‌ای که توسط این دستور تولید می‌شود به صورت زیر است که بسیار شبیه به ساختار یک برنامه‌ی ASP.NET Core است:
appsettings.Development.json
appsettings.json
MyWorkerServiceApp.csproj
Program.cs
Worker.cs

- فایل csproj آن دارای این محتوا است:
<Project Sdk="Microsoft.NET.Sdk.Worker">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <UserSecretsId>dotnet-MyWorkerServiceApp-B76DB08E-FFBB-4AD1-89B5-93BF483D1BD0</UserSecretsId>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="3.0.0-preview8.19405.4" />
  </ItemGroup>
</Project>
در آن ویژگی Sdk به Microsoft.NET.Sdk.Worker اشاره می‌کند و همچنین از بسته‌ی Microsoft.Extensions.Hosting استفاده شده‌است.

- محتوای فایل Program.cs آن بسیار آشنا است و دقیقا کپی همان فایلی است که در برنامه‌های ASP.NET Core 3x حضور دارد:
namespace MyWorkerServiceApp
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddHostedService<Worker>();
                });
    }
}
در اینجا یک Generic host را بجای Web host قالب‌های پیشین فایل Program.cs ملاحظه می‌کنید که هدف اصلی آن، عمومی کردن این قالب، برای استفاده‌ی از آن در برنامه‌های غیر وبی نیز می‌باشد.
در متد ConfigureServices، انواع اقسام سرویس‌ها را منجمله یک HostedService که در مطلب جاری به آن پرداخته شده، می‌توان افزود. سرویس Worker ای که در اینجا به آن ارجاعی وجود دارد، به صورت زیر تعریف شده‌است:
    public class Worker : BackgroundService
    {
        private readonly ILogger<Worker> _logger;

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

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
                await Task.Delay(1000, stoppingToken);
            }
        }
    }
با ساختار این کلاس نیز آشنا هستید و موضوع اصلی مطلب جاری است.


یک نکته‌ی تکمیلی: روش تبدیل کردن یک BackgroundService به یک Windows Service

اگر برنامه‌ی NET Core. شما در ویندوز اجرا می‌شود، می‌توانید این برنامه‌ی BackgroundService را به یک سرویس ویندوز NT نیز تبدیل کنید. برای اینکار ابتدا بسته‌ی نیوگت Microsoft.Extensions.Hosting.WindowsServices را به پروژه اضافه کنید. سپس جائیکه CreateHostBuilder صورت می‌گیرد، متد UseWindowsService را فراخوانی کنید:
public static IHostBuilder CreateHostBuilder(string[] args) => 
            Host.CreateDefaultBuilder(args) 
                .UseWindowsService() 
                .ConfigureServices((hostContext, services) => 
                { 
                   //services.AddHttpClient(); 
                   services.AddHostedService<Worker>(); 
                });
تا اینجا هنوز هم برنامه، شبیه به یک برنامه‌ی کنسول دات نت Core قابل اجرا و دیباگ است. اما اگر خواستید آن‌را به صورت یک سرویس ویندوز نیز نصب کنید، تنها کافی است از دستور زیر استفاده کنید:
 cs create WorkerServiceDemo binPath=C:\Path\To\WorkerServiceDemo.exe

البته برای لینوکس نیز می‌توان از UseSystemd استفاده کرد که نیاز به نصب بسته‌ی Microsoft.Extensions.Hosting.Systemd را دارد:
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .UseSystemd()
        .ConfigureServices((hostContext, services) =>
        {
            services.AddHostedService<Worker>();
        });