مطالب
بررسی چند کتابخانه آپلود با پشتیبانی از DragDrop
برای یکی از پروژه‌ها نیاز به یک آپلودر داشتم که قابلیت  Drag&Drop را نیز داشته باشد و در ضمن پیاده سازی آسانی هم داشته باشد. در این بین به تعدادی از کتابخانه‌های جی کوئری می‌پردازیم.
FileDrop
اولین کتابخانه‌ای که با آن آشنا شدم و از آن استفاده کردم، کتابخانه‌ی FileDrop است که بسیار ساده و در عین حال قابلیت‌های خوبی را می‌دهد و از فناوری Filereader  (+) در Html5 برای اینکار استفاده می‌کند. مرورگرهای کروم، فایرفاکس 3.6 به بعد، IE10 به بعد و Opera 12 به بعد از آن پشتیبانی می‌کنند.
فایل‌های مورد نیاز را از اینجا دانلود کنید . فایل اسکریت آن را ابتدا صدا بزنید:
 <script src="~/scripts/jquery.filedrop.js" type="text/javascript"></script>
سپس المان‌های زیر را به کد HTML خود اضافه کنید:
 <div id="dropZone">فایل برنامه را به داخل این کادر بکشانید</div>
        <br>
        فایل یا فایل‌های آپلود شده:
        <ul id="uploadResult"></ul>
تگ اول، محلی است که فایل‌ها به سمت آن درگ و روی آن دراپ می‌شوند که از این به بعد به آن محل آپلود می‌گوییم. المان بعدی جهت گزارش فایل‌هایی است که آپلود شده‌اند. با آپلود شدن هر تعداد فایل، اسم آن به لیست اضافه می‌گردد.
کدهای css زیر را هم به صفحه اضافه کنید تا محل آپلود زیباتر شود:
        .files {
            min-height: 42px;
            background: #CCC none repeat scroll 0% 0%;
            border-top: 1px solid #FFF;
            margin: 11px 0px;
            padding: 11px 13px;
            border-radius: 6px;
        }

        #dropZone.mouse-over {
            background-color: #1d4257;
        }

کد جی کوئری زیر را به صفحه اضافه کنید:
      $('#dropZone').filedrop({
                url: uploadAddress,
                paramname: 'files',
                maxFiles: 1,
                dragOver: function() {
                    $('#dropZone').addClass('mouse-over');
                },
                dragLeave: function() {
                    $('#dropZone').removeClass('mouse-over');
                },
                drop: function() {
                    $('#dropZone').removeClass('mouse-over');
                },
                afterAll: function() {
                    $('#dropZone').html('آپلود با موفقیت انجام شد');
                },
                uploadFinished: function(i, file, response, time) {
                    $('#uploadResult').append('<li>' + file.name + '</li>');
                }
            });
ابتدا پلاگین جی کوئری را روی تگ مربوطه اعمال می‌کنیم و سپس پارامترها را با مشخصات زیر اعمال می‌کنیم:
Url  آدرسی که قرار است فایل‌ها به آن سمت ارسال شوند. 
 Paramname  در سمت سرور باید فایل‌ها را با استفاده از این نام پارامتر دریافت  کنید.
 maxFiles  تعداد فایلهایی که میتوان با درگ و دراپ کردن روی آن به دست آورد. در بالا به یک فایل محدود شده است.
 dragOver  این رویداد زمانی اجرا خواهد شد که اشاره گر با حالت درگ کرده فایل‌ها را به محل آپلود آورده است.
 dragLeave  موقعی که ماوس از محل آپلود خارج می‌شود
 drop  موقعی که شما فایل‌ها را روی محل  آپلود رها می‌کنید.
 afterAll  بعد از اینکه همه کارها تمام شد اجرا می‌شود.(آخرین رویداد)
 uploadFinished  کار آپلود به پایان رسیده است. در مثال بالا پس از پایان آپلود، نام فایل آپلود شده را به کاربر نشان داده‌ایم.

نحوه‌ی دریافت آن در سمت سرور, در یک اکشن متد به صورت زیر است:

[HttpPost]
        public virtual ActionResult UpdateApp(IEnumerable<HttpPostedFileBase>files)
        {
            foreach (HttpPostedFileBase file in files)
            {
                string filePath = Path.Combine(TempPath, file.FileName);
                file.SaveAs(filePath);
            }

            return Json(new {state = "success", message = "با موفقیت عملیات ارسال فایل انجام شد"}, JsonRequestBehavior.AllowGet);
        }

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

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


fallback_id 
 اگر دوست دارید این آپلودر را نیر به یک آپلودر معمولی اتصال دهید از این شناسه استفاده کنید.
 withCredentials   با استفاده از کوکی‌ها یک درخواست cross-origin ایجاد می‌کند.
 data  اگر دوست دارید به همراه فایل‌ها اطلاعات دیگری هم به همراه آن ارسال و پست شوند از این طریق اقدام نمایید. می‌تواند در قالب یک متغیر باشد یا خروجی یک تابع.
