بازخوردهای پروژه‌ها
خطا هنگام اجرا در IIS/7.5 کلیه آدرس ها نیاز به دریافت پارامتر ارسالی دارند
با سلام و تشکر از برنامه خوبتون
بنده برنامه رو بروی هاست آپلود کردم و مشکل از آنجا شروع شد که نه خطایی دریافت می‌شه و نه برنامه اجرا می‌شود و بطور اتفاقی متوجه شدم که تنها صفحه ای که لود می‌شود User/logon?returnurl=/admin
که بعد از آن با قرار دادن پارامتر در انتهای هر لینک یا آدرس برنامه به درستی کار کرد (http://blob.com/?blob)و مشخص شد مشکل از routeconfig برنامه می‌باشد که متاسفانه به دلیل حطا ندادن برنامه و اجرا شدن آن در محیط لوکال بدون ایراد راه حل مناسبی پیدانشد
            routes.MapRouteLowercase("Default", "{controller}/{action}/{id}", new
            {
                area = "",
                controller = "Home",
                action = "Index",
                id = UrlParameter.Optional,
            }, new[] { "Iris.Web.Controllers" }
                );
routing سایت را مجبور به دریافت پارامتر نموده./
مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 5 - فعال سازی صفحات مخصوص توسعه دهنده‌ها
اولین Middleware موجود در بسته‌ی Microsoft.AspNetCore.Diagnostics را در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 3 - Middleware چیست؟» با نمایش welcome page آن، بررسی کردیم. در این مطلب سایر صفحات مخصوص توسعه دهنده‌های موجود در این بسته را مرور خواهیم کرد.



مشاهده‌ی جزئیات اطلاعات سرور و بسته‌های نصب شده‌ی بر روی آن

در نگارش‌های قبل از RTM، با فراخوانی app.UseRuntimeInfoPage در متد Configure کلاس Startup، ریز اطلاعاتی از وضعیت سرور و بسته‌های موجود در آن با مراجعه‌ی به آدرس http://site/runtimeinfo نمایش داده می‌شدند. این مورد خاص از نگارش RTM حذف شده‌است (احتمالا به دلایل امنیتی). البته اگر علاقمند به بررسی کدهای آن باشید، هنوز تاریخچه‌ی آن در GitHub موجود است .


مدیریت خطاها در برنامه‌های ASP.NET Core 1.0

به متد Configure کلاس Startup مراجعه کرد و یک سطر استثناء را به ابتدای کدهای Middleware انتهایی آن اضافه کنید:
public void Configure(IApplicationBuilder app)
{
    app.Run(async context =>
    {
        throw new Exception("Generic Error");
        await context.Response.WriteAsync("Hello DNT!");
    });
}
هدف این است که بررسی کنیم اگر استثنایی در یک Middleware رخ داد، برنامه چه خروجی را نمایش می‌دهد.
در این حالت اگر برنامه را اجرا کنیم، این خروجی را دریافت خواهیم کرد:


و اگر به وضعیت بازگشت داده شده‌ی از طرف سرور دقت کنیم، فقط internal server error است:


در اینجا برخلاف نگارش‌های قبلی ASP.NET، دیگر حتی صفحه‌ی زرد رنگ معروف نمایش خطاها (yellow screen of death) نیز فعال نیستند. برای فعال سازی آن نیاز است Middleware مرتبط با آن‌را به نحو ذیل به برنامه معرفی کنیم:
public void Configure(IApplicationBuilder app)
{
   app.UseDeveloperExceptionPage();
پس از این فعال سازی، اگر مجددا برنامه را اجرا کنید، این خروجی را می‌توان در مرورگر مشاهده کرد:


به دلایل امنیتی و عدم نشت اطلاعات سمت سرور و خصوصا عدم امکان دیباگ از راه دور برنامه توسط مهاجمین، این Middleware به صورت پیش فرض فعال نیست.
بنابراین این سؤال مطرح می‌شود که چگونه می‌توان این صفحه را تنها در حین توسعه‌ی برنامه نمایش داد؟
پاسخ آن به نحوه‌ی طراحی متد Configure در کلاس Startup بر می‌گردد. این متد امضای ثابتی را ندارد. هر تعداد سرویسی را که نیاز داشتید، می‌توانید به عنوان پارامتر این متد معرفی کنید و کار تزریق وابستگی‌ها و نمونه سازی آن‌ها، توسط امکانات توکار ASP.NET Core به صورت خودکار انجام می‌شود. برای مثال سرویس IApplicationBuilder، یکی از سرویس‌های توکار ASP.NET Core است و برای تنظیم آن نیازی نیست تا کار خاصی را انجام دهیم. به همین جهت است که صرفا معرفی اینترفیس آن در این متد، وهله‌ای را از سازنده‌ی برنامه در اختیار ما قرار می‌دهد. سرویس‌ها را در مطلبی جداگانه مورد بررسی قرار خواهیم داد، اما فعلا جهت تکمیل بحث باید درنظر داشت که یکی دیگر از سرویس‌های توکار ASP.NET Core، به نام IHostingEnvironment، اطلاعاتی را در مورد محیطی که برنامه را در آن اجرا می‌کنیم در اختیار ما قرار می‌دهد:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
روش معرفی آن نیز همانند روش معرفی سرویس IApplicationBuilder است و تنها کافی است به عنوان یک پارامتر جدید متد Configure معرفی شود. وهله سازی و تنظیمات آن نیز به صورت خودکار توسط ASP.NET Core انجام خواهد شد. اکنون پس از تزریق این سرویس، می‌توان صفحه‌ی نمایش جزئیات خطاها را تنها محدود به محیط توسعه کرد:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
در مورد انواع محیط‌های توسعه، در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 2 - بررسی ساختار جدید Solution» در انتهای بحث به «نقش فایل launchsetting.json» اشاره شد. اگر بر روی پروژه کلیک راست کرده و به صفحه‌ی properties آن مراجعه کنید و یا دوبار کلیک بر روی گره properties، یک چنین تنظیمی را می‌توان مشاهده کرد:


این متغیر محیطی می‌تواند سه مقدار Development, Staging و Production را داشته باشد و بر اساس این متغیر و مقدار آن است که یکی از سه متد ذیل مفهوم پیدا می‌کنند و true یا false را باز می‌گردانند:
if(env.IsDevelopment()){ }
if(env.IsProduction()){ }
if(env.IsStaging()){ }


نمایش و مدیریت خطاها در حالت Production

از app.UseDeveloperExceptionPage صرفا در حالت توسعه استفاده کنید؛ چون اطلاعات نمایش داده شده‌ی توسط آن، بیش از اندازه برای مهاجمین مفید است. اما در حالت توزیع نهایی بر روی سرور چه باید کرد؟
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler(errorHandlingPath: "/MyControllerName/SomeActionMethodName");
    }
در این حالت از Middleware دیگری به نام ExceptionHandler با فراخوانی app.UseExceptionHandler می‌توان کمک گرفت. کار آن هدایت کاربر به صفحه‌ای خاص از برنامه، در صورت بروز استثنایی است. در اینجا شما می‌توانید یک صفحه‌ی عمومی «خطایی رخ داده‌است» را بدون ذکر هیچ نوع جزئیاتی، به کاربر نمایش دهید.
به علاوه در اینجا (در این قسمت خاص برنامه که توسط پارامتر errorHandlingPath مشخص شده‌است) با استفاده از قطعه کد ذیل، دسترسی کاملی را به اطلاعات خطای رخ داده، جهت ثبت و لاگ آن دارید:
 var feature = HttpContext.Features.Get<IExceptionHandlerFeature>();
var error = feature?.Error;


بررسی میان‌افزار StatusCode

این میان افزار برای مدیریت responseهایی که status code آن‌ها بین 400 تا 600 هستند، طراحی شده‌است. بر اساس این شماره‌ها، می‌توان خطای خاصی را بازگشت داده و یا کاربر را به یک صفحه یا کنترلر خاصی در برنامه، هدایت کرد.
در حالت عادی ثبت آن
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseStatusCodePages();
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler(errorHandlingPath: "/MyControllerName/SomeActionMethodName");
    }
}
تنها یک خروجی متنی را نمایش می‌دهد.


