اشتراک‌ها
NET 7 Preview 4. منتشر شد

The fourth preview of .NET 7 includes enhancements to observability in the .NET implementation of OpenTelemetry, the addition of properties to track microseconds and nanoseconds in date and time structures, new metrics for caching extensions, performance-boosting “on stack replacement,” APIs to work with .tar archives, and additional features as part of an ongoing effort to improve the performance of and add features to regular expressions in .NET 7.  

NET 7 Preview 4. منتشر شد
مطالب
آزمون‌های یکپارچگی در برنامه‌های ASP.NET Core
تا اینجا دو روش را برای آزمایش کلی یک سیستم Web API به همراه تمام زیر ساخت‌های آن، بررسی کردیم:
- استفاده از Postman برای آزمایش یک برنامه‌ی Web API
- استفاده از strest برای آزمایش یک برنامه‌ی Web API

روش سومی هم برای انجام اینکار وجود دارد که به صورت توکار از زمان ارائه‌ی ASP.NET Core 2.1 به همراه TestServer آزمایشی آن میسر شد. این روش در نگارش 3.1، با تغییر روش تعریف فایل program.cs، جهت سازگاری آن با آزمون‌های یکپارچگی/آزمایش کل سیستم، بهبود یافته‌است که خلاصه‌ای از آن را در این مطلب بررسی می‌کنیم.


آزمون‌های یکپارچگی در ASP.NET Core

آزمون‌های یکپارچگی، برخلاف آزمون‌های واحد که عموما از اشیاء تقلیدی استفاده می‌کنند، دقیقا بر روی همان سیستمی که قرار است به کاربر نهایی ارائه شود، اجرا می‌شوند. به همین جهت تنظیمات اولیه‌ی آن‌ها کمی بیشتر است و همچنین زمان اجرای آن‌ها نیز به علت وابستگی به بانک اطلاعاتی واقعی، فایل سیستم، شبکه و غیره، نسبت به آزمون‌های واحد بیشتر است.

برای ایجاد آزمون‌های یکپارچگی در برنامه‌های ASP.NET Core، حداقل سه مرحله باید طی شوند:
الف) ایجاد یک class library که ارجاعی را به پروژه‌ی اصلی دارد. این پروژه حاوی آزمایش‌های ما خواهد بود.
ب) راه اندازی یک هاست وب آزمایشی برای ارسال درخواست‌ها به آن و دریافت پاسخ‌های نهایی.
ج) استفاده از یک test runner (انواع و اقسام فریم ورک‌های unit testing) برای اجرای آزمایش‌ها


ایجاد یک پروژه‌ی کتابخانه برای هاست و اجرای آزمایش‌های یکپارچگی

فرض کنید می‌خواهیم برای همان پروژه‌ی ایجاد JWTها، آزمایش یکپارچگی بنویسیم. پس از ایجاد یک پروژه‌ی کتابخانه‌ی جدید که قرار است هاست آزمایش‌های ما شود، نیاز است محتوای فایل csproj آن‌را به صورت زیر تغییر داد:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <NoWarn>RCS1090</NoWarn>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\ASPNETCore2JwtAuthentication.WebApp\ASPNETCore2JwtAuthentication.WebApp.csproj" />
  </ItemGroup>
  <ItemGroup>
    <None Include="..\ASPNETCore2JwtAuthentication.WebApp\appsettings.json" CopyToOutputDirectory="PreserveNewest" />
  </ItemGroup>
  <ItemGroup>
    <Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c329}" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="fluentassertions" Version="5.10.3" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.8" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
    <PackageReference Include="MSTest.TestAdapter" Version="2.1.2" />
    <PackageReference Include="MSTest.TestFramework" Version="2.1.2" />
  </ItemGroup>
</Project>
در اینجا، این نکات قابل مشاهده هستند:
1) TargetFramework آن باید به netcoreapp تنظیم شود.
2) باید ارجاع مستقیمی به کل پروژه‌ی نهایی WebApp در آن وجود داشته باشد. چون در ادامه می‌خواهیم فایل Program.cs آن‌را برای راه اندازی یک هاست وب آزمایشی، فراخوانی کنیم.
3) بسته‌ی نیوگتی که کار راه اندازی هاست وب آزمایشی را انجام می‌دهد، Microsoft.AspNetCore.Mvc.Testing نام دارد. این بسته، کار کپی فایل‌های پروژه‌ی اصلی و همچنین تنظیم مسیر پروژه را به این مسیر جدید نیز انجام می‌دهد.
4) روش افزودن بسته‌های MSTest را مشاهده می‌کنید.
5) همچنین جهت ساده‌تر شدن بررسی نتایج آزمون‌های انجام شده می‌توان از fluentassertions نیز استفاده کرد.


راه اندازی هاست وب آزمایشی جهت انجام آزمون‌های واحد

پس از انجام تنظیمات ابتدایی پروژه‌ی آزمون یکپارچگی، نیاز است یک WebApplicationFactory سفارشی را ایجاد کرد:
using ASPNETCore2JwtAuthentication.WebApp;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;

namespace ASPNETCore2JwtAuthentication.IntegrationTests
{
    public class CustomWebApplicationFactory : WebApplicationFactory<Program>
    {
        protected override IWebHostBuilder CreateWebHostBuilder()
        {
            var builder = base.CreateWebHostBuilder();
            builder.ConfigureLogging(logging =>
            {
                //TODO: ...
            });
            return builder;
        }

        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureTestServices(services =>
            {
                // Don't run `IHostedService`s when running as a test
                services.RemoveAll(typeof(IHostedService));
            });
        }
    }
}
در این تعریف، Program در <WebApplicationFactory<Program، دقیقا به همان کلاس Program برنامه‌ی اصلی وب اشاره می‌کند. به همین جهت امضای این کلاس در نگارش 3.1 تغییر کرده‌است تا با WebApplicationFactory بسته‌ی Microsoft.AspNetCore.Mvc.Testing هماهنگ شود.
در ادامه روش سفارشی سازی WebApplicationFactory  را مشاهده می‌کنید. برای مثال اگر خواستید سرویس‌ها و تنظیمات پیش‌فرض برنامه‌ی اصلی را تغییر دهید می‌توانید متد CreateWebHostBuilder را بازنویسی کنید و یا اگر خواستید سرویس جدیدی را اضافه و یا حذف کنید، می‌توان متد ConfigureWebHost را بازنویسی کرد.


استفاده از WebApplicationFactory سفارشی، جهت ایجاد یک HttpClient

هدف اصلی از ایجاد CustomWebApplicationFactory نه فقط راه اندازی یک هاست وب سفارشی است، بلکه توسط متد CreateClient آن می‌توان به یک HttpClient دسترسی یافت که قابلیت ارسال اطلاعات را به برنامه‌ی وبی که در پشت صحنه راه اندازی می‌شود، دارا است. کار CustomWebApplicationFactory شبیه به راه اندازی dotnet run در پشت صحنه‌است. در اینجا دیگر نیازی نیست تا اینکار را به صورت دستی انجام داد. به همین جهت چون برنامه‌ی وب اصلی به نحو متداولی در پشت صحنه اجرا می‌شود، عموما راه اندازی آن که شامل تنظیمات اولیه و یا حتی ایجاد بانک اطلاعاتی است، کمی کند است و اگر قرار باشد هربار اینکار صورت گیرد، به آزمون‌های بسیار کندی خواهیم رسید. به همین جهت می‌توان یک کلاس singleton را برای مدیریت تک وهله‌ی نهایی HttpClient آن به صورت زیر ایجاد کرد:
using System;
using System.Threading;
using System.Net.Http;

namespace ASPNETCore2JwtAuthentication.IntegrationTests
{
    public static class TestsHttpClient
    {
        private static readonly Lazy<HttpClient> _serviceProviderBuilder =
                new Lazy<HttpClient>(getHttpClient, LazyThreadSafetyMode.ExecutionAndPublication);

        /// <summary>
        /// A lazy loaded thread-safe singleton
        /// </summary>
        public static HttpClient Instance { get; } = _serviceProviderBuilder.Value;

        private static HttpClient getHttpClient()
        {
            var services = new CustomWebApplicationFactory();
            return services.CreateClient(); //NOTE: This action is very time consuming, so it should be defined as a singleton.
        }
    }
}
مزیت کار با این کلاس، عدم راه اندازی مجدد برنامه به ازای هر آزمون انجام شده‌است. چون به ازای هر آزمونی که خواهیم نوشت، نیاز است به HttpClient دسترسی داشته باشیم.


نوشتن اولین آزمون یکپارچگی

