مطالب
نکات ویژه کار با عملیات نامتقارن در Blazor Server
 در برنامه‌های Blazor Server، تنها از یک نخ رابط کاربری واحد ( single UI thread ) استفاده نمی‌شود؛ بلکه هر نخی که در دسترس باشد، می‌تواند در موقع رندر، استفاده شود. علاوه بر این اگر از عملیات نامتقارن استفاده شود، زمانیکه به کلمه‌ی کلیدی await می‌رسیم، آنگاه نخ اختصاص داده شده‌ی برای ادامه پردازش متد، ممکن است لزوما همان چیزی نباشد که آن را شروع کرده است. برای نشان دادن این موضوع مثالی را در پیش می‌گیریم.
کامپوننتی را با نام  SynchronousInitComponent با کد زیر درنظر می‌گیریم. همانطور که از اسم آن مشخص است این کامپوننت به صورت متقارن یا همزمان پیاده‌سازی شده است:
<p>Sync rendered by thread @IdOfRenderingThread</p>

@code
{
  int IdOfRenderingThread;

  protected override void OnInitialized()
  {
    base.OnInitialized();
    IdOfRenderingThread =
      System.Threading.Thread.CurrentThread.ManagedThreadId;
  }
}
در حقیقت در متد OnInitialized آن، مقدار نخ جاری را توسط Thread.ManagedThreadId به دست می‌آوریم. بنابراین شماره نخ جاری پس از رندر شدن کامپوننت، در صفحه نمایش داده می‌شود.
حال در کامپوننت دیگری برای مثال کامپوننت index، کامپوننت همزمان فوق را به شکل زیر فراخوانی می‌کنیم:
@page "/"

<h1>Components with synchronous OnInitialized()</h1>
@for (int i = 0; i < 5; i++)
{
  <SynchronousInitComponent />
}
با این نتیجه:
Components with synchronous OnInitialized()
Sync rendered by thread 4
Sync rendered by thread 4
Sync rendered by thread 4
Sync rendered by thread 4
Sync rendered by thread 4
همانطور که ملاحظه می‌نمایید شناسه نخ یکسانی برای هر فراخوانی کامپوننت نشان داده می‌شود. بدیهی است در صورتیکه شما همین کد را اجرا کنید، ممکن است شماره نخ برنامه شما با کد من یکی نباشد؛ اما نتیجه یکی است. یعنی در تمامی موارد، یک عدد مشاهده می‌شود.
حال همین آزمایش را با متدهای نامتقارن یا ناهمزمان انجام می‌دهیم. کامپوننت AsynchronousInitComponent را با کد زیر درنظر بگیرید:
<p>Async rendered by thread @IdOfRenderingThread</p>

@code
{
  int IdOfRenderingThread;

  protected override async Task OnInitializedAsync()
  {
    // Runs synchronously as there is no code in base.OnInitialized(),
    // so the same thread is used
    await base.OnInitializedAsync().ConfigureAwait(false);
    IdOfRenderingThread =
      System.Threading.Thread.CurrentThread.ManagedThreadId;

    // Awaiting will schedule a job for later, and we will be assigned
    // whichever worker thread is next available
    await Task.Delay(1000).ConfigureAwait(false);
    IdOfRenderingThread =
      System.Threading.Thread.CurrentThread.ManagedThreadId;
  }
}
این کامپوننت هم دقیقا شبیه کامپوننت قبلی است؛ با این تفاوت که IdOfRenderingThread، مجددا بعد از یک تاخیر یک ثانیه‌ای مقداردهی شده‌است. این مقداردهی سبب رندر مجدد کامپوننت با تاخیر یک ثانیه می‌شود. حال در کامپوننت دیگری، کامپوننت غیرمتقارن فوق را به شکل زیر فراخوانی می‌کنیم:
@page "/async-init"

<h1>Components with asynchronous OnInitializedAsync()</h1>
@for (int i = 0; i < 5; i++)
{
  <AsynchronousInitComponent/>
}
با اجرا کردن برنامه، دقیقا نتایج، شبیه نتایج نتایج کامپوننت متقارن می‌باشد:
Components with asynchronous OnInitializedAsync()
Async rendered by thread 4
Async rendered by thread 4
Async rendered by thread 4
Async rendered by thread 4
Async rendered by thread 4
اما تنها بعد از یک ثانیه (await Task.Delay(1000)) ، متدهای OnInitializedAsync کامپوننت‌ها، به پایان می‌رسند و مقدار IdOfRenderingThread را پیش از به پایان رسیدن رندر صفحه، به روز رسانی می‌نمایند. اینبار، دیگر مقادیر نخ‌ها متفاوت خواهند بود:
Components with asynchronous OnInitializedAsync()
Async rendered by thread 7
Async rendered by thread 18
Async rendered by thread 10
Async rendered by thread 13
Async rendered by thread 11
با توجه به مطالب مطرح شده به این نتیجه می‌رسیم که این موضوع می‌تواند هنگام استفاده از یک وابستگی غیر ایمن ( non-thread-safe dependency ) مانند DBContext در چندین کامپوننت باعث بروز مشکل شود. نمونه‌ای از نحوه‌ی رویارویی با مشکلات احتمالی آن، در اینجا و اینجا بررسی شده‌است.  
مطالب
PowerShell 7.x - قسمت نهم - آشنایی با Crescendo
همانطور که در ابتدای این سری نیز اشاره شد، یکی از ویژگی‌های منحصربه‌فرد PowerShell، طراحی شیءگرای آن است، به‌طوریکه خروجی cmdletهای آن، به صورت آبجکت هستند. همچنین، در PowerShell امکان اجرای کامندهای native نیز وجود دارد. به عنوان مثال اگر کامند زیر را وارد کنید: 
git log --oneline
خروجی، همانطوری که در دیگر shellها انتظار میرود، نمایش داده خواهد شد؛ یعنی به صورت string. همچنین امکان intellisense را نیز برای پارامترهای کامند موردنظر نخواهیم داشت؛ چون در اصل، به اصطلاح یک legacy command است و نه یک cmdlet. برای بهره بردن از امکانات PowerShell میتوانیم این نوع کامندها را توسط یک wrapper به cmdlet تبدیل کنیم، اما آپدیت نگه‌داشتن این wrapper و نوشتن آن فرآیند سختی است. برای سهولت انجام اینکار، یک فریم‌ورک تحت عنوان Crescendo توسط مایکروسافت ارائه شده است.
یک مثال
فرض کنید میخواهیم کامند git log را به همراه تعدادی از دستورات آن به یک PowerShell cmdlet تبدیل کنیم؛ برای اینکار ابتدا نیاز است ماژول عنوان شده را نصب کنیم: 
Install-Module -Name Microsoft.PowerShell.Crescendo
بعد از نصب ماژول فوق، یکسری cmdlet به مجموعه کامندهای PowerShell اضافه خواهند شد. یکی از این کامندها New-CrescendoCommand است. با کمک این کامند، فایل JSON موردنیاز Crescendo را میتوانیم تولید کنیم: 
$Configuration = @{
    '$schema' = "https://aka.ms/PowerShell/Crescendo/Schemas/2021-11"
    Commands  = @()
}
$parameters = @{
    Verb = "Get"
    Noun = "GitLog"
    OriginalName = "git"
}
$Configuration.Commands += New-CrescendoCommand @parameters

$Configuration | ConvertTo-Json -Depth 3 | Out-File ./git-ps.json
در اینجا تعیین کرده‌ایم که کامندی که میخواهیم برایمان تولید شود، چه ویژگی‌هایی باید داشته باشد. به عنوان مثال Verb آن Get و Noun آن باید GitLog باشد (براساس استانداری که مایکروسافت برای نامگذاری cmdletها پیشنهاد میدهد). در نهایت میتوانیم به صورت Get-GitLog از آن استفاده کنیم. همچنین legacy command اصلی که میخواهیم برای آن cmdlet ایجاد کنیم نیز توسط OriginalName تعیین شده‌است. لازم به ذکر است که در ویندوز باید مسیر کامل آن را وارد کنید. سپس با اجرای دستورات فوق، خروجی زیر برایمان تولید خواهد شد: 
{
  "Commands": [
    {
      "Verb": "Get",
      "Noun": "GitLog",
      "OriginalName": "git",
      "OriginalCommandElements": null,
      "Platform": [
        "Windows",
        "Linux",
        "MacOS"
      ],
      "Elevation": null,
      "Aliases": null,
      "DefaultParameterSetName": null,
      "SupportsShouldProcess": false,
      "ConfirmImpact": null,
      "SupportsTransactions": false,
      "NoInvocation": false,
      "Description": null,
      "Usage": null,
      "Parameters": [],
      "Examples": [],
      "OriginalText": null,
      "HelpLinks": null,
      "OutputHandlers": null
    }
  ],
  "$schema": "https://aka.ms/PowerShell/Crescendo/Schemas/2021-11"
}
نکته: دقت داشته باشید که schema$ باید درون single quote نوشته شود؛ چون در غیراینصورت، key آن درون فایل تولید شده، خالی خواهد بود: 
"": "https://aka.ms/PowerShell/Crescendo/Schemas/2021-11",
با کمک این schema درون Visual Studio Code امکان Intelisense را نیز خواهیم داشت: 


اکنون باید این فایل Configuration را به Crescendo معرفی کنیم تا cmdlet را برایمان تولید کند. اینکار را توسط Export-CrescendoModule انجام خواهیم داد: 

Export-CrescendoModule -Configuration ./git-ps.json -ModuleName ./git-ps.psm1

با اجرای دستور فوق، فایل‌های git.psm1 و همچنین git.psd1 تولید خواهند شد. نیاز به بررسی فایل‌های جنریت شده نیست؛ چون تنها جایی که با آن باید در ارتباط باشیم، همان فایل JSON ابتدای بحث است که در ادامه آن را بررسی خواهیم کرد. اما قبل از آن اجازه دهید ماژول تولید شده را Import کنیم و دستور Get-GitLog را وارد کنیم: 

PP /> Import-Module ./git-ps.psd1
PS /> Get-GitLog