data: {
        param1: 'value1',           
        param2: function(){
            return calculated_data; 
        }
 headers برای ارسال مقدار اضافه‌تر در هدر درخواست به کار میرود و صدا زدن آن همانند کد data می‌باشد. 
 error   در صورتیکه در فرایند آپلود خطایی رخ دهد، اجرا می‌گردد. نحوه‌ی کدنویسی آن و بررسی خطاهای آن به شرح زیر است:
error: function(err, file) {
        switch(err) {
            case 'BrowserNotSupported':
                alert('مرورگر از این فناوری پشتیبانی نمی‌کند')
                break;
            case 'TooManyFiles':
                // قصد آپلود همزمان فایل‌های بیشتری از حد مجاز تعیین شده دارید
                break;
            case 'FileTooLarge':
                //حداقل حجم یکی از فایل‌ها از حجم مجاز تعیین شده بیشتر است
                //برای دسترسی به نام آن فایل از کد زیر استفاده کنید
              //file.name
                break;
            case 'FileTypeNotAllowed':
                // نوع حداقل یکی از فایل‌ها با نوع‌ها مشخص شده ما یکی نیست
                break;
            case 'FileExtensionNotAllowed':
                // پسوند حداقل یکی از فایل‌ها مورد تایید نیست
                break;
            default:
                break;
        }
    }
 allowedfiletypes   نوع فایل‌های مجاز را تعیین می‌کند:
allowedfiletypes: 
['image/jpeg','image/png','image/gif']
 allowedfileextensions   پسوند فایل هایی که برای آپلود مجاز هستند را معرفی می‌کند.
allowedfileextensions:
 ['.apk','.jar']
 maxfilesize   حداکثر حجم مجاز برای هر فایل که به مگابایت بیان می‌شود.
 docOver   این رویداد زمانی اجرا می‌شود که فایل‌های درگ شده شما وارد محیط یا پنجره مرورگر می‌شود.
 uploadStarted 
 این رویداد زمانی اجرا میگردد که فرایند آپلود هر فایل به طور جداگانه در حال آغاز شدن است:
متغیر i در کد زیر شامل اندیس فایلی است که آپلودش آغاز شده است و این اندیس از صفر آغاز می‌شود.
متغیر file دسترسی شما را به اطلاعات یک فایل باز میکند مانند نام فایل.
متغیر len تعداد فایل هایی را که کاربر در محل آپلود رها کرده است، باز میگرداند.
function(i, file, len){

    },
uploadFinished 
با اتمام آپلود هر فایل، این رویداد فراخوانی می‌گردد. دو پارامتر اول آن، همانند سابق هستند. پارامتر response خروجی json ایی را که در سمت سرور برگرداندیم، به ما باز می‌گرداند. پارامتر بعدی، زمانی را که برای آپلود طول کشیده است، بر می‌گرداند.
 function(i, file, response, time) {
    }
progressUpdated 
این رویداد برای نمایش پیشرفت یک آپلود مناسب است که آخرین پارامتر آن یک عدد صحیح از پیشرفت فایل را بر می‌گرداند.
function(i, file, progress) {
    },
globalProgressUpdated 
این رویداد میزان پیشرفت کلیه فایل‌ها را به درصد باز می‌گرداند:
function(progress) {
        $('#progress div')
.width(progress+"%"); }
speedUpdated 
سرعت آپلود هر فایل را با کیلوبیت بر ثانیه مشخص می‌کند.
function(i, file, speed) {
    }
rename
در صورتی که قصد تغییر نام فایل ارسالی را دارید می‌توانید از این رویداد استفاده کنید. پارامتر name، نام اصلی فایل را بر می‌گرداند که می‌توانید آن را دستکاری کنید و نام جدیدی را به عنوان خروجی برگردانید. نمونه کاربردی از این رویداد
 rename: function(name) {
    }

beforeEach 
این رویداد قبل از آپلود هر فایل آغاز می‌گردد و برگرداندن مقدار false در آن باعث جلوگیری و کنسل شدن آپلود آن فایل می‌گردد.
function(file) {
    }

beforeSend 
پارامترهای اولی تکراری هستند ولی آخرین پارامتر یک تابع done را می‌توان به آن پاس کرد که قبل از اجرای کل عملیات آپلود صدا زده می‌شود.
 function(file, i, done) {
    }
رویدادی به اسم queuefiles هم هست تعداد فایل‌هایی را که میتوانند به طور موازی و همزمان آپلود گردند، مشخص می‌کند. ولی دراین حالت maxfiles مورد استفاده قرار نمی‌گیرد. جهت بررسی یک مثال عملی و همچنین کدهای سمت سرور در PHP میتوانید از این آموزش استفاده کنید.
با تستی که به صورت لوکال رو آن انجام دادم به نظر نمی‌رسد برای فایل‌های با حجم متوسط به بالا مناسب باشد و برای فایل‌های با حجم کم مناسب می‌باشد. یک فایل 8 مگابایتی در حالت لوکال 9 ثانیه آپلود آن زمان برد و برای فایل‌های بزرگتر، فایرفاکس دیالوگ Stop Script را نشان داد.

PlUpload
این کتابخانه متن باز هم بسیار کارآمد و ساده و قابل انعطاف است و مثالهای آماده زیادی دارد. سایت سابسن هم در بخش آپلود زیرنویس‌ها از این کتابخانه استفاده می‌کند. از آنجا که آموزش این کتابخانه در سایت جاری آمده است از ذکر نکات بیشتر در مورد آن خودداری می‌نماییم.

Bootstrap FileStyle
اگر از قالب بوت استراپ استفاده می‌کنید و دوست دارید روی المان input file  قدیمی، ولی به شکلی مدرن کار کنید این کتابخانه هم فراموش نشود.

DropZoneJS
این کتابخانه به نسبت DropFile امکانات بیشتری را دارد و در سایت اختصاصی آن مثال‌ها و مستندات خوبی قرار گرفته است. در ساده‌ترین حالت آن ابتدا فایل کتابخانه  را صدا زده و سپس تگ فرم را به آن نسبت دهید:
<script src="https://rawgit.com/enyo/dropzone/master/dist/dropzone.js"></script>

<form action="/upload-target" class="dropzone"></form>
ولی اگر بخواهید آن را به سمت سرور ارسال کنید و  از آنجا آن را کنترل کنید، کد فرم را به شکل زیر تغییر دهید:
ابتدا بسته‌ی نیوگت آن را صدا بزنید:
Install-Package dropzone

با نصب این کتابخانه یک سری فایل CSS هم به سیستم اضافه می‌شود که می‌توانید برای استایل دهی هر چه بیشتر از آن بهره ببرید. کد فرم را به شکل زیر تغییر دهید:
    <form action="~/Home/SaveUploadedFile" method="post" enctype="multipart/form-data" class="dropzone" id="dropzoneForm" style="width: 50px; background: none; border: none;">
        <div class="fallback">
            <input name="file" type="file" multiple />
            <input type="submit" value="Upload" />
        </div>
    </form>
تگی که با کلاس fallback مزین شده است موقعی به کار می‌آید که مرورگر از این کتابخانه پشتیبانی نکرده و مجبور به نمایش یک آپلود معمولی می‌شویم.
با استفاده از کدنویسی هم می‌توان یک المان را به یک آپلودر تبدیل کرد:
var myDropzone = new Dropzone("div#myId", { url: "/file/post"});

//============ OR ====================
$("div#myId").dropzone({ url: "/file/post" });
همانطور که می‌بینید الزامی برای اینکه از یک تگ فرم استفاده کنید ندارد.
برای کانفیگ آپلودرهایی که از طریف المانهای Html ایجاد می‌شوند، می‌توان از کد زیر استفاده کرد و یک تنظیم عمومی برای تمامی آپلودرهای html آن صفحه ایجاد کرد.
Dropzone.options.myId= {
  paramName: "file", //نام پارامتری که فایل از طریق آن انتقال می‌بابد
  maxFilesize: 2, // MB
  accept: function(file, done) {
    if (file.name == "justinbieber.jpg") {
      done("Naha, you don't.");
    }
    else { done(); }
  }
};
تابع بالا یک آرگومان از نوع file را برگردانده و اگر این تابع، تابع done را با پارامتری رشته‌ای صدا بزند، عملیات آپلود آن فایل کنسل شده و پیام خطایی را نمایش می‌دهد و در صورتیکه بدون پارامتر صدا زده شود، عمل آپلود بدون مشکل انجام می‌شود.
ازآنجا که این کتابخانه از تنظیمات وسیعی استفاده می‌کند و از حوصله‌ی این مقاله خارج است، بهتر هست که صفحه‌ی مستندات آن را که کامل هم هست، مطالعه بفرمایید. از سری قابلیت‌هایی که پشتیبانی می‌کند: موارد پوشش داده شده در FileDrop، ساخت layout، ایجاد صف، متد حذف و اضافه و از این قبیل، ایجاد تصویر تمبر مانند و ...

یک نکته تکمیلی در مورد آپلود: در ASP.net به طور پیش فرض نهایت حجم فایل آپلودی 4 مگابایتی تعیین شده است که میتوانید آن را از طریق web.config تغییر دهید:
<configuration>
    <system.web>
        <httpRuntime maxRequestLength="1048576" />
    </system.web>
</configuration>
برای IIS 7 به بعد هم از تکه کد زیر استفاده کنید:
<system.webServer>
   <security>
      <requestFiltering>
         <requestLimits maxAllowedContentLength="1073741824" />
      </requestFiltering>
   </security>
 </system.webServer>
در هر دو کد بالا نهایت حجم بر روی یک گیگابایت تعیین شده است که maxRequestLength به صورت کیلوبایت و maxAllowContentLength به صورت بایت تعیین شده است. توصیه می‌شود هر دو شکل آن را وارد کنید. به خصوص که IIS Express از کد ابتدایی استفاده می‌کند و بخواهید نتیجه‌ی آن را در تست‌ها ببینید.

مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت دهم- ذخیره سازی اطلاعات کاربران IDP در بانک اطلاعاتی
تا اینجا تمام قسمت‌های این سری، برای اساس اطلاعات یک کلاس Config استاتیک تشکیل شده‌ی در حافظه ارائه شدند. این روش برای دمو و توضیح مفاهیم پایه‌ی IdentityServer بسیار مفید است؛ اما برای دنیای واقعی خیر. بنابراین در ادامه می‌خواهیم این قسمت را با اطلاعات ذخیره شده‌ی در بانک اطلاعاتی تعویض کنیم. یک روش مدیریت آن، نصب ASP.NET Core Identity دقیقا داخل همان پروژه‌ی IDP است. در این حالت کدهای ASP.NET Core Identity مایکروسافت، کار مدیریت کاربران IDP را انجام می‌دهند. روش دیگر اینکار را که در اینجا بررسی خواهیم کرد، تغییر کدهای Quick Start UI اضافه شده‌ی در «قسمت چهارم - نصب و راه اندازی IdentityServer»، جهت پذیرفتن مدیریت کاربران مبتنی بر بانک اطلاعاتی تهیه شده‌ی توسط خودمان است. مزیت آن آشنا شدن بیشتر با کدهای Quick Start UI و درک زیرساخت آن است.


تکمیل ساختار پروژه‌ی IDP

تا اینجا برای IDP، یک پروژه‌ی خالی وب را ایجاد و به مرور، آن‌را تکمیل کردیم. اما اکنون نیاز است پشتیبانی از بانک اطلاعاتی را نیز به آن اضافه کنیم. برای این منظور چهار پروژه‌ی Class library کمکی را نیز به Solution آن اضافه می‌کنیم:


- DNT.IDP.DomainClasses
در این پروژه، کلاس‌های متناظر با موجودیت‌های جداول مرتبط با اطلاعات کاربران قرار می‌گیرند.
- DNT.IDP.DataLayer
این پروژه Context برنامه و Migrations آن‌را تشکیل می‌دهد. همچنین به همراه تنظیمات و Seed اولیه‌ی اطلاعات بانک اطلاعاتی نیز می‌باشد.
رشته‌ی اتصالی آن نیز در فایل DNT.IDP\appsettings.json ذخیره شده‌است.
- DNT.IDP.Common
الگوریتم هش کردن اطلاعات، در این پروژه‌ی مشترک بین چند پروژه‌ی دیگر قرار گرفته‌است. از آن جهت هش کردن کلمات عبور، در دو پروژه‌ی DataLayer و همچنین Services استفاده می‌کنیم.
- DNT.IDP.Services
کلاس سرویس کاربران که با استفاده از DataLayer با بانک اطلاعاتی ارتباط برقرار می‌کند، در این پروژه قرار گرفته‌است.


ساختار بانک اطلاعاتی کاربران IdentityServer

در اینجا ساختار بانک اطلاعاتی کاربران IdentityServer، بر اساس جداول کاربران و Claims آن‌ها تشکیل می‌شود:
namespace DNT.IDP.DomainClasses
{
    public class User
    {
        [Key]
        [MaxLength(50)]       
        public string SubjectId { get; set; }
    
        [MaxLength(100)]
        [Required]
        public string Username { get; set; }

        [MaxLength(100)]
        public string Password { get; set; }

        [Required]
        public bool IsActive { get; set; }

        public ICollection<UserClaim> UserClaims { get; set; }

        public ICollection<UserLogin> UserLogins { get; set; }
    }
}
در اینجا SubjectId همان Id کاربر، در سطح IDP است. این خاصیت به صورت یک کلید خارجی در جداول UserClaims و UserLogins نیز بکار می‌رود.
ساختار Claims او نیز به صورت زیر تعریف می‌شود که با تعریف یک Claim استاندارد، سازگاری دارد:
namespace DNT.IDP.DomainClasses
{
    public class UserClaim
    {         
        public int Id { get; set; }

        [MaxLength(50)]
        [Required]
        public string SubjectId { get; set; }
        
        public User User { get; set; }

        [Required]
        [MaxLength(250)]
        public string ClaimType { get; set; }

        [Required]
        [MaxLength(250)]
        public string ClaimValue { get; set; }
    }
}
همچنین کاربر می‌توان تعدادی لاگین نیز داشته باشد:
namespace DNT.IDP.DomainClasses
{
    public class UserLogin
    {
        public int Id { get; set; }

        [MaxLength(50)]
        [Required]
        public string SubjectId { get; set; }
        
        public User User { get; set; }

        [Required]
        [MaxLength(250)]
        public string LoginProvider { get; set; }

        [Required]
        [MaxLength(250)]
        public string ProviderKey { get; set; }
    }
}
هدف از آن، یکپارچه سازی سیستم، با IDPهای ثالث مانند گوگل، توئیتر و امثال آن‌ها است.

در پروژه‌ی DNT.IDP.DataLayer در پوشه‌ی Configurations آن، کلاس‌های UserConfiguration و UserClaimConfiguration را مشاهده می‌کنید که حاوی اطلاعات اولیه‌ای برای تشکیل User 1 و User 2 به همراه Claims آن‌ها هستند. این اطلاعات را دقیقا از فایل استاتیک ‍Config که در قسمت‌های قبل تکمیل کردیم، به این دو کلاس جدید IEntityTypeConfiguration منتقل کرده‌ایم تا به این ترتیب متد GetUsers فایل استاتیک Config را با نمونه‌ی دیتابیسی آن جایگزین کنیم.
سرویسی که از طریق Context برنامه با بانک اطلاعاتی ارتباط برقرار می‌کند، چنین ساختاری را دارد:
    public interface IUsersService
    {
        Task<bool> AreUserCredentialsValidAsync(string username, string password);
        Task<User> GetUserByEmailAsync(string email);
        Task<User> GetUserByProviderAsync(string loginProvider, string providerKey);
        Task<User> GetUserBySubjectIdAsync(string subjectId);
        Task<User> GetUserByUsernameAsync(string username);
        Task<IEnumerable<UserClaim>> GetUserClaimsBySubjectIdAsync(string subjectId);
        Task<IEnumerable<UserLogin>> GetUserLoginsBySubjectIdAsync(string subjectId);
        Task<bool> IsUserActiveAsync(string subjectId);
        Task AddUserAsync(User user);
        Task AddUserLoginAsync(string subjectId, string loginProvider, string providerKey);
        Task AddUserClaimAsync(string subjectId, string claimType, string claimValue);
    }
که توسط آن امکان دسترسی به یک کاربر، اطلاعات Claims او و افزودن رکوردهایی جدید وجود دارد.
تنظیمات نهایی این سرویس‌ها و Context برنامه نیز در فایل DNT.IDP\Startup.cs جهت معرفی به سیستم تزریق وابستگی‌ها، صورت گرفته‌اند. همچنین در اینجا متد initializeDb را نیز مشاهده می‌کنید که با فراخوانی متد context.Database.Migrate، تمام کلاس‌های Migrations پروژه‌ی DataLayer را به صورت خودکار به بانک اطلاعاتی اعمال می‌کند.


غیرفعال کردن صفحه‌ی Consent در Quick Start UI

در «قسمت چهارم - نصب و راه اندازی IdentityServer» فایل‌های Quick Start UI را به پروژه‌ی IDP اضافه کردیم. در ادامه می‌خواهیم قدم به قدم این پروژه را تغییر دهیم.
در صفحه‌ی Consent در Quick Start UI، لیست scopes درخواستی برنامه‌ی کلاینت ذکر شده و سپس کاربر انتخاب می‌کند که کدامیک از آن‌ها، باید به برنامه‌ی کلاینت ارائه شوند. این صفحه، برای سناریوی ما که تمام برنامه‌های کلاینت توسط ما توسعه یافته‌اند، بی‌معنا است و صرفا برای کلاینت‌های ثالثی که قرار است از IDP ما استفاده کنند، معنا پیدا می‌کند. برای غیرفعال کردن آن کافی است به فایل استاتیک Config مراجعه کرده و خاصیت RequireConsent کلاینت مدنظر را به false تنظیم کرد.


تغییر نام پوشه‌ی Quickstart و سپس اصلاح فضای نام پیش‌فرض کنترلرهای آن

در حال حاضر کدهای کنترلرهای Quick Start UI داخل پوشه‌ی Quickstart برنامه‌ی IDP قرار گرفته‌اند. با توجه به اینکه قصد داریم این کدها را تغییر دهیم و همچنین این پوشه در اساس، همان پوشه‌ی استاندارد Controllers است، ابتدا نام این پوشه را به Controllers تغییر داده و سپس در تمام کنترلرهای ذیل آن، فضای نام پیش‌فرض IdentityServer4.Quickstart.UI را نیز به فضای نام متناسبی با پوشه بندی پروژه‌ی جاری تغییر می‌دهیم. برای مثال کنترلر Account واقع در پوشه‌ی Account، اینبار دارای فضای نام DNT.IDP.Controllers.Account خواهد شد و به همین ترتیب برای مابقی کنترل‌ها عمل می‌کنیم.
پس از این تغییرات، عبارات using موجود در Viewها را نیز باید تغییر دهید تا برنامه در زمان اجرا به مشکلی برنخورد. البته ASP.NET Core 2.1 در زمان کامپایل برنامه، تمام Viewهای آن‌را نیز کامپایل می‌کند و اگر خطایی در آن‌ها وجود داشته باشد، امکان بررسی و رفع آن‌ها پیش از اجرای برنامه، میسر است.
و یا می‌توان جهت سهولت کار، فایل DNT.IDP\Views\_ViewImports.cshtml را جهت معرفی این فضاهای نام جدید ویرایش کرد تا نیازی به تغییر Viewها نباشد:
@using DNT.IDP.Controllers.Account;
@using DNT.IDP.Controllers.Consent;
@using DNT.IDP.Controllers.Grants;
@using DNT.IDP.Controllers.Home;
@using DNT.IDP.Controllers.Diagnostics;
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers


تعامل با IdentityServer از طریق کدهای سفارشی

پس از تشکیل «ساختار بانک اطلاعاتی کاربران IdentityServer» و همچنین تهیه سرویس‌های متناظری جهت کار با آن، اکنون نیاز است مطمئن شویم IdentityServer از این بانک اطلاعاتی برای دریافت اطلاعات کاربران خود استفاده می‌کند.
در حال حاضر، با استفاده از متد الحاقی AddTestUsers معرفی شده‌ی در فایل DNT.IDP\Startup.cs، اطلاعات کاربران درون حافظه‌ای برنامه را از متد ()Config.GetUsers دریافت می‌کنیم.
بنابراین اولین قدم، بررسی ساختار متد AddTestUsers است. برای این منظور به مخزن کد IdentityServer4 مراجعه کرده و کدهای متد الحاقی AddTestUsers را بررسی می‌کنیم:
 public static class IdentityServerBuilderExtensions
 {
        public static IIdentityServerBuilder AddTestUsers(this IIdentityServerBuilder builder, List<TestUser> users)
        {
            builder.Services.AddSingleton(new TestUserStore(users));
            builder.AddProfileService<TestUserProfileService>();
            builder.AddResourceOwnerValidator<TestUserResourceOwnerPasswordValidator>();

            return builder;
        }
}
- ابتدا یک TestUserStore را به صورت Singleton ثبت کرده‌است.
- سپس سرویس پروفایل کاربران را اضافه کرده‌است. این سرویس با پیاده سازی اینترفیس IProfileService تهیه می‌شود. کار آن اتصال یک User Store سفارشی به سرویس کاربران و دریافت اطلاعات پروفایل آن‌ها مانند Claims است.
- در آخر TestUserResourceOwnerPasswordValidator، کار اعتبارسنجی کلمه‌ی عبور و نام کاربری را در صورت استفاده‌ی از Flow ویژه‌ای به نام ResourceOwner که استفاده‌ی از آن توصیه نمی‌شود (ROBC Flow)، انجام می‌دهد.

برای جایگزین کردن AddTestUsers، کلاس جدید IdentityServerBuilderExtensions را در ریشه‌ی پروژه‌ی IDP با محتوای ذیل اضافه می‌کنیم:
using DNT.IDP.Services;
using Microsoft.Extensions.DependencyInjection;

namespace DNT.IDP
{
    public static class IdentityServerBuilderExtensions
    {
        public static IIdentityServerBuilder AddCustomUserStore(this IIdentityServerBuilder builder)
        {
            // builder.Services.AddScoped<IUsersService, UsersService>();
            builder.AddProfileService<CustomUserProfileService>();
            return builder;
        }
    }
}
در اینجا ابتدا IUsersService سفارشی برنامه معرفی شده‌است که User Store سفارشی برنامه است. البته چون UsersService ما با بانک اطلاعاتی کار می‌کند، نباید به صورت Singleton ثبت شود و باید در پایان هر درخواست به صورت خودکار Dispose گردد. به همین جهت طول عمر آن Scoped تعریف شده‌است. در کل ضرورتی به ذکر این سطر نیست؛ چون پیشتر کار ثبت IUsersService در کلاس Startup برنامه انجام شده‌است.
سپس یک ProfileService سفارشی را ثبت کرده‌ایم. این سرویس، با پیاده سازی IProfileService به صورت زیر پیاده سازی می‌شود:
namespace DNT.IDP.Services
{
    public class CustomUserProfileService : IProfileService
    {
        private readonly IUsersService _usersService;

        public CustomUserProfileService(IUsersService usersService)
        {
            _usersService = usersService;
        }

        public async Task GetProfileDataAsync(ProfileDataRequestContext context)
        {
            var subjectId = context.Subject.GetSubjectId();
            var claimsForUser = await _usersService.GetUserClaimsBySubjectIdAsync(subjectId);
            context.IssuedClaims = claimsForUser.Select(c => new Claim(c.ClaimType, c.ClaimValue)).ToList();
        }

        public async Task IsActiveAsync(IsActiveContext context)
        {
            var subjectId = context.Subject.GetSubjectId();
            context.IsActive = await _usersService.IsUserActiveAsync(subjectId);
        }
    }
}
سرویس پروفایل، توسط سرویس کاربران برنامه که در ابتدای مطلب آن‌را تهیه کردیم، امکان دسترسی به اطلاعات پروفایل کاربران را مانند Claims او، پیدا می‌کند.
در متدهای آن، ابتدا subjectId و یا همان Id منحصربفرد کاربر جاری سیستم، دریافت شده و سپس بر اساس آن می‌توان از usersService، جهت دریافت اطلاعات مختلف کاربر، کوئری گرفت و نتیجه را در خواص context جاری، برای استفاده‌های بعدی، ذخیره کرد.

اکنون به کلاس src\IDP\DNT.IDP\Startup.cs مراجعه کرده و متد AddTestUsers را با AddCustomUserStore جایگزین می‌کنیم:
namespace DNT.IDP
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddIdentityServer()
             .AddDeveloperSigningCredential()
             .AddCustomUserStore()
             .AddInMemoryIdentityResources(Config.GetIdentityResources())
             .AddInMemoryApiResources(Config.GetApiResources())
             .AddInMemoryClients(Config.GetClients());
تا اینجا فقط این سرویس‌های جدید را ثبت کرده‌ایم، اما هنوز کار خاصی را انجام نمی‌دهند و باید از آن‌ها در برنامه استفاده کرد.


اتصال IdentityServer به User Store سفارشی

در ادامه، سازنده‌ی کنترلر DNT.IDP\Quickstart\Account\AccountController.cs را بررسی می‌کنیم:
        public AccountController(
            IIdentityServerInteractionService interaction,
            IClientStore clientStore,
            IAuthenticationSchemeProvider schemeProvider,
            IEventService events,
            TestUserStore users = null)
        {
            _users = users ?? new TestUserStore(TestUsers.Users);

            _interaction = interaction;
            _clientStore = clientStore;
            _schemeProvider = schemeProvider;
            _events = events;
        }
- سرویس توکار IIdentityServerInteractionService، کار تعامل برنامه با IdentityServer4‌  را انجام می‌دهد.
- IClientStore پیاده سازی محل ذخیره سازی اطلاعات کلاینت‌ها را ارائه می‌دهد که در حال حاضر توسط متد استاتیک Config در اختیار آن قرار می‌گیرد.
- IEventService رخ‌دادهایی مانند لاگین موفقیت آمیز یک کاربر را گزارش می‌دهد.
- در آخر، TestUserStore تزریق شده‌است که می‌خواهیم آن‌را با User Store سفارشی خودمان جایگزین کنیم.  بنابراین در ابتدا TestUserStore را با UserStore سفارشی خودمان جایگزین می‌کنیم:
        private readonly TestUserStore _users;
        private readonly IUsersService _usersService;
        public AccountController(
    // ...
            IUsersService usersService)
        {
            _usersService = usersService;
    // ...
        }
فعلا فیلد TestUserStore را نیز سطح کلاس جاری باقی نگه می‌داریم. از این جهت که قسمت‌های لاگین خارجی سیستم (استفاده از گوگل، توئیتر و ...) هنوز از آن استفاده می‌کنند و آن‌را در قسمتی دیگر تغییر خواهیم داد.
پس از معرفی فیلد usersService_، اکنون در قسمت زیر از آن استفاده می‌کنیم:
در اکشن متد لاگین، جهت بررسی صحت نام کاربری و کلمه‌ی عبور و همچنین یافتن کاربر متناظر با آن:
        public async Task<IActionResult> Login(LoginInputModel model, string button)
        {
    //...
            if (ModelState.IsValid)
            {
                if (await _usersService.AreUserCredentialsValidAsync(model.Username, model.Password))
                {
                    var user = await _usersService.GetUserByUsernameAsync(model.Username);
تا همینجا برنامه را کامپایل کرده و اجرا کنید. پس از لاگین در آدرس https://localhost:5001/Gallery/IdentityInformation، هنوز اطلاعات User Claims کاربر وارد شده‌ی به سیستم نمایش داده می‌شوند که بیانگر صحت عملکرد CustomUserProfileService است.


افزودن امکان ثبت کاربران جدید به برنامه‌ی IDP

پس از اتصال قسمت login برنامه‌ی IDP به بانک اطلاعاتی، اکنون می‌خواهیم امکان ثبت کاربران را نیز به آن اضافه کنیم.
این قسمت شامل تغییرات ذیل است:
الف) اضافه شدن RegisterUserViewModel
این ViewModel که فیلدهای فرم ثبت‌نام را تشکیل می‌دهد، ابتدا با نام کاربری و کلمه‌ی عبور شروع می‌شود:
    public class RegisterUserViewModel
    {
        // credentials       
        [MaxLength(100)]
        public string Username { get; set; }

        [MaxLength(100)]
        public string Password { get; set; }
سپس سایر خواصی که در اینجا اضافه می‌شوند:
    public class RegisterUserViewModel
    {
   // ...

        // claims 
        [Required]
        [MaxLength(100)]
        public string Firstname { get; set; }

        [Required]
        [MaxLength(100)]
        public string Lastname { get; set; }

        [Required]
        [MaxLength(150)]
        public string Email { get; set; }

        [Required]
        [MaxLength(200)]
        public string Address { get; set; }

        [Required]
        [MaxLength(2)]
        public string Country { get; set; }
در کنترلر UserRegistrationController، تبدیل به UserClaims شده و در جدول مخصوص آن ذخیره خواهند شد.
ب) افزودن UserRegistrationController
این کنترلر، RegisterUserViewModel را دریافت کرده و سپس بر اساس آن، شیء User ابتدای بحث را تشکیل می‌دهد. ابتدا نام کاربری و کلمه‌ی عبور را در جدول کاربران ثبت می‌کند و سپس سایر خواص این ViewModel را در جدول UserClaims:
varuserToCreate=newUser
{
  Password=model.Password.GetSha256Hash(),
  Username=model.Username,
  IsActive=true
};
userToCreate.UserClaims.Add(newUserClaim("country",model.Country));
userToCreate.UserClaims.Add(newUserClaim("address",model.Address));
userToCreate.UserClaims.Add(newUserClaim("given_name",model.Firstname));
userToCreate.UserClaims.Add(newUserClaim("family_name",model.Lastname));
userToCreate.UserClaims.Add(newUserClaim("email",model.Email));
userToCreate.UserClaims.Add(newUserClaim("subscriptionlevel","FreeUser"));
ج) افزودن RegisterUser.cshtml
این فایل، view متناظر با ViewModel فوق را ارائه می‌دهد که توسط آن، کاربری می‌تواند اطلاعات خود را ثبت کرده و وارد سیستم شود.
د) اصلاح فایل ViewImports.cshtml_ جهت تعریف فضای نام UserRegistration
در RegisterUser.cshtml از RegisterUserViewModel استفاده می‌شود. به همین جهت بهتر است فضای نام آن‌را به ViewImports اضافه کرد.
ه) افزودن لینک ثبت نام به صفحه‌ی لاگین در Login.cshtml
این لینک دقیقا در ذیل چک‌باکس Remember My Login اضافه شده‌است.


اکنون اگر برنامه را اجرا کنیم، ابتدا مشاهده می‌کنیم که صفحه‌ی لاگین به همراه لینک ثبت نام ظاهر می‌شود:


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


برای آزمایش، کاربری را ثبت کنید. پس از ثبت اطلاعات، بلافاصله وارد سیستم خواهید شد. البته چون در اینجا subscriptionlevel به FreeUser تنظیم شده‌است، این کاربر یکسری از لینک‌های برنامه‌ی MVC Client را به علت نداشتن دسترسی، مشاهده نخواهد کرد.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشه‌ی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشه‌ی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آن‌را اجرا کنید تا برنامه‌ی IDP راه اندازی شود.
- در آخر به پوشه‌ی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحه‌ی login نام کاربری را User 1 و کلمه‌ی عبور آن‌را password وارد کنید.
مطالب
Blazor 5x - قسمت پنجم - مبانی Blazor - بخش 2 - کامپوننت‌ها
انتقال محتوای کامپوننت Index به یک کامپوننت جدید و تعریف مسیریابی و مدخل منوی آن

پیش از ادامه‌ی مثال قسمت قبل، قصد داریم تمام کدهای موجود در فایل Pages\Index.razor را به یک فایل اختصاصی آن‌ها منتقل کرده و مسیریابی و منوی آن‌را تکمیل کنیم. به همین جهت در پوشه‌ی Pages، یک پوشه‌ی جدید را به نام LearnBlazor ایجاد کرده و درون آن، فایل خالی BindProp.razor را ایجاد می‌کنیم. سپس تمام محتوای فایل Pages\Index.razor را cut کرده و به درون فایل جدید Pages\LearnBlazor\BindProp.razor، منتقل و Paste می‌کنیم.
پس از این تغییرات، در فایل Pages\Index.razor، مهم‌ترین سطر آن، همان اولین سطر تعریف مسیریابی آن خواهد بود و هر محتوای دلخواهی که علاقمند بودید:
@page "/"

<h1>Hello, world!</h1>
در ادامه چون می‌خواهیم گزینه‌ی منوی جدیدی را برای BindProp.razor تعریف کنیم، سطر اول آن‌را به صورت زیر تغییر می‌دهیم:
@page "/bindprop"
با اینکار، این کامپوننت صرفنظر از محل قرارگیری آن که اکنون در پوشه‌ی Pages\LearnBlazor است، در مسیر https://localhost:5001/bindprop قابل دسترسی خواهد شد. اما چگونه باید مدخل منوی جدیدی را برای آن تعریف کرد؟ برای اینکار به فایل Shared\NavMenu.razor مراجعه کرده و دقیقا شبیه به ساختار مداخل منوهای Home ، Counter و غیره، مدخل جدیدی را برای آن تعریف می‌کنیم:
<li class="nav-item px-3">
    <NavLink class="nav-link" href="bindprop">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Bind Properties
    </NavLink>
</li>
در اینجا برچسب مدخل جدید تعریف شده، Bind Properties است و href لینک به آن، دقیقا به مسیریابی تعریف شده‌ی در فایل BindProp.razor اشاره می‌کند.



نمایش لیست اتاق‌های تعریف شده، به همراه ویژگی‌های آن‌ها

در قسمت قبل، نمایش ردیفی لیست اتاق‌های تعریف شده را مشاهده کردید. در این قسمت می‌خواهیم هر اتاق تعریف شده را در یک card جداگانه نمایش دهیم. هدف این است که در ابتدا به یک UI متداول شلوغ برسیم و بعد شروع کنیم به Refactoring این UI پیچیده، به کامپوننت‌های کوچک‌تر تشکیل دهنده‌ی آن، جهت مدیریت ساده‌تر این UI و درک بهتر آن. بنابراین در ابتدا با یک کامپوننت کلی شلوغ، شروع خواهیم کرد.
به همین جهت فایل جدید Pages\LearnBlazor\DemoHotel.razor را برای نمایش لیست اتاق‌های موجود اضافه می‌کنیم. سپس محتوای آن‌را به صورت زیر تغییر خواهیم داد:
@page "/demoHotel"

<h3>Hotel Rooms</h3>
<div class="border p-2 mt-2" style="background-color:azure">
    <h2 class="text-info">Rooms List</h2>
    <div class="row container">
        @foreach (var room in Rooms)
        {
            <div class="bg-light border p-2 col-5 ml-2">
                <h4 class="text-secondary">Room - @room.Id</h4>

                @room.Name<br />
                @room.Price.ToString("c")<br />
                <input type="checkbox" @bind-value="room.IsActive" checked="@(room.IsActive?"checked":null)" /> &nbsp; Is Active<br />
                <span>This room is @(room.IsActive?"Active": "InActive")</span>

                @if (room.IsActive)
                {
                    @foreach (var roomProp in room.RoomProps)
                    {
                        <p>@roomProp.Name - @roomProp.Value</p>
                    }
                }

                <input type="button" class="btn btn-danger" value="Delete" />
                <input type="button" class="btn btn-success" value="Edit" />
            </div>
        }
    </div>
</div>
- قسمت کدهای آن که در اینجا ذکر نشده (code@)، با قسمت کدهای کامپوننت Pages\LearnBlazor\BindProp.razor که در قسمت قبل تهیه کردیم، یکی است و هدف از آن، ارائه‌ی List<BlazorRoom> Rooms است که در کدهای razor جاری استفاده شده‌است.
- سپس مسیریابی منتهی به این کامپوننت، به آدرس demoHotel/ تنظیم شده‌است. این مسیریابی را در کامپوننت Shared\NavMenu.razor به صورت زیر مورد استفاده قرار خواهیم داد تا مدخل منوی جدیدی برای آن تهیه شود:
<li class="nav-item px-3">
    <NavLink class="nav-link" href="demoHotel">
      <span class="oi oi-list-rich" aria-hidden="true"></span> Demo Hotel
    </NavLink>
</li>
- در این کامپوننت، با ایجاد حلقه‌ای بر روی لیست اتاق‌ها، مشخصات هر کدام نمایش داده می‌شود. همچنین در اینجا اگر اتاق در حال نمایش فعال باشد، لیست خواص آن نیز درج خواهد شد. به علاوه دو دکمه‌ی جدید حذف و ویرایش نیز در انتهای هر برگه اضافه شده‌است:



تبدیل دکمه‌های حذف و ویرایش هر اتاق به یک کامپوننت جدید

اکنون می‌خواهیم کامپوننت شلوغ Pages\LearnBlazor\DemoHotel.razor را به چند زیر کامپوننت بشکنیم تا هر کدام وظایف خاص خود را انجام دهند و در نهایت به یک UI قابل درک‌تر برسیم. برای مثال می‌خواهیم دکمه‌های حذف و ویرایش هر اتاق را به یک کامپوننت جدید منتقل کنیم تا هم این UI خلوت‌تر شود و هم اگر در قسمت دیگری از برنامه نیاز به یک چنین دکمه‌هایی بود، بتوان از آن کامپوننت اختصاصی، استفاده‌ی مجدد کرد.
برای این منظور ابتدا پوشه‌ی جدید Pages\LearnBlazor\LearnBlazor‍Components را افزوده و سپس در داخل آن، فایل جدید کامپوننت EditDeleteButton.razor را نیز ایجاد می‌کنیم. در این فایل جدید در ابتدا کدهای دو دکمه‌ی تعریف شده را از کامپوننت DemoHotel.razor انتخاب و cut کرده و سپس در این فایل جدید paste می‌کنیم. در این کامپوننت جدید، نیازی به تعریف page@ و مسیریابی آن نیست. به این معنا که این کامپوننت، یک کامپوننت اشتراکی است و routable نیست و قرار است در داخل یک کامپوننت دیگر مورد استفاده قرار گیرد.
بنابراین تا اینجا محتوای کامپوننت EditDeleteButton.razor فقط از دو سطر زیر تشکیل می‌شود:
<input type="button" class="btn btn-danger" value="Delete" />
<input type="button" class="btn btn-success" value="Edit" />
در ادامه برای درج این کامپوننت در حلقه‌ی نمایشی آن در کامپوننت DemoHotel، باید به صورت زیر عمل کرد که به فضای نام کامل این کامپوننت اشاره می‌کند:
<BlazorServerSample.Pages.LearnBlazor.LearnBlazorComponents.EditDeleteButton></BlazorServerSample.Pages.LearnBlazor.LearnBlazorComponents.EditDeleteButton>
برای اینکه مجبور به تعریف یک چنین نام طولانی نباشیم، می‌توان فضای نام پوشه‌ی آن‌را در انتهای فایل Imports.razor_ قرار داد:
@using BlazorServerSample.Pages.LearnBlazor.LearnBlazorComponents
البته اگر قرار نیست از این کامپوننت در سایر کامپوننت‌ها استفاده شود و فقط یک محل استفاده را دارد، می‌توان این using را در بالای تعاریف فایل DemoHotel.razor نیز قرار داد.

اکنون می‌توان تعریف مدخل کامپوننت را به صورت زیر خلاصه کرد:
<EditDeleteButton></EditDeleteButton>


ارسال پارامترها به یک کامپوننت

فرض کنید قصد داریم دکمه‌های ویرایش و حذف را تنها به کاربران ادمین نمایش دهیم. به همین جهت نیاز است بتوان پارامتری مانند IsAdmin را به کامپوننت EditDeleteButton ارسال کرد. برای اینکار کامپوننت Pages\LearnBlazor\LearnBlazor‍Components\EditDeleteButton.razor را به صورت زیر ویرایش می‌کنیم:
@if (IsAdmin)
{
    <input type="button" class="btn btn-danger" value="Delete" />
    <input type="button" class="btn btn-success" value="Edit" />
}

@code
{
    [Parameter]
    public bool IsAdmin { get; set; }
}
در اینجا خواص عمومی مزین شده‌ی با ویژگی Parameter، به عنوان پارامتر ورودی کامپوننت عمل می‌کنند. برای نمونه بر اساس مقدار خاصیت IsAdmin، توسط یک if@ تصمیم خواهیم گرفت که آیا قسمتی از UI نمایش داده شود یا خیر؟

پس از تعریف این پارامتر ورودی، روش استفاده‌ی از آن در کامپوننت DemoHotel به صورت زیر است:
<EditDeleteButton IsAdmin="true"></EditDeleteButton>


انتقال هر اتاق به کامپوننت مجزای خاص خودش

در ادامه می‌خواهیم محتوای حلقه‌ی foreach (var room in Rooms)@ کامپوننت DemoHotel را به طور کامل cut کرده و در یک کامپوننت جدید paste کنیم تا به حلقه‌ای خواناتر و با مسئولیت‌های کمتری برسیم. نگهداری کدهایی که قسمت‌های مختلف آن از هم ایزوله شده‌اند و دامنه‌ی تغییرات آن‌ها کاملا مشخص و محدود است، در طول زمان بسیار ساده‌تر از نگهداری کدهای UI ای در هم تنیده‌است.
به همین جهت ابتدا کامپوننت جدید Pages\LearnBlazor\LearnBlazor‍Components\IndividualRoom.razor را ایجاد می‌کنیم و سپس، هر آنچه داخل حلقه‌ی foreach یاد شده قرار دارد را انتخاب و cut کرده و درون این کامپوننت جدید paste می‌کنیم:
<div class="bg-light border p-2 col-5 offset-1">
    <h4 class="text-secondary">Room - @Room.Id</h4>

    @Room.Name<br />
    @Room.Price.ToString("c")<br />
    <input type="checkbox" @bind-value="Room.IsActive" checked="@(Room.IsActive?"checked":null)" /> &nbsp; Is Active<br />
    <span>This room is @(Room.IsActive?"Active": "InActive")</span>

    @if (Room.IsActive)
    {
        @foreach (var roomProp in Room.RoomProps)
        {
            <p>@roomProp.Name - @roomProp.Value</p>
        }
    }

    <EditDeleteButton IsAdmin="true"></EditDeleteButton>
</div>

@code
{
    [Parameter]
    public BlazorRoom Room { get; set; }
}
در اینجا پس از paste کدهای داخل حلقه، نیاز به یک پارامتر ورودی که همان شیء Room در حال رندر است، خواهد بود. به همین جهت پارامتر آن‌را تعریف کرده و همچنین کدهای موجود را نیز اندکی ویرایش می‌کنیم، تا از نام این پارامتر جدید استفاده کند.

پس از این تغییر، کدهای حلقه‌ی foreach کامپوننت DemoHotel.razor به صورت زیر خلاصه می‌شوند. در اینجا روش ارسال یک شیء را به پارامتر Room نیز مشاهده می‌کنید (البته ذکر @ در اینجا الزامی نیست و می‌شد از روش مقدار دهی "Room="room نیز استفاده کرد):
<div class="row container">
  @foreach (var room in Rooms)
  {
    <IndividualRoom Room="@room"></IndividualRoom>
  }
</div>
در اینجا می‌توان سلسه مراتب کامپوننت‌ها را مشاهده کرد. کامپوننت DemoHotel، کامپوننت IndividualRoom را فراخوانی می‌کند و این کامپوننت نیز کامپوننت EditDeleteButton را مورد استفاده قرار می‌دهد.


یک تمرین: نمایش لیست امکانات رفاهی هتل

پس از نمایش لیست اتاق‌های یک هتل، قصد داریم لیست امکانات رفاهی آن‌را نیز نمایش دهیم:


 مدل این امکانات را به صورت زیر به پوشه‌ی Models برنامه اضافه می‌کنیم:
namespace BlazorServerSample.Models
{
    public class BlazorAmenity
    {
        public int Id { get; set; }

        public string Name { get; set; }

        public string Description { get; set; }
    }
}
از آنجائیکه قصد داریم لیست آن‌ها را در همان کامپوننت DemoHotel.razor نمایش دهیم، این لیست را به صورت زیر تشکیل می‌دهیم:
@code{

    List<BlazorAmenity> AmenitiesList = new List<BlazorAmenity>();
    // ...

    protected override void OnInitialized()
    {
        base.OnInitialized();

        // ...

        AmenitiesList.Add(new BlazorAmenity
        {
            Id = 111,
            Name = "Gym",
            Description = "24x7 gym room is available."
        });
        AmenitiesList.Add(new BlazorAmenity
        {
            Id = 222,
            Name = "Swimming Pool",
            Description = "Pool room is open from 6am to 10pm."
        });
        AmenitiesList.Add(new BlazorAmenity
        {
            Id = 333,
            Name = "Free Brakfast",
            Description = "Enjoy free breakfast at out hotel."
        });
    }
}
در ابتدا فیلد List<BlazorAmenity> AmenitiesList جهت دسترسی به لیست امکانات رفاهی تعریف شده و سپس آن‌را در رویدادگردان OnInitialized، مقدار دهی اولیه کرده‌ایم. در مورد این متدهای چرخه‌ی حیات، در قسمت بعدی بیشتر بحث خواهیم کرد.

اکنون برای نمایش تک تک عناصر این لیست، ابتدا یک کامپوننت منحصر به یک BlazorAmenity را به نام Pages\LearnBlazor\LearnBlazor‍Components\IndividualAmenity.razor ایجاد می‌کنیم با این محتوا:
<div class="bg-light border p-2 col-5 offset-1 mt-2">
    <h4 class="text-secondary">Amenity - @Amenity.Id</h4>

    @Amenity.Name<br />
    @Amenity.Description<br />
</div>

@code
{
    [Parameter]
    public BlazorAmenity Amenity { get; set; }
}
این کامپوننت، یک شیء BlazorAmenity را به عنوان پارامتر دریافت کرده و سپس Id، نام و توضیحات آن‌را نمایش می‌دهد.

در آخر پس از تعریف کامپوننت IndividualAmenity.razor، روش استفاده‌ی از آن در کامپوننت DemoHotel به صورت زیر است:
<div class="col-12 mt-4">
    <h4 class="text-info">Hotel Amenities</h4>
</div>
@foreach (var amenity in AmenitiesList)
{
    <IndividualAmenity Amenity="@amenity"></IndividualAmenity>
}
در اینجا بر روی لیست امکانات، یک حلقه را تشکیل داده و سپس توسط کامپوننت IndividualAmenity، هر کدام از امکانات را جداگانه نمایش داده‌ایم.

کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-05.zip
مطالب
مقدمه ای بر AutoMapper
AutoMapper کتابخانه ای ساده و سبک برای نگاشت اطلاعات یک شی به شی دیگر به صورت خودکار هست و...
اگه این پست رو مطالعه کرده باشید یه مشکل امنیتی بنام «Mass Assignment» مطرح شد.برای رفع این مشکل یک روش استفاده از ViewModel بود.
فرض کنید Model ما
 public class User
    {
        public int Id { get; set; }
       
        public string FirstName { get; set; }

        public string LastName { get; set; }

        public string UserName { get; set; }

        public string Password { get; set; }

        public bool IsAdmin { get; set; }

        public virtual ICollection<BlogPost> BlogPosts { get; set; }
    }
و ViewModel ما
 public class UserViewModel
    {
        public string FirstName { get; set; }

        public string LastName { get; set; }

        public string Password { get; set; }
    }
باشه(توجه کنید در واقع من برای View ی مورد نظرم فقط به نام , نام خانوادگی و پسورد نیاز دارم)
برای استفاده UserViewModel بعنوان Model در View ی مورد نظر باید شی UserViewModel رو با اطلاعات شی User مقدار دهی کنیم مثلا با کدی مثل این در کنترلر.
 
public ActionResult Index(int id = 1)
        {
            var user = _userService.GetById(id);
            var userViewModel = new UserViewModel
                {
                    FirstName = user.FirstName,
                    LastName = user.LastName,
                    Password = user.Password
                };

            return View(userViewModel);
        }
رهایی از نوشتن اینجور کدهای تکراری و خسته کننده باعث پیدایش AutoMapper شد...
برای استفاده از AutoMapper از نوگت استفاده میکنیم.
PM> Install-Package AutoMapper
در شروع برنامه نگاشت‌ها رو تعریف میکنم.یک روش ابداعی تعریف نگاشت‌ها در یک کلاس استاتیک و فراخوانی اون تو متد  Application_Start هست.
public static class AutoMapperWebConfiguration
    {

        public static void Configure()
        {
            ConfigureUserMapping();
        }

        private static void ConfigureUserMapping()
        {
            Mapper.CreateMap<User, UserViewModel>();
        }
    }

اولین پارامتر نوع مبدا و دومین پارامتر نوع مقصد هست.
برای انجام نگاشت هم از متد Map استفاده میکنیم.
public ActionResult Index(int id=1)
        {
            var user = _userService.GetById(id);
            var userViewModel=new UserViewModel();
            AutoMapper.Mapper.Map(user, userViewModel);
            
            return View(userViewModel);
        }
همنطور که میبنید با نوشتن چندین خط کد عملیات نگاشت اطلاعات یک شی به شی دیگه انجام شد.
ادامه دارد...
مطالب
ModelBinder سفارشی در ASP.NET MVC
زمانی که درخواستی به سمت یک Action پارامتر دار ارسال میشود، قسمت  ActionInvoker قبل از فراخوانی اکشن مربوطه، به دنبال Model Binder مناسبی برای داده‌های پارامترها می‌گردد و در صورت یافت نشدن، از ModelBinder پیش فرض ASP.NET MVC استفاده می‌کند.
اما وظیفه‌ی ModelBinder چیست ؟
ModelBinder  داده‌های ارسال شده از مرورگر را که توسط درخواست‌های HTTP (کوئری استرینگ‌ها و یا داده‌های همراه با فرم‌ها ) ارسال شده است، تبدیل به داده‌های قابل فهم برای پارامترها میکند.
به عبارتی ModelBinder وظیفه تبدیل داده‌های ارسال شده از سمت مرورگر به اشیاء NET. را دارد.
فرض کنید ما مدلی به شکل زیر داریم :
public class CustomerInfo
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime BirthDate { get; set; }
}
فیلد آخر برای ذخیره‌ی تاریخ تولد مشتری استفاده میشود. که View مربوط به آن به شکل زیر خواهد بود :

