در C# 7.2 میتوان با value types (مانند structs) همانند reference types (مانند کلاسها) رفتار کرد. جائیکه کارآیی برنامه بسیار حائز اهمیت باشد (مانند بازیها)، استفاده از structs و value types بسیار مرسوم است؛ از این جهت که این نوعها بر روی heap تخصیص داده نمیشوند. اما مشکل آنها این است که زمانیکه به متدها ارسال میشوند، مقدار آنها ارسال خواهد شد و برای این منظور نیاز به ایجاد یک کپی جدید از آنها میباشد. برای رفع این مشکل و کاهش سربار کپی کردن اشیاء، اکنون در C# 7.2 میتوان value types را همانند reference types به متدها ارسال کرد.
واژهی کلیدی جدید in
C# 7.2، واژهی کلیدی جدیدی را به نام in جهت تعریف پارامترها، معرفی کردهاست. زمانیکه از آن استفاده میشود به این معنا است که value type ارسالی به آن، توسط ارجاعی از آن، در اختیار متد قرار میگیرد و نه توسط مقدار کپی شدهی آن (حالت پیشفرض) و همچنین متد استفاده کنندهی از آن، مقدار این شیء را تغییر نمیدهد.
واژهی کلیدی in مکمل واژههای کلیدی ref و out است که پیشتر به همراه زبان #C ارائه شده بودند:
- واژهی کلیدی out: مقدار آرگومان مزین شدهی توسط آن، باید درون متد تنظیم شود و صرفا کاربرد ارائهی یک خروجی اضافهتر توسط آن متد را دارد.
- واژهی کلیدی ref: مقدار آرگومان مزین شدهی توسط آن، ممکن است درون متد تنظیم شود، یا خیر و همچنین توسط ارجاع به آن منتقل میشود.
- واژهی کلیدی in: مقدار آرگومان مزین شدهی توسط آن، درون متد تغییر نخواهد کرد و همچنین توسط ارجاع به آن منتقل میشود.
برای مثال اگر پارامترهای value type متد زیر را از نوع in معرفی کنیم، امکان تغییر مقدار آنها درون متد وجود نخواهد داشت:
و کامپایلر با صدور خطای readonly بودن پارامتر number1، از انجام اینکار جلوگیری میکند
واژهی کلیدی جدید in تا چه اندازهای بر روی کارآیی برنامه تاثیر دارد؟
زمانیکه یک value type را به متدی ارسال میکنیم، ابتدا به مکان جدیدی از حافظه کپی شده و سپس مقدار clone شدهی آن، به متد ارسال میشود. با استفاده از واژهی کلیدی in، دقیقا همان ارجاع به مقدار اولیه، به متد ارسال خواهد شد؛ بدون ایجاد کپی اضافهتری از آن. برای بررسی تاثیر این عملیات بر روی کارآیی برنامه، میتوان از BenchmarkDotNet استفاده کرد. برای این منظور ابتدا ارجاعی را به BenchmarkDotNet اضافه میکنیم:
سپس متدهایی را که قرار است کارآیی آنها بررسی شوند، با ویژگی Benchmark مزین خواهیم کرد:
در آخر برای اجرای آن خواهیم داشت:
و در این حالت برنامه را باید توسط دستور «dotnet run -c release» اجرا کرد (اندازه گیری کارآیی در حالت release و نه دیباگ پیشفرض)
با این خروجی نهایی:
همانطور که ملاحظه میکنید، کارآیی برنامه در حالت استفادهی از پارامترهای in، حداقل 3 برابر شدهاست.
امکان استفادهی از واژهی کلیدی in در حین تعریف متدهای الحاقی
در حین تعریف متدهای الحاقی، واژهی کلیدی in باید پیش از واژهی کلیدی this ذکر شود:
در این حالت اگر برنامه را به صورت زیر اجرا کنیم (یکبار با ذکر صریح in، بار دیگر بدون in و یکبار هم به صورت فراخوانی متد الحاقی بر روی عدد):
خروجیهای ذیل را دریافت خواهیم کرد:
به عبارتی حین فراخوانی و استفادهی از متدی که پارامتر آن به صورت in تعریف شدهاست، ذکر in ضروری نیست.
و به طور کلی استفادهی از in در مکانهای ذیل مجاز است:
• methods
• delegates
• lambdas
• local functions
• indexers
• operators
محدودیتهای استفادهی از پارامترهای in
الف) محدودیت استفاده از پارامترهای in در تعریف overloads
مثال زیر را در نظر بگیرید:
در اینجا overloadهای تعریف شدهی متد A تنها در ذکر واژهی کلیدی in یا modifier متفاوت هستند.
اگر سعی کنیم وهلهای از این کلاس را ایجاد کرده و از متدهای A آن استفاده کنیم:
خطای کامپایلر مبهم بودن متد A مورد استفاده صادر خواهد شد. یعنی نمیتوان overload ایی را تعریف کرد که تنها در modifier از نوع in با دیگری متفاوت باشد؛ چون ذکر in در حین فراخوانی متد، اختیاری است.
ب) پارامترهای از نوع in را در متدهای iterator نمیتوان استفاده کرد:
ج) پارامترهای از نوع in را در متدهای async نمیتوان استفاده کرد:
تاثیر کار با متدهای داخلی تغییر دهندهی وضعیت یک struct
مثال زیر را درنظر بگیرید. به نظر شما خروجی آن چیست؟
در اینجا اگر متد TestInStructs.Run را اجرا کنیم، خروجی آن، نمایش عدد 1 خواهد بود.
در ابتدا مقدار struct را به 1 تنظیم و سپس ارجاع آنرا به متدی دیگر که مقدار آنرا به 5 تنظیم میکند، ارسال کردیم. در این حالت برنامه بدون مشکل کامپایل و اجرا میشود. علت اینجا است که کامپایلر #C زمانیکه متدی را در داخل یک struct فراخوانی میکند، یک clone از آن struct را ایجاد کرده و متد را بر روی آن clone اجرا میکند؛ چون نمیداند که آیا این متد وضعیت و مقدار این struct را تغییر میدهد یا خیر. در این حالت کپی اصلی بدون تغییر باقی میماند (در نهایت عدد 1 را مشاهده خواهیم کرد)، اما در آخر فراخوان، ارجاعی از struct را دریافت نکرده و بر روی کپی آن کار میکند. بنابراین مزیت بهبود کارآیی، از دست خواهد رفت.
البته در اینجا اگر میخواستیم مقدار MyValue را مستقیما تغییر دهیم، کامپایلر از آن جلوگیری میکرد و این کد هیچگاه کامپایل نمیشد:
واژهی کلیدی جدید in
C# 7.2، واژهی کلیدی جدیدی را به نام in جهت تعریف پارامترها، معرفی کردهاست. زمانیکه از آن استفاده میشود به این معنا است که value type ارسالی به آن، توسط ارجاعی از آن، در اختیار متد قرار میگیرد و نه توسط مقدار کپی شدهی آن (حالت پیشفرض) و همچنین متد استفاده کنندهی از آن، مقدار این شیء را تغییر نمیدهد.
واژهی کلیدی in مکمل واژههای کلیدی ref و out است که پیشتر به همراه زبان #C ارائه شده بودند:
- واژهی کلیدی out: مقدار آرگومان مزین شدهی توسط آن، باید درون متد تنظیم شود و صرفا کاربرد ارائهی یک خروجی اضافهتر توسط آن متد را دارد.
- واژهی کلیدی ref: مقدار آرگومان مزین شدهی توسط آن، ممکن است درون متد تنظیم شود، یا خیر و همچنین توسط ارجاع به آن منتقل میشود.
- واژهی کلیدی in: مقدار آرگومان مزین شدهی توسط آن، درون متد تغییر نخواهد کرد و همچنین توسط ارجاع به آن منتقل میشود.
برای مثال اگر پارامترهای value type متد زیر را از نوع in معرفی کنیم، امکان تغییر مقدار آنها درون متد وجود نخواهد داشت:
public static int Add(in int number1, in int number2) { number1 = 5; // Cannot assign to variable 'in int' because it is a readonly variable return number1 + number2; }
واژهی کلیدی جدید in تا چه اندازهای بر روی کارآیی برنامه تاثیر دارد؟
زمانیکه یک value type را به متدی ارسال میکنیم، ابتدا به مکان جدیدی از حافظه کپی شده و سپس مقدار clone شدهی آن، به متد ارسال میشود. با استفاده از واژهی کلیدی in، دقیقا همان ارجاع به مقدار اولیه، به متد ارسال خواهد شد؛ بدون ایجاد کپی اضافهتری از آن. برای بررسی تاثیر این عملیات بر روی کارآیی برنامه، میتوان از BenchmarkDotNet استفاده کرد. برای این منظور ابتدا ارجاعی را به BenchmarkDotNet اضافه میکنیم:
<ItemGroup> <PackageReference Include="BenchmarkDotNet" Version="0.10.12" /> </ItemGroup>
using BenchmarkDotNet.Attributes; namespace CS72Tests { public struct Input { public decimal Number1; public decimal Number2; } [MemoryDiagnoser] public class InBenchmarking { const int loops = 50000000; Input inputInstance = new Input(); [Benchmark(Baseline = true)] public decimal RunNormalLoop_Pass_By_Value() { decimal result = 0M; for (int i = 0; i < loops; i++) { result = Run(inputInstance); } return result; } [Benchmark] public decimal RunInLoop_Pass_By_Reference() { decimal result = 0M; for (int i = 0; i < loops; i++) { result = RunIn(in inputInstance); } return result; } public decimal Run(Input input) { return input.Number1; } public decimal RunIn(in Input input) { return input.Number1; } } }
static void Main(string[] args) { var summary = BenchmarkRunner.Run<InBenchmarking>();
با این خروجی نهایی:
Method | Mean | Error | StdDev | Scaled | Allocated | ---------------------------- |----------:|---------:|---------:|-------:|----------:| RunNormalLoop_Pass_By_Value | 280.04 ms | 2.219 ms | 1.733 ms | 1.00 | 0 B | RunInLoop_Pass_By_Reference | 91.75 ms | 1.733 ms | 1.780 ms | 0.33 | 0 B |
امکان استفادهی از واژهی کلیدی in در حین تعریف متدهای الحاقی
در حین تعریف متدهای الحاقی، واژهی کلیدی in باید پیش از واژهی کلیدی this ذکر شود:
public static class Factorial { public static int Calculate(in this int num) { int result = 1; for (int i = num; i > 1; i--) result *= i; return result; } }
int num = 3; Console.WriteLine($"(in num) -> {Factorial.Calculate(in num)}"); Console.WriteLine($"(num) -> {Factorial.Calculate(num)}"); Console.WriteLine($"num. -> {num.Calculate()}");
(in num) -> 6 (num) -> 6 num. -> 6
و به طور کلی استفادهی از in در مکانهای ذیل مجاز است:
• methods
• delegates
• lambdas
• local functions
• indexers
• operators
محدودیتهای استفادهی از پارامترهای in
الف) محدودیت استفاده از پارامترهای in در تعریف overloads
مثال زیر را در نظر بگیرید:
public class CX { public void A(Input a) { Console.WriteLine("int a"); } public void A(in Input a) { Console.WriteLine("in int a"); } }
اگر سعی کنیم وهلهای از این کلاس را ایجاد کرده و از متدهای A آن استفاده کنیم:
public class Y { public void Test() { var inputInstance = new Input(); var cx = new CX(); cx.A(inputInstance); // The call is ambiguous between the following methods or properties: 'CX.A(Input)' and 'CX.A(in Input)' } }
ب) پارامترهای از نوع in را در متدهای iterator نمیتوان استفاده کرد:
public IEnumerable<int> B(in int a) // Iterators cannot have ref or out parameters { Console.WriteLine("in int a"); yield return 1; }
ج) پارامترهای از نوع in را در متدهای async نمیتوان استفاده کرد:
public async Task C(in int a) // Async methods cannot have ref or out parameters { await Task.Delay(1000); }
تاثیر کار با متدهای داخلی تغییر دهندهی وضعیت یک struct
مثال زیر را درنظر بگیرید. به نظر شما خروجی آن چیست؟
using System; namespace CS72Tests { struct MyStruct { public int MyValue { get; set; } public void UpdateMyValue(int value) { MyValue = value; } } public static class TestInStructs { public static void Run() { var myStruct = new MyStruct(); myStruct.UpdateMyValue(1); UpdateMyValue(myStruct); Console.WriteLine(myStruct.MyValue); } static void UpdateMyValue(in MyStruct myStruct) { myStruct.UpdateMyValue(5); } } }
در ابتدا مقدار struct را به 1 تنظیم و سپس ارجاع آنرا به متدی دیگر که مقدار آنرا به 5 تنظیم میکند، ارسال کردیم. در این حالت برنامه بدون مشکل کامپایل و اجرا میشود. علت اینجا است که کامپایلر #C زمانیکه متدی را در داخل یک struct فراخوانی میکند، یک clone از آن struct را ایجاد کرده و متد را بر روی آن clone اجرا میکند؛ چون نمیداند که آیا این متد وضعیت و مقدار این struct را تغییر میدهد یا خیر. در این حالت کپی اصلی بدون تغییر باقی میماند (در نهایت عدد 1 را مشاهده خواهیم کرد)، اما در آخر فراخوان، ارجاعی از struct را دریافت نکرده و بر روی کپی آن کار میکند. بنابراین مزیت بهبود کارآیی، از دست خواهد رفت.
البته در اینجا اگر میخواستیم مقدار MyValue را مستقیما تغییر دهیم، کامپایلر از آن جلوگیری میکرد و این کد هیچگاه کامپایل نمیشد:
static void UpdateMyValue(in MyStruct myStruct) { myStruct.MyValue = 5; // Cannot assign to a member of variable 'in MyStruct' because it is a readonly variable myStruct.UpdateMyValue(5); }
در این قسمت قصد داریم به بررسی Behavior ها در فریمورک MediatR بپردازیم. کدهای این قسمت بهروزرسانی و از این ریپازیتوری قابل دسترسی است.
در این صورت تمام متدهایی که نیاز به محاسبه زمان پردازش را دارند، باید به کلاسشان Logger تزریق شود. Stopwatch باید ایجاد، Start و Stop شود و در نهایت، بررسی کنیم که آیا زمان انجام این متد از حداکثری که برای آن مشخص کردهایم گذشته است یا خیر.
علاوه بر این تصور کنید روزی تصمیم بگیرید که حداکثر زمان برای Log کردن را از 5 ثانیه به 10 ثانیه تغییر دهید. در این صورت بدلیل اینکه در همه متدها این قطعه کد تکرار شدهاست، مجبور به تغییر تمام کدهای برنامه برای اصلاح این بخش خواهید شد. در اینجا اصل DRY نقض شدهاست.
برای حل این مشکل از Behaviorها استفاده میکنیم. برای پیاده سازی Behaviorها داخل MediatR، کافیست از interface ای بنام IPipelineBehavior ارث بری کنیم:
همانطور که میبینید منطق کد ما تغییری نکردهاست. از IPipelineBehavior ارث بری کرده و متد Handle آن را پیاده سازی کردهایم. همانند Middleware ها در ASP.NET Core، در اینجا نیز یک RequestHandlerDelegate بنام next داریم که با اجرا و return آن، روند اجرای بقیه Command/Queryها ادامه پیدا خواهد کرد.
سپس باید Behaviorهای خود را از طریق DI به MediatR معرفی کنیم. داخل Startup.cs به این صورت RequestPerformanceBehavior خود را Register میکنیم:
در نهایت برای تست کارکرد این Behavior، در کوئری GetCustomerByIdQueryHandler خود 5 ثانیه Delay ایجاد میکنیم تا طول اجرای آن، از Maximum زمان مشخص شده بیشتر و Log انجام شود:
پس از اجرای برنامه و فراخوانی GetCustomerById ، داخل Console این پیغام را خواهید دید:
Transaction Behavior
یکی دیگر از استفادههای Behaviorها میتواند پیاده سازی Transaction و Rollback باشد. فرض کنید میخواهیم افزودن یک مشتری به دیتابیس فقط زمانی صورت گیرد که تمام کارهای داخل Command با موفقیت و بدون رخ دادن Exception انجام شود. برای انجام اینکار میتوان یک TransactionBehavior نوشت تا بدنه Commandها را داخل یک TransactionScope قرار دهد و در صورت وقوع Exception ، عمل Rollback صورت گیرد :
سپس این Behavior را داخل DI Container خود Register میکنیم :
در نهایت متد Handle در CreateCustomerCommandHandler را که در قسمتهای قبل ایجاد کردیم، تغییر داده و بعد از SaveChanges مربوط به Entity Framework، یک Exception را صادر میکنیم:
اگر برنامه را اجرا کنید خواهید دید با اینکه Exception ما بعد از SaveChanges رخ داده است، اما بدلیل استفاده از Transaction Behavior ای که نوشتیم، عملیات Rollback صورت گرفته و داخل دیتابیس رکوردی ثبت نشدهاست.
MediatR دارای 2 اینترفیس IRequestPreProcessor و IRequestPostProcessor نیز هست که اگر نیاز داشته باشید یک عمل فقط قبل یا بعد از انجام یک Command/Query صورت گیرد، میتوانید از آنها استفاده کنید.
با استفاده از Behaviorها امکان پیاده سازی AOP را براحتی خواهید داشت. Behaviorها، مانند Filter ها در ASP.NET MVC هستند. همانطور که با استفاده از متدهای OnActionExecuting و OnActionExecuted میتوانستیم اعمالی را قبل و بعد از اجرای یک اکشنمتد انجام دهیم، چنین قابلیتی را با Behaviorها در MediatR نیز خواهیم داشت. مزیت اینکار این است که شما میتوانید کدهای Cross-Cutting-Concern خود را یکبار نوشته و چندین بار بدون تکرار مجدد، از آن استفاده کنید.
فرض کنید میخواهید زمان انجام کار یک متد را اندازه گیری کرده و در صورت طولانی بودن زمان انجام آن، لاگی را مبنی بر کند بودن بیش از حد مجاز این متد، ثبت کنید. شاید اولین راهی که برای انجام اینکار به ذهنتان بیاید این باشد که داخل تمام متدهایی که میخواهیم زمان انجام آنها را محاسبه کنیم، چنین کدی را تکرار کنیم:
Performance Counter Behavior
فرض کنید میخواهید زمان انجام کار یک متد را اندازه گیری کرده و در صورت طولانی بودن زمان انجام آن، لاگی را مبنی بر کند بودن بیش از حد مجاز این متد، ثبت کنید. شاید اولین راهی که برای انجام اینکار به ذهنتان بیاید این باشد که داخل تمام متدهایی که میخواهیم زمان انجام آنها را محاسبه کنیم، چنین کدی را تکرار کنیم:public class SomeClass { private readonly ILogger _logger; public SomeClass(ILogger logger) { _logger = logger; } public void SomeMethod() { Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); // TODO: Do some work here stopwatch.Stop(); if (stopwatch.ElapsedMilliseconds > TimeSpan.FromSeconds(5).Milliseconds) { // This method has taken a long time, So we log that to check it later. _logger.LogWarning($"SomeClass.SomeMethod has taken {stopwatch.ElapsedMilliseconds} to run completely !"); } } }
علاوه بر این تصور کنید روزی تصمیم بگیرید که حداکثر زمان برای Log کردن را از 5 ثانیه به 10 ثانیه تغییر دهید. در این صورت بدلیل اینکه در همه متدها این قطعه کد تکرار شدهاست، مجبور به تغییر تمام کدهای برنامه برای اصلاح این بخش خواهید شد. در اینجا اصل DRY نقض شدهاست.
برای حل این مشکل از Behaviorها استفاده میکنیم. برای پیاده سازی Behaviorها داخل MediatR، کافیست از interface ای بنام IPipelineBehavior ارث بری کنیم:
public class RequestPerformanceBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> { private readonly ILogger<RequestPerformanceBehavior<TRequest, TResponse>> _logger; public RequestPerformanceBehavior(ILogger<RequestPerformanceBehavior<TRequest, TResponse>> logger) { _logger = logger; } public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next) { Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); TResponse response = await next(); stopwatch.Stop(); if (stopwatch.ElapsedMilliseconds > TimeSpan.FromSeconds(5).Milliseconds) { // This method has taken a long time, So we log that to check it later. _logger.LogWarning($"{request} has taken {stopwatch.ElapsedMilliseconds} to run completely !"); } return response; } }
سپس باید Behaviorهای خود را از طریق DI به MediatR معرفی کنیم. داخل Startup.cs به این صورت RequestPerformanceBehavior خود را Register میکنیم:
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(RequestPerformanceBehavior<,>));
در نهایت برای تست کارکرد این Behavior، در کوئری GetCustomerByIdQueryHandler خود 5 ثانیه Delay ایجاد میکنیم تا طول اجرای آن، از Maximum زمان مشخص شده بیشتر و Log انجام شود:
public class GetCustomerByIdQueryHandler : IRequestHandler<GetCustomerByIdQuery, CustomerDto> { private readonly ApplicationDbContext _context; private readonly IMapper _mapper; public GetCustomerByIdQueryHandler(ApplicationDbContext context, IMapper mapper) { _context = context; _mapper = mapper; } public async Task<CustomerDto> Handle(GetCustomerByIdQuery request, CancellationToken cancellationToken) { Customer customer = await _context.Customers .FindAsync(request.CustomerId); if (customer == null) { throw new RestException(HttpStatusCode.NotFound, "Customer with given ID is not found."); } // For testing PerformanceBehavior await Task.Delay(5000, cancellationToken); return _mapper.Map<CustomerDto>(customer); } }
پس از اجرای برنامه و فراخوانی GetCustomerById ، داخل Console این پیغام را خواهید دید:
Transaction Behavior
یکی دیگر از استفادههای Behaviorها میتواند پیاده سازی Transaction و Rollback باشد. فرض کنید میخواهیم افزودن یک مشتری به دیتابیس فقط زمانی صورت گیرد که تمام کارهای داخل Command با موفقیت و بدون رخ دادن Exception انجام شود. برای انجام اینکار میتوان یک TransactionBehavior نوشت تا بدنه Commandها را داخل یک TransactionScope قرار دهد و در صورت وقوع Exception ، عمل Rollback صورت گیرد :
public class TransactionBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> { public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next) { var transactionOptions = new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted, Timeout = TransactionManager.MaximumTimeout }; using (var transaction = new TransactionScope(TransactionScopeOption.Required, transactionOptions, TransactionScopeAsyncFlowOption.Enabled)) { TResponse response = await next(); transaction.Complete(); return response; } } }
سپس این Behavior را داخل DI Container خود Register میکنیم :
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(TransactionBehavior<,>));
در نهایت متد Handle در CreateCustomerCommandHandler را که در قسمتهای قبل ایجاد کردیم، تغییر داده و بعد از SaveChanges مربوط به Entity Framework، یک Exception را صادر میکنیم:
public class CreateCustomerCommandHandler : IRequestHandler<CreateCustomerCommand, CustomerDto> { readonly ApplicationDbContext _context; readonly IMapper _mapper; readonly IMediator _mediator; public CreateCustomerCommandHandler(ApplicationDbContext context, IMapper mapper, IMediator mediator) { _context = context; _mapper = mapper; _mediator = mediator; } public async Task<CustomerDto> Handle(CreateCustomerCommand createCustomerCommand, CancellationToken cancellationToken) { Domain.Customer customer = _mapper.Map<Domain.Customer>(createCustomerCommand); await _context.Customers.AddAsync(customer, cancellationToken); await _context.SaveChangesAsync(cancellationToken); throw new Exception("======= MY CUSTOM EXCEPTION ======="); // Raising Event ... await _mediator.Publish(new CustomerCreatedEvent(customer.FirstName, customer.LastName, customer.RegistrationDate), cancellationToken); return _mapper.Map<CustomerDto>(customer); } }
اگر برنامه را اجرا کنید خواهید دید با اینکه Exception ما بعد از SaveChanges رخ داده است، اما بدلیل استفاده از Transaction Behavior ای که نوشتیم، عملیات Rollback صورت گرفته و داخل دیتابیس رکوردی ثبت نشدهاست.
MediatR دارای 2 اینترفیس IRequestPreProcessor و IRequestPostProcessor نیز هست که اگر نیاز داشته باشید یک عمل فقط قبل یا بعد از انجام یک Command/Query صورت گیرد، میتوانید از آنها استفاده کنید.
همچنین پیاده سازیهای پیشفرضی از این 2 اینترفیس با نامهای RequestPreProcessorBehavior و RequestPostProcessorBehavior داخل فریمورک، بطور پیشفرض وجود دارد که قبل و بعد از تمامی Handlerها اجرا خواهند شد.
مطالب دورهها
دسترسی سریع به مقادیر خواص توسط Reflection.Emit
اگر پروژههای چندسال اخیر را مرور کرده باشید خصوصا در زمینه ORMها و یا Serializerها و کلا مواردی که با Reflection زیاد سروکار دارند، تعدادی از آنها پیشوند fast را یدک میکشند و با ارائه نمودارهایی نشان میدهند که سرعت عملیات و کتابخانههای آنها چندین برابر کتابخانههای معمولی است و ... سؤال مهم اینجا است که رمز و راز اینها چیست؟
فرض کنید تعاریف کلاس User به صورت زیر است:
همانطور که در قسمتهای قبل نیز عنوان شد، خاصیت Id در کدهای IL نهایی به صورت متدهای get_Id و set_Id ظاهر میشوند.
حال اگر یک متد پویا ایجاد کنیم که بجای هر بار Reflection جهت دریافت مقدار Id، خود متد get_Id را مستقیما صدا بزند، چه خواهد شد؟
پیاده سازی این نکته را در ادامه ملاحظه میکنید:
توضیحات:
از کلاس Benchmark برای نمایش زمان انجام عملیات دریافت مقادیر Id از یک لیست، به دو روش Reflection متداول و روش صدا زدن مستقیم متد get_Id استفاده شده است.
در متد GetFastGetterFunc، ابتدا به متد get_Id خاصیت Id دسترسی پیدا خواهیم کرد. سپس یک متد پویا ایجاد میکنیم تا این get_Id را مستقیما صدا بزند. حاصل کار را به صورت یک delegate بازگشت میدهیم. شاید عنوان کنید که در اینجا هم حداقل در ابتدای کار متد، یک Reflection اولیه وجود دارد. پاسخ این است که مهم نیست؛ چون در یک برنامه واقعی، تهیه delegates در زمان آغاز برنامه انجام شده و حاصل کش میشود. بنابراین در زمان استفاده نهایی، به هیچ عنوان با سربار Reflection مواجه نخواهیم بود.
خروجی آزمایش فوق بر روی سیستم معمولی من به صورت زیر است:
بله. نتیجه روش GetFastGetterFunc واقعا سریع و باور نکردنی است!
چند پروژه که از این روش استفاده میکنند
Dapper
AutoMapper
fastJson
در سورس این کتابخانهها روشهای فراخوانی مستقیم متدهای set نیز پیاده سازی شدهاند که جهت تکمیل بحث میتوان به آنها مراجعه نمود.
ماخذ اصلی
این کشف و استفاده خاص، از اینجا شروع و عمومیت یافته است و پایه تمام کتابخانههایی است که پیشوند fast را به خود دادهاند:
2000% faster using dynamic method calls
فرض کنید تعاریف کلاس User به صورت زیر است:
public class User { public int Id { set; get; } }
حال اگر یک متد پویا ایجاد کنیم که بجای هر بار Reflection جهت دریافت مقدار Id، خود متد get_Id را مستقیما صدا بزند، چه خواهد شد؟
پیاده سازی این نکته را در ادامه ملاحظه میکنید:
using System; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Reflection.Emit; namespace FastReflectionTests { /// <summary> /// کلاسی برای اندازه گیری زمان اجرای عملیات /// </summary> public class Benchmark : IDisposable { Stopwatch _watch; string _name; public static Benchmark Start(string name) { return new Benchmark(name); } private Benchmark(string name) { _name = name; _watch = new Stopwatch(); _watch.Start(); } public void Dispose() { _watch.Stop(); Console.WriteLine("{0} Total seconds: {1}" , _name, _watch.Elapsed.TotalSeconds); } } public class User { public int Id { set; get; } } class Program { public static Func<object, object> GetFastGetterFunc(string propertyName, Type ownerType) { var propertyInfo = ownerType.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public); if (propertyInfo == null) return null; var getter = ownerType.GetMethod("get_" + propertyInfo.Name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy); if (getter == null) return null; var dynamicGetterMethod = new DynamicMethod( name: "_", returnType: typeof(object), parameterTypes: new[] { typeof(object) }, owner: propertyInfo.DeclaringType, skipVisibility: true); var il = dynamicGetterMethod.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); // Load input to stack il.Emit(OpCodes.Castclass, propertyInfo.DeclaringType); // Cast to source type // نکته مهم در اینجا فراخوانی نهایی متد گت بدون استفاده از ریفلکشن است il.Emit(OpCodes.Callvirt, getter); //calls its get method if (propertyInfo.PropertyType.IsValueType) il.Emit(OpCodes.Box, propertyInfo.PropertyType);//box il.Emit(OpCodes.Ret); return (Func<object, object>)dynamicGetterMethod.CreateDelegate(typeof(Func<object, object>)); } static void Main(string[] args) { //تهیه لیستی از دادهها جهت آزمایش var list = new List<User>(); for (int i = 0; i < 1000000; i++) { list.Add(new User { Id = i }); } // دسترسی به اطلاعات لیست به صورت متداول از طریق ریفلکشن معمولی var idProperty = typeof(User).GetProperty("Id"); using (Benchmark.Start("Normal reflection")) { foreach (var item in list) { var id = idProperty.GetValue(item, null); } } // دسترسی از طریق روش سریع دستیابی به اطلاعات خواص var fastIdProperty = GetFastGetterFunc("Id", typeof(User)); using (Benchmark.Start("Fast Property")) { foreach (var item in list) { var id = fastIdProperty(item); } } } } }
از کلاس Benchmark برای نمایش زمان انجام عملیات دریافت مقادیر Id از یک لیست، به دو روش Reflection متداول و روش صدا زدن مستقیم متد get_Id استفاده شده است.
در متد GetFastGetterFunc، ابتدا به متد get_Id خاصیت Id دسترسی پیدا خواهیم کرد. سپس یک متد پویا ایجاد میکنیم تا این get_Id را مستقیما صدا بزند. حاصل کار را به صورت یک delegate بازگشت میدهیم. شاید عنوان کنید که در اینجا هم حداقل در ابتدای کار متد، یک Reflection اولیه وجود دارد. پاسخ این است که مهم نیست؛ چون در یک برنامه واقعی، تهیه delegates در زمان آغاز برنامه انجام شده و حاصل کش میشود. بنابراین در زمان استفاده نهایی، به هیچ عنوان با سربار Reflection مواجه نخواهیم بود.
خروجی آزمایش فوق بر روی سیستم معمولی من به صورت زیر است:
Normal reflection Total seconds: 2.0054177 Fast Property Total seconds: 0.0552056
چند پروژه که از این روش استفاده میکنند
Dapper
AutoMapper
fastJson
در سورس این کتابخانهها روشهای فراخوانی مستقیم متدهای set نیز پیاده سازی شدهاند که جهت تکمیل بحث میتوان به آنها مراجعه نمود.
ماخذ اصلی
این کشف و استفاده خاص، از اینجا شروع و عمومیت یافته است و پایه تمام کتابخانههایی است که پیشوند fast را به خود دادهاند:
2000% faster using dynamic method calls
اشتراکها
نکاتی در مورد xaml
اشتراکها
نکاتی در مورد Logging
اشتراکها
نکاتی در مورد استفاده بهتر از jQuery
اشتراکها
نکاتی در مورد تهیه یک Backlog خوب
اشتراکها