پس از تنظیم هاست وب آزمایشی و ایجاد یک HttpClient از پیش تنظیم شده که به آن اشاره می‌کند، اکنون می‌توان اولین آزمون یکپارچگی را به صورت زیر نوشت:
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace ASPNETCore2JwtAuthentication.IntegrationTests
{
    [TestClass]
    public class JwtTests
    {
        [TestMethod]
        public async Task TestLoginWorks()
        {
            // Arrange
            var client = TestsHttpClient.Instance;

            // Act
            var token = await doLoginAsync(client);

            // Assert
            token.Should().NotBeNull();
            token.AccessToken.Should().NotBeNullOrEmpty();
            token.RefreshToken.Should().NotBeNullOrEmpty();
        }

        [TestMethod]
        public async Task TestCallProtectedApiWorks()
        {
            // Arrange
            var client = TestsHttpClient.Instance;

            // Act
            var token = await doLoginAsync(client);

            // Assert
            token.Should().NotBeNull();
            token.AccessToken.Should().NotBeNullOrEmpty();
            token.RefreshToken.Should().NotBeNullOrEmpty();

            // Act
            const string protectedApiUrl = "/api/MyProtectedApi";
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken);
            var response = await client.GetAsync(protectedApiUrl);
            response.EnsureSuccessStatusCode();

            // Assert
            var responseString = await response.Content.ReadAsStringAsync();
            responseString.Should().NotBeNullOrEmpty();
            var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
            var apiResponse = JsonSerializer.Deserialize<MyProtectedApiResponse>(responseString, options);
            apiResponse.Title.Should().NotBeNullOrEmpty();
            apiResponse.Title.Should().Be("Hello from My Protected Controller! [Authorize]");
        }

        private static async Task<Token> doLoginAsync(HttpClient client)
        {
            const string loginUrl = "/api/account/login";
            var user = new { Username = "Vahid", Password = "1234" };
            var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Post, loginUrl)
            {
                Content = new StringContent(JsonSerializer.Serialize(user), Encoding.UTF8, "application/json")
            });
            response.EnsureSuccessStatusCode();
            var responseString = await response.Content.ReadAsStringAsync();
            responseString.Should().NotBeNullOrEmpty();
            return JsonSerializer.Deserialize<Token>(responseString);
        }
    }
}
توضیحات:
- در هر آزمونی نیاز است در ابتدا به TestsHttpClient.Instance، که همان HttpClient ساخته شده‌ی توسط CustomWebApplicationFactory است، دسترسی یافت و همانطور که عنوان شد، دسترسی به وهله‌ای از HttpClient که به هاست وب آزمایشی برنامه‌ی اصلی اشاره می‌کند، عموما بسیار زمانبراست و برای مثال در دو آزمایش نوشته شده‌ی در اینجا اگر قرا باشد هربار اینکار از صفر انجام شود، زمان به اتمام رسیدن این آزمایش‌ها بسیار طولانی خواهد شد. به همین جهت طول عمر TestsHttpClient را singleton تعریف کردیم تا فقط یکبار کار برپایی وب سرور آزمایشی در پشت صحنه انجام شود.
- سپس مابقی کار، همان روش استاندارد کار با HttpClient است. در ابتدا درخواستی را به سمت سرور آزمایشی که در پشت صحنه در حال اجرا است، ارسال می‌کنیم. چون HttpClient دریافتی توسط  CustomWebApplicationFactory تنظیم شده‌است، دیگر نیازی به ذکر آدرس پایه‌ی وب سایت مانند https://localhost:5001 نیست و آدرس‌های ذکر شده‌ی در اینجا، نسبی هستند. سپس محتوای Response دریافتی از سرور را جهت تکمیل آزمایشات، بررسی خواهیم کرد.


یک نکته: اگر OpenAPI را در برنامه‌های Web API فعال کنید، می‌توان با استفاده از ابزارهای تولید کد، کدهای مرتبط با HttpClient را نیز به صورت خودکار تولید و سپس از آن‌ها در اینجا استفاده کرد.


اجرای آزمون‌های یکپارچگی نوشته شده

چون ظاهر این آزمون‌ها با آزمون‌های واحد MSTest یا هر فریم ورک مشابه دیگری یکسان است، می‌توان از امکانات IDEها برای اجرای آن‌ها استفاده کرد و یا حتی می‌توان دستور dotnet test را نیز در ریشه‌ی این پروژه‌ی جدید برای اجرای تمام آزمون‌های نوشته شده، اجرا کرد:



کدهای کامل این مطلب را در اینجا می‌توانید مشاهده کنید.
مطالب
آموزش Cache در ASP.NET Core - (قسمت اول : مفاهیم اولیه)
امروزه در وب‌سایت‌های شخصی و تجاری، یکی از مهم‌ترین پارامتر‌ها، سرعت پاسخگویی درخواست‌ها به وب‌سایت است. طبق آمار، کاربران آنلاین کنونی که ما با آن‌ها طرفیم، سطح تحملشان به سه ثانیه در یک صفحه میرسد؛ پس ما باید بتوانیم سرعت وب‌سایت‌های خودمان را تا حد ممکن بهبود بخشیم. از طرفی پارامتر سرعت، روی سئو گوگل هم تاثیر بسزایی دارد و Ranking وب‌سایت شمارا تا حد زیادی افزایش می‌دهد. قطعا همه می‌دانید که سرعت وبسایت و برنامه چقدر مهم هست؛ پس زیاده گویی نمی‌کنیم و می‌رویم سراغ اصل مطلب.
یکی از کارهایی که میتوانیم برای افزایش سرعت برنامه انجام دهیم، استفاده از Cache هست. بطور خیلی ساده، Cache یعنی قرار دادن دیتای پرکاربرد، در یک حافظه‌ی نزدیک‌تر از دیتابیس که هروقت به آن نیاز داشتیم، به آن دسترسی سریعی داشته باشیم و سرعت واکشی اطلاعات، از سرعتی که دیتابیس به ما می‌دهد، بیشتر باشد تا درخواست‌های ما با پاسخ سریع‌تری همراه شوند.
این حافظه، Ram هست و عمل Caching به اینصورت خواهد بود که هر وقت دیتای مورد نظر یکبار از دیتابیس واکشی شود، از دفعات بعد، آن دیتا را در Ram ذخیره میکند و برای درخواست‌های بعدی به دیتابیس Query نمیزند و دیتای مورد نیازش را از Ram میگیرد.
این امر در کنار مزایایی که دارد ، حساسیت بالایی هم بهمراه خواهد داشت؛ چرا که حافظه مورد استفاده Ram، یک حافظه محدود هست همچنین میتواند برای هر سخت افزاری متفاوت باشد. پس پیاده سازی این سیستم نیاز به دو دو تا چهارتا و ساختار درست دارد؛ در غیر اینصورت Cache کردن دیتای غلط میتواند به تنهایی وب‌سایتتان را Down کند؛ پس خیلی باید به این موضوع دقت داشت.

چه زمانی بهتر است از کش استفاده کنیم؟
  • وقتی دیتایی داریم که به تکرار از آن در برنامه استفاده میکنیم.
  • وقتی بعد از گرفتن دیتایی از دیتابیس، محاسباتی بر روی آن انجام میدهیم و پاسخ نهایی محاسبه را به کاربر نمایش میدهیم، میتوانیم یکبار پاسخ را کش کنیم تا از محاسبه‌ی هر باره‌ی آن جلوگیری شود.

آیا تمام اطلاعات را میتوان کش کرد؟
خیر.
  • سخت افزاری که برای کش استفاده میکنیم یعنی Ram، بسیار گران‌تر از دیتابیس برای ما تمام میشود؛ چرا که محدود است.
  • اگر همه دیتاهارا کش کنید، عمل سرچ میان آن زمان بیشتری خواهد برد.
پس اکنون میدانید که میتوانیم داده‌های بی نهایتی را در دیتابیس ذخیره کنیم و فقط با ارزش‌ترین‌ها و پر مصرف‌ترین هارا در حافظه کش، ذخیره میکنیم.

عملیات Cache در Asp.Net Core توسط اینترفیس‌های IMemoryCache و IDistributedCache مدیریت میشود و میتوانید با تزریق این اینترفیس‌ها براحتی از متدهایشان استفاده کنید؛ اما قبل از استفاده لازم است با عملکرد هر یک از آن‌ها آشنا شویم.

روش اول : In-memory Caching (Local Caching)
معمول‌ترین و ابتدایی‌ترین روش برای کش کردن اطلاعات، روش Local Caching و بصورت In-Memory است که اطلاعات را در حافظه Ram همان سروری که برنامه در آن اجرا میشود، کش میکند.

این روش تا زمانیکه برنامه‌ی ما برای اجرا شدن، تنها از یک سرور استفاده کند، بهترین انتخاب خواهد بود؛ چرا که به دلیل نزدیک بودن، سریع‌ترین بازخورد را نیز به درخواست‌ها ارائه میدهد.


