مطالب
کش خروجی API در ASP.NET Core با Redis
در این مقاله نمی‌خواهیم به طور عمیقی وارد جزییاتی مثل توضیح Redis یا کش بشویم؛ فرض شده‌است که کاربر با این مفاهیم آشناست. به طور خلاصه کش کردن یعنی همیشه به دیتابیس یا هارددیسک برای گرفتن اطلاعاتی که می‌خواهیم و گرفتنش هم کند است، وصل نشویم و بجای آن، اطلاعات را در یک محل موقتی که گرفتنش خیلی سریعتر بوده قرار دهیم و برای استفاده به آنجا برویم و اطلاعات را با سرعت بالا بخوانیم. کش کردن هم دسته بندی‌های مختلفی دارد که بر حسب سناریوهای مختلفی که وجود دارد، کاربرد خود را دارند. مثلا ساده‌ترین کش در ASP.NET Core، کش محلی (In-Memory Cache) می‌باشد که اینترفیس IMemoryCache را اعمال می‌کند و نیازی به هیچ پکیجی ندارد و به صورت درونی در ASP.NET Core در دسترس است که برای حالت توسعه، یا حالتیکه فقط یک سرور داشته باشیم، مناسب است؛ ولی برای برنامه‌های چند سروری، نوع دیگری از کش که به اصطلاح به آن Distributed Cache می‌گویند، بهتر است استفاده شود. چند روش برای پیاده‌سازی با این ساختار وجود دارد که نکته مشترکشان اعمال اینترفیس واحد IDistributedCache می‌باشد. در نتیجه‌ی آن، تغییر ساختار کش به روش‌های دیگر، که اینترفیس مشابهی را اعمال می‌کنند، با کمترین زحمت صورت می‌گیرد. این روش‌ها به طور خیلی خلاصه شامل موارد زیر می‌باشند: 

1- Distributed Memory Cache: در واقع Distributed نیست و کش معمولی است؛ فقط برای اعمال اینترفیس IDistributedCache که امکان تغییر آن در ادامه‌ی توسعه نرم‌افزار میسر باشد، این روش توسط مایکروسافت اضافه شده‌است. نیاز به نصب پکیجی را ندارد و به صورت توکار در ASP.NET Core در دسترس است.
2- Distributed SQL Server Cache: کاربرد چندانی ندارد. با توجه به اینکه هدف اصلی از کش کردن، افزایش سرعت و عدم اتصال به دیتابیس است، استفاده از حافظه‌ی رم، بجای دیتابیس ترجیح داده می‌شود.
3- Distributed Redis Cache: استفاده از Redis که به طور خلاصه یک دیتابیس Key/Value در حافظه است. سرعت بالایی دارد و محبوب‌ترین روش بین برنامه‌نویسان است. برای اعمال آن در ASP.NET Core نیاز به نصب پکیج می‌باشد.

موارد بالا انواع زیرساخت و ساختار (Cache Provider) برای پیاده‌سازی کش می‌باشند. روش‌های مختلفی برای استفاده از این Cache Providerها وجود دارد. مثلا یک روش، استفاده مستقیم در کدهای درونی متد یا کلاسمان می‌باشد و یا در روش دیگر می‌توانیم به صورت یک Middleware این پروسه را مدیریت کنیم، یا در روش دیگر (که موضوع این مقاله است) از ActionFilterAttribute استفاده می‌کنیم. یکی از روش‌های جالب دیگر کش کردن، اگر از Entity Framework به عنوان ORM استفاده می‌کنیم، استفاده از سطح دوم کش آن (EF Second Level Cache) می‌باشد. EF دو سطح کش دارد که سطح اول آن توسط خود Context به صورت درونی استفاده می‌شود و ما می‌توانیم از سطح دوم آن استفاده کنیم. مزیت آن به نسبت روش‌های قبلی این است که نتیجه‌ی کوئری ما (که با عبارات لامبدا نوشته می‌شود) را کش می‌کند و علاوه بر امکان تنظیم زمان انقضا برای این کش، در صورت تغییر یک entity خاص (انجام عملیات Update/Insert/Delete) خود به خود، کش کوئری مربوط به آن entity پاک می‌شود تا با مقدار جدید آن جایگزین شود که روش‌های دیگر این مزیت را ندارند. در این مقاله قرار نیست در مورد این روش کش صحبت کنیم. استفاده از این روش کش به صورت توکار در EF Core وجود ندارد و برای استفاده از آن در صورتی که از EF Core قبل از ورژن 3 استفاده می‌کنید می‌توانید از پکیج  EFSecondLevelCache.Core  و در صورت استفاده از EF Core 3 از پکیج  EF Core Second Level Cache Interceptor  استفاده نمایید که در هر دو حالت می‌توان هم از Memory Cache Provider و هم از Redis Cache Provider استفاده نمود.

در این مقاله می‌خواهیم Responseهای APIهایمان را در یک پروژه‌ی Web API، به ساده‌ترین حالت ممکن کش کنیم. زیرساخت این کش می‌تواند هر کدام از موارد ذکر شده‌ی بالا باشد. در این مقاله از Redis برای پیاده‌سازی آن استفاده می‌کنیم که با نصب پکیج Microsoft.Extensions.Caching.StackExchangeRedis انجام می‌گیرد. این بسته‌ی نیوگت که متعلق به مایکروسافت بوده و روش پایه‌ی استفاده از Redis در ASP.NET Core است، اینترفیس IDistributedCache را اعمال می‌کند:
Install-Package Microsoft.Extensions.Caching.StackExchangeRedis

سپس اینترفیس IResponseCacheService را می‌سازیم تا از این اینترفیس به جای IDistributedCache استفاده کنیم. البته می‌توان از IDistributedCache به طور مستقیم استفاده کرد؛ ولی چون همه‌ی ویژگی‌های این اینترفیس را نمی‌خواهیم و هم اینکه می‌خواهیم serialize کردن نتایج API را در کلاسی که از این اینترفیس ارث‌بری می‌کند (ResponseCacheService) بیاوریم (تا آن را کپسوله‌سازی (Encapsulation) کرده باشیم تا بعدا بتوانیم مثلا بجای پکیج Newtonsoft.Json، از System.Text.Json برای serialize کردن‌ها استفاده کنیم):
public interface IResponseCacheService
    {
        Task CacheResponseAsync(string cacheKey, object response, TimeSpan timeToLive);
        Task<string> GetCachedResponseAsync(string cacheKey);
    }
یادآوری: Redis قابلیت ذخیره‌ی داده‌هایی از نوع آرایه‌ی بایت‌ها را دارد (و نه هر نوع دلخواهی را). بنابراین اینجا ما بجای ذخیره‌ی مستقیم نتایج APIهایمان (که ممکن نیست)، می‌خواهیم ابتدا آن‌ها را با serialize کردن به نوع رشته‌ای (که فرمت json دارد) تبدیل کنیم و سپس آن را ذخیره نماییم.

حالا کلاس ResponseCacheService که این اینترفیس را اعمال می‌کند می‌سازیم: 
    public class ResponseCacheService : IResponseCacheService, ISingletonDependency
    {
        private readonly IDistributedCache _distributedCache;

        public ResponseCacheService(IDistributedCache distributedCache)
        {
            _distributedCache = distributedCache;
        }

        public async Task CacheResponseAsync(string cacheKey, object response, TimeSpan timeToLive)
        {
            if (response == null) return;
            var serializedResponse = JsonConvert.SerializeObject(response);
            await _distributedCache.SetStringAsync(cacheKey, serializedResponse, new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = timeToLive
            });
        }

        public async Task<string> GetCachedResponseAsync(string cacheKey)
        {
            var cachedResponse = await _distributedCache.GetStringAsync(cacheKey);
            return string.IsNullOrWhiteSpace(cachedResponse) ? null : cachedResponse;
        }
    }
دقت کنید که اینترفیس IDistributedCache در این کلاس استفاده شده است. اینترفیس ISingletonDependency صرفا یک اینترفیس نشان گذاری برای اعمال خودکار ثبت سرویس به صورت Singleton می‌باشد (اینترفیس را خودمان ساخته‌ایم و آن را برای رجیستر راحت سرویس‌هایمان تنظیم کرده‌ایم). اگر نمی‌خواهید از این روش برای ثبت این سرویس استفاده کنید، می‌توانید به صورت عادی این سرویس را رجیستر کنید که در ادامه، در قسمت مربوطه به صورت کامنت شده آمده است.

حالا کدهای لازم برای رجیستر کردن Redis و تنظیمات آن را در برنامه اضافه می‌کنیم. قدم اول ایجاد یک کلاس POCO به نام RedisCacheSettings است که به فیلدی به همین نام در appsettings.json نگاشت می‌شود:
public class RedisCacheSettings
    {
        public bool Enabled { get; set; }
        public string ConnectionString { get; set; }
        public int DefaultSecondsToCache { get; set; }
    }

