Check | Code | Description |
Is Null | if(variable is null) return true; |
|
Is Not Null | if(variable is { }) return false |
|
Is Not Null | if(variable is object) return false |
|
Is Null | if(variable == null) return true |
|
Is Not Null | if(variable != null) return false |
|
public class LanguageRouteConstraint : IRouteConstraint
{
public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
{
if (!values.ContainsKey("lang"))
{
return false;
}
var lang = values["lang"].ToString();
var result = lang == "fa" || lang == "en";
return result;
}
}
public void ConfigureServices(IServiceCollection services) { services.AddLocalization(o => o.ResourcesPath = "Resources"); services.Configure<RouteOptions>(options => { options.ConstraintMap.Add("lang", typeof(LanguageRouteConstraint)); }); services.AddMvc() .SetCompatibilityVersion(CompatibilityVersion.Version_2_2) .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix) .AddDataAnnotationsLocalization();
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseRequestLocalization(new RequestLocalizationOptions { DefaultRequestCulture = new RequestCulture(new CultureInfo("fa-IR")), SupportedCultures = new[] { new CultureInfo("en-US"), new CultureInfo("fa-IR"), }, SupportedUICultures = new[] { new CultureInfo("en-US"), new CultureInfo("fa-IR"), }, RequestCultureProviders = new List<IRequestCultureProvider>() { new RouteDataRequestCultureProvider() { UIRouteDataStringKey = "lang", RouteDataStringKey = "lang" } } }); app.UseMvc(routes => { routes.MapRoute( name: "LocalizedAreas", template: "{lang:lang}/{area:exists}/{controller=Home}/{action=Index}/{id?}"); routes.MapRoute( name: "LocalizedDefault", template: "{lang:lang}/{controller=Home}/{action=Index}/{id?}" ); routes.MapRoute( name: "default", template: "{*catchall}", defaults: new { controller = "Home", action = "RedirectToDefaultLanguage" }); });
نگاهی به SignalR Clients
مصرف کنندگان یک Hub میتوانند انواع و اقسام برنامههای کلاینت مانند jQuery Clients و یا حتی یک برنامه کنسول ساده باشند و همچنین Hubهای دیگر نیز قابلیت استفاده از این امکانات Hubهای موجود را دارند. تیم SignalR امکان استفاده از Hubهای آنرا در برنامههای دات نت 4 به بعد، برنامههای WinRT، ویندوز فون 8، سیلورلایت 5، jQuery و همچنین برنامههای CPP نیز مهیا کردهاند. به علاوه گروههای مختلف نیز با توجه به سورس باز بودن این مجموعه، کلاینتهای iOS Native، iOS via Mono و Android via Mono را نیز به این لیست اضافه کردهاند.
بررسی کلاینتهای jQuery
با توجه به پروتکل مبتنی بر JSON سیگنالآر، استفاده از آن در کتابخانههای جاوا اسکریپتی همانند jQuery نیز به سادگی مهیا است. برای نصب آن نیاز است در کنسول پاور شل نوگت، دستور زیر را صادر کنید:
PM> Install-Package Microsoft.AspNet.SignalR.JS
با استفاده از افزونه SignalR jQuery، به دو طریق میتوان به یک Hub اتصال برقرار کرد:
الف) استفاده از فایل proxy تولید شده آن (این فایل، در زمان اجرای برنامه تولید میشود و یا امکان استفاده از آن به کمک ابزارهای کمکی نیز وجود دارد)
نمونهای از آنرا در قسمت قبل ملاحظه کردید؛ همان فایل تولید شده در مسیر /signalr/hubs برنامه. به نوعی به آن Service contract نیز گفته میشود (ارائه متادیتا و قراردادهای کار با یک سرویس Hub). این فایل همانطور که عنوان شد به صورت پویا در زمان اجرای برنامه ایجاد میشود.
امکان تولید آن توسط برنامه کمکی signalr.exe نیز وجود دارد؛ برای دریافت آن میتوان از طریق NuGet اقدام کرد (بسته Microsoft.AspNet.SignalR.Utils) که نهایتا در پوشه packages قرار خواهد گرفت. نحوه استفاده از آن نیز به صورت زیر است:
Signalr.exe ghp http://localhost/
ب) بدون استفاده از فایل proxy و به کمک روش late binding (انقیاد دیر هنگام)
برای کار با یک Hub از طریق jQuery مراحل ذیل باید طی شوند:
1) ارجاعی به Hub باید مشخص شود.
2) روالهای رخدادگردان تنظیم گردند.
3) اتصال به Hub برقرار گردد.
4) متدی فراخوانی شود.
در اینجا باید دقت داشت که امکانات Hub به صورت خواص
$.connection
$.connection.chatHub
خوب، تا اینجا فرض بر این است که یک پروژه خالی ASP.NET را آغاز و سپس فرمان نصب Microsoft.AspNet.SignalR.JS را نیز همانطور که عنوان شد، صادر کردهاید. در ادامه یک فایل ساده html را به نام chat.htm، به این پروژه جدید اضافه کنید (برای استفاده از کتابخانه جاوا اسکریپتی SignalR الزامی به استفاده از صفحات کامل پروژههای وب نیست).
<!DOCTYPE> <html> <head> <title></title> <script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script> <script src="Scripts/jquery.signalR-1.0.1.min.js" type="text/javascript"></script> <script src="http://localhost:1072/signalr/hubs" type="text/javascript"></script> </head> <body> <div> <input id="txtMsg" type="text" /><input id="send" type="button" value="send msg" /> <ul id="messages"> </ul> </div> <script type="text/javascript"> $(function () { var chat; $.connection.hub.logging = true; //اطلاعات بیشتری را در جاوا اسکریپت کنسول مرورگر لاگ میکند chat = $.connection.chat; //این نام مستعار پیشتر توسط ویژگی نام هاب تنظیم شده است chat.client.hello = function (message) { //متدی که در اینجا تعریف شده دقیقا مطابق نام متد پویایی است که در هاب تعریف شده است //به این ترتیب سرور میتواند کلاینت را فراخوانی کند $("#messages").append("<li>" + message + "</li>"); }; $.connection.hub.start(/*{ transport: 'longPolling' }*/); // فاز اولیه ارتباط را آغاز میکند $("#send").click(function () { // Hub's `SendMessage` should be camel case here chat.server.sendMessage($("#txtMsg").val()); }); }); </script> </body> </html>
توضیحات:
همانطور که ملاحظه میکنید ابتدا ارجاعاتی به jquery و jquery.signalR-1.0.1.min.js اضافه شدهاند. سپس نیاز است مسیر دقیق فایل پروکسی هاب خود را نیز مشخص کنیم. اینکار با تعریف مسیر signalr/hubs انجام شده است.
<script src="http://localhost:1072/signalr/hubs" type="text/javascript"></script>
سپس ارجاعی به هاب تعریف شده، تعریف گردیده است. اگر از قسمت قبل به خاطر داشته باشید، توسط ویژگی HubName، نام chat را برگزیدیم. بنابراین connection.chat ذکر شده دقیقا به این هاب اشاره میکند.
سپس سطر chat.client.hello مقدار دهی شده است. متد hello، متدی dynamic و تعریف شده در سمت هاب برنامه است. به این ترتیب میتوان به پیامهای رسیده از طرف سرور گوش فرا داد. در اینجا، این پیامها، به li ایی با id مساوی messages اضافه میشوند.
سپس توسط فراخوانی متد connection.hub.start، فاز negotiation شروع میشود. در اینجا حتی میتوان نوع transport را نیز صریحا انتخاب کرد که نمونهای از آن را به صورت کامنت شده جهت آشنایی با نحوه تعریف آن مشاهده میکنید. مقادیر قابل استفاده در آن به شرح زیر هستند:
- webSockets - forverFrame - serverSentEvents - longPolling
اکنون به صورت جداگانه یکبار برنامه hub را در مرورگر باز کنید. سپس بر روی فایل chat.htm کلیک راست کرده و گزینه مشاهده آن را در مرورگر نیز انتخاب نمائید (گزینه View in browser منوی کلیک راست).
خوب! پروژه کار نمیکند! برای اینکه مشکلات را بهتر بتوانید مشاهده کنید نیاز است به JavaScript Console مرورگر خود مراجعه نمائید. برای مثال در مرورگر کروم دکمه F12 را فشرده و برگه Console آنرا باز کنید. در اینجا اعلام میکند که فاز negotiation قابل انجام نیست؛ چون مسیر پیش فرضی را که انتخاب کرده است، همین مسیر پروژه دومی است که اضافه کردهایم (کلاینت ما در پروژه دوم قرار دارد و نه در همان پروژه اول هاب).
برای اینکه مسیر دقیق hub را در این حالت مشخص کنیم، سطر زیر را به ابتدای کدهای جاوا اسکریپتی فوق اضافه نمائید:
$.connection.hub.url = 'http://localhost:1072/signalr'; //چون در یک پروژه دیگر قرار داریم
using System; using System.Web; using System.Web.Routing; using Microsoft.AspNet.SignalR; namespace SignalR02 { public class Global : HttpApplication { protected void Application_Start(object sender, EventArgs e) { // Register the default hubs route: ~/signalr RouteTable.Routes.MapHubs(new HubConfiguration { EnableCrossDomain = true }); } } }
SignalR: Auto detected cross domain url. jquery.signalR-1.0.1.min.js:10 SignalR: Negotiating with 'http://localhost:1072/signalr/negotiate'. jquery.signalR-1.0.1.min.js:10 SignalR: SignalR: Initializing long polling connection with server. jquery.signalR-1.0.1.min.js:10 SignalR: Attempting to connect to 'http://localhost:1072/signalr/connect?transport=longPolling&connectionToken…NRh72omzsPkKqhKw2&connectionData=%5B%7B%22name%22%3A%22chat%22%7D%5D&tid=3' using longPolling. jquery.signalR-1.0.1.min.js:10 SignalR: Longpolling connected jquery.signalR-1.0.1.min.js:10
در برگه شبکه، مطابق شکل فوق، امکان آنالیز اطلاعات رد و بدل شده مهیا است. برای مثال در حالتیکه سرور پیام دریافتی را به کلیه کلاینتها ارسال میکند، نام متد و نام هاب و سایر پارامترها در اطلاعات به فرمت JSON آن به خوبی قابل مشاهده هستند.
یک نکته:
اگر از ویندوز 8 (یعنی IIS8) و VS 2012 استفاده میکنید، برای استفاده از حالت Web socket، ابتدا فایل وب کانفیگ برنامه را باز کرده و در قسمت httpRunTime، مقدار ویژگی targetFramework را بر روی 4.5 تنظیم کنید. اینبار اگر مراحل negotiation را بررسی کنید در همان مرحله اول برقراری اتصال، از روش Web socket استفاده گردیده است.
تمرین 1
به پروژه ساده و ابتدایی فوق یک تکست باکس دیگر به نام Room را اضافه کنید؛ به همراه دکمه join. سپس نکات قسمت قبل را در مورد الحاق به یک گروه و سپس ارسال پیام به اعضای گروه را پیاده سازی نمائید. (تمام نکات آن با مطلب فوق پوشش داده شده است و در اینجا باید صرفا فراخوانی متدهای عمومی دیگری در سمت هاب، صورت گیرد)
تمرین 2
در انتهای قسمت دوم به نحوه ارسال پیام از یک هاب به هابی دیگر اشاره شد. این MonitorHub را ایجاد کرده و همچنین یک کلاینت جاوا اسکریپتی را نیز برای آن تهیه کنید تا بتوان اتصال و قطع اتصال کلیه کاربران سیستم را مانیتور و مشاهده کرد.
پیاده سازی کلاینت jQuery بدون استفاده از کلاس Proxy
در مثال قبل، از پروکسی پویای مهیای در آدرس signalr/hubs استفاده کردیم. در اینجا قصد داریم، بدون استفاده از آن نیز کار برپایی کلاینت را بررسی کنیم.
بنابراین یک فایل جدید html را مثلا به نام chat_np.html به پروژه دوم برنامه اضافه کنید. سپس محتویات آنرا به نحو زیر تغییر دهید:
<!DOCTYPE> <html> <head> <title></title> <script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script> <script src="Scripts/jquery.signalR-1.0.1.min.js" type="text/javascript"></script> </head> <body> <div> <input id="txtMsg" type="text" /><input id="send" type="button" value="send msg" /> <ul id="messages"> </ul> </div> <script type="text/javascript"> $(function () { $.connection.hub.logging = true; //اطلاعات بیشتری را در جاوا اسکریپت کنسول مرورگر لاگ میکند var connection = $.hubConnection(); connection.url = 'http://localhost:1072/signalr'; //چون در یک پروژه دیگر قرار داریم var proxy = connection.createHubProxy('chat'); proxy.on('hello', function (message) { //متدی که در اینجا تعریف شده دقیقا مطابق نام متد پویایی است که در هاب تعریف شده است //به این ترتیب سرور میتواند کلاینت را فراخوانی کند $("#messages").append("<li>" + message + "</li>"); }); $("#send").click(function () { // Hub's `SendMessage` should be camel case here proxy.invoke('sendMessage', $("#txtMsg").val()); }); connection.start(); }); </script> </body> </html>
کلاینتهای دات نتی SignalR
تا کنون Solution ما حاوی یک پروژه Hub و یک پروژه وب کلاینت جیکوئری است. به همین Solution، یک پروژه کلاینت کنسول ویندوزی را نیز اضافه کنید.
سپس در خط فرمان پاور شل نوگت دستور زیر را صادر نمائید تا فایلهای مورد نیاز به پروژه کنسول اضافه شوند:
PM> Install-Package Microsoft.AspNet.SignalR.Client
پس از نصب آن اگر به پوشه packages مراجعه کنید، نگارشهای مختلف آنرا مخصوص سیلورلایت، دات نتهای 4 و 4.5، WinRT و ویندوز فون8 نیز میتوانید در پوشه Microsoft.AspNet.SignalR.Client ملاحظه نمائید. البته در ابتدای نصب، انتخاب نگارش مناسب، بر اساس نوع پروژه جاری به صورت خودکار صورت میگیرد.
مدل برنامه نویسی آن نیز بسیار شبیه است به حالت عدم استفاده از پروکسی در حین استفاده از jQuery که در قسمت قبل بررسی گردید و شامل این مراحل است:
1) یک وهله از شیء HubConnection را ایجاد کنید.
2) پروکسی مورد نیاز را جهت اتصال به Hub از طریق متد CreateProxy تهیه کنید.
3) رویدادگردانها را همانند نمونه کدهای جاوا اسکریپتی قسمت قبل، توسط متد On تعریف کنید.
4) به کمک متد Start، اتصال را آغاز نمائید.
5) متدها را به کمک متد Invoke فراخوانی نمائید.
using System; using Microsoft.AspNet.SignalR.Client.Hubs; namespace SignalR02.WinClient { class Program { static void Main(string[] args) { var hubConnection = new HubConnection(url: "http://localhost:1072/signalr"); var chat = hubConnection.CreateHubProxy(hubName: "chat"); chat.On<string>("hello", msg => { Console.WriteLine(msg); }); hubConnection.Start().Wait(); chat.Invoke<string>("sendMessage", "Hello!"); Console.WriteLine("Press a key to terminate the client..."); Console.Read(); } } }
نکته مهم
کلیه فراخوانیهایی که در اینجا ملاحظه میکنید غیرهمزمان هستند.
به همین جهت پس از متد Start، متد Wait ذکر شدهاست تا در این برنامه ساده، پس از برقراری کامل اتصال، کار invoke صورت گیرد و یا زمانیکه callback تعریف شده توسط متد chat.On فراخوانی میشود نیز این فراخوانی غیرهمزمان است و خصوصا اگر نیاز است رابط کاربری برنامه را در این بین به روز کنید باید به نکات به روز رسانی رابط کاربری از طریق یک ترد دیگر دقت داشت.
namespace Project.Models { public class EmployeeCreateModel { [Required] [Display(Name = "آدرس ایمیل")] [EmailAddress(ErrorMessage = "لطفاً {0} معتبر وارد کنید.")] [Remote("UserExistByEmailValidation", HttpMethod = "POST", ErrorMessage = "ایمیل وارد شده هم اکنون توسط یکی از کاربران مورد استفاده است.")] public string Email { get; set; } ... } }
namespace Project.Models { public class EmployeeEditModel { public int Id { get; set; } [Required] [Display(Name = "آدرس ایمیل")] [EmailAddress(ErrorMessage = "لطفاً {0} معتبر وارد کنید.")] [Remote("EmailExistForOtherUserValidation", AdditionalFields = "Id", HttpMethod = "POST", ErrorMessage = "ایمیل وارد شده هم اکنون توسط یکی از کاربران مورد استفاده است.")] public string Email { get; set; } .... } }
namespace Project.Web.Controllers { [RoutePrefix("UserValidation")] [Route("{Action}")] [OutputCache(Location = OutputCacheLocation.None, NoStore = true)] public partial class UserValidationController : Controller { readonly IUserService<User> _userService; readonly IUnitOfWork _uow; public UserValidationController(IUnitOfWork uow, IUserService<User> userService) { _userService = userService; _uow = uow; } [HttpPost] [Route("~/CheckEmail", Name = "UserExistByEmailValidation")] public virtual JsonResult CheckEmail(string email) { return Json(!_userService.UserExistsByEmail(email)); } [HttpPost] [Route("~/CheckEmailForOtherUser", Name = "EmailExistForOtherUserValidation")] public virtual JsonResult CheckEmailForOtherUser(string email, int id) { return Json(!_userService.EmailExistForOtherUser(email, id)); } } }
@section Scripts { @Scripts.Render("~/bundles/jqueryval") }
من از این کپچا در یک سرور استفاده کردم، بر روی پورت 80 هیچ خطایی نمیگیرم و تصویر امنیتی بدرستی نمایش داده میشود ولی روی پورت 8080 تصویر کپچا لود نمیشود و خطای زیر را در کنسول کروم لاگ میکند:
System.Security.Cryptography.CryptographicException: Object already exists
Resource interpreted as Image but transferred with MIME type text/html: "http://site:8080/Error/Index?aspxerrorpath=/Shared/CaptchaImage"
1. شاید یکی از آزاردهندهترین مشکلات، برخورد با پیغامهای خطا، هنگام عملیات migration باشد. یکی از دهها نوع خطا، زمانی رخ میدهد که متد seed در حال اجراست. در این حالت هیچ نوع break-point ایی به کمک ما نخواهد آمد.
سوال ایجاست که آیا میتوان این بخش را دیباگ نمود؟ بهترین راه حل، اجرای آپدیت از طریق متدها(یا اکشن ها) است.
فراخوانی migration بسیار ساده است. باید یک نمونه از کلاس Configuration را ساخته و در جایی از پروژه قرار دهیم و صد
البته مطمئن باشیم که migration فعال است.
var configuration = new Configuration(); var migrator = new DbMigrator(configuration); migrator.Update();
اگر بخواهید این تغییرات بر روی دیتابیسی با اسم و رسم انجام شود، از کد زیر بهره بگیرید:
var configuration = new Configuration(); configuration.TargetDatabase = new DbConnectionInfo( "Server=MyServer;Database=MyDatabase;Trusted_Connection=True;", "System.Data.SqlClient"); var migrator = new DbMigrator(configuration); migrator.Update();
2. خطای :
Duplicate type name within an assembly.
معمولا بخاطر وجود break-point این مشکل رخ میدهد. یا break-pointهای درون seed را حذف کنید یا جای آنها را تغییر دهید. [اینجا]
3. خطای :
Validation failed for one or more entities. See 'EntityValidationErrors' property for more details.
این مشکل میتواند دلایل مختلفی داشته
باشد. کد درون متد seed را داخلtry/catch قرا ر دهید و علت آن را بررسی کنید:
try { var user = new ApplicationUser { UserName = "Admin", Phone = "09120000000", Email = "m@gmail.com" }; usermanager.Create(user, "09120000000"); usermanager.AddToRole(user.Id, "admin"); } catch (DbEntityValidationException dbEx) { foreach (var validationErrors in dbEx.EntityValidationErrors) { foreach (var validationError in validationErrors.ValidationErrors) { Trace.TraceInformation("Property: {0} Error: {1}", validationError.PropertyName, validationError.ErrorMessage); } } }
فراموش نکنید، seed را به کمک حالتی که در شماره 1 گفته شد اجرا کنید تا بتوانید از BreakPoint استفاده کنید.
4.خطای :
More than one context type was found in the assembly 'Ex1_CodeFirst'
این خطا وقتی ظاهر میشود که چندین Context برای
پروژه جاری داشته باشیم و ویژوال استودیو نتواند Context مورد نظر ما را تشخیص
دهد. بهتر است هنگام اجرای migration نام context مورد نظر را بنویسیم (مثلا MyConfiguration نام کانتکست شماست) :
Update-Database -ConfigurationTypeName MyConfiguration
There is already an object named 'UserProfile' in the database.
یعنی مدل شما پس از اینکه جدولی با همین نام (در این جا به عنوان مثال UserProfile) از پیش موجود بوده، تغییر کرده است؛ پس migration آپدیت شدنی نیست. ابتدا این دستور را مینویسیم (پیش از آنکه تغییراتمان را روی کلاس مربوط به جدول UserProfile اعمال کنیم):
Add-Migration Initial –IgnoreChanges
update-database –verbose
گاهی این مشکل زمانی پیش میآید که واقعا تغییری در جدول نامبرده انجام ندادهایم. در این حالت روش پلهای زیر را به کار میبریم:
- پاک کردن یا ریست کردن migration (در شماره 9 همین مقاله این کار در چند مرحله توضیح داده شده است. در مرحله سوم حتما از Add-Migration Initial –IgnoreChanges استفاده کنید)
- بعد از آپدیت دیتابیس باید فایل زیر دارای محتویات باشد
محتویات این فایل دقیقا شرایط فعلی جدول شماست.
- اگر این فایل ایجاد نشده است مراحل را تکرار کنید تا محتویات درون آن را ببینید.
- حالا تغییری را در یکی از مدلهای خود انجام دهید. احتمالا با مشکل آپدیت شدن مواجه میشوید و پیغام زیر را دوباره خواهید دید:
There is already an object named 'UserProfile' in the database
- جدولی را که پیغام خطا به آن اشاره کرده، در فایل فوق بیابید و محدوده create آن را کامنت کنید تا ساخته نشود.
- دیتابیس را آپدیت کنید، احتمالا پیغام خطای فوق برای جدول دیگری نمایش داده میشود. آن را هم کامنت کنید و دیتابیس را آپدیت کنید و اگر باز هم خطا بود مکانیزم بالا را تا جایی تکرار کنید که خطایی نبینید .
- حالا جدولی را که تغییراتی در آن داده بودید، در دیتابیس چک کنید که تغییرات اعمال شده باشد.
- هر آنچه را در فایل initial کامنت کرده بودید، از کامنت خارج کنید و دیتابیس را آپدیت کنید .
- برای آزمایش، یک آیتم به یکی از مدلها اضافه کنید و ببینید که migration درست کار میکند یا خیر.
Unable to update database to match the current model because there are pending changes and automatic migration is disabled. Either write the pending model changes to a code-based migration or enable automatic migration. Set DbMigrationsConfiguration.AutomaticMigrationsEnabled to true to enable automatic migration. You can use the Add-Migration command to write the pending model changes to a code-based migration.
7.خطای :
Automatic migration was not applied because it would result in data loss.
AutomaticMigrationDataLossAllowed = true;
Introducing FOREIGN KEY constraint 'FK_dbo.ProductProductGroups_dbo.ProductGroups_ProductGroupId' on table 'ProductProductGroups' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints. Could not create constraint or index. See previous errors.
9. راه حل برای خطاهای عجیبی که شاید نیاز به صرف زمان بیشتر برای کشف و برطرف کردن داشته باشند :
بهتر نیست مایگریشن خود را از نو بسازید؟
روش به روز رسانی و بازسازی migration [اینجا ]:
- پاک کردن فولدر Migrations در پروژه
- پاک کردن جدولی به نام MigrationHistory_ در دیتابیس (ممکن است زیر مجموعه جدولهای system باشد)
- اجرای دستور زیر کنسول پکیچ منیجر ویژوال استودیو :
Enable-Migrations -EnableAutomaticMigrations -Force
- اجرای دستور زیر :
Add-Migration Initial
میانافزار چندسکویی فشرده سازی صفحات در ASP.NET Core
پیشتر مطلب «استفاده از GZip توکار IISهای جدید و تنظیمات مرتبط با آنها» را در سایت جاری مطالعه کردهاید. این قابلیت صرفا وابستهاست به IIS و همچنین در صورت نصب بودن ماژول httpCompression آن کار میکند. بنابراین قابلیت انتقال به سایر سیستم عاملها را نخواهد داشت و هرچند تنظیمات فایل web.config آن هنوز هم در برنامههای ASP.NET Core معتبر هستند، اما چندسکویی نیستند. برای رفع این مشکل، تیم ASP.NET Core، میانافزار توکاری را برای فشرده سازی صفحات ارائه دادهاست که جزئی از تازههای ASP.NET Core 1.1 نیز بهشمار میرود.
برای نصب آن دستور ذیل را در کنسول پاورشل نیوگت، اجرا کنید:
PM> Install-Package Microsoft.AspNetCore.ResponseCompression
{ "dependencies": { "Microsoft.AspNetCore.ResponseCompression": "1.0.0" } }
مرحلهی بعد، افزودن سرویسهای و میان افزار مرتبط، به کلاس آغازین برنامه هستند. همیشه متدهای Add کار ثبت سرویسهای میانافزار را انجام میدهند و متدهای Use کار افزودن خود میانافزار را به مجموعهی موجود تکمیل میکنند.
public void ConfigureServices(IServiceCollection services) { services.AddResponseCompression(options => { options.MimeTypes = Microsoft.AspNetCore.ResponseCompression.ResponseCompressionDefaults.MimeTypes; }); }
namespace Microsoft.AspNetCore.ResponseCompression { /// <summary> /// Defaults for the ResponseCompressionMiddleware /// </summary> public class ResponseCompressionDefaults { /// <summary> /// Default MIME types to compress responses for. /// </summary> // This list is not intended to be exhaustive, it's a baseline for the 90% case. public static readonly IEnumerable<string> MimeTypes = new[] { // General "text/plain", // Static files "text/css", "application/javascript", // MVC "text/html", "application/xml", "text/xml", "application/json", "text/json", }; } }
services.AddResponseCompression(options => { options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[] { "image/svg+xml", "application/font-woff2" }); });
به علاوه options ذکر شدهی در اینجا دارای خاصیت options.Providers نیز میباشد که نوع و الگوریتم فشرده سازی را مشخص میکند. در صورتیکه مقدار دهی نشود، مقدار پیش فرض آن Gzip خواهد بود:
services.AddResponseCompression(options => { //If no compression providers are specified then GZip is used by default. //options.Providers.Add<GzipCompressionProvider>();
همچنین اگر علاقمند بودید تا میزان فشرده سازی تامین کنندهی Gzip را تغییر دهید، نحوهی تنظیمات آن به صورت ذیل است:
services.Configure<GzipCompressionProviderOptions>(options => { options.Level = System.IO.Compression.CompressionLevel.Optimal; });
به صورت پیشفرض، فشرده سازی صفحات Https انجام نمیشود. برای فعال سازی آن تنظیم ذیل را نیز باید قید کرد:
options.EnableForHttps = true;
مرحلهی آخر این تنظیمات، افزودن میان افزار فشرده سازی خروجی به لیست میان افزارهای موجود است:
public void Configure(IApplicationBuilder app) { app.UseResponseCompression() // Adds the response compression to the request pipeline .UseStaticFiles(); // Adds the static middleware to the request pipeline }
تنظیمات کش کردن چندسکویی فایلهای ایستا در ASP.NET Core
تنظیمات کش کردن فایلهای ایستا در web.config مخصوص IIS به صورت ذیل است :
<staticContent> <clientCache httpExpires="Sun, 29 Mar 2020 00:00:00 GMT" cacheControlMode="UseExpires" /> </staticContent>
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { app.UseResponseCompression() .UseStaticFiles( new StaticFileOptions { OnPrepareResponse = _ => _.Context.Response.Headers[HeaderNames.CacheControl] = "public,max-age=604800" // A week in seconds }) .UseMvc(routes => routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}")); }
معادل چندسکویی ماژول URL Rewrite در ASP.NET Core
مثالهایی از ماژول URL Rewrite را در مباحث بهینه سازی سایت برای بهبود SEO پیشتر بررسی کردهایم (^ و ^ و ^). این ماژول نیز همچنان در ASP.NET Core هاست شدهی در ویندوز و IIS قابل استفاده است (البته به شرطی که ماژول مخصوص آن در IIS نصب و فعال شده باشد). معادل چندسکویی این ماژول به صورت یک میانافزار توکار به ASP.NET Core 1.1 اضافه شدهاست.
برای استفادهی از آن، ابتدا نیاز است بستهی نیوگت آنرا به نحو ذیل نصب کرد:
PM> Install-Package Microsoft.AspNetCore.Rewrite
{ "dependencies": { "Microsoft.AspNetCore.Rewrite": "1.0.0" } }
پس از نصب آن، نمونهای از نحوهی تعریف و استفادهی آن در کلاس آغازین برنامه به صورت ذیل خواهد بود:
public void Configure(IApplicationBuilder app) { app.UseRewriter(new RewriteOptions() .AddRedirectToHttps() .AddRewrite(@"app/(\d+)", "app?id=$1", skipRemainingRules: false) // Rewrite based on a Regular expression //.AddRedirectToHttps(302, 5001) // Redirect to a different port and use HTTPS .AddRedirect("(.*)/$", "$1") // remove trailing slash, Redirect using a regular expression .AddRedirect(@"^section1/(.*)", "new/$1", (int)HttpStatusCode.Redirect) .AddRedirect(@"^section2/(\\d+)/(.*)", "new/$1/$2", (int)HttpStatusCode.MovedPermanently) .AddRewrite("^feed$", "/?format=rss", skipRemainingRules: false));
در اینجا مثالهایی را از اجبار به استفادهی از HTTPS، تا حذف / از انتهای مسیرهای وب سایت و یا هدایت آدرس قدیمی فید سایت، به آدرسی جدید واقع در مسیر format=rss، توسط عبارات باقاعده مشاهده میکنید.
در این تنظیمات اگر پارامتر skipRemainingRules به true تنظیم شود، به محض برآورده شدن شرط انطباق مسیر (پارامتر اول ذکر شده)، بازنویسی مسیر بر اساس پارامتر دوم، صورت گرفته و دیگر شرطهای ذکر شده، پردازش نخواهند شد.
این میانافزار قابلیت دریافت تعاریف خود را از فایلهای web.config و یا htaccess (لینوکسی) نیز دارد:
app.UseRewriter(new RewriteOptions() .AddIISUrlRewrite(env.ContentRootFileProvider, "web.config") .AddApacheModRewrite(env.ContentRootFileProvider, ".htaccess"));
و یا اگر خواستید منطق پیچیدهتری را نسبت به عبارات باقاعده اعمال کنید، میتوان یک IRule سفارشی را نیز به نحو ذیل تدارک دید:
public class RedirectWwwRule : Microsoft.AspNetCore.Rewrite.IRule { public int StatusCode { get; } = (int)HttpStatusCode.MovedPermanently; public bool ExcludeLocalhost { get; set; } = true; public void ApplyRule(RewriteContext context) { var request = context.HttpContext.Request; var host = request.Host; if (host.Host.StartsWith("www", StringComparison.OrdinalIgnoreCase)) { context.Result = RuleResult.ContinueRules; return; } if (ExcludeLocalhost && string.Equals(host.Host, "localhost", StringComparison.OrdinalIgnoreCase)) { context.Result = RuleResult.ContinueRules; return; } string newPath = request.Scheme + "://www." + host.Value + request.PathBase + request.Path + request.QueryString; var response = context.HttpContext.Response; response.StatusCode = StatusCode; response.Headers[HeaderNames.Location] = newPath; context.Result = RuleResult.EndResponse; // Do not continue processing the request } }
و سپس میتوان آنرا به عنوان یک گزینهی جدید Rewriter معرفی نمود:
app.UseRewriter(new RewriteOptions().Add(new RedirectWwwRule()));
یک نکته: در اینجا در صورت نیاز میتوان از تزریق وابستگیهای در سازندهی کلاس Rule جدید تعریف شده نیز استفاده کرد. برای اینکار باید RedirectWwwRule را به لیست سرویسهای متد ConfigureServices معرفی کرد و سپس نحوهی دریافت وهلهای از آن جهت معرفی به میانافزار بازنویسی مسیرهای وب به صورت ذیل درخواهد آمد:
var options = new RewriteOptions().Add(app.ApplicationServices.GetService<RedirectWwwRule>());
EF Code First #4
<add name="TestContext" connectionString="Data Source=localhost;Initial Catalog=Test;User ID=sa;Password=123;MultipleActiveResultSets=True;" providerName="System.Data.EntityClient" />
<add name="TestContext" connectionString="Data Source=localhost;Initial Catalog=Test;Integrated Security=True" providerName="System.Data.EntityClient" />
پیش از ادامهی مثال قسمت قبل، قصد داریم تمام کدهای موجود در فایل Pages\Index.razor را به یک فایل اختصاصی آنها منتقل کرده و مسیریابی و منوی آنرا تکمیل کنیم. به همین جهت در پوشهی Pages، یک پوشهی جدید را به نام LearnBlazor ایجاد کرده و درون آن، فایل خالی BindProp.razor را ایجاد میکنیم. سپس تمام محتوای فایل Pages\Index.razor را cut کرده و به درون فایل جدید Pages\LearnBlazor\BindProp.razor، منتقل و Paste میکنیم.
پس از این تغییرات، در فایل Pages\Index.razor، مهمترین سطر آن، همان اولین سطر تعریف مسیریابی آن خواهد بود و هر محتوای دلخواهی که علاقمند بودید:
@page "/" <h1>Hello, world!</h1>
@page "/bindprop"
<li class="nav-item px-3"> <NavLink class="nav-link" href="bindprop"> <span class="oi oi-list-rich" aria-hidden="true"></span> Bind Properties </NavLink> </li>
نمایش لیست اتاقهای تعریف شده، به همراه ویژگیهای آنها
در قسمت قبل، نمایش ردیفی لیست اتاقهای تعریف شده را مشاهده کردید. در این قسمت میخواهیم هر اتاق تعریف شده را در یک card جداگانه نمایش دهیم. هدف این است که در ابتدا به یک UI متداول شلوغ برسیم و بعد شروع کنیم به Refactoring این UI پیچیده، به کامپوننتهای کوچکتر تشکیل دهندهی آن، جهت مدیریت سادهتر این UI و درک بهتر آن. بنابراین در ابتدا با یک کامپوننت کلی شلوغ، شروع خواهیم کرد.
به همین جهت فایل جدید Pages\LearnBlazor\DemoHotel.razor را برای نمایش لیست اتاقهای موجود اضافه میکنیم. سپس محتوای آنرا به صورت زیر تغییر خواهیم داد:
@page "/demoHotel" <h3>Hotel Rooms</h3> <div class="border p-2 mt-2" style="background-color:azure"> <h2 class="text-info">Rooms List</h2> <div class="row container"> @foreach (var room in Rooms) { <div class="bg-light border p-2 col-5 ml-2"> <h4 class="text-secondary">Room - @room.Id</h4> @room.Name<br /> @room.Price.ToString("c")<br /> <input type="checkbox" @bind-value="room.IsActive" checked="@(room.IsActive?"checked":null)" /> Is Active<br /> <span>This room is @(room.IsActive?"Active": "InActive")</span> @if (room.IsActive) { @foreach (var roomProp in room.RoomProps) { <p>@roomProp.Name - @roomProp.Value</p> } } <input type="button" class="btn btn-danger" value="Delete" /> <input type="button" class="btn btn-success" value="Edit" /> </div> } </div> </div>
- سپس مسیریابی منتهی به این کامپوننت، به آدرس demoHotel/ تنظیم شدهاست. این مسیریابی را در کامپوننت Shared\NavMenu.razor به صورت زیر مورد استفاده قرار خواهیم داد تا مدخل منوی جدیدی برای آن تهیه شود:
<li class="nav-item px-3"> <NavLink class="nav-link" href="demoHotel"> <span class="oi oi-list-rich" aria-hidden="true"></span> Demo Hotel </NavLink> </li>
تبدیل دکمههای حذف و ویرایش هر اتاق به یک کامپوننت جدید
اکنون میخواهیم کامپوننت شلوغ Pages\LearnBlazor\DemoHotel.razor را به چند زیر کامپوننت بشکنیم تا هر کدام وظایف خاص خود را انجام دهند و در نهایت به یک UI قابل درکتر برسیم. برای مثال میخواهیم دکمههای حذف و ویرایش هر اتاق را به یک کامپوننت جدید منتقل کنیم تا هم این UI خلوتتر شود و هم اگر در قسمت دیگری از برنامه نیاز به یک چنین دکمههایی بود، بتوان از آن کامپوننت اختصاصی، استفادهی مجدد کرد.
برای این منظور ابتدا پوشهی جدید Pages\LearnBlazor\LearnBlazorComponents را افزوده و سپس در داخل آن، فایل جدید کامپوننت EditDeleteButton.razor را نیز ایجاد میکنیم. در این فایل جدید در ابتدا کدهای دو دکمهی تعریف شده را از کامپوننت DemoHotel.razor انتخاب و cut کرده و سپس در این فایل جدید paste میکنیم. در این کامپوننت جدید، نیازی به تعریف page@ و مسیریابی آن نیست. به این معنا که این کامپوننت، یک کامپوننت اشتراکی است و routable نیست و قرار است در داخل یک کامپوننت دیگر مورد استفاده قرار گیرد.
بنابراین تا اینجا محتوای کامپوننت EditDeleteButton.razor فقط از دو سطر زیر تشکیل میشود:
<input type="button" class="btn btn-danger" value="Delete" /> <input type="button" class="btn btn-success" value="Edit" />
<BlazorServerSample.Pages.LearnBlazor.LearnBlazorComponents.EditDeleteButton></BlazorServerSample.Pages.LearnBlazor.LearnBlazorComponents.EditDeleteButton>
@using BlazorServerSample.Pages.LearnBlazor.LearnBlazorComponents
اکنون میتوان تعریف مدخل کامپوننت را به صورت زیر خلاصه کرد:
<EditDeleteButton></EditDeleteButton>
ارسال پارامترها به یک کامپوننت
فرض کنید قصد داریم دکمههای ویرایش و حذف را تنها به کاربران ادمین نمایش دهیم. به همین جهت نیاز است بتوان پارامتری مانند IsAdmin را به کامپوننت EditDeleteButton ارسال کرد. برای اینکار کامپوننت Pages\LearnBlazor\LearnBlazorComponents\EditDeleteButton.razor را به صورت زیر ویرایش میکنیم:
@if (IsAdmin) { <input type="button" class="btn btn-danger" value="Delete" /> <input type="button" class="btn btn-success" value="Edit" /> } @code { [Parameter] public bool IsAdmin { get; set; } }
پس از تعریف این پارامتر ورودی، روش استفادهی از آن در کامپوننت DemoHotel به صورت زیر است:
<EditDeleteButton IsAdmin="true"></EditDeleteButton>
انتقال هر اتاق به کامپوننت مجزای خاص خودش
در ادامه میخواهیم محتوای حلقهی foreach (var room in Rooms)@ کامپوننت DemoHotel را به طور کامل cut کرده و در یک کامپوننت جدید paste کنیم تا به حلقهای خواناتر و با مسئولیتهای کمتری برسیم. نگهداری کدهایی که قسمتهای مختلف آن از هم ایزوله شدهاند و دامنهی تغییرات آنها کاملا مشخص و محدود است، در طول زمان بسیار سادهتر از نگهداری کدهای UI ای در هم تنیدهاست.
به همین جهت ابتدا کامپوننت جدید Pages\LearnBlazor\LearnBlazorComponents\IndividualRoom.razor را ایجاد میکنیم و سپس، هر آنچه داخل حلقهی foreach یاد شده قرار دارد را انتخاب و cut کرده و درون این کامپوننت جدید paste میکنیم:
<div class="bg-light border p-2 col-5 offset-1"> <h4 class="text-secondary">Room - @Room.Id</h4> @Room.Name<br /> @Room.Price.ToString("c")<br /> <input type="checkbox" @bind-value="Room.IsActive" checked="@(Room.IsActive?"checked":null)" /> Is Active<br /> <span>This room is @(Room.IsActive?"Active": "InActive")</span> @if (Room.IsActive) { @foreach (var roomProp in Room.RoomProps) { <p>@roomProp.Name - @roomProp.Value</p> } } <EditDeleteButton IsAdmin="true"></EditDeleteButton> </div> @code { [Parameter] public BlazorRoom Room { get; set; } }
پس از این تغییر، کدهای حلقهی foreach کامپوننت DemoHotel.razor به صورت زیر خلاصه میشوند. در اینجا روش ارسال یک شیء را به پارامتر Room نیز مشاهده میکنید (البته ذکر @ در اینجا الزامی نیست و میشد از روش مقدار دهی "Room="room نیز استفاده کرد):
<div class="row container"> @foreach (var room in Rooms) { <IndividualRoom Room="@room"></IndividualRoom> } </div>
یک تمرین: نمایش لیست امکانات رفاهی هتل
پس از نمایش لیست اتاقهای یک هتل، قصد داریم لیست امکانات رفاهی آنرا نیز نمایش دهیم:
مدل این امکانات را به صورت زیر به پوشهی Models برنامه اضافه میکنیم:
namespace BlazorServerSample.Models { public class BlazorAmenity { public int Id { get; set; } public string Name { get; set; } public string Description { get; set; } } }
@code{ List<BlazorAmenity> AmenitiesList = new List<BlazorAmenity>(); // ... protected override void OnInitialized() { base.OnInitialized(); // ... AmenitiesList.Add(new BlazorAmenity { Id = 111, Name = "Gym", Description = "24x7 gym room is available." }); AmenitiesList.Add(new BlazorAmenity { Id = 222, Name = "Swimming Pool", Description = "Pool room is open from 6am to 10pm." }); AmenitiesList.Add(new BlazorAmenity { Id = 333, Name = "Free Brakfast", Description = "Enjoy free breakfast at out hotel." }); } }
اکنون برای نمایش تک تک عناصر این لیست، ابتدا یک کامپوننت منحصر به یک BlazorAmenity را به نام Pages\LearnBlazor\LearnBlazorComponents\IndividualAmenity.razor ایجاد میکنیم با این محتوا:
<div class="bg-light border p-2 col-5 offset-1 mt-2"> <h4 class="text-secondary">Amenity - @Amenity.Id</h4> @Amenity.Name<br /> @Amenity.Description<br /> </div> @code { [Parameter] public BlazorAmenity Amenity { get; set; } }
در آخر پس از تعریف کامپوننت IndividualAmenity.razor، روش استفادهی از آن در کامپوننت DemoHotel به صورت زیر است:
<div class="col-12 mt-4"> <h4 class="text-info">Hotel Amenities</h4> </div> @foreach (var amenity in AmenitiesList) { <IndividualAmenity Amenity="@amenity"></IndividualAmenity> }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-05.zip
یک: ASP.NET Core مستقل از Platform است
دو: Open Source است
سه: جدا بودن از Web Server
using System; using Microsoft.AspNetCore.Hosting; namespace aspnetcoreapp { public class Program { public static void Main(string[] args) { var host = new WebHostBuilder() .UseKestrel() .UseStartup<Startup>() .Build(); host.Run(); } } }
چهار: تزریق وابستگی (Dependency Injection) تو کار
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices (IServiceCollection services) { // Add framework services. services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); services.AddIdentity<ApplicationUser, IdentityRole>() .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders(); services.AddMvc(); // Add application services. services.AddTransient<IEmailSender, AuthMessageSender>(); services.AddTransient<ISmsSender, AuthMessageSender>(); }
پنج: یکپارچگی با frameworkهای مدرن سمت کلاینت
- bundle و minify کردن فایلهای جاوا اسکریپت و همینطور CSS
- اجرای ابزارهایی برای bundle و minify کردن قبل از هر build
- کامپایل کردن فایلهای LESS و SASS در CSS
- کامپیال کردن فایلهای CoffeeScript و TypeScript در JavaScript