پشتیبانی GitHub از RTL به صورت توکار
The Internet Archive now lets you run old-school Mac OS (and its games) in your browser
سعی مجدد خودکار درخواستها توسط کتابخانهی RxJS
با استفاده از عملگر retry میتوان به صورت خودکار، درخواستهای شکست خورده را به تعداد باری که مشخص میشود، تکرار کرد:
import { Observable } from "rxjs"; import { catchError, map, retry } from "rxjs/operators"; postEmployeeForm(employee: Employee): Observable<Employee> { const headers = new HttpHeaders({ "Content-Type": "application/json" }); return this.http .post(this.baseUrl, employee, { headers: headers }) .pipe( map((response: any) => response["fields"] || {}), catchError(this.handleError), retry(3) ); }
مشکل این روش در عدم وجود مکثی بین درخواستها است. در اینجا تمام درخواستهای سعی مجدد، بلافاصله به سمت سرور ارسال میشوند. همچنین نمیتوان مشخص کرد که اگر مثلا خطای timeout وجود داشت، اینکار را تکرار کن و نه برای سایر حالات.
سفارشی سازی سعی مجدد خودکار درخواستها، توسط کتابخانهی RxJS
برای اینکه بتوان کنترل بیشتری را بر روی سعیهای مجدد انجام شده داشت، میتوان از عملگر retryWhen بجای retry استفاده کرد:
import { Observable, of, throwError as observableThrowError, throwError } from "rxjs"; import { catchError, delay, map, retryWhen, take } from "rxjs/operators"; postEmployeeForm(employee: Employee): Observable<Employee> { const headers = new HttpHeaders({ "Content-Type": "application/json" }); return this.http .post(this.baseUrl, employee, { headers: headers }) .pipe( map((response: any) => response["fields"] || {}), retryWhen(errors => errors.pipe( delay(1000), take(3) )), catchError(this.handleError) ); }
در ادامه اگر بخواهیم صرفا به خطاهای خاصی واکنش نشان دهیم میتوان به صورت زیر عمل کرد:
import { Observable, of, throwError as observableThrowError, throwError } from "rxjs"; import { catchError, delay, map, mergeMap, retryWhen, take } from "rxjs/operators"; postEmployeeForm(employee: Employee): Observable<Employee> { const headers = new HttpHeaders({ "Content-Type": "application/json" }); return this.http .post(this.baseUrl, employee, { headers: headers }) .pipe( map((response: any) => response["fields"] || {}), retryWhen(errors => errors.pipe( mergeMap((error: HttpErrorResponse, retryAttempt: number) => { if (retryAttempt === 3 - 1) { console.log(`HTTP call failed after 3 retries.`); return throwError(error); // no retry } switch (error.status) { case 400: case 404: return throwError(error); // no retry } return of(error); // retry }), delay(1000), take(3) )), catchError(this.handleError) ); }
در داخل mergeMap اگر یک Observable معمولی بازگشت داده شود، به معنای صدور مجوز سعی مجدد است؛ اما اگر throwError بازگشت داده شود، دقیقا در همان لحظه کار retryWhen و سعیهای مجدد خاتمه خواهد یافت. برای مثال در اینجا پس از 2 بار سعی مجدد، اصل خطا صادر میشود که سبب خواهد شد قسمت catchError اجرا شود و یا روش صرفنظر کردن از خطاهای با شمارههای 400 یا 404 را نیز مشاهده میکنید. برای مثال اگر از سمت سرور خطای 404 و یا «یافت نشد» صادر شد، return throwError سبب خاتمهی سعیهای مجدد و خاتمهی عملیات retryWhen میشود.
سعی مجدد تمام درخواستهای شکست خوردهی کل برنامه
روش فوق را باید به ازای تک تک درخواستهای HTTP برنامه تکرار کنیم. برای مدیریت یک چنین اعمال تکراری در برنامههای Angular میتوان یک HttpInterceptor سفارشی را تدارک دید و توسط آن تمام درخواستهای HTTP سراسر برنامه را به صورت متمرکز تحت نظر قرار داد:
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { Observable, of, throwError } from "rxjs"; import { catchError, delay, mergeMap, retryWhen, take } from "rxjs/operators"; @Injectable() export class RetryInterceptor implements HttpInterceptor { private delayBetweenRetriesMs = 1000; private numberOfRetries = 3; intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { return next.handle(request).pipe( retryWhen(errors => errors.pipe( mergeMap((error: HttpErrorResponse, retryAttempt: number) => { if (retryAttempt === this.numberOfRetries - 1) { console.log(`HTTP call '${request.method} ${request.url}' failed after ${this.numberOfRetries} retries.`); return throwError(error); // no retry } switch (error.status) { case 400: case 404: return throwError(error); // no retry } return of(error); // retry }), delay(this.delayBetweenRetriesMs), take(this.numberOfRetries) )), catchError((error: any, caught: Observable<HttpEvent<any>>) => { console.error({ error, caught }); if (error.status === 401 || error.status === 403) { // this.router.navigate(["/accessDenied"]); } return throwError(error); }) ); } }
روش ثبت آن در قسمت providers مربوط به core.module.ts به صورت زیر است:
providers: [ { provide: HTTP_INTERCEPTORS, useClass: RetryInterceptor, multi: true },
معرفی کد آنالیزر Serilog
همانطور که میدانید Serilog قویترین و محبوبترین کتابخانه Logging در دات نت است. اگر از آن استفاده میکنید پیشنهاد میکنم افزونه و کتابخونه زیر رو هم نصب کنین
ابزار Serilog Analyzer یک آنالیزر roslyn-based برای Serilog بوده و خطاهای رایج و اشتباهات متداول به هنگام استفاده از Serilog را گوشزد کرده و اصلاح میکند.
مروری بر ASP.NET Core View Component
Partial Views and Child Actions are one the most used features of ASP.NET MVC. Partial Views provides us a way to create a reusable component that can be used in multiple Views. There are Actions which can be marked as Child Actions and these cannot be invoked via URL but inside views or partial views. Child Actions are no more available with ASP.NET Core. View Components are new way to implement this feature in ASP.NET Core.
امن سازی برنامههای ASP.NET Core توسط IdentityServer 4x - قسمت دوازدهم- یکپارچه سازی با اکانت گوگل
ثبت یک برنامهی جدید در گوگل
اگر بخواهیم از گوگل به عنوان یک 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
• نام: همان مقدار پیشفرض آن
• Authorized JavaScript origins: آنرا خالی بگذارید.
• Authorized redirect URIs: این مورد همان callback address مربوط به IDP ما است که در اینجا آنرا با آدرس زیر مقدار دهی خواهیم کرد.
https://localhost:6001/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" } } }
ب) تکمیل اطلاعات گوگل در کلاس آغازین برنامه
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"]; }); }
- 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); }
در اینجا نحوهی اصلاح اکشن متد 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}"); }
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
[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); }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی 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 وارد کنید.
The key highlights to cover this month include:
- Announcing SQL Server 2019 support
- New notebook features
- Announcing PowerShell notebooks
- Announcing collapsible code cells
- Performance improvements in notebooks
- Announcing Jupyter Books
- General availability of Schema Compare and SQL Server Dacpac extensions
- Announcing Visual Studio IntelliCode extension
- Bug fixes
آشنایی با نسخه تحت لینوکس SQL Server
Did you know that DotVVM can be used to incrementally modernize old ASP.NET Web Forms applications and lift them to .NET Core? It is much easier than doing a full rewrite, and the application can be deployed at any time during the entire process.
- Install DotVVM NuGet package in your Web Forms site
- Create a DotVVM master page using the same CSS
- Start converting ASPX pages to DotHTML syntax, one at a time
- When all the Web Forms pages are gone, change your CSPROJ to use .NET Core