این فیلد را در appsettings.json هم اضافه می‌کنیم تا در استارتاپ برنامه، با مپ شدن به کلاس RedisCacheSettings، قابلیت استفاده شدن در تنظیمات Redis را داشته باشد. 
"RedisCacheSettings": {
      "Enabled": true,
      "ConnectionString": "192.168.1.107:6379,ssl=False,allowAdmin=True,abortConnect=False,defaultDatabase=0,connectTimeout=500,connectRetry=3",
      "DefaultSecondsToCache": 600
    },

  حالا باید سرویس Redis را در متد ConfigureServices، به همراه تنظیمات آن رجیستر کنیم. می‌توانیم کدهای مربوطه را مستقیم در متد ConfigureServices بنویسیم و یا به صورت یک متد الحاقی در کلاس جداگانه بنویسیم و از آن در ConfigureServices استفاده کنیم و یا اینکه از روش Installer برای ثبت خودکار سرویس و تنظیماتش استفاده کنیم. اینجا از روش آخر استفاده می‌کنیم. برای این منظور کلاس CacheInstaller را می‌سازیم: 
    public class CacheInstaller : IServiceInstaller
    {
        public void InstallServices(IServiceCollection services, AppSettings appSettings, Assembly startupProjectAssembly)
        {
            var redisCacheService = appSettings.RedisCacheSettings;
            services.AddSingleton(redisCacheService);

            if (!appSettings.RedisCacheSettings.Enabled) return;

            services.AddStackExchangeRedisCache(options =>
                options.Configuration = appSettings.RedisCacheSettings.ConnectionString);

            // Below code applied with ISingletonDependency Interface
            // services.AddSingleton<IResponseCacheService, ResponseCacheService>();
        }
    }

خب تا اینجا اینترفیس اختصاصی خودمان را ساختیم و Redis را به همراه تنظیمات آن، رجیستر کردیم. برای اعمال کش، چند روش وجود دارد که همانطور که گفته شد، اینجا از روش ActionFilterAttribute استفاده می‌کنیم که یکی از راحت‌ترین راه‌های اعمال کش در APIهای ماست. کلاس CachedAttribute را ایجاد می‌کنیم:
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
    public class CachedAttribute : Attribute, IAsyncActionFilter
    {
        private readonly int _secondsToCache;
        private readonly bool _useDefaultCacheSeconds;
        public CachedAttribute()
        {
            _useDefaultCacheSeconds = true;
        }
        public CachedAttribute(int secondsToCache)
        {
            _secondsToCache = secondsToCache;
            _useDefaultCacheSeconds = false;
        }

        public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            var cacheSettings = context.HttpContext.RequestServices.GetRequiredService<RedisCacheSettings>();

            if (!cacheSettings.Enabled)
            {
                await next();
                return;
            }

            var cacheService = context.HttpContext.RequestServices.GetRequiredService<IResponseCacheService>();

            // Check if request has Cache
            var cacheKey = GenerateCacheKeyFromRequest(context.HttpContext.Request);
            var cachedResponse = await cacheService.GetCachedResponseAsync(cacheKey);

            // If Yes => return Value
            if (!string.IsNullOrWhiteSpace(cachedResponse))
            {
                var contentResult = new ContentResult
                {
                    Content = cachedResponse,
                    ContentType = "application/json",
                    StatusCode = 200
                };
                context.Result = contentResult;
                return;
            }

            // If No => Go to method => Cache Value
            var actionExecutedContext = await next();

            if (actionExecutedContext.Result is OkObjectResult okObjectResult)
            {
                var secondsToCache = _useDefaultCacheSeconds ? cacheSettings.DefaultSecondsToCache : _secondsToCache;
                await cacheService.CacheResponseAsync(cacheKey, okObjectResult.Value,
                    TimeSpan.FromSeconds(secondsToCache));
            }
        }

        private static string GenerateCacheKeyFromRequest(HttpRequest httpRequest)
        {
            var keyBuilder = new StringBuilder();
            keyBuilder.Append($"{httpRequest.Path}");
            foreach (var (key, value) in httpRequest.Query.OrderBy(x => x.Key))
            {
                keyBuilder.Append($"|{key}-{value}");
            }

            return keyBuilder.ToString();
        }
    }
در این کلاس، تزریق وابستگی‌های IResponseCacheService و RedisCacheSettings به روش خاصی انجام شده است و نمی‌توانستیم از روش Constructor Dependency Injection استفاده کنیم چون در این حالت می‌بایستی این ورودی در Controller مورد استفاده هم تزریق شود و سپس در اتریبیوت [Cached] بیاید که مجاز به اینکار نیستیم؛ بنابراین از این روش خاص استفاده کردیم. مورد دیگر فرمول ساخت کلید کش می‌باشد تا بتواند کش بودن یک Endpoint خاص را به طور خودکار تشخیص دهد که این متد در همین کلاس آمده است. 
 
حالا ما می‌توانیم با استفاده از attributeی به نام  [Cached]  که روی APIهای از نوع HttpGet قرار می‌گیرد آن‌ها را براحتی کش کنیم. کلاس بالا هم طوری طراحی شده (با دو سازنده متفاوت) که در حالت استفاده به صورت [Cached] از مقدار زمان پیشفرضی استفاده می‌کند که در فایل appsettings.json تنظیم شده است و یا اگر زمان خاصی را مد نظر داشتیم (مثال 1000 ثانیه) می‌توانیم آن را به صورت  [(Cached(1000]  بیاوریم. کلاس زیر نمونه‌ی استفاده‌ی از آن می‌باشد:
[Cached]
[HttpGet]
public IActionResult Get()
  {
    var rng = new Random();
    var weatherForecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
    {
      Date = DateTime.Now.AddDays(index),
      TemperatureC = rng.Next(-20, 55),
      Summary = Summaries[rng.Next(Summaries.Length)]
    })
      .ToArray();
    return Ok(weatherForecasts);
  }
بنابراین وقتی تنظیمات اولیه، برای پیاده‌سازی این کش انجام شود، اعمال کردن آن به سادگی قرار دادن یک اتریبیوت ساده‌ی [Cached] روی هر apiی است که بخواهیم خروجی آن را کش کنیم. فقط توجه نمایید که این روش فقط برای اکشن‌هایی که کد 200 را بر می‌گردانند، یعنی متد Ok را return می‌کنند (OkObjectResult) کار می‌کند. بعلاوه اگر از اتریبیوت ApiResultFilter یا مفهوم مشابه آن برای تغییر خروجی API به فرمت خاص استفاده می‌کنید، باید در آن تغییرات کوچکی را انجام دهید تا با این حالت هماهنگ شود. 
مطالب
چرا به الگوهای طراحی JavaScript نیاز داریم ؟
امروزه اهمیت یادگیری JavaScript بر هیچ کس پوشیده نیست ، API‌های جدید HTML 5 و امکانات جدید وب مثل Geo Location ، Offline storage ، Canvas ، Offline Web applications و ... همه نیاز به دانش JavaScript دارند. در نتیجه نیاز است علاوه بر یادگیری JavaScript به تمیز کد نوشتن جهت سهولت نگهداری آگاه بود.  همانطور که در کدهای سمت سرور مثل C# و یا PHP نیاز به استفاده از الگوهای طراحی (Design Patterns)است در JavaScript هم اوضاع به همین منوال است.
الگوی طراحی یک راه حل قابل استفاده مجدد است که برای حل مشکلات متداول در طراحی نرم افزار به کار می‌رود.
چرا به الگوی‌های طراحی JavaScript نیازمندیم ؟ 
  1. می خواهیم کد هایی با قابلیت استفاده‌ی مجدد بنویسیم ، استفاده از عملکرد‌های مشابه در سطح صفحات یک Web application یا چند Web Application.
  2. می خواهیم کد هایی با قابلیت نگهداری بنویسیم ، هر چه قدر در فاز توسعه کد‌های با کیفیت بنویسیم در فاز نگهداری از آن بهره می‌بریم. باید کد هایی بنویسیم که قابل Debug و خواندن توسط دیگر افراد تیم باشند.
  3. کدهای ما نباید با توابع و متغیر‌های دیگر پلاگین‌ها تداخل نامگزاری داشته باشند. در برنامه‌های امروزی بسیار مرسوم است که از پلاگین‌های Third party استفاده شود. می‌خواهیم با رعایت Encapsulation and modularization در کدهایمان از این تداخل جلوگیری کنیم.
معمولا کد‌های JavaScript که توسط اکثر ما نوشته می‌شود یک سری تابع پشت سرهم هست ، بدون هیچ کپسوله سازی : 
function getDate() {
    var now = new Date();
    var utc = now.getTime() + (now.getTimezoneOffset() * 60000);
    var est;
    est = new Date(utc + (3600000 * -4));
    return dateFormat(est, "dddd, mmmm dS, yyyy, h:MM:ss TT") + " EST";

}
function initiate_geolocationToTextbox() {
    navigator.geolocation.getCurrentPosition(handle_geolocation_queryToTextBox);
}

function handle_geolocation_queryToTextBox(position) {
    var longitude = position.coords.longitude;
    var latitude = position.coords.latitude;
    $("#IncidentLocation").val(latitude + " " + longitude);
}
به این روش کدنویسی Function Spaghetti Code گفته می‌شود که معایبی دارد : 
  • توابع و متغیر‌ها به Global scope برنامه افزوده می‌شوند.
  • کد Modular نیست.
  • احتمال رخ دادن Conflict در اسامی متغیر‌ها و توابع بالا می‌رود.
  • نگهداری کد به مرور زمان سخت می‌شود.
با شبیه سازی یک مثال مشکلات احتمالی را بررسی می‌کنیم : 
// file1.js
function saveState(obj) {
    // write code here to saveState of some object
    alert('file1 saveState');
}
// file2.js (remote team or some third party scripts)
function saveState(obj, obj2) {
     // further code...
    alert('file2 saveState");
}
همانطور که می‌بینید در این مثال در 2 فایل متفاوت در برنامه مان از 2 تابع با اسامی یکسان و امضای متفاوت استفاده کرده ایم . اگر فایل‌ها را اینگونه در برنامه آدرس دهی کنیم : 
<script src="file1.js" type="text/javascript"></script>
<script src="file2.js" type="text/javascript"></script>
متد saveState در فایلی که دیرتر آدرس داده شده (file2.js) ، متد saveState در file1.js را Override می‌کند ، در نتیجه عملکردی که از متد saveState در فایل اول انتظار داریم اتفاق نمی‌افتد.
 در پست بعدی به راه حل این مشکلات و کپسوله سازی خواهم پرداخت.
برای مطالعه‌ی بیشتر کتاب (Learning JavaScript Design Patterns) را از دست ندهید.
مطالب
بازنویسی متد مقدار دهی اولیه‌ی کاربر ادمین در ASP.NET Core Identity‌ توسط متد HasData در EF Core
فرض کنید قصد داریم متد «SeedDatabaseWithAdminUserAsync» را توسط روش جدید «مقدار دهی اولیه‌ی بانک اطلاعاتی توسط Entity framework Core» بازنویسی کنیم. در ادامه مراحل اینکار را مرور خواهیم کرد.


اضافه کردن نقش پیش‌فرض Admin

اولین تغییری که در اینجا مورد نیاز است، افزودن نقش پیش‌فرض Admin است. برای این منظور توسط یک IEntityTypeConfiguration جدید، تنظیمات موجودیت سفارشی Role برنامه را به نحو زیر انجام می‌دهیم:
namespace ASPNETCoreIdentitySample.DataLayer.Mappings
{
    public class RoleConfiguration : IEntityTypeConfiguration<Role>
    {
        private readonly SiteSettings _siteSettings;
        private readonly ILookupNormalizer _keyNormalizer;

        public RoleConfiguration(SiteSettings siteSettings, ILookupNormalizer keyNormalizer)
        {
            _siteSettings = siteSettings ?? throw new ArgumentNullException(nameof(siteSettings));
            _keyNormalizer = keyNormalizer ?? throw new ArgumentNullException(nameof(keyNormalizer));
        }

        public void Configure(EntityTypeBuilder<Role> builder)
        {
            builder.ToTable("AppRoles");

            var adminUserSeed = _siteSettings.AdminUserSeed;
            builder.HasData(
                new Role
                {
                    Id = 1,
                    Name = adminUserSeed.RoleName,
                    NormalizedName = _keyNormalizer.NormalizeName(adminUserSeed.RoleName),
                    ConcurrencyStamp = Guid.NewGuid().ToString()
                });
        }
    }
}
در حین افزودن new Role، دو نکته مدنظر قرار گرفته‌اند:
الف) NormalizedName از طریق سرویس ILookupNormalizer در ASP.NET Core Identity تامین می‌شود.
ب) SiteSettings در اینجا جهت دریافت نام نقش Admin از فایل appsettings.json، تعریف شده‌است (تامین مقدار پیش‌فرض).


