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

I have been using Windows operating system from the beginning. When I first started using computer Windows XP was the latest operating system and it was amazing. After few years I started my career as a Java developer and in my office also I have been using Windows only.
 After few years Windows Vista got released and I suffered with it for few months and then Microsoft released Windows 7 which is the best Windows operating system IMO. And then they released Windows 8 which I don’t like much. Recently they released Windows 10 and I have upgraded from Windows 8.1 to Windows 10. Windows 10 is certainly better than Windows 8 but it still sucks compared to Windows 7. 

تاملی در مهاجرت از ویندوز به لینوکس
مطالب
معرفی کتابخانه PdfReport
مدتی هست که در حال تهیه کتابخانه‌ی گزارش سازی مبتنی بر iTextSharp هستم و عمده‌ی استفاده از آن برای من تاکنون، تهیه گزارشات باکیفیت PDF فارسی تحت وب بوده؛ هر چند این کتابخانه وابستگی خاصی به فناوری مورد استفاده ندارد و با WinForms، WPF، مشتقات ASP.NET ، سرویس‌های WCF و کلا هرجایی که دات نت فریم ورک کامل در دسترس باشد، قابل استفاده است. همچنین به منبع داده خاصی گره نخورده است و حتی می‌توانید از یک anonymously typed list عدم متصل به بانک اطلاعاتی خاصی نیز به عنوان منبع داده آن استفاده کنید.
کتابخانه PdfReport به عمد جهت دات نت 3.5 تهیه شده است تا بازه وسیعی از سیستم عامل‌ها را پوشش دهد.
این کتابخانه علاوه بر تبدیل اطلاعات شما به گزارشات مبتنی بر PDF، امکان تهیه خروجی خودکار اکسل (2007 به بعد) را نیز دارد. فایل خروجی آن، به صورت پیوست درون فایل PDF تهیه شده قرار می‌گیرد و جزئی از آن می‌شود.
بدیهی است اینبار با کتابخانه گزارش سازی روبرو هستید که با راست به چپ مشکلی ندارد!
کتابخانه PdfReport بر پایه کتابخانه‌های معروف سورس باز iTextSharp و EPPlus تهیه شده است. حداقل مزیت استفاده از آن، صرفه جویی در وقت شما جهت آموختن ریزه کاری‌های مرتبط با هر کدام از کتابخانه‌های یاده شده است. برای نمونه جهت فراگیری کار با iTextSharp نیاز است یک کتاب 600 صفحه‌ای به نام iText in action را مطالعه و تمرین کنید. این مورد منهای مسایل و نکات متعدد مرتبط با زبان فارسی است که در این کتاب به آن‌ها اشاره‌ای نشده است.
برای تهیه آن، مشکلات متداولی که کاربران مدام در انجمن‌های برنامه نویسی مطرح می‌کنند و ابزارهای موجود عاجز از ارائه راه حلی ساده برای حل آن‌ها هستند، مد نظر بوده و امید است نگارش یک این کتابخانه بتواند بسیاری از این دردها را کاهش دهد.
کار با این کتابخانه صرفا با کدنویسی میسر است (code first) و همین مساله انعطاف پذیری قابل توجهی را ایجاد کرده که در طی روزهای آینده با جزئیات بیشتر آن آشنا خواهید شد.


اما چرا PDF؟

استفاده از قالب PDF برای تهیه گزارشات، این مزایا را به همراه دارد:
- دقیقا همان چیزی که مشاهده می‌شود، در هر مکانی قابل چاپ است. با همان کیفیت، همان اندازه صفحه، همان فونت و غیره. به این ترتیب به صفحه بندی بسیار مناسب و بهینه‌ای می‌توان رسید و مشکلات گزارشات HTML ایی وب را ندارد.
- امکان استفاد از فونت‌های شکیل‌تر در آن بدون مشکل و بدون نیاز به نصب بر روی کامپیوتری میسر است؛ چون فونت را می‌توان در فایل PDF نیز قرار داد.
- این فایل در تمام سیستم عامل‌ها پشتیبانی می‌شود. خصوصا اینکه فایل نهایی در تمام کامپیوترها و در انواع و اقسام سیستم عامل‌ها به یک شکل و اندازه نمایش داده خواهد شد. برای مثال اینطور نیست که در ویندوز XP ،‌اعداد آن فارسی نمایش داده شوند و در ویندوز 7 با تنظیمات محلی خاصی، دیگر اینطور نباشد. حتی تحت لینوکس هم اعداد آن فارسی نمایش داده خواهد شد چون فونت مخصوص بکار رفته، در خود فایل PDF قابل قرار دادن است.
- برنامه معروف و رایگان Adobe reader برای خواندن و مشاهده آن کفایت می‌کند و البته کلاینت یکبار باید این برنامه را نصب کند. همچنین از این نوع برنامه‌های رایگان برای مشاهده فایل‌های PDF زیاد است.
- تمام صفحات گزارش را در یک فایل می‌توان داشت و به یکباره تمام آن نیز به سادگی قابل چاپ است. این مشکلی است که با گزارشات تحت وب وجود دارد که نمی‌شود مثلا یک گزارش 100 صفحه‌ای را به یکباره به چاپگر ارسال کرد. به همین جهت عموما کاربران درخواست می‌دهند تا کل گزارش را در یک صفحه HTML نمایش دهید تا ما راحت آن‌را چاپ کنیم یا راحت از آن خروجی بگیریم. اما زمانیکه فایل PDF تهیه می‌شود این مشکلات وجود نخواهد داشت و جهت Print بسیار بهینه سازی شده است. تا حدی که الان فرمت برگزیده تهیه کتاب‌های الکترونیکی نیز PDF است. مثلا سایت معروف آمازون امکان فروش نسخه PDF کتاب‌ها را هم پیش بینی کرده است.
-امکان صفحه بندی دقیق به همراه مشخص سازی landscape یا portrait بودن صفحه نهایی میسر است. چیزی که در گزارشات صفحات وب آنچنان معنایی ندارد.
- امکان رمزنگاری اطلاعات در آن پیش بینی شده است. همچنین می‌توان به فایل‌های PDF امضای دیجیتال نیز اضافه کرد. به این ترتیب هرگونه تغییری در محتوای فایل توسط برنامه‌های PDF خوان معتبر گزارش داده شده و می‌توان از صحت اطلاعات ارائه شده توسط آن اطمینان حاصل کرد.
- از فشرده سازی مطالب، فایل‌ها و تصاویر قرار داده شده در آن پشتیبانی می‌کند.
- از گرافیک برداری پشتیبانی می‌کند.


