مطالب
کار با بانک‌های اطلاعاتی فاکس‌پرو و OleDB در دات نت 7
فرض کنید قصد خواندن اطلاعات یک بانک اطلاعاتی قدیمی فاکس‌پرو را با آخرین نگارش دات نت دارید. اگر سعی کنید از روش‌های و مطالب موجود استفاده کنید، هیچکدام جواب نخواهند داد! در این مطلب تغییرات صورت گرفته را بررسی می‌کنیم.


نیاز به درایور OleDB مخصوص بانک‌های اطلاعاتی قدیمی

برای کار با بانک‌های اطلاعاتی قدیمی از طریق ADO.NET، نیاز است بتوان به نحوی با آن‌ها ارتباط برقرار کرد و اینکار از طریق استاندارد OleDB که صرفا مختص به ویندوز است، قابل انجام است. برای مثال برای کار با فاکس‌پرو نیز در ابتدا باید درایور OleDB آن‌را نصب کرد که ... هیچکدام از لینک‌های قدیمی مایکروسافت در این زمینه دیگر وجود خارجی ندارند! آخرین نگارش مرتبط را می‌توانید در این آدرس و ذیل نام VFPOLEDBSetup.msi دریافت کنید (نگارش 9 را نصب می‌کند).


نیاز به دریافت بسته‌ی System.Data.OleDb

در اولین قدم جهت کار با درایور OleDB نصب شده، باید یک اتصال را توسط نمونه سازی شیء OleDbConnection ایجاد کرد که ... این شیء هم شناسایی نمی‌شود. به همین جهت باید بسته‌ی نیوگت مرتبط با آن‌را به صورت جداگانه‌ای دریافت و نصب کرد:
<ItemGroup>
   <PackageReference Include="System.Data.OleDb" Version="7.0.0"/>
</ItemGroup>


برنامه‌ی مبتنی بر درایور OleDB فاکس‌پرو اجرا نمی‌شود!

اولین سعی در برقراری ارتباط با درایور OleDB نصب شده، با خطای زیر خاتمه می‌یابد:
The 'VFPOLEDB' provider is not registered on the local machine.
مشکل اینجا است که درایور ارائه شده، 32 بیتی است و ما سعی داریم آن‌را در یک محیط 64 بیتی اجرا کنیم. به همین جهت خطای فوق ظاهر می‌شود. برای رفع آن باید PlatformTarget را به x86 تنظیم کرد:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
   <PlatformTarget>x86</PlatformTarget>
</PropertyGroup>
البته این تنظیم هم اگر پیشتر تنها runtime و یا SDK پیش‌فرض 64 بیتی را نصب کرده‌اید، سبب اجرای برنامه نخواهد شد. برای فعالسازی آن باید SDK x86 را هم جداگانه دریافت و نصب کنید.



روش یافتن نام پروایدر OleDB نصب شده

رشته‌های اتصالی OleDB، با =Provider، شروع می‌شوند. اکنون این سؤال مطرح می‌شود که پس از نصب VFPOLEDBSetup.ms یاد شده، دقیقا چه پروایدری به سیستم اضافه شده‌است و نام آن چیست؟
برای اینکار می‌توان از چندسطر زیر برای یافتن نام تمام پروایدرهای OleDB نصب شده‌ی بر روی سیستم استفاده کرد:
var oleDbEnumerator = new OleDbEnumerator();
using var elements = oleDbEnumerator.GetElements();
foreach (DataRow row in elements.Rows)
{
    Console.WriteLine($"{row["SOURCES_DESCRIPTION"]}: {row["SOURCES_NAME"]}");
}
که برای مثال یک خروجی آن می‌تواند به صورت زیر باشد:
Microsoft OLE DB Provider for SQL Server: SQLOLEDB
MSDataShape: MSDataShape
SQL Server Native Client 11.0: SQLNCLI11
Microsoft OLE DB Provider for Visual FoxPro: VFPOLEDB
OLE DB Provider for Microsoft Directory Services: ADsDSOObject
Microsoft OLE DB Driver for SQL Server: MSOLEDBSQL
Microsoft OLE DB Driver for SQL Server Enumerator: MSOLEDBSQL Enumerator
SQL Server Native Client 11.0 Enumerator: SQLNCLI11 Enumerator
Microsoft OLE DB Provider for Search: Windows Search Data Source
Microsoft OLE DB Provider for ODBC Drivers: MSDASQL
Microsoft OLE DB Enumerator for ODBC Drivers: MSDASQL Enumerator
Microsoft OLE DB Provider for Analysis Services 14.0: MSOLAP
Microsoft OLE DB Provider for Analysis Services 14.0: MSOLAP
Microsoft Jet 4.0 OLE DB Provider: Microsoft.Jet.OLEDB.4.0
Microsoft OLE DB Enumerator for SQL Server: SQLOLEDB Enumerator
Microsoft OLE DB Simple Provider: MSDAOSP
Microsoft OLE DB Provider for Oracle: MSDAORA
که در اینجا ما به دنبال یک سطر زیر هستیم:
Microsoft OLE DB Provider for Visual FoxPro: VFPOLEDB
یعنی نام دقیق پروایدر مرتبط با فاکس‌پرو، VFPOLEDB است.


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

پس از این مقدمات و تنظیمات، اکنون می‌توانیم از قطعه کد متداول ADO.NET زیر، جهت خواندن اطلاعات یک بانک اطلاعاتی فاکس‌پرو، استفاده کنیم:
var connectionString = "Provider=VFPOLEDB;Data Source=" +
       @"C:\path\Db.DBF;Password=;Collating Sequence=MACHINE";
using var dbConnection = new OleDbConnection(connectionString);
using var dataAdapter = new OleDbDataAdapter("select family from Db.DBF", dbConnection);
using var dataset = new DataSet();
dataAdapter.Fill(dataset, "table1");