اضافه کردن کاربر پیش‌فرض Admin

برای اضافه کردن کاربر پیش‌فرض ادمین، علاوه بر سرویس ILookupNormalizer که کار تامین مقادیر NormalizedEmail و NormalizedUserName را به عهده دارد، نیاز به سرویس IPasswordHasher نیز می‌باشد. از آن برای تامین مقدار فیلد PasswordHash، بر اساس سرویس هش کردن پسوردهای توکار ASP.NET Core Identity، استفاده می‌کنیم. برای این منظور توسط یک IEntityTypeConfiguration جدید، تنظیمات موجودیت سفارشی User برنامه را به نحو زیر انجام می‌دهیم:
    public class UserConfiguration : IEntityTypeConfiguration<User>
    {
        private readonly SiteSettings _siteSettings;
        private readonly ILookupNormalizer _keyNormalizer;
        private readonly IPasswordHasher<User> _passwordHasher;

        public UserConfiguration(
            SiteSettings siteSettings,
            ILookupNormalizer keyNormalizer,
            IPasswordHasher<User> passwordHasher)
        {
            _siteSettings = siteSettings ?? throw new ArgumentNullException(nameof(siteSettings));
            _keyNormalizer = keyNormalizer ?? throw new ArgumentNullException(nameof(keyNormalizer));
            _passwordHasher = passwordHasher ?? throw new ArgumentNullException(nameof(passwordHasher));
        }

        public void Configure(EntityTypeBuilder<User> builder)
        {
            builder.ToTable("AppUsers");

            var adminUserSeed = _siteSettings.AdminUserSeed;
            builder.HasData(
                new User
                {
                    Id = 1,
                    UserName = adminUserSeed.Username,
                    NormalizedUserName = _keyNormalizer.NormalizeName(adminUserSeed.Username),
                    Email = adminUserSeed.Email,
                    NormalizedEmail = _keyNormalizer.NormalizeEmail(adminUserSeed.Email),
                    EmailConfirmed = true,
                    IsEmailPublic = true,
                    LockoutEnabled = true,
                    TwoFactorEnabled = false,
                    PasswordHash = _passwordHasher.HashPassword(null, adminUserSeed.Password),
                    ConcurrencyStamp = Guid.NewGuid().ToString(),
                    SecurityStamp = string.Empty,
                    IsActive = true
                });
        }
    }


انتساب دادن نقش Admin، به کاربر Admin

تا اینجا نقش ادمین و کاربر ادمین را به صورت مجزا ایجاد کردیم. مرحله‌ی آخر، انتساب این نقش، به کاربر ادمین است که بر اساس Id این دو صورت می‌گیرد. برای این منظور توسط یک IEntityTypeConfiguration جدید، تنظیمات موجودیت سفارشی UserRole برنامه را به نحو زیر انجام می‌دهیم:
    public class UserRoleConfiguration : IEntityTypeConfiguration<UserRole>
    {
        public void Configure(EntityTypeBuilder<UserRole> builder)
        {
            builder.HasOne(userRole => userRole.Role)
                   .WithMany(role => role.Users)
                   .HasForeignKey(userRole => userRole.RoleId);

            builder.HasOne(userRole => userRole.User)
                   .WithMany(user => user.Roles)
                   .HasForeignKey(userRole => userRole.UserId);

            builder.ToTable("AppUserRoles");

            builder.HasData(
                new UserRole
                {
                    UserId = 1,
                    RoleId = 1
                });
        }
    }


اتصال IEntityTypeConfiguration به DbContext برنامه

IEntityTypeConfigurationهای تهیه شده، دارای سازنده‌هایی هستند که تعدادی سرویس را دریافت می‌کنند. روش معرفی آن‌ها به این صورت است:

الف) تامین دو سرویس ILookupNormalizer و IPasswordHasher در IDesignTimeDbContextFactory تهیه شده:
    public class ApplicationDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
    {
        public ApplicationDbContext CreateDbContext(string[] args)
        {
            // ....
            services.TryAddScoped<ILookupNormalizer, UpperInvariantLookupNormalizer>();
            services.TryAddScoped<IPasswordHasher<User>, PasswordHasher<User>>();
در حین انجام عملیات Migration، کار به افزودن و اجرای IEntityTypeConfigurationهای تهیه شده می‌رسد و این سرویس‌ها قرار است از DbContext تامین شوند؛ به همین جهت نیاز است روش تامین این سرویس‌ها را در همینجا معرفی کنیم.

ب) پس از تامین این سرویس‌ها، روش معرفی آن‌ها به متدهای modelBuilder.ApplyConfiguration در متد OnModelCreating، توسط متد this.GetService به صورت زیر است. متد this.GetService از فضای نام Microsoft.EntityFrameworkCore.Infrastructure تامین می‌شود و فقط یکبار در زمان Migration از طریق IDesignTimeDbContextFactory تامین خواهد شد:
using Microsoft.EntityFrameworkCore.Infrastructure;

namespace ASPNETCoreIdentitySample.DataLayer.Context
{
    public class ApplicationDbContext :
        IdentityDbContext<User, Role, int, UserClaim, UserRole, UserLogin, RoleClaim, UserToken>,
        IUnitOfWork
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options) { }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            var siteSettings = this.GetService<IOptionsSnapshot<SiteSettings>>()?.Value;
            var lookupNormalizer = this.GetService<ILookupNormalizer>();
            var passwordHasher =  this.GetService<IPasswordHasher<User>>()

            modelBuilder.ApplyConfiguration(new RoleConfiguration(siteSettings, lookupNormalizer));
            modelBuilder.ApplyConfiguration(new UserConfiguration(siteSettings, lookupNormalizer, passwordHasher));
            modelBuilder.ApplyConfiguration(new UserUsedPasswordConfiguration(siteSettings, passwordHasher));
// ...

پس از این تغییرات اگر دستور Migration زیر را صادر کنیم:
dotnet ef migrations --startup-project ../ASPNETCoreIdentitySample/ add V1
مقدار دهی اولیه‌ی نقش و کاربر ادمین و همچنین اتصال این دو را در فایل Migrations\ApplicationDbContextModelSnapshot.cs تولید شده، می‌توان مشاهده کرد.
مطالب
ارتقاء به ASP.NET Core 2.0 - معرفی بسته‌ی Microsoft.AspNetCore.All
یکی از مهم‌ترین تغییرات ASP.NET Core 2.0، نسبت به نگارش‌های قبلی آن، ارائه‌ی یک «متا پکیج» جدید به نام Microsoft.AspNetCore.All است. این بسته به همراه تمام وابستگی‌های مورد نیاز جهت توسعه‌ی برنامه‌های ASP.NET Core 2.0 است؛ این «تمام» شامل تمام بسته‌های Razor، بسته‌های MVC، بسته‌های EF Core و غیره است. به این ترتیب به روز رسانی بسته‌های وابسته‌ی به هم، بسیار ساده خواهد شد و همچنین به فایل‌های csproj بسیار خلوت و قابل مدیریتی، خواهیم رسید.


بسته‌ی Microsoft.AspNetCore.All فقط مخصوص پروژه‌های netcoreapp2.0 است

این «متا پکیج» تنها در پروژه‌هایی که TargetFramework آن‌ها به netcoreapp2.0 تنظیم شده‌است، قابل استفاده می‌باشد:
 <TargetFramework>netcoreapp2.0</TargetFramework>
اگر TargetFramework تنظیمی netstandard2.0 باشد، قادر به استفاده‌ی از آن نخواهید بود. در این حالت باید مانند سابق، تک تک بسته‌های مورد نیاز را در فایل csproj به صورت جداگانه‌ای تعریف کنید.


نحوه‌ی به روز رسانی پروژه‌ها جهت استفاده‌ی از Microsoft.AspNetCore.All

پیش از ناقص کردن برنامه و حذف بسته‌های نیوگتی که نباید از فایل csproj حذف شوند، ابتدا باید لیستی را که توسط «متا پکیج» Microsoft.AspNetCore.All ارائه می‌شود، بررسی کرد. این لیست را پس از نصب SDK جدید، در آدرس ذیل می‌توانید مشاهده کنید:
 C:\Program Files\dotnet\store\x64\netcoreapp2.0


روش دیگر یافتن این لیست، مراجعه‌ی به سایت نیوگت و بررسی قسمت dependencies آدرس https://www.nuget.org/packages/Microsoft.AspNetCore.All است:


سپس به فایل csproj ایی که دارای TargetFramework مساوی netcoreapp2.0 است مراجعه کرده و هر کدام از بسته‌هایی را که در این لیست قرار دارند ... حذف کنید. در آخر بجای تمام این مداخل حذف شده، یک مدخل کلی ذیل را تعریف کنید:
<ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
</ItemGroup>


سؤال: آیا استفاده‌ی از بسته‌ی Microsoft.AspNetCore.All، ارائه‌ی نهایی برنامه را حجیم نمی‌کند؟

اگر به مسیر dotnet\store ایی که پیشتر عنوان شد مراجعه کنید، در اینجا بیش از 180 بسته را خواهید یافت. در این حالت شاید به نظر برسد که حجم نهایی قابل توزیع برنامه‌های ASP.NET Core با استفاده از تک مدخل Microsoft.AspNetCore.All بسیار بالا خواهد رفت. اما ... خیر.
NET Core 2.0. به همراه ویژگی جدیدی است به نام Runtime store. هدف از آن، پیش نصب بسته‌ها بر روی سیستم جاری، در یک مکان مرکزی است تا دیگر در حین توزیع نهایی برنامه، نیازی به توزیع مجدد آن‌ها نباشد. به همین جهت، به آن می‌توان شبیه به مفهوم پیشین Global assembly cache یا GAC مخصوص NET Core. نگاه کرد. به علاوه تمام این بسته‌ها ngen شده و سرعت آغاز و اجرای برنامه‌ها را بهبود می‌بخشند.
زمانیکه SDK جدید NET Core 2.0. را نصب می‌کنید، تمام بسته‌های مورد نیاز آن، در مسیر مرکزی C:\Program Files\dotnet\store نصب می‌شوند. بنابراین سیستمی که به همراه این SDK باشد، حتما حاوی تمام وابستگی‌های ذکر شده‌ی در متاپکیج Microsoft.AspNetCore.All نیز خواهد بود و در این حالت نیازی به توزیع مجدد آن‌ها نیست.
پس از آن مهم‌ترین تفاوتی را که مشاهده خواهید کرد، کاهش حجم نهایی برنامه‌های ASP.NET Core 2.0 نسبت به نگارش‌های 1x است. برای آزمایش، یک برنامه‌ی ASP.NET Core 1.x و سپس یک برنامه‌ی ساده‌ی ASP.NET Core 2.x را publish کنید.
این تصویر، پوشه‌ی نهایی قابل توزیع یک برنامه‌ی ASP.NET Core 1.x را پس از publish نمایش می‌دهد:

و این تصویر، پوشه‌ی نهایی قابل توزیع یک برنامه‌ی ASP.NET Core 2.x را پس از publish نمایش می‌دهد:

در این حالت پوشه‌ی نهایی نگارش 1x شامل 94 آیتم و پوشه‌ی نهایی نگارش 2x شامل 13 آیتم است. یعنی حجم نهایی را که باید ارائه داد، به شدت کاهش یافته‌است.


بالا رفتن کارآیی تازه واردان به دنیای ASP.NET Core با متاپکیج جدید Microsoft.AspNetCore.All

یکی از مشکلاتی که به همراه کار با ASP.NET Core 1.x وجود دارد، مشخص نبودن محل قرارگیری ویژگی‌های جدید و بسته‌های مرتبط با آن‌ها است. همچنین به ازای هر ویژگی جدید باید یک بسته‌ی نیوگت جدید را نصب کرد و عموما یافتن این‌ها و یا دانستن وجود آن‌ها، کار دشواری می‌باشد.
اما زمانیکه متابسته‌ی Microsoft.AspNetCore.All به قسمت ارجاعات پروژه اضافه می‌شود، در آغاز کار برنامه، سیستم IntelliSense آن‌ها را پردازش کرده و بلافاصله در اختیار برنامه نویس قرار می‌گیرند. این قابلیت حتی در VSCode نیز همانند Visual Studio کار می‌کند و توسعه دهنده‌ها بلافاصله IntelliSense بسیار کاملی را از قابلیت‌های موجود در اختیار خواهند داشت؛ به همراه ویژگی‌های تکمیلی دیگری مانند افزودن و یا اصلاح ساده‌تر فضاهای نام مرتبط.
مطالب
طراحی شیء گرا: OO Design Heuristics - قسمت سوم
پیشنهاد می‌کنم قسمت‌های قبل را مطالعه کنید تا با اصطلاحات استفاده شده در ادامه مقالات آشنا باشید. در مقالات آتی، مباحث کمی قابل بحث‌تر خواهند بود.

Class Coupling and Cohesion

تعدادی از قواعد شهودی هم، با Coupling و Cohesion به ترتیب مابین و درون کلاس‌ها، سروکار دارند. تلاش ما در راستای افزایش Cohesion درون کلاس‌ها و سست کردن و کاهش Coupling مابین کلاس‌ها می‌باشد. این قواعد شهودی همین اهداف را در پارادایم action-oriented، در ارتباط با توابع دارند. هدف از Tight Cohesion (انسجام و چسبندگی قوی) در توابع، انسجام بالا و ارتباط نزدیک مابین کدهای موجود در تابع، می‌باشد. هدفی که Loose Coupling (اتصال سست و ضعیف، وابستگی ضعیف) در بین توابع دنبال می‌کند، اشاره دارد به اینکه اگر تابعی قصد استفاده از تابع دیگری را داشته باشد، باید وارد شدن و خروج از آن، از یک نقطه صورت گیرد. این مباحث منجربه مطرح شدن قواعد شهودی از جمله: «یک تابع باید طوری سازماندهی شود که تنها یک دستور return  داشته باشد»، در پارادایم action-oriented می‌شود.
ما در پارادایم شیء گرا، اهداف خود از Loose Coupling و Tight Cohesion را در سطح کلاس مطرح می‌کنیم. 5 شکل اصلی Coupling مابین کلاس‌ها به شرح زیر می‌باشد:

  •  Ni Coupling
  •  Export Coupling
  •  Overt Coupling
  •  Covert Coupling
  •  Surreptitious Coupling
Nil Coupling

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

Export Coupling

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

Overt Coupling

این نوع اتصال زمانی رخ می‌دهد که یک کلاس از جزئیات پیاده سازی کلاس دیگر با داشتن اجازه دسترسی از جانب آن، استفاده کند. به عنوان مثال، مکانیزم کلاس‌های friend در زبان سی پلاس پلاس، که امکان این را می‌دهد کلاس X اجازه دوستی به کلاس Y را اعطا کند و در این صورت کلاس Y می‌تواند به جزئیات پیاده سازی خصوصی کلاس X دسترسی داشته باشد.

Cover Coupling

این نوع اتصال هم به مانند Overt می‌باشد؛ با این تفاوت که هیچ اجازه دسترسی به کلاس Y داده نشده است. اگر زبانی داشته باشیم که به کلاس Y اجازه دهد خود را به عنوان دوست کلاس X معرفی کند، در این صورت نوع اتصال بین دو کلاس X و Y از نوع Covert می‌باشد. به عنوان مثال واقعی، می‌توان به استفاده از Reflection در دات نت اشاره کرد.

Surreptitious Coupling (اتصال پنهان)

آخرین نوع اتصال که بدترین حالت هم محسوب می‌شود، مربوط است به زمانیکه کلاس X به هر طریقی که شده از جزئیات داخلی کلاس Y آگاه باشد و از اعضای عمومی داده‌ای (public data member) آن کلاس استفاده کند. منظور این است که با تغییر این داده‌های کلاس متوجه میشود که بر روی عملیات b از کلاس چه تأثیری می‌گذارد و با اتکاء به این دستاورد، جزئیات داخلی خود را پیاده سازی می‌کند و یک اتصال پنهان را با کلاس Y ایجاد کرده است. در این حالت یک وابستگی قوی به صورت پنهان مابین رفتاری از کلاس Y و پیاده سازی کلاس X ایجاد شده است.

قاعده شهودی 2.7
اتصال و پیوستگی مابین کلاس‌ها باید از نوع Nil یا Export باشد؛ به این معنی که یک کلاس فقط از واسط عمومی کلاس دیگر استفاده کند یا کاری با آن نداشته باشد. (Classes should only exhibit nil or export coupling with other classes, that is, a class should only use operations in the public interface of another class or have nothing to do with that class.)
بجز این دو نوع اتصال، بقیه شکل‌های اتصال به طریقی اجازه دسترسی به جزئیات پیاده سازی کلاس‌ها را اعطا می‌کنند. در نتیجه باعث ایجاد وابستگی مابین پیاده سازی دو کلاس می‌شوند. این وابستگی ما بین پیاده سازی‌ها به محض نیاز به تغییر پیاده سازی یکی از کلاس‌ها ، باعث به وجود آمدن مشکلات نگهداری خواهند شد.
Cohesion درون کلاس‌ها سعی بر این دارد که مطمئن شود تمام اجزای یک کلاس به شدت باهم مرتبط هستند. تعدادی از قواعد شهودی نیز در ادامه بر این خصوصیت دلالت دارند.

قاعده شهودی 2.8
یک کلاس باید یک و تنها یک Key Abstraction را تسخیر نماید. (A class should capture one and only one key abstraction)
یک Key Abstraction به عنوان یک Entity در Domain Model تعریف می‌شود و اغلب در غالب اسم در اسناد و مشخصات نیازمندی‌ها ظاهر می‌شوند. هر کدام از آنها باید فقط به یک کلاس نگاشت پیدا کنند. اگر این نگاشت به بیش از یک کلاس انجام گیرد، در نتیجه احتمالا طراح هر تابع را به عنوان یک کلاس تسخیر کرده است. اگر بیش از یک Key Abstraction به یک کلاس نگاشت پیدا کرده باشد، پس احتمالا طراح یک سیستم متمرکز را طراحی کرده است. این کلاس‌ها Vague Classes نامیده می‌شوند و باید آنها در دو کلاس یا بیشتر، تسخیر شوند.

قاعده شهودی 2.9
داده و رفتار مرتبط را در یک جا (کلاس) نگه دارید. (Keep related data and behavior in one place)
در واقع هدفی که این قاعده به دنبال آن می‌باشد این است که هر دو جزء تشکیل دهنده یک Key Abstraction ، یعنی همان داده و رفتار، باید توسط فقط یک کلاس تسخیر شوند. با نقض این قاعده، توسعه دهنده باید با قرار داد (Convention) خاصی برنامه نویسی کند. 
راه شناسایی
طراح باید کلاس‌هایی را که مرتبا داده‌های مورد نیاز خود را با متدهای get از سایر کلاس‌ها دریافت می‌کنند، شناسایی کند. زیرا این نوع کلاس‌ها این قاعده شهودی را نقض کرده‌اند.

مثال واقعی
استفاده از الگوی Domain Model ارائه شده توسط اقای Martin Fowler که دقیقا اشاره به این قاعده دارد.
یا برعکس آن ضد الگوی Anemic Domain Model که ناقض این قاعده می‌باشد. 
در قسمت اول اشاره کردیم این قواعد را به راحتی می‌توان در صورت نیاز نقض کرد. بعضی از مواقع نیاز به طراحی فیزیکی است که باعث تغییر در طراحی منطقی شده و چه بسا می‌تواند باعث نقض هر کدام از این قواعد شهودی نیز شود. اگر به بخش پروژه‌های سایت رجوع کنید این نقض کاملا مشهود (DomainClasses و ServiceLayer موجود در طراحی فیزیکی آنها) می‌باشد (بیشتر از Anemic Domain Model استفاده شده است)؛ ولی نمی‌توان گفت که این کار اشتباه است.

قاعده شهودی 2.10
اطلاعات نامرتبط به هم را در کلاس‌های جدا از هم قرار دهید. ((Spin off nonrelated information into another class (i.e., noncommunicating behavior) 

هدف از این قاعده این است که اگر کلاسی داریم که یکسری از متدهایش با بخشی از داده و یکسری دیگر با بخش دیگر داده‌ها کار میکنند، در واقع شما دو Key Abstraction را به یک کلاس نگاشت کرده اید (Vague Class) و باید آنها را به کلاس‌های جدا نگاشت کنید.

شکل 2.6 A class with noncommunicating behavior

مثال واقعی

به کلاس Dictionary در تصویر زیر توجه کنید.
برای تعداد کمی داده، بهترین پیاده سازی با استفاده از List و در مقابل برای تعداد داده زیاد بهترین پیاده سازی، استفاده از HashTable می‌باشد. هر یک از این پیاده سازی‌ها، به متدهایی برای add و find کلمات نیاز دارند. طراحی سمت چپ تصویر نشان از نقض این قاعده شهودی دارد.

شکل 2.7 (Noncommunicating behavior (real-world example

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

مطالب
روش یکی کردن پروژه‌های React و ASP.NET Core
یک روش کار کردن با پروژه‌های SPA، توسعه‌ی مجزای قسمت‌های front-end و back-end است. برای مثال پروژه‌ی React را به صورت جداگانه‌ای توسعه می‌دهیم، پروژه‌ی ASP.NET Core را نیز به همین صورت. هنگام آزمایش برنامه، در یکی دستور npm start را اجرا می‌کنیم تا وب سرور آزمایشی React، آن‌را در آدرس http://localhost:3000 قابل دسترسی کند و در دیگری دستور dotnet watch run را صادر می‌کنیم تا برنامه‌ی وب ASP.NET Core را بر روی آدرس https://localhost:5001 مهیا کند. سپس برای اینکه از پورت 3000 بتوان با پورت 5001 کار کرد، نیاز خواهد بود تا CORS را در برنامه‌ی ASP.NET Core فعالسازی کنیم. در حین ارائه‌ی نهایی برنامه نیز هر کدام را به صورت مجزا publish کرده و بعد هم خروجی نهایی پروژه‌ی SPA را در پوشه‌ی wwwroot برنامه‌ی وب کپی می‌کنیم تا قابل دسترسی و استفاده شود. روش دیگری نیز برای یکی/ساده سازی این تجربه وجود دارد که در این مطلب به آن خواهیم پرداخت.


پیشنیاز: ایجاد یک برنامه‌ی خالی React و ASP.NET Core

یک پوشه‌ی خالی را ایجاد کرده و در آن دستور dotnet new react را صادر کنید، تا قالب خاص پروژه‌های React یکی سازی شده‌ی با پروژه‌های ASP.NET Core، یک پروژه‌ی جدید را ایجاد کند.


همانطور که در تصویر فوق نیز مشاهده می‌کنید، این پروژه از دو برنامه تشکیل شده‌است:
الف) برنامه‌ی SPA که در پوشه‌ی ClientApp قرار گرفته‌است و شامل کدهای کامل یک برنامه‌ی React است.
ب) برنامه‌ی سمت سرور ASP.NET Core که یک برنامه‌ی متداول وب، به همراه فایل Startup.cs و سایر فایل‌های مورد نیاز آن است.

در ادامه نکات ویژه‌ی ساختار این پروژه را بررسی خواهیم کرد.


تجربه‌ی توسعه‌ی برنامه‌ها توسط این قالب ویژه

اکنون اگر این پروژه‌ی وب را برای مثال با فشردن دکمه‌ی F5 و یا اجرای دستور dotnet run، اجرا کنیم، چه اتفاقی رخ می‌دهد؟
- به صورت خلاصه برنامه‌ی ASP.NET Core شروع به کار کرده و سبب ارائه همزمان برنامه‌ی SPA نیز خواهد شد.
- پورتی که برنامه‌ی وب بر روی آن قرار دارد، با پورتی که برنامه‌ی React بر روی روی آن ارائه می‌شود، یکی است. یعنی نیازی به تنظیمات CORS را ندارد.
- در این حالت اگر در برنامه‌ی React تغییری را ایجاد کنیم (در هر قسمتی از آن)، hot reloading آن هنوز هم برقرار است و سبب بارگذاری مجدد برنامه‌ی SPA در مرورگر خواهد شد و برای اینکار نیازی به توقف و راه اندازی مجدد برنامه‌ی ASP.NET Core نیست.

اما این تجربه‌ی روان کاربری و توسعه، چگونه حاصل شده‌است؟


بررسی ساختار فایل Startup.cs یک پروژه‌ی مبتنی بر dotnet new react

برای درک نحوه‌ی عملکرد این قالب ویژه، نیاز است از فایل Startup.cs آن شروع کرد.
// ...
using Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer;

namespace dotnet_template_sample
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {

            services.AddControllersWithViews();

            // In production, the React files will be served from this directory
            services.AddSpaStaticFiles(configuration =>
            {
                configuration.RootPath = "ClientApp/build";
            });
        }
در ابتدا تعریف فضای نام SpaServices را مشاهده می‌کنید. بسته‌ی متناظر با آن در فایل csproj برنامه به صورت زیر ثبت شده‌است:
<ItemGroup>
   <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="3.1.2" />
</ItemGroup>
این بسته، همان بسته‌ی جدید SpaServices است و در NET 5x. نیز پشتیبانی خواهد شد .

در متد ConfigureServices، ثبت سرویس‌های مرتبط با فایل‌های استاتیک پروژه‌ی SPA، توسط متد AddSpaStaticFiles صورت گرفته‌است. در اینجا RootPath آن، به پوشه‌ی ClientApp/build اشاره می‌کند. البته این پوشه هنوز در این ساختار، قابل مشاهده نیست؛ اما زمانیکه پروژه‌ی ASP.NET Core را برای ارائه‌ی نهایی، publish کردیم، به صورت خودکار ایجاد شده و حاوی فایل‌های قابل ارائه‌ی برنامه‌ی React نیز خواهد بود.

قسمت مهم دیگر کلاس آغازین برنامه، متد Configure آن است:
// ...
using Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer;

namespace dotnet_template_sample
{
    public class Startup
    {
        // ...

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            // ...
            app.UseStaticFiles();
            app.UseSpaStaticFiles();

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller}/{action=Index}/{id?}");
            });

            app.UseSpa(spa =>
            {
                spa.Options.SourcePath = "ClientApp";

                if (env.IsDevelopment())
                {
                    spa.UseReactDevelopmentServer(npmScript: "start");
                }
            });
        }
    }
}
در اینجا ثبت سه میان افزار جدید را مشاهده می‌کنید:
- متد UseSpaStaticFiles، سبب ثبت میان‌افزاری می‌شود که امکان دسترسی به فایل‌های استاتیک پوشه‌ی ClientApp حاوی برنامه‌ی React را میسر می‌کند؛ مسیر این پوشه را در متد ConfigureServices تنظیم کردیم.
- متد UseSpa، سبب ثبت میان‌افزاری می‌شود که دو کار مهم را انجام می‌دهد:
1- کار اصلی آن، ثبت مسیریابی معروف catch all است تا مسیریابی‌هایی را که توسط کنترلرهای برنامه‌ی ASP.NET Core مدیریت نمی‌شوند، به سمت برنامه‌ی React هدایت کند. برای مثال مسیر https://localhost:5001/api/users به یک کنترلر API برنامه‌ی سمت سرور ختم می‌شود، اما سایر مسیرها مانند https://localhost:5001/login قرار است صفحه‌ی login برنامه‌ی سمت کلاینت SPA را نمایش دهند و متناظر با اکشن متد خاصی در کنترلرهای برنامه‌ی وب ما نیستند. در این حالت، کار این مسیریابی catch all، نمایش صفحه‌ی پیش‌فرض برنامه‌ی SPA است.
2- بررسی می‌کند که آیا شرایط IsDevelopment برقرار است؟ آیا در حال توسعه‌ی برنامه هستیم؟ اگر بله، میان‌افزار دیگری را به نام UseReactDevelopmentServer، اجرا و ثبت می‌کند.

