نظرات مطالب
اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity
ذخیره سازی اصل توکن که در اینجا انجام شده، در ASP.NET Core Identity معادلی ندارد و جزو ابتکارات این پروژه است. هرچند پروژه‌ی Identity Server چیزی شبیه به آن‌را دارد: «امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت نهم- مدیریت طول عمر توکن‌ها»؛ تحت مفهوم «Reference Tokens».
نظرات مطالب
اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity
- مقدمه‌ی مطلب «پیاده سازی JSON Web Token با ASP.NET Web API 2.x» را مطالعه کنید.
- یک چنین کاری در Identity Server هم انجام می‌شود: «امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت نهم- مدیریت طول عمر توکن‌ها»؛ تحت مفهوم «Reference Tokens»:

نظرات مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت چهاردهم- آماده شدن برای انتشار برنامه
یک نکته‌ی تکمیلی
Identity Server به همراه یک Admin UI هم هست (برای مدیریت جداولی که در این قسمت اضافه شدند). این مورد تجاری است و حدود 500 یورو قیمت دارد. بجای آن می‌توان از پروژه‌ی skoruba / IdentityServer4.Admin نیز استفاده کرد (با مجوز MIT):

پ.ن.
اگر نیاز به پشتیبانی در مورد این سیستم ثالث دارید، لطفا به صفحه‌ی issue tracker آن مراجعه کنید.
نظرات نظرسنجی‌ها
ایجاد یک پیام رسان کدباز بومی با استقبال کاربران ایرانی روبرو میشود؟
این رفتار درستی نیست که سمت سرور هم باز باشه!

الان به طور مثال ما به اپ تلگرام با اینکه اپن سورس هست دسترسی داریم ولی نه کاملا چون فرضا بخش api‌های سمت سرور (api server) دست ما نیست و برای امنیت بیشتر تحت نظر خودشونه یا موارد دیگه که بازهم امکان دسترسی رو به ما نمیده مثل رابط‌های api‌های سمت سرور 
در کل به بهترین شیوه هم رمزگذاری بشه، اینکه سورس سمت سرور منتشر بشه بنده مخالفم
سمت سرور رو با تعدادی فرد خبره میشه خیلی عالی مدیریت کرد و نیازی به انتشار عمومی نیست!
مطالب
مهاجرت داده عضویت و پروفایل از Universal Providers به ASP.NET Identity
در این مقاله مهاجرت داده‌های سیستم عضویت، نقش‌ها و پروفایل‌های کاربران که توسط Universal Providers ساخته شده اند به مدل ASP.NET Identity را بررسی می‌کنیم. رویکردی که در این مقاله استفاده شده و قدم‌های لازمی که توضیح داده شده اند، برای اپلیکیشنی که با SQL Membership کار می‌کند هم می‌توانند کارساز باشند.

با انتشار Visual Studio 2013، تیم ASP.NET سیستم جدیدی با نام ASP.NET Identity معرفی کردند. می‌توانید  در این لینک  بیشتر درباره این انتشار بخوانید. 

در ادامه مقاله قبلی تحت عنوان  مهاجرت از SQL Membership به ASP.NET Identity ، در این پست به مهاجرت داده‌های یک اپلیکیشن که از مدل Providers برای مدیریت اطلاعات کاربران، نقش‌ها و پروفایل‌ها استفاده می‌کند به مدل جدید ASP.NET Identity می‌پردازیم. تمرکز این مقاله اساسا روی مهاجرت داده‌های پروفایل کاربران خواهد بود، تا بتوان به سرعت از آنها در اپلیکیشن استفاده کرد. مهاجرت داده‌های عضویت و نقش ها، شبیه پروسه مهاجرت SQL Membership است. رویکردی که در ادامه برای مهاجرت داده پروفایل‌ها دنبال شده است، می‌تواند برای اپلیکیشنی با SQL Membership نیز استفاده شود.

بعنوان یک مثال، با اپلیکیشن وبی شروع می‌کنیم که توسط Visual Studio 2012 ساخته شده و از مدل Providers استفاده می‌کند. پس از آن یک سری کد برای مدیریت پروفایل ها، ثبت نام کاربران، افزودن اطلاعات پروفایل به کاربران و مهاجرت الگوی دیتابیس می‌نویسیم و نهایتا اپلیکیشن را بروز رسانی می‌کنیم تا برای استفاده از سیستم Identity برای مدیریت کاربران و نقش‌ها آماده باشد. و بعنوان یک تست، کاربرانی که قبلا توسط Universal Providers ساخته شده اند باید بتوانند به سایت وارد شوند، و کاربران جدید هم باید قادر به ثبت نام در سایت باشند.

سورس کد کامل این مثال را می‌توانید از  این لینک  دریافت کنید.



خلاصه مهاجرت داده پروفایل ها

قبل از آنکه با مهاجرت‌ها شروع کنیم، بگذارید تا نگاهی به تجربه مان از ذخیره اطلاعات پروفایل‌ها در مدل Providers بیاندازیم. اطلاعات پروفایل کاربران یک اپلیکیشن به طرق مختلفی می‌تواند ذخیره شود. یکی از رایج‌ترین این راه ها، استفاده از تامین کننده‌های پیش فرضی است که بهمراه Universal Providers منتشر شدند. بدین منظور انجام مراحل زیر لازم است
  1. کلاس جدیدی بسازید که دارای خواصی برای ذخیره اطلاعات پروفایل است.
  2. کلاس جدیدی بسازید که از 'ProfileBase' ارث بری می‌کند و متدهای لازم برای دریافت پروفایل کاربران را پیاده سازی می‌کند.
  3. استفاده از تامین کننده‌های پیش فرض را، در فایل web.config فعال کنید. و کلاسی که در مرحله 2 ساختید را بعنوان کلاس پیش فرض برای خواندن اطلاعات پروفایل معرفی کنید.


