مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت هفتم- امن سازی Web API
تا اینجا بجای قرار دادن مستقیم قسمت مدیریت هویت کاربران، داخل یک یا چند برنامه‌ی مختلف، این دغدغه‌ی مشترک (common concern) بین برنامه‌ها را به یک برنامه‌ی کاملا مجزای دیگری به نام Identity provider و یا به اختصار IDP منتقل و همچنین دسترسی به کلاینت MVC برنامه‌ی گالری تصاویر را نیز توسط آن امن سازی کردیم. اما هنوز یک قسمت باقی مانده‌است: برنامه‌ی کلاینت MVC، منابع خودش را از یک برنامه‌ی Web API دیگر دریافت می‌کند و هرچند دسترسی به برنامه‌ی MVC امن شده‌است، اما دسترسی به منابع برنامه‌ی Web API آن کاملا آزاد و بدون محدودیت است. بنابراین امن سازی Web API را توسط IDP، در این قسمت پیگیری می‌کنیم. پیش از مطالعه‌ی این قسمت نیاز است مطلب «آشنایی با JSON Web Token» را مطالعه کرده و با ساختار ابتدایی یک JWT آشنا باشید.


بررسی Hybrid Flow جهت امن سازی Web API

این Flow را پیشتر نیز مرور کرده بودیم. تفاوت آن با قسمت‌های قبل، در استفاده از توکن دومی است به نام access token که به همراه identity token از طرف IDP صادر می‌شود و تا این قسمت از آن بجز در قسمت «دریافت اطلاعات بیشتری از کاربران از طریق UserInfo Endpoint» استفاده نکرده بودیم.


در اینجا، ابتدا برنامه‌ی وب، یک درخواست اعتبارسنجی را به سمت IDP ارسال می‌کند که response type آن از نوع code id_token است (یا همان مشخصه‌ی Hybrid Flow) و همچنین تعدادی scope نیز جهت دریافت claims متناظر با آن‌ها در این درخواست ذکر شده‌اند. در سمت IDP، کاربر با ارائه‌ی مشخصات خود، اعتبارسنجی شده و پس از آن IDP صفحه‌ی اجازه‌ی دسترسی به اطلاعات کاربر (صفحه‌ی consent) را ارائه می‌دهد. پس از آن IDP اطلاعات code و id_token را به سمت برنامه‌ی وب ارسال می‌کند. در ادامه کلاینت وب، توکن هویت رسیده را اعتبارسنجی می‌کند. پس از موفقیت آمیز بودن این عملیات، اکنون کلاینت درخواست دریافت یک access token را از IDP ارائه می‌دهد. اینکار در پشت صحنه و بدون دخالت کاربر صورت می‌گیرد که به آن استفاده‌ی از back channel هم گفته می‌شود. یک چنین درخواستی به token endpoint، شامل اطلاعات code و مشخصات دقیق کلاینت جاری است. به عبارتی نوعی اعتبارسنجی هویت برنامه‌ی کلاینت نیز می‌باشد. در پاسخ، دو توکن جدید را دریافت می‌کنیم: identity token و access token. در اینجا access token توسط خاصیت at_hash موجود در id_token به آن لینک می‌شود. سپس هر دو توکن اعتبارسنجی می‌شوند. در این مرحله، میان‌افزار اعتبارسنجی، هویت کاربر را از identity token استخراج می‌کند. به این ترتیب امکان وارد شدن به برنامه‌ی کلاینت میسر می‌شود. در اینجا همچنین access token ای نیز صادر شده‌است.
اکنون علاقمند به کار با Web API برنامه‌ی کلاینت MVC خود هستیم. برای این منظور access token که اکنون در برنامه‌ی MVC Client در دسترس است، به صورت یک Bearer token به هدر ویژه‌ای با کلید Authorization اضافه می‌شود و به همراه هر درخواست، به سمت API ارسال خواهد شد. در سمت Web API این access token رسیده، اعتبارسنجی می‌شود و در صورت موفقیت آمیز بودن عملیات، دسترسی به منابع Web API صادر خواهد شد.


امن سازی دسترسی به Web API

تنظیمات برنامه‌ی IDP
برای امن سازی دسترسی به Web API از کلاس src\IDP\DNT.IDP\Config.cs در سطح IDP شروع می‌کنیم. در اینجا باید یک scope جدید مخصوص دسترسی به منابع Web API را تعریف کنیم:
namespace DNT.IDP
{
    public static class Config
    {
        // api-related resources (scopes)
        public static IEnumerable<ApiResource> GetApiResources()
        {
            return new List<ApiResource>
            {
                new ApiResource(
                    name: "imagegalleryapi",
                    displayName: "Image Gallery API",
                    claimTypes: new List<string> {"role" })
            };
        }
هدف آن داشتن access token ای است که در قسمت Audience آن، نام این ApiResource، درج شده باشد؛ پیش از اینکه دسترسی به API را پیدا کند. برای تعریف آن، متد جدید GetApiResources را به صورت فوق به کلاس Config اضافه می‌کنیم.
پس از آن در قسمت تعریف کلاینت، مجوز درخواست این scope جدید imagegalleryapi را نیز صادر می‌کنیم:
AllowedScopes =
{
  IdentityServerConstants.StandardScopes.OpenId,
  IdentityServerConstants.StandardScopes.Profile,
  IdentityServerConstants.StandardScopes.Address,
  "roles",
  "imagegalleryapi"
},
اکنون باید متد جدید GetApiResources را به کلاس src\IDP\DNT.IDP\Startup.cs معرفی کنیم که توسط متد AddInMemoryApiResources به صورت زیر قابل انجام است:
namespace DNT.IDP
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            services.AddIdentityServer()
             .AddDeveloperSigningCredential()
             .AddTestUsers(Config.GetUsers())
             .AddInMemoryIdentityResources(Config.GetIdentityResources())
             .AddInMemoryApiResources(Config.GetApiResources())
             .AddInMemoryClients(Config.GetClients());
        }

تنظیمات برنامه‌ی MVC Client
اکنون نوبت انجام تنظیمات برنامه‌ی MVC Client در فایل ImageGallery.MvcClient.WebApp\Startup.cs است. در اینجا در متد AddOpenIdConnect، درخواست scope جدید imagegalleryapi را صادر می‌کنیم:
options.Scope.Add("imagegalleryapi");

تنظیمات برنامه‌ی Web API
اکنون می‌خواهیم مطمئن شویم که Web API، به access token ای که قسمت Audience آن درست مقدار دهی شده‌است، دسترسی خواهد داشت.
برای این منظور به پوشه‌ی پروژه‌ی Web API در مسیر src\WebApi\ImageGallery.WebApi.WebApp وارد شده و دستور زیر را صادر کنید تا بسته‌ی نیوگت AccessTokenValidation نصب شود:
dotnet add package IdentityServer4.AccessTokenValidation
اکنون کلاس startup در سطح Web API را در فایل src\WebApi\ImageGallery.WebApi.WebApp\Startup.cs به صورت زیر تکمیل می‌کنیم:
using IdentityServer4.AccessTokenValidation;

namespace ImageGallery.WebApi.WebApp
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(defaultScheme: IdentityServerAuthenticationDefaults.AuthenticationScheme)
               .AddIdentityServerAuthentication(options =>
               {
                   options.Authority = Configuration["IDPBaseAddress"];
                   options.ApiName = "imagegalleryapi";
               });
متد AddAuthentication یک defaultScheme را تعریف می‌کند که در بسته‌ی IdentityServer4.AccessTokenValidation قرار دارد و این scheme در اصل دارای مقدار Bearer است.
سپس متد AddIdentityServerAuthentication فراخوانی شده‌است که به آدرس IDP اشاره می‌کند که مقدار آن‌را در فایل appsettings.json قرار داده‌ایم. از این آدرس برای بارگذاری متادیتای IDP استفاده می‌شود. کار دیگر این میان‌افزار، اعتبارسنجی access token رسیده‌ی به آن است. مقدار خاصیت ApiName آن، به نام API resource تعریف شده‌ی در سمت IDP اشاره می‌کند. هدف این است که بررسی شود آیا خاصیت aud موجود در access token رسیده به مقدار imagegalleryapi تنظیم شده‌است یا خیر؟

پس از تنظیم این میان‌افزار، اکنون نوبت به افزودن آن به ASP.NET Core request pipeline است:
namespace ImageGallery.WebApi.WebApp
{
    public class Startup
    {
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseAuthentication();
محل فراخوانی UseAuthentication باید پیش از فراخوانی app.UseMvc باشد تا پس از اعتبارسنجی درخواست، به میان‌افزار MVC منتقل شود.

اکنون می‌توانیم اجبار به Authorization را در تمام اکشن متدهای این Web API در فایل ImageGallery.WebApi.WebApp\Controllers\ImagesController.cs فعالسازی کنیم:
namespace ImageGallery.WebApi.WebApp.Controllers
{
    [Route("api/images")]
    [Authorize]
    public class ImagesController : Controller
    {


ارسال Access Token به همراه هر درخواست به سمت Web API

تا اینجا اگر مراحل اجرای برنامه‌ها را طی کنید، مشاهده خواهید کرد که برنامه‌ی MVC Client دیگر کار نمی‌کند و نمی‌تواند از فیلتر Authorize فوق رد شود. علت اینجا است که در حال حاضر، تمامی درخواست‌های رسیده‌ی به Web API، فاقد Access token هستند. بنابراین اعتبارسنجی آن‌ها با شکست مواجه می‌شود.
برای رفع این مشکل، سرویس ImageGalleryHttpClient را به نحو زیر اصلاح می‌کنیم تا در صورت وجود Access token، آن‌را به صورت خودکار به هدرهای ارسالی توسط HttpClient اضافه کند:
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

namespace ImageGallery.MvcClient.Services
{
    public interface IImageGalleryHttpClient
    {
        Task<HttpClient> GetHttpClientAsync();
    }

    /// <summary>
    /// A typed HttpClient.
    /// </summary>
    public class ImageGalleryHttpClient : IImageGalleryHttpClient
    {
        private readonly HttpClient _httpClient;
        private readonly IConfiguration _configuration;
        private readonly IHttpContextAccessor _httpContextAccessor;

        public ImageGalleryHttpClient(
            HttpClient httpClient,
            IConfiguration configuration,
            IHttpContextAccessor httpContextAccessor)
        {
            _httpClient = httpClient;
            _configuration = configuration;
            _httpContextAccessor = httpContextAccessor;
        }