برای درک عملکرد میان‌افزار ReactDevelopmentServer نیاز است به سورس آن مراجعه کرد. این میان‌افزار بر اساس پارامتر start ای که دریافت می‌کند، سبب اجرای npm run start خواهد شد. به این ترتیب دیگر نیازی به اجرای جداگانه‌ی این دستور نخواهد بود و همچنین این اجرا، به همراه تنظیمات proxy مخصوصی نیز هست تا پورت اجرایی برنامه‌ی React و برنامه‌ی ASP.NET Core یکی شده و دیگر نیازی به تنظیمات CORS مخصوص برنامه‌های React نباشد. بنابراین hot reloading ای که از آن صحبت شد، توسط ASP.NET Core مدیریت نمی‌شود. در پشت صحنه همان npm run start اصلی برنامه‌های React، در حال اجرای وب سرور آزمایشی React است که از hot reloading پشتیبانی می‌کند.

یک مشکل: با این تنظیم، هربار که برنامه‌ی ASP.NET Core اجرا می‌شود (به علت تغییرات در کدها و فایل‌های پروژه)، سبب اجرای مجدد و پشت صحنه‌ی react development server نیز خواهد شد که ... آغاز برنامه را در حالت توسعه، کند می‌کند. برای رفع این مشکل می‌توان این وب سرور توسعه‌ی برنامه‌های React را به صورت جداگانه‌ای اجرا کرد و فقط تنظیمات پروکسی آن‌را در اینجا ذکر نمود:
// replace
spa.UseReactDevelopmentServer(npmScript: "start");
// with
spa.UseProxyToSpaDevelopmentServer("http://localhost:3000");
در اینجا فقط کافی است سطر UseReactDevelopmentServer را با تنظیم UseProxyToSpaDevelopmentServer که به آدرس وب سرور توسعه‌ی برنامه‌های React اشاره می‌کند، تنظیم کنیم. بدیهی است در اینجا حالت باید از طریق خط فرمان به پوشه‌ی clientApp وارد شد و دستور npm start را یکبار به صورت دستی اجرا کرد، تا این وب سرور، راه اندازی شود.


