نظرات مطالب
مدیریت فایلهای یک پروژه نرم افزاری با استفاده از Subversion
سلام!
دستت درد نکنه بابت این کتابچه. البته من هنوز نخوندمش (فعلاً داره دانلود می‌شه... اینترنت ایران :( ) باید بگم مدت‌ها دنبال یه راهنما برا همین می‌گشتم! خیلی اتفاقی این وبلاگو دیدم. دستت درد نکنه.

موفق باشی.
مطالب
لیستی از برگه‌های مرجع

اخیرا سایت dzone ، تعدادی برگه مرجع رایگان (یا همان برگه تقلب سابق!) را منتشر کرده است.
مشاهده صفحه مربوطه

فقط باید عضو شد و به ازای هر فایل pdf یک ایمیل دریافت کرد! که کمی دشوار و وقت گیر است. لیست فایل‌های pdf آن‌را از اینجا می‌توانید دریافت کنید.


موفق باشید
مطالب
Mirror برای ویدیوهای رایگان جی کوئری

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

موفق باشید!

پاسخ به بازخورد‌های پروژه‌ها
خطای Payment Is Not Valid
پیغام Payment is not valid فقط در صورتی داده میشه که یک پرداخت، مقداری غیر از Requested داشته باشه. طبق نتیجه ای که اینجا نوشتید پس همه چیز باید درست باشه.
شما آیا پیغام موفق در تست اول (پرداخت) و پیغام عدم موفقیت در تست دوم (انصراف) رو هم مشاهده کردید؟
نظرات مطالب
ساختار پروژه های Angular
زمانی که تعداد فایل‌ها و دایرکتوری‌ها در پروژه زیاد می‌شود(البته این جزء جدانشدنی پروژه‌های مقیاس بزرگ است) برای جلوگیری از لود یک باره کنترلرها و دایرکتیوها، بهتر از lazy loading برای لود فایل‌های مورد نیاز استفاده شود.  متاسفانه Angular به صورت رسمی از lazy loading پشتیبانی نمی‌کند اما با کمی تغییر در ساختار و استفاده از کتابخانه‌های جانبی مثل requireJs یا ScriptJs می‌توان به این مهم دست یافت.
(با عنوان این مطلب که قصد داشتم این مورد را طی یک پست جداگانه بررسی کنم)
برای مثال:
ابتدا ماژول app خود را به این شکل تنظیم کنید:
(function()
{
    var app = angular.module('app', []);

    app.config(function($routeProvider, $controllerProvider, $compileProvider, $filterProvider, $provide)
    {
        app.controllerProvider = $controllerProvider;
        app.compileProvider    = $compileProvider;
        app.routeProvider      = $routeProvider;
        app.filterProvider     = $filterProvider;
        app.provide            = $provide;

    });
})();

با استفاده از سرویس controllerProvider$ می‌توان چرخه ساخت کنترلر را به دست گرفت. هم چنین سرویس compileProvider$ برای نمونه سازی دایرکتیو‌ها و filterProvider$ برای فیلتر‌ها استفاده می‌شوند.
ساخت کنترلر‌ها و دایرکتیو‌ها نیز به صورت زیر انجام خواهد شد:
angular.module('app').controllerProvider.resgister('SomeLazyController', function($scope)
{
    $scope.key = '...';
});
و هم چنین یک نمونه از ساخت directive
$compileProvider.directive('SomeLazyDirective', function()
{
    return {
        restrict: 'A',
        templateUrl: 'templates/some-lazy-directive.html'
    }
})

فقط کافیست در هنگام پیاده سازی routing (که در این مقاله شرح داده شده است) نوع بارگذاری کنترلرها و دایرکتیو و ... را به صورت lazy انجام دهید :
$routeProvider.when('/about', {templateUrl:'views/about.html', resolve:{deps:function($q, $rootScope)
{
    var deferred = $q.defer();
    var dependencies =
    [
        'controllers/AboutViewController.js',
        'directives/some-directive.js'
    ];
 
    //*نکته اول 
    $script(dependencies, function()
    {
        // *نکته دوم
        $rootScope.$apply(function()
        {
            deferred.resolve();
        });
    });
 
    return deferred.promise;
}}})
*نکته اول: تمام وابستگی‌ها توسط scriptJs مدیریت می‌شوند.
*نکته دوم: تمام وابستگی‌ها مروبط به این scope بعد از فراخوانی تابع deffered.resolved بارگذاری خواهند شد.
نقطه شروع برنامه نیز به صورت زیر است:
$script(['appModule.js'], function()
{
    angular.bootstrap(document, ['app'])
});
angular.bootstrap
مطالب
آموزش Prism #3
در پست‌های قبلی با Prism و روش استفاده از آن آشنا شدیم (قسمت اول) و (قسمت دوم). در این پست با استفاده از Mef قصد ایجاد یک پروژه Silverlight رو به صورت ماژولار داریم. مثال پیاده سازی شده در پست قبلی را در این پست به صورت دیگر پیاده سازی خواهیم کرد.
تفاوت‌های پیاده سازی مثال پست قبلی با این پست:
  • در مثال قبل پروژه به صورت Desktop و با WPF پیاده سازی شده بود ولی در این مثال با Silverlight می‌باشد؛
  • در مثال قبل از UnityBootstrapper استفاده شده بود ولی در این مثال از MefBootstrapper؛
  • در مثال قبل هر View در یک ماژول قرار داشت ولی در این مثال هر دو View را در یک ماژول قرار دادم؛
  • در مثال قبل از Prism Libary 2.x استفاده شده بود ولی در این مثال از PrismLibrary 4.x؛
  • و...

نکته : برای فهم بهتر مفاهیم، آشنایی اولیه با MEF و مفاهیمی نظیر Export و Import و AggregateCatalog و AssemblyCatalog نیاز است. در صورتی که با این مطالب آشنایی ندارید می‌توانید از (^) شروع کنید.


برای شروع یک پروژه Silverlight ایجاد کنید. بعد از اضافه شدن دو پروژه Silverlight و Web، یک Silverlight Class Library جدید بسازید.
ابتدا یک Page ایجاد کنید و کد‌های زیر را در آن کپی کنید.
<UserControl 
             x:Class="Module1.Module1View1"
             xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk" 
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" FlowDirection="RightToLeft" FontFamily="Tahoma">
    <StackPanel>
        <sdk:DataGrid Height="100">
            <sdk:DataGrid.Columns>
                <sdk:DataGridTextColumn Header="کد" Width="50"  />
                <sdk:DataGridTextColumn Header="عنوان" Width="200" />
                <sdk:DataGridTextColumn Header="نویسنده" Width="150"  />
            </sdk:DataGrid.Columns>
        </sdk:DataGrid>
        <Button x:Name="NextViewButton"
                Width="150"
                Height="25"
                Foreground="Red"
                Background="Blue"
                Content="لیست طبقه بندی ها" />
    </StackPanel>
</UserControl>
بر روی Page مربوطه راست کلیک کنید و گزینه ViewCode را انتخاب کنید و کد‌های زیر را در آن کپی کنید.
 [Export(typeof(Module1View1))]
    public partial class Module1View1 : UserControl
    {
        [Import]
        public IRegionManager TheRegionManager { private get; set; }

        public Module1View1()
        {
            InitializeComponent();

            NextViewButton.Click += NextViewButton_Click;
        }

        void NextViewButton_Click(object sender, RoutedEventArgs e)
        {
            TheRegionManager.RequestNavigate
            (
                "MyRegion1",
                new Uri("Module1View2", UriKind.Relative),
                a => { }
            );
        }
    }
ابتدا خود این View باید حتما Export شود. در رویداد کلیک با استفاده از متد RequestNavigate می‌توانیم به View مورد نظر برای نمایش در Shell اشاره کنیم و این View در Region نمایش داده می‌شود. به دلیل اینکه در این کلاس به RegionManager نیاز داریم از ImportAttribute استفاده کردیم. این بدین معنی است که کلاس Module1View1 وابستگی مستقیم به IRegionManager دارد.

حال یک Page دیگر برای طبقه بندی کتاب‌ها ایجاد کنید و کدهای زیر را در آن کپی کنید.
<UserControl 
             xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk"  x:Class="Module1.Module1View2"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" FlowDirection="RightToLeft" FontFamily="Tahoma">
    <StackPanel>
        <sdk:DataGrid Height="100">
            <sdk:DataGrid.Columns>
                <sdk:DataGridTextColumn Header="کد" Width="150"/>
                <sdk:DataGridTextColumn Header="عنوان" Width="150"/>                
            </sdk:DataGrid.Columns>
        </sdk:DataGrid>
        <Button x:Name="NextViewButton"
                Width="150"
                Height="25"
                Foreground="Green"
                Background="Yellow"
                Content="لیست کتاب ها" />
    </StackPanel>
</UserControl>
در Code Behind این Page نیز کد‌های زیر را قرار دهید.
using Microsoft.Practices.Prism.Regions;
using System;
using System.ComponentModel.Composition;
using System.Windows;
using System.Windows.Controls;

namespace Module1
{
    [Export]
    public partial class Module1View2 : UserControl
    {
        IRegion _region1;

        [ImportingConstructor]
        public Module1View2( [Import] IRegionManager regionManager )
        {
            InitializeComponent();
         
            ViewModel viewModel = new ViewModel();
            DataContext = viewModel;

            viewModel.ShouldNavigateFromCurrentViewEvent += () => { return true; };

            _region1 = regionManager.Regions["MyRegion1"];

            NextViewButton.Click += NextViewButton_Click;
        }

        void NextViewButton_Click( object sender, RoutedEventArgs e )
        {
            _region1.RequestNavigate
            (
                new Uri( "Module1View1", UriKind.Relative ),
                a => { }
            );
        }
    }
}
در این ماژول برای اینکه بتوانیم حالت گردشی در فراخوانی ماژول‌ها را داشته باشیم ابتدا DataContext این کلاس را برابر با ViewModel ساخته شده قرار دادیم. با استفاده از رویداد ShoudlNavigateFromCurrentViewEvent که در کلاس ViewModel وجود دارد تعیین می‌کنیم که آیا باید از این View به View قبلی برگشت داشته باشیم یا نه. در صورتی که مقدار false برگشت داده شود خواهید دید که امکان فراخوانی View1 از View2 امکان پذیر نیست. در رویداد کلیک نیز همانند Page قبلی با استفاده از RegionManager و متد RequestNavigate به View مورد نظر راهبری کرده ایم.
نکته: اگر یک کلاس، سازنده با پارامتر داشته باشد باید با استفاده از ImportingConstructor حتما سازنده مورد نظر را هنگام وهله سازی مشخص کنیم در غیر این صورت با Exception مواجه خواهید شد.
حال قصد ایجاد کلاس ViewModel بالا را داریم:
using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.ComponentModel.Composition;
using Microsoft.Practices.Prism.Regions;

namespace Module1
{
    public class ViewModel : IConfirmNavigationRequest
    {       
        public event Func<bool> ShouldNavigateFromCurrentViewEvent;      

        public bool IsNavigationTarget( NavigationContext navigationContext )
        {
            return true;
        }

        public void OnNavigatedTo( NavigationContext navigationContext )
        {

        }

        public void OnNavigatedFrom( NavigationContext navigationContext )
        {

        }         

        public void ConfirmNavigationRequest( NavigationContext navigationContext, Action<bool> continuationCallback )
        {
            bool shouldNavigateFromCurrentViewFlag = false;

            if ( ShouldNavigateFromCurrentViewEvent != null )
                shouldNavigateFromCurrentViewFlag = ShouldNavigateFromCurrentViewEvent();

            continuationCallback( shouldNavigateFromCurrentViewFlag );
        }    
    }
}
توضیح متد‌های بالا:
  • IsNavigateTarget : برای تعیین اینکه آیا کلاس پیاده سازی کننده اینترفیس، می‌تواند عملیات راهبری را مدیریت کند یا نه.
  • OnNavigateTo : زمانی عملیات راهبری وارد View شود(بهتره بگم View مورد نظر در Region صفحه لود شود) این متد فراخوانی می‌شود.
  • OnNavigateFrom : زمانی که راهبری از این View خارج می‌شود (View از حالت لود خارج می‌شود) این متد فراخوانی خواهد شد.
  • ConfirmNavigationRequest : برای تایید عملیات راهبری توسط کلاس پیاده سازی کننده اینترفیس استفاده می‌شود. 
حال یک کلاس برای پیاده سازی و مدیریت ماژول می‌سازیم.
using Microsoft.Practices.Prism.MefExtensions.Modularity;
using Microsoft.Practices.Prism.Modularity;
using Microsoft.Practices.Prism.Regions;
using System.ComponentModel.Composition;

namespace Module1
{
    [ModuleExport(typeof(Module1Impl))]
    public class Module1Impl : IModule
    {       
        [Import]
        public IRegionManager TheRegionManager { private get; set; }     

        public void Initialize()
        {
            TheRegionManager.RegisterViewWithRegion("MyRegion1", typeof(Module1View1));

            TheRegionManager.RegisterViewWithRegion("MyRegion1", typeof(Module1View2));
        }
    }
}
همان طور که مشاهده می‌کنید از ModuleExportAttribute برای شناسایی ماژول توسط MefBootstrapper استفاده کردیم و نوع آن را Module1Imp1 قرار دادیم.  ImportAttribute استفاده شده در این کلاس و خاصیت TheRegionManager برای این است که در هنگام ساخت Instance از این کلاس IRegionManager موجود در Container باید در اختیار این کلاس قرار گیرد(نشان دهنده وابستگی مستقیم این کلاس با IRegionManager است). روش دیگر این است که در سازنده این کلاس هم این اینترفیس را تزریق کنیم.
در متد Initialize برای RegionManager دو View ساخته شده را رجیستر کردیم. این کار باید به تعداد View‌های موجود در ماژول انجام شود.
Shell
در پروژه اصلی بک Page به نام Shell ایجاد کنید و کد‌های زیر را در آن کپی کنید.
<UserControl x:Class="NavigationViaViewModel.Shell"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:prism="http://www.codeplex.com/prism" FlowDirection="RightToLeft" FontFamily="Tahoma">

    <Grid x:Name="LayoutRoot"
          Background="White">
        <TextBlock Text="لیست کتاب‌ها به همراه طبقه بندی آن ها"
                   FontSize="19"
                   Foreground="Black"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Top" />
        <ContentControl HorizontalAlignment="Center"
                        VerticalAlignment="Center"
                        prism:RegionManager.RegionName="MyRegion1" />
    </Grid>
</UserControl>
همانند مثال قبلی یک ContentControl داریم و به وسیله RegionName که یک AttachedProperty است یک Region به نام MyRegion1 ایجاد کردیم. تمام ماژول‌های این مثال در این محدوده نمایش داده خواهند شد.
Bootstrapper
حال نیاز به یک Bootstrapper داریم. برای این کار یک کلاس به نام TheBootstrapper بسازید:
using Microsoft.Practices.Prism.MefExtensions;
using Microsoft.Practices.Prism.Modularity;
using System.ComponentModel.Composition.Hosting;
using System.Windows;

namespace NavigationViaViewModel
{
    public class TheBootstrapper : MefBootstrapper
    {
        protected override void InitializeShell()
        {
            base.InitializeShell();

            Application.Current.RootVisual = (UIElement)Shell;
        }

        protected override DependencyObject CreateShell()
        {
            return Container.GetExportedValue<Shell>();
        }

        protected override void ConfigureAggregateCatalog()
        {
            base.ConfigureAggregateCatalog();        
            AggregateCatalog.Catalogs.Add(new AssemblyCatalog(this.GetType().Assembly));
        }

        protected override IModuleCatalog CreateModuleCatalog()
        {
            ModuleCatalog moduleCatalog = new ModuleCatalog();
    
            moduleCatalog.AddModule
            (
                new ModuleInfo
                {
                    InitializationMode = InitializationMode.WhenAvailable,
                    Ref = "Module1.xap",
                    ModuleName = "Module1Impl",
                    ModuleType = "Module1.Module1Impl, Module1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
                }
            );

            return moduleCatalog;
        }
    }
}
متد CreateShell اولین متد در این کلاس است که اجرا خواهد شد. بعد از متد CreateShell، متد InitializeShell اجرا خواهد شد. خاصیت Shell دقیقا به مقدار برگشتی متد CreateShell اشاره خواهد کرد. در متد InitializeShell  مقدار خاصیت Shell به RootVisual این پروژه اشاره می‌کند(مانند MainWindow در کلاس Application پروژه‌های WPF).
متد ConfigureAggregateCatalog برای مدیریت کاتالوگ‌ها و ماژول‌ها که هر کدام در یک اسمبلی جدا وجود خواهند شد استفاده می‌شود. در این متد من از AssemblyCatalog استفاده کردم. AssemblyCatalog  تمام کلاس هایی که ExportAttribute را به همراه دارند شناسایی می‌کند و آن‌ها را در Container نگهداری خواهد کرد(^). مانند یک ServiceLocator در Microsoft unity Service Locator(^) .
متد آخر به نام CreateModuleCatalog است و باید در آن تمام ماژول‌های برنامه را به کلاس ModuleCatalog اضافه کنیم. در مثال پست قبلی به دلیل استفاده از UnityBootstrapper باید این کار را از طریق BuildEvent ‌ها مدیریت می‌کردیم ولی در این جا Mef به راحتی این کار را انجام خواهد داد.
تغییرات زیر را در فایل App.Xaml قرار دهید و پروژه را اجرا کنید.
public partial class App : Application
 {
        public App()
        {
            this.Startup += this.Application_Startup;                 
            InitializeComponent();
        }

        private void Application_Startup(object sender, StartupEventArgs e)
        {
           var bootstrapper = new TheBootstrapper();
            bootstrapper.Run(); 
        }
}
با کلیک بر روی ماژول عملیات راهبری برای ماژول انجام خواهد شد.

دریافت سورس پروژه
ادامه دارد..
مطالب
غیرمعتبر کردن توکن و یا کوکی سرقت شده در برنامه‌های مبتنی بر ASP.NET Core
چند روز قبل، یکی از کانال‌های فنی معروف یوتیوب با بیش از 15 میلیون مشترک، هک و پاک شد! که داستان آن‌را در اینجا می‌توانید پیگیری کنید. در این هک، مهاجم در سعی اول، پیشنهاد پشتیبانی مالی از شبکه را داده و در ایمیل دوم، پس از جلب اعتماد اولیه، یک فایل به ظاهر PDF مفاد قرارداد را ارسال کرده که با کلیک بر روی آن، تمام کوکی‌های یوتیوب مالک کانال، سرقت و مورد سوء استفاده قرار گرفته! در یک چنین حالتی، مهم نیست که شما اعتبارسنجی دو مرحله‌ای را فعال کرده‌اید و یا از بهترین روش‌های رمزنگاری برای امن کردن اطلاعات کوکی و یا توکن خود استفاده کرده‌اید، همینقدر که اصل محتوای کوکی و یا توکن شما در اختیار شخص دیگری قرار گیرد، می‌تواند بدون نیاز به لاگین و دانستن کلمه‌ی عبور شما، بجای شما وارد سیستم شده و تغییرات دلخواهی را اعمال کند!
بنابراین سؤال اینجاست که ما (توسعه دهندگان) چگونه می‌توانیم یک چنین حملاتی را مشکل‌تر کنیم؟ در این مطلب روشی را در جهت سعی در غیرمعتبر کردن توکن‌ها و یا کوکی‌های سرقت شده، در برنامه‌های مبتنی بر ASP.NET Core بررسی خواهیم کرد.


توسعه‌ی یک سرویس تشخیص مرورگر و سیستم عامل شخص وارد شده‌ی به سیستم

یکی از روش‌های غیرممکن کردن یک چنین حملاتی، درج مشخصات سیستم عامل و مرورگر شخص وارد شده‌ی به سیستم، در کوکی و همچنین توکن صادر شده‌ی حاصل از اعتبارسنجی موفق است. سپس زمانیکه قرار است از اطلاعات این کوکی و یا توکن در برنامه استفاده شود، این اطلاعات را با اطلاعات درخواست جاری کاربر مقایسه کرده و در صورت عدم تطابق، درخواست او را برگشت می‌زنیم. برای مثال اگر عملیات لاگین، در ویندوز انجام شده و اکنون توکن و یا کوکی حاصل، در سیستم عامل اندروید در حاصل استفاده‌است، یعنی ... این عملیات مشکوک است و باید خاتمه یابد و کاربر باید مجبور به لاگین مجدد شود و نه اعتبارسنجی خودکار بدون زحمت!
برای این منظور می‌توان از کتابخانه‌ی UA-Parser استفاده کرد و توسط آن سرویس زیر را توسعه داد:
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
using UAParser;

namespace ASPNETCore2JwtAuthentication.Services;

/// <summary>
///     To invalidate an old user's token from a new device
/// </summary>
public class DeviceDetectionService : IDeviceDetectionService
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly ISecurityService _securityService;

    public DeviceDetectionService(ISecurityService securityService, IHttpContextAccessor httpContextAccessor)
    {
        _securityService = securityService ?? throw new ArgumentNullException(nameof(securityService));
        _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
    }

    public string GetCurrentRequestDeviceDetails() => GetDeviceDetails(_httpContextAccessor.HttpContext);

    public string GetDeviceDetails(HttpContext context)
    {
        var ua = GetUserAgent(context);
        if (ua is null)
        {
            return "unknown";
        }

        var client = Parser.GetDefault().Parse(ua);
        var deviceInfo = client.Device.Family;
        var browserInfo = $"{client.UA.Family}, {client.UA.Major}.{client.UA.Minor}";
        var osInfo = $"{client.OS.Family}, {client.OS.Major}.{client.OS.Minor}";
        //TODO: Add the user's IP address here, if it's a banking system.
        return $"{deviceInfo}, {browserInfo}, {osInfo}";
    }

    public string GetDeviceDetailsHash(HttpContext context) =>
        _securityService.GetSha256Hash(GetDeviceDetails(context));

    public string GetCurrentRequestDeviceDetailsHash() => GetDeviceDetailsHash(_httpContextAccessor.HttpContext);

    public string GetCurrentUserTokenDeviceDetailsHash() =>
        GetUserTokenDeviceDetailsHash(_httpContextAccessor.HttpContext?.User.Identity as ClaimsIdentity);

    public string GetUserTokenDeviceDetailsHash(ClaimsIdentity claimsIdentity)
    {
        if (claimsIdentity?.Claims == null || !claimsIdentity.Claims.Any())
        {
            return null;
        }

        return claimsIdentity.FindFirst(ClaimTypes.System)?.Value;
    }

    public bool HasCurrentUserTokenValidDeviceDetails() =>
        HasUserTokenValidDeviceDetails(_httpContextAccessor.HttpContext?.User.Identity as ClaimsIdentity);

    public bool HasUserTokenValidDeviceDetails(ClaimsIdentity claimsIdentity) =>
        string.Equals(GetCurrentRequestDeviceDetailsHash(), GetUserTokenDeviceDetailsHash(claimsIdentity),
                      StringComparison.Ordinal);

    private static string GetUserAgent(HttpContext context)
    {
        if (context is null)
        {
            return null;
        }

        return context.Request.Headers.TryGetValue(HeaderNames.UserAgent, out var userAgent)
                   ? userAgent.ToString()
                   : null;
    }
}
توضیحات:
اصل کار این سرویس در متد زیر رخ می‌دهد:
    public string GetDeviceDetails(HttpContext context)
    {
        var ua = GetUserAgent(context);
        if (ua is null)
        {
            return "unknown";
        }

        var client = Parser.GetDefault().Parse(ua);
        var deviceInfo = client.Device.Family;
        var browserInfo = $"{client.UA.Family}, {client.UA.Major}.{client.UA.Minor}";
        var osInfo = $"{client.OS.Family}, {client.OS.Major}.{client.OS.Minor}";
        //TODO: Add the user's IP address here, if it's a banking system.
        return $"{deviceInfo}, {browserInfo}, {osInfo}";
    }