اطلاعات پروفایل‌ها بصورت serialized xml و binary در جدول 'Profiles' ذخیره می‌شوند.


پس از آنکه به سیستم ASP.NET Identity مهاجرت کردیم، اطلاعات پروفایل  deserialized شده و در قالب خواص کلاس User ذخیره می‌شوند. هر خاصیت، بعدا می‌تواند به  یک ستون در دیتابیس متصل شود. مزیت بدست آمده این است که مستقیما از کلاس User به اطلاعات پروفایل دسترسی داریم. ناگفته نماند که دیگر داده‌ها serialize/deserialize هم نمی‌شوند.


شروع به کار

در Visual Studio 2012 پروژه جدیدی از نوع ASP.NET 4.5 Web Forms application بسازید. مثال جاری از یک قالب Web Forms استفاده می‌کند، اما می‌توانید از یک قالب MVC هم استفاده کنید.

پوشه جدیدی با نام 'Models' بسازید تا اطلاعات پروفایل را در آن قرار دهیم.

بعنوان یک مثال، بگذارید تا تاریخ تولد کاربر، شهر سکونت، قد و وزن او را در پروفایلش ذخیره کنیم. قد و وزن بصورت یک کلاس سفارشی (custom class) بنام 'PersonalStats' ذخیره می‌شوند. برای ذخیره و بازیابی پروفایل ها، به کلاسی احتیاج داریم که 'ProfileBase' را ارث بری می‌کند. پس کلاس جدیدی با نام 'AppProfile' بسازید.

public class ProfileInfo
{
    public ProfileInfo()
    {
        UserStats = new PersonalStats();
    }
    public DateTime? DateOfBirth { get; set; }
    public PersonalStats UserStats { get; set; }
    public string City { get; set; }
}

public class PersonalStats
{
    public int? Weight { get; set; }
    public int? Height { get; set; }
}

public class AppProfile : ProfileBase
{
    public ProfileInfo ProfileInfo
    {
        get { return (ProfileInfo)GetPropertyValue("ProfileInfo"); }
    }
    public static AppProfile GetProfile()
    {
        return (AppProfile)HttpContext.Current.Profile;
    }
    public static AppProfile GetProfile(string userName)
    {
        return (AppProfile)Create(userName);
    }
}

پروفایل را در فایل web.config خود فعال کنید. نام کلاسی را که در مرحله قبل ساختید، بعنوان کلاس پیش فرض برای ذخیره و بازیابی پروفایل‌ها معرفی کنید.

<profile defaultProvider="DefaultProfileProvider" enabled="true"
    inherits="UniversalProviders_ProfileMigrations.Models.AppProfile">
  <providers>
    .....
  </providers>
</profile>

برای دریافت اطلاعات پروفایل از کاربر، فرم وب جدیدی در پوشه Account بسازید و آنرا 'AddProfileData.aspx' نامگذاری کنید.

<h2> Add Profile Data for <%# User.Identity.Name %></h2>
<asp:Label Text="" ID="Result" runat="server" />
<div>
    Date of Birth:
    <asp:TextBox runat="server" ID="DateOfBirth"/>
</div>
<div>
    Weight:
    <asp:TextBox runat="server" ID="Weight"/>
</div>
<div>
    Height:
    <asp:TextBox runat="server" ID="Height"/>
</div>
<div>
    City:
    <asp:TextBox runat="server" ID="City"/>
</div>
<div>
    <asp:Button Text="Add Profile" ID="Add" OnClick="Add_Click" runat="server" />
</div>

کد زیر را هم به فایل code-behind اضافه کنید.

protected void Add_Click(object sender, EventArgs e)
{
    AppProfile profile = AppProfile.GetProfile(User.Identity.Name);
    profile.ProfileInfo.DateOfBirth = DateTime.Parse(DateOfBirth.Text);
    profile.ProfileInfo.UserStats.Weight = Int32.Parse(Weight.Text);
    profile.ProfileInfo.UserStats.Height = Int32.Parse(Height.Text);
    profile.ProfileInfo.City = City.Text;
    profile.Save();
}

دقت کنید که فضای نامی که کلاس AppProfile در آن قرار دارد را وارد کرده باشید.

اپلیکیشن را اجرا کنید و کاربر جدیدی با نام 'olduser' بسازید. به صفحه جدید 'AddProfileData' بروید و اطلاعات پروفایل کاربر را وارد کنید.

با استفاده از پنجره Server Explorer می‌توانید تایید کنید که اطلاعات پروفایل با فرمت xml در جدول 'Profiles' ذخیره می‌شوند.

مهاجرت الگوی دیتابیس

برای اینکه دیتابیس فعلی بتواند با سیستم ASP.NET Identity کار کند، باید الگوی ASP.NET Identity را بروز رسانی کنیم تا فیلدهای جدیدی که اضافه کردیم را هم در نظر بگیرد. این کار می‌تواند توسط اسکریپت‌های SQL انجام شود، باید جداول جدیدی بسازیم و اطلاعات موجود را به آنها انتقال دهیم. در پنجره 'Server Explorer' گره 'DefaultConnection' را باز کنید تا جداول لیست شوند. روی Tables کلیک راست کنید و 'New Query' را انتخاب کنید.

اسکریپت مورد نیاز را از آدرس https://raw.github.com/suhasj/UniversalProviders-Identity-Migrations/master/Migration.txt دریافت کرده و آن را اجرا کنید. اگر اتصال خود به دیتابیس را تازه کنید خواهید دید که جداول جدیدی اضافه شده اند. می‌توانید داده‌های این جداول را بررسی کنید تا ببینید چگونه اطلاعات منتقل شده اند.

مهاجرت اپلیکیشن برای استفاده از ASP.NET Identity