تغییرات ویژه‌ی فایل csproj برنامه

اگر به فایل csproj برنامه دقت کنیم، دو تغییر جدید نیز در آن قابل مشاهده هستند:
الف) نصب خودکار وابستگی‌های برنامه‌ی client
  <Target Name="DebugEnsureNodeEnv"
     BeforeTargets="Build" 
     Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
    <!-- Ensure Node.js is installed -->
    <Exec Command="node --version" ContinueOnError="true">
      <Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
    </Exec>
    <Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." />
    <Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." />
    <Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
  </Target>
در این تنظیم، در حالت build و debug، ابتدا بررسی می‌کند که آیا پوشه‌ی node_modules برنامه‌ی SPA وجود دارد؟ اگر خیر، ابتدا مطمئن می‌شود که node.js بر روی سیستم نصب است و سپس دستور npm install را صادر می‌کند تا تمام وابستگی‌های برنامه‌ی client، دریافت و نصب شوند.

ب) یکی کردن تجربه‌ی publish برنامه‌ی ASP.NET Core با publish پروژه‌های React
  <Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
    <!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
    <Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
    <Exec WorkingDirectory="$(SpaRoot)" Command="npm run build" />

    <!-- Include the newly-built files in the publish output -->
    <ItemGroup>
      <DistFiles Include="$(SpaRoot)build\**" />
      <ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
        <RelativePath>%(DistFiles.Identity)</RelativePath>
        <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
        <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
      </ResolvedFileToPublish>
    </ItemGroup>
  </Target>
میان‌افزار ReactDevelopmentServer کار اجرا و پروکسی دستور npm run start را در حالت توسعه انجام می‌دهد. اما در حالت ارائه‌ی نهایی چطور؟ در اینجا نیاز است دستور npm run build اجرا شده و فایل‌های مخصوص ارائه‌ی نهایی برنامه‌ی React تولید و سپس به پوشه‌ی wwwroot، کپی شوند. تنظیم فوق، دقیقا همین کار را در حین publish برنامه‌ی ASP.NET Core، به صورت خودکار انجام می‌دهد و شامل این مراحل است:
-  ابتدا npm install را جهت اطمینان از به روز بودن وابستگی‌های برنامه مجددا اجرا می‌کند.
- سپس npm run build را برای تولید فایل‌های قابل ارائه‌ی برنامه‌ی React اجرا می‌کند.
- در آخر تمام فایل‌های پوشه‌ی ClientApp/build تولیدی را به بسته‌ی نهایی توزیعی برنامه‌ی ASP.NET Core، اضافه می‌کند.
مطالب
استفاده از EF در اپلیکیشن های N-Tier : قسمت چهارم
در قسمت قبل تشخیص تغییرات توسط Web API را بررسی کردیم. در این قسمت نگاهی به پیاده سازی Change-tracking در سمت کلاینت خواهیم داشت.


ردیابی تغییرات در سمت کلاینت توسط Web API

فرض کنید می‌خواهیم از سرویس‌های REST-based برای انجام عملیات CRUD روی یک Object graph استفاده کنیم. همچنین می‌خواهیم رویکردی در سمت کلاینت برای بروز رسانی کلاس موجودیت‌ها پیاده سازی کنیم که قابل استفاده مجدد (reusable) باشد. علاوه بر این دسترسی داده‌ها توسط مدل Code-First انجام می‌شود.

در مثال جاری یک اپلیکیشن کلاینت (برنامه کنسول) خواهیم داشت که سرویس‌های ارائه شده توسط پروژه Web API را فراخوانی می‌کند. هر پروژه در یک Solution مجزا قرار دارد، با این کار یک محیط n-Tier را شبیه سازی می‌کنیم.

مدل زیر را در نظر بگیرید.

همانطور که می‌بینید مدل مثال جاری مشتریان و شماره تماس آنها را ارائه می‌کند. می‌خواهیم مدل‌ها و کد دسترسی به داده‌ها را در یک سرویس Web API پیاده سازی کنیم تا هر کلاینتی که به HTTP دسترسی دارد بتواند از آن استفاده کند. برای ساخت سرویس مذکور مراحل زیر را دنبال کنید.

  • در ویژوال استودیو پروژه جدیدی از نوع ASP.NET Web Application بسازید و قالب پروژه را Web API انتخاب کنید. نام پروژه را به Recipe4.Service تغییر دهید.
  • کنترلر جدیدی با نام CustomerController به پروژه اضافه کنید.
  • کلاسی با نام BaseEntity ایجاد کنید و کد آن را مطابق لیست زیر تغییر دهید. تمام موجودیت‌ها از این کلاس پایه مشتق خواهند شد که خاصیتی بنام TrackingState را به آنها اضافه می‌کند. کلاینت‌ها هنگام ویرایش آبجکت موجودیت‌ها باید این فیلد را مقدار دهی کنند. همانطور که می‌بینید این خاصیت از نوع TrackingState enum مشتق می‌شود. توجه داشته باشید که این خاصیت در دیتابیس ذخیره نخواهد شد. با پیاده سازی enum وضعیت ردیابی موجودیت‌ها بدین روش، وابستگی‌های EF را برای کلاینت از بین می‌بریم. اگر قرار بود وضعیت ردیابی را مستقیما از EF به کلاینت پاس دهیم وابستگی‌های بخصوصی معرفی می‌شدند. کلاس DbContext اپلیکیشن در متد OnModelCreating به EF دستور می‌دهد که خاصیت TrackingState را به جدول موجودیت نگاشت نکند.
