public void Add(IWritable htmlElement) { var writableElement = htmlElement as WritableElement; if (writableElement == null) return; foreach (var element in writableElement.Elements()) { if (element is PdfDiv) { var div = element as PdfDiv; foreach (var divChildElement in div.Content) { fixNestedTablesRunDirection(divChildElement); _paragraph.Add(divChildElement); } } else if(element is Paragraph) { var paragraph = element as Paragraph; paragraph.Alignment = Element.ALIGN_LEFT; _paragraph.Add(element); } else { fixNestedTablesRunDirection(element); _paragraph.Add(element); } } }
class HandlerChain
{
setNextObj(nextObjInChain){}
processMultiple(req){
console.log("No multiple for: " + req.getMultiple());
}
}
class Multiple
{
constructor(multiple){
this.multiple = multiple;
}
getMultiple(){
return this.multiple;
}
}
class MultipleofTwoHandler extends HandlerChain
{
constructor(){
super()
this.nextObjInChain = new HandlerChain()
}
setNextObj(nextObj){
this.nextObjInChain = nextObj;
}
processMultiple(req) {
if ((req.getMultiple() % 2) == 0) {
console.log("Multiple of 2: " + req.getMultiple());
}else{
this.nextObjInChain.processMultiple(req);
}
}
}
class MultipleofThreeHandler extends HandlerChain
{
constructor(){
super()
this.nextObjInChain = new HandlerChain()
}
setNextObj(nextObj){
this.nextObjInChain = nextObj;
}
processMultiple(req)
{
if ((req.getMultiple() % 3) == 0) {
console.log("Multiple of 3: " + req.getMultiple());
}else{
this.nextObjInChain.processMultiple(req);
}
}
}
class MultipleofFiveHandler extends HandlerChain
{
constructor(){
super()
this.nextObjInChain = new HandlerChain()
}
setNextObj(nextObj){
this.nextObjInChain = nextObj;
}
processMultiple(req) {
if ((req.getMultiple() % 5) == 0) {
console.log("Multiple of 5: " + req.getMultiple());
}else{
this.nextObjInChain.processMultiple(req);
}
}
}
//configuring the chain of handler objects
var c1 = new MultipleofTwoHandler();
var c2 = new MultipleofThreeHandler();
var c3 = new MultipleofFiveHandler();
c1.setNextObj(c2);
c2.setNextObj(c3);
//the chain handling different cases
c1.processMultiple(new Multiple(95)); // Multiple of 5: 95
c1.processMultiple(new Multiple(50)); // Multiple of 2: 50
c1.processMultiple(new Multiple(9)); // Multiple of 3: 9
c1.processMultiple(new Multiple(4)); // Multiple of 2: 4
c1.processMultiple(new Multiple(21)); // Multiple of 3: 21
c1.processMultiple(new Multiple(23)); // No multiple for: 23
- MultipleofTwoHandler: بررسی میکند که آیا عدد وارد شده، مضربی از 2 است یا نه.
- MultipleofThreeHandler: بررسی میکند که آیا عدد وارد شده، مضربی از 3 است یا نه
- MultipleofFiveHandler: بررسی میکند که آیا عدد وارد شده، مضربی از 5 است یا نه.
class HandlerChain { setNextObj(nextObjInChain){} processMultiple(req){ console.log("No multiple for: " + req.getMultiple()); } }
- تنظیم کردن handler بعدی در زنجیره
- پردازش عدد وارد شده به این منظور که آیا در مضرب مورد نظر قرار دارد یا نه.
class MultipleofTwoHandler extends HandlerChain { constructor(){/*code*/} setNextObj(nextObj){/*code*/} processMultiple(req){/*code*/} } class MultipleofThreeHandler extends HandlerChain { constructor(){/*code*/} setNextObj(nextObj){/*code*/} processMultiple(req){/*code*/} } class MultipleofFiveHandler extends HandlerChain { constructor(){/*code*/} setNextObj(nextObj){/*code*/} processMultiple(req){/*code*/} }
constructor(){ super() this.nextObjInChain = new HandlerChain() }
مقدار دهی اولیه میشود که تعیین کنندهی شیء بعدی در زنجیره میباشد.
setNextObj(nextObj){ this.nextObjInChain = nextObj; }
var c1 = new MultipleofTwoHandler(); var c2 = new MultipleofThreeHandler(); var c3 = new MultipleofFiveHandler(); c1.setNextObj(c2); c2.setNextObj(c3);
class Multiple { constructor(multiple){ this.multiple = multiple } getMultiple(){ return this.multiple; } }
processMultiple(req) { if ((req.getMultiple() % 2) == 0) { console.log("Multiple of 2: " + req.getMultiple()); }else{ this.nextObjInChain.processMultiple(req); } }
c1.processMultiple(new Multiple(95)) // Multiple of 5: 95
- Performance : کارآیی - که در ارتباط با مدت زمان پاسخ گویی و قابل استفاده شدن برنامه میباشد. (مقایسه به درصد - هرچه بیشتر عملکرد بهتر)
- Size : اندازه - که نشان دهنده حجم نهایی فایلهای تولید شده ( Css-Html-JavaScript ) فریم ورک است. این مقایسه اندازه فریم ورک و تمام وابستگیهای آن است که به bundle نهایی برنامه اضافه شده است (هر چه اندازه فایل کمتر باشد بهتر است چراکه توسط کاربر نهایی زودتر دانلود میشود).
- Lines of Code : تعداد خطوط کد - نشان دهنده این است که یک نویسنده بر اساس این فریم ورکها چند سطر کد را باید برای تهیهی یک برنامهی جدید بنویسد.
npx degit sveltejs/template my-svelte-project cd my-svelte-project npm install npm run dev
using System.Collections.Generic; namespace AutoMapperComparison.Models { public class User { public int Id { get; set; } public string Name { get; set; } public ICollection<Address> Addresses { get; set; } } }
using System.ComponentModel.DataAnnotations.Schema; namespace AutoMapperComparison.Models { public class Address { public int Id { get; set; } public double? Code { get; set; } public string Title { get; set; } public int UserId { get; set; } [ForeignKey(nameof(UserId))] public virtual User User { get; set; } } }
using EntityFramework.BulkInsert.Extensions; using System.Collections.Generic; using System.Data.Entity; using System.Data.SqlClient; namespace AutoMapperComparison.Models { public class AppDbContextInitializer : DropCreateDatabaseAlways<AppDbContext> { protected override void Seed(AppDbContext context) { User user = context.Users.Add(new User { Name = "Test" }); context.SaveChanges(); List<Address> addresses = new List<Address>(); for (int i = 0; i < 500000; i++) { addresses.Add(new Address { Id = i, Code = 1, Title = "Test", UserId = user.Id }); } context.BulkInsert(addresses); base.Seed(context); } } public class AppDbContext : DbContext { static AppDbContext() { Database.SetInitializer(new AppDbContextInitializer()); //Database.SetInitializer<AppDbContext>(null); } public AppDbContext() : base(new SqlConnection(@"Data Source=.;Initial Catalog=AppDbContext;Integrated Security=True"), contextOwnsConnection: true) { Configuration.AutoDetectChangesEnabled = false; Configuration.EnsureTransactionsForFunctionsAndCommands = false; Configuration.LazyLoadingEnabled = false; Configuration.ProxyCreationEnabled = false; Configuration.ValidateOnSaveEnabled = false; Configuration.UseDatabaseNullSemantics = false; } public DbSet<User> Users { get; set; } public DbSet<Address> Addresses { get; set; } } }
namespace AutoMapperComparison.Models { public class AddressDto { public int Id { get; set; } public double? Code { get; set; } public string Title { get; set; } public int UserId { get; set; } public string UserName { get; set; } } }
using AutoMapper; using AutoMapper.QueryableExtensions; using AutoMapperComparison.Models; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; namespace AutoMapperComparison { public class Program { public static void Main() { Mapper.Initialize(cfg => { cfg.CreateMap<Address, AddressDto>(); }); Console.WriteLine($"Create Db {DateTimeOffset.UtcNow}"); using (AppDbContext db = new AppDbContext()) { db.Database.Initialize(force: true); db.Database.ExecuteSqlCommand("DBCC DROPCLEANBUFFERS"); //Removes all clean buffers from the buffer pool, and columnstore objects from the columnstore object pool Console.WriteLine(db.Addresses.ProjectTo<AddressDto>()); Console.WriteLine(db.Addresses.Select(add => new AddressDto { Id = add.Id, Code = add.Code, Title = add.Title, UserId = add.UserId, UserName = add.User.Name })); } Console.WriteLine($"Normal Select {DateTimeOffset.UtcNow}"); using (AppDbContext db = new AppDbContext()) { db.Database.ExecuteSqlCommand("DBCC DROPCLEANBUFFERS"); Stopwatch watch = Stopwatch.StartNew(); List<AddressDto> addresses = db.Addresses.AsNoTracking().Select(add => new AddressDto { Id = add.Id, Code = add.Code, Title = add.Title, UserId = add.UserId, UserName = add.User.Name }).ToList(); List<AddressDto> addresses2 = db.Addresses.AsNoTracking().Select(add => new AddressDto { Id = add.Id, Code = add.Code, Title = add.Title, UserId = add.UserId, UserName = add.User.Name }).ToList(); watch.Stop(); Console.WriteLine($"{watch.ElapsedMilliseconds} {addresses.Count} {addresses2.Count}"); } Console.WriteLine($"AutoMapper Exec {DateTimeOffset.UtcNow}"); using (AppDbContext db = new AppDbContext()) { db.Database.ExecuteSqlCommand("DBCC DROPCLEANBUFFERS"); Stopwatch watch = Stopwatch.StartNew(); List<AddressDto> addresses = db.Addresses.AsNoTracking().ProjectTo<AddressDto>().ToList(); List<AddressDto> addresses2 = db.Addresses.AsNoTracking().ProjectTo<AddressDto>().ToList(); watch.Stop(); Console.WriteLine($"{watch.ElapsedMilliseconds} {addresses.Count} {addresses2.Count}"); } Console.ReadKey(); } } }
حال نتایج بدست آمده، در قسمت پایینتر آن نمایان میشود:
البته نتیجهی این آزمایش بسته به سخت افزار سیستم شما ممکن است کمی متفاوت باشد.
در سه آزمایش دیگر به صورت متوالی نتیجهی زیر بدست آمد:
Normal | AutoMapper |
2451 | 2378 |
2120 | 2111 |
2202 | 2124 |
اگر این مقدار جزئی از تفاوت بین دو نوع مختلف آزمایش را مورد نظر نگیریم، میتوان گفت که هر دو روش نتیجهی کاملا یکسانی خواهند داشت. فقط با استفاده از AutoMapper کدهای کمتری نوشته شدهاست!
اما دلیل چیست؟ از آنجایی که ProjectTo از Dto به Model انجام شده و Lambda Expressionی که به سمت Entity Framework فرستاده شدهاست با روش Normal کاملا برابر است و بقیهی عملیات توسط EF انجام میشود، با قاطعیت میتوان گفت که هر دو روش ذکر شده از نظر Performance کاملا یکسان خواهند بود.
نکته: البته به این موضوع باید توجه شود که اگر همین آزمایش را بطور مثال با استفاده از یک Listی از رکوردهای درون Memory ساخته شده توسط خودمان انجام دهیم، آن موقع نتیجهی یکسانی نخواهیم داشت، به دلیل اینکه EFی دیگر وجود نخواهد داشت که مسئولیت بازگشت دادهها را بر عهده بگیرد. از آنجائیکه اکثر کارهایی که توقع داریم AutoMapper برای ما انجام دهد، توسط ORM بازگشت داده میشود، پس میتوان گفت نکتهی فوق تقریبا در دنیای واقعی رخ نخواهد داد و باعث مشکل نخواهد شد.
public static class MyAttributes { [Obsolete] public static void MyMethod1() { } public static void MyMetho2() { } }
حال در این بین این سؤال پیش میآید که چگونه ما هم میتوانیم متادیتاهایی را با سلیقهی خود ایجاد کنیم.
برای تهیهی یک متادیتا از کلاس system.attribute استفاده میکنیم:
public class MyMaxLength:Attribute { }
[MyMaxLength] public class GetCustomProperties { //... }
public class MyMaxLength:Attribute { private int max; public string ErrorText = ""; public MyMaxLength(int max) { this.max = max; ErrorText = string.Format("max Length is {0} chars", max); } }
[MyMaxLength(30)] public class GetCustomProperties { //... } //or [MyMaxLength(30,ErrorText = "شما اجازه ندارید بیش از 30 کاراکتر وارد نمایید")] public class GetCustomProperties { //... }
اجباری کردن Type
هر متادیتا میتواند مختص یک نوع Type باشد که این نوع میتواند یک کلاس، متد، پراپرتی یا ساختار و ... باشد. نحوهی محدود سازی آن توسط یک متادیتا مشخص میشود:
[System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Struct)] public class MyMaxLength:Attribute { private int max; public string ErrorText = ""; public MyMaxLength(int max) { this.max = max; ErrorText = string.Format("max Length is {0} chars", max); } }
[AttributeUsage(AttributeTargets.Property)]
public class User { [MyMaxLength(30, ErrorText = "شما اجازه ندارید بیش از 30 کاراکتر وارد نمایید")] public string Name { get; set; } }
[AttributeUsage(AttributeTargets.Property,AllowMultiple = true)] public class MyMaxLength:Attribute { //.... }
[MyMaxLength(40, ErrorText = "شما اجازه ندارید بیش از 40 کاراکتر وارد نمایید")] [MyMaxLength(50, ErrorText = "شما اجازه ندارید بیش از 50 کاراکتر وارد نمایید")] [MyMaxLength(30, ErrorText = "شما اجازه ندارید بیش از 30 کاراکتر وارد نمایید")] public string Name { get; set; }
آخرین ویژگی که این متادیتا در دسترس ما قرار میدهد، استفاده از خصوصیت ارث بری است که به طور پیش فرض با True مقداردهی شده است. موقعی که شما یک متادیتا را به ویژگی ارث بری مزین کنید، در صورتی که آن کلاس که برایش متادیتا تعریف میکنید به عنوان والد مورد استفاده قرار بگیرد، فرزند آن هم به طور خودکار این متادیتا برایش منظور میگردد. به مثالهای زیر دقت کنید:
دو عدد متادیتا تعریف شده که یکی از آنها ارث بری در آن فعال شده و دیگری خیر.
public class MyAttribute : Attribute { //... } [AttributeUsage(AttributeTargets.Method, Inherited = false)] public class YourAttribute : Attribute { //... }
هر دو متادیتا بر سر یک متد در یک کلاسی که بعدا از آن ارث بری میشود تعریف شده اند.
public class MyClass { [MyAttribute] [YourAttribute] public virtual void MyMethod() { //... } }
public class YourClass : MyClass { public override void MyMethod() { //... } }
Type type = typeof (User); foreach (PropertyInfo property in type.GetProperties()) { foreach (Attribute attribute in property.GetCustomAttributes(true)) { MyMaxLength max = attribute as MyMaxLength; if (max != null) { string Max = max.ErrorText; //انجام عملیات } } }
[MyMaxLength(30, typeof(User))]
public class XamAppExceptionHandler : BitExceptionHandler { public override void OnExceptionReceived(Exception exp, IDictionary<string, string> properties = null) { #if DEBUG System.Diagnostics.Debugger.Break(); #endif base.OnExceptionReceived(exp, properties); } }
BitExceptionHandler.Current = new XamAppExceptionHandler();
حال برای لاگ کردن این خطاها، میتوانید از Microsoft's AppCenter استفاده کنید. استفاده از امکانات App Center رایگان بوده و برای استفادهی در ایران محدودیتی ندارد. ابتدا در سایت مربوطه ثبت نام کنید و سپس سه بار Add New app را بزنید و به نامهای XamApp_Windows، XamApp_iOS و XamApp_Android سه برنامه را بسازید و برای Android و iOS گزینهی Xamarin را انتخاب و برای ویندوز نیز UWP را انتخاب کنید.
سپس پکیجهای Microsoft.AppCenter.Crashes و Microsoft.AppCenter.Analytics را بر روی XamApp نصب نموده و کد زیر را در سه فایل AppDelegate.cs/MainActivity.cs/App.xaml.cs برای iOS/Android/Windows کپی کنید:
AppCenter.Start("copy-your-guid-key-for-iOS-Android-Windows-here", typeof(Crashes), typeof(Analytics));
برای هر یک از برنامههای Android/iOS/Windows یک Guid متفاوت دارید که در قسمت Getting Started در سایت App Center میتوانید آنها را مشاهده کنید. هر بار که این کد را کپی میکنید، مقدار Guid درست را بگذارید.
برای گزارش کردن خطاهای برنامه، کافی است کد زیر را به XamAppExceptionHandler.cs که در ابتدای این قسمت در موردش صحبت کرده بودیم اضافه کنید:
Crashes.TrackError(exp, properties);
حال اگر برنامه را اجرا و شروع به تست کنید و یا آن را در اختیار تسترها و مشتریها بگذارید، نه تنها گزارش تمامی خطاها و کرشها را خواهید داشت که حتی آمار استفاده کنندههای برنامه (شامل کشور و مشخصات دستگاه و ...) را نیز خواهید داشت.
حال در یک کد ده خطی، اگر در خط پنجم خطایی رخ دهد، اگر چه باعث بسته شدن برنامه نمیشود و لاگ نیز میشود، ولی در مواقعی خیلی خاص، شاید بخواهید در صورت رخ دادن خطا، چند خط کد بعدی کماکان اجرا شوند. در این حالت شما Try/Catch مینویسید که برای عبور کردن از خطا از آن استفاده کردهاید. در این صورت، ترجیحا آن را به شکل زیر بنویسید:
// code 1... try { // code 2... } catch (Exception ex) { BitExceptionHandler.Current.OnExceptionReceived(ex, new Dictionary<string, string> { { "SomeData", "2" } }); } // code 3...
در این کد مثال، فرض کنیم که برخی اوقات در code 2 خطایی رخ میدهد که برای ما مهم نیست و میخواهیم حتی در صورت رخ دادن خطا، code 3 اجرا شود. توصیه میکنیم در این موارد که در برنامه خیلی هم نباید متداول باشد، لااقل خطا را با کمک کد BitExceptionHandler.Current.OnExceptionReceived لاگ کنید و همچنین با داشتن یک Dictionary میتوانید حتی دیتای بیشتری را نیز به AppCenter فرستاده و در پرتال مربوطه مشاهده کنید.
به صورت کلی بهتر است از این نوع Try/Catchها پرهیز کنید و حتی اگر جایی Catch ای نوشتید، در نهایت دوباره خطا را throw کنید.
try { // some codes... } catch { // Do something related to exception... // for example, show some alerts to the user. throw; // You don't need to call BitExceptionHandler.Current.OnExceptionReceived... }
در مثال فوق، قصد داریم وقتی خطایی رخ داد، پیامی را به کاربر اطلاع دهیم. در این صورت، پس از نمایش پیام مربوطه، مجددا خطا را throw کنید. در این صورت، نیازی به فراخوانی BitExceptionHandler.Current.OnExceptionReceieved نیز نیست.
البته AppCenter در زمینه پابلیش کردن برنامه و همچنین Push Notification و ... نیز دارای امکاناتی هست که به موضوع این قسمت ارتباطی ندارند.
یکی دیگر از تغییرات عمدهی ASP.NET Core با نگارشهای قبلی آن، نحوهی مدیریت مسیریابیهای سیستم است. در نگارشهای قبلی مبتنی بر HTTP Moduleها، مسیریابیها توسط یک HTTP Module مخصوص، با pipeline اصلی ASP.NET یکپارچه شدهاند و زمانیکه مسیر درخواستی با تنظیمات سیستم تطابق داشته باشد، پردازش کار به HTTP Handler مخصوص ASP.NET MVC منتقل میشود:
اما در ASP.NET Core مبتنی بر میان افزارها، زیر ساخت مسیریابی به صورت زیر تغییر کردهاست:
میان افزار ASP.NET MVC را که در قسمت قبل فعال کردیم، باید بتواند کنترلر و اکشن متد متناظر با URL درخواستی را مشخص کند. این تصمیم گیری نیز بر اساس تنظیماتی به نام Routing انجام میشود. در قسمت قبل، حالت ساده و پیش فرض این تنظیمات را مورد استفاده قرار دادیم
app.UseMvcWithDefaultRoute();
public static IApplicationBuilder UseMvcWithDefaultRoute(this IApplicationBuilder app) { if (app == null) throw new ArgumentNullException("app"); return app.UseMvc((Action<IRouteBuilder>) (routes => routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}"))); }
روش دیگر معرفی این تنظیمات، استفاده از Attribute routing است:
[Route("[controller]/[action]")]
مسیریابیهای قراردادی
در قسمت قبل، یک POCO Controller را به صورت ذیل تعریف کردیم و این کنترلر، بدون تعریف هیچ نوع مسیریابی خاصی در دسترس بود:
namespace Core1RtmEmptyTest.Controllers { public class HomeController { public string Index() { return "Running a POCO controller!"; } } }
app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); });
در این تعاریف، هر کدام از قسمتهای قرارگرفتهی داخل {}، مشخص کنندهی قسمتی از URL دریافتی بوده و نامهای controller و action در اینجا جزو نامهای از پیش مشخص شده هستند و برای نگاشت اطلاعات مورد استفاده قرار میگیرند. برای مثال اگر آدرس home/index/ درخواست شد، برنامه به کلاس HomeController و متد عمومی Index آن هدایت میشود. همچنین قسمت آخر این پردازش به ?id ختم شدهاست. وجود ?، به معنای اختیاری بودن این پارامتر است و اگر در URL ذکر شود، به پارامتر id این اکشن متد، نگاشت خواهد شد. مواردی که پس از = ذکر شدهاند، مقادیر پیش فرض مسیریابی هستند. برای مثال اگر صرفا آدرس home/ درخواست شود، مقدار اکشن متد آن با مقدار پیش فرض index جایگزین خواهد شد و اگر تنها مسیر / درخواست شود، کنترل Home و اکشن متد Index آن پردازش میشوند.
در اینجا به هر تعدادی که نیاز است میتوان متدهای routes.MapRoute را فراخوانی و استفاده کرد؛ اما ترتیب تعریف آنها حائز اهمیت است. هر مسیریابی که در ابتدای لیست اضافه شود، حق تقدم بالاتری خواهد داشت و هر تطابقی با یکی از مسیریابیهای تعریف شده، در همان سطح سبب خاتمهی پردازش سایر مسیریابیها میشود.
استفاده از Attributes برای تعریف مسیریابیها
بجای تعریف قرار دادهای پیش فرض مسیریابی در کلاس آغازین برنامه، میتوان از ویژگی Route نیز استفاده کرد. هرچند روش تعریف مسیریابیهای قراردادی، از نگارشهای آغازین ASP.NET MVC به همراه آن بودهاند، اما با زیاد شدن تعداد کنترلرها و مسیریابیهای سفارشی هر کدام، اینبار با نگاه کردن به یک کنترلر، سریع نمیتوان تشخیص داد که چه مسیریابیهای خاصی به آن مرتبط هستند. برای ساده سازی مدیریت برنامههای بزرگ و ساده سازی تعاریف مسیریابیهای خاص آنها، استفاده از ویژگی Route نیز به ASP.NET MVC اضافه شدهاست.
یک مثال: کنترلر About را درنظر بگیرید:
using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { public class AboutController : Controller { public ActionResult Hello() { return Content("Hello from DNT!"); } public ActionResult SiteName() { return Content("DNT"); } } }
اما اگر آدرس /About/ را درخواست دهیم چطور؟ چون در مسیریابی پیش فرض، تعریف {action=Index} را داریم، یعنی هر زمانیکه در URL درخواستی، قسمت action آن ذکر نشد، آنرا با index جایگزین کن و این کنترلر دارای متد Index نیست. در ادامه اگر بخواهیم متد Hello را تبدیل به متد پیش فرض این کنترلر کنیم، میتوان با استفاده از ویژگی Route به صورت ذیل عمل کرد:
using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { [Route("About")] public class AboutController : Controller { [Route("")] public ActionResult Hello() { return Content("Hello from DNT!"); } [Route("SiteName")] public ActionResult SiteName() { return Content("DNT"); } } }
AmbiguousActionException: Multiple actions matched. The following actions matched route data and had all constraints satisfied: Core1RtmEmptyTest.Controllers.AboutController.Hello (Core1RtmEmptyTest) Core1RtmEmptyTest.Controllers.AboutController.SiteName (Core1RtmEmptyTest)
روش بهتر و refactoring friendly آن نیز به صورت ذیل است:
using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { [Route("[controller]")] public class AboutController : Controller { [Route("")] public ActionResult Hello() { return Content("Hello from DNT!"); } [Route("[action]")] public ActionResult SiteName() { return Content("DNT"); } } }
یک نکته: در حین تعریف مسیریابی یک کنترلر میتوان پیشوندهایی را نیز ذکر کرد؛ برای مثال:
[Route("api/[controller]")]
تعریف قیود، برای مسیریابیهای تعریف شده
فرض کنید به کنترلر About فوق، اکشن متد ذیل را که یک خروجی JSON را بازگشت میدهد، اضافه کردهایم:
//[Route("/Users/{userid}")] [Route("Users/{userid}")] public IActionResult GetUsers(int userId) { return Json(new { userId = userId }); }
و اگر مسیریابی Users/{userid} را تعریف کنیم، یعنی این مسیریابی پس از ذکر کنترلر about، به عنوان یک اکشن متد آن مفهوم پیدا میکند:
http://localhost:7742/about/users/1
در هر دو حالت، ذکر پارامتر userid الزامی است (چون با ? مشخص نشدهاست)؛ مانند:
[Route("/Users/{userid:int?}")]
[Route("Users/{userid:int}")]
قیودی را که در اینجا میتوان ذکر کرد به شرح زیر هستند:
• alpha - معادل است با (a-z, A-Z).
• bool - برای تطابق با مقادیر بولی.
• datetime - برای تطابق با تاریخ میلادی.
• decimal - برای تطابق با ورودیهای اعشاری.
• double - برای تطابق با اعداد اعشاری 64 بیتی.
• float - برای تطابق با اعداد اعشاری 32 بیتی.
• guid - برای تطابق با GUID ها
• int - برای تطابق با اعداد صحیح 32 بیتی.
• length - برای تعیین طول رشته.
• long - برای تطابق با اعداد صحیح 64 بیتی.
• max - برای ذکر حداکثر مقدار یک عدد صحیح.
• maxlength - جهت ذکر حداکثر طول رشتهی مجاز ورودی.
• min - برای ذکر حداقل مقدار یک عدد صحیح.
• minlength - جهت ذکر حداقل طول رشتهی مجاز ورودی.
• range - ذکر بازهی اعداد صحیح مجاز.
• regex - ذکر یک عبارت با قاعده جهت مشخص سازی الگوی قابل پذیرش.
برای ترکیب چندین قید مختلف نیز میتوان از : استفاده کرد:
[Route("/Users/{userid:int:max(1000):min(10)}")]
ذکر نام Route برای ساده سازی تعریف آدرسی به آن
در حین تعریف یک Route میتوان نام دلخواهی را نیز به آن انتساب داد (همانند نام default مسیریابی ثبت شدهی در کلاس آغازین برنامه):
[Route("/Users/{userid:int}", Name="GetUserById")]
string uri = Url.Link("GetUserById", new { userid = 1 });
مشخص سازی ترتیب پردازش مسیریابیها
ترتیب مسیریابیهای ثبت شدهی در کلاس آغازین برنامه، همان ترتیب افزوده شدن و ذکر آنها است.
در اینجا میتوان از خاصیت order نیز استفاده کرد و اعداد کوچکتر، ابتدا پردازش میشوند (مقدار پیش فرض آن نیز صفر است):
[Route("/Users/{userid:int}", Name = "GetUserById", Order = 1)]
امکان تعریف قیود سفارشی
اگر قیودی که تا اینجا ذکر شدند، برای کار شما مناسب نبودند و نیاز بود تا الگوریتم خاصی را جهت محدود سازی دسترسی به یک مسیریابی خاص پیاده سازی کنید، میتوان به صورت ذیل عمل کرد:
using System; using System.Globalization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; namespace Core1RtmEmptyTest { public class CustomRouteConstraint : IRouteConstraint { public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection) { object value; if (!values.TryGetValue(routeKey, out value) || value == null) { return false; } long longValue; if (value is long) { longValue = (long)value; return longValue != 10; } var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); if (long.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out longValue)) { return longValue != 10; } return false; } } }
public class CustomRouteConstraint : IRouteConstraint
در آخر برای ثبت و معرفی آن باید به متد ConfigureServices کلاس آغازین برنامه مراجعه کرد:
public void ConfigureServices(IServiceCollection services) { services.AddRouting(options =>options.ConstraintMap.Add("Custom", typeof(CustomRouteConstraint)));
[Route("/Users/{userid:int:custom}")]
یک نکته: اگر به سورس ASP.NET Core مراجعه کنید ، تمام قیودی را که پیشتر نام بردیم (مانند int، guid و امثال آن) نیز به همین روش تعریف و پیشتر ثبت شدهاند.
معرفی بستهی نیوگت Microsoft.AspNetCore.SpaServices
مسیریابیهای پیش فرض ASP.NET Core با مسیریابیهای برنامههای SPA مانند AngularJS (و امثال آن) تداخل دارند؛ از این جهت که درخواستهای رسیدهی به سرور، ابتدا به موتور پردازشی ASP.NET وارد میشوند و اگر یافت نشدند، کاربر با پیام 404 مواجه خواهد شد و دیگر در اینجا برنامه به مسیریابی خاص مثلا AngularJS 2.0 هدایت نمیشود.
برای این موارد مرسوم است که یک fallback route را در انتهای مسیریابیهای موجود اضافه کنند (به آن catch all هم میگویند)
app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); routes.MapRoute( name: "spa-fallback", template: "{*url}", defaults: new { controller = "Home", action = "Index" }); });
برای حل این مشکل مایکروسافت بستهای را به نام Microsoft.AspNetCore.SpaServices ارائه داده است.
برای افزودن آن بر روی گره references کلیک راست کرده و گزینهی manage nuget packages را انتخاب کنید. سپس در برگهی browse آن Microsoft.AspNetCore.SpaServices را جستجو کرده و نصب نمائید:
انجام این مراحل معادل هستند با افزودن یک سطر ذیل به فایل project.json برنامه:
{ "dependencies": { //same as before "Microsoft.AspNetCore.SpaServices": "1.0.0-beta-000007" },
app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); routes.MapSpaFallbackRoute("spa-fallback", new { controller = "Home", action = "Index" }); });
// Serve wwwroot as root app.UseFileServer(); // Serve /node_modules as a separate root (for packages that use other npm modules client side) app.UseFileServer(new FileServerOptions { // Set root of file server FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "node_modules")), // Only react to requests that match this path RequestPath = "/node_modules", // Don't expose file system EnableDirectoryBrowsing = false });
در صورتیکه طراحی شما بر اساس MVC صورت گرفته است، در کمتر از چند دقیقه و در سه مرحله میتوانید پرونده Rss و Sitemap را برای همیشه ببندید.
پیش از تشریح مراحل، به ساختار این دو فایل توجه کنید.
مراحل کار :
مرحله 1. ایجاد نوع(Type) مورد نیاز برای ایجاد Xml های فوق
مرحله 2 . ایجاد کنترلر XML
مرحله 3. ایجاد مسیریابی(Routing)
مرحله 1 : ابتدا یک کلاس به منظور شکل دهی به اطلاعات، بر اساس خواستههای xml مرتبط با RSS و Sitemap تشکیل دهید:
public class PostToXml { public int PostId { get; set; } public string title { get; set; } public string link { get; set; } public string description { get; set; } public Nullable<DateTime> pubDate { get; set; } }
مرحله 2 : یک کنترلر به نام xml ایجاد کنید و اکشن متدهای زیر را درون آن قرار دهید :
public ContentResult RSS() { var items = GetRssFeed(); var rss = new XDocument(new XDeclaration("1.0", "utf-8", "yes"), new XElement("rss", new XAttribute("version", "2.0"), new XElement("channel", new XElement("title", "آخرین مطالب سایت"), new XElement("link", "http://" + Request.Url.Host+"/rss"), new XElement("description", "آخرین مطالب سایت من"), new XElement("copyright","(c)" + DateTime.Now.Year + ", نام سایت من.تمامی حقوق محفوظ است"), from item in items select new XElement("item", new XElement("title", item.title), new XElement("description", item.description), new XElement("link", item.link), new XElement("pubDate", item.pubDate) ) ) ) ); return Content(rss.ToString(), "text/xml"); } public ContentResult Sitemap() { XNamespace ns = "http://www.sitemaps.org/schemas/sitemap/0.9"; var items = GetLinks(); var sitemap = new XDocument(new XDeclaration("1.0", "utf-8", "yes"), new XElement(ns + "urlset", from item in items select new XElement("url", new XElement("loc", item.link), new XElement("changefreq", "monthly"), new XElement("priority", "0.5") ) ) ); return Content(sitemap.ToString(), "text/xml"); } public IEnumerable<PostToXml> GetRssFeed() { // یک کوئری که لیستی از تایپ مشخص شده به ما بدهد } public IEnumerable<PostToXml> GetLinks() { // یک کوئری که لیستی از تایپ مشخص شده به ما بدهد }
این کنترلر دارای دو اکشن متد Rss و Sitemap است و این اکشنها وظیفهی ایجاد فایلهای Xml را به عهده دارند. مواد اولیه این xml ها از دو متد GetRssFeed و GetLinks تهیه میشوند. ما این مواد را در تمپلیت Rss و Sitemap جایگذاری خواهیم کرد. (به کمک دو اکشن متد Rss و Sitemap )
کافیست لیستی از مواردی را که میخواهیم در Rss یا Sitemap ثبت شوند، تهیه کنیم. این لیست بر اساس شکل تنظیم دیتابیس و مسیریابی سایت، میتواند پیچیده و یا ساده باشد. (به کمک کوئری گرفتن با linq و یا اضافه کردن مستقیم آدرسها به لیست و یا ترکیبی از هر دو مورد) برای درک بهتر موضوع، لطفا تصویر موجود در ابتدای مقاله را مشاهده نمایید.
مرحله 3 : در مرحله آخر کافیست دو مورد زیر را به فایل RoutConfig.cs بیافزایید:
routes.MapRoute( "Sitemap", "sitemap", new { controller = "XML", action = "Sitemap" }); routes.MapRoute( "RSS", "rss", new { controller = "XML", action = "RSS" });
به کمک آدرسهای زیر میتوانید به آنچه که تهیه کردهاید دسترسی داشته باشید :
http://domain.com/rss http://domain.com/sitemap
فایل پروژه را دریافت کنید :
MVC_RSS_Sitemap-43ad3c6681734b34b91deaaabcdba871.rar
معرفی System.Text.Json در NET Core 3.0.
using System; using System.Buffers; using System.Buffers.Text; using System.Text.Json; using System.Text.Json.Serialization; namespace Test { public class LongToStringConverter : JsonConverter<long> { public override long Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.String) { ReadOnlySpan<byte> span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan; if (Utf8Parser.TryParse(span, out long number, out int bytesConsumed) && span.Length == bytesConsumed) { return number; } if (long.TryParse(reader.GetString(), out number)) { return number; } } return reader.GetInt64(); } public override void Write(Utf8JsonWriter writer, long value, JsonSerializerOptions options) { writer.WriteStringValue(value.ToString()); } } }
آشنایی با Leaflet
<script src="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.js"></script> <link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.css" />
#map { height: 600px; }
var map = L.map('map').setView([29.6760859,52.4950737], 13);
var osmUrl='http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; var osmAttrib='Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors'; var osm = new L.TileLayer(osmUrl, { maxZoom: 18, attribution: osmAttrib}).addTo(map);
Marker، دایره و چندضلعی
در کنار نمایش Tileها میتوان اشکال گرافیکی نیز به نقشه اضافه کرد؛ مثل مارکر(نقطه)، مستطیل، دایره و یا یک Popup. اضافه کردن یک Marker به سادگی، با کد زیر صورت میپذیرد:
var marker = L.marker([29.623116,52.497856]).addTo(map);
محل مورد نظر به شیء مارکر پاس داده شده و مقدار بازگشتی به map اضافه شده است.
نمایش چند ضلعی و دایره هم کار ساده ای است. برای دایره باید ابتدا مختصات مرکز دایره و شعاع به متر را به L.circle پاس داد:
var circle = L.circle([29.6308217,52.5048021], 500, { color: 'red', fillColor: '#f03', fillOpacity: 0.5 }).addTo(map);
در کد بالا علاوه بر محل و اندازه دایره، رنگ محیط، رنگ داخل و شفافیت (opacity) نیز مشخص شدهاند.
برای چند ضلعیها میتوان به این صورت عمل کرد:
var polygon = L.polygon([ [29.628453, 52.488838], [29.637368, 52.493987], [29.637168, 52.503987] ]).addTo(map);
کار کردن با Popup ها
از Popup میتوان برای نمایش اطلاعات اضافهای بر روی یک محل خاص یا یک عنوان به مانند Marker استفاده کرد. برای مثال میتوان اطلاعاتی دربارهی محل یک Marker یا دایره نمایش داد. در هنگام ایجاد marker، دایره و چندضلعی مقادیر بازگشتی در متغیرهای جدایی ذخیره شدند. اکنون میتوان به آن اشیاء یک popup اضافه کرد:
marker.bindPopup("باغ عفیف آباد").openPopup(); circle.bindPopup("I am a circle."); polygon.bindPopup("I am a polygon.");
به علت اینکه openPopup برای Marker صدا زده شده، به صورت پیشفرض popup را نمایش میدهد. اما برای بقیه، نمایش با کلیک خواهد بود. البته الزاما نیازی نیست که popup روی یک شیء نمایش داده شود، میتوان popupهای مستقلی نیز ایجاد کرد:
var popup = L.popup() .setLatLng([51.5, -0.09]) .setContent("I am a standalone popup.") .openOn(map);