در اینجا با استفاده از کتابخانه‌ی UA-Parser، سعی می‌کنیم تا جزئیات مرورگر و سیستم عامل شخص را تهیه کنیم. سپس در قسمت دیگری از این سرویس، این اطلاعات را هش می‌کنیم. از این جهت که هم حجم آن کاهش یابد و بی‌جهت کوکی و یا توکن ما را حجیم نکند و هم بررسی محتوای آن جهت شبیه سازی آن، غیرممکن شود. هر مشخصات دریافتی در حین لاگین، همواره یک هش مشخص و یکتا را دارد. به همین جهت متدهای هش کردن اطلاعات را هم در اینجا مشاهده می‌کنید. به علاوه‌ی متد HasUserTokenValidDeviceDetails که کار آن، دریافت Claim مرتبط با این اطلاعات، از کوکی و یا توکن جاری و مقایسه‌ی آن با اطلاعات Http Request جاری است. اگر این دو یکی نبودند، یعنی احتمال سوء استفاده‌ی از اطلاعات شخص، وجود دارد.


اضافه کردن اطلاعات مشخصات دستگاه کاربر به کوکی و یا توکن او

همانطور که عنوان شد، در متد HasUserTokenValidDeviceDetails، ابتدا مشخصات دستگاه موجود در کوکی و یا توکن دریافتی، استخراج می‌شود. به همین جهت نیاز است این مشخصات را دقیقا در حین لاگین موفق، به صورت یک Claim جدید، برای مثال از نوع ClaimTypes.System به مجموعه‌ی Claims کاربر اضافه کرد:
new(ClaimTypes.System, _deviceDetectionService.GetCurrentRequestDeviceDetailsHash(),
ClaimValueTypes.String, _configuration.Value.Issuer),