اما شرایطی را فرض کنید که برنامه از چندین سرور برای اجرا شدن استفاده میکند و به طبع هر سرور درخواست‌های خودش را داراست که ما باید برای هر یک بصورت جداگانه‌ای یک کش In-Memory را در حافظه Ram هرکدام ایجاد کنیم. 

فرض کنید دیتای ما 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 باشد. بخشی از دیتا در Server 1 کش میشود (1 , 3 , 5 , 9) و بخشی دیگر در Server 2 کش خواهد شد (2 , 4 , 6 ,7 , 8 , 10).


در اینجا مشکلات و ضعف هایی به وجود خواهد آمد :


  • برای مثال اگر Server 1 به هر دلیلی از بین برود یا Down شود، اطلاعات کش درون آن نیز پاک خواهد شد و بعد از راه اندازی باید همه آن را دوباره از دیتابیس بخواند.
  • هر کدام از سرور‌ها کش‌های جدایی دارند و باهم Sync نیستند و امکان وجود یک داده‌ی حیاتی در یکی و عدم وجود آن در دیگری، بالاست. فرض کنید برنامه برای هر درخواست، نیاز به اطلاعات دسترسی کاربری را دارد. دسترسی‌های کاربر، در Server 1 کش شده، اما در Server 2 موجود نیست. در Server 2 به دلیل عدم وجود این کش، برنامه برای درخواست‌های معمول خود و چک کردن دسترسی کاربر یا باید هربار به دیتابیس درخواستی را ارسال کند که این برخلاف خواسته ماست و یا باید دیتای مربوط به دسترسی‌های کاربر را بعد از یکبار درخواست، از دیتابیس در خودش کش کند که این‌هم دوباره کاری به حساب میاید و دوبار کش کردن یک دیتا، امر مطلوبی نخواهد بود.

روش هایی وجود دارد که بتوان از سیستم Local Caching در حالت چند سروری هم استفاده کرد و این مشکلات را از بین برد، اما روش استاندارد در حالت چند سروری، استفاده از Distributed Cache‌ها است.


روش دوم : Distributed Caching

در این روش برنامه‌ی ما برای اجرا شدن از چندین سرور شبکه شده به هم، در حال استفاده هست و Cache برنامه، توسط سرورها به اشتراک گذاشته شده. 

در این حالت سرور‌های ما از یک کش عمومی استفاده میکنند که مزایای آن شامل :

■ درخواست‌ها به چندین سرور مختلف از هم ارسال شده، اما دیتای کش بصورت منسجم در هریک وجود خواهد داشت.

■ با خراب شدن یا Down شدن یک سرور، کش موجود در سرور‌های دیگر پاک نمیشود و کماکان قابل استفاده است.

■ به حافظه Ram یک سرور محدود نیست و مشکلات زیادی همچون کمبود سخت افزاری و محدودیت‌های حافظه‌ی Ram را تا حد معقولی کاهش میدهد.


طریقه استفاده از Cache در Asp.Net Core :

  • بر خلاف ASP.NET web forms و ASP.NET MVC در نسخه‌های Core به بعد، Cache بصورت از پیش ثبت شده، وجود ندارد. کش در Asp.Net Core با فراخوانی سرویس‌های مربوطه‌ی آن قابل استفاده است و نیاز است قبل از استفاده، سرویس آن را در کلاس Startup برنامه فراخوانی کنید. 
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddMemoryCache();
}

  • اینترفیس IMemoryCache از سیستم تزریق وابستگی‌ها در Core استفاده میکند و برای استفاده از اینترفیس آن، پس از اضافه کردن MemoryCache به Startup ، باید در کنترلر، عمل تزریق وابستگی (DI) را انجام دهید؛ سپس متد‌های مورد نیاز برای کش، در دسترس خواهد بود. 
public class HomeController : Controller
{
    private readonly IMemoryCache  _cache;
    public HomeController(IMemoryCache  cache)
    {
        _cache = cache;
    }
    ....
}

  • برای ذخیره‌ی کش میتوانید از متد Set موجود در این اینترفیس استفاده کنید. 
public IActionResult Set()
{
  _cache.Set("CacheKey", data , TimeSpan.FromDays(1));
  return View();
}

در پارامتر اول این متد (CacheKey)، یک کلید، برای اطلاعاتی که میخواهیم کش کنیم قرار میدهیم. دقت کنید که این کلید، شناسه‌ی دیتای شماست و باید طوری آن را در نظر گرفت که با صدا زدن این کلید از سرویس کش، همان دیتای مورد نظر را برگشت دهد (هر Object دیتا، باید کلید Unique خود را داشته باشد).


در پارامتر دوم، دیتای مورد نظر را که میخواهیم کش کنیم، به متد میدهیم و در پارامتر سوم نیز زمان اعتبار و تاریخ انقضای دیتای کش شده را وارد میکنیم؛ به این معنا که دیتای کش شده، بعد از مدت زمان گفته شده، از حافظه کش(Ram) حذف شود و برای دسترسی دوباره و کش کردن دوباره اطلاعات، نیاز به خواندن مجدد از دیتابیس باشد.


  • برای دسترسی به اطلاعات کش شده میتوانید از متد Get استفاده کنید. 
public IActionResult Get()
{
  string data = _cache.Get("CacheKey");
  return View(data);
}

تنها پارامتر ورودی این متد، کلید از قبل نسبت داده شده به اطلاعات کش هست که با استفاده از یکسان بودن کلید در ورودی این متد و کلید Set شده از قبل در حافظه Ram، دیتا مربوط به آن را برگشت میدهد.


  • متد TryGetValue برای بررسی وجود یا عدم وجود یک کلید در حافظه کش هست و یک Boolean را خروجی میدهد. 
public IActionResult Set()
  {
        DateTime data;
       // Look for cache key.
       if (!_cache.TryGetValue( "CacheKey" , out data))
       {
              // Key not in cache, so get data.
              data= DateTime.Now;

            // Save data in cache and set the relative expiration time to one day
             _cache.Set( "CacheKey" , data, TimeSpan.FromDays(1));
        }
        return View(data);
  }

این متد ابتدا بررسی میکند که کلیدی با نام "CacheKey" وجود دارد یا خیر؟ در صورت عدم وجود، آن را میسازد و دیتای مورد نظر را به آن نسبت میدهد.


  • با استفاده از متد GetOrCreate میتوانید کار متد‌های Get و Set را باهم انجام دهید و در یک متد، وجود یا عدم وجود کش را بررسی و در صورت وجود، مقداری را return و در صورت عدم وجود، ابتدا ایجاد کش و بعد return مقدار کش شده را انجام دهید. 
 public IActionResult GetOrCreate()
{
         var data = _cache.GetOrCreate( "CacheKey" , entry =>
         entry.SlidingExpiration = TimeSpan.FromSeconds(3);
         return View(data);
});
    return View(data);
}

  • برای مدیریت حافظه‌ی Ram شما باید یک Expiration Time را برای کش‌های خود مشخص کنید؛ تا هم حافظه Ram را حجیم نکنید و هم در هر بازه‌ی زمانی، دیتای بروز را از دیتابیس بخوانید. برای این کار option‌های متفاوتی از جمله absolute expiration و sliding expiration وجود دارند.

در اینجا absolute expiration به این معنا است که یک زمان قطعی را برای منقضی شدن کش‌ها مشخص میکند؛ به عبارتی میگوییم کش با کلید فلان، در تاریخ و ساعت فلان حذف شود. اما در sliding expiration یک بازه زمانی برای منقضی شدن کش‌ها مشخص میکنیم؛ یعنی میگوییم بعد از گذشت فلان دقیقه از ایجاد کش، آن را حذف کن و اگر در طی این مدت مجددا خوانده شد، طول مدت زمان آن تمدید خواهد شد.

این تنظیمات را میتوانید در قالب یک option زمان Set کردن یک کش، به آن بدهید. 

MemoryCacheEntryOptions options = new MemoryCacheEntryOptions();
options.AbsoluteExpiration = DateTime.Now.AddMinutes(1);
options.SlidingExpiration = TimeSpan.FromMinutes(1);
_cache.Set("CacheKey", data, options );

در مثال بالا هردو option اضافه شده یک کار را انجام میدهند؛ با این تفاوت که absolute expiration تاریخ now را گرفته و یک دقیقه بعد را به آن اضافه کرده و تاریخ انقضای کش را با آن تاریخ set میکند. اما sliding expiration از حالا بمدت یک دقیقه اعتبار دارد.


  • یکی از روش‌های مدیریت حافظه Ram در کش‌ها این است که برای حذف شدن کش‌ها از حافظه، اولویت بندی‌هایی را تعریف کنید. اولویت‌ها در چهار سطح قابل دسترسی است: 

  1.  NeverRemove = 3
  2.  High = 2
  3. Normal = 1
  4.  Low = 0 