public abstract class BaseEntity
{
    protected BaseEntity()
    {
        TrackingState = TrackingState.Nochange;
    }

    public TrackingState TrackingState { get; set; }
}

public enum TrackingState
{
    Nochange,
    Add,
    Update,
    Remove,
}
  • کلاس‌های موجودیت Customer و PhoneNumber را ایجاد کنید و کد آنها را مطابق لیست زیر تغییر دهید.
public class Customer : BaseEntity
{
    public int CustomerId { get; set; }
    public string Name { get; set; }
    public string Company { get; set; }
    public virtual ICollection<Phone> Phones { get; set; }
}

public class Phone : BaseEntity
{
    public int PhoneId { get; set; }
    public string Number { get; set; }
    public string PhoneType { get; set; }
    public int CustomerId { get; set; }
    public virtual Customer Customer { get; set; }
}
  • با استفاده از NuGet Package Manager کتابخانه Entity Framework 6 را به پروژه اضافه کنید.
  • کلاسی با نام Recipe4Context ایجاد کنید و کد آن را مطابق لیست زیر تغییر دهید. در این کلاس از یکی از قابلیت‌های جدید EF 6 بنام "Configuring Unmapped Base Types" استفاده کرده ایم. با استفاده از این قابلیت جدید هر موجودیت را طوری پیکربندی می‌کنیم که خاصیت TrackingState را نادیده بگیرند. برای اطلاعات بیشتر درباره این قابلیت EF 6 به این لینک مراجعه کنید.
public class Recipe4Context : DbContext
{
    public Recipe4Context() : base("Recipe4ConnectionString") { }
    public DbSet<Customer> Customers { get; set; }
    public DbSet<Phone> Phones { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        // Do not persist TrackingState property to data store
        // This property is used internally to track state of
        // disconnected entities across service boundaries.
        // Leverage the Custom Code First Conventions features from Entity Framework 6.
        // Define a convention that performs a configuration for every entity
        // that derives from a base entity class.
        modelBuilder.Types<BaseEntity>().Configure(x => x.Ignore(y => y.TrackingState));
        modelBuilder.Entity<Customer>().ToTable("Customers");
        modelBuilder.Entity<Phone>().ToTable("Phones");
}
}
  • فایل Web.config پروژه را باز کنید و رشته اتصال زیر را به قسمت ConnectionStrings اضافه نمایید.
<connectionStrings>
  <add name="Recipe4ConnectionString"
    connectionString="Data Source=.;
    Initial Catalog=EFRecipes;
    Integrated Security=True;
    MultipleActiveResultSets=True"
    providerName="System.Data.SqlClient" />
</connectionStrings>
  • فایل Global.asax را باز کنید و کد زیر را به متد Application_Start اضافه نمایید. این کد بررسی Entity Framework Model Compatibility را غیرفعال می‌کند و به JSON serializer دستور می‌دهد که self-referencing loop خواص پیمایشی را نادیده بگیرد. این حلقه بدلیل رابطه bidirectional بین موجودیت‌های Customer و PhoneNumber بوجود می‌آید.
protected void Application_Start()
{
    // Disable Entity Framework Model Compatibilty
    Database.SetInitializer<Recipe1Context>(null);
    // The bidirectional navigation properties between related entities
    // create a self-referencing loop that breaks Web API's effort to
    // serialize the objects as JSON. By default, Json.NET is configured
    // to error when a reference loop is detected. To resolve problem,
    // simply configure JSON serializer to ignore self-referencing loops.
    GlobalConfiguration.Configuration.Formatters.JsonFormatter
        .SerializerSettings.ReferenceLoopHandling =
            Newtonsoft.Json.ReferenceLoopHandling.Ignore;
    ...
}
  • کلاسی با نام EntityStateFactory بسازید و کد آن را مطابق لیست زیر تغییر دهید. این کلاس مقدار خاصیت TrackingState که به کلاینت‌ها ارائه می‌شود را به مقادیر متناظر کامپوننت‌های ردیابی EF تبدیل می‌کند.
public static EntityState Set(TrackingState trackingState)
{
    switch (trackingState)
    {
        case TrackingState.Add:
            return EntityState.Added;
        case TrackingState.Update:
            return EntityState.Modified;
        case TrackingState.Remove:
            return EntityState.Deleted;
        default:
            return EntityState.Unchanged;
    }
}
  • در آخر کد کنترلر CustomerController را مطابق لیست زیر بروز رسانی کنید.
public class CustomerController : ApiController
{
    // GET api/customer
    public IEnumerable<Customer> Get()
    {
        using (var context = new Recipe4Context())
        {
            return context.Customers.Include(x => x.Phones).ToList();
        }
    }

    // GET api/customer/5
    public Customer Get(int id)
    {
        using (var context = new Recipe4Context())
        {
            return context.Customers.Include(x => x.Phones).FirstOrDefault(x => x.CustomerId == id);
        }
    }

    [ActionName("Update")]
    public HttpResponseMessage UpdateCustomer(Customer customer)
    {
        using (var context = new Recipe4Context())
        {
            // Add object graph to context setting default state of 'Added'.
            // Adding parent to context automatically attaches entire graph
            // (parent and child entities) to context and sets state to 'Added'
            // for all entities.
            context.Customers.Add(customer);
            foreach (var entry in context.ChangeTracker.Entries<BaseEntity>())
            {
                entry.State = EntityStateFactory.Set(entry.Entity.TrackingState);
                if (entry.State == EntityState.Modified)
                {
                    // For entity updates, we fetch a current copy of the entity
                    // from the database and assign the values to the orginal values
                    // property from the Entry object. OriginalValues wrap a dictionary
                    // that represents the values of the entity before applying changes.
                    // The Entity Framework change tracker will detect
                    // differences between the current and original values and mark
                    // each property and the entity as modified. Start by setting
                    // the state for the entity as 'Unchanged'.
                    entry.State = EntityState.Unchanged;
                    var databaseValues = entry.GetDatabaseValues();
                    entry.OriginalValues.SetValues(databaseValues);
                }
            }

        context.SaveChanges();
    }

    return Request.CreateResponse(HttpStatusCode.OK, customer);
}

    [HttpDelete]
    [ActionName("Cleanup")]
    public HttpResponseMessage Cleanup()
    {
        using (var context = new Recipe4Context())
        {
            context.Database.ExecuteSqlCommand("delete from phones");
            context.Database.ExecuteSqlCommand("delete from customers");
            return Request.CreateResponse(HttpStatusCode.OK);
        }
    }
}
حال اپلیکیشن کلاینت (برنامه کنسول) را می‌سازیم که از این سرویس استفاده می‌کند.

  • در ویژوال استودیو پروژه جدیدی از نوع Console Application بسازید و نام آن را به Recipe4.Client تغییر دهید.
  • فایل program.cs را باز کنید و کد آن را مطابق لیست زیر تغییر دهید.
internal class Program
{
    private HttpClient _client;
    private Customer _bush, _obama;
    private Phone _whiteHousePhone, _bushMobilePhone, _obamaMobilePhone;
    private HttpResponseMessage _response;

    private static void Main()
    {
        Task t = Run();
        t.Wait();
        Console.WriteLine("\nPress <enter> to continue...");
        Console.ReadLine();
    }

    private static async Task Run()
    {
        var program = new Program();
        program.ServiceSetup();
        // do not proceed until clean-up completes
        await program.CleanupAsync();
        program.CreateFirstCustomer();
        // do not proceed until customer is added
        await program.AddCustomerAsync();
        program.CreateSecondCustomer();
        // do not proceed until customer is added
        await program.AddSecondCustomerAsync();
        // do not proceed until customer is removed
        await program.RemoveFirstCustomerAsync();
        // do not proceed until customers are fetched
        await program.FetchCustomersAsync();
    }