یکپارچه کردن DeviceDetectionService با اعتبارسنج‌های کوکی‌ها و توکن‌ها

پس از افزودن مشخصات سیستم کاربر وارد شده‌ی به سیستم، به صورت یک Claim جدید به توکن‌ها، روش اعتبارسنجی اطلاعات موجود در توکن رسیده، در رخ‌داد گردان OnTokenValidated است که امکان دسترسی به HttpContext و محتوای توکن را میسر می‌کند:
.AddJwtBearer(cfg =>
{
      cfg.Events = new JwtBearerEvents
      {
           OnTokenValidated = context =>
           {
               var tokenValidatorService = context.HttpContext.RequestServices.GetRequiredService<ITokenValidatorService>();
              return tokenValidatorService.ValidateAsync(context);
           },
       };
  });
و یا اگر از کوکی‌ها استفاده می‌کنید، معادل آن به صورت زیر است:
.AddCookie(options =>
{
    options.Events = new CookieAuthenticationEvents
    {
       OnValidatePrincipal = context =>
       {
         var cookieValidatorService = context.HttpContext.RequestServices.GetRequiredService<ICookieValidatorService>();
         return cookieValidatorService.ValidateAsync(context);
       }
    };
});

در کل تمام تغییرات مورد نیاز مرتبط را جهت یک برنامه‌ی تولید کننده‌ی JWT در اینجا و برای یک برنامه‌ی مبتنی بر کوکی‌ها در اینجا می‌توانید مشاهده کنید.
مطالب
پشتیبانی توکار از انجام کارهای پس‌زمینه در ASP.NET Core 2x
از زمان ASP.NET Core 2.1، قابلیت جدیدی به نام Generic Host، به آن اضافه شده‌است که از آن می‌توان برای انجام کارهای متداول پس زمینه، مانند ارسال ایمیل‌های خبرنامه‌ی یک برنامه، تهیه فایل‌های پشتیبان و غیره استفاده کرد.