این اولویت بندی‌ها زمانی کاربرد خواهند داشت که حافظه اختصاصی Ram، برای کش‌ها پر شده باشد و در این حالت سیستم کشینگ بصورت خودمختار، کش‌های با الویت پایین را از حافظه حذف میکند و کش‌های با الویت بیشتر، در حافظه باقی میمانند. این با شماست که الویت را برای دیتا‌های خود تعیین کنید؛ پس باید با دقت و فکر شده این کار را انجام دهید. 

MemoryCacheEntryOptions options = new MemoryCacheEntryOptions();
// Low / Normal / High / NeverRemove
options.Priority = CacheItemPriority.High;
cache.Set("CacheKey", data, options);

به این صورت میتوانید الویت‌های متفاوت را در قالب option به کش‌های خود اختصاص دهید. 

در این مقاله سعی شد مفاهیم اولیه Cache، طوری گفته شود، تا برای افرادی که میخواهند به تازگی این سیستم را بیاموزند و در پروژه‌های خود استفاده کنند، کاربردی باشد و درک نسبی را نسبت به مزایا و محدودیت‌های این سیستم بدست آورند.


در قسمت دوم همین مقاله بطور تخصصی‌تر به این مبحث میپردازیم و یک پکیج آماده را معرفی میکنیم که خیلی راحت‌تر و اصولی‌تر کش را برای ما پیاده سازی میکند.

اشتراک‌ها
NET. چه چیزی است؟ پلتفرم یا فریم ورک؟

You'll often hear the terms framework and platform used interchangeably. This can be very confusing. You'll hear that .NET Core is a platform. Then you'll read that .NET Core is cross-platform. Next, you'll learn that .NET Core is a framework. And it doesn't help that framework is part of the name of the original full .NET Framework.

In this post, I explain the difference between frameworks and platforms and explain why sometimes you'll see .NET being referred to as a framework and other times see it referred to as a platform. 

NET. چه چیزی است؟ پلتفرم یا فریم ورک؟
مطالب
پیاده سازی سیاست‌های دسترسی پویای سمت سرور و کلاینت در برنامه‌های Blazor WASM
فرض کنید در حال توسعه‌ی یک برنامه‌ی Blazor WASM هاست شده هستید و می‌خواهید که نیازی نباشد تا به ازای هر صفحه‌ای که به برنامه اضافه می‌کنید، یکبار منوی آن‌را به روز رسانی کنید و نمایش منو به صورت خودکار توسط برنامه صورت گیرد. همچنین در این حالت نیاز است در قسمت مدیریتی برنامه، بتوان به صورت پویا، به ازای هر کاربری، مشخص کرد که به کدامیک از صفحات برنامه دسترسی دارد و یا خیر و به علاوه اگر به صفحاتی دسترسی ندارد، مشخصات این صفحه، در منوی پویا برنامه ظاهر نشود و همچنین با تایپ آدرس آن در نوار آدرس مرورگر نیز قابل دسترسی نباشد. امن سازی پویای سمت کلاینت، یک قسمت از پروژه‌است؛ قسمت دیگر چنین پروژه‌ای، لیست کردن اکشن متدهای API سمت سرور پروژه و انتساب دسترسی‌های پویایی به این اکشن متدها، به کاربران مختلف برنامه‌است.


دریافت کدهای کامل این پروژه

کدهای کامل پروژه‌ای که نیازمندی‌های فوق را پیاده سازی می‌کند، در اینجا می‌توانید مشاهده و دریافت کنید. در این مطلب از قرار دادن مستقیم این کدها صرفنظر شده و سعی خواهد شد بجای آن، نقشه‌ی ذهنی درک کدهای آن توضیح داده شود.


پیشنیازها

در پروژه‌ی فوق برای شروع به کار، از اطلاعات مطرح شده‌ی در سلسله مطالب زیر استفاده شده‌است:

- «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity»
- پیاده سازی اعتبارسنجی کاربران در برنامه‌های Blazor WASM؛ قسمت‌های 31 تا 33 .
- «غنی سازی کامپایلر C# 9.0 با افزونه‌ها»
- «مدیریت مرکزی شماره نگارش‌های بسته‌های NuGet در پروژه‌های NET Core.»
- «کاهش تعداد بار تعریف using‌ها در C# 10.0 و NET 6.0.»
- «روش یافتن لیست تمام کنترلرها و اکشن متدهای یک برنامه‌ی ASP.NET Core»


  نیاز به علامتگذاری صفحات امن شده‌ی سمت کلاینت، جهت نمایش خودکار آن‌ها 

صفحات امن سازی شده‌ی سمت کلاینت، با ویژگی Authorize مشخص می‌شوند. بنابراین قید آن الزامی است، تا صرفا جهت کاربران اعتبارسنجی شده، قابل دسترسی شوند. در اینجا می‌توان یک نمونه‌ی سفارشی سازی شده‌ی ویژگی Authorize را به نام ProtectedPageAttribute نیز مورد استفاده قرار داد. این ویژگی از AuthorizeAttribute ارث‌بری کرده و دقیقا مانند آن عمل می‌کند؛ اما این اضافات را نیز به همراه دارد:
- به همراه یک Policy از پیش تعیین شده به نام CustomPolicies.DynamicClientPermission است تا توسط قسمت‌های بررسی سطوح دسترسی پویا و همچنین منوساز برنامه، یافت شده و مورد استفاده قرار گیرد.
- به همراه خواص اضافه‌تری مانند GroupName و Title نیز هست. GroupName نام سرتیتر منوی dropdown نمایش داده شده‌ی در منوی اصلی برنامه‌است و Title همان عنوان صفحه که در این منو نمایش داده می‌شود. اگر صفحه‌ی محافظت شده‌ای به همراه GroupName نباشد، یعنی باید به صورت یک آیتم اصلی نمایش داده شود. همچنین در اینجا یک سری Order هم درنظر گرفته شده‌اند تا بتوان ترتیب نمایش صفحات را نیز به دلخواه تغییر داد.


نمونه‌ای از استفاده‌ی از ویژگی فوق را در مسیر src\Client\Pages\Feature1 می‌توانید مشاهده کنید که خلاصه‌ی آن به صورت زیر است:
 @attribute [ProtectedPage(GroupName = "Feature 1", Title = "Page 1", GlyphIcon = "bi bi-dot", GroupOrder = 1, ItemOrder = 1)]

ویژگی ProtectedPage را معادل یک ویژگی Authorize سفارشی، به همراه چند خاصیت بیشتر، جهت منوساز پویای برنامه درنظر بگیرید.


نیاز به لیست کردن صفحات علامتگذاری شده‌ی با ویژگی ProtectedPage

پس از اینکه صفحات مختلف برنامه را توسط ویژگی ProtectedPage علامتگذاری کردیم، اکنون نوبت به لیست کردن پویای آن‌ها است. اینکار توسط سرویس ProtectedPagesProvider صورت می‌گیرد. این سرویس با استفاده از Reflection، ابتدا تمام IComponentها یا همان کامپوننت‌های تعریف شده‌ی در برنامه را از اسمبلی جاری استخراج می‌کند. بنابراین اگر نیاز دارید که این جستجو در چندین اسمبلی صورت گیرد، فقط کافی است ابتدای این کدها را تغییر دهید. پس از یافت شدن IComponent ها، فقط آن‌هایی که دارای RouteAttribute هستند، پردازش می‌شوند؛ یعنی کامپوننت‌هایی که به همراه مسیریابی هستند. پس از آن بررسی می‌شود که آیا این کامپوننت دارای ProtectedPageAttribute هست یا خیر؟ اگر بله، این کامپوننت در لیست نهایی درج خواهد شد.


نیاز به یک منوساز پویا جهت نمایش خودکار صفحات امن سازی شده‌ی با ویژگی ProtectedPage

اکنون که لیست صفحات امن سازی شده‌ی توسط ویژگی ProtectedPage را در اختیار داریم، می‌توانیم آن‌ها را توسط کامپوننت سفارشی NavBarDynamicMenus به صورت خودکار نمایش دهیم. این کامپوننت لیست صفحات را توسط کامپوننت NavBarDropdownMenu نمایش می‌دهد.


تهیه‌ی جداول و سرویس‌های ثبت دسترسی‌های پویای سمت کلاینت


جداول و فیلدهای مورد استفاده‌ی در این پروژه را در تصویر فوق ملاحظه می‌کنید که در پوشه‌ی src\Server\Entities نیز قابل دسترسی هستند. در این برنامه نیاز به ذخیره سازی اطلاعات نقش‌های کاربران مانند نقش Admin، ذخیره سازی سطوح دسترسی پویای سمت کلاینت و همچنین سمت سرور است. بنابراین بجای اینکه به ازای هر کدام، یک جدول جداگانه را تعریف کنیم، می‌توان از همان طراحی ASP.NET Core Identity مایکروسافت با استفاده از جدول UserClaimها ایده گرفت. یعنی هر کدام از این موارد، یک Claim خواهند شد:


در اینجا نقش‌ها با Claim استانداردی به نام http://schemas.microsoft.com/ws/2008/06/identity/claims/role که توسط خود مایکروسافت نامگذاری شده و سیستم‌های اعتبارسنجی آن بر همین اساس کار می‌کنند، قابل مشاهده‌است. همچنین دو Claim سفارشی دیگر ::DynamicClientPermission:: برای ذخیره سازی اطلاعات صفحات محافظت شده‌ی سمت کلاینت و ::DynamicServerPermission::  جهت ذخیره سازی اطلاعات اکشن متدهای محافظت شده‌ی سمت سرور نیز تعریف شده‌اند. رابطه‌ای این اطلاعات با جدول کاربران، many-to-many است.


به این ترتیب است که مشخص می‌شود کدام کاربر، به چه claimهایی دسترسی دارد.

برای کار با این جداول، سه سرویس UsersService، UserClaimsService و UserTokensService پیش بینی شده‌اند. UserTokens اطلاعات توکن‌های صادر شده‌ی توسط برنامه را ذخیره می‌کند و توسط آن می‌توان logout سمت سرور را پیاده سازی کرد؛ از این جهت که JWTها متکی به خود هستند و تا زمانیکه منقضی نشوند، در سمت سرور پردازش خواهند شد، نیاز است بتوان به نحوی اگر کاربری غیرفعال شد، از آن ثانیه به بعد، توکن‌های او در سمت سرور پردازش نشوند که به این نکات در مطلب «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» پیشتر پرداخته شده‌است.
اطلاعات این سرویس‌ها توسط اکشن متدهای UsersAccountManagerController، در اختیار برنامه‌ی کلاینت قرار می‌گیرند.


نیاز به قسمت مدیریتی ثبت دسترسی‌های پویای سمت کلاینت و سرور

قبل از اینکه بتوان قسمت‌های مختلف کامپوننت NavBarDynamicMenus را توضیح داد، نیاز است ابتدا یک قسمت مدیریتی را جهت استفاده‌ی از لیست ProtectedPageها نیز تهیه کرد:


در این برنامه، کامپوننت src\Client\Pages\Identity\UsersManager.razor کار لیست کردن کاربران، که اطلاعات آن‌را از کنترلر UsersAccountManagerController دریافت می‌کند، انجام می‌دهد. در مقابل نام هر کاربر، دو دکمه‌ی ثبت اطلاعات پویای دسترسی‌های سمت کلاینت و سمت سرور وجود دارد. سمت کلاینت آن توسط کامپوننت UserClientSidePermissions.razor مدیریت می‌شود و سمت سرور آن توسط UserServerSidePermissions.razor.
کامپوننت UserClientSidePermissions.razor، همان لیست صفحات محافظت شده‌ی توسط ویژگی ProtectedPage را به صورت گروه بندی شده و به همراه یک سری chekmark، ارائه می‌دهد. اگر در اینجا صفحه‌ای انتخاب شد، اطلاعات آن به سمت سرور ارسال می‌شود تا توسط Claim ای به نام ::DynamicClientPermission:: به کاربر انتخابی انتساب داده شود.


شبیه به همین عملکرد در مورد دسترسی سمت سرور نیز برقرار است. UserServerSidePermissions.razor، لیست اکشن متدهای محافظت شده را از کنترلر DynamicPermissionsManagerController دریافت کرده و نمایش می‌دهد. این اطلاعات توسط سرویس ApiActionsDiscoveryService جمع آوری می‌شود. همچنین این اکشن متدهای ویژه نیز باید با ویژگی Authorize(Policy = CustomPolicies.DynamicServerPermission) مزین شده باشند که نمونه مثال آن‌ها را در مسیر src\Server\Controllers\Tests می‌توانید مشاهده کنید. اگر در سمت کلاینت و قسمت مدیریتی آن، اکشن متدی جهت کاربر خاصی انتخاب شد، اطلاعات آن ذیل Claimای به نام ::DynamicServerPermission::  به کاربر انتخابی انتساب داده می‌شود.



بازگشت اطلاعات پویای دسترسی‌های سمت کلاینت از API

تا اینجا کامپوننت‌های امن سازی شده‌ی سمت کلاینت و اکشن متدهای امن سازی شده‌ی سمت سرور را توسط صفحات مدیریتی برنامه، به کاربران مدنظر خود انتساب دادیم و توسط سرویس‌های سمت سرور، اطلاعات آن‌ها را در بانک اطلاعاتی ذخیره کردیم. اکنون نوبت به استفاده‌ی از claims تعریف شده و مرتبط با هر کاربر است. پس از یک لاگین موفقیت آمیز توسط UsersAccountManagerController، سه توکن به سمت کاربر ارسال می‌شوند:
- توکن دسترسی: اطلاعات اعتبارسنجی کاربر به همراه نام و نقش‌های او در این توکن وجود دارند.
- توکن به روز رسانی: هدف از آن، دریافت یک توکن دسترسی جدید، بدون نیاز به لاگین مجدد است. به این ترتیب کاربر مدام نیاز به لاگین مجدد نخواهد داشت و تا زمانیکه refresh token منقضی نشده‌است، برنامه می‌تواند از آن جهت دریافت یک access token جدید استفاده کند.
- توکن سطوح دسترسی پویای سمت کلاینت: در اینجا لیست ::DynamicClientPermission::ها به صورت یک توکن مجزا به سمت کاربر ارسال می‌شود. این اطلاعات به توکن دسترسی اضافه نشده‌اند تا بی‌جهت حجم آن اضافه نشود؛ از این جهت که نیازی نیست تا به ازای هر درخواست HTTP به سمت سرور، این لیست حجیم claims پویای سمت کلاینت نیز به سمت سرور ارسال شود. چون سمت سرور از claims دیگری به نام ::DynamicServerPermission:: استفاده می‌کند.


اگر دقت کنید، هم refresh-token و هم DynamicPermissions هر دو به صورت JWT ارسال شده‌اند. می‌شد هر دو را به صورت plain و ساده نیز ارسال کرد. اما مزیت refresh token ارسال شده‌ی به صورت JWT، انجام اعتبارسنجی خودکار سمت سرور اطلاعات آن است که دستکاری سمت کلاینت آن‌را مشکل می‌کند.
این سه توکن توسط سرویس BearerTokensStore، در برنامه‌ی سمت کلاینت ذخیره و بازیابی می‌شوند. توکن دسترسی یا همان access token، توسط ClientHttpInterceptorService به صورت خودکار به تمام درخواست‌های ارسالی توسط برنامه الصاق خواهد شد.


مدیریت خودکار اجرای Refresh Token در برنامه‌های Blazor WASM

دریافت refresh token از سمت سرور تنها قسمتی از مدیریت دریافت مجدد یک access token معتبر است. قسمت مهم آن شامل دو مرحله‌ی زیر است:
الف) اگر خطاهای سمت سرور 401 و یا 403 رخ دادند، ممکن است نیاز به refresh token باشد؛ چون احتمالا یا کاربر جاری به این منبع دسترسی ندارد و یا access token دریافتی که طول عمر آن کمتر از refresh token است، منقضی شده و دیگر قابل استفاده نیست.
ب) پیش از منقضی شدن access token، بهتر است با استفاده از refresh token، یک access token جدید را دریافت کرد تا حالت الف رخ ندهد.

- برای مدیریت حالت الف، یک Policy ویژه‌ی Polly طراحی شده‌است که آن‌را در کلاس ClientRefreshTokenRetryPolicy مشاهده می‌کنید. در این Policy ویژه، هرگاه خطاهای 401 و یا 403 رخ دهند، با استفاده از سرویس جدید IClientRefreshTokenService، کار به روز رسانی توکن انجام خواهد شد. این Policy در کلاس program برنامه ثبت شده‌است. مزیت کار با Policyهای Polly، عدم نیاز به try/catch نوشتن‌های تکراری، در هر جائیکه از سرویس‌های HttpClient استفاده می‌شود، می‌باشد.

- برای مدیریت حالت ب، حتما نیاز به یک تایمر سمت کلاینت است که چند ثانیه پیش از منقضی شدن access token دریافتی پس از لاگین، کار دریافت access token جدیدی را به کمک refresh token موجود، انجام دهد. پیاده سازی این تایمر را در کلاس ClientRefreshTokenTimer مشاهده می‌کنید که محل فراخوانی و راه اندازی آن یا پس از لاگین موفق در سمت کلاینت و یا با ریفرش صفحه (فشرده شدن دکمه‌ی F5) و در کلاس آغازین ClientAuthenticationStateProvider می‌باشد.



نیاز به پیاده سازی Security Trimming سمت کلاینت