مجوز استفاده از این کتابخانه:
کار من مبتنی بر LGPL است. به این معنا که به صورت باینری (فایل dll) در هر نوع پروژه‌ای قابل استفاده است.
اما ... PdfReport از دو کتابخانه دیگر نیز استفاده می‌کند:
- کتابخانه iTextSharp که دارای مجوز AGPL است. این مجوز رایگان نیست.
- کتابخانه EPPlus برای تولید فایل‌های اکسل با کیفیت. مجوز استفاده از این کتابخانه LGPL است و تا زمانیکه به صورت باینری از آن استفاده می‌کنید، محدودیتی را برای شما ایجاد نخواهد کرد.


کتابخانه PdfReport به صورت سورس باز در CodePlex قرار گرفته ؛ اما جهت پرسیدن سؤالات، پیشنهادات، ارائه بهبود و غیره می‌توانید (و بهتر است) از قسمت مدیریت پروژه مرتبط در سایت جاری نیز استفاده کنید.


نحوه تهیه اولین گزارش، با کتابخانه PdfReport

الف) یک پروژه Class library جدید را شروع کنید. از این جهت که گزارشات PdfReport در انواع و اقسام پروژه‌های VS.NET قابل استفاده است، می‌توان از این پروژه Class library به عنوان کلاس‌های پایه قابل استفاده در انواع و اقسام پروژه‌های مختلف، بدون نیاز به تغییری در کدهای آن استفاده کرد.

ب) آخرین نگارش فایل‌های مرتبط با PdfReport را از اینجا دریافت کنید و سپس ارجاعاتی را به اسمبلی‌های موجود در بسته آن به پروژه خود اضافه نمائید (ارجاعاتی به PdfReport، iTextSharp و EPPlus). فایل XML راهنمای کتابخانه نیز به همراه بسته آن می‌باشد که در حین استفاده از متدها و خواص PdfReport کمک بزرگی خواهد بود.

ج) کلاس‌های زیر را به آن اضافه کنید:
using System.Web;
using System.Windows.Forms;

namespace PdfReportSamples
{
    public static class AppPath
    {
        public static string ApplicationPath
        {
            get
            {
                if (isInWeb)
                    return HttpRuntime.AppDomainAppPath;

                return Application.StartupPath;
            }
        }

        private static bool isInWeb
        {
            get
            {
                return HttpContext.Current != null;
            }
        }
    }
}
از این کلاس برای مشخص سازی محل ذخیره سازی فایل‌های نهایی PDF تولیدی استفاده خواهیم کرد.
همانطور که مشاهده می‌کنید ارجاعاتی را به System.Windows.Forms.dll و System.Web.dll نیاز دارد.

در ادامه کلاس User را جهت ساخت یک منبع داده درون حافظه‌ای تعریف خواهیم کرد:
using System;

namespace PdfReportSamples.IList
{
    public class User
    {
        public int Id { set; get; }
        public string Name { set; get; }
        public string LastName { set; get; }
        public long Balance { set; get; }
        public DateTime RegisterDate { set; get; }
    }
}
اکنون کلاس اصلی گزارش ما به صورت زیر خواهد بود:
using System;
using System.Collections.Generic;
using PdfRpt.Core.Contracts;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.IList
{
    public class IListPdfReport
    {
        public IPdfReportData CreatePdfReport()
        {
            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.RightToLeft);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" });
            })
            .DefaultFonts(fonts =>
            {
                fonts.Path(Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf",
                                  Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
            })
            .PagesFooter(footer =>
            {
                footer.DefaultFooter(DateTime.Now.ToString("MM/dd/yyyy"));
            })
            .PagesHeader(header =>
            {
                header.DefaultHeader(defaultHeader =>
                {
                    defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png");
                    defaultHeader.Message("گزارش جدید ما");
                });
            })
            .MainTableTemplate(template =>
            {
                template.BasicTemplate(BasicTemplate.ClassicTemplate);
            })
            .MainTablePreferences(table =>
            {
                table.ColumnsWidthsType(TableColumnWidthType.Relative);
                table.NumberOfDataRowsPerPage(5);
            })
            .MainTableDataSource(dataSource =>
            {
                var listOfRows = new List<User>();
                for (int i = 0; i < 200; i++)
                {
                    listOfRows.Add(new User { Id = i, LastName = "نام خانوادگی " + i, Name = "نام " + i, Balance = i + 1000 });
                }
                dataSource.StronglyTypedList<User>(listOfRows);
            })
            .MainTableSummarySettings(summarySettings =>
            {
                summarySettings.OverallSummarySettings("جمع کل");
                summarySettings.PerviousPageSummarySettings("نقل از صفحه قبل");
                summarySettings.PageSummarySettings("جمع صفحه");
            })
            .MainTableColumns(columns =>
            {
                columns.AddColumn(column =>
                {
                    column.PropertyName("rowNo");
                    column.IsRowNumber(true);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(0);
                    column.Width(1);
                    column.HeaderCell("#");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.Id);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(1);
                    column.Width(2);
                    column.HeaderCell("شماره");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.Name);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(2);
                    column.Width(3);
                    column.HeaderCell("نام");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.LastName);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(3);
                    column.Width(3);
                    column.HeaderCell("نام خانوادگی");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.Balance);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(4);
                    column.Width(2);
                    column.HeaderCell("موجودی");
                    column.ColumnItemsTemplate(template =>
                    {
                        template.TextBlock();
                        template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });
                    column.AggregateFunction(aggregateFunction =>
                    {
                        aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum);
                        aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });
                });

            })
            .MainTableEvents(events =>
            {
                events.DataSourceIsEmpty(message: "رکوردی یافت نشد.");
            })
            .Export(export =>
            {
                export.ToExcel();
                export.ToCsv();
                export.ToXml();
            })
            .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\RptIListSample.pdf"));
        }
    }
}
و برای استفاده از آن:
var rpt = new IListPdfReport().CreatePdfReport();
// rpt.FileName