برای نمونه در اینجا مسیری درخواست داده شده‌است که توسط برنامه پردازش نمی‌شود و وجود ندارد.
اگر خواستید تا status code واقعی، به کاربر بازگشت داده شود، اما خروجی نمایش داده شده را سفارشی سازی کنید، می‌توانید از متد UseStatusCodePagesWithReExecute استفاده نمائید:
 app.UseStatusCodePagesWithReExecute("/MyControllerName/SomeActionMethodName/{0}");
در اینجا کاربر هنوز status code مساوی 404 را دریافت می‌کند (مناسب برای موتورهای جستجو)، اما اکشن متد خاصی در برنامه، سبب بازگشت یک View سفارشی به کاربر خواهد شد (بجای نمایش یک متن ساده). پارامتر {0} آن نیز همان شماره status code بازگشتی است.
نظرات مطالب
AngularJS #2
با سلام. اگر partialview ما نیاز به اعتبارسنجی داشته باشد، ما در سمت سرور می‌توانستیم یک سری attribute برای اعتبارسنجی هر خاصیت درون ویومدل‌های خود ایجاد کنیم، و خود razor این attributeها را درون html نهایی رندر می‌کرد. آیا این امکان در سمت کلاینت برای انگولار نیز وجود دارد؟
مطالب
اضافه کردن پیوست به فایل‌های PDF با استفاده از iTextSharp

فایل PDF موجود عجیب و غریبی است. می‌شود به آن فایل پیوست اضافه کرد. مثلا اگر یک راهنمای آموزشی را با فرمت PDF تهیه می‌کنید، لازم نیست تا فایل‌های مرتبط با آن‌را جداگانه ارائه دهید. می‌شود تمام این‌ها را داخل همان فایل PDF مدفون کرد. روش انجام اینکار به کمک iTextSharp ساده است اما چند نکته را نیز به همراه دارد:

using System.Diagnostics;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace PDFAttachment
{
class Program
{
static void Main(string[] args)
{
using (var pdfDoc = new Document(PageSize.A4))
{
var pdfWriter = PdfWriter.GetInstance(pdfDoc, new FileStream("Test.pdf", FileMode.Create));
pdfDoc.Open();

pdfDoc.Add(new Phrase("Test"));

var fs = PdfFileSpecification.FileEmbedded(pdfWriter, @"C:\path\logo.png", "logo.png", null);
pdfWriter.AddFileAttachment("توضیحات",fs);
}

Process.Start("Test.pdf");
}
}
}

در ساده‌ترین حالت ممکن، با استفاده از متد AddFileAttachmen شیء PdfWriter می‌توان پیوستی را به یک فایل PDF در حال تولید اضافه کرد. اگر به فایل نهایی مراجعه کنیم و همچنین قسمت attachments را هم دستی در Adobe reader انتخاب نمائیم، شکل زیر حاصل خواهد شد:


روش متداول بکارگرفته شده دو مشکل را به همراه دارد:
  • قسمت modified مقدار دهی نشده است.
  • پنل مربوط به پیوست‌ها باید دستی باز شود.

نحوه مقدار دهی ستون modified پس از تعریف یک PdfDictionary و قرار دادن PdfName.MODDATE در آن، به صورت زیر است:

using System.Diagnostics;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace PDFAttachment
{
class Program
{
static void Main(string[] args)
{
using (var pdfDoc = new Document(PageSize.A4))
{
var pdfWriter = PdfWriter.GetInstance(pdfDoc, new FileStream("Test.pdf", FileMode.Create));
pdfDoc.Open();

pdfDoc.Add(new Phrase("Test"));

var filePath = @"C:\path\logo.png";
var fileInfo = new FileInfo(filePath);
var pdfDictionary = new PdfDictionary();
pdfDictionary.Put(PdfName.MODDATE, new PdfDate(fileInfo.LastWriteTime));
var fs = PdfFileSpecification.FileEmbedded(pdfWriter, filePath, fileInfo.Name, null, true, null, pdfDictionary);
pdfWriter.AddFileAttachment("توضیحات", fs);
}

Process.Start("Test.pdf");
}
}
}

که اینبار خروجی زیر را به همراه دارد:


و برای نمایش خودکار پنل پیوست‌ها در Adobe reader به طوری که کاربر نهایی متوجه وجود این فایل‌های پیوست شده گردد، می‌توان ViewerPreferences شیء pdfWriter را مقدار دهی نمود:

pdfWriter.ViewerPreferences = PdfWriter.PageModeUseAttachments;

در مورد فایل‌های موجود چطور؟ آیا می‌توان به یک فایل PDF از پیش تهیه شده هم فایل پیوست کرد؟
پاسخ: بله. باید از امکانات شیء PdfReader استفاده کرد:

using System.Diagnostics;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace PDFAttachment
{
class Program
{
static void Main(string[] args)
{
var reader = new PdfReader("Test.pdf");
using (var stamper = new PdfStamper(reader, new FileStream("newTest.pdf", FileMode.Create)))
{
var filePath = @"C:\path\logo.png";
addAttachment(stamper, filePath, "توضیحات");
stamper.Close();
}

Process.Start("newTest.pdf");
}

private static void addAttachment(PdfStamper stamper, string filePath, string description)
{
var fileInfo = new FileInfo(filePath);
var pdfDictionary = new PdfDictionary();
pdfDictionary.Put(PdfName.MODDATE, new PdfDate(fileInfo.LastWriteTime));
var pdfWriter = stamper.Writer;
var fs = PdfFileSpecification.FileEmbedded(pdfWriter, filePath, fileInfo.Name, null, true, null, pdfDictionary);
stamper.AddFileAttachment(description, fs);
}
}
}

در اینجا به کمک کلاس PdfReader، یک فایل موجود خوانده شده و سپس با استفاده از امکانات کلاس PdfStamper که خاصیت Writer آن همان pdfWriter است می‌توان فایل مورد نظر را به فایل موجود افزود.


مطالب
Angular Material 6x - قسمت سوم - طرحبندی برنامه
پس از نصب بسته‌ی Angular Material و آشنایی با سیستم Angular Flex Layout برای پوشش کمبود سیستم طرحبندی آن، در این قسمت طرح ابتدایی دفترچه تلفن این سری را پیگیری می‌کنیم تا به طراحی زیر برای حالت‌های دسکتاپ و موبایل برسیم:



در اینجا توسط کامپوننت sidenav، کار نمایش لیست تماس‌ها صورت می‌گیرد و نمایش این کامپوننت واکنشگرا است. به این معنا که در اندازه‌های صفحات نمایشی بزرگ، نمایان است و در صفحات نمایشی کوچک، مخفی خواهد شد. در بالای صفحه یک Toolbar قرار دارد که همیشه نمایان است و از آن برای نمایش گزینه‌های منوی برنامه استفاده می‌کنیم. همچنین ناحیه‌ی main content را هم مشاهده می‌کنید که با انتخاب هر شخص از لیست تماس‌ها، جزئیات او در این قسمت نمایش داده خواهد شد.


ایجاد ماژول مدیریت تماس‌ها

در قسمت اول، برنامه را به همراه تنظیمات ابتدایی مسیریابی آن ایجاد کردیم که نتیجه‌ی آن تولید فایل src\app\app-routing.module.ts می‌باشد:
 ng new MaterialAngularClient --routing
در ادامه ماژول مخصوص مدیریت تماس‌ها را ایجاد می‌کنیم که به آن feature module هم گفته می‌شود. برای این منظور دستور زیر را اجرا کنید:
 ng g m ContactManager -m app.module --routing


این دستور ماژول جدید contact-manager را به همراه تنظیمات ابتدایی مسیریابی و همچنین به روز رسانی app.module، برای درج آن، ایجاد می‌کند. البته در این حالت نیاز است به app.module.ts مراجعه کرد و محل درج آن‌را تغییر داد:
import { ContactManagerModule } from "./contact-manager/contact-manager.module";

@NgModule({
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    CoreModule,
    SharedModule.forRoot(),
    ContactManagerModule,
    AppRoutingModule
  ],
})
export class AppModule { }
به صورت پیش‌فرض ContactManagerModule، پس از AppRoutingModule ذکر می‌شود و چون مسیر catch all را در ادامه در AppRoutingModule قرار می‌دهیم، دیگر هیچگاه مسیریابی‌های ContactManagerModule پردازش نخواهند شد. به همین جهت باید آن‌را پیش از AppRoutingModule قرار داد.
سپس دستور زیر را اجرا می‌کنیم تا کامپوننت contact-manager-app در ماژول contact-manager ایجاد شود:
 ng g c contact-manager/ContactManagerApp --no-spec
با این خروجی:
CREATE src/app/contact-manager/contact-manager-app/contact-manager-app.component.html (38 bytes)
CREATE src/app/contact-manager/contact-manager-app/contact-manager-app.component.ts (319 bytes)
CREATE src/app/contact-manager/contact-manager-app/contact-manager-app.component.css (0 bytes)
UPDATE src/app/contact-manager/contact-manager.module.ts (436 bytes)
همانطور که ملاحظه می‌کنید این دستور کار به روز رسانی contact-manager.module را نیز جهت معرفی این کامپوننت جدید انجام داده‌است.
این کامپوننت به عنوان میزبان سایر کامپوننت‌هایی که در مقدمه‌ی بحث عنوان شدند، عمل می‌کند. این کامپوننت‌ها را به صورت زیر در پوشه‌ی components ایجاد می‌کنیم:
ng g c contact-manager/components/toolbar --no-spec
ng g c contact-manager/components/main-content --no-spec
ng g c contact-manager/components/sidenav --no-spec
با این خروجی:



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

در ادامه به src\app\app-routing.module.ts مراجعه کرده و این ماژول جدید را به صورت lazy load معرفی می‌کنیم:
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";

