پشتیبانی از 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
public class OnlineUserMiddleWare { private readonly RequestDelegate _next; private readonly IMemoryCache _memoryCache; public OnlineUserMiddleWare(RequestDelegate next, IMemoryCache memoryCache) { _next = next; _memoryCache = memoryCache; } public async Task Invoke(HttpContext context) { if (!_memoryCache.TryGetValue("OnlineUsers", out Dictionary<string,DateTime> onlineUsers)) { onlineUsers = new Dictionary<string, DateTime>(); _memoryCache.Set("OnlineUsers", onlineUsers); } if (context.User.Identity.IsAuthenticated) { var name = context.User.Identity.Name; if (name != null) { if (onlineUsers.ContainsKey(name)) onlineUsers[name] = DateTime.Now; else onlineUsers.Add(name, DateTime.Now); } } foreach (var online in onlineUsers) { if (online.Value < DateTime.Now.AddMinutes(-10)) onlineUsers.Remove(online.Key); } await _next(context); } }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider) { ... app.UseMiddleware<OnlineUserMiddleWare>(); ... }
public class DashboardController { private readonly IMemoryCache _memoryCache; public DashboardController(memoryCache) { _memoryCache = memoryCache; } public IActionResult Index() { _memoryCache.TryGetValue("OnlineUsers", out Dictionary<string, DateTime> onlineUsers); ViewBag.OnlineUsers = onlineUsers.Count; return View(); } }
if(context.Request.Path.StartsWithSegments("/Identity/Account/Login")) { foreach (var online in onlineUsers) { if (online.Value < DateTime.Now.AddMinutes(-10)) onlineUsers.Remove(online.Key); } }
- شناسایی منابع
- بکارگیری منابع از طریق نمایش آنها (منابع وب)
- پیامهای خودتوصیفی
- ابررسانه بعنوان قلب تپنده موقعیت برنامه
- درگیر نکردن برنامه “
- توزیع (HTTP , Feeds)
- ترکیب (Hypermedia , Mashups)
- امنیت (Open ID, SSL)
- قابلیت انتقال داده (XML,RDF)
- قابلیت نمایش داده (ATOM, JSON)
- متدهای انتقال (REST, HTTP, Bit Torrent)
- هر چیزی یک منبع است.
- هر منبعی یک تمثیل دارد.
- هر منبعی یک نام بخصوص دارد.
- انتقال موقعیت نیازمند کشف و شهود (Discovery) است.
- پروتکل شبکه پایه WOA میباشد
- اطلاعات در قالب منابع (Resources) نمایش مییابند.
- منابع توسط URIها شناخته میشوند.
- منابع از طریق HTTP اداره میشوند.
- معاهدات به صورت ضمنی در نمایش منابع میباشند.
- رابطها بطور کلی عام هستند.
- ساده سازی توسعه پذیری، مقیاس پذیری
- کاهش زمان توسعه ویژگیهای جدید
- کاهش زمان مهندسی مورد نیاز برای یکپارچه سازی
- سازنده فرصتهایی جدید برای mash-ups و دیگر داستانهای غیرقابل پیش بینی کاربری
- اما وضعیت ارتباطی کلاینتها و سرورها در WOA چگونه است ؟
- سرویسها وابسته به دیگر سرویسها هستند.
- ارتباطات از طریق HTTP صورت میگیرد.
- کلاینتها حکم منبع و سرویس دهی به دیگر کلاینتها را دارند.
- مقیاس پذیری ، توسعه پذیری == اتصالات داخی
بعنوان مثال میتوان با ترکیب تصاویر و آدرسهای مختلف دانشگاههای تهران، یک map Mashup درست کرد.
برای Photo Mashup ابزار Color Picker هم هست که امکان جستجوی تصاویر را بر اساس رنگ فراهم میکند و از سرویس اشتراک گذاری عکس Flickr استفاده میکند که در این آدرس قابل استفاده است.
معماری Mashup هم مثل معماری MVC (البته با تفاوتهای فاحش) سه لایهای است :
لایه نمایش / تعامل کاربر (همان رابط کاربری است)
تکنولوژیها : HTML/XHTML, CSS, Javascript, Asynchronous JS and Xml (Ajax).
وب سرویسها : عملکرد محصول از طریق سرویسهای API هم قابل دسترسی است
تکنولوژیها : XMLHTTPRequest, XML-RPC, JSON-RPC, SOAP, REST
داده : فراهم آوردن امکان ارسال ، مرتب سازی و دریافت داده
تکنولوژیها : XML , JSON , KML
از نظر معماری Mashup دارای 2 سبک است : الف) مبتنی بر وب – ب) مبتنی بر سرور
در ادامه با هم نمونه ای از استقرار معماری وب گرا WOA را در سازمان، بصورت شماتیک میبینیم. با هم مشاهده میکنیم با این پیاده سازی، موانع سر راه ما کاهش پیدا میکنند و سرعت یکپارچگی افزایش پیدا میکند. بدین صورت که میتوان از قدرت شبکه جهانی وب در جهت انتقال محتوای مورد نیازمان به هر جا و در هر زمانی بهره جست.
شاید برای شما سوال پیش بیاید که ما در معماری وب گرا بحث میکردیم، اصلا چرا وارد مفهوم Mashup شدیم؟
بهعبارت فنیتر چرا معماری وب گرا (WOA) برای Mashups اهمیت دارد ؟
پاسخ یک کلمه است : REST . همانطور که بالاتر نیز اشاره کردم، Mashup از REST بهره میبرد. به منظور افزایش اطلاعات در رابطه با REST باید گفت Roy Fielding آنرا بنیان نهادهاست. میخواهید او را بهتر معرفی کنم؟ وی یکی از خالقان HTTP است و مگر میتوان وب را بدون HTTP فرض کرد که مهمترین پروتکل انتقال ابر متن در جهان و پروتکل زیربنایی وب است؟!
REST به خوبی با معماری اینترنت عجین شده است! بپرسید چرا؟ چون پروتکل اصلی اینترنت HTTP است و هر دوی اینها از یک ذهن نشات گرفته و او کسی نیست جز Roy Fielding. اما باید بگویم REST یک استاندارد نیست؛ با وجود سادگی بسیار زیاد، تنها یک سبک استفاده از HTTP است.
REST همچنین از متدهای اختصاصی HTTP نظیر GET, PUT , POST , DELETE در بالای یک URL استفاده میکند تا نشان دهد چه رویدادی رخ میدهد.
در پایان گفتهها در رابطه با REST باید بگویم ATOM همان REST است. منظورم از ATOM ویرایشگر معروف متنی نیست که برای نوشتن کدهای برنامه نویسی استفاده قرار میگیرد؛ آنرا غالبا به شکل Atom مینویسند چرا که مخفف چند کلمه نیست و یک کلمه خاص است اما ATOM یک استاندارد وب به زبان XML است که برای خوراک وب بعنوان جایگزینی برای RSS استفاده میشود. ATOM را با AtomPub یا APP اشتباه نگیرید؛ چرا که APP پروتکل انتشاری است مبتنی بر پروتکل انتقال ابرمتن (HTTP) و برای به روزرسانی محتوی وب مورد استفاده قرار میگیرد.
در ادامه مباحث دررابطه با معماری وب گرا باید گفت WOA امروزه بعنوان مدل حاکم برنامههای تحت شبکه مطرح است. اما متاسفانه فروشندگان بزرگی در پشت آن حضور ندارند به همین دلیل آنچنان که باید عمومیت نیافته است. WOA همچنان میتواند بیشترین سود حاصل را از طریق شبکه فراهم کند. شاید بتوان گفت کوتاهترین مسیر برای رسیدن به چنین نتیجهای همین معماری وب گرا است.
فرمول جالبی هم برای تعریف وب ارائه شدهاست که با هم میبینیم :
HTTP + URIs = Web
ظرافت فرمول بالا به اهمیت پروتکل زیربنایی وب یعنی HTTP اشاره دارد. URI هم مجموعهای از رشتههاست که برای شناسایی یک منبع خاص تحت وب به کار میروند. در شکل زیر رابطه بین URI , URN , URL را بررسی میکنیم. URI تشکیل شدهاست از URL و URN .URL متد دسترسی به منبع را مشخص میکند، در حالیکه URN تنها مشخص کننده نام منبع میباشد و هیچگونه روشی را برای دسترسی به ما ارائه نمیدهد. بعنوان مثال یک شماره ISBN کتاب، یک نوع URN است.
برای اغلب توسعه دهندههای دات نت (برنامههای وب و دسکتاپ) تنها یک دات نت فریم ورک شناخته شده وجود دارد: The `Full` .NET Framework
که تنها بر روی ویندوز قابل اجرا است و آخرین نگارش پایدار آن در زمان نگارش این مطلب، 4.6.1 است. این فریم ورک بزرگ، از اجزایی تشکیل شدهاست که در تصویر ذیل قابل مشاهدهاند:
مهمترین قسمتهای این فریم ورک «بزرگ» شامل مواردی مانند CLR که کار تبدیل کدهای IL را به کدهای ماشین انجام میدهد، BCL که کلاسهای پایهای را جهت کار با IO، Text و غیره، فراهم میکنند، هستند؛ به علاوه کتابخانههایی مانند Windows Forms، WPF و ASP.NET که برفراز BCL و CLR کار میکنند.
هرچند تعدادی از توسعه دهندههای دات نت تنها با Full framework کار میکنند، اما در طی سالهای اخیر انشعابات بسیار دیگری از آن به وجود آمدهاند؛ مانند دات نتهای ویژهی ویندوزهای 8 و Universal Windows Platform، دات نت مخصوص ویندوز فون 8 و ویندوز فون مبتنی بر پلتفرم سیلورلایت، به علاوه دات نت پلتفرم زامارین برای توسعهی برنامههای iOS و Android نیز هم اکنون وجود دارند (البته در اینجا Mono، دات نت میکرو و غیره را هم باید ذکر کرد). این فریم ورکها و انشعابات، به همراه پیاده سازی یک سری موارد مشترک و مواردی کاملا اختصاصی هستند که به سایر پلتفرمهای دیگر قابل انتقال نیستند.
با زیاد شدن تعداد انشعابات دات نت «بزرگ»، نوشتن کدی که قابل اجرای بر روی تمام پلتفرمهای یاد شده باشد، مشکل شد. اینجا بود که مفهومی را به نام PCL یا Portable class libraries معرفی کردند:
هدف از PCLها، ساده سازی کامپایل و به اشتراک گذاری کد بین پلتفرمهای مختلف بود و پشتیبانی قابل توجهی هم از آن در VS.NET وجود دارد. هرچند این روش نسبتا موفق بود اما مشکلاتی را هم به همراه داشت. برای مثال با ارائهی یک انشعاب و پلتفرم دیگری از دات نت «بزرگ»، کتابخانهی PCL موجود، باید برای این انشعاب جدید مجددا کامپایل شود. به علاوه در اینجا تنها محدود به انتخاب امکانات مشترک بین پلتفرمهای مختلف هستید.
برای رفع این مشکلات در پایان سال 2014، یک «دات نت فریم ورک جدید» به نام NET Core. معرفی شد که سورس باز است و همچنین چندسکویی (از ویندوز، لینوکس و OSX پشتیبانی میکند).
هرچند پیشتر Windows Store و ASP.NET Core app به صورت پلتفرمهایی مجزا ارائه شده بودند، اما اکنون از یک BCL مشترک به نام CoreFX استفاده میکنند و نحوهی توزیع آنها صرفا از طریق نیوگت است. به عبارتی اینبار بجای دریافت یک فریم ورک «بزرگ»، تنها اجزایی را دریافت میکنید که از طریق نیوگت سفارش دادهاید.
به این ترتیب نه تنها کار توزیع برنامههای مبتنی بر NET Core. با سهولت بیشتری انجام خواهد شد، بلکه به روز رسانی اجزای یک برنامه، تاثیری بر روی سایر برنامهها نخواهد داشت و مشکلات جانبی را به وجود نمیآورد. به علاوه دیگر نیازی نیست تا منتظر یک نگارش «بزرگ» دیگر باشید تا بتوانید آخرین به روز رسانیها را دریافت کنید. اینبار به روز رسانی بستههای نیوگت برنامه معادل هستند با به روز رسانی کل فریم ورک در نگارشهای قبلی «بزرگ» آن. در اینجا حتی CoreCLR و NET Native runtime. که مربوط به Windows runtime است هم از طریق نیوگت به روز رسانی میشود.
البته NET Core. انتهای مسیر نیست و هم اکنون NETStandard نیز جهت رفع مشکلات کامپایل مجدد PCLها در حال توسعه است و پس از ارائهی آن، PCLها منسوخ شده درنظر گرفته میشوند. در این حالت با انتخاب target platform ایی به نام NETStandard (بجای مثلا انتخاب دات نت 4.5 و ویندوز فون 8)، اینبار دات نت 4.5 و ویندوز فون 8 و تمام پلتفرمهای دیگر، به صورت یکجا انتخاب میشوند و اگر پلتفرم جدیدی برای مثال از NETStandard نگارش 1.1 پشتیبانی کند، به این معنا است که کتابخانهی شما هم اکنون با آن سازگار است و دیگر نیازی به کامپایل مجدد آن نخواهد بود.
به علاوه هر برنامهای که بر اساس NETStandard تهیه شود، قابلیت اجرای بر روی NET Core. را نیز خواهد داشت. به عبارتی برنامههای NETStandard همان برنامههای مبتنی بر NET Core. هستند.
ASP.NET Core چیست؟
در زمان نگارش این مطلب، دو گزینهی برنامههای وب ASP.NET Core 1.0 و همچنین Windows Store apps (مبتنی بر NET Native Runtime.) قابلیت استفادهی از این پلتفرم جدید NET Core. دارند.
ASP.NET Core 1.0، که پیشتر با نام ASP.NET 5 معرفی شده بود، بازنویسی کامل ASP.NET است که با ایدهی کاملا ماژولار بودن، تهیه شدهاست و از طریق آن، قابلیت به روز رسانی منظم و توزیع آسان از طریق نیوگت، میسر خواهد شد. به علاوه در آن، بسیاری از الگوهای برنامه نویسی شیءگرا مانند تزریق وابستگیها، به صورت توکار و از ابتدا پشتیبانی میشوند.
ASP.NET Core 1.0 از WebForms ، VB ، WebPages و SignalR پشتیبانی نمیکند. البته در این بین عدم پشتیبانی از «وب فرمها» قطعی است؛ اما افزودن سه مورد دیگر یاد شده، جزو لیست کارهای پس از ارائهی نگارش 1 این فریم ورک قرار دارند و به زودی ارائه خواهند شد.
اکنون وضعیت ASP.NET MVC 5 و ASP.NET Web API 2 چگونه است؟
ASP.NET Core 1.0 مدل برنامه نویسی ASP.NET MVC و Web API را به صورت یکپارچه ارائه میدهد و دیگر خبری از ارائهی مجزای اینها نخواهد بود و دقیقا بر مبنای مفاهیم برنامه نویسی این دو بنا شدهاست. به صورت خلاصه MVC + Web API + Web Pages = Core MVC 1.0
پیشتر فضای نام System.Web.MVC مخصوص ASP.NET MVC بود و فضای نام مجزای دیگری به نام System.Web.Http مخصوص ASP.NET Web API. اما اکنون تنها یک فضای نام مشترک و یکپارچه به نام Microsoft.AspNet.Mvc هر دوی اینها را پوشش میدهد.
در این نگارش جدید وابستگی از system.web مبتنی بر IIS حذف شدهاست و با استفاده از هاست جدید چندسکویی به نام Kesterl، به سرعتی 5 برابر سرعت NodeJS دست یافتهاند.
آخرین تاریخ به روز رسانی ASP.NET MVC 5.x دوشنبه، 20 بهمن 1393 است (با ارائه نگارش 5.2.3 که آخرین نگارش رسمی و پایدار آن است) و آخرین تاریخ به روز رسانی ASP.NET Web API 2.x نیز همان روز است.
هرچند مایکروسافت عادت به اعلام رسمی پایان پشتیبانی از بسیاری از محصولات خود را ندارد اما تمام فناوریهای «قدیمی» خودش را بر روی CodePlex نگهداری میکند و تمام فناوریهای «جدید» را به GitHub منتقل کردهاست. بنابراین اگر در مورد فناوری خاصی به Codeplex رسیدید، یعنی «دیگر ادامهی رسمی نخواهد یافت» و حداکثر در حد رفع یک سری باگها و مشکلات گزارش شده باقی میمانند.
مثال 1: هم اکنون نگارش دوم ASP.NET Identity را بر روی Codeplex میتوانید مشاهده کنید. نگارش سوم آن به GitHub منتقل شدهاست که این نگارش صرفا با ASP.NET Core 1.0 سازگار است. در مورد ASP.NET MVC و Web API نیز چنین حالتی رخ دادهاست. نگارشهای 5 و 2 آنها بر روی Codeplex موجود هستند و نگارش ششم که به ASP.NET Core 1.0 تغییر نام یافت و ترکیبی است از MVC و Web API، در GitHub توسعه مییابد.
مثال 2: WCF به علت پیچیدگی بیش از حد و مدرن نبودن طراحی آن، رقابت را به ASP.NET Web API 2.x واگذار کرد و مدل برنامه نویسی ASP.NET Web API 2.x نیز هم اکنون جزئی از ASP.NET Core 1.0 است. بنابراین اگر قصد ایجاد پروژهی جدیدی را بر این مبنا دارید، بهتر است با APS.NET Core 1.0 کار را شروع کنید.
اما هنوز تعداد زیادی از کتابخانههای Full framework به NET Core. انتقال پیدا نکردهاند
برای نمونه هنوز EF Core 1.0 که پیشتر نام EF 7.x به آن داده شده بود، به مرحلهی نهایی تکمیل قابلیتهای آن نرسیدهاست. اما باید دانست که ASP.NET Core 1.0 صرفا بر فراز NET Core. قابل اجرا نیست؛ بلکه قابلیت اجرای بر فراز NET 4.6. و یا همان دات نت «بزرگ و کامل» را نیز دارد. بنابراین به سادگی قابلیت اجرای EF 6.x و یا NHibernate را نیز دارا است. تنها مزیتی را که در اینجا از دست خواهید، قابلیت چندسکویی بودن ASP.NET Core 1.0 است؛ زیرا EF 6.x با چنین دیدی طراحی نشدهاست.
همانطور که ملاحظه میکنید، ASP.NET Core 1.0 قابلیت اجرای بر روی هر دوی NET Core 1.0. و NET 4.6. را دارا است. اما یکی، چندسکویی است و دیگری صرفا مختص به ویندوز.
فناورهای منسوخ شدهی در NET Core.
یکسری از فناوریها و کتابخانهها احتمالا هیچگاه قابلیت انتقال به NET Core. را نخواهند یافت و یا حداقل باید تا چندنگارش بعدی آن صبر کنند. فناوریهای خاتمه یافتهی با NET Core. به شرح زیر هستند:
- Reflection: همانطور که عنوان شد، NET Core. بر فراز CoreCLR و همچنین NET Native runtime. اجرا میشود و تولید برنامههای native و static linking آنها مانند برنامههای ++C، نیاز به دانستن اجزایی دارد که به صورت پویا فراخوانی نمیشوند و بلافاصله و در زمان کامپایل، توسط کامپایلر قابل تشخیص هستند. همین محدودیت سبب شدهاست که استفادهی از Reflection در NET Core. به حداقل ممکن آن برسد. برای مثال در System.Object متد GetType آن تنها نام نوع را باز میگرداند و نه اطلاعات بیشتری را مانند GetMembers سابق.
- App Domains: هرچند CoreCLR از App Domains پشتیبانی میکند اما NET Native runtime. خیر. به همین جهت برای ایزوله سازی برنامهها توصیه شدهاست که از containerهایی مانند docker استفاده شود.
- Remoting: پیش از WCF جهت برقراری ارتباط بین برنامهها مطرح شده بود و هم اکنون در دات نت کامل هم آنچنان استفادهای از آن نمیشود.
- binary serialization: البته کتابخانههایی مانند JSON.NET و امثال آن، نگارش NET Core. هم دارند؛ اما چون binary serialization نیاز به اطلاعات reflection قابل توجهی دارد دیگر پشتیبانی نخواهد شد.
فناورهایی که به زودی به NET Core. منتقل میشوند
یکسری از فناوریها مانند XAML هنوز معادل NET Core. ندارند و لیست زیر قرار است از طرف مایکروسافت سورس باز شده و همچنین به NET Core. منتقل شود:
System.Data
System.DirectoryServices
System.Drawing
System.Transactions
System.Xml.Xsl and System.Xml.Schema
System.Net.Mail
System.IO.Ports
System.Workflow
System.Xaml
مراحل نصب ASP.NET Core 1.0
پیش از نصب نگارش 1.0 RTM باید به این نکته دقت داشت که نصاب آن، نگارشهای آزمایشی قبلی را حذف و یا بازنویسی نمیکند و همین مساله ممکن است سبب بروز تداخلهایی و یا حتی از کار افتادن VS.NET شما شود. بنابراین اگر نگارشهای RC یا بتا را پیشتر نصب کردهاید، به Add remove programs ویندوز مراجعه کرده و سه مورد ذیل را حتما حذف کنید (خیلی مهم):
- Preview Tooling (all versions)
- NET Core Runtime SDK (all versions).
- NET Core Runtime (all Versions).
پس از حذف بستههای قدیمی، برای نصب نگارش 1.0 RTM، ابتدا نیاز است Visual Studio 2015 Update 3 را نصب کنید و پس از آن با استفاده از NET Core for Visual Studio Official MSI Installer. کار نصب اجزای مورد نیاز آن انجام خواهد شد.
بررسی شماره نگارش 1.0 RTM
پس از نصب اجزای عنوان شده، خط فرمان را گشوده و دستور ذیل را صادر کنید:
C:\Users\Vahid>dotnet --version 1.0.0-preview2-003121
اگر یک پروژهی خالی ASP.NET Core Web Application را نیز شروع کنید (با طی مراحل زیر جهت ایجاد یک پروژهی جدید):
.NET Core -> ASP.NET Core Web Application (.NET Core) -> Select `Empty` Template
در اینجا فایل جدیدی را به نام global.json مشاهده میکنید که محتوایات آن شامل دقیقا همین شماره نگارش است؛ به همراه معرفی پوشههای اصلی پروژه:
{ "projects": [ "src", "test" ], "sdk": { "version": "1.0.0-preview2-003121" } }
معمولا برای شما هم پیش آمده که به عنوان ادمین شبکه گاها دیده اید که کاربران شبکه از تغییر کلمه عبور خود نالان هستند.
بسته به سیاستهای تغییر کلمه عبور و موارد مورد نیاز که بایستی حتما در کلمه عبور لحاظ شوند گاهی پیش میآید که کاربران در تغییر کلمه عبور خود دچار مشکل شوند.
ابزاری که در این لینک معرفی شده است پیامهای مناسب را به کاربر نشان داده تا کاربر بهتر بتواند دلیل قبول نکردن کلمه عبور جدید خود را در سیستم متوجه شود
تهیه سرویس اطلاعات پویای برنامه
سرویس Web API ارائه شدهی توسط ASP.NET Core در این برنامه، لیست کاربران را به همراه یادداشتهای آنها به سمت کلاینت باز میگرداند و ساختار موجودیتهای آنها به صورت زیر است:
موجودیت کاربر که یک رابطهی one-to-many را با UserNotes دارد:
using System; using System.Collections.Generic; namespace MaterialAspNetCoreBackend.DomainClasses { public class User { public User() { UserNotes = new HashSet<UserNote>(); } public int Id { set; get; } public DateTimeOffset BirthDate { set; get; } public string Name { set; get; } public string Avatar { set; get; } public string Bio { set; get; } public ICollection<UserNote> UserNotes { set; get; } } }
using System; namespace MaterialAspNetCoreBackend.DomainClasses { public class UserNote { public int Id { set; get; } public DateTimeOffset Date { set; get; } public string Title { set; get; } public User User { set; get; } public int UserId { set; get; } } }
public interface IUsersService { Task<List<User>> GetAllUsersIncludeNotesAsync(); Task<User> GetUserIncludeNotesAsync(int id); }
namespace MaterialAspNetCoreBackend.WebApp.Controllers { [Route("api/[controller]")] public class UsersController : Controller { private readonly IUsersService _usersService; public UsersController(IUsersService usersService) { _usersService = usersService ?? throw new ArgumentNullException(nameof(usersService)); } [HttpGet] public async Task<IActionResult> Get() { return Ok(await _usersService.GetAllUsersIncludeNotesAsync()); } [HttpGet("{id:int}")] public async Task<IActionResult> Get(int id) { return Ok(await _usersService.GetUserIncludeNotesAsync(id)); } } }
در این حالت اگر برنامه را اجرا کنیم، در مسیر زیر
https://localhost:5001/api/users
و آدرس https://localhost:5001/api/users/1 صرفا مشخصات اولین کاربر را بازگشت میدهد.
تنظیم محل تولید خروجی Angular CLI
ساختار پوشه بندی پروژهی جاری به صورت زیر است:
همانطور که ملاحظه میکنید، کلاینت Angular در یک پوشهاست و برنامهی سمت سرور ASP.NET Core در پوشهای دیگر. برای اینکه خروجی نهایی Angular CLI را به پوشهی wwwroot پروژهی وب کپی کنیم، فایل angular.json کلاینت Angular را به صورت زیر ویرایش میکنیم:
"build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "../MaterialAspNetCoreBackend/MaterialAspNetCoreBackend.WebApp/wwwroot",
ng build --no-delete-output-path --watch
dotnet watch run
بنابراین دو صفحهی کنسول مجزا را باز کنید. در اولی ng build (را با پارامترهای یاد شده در پوشهی MaterialAngularClient) و در دومی dotnet watch run را در پوشهی MaterialAspNetCoreBackend.WebApp اجرا نمائید.
هر دو دستور در حالت watch اجرا میشوند. مزیت مهم آن این است که اگر تغییر کوچکی را در هر کدام از پروژهها ایجاد کردید، صرفا همان قسمت کامپایل میشود و در نهایت سرعت کامپایل نهایی برنامه به شدت افزایش خواهد یافت.
تعریف معادلهای کلاسهای موجودیتهای سمت سرور، در برنامهی Angular
در ادامه پیش از تکمیل سرویس دریافت اطلاعات از سرور، نیاز است معادلهای کلاسهای موجودیتهای سمت سرور خود را به صورت اینترفیسهایی تایپاسکریپتی تعریف کنیم:
ng g i contact-manager/models/user ng g i contact-manager/models/user-note
محتویات فایل contact-manager\models\user-note.ts :
export interface UserNote { id: number; title: string; date: Date; userId: number; }
import { UserNote } from "./user-note"; export interface User { id: number; birthDate: Date; name: string; avatar: string; bio: string; userNotes: UserNote[]; }
ایجاد سرویس Angular دریافت اطلاعات از سرور
ساختار ابتدایی سرویس دریافت اطلاعات از سرور را توسط دستور زیر ایجاد میکنیم:
ng g s contact-manager/services/user --no-spec
import { Injectable } from "@angular/core"; @Injectable({ providedIn: "root" }) export class UserService { constructor() { } }
کدهای تکمیل شدهی UserService را در ذیل مشاهده میکنید:
import { HttpClient, HttpErrorResponse } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { Observable, throwError } from "rxjs"; import { catchError, map } from "rxjs/operators"; import { User } from "../models/user"; @Injectable({ providedIn: "root" }) export class UserService { constructor(private http: HttpClient) { } getAllUsersIncludeNotes(): Observable<User[]> { return this.http .get<User[]>("/api/users").pipe( map(response => response || []), catchError((error: HttpErrorResponse) => throwError(error)) ); } getUserIncludeNotes(id: number): Observable<User> { return this.http .get<User>(`/api/users/${id}`).pipe( map(response => response || {} as User), catchError((error: HttpErrorResponse) => throwError(error)) ); } }
- متد getAllUsersIncludeNotes، لیست تمام کاربران را به همراه یادداشتهای آنها از سرور واکشی میکند.
- متد getUserIncludeNotes صرفا اطلاعات یک کاربر را به همراه یادداشتهای او از سرور دریافت میکند.
بارگذاری و معرفی فایل svg نمایش avatars کاربران به Angular Material
بستهی Angular Material و کامپوننت mat-icon آن به همراه یک MatIconRegistry نیز هست که قصد داریم از آن برای نمایش avatars کاربران استفاده کنیم.
در قسمت اول، نحوهی «افزودن آیکنهای متریال به برنامه» را بررسی کردیم که در آنجا آیکنهای مرتبط، از فایلهای قلم، دریافت و نمایش داده میشوند. این کامپوننت، علاوه بر قلم آیکنها، از فایلهای svg حاوی آیکنها نیز پشتیبانی میکند که یک نمونه از این فایلها در مسیر wwwroot\assets\avatars.svg فایل پیوستی انتهای مطلب کپی شدهاست (چون برنامهی وب ASP.NET Core، هاست برنامه است، این فایل را در آنجا کپی کردیم).
ساختار این فایل svg نیز به صورت زیر است:
<?xml version="1.0" encoding="utf-8"?> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <defs> <svg viewBox="0 0 128 128" height="100%" width="100%" pointer-events="none" display="block" id="user1" >
ابتدا به فایل contact-manager-app.component.ts مراجعه و سپس این کامپوننت آغازین ماژول مدیریت تماسها را با صورت زیر تکمیل میکنیم:
import { Component } from "@angular/core"; import { MatIconRegistry } from "@angular/material"; import { DomSanitizer } from "@angular/platform-browser"; @Component() export class ContactManagerAppComponent { constructor(iconRegistry: MatIconRegistry, sanitizer: DomSanitizer) { iconRegistry.addSvgIconSet(sanitizer.bypassSecurityTrustResourceUrl("assets/avatars.svg")); } }
در اینجا در صورتیکه فایل svg شما دارای یک تک آیکن است، روش ثبت آن به صورت زیر است:
iconRegistry.addSvgIcon( "unicorn", this.domSanitizer.bypassSecurityTrustResourceUrl("assets/unicorn_icon.svg") );
<mat-icon svgIcon="unicorn"></mat-icon>
یک نکته: پوشهی node_modules\material-design-icons به همراه تعداد قابل ملاحظهای فایل svg نیز هست.
نمایش لیست کاربران در sidenav
در ادامه به فایل sidenav\sidenav.component.ts مراجعه کرده و سرویس فوق را به آن جهت دریافت لیست کاربران، تزریق میکنیم:
import { User } from "../../models/user"; import { UserService } from "../../services/user.service"; @Component() export class SidenavComponent implements OnInit { users: User[] = []; constructor(private userService: UserService) { } ngOnInit() { this.userService.getAllUsersIncludeNotes() .subscribe(data => this.users = data); } }
اکنون میخواهیم از این اطلاعات جهت نمایش پویای آنها در sidenav استفاده کنیم. در قسمت قبل، جای آنها را در منوی سمت چپ صفحه به صورت زیر با اطلاعات ایستا مشخص کردیم:
<mat-list> <mat-list-item>Item 1</mat-list-item> <mat-list-item>Item 2</mat-list-item> <mat-list-item>Item 3</mat-list-item> </mat-list>
<mat-nav-list> <mat-list-item *ngFor="let user of users"> <a matLine href="#"> <mat-icon svgIcon="{{user.avatar}}"></mat-icon> {{ user.name }} </a> </mat-list-item> </mat-nav-list>
که در اینجا علاوه بر لیست کاربران که از سرویس Users دریافت شده، آیکن avatar آنها که از فایل assets/avatars.svg بارگذاری شده نیز قابل مشاهده است.
اتصال کاربران به صفحهی نمایش جزئیات آنها
در mat-nav-list فوق، فعلا هر کاربر به آدرس # لینک شدهاست. در ادامه میخواهیم با کمک سیستم مسیریابی، با کلیک بر روی نام هر کاربر، در سمت راست صفحه جزئیات او نیز نمایش داده شود:
<mat-nav-list> <mat-list-item *ngFor="let user of users"> <a matLine [routerLink]="['/contactmanager', user.id]"> <mat-icon svgIcon="{{user.avatar}}"></mat-icon> {{ user.name }} </a> </mat-list-item> </mat-nav-list>
const routes: Routes = [ { path: "", component: ContactManagerAppComponent, children: [ { path: ":id", component: MainContentComponent }, { path: "", component: MainContentComponent } ] }, { path: "**", redirectTo: "" } ];
این مشکل دو علت دارد:
الف) چون ContactManagerModule را به صورت lazy load تعریف کردهایم، دیگر نباید در لیست imports فایل AppModule ظاهر شود. بنابراین فایل app.module.ts را گشوده و سپس تعریف ContactManagerModule را هم از قسمت imports بالای صفحه و هم از قسمت imports ماژول حذف کنید؛ چون نیازی به آن نیست.
ب) برای مدیریت خواندن id کاربر، فایل main-content\main-content.component.ts را گشوده و به صورت زیر تکمیل میکنیم:
import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { User } from "../../models/user"; import { UserService } from "../../services/user.service"; @Component({ selector: "app-main-content", templateUrl: "./main-content.component.html", styleUrls: ["./main-content.component.css"] }) export class MainContentComponent implements OnInit { user: User; constructor(private route: ActivatedRoute, private userService: UserService) { } ngOnInit() { this.route.params.subscribe(params => { this.user = null; const id = params["id"]; if (!id) { return; } this.userService.getUserIncludeNotes(id) .subscribe(data => this.user = data); }); } }
اکنون میتوان از اطلاعات این user دریافتی، در قالب این کامپوننت و یا همان فایل main-content.component.html استفاده کرد:
<div *ngIf="!user"> <mat-spinner></mat-spinner> </div> <div *ngIf="user"> <mat-card> <mat-card-header> <mat-icon mat-card-avatar svgIcon="{{user.avatar}}"></mat-icon> <mat-card-title> <h2>{{ user.name }}</h2> </mat-card-title> <mat-card-subtitle> Birthday {{ user.birthDate | date:'d LLLL' }} </mat-card-subtitle> </mat-card-header> <mat-card-content> <mat-tab-group> <mat-tab label="Bio"> <p> {{user.bio}} </p> </mat-tab> <!-- <mat-tab label="Notes"></mat-tab> --> </mat-tab-group> </mat-card-content> </mat-card> </div>
همچنین mat-card را هم بر اساس مثال مستندات آن، ابتدا کپی و سپس سفارشی سازی کردهایم (اگر دقت کنید، هر کامپوننت آن سه برگهی overview، سپس API و در آخر Example را به همراه دارد). این روشی است که همواره میتوان با کامپوننتهای این مجموعه انجام داد. ابتدا مثالی را در مستندات آن پیدا میکنیم که مناسب کار ما باشد. سپس سورس آنرا از همانجا کپی و در برنامه قرار میدهیم و در آخر آنرا بر اساس اطلاعات خود سفارشی سازی میکنیم.
نمایش جزئیات اولین کاربر در حین بارگذاری اولیهی برنامه
تا اینجای کار اگر برنامه را از ابتدا بارگذاری کنیم، mat-spinner قسمت نمایش جزئیات تماسها ظاهر میشود و همانطور باقی میماند، با اینکه هنوز موردی انتخاب نشدهاست. برای رفع آن به کامپوننت sidnav مراجعه کرده و در لحظهی بارگذاری اطلاعات، اولین مورد را به صورت دستی نمایش میدهیم:
import { Router } from "@angular/router"; @Component() export class SidenavComponent implements OnInit, OnDestroy { users: User[] = []; constructor(private userService: UserService, private router: Router) { } ngOnInit() { this.userService.getAllUsersIncludeNotes() .subscribe(data => { this.users = data; if (data && data.length > 0 && !this.router.navigated) { this.router.navigate(["/contactmanager", data[0].id]); } }); } }
البته روش دیگر مدیریت این حالت، حذف کدهای فوق و تبدیل کدهای کامپوننت main-content به صورت زیر است:
let id = params['id']; if (!id) id = 1;
بستن خودکار sidenav در حالت نمایش موبایل
اگر اندازهی صفحهی نمایشی را کوچکتر کنیم، قسمت sidenav در حالت over نمایان خواهد شد. در این حالت اگر آیتمهای آنرا انتخاب کنیم، هرچند آنها نمایش داده میشوند، اما زیر این sidenav مخفی باقی خواهند ماند:
بنابراین در جهت بهبود کاربری این قسمت بهتر است با کلیک کاربر بر روی sidenav و گزینههای آن، این قسمت بسته شده و ناحیهی زیر آن نمایش داده شود.
در کدهای قالب sidenav، یک template reference variable برای آن به نام sidenav درنظر گرفته شدهاست:
<mat-sidenav #sidenav
import { MatSidenav } from "@angular/material"; @Component() export class SidenavComponent implements OnInit, OnDestroy { @ViewChild(MatSidenav) sidenav: MatSidenav;
ngOnInit() { this.router.events.subscribe(() => { if (this.isScreenSmall) { this.sidenav.close(); } }); }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MaterialAngularClient-03.zip
برای اجرای آن:
الف) ابتدا به پوشهی src\MaterialAngularClient وارد شده و فایلهای restore.bat و ng-build-dev.bat را اجرا کنید.
ب) سپس به پوشهی src\MaterialAspNetCoreBackend\MaterialAspNetCoreBackend.WebApp وارد شده و فایلهای restore.bat و dotnet_run.bat را اجرا کنید.
اکنون برنامه در آدرس https://localhost:5001 قابل دسترسی است.