var sb = new StringBuilder();
foreach (DataRow dataRow in dataset.Tables[0].Rows)
{
    var iranSystem = dataRow[0] as string;
    var unicode = iranSystem.FromIranSystemToUnicode();
    if (!string.IsNullOrWhiteSpace(unicode))
    {
       sb.AppendLine($"[DataRow(\"{iranSystem}\", \"{unicode}\")]");
    }
}
توضیحات:
- نام پروایدر در رشته اتصالی، به VFPOLEDB تنظیم شده‌است.
- select انجام شده بر روی نام فایل dbf انجام می‌شود. یعنی هر فایل dbf، یک جدول را تشکیل می‌دهد.
- اگر نام فیلدهای موجود را نمی‌دانید، بجای select family از * select استفاده کنید و سپس بر روی DataSet پر شده، یک break-point را قرار دهید تا بتوانید نام تمام ستون‌ها را از آن استخراج کنید.
- رشته‌ای را که توسط درایور فاکس‌پرو دریافت می‌کنید، یک رشته‌ی اسکی سیستم عامل داس است که در دات نت، با encoding مساوی 1252 شناخته می‌شود. یعنی encoding این رشته‌ی دریافتی، یونیکد پیش‌فرض دات‌نت نیست و باید توسط متد Encoding.GetEncoding(1252).GetBytes پردازش شود. که در نگارش‌های جدید دات نت، این کد‌پیج‌ها به صورت پیش‌فرض مهیا نیستند و باید در ابتدا ثبت شوند تا قابل استفاده شوند:
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance)
- متد FromIranSystemToUnicode استفاده شده، جزئی از «DNTPersianUtils.Core» است که در صورت نیاز به تبدیل اطلاعات قدیمی ایران سیستمی به یونیکد، می‌تواند مورد استفاده قرار گیرد.
نظرات مطالب
EF Code First #1
- حداقل دو علت می‌تونه داشته باشه:
الف) از پروژه‌ای استفاده می‌کنید که از چند ماژول تشکیل شده. اولی به EF نگارش A ارجاع دارد دومی به EF نگارش B. همه این‌ها رو باید یک دست کنید.
ب) EF رو به روز کردید اما تعریف آن‌را در فایل کانفیگ به روز نکردید. برای مثال این تعریف قدیمی در فایل کانفیگ شما هست
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=4.3.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
که در آن به EF 4.3 اشاره شده. بعد پروژه رو به EF 5 آپدیت کردید اما این مورد به روز نشده که باید به صورت زیر تغییر کند:
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
ج) تعریف ConnectionStrings در فایل کانفیگ باید بعد از ConfigSections باشد و نه قبل از آن.

- ضمنا تمام مثال‌های این سری از اینجا قابل دریافت است.
مطالب دوره‌ها
ابزاری برای تولید کدهای Reflection.Emit
برنامه معروف Reflector دارای افزونه‌ای است به نام Reflector.ReflectionEmitLanguage که سورس آن از آدرس ذیل قابل دریافت است:


مشخصات آن‌را نیز در آدرس زیر می‌توانید مشاهده نمائید:

به این ترتیب به منوی انتخاب زبان‌های Reflector، یک زبان جدید به نام ReflectionEmit اضافه خواهد شد:




مشکل!
این افزونه مدت زیادی است که به روز نشده و با آخرین نگارش Reflector سازگار نیست. برای رفع این مشکل ابتدا سورس آن‌را از کدپلکس دریافت و سپس تغییرات ذیل را به آن اعمال کنید:
الف) به قسمت ارجاعات پروژه افزونه مراجعه و ارجاع به Reflector قدیمی آن‌را حذف و آدرس فایل exe برنامه Reflector جدید را به عنوان ارجاعی تازه، ثبت کنید.
ب) در فایل Visitor.cs آن باید تغییر کوچکی در متد ذیل به نحوی که مشاهده می‌کنید صورت گیرد:
public virtual void VisitOrderClause(IOrderClause value)
{
    this.VisitOrderClause(value);
}
پس از آن، پروژه را کامپایل کرده و فایل dll حاصل را در پوشه Addins نگارش جدید Reflector کپی کنید. سپس به منوی Tools و گزینه Addins در برنامه مراجعه کرده و آدرس فایل Reflector.ReflectionEmitLanguage.dll را برای معرفی به برنامه مشخص نمائید.
به این ترتیب نگارش قدیمی افزونه Reflector.ReflectionEmitLanguage.dll با نگارش جدید برنامه Reflector سازگار خواهد شد.
سورس تغییر یافته این افزونه را از اینجا نیز می‌توانید دریافت کنید:
بدیهی است به ازای هر نگارش جدید Reflector، یکبار باید قسمت الف توضیحات فوق تکرار شود.
مطالب
روش صحیح کار با HttpClient در ASP.NET Core 2x
پیشتر مطلب «روش استفاده‌ی صحیح از HttpClient در برنامه‌های دات نت» را مطالعه کرده بودید. پس از ارائه‌ی NET Core 2.1.، این مجموعه به همراه یک IHttpClientFactory نیز ارائه می‌شود که در اینجا قصد داریم این مورد و همچنین سایر موارد مشابه را بررسی کنیم.


صورت مساله

قصد داریم اطلاعاتی را با فرمت JSON، از یک API خارجی، توسط HttpClient دریافت و سپس آن‌را به یک DTO فرضی، به نام GitHubRepositoryDto نگاشت کنیم.


راه حل 1

در این روش از وهله سازی مستقیم HttpClient به همراه استفاده‌ی از یک عبارت using کمک گرفته شده‌است. همچنین چون عملیات async است، نتیجه‌ی آن‌را به کمک خاصیت Result دریافت کرده‌ایم که پس از آن، کل اطلاعات دریافتی را به صورت یک رشته، در اختیار خواهیم داشت:
public class GitHubClient
{
    public IReadOnlyCollection<GitHubRepositoryDto> GetRepositories()
    {
        using (var httpClient = new HttpClient{BaseAddress = new Uri(GitHubConstants.ApiBaseUrl)})
        {
            var result = httpClient.GetStringAsync(GitHubConstants.RepositoriesPath).Result;
            return JsonConvert.DeserializeObject<List<GitHubRepositoryDto>>(result);
        }
    }
}
مشکلات این راه حل:
- استفاده از خاصیت Result، هیچگاه ایده‌ی خوبی نبوده است و یک عملیات async را تبدیل به عملیاتی Blocking می‌کند که حتی می‌تواند سبب بروز dead-lock نیز شود.
- HttpClient نباید Dispose شود. علت آن‌را در مطلب «روش استفاده‌ی صحیح از HttpClient در برنامه‌های دات نت» مفصل بررسی کرده‌ایم.
- دریافت کل response یک API به صورت یک رشته‌ی بزرگ، یک Large object heap را به‌وجود می‌آورد که باز هم ایده‌ی خوبی نیست.


راه حل 2

اگر خاصیت Result راه حل 1 را حذف کنیم، به راه حل 2 خواهیم رسید:
public class GitHubClient : IGitHubClient
{
    public async Task<IReadOnlyCollection<GitHubRepositoryDto>> GetRepositories()
    {
        using (var httpClient = new HttpClient { BaseAddress = new Uri(GitHubConstants.ApiBaseUrl) })
        {
            var result = await httpClient.GetStringAsync(GitHubConstants.RepositoriesPath);
            return JsonConvert.DeserializeObject<List<GitHubRepositoryDto>>(result);
        }
    }
}
مزایا:
- اینبار از دسترسی asynchronous واقعی استفاده شده‌است.