const routes: Routes = [
  { path: "contactmanager", loadChildren: "./contact-manager/contact-manager.module#ContactManagerModule" },
  { path: "", redirectTo: "contactmanager", pathMatch: "full" },
  { path: "**", redirectTo: "contactmanager" }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
در اینجا تنظیمات صفحه‌ی پیش‌فرض برنامه و همچنین not found و یا catch all را نیز مشاهده می‌کنید که هر دو به contactmanager تنظیم شده‌اند.

سپس تنظیمات مسیریابی ماژول مدیریت تماس‌ها را در فایل src\app\contact-manager\contact-manager-routing.module.ts به صورت زیر تغییر می‌دهیم:
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";

import { MainContentComponent } from "./components/main-content/main-content.component";
import { ContactManagerAppComponent } from "./contact-manager-app/contact-manager-app.component";

const routes: Routes = [
  {
    path: "", component: ContactManagerAppComponent,
    children: [
      { path: "", component: MainContentComponent }
    ]
  },
  { path: "**", redirectTo: "" }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class ContactManagerRoutingModule { }
در اولین بار بارگذاری این ماژول، کامپوننت ContactManagerApp بارگذاری می‌شود. همچنین مسیر not found نیز به همان مسیر ریشه‌ی این کامپوننت تنظیم شده‌است.
کامپوننت ContactManagerApp که کار هاست سایر کامپوننت‌های این ماژول را بر عهده دارد، دارای router-outlet خاص خود خواهد بود. به همین جهت برای آن children تعریف شده‌است که مسیر پیش‌فرض آن، بارگذاری کامپوننت MainContent است.
بنابراین نیاز است به فایل contact-manager-app\contact-manager-app.component.html مراجعه و ابتدا منوی کنار صفحه را به آن افزود:
 <app-sidenav></app-sidenav>
app-sidenav همان selector کامپوننت sidenav است که در فایل sidenav\sidenav.component.ts قابل مشاهده‌است.
سپس در قالب sidenav\sidenav.component.html، کار تعریف toolbar و همچنین router-outlet را انجام می‌دهیم:
<app-toolbar></app-toolbar>
<router-outlet></router-outlet>
بر اساس مسیریابی که تعریف کردیم، router-outlet کار نمایش Main Content را انجام می‌دهد.


معرفی Angular Material به ماژول جدید مدیریت تماس‌ها

در قسمت اول، یک فایل material.module.ts را ایجاد کردیم که به همراه تعریف تمامی کامپوننت‌های Angular Material بود. سپس آن‌را به shared.module.ts افزودیم که حاوی تعریف ماژول فرم‌ها و همچنین Flex Layout نیز هست. به همین جهت برای معرفی این‌ها به این ماژول جدید تنها کافی است در فایل src\app\contact-manager\contact-manager.module.ts در قسمت imports، کار معرفی SharedModule صورت گیرد:
import { SharedModule } from "../shared/shared.module";

@NgModule({
  imports: [
    CommonModule,
    SharedModule,
    ContactManagerRoutingModule
  ]
})
export class ContactManagerModule { }
تا اینجا پیش از ادامه‌ی کار، فرمان ng serve -o را صادر کنید تا مطمئن شویم همه چیز به درستی قابل دریافت و کامپایل است.


پس از اجرای برنامه مشاهده می‌کنید که ابتدا ماژول مدیریت تماس‌ها بارگذاری شده‌است و سپس contact-manager-app عمل و sidenav را بارگذاری کرده و آن نیز سبب نمایش کامپوننت toolbar و سپس main-content شده‌است.


تنظیم طرحبندی برنامه توسط کامپوننت‌های Angular Material و همچنین Flex Layout


پس از این تنظیمات اکنون نوبت به تنظیم طرحبندی برنامه‌است و آن‌را با قراردادن کامپوننت Sidenav بسته‌ی Angular Material شروع می‌کنیم:
<mat-sidenav-container *ngIf="shouldRun">
   <mat-sidenav mode="side" opened>
      Sidenav content
   </mat-sidenav>
   Primary content
</mat-sidenav-container>
از کامپوننت Sidenav عموما برای طراحی منوی راهبری سایت استفاده می‌شود. این کامپوننت در سه حالت قابل تنظیم است که بر روی نحوه‌ی نمایش Sidenav content و Primary content تاثیرگذار است:
- Over: قسمت Sidenav content بر روی Primary content قرار می‌گیرد.
- Push: قسمت Sidenav content قسمت Primary content را از سر راه خود بر می‌دارد.
- Side:  قسمت Sidenav content در کنار Primary content قرار می‌گیرد.

در اینجا از حالت Side، در صفحات نمایشی بزرگ (اولین تصویر این قسمت) و از حالت Over، در صفحات نمایشی موبایل (مانند تصویر زیر) استفاده خواهیم کرد.



در ابتدا کدهای کامل هر سه کامپوننت و سپس توضیحات آن‌ها را مشاهده خواهید کرد:


تنظیم margin در CSS اصلی برنامه

زمانیکه sidenav و toolbar را بر روی صفحه قرار می‌دهیم، فاصله‌ای بین آن‌ها و لبه‌های صفحه مشاهده می‌شود. برای اینکه این فاصله را به صفر برسانیم، به فایل src\styles.css مراجعه کرده و margin بدنه‌ی صفحه را به صفر تنظیم می‌کنیم:
@import "~@angular/material/prebuilt-themes/indigo-pink.css";

body {
  margin: 0;
}


طراحی قالب main content

<mat-card>
  <h1>Main content</h1>
</mat-card>
برای نمایش main-content از کامپوننت ساده‌ی card استفاده شده‌است که به فایل main-content\main-content.component.html اضافه خواهد شد. این قسمت در نهایت توسط router-outlet نمایش داده می‌شود.


طرای منوی راهبری واکنشگرا

sidenav\sidenav.component.css 
 sidenav\sidenav.component.html 
.app-sidenav-container {
  position: fixed;
}

.app-sidenav {
  width: 240px;
}

.wrapper {
  margin: 50px;
}
<mat-sidenav-container fxLayout="row"
         fxFill>
  <mat-sidenav #sidenav 
    fxFlex="1 1 100%" [opened]="!isScreenSmall"
    [mode]="isScreenSmall ? 'over' : 'side'">
    <mat-toolbar color="primary">
      Contacts
    </mat-toolbar>
    <mat-list>
      <mat-list-item>Item 1</mat-list-item>
      <mat-list-item>Item 2</mat-list-item>
      <mat-list-item>Item 3</mat-list-item>
    </mat-list>
  </mat-sidenav>

  <mat-sidenav-content fxLayout="column" fxFlex="1 1 100%" fxFill>
    <app-toolbar (toggleSidenav)="sidenav.toggle()"></app-toolbar>
    <div>
      <router-outlet></router-outlet>
    </div>
  </mat-sidenav-content>
</mat-sidenav-container>

- نمای کلی صفحه در این قسمت طراحی شده‌است. sidenav-container که در برگیرنده‌ی اصلی است، به fxLayout از نوع row تنظیم شده‌است. یعنی mat-sidenav و mat-sidenav-content دو ستون آن‌را از چپ به راست تشکیل می‌دهند و درون یک ردیف، سیلان خواهند یافت. همچنین می‌خواهیم این container کل صفحه را پر کند، به همین جهت از fxFill استفاده شده‌است. این fxFill اعمال شده‌ی به container، زمانی عمل خواهد کرد که position آن در css، به fixed تنظیم شود که اینکار در css این قالب و در کلاس app-sidenav-container آن انجام شده‌است.
- سپس toolbar و همچنین router-outlet که main content را نمایش می‌دهند، داخل sidenav-content قرار گرفته‌اند و هر دو با هم، ستون دوم این طرحبندی را تشکیل می‌دهند. به همین جهت fxLayout آن به column تنظیم شده‌است (ستون اول آن، لیست تماس‌ها است و ستون دوم آن، از دو ردیف toolbar و main-content تشکیل می‌شود).
- اگر دقت کنید یک template reference variable به نام sidenav# به container اعمال شده‌است. از آن، جهت باز و بسته کردن sidenav استفاده می‌شود:
<app-toolbar (toggleSidenav)="sidenav.toggle()"></app-toolbar>
زمانیکه در toolbar بر روی دکمه‌ی منوی همبرگری کلیک شود، متد sidenav.toggle فراخوانی شده و این مورد سبب نمایان شدن مجدد sidenav خواهد شد. در این مورد در ادامه بیشتر بحث می‌کنیم.
- mat-sidenav از دو قسمت تشکیل شده‌است. بالای آن توسط mat-toolbar صرفا کلمه‌ی Contacts نمایش داده می‌شود و سپس ذیل آن، لیست فرضی تماس‌ها توسط کامپوننت mat-list قرار گرفته‌اند (تا فعلا خالی نباشد. در قسمت‌های بعدی آن‌را پویا خواهیم کرد). رنگ تولبار آن‌را ("color="primary) نیز به primary تنظیم کرده‌ایم تا خاکستری پیش‌فرض آن نباشد.
- کار کلاس mat-elevation-z10 این است که بین sidenav و main-content یک سایه‌ی سه بعدی را ایجاد کند که آن‌را در تصاویر مشاهده می‌کنید. عددی که پس از z قرار می‌گیرد، میزان عمق سایه را مشخص می‌کند.
- این قسمت از sidenav به همراه دو خاصیت opened و همچنین mode است که به مقدار isScreenSmall عکس العمل نشان می‌دهند:
<mat-sidenav  [opened]="!isScreenSmall" [mode]="isScreenSmall ? 'over' : 'side'">
در اینجا می‌خواهیم اگر اندازه‌ی صفحه xs شد، حالت over بجای حالت پیش‌فرض side تنظیم شود. یعنی در حالت موبایل و اندازه‌ی صفحه‌ی کوچک، sidenav در صورت فراخوانی متد sidenav.toggle در toolbar، بر روی قسمتی از صفحه ظاهر شود و نه در کنار آن که مخصوص حالت تمام صفحه است. همچنین می‌خواهیم اگر اندازه‌ی صفحه کوچک بود، sidenav بسته شود و نمایان نباشد. به همین جهت خاصیت opened آن به isScreenSmall تنظیم شده‌است. مدیریت خاصیت isScreenSmall در کدهای این کامپوننت به صورت زیر انجام می‌شود:

محتویات فایل sidenav\sidenav.component.ts:
import { Component, OnDestroy, OnInit } from "@angular/core";
import { MediaChange, ObservableMedia } from "@angular/flex-layout";
import { Subscription } from "rxjs";

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

  isScreenSmall = false;
  watcher: Subscription;

  constructor(private media: ObservableMedia) {
    this.watcher = media.subscribe((change: MediaChange) => {
      this.isScreenSmall = change.mqAlias === "xs";
    });
  }

  ngOnInit() {
  }

  ngOnDestroy() {
    this.watcher.unsubscribe();
  }
}
ObservableMedia را در انتهای قسمت دوم این سری بررسی کردیم. کار آن گوش فرادادن به تغییرات اندازه‌ی صفحه‌است. زمانیکه mqAlias آن برای مثال مساوی xs شد، یعنی در حالت موبایل قرار داریم. در این حالت مقدار خاصیت isScreenSmall به true تنظیم می‌شود و برعکس. با توجه به اینکه این media یک Observable است، نیاز است کار unsubscribe از آن نیز همواره در کدها وجود داشته باشد که نمونه‌ای از آن در متد ngOnDestroy صورت گرفته‌است.
تاثیر خاصیت isScreenSmall بر روی دو خاصیت opened و mode کامپوننت sidenav را در دو تصویر زیر مشاهده می‌کنید. اگر اندازه‌ی صفحه کوچک شود، ابتدا sidenav مخفی می‌شود. اگر کاربر بر روی دکمه‌ی منوی همبرگری کلیک کند، سبب نمایش مجدد sidenav، اینبار با حالت over و بر روی محتوای زیرین آن خواهد شد:




طراحی نوار ابزار واکنشگرا

کدهای قالب و css تولبار (ستون دوم طرحبندی کلی صفحه) را در ادامه مشاهده می‌کنید:
  toolbar\toolbar.component.css    toolbar\toolbar.component.html 
 
.sidenav-toggle {
  padding: 0;
  margin: 8px;
  min-width:56px;
}
 
<mat-toolbar color="primary">
  <button mat-button fxHide fxHide.xs="false" 
              class="sidenav-toggle" (click)="toggleSidenav.emit()">
    <mat-icon>menu</mat-icon>
  </button>

  <span>Contact Manager</span>
</mat-toolbar>

با توجه به استفاده‌ی از fxHide، یعنی دکمه‌ی نمایش منوی همبرگری در تمام حالات مخفی خواهد بود. برای لغو آن و نمایش آن در حالت موبایل، از حالت واکنشگرای آن یعنی fxHide.xs استفاده می‌کنیم (قسمت «کار با API واکنشگرای Angular Flex layout» در مطلب قبلی این سری). به این ترتیب زمانیکه کاربر اندازه‌ی صفحه را کوچک می‌کند و یا اندازه‌ی واقعی صفحه‌ی نمایش او کوچک است، این دکمه نمایان خواهد شد.
همچنین در sidenav یک چنین تعریفی را داریم:
 <app-toolbar (toggleSidenav)="sidenav.toggle()"></app-toolbar>
بروز رخ‌داد toggleSidenav سبب خواهد شد که متد sidenav.toggle فراخوانی شود و سبب نمایش sidenav در اندازه‌های کوچک صفحه‌ی نمایشی گردد. این رخ‌داد سفارشی را نیز به رخ‌داد click دکمه‌ی همبرگری تولبار متصل کرده‌ایم که با کلیک بر روی آن، کار emit آن صورت می‌گیرد. این emit نیز سبب خواهد شد تا sidenav.toggle متصل به سمتی دیگر، فعال شود. نحوه‌ی تعریف این رخ‌داد سفارشی را در کدهای کامپوننت تولبار، در ادامه مشاهده می‌کنید:

محتویات فایل toolbar\toolbar.component.ts:
import { Component, EventEmitter, OnInit, Output } from "@angular/core";

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

  @Output() toggleSidenav = new EventEmitter<void>();

  constructor() { }

  ngOnInit() {
  }
}


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: MaterialAngularClient-02.zip
برای اجرای آن نیز ابتدا فایل restore.bat و سپس فایل ng-serve.bat را اجرا کنید. پس از اجرای برنامه، یکبار آن‌را در حالت تمام صفحه و بار دیگر با کوچک‌تر کردن اندازه‌ی مرورگر آزمایش کنید. در حالتیکه به اندازه‌ی موبایل رسیدید، بر روی دکمه‌ی همبرگری نمایان شده کلیک کنید تا عکس العمل آن و نمایش مجدد sidenav را در حالت over، مشاهده نمائید.
نظرات مطالب
تفاوت AngularJS با KnockoutJS
مقایسه از این قبیل زیاد است  
اگر نگاهی به جامعه کاربری استفاده کننده کنیم به طور مثال در Stackoverflow با تگ Angular حدود 25 هزار سوال پرسیده شده در حالی که با تگ Backbone حدود 14 هزار سوال پرسیده شده.
Angular امکانات کاملی برای توسعه‌ی SPA در بر دارد.
نظرات مطالب
نمایش حداکثر اندازه مجاز فایل قابل آپلود به کاربر، در ASP.Net
سلام استاد نصیری
در این موردی که فرمودین اگر machin.config قفل باشه اون خط تاثیر گذار نخواهد بود آیا وقتی که قبلش به تگ clear استفاده کنیم هم باز تاثیرگذار نیست؟چون تگ clear تنظیمات والد رو بی تاثیر میکنه.
ممنون از مطلب مفیدتون
موفق باشید
مطالب
RequireJs
در طراحی و توسعه پروژه‌های تحت وب در مقیاس بزرگ برای اینکه مدیریت پروژه راحت‌تر شود کد‌های مورد نظر را در چند ماژول قرار می‌دهند در نتیجه کد‌های پروژه در بلاک‌های کوچک‌تر قرار خواهند داشت. نوشتن پروژه به صورت ماژولار قابلیت استفاده مجدد از کد‌های برنامه را افزایش می‌دهد، علاوه بر آن مدیریت پروژه در فاز نگهداری آسان‌تر خواهد شد؛ از طرفی دیگر وابستگی بین ماژول‌ها و تامین آن ها، همواره مهم‌ترین مفهوم برای توسعه دهندگان پروژه‌های وب است. RequireJs یکی از فریم ورک‌های محبوب برای مدیریت وابستگی‌های بین ماژول‌ها است و کاربرد اصلی آن راحت سازی مفهوم modularity در اینگونه پروژه هاست.
پروژه‌های بزرگ عموما دارای یک یا چند فایل جاوااسکریپ هستند که برای استفاده از آن‌ها در صفحات از تگ script استفاده می‌شود. اگر این فایل‌ها دارای وابستگی به هم باشند، ترتیب فراخوانی این فایل در تگ script مهم است. برای مثال:
یک پروژه دارای فایل‌های زیر خواهد بود:

purchase.js
function purchaseProduct(){
  console.log("Function : purchaseProduct");
  var credits = getCredits();
  if(credits > 0){
    reserveProduct();
    return true;
  }
  return false;
}
product.js
function reserveProduct(){
  console.log("Function : reserveProduct"); 
  return true;
}
credits.js
function getCredits(){
  console.log("Function : getCredits"); 
  var credits = "100";
  return credits;
}
همان طور که در فایل‌های بالا مشاهده می‌کنید در فایل purchase.js از دو فایل دیگر استفاده شده است. در فایل main.js پروژه کد زیر را برای استفاده از فایل‌های بالا می‌نویسیم:
main.js
var result = purchaseProduct();
فرض کنید در فایل Html تگ‌های script ما به صورت زیر باشد:
<script src="products.js"></script>
<script src="purchase.js"></script>
<script src="main.js"></script>
<script src="credits.js"></script>
از آنجا که لود فایل purchase.js وابستگی مستقیم به لود فایل credits.js دارد و از طرفی دیگر به دلیل این که فایل credit.Js بعد از تمام فایل‌ها لود می‌شود در نتیجه برنامه اجرا نخواهد شد و با خطای زیر روبرو می‌شویم:

فقط کافیست تصور کنید که تعداد و حجم کد‌های این فایل‌ها در یک پروژه زیاد باشد یا حتی این فایل‌ها توسط یک برنامه نویس دیگر تهیه و تدوین شده باشد؛ در نتیجه به خاطر سپردن این وابستگی‌ها به طور قطع کار سخت خواهد بود و در خیلی موارد طافت فرسا.

 RequireJs چگونه برای حل این مشکل به ما کمک می‌کند؟