Generic Host چیست؟

Generic Host یکی از ویژگی‌های جدید ASP.NET Core 2.1 است. هدف آن جداسازی HTTP pipeline برنامه، از Web Host API آن است. یکی از مزایای این‌کار، امکان استفاده‌ی از آن نه فقط در پروژه‌های وب، بلکه در پروژه‌های کنسول نیز می‌باشد. به این ترتیب می‌توان کارهای غیر HTTP را از برنامه‌ی وب مجزا کرد تا به کارآیی بیشتری رسید و برای این منظور اینترفیس IHostedService را که در فضای نام Microsoft.Extensions.Hosting قرار دارد، برای ثبت کارهای پس‌زمینه‌ی خارج از اعمال web host جاری، ارائه داده‌اند:
namespace Microsoft.Extensions.Hosting
{
    public interface IHostedService
    {
        Task StartAsync(CancellationToken cancellationToken);
        Task StopAsync(CancellationToken cancellationToken);
    }
}
بنابراین برای ایجاد یک HostedService، نیاز است سرویس کارهای پس‌زمینه‌ی ما، اینترفیس IHostedService را پیاده سازی کند. متد StartAsync آن جائی‌است که تنها یکبار پس از آغاز برنامه اجرا می‌شود و هدف آن اجرای کار پس‌زمینه‌ی مدنظر است. متد StopAsync نیز دقیقا پیش از خاتمه‌ی برنامه فراخوانی خواهد شد تا اگر نیاز به پاکسازی منابعی وجود داشته باشد، بتوان از این فرصت استفاده کرد. به این ترتیب اگر نیاز به اجرای متناوب کار پس‌زمینه‌ای وجود دارد، پیاده سازی آن به خود ما واگذار شده‌است.


