در باب ضرورت نوشتن کدهای تست پذیر، توسعه کلاسهای کوچک تک مسئولیتی و اهمیت تزریق وابستگیها بارها و بارها بحث شده و مطلب نوشته شده است. این روزها کم پیش میاید که نرم افزاری توسعه داده شود و از پایگاه داده به جهت ذخیره و بازیابی دادهها استفاده نکند. با گسترش و رواج ORM ها، نوشتن کدهای دسترسی به دادهها سهولت یافته است و استفاده از ORM در لایهی سرویس که نگهدارندهی منطق تجاری برنامه است، امری اجتناب ناپذیر میباشد.
در این مطلب نحوهی نوشتن آزمون واحد برای کلاس سرویسی که وابسته به DbContext میباشد، به همراه محدودیتها شرح داده میشود.
ابتدا یک روش که که در آن مستقیما از DbContext در سرویس استفاده شده را بررسی میکنیم. در مثال زیر کلاس ProductService وظیفهی برگرداندن لیست کالاها را به ترتیب نام دارد. در آن DbContext مستقیما وهله سازی شده و از آن جهت انجام تراکنشهای دیتابیس کمک گرفته شده است:
public class ProductService { public IEnumerable<Product> GetOrderedProducts() { using (var ctx = new Entites()) { return ctx.Products.OrderBy(x => x.Name).ToList(); } } }
برای این کلاس نمیتوان Unit Test نوشت چرا که یک وابستگی به شی DbContext دارد و این وابستگی مستقیما درون متد GetOrderedProducts نمونه سازی شده است. در مطالب پیشین شرح داده شد که برای تست پذیر کردن کدها باید این وابستگیها را از بیرون، در اختیار کلاس مورد نظر قرار داد.
برای نوشتن تست برای کلاس ProductService حداقل دو روش در اختیار است:
- نوشتن Integration Test:
یعنی کلاس جاری را به همین شکل نگاه داریم و در تست، مستقیما به یک پایگاه داده که به منظور تست فراهم شده وصل شویم. برای سهولت مدیریت پایگاه داده میتوان عمل درج را در یک Transaction قرار داد و پس از پایان یافتن تست Transaction را RollBack کرد. این روش مورد بحث مطلب جاری نمیباشد، لطفا برای آشنایی این دو مطلب را مطالعه بفرمایید:
- بهره جستن از تزریق وابستگی و نوشتن Unit Test که وابستگی به دیتابیس ندارد
یکی از قانونهای یک آزمون واحد این است که وابستگی به منابع خارجی مثل پایگاه داده نداشته باشد. این مطلب نحوهی صحیح پیاده سازی الگوی Unit of Work را شرح داده است. بعد از پیاده سازی Unit Of Work، کلاس DbContext به شرح زیر میشود. همانطور که مشاهده میکنید، اکنون DbContext یک Interface را پیاده سازی کرده است.
public interface IUnitOfWork { IDbSet<TEntity> Set<TEntity>() where TEntity : class; int SaveAllChanges(); } public class Entites : DbContext, IUnitOfWork { public virtual DbSet<Product> Products { get; set; } // This is virtual because Moq needs to override the behaviour public new virtual IDbSet<TEntity> Set<TEntity>() where TEntity : class // This is virtual because Moq needs to override the behaviour { return base.Set<TEntity>(); } public int SaveAllChanges() { return base.SaveChanges(); } }
در این حالت میتوان به جای وهله سازی مستقیم DbContext در ProductService آن را خارج از کلاس سرویس در اختیار استفاده کننده قرار داد:
public class ProductService { private readonly IDbSet<Product> _products; private readonly IUnitOfWork _uow; public ProductService(IUnitOfWork uow) { _uow = uow; _products = _uow.Set<Product>(); } public IEnumerable<Product> GetOrderedProducts() { return _products.OrderBy(x => x.Name).ToList(); } }
اکنون برای تست این سرویس میتوان پیاده سازی دیگری را از IUnitOfWork انجام داد و در کدهای تست به سرویس مورد نظر تزریق کرد. برای سهولت این امر قصد داریم از moq به عنوان چارچوب تقلید (Mocking framework) استفاده کنیم. برای نصب moq می توان از بستهی نیوگت آن بهره جست. پیشتر مطلبی در رابطه با چارچوبهای تقلید در سایت نوشته شده است.
با توجه به اینکه PoductService به دیتابیس وابستگی دارد، مقصود این است که این وابستگی با ایجاد یک نمونهی mock از IUnitOfWork حذف شود. برای این منظور در سازندهی کلاس، تعدادی کالای درون حافظه ایجاد شده و به صورت IQueryable جایگزین DbSet شده است.
اگر به تعریف کلاس Entities که همان DbContext میباشد دقت کنید، مشاهده میشود که Products و تابع Set، هر دو به صورت Virtual تعریف شده اند. برای تغییر رفتار DbContext نیاز است در آزمون واحد، این دو با دادههای درون حافظه کار کنند و رفتار آنها قرار است عوض شود. این تغییر رفتار از طریق چند ریختی (Polymorphism) خواهد بود.
کلاس تست در نهایت اینگونه تعریف میشود:
[TestFixture] public class ProductServiceTest { private readonly ProductService _productService; public ProductServiceTest() { IQueryable<Product> data = GetRoadNetworks().AsQueryable(); var mockSet = new Mock<DbSet<Product>>(); mockSet.As<IQueryable<Product>>().Setup(m => m.Provider).Returns(data.Provider); mockSet.As<IQueryable<Product>>().Setup(m => m.Expression).Returns(data.Expression); mockSet.As<IQueryable<Product>>().Setup(m => m.ElementType).Returns(data.ElementType); mockSet.As<IQueryable<Product>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator()); var context = new Mock<Entites>(); context.Setup(c => c.Products).Returns(mockSet.Object); context.Setup(m => m.Set<Product>()).Returns(mockSet.Object); _productService = new ProductService(context.Object); } private IEnumerable<Product> GetRoadNetworks() { return new List<Product> { new Product { Id = 1, Name = "A" }, new Product { Id = 2, Name = "B" }, new Product { Id = 3, Name = "C" } }; } [Test] public void GetOrderedProductTest() { IEnumerable<Product> products = _productService.GetOrderedProducts(); List<string> names = products.Select(x => x.Name).ToList(); var expected = new List<string> {"A", "B", "C"}; CollectionAssert.AreEqual(names, expected); } }
در نهایت، در یک تست تلاش شده است تا منطق متد GerOrderedProducts مورد آزمون قرار گیرد.
محدودیت این روش:
با اینکه LINQ یک روش و سینتکس یکتا برای دسترسی به منابع دادهای مختلف را محیا میکند، اما این الزامی برای یکسان بودن نتایج، هنگام استفاده از Providerهای مختلف LINQ نمیباشد. در تست نوشته شده از LINQ To Objects برای کوئری گرفتن از منبع داده استفاده شده است؛ در صورتیکه در برنامهی اصلی از LINQ To Entities استفاده میشود و الزامی نیست که یک کوئری LINQ در دو Provider متفاوت یک رفتار را داشته باشد.
این نکته در قسمت Limitations of EF in-memory test doubles این مطلب هم شرح داده شده است.
در نهایت این پرسش به وجود میآید که با وجود محدودیت ذکر شده، از این روش استفاده شود یا خیر؟ پاسخ این پرسش، بسته به هر سناریو، متفاوت است.
به عنوان نمونه اگر در یک سناریو دادهها با یک کوئری نه چندان پیچیده از منبع داده ای گرفته میشود و اعمال دیگری دیگری روی نتیجهی کوئری درون حافظه انجام میشود میتوان این روش را قابل اعتماد قلمداد کرد.
برای مطالعهی بیشتر مطالب متعددی در سایت در رابطه با تزریق وابستگی و آزمونهای واحد نوشته شده است.
اشتراکها
جنریک چگونه به دات نت اضافه شد ؟
Before we dive into the technical details, let’s start with a quick history lesson, courtesy of Don Syme who worked on adding generics to .NET and then went on to design and implement F#, which is a pretty impressive set of achievements!!
Background and History
- 1999 Initial research, design and planning
- 1999 First ‘white paper’ published
- 2001 C# Language Design Specification created
- 2001 Research paper published
- 2004 Work completed and all bugs fixed
یکی از مواردی که فشاری بر روی garbage collector را بالا میبرد، تخصیصهای حافظهی مخفی یا Hidden allocations هستند که سبب تخصیصهای حافظهی کوچک و عموما پر تعدادی بر روی heap میشوند. برای نمونه به مثال ذیل دقت کنید و سعی کنید تعداد تخصیصهای حافظهی آن را حدس بزنید:
در این مثال ... سه تخصیص حافظهی کوچک رخ میدهد. از این جهت که متد Console.WriteLine ایی که در اینجا استفاده میشود، در نهایت به یک چنین کدی کامپایل خواهد شد:
در این مثال بر روی تمام پارامترهای int دریافتی، عملیات boxing (تبدیل یا cast) به object صورت میگیرد و عملیات boxing، یک نوع allocation است که نتیجهی آن بر روی heap ذخیره میگردد.
روشی برای نمایان ساختن تخصیصهای حافظهی نهان در ویژوال استودیو
اگر از ReSharper استفاده میکنید، افزونهی «Heap Allocations Viewer» آن و یا اگر از VS 2015 و Roslyn استفاده کنید، افزونهی «Roslyn Clr Heap Allocation Analyzer» آن، سبب نمایان شدن allocationهای مخفی میشوند. برای مثال قطعه کد فوق یک چنین نمایشی را پیدا میکند:
در اینجا در ذیل هر سه موردی که عملیات boxing allocation رخ داده، یک خط قرمز کشیده است. یکی از روشهایی که میتواند boxing allocation فوق را حذف کند، بکار گیری متد ToString بر روی مقادیر int است:
همانطور که مشاهده میکنید، اینبار دیگر خبری از خطوط قرمز، ذیل پارامترهای متد Console.WriteLine نیست. باید دقت داشت که ToString نیز سبب تخصیص حافظه میشود، اما اینبار دیگر int32 آن بر روی heap ذخیره نمیگردد. به عبارتی هر دو حالت سبب تخصیص حافظهی یک رشتهی جدید میشوند؛ اما در حالت اول علاوه بر این شیء جدید، شیء int32 نیز بر روی heap ذخیره میگردد.
تشخیص تخصیص اشیاء مخفی با افزونههای Heap Allocations Viewer
نمونهی دیگر پر کاربرد این نوع بهینه سازیها را در مثال ذیل میتوان مشاهده کرد:
این مثال، یک چنین نمایش بصری دارد:
اینبار یک خط زرد رنگ ظاهر شده به همراه یک خط قرمز رنگ. خط قرمز رنگ را پیشتر بررسی کردیم و علت وجودی آن Boxing allocation ایی است که رخ میدهد. خط زرد رنگ در ذیل + ظاهر شدهاست و عنوان میکند که عملیات جمع زدن رشتهها، سبب تخصیص حافظهی یک شیء جدید میشود. رشتهها در دات نت immutable هستند. به همین جهت هر تغییری در آنها، سبب تخصیص یک شیء جدید میشود. بنابراین در همین مثال ساده، دو تخصیص حافظهی مخفی وجود دارند. مورد جمع زدن را با بکارگیری string.Format و مشکل boxing را با ToString میتوان برطرف کرد:
منابع دیگری که سبب تخصیصهای حافظهی مخفی میشوند
تا اینجا دو مورد از منابع متداول تخصیصهای حافظهی مخفی را بررسی کردیم. اما این لیست شامل موارد ذیل نیز میشود:
1) فراخوانی متدهایی با پارامترهایی از نوع param همیشه سبب تخصیص حافظهای جهت تشکیل یک آرایهی در برگیرندهی پارامترهای ارسالی میشود.
2) متدهایی که پارامتر از نوع IEnumerable دارند:
در این مثال هربار که متد Sum فراخوانی شود، یکبار دیگر IEnumerable آن تخصیص خواهد یافت که در تصویر ذیل با enumerator allocation مشخص شدهاست:
برای حل این مشکل فقط کافی است IEnumerable را با List تعویض کنید.
3) کار با LINQ نیز سبب تخصیصهای حافظهی قابل توجهی است. برای مثال در کد پایهی Roslyn، برای رسیدن به حداکثر کارآیی، بسیاری از الگوریتمها را با روشهای غیر LINQ پیاده سازی کردهاند. البته برای تیمی مانند Roslyn رسیدن به یک چنین کارآیی جهت رقابت با سایر محصولات مشابه ضروری بودهاست و گرنه در بسیاری از کارهای متداول، استفاده از LINQ به خوانایی هر چه بیشتر کدها کمک شایانی میکند.
برای مطالعهی بیشتر
Roslyn code base – performance lessons - part 2
Unusual Ways of Boosting Up App Performance. Boxing and Collections
On performance in .NET
public static void PrintSum(int a, int b) { Console.WriteLine("Sum of a {0} b {1} is {2}", a, b, a + b); }
Console::WriteLine(string, object, object, object)
روشی برای نمایان ساختن تخصیصهای حافظهی نهان در ویژوال استودیو
اگر از ReSharper استفاده میکنید، افزونهی «Heap Allocations Viewer» آن و یا اگر از VS 2015 و Roslyn استفاده کنید، افزونهی «Roslyn Clr Heap Allocation Analyzer» آن، سبب نمایان شدن allocationهای مخفی میشوند. برای مثال قطعه کد فوق یک چنین نمایشی را پیدا میکند:
در اینجا در ذیل هر سه موردی که عملیات boxing allocation رخ داده، یک خط قرمز کشیده است. یکی از روشهایی که میتواند boxing allocation فوق را حذف کند، بکار گیری متد ToString بر روی مقادیر int است:
همانطور که مشاهده میکنید، اینبار دیگر خبری از خطوط قرمز، ذیل پارامترهای متد Console.WriteLine نیست. باید دقت داشت که ToString نیز سبب تخصیص حافظه میشود، اما اینبار دیگر int32 آن بر روی heap ذخیره نمیگردد. به عبارتی هر دو حالت سبب تخصیص حافظهی یک رشتهی جدید میشوند؛ اما در حالت اول علاوه بر این شیء جدید، شیء int32 نیز بر روی heap ذخیره میگردد.
تشخیص تخصیص اشیاء مخفی با افزونههای Heap Allocations Viewer
نمونهی دیگر پر کاربرد این نوع بهینه سازیها را در مثال ذیل میتوان مشاهده کرد:
public static void PrintA(int a) { Console.WriteLine("a is " + a); }
اینبار یک خط زرد رنگ ظاهر شده به همراه یک خط قرمز رنگ. خط قرمز رنگ را پیشتر بررسی کردیم و علت وجودی آن Boxing allocation ایی است که رخ میدهد. خط زرد رنگ در ذیل + ظاهر شدهاست و عنوان میکند که عملیات جمع زدن رشتهها، سبب تخصیص حافظهی یک شیء جدید میشود. رشتهها در دات نت immutable هستند. به همین جهت هر تغییری در آنها، سبب تخصیص یک شیء جدید میشود. بنابراین در همین مثال ساده، دو تخصیص حافظهی مخفی وجود دارند. مورد جمع زدن را با بکارگیری string.Format و مشکل boxing را با ToString میتوان برطرف کرد:
public static void PrintA(int a) { Console.WriteLine("a is {0}", a.ToString()); }
منابع دیگری که سبب تخصیصهای حافظهی مخفی میشوند
تا اینجا دو مورد از منابع متداول تخصیصهای حافظهی مخفی را بررسی کردیم. اما این لیست شامل موارد ذیل نیز میشود:
1) فراخوانی متدهایی با پارامترهایی از نوع param همیشه سبب تخصیص حافظهای جهت تشکیل یک آرایهی در برگیرندهی پارامترهای ارسالی میشود.
2) متدهایی که پارامتر از نوع IEnumerable دارند:
public static int Sum(IEnumerable<int> list) { var sum = 0; foreach (var number in list) { sum += number; } return sum; }
برای حل این مشکل فقط کافی است IEnumerable را با List تعویض کنید.
3) کار با LINQ نیز سبب تخصیصهای حافظهی قابل توجهی است. برای مثال در کد پایهی Roslyn، برای رسیدن به حداکثر کارآیی، بسیاری از الگوریتمها را با روشهای غیر LINQ پیاده سازی کردهاند. البته برای تیمی مانند Roslyn رسیدن به یک چنین کارآیی جهت رقابت با سایر محصولات مشابه ضروری بودهاست و گرنه در بسیاری از کارهای متداول، استفاده از LINQ به خوانایی هر چه بیشتر کدها کمک شایانی میکند.
برای مطالعهی بیشتر
Roslyn code base – performance lessons - part 2
Unusual Ways of Boosting Up App Performance. Boxing and Collections
On performance in .NET
فایلهای پروژهها
PdfRpt-2.7.rar
- Added .NET 3.5, 4.0 and 4.5 profiles to the NuGet package. PM> Install-Package PdfReport - Updated the project to use itextsharp.dll 5.5.4.0. - Fixed issue #2392. - Fixed reopening of MemoryStreams. - Added `Merge In-Memory Pdf Files` sample. - Improved Grouping/GroupingPdfReport.cs sample to show how to inject an arbitrary HTML data at the end of the each group.
پیشنیاز مطلب:
پشتیبانی از Full Text Search در SQL Server
Full Text Search یا به اختصار FTS یکی از قابلیتهای SQL Server جهت جستجوی پیشرفته در متون میباشد. این قابلیت تا کنون در EF 6.1.1 ایجاد نشده است.
در ادامه پیاده سازی از FTS در EF را مشاهده مینمایید.
جهت ایجاد قابلیت FTS از متد Contains در Linq استفاده شده است.
ابتدا متدهای الحاقی جهت اعمال دستورات FREETEXT و CONTAINS اضافه میشود.سپس دستورات تولیدی EF را قبل از اجرا بر روی بانک اطلاعاتی توسط امکان Command Interception به دستورات FTS تغییر میدهیم.
همانطور که میدانید دستور Contains در Linq توسط EF به دستور LIKE تبدیل میشود. به جهت اینکه ممکن است بخواهیم از دستور LIKE نیز استفاده کنیم یک پیشوند به مقادیری که میخواهیم به دستورات FTS تبدیل شوند اضافه مینماییم.
جهت استفاده از Fts در EF کلاسهای زیر را ایجاد نمایید.
کلاس FullTextPrefixes :
کلاس جاری جهت علامت گذاری دستورات FTS میباشد و توسط متدهای الحاقی و کلاس FtsInterceptor استفاده میگردد.
کلاس FullTextSearchExtensions :
در این کلاس متدهای الحاقی جهت اعمال قابلیت Fts ایجاد شده است.
متد FreeTextSearch جهت استفاده از دستور FREETEXT استفاده میشود.در این متد پیشوند -FREETEXT- جهت علامت گذاری این دستور به ابتدای مقدار جستجو اضافه میشود. این متد دارای دو پارامتر میباشد ، اولی ستونی که میخواهیم بر روی آن جستجوی FTS انجام دهیم و دومی عبارت جستجو.
متد ContainsSearch جهت استفاده از دستور CONTAINS استفاده میشود.در این متد پیشوند -CONTAINS- جهت علامت گذاری این دستور به ابتدای مقدار جستجو اضافه میشود. این متد دارای دو پارامتر میباشد ، اولی ستونی که میخواهیم بر روی آن جستجوی FTS انجام دهیم و دومی عبارت جستجو.
متد FreeTextSearchImp جهت ایجاد دستور Contains در Linq میباشد.
کلاس FtsInterceptor :
در این کلاس دستوراتی را که توسط متدهای الحاقی جهت امکان Fts علامت گذاری شده بودند را یافته و دستور LIKE را با دستورات CONTAINSو FREETEXT جایگزین میکنیم.
در ادامه برنامه ای جهت استفاده از این امکان به همراه دستورات تولیدی آنرا مشاهده مینمایید.
ابتدا کلاس FtsInterceptor را به EF معرفی مینماییم. سپس از دو متد الحاقی مذکور استفاده مینماییم. خروجی هر دو متد توسط SQL Server Profiler در زیر هر متد مشاهده مینمایید.
تمامی کدها و مثال مربوطه در آدرس https://effts.codeplex.com قرار گرفته است.
منبع:
Full Text Search in Entity Framework 6
پشتیبانی از Full Text Search در SQL Server
Full Text Search یا به اختصار FTS یکی از قابلیتهای SQL Server جهت جستجوی پیشرفته در متون میباشد. این قابلیت تا کنون در EF 6.1.1 ایجاد نشده است.
در ادامه پیاده سازی از FTS در EF را مشاهده مینمایید.
جهت ایجاد قابلیت FTS از متد Contains در Linq استفاده شده است.
ابتدا متدهای الحاقی جهت اعمال دستورات FREETEXT و CONTAINS اضافه میشود.سپس دستورات تولیدی EF را قبل از اجرا بر روی بانک اطلاعاتی توسط امکان Command Interception به دستورات FTS تغییر میدهیم.
همانطور که میدانید دستور Contains در Linq توسط EF به دستور LIKE تبدیل میشود. به جهت اینکه ممکن است بخواهیم از دستور LIKE نیز استفاده کنیم یک پیشوند به مقادیری که میخواهیم به دستورات FTS تبدیل شوند اضافه مینماییم.
جهت استفاده از Fts در EF کلاسهای زیر را ایجاد نمایید.
کلاس FullTextPrefixes :
/// <summary> /// /// </summary> public static class FullTextPrefixes { /// <summary> /// /// </summary> public const string ContainsPrefix = "-CONTAINS-"; /// <summary> /// /// </summary> public const string FreetextPrefix = "-FREETEXT-"; /// <summary> /// /// </summary> /// <param name="searchTerm"></param> /// <returns></returns> public static string Contains(string searchTerm) { return string.Format("({0}{1})", ContainsPrefix, searchTerm); } /// <summary> /// /// </summary> /// <param name="searchTerm"></param> /// <returns></returns> public static string Freetext(string searchTerm) { return string.Format("({0}{1})", FreetextPrefix, searchTerm); } }
کلاس جاری جهت علامت گذاری دستورات FTS میباشد و توسط متدهای الحاقی و کلاس FtsInterceptor استفاده میگردد.
کلاس FullTextSearchExtensions :
public static class FullTextSearchExtensions { /// <summary> /// /// </summary> /// <typeparam name="TEntity"></typeparam> /// <param name="source"></param> /// <param name="expression"></param> /// <param name="searchTerm"></param> /// <returns></returns> public static IQueryable<TEntity> FreeTextSearch<TEntity>(this IQueryable<TEntity> source, Expression<Func<TEntity, object>> expression, string searchTerm) where TEntity : class { return FreeTextSearchImp(source, expression, FullTextPrefixes.Freetext(searchTerm)); } /// <summary> /// /// </summary> /// <typeparam name="TEntity"></typeparam> /// <param name="source"></param> /// <param name="expression"></param> /// <param name="searchTerm"></param> /// <returns></returns> public static IQueryable<TEntity> ContainsSearch<TEntity>(this IQueryable<TEntity> source, Expression<Func<TEntity, object>> expression, string searchTerm) where TEntity : class { return FreeTextSearchImp(source, expression, FullTextPrefixes.Contains(searchTerm)); } /// <summary> /// /// </summary> /// <typeparam name="TEntity"></typeparam> /// <param name="source"></param> /// <param name="expression"></param> /// <param name="searchTerm"></param> /// <returns></returns> private static IQueryable<TEntity> FreeTextSearchImp<TEntity>(this IQueryable<TEntity> source, Expression<Func<TEntity, object>> expression, string searchTerm) { if (String.IsNullOrEmpty(searchTerm)) { return source; } // The below represents the following lamda: // source.Where(x => x.[property].Contains(searchTerm)) //Create expression to represent x.[property].Contains(searchTerm) //var searchTermExpression = Expression.Constant(searchTerm); var searchTermExpression = Expression.Property(Expression.Constant(new { Value = searchTerm }), "Value"); var checkContainsExpression = Expression.Call(expression.Body, typeof(string).GetMethod("Contains"), searchTermExpression); //Join not null and contains expressions var methodCallExpression = Expression.Call(typeof(Queryable), "Where", new[] { source.ElementType }, source.Expression, Expression.Lambda<Func<TEntity, bool>>(checkContainsExpression, expression.Parameters)); return source.Provider.CreateQuery<TEntity>(methodCallExpression); }
در این کلاس متدهای الحاقی جهت اعمال قابلیت Fts ایجاد شده است.
متد FreeTextSearch جهت استفاده از دستور FREETEXT استفاده میشود.در این متد پیشوند -FREETEXT- جهت علامت گذاری این دستور به ابتدای مقدار جستجو اضافه میشود. این متد دارای دو پارامتر میباشد ، اولی ستونی که میخواهیم بر روی آن جستجوی FTS انجام دهیم و دومی عبارت جستجو.
متد ContainsSearch جهت استفاده از دستور CONTAINS استفاده میشود.در این متد پیشوند -CONTAINS- جهت علامت گذاری این دستور به ابتدای مقدار جستجو اضافه میشود. این متد دارای دو پارامتر میباشد ، اولی ستونی که میخواهیم بر روی آن جستجوی FTS انجام دهیم و دومی عبارت جستجو.
متد FreeTextSearchImp جهت ایجاد دستور Contains در Linq میباشد.
کلاس FtsInterceptor :
public class FtsInterceptor : IDbCommandInterceptor { /// <summary> /// /// </summary> /// <param name="command"></param> /// <param name="interceptionContext"></param> public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext) { } /// <summary> /// /// </summary> /// <param name="command"></param> /// <param name="interceptionContext"></param> public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext) { } /// <summary> /// /// </summary> /// <param name="command"></param> /// <param name="interceptionContext"></param> public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) { RewriteFullTextQuery(command); } /// <summary> /// /// </summary> /// <param name="command"></param> /// <param name="interceptionContext"></param> public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) { } /// <summary> /// /// </summary> /// <param name="command"></param> /// <param name="interceptionContext"></param> public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext) { RewriteFullTextQuery(command); } /// <summary> /// /// </summary> /// <param name="command"></param> /// <param name="interceptionContext"></param> public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext) { } /// <summary> /// /// </summary> /// <param name="cmd"></param> public static void RewriteFullTextQuery(DbCommand cmd) { var text = cmd.CommandText; for (var i = 0; i < cmd.Parameters.Count; i++) { var parameter = cmd.Parameters[i]; if ( !parameter.DbType.In(DbType.String, DbType.AnsiString, DbType.StringFixedLength, DbType.AnsiStringFixedLength)) continue; if (parameter.Value == DBNull.Value) continue; var value = (string)parameter.Value; if (value.IndexOf(FullTextPrefixes.ContainsPrefix, StringComparison.Ordinal) >= 0) { parameter.Size = 4096; parameter.DbType = DbType.AnsiStringFixedLength; value = value.Replace(FullTextPrefixes.ContainsPrefix, ""); // remove prefix we added n linq query value = value.Substring(1, value.Length - 2); // remove %% escaping by linq translator from string.Contains to sql LIKE parameter.Value = value; cmd.CommandText = Regex.Replace(text, string.Format( @"\[(\w*)\].\[(\w*)\]\s*LIKE\s*@{0}\s?(?:ESCAPE N?'~')", parameter.ParameterName), string.Format(@"CONTAINS([$1].[$2], @{0})", parameter.ParameterName)); if (text == cmd.CommandText) throw new Exception("FTS was not replaced on: " + text); text = cmd.CommandText; } else if (value.IndexOf(FullTextPrefixes.FreetextPrefix, StringComparison.Ordinal) >= 0) { parameter.Size = 4096; parameter.DbType = DbType.AnsiStringFixedLength; value = value.Replace(FullTextPrefixes.FreetextPrefix, ""); // remove prefix we added n linq query value = value.Substring(1, value.Length - 2); // remove %% escaping by linq translator from string.Contains to sql LIKE parameter.Value = value; cmd.CommandText = Regex.Replace(text, string.Format( @"\[(\w*)\].\[(\w*)\]\s*LIKE\s*@{0}\s?(?:ESCAPE N?'~')", parameter.ParameterName), string.Format(@"FREETEXT([$1].[$2], @{0})", parameter.ParameterName)); if (text == cmd.CommandText) throw new Exception("FTS was not replaced on: " + text); text = cmd.CommandText; } } } }
در این کلاس دستوراتی را که توسط متدهای الحاقی جهت امکان Fts علامت گذاری شده بودند را یافته و دستور LIKE را با دستورات CONTAINSو FREETEXT جایگزین میکنیم.
در ادامه برنامه ای جهت استفاده از این امکان به همراه دستورات تولیدی آنرا مشاهده مینمایید.
public class Note { /// <summary> /// /// </summary> public int Id { get; set; } /// <summary> /// /// </summary> public string NoteText { get; set; } } public class FtsSampleContext : DbContext { /// <summary> /// /// </summary> public DbSet<Note> Notes { get; set; } /// <summary> /// /// </summary> /// <param name="modelBuilder"></param> protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Configurations.Add(new NoteMap()); } } /// <summary> /// /// </summary> class Program { /// <summary> /// /// </summary> /// <param name="args"></param> static void Main(string[] args) { DbInterception.Add(new FtsInterceptor()); const string searchTerm = "john"; using (var db = new FtsSampleContext()) { var result1 = db.Notes.FreeTextSearch(a => a.NoteText, searchTerm).ToList(); //SQL Server Profiler result ===>>> //exec sp_executesql N'SELECT // [Extent1].[Id] AS [Id], // [Extent1].[NoteText] AS [NoteText] // FROM [dbo].[Notes] AS [Extent1] // WHERE FREETEXT([Extent1].[NoteText], @p__linq__0)',N'@p__linq__0 //char(4096)',@p__linq__0='(john)' var result2 = db.Notes.ContainsSearch(a => a.NoteText, searchTerm).ToList(); //SQL Server Profiler result ===>>> //exec sp_executesql N'SELECT // [Extent1].[Id] AS [Id], // [Extent1].[NoteText] AS [NoteText] // FROM [dbo].[Notes] AS [Extent1] // WHERE CONTAINS([Extent1].[NoteText], @p__linq__0)',N'@p__linq__0 //char(4096)',@p__linq__0='(john)' } Console.ReadKey(); } }
ابتدا کلاس FtsInterceptor را به EF معرفی مینماییم. سپس از دو متد الحاقی مذکور استفاده مینماییم. خروجی هر دو متد توسط SQL Server Profiler در زیر هر متد مشاهده مینمایید.
تمامی کدها و مثال مربوطه در آدرس https://effts.codeplex.com قرار گرفته است.
منبع:
Full Text Search in Entity Framework 6
اشتراکها
NET MAUI. چیست؟
Here’s what’s new in this preview release:
- Parity with existing experiences for minimal APIs
- Added
IResult
implementations for producing common HTTP responses - Support Request, Response and User for minimal actions
- Minimal host and template improvements
- Supply Blazor component parameters from the query string
- Replace the current URI in the browser history from Blazor
- New
DynamicComponent.Instance
property - Blazor streaming interop from JavaScript to .NET
- Large file upload & faster file uploads with Blazor
- Modify HTML
<head>
content from Blazor components - Support for the
multiple
attribute on<select>
elements in Blazor - Support for HTTP/3 in Kestrel
- QUIC support moved to the shared framework
- Allow control over
Activity
creation - Support for non-ASCII characters in Kestrel response headers
- Add W3CLogger
- Add authentication expiration option to SignalR
اشتراکها
مستندات جدید StructureMap
StructureMap is the oldest, continuously used IoC/DI container for .Net dating back to its first public release and production usage all the way back in June 2004 on .Net 1.1. The current 3.* release represents 10+ years of lessons learned in the StructureMap and greater .Net community -- while also wiping away a great deal of legacy design decisions that no longer make sense today.