با استفاده از فریم ورک RequireJs کد‌های ما به ماژول‌های کوچک‌تر شکسته می‌شود و وابستگی ماژول‌ها در تنظیمات لود فایل ثبت می‌شود. در ضمن این فریم ورک با مرورگرها جدید و محبوب کاملا سازگار است. برای شروع فایل‌های مورد نیاز را از اینجا دانلود نمایید. البته می‌توانید از nuget هم استفاده کنید:
PM> Install-Package RequireJS
در مثال بالا فایل‌های جاوااسکریپ به صورت زیر تغییر خواهد کرد:
purchase.js
define(["credits","products"], function(credits,products) { 
  console.log("Function : purchaseProduct"); 
  return {
    purchaseProduct: function() { 
      var credit = credits.getCredits();
      if(credit > 0){
        products.reserveProduct();
        return true;
      }
      return false;
    }
  }
});
همان طور که مشاهده می‌کنید در فایل بالا از دستور define برای تعریف ماژول استفاده شده است و نام دو ماژول products و credits را به صورت پارامتر برای مشخص کردن وابستگی ماژول‌ها تعریف کردیم.
products.js
define(function(products) {
  return {
    reserveProduct: function() {
      console.log("Function : reserveProduct"); 
      return true;
    }
  }
});
credits.js
define(function() {
  console.log("Function : getCredits"); 
  return {
    getCredits: function() {
      var credits = "100";
      return credits;
    }
  }
});
به دلیل این که فایل‌های بالا وابستگی به ماژول‌های دیگر ندارند در نتیجه دستور define فقط شامل تعریف تابع است.
کافیست در فایل main.js، برای استفاده از فایل purchase.js از دستور require استفاده کنید.
require(['purchase'], function( purchase ) {
    var result = purchase.purchaseProduct(); 
});
مهم‌ترین مزیتی که این روش دارد این است که دیگر نیازی به نوشتن تگ‌های Script (آن هم به ترتیب درست) برای فراخوانی فایل‌های جاوااسکریپتی نخواهد بود؛ از طرفی دیگر وابستگی بین این فایل‌ها در هنگام تعریف ماژول مشخص خواهد شد. از آن به بعد وظیفه تامین فایل‌های مورد نیاز برای لود ماژول بر عهده RequireJs است.

تفاوت بین دستور require و define
دستور define صرفا برای تعیین وابستگی ماژول استفاده می‌شود در حالی که دستور require برای فراخوانی و لود ماژول کاربرد دارد و به نوعی به عنوان نقطه شروع اجرای برنامه خواهد بود.
در پست بعدی به پیاده سازی مثال بالا به کمک RequireJs در قالب یک پروژه Asp.Net MVC خواهم پرداخت.
مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت دوازدهم- یکپارچه سازی با اکانت گوگل
در مطلب قبلی «استفاده از تامین کننده‌های هویت خارجی»، نحوه‌ی استفاده از اکانت‌های ویندوزی کاربران یک شبکه، به عنوان یک تامین کننده‌ی هویت خارجی بررسی شد. در ادامه می‌خواهیم از اطلاعات اکانت گوگل و IDP مبتنی بر OAuth 2.0 آن به عنوان یک تامین کننده‌ی هویت خارجی دیگر استفاده کنیم.


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