همانطور که می‌بینید تایپ کردن تاریخ به این صورت (1/1/2009  12:00:00 AM) ، هم زیاد جالب نیست و هم کمی مشکل است. به همین دلیل برخی سایت‌ها  از سه قسمت جدا برای گرفتن روز ، ماه و سال استفاده می‌کنند و در نهایت آنها را با یکدیگر ترکیب میکنند.
در این مثال ما نیز می‌خواهیم تاریخ را به صورت زیر دریافت و پس از تبدیل آن به تاریخ میلادی، آن را به کاربر نمایش دهیم :

اما هنگام ارسال فرم  به صورت بالا ، ModelBinder  توانایی تبدیل این سه ورودی (روز ، ماه و سال)  به فیلد BirthDate موجود در کلاس CustomerInfo را ندارد. به همین خاطر ما باید یک ModelBinder متناسب با نیاز خود را طراحی کنیم.
برای ایجاد یک ModelBinder  سفارشی نیاز است که از کلاس IModelBinder ارثبری و متد BindModel آن را پیاده سازی کنیم.
ساختار این اینترفیس به شکل زیر است :
public interface IModelBinder
{ 
    object BindModel(ControllerContext controllerContext, 
        ModelBindingContext bindingContext); 
}
متد BindModel حاوی 2 پارامتر است :
ControllerContext : حاوی اطلاعاتی در مورد درخواست http جاری 
ModelBindingContext : این کلاس حاوی یک property به نام Model  است که حاوی ارجاعی به مدلی که همکنون قصد پردازشش آن را دارد.
با توجه به موارد بالا کلاس ما به شکل زیر خواهد بود :
using System;
using System.Web;
using System.Web.Mvc;
using ModelBinderExample.Models;
using Persia;