معایب:
- ایجاد و تخریب یک HttpClient جدید به ازای هر فراخوانی.
- دریافت و ذخیره سازی کل response به صورت یک رشته.


راه حل 3

در این نگارش، HttpClient از طریق وهله سازی در سازنده‌ی کلاس دریافت شده و به این ترتیب امکان استفاده‌ی مجدد را پیدا می‌کند:
public class GitHubClient : IGitHubClient
{
    private readonly HttpClient _httpClient;
    public GitHubClient()
    {
        _httpClient = new HttpClient { BaseAddress = new Uri(GitHubConstants.ApiBaseUrl) };
    }
    public async Task<IReadOnlyCollection<GitHubRepositoryDto>> GetRepositories()
    {
        var result = await _httpClient.GetStringAsync(GitHubConstants.RepositoriesPath).ConfigureAwait(false);
        return JsonConvert.DeserializeObject<List<GitHubRepositoryDto>>(result);
    }
}
طول عمر GitHubClient نیز Singleton معرفی می‌شود.
services.AddSingleton<GitHubClient>();
مزایا:
- دسترسی asynchronous واقعی به API مدنظر.
- استفاده‌ی مجدد از HttpClient

معایب:
- دریافت و ذخیره سازی کل response به صورت یک رشته.
- چون طول عمر GitHubClient از نوع Singleton است و برای همیشه از یک وهله‌ی سراسری استفاده می‌کند، از تغییرات DNS آگاه نخواهد شد.


راه حل 4

تا اینجا همانطور که ملاحظه کردید، به سادگی می‌توان HttpClient را به نحو نادرستی مورد استفاده قرار داد. ایجاد مجدد آن به علت عدم رها شدن بلافاصله‌ی سوکت‌های لایه‌ی زرین آن توسط سیستم عامل، مشکل حادی را به نام sockets exhaustion پدید می‌آورد. به همین جهت، این کلاس باید یکبار نمونه سازی شده و در طول عمر برنامه از همین تک وهله‌ی آن استفاده شود. یک روش اینکار تعریف آن به صورت اشیاء singleton و یا static است. مشکلی که این روش به همراه دارد، عدم باخبر شدن آن از تغییرات DNS است. برای رفع این مسایل، از NET Core 2.1. به بعد، خود مایکروسافت با ارائه‌ی یک IHttpClientFactory، روش استانداری را برای مدیریت وهله‌های HttpClient ارائه کرده‌است:
public class GitHubClient : IGitHubClient
{
    private readonly IHttpClientFactory _httpClientFactory;
    public GitHubClient(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
    }
    public async Task<IReadOnlyCollection<GitHubRepositoryDto>> GetRepositories()
    {
        var httpClient = _httpClientFactory.CreateClient("GitHub");
        var result = await httpClient.GetStringAsync(GitHubConstants.RepositoriesPath).ConfigureAwait(false);
        return JsonConvert.DeserializeObject<List<GitHubRepositoryDto>>(result);
    }
}
با این روش ثبت
services.AddHttpClient("GitHub", x => { x.BaseAddress = new Uri(GitHubConstants.ApiBaseUrl); });
services.AddSingleton<GitHubClient>();
در این روش، IHttpClientFactory به سازنده‌ی کلاس تزریق می‌شوند و از آن برای دسترسی به یک HttpClient جدید، هربار که این متد فراخوانی خواهد شد، استفاده می‌کنیم. بله ... در این حالت نیز یک HttpClient هربار ایجاد خواهد شد؛ اما چون از IHttpClientFactory استفاده می‌کنیم، مشکلی به شمار نمی‌رود. از این جهت که مطابق مستندات آن، هر HttpClient‌ای که به این نحو تولید می‌شود، یک HttpMessageHandler را در پشت صحنه مورد استفاده قرار می‌دهد که عملیات pooling و استفاده‌ی مجدد از آن‌ها، صورت می‌گیرد. یعنی IHttpClientFactory از HttpClient خود، به نحو بهینه‌ای استفاده‌ی مجدد می‌کند و در این حالت سیستم با مشکل کمبود منابع مواجه نخواهد شد و همچنین سربار ایجاد HttpClient‌های جدید نیز به حداقل می‌رسند.

مزیت‌ها:
- استفاده‌ی از یک IHttpClientFactory توکار

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

روش ثبت services.AddHttpClient را که در اینجا ملاحظه می‌کنید، یک روش ثبت نامدار است و بر اساس نام رشته‌ای GitHub کار می‌کند. همین نام در متد GetRepositories به صورت httpClientFactory.CreateClient("GitHub") برای دسترسی به یک HttpClient جدید استفاده شده‌است.


راه حل 5

در اینجا از یک کلاینت نوع‌دار، بجای یک کلاینت نامدار، استفاده شده‌است:
public class GitHubClient : IGitHubClient
{
    private readonly HttpClient _httpClient;
    public GitHubClient(HttpClient httpClient)
    {
        _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
    }
    public async Task<IReadOnlyCollection<GitHubRepositoryDto>> GetRepositories()
    {
        var result = await _httpClient.GetStringAsync(GitHubConstants.RepositoriesPath).ConfigureAwait(false);
        return JsonConvert.DeserializeObject<List<GitHubRepositoryDto>>(result);
    }
}
با این روش ثبت:
services.AddHttpClient<GitHubClient>(x => { x.BaseAddress = new Uri(GitHubConstants.ApiBaseUrl); });
برای ثبت یک کلاینت نوع‌دار، از متد AddHttpClient به همراه ذکر نوع کلاس کلاینت، استفاده می‌شود.

مزایا:
- استفاده از IHttpClientFactory
- استفاده از یک کلاینت نوع‌دار، بجای یک نمونه‌ی نامدار

معایب:
- اینبار تمام استفاده کنندگان از IGitHubClient ما باید دارای طول عمر transient باشند (خصوصیت کلاینت‌های نوع‌دار است)؛ برخلاف راه حل‌های پیشین که می‌توانستند singleton تعریف شوند (یا امکان فراخوانی IGitHubClient از سرویس‌های singleton نیز وجود داشت).
- دریافت و ذخیره سازی کل response به صورت یک رشته.


راه حل 6