پکیج‌های مورد نیاز برای ASP.NET Identity را نصب کنید:
  • Microsoft.AspNet.Identity.EntityFramework
  • Microsoft.AspNet.Identity.Owin
  • Microsoft.Owin.Host.SystemWeb
  • Microsoft.Owin.Security.Facebook
  • Microsoft.Owin.Security.Google
  • Microsoft.Owin.Security.MicrosoftAccount
  • Microsoft.Owin.Security.Twitter
اطلاعات بیشتری درباره مدیریت پکیج‌های NuGet از اینجا قابل دسترسی هستند.

برای اینکه بتوانیم از الگوی جاری دیتابیس استفاده کنیم، ابتدا باید مدل‌های لازم ASP.NET Identity را تعریف کنیم تا موجودیت‌های دیتابیس را Map کنیم. طبق قرارداد سیستم Identity کلاس‌های مدل یا باید اینترفیس‌های تعریف شده در Identity.Core dll را پیاده سازی کنند، یا می‌توانند پیاده سازی‌های پیش فرضی را که در Microsoft.AspNet.Identity.EntityFramework وجود دارند گسترش دهند. ما برای نقش ها، اطلاعات ورود کاربران و claim‌ها از پیاده سازی‌های پیش فرض استفاده خواهیم کرد. نیاز به استفاده از یک کلاس سفارشی User داریم. پوشه جدیدی در پروژه با نام 'IdentityModels' بسازید. کلاسی با نام 'User' در این پوشه بسازید و کد آن را با لیست زیر تطابق دهید.
using Microsoft.AspNet.Identity.EntityFramework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using UniversalProviders_ProfileMigrations.Models;

namespace UniversalProviders_Identity_Migrations
{
    public class User : IdentityUser
    {
        public User()
        {
            CreateDate = DateTime.UtcNow;
            IsApproved = false;
            LastLoginDate = DateTime.UtcNow;
            LastActivityDate = DateTime.UtcNow;
            LastPasswordChangedDate = DateTime.UtcNow;
            Profile = new ProfileInfo();
        }

        public System.Guid ApplicationId { get; set; }
        public bool IsAnonymous { get; set; }
        public System.DateTime? LastActivityDate { get; set; }
        public string Email { get; set; }
        public string PasswordQuestion { get; set; }
        public string PasswordAnswer { get; set; }
        public bool IsApproved { get; set; }
        public bool IsLockedOut { get; set; }
        public System.DateTime? CreateDate { get; set; }
        public System.DateTime? LastLoginDate { get; set; }
        public System.DateTime? LastPasswordChangedDate { get; set; }
        public System.DateTime? LastLockoutDate { get; set; }
        public int FailedPasswordAttemptCount { get; set; }
        public System.DateTime? FailedPasswordAttemptWindowStart { get; set; }
        public int FailedPasswordAnswerAttemptCount { get; set; }
        public System.DateTime? FailedPasswordAnswerAttemptWindowStart { get; set; }
        public string Comment { get; set; }
        public ProfileInfo Profile { get; set; }
    }
}
دقت کنید که 'ProfileInfo' حالا بعنوان یک خاصیت روی کلاس User تعریف شده است. بنابراین می‌توانیم مستقیما از کلاس کاربر با اطلاعات پروفایل کار کنیم.

محتویات پوشه‌های IdentityModels  و IdentityAccount  را از آدرس  https://github.com/suhasj/UniversalProviders-Identity-Migrations/tree/master/UniversalProviders-Identity-Migrations  دریافت و کپی کنید. این فایل‌ها مابقی مدل ها، و صفحاتی برای مدیریت کاربران و نقش‌ها در سیستم جدید ASP.NET Identity هستند.


انتقال داده پروفایل‌ها به جداول جدید

همانطور که گفته شد ابتدا باید داده‌های پروفایل را deserialize کرده و از فرمت xml خارج کنیم، سپس آنها را در ستون‌های جدول AspNetUsers ذخیره کنیم. ستون‌های جدید در مرحله قبل به دیتابیس اضافه شدند، پس تنها کاری که باقی مانده پر کردن این ستون‌ها با داده‌های ضروری است. بدین منظور ما از یک اپلیکیشن کنسول استفاده می‌کنیم که تنها یک بار اجرا خواهد شد، و ستون‌های جدید را با داده‌های لازم پر می‌کند.

در solution جاری یک پروژه اپلیکیشن کنسول بسازید.

آخرین نسخه پکیج Entity Framework را نصب کنید. همچنین یک رفرنس به اپلیکیشن وب پروژه بدهید (کلیک راست روی پروژه و گزینه 'Add Reference').

کد زیر را در کلاس Program.cs وارد کنید. این قطعه کد پروفایل تک تک کاربران را می‌خواند و در قالب 'ProfileInfo' آنها را serialize می‌کند و در دیتابیس ذخیره می‌کند.

public class Program
{
    var dbContext = new ApplicationDbContext();
    foreach (var profile in dbContext.Profiles)
    {
        var stringId = profile.UserId.ToString();
        var user = dbContext.Users.Where(x => x.Id == stringId).FirstOrDefault();
        Console.WriteLine("Adding Profile for user:" + user.UserName);
        var serializer = new XmlSerializer(typeof(ProfileInfo));
        var stringReader = new StringReader(profile.PropertyValueStrings);
        var profileData = serializer.Deserialize(stringReader) as ProfileInfo;
        if (profileData == null)
        {
            Console.WriteLine("Profile data deserialization error for user:" + user.UserName);
        }
        else
        {
            user.Profile = profileData;
        }
    }
    dbContext.SaveChanges();
}

برخی از مدل‌های استفاده شده در پوشه 'IdentityModels' تعریف شده اند که در پروژه اپلیکیشن وبمان قرار دارند، بنابراین افزودن فضاهای نام مورد نیاز فراموش نشود.