usage: git [-v | --version] [-h | --help] [-C <path>] [-c <name>=<value>]
           [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
           [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare]
           [--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
           [--super-prefix=<path>] [--config-env=<name>=<envvar>]
           <command> [<args>]

These are common Git commands used in various situations:

start a working area (see also: git help tutorial)
   clone     Clone a repository into a new directory
   init      Create an empty Git repository or reinitialize an existing one

work on the current change (see also: git help everyday)
   add       Add file contents to the index
   mv        Move or rename a file, a directory, or a symlink
   restore   Restore working tree files
   rm        Remove files from the working tree and from the index

examine the history and state (see also: git help revisions)
   bisect    Use binary search to find the commit that introduced a bug
   diff      Show changes between commits, commit and working tree, etc
   grep      Print lines matching a pattern
   log       Show commit logs
   show      Show various types of objects
   status    Show the working tree status

grow, mark and tweak your common history
   branch    List, create, or delete branches
   commit    Record changes to the repository
   merge     Join two or more development histories together
   rebase    Reapply commits on top of another base tip
   reset     Reset current HEAD to the specified state
   switch    Switch branches
   tag       Create, list, delete or verify a tag object signed with GPG

collaborate (see also: git help workflows)
   fetch     Download objects and refs from another repository
   pull      Fetch from and integrate with another repository or a local branch
   push      Update remote refs along with associated objects

'git help -a' and 'git help -g' list available subcommands and some
concept guides. See 'git help <command>' or 'git help <concept>'
to read about a specific subcommand or concept.
See 'git help git' for an overview of the system.

همانطور که مشاهده میکنید، خروجی دستور git، نمایش داده شده‌است. دلیل آن نیز این است که در فایل configuration، هیچ آرگومانی را به عنوان ورودی آن تعیین نکرده‌ایم. برای اضافه کردن آرگومان‌های موردنظر باید پراپرتی OrginalCommandElements را مقدار دهی کنیم: 

"OriginalCommandElements": ["log", "--oneline"],

بنابراین با فراخوانی دستور Get-GitLog، در اصل دستور git log —oneline فراخوانی خواهد شد:  

PS /> Get-GitLog

e9590e8 init

اما تا اینجا نیز خروجی به صورت رشته‌ایی است. برای داشتن یک خروجی Object، باید پراپرتی OutputHandlers را از Configuration، تغییر دهیم: 

"OutputHandlers": [
  {
    "ParameterSetName": "Default",
    "Handler": "$args[0] | ForEach-Object { $hash, $message = $_.Split(' ', 2) ; [PSCustomObject]@{ Hash = $hash; Message = $message } }"
  }
]

در اینجا توسط args$ به خروجی کامند اصلی دسترسی خواهیم داشت. این خروجی را سپس با کمک ForEach-Object، به یک شیء با پراپرتی‌های Hash و Message تبدیل کرده‌ایم. در اینجا فقط میخواستم روال تهیه یک آبجکت را از کامندهایی که خروجی JSON ندارند، نشان دهم؛ اما خوشبختانه توسط پرچم pretty در git log، امکان تهیه‌ی خروجی JSON را نیز داریم: 

git log --pretty=format:'{"commit": "%h", "author": "%an", "date": "%ad", "message": "%s"}'

در نتیجه عملاً نیازی به split کردن نیست و بجای آن میتوانیم به صورت مستقیم، خروجی را توسط ConvertFrom-Json پارز کنیم: 

"OutputHandlers": [
  {
    "ParameterSetName": "Default",
    "Handler": "$args[0] | ConvertFrom-Json"
  }
]

همچنین درون فایل schema با کمک پراپرتی Parameters، امکان تعریف پارامتر را نیز برای کامند Get-GitLog خواهیم داشت. به عنوان مثال میتوانیم فلگ reverse را نیز به کامند اصلی از طریق PowerShell ارسال کنیم: 

"Parameters": [
  {
    "Name": "reverse",
    "OriginalName": "--reverse",
    "ParameterType": "switch",
    "Description": "Reverse the order of the commits in the output."
  }
],

دقت داشته باشیم که با هربار تغییر فایل schema باید توسط دستور Export-CrescendoModule ماژول موردنظر را تولید کنید:

Export-CrescendoModule -Configuration ./git-ps.json -ModuleName ./git-ps.psm1
Import-Module ./git-ps.psd1

در نهایت cmdletمان به این صورت قابل استفاده خواهد بود:

مطالب
جایگزین کردن StructureMap با سیستم توکار تزریق وابستگی‌ها در ASP.NET Core 1.0
مدل برنامه زیر را در نظر بگیرید:
 public class Service
    {
        public int ServiceId { get; set; }
        public string ServiceName { get; set; }
    }
اینترفیس ICoreService عمل بازیابی اطلاعات کلاس بالا را بر عهده دارد:
 public interface ICoreService
    {
        Service LoadDefaultService();
    }
نتیجه تزریق وابستگی ICoreService برای کنترلر Home در یک پروژه ASP.NET Core 1.0/Asp.Net Mvc 6 چنین استثنایی بود:
An unhandled exception occurred while processing the request
  InvalidOperationException: Unable to resolve service for type 'WebApplication1.Models.ICoreService' while attempting to activate 'WebApplication1.Controllers.HomeController'
Microsoft.Extensions.Internal.ActivatorUtilities.GetService(IServiceProvider sp, Type type, Type requiredBy, Boolean isDefaultParameterRequired)
 
یعنی زمانیکه Activator میخواست کنترلر Home را فعالسازی کند، نتوانسته وابستگی ICoreService را برای کنترلر فراهم کند. این استثناء در Microsoft.Extensions.Internal.ActivatorUtilities.GetService اتفاق افتاده‌است.

در نسخه‌های قدیمی MVC (منظور نسخه‌های قبل از 6)، برای تزریق وابستگی‌ها از یک Controller Factory یا Dependency Resolver سفارشی استفاده می‌شد. اما در نسخه جدید MVC دیگری خبری از روشهای قدیمی نیست. چونکه یک سیستم تزریق وابستگی توکار، همراه با MVC یکپارچه شده‌است که عملیات تزریق وابستگی‌ها را انجام می‌دهد. سیستم تزریق وابستگی پیش فرض، تنها از 4 حالت عملیاتی پشتیبانی می‌کند:
1- Instance : در همه حال ، تنها یک نمونه خاص ارائه شده و شما مسئول وهله سازی آن هستید.
2- Transient : در هر بار (مثلا در هر درخواست) یک نمونه جدید ساخته میشود.
3- Singleton
4- Scoped : تنها یک نمونه در Scope فعلی ساخته می‌شود.

 تیم Asp.Net برای فراهم آوردن امکان تزریق وابستگی‌ها، تصمیم به انتزاعی کردن ویژگی‌های مشترک محبوبترین Ioc Containerها و اجازه دادن به میان افزارها، جهت ارتباط با این اینترفیس‌ها برای دستیابی به تزریق وابستگی بود.
حال بیایم نگاهی به این اینترفیس‌ها بیندازیم.
اگر به استثنای فوق نگاهی بیندازیم، می‌بینیم که متد GetService یک پارامتر از نوع IServiceProvider را میگیرد. پس اولین اینترفیس IServiceProvider می باشد. همانطور که از اسمش پیداست، کارش فرآهم آوردن سرویس می‌باشد. این اینترفیس فقط یک متد دارد، متد GetService. متد GetService مانند Container.GetInstance در StructureMap می‌باشد. تمام میان افزارها به 2 روش می‌توانند به نمونه‌ای از IServiceProvider دسترسی داشته باشند:
1- Application-Level : از طریق HttpContext.ApplicationServices برای میان افزار قابل دسترسی خواهد بود.
2- Request-Level : از طریق HttpContext.RequestServices. این Service Scope Provider توسط میان افزار در شروع هر Request Pipeline، برای هر درخواست ایجاد و در پایان درخواست توسط همان میان افزار نابود می‌گردد.
اینترفیس بعدی IServiceScope می‌باشد. همان طور که قبلا گفتیم RequestServices یک Scope Container را برای هر درخواست ساخته و در پایان همان درخواست، آن را نابود میکند. اما این کار چگونه مدیریت می‌شود؟ پاسخ، اینترفیس IServiceScope می باشد. این اینترفیس مانند یک Wrapper حول Scope Container عمل میکند و در پایان هر درخواست آن را نابود می‌کند. حال سوال اینجاست که چه کسی مسئول ساخت IServiceScope می‌باشد؟ پاسخ اینترفیس IServiceScopeFactory می‌باشد. این اینترفیس توسط متد CreateScope اقدام به ساخت یک نمونه از اینترفیس IserviceScope می‌نماید.
مورد بعدی ServiceLifeTime می‌باشد. یک Enum که حاوی سه مقدار زیر می‌باشد:
namespace Microsoft.Extensions.DependencyInjection
{
    //
    // Summary:
    //     Specifies the lifetime of a service in an Microsoft.Extensions.DependencyInjection.IServiceCollection.
    public enum ServiceLifetime
    {
        //
        // Summary:
        //     Specifies that a single instance of the service will be created.
        Singleton = 0,
        //
        // Summary:
        //     Specifies that a new instance of the service will be created for each scope.
        //
        // Remarks:
        //     In ASP.NET Core applications a scope is created around each server request.
        Scoped = 1,
        //
        // Summary:
        //     Specifies that a new instance of the service will be created every time it is
        //     requested.
        Transient = 2
    }
}
آخرین مورد کلاس ServiceDescriptor می‌باشد.  این کلاس اطلاعاتی را که Container برای رجیستر کردن سرویس به آنها نیاز دارد، در خود نگهداری می‌کند. این جمله را در نظر بگیرید : " هی Container، وقتی میخواهی این سرویس را رجیستر کنی، اطمینان حاصل کن که Singleton باشد و یک نمونه از نوع X را پیاده سازی کند." تمامی اطلاعات جمله قبل در ServiceDescriptor نگهداری می‌شود.
خوب! حال بیایم تا سرویس خود را رجیستر کنیم. در کلاس StartUp پروژه در متد ConfigurationServices خط زیر را اضافه می‌کنیم:
public void ConfigureServices(IServiceCollection services)
        {                        
            ServiceDescriptor descriptor = new ServiceDescriptor(typeof(ICoreService),typeof(CoreServise),ServiceLifetime.Transient);
            services.Add(descriptor);
            services.AddMvc();          
        }
حال اگر برنامه را اجرا کنیم مشکل برطرف شده است.

ساخت یک Service Descriptor و اضافه کردن آن به سرویسها، فلسفه وجودی میان افزارها را زیر سوال می‌برد. پس بجای ایجاد یک Service Descriptor، از متدهای الحاقی تدارک دیده شده استفاده میکنیم. مثلا بجای دو خط کد بالا می‌توان از کد زیر استفاده نمود:

services.AddTransient<ICoreService,CoreServise>();

حال که یک دید کلی از نحوه کار مکانیزم تزریق وابستگی بدست آوردیم، میخواهیم این مکانیزم را با StructureMap جایگزین کنیم. بدین منظور ابتدا پکیج StructureMap را نصب میکنم.

در مرحله اول باید کلاسهایی را تدارک ببینیم که اینترفیس‌های بالا را پیاده سازی نمایند. یعنی کلاسهای ما باید بتوانند همان کاری را انجام دهند که مکانیزم پیش فرض MVC انجام می‌دهد. 

اولین مورد، کلاس StructureMapServiceProvider می‌باشد.

internal class StructureMapServiceProvider : IServiceProvider
    {
        private readonly IContainer _container;

        public StructureMapServiceProvider(IContainer container, bool scoped = false)
        {            
            _container = container;
        }

        public object GetService(Type type)
        {
            try
            {
                return _container.GetInstance(type);
            }
            catch
            {
                return null;
            }
        }
    }

مورد دوم کلاس StructureMapServiceScope می‌باشد:

internal class StructureMapServiceScope : IServiceScope
    {
        private readonly IContainer _container;
        private readonly IContainer _childContainer;
        private IServiceProvider _provider;

        public StructureMapServiceScope(IContainer container)
        {
            _container = container;
            _childContainer = _container.GetNestedContainer();
            _provider = new StructureMapServiceProvider(_childContainer, true);
        }

        public IServiceProvider ServiceProvider => _provider;

        public void Dispose()
        {
            _provider = null;
            if (_childContainer != null)
                _childContainer.Dispose();
        }
    }

مورد سوم StructureMapServiceScopeFactory می‌باشد:

internal class StructureMapServiceScopeFactory : IServiceScopeFactory
    {
        private IContainer _container;

        public StructureMapServiceScopeFactory(IContainer container)
        {
            _container = container;
        }

        public IServiceScope CreateScope()
        {
            return new StructureMapServiceScope(_container);
        }
    }

مورد بعدی کلاس StructureMapPopulator می‌باشد. وظیفه این کلاس جمع آوری اطلاعات مربوط به سرویس‌ها می‌باشد.

internal class StructureMapPopulator
    {
        private IContainer _container;

        public StructureMapPopulator(IContainer container)
        {
            _container = container;
        }

        public void Populate(IEnumerable<ServiceDescriptor> descriptors)
        {
            _container.Configure(c =>
            {
                c.For<IServiceProvider>().Use(new StructureMapServiceProvider(_container));
                c.For<IServiceScopeFactory>().Use<StructureMapServiceScopeFactory>();

                foreach (var descriptor in descriptors)
                {
                    switch (descriptor.Lifetime)
                    {
                        case ServiceLifetime.Singleton:
                            Use(c.For(descriptor.ServiceType).Singleton(), descriptor);
                            break;
                        case ServiceLifetime.Transient:
                            Use(c.For(descriptor.ServiceType), descriptor);
                            break;
                        case ServiceLifetime.Scoped:
                            Use(c.For(descriptor.ServiceType), descriptor);
                            break;
                    }
                }
            });
        }

        private static void Use(GenericFamilyExpression expression, ServiceDescriptor descriptor)
        {
            if (descriptor.ImplementationFactory != null)
            {
                expression.Use(Guid.NewGuid().ToString(), context => { return descriptor.ImplementationFactory(context.GetInstance<IServiceProvider>()); });
            }
            else if (descriptor.ImplementationInstance != null)
            {
                expression.Use(descriptor.ImplementationInstance);
            }
            else if (descriptor.ImplementationType != null)
            {
                expression.Use(descriptor.ImplementationType);
            }
            else
            {
                throw new InvalidOperationException("IServiceDescriptor is invalid");
            }
        }
    }

و در آخر کلاس StructureMapRegistration می‌باشد:

public static class StructureMapRegistration
    {
        public static void Populate(this IContainer container, IEnumerable<ServiceDescriptor> descriptors)
        {
            var populator = new StructureMapPopulator(container);
            populator.Populate(descriptors);
        }
    }

نهایتاً باید متد ConfigurationServices در کلاس StartUp را اندکی تغییر دهیم.

public IServiceProvider ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
           
            var container = new Container();
            container.Configure(configure =>
            {
                configure.For<ICoreService>().Use<CoreServise>();
            });

            container.Populate(services);

            return container.GetInstance<IServiceProvider>();
        }

در کد بالا، متد ConfigurationServices به جای آنکه Void برگرداند، نمونه‌ای از اینترفیس IServiceProvider را برمی‌گرداند. حال اگر برنامه را اجرا کنیم، وابستگی‌ها توسط StructureMap تزریق شده و برنامه بدون هیچ مشکلی اجرا می‌شود.

مطالب
پیاده سازی JSON Web Token با ASP.NET Web API 2.x
- پیشنیار بحث «معرفی JSON Web Token»

پیاده سازی‌های زیادی را در مورد JSON Web Token با ASP.NET Web API، با کمی جستجو می‌توانید پیدا کنید. اما مشکلی که تمام آن‌ها دارند، شامل این موارد هستند:
- چون توکن‌های JWT، خودشمول هستند (در پیشنیاز بحث مطرح شده‌است)، تا زمانیکه این توکن منقضی نشود، کاربر با همان سطح دسترسی قبلی می‌تواند به سیستم، بدون هیچگونه مانعی لاگین کند. در این حالت اگر این کاربر غیرفعال شود، کلمه‌ی عبور او تغییر کند و یا سطح دسترسی‌های او کاهش یابند ... مهم نیست! باز هم می‌تواند با همان توکن قبلی لاگین کند.
- در روش JSON Web Token، عملیات Logout سمت سرور بی‌معنا است. یعنی اگر برنامه‌ای در سمت کاربر، قسمت logout را تدارک دیده باشد، چون در سمت سرور این توکن‌ها جایی ذخیره نمی‌شوند، عملا این logout بی‌مفهوم است و مجددا می‌توان از همان توکن قبلی، برای لاگین به سرور استفاده کرد. چون این توکن شامل تمام اطلاعات لازم برای لاگین است و همچنین جایی هم در سرور ثبت نشده‌است که این توکن در اثر logout، باید غیرمعتبر شود.
- با یک توکن از مکان‌های مختلفی می‌توان دسترسی لازم را جهت استفاده‌ی از قسمت‌های محافظت شده‌ی برنامه یافت (در صورت دسترسی، چندین نفر می‌توانند از آن استفاده کنند).

به همین جهت راه حلی عمومی برای ذخیره سازی توکن‌های صادر شده از سمت سرور، در بانک اطلاعاتی تدارک دیده شد که در ادامه به بررسی آن خواهیم پرداخت و این روشی است که می‌تواند به عنوان پایه مباحث Authentication و Authorization برنامه‌های تک صفحه‌ای وب استفاده شود. البته سمت کلاینت این راه حل با jQuery پیاده سازی شده‌است (عمومی است؛ برای طرح مفاهیم پایه) و سمت سرور آن به عمد از هیچ نوع بانک اطلاعات و یا ORM خاصی استفاده نمی‌کند. سرویس‌های آن برای بکارگیری انواع و اقسام روش‌های ذخیره سازی اطلاعات قابل تغییر هستند و الزامی نیست که حتما از EF استفاده کنید یا از ASP.NET Identity یا هر روش خاص دیگری.


نگاهی به برنامه


در اینجا تمام قابلیت‌های این پروژه را مشاهده می‌کنید.
- امکان لاگین
- امکان دسترسی به یک کنترلر مزین شده‌ی با فلیتر Authorize
- امکان دسترسی به یک کنترلر مزین شده‌ی با فلیتر Authorize جهت کاربری با نقش Admin
- پیاده سازی مفهوم ویژه‌ای به نام refresh token که نیاز به لاگین مجدد را پس از منقضی شدن زمان توکن اولیه‌ی لاگین، برطرف می‌کند.
- پیاده سازی logout


بسته‌های پیشنیاز برنامه

پروژه‌ای که در اینجا بررسی شده‌است، یک پروژه‌ی خالی ASP.NET Web API 2.x است و برای شروع به کار با JSON Web Tokenها، تنها نیاز به نصب 4 بسته‌ی زیر را دارد:
PM> Install-Package Microsoft.Owin.Host.SystemWeb
PM> Install-Package Microsoft.Owin.Security.Jwt
PM> Install-Package structuremap
PM> Install-Package structuremap.web
بسته‌ی Microsoft.Owin.Host.SystemWeb سبب می‌شود تا کلاس OwinStartup به صورت خودکار شناسایی و بارگذاری شود. این کلاسی است که کار تنظیمات اولیه‌ی JSON Web token را انجام می‌دهد و بسته‌ی Microsoft.Owin.Security.Jwt شامل تمام امکاناتی است که برای راه اندازی توکن‌های JWT نیاز داریم.
از structuremap هم برای تنظیمات تزریق وابستگی‌های برنامه استفاده شده‌است. به این صورت قسمت تنظیمات اولیه‌ی JWT ثابت باقی خواهد ماند و صرفا نیاز خواهید داشت تا کمی قسمت سرویس‌های برنامه را بر اساس بانک اطلاعاتی و روش ذخیره سازی خودتان سفارشی سازی کنید.


دریافت کدهای کامل برنامه

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


بررسی کلاس AppJwtConfiguration

کلاس AppJwtConfiguration، جهت نظم بخشیدن به تعاریف ابتدایی توکن‌های برنامه در فایل web.config، ایجاد شده‌است. اگر به فایل web.config برنامه مراجعه کنید، یک چنین تعریفی را مشاهده خواهید کرد:
<appJwtConfiguration
    tokenPath="/login"
    expirationMinutes="2"
    refreshTokenExpirationMinutes="60"
    jwtKey="This is my shared key, not so secret, secret!"
    jwtIssuer="http://localhost/"
    jwtAudience="Any" />
این قسمت جدید بر اساس configSection ذیل که به کلاس AppJwtConfiguration اشاره می‌کند، قابل استفاده شده‌است (بنابراین اگر فضای نام این کلاس را تغییر دادید، باید این قسمت را نیز مطابق آن ویرایش کنید؛ درغیراینصورت، appJwtConfiguration قابل شناسایی نخواهد بود):
 <configSections>
    <section name="appJwtConfiguration" type="JwtWithWebAPI.JsonWebTokenConfig.AppJwtConfiguration" />
</configSections>
- در اینجا tokenPath، یک مسیر دلخواه است. برای مثال در اینجا به مسیر login تنظیم شده‌است. برنامه‌ای که با Microsoft.Owin.Security.Jwt کار می‌کند، نیازی ندارد تا یک قسمت لاگین مجزا داشته باشد (مثلا یک کنترلر User و بعد یک اکشن متد اختصاصی Login). کار لاگین، در متد GrantResourceOwnerCredentials کلاس AppOAuthProvider انجام می‌شود. اینجا است که نام کاربری و کلمه‌ی عبور کاربری که به سمت سرور ارسال می‌شوند، توسط Owin دریافت و در اختیار ما قرار می‌گیرند.
- در این تنظیمات، دو زمان منقضی شدن را مشاهده می‌کنید؛ یکی مرتبط است به access tokenها و دیگری مرتبط است به refresh tokenها که در مورد این‌ها، در ادامه بیشتر توضیح داده خواهد شد.
- jwtKey، یک کلید قوی است که از آن برای امضاء کردن توکن‌ها در سمت سرور استفاده می‌شود.
- تنظیمات Issuer و Audience هم در اینجا اختیاری هستند.

یک نکته
جهت سهولت کار تزریق وابستگی‌ها، برای کلاس AppJwtConfiguration، اینترفیس IAppJwtConfiguration نیز تدارک دیده شده‌است و در تمام تنظیمات ابتدایی JWT، از این اینترفیس بجای استفاده‌ی مستقیم از کلاس AppJwtConfiguration استفاده شده‌است.


بررسی کلاس OwinStartup

شروع به کار تنظیمات JWT و ورود آن‌ها به چرخه‌ی حیات Owin از کلاس OwinStartup آغاز می‌شود. در اینجا علت استفاده‌ی از SmObjectFactory.Container.GetInstance انجام تزریق وابستگی‌های لازم جهت کار با دو کلاس AppOAuthOptions و AppJwtOptions است.
- در کلاس AppOAuthOptions تنظیماتی مانند نحوه‌ی تهیه‌ی access token و همچنین refresh token ذکر می‌شوند.
- در کلاس AppJwtOptions تنظیمات فایل وب کانفیگ، مانند کلید مورد استفاده‌ی جهت امضای توکن‌های صادر شده، ذکر می‌شوند.


حداقل‌های بانک اطلاعاتی مورد نیاز جهت ذخیره سازی وضعیت کاربران و توکن‌های آن‌ها

همانطور که در ابتدای بحث عنوان شد، می‌خواهیم اگر سطوح دسترسی کاربر تغییر کرد و یا اگر کاربر logout کرد، توکن فعلی او صرفنظر از زمان انقضای آن، بلافاصله غیرقابل استفاده شود. به همین جهت نیاز است حداقل دو جدول زیر را در بانک اطلاعاتی تدارک ببینید:
الف) کلاس User
در کلاس User، بر مبنای اطلاعات خاصیت Roles آن است که ویژگی Authorize با ذکر نقش مثلا Admin کار می‌کند. بنابراین حداقل نقشی را که برای کاربران، در ابتدای کار نیاز است مشخص کنید، نقش user است.
همچنین خاصیت اضافه‌تری به نام SerialNumber نیز در اینجا درنظر گرفته شده‌است. این مورد را باید به صورت دستی مدیریت کنید. اگر کاربری کلمه‌ی عبورش را تغییر داد، اگر مدیری نقشی را به او انتساب داد یا از او گرفت و یا اگر کاربری غیرفعال شد، مقدار خاصیت و فیلد SerialNumber را با یک Guid جدید به روز رسانی کنید. این Guid در برنامه با Guid موجود در توکن مقایسه شده و بلافاصله سبب عدم دسترسی او خواهد شد (در صورت عدم تطابق).