اگر در جائی نیاز به استفاده و تزریق یک کلاینت نوع‌دار، در یک سرویس با طول عمر singleton را داشتید، روش آن به صورت زیر است:
public class GitHubClientFactory
{
    private readonly IServiceProvider _serviceProvider;
    public GitHubClientFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public GitHubClient Create()
    {
        return _serviceProvider.GetRequiredService<GitHubClient>();
    }
}
با این روش ثبت:
services.AddHttpClient<GitHubClient>(x => { x.BaseAddress = new Uri(GitHubConstants.ApiBaseUrl); });
services.AddSingleton<GitHubClientFactory>();
در این روش، یک GitHubClientFactory را داریم که یک GitHubClient را باز می‌گرداند. نکته‌ی اصلی آن، کار با ServiceProvider، جهت دسترسی به GitHubClient است. مابقی آن یعنی تعریف GitHubClient، مانند روش 5 است.

مزایا:
- استفاده از IHttpClientFactory
- استفاده از یک کلاینت نوع‌دار
- استفاده کننده‌ی از GitHubClientFactory، می‌توانند طول عمر singleton نیز داشته باشد

معایب:
- دریافت و ذخیره سازی کل response به صورت یک رشته.


راه حل 7

از اینجا به بعد، هدف ما بهینه سازی عملیات است و رفع مشکل کار با یک رشته‌ی بزرگ. برای این منظور بجای متد GetStringAsync، از متد SendAsync که امکان streaming را فراهم می‌کند، استفاده خواهیم کرد. به این ترتیب، بجای ارسال یک رشته‌ی بزرگ به متد Deserialize، امکان دسترسی به استریم response را توسط آن میسر کرده‌ایم.
public class GitHubClient : IGitHubClient
{
    private readonly HttpClient _httpClient;
    private readonly JsonSerializer _jsonSerializer;
    public GitHubClient(HttpClient httpClient, JsonSerializer jsonSerializer)
    {
        _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
        _jsonSerializer = jsonSerializer ?? throw new ArgumentNullException(nameof(jsonSerializer));
    }

    public async Task<IReadOnlyCollection<GitHubRepositoryDto>> GetRepositories()
    {
        var request = CreateRequest();
        var result = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead);
        using (var responseStream = await result.Content.ReadAsStreamAsync())
        {
            using (var streamReader = new StreamReader(responseStream))
            using (var jsonTextReader = new JsonTextReader(streamReader))
            {
                return _jsonSerializer.Deserialize<List<GitHubRepositoryDto>>(jsonTextReader);
            }
        }
    }

    private static HttpRequestMessage CreateRequest()
    {
        return new HttpRequestMessage(HttpMethod.Get, GitHubConstants.RepositoriesPath);
    }
}
با این روش ثبت:
services.AddHttpClient<GitHubClient>(x => { x.BaseAddress = new Uri(GitHubConstants.ApiBaseUrl); });
services.AddSingleton<GitHubClientFactory>();
services.AddSingleton<JsonSerializer>();
مزایا:
- کار با IHttpClientFactory
- استفاده از یک کلاینت نوع‌دار
- کار با استریم response

معایب:
- استفاده از ResponseContentRead


راه حل 8

در این روش بجای سطر ذیل در راه حل 7
var result = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead);
از این سطر استفاده خواهیم کرد:
var result = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
در حالت ResponseContentRead که حالت پیش‌فرض نیز هست، تمام هدرها و کل محتوای بازگشتی از سمت سرور باید خوانده شوند تا در اختیار مصرف کننده قرار گیرند، اما در حالت ResponseHeadersRead، فقط برای دریافت هدرها صبر خواهد شد و مابقی آن سریعا به صورت یک استریم در اختیار مصرف کننده قرار می‌گیرد.


مزایا:
- کار با IHttpClientFactory
- استفاده از یک کلاینت نوع‌دار
- کار با استریم response
- استفاده از ResponseHeadersRead

معایب:
- شاید بتوان از کتابخانه‌ی دیگری برای json deserialization استفاده کرد؟
بازخوردهای دوره
معرفی پروژه NotifyPropertyWeaver
من امروز با این مطلب آشنا شدم و سعی در استفاده از آن داشتم ولی  بعد از مزین کردن کلاس به خصوصیت [ImpementPropertyChanged] با خطای زیر مواجه شدم :
ImplementPropertyChangedAttribute' is obsolete: 'This configuration option has been deprecated. The use of this attribute was to add INotifyPropertyChanged to a class with its associated event definition. After that all classes that implement INotifyPropertyChanged have their properties weaved, weather they have the ImplementPropertyChangedAttribute or not.
بعد از بررسی متوجه شدم که در بروز رسانی‌های جدید این بسته نیازی به قید این خصوصیت نیست.و پیاده سازی اینترفیس InotifyPropertyChanged برای استفاده از این بسته کفایت می‌کنه
مطالب
شروع به کار با EF Core 1.0 - قسمت 8 - بررسی رابطه‌ی Many-to-Many
در مطلب «بررسی تفصیلی رابطه Many-to-Many در EF Code first» نحوه‌ی مدلسازی رابطه‌ی چند به چند را در EF 6.x بررسی کردیم. یک چنین رابطه‌ای که به همراه مدیریت خودکار join table آن است (جدول BlogPostsJoinTags در شکل زیر)، در EF Core 1.0 RTM پشتیبانی نمی‌شود.


البته همیشه درخواست کنترل این جدول واسط که کاملا از دیدگاه ORMها (تمام آن‌ها) مخفی است، وجود داشته‌است و قرار است این پشتیبانی توسط مفهوم ویژه‌ای به نام shadow properties به نگارش‌های بعدی EF Core اضافه شود.
اما فعلا در نگارش اول آن، توصیه شده‌است که رابطه‌ی many-to-many را به صورت دو رابطه‌ی one-to-many مدلسازی کنید که در ادامه آن‌را بررسی خواهیم کرد. بنابراین پیشنیاز آن مطالعه‌ی مطلب «شروع به کار با EF Core 1.0 - قسمت 7 - بررسی رابطه‌ی One-to-Many» می‌باشد.


مدلسازی موجودیت‌های یک رابطه‌ی چند به چند در EF Core 1.0 RTM توسط Fluent API