از داخل DynamicPermissions دریافتی پس از لاگین، لیست claimهای دسترسی پویای سمت کلاینت کاربر لاگین شده استخراج می‌شود. بنابراین مرحله‌ی بعد، استخراج، پردازش و اعمال این سطوح دسترسی پویای دریافت شده‌ی از سرور است.
سرویس BearerTokensStore، کار ذخیره سازی توکن‌های دریافتی پس از لاگین را انجام می‌دهد و سپس با استفاده از سرویس DynamicClientPermissionsProvider، توکن سوم دریافت شده که مرتبط با لیست claims دسترسی کاربر جاری است را پردازش کرده و تبدیل به یک لیست قابل استفاده می‌کنیم تا توسط آن بتوان زمانیکه قرار است آیتم‌های منوها را به صورت پویا نمایش داد، مشخص کنیم که کاربر، به کدامیک دسترسی دارد و به کدامیک خیر. عدم نمایش قسمتی از صفحه که کاربر به آن دسترسی ندارد را security trimming گویند. برای نمونه کامپوننت ویژه‌ی SecurityTrim.razor، با استفاده از نقش‌ها و claims یک کاربر، می‌تواند تعیین کند که آیا قسمت محصور شده‌ی صفحه توسط آن قابل نمایش به کاربر است یا خیر. این کامپوننت از متدهای کمکی AuthenticationStateExtensions که کار با user claims دریافتی از طریق JWTها را ساده می‌کنند، استفاده می‌کند. یک نمونه از کاربرد کامپوننت SecurityTrim را در فایل src\Client\Shared\MainLayout.razor می‌توانید مشاهده کنید که توسط آن لینک Users Manager، فقط به کاربران دارای نقش Admin نمایش داده می‌شود.
نحوه‌ی مدیریت security trimming منوی پویای برنامه، اندکی متفاوت است. DynamicClientPermissionsProvider لیست claims متعلق به کاربر را بازگشت می‌دهد. این لیست پس از لاگین موفقیت آمیز دریافت شده‌است. سپس لیست کلی صفحاتی را که در ابتدای برنامه استخراج کردیم، در طی حلقه‌ای از سرویس ClientSecurityTrimmingService عبور می‌دهیم. یعنی مسیر صفحه و همچنین دسترسی‌های پویای کاربر، مشخص هستند. در این بین هر مسیری که در لیست claims پویای کاربر نبود، در لیست آیتم‌های منوی پویای برنامه، نمایش داده نمی‌شود.


نیاز به قطع دسترسی به مسیرهایی در سمت کلاینت که کاربر به صورت پویا به آن‌ها دسترسی ندارد

با استفاده از ClientSecurityTrimmingService، در حلقه‌ای که آیتم‌های منوی سایت را نمایش می‌دهد، موارد غیرمرتبط با کاربر جاری را حذف کردیم و نمایش ندادیم. اما این حذف، به این معنا نیست که اگر این آدرس‌ها را به صورت مستقیم در مرورگر وارد کند، به آن‌ها دسترسی نخواهد داشت. برای رفع این مشکل، نیاز به پیاده سازی یک سیاست دسترسی پویای سمت کلاینت است. روش ثبت این سیاست را در کلاس DynamicClientPermissionsPolicyExtensions مشاهده می‌کنید. کلید آن همان CustomPolicies.DynamicClientPermission که در حین تعریف ProtectedPageAttribute به عنوان مقدار Policy پیش‌فرض مقدار دهی شد. یعنی هرگاه ویژگی ProtectedPage به صفحه‌ای اعمال شد، از این سیاست دسترسی استفاده می‌کند که پردازشگر آن DynamicClientPermissionsAuthorizationHandler است. این هندلر نیز از ClientSecurityTrimmingService استفاده می‌کند. در هندلر context.User جاری مشخص است. این کاربر را به متد تعیین دسترسی مسیر جاری به سرویس ClientSecurityTrimming ارسال می‌کنیم تا مشخص شود که آیا به مسیر درخواستی دسترسی دارد یا خیر؟


نیاز به قطع دسترسی به منابعی در سمت سرور که کاربر به صورت پویا به آن‌ها دسترسی ندارد

شبیه به ClientSecurityTrimmingService سمت کلاینت را در سمت سرور نیز داریم؛ به نام ServerSecurityTrimmingService که کار آن، پردازش claimهایی از نوع ::DynamicServerPermission::  است که در صفحه‌ی مدیریتی مرتبطی در سمت کلاینت، به هر کاربر قابل انتساب است. هندلر سیاست دسترسی پویایی که از آن استفاده می‌کند نیز DynamicServerPermissionsAuthorizationHandler می‌باشد. این سیاست دسترسی پویا با کلید CustomPolicies.DynamicServerPermission در کلاس ConfigureServicesExtensions تعریف شده‌است. به همین جهت هر اکشن متدی که Policy آن با این کلید مقدار دهی شده باشد، از هندلر پویای فوق جهت تعیین دسترسی پویا عبور خواهد کرد. منطق پیاده سازی شده‌ی در اینجا، بسیار شبیه به مطلب «سفارشی سازی ASP.NET Core Identity - قسمت پنجم - سیاست‌های دسترسی پویا» است؛ اما بدون استفاده‌ی از ASP.NET Core Identity.


روش اجرای برنامه

چون این برنامه از نوع Blazor WASM هاست شده‌است، نیاز است تا برنامه‌ی Server آن‌را در ابتدا اجرا کنید. با اجرای آن، بانک اطلاعاتی SQLite برنامه به صورت خودکار توسط EF-Core ساخته شده و مقدار دهی اولیه می‌شود. لیست کاربران پیش‌فرض آن‌را در اینجا می‌توانید مشاهده کنید. ابتدا با کاربر ادمین وارد شده و سطوح دسترسی سایر کاربران را تغییر دهید. سپس بجای آن‌ها وارد سیستم شده و تغییرات منوها و سطوح دسترسی پویا را بررسی کنید.
نظرات مطالب
بررسی نحوه‌ی راه اندازی پروژه‌ی Decision
با سلام؛ برای راه اندازی به این خطا خوردم:
 The path specified by "C:\FileStream\ApplicantDocuements" is not in a valid directory.
One or more files listed in the statement could not be found or could not be initialized.
پوشه FileStream را هم ساخته‌ام. 
اشتراک‌ها
NET Framework 4.8. منتشر شد

The .NET Framework 4.8 includes an updated toolset as well as improvements in several areas:

  • [Runtime] JIT and NGEN Improvements
  • [BCL] Updated ZLib
  • [BCL] Reducing FIPS Impact on Cryptography
  • [WinForms] Accessibility Enhancements
  • [WCF] Service Behavior Enhancements
  • [WPF] High DPI Enhancements, UIAutomation Improvements 
NET Framework 4.8. منتشر شد
اشتراک‌ها
نگاهی به کارآیی ASP.NET Core 2.2 در مقایسه با سایر فریم ورک‌ها و پلتفرم‌های مشابه

ASP.NET Core: Saturating 10GbE at 7+ million request/s



In these “platform” comparisons that’s:
- x1.78 faster than ngnix
- x2.93 faster than Java’s Servlet (x7.76 faster than Servlet on Tomcat)
- x7.36 faster than Golang’s “net/http” package
- x8.06 faster than node.js running as a cluster of 28 processes (as node.js is single threaded)
 

نگاهی به کارآیی ASP.NET Core 2.2 در مقایسه با سایر فریم ورک‌ها و پلتفرم‌های مشابه
مسیرراه‌ها
توسعه برنامه های Cross Platform با Xamarin Forms & Bit Framework
لطفا علاوه بر مطالب، نظرات نیز مطالعه شوند.

قسمت اول چرا Xamarin Forms؟
قسمت دوم نصب و راه اندازی
قسمت سوم اجرا و دیباگ پروژه مثال روی UWP و راه اندازی Simulator و Edit & Continue برای C# - XAML
قسمت چهارم اجرا و دیباگ پروژه مثال روی Android + راه اندازی Emulator و Device + نکات بهبود Performance در Android
قسمت پنجم اجرا و دیباگ پروژه مثال روی iOS + راه اندازی Virtual Mac OS + تست بر روی Emulator و Device
قسمت ششم بررسی کلیات پروژه مثال و بررسی ساختار Page - Layout - Control
قسمت هفتم بررسی جزئیات Layout شامل Unit - Padding -Margin و ...
قسمت هشتم بررسی نقش View Model در MVVM و طرز کار Navigation
قسمت نهم Binding & Commanding & x:DataType
قسمت دهم Triggers & Value Converters
قسمت یازدهم List View & Syncfusion Controls
قسمت دوازدهم چند زبانه سازی و Right To Left
قسمت سیزدهم بهبود Performance با Compressed Layout و FF Image Loading
قسمت چهاردهم مدیریت خطاها و استفاده از App Center برای لاگ کردن آنها
قسمت پانزدهم زدن کدهای Platform Specific و Custom Renderers
قسمت هجدهم ارتباط با Rest Api
قسمت نوزدهم نوشتن Component و Partial View
قسمت بیستم Entity Framework Core & Sqlite
قسمت بیست و یکم آشنایی با پروژه ToDoStand
قسمت بیست و دوم ارتباط با پروژه ای که سمت سرور آن با Bit توسعه داده شده است و مزیت‌های کار با Bit در سمت سرور
قسمت بیست و سوم آشنایی با پروژه ToDoLine
مطالب
شروع کار با webpack - قسمت دوم
در مطلب قبلی بیشتر از لحاظ تئوریک با وب‌پک آشنا شدیم و در آخر نیز یک تک اسکریپت را با استفاده از آن باندل کرده و در صفحه‌ی index.html اضافه کردیم.