ب) کلاس UserToken
در کلاس UserToken کار نگهداری ریز اطلاعات توکن‌های صادر شده صورت می‌گیرد. توکن‌های صادر شده دارای access token و refresh token هستند؛ به همراه زمان انقضای آن‌ها. به این ترتیب زمانیکه کاربری درخواستی را به سرور ارسال می‌کند، ابتدا token او را دریافت کرده و سپس بررسی می‌کنیم که آیا اصلا چنین توکنی در بانک اطلاعاتی ما وجود خارجی دارد یا خیر؟ آیا توسط ما صادر شده‌است یا خیر؟ اگر خیر، بلافاصله دسترسی او قطع خواهد شد. برای مثال عملیات logout را طوری طراحی می‌کنیم که تمام توکن‌های یک شخص را در بانک اطلاعاتی حذف کند. به این ترتیب توکن قبلی او دیگر قابلیت استفاده‌ی مجدد را نخواهد داشت.


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

در لایه سرویس برنامه، شما سه سرویس را مشاهده خواهید کرد که قابلیت جایگزین شدن با کدهای یک ORM را دارند (نوع آن ORM مهم نیست):
الف) سرویس TokenStoreService
public interface ITokenStoreService
{
    void CreateUserToken(UserToken userToken);
    bool IsValidToken(string accessToken, int userId);
    void DeleteExpiredTokens();
    UserToken FindToken(string refreshTokenIdHash);
    void DeleteToken(string refreshTokenIdHash);
    void InvalidateUserTokens(int userId);
    void UpdateUserToken(int userId, string accessTokenHash);
}
کار سرویس TokenStore، ذخیره سازی و تعیین اعتبار توکن‌های صادر شده‌است. در اینجا ثبت یک توکن، بررسی صحت وجود یک توکن رسیده، حذف توکن‌های منقضی شده، یافتن یک توکن بر اساس هش توکن، حذف یک توکن بر اساس هش توکن، غیرمعتبر کردن و حذف تمام توکن‌های یک شخص و به روز رسانی توکن یک کاربر، پیش بینی شده‌اند.
پیاده سازی این کلاس بسیار شبیه است به پیاده سازی ORMهای موجود و فقط یک SaveChanges را کم دارد.