namespace ModelBinderExample.CustomModelBinder
{
    // Article written for www.dotnettips.info
    public class CustomerInfoModelBinder : IModelBinder
    {
        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            HttpRequestBase request = controllerContext.HttpContext.Request;

            string firstName = request.Form.Get("FirstName");
            string lastName  = request.Form.Get("LastName");

            DateTime birthDate = this.GetMiladiDate(request);

            return new CustomerInfo()
            {
                FirstName = firstName,
                LastName  = lastName,
                BirthDate = birthDate
            };
        }

        private DateTime GetMiladiDate(HttpRequestBase request)
        {
            int day   = int.Parse(request.Form.Get("Day"));
            int month = int.Parse(request.Form.Get("Month"));
            int years = int.Parse(request.Form.Get("Years"));

            //Convert shamsi to miladi
            return Persia.Calendar.ConvertToGregorian(years, month, day, DateType.Gerigorian);
        }
    }
}
در کد بالا ایتدا موارد ارسال شده را دریافت میکنیم و توسط متد ()GetMiladiDate تاریخ دریافتی از کاربر که به صورت روز، ماه و سال میباشد را  تبدیل به میلادی میکنیم و سپس در قالب یک شی customerInfo آنها را برگشت می‌دهیم.
نکته : جهت تبدیل تاریخ شمسی به میلادی از کتابخانه‌ی Persia کمک گرفته شده است که در فایل پیوستی قرار داده شده.
کار ایجاد یک ModelBinder سفارشی تمام شده و حال نیاز است کلاس را در فایل Global.asax  در قسمت ()Application_start ثبت کنیم به شکل زیر :
protected void Application_Start()
{
     AreaRegistration.RegisterAllAreas();

     WebApiConfig.Register(GlobalConfiguration.Configuration);
     FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
     RouteConfig.RegisterRoutes(RouteTable.Routes);
     //Register New ModelBinder
     ModelBinders.Binders.Add(typeof(CustomerInfo), new CustomerInfoModelBinder());
}
و برای استفاده از این ModelBinder ، ما باید به کنترلر اطلاع دهیم که میخواهیم از چه نوع Binding استفاده کنیم به همین دلیل از attribute زیر برای انجام این کار استفاده می‌کنیم: 
[HttpPost]
public ActionResult Create([ModelBinder(typeof (CustomerInfoModelBinder))] CustomerInfo customerInfo)
{
      if (ModelState.IsValid)
      {
           ViewBag.FirstName = customerInfo.FirstName;
           ViewBag.LastName = customerInfo.LastName;
           ViewBag.BirthDate = customerInfo.BirthDate;
      }
      return View();
}