کد بالا روی دیتابیسی که در پوشه App_Data وجود دارد کار می‌کند، این دیتابیس در مراحل قبلی در اپلیکیشن وب پروژه ایجاد شد. برای اینکه این دیتابیس را رفرنس کنیم باید رشته اتصال فایل app.config اپلیکیشن کنسول را بروز رسانی کنید. از همان رشته اتصال web.config در اپلیکیشن وب پروژه استفاده کنید. همچنین آدرس فیزیکی کامل را در خاصیت 'AttachDbFilename' وارد کنید.


یک Command Prompt باز کنید و به پوشه bin اپلیکیشن کنسول بالا بروید. فایل اجرایی را اجرا کنید و نتیجه را مانند تصویر زیر بررسی کنید.

در پنجره Server Explorer جدول 'AspNetUsers' را باز کنید. حال ستون‌های این جدول باید خواص کلاس مدل را منعکس کنند.


کارایی سیستم را تایید کنید

با استفاده از صفحات جدیدی که برای کار با ASP.NET Identity پیاده سازی شده اند سیستم را تست کنید. با کاربران قدیمی که در دیتابیس قبلی وجود دارند وارد شوید. کاربران باید با همان اطلاعات پیشین بتوانند وارد سیستم شوند. مابقی قابلیت‌ها را هم بررسی کنید. مثلا افزودن OAuth، ثبت کاربر جدید، تغییر کلمه عبور، افزودن نقش ها، تخصیص کاربران به نقش‌ها و غیره.

داده‌های پروفایل کاربران قدیمی و جدید همگی باید در جدول کاربران ذخیره شده و بازیابی شوند. جدول قبلی دیگر نباید رفرنس شود.
مطالب
ارسال خطاهای رخ‌داده‌ی در برنامه‌های سمت کلاینت Blazor WASM، به تلگرام
هر زمانیکه در سمت کلاینت، استثناء یا خطایی رخ می‌دهد، کاربر با نوار زرد رنگی در پایین صفحه، از آن مطلع می‌شود؛ اما برنامه نویس چطور؟! به همین جهت در این مطلب قصد داریم تمام خطاهای رخ داده‌ی در برنامه‌ی سمت کلاینت را لاگ کرده و به سرور تلگرام ارسال کنیم. مزیت کار کردن با تلگرام، دسترسی به سروری است که تقریبا همواره در دسترس است و برخلاف بانک اطلاعاتی برنامه که ممکن است در لحظه‌ی بروز خطا، خودش سبب ساز اصلی باشد و قادر به ثبت اطلاعات خطاهای رسیده‌ی از سمت کلاینت نباشد، چنین مشکلی را با تلگرام نداریم (مانند همان جمله‌ی معروف: «بک‌آپ سروری که روی همان سرور گرفته می‌شود، بک آپ نام ندارد!»). همچنین بررسی و حذف گزارش‌های رسیده‌ی به آن نیز بسیار ساده‌است و می‌توان این گزارش‌ها را مستقل از سرور برنامه و از طریق وسایل مختلفی مانند گوشی‌های همراه، تبلت‌ها و غیره نیز بررسی کرد.




نحوه‌ی نمایش خطاها در برنامه‌های Blazor

در حین توسعه‌ی برنامه‌های Blazor، اگر استثنائی رخ دهد، نوار زرد رنگی در پایین صفحه، ظاهر می‌شود که امکان هدایت توسعه دهنده را به کنسول مرورگر، برای مشاهده‌ی جزئیات بیشتر آن خطا را دارد. در حالت توزیع برنامه، این نوار زرد رنگ تنها به ذکر خطایی رخ داده‌است اکتفا کرده و گزینه‌ی راه اندازی مجدد برنامه را با ریفرش کردن مرورگر، پیشنهاد می‌دهد. سفارشی سازی آن هم در فایل wwwroot/index.html در قسمت زیر صورت می‌گیرد:
<div id="blazor-error-ui">
    An unhandled error has occurred.
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>
که شیوه نامه‌های پیش‌فرض آن در فایل wwwroot/css/app.css قرار دارند. در حالت عادی المان blazor-error-ui به همراه یک display: none است که از نمایش آن جلوگیری می‌کند. اما در زمان بروز خطایی، فریم‌ورک آن‌را به صورت display: block نمایش می‌دهد.


نحوه‌ی مدیریت استثناءها در برنامه‌های Blazor

توصیه شده‌است که کار مدیریت استثناءها باید توسط توسعه دهنده صورت گیرد و بهتر است جزئیات آن‌ها و یا stack-trace آن‌ها را به کاربر نمایش نداد؛ تا مبادا اطلاعات حساسی فاش شوند و یا کاربر مهاجم بتواند توسط آن‌ها اطلاعات ارزشمندی را از نحوه‌ی عملکرد برنامه بدست آورد.
برخلاف برنامه‌های ASP.NET Core که دارای یک middleware pipeline هستند و برای مثال توسط آن‌ها می‌توان مدیریت سراسری خطاهای رخ‌داده را انجام داد، چنین ویژگی در برنامه‌های Blazor وجود ندارد؛ چون در اینجا مرورگر است که هاست برنامه بوده و processing pipeline آن‌را تشکیل می‌دهد.
اما ... اگر استثنائی مدیریت نشده در یک برنامه‌ی Blazor رخ‌دهد، این استثناء در ابتدا توسط یک ILogger، لاگ شده و سپس در کنسول مرورگر نمایش داده می‌شود. در اینجا Console Logging Provider، تامین کننده‌ی پیش‌فرض سیستم ثبت وقایع برنامه‌های Blazor است. به همین جهت استثناءهای مدیریت نشده‌ی برنامه را می‌توان در کنسول توسعه دهندگان مرورگر نیز مشاهده کرد. برای مثال اگر سطح لاگ ارائه شده LogLevel.Error باشد، به صورت خودکار به معادل console.error ترجمه می‌شود.
بنابراین اگر در برنامه‌ی Blazor جاری یک ILoggerProvider سفارشی را تهیه و آن‌را به سیستم تزریق وابستگی‌های برنامه معرفی کنیم، می‌توان از تمام وقایع سیستم (هر قسمتی از آن که از ILogger استفاده می‌کند)، منجمله تمام خطاهای رخ‌داده (و مدیریت نشده) مطلع شد و برای مثال آن‌ها را به سمت Web API برنامه، جهت ثبت در بانک اطلاعاتی و یا نمایش در برنامه‌ی تلگرام، ارسال کرد و این دقیقا همان کاری است که قصد داریم در ادامه انجام دهیم.


