اصول و قراردادهای نامگذاری در داتنت
در ادامه بجای این روشها، از SignalR برای محاسبه تعداد کاربران آنلاین و همچنین به روز رسانی بلادرنگ این عدد در سمت کاربر، استفاده خواهیم کرد.
تشخیص اتصال و قطع اتصال کاربران در SignalR
زیر ساختهای کلاس Hub موجود در SignalR، دارای متدهای ردیابی اتصال (OnConnected)، قطع اتصال (OnDisconnected) و یا برقراری مجدد اتصال کاربران (OnReconnected) هستند. با بازنویسی این متدها میتوان به تخمین بسیار دقیقی از تعداد کاربران آنلاین یک سایت رسید.
پیشنیازهای بحث
پیشنیازهای این بحث با مطلب «مثال - نمایش درصد پیشرفت عملیات توسط SignalR» یکی است. برای مثال نحوه دریافت وابستگیها، تنظیمات فایل global.asax و افزودن اسکریپتها، تفاوتی با مثال یاد شده ندارند.
تعریف هاب کاربران آنلاین برنامه
using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNet.SignalR; namespace SignalR05.Common { public class OnlineUsersHub : Hub { public static readonly ConcurrentDictionary<string, string> OnlineUsers = new ConcurrentDictionary<string, string>(); public void UpdateUsersOnlineCount() { // آی پی معرف یک کاربر است // اما کانکشن آی دی معرف یک برگه جدید در مرورگر او است // هر کاربر میتواند چندین برگه را به یک سایت گشوده یا ببندد var ipsCount = OnlineUsers.Select(x => x.Value).Distinct().Count(); this.Clients.All.updateUsersOnlineCount(ipsCount); } /// <summary> /// اگر کاربران اعتبار سنجی شدهاند بهتر است از /// this.Context.User.Identity.Name /// بجای آی پی استفاده شود /// </summary> protected string GetUserIpAddress() { object environment; if (!Context.Request.Items.TryGetValue("owin.environment", out environment)) return null; object serverRemoteIpAddress; if (!((IDictionary<string, object>)environment).TryGetValue("server.RemoteIpAddress", out serverRemoteIpAddress)) return null; return serverRemoteIpAddress.ToString(); } public override Task OnConnected() { var ip = GetUserIpAddress(); OnlineUsers.TryAdd(this.Context.ConnectionId, ip); UpdateUsersOnlineCount(); return base.OnConnected(); } public override Task OnReconnected() { var ip = GetUserIpAddress(); OnlineUsers.TryAdd(this.Context.ConnectionId, ip); UpdateUsersOnlineCount(); return base.OnReconnected(); } public override Task OnDisconnected() { // در این حالت ممکن است مرورگر کاملا بسته شده باشد // یا حتی صرفا یک برگه مرورگر از چندین برگه متصل به سایت بسته شده باشند string ip; OnlineUsers.TryRemove(this.Context.ConnectionId, out ip); UpdateUsersOnlineCount(); return base.OnDisconnected(); } } }
در اینجا، هم به IP کاربر و هم به ConnectionId او نیاز است. از این جهت که هر ConnectionId، معرف یک برگه جدید باز شده در مرورگر کاربر است. اگر صرفا IPها را پردازش کنیم، با بسته شدن یکی از چندین برگه مرورگر او که اکنون به سایت متصل هستند، آمار او را از دست خواهیم داد. این کاربر هنوز چندین برگه باز دیگر را دارد که با سایت در ارتباط هستند، اما چون IP او را از لیست حذف کردهایم (در نتیجه بسته شدن یکی از برگهها)، آمار کلی شخص را نیز از دست خواهیم داد. بنابراین هر دوی IP و ConnectionIdها باید پردازش شوند.
اگر برنامه شما دارای اعتبارسنجی است (یک صفحه لاگین دارد)، بهتر است بجای IP از this.Context.User.Identity.Name استفاده کنید.
کدهای سمت کلاینت نمایش آمار کاربران
<html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title></title> <script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script> <script src="Scripts/jquery.signalR-1.1.3.min.js" type="text/javascript"></script> <script type="text/javascript" src='<%= ResolveClientUrl("~/signalr/hubs") %>'></script> </head> <body> <form id="form1" runat="server"> online users count: <span id="usersCount"></span> </form> <script type="text/javascript"> $(function () { $.connection.hub.logging = true; var onlineUsersHub = $.connection.onlineUsersHub; onlineUsersHub.client.updateUsersOnlineCount = function (count) { $('#usersCount').text(count); }; $.connection.hub.start(); }); </script> </body> </html>
کدهای کامل این مثال را از اینجا نیز میتوانید دریافت کنید:
SignalR05.zip
مقدمه:
شروع را با نصب ویژوال استودیوی نسخه رایگان 2013 برای وب و یا نسخهی 2013 آغاز میکنیم. برای راهنمایی استفاده ازDropbox, GitHub, Linkedin, Instagram, buffer salesforce STEAM, Stack Exchange, Tripit, twitch, Twitter, Yahoo و بیشتر اینجا کلیک کنید.توجه:
برای استفاده از Google OAuth 2 و دیباگ به صورت لوکال بدون اخطار SSL، شما میبایستی نسخهی ویژوال استودیو 2013 آپدیت 3 و یا بالاتر را نصب کرده باشید.ساخت اولین پروژه:
ویژوال استودیو را اجرا نماید. در سمت چپ بر روی آیکن Web کلیک کنید تا آیتم ASP.NET Web Application در دات نت 4.5.1 نمایش داده شود. یک نام را برای پروژه انتخاب نموده و OK را انتخاب نماید.در دیالوگ بعدی آیتم MVC را انتخاب و اطمینان داشته باشید Individual User Accounts که با انتخاب Change Authentication به صورت دیالوگ برای شما نمایش داده میشود، انتخاب گردیده و در نهایت بر روی OK کلیک کنید.
فعال نمودن حساب کاربری گوگل و اعمال تنظیمات اولیه:
در این بخش در صورتیکه حساب کاربری گوگل ندارید، وارد سایت گوگل شده و یک حساب کاربری را ایجاد نماید. در غیر اینصورت اینجا کلیک کنید تا وارد بخش Google Developers Console شوید.در بخش منو بر روی ایجاد پروژه کلیک کنید تا پروژهای جدید ایجاد گردد.
در دیالوگ باز شده نام پروژه خودتان را وارد کنید و دکمهی Create را زده تا عملیات ایجاد پروژه انجام شود. در صورتیکه با موفقیت پیش رفته باشید، این صفحه برای شما بارگزاری میگردد.
فعال سازی Google+API
در سمت چپ تصویر بالا آیتمی با نام APIs & auth خواهید دید که بعد از کلیک بر روی آن، زیر مجموعهای برای این آیتم فعال میگردد که میبایستی بر روی APIs کلیک و در این قسمت به جستجوی آیتمی با نام Google+ API پرداخته و در نهایت این آیتم را برای پروژه فعال سازید.
ایجاد یک Client ID :
در بخش Credentials بر روی دکمهی Create new Client ID کلیک نماید.
در دیالوگ باز شده از شما درخواست میشود تا نوع اپلیکشن را انتخاب کنید که در اینجا میبایستی آیتم اول (Web application ) را برای گام بعدی انتخاب کنید و با کلیک بر روی Configure consent screen به صفحهی Consent screen هدایت خواهید شد. فیلدهای مربوطه را به درستی پر کنید (این بخش به عنوان توضیحات مجوز ورود بین سایت شما و گوگل است).
در نهایت بعد از کلیک بر روی Save به صفحهی Client ID بازگشت داده خواهید شد که در این صفحه با این دیالوگ برخورد خواهید کرد.
پروژهی MVC خودتان را اجرا و لینک و پورت مربوطه را کپی کنید ( http://localhost:5063 ).
در Authorized JavaScript Origins لینک را کپی نماید و در بخش Authorized redirect URls لینک را مجدد کپی نماید. با این تفاوت که بعد از پورت signin-google را هم قرار دهید. ( http://localhost:5063/signin-google )
حال بر روی دکمهی Create Client ID کلیک کنید.
پیکربندی فایل Startup.Auth :
فایل web.config را که در ریشهی پروژه قرار دارد باز کنید. در داخل تگ appSettings کد زیر را کپی کنید. توجه شود بجای دو مقدار value، مقداری را که گوگل برای شما ثبت کرده است، وارد کنید.
<appSettings> <!--Google--> <add key="GoogleClientId" value="555533955993-fgk9d4a9999ehvfpqrukjl7r0a4r5tus.apps.googleusercontent.com" /> <add key="GoogleClientSecretId" value="QGEF4zY4GEwQNXe8ETwnVHfz" /> </appSettings>
فایل Startup.Auth را باز کنید و دو پراپرتی و یک سازندهی بدون ورودی را تعریف نماید. توضیحات بیشتر به صورت کامنت در کد زیر قرار گرفته است.
//فضا نامهای استفاده شده در این کلاس using System; using System.Configuration; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin; using Microsoft.Owin.Security.Cookies; using Microsoft.Owin.Security.Google; using Owin; using Login.Models; //فضا نام جاری پروژه namespace Login { /// <summary> /// در ریشه سایت فایلی با نام استارت آپ که کلاسی هم نام این کلاس با یک تابع و یک ورودی از نوع اینترفیس تعریف شده است ///که این دو کلاس به صورت پارشال مهروموم شده اند /// </summary> public partial class Startup { /// <summary> /// این پراپرتی مقدار کلایت ای دی رو از وب دات کانفیگ در سازنده بدون ورودی در خودش ذخیره میکند /// </summary> public string GoogleClientId { get; set; } /// <summary> /// این پراپرتی مقدار کلایت سیکرت ای دی رو از وب دات کانفیگ در سازنده بدون ورودی در خودش ذخیره میکند /// </summary> public string GoogleClientSecretId { get; set; } /// <summary> /// سازنده بدون ورودی /// به ازای هر بار نمونه سازی از کلاس، سازندههای بدون ورودی کلاس هر بار اجرا خواهند شد، توجه شود که میتوان از /// سازندههای استاتیک هم استفاده کرد، این سازنده فقط یک بار، در صورتی که از کلاس نمونه سازی شود ایجاد میگردد /// </summary> public Startup() { //Get Client ID from Web.Config GoogleClientId = ConfigurationManager.AppSettings["GoogleClientId"]; //Get Client Secret ID from Web.Config GoogleClientSecretId = ConfigurationManager.AppSettings["GoogleClientSecretId"]; } ///// <summary> ///// سازنده استاتیک کلاس ///// </summary> //static Startup() //{ //در صورتی که از این سازنده استفاده شود میبایست پراپرتیهای تعریف شده در سطح کلاس به صورت استاتیک تعریف گردد تا //بتوان در این سازنده سطح دسترسی گرفت // GoogleClientId = ConfigurationManager.AppSettings["GoogleClientId"]; // GoogleClientSecretId = ConfigurationManager.AppSettings["GoogleClientSecretId"]; //} // For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864 public void ConfigureAuth(IAppBuilder app) { // Configure the db context, user manager and signin manager to use a single instance per request app.CreatePerOwinContext(ApplicationDbContext.Create); app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create); app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create); // Enable the application to use a cookie to store information for the signed in user // and to use a cookie to temporarily store information about a user logging in with a third party login provider // Configure the sign in cookie app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, LoginPath = new PathString("/Account/Login"), Provider = new CookieAuthenticationProvider { // Enables the application to validate the security stamp when the user logs in. // This is a security feature which is used when you change a password or add an external login to your account. OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>( validateInterval: TimeSpan.FromMinutes(30), regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager)) } }); app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); // Enables the application to temporarily store user information when they are verifying the second factor in the two-factor authentication process. app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5)); // Enables the application to remember the second login verification factor such as phone or email. // Once you check this option, your second step of verification during the login process will be remembered on the device where you logged in from. // This is similar to the RememberMe option when you log in. app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie); //Initialize UseGoogleAuthentication app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions() { ClientId = GoogleClientId, ClientSecret = GoogleClientSecretId }); } } }
حال پروژه را اجرا کرده و به صفحهی ورود کاربر رجوع نمائید. همانگونه که در تصوبر زیر مشاهده میکنید، دکمهای با مقدار نمایشی گوگل در سمت راست، در بخش Use another service to log in اضافه شده است که بعد از کلیک بر روی آن، به صفحهی هویت سنجی گوگل ریداریکت میشوید.
در اینجا از کاربر سوال پرسیده میشود که آیا به سایت پذیرنده اجازه داده شود که اطلاعات و ایمیل شما ارسال گردند که بعد از انتخاب دکمهی Accept، لاگین انجام گرفته و اطلاعات ارسال میگردد.
توجه: رمز عبور شما به هیچ عنوان برای سایت پذیرنده ارسال نمیگردد.
لاگین با موفقیت انجام شد.
در مطلب بعدی سایر سایتهای اجتماعی قرار خواهند گرفت.
گرفتن خروجی CamelCase از JSON.NET
یک سری از کتابخانههای جاوا اسکریپتی سمت کلاینت، به نامهای خواص CamelCase نیاز دارند و حالت پیش فرض اصول نامگذاری خواص در دات نت عکس آن است. برای مثال بجای UserName به userName نیاز دارند تا بتوانند صحیح کار کنند.
روش اول حل این مشکل، استفاده از ویژگی JsonProperty بر روی تک تک خواص و مشخص کردن نامهای مورد نیاز کتابخانهی جاوا اسکریپتی به صورت صریح است.
روش دوم، استفاده از تنظیمات ContractResolver میباشد که با تنظیم آن به CamelCasePropertyNamesContractResolver به صورت خودکار به تمامی خواص به صورت یکسانی اعمال میگردد:
var json = JsonConvert.SerializeObject(obj, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() });
درج نامهای المانهای یک Enum در خروجی JSON
اگر یکی از عناصر در حال تبدیل به JSON، از نوع enum باشد، به صورت پیش فرض مقدار عددی آن در JSON نهایی درج میگردد:
using Newtonsoft.Json; namespace JsonNetTests { public enum Color { Red, Green, Blue, White } public class Item { public string Name { set; get; } public Color Color { set; get; } } public class EnumTests { public string GetJson() { var item = new Item { Name = "Item 1", Color = Color.Blue }; return JsonConvert.SerializeObject(item, Formatting.Indented); } } }
{ "Name": "Item 1", "Color": 2 }
الف) مزین کردن خاصیت از نوع enum به ویژگی JsonConverter از نوع StringEnumConverter:
[JsonConverter(typeof(StringEnumConverter))] public Color Color { set; get; }
return JsonConvert.SerializeObject(item, new JsonSerializerSettings { Formatting = Formatting.Indented, Converters = { new StringEnumConverter() } });
تهیه خروجی JSON از مدلهای مرتبط، بدون Stack overflow
دو کلاس گروههای محصولات و محصولات ذیل را درنظر بگیرید:
public class Category { public int Id { get; set; } public string Name { get; set; } public virtual ICollection<Product> Products { get; set; } public Category() { Products = new List<Product>(); } } public class Product { public int Id { get; set; } public string Name { get; set; } public virtual Category Category { get; set; } }
با توجه به این دو کلاس، سعی کنید مثال ذیل را اجرا کرده و از آن، خروجی JSON تهیه کنید:
using System.Collections.Generic; using Newtonsoft.Json; using Newtonsoft.Json.Converters; namespace JsonNetTests { public class SelfReferencingLoops { public string GetJson() { var category = new Category { Id = 1, Name = "Category 1" }; var product = new Product { Id = 1, Name = "Product 1" }; category.Products.Add(product); product.Category = category; return JsonConvert.SerializeObject(category, new JsonSerializerSettings { Formatting = Formatting.Indented, Converters = { new StringEnumConverter() } }); } } }
An unhandled exception of type 'Newtonsoft.Json.JsonSerializationException' occurred in Newtonsoft.Json.dll Additional information: Self referencing loop detected for property 'Category' with type 'JsonNetTests.Category'. Path 'Products[0]'.
راه حل اول:
به تنظیمات JSON.NET، مقدار ReferenceLoopHandling = ReferenceLoopHandling.Ignore را اضافه کنید تا از حلقهی بازگشتی بیپایان جلوگیری شود:
return JsonConvert.SerializeObject(category, new JsonSerializerSettings { Formatting = Formatting.Indented, ReferenceLoopHandling = ReferenceLoopHandling.Ignore, Converters = { new StringEnumConverter() } });
به تنظیمات JSON.NET، مقدار PreserveReferencesHandling = PreserveReferencesHandling.Objects را اضافه کنید تا مدیریت ارجاعات اشیاء توسط خود JSON.NET انجام شود:
return JsonConvert.SerializeObject(category, new JsonSerializerSettings { Formatting = Formatting.Indented, PreserveReferencesHandling = PreserveReferencesHandling.Objects, Converters = { new StringEnumConverter() } });
{ "$id": "1", "Id": 1, "Name": "Category 1", "Products": [ { "$id": "2", "Id": 1, "Name": "Product 1", "Category": { "$ref": "1" } } ] }
همچنین مزیت دیگر آن، انتقال سادهتر کدهای جاوا به سیشارپ است؛ از این لحاظ که ویژگی مشابهی در زبان جاوا تحت عنوان «Default Methods» سالها است که وجود دارد.
یک مثال از ویژگی «پیاده سازیهای پیشفرض در اینترفیسها»
interface ILogger { void Log(string message); } class ConsoleLogger : ILogger { public void Log(string message) { Console.WriteLine(message); } }
مدتی بعد بر اساس نیازمندیهای مشخصی به این نتیجه خواهید رسید که بهتر است overload دیگری را برای متد Log در اینترفیس ILogger، درنظر بگیریم. مشکلی که این تغییر به همراه دارد، کامپایل نشدن کلاس ConsoleLogger در یک برنامهی ثالث است و این کلاس باید الزاما این overload جدید را پیاده سازی کند؛ در غیراینصورت قادر به کامپایل برنامهی خود نخواهد شد. اکنون در C# 8.0 میتوان برای این نوع تغییرات، در همان اینترفیس اصلی، یک پیاده سازی پیشفرض را نیز قرار داد:
interface ILogger { void Log(string message); void Log(Exception exception) => Console.WriteLine(exception); }
ویژگی «پیاده سازیهای پیشفرض در اینترفیسها» چگونه پیاده سازی شدهاست؟
واقعیت این است که امکان پیاده سازی این ویژگی، سالها است که در سطح کدهای IL دات نت وجود داشته (از زمان دات نت 2) و اکنون از طریق کدهای برنامه با بهبود کامپایلر آن، قابل دسترسی شدهاست.
تاثیر زمینهی کاری بر روی دسترسی به پیاده سازیهای پیشفرض
مثال زیر را درنظر بگیرید:
interface IDeveloper { void LearnNewLanguage(string language, DateTime dueDate); void LearnNewLanguage(string language) { // default implementation LearnNewLanguage(language, DateTime.Now.AddMonths(6)); } } class BackendDev : IDeveloper // compiles OK { public void LearnNewLanguage(string language, DateTime dueDate) { // Learning new language... } }
سؤال: به نظر شما اکنون کدامیک از کاربردهای زیر از کلاس BackendDev، کامپایل میشود و کدامیک خیر؟
IDeveloper dev1 = new BackendDev(); dev1.LearnNewLanguage("Rust"); var dev2 = new BackendDev(); dev2.LearnNewLanguage("Rust");
There is no argument given that corresponds to the required formal parameter 'dueDate' of 'BackendDev.LearnNewLanguage(string, DateTime)' (CS7036) [ConsoleApp]
ارثبری چندگانه چطور؟
احتمالا حدس زدهاید که این قابلیت ممکن است ارثبری چندگانه را که در سیشارپ ممنوع است، میسر کند. تا C# 8.0، یک کلاس تنها از یک کلاس دیگر میتواند مشتق شود؛ اما این محدودیت در مورد اینترفیسها وجود ندارد. به علاوه تاکنون اینترفیسها مانند کلاسها، امکان تعریف پیاده سازی خاصی را نداشتند و صرفا یک قرارداد بیشتر نبودند. بنابراین اکنون این سؤال مطرح میشود که آیا میتوان با ارائهی پیاده سازی پیشفرض متدها در اینترفیسها، ارثبری چندگانه را در سیشارپ پیاده سازی کرد؛ مانند مثال زیر؟!
using System; namespace ConsoleApp { public interface IDev { void LearnNewLanguage(string language) => Console.Write($"Learning {language} in a default way."); } public interface IBackendDev : IDev { void LearnNewLanguage(string language) => Console.Write($"Learning {language} in a backend way."); } public interface IFrontendDev : IDev { void LearnNewLanguage(string language) => Console.Write($"Learning {language} in a frontend way."); } public interface IFullStackDev : IBackendDev, IFrontendDev { } public class Dev : IFullStackDev { } }
IFullStackDev dev = new Dev(); dev.LearnNewLanguage("TypeScript");
The call is ambiguous between the following methods or properties: 'IBackendDev.LearnNewLanguage(string)' and 'IFrontendDev.LearnNewLanguage(string)' (CS0121)
تفاوت امکانات کلاسهای Abstract با متدهای پیشفرض اینترفیسها چیست؟
اینترفیسها هنوز نمیتوانند مانند کلاسها، سازندهای را تعریف کنند. نمیتوانند متغیرها/فیلدهایی را در سطح اینترفیس داشته باشند. همچنین در اینترفیسها همهچیز public است و امکان تعریف سطح دسترسی دیگری وجود ندارد.
بنابراین باید بخاطر داشت که هدف از تعریف اینترفیسها، ارائهی «یک رفتار» است و هدف از تعریف کلاسها، ارائه «یک حالت».
یک نکته: در نگارشهای پیش از C# 8.0 هم میتوان ویژگی «متدهای پیشفرض» را شبیه سازی کرد
واقعیت این است که توسط ویژگی «متدهای الحاقی»، سالها است که امکان افزودن «متدهای پیشفرضی» به اینترفیسها در زبان سیشارپ وجود دارد:
namespace MyNamespace { public interface IMyInterface { IList<int> Values { get; set; } } public static class MyInterfaceExtensions { public static int CountGreaterThan(this IMyInterface myInterface, int threshold) { return myInterface.Values?.Where(p => p > threshold).Count() ?? 0; } } }
var myImplementation = new MyInterfaceImplementation(); // Note that there's no typecast to IMyInterface required var countGreaterThanFive = myImplementation.CountGreaterThan(5);
FirstName
firstName
strFirstName lblFirstName
<Company>.<Technology|Produt|Project>[.<Feature>][.<SubNamespace>]
Microsoft.Reporting.WebForms Kara.Support.Manager.Enums Kara.CSS.HQ.WebUI.Configuration
PackageManager , PacakgeConfigGenerator Circle , Utility , Package
CreateConfig , classdata CManager , ClsPackage , Config_Creator , Config1389
CenterCollection , PackageCollection
public delegate void Logger (string log); public delegate void LoggingCallback (object sender, string reason);
AddEventArgs , EditEventArgs , DeleteEventArgs
Adding , Add , Added
public delegate void <EventName>EventHandler (object sender, <EventName>EventArgs e);
public event EventHandler <AddEventArgs> Adding;
DisplayNameAttribute , MessageTypeAttribute
IComponent (اسم) IConnectionProvider (موصوف) ICloneable (صفت)
public enum FileMode { Append, Read, … }
[Flag] public enum KeyModifiers { Alt = 1, Control = 2, Shift = 4 }
public enum OperationState { DoneState, FaultState, RollbackState }
AddDays , Save , DeleteRow , BindData , Close , Open
Radius , ReportType , DataSource , Mode , CurrentCenterId
public CenterCollection Centers { get; set; }
if (list.Contains(item)) if (regularExpression.Matches(text)) if (stream.CanSeek) if (context.Created) if (form.Enabled)
if (list.IsContains(item)) if (regularExpression.Match(text)) if (stream.Seekable) if (context.IsCreated) if (form.IsEnabled)
public Color Color { get; set; }
firstName , e , id , packageId , centerName , name
public static MyType operator +(MyType left, MyType right) public static bool operator ==(MyType left, MyType right)
public static MyType operator ++(MyType value)
public static MyType operator /(MyType dividend, MyType divisor)
public static MyType operator -(MyType d1, MyType d2) // incorrect!
Area , DataBinder , PublicCacheName
_centersList _firstName _currentCenter
parameterType , packageOperationTypeId
lblName (Label) txtHeader (TextBox) btnSave (Button)
ArgumentNullException , InvalidOperaionException
System.Data
<Company>.<Component>.dll <Company>.<Project|Product|Technology>.<Component>.dll
Microsoft.CSharp.dll , Kara.CSS.Manager.dll
public int IComparer<T> {…} public delegate bool Predicate<T> (T item)
public int ISessionChannel<TSession> {…} public delegate TOutput Converter<TInput, TOutput> (TInput from) public class Nullable<T> {…} public class List<T> {…}
ArgumentExceptionIllegalCharacters ArgumentExceptionInvalidName ArgumentExceptionFileNotFound
Pascal Casing |
camel Casing |
Wrong |
Callback |
callback |
CallBack |
BitFlag |
bitFlag |
Bitflag / bitflag |
Canceled |
canceled |
Cancelled |
DoNot |
doNot |
Donot / Don’t |
|
|
|
Endpoint |
endpoint |
EndPoint / endPoint |
FileName |
fileName |
Filename / filename |
Gridline |
gridline |
GridLine / gridLine |
Hashtable |
hashtable |
HashTable / hashTable |
Id |
id |
ID |
Indexes |
indexes |
Indices |
LogOff |
logOff |
Logoff / LogOut ! |
LogOn |
logOn |
Logon / LogIn ! |
SignOut |
signOut |
Signout / SignOff |
SignIn |
signIn |
Signin / SignOn |
Metadata |
metadata |
MetaData / metaData |
Multipanel |
multipanel |
MultiPanel / multiPanel |
Multiview |
multiview |
MultiView / multiView |
Namespace |
namespace |
NameSpace / nameSpace |
Ok |
ok |
OK |
Pi |
pi |
PI |
Placeholder |
placeholder |
PlaceHolder / placeHolder |
UserName |
username |
Username / username |
WhiteSpace |
whiteSpace |
Whitespace / whitespace |
Writable |
writable |
Writeable / writeable |
public class Color { … public static Color FromArgb(…) { … } }
بررسی Claims Transformations
میخواهیم Claims بازگشت داده شدهی توسط IDP را به یکسری Claims که کار کردن با آنها در برنامهی MVC Client سادهتر است، تبدیل کنیم.
زمانیکه اطلاعات Claim، توسط میانافزار oidc دریافت میشود، ابتدا بررسی میشود که آیا دیکشنری نگاشتها وجود دارد یا خیر؟ اگر بله، کار نگاشتها از یک claim type به claim type دیگر انجام میشود.
برای مثال لیست claims اصلی بازگشت داده شدهی توسط IDP، پس از تبدیلات و نگاشتهای آن در برنامهی کلاینت، یک چنین شکلی را پیدا میکند:
Claim type: sid - Claim value: f3940d6e58cbb576669ee49c90e22cb1 Claim type: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier - Claim value: d860efca-22d9-47fd-8249-791ba61b07c7 Claim type: http://schemas.microsoft.com/identity/claims/identityprovider - Claim value: local Claim type: http://schemas.microsoft.com/claims/authnmethodsreferences - Claim value: pwd Claim type: given_name - Claim value: Vahid Claim type: family_name - Claim value: N
namespace ImageGallery.MvcClient.WebApp { public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); }
در ادامه اگر مجددا لیست claims را پس از logout و login، بررسی کنیم، به این صورت تبدیل شدهاست:
• Claim type: sid - Claim value: 91f5a09da5cdbbe18762526da1b996fb • Claim type: sub - Claim value: d860efca-22d9-47fd-8249-791ba61b07c7 • Claim type: idp - Claim value: local • Claim type: given_name - Claim value: Vahid • Claim type: family_name - Claim value: N
کار با مجموعهی User Claims
تا اینجا لیست this.User.Claims، به همراه تعدادی Claims است که به آنها نیازی نداریم؛ مانند sid که بیانگر session id در سمت IDP است و یا idp به معنای identity provider میباشد. حذف آنها حجم کوکی نگهداری کنندهی آنها را کاهش میدهد. همچنین میخواهیم تعدادی دیگر را نیز به آنها اضافه کنیم.
علاوه بر اینها میانافزار oidc، یکسری از claims دریافتی را راسا فیلتر و حذف میکند؛ مانند زمان منقضی شدن توکن و امثال آن که در عمل واقعا به تعدادی از آنها نیازی نداریم. اما میتوان این سطح تصمیم گیری فیلتر claims رسیده را نیز کنترل کرد. در تنظیمات متد AddOpenIdConnect، خاصیت options.ClaimActions نیز وجود دارد که توسط آن میتوان بر روی حذف و یا افزوده شدن Claims، کنترل بیشتری را اعمال کرد:
namespace ImageGallery.MvcClient.WebApp { public void ConfigureServices(IServiceCollection services) { // ... .AddOpenIdConnect("oidc", options => { // ... options.ClaimActions.Remove("amr"); options.ClaimActions.DeleteClaim("sid"); options.ClaimActions.DeleteClaim("idp"); }); }
اکنون اگر پس از logout و login، لیست this.User.Claims را بررسی کنیم، دیگر خبری از sid و idp در آن نیست. همچنین claim از نوع amr نیز به صورت پیشفرض حذف نشدهاست:
• Claim type: sub - Claim value: d860efca-22d9-47fd-8249-791ba61b07c7 • Claim type: amr - Claim value: pwd • Claim type: given_name - Claim value: Vahid • Claim type: family_name - Claim value: N
افزودن Claim جدید آدرس کاربر
برای افزودن Claim جدید آدرس کاربر، به کلاس src\IDP\DNT.IDP\Config.cs مراجعه کرده و آنرا به صورت زیر تکمیل میکنیم:
namespace DNT.IDP { public static class Config { // identity-related resources (scopes) public static IEnumerable<IdentityResource> GetIdentityResources() { return new List<IdentityResource> { new IdentityResources.OpenId(), new IdentityResources.Profile(), new IdentityResources.Address() }; }
همین مورد را به لیست AllowedScopes متد GetClients نیز اضافه میکنیم:
AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.Address },
namespace DNT.IDP { public static class Config { public static List<TestUser> GetUsers() { return new List<TestUser> { new TestUser { // ... Claims = new List<Claim> { // ... new Claim("address", "Main Road 1") } }, new TestUser { // ... Claims = new List<Claim> { // ... new Claim("address", "Big Street 2") } } }; }
پس از آن به کلاس ImageGallery.MvcClient.WebApp\Startup.cs مراجعه میکنیم تا درخواست این claim را به لیست scopes میانافزار oidc اضافه کنیم:
namespace ImageGallery.MvcClient.WebApp { public class Startup { public void ConfigureServices(IServiceCollection services) { // ... .AddOpenIdConnect("oidc", options => { // ... options.Scope.Add("address"); // … options.ClaimActions.DeleteClaim("address"); }); }
یک نکته: فراخوانی DeleteClaim بر روی address غیر ضروری است و میشود این سطر را حذف کرد. از این جهت که اگر به سورس OpenID Connect Options مایکروسافت مراجعه کنیم، مشاهده خواهیم کرد که میانافزار اعتبارسنجی استاندارد ASP.NET Core، تنها تعداد معدودی از claims را نگاشت میکند. به این معنا که هر claim ای که در token وجود داشته باشد، اما اینجا نگاشت نشده باشد، در claims نهایی حضور نخواهند داشت و address claim یکی از اینها نیست. بنابراین در لیست نهایی this.User.Claims حضور نخواهد داشت؛ مگر اینکه مطابق همین سورس، با استفاده از متد options.ClaimActions.MapUniqueJsonKey، یک نگاشت جدید را برای آن تهیه کنیم و البته چون نمیخواهیم آدرس در لیست claims وجود داشته باشد، این نگاشت را تعریف نخواهیم کرد.
دریافت اطلاعات بیشتری از کاربران از طریق UserInfo Endpoint
همانطور که در قسمت قبل با بررسی «تنظیمات بازگشت Claims کاربر به برنامهی کلاینت» عنوان شد، میانافزار oidc با UserInfo Endpoint کار میکند که تمام عملیات آن خودکار است. در اینجا امکان کار با آن از طریق برنامه نویسی مستقیم نیز جهت دریافت اطلاعات بیشتری از کاربران، وجود دارد. برای مثال شاید به دلایل امنیتی نخواهیم آدرس کاربر را در لیست Claims او قرار دهیم. این مورد سبب کوچکتر شدن کوکی متناظر با این اطلاعات و همچنین دسترسی به اطلاعات به روزتری از کاربر میشود.
درخواستی که به سمت UserInfo Endpoint ارسال میشود، باید یک چنین فرمتی را داشته باشد:
GET idphostaddress/connect/userinfo Authorization: Bearer R9aty5OPlk
اکنون برای دریافت دستی اطلاعات آدرس از IDP و UserInfo Endpoint آن، ابتدا نیاز است بستهی نیوگت IdentityModel را به پروژهی Mvc Client اضافه کنیم:
dotnet add package IdentityModel
namespace ImageGallery.MvcClient.WebApp.Controllers { [Authorize] public class GalleryController : Controller { public async Task<IActionResult> OrderFrame() { var discoveryClient = new DiscoveryClient(_configuration["IDPBaseAddress"]); var metaDataResponse = await discoveryClient.GetAsync(); var userInfoClient = new UserInfoClient(metaDataResponse.UserInfoEndpoint); var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken); var response = await userInfoClient.GetAsync(accessToken); if (response.IsError) { throw new Exception("Problem accessing the UserInfo endpoint.", response.Exception); } var address = response.Claims.FirstOrDefault(c => c.Type == "address")?.Value; return View(new OrderFrameViewModel(address)); }
OrderFrameViewModel ارسالی به View، یک چنین شکلی را دارد:
namespace ImageGallery.MvcClient.ViewModels { public class OrderFrameViewModel { public string Address { get; } = string.Empty; public OrderFrameViewModel(string address) { Address = address; } } }
@model ImageGallery.MvcClient.ViewModels.OrderFrameViewModel <div class="container"> <div class="h3 bottomMarginDefault">Order a framed version of your favorite picture.</div> <div class="text bottomMarginSmall">We've got this address on record for you:</div> <div class="text text-info bottomMarginSmall">@Model.Address</div> <div class="text">If this isn't correct, please contact us.</div> </div>
سپس به Shared\_Layout.cshtml مراجعه کرده و لینکی را به این اکشن متد و View، اضافه میکنیم:
<li><a asp-area="" asp-controller="Gallery" asp-action="OrderFrame">Order a framed picture</a></li>
اکنون اگر برنامه را اجرا کنیم، پس از login، یک چنین خروجی قابل مشاهده است:
همانطور که ملاحظه میکنید، آدرس شخص به صورت مستقیم از UserInfo Endpoint دریافت و نمایش داده شدهاست.
بررسی Authorization مبتنی بر نقشها
تا اینجا مرحلهی Authentication را که مشخص میکند کاربر وار شدهی به سیستم کیست، بررسی کردیم که اطلاعات آن از Identity token دریافتی از IDP استخراج میشود. مرحلهی پس از ورود به سیستم، مشخص کردن سطوح دسترسی کاربر و تعیین این مورد است که کاربر مجاز به انجام چه کارهایی میباشد. به این مرحله Authorization میگویند و روشهای مختلفی برای مدیریت آن وجود دارد:
الف) RBAC و یا Role-based Authorization و یا تعیین سطوح دسترسی بر اساس نقشهای کاربر
در این حالت claim ویژهی role، از IDP دریافت شده و توسط آن یکسری سطوح دسترسی کاربر مشخص میشوند. برای مثال کاربر وارد شدهی به سیستم میتواند تصویری را اضافه کند و یا آیا مجاز است نگارش قاب شدهی تصویری را درخواست دهد؟
ب) ABAC و یا Attribute based access control روش دیگر مدیریت سطوح دسترسی است و عموما آنرا نسبت به حالت الف ترجیح میدهند که آنرا در قسمتهای بعدی بررسی خواهیم کرد.
در اینجا روش «تعیین سطوح دسترسی بر اساس نقشهای کاربر» را بررسی میکنیم. برای این منظور به تنظیمات IDP در فایل src\IDP\DNT.IDP\Config.cs مراجعه کرده و claims جدیدی را تعریف میکنیم:
namespace DNT.IDP { public static class Config { public static List<TestUser> GetUsers() { return new List<TestUser> { new TestUser { //... Claims = new List<Claim> { //... new Claim("role", "PayingUser") } }, new TestUser { //... Claims = new List<Claim> { //... new Claim("role", "FreeUser") } } }; }
سپس باید برای این claim جدید یک scope جدید را نیز به قسمت GetIdentityResources اضافه کنیم تا توسط client قابل دریافت شود:
namespace DNT.IDP { public static class Config { public static IEnumerable<IdentityResource> GetIdentityResources() { return new List<IdentityResource> { // ... new IdentityResource( name: "roles", displayName: "Your role(s)", claimTypes: new List<string>() { "role" }) }; }
همچنین باید به کلاینت مجوز درخواست این scope را نیز بدهیم. به همین جهت آنرا به AllowedScopes مشخصات Client نیز اضافه میکنیم:
namespace DNT.IDP { public static class Config { public static IEnumerable<Client> GetClients() { return new List<Client> { new Client { // ... AllowedScopes = { // ... "roles" } // ... } }; }
در ادامه قرار است تنها کاربری که دارای نقش PayingUser است، امکان دسترسی به سفارش نگارش قاب شدهی تصاویر را داشته باشد. به همین جهت به کلاس ImageGallery.MvcClient.WebApp\Startup.cs برنامهی کلاینت مراجعه کرده و درخواست scope نقشهای کاربر را به متد تنظیمات AddOpenIdConnect اضافه میکنیم:
options.Scope.Add("roles");
برای آزمایش آن، یکبار از برنامه خارج شده و مجددا به آن وارد شوید. اینبار در صفحهی consent، از کاربر مجوز دسترسی به نقشهای او نیز سؤال پرسیده میشود:
اما اگر به لیست موجود در this.User.Claims در برنامهی کلاینت مراجعه کنیم، نقش او را مشاهده نخواهیم کرد و به این لیست اضافه نشدهاست:
• Claim type: sub - Claim value: d860efca-22d9-47fd-8249-791ba61b07c7 • Claim type: amr - Claim value: pwd • Claim type: given_name - Claim value: Vahid • Claim type: family_name - Claim value: N
همانطور که در نکتهای پیشتر نیز ذکر شد، چون role جزو لیست نگاشتهای OpenID Connect Options مایکروسافت نیست، آنرا به صورت خودکار به لیست claims اضافه نمیکند؛ دقیقا مانند آدرسی که بررسی کردیم. برای رفع این مشکل و افزودن نگاشت آن در متد تنظیمات AddOpenIdConnect، میتوان از متد MapUniqueJsonKey به صورت زیر استفاده کرد:
options.ClaimActions.MapUniqueJsonKey(claimType: "role", jsonKey: "role");
• Claim type: sub - Claim value: d860efca-22d9-47fd-8249-791ba61b07c7 • Claim type: amr - Claim value: pwd • Claim type: given_name - Claim value: Vahid • Claim type: family_name - Claim value: N • Claim type: role - Claim value: PayingUser
استفاده از نقش تعریف شده جهت محدود کردن دسترسی به سفارش تصاویر قاب شده
در ادامه قصد داریم لینک درخواست تصاویر قاب شده را فقط برای کاربرانی که دارای نقش PayingUser هستند، نمایش دهیم. به همین جهت به فایل Views\Shared\_Layout.cshtml مراجعه کرده و آنرا به صورت زیر تغییر میدهیم:
@if(User.IsInRole("PayingUser")) { <li><a asp-area="" asp-controller="Gallery" asp-action="OrderFrame">Order a framed picture</a></li> }
options.TokenValidationParameters = new TokenValidationParameters { NameClaimType = JwtClaimTypes.GivenName, RoleClaimType = JwtClaimTypes.Role, };
اکنون برای آزمایش آن یکبار از سیستم خارج شده و مجددا به آن وارد شوید. پس از آن لینک درخواست نگارش قاب شدهی تصاویر، برای کاربر User 1 نمایان خواهد بود و نه برای User 2 که FreeUser است.
البته هرچند تا این لحظه لینک نمایش View متناظر با اکشن متد OrderFrame را امن کردهایم، اما هنوز خود این اکشن متد به صورت مستقیم با وارد کردن آدرس https://localhost:5001/Gallery/OrderFrame در مرورگر قابل دسترسی است. برای رفع این مشکل به کنترلر گالری مراجعه کرده و دسترسی به اکشن متد OrderFrame را توسط فیلتر Authorize و با مقدار دهی خاصیت Roles آن محدود میکنیم:
namespace ImageGallery.MvcClient.WebApp.Controllers { [Authorize] public class GalleryController : Controller { [Authorize(Roles = "PayingUser")] public async Task<IActionResult> OrderFrame() {
برای آزمایش آن، توسط مشخصات کاربر User 2 به سیستم وارد شده و آدرس https://localhost:5001/Gallery/OrderFrame را مستقیما در مرورگر وارد کنید. در این حالت یک چنین تصویری نمایان خواهد شد:
همانطور که مشاهده میکنید، علاوه بر عدم دسترسی به این اکشن متد، به صفحهی Account/AccessDenied که هنوز در برنامهی کلاینت تعریف نشدهاست، هدایت شدهایم. به همین جهت خطای 404 و یا یافت نشد، نمایش داده شدهاست.
برای تغییر مقدار پیشفرض صفحهی عدم دسترسی، ابتدا Controllers\AuthorizationController.cs را با این محتوا ایجاد میکنیم:
using Microsoft.AspNetCore.Mvc; public class AuthorizationController : Controller { public IActionResult AccessDenied() { return View(); } }
<div class="container"> <div class="h3">Woops, looks like you're not authorized to view this page.</div> <div>Would you prefer to <a asp-controller="Gallery" asp-action="Logout">log in as someone else</a>?</div> </div>
اکنون نیاز است تا این آدرس جدید را به کلاس ImageGallery.MvcClient.WebApp\Startup.cs معرفی کنیم.
namespace ImageGallery.MvcClient.WebApp { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(options => { // ... }).AddCookie("Cookies", options => { options.AccessDeniedPath = "/Authorization/AccessDenied"; }) // ...
برای آزمایش آن، یکبار از برنامه خارج شده و مجددا با اکانت User 2 به آن وارد شوید و آدرس https://localhost:5001/Gallery/OrderFrame را مستقیما در مرورگر وارد کنید. اینبار تصویر زیر که همان آدرس جدید تنظیم شدهاست نمایش داده خواهد شد:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی 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 وارد کنید.
پیاده سازی الگوی Decorator به کمک سیستم تزریق وابستگیهای NET Core.
مثال زیر را در نظر بگیرید که در آن یک سرویس تعریف شدهاست و در این بین استثنائی رخ دادهاست.
public interface ITaskService { void Run(); } public class MyTaskService : ITaskService { public void Run() { throw new InvalidOperationException("An exception from the MyTaskService!"); } }
using System; using Microsoft.Extensions.Logging; namespace CoreIocServices { public class MyTaskServiceDecorator : ITaskService { private readonly ILogger<MyTaskServiceDecorator> _logger; private readonly ITaskService _decorated; public MyTaskServiceDecorator( ILogger<MyTaskServiceDecorator> logger, ITaskService decorated) { _logger = logger; _decorated = decorated; } public void Run() { try { _decorated.Run(); } catch (Exception ex) { _logger.LogCritical(ex, "An unhandled exception has been occurred."); } } } }
مزیت اینکار، پیاده سازی اصل DRY یا Don't repeat yourself است. کاری که برای رفع این مشکل قرار است انجام دهیم، استفاده از یک تزئین کننده (محصور کننده)، کپسوله سازی اعمال تکراری و سپس اتصال آن به قسمتهای مختلف برنامه است. همچنین در این حالت اصل open closed principle نیز بهتر رعایت خواهد شد. از این جهت که کدهای تکراری برنامه به یک لایهی دیگر منتقل شدهاند و دیگر نیازی نیست برای تغییر آنها، کدهای قسمتهای اصلی برنامه را تغییر داد (کدهای برنامه باز خواهند بود برای توسعه و بسته برای تغییر).
پس از طراحی این تزئین کننده، اکنون نوبت به معرفی آن به سیستم تزریق وابستگیهای NET Core. است:
namespace CoreIocSample02 { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddTransient<MyTaskService>(); services.AddTransient<ITaskService>(serviceProvider => new MyTaskServiceDecorator( serviceProvider.GetService<ILogger<MyTaskServiceDecorator>>(), serviceProvider.GetService<MyTaskService>()) );
در اینجا هم میتوان در صورت نیاز اصل کلاس MyTaskService را بدون هیچ نوع تزئین کنندهای از سیستم تزریق وابستگیها دریافت کرد و یا اگر وهلهای از سرویس ITaskService را از آن درخواست کردیم، ابتدا شیء MyTaskServiceDecorator وهله سازی شده و سپس توسط آن یک نمونهی محصور شده و تزئین شدهی MyTaskService به فراخوان بازگشت داده خواهد شد.
ساده سازی معرفی تزئین کنندهها به سیستم تزریق وابستگیهای NET Core. به کمک Scrutor
در «قسمت هشتم - ساده سازی معرفی سرویسها توسط Scrutor» با کتابخانهی Scrutor آشنا شدیم. یکی دیگر از قابلیتهای آن، امکان ساده سازی تعریف تزئین کنندها است:
namespace CoreIocSample02 { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddTransient<ITaskService, MyTaskService>(); services.Decorate<ITaskService, MyTaskServiceDecorator>();