اگر بخواهیم از گوگل به عنوان یک IDP ثالث در IdentityServer استفاده کنیم، نیاز است در ابتدا برنامه‌ی IDP خود را به آن معرفی و در آنجا ثبت کنیم. برای این منظور مراحل زیر را طی خواهیم کرد:

1- مراجعه به developer console گوگل و ایجاد یک پروژه‌ی جدید
https://console.developers.google.com
در صفحه‌ی باز شده، بر روی دکمه‌ی select project در صفحه و یا لینک select a project در نوار ابزار آن کلیک کنید. در اینجا دکمه‌ی new project و یا create را مشاهده خواهید کرد. هر دوی این مفاهیم به صفحه‌ی زیر ختم می‌شوند:


در اینجا نامی دلخواه را وارد کرده و بر روی دکمه‌ی create کلیک کنید.

2- فعالسازی API بر روی این پروژه‌ی جدید


در ادامه بر روی لینک Enable APIs And Services کلیک کنید و سپس google+ api را جستجو نمائید.
پس از ظاهر شدن آن، این گزینه را انتخاب و در صفحه‌ی بعدی، آن‌را با کلیک بر روی دکمه‌ی enable، فعال کنید.

3- ایجاد credentials


در اینجا بر روی دکمه‌ی create credentials کلیک کرده و در صفحه‌ی بعدی، این سه گزینه را با مقادیر مشخص شده، تکمیل کنید:
• Which API are you using? – Google+ API
• Where will you be calling the API from? – Web server (e.g. node.js, Tomcat)
• What data will you be accessing? – User data
سپس در ذیل این صفحه بر روی دکمه‌ی «What credentials do I need» کلیک کنید تا به صفحه‌ی پس از آن هدایت شوید. اینجا است که مشخصات کلاینت OAuth 2.0 تکمیل می‌شوند. در این صفحه، سه گزینه‌ی آن‌را به صورت زیر تکمیل کنید:
• نام: همان مقدار پیش‌فرض آن
• Authorized JavaScript origins: آن‌را خالی بگذارید.
• Authorized redirect URIs: این مورد همان callback address مربوط به IDP ما است که در اینجا آن‌را با آدرس زیر مقدار دهی خواهیم کرد.
https://localhost:6001/signin-google
این آدرس، به آدرس IDP لوکال ما اشاره می‌کند و مسیر signin-google/ آن باید به همین نحو تنظیم شود تا توسط برنامه شناسایی شود.
سپس در ذیل این صفحه بر روی دکمه‌ی «Create OAuth 2.0 Client ID» کلیک کنید تا به صفحه‌ی «Set up the OAuth 2.0 consent screen» بعدی هدایت شوید. در اینجا دو گزینه‌ی آن‌را به صورت زیر تکمیل کنید:
- Email address: همان آدرس ایمیل واقعی شما است.
- Product name shown to users: یک نام دلخواه است. نام برنامه‌ی خود را برای نمونه ImageGallery وارد کنید.
برای ادامه بر روی دکمه‌ی Continue کلیک نمائید.

4- دریافت credentials
در پایان این گردش کاری، به صفحه‌ی نهایی «Download credentials» می‌رسیم. در اینجا بر روی دکمه‌ی download کلیک کنید تا ClientId  و ClientSecret خود را توسط فایلی به نام client_id.json دریافت نمائید.
سپس بر روی دکمه‌ی Done در ذیل صفحه کلیک کنید تا این پروسه خاتمه یابد.


تنظیم برنامه‌ی IDP برای استفاده‌ی از محتویات فایل client_id.json

پس از پایان عملیات ایجاد یک برنامه‌ی جدید در گوگل و فعالسازی Google+ API در آن، یک فایل client_id.json را دریافت می‌کنیم که اطلاعات آن باید به صورت زیر به فایل آغازین برنامه‌ی IDP اضافه شود:
الف) تکمیل فایل src\IDP\DNT.IDP\appsettings.json
{
  "Authentication": {
    "Google": {
      "ClientId": "xxxx",
      "ClientSecret": "xxxx"
    }
  }
}
در اینجا مقادیر خواص client_secret و client_id موجود در فایل client_id.json دریافت شده‌ی از گوگل را به صورت فوق به فایل appsettings.json اضافه می‌کنیم.
ب) تکمیل اطلاعات گوگل در کلاس آغازین برنامه
namespace DNT.IDP
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
   // ... 

            services.AddAuthentication()
                .AddGoogle(authenticationScheme: "Google", configureOptions: options =>
                {
                    options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
                    options.ClientId = Configuration["Authentication:Google:ClientId"];
                    options.ClientSecret = Configuration["Authentication:Google:ClientSecret"];
                });
        }
خود ASP.NET Core از تعریف اطلاعات اکانت‌های Google, Facebook, Twitter, Microsoft Account و  OpenID Connect پشتیبانی می‌کند که در اینجا نحوه‌ی تنظیم اکانت گوگل آن‌را مشاهده می‌کنید.
- authenticationScheme تنظیم شده باید یک عبارت منحصربفرد باشد.
- همچنین SignInScheme یک چنین مقداری را در اصل دارد:
 public const string ExternalCookieAuthenticationScheme = "idsrv.external";
از این نام برای تشکیل قسمتی از نام کوکی که اطلاعات اعتبارسنجی گوگل در آن ذخیره می‌شود، کمک گرفته خواهد شد.


آزمایش اعتبارسنجی کاربران توسط اکانت گوگل آن‌ها

اکنون که تنظیمات اکانت گوگل به پایان رسید و همچنین به برنامه نیز معرفی شد، برنامه‌ها را اجرا کنید. مشاهده خواهید کرد که امکان لاگین توسط اکانت گوگل نیز به صورت خودکار به صفحه‌ی لاگین IDP ما اضافه شده‌است:


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


پس از آن، از طرف گوگل به صورت خودکار به IDP (همان آدرسی که در فیلد Authorized redirect URIs وارد کردیم)، هدایت شده و callback رخ‌داده، ما را به سمت صفحه‌ی ثبت اطلاعات کاربر جدید هدایت می‌کند. این تنظیمات را در قسمت قبل ایجاد کردیم:
namespace DNT.IDP.Controllers.Account
{
    [SecurityHeaders]
    [AllowAnonymous]
    public class ExternalController : Controller
    {
        public async Task<IActionResult> Callback()
        {
            var result = await HttpContext.AuthenticateAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme);
            var returnUrl = result.Properties.Items["returnUrl"] ?? "~/";

