یکی از اهداف کار با ORMها، رسیدن به کدی قابل ترجمه و استفادهی توسط تمام بانکهای اطلاعاتی ممکن است و یکی از الزامات رسیدن به این هدف، صرفنظر کردن از قابلیتهای بومی بانکهای اطلاعاتی است که در سایر بانکهای اطلاعاتی دیگر معادلی ندارند. برای مثال SQL Server به همراه توابع توکاری مانند datediff و datepart برای کار با زمان و تاریخ است؛ اما این توابع را به صورت مستقیم نمیتوان در ORMها استفاده کرد. چون به محض استفادهی از آنها، کد تهیه شده دیگر قابلیت انتقال به سایر بانکهای اطلاعاتی را نخواهد داشت. اما ... اگر این هدف را نداشته باشیم، چطور؟ آیا میتوان یک تابع DateDiff سفارشی را برای EF Core تهیه نمود و از تمام قابلیتهای بومی آن در کوئریهای LINQ استفاده کرد؟ بله! یک چنین قابلیتی تحت عنوان DbFunctions در EF Core پشتیبانی میشود که روش تهیهی آنها را در این مطلب بررسی خواهیم کرد.
معرفی موجودیت Person
در مثال این مطلب قصد داریم، معادل توابع بومی مخصوص SQL Server را که امکان کار با DateTime را مهیا میکنند، در EF Core تعریف کنیم. به همین جهت نیاز به موجودیتی داریم که دارای خاصیتی از این نوع باشد:
گزارشگیری بر اساس تعداد روز گذشتهی از ثبت نام
اکنون فرض کنید میخواهیم گزارشی را از تمام کاربرانی که در طی 10 روز قبل ثبت نام کردهاند، تهیه کنیم. اگر کوئری زیر را برای این منظور تهیه کنیم:
با استثنای زیر متوقف خواهیم شد:
عنوان میکند که یک چنین کوئری LINQ ای قابلیت ترجمهی به SQL را ندارد. اما ... نکتهی مهم اینجا است که خود SQL Server یک چنین توانمندی را به صورت توکار دارا است:
برای انجام کوئری مدنظر فقط کافی است از تابع DATEDIFF توکار آن با پارامتر Day، استفاده کنیم تا لیست تمام کاربران ثبت نام کردهی در طی 10 روز قبل را بازگشت دهد. اکنون سؤال اینجا است که آیا میتوان چنین تابعی را به EF Core معرفی کرد؟
روش تعریف تابع DATEDIFF سفارشی در EF Core
برای تعریف متد DateDiff مخصوص EF Core، ابتدا باید یک کلاس static را تعریف کرد و سپس تنها امضای این متد را، معادل امضای تابع توکار SQL Server تعریف کرد. این متد نیازی نیست تا پیاده سازی را داشته باشد. به همین جهت بدنهی آنرا صرفا با یک throw new InvalidOperationException مقدار دهی میکنیم. هدف از این متد، استفادهی از آن در LINQ Expressions است و قرار نیست به صورت مستقیمی بکار گرفته شود:
در اینجا علاوه بر تعریف امضای متد DateDiff که در اینجا SqlDateDiff نام گرفتهاست، فیلد SqlDateDiffMethodInfo را نیز مشاهده میکنید. در حین تعریف و معرفی DbFunctions سفارشی به EF Core، متدهایی که اینکار را انجام میدهند، پارامترهای ورودی از نوع MethodInfo دارند. به همین جهت یک چنین تعریفی انجام شدهاست.
روش معرفی تابع DATEDIFF سفارشی به EF Core
پس از تعریف امضای متد معادل DateDiff، اکنون نوبت به معرفی آن به EF Core است:
کار تعریف DbFunctions سفارشی توسط متد HasDbFunction صورت میگیرد. پارامتر این متد، همان MethodInfo معادل امضای تابع توکار مدنظر است.
سپس توسط متد HasTranslation، مشخص میکنیم که این متد به چه نحوی قرار است به یک عبارت SQL ترجمه شود. پارامتر args ای که در اینجا در اختیار ما قرار میگیرد، دقیقا همان پارامترهای متد public static int SqlDateDiff(SqlDateDiff interval, DateTime initial, DateTime end) هستند که در این مثال خاص، شامل سه پارامتر میشوند. پارامترهای دوم و سوم آنرا به همان نحوی که دریافت میکنیم، به SqlFunctionExpression.Create ارسال خواهیم کرد. اما پارامتر اول را از نوع enum تعریف کردهایم و همچنین قرار نیست به صورت 'N'day و رشتهای به سمت بانک اطلاعاتی ارسال شود، بلکه باید به همان نحو اصلی آن (یعنی day)، در کوئری نهایی درج گردد، به همین جهت ابتدا Value آنرا استخراج کرده و سپس توسط SqlFragmentExpression عنوان میکنیم آنرا باید به همین نحو درج کرد.
پارامتر اول متد SqlFunctionExpression.Create، باید دقیقا معادل نام متد توکار مدنظر باشد. پارامتر دوم آن، لیست پارامترهای این تابع است. پارامتر سوم آن، نوع خروجی این تابع است که از طریق MethodInfo معادل، قابل استخراج است.
استفادهی از DbFunction سفارشی جدید در برنامه
پس از این تعاریف و معرفیها، اکنون میتوان متد سفارشی SqlDateDiff تهیه شده را به صورت مستقیمی در کوئریهای LINQ استفاده کرد تا قابلیت ترجمهی به SQL را پیدا کنند:
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید: EFCoreDbFunctionsSample.zip
این کدها به همراه چند تابع سفارشی دیگر نیز هستند.
معرفی موجودیت Person
در مثال این مطلب قصد داریم، معادل توابع بومی مخصوص SQL Server را که امکان کار با DateTime را مهیا میکنند، در EF Core تعریف کنیم. به همین جهت نیاز به موجودیتی داریم که دارای خاصیتی از این نوع باشد:
using System; namespace EFCoreDbFunctionsSample.Entities { public class Person { public int Id { get; set; } public string Name { get; set; } public DateTime AddDate { get; set; } } }
گزارشگیری بر اساس تعداد روز گذشتهی از ثبت نام
اکنون فرض کنید میخواهیم گزارشی را از تمام کاربرانی که در طی 10 روز قبل ثبت نام کردهاند، تهیه کنیم. اگر کوئری زیر را برای این منظور تهیه کنیم:
var usersInfo = context.People.Where(person => (DateTime.Now - person.AddDate).Days <= 10).ToList();
'The LINQ expression 'DbSet<Person>.Where(p => (DateTime.Now - p.AddDate).Days <= 10)' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.'
SELECT [p].[Id], [p].[AddDate], [p].[Name] FROM [People] AS [p] WHERE DATEDIFF(Day, [p].[AddDate], GETDATE()) <= 10
روش تعریف تابع DATEDIFF سفارشی در EF Core
برای تعریف متد DateDiff مخصوص EF Core، ابتدا باید یک کلاس static را تعریف کرد و سپس تنها امضای این متد را، معادل امضای تابع توکار SQL Server تعریف کرد. این متد نیازی نیست تا پیاده سازی را داشته باشد. به همین جهت بدنهی آنرا صرفا با یک throw new InvalidOperationException مقدار دهی میکنیم. هدف از این متد، استفادهی از آن در LINQ Expressions است و قرار نیست به صورت مستقیمی بکار گرفته شود:
namespace EFCoreDbFunctionsSample.DataLayer { public enum SqlDateDiff { Year, Quarter, Month, DayOfYear, Day, Week, Hour, Minute, Second, MilliSecond, MicroSecond, NanoSecond } public static class SqlDbFunctionsExtensions { public static int SqlDateDiff(SqlDateDiff interval, DateTime initial, DateTime end) => throw new InvalidOperationException($"{nameof(SqlDateDiff)} method cannot be called from the client side."); public static readonly MethodInfo SqlDateDiffMethodInfo = typeof(SqlDbFunctionsExtensions) .GetRuntimeMethod( nameof(SqlDbFunctionsExtensions.SqlDateDiff), new[] { typeof(SqlDateDiff), typeof(DateTime), typeof(DateTime) } ); } }
روش معرفی تابع DATEDIFF سفارشی به EF Core
پس از تعریف امضای متد معادل DateDiff، اکنون نوبت به معرفی آن به EF Core است:
namespace EFCoreDbFunctionsSample.DataLayer { public class ApplicationDbContext : DbContext { // ... protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.HasDbFunction(SqlDbFunctionsExtensions.SqlDateDiffMethodInfo) .HasTranslation(args => { var parameters = args.ToArray(); var param0 = ((SqlConstantExpression)parameters[0]).Value.ToString(); return SqlFunctionExpression.Create("DATEDIFF", new[] { new SqlFragmentExpression(param0), // It should be written as DateDiff(day, ...) and not DateDiff(N'day', ...) . parameters[1], parameters[2] }, SqlDbFunctionsExtensions.SqlDateDiffMethodInfo.ReturnType, typeMapping: null); }); } } }
سپس توسط متد HasTranslation، مشخص میکنیم که این متد به چه نحوی قرار است به یک عبارت SQL ترجمه شود. پارامتر args ای که در اینجا در اختیار ما قرار میگیرد، دقیقا همان پارامترهای متد public static int SqlDateDiff(SqlDateDiff interval, DateTime initial, DateTime end) هستند که در این مثال خاص، شامل سه پارامتر میشوند. پارامترهای دوم و سوم آنرا به همان نحوی که دریافت میکنیم، به SqlFunctionExpression.Create ارسال خواهیم کرد. اما پارامتر اول را از نوع enum تعریف کردهایم و همچنین قرار نیست به صورت 'N'day و رشتهای به سمت بانک اطلاعاتی ارسال شود، بلکه باید به همان نحو اصلی آن (یعنی day)، در کوئری نهایی درج گردد، به همین جهت ابتدا Value آنرا استخراج کرده و سپس توسط SqlFragmentExpression عنوان میکنیم آنرا باید به همین نحو درج کرد.
پارامتر اول متد SqlFunctionExpression.Create، باید دقیقا معادل نام متد توکار مدنظر باشد. پارامتر دوم آن، لیست پارامترهای این تابع است. پارامتر سوم آن، نوع خروجی این تابع است که از طریق MethodInfo معادل، قابل استخراج است.
استفادهی از DbFunction سفارشی جدید در برنامه
پس از این تعاریف و معرفیها، اکنون میتوان متد سفارشی SqlDateDiff تهیه شده را به صورت مستقیمی در کوئریهای LINQ استفاده کرد تا قابلیت ترجمهی به SQL را پیدا کنند:
var sinceDays = 10; users = context.People.Where(person => SqlDbFunctionsExtensions.SqlDateDiff(SqlDateDiff.Day, person.AddDate, DateTime.Now) <= sinceDays).ToList(); /* SELECT [p].[Id], [p].[AddDate], [p].[Name] FROM [People] AS [p] WHERE DATEDIFF(Day, [p].[AddDate], GETDATE()) <= @__sinceDays_0 */
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید: EFCoreDbFunctionsSample.zip
این کدها به همراه چند تابع سفارشی دیگر نیز هستند.