برای نمونه، جهت مشاهده نمایش این خروجی در یک برنامه ویندوزی، به مثال‌های همراه سورس پروژه در مخزن کد آن مراجعه نمائید.

توضیحات بیشتر:

- در قسمت  DocumentPreferences، جهت راست به چپ (PdfRunDirection)، اندازه صفحه (PdfPageSize)، جهت صفحه (PageOrientation) و امثال آن تنظیم می‌شوند.
- سپس نیاز است قلم‌های مورد استفاده در گزارش مشخص شوند. در متد DefaultFonts باید دو  قلم را معرفی کنید. قلم اول، قلم پیش فرض خواهد بود و قلم دوم برای رفع نواقص قلم اول مورد استفاده قرار می‌گیرد. برای مثال اگر قلم اول فاقد حروف انگلیسی است، به صورت خودکار به قلم دوم رجوع خواهد شد.
- در ادامه در متد PagesFooter، تاریخ درج شده در پایین تمام صفحات مشخص می‌شود. در مورد ساخت Footer سفارشی در قسمت‌های بعدی بحث خواهد شد.
- در متد PagesHeader، متن و تصویر قرار گرفته در Header تمام صفحات گزارش قابل تنظیم است. این مورد نیز قابل سفارشی سازی است که در قسمت‌های بعد به آن خواهیم پرداخت.
- توسط MainTableTemplate، قالب ظاهری ردیف‌های گزارش مشخص می‌شود. یک سری قالب پیش فرض در کتابخانه PdfReport موجود است که توسط متد BasicTemplate آن قابل دسترسی است. در مورد نحوه تعریف قالب‌‌های سفارشی به مرور در قسمت‌های بعد، بحث خواهد شد.
- در قسمت MainTablePreferences تنظیمات جدول اصلی گزارش تعیین می‌شود. برای مثال چه تعداد ردیف در صفحه نمایش داده شود. اگر این مورد را تنظیم نکنید، به صورت خودکار محاسبه خواهد شد. نحوه تعیین عرض ستون‌های گزارش به کمک متد ColumnsWidthsType مشخص می‌شود که در اینجا حالت نسبی درنظر گرفته شده است.
- منبع داده مورد استفاده توسط متد MainTableDataSource مشخص می‌شود که در اینجا یک لیست جنریک تعیین شده و سپس توسط متد StronglyTypedList در اختیار گزارش ساز جاری قرار می‌گیرد. تعدادی منبع داده پیش فرض در PdfReport وجود دارند که هر کدام را در قسمت‌های بعدی بررسی خواهیم کرد. همچنین امکان تعریف منابع داده سفارشی نیز وجود دارد.
- با کمک متد MainTableSummarySettings، برچسب‌های جمع‌های پایین صفحات مشخص می‌شود.
- در قسمت MainTableColumns، ستون‌هایی را که علاقمندیم در گزارش ظاهر شوند، قید می‌کنیم. هر ستون باید با یک فیلد یا خاصیت منبع داده متناظر باشد. همچنین همانطور که مشاهده می‌کنید امکان تعیین Visibility، عرض و غیره آن نیز مهیا است (قابلیت ساخت گزارشاتی که به انتخاب کاربر، ستون‌های آن ظاهر یا مخفی شوند). در اینجا توسط callbackهایی که در متد ColumnItemsTemplate قابل دسترسی هستند، می‌توان اطلاعات را پیش از نمایش فرمت کرد. برای مثال سه رقم جدا کننده به اعداد اضافه کرد (برای نمونه در خاصیت موجودی فوق) و یا توسط متد AggregateFunction، می‌توان متد تجمعی مناسبی را جهت ستون جاری مشخص کرد.
- توسط متد MainTableEvents به بسیاری از رخدادهای داخلی PdfReport دسترسی خواهیم یافت. برای مثال اگر در اینجا رکوردی موجود نباشد، رخداد DataSourceIsEmpty صادر خواهد شد.
- به کمک متد Export، خروجی‌های دلخواه مورد نظر را می‌توان مشخص کرد. تعدادی خروجی، مانند اکسل، XML و CSV در این کتابخانه موجود است. امکان سفارشی سازی آن‌ها نیز پیش بینی شده است.
- و نهایتا توسط متد Generate مشخص خواهیم کرد که فایل گزارش کجا ذخیره شود.

 لطفا برای طرح مشکلات و سؤالات خود در رابطه با کتابخانه PdfReport از این قسمت سایت استفاده کنید.
