/// <summary> /// /// </summary> /// <typeparam name="T"></typeparam> /// <param name="expression"></param> /// <returns></returns> public static string PropertyName<T>(this Expression<Func<T, object>> expression) { return new PropertyHelper().GetNestedPropertyName(expression); }
بهبود کارآیی Reflection در دات نت 7
آشنایی با Reflection.Emit
ابتدا مثال کامل ذیل را در نظر بگیرید:
using System; using System.Collections.Generic; using System.Data.Entity; using System.Data.Entity.Migrations; using System.Linq; using System.Linq.Expressions; namespace Sample { public abstract class BaseEntity { public int Id { set; get; } } public class Receipt : BaseEntity { public int TotalPrice { set; get; } } public class MyContext : DbContext { public DbSet<Receipt> Receipts { get; set; } } public class Configuration : DbMigrationsConfiguration<MyContext> { public Configuration() { AutomaticMigrationsEnabled = true; AutomaticMigrationDataLossAllowed = true; } protected override void Seed(MyContext context) { if (!context.Receipts.Any()) { for (int i = 0; i < 20; i++) { context.Receipts.Add(new Receipt { TotalPrice = i }); } } base.Seed(context); } } public static class EFUtils { public static IList<T> LoadEntities<T>(this DbContext ctx, Expression<Func<T, bool>> predicate) where T : class { return ctx.Set<T>().Where(predicate).ToList(); } public static IList<T> LoadData<T>(this DbContext ctx, Func<T, bool> predicate) where T : class { return ctx.Set<T>().Where(predicate).ToList(); } } public static class Test { public static void RunTests() { startDB(); using (var context = new MyContext()) { var list1 = context.LoadEntities<Receipt>(x => x.TotalPrice == 10); var list2 = context.LoadData<Receipt>(x => x.TotalPrice == 20); } } private static void startDB() { Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>()); // Forces initialization of database on model changes. using (var context = new MyContext()) { context.Database.Initialize(force: true); } } } }
نکته اصلی مورد بحث، کلاس کمکی EFUtils است که در آن دو متد الحاقی LoadEntities و LoadData تعریف شدهاند. در متد LoadEntities، امضای متد شامل Expression Func است و در متد LoadData فقط Func ذکر شده است.
در ادامه اگر برنامه را توسط فراخوانی متد RunTests اجرا کنیم، به نظر شما خروجی SQL حاصل از list1 و list2 چیست؟
احتمالا شاید عنوان کنید که هر دو یک خروجی SQL دارند (با توجه به اینکه بدنه متدهای LoadEntities و LoadData دقیقا/یا به نظر یکی هستند) اما یکی از پارامتر 10 استفاده میکند و دیگری از پارامتر 20. تفاوت دیگری ندارند.
اما ... اینطور نیست!
خروجی SQL متد LoadEntities در متد RunTests به صورت زیر است:
SELECT [Extent1].[Id] AS [Id], [Extent1].[TotalPrice] AS [TotalPrice] FROM [dbo].[Receipts] AS [Extent1] WHERE 10 = [Extent1].[TotalPrice]
SELECT [Extent1].[Id] AS [Id], [Extent1].[TotalPrice] AS [TotalPrice] FROM [dbo].[Receipts] AS [Extent1]
چرا؟
Func اشارهگری است به یک متد و Expression Func بیانگر ساختار درختی عبارت lambda نوشته شده است. این ساختار درختی صرفا بیان میکند که عبارت lambda منتسب، چه کاری را قرار است یا میتواند انجام دهد؛ بجای انجام واقعی آن.
public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate); public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate);
بنابراین هرچند بدنه دو متد LoadEntities و LoadData به ظاهر یکی هستند، اما بر اساس نوع ورودی Where ایی که دریافت میکنند، اگر Expression Func باشد، EF فرصت آنالیز و ترجمه عبارت ورودی را خواهد یافت اما اگر Func باشد، ابتدا باید کل اطلاعات را به صورت یک لیست IEnumerable دریافت و سپس سمت کلاینت، خروجی نهایی را فیلتر کند.
اگر برنامه را اجرا کنید نهایتا هر دو لیست یک و دو، بر اساس شرط عنوان شده عمل خواهند کرد و فیلتر خواهند شد. اما در حالت اول این فیلتر شدن سمت بانک اطلاعاتی است و در حالت دوم کل اطلاعات بارگذاری شده و سپس سمت کاربر فیلتر میشود (که کارآیی پایینی دارد).
نتیجه گیری
به امضای متد Where ایی که در حال استفاده است دقت کنید. همینطور در مورد Sum ، Count و یا موارد مشابه دیگری که predicate قبول میکنند.
پیشنیازهای کار با Reflection در NET Core.
ابتدا نیاز است اسمبلی System.Reflection به قسمت وابستگیهای فایل project.json اضافه شود:
"dependencies": { "System.Reflection": "4.3.0" },
پس از این مورد، ذکر فضای نام System.Reflection را در ابتدای کدها نیز فراموش نکنید. همین مورد بسیاری از خطاهای انتقال کدها را بدون تغییری برطرف میکند:
using System.Reflection;
نوشتن کدهای شرطی، برای کامپایل یک کد، مخصوص فریم ورکهای مختلف
در کدهای سی شارپ میتوان توسط if# و else#، کامپایلر را جهت استفاده یا عدم استفادهی کدهای نوشته شده، راهنمایی کرد. برای این منظور در NET Core.، به فایل project.json مراجعه کرده و ثابت دلخواهی را به نام COREFX به قسمت buildOptions اضافه میکنیم.
"buildOptions": { "define": [ "COREFX" ] },
#if COREFX // corefx codes #else // other frameworks #endif
اضافه شدن متدهای GetMethodInfo و GetTypeInfo
بسیاری از امکانات System.Type نگارش کامل دات نت به TypeInfo در NET Core. منتقل شدهاند. برای مثال بجای کد پیشین
var members = obj.GetType().GetMembers();
var members = obj.GetType().GetTypeInfo().GetMembers();
#if COREFX toType.GetTypeInfo().IsGenericType #else toType.IsGenericType #endif
یافتن اسمبلی جاری
در NET Core. متد GetExecutingAssembly حذف شدهاست و باید ابتدا نوع کلاس جاری را یافت و سپس توسط GetTypeInfo به Assembly آن دسترسی یافت:
#if COREFX return typeof(MyClassName).GetTypeInfo().Assembly; #else return System.Reflection.Assembly.GetExecutingAssembly(); #endif
خلاصهی بحث
برای تبدیل کدهای مبتنی بر Reflection موجود، ابتدا از وجود وابستگی System.Reflection اطمینان حاصل کرده، فضای نام آنرا الحاق و در موارد جزئی نیاز است از متد GetTypeInfo برای دسترسی به خواص API قبلی استفاده کرد.
کار با Expression Tree در سی شارپ
میشه گفت یکی از advancedترین قسمتهای دات نت، مفهوم Expression Tree و کلاس Expression هست که یه جورایی قلب IQueryable رو هم تشکیل میده
شاید نهایت استفاده افراد، کار با <<Expression<Func برای شرطهای predicate بر روی متد Where و یا selector برای متد Select باشه
ولی Expression خیلی بزرگتر از اینهاست
توضیح مفهوم Expression Tree طولانیه اگه میخواین بیشتر باهاش اشنا بشین قبلا اینجا یه پست نوشتم براش.
لینک اشتراک جاری هم یکی از بهترین مقالاتی که این مفهوم رو به خوبی به همراه مثال توضیح داده
زمانیکه پروسهی کامپایل برنامه شروع میشود، در این بین، به مرحلهی جدیدی به نام «تولید کدها» میرسد. در این حالت، کامپایلر تمام اطلاعاتی را که در مورد پروژهی جاری در اختیار دارد، به تولید کنندهی کد معرفی شدهی به آن ارائه میدهد. بر اساس این اطلاعات غنی ارائه شدهی توسط کامپایلر، تولید کنندهی کد، شروع به تولید کدهای جدیدی کرده و آنها را در اختیار ادامهی پروسهی کامپایل، قرار میدهد. پس از آن، کامپایلر با این کدهای جدید، همانند سایر کدهای موجود در پروژه رفتار کرده و عملکرد عادی خودش را ادامه میدهد.
یک برنامه میتواند از چندین Source Generators نیز استفاده کند که روش قرار گرفتن آنها را در پروسهی کامپایل، در شکل زیر مشاهده میکنید:
Source Generators از یکدیگر کاملا مستقل هستند و اطلاعات آنها Immutable است. یعنی نمیتوان اطلاعات تولیدی توسط یک Source Generator را در دیگری تغییر داد و تمام فایلهای تولیدی توسط انواع Source Generators موجود، به پروسهی کامپایل نهایی اضافه میشوند. هرچند زمانیکه فایلی توسط یک تولید کنندهی کد، به کامپایلر اضافه میشود، بلافاصله اطلاعات آن در کل برنامه و IDE و تمام Source Generators موجود دیگر، قابل مشاهده و استفاده است.
مقایسهای بین تولید کنندههای کد و فناوری IL Weaving
Source Generators، تنها راه و روش تولید کد، نیستند و پیش از آن روشهایی مانند استفاده از T4 templates ، Fody ، PostSharp و امثال آن نیز ارائه شدهاست. در ادامه مقایسهای را بین تولید کنندههای کد و فناوری IL Weaving را که پیشتر در سری AOP در این سایت مطالعه کردهاید، مشاهده میکنید:
تولید کنندههای کد:
- تنها میتوانند فایلهای جدید را اضافه کنند. یعنی «در حین» پروسهی کامپایل ظاهر میشوند و به عنوان یک مکمل، تاثیر گذارند. برای مثال نمیتوانند محتوای یک خاصیت یا متد از پیش موجود را تغییر دهند. اما میتوانند هر نوع کد partial ای را «تکمیل» کنند.
- محتوای اضافه شدهی توسط یک تولید کنندهی کد، بلافاصله توسط Compiler شناسایی شده و بررسی میشود و همچنین در Intellisense ظاهر شده و به سادگی قابل دسترسی است. همچنین، قابلیت دیباگ نیز دارد.
IL Weaving:
- میتوانند bytecode برنامه را تغییر دهند. یعنی «پس از» پروسهی کامپایل ظاهر شده و کدهایی را به اسمبلی نهایی تولید شده اضافه میکنند. در این حالت محدودیتی از لحاظ محل تغییر کدها وجود ندارد. برای مثال میتوان بدنهی یک متد یا خاصیت را بطور کامل بازنویسی کرد و کارکردهایی مانند تزریق کدهای caching و logging را دارند.
- کدهایی که توسط این پروسه اضافه میشوند، در حین کدنویسی متداول، قابلیت دسترسی ندارند؛ چون پس از پروسهی کامپایل، به فایل باینری نهایی تولیدی، اضافه میشوند. بنابراین قابلیت دیباگ به همراه سایر کدهای برنامه را نیز ندارند. به علاوه چون توسط کامپایلر در حین پروسهی کامپایل، بررسی نمیشوند، ممکن است به همراه قطعه کدهای غیرقابل اجرایی نیز باشند و دیباگ آنها بسیار مشکل است.
آیندهی Reflection به چه صورتی خواهد شد؟
هرچند Reflection کار تولید کدی را انجام نمیدهد، اما یکی از کارهای متداول با آن، یافتن و محاسبهی اطلاعات خواص و فیلدهای اشیاء، در زمان اجرا است و مزیت کار کردن با آن نیز این است که اگر خاصیتی یا فیلدی تغییر کند، نیازی به بازنویسی قسمتهای پیاده سازی شدهی با Reflection نیست. به همین جهت برای مثال تقریبا تمام کتابخانههای Serialization، از Reflection برای پیاده سازی اعمال خود استفاده میکنند.
امروز، تمام اینگونه عملیات را توسط Source Generators نیز میتوان انجام داد و این فناوری جدید، قابلیت به روز رسانی خودکار کدهای تولیدی را با کم و زیاد شدن خواص و فیلدهای کلاسها دارد و نمونهای از آن، Source Generator توکار مرتبط با کار با JSON در دات نت 6 است که به شدت سبب بهبود کارآیی برنامه، در مقایسه با استفادهی از Reflection میشود؛ چون اینبار تمام محاسبات دقیق مرتبط با Serialization به صورت خودکار در زمان کامپایل برنامه انجام میشود و جزئی از خروجی برنامهی نهایی خواهد شد و دیگر نیازی به محاسبهی هربارهی اطلاعات مورد نیاز، در زمان اجرای برنامه نیست.
نمونهای از روش دسترسی به اطلاعات کلاسها و خواص و فیلدهای آنها را در زمان کامپایل برنامه توسط Source Generators، در مثال قسمت بعد، مشاهده خواهید کرد.
وضعیت T4 templates چگونه خواهد شد؟
در سالهای آغازین ارائهی دات نت، استفاده از T4 templates جهت تولید کدها بسیار مرسوم بود؛ اما با ارائهی Source Generators، این ابزار نیز منسوخ شده در نظر گرفته میشود.
T4 Templates همانند Source Generators تنها کدها و فایلهای جدیدی را تولید میکنند و توانایی تغییر کدهای موجود را ندارند. اما مشکل مهم آن، داشتن Syntax ای خاص است که توسط اکثر IDEها پشتیبانی نمیشود. همچنین عموما اجرای آنها نیز دستی است و برخلاف Source Generators، با تغییرات کدها، به صورت خودکار به روز نمیشوند.
تغییرات زبان #C در جهت پشتیبانی از تولید کنندههای کد
از سالهای اول ارائهی زبان #C، واژهی کلیدی partial، جهت فراهم آوردن امکان تقسیم کدهای یک کلاس، به چندین فایل، میسر شد که از این قابلیت در فناوری T4 Templates زیاد استفاده میشد. اکنون با ارائهی تولید کنندههای کد، واژهی کلیدی partial را میتوان به متدها نیز افزود تا پیاده سازی اصلی آنها، در فایلی دیگر، توسط تولید کنندههای کد انجام شود. تا C# 8.0 امکان افزودن واژهی کلیدی partial به متدهای خصوصی یک کلاس و آن هم از نوع void وجود داشت و در C# 9.0 به متدهای عمومی کلاسها نیز اضافه شدهاست و اکنون این متدها میتوانند void هم نباشند:
partial class MyType { partial void OnModelCreating(string input); // C# 8.0 public partial bool IsPet(string input); // C# 9.0 } partial class MyType { public partial bool IsPet(string input) => input is "dog" or "cat" or "fish"; }
مروری بر کاربردهای Action و Func - قسمت اول
تا پیش از C# 10.0 جهت تعریف Lambda Expressions نیاز بود تا کمی بیشتر کد نوشت. برای مثال:
Func<string, int> parse = (string s) => int.Parse(s);
با ارائهی C# 10.0، مفهومی به نام natural lambda expression ارائه شدهاست که در آن کامپایلر سعی میکند تا نوع این Action و Funcها را بر اساس تعریف lambda expression، تشخیص دهد. در این حالت قطعه کد فوق، به صورت زیر خلاصه میشود:
var parse = (string s) => int.Parse(s);
var upper = (s) => s.ToUpperInvariant();
همچنین در C# 10.0 میتوان این نوع پیشفرض تشخیص داده شدهی توسط کامپایلر را نیز صراحتا مشخص کرد و تغییر داد:
var createException = (bool b) => b ? new ArgumentNullException() : new DivideByZeroException();
var createException = Exception (bool b) => b ? new ArgumentNullException() : new DivideByZeroException();
var oneTwoThreeArray = () => new[]{1, 2, 3}; // inferred type is Func<int[]> var oneTwoThreeList = IList<int> () => new[]{1, 2, 3}; // same body, but inferred type is now Func<IList<int>>
این natural return types، به method groups نیز بسط یافتهاست. منظور از method groups، متدهایی بدون ذکر لیست آرگومانهای آنها است:
Func<int> read = Console.Read; Action<string> write = Console.Write;
var read = Console.Read; // Just one overload; Func<int> inferred var write = Console.Write; // ERROR: Multiple overloads, can't choose
و در آخر امکان تعریف ویژگیها (attributes) نیز بر روی lambda expressions در C# 10.0 میسر شدهاست:
var choose = [Example(2)][Example(3)] object (bool b) => b ? 1 : "two";
پ.ن.
تمام اینها در جهت پشتیبانی و ساده کردن کار با Minimal APIs ارائه شدهی در ASP.NET Core 6x به زبان #C اضافه شدهاند.
Expression<Func<string, bool>> f = s => s.Length < 5;
منبع : کتاب C# 8 in a Nutshell
ParameterCollection به پارامترهای استفاده شده در فیلتر اشاره دارد که در فیلتر بالا فقط s استفاده شدهاست و از نوع string است.
BinaryExpression شامل سه قسمت مهم Left , Right و NodeType میباشد. برای فیلتر بالا، مقدار پراپرتی Left برابر s.Length میباشد و پراپرتی Right شامل مقدار 5 و مقدار NodeType هم برابر LessThan میباشد. یعنی فیلتر بالا به یک درخت تبدیل شده که نود اصلی آن LessThan است و دو مقدار Left و Right را باهم مقایسه میکند. اما اگر یک شرط دیگر را به فیلتر بالا اعمال کنیم، ساختار Expression کمی تغییر میکند. برای مثال:
Expression<Func<string, bool>> filter = s => s.Length > 5 && s.Length < 45;
Expression ایجاد شده برای این فیلتر شامل همان ساختار قبلی است؛ اما با این تغییر که هر کدام از پراپرتیهای Right و Left، خود یک BinaryExpression شدهاند و مقدار NodeType اصلی از LessThan به AndAlso تغییر پیدا کردهاست. Expression ایجاد شده از فیلتر بالا ( filter.Body ) به این صورت است که پراپرتی Left آن برابر است با یک BinaryExpression که مقدار NodeType آن برابر است با GreaterThan و پراپرتی Left آن شامل s.Length میباشد و پراپرتی Right آن برابر 5 میباشد. همچنین پراپرتی Right مربوط به filter.Body برابر یک ExpressionBinary است که مقدار NodeType آن برابر است با LessThan و پراپرتی Left آن برابر s.Length است و پراپرتی Right آن برابر 45 میباشد.
filter.Body شبیه به تصویر زیر میباشد :
اگر بخواهیم خودمان یک Expression tree را ایجاد کنیم، باید از پایینترین نود آن شروع کنیم. یعنی ابتدا باید پراپرتی Left و Right را ایجاد کنیم و سپس این دو پراپرتی را با هم مقایسه کنیم (NodeType). در کد زیر Expression مربوط به فیلتر بالا را نوشتهایم:
ParameterExpression parameterExpression = Expression.Parameter(typeof(string)); MemberExpression memberExpression = Expression.Property(parameterExpression, "Length"); ConstantExpression greaterThanConstantExpression = Expression.Constant(5); BinaryExpression greaterThanComparison = Expression.GreaterThan(memberExpression, greaterThanConstantExpression); var greaterThan = Expression.Lambda<Func<string, bool>>(greaterThanComparison, parameterExpression); ConstantExpression lessThanConstantExpression = Expression.Constant(45); BinaryExpression lessThanComparsion = Expression.LessThan(memberExpression, lessThanConstantExpression); var lessThan = Expression.Lambda<Func<string, bool>>(lessThanComparsion, parameterExpression); var param = Expression.Parameter(typeof(string), "x"); var body = Expression.AndAlso( Expression.Invoke(greaterThan, param), Expression.Invoke(lessThan, param) ); Expression<Func<string, bool>> filter = Expression.Lambda<Func<string, bool>>(body, param);
ParameterExpression : نوع پارامتری را که میخواهیم روی آن شرط را روی آن اعمال کنیم، مشخص کردهایم.
MemberExpression : پراپرتی Length را معرفی کردهایم که قرار است شرطی بر روی این پراپرتی اعمال شود.
ConstantExpression : مقدار ثابتی که پراپرتی MemeberExpression قرار است با آن مقایسه شود.
BinaryExpression : نود تایپ را مشخص کردهایم که برابر است با GreaterThan.
سپس Expression مربوط به هرکدام را در greaterThan و lessThan ایجاد کردهایم و این دو را باهم And کرده و در متغییر body قرار دادهایم و در نهایت filter را با دستور Expression.Lambda ایجاد کردهایم که برابر است با :
Expression<Func<string, bool>> filter = s => s.Length > 5 && s.Length < 45;
ساخت یک داینامیک فیلتر
در ادامه میخواهیم یک داینامیک فیلتر را ایجاد کنیم که به طور مثال برنامه نویس از سمت فرانتاند بتواند فیلترهای سادهای را اعمال کند. برای این کار یک کلاس برای فیلتر ایجاد میکنیم :
public class DynamicModel { public string Name { get; set; } public string Comparison { get; set; } public object Data { get; set; } }
پراپرتی Data مقداری است که باید با آن مقایسه انجام شود.
Comparison نوع عملیات را مشخص میکند مانند : Equal, LessThan, GreaterThan و... .
پراپرتی Name نام پراپرتی است که باید شرط روی آن اعمال شود.
کلاس ثابت ها:
public static class ComparisonConstant { public const string LessThan = "LesThan"; public const string LessThanEqual = "LesThanEqual"; public const string GreaterThan = "GreaterThan"; public const string GreaterThanEqual = "GreaterThanEqual"; public const string Equal = "Equal"; public const string NotEqual = "NotEqual"; }
ساخت اکستنشن متد:
public static IQueryable<TModel> DynamicFilter<TModel>(this IQueryable<TModel> iqueryable, IEnumerable<DynamicModel> dynamicModel) { return iqueryable.Where(Filter<TModel>(dynamicModel)); }
public static Expression<Func<TModel, bool>> Filter<TModel>(IEnumerable<DynamicModel> dynamicModel) { Expression<Func<TModel, bool>> result = a => true; foreach (var item in dynamicModel) { ParameterExpression parameterExpression = Expression.Parameter(typeof(TModel)); MemberExpression memberExpression = Expression.Property(parameterExpression, item.Name); ConstantExpression constantExpression = Expression.Constant(item.Data); BinaryExpression comparison = GetBinaryExpression(item.Comparison, memberExpression, constantExpression); var expression = Expression.Lambda<Func<TModel, bool>>(comparison, parameterExpression); var param = Expression.Parameter(typeof(TModel), "x"); var body = Expression.AndAlso( Expression.Invoke(result, param), Expression.Invoke(expression, param) ); result = Expression.Lambda<Func<TModel, bool>>(body, param); } return result; }
ورودی این مدل، لیستی از DynamicModel میباشد که به ازای هر کدام از آیتمها، یک BinaryExpression ایجاد میکند و آن را با result تعریف شده And میکند. یعنی تمامی آیتمهای ارسال شده باهم And میشوند.
متد GetBinaryExpression بر اساس مقدار فیلد Comparison که از سمت فرانت ارسال میشود، کار میکند:
private static BinaryExpression GetBinaryExpression(string comparison, MemberExpression memberExpression, ConstantExpression constantExpression) { switch (comparison) { case ComparisonConstant.Equal: return Expression.Equal(memberExpression, constantExpression); case ComparisonConstant.LessThan: return Expression.LessThan(memberExpression, constantExpression); case ComparisonConstant.GreaterThan: return Expression.GreaterThan(memberExpression, constantExpression); case ComparisonConstant.NotEqual: return Expression.NotEqual(memberExpression, constantExpression); case ComparisonConstant.GreaterThanEqual: return Expression.GreaterThanOrEqual(memberExpression, constantExpression); case ComparisonConstant.LessThanEqual: return Expression.LessThanOrEqual(memberExpression, constantExpression); default: return null; } }
کلاس Category را در نظر بگیرید که شامل دو پراپرتی Title و Id میباشد و میخواهیم از این داینامیک فیلتر، برای فیلتر کردن دیتاها استفاده کنیم از سمت فرانتاند. اگر از سمت فرانتاند چنین دیتایی ارسال شود:
[ { "Name":"Title", "Comparison":"Equal", "Data":"Hi" }, { "Name":"Id", "Comparison":"LesThanEqual", "Data": 100 } ]
تمامی رکوردهایی که مقدار پراپرتی Title آنها برابر Hi باشد و Id آن کوچکتر مساوی 100 باشد، از دیتابیس خوانده میشود.
var categories = _dbContext.Categories .DynamicFilter(filter)//filter => IEnumerable<DynamicModel> .ToList();
گیت هاب داینامیک فیلتر