یک نکته:
در سرویس ذخیره سازی توکن‌ها، یک چنین متدی قابل مشاهده است:
public void CreateUserToken(UserToken userToken)
{
   InvalidateUserTokens(userToken.OwnerUserId);
   _tokens.Add(userToken);
}
استفاده از InvalidateUserTokens در اینجا سبب خواهد شد با لاگین شخص و یا صدور توکن جدیدی برای او، تمام توکن‌های قبلی او حذف شوند. به این ترتیب امکان لاگین چندباره و یا یافتن دسترسی به منابع محافظت شده‌ی برنامه در سرور توسط چندین نفر (که به توکن شخص دسترسی یافته‌اند یا حتی تقاضای توکن جدیدی کرده‌اند)، میسر نباشد. همینکه توکن جدیدی صادر شود، تمام لاگین‌های دیگر این شخص غیرمعتبر خواهند شد.


ب) سرویس UsersService
public interface IUsersService
{
    string GetSerialNumber(int userId);
    IEnumerable<string> GetUserRoles(int userId);
    User FindUser(string username, string password);
    User FindUser(int userId);
    void UpdateUserLastActivityDate(int userId);
}
از کلاس سرویس کاربران، برای یافتن شماره سریال یک کاربر استفاده می‌شود. در مورد شماره سریال پیشتر بحث کردیم و هدف آن مشخص سازی وضعیت تغییر این موجودیت است. اگر کاربری اطلاعاتش تغییر کرد، این فیلد باید با یک Guid جدید مقدار دهی شود.
همچنین متدهای دیگری برای یافتن یک کاربر بر اساس نام کاربری و کلمه‌ی عبور او (جهت مدیریت مرحله‌ی لاگین)، یافتن کاربر بر اساس Id او (جهت استخراج اطلاعات کاربر) و همچنین یک متد اختیاری نیز برای به روز رسانی فیلد آخرین تاریخ فعالیت کاربر در اینجا پیش بینی شده‌اند.

ج) سرویس SecurityService
public interface ISecurityService
{
   string GetSha256Hash(string input);
}
در قسمت‌های مختلف این برنامه، هش SHA256 مورد استفاده قرار گرفته‌است که با توجه به مباحث تزریق وابستگی‌ها، کاملا قابل تعویض بوده و برنامه صرفا با اینترفیس آن کار می‌کند.


پیاده سازی قسمت لاگین و صدور access token

