Today, we’re releasing .NET 5.0 Preview 2. It contains a set of smaller features and performance improvements. We’re continuing to work on the bigger features that will define the 5.0 release, some of which are starting to show up as initial designs at dotnet/designs.
کش توزیع شده در ASP.NET Core
Caching can help improve the performance of an ASP.NET Core application. Distributed caching is helpful when working with an ASP.NET application that’s deployed to a server farm or scalable cloud environment. Microsoft documentation contains examples of doing this with SQL Server or Redis, but in this post,I’ll show you an alternative.
در Blazor میتوان مسیریابیهای پارامتری را به صورت زیر نیز تعریف کرد:
@page "/post/edit/{EditId:int}"
که در اینجا EditId، یک پارامتر مسیریابی از نوع int تعریف شده و به صورت زیر در کدهای صفحهی مرتبط، قابل دسترسی است:
[Parameter] public int? EditId { set; get; }
int تعریف شدهی در این مسیریابی، یک routing constraint و یا یک قید مسیریابی محسوب میشود و استفادهی از آن، چنین مزایایی را به همراه دارد:
- در این حالت فقط EditId های عددی پردازش میشوند و اگر رشتهای دریافت شود، کاربر با خروجی از نوع 404 و یا «یافت نشد»، مواجه خواهد شد.
- امکان اعتبارسنجی مقادیر دریافتی، پیش از ارسال آنها به صفحه و پردازش صفحه.
قیود پیشفرض تعریف شدهی در Blazor
اگر به مستندات مسیریابی Blazor مراجعه کنیم، بهنظر فقط این موارد را میتوان بهعنوان قیود پارامترهای مسیریابی تعریف کرد:
bool, datetime, decimal, double, float, guid, int, long, nonfile
و ... توضیحاتی در مورد اینکه آیا امکان بسط آنها وجود دارند یا خیر، فعلا در مستندات رسمی آن، ذکر نشدهاند.
در Blazor 8x میتوان از قیود مسیریابی سفارشی ASP.NET Core نیز استفاده کرد!
ASP.NET Core سمت سرور، به همراه امکان سفارشی سازی قیودمسیریابی خود نیز هست که آنرا میتوان به کمک اینترفیسIRouteConstraint پیاده سازی کرد:
namespace Microsoft.AspNetCore.Routing { public interface IRouteConstraint { bool Match( HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection); } }
جالب اینجا است که میتوان این نمونههای سفارشی را حداقل در نگارش جدید Blazor 8x SSR نیز استفاده کرد؛ هرچند در مستندات رسمی Blazor هنوز به آن اشارهای نشدهاست.
در امضای متد Match فوق، دو پارامتر routeKey و values آن بیش از مابقی مهم هستند:
- routeKey مشخص میکند که الان کدام پارامتر مسیریابی (مانند EditId در این مطلب) در حال پردازش است.
- values، یک دیکشنری است که کلید هر عضو آن، پارامتر مسیریابی و مقدار آن، مقدار دریافتی از URL جاری است.
- اگر این متد مقدار true را برگرداند، یعنی مسیریابی وارد شدهی به آن، با موفقیت پردازش و اعتبارسنجی شده و میتوان صفحهی مرتبط را نمایش داد؛ در غیراینصورت، کاربر پیام یافت نشدن آن صفحه و مسیر درخواستی را مشاهده میکند.
پیاده سازی یک قید سفارشی رمزگشایی پارامترهای مسیریابی
فرض کنید قصد ندارید که پارامترهای مسیریابی ویرایش رکوردهای خاصی را دقیقا بر اساس Id متناظر عددی آنها در بانک اطلاعاتی، نمایش دهید؛ برای مثال نمیخواهید دقیقا آدرس post/edit/1 را به کاربر نمایش دهید؛ چون نمایش این اعداد عموما ساده و ترتیبی، حدس زدن آنها را ساده کرده و ممکن است در آینده مشکلات امنیتی را به همراه داشته باشد.
میخواهیم از آدرسهای متداول و سادهی عددی زیر:
@page "/post/edit/{EditId:int}"
به آدرس رمزنگاری شدهی زیر برسیم:
@page "/post/edit/{EditId:encrypt}"
اگر به این آدرس جدید دقت کنید، در اینجا از نام قید جدیدی به نام encrypt استفاده شدهاست که جزو قیود پیشفرض سیستم مسیریابی Blazor نیست. روش تعریف آن به صورت زیر است:
using System.Globalization; using DNTCommon.Web.Core; namespace Blazor8xSsrComponents.Utils; public class EncryptedRouteConstraint(IProtectionProviderService protectionProvider) : IRouteConstraint { public const string Name = "encrypt"; public bool Match(HttpContext? httpContext, IRouter? route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection) { ArgumentNullException.ThrowIfNull(routeKey); ArgumentNullException.ThrowIfNull(values); if (!values.TryGetValue(routeKey, out var routeValue)) { return false; } var valueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture); values[routeKey] = string.IsNullOrEmpty(valueString) ? null : protectionProvider.Decrypt(valueString); return true; } }
توضیحات:
- در قیود سفارشی میتوان سرویسها را به سازندهی کلاس تزریق کرد و برای مثال از سرویس IProtectionProviderService که در کتابخانهی DNTCommon.Web.Core تعریف شده، برای رمزگشایی اطلاعات رسیده، استفاده کردهایم.
- یک نام را هم برای آن درنظر گرفتهایم که از این نام در فایل Program.cs به صورت زیر استفاده میشود:
builder.Services.Configure<RouteOptions>(opt => { opt.ConstraintMap.Add(EncryptedRouteConstraint.Name, typeof(EncryptedRouteConstraint)); });
یعنی زمانیکه سیستم مسیریابی به قید جدیدی به نام encrypt میرسد:
@page "/post/edit/{EditId:encrypt}"
آنرا در لیست ConstraintMap ای که به نحو فوق به سیستم معرفی شده، جستجو میکند. اگر این نام یافت شد، سپس کلاس EncryptedRouteConstraint متناظر را نمونه سازی کرده و در جهت پردازش مسیر رسیده، مورد استفاده قرار میدهد.
- در کلاس EncryptedRouteConstraint و متد Match آن، مقدار رشتهای EditId دریافت شدهی از طریق آدرس جاری درخواستی، رمزگشایی شده و بجای مقدار فعلی رمزنگاری شدهی آن درج میشود. همین اندازه برای مقدار دهی خودکار پارامتر EditId ذیل در صفحات مرتبط، کفایت میکند:
[Parameter] public string? EditId { set; get; }
یعنی دیگر نیازی نیست تا در صفحات مرتبط، کار رمزگشایی EditId، به صورت دستی انجام شود.
public async Task Sample() { var customers = await GetCustomersFromSomeWhere(); foreach (var customer in customers) { await DoSomethingWithCustomer(customer); } }
public async Task Sample() { var customers = await GetCustomersFromSomeWhere(); await Task.WhenAll(customers.Select(c => DoSomethingWithCustomer(c))); }
public async Task Sample() { var customers = await GetCustomersFromSomeWhere(); await customers.Select(c => Observable.FromAsync(() => DoSomethingWithCustomer(c))).Merge(maxConcurrent: 25); }
public async Task Sample() { var customers = await GetCustomersFromSomeWhere(); ActionBlock<Customer> action = new ActionBlock<Customer>(c => DoSomethingWithCustomer(c), new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 25 }); foreach (var customer in customers) { action.Post(customer); } action.Complete(); await action.Completion; }
public class TokenFactoryService { private readonly JwtBearerOptions _jwtBearerOptions; public TokenFactoryService(IOptionsSnapshot<JwtBearerOptions> jwtBearerOptions) { if (jwtBearerOptions == null) { throw new ArgumentNullException(nameof(jwtBearerOptions)); } _jwtBearerOptions = jwtBearerOptions.Value ?? throw new ArgumentNullException(nameof(jwtBearerOptions)); } // From: https://github.com/dotnet/aspnetcore/blob/a450cb69b5e4549f5515cdb057a68771f56cefd7/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs public bool ValidateJwt(string token) { foreach (var validator in _jwtBearerOptions.SecurityTokenValidators) { try { if (validator.CanReadToken(token)) { validator.ValidateToken(token, _jwtBearerOptions.TokenValidationParameters, out _); } } catch { return false; } } return true; } }
بررسی کارآیی Span در C# 7.2
متدی برای بررسی صحت کد ملی وارد شده
public static bool IsValidIranianNationalCode(string input) { // input has 10 digits that all of them are not equal if (!System.Text.RegularExpressions.Regex.IsMatch(input, @"^(?!(\d)\1{9})\d{10}$")) return false; var check = Convert.ToInt32(input.Substring(9, 1)); var sum = Enumerable.Range(0, 9) .Select(x => Convert.ToInt32(input.Substring(x, 1)) * (10 - x)) .Sum() % 11; return sum < 2 && check == sum || sum >= 2 && check + sum == 11; }
public abstract class ResourceProviderFactory { public abstract IResourceProvider CreateGlobalResourceProvider(string classKey); public abstract IResourceProvider CreateLocalResourceProvider(string virtualPath); }
public interface IResourceProvider { IResourceReader ResourceReader { get; } object GetObject(string resourceKey, CultureInfo culture); }
// Type: System.Web.Compilation.ResXResourceProviderFactory // Assembly: System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a // Assembly location: C:\Windows\Microsoft.NET\assembly\GAC_32\System.Web\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Web.dll using System.Runtime; using System.Web; namespace System.Web.Compilation { internal class ResXResourceProviderFactory : ResourceProviderFactory { [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")] public ResXResourceProviderFactory() { } public override IResourceProvider CreateGlobalResourceProvider(string classKey) { return (IResourceProvider) new GlobalResXResourceProvider(classKey); } public override IResourceProvider CreateLocalResourceProvider(string virtualPath) { return (IResourceProvider) new LocalResXResourceProvider(VirtualPath.Create(virtualPath)); } } }
internal class GlobalResXResourceProvider : BaseResXResourceProvider { private string _classKey; internal GlobalResXResourceProvider(string classKey) { _classKey = classKey; } protected override ResourceManager CreateResourceManager() { string fullClassName = BaseResourcesBuildProvider.DefaultResourcesNamespace + "." + _classKey; // If there is no app resource assembly, return null if (BuildManager.AppResourcesAssembly == null) return null; ResourceManager resourceManager = new ResourceManager(fullClassName, BuildManager.AppResourcesAssembly); resourceManager.IgnoreCase = true; return resourceManager; } public override IResourceReader ResourceReader { get { // App resources don't support implicit resources, so the IResourceReader should never be needed throw new NotSupportedException(); } } }
internal const string DefaultResourcesNamespace = "Resources";
internal class LocalResXResourceProvider : BaseResXResourceProvider { private VirtualPath _virtualPath; internal LocalResXResourceProvider(VirtualPath virtualPath) { _virtualPath = virtualPath; } protected override ResourceManager CreateResourceManager() { ResourceManager resourceManager = null; Assembly pageResAssembly = GetLocalResourceAssembly(); if (pageResAssembly != null) { string fileName = _virtualPath.FileName; resourceManager = new ResourceManager(fileName, pageResAssembly); resourceManager.IgnoreCase = true; } else { throw new InvalidOperationException(SR.GetString(SR.ResourceExpresionBuilder_PageResourceNotFound)); } return resourceManager; } public override IResourceReader ResourceReader { get { // Get the local resource assembly for this page Assembly pageResAssembly = GetLocalResourceAssembly(); if (pageResAssembly == null) return null; // Get the name of the embedded .resource file for this page string resourceFileName = _virtualPath.FileName + ".resources"; // Make it lower case, since GetManifestResourceStream is case sensitive resourceFileName = resourceFileName.ToLower(CultureInfo.InvariantCulture); // Get the resource stream from the resource assembly Stream resourceStream = pageResAssembly.GetManifestResourceStream(resourceFileName); // If this page has no resources, return null if (resourceStream == null) return null; return new ResourceReader(resourceStream); } } [PermissionSet(SecurityAction.Assert, Unrestricted = true)] private Assembly GetLocalResourceAssembly() { // Remove the page file name to get its directory VirtualPath virtualDir = _virtualPath.Parent; // Get the name of the local resource assembly string cacheKey = BuildManager.GetLocalResourcesAssemblyName(virtualDir); BuildResult result = BuildManager.GetBuildResultFromCache(cacheKey); if (result != null) { return ((BuildResultCompiledAssembly)result).ResultAssembly; } return null; } }
نکته: باتوجه به استفاده از عبارات بومیسازی ضمنی در استفاده از ورودیهای منابع محلی، خاصیت ResourceReader در این کلاس نمونهای متناظر برای درخواست جاری از کلاس ResourceReader با استفاده از Stream استخراج شده از اسمبلی یافته شده، تولید میکند.
internal abstract class BaseResXResourceProvider : IResourceProvider { private ResourceManager _resourceManager; ///// IResourceProvider implementation public virtual object GetObject(string resourceKey, CultureInfo culture) { // Attempt to get the resource manager EnsureResourceManager(); // If we couldn't get a resource manager, return null if (_resourceManager == null) return null; if (culture == null) culture = CultureInfo.CurrentUICulture; return _resourceManager.GetObject(resourceKey, culture); } public virtual IResourceReader ResourceReader { get { return null; } } ///// End of IResourceProvider implementation protected abstract ResourceManager CreateResourceManager(); private void EnsureResourceManager() { if (_resourceManager != null) return; _resourceManager = CreateResourceManager(); } }
System.Resources.ResourceManager(string baseName, Assembly assemblyName)
var manager = new System.Resources.ResourceManager("Resources.Resource1", typeof(Resource1).Assembly)
var manager = new System.Resources.ResourceManager("Resources.Resource1", Assembly.LoadFile(@"c:\MyResources\MyGlobalResources.dll"))
public static ResourceManager CreateFileBasedResourceManager(string baseName, string resourceDir, Type usingResourceSet)
resgen d:\MyResources\MyResource.fa.resx
private ResourceManager CreateGlobalResourceManager(string classKey) { var baseName = "Resources." + classKey; var buildManagerType = typeof(BuildManager); var property = buildManagerType.GetProperty("AppResourcesAssembly", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.GetField); var appResourcesAssembly = (Assembly)property.GetValue(null, null); return new ResourceManager(baseName, appResourcesAssembly) { IgnoreCase = true }; }
var manager = CreateGlobalResourceManager("Resource1"); Label1.Text = manager.GetString("String1");
private ResourceManager CreateLocalResourceManager(string virtualPath) { var virtualPathType = typeof(BuildManager).Assembly.GetType("System.Web.VirtualPath", true); var virtualPathInstance = Activator.CreateInstance(virtualPathType, BindingFlags.NonPublic | BindingFlags.Instance, null, new object[] { virtualPath }, CultureInfo.InvariantCulture); var buildResultCompiledAssemblyType = typeof(BuildManager).Assembly.GetType("System.Web.Compilation.BuildResultCompiledAssembly", true); var propertyResultAssembly = buildResultCompiledAssemblyType.GetProperty("ResultAssembly", BindingFlags.NonPublic | BindingFlags.Instance); var methodGetLocalResourcesAssemblyName = typeof(BuildManager).GetMethod("GetLocalResourcesAssemblyName", BindingFlags.NonPublic | BindingFlags.Static); var methodGetBuildResultFromCache = typeof(BuildManager).GetMethod("GetBuildResultFromCache", BindingFlags.NonPublic | BindingFlags.Static, null, new Type[] { typeof(string) }, null); var fileNameProperty = virtualPathType.GetProperty("FileName"); var virtualPathFileName = (string)fileNameProperty.GetValue(virtualPathInstance, null); var parentProperty = virtualPathType.GetProperty("Parent"); var virtualPathParent = parentProperty.GetValue(virtualPathInstance, null); var localResourceAssemblyName = (string)methodGetLocalResourcesAssemblyName.Invoke(null, new object[] { virtualPathParent }); var buildResultFromCache = methodGetBuildResultFromCache.Invoke(null, new object[] { localResourceAssemblyName }); Assembly localResourceAssembly = null; if (buildResultFromCache != null) localResourceAssembly = (Assembly)propertyResultAssembly.GetValue(buildResultFromCache, null); if (localResourceAssembly == null) throw new InvalidOperationException("Unable to find the matching resource file."); return new ResourceManager(virtualPathFileName, localResourceAssembly) { IgnoreCase = true }; }
نحوه استفاده از متد فوق نیز به صورت زیر است:
var manager = CreateLocalResourceManager("~/Default.aspx"); Label1.Text = manager.GetString("Label1.Text");
بررسی ساختار کامپوننت Like
در پوشهی components، ابتدا پوشهی جدید common را ایجاد میکنید. در اینجا تمام کامپوننتهای عمومی برنامه را که منحصر به دومین آن برنامه نیستند، قرار میدهیم. کامپوننتهایی را که اگر آنها را به برنامههای دیگری نیز کپی کردیم، بدون هیچ مشکلی قابلیت استفادهی مجدد را داشته باشند و متصل به سرویسها و زیرساخت برنامهی جاری نباشند. سپس در پوشهی common، فایل جدید src\components\common\like.jsx را ایجاد میکنیم و داخل آن توسط میانبرهای imrc و cc در VSCode، ساختار ابتدایی کامپوننت Like را ایجاد میکنیم.
ساختار کلی این کامپوننت به صورت زیر است:
- ورودی این کامپوننت به این صورت است که در آن مشخص شده آیا یک فیلم، مورد علاقه واقع شده یا خیر؛ مانند خاصیت liked که یک boolean است. اگر true باشد، یک آیکن قلب توپر را نمایش میدهد و برعکس.
- خروجی این کامپوننت نیز به صورت یک رخداد است. هر زمانیکه بر روی آیکن قلب آن کلیک میشود، این کامپوننت یک رخداد onClick را سبب خواهد شد. اکنون هر کامپوننت دیگری که در حال استفادهی از آن است، مطلع شده و خاصیت liked شیء مرتبط را تغییر میدهد.
فعلا ساختار ابتدایی آنرا به رندر یک قلب خالی که توسط قلم آیکنهای font-awesome تامین میشود، تنظیم میکنیم:
import React, { Component } from "react"; class Like extends Component { render() { return <i className="fa fa-heart-o" aria-hidden="true"></i>; } } export default Like;
نمایش ابتدایی کامپوننت Like در جدول لیست فیلمها
فعلا مهم نیست که این کامپوننت کار خاصی را انجام نمیدهد. فقط قصد داریم آنرا در UI برنامه نمایش دهیم. به همین جهت ابتدا یک ستون جدید را مخصوص آن، در جدول فعلی نمایش لیست فیلمها، ایجاد کرده و المان آنرا درج میکنیم. برای این منظور به فایل movies.jsx مراجعه کرده و ابتدا این کامپوننت را import میکنیم:
import Like from "./common/like";
سپس در سرستونهای جدول، یک th جدید را تعریف میکنیم تا ستونی برای درج آن ایجاد شود. همچنین در قسمت بدنهی جدول، پیش از دکمهی حذف، یک td مخصوص درج المان </Like> را اضافه میکنیم:
تا اینجا ستون جدید Like را مشاهده میکنید که کار رندر کامپوننتهای Like در آن انجام شدهاست.
واکنش نشان دادن به ورودیها، در کامپوننت Like
در ادامه باید این کامپوننت بر اساس مقدار Boolean ای که از والد خود دریافت میکند، یک آیکن قلب توپر و یا خالی را نمایش دهد. برای این منظور فعلا در کامپوننت movies، جائیکه المان کامپوننت Like درج شدهاست، ویژگی جدید liked را به مقدار ثابت true تنظیم میکنیم </Like liked={true}> تا بتوان قسمت props این کامپوننت را تکمیل کرد.
در کامپوننت Like، تفاوت بین آیکن قلب توپر و خالی در یک o- در انتهای کلاسهای font-awesome است:
import React, { Component } from "react"; class Like extends Component { render() { let classes = "fa fa-heart"; if (!this.props.liked) { classes += "-o"; } return <i className={classes} aria-hidden="true"></i>; } } export default Like;
پس از این تغییرات اگر برنامه را ذخیره کرده و مجددا در مرورگر بارگذاری کنیم، با توجه به تنظیم liked={true} در کامپوننت movies، ستون like آن با آیکنهای قلب توپر نمایش داده میشود که بیانگر واکنش نشان دادن صحیح به ورودیها در کامپوننت Like است:
پویا سازی مقدار پیشفرض ویژگی liked در کامپوننت movies
برای پویاسازی نمایش مقدار liked در کامپوننت movies، از آنجائیکه هر ردیف بیانگر یک شیء movie است، میتوان به این صورت عمل کرد:
<Like liked={movie.liked} />
const movies = [ { _id: "5b21ca3eeb7f6fbccd471815", title: "Terminator", genre: { _id: "5b21ca3eeb7f6fbccd471818", name: "Action" }, numberInStock: 6, dailyRentalRate: 2.5, publishDate: "2018-01-03T19:04:28.809Z", liked: true },
افزودن رخداد کلیک به کامپوننت Like
برای اینکه کامپوننت Like، رویداد کلیک بر روی آیکن قلب را به والد خود گزارش دهد، ابتدا ویژگی جدید onClick را بر روی تعریف المان آن در کامپوننت movies اضافه میکنیم:
<Like liked={movie.liked} onClick={() => this.handleLike(movie)} />
handleLike = movie => { console.log("handleLike", movie); };
return ( <i className={classes} onClick={this.props.onClick} aria-hidden="true" style={{ cursor: "pointer" }} ></i> );
در اینجا همچنین style این المان نیز جهت نمایش cursor با آیکن pointer، توسط یک شیء از نوع inline style تنظیم شدهاست.
یک نکته: کامپوننت Like تا اینجا یک controlled component است؛ دارای state نیست و همچنین تمام اطلاعات خودش را از طریق props تامین میکند و تنها دارای یک متد render است. بنابراین اگر علاقمند بودید میتوان این کامپوننت را به یک «Stateless Functional Component» که در قسمت 8 معرفی شد نیز تبدیل کرد.
تغییر حالت کامپوننت Like جهت نمایش تغییرات
تا اینجا کامپوننت Like ما میتواند ورودی true/false را به آیکنهای متناظری تبدیل کند. همچنین اگر بر روی این آیکن کلیک شود، آنرا توسط رخدادی به والد خود اطلاع رسانی میکند. اکنون میخواهیم با تکمیل متد handleLike، خاصیت like اشیاء انتخابی (آیکنهایی که بر روی آنها کلیک شدهاند) را از true به false و برعکس تبدیل کرده و سپس UI را نیز به روز رسانی کنیم:
handleLike = movie => { console.log("handleLike", movie); const movies = [...this.state.movies]; // cloning an array const index = movies.indexOf(movie); movies[index] = { ...movies[index] }; // cloning an object movies[index].liked = !movies[index].liked; this.setState({ movies }); };
پس از این تغییرات اگر برنامه را اجرا کنیم، با کلیک بر روی هر آیکن، عکس آن آیکن نمایش داده میشود؛ برای مثال آیکن قلب توپر، تبدیل به آیکن قلب توخالی خواهد شد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-10.zip
- name: Push changes to repo run: | git config http.sslVerify false git config user.name "${{ github.actor }}" git config user.email "${{ github.actor }}@users.noreply.github.com" git remote add publisher "https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" git show-ref git branch --verbose git lfs install git checkout main git add -A git commit -m "Automated publish" git pull --rebase publisher main git push publisher main