        public async Task<HttpClient> GetHttpClientAsync()
        {
            var currentContext = _httpContextAccessor.HttpContext;
            var accessToken = await currentContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
            if (!string.IsNullOrWhiteSpace(accessToken))
            {
                _httpClient.SetBearerToken(accessToken);
            }

            _httpClient.BaseAddress = new Uri(_configuration["WebApiBaseAddress"]);
            _httpClient.DefaultRequestHeaders.Accept.Clear();
            _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            return _httpClient;
        }
    }
}
اسمبلی این سرویس برای اینکه به درستی کامپایل شود، نیاز به این وابستگی‌ها نیز دارد:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.1.1.0" />
    <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.1.1.0" />
    <PackageReference Include="Microsoft.AspNetCore.Authentication.Abstractions" Version="2.1.1.0" />
    <PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="5.2.0.0" />
    <PackageReference Include="IdentityModel" Version="3.9.0" />
  </ItemGroup>
</Project>
در اینجا با استفاده از سرویس IHttpContextAccessor، به HttpContext جاری درخواست دسترسی یافته و سپس توسط متد GetTokenAsync، توکن دسترسی آن‌را استخراج می‌کنیم. سپس این توکن را در صورت وجود، توسط متد SetBearerToken به عنوان هدر Authorization از نوع Bearer، به سمت Web API ارسال خواهیم کرد.
البته پس از این تغییرات نیاز است به کنترلر گالری مراجعه و از متد جدید GetHttpClientAsync بجای خاصیت HttpClient قبلی استفاده کرد.

اکنون اگر برنامه را اجرا کنیم، پس از لاگین، دسترسی به Web API امن شده، برقرار شده و برنامه بدون مشکل کار می‌کند.


بررسی محتوای Access Token

اگر بر روی سطر if (!string.IsNullOrWhiteSpace(accessToken)) در سرویس ImageGalleryHttpClient یک break-point را قرار دهیم و محتویات Access Token را در حافظه ذخیره کنیم، می‌توانیم با مراجعه‌ی به سایت jwt.io، محتویات آن‌را بررسی نمائیم:


که در حقیقت این محتوا را به همراه دارد:
{
  "nbf": 1536394771,
  "exp": 1536398371,
  "iss": "https://localhost:6001",
  "aud": [
    "https://localhost:6001/resources",
    "imagegalleryapi"
  ],
  "client_id": "imagegalleryclient",
  "sub": "d860efca-22d9-47fd-8249-791ba61b07c7",
  "auth_time": 1536394763,
  "idp": "local",  
  "role": "PayingUser",
  "scope": [
    "openid",
    "profile",
    "address",
    "roles",
    "imagegalleryapi"
  ],
  "amr": [
    "pwd"
  ]
}
در اینجا در لیست scope، مقدار imagegalleryapi وجود دارد. همچنین در قسمت audience و یا aud نیز ذکر شده‌است. بنابراین یک چنین توکنی قابلیت دسترسی به Web API تنظیم شده‌ی ما را دارد.
همچنین اگر دقت کنید، Id کاربر جاری در خاصیت sub آن قرار دارد.


مدیریت صفحه‌ی عدم دسترسی به Web API

با اضافه شدن scope جدید دسترسی به API در سمت IDP، این مورد در صفحه‌ی دریافت رضایت کاربر نیز ظاهر می‌شود:


در این حالت اگر کاربر این گزینه را انتخاب نکند، پس از هدایت به برنامه‌ی کلاینت، در سطر response.EnsureSuccessStatusCode استثنای زیر ظاهر خواهد شد:
An unhandled exception occurred while processing the request.
HttpRequestException: Response status code does not indicate success: 401 (Unauthorized).
 System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()
برای اینکه این صفحه‌ی نمایش استثناء را با صفحه‌ی عدم دسترسی جایگزین کنیم، می‌توان پس از دریافت response از سمت Web API، به StatusCode مساوی Unauthorized = 401 به صورت زیر عکس‌العمل نشان داد:
        public async Task<IActionResult> Index()
        {
            var httpClient = await _imageGalleryHttpClient.GetHttpClientAsync();
            var response = await httpClient.GetAsync("api/images");

            if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized ||
                response.StatusCode == System.Net.HttpStatusCode.Forbidden)
            {
                return RedirectToAction("AccessDenied", "Authorization");
            }
            response.EnsureSuccessStatusCode();


فیلتر کردن تصاویر نمایش داده شده بر اساس هویت کاربر وارد شده‌ی به سیستم

تا اینجا هرچند دسترسی به API امن شده‌است، اما هنوز کاربر وارد شده‌ی به سیستم می‌تواند تصاویر سایر کاربران را نیز مشاهده کند. بنابراین قدم بعدی امن سازی API، عکس العمل نشان دادن به هویت کاربر جاری سیستم است.
برای این منظور به کنترلر ImageGallery.WebApi.WebApp\Controllers\ImagesController.cs سمت API مراجعه کرده و Id کاربر جاری را از لیست Claims او استخراج می‌کنیم:
namespace ImageGallery.WebApi.WebApp.Controllers
{
    [Route("api/images")]
    [Authorize]
    public class ImagesController : Controller
    {
        [HttpGet()]
        public async Task<IActionResult> GetImages()
        {
            var ownerId = this.User.Claims.FirstOrDefault(claim => claim.Type == "sub").Value;
اگر به قسمت «بررسی محتوای Access Token» مطلب جاری دقت کنید، مقدار Id کاربر در خاصیت sub این Access token قرار گرفته‌است که روش دسترسی به آن‌را در ابتدای اکشن متد GetImages فوق ملاحظه می‌کنید.
مرحله‌ی بعد، مراجعه به ImageGallery.WebApi.Services\ImagesService.cs و تغییر متد GetImagesAsync است تا صرفا بر اساس ownerId دریافت شده کار کند:
namespace ImageGallery.WebApi.Services
{
    public class ImagesService : IImagesService
    {
        public Task<List<Image>> GetImagesAsync(string ownerId)
        {
            return _images.Where(image => image.OwnerId == ownerId).OrderBy(image => image.Title).ToListAsync();
        }
پس از این تغییرات، اکشن متد GetImages سمت API چنین پیاده سازی را پیدا می‌کند که در آن بر اساس Id شخص وارد شده‌ی به سیستم، صرفا لیست تصاویر مرتبط با او بازگشت داده خواهد شد و نه لیست تصاویر تمام کاربران سیستم:
namespace ImageGallery.WebApi.WebApp.Controllers
{
    [Route("api/images")]
    [Authorize]
    public class ImagesController : Controller
    {
        [HttpGet()]
        public async Task<IActionResult> GetImages()
        {
            var ownerId = this.User.Claims.FirstOrDefault(claim => claim.Type == "sub").Value;
            var imagesFromRepo = await _imagesService.GetImagesAsync(ownerId);
            var imagesToReturn = _mapper.Map<IEnumerable<ImageModel>>(imagesFromRepo);
            return Ok(imagesToReturn);
        }
اکنون اگر از برنامه‌ی کلاینت خارج شده و مجددا به آن وارد شویم، تنها لیست تصاویر مرتبط با کاربر وارد شده، نمایش داده می‌شوند.

هنوز یک مشکل دیگر باقی است: سایر اکشن متدهای این کنترلر Web API همچنان محدود به کاربر جاری نشده‌اند. یک روش آن تغییر دستی تمام کدهای آن است. در این حالت متد IsImageOwnerAsync زیر، جهت بررسی اینکه آیا رکورد درخواستی متعلق به کاربر جاری است یا خیر، به سرویس تصاویر اضافه می‌شود:
namespace ImageGallery.WebApi.Services
{
    public class ImagesService : IImagesService
    {
        public Task<bool> IsImageOwnerAsync(Guid id, string ownerId)
        {
            return _images.AnyAsync(i => i.Id == id && i.OwnerId == ownerId);
        }
و سپس در تمام اکشن متدهای دیگر، در ابتدای آن‌ها باید این بررسی را انجام دهیم و در صورت شکست آن return Unauthorized را بازگشت دهیم.
اما روش بهتر انجام این عملیات را که در قسمت بعدی بررسی می‌کنیم، بر اساس بستن دسترسی ورود به اکشن متدها بر اساس Authorization policy است. در این حالت اگر کاربری مجوز انجام عملیاتی را نداشت، اصلا وارد کدهای یک اکشن متد نخواهد شد.


ارسال سایر User Claims مانند نقش‌ها به همراه یک Access Token

برای تکمیل قسمت ارسال تصاویر می‌خواهیم تنها کاربران نقش خاصی قادر به انجام اینکار باشند. اما اگر به محتوای access token ارسالی به سمت Web API دقت کرده باشید، حاوی Identity claims نیست. البته می‌توان مستقیما در برنامه‌ی Web API با UserInfo Endpoint، برای دریافت اطلاعات بیشتر، کار کرد که نمونه‌ای از آن‌را در قسمت قبل مشاهده کردید، اما مشکل آن زیاد شدن تعداد رفت و برگشت‌های به سمت IDP است. همچنین باید درنظر داشت که فراخوانی مستقیم UserInfo Endpoint جهت برنامه‌ی MVC client که درخواست دریافت access token را از IDP می‌دهد، متداول است و نه برنامه‌ی Web API.
برای رفع این مشکل باید در حین تعریف ApiResource، لیست claim مورد نیاز را هم ذکر کرد:
namespace DNT.IDP
{
    public static class Config
    {
        // api-related resources (scopes)
        public static IEnumerable<ApiResource> GetApiResources()
        {
            return new List<ApiResource>
            {
                new ApiResource(
                    name: "imagegalleryapi",
                    displayName: "Image Gallery API",
                    claimTypes: new List<string> {"role" })
            };
        }
در اینجا ذکر claimTypes است که سبب خواهد شد نقش کاربر جاری به توکن دسترسی اضافه شود.

سپس کار با اکشن متد CreateImage در سمت API را به نقش PayingUser محدود می‌کنیم:
namespace ImageGallery.WebApi.WebApp.Controllers
{
    [Route("api/images")]
    [Authorize]
    public class ImagesController : Controller
    {
        [HttpPost]
        [Authorize(Roles = "PayingUser")]
        public async Task<IActionResult> CreateImage([FromBody] ImageForCreationModel imageForCreation)
        {
همچنین در این اکشن متد، پیش از فراخوانی متد AddImageAsync نیاز است مشخص کنیم OwnerId این تصویر کیست تا رکورد بانک اطلاعاتی تصویر آپلود شده، دقیقا به اکانت متناظری در سمت IDP مرتبط شود:
var ownerId = User.Claims.FirstOrDefault(c => c.Type == "sub").Value;
imageEntity.OwnerId = ownerId;
// add and save.
await _imagesService.AddImageAsync(imageEntity);

نکته‌ی مهم: در اینجا نباید این OwnerId را از سمت برنامه‌ی کلاینت MVC به سمت برنامه‌ی Web API ارسال کرد. برنامه‌ی Web API باید این اطلاعات را از access token اعتبارسنجی شده‌ی رسیده استخراج و استفاده کند؛ از این جهت که دستکاری اطلاعات اعتبارسنجی نشده‌ی ارسالی به سمت Web API ساده‌است؛ اما access tokenها دارای امضای دیجیتال هستند.

در سمت کلاینت نیز در فایل ImageGallery.MvcClient.WebApp\Views\Shared\_Layout.cshtml نمایش لینک افزودن تصویر را نیز محدود به PayingUser می‌کنیم:
@if(User.IsInRole("PayingUser"))
{
  <li><a asp-area="" asp-controller="Gallery" asp-action="AddImage">Add an image</a></li>
  <li><a asp-area="" asp-controller="Gallery" asp-action="OrderFrame">Order a framed picture</a></li>
}
علاوه بر آن، در کنترلر ImageGallery.MvcClient.WebApp\Controllers\GalleryController.cs نیاز است فیلتر Authorize زیر نیز به اکشن متد نمایش صفحه‌ی AddImage اضافه شود تا فراخوانی مستقیم آدرس آن در مرورگر، توسط سایر کاربران میسر نباشد:
namespace ImageGallery.MvcClient.WebApp.Controllers
{
    [Authorize]
    public class GalleryController : Controller
    {
        [Authorize(Roles = "PayingUser")]
        public IActionResult AddImage()
        {
            return View();
        }
این مورد را باید به متد AddImage در حالت دریافت اطلاعات از کاربر نیز افزود تا اگر شخصی مستقیما با این قسمت کار کرد، حتما سطح دسترسی او بررسی شود:
[HttpPost]
[Authorize(Roles = "PayingUser")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> AddImage(AddImageViewModel addImageViewModel)

برای آزمایش این قسمت یکبار از برنامه خارج شده و سپس با اکانت User 1 که PayingUser است به سیستم وارد شوید. در ادامه از منوی بالای سایت، گزینه‌ی Add an image را انتخاب کرده و تصویری را آپلود کنید. پس از آن، این تصویر آپلود شده را در لیست تصاویر صفحه‌ی اول سایت، مشاهده خواهید کرد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشه‌ی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشه‌ی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آن‌را اجرا کنید تا برنامه‌ی IDP راه اندازی شود.
- در آخر به پوشه‌ی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحه‌ی login نام کاربری را User 1 و کلمه‌ی عبور آن‌را password وارد کنید.
مطالب
مسیریابی در Angular - قسمت اول - معرفی
مسیریابی در +Angular 2 به همراه قابلیت‌های فراوان و ویژه‌ای است که تعدادی از آن‌ها را تابحال در این سایت بررسی کرده‌ایم. مورد مقدماتی اول، نیاز به بازنویسی کامل دارد، مورد دوم جهت آشنایی با ساختار Angular CLI مفید است و مورد سوم یکی از مباحث تکمیلی آن است. به همین جهت در طی یک سری، ویژگی‌های متعدد سیستم مسیریابی +Angular 2 را از ابتدا بررسی خواهیم کرد.


مسیریابی در +Angular 2

عموما از مسیریابی جهت حرکت بین Viewهای مختلف برنامه استفاده می‌شود، اما کارهای بیشتری را نیز می‌توان با آن انجام داد؛ مانند ارسال اطلاعات، به مسیریابی‌ها، پیش بارگذاری اطلاعات، جهت نمایش در Viewها، گروه بندی و محافظت از مسیریابی‌ها، پویانمایی و انیمیشن و همچنین بهبود کارآیی، با بارگذاری async مسیرهای مختلف.
کار سیستم مسیریاب +Angular 2 زمانی شروع می‌شود که تغییری را در آدرس درخواستی از برنامه مشاهده می‌کند؛ یا از طریق درخواست آدرسی توسط مرورگر و یا هدایت به قسمتی خاص، از طریق کدنویسی. سپس مسیریاب به آرایه‌ی تنظیم شده‌ی مسیرهای سیستم مراجعه می‌کند تا بتواند تطابقی را بین آدرس درخواستی و یکی از کلیدهای تنظیم شده‌ی در آن پیدا کند. در این حالت اگر تطابقی یافت نشود، کارمسیریابی خاتمه خواهد یافت. در غیراینصورت کار ادامه یافته و سپس مسیریاب، محافظ‌های مسیر درخواستی را بررسی می‌کند تا مشخص شود که آیا کاربر مجاز به هدایت به این قسمت خاص از برنامه هست یا خیر؟ در صورت مثبت بودن پاسخ، مرحله‌ی بعد، پیش بارگذاری اطلاعات درخواستی جهت نمایش View مرتبط است. در ادامه کامپوننت متناظر با مسیریابی فعالسازی می‌شود. سپس قالب این کامپوننت را در قسمتی که توسط router-outlet مشخص می‌گردد، جایگذاری کرده و نمایش می‌دهد.


تعریف مسیر پایه یا Base path

اولین مرحله‌ی کار با سیستم مسیریابی +Angular 2، تعریف یک base path است. مسیرپایه، به زیرپوشه‌ای اشاره می‌کند که برنامه‌ی ما در آن قرار گرفته‌است:
 www.mysite.com/myapp
در این مثال، مسیرپایه برنامه، /myapp/ است.
مسیریاب از این مسیرپایه جهت ساخت آدرس‌های مسیریابی استفاده می‌کند. مقدار آن نیز به صورت ذیل در فایل index.html برنامه، درست پس از تگ head تعیین می‌گردد:
<!DOCTYPE html>
<html>
<head>
    <base href="/">
در اینجا با ذکر / ، به عنوان مسیرپایه، به ریشه‌ی سایت اشاره شده‌است. اگر برنامه‌ی شما قرار است در یک زیرپوشه قرار بگیرد، در این حالت نحوه‌ی تعیین این زیر پوشه به صورت ذیل خواهد بود:
<base href="/myapp/">


تعیین مسیرپایه جهت ارائه‌ی نهایی

استفاده از مسیر پایه / برای حالت توسعه و همچنین زمانیکه برنامه‌ی نهایی شما در ریشه‌ی سایت توزیع می‌شود، بسیار مناسب است. اما اگر برای حالت توسعه از مقدار / و برای حالت توزیع از مقدار /myapp/ بخواهید استفاده کنید، مدام نیاز خواهید داشت تا فایل index.html نهایی سایت را ویرایش کنید. برای این منظور Angular CLI دارای پرچمی است به نام base-href:
 > ng build --base-href /myapp/
به این ترتیب در زمان ساخت و توزیع نهایی برنامه توسط Angular CLI، کار تنظیم خودکار مسیرپایه نیز صورت خواهد گرفت.
حالت پیش فرض تولید برنامه‌های Angular توسط Angular CLI، تنظیم مسیرپایه در فایل src\index.html به صورت خودکار به / می‌باشد.


تعریف مسیریاب Angular

مسیریاب Angular در ماژولی به نام RouterModule قرار گرفته‌است و باید در ابتدای کار import شود. این ماژول شامل سرویسی است جهت هدایت کاربران به صفحات دیگر و مدیریت URLها، تنظیماتی برای تعریف جزئیات مسیریابی‌ها و تعدادی دایرکتیو که برای فعالسازی و نمایش مسیرها از آن‌ها استفاده می‌شود. برای مثال دایرکتیو RouterLink آن یک المان قابل کلیک HTML را به مسیر و کامپوننتی خاص در برنامه متصل می‌کند. RouterLinkActive، شیوه‌نامه‌ها را به لینک فعال انتساب می‌دهد و RouterOutlet محل نمایش قالب کامپوننت فعال شده را مشخص می‌کند.

یک مثال: در ادامه، یک پروژه‌ی جدید مبتنی بر Angular CLI را به نام angular-routing-lab به همراه تنظیمات ابتدایی مسیریابی آن ایجاد می‌کنیم:
 > ng new angular-routing-lab --routing
به علاوه قصد استفاده‌ی از بوت استرپ را نیز داریم. به همین جهت ابتدا به ریشه‌ی پروژه وارد شده و سپس دستور ذیل را صادر کنید تا بوت استرپ نیز نصب شود و پرچم save آن سبب به روز رسانی فایل package.json نیز گردد:
 > npm install bootstrap --save
پس از آن نیاز است به فایل angular-cli.json. مراجعه کرده و شیوه‌نامه‌ی بوت استرپ را تعریف کنیم:
  "apps": [
    {
      "styles": [
    "../node_modules/bootstrap/dist/css/bootstrap.min.css",
        "styles.css"
      ],
به این ترتیب به صورت خودکار این شیوه نامه به همراه توزیع برنامه حضور خواهد داشت و نیازی به تعریف مستقیم آن در فایل index.html نیست.

در ادامه اگر به فایل src\app\app-routing.module.ts مراجعه کنید، یک چنین محتوایی را خواهید یافت:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  {
    path: '',
    children: []
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
در اینجا برای شروع به کار با سیستم مسیریابی، RouterModule معرفی شده‌است. ابتدا import آن‌را مشاهده می‌کنید و سپس این ماژول به قسمت imports مربوط به NgModule اضافه شده‌است.
می‌توان این قسمت را خلاصه کرد و فایل app-routing.module.ts را نیز حذف کرد و سپس import لازم و تعریف ماژول آن‌را به ماژول آغازین برنامه یا همان src\app\app.module.ts نیز منتقل کرد. اما پس از مدتی تنظیمات مسیریابی آن، فایل ماژول اصلی برنامه را بیش از اندازه شلوغ خواهند کرد. بنابراین Angular-CLI تصمیم به ایجاد یک ماژول مستقل را برای تعریف تنظیمات مسیریابی برنامه گرفته‌است. سپس تعریف آن را به فایل src\app\app.module.ts به صورت خودکار اضافه می‌کند:
import { AppRoutingModule } from './app-routing.module';

@NgModule({
  imports: [
    AppRoutingModule
  ],

اگر به قسمت import مربوط به NgModule فایل src\app\app-routing.module.ts دقت کنید، این ماژول به همراه متد forRoot معرفی شده‌است.
@NgModule({
  imports: [RouterModule.forRoot(routes)],
ماژول مسیریابی به همراه دو متد ذیل است:
الف) forRoot
- کار آن تعریف دایرکتیوهای مسیریابی، مدیریت تنظیمات مسیریابی و ثبت سرویس مسیریابی است.
- نکته‌ی مهم اینجا است که متد forRoot تنها یکبار باید در طول عمر یک برنامه تعریف شود.
- این متد آرایه‌ای از تنظیمات مسیریابی‌های تعریف شده را دریافت می‌کند.
ب) forChild
- کار آن تعریف دایرکتیوهای مسیریابی و مدیریت تنظیمات مسیریابی است؛ اما سرویس مسیریابی را مجددا ثبت نمی‌کند.
- از این متد در جهت تعریف مسیریابی‌های ماژول‌های ویژگی‌های مختلف برنامه و نظم بخشیدن به آن‌ها استفاده می‌شود.

بنابراین زمانیکه از forRoot استفاده می‌شود، سرویس مسیریابی تنها یکبار ثبت خواهد شد و تنها یک وهله از آن موجود خواهد بود. در ادامه هر کدام از ماژول‌های دیگر برنامه می‌توانند forChild خاص خودشان را داشته باشند.

اکنون تمام کامپوننت‌های قید شده‌ی در قسمت declaration، امکان دسترسی به دایرکتیوهای مسیریابی را پیدا می‌کنند. همچنین از آنجائیکه AppRoutingModule به همراه متد forRoot است، سرویس مسیریابی نیز در کل برنامه قابل دسترسی است.


تنظیمات اولیه مسیریابی برنامه

آرایه‌ی const routes: Routes فایل src\app\app-routing.module.ts در ابتدای کار خالی است. در اینجا کار تعریف URL segments و سپس اتصال آن‌ها به کامپوننت‌های متناظری جهت فعالسازی و نمایش قالب آن‌ها صورت می‌گیرد. این نمایش نیز در محل router-outlet تعریف شده‌ی در فایل src\app\app.component.html انجام می‌شود:
<h1>
  {{title}}
</h1>
<router-outlet></router-outlet>
به ازای هر router-outlet، تنها یک مسیر را در هر زمان می‌توان نمایش داد. بنابراین با فعال شدن هر کامپوننت، قالب آن، قالب کامپوننت پیشین را در router-outlet جایگزین خواهد کرد. امکان تعریف router-outletهای دیگری نیز وجود دارند که به آن‌ها secondary routes گفته می‌شود.

در ادامه برای تکمیل مثال جاری، دو کامپوننت جدید خوش‌آمد گویی و همچنین یافتن نشدن مسیرها را به برنامه اضافه می‌کنیم:
>ng g c welcome
>ng g c PageNotFound
که سبب ایجاد کامپوننت‌های src\app\welcome\welcome.component.ts و src\app\page-not-found\page-not-found.component.ts خواهند شد، به همراه به روز رسانی خودکار فایل src\app\app.module.ts جهت تکمیل قسمت declarations آن:
@NgModule({
  declarations: [
    AppComponent,
    WelcomeComponent,
    PageNotFoundComponent
  ],

سپس فایل src\app\app-routing.module.ts را به نحو ذیل تکمیل نمائید:
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
import { WelcomeComponent } from './welcome/welcome.component';
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  { path: 'welcome', component: WelcomeComponent },
  { path: '', redirectTo: 'welcome', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
در اینجا در ابتدا دو کامپوننت جدید تعریف شده، import شده‌اند.

یک نکته: افزونه‌ی auto import، کار تعریف کامپوننت‌ها را در VSCode بسیار ساده می‌کند و امکان تشکیل خودکار قسمت import را با ارائه‌ی یک intellisense به همراه دارد.

سپس کار تکمیل آرایه‌ی Routes انجام شده‌است. همانطور که مشاهده می‌کنید، این آرایه متشکل است از اشیایی که به همراه خاصیت path و سایر پارامترهای مورد نیاز هستند.
کار خاصیت path، تعیین URL segment متناظری است که این مسیریابی را فعال می‌کند. برای مثال اولین شیء تعریف شده با آدرس‌هایی مانند www.mysite.com/welcome متناظر است.
 { path: 'welcome', component: WelcomeComponent },
در اغلب حالات به همراه path، خاصیت کامپوننت متناظر با آن مسیر نیز ذکر می‌شود؛ مانندWelcomeComponent ایی که در اینجا مقدار دهی شده‌است. هنگامیکه این مسیریابی فعال می‌شود، قالب این کامپوننت‌است که در router-outlet نمایش داده خواهد شد (فایل src\app\welcome\welcome.component.html).

چند نکته:
- در حین تعریف مقدار خاصیت path، هیچ / آغاز کننده‌ای تعریف نشده‌است.
- مقدار خاصیت path، حساس به کوچکی و بزرگی حروف است.
- WelcomeComponent تعریف شده، یک رشته نیست و ارجاعی را به کامپوننت مرتبط دارد. به همین جهت نیاز به import statement ابتدایی را دارد و وجود آن توسط کامپایلر بررسی می‌شود.


تعیین مسیریابی پیش فرض سایت

اما زمانیکه برنامه برای بار اول بارگذاری می‌شود، چطور؟ در این حالت هیچ URL segment ایی وجود ندارد. بنابراین برای تنظیم مسیرپیش فرض سایت، خاصیت path، به یک رشته‌ی خالی همانند دومین شیء تنظیمات مسیریابی، تنظیم می‌شود:
   { path: '', redirectTo: 'welcome', pathMatch: 'full' },
در اینجا زمانیکه کاربر، ریشه‌ی سایت را درخواست می‌کند، به کامپوننت welcome هدایت خواهد شد. در این تنظیم خاصیت pathMatch نحوه‌ی تطابق با خاصیت path را مشخص می‌کند. مقدار full نیز به این معنا است که اگر تمام URL segment دریافتی خالی بود، از این تنظیم استفاده کن.


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

تنظیم سومی را نیز در اینجا مشاهده می‌کنید:
 { path: '**', component: PageNotFoundComponent },
** به این معنا است که اگر مسیریاب موفق به تطابق URL segment دریافتی، با هیچکدام از pathهای تعریف شده نبود، از این تنظیم استفاده کند (catch all). برای مثال اگر کاربر آدرس www.mysite.com/someAddr را درخواست کند، چون URL segment آن در آرایه‌ی تنظیمات، تعریف نشده‌است، می‌خواهیم یک صفحه‌ی یافت نشد را نمایش دهیم.

یک نکته: ترتیب مسیریابی‌ها در آرایه‌ی تعریف آن‌ها اهمیت دارد. در اینجا از استراتژی «اولین تطابق یافته‌، برنده خواهد بود» استفاده می‌شود. بنابراین تنظیم ** باید در انتهای لیست ذکر شود؛ در غیراینصورت هیچکدام از مسیریابی‌های تعریف شده‌ی پس از آن پردازش نخواهند شد.


مدیریت تغییرات آدرس‌های برنامه

در طول عمر برنامه ممکن است نیاز به تغییر آدرس‌های برنامه باشد. برای مثال بجای مسیر welcome مسیر home نمایش داده شود:
const routes: Routes = [
  { path: 'home', component: WelcomeComponent },
  { path: 'welcome', redirectTo: 'home', pathMatch: 'full' },
  { path: '', redirectTo: 'welcome', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent }
];
در این تنظیمات جدید برنامه، مسیر home به WelcomeComponent اشاره می‌کند. اما جهت هدایت آدرس‌های قدیمی welcome، به این آدرس جدید home می‌توان از redirectTo، مانند دومین شیء مسیریابی اضافه شده‌ی به آرایه‌ی تنظیمات استفاده کرد. به این ترتیب هم تغییرات آرایه‌ی Routes به حداقل می‌رسد و هم آدرس‌های ذخیره شده‌ی توسط کاربران هنوز هم معتبر خواهند بود.

نکته: redirectToها قابلیت تعریف زنجیره‌ای را ندارند. به این معنا که اگر ریشه‌ی سایت درخواست شود، ابتدا به مسیر welcome هدایت خواهیم شد. مسیر welcome هم یک redirectTo دیگر به مسیر home را دارد. اما در اینجا کار به این redirectTo دوم نخواهد رسید و این پردازش، زنجیره‌ای نیست. بنابراین مسیریابی پیش‌فرض را نیز باید ویرایش کرد و به home تغییر داد:
const routes: Routes = [
  { path: 'home', component: WelcomeComponent },
  { path: 'welcome', redirectTo: 'home', pathMatch: 'full' },
  { path: '', redirectTo: 'home', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent }
];
به این ترتیب دیگر درخواست زنجیره‌وار redirectToها رخ نخواهد داد.

نکته: redirectToها می‌توانند local و یا absolute باشند. تعریف محلی آن‌ها مانند ذکر home و welcome در اینجا است و تنها سبب تغییر یک URL segment می‌شود. اما اگر در ابتدای مقادیر redirectToها یک / قرار دهیم، به معنای تعریف یک مسیر مطلق است و کل URL را جایگزین می‌کند.


تعیین محل نمایش قالب‌های کامپوننت‌ها

زمانیکه یک کامپوننت فعالسازی می‌شود، قالب آن در router-outlet نمایش داده خواهد شد. برای این منظور فایل src\app\app.component.html را گشوده و به نحو ذیل تغییر دهید:
<nav class="navbar navbar-default">
  <div class="container-fluid">
    <a class="navbar-brand">{{title}}</a>
    <ul class="nav navbar-nav">
      <li>
        <a [routerLink]="['/home']">Home</a>
      </li>
    </ul>
  </div>
</nav>
<div class="container">
  <router-outlet></router-outlet>
</div>
در اینجا دایرکتیو router-outlet محلی است که قالب کامپوننت فعالسازی شده، نمایش داده می‌شود. برای مثال اگر کاربر بر روی لینک Home کلیک کند، کامپوننت متناظر با آن از طریق تنظیمات مسیریابی یافت شده و فعالسازی می‌شود و سپس قالب آن در محل router-outlet رندر خواهد شد.

یک نکته: چون کامپوننت welcome از طریق مسیریابی نمایش داده می‌شود و دیگر به صورت مستقیم با درج تگ selector آن در صفحه فعالسازی نخواهد شد، می‌توان به تعریف کامپوننت آن مراجعه کرده و selector آن‌را حذف کرد.
 @Component({
//selector: 'app-welcome',
templateUrl: './welcome.component.html',
styleUrls: ['./welcome.component.css']
})

تا اینجا اگر دستور ng serve -o را صادر کنیم (کار build درون حافظه‌ای جهت محیط توسعه و نمایش خودکار برنامه در مرورگر)، چنین خروجی در مرورگر نمایان خواهد شد:


اگر به آدرس تنظیم شده‌ی در مرورگر دقت کنید، http://localhost:4200/home آدرسی است که در ابتدای نمایش سایت نمایان خواهد شد. علت آن نیز به تنظیم مسیریابی پیش فرض سایت برمی‌گردد.
و اگر یک مسیر غیرموجود را درخواست دهیم، قالب کامپوننت PageNotFound ظاهر می‌شود:



هدایت کاربران به قسمت‌های مختلف برنامه

کاربران را می‌توان به روش‌های مختلفی به قسمت‌های گوناگون برنامه هدایت کرد؛ برای مثال با کلیک بر روی المان‌های قابل کلیک HTML و سپس اتصال آن‌ها به کامپوننت‌های برنامه. استفاده‌ی کاربر از bookmark مرورگر و یا ورود مستقیم و دستی آدرس قسمتی از برنامه و یا کلیک بر روی دکمه‌های forward و back مرورگر. تنها مورد اول است که نیاز به تنظیم دارد و سایر قسمت‌ها به صورت خودکار مدیریت خواهند شد. نمونه‌ی آن‌را نیز با تعریف لینک Home پیشتر مشاهده کردید:
  <a [routerLink]="['/home']">Home</a>
- در اینجا با استفاده از یک دایرکتیو ویژه به نام routerLink، المان‌های قابل کلیک HTML را به خاصیت‌های path تعریف شده‌ی در تنظیمات مسیریابی برنامه متصل می‌کنیم. routerLink یک attribute directive است؛ بنابراین می‌توان آن‌را به یک المان قابل کلیک anchor tag، مانند مثال فوق نسبت داد.
- زمانیکه کاربر بر روی این لینک کلیک می‌کند، اولین path متناظر با routerLink یافت شده و فعالسازی خواهد شد.
- علت تعریف مقدار routerLink به صورت [] این است که آرایه‌ی پارامترهای لینک را مشخص می‌کند. بنابراین چون آرایه‌است، نیاز به [] دارد. اولین پارامتر این آرایه مفهوم root URL segment را دارد. در اینجا حتما نیاز است URL segment را با یک / شروع کرد. به علاوه باید دقت داشت که خاصیت path تنظیمات مسیریابی، حساس به حروف کوچک و بزرگ است. بنابراین این مورد را باید در اینجا نیز مدنظر داشت.
- پارامترهای دیگر routerLink می‌توانند مفهوم پارامترهای این segment و یا حتی segments دیگری باشند.

یک نکته: چون در مثال فوق، آرایه‌ی تعریف شده تنها دارای یک عضو است، آن‌را می‌توان به صورت ذیل نیز خلاصه نویسی کرد (one-time binding):
 <a routerLink="/home">Home</a>


تفاوت بین آدرس‌های HTML 5 و Hash-based

زمانیکه مسیریاب Angular کار پردازش آدرس‌های رسیده را انجام می‌دهد، اینکار در سمت کلاینت صورت می‌گیرد و تنها URL segment مدنظر را تغییر داده و این درخواست را به سمت سرور ارسال نمی‌کند. به همین جهت سبب reload صفحه نمی‌شود. دو روش در اینجا جهت مدیریت سمت کلاینت آدرس‌ها قابل استفاده است:
الف) HTML 5 Style
- آدرسی مانند http://localhost:4200/home، یک آدرس به شیوه‌ی HTML 5 است. در اینجا مسیریاب Angular با استفاده از HTML 5 history pushState سبب به روز رسانی History مرورگر شده و آدرس‌ها را بدون ارسال درخواستی به سمت سرور، در همان سمت کلاینت تغییر می‌دهد.
- این روش حالت پیش فرض Angular است و نحوه‌ی نمایش آن بسیار طبیعی به نظر می‌رسد.
- در اینجا URL rewriting سمت سرور نیز جهت هدایت آدرس‌ها، به برنامه‌ی Angular ضروری است. برای مثال زمانیکه کاربری آدرس http://localhost:4200/home را مستقیما در مرورگر وارد می‌کند، این درخواست ابتدا به سمت سرور ارسال خواهد شد و چون چنین صفحه‌ای در سمت سرور وجود ندارد، پیغام خطای 404 را دریافت می‌کند. اینجا است که URL rewriting سمت سرور به فایل index.html برنامه، جهت مدیریت یک چنین حالت‌هایی ضروری است.
برای نمونه اگر از وب سرور IIS استفاده می‌کنید، تنظیم ذیل را به فایل web.config در قسمت system.webServer اضافه کنید (کار کرد آن هم وابسته‌است به نصب و فعالسازی ماژول URL Rewrite بر روی IIS):
<rewrite>
    <rules>
        <rule name="Angular 2+ pushState routing" stopProcessing="true">
            <match url=".*" />
                <conditions logicalGrouping="MatchAll">
                    <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
                    <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
                    <add input="{REQUEST_FILENAME}" pattern=".*\.[\d\w]+$" negate="true" />
                    <add input="{REQUEST_URI}" pattern="^/(api)" negate="true" />
                </conditions>
                <action type="Rewrite" url="/index.html" />
        </rule>
    </rules>
</rewrite>

ب) Hash-based
- آدرسی مانند http://localhost:4200/#/home یک آدرس به شیوه‌ی Hash-based بوده و مخصوص مرورگرهایی است بسیار قدیمی که از HTML 5 پشتیبانی نمی‌کنند. اینبار قطعات قرار گرفته‌ی پس از علامت # دارای نام URL fragments بوده و قابلیت پردازش در سمت کلاینت را دارا می‌باشند.
- اگر علاقمند به استفاده‌ی از این روش هستید، نیاز است خاصیت useHash را به true تنظیم کنید:
@NgModule({
   imports: [RouterModule.forRoot(routes, { useHash: true })],
-  این روش نیازی به URL rewriting سمت سرور ندارد. از آنجائیکه سرور هرچیزی را که پس # باشد ندید خواهد گرفت و سعی در یافتن آن‌، در سمت سرور نخواهد کرد.
مطالب
بازیابی پایگاه داده (database recovery)

در این مقاله آموزشی که یکی دیگر از سری مقالات آموزشی اصول و مبانی پایگاه داده پیشرفته می‌باشد، قصد داریم به یکی دیگر از مقوله‌های مهم در طراحی سیستم‌های مدیریت پایگاه داده (DBMS) بپردازیم. همانطور که در مباحث قبلی  بیان کردیم یکی از وظایف سیستم مدیریت پایگاه داده، حفظ سازگاری(consistency) داده‌ها می‌باشد. برای مثال یکی از راهکار هایی که برای این منظور ارائه می‌دهد انجام عملیات در قالب تراکنش هاست که در مبحث مربوط به تراکنش ها مفصل در مورد آن بحث کردیم. با این حال گاهی خطا‌ها و شکست هایی (failure) در حین عملیات ممکن است پیش بیاید که منجر به خروج سیستم از وضعیت سازگار خود گردد. بعنوان مثال ممکن است سخت افزار سیستم دچار مشکل شود، مثلا دیسک از کار بیفتد (disk crash) یا آنکه برق قطع شود. خطاهای نرم افزاری نیز می‌توانند جزو موارد شکست و خرابی بحساب آیند که خطای منطق برنامه (logic) از این نمونه می‌باشد. در چنین شرایطی بحثی مطرح می‌شود تحت عنوان بازیابی  (recovery)  و ترمیم پایگاه داده که در این مقاله قصد داریم در مورد آن صحبت کنیم. بنا به تعریف بازیابی به معنای بازگرداندن یک پایگاه داده به وضعیت سازگار گذشته خود، بعد از وقوع یک شکست یا خرابی است. توجه داشته باشید که اهمیت بازیابی و ترمیم پایگاه داده تا آنجایی است که حدود 10 درصد از سیستم‌های مدیریت پایگاه داده را به خود اختصاص می‌دهند. 

آنچه که در اینجا در مورد آن صحبت خواهیم کرد بازیابی بصورت نرم افزاری است که از آن تحت عنوان fail soft نام برده می‌شود. دقت داشته باشید در بیشتر مواقع می‌توان از طریق نرم افزاری عمل بازیابی را انجام داد، اما در کنار راهکار‌های نرم افزاری باید حتما اقدامات سخت افزاری ضروری نیز پیش بینی شود. بعنوان مثال گرفتن نسخه‌های پشتیبان یک امر ضروری در سیستم‌های اطلاعاتی است. چرا که گاهی اوقات خرابی‌های فیزیکی باعث از دست رفتن تمامی اطلاعات می‌گردند که در این صورت نسخه‌های پشتیبان می‌توانند به کمک آیند و با کمک آنها سیستم را مجدد بازیابی کرد. در شکل زیر نمونه ای از روش‌های پشتیبان گیری بنام mirroring نشان داده شده است که روش رایجی در سیستم‌های بانک اطلاعاتی بشمار می‌رود. همانطور که در شکل نشان داده شده است در کنار نسخه اصلی (DISK)، نسخه(MIRROR) آن  قرار داده شده است. این دو نسخه کاملا مشابه یکدیگرند و هر عملی که در DICK انجام می‌شود در MIRROR ان نیز اعمال می‌شود تا در مواقع خرابی DISK بتوان از نسخه MIRROR استفاده نمود. 

در شکل زیر نمونه بسیار ساده از نحوه لاگ کردن در حین اجرای تراکنش‌ها را مشاهده می‌کنید. 

نیازمندی‌های اصلی در بازیابی پایگاه داده

برای آنکه وارد بحث اصلی شویم باید بگویم در یک نگاه کلی می‌توان گفت که ساختار زیر سیستم بازیابی پایگاه داده بر پایه سه عملیات استوار است که عبارتند از  log ،  redo  و  undo . برای آنکه بتوان در هنگام رخ دادن خطا عمل ترمیم و بازیابی را انجام داد، سیستم پایگاه داده با استفاده از مکانیزم لاگ کردن(logging) خود تمامی عملیاتی را که در پایگاه داده رخ می‌دهد و بنحوی منجر به تغییر وضعیت ان می‌گردد را در جایی ثبت و نگهداری می‌کند. اهمیت لاگ کردن وقایع بسیار بالاست، چرا که پس از رخ دادن شکست در سیستم ملاک ما برای بازیابی و ترمیم فایل‌های لاگ  (log files)  می باشند.

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

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

Recovery Manager

مسئولیت انجام بازیابی بصورت نرم افزاری (fail soft) بر عهده زیر سیستمی از DBMS بنام مدیر بازیابی (recovery manager) می باشد و همانطور که اشاره شد این زیر سیستم چیزی در حدود 10 در صد DBMSرا به خود اختصاص می‌دهد. برای آنکه این زیر سیستم بتواند مسئولیت خود را بنحو احسن انجام دهد بطوری که عمل بازیابی بدون نقص و قابل اعتماد باشد، باید به نکاتی توجه نمود. اولین نکته اینست که در لاگ کردن و همچنین خواندن لاگ فایل به جهت بازیابی و ترمیم پایگاه داده هیچ تراکنشی نباید از قلم بیفتد. تمامی تراکنش‌ها در طول حیات سیستم باید لاگ شود تا بازیابی ما قابل اعتماد و بدون نقص باشد. نکته دوم اینست که اگر تصمیم به اجرای مجدد (redo) تراکنشی گرفته شد، طوری باید عمل Redo انجام شود که بلحاظ منطقی آن تراکنش یک بار انجام شود و تاثیرش یکبار بر دیتابیس اعمال گردد. بعنوان مثال فرض کنید که در طی یک تراکنش مبلغ یک میلیون تومان به حساب شخصی واریز می‌شود. مدتی بعد از اجرای و تمکیل تراکنش سیستم دچار مشکل می‌شود و مجبور به انجام بازیابی می‌شویم. در حین عمل بازیابی سیستم مدیریت بازیابی و ترمیم تصمیم به اجرای مجدد تراکنش مذکور می‌گیرد. در اینجا سیستم نباید مجدد یک میلیون تومان دیگر به حساب ان شخص واریز کند. چرا که در این صورت موجودی حساب فرد دو میلیون تومان خواهد شد که این اشتباه است. سیستم باید طوری عمل کند که پس از انجام مجدد تراکنش باز هم موجودی همان یک میلیون تومان باشد. یعنی مثلا ابتدا یک میلیون کسر و سپس یک میلیون به آن اضافه کند. این مسئله نکته بسیار مهمی است که طراحان DBMS باید حتما آن را مد نظر قرار دهند.

لاگ کردن:

همانطور که گفته شد هر تغییری که در پایگاه داده رخ می‌دهد باید لاگ شود. لاگ کردن به این معنی است که هر گونه عملیاتی که در پایگاه داده انجام می‌شود در فایل هایی به نام فایل لاگ (log file) ذخیره شود. توجه داشته باشید  لاگ فایل‌ها در بسیاری از سیستم‌های نرم افزاری دیگر نیز استفاده می‌شود. بعنوان مثال در سیستم عامل ما انواع مختلفی فایل لاگ داریم. بعنوان نمونه یک فراخوانی سیستمی (system call) که در سیستم عامل توسط کاربر انجام می‌شود در فایلی مخصوص لاگ می‌شود. یکی از کاربرد این لاگ فایل شناسایی کاربران بد و خرابکار (malicious users) می تواند باشد که کارهای تحقیقاتی زیادی هم در این رابطه انجام شده و میشود. بدین صورت که می‌توان با بررسی این فایل لاگ و آنالیز فراخوانی‌های یک کاربر بدنبال فراخوانی هایی غیر عادی گشت و از این طریق تشخیص داد که کاربر بدنبال خرابکاری بوده یا خیر. مشابه چنین فایل هایی در DBMS نیز وجود دارد که هدف نهایی تمامی انها حفظ صحت، سازگاری و امنیت اطلاعات می‌باشد.

حال ببینیم در لاگ فایل مربوط به بازیابی اطلاعات چه چیز هایی نوشته می‌شود. در طول حیات پایگاه داده عملیات بسیار گوناگونی انجام می‌گیرد که جزئیات تمامی آنها باید لاگ شود. بعنوان مثال هنگامی که رکوردی درج می‌شود در لاگ فایل باید مشخص شود که در چه زمانی، توسط چه کاربری چه رکوردی، با چه شناسه ای به کدام جدول از دیتابیس اضافه شد. یا اینکه در موقع حذف باید مشخص شود چه رکوردی از چه جدولی حذف شده است. در هنگام بروز رسانی (update) باید علاوه بر مواردی که در درج لاگ می‌کنیم نام فیلد ویرایش شده، مقدار قبلی و مقدار جدید آن نیز مشخص شود. تمامی عملیات ریز لاگ می‌شوند و هیچ عملی نباید از قلم بیفتد. بنابراین فایل لاگ با سرعت زیاد بزرگ خواهد و اندازه دیتابیس نیز افزایش خواهد یافت. این افزایش اندازه مشکل ساز می‌تواند باشد. چراکه معمولا فضایی که ما بر روی دیسک به دیتابیس اختصاص می‌دهیم فضایی محدود است. بهمین دلیل به لحاظ فیزیکی نمی‌توان فایل لاگی با اندازه نامحدود داشت. این در حالی است که چنین فایل هایی باید نامحدود باشند تا همه چیز را در خود ثبت نمایند. برای پیاده سازی ظرفیت نامحدود به لحاظ منطقی یکی از روش‌ها پیاده سازی فایل‌های حلقه ای(circular) است. بدین صورت که هنگامی که سیستم به انتهای فایل لاگ می‌رسد مجددا به ابتدا آن بر می‌گردد و از ابتدا شروع به نوشتن می‌کند. البته چنین ساختار هایی بدون اشکال نیستند. چرا که پس از رسیدن به انتهای فایل و شروع مجدد از ابتدا ما برخی از تراکنش‌های گذشته را از دست خواهیم داد. این مسئله یکی از دلایلی است که بر اساس آن پیشنهاد می‌شود تا جایی که امکان دارد تراکنش‌ها را کوچک پیاده سازی کنیم. گاهی اوقات بر روی لاگ فایل عمل فشرده سازی را نیز انجام می‌دهند. البته فشرده سازی بمعنای رایج ان مطرح نیست. بلکه منظور از فشرده سازی آنست که رکورد هایی که غیر ضروری هستند را حذف کنیم. بعنوان مثال فرض کنید رکوردی را از 50 به 60 تغییر داده ایم. مجددا همان رکورد را از 60 به 70 تغییر می‌دهیم. در این صورت برای این عملیات دو رکورد در فایل لاگ ثبت شده است که در هنگام فشرده سازی در صورت امکان می‌توان ان دو را به یک رکورد تبدیل نمود (تغییر از 50 به 70 را بجای ان دو لاگ کرد). بعنوان مثال دیگر فرض کنید تراکنشی در گذشته دور انجام شده است و با موفقیت کامیت شده است. می‌توان رکورد‌های لاگ مربوط به این تراکنش را نیز بنا به شرایط حذف کرد.

دقت داشته باشید که ما عملیاتی مانند عملیات محاسباتی را در این لاگ فایل ثبت نمی‌کنیم. بعنوان مثال اگر دو فیلد با هم باید جمع شوند و نتیجه در فیلدی باید بروز گردد، جمع دو فیل را در سیستم لاگ نمی‌کنیم بلکه تنها مقدار نهایی ویرایش شده را ثبت می‌کنیم. چرا که عملیات محاسباتی در بازیابی ضروری نیستند و ثبت انها تنها باعث بزرگ شدن فایل می‌شود.

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

انواع رکورد‌های لاگ فایل :

در فایل لاگ رکورد‌های مختلفی  ممکن است درج شود که در این جا به چند نمونه از انها اشاره می‌کنیم:

  • [start-transaction, T]
  • [write-item, T, X, old-value, new-value]
  • [read-item, T, X]
  • [commit, T]

در آیتم‌های بالا منظور از  T  شناسه تراکنش است،  X  نیز می‌تواند شامل نام دیتابیس، نام جدول، شماره رکورد و فیلد‌ها باشد. البته توجه داشته باشید که این‌ها تنها نمونه هایی از رکورد‌های فایل‌های لاگ هستند که در اینجا آورده شده اند. بعنوان مثال رکورد مربوط به عملیات نوشتن خود شامل سه رکورد درج، حذف و بروز رسانی می‌شود.

در شکل زیر نمونه بسیار ساده از نحوه لاگ کردن در حین اجرای تراکنش‌ها را مشاهده می‌کنید.

در  این شکل نکته ای وجود دارد که به آن اشاره ای می‌کنیم. همانطور که میبینید در شکل از اصطلاحimmediate update استفاده شده است. در برخی از سیستم‌ها تغییرات تراکنش‌ها بصورت فوری اعمال میشوند که اصطلاحا می‌گوییم immediate updates دارند. در مقابل این اصطلاح ما deffered را داریم. در این مدل تغییرات در انتهای کار اعمال می‌شوند (در زمان commit). 

Write-Ahead Log (WAL) :

بر اساس آنچه تابحال گفته شد هر تغییری در پایگاه داده شامل دو عمل می‌شود. یکی انجام تغییر (اجرای تراکنش) و دیگری ثبت آن در لاگ فایل. حال سوالی که ممکن است مطرح شود اینست  که کدامیک از این دو کار بر دیگری تقدم دارد؟ آیا اول تراکنش را باید اجرا کرد و سپس لاگ آن را نوشت و یا برعکس باید عمل کرد. یعنی پیش از هر تراکنشی ابتدا باید لاگ آن را ثبت کرد و سپس تراکنش را اجرا نمود. بر همین اساس سیاستی تعریف می‌شود بنام سیاست write-ahead log یا WAL که سوال دوم را تایید می‌کند. یعنی می‌گوید هنگامی که قرار است عملی در پایگاه داده صورت گیرد ابتدا باید ان عمل بطور کامل لاگ شود و سپس آن را اجرا نمود. این سیاست هدفی را دنبال می‌کند. 

پیش از آنکه هدف این سیاست را توضیح دهیم لازم است نکته ای در مورد عملیات redo و  undo بیان شود. شما با این دو عملیات در برنامه‌های مختلفی مانند آفیس، فتوشاپ و غیره آشنایی دارید. اما توجه داشته باشید که در DBMS این دو عملیات از پیچیدگی بیشتری برخوردار می‌باشند. اصطلاحا در پایگاه داده گفته میشود که عملیات redo و undo باید idempotent باشند. معنی idempotent بودن اینست که اگر قرار است تراکنشی در پایگاه داده undo شود، اگر بار‌ها و بارها عمل undo را بر روی آن تراکنش انجام دهیم مانند این باشد این عمل را تنها یکبار انجام داده ایم. در مورد redo نیز این مسئله صادق است. 

در تعریف idempotent بودن ویژگی‌های دیگری نیز وجود دارد. بعنوان مثال گفته می‌شود undo بر روی عملی که هنوز انجام نشده هیچ تاثیری نخواهد داشت. این مسئله یکی از دلایل اهمیت استفاده از سیاستWAL را بیان می‌کند. بعنوان مثال فرض کنید می‌خواهیم رکوردی را در جدولی درج کنیم. همانطور که گفتیم دو روش برای این منظور وجود  دارد. در روش اول ابتدا رکورد را در جدول مورد نظر درج می‌کنیم و سپس لاگ آن را می‌نویسیم. در این صورت اگر پس از درج رکورد سیستم با مشکل مواجه شود و مجبور به انجام عمل بازیابی شویم، بدلیل آنکه برای بازیابی بر اساس لاگ فایل عمل می‌کنیم و برای درج آن رکورد لاگی در سیستم ثبت نشده است، آن عمل را از دست می‌دهیم. در نتیجه بازیابی بطور کامل نمی‌تواند سیستم را ترمیم نماید. چراکه درج صورت گرفته اما لاگی برای آن ثبت نشده است. در روش دوم فرض کنید بر اساس سیاست WAL عمل می‌کنیم. ابتدا لاگ مربوط به درج رکورد را می‌نویسم. سپس پیش از آنکه عمل درج را انجام دهیم سیستم crash می کند و مجبور به بازیابی می‌شویم. دراین صورت هنگامی که Recovery Manager به رکورد مربوط به عمل درج در لاگ فایل می‌رسد یا باید آن را redo کند و یا undo (بعدا می‌گوییم بر چه اساس تصمیم گیری می‌کند). اگر تصمیم به undo کردن بگیرد بدلیل ویژگی گفته شده، عمل undo بر روی عملی که انجام نشده است هیچ تاثیری در پایگاه داده نخواهد گذاشت. اگر عمل redo را بخواهد انجام دهد نیز بدلیل آنکه لاگ مربوط به عمل درج در سیستم ثبت شده بدون هیچ مشکلی این عمل مجددا انجام می‌گیرد. بنابراین بر خلاف روش قبل هیچ تراکنشی را از دست نمی‌دهیم و سیستم بطور کامل بازیابی و ترمیم می‌شود. به این دلیل است که توصیه می‌شود در طراحیDBMS ها سیاست WAL بکار گیری شود. 

نکته بسیار مهمی که در اینجا ذکر آن ضروری بنظر می‌رسد اینست که در هنگام لاگ کردن تراکنش ها، علاوه بر آنکه خود تراکنش لاگ می‌شود و این لاگ‌ها نیز در فایل فیزیکی باید نوشته شوند، عملیات لازم برای Redo کردن و یا undo کردن آن نیز لاگ می‌شود تا سیستم در هنگام بازیابی بداند که چه کاری برایredo و undo کردن باید انجام دهد. توجه داشته باشید در این سیاست، COMMIT تراکنشی انجام نمی‌شود مگر انکه تمامی لاگ‌های مربوط به عملیات redo و undo آن تراکنش در لاگ فایل فیزیکی ثبت شود. 

قرار دادن  checkpoint  در لاگ فایل:

گفتیم که در هنگام رخ دادن یک خطا، برای بازیابی و ترمیم پایگاه داده به لاگ فایل مراجعه می‌کنیم و بر اساس تراکنش هایی که در آن ثبت شده است، عمل ترمیم را انجام می‌دهیم. علاوه بر آن، این را هم گفتیم که لاگ فایل، معمولا فایلی بزرگ است که از نظر منطقی با ظرفیت بینهایت پیاده سازی می‌شود. حال سوال اینجاست که اگر  بعد گذشت ساعت‌ها از عمر پایگاه داده و ثبت رکورد‌های متعدد در لاگ فایل خطایی رخ داد، آیا مدیر بازیابی و ترمیم پایگاه داده باید از ابتدای لاگ فایل شروع به خواندن و بازیابی نماید؟ اگر چنین باشد در بانک‌های اطلاعاتی بسیار بزرگ عمل بازیابی بسیار زمان بر و پر هزینه خواهد بود. برای جلوگیری از این کار مدیر بازیابی پایگاه داده وظیفه دارد در فواصل مشخصی در لاگ فایل نقاطی را علامت گذاری کند تا اگر خطایی رخ داد عمل undo کردن تراکنش را تنها تا همان نقطه انجام دهیم (نه تا ابتدای فایل). به این نقاط checkpoint گفته می‌شود که انتخاب صحیح آنها تاثیر بسیاری در کیفیت و کارایی عمل بازیابی دارد. 


نکته بسیار مهمی که در مورد checkpoint ها وجود دارد اینست که آنها چیزی فراتر از یک علامت در لاگ فایل هستند. هنگامی که DBMS به زمانی میرسد که باید در لاگ فایل checkpoint قرار دهد، باید اعمال مهمی ابتدا انجام شود.  اولین کاری که در زمان checkpoint باید صورت بگیرد اینست که رکورد هایی از لاگ فایل که هنوز به دیسک منتقل نشده اند، بر روی لاگ فایل فیزیکی بر روی دیسک نوشته شوند. به این عمل flush کردن لاگ رکورد‌ها نیز گفته می‌شود. دومین کاری که در این زمان باید صورت بگیرید اینست که رکوردی خاص بعنوان checkpoint record در لاگ فایل درج گردد. در این رکورد در واقع تصویری از وضعیت دیتابیس در زمان checkpoint را نگهداری می‌کنیم. دقت داشته باشید که در زمان checkpoint،DBMS برای یک لحظه تمامی تراکنش‌های در حال اجرا را متوقف می‌کند و لیستی از این تراکنش‌ها را در رکورد مربوط به checkpoint نگهداری می‌کند تا در زمان بازیابی بداند چه تراکنش هایی در آن زمان هنوز commit نشده و تاثیرشان به پایگاه داده اعمال نشده است. سومین کاری که در این لحظه بایدا انجام گیرد ایسنت که اگر داده هایی از پایگاه داده هستند که عملیات مربوط به آنها COMMIT شده اند اما هنوز به دیسک منتقل نشده اند بر روی دیسک نوشته شوند.آخرین کاری که باید انجام شود اینست که آدرس رکورد مربوط به checkpoint در فایلی بنام raster file ذخیره شود. علت این کار آنست که در هنگام بازیابی بتوانیم بسرعت آدرس آخرین checkpoint را بدست آوریم.


عمل  UNDO :

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

  • هنگامی که می‌گوییم یک عمل بروز رسانی (update) را می‌خواهیم undo کنیم منظور اینست که مقدار قبلی فیلد مورد نظر را به جای مقدار جدید آن قرار دهیم.
  • هنگامی که عمل undo را بر روی عملیات حذف می‌خواهیم انجام دهیم منظور اینست که مقدار قبلی جدول (رکورد حذف شده) را مجددا باز گردانیم.
  • هنگامی که عمل undo را بر روی عملیات درج (insert) می خواهیم انجام دهیم منظور این است که مقدار جدید درج شده در جدول را حذف کنیم.
البته این موارد ممکن است کمی بدیهی بنظر برسد اما برای کامل‌تر شدن این مقاله آموزشی بهتر دانستیم که اشاره ای به آنها کرده باشیم. 

انجام عمل بازیابی و ترمیم :

تا اینجا مقدمات لازم برای ترمیم پایگاه داده را گفتیم. حال می‌خواهیم بسراغ چگونگی انجام عمل ترمیم برویم. هنگامی که می‌خواهیم پایگاه داده ای را ترمیم کنیم اولین کاری که باید انجام گیرد اینست که بوسیله raster file، آدرس آخرین checkpoint لاگ فایل را پیدا کنیم. سپس فایل لاگ را از نقطه checkpoint  به پایین اسکن می‌کنیم. در هنگام اسکن کردن باید تراکنش‌ها را به دو گروه تقکیک کنیم، تراکنش هایی که باید undo شوند و تراکنش هایی که باید عمل redo بر روی انها انجام گیرد. علت این کار اینست که در هنگام undo کردن از انتهای لاگ فایل به سمت بالا باید حرکت کنیم و برای Redo کردن بصورت عکس، از بالا به سمت پایین می‌آییم. بنابراین جهت حرکت در لاگ فایل برای این دو عمل متفاوت است. بهمین دلیل باید ابتدا تراکنش‌ها تفکیک شوند. اما چگونه این تفکیک صورت می‌گیرد؟

  

هنگام اسکن کردن (از نقطه checkpoint به سمت انتهای لاگ فایل (لحظه خطا) )، هر تراکنشی که رکورد لاگ مربوط به commit آن دیده شود باید در گروه redo قرار گیرد. بعبارت دیگر تراکنش هایی که در این فاصله commit شده اند را در گروه redo قرار می‌دهیم. در مقابل هر تراکنشی که commit آن دیده نشود (commit نشده اند) باید undo  شود. باز هم تاکید می‌کنیم که این عمل تنها در فاصله بین آخرینcheckpoint تا لحظه وقوع خطا انجام می‌شود.

  

  دقت داشته باشید که در شروع اسکن کردن اولین رکوردی که خوانده می‌شود رکورد مربوط بهcheckpoint می باشد که حاوی تراکنش هایی است که در زمان checkpoint در حال انجام بوده اند، یعنی هنوز commit نشده اند. بنابراین تمامی این تراکنش‌ها را ابتدا در گروه تراکنش هایی که باید undo شوند قرار می‌دهیم. بمرور که عمل اسکن را ادامه می‌دهیم اگر به تراکنشی رسیدیم که رکورد مربوط به شروع ان ثبت شده باشد، باید آن تراکنش را در لیست undo قرار دهیم. تراکنش هایی که commit آنها دیده شود را نیز باید از گروه undo حذف و به گروه Redo اضافه نماییم. پس از خاتمه عمل اسکن ما دو لیست از تراکنش‌ها داریم. یکی تراکنش هایی که باید Redo شوند و دیگری  آنهایی که باید undo  گردند. 


پس از مشخص شدن دو لیست Redo و Undo، باید دو کار دیگر انجام شود. اولین کار اینست که تراکنش هایی که باید undo شوند را از پایین به بالا undo کنیم. یکی از دلایل اینکه ابتدا عملیات undo را انجام می‌دهیم ایسنت هنگامی که تراکنش ها commit نشده اند، قفل هایی را که بر روی منابع پایگاه داده زده اند هنوز آزاد نکرده اند. با عمل undo کردن این قفل‌ها را آزاد می‌کنیم و بدین وسیله کمک می‌کنیم تا درجه همروندی پایگاه داده پایین نیاید. پس از خاتمه عملیات undo، به نقطه checkpoint می رسیم. در این لحظه مانند اینست که هیچ تراکنشی در سیستم وجود ندارد. حالا بر اساس لیست redo از بالا یعنی نقطهcheckpoint به سمت پایین فایل لاگ حرکت می‌کنیم و تراکنش‌های موجود در لیست  redo را مجدد اجرا می‌کنیم. پس از خاتمه این گام نیز عملیات بازیابی خاتمه می‌یابد می‌توان گفت سیستم به وضعیت پایدار قبلی خود باز گشسته است.

  

برای روشن‌تر شدن موضوع به شکل زیر توجه کنید. در این شکل نقطه Tf زمان رخ دادن خطا را در پایگاه داده نشان می‌دهد. اولین کاری که برای بازیابی باید انجام گیرد، همانطور که گفته شده اینست که آدرس مربوط به زمان checkpoint (Tc) از raster file خوانده شود. پس از این کار از لحظه Tc به سمت Tf شروع به اسکن کردن لاگ فایل می‌کنیم. بدلیل آنکه در زمان Tc دو تراکنش T2 و T3 در حال اجرا بودند (و نام آنها در checkpoint record نیز ثبت شده است)، این دو تراکنش را در لیست redo قرار می‌دهیم. سپس عمل اسکن را به سمت پایین ادامه می‌دهیم. در حین اسکن کردن ابتدا به رکورد start trasnactionمربوط به تراکنش T4 می رسیم. بهمین دلیل این تراکنش را به لیست undo ها اضافه می‌کنیم. پس از آن به commit تراکنش T2 می رسیم. همانطور که گفته شد باید T2 را از لیست undo ها خارج و به یست تراکنش هایی که باید redo شوند اضافه گردد. سپس به تراکنش T5 می رسیم که تازه آغاز شده است. ان را نیز در گروه undo قرار می‌دهیم. بعد از ان رکورد مربوط به commit تراکنش T4 دیده می‌شود و ان را از لیست undo حذف و لیست redo اضافه می‌کنی. اسکن را ادامه می‌دهیم تا به نقطه Tf می رسیم. در ان لحظه لیست undo ها شامل دو تراکنش T3 و T5 و لیست Redo ها شامل تراکنش های T2 و T4 می باشند. در مورد تراکنش T1 نیز چون پیش از لحظه Tc کامیت شده است عملی صورت نمی‌گیرد. 


موفق و پیروز باشید

مطالب
اعمال تزریق وابستگی‌ها به مثال رسمی ASP.NET Identity
پروژه‌ی ASP.NET Identity که نسل جدید سیستم Authentication و Authorization مخصوص ASP.NET است، دارای دو سری مثال رسمی است:
الف) مثال‌های کدپلکس
 ب) مثال نیوگت

در ادامه قصد داریم مثال نیوگت آن‌را که مثال کاملی است از نحوه‌ی استفاده از ASP.NET Identity در ASP.NET MVC، جهت اعمال الگوی واحد کار و تزریق وابستگی‌ها، بازنویسی کنیم.


پیشنیازها
- برای درک مطلب جاری نیاز است ابتدا دور‌ه‌ی مرتبطی را در سایت مطالعه کنید و همچنین با نحوه‌ی پیاده سازی الگوی واحد کار در EF Code First آشنا باشید.
- به علاوه فرض بر این است که یک پروژه‌ی خالی ASP.NET MVC 5 را نیز آغاز کرده‌اید و توسط کنسول پاور شل نیوگت، فایل‌های مثال Microsoft.AspNet.Identity.Samples را به آن افزوده‌اید:
 PM> Install-Package Microsoft.AspNet.Identity.Samples -Pre


ساختار پروژه‌ی تکمیلی

همانند مطلب پیاده سازی الگوی واحد کار در EF Code First، این پروژه‌ی جدید را با چهار اسمبلی class library دیگر به نام‌های
AspNetIdentityDependencyInjectionSample.DataLayer
AspNetIdentityDependencyInjectionSample.DomainClasses
AspNetIdentityDependencyInjectionSample.IocConfig
AspNetIdentityDependencyInjectionSample.ServiceLayer
تکمیل می‌کنیم.


ساختار پروژه‌ی AspNetIdentityDependencyInjectionSample.DomainClasses

مثال Microsoft.AspNet.Identity.Samples بر مبنای primary key از نوع string است. برای نمونه کلاس کاربران آن‌را به نام ApplicationUser در فایل Models\IdentityModels.cs می‌توانید مشاهده کنید. در مطلب جاری، این نوع پیش فرض، به نوع متداول int تغییر خواهد یافت. به همین جهت نیاز است کلاس‌های ذیل را به پروژه‌ی DomainClasses اضافه کرد:
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.AspNet.Identity.EntityFramework;
 
namespace AspNetIdentityDependencyInjectionSample.DomainClasses
{
  public class ApplicationUser : IdentityUser<int, CustomUserLogin, CustomUserRole, CustomUserClaim>
  {
   // سایر خواص اضافی در اینجا
 
   [ForeignKey("AddressId")]
   public virtual Address Address { get; set; }
   public int? AddressId { get; set; }
  }
}

using System.Collections.Generic;
 
namespace AspNetIdentityDependencyInjectionSample.DomainClasses
{
  public class Address
  {
   public int Id { get; set; }
   public string City { get; set; }
   public string State { get; set; }
 
   public virtual ICollection<ApplicationUser> ApplicationUsers { set; get; }
  }
}

using Microsoft.AspNet.Identity.EntityFramework;
 
namespace AspNetIdentityDependencyInjectionSample.DomainClasses
{
  public class CustomRole : IdentityRole<int, CustomUserRole>
  {
   public CustomRole() { }
   public CustomRole(string name) { Name = name; }
 
 
  }
}

using Microsoft.AspNet.Identity.EntityFramework;
 
namespace AspNetIdentityDependencyInjectionSample.DomainClasses
{
  public class CustomUserClaim : IdentityUserClaim<int>
  {
 
  }
}

using Microsoft.AspNet.Identity.EntityFramework;
 
namespace AspNetIdentityDependencyInjectionSample.DomainClasses
{
  public class CustomUserLogin : IdentityUserLogin<int>
  {
 
  }
}

using Microsoft.AspNet.Identity.EntityFramework;
 
namespace AspNetIdentityDependencyInjectionSample.DomainClasses
{
  public class CustomUserRole : IdentityUserRole<int>
  {
 
  }
}
در اینجا نحوه‌ی تغییر primary key از نوع string را به نوع int، مشاهده می‌کنید. این تغییر نیاز به اعمال به کلاس‌های کاربران و همچنین نقش‌های آن‌ها نیز دارد. به همین جهت صرفا تغییر کلاس ابتدایی ApplicationUser کافی نیست و باید کلاس‌های فوق را نیز اضافه کرد و تغییر داد.
بدیهی است در اینجا کلاس پایه کاربران را می‌توان سفارشی سازی کرد و خواص دیگری را نیز به آن افزود. برای مثال در اینجا یک کلاس جدید آدرس تعریف شده‌است که ارجاعی از آن در کلاس کاربران نیز قابل مشاهده است.
سایر کلاس‌های مدل‌های اصلی برنامه که جداول بانک اطلاعاتی را تشکیل خواهند داد نیز در آینده به همین اسمبلی DomainClasses اضافه می‌شوند.


ساختار پروژه‌ی AspNetIdentityDependencyInjectionSample.DataLayer جهت اعمال الگوی واحد کار

اگر به همان فایل Models\IdentityModels.cs ابتدایی پروژه که اکنون کلاس ApplicationUser آن‌را به پروژه‌ی DomainClasses منتقل کرده‌ایم، مجددا مراجعه کنید، کلاس DbContext مخصوص ASP.NET Identity نیز در آن تعریف شده‌است:
 public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
این کلاس را به پروژه‌ی DataLayer منتقل می‌کنیم و از آن به عنوان DbContext اصلی برنامه استفاده خواهیم کرد. بنابراین دیگر نیازی نیست چندین DbContext در برنامه داشته باشیم. IdentityDbContext، در اصل از DbContext مشتق شده‌است.
اینترفیس IUnitOfWork برنامه، در پروژه‌ی DataLayer چنین شکلی را دارد که نمونه‌ای از آن‌را در مطلب آشنایی با نحوه‌ی پیاده سازی الگوی واحد کار در EF Code First، پیشتر ملاحظه کرده‌اید.
using System.Collections.Generic;
using System.Data.Entity;
 
namespace AspNetIdentityDependencyInjectionSample.DataLayer.Context
{
  public interface IUnitOfWork
  {
   IDbSet<TEntity> Set<TEntity>() where TEntity : class;
   int SaveAllChanges();
   void MarkAsChanged<TEntity>(TEntity entity) where TEntity : class;
   IList<T> GetRows<T>(string sql, params object[] parameters) where T : class;
   IEnumerable<TEntity> AddThisRange<TEntity>(IEnumerable<TEntity> entities) where TEntity : class;
   void ForceDatabaseInitialize();
  }
}
اکنون کلاس ApplicationDbContext منتقل شده به DataLayer یک چنین امضایی را خواهد یافت:
public class ApplicationDbContext :
  IdentityDbContext<ApplicationUser, CustomRole, int, CustomUserLogin, CustomUserRole, CustomUserClaim>,
  IUnitOfWork
{
  public DbSet<Category> Categories { set; get; }
  public DbSet<Product> Products { set; get; }
  public DbSet<Address> Addresses { set; get; }
تعریف آن باید جهت اعمال کلاس‌های سفارشی سازی شده‌ی کاربران و نقش‌های آن‌ها برای استفاده از primary key از نوع int به شکل فوق، تغییر یابد. همچنین در انتهای آن مانند قبل، IUnitOfWork نیز ذکر شده‌است. پیاده سازی کامل این کلاس را از پروژه‌ی پیوست انتهای بحث می‌توانید دریافت کنید.
کار کردن با این کلاس، هیچ تفاوتی با DbContext‌های متداول EF Code First ندارد و تمام اصول آن‌ها یکی است.

در ادامه اگر به فایل App_Start\IdentityConfig.cs مراجعه کنید، کلاس ذیل در آن قابل مشاهده‌است:
 public class ApplicationDbInitializer : DropCreateDatabaseIfModelChanges<ApplicationDbContext>
نیازی به این کلاس به این شکل نیست. آن‌را حذف کنید و در پروژه‌ی DataLayer، کلاس جدید ذیل را اضافه نمائید:
using System.Data.Entity.Migrations;
 
namespace AspNetIdentityDependencyInjectionSample.DataLayer.Context
{
  public class Configuration : DbMigrationsConfiguration<ApplicationDbContext>
  {
   public Configuration()
   {
    AutomaticMigrationsEnabled = true;
    AutomaticMigrationDataLossAllowed = true;
   }
  }
}
در این مثال، بحث migrations به حالت خودکار تنظیم شده‌است و تمام تغییرات در پروژه‌ی DomainClasses را به صورت خودکار به بانک اطلاعاتی اعمال می‌کند. تا همینجا کار تنظیم DataLayer به پایان می‌رسد.


ساختار پروژ‌ه‌ی AspNetIdentityDependencyInjectionSample.ServiceLayer

در ادامه مابقی کلاس‌‌های موجود در فایل App_Start\IdentityConfig.cs را به لایه سرویس برنامه منتقل خواهیم کرد. همچنین برای آن‌ها یک سری اینترفیس جدید نیز تعریف می‌کنیم، تا تزریق وابستگی‌ها به نحو صحیحی صورت گیرد. اگر به فایل‌های کنترلر این مثال پیش فرض مراجعه کنید (پیش از تغییرات بحث جاری)، هرچند به نظر در کنترلرها، کلاس‌های موجود در فایل App_Start\IdentityConfig.cs تزریق شده‌اند، اما به دلیل عدم استفاده از اینترفیس‌ها، وابستگی کاملی بین جزئیات پیاده سازی این کلاس‌ها و نمونه‌های تزریق شده به کنترلرها وجود دارد و عملا معکوس سازی واقعی وابستگی‌ها رخ نداده‌است. بنابراین نیاز است این مسایل را اصلاح کنیم.

الف) انتقال کلاس ApplicationUserManager به لایه سرویس برنامه
کلاس ApplicationUserManager فایل App_Start\IdentityConfig.c را به لایه سرویس منتقل می‌کنیم:
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using AspNetIdentityDependencyInjectionSample.DomainClasses;
using AspNetIdentityDependencyInjectionSample.ServiceLayer.Contracts;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.DataProtection;
 
namespace AspNetIdentityDependencyInjectionSample.ServiceLayer
{
  public class ApplicationUserManager
   : UserManager<ApplicationUser, int>, IApplicationUserManager
  {
   private readonly IDataProtectionProvider _dataProtectionProvider;
   private readonly IIdentityMessageService _emailService;
   private readonly IApplicationRoleManager _roleManager;
   private readonly IIdentityMessageService _smsService;
   private readonly IUserStore<ApplicationUser, int> _store;
 
   public ApplicationUserManager(IUserStore<ApplicationUser, int> store,
    IApplicationRoleManager roleManager,
    IDataProtectionProvider dataProtectionProvider,
    IIdentityMessageService smsService,
    IIdentityMessageService emailService)
    : base(store)
   {
    _store = store;
    _roleManager = roleManager;
    _dataProtectionProvider = dataProtectionProvider;
    _smsService = smsService;
    _emailService = emailService;
 
    createApplicationUserManager();
   }
 
 
   public void SeedDatabase()
   {
   }
 
   private void createApplicationUserManager()
   {
    // Configure validation logic for usernames
    this.UserValidator = new UserValidator<ApplicationUser, int>(this)
    {
      AllowOnlyAlphanumericUserNames = false,
      RequireUniqueEmail = true
    };
 
    // Configure validation logic for passwords
    this.PasswordValidator = new PasswordValidator
    {
      RequiredLength = 6,
      RequireNonLetterOrDigit = true,
      RequireDigit = true,
      RequireLowercase = true,
      RequireUppercase = true,
    };
 
    // Configure user lockout defaults
    this.UserLockoutEnabledByDefault = true;
    this.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
    this.MaxFailedAccessAttemptsBeforeLockout = 5;
 
    // Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user
    // You can write your own provider and plug in here.
    this.RegisterTwoFactorProvider("PhoneCode", new PhoneNumberTokenProvider<ApplicationUser, int>
    {
      MessageFormat = "Your security code is: {0}"
    });
    this.RegisterTwoFactorProvider("EmailCode", new EmailTokenProvider<ApplicationUser, int>
    {
      Subject = "SecurityCode",
      BodyFormat = "Your security code is {0}"
    });
    this.EmailService = _emailService;
    this.SmsService = _smsService;
 
    if (_dataProtectionProvider != null)
    {
      var dataProtector = _dataProtectionProvider.Create("ASP.NET Identity");
      this.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser, int>(dataProtector);
    }
   } 
  }
}
تغییراتی که در اینجا اعمال شده‌اند، به شرح زیر می‌باشند:
- متد استاتیک Create این کلاس حذف و تعاریف آن به سازنده‌ی کلاس منتقل شده‌اند. به این ترتیب با هربار وهله سازی این کلاس توسط IoC Container به صورت خودکار این تنظیمات نیز به کلاس پایه UserManager اعمال می‌شوند.
- اگر به کلاس پایه UserManager دقت کنید، به آرگومان‌های جنریک آن یک int هم اضافه شده‌است. این مورد جهت استفاده از primary key از نوع int ضروری است.
- در کلاس پایه UserManager تعدادی متد وجود دارند. تعاریف آن‌ها را به اینترفیس IApplicationUserManager منتقل خواهیم کرد. نیازی هم به پیاده سازی این متدها در کلاس جدید ApplicationUserManager نیست؛ زیرا کلاس پایه UserManager پیشتر آن‌ها را پیاده سازی کرده‌است. به این ترتیب می‌توان به یک تزریق وابستگی واقعی و بدون وابستگی به پیاده سازی خاص UserManager رسید. کنترلری که با IApplicationUserManager بجای ApplicationUserManager کار می‌کند، قابلیت تعویض پیاده سازی آن‌را جهت آزمون‌های واحد خواهد یافت.
- در کلاس اصلی ApplicationDbInitializer پیش فرض این مثال، متد Seed هم قابل مشاهده‌است. این متد را از کلاس جدید Configuration اضافه شده به DataLayer حذف کرده‌ایم. از این جهت که در آن از متدهای کلاس ApplicationUserManager مستقیما استفاده شده‌است. متد Seed اکنون به کلاس جدید اضافه شده به لایه سرویس منتقل شده و در آغاز برنامه فراخوانی خواهد شد. DataLayer نباید وابستگی به لایه سرویس داشته باشد. لایه سرویس است که از امکانات DataLayer استفاده می‌کند.
- اگر به سازنده‌ی کلاس جدید ApplicationUserManager دقت کنید، چند اینترفیس دیگر نیز به آن تزریق شده‌اند. اینترفیس IApplicationRoleManager را ادامه تعریف خواهیم کرد. سایر اینترفیس‌های تزریق شده مانند IUserStore، IDataProtectionProvider و IIdentityMessageService جزو تعاریف اصلی ASP.NET Identity بوده و نیازی به تعریف مجدد آن‌ها نیست. فقط کلاس‌های EmailService و SmsService فایل App_Start\IdentityConfig.c را نیز به لایه سرویس منتقل کرده‌ایم. این کلاس‌ها بر اساس تنظیمات IoC Container مورد استفاده، در اینجا به صورت خودکار ترزیق خواهند شد. حالت پیش فرض آن، وهله سازی مستقیم است که مطابق کدهای فوق به حالت تزریق وابستگی‌ها بهبود یافته‌است.


ب) انتقال کلاس ApplicationSignInManager به لایه سرویس برنامه
کلاس ApplicationSignInManager فایل App_Start\IdentityConfig.c را نیز به لایه سرویس منتقل می‌کنیم.
using AspNetIdentityDependencyInjectionSample.DomainClasses;
using AspNetIdentityDependencyInjectionSample.ServiceLayer.Contracts;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin.Security;
 
namespace AspNetIdentityDependencyInjectionSample.ServiceLayer
{
  public class ApplicationSignInManager :
   SignInManager<ApplicationUser, int>, IApplicationSignInManager
  {
   private readonly ApplicationUserManager _userManager;
   private readonly IAuthenticationManager _authenticationManager;
 
   public ApplicationSignInManager(ApplicationUserManager userManager,
              IAuthenticationManager authenticationManager) :
    base(userManager, authenticationManager)
   {
    _userManager = userManager;
    _authenticationManager = authenticationManager;
   }
  }
}
در اینجا نیز اینترفیس جدید IApplicationSignInManager را برای مخفی سازی پیاده سازی کلاس پایه توکار SignInManager، اضافه کرده‌ایم. این اینترفیس دقیقا حاوی تعاریف متدهای کلاس پایه SignInManager است و نیازی به پیاده سازی مجدد در کلاس ApplicationSignInManager نخواهد داشت.


ج) انتقال کلاس ApplicationRoleManager به لایه سرویس برنامه
کلاس ApplicationRoleManager فایل App_Start\IdentityConfig.c را نیز به لایه سرویس منتقل خواهیم کرد:
using AspNetIdentityDependencyInjectionSample.DomainClasses;
using AspNetIdentityDependencyInjectionSample.ServiceLayer.Contracts;
using Microsoft.AspNet.Identity;
 
namespace AspNetIdentityDependencyInjectionSample.ServiceLayer
{
  public class ApplicationRoleManager : RoleManager<CustomRole, int>, IApplicationRoleManager
  {
   private readonly IRoleStore<CustomRole, int> _roleStore;
   public ApplicationRoleManager(IRoleStore<CustomRole, int> roleStore)
    : base(roleStore)
   {
    _roleStore = roleStore;
   }
 
 
   public CustomRole FindRoleByName(string roleName)
   {
    return this.FindByName(roleName); // RoleManagerExtensions
   }
 
   public IdentityResult CreateRole(CustomRole role)
   {
    return this.Create(role); // RoleManagerExtensions
   }
  }
}
روش کار نیز در اینجا همانند دو کلاس قبل است. اینترفیس جدید IApplicationRoleManager را که حاوی تعاریف متدهای کلاس پایه توکار RoleManager است، به لایه سرویس اضافه می‌کنیم. کنترلرهای برنامه با این اینترفیس بجای استفاده مستقیم از کلاس ApplicationRoleManager کار خواهند کرد.

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


ساختار پروژه‌ی AspNetIdentityDependencyInjectionSample.IocConfig 

پروژه‌ی IocConfig جایی است که تنظیمات StructureMap را به آن منتقل کرده‌ایم:
using System;
using System.Data.Entity;
using System.Threading;
using System.Web;
using AspNetIdentityDependencyInjectionSample.DataLayer.Context;
using AspNetIdentityDependencyInjectionSample.DomainClasses;
using AspNetIdentityDependencyInjectionSample.ServiceLayer;
using AspNetIdentityDependencyInjectionSample.ServiceLayer.Contracts;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.Owin.Security;
using StructureMap;
using StructureMap.Web;
 
namespace AspNetIdentityDependencyInjectionSample.IocConfig
{
  public static class SmObjectFactory
  {
   private static readonly Lazy<Container> _containerBuilder =
    new Lazy<Container>(defaultContainer, LazyThreadSafetyMode.ExecutionAndPublication);
 
   public static IContainer Container
   {
    get { return _containerBuilder.Value; }
   }
 
   private static Container defaultContainer()
   {
    return new Container(ioc =>
    {
      ioc.For<IUnitOfWork>()
        .HybridHttpOrThreadLocalScoped()
        .Use<ApplicationDbContext>();
 
      ioc.For<ApplicationDbContext>().HybridHttpOrThreadLocalScoped().Use<ApplicationDbContext>();
      ioc.For<DbContext>().HybridHttpOrThreadLocalScoped().Use<ApplicationDbContext>();
 
      ioc.For<IUserStore<ApplicationUser, int>>()
       .HybridHttpOrThreadLocalScoped()
       .Use<UserStore<ApplicationUser, CustomRole, int, CustomUserLogin, CustomUserRole, CustomUserClaim>>();
 
      ioc.For<IRoleStore<CustomRole, int>>()
       .HybridHttpOrThreadLocalScoped()
       .Use<RoleStore<CustomRole, int, CustomUserRole>>();
 
      ioc.For<IAuthenticationManager>()
        .Use(() => HttpContext.Current.GetOwinContext().Authentication);
 
      ioc.For<IApplicationSignInManager>()
        .HybridHttpOrThreadLocalScoped()
        .Use<ApplicationSignInManager>();
 
      ioc.For<IApplicationUserManager>()
        .HybridHttpOrThreadLocalScoped()
        .Use<ApplicationUserManager>();
 
      ioc.For<IApplicationRoleManager>()
        .HybridHttpOrThreadLocalScoped()
        .Use<ApplicationRoleManager>();
 
      ioc.For<IIdentityMessageService>().Use<SmsService>();
      ioc.For<IIdentityMessageService>().Use<EmailService>();
      ioc.For<ICustomRoleStore>()
        .HybridHttpOrThreadLocalScoped()
        .Use<CustomRoleStore>();
 
      ioc.For<ICustomUserStore>()
        .HybridHttpOrThreadLocalScoped()
        .Use<CustomUserStore>();
 
      //config.For<IDataProtectionProvider>().Use(()=> app.GetDataProtectionProvider()); // In Startup class
 
      ioc.For<ICategoryService>().Use<EfCategoryService>();
      ioc.For<IProductService>().Use<EfProductService>();
    });
   }
  }
}
در اینجا نحوه‌ی اتصال اینترفیس‌های برنامه را به کلاس‌ها و یا نمونه‌هایی که آن‌ها را می‌توانند پیاده سازی کنند، مشاهده می‌کنید. برای مثال IUnitOfWork به ApplicationDbContext مرتبط شده‌است و یا دوبار تعاریف متناظر با DbContext را مشاهده می‌کنید. از این تعاریف به صورت توکار توسط ASP.NET Identity زمانیکه قرار است UserStore و RoleStore را وهله سازی کند، استفاده می‌شوند و ذکر آن‌ها الزامی است.
در تعاریف فوق یک مورد را به فایل Startup.cs موکول کرده‌ایم. برای مشخص سازی نمونه‌ی پیاده سازی کننده‌ی IDataProtectionProvider نیاز است به IAppBuilder کلاس Startup برنامه دسترسی داشت. این کلاس آغازین Owin اکنون به نحو ذیل بازنویسی شده‌است و در آن، تنظیمات IDataProtectionProvider را به همراه وهله سازی CreatePerOwinContext مشاهده می‌کنید:
using System;
using AspNetIdentityDependencyInjectionSample.IocConfig;
using AspNetIdentityDependencyInjectionSample.ServiceLayer.Contracts;
using Microsoft.AspNet.Identity;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.DataProtection;
using Owin;
using StructureMap.Web;
 
namespace AspNetIdentityDependencyInjectionSample
{
  public class Startup
  {
   public void Configuration(IAppBuilder app)
   {
    configureAuth(app);
   }
 
   private static void configureAuth(IAppBuilder app)
   {
    SmObjectFactory.Container.Configure(config =>
    {
      config.For<IDataProtectionProvider>()
        .HybridHttpOrThreadLocalScoped()
        .Use(()=> app.GetDataProtectionProvider());
    });
    SmObjectFactory.Container.GetInstance<IApplicationUserManager>().SeedDatabase();
 
    // Configure the db context, user manager and role manager to use a single instance per request
    app.CreatePerOwinContext(() => SmObjectFactory.Container.GetInstance<IApplicationUserManager>());
 
    // Enable the application to use a cookie to store information for the signed in user
    // and to use a cookie to temporarily store information about a user logging in with a third party login provider
    // Configure the sign in cookie
    app.UseCookieAuthentication(new CookieAuthenticationOptions
    {
      AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
      LoginPath = new PathString("/Account/Login"),
      Provider = new CookieAuthenticationProvider
      {
       // Enables the application to validate the security stamp when the user logs in.
       // This is a security feature which is used when you change a password or add an external login to your account.
       OnValidateIdentity = SmObjectFactory.Container.GetInstance<IApplicationUserManager>().OnValidateIdentity()
      }
    });
    app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
 
    // Enables the application to temporarily store user information when they are verifying the second factor in the two-factor authentication process.
    app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));
 
    // Enables the application to remember the second login verification factor such as phone or email.
    // Once you check this option, your second step of verification during the login process will be remembered on the device where you logged in from.
    // This is similar to the RememberMe option when you log in.
    app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie); 
   }
 
  }
}
این تعاریف از فایل پیش فرض Startup.Auth.cs پوشه‌ی App_Start دریافت و جهت کار با IoC Container برنامه، بازنویسی شده‌اند.


تنظیمات برنامه‌ی اصلی ASP.NET MVC، جهت اعمال تزریق وابستگی‌ها

الف) ابتدا نیاز است فایل Global.asax.cs را به نحو ذیل بازنویسی کنیم:
using System;
using System.Data.Entity;
using System.Web;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using AspNetIdentityDependencyInjectionSample.DataLayer.Context;
using AspNetIdentityDependencyInjectionSample.IocConfig;
using StructureMap.Web.Pipeline;
 
namespace AspNetIdentityDependencyInjectionSample
{
  public class MvcApplication : HttpApplication
  {
   protected void Application_Start()
   {
    AreaRegistration.RegisterAllAreas();
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);
 
 
    setDbInitializer();
    //Set current Controller factory as StructureMapControllerFactory
    ControllerBuilder.Current.SetControllerFactory(new StructureMapControllerFactory());
   }
 
   protected void Application_EndRequest(object sender, EventArgs e)
   {
    HttpContextLifecycle.DisposeAndClearAll();
   }
 
   public class StructureMapControllerFactory : DefaultControllerFactory
   {
    protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
    {
      if (controllerType == null)
       throw new InvalidOperationException(string.Format("Page not found: {0}", requestContext.HttpContext.Request.RawUrl));
      return SmObjectFactory.Container.GetInstance(controllerType) as Controller;
    }
   }
 
   private static void setDbInitializer()
   {
    Database.SetInitializer(new MigrateDatabaseToLatestVersion<ApplicationDbContext, Configuration>());
    SmObjectFactory.Container.GetInstance<IUnitOfWork>().ForceDatabaseInitialize();
   }
  }
}
در اینجا در متد setDbInitializer، نحوه‌ی استفاده و تعریف فایل Configuration لایه Data را ملاحظه می‌کنید؛ به همراه متد آغاز بانک اطلاعاتی و اعمال تغییرات لازم به آن در ابتدای کار برنامه. همچنین ControllerFactory برنامه نیز به StructureMapControllerFactory تنظیم شده‌است تا کار تزریق وابستگی‌ها به کنترلرهای برنامه به صورت خودکار میسر شود. در پایان کار هر درخواست نیز منابع Disposable رها می‌شوند.

ب) به پوشه‌ی Models برنامه مراجعه کنید. در اینجا در هر کلاسی که Id از نوع string وجود داشت، باید تبدیل به نوع int شوند. چون primary key برنامه را به نوع int تغییر داده‌ایم. برای مثال کلاس‌های EditUserViewModel و RoleViewModel باید تغییر کنند.

ج) اصلاح کنترلرهای برنامه جهت اعمال تزریق وابستگی‌ها

اکنون اصلاح کنترلرها جهت اعمال تزریق وابستگی‌ها ساده‌است. در ادامه نحوه‌ی تغییر امضای سازنده‌های این کنترلرها را جهت استفاده از اینترفیس‌های جدید مشاهده می‌کنید:
  [Authorize]
public class AccountController : Controller
{
  private readonly IAuthenticationManager _authenticationManager;
  private readonly IApplicationSignInManager _signInManager;
  private readonly IApplicationUserManager _userManager;
  public AccountController(IApplicationUserManager userManager,
          IApplicationSignInManager signInManager,
          IAuthenticationManager authenticationManager)
  {
   _userManager = userManager;
   _signInManager = signInManager;
   _authenticationManager = authenticationManager;
  }

  [Authorize]
public class ManageController : Controller
{
  // Used for XSRF protection when adding external logins
  private const string XsrfKey = "XsrfId";
 
  private readonly IAuthenticationManager _authenticationManager;
  private readonly IApplicationUserManager _userManager;
  public ManageController(IApplicationUserManager userManager, IAuthenticationManager authenticationManager)
  {
   _userManager = userManager;
   _authenticationManager = authenticationManager;
  }

  [Authorize(Roles = "Admin")]
public class RolesAdminController : Controller
{
  private readonly IApplicationRoleManager _roleManager;
  private readonly IApplicationUserManager _userManager;
  public RolesAdminController(IApplicationUserManager userManager,
           IApplicationRoleManager roleManager)
  {
   _userManager = userManager;
   _roleManager = roleManager;
  }


  [Authorize(Roles = "Admin")]
public class UsersAdminController : Controller
{
  private readonly IApplicationRoleManager _roleManager;
  private readonly IApplicationUserManager _userManager;
  public UsersAdminController(IApplicationUserManager userManager,
           IApplicationRoleManager roleManager)
  {
   _userManager = userManager;
   _roleManager = roleManager;
  }
پس از این تغییرات، فقط کافی است بجای خواص برای مثال RoleManager سابق از فیلدهای تزریق شده در کلاس، مثلا roleManager_ جدید استفاده کرد. امضای متدهای یکی است و تنها به یک search و replace نیاز دارد.
البته تعدادی اکشن متد نیز در اینجا وجود دارند که از string id استفاده می‌کنند. این‌ها را باید به int? Id تغییر داد تا با نوع primary key جدید مورد استفاده تطابق پیدا کنند.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید:
AspNetIdentityDependencyInjectionSample


معادل این پروژه جهت ASP.NET Core Identity : «سفارشی سازی ASP.NET Core Identity - قسمت اول - موجودیت‌های پایه و DbContext برنامه »
مطالب
ردیابی تغییرات در Entity Framework، بخش اول
همان طور که می‌دانید، Entity Framework  تغییراتی را که بر روی اشیا انجام می‌دهید، ردیابی می‌کند. بدیهی است که EF از طریق ردیابی این تغییرات است که می‌تواند تغییرات انجام شده را شناسایی کند و آن‌ها را در مواقع مورد نیاز مانند ذخیره‌ی تغییرات  (DbContext.SaveChanges)، بر روی پایگاه داده اعمال  کند. شما می‌توانید به اطلاعات این ردیاب تغییر و اعمال مرتبط به آن از طریق ویژگی  DbContext.ChangeTracker دسترسی پیدا کنید. 
در این مقاله بیشتر سعی به بررسی مفاهیم ردیابی و روش هایی که EF برای ردیابی تغییرات استفاده می‌کند، بسنده می‌کنم و بررسی  API‌های مختلف آن را به مقاله ای دیگر موکول می‌کنم.
به طور کلی EF از دو روش برای ردیابی تغییرات رخ داده شده در اشیا استفاده می‌کند:
1) ردیابی تغییر عکس فوری! (Snapshot change tracking) 
2) پروکسی‌های ردیابی تغییر  (Change tracking proxies)

 ردیابی تغییر عکس فوری

به نظر من، اسم مناسبی برای این روش انتخاب کرده اند و دقیقا بیان گر کاری است که  EF انجام می‌دهد. در حالت عادی کلاس‌های دامین ما یا همان کلاس‌های POCO، هیچ منطق و کدی را برای مطلع ساختن EF از تغییراتی که در آن‌ها رخ می‌دهد پیاده سازی نکرده اند. چون هیچ راهی برای EF، برای مطلع شدن از تغییرات رخ داده وجود ندارد، EF راه جالبی را بر می‌گزیند. EF هر گاه شیئی را می‌بیند از مقادیر ویژگی‌های آن یک عکس فوری می‌گیرد! و آن‌ها را در حافظه ذخیره می‌کند.این عمل هنگامی که یک شی از پرس و جو (query) حاصل می‌شود، و یا شیئی را به DbSet اضافه می‌کنیم رخ می‌دهد.
زمانی که EF می‌خواهد بفهمد که چه تغییراتی رخ داده است، مقادیر کنونی موجود در کلیه اشیا را اسکن می‌کند و با مقادیری که در عکس فوری ذخیره کرده است مقایسه می‌کند و متوجه تغییرات رخ داده می‌شود. این فرآیند اسکن کردن کلیه اشیا زمانی رخ می‌دهد که متد DetectChanges ویژگی DbSet.ChangeTracker صدا زده شود.

 پروکسی‌های ردیابی تغییر

پروکسی‌های ردیابی تغییر، مکانیزم دیگری برای ردیابی تغییرات EF است و به EF این اجازه را می‌دهد تا از تغییرات رخ داده، مطلع شود.
اگر به یاد داشته باشید در مباحث Lazy loading نیز از واژه پروکسی‌های پویا استفاده شد. پروکسی‌های ردیابی تغییر نیز با استفاده از همان مکانیزم کار می‌کنند و علاوه بر فراهم کردن Lazy loading ،این امکان را می‌دهند تا تغییرات را به Context انتقال دهند.
برای استفاده از پروکسی‌های ردیابی تغییر، شما باید ساختار کلاس‌های خود را به گونه ای تغییر دهید، تا EF بتواند در زمان اجرا، نوع پویایی را که هریک، از کلاس‌های POCO شما مشتق می‌شوند ایجاد کند، و تک تک ویژگی‌های آن‌ها را تحریف (override) کند.
این نوع پویا که به عنوان پروکسی پویا نیز شناخته می‌شود، منطقی را در ویژگی‌های تحریف شده شامل می‌شود، تا EF را از تغییرات صورت گرفته در ویژگی هایش مطلع سازد.
 برای بیان ادامه‌ی مطلب، من مدل یک دفترچه تلفن ساده را به شرح زیر در نظر گرفتم که روابط مهم و اساسی در آن در نظر گرفته شده است. 
namespace EntitySample1.DomainClasses
{
    public class Person
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public DateTime BirthDate { get; set; }
        public virtual PersonInfo PersonInfo { get; set; }
        public virtual ICollection<PhoneNumber> PhoneNumbers { get; set; }
        public virtual ICollection<Address> Addresses { get; set; }
    }
}

namespace EntitySample1.DomainClasses
{
    public class PersonInfo
    {
        public int Id { get; set; }
        public string Note { get; set; }
        public string Major { get; set; }
    }
}

namespace EntitySample1.DomainClasses
{
    public enum PhoneType
    {
        Home,
        Mobile,
        Work
    }

    public class PhoneNumber
    {
        public int Id { get; set; }
        public string Number { get; set; }
        public PhoneType PhoneType { get; set; }
        public virtual Person Person { get; set; }
    }
}

namespace EntitySample1.DomainClasses
{
    public class Address
    {
        public int Id { get; set; }
        public string City { get; set; }
        public string Street { get; set; }
        public virtual ICollection<Person> Persons { get; set; }
    }
}
طبق کلاس‌های فوق ، ما تعدادی شخص ، اطلاعات شخص ، شماره تلفن و آدرس داریم. رابطه‌ی بین شخص و اطلاعات آن شخص یک به یک، شخص و آدرس  چند به چند  و شخص با شماره تلفن یک به چند است. همچنین به این نکته توجه داشته باشید که  کلیه کلاس‌های فوق به صورت public تعریف، و کلیه خواص راهبری (navigation properties) به صورت virtual تعریف شده اند. دلیل این کار هم این است که این دو مورد، جز الزامات، برای فعال سازی Lazy loading هستند. 
تعریف کلاس context نیز به شکل زیر است:

namespace EntitySample1.DataLayer
{
    public class PhoneBookDbContext : DbContext
    {
        public DbSet<Person> Persons { get; set; }
        public DbSet<PhoneNumber> PhoneNumbers { get; set; }
        public DbSet<Address> Addresses { get; set; }

    }
}

استفاده از ردیابی تغییر عکس فوری

ردیابی تغییر عکس فوری، وابسته به این است که EF بفهمد، چه زمانی تغییرات رخ داده است. رفتار پیش فرض DbContext API ، این هست که به صورت خودکار بازرسی لازم را در نتیجه‌ی رخداد‌های DbContext انجام دهد. DetectChanges تنها اطلاعات مدیریت حالت context، که وظیفه‌ی انعکاس تغییرات صورت گرفته به پایگاه داده را دارد، به روز نمی‌کند، بلکه اصلاح رابطه(ralationship) ترکیبی از خواص راهبری مرجع ، مجموعه ای  و کلید‌های خارجی را انجام می‌دهد. این خیلی مهم خواهد بود که درک روشنی داشته باشیم از این که چگونه و چه زمانی تغییرات تشخیص داده می‌شوند،چه چیزی باید از آن انتظار داشته باشیم و چگونه کنترلش کنیم.

چه زمانی تشخیص خودکار تغییرات اجرا می‌شود؟

متد DetectChanges کلاس ObjectContext، از EF نسخه‌ی 4 به عنوان بخشی از الگوی ردیابی تغییر عکس فوری اشیای POCO ،در دسترس بوده است. تفاوتی که در مورد DataContext.ChangeTracker.DetectChanges( در حقیقت ObjectContext.DetectChanges فراخوانی می‌شود) وجود دارد این است که، رویداد‌های خیلی بیشتری وجود دارند که به صورت خودکار DetectChanges را فراخوانی می‌کنند.
لیستی از متدهایی که باعث انجام عمل تشخیص تغییرات (DetectChanges)، می‌شوند را  در ادامه مشاهده می‌کنید:
• DbSet.Add
• DbSet.Find
• DbSet.Remove
• DbSet.Local
• DbSet.SaveChanges
• فراخوانی Linq Query از DbSet
• DbSet.Attach
• DbContext.GetValidationErrors
• DbContext.Entry
• DbChangeTracker.Entries

 
کنترل زمان فراخوانی DetectChanges

بیشترین زمانی که EF احتیاج به فهمیدن تغییرات دارد، در زمان SaveChanges است، اما حالت‌های زیاد دیگری نیز هست. برای مثال، اگر ما از ردیاب تغییرات، درخواست وضعیت فعلی یک شی را بکنیم،EF احتیاج به اسکن کردن و بررسی تغییرات رخ داده را دارد. همچنین وضعیتی را در نظر بگیرید که شما از پایگاه داده یک شماره تلفن را واکشی می‌کنید و سپس آن را به مجموعه شماره تلفن‌های یک شخص جدید اضافه می‌کنید.آن شماره تلفن اکنون تغییر کرده است، چرا که انتساب آن به یک شخص جدید،خاصیت PersonId آن را تغییر داده است. ولی EF برای اینکه بفهمد تغییر رخ داده است(یا حتی نداده است) ، احتیاج به اسکن کردن همه‌ی اشیا Person دارد.
بیشتر عملیاتی که بر روی DbContext API انجام می‌دهید، موجب فراخوانی DetectChanges می‌شود. در بیشتر موارد DetectChanges به اندازه کافی سریع هست تا باعث ایجاد مشکل کارایی نشود. با این حال ممکن است ، شما تعداد خیلی زیادی اشیا در حافظه داشته باشید، و یا تعداد زیادی عملیات در DbContext ، در مدت خیلی کوتاهی انجام دهید، رفتار تشخیص خودکار تغییرات ممکن است، باعث نگرانی‌های کارایی شود. خوشبختانه گزینه ای برای خاموش کردن رفتار تشخیص خودکار تغییرات وجود دارد و هر زمانی که می‌دانید لازم است، می‌توانید آن را به صورت دستی فراخوانی کنید.
 EF بر مبنای این فرض ساخته شده است که شما ، در صورتی که در فراخوانی آخرین API، موجودیتی تغییر پیدا کرده است، قبل از فراخوانی API جدید، باید DetectChanges صدا زده شود. این شامل فراخوانی DetectChanges، قبل از اجرای هر query نیز می‌شود.اگر این عمل ناموفق یا نابجا انجام شود،ممکن است عواقب غیر منتظره ای در بر داشته باشد. DbContext انجام این وظیفه را بر عهده گرفته است و به همین دلیل به طور پیش فرض تشخیص تغییرات خودکار آن فعال است.

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

برای خاموش یا روشن کردن تشخیص خودکار تغییرات، باید متغیر بولین DbContext.Configuration.AutoDetectChangesEnabled را تنظیم کنید.
در مثال زیر، ما در متد ManualDetectChanges، تشخیص خودکار تغییرات را خاموش کرده ایم و تاثیرات آن را بررسی کرده ایم.

        private static void ManualDetectChanges()
        {
            using (var context = new PhoneBookDbContext())
            {
                context.Configuration.AutoDetectChangesEnabled = false; // turn off Auto Detect Changes

                var p1 = context.Persons.Single(p => p.FirstName == "joe");

                p1.LastName = "Brown";

                Console.WriteLine("Before DetectChanges: {0}", context.Entry(p1).State);

                context.ChangeTracker.DetectChanges(); // call detect changes manually

                Console.WriteLine("After DetectChanges: {0}", context.Entry(p1).State);
            }
        }

در کدهای بالا ابتدا تشخیص خودکار تغییرات را خاموش کرده ایم و سپس یک شخص با نام joe را از دیتابیس فراخواندیم و سپس نام خانوادگی آن را به Brown تغییر دادیم. سپس در خط بعد، وضعیت فعلی موجودیت p1 را از context جاری پرسیدیم. در خط بعدی، DetectChanges را به صورت دستی صدا زده ایم و دوباره همان پروسه را برای به دست آوردن وضیعت شی p1، انجام داده ایم. همان طور که می‌بینید ، برای به دست آوردن وضعیت فعلی شی مورد نظر از متد Entry متعلق به ChangeTracker API استفاده می‌کنیم، که در آینده مفصل در مورد آن بحث خواهد شد. اگر شما متد Main را با صدا زدن ManualDetectChanges ویرایش کنید ، خروجی زیر را مشاهده خواهید کرد:

Before DetectChanges: Unchanged
After DetectChanges: Modified

همان طور که انتظار می‌رفت، به دلیل خاموش کردن تشخیص خودکار تغییرات، context قادر به تشخیص تغییرات صورت گرفته در شی p1 نیست، تا زمانی که متد DetectChanges را به صورت دستی صدا بزنیم. دلیل این که در دفعه اول، ما نتیجه‌ی غلطی مشاهده می‌کنیم، این است که ما قانون را نقض کرده ایم و قبل از صدا زدن هر API ، متد DetectChanges را صدا نزده ایم. خوشبختانه چون ما در اینجا وضعیت یک شی را بررسی کردیم، با عوارض جانبی آن روبرو نشدیم.

نکته: به این نکته توجه داشته باشید که متد Entry به صورت خودکار، DetectChanges را فراخوانی می‌کند. برای اینکه دانسته بخواهیم این رفتار را غیر فعال کنیم، باید AutoDetectChangesEnabled را غیر فعال کنیم.
در مثال فوق ،خاموش کردن تشخیص خودکار تغییرات، برای ما مزیتی به همراه نداشت و حتی ممکن بود برای ما دردسر ساز شود. ولی حالتی را  در نظر بگیرید که ما یک سری API را فراخوانی می‌کنیم ،بدون این که در این بین ،در حالت اشیا تغییری ایجاد کنیم.در نتیجه می‌توانیم از فراخوانی‌های بی جهت DetectChanges جلوگیری کنیم.
 
در متد AddMultiplePersons مثال بعدی، این کار را نشان داده ام:

        private static void AddMultiplePerson()
        {
            using (var context = new PhoneBookDbContext())
            {
                context.Configuration.AutoDetectChangesEnabled = false;

                context.Persons.Add(new Person
                    {
                        FirstName = "brad",
                        LastName = "watson",
                        BirthDate = new DateTime(1990, 6, 8)
                    });

                context.Persons.Add(new Person
                {
                    FirstName = "david",
                    LastName = "brown",
                    BirthDate = new DateTime(1990, 6, 8)
                });

                context.Persons.Add(new Person
                {
                    FirstName = "will",
                    LastName = "smith",
                    BirthDate = new DateTime(1990, 6, 8)
                });

                context.SaveChanges();

            }
        }
در مثال بالا ما از فراخوانی چهار DetectChanges غیر ضروری که شامل DbSet.Add و SaveChanges می‌شود، جلوگیری کرده ایم.

استفاده از DetectChanges برای فراخوانی اصلاح رابطه


DetectChanges همچنین مسئولیت انجام اصلاح رابطه ، برای هر رابطه ای که تشخیص دهد تغییر کرده است را دارد.اگر شما بعضی از روابط را تغییر دادید و مایل بودید تا همه‌ی خواص راهبری و خواص کلید خارجی را منطبق کنید، DetectChanges این کار را برای شما انجام می‌دهد. این قابلیت می‌تواند برای سناریوهای data-binding که در آن ممکن است در رابط کاربری(UI) یکی از خواص راهبری (یا حتی یک کلید خارجی) تغییر کند، و شما بخواهید که خواص دیگری این رابطه به روز شوند و تغییرات را نشان دهند، مفید واقع شود.
متد DetectRelationshipChanges در مثال زیر از DetectChanges برای انجام اصلاح رابطه استفاده می‌کند.
 
        private static void DetectRelationshipChanges()
        {
            using (var context = new PhoneBookDbContext())
            {

                var phone1 = context.PhoneNumbers.Single(x => x.Number == "09351234567");

                var person1 = context.Persons.Single(x => x.FirstName == "will");

                person1.PhoneNumbers.Add(phone1);

                Console.WriteLine("Before DetectChanges: {0}", phone1.Person.FirstName);

                context.ChangeTracker.DetectChanges(); // ralationships fix-up

                Console.WriteLine("After DetectChanges: {0}", phone1.Person.FirstName);
            }
        }

در اینجا ابتدا ما شماره تلفنی را از دیتابیس لود می‌کنیم. سپس شخص دیگری را نیز با نام will از دیتابیس می‌خوانیم. قصد داریم شماره تلفن خوانده شده را به این شخص نسبت دهیم و مجموعه شماره تلفن‌های وی اضافه کنیم و ما این کار را با افزودن phone1 به مجموعه شماره تلفن‌های person1 انجام داده ایم. چون ما از اشیای POCO استفاده کرده ایم،EF  نمی‌فهمد که ما این تغییر را ایجاد کرده ایم و در نتیجه کلید خارجی PersonId شی phone1 را اصلاح نمی‌کند. ما می‌توانیم تا زمانی صبر کنیم تا متدی مثل SaveChanges، متد DetectChanges را فراخوانی کند،ولی اگر بخواهیم این عمل در همان لحظه انجام شود، می‌توانیم DetectChanges را دستی صدا بزنیم.

اگر ما متد Main را با اضافه کردن فرخوانی DetectRealtionShipsChanges تغییر بدهیم و آن را اجرا کنیم، نتیجه زیر را مشاهده می‌کنید:

Before DetectChanges: david
After DetectChanges: will

تا قبل از فراخوانی تشخیص تغییرات(DetectChanegs)، هنوز phone1 منتسب به شخص قدیمی(david) بوده، ولی پس از فراخوانی DetectChanges ، اصلاح رابطه رخ داده و همه  چیز با یکدیگر منطبق می‌شوند.

فعال سازی و کار با پروکسی‌های ردیابی تغییر

اگر پروفایلر کارایی شما، فراخوانی‌های بیش از اندازه DetectChnages را به عنوان یک مشکل شناسایی کند، و یا شما ترجیح می‌دهید که اصلاح رابطه به صورت بلادرنگ صورت گیرد ، ردیابی تغییر پروکسی‌های پویا، به عنوان گزینه ای دیگر مطرح می‌شود.فقط با چند تغییر کوچک در کلاس‌های POCO، EF قادر به ساخت پروکسی‌های پویا خواهد بود.پروکسی‌های ردیابی تغییر به EF اجازه ردیابی تغییرات در همان لحظه ای که ما تغییری در اشیای خود می‌دهیم را می‌دهند و همچنین امکان انجام اصلاح رابطه را در هر زمانی که تغییرات روابط را تشخیص دهد، دارد.
برای اینکه پروکسی ردیابی تغییر بتواند ساخته شود، باید قوانین زیر رعایت شود:
 
• کلاس باید public باشد و seald نباشد.
• همه‌ی خواص(properties) باید virtual تعریف شوند.
• همه‌ی خواص باید getter و setter با سطح دسترسی public داشته باشند.
• همه‌ی خواص راهبری مجموعه ای باید نوعشان، از نوع ICollection<T> تعریف شوند.


کلاس Person مثال خود را به گونه ای بازنویسی کرده ایم که تمام قوانین فوق را پیاده سازی کرده باشد.

نکته: توجه داشته باشید که ما دیگر در داخل سازنده کلاس ،کدی نمی‌نویسیم و منطقی  که باعث نمونه سازی اولیه خواص راهبری می‌شدند، را پیاده سازی نمی‌کنیم. این پروکسی ردیاب تغییر، همه‌ی خواص راهبری مجموعه ای را تحریف کرده و ار نوع مجموعه ای مخصوص خود(EntityCollection<T>) استفاده می‌کند. این نوع مجموعه ای، هر تغییری که در این مجموعه صورت می‌گیرد را زیر نظر گرفته و به ردیاب تغییر گزارش می‌دهد. اگر تلاش کنید تا نوع دیگری مانند List<T> که معمولا در سازنده کلاس از آن استفاده می‌کردیم را به آن انتساب دهیم، پروکسی، استثنایی را پرتاب می‌کند.

namespace EntitySample1.DomainClasses
{
    public class Person
    {
        public virtual int Id { get; set; }
        public virtual string FirstName { get; set; }
        public virtual string LastName { get; set; }
        public virtual DateTime BirthDate { get; set; }
        public virtual PersonInfo PersonInfo { get; set; }
        public virtual ICollection<PhoneNumber> PhoneNumbers { get; set; }
        public virtual ICollection<Address> Addresses { get; set; }
    }
}
همان طور که در مباحث مربوط به Lazy loading نیز مشاهده کردید،EF زمانی پروکسی‌های پویا را برای یک کلاس ایجاد می‌کند که یک یا چند خاصیت راهبری آن با virtual علامت گذاری شده باشند.آن پروکسی‌ها که از کلاس مورد نظر، مشتق شده اند، به خواص راهبری virtual امکان می‌دهند تا به صورت lazy لود شوند.پروکسی‌های ردیابی تغییر نیز به همان شکل در زمان اجرا ایجاد می‌شوند، با این تفاوت که این پروکسی ها، امکانات بیشتری دارند.
با این که احتیاجات رسیدن به پروکسی‌های ردیابی تغییر خیلی ساده هستند، اما ساده‌تر از آن ها، فراموش کردن یکی از آن هاست.حتی از این هم ساده‌تر می‌شود که در آینده تغییری در آن کلاس‌ها ایجاد کنید و ناخواسته یکی از آن قوانین را نقض کنید.به این خاطر، فکر خوبیست که یک آزمون واحد نیز اضافه کنیم تا مطمئن شویم که EF توانسته، پروکسی ردیابی تغییر را ایجاد کند یا نه.
در مثال زیر یک متد نوشته شده که این مورد را مورد آزمایش قرار می‌دهد. همچنین فراموش نکنید که فضای نام System.Data.Object.DataClasses را به using‌های خود اضافه کنید.
 
        private static void TestForChangeTrackingProxy()
        {
            using (var context = new PhoneBookDbContext())
            {
                var person = context.Persons.First();
                var isProxy = person is IEntityWithChangeTracker;
                Console.WriteLine("person is a proxy: {0}", isProxy);
            }
        }
زمانی که EF ، پروکسی پویا برای ردیابی تغییر ایجاد می‌کند، اینترفیس IentityWithChangeTrackerرا پیاده سازی  خواهد کرد.متد تست در مثال بالا، نمونه ای از Person را با دریافت آن از دیتابیس ایجاد می‌کند و سپس آن را با اینترفیس ذکر شده چک می‌کند تا مطمئن شود که Person ، توسط پروکسی ردیابی تغییر احاطه شده است. این نکته را نیز به یاد داشته باشید که چک کردن این که EF ، کلاس پروکسی ای که از کلاس ما مشتق شده است ایجاد کرده است یا نه،کفایت نمی‌کند، چرا که پروکسی‌های Lazy Loading نیز چنین کاری انجام می‌دهند. در حقیقت آن چیزی که سبب می‌شود EF به تغییرات صورت گرفته به صورت بلادرنگ گوش دهد،حضور IEntityWithChangeTracker است.
اکنون متد ManualDetectChanges را که کمی بالاتر بررسی کرده ایم را در نظر بگیرید و کد context.ChangeTracker.DetectChanges آن را حذف کنید و بار دیگر آن را فرا بخوانید  و نتیجه را مشاهده کنید:
 
Before DetectChanges: Modified
After DetectChanges: Modified
این دفعه،EF از تغییرات صورت گرفته آگاه است،حال چه DetectChanges فراخوانده شود یا نشود.

اکنون متد DetectRelationshipChanges را ویرایش کرده و برنامه را اجرا کنید:

Before DetectChanges: will
After DetectChanges: will
این بار می‌بینیم که EF، تغییر رابطه را تشخیص داده و اصلاح رابطه را بدون فراخوانی DetectChanges انجام داده است.

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

تذکر: اگر شما موجودیت هایی داشته باشید که شامل انواع پیچیده(Complex Types) می‌شوند،EF هنوز هم از ردیابی تغییر عکس فوری، برای خواص موجود در نوع پیچیده استفاده می‌کند، و از این جهت لازم است کهEF، برای نمونه‌ی نوع پیچیده، پروکسی ایجاد نمی‌کند.شما هنوز هم، تشخیص خودکار تغییرات خواصی که مستقیما درون آن موجودیت(Entity) تعریف شده اند را دارید، ولی تغییرات رخ داده درون نوع پیچیده، فقط از طریق DetectChanges قابل تشخیص است.

چگونگی اطمینان از اینکه نمونه‌های جدید ، پروکسی‌ها را دریافت خواهند کرد

EF به صورت خودکار برای نتایج حاصل از کوئری هایی که شما اجرا می‌کنید، پروکسی‌ها را ایجاد می‌کند. با این حال اگر شما فقط از سازنده‌ی کلاس POCO خود برای ایجاد نمونه‌ی جدید استفاده کنید،دیگر پروکسی‌ها ایجاد نخواهند شد.بدین منظور برای دریافت پروکسی ها، شما باید از متد DbSet.Create برای دریافت نمونه‌های جدید آن موجودیت استفاده کنید.
نکته: اگر شما، پروکسی‌های ردیابی تغییر را برای موجودیتی از مدلتان فعال کرده باشید،هنوز هم می‌توانید،نمونه‌های فاقد پروکسی آن موجودیت را ایجاد و بیافزایید.خوشبختانه EF با موجودیت‌های پروکسی و غیر پروکسی در همان مجموعه(set) کار می‌کند.شما باید آگاه باشید که ردیابی خودکار تغییرات و یا اصلاح رابطه، برای نمونه هایی که پروکسی هایی ردیابی تغییر نیستند، قابل استفاده نیستند.داشتن مخلوطی از نمونه‌های پروکسی و غیر پروکسی در همان مجموعه، می‌تواند گیج کننده باشد.بنابر این عموما توصیه می‌شود که برای ایجاد نمونه‌های جدید از DbSet.Create استفاده کنید، تا همه‌ی موجودیت‌های موجود در مجموعه، پروکسی‌های ردیابی تغییر باشند.
متد CreateNewProxies را به برنامه‌ی خود اضافه کرده و آن را اجرا کنید.

        private static void CreateNewProxies()
        {
            using (var context = new PhoneBookDbContext())
            {
                var phoneNumber = new PhoneNumber { Number = "987" };

                var davidPersonProxy = context.Persons.Create();
                davidPersonProxy.FirstName = "david";
                davidPersonProxy.PhoneNumbers.Add(phoneNumber);

                Console.WriteLine(phoneNumber.Person.FirstName);
            }
        }

خروجی مثال فوق david خواهد بود.همان طور که می‌بینید با استفاده از context.Persons.Create، نمونه‌ی ساخته شده، دیگر شی POCO نیست، بلکه davidPersonProxy، از جنس پروکسی ردیابی تغییر است و تغییرات آن به طور خودکار ردیابی شده و رابطه آن نیز به صورت خودکار اصلاح  می‌شود.در اینجا نیز با افزودن phoneNumber به شماره تلفن‌های davidPersonProxy، به طور خودکار رابطه‌ی بین phoneNumber و davidPersonPeroxy بر قرار شده است. همان طور که می‌دانید این عملیات بدون استفاده از پروکسی‌های ردیابی تغییرات امکان پذیر نیست و موجب بروز خطا می‌شود.


ایجاد نمونه‌های پروکسی برای انواع مشتق شده

اورلود جنریک دیگری برای DbSet.Create وجود  دارد که برای نمونه سازی کلاس‌های مشتق شده در مجموعه ما استفاده می‌شود .برای مثال، فراخوانی Create بر روی مجموعه‌ی Persons ،نمونه ای از کلاس Person را  بر می‌گرداند.ولی ممکن است کلاس هایی در مجموعه‌ی Persons وجود داشته باشند، که از آن مشتق شده باشند، مانند Student. برای دریافت نمونه‌ی پروکسی Student، از اورلود جنریک Create استفاده می‌کنیم.

var newStudent = context.Persons.Create<Student>();
واکشی موجودیت‌ها بدون ردیابی تغییرات

تا به این جای کار باید متوجه شده باشید که ردیابی تغییرات، فرآیندی ساده و بدیهی نیست و مقداری سربار در کار است. در بعضی از بخش‌های برنامه تان، احتمالا داده‌ها را به صورت فقط خواندنی در اختیار کاربران قرار می‌دهید و چون اطلاعات هیچ وقت تغییر نمی‌کنند، شما می‌خواهید که سربار ناشی از ردیابی تغییرات را حذف کنید.
خوشبختانه EF شامل متد AsNoTracking است که می‌توان از آن برای اجرای کوئری‌های بدون ردیابی استفاده کرد.یک کوئری بدون ردیابی، یک کوئری ساده هست که نتایج آن توسط context برای تشخیص تغییرات ردیابی نخواهد شد.

متد PrintPersonsWithoutChangeTracking را به برنامه اضافه کنید و آن را اجرا کنید: 
 
        private static void PrintPersonsWithoutChangeTracking()
        {
            using (var context = new PhoneBookDbContext())
            {
                var persons = context.Persons.AsNoTracking().ToList();

                foreach (var person in persons)
                {
                    Console.WriteLine(person.FirstName);
                }

            }
        }
در مثال بالا از متد AsNoTracking برای گرفتن کوئری فاقد ردیابی استفاده کردیم تا محتویات مجموعه Persons را دریافت کنیم. در نهایت با یک حلقه‌ی foreach، نتایج را بر روی کنسول به نمایش در آوردیم.به دلیل اینکه، این یک کوئری بدون ردیابی هست، context دیگر تغییراتی که روی Persons رخ می‌دهد را ردیابی نمی‌کند.در نتیجه اگر شما یکی از خواص یکی از Persons را تغییردهید و SaveChanges را صدا بزنید، تغییرات به دیتابیس ارسال نمی‌شوند.

نکته: واکشی داده‌ها بدون ردیابی تغییرات،معمولا وقتی باعث افزایش قابل توجه کارایی می‌شود که بخواهیم تعداد خیلی زیادی داده  را به صورت فقط خواندنی نمایش دهیم. اگر برنامه‌ی شما داده ای را تغییر می‌دهد و می‌خواهد آن را ذخیره کند، باید از AsNoTracking استفاده نکنید.

AsNoTracking یک متد الحاقی است، که در <IQueryable<T تعریف شده است، در نتیجه شما می‌توانید از آن، در کوئری‌های LINQ نیز استفاده کنید. شما می‌توانید از AsNoTracking، در انتهای DbSet ،در خط from کوئری استفاده کنید.

var query = from p in context.Persons.AsNoTracking()
                            where p.FirstName == "joe"
                            select p;

شما همچنین از AsNoTracking می‌توانید برای تبدیل یک کوئری LINQ موجود، به یک کوئری فاقد ردیابی استفاده کنید. این نکته را به یاد داشته باشید که فقط AsNoTracking بر روی کوئری، فرانخوانده شده است، بلکه متغیر query را با نتیجه‌ی حاصل از فراخوانی AsNoTracking بازنویسی(override)  کرده است و این، از این جهت لازم است که AsNoTracking ،تغییری در کوئری ای که بر روی آن فراخوانده شده نمی‌دهد، بلکه یک کوئری جدید بر می‌گرداند. 

 
                var query = from p in context.Persons
                            where p.FirstName == "joe"
                            select p;
                query = query.AsNoTracking();
نکته: به دلیل اینکه AsNoTracking یک متد الحاقی است، شما احتیاج به افزودن فضای نام System.Data.Entity به فضا‌های نام خود دارید.

منبع: ترجمه ای آزاد از کتاب Programming Entity Framework: DbContext
مطالب
آموزش Linq - بخش ششم : عملگرهای پرس و جو قسمت سوم
عملگر‌های تبدیل Conversion Operator

عملگر‌های پرس و جوی تبدیل، توالی‌هایی را که از جنس <IEnumerable<T هستند، به انواع دیگر مجموعه تبدیل می‌کنند.
از عملگر‌های پرس و جوی زیر می‌توان برای تبدیل توالی‌ها استفاده کرد :
  • OfType
  • Cast
  • ToArray
  • ToList
  • ToDictionary
  • ToLookup

عملگر OfType


این عملگر عناصری از توالی را که نوع آنها را مشخص می‌کنیم باز می‌گرداند.
امضاء عملگر پرس و جوی OfType  به صورت زیر است :
 public static IEnumerable<TResult> OfType<TResult>(this IEnumerable source)
همانطور که مشاهده می‌کنید توالی ورودی از یک نوع IEnumerable غیر جنریک می‌باشد. بدین معنی که عناصر توالی ورودی می‌توانند از نوع داده‌های مختلف باشند (توالی از اشیاء، از جنس Object).
در مثال زیر یک توالی IEnumerable (آرایه‌ای از اشیاء)، از عناصر با نوع داده‌های مختلفی را ایجاد کرده‌ایم. عملگر OfType در اینجا کلیه عناصر از جنس (string) را باز می‌گرداند. توالی خروجی یک نوع IEnumerable جنریک است(در این مثال <IEnumerable<List).
مثال :
IEnumerable input = new object[] { "Apple", 33, "Sugar", 44, 'a', new DateTime()};
IEnumerable<string> query = input.OfType<string>();
foreach (var item in query)
{
   Console.WriteLine(item);
}
خروجی مثال بالا :
Apple
Sugar
عملگر OfType را می‌توان به‌همراه  Strongly Type‌‌ها نیز استفاده کرد.
مثال :کد زیر یک ساختار سلسله مراتبی شیء گرا را نمایش می‌دهد:
 class Ingredient
  {
     public string Name { get; set; }
  }
  class DryIngredient : Ingredient
  {
     public int Grams { get; set; }
  }

  class WetIngredient : Ingredient
  {
     public int Millilitres { get; set; }
  }
کد زیر چگونگی استفاده از OfType را برای بدست آوردن یک زیر نوع (Subtype) مشخص، نشان می‌دهد (در این مثال، نوع WetIngredient):
IEnumerable<Ingredient> input = new Ingredient[]
{
   new DryIngredient { Name = "Flour" },
   new WetIngredient { Name = "Milk" },
   new WetIngredient { Name = "Water" }
};

IEnumerable<WetIngredient> query = input.OfType<WetIngredient>();
foreach (WetIngredient item in query)
{
   Console.WriteLine(item.Name);
}
خروجی مثال بالا :
Milk
Water

پیاده سازی توسط عبارت‌های جستجو


معادل این عملگر، کلمه‌ی کلیدی جدیدی در عبارت‌های جستجو وجود ندارد و ترکیب دو روش می‌تواند خروجی دلخواه را تولید کند.


عملگر Cast


عملگر Cast همانند عملگر OfType رفتار می‌کند. این عملگر یک توالی ورودی را دریافت و بر اساس نوع مشخص شده، توالی خروجی را تولید می‌کند. همه‌ی عناصر توالی ورودی به نوع مشخص شده Cast می‌شوند. اما بر عکس عملگر OfType که عناصری را که با نوع داده‌ی ما سازگاری نداشت، نادیده می‌گرفت، این عملگر در صورت عدم موفقیت در عملیات تغییر نقش (Cast)، یک استثناء را پرتاب می‌کند.
مثال : 
IEnumerable input = new object[]
{
   "Apple", 33, "Sugar", 44, 'a', new DateTime()
};

IEnumerable<string> query = input.Cast<string>();
foreach (string item in query)
{
   Console.WriteLine(item);
}
با اجرای برنامه‌ی فوق، خطای زیر را مشاهده خواهید کرد:
 Unhandled Exception: System.InvalidCastException: Unable to cast object of type 'System.Int32' to type 'System.String'.

پیاده سازی توسط عبارت‌های جستجو


کلمه‌ی کلیدی جایگزینی برای عملگر Cast، در عبارت‌های جستجو وجود ندارد.این عملگر با استفاده از متغیر Range که در مطالب قبلی این سری معرفی شد، قابل پیاده سازی می‌باشد.
IEnumerable input = new object[]{ "Apple", "Sugar", "Flour" };
IEnumerable<string> query =
from string i in input
select i;

foreach (var item in query)
{
   Console.WriteLine(item);
}
نکته:  در مثال فوق تعریف صریح (Explicit) نوع داده، قبل از متغیر Range انجام شده است (معادل همان نوع داده در عملیات Cast).


عملگر ToArray


عملگر ToArray یک توالی ورودی را دریافت و یک توالی خروجی را به صورت آرایه تولید می‌کند. این عملگر باعث اجرای سریع پرس و جو می‌شود و رفتار پیش فرض LINQ را که اجرای با تاخیر می‌باشد، تحریف/بازنویسی (Override) می‌کند.
مثال: در این مثال یک توالی از نوع <IEnumerable<string به یک آرایه رشته‌ای تبدیل شده است (تبدیل لیست به آرایه).
 IEnumerable<string> input = new List<string> { "Apple", "Sugar", "Flour" };
string[] array = input.ToArray();

پیاده سازی توسط عبارت‌های جستجو


معادل این عملگر، کلمه‌ی کلیدی جدیدی در عبارت‌های جستجو وجود ندارد و ترکیب دو روش می‌تواند خروجی دلخواه را تولید کند.


عملگر ToList

عملگر ToList همچون ToArray، اجرای با تاخیر را نادیده می‌گیرد. عملگر ToList همانطور که از نامش پیداست، توالی خروجی را به‌صورت لیست مهیا می‌کند.
مثال:
 IEnumerable<string> input = new[] { "Apple", "Sugar", "Flour" };
List<string> list = input.ToList();

پیاده سازی توسط عبارت‌های جستجو


معادل این عملگر، کلمه‌ی کلیدی جدیدی در عبارت‌های جستجو وجود ندارد و ترکیب دو روش می‌تواند خروجی دلخواه را تولید کند.


عملگر ToDictionary

این عملگر توالی ورودی را به یک  دیکشنری جنریک تبدیل می‌کند (<Dictinary<TKey,TValue) .
ساده‌ترین امضاء عملگر ToDictionary، یک عبارت Lambda می‌باشد. این عبارت Lambda  نشان دهنده‌ی یک تابع است که عنصر کلید(Key) را در دیکشنری، مشخص می‌کند.
مثال:
class Recipe
{
   public int Id { get; set; }
   public string Name { get; set; }
   public int Rating { get; set; }
}

IEnumerable<Recipe> recipes = new[]
{
   new Recipe { Id = 1, Name = "Apple Pie", Rating = 5 },
   new Recipe { Id = 2, Name = "Cherry Pie", Rating = 2 },
   new Recipe { Id = 3, Name = "Beef Pie", Rating = 3 }
};

Dictionary<int, Recipe> dict = recipes.ToDictionary(x => x.Id);
foreach (KeyValuePair<int, Recipe> item in dict)
{
   Console.WriteLine($"Key={item.Key}, Recipe={item.Value}");
}
در کد بالا ، کلید دیکشنری نهایی، از نوع  int می‌باشد که بر اساس Id کلاس Recipe تنظیم شده است. مقادیر (value) دیکشنری هم همان اشیاء از جنس کلاس Recipe می‌باشند.
خروجی مثال بالا:
Key=1, Recipe=Apple Pie
Key=2, Recipe=Cherry Pie
Key=3, Recipe=Beef Pie

پیاده سازی توسط عبارت‌های جستجو


معادل این عملگر، کلمه‌ی کلیدی جدیدی در عبارت‌های جستجو وجود ندارد و ترکیب دو روش می‌تواند خروجی دلخواه را تولید کند.


عملگر ToLookup


این عملگر رفتاری شبیه به عملگر ToDictionary را دارد، اما به جای تولید خروجی از نوع دیکشنری، نمونه‌ای از جنس ILookUp را ایجاد می‌کند.
در کد زیر خروجی ایجاد شده توسط lookup دستورالعمل‌ها (Recipes) را بر حسب  امتیاز آنها گروه بندی کرده است. در این مثال کلید، بر حسب Byte می‌باشد.
مثال :
class Recipe
{
   public int Id { get; set; }
   public string Name { get; set; }
   public byte Rating { get; set; }
}

IEnumerable<Recipe> recipes = new[]
{
   new Recipe { Id = 1, Name = "Apple Pie", Rating = 5 },
   new Recipe { Id = 1, Name = "Banana Pie", Rating = 5 },
   new Recipe { Id = 2, Name = "Cherry Pie", Rating = 2 },
   new Recipe { Id = 3, Name = "Beef Pie", Rating = 3 }
};

ILookup<byte, Recipe> look = recipes.ToLookup(x => x.Rating);
foreach (IGrouping<byte, Recipe> ratingGroup in look)
{
   byte rating = ratingGroup.Key;
   Console.WriteLine($"Rating {rating}");
   foreach (var recipe in ratingGroup)
   {
      Console.WriteLine($" - {recipe.Name}");
   }
}
خروجی مثال بالا:
 Rating 5
 - Apple Pie
 - Banana Pie
Rating 2
 - Cherry Pie
Rating 3
 - Beef Pie

پیاده سازی توسط عبارت‌های جستجو


معادل این عملگر، کلمه‌ی کلیدی جدیدی در عبارت‌های جستجو وجود ندارد و ترکیب دو روش می‌تواند خروجی دلخواه را تولید کند.


عملگر‌های عناصر  Element Operators

این عملگر‌ها، یک توالی ورودی را دریافت و تنها یک عنصر از توالی ورودی و یا یک عنصر را به عنوان عنصر پیش فرض باز می‌گردانند. این نوع عملگر‌ها توالی خروجی را تولید نمی‌کنند.


عملگر First

این عملگر اولین عنصر توالی را باز می‌گرداند.
مثال :
Ingredient[] ingredients =
{
   new Ingredient {Name = "Sugar", Calories = 500},
   new Ingredient {Name = "Egg", Calories = 100},
   new Ingredient {Name = "Milk", Calories = 150},
   new Ingredient {Name = "Flour", Calories = 50},
   new Ingredient {Name = "Butter", Calories = 500}
};

Ingredient element = ingredients.First();
Console.WriteLine(element.Name);
خروجی مثال بالا :
 Sugar
امضای دیگر این متد، امکان تعریف یک شرط را مهیا می‌کند. خروجی این حالت اولین عنصری است که شرط را تامین می‌کند. در کد زیر اولین عنصری که کالری آن برابر 150 باشد به خروجی ارسال می‌شود.
Ingredient[] ingredients =
{
   new Ingredient {Name = "Sugar", Calories = 500},
   new Ingredient {Name = "Egg", Calories = 100},
   new Ingredient {Name = "Milk", Calories = 150},
   new Ingredient {Name = "Flour", Calories = 50},
   new Ingredient {Name = "Butter", Calories = 500}
};

Ingredient element = ingredients.First(x=>x.Calories==150);
Console.WriteLine(element.Name);
خروجی مثال بالا:
 Milk
در زمان استفاده از عملگر First، اگر توالی ورودی هیچ عنصری نداشته باشد، یک استثناء رخ خواهد داد:
 Unhandled Exception: System.InvalidOperationException: Sequence contains no elements
کد زیر نمونه‌ای از این حالت است:
Ingredient[] ingredients = { };
Ingredient element = ingredients.First();
در زمان استفاده‌ی از امضاء دیگر عملگر First، اگر هیچ عنصری شرط معرفی شده‌ی در پارامتر را تامین نکند، باز هم یک استثناء رخ خواهد داد:
 Unhandled Exception: System.InvalidOperationException: Sequence contains no matching element
کد زیر حالت فوق را نشان می‌دهد:
 Ingredient[] ingredients =
{
   new Ingredient {Name = "Sugar", Calories = 500},
   new Ingredient {Name = "Egg", Calories = 100},
   new Ingredient {Name = "Milk", Calories = 150},
   new Ingredient {Name = "Flour", Calories = 50},
   new Ingredient {Name = "Butter", Calories = 500}
};
Ingredient element = ingredients.First(x=>x.Calories==1500);

پیاده سازی توسط عبارت‌های جستجو

معادل این عملگر، کلمه‌ی کلیدی جدیدی در عبارت‌های جستجو وجود ندارد و ترکیب دو روش می‌تواند خروجی دلخواه را تولید کند.


عملگر FirstOrDefault

عملگر FirstOrDefalt همانند عملگر First عمل می‌کند، اما با این تفاوت که به جای پرتاب یک استثناء در شرایط معرفی شده در عملگر First، یک مقدار پیش فرض را بر اساس نوع  عناصر توالی باز می‌گرداند. در صورتیکه توالی از نوع عددی باشد، مقدار 0 و اگر عناصر توالی از انواع ارجاعی باشند، مقدار Null و برای مقادیر منطقی، ارزش False به‌عنوان مقادیر پیش فرض باز گردانده می‌شوند.
مثال :
 Ingredient[] ingredients = { };
Ingredient element = ingredients.FirstOrDefault();
Console.WriteLine(element == null);
خروجی مثال بالا :
 True
پیاده سازی حالتی که هیچ یک از عناصر با شرط عملگر کطالبقت ندارند.
Ingredient[] ingredients =
{
   new Ingredient {Name = "Sugar", Calories = 500},
   new Ingredient {Name = "Egg", Calories = 100},
   new Ingredient {Name = "Milk", Calories = 150},
   new Ingredient {Name = "Flour", Calories = 50},
   new Ingredient {Name = "Butter", Calories = 500}
};

Ingredient element = ingredients.FirstOrDefault(x=>x.Calories==1500);
Console.WriteLine(element==null);
خروجی مثال بالا :
 True

پیاده سازی توسط عبارت‌های جستجو


معادل این عملگر، کلمه‌ی کلیدی جدیدی در عبارت‌های جستجو وجود ندارد و ترکیب دو روش می‌تواند خروجی دلخواه را تولید کند.


 عملگر Last

این عملگر آخرین عنصر توالی را باز می‌گرداند. همچون عملگر First، این عملگر نیز یک امضاء برای دریافت یک عبارت شرط یا پیش بینی دارد. این پیش بینی، آخرین عنصری را که شرط را تامین کند، باز می‌گرداند. باز هم مثل عملگر First، در صورتی که توالی هیچ عنصری نداشته باشد و یا عدم تامین شرط توسط عناصر توالی، استثنایی رخ خواهد داد.
مثال :
Ingredient[] ingredients =
{
   new Ingredient {Name = "Sugar", Calories = 500},
   new Ingredient {Name = "Egg", Calories = 100},
   new Ingredient {Name = "Milk", Calories = 150},
   new Ingredient {Name = "Flour", Calories = 50},
   new Ingredient {Name = "Butter", Calories = 500}
};
Ingredient element = ingredients.Last(x=>x.Calories==500);
Console.WriteLine(element.Name);
خروجی مثال بالا :
 Flour

پیاده سازی توسط عبارت‌های جستجو


معادل این عملگر، کلمه‌ی کلیدی جدیدی در عبارت‌های جستجو وجود ندارد و ترکیب دو روش می‌تواند خروجی دلخواه را تولید کند.


عملگر LastOrDefault

این عملگر همچون عملگر FirstOrDefault عمل می‌کند. از بروز استثناء جلوگیری کرده و مقدار پیش فرض را به خروجی ارسال می‌کند.

پیاده سازی توسط عبارت‌های جستجو


معادل این عملگر، کلمه‌ی کلیدی جدیدی در عبارت‌های جستجو وجود ندارد و ترکیب دو روش می‌تواند خروجی دلخواه را تولید کند.


عملگر Single

عملگر Single ، تنها عنصر توالی ورودی را باز می‌گرداند.در صورتی که توالی ما بیش از یک عنصر داشته باشد و یا توالی هیچ عنصری نداشته باشد، یک استثناء رخ خواهد داد.
Unhandled Exception: System.InvalidOperationException: Sequence contains more than one matching element
Unhandled Exception: System.InvalidOperationException: Sequence contains no matching element
مثال :
Ingredient[] ingredients =
{
   new Ingredient { Name = "Sugar", Calories = 500 }
};

Ingredient element = ingredients.Single();
Console.WriteLine(element.Name);
خروجی مثال بالا :
 Sugar
عملگر Single، یک امضاء دیگر نیز دارد که یک عبارت پیش بینی را می‌پذیرد. در صورتی که بیش از یک عنصر، با پیش بینی مطابقت داشته باشد و یا هیچ عنصری شرط پیش بینی را تامین نکند، استثنائی رخ خواهد داد.
Ingredient[] ingredients =
{
   new Ingredient { Name = "Sugar", Calories = 500 },
   new Ingredient {Name = "Butter", Calories = 150},
   new Ingredient {Name = "Milk", Calories = 500}
};
Ingredient element = ingredients.Single(x => x.Calories == 150);
Console.WriteLine(element.Name);
خروجی مثال بالا :
 Butter

پیاده سازی توسط عبارت‌های جستجو


معادل این عملگر، کلمه‌ی کلیدی جدیدی در عبارت‌های جستجو وجود ندارد و ترکیب دو روش می‌تواند خروجی دلخواه را تولید کند.


عملگر SingleOrDefault

عملگر SingleOrDefault همچون عملگر Single عمل می‌کند؛ اما با این تفاوت که اگر توالی هیچ عنصری نداشته باشد، مقدار پیش فرض نوع توالی، باز گردانده می‌شود و در صورتیکه هیچ عنصری شرط مشخص شده را تامین نکند، باز هم مقدار پیش فرض توالی، به جای رخ دادن استثناء باز گردانده می‌شود.
مثال : در این مثال هیچ عنصری با پیش بینی مشخص شده مطالبقت ندارد:
 Ingredient[] ingredients =
{
   new Ingredient { Name = "Sugar", Calories = 500 },
   new Ingredient {Name = "Egg", Calories = 100},
   new Ingredient {Name = "Milk", Calories = 50}
};
Ingredient element = ingredients.SingleOrDefault(x => x.Calories == 9999);
Console.WriteLine(element==null);
خروجی مثال بالا :
True
توجه داشته باشید که استثنائی رخ نداده است و مقدار پیش فرض انواع ارجاعی که Null می‌باشد باز گردانده شده است.

پیاده سازی توسط عبارت‌های جستجو

معادل این عملگر، کلمه‌ی کلیدی جدیدی در عبارت‌های جستجو وجود ندارد و ترکیب دو روش می‌تواند خروجی دلخواه را تولید کند.


عملگر ElementAt


عملگر ElementAt   عنصری را در یک جایگاه مشخص شده‌ی در توالی، باز می‌گرداند.
مثال: در کد زیر سومین عنصر توالی ورودی انتخاب می‌شود:
 Ingredient[] ingredients =
{
   new Ingredient { Name = "Sugar", Calories = 500 },
   new Ingredient {Name = "Egg", Calories = 100},
   new Ingredient {Name = "Milk", Calories = 50}
};

Ingredient element = ingredients.ElementAt(2);
Console.WriteLine(element.Name);
خروجی مثال بالا :
 Milk
باید دقت کرد که مقدار ارسالی به عملگر  ElementAt، اندیسی با نقطه‌ی آغاز صفر می‌باشد. بدین معنی که برای بدست آوردن اولین عنصر باید مقدار 0 را به عملگر ElementAt ارسال کرد. در صورتی که مقدار ارسالی با بازه اندیس‌های عناصر توالی مطابقت نداشته باشد (بزرگتر از شماره اندیس آخرین عنصر توالی باشد) یک استثناء رخ خواهد داد.
 System.ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection.

پیاده سازی توسط عبارت‌های جستجو


معادل این عملگر، کلمه‌ی کلیدی جدیدی در عبارت‌های جستجو وجود ندارد و ترکیب دو روش می‌تواند خروجی دلخواه را تولید کند.


عملگر ElementAtOrDefualt

عملگر ElementAtOrDefualt نیز همچون عملگر ElementAt کار می‌کند؛ اما در صورت وارد کردن اندیسی بزرگتر از اندیس مجاز توالی، دیگر یک استثناء رخ نخواهد داد و یک مقدار پیش فرض، بر اساس نوع عناصر توالی باز گردانده می‌شود.
مثال :
Ingredient[] ingredients =
{
   new Ingredient { Name = "Sugar", Calories = 500 },
   new Ingredient {Name = "Egg", Calories = 100},
   new Ingredient {Name = "Milk", Calories = 50}
};
Ingredient element = ingredients.ElementAtOrDefault(5);
Console.WriteLine(element==null);
خروجی مثال بالا:
 True

پیاده سازی توسط عبارت‌های جستجو


معادل این عملگر، کلمه‌ی کلیدی جدیدی در عبارت‌های جستجو وجود ندارد و ترکیب دو روش می‌تواند خروجی دلخواه را تولید کند.


عملگر DefaultIfEmpty
عملگر DefaultIfEmpty یک توالی را دریافت کرده و به دو شکل عمل می‌کند:
1- اگر توالی شامل حداقل یک عنصر باشد، این توالی بدون هیچ تغییری به خروجی ارسال می‌شود.
2- اگر توالی هیچ عنصری نداشته باشد، توالی خروجی خالی نخواهد بود. در این حالت توالی خروجی تنها یک عضو دارد و آن هم مقدار پیش فرضی بر اساس نوع توالی می‌باشد.
مثال :
Ingredient[] ingredients =
{
   new Ingredient { Name = "Sugar", Calories = 500 },
   new Ingredient {Name = "Egg", Calories = 100},
   new Ingredient {Name = "Milk", Calories = 50}
};

IEnumerable<Ingredient> query = ingredients.DefaultIfEmpty();
foreach (Ingredient item in query)
{
  Console.WriteLine(item.Name);
}
خروجی مثال بالا :
Sugar
Egg
Milk
همانطور که می‌بینید توالی خروجی دقیقا شبیه توالی ورودی می‌باشد.
کد زیر حالت دوم معرفی شده‌ی در تعریف DefaultIfEmpty را نشان می‌دهد.
Ingredient[] ingredients = { };
IEnumerable<Ingredient> query = ingredients.DefaultIfEmpty();
foreach (Ingredient item in query)
{
   Console.WriteLine(item == null);
}
خروجی کد بالا :
 True

پیاده سازی توسط عبارت‌های جستجو


معادل این عملگر، کلمه‌ی کلیدی جدیدی در عبارت‌های جستجو وجود ندارد و ترکیب دو روش می‌تواند خروجی دلخواه را تولید کند.
اشتراک‌ها
Memory Profiler در Visual Studio 2015

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

با Memory Usage Tool   که در VS2015 وجود دارد، شما میتوانید در حالت عیب یابی (debugging) ، مصرف حافظه‌ی خود را بسنجید.

·   Break-Aware Live Graph

یکی از قابلیت‌های کلیدی Diagnostic Tools window این است که فقط در زمانی که برنامه در حال اجرا با در زمانی که به وسیله‌ی breakpoint متوقف شده باشد،گراف را ثبت میکند. پس وقتی که به وسیله‌ی یک breakpoint برنامه متوقف شده باشد، این گراف هیچ پیشروی نخواهد کرد مگر اینکه شما برنامه را ادامه دهید. در این صورت گراف با شرایط فعلی برنامه شما پیشروی خواهد کرد.

شما میتوانید این ابزار را در موقعی که برنامه‌ی شما در حال اجراست در سمت راست مشاهده کنید. همچنین میتوانید در هنگامی که برنامه‌ی شما از حالت اجرا خارج شد این ابزار را از مسیر
  Debug -> Show Diagnostic Tools بیاورید

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

ابتدا بر روی گزینه‌ی Memory Usage کلیک کرده و گزینه‌ی Take Snapshot را انتخاب کنید

وقتی که بر روی این گزینه کلیک کردید مصرف حافظه‌ی شما به جزئیات به شما نشان داده میشود.

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

با این ابزار شما قادر خواهید بود که میزان تغییر حافظه بر اساس مقایسه بین دو Snapshot را بسنجید. برای اینکار شما میتوانید بعد از اعمال تغییرات، و یا بعد از زمانی مشخص دوباره بر روی گزینه‌ی Take Snapshot کلیک کنید. در این حالت شما میتوانید تغییری که بر روی حافظه‌ی شما اعمال گردیده را مشاهده کنید.


Memory Profiler در Visual Studio 2015
اشتراک‌ها
تفاوت دستور Delete و Truncate
تفاوت Delete  و  Truncate :
دستور  Delete  جز دستورات  DML  هست یا بعبارتی  Data Manipulation Language میباشد که برای اعمالی چون واکشی ، ذخیره سازی ، تغییرات و حذف مورد استفاده قرار میگیرند
دستور  Truncate  جز دستورات  DDL  هست یا بعبارتی  Data Definition Language می‌باشد و برای ایجاد ,  تغییرات  در ساختار آبجکت‌ها ( مثل جدول ، ایندکس و ..) مورد استفاده قرار میگیرد
دستور Delete  در زمان اجرا قفل گذاری را در سطح ردیف انجام میدهد (row lock) 
دستور Truncate  قفل گذاری را در سطح جدول در نظر میگیرد (table lock)
دستور Delete میتواند شامل فیلتر (where) برای حذف ردیف‌های خاصی باشد
دستور  Truncate  نمیتواند شامل فیلتر (where) باشد و روی حذف تمام جدول عمل میکند
نکته :
با استفاده از دستور Delete  میتوان بخشی از رکوردهای مشخص را حذف نمود ولی با دستور  Truncate  تمام رکوردها ی جدول حذف می‌شوند  (دستور Truncate نتیجه اجرای دو دستور  Drop  و  Create  می‌باشد)
دستور Delete  نسبت به دستور Truncate  کندتر عمل میکند زیرا تمام تغییرات در Log  ذخیره میکند در نتیجه قابلیت  Rollback  ممکن می‌باشد
دستور Truncate  نسبت به دستور Delete سریعتر عمل میکند زیرا به ازای  اجرای آن در فایل  Log  چیزی ثبت نمیشود در نتیجه قابلیت  Rollback  را ندارد
همراه دستور Delete  قابلیت اجرای  تریگر را داریم
در اجرای دستور Truncate  قابلیت اجرای تریگر را نداریم
نکته:
اگر  از Transaction استفاده کنیم هر  دو دستور  Delete  و  Truncate  قابلیت rollback  شدن را دارا هستند  
تفاوت دستور  Delete  و  Truncate
نظرات مطالب
کار با SignalR Core از طریق یک کلاینت جاوا اسکریپتی
تغییرات مورد نیاز جهت ارتقاء به ASP.NET Core 2.1

تغییرات سمت سرور:
1) ابتدا فایل csproj فوق به صورت زیر تغییر می‌کند:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>netcoreapp2.1</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.0-preview1-final" />
  </ItemGroup>
  <ItemGroup>
    <DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.1.0-preview1-final" />
    <DotNetCliToolReference Include="BundlerMinifier.Core" Version="2.6.362" />
  </ItemGroup>
  <Target Name="PrecompileScript" BeforeTargets="BeforeBuild">
    <Exec Command="dotnet bundle" />
  </Target>
</Project>
در اینجا چون از بسته‌ی Microsoft.AspNetCore.App استفاده می‌شود، دیگر نیازی به ذکر مستقیم بسته‌ی SignalR نیست و این بسته جزئی از آن است.

2) سپس در فایل Startup.cs، به ابتدای path باید یک / اضافه شود؛ وگرنه در زمان اجرا برنامه کرش می‌کند:
app.UseSignalR(routes =>
{
   routes.MapHub<MessageHub>(path: "/message");
});
3) تمام متدهای InvokeAsync شده‌اند SendAsync:
 await _messageHubContext.Clients.All.SendAsync("broadcastMessage", message);
و
 return Clients.All.SendAsync("broadcastMessage", message);


تغییرات سمت کلاینت:
1) نام بسته‌ی signalr در npm به صورت ذیل تغییر کرده‌است و دیگر signalr-client نیست:
 npm install @aspnet/signalr
2) پس از نصب بسته‌ی جدید signalr، مسیر فایل signalr.min.js در فایل bundleconfig.json به صورت زیر تغییر می‌کند:
 node_modules/@aspnet/signalr/dist/browser/signalr.min.js
3) متد invoke به send تغییر نام یافته‌است:
 connection.send('send', $('#message').val());
نظرات مطالب
ObservableCollection در Entity Framework
من در winform یک BindingSource دارم و این رو به گرید می‌دم. گرید به یک BindingSource بایند شده، اگه مقادیری رو تغییر بدم یا اضافه کنم در گرید (در واقع به BindingSource پشت صحنه) ، ChangeTracker تشخیص می‌ده و می‌تونم کار رو هندل کنم اما اگه سطری رو از BindingSource (یا همون گرید) حذف کنم ChangeTracker متوجه نمی‌شه و وضعیتشون Unchanged می‌مونه! (Local هم فایده نداره) و نمی‌تونم کل تغییرات رو ذخیره کنم با صدا زدن SaveChanges
در ChangeTracker ایتم هایی با State‌های Add, Modified رو می‌تونم ببینم ولی اگه ایتمی رو Delete کنم نمی‌تونم ببینمش و کاری انجام بدم! مشکل از کجاست؟
اینا مربوط به Context هست برای بررسی وضعیت تغییرات:
  public bool HasChanges()
        {
            return this.ChangeTracker.Entries().Any(e => e.State != EntityState.Unchanged);
        }

        public void RejectChanges()
        {
            foreach (var entry in this.ChangeTracker.Entries())
            {
                switch (entry.State)
                {
                    case EntityState.Modified:
                        entry.State = EntityState.Unchanged;
                        break;

                    case EntityState.Added:
                        entry.State = EntityState.Detached;
                        break;
                }
            }
        }
توضیحات بیشتر:
چرا مثل Add , Modify عمل Delete در Entity‌ها و ChangeTracker بصورت خودکار شنیده نمی‌شه؟! من یک کتابخانه‌ی مستقل تهیه کردم که گرید ،‌خودش یک کنترل BindingSource داره و اصلا نباید به EntityFramework و سرویسهایی که با Entity‌ها کار می‌کنن در ارتباط باشه ، BindingSource باید بتونه CurrentItem اش رو Remove کنه در حافظه مثل کاری که برای Add,Update انجام می‌ده و در انتها من (بعنوان کاربر نهایی) در لایه نمایش تصمیم بگیرم که تمام تغییرات انجام شده در گرید (Add, Modify, Delete) رو ذخیره یا لغو کنم.
******* شاید بشه با کمک رویداد BindingSource.ListChanged به نحوی حل کرد ولی اصلا جالب نمی‌شه چون BindingSource من یک POCO Entity هست و فقط و فقط چنتا property داره و اصلا از وضعیت خودش خبر نداره و قرار هم نیست بیشتر از این باشه و من نمی‌تونم وضعیتش رو تغییر بدم در این رویداد چون همچین چیزی نداره اصلا!