public void Remove(int id) { var selectedComment = _comments.Find(id); _comments.Where(x => x.ParentId == id).Load(); _comments.Remove(selectedComment); }
امکان نمونه سازی بدون قید و شرط کلاسها
تعریف کلاس Article1 را به صورت زیر درنظر بگیرید:
public class Article1 { public string Title { get; set; } public string? Subtitle { get; set; } public string Author { get; set; } public DateTime Published { get; set; } }
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net7.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> </Project>
Non-nullable property 'Title' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [CS11Tests]csharp(CS8618) Non-nullable property 'Author' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [CS11Tests]csharp(CS8618)
مدیریت کردن نحوهی نمونه سازی کلاسها، با وابستگی به سازندههای آن
یکی از روشهای مدیریت مشکلی که تا اینجا بررسی شد، تعریف سازندههای متعددی برای یک کلاس است؛ تا توسط آن بتوان مقدار دهی یک سری از خواص را اجباری کرد:
public class Article2 { public Article2(string title, string subtitle, string author, DateTime published) { Title = title; Subtitle = subtitle; Author = author; Published = published; } public Article2(string title, string author, DateTime published) { Title = title; Author = author; Published = published; } public string Title { get; set; } public string? Subtitle { get; set; } public string Author { get; set; } public DateTime Published { get; set; } }
- تعداد سطرهای تعریف این کلاس، به شدت افزایش یافتهاست.
- با اضافه شدن خواص بیشتری به کلاس، به تعاریف بیشتری نیاز خواهد بود.
- سازندهها کار خاصی را بجز نگاشت مقادیر ارائه شده، به خواص کلاس، انجام نمیدهند.
- نمونه سازی این کلاسها، شکل طولانی و غیرواضح زیر را پیدا میکند و زیبایی inline object initializers را ندارند:
Article2 article = new("C# 11 Required Keyword", "A new language feature", "Name", new DateTime(2022, 11, 12));
البته روش دیگر مدیریت یک چنین اخطارهایی، استفاده از مقدار ویژهی !default است که سبب محو اخطارهای یاد شده میشود؛ اما باز هم مقدار دهی آنرا الزامی نمیکند. فقط به این معنا است که قول میدهیم این خاصیت را در جای دیگری مقدار دهی کنیم و هیچگاه نال نباشد!
public string Title { get; set; } = default!;
مدیریت کردن نحوهی نمونه سازی کلاسها، بدون وابستگی به سازندههای آن در C# 11.0
C# 11 به همراه واژهی کلیدی جدیدی به نام required است تا دیگر نیازی نباشد همانند راه حل فوق، سازندههای متعددی را جهت اجبار به مقدار دهی خواص یک شیء، تعریف کنیم. در این حالت تعریف کلاس Article به صورت زیر خلاصه میشود و دیگر به همراه اخطارهای کامپایلر نمایش داده شده نیز نیست:
public class Article3 { public required string Title { get; set; } public string? Subtitle { get; set; } public required string Author { get; set; } public DateTime Published { get; set; } }
معرفی ویژگی جدید SetsRequiredMembers
کلاس Book زیر را که به همراه یک خاصیت required و دو سازندهاست، درنظر بگیرید:
public class Book { public Book() => Name = string.Empty; public Book(string name) => Name = name; public required string Name { get; set; } }
Book book = new("Book's Name");
Required member 'Book.Name' must be set in the object initializer or attribute constructor. [CS11Tests]csharp(CS9035)
public class Book { [SetsRequiredMembers] public Book() => Name = string.Empty; [SetsRequiredMembers] public Book(string name) => Name = name; public required string Name { get; set; } }
محدودیتهای کار با خواص required
- واژهی کلیدی required را میتوان تنها به خواص و فیلدهای نوعهای class, record, record struct اعمال کرد. امکان اعمال این واژهی کلیدی به اجزای یک اینترفیس وجود ندارد.
- میدان دید اعضای required باید حداقل در حد نوعهای دربرگیرندهی آنها باشند. برای مثال اگر کلاسی public است، نمیتوان در آن یک فیلد required با میدان دید protected را تعریف کرد.
- نوعهای مشتق شدهی از یک نوع پایه، نمیتوانند اعضای required آنرا مخفی کنند و اگر قصد بازنویسی آنرا دارند، باید حتما واژهی کلیدی required را لحاظ کنند.
- اگر سازندهای به سازندهی دیگری از طریق ذکر ()base و یا ()this زنجیر شده باشد نیز باید ویژگی SetsRequiredMembers مرتبط را تکرار کند.
Primary Constructors چیست؟
Primary Constructors، قابلیتی است که به C# 12 اضافه شدهاست تا توسط آن بتوان خواص را مستقیما توسط پارامترهای سازندهی یک کلاس تعریف و همچنین مقدار دهی کرد. هدف از آن، کاهش قابل ملاحظهی یکسری کدهای تکراری و مشخص است تا به کلاسهایی زیباتر، کمحجمتر و خواناتر برسیم. برای مثال کلاس متداول زیر را درنظر بگیرید:
public class Employee { public string FirstName { get; set; } public string LastName { get; set; } public DateTime HireDate { get; set; } public decimal Salary { get; set; } public Employee(string firstName, string lastName, DateTime hireDate, decimal salary) { FirstName = firstName; LastName = lastName; HireDate = hireDate; Salary = salary; } }
اکنون اگر بخواهیم همین کلاس را با استفاده از ویژگی Primary Constructor اضافه شده به C# 12.0 بازنویسی کنیم، به قطعه کد زیر میرسیم:
public class Employee(string firstName, string lastName, DateTime hireDate, decimal salary) { public string FirstName { get; set; } = firstName; public string LastName { get; set; } = lastName; public DateTime HireDate { get; set; } = hireDate; public decimal Salary { get; set; } = salary; }
var employee = new Employee("John", "Doe", new DateTime(2020, 1, 1), 50000);
یک نکته: اگر از Rider و یا ReSharper استفاده میکنید، یک چنین Refactoring توکاری جهت سهولت کار، به آنها اضافه شدهاست و به سرعت میتوان این تبدیلات را توسط آنها انجام داد.
توضیحات:
- متد سازنده در این حالت، به ظاهر حذف شده و به قسمت تعریف کلاس منتقل شدهاست.
- تمام مقدار دهیهای آغازین موجود در متد سازندهی پیشین نیز حذف شدهاند و مستقیما به قسمت تعریف خواص، منتقل شدهاند.
در نتیجه از یک کلاس 15 سطری، به کلاسی 7 سطری رسیدهایم که کاهش حجم قابل ملاحظهای را پیدا کردهاست.
نکته 1: هیچ ضرورتی وجود ندارد که به همراه یک primary constructor، خواصی هم مانند مثال فوق ارائه شوند؛ چون پارامترهای آن در تمام اعضای این کلاس، به همین شکل، قابل دسترسی هستند. در این مثال صرفا جهت بازسازی کد قبلی، این خواص اضافی را مشاهده میکنید. یعنی اگر تنها قرار بود، کار تزریق وابستگیها صورت گیرد که عموما به همراه تعریف فیلدهایی جهت انتساب پارامترهای متد سازنده به آنها است، استفاده از یک primary constructor، کدهای فوق را بیش از این هم فشردهتر میکرد و ... یک سطری میشد.
نکته 2: استفاده از پارامترهای سازندهی اولیه، صرفا جهت مقدار دهی خواص عمومی یک کلاس، یک code smell هستند! چون میتوان یک چنین کارهایی را به نحو شکیلتری توسط required properties معرفی شدهی در C# 11، پیاده سازی کرد.
بررسی تاریخچهی primary constructors
همانطور که در مقدمهی بحث نیز عنوان شد، primary constructors قابلیت جدیدی نیست و برای نمونه به همراه C# 9 و مفهوم جدید رکوردهای آن، ارائه شد:
public record class Book(string Title, string Publisher);
پس از آن در C# 10، این توسعه ادامه یافت و به امکان تعریف record structها، بسط یافت که در اینجا هم قابلیت تعریف primary constructors وجود دارد:
public record struct Color(int R, int G, int B);
پس از این مقدمات، اکنون در C# 12 نیز میتوان primary constructors را به تمام کلاسها و structهای معمولی هم اعمال کرد؛ با این تفاوت که در اینجا برخلاف رکوردها، کدهای خواصهای متناظر، به صورت خودکار تولید نمیشوند و اگر به آنها نیاز دارید، باید آنها را همانند مثال ابتدای بحث، خودتان به صورت دستی تعریف کنید.
primary constructors کلاسها و structهای معمولی، با primary constructors رکوردها یکی نیست
در C# 12 و به همراه معرفی primary constructors مخصوص کلاسها و structهای معمولی آن، از روش متفاوتی برای دسترسی به پارامترهای تعریف شده، استفاده میکند که به آن capturing semantics هم میگویند. در این حالت پارامترهای تعریف شدهی در یک primary constructor، توسط هر عضوی از آن کلاس قابل استفادهاست که یکی از کاربردهای آن، ساده کردن تعاریف تزریق وابستگیها است. در این حالت دیگر نیازی نیست تا ابتدا یک فیلد را برای انتساب به پارامتر تزریق شده تعریف کرد و سپس از آن فیلد، استفاده نمود؛ مستقیما میتوان با همان پارامتر تعریف شده، در متدها و اعضای کلاس، کار کرد.
برای مثال سرویس زیر را که از تزریق وابستگیها، در سازندهی خود استفاده میکند، درنظر بگیرید:
public class MyService { private readonly IDepedent _dependent; public MyService(IDependent dependent) { _dependent = dependent; } public void Do() { _dependent.DoWork(); } }
public class MyService(IDependent dependent) { public void Do() { dependent.DoWork(); } }
البته مفهوم Captures هم در زبان #C جدید نیست و در ابتدا به همراه anonymous methods و بعدها به همراه lambda expressions، معرفی و بکار گرفته شد. برای مثال درون یک lambda expression، اگر از متغیری خارج از آن lambda expressions استفاده شود، کامپایلر یک capture از آن متغیر را تهیه کرده و استفاده میکند.
بنابراین به صورت خلاصه primary constructors در رکوردها، با هدف تعریف خواص عمومی فقط خواندنی، ارائه شدند؛ اما primary constructors ارائه شدهی در C# 12 که اینبار قابل اعمال به کلاسها و structs معمولی است، بیشتر هدف ساده سازی تعریف کدهای تکراری private fields را دنبال میکند. برای نمونه این کدی است که کامپایلر برای primary constructor مثال ابتدای بحث تولید میکند و در اینجا نحوهی تولید خودکار این فیلدهای خصوصی را مشاهده میکنید:
using System; using System.Diagnostics; using System.Runtime.CompilerServices; namespace CS8Tests { [NullableContext(1)] [Nullable(0)] public class Employee { [CompilerGenerated] [DebuggerBrowsable(DebuggerBrowsableState.Never)] private string <FirstName>k__BackingField; [CompilerGenerated] [DebuggerBrowsable(DebuggerBrowsableState.Never)] private string <LastName>k__BackingField; [CompilerGenerated] [DebuggerBrowsable(DebuggerBrowsableState.Never)] private DateTime <HireDate>k__BackingField; [CompilerGenerated] [DebuggerBrowsable(DebuggerBrowsableState.Never)] private Decimal <Salary>k__BackingField; public Employee(string firstName, string lastName, DateTime hireDate, Decimal salary) { this.<FirstName>k__BackingField = firstName; this.<LastName>k__BackingField = lastName; this.<HireDate>k__BackingField = hireDate; this.<Salary>k__BackingField = salary; base..ctor(); } public string FirstName { [CompilerGenerated] get { return this.<FirstName>k__BackingField; } [CompilerGenerated] set { this.<FirstName>k__BackingField = value; } } public string LastName { [CompilerGenerated] get { return this.<LastName>k__BackingField; } [CompilerGenerated] set { this.<LastName>k__BackingField = value; } } public DateTime HireDate { [CompilerGenerated] get { return this.<HireDate>k__BackingField; } [CompilerGenerated] set { this.<HireDate>k__BackingField = value; } } public Decimal Salary { [CompilerGenerated] get { return this.<Salary>k__BackingField; } [CompilerGenerated] set { this.<Salary>k__BackingField = value; } } } }
یک نکته: برای مشاهدهی یک چنین کدهایی میتوانید از منوی Tools->IL Viewer برنامهی Rider استفاده کرده و در برگهی ظاهر شده، گزینهی #Low-Level C آنرا انتخاب نمائید.
امکان تعریف سازندههای دیگر، به همراه سازندهی اولیه
اگر به کدهای #Low-Level C تولیدی فوق دقت کنید، این کلاس، به همراه یک سازندهی خالی بدون پارامتر (parameter less constructor) نیست و سازندهی پیشفرضی (default constructor) برای آن درنظر گرفته نشدهاست ... اما اگر کلاسی به همراه یک primary constructor تعریف شد، میتوان با استفاده از واژهی کلیدی this، سازندهی ثانویهای را هم برای آن تعریف کرد:
public class Person(string firstName, string lastName) { public Person() : this("John", "Smith") { } public Person(string firstName) : this(firstName, "Smith") { } public string FullName => $"{firstName} {lastName}"; }
امکان ارثبری و تعریف سازندهی اولیه
مثال زیر را درنظر بگیرید که در آن کلاس مشتق شدهی از کلاس User، یک سازندهی اولیه را تعریف کرده:
public class User { public User(string firstName, string lastName) { } } public class Editor(string firstName, string lastName) : User { }
البته این محدودیت با structها وجود ندارد؛ چون structها، value type هستند و همواره به صورت پیشفرض، به همراه یک سازندهی پیش فرض بدون پارامتر، تولید میشوند.
یک مثال: قطعه کد متداول ارثبری زیر را درنظر بگیرید که در آن، کلاس مشتق شده به کمک واژهی کلید base، امکان تعریف سازندهی جدیدی را یافته و یکی از پارامترهای سازندهی کلاس پایه را مقدار دهی میکند:
public class Automobile { public Automobile(int wheels, int seats) { Wheels = wheels; Seats = seats; } public int Wheels { get; } public int Seats { get; } } public class Car : Automobile { public Car(int seats) : base(4, seats) { } }
public class Automobile(int wheels, int seats) { public int Wheels { get; } = wheels; public int Seats { get; } = seats; } public class Car(int seats) : Automobile(4, seats);
و یا یک نمونه مثال دیگر آن به صورت زیر است که در آن، ذکر بدنهی کلاس در C# 12، الزامی ندارد:
public class MyBaseClass(string s); // no body required public class Derived(int i, string s, bool b) : MyBaseClass(s) { public int I { get; set; } = i; public string B => b.ToString(); }
توصیه به پرهیز از double capturing
با مفهوم capture در این مطلب آشنا شدیم. در مثال زیر دوبار از پارامتر سازندهی age، در دو قسمت عمومی شده، استفاده شدهاست:
public class Human(int age) { // initialization public int Age { get; set; } = age; // capture public string Bio => $"My age is {age}!"; }
var p = new Human(42); Console.WriteLine(p.Age); // Output: 42 Console.WriteLine(p.Bio); // Output: My age is 42! p.Age++; Console.WriteLine(p.Age); // Output: 43 Console.WriteLine(p.Bio); // Output: My age is 42! // !
درک بهتر آن، نیاز به #Low-Level C کلاس Human را دارد:
using System.Diagnostics; using System.Runtime.CompilerServices; namespace CS8Tests { [NullableContext(1)] [Nullable(0)] public class Human { [CompilerGenerated] [DebuggerBrowsable(DebuggerBrowsableState.Never)] private int <age>P; [CompilerGenerated] [DebuggerBrowsable(DebuggerBrowsableState.Never)] private int <Age>k__BackingField; public Human(int age) { this.<age>P = age; this.<Age>k__BackingField = this.<age>P; base..ctor(); } public int Age { [CompilerGenerated] get { return this.<Age>k__BackingField; } [CompilerGenerated] set { this.<Age>k__BackingField = value; } } public string Bio { get { DefaultInterpolatedStringHandler interpolatedStringHandler = new DefaultInterpolatedStringHandler(11, 1); interpolatedStringHandler.AppendLiteral("My age is "); interpolatedStringHandler.AppendFormatted<int>(this.<age>P); interpolatedStringHandler.AppendLiteral("!"); return interpolatedStringHandler.ToStringAndClear(); } } } }
public Human(int age) { this.<age>P = age; this.<Age>k__BackingField = this.<age>P; base..ctor(); }
مدیریت رجیستری در #C
رجیستری یک پایگاه دادهی سیستمی است که برنامهها، اجزای سیستم و اطلاعات پیکربندی در آن ذخیره و بازیابی میشود. دادههای ذخیره شده در رجیستری مطابق با نسخه ویندوز فرق میکنند. نرمافزارها برای بازیابی، تغییر و پاک کردن رجیستری از API های مختلفی استفاده میکنند. خوشبختانه .NET نیز امکانات لازم برای مدیریت رجیستری را فراهم کرده است.
در صورت رخداد خطا در رجیستری، امکان خراب شدن ویندوز وجود دارد در نتیجه با احتیاط عمل کنید و قبل از هر کاری از رجیستری پشتیبان تهیه نمایید. قبل از شروع به کدنویسی قدری با ساختار رجیستری آشنا شویم تا در ادامه قادر به درک مفاهیم باشیم.
ساختار رجیستری
رجیستری اطلاعات را در ساختار درختی نگاه میدارد. هر گره در درخت، یک کلید ( key ) نامیده میشود. هر کلید میتواند شامل چندین زیرکلید ( subkey ) و چندین مقدار ( value ) باشد. در برخی موارد، وجود یک کلید تمام اطلاعاتی است که نرم افزار بدان نیاز دارد و در برخی موارد، برنامه کلید را باز کرده و مقادیر مربوط به آن کلید را میخواند. یک کلید میتواند هر تعداد مقدار داشته باشد و مقادیر به هر شکلی میتوانند باشند. هر کلید شامل یک یا چند کاراکتر است. نام کلیدها نمیتوانند کاراکتر “\” را داشته باشند. نام هر زیرکلید یکتاست و وابسته به کلیدی است که در سلسله مراتب، بلافاصله بالای آن میآید. نام کلیدها باید انگلیسی باشند اما مقادیر را به هر زبانی میتوان نوشت. در زیر یک نمونه از ساختار رجیستری را مشاهده میکنید که در نرمافزار registry editor به نمایش در آمده است.
هر کدام از درختهای زیر my computer یک کلید است. HKEY_LOCAL_MACHINE دارای زیرکلیدهایی مثل HARDWARE ، SAM و SECURITY است. هر مقدار شامل یک اسم، نوع و دادههای درون آن است. برای مثال MaxObjectNumber از مقادیر زیرکلید HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\VIDEO است. دادههای درون هر مقدار میتواند از انواع باینری، رشتهای و عددی باشد؛ برای مثال MaxObjectNumber یک عدد ۳۲ بیتی است.
محدودیتهای فنی برای نوع و اندازهی اطلاعاتی که در رجیستری ذخیره میگردد، وجود دارد. برنامهها باید اطلاعات اولیه و پیکربندی را در رجیستری نگه دارند وسایر دادهها را در جای دیگر ذخیره کنند. معمولا دادههای بیشتر از یک یا دو کیلوبایت باید در یک فایل ذخیره شوند و با استفاده از یک کلید در رجیستری به آن فایل رجوع کرد. برای حفظ فضای ذخیره سازی باید دادههای شبیه به هم در یک ساختار جمع آوری گردند و ساختار را به عنوان یک مقدار ذخیره کرد؛ به جای آن که هر عضو ساختار را به عنوان یک کلید ذخیره کرد. ذخیره سازی اطلاعات به صورت باینری این امکان را میدهد که اطلاعات را در یک مقدار ذخیره کنید.
اطلاعات رجیستری در پیج فایل ( Page File ) ذخیره میشوند. پیج فایل ناحیهای از حافظه RAM است که میتواند در زمانی که استفاده نمیشود به Hard منتقل شود. اندازهی پیج فایل به وسیلهی مقدار PagedPoolSize در کلید HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management مطابق با جدول زیر تنظیم میگردد.
مقدار | توضیحات |
0×00000000 | سیستم یک مقدار بهینه را تعیین میکند |
0x1–0x20000000 | یک اندازه مشخص برحسب بایت که در این بازه باشد |
0xFFFFFFFF | سیستم بیشترین مقدار ممکن را تشخیص میدهد |
کلیدهای از پیش تعریف شده
یک برنامه قبل از آن که اطلاعاتی را در رجیستری درج کند باید یک کلید را باز کند. برای باز کردن یک کلید میتوان از سایر کلیدهایی که باز هستند، استفاده کرد. سیستم کلیدهایی را از پیش تعریف کرده که همیشه باز هستند. در ادامه کلیدهای از پیش تعریف شده را قدری بررسی میکنیم.
HKEY_CLASSES_ROOT
زیرشاخههای این کلید، انواع اسناد و خصوصیات مربوط به آنها را مشخص میکنند. این شاخه نباید در یک سرویس یا برنامهای که کاربران متعدد دارد، مورد استفاده قرار گیرد.
HKEY_CURRENT_USER
زیرشاخههای این کلید، تنظیمات مربوط به کاربر جاری را مشخص میکنند. این تنظیمات شامل متغیرهای محیطی، اطلاعات دربارهی برنامهها، رنگها، پرینترها، ارتباطات شبکه و تنظیمات برنامههاست. به طور مثال مایکروسافت اطلاعات مربوط به برنامههای خود را در کلید HKEY_CURRENT_USER\Software\Microsoft ذخیره میکند. هر کدام از برنامهها یک زیرکلید در کلید مزبور را به خود اختصاص دادهاند. این شاخه نیز نباید در یک سرویس یا برنامهای که کاربران متعدد دارد، مورد استفاده قرار گیرد.
HKEY_LOCAL_MACHINE
زیرشاخههای این کلید، وضعیت فیزیکی کامپیوتر را مشخص میکنند که شامل حافظهی سیستم، سختافزار و نرمافزارهای نصب شده بر روی سیستم، اطلاعات پیکربندی، تنظیمات ورود به سیستم، اطلاعات امنیتی شبکه و اطلاعات دیگر سیستم است.
HKEY_USERS
زیرشاخههای این کلید، پیکربندی کاربران پیش فرض، جدید، جاری سیستم و به طور کلی همهی کاربران را مشخص میکند.
HKEY_CURRENT_CONFIG
زیرشاخههای این کلید، اطلاعاتی درباره وضعیت سختافزار کامپیوتر در اختیار ما میگذارند. در واقع این کلید نام مستعاری برای کلید HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Hardware Profiles\Current است که در ویندوزهای قبل از ۳.۵۱ NT وجود نداشته است.
کندوهای رجیستری
یک کندو ( Hive ) یک گروه از کلیدها، زیرکلیدها و مقادیر در رجیستری است که یک مجموعه از فایلهای پشتیبان را به همراه دارد. در هنگام بوت ویندوز، اطلاعات از این فایلها استخراج میشوند. شما هم چنین میتوانید با استفاده از Import در منوی فایل registry editor به صورت دستی این کار را انجام دهید. زمانی که ویندوز را خاموش میکنید، اطلاعات کندوها در فایلهای پشتیبان نوشته میشوند. شما میتوانید این کار را به طور دستی با Export در منوی فایل registry editor نیز انجام دهید.
فایلهای پشتیبان همه کندوها به جز HKEY_CURRENT_USER در شاخهی Windows Root\System32\config قرار دارند. فایلهای پشتیبان HKEY_CURRENT_USER در شاخهی System Root\Documents and Settings\Username قرار دارند. پسوند فایلها در این شاخهها، نوع دادههایی که در بر دارند را نشان میدهند. در جدول زیر برخی کندوها و فایلهای پشتیبانشان آمده است.
کندوی رجیستری | فایلهای پشتیبان |
HKEY_CURRENT_CONFIG | System, System.alt, System.log, System.sav |
HKEY_CURRENT_USER | Ntuser.dat, Ntuser.dat.log |
HKEY_LOCAL_MACHINE\SAM | Sam, Sam.log, Sam.sav |
HKEY_LOCAL_MACHINE\Security | Security, Security.log, Security.sav |
HKEY_LOCAL_MACHINE\Software | Software, Software.log, Software.sav |
HKEY_LOCAL_MACHINE\System | System, System.alt, System.log, System.sav |
HKEY_USERS\.DEFAULT | Default, Default.log, Default.sav |
دسته بندی اطلاعات
قبل از قرار دادن اطلاعات در رجیستری باید آنها را به دو دسته اطلاعات کامپیوتر و اطلاعات کاربر تقسیم کرد. با این تقسیم بندی، چندین کاربر میتوانند از یک برنامه استفاده کنند و یا اطلاعات را بر روی شبکه قرار دهند. زمانی که یک برنامه نصب میشود، باید اطلاعات کامپیوتری خود را در شاخه فرضی HKEY_LOCAL_MACHINE\Software\MyCompany\MyProduct\1.0 به گونهای تعریف کند که نام شرکت، نام محصول و نسخه برنامه به خوبی مشخص گردند و هم چنین اطلاعات مربوط به کاربران را در شاخه فرضی HKEY_CURRENT_USER\Software\MyCompany\MyProduct\1.0 نگاه دارد.
باز کردن، ساختن و بستن کلیدها
قبل از آن که بتوانیم یک اطلاعات را در رجیستری درج کنیم، باید یک کلید بسازیم و یا یک کلید موجود را باز کنیم. یک برنامه همیشه به یک کلید به عنوان زیرکلیدی از یک کلید باز رجوع میکند. کلیدهای از پیش تعریف شده همیشه باز هستند.
کلاسهای تعریف شده برای کار با رجیستری در فضانام Microsoft.Win32 قرار دارند. کلاس Microsoft.Win32.Registry مربوط به کلاسهای از پیش تعریف شده و کلاس Microsoft.Win32.RegistryKey برای کار با رجیستری است. برای باز کردن یک کلید از متد RegistryKey.OpenSubKey استفاده میکنیم. به یاد داشته باشید که کلیدهای از پیش تعریف شده همیشه باز هستند و نیازی به باز کردن ندارند. برای ساختن یک کلید از متد RegistryKey.CreateSubKey استفاده میکنیم. دقت کنید زیرکلیدی که میخواهید بسازید، باید به یک کلید باز رجوع کند. برای خاتمه دسترسی به یک کلید، باید آن را ببندیم. برای بستن یک کلید از متد RegistryKey.Close استفاده میکنیم.
اکنون که با ساختار رجیستری و کلاسهای مربوطه در .NET برای کار با رجیستری آشنا شدیم، به کدنویسی میپردازیم.
ساختن یک زیرکلید جدید
برای ساختن یک زیرکلید جدید از متد RegistryKey.CreateSubKey به صورت زیر استفاده میکنیم.
public RegistryKey CreateSubKey( string subkey);
subkey نام و مسیر کلیدی که میخواهید بسازید را مشخص میکند که معمولا به فرم فرضی key name\Company Name\Application Name\version است. این متد یک زیرکلید را برمیگرداند و در صورت بروز خطا مقدار null را برمیگرداند و یک exception را فرا میخواند. خطا به دلایلی چون عدم داشتن مجوز، وجود نداشتن مسیر درخواستی و غیره رخ میدهد. برای بررسی exception ها میتوانید از بلوک try-catch استفاده کنید.
RegistryKey MyReg = Registry .CurrentUser.CreateSubKey( "SOFTWARE\\SomeCompany\\SomeApp\\SomeVer" );
برای دست یابی به کلیدهای از پیش تعریف شده از کلاس Registry مطابق جدول زیر استفاده میکنیم.
فیلد | کلید |
ClassesRoot | HKEY_CLASSES_ROOT |
CurrentUser | HKEY_CURRENT_USER |
LocalMachine | HKEY_LOCAL_MACHINE |
Users | HKEY_USERS |
CurrentConfig | HKEY_CURRENT_CONFIG |
باز کردن زیرکلید موجود
برای باز کردن یک زیرکلید موجود از متد RegistryKey.OpenSubKey به دو صورت استفاده میکنیم.
public RegistryKey OpenSubKey( string name); public RegistryKey OpenSubKey( string name, bool writable);
RegistryKey MyReg = Registry .CurrentUser.OpenSubKey( "SOFTWARE\\SomeCompany\\SomeApp\\SomeVer" , true );
مثال فوق کلید مشخص شده را در شاخهی HKEY_CURRENT_USER و در حالت ویرایش باز میکند.
خواندن اطلاعات از رجیستری
اگر یک شیء RegistryKey سالم داشته باشید میتوانید به مقادیر و اطلاعات درون مقادیر آن دسترسی داشته باشید. برای دست یابی به اطلاعات درون یک مقدار مشخص در کلید از متد RegistryKey.GetValue به دو صورت استفاده کنیم.
public object GetValue( string name); public object GetValue( string name, object defaultValue);
نوشتن اطلاعات در رجیستری
برای نوشتن اطلاعات در یک مقدار از متد RegistryKey.SetValue به صورت زیر استفاده میکنیم.
public void SetValue( string name, object value);
بستن یک کلید
زمانی که دیگر با کلید کاری ندارید و میخواهید تغییرات در رجیستری ثبت گردد باید فرآیندی به نام flushing را انجام دهید. برای انجام این کار به راحتی از متد RegistryKey.Close استفاده کنید.
RegistryKey MyReg = Registry .CurrentUser.CreateSubKey( "SOFTWARE\\SomeCompany\\SomeApp\\SomeVer" ); int nSomeVal = ( int )MyReg.GetValue( "SomeVal" , 0); MyReg.SetValue( "SomeValue" , nSomeVal + 1); MyReg.Close();
پاک کردن یک کلید
برای پاک کردن یک زیرکلید از متد RegistryKey.DeleteSubKey به دو صورت استفاده میکنیم.
public void DeleteSubKey( string subkey); public void DeleteSubKey( string subkey, bool throwOnMissingSubKey);
پاک کردن کل یک درخت
برای پاک کردن کل یک درخت با همهی کلیدهای فرزند و مقادیر آنها از متد RegistryKey.DeleteSubKeyTree به دو صورت استفاده میکنیم.
public void DeleteSubKeyTree( string subkey); public void DeleteSubKeyTree( string subkey, bool throwOnMissingSubKey);
پاک کردن یک مقدار
برای پاک کردن یک مقدار از متد RegistryKey.DeleteValue به دو صورت زیر استفاده میکنیم.
public void DeleteValue( string name); public void DeleteValue( string name, bool throwOnMissingValue);
لیست کردن زیرکلیدها
برای به دست آوردن یک لیست از همه زیرکلیدهای یک شیء RegistryKey از متد RegistryKey.GetSubKeyNames به صورت زیر استفاده میکنیم که یک آرایه رشتهای از نام زیرکلیدها را برمیگرداند.
public string [] GetSubKeyNames();
لیست کردن نام مقادیر
برای به دست آوردن یک لیست از همه مقادیری که در یک شیء RegistryKey وجود دارند از متد RegistryKey.GetValueNames به صورت زیر استفاده میکنیم که یک آرایه رشتهای از نام مقادیر را برمیگرداند.
public string [] GetSubKeyNames();
ثبت تغییرات به صورت دستی
برای ثبت تغییرات یا به اصطلاح فلاش کردن به صورت دستی میتوانید از متد RegistryKey.Flush به صورت زیر استفاده نمایید. زمانی که از RegistryKey.Close استفاده میکنید فرآیند فلاش کردن به طور اتوماتیک انجام میگیرد.
public void Flush();
تنظیم Ember data برای کار با سرور
Ember data به صورت پیش فرض و در پشت صحنه با استفاده از Ajax برای کار با یک REST Web Service طراحی شدهاست و کلیه تبادلات آن نیز با فرمت JSON انجام میشود. بنابراین تمام کدهای سمت کاربر قسمت قبل نیز در این حالت کار خواهند کرد. تنها کاری که باید انجام شود، حذف تنظیمات ابتدایی آن برای کار با HTML 5 local storage است.
برای این منظور ابتدا فایل index.html را گشوده و سپس مدخل localstorage_adapter.js را از آن حذف کنید:
<!--<script src="Scripts/Libs/localstorage_adapter.js" type="text/javascript"></script>-->
<!--<script src="Scripts/App/store.js" type="text/javascript"></script>-->
اکنون برنامه را اجرا کنید، چنین پیام خطایی را مشاهده خواهید کرد:
همانطور که عنوان شد، ember data به صورت پیش فرض با سرور کار میکند و در اینجا به صورت خودکار، یک درخواست Get را به آدرس http://localhost:25918/posts جهت دریافت آخرین مطالب ثبت شده، ارسال کردهاست و چون هنوز وب سرویسی در برنامه تعریف نشده، با خطای 404 و یا یافت نشد، مواجه شدهاست.
این درخواست نیز بر اساس تعاریف موجود در فایل Scripts\Routes\posts.js، به سرور ارسال شدهاست:
Blogger.PostsRoute = Ember.Route.extend({ model: function () { return this.store.find('post'); } });
تغییر تنظیمات پیش فرض آغازین Ember data
آدرس درخواستی http://localhost:25918/posts به این معنا است که کلیه درخواستها، به همان آدرس و پورت ریشهی اصلی سایت ارسال میشوند. اما اگر یک ASP.NET Web API Controller را تعریف کنیم، نیاز است این درخواستها، برای مثال به آدرس api/posts ارسال شوند؛ بجای /posts.
برای این منظور پوشهی جدید Scripts\Adapters را ایجاد کرده و فایل web_api_adapter.js را با این محتوا به آن اضافه کنید:
DS.RESTAdapter.reopen({ namespace: 'api' });
<script src="Scripts/Adapters/web_api_adapter.js" type="text/javascript"></script>
تغییر تنظیمات پیش فرض ASP.NET Web API
در سمت سرور، بنابر اصول نامگذاری خواص، نامها با حروف بزرگ شروع میشوند:
namespace EmberJS03.Models { public class Post { public int Id { set; get; } public string Title { set; get; } public string Body { set; get; } } }
using System; using System.Web.Http; using System.Web.Routing; using Newtonsoft.Json.Serialization; namespace EmberJS03 { public class Global : System.Web.HttpApplication { protected void Application_Start(object sender, EventArgs e) { RouteTable.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); var settings = GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings; settings.ContractResolver = new CamelCasePropertyNamesContractResolver(); } } }
نحوهی صحیح بازگشت اطلاعات از یک ASP.NET Web API جهت استفاده در Ember data
با تنظیمات فوق، اگر کنترلر جدیدی را به صورت ذیل جهت بازگشت لیست مطالب تهیه کنیم:
namespace EmberJS03.Controllers { public class PostsController : ApiController { public IEnumerable<Post> Get() { return DataSource.PostsList; } } }
WARNING: Encountered "0" in payload, but no model was found for model name "0" (resolved model name using DS.RESTSerializer.typeForRoot("0"))
http://jsonapi.codeplex.com
https://github.com/xqiu/MVCSPAWithEmberjs
https://github.com/rmichela/EmberDataAdapter
https://github.com/MilkyWayJoe/Ember-WebAPI-Adapter
http://blog.yodersolutions.com/using-ember-data-with-asp-net-web-api
http://emadibrahim.com/2014/04/09/emberjs-and-asp-net-web-api-and-json-serialization
و خلاصهی آنها به این صورت است:
خروجی JSON تولیدی توسط ASP.NET Web API چنین شکلی را دارد:
[ { Id: 1, Title: 'First Post' }, { Id: 2, Title: 'Second Post' } ]
{ posts: [{ id: 1, title: 'First Post' }, { id: 2, title: 'Second Post' }] }
using System.Web.Http; using EmberJS03.Models; namespace EmberJS03.Controllers { public class PostsController : ApiController { public object Get() { return new { posts = DataSource.PostsList }; } } }
اکنون اگر برنامه را اجرا کنید، در صفحهی اول آن، لیست عناوین مطالب را مشاهده خواهید کرد.
تاثیر قرارداد JSON API در حین ارسال اطلاعات به سرور توسط Ember data
در تکمیل کنترلرهای Web API مورد نیاز (کنترلرهای مطالب و نظرات)، نیاز به متدهای Post، Update و Delete هم خواهد بود. دقیقا فرامین ارسالی توسط Ember data توسط همین HTTP Verbs به سمت سرور ارسال میشوند. در این حالت اگر متد Post کنترلر نظرات را به این شکل طراحی کنیم:
public HttpResponseMessage Post(Comment comment)
{"comment":{"text":"data...","post":"3"}}
برای پردازش آن، یا باید از راه حلهای ثالث مطرح شده در ابتدای بحث استفاده کنید و یا میتوان مطابق کدهای ذیل، کل اطلاعات JSON ارسالی را توسط کتابخانهی JSON.NET نیز پردازش کرد:
namespace EmberJS03.Controllers { public class CommentsController : ApiController { public HttpResponseMessage Post(HttpRequestMessage requestMessage) { var jsonContent = requestMessage.Content.ReadAsStringAsync().Result; // {"comment":{"text":"data...","post":"3"}} var jObj = JObject.Parse(jsonContent); var comment = jObj.SelectToken("comment", false).ToObject<Comment>(); var id = 1; var lastItem = DataSource.CommentsList.LastOrDefault(); if (lastItem != null) { id = lastItem.Id + 1; } comment.Id = id; DataSource.CommentsList.Add(comment); // ارسال آی دی با فرمت خاص مهم است return Request.CreateResponse(HttpStatusCode.Created, new { comment = comment }); } } }
همچنین فرمت return نهایی هم مهم است. در این حالت خروجی ارسالی به سمت کاربر، باید مجددا با فرمت JSON API باشد؛ یعنی باید comment اصلاح شده را به همراه ریشهی comment ارسال کرد. در اینجا نیز anonymous object تهیه شده، چنین کاری را انجام میدهد.
Lazy loading در Ember data
تا اینجا اگر برنامه را اجرا کنید، لیست مطالب صفحهی اول را مشاهده خواهید کرد، اما لیست نظرات آنها را خیر؛ از این جهت که ضرورتی نداشت تا در بار اول ارسال لیست مطالب به سمت کاربر، تمام نظرات متناظر با آنها را هم ارسال کرد. بهتر است زمانیکه کاربر یک مطلب خاص را مشاهده میکند، نظرات خاص آنرا به سمت کاربر ارسال کنیم.
در تعاریف سمت کاربر Ember data، پارامتر دوم رابطهی hasMany که با async:true مشخص شدهاست، دقیقا معنای lazy loading را دارد.
Blogger.Post = DS.Model.extend({ title: DS.attr(), body: DS.attr(), comments: DS.hasMany('comment', { async: true } /* lazy loading */) });
الف) Idهای نظرات هر مطلب را به صورت یک آرایه، در بار اول ارسال لیست نظرات به سمت کاربر، تهیه و ارسال کنیم:
namespace EmberJS03.Models { public class Post { public int Id { set; get; } public string Title { set; get; } public string Body { set; get; } // lazy loading via an array of IDs public int[] Comments { set; get; } } }
ب) این روش به علت تعداد رفت و برگشت بیش از حد به سرور، کارآیی آنچنانی ندارد. بهتر است جهت مشاهدهی جزئیات یک مطلب، تنها یکبار درخواست Get کلیه نظرات آن صادر شود.
برای اینکار باید مدل برنامه را به شکل زیر تغییر دهیم:
namespace EmberJS03.Models { public class Post { public int Id { set; get; } public string Title { set; get; } public string Body { set; get; } // load related models via URLs instead of an array of IDs // ref. https://github.com/emberjs/data/pull/1371 public object Links { set; get; } public Post() { Links = new { comments = "comments" }; // api/posts/id/comments } } }
namespace EmberJS03 { public class Global : System.Web.HttpApplication { protected void Application_Start(object sender, EventArgs e) { RouteTable.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}/{name}", defaults: new { id = RouteParameter.Optional, name = RouteParameter.Optional } ); var settings = GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings; settings.ContractResolver = new CamelCasePropertyNamesContractResolver(); } } }
در این حالت در طی یک درخواست میتوان کلیه نظرات را به سمت کاربر ارسال کرد. در اینجا نیز ذکر ریشهی comments همانند ریشه posts، الزامی است:
namespace EmberJS03.Controllers { public class PostsController : ApiController { //GET api/posts/id public object Get(int id) { return new { posts = DataSource.PostsList.FirstOrDefault(post => post.Id == id), comments = DataSource.CommentsList.Where(comment => comment.Post == id).ToList() }; } } }
پردازشهای async و متد transitionToRoute در Ember.js
اگر متد حذف مطالب را نیز به کنترلر Posts اضافه کنیم:
namespace EmberJS03.Controllers { public class PostsController : ApiController { public HttpResponseMessage Delete(int id) { var item = DataSource.PostsList.FirstOrDefault(x => x.Id == id); if (item == null) return Request.CreateResponse(HttpStatusCode.NotFound); DataSource.PostsList.Remove(item); //حذف کامنتهای مرتبط var relatedComments = DataSource.CommentsList.Where(comment => comment.Post == id).ToList(); relatedComments.ForEach(comment => DataSource.CommentsList.Remove(comment)); return Request.CreateResponse(HttpStatusCode.OK, new { post = item }); } } }
Attempted to handle event `pushedData` on while in state root.deleted.inFlight.
Blogger.PostController = Ember.ObjectController.extend({ isEditing: false, actions: { edit: function () { this.set('isEditing', true); }, save: function () { var post = this.get('model'); post.save(); this.set('isEditing', false); }, delete: function () { if (confirm('Do you want to delete this post?')) { var thisController = this; var post = this.get('model'); post.destroyRecord().then(function () { thisController.transitionToRoute('posts'); }); } } } });
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
EmberJS03_05.zip
IIS Hosting:
if (request.Properties.ContainsKey["MS_HttpContext"]) { var ctx = request.Properties["MS_HttpContext"] as HttpContextWrapper; if (ctx != null) { var ip = ctx.Request.UserHostAddress; } }
public static class HttpRequestMessageExtensions { private const string HttpContext = "MS_HttpContext"; public static string GetClientIpAddress(this HttpRequestMessage request) { if (request.Properties.ContainsKey(HttpContext)) { dynamic ctx = request.Properties[HttpContext]; if (ctx != null) { return ctx.Request.UserHostAddress; } } return null; } }
Self Hosting:
if (request.Properties.ContainsKey[RemoteEndpointMessageProperty.Name]) { var remote = request.Properties[RemoteEndpointMessageProperty.Name] as RemoteEndpointMessageProperty; if (remote != null) { var ip = remote.Address; } }
ترکیب حالتهای قبلی:
»در این صورت دیگر نیازی به اضافه کردن اسمبلی System.ServiceModel نیست.
public static class HttpRequestMessageExtensions { private const string HttpContext = "MS_HttpContext"; private const string RemoteEndpointMessage = "System.ServiceModel.Channels.RemoteEndpointMessageProperty"; public static string GetClientIpAddress(this HttpRequestMessage request) { if (request.Properties.ContainsKey(HttpContext)) { dynamic ctx = request.Properties[HttpContext]; if (ctx != null) { return ctx.Request.UserHostAddress; } } if (request.Properties.ContainsKey(RemoteEndpointMessage)) { dynamic remoteEndpoint = request.Properties[RemoteEndpointMessage]; if (remoteEndpoint != null) { return remoteEndpoint.Address; } } return null; } }
public class MyHandler : DelegatingHandler { private readonly HashSet<string> deniedIps; protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { if (deniedIps.Contains(request.GetClientIpAddress())) { return Task.FromResult( new HttpResponseMessage( HttpStatusCode.Unauthorized ) ); } return base.SendAsync(request, cancellationToken); } }
Owin :
زمانی که از Owin برای هاست سرویسهای Web Api خود استفاده میکنید کمی روال انجام کار متفاوت خواهد شد. در این مورد نیز میتوانید از DelegatingHandlerها استفاده کنید. معرفی DelegatingHandler طراحی شده به Asp.Net PipeLine به صورت زیر خواهد بود:
public class Startup { public void Configuration( IAppBuilder appBuilder ) { var config = new HttpConfiguration(); var routeHandler = HttpClientFactory.CreatePipeline( new HttpControllerDispatcher( config ), new DelegatingHandler[] { new MyHandler(), } ); config.Routes.MapHttpRoute( name: "Default", routeTemplate: "{controller}/{action}", defaults: null, constraints: null, handler: routeHandler ); config.EnsureInitialized(); appBuilder.UseWebApi( config ); } }
public class IpMiddleware : OwinMiddleware { private readonly HashSet<string> _deniedIps; public IpMiddleware(OwinMiddleware next, HashSet<string> deniedIps) : base(next) { _deniedIps = deniedIps; } public override async Task Invoke(OwinRequest request, OwinResponse response) { var ipAddress = (string)request.Environment["server.RemoteIpAddress"]; if (_deniedIps.Contains(ipAddress)) { response.StatusCode = 403; return; } await Next.Invoke(request, response); } }
در نهایت برای معرفی این Middleware طراحی شده به Application، مراحل زیر را انجام دهید.
public class Startup { public void Configuration( IAppBuilder appBuilder ) { var config = new HttpConfiguration(); var deniedIps = new HashSet<string> {"192.168.0.100", "192.168.0.101"};
app.Use(typeof(IpMiddleware), deniedIps); appBuilder.UseWebApi( config ); } }
تعریف مقدماتی fluent interface در ویکی پدیا به شرح زیر است: (+)
In software engineering, a fluent interface (as first coined by Eric Evans and Martin Fowler) is a way of implementing an object oriented API in a way that aims to provide for more readable code.
به صورت خلاصه هدف آن فراهم آوردن روشی است که بتوان متدها را زنجیر وار فراخوانی کرد و به این ترتیب خوانایی کد نوشته شده را بالا برد. پیاده سازی آن هم شامل دو نکته است:
الف) نوع متد تعریف شده باید مساوی با نام کلاس جاری باشد.
ب) در این حالت خروجی متدهای ما کلمه کلیدی this خواهند بود.
برای مثال:
using System;
namespace FluentInt
{
public class FluentApiTest
{
private int _val;
public FluentApiTest Number(int val)
{
_val = val;
return this;
}
public FluentApiTest Abs()
{
_val = Math.Abs(_val);
return this;
}
public bool IsEqualTo(int val)
{
return val == _val;
}
}
}
if (new FluentApiTest().Number(-10).Abs().IsEqualTo(10))
{
Console.WriteLine("Abs(-10)==10");
}
خوب! این مطلبی است که همه جا پیدا میکنید و مطلب جدیدی هم نیست. اما موردی را که سخت میشود یافت این است که طراحی کلاس فوق ایراد دارد. برای مثال شما میتوانید ترکیبهای زیر را هم تشکیل دهید و کار میکند؛ یا به عبارتی برنامه کامپایل میشود و این خوب نیست:
if(new FluentApiTest().Abs().Number(-10).IsEqualTo(10)) ...
if (new FluentApiTest().Abs().IsEqualTo(10)) ...
ولی ... این روش هم صحیح نیست. از ابتدای کار نباید بتوان متد بیربطی را در طول این زنجیره مشاهده کرد. اگر قرار نیست استفاده گردد، نباید هم در intellisense ظاهر شود و پس از آن هم نباید قابل کامپایل باشد.
بنابراین صورت مساله به این ترتیب اصلاح میشود:
میخواهیم پس از نوشتن FluentApiTest و قرار دادن یک نقطه، در intellisense فقط Number ظاهر شود و نه هیچ متد دیگری. پس از ذکر متد Number فقط متد Abs یا مواردی شبیه به آن مانند Sqrt ظاهر شوند. پس از انتخاب مثلا Abs آنگاه متد IsEqualTo توسط Intellisense قابل دسترسی باشد. در روش اول فوق، به صورت دوستانه همه چیز در دسترس است و هر ترکیب قابل کامپایلی را میشود با متدها ساخت که این مورد نظر ما نیست.
اینبار پیاده سازی اولیه به شرح زیر تغییر خواهد کرد:
using System;
namespace FluentInt
{
public class FluentApiTest
{
public MathMethods<FluentApiTest> Number(int val)
{
return new MathMethods<FluentApiTest>(this, val);
}
}
public class MathMethods<TParent>
{
private int _val;
private readonly TParent _parent;
public MathMethods(TParent parent, int val)
{
_val = val;
_parent = parent;
}
public Restrictions<MathMethods<TParent>> Abs()
{
_val = Math.Abs(_val);
return new Restrictions<MathMethods<TParent>>(this, _val);
}
}
public class Restrictions<TParent>
{
private readonly int _val;
private readonly TParent _parent;
public Restrictions(TParent parent, int val)
{
_val = val;
_parent = parent;
}
public bool IsEqualTo(int val)
{
return _val == val;
}
}
}
if (new FluentApiTest().Number(-10).Abs().IsEqualTo(10))
{
Console.WriteLine("Abs(-10)==10");
}
در پیاده سازی کلاس MathMethods از Generics استفاده شده به این جهت که بتوان نوع متد Number را بر همین اساس تعیین کرد تا متدهای کلاس MathMethods در Intellisense (یا به قولی در طول زنجیره مورد نظر) ظاهر شوند. کلاس Restrictions نیز به همین ترتیب معرفی شده است و از آن جهت تعریف نوع متد Abs استفاده کردیم. هر کلاس جدید در طول زنجیره، توسط سازنده خود به وهلهای از کلاس قبلی به همراه مقادیر پاس شده دسترسی خواهد داشت. به این ترتیب زنجیرهای را تشکیل دادهایم که سازمان یافته است و نمیتوان در آن متدی را بیجهت پیش یا پس از دیگری صدا زد و همچنین دیگر نیازی به بررسی نحوهی فراخوانیهای یک مصرف کننده نیز نخواهد بود زیرا برنامه کامپایل نمیشود.
فرض کنید قصد دارید برای کتابخانهای که در حال طراحی هستید، تنظیماتی را از استفاده کننده دریافت کنید؛ مثلا از طریق کلاسی شبیه به تعریف SmtpConfig مطلب فوق.
برای اینکار، ابتدا یک متد الحاقی را تعریف میکنیم که IServiceCollection را به همراه SmtpConfig دریافت کند:
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; namespace SampleLib { public static class MyServiceCollectionExtensions { public static void AddMySampleLib(this IServiceCollection services, SmtpConfig configureOptions) { services.TryAddSingleton(Options.Create(configureOptions)); } } }
استفاده کنند، تنها نیاز دارد تا این تنظیمات را به صورت ذیل در کلاس آغازین برنامه وارد کند:
public void ConfigureServices(IServiceCollection services) { services.AddMySampleLib(new SmtpConfig { /*...*/ });
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Security.Cryptography; using System.Text; using System.Web.Mvc; using System.Web.Security; namespace OpenIDExample.Models { #region Models public class ChangePasswordModel { [Required] [DataType(DataType.Password)] [Display(Name = "Current password")] public string OldPassword { get; set; } [Required] [ValidatePasswordLength] [DataType(DataType.Password)] [Display(Name = "New password")] public string NewPassword { get; set; } [DataType(DataType.Password)] [Display(Name = "Confirm new password")] [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] public string ConfirmPassword { get; set; } } public class LogOnModel { [Display(Name = "OpenID")] public string OpenID { get; set; } [Required] [Display(Name = "User name")] public string UserName { get; set; } [Required] [DataType(DataType.Password)] [Display(Name = "Password")] public string Password { get; set; } [Display(Name = "Remember me?")] public bool RememberMe { get; set; } } public class RegisterModel { [Display(Name = "OpenID")] public string OpenID { get; set; } [Required] [Display(Name = "User name")] public string UserName { get; set; } [Required] [DataType(DataType.EmailAddress)] [Display(Name = "Email address")] public string Email { get; set; } [Required] [ValidatePasswordLength] [DataType(DataType.Password)] [Display(Name = "Password")] public string Password { get; set; } [DataType(DataType.Password)] [Display(Name = "Confirm password")] [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] public string ConfirmPassword { get; set; } } #endregion Models #region Services // The FormsAuthentication type is sealed and contains static members, so it is difficult to // unit test code that calls its members. The interface and helper class below demonstrate // how to create an abstract wrapper around such a type in order to make the AccountController // code unit testable. public interface IMembershipService { int MinPasswordLength { get; } bool ValidateUser(string userName, string password); MembershipCreateStatus CreateUser(string userName, string password, string email, string OpenID); bool ChangePassword(string userName, string oldPassword, string newPassword); MembershipUser GetUser(string OpenID); } public class AccountMembershipService : IMembershipService { private readonly MembershipProvider _provider; public AccountMembershipService() : this(null) { } public AccountMembershipService(MembershipProvider provider) { _provider = provider ?? Membership.Provider; } public int MinPasswordLength { get { return _provider.MinRequiredPasswordLength; } } public bool ValidateUser(string userName, string password) { if (String.IsNullOrEmpty(userName)) throw new ArgumentException("Value cannot be null or empty.", "userName"); if (String.IsNullOrEmpty(password)) throw new ArgumentException("Value cannot be null or empty.", "password"); return _provider.ValidateUser(userName, password); } public Guid StringToGUID(string value) { // Create a new instance of the MD5CryptoServiceProvider object. MD5 md5Hasher = MD5.Create(); // Convert the input string to a byte array and compute the hash. byte[] data = md5Hasher.ComputeHash(Encoding.Default.GetBytes(value)); return new Guid(data); } public MembershipCreateStatus CreateUser(string userName, string password, string email, string OpenID) { if (String.IsNullOrEmpty(userName)) throw new ArgumentException("Value cannot be null or empty.", "userName"); if (String.IsNullOrEmpty(password)) throw new ArgumentException("Value cannot be null or empty.", "password"); if (String.IsNullOrEmpty(email)) throw new ArgumentException("Value cannot be null or empty.", "email"); MembershipCreateStatus status; _provider.CreateUser(userName, password, email, null, null, true, StringToGUID(OpenID), out status); return status; } public MembershipUser GetUser(string OpenID) { return _provider.GetUser(StringToGUID(OpenID), true); } public bool ChangePassword(string userName, string oldPassword, string newPassword) { if (String.IsNullOrEmpty(userName)) throw new ArgumentException("Value cannot be null or empty.", "userName"); if (String.IsNullOrEmpty(oldPassword)) throw new ArgumentException("Value cannot be null or empty.", "oldPassword"); if (String.IsNullOrEmpty(newPassword)) throw new ArgumentException("Value cannot be null or empty.", "newPassword"); // The underlying ChangePassword() will throw an exception rather // than return false in certain failure scenarios. try { MembershipUser currentUser = _provider.GetUser(userName, true /* userIsOnline */); return currentUser.ChangePassword(oldPassword, newPassword); } catch (ArgumentException) { return false; } catch (MembershipPasswordException) { return false; } } public MembershipCreateStatus CreateUser(string userName, string password, string email) { throw new NotImplementedException(); } } public interface IFormsAuthenticationService { void SignIn(string userName, bool createPersistentCookie); void SignOut(); } public class FormsAuthenticationService : IFormsAuthenticationService { public void SignIn(string userName, bool createPersistentCookie) { if (String.IsNullOrEmpty(userName)) throw new ArgumentException("Value cannot be null or empty.", "userName"); FormsAuthentication.SetAuthCookie(userName, createPersistentCookie); } public void SignOut() { FormsAuthentication.SignOut(); } } #endregion Services #region Validation public static class AccountValidation { public static string ErrorCodeToString(MembershipCreateStatus createStatus) { // See http://go.microsoft.com/fwlink/?LinkID=177550 for // a full list of status codes. switch (createStatus) { case MembershipCreateStatus.DuplicateUserName: return "Username already exists. Please enter a different user name."; case MembershipCreateStatus.DuplicateEmail: return "A username for that e-mail address already exists. Please enter a different e-mail address."; case MembershipCreateStatus.InvalidPassword: return "The password provided is invalid. Please enter a valid password value."; case MembershipCreateStatus.InvalidEmail: return "The e-mail address provided is invalid. Please check the value and try again."; case MembershipCreateStatus.InvalidAnswer: return "The password retrieval answer provided is invalid. Please check the value and try again."; case MembershipCreateStatus.InvalidQuestion: return "The password retrieval question provided is invalid. Please check the value and try again."; case MembershipCreateStatus.InvalidUserName: return "The user name provided is invalid. Please check the value and try again."; case MembershipCreateStatus.ProviderError: return "The authentication provider returned an error. Please verify your entry and try again. If the problem persists, please contact your system administrator."; case MembershipCreateStatus.UserRejected: return "The user creation request has been canceled. Please verify your entry and try again. If the problem persists, please contact your system administrator."; default: return "An unknown error occurred. Please verify your entry and try again. If the problem persists, please contact your system administrator."; } } } [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] public sealed class ValidatePasswordLengthAttribute : ValidationAttribute, IClientValidatable { private const string _defaultErrorMessage = "'{0}' must be at least {1} characters long."; private readonly int _minCharacters = Membership.Provider.MinRequiredPasswordLength; public ValidatePasswordLengthAttribute() : base(_defaultErrorMessage) { } public override string FormatErrorMessage(string name) { return String.Format(CultureInfo.CurrentCulture, ErrorMessageString, name, _minCharacters); } public override bool IsValid(object value) { string valueAsString = value as string; return (valueAsString != null && valueAsString.Length >= _minCharacters); } public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context) { return new[]{ new ModelClientValidationStringLengthRule(FormatErrorMessage(metadata.GetDisplayName()), _minCharacters, int.MaxValue) }; } } #endregion Validation }
using System.Web.Mvc; using System.Web.Routing; using System.Web.Security; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.OpenId; using DotNetOpenAuth.OpenId.RelyingParty; using OpenIDExample.Models; namespace OpenIDExample.Controllers { public class AccountController : Controller { private static OpenIdRelyingParty openid = new OpenIdRelyingParty(); public IFormsAuthenticationService FormsService { get; set; } public IMembershipService MembershipService { get; set; } protected override void Initialize(RequestContext requestContext) { if (FormsService == null) { FormsService = new FormsAuthenticationService(); } if (MembershipService == null) { MembershipService = new AccountMembershipService(); } base.Initialize(requestContext); } // ************************************** // URL: /Account/LogOn // ************************************** public ActionResult LogOn() { return View(); } [HttpPost] public ActionResult LogOn(LogOnModel model, string returnUrl) { if (ModelState.IsValid) { if (MembershipService.ValidateUser(model.UserName, model.Password)) { FormsService.SignIn(model.UserName, model.RememberMe); if (Url.IsLocalUrl(returnUrl)) { return Redirect(returnUrl); } else { return RedirectToAction("Index", "Home"); } } else { ModelState.AddModelError("", "The user name or password provided is incorrect."); } } // If we got this far, something failed, redisplay form return View(model); } // ************************************** // URL: /Account/LogOff // ************************************** public ActionResult LogOff() { FormsService.SignOut(); return RedirectToAction("Index", "Home"); } // ************************************** // URL: /Account/Register // ************************************** public ActionResult Register(string OpenID) { ViewBag.PasswordLength = MembershipService.MinPasswordLength; ViewBag.OpenID = OpenID; return View(); } [HttpPost] public ActionResult Register(RegisterModel model) { if (ModelState.IsValid) { // Attempt to register the user MembershipCreateStatus createStatus = MembershipService.CreateUser(model.UserName, model.Password, model.Email, model.OpenID); if (createStatus == MembershipCreateStatus.Success) { FormsService.SignIn(model.UserName, false /* createPersistentCookie */); return RedirectToAction("Index", "Home"); } else { ModelState.AddModelError("", AccountValidation.ErrorCodeToString(createStatus)); } } // If we got this far, something failed, redisplay form ViewBag.PasswordLength = MembershipService.MinPasswordLength; return View(model); } // ************************************** // URL: /Account/ChangePassword // ************************************** [Authorize] public ActionResult ChangePassword() { ViewBag.PasswordLength = MembershipService.MinPasswordLength; return View(); } [Authorize] [HttpPost] public ActionResult ChangePassword(ChangePasswordModel model) { if (ModelState.IsValid) { if (MembershipService.ChangePassword(User.Identity.Name, model.OldPassword, model.NewPassword)) { return RedirectToAction("ChangePasswordSuccess"); } else { ModelState.AddModelError("", "The current password is incorrect or the new password is invalid."); } } // If we got this far, something failed, redisplay form ViewBag.PasswordLength = MembershipService.MinPasswordLength; return View(model); } // ************************************** // URL: /Account/ChangePasswordSuccess // ************************************** public ActionResult ChangePasswordSuccess() { return View(); } [ValidateInput(false)] public ActionResult Authenticate(string returnUrl) { var response = openid.GetResponse(); if (response == null) { //Let us submit the request to OpenID provider Identifier id; if (Identifier.TryParse(Request.Form["openid_identifier"], out id)) { try { var request = openid.CreateRequest(Request.Form["openid_identifier"]); return request.RedirectingResponse.AsActionResult(); } catch (ProtocolException ex) { ViewBag.Message = ex.Message; return View("LogOn"); } } ViewBag.Message = "Invalid identifier"; return View("LogOn"); } //Let us check the response switch (response.Status) { case AuthenticationStatus.Authenticated: LogOnModel lm = new LogOnModel(); lm.OpenID = response.ClaimedIdentifier; //check if user exist MembershipUser user = MembershipService.GetUser(lm.OpenID); if (user != null) { lm.UserName = user.UserName; FormsService.SignIn(user.UserName, false); } return View("LogOn", lm); case AuthenticationStatus.Canceled: ViewBag.Message = "Canceled at provider"; return View("LogOn"); case AuthenticationStatus.Failed: ViewBag.Message = response.Exception.Message; return View("LogOn"); } return new EmptyResult(); } } }
6- سپس برای Action به نام LogOn یک View میسازیم، برای Authenticate نیازی به ایجاد View ندارد چون قرار است درخواست کاربر را به آدرس دیگری Redirect کند. سپس کدهای زیر را برای View ایجاد شده وارد میکنیم.
@model OpenIDExample.Models.LogOnModel @{ ViewBag.Title = "Log On"; } <h2> Log On</h2> <p> Please enter your username and password. @Html.ActionLink("Register", "Register") if you don't have an account. </p> <script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script> <form action="Authenticate?ReturnUrl=@HttpUtility.UrlEncode(Request.QueryString["ReturnUrl"])" method="post" id="openid_form"> <input type="hidden" name="action" value="verify" /> <div> <fieldset> <legend>Login using OpenID</legend> <div class="openid_choice"> <p> Please click your account provider:</p> <div id="openid_btns"> </div> </div> <div id="openid_input_area"> @Html.TextBox("openid_identifier") <input type="submit" value="Log On" /> </div> <noscript> <p> OpenID is service that allows you to log-on to many different websites using a single indentity. Find out <a href="http://openid.net/what/">more about OpenID</a> and <a href="http://openid.net/get/">how to get an OpenID enabled account</a>.</p> </noscript> <div> @if (Model != null) { if (String.IsNullOrEmpty(Model.UserName)) { <div class="editor-label"> @Html.LabelFor(model => model.OpenID) </div> <div class="editor-field"> @Html.DisplayFor(model => model.OpenID) </div> <p class="button"> @Html.ActionLink("New User ,Register", "Register", new { OpenID = Model.OpenID }) </p> } else { //user exist <p class="buttonGreen"> <a href="@Url.Action("Index", "Home")">Welcome , @Model.UserName, Continue..." </a> </p> } } </div> </fieldset> </div> </form> @Html.ValidationSummary(true, "Login was unsuccessful. Please correct the errors and try again.") @using (Html.BeginForm()) { <div> <fieldset> <legend>Or Login Normally</legend> <div class="editor-label"> @Html.LabelFor(m => m.UserName) </div> <div class="editor-field"> @Html.TextBoxFor(m => m.UserName) @Html.ValidationMessageFor(m => m.UserName) </div> <div class="editor-label"> @Html.LabelFor(m => m.Password) </div> <div class="editor-field"> @Html.PasswordFor(m => m.Password) @Html.ValidationMessageFor(m => m.Password) </div> <div class="editor-label"> @Html.CheckBoxFor(m => m.RememberMe) @Html.LabelFor(m => m.RememberMe) </div> <p> <input type="submit" value="Log On" /> </p> </fieldset> </div> }
پس از اجرای پروژه صفحه ای شبیه به پایین مشاهده کرده و سرویس دهنده OpenID خاص خود را میتوانید انتخاب نمایید.
7- برای فعال سازی عملیات احراز هویت توسط FormsAuthentication در سایت باید تنطیمات زیر را در فایل web.config انجام دهید.
<authentication mode="Forms"> <forms loginUrl="~/Account/LogOn" timeout="2880" /> </authentication>
جهت مطالعات بیشتر ودانلود نمونه کدهای آماده میتوانید به لینکهای (^ و ^ و ^ و ^ و ^ و ^ و ^ ) مراجعه کنید.
کد کامل پروژه را میتوانید از اینجا دانلود نمایید.
منبع
dotnet new api -n DotNetGraphQLClient
https://localhost:5003;http://localhost:5004
{ "Logging": { "LogLevel": { "Default": "Warning" } }, "GraphQLURI": "https://localhost:5001/graphql", "AllowedHosts": "*" }
dotnet add package GraphQL.Client
public void ConfigureServices(IServiceCollection services) { services.AddScoped(x => new GraphQL.Client.GraphQLClient(Configuration["GraphQLURI"])); ... }
public class OwnerConsumer { private readonly GraphQL.Client.GraphQLClient _client; public OwnerConsumer(GraphQL.Client.GraphQLClient client) { _client = client; } }
public void ConfigureServices(IServiceCollection services) { services.AddScoped(x => new GraphQL.Client.GraphQLClient(Configuration["GraphQLURI"])); services.AddScoped<OwnerConsumer>(); ... }
public enum TypeOfAccount { Cash, Savings, Expense, Income }
public class Account { public Guid Id { get; set; } public TypeOfAccount Type { get; set; } public string Description { get; set; } }
public class Owner { public Guid Id { get; set; } public string Name { get; set; } public string Address { get; set; } public ICollection<Account> Accounts { get; set; } }
public class OwnerInput { public string Name { get; set; } public string Address { get; set; } }
public async Task<List<Owner>> GetAllOwners() { var query = new GraphQLRequest { Query = @" query ownersQuery{ owners { id name address accounts { id type description } } }" }; var response = await _client.PostAsync(query); return response.GetDataFieldAs<List<Owner>>("owners"); }
[Route("api/owners")] public class OwnerController: Controller { private readonly OwnerConsumer _consumer; public OwnerController(OwnerConsumer consumer) { _consumer = consumer; } [HttpGet] public async Task<IActionResult> Get() { var owners = await _consumer.GetAllOwners(); return Ok(owners); } }
public async Task<Owner> CreateOwner(OwnerInput ownerToCreate) { var query = new GraphQLRequest { Query = @" mutation($owner: ownerInput!){ createOwner(owner: $owner){ id, name, address } }", Variables = new { owner = ownerToCreate } }; var response = await _client.PostAsync(query); return response.GetDataFieldAs<Owner>("createOwner"); }
[HttpPost] public async Task<IActionResult> PostItem([FromBody]OwnerInput model) { var result = await _consumer.CreateOwner(model); return Json(result); }
- یک کلاینت ASP.NET Core را برای فراخوانی GraphQL API آماده سازی کنیم.
- چه کتابخانههایی مورد نیاز است که میتوانند به ما در انجام فرآیند فراخوانی GraphQL API کمک کنند.
- ایجاد کردن queryها و mutationها در یک کلاینت ASP.NET Core.