پروژه پیوستی : ModelBinder-Example.zip
مطالب
آشنایی با Gridify
Gridify چیست ؟

به طور خلاصه Gridify یک کتابخانه ساده و سریع است که عملیات‌های Filtering , Pagination و Sorting را با استفاده از شرط‌های متنی (string based) امکان پذیر میکند.
به طور مثال فرض کنید که یک API را برای دریافت لیست کاربران با نام UsersList نوشته‌اید. مثال:
 [HttpGet("[action]")]
 public async Task<IActionResult> UsersList()
 {
    var users =  await _dbContext.Users.AsNoTracking().ToListAsync();
    return Ok(users);
 }
طبیعتا بخش FrontEnd نرم افزار شما نیاز دارد این اطلاعات را به کاربر نمایش دهد. به همین جهت در بیشتر مواقع از یک گرید برای نمایش این اطلاعات استفاده میشود.
پس از دریافت اطلاعات از سرور با مشکلات زیر مواجه خواهیم شد.
  1. عدم پشتیبانی از Pagination: چون API، تمامی کاربران را به سمت کلاینت ارسال میکند؛ به همین جهت، هم با مشکل کارآیی (performance) در آینده مواجه میشویم و هم امکان گذاشتن صفحه بندی (Pagination) وجود نخواهد داشت. 
  2. عدم پشتیبانی از Sorting: اگر در گرید نمایش داده شده کاربر بخواهد اطلاعات را Sort کند، چون چنین امکانی هنوز برای API ما تعریف نشده، این عملیات سمت سرور امکان پذیر نیست.
  3. عدم پشتیبانی از Filtering: همیشه نمایش تمامی اطلاعات مفید نیست. در اکثر مواقع ما نیاز داریم تا قسمتی از اطلاعات را با شرطی خاص، برگردانیم. به طور مثال لیست کاربران فعال در سامانه یا لیست کاربران غیرفعال. 