در کلاس AppOAuthProvider کار پیاده سازی قسمت لاگین برنامه انجام شده‌است. این کلاسی است که توسط کلاس AppOAuthOptions به OwinStartup معرفی می‌شود. قسمت‌های مهم کلاس AppOAuthProvider به شرح زیر هستند:
برای درک عملکرد این کلاس، در ابتدای متدهای مختلف آن، یک break point قرار دهید. برنامه را اجرا کرده و سپس بر روی دکمه‌ی login کلیک کنید. به این ترتیب جریان کاری این کلاس را بهتر می‌توانید درک کنید. کار آن با فراخوانی متد ValidateClientAuthentication شروع می‌شود. چون با یک برنامه‌ی وب در حال کار هستیم، ClientId آن‌را نال درنظر می‌گیریم و برای ما مهم نیست. اگر کلاینت ویندوزی خاصی را تدارک دیدید، این کلاینت می‌تواند ClientId ویژه‌ای را به سمت سرور ارسال کند که در اینجا مدنظر ما نیست.
مهم‌ترین قسمت این کلاس، متد GrantResourceOwnerCredentials است که پس از ValidateClientAuthentication بلافاصله فراخوانی می‌شود. اگر به کدهای آن دقت کنید،  خود owin دارای خاصیت‌های user name و password نیز هست.
این اطلاعات را به نحو ذیل از کلاینت خود دریافت می‌کند. اگر به فایل index.html مراجعه کنید، یک چنین تعریفی را برای متد login می‌توانید مشاهده کنید:
function doLogin() {
    $.ajax({
        url: "/login", // web.config --> appConfiguration -> tokenPath
        data: {
            username: "Vahid",
            password: "1234",
            grant_type: "password"
        },
        type: 'POST', // POST `form encoded` data
        contentType: 'application/x-www-form-urlencoded'
url آن به همان مسیری که در فایل web.config تنظیم کردیم، اشاره می‌کند. فرمت data ایی که به سرور ارسال می‌شود، دقیقا باید به همین نحو باشد و content type آن نیز مهم است و owin فقط حالت form-urlencoded را پردازش می‌کند. به این ترتیب است که user name و password توسط owin قابل شناسایی شده و grant_type آن است که سبب فراخوانی GrantResourceOwnerCredentials می‌شود و مقدار آن نیز دقیقا باید password باشد (در حین لاگین).
در متد GrantResourceOwnerCredentials کار بررسی نام کاربری و کلمه‌ی عبور کاربر صورت گرفته و در صورت یافت شدن کاربر (صحیح بودن اطلاعات)، نقش‌های او نیز به عنوان Claim جدید به توکن اضافه می‌شوند.

در اینجا یک Claim سفارشی هم اضافه شده‌است:
 identity.AddClaim(new Claim(ClaimTypes.UserData, user.UserId.ToString()));
کار آن ذخیره سازی userId کاربر، در توکن صادر شده‌است. به این صورت هربار که توکن به سمت سرور ارسال می‌شود، نیازی نیست تا یکبار از بانک اطلاعاتی بر اساس نام او، کوئری گرفت و سپس id او را یافت. این id در توکن امضاء شده، موجود است. نمونه‌ی نحوه‌ی کار با آن‌را در کنترلرهای این API می‌توانید مشاهده کنید. برای مثال در MyProtectedAdminApiController این اطلاعات از توکن استخراج شده‌اند (جهت نمایش مفهوم).

در انتهای این کلاس، از متد TokenEndpointResponse جهت دسترسی به access token نهایی صادر شده‌ی برای کاربر، استفاده کرده‌ایم. هش این access token را در بانک اطلاعاتی ذخیره می‌کنیم (جستجوی هش‌ها سریعتر هستند از جستجوی یک رشته‌ی طولانی؛ به علاوه در صورت دسترسی به بانک اطلاعاتی، اطلاعات هش‌ها برای مهاجم قابل استفاده نیست).

اگر بخواهیم اطلاعات ارسالی به کاربر را پس از لاگین، نمایش دهیم، به شکل زیر خواهیم رسید:


در اینجا access_token همان JSON Web Token صادر شده‌است که برنامه‌ی کلاینت از آن برای اعتبارسنجی استفاده خواهد کرد.

بنابراین خلاصه‌ی مراحل لاگین در اینجا به ترتیب ذیل است:
- فراخوانی متد  ValidateClientAuthenticationدر کلاس AppOAuthProvider . طبق معمول چون ClientID نداریم، این مرحله را قبول می‌کنیم.
- فراخوانی متد GrantResourceOwnerCredentials در کلاس AppOAuthProvider . در اینجا کار اصلی لاگین به همراه تنظیم Claims کاربر انجام می‌شود. برای مثال نقش‌های او به توکن صادر شده اضافه می‌شوند.
- فراخوانی متد Protect در کلاس AppJwtWriterFormat که کار امضای دیجیتال access token را انجام می‌دهد.
- فراخوانی متد CreateAsync در کلاس RefreshTokenProvider. کار این متد صدور توکن ویژه‌ای به نام refresh است. این توکن را در بانک اطلاعاتی ذخیره خواهیم کرد. در اینجا چیزی که به سمت کلاینت ارسال می‌شود صرفا یک guid است و نه اصل refresh token.
- فرخوانی متد TokenEndpointResponse در کلاس AppOAuthProvider . از این متد جهت یافتن access token نهایی تولید شده و ثبت هش آن در بانک اطلاعاتی استفاده می‌کنیم.


پیاده سازی قسمت صدور Refresh token

در تصویر فوق، خاصیت refresh_token را هم در شیء JSON ارسالی به سمت کاربر مشاهده می‌کنید. هدف از refresh_token، تمدید یک توکن است؛ بدون ارسال کلمه‌ی عبور و نام کاربری به سرور. در اینجا access token صادر شده، مطابق تنظیم expirationMinutes در فایل وب کانفیگ، منقضی خواهد شد. اما طول عمر refresh token را بیشتر از طول عمر access token در نظر می‌گیریم. بنابراین طول عمر یک access token کوتاه است. زمانیکه access token منقضی شد، نیازی نیست تا حتما کاربر را به صفحه‌ی لاگین هدایت کنیم. می‌توانیم refresh_token را به سمت سرور ارسال کرده و به این ترتیب درخواست صدور یک access token جدید را ارائه دهیم. این روش هم سریعتر است (کاربر متوجه این retry نخواهد شد) و هم امن‌تر است چون نیازی به ارسال کلمه‌ی عبور و نام کاربری به سمت سرور وجود ندارند.
سمت کاربر، برای درخواست صدور یک access token جدید بر اساس refresh token صادر شده‌ی در زمان لاگین، به صورت زیر عمل می‌شود:
function doRefreshToken() {
    // obtaining new tokens using the refresh_token should happen only if the id_token has expired.
    // it is a bad practice to call the endpoint to get a new token every time you do an API call.
    $.ajax({
        url: "/login", // web.config --> appConfiguration -> tokenPath
        data: {
            grant_type: "refresh_token",
            refresh_token: refreshToken
        },
        type: 'POST', // POST `form encoded` data
        contentType: 'application/x-www-form-urlencoded'
در اینجا نحوه‌ی عملکرد، همانند متد login است. با این تفاوت که grant_type آن اینبار بجای password مساوی refresh_token است و مقداری که به عنوان refresh_token به سمت سرور ارسال می‌کند، همان مقداری است که در عملیات لاگین از سمت سرور دریافت کرده‌است. آدرس ارسال این درخواست نیز همان tokenPath تنظیم شده‌ی در فایل web.config است. بنابراین مراحلی که در اینجا طی خواهند شد، به ترتیب زیر است:
- فرخوانی متد ValidateClientAuthentication در کلاس AppOAuthProvider . طبق معمول چون ClientID نداریم، این مرحله را قبول می‌کنیم.
- فراخوانی متد ReceiveAsync در کلاس RefreshTokenProvider. در قسمت توضیح مراحل لاگین، عنوان شد که پس از فراخوانی متد GrantResourceOwnerCredentials جهت لاگین، متد CreateAsync در کلاس RefreshTokenProvider فراخوانی می‌شود. اکنون در متد ReceiveAsync این refresh token ذخیره شده‌ی در بانک اطلاعاتی را یافته (بر اساس Guid ارسالی از طرف کلاینت) و سپس Deserialize می‌کنیم. به این ترتیب است که کار درخواست یک access token جدید بر مبنای refresh token موجود آغاز می‌شود.
- فراخوانی GrantRefreshToken در کلاس AppOAuthProvider . در اینجا اگر نیاز به تنظیم Claim اضافه‌تری وجود داشت، می‌توان اینکار را انجام داد.
- فراخوانی متد Protect در کلاس AppJwtWriterFormat که کار امضای دیجیتال access token جدید را انجام می‌دهد.
- فراخوانی CreateAsync در کلاس RefreshTokenProvider . پس از اینکه context.DeserializeTicket در متد ReceiveAsync  بر مبنای refresh token قبلی انجام شد، مجددا کار تولید یک توکن جدید در متد CreateAsync شروع می‌شود و زمان انقضاءها تنظیم خواهند شد.
- فراخوانی TokenEndpointResponse در کلاس AppOAuthProvider . مجددا از این متد برای دسترسی به access token جدید و ذخیره‌ی هش آن در بانک اطلاعاتی استفاده می‌کنیم.


پیاده سازی فیلتر سفارشی JwtAuthorizeAttribute

در ابتدای بحث عنوان کردیم که اگر مشخصات کاربر تغییر کردند یا کاربر logout کرد، امکان غیرفعال کردن یک توکن را نداریم و این توکن تا زمان انقضای آن معتبر است. این نقیصه را با طراحی یک AuthorizeAttribute سفارشی جدید به نام JwtAuthorizeAttribute برطرف می‌کنیم. نکات مهم این فیلتر به شرح زیر هستند:
- در اینجا در ابتدا بررسی می‌شود که آیا درخواست رسیده‌ی به سرور، حاوی access token هست یا خیر؟ اگر خیر، کار همینجا به پایان می‌رسد و دسترسی کاربر قطع خواهد شد.
- سپس بررسی می‌کنیم که آیا درخواست رسیده پس از مدیریت توسط Owin، دارای Claims است یا خیر؟ اگر خیر، یعنی این توکن توسط ما صادر نشده‌است.
- در ادامه شماره سریال موجود در access token را استخراج کرده و آن‌را با نمونه‌ی موجود در دیتابیس مقایسه می‌کنیم. اگر این دو یکی نبودند، دسترسی کاربر قطع می‌شود.
- همچنین در آخر بررسی می‌کنیم که آیا هش این توکن رسیده، در بانک اطلاعاتی ما موجود است یا خیر؟ اگر خیر باز هم یعنی این توکن توسط ما صادر نشده‌است.

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

و نکته‌ی مهم اینجا است که از این پس بجای فیلتر معمولی Authorize، از فیلتر جدید JwtAuthorize در برنامه استفاده خواهیم کرد:
 [JwtAuthorize(Roles = "Admin")]
public class MyProtectedAdminApiController : ApiController


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

تا اینجا کار صدور توکن‌های برنامه به پایان می‌رسد. برای استفاده‌ی از این توکن‌ها در سمت کاربر، به فایل index.html دقت کنید. در متد doLogin، پس از موفقیت عملیات دو متغیر جدید مقدار دهی می‌شوند:
var jwtToken;
var refreshToken;
 
function doLogin() {
    $.ajax({
     // same as before
    }).then(function (response) {
        jwtToken = response.access_token;
        refreshToken = response.refresh_token; 
    }
از متغیر jwtToken به ازای هربار درخواستی به یکی از کنترلرهای سایت، استفاده می‌کنیم و از متغیر refreshToken در متد doRefreshToken برای درخواست یک access token جدید. برای مثال اینبار برای اعتبارسنجی درخواست ارسالی به سمت سرور، نیاز است خاصیت headers را به نحو ذیل مقدار دهی کرد:
function doCallApi() {
    $.ajax({
        headers: { 'Authorization': 'Bearer ' + jwtToken },
        url: "/api/MyProtectedApi",
        type: 'GET'
    }).then(function (response) {
بنابراین هر درخواست ارسالی به سرور باید دارای هدر ویژه‌ی Bearer فوق باشد؛ در غیراینصورت با پیام خطای 401، از دسترسی به منابع سرور منع می‌شود.


پیاده سازی logout سمت سرور و کلاینت

پیاده سازی سمت سرور logout را در کنترلر UserController مشاهده می‌کنید. در اینجا در اکشن متد Logout، کار حذف توکن‌های کاربر از بانک اطلاعاتی انجام می‌شود. به این ترتیب دیگر مهم نیست که توکن او هنوز منقضی شده‌است یا خیر. چون هش آن دیگر در جدول توکن‌ها وجود ندارد، از فیلتر JwtAuthorizeAttribute رد نخواهد شد.
سمت کلاینت آن نیز در فایل index.html ذکر شده‌است:
function doLogout() {
    $.ajax({
        headers: { 'Authorization': 'Bearer ' + jwtToken },
        url: "/api/user/logout",
        type: 'GET'
تنها کاری که در اینجا انجام شده، فراخوانی اکشن متد logout سمت سرور است. البته ذکر jwtToken نیز در اینجا الزامی است. زیرا این توکن است که حاوی اطلاعاتی مانند userId کاربر فعلی است و بر این اساس است که می‌توان رکوردهای او را در جدول توکن‌ها یافت و حذف کرد.


بررسی تنظیمات IoC Container برنامه

تنظیمات IoC Container برنامه را در پوشه‌ی IoCConfig می‌توانید ملاحظه کنید. از کلاس SmWebApiControllerActivator برای فعال سازی تزریق وابستگی‌ها در کنترلرهای Web API استفاده می‌شود و از کلاس SmWebApiFilterProvider برای فعال سازی تزریق وابستگی‌ها در فیلتر سفارشی که ایجاد کردیم، کمک گرفته خواهد شد.
هر دوی این تنظیمات نیز در کلاس WebApiConfig ثبت و معرفی شده‌اند.
به علاوه در کلاس SmObjectFactory، کار معرفی وهله‌های مورد استفاده و تنظیم طول عمر آن‌ها انجام شده‌است. برای مثال طول عمر IOAuthAuthorizationServerProvider از نوع Singleton است؛ چون تنها یک وهله از AppOAuthProvider در طول عمر برنامه توسط Owin استفاده می‌شود و Owin هربار آن‌را وهله سازی نمی‌کند. همین مساله سبب شده‌است که معرفی وابستگی‌ها در سازنده‌ی کلاس AppOAuthProvider کمی با حالات متداول، متفاوت باشند:
public AppOAuthProvider(
   Func<IUsersService> usersService,
   Func<ITokenStoreService> tokenStoreService,
   ISecurityService securityService,
   IAppJwtConfiguration configuration)
در کلاسی که طول عمر singleton دارد، وابستگی‌های تعریف شده‌ی در سازنده‌ی آن هم تنها یکبار در طول عمر برنامه نمونه سازی می‌شوند. برای رفع این مشکل و transient کردن آن‌ها، می‌توان از Func استفاده کرد. به این ترتیب هر بار ارجاهی به usersService، سبب وهله سازی مجدد آن می‌شود و این مساله برای کار با سرویس‌هایی که قرار است با بانک اطلاعاتی کار کنند ضروری است؛ چون باید طول عمر کوتاهی داشته باشند.
در اینجا سرویس IAppJwtConfiguration  با Func معرفی نشده‌است؛ چون طول عمر تنظیمات خوانده شده‌ی از Web.config نیز Singleton هستند و معرفی آن به همین نحو صحیح است.
اگر علاقمند بودید که بررسی کنید یک سرویس چندبار وهله سازی می‌شود، یک سازنده‌ی خالی را به آن اضافه کنید و سپس یک break point را بر روی آن قرار دهید و برنامه را اجرا و در این حالت چندبار Login کنید.
مطالب
مهارت‌های تزریق وابستگی‌ها در برنامه‌های NET Core. - قسمت دوم - الگوی Service Locator
در قسمت قبل برای دریافت وهله‌ای از سرویس TestService، به صورت ()<serviceProvider.GetService<ITestService عمل کردیم. این روش در اصل الگوی Service Locator نام دارد که جزئیات بیشتری از آن‌را در این قسمت بررسی خواهیم کرد.


قلب سیستم تزریق وابستگی‌های NET Core. اینترفیس IServiceProvider است

IServiceProvider که اساس IoC Container برنامه‌های مبتنی بر NET Core. را تشکیل می‌دهد، در اسمبلی System.ComponentModel و در فضای نام System تعریف شده‌است:
namespace System
{
    public interface IServiceProvider
    {
        object GetService(Type serviceType);
    }
}
زمانیکه به کمک IServiceCollection، تمام اینترفیس‌ها و کلاس‌های خود را به IoC Container معرفی کردیم، مرحله‌ی بعدی، فراهم آوردن روشی برای دریافت وهله‌ای از این سرویس‌ها توسط متد GetService است.
استفاده‌ی مستقیم از اینترفیس IServiceProvider برای دسترسی به وهله‌های سرویس‌ها، اصطلاحا الگوی Service Locator نامیده می‌شود و باید تا حد ممکن از آن پرهیز کرد؛ چون وابستگی مستقیمی از IoC Container را درون کدهای ما قرار می‌دهد و به این ترتیب یک مرحله، نوشتن آزمون‌های واحد برای آن‌را مشکل‌تر می‌کند؛ چون زمان وهله سازی از یک سرویس، دقیقا مشخص نیست به چه وابستگی‌هایی نیاز دارد. به همین جهت همیشه باید با روش تزریق وابستگی‌ها در سازنده‌ی کلاس شروع کرد و اگر به هر دلیلی این روش مهیا نبود و توسط سیستم تزریق وابستگی‌های جاری شناسایی و یا پشتیبانی نمی‌شد (مانند تزریق وابستگی در سازنده‌های Attributes)، آنگاه می‌توان به الگوی Service Locator مراجعه کرد.
برای مثال در اکثر قسمت‌های برنامه‌های ASP.NET Core امکان تزریق وابستگی‌ها در سازنده‌ی کنترلرها، میان افزارها و سایر اجزای آن وجود دارد و در این حالات نیازی به مراجعه‌ی مستقیم به IServiceProvider برای دریافت وهله‌های سرویس‌های مورد نیاز نیست. به عبارتی نگرانی در مورد IServiceProvider بهتر است مشکل IoC Container باشد و نه ما.

در مثال زیر، روش استفاده‌ی از IServiceProvider را جهت انجام تزریق وابستگی‌ها (یا به عبارتی بهتر، روش دسترسی به وهله‌های وابستگی‌ها) را مشاهده می‌کنید:
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

namespace CoreIocServices
{
    public interface IProductService
    {
        void Delete(int id);
    }

    public class ProductService : IProductService
    {
        private readonly ITestService _testService;
        private readonly ILogger<ProductService> _logger;

        public ProductService(IServiceProvider serviceProvider)
        {
            _testService = serviceProvider.GetRequiredService<ITestService>();
            _logger = serviceProvider.GetService<ILogger<ProductService>>() ?? NullLogger<ProductService>.Instance;
        }

        public void Delete(int id)
        {
            _testService.Run();
            _logger.LogInformation($"Deleted a product with id = {id}");
        }
    }
}
این روش یا کار مستقیم با Service locator، هر چند کار می‌کند، اما روشی است که باید تا حد ممکن از آن پرهیز کنید؛ زیرا:
- با نگاه کردن به امضای سازنده‌ی این سرویس مشخص نیست که دقیقا از چه وابستگی‌هایی استفاده می‌کند. اینکار نوشتن آزمون‌های واحد آن‌را مشکل می‌کند.
- این سرویس یک وابستگی اضافه‌تر را به نام IServiceProvider، نیز پیدا کرده‌است که اگر از روش متداول تزریق وابستگی‌ها در سازنده‌ی کلاس استفاده می‌شد، نیازی به ذکر آن نبود.
- پیاده سازی Dispose Pattern در این حالت مشکل‌تر است و در قسمتی دیگر بررسی خواهد شد.



تفاوت‌های بین متدهای ()<GetService<T  و  ()<GetRequiredService<T

از آنجائیکه دیگر از NET 1.0. استفاده نمی‌کنیم، استفاده‌ی از متد GetService با امضایی که در اینترفیس IServiceProvider تعریف شده و strongly typed نیست، بیشتر برای کارهای پویا مناسب است. به همین جهت دو نگارش جنریک از آن در اسمبلی Microsoft.Extensions.DependencyInjection.Abstractions با امضای زیر تعریف شده‌اند که نمونه‌ای از آن‌را در قسمت قبل نیز استفاده کردیم و برای استفاده‌ی از آن‌ها ذکر فضای نام Microsoft.Extensions.DependencyInjection ضروری است:
namespace Microsoft.Extensions.DependencyInjection
{
    public static class ServiceProviderServiceExtensions
    {
        public static T GetRequiredService<T>(this IServiceProvider provider);
        public static T GetService<T>(this IServiceProvider provider);
    }
}
اکنون این سؤال مطرح می‌شود که تفاوت‌های بین این دو متد چیست؟
- متد GetService یک شیء سرویس از نوع T را بازگشت می‌دهد و یا نال؛ اگر سرویسی از نوع T، پیشتر به سیستم معرفی نشده باشد.
- متد GetRequiredService یک شیء سرویس از نوع T را بازگشت می‌دهد و یا اگر سرویسی از نوع T پیشتر به سیستم معرفی نشده باشد، استثنای InvalidOperationException را صادر می‌کند.

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


با توجه به این تفاوت‌ها کدامیک از متدهای GetService و یا GetRequiredService را باید استفاده کرد؟

همانطور که پیشتر نیز در توضیحات الگوی Service locator عنوان شد، هیچکدام! ابتدا با تزریق وابستگی‌های در سازنده‌ی کلاس شروع کنید و اگر تامین این وابستگی، توسط IoC Container جاری پشتیبانی نمی‌شد، آنگاه نیاز به استفاده‌ی از یکی از نگارش‌های متد GetService خواهد بود و متد توصیه شده نیز GetRequiredService است و نه GetService؛ به این دلایل:
- حذف کدهای تکراری: اگر از GetService استفاده کنید، نیاز خواهید داشت پس از تمام فراخوانی‌های آن، بررسی نال بودن آن‌را نیز انجام دهید. برای حذف این نوع کدهای تکراری، بهتر است از همان متد GetRequiredService استفاده کنید که به صورت توکار این بررسی را نیز انجام می‌دهد.
- پشتیبانی از روش Fail Fast و یا همان Defensive programming: اگر بررسی نال بودن GetService را فراموش کنید، در سطرهای بعدی، یافتن علت NullReferenceException صادر شده مشکل‌تر از رسیدگی به InvalidOperationException صادر شده‌ی توسط GetRequiredService خواهد بود که توضیحات دقیقی را در مورد سرویس ثبت نشده ارائه می‌دهد.
- اگر بر روی IoC Container پیش‌فرض NET Core. یک IoC Container دیگر را مانند AutoFac قرار داده‌اید، استفاده‌ی از GetRequiredService، سبب می‌شود تا اینگونه IoC Containerهای ثالث بتوانند اطلاعات مفیدتری را از سرویس‌های ثبت نشده ارائه دهند.

تنها حالتی که استفاده‌ی از روش GetService را نیاز دارد، شرطی کردن ثبت و معرفی کردن سرویس‌ها به IoC Container است؛ اگر سرویسی ثبت شده بود، آنگاه قطعه کدی اجرا شود.
مطالب
استفاده از لوسین برای انجام محاسبات آماری بر روی متون
احتمالا یک سری از کارهای اینفوگرافیک مانند tags cloud و words cloud را دیده‌اید. برای مثال در یک سخنرانی خاص، سخنران بیشتر از چه واژه‌هایی استفاده کرده است و سپس ترسیم درشت‌تر واژه‌هایی با تکرار بیشتر در یک تصویر نهایی. محاسبات آماری این نوع بررسی‌ها را توسط لوسین نیز می‌توان انجام داد که در ادامه به نحوه انجام آن خواهیم پرداخت.

بررسی آماری واژه‌های بکار رفته در شاهنامه

مرحله اول: ایجاد ایندکس

using System;
using System.Collections.Generic;
using System.IO;
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Documents;
using Lucene.Net.Index;
using Lucene.Net.Store;

namespace ShaahnamehAnalysis
{
    public static class CreateIndex
    {
        static readonly Lucene.Net.Util.Version _version = Lucene.Net.Util.Version.LUCENE_CURRENT;

        static HashSet<string> getStopWords()
        {
            var result = new HashSet<string>();
            var stopWords = new[]
            {
                "به",
                "با",
                "از",
                "تا",
                "و",
                "است",
                "هست",
                "هستم",
                "هستیم",
                "هستید",
                "هستند",
                "نیست",
                "نیستم",
                "نیستیم",
                "نیستند",
                "اما",
                "یا",
                "این",
                "آن",
                "اینجا",
                "آنجا",
                "بود",
                "باد",
                "برای",
                "که",
                "دارم",
                "داری",
                "دارد",
                "داریم",
                "دارید",
                "دارند",
                "چند",
                "را",
                "ها",
                "های",
                "می",
                "هم",
                "در",
                "باشم",
                "باشی",
                "باشد",
                "باشیم",
                "باشید",
                "باشند",
                "اگر",
                "مگر",
                "بجز",
                "جز",
                "الا",
                "اینکه",
                "چرا",
                "کی",
                "چه",
                "چطور",
                "چی",
                "چیست",
                "آیا",
                "چنین",
                "اینچنین",
                "نخست",
                "اول",
                "آخر",
                "انتها",
                "صد",
                "هزار",
                "میلیون",
                "ملیون",
                "میلیارد",
                "ملیارد",
                "یکهزار",
                "تریلیون",
                "تریلیارد",
                "میان",
                "بین",
                "زیر",
                "بیش",
                "روی",
                "ضمن",
                "همانا",
                "ای",
                "بعد",
                "پس",
                "قبل",
                "پیش",
                "هیچ",
                "همه",
                "واما",
                "شد",
                "شده",
                "شدم",
                "شدی",
                "شدیم",
                "شدند",
                "یک",
                "یکی",
                "نبود",
                "میکند",
                "میکنم",                
                "میکنیم",
                "میکنید",
                "میکنند",
                "میکنی",
                "طور",
                "اینطور",
                "آنطور",
                "هر",
                "حال",
                "مثل",
                "خواهم",
                "خواهی",
                "خواهد",
                "خواهیم",
                "خواهید",
                "خواهند",
                "داشته",
                "داشت",
                "داشتی",
                "داشتم",
                "داشتیم",
                "داشتید",
                "داشتند",
                "آنکه",
                "مورد",
                "کنید",
                "کنم",
                "کنی",
                "کنند",
                "کنیم",
                "نکنم",
                "نکنی",
                "نکند",
                "نکنیم",
                "نکنید",
                "نکنند",
                "نکن",
                "بگو",
                "نگو",
                "مگو",
                "بنابراین",
                "بدین",
                "من",
                "تو",
                "او",
                "ما",
                "شما",
                "ایشان",
                "ی",
                "ـ",
                "هایی",
                "خیلی",
                "بسیار",
                "1",
                "بر",
                "l",
                "شود",
                "کرد",
                "کرده",
                "نیز",
                "خود",
                "شوند",
                "اند",
                "داد",
                "دهد",
                "گشت",
                "ز",
                "گفت",
                "آمد",
                "اندر",
                "چون",
                "بد",
                "چو",
                "همی",
                "پر",
                "سوی",
                "دو",
                "گر",
                "بی",
                "گرد",
                "زین",
                "کس",
                "زان",
                "جای",
                "آید"
            };

            foreach (var item in stopWords)
                result.Add(item);

            return result;
        }

        public static void CreateShaahnamehIndex(string file = "shaahnameh.txt")
        {
            var directory = FSDirectory.Open(new DirectoryInfo(Environment.CurrentDirectory + "\\LuceneIndex"));
            var analyzer = new StandardAnalyzer(_version, getStopWords());
            using (var writer = new IndexWriter(directory, analyzer, create: true, mfl: IndexWriter.MaxFieldLength.UNLIMITED))
            {
                var section = string.Empty;
                foreach (var line in File.ReadAllLines(file))
                {
                    int result;
                    if (int.TryParse(line, out result))
                    {
                        var postDocument = new Document();
                        postDocument.Add(new Field("Id", result.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED));
                        postDocument.Add(new Field("Body", section, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS));
                        writer.AddDocument(postDocument);
                        section = string.Empty;
                    }
                    else
                        section += line;
                }

                writer.Optimize();
                writer.Commit();
                writer.Close();
                directory.Close();
            }
        }
    }
}

با ایجاد ایندکس‌های لوسین پیشتر در این سایت آشنا شده‌اید . روش کار نیز همانند سابق است. اطلاعات خود را، به هر فرمتی که تهیه شده باید تبدیل به اشیاء Document لوسین کرد. برای مثال در اینجا فقط یک فایل txt داریم که تشکیل شده است از تمام صفحات. به ازای هر صفحه، یک شیء Document تهیه و نوشته خواهد شد. همچنین در تهیه ایندکس از یک سری از واژه‌‌های بسیار متداول مانند «از»، «به»، «اندر»، (stopWords) صرفنظر شده است.


مرحله دوم: ایجاد ابر واژه‌ها

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Lucene.Net.Index;
using Lucene.Net.Store;

namespace ShaahnamehAnalysis
{
    [DebuggerDisplay("{Frequency}, {Text}")]
    public class Tag
    {
        public string Text { set; get; }

        /// <summary>
        /// The frequency of a term is defined as the number of 
        /// documents in which a specific term appears.
        /// </summary>
        public int Frequency { set; get; }
    }

    public static class WordsCloud
    {
        /// <summary>
        /// Create Words Cloud
        /// </summary>
        /// <param name="threshold">every term that appears in more than x Body</param>
        public static IList<Tag> Create(int threshold = 200)
        {
            var path = Environment.CurrentDirectory + "\\LuceneIndex";

            var results = new List<Tag>();
            var field = "Body";

            IndexReader indexReader = IndexReader.Open(FSDirectory.Open(path ), true);

            var termFrequency = indexReader.Terms();
            while (termFrequency.Next())
            {
                if (termFrequency.DocFreq() >= threshold && termFrequency.Term.Field == field)
                {
                    results.Add(new Tag { Text = termFrequency.Term.Text, Frequency = termFrequency.DocFreq() });
                }
            }
            return results.OrderByDescending(x => x.Frequency).ToList();
        }
    }
}

پس از اینکه ایندکس لوسین تهیه شد، می‌توان به مداخل موجود در آن توسط متد indexReader.Terms دسترسی یافت.
نکته جالب آن فراهم بودن DocFreq هر واژه ایندکس شده است (فرکانس تکرار واژه؛ تعداد اشیاء Document ایی که واژه مورد نظر در آن‌ها تکرار شده است). برای مثال در اینجا اگر واژه‌ای 200 بار یا بیشتر در صفحات مختلف شاهنامه تکرار شده باشد، به عنوان یک واژه پر اهمیت انتخاب شده و به ابر واژه‌های نهایی اضافه می‌گردد.


مرحله سوم: استفاده از نتایج

using System;
using System.Diagnostics;
using System.IO;
using System.Linq;

namespace ShaahnamehAnalysis
{
    class Program
    {
        static void Main(string[] args)
        {
            CreateIndex.CreateShaahnamehIndex();
            var wordsCloudList = WordsCloud.Create();

            var data = wordsCloudList.Select(x => x.Text + ", " + x.Frequency)
                                     .Aggregate((s1, s2) => s1 + Environment.NewLine + s2);
            var output = "ShaahnamehAnalysis.txt";
            File.WriteAllText(output, data);
            Process.Start(output);
        }
    }
}

که نتیجه 15 مورد اول آن به صورت زیر است:
واژه |  فرکانس
شاه, 1191
دل, 1088
سر, 1070
کار, 840
لشکر, 801
تخت, 755
روز, 745
ایران, 740
جهان, 724
مرد, 660
دست, 630
تاج, 623
نزدیک, 623
گیتی, 585
راه, 584


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

اشتراک‌ها
سایت BlazorGames

Welcome Players! On this site we make games with ASP.NET Core and Blazor and have some fun while learning how to do it ourselves.

This site will be updated with new examples as they are made. In the meantime, check out the existing games below. 

سایت BlazorGames
مطالب
استفاده از نگارش سوم Google Analytics API در سرویس‌های ویندوز یا برنامه‌های وب
در زمان نگارش این مطلب، آخرین نگارش API مخصوص Google Analytics، نگارش سوم آن است و ... کار کردن با آن دارای مراحل خاصی است که حتما باید رعایت شوند. در غیر اینصورت عملا در یک برنامه‌ی وب یا سرویس ویندوز قابل اجرا نخواهند بود. زیرا در حالت متداول کار با API مخصوص Google Analytics، ابتدا یک صفحه‌ی لاگین به Gmail باز می‌شود که باید به صورت اجباری، مراحل آن را انجام داد تا مشخصات تائید شده‌ی اکانت در حال استفاده‌ی از API، در پوشه‌ی AppData ویندوز برای استفاده‌های بعدی ذخیره شود. این مورد برای یک برنامه‌ی دسکتاپ معمولی مشکل ساز نیست؛ زیرا کاربر برنامه، به سادگی می‌تواند صفحه‌ی مرورگری را که باز شده‌است، دنبال کرده و به اکانت گوگل خود وارد شود. اما این مراحل را نمی‌توان در یک برنامه‌ی وب یا سرویس ویندوز پیگیری کرد، زیرا عموما امکان لاگین از راه دور به سرور و مدیریت صفحه‌ی لاگین به Gmail وجود ندارد یا بهتر است عنوان شود، بی‌معنا است. برای حل این مشکل، گوگل راه حل دیگری را تحت عنوان اکانت‌های سرویس، ارائه داده است که پس از ایجاد آن، یک یک فایل X509 Certificate برای اعتبارسنجی سرویس، در اختیار برنامه نویس قرار می‌گیرد تا بدون نیاز به لاگین دستی به Gmail، بتواند از API گوگل استفاده کند. در ادامه نحوه‌ی فعال سازی این قابلیت و استفاده از آن‌را بررسی خواهیم کرد.


ثبت برنامه‌ی خود در گوگل و انجام تنظیمات آن

اولین کاری که برای استفاده از نگارش سوم Google Analytics API باید صورت گیرد، ثبت برنامه‌ی خود در Google Developer Console است. برای این منظور ابتدا به آدرس ذیل وارد شوید:
سپس بر روی دکمه‌ی Create project کلیک کنید. نام دلخواهی را وارد کرده و در ادامه بر روی دکمه‌ی Create کلیک نمائید تا پروفایل این پروژه ایجاد شود.



تنها نکته‌ی مهم این قسمت، بخاطر سپردن نام پروژه است. زیرا از آن جهت اتصال به API گوگل استفاده خواهد شد.
پس از ایجاد پروژه، به صفحه‌ی آن وارد شوید و از منوی سمت چپ صفحه، گزینه‌ی Credentials را انتخاب کنید. در ادامه در صفحه‌ی باز شده، بر روی دکمه‌ی Create new client id کلیک نمائید.


در صفحه‌ی باز شده، گزینه‌ی Service account را انتخاب کنید. اگر سایر گزینه‌ها را انتخاب نمائید، کاربری که قرار است از API استفاده کند، باید بتواند توسط مرورگر نصب شده‌ی بر روی کامپیوتر اجرا کننده‌ی برنامه، یکبار به گوگل لاگین نماید که این مورد مطلوب برنامه‌های وب و همچنین سرویس‌ها نیست.


در اینجا ابتدا یک فایل مجوز p12 را به صورت خودکار دریافت خواهید کرد و همچنین پس از ایجاد client id، نیاز است، ایمیل آن‌را جایی یادداشت نمائید:


از این ایمیل و همچنین فایل p12 ارائه شده، جهت لاگین به سرور استفاده خواهد شد.
همچنین نیاز است تا به برگه‌ی APIs پروژه‌ی ایجاد شده رجوع کرد و گزینه‌ی Analytics API آن‌را فعال نمود:


تا اینجا کار ثبت و فعال سازی برنامه‌ی خود در گوگل به پایان می‌رسد.


دادن دسترسی به Client ID ثبت شده در برنامه‌ی Google Analytics

پس از اینکه Client ID سرویس خود را ثبت کردید، نیاز است به اکانت Google Analytics خود وارد شوید. سپس در منوی آن، گزینه‌ی Admin را پیدا کرده و به آن قسمت، وارد شوید:


در ادامه به گزینه‌ی User management آن وارد شده و به ایمیل Client ID ایجاد شده در قسمت قبل، دسترسی خواندن و آنالیز را اعطاء کنید:


در صورت عدم رعایت این مساله، کلاینت API، قادر به دسترسی به Google Analytics نخواهد بود.


استفاده از نگارش سوم Google Analytics API در دات نت

قسمت مهم کار، تنظیمات فوق است که در صورت عدم رعایت آن‌ها، شاید نصف روزی را مشغول به دیباگ برنامه شوید. در ادامه نیاز است پیشنیازهای دسترسی به نگارش سوم Google Analytics API را نصب کنیم. برای این منظور، سه بسته‌ی نیوگت ذیل را توسط کنسول پاورشل نیوگت، به برنامه اضافه کنید:
 PM> Install-Package Google.Apis
PM> Install-Package Google.Apis.auth
PM> Install-Package Google.Apis.Analytics.v3
پس از نصب، کلاس GoogleAnalyticsApiV3 زیر، جزئیات دسترسی به Google Analytics API را کپسوله می‌کند:
using System;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using Google.Apis.Analytics.v3;
using Google.Apis.Analytics.v3.Data;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Services;

namespace GoogleAnalyticsAPIv3Tests
{
    public class AnalyticsQueryParameters
    {
        public DateTime Start { set; get; }
        public DateTime End { set; get; } 
        public string Dimensions { set; get; } 
        public string Filters { set; get; }
        public string Metrics { set; get; }
    }

    public class AnalyticsAuthentication
    {
        public Uri SiteUrl { set; get; }
        public string ApplicationName { set; get; }
        public string ServiceAccountEmail { set; get; }
        public string KeyFilePath { set; get; }
        public string KeyFilePassword { set; get; }

        public AnalyticsAuthentication()
        {
            KeyFilePassword = "notasecret";
        }
    }

    public class GoogleAnalyticsApiV3
    {
        public AnalyticsAuthentication Authentication { set; get; }
        public AnalyticsQueryParameters QueryParameters { set; get; }

        public GaData GetData()
        {
            var service = createAnalyticsService();
            var profile = getProfile(service);
            var query = service.Data.Ga.Get("ga:" + profile.Id,
                                QueryParameters.Start.ToString("yyyy-MM-dd"),
                                QueryParameters.End.ToString("yyyy-MM-dd"),
                                QueryParameters.Metrics);
            query.Dimensions = QueryParameters.Dimensions;
            query.Filters = QueryParameters.Filters;
            query.SamplingLevel = DataResource.GaResource.GetRequest.SamplingLevelEnum.HIGHERPRECISION;
            return query.Execute();
        }

        private AnalyticsService createAnalyticsService()
        {
            var certificate = new X509Certificate2(Authentication.KeyFilePath, Authentication.KeyFilePassword, X509KeyStorageFlags.Exportable);
            var credential = new ServiceAccountCredential(
                new ServiceAccountCredential.Initializer(Authentication.ServiceAccountEmail)
                {
                    Scopes = new[] { AnalyticsService.Scope.AnalyticsReadonly }
                }.FromCertificate(certificate));

            return new AnalyticsService(new BaseClientService.Initializer
            {
                HttpClientInitializer = credential,
                ApplicationName = Authentication.ApplicationName
            });
        }

        private Profile getProfile(AnalyticsService service)
        {
            var accountListRequest = service.Management.Accounts.List();
            var accountList = accountListRequest.Execute();
            var site = Authentication.SiteUrl.Host.ToLowerInvariant();
            var account = accountList.Items.FirstOrDefault(x => x.Name.ToLowerInvariant().Contains(site));
            var webPropertyListRequest = service.Management.Webproperties.List(account.Id);
            var webPropertyList = webPropertyListRequest.Execute();
            var sitePropertyList = webPropertyList.Items.FirstOrDefault(a => a.Name.ToLowerInvariant().Contains(site));
            var profileListRequest = service.Management.Profiles.List(account.Id, sitePropertyList.Id);
            var profileList = profileListRequest.Execute();
            return profileList.Items.FirstOrDefault(a => a.Name.ToLowerInvariant().Contains(site));
        }
    }
}
در اینجا در ابتدا بر اساس فایل p12 ایی که از گوگل دریافت شد، یک X509Certificate2 ایجاد می‌شود. پسورد این فایل مساوی است با ثابت notasecret که در همان زمان تولید اکانت سرویس در گوگل، لحظه‌ای در صفحه نمایش داده خواهد شد. به کمک آن و همچنین ServiceAccountEmail ایمیلی که پیشتر به آن اشاره شد، می‌توان به AnalyticsService لاگین کرد. به این ترتیب به صورت خودکار می‌توان شماره پروفایل اکانت سایت خود را یافت و از آن در حین فراخوانی service.Data.Ga.Get استفاده کرد.


مثالی از نحوه استفاده از کلاس GoogleAnalyticsApiV3

در ادامه یک برنامه‌ی کنسول را ملاحظه می‌کنید که از کلاس GoogleAnalyticsApiV3 استفاده می‌کند:
using System;
using System.Collections.Generic;
using System.Linq;

namespace GoogleAnalyticsAPIv3Tests
{
    class Program
    {

        static void Main(string[] args)
        {
            var statistics = new GoogleAnalyticsApiV3
            {
                Authentication = new AnalyticsAuthentication
                {
                    ApplicationName = "My Project",
                    KeyFilePath = "811e1d9976cd516b55-privatekey.p12",
                    ServiceAccountEmail = "10152bng4j3mq@developer.gserviceaccount.com",
                    SiteUrl = new Uri("https://www.dntips.ir/")
                },
                QueryParameters = new AnalyticsQueryParameters
                {
                    Start = DateTime.Now.AddDays(-7),
                    End = DateTime.Now,
                    Dimensions = "ga:date",
                    Filters = null,
                    Metrics = "ga:users,ga:sessions,ga:pageviews"
                }
            }.GetData();


            foreach (var result in statistics.TotalsForAllResults)
            {
                Console.WriteLine(result.Key + " -> total:" + result.Value);
            }
            Console.WriteLine();

            foreach (var row in statistics.ColumnHeaders)
            {
                Console.Write(row.Name + "\t");
            }
            Console.WriteLine();

            foreach (var row in statistics.Rows)
            {
                var rowItems = (List<string>)row;
                Console.WriteLine(rowItems.Aggregate((s1, s2) => s1 + "\t" + s2));
            }

            Console.ReadLine();
        }
    }
}

چند نکته
ApplicationName همان نام پروژه‌ای است که ابتدای کار، در گوگل ایجاد کردیم.
KeyFilePath مسیر فایل مجوز p12 ایی است که گوگل در حین ایجاد اکانت سرویس، در اختیار ما قرار می‌دهد.
ServiceAccountEmail آدرس ایمیل اکانت سرویس است که در قسمت ادمین Google Analytics به آن دسترسی دادیم.
SiteUrl آدرس سایت شما است که هم اکنون در Google Analytics دارای یک اکانت و پروفایل ثبت شده‌است.
توسط AnalyticsQueryParameters می‌توان نحوه‌ی کوئری گرفتن از Google Analytics را مشخص کرد. تاریخ شروع و پایان گزارش گیری در آن مشخص هستند. در مورد پارامترهایی مانند Dimensions و Metrics بهتر است به مرجع کامل آن در گوگل مراجعه نمائید:
Dimensions & Metrics Reference

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


برای مطالعه بیشتر
Using Google APIs in Windows Store Apps
How To Use Google Analytics From C# With OAuth
Google Analytic’s API v3 with C#
.NET Library for Accessing and Querying Google Analytics V3 via Service Account
Google OAuth2 C#
اشتراک‌ها
دوره 8 ساعته Microservices در دات نت

Introduction to .NET Microservices (.NET 8)

In this Introduction course, we will learn Microservices with .NET 8 (MVC).
Microservices is an upcoming technology, where it is very easy to scale and break down large project in simple and manageable services.
In this course we will build multiple services and see how they function together by communicating in synchronous and asynchronous manner.

⭐️ Course Contents ⭐️
⌨️ (0:00:01) Section 1 -  Welcome & Getting Started
⌨️ (0:28:15) Section 2 -  Coupon API - Fundamentals
⌨️ (1:15:54) Section 3 -  Coupon API - CRUD
⌨️ (2:21:24) Section 4 -  Auth API
⌨️ (3:20:08) Section 5 -  Consuming Auth API
⌨️ (4:26:53) Section 6 - Product API
⌨️ (4:57:59) Section 7 - Home and Details Page
⌨️ (5:09:35) Section 8 - Shopping Cart
⌨️ (6:08:04) Section 9 - Shopping Cart in Web Project
⌨️ (6:58:06) Section 10 - Service Bus
⌨️ (7:23:42) Section 11 - Email API - Service Bus
⌨️ (7:54:11) What's Next? 

دوره 8 ساعته Microservices در دات نت
اشتراک‌ها
مقاله ای کامل درباره استفاده از MVC و jQuery و JSON و paging و mapRoute
How to make CRUD with good performance in MVC
How to use jQuery dialog instead of JavaScript confirm or alert
How to make paging in an MVC list
How to make "show more" link using jQuery in MVC
How to use attributes with link
How to make a AJAX call in jQuery
How to use the Form collection in MVC
How to delete multiple records at one shot
How to use partial action in MVC
How to use JSON format in an MVC application
How to fill a master-detail combobox
How to use the jQuery datepicker
How to upload an image in MVC with a jQuery dialog
How to create a table row at client side
How to customize a maproute in Global.asax 
مقاله ای کامل درباره استفاده از MVC و  jQuery و  JSON و  paging و  mapRoute