مطالب
اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity
AuthenticationMiddleware در ASP.NET Core 2.0، فقط مختص به کار با کوکی‌ها جهت اعتبارسنجی کاربران نیست. از این میان‌افزار می‌توان برای اعتبار سنجی‌های مبتنی بر JSON Web Tokens نیز استفاده کرد. مطلبی را که در ادامه مطالعه خواهید کرد دقیقا بر اساس نکات مطلب «پیاده سازی JSON Web Token با ASP.NET Web API 2.x» تدارک دیده شده‌است و به همراه نکاتی مانند تولید Refresh Tokens و یا غیرمعتبر سازی توکن‌ها نیز هست. همچنین ساختار جداول کاربران و نقش‌های آن‌ها، سرویس‌های مرتبط و قسمت تنظیمات Context آن با مطلب «اعتبارسنجی مبتنی بر کوکی‌ها در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» یکی است. در اینجا بیشتر به تفاوت‌های پیاده سازی این روش نسبت به حالت اعتبارسنجی مبتنی بر کوکی‌ها خواهیم پرداخت.
همچنین باید درنظر داشت، ASP.NET Core Identity یک سیستم اعتبارسنجی مبتنی بر کوکی‌ها است. دقیقا زمانیکه کار AddIdentity را انجام می‌دهیم، در پشت صحنه همان  services.AddAuthentication().AddCookie قسمت قبل فراخوانی می‌شود. بنابراین بکارگیری آن با JSON Web Tokens هرچند مشکلی را به همراه ندارد و می‌توان یک سیستم اعتبارسنجی «دوگانه» را نیز در اینجا داشت، اما ... سربار اضافی تولید کوکی‌ها را نیز به همراه دارد؛ هرچند برای کار با میان‌افزار اعتبارسنجی، الزامی به استفاده‌ی از ASP.NET Core Identity نیست و عموما اگر از آن به همراه JWT استفاده می‌کنند، بیشتر به دنبال پیاده سازی‌های پیش‌فرض مدیریت کاربران و نقش‌های آن هستند و نه قسمت تولید کوکی‌های آن. البته در مطلب جاری این موارد را نیز همانند مطلب اعتبارسنجی مبتنی بر کوکی‌ها، خودمان مدیریت خواهیم کرد و در نهایت سیستم تهیه شده، هیچ نوع کوکی را تولید و یا مدیریت نمی‌کند.



تنظیمات آغازین برنامه جهت فعالسازی اعتبارسنجی مبتنی بر JSON Web Tokens

اولین تفاوت پیاده سازی یک سیستم اعتبارسنجی مبتنی بر JWT، با روش مبتنی بر کوکی‌ها، تنظیمات متد ConfigureServices فایل آغازین برنامه است:
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<BearerTokensOptions>(options => Configuration.GetSection("BearerTokens").Bind(options));

            services
                .AddAuthentication(options =>
                {
                    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                    options.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme;
                    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                })
                .AddJwtBearer(cfg =>
                {
                    cfg.RequireHttpsMetadata = false;
                    cfg.SaveToken = true;
                    cfg.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidIssuer = Configuration["BearerTokens:Issuer"],
                        ValidAudience = Configuration["BearerTokens:Audience"],
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["BearerTokens:Key"])),
                        ValidateIssuerSigningKey = true,
                        ValidateLifetime = true,
                        ClockSkew = TimeSpan.Zero
                    };
                    cfg.Events = new JwtBearerEvents
                    {
                        OnAuthenticationFailed = context =>
                        {
                            var logger = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger(nameof(JwtBearerEvents));
                            logger.LogError("Authentication failed.", context.Exception);
                            return Task.CompletedTask;
                        },
                        OnTokenValidated = context =>
                        {
                            var tokenValidatorService = context.HttpContext.RequestServices.GetRequiredService<ITokenValidatorService>();
                            return tokenValidatorService.ValidateAsync(context);
                        },
                        OnMessageReceived = context =>
                         {
                             return Task.CompletedTask;
                         },
                        OnChallenge = context =>
                        {
                            var logger = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger(nameof(JwtBearerEvents));
                            logger.LogError("OnChallenge error", context.Error, context.ErrorDescription);
                            return Task.CompletedTask;
                        }
                    };
                });
در اینجا در ابتدا تنظیمات JWT فایل appsettings.json
{
  "BearerTokens": {
    "Key": "This is my shared key, not so secret, secret!",
    "Issuer": "http://localhost/",
    "Audience": "Any",
    "AccessTokenExpirationMinutes": 2,
    "RefreshTokenExpirationMinutes": 60
  }
}
به کلاسی دقیقا با همین ساختار به نام BearerTokensOptions، نگاشت شده‌اند. به این ترتیب می‌توان با تزریق اینترفیس <IOptionsSnapshot<BearerTokensOptions در قسمت‌های مختلف برنامه، به این تنظیمات مانند کلید رمزنگاری، مشخصات صادر کننده، مخاطبین و طول عمرهای توکن‌های صادر شده، دسترسی یافت.

سپس کار فراخوانی  services.AddAuthentication صورت گرفته‌است. تفاوت این مورد با حالت اعتبارسنجی مبتنی بر کوکی‌ها، ثوابتی است که با JwtBearerDefaults شروع می‌شوند. در حالت استفاده‌ی از کوکی‌ها، این ثوابت بر اساس CookieAuthenticationDefaults تنظیم خواهند شد.
البته می‌توان متد AddAuthentication را بدون هیچگونه پارامتری نیز فراخوانی کرد. این حالت برای اعتبارسنجی‌های دوگانه مفید است. برای مثال زمانیکه پس از AddAuthentication هم AddJwtBearer را ذکر کرده‌اید و هم AddCookie اضافه شده‌است. اگر چنین کاری را انجام دادید، اینبار باید درحین تعریف فیلتر Authorize، دقیقا مشخص کنید که حالت مبتنی بر JWT مدنظر شما است، یا حالت مبتنی بر کوکی‌ها:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
اگر متد AddAuthentication، مانند تنظیمات فوق به همراه این تنظیمات پیش‌فرض بود، دیگر نیازی به ذکر صریح AuthenticationSchemes در فیلتر Authorize نخواهد بود.


بررسی تنظیمات متد AddJwtBearer

در کدهای فوق، تنظیمات متد AddJwtBearer یک چنین مفاهیمی را به همراه دارند:
- تنظیم SaveToken به true، به این معنا است که می‌توان به توکن دریافتی از سمت کاربر، توسط متد HttpContext.GetTokenAsync در کنترلرهای برنامه دسترسی یافت.
در قسمت تنظیمات TokenValidationParameters آن:
- کار خواندن فایل appsettings.json برنامه جهت تنظیم صادر کننده و مخاطبین توکن انجام می‌شود. سپس IssuerSigningKey به یک کلید رمزنگاری متقارن تنظیم خواهد شد. این کلید نیز در تنظیمات برنامه قید می‌شود.
- تنظیم ValidateIssuerSigningKey به true سبب خواهد شد تا میان‌افزار اعتبارسنجی، بررسی کند که آیا توکن دریافتی از سمت کاربر توسط برنامه‌ی ما امضاء شده‌است یا خیر؟
- تنظیم ValidateLifetime به معنای بررسی خودکار طول عمر توکن دریافتی از سمت کاربر است. اگر توکن منقضی شده باشد، اعتبارسنجی به صورت خودکار خاتمه خواهد یافت.
- ClockSkew به معنای تنظیم یک تلرانس و حد تحمل مدت زمان منقضی شدن توکن در حالت ValidateLifetime است. در اینجا به صفر تنظیم شده‌است.