نوشتن یک ILoggerProvider سفارشی جهت ارسال رخ‌دادها برنامه‌ی سمت کلاینت، به یک Web API

برای ارسال تمام وقایع برنامه‌ی کلاینت به سمت سرور، نیاز است یک ILoggerProvider سفارشی را تهیه کنیم که شروع آن به صورت زیر است:
using System;
using System.Net.Http;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace BlazorWasmTelegramLogger.Client.Logging
{
    public class ClientLoggerProvider : ILoggerProvider
    {
        private readonly HttpClient _httpClient;
        private readonly WebApiLoggerOptions _options;
        private readonly NavigationManager _navigationManager;

        public ClientLoggerProvider(
                IServiceProvider serviceProvider,
                IOptions<WebApiLoggerOptions> options,
                NavigationManager navigationManager)
        {
            if (serviceProvider is null)
            {
                throw new ArgumentNullException(nameof(serviceProvider));
            }

            if (options is null)
            {
                throw new ArgumentNullException(nameof(options));
            }

            _httpClient = serviceProvider.CreateScope().ServiceProvider.GetRequiredService<HttpClient>();
            _options = options.Value;
            _navigationManager = navigationManager ?? throw new ArgumentNullException(nameof(navigationManager));
        }

        public ILogger CreateLogger(string categoryName)
        {
            return new WebApiLogger(_httpClient, _options, _navigationManager);
        }

        public void Dispose()
        {
        }
    }
}
توضیحات:
زمانیکه قرار است یک لاگر سفارشی را به سیستم تزریق وابستگی‌های برنامه معرفی کنیم، روش آن به صورت زیر است:
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace BlazorWasmTelegramLogger.Client.Logging
{
    public static class ClientLoggerProviderExtensions
    {
        public static ILoggingBuilder AddWebApiLogger(this ILoggingBuilder builder)
        {
            if (builder == null)
            {
                throw new ArgumentNullException(nameof(builder));
            }

            builder.Services.AddSingleton<ILoggerProvider, ClientLoggerProvider>();
            return builder;
        }
    }
}
باید کلاسی را داشته باشیم مانند ClientLoggerProvider که یک ILoggerProvider را پیاده سازی می‌کند و نحوه‌ی ثبت آن نیز باید حتما Singleton باشد. مزیت معرفی ILoggerProvider به این نحو، امکان دسترسی به سرویس‌های برنامه در سازنده‌ی کلاس ClientLoggerProvider است و در این حالت دیگر نیاز به نوشتن new ClientLoggerProvider نبوده و خود سیستم تزریق وابستگی‌ها، سازنده‌های ClientLoggerProvider را تامین می‌کند.
در کلاس ClientLoggerProvider فوق، سه وابستگی تزریق شده را مشاهده می‌کنید:
public ClientLoggerProvider(
                IServiceProvider serviceProvider,
                IOptions<WebApiLoggerOptions> options,
                NavigationManager navigationManager)
با استفاده از IServiceProvider می‌توان به HttpClient برنامه دسترسی یافت. از این جهت که چون HttpClient به صورت پیش‌فرض با طول عمر Scoped به سیستم معرفی شده، امکان تزریق مستقیم آن به سازنده‌ی یک ILoggerProvider از نوع Singleton وجود ندارد. به همین جهت از IServiceProvider برای تامین آن استفاده خواهیم کرد. مابقی موارد مانند IOptions که تنظیمات این لاگر را فراهم می‌کند و یا NavigationManager استاندارد برنامه که امکان دسترسی به Url جاری را میسر می‌کند، به صورت پیش‌فرض دارای طول عمر Singleton هستند و می‌توان آن‌ها را بدون مشکل، به سازنده‌ی لاگر سفارشی، تزریق کرد.
مهم‌ترین قسمت ILoggerProvider سفارشی، متد CreateLogger آن است که یک ILogger را بازگشت می‌دهد:
public ILogger CreateLogger(string categoryName)
{
   return new WebApiLogger(_httpClient, _options, _navigationManager);
}
بنابراین در ادامه نیاز است، یک ILogger سفارشی را نیز پیاده سازی کنیم:
using System;
using System.Net.Http;
using System.Net.Http.Json;
using BlazorWasmTelegramLogger.Shared;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;

namespace BlazorWasmTelegramLogger.Client.Logging
{
    public class WebApiLogger : ILogger
    {
        private readonly WebApiLoggerOptions _options;
        private readonly HttpClient _httpClient;
        private readonly NavigationManager _navigationManager;

        public WebApiLogger(HttpClient httpClient, WebApiLoggerOptions options, NavigationManager navigationManager)
        {
            _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
            _options = options ?? throw new ArgumentNullException(nameof(options));
            _navigationManager = navigationManager ?? throw new ArgumentNullException(nameof(navigationManager));
        }

        public IDisposable BeginScope<TState>(TState state) => default;

        public bool IsEnabled(LogLevel logLevel) => logLevel >= _options.LogLevel;