در اینجا نحوه‌ی مدلسازی یک رابطه‌ی چند به چند را توسط دو رابطه‌ی one-to-many مشاهده می‌کنید. تنها تفاوت آن با EF 6.x، قید صریح جدول واسط BlogPostsJoinTags است که یک چنین جدولی در EF 6.x به صورت خودکار تشکیل شده و مدیریت می‌شود و کاملا از دید برنامه مخفی است. اما در اینجا (در نگارش اول EF Core) نیاز است این جدول واسط را از حالت مخفی خارج کرد و سپس دو رابطه‌ی یک به چند را به جداول مطالب و تگ‌های آن‌ها تشکیل داد.
مزیت این حالت، دسترسی کامل به طراحی جدول واسط، توسط برنامه است. بنابراین اگر به هر دلیلی نیاز به افزودن خواص بیشتری به این جدول ویژه دارید، اکنون امکان آن میسر است.
    public class Tags
    {
        public Tags()
        {
            BlogPostsJoinTags = new HashSet<BlogPostsJoinTags>();
        }

        public int Id { get; set; }
        public string Name { get; set; }

        public virtual ICollection<BlogPostsJoinTags> BlogPostsJoinTags { get; set; }
    }

    public class BlogPostsJoinTags
    {
        public virtual BlogPosts BlogPost { get; set; }
        public int BlogPostId { get; set; }

        public virtual Tags Tag { get; set; }
        public int TagId { get; set; }
    }

    public class BlogPosts
    {
        public BlogPosts()
        {
            BlogPostsJoinTags = new HashSet<BlogPostsJoinTags>();
        }

        public int Id { get; set; }
        public string Title { get; set; }
        public string Body { get; set; }

        public virtual ICollection<BlogPostsJoinTags> BlogPostsJoinTags { get; set; }
    }
پس از آن باید Context برنامه را نیز جهت ذکر صریح رابطه‌ی یک به چند، ویرایش کرد. با متدهای HasOne و WithMany در مطلب قبل «شروع به کار با EF Core 1.0 - قسمت 7 - بررسی رابطه‌ی One-to-Many» آشنا شدیم و علت نیاز به ذکر صریح آن‌ها، وجود بیش از یک سر رابطه در جدول واسط BlogPostsJoinTags است. در این حالت یافتن InversePropertyها توسط EF Core میسر نیست و حتما باید از یک طرف شروع و سمت دیگر را مشخص کرد.
به علاوه در اینجا تعریف یک composite key را هم بر روی خواص کلید خارجی جدول واسط مشاهده می‌کنید. وجود این کلید ترکیبی سبب خواهد شد که ملزم به ثبت هر دو Id (کلیدهای جداول مطلب و تگ) در حین ثبت در این جدول شویم (یا قید اجباری هر دو طرف رابطه).
    public class MyDBDataContext : DbContext
    {
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"Data Source=(local);Initial Catalog=testdb2;Integrated Security = true");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<BlogPosts>(entity =>
            {
                entity.Property(e => e.Title)
                    .IsRequired()
                    .HasMaxLength(450);
            });

            modelBuilder.Entity<Tags>(entity =>
            {
                entity.Property(e => e.Name)
                    .IsRequired()
                    .HasMaxLength(450);
            });

            modelBuilder.Entity<BlogPostsJoinTags>(entity =>
            {
                entity.HasKey(e => new { e.TagId, e.BlogPostId })
                    .HasName("PK_dbo.BlogPostsJoinTags");

                entity.HasIndex(e => e.BlogPostId)
                    .HasName("IX_BlogPostId");

                entity.HasIndex(e => e.TagId)
                    .HasName("IX_TagId");

                entity.HasOne(d => d.BlogPost)
                    .WithMany(p => p.BlogPostsJoinTags)
                    .HasForeignKey(d => d.BlogPostId)
                    .HasConstraintName("FK_dbo.BlogPostsJoinTags_dbo.BlogPosts_BlogPostId");

                entity.HasOne(d => d.Tag)
                    .WithMany(p => p.BlogPostsJoinTags)
                    .HasForeignKey(d => d.TagId)
                    .HasConstraintName("FK_dbo.BlogPostsJoinTags_dbo.Tags_TagId");
            });
        }

        public virtual DbSet<BlogPosts> BlogPosts { get; set; }
        public virtual DbSet<BlogPostsJoinTags> BlogPostsJoinTags { get; set; }
        public virtual DbSet<Tags> Tags { get; set; }
    }


مدلسازی موجودیت‌های یک رابطه‌ی چند به چند در EF Core 1.0 RTM توسط Data Annotations

در حالت مدلسازی توسط ویژگی‌ها، ذکر InversePropertyها و همچنین ForeignKeyها مقداری واضح‌تر به نظر می‌رسند. به علاوه، یک سری از تنظیمات هم معادل data annotations ایی ندارند؛ مانند composite key تعریف شده‌ی بر روی خواص جدول واسط و همچنین ایندکس‌های تعریف شده، که حتما باید توسط Fluent API تنظیم شوند.
    public class Tags
    {
        public Tags()
        {
            BlogPostsJoinTags = new HashSet<BlogPostsJoinTags>();
        }

        public int Id { get; set; }

        [Required]
        [MaxLength(450)]
        public string Name { get; set; }

        [InverseProperty("Tag")]
        public virtual ICollection<BlogPostsJoinTags> BlogPostsJoinTags { get; set; }
    }

    public class BlogPostsJoinTags
    {
        [ForeignKey("BlogPostId")]
        [InverseProperty("BlogPostsJoinTags")]
        public virtual BlogPosts BlogPost { get; set; }
        public int BlogPostId { get; set; }

        [ForeignKey("TagId")]
        [InverseProperty("BlogPostsJoinTags")]
        public virtual Tags Tag { get; set; }
        public int TagId { get; set; }
    }

    public class BlogPosts
    {
        public BlogPosts()
        {
            BlogPostsJoinTags = new HashSet<BlogPostsJoinTags>();
        }

        public int Id { get; set; }

        [Required]
        [MaxLength(450)]
        public string Title { get; set; }

        public string Body { get; set; }

        [InverseProperty("BlogPost")]
        public virtual ICollection<BlogPostsJoinTags> BlogPostsJoinTags { get; set; }
    }

    public class MyDBDataContext : DbContext
    {
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"Data Source=(local);Initial Catalog=testdb2;Integrated Security = true");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<BlogPostsJoinTags>(entity =>
            {
                entity.HasKey(e => new { e.TagId, e.BlogPostId })
                    .HasName("PK_dbo.BlogPostsJoinTags");

                entity.HasIndex(e => e.BlogPostId)
                    .HasName("IX_BlogPostId");

                entity.HasIndex(e => e.TagId)
                    .HasName("IX_TagId");
            });
        }

        public virtual DbSet<BlogPosts> BlogPosts { get; set; }
        public virtual DbSet<BlogPostsJoinTags> BlogPostsJoinTags { get; set; }
        public virtual DbSet<Tags> Tags { get; set; }
    }