توجه :
در مطلب قبلی برای استفاده و نصب وبپک دو راه پیشنهاد شد؛ یکی نصب وبپک به صورت سراسری و دیگری به صورت محلی در محیط کاری فعلی پروژه. استفاده‌ی نگارنده به صورت محلی می‌باشد و برای فراخوانی وبپک از دستور npm run webpack استفاده خواهد شد. در صورتی که از وبپک به صورت سراسری (گلوبال ) استفاده می‌کنید، به جای این دستور فقط کافی است در خط فرمان دستور webpack را نوشته و آن را اجرا کنید.

اضافه کردن فایل تنظیمات وبپک

وبپک دارای تنظیمات و حالت‌های مختلفی برای تولید خروجی نهایی می‌باشد که می‌توان این تنظیمات را به صورت پارامترهای ورودی، در هنگام فراخوانی برای آن مشخص کرد. ولی برای ساده کردن و همچنین عدم الزام به تکرار برای تنظیمات مورد نیاز می‌توانیم یک فایل پیکربندی را ایجاد کنیم و موارد مورد نیاز را در آن تعریف کرده و تنها با فراخوانی نام webpack در خط فرمان، به صورت خودکار این تنظیمات خوانده شده و دستورات ما اجرا شوند. TaskRunner‌های گالپ و گرانت نیز دارای یک فایل پیکربندی، برای مشخص کردن تنظیمات مورد نیاز کاربر می‌باشند.

ساخت فایل پیکربندی وبپک

در محیط کاری پروژه یک فایل جدید را با نام webpack.config.js ایجاد می‌کنیم، تا پیکر بندی مورد نظرمان را برای وبپک در آن مشخص کنیم (نام این فایل قراردادی است و امکان مشخص کردن فایلی با نام دیگر نیز وجود دارد که در آینده با آن برخورد خواهیم کرد).
این فایل به صورت یک ماژول در فرمت commonjs می‌باشد (در صورتی که با ماژول‌های مختلف آشنا نیستید، مطالعه‌ی این مقاله پیشنهاد می‌شود ماژول‌ها در es6).
پس از ایجاد فایل پیکربندی در محیط کاری پروژه، محتوای زیر را به آن اضافه خواهیم کرد. این حالت را می‌توان ساده‌ترین پیکربندی وبپک دانست و با دستور webpack ./main.js bundle.js که در پایان مطلب قبلی در خط فرمان اجرا کردیم، تفاوتی ندارد.
// webpack.config.js file
module.exports = {
    entry:'./main.js'
    ,output:{
        filename:'bundle.js'
    }
}
پروپرتی entry مشخص کننده‌ی فایل ورودی است که قصد پردازش آن را داریم و پروپرتی output نیز خود یک آبجکت می‌باشد که در ساده‌ترین حالت، احتیاج به تعریف یک پروپرتی با نام filename را در آن داریم که مشخص کننده‌ی نام فایل باندل شونده توسط وبپک می‌باشد.
حال با اجرای دستور npm run webpack، وبپک به صورت خودکار محتوای فایل پیکربندی را خوانده و تنظیمات تعریف شده را در فایل باندل نهایی ترتیب اثر می‌دهد.

حالت نظاره گر یا watch mode

اضافه کردن فایل پیکربندی می‌تواند مفید باشد و ما را از الزام به تکرار برای مشخص کردن پارامترهای مورد نیاز در هر بار اجرای وبپک بی‌نیاز می‌کند. ولی فرض کنید در حال توسعه‌ی پروژه‌ای هستید و مدام در حال تغییر فایل‌های پروژه می‌باشید. فایلی اضافه، حذف و یا دچار تغییر می‌شود و برای هر بار انجام شدن پروسه‌ی باندلینگ باید وبپک را فراخوانی کنیم. برای جلوگیری از این پروسه‌ی تکراری، وبپک دارای حالت نظاره‌گر یا watch mode می‌باشد. معنای این حالت این است که وبپک تغییرات محیط کاری شما را در نظر می‌گیرد و با انجام هر تغییری، دوباره باندل مربوطه را از نو می‌سازد.
برای وارد شدن به این حالت یک راه کار این می‌باشد که در هنگام فراخوانی وبپک در خط فرمان، پرچم زیر را به آن اضافه کنیم:
//for when webpack is installed globally 
webpack --watch
//for when webpack is installed locally in project 
npm run webpack -- --watch
(در فراخوانی بالا دو حالت نصب سراسری و محلی وبپک در نظر گرفته شده‌است. حالت اول نکته‌ای را ندارد. ولی در حالت دوم برای اینکه پارامترهای خط فرمان توسط npm به دست وبپک برسد، احتیاج به اضافه کردن -- می‌باشد. جهت عدم آشنایی با این مورد می‌توانید به اینجا مراجعه کنید: فرستادن پارامتر به اسکریپت‌های npm)
راه کار دوم جهت تنظیم کردن وبپک در حالت نظاره گر، اضافه کردن پروپرتی watch به فایل پیکربندی وبپک است. پس از انجام این تغییر، محتوای فایل پیکربندی به این صورت خواهد بود:
//webpack.config.js file
module.exports = {
    entry:'./main.js'
    ,output:{
        filename:'bundle.js'
    }
    ,watch :true
}
اینبار برای ورود وپ بک به حالت نظاره‌گر کافی است وبپک را یک بار از طریق خط فرمان با دستور npm run webpack، فراخوانی کنیم.
در صورتی که مشکلی وجود نداشته باشد، با اجرای این دستور، کنترل خط فرمان به شما برنخواهد گشت و وبپک در حالت اجرا باقی می‌ماند که در تصویر زیر قابل مشاهده‌است.

حال اگر در اسکریپت main.js تغییری ایجاد کنید، خواهید دید که وبپک به صورت خودکار باندل را از اول خواهد ساخت.

وب سرور وبپک

تا اینجا از وبپک به عنوان یک باندل کننده بهره برده‌ایم و جهت میزبانی فایل‌های پروژه از فایل سیستم و سیستم عامل بهره بردیم. ولی می‌دانیم که در حین توسعه دادن برنامه‌های وب، استفاده از فایل سیستم و سیستم عامل مفید نیست و دچار مشکلات عدیده‌ای هم از سمت مرورگرها و هم از سمت کتابخانه‌های معروف جاوا اسکریپتی خواهیم شد( مانند مباحث cors و ...). جهت حذف این مشکلات می‌توانیم وب سرور مورد علاقه‌ی خود را اجرا کنیم یا از وب سرور فراهم شده توسط وبپک بهره ببریم.
جهت نصب وب سرور وبپک دستور زیر را در خط فرمان  اجرا خواهیم کرد ( به صورت سراسری یا محلی به انتخاب شما خواهد بود و قبلا توضیح داده شده است).
// to install globally :
npm install -g webpack-dev-server

//to install locally in project :
npm install -D webpack-dev-server
در حالتی که وبپک به صورت سراسری نصب شده باشد، با اجرای دستور webpack-dev-server در خط فرمان، وب سرور وبپک شروع به کار خواهد کرد و تنظیمات را نیز از فایل پیکربندی اعمال می‌کند.
در صورتی که وبپک به صورت محلی نصب شده باشد، بایستی یک مدخل به قسمت اسکریپت‌های package.json برای راهنمایی npm اضافه کنیم. محتویات این فایل پس از تغییرات، از این قرار است:
//package.json file
{
  "name": "dntwebpack",
  "version": "1.0.0",
  "description": "",
  "main": "main.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "webpack": "webpack",
    "webpackserver": "webpack-dev-server"
  },
  "author": "mehdi",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^1.13.1",
    "webpack-dev-server": "^1.14.1"
  }
}
در اینجا پروپرتی جدیدی به قسمت scripts، با نام webpackserver اضافه شده‌است. حال با فراخوانی این اسکریپت با دستور زیر، وب سرور وبپک شروع به کار خواهد کرد:
npm run webpackserver
(دقت کنید که نام‌های قرار داده شده‌ی در قسمت scripts می‌توانند به صورت دلخواه باشند و شما می‌توانید نامی را که دلخواه خودتان است، برگزینید؛ به طور مثال به جای webpackserver نام دیگری را در فایل package.json برای آن مشخص کنید و در هنگام فراخوانی از آن استفاده کنید).
در صورتی که همه چیز بدون مشکل باشد، خروجی شبیه به تصویر زیر را مشاهده خواهید کرد که آدرسی که به صورت محلی، سرور بر روی آن میزبان شده است نیز قابل مشاهده است:


باندل کردن اسکریپت‌های گوناگون توسط وبپک

تا اینجای کار تنها از یک تک اسکریپت، با نام main.js استفاده کردیم. قطعا پروژه‌های واقعی از یک تک اسکریپت تشکیل نخواهند شد و اسکریپت‌های گوناگونی خواهیم داشت. جهت استفاده از چندین اسکریپت توسط وبپک، دو سناریوی مختلف رخ خواهند داد که هر دو را برسی خواهیم کرد:
اضافه کردن اسکریپت‌ها به صورت داینامیک یا پویا توسط وبپک 

در محیط کاری پروژه، یک فایل جدید user.js را اضافه می‌کنیم که از این فایل در فایل main.js استفاده خواهد شد.
محتوای فایل user.js یک تابع ساده‌ی جاوا اسکریپتی خواهد بود:
// user.js file 

function userLog() {
    console.log("ahooy from user module file");
}

module.exports={
    userLog:userLog
}  
حال جهت استفاده از این ماژول در فایل main.js تغییرات زیر را اعمال خواهیم کرد:
//main.js file

var user = require("./user");

user.userLog();

console.log(`i'm bundled by webpack`);
پس از ذخیره‌ی تغییرات خواهید دید که وب سرور وبپک از این تغییرات آگاه شده و باندل جدید را خواهد ساخت که در اینجا خروجی مانند تصویر زیر را خواهید دید:
در تصویر قابل مشاهده است که ماژول user.js نیز وارد باندل شده است.


در صورتی که به مسیری که وبپک در حال میزبانی بر روی آن است در مرورگر خود مراجعه کنید، پیغام‌های چاپ شده را در کنسول مشاهده خواهید کرد.

اضافه کردن اسکریپت‌ها به باندل به صورت استاتیک توسط وبپک

قطعا در پروژه‌های خود از کتابخانه‌هایی که توسط برنامه نویسان دیگر تولید شده‌اند مانند جی کوئری و ... استفاده خواهیم کرد. استفاده از این اسکریپت‌ها به صورت داینامیک و ایمپورت کردن آنها در هر ماژول جالب نخواهد بود و یا ممکن است ماژولی که خود شما نوشته اید به صورت اشتراکی بین تمام برنامه اجرا شود. در این گونه از موارد می‌توانیم این اسکریپت‌ها را در فایل پیکربندی به وبپک معرفی کنیم تا در هنگام باندلینگ، به باندل وارد شوند.
اسکریپت جدیدی را در پروژه اضافه می‌کنیم و نامش را shared.js می‌گذاریم که دارای محتوای زیر است :
// shared.js file
console.log('log message from shared module !');
حال برای اینکه این اسکریپت را به وبپک معرفی کنیم، فایل پیکربندی وبپک را باز کرده و تغییرات زیر را در آن اعمال می‌کنیم :
//webpack.config.js file
module.exports = {
    entry:['./shared.js','./main.js']
    ,output:{
        filename:'bundle.js'
    }
    ,watch :true
}
قابل مشاهده است که قسمت entry، به جای این که یک تک فایل را معرفی کند، تبدیل به یک آرایه شده‌است که هم فایل shared.js را در بر می‌گیرد و هم فایل main.js را دارد.
در مواقعی که فایل پیکربندی دچار تغییر می‌شود، بایستی وبپک را متوقف و دوباره اجرا کنید تا تنظیمات جدید، اعمال شوند. پس از راه اندازی دوباره وبپک، در صورت موفقیت آمیز بودن تغییراتتان، خروجی را شبیه تصویر رو به رو خواهید گرفت و مشخص است که فایل shared.js نیز در باندل وارد شده است.


استفاده از Loader‌ها در وبپک

به صورت پیش فرض وبپک قابلیت باندل کردن ماژول‌های جاوا اسکریپت را دارد و همچنین می‌تواند این فایل‌ها را Minify  کند (در مطالب بعدی خواهیم دید). ولی به طور مثال استفاده از تایپ اسکریپت از توانایی‌های وبپک به صورت توکار خارج است. اینجاست که Loader‌ها وارد کار می‌شوند.
اگر بخواهیم به زبان ساده Loader‌‌ها را تعریف کنیم می‌توان  آنها را کامپوننت هایی دانست که به وبپک فوت و فن کار جدیدی را یاد می‌دهند.
در ادامه Loader تایپ اسکریپت را نصب خواهیم کرد و به کمک آن فایل‌های پروژه را تبدیل به تایپ اسکریپت کرده و در هنگام باندل کردن از وبپک می‌خواهیم که این فایل‌ها را ترنسپایل کند و سپس باندل را از روی آنها بسازد ( برای مطالعه‌ی ادامه‌ی این مطلب احتیاجی به آشنایی به تایپ اسکریپت نیست و هدف استفاده از یک loader است. ولی در صورت علاقه می‌توانید به اینجا مراجعه کنید سری آموزش تایپ اسکریپت)

نصب Loader تایپ اسکریپت 

به خط فرمان برگشته و با استفاده از npm، لودر تایپ اسکریپت مورد نیاز وبپک را نصب می‌کنیم. دستور مورد نیاز این قرار است :
npm install -D ts-loader

( توجه :
در ادامه این مطلب از پیکربندی ساده‌ی یک پروژه‌ی تایپ اسکریپتی استفاده شده است که اعم از ایجاد فایل tsconfig.json و اضافه کردن پوشه‌ی typings به پروژه می‌باشد.)
فایل main.ts را که یک فایل تایپ اسکریپتی می‌باشد، به پروژه اضافه می‌کنیم. محتوای آن به صورت زیر خواهد بود. قابل مشاهده است که از ویژگی‌های ES6 در این فایل استفاده شده و این انتظار را از لودر تایپ اسکریپت داریم که این فایل را در هنگام باندلینگ برای ما ترنسپایل کند.
// main.ts file
let user = require("./user");

user.userLog();
let mainlogger = () => {
    console.log(`i'm bundled by webpack in an arrow function`);
}

mainlogger();
برای اینکه به وبپک خبر دهیم که در پروژه در حال استفاده از تایپ اسکریپت هستیم، فایل پیکربندی وبپک را باز کرده و پروپرتی جدیدی را با نام module به آن معرفی می‌کنیم که خود یک آبجکت می‌باشد. حال در آبجکت module یک پروپرتی جدید را با نام loaders که جنس آرایه‌ای دارد، اضافه می‌کنیم. آرایه‌ی loaders شامل همه‌ی loader هایی خواهد بود که شما قصد استفاده‌ی آنها را به همراه وبپک دارید. هر عضو از این آرایه خود نیز یک آبجکت می‌باشد که دارای سه پروپرتی زیر می‌باشد:
test : یک رجکس می‌باشد که به loader می‌گوید به دنبال چه فایل‌هایی بگردد.
exclude : از جنس رجکس و مشخص کننده‌ی مسیرهایی است که از پروژه باید جدا شوند و توسط loader پردازش نشوند (مانند فایل‌های از قبل کامپایل شده‌ی کتابخانه‌ها).
loader : مشخص کننده‌ی نام loader مورد نظر .
محتوای فایل پیکربندی وبپک، پس از معرفی loader تایپ اسکریپت، به این صورت خواهد بود:
//webpack.config.js
module.exports = {
    entry:['./shared.js','./main.js']
    ,output:{
        filename:'bundle.js'
    }
    ,watch :true
    ,module:{
        loaders:[
            {
                test:/\.ts$/
                ,exclude:/node_modules/
                ,loader:'ts-loader'
            }
        ]
    }
}
حال با اجرای دوباره‌ی وبپک، loader تایپ اسکریپت ابتدا اجرا شده، سپس وبپک وارد کار می‌شود و فایل‌ها را باندل خواهد کرد. در صورتی که بدون مشکل همه چیز اجرا شود، خروجی مانند تصویر زیر را خواهید داشت:

در این مطلب تنظیمات مختلف وبپک، فایل پیکربندی، استفاده از چندین فایل به همراه وبپک، وب سرور وبپک و همچنین با loader‌های وبپک آشنا شدیم.
دریافت فایل‌ها dntwebpack-part2.zip