        public void Log<TState>(
            LogLevel logLevel,
            EventId eventId,
            TState state,
            Exception exception,
            Func<TState, Exception, string> formatter)
        {
            if (!IsEnabled(logLevel))
            {
                return;
            }

            if (formatter is null)
            {
                throw new ArgumentNullException(nameof(formatter));
            }

            try
            {
                ClientLog log = new()
                {
                    LogLevel = logLevel,
                    EventId = eventId,
                    Message = formatter(state, exception),
                    Exception = exception?.Message,
                    StackTrace = exception?.StackTrace,
                    Url = _navigationManager.Uri
                };
                _httpClient.PostAsJsonAsync(_options.LoggerEndpointUrl, log);
            }
            catch
            {
                // don't throw exceptions from the logger
            }
        }
    }
}
نحوه‌ی عملکرد این ILogger سفارشی بسیار ساده‌است:
- متد IsEnabled آن مشخص می‌کند که چه سطحی از رخ‌دادهای سیستم را باید لاگ کند. این سطح را نیز از تنظیمات برنامه دریافت می‌کند:
using Microsoft.Extensions.Logging;

namespace BlazorWasmTelegramLogger.Client.Logging
{
    public class WebApiLoggerOptions
    {
        public string LoggerEndpointUrl { set; get; }

        public LogLevel LogLevel { get; set; } = LogLevel.Information;
    }
}
در این تنظیمات مشخص می‌کنیم که Url مربوط به اکشن متد Web API ما که قرار است اطلاعات به سمت آن ارسال شوند، چیست؟ همچنین حداقل سطح لاگ مدنظر را نیز باید مشخص کنیم. اطلاعات آن توسط فایل Client\wwwroot\appsettings.json با این محتوای فرضی قابل تنظیم است:
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "WebApiLogger": {
    "LogLevel": "Warning",
    "LoggerEndpointUrl": "/api/logs"
  }
}
و همچنین باید کلاس WebApiLoggerOptions را به نحو زیر در کلاس Program برنامه به سیستم تزریق وابستگی‌ها، معرفی کرد تا <IOptions<WebApiLoggerOptions قابلیت تزریق به سازنده‌ی تامین کننده‌ی لاگر را پیدا کند:
namespace BlazorWasmTelegramLogger.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("#app");

            builder.Services.Configure<WebApiLoggerOptions>(options => builder.Configuration.GetSection("WebApiLogger").Bind(options));
            // …
        }
    }
}
- متد لاگ این لاگر سفارشی، پیام نهایی قابل ارسال به سمت Web API را تشکیل داده و توسط متد httpClient.PostAsJsonAsync آن‌را ارسال می‌کند. به همین جهت ساختار لاگ مدنظر را در فایل Shared\ClientLog.cs به صورت زیر تعریف کرده‌ایم که بین برنامه‌ی کلاینت و سرور، مشترک است:
using Microsoft.Extensions.Logging;

namespace BlazorWasmTelegramLogger.Shared
{
    public class ClientLog
    {
        public LogLevel LogLevel { get; set; }

        public EventId EventId { get; set; }

        public string Message { get; set; }

        public string Exception { get; set; }

        public string StackTrace { get; set; }

        public string Url { get; set; }
    }
}
این اطلاعاتی است که کلاینت به ازای رخ‌دادی خاص، جمع آوری کرده و به سمت سرور ارسال می‌کند.

در آخر هم کار ثبت متد ()AddWebApiLogger که معرفی ILoggerProvider سفارشی ما را انجام می‌دهد، به صورت زیر خواهد بود:
namespace BlazorWasmTelegramLogger.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("#app");

            builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

            builder.Services.Configure<WebApiLoggerOptions>(options => builder.Configuration.GetSection("WebApiLogger").Bind(options));
            builder.Services.AddLogging(configure =>
            {
                configure.AddWebApiLogger();
            });

            await builder.Build().RunAsync();
        }
    }
}
تا اینجا اگر هر نوع استثنای مدیریت نشده‌ای در برنامه‌ی Blazor WASM رخ دهد، چون سطح لاگ آن بالاتر از Warning تنظیم شده‌ی در فایل Client\wwwroot\appsettings.json است:
public bool IsEnabled(LogLevel logLevel) => logLevel >= _options.LogLevel;
به صورت خودکار به سمت کنترلر api/logs ارسال خواهد شد. بنابراین مرحله‌ی بعدی، تکمیل کنترلر یاد شده‌است.


ایجاد سرویسی برای ارسال لاگ‌های برنامه به سمت تلگرام

پیش از اینکه کار تکمیل کنترلر api/logs را در برنامه‌ی Web API انجام دهیم، ابتدا در همان برنامه‌ی Web API، سرویسی را برای ارسال لاگ‌های رسیده به سمت تلگرام، تهیه می‌کنیم. علت اینکه این قسمت را به برنامه‌ی سمت سرور محول کرده‌ایم، شامل موارد زیر است:
- درست است که می‌توان کتابخانه‌های مرتبط با تلگرام را به برنامه‌ی سی‌شارپی Blazor خود اضافه کرد، اما هر وابستگی سمت کلاینتی، سبب حجیم‌تر شدن توزیع نهایی برنامه خواهد شد که مطلوب نیست.
- برای کار با تلگرام نیاز است توکن اتصال به آن‌را در یک محل امن، نگهداری کرد. قرار دادن این نوع اطلاعات حساس، در برنامه‌ی سمت کلاینتی که تمام اجزای آن از مرورگر قابل استخراج و بررسی است، کار اشتباهی است.
- ارسال اطلاعات لاگ برنامه‌ی سمت کلاینت به Web API، مزیت لاگ سمت سرور آن‌را مانند ثبت در یک فایل محلی، ثبت در بانک اطلاعاتی و غیره را نیز میسر می‌کند و صرفا محدود به تلگرام نیست.

برای ارسال اطلاعات به تلگرام، سرویس سمت سرور زیر را تهیه می‌کنیم:
using System;
using System.Text;
using System.Threading.Tasks;
using BlazorWasmTelegramLogger.Shared;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Telegram.Bot;
using Telegram.Bot.Types.Enums;