این مشکلات بدون استفاده از هیچ کتابخانه‌ای قابل حل است ولی نه به سادگی؛ به طور مثال یا باید چندین API مختلف با امکانات مختلف بنویسیم، یا یک API را برای پشتیبانی از این موارد تغییر بسیار دهیم. من برای اینکه از بحث دور نشویم، به پیاده سازی نمونه دستی پشتیبانی از این موارد در اینجا نمی‌پردازم، چرا که اگر یکبار تلاشی را برای اینکار انجام داده باشیم، طبیعتا  مشکلات و کد کثیفی که در نهایت تولید شده است، یادآوری خواهد شد. 
برای رفع این مشکلات میتوان از کتابخانه Gridify استفاده کرد. مثال :
 [HttpGet("[action]")]
 public async Task<IActionResult> UsersList(GridifyQuery filter)
 {
    var users =  await _dbContext.Users.AsNoTracking().GridifyAsync(filter);
    return Ok(users);
 }
در مثال بالا با استفاده از کلاس GridifyQuery میتوانیم به کنترل هر سه مشکل Sorting - Pagination - Filtering سمت کلاینت بپردازیم. (در ادامه با این کلاس بیشتر آشنا خواهیم شد).


استفاده از Gridify به API‌ها محدود نمیشود. به طور کلی ما میتوانیم در هر نوع لیستی که امکان استفاده از IQueryable  را به ما میدهد از آن استفاده نماییم. 
فرض کنید در یک برنامه Console Application قصد داریم یک فیلتر را از کاربر دریافت کرده و آن را روی لیست خروجی خود اعمال کنیم. به دلیل اینکه امکان جستجوی متنی در دات نت وجود ندارد، برای انجام اینکار مجبور خواهیم شد که برای تک تک فیلدهایی که قرار است برای فیلترینگ پشتیبانی کنیم، یک query جداگانه بنویسیم؛ ولی این عملیات توسط کتابخانه Gridify امکان پذیر است. به طور مثال فرض کنید قصد داریم در لیست کاربران، کاربرانی را  با نام Ali، پیدا کنیم. 
var result = Users.AsQueryable().ApplyFiltering("name==Ali");
این کد دقیقا معادل کد زیر است.
var result = Users.AsQueryable().Where(q => q.Name == "Ali");
در اینجا با استفاده از کتابخانه Gridify ما توانستیم یک static Linq را به یک dynamic Linq که در runtime مقدار دهی خواهد شد، تغییر دهیم. به همین جهت استفاده از مورد اول در برنامه‌ی Console ما امکان پذیر است. تا اینجا ما با امکانات کلی این کتابخانه آشنا شدیم. در مقالات بعدی سعی میکنم به سایر امکانات این کتابخانه و بیشتر به جزئیات بپردازم. همینطور برای کسب اطلاعات بیشتر میتوانید به لینک زیر مراجعه نمایید.
نظرات مطالب
ایجاد یک ActionFilter جهت تمیز کردن اطلاعات ورودی در ASP.NET Core
نسخه کاملتر این متد به صورت زیر می‌باشد که ورودی  string را هم پوشش می‌دهد :
 [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
    public class SanitizeInputAttribute : ActionFilterAttribute
    {
        private readonly ISanitizer _sanitizer;

        public SanitizeInputAttribute()
        {
            _sanitizer = new Sanitizer();
        }

        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            if (filterContext.ActionArguments != null)
            {
                foreach (var parameter in filterContext.ActionArguments.ToList())
                {
                    if (parameter.Value != null)
                    {
                        var type = parameter.Value.GetType();
                        if (type == typeof(string))
                        {
                           var sanitized = _sanitizer.Sanitize(parameter.Value.ToString());
                           filterContext.ActionArguments[parameter.Key] = sanitized;
                        }
                        else
                        {
                            var properties = parameter.Value.GetType()
                                .GetProperties(BindingFlags.Instance | BindingFlags.Public)
                                .Where(x => x.CanRead && x.CanWrite && x.PropertyType == typeof(string) &&
                                            x.GetGetMethod(true).IsPublic && x.GetSetMethod(true).IsPublic);



                            foreach (var propertyInfo in properties)
                            {
                                if (propertyInfo.GetValue(parameter.Value) != null)
                                    propertyInfo.SetValue(parameter.Value,
                                        _sanitizer.Sanitize(propertyInfo.GetValue(parameter.Value).ToString()));
                            }
                        }
                    }
                }
            }

        }
    }