همانطور که در مطلب «شروع به کار با EF Core 1.0 - قسمت 6 - تعیین نوع‌های داده و ویژگی‌های آن‌ها» نیز عنوان شد، علت تنظیم MaxLength به عدد 450 این است که بتوان بر روی این ستون ایندکس ایجاد کرد.


نحوه‌ی ثبت اطلاعات در دو رابطه‌ی یک به چند به همراه جدول واسط

در EF 6.x، کار مقدار دهی Idهای جدول واسط به صورت خودکار انجام می‌شود. در اینجا این مقدار دهی را باید به صورت صریح انجام داد:
 var post = new BlogPosts { ... };
context.BlogPosts.Add(post);

var tag = new Tags { ... };
context.Tags.Add(tag);

var postTag = new BlogPostsJoinTags { Tag = tag, BlogPost = post };
context.PostsTags.Add(postTag);

context.SaveChanges();


نحوه‌ی واکشی اطلاعات به هم مرتبط در دو رابطه‌ی یک به چند به همراه جدول واسط

در مورد متدهای Include و ThenInclude در مطلب «شروع به کار با EF Core 1.0 - قسمت 7 - بررسی رابطه‌ی One-to-Many» پیشتر بحث شد.
            BlogPosts post1 = this.BlogPosts
                  .Include(blogPosts => blogPosts.BlogPostsJoinTags)
                    .ThenInclude(joinTags => joinTags.Tag)
                  .First(blogPosts => blogPosts.Id == 1);
            IEnumerable<Tags> post1Tags = post1.BlogPostsJoinTags.Select(x => x.Tag);
در اینجا اگر می‌خواهیم به لیست تمام برچسب‌های اولین مطلب دسترسی پیدا کنیم، ابتدا باید رابطه‌ی جدول واسط را Include کنیم. سپس چون در همین سطح می‌خواهیم به سطح بعدی تگ‌های مرتبط برسیم، باید از متد الحاقی جدید ThenInclude استفاده کرد. در غیراینصورت در سطر بعدی، post1.BlogPostsJoinTags نال خواهد بود و همچنین حاوی لیست تگ‌ها نیز نخواهد بود.


یک نکته‌ی تکمیلی
وضعیت پشتیبانی از رابطه‌ی many-to-many را همانند EF 6.x در EF Core، در اینجا می‌توانید پیگیری کنید.
مطالب
VS Code برای توسعه دهندگان ASP.NET Core - قسمت دوم - ایجاد و اجرای اولین برنامه
پس از معرفی ابتدایی VSCode و نصب افزونه‌ی #C در قسمت قبل، در ادامه می‌خواهیم اولین پروژه‌ی ASP.NET Core خود را در آن ایجاد کنیم.


نصب ASP.NET Core بر روی سیستم عامل‌های مختلف

برای نصب پیشنیازهای کار با ASP.NET Core به آدرس https://www.microsoft.com/net/download/core مراجعه کرده و NET Core SDK. را دریافت و نصب کنید. پس از نصب آن جهت اطمینان از صحت انجام عملیات، دستور dotnet --version را در خط فرمان صادر کنید:
 C:\>dotnet --version
1.0.1
در اینجا SDK نصب شده، شامل هر دو نگارش 1.0 و 1,1 است. همچنین در همین صفحه‌ی دریافت فایل‌ها، علاوه بر نگارش ویندوز، نگارش‌های Mac و لینوکس آن نیز موجود هستند. بر روی هر کدام که کلیک کنید، ریز مراحل نصب هم به همراه آن‌ها وجود دارد. برای مثال نصب NET Core. بر روی Mac شامل نصب OpenSSL به صورت جداگانه است و یا نصب آن بر روی لینوکس به همراه چند دستور مختص به توزیع مورد استفاده می‌باشد که به خوبی مستند شده‌اند و نیازی به تکرار آن‌ها نیست و همواره آخرین نگارش آن‌ها بر روی سایت dot.net موجود است.


ایجاد اولین پروژه‌ی ASP.NET Core توسط NET Core SDK.

پس از نصب NET Core SDK.، به پیشنیاز دیگری برای شروع به کار با ASP.NET Core نیازی نیست. نه نیازی است تا آخرین نگارش ویژوال استودیوی کامل را نصب کنید و نه با به روز رسانی آن، نیاز است چندگیگابایت فایل به روز رسانی تکمیلی را دریافت کرد. همینقدر که این SDK نصب شود، می‌توان بلافاصله شروع به کار با این نگارش جدید کرد.
در ادامه ابتدا پوشه‌ی جدید پروژه‌ی خود را ساخته (برای مثال در مسیر D:\vs-code-examples\FirstAspNetCoreProject) و سپس از طریق خط فرمان به این پوشه وارد می‌شویم.

یک نکته: در ویندوزهای جدید فقط کافی است در نوار آدرس بالای صفحه تایپ کنید cmd. به این صورت بلافاصله command prompt استاندارد ویندوز در پوشه‌ی جاری در دسترس خواهد بود و دیگر نیازی نیست تا چند مرحله را جهت رسیدن به آن طی کرد.

پس از وارد شدن به پوشه‌ی جدید از طریق خط فرمان، دستور dotnet new --help را صادر کنید:
D:\vs-code-examples\FirstAspNetCoreProject>dotnet new --help
Getting ready...
Template Instantiation Commands for .NET Core CLI.

Usage: dotnet new [arguments] [options]

Arguments:
  template  The template to instantiate.

Options:
  -l|--list         List templates containing the specified name.
  -lang|--language  Specifies the language of the template to create
  -n|--name         The name for the output being created. If no name is specified, the name of the current directory is used.
  -o|--output       Location to place the generated output.
  -h|--help         Displays help for this command.
  -all|--show-all   Shows all templates