یک مثال: معرفی کار پس‌زمینه‌ای که هر دو ثانیه یکبار انجام می‌شود

در SampleHostedService زیر، عبارت Hosted service executing به همراه زمان جاری، هر دو ثانیه یکبار لاگ می‌شود و اگر برنامه را توسط دستور dotnet run اجرا کنید، می‌توانید خروجی آن‌را در کنسول، مشاهده کنید:
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace MvcTest
{
    public class SampleHostedService : IHostedService
    {
        private readonly ILogger<SampleHostedService> _logger;

        public SampleHostedService(ILogger<SampleHostedService> logger)
        {
            _logger = logger;
        }

        public async Task StartAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation("Starting Hosted service");

            while (!cancellationToken.IsCancellationRequested)
            {
                _logger.LogInformation("Hosted service executing - {0}", DateTime.Now);
                await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
            }
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation("Stopping Hosted service");
            return Task.CompletedTask;
        }
    }
}
در ادامه برای معرفی این کار پس‌زمینه به سیستم به صورت یک سرویس با طول عمر Singleton خواهیم داشت:
namespace MvcTest
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<IHostedService, SampleHostedService>();
روش دیگر انجام اینکار استفاده از متد الحاقی AddHostedService است:
services.AddHostedService<SampleHostedService>();
مزیت اینکار این است که متد Configure واقع در کلاس Startup یک چنین امضایی را دارد:
 public void Configure(IApplicationBuilder app, IHostingEnvironment env)
و IHostingEnvironment هم در فضای نام Microsoft.AspNetCore.Hosting واقع شده‌است و هم در فضای نام Microsoft.Extensions.Hosting که IHostedService در آن قرار دارد. به همین جهت چون متد AddHostedService، تعریف IHostedService را مخفی می‌کند، خطای زمان کامپایلی را جهت مشخص سازی صریح فضای نام  IHostingEnvironment دریافت نخواهید کرد:
Startup.cs(82,56): error CS0104: 'IHostingEnvironment' is an ambiguous reference between
'Microsoft.AspNetCore.Hosting.IHostingEnvironment' and 'Microsoft.Extensions.Hosting.IHostingEnvironment'


مشکلات پیاده سازی کار پس‌زمینه‌ی SampleHostedService فوق

هر چند اگر مثال فوق را اجرا کنید، خروجی مناسبی را دریافت خواهید کرد، اما دارای این اشکال مهم نیز هست:
D:\MvcTest>dotnet run
info: MvcTest.SampleHostedService[0]
      Starting Hosted service
info: MvcTest.SampleHostedService[0]
      Hosted service executing - 02/19/2019 14:45:10
info: MvcTest.SampleHostedService[0]
      Hosted service executing - 02/19/2019 14:45:12