    private void ServiceSetup()
    {
        // set up infrastructure for Web API call
        _client = new HttpClient { BaseAddress = new Uri("http://localhost:62799/") };
        // add Accept Header to request Web API content negotiation to return resource in JSON format
        _client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue
        ("application/json"));
    }
    private async Task CleanupAsync()
    {
        // call the cleanup method from the service
        _response = await _client.DeleteAsync("api/customer/cleanup/");
    }

    private void CreateFirstCustomer()
    {
        // create customer #1 and two phone numbers
        _bush = new Customer
        {
            Name = "George Bush",
            Company = "Ex President",
            // set tracking state to 'Add' to generate a SQL Insert statement
            TrackingState = TrackingState.Add,
        };
        _whiteHousePhone = new Phone
        {
            Number = "212 222-2222",
            PhoneType = "White House Red Phone",
            // set tracking state to 'Add' to generate a SQL Insert statement
            TrackingState = TrackingState.Add,
        };
        _bushMobilePhone = new Phone
        {
            Number = "212 333-3333",
            PhoneType = "Bush Mobile Phone",
            // set tracking state to 'Add' to generate a SQL Insert statement
            TrackingState = TrackingState.Add,
        };
        _bush.Phones.Add(_whiteHousePhone);
        _bush.Phones.Add(_bushMobilePhone);
    }

    private async Task AddCustomerAsync()
    {
        // construct call to invoke UpdateCustomer action method in Web API service
        _response = await _client.PostAsync("api/customer/updatecustomer/", _bush, new JsonMediaTypeFormatter());
        if (_response.IsSuccessStatusCode)
        {
            // capture newly created customer entity from service, which will include
            // database-generated Ids for all entities
            _bush = await _response.Content.ReadAsAsync<Customer>();
            _whiteHousePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _bush.CustomerId);
            _bushMobilePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _bush.CustomerId);
            Console.WriteLine("Successfully created Customer {0} and {1} Phone Numbers(s)",
            _bush.Name, _bush.Phones.Count);
            foreach (var phoneType in _bush.Phones)
            {
                Console.WriteLine("Added Phone Type: {0}", phoneType.PhoneType);
            }
        }
        else
            Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase);
    }

    private void CreateSecondCustomer()
    {
        // create customer #2 and phone numbers
        _obama = new Customer
        {
            Name = "Barack Obama",
            Company = "President",
            // set tracking state to 'Add' to generate a SQL Insert statement
            TrackingState = TrackingState.Add,
        };
        _obamaMobilePhone = new Phone
        {
            Number = "212 444-4444",
            PhoneType = "Obama Mobile Phone",
            // set tracking state to 'Add' to generate a SQL Insert statement
            TrackingState = TrackingState.Add,
        };
        // set tracking state to 'Modifed' to generate a SQL Update statement
        _whiteHousePhone.TrackingState = TrackingState.Update;
        _obama.Phones.Add(_obamaMobilePhone);
        _obama.Phones.Add(_whiteHousePhone);
    }

    private async Task AddSecondCustomerAsync()
    {
        // construct call to invoke UpdateCustomer action method in Web API service
        _response = await _client.PostAsync("api/customer/updatecustomer/", _obama, new JsonMediaTypeFormatter());
        if (_response.IsSuccessStatusCode)
        {
            // capture newly created customer entity from service, which will include
            // database-generated Ids for all entities
            _obama = await _response.Content.ReadAsAsync<Customer>();
            _whiteHousePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _obama.CustomerId);
            _bushMobilePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _obama.CustomerId);
            Console.WriteLine("Successfully created Customer {0} and {1} Phone Numbers(s)",
            _obama.Name, _obama.Phones.Count);
            foreach (var phoneType in _obama.Phones)
            {
                Console.WriteLine("Added Phone Type: {0}", phoneType.PhoneType);
            }
        }
        else
            Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase);
    }

    private async Task RemoveFirstCustomerAsync()
    {
        // remove George Bush from underlying data store.
        // first, fetch George Bush entity, demonstrating a call to the
        // get action method on the service while passing a parameter
        var query = "api/customer/" + _bush.CustomerId;
        _response = _client.GetAsync(query).Result;

        if (_response.IsSuccessStatusCode)
        {
            _bush = await _response.Content.ReadAsAsync<Customer>();
            // set tracking state to 'Remove' to generate a SQL Delete statement
            _bush.TrackingState = TrackingState.Remove;
            // must also remove bush's mobile number -- must delete child before removing parent
            foreach (var phoneType in _bush.Phones)
            {
                // set tracking state to 'Remove' to generate a SQL Delete statement
                phoneType.TrackingState = TrackingState.Remove;
            }
            // construct call to remove Bush from underlying database table
            _response = await _client.PostAsync("api/customer/updatecustomer/", _bush, new JsonMediaTypeFormatter());
            if (_response.IsSuccessStatusCode)
            {
                Console.WriteLine("Removed {0} from database", _bush.Name);
                foreach (var phoneType in _bush.Phones)
                {
                    Console.WriteLine("Remove {0} from data store", phoneType.PhoneType);
                }
            }
            else
                Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase);
        }
        else
        {
            Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase);
        }
    }

    private async Task FetchCustomersAsync()
    {
        // finally, return remaining customers from underlying data store
        _response = await _client.GetAsync("api/customer/");
        if (_response.IsSuccessStatusCode)
        {
            var customers = await _response.Content.ReadAsAsync<IEnumerable<Customer>>();
            foreach (var customer in customers)
            {
                Console.WriteLine("Customer {0} has {1} Phone Numbers(s)",
                customer.Name, customer.Phones.Count());
                foreach (var phoneType in customer.Phones)
                {
                    Console.WriteLine("Phone Type: {0}", phoneType.PhoneType);
                }
            }
        }
        else
        {
            Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase);
        }
    }
}

  • در آخر کلاس‌های Customer, Phone و BaseEntity را به پروژه کلاینت اضافه کنید. چنین کدهایی بهتر است در لایه مجزایی قرار گیرند و بین لایه‌های مختلف اپلیکیشن به اشتراک گذاشته شوند.

اگر اپلیکیشن کلاینت را اجرا کنید با خروجی زیر مواجه خواهید شد.








شرح مثال جاری

با اجرای اپلیکیشن Web API شروع کنید. این اپلیکیشن یک MVC Web Controller دارد که پس از اجرا شما را به صفحه خانه هدایت می‌کند. در این مرحله سایت در حال اجرا است و سرویس‌ها قابل دسترسی هستند.

سپس اپلیکیشن کنسول را باز کنید و روی خط اول کد فایل program.cs یک breakpoint قرار داده و آن را اجرا کنید. ابتدا آدرس سرویس را نگاشت می‌کنیم و از سرویس درخواست می‌کنیم که اطلاعات را با فرمت JSON بازگرداند.

سپس توسط متد DeleteAsync که روی آبجکت HttpClient تعریف شده است اکشن متد Cleanup را روی سرویس فراخوانی می‌کنیم. این فراخوانی تمام داده‌های پیشین را حذف می‌کند.

در قدم بعدی یک مشتری بهمراه دو شماره تماس می‌سازیم. توجه کنید که برای هر موجودیت مشخصا خاصیت TrackingState را مقدار دهی می‌کنیم تا کامپوننت‌های Change-tracking در EF عملیات لازم SQL برای هر موجودیت را تولید کنند.

سپس توسط متد PostAsync که روی آبجکت HttpClient تعریف شده اکشن متد UpdateCustomer را روی سرویس فراخوانی می‌کنیم. اگر به این اکشن متد یک breakpoint اضافه کنید خواهید دید که موجودیت مشتری را بعنوان یک پارامتر دریافت می‌کند و آن را به context جاری اضافه می‌نماید. با اضافه کردن موجودیت به کانتکست جاری کل object graph اضافه می‌شود و EF شروع به ردیابی تغییرات آن می‌کند. دقت کنید که آبجکت موجودیت باید Add شود و نه Attach.

قدم بعدی جالب است، هنگامی که از خاصیت DbChangeTracker استفاده می‌کنیم. این خاصیت روی آبجکت context تعریف شده و یک <IEnumerable<DbEntityEntry را با نام Entries ارائه می‌کند. در اینجا بسادگی نوع پایه EntityType را تنظیم میکنیم. این کار به ما اجازه می‌دهد که در تمام موجودیت هایی که از نوع BaseEntity هستند پیمایش کنیم. اگر بیاد داشته باشید این کلاس، کلاس پایه تمام موجودیت‌ها است. در هر مرحله از پیمایش (iteration) با استفاده از کلاس EntityStateFactory مقدار خاصیت TrackingState را به مقدار متناظر در سیستم ردیابی EF تبدیل می‌کنیم. اگر کلاینت مقدار این فیلد را به Modified تنظیم کرده باشد پردازش بیشتری انجام می‌شود. ابتدا وضعیت موجودیت را از Modified به Unchanged تغییر می‌دهیم. سپس مقادیر اصلی را با فراخوانی متد GetDatabaseValues روی آبجکت Entry از دیتابیس دریافت می‌کنیم. فراخوانی این متد مقادیر موجود در دیتابیس را برای موجودیت جاری دریافت می‌کند. سپس مقادیر بدست آمده را به کلکسیون OriginalValues اختصاص می‌دهیم. پشت پرده، کامپوننت‌های EF Change-tracking بصورت خودکار تفاوت‌های مقادیر اصلی و مقادیر ارسالی را تشخیص می‌دهند و فیلدهای مربوطه را با وضعیت Modified علامت گذاری می‌کنند. فراخوانی‌های بعدی متد SaveChanges تنها فیلدهایی که در سمت کلاینت تغییر کرده اند را بروز رسانی خواهد کرد و نه تمام خواص موجودیت را.

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

متد UpdateCustomer در سرویس ما مقادیر TrackingState را به مقادیر متناظر EF تبدیل می‌کند و آبجکت‌ها را به موتور change-tracking ارسال می‌کند که نهایتا منجر به تولید دستورات لازم SQL می‌شود.

نکته: در اپلیکیشن‌های واقعی بهتر است کد دسترسی داده‌ها و مدل‌های دامنه را به لایه مجزایی منتقل کنید. همچنین پیاده سازی فعلی change-tracking در سمت کلاینت می‌تواند توسعه داده شود تا با انواع جنریک کار کند. در این صورت از نوشتن مقادیر زیادی کد تکراری جلوگیری خواهید کرد و از یک پیاده سازی می‌توانید برای تمام موجودیت‌ها استفاده کنید.

نظرات مطالب
حذف تگ‌های زاید دریافتی از متون MS-Word
سلام. با تشکر از شما، برنامه مربوط به کدینگ هارور رو قبلا تست کردم، مشکل داشت. کدی شکه شما زحمت کشیدید هم غیر از مسئله ذکر شده درکامنت اول، خوب کار میکرد فقط مشکل اینست که اختصاصی به فرمت های ورد ندارد و همه استایل ها را پاک میکند که برای ما بعنوان یک بلاگ سرویس قابل استفاده نیست، چون کاربران پس از کپی پیست، استایل های خودشان را هم اضافه میکنند و این کد همه را پاک میکند. اگر برنامه بود که فقط بتواند اضافات ورد را پاک کند خوب بود. و اینکه بصورت جاوا اسکریپت باشد تا بتوان درکلاینت هم از آن استفاده کرد(من متاسفانه رگولار اکسپرشن را عمیق کار نکرده ام، همین رگولاررا میتوان در جاوا هم بکار برد؟).