namespace BlazorWasmTelegramLogger.Server.Services
{
    public class TelegramLoggingBotOptions
    {
        public string AccessToken { get; set; }
        public string ChatId { get; set; }
    }

    public interface ITelegramBotService
    {
        Task SendLogAsync(ClientLog log);
    }

    public class TelegramBotService : ITelegramBotService
    {
        private readonly string _chatId;
        private readonly TelegramBotClient _client;

        public TelegramBotService(IOptions<TelegramLoggingBotOptions> options)
        {
            _chatId = options.Value.ChatId;
            _client = new TelegramBotClient(options.Value.AccessToken);
        }

        public async Task SendLogAsync(ClientLog log)
        {
            var text = formatMessage(log);
            if (string.IsNullOrWhiteSpace(text))
            {
                return;
            }

            await _client.SendTextMessageAsync(_chatId, text, ParseMode.Markdown);
        }

        private static string formatMessage(ClientLog log)
        {
            if (string.IsNullOrWhiteSpace(log.Message))
            {
                return string.Empty;
            }

            var sb = new StringBuilder();
            sb.Append(toEmoji(log.LogLevel))
                .Append(" *")
                .AppendFormat("{0:hh:mm:ss}", DateTime.Now)
                .Append("* ")
                .AppendLine(log.Message);

            if (!string.IsNullOrWhiteSpace(log.Exception))
            {
                sb.AppendLine()
                    .Append('`')
                    .AppendLine(log.Exception)
                    .AppendLine(log.StackTrace)
                    .AppendLine("`")
                    .AppendLine();
            }

            sb.Append("*Url:* ").AppendLine(log.Url);
            return sb.ToString();
        }

        private static string toEmoji(LogLevel level) =>
            level switch
            {
                LogLevel.Trace => "⬜️",
                LogLevel.Debug => "🟦",
                LogLevel.Information => "⬛️️️",
                LogLevel.Warning => "🟧",
                LogLevel.Error => "🟥",
                LogLevel.Critical => "❌",
                LogLevel.None => "🔳",
                _ => throw new ArgumentOutOfRangeException(nameof(level), level, null)
            };
    }
}
توضیحات:
- برای کار با API تلگرام، از کتابخانه‌ی معروف Telegram.Bot استفاده کرده‌ایم که به صورت زیر، وابستگی آن به برنامه‌ی Web API اضافه می‌شود:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <ItemGroup>
    <PackageReference Include="Telegram.Bot" Version="15.7.1" />
  </ItemGroup>
</Project>
- این سرویس برای کار کردن، نیاز به تنظیمات زیر را دارد:
    public class TelegramLoggingBotOptions
    {
        public string AccessToken { get; set; }
        public string ChatId { get; set; }
    }
- برای دریافت AccessToken، در برنامه‌ی تلگرام خود، بات مخصوصی را به نام https://t.me/botfather یافته و سپس آن‌را استارت کنید:


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

- مرحله‌ی بعد تنظیم ChatId است. نحوه‌ی کار برنامه به این صورت است که پیام‌ها را به این بات سفارشی خود ارسال کرده و این بات، آن‌ها را به کانال اختصاصی ما هدایت می‌کند. بنابراین یک کانال جدید را ایجاد کنید. ترجیحا بهتر است این کانال خصوصی باشد. سپس کاربر test_2021_logs_bot@ (همان نام منحصربفرد بات که حتما باید با @ شروع شود) را به عنوان عضو جدید کانال خود اضافه کنید. در اینجا عنوان می‌کند که این کاربر چون بات است، باید دسترسی ادمین را داشته باشد که دقیقا این دسترسی را نیز باید برقرار کنید تا بتوان توسط این بات، پیامی را به کانال اختصاصی خود ارسال کرد.
بنابراین تا اینجا یک کانال خصوصی را ایجاد کرده‌ایم که بات جدید test_2021_logs_bot@ عضو با دسترسی ادمین آن است. اکنون باید Id این کانال را بیابیم. برای اینکار بات دیگری را به نام JsonDumpBot@ یافته و استارت کنید. سپس در کانال خود یک پیام آزمایشی جدید را ارسال کنید و در ادامه این پیام را به بات JsonDumpBot@ ارسال کنید (forward کنید). همان لحظه‌ای که کار ارسال پیام به این بات صورت گرفت، Id کانال خود را در پاسخ آن می‌توانید مشاهده کنید:


در این تصویر مقدار forward_from_chat:id همان ChatId تنظیمات برنامه‌ی شما است.

در آخر این اطلاعات را در فایل Server\appsettings.json قرار می‌دهیم:
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "TelegramLoggingBot": {
    "AccessToken": "1826…",
    "ChatId": "-1001…" 
  }
}
که نحوه‌ی ثبت و معرفی آن‌ها به سیستم تزریق وابستگی‌های برنامه‌ی Web API، به صورت زیر است:
namespace BlazorWasmTelegramLogger.Server
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<TelegramLoggingBotOptions>(options =>
                            Configuration.GetSection("TelegramLoggingBot").Bind(options));
            services.AddSingleton<ITelegramBotService, TelegramBotService>();

            // ...
        }

        // ...
    }
}
سرویس ITelegramBotService را با طول عمر Singleton معرفی کرده‌ایم. چون new TelegramBotClient ای که در سازنده‌ی آن صورت می‌گیرد:
    public class TelegramBotService : ITelegramBotService
    {
        private readonly string _chatId;
        private readonly TelegramBotClient _client;

        public TelegramBotService(IOptions<TelegramLoggingBotOptions> options)
        {
            _chatId = options.Value.ChatId;
            _client = new TelegramBotClient(options.Value.AccessToken);
        }
باید فقط یکبار در طول عمر برنامه انجام شود و از این پس، هر بار که متد client.SendTextMessageAsync_ آن فراخوانی می‌گردد، پیامی به سمت بات و سپس کانال اختصاصی ما ارسال می‌شود.


ایجاد کنترلر Logs، جهت دریافت لاگ‌های رسیده‌ی از سمت کلاینت

مرحله‌ی آخر کار بسیار ساده‌است. سرویس تکمیل شده‌ی ITelegramBotService را به سازنده‌ی کنترلر Logs تزریق کرده و سپس متد SendLogAsync آن‌را فراخوانی می‌کنیم تا لاگی را که از کلاینت دریافت کرده، به سمت تلگرام هدایت کند:
using System;
using System.Threading.Tasks;
using BlazorWasmTelegramLogger.Server.Services;
using BlazorWasmTelegramLogger.Shared;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace BlazorWasmTelegramLogger.Server.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class LogsController : ControllerBase
    {
        private readonly ILogger<LogsController> _logger;
        private readonly ITelegramBotService _telegramBotService;

        public LogsController(ILogger<LogsController> logger, ITelegramBotService telegramBotService)
        {
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
            _telegramBotService = telegramBotService;
        }

        [HttpPost]
        public async Task<IActionResult> PostLog(ClientLog log)
        {
            // TODO: Save the client's `log` in the database

            _logger.Log(log.LogLevel, log.EventId, log.Url + Environment.NewLine + log.Message);

            await _telegramBotService.SendLogAsync(log);

            return Ok();
        }
    }
}


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