info: MvcTest.SampleHostedService[0]
      Hosted service executing - 02/19/2019 14:45:14
Ctrl+C
Application is shutting down...
Hosting environment: Development
Content root path: D:\MvcTest
Now listening on: https://localhost:5001
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.
پس از اجرای دستور dotnet run، سرویس پس زمینه شروع به کار کرده‌است. پس از مدتی کلیدهای Ctrl+C را فشرده‌ایم تا این حلقه‌ی بی‌نهایت و برنامه خاتمه یابد. اینجا است که مشاهده می‌کنید تازه قسمت هاست برنامه‌ی وب ما شروع به کار کرده‌است؛ یعنی دقیقا زمانیکه پروسه‌ی برنامه در حال خاتمه یافتن است. چرا اینگونه رفتار کرده‌است؟
از دیدگاه ASP.NET Core، یک کار پس زمینه زمانی خاتمه یافته محسوب می‌شود که متد StartAsync، مقدار Task.CompletedTask را بازگرداند؛ در غیراینصورت، در حال اجرا درنظر گرفته می‌شود و چون در پیاده سازی فوق این نکته رعایت نشده‌است، این Task همواره در حال اجرا و خاتمه نیافته محسوب می‌شود و نوبت به مابقی کارها نخواهد رسید. همچنین در قسمت StopAsync نیز بهتر است یک فیلد CancellationTokenSource تعریف شده‌ی در سطح کلاس را مورد استفاده قرار داد و متد Cancel آن‌را فراخوانی کرد تا اطلاع رسانی صحیحی را به متد StartAsync در مورد خاتمه‌ی برنامه، انجام دهد.
برای این منظور و جهت ساده سازی و پیاده سازی تمام این نکات، از اینترفیس خام IHostedService، یک کلاس abstract به نام BackgroundService نیز در فضای نام Microsoft.Extensions.Hosting پیش بینی شده‌است:
namespace Microsoft.Extensions.Hosting
{
    public abstract class BackgroundService : IHostedService, IDisposable
    {
        protected BackgroundService();
        public virtual void Dispose();
        public virtual Task StartAsync(CancellationToken cancellationToken);
        public virtual Task StopAsync(CancellationToken cancellationToken);
        protected abstract Task ExecuteAsync(CancellationToken stoppingToken);
    }
}
برای استفاده‌ی از آن تنها کافی است متد ExecuteAsync آن‌را پیاده سازی کنیم. به این ترتیب اینبار پیاده سازی SampleHostedService به صورت زیر تغییر می‌کند:
namespace MvcTest
{
    public class PrinterHostedService : BackgroundService
    {
        private readonly ILogger<SampleHostedService> _logger;

        public PrinterHostedService(ILogger<SampleHostedService> logger)
        {
            _logger = logger;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            _logger.LogInformation("Starting Hosted service");

            while (!stoppingToken.IsCancellationRequested)
            {
                _logger.LogInformation("Hosted service executing - {0}", DateTime.Now);
                await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken);
            }
        }
    }
}
اینبار اگر این کار پس‌زمینه را به سیستم معرفی:
namespace MvcTest
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddHostedService<PrinterHostedService>();
و سپس برنامه را اجرا کنیم:
D:\MvcTest>dotnet run
Hosting environment: Development
infoContent root path: D:\MvcTest
Now listening on: https://localhost:5001
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.
: MvcTest.SampleHostedService[0]
      Starting Hosted service
info: MvcTest.SampleHostedService[0]
      Hosted service executing - 02/19/2019 15:00:23
info: MvcTest.SampleHostedService[0]
      Hosted service executing - 02/19/2019 15:00:25
info: MvcTest.SampleHostedService[0]
      Hosted service executing - 02/19/2019 15:00:27
Application is shutting down...
^C
مشاهده می‌کنیم که ابتدا هاست وب برنامه شروع به کار کرده‌است و سپس سرویس انجام کارهای پس‌زمینه در حال اجرا است و به این ترتیب اجرای این سرویس پس‌زمینه، تداخلی را در کار برنامه‌ی وب ایجاد نکرده‌است. بنابراین از این پس بجای استفاده‌ی از IHostedService خام، از نمونه‌ی بهبود یافته‌ی BackgroundService آن استفاده کنید.


یک نکته: تزریق وابستگی DbContext برنامه در یک سرویس کار پس‌زمینه

IHostedServiceها با طول عمر singleton به سیستم تزریق وابستگی‌ها معرفی می‌شوند. در این حالت اگر سرویس‌هایی با طول عمر transient و یا scoped را به آن‌ها تزریق کنید، دیگر طول عمر مدنظر شما را نداشته و آن‌ها هم به صورت singleton عمل خواهند کرد. هر چند خود سیستم تزریق وابستگی‌های NET Core. با صدور استثنائی، از این مساله جلوگیری می‌کند (در این مورد در مطالب «مهارت‌های تزریق وابستگی‌ها در برنامه‌های NET Core. - قسمت چهارم - پرهیز از الگوی Service Locator در برنامه‌های وب» و همچنین «قسمت سوم - رهاسازی منابع سرویس‌های IDisposable» بیشتر بحث شده‌است). یک چنین مواردی را به صورت زیر با تزریق IServiceScopeFactory و ساخت صریح یک Scope می‌توان مدیریت کرد:
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

public abstract class ScopedBackgroundService : BackgroundService
{
    private readonly IServiceScopeFactory _serviceScopeFactory;

    public ScopedBackgroundService(IServiceScopeFactory serviceScopeFactory)
    {
        _serviceScopeFactory = serviceScopeFactory;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using (var scope = _serviceScopeFactory.CreateScope())
        {
            await ExecuteInScope(scope.ServiceProvider, stoppingToken);
        }
    }

    public abstract Task ExecuteInScope(IServiceProvider serviceProvider, CancellationToken stoppingToken);
}
از این پس برای تعریف کارهای پس‌زمینه‌ای که نیاز به تزریق سرویس‌هایی با طول عمر Scoped یا Transient دارند، می‌توان کلاس سرویس وظیفه را از ScopedBackgroundService مشتق کرد و سپس متد ExecuteInScope آن‌را پیاده سازی نمود. serviceProvider ای که در اینجا در اختیار مصرف کننده قرار می‌گیرد، داخل Scope قرار دارد و توسط آن می‌توان سرویس‌های مدنظر را توسط متدهایی مانند serviceProvider.GetRequiredService، دریافت کرد.


طراحی سرویس کارهای پس‌زمینه‌ی زمان‌بندی شده