            var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result);
            if (user == null)
            {
                // user = AutoProvisionUser(provider, providerUserId, claims);
                
                var returnUrlAfterRegistration = Url.Action("Callback", new { returnUrl = returnUrl });
                var continueWithUrl = Url.Action("RegisterUser", "UserRegistration" ,
                    new { returnUrl = returnUrlAfterRegistration, provider = provider, providerUserId = providerUserId });
                return Redirect(continueWithUrl);
            }
اگر خروجی متد FindUserFromExternalProvider آن null باشد، یعنی کاربری که از سمت تامین کننده‌ی هویت خارجی/گوگل به برنامه‌ی ما وارد شده‌است، دارای اکانتی در سمت IDP نیست. به همین جهت او را به سمت صفحه‌ی ثبت نام کاربر هدایت می‌کنیم.
در اینجا نحوه‌ی اصلاح اکشن متد Callback را جهت هدایت یک کاربر جدید به صفحه‌ی ثبت نام و تکمیل اطلاعات مورد نیاز IDP را مشاهده می‌کنید.
returnUrl ارسالی به اکشن متد RegisterUser، به همین اکشن متد جاری اشاره می‌کند. یعنی کاربر پس از تکمیل اطلاعات و اینبار نال نبودن user او، گردش کاری جاری را ادامه خواهد داد و به برنامه با این هویت جدید وارد می‌شود.


اتصال کاربر وارد شده‌ی از طریق یک IDP خارجی به اکانتی که هم اکنون در سطح IDP ما موجود است

تا اینجا اگر کاربری از طریق یک IDP خارجی به برنامه وارد شود، او را به صفحه‌ی ثبت نام کاربر هدایت کرده و پس از دریافت اطلاعات او، اکانت خارجی او را به اکانتی جدید که در IDP خود ایجاد می‌کنیم، متصل خواهیم کرد. به همین جهت بار دومی که این کاربر به همین ترتیب وارد سایت می‌شود، دیگر صفحه‌ی ثبت نام و تکمیل اطلاعات را مشاهده نمی‌کند. اما ممکن است کاربری که برای اولین بار از طریق یک IDP خارجی به سایت ما وارد شده‌است، هم اکنون دارای یک اکانت دیگری در سطح IDP ما باشد؛ در اینجا فقط اتصالی بین این دو صورت نگرفته‌است. بنابراین در این حالت بجای ایجاد یک اکانت جدید، بهتر است از همین اکانت موجود استفاده کرد و صرفا اتصال UserLogins او را تکمیل نمود.
به همین جهت ابتدا نیاز است لیست Claims بازگشتی از گوگل را بررسی کنیم:
var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result);  
foreach (var claim in claims)
{
   _logger.LogInformation($"External provider[{provider}] info-> claim:{claim.Type}, value:{claim.Value}");
}
در اینجا پس از فراخوانی FindUserFromExternalProvider، لیست Claims بازگشت داده شده‌ی توسط IDP خارجی را لاگ می‌کنیم که در حالت استفاده‌ی از گوگل چنین خروجی را دارد:
External provider[Google] info-> claim:http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name, value:Vahid N.
External provider[Google] info-> claim:http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname, value:Vahid
External provider[Google] info-> claim:http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname, value:N.
External provider[Google] info-> claim:urn:google:profile, value:https://plus.google.com/105013528531611201860
External provider[Google] info-> claim:http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress, value:my.name@gmail.com
بنابراین اگر بخواهیم بر اساس این claims بازگشتی از گوگل، کاربر جاری در بانک اطلاعاتی خود را بیابیم، فقط کافی است اطلاعات claim مخصوص emailaddress آن‌را مورد استفاده قرار دهیم:
        [HttpGet]
        public async Task<IActionResult> Callback()
        {
            // ...

            var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result);
            if (user == null)
            {
                // user wasn't found by provider, but maybe one exists with the same email address?  
                if (provider == "Google")
                {
                    // email claim from Google
                    var email = claims.FirstOrDefault(c =>
                        c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress");
                    if (email != null)
                    {
                        var userByEmail = await _usersService.GetUserByEmailAsync(email.Value);
                        if (userByEmail != null)
                        {
                            // add Google as a provider for this user
                            await _usersService.AddUserLoginAsync(userByEmail.SubjectId, provider, providerUserId);

                            // redirect to ExternalLoginCallback
                            var continueWithUrlAfterAddingUserLogin =
                                Url.Action("Callback", new {returnUrl = returnUrl});
                            return Redirect(continueWithUrlAfterAddingUserLogin);
                        }
                    }
                }


                var returnUrlAfterRegistration = Url.Action("Callback", new {returnUrl = returnUrl});
                var continueWithUrl = Url.Action("RegisterUser", "UserRegistration",
                    new {returnUrl = returnUrlAfterRegistration, provider = provider, providerUserId = providerUserId});
                return Redirect(continueWithUrl);
            }
در اینجا ابتدا بررسی شده‌است که آیا کاربر جاری واکشی شده‌ی از بانک اطلاعاتی نال است؟ اگر بله، اینبار بجای هدایت مستقیم او به صفحه‌ی ثبت کاربر و تکمیل مشخصات او، مقدار email این کاربر را از لیست claims بازگشتی او از طرف گوگل، استخراج می‌کنیم. سپس بر این اساس اگر کاربری در بانک اطلاعاتی وجود داشت، تنها اطلاعات تکمیلی UserLogin او را که در اینجا خالی است، به اکانت گوگل او متصل می‌کنیم. به این ترتیب دیگر کاربر نیازی نخواهد داشت تا به صفحه‌ی ثبت اطلاعات تکمیلی هدایت شود و یا اینکه بی‌جهت رکورد User جدیدی را مخصوص او به بانک اطلاعاتی اضافه کنیم.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشه‌ی 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 وارد کنید.
پروژه‌ها
مشاهده گر سایت dotnettips
پروژه انجام شده، برنامه ای برای خواندن مطالب همین سایت با قابلیت هایی مانند جستجو و تم‌های مختلف مخصوصا Dark می‌باشد.
از این پروژه می‌توانید هم به صورت آفلاین و یا آنلاین از آدرس زیر استفاده کنید.
توضیحات و عکس از پروژه را می‌توانید در آدرس مخزن پروژه ببینید.
موارد قابل بهبود :
  • قسمت (بر اساس تگ) به درستی کار نمی‌کند.
  • پروژه بر اساس فایل json سایت کار می‌کند که می‌توان آن را به xml تغییر داد.
  • سرعت لود مطالب به علت سنگین بودن فایل سایت جاری کمی در نسخه آنلاین طول می‌کشد.