برای آزمایش برنامه، برای مثال در فایل Client\Pages\Counter.razor یک استثنای عمدی مدیریت نشده را قرار داده‌ایم:
@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;

        throw new InvalidOperationException("This is an exception message from the client!");
    }
}
اکنون اگر برنامه را اجرا کرده و سپس بر روی دکمه‌ی شمارشگر کلیک کنیم، همان تصویر ابتدای مطلب را که حاصل از ارسال جزئیات این استثنای مدیریت نشده به سمت تلگرام است، مشاهده خواهیم کرد.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: BlazorWasmTelegramLogger.zip
اشتراک‌ها
نصب و پیکربندی Microsoft Team Foundation Server 2015
در کار تیمی بر روی پروژه‌های بزرگ ، نرم افزار‌هایی همچون Microsoft Team Foundation Server و ... باعث شده که خیلی از دغدغه‌های تیم‌های توسعه دهنده  نرم افزار برطرف شود . 

برای نصب و پیکربندی :
روش اول : اگر دسترسی به سرور ندارید ، مایکروسافت  برای توسعه دهندگان Net. ،  روی سرور‌های خود Microsoft Team Foundation Server را پیاده کرده است و در این روش ما به ویژوال استودیو  و به یک حساب کاربری مایکروسافت نیاز داریم و با مراجعه به سایت www.visualstudio.com وارد حساب کاربری مایکروسافت خود شده و بر روی Create Account کلیک کرده و اطلاعات مورد نیاز را وارد می‌کنیم .در مرحله بعد طبق تصویر زیر اطلاعات پروژه ای که قرار است توسعه داده شود را وارد می‌کنیم .

که وارد قسمت مدیریتی می‌شویم طبق تصویر زیر :

 در مرحله بعد با مراجعه به ویژوال استودیو پروژه خود را با ویژوال استودیو ایجاد می‌کنیم و پروژه خود را به Team Foundation Serve مایکروسافت متصل خواهیم کرد . با کلیک بر روی Team از منوی ویژوال استودیو Manage Connections را انتخاب کرده و طبق تصویر زیر اطلاعات در خواستی را وارد می‌کنیم و بر روی Connect کلیک می‌کنیم


در مرحله بعد بر روی Solution پروژه راست کلیک کرده و Add Solution to Source Control را انتخاب میکنیم و با مشاهده پنجره Add Solution to Source Control بر روی ok کلیک کرده و با مراجعه به Solution Explorer پروژه مشاهده می‌کنیم که علامت + سبز رنگ در کنار Item‌های پروژه مشاهد می‌شود و این نشان دهنده این می‌باشد که پروژه ما  هنوز به Team Foundation Server مایکروسافت   منتقل نشده است . و برای انتقال بر روی Solution  پروژه راست کلیلک کرده و ..Check In را انتخاب می‌کنیم و در قسمت Comment می‌توانیم توضیحاتی را اضافه کرده و دوباره بر روی Check کلیک کرده و Item‌های پروژه ما منتقل سرور مایکروسافت می‌شود . 

روش دوم : 

در این روش ما نیاز به Windows Server 2012 R2  و  نرم افزار 2015 Microsoft Team Foundation Server  داریم و مراحل را در این روش طبق مقاله زیر جلو می‌بریم که می‌توانید به مراجعه به لینک زیر به مقاله دسترسی داشته باشید . 

Benday TFS 2015 Install guide v1.0

Team Foundation Server 2015 (TFS2015) Installation Guide

  
نصب و پیکربندی Microsoft Team Foundation Server 2015
اشتراک‌ها
امکان استفاده از کامپوننت‌های Blazor در برنامه‌های SPA مانند React و Angular در دات نت 6

Once created, these custom elements -- a custom counter, for example -- can also be used in other single-page application (SPA) web frameworks such as React and Angular. A sample project, aptly titled Blazor Custom Elements, shows how to do just that, providing examples about how to work with those frameworks and the client-side Blazor WebAssembly component as well as Blazor Server.  

امکان استفاده از کامپوننت‌های Blazor در برنامه‌های SPA مانند React و Angular در دات نت 6
اشتراک‌ها
اورلیا به صورت عملی

This books shows all the steps necessary for the development of SPA (Single Page Application) applications with the Aurelia. With this book you will learn:

  • How to create a project in Aurelia
  • How to create Aurelia plugin
  • Understand how the SystemJS imports works
  • Using HTTP to connect to a server
  • HttpClient
  • FetchClient
  • How to use Firebase
  • Offline the app data
  • Working with loopback
  • ... 
اورلیا به صورت عملی