ASP.NET Core، متد ExecuteAsync را یکبار بیشتر اجرا نمی‌کند. بنابراین پیاده سازی تایمری که بخواهد برای مثال ارسال ایمیل‌های خبرنامه‌ی سایت را هر روز ساعت 11 شب انجام دهد، به خود ما واگذار شده‌است. برای پیاده سازی بهتر این تایمر می‌توان از کتابخانه‌ی NCrontab که توسط نویسنده‌ی کتابخانه‌ی معروف ELMAH تهیه شده‌است، استفاده کرد که با برنامه‌های NET Core. نیز سازگاری دارد:
 dotnet add package ncrontab
عبارات Cron، روش بسیار متداولی برای تعریف و انجام کارهای زمانبندی شده در سیستم‌های لینوکسی هستند. برای مثال عبارت * * * 0 1 سبب اجرای یک وظیفه، هر روز یک دقیقه پس از نیمه‌شب، می‌شود و فرمت کلی 5 قسمتی آن، به صورت زیر است:
┌───────────── minute (0 - 59) 
│ ┌───────────── hour (0 - 23) 
│ │ ┌───────────── day of month (1 - 31) 
│ │ │ ┌───────────── month (1 - 12) 
│ │ │ │ ┌───────────── day of week (0 - 6) (Sunday to Saturday; 
│ │ │ │ │                                       7 is also Sunday on some systems) 
│ │ │ │ │ 
│ │ │ │ │ 
* * * * *
و یا عبارت 6 قسمتی آن چنین مفهومی را دارد:
* * * * * *
- - - - - -
| | | | | |
| | | | | +--- day of week (0 - 6) (Sunday=0)
| | | | +----- month (1 - 12)
| | | +------- day of month (1 - 31)
| | +--------- hour (0 - 23)
| +----------- min (0 - 59)
+------------- sec (0 - 59)
اگر ScopedBackgroundService فوق را با CrontabSchedule یاد شده ترکیب کنیم، می‌توانیم به یک کلاس abstract دیگر برسیم که طراحی کلاس پایه‌ی اجرای کارهای زمانبندی شده را ارائه می‌دهد:
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using NCrontab;
using static NCrontab.CrontabSchedule;

public abstract class ScheduledScopedBackgroundService : ScopedBackgroundService
{
    private CrontabSchedule _schedule;
    private DateTime _nextRun;

    protected abstract string Schedule { get; }

    public ScheduledScopedBackgroundService(IServiceScopeFactory serviceScopeFactory)
     : base(serviceScopeFactory)
    {
        _schedule = CrontabSchedule.Parse(Schedule, new ParseOptions { IncludingSeconds = true });
        _nextRun = _schedule.GetNextOccurrence(DateTime.Now);
    }

    public override async Task ExecuteInScope(IServiceProvider serviceProvider, CancellationToken stoppingToken)
    {
        do
        {
            var now = DateTime.Now;
            if (now > _nextRun)
            {
                await ScheduledExecuteInScope(serviceProvider, stoppingToken);
                _nextRun = _schedule.GetNextOccurrence(DateTime.Now);
            }
            await Task.Delay(1000, stoppingToken); //1 second delay
        }
        while (!stoppingToken.IsCancellationRequested);
    }

    public abstract Task ScheduledExecuteInScope(IServiceProvider serviceProvider, CancellationToken stoppingToken);
}
این کلاس پایه، توسط متد CrontabSchedule.Parse، مقدار رشته‌ای Schedule را با فرمت Cron (فرمت 6 قسمتی که دارای ثانیه هم هست) دریافت و پردازش می‌کند. سپس متد GetNextOccurrence، زمان بعدی اجرای این وظیفه را مشخص می‌کند.
روش استفاده‌ی از آن برای تعریف یک وظیفه‌ی جدید نیز به صورت زیر است:
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

public class MyScheduledTask : ScheduledScopedBackgroundService
{
    private readonly ILogger<MyScheduledTask> _logger;

    public MyScheduledTask(
        IServiceScopeFactory serviceScopeFactory,
        ILogger<MyScheduledTask> logger) : base(serviceScopeFactory)
    {
        _logger = logger;
    }

    protected override string Schedule => "*/10 * * * * *"; //Runs every 10 seconds

    public override Task ScheduledExecuteInScope(IServiceProvider serviceProvider, CancellationToken stoppingToken)
    {
        _logger.LogInformation("MyScheduledTask executing - {0}", DateTime.Now);
        return Task.CompletedTask;
    }
}
در اینجا ابتدا کار با پیاده سازی کلاس پایه ScheduledScopedBackgroundService شروع می‌شود. سپس باید مقدار Schedule را با فرمت 6 قسمتی مشخص کرد. برای مثال در سرویس فوق، این تنظیم سبب اجرای هر 10 ثانیه یکبار این وظیفه می‌گردد. در آخر، خود وظیفه داخل متد ScheduledExecuteInScope تعریف خواهد شد که serviceProvider دریافتی آن، داخل یک Scope قرار دارد.
روش معرفی آن به سیستم نیز مانند قبل است:
namespace MvcTest
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddHostedService<MyScheduledTask>();
در این حالت اگر برنامه را اجرا کنید، یک چنین خروجی را که بیانگر اجرای هر 10 ثانیه یکبار وظیفه‌ی تعریف شده‌است، مشاهده می‌کنید:
D:\MvcTest>dotnet run
Hosting environment: Development
Content root path: D:\MvcTest
Now listening on: https://localhost:5001
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.
info: MyScheduledTask[0]
      MyScheduledTask executing - 02/19/2019 19:18:50
info: MyScheduledTask[0]
      MyScheduledTask executing - 02/19/2019 19:19:00
info: MyScheduledTask[0]
      MyScheduledTask executing - 02/19/2019 19:19:10
Application is shutting down...
^C
مطالب
راهکار لاگ متناسب با Cloud و On-Premise
Application Insights راهکار ارائه شده توسط Microsoft است که در سه بخش به ما کمک می‌کند تا سیستم لاگ مؤثر و کارآمدی داشته باشیم:

۱- متدهای پایه Log که به صورت دستی فراخوانی می‌شوند، مانند TrackEvent برای ثبت یک رویداد بیزینسی که این متدها فراتر از متدهای معمول loggerهای متداول هستند.