مطالب
صدور رخدادها از سرویس‌ها به کامپوننت‌ها در برنامه‌های Angular
در طراحی برنامه‌های Angular توصیه شده‌است تا هرگونه منطقی که مستقیما به View یک کامپوننت مرتبط نیست، به یک کلاس سرویس منتقل شود. در این بین ممکن است نیاز به صدور رخدادی از یک سرویس به خارج از آن باشد؛ چیزی مانند EventEmitter. اما EventEmitter برای سرویس‌ها طراحی نشده‌است و کاربرد صحیح آن صرفا محدود به کامپوننت‌ها است. برای حل این مساله، API سرویس ما باید یک Observable را در معرض دید استفاده کننده قرار دهد تا توسط آن بتوان رخ‌دادهایی را به کامپوننت‌های مشترک شده‌ی به آن، صادر کرد.


چگونه می‌توان رخ‌دادهایی از نوع Observable را ایجاد کرد؟

کلاس Subject پاسخی است به این پرسش. Subjectها Observableهایی هستند که می‌توانند چندین مشترک داشته باشند و رخ‌دادهایی را به مشترکین خود صادر کنند. برای کار با آن‌ها باید یک private Subject را در سرویس خود ایجاد کرد و سپس جریان منتقل شده‌ی توسط آن‌را توسط یک public Observable در اختیار مصرف کنندگان قرار داد. با فراخوانی متد next یک Subject، رخ‌دادی به مشترکین آن منتقل می‌شود.
import { Subject } from “rxjs/Subject”;

public countdown: number = 0;

private countdownEndSource = new Subject<void>();
public countdownEnd$ = this.countdownEndSource.asObservable();
مرسوم است نام Observableهایی را که قرار است رخ‌دادی را صادر کنند به $ ختم می‌کنند.
استفاده کنندگان نیز مشترک این $countdownEnd شده و هر بار که در طرف سرویس، متد next آن فراخوانی می‌شود، از به روز رسانی آن مطلع خواهند شد.


چرا مستقیما از مقدار countdown استفاده نکنیم؟

در قسمتی از سرویس فوق که ملاحظه می‌کنید، می‌توان مقدار countdown را مستقیما نیز در کامپوننت‌ها مورد استفاده قرار داد. اما این روش بهینه نیست. از این جهت که Angular باید مدام تغییرات این خاصیت را رصد کند و به آن واکنش نشان دهد. آیا بهتر نیست ما به Angular اعلام کنیم که مقدار آن تغییر کرده‌است و اکنون بهتر است View را به روز رسانی کنی؟ با ارائه‌ی مقادیر جدیدی توسط یک Observable، اکنون Angular صرفا به تغییرات آن واکنش نشان خواهد داد و دیگری نیاز به بررسی مداوم تغییرات مقدار countdown ندارد.


یک مشکل! Subject تعریف شده، مقادیر را تنها در زمان فراخوانی متد next ارائه می‌دهد و نه به صورت دیگری.

پیشتر با دسترسی مستقیم به خاصیت countdown، همواره به مقادیر آن هم دسترسی داشتیم. اما با استفاده از یک Subject، تنها زمانیکه متد next آن فراخوانی شود می‌توان به این مقدار دسترسی یافت. برای رفع این مشکل یک Subject ویژه به نام BehaviorSubject طراحی شده‌است که به محض مشترک شدن به آن، اولین و یا آخرین مقدار آن‌را می‌توان دریافت کرد.


تفاوت Subject با BehaviorSubject

