- این خطا عموما زمانی حاصل میشود که محل تعریف connectionStrings در فایل کانفیگ قبل از configSections باشد. ترتیب اینها مهم است.
- به علاوه صرفا تعریف یک کلاس لایه داده و رشته اتصالی کافی نیست . نیاز است مباحث migration را هم اضافه کنید. مراجعه کنید به سری EF Code first سایت و 5 قسمت اول آنرا مطالعه کنید.
این تصویر را پیشتر در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 6 - سرویسها و تزریق وابستگیها» مشاهده کردهاید. در اینجا لیست سرویسهایی را مشاهده میکنید که به صورت پیش فرض، ثبت شدهاند و فعال هستند و ILogger و ILoggerFactory نیز جزئی از آنها هستند. بنابراین نیازی به فعال سازی آنها نیست؛ اما برای استفادهی از آنها نیاز به انجام یک سری تنظیمات است.
پیاده سازی ثبت وقایع در ASP.NET Core
اولین قدم کار با فریم ورک ثبت وقایع ASP.NET Core، معرفی ILoggerFactory به متد Configure کلاس آغازین برنامه است:
public void Configure(ILoggerFactory loggerFactory, IApplicationBuilder app, IHostingEnvironment env) { loggerFactory.AddConsole(Configuration.GetSection("Logging")); loggerFactory.AddDebug();
سطر اول متد، تنظیمات ثبت وقایع را از خاصیت Logging فایل appsettings.json برنامه میخواند (در مورد خاصیت Configuration، در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایلهای config» بیشتر بحث شد) و لاگ کردن ویژهی در کنسول NET Core. را فعال میکند:
{ "Logging": { "IncludeScopes": false, "LogLevel": { "Default": "Debug", "System": "Information", "Microsoft": "Information" } } }
و سطر دوم سبب نمایش اطلاعات لاگ شده در کنسول دیباگ ویژوال استودیو میشود.
متد AddDebug برای شناسایی، نیاز به افزودن وابستگیهای ذیل در فایل project.json برنامه را دارد:
{ "dependencies": { //same as before "Microsoft.Extensions.Logging": "1.0.0", "Microsoft.Extensions.Logging.Console": "1.0.0", "Microsoft.Extensions.Logging.Debug": "1.0.0" } }
در اینجا میتوانید ریز وقایعی را که توسط ASP.NET Core لاگ شدهاست، مشاهده کنید. برای مثال چه درخواستی صورت گرفتهاست و چقدر اجرای آن زمانبردهاست.
این فعال سازی مرتبط است به متد AddDebug که اضافه شد. اگر میخواهید خروجی AddConsole را هم مشاهده کنید، از طریق خط فرمان، به پوشهی اصلی پروژه وارد شده و سپس دستور dotnet run را اجرا کنید:
دستور dotnet run سبب راه اندازی وب سرور برنامه بر روی پورت 5000 شدهاست که در تصویر نیز مشخص است.
بنابراین اینبار برای دسترسی به برنامه باید مسیر http://localhost:5000 را در مرورگر خود طی کنید. در اینجا نیز میتوان حالتهای مختلف اطلاعات لاگ شده را مشاهده کرد و تمام اینها مرتبط هستند به ذکر متد AddConsole .
کار با سرویس ثبت وقایع ASP.NET Core از طریق تزریق وابستگیها
برای کار با سرویس ثبت وقایع توکار ASP.NET Core در قسمتهای مختلف برنامه، میتوان از ترزیق وابستگی ILogger آن استفاده کرد:
[Route("[controller]")] public class AboutController : Controller { private readonly ILogger<AboutController> _logger; public AboutController(ILogger<AboutController> logger) { _logger = logger; } [Route("")] public ActionResult Hello() { _logger.LogInformation("Running Hello"); return Content("Hello from DNT!"); }
سپس با توجه به اینکه این سرویس جزو سرویسهای از پیش ثبت شدهی ASP.NET Core است، امکانات آن بدون نیاز به تنظیمات بیشتری در دسترس است. برای مثال از متد LogInformation آن در اکشن متد Hello استفاده شدهاست و خروجی عبارت لاگ شدهی آنرا در اینجا میتوانید مشاهده کنید:
سطوح مختلف ثبت وقایع
اینترفیس ILogger به همراه متدهای مختلفی است؛ مانند LogError، LogDebug و غیره. معانی آنها به شرح زیر هستند:
Debug (1): ثبت واقعهای است با بیشترین حد جزئیات ممکن که عموما شامل اطلاعات حساسی نیز میباشد. بنابراین نباید در حالت ارائهی نهایی برنامه فعال شود.
(2) Verbose: ثبت وقایعی مفصل، جهت بررسی مشکلات در حین توسعهی برنامه. تنها باید حاوی اطلاعاتی برای دیباگ برنامه باشند.
(3) Information: عموما برای ردیابی قسمتهای مختلف برنامه مورد استفاده قرار میگیرند.
(4) Warning: جهت ثبت واقعهای نامطلوب در سیستم بکار میرود و سبب قطع اجرای برنامه نمیشود.
(5) Errors: مشکلات برنامه را که سبب قطع سرویس دهی آن شدهاند را ثبت میکند. هدف آن ثبت مشکلات واحد جاری است و نه کل برنامه.
Critical (6): هدف آن ثبت مشکلات بحرانی کل سیستم است که سبب از کار افتادن آن شدهاند.
برای مثال در حین تنظیم متد AddDebug که سبب نمایش اطلاعات لاگ شده در کنسول دیباگ ویژوال استودیو میشود، میتوان حداقل سطح ثبت وقایع را نیز ذکر کرد:
loggerFactory.AddDebug(minLevel: LogLevel.Information);
البته ترتیب واقعی این سطوح را در enum مرتبط با آنها بهتر میتوان مشاهده کرد:
public enum LogLevel { Trace, Debug, Information, Warning, Error, Critical, None, }
یک نکته: زمانیکه متد AddDebug را بدون پارامتر فراخوانی میکنید، حداقل سطح ثبت وقایع آن به Information تنظیم شدهاست. یعنی در این لاگ، خبری از اطلاعات Debug نخواهد بود (چون سطح دیباگ پایینتر است از Information). بنابراین اگر میخواهید این اطلاعات را هم مشاهده کنید باید پارامتر minLevel آنرا به LogLevel.Debug تنظیم نمائید.
امکان استفادهی از پروایدرهای ثبت وقایع ثالث
تا اینجا، دو نمونه از پروایدرهای توکار ثبت وقایع ASP.NET Core را بررسی کردیم. اگر نیاز به ثبت این اطلاعات با فرمتهای مختلف و یا در بانک اطلاعاتی وجود دارد، میتوان به تامین کنندههای ثالثی که قابلیت کار با ILoggerFactory را دارند نیز مراجعه کرد. برای مثال:
- elmah.io - provider for the elmah.io service
- Loggr - provider for the Loggr service
- NLog - provider for the NLog library
- Serilog - provider for the Serilog library
من بلدم با set identity_insert table_name on/off کاری کنم که خودم دستی مقداری را برای خصیصه identity لحاظ کنم. ولی متاسفانه نتونستم مقدار یک ستون با خصیصه Identity رو بروز رسانی (یا همون update) کنم. لطفا بهم بگید که اصلا این کار ممکنه یا من بلد نیستم. البته براساس query زیر بمن SQL Server گفته که نمیشه این ستون را update کرد که ظاهرا هم همین طور(ستون id همانطور که در پیام آمده از نوع identity هست)
update t set id = new_id from (select id, row_number() over(order by id) new_id from #temp)t --Cannot update identity column 'id'.
اصلا اجازه بدین یه جور دیگه سوال رو مطرح کنم من نیاز دارم تمام مقادیر identity رو بروز رسانی کنم تا کاملا پشت سر هم و متوالی بشن این کار را میتونم با یک تابع row_number و یک derived table انجام بدم (اگر بذارن!) همانطور که قبلا نشان دادم، یا با روش زیر این کار را بکنم که البته اجرا نمیشه به این دلیل که در یک جدول نمیشه دو identity property داشت. با فرض اجرا شدن دستور select into باز هم در دستور update با مشکل بر میخوردیم (چون نمیشه ستون id را بروز رسانی کرد)
select id, identity(int, 1,1) new_id into #temptable from #temp order by id asc /* cannot add identity column, using the SELECT INTO statement, to table '#temptable', which already has column 'id' that inherits the identity property. */ update t set id = new_id from #temp t join #temptable d on t.id = d.id;
declare @t table(id int) insert into @t select id from #temp delete from #temp set identity_insert #temp on insert #temp (id) select row_number() over(order by id) from @t set identity_insert #temp off
من قصد ندارم صورت مساله نقد و بررسی بشه و اصولی بودن یا صحیح بودنش مورد ارزیابی قرار بگیره فقط برام این یک سوال شده.
مساله عمومی که راجب این ستون وجود داره استفاده کردن از Gapهای حاصل شده در این ستون برای درجهای بعدی است. که query آن نیز بسیار ساده و در دسترس است.
آیا شما میدانید که چگونه این مشکل با sequence ای که در نسخه 2012 معرفی شده است حل میشود؟
مدلها و تنظیمات برنامه
مدلها و تنظیمات مورد استفادهی در مثال جاری، با مدلهای مطلب «لغو Lazy Loading در حین کار با AutoMapper و Entity Framework» یکی است. فقط ViewModel مورد استفاده اینبار یکچنین ساختاری را دارد:
public class UserViewModel { public int Id { set; get; } public string CustomName { set; get; } public int PostsCount { set; get; } }
- خاصیت CustomName از جمع نام و سن شخص تشکیل شود.
- خاصیت PostsCount بیانگر جمع مطالب ارسالی آن شخص باشد.
نگاشتهای AutoMapper میتوانند حاوی توابع تجمعی نیز باشند
برای حل مسالهی فوق تنها کافی است نگاشت ذیل را تهیه کنیم:
public class TestProfile : Profile { protected override void Configure() { this.CreateMap<User, UserViewModel>() .ForMember(dest => dest.CustomName, opt => opt.MapFrom(src => src.Name + "[" + src.Age + "]")) .ForMember(dest => dest.PostsCount, opt => opt.MapFrom(src => src.BlogPosts.Count())); } public override string ProfileName { get { return this.GetType().Name; } } }
کوئری نهایی استفاده کننده از تنظیمات نگاشت تهیه شده
در ادامه متدهای Project To را جهت استفادهی از تنظیمات نگاشت فوق بکار میگیریم:
using (var context = new MyContext()) { var user1 = context.Users .Project() .To<UserViewModel>() .FirstOrDefault(); if (user1 != null) { Console.Write(user1.CustomName); Console.Write(user1.PostsCount); } }
SELECT [Limit1].[Id] AS [Id], [Limit1].[C1] AS [C1], [Limit1].[C2] AS [C2] FROM ( SELECT TOP (1) [Project1].[Id] AS [Id], CASE WHEN ([Project1].[Name] IS NULL) THEN N'' ELSE [Project1].[Name] END + N'[' + CAST( [Project1].[Age] AS nvarchar(max)) + N']' AS [C1], [Project1].[C1] AS [C2] FROM ( SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], [Extent1].[Age] AS [Age], (SELECT COUNT(1) AS [A1] FROM [dbo].[BlogPosts] AS [Extent2] WHERE [Extent1].[Id] = [Extent2].[UserId]) AS [C1] FROM [dbo].[Users] AS [Extent1] ) AS [Project1] ) AS [Limit1]
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید.
تفاوت بین متدهای app.Use و app.Run در چیست؟
Middlewareها به همان ترتیبی که در متد Configure کلاس آغازین برنامه معرفی میشوند، اجرا خواهند شد؛ اما نکتهی مهم اینجا است که middleware ایی که توسط متد app.Use تعریف میشود، میتواند middleware بعدی ثبت شدهرا، فراخوانی کند؛ اما app.Run خیر. برای درک بهتر این مفهوم، به مثال زیر دقت کنید:
using Microsoft.AspNetCore.Http; public class Startup { public void Configure(IApplicationBuilder app) { app.Use(async (context, next) => { await context.Response.WriteAsync("<div>from middleware-1, inside app.Use, before next()</div>"); await next(); await context.Response.WriteAsync("<div>from middleware-1, inside app.Use, after next()</div>"); }); app.Run(async context => { await context.Response.WriteAsync("<div>Inside middleware-2 defined using app.Run</div>"); }); app.Use(async (context, next) => { await context.Response.WriteAsync("<div>from middleware-3, inside app.Use, before next()</div>"); await next(); await context.Response.WriteAsync("<div>from middleware-3, inside app.Use, after next()</div>"); });
همانطور که در تصویر نیز مشخص است، ابتدا کدهای پیش از فراخوانی دلیگیت next میانافزار اول اجرا شدهاست. سپس باتوجه به فراخوانی دلیگیت next، کدهای دومین میانافزار ثبت شده، فراخوانی گردیدهاست و سپس کدهای پس از فراخوانی دلیگیت next میانافزار اول، اجرا شدهاند.
این دلیگیت در اصل یک چنین امضایی را دارد:
public delegate Task RequestDelegate(HttpContext context);
باید دقت داشت که فراخوانی دلیگیت next در میانافزارهای از نوع app.Use الزامی نبوده و اگر اینکار انجام نشود، بین app.Run و app.Use تفاوتی نخواهد بود و هر دو terminal به حساب میآیند.
تفاوت بین متدهایapp.Map و app.MapWhen در چیست؟
متد app.Map در صورت برآورده شدن شرطی، سبب اجرای میانافزاری مشخص میشود (امکان اجرای غیر خطی میانافزارها).
فرض کنید قطعه کد زیر را پس از اولین app.Use مثال فوق قرار دادهایم:
app.Map("/dnt", appBuilder => { appBuilder.Run(async context => { await context.Response.WriteAsync(@"<div>Inside Map(/dnt) --> Run</div>"); }); });
در اینجا چون app.Run داخلی فراخوانی شده، از نوع terminal است، دیگر میان افزارهای پس از آن اجرا نشدهاند. بدیهی است در اینجا نیز میتوان به هر تعدادی که نیاز است میان افزارهای جدیدی را به appBuilder متد app.Map اضافه کرد.
پارامتر اول متد Map برای تطابق با الگوهایی خاص و مشخص، مناسب است. اما در اگر در اینجا نیاز به اطلاعات بیشتری از HttpContext جاری داشته باشیم، میتوانیم از متد app.MapWhen استفاده کنیم که اولین پارامتر آن یک دلیگیت است که HttpContext را در اختیار استفاده کننده قرار میدهد و اگر در نهایت true را دریافت کند، سبب اجرای میان افزارهای قسمت appBuilder آن خواهد شد:
app.MapWhen(context => { return context.Request.Query.ContainsKey("dnt"); }, appBuilder => { appBuilder.Run(async context => { await context.Response.WriteAsync(@"<div>Inside MapWhen(?dnt) --> Run</div>"); }); });
http://localhost:7742/?dnt=true
نظم بخشیدن به تعاریف میانافزارها
متدهای app.Run و app.Use و امثال آنها برای تعریف سریع میان افزارها مناسب هستند. اما اگر بخواهیم کدهای کلاس آغازین برنامه را اندکی خلوت کرده و به تعاریف میانافزارها نظم ببخشیم، میتوان کدهای آنها را به کلاسهایی با امضایی خاص منتقل کرد:
using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace Core1RtmEmptyTest.StartupCustomizations { public class MyMiddleware1 { private readonly RequestDelegate _next; public MyMiddleware1(RequestDelegate next) { _next = next; } public async Task Invoke(HttpContext context) { context.Response.ContentType = "text/html"; context.Response.StatusCode = 200; await context.Response.WriteAsync("<div>Hello from MyMiddleware1.</div>"); await _next.Invoke(context); await context.Response.WriteAsync("<div>End of action.</div>"); } } }
در اینجا نیز اگر دلیگیت next_ فراخوانی نشود، این میانافزار سبب خاتمهی اجرای پردازش درخواست جاری میگردد.
مرحلهی بعد، روش معرفی این میانافزار تعریف شده، به لیست میانافزارهای موجود است. برای این منظور میتوان متد app.UseMiddleware را به صورت مستقیم در کلاس آغازین برنامه فراخوانی کرد و یا مرسوم است اگر کتابخانهای را طراحی کردهاید، به نحو ذیل متد الحاقی خاصی را برای آن تدارک دید:
using Microsoft.AspNetCore.Builder; public static class MyMiddlewareExtensions { public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder app) { app.UseMiddleware<MyMiddleware1>(); return app; } }
public void Configure(IApplicationBuilder app) { app.UseMyMiddleware();
- توسعه پایگاه داده سیستم دسترسی مبتنی بر نقش
- توسعه یک Customized Filter Attribute بر پایه Authorize Attribute
- توسعه سرویسهای مورد استفاده در Authorize Attribute
- توسعه کنترلر Permissions: تمامی APIهایی که در جهت همگام سازی دسترسیها بین کلاینت و سرور را بر عهده دارند در این کنترلر توسعه داده میشود.
- توسعه سرویس مدیریت دسترسی در کلاینت توسط AngularJS
توسعه پایگاه داده
public class Permission { [Key] public string Id { get; set; } public string Title { get; set; } public string Description { get; set; } public string Area { get; set; } public string Control { get; set; } public virtual ICollection<Role> Roles { get; set; } }
Control | Area |
view | products |
add | products |
edit | products |
delete | products |
با توجه به جدول فوق همانطور که مشاهده میکنید تمامی آنچه که برای دسترسی Products مورد نیاز است در یک حوزه و 4 کنترل گنجانده میشود. البته توجه داشته باشید سناریویی که مطرح کردیم برای روشن سازی مفهوم ناحیه یا حوزه و کنترل بود. همانطور که میدانیم در AngularJS تمامی اطلاعات توسط APIها فراخوانی میگردند. از این رو یک موهبت دیگر این روش، خوانایی مفهوم حوزه و کنترل نسبت به نام کنترلر و متد است.
مدل Roles را ما به صورت زیر توسعه دادهایم:
public class Role { [Key] public string Id { get; set; } public string Title { get; set; } public string Description { get; set; } public virtual ICollection<Permission> Permissions { get; set; } public virtual ICollection<User> Users { get; set; } }
در مدل فوق میبینید که دو رابطه چند به چند وجود دارد. رابطه اول که همان Permissions است و در مدل پیشین تشریح شد. رابطهی دوم رابطه چند به چند بین کاربر و نقش است. چند کاربر قادرند یک نقش در سیستم داشته باشند و همینطور چندین نقش میتواند به یک کاربر انتساب داده شود.
ما در این سیستم از ASP.NET Identity 2.1 استفاده و کلاس IdentityUser را override کردهایم. در مدل override شده، برخی اطلاعات جدید کاربر، به جدول کاربر اضافه شدهاند. این اطلاعات شامل نام، نام خانوادگی، شماره تماس و ... میباشد.
public class ApplicationUser : IdentityUser { [MaxLength(100)] public string FirstName { get; set; } [MaxLength(100)] public string LastName { get; set; } public bool IsSysAdmin { get; set; } public DateTime JoinDate { get; set; } public virtual ICollection<Role> Roles { get; set; } }
در نهایت تمامی این مدلها به وسیله EF Code First پایگاه داده سیستم ما را تشکیل خواهند داد.
توسعه یک Customized Filter Attribute بر پایه Authorize Attribute
public class RBACAttribute : AuthorizeAttribute { public string Area { get; set; } public string Control { get; set; } AccessControlService _AccessControl = new AccessControlService(); public override void OnAuthorization(HttpActionContext actionContext) { var userId = HttpContext.Current.User.Identity.GetCurrentUserId(); // If User Ticket is Not Expired if (userId == null || !_AccessControl.HasPermission(userId, this.Area, this.Control)) { actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized); } } }
[HttpPost] [Route("ChangeProductStatus")] [RBAC(Area = "products", Control = "edit")] public async Task<HttpResponseMessage> ChangeProductStatus(StatusCodeBindingModel model) { // Method Body }
MongoDB #7
- String: این نوع پرکاربردترین نوع داده برای ذخیره اطلاعات است. رشته در MongoDB باید بصورت یونیکد (utf-8) معتبر باشد.
- Integer: این نوع برای ذخیره کردن یک مقدار عددی استفاده میشود. Integer بسته به نوع سرور میتواند 32 یا 64 بیت باشد.
- Boolean: این نوع برای ذخیره کردن یک مقدار بولی (true/false) استفاده میشود.
- Double: این نوع برای مقادیر با ممیز شناور استفاده میشود.
- کلیدهای Min/Max: این نوع برای مقایسه یک مقدار با کمترین یا بیشترین عناصر BSON استفاده میشود.
- Array: این نوع برای ذخیره آرایهها یا لیست یا چندین مقدار در یک کلید استفاده میشود.
- Timestamp: این نوع میتواند برای ضبط زمان تغییرات (مثلا وقتی یک سند درج میشود یا تغییر میکند) مفید باشد.
- Object: این نوع برای سندهای توکار استفاده میشود.
- Null: این نوع برای ذخیره مقدار تهی (Null) استفاده میشود.
- Symbol: این نوع بطور یکسان برای ذخیره رشته استفاده میشود، اما عموما برای زبانهایی که از یک نوع نماد (Symbol) مشخص استفاده میکنند تعبیه شده است.
- Date: این نوع برای ذخیره تاریخ یا زمان جاری به فرمت زمان در یونیکس (UNIX) استفاده میشود. با ساخت یک شی از نوع Date و ارسال روز، ماه و سال به آن میتوانید تاریخ مشخص خود را داشته باشید.
- Object ID: ای نوع برای ذخیره سازی شناسه سند استفاه میشود.
- Binary Data: این نوع برای ذخیره سازی داده باینری استفاده میشود.
- Code: این نوع برای ذخیره سازی کد جاوا اسکریپت داخل سند استفاده میشود.
- Regular Expression: این نوع برای ذخیره سازی عبارت باقاعده استفاده میشود.
درج سند در MongoDB
>db.COLLECTION_NAME.insert(document)
>db.mycol.insert({ _id: ObjectId(7df78ad8902c), title: 'MongoDB Overview', description: 'MongoDB is no sql database', by: 'tutorials point', url: 'http://www.tutorialspoint.com', tags: ['mongodb', 'database', 'NoSQL'], likes: 100 })
>db.post.insert([ { title: 'MongoDB Overview', description: 'MongoDB is no sql database', by: 'tutorials point', url: 'http://www.tutorialspoint.com', tags: ['mongodb', 'database', 'NoSQL'], likes: 100 }, { title: 'NoSQL Database', description: 'NoSQL database doesn't have tables', by: 'tutorials point', url: 'http://www.tutorialspoint.com', tags: ['mongodb', 'database', 'NoSQL'], likes: 20, comments: [ { user:'user1', message: 'My first comment', dateCreated: new Date(2013,11,10,2,35), like: 0 } ] } ])
Install-Package Microsoft.Extensions.Caching.StackExchangeRedis
public interface IResponseCacheService { Task CacheResponseAsync(string cacheKey, object response, TimeSpan timeToLive); Task<string> GetCachedResponseAsync(string cacheKey); }
public class ResponseCacheService : IResponseCacheService, ISingletonDependency { private readonly IDistributedCache _distributedCache; public ResponseCacheService(IDistributedCache distributedCache) { _distributedCache = distributedCache; } public async Task CacheResponseAsync(string cacheKey, object response, TimeSpan timeToLive) { if (response == null) return; var serializedResponse = JsonConvert.SerializeObject(response); await _distributedCache.SetStringAsync(cacheKey, serializedResponse, new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = timeToLive }); } public async Task<string> GetCachedResponseAsync(string cacheKey) { var cachedResponse = await _distributedCache.GetStringAsync(cacheKey); return string.IsNullOrWhiteSpace(cachedResponse) ? null : cachedResponse; } }
public class RedisCacheSettings { public bool Enabled { get; set; } public string ConnectionString { get; set; } public int DefaultSecondsToCache { get; set; } }
"RedisCacheSettings": { "Enabled": true, "ConnectionString": "192.168.1.107:6379,ssl=False,allowAdmin=True,abortConnect=False,defaultDatabase=0,connectTimeout=500,connectRetry=3", "DefaultSecondsToCache": 600 },
public class CacheInstaller : IServiceInstaller { public void InstallServices(IServiceCollection services, AppSettings appSettings, Assembly startupProjectAssembly) { var redisCacheService = appSettings.RedisCacheSettings; services.AddSingleton(redisCacheService); if (!appSettings.RedisCacheSettings.Enabled) return; services.AddStackExchangeRedisCache(options => options.Configuration = appSettings.RedisCacheSettings.ConnectionString); // Below code applied with ISingletonDependency Interface // services.AddSingleton<IResponseCacheService, ResponseCacheService>(); } }
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class CachedAttribute : Attribute, IAsyncActionFilter { private readonly int _secondsToCache; private readonly bool _useDefaultCacheSeconds; public CachedAttribute() { _useDefaultCacheSeconds = true; } public CachedAttribute(int secondsToCache) { _secondsToCache = secondsToCache; _useDefaultCacheSeconds = false; } public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { var cacheSettings = context.HttpContext.RequestServices.GetRequiredService<RedisCacheSettings>(); if (!cacheSettings.Enabled) { await next(); return; } var cacheService = context.HttpContext.RequestServices.GetRequiredService<IResponseCacheService>(); // Check if request has Cache var cacheKey = GenerateCacheKeyFromRequest(context.HttpContext.Request); var cachedResponse = await cacheService.GetCachedResponseAsync(cacheKey); // If Yes => return Value if (!string.IsNullOrWhiteSpace(cachedResponse)) { var contentResult = new ContentResult { Content = cachedResponse, ContentType = "application/json", StatusCode = 200 }; context.Result = contentResult; return; } // If No => Go to method => Cache Value var actionExecutedContext = await next(); if (actionExecutedContext.Result is OkObjectResult okObjectResult) { var secondsToCache = _useDefaultCacheSeconds ? cacheSettings.DefaultSecondsToCache : _secondsToCache; await cacheService.CacheResponseAsync(cacheKey, okObjectResult.Value, TimeSpan.FromSeconds(secondsToCache)); } } private static string GenerateCacheKeyFromRequest(HttpRequest httpRequest) { var keyBuilder = new StringBuilder(); keyBuilder.Append($"{httpRequest.Path}"); foreach (var (key, value) in httpRequest.Query.OrderBy(x => x.Key)) { keyBuilder.Append($"|{key}-{value}"); } return keyBuilder.ToString(); } }
[Cached] [HttpGet] public IActionResult Get() { var rng = new Random(); var weatherForecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateTime.Now.AddDays(index), TemperatureC = rng.Next(-20, 55), Summary = Summaries[rng.Next(Summaries.Length)] }) .ToArray(); return Ok(weatherForecasts); }
در اینجا توسط کامپوننت sidenav، کار نمایش لیست تماسها صورت میگیرد و نمایش این کامپوننت واکنشگرا است. به این معنا که در اندازههای صفحات نمایشی بزرگ، نمایان است و در صفحات نمایشی کوچک، مخفی خواهد شد. در بالای صفحه یک Toolbar قرار دارد که همیشه نمایان است و از آن برای نمایش گزینههای منوی برنامه استفاده میکنیم. همچنین ناحیهی main content را هم مشاهده میکنید که با انتخاب هر شخص از لیست تماسها، جزئیات او در این قسمت نمایش داده خواهد شد.
ایجاد ماژول مدیریت تماسها
در قسمت اول، برنامه را به همراه تنظیمات ابتدایی مسیریابی آن ایجاد کردیم که نتیجهی آن تولید فایل src\app\app-routing.module.ts میباشد:
ng new MaterialAngularClient --routing
ng g m ContactManager -m app.module --routing
این دستور ماژول جدید contact-manager را به همراه تنظیمات ابتدایی مسیریابی و همچنین به روز رسانی app.module، برای درج آن، ایجاد میکند. البته در این حالت نیاز است به app.module.ts مراجعه کرد و محل درج آنرا تغییر داد:
import { ContactManagerModule } from "./contact-manager/contact-manager.module"; @NgModule({ imports: [ BrowserModule, BrowserAnimationsModule, CoreModule, SharedModule.forRoot(), ContactManagerModule, AppRoutingModule ], }) export class AppModule { }
سپس دستور زیر را اجرا میکنیم تا کامپوننت contact-manager-app در ماژول contact-manager ایجاد شود:
ng g c contact-manager/ContactManagerApp --no-spec
CREATE src/app/contact-manager/contact-manager-app/contact-manager-app.component.html (38 bytes) CREATE src/app/contact-manager/contact-manager-app/contact-manager-app.component.ts (319 bytes) CREATE src/app/contact-manager/contact-manager-app/contact-manager-app.component.css (0 bytes) UPDATE src/app/contact-manager/contact-manager.module.ts (436 bytes)
این کامپوننت به عنوان میزبان سایر کامپوننتهایی که در مقدمهی بحث عنوان شدند، عمل میکند. این کامپوننتها را به صورت زیر در پوشهی components ایجاد میکنیم:
ng g c contact-manager/components/toolbar --no-spec ng g c contact-manager/components/main-content --no-spec ng g c contact-manager/components/sidenav --no-spec
تنظیمات مسیریابی برنامه
در ادامه به src\app\app-routing.module.ts مراجعه کرده و این ماژول جدید را به صورت lazy load معرفی میکنیم:
import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; const routes: Routes = [ { path: "contactmanager", loadChildren: "./contact-manager/contact-manager.module#ContactManagerModule" }, { path: "", redirectTo: "contactmanager", pathMatch: "full" }, { path: "**", redirectTo: "contactmanager" } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
سپس تنظیمات مسیریابی ماژول مدیریت تماسها را در فایل src\app\contact-manager\contact-manager-routing.module.ts به صورت زیر تغییر میدهیم:
import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; import { MainContentComponent } from "./components/main-content/main-content.component"; import { ContactManagerAppComponent } from "./contact-manager-app/contact-manager-app.component"; const routes: Routes = [ { path: "", component: ContactManagerAppComponent, children: [ { path: "", component: MainContentComponent } ] }, { path: "**", redirectTo: "" } ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule] }) export class ContactManagerRoutingModule { }
کامپوننت ContactManagerApp که کار هاست سایر کامپوننتهای این ماژول را بر عهده دارد، دارای router-outlet خاص خود خواهد بود. به همین جهت برای آن children تعریف شدهاست که مسیر پیشفرض آن، بارگذاری کامپوننت MainContent است.
بنابراین نیاز است به فایل contact-manager-app\contact-manager-app.component.html مراجعه و ابتدا منوی کنار صفحه را به آن افزود:
<app-sidenav></app-sidenav>
سپس در قالب sidenav\sidenav.component.html، کار تعریف toolbar و همچنین router-outlet را انجام میدهیم:
<app-toolbar></app-toolbar> <router-outlet></router-outlet>
معرفی Angular Material به ماژول جدید مدیریت تماسها
در قسمت اول، یک فایل material.module.ts را ایجاد کردیم که به همراه تعریف تمامی کامپوننتهای Angular Material بود. سپس آنرا به shared.module.ts افزودیم که حاوی تعریف ماژول فرمها و همچنین Flex Layout نیز هست. به همین جهت برای معرفی اینها به این ماژول جدید تنها کافی است در فایل src\app\contact-manager\contact-manager.module.ts در قسمت imports، کار معرفی SharedModule صورت گیرد:
import { SharedModule } from "../shared/shared.module"; @NgModule({ imports: [ CommonModule, SharedModule, ContactManagerRoutingModule ] }) export class ContactManagerModule { }
پس از اجرای برنامه مشاهده میکنید که ابتدا ماژول مدیریت تماسها بارگذاری شدهاست و سپس contact-manager-app عمل و sidenav را بارگذاری کرده و آن نیز سبب نمایش کامپوننت toolbar و سپس main-content شدهاست.
تنظیم طرحبندی برنامه توسط کامپوننتهای Angular Material و همچنین Flex Layout
پس از این تنظیمات اکنون نوبت به تنظیم طرحبندی برنامهاست و آنرا با قراردادن کامپوننت Sidenav بستهی Angular Material شروع میکنیم:
<mat-sidenav-container *ngIf="shouldRun"> <mat-sidenav mode="side" opened> Sidenav content </mat-sidenav> Primary content </mat-sidenav-container>
- Over: قسمت Sidenav content بر روی Primary content قرار میگیرد.
- Push: قسمت Sidenav content قسمت Primary content را از سر راه خود بر میدارد.
- Side: قسمت Sidenav content در کنار Primary content قرار میگیرد.
در اینجا از حالت Side، در صفحات نمایشی بزرگ (اولین تصویر این قسمت) و از حالت Over، در صفحات نمایشی موبایل (مانند تصویر زیر) استفاده خواهیم کرد.
در ابتدا کدهای کامل هر سه کامپوننت و سپس توضیحات آنها را مشاهده خواهید کرد:
تنظیم margin در CSS اصلی برنامه
زمانیکه sidenav و toolbar را بر روی صفحه قرار میدهیم، فاصلهای بین آنها و لبههای صفحه مشاهده میشود. برای اینکه این فاصله را به صفر برسانیم، به فایل src\styles.css مراجعه کرده و margin بدنهی صفحه را به صفر تنظیم میکنیم:
@import "~@angular/material/prebuilt-themes/indigo-pink.css"; body { margin: 0; }
طراحی قالب main content
<mat-card> <h1>Main content</h1> </mat-card>
sidenav\sidenav.component.css | sidenav\sidenav.component.html |
.app-sidenav-container { position: fixed; } .app-sidenav { width: 240px; } .wrapper { margin: 50px; } | <mat-sidenav-container fxLayout="row" fxFill> <mat-sidenav #sidenav fxFlex="1 1 100%" [opened]="!isScreenSmall" [mode]="isScreenSmall ? 'over' : 'side'"> <mat-toolbar color="primary"> Contacts </mat-toolbar> <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-sidenav> <mat-sidenav-content fxLayout="column" fxFlex="1 1 100%" fxFill> <app-toolbar (toggleSidenav)="sidenav.toggle()"></app-toolbar> <div> <router-outlet></router-outlet> </div> </mat-sidenav-content> </mat-sidenav-container> |
- نمای کلی صفحه در این قسمت طراحی شدهاست. sidenav-container که در برگیرندهی اصلی است، به fxLayout از نوع row تنظیم شدهاست. یعنی mat-sidenav و mat-sidenav-content دو ستون آنرا از چپ به راست تشکیل میدهند و درون یک ردیف، سیلان خواهند یافت. همچنین میخواهیم این container کل صفحه را پر کند، به همین جهت از fxFill استفاده شدهاست. این fxFill اعمال شدهی به container، زمانی عمل خواهد کرد که position آن در css، به fixed تنظیم شود که اینکار در css این قالب و در کلاس app-sidenav-container آن انجام شدهاست.
- سپس toolbar و همچنین router-outlet که main content را نمایش میدهند، داخل sidenav-content قرار گرفتهاند و هر دو با هم، ستون دوم این طرحبندی را تشکیل میدهند. به همین جهت fxLayout آن به column تنظیم شدهاست (ستون اول آن، لیست تماسها است و ستون دوم آن، از دو ردیف toolbar و main-content تشکیل میشود).
- اگر دقت کنید یک template reference variable به نام sidenav# به container اعمال شدهاست. از آن، جهت باز و بسته کردن sidenav استفاده میشود:
<app-toolbar (toggleSidenav)="sidenav.toggle()"></app-toolbar>
- mat-sidenav از دو قسمت تشکیل شدهاست. بالای آن توسط mat-toolbar صرفا کلمهی Contacts نمایش داده میشود و سپس ذیل آن، لیست فرضی تماسها توسط کامپوننت mat-list قرار گرفتهاند (تا فعلا خالی نباشد. در قسمتهای بعدی آنرا پویا خواهیم کرد). رنگ تولبار آنرا ("color="primary) نیز به primary تنظیم کردهایم تا خاکستری پیشفرض آن نباشد.
- کار کلاس mat-elevation-z10 این است که بین sidenav و main-content یک سایهی سه بعدی را ایجاد کند که آنرا در تصاویر مشاهده میکنید. عددی که پس از z قرار میگیرد، میزان عمق سایه را مشخص میکند.
- این قسمت از sidenav به همراه دو خاصیت opened و همچنین mode است که به مقدار isScreenSmall عکس العمل نشان میدهند:
<mat-sidenav [opened]="!isScreenSmall" [mode]="isScreenSmall ? 'over' : 'side'">
محتویات فایل sidenav\sidenav.component.ts:
import { Component, OnDestroy, OnInit } from "@angular/core"; import { MediaChange, ObservableMedia } from "@angular/flex-layout"; import { Subscription } from "rxjs"; @Component({ selector: "app-sidenav", templateUrl: "./sidenav.component.html", styleUrls: ["./sidenav.component.css"] }) export class SidenavComponent implements OnInit, OnDestroy { isScreenSmall = false; watcher: Subscription; constructor(private media: ObservableMedia) { this.watcher = media.subscribe((change: MediaChange) => { this.isScreenSmall = change.mqAlias === "xs"; }); } ngOnInit() { } ngOnDestroy() { this.watcher.unsubscribe(); } }
تاثیر خاصیت isScreenSmall بر روی دو خاصیت opened و mode کامپوننت sidenav را در دو تصویر زیر مشاهده میکنید. اگر اندازهی صفحه کوچک شود، ابتدا sidenav مخفی میشود. اگر کاربر بر روی دکمهی منوی همبرگری کلیک کند، سبب نمایش مجدد sidenav، اینبار با حالت over و بر روی محتوای زیرین آن خواهد شد:
طراحی نوار ابزار واکنشگرا
کدهای قالب و css تولبار (ستون دوم طرحبندی کلی صفحه) را در ادامه مشاهده میکنید:
toolbar\toolbar.component.css | toolbar\toolbar.component.html |
.sidenav-toggle { padding: 0; margin: 8px; min-width:56px; } | <mat-toolbar color="primary"> <button mat-button fxHide fxHide.xs="false" class="sidenav-toggle" (click)="toggleSidenav.emit()"> <mat-icon>menu</mat-icon> </button> <span>Contact Manager</span> </mat-toolbar> |
با توجه به استفادهی از fxHide، یعنی دکمهی نمایش منوی همبرگری در تمام حالات مخفی خواهد بود. برای لغو آن و نمایش آن در حالت موبایل، از حالت واکنشگرای آن یعنی fxHide.xs استفاده میکنیم (قسمت «کار با API واکنشگرای Angular Flex layout» در مطلب قبلی این سری). به این ترتیب زمانیکه کاربر اندازهی صفحه را کوچک میکند و یا اندازهی واقعی صفحهی نمایش او کوچک است، این دکمه نمایان خواهد شد.
همچنین در sidenav یک چنین تعریفی را داریم:
<app-toolbar (toggleSidenav)="sidenav.toggle()"></app-toolbar>
محتویات فایل toolbar\toolbar.component.ts:
import { Component, EventEmitter, OnInit, Output } from "@angular/core"; @Component({ selector: "app-toolbar", templateUrl: "./toolbar.component.html", styleUrls: ["./toolbar.component.css"] }) export class ToolbarComponent implements OnInit { @Output() toggleSidenav = new EventEmitter<void>(); constructor() { } ngOnInit() { } }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MaterialAngularClient-02.zip
برای اجرای آن نیز ابتدا فایل restore.bat و سپس فایل ng-serve.bat را اجرا کنید. پس از اجرای برنامه، یکبار آنرا در حالت تمام صفحه و بار دیگر با کوچکتر کردن اندازهی مرورگر آزمایش کنید. در حالتیکه به اندازهی موبایل رسیدید، بر روی دکمهی همبرگری نمایان شده کلیک کنید تا عکس العمل آن و نمایش مجدد sidenav را در حالت over، مشاهده نمائید.