یکی از وب سرویسهای سایت name api، امکان تشخیص موقتی بودن ایمیل مورد استفادهی جهت ثبت نام در یک سایت را فراهم میکند. آدرس WSDL آن نیز در اینجا قرار دارد. اگر مطابق معمول استفاده از سرویسهای وب در دات نت، بر روی ارجاعات پروژه کلیک راست کرده و گزینهی Add service refrence را انتخاب کنیم و سپس آدرس WSDL یاد شده را به آن معرفی کنیم، بدون مشکل ساختار این وب سرویس دریافت و برای استفادهی از آن به یک چنین کدی خواهیم رسید:
متد isDisposable ارائه شدهی توسط این وب سرویس، دو پارامتر context که در آن باید API Key خود را مشخص کرد و همچنین آدرس ایمیل مورد بررسی را دریافت میکند. اگر به همین ترتیب این پروژه را اجرا کنید، با خطای Bad request از طرف سرور متوقف خواهید شد:
اگر به خروجی این وب سرویس در فیدلر مراجعه کنیم، چنین شکلی را خواهد داشت:
عنوان کردهاست که api-key را، در درخواست وب خود ذکر نکردهایم.
اگر همین وب سرویس را توسط امکانات سایت http://wsdlbrowser.com بررسی کنید، بدون مشکل کار میکند. اما تفاوت در کجاست؟
خروجی ارسالی به سرور، توسط سایت http://wsdlbrowser.com به این شکل است:
و نمونهی تولید شدهی توسط WCF (امکان Add service reference در حقیقت یک WCF Client را ایجاد میکند) به صورت زیر میباشد:
از لحاظ اصول XML، خروجی تولیدی توسط WCF هیچ ایرادی ندارد. از این جهت که نام فضای نام مرتبط با http://schemas.xmlsoap.org/soap/envelope/ را به s تنظیم کردهاست و سپس با استفاده از این نام، Envelope را تشکیل دادهاست. اما ... این وب سرور جاوایی دقیقا با نام SOAP-ENV کار میکند و فضای نام ns1 بعدی آن. کاری هم به اصول XML ندارد که باید بر اساس نام xmlns ذکر شده، کار Parse ورودی دریافتی صورت گیرد و نه بر اساس یک رشتهی ثابت از پیش تعیین شده. بنابراین باید راهی را پیدا کنیم تا بتوان این s را تبدیل به SOAP-ENV کرد.
برای این منظور به سه کلاس ذیل خواهیم رسید:
که پس از تعریف client به نحو ذیل معرفی میشوند:
توسط EndpointBehavior سفارشی، میتوان به متد OnWriteStartEnvelope دسترسی یافت و سپس s آنرا با SOAP-ENV درخواستی این وب سرویس جایگزین کرد. اکنون اگر برنامه را اجرا کنید، بدون مشکل کار خواهد کرد و دیگر پیام یافت نشدن API-Key را صادر نمیکند.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید.
var client = new SoapDisposableEmailAddressDetectorClient(); var context = new soapContext { //todo: get your API key here: http://www.nameapi.org/en/register/ apiKey = "test" }; var result = client.isDisposable(context, "DaDiDoo@mailinator.com"); if (result.disposable.ToString() == "YES") { Console.WriteLine("YES! It's Disposable!"); }
Additional information: The remote server returned an unexpected response: (400) Bad Request.
<html><head><title>Bad Request</title></head><body><h1>Bad Request</h1><p>No api-key provided!</p></body></html>
اگر همین وب سرویس را توسط امکانات سایت http://wsdlbrowser.com بررسی کنید، بدون مشکل کار میکند. اما تفاوت در کجاست؟
خروجی ارسالی به سرور، توسط سایت http://wsdlbrowser.com به این شکل است:
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://disposableemailaddressdetector.email.services.v4_0.soap.server.nameapi.org/"> <SOAP-ENV:Body> <ns1:isDisposable> <context> <apiKey>test</apiKey> </context> <emailAddress>sdsdg@site.com</emailAddress> </ns1:isDisposable> </SOAP-ENV:Body> </SOAP-ENV:Envelope>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"> <s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <isDisposable xmlns="http://disposableemailaddressdetector.email.services.v4_0.soap.server.nameapi.org/"> <context xmlns=""><apiKey>test</apiKey></context> <emailAddress xmlns="">DaDiDoo@mailinator.com</emailAddress> </isDisposable> </s:Body> </s:Envelope>
برای این منظور به سه کلاس ذیل خواهیم رسید:
public class EndpointBehavior : IEndpointBehavior { public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { } public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { } public void Validate(ServiceEndpoint endpoint) { } public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) { clientRuntime.MessageInspectors.Add(new ClientMessageInspector()); } } public class ClientMessageInspector : IClientMessageInspector { public void AfterReceiveReply(ref Message reply, object correlationState) { } public object BeforeSendRequest(ref Message request, System.ServiceModel.IClientChannel channel) { request = new MyCustomMessage(request); return request; } } /// <summary> /// To customize WCF envelope and namespace prefix /// </summary> public class MyCustomMessage : Message { private readonly Message _message; public MyCustomMessage(Message message) { _message = message; } public override MessageHeaders Headers { get { return _message.Headers; } } public override MessageProperties Properties { get { return _message.Properties; } } public override MessageVersion Version { get { return _message.Version; } } protected override void OnWriteStartBody(XmlDictionaryWriter writer) { writer.WriteStartElement("Body", "http://schemas.xmlsoap.org/soap/envelope/"); } protected override void OnWriteBodyContents(XmlDictionaryWriter writer) { _message.WriteBodyContents(writer); } protected override void OnWriteStartEnvelope(XmlDictionaryWriter writer) { writer.WriteStartElement("SOAP-ENV", "Envelope", "http://schemas.xmlsoap.org/soap/envelope/"); writer.WriteAttributeString("xmlns", "ns1", null, value: "http://disposableemailaddressdetector.email.services.v4_0.soap.server.nameapi.org/"); } }
var client = new SoapDisposableEmailAddressDetectorClient(); client.Endpoint.Behaviors.Add(new EndpointBehavior());
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید.
یک نکتهی تکمیلی: پیاده سازی IXmlRepository مایکروسافت برای EF Core
از زمان ارائهی NET Core 2.2.، بستهی نیوگت جدید Microsoft.AspNetCore.DataProtection.EntityFrameworkCore ارائه شدهاست که کار آن دقیقا شبیه به پیاده سازی «یک نکتهی تکمیلی: روش ذخیره سازی کلید موقتی تولید شده در بانک اطلاعاتی بجای حافظهی سرور» است که در نظرات فوق ارائه شد.
برای استفادهی از آن، ابتدا بستهی نیوگت آنرا به برنامه اضافه کنید:
dotnet add package Microsoft.AspNetCore.DataProtection.EntityFrameworkCore
سپس Context ای را که بر اساس اینترفیس IDataProtectionKeyContext آن پیاده سازی شدهاست و دارای DbSet جدید از نوع DataProtectionKey است، تعریف کنید:
public class MyKeysContext : DbContext, IDataProtectionKeyContext { // A recommended constructor overload when using EF Core // with dependency injection. public MyKeysContext(DbContextOptions<MyKeysContext> options) : base(options) { } // This maps to the table that stores keys. public DbSet<DataProtectionKey> DataProtectionKeys { get; set; } }
که با اجرای مهاجرتها، یک جدول جدید را با سه فیلد زیر، ایجاد میکند:
public int Id { get; set; } public string FriendlyName { get; set; } public string XmlData { get; set; }
در آخر روش معرفی این Context به سیستم DataProtection به صورت زیر است:
public void ConfigureServices(IServiceCollection services) { // using Microsoft.AspNetCore.DataProtection; services.AddDataProtection() .PersistKeysToDbContext<MyKeysContext>(); }
ارسال پیامهای تبلیغاتی از طریق نرم افزارهایی مثل Viber , Telegram این روزها بازار داغی دارند. این نرم افزارها به همراه خود Api هایی را نیز جهت توسعه دهندگان ارائه میدهند. Telagram هم که به یکی از محبوبترین نرم افزارها در ایران تبدیل شدهاست. اگر به مستندات Telegram مراجعه کنید، میتوانید نحوهی استفاده را مشاهده کنید. ولی روشهای دیگری هم هستند که بسیار سادهتر هستند.
اگر به سایت notificatio.me مراجعه کنید، در این زمینه Api ایی را ارائه میدهد که به راحتی میتوانید از آن برای ارسال پیام استفاده کنید. البته تا ارسال 100 پیام آن رایگان هست.
ابتدا یک پروژهی از نوع Windows و یا console را ایجاد کنید.
سپس در خط فرمان package manager console دستور زیر را کپی کنید:
Install-Package Notificatio.TelegramClient
پس از نصب شدن بستهی Nuget، یک دکمه روی فرم قرار دهید و در رویداد OnClick آن دستورات زیر را تایپ کنید:
var api = NotificatioApi.Initialize(" Your Hash_Key"); api.SendMessage("Phone Number", "this is a test Message");
در آخر برنامه را اجرا کنید و بر روی دکمه، کلیک کنید. پس از اتمام کار ارسال، برای مشاهدهی تعداد پیامهای ارسال شده و یا آمار ماهانه، در سایت فوق میتوانید به Dashboard آن مراجعه کنید و تعداد و آمار پیامهای ارسالی را ببینید.
البته با استفاده از jQuery هم میتوانید کار ارسال پیام را انجام دهید:
$.ajax({ url: "http://www.api.notificatio.me/v1/user/message", type: "POST", dataType: "json", crossDomaint: true, data: { phoneNumber: "your_phone_number", apiHash: "your_api_hash", message: "your_message" }, cache: false, success: function() { // Your code to handle success message sent }, error: function(error) { // Your code to handle error } });
مطالب دورهها
شی گرایی در #F
برنامه نویسی شی گرای سومین نسل از الگوهای اصلی برنامه نویسی است. در توضیحات فصل اول گفته شد که #F یک زبان تابع گرا است ولی این بدان معنی نیست که #F از مفاهیمی نظیر کلاس و یا interface پشتیبانی نکند. برعکس در #F امکان تعریف کلاس و interface و هم چنین پیاده سازی مفاهیم شی گرایی وجود دارد.
*با توجه به این موضوع که فرض است دوستان با مفاهیم شی گرایی آشنایی دارند از توضیح و تشریح این مفاهیم خودداری میکنم.
Classes
کلاس چارچوبی از اشیا است برای نگهداری خواص(Properties) و رفتار ها(Methods) و رخدادها(Events). کلاس پایه ایترین مفهوم در برنامه نویسی شی گراست. ساختار کلی تعربف کلاس در #F به صورت زیر است:
همان طور که در ساختار بالا میبینید مفاهیم access-modifier و inherit و constructor هم در #F وجود دارد.
انواع access-modifier در #F
کلاس بالا دارای یک سازنده است که دو پارامتر ورودی میگیرد. کلمه end به معنای انتهای کلاس است. برای استفاده کلاس باید به صورت زیر عمل کنید:
توابع و خواص در کلاس ها
برای تعریف خاصیت در #F باید از کلمه کلیدی member استفاده کنید. در مثال بعدی برای کلاس بالا تابع و خاصیت تعریف خواهیم کرد.
کلاس بالا دارای سه خاصیت به نامهای Number و Name و Amount است و دو تابع به نامهای Deposit و Withdraw دارد. اما x استفاده شده قبل از هر member به معنی this در #C است. در #F شما برای اشاره به شناسههای یک محدوده خودتون باید یک نام رو برای اشاره گر مربوطه تعیین کنید.
استفاده از کلمه do
در #F زمانی که قصد داشته باشیم در بعد از وهله سازی از کلاس و فراخوانی سازنده، عملیات خاصی انجام شود(مثل انجام برخی عملیات متداول در سازندههای کلاسهای دات نت) باید از کلمه کلیدی do به همراه یک بلاک از کد استفاده کنیم.
یک مثال در این زمینه:
در مثال بالا دو عبارت do یکی به صورت static و دیگری به صورت غیر static تعریف شده اند. استفاده از do به صورت غیر static این امکان را به ما میدهد که بتوانیم به تمام شناسهها و توابع تعریف شده در کلاس استفاده کنیم ولی do به صورت static فقط به خواص و توابع از نوع static در کلاس دسترسی دارد.
خروجی مثال بالا:
خواص static:
برای تعریف خواص به صورت استاتیک مانند #C از کلمه کلیدی static استفاده کنید.مثالی در این زمینه:
SomeStaticMethod به صورت استاتیک تعریف شده در حالی که x.Prop به صورت غیر استاتیک. دسترسی به متدها یا خواص static باید بدون وهله سازی از کلاس انجام بگیرد در غیر این صورت با خطای کامپایلر روبرو خواهید شد.
روش استفاده درست:
متدهای get , set در خاصیت ها:
همانند #C و سایر زبانهای دات نت امکان تعریف متدهای get و set برای خاصیتهای یک کلاس وجود دارد.
ساختار کلی:
مثالی در این زمینه:
کد متناظر در #C:
یا به صورت:
Interface ها
اینترفیس به تمامی خواص و توابع عمومی اشئایی که آن را پیاده سازی کرده اند اشاره میکند. (توضیحات بیشتر (^ ) و (^ ))ساختار کلی برای تعریف آن به صورت زیر است:
مثال:
استفاده از حرف I برای شروع نام اینترفیس طبق قوانین تعریف شده (اختیاری) برای نام گذاری است.
نکته: در هنگام تعریف توابع و خاصیت در interfaceها باید از کلمه abstract استفاده کنیم. هر کلاسی که از یک یا چند تا اینترفیس ارث ببرد باید تمام خواص و توابع اینتریسها را پیاده سازی کند. در مثال بعدی کلاس SomeClass1 اینترفیس بالا را پیاده سازی میکند. دقت کنید که کلمه this توسط من به عنوان اشاره گر به اشیای کلاس تعیین شده و شما میتونید از هر کلمه یا حرف دیگری استفاده کنید.
نکته مهم: اگر قصد فراخوانی متد Print را در کلاس بالا دارید نمیتونید به صورت مستقیم متد بالا را فراخوانی کنید. بلکه حتما باید کلاس به
اینترفیس مربوطه cast شود.
روش نادرست:
روش درست:
برای عملیات cast ازاستفاده کنید.
در مثال بعدی کلاسی خواهیم داشت که از سه اینترفیس ارث میبرد. در نتیجه باید تمام متدهای هر سه اینترفیس را پیاده سازی کند.
فراخوانی این متدها نیز به صورت زیر خواهد بود:
کلاسهای Abstract
#F از کلاسهای abstract هم پشتیبانی میکند. اگر با کلاسهای abstract در #C آشنایی ندارید میتونید مطالب مورد نظر رو در (^ ) و (^ ) مطالعه کنید. به صورت خلاصه کلاسهای abstract به عنوان کلاسهای پایه در برنامه نویسی شی گرا استفاده میشوند. این کلاسها دارای خواص و متدهای پیاده سازی شده و نشده هستند. خواص و متد هایی که در کلاس پایه abstract پیاده سازی نشده اند باید توسط کلاس هایی که از این کلاس پایه ارث میبرند حتما پیاده سازی شوند.
ساختار کلی تعریف کلاسهای abstract:
در #F برای این که مشخص کنیم که یک کلاس abstract است حتما باید [<AbstractClass>] در بالای کلاس تعریف شود.
کلاس بالا تعریفی از کلاس abstract است که سه خصوصیت abstract دارد (برای
تعیین خصوصیتها و متد هایی که در کلاس پایه پیاده سازی نمیشوند از کلمه
کلیدی abstract در هنگام تعریف آنها استفاده میکنیم). حال دو کلاس ایجاد میکنیم که این کلاس پایه را پیاده سازی کنند.
#1 کلاس اول
#2 کلاس دوم
Structures
structureها در #F دقیقا معال struct در #C هستند. توضیحات بیشتر درباره struct در #C (^ ) و (^ )). اما به طور خلاصه باید ذکر کنم که strucureها تقریبا دارای مفهوم کلاس هستند با اندکی تفاوت که شامل موارد زیر است:
یک نکته مهم هنگام کار با structها در #F این است که امکان استفاده از let
و Binding در structها وجود ندارد. به جای آن باید از val استفاده کنید.
تفاوت اصلی بین val و let در این است که هنگام تعریف
شناسه با val امکان مقدار دهی اولیه به شناسه وجود ندارد. در مثال بالا
مقادیر برای x و y و z برابر 0.0 است که توسط کامپایلر انجام میشود. در
ادامه یک struct به همراه سازنده تعریف میکنیم:
توسط سازنده struct بالا مقادیر اولیه x و y دریافت میشود به متغیرهای متناظر انتساب میشود.
در پایان یک مثال مشترک رو در #C و #F پیاده سازی میکنیم:
*با توجه به این موضوع که فرض است دوستان با مفاهیم شی گرایی آشنایی دارند از توضیح و تشریح این مفاهیم خودداری میکنم.
Classes
کلاس چارچوبی از اشیا است برای نگهداری خواص(Properties) و رفتار ها(Methods) و رخدادها(Events). کلاس پایه ایترین مفهوم در برنامه نویسی شی گراست. ساختار کلی تعربف کلاس در #F به صورت زیر است:
type [access-modifier] type-name [type-params] [access-modifier] ( parameter-list ) [ as identifier ] = [ class ] [ inherit base-type-name(base-constructor-args) ] [ let-bindings ] [ do-bindings ] member-list ... [ end ] type [access-modifier] type-name1 ... and [access-modifier] type-name2 ... ...
انواع access-modifier در #F
- public : دسترسی برای تمام فراخوانها امکان پذیر است
- internal : دسترسی برای تمام فراخوان هایی که در همین assembly هستند امکان پذیر است
- private : دسترسی فقط برای فراخوانهای موجود در همین ماژول امکان پذیر است
نکته : protected access modifier در #F پشتیبانی نمیشود.
مثالی از تعریف کلاس:
type Account(number : int, name : string) = class let mutable amount = 0m end
let myAccount = new Account(123456, "Masoud")
برای تعریف خاصیت در #F باید از کلمه کلیدی member استفاده کنید. در مثال بعدی برای کلاس بالا تابع و خاصیت تعریف خواهیم کرد.
type Account(number : int, name: string) = class let mutable amount = 0m member x.Number = number member x.Name= name member x.Amount = amount member x.Deposit(value) = amount <- amount + value member x.Withdraw(value) = amount <- amount - value end
open System type Account(number : int, name: string) = class let mutable amount = 0m member x.Number = number member x.Name= name member x.Amount = amount member x.Deposit(value) = amount <- amount + value member x.Withdraw(value) = amount <- amount - value end
let masoud= new Account(12345, "Masoud") let saeed = new Account(67890, "Saeed") let transfer amount (source : Account) (target : Account) = source.Withdraw amount target.Deposit amount let printAccount (x : Account) = printfn "x.Number: %i, x.Name: %s, x.Amount: %M" x.Number x.Name x.Amount let main() = let printAccounts() = [masoud; saeed] |> Seq.iter printAccount printfn "\nInializing account" homer.Deposit 50M marge.Deposit 100M printAccounts() printfn "\nTransferring $30 from Masoud to Saeed" transfer 30M masoud saeed
printAccounts() printfn "\nTransferring $75 from Saeed to Masoud" transfer 75M saeed masoud printAccounts() main()
در #F زمانی که قصد داشته باشیم در بعد از وهله سازی از کلاس و فراخوانی سازنده، عملیات خاصی انجام شود(مثل انجام برخی عملیات متداول در سازندههای کلاسهای دات نت) باید از کلمه کلیدی do به همراه یک بلاک از کد استفاده کنیم.
open System open System.Net type Stock(symbol : string) = class let mutable _symbol = String.Empty do //کد مورد نظر در این جا نوشته میشود end
open System type MyType(a:int, b:int) as this = inherit Object() let x = 2*a let y = 2*b do printfn "Initializing object %d %d %d %d %d %d" a b x y (this.Prop1) (this.Prop2) static do printfn "Initializing MyType." member this.Prop1 = 4*x member this.Prop2 = 4*y override this.ToString() = System.String.Format("{0} {1}", this.Prop1, this.Prop2) let obj1 = new MyType(1, 2)
خروجی مثال بالا:
Initializing MyType. Initializing object 1 2 2 4 8 16
برای تعریف خواص به صورت استاتیک مانند #C از کلمه کلیدی static استفاده کنید.مثالی در این زمینه:
type SomeClass(prop : int) = class member x.Prop = prop static member SomeStaticMethod = "This is a static method" end
let instance = new SomeClass(5);; instance.SomeStaticMethod;; output: stdin(81,1): error FS0191: property 'SomeStaticMethod' is static.
SomeClass.SomeStaticMethod;; (* invoking static method *)
همانند #C و سایر زبانهای دات نت امکان تعریف متدهای get و set برای خاصیتهای یک کلاس وجود دارد.
ساختار کلی:
member alias.PropertyName with get() = some-value and set(value) = some-assignment
type MyClass() = class let mutable num = 0 member x.Num with get() = num and set(value) = num <- value end;;
public int Num { get{return num;} set{num=value;} }
type MyClass() = class let mutable num = 0 member x.Num with get() = num and set(value) = if value > 10 || value < 0 then raise (new Exception("Values must be between 0 and 10")) else num <- value end
Interface ها
اینترفیس به تمامی خواص و توابع عمومی اشئایی که آن را پیاده سازی کرده اند اشاره میکند. (توضیحات بیشتر (^ ) و (^ ))ساختار کلی برای تعریف آن به صورت زیر است:
type type-name = interface inherits-decl member-defns end
type IPrintable = abstract member Print : unit -> unit
نکته: در هنگام تعریف توابع و خاصیت در interfaceها باید از کلمه abstract استفاده کنیم. هر کلاسی که از یک یا چند تا اینترفیس ارث ببرد باید تمام خواص و توابع اینتریسها را پیاده سازی کند. در مثال بعدی کلاس SomeClass1 اینترفیس بالا را پیاده سازی میکند. دقت کنید که کلمه this توسط من به عنوان اشاره گر به اشیای کلاس تعیین شده و شما میتونید از هر کلمه یا حرف دیگری استفاده کنید.
type SomeClass1(x: int, y: float) = interface IPrintable with member this.Print() = printfn "%d %f" x y
روش نادرست:
let instance = new SomeClass1(10,20) instance.Print//فراخوانی این متد باعث ایجاد خطای کامپایلری میشود.
let instance = new SomeClass1(10,20) let instanceCast = instance :> IPrintable// استفاده از (<:) برای عملیات تبدیل کلاس به اینترفیس instanceCast.Print
در مثال بعدی کلاسی خواهیم داشت که از سه اینترفیس ارث میبرد. در نتیجه باید تمام متدهای هر سه اینترفیس را پیاده سازی کند.
type Interface1 = abstract member Method1 : int -> int type Interface2 = abstract member Method2 : int -> int type Interface3 = inherit Interface1 inherit Interface2 abstract member Method3 : int -> int type MyClass() = interface Interface3 with member this.Method1(n) = 2 * n member this.Method2(n) = n + 100 member this.Method3(n) = n / 10
let instance = new MyClass() let instanceToCast = instance :> Interface3 instanceToCast.Method3 10
#F از کلاسهای abstract هم پشتیبانی میکند. اگر با کلاسهای abstract در #C آشنایی ندارید میتونید مطالب مورد نظر رو در (^ ) و (^ ) مطالعه کنید. به صورت خلاصه کلاسهای abstract به عنوان کلاسهای پایه در برنامه نویسی شی گرا استفاده میشوند. این کلاسها دارای خواص و متدهای پیاده سازی شده و نشده هستند. خواص و متد هایی که در کلاس پایه abstract پیاده سازی نشده اند باید توسط کلاس هایی که از این کلاس پایه ارث میبرند حتما پیاده سازی شوند.
ساختار کلی تعریف کلاسهای abstract:
[<AbstractClass>] type [ accessibility-modifier ] abstract-class-name = [ inherit base-class-or-interface-name ] [ abstract-member-declarations-and-member-definitions ] abstract member member-name : type-signature
[<AbstractClass>] type Shape(x0 : float, y0 : float) = let mutable x, y = x0, y0 let mutable rotAngle = 0.0 abstract Area : float with get abstract Perimeter : float with get abstract Name : string with get
#1 کلاس اول
type Square(x, y,SideLength) = inherit Shape(x, y)
override this.Area = this.SideLength * this.SideLength override this.Perimeter = this.SideLength * 4. override this.Name = "Square"
type Circle(x, y, radius) = inherit Shape(x, y)
let PI = 3.141592654 member this.Radius = radius override this.Area = PI * this.Radius * this.Radius override this.Perimeter = 2. * PI * this.Radius
structureها در #F دقیقا معال struct در #C هستند. توضیحات بیشتر درباره struct در #C (^ ) و (^ )). اما به طور خلاصه باید ذکر کنم که strucureها تقریبا دارای مفهوم کلاس هستند با اندکی تفاوت که شامل موارد زیر است:
- structureها از نوع مقداری هستند و این بدین معنی است مستقیما درون پشته ذخیره میشوند.
- ارجاع به structureها از نوع ارجاع با مقدار است بر خلاف کلاسها که از نوع ارجاع به منبع هستند.(^ )
- structureها دارای خواص ارث بری نیستند.
- عموما از structure برای ذخیره مجموعه ای از دادهها با حجم و اندازه کم استفاده میشود.
ساختار کلی تعریف structure
[ attributes ] type [accessibility-modifier] type-name = struct type-definition-elements end //یا به صورت زیر [ attributes ] [<StructAttribute>] type [accessibility-modifier] type-name = type-definition-elements
type Point3D = struct val x: float val y: float val z: float end
type Point2D = struct val X: float val Y: float new(x: float, y: float) = { X = x; Y = y } end
در پایان یک مثال مشترک رو در #C و #F پیاده سازی میکنیم:
قابلیتهای قرار گرفتهی در اسمبلی Microsoft.Extensions.DependencyInjection که پایهی تزریق وابستگیهای برنامههای مبتنی بر NET Core. را ارائه میدهد، برای پیاده سازی اکثر پروژهها کافی است. اما اگر از نگارشهای پیشین ASP.NET MVC به ASP.NET Core مهاجرت کرده باشید، حتما با قابلیتهای ویژهی اسکن اسمبلیهای موجود در IoC Containers ثالث، جهت ساده سازی معرفی سرویسهای برنامه به سیستم تزریق وابستگیها، آشنایی دارید. برای مثال StructureMap قابلیت اسکن اسمبلیهای موجود در برنامه و معرفی اینترفیسها و سرویسهای موجود در آنرا به Container خود دارد:
و یا AutoFac نیز به همین صورت:
البته میتوان مجددا به تمام این قابلیتها رسید؛ به شرطیکه سیستم تزریق وابستگیهای پایهی NET Core. را با یکی از IoC Containers ثالث به طور کامل تعویض کنیم. اگر قصد چنین تعویض پایهای را ندارید و هنوز قصد دارید از همان Microsoft.Extensions.DependencyInjection استفاده کنید، اما تعدادی متد الحاقی جدید تعریف شدهی بر فراز آن، کار اسکن کردن اسمبلیها را مانند قبل انجام دهند، میتوان از کتابخانهی کمکی Scrutor استفاده کرد. این کتابخانه، جایگزین سیستم تزریق وابستگیهای توکار برنامههای NET Core. نیست؛ بلکه صرفا مکمل آن است.
دریافت و نصب کتابخانهی کمکی Scrutor
کتابخانهی کمکی Scrutor سورس باز بوده و بستهی NuGet آن توسط یکی از دستورات زیر به پروژه افزوده میشود:
و یا به صورت مدخلی جدید در فایل csproj:
ثبت و معرفی سادهتر سرویسها بر اساس قواعد نامگذاری آنها توسط Scrutor
فرض کنید تعدادی سرویس را به صورت زیر تعریف کردهاید:
روش متداول معرفی آنها به IoC Container برنامه به صورت زیر است:
و هرچقدر تعداد سرویسهای برنامه بیشتر شود، سطرهای فوق نیز بیشتر خواهند شد.
در اینجا در حین تعریف سرویسهای فوق این روش نامگذاری رعایت شدهاست: هر اینترفیس، نامش یک I بیشتر از نام کلاس مشتق شدهی از آن دارد؛ مانند اینترفیس IFoo و کلاس Foo. کتابخانهی StructureMap که در ابتدای بحث معرفی شد، کار اسکن و اتصال یک چنین سرویسهایی را با تعریف scanner.WithDefaultConventions انجام میدهد. معادل آن با Scrutor به صورت زیر است:
تعریف فوق به این معنا است:
- scan.FromAssemblyOf کار اسکن اسمبلی را انجام میدهد که نوع IFoo در آن قرار دارد. اگر از scan.FromCallingAssembly استفاده کنیم، به این معنا است که کار اسکن را دقیقا از همین اسمبلی فراخوان کدهای جاری، شروع کن. اما چون IFoo تعریف شده، در یک پروژه و اسمبلی دیگر قرار دارد، به همین جهت نیاز به ذکر صریح اسمبلی آن نیز هست.
- AddClasses یعنی تمام کلاسهای public, non-abstract را به لیست services اضافه کن.
- AsMatchingInterface یعنی بر اساس قرارداد نامگذاری IClassName و ClassName، اتصالات سرویسها را انجام بده.
بجای آن میتوان از AsImplementedInterfaces نیز استفاده کرد. این حالت برای زمانی مناسب است که یک کلاس، چندین اینترفیس را پیاده سازی کند (مثلا کلاس TestService اینترفیسهای ITestService و IService را پیاده سازی کرده باشد) و علاقمند باشید به ازای هر اینترفیس، یکبار سرویس آن نیز ثبت شود؛ کاری مانند تنظیمات زیر:
یا حتی میتوان از متد ()<As<T نیز استفاده کرد. در اینجا به Scrutor گفته میشود که تمام کلاسهای یافت شده را بر اساس نوع سرویس T ثبت و معرفی کن. البته اگر کلاسی نتواند نوع اینترفیس T را پیاده سازی کند، در زمان اجرا با استثناء مواجه خواهید شد.
- WithScopedLifetime نیز طول عمر این سرویسهای اضافه شده را مشخص میکند. در اینجا میتوان WithTransientLifetime و WithSingletonLifetime را نیز ذکر کرد.
بنابراین همانطور که ملاحظه میکنید، هنوز هم همان سیستم Microsoft.Extensions.DependencyInjection برقرار است؛ اما با وجود متد الحاقی جدید Scan، کار تعاریف سرویسهای برنامه به شدت ساده میشود.
کار با وهلههای کلاسهای سرویسها بجای اینترفیسهای آن توسط Scrutor
میخواهیم مثال سوم قسمت ششم «چگونه بجای اینترفیسها، یک وهله از کلاسی مشخص را از سیستم تزریق وابستگیها درخواست کنیم؟» را توسط Scrutor پیاده سازی کنیم:
در حالت متداول آن میتوان از روش زیر نیز استفاده کرد:
که با افزایش تعداد کلاسهای سرویس برنامه به همین نحو نیز افزایش خواهند یافت. معادل این تنظیمات با Scrutor به صورت زیر است:
در اینجا اسمبلی حاوی IService اسکن خواهد شد و سپس تمام کلاسهای public, non-abstract آن AsSelf (ثبت پیاده سازی خود کلاس به عنوان سرویس) با طول عمر Transient به لیست services اضافه میشوند و یا اگر صرفا تعدادی سرویس مشخص مد نظر بود میتوان به صورت زیر عمل کرد:
متدهایی که در Scrutor، یک پیاده سازی را به عنوان سرویس معرفی میکنند، شامل این موارد هستند:
AsSelf: معادل ()<services.AddTransient<TestService است. در این حالت کلاسهایی که اینترفیسی را پیاده سازی نمیکنند و یا در کل مایل هستید که از طریق تزریق وابستگیها در دسترس باشند، میتوان توسط متد AsSelf به سیستم معرفی کرد.
AsSelfWithInterfaces: معادل تنظیمات زیر است:
فرض کنید کلاس TestService اینترفیسهای ITestService و IService را پیاده سازی کرده باشد. با استفاده از AsSelfWithInterfaces، یکبار پیاده سازی خود سرویس به سیستم معرفی میشود، سپس به ازای هر اینترفیس، از همان وهلهی TestService برای وهله سازی سرویسهای ITestService و IService نیز استفاده میشود.
روشهای متفاوت اسکن اسمبلیها در Scrutor
Scrutor به همراه روشهای متعددی برای تعریف اسمبلی یا اسمبلیهایی است که باید اسکن شوند و نمونهای از آنرا با FromAssemblyOf بررسی کردیم:
سایر موارد آن به شرح زیر هستند:
الف) FromAssemblyOf<>, FromAssembliesOf : اسمبلی یا اسمبلیهایی که نوع یا نوعهای تعیین شده را به همراه دارند، اسکن میکند.
ب) FromCallingAssembly, FromExecutingAssembly, FromEntryAssembly کار اسکن اسمبلیهای فراخوان، اسمبلی که هم اکنون در حال اجرا است و اسمبلی آغازین برنامه را انجام میدهند.
ج) FromAssemblyDependencies: تمام اسمبلیهایی را که وابستهی به اسمبلی معرفی شدهی به آن هستند، اسکن میکند.
د) FromApplicationDependencies, FromDependencyContext: تمام اسمبلیهایی را که توسط برنامه، ارجاعی به آنها وجود دارند، اسکن میکند.
انتخاب دقیقتر کلاسها و سرویسهای مدنظر توسط Scrutor
شاید عملکرد کلی متد AddClasses مدنظر شما نباشد و نیاز به انتخاب دقیقتری از سرویسهای اسکن شده را داشته باشید؛ برای این مورد نیز Scrutor روشهای زیر را ارائه میدهد. برای مثال خود کلاس AddClasses دارای overloadهای زیر نیز هست:
حالت پیشفرض آن انتخاب تمام کلاسهای public, non-abstract است. اگر پارامتر publicOnly را با false مقدار دهی کنید، internal/private nested classes را نیز انتخاب میکند. پارامتر action ای که در اینجا درنظر گرفته شده، جهت فیلتر کردن سرویسهای انتخابی است که تعدادی از مثالهای آنرا در زیر بررسی میکنیم:
در اینجا در حالت اول، کلاسهایی که صرفا اینترفیس IService را پیاده سازی کرده باشند، انتخاب میشوند. حالت دوم آن، انتخابها را به یک فضای نام محدود میکند و حالت سوم اگر نام کلاسی به Repository ختم شود، آنرا به عنوان سرویس انتخاب خواهد کرد.
مدیریت جایگزینی سرویسها توسط Scrutor
یکی از مزیتهای طراحی یک برنامه با درنظر گرفتن الگوی تزریق وابستگیها، امکان جایگزین کردن سرویسهای پیشفرض آن با سرویسهای دیگری است. فرض کنید کتابخانهای ارائه شده و از الگوریتم هش کردن X استفاده کردهاست؛ اما شما علاقمندید تا از الگوریتم Y بجای آن استفاده کنید. اگر این کتابخانه وهلهی الگوریتم هش کردن را از طریق تزریق وابستگیها تامین کرده باشد، فقط کافی است در ابتدای معرفی تنظیمات تزریق وابستگیهای آن، سرویس الگوریتم هش کردن موجود را با نمونهی خاص خودتان جایگزین کنید.
اکنون فرض کنید پیش از استفادهی از Scrutor، تعدادی سرویس را به روش متداولی ثبت و معرفی کردهاید:
حال که قرار است متد Scan آن، سرویسهای یک اسمبلی را به لیست موجود اضافه کند، به سرویسهای زیر میرسد:
رفتار آن با سرویسهای معادلی که از پیش ثبت شدهاند چگونه باید باشد؟ برای مدیریت این مساله، متد UsingRegistrationStrategy پیش بینی شدهاست:
و پارامتر دریافتی آن یک چنین امضایی را دارد:
- حالت Append آن که حالت پیشفرض نیز هست، تمام سرویسهای یافت شده را به لیست IServiceCollection اضافه میکند؛ صرفنظر از اینکه پیشتر ثبت شدهاست یا خیر.
- حالت Skip آن، سرویسی را تکراری ثبت نمیکند. یعنی اگر سرویسی پیشتر در مجموعهی IServiceCollection موجود بود، مجددا آنرا ثبت نمیکند.
سپس نوبت به متدهای Replace میرسد که یک چنین پارامتری را قبول میکنند:
- در حالت استفادهی از Replace(ReplacementBehavior.ServiceType)، اگر سرویسی پیشتر در لیست IServiceCollection ثبت شده باشد، آنرا حذف کرده و سپس نمونهی جدید را ثبت میکند (ثبت سرویس بر اساس اینترفیس و پیاده سازی آن).
- در حالت استفادهی از Replace(ReplacementBehavior.ImplementationType)، اگر پیاده سازی کلاسی پیشتر در لیست IServiceCollection ثبت شده باشد، آنرا حذف کرده و سپس نمونهی جدید را ثبت میکند (ثبت سرویس صرفا بر اساس نام کلاس آن).
- حالت Replace(ReplacementBehavior.All) هر دو حالت قبل را با هم شامل میشود.
امکان ترکیب چندین استراتژی جستجو با هم توسط Scrutor
در یک برنامهی واقعی غیرممکن است که بخواهید تمام کلاسها را با یک طول عمر، اسکن و ثبت کنید. برای این منظور میتوان از قابلیت فیلتر کردن کلاسها که در مورد آن بحث شد و همچنین امکان ترکیب زنجیر وار حالتهای مختلف اسکن، استفاده کرد:
var container = new Container(x => { x.Scan(scanner => { scanner.AssemblyContainingType<IOrderHandler>(); // connects `IAccounting` to `Accounting` and `ISales` to `Sales` automatically. scanner.WithDefaultConventions(); }); });
builder.RegisterAssemblyTypes(myAssembly) .Where(t => t.IsAssignableTo<IMyInterface>()) .AsImplementedInterfaces();
دریافت و نصب کتابخانهی کمکی Scrutor
کتابخانهی کمکی Scrutor سورس باز بوده و بستهی NuGet آن توسط یکی از دستورات زیر به پروژه افزوده میشود:
> Install-Package Scrutor > dotnet add package Scrutor
<Project Sdk="Microsoft.NET.Sdk.Web"> <ItemGroup> <PackageReference Include="Scrutor" Version="3.0.2" /> </ItemGroup> </Project>
ثبت و معرفی سادهتر سرویسها بر اساس قواعد نامگذاری آنها توسط Scrutor
فرض کنید تعدادی سرویس را به صورت زیر تعریف کردهاید:
namespace CoreIocServices { public interface IFoo { void Run(); } public class Foo : IFoo { public void Run() { throw new System.NotImplementedException(); } } public interface IBar { void Add(); } public class Bar : IBar { public void Add() { throw new System.NotImplementedException(); } } public interface IBaz { void Stop(); } public class Baz : IBaz { public void Stop() { throw new System.NotImplementedException(); } } }
services.AddScoped<IFoo, Foo>(); services.AddScoped<IBar, Bar>(); services.AddScoped<IBaz, Baz>();
در اینجا در حین تعریف سرویسهای فوق این روش نامگذاری رعایت شدهاست: هر اینترفیس، نامش یک I بیشتر از نام کلاس مشتق شدهی از آن دارد؛ مانند اینترفیس IFoo و کلاس Foo. کتابخانهی StructureMap که در ابتدای بحث معرفی شد، کار اسکن و اتصال یک چنین سرویسهایی را با تعریف scanner.WithDefaultConventions انجام میدهد. معادل آن با Scrutor به صورت زیر است:
namespace CoreIocSample02 { public class Startup { public void ConfigureServices(IServiceCollection services) { services.Scan(scan => //scan.FromCallingAssembly() scan.FromAssemblyOf<IFoo>() .AddClasses() .AsMatchingInterface() .WithScopedLifetime());
- scan.FromAssemblyOf کار اسکن اسمبلی را انجام میدهد که نوع IFoo در آن قرار دارد. اگر از scan.FromCallingAssembly استفاده کنیم، به این معنا است که کار اسکن را دقیقا از همین اسمبلی فراخوان کدهای جاری، شروع کن. اما چون IFoo تعریف شده، در یک پروژه و اسمبلی دیگر قرار دارد، به همین جهت نیاز به ذکر صریح اسمبلی آن نیز هست.
- AddClasses یعنی تمام کلاسهای public, non-abstract را به لیست services اضافه کن.
- AsMatchingInterface یعنی بر اساس قرارداد نامگذاری IClassName و ClassName، اتصالات سرویسها را انجام بده.
بجای آن میتوان از AsImplementedInterfaces نیز استفاده کرد. این حالت برای زمانی مناسب است که یک کلاس، چندین اینترفیس را پیاده سازی کند (مثلا کلاس TestService اینترفیسهای ITestService و IService را پیاده سازی کرده باشد) و علاقمند باشید به ازای هر اینترفیس، یکبار سرویس آن نیز ثبت شود؛ کاری مانند تنظیمات زیر:
services.AddScoped<ITestService, TestService>(); services.AddScoped<IService, TestService>();
- WithScopedLifetime نیز طول عمر این سرویسهای اضافه شده را مشخص میکند. در اینجا میتوان WithTransientLifetime و WithSingletonLifetime را نیز ذکر کرد.
بنابراین همانطور که ملاحظه میکنید، هنوز هم همان سیستم Microsoft.Extensions.DependencyInjection برقرار است؛ اما با وجود متد الحاقی جدید Scan، کار تعاریف سرویسهای برنامه به شدت ساده میشود.
کار با وهلههای کلاسهای سرویسها بجای اینترفیسهای آن توسط Scrutor
میخواهیم مثال سوم قسمت ششم «چگونه بجای اینترفیسها، یک وهله از کلاسی مشخص را از سیستم تزریق وابستگیها درخواست کنیم؟» را توسط Scrutor پیاده سازی کنیم:
namespace CoreIocServices { public interface IService { } public class Service1 : IService { } public class Service2 : IService { } public class Service : IService { } }
services.AddTransient<Service1>(); services.AddTransient<Service2>(); services.AddTransient<Service>();
namespace CoreIocSample02 { public class Startup { public void ConfigureServices(IServiceCollection services) { services.Scan(scan => //scan.FromCallingAssembly() scan.FromAssemblyOf<IService>() .AddClasses() .AsSelf() .WithTransientLifetime());
services.Scan(scan => scan.AddTypes(new[] { typeof(Service1), typeof(Service2) }) .AsSelf() .WithTransientLifetime());
AsSelf: معادل ()<services.AddTransient<TestService است. در این حالت کلاسهایی که اینترفیسی را پیاده سازی نمیکنند و یا در کل مایل هستید که از طریق تزریق وابستگیها در دسترس باشند، میتوان توسط متد AsSelf به سیستم معرفی کرد.
AsSelfWithInterfaces: معادل تنظیمات زیر است:
services.AddSingleton<TestService>(); services.AddSingleton<ITestService>(x => x.GetRequiredService<TestService>()); services.AddSingleton<IService>(x => x.GetRequiredService<TestService>());
روشهای متفاوت اسکن اسمبلیها در Scrutor
Scrutor به همراه روشهای متعددی برای تعریف اسمبلی یا اسمبلیهایی است که باید اسکن شوند و نمونهای از آنرا با FromAssemblyOf بررسی کردیم:
services.Scan(scan => //scan.FromCallingAssembly() scan.FromAssemblyOf<IService>()
الف) FromAssemblyOf<>, FromAssembliesOf : اسمبلی یا اسمبلیهایی که نوع یا نوعهای تعیین شده را به همراه دارند، اسکن میکند.
ب) FromCallingAssembly, FromExecutingAssembly, FromEntryAssembly کار اسکن اسمبلیهای فراخوان، اسمبلی که هم اکنون در حال اجرا است و اسمبلی آغازین برنامه را انجام میدهند.
ج) FromAssemblyDependencies: تمام اسمبلیهایی را که وابستهی به اسمبلی معرفی شدهی به آن هستند، اسکن میکند.
د) FromApplicationDependencies, FromDependencyContext: تمام اسمبلیهایی را که توسط برنامه، ارجاعی به آنها وجود دارند، اسکن میکند.
انتخاب دقیقتر کلاسها و سرویسهای مدنظر توسط Scrutor
شاید عملکرد کلی متد AddClasses مدنظر شما نباشد و نیاز به انتخاب دقیقتری از سرویسهای اسکن شده را داشته باشید؛ برای این مورد نیز Scrutor روشهای زیر را ارائه میدهد. برای مثال خود کلاس AddClasses دارای overloadهای زیر نیز هست:
public interface IImplementationTypeSelector : IAssemblySelector, IFluentInterface { IServiceTypeSelector AddClasses(); IServiceTypeSelector AddClasses(bool publicOnly); IServiceTypeSelector AddClasses(Action<IImplementationTypeFilter> action); IServiceTypeSelector AddClasses(Action<IImplementationTypeFilter> action, bool publicOnly); }
services.Scan(scan => scan .FromAssemblyOf<IService>() .AddClasses(classes => classes.AssignableTo<IService>()) // .AddClasses(classes => classes.InNamespaces("MyApp")) // .AddClasses(classes => classes.Where(type => type.Name.EndsWith("Repository")) .AsImplementedInterfaces() .WithTransientLifetime());
مدیریت جایگزینی سرویسها توسط Scrutor
یکی از مزیتهای طراحی یک برنامه با درنظر گرفتن الگوی تزریق وابستگیها، امکان جایگزین کردن سرویسهای پیشفرض آن با سرویسهای دیگری است. فرض کنید کتابخانهای ارائه شده و از الگوریتم هش کردن X استفاده کردهاست؛ اما شما علاقمندید تا از الگوریتم Y بجای آن استفاده کنید. اگر این کتابخانه وهلهی الگوریتم هش کردن را از طریق تزریق وابستگیها تامین کرده باشد، فقط کافی است در ابتدای معرفی تنظیمات تزریق وابستگیهای آن، سرویس الگوریتم هش کردن موجود را با نمونهی خاص خودتان جایگزین کنید.
اکنون فرض کنید پیش از استفادهی از Scrutor، تعدادی سرویس را به روش متداولی ثبت و معرفی کردهاید:
services.AddTransient<ITransientService, TransientService>(); services.AddScoped<IScopedService, ScopedService>();
public class TransientService : IFooService {} public class AnotherService : IScopedService {}
services.Scan(scan => scan.FromAssemblyOf<IFoo>() .AddClasses() .UsingRegistrationStrategy(RegistrationStrategy.Skip) .AsMatchingInterface() .WithScopedLifetime());
namespace Scrutor { public abstract class RegistrationStrategy { public static readonly RegistrationStrategy Skip; public static readonly RegistrationStrategy Append; protected RegistrationStrategy(); public static RegistrationStrategy Replace(); public static RegistrationStrategy Replace(ReplacementBehavior behavior); public abstract void Apply(IServiceCollection services, ServiceDescriptor descriptor); } }
- حالت Skip آن، سرویسی را تکراری ثبت نمیکند. یعنی اگر سرویسی پیشتر در مجموعهی IServiceCollection موجود بود، مجددا آنرا ثبت نمیکند.
سپس نوبت به متدهای Replace میرسد که یک چنین پارامتری را قبول میکنند:
namespace Scrutor { [Flags] public enum ReplacementBehavior { Default = 0, ServiceType = 1, ImplementationType = 2, All = 3 } }
- در حالت استفادهی از Replace(ReplacementBehavior.ImplementationType)، اگر پیاده سازی کلاسی پیشتر در لیست IServiceCollection ثبت شده باشد، آنرا حذف کرده و سپس نمونهی جدید را ثبت میکند (ثبت سرویس صرفا بر اساس نام کلاس آن).
- حالت Replace(ReplacementBehavior.All) هر دو حالت قبل را با هم شامل میشود.
امکان ترکیب چندین استراتژی جستجو با هم توسط Scrutor
در یک برنامهی واقعی غیرممکن است که بخواهید تمام کلاسها را با یک طول عمر، اسکن و ثبت کنید. برای این منظور میتوان از قابلیت فیلتر کردن کلاسها که در مورد آن بحث شد و همچنین امکان ترکیب زنجیر وار حالتهای مختلف اسکن، استفاده کرد:
services.Scan(scan => scan .FromAssemblyOf<CombinedService>() .AddClasses(classes => classes.AssignableTo<ICombinedService>()) // Filter classes .AsSelfWithInterfaces() .WithSingletonLifetime() .AddClasses(x=> x.AssignableTo(typeof(IOpenGeneric<>))) // Can close generic types .AsMatchingInterface() .AddClasses(x=> x.InNamespaceOf<MyClass>()) .UsingRegistrationStrategy(RegistrationStrategy.Replace()) // Defaults to ReplacementBehavior.ServiceType .AsMatchingInterface() .WithScopedLifetime() .FromAssemblyOf<DatabaseContext>() // Can load from multiple assemblies within one Scan() .AddClasses() .AsImplementedInterfaces() );
با کد زیر
public class Book { public string Title { get; set; } public int Id { get; set; } public Author Author { get; set; } public Publisher Publisher { get; set; } public IList<Book> GetBooks() { var books = new List<Book>(); for (int i = 0; i < 10; i++) { var book = new Book() { Id = i, Title = $"Title {i}", Author = new Author() { FirstName = $"Author {i} First Name", LastName = $"Author {i} Last Name", Person = new Person() { Type = $"Type {i}", Value = $"Value {i}" } }, Publisher = new Report.Publisher() { Name = $"Publiser {i}" } }; books.Add(book); } return books; } } public class Author { public string FirstName { get; set; } public string LastName { get; set; } public Person Person { get; set; } } public class Person { public string Type { get; set; } public string Value { get; set; } } public class Publisher { public string Name { get; set; } }
و فایل mrt زیر مشکلی دیده نشد.
مطالب
Blazor 5x - قسمت 19 - کار با فرمها - بخش 7 - نکات ویژهی کار با EF-Core در برنامههای Blazor Server
تا قسمت قبل، روشی را که برای کار با EF-Core درنظر گرفتیم، روش متداول کار با آن، در برنامههای ASP.NET Core Web API بود؛ یعنی این روش با برنامههای مبتنی بر Blazor WASM که از دو قسمت مجزای Web API سمت سرور و Web Assembly سمت کلاینت تشکیل شدهاند، به خوبی جواب میدهد؛ اما ... با Blazor Server یکپارچه که تمام قسمتهای مدیریتی آن سمت سرور رخ میدهند، خیر! در این مطلب، دلایل این موضوع را به همراه ارائه راهحلی، بررسی خواهیم کرد.
طول عمر سرویسها، در برنامههای Blazor Server متفاوت هستند
هنگامیکه با یک ASP.NET Core Web API متداول کار میکنیم، درخواستهای HTTP رسیده، از میانافزارهای موجود رد شده و پردازش میشوند. اما هنگامیکه با Blazor Server کار میکنیم، به علت وجود یک اتصال دائم SignalR که عموما از نوع Web socket است، دیگر درخواست HTTP وجود ندارد. تمام رفت و برگشتهای برنامه به سرور و پاسخهای دریافتی، از طریق Web socket منتقل میشوند و نه درخواستها و پاسخهای متداول HTTP.
این روش پردازشی، اولین تاثیری را که بر روی رفتار یک برنامه میگذارد، تغییر طول عمر سرویسهای آن است. برای مثال در برنامههای Web API، طول عمر درخواستها، از نوع Scoped هستند و با شروع پردازش یک درخواست، سرویسهای مورد نیاز وهله سازی شده و در پایان درخواست، رها میشوند.
این مساله در حین کار با EF-Core نیز بسیار مهم است؛ از این جهت که در برنامههای Web API نیز EF-Core و DbContext آن، به صورت سرویسهایی با طول عمر Scoped تعریف میشوند. برای مثال زمانیکه یک چنین تعریفی را در برنامه داریم:
امضای واقعی متد AddDbContext مورد استفادهی فوق، به صورت زیر است:
همانطور که مشاهده میکنید، طول عمرهای پیشفرض تعریف شدهی در اینجا، از نوع Scoped هستند. یعنی زمانیکه سرویسهای ApplicationDbContext را از طریق سیستم تزریق وابستگیهای برنامه دریافت میکنیم، در ابتدای یک درخواست Web API، به صورت خودکار وهله سازی شده و در پایان درخواست رها میشوند. به این ترتیب به ازای هر درخواست رسیده، وهلهی متفاوتی از DbContex را دریافت میکنیم که با وهلهی استفاده شدهی در درخواست قبلی، یکی نیست.
اما زمانیکه مانند یک برنامهی مبتنی بر Blazor Server، دیگر HTTP Requests متداولی را نداریم، چطور؟ در این حالت زمانیکه یک اتصال SignalR برقرار شد، وهلهای از DbContext که در اختیار برنامهی Blazor Server قرار میگیرد، تا زمانیکه کاربر این اتصال را به نحوی قطع نکرده (مانند بستن کامل مرورگر و یا ریفرش صفحه)، ثابت باقی خواهد ماند. یعنی به ازای هر اتصال SignalR، طول عمر ServiceLifetime.Scoped پیشفرض تعریف شده، همانند یک وهلهی با طول عمر Singleton عمل میکند. در این حالت تمام صفحات و کامپوننتهای یک برنامهی Blazor Server، از یک تک وهلهی مشخص DbContext که در ابتدای کار دریافت کردهاند، کار میکنند و از آنجائیکه DbContext به صورت thread-safe کار نمیکند، این تک وهله مشکلات زیادی را ایجاد خواهد کرد که یک نمونه از آنرا در عمل، در پایان قسمت قبل مشاهده کردید:
«اگر برنامه را اجرا کرده و سعی در حذف یک ردیف کنیم، به خطای زیر میرسیم و یا حتی اگر کاربر شروع کند به کلیک کردن سریع در قسمتهای مختلف برنامه، باز هم این خطا مشاهده میشود:
عنوان میکند که متد OnConfirmDeleteRoomClicked، بر روی ترد دیگری نسبت به ترد اولیهای که DbContext بر روی آن ایجاد شده، در حال اجرا است و چون DbContext برای یک چنین سناریوهایی، thread-safe نیست، اجازهی استفادهی از آنرا نمیدهد.»
هر درخواست Web API نیز بر روی یک ترد جداگانه اجرا میشود؛ اما چون ابتدا و انتهای درخواستها مشخص است، طول عمر Scoped، در ابتدای درخواست شروع شده و در پایان آن رها سازی میشود. به همین جهت استثنائی را که در اینجا مشاهده میکنید، در برنامههای Web API شاید هیچگاه مشاهده نشود.
معرفی DbContextFactory در EF Core 5x
همواره باید طول عمر DbContext را تا جای ممکن، کوتاه نگه داشت. مشکل فعلی ما، Singleton رفتار کردن DbContextها (داشتن طول عمر طولانی) در برنامههای Blazor Server هستند. یک چنین رفتاری را شاید در برنامههای دسکتاپ هم پیشتر مشاهده کرده باشید. برای مثال در برنامههای دسکتاپ WPF، تا زمانیکه یک فرم باز است، Context ایجاد شدهی در آن هم برقرار است و Dispose نمیشود. در یک چنین حالتهایی، عموما Context را در زمان نیاز، ایجاد کرده و پس از پایان آن کار کوتاه، Context را رها میکنند. به همین جهت نیاز به DbContext Factory ای وجود دارد که بتواند یک چنین پیاده سازیهایی را میسر کند و خوشبختانه از زمان EF Core 5x، یک چنین امکانی خصوصا برای برنامههای Blazor Server تحت عنوان DbContextFactory ارائه شدهاست که به عنوان راه حل استاندارد دسترسی به DbContext در اینگونه برنامهها مورد استفاده قرار میگیرد.
برای کار با DbContextFactory، اینبار در فایل BlazorServer.App\Startup.cs، بجای استفاده از services.AddDbContext، از متد AddDbContextFactory استفاده میشود:
سپس باید دقت داشت که روش استفادهی از آن، نسبت به کار مستقیم با ApplicationDbContext، کاملا متفاوت است. هدف از DbContextFactory، ساخت دستی Context در زمان نیاز و سپس Dispose صریح آن است. بنابراین طول عمر Context دریافت شدهی توسط آن باید توسط برنامه نویس مدیریت شود و به صورت خودکار توسط IoC Container برنامه مدیریت نخواهد شد. در این حالت دو روش استفادهی از آن در کامپوننتهای برنامههای Blazor Server، پیشنهاد میشود.
روش اول کار با DbContextFactory در کامپوننتهای Blazor Server : وهله سازی از نو، به ازای هر متد
در این روش پس از ثبت AddDbContextFactory در فایل Startup برنامه مانند مثال فوق، ابتدا سرویس IDbContextFactory که به ApplicationDbContext اشاره میکند به ابتدای کامپوننت تزریق میشود:
سپس در هر جائی که نیاز به وهلهای از ApplicationDbContext است، آنرا به صورت دستی وهله سازی کرده و همانجا هم Dispose میکنیم:
در اینجا یکی متدهای یک کامپوننت فرضی را مشاهده میکند که از DbFactory تزریق شده استفاده کرد و سپس با استفاده از متد ()CreateDbContext، وهلهی جدیدی از ApplicationDbContext را ایجاد میکند. همچنین در همان سطر، وجود عبارت using نیز مشاهده میشود. یعنی در پایان کار این متد، context ایجاد شده حتما Dispose شده و طول عمر کوتاهی خواهد داشت.
روش دوم کار با DbContextFactory در کامپوننتهای Blazor Server : یکبار وهله سازی Context به ازای هر کامپوننت
در این روش میتوان طول عمر Context را معادل طول عمر کامپوننت تعریف کرد که مزیت استفادهی از Change tracking موجود در EF-Core را به همراه خواهد داشت. در این حالت کامپوننتهای Blazor Server، شبیه به فرمهای برنامههای دسکتاپ عمل میکنند:
- در اینجا همانند روش اول، کار با تزریق IDbContextFactory شروع میشود
- اما بجای اینکه به ازای هر متد، کار فراخوانی DbFactory.CreateDbContext صورت گیرد، یکبار در آغاز کار کامپوننت و در روال رویدادگردان OnInitializedAsync، کار وهله سازی Context کامپوننت انجام شده و از این تک Context در تمام متدهای کامپوننت استفاده خواهد شد.
- در این حالت کار Dispose خودکار این Context به متد Dispose نهایی کل کامپوننت واگذار شدهاست. برای اینکه این متد فراخوانی شود، نیاز است در ابتدای تعاریف کامپوننت، از دایرکتیو implements IDisposable@ استفاده کرد.
سؤال: اگر سرویسی از ApplicationDbContext تزریق شدهی در سازندهی خود استفاده میکند، چکار باید کرد؟
برای نمونه سرویسهای از پیش تعریف شدهی ASP.NET Core Identity، در سازندهی خود از ApplicationDbContext استفاده میکنند و نه از IDbContextFactory. در این حالت برای تامین ApplicationDbContextهای تزریق شده، فقط کافی است از روش زیر استفاده کنیم:
در این حالت به ازای هر Scope تعریف شدهی در برنامه، جهت دسترسی به ApplicationDbContext از طریق سیستم تزریق وابستگیها، کار فراخوانی DbFactory.CreateDbContext به صورت خودکار انجام خواهد شد.
سؤال: روش پیاده سازی سرویسهای یک برنامه Blazor Server به چه صورتی باید تغییر کند؟
تا اینجا روشهایی که برای استفاده از IDbContextFactory معرفی شدند (که روشهای رسمی و توصیه شدهی اینکار نیز هستند)، فرض را بر این گذاشتهاند که ما قرار است تمام منطق تجاری کار با بانک اطلاعاتی را داخل همان متدهای کامپوننتها انجام دهیم (این روش برنامه نویسی، بسیار مورد علاقهی مایکروسافت است و در تمام مثالهای رسمی آن به صورت ضمنی توصیه میشود!). اما اگر همانند مثالی که تاکنون در این سری بررسی کردیم، نخواهیم اینکار را انجام دهیم و علاقمند باشیم تا این منطق تجاری را به سرویسهای مجزایی، با مسئولیتهای مشخصی انتقال دهیم، روش استفادهی از IDbContextFactory چگونه خواهد بود؟
در این حالت از ترکیب روش دوم مطرح شدهی استفاده از IDbContextFactory که به همراه مزیت دسترسی کامل به Change Tracking توکار EF-Core و پیاده سازی الگوی واحد کار است و وهله سازی خودکار ApplicationDbContext که معرفی شد، استفاده خواهیم کرد؛ به این صورت:
الف) تمام سرویسهای EF-Core یک برنامهی Blazor Server باید اینترفیس IDisposable را پیاده سازی کنند.
این مورد برای سرویسهای پروژههای Web API، ضروری نیست؛ چون طول عمر Context آنها توسط خود IoC Container مدیریت میشود؛ اما در برنامههای Blazor Server، مطابق توضیحاتی که ارائه شد، خودمان باید این طول عمر را مدیریت کنیم.
بنابراین به پروژهی سرویسهای برنامه مراجعه کرده و هر سرویسی که ApplicationDbContext تزریق شدهای را در سازندهی خود میپذیرد، یافته و تعریف اینترفیس آنرا به صورت زیر تغییر میدهیم:
سپس باید اینترفیسهای IDisposable را پیاده سازی کرد که روش مورد پذیرش code analyzerها در این زمینه، رعایت الگوی زیر، دقیقا به همین شکل است و باید از دو متد تشکیل شود:
این الگو را به همین شکل برای سرویس HotelRoomImageService نیز پیاده سازی میکنیم.
ب) Dispose دستی تمام سرویسها، در کامپوننتهای مرتبط
در ادامه تمام کامپوننتهایی را که از سرویسهای فوق استفاده میکنند یافته و ابتدا دایرکتیو implements IDisposable@ را به ابتدای آنها اضافه میکنیم. سپس متد Dispose آنها را جهت فراخوانی متد Dispose سرویسهای فوق، تکمیل خواهیم کرد:
بنابراین ابتدا به فایل BlazorServer\BlazorServer.App\Pages\HotelRoom\HotelRoomUpsert.razor مراجعه کرده و تغییرات زیر را اعمال میکنیم:
و همچنین به کامپوننت BlazorServer\BlazorServer.App\Pages\HotelRoom\HotelRoomList.razor مراجعه کرده و آنرا به صورت زیر جهت Dispose دستی سرویسها، تکمیل میکنیم:
مشکل! اینبار خطای dispose شدن context را دریافت میکنیم!
هم برنامههای Blazor WASM و هم برنامههای Blazor Server از مفهوم طول عمرهای تنظیم شدهی سرویسها پشتیبانی نمیکنند! در هر دوی اینها اگر سرویسی را با طول عمر Scoped تنظیم کردیم، رفتار آن همانند سرویسهای Singleton خواهد بود. تنها زمانی رفتارهای Scoped و یا Transient پشتیبانی میشوند که درخواست HTTP ای رخ داده باشد که این مورد خارج است از طول عمر یک برنامهی Blazor WASM و همچنین اتصال SignalR برنامههای Blazor Server. فقط قسمتهایی از برنامهی Blazor Server که با مدل قبلی Razor pages طراحی شدهاند، چون سبب شروع یک درخواست HTTP معمولی میشوند، همانند برنامههای متداول ASP.NET Core رفتار میکنند و در این حالت طول عمرهای غیر Singleton مفهوم پیدا میکنند.
مشکلی که در اینجا رخ داده این است که سرویسهایی را داریم با طول عمر به ظاهر Scoped که یکی از وابستگیهای آنها را به صورت دستی Dispose کردهایم. چون طول عمر Scoped در اینجا وجود ندارد و طول عمرها در اصل Singleton هستند، هربار که سرویس مدنظر مجددا درخواست شود، همان وهلهی ابتدایی که اکنون یکی از وابستگیهای آن Dispose شده، در اختیار برنامه قرار میگیرد.
پس از این تغییرات، اولین باری که برنامه را اجرا میکنیم، لیست اتاقها به خوبی نمایش داده میشوند و مشکلی نیست. بعد در همین حال و در همین صفحه، اگر بر روی دکمهی افزودن یک اتاق جدید کلیک کنیم، اتفاقی که رخ میدهد، فراخوانی متد Dispose کامپوننت لیست اتاقها است (بر روی آن یک break-point قرار دهید). بنابراین متد Dispose یک کامپوننت، با هدایت به یک مسیر دیگر، به صورت خودکار فراخوانی میشود. در این حالت Context برنامه Dispose شده و در کامپوننت ثبت یک اتاق جدید دیگر، در دسترس نخواهد بود؛ چون IHotelRoomService مورد استفاده مجددا وهله سازی نمیشود و از همان وهلهای که بار اول ایجاد شده، استفاده خواهد شد.
بنابراین سؤال اینجا است که چگونه میتوان سیستم تزریق وابستگیها را وادار کرد تا تمام سرویسهای تزریق شدهی به سازندههای سرویسهای HotelRoomService و HotelRoomImageService را مجددا وهله سازی کند و سعی نکند از همان وهلههای قبلی استفاده کند؟
پاسخ: یک روش این است که IHotelRoomImageService را خودمان به ازای هر کامپوننت به صورت دستی در روال رویدادگردان OnInitializedAsync وهله سازی کرده و DbFactory.CreateDbContext جدیدی را مستقیما به سازندهی آن ارسال کنیم. در این حالت مطمئن خواهیم شد که این وهله، جای دیگری به اشتراک گذاشته نمیشود:
هرچند این روش کار میکند، اما در زمان استفاده از IoC Containerها قرار نیست کار انجام newها را خودمان به صورت دستی انجام دهیم و بهتر است مدیریت این مساله به آنها واگذار شود.
وادار کردن Blazor Server به وهله سازی مجدد سرویسهای کامپوننتها
بنابراین مشکل ما Singleton رفتار کردن سرویسها، در برنامههای Blazor است. برای مثال در برنامههای Blazor Server، تا زمانیکه اتصال SignalR برنامه برقرار است (مرورگر بسته نشده، برگهی جاری بسته نشده و یا کاربر صفحه را ریفرش نکرده)، هیچ سرویسی دوباره وهله سازی نمیشود.
برای رفع این مشکل، امکان Scoped رفتار کردن سرویسهای یک کامپوننت نیز در نظر گرفته شدهاند. برای نمونه کدهای کامپوننت HotelRoomList.razor را به صورت زیر تغییر میدهیم:
با استفاده از دایرکتیو جدید inherits OwningComponentBase@ میتوان میدان دید یک سرویس را به طول عمر کامپوننت جاری محدود کرد. هربار که این کامپوننت نمایش داده میشود، وهله سازی شده و هربار که به کامپوننت دیگری هدایت میشویم، به صورت خودکار سرویس مورد استفاده را Dispose میکند. بنابراین در اینجا دیگر نیازی به ذکر دایرکتیو implements IDisposable@ نیست.
چند نکته:
- فقط یکبار به ازای هر کامپوننت میتوان از دایرکتیو inherits استفاده کرد.
- زمانیکه طول عمر سرویسی را توسط OwningComponentBase مدیریت میکنیم، در حقیقت یک کلاس پایه را برای آن کامپوننت درنظر گرفتهایم که به همراه یک خاصیت عمومی ویژه، به نام Service و از نوع سرویس مدنظر ما است. در این حالت یا میتوان از خاصیت Service به صورت مستقیم استفاده کرد و یا میتوان به صورت زیر، همان کدهای قبلی را داشت و هربار که نیازی به HotelRoomService بود، آنرا به خاصیت عمومی Service هدایت کرد:
- اگر نیاز به بیش از یک سرویس وجود داشت، چون نمیتوان بیش از یک inherits را تعریف کرد، میتوان از نمونهی غیرجنریک OwningComponentBase استفاده کرد:
در این حالت کلاس پایهی OwningComponentBase، به همراه خاصیت جدید ScopedServices است که با فراخوانی متد GetRequiredService در روال رویدادگردان OnInitialized بر روی آن، سبب وهله سازی Scoped سرویس مدنظر خواهد شد. نمونهی جنریک آن، تمام این موارد را در پشت صحنه انجام میدهد و کار کردن با آن سادهتر و خلاصهتر است.
خلاصهی بحث جاری در مورد روش مدیریت DbContext برنامههای Blazor Server:
- بجای services.AddDbContext متداول، باید از AddDbContextFactory استفاده کرد:
- تمام سرویسهایی که از ApplicationDbContext استفاده میکنند، باید به همراه پیاده سازی Dispose آن نیز باشند؛ چون Scope یک سرویس، معادل طول عمر اتصال SignalR برنامه است و مدام وهله سازی نمیشود. در این حالت باید وهله سازی و Dispose آنرا دستی مدیریت کرد.
- کامپوننتهای برنامه، سرویسهایی را که باید Scoped عمل کنند، دیگر نباید از طریق تزریق مستقیم آنها دریافت کنند؛ چون در این حالت همواره به همان وهلهای که در ابتدای کار ایجاد شده، میرسیم:
این دریافت باید با استفاده از کلاس پایه OwningComponentBase صورت گیرد:
تا عملیات فراخوانی خودکار ScopedServices.GetRequiredService (دریافت وهلهی جدید Scoped) و همچنین Dispose خودکار آنها را به ازای هر کامپوننت مجزا، مدیریت کند.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-19.zip
طول عمر سرویسها، در برنامههای Blazor Server متفاوت هستند
هنگامیکه با یک ASP.NET Core Web API متداول کار میکنیم، درخواستهای HTTP رسیده، از میانافزارهای موجود رد شده و پردازش میشوند. اما هنگامیکه با Blazor Server کار میکنیم، به علت وجود یک اتصال دائم SignalR که عموما از نوع Web socket است، دیگر درخواست HTTP وجود ندارد. تمام رفت و برگشتهای برنامه به سرور و پاسخهای دریافتی، از طریق Web socket منتقل میشوند و نه درخواستها و پاسخهای متداول HTTP.
این روش پردازشی، اولین تاثیری را که بر روی رفتار یک برنامه میگذارد، تغییر طول عمر سرویسهای آن است. برای مثال در برنامههای Web API، طول عمر درخواستها، از نوع Scoped هستند و با شروع پردازش یک درخواست، سرویسهای مورد نیاز وهله سازی شده و در پایان درخواست، رها میشوند.
این مساله در حین کار با EF-Core نیز بسیار مهم است؛ از این جهت که در برنامههای Web API نیز EF-Core و DbContext آن، به صورت سرویسهایی با طول عمر Scoped تعریف میشوند. برای مثال زمانیکه یک چنین تعریفی را در برنامه داریم:
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString));
public static IServiceCollection AddDbContext<TContext>( [NotNullAttribute] this IServiceCollection serviceCollection, [CanBeNullAttribute] Action<DbContextOptionsBuilder> optionsAction = null, ServiceLifetime contextLifetime = ServiceLifetime.Scoped, ServiceLifetime optionsLifetime = ServiceLifetime.Scoped) where TContext : DbContext;
اما زمانیکه مانند یک برنامهی مبتنی بر Blazor Server، دیگر HTTP Requests متداولی را نداریم، چطور؟ در این حالت زمانیکه یک اتصال SignalR برقرار شد، وهلهای از DbContext که در اختیار برنامهی Blazor Server قرار میگیرد، تا زمانیکه کاربر این اتصال را به نحوی قطع نکرده (مانند بستن کامل مرورگر و یا ریفرش صفحه)، ثابت باقی خواهد ماند. یعنی به ازای هر اتصال SignalR، طول عمر ServiceLifetime.Scoped پیشفرض تعریف شده، همانند یک وهلهی با طول عمر Singleton عمل میکند. در این حالت تمام صفحات و کامپوننتهای یک برنامهی Blazor Server، از یک تک وهلهی مشخص DbContext که در ابتدای کار دریافت کردهاند، کار میکنند و از آنجائیکه DbContext به صورت thread-safe کار نمیکند، این تک وهله مشکلات زیادی را ایجاد خواهد کرد که یک نمونه از آنرا در عمل، در پایان قسمت قبل مشاهده کردید:
«اگر برنامه را اجرا کرده و سعی در حذف یک ردیف کنیم، به خطای زیر میرسیم و یا حتی اگر کاربر شروع کند به کلیک کردن سریع در قسمتهای مختلف برنامه، باز هم این خطا مشاهده میشود:
An exception occurred while iterating over the results of a query for context type 'BlazorServer.DataAccess.ApplicationDbContext'. System.InvalidOperationException: A second operation was started on this context before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
هر درخواست Web API نیز بر روی یک ترد جداگانه اجرا میشود؛ اما چون ابتدا و انتهای درخواستها مشخص است، طول عمر Scoped، در ابتدای درخواست شروع شده و در پایان آن رها سازی میشود. به همین جهت استثنائی را که در اینجا مشاهده میکنید، در برنامههای Web API شاید هیچگاه مشاهده نشود.
معرفی DbContextFactory در EF Core 5x
همواره باید طول عمر DbContext را تا جای ممکن، کوتاه نگه داشت. مشکل فعلی ما، Singleton رفتار کردن DbContextها (داشتن طول عمر طولانی) در برنامههای Blazor Server هستند. یک چنین رفتاری را شاید در برنامههای دسکتاپ هم پیشتر مشاهده کرده باشید. برای مثال در برنامههای دسکتاپ WPF، تا زمانیکه یک فرم باز است، Context ایجاد شدهی در آن هم برقرار است و Dispose نمیشود. در یک چنین حالتهایی، عموما Context را در زمان نیاز، ایجاد کرده و پس از پایان آن کار کوتاه، Context را رها میکنند. به همین جهت نیاز به DbContext Factory ای وجود دارد که بتواند یک چنین پیاده سازیهایی را میسر کند و خوشبختانه از زمان EF Core 5x، یک چنین امکانی خصوصا برای برنامههای Blazor Server تحت عنوان DbContextFactory ارائه شدهاست که به عنوان راه حل استاندارد دسترسی به DbContext در اینگونه برنامهها مورد استفاده قرار میگیرد.
برای کار با DbContextFactory، اینبار در فایل BlazorServer.App\Startup.cs، بجای استفاده از services.AddDbContext، از متد AddDbContextFactory استفاده میشود:
public void ConfigureServices(IServiceCollection services) { var connectionString = Configuration.GetConnectionString("DefaultConnection"); //services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString)); services.AddDbContextFactory<ApplicationDbContext>(options => options.UseSqlServer(connectionString));
روش اول کار با DbContextFactory در کامپوننتهای Blazor Server : وهله سازی از نو، به ازای هر متد
در این روش پس از ثبت AddDbContextFactory در فایل Startup برنامه مانند مثال فوق، ابتدا سرویس IDbContextFactory که به ApplicationDbContext اشاره میکند به ابتدای کامپوننت تزریق میشود:
@inject IDbContextFactory<ApplicationDbContext> DbFactory
private async Task DeleteImageAsync() { using var context = DbFactory.CreateDbContext(); var image = await context.HotelRoomImages.FindAsync(1); // ... }
روش دوم کار با DbContextFactory در کامپوننتهای Blazor Server : یکبار وهله سازی Context به ازای هر کامپوننت
در این روش میتوان طول عمر Context را معادل طول عمر کامپوننت تعریف کرد که مزیت استفادهی از Change tracking موجود در EF-Core را به همراه خواهد داشت. در این حالت کامپوننتهای Blazor Server، شبیه به فرمهای برنامههای دسکتاپ عمل میکنند:
@implements IDisposable @inject IDbContextFactory<ApplicationDbContext> DbFactory @code { private ApplicationDbContext Context; protected override async Task OnInitializedAsync() { Context = DbFactory.CreateDbContext(); await base.OnInitializedAsync(); } private async Task DeleteImageAsync() { var image = await Context.HotelRoomImages.FindAsync(1); // ... } public void Dispose() { Context.Dispose(); } }
- اما بجای اینکه به ازای هر متد، کار فراخوانی DbFactory.CreateDbContext صورت گیرد، یکبار در آغاز کار کامپوننت و در روال رویدادگردان OnInitializedAsync، کار وهله سازی Context کامپوننت انجام شده و از این تک Context در تمام متدهای کامپوننت استفاده خواهد شد.
- در این حالت کار Dispose خودکار این Context به متد Dispose نهایی کل کامپوننت واگذار شدهاست. برای اینکه این متد فراخوانی شود، نیاز است در ابتدای تعاریف کامپوننت، از دایرکتیو implements IDisposable@ استفاده کرد.
سؤال: اگر سرویسی از ApplicationDbContext تزریق شدهی در سازندهی خود استفاده میکند، چکار باید کرد؟
برای نمونه سرویسهای از پیش تعریف شدهی ASP.NET Core Identity، در سازندهی خود از ApplicationDbContext استفاده میکنند و نه از IDbContextFactory. در این حالت برای تامین ApplicationDbContextهای تزریق شده، فقط کافی است از روش زیر استفاده کنیم:
services.AddScoped<ApplicationDbContext>(serviceProvider => serviceProvider.GetRequiredService<IDbContextFactory<ApplicationDbContext>>().CreateDbContext());
سؤال: روش پیاده سازی سرویسهای یک برنامه Blazor Server به چه صورتی باید تغییر کند؟
تا اینجا روشهایی که برای استفاده از IDbContextFactory معرفی شدند (که روشهای رسمی و توصیه شدهی اینکار نیز هستند)، فرض را بر این گذاشتهاند که ما قرار است تمام منطق تجاری کار با بانک اطلاعاتی را داخل همان متدهای کامپوننتها انجام دهیم (این روش برنامه نویسی، بسیار مورد علاقهی مایکروسافت است و در تمام مثالهای رسمی آن به صورت ضمنی توصیه میشود!). اما اگر همانند مثالی که تاکنون در این سری بررسی کردیم، نخواهیم اینکار را انجام دهیم و علاقمند باشیم تا این منطق تجاری را به سرویسهای مجزایی، با مسئولیتهای مشخصی انتقال دهیم، روش استفادهی از IDbContextFactory چگونه خواهد بود؟
در این حالت از ترکیب روش دوم مطرح شدهی استفاده از IDbContextFactory که به همراه مزیت دسترسی کامل به Change Tracking توکار EF-Core و پیاده سازی الگوی واحد کار است و وهله سازی خودکار ApplicationDbContext که معرفی شد، استفاده خواهیم کرد؛ به این صورت:
الف) تمام سرویسهای EF-Core یک برنامهی Blazor Server باید اینترفیس IDisposable را پیاده سازی کنند.
این مورد برای سرویسهای پروژههای Web API، ضروری نیست؛ چون طول عمر Context آنها توسط خود IoC Container مدیریت میشود؛ اما در برنامههای Blazor Server، مطابق توضیحاتی که ارائه شد، خودمان باید این طول عمر را مدیریت کنیم.
بنابراین به پروژهی سرویسهای برنامه مراجعه کرده و هر سرویسی که ApplicationDbContext تزریق شدهای را در سازندهی خود میپذیرد، یافته و تعریف اینترفیس آنرا به صورت زیر تغییر میدهیم:
public interface IHotelRoomService : IDisposable { // ... } public interface IHotelRoomImageService : IDisposable { // ... }
public class HotelRoomService : IHotelRoomService { private bool _isDisposed; // ... public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (!_isDisposed) { try { if (disposing) { _dbContext.Dispose(); } } finally { _isDisposed = true; } } } }
ب) Dispose دستی تمام سرویسها، در کامپوننتهای مرتبط
در ادامه تمام کامپوننتهایی را که از سرویسهای فوق استفاده میکنند یافته و ابتدا دایرکتیو implements IDisposable@ را به ابتدای آنها اضافه میکنیم. سپس متد Dispose آنها را جهت فراخوانی متد Dispose سرویسهای فوق، تکمیل خواهیم کرد:
بنابراین ابتدا به فایل BlazorServer\BlazorServer.App\Pages\HotelRoom\HotelRoomUpsert.razor مراجعه کرده و تغییرات زیر را اعمال میکنیم:
@page "/hotel-room/create" @page "/hotel-room/edit/{Id:int}" @implements IDisposable // ... @code { // ... public void Dispose() { HotelRoomImageService.Dispose(); HotelRoomService.Dispose(); } }
@page "/hotel-room" @implements IDisposable // ... @code { // ... public void Dispose() { HotelRoomService.Dispose(); } }
مشکل! اینبار خطای dispose شدن context را دریافت میکنیم!
System.ObjectDisposedException: Cannot access a disposed context instance. A common cause of this error is disposing a context instance that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling 'Dispose' on the context instance, or wrapping it in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances. Object name: 'ApplicationDbContext'.
مشکلی که در اینجا رخ داده این است که سرویسهایی را داریم با طول عمر به ظاهر Scoped که یکی از وابستگیهای آنها را به صورت دستی Dispose کردهایم. چون طول عمر Scoped در اینجا وجود ندارد و طول عمرها در اصل Singleton هستند، هربار که سرویس مدنظر مجددا درخواست شود، همان وهلهی ابتدایی که اکنون یکی از وابستگیهای آن Dispose شده، در اختیار برنامه قرار میگیرد.
پس از این تغییرات، اولین باری که برنامه را اجرا میکنیم، لیست اتاقها به خوبی نمایش داده میشوند و مشکلی نیست. بعد در همین حال و در همین صفحه، اگر بر روی دکمهی افزودن یک اتاق جدید کلیک کنیم، اتفاقی که رخ میدهد، فراخوانی متد Dispose کامپوننت لیست اتاقها است (بر روی آن یک break-point قرار دهید). بنابراین متد Dispose یک کامپوننت، با هدایت به یک مسیر دیگر، به صورت خودکار فراخوانی میشود. در این حالت Context برنامه Dispose شده و در کامپوننت ثبت یک اتاق جدید دیگر، در دسترس نخواهد بود؛ چون IHotelRoomService مورد استفاده مجددا وهله سازی نمیشود و از همان وهلهای که بار اول ایجاد شده، استفاده خواهد شد.
بنابراین سؤال اینجا است که چگونه میتوان سیستم تزریق وابستگیها را وادار کرد تا تمام سرویسهای تزریق شدهی به سازندههای سرویسهای HotelRoomService و HotelRoomImageService را مجددا وهله سازی کند و سعی نکند از همان وهلههای قبلی استفاده کند؟
پاسخ: یک روش این است که IHotelRoomImageService را خودمان به ازای هر کامپوننت به صورت دستی در روال رویدادگردان OnInitializedAsync وهله سازی کرده و DbFactory.CreateDbContext جدیدی را مستقیما به سازندهی آن ارسال کنیم. در این حالت مطمئن خواهیم شد که این وهله، جای دیگری به اشتراک گذاشته نمیشود:
@code { private IHotelRoomImageService HotelRoomImageService; protected override async Task OnInitializedAsync() { HotelRoomImageService = new HotelRoomImageService(DbFactory.CreateDbContext(), mapper); await base.OnInitializedAsync(); } private async Task DeleteImageAsync() { await HotelRoomImageService.DeleteAsync(1); // ... } public void Dispose() { HotelRoomImageService.Dispose(); } }
وادار کردن Blazor Server به وهله سازی مجدد سرویسهای کامپوننتها
بنابراین مشکل ما Singleton رفتار کردن سرویسها، در برنامههای Blazor است. برای مثال در برنامههای Blazor Server، تا زمانیکه اتصال SignalR برنامه برقرار است (مرورگر بسته نشده، برگهی جاری بسته نشده و یا کاربر صفحه را ریفرش نکرده)، هیچ سرویسی دوباره وهله سازی نمیشود.
برای رفع این مشکل، امکان Scoped رفتار کردن سرویسهای یک کامپوننت نیز در نظر گرفته شدهاند. برای نمونه کدهای کامپوننت HotelRoomList.razor را به صورت زیر تغییر میدهیم:
@page "/hotel-room" @*@implements IDisposable*@ @*@inject IHotelRoomService HotelRoomService*@ @inherits OwningComponentBase<IHotelRoomService>
چند نکته:
- فقط یکبار به ازای هر کامپوننت میتوان از دایرکتیو inherits استفاده کرد.
- زمانیکه طول عمر سرویسی را توسط OwningComponentBase مدیریت میکنیم، در حقیقت یک کلاس پایه را برای آن کامپوننت درنظر گرفتهایم که به همراه یک خاصیت عمومی ویژه، به نام Service و از نوع سرویس مدنظر ما است. در این حالت یا میتوان از خاصیت Service به صورت مستقیم استفاده کرد و یا میتوان به صورت زیر، همان کدهای قبلی را داشت و هربار که نیازی به HotelRoomService بود، آنرا به خاصیت عمومی Service هدایت کرد:
@code { private IHotelRoomService HotelRoomService => Service;
@page "/preferences" @using Microsoft.Extensions.DependencyInjection @inherits OwningComponentBase @code { private IHotelRoomService HotelRoomService { get; set; } private IHotelRoomImageService HotelRoomImageService { get; set; } protected override void OnInitialized() { HotelRoomService = ScopedServices.GetRequiredService<IHotelRoomService>(); HotelRoomImageService = ScopedServices.GetRequiredService<IHotelRoomImageService>(); } }
خلاصهی بحث جاری در مورد روش مدیریت DbContext برنامههای Blazor Server:
- بجای services.AddDbContext متداول، باید از AddDbContextFactory استفاده کرد:
services.AddDbContextFactory<ApplicationDbContext>(options => options.UseSqlServer(connectionString)); services.AddScoped<ApplicationDbContext>(serviceProvider => serviceProvider.GetRequiredService<IDbContextFactory<ApplicationDbContext>>().CreateDbContext());
- کامپوننتهای برنامه، سرویسهایی را که باید Scoped عمل کنند، دیگر نباید از طریق تزریق مستقیم آنها دریافت کنند؛ چون در این حالت همواره به همان وهلهای که در ابتدای کار ایجاد شده، میرسیم:
@inject IHotelRoomService HotelRoomService
@inherits OwningComponentBase<IHotelRoomService>
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-19.zip
در ادامه مطلب پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 1# به تشریح مابقی کلاسهای برنامه میپردازیم.
با توجه به تجزیه و تحلیل انجام شده تمامی اشیا از کلاس پایه به نام Shape ارث بری دارند حال به توضیح کدهای این کلاس میپردازیم. (به دلیل اینکه توضیحات این کلاس در دو پست نوشته خواهد شد برای این کلاسها از partial class استفاده شده است)
ابتدا به تشریح خصوصیات کلاس میپردازیم:
خصوصیات:
در پست بعدی به توضیح متدهای این کلاس میپردازیم.
با توجه به تجزیه و تحلیل انجام شده تمامی اشیا از کلاس پایه به نام Shape ارث بری دارند حال به توضیح کدهای این کلاس میپردازیم. (به دلیل اینکه توضیحات این کلاس در دو پست نوشته خواهد شد برای این کلاسها از partial class استفاده شده است)
using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Net; namespace PWS.ObjectOrientedPaint.Models { /// <summary> /// Shape (Base Class) /// </summary> public abstract partial class Shape { #region Fields (1) private Brush _backgroundBrush; #endregion Fields #region Properties (16) /// <summary> /// Gets or sets the brush. /// </summary> /// <value> /// The brush. /// </value> public Brush BackgroundBrush { get { return _backgroundBrush ?? (_backgroundBrush = new SolidBrush(BackgroundColor)); } private set { _backgroundBrush = value ?? new SolidBrush(BackgroundColor); } } /// <summary> /// Gets or sets the color of the background. /// </summary> /// <value> /// The color of the background. /// </value> public Color BackgroundColor { get; set; } /// <summary> /// Gets or sets the end point. /// </summary> /// <value> /// The end point. /// </value> public PointF EndPoint { get; set; } /// <summary> /// Gets or sets the color of the fore. /// </summary> /// <value> /// The color of the fore. /// </value> public Color ForeColor { get; set; } /// <summary> /// Gets or sets the height. /// </summary> /// <value> /// The height. /// </value> public float Height { get { return Math.Abs(StartPoint.Y - EndPoint.Y); } set { if (value > 0) EndPoint = new PointF(EndPoint.X, StartPoint.Y + value); } } /// <summary> /// Gets or sets a value indicating whether this instance is fill. /// </summary> /// <value> /// <c>true</c> if this instance is fill; otherwise, <c>false</c>. /// </value> public bool IsFill { get; set; } /// <summary> /// Gets or sets a value indicating whether this instance is selected. /// </summary> /// <value> /// <c>true</c> if this instance is selected; otherwise, <c>false</c>. /// </value> public bool IsSelected { get; set; } /// <summary> /// Gets or sets my pen. /// </summary> /// <value> /// My pen. /// </value> public Pen Pen { get { return new Pen(ForeColor, Thickness); } } /// <summary> /// Gets or sets the type of the shape. /// </summary> /// <value> /// The type of the shape. /// </value> public ShapeType ShapeType { get; protected set; } /// <summary> /// Gets the size. /// </summary> /// <value> /// The size. /// </value> public SizeF Size { get { return new SizeF(Width, Height); } } /// <summary> /// Gets or sets the start point. /// </summary> /// <value> /// The start point. /// </value> public PointF StartPoint { get; set; } /// <summary> /// Gets or sets the thickness. /// </summary> /// <value> /// The thickness. /// </value> public byte Thickness { get; set; } /// <summary> /// Gets or sets the width. /// </summary> /// <value> /// The width. /// </value> public float Width { get { return Math.Abs(StartPoint.X - EndPoint.X); } set { if (value > 0) EndPoint = new PointF(StartPoint.X + value, EndPoint.Y); } } /// <summary> /// Gets or sets the X. /// </summary> /// <value> /// The X. /// </value> public float X { get { return StartPoint.X; } set { if (value > 0) StartPoint = new PointF(value, StartPoint.Y); } } /// <summary> /// Gets or sets the Y. /// </summary> /// <value> /// The Y. /// </value> public float Y { get { return StartPoint.Y; } set { if (value > 0) StartPoint = new PointF(StartPoint.X, value); } } /// <summary> /// Gets or sets the index of the Z. /// </summary> /// <value> /// The index of the Z. /// </value> public int Zindex { get; set; } #endregion Properties } }
خصوصیات:
- BackgroundColor: در صورتی که شی مورد نظر به صورت توپررسم شود، این خاصیت رنگ پس زمینه شی را مشخص میکند.
- BackgroundBrush: خاصیتی است که با توجه به خاصیت BackgroundColor یک الگوی پر کردن زمینه شی میسازد.
- StartPoint: نقطه شروع شی را در خود نگهداری میکند.
- EndPoint: نقطه انتهای شی را در خود نگهداری میکند. (قبلا گفته شد که هر شی را در صورتی که در یک مستطیل فرض کنیم یک نقطه شروع و یک نقطه پایان دارد)
- ForeColor: رنگ قلم ترسیم شی مورد نظر را تعیین میکند.
- Height: ارتفاع شی مورد نظر را تعیین میکند ( این خصوصیت اختلاف عمودی StartPoint.Y و EndPoint.Y را محاسبه میکند و در زمان مقدار دهی EndPoint جدیدی ایجاد میکند).
- Width: عرض شی مورد نظر را تعیین میکند ( این خصوصیت اختلاف افقیStartPoint.X و EndPoint.X را محاسبه میکند و در زمان مقدار دهی EndPoint جدیدی ایجاد میکند).
- IsFill: این خصوصیت تعیین کننده توپر و یا توخالی بودن شی است.
- IsSelected: این خاصیت تعیین میکند که آیا شی انتخاب شده است یا خیر (در زمان انتخاب شی چهار مربع کوچک روی شی رسم میشود).
- Pen: قلم خط ترسیم شی را مشخص میکند. (قلم با ضخامت دلخواه)
- ShapeType: این خصوصیت نوع شی را مشخص میکند (این خاصیت بیشتر برای زمان پیش نمایش ترسیم شی در زمان اجراست البته به نظر خودم اضافه هست اما راه بهتری به ذهنم نرسید)
- Size: با استفاده از خصوصیات Height و Width ایجاد شده و تعیین کننده Size شی است.
- Thickness: ضخامت خط ترسیمی شی را مشخص میکند، این خاصیت در خصوصیت Pen استفاده شده است.
- X: مقدار افقی نقطه شروع شی را تعیین میکند در واقع StartPoint.X را برمیگرداند (این خاصیت اضافی بوده و جهت راحتی کار استفاده شده میتوان آن را ننوشت).
- Y: مقدار عمودی نقطه شروع شی را تعیین میکند در واقع StartPoint.Y را برمیگرداند (این خاصیت اضافی بوده و جهت راحتی کار استفاده شده میتوان آن را ننوشت).
- Zindex: در زمان ترسیم اشیا ممکن است اشیا روی هم ترسیم شوند، در واقع Zindex تعیین کننده عمق شی روی بوم گرافیکی است.
در پست بعدی به توضیح متدهای این کلاس میپردازیم.
پاسخ به بازخوردهای پروژهها
چند متد الحاقی پیشنهادی
متد پیش فرض دات نت برای بررسی مقادیر String مناسب است. شاید با ترکیب دو متد IsNullOrWhiteSpace و IsNullOrEmpty بتوان متد بهتری ساخت. متد پیشنهادی من به صورت زیر است:
/// <summary> /// It returns true if string is null or empty or just a white space otherwise it returns false. /// </summary> /// <param name="input">Input String</param> /// <returns>bool</returns> public static bool IsEmpty(this string input) { return string.IsNullOrEmpty(input) || string.IsNullOrWhiteSpace(input); }