Templates                 Short Name      Language      Tags
----------------------------------------------------------------------
Console Application       console         [C#], F#      Common/Console
Class library             classlib        [C#], F#      Common/Library
Unit Test Project         mstest          [C#], F#      Test/MSTest
xUnit Test Project        xunit           [C#], F#      Test/xUnit
ASP.NET Core Empty        web             [C#]          Web/Empty
ASP.NET Core Web App      mvc             [C#], F#      Web/MVC
ASP.NET Core Web API      webapi          [C#]          Web/WebAPI
Solution File             sln                           Solution

Examples:
    dotnet new mvc --auth None --framework netcoreapp1.1
    dotnet new xunit --framework netcoreapp1.1
    dotnet new --help
همانطور که مشاهده می‌کنید، اینبار بجای انتخاب گزینه‌های مختلف از صفحه دیالوگ new project template داخل ویژوال استودیوی کامل، تمام این قالب‌ها از طریق خط فرمان در اختیار ما هستند. برای مثال می‌توان یک برنامه کنسول و یا یک کتابخانه‌ی جدید را ایجاد کرد.
در ادامه دستور ذیل را صادر کنید:
 D:\vs-code-examples\FirstAspNetCoreProject>dotnet new mvc --auth None
به این ترتیب یک پروژه‌ی جدید ASP.NET Core، بدون تنظیمات اعتبارسنجی و ASP.NET Core Identity، در کسری از ثانیه ایجاد خواهد شد.


سپس جهت گشودن این پروژه در VSCode تنها کافی است دستور ذیل را صادر کنیم:
 D:\vs-code-examples\FirstAspNetCoreProject>code .
در ادامه یکی از فایل‌های #C آن‌را گشوده و منتظر شوید تا کار دریافت خودکار بسته‌های مرتبط با افزونه‌ی #C ایی که در قسمت قبل نصب کردیم به پایان برسند:


اینکار یکبار باید انجام شود و پس از آن امکانات زبان #C و همچنین دیباگر NET Core. در VS Code قابل استفاده خواهند بود.
در تصویر فوق دو اخطار را هم مشاهده می‌کنید. مورد اول برای فعال سازی دیباگ و ساخت پروژه‌ی جاری است. گزینه‌ی Yes آن‌را انتخاب کنید و اخطار دوم جهت بازیابی بسته‌های نیوگت پروژه‌است که گزینه‌ی restore آن‌را انتخاب نمائید. البته کار بازیابی بسته‌ها از طریق کش موجود در سیستم به سرعت انجام خواهد شد.


گشودن کنسول از درون VS Code و اجرای برنامه‌ی ASP.NET Core

روش‌های متعددی برای گشودن کنسول خط فرمان در VS Code وجود دارند:
الف) فشردن دکمه‌های Ctrl+Shift+C
اینکار باعث می‌شود تا command prompt ویندوز از پوشه‌ی جاری به صورت مجزایی اجرا شود.
ب) فشردن دکمه‌های Ctrl+` (و یا Ctrl+ back-tick)
به این ترتیب کنسول پاورشل درون خود VS Code باز خواهد شد. اگر علاقمند نیستید تا از پاورشل استفاده کنید، می‌توانید این پیش‌فرض را به نحو ذیل بازنویسی کنید:
همانطور که در قسمت قبل نیز ذکر شد، از طریق منوی File->Preferences->Settings می‌توان به تنظیمات VS Code دسترسی یافت. پس از گشودن آن، یک سطر ذیل را به آن اضافه کنید:
 "terminal.integrated.shell.windows": "cmd.exe"
اکنون Ctrl+ back-tick را فشرده تا کنسول خط فرمان، داخل VS Code نمایان شود و سپس دستور ذیل را صادر کنید:
 D:\vs-code-examples\FirstAspNetCoreProject>dotnet run
Hosting environment: Production
Content root path: D:\vs-code-examples\FirstAspNetCoreProject
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.
در اینجا دستور dotnet run سبب کامپایل و همچنین اجرای پروژه شده‌است و اکنون این برنامه‌ی وب بر روی پورت 5000 قابل دسترسی است:



ساده سازی ساخت و اجرای یک برنامه‌ی ASP.NET Core در VS Code


زمانیکه گزینه‌ی افزودن امکانات ساخت و دیباگ را انتخاب می‌کنیم (تصویر فوق)، دو فایل جدید به پوشه‌ی vscode. اضافه می‌شوند:


دراینجا فایل tasks.json، حاوی وظیفه‌ای است جهت ساخت برنامه. یعنی بجای اینکه مدام بخواهیم به خط فرمان مراجعه کرده و دستوراتی را صادر کنیم، می‌توان از وظایفی که در پشت صحنه همین فرامین را اجرا می‌کنند، استفاده کنیم؛ که نمونه‌ای از آن، به صورت پیش فرض به پروژه اضافه شده است.
برای دسترسی به آن، دکمه‌های ctrl+shift+p را فشرده و سپس در منوی ظاهر شده، واژه‌ی build را جستجو کنید:


با انتخاب این گزینه (که توسط Ctrl+Shift+B هم در دسترس است)، کار ساخت برنامه انجام شده و dll مرتبط با آن در پوشه‌ی bin تشکیل می‌شود.
 
همچنین در اینجا برای ساخت و بلافاصله نمایش آن در مرورگر پیش فرض سیستم، می‌توان مجددا دکمه‌های ctrl+shift+p را فشرد و در منوی ظاهر شده، واژه‌ی without را جستجو کرد:


با انتخاب این گزینه (که توسط Ctrl+F5 نیز در دسترس است)، برنامه ساخته شده، اجرا و نمایش داده می‌شود و برای خاتمه‌ی آن می‌توانید دکمه‌های Ctrl+C را بفشارید تا کار وب سرور موقتی آن خاتمه یابد.


در قسمت بعد مباحث دیباگ برنامه و گردش کار متداول یک پروژه‌ی ASP.NET Core را بررسی خواهیم کرد.
مطالب
سه مطلب کوتاه

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

Bad Request - Request Too Long
HTTP Error 400. The size of the request headers is too long.

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


IE8 و ارائه‌ی fakepath بجای آدرس فایل
کنترل استاندارد آپلود فایل در مرورگرهای جدید، دیگر آدرس محلی فایل را حتی در اختیار اسکریپت‌های سمت کاربر نیز قرار نمی‌دهند. فایرفاکس مدت زیادی است که این مورد را پیاده سازی کرده. اما IE بجای اینکه یک رشته‌ی خالی را بازگشت دهد مسیر c:\fakepath را ارائه خواهد داد (fakepath جزو استاندارد html 5 است). اگر احیانا با این مورد برخورد داشتید، با استفاده از تنظیم زیر می‌توان مانند سابق مسیر کامل را نیز دریافت کرد:

Internet Explorer -> Tools -> Internet Option -> Security -> Custom ->
find the "Include local directory path when uploading files to a server"
-> click on "Enable"

اهمیت این مورد هم برای من این است که IE، یعنی همان مرورگر کاری در اکثر شرکت‌ها (و این مورد فوق را به سادگی از طریق گروپ پالیسی می‌توان به تمام کامپیوترها اعمال کرد).

علت تایم آوت و باز نشدن یک سری از سایت‌ها در ایران
مدت زیادی این سؤال برای من وجود داشت که وجه مشترک سایت‌هایی مانند dotnetkicks.com ، summerofnhibernate.com ، lessthandot.com و امثال آن چیست که از این طرف باز نمی‌شوند؟
اگر به آدرس‌های فوق که به سایت domaintools.com ختم می‌شوند، مراجعه کنید و سپس برگه‌ی Server Stats آن‌ها را ملاحظه نمائید، همگی توسط Godaddy.com Inc هاست می‌شوند. این شرکت غیرمحترم، IP های ایرانی را بسته است (مطلب جدیدی هم نیست).

مطالب
بهبود SEO در ASP.NET MVC
گوگل خلاصه نتایج Indexing یک سایت را توسط ابزاری به نام Google webmaster tools در اختیار علاقمندان قرار می‌دهد. Bing نیز چنین ابزاری را تدارک دیده است.
به آمارهای خطای حاصل از سایت جاری که دقت می‌کردم یک نکته آن جالب بود: «محتوای تکراری»


mydomain.com/Home/Index
mydomain.com/home/index
mydomain.com/Home/index
mydomain.com/home/Index
همانطور که ملاحظه می‌کنید، گوگل به کوچکی و بزرگی حروف بکار رفته در لینک‌ها حساس است. هرچند 4 لینک فوق به یک صفحه اشاره می‌کنند، اما گوگل 4 بار آن‌ها را ایندکس خواهد کرد و نهایتا به صورت یک خطای «محتوای تکراری» در گزارشات SEO آن ظاهر خواهد شد (به همراه کاهش رتبه SEO سایت).

راه حل

برای حل این مساله دو نکته باید درنظر گرفته شود:
الف) هدایت دائمی (Redirect permanent) صفحات قدیمی به صفحاتی جدید، با آدرس lowercase

using System.Globalization;
using System.Web;
using System.Web.Mvc;

namespace WebToolkit
{
    public class ForceWww : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            modifyUrlAndRedirectPermanent(filterContext);
            base.OnActionExecuting(filterContext);
        }

        private static void modifyUrlAndRedirectPermanent(ActionExecutingContext filterContext)
        {
            if (canIgnoreRequest(filterContext))
                return;

            var absoluteUrl = HttpUtility.UrlDecode(filterContext.RequestContext.HttpContext.Request.Url.AbsoluteUri.ToString(CultureInfo.InvariantCulture));
            var absoluteUrlToLower = absoluteUrl.ToLowerInvariant();

            absoluteUrlToLower = forceWwwAndLowercase(filterContext, absoluteUrlToLower);
            absoluteUrlToLower = avoidTrailingSlashes(filterContext, absoluteUrlToLower);

            if (!absoluteUrl.Equals(absoluteUrlToLower))
            {
                filterContext.Result = new RedirectResult(absoluteUrlToLower, permanent: true);
            }
        }

        private static string avoidTrailingSlashes(ActionExecutingContext filterContext, string absoluteUrlToLower)
        {
            if (!isRootRequest(filterContext) && absoluteUrlToLower.EndsWith("/"))
                return absoluteUrlToLower.TrimEnd(new[] { '/' });

            return absoluteUrlToLower;
        }

        private static bool isRootRequest(ActionExecutingContext filterContext)
        {
            return filterContext.RequestContext.HttpContext.Request.Url.AbsolutePath == "/";
        }

        private static bool canIgnoreRequest(ActionExecutingContext filterContext)
        {
            return filterContext.IsChildAction || 
                   filterContext.HttpContext.Request.IsAjaxRequest() ||
                   filterContext.RequestContext.HttpContext.Request.Url.AbsoluteUri.Contains("?");
        }

        private static string forceWwwAndLowercase(ActionExecutingContext filterContext, string absoluteUrlToLower)
        {
            if (isLocalRequet(filterContext))
                return absoluteUrlToLower;

            if (absoluteUrlToLower.Contains("www"))
                return absoluteUrlToLower;

            return absoluteUrlToLower.Replace("http://", "http://www.")
                                     .Replace("https://", "https://www.");
        }

        private static bool isLocalRequet(ActionExecutingContext filterContext)
        {
            return filterContext.RequestContext.HttpContext.Request.IsLocal;
        }
    }
}
کلاس فوق، نگارش تکمیل شده ForceWww که پیشتر در این سایت دیده‌اید. توسط آن سه بررسی مختلف بر روی لینک جاری در حال پردازش صورت خواهد گرفت:
- تمام آدرس‌های سایت باید www داشته باشند؛ تا آدرس‌های آن یکنواخت شده و خصوصا مشکلات لاگین و نوشته شدن کوکی‌ها به ازای آدرس‌های مختلف و سر درگمی کاربران کاهش یابد.
- اگر آدرس جاری lowercase نباشد، تبدیل به نمونه lowercase شده و درخواست کننده، به آدرس جدید هدایت می‌شود. این مورد خصوصا جهت موتورهای جستجو برای تصحیح نتایج آن‌ها بسیار مفید است.
- اسلش انتهای لینک‌ها در صورت وجود حذف خواهد شد. این مورد نیز در کاهش تعداد خطاهای «محتوای تکراری» مؤثر است.
- اگر آدرسی، کوئری استرینگ داشته باشد از آن صرفنظر خواهد شد؛ زیرا ممکن است اطلاعات موجود در آن به کوچکی و بزرگی حروف حساس باشند.


ب) کاهش بار سایت توسط تولید خودکار Urlهایی که در بدو امر lowercase هستند

برای پیاده سازی این مطلب می‌توان از پروژه سورس باز «LowercaseRoutesMVC» استفاده کرد. سه فایل cs دارد که می‌توانید به پروژه خود اضافه کنید. پس از آن، هرجایی در پروژه خود routes.MapRoute دارید تبدیل کنید به routes.MapRouteLowercase .
به این ترتیب به صورت خودکار تمام Urlهای تولید شده توسط HTML helpers توکار ASP.NET MVC (و نه Urlهایی که دستی نوشته شده‌اند)، در حین درج در صفحه به صورت lowercase ظاهر خواهند شد (صرفنظر از اینکه نام‌های کنترلرها و یا اکشن متدهای تعریف شده camel case هستند یا خیر). مزیت این مساله کاهش یک مرحله Redirect است که در قسمت الف ذکر شد. در این کتابخانه کمکی نیز از آدرس‌هایی که دارای کوئری استرینگ باشند، صرفنظر می‌شود.