BehaviorSubject مانند یک Subject است؛ با این تفاوت که همواره از وضعیت خود آگاه می‌باشد. یک BehaviorSubject:
- همواره دارای مقداری است. حتی در زمان وهله سازی، باید مقدار اولیه‌ای را برای آن مشخص کرد.
- در زمان اشتراک به آن، می‌توان آخرین مقدار موجود در آن را که ممکن است اولین مقدار آن نیز باشد، دریافت کرد.
- همواره می‌توان مقدار آن‌را توسط متد getValue بدست آورد.

و مهم‌ترین مزیت آن نسبت به Subject، همان مورد دوم است. اگر مشترک یک Subject شویم، تا متد next آن فراخوانی نشود، مقداری را دریافت نمی‌کنیم. اما همان لحظه که مشترک BehaviorSubject می‌شویم، آخرین مقدار موجود در آن‌را دریافت خواهیم کرد.
برای مثال فرض کنید کامپوننتی را دارید که به خاصیت isLoggedIn از نوع Observable یک Subject گوش فرا می‌دهد. اما اشتراک آن پس از فراخوانی متد next در این سرویس بوده‌است. از این رو این کامپوننت هیچگاه متوجه تغییر و یا مقدار نهایی isLoggedIn نخواهد شد. به همین جهت است که به BehaviorSubject نیاز داریم. در این بین مهم نیست که چه زمانی مشترک آن می‌شویم؛ همواره در زمان اشتراک، آخرین و یا اولین مقدار موجود در آن‌را دریافت خواهیم کرد.


یک مثال: بررسی عملکرد BehaviorSubject

در ادامه یک ماژول را به همراه 4 کامپوننت و یک سرویس سفارشی ایجاد می‌کنیم:
ng g m ServiceComponentCommunication -m app.module --routing
ng g c ServiceComponentCommunication/First
ng g c ServiceComponentCommunication/Second
ng g c ServiceComponentCommunication/Third
ng g c ServiceComponentCommunication/Final
ng g s ServiceComponentCommunication/Sample


هدف این است که سه کامپوننت اول، دوم و سوم را در کامپوننت final، همانند تصویر فوق نمایش دهیم.
در این بین یک سرویس انتشار اطلاعات نیز طراحی شده‌است:
import { Injectable } from "@angular/core";
import { BehaviorSubject } from "rxjs/BehaviorSubject";

@Injectable()
export class SampleService {

  private msgSource = new BehaviorSubject<string>("default service value");

  telecast$ = this.msgSource.asObservable();

  constructor() { }

  editMsg(newMsg: string) {
    this.msgSource.next(newMsg);
  }

}
کار این سرویس ارائه یک پیام از نوع BehaviorSubject از طریق خاصیت عمومی $telecast آن است که به صورت Observable در معرض دید کامپوننت‌های مشترک به آن قرار خواهد گرفت. هدف این است که کامپوننت‌ها مدام تغییرات msg را بررسی نکنند و فقط به آخرین تغییر صادر شده‌ی توسط کامپوننت که از طریق فراخوانی متد next در متد editMsg صورت می‌گیرد، واکنش نشان دهند.

در کامپوننت اول، نحوه‌ی اشتراک به این سرویس را مشاهده می‌کنید:
import { SampleService } from "./../sample.service";
import { Component, OnInit, OnDestroy } from "@angular/core";
import { Subscription } from "rxjs/Subscription";

@Component({
  selector: "app-first",
  templateUrl: "./first.component.html",
  styleUrls: ["./first.component.css"]
})
export class FirstComponent implements OnInit, OnDestroy {

  editedMsg: string;
  sampleSubscription: Subscription;

  constructor(private sampleService: SampleService) { }

  ngOnInit() {
    this.sampleSubscription = this.sampleService.telecast$.subscribe(message => {
      this.editedMsg = message;
    });
  }

  editMsg() {
    this.sampleService.editMsg(this.editedMsg);
  }

  ngOnDestroy() {
    this.sampleSubscription.unsubscribe();
  }
}
کار اشتراک در این کامپوننت در متد ngOnInit انجام شده‌است. بسیار مهم است جهت عدم بروز نشتی حافظه، در متد ngOnDestroy کار unsubscribe بر روی این اشتراک نیز صورت گیرد.
در اینجا هر زمانیکه متد next در سرویس فراخوانی شود، this.editedMsg مقدار جدیدی را دریافت می‌کند.
با این قالب:
<div class="panel panel-default">
  <div class="panel-heading">
    <h2 class="panel-title">First Component</h2>
  </div>
  <div class="panel-body">
    <p> {{editedMsg}}</p>
    <input class="form-control" type="text" [(ngModel)]="editedMsg">
    <button (click)="editMsg()" class="btn btn-primary">Change</button>
  </div>
</div>


اما اگر به تصویر دقت کنید، this.editedMsg هم اکنون دارای مقدار است (در اولین بار اجرای این کامپوننت). علت آن به داشتن مقدار اولیه‌ای در BehaviorSubject تعریف شده بر می‌گردد که در اولین بار اشتراک به آن، در اختیار مشترک قرار خواهد گرفت. این مورد، مهم‌ترین تفاوت BehaviorSubject با Subject است.
در این کامپوننت اگر کاربر مقداری را در textbox وارد کند و سپس بر روی دکمه‌ی Change کلیک نماید، این تغییر از طریق سرویس، به تمام مشترکین آن صادر خواهد شد.

کامپوننت دوم نیز مانند کامپوننت اول است، فقط یک textbox ورود اطلاعات را به همراه ندارد.


همانطور که ملاحظه می‌کنید، این کامپوننت نیز دارای مقدار اولیه‌ی BehaviorSubject است.

کامپوننت سوم، اندکی متفاوت است:
import { SampleService } from "./../sample.service";
import { Component, OnInit, OnDestroy } from "@angular/core";
import { Subscription } from "rxjs/Subscription";

@Component({
  selector: "app-third",
  templateUrl: "./third.component.html",
  styleUrls: ["./third.component.css"]
})
export class ThirdComponent implements OnInit, OnDestroy {

  message: string;
  sampleSubscription: Subscription;

  constructor(private sampleService: SampleService) { }

  ngOnInit() {
  }

  subscribe() {
    this.sampleSubscription = this.sampleService.telecast$.subscribe(message => {
      this.message = message;
    });
  }

  ngOnDestroy() {
    if (this.sampleSubscription) {
      this.sampleSubscription.unsubscribe();
    }
  }
}
در اینجا کار اشتراک در متد subscribe فراخوانی شده‌ی توسط قالب آن صورت می‌گیرد:
<div class="panel panel-default">
  <div class="panel-heading">
    <h2 class="panel-title">Third Component</h2>
  </div>
  <div class="panel-body">
    <p>{{message}}</p>
    <button (click)="subscribe()" class="btn btn-success">Subscribe</button>
  </div>
</div>


و چون این متد پس از ngOnInit قرار است توسط کاربر فراخوانی شود، مقدار message این کامپوننت هنوز خالی است.
اکنون اگر بر روی دکمه‌ی Subscribe آن کلیک کنیم، بلافاصله در لحظه‌ی اشتراک، اولین/آخرین مقدار موجود در BehaviorSubject را دریافت خواهیم کرد:


کامپوننت Final نیز تمام کامپوننت‌ها را در صفحه نمایش می‌دهد:
<div class="row">
  <div class="col-md-4">
    <app-first></app-first>
  </div>
  <div class="col-md-4">
    <app-second></app-second>
  </div>
  <div class="col-md-4">
    <app-third></app-third>
  </div>
</div>

و اگر در textbox کامپوننت اول، مقدار Test را وارد کنیم و سپس بر روی دکمه‌ی Change آن کلیک نمائیم، این مقدار به تمام کامپوننت‌های مشترک به BehaviorSubject سرویس برنامه، منتشر خواهد شد:


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید.
نظرات مطالب
بررسی قابلیت Endpoint Routing در ASP.NET Core
ارتقاء به ASP.NET Core 3.0 : تغییرات مورد نیاز در فایل آغازین برنامه جهت ارتقاء به Endpoint routing

اگر در نگارش 2.2، فایل کلاس آغازین برنامه‌ی شما چنین شکلی را دارد:
public void Configure(IApplicationBuilder app)
{
    ...

    app.UseStaticFiles();
    
    app.UseAuthentication();

    app.UseSignalR(hubs =>
    {
        hubs.MapHub<ChatHub>("/chat");
    });

    app.UseMvc(routes =>
    {
        routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
    });
}
در نگارش 3x به صورت زیر تغییر می‌کند:
public void Configure(IApplicationBuilder app)
{
    ...

    app.UseStaticFiles();
    
    app.UseRouting();

    app.UseAuthentication();
    app.UseAuthorization();
    app.UseCors();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapHub<ChatHub>("/chat");
        endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
    });
}
نکاتی که در اینجا حائز اهمیت هستند:
- متد UseRouting اضافه شده‌است.
- UseStaticFiles (در صورت نیاز) باید قبل از UseRouting فراخوانی شود.
- اگر برنامه‌ی شما به مباحث اعتبارسنجی پرداخته و در کنترلرهای آن از فیلتر Authorize استفاده می‌شود، دو تغییر مهم فراخوانی UseAuthentication و سپس UseAuthorization (که این مورد جدید هست) را پس از فراخوانی UseRouting باید انجام دهید. در غیر اینصورت در حین مرور این صفحات و قسمت‌های محافظت شده، برنامه با خطای زیر متوقف خواهد شد:
Endpoint xyz contains authorization metadata, but a middleware was not found that supports authorization.
Configure your application startup by adding app.UseAuthorization() inside the call to Configure(..) in the application startup code.
- محل فراخوانی UseCors را (در صورت نیاز) دقیقا مطابق کدهای فوق و به ترتیبی که ذکر شده باید انجام دهید.
- متدهای UseMvc و یا UseSignalR قبلی، به متد UseEndpoints تبدیل شده‌اند.
اشتراک‌ها
شرح #String Interning in C

String interning is a technique used in C# to improve the efficiency of string comparison and manipulation. It allows strings that have the same value to be stored in the same location in memory 

شرح #String Interning in C