۲- به صورت خودکار، Application Insights خطاهای سیستم را لاگ نموده و همچنین در زمان کار کردن با Http Client، دیتابیس و سایر Dependencyها، میزان طول کشیدن آنها را به همراه آدرس Request یا متن Sql Command و سایر اطلاعات مفید را نیز ذخیره می‌کند که این خود منجر به جمع شدن دیتایی بسیار ارزشمند در سیستم می‌شود.
البته اگر یک Dependency به صورت خودکار شناسایی نشود، مانند Redis، شما می‌توانید خودتان با متد TrackDependency اطلاعات آن‌را به AppInsights بدهید.

۳- داشبورد App Insights در Azure این امکان را می‌دهد که به سریع‌ترین شکل ممکن در لاگ‌ها جستجو نمود و برای مثال تمامی کارهای انجام شده توسط یک کاربر خاص را به صورت یک‌جا مشاهده و بررسی کرد.
فرضا اگر کاربر درخواست گرفتن خروجی Excel از لیست محصولات را داشته و این ۱ ثانیه طول کشیده، چقدر آن در انتظار دیتابیس بوده و ...
به علاوه از Power BI نیز می‌توانید برای بیرون کشیدن نکات مهم استفاده کنید.

البته شاید App Insights برای کسانی که Azure Account نداشته باشند، مناسب به نظر نرسد، ولی اگر راهکاری برای ذخیره سازی On-Premise اطلاعات لاگ شده وجود داشته باشد چه؟ مثلا اطلاعات آن را در Elastic موجود در سرورهای شرکت، داخل ایران ذخیره نمود، بدون الزام به این‌که حتی آن سرور دسترسی به اینترنت داشته باشد.

بله، این امکان وجود دارد و با کمک  Microsoft Diagnostics EventFlow می‌توان اطلاعات App Insights را در هر جایی از جمله Elastic ذخیره نمود و بدین طریق از عمده مزایای App Insights بدون داشتن Azure Account بهره مند شد.

برای این منظور به شکل زیر عمل کنید: (آموزش برای ASP.NET Core 3.1 بوده، ولی برای سایر پروژه‌ها نیز قابل استفاده است)

۱- ابتدا Application Insights را به پروژه خود اضافه کنید.بدین منظور لازم است Packageهای Microsoft.Extensions.Logging.ApplicationInsights و Microsoft.ApplicationInsights.AspNetCore را نصب کنید. 

۲- در Program.cs بعد از
Host.CreateDefaultBuilder(args)
کد زیر را قرار دهید
.ConfigureLogging(loggingBuilder  =>
{
    loggingBuilder.ClearProviders();
    loggingBuilder.AddApplicationInsights(); 
})
این باعث می‌شود تا Loggerهای پیش فرض Console و Debug حذف شوند و البته اگر کتابخانه 3rd party ای از Microsoft.Extensions.Logging استفاده کرده باشد، اطلاعات لاگ آن به AppInsights نیز داده شود و در نهایت شما با کمک Microsoft Diagnostics EventFlow، آن اطلاعات را در Elastic و Console و سایر Outputها خواهید داشت.

۳- اگر جایی قصد لاگ کردن یک Event را دارید، یا در مثال استفاده از Redis میخواهید اطلاعات زمان طول کشیدن رفت و برگشت به Redis را لاگ کنید یا یک try/catch دارید که در catch آن خطا را مجدد throw نمی‌کنید، ولی قصد لاگ کردن exception را دارید، ابتدا TelemetryClient را inject نموده و از متدهای آن مانند TrackException استفاده کنید.
توجه داشته باشید که اگر از ILogger ارائه شده توسط MS.Ext.Logging استفاده کنید نیز کار خواهد کرد.

۴- پکیج Microsoft.Diagnostics.EventFlow.Inputs.ApplicationInsights را در پروژه نصب کنید و سپس از بین Output‌های معرفی شده نیز یکی را انتخاب و پکیج آن را نیز نصب کنید. شما می‌توانید دیتایی را که AppInsights به صورت خودکار جمع نموده را + دیتای ارائه شده توسط خودتان را به Elastic، Splunk و ... بفرستید.
ما در این مثال برای سادگی Std Out - Console Output را انتخاب می‌کنیم و پکیج Microsoft.Diagnostics.EventFlow.Outputs.StdOutput را نصب می‌کنیم.

۵- فایل eventFlowConfig.json را به پروژه اضافه کنید و موارد زیر را در آن قرار دهید:
 {
  "inputs": [
    {
      "type": "ApplicationInsights"
    }
  ],
  "outputs": [
    {
      "type": "StdOutput" // console output
    }
  ],
  "schemaVersion": "2016-08-11"
}  
در این فایل، inputs شامل ApplicationInsights بوده و البته موارد جالبی چون PerformanceCounters را نیز میتوانید در آن بگنجانید و outputs بسته به نیاز شما برابر با Elastic و ... است که در این صورت باید اطلاعات اتصال به Elastic شامل آدرس سرور و ... را نیز ارائه کنید. توجه داشته باشید که ما در حال استفاده از ApplicationInsights SDK هستیم، به عنوان کتابخانه Logging و در نهایت دیتا نه به ApplicationInsights در سرورهای Microsoft Azure، که به output یا outputهایی که در فایل eventFlowConfig.json می‌گوییم ارسال می‌شود.

۶- در Program.cs متد Main را به شکل زیر در بیاورید:
using (var pipeline = DiagnosticPipelineFactory.CreatePipeline("eventFlowConfig.json"))
{
    CreateHostBuilder(args, pipeline)
        .Build()
        .Run();
}  
و CreateHostBuilder نیز به شکل زیر باشد:
public static IHostBuilder CreateHostBuilder(string[] args, DiagnosticPipeline pipeline) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureServices(services => services.AddSingleton<ITelemetryProcessorFactory>(sp => new EventFlowTelemetryProcessorFactory(pipeline)))
        .ConfigureLogging(logginBuilder =>
        {
            logginBuilder.ClearProviders();
            loggingBuilder.AddApplicationInsights(); 
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });  

همه چیز آماده است. هم اکنون اگر Azure Account داشته باشید، می‌توانید با دادن instrumentationKey در appsetting.json از داشبورد فوق العاده ApplicationInsights استفاده کنید و اگر نه هم در سرورهای داخلی خودتان Splunk و ... را راه اندازی و در فایل eventFlowConfig.json، در قسمت outputs، اطلاعات آدرس آنها را بدهید و لاگ‌های مفصل و کاربردی ای که به صورت خودکار جمع آوری شده را به همراه اطلاعاتی که خودتان دستی ارائه کرده اید، یکجا تحویل بگیرید.

لینک پروژه در GitHub که حاوی مثال Elastic است.