سپس به قسمت JwtBearerEvents می‌رسیم:
- OnAuthenticationFailed زمانی فراخوانی می‌شود که اعتبارسنج‌های تنظیمی فوق، با شکست مواجه شوند. برای مثال طول عمر توکن منقضی شده باشد و یا توسط ما امضاء نشده‌باشد. در اینجا می‌توان به این خطاها دسترسی یافت و درصورت نیاز آن‌ها را لاگ کرد.
- OnChallenge نیز یک سری دیگر از خطاهای اعتبارسنجی را پیش از ارسال آن‌ها به فراخوان در اختیار ما قرار می‌دهد.
- OnMessageReceived برای حالتی است که توکن دریافتی، توسط هدر مخصوص Bearer به سمت سرور ارسال نمی‌شود. عموما هدر ارسالی به سمت سرور یک چنین شکلی را دارد:
$.ajax({
     headers: { 'Authorization': 'Bearer ' + jwtToken },
اما اگر توکن شما به این شکل استاندارد دریافت نمی‌شود، می‌توان در رخ‌داد OnMessageReceived به اطلاعات درخواست جاری دسترسی یافت، توکن را از آن استخراج کرد و سپس آن‌را به خاصیت context.Token انتساب داد، تا به عنوان توکن اصلی مورد استفاده قرار گیرد. برای مثال:
const string tokenKey = "my.custom.jwt.token.key";
if (context.HttpContext.Items.ContainsKey(tokenKey))
{
    context.Token = (string)context.HttpContext.Items[tokenKey];
}
 - OnTokenValidated پس از کامل شدن اعتبارسنجی توکن دریافتی از سمت کاربر فراخوانی می‌شود. در اینجا اگر متد context.Fail را فراخوانی کنیم، این توکن، به عنوان یک توکن غیرمعتبر علامتگذاری می‌شود و عملیات اعتبارسنجی با شکست خاتمه خواهد یافت. بنابراین می‌توان از آن دقیقا مانند CookieValidatorService قسمت قبل که جهت واکنش نشان دادن به تغییرات اطلاعات کاربر در سمت سرور مورد استفاده قرار دادیم، در اینجا نیز یک چنین منطقی را پیاده سازی کنیم.


تهیه یک اعتبارسنج توکن سفارشی

قسمت OnTokenValidated تنظیمات ابتدای برنامه به این صورت مقدار دهی شده‌است:
OnTokenValidated = context =>
{
      var tokenValidatorService = context.HttpContext.RequestServices.GetRequiredService<ITokenValidatorService>();
      return tokenValidatorService.ValidateAsync(context);
},
TokenValidatorService سفارشی ما چنین پیاده سازی را دارد:
    public class TokenValidatorService : ITokenValidatorService
    {
        private readonly IUsersService _usersService;
        private readonly ITokenStoreService _tokenStoreService;

        public TokenValidatorService(IUsersService usersService, ITokenStoreService tokenStoreService)
        {
            _usersService = usersService;
            _usersService.CheckArgumentIsNull(nameof(usersService));

            _tokenStoreService = tokenStoreService;
            _tokenStoreService.CheckArgumentIsNull(nameof(_tokenStoreService));
        }

        public async Task ValidateAsync(TokenValidatedContext context)
        {
            var userPrincipal = context.Principal;

            var claimsIdentity = context.Principal.Identity as ClaimsIdentity;
            if (claimsIdentity?.Claims == null || !claimsIdentity.Claims.Any())
            {
                context.Fail("This is not our issued token. It has no claims.");
                return;
            }

            var serialNumberClaim = claimsIdentity.FindFirst(ClaimTypes.SerialNumber);
            if (serialNumberClaim == null)
            {
                context.Fail("This is not our issued token. It has no serial.");
                return;
            }

            var userIdString = claimsIdentity.FindFirst(ClaimTypes.UserData).Value;
            if (!int.TryParse(userIdString, out int userId))
            {
                context.Fail("This is not our issued token. It has no user-id.");
                return;
            }

            var user = await _usersService.FindUserAsync(userId).ConfigureAwait(false);
            if (user == null || user.SerialNumber != serialNumberClaim.Value || !user.IsActive)
            {
                // user has changed his/her password/roles/stat/IsActive
                context.Fail("This token is expired. Please login again.");
            }

            var accessToken = context.SecurityToken as JwtSecurityToken;
            if (accessToken == null || string.IsNullOrWhiteSpace(accessToken.RawData) ||
                !await _tokenStoreService.IsValidTokenAsync(accessToken.RawData, userId).ConfigureAwait(false))
            {
                context.Fail("This token is not in our database.");
                return;
            }

            await _usersService.UpdateUserLastActivityDateAsync(userId).ConfigureAwait(false);
        }
    }
در اینجا بررسی می‌کنیم:
- آیا توکن دریافتی به همراه Claims تنظیم شده‌ی درحین لاگین هست یا خیر؟
- آیا توکن دریافتی دارای یک Claim سفارشی به نام SerialNumber است؟ این SerialNumber معادل چنین فیلدی در جدول کاربران است.
- آیا توکن دریافتی دارای user-id است؟
- آیا کاربر یافت شده‌ی بر اساس این user-id هنوز فعال است و یا اطلاعات او تغییر نکرده‌است؟
- همچنین در آخر کار بررسی می‌کنیم که آیا اصل توکن دریافتی، در بانک اطلاعاتی ما پیشتر ثبت شده‌است یا خیر؟

اگر خیر، بلافاصله متد context.Fail فراخوانی شده و کار اعتبارسنجی را با اعلام شکست آن، به پایان می‌رسانیم.

در قسمت آخر، نیاز است اطلاعات توکن‌های صادر شده را ذخیره کنیم. به همین جهت نسبت به مطلب قبلی، جدول UserToken ذیل به برنامه اضافه شده‌است:
    public class UserToken
    {
        public int Id { get; set; }

        public string AccessTokenHash { get; set; }

        public DateTimeOffset AccessTokenExpiresDateTime { get; set; }

        public string RefreshTokenIdHash { get; set; }

        public DateTimeOffset RefreshTokenExpiresDateTime { get; set; }

        public int UserId { get; set; } // one-to-one association
        public virtual User User { get; set; }
    }
در اینجا هش‌های توکن‌های صادر شده‌ی توسط برنامه و طول عمر آن‌ها را ذخیره خواهیم کرد.
از اطلاعات آن در دو قسمت TokenValidatorService فوق و همچنین قسمت logout برنامه استفاده می‌کنیم. در سیستم JWT، مفهوم logout سمت سرور وجود خارجی ندارد. اما با ذخیره سازی هش توکن‌ها در بانک اطلاعاتی می‌توان لیستی از توکن‌های صادر شده‌ی توسط برنامه را تدارک دید. سپس در حین logout فقط کافی است tokenهای یک کاربر را حذف کرد. همینقدر سبب خواهد شد تا قسمت آخر TokenValidatorService با شکست مواجه شود؛ چون توکن ارسالی به سمت سرور دیگر در بانک اطلاعاتی وجود ندارد.


سرویس TokenStore

    public interface ITokenStoreService
    {
        Task AddUserTokenAsync(UserToken userToken);
        Task AddUserTokenAsync(
                User user, string refreshToken, string accessToken,
                DateTimeOffset refreshTokenExpiresDateTime, DateTimeOffset accessTokenExpiresDateTime);
        Task<bool> IsValidTokenAsync(string accessToken, int userId);
        Task DeleteExpiredTokensAsync();
        Task<UserToken> FindTokenAsync(string refreshToken);
        Task DeleteTokenAsync(string refreshToken);
        Task InvalidateUserTokensAsync(int userId);
        Task<(string accessToken, string refreshToken)> CreateJwtTokens(User user);
    }
در قسمت آخر اعتبارسنج سفارشی توکن، بررسی وجود توکن دریافتی، توسط سرویس TokenStore فوق صورت می‌گیرد. از این سرویس برای تولید، ذخیره سازی و حذف توکن‌ها استفاده خواهیم کرد.
پیاده سازی کامل این سرویس را در اینجا می‌توانید مشاهده کنید.


تولید Access Tokens و Refresh Tokens

پس از تنظیمات ابتدایی برنامه، اکنون می‌توانیم دو نوع توکن را تولید کنیم:

تولید Access Tokens
        private async Task<string> createAccessTokenAsync(User user, DateTime expires)
        {
            var claims = new List<Claim>
            {
                // Unique Id for all Jwt tokes
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                // Issuer
                new Claim(JwtRegisteredClaimNames.Iss, _configuration.Value.Issuer),
                // Issued at
                new Claim(JwtRegisteredClaimNames.Iat, DateTime.UtcNow.ToUnixEpochDate().ToString(), ClaimValueTypes.Integer64),
                new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
                new Claim(ClaimTypes.Name, user.Username),
                new Claim("DisplayName", user.DisplayName),
                // to invalidate the cookie
                new Claim(ClaimTypes.SerialNumber, user.SerialNumber),
                // custom data
                new Claim(ClaimTypes.UserData, user.Id.ToString())
            };

            // add roles
            var roles = await _rolesService.FindUserRolesAsync(user.Id).ConfigureAwait(false);
            foreach (var role in roles)
            {
                claims.Add(new Claim(ClaimTypes.Role, role.Name));
            }

            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.Value.Key));
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            var token = new JwtSecurityToken(
                issuer: _configuration.Value.Issuer,
                audience: _configuration.Value.Audience,
                claims: claims,
                notBefore: DateTime.UtcNow,
                expires: expires,
                signingCredentials: creds);
            return new JwtSecurityTokenHandler().WriteToken(token);
        }
این امکانات در اسمبلی زیر قرار دارند:
<ItemGroup>
   <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="2.0.0" />
</ItemGroup>
در اینجا ابتدا همانند کار با سیستم اعتبارسنجی مبتنی بر کوکی‌ها، نیاز است یک سری Claim تهیه شوند. به همین جهت SerialNumber، UserId و همچنین نقش‌های کاربر لاگین شده‌ی به سیستم را در اینجا به مجموعه‌ی Claims اضافه می‌کنیم. وجود این Claims است که سبب می‌شود فیلتر Authorize بتواند نقش‌ها را تشخیص داده و یا کاربر را اعتبارسنجی کند.
پس از تهیه‌ی Claims، اینبار بجای یک کوکی، یک JSON Web Toekn را توسط متد new JwtSecurityTokenHandler().WriteToken تهیه خواهیم کرد. این توکن حاوی Claims، به همراه اطلاعات طول عمر و امضای مرتبطی است.
حاصل آن نیز یک رشته‌است که دقیقا به همین فرمت به سمت کلاینت ارسال خواهد شد. البته ما در اینجا دو نوع توکن را به سمت کلاینت ارسال می‌کنیم:
        public async Task<(string accessToken, string refreshToken)> CreateJwtTokens(User user)
        {
            var now = DateTimeOffset.UtcNow;
            var accessTokenExpiresDateTime = now.AddMinutes(_configuration.Value.AccessTokenExpirationMinutes);
            var refreshTokenExpiresDateTime = now.AddMinutes(_configuration.Value.RefreshTokenExpirationMinutes);
            var accessToken = await createAccessTokenAsync(user, accessTokenExpiresDateTime.UtcDateTime).ConfigureAwait(false);
            var refreshToken = Guid.NewGuid().ToString().Replace("-", "");

            await AddUserTokenAsync(user, refreshToken, accessToken, refreshTokenExpiresDateTime, accessTokenExpiresDateTime).ConfigureAwait(false);
            await _uow.SaveChangesAsync().ConfigureAwait(false);

            return (accessToken, refreshToken);
        }
accessToken همان JSON Web Token اصلی است. refreshToken فقط یک Guid است. کار آن ساده سازی و به روز رسانی عملیات Login بدون ارائه‌ی نام کاربری و کلمه‌ی عبور است. به همین جهت است که نیاز داریم تا این اطلاعات را در سمت بانک اطلاعاتی برنامه نیز ذخیره کنیم. فرآیند اعتبارسنجی یک refreshToken بدون ذخیره سازی این Guid در بانک اطلاعاتی مسیر نیست که در اینجا در فیلد RefreshTokenIdHash جدول UserToken ذخیره می‌شود.
جهت بالا رفتن امنیت سیستم، این Guid را هش کرد و سپس این هش را در بانک اطلاعاتی ذخیره می‌کنیم. به این ترتیب دسترسی غیرمجاز به این هش‌ها، امکان بازیابی توکن‌های اصلی را غیرممکن می‌کند.


پیاده سازی Login

پس از پیاده سازی متد CreateJwtTokens، کار ورود به سیستم به سادگی ذیل خواهد بود:
        [AllowAnonymous]
        [HttpPost("[action]")]
        public async Task<IActionResult> Login([FromBody]  User loginUser)
        {
            if (loginUser == null)
            {
                return BadRequest("user is not set.");
            }

            var user = await _usersService.FindUserAsync(loginUser.Username, loginUser.Password).ConfigureAwait(false);
            if (user == null || !user.IsActive)
            {
                return Unauthorized();
            }

            var (accessToken, refreshToken) = await _tokenStoreService.CreateJwtTokens(user).ConfigureAwait(false);
            return Ok(new { access_token = accessToken, refresh_token = refreshToken });
        }
ابتدا بررسی می‌شود که آیا کلمه‌ی عبور و نام کاربری وارد شده صحیح هستند یا خیر و آیا کاربر متناظر با آن هنوز فعال است. اگر بله، دو توکن دسترسی و به روز رسانی را تولید و به سمت کلاینت ارسال می‌کنیم.


پیاده سازی Refresh Token

پیاده سازی توکن به روز رسانی همانند عملیات لاگین است:
        [AllowAnonymous]
        [HttpPost("[action]")]
        public async Task<IActionResult> RefreshToken([FromBody]JToken jsonBody)
        {
            var refreshToken = jsonBody.Value<string>("refreshToken");
            if (string.IsNullOrWhiteSpace(refreshToken))
            {
                return BadRequest("refreshToken is not set.");
            }

            var token = await _tokenStoreService.FindTokenAsync(refreshToken);
            if (token == null)
            {
                return Unauthorized();
            }

            var (accessToken, newRefreshToken) = await _tokenStoreService.CreateJwtTokens(token.User).ConfigureAwait(false);
            return Ok(new { access_token = accessToken, refresh_token = newRefreshToken });
        }
با این تفاوت که در اینجا فقط یک Guid از سمت کاربر دریافت شده، سپس بر اساس این Guid، توکن و کاربر متناظر با آن یافت می‌شوند. سپس یک توکن جدید را بر اساس این اطلاعات تولید کرده و به سمت کاربر ارسال می‌کنیم.


پیاده سازی Logout

در سیستم‌های مبتنی بر JWT، پیاده سازی Logout سمت سرور بی‌مفهوم است؛ از این جهت که تا زمان انقضای یک توکن می‌توان از آن توکن جهت ورود به سیستم و دسترسی به منابع آن استفاده کرد. بنابراین تنها راه پیاده سازی Logout، ذخیره سازی توکن‌ها در بانک اطلاعاتی و سپس حذف آن‌ها در حین خروج از سیستم است. به این ترتیب اعتبارسنج سفارشی توکن‌ها، از استفاده‌ی مجدد از توکنی که هنوز هم معتبر است و منقضی نشده‌است، جلوگیری خواهد کرد:
        [AllowAnonymous]
        [HttpGet("[action]"), HttpPost("[action]")]
        public async Task<bool> Logout()
        {
            var claimsIdentity = this.User.Identity as ClaimsIdentity;
            var userIdValue = claimsIdentity.FindFirst(ClaimTypes.UserData)?.Value;

            // The Jwt implementation does not support "revoke OAuth token" (logout) by design.
            // Delete the user's tokens from the database (revoke its bearer token)
            if (!string.IsNullOrWhiteSpace(userIdValue) && int.TryParse(userIdValue, out int userId))
            {
                await _tokenStoreService.InvalidateUserTokensAsync(userId).ConfigureAwait(false);
            }
            await _tokenStoreService.DeleteExpiredTokensAsync().ConfigureAwait(false);
            await _uow.SaveChangesAsync().ConfigureAwait(false);

            return true;
        }


آزمایش نهایی برنامه

در فایل index.html، نمونه‌ای از متدهای لاگین، خروج و فراخوانی اکشن متدهای محافظت شده را مشاهده می‌کنید. این روش برای برنامه‌های تک صفحه‌ای وب یا SPA نیز می‌تواند مفید باشد و به همین نحو کار می‌کنند.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید.
اشتراک‌ها
اضافه شدن پشتیبانی از رندر زبان‌های راست به چپ به ImageSharp.Drawing

در دات نت 6، فضای نام و بسته‌ی System.Drawing.Common صرفا مختص به ویندوز شده و دیگر برای سایر سیستم عامل‌ها توسعه داده نمی‌شود. بجای آن، بسته‌های چندسکویی دیگری را توصیه کرده‌اند که مهم‌ترین مشکل آن‌ها، عدم امکان رندر متون راست به چپ، توسط آن‌ها است ... این پشتیبانی اخیرا به ImageSharp.Drawing اضافه شده‌است.

اضافه شدن پشتیبانی از رندر زبان‌های راست به چپ به ImageSharp.Drawing
نظرات مطالب
رفع مشکل تبدیلات UTC با تغییر اطلاعات TimeZone ایران در ویندوز

یک نکته‌ی تکمیلی: اگر از کتابخانه‌ی DNTPersianUtils.Core استفاده می‌کنید، چون اطلاعات دقیق منطقه‌ی زمانی در یکسری از سیستم‌ عامل‌ها نه‌ به‌روز می‌شود و نه وجود دارد (خصوصا برای کم حجم‌تر شدن توزیع‌های ویژه‌ی لینوکسی)، کلاس IranTimeZoneInfo اختصاصی به آن اضافه شده و به صورت توکار از آن استفاده می‌کند. بنابراین دیگر مهم نیست که از چه سیستم عاملی استفاده می‌کنید و اینکه آیا به ‌روز است یا خیر.

نظرات مطالب
تبدیل صفحات یک فایل PDF به تصویر، با استفاده از Acrobat SDK
ghostscript روش چندسکویی تبدیل صفحات PDF به تصویر است. نسخه‌ی خط فرمان آن با ارسال چند پارامتر به آن در تمام سیستم عامل‌ها قابل استفاده‌است.
gs -dNOPAUSE -q -sDEVICE=pnggray -r500 -dBATCH -dFirstPage=2 -dLastPage=2 -sOutputFile=test.png test.pdf
نظرات مطالب
همه چیز در مورد CLR : قسمت اول
زبان‌های CLR همیشه این مزیت را داشته‌اند که اصول امنیتی چون DEP یا Data Execution Prevention و همچنین ASLR یا Address Space Layout Randomization در آن‌ها لحاظ شده باشد.
 DEP و ASLR مکانیزهای امنیتی سیستم عامل‌ها هستند و ربطی به CLR و زبان برنامه نویسی ندارند .  
اشتراک‌ها
Entity Framework 6.2 منتشر شد


- Reduce start up time by loading finished code first models from a persistent cache
- Fluent API to define indexes
- DbFunctions.Like() to enable writing LINQ queries that translate to LIKE in SQL
- Migrate.exe should support -script option
- EF6 does not work with primary key from sequence
- Update error numbers for SQL Azure Execution Strategy
- Bug: Retrying queries or SQL commands fails with “The SqlParameter is already contained by another SqlParameterCollection”
- Bug: Evaluation of DbQuery.ToString() frequently times out in the debugger

Entity Framework 6.2 منتشر شد
اشتراک‌ها
0.Visual Studio 2019 version 16.7 منتشر شد
حجم دانلود از نسخه 16.6.5 به نسخه 16.7.0 تقریبا 2.9GB
  • Fixed WMI Provider component installation failure.
  • Fixed an issue where users were not able to see test run progress by clicking the left button at the bottom of the bar.
  • Improved performance of discovering available Visual Studio Codespace billing plans.
  • When creating a Visual Studio Codespace, we changed the default suspend time from 30minutes to 3 hours.
  • Fix bug where the Git Changes window informs the user they have incoming/outgoing commits, when in fact they have none.
  • Improved stability of the Diagnostic Tools and Performance Profiler.
  • Addressed an issue where Azure Sphere Visual Studio extension will not be automatically updated due to a minor version mismatch. With the fix, Azure Sphere will be able to get automatically updated when the VSIX auto updater runs. 
0.Visual Studio 2019 version 16.7 منتشر شد
نظرات مطالب
Blazor 5x - قسمت دهم - مبانی Blazor - بخش 7 - مسیریابی
تعدادی پرسش و پاسخ تکمیلی مباحث مسیریابی

چگونه می‌توان DateTime را به عنوان پارامتر مسیریابی ارسال کرد؟
Blazor از پارامترهایی از نوع DateTime، در حین مسیریابی پشتیبانی می‌کند؛ با این شرط که قید datetime در حین مسیریابی صریحا ذکر شود:
@page "/route/{parameter:datetime}"
یک مثال: کامپوننت index.razor که سبب هدایت به سمت کامپوننت دیگری به همراه ارسال پارامتری از نوع DateTime می‌شود:
@page "/"
@inject NavigationManager NavManager
<button @onclick="CurrentTime">Current Time</button>
@code {
    public void CurrentTime()
    {
        NavManager.NavigateTo("/time/" + DateTime.Now);
    }
}
و کامپوننت Time.razor که قادر است این DateTime را دریافت کرده و به یک پارامتر، Bind کند:
@page "/time/{param:datetime}"
<h3>Time</h3>
<p>@Param</p>
@code {
    [Parameter]
    public DateTime Param { get; set; }
}


چگونه می‌توان به عنوان صفحه‌ی جاری دسترسی یافت؟
برای اینکار نیاز است از JavaScript interop استفاده کرد. ابتدا برای مثال تابع عمومی getTitle را که بر اساس DOM API مرورگر کار می‌کند، تهیه می‌کنیم:
window.getTitle = () => {
   return document.title;
};
سپس می‌توان از آن در یک کامپوننت Blazor به صورت زیر استفاده کرد:
@page "/"
@inject IJSRuntime jsRuntime
<h2>Page Title: @Title</h2>
<button class="btn btn-primary" @onclick="@GetTitle">Get Title</button>
@code {
public string Title = "";
    
    public async void GetTitle()
    {
            Title = await jsRuntime.InvokeAsync<string>("getTitle");
     }
}

چگونه می‌توان مسیری را از طریق کدهای برنامه در یک برگه‌ی مجزای مرورگر باز کرد؟
در اینجا نیز می‌توان با استفاده از JavaScript interop، متد استاندارد open مرورگر را فراخوانی کرد و پارامتر اول آن‌را به url مدنظر و پارامتر بعدی آن‌را به blank_ تنظیم کرد تا مرورگر آدرس درخواستی را در یک برگه‌ی جدید باز کند:
@inject IJSRuntime jsRuntime

<button @onclick="NavigateToNewTab">New Tab Navigation</button>
@code {
public async Task NavigateToNewTab()
    {
        string url = "/counter";
        await jsRuntime.InvokeAsync<object>("open", url, "_blank");
    }
}