Target framework version C# language version default .NET 5.x C# 9.0 .NET Core 3.x C# 8.0 .NET Core 2.x C# 7.3 .NET Standard 2.1 C# 8.0 .NET Standard 2.0 C# 7.3 .NET Standard 1.x C# 7.3 .NET Framework all C# 7.3
{ "compilerOptions": { "strict": true, "removeComments": false, "sourceMap": false, "noEmitOnError": true, "target": "ES2020", "module": "ES2020", "outDir": "wwwroot/scripts" }, "include": [ "Scripts/**/*.ts" ], "exclude": [ "node_modules" ] }
<Project Sdk="Microsoft.NET.Sdk.Razor"> <ItemGroup> <PackageReference Include="Microsoft.TypeScript.MSBuild" Version="4.3.5"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> </ItemGroup> <ItemGroup> <Content Remove="tsconfig.json" /> </ItemGroup> <ItemGroup> <TypeScriptCompile Include="tsconfig.json"> <CopyToOutputDirectory>Never</CopyToOutputDirectory> </TypeScriptCompile> </ItemGroup> </Project>
window.exampleJsFunctions = { showPrompt: function (message) { return prompt(message, 'Type anything here'); } };
namespace JSInteropWithTypeScript { export class ExampleJsFunctions { public showPrompt(message: string): string { return prompt(message, 'Type anything here'); } } } export function showPrompt(message: string): string { var fns = new JSInteropWithTypeScript. ExampleJsFunctions(); return fns.showPrompt(message); }
private const string ScriptPath = "./_content/----namespace-here---/scripts/file.js"; private IJSObjectReference scriptModule;
protected override async Task OnAfterRenderAsync(bool firstRender) { if (scriptModule == null) scriptModule = await JSRuntime.InvokeAsync<IJSObjectReference>("import", ScriptPath);
await scriptModule.InvokeVoidAsync("exported fn name", params);
public partial class MyComponent : IAsyncDisposable
public async ValueTask DisposeAsync() { if (scriptModule != null) { await scriptModule.DisposeAsync(); } }
در کتابخانهی Microsoft AspNetCore Identity میتوان با این کد، فیلد Email را منحصر بهفرد کرد:
//Program.cs file builder.Services.AddIdentity<User, Role>(options => { options.User.RequireUniqueEmail = true; }).AddEntityFrameworkStores<DatabaseContext>();
برنامه را اجرا و درخواستها را یکی یکی به سمت سرور ارسال میکنیم و اگر ایمیل تکراری باشد به ما خطا میده و میگه: "ایمیل تکراری است".
ولی مشکل اینجاست که کد بالا فیلد Email رو داخل دیتابیس منحصر بهفرد نمیکنه و فقط از سمت نرم افزار بررسی تکراری بودن ایمیل رو انجام میده. حالا اگه ما با استفاده از نرم افزارهای "تست برنامههای وب" مثل Apache JMeter تعداد زیادی درخواست را به سمت برنامهمان ارسال کنیم و بعد رکوردهای داخل جدول کاربران را نگاه کنیم، با وجود اینکه داخل نرم افزارمان پراپرتی Email را منحصر بهفرد کردهایم، ولی چندین رکورد، با یک ایمیل مشابه در داخل جدول User وجود خواهد داشت.
برای تست این سناریو، برنامه Apache JMeter را از این لینک دانلود میکنیم (در بخش Binaries فایل zip رو دانلود می کنیم).
نکته: داشتن jdk ورژن 8 به بالا پیش نیاز است. برای اینکه بدونید ورژن جاوای سیستمتون چنده، داخل cmd دستور java -version رو صادر کنید.
اگه تمایل به نصب، یا به روز رسانی jdk را داشتید، میتونید از این لینک استفاده کنید و بسته به سیستم عاملتون، یکی از تبهای Windows, macOS یا Linux رو انتخاب کنید و فایل مورد نظر رو دانلود کنید (برای Windows فایل x64 Compressed Archive رو دانلود و نصب میکنیم).
حالا فایل دانلود شده JMeter رو استخراج میکنیم، وارد پوشهی bin میشیم و فایل jmeter.bat رو اجرا میکنیم تا برنامهی JMeter اجرا بشه.
قبل از اینکه وارد برنامه JMeter بشیم، کدهای برنامه رو بررسی میکنیم.
موجودیت کاربر:
public class User : IdentityUser<int>;
ویوو مدل ساخت کاربر:
public class UserViewModel { public string UserName { get; set; } = null!; public string Email { get; set; } = null!; public string Password { get; set; } = null!; }
کنترلر ساخت کاربر:
[ApiController] [Route("/api/[controller]")] public class UserController(UserManager<User> userManager) : Controller { [HttpPost] public async Task<IActionResult> Add(UserViewModel model) { var user = new User { UserName = model.UserName, Email = model.Email }; var result = await userManager.CreateAsync(user, model.Password); if (result.Succeeded) { return Ok(); } return BadRequest(result.Errors); } }
حالا وارد برنامه JMeter میشیم و اولین کاری که باید انجام بدیم این است که مشخص کنیم چند درخواست را در چند ثانیه قرار است ارسال کنیم. برای اینکار در برنامه JMeter روی TestPlan کلیک راست میکنیم و بعد:
Add -> Threads (Users) -> Thread Group
حالا باید بر روی Thread Group کلیک کنیم و بعد در بخش Number of threads (users) تعداد درخواستهایی را که قرار است به سمت سرور ارسال کنیم، مشخص کنیم؛ برای مثال عدد 100.
گزینه Ramp-up period (seconds) برای اینه که مشخص کنیم این 100 درخواست قرار است در چند ثانیه ارسال شوند که آن را روی 0.1 ثانیه قرار میدهیم تا درخواستها را با سرعت بسیار زیاد ارسال کند.
الان باید مشخص کنیم چه دیتایی قرار است به سمت سرور ارسال شود:
برای اینکار باید یک Http Request اضافه کنیم. برای این منظور روی Thread Group که از قبل ایجاد کردیم، کلیک راست میکنیم و بعد:
Add -> Sampler -> Http Request
حالا روی Http Request کلیک میکنیم و متد ارسال درخواست رو که روی Get هست، به Post تغییر میدیم و بعد Path رو هم به آدرسی که قراره دیتا رو بهش ارسال کنیم، تغییر میدهیم:
https://localhost:7091/api/User
حالا پایینتر Body Data رو انتخاب میکنیم و دیتایی رو که قراره به سمت سرور ارسال کنیم، در قالب Json وارد میکنیم:
{ "UserName": "payam${__Random(1000, 9999999)}", "Email": "payam@gmail.com", "Password": "123456aA@" }
چون بخش UserName در پایگاه داده منحصر بهفرد است، با این دستور:
${__Random(1000, 9999999)}
یک عدد Random رو به UserName اضافه میکنیم که دچار خطا نشیم.
حالا فقط باید یک Header رو هم به درخواستمون اضافه کنیم، برای اینکار روی Http Request که از قبل ایجاد کردیم، کلیک راست میکنیم و بعد:
Add -> Config Element -> Http Header Manager
حالا روی دکمهی Add در پایین صفحه کلیک میکنیم و این Header رو اضافه میکنیم:
Name: Content-Type Value: application/json
همچنین میتونیم یک View result رو هم اضافه کنیم تا وضعیت تمامی درخواستهای ارسال شده رو مشاهده کنیم. برای اینکار روی Http Request که از قبل ایجاد کردیم، کلیک راست میکنیم و بعد:
Add -> Listener -> View Results Tree
فایل Backup، برای اینکه مراحل بالا رو سریعتر انجام بدید:
File -> Open
حالا بر روی دکمهی سبز رنگ Play در Toolbar بالا کلیک میکنیم تا تمامی درخواست ها را به سمت سرور ارسال کنه و همچنین میتونیم از طریق View result tree ببینیم که چند درخواست موفقیت آمیز و چند درخواست ناموفق انجام شدهاست.
حالا اگر وارد پایگاه داده بشیم، میبینیم که چندین رکورد، با Email یکسان، در جدول User وجود داره:
در حالیکه ایمیل رو در تنظیمات کتابخانه Microsoft AspNetCore Identity به صورت Unique تعریف کردهایم:
//Program.cs file builder.Services.AddIdentity<User, Role>(options => { options.User.RequireUniqueEmail = true; }).AddEntityFrameworkStores<DatabaseContext>();
دلیل این مشکل این است که درخواستها در قالب یک صف، یک به یک اجرا نمیشوند؛ بلکه به صورت همزمان فریم ورک ASP.NET Core برای بالا بردن سرعت اجرای درخواستها از تمامی Thread هایی که در اختیارش هست استفاده میکند و در چندین Thread جداگانه، درخواستهایی رو به کنترلر User میفرسته و در نتیجه، در یک زمان مشابه، چندین درخواست ارسال میشه که آیا یک ایمیل برای مثال با مقدار payam@yahoo.com وجود داره یا خیر و در تمامی درخواستها چون همزمان انجام شده، جواب خیر است. یعنی ایمیل تکراری با آن مقدار، در پایگاه داده وجود ندارد و تمامی درخواستهایی که همزمان به سرور رسیدهاند، کاربر جدید را با ایمیل مشابهی ایجاد میکنند.
این مشکل را میتوان حتی در سایتهای فروش بلیط نیز پیدا کرد؛ یعنی چند نفر یک صندلی را رزرو کردهاند و همزمان وارد درگاه پرداخت شده و هزینهایی را برای آن پرداخت میکنند. اگر آن درخواستها را وارد صف نکنیم، امکان دارد که یک صندلی را به چند نفر بفروشیم. این سناریو برای زمانی است که در پایگاه داده، فیلدها را Unique تعریف نکرده باشیم. هر چند که اگر فیلدها را نیز Unique تعریف کرده باشیم تا یک صندلی را به چند نفر نفروشیم، در آن صورت هم برنامه دچار خطای 500 خواهد شد. پس بهتر است که حتی در زمانهایی هم که فیلدها را Unique تعریف میکنیم، باز هم از ورود چند درخواست همزمان به اکشن رزرو صندلی جلوگیری کنیم.
راه حل
برای حل این مشکل میتوان از Lock statement استفاده کرد که این راه حل نیز یک مشکل دارد که در ادامه به آن اشاره خواهم کرد.
Lock statement به ما این امکان رو میده تا اگر بخشی از کد ما در یک Thread در حال اجرا شدن است، Thread دیگری به آن بخش از کد، دسترسی نداشته باشد و منتظر بماند تا آن Thread کارش با کد ما تموم شود و بعد Thread جدید بتونه کد مارو اجرا کنه.
نحوه استفاده از Lock statement هم بسیار سادهاست:
public class TestClass { private static readonly object _lock1 = new(); public void Method1() { lock (_lock1) { // Body } } }
حالا باید کدهای خودمون رو در بخش Body اضافه کنیم تا دیگر چندین Thread به صورت همزمان، کدهای ما رو اجرا نکنند.
اما یک مشکل وجود داره و آن این است که ما نمیتوانیم در Lock statement، از کلمه کلیدی await استفاده کنیم؛ در حالیکه برای ساخت User جدید باید از await استفاده کنیم:
var result = await userManager.CreateAsync(user, model.Password);
برای حل این مشکل میتوان از کلاس SemaphoreSlim بجای کلمهی کلیدی lock استفاده کرد:
[ApiController] [Route("/api/[controller]")] public class UserController(UserManager<User> userManager) : Controller { private static readonly SemaphoreSlim Semaphore = new (initialCount: 1, maxCount: 1); [HttpPost] public async Task<IActionResult> Add(UserViewModel model) { var user = new User { UserName = model.UserName, Email = model.Email }; // Acquire the semaphore await Semaphore.WaitAsync(); try { // Perform user creation var result = await userManager.CreateAsync(user, model.Password); if (result.Succeeded) { return Ok(); } return BadRequest(result.Errors); } finally { // Release the semaphore Semaphore.Release(); } } }
این کلاس نیز مانند lock عمل میکند، ولی تواناییهای بیشتری را در اختیار ما قرار میدهد؛ برای مثال میتوان تعیین کرد که همزمان چند ترد میتوانند به این کد دسترسی داشته باشند؛ در حالیکه در lock statement فقط یک Thread میتوانست به کد دسترسی داشته باشد. مزیت دیگر کلاس SemaphoreSlim این است که میتوان برای اجرای کدمان Timeout در نظر گرفت تا از بلاک شدن نامحدود Thread جلوگیری کنیم.
با فراخوانی await semaphore.WaitAsync، دسترسی کد ما توسط سایر Thread ها محدود و با فراخوانی Release، کد ما توسط سایر Thread ها قابل دسترسی میشود.
مشکل قفل کردن Thread ها
هنگام قفل کردن Thread ها، مشکلی وجود دارد و آن این است که اگر برنامهی ما روی چندین سرور مختلف اجرا شود، این روش جوابگو نخواهد بود؛ چون قفل کردن Thread روی یک سرور تاثیری در سایر سرورها جهت محدود کردن دسترسی به کد ما ندارد. اما به صورت کلی میتوان از این روش برای بخشهایی خاص از برنامههایمان استفاده کنیم.
پیاده سازی با کمک الگوی AOP
برای اینکه کارمون راحت تر بشه، میتونیم کدهای بالا رو به یک Attribute انتقال بدیم و از اون Attribute در بالای اکشنهامون استفاده کنیم تا کل عملیات اکشنهامونو رو در یک Thread قفل کنیم:
[AttributeUsage(AttributeTargets.Method)] public class SemaphoreLockAttribute : Attribute, IAsyncActionFilter { private static readonly SemaphoreSlim Semaphore = new (1, 1); public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { // Acquire the semaphore await Semaphore.WaitAsync(); try { // Proceed with the action await next(); } finally { // Release the semaphore Semaphore.Release(); } } }
حالا میتونیم این Attribute را برای هر اکشنی استفاده کنیم:
[HttpPost] [SemaphoreLock] public async Task<IActionResult> Add(UserViewModel model) { var user = new User { UserName = model.UserName, Email = model.Email }; var result = await userManager.CreateAsync(user, model.Password); if (result.Succeeded) { return Ok(); } return BadRequest(result.Errors); }
نحوهی اعمال تنظیمات کامپایلر TypeScript
روشهای متفاوتی جهت اعمال تنظیمات کامپایلر TypeScript وجود دارند:
الف) ذکر پارامترها و سوئیچهای کامپایلر خط فرمان tsc به صورت مستقیم.
ب) بعضی از ادیتورها و IDEها این پارامترها را به صورت گزینهها و دیالوگهایی ارائه میدهند.
ج) استفاده از یک Build task، همانند روشی که در تنظیمات VSCode در مطلب «چرا TypeScript» مشاهده کردید.
د) ذکر تنظیمات کامپایلر، در فایل مخصوصی به نام tsconfig.json.
گزینههای متداول کامپایلر TypeScript
گزینههای کامپایلر TypeScript نسبتا قابل توجه هستند و لیست کامل و به روز آنها را در هندبوک تایپاسکریپت میتوانید مشاهده کنید. در اینجا تعدادی از مهمترینها را بررسی خواهیم کرد:
- سوئیچ module-- جهت مشخص سازی فرمت خروجی ماژولهای TypeScript بکار میرود. در مطلب بررسی ماژولها عنوان شد که TypeScript قادر است ماژولهای تعریف شده را با سایر فرمتهای متداول جاوا اسکریپت مانند common.js و amd نیز تولید کند. سوئیچ module-- جهت تنظیم این گزینه درنظر گرفته شدهاست. خلاصهای این سوئیچ نیز m-- است. این سوئیچ یکی از مقادیر commonjs, amd, system, es2015 را میپذیرد. اگر es2015 را مشخص کردید، نیاز است target را نیز به ES6 تنظیم کنید.
- سوئیچ moduleResolution-- نحوهی یافتن ارجاعات به ماژولها را مشخص میکند. در اینجا روشهای node.js و کلاسیک را میتوان قید کرد.
- سوئیچ target-- برای تعیین نگارش خروجی جاوا اسکریپت تولیدی بکار میرود. حالت پیش فرض آن ES3 است و ES5 و ES6 را نیز پشتیبانی میکند.
- گزینهی watch-- کامپایلر را در حالت watch نگه میدارد. در این حالت تغییرات آخرین تاریخ نوشته شدن در فایلهای ts بررسی شده و در صورت یافتن تغییری، بلافاصله خروجی js آنها تهیه میشود.
- سوئیچ outDir-- برای مشخص کردن پوشهی فایلهای تولیدی نهایی بکار میرود.
- گزینهی noImplicitAny-- برای ممنوع کردن نوعهای Any متغیرها به صورت پیش فرض است و در این حالت خطای کامپایلری را مشاهده خواهید کرد. استفاده از این گزینه به این معنا نیست که دیگر نمیتوان از نوع Any استفاده کرد؛ بلکه به این معنا است که در صورت نیاز باید آنرا به صورت صریح قید کنید.
یک مثال:
در VSCode و در پوشهی vscode. آن، در تنظیمات فایل tasks.json، چنین گزینههایی را میتوان برای کامپایلر tsc، ذکر کرد:
"args": ["--target", "ES5", "--outDir", "js", "--module", "commonjs", "--sourceMap", "--watch", "app.ts"],
بررسی کاربرد فایل tsconfig.json
فایل ویژهی tsconfig.json در نگارش 1.5 تایپاسکریپت معرفی گردید. هدف از این فایل، ساده کردن تعریف پارامترهای کامپایلر است؛ البته الزامی به استفادهی از آن وجود ندارد.
این فایل مزایای ذیل را به همراه دارد:
الف) محل قرارگیری آن، ریشهی پروژهی TypeScript را مشخص میکند.
ب) تنظیمات ذکر شدهی در این فایل، به تمام فایلهای موجود در پوشه و زیر پوشههای محل قرارگیری آن به صورت پیش فرض اعمال میشوند.
هنگامیکه در تنظیمات کامپایلر tsc، نام فایل یا فایلهای ts ایی را ذکر نمیکنید، این کامپایلر در ابتدا به دنبال فایل tsconfig.json میگردد و بر این اساس فایلهای ts را پردازش خواهد کرد. اگر مانند مثال فوق، در انتهای پارامترها، نام فایلی را ذکر کنید، از فایل tsconfig.json صرفنظر خواهد شد.
یک نکته: برای ذکر صریح محل فایل tsconfig از پارامتر project استفاده کنید:
tsc --project ./lib
در این حالت میتوان کامپایلر tsc را بدون پارامتری اجرا کرد و این برنامه اطلاعات مورد نیاز خود را از فایل tsconfig.json دریافت خواهد کرد. باید دقت داشت، هر سوئیچی که در خط فرمان ذکر شود، پارامترهای معادل ذکر شدهی در فایل tsconfig.json را بازنویسی میکند. بنابراین در صورت وجود این فایل، میتوان خاصیت args مثال قبل را به یک آرایهی خالی تنظیم کرد.
د) امکان مشخص سازی الحاق و عدم الحاق فایلهای ts را به همراه دارد.
{ "compilerOptions": { "target": "es5", "outDir": "js", "module":"commonjs", "sourceMap":true }, "files": [ "app.ts", "classes.ts" ] }
کار خاصیت files الحاق و include است. اگر میخواهید از پوشهها و یا فایلهایی صرفنظر شود، از خاصیت exclude استفاده کنید:
{ "compilerOptions": { "target": "es5", "outDir": "js" }, "exclude": [ "node_modules", "lib" ] }
یک نکته
در VSCode داخل فایل tsconfig.json با فشردن ctrl+space، به یک intellisense حاوی گزینههای تکمیل کنندهی آن خواهید رسید.
ساده سازی الحاق فایلهای تعاریف نوعها
در مطلب «مبانی TypeScript؛ تهیه فایلهای تعاریف نوعها» با فایلهای ویژهی d.ts. آشنا شدیم. استفادهی از این فایلها به همراه ذکر اجباری reference path مرتبط در ابتدای هر فایل ts است. اینکار اضافی را با استفاده از فایل tsconfig.json میتوان حذف کرد:
{ "compilerOptions": { "target": "es5", "outDir": "js", "module": "commonjs", "sourceMap": true, "watch": true }, "files": [ "app.ts", "typings/main.d.ts" ] }
در قسمت قبل که لیست اتاقهای دریافتی از Web API را نمایش دادیم، هرکدام از آنها، به همراه یک دکمهی Book هم هستند (تصویر فوق) که هدف از آن، فراهم آوردن امکان رزرو کردن آن اتاق، توسط کاربران سایت است. این قسمت را میتوان به عنوان تمرینی جهت یادآوری مراحل مختلف تهیهی یک Web API و قسمتهای سمت کلاینت آن، تکمیل کرد.
تهیه موجودیت و مدل متناظر با صفحهی ثبت رزرو یک اتاق
تا اینجا در برنامهی سمت کلاینت، زمانیکه بر روی دکمهی Go صفحهی اول کلیک میکنیم، تاریخ شروع رزرو و تعداد روز مدنظر، به صفحهی مشاهدهی لیست اتاقها ارسال میشود. اکنون میخواهیم در این لیست اتاقهای نمایش داده شده، اگر بر روی لینک Book اتاقی کلیک شد، به صفحهی اختصاصی رزرو آن اتاق هدایت شویم (مانند تصویر فوق). به همین جهت نیاز است موجودیت متناظر با اطلاعاتی را که قرار است از کاربر دریافت کنیم، به صورت زیر به پروژهی BlazorServer.Entities اضافه کنیم:
using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace BlazorServer.Entities { public class RoomOrderDetail { public int Id { get; set; } [Required] public string UserId { get; set; } [Required] public string StripeSessionId { get; set; } public DateTime CheckInDate { get; set; } public DateTime CheckOutDate { get; set; } public DateTime ActualCheckInDate { get; set; } public DateTime ActualCheckOutDate { get; set; } public long TotalCost { get; set; } public int RoomId { get; set; } public bool IsPaymentSuccessful { get; set; } [Required] public string Name { get; set; } [Required] public string Email { get; set; } public string Phone { get; set; } [ForeignKey("RoomId")] public HotelRoom HotelRoom { get; set; } public string Status { get; set; } } }
namespace BlazorServer.Common { public static class BookingStatus { public const string Pending = "Pending"; public const string Booked = "Booked"; public const string CheckedIn = "CheckedIn"; public const string CheckedOutCompleted = "CheckedOut"; public const string NoShow = "NoShow"; public const string Cancelled = "Cancelled"; } }
namespace BlazorServer.DataAccess { public class ApplicationDbContext : IdentityDbContext<ApplicationUser> { public DbSet<RoomOrderDetail> RoomOrderDetails { get; set; } // ... } }
dotnet tool update --global dotnet-ef --version 5.0.4 dotnet build dotnet ef migrations --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ add AddRoomOrderDetails --context ApplicationDbContext dotnet ef --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ database update --context ApplicationDbContext
پس از تعریف یک موجودیت، یک DTO متناظر با آنرا که جهت مدلسازی UI از آن استفاده خواهیم کرد، در پروژهی BlazorServer.Models ایجاد میکنیم:
using System; using System.ComponentModel.DataAnnotations; namespace BlazorServer.Models { public class RoomOrderDetailsDTO { public int Id { get; set; } [Required] public string UserId { get; set; } [Required] public string StripeSessionId { get; set; } [Required] public DateTime CheckInDate { get; set; } [Required] public DateTime CheckOutDate { get; set; } public DateTime ActualCheckInDate { get; set; } public DateTime ActualCheckOutDate { get; set; } [Required] public long TotalCost { get; set; } [Required] public int RoomId { get; set; } public bool IsPaymentSuccessful { get; set; } [Required] public string Name { get; set; } [Required] public string Email { get; set; } public string Phone { get; set; } public HotelRoomDTO HotelRoomDTO { get; set; } public string Status { get; set; } } }
namespace BlazorServer.Models.Mappings { public class MappingProfile : Profile { public MappingProfile() { // ... CreateMap<RoomOrderDetail, RoomOrderDetailsDTO>().ReverseMap(); // two-way mapping } } }
ایجاد سرویسی برای کار با جدول RoomOrderDetails
در برنامهی سمت کلاینت برای کار با بانک اطلاعاتی، دیگر نمیتوان از سرویسهای سمت سرور به صورت مستقیم استفاده کرد. به همین جهت آنها را از طریق یک Web API endpoint، در معرض دید استفاده کننده قرار میدهیم. اما پیش از اینکار، سرویس سمت سرور Web API باید بتواند با سرویس دسترسی به اطلاعات جدول RoomOrderDetails، کار کند. بنابراین در ادامه این سرویس را تهیه میکنیم:
namespace BlazorServer.Services { public interface IRoomOrderDetailsService { Task<RoomOrderDetailsDTO> CreateAsync(RoomOrderDetailsDTO details); Task<List<RoomOrderDetailsDTO>> GetAllRoomOrderDetailsAsync(); Task<RoomOrderDetailsDTO> GetRoomOrderDetailAsync(int roomOrderId); Task<bool> IsRoomBookedAsync(int RoomId, DateTime checkInDate, DateTime checkOutDate); Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(int id); Task<bool> UpdateOrderStatusAsync(int RoomOrderId, string status); } }
namespace BlazorServer.Services { public class RoomOrderDetailsService : IRoomOrderDetailsService { private readonly ApplicationDbContext _dbContext; private readonly IMapper _mapper; private readonly IConfigurationProvider _mapperConfiguration; public RoomOrderDetailsService(ApplicationDbContext dbContext, IMapper mapper) { _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); _mapperConfiguration = mapper.ConfigurationProvider; } public async Task<RoomOrderDetailsDTO> CreateAsync(RoomOrderDetailsDTO details) { var roomOrder = _mapper.Map<RoomOrderDetail>(details); roomOrder.Status = BookingStatus.Pending; var result = await _dbContext.RoomOrderDetails.AddAsync(roomOrder); await _dbContext.SaveChangesAsync(); return _mapper.Map<RoomOrderDetailsDTO>(result.Entity); } public Task<List<RoomOrderDetailsDTO>> GetAllRoomOrderDetailsAsync() { return _dbContext.RoomOrderDetails .Include(roomOrderDetail => roomOrderDetail.HotelRoom) .ProjectTo<RoomOrderDetailsDTO>(_mapperConfiguration) .ToListAsync(); } public async Task<RoomOrderDetailsDTO> GetRoomOrderDetailAsync(int roomOrderId) { var roomOrderDetailsDTO = await _dbContext.RoomOrderDetails .Include(u => u.HotelRoom) .ThenInclude(x => x.HotelRoomImages) .ProjectTo<RoomOrderDetailsDTO>(_mapperConfiguration) .FirstOrDefaultAsync(u => u.Id == roomOrderId); roomOrderDetailsDTO.HotelRoomDTO.TotalDays = roomOrderDetailsDTO.CheckOutDate.Subtract(roomOrderDetailsDTO.CheckInDate).Days; return roomOrderDetailsDTO; } public Task<bool> IsRoomBookedAsync(int RoomId, DateTime checkInDate, DateTime checkOutDate) { return _dbContext.RoomOrderDetails .AnyAsync( roomOrderDetail => roomOrderDetail.RoomId == RoomId && roomOrderDetail.IsPaymentSuccessful && ( (checkInDate < roomOrderDetail.CheckOutDate && checkInDate > roomOrderDetail.CheckInDate) || (checkOutDate > roomOrderDetail.CheckInDate && checkInDate < roomOrderDetail.CheckInDate) ) ); } public Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(int id) { throw new NotImplementedException(); } public Task<bool> UpdateOrderStatusAsync(int RoomOrderId, string status) { throw new NotImplementedException(); } } }
- از متد CreateAsync برای تبدیل مدل فرم ثبت اطلاعات، به یک رکورد جدول RoomOrderDetails، استفاده میکنیم.
- متد GetAllRoomOrderDetailsAsync، لیست تمام سفارشهای ثبت شده را بازگشت میدهد.
- متد GetRoomOrderDetailAsync بر اساس شماره اتاقی که دریافت میکند، لیست سفارشات آن اتاق خاص را بازگشت میدهد. این لیست به علت استفاده از Includeهای تعریف شده، به همراه مشخصات اتاق و همچنین تصاویر مرتبط با آن اتاق نیز هست.
- متد IsRoomBookedAsync بر اساس شماره اتاق و بازهی زمانی درخواستی توسط یک کاربر مشخص میکند که آیا اتاق خالی شدهاست یا خیر؟
پس از تعریف این سرویس، به کلاس آغازین پروژهی Web API مراجعه کرده و آنرا به سیستم تزریق وابستگیها، معرفی میکنیم:
namespace BlazorWasm.WebApi { public class Startup { // ... public void ConfigureServices(IServiceCollection services) { services.AddScoped<IRoomOrderDetailsService, RoomOrderDetailsService>(); // ...
تشکیل سرویس ابتدایی کار با RoomOrderDetails در پروژهی WASM
در ادامه، تعاریف خالی سرویس سمت کلاینت کار با RoomOrderDetails را به پروژهی WASM اضافه میکنیم. تکمیل این سرویس را به قسمت بعدی واگذار خواهیم کرد:
namespace BlazorWasm.Client.Services { public interface IClientRoomOrderDetailsService { Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(RoomOrderDetailsDTO details); Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details); } }
namespace BlazorWasm.Client.Services { public class ClientRoomOrderDetailsService : IClientRoomOrderDetailsService { private readonly HttpClient _httpClient; public ClientRoomOrderDetailsService(HttpClient httpClient) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); } public Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(RoomOrderDetailsDTO details) { throw new NotImplementedException(); } public Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details) { throw new NotImplementedException(); } } }
namespace BlazorWasm.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); // ... builder.Services.AddScoped<IClientRoomOrderDetailsService, ClientRoomOrderDetailsService>(); // ... } } }
تعریف مدل فرم ثبت اطلاعات سفارش
پس از تدارک مقدمات فوق، اکنون میتوانیم کار تکمیل فرم ثبت اطلاعات سفارش را شروع کنیم. به همین جهت مدل مخصوص آنرا در برنامهی سمت کلاینت به صورت زیر تشکیل میدهیم:
using BlazorServer.Models; namespace BlazorWasm.Client.Models.ViewModels { public class HotelRoomBookingVM { public RoomOrderDetailsDTO OrderDetails { get; set; } } }
تعریف کامپوننت جدید RoomDetails و مقدار دهی اولیهی مدل آن
در ادامه فایل جدید BlazorWasm.Client\Pages\HotelRooms\RoomDetails.razor را ایجاد کرده و به صورت زیر مقدار دهی اولیه میکنیم:
@page "/hotel/room-details/{Id:int}" @inject IJSRuntime JsRuntime @inject ILocalStorageService LocalStorage @inject IClientHotelRoomService HotelRoomService @if (HotelBooking?.OrderDetails?.HotelRoomDTO?.HotelRoomImages == null) { <div class="spinner"></div> } else { } @code { [Parameter] public int? Id { get; set; } HotelRoomBookingVM HotelBooking = new HotelRoomBookingVM(); int NoOfNights = 1; protected override async Task OnInitializedAsync() { try { HotelBooking.OrderDetails = new RoomOrderDetailsDTO(); if (Id != null) { if (await LocalStorage.GetItemAsync<HomeVM>(ConstantKeys.LocalInitialBooking) != null) { var roomInitialInfo = await LocalStorage.GetItemAsync<HomeVM>(ConstantKeys.LocalInitialBooking); HotelBooking.OrderDetails.HotelRoomDTO = await HotelRoomService.GetHotelRoomDetailsAsync( Id.Value, roomInitialInfo.StartDate, roomInitialInfo.EndDate); NoOfNights = roomInitialInfo.NoOfNights; HotelBooking.OrderDetails.CheckInDate = roomInitialInfo.StartDate; HotelBooking.OrderDetails.CheckOutDate = roomInitialInfo.EndDate; HotelBooking.OrderDetails.HotelRoomDTO.TotalDays = roomInitialInfo.NoOfNights; HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount = roomInitialInfo.NoOfNights * HotelBooking.OrderDetails.HotelRoomDTO.RegularRate; } else { HotelBooking.OrderDetails.HotelRoomDTO = await HotelRoomService.GetHotelRoomDetailsAsync( Id.Value, DateTime.Now, DateTime.Now.AddDays(1)); NoOfNights = 1; HotelBooking.OrderDetails.CheckInDate = DateTime.Now; HotelBooking.OrderDetails.CheckOutDate = DateTime.Now.AddDays(1); HotelBooking.OrderDetails.HotelRoomDTO.TotalDays = 1; HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount = HotelBooking.OrderDetails.HotelRoomDTO.RegularRate; } } } catch (Exception e) { await JsRuntime.ToastrError(e.Message); } } }
- سپس سرویس توکار IJSRuntime به کامپوننت تزریق شدهاست تا توسط آن و Toastr، بتوان خطاهایی را به کاربر نمایش داد.
- از سرویس ILocalStorageService برای دسترسی به اطلاعات شروع به رزرو شخص و تعداد روز مدنظر او استفاده میکنیم که در قسمت قبل آنرا مقدار دهی کردیم.
- همچنین از سرویس IClientHotelRoomService که آنرا نیز در قسمت قبل افزودیم، برای فراخوانی متد GetHotelRoomDetailsAsync آن استفاده کردهایم.
در روال آغازین OnInitializedAsync، اگر Id تنظیم شده بود، یعنی کاربر به درستی وارد این صفحه شدهاست. سپس بررسی میکنیم که آیا اطلاعاتی از درخواست ابتدایی او در Local Storage مرورگر وجود دارد یا خیر؟ اگر این اطلاعات وجود داشته باشد، بر اساس آن، بازهی تاریخی دقیقی را میتوان تشکیل داد و اگر خیر، این بازه را از امروز، به مدت 1 روز درنظر میگیریم.
پس از پایان کار متد OnInitializedAsync، چون اجزای HotelBooking مقدار دهی کامل شدهاند، نمایش loading ابتدای کامپوننت، متوقف شده و قسمت else شرط نوشته شده اجرا میشود؛ یعنی اصل UI فرم نمایان خواهد شد.
در قسمت قبل، متد GetHotelRoomDetailsAsync را تکمیل نکردیم؛ چون به آن نیازی نداشتیم و فقط قصد داشتیم تا لیست تمام اتاقها را نمایش دهیم. اما در اینجا برای تکمیل کدهای آغازین کامپوننت RoomDetails، متد دریافت اطلاعات یک اتاق را نیز تکمیل میکنیم تا توسط آن بتوان در این کامپوننت نیز جزئیات اتاق انتخابی را نمایش داد:
namespace BlazorWasm.Client.Services { public class ClientHotelRoomService : IClientHotelRoomService { private readonly HttpClient _httpClient; public ClientHotelRoomService(HttpClient httpClient) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); } public Task<HotelRoomDTO> GetHotelRoomDetailsAsync(int roomId, DateTime checkInDate, DateTime checkOutDate) { // How to url-encode query-string parameters properly var uri = new UriBuilderExt(new Uri(_httpClient.BaseAddress, $"/api/hotelroom/{roomId}")) .AddParameter("checkInDate", $"{checkInDate:yyyy'-'MM'-'dd}") .AddParameter("checkOutDate", $"{checkOutDate:yyyy'-'MM'-'dd}") .Uri; return _httpClient.GetFromJsonAsync<HotelRoomDTO>(uri); } public Task<IEnumerable<HotelRoomDTO>> GetHotelRoomsAsync(DateTime checkInDate, DateTime checkOutDate) { // ... } } }
اتصال مدل کامپوننت RoomDetails به فرم ثبت سفارش آن
تا اینجا مدل فرم را مقدار دهی اولیه کردیم. اکنون میتوانیم قسمت else شرط نوشته شده را تکمیل کرده و در قسمتی از آن، مشخصات اتاق جاری را نمایش دهیم و در قسمتی دیگر، فرم ثبت سفارش را تکمیل کنیم.
الف) نمایش مشخصات اتاق جاری
در کامپوننت جاری با استفاده از خواص مقدار دهی اولیه شدهی شیء HotelBooking.OrderDetails.HotelRoomDTO، میتوان جزئیات اتاق انتخابی را نمایش داد که نمونهای از آنرا در قسمت قبل هم مشاهده کردید:
@if (HotelBooking?.OrderDetails?.HotelRoomDTO?.HotelRoomImages == null) { <div class="spinner"></div> } else { <div class="mt-4 mx-4 px-0 px-md-5 mx-md-5"> <div class="row p-2 my-3 " style="border-radius:20px; "> <div class="col-12 col-lg-7 p-4" style="border: 1px solid gray"> <div class="row px-2 text-success border-bottom"> <div class="col-8 py-1"><p style="font-size:x-large;margin:0px;">Selected Room</p></div> <div class="col-4 p-0"><a href="hotel/rooms" class="btn btn-secondary btn-block">Back to Room's</a></div> </div> <div class="row"> <div class="col-6"> <div id="" class="carousel slide mb-4 m-md-3 m-0 pt-3 pt-md-0" data-ride="carousel"> <div id="carouselExampleIndicators" class="carousel slide" data-ride="carousel"> <ol class="carousel-indicators"> <li data-target="#carouselExampleIndicators" data-slide-to="0" class="active"></li> <li data-target="#carouselExampleIndicators" data-slide-to="1"></li> </ol> <div class="carousel-inner"> <div class="carousel-item active"> <img class="d-block w-100" src="images/slide1.jpg" alt="First slide"> </div> </div> <a class="carousel-control-prev" href="#carouselExampleIndicators" role="button" data-slide="prev"> <span class="carousel-control-prev-icon" aria-hidden="true"></span> <span class="sr-only">Previous</span> </a> <a class="carousel-control-next" href="#carouselExampleIndicators" role="button" data-slide="next"> <span class="carousel-control-next-icon" aria-hidden="true"></span> <span class="sr-only">Next</span> </a> </div> </div> </div> <div class="col-6"> <span class="float-right pt-4"> <span class="float-right">Occupancy : @HotelBooking.OrderDetails.HotelRoomDTO.Occupancy adults </span><br /> <span class="float-right pt-1">Size : @HotelBooking.OrderDetails.HotelRoomDTO.SqFt sqft</span><br /> <h4 class="text-warning font-weight-bold pt-5"> <span style="border-bottom:1px solid #ff6a00"> @HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount.ToString("#,#.00#;(#,#.00#)") </span> </h4> <span class="float-right">Cost for @HotelBooking.OrderDetails.HotelRoomDTO.TotalDays nights</span> </span> </div> </div> <div class="row p-2"> <div class="col-12"> <p class="card-title text-warning" style="font-size:xx-large">@HotelBooking.OrderDetails.HotelRoomDTO.Name</p> <p class="card-text" style="font-size:large"> @((MarkupString)@HotelBooking.OrderDetails.HotelRoomDTO.Details) </p> </div> </div> </div> }
قسمت دوم UI کامپوننت جاری، نمایش فرم زیر است که اجزای مختلف آن به فیلد HotelBooking متصل شدهاند:
@if (HotelBooking?.OrderDetails?.HotelRoomDTO?.HotelRoomImages == null) { <div class="spinner"></div> } else { // ... <div class="col-12 col-lg-5 p-4 2 mt-4 mt-md-0" style="border: 1px solid gray;"> <EditForm Model="HotelBooking" class="container" OnValidSubmit="HandleCheckout"> <div class="row px-2 text-success border-bottom"><div class="col-7 py-1"><p style="font-size:x-large;margin:0px;">Enter Details</p></div></div> <div class="form-group pt-2"> <label class="text-warning">Name</label> <InputText @bind-Value="HotelBooking.OrderDetails.Name" type="text" class="form-control" /> </div> <div class="form-group pt-2"> <label class="text-warning">Phone</label> <InputText @bind-Value="HotelBooking.OrderDetails.Phone" type="text" class="form-control" /> </div> <div class="form-group"> <label class="text-warning">Email</label> <InputText @bind-Value="HotelBooking.OrderDetails.Email" type="text" class="form-control" /> </div> <div class="form-group"> <label class="text-warning">Check in Date</label> <InputDate @bind-Value="HotelBooking.OrderDetails.CheckInDate" type="date" disabled class="form-control" /> </div> <div class="form-group"> <label class="text-warning">Check Out Date</label> <InputDate @bind-Value="HotelBooking.OrderDetails.CheckOutDate" type="date" disabled class="form-control" /> </div> <div class="form-group"> <label class="text-warning">No. of nights</label> <select class="form-control" value="@NoOfNights" @onchange="HandleNoOfNightsChange"> @for (var i = 1; i <= 10; i++) { if (i == NoOfNights) { <option value="@i" selected="selected">@i</option> } else { <option value="@i">@i</option> } } </select> </div> <div class="form-group"> <button type="submit" class="btn btn-success form-control">Checkout Now</button> </div> </EditForm> </div> </div> </div> }
@code { // ... private async Task HandleNoOfNightsChange(ChangeEventArgs e) { NoOfNights = Convert.ToInt32(e.Value.ToString()); HotelBooking.OrderDetails.HotelRoomDTO = await HotelRoomService.GetHotelRoomDetailsAsync( Id.Value, HotelBooking.OrderDetails.CheckInDate, HotelBooking.OrderDetails.CheckInDate.AddDays(NoOfNights)); HotelBooking.OrderDetails.CheckOutDate = HotelBooking.OrderDetails.CheckInDate.AddDays(NoOfNights); HotelBooking.OrderDetails.HotelRoomDTO.TotalDays = NoOfNights; HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount = NoOfNights * HotelBooking.OrderDetails.HotelRoomDTO.RegularRate; } private async Task HandleCheckout() { if (!await HandleValidation()) { return; } } private async Task<bool> HandleValidation() { if (string.IsNullOrEmpty(HotelBooking.OrderDetails.Name)) { await JsRuntime.ToastrError("Name cannot be empty"); return false; } if (string.IsNullOrEmpty(HotelBooking.OrderDetails.Phone)) { await JsRuntime.ToastrError("Phone cannot be empty"); return false; } if (string.IsNullOrEmpty(HotelBooking.OrderDetails.Email)) { await JsRuntime.ToastrError("Email cannot be empty"); return false; } return true; } }
- همچنین کدهای ابتدایی HandleCheckout را که برای ثبت نهایی اطلاعات فرم است، تهیه کردهایم. البته در این قسمت این مورد را فقط محدود به اعتبارسنجی دستی و سفارشی که در متد HandleValidation مشاهده میکنید، کردهایم. این روش دستی را نیز میتوان برای تعریف منطق اعتبارسنجی یک فرم بکار برد و آنرا توسط کدهای #C تکمیل کرد. البته باید درنظر داشت که data annotation validator توکار، هنوز از اعتبارسنجی خواص تو در تو، پشتیبانی نمیکند. به همین جهت است که در اینجا خودمان این اعتبارسنجی را به صورت دستی تعریف کردهایم.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-29.zip
در این مطلب نحوهی یکپارچه سازی Windows Authentication دومینهای ویندوزی را با IdentityServer بررسی میکنیم.
کار با تامین کنندههای هویت خارجی
اغلب کاربران، دارای اکانت ثبت شدهای در جای دیگری نیز هستند و شاید آنچنان نسبت به ایجاد اکانت جدیدی در IDP ما رضایت نداشته باشند. برای چنین حالتی، امکان یکپارچه سازی IdentityServer با انواع و اقسام IDPهای دیگر نیز پیش بینی شدهاست. در اینجا تمام اینها، روشهای مختلفی برای ورود به سیستم، توسط یک کاربر هستند. کاربر ممکن است توسط اکانت خود در شبکهی ویندوزی به سیستم وارد شود و یا توسط اکانت خود در گوگل، اما در نهایت از دیدگاه سیستم ما، یک کاربر مشخص بیشتر نیست.
نگاهی به شیوهی پشتیبانی از تامین کنندههای هویت خارجی توسط Quick Start UI
Quick Start UI ای را که در «قسمت چهارم - نصب و راه اندازی IdentityServer» به IDP اضافه کردیم، دارای کدهای کار با تامین کنندههای هویت خارجی نیز میباشد. برای بررسی آن، کنترلر DNT.IDP\Controllers\Account\ExternalController.cs را باز کنید:
[HttpGet] public async Task<IActionResult> Challenge(string provider, string returnUrl) [HttpGet] public async Task<IActionResult> Callback()
در اکشن متد Callback، اطلاعات کاربر از کوکی رمزنگاری شدهی متد Challenge استخراج میشود و بر اساس آن هویت کاربر در سطح IDP شکل میگیرد.
فعالسازی Windows Authentication برای ورود به IDP
در ادامه میخواهیم برنامه را جهت استفادهی از اکانت ویندوزی کاربران جهت ورود به IDP تنظیم کنیم. برای این منظور باید نکات مطلب «فعالسازی Windows Authentication در برنامههای ASP.NET Core 2.0» را پیشتر مطالعه کرده باشید.
پس از فعالسازی Windows Authentication در برنامه، اگر برنامهی IDP را توسط IIS و یا IIS Express و یا HttpSys اجرا کنید، دکمهی جدید Windows را در قسمت External Login مشاهده خواهید کرد:
یک نکته: برچسب این دکمه را در حالت استفادهی از مشتقات IIS، به صورت زیر میتوان تغییر داد:
namespace DNT.IDP { public class Startup { public void ConfigureServices(IServiceCollection services) { services.Configure<IISOptions>(iis => { iis.AuthenticationDisplayName = "Windows Account"; iis.AutomaticAuthentication = false; });
اتصال کاربر وارد شدهی از یک تامین کنندهی هویت خارجی به کاربران بانک اطلاعاتی برنامه
سازندهی کنترلر DNT.IDP\Controllers\Account\ExternalController.cs نیز همانند کنترلر Account که آنرا در قسمت قبل تغییر دادیم، از TestUserStore استفاده میکند:
public ExternalController( IIdentityServerInteractionService interaction, IClientStore clientStore, IEventService events, TestUserStore users = null) { _users = users ?? new TestUserStore(TestUsers.Users); _interaction = interaction; _clientStore = clientStore; _events = events; }
private readonly IUsersService _usersService; public ExternalController( // ... IUsersService usersService) { // ... _usersService = usersService; }
الف) در متد FindUserFromExternalProvider
سطر قدیمی
var user = _users.FindByExternalProvider(provider, providerUserId);
var user = await _usersService.GetUserByProviderAsync(provider, providerUserId);
private async Task<(User user, string provider, string providerUserId, IEnumerable<Claim> claims)> FindUserFromExternalProvider(AuthenticateResult result)
private TestUser AutoProvisionUser(string provider, string providerUserId, IEnumerable<Claim> claims) { var user = _users.AutoProvisionUser(provider, providerUserId, claims.ToList()); return user; }
مفهوم «Provisioning a user» در اینجا به معنای درخواست از کاربر، جهت ورود اطلاعاتی مانند نام و نام خانوادگی او است که پیشتر صفحهی ثبت کاربر جدید را برای این منظور در قسمت قبل ایجاد کردهایم و از آن میشود در اینجا استفادهی مجدد کرد. بنابراین در ادامه، گردش کاری ورود کاربر از طریق تامین کنندهی هویت خارجی را به نحوی اصلاح میکنیم که کاربر جدید، ابتدا به صفحهی ثبت نام وارد شود و اطلاعات تکمیلی خود را وارد کند؛ سپس به صورت خودکار به متد Callback بازگشته و ادامهی مراحل را طی نماید:
در اکشن متد نمایش صفحهی ثبت نام کاربر جدید، متد RegisterUser تنها آدرس بازگشت به صفحهی قبلی را دریافت میکند:
[HttpGet] public IActionResult RegisterUser(string returnUrl)
namespace DNT.IDP.Controllers.UserRegistration { public class RegistrationInputModel { public string ReturnUrl { get; set; } public string Provider { get; set; } public string ProviderUserId { get; set; } public bool IsProvisioningFromExternal => !string.IsNullOrWhiteSpace(Provider); } }
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); }
returnUrl ارسالی به اکشن متد RegisterUser، به همین اکشن متد جاری اشاره میکند. یعنی کاربر پس از تکمیل اطلاعات و اینبار نال نبودن user او، گردش کاری جاری را ادامه خواهد داد.
در ادامه نیاز است امضای متد نمایش صفحهی ثبت نام را نیز بر این اساس اصلاح کنیم:
namespace DNT.IDP.Controllers.UserRegistration { public class UserRegistrationController : Controller { [HttpGet] public IActionResult RegisterUser(RegistrationInputModel registrationInputModel) { var vm = new RegisterUserViewModel { ReturnUrl = registrationInputModel.ReturnUrl, Provider = registrationInputModel.Provider, ProviderUserId = registrationInputModel.ProviderUserId }; return View(vm); }
namespace DNT.IDP.Controllers.UserRegistration { public class RegisterUserViewModel : RegistrationInputModel {
اکنون نیاز است RegisterUser.cshtml را اصلاح کنیم:
- ابتدا دو فیلد مخفی دیگر Provider و ProviderUserId را نیز به این فرم اضافه میکنیم؛ از این جهت که در حین postback به سمت سرور به مقادیر آنها نیاز داریم:
<inputtype="hidden"asp-for="ReturnUrl"/> <inputtype="hidden"asp-for="Provider"/> <inputtype="hidden"asp-for="ProviderUserId"/>
@if (!Model.IsProvisioningFromExternal) { <div> <label asp-for="Password"></label> <input type="password" placeholder="Password" asp-for="Password" autocomplete="off"> </div> }
پس از آن نیاز است اطلاعات اکانت خارجی این کاربر را در حین postback و ارسال اطلاعات به اکشن متد RegisterUser، ثبت کنیم:
namespace DNT.IDP.Controllers.UserRegistration { public class UserRegistrationController : Controller { [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> RegisterUser(RegisterUserViewModel model) { // ... if (model.IsProvisioningFromExternal) { userToCreate.UserLogins.Add(new UserLogin { LoginProvider = model.Provider, ProviderKey = model.ProviderUserId }); } // add it through the repository await _usersService.AddUserAsync(userToCreate); // ... } }
همچنین در ادامهی این اکشن متد، کار لاگین خودکار کاربر نیز انجام میشود. با توجه به اینکه پس از ثبت اطلاعات کاربر نیاز است مجددا گردش کاری اکشن متد Callback طی شود، این لاگین خودکار را نیز برای حالت ورود از طریق تامین کنندهی خارجی، غیرفعال میکنیم:
if (!model.IsProvisioningFromExternal) { // log the user in // issue authentication cookie with subject ID and username var props = new AuthenticationProperties { IsPersistent = false, ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration) }; await HttpContext.SignInAsync(userToCreate.SubjectId, userToCreate.Username, props); }
بررسی ورود به سیستم توسط دکمهی External Login -> Windows
پس از این تغییرات، اکنون در حین ورود به سیستم (تصویر ابتدای بحث در قسمت فعالسازی اعتبارسنجی ویندوزی)، گزینهی External Login -> Windows را انتخاب میکنیم. بلافاصله به صفحهی ثبتنام کاربر هدایت خواهیم شد:
همانطور که مشاهده میکنید، IDP اکانت ویندوزی جاری را تشخیص داده و فعال کردهاست. همچنین در اینجا خبری از ورود کلمهی عبور هم نیست.
پس از تکمیل این فرم، بلافاصله کار ثبت اطلاعات کاربر و هدایت خودکار به برنامهی MVC Client انجام میشود.
در ادامه از برنامهی کلاینت logout کنید. اکنون در صفحهی login مجددا بر روی دکمهی Windows کلیک نمائید. اینبار بدون پرسیدن سؤالی، لاگین شده و وارد برنامهی کلاینت خواهید شد؛ چون پیشتر کار اتصال اکانت ویندوزی به اکانتی در سمت IDP انجام شدهاست.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی 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 وارد کنید.
یک نکته: برای آزمایش برنامه جهت فعالسازی Windows Authentication بهتر است برنامهی IDP را توسط IIS Express اجرا کنید و یا اگر از IIS Express استفاده نمیکنید، نیاز است UseHttpSys فایل program.cs را مطابق توضیحات «یک نکتهی تکمیلی: UseHttpSys و استفادهی از HTTPS» فعال کنید.
// student-model.ts export interface Student { id: number; name: string; }
// app.component.ts public model: Student; ngOnInit(): void { this.setDefaultValueForModel(); } saveForm(form: NgForm, evetn: Event) { evetn.preventDefault(); if (form.valid) { alert('everything is ok'); } } setDefaultValueForModel() { this.model = { id: 1, name: '' }; }
<form #form="ngForm" novalidate (submit)="saveForm(form,$event)"> <div> <label>Name</label> <input type="text" required name="name" autocomplete="off" [(ngModel)]="model.name" #name="ngModel"> <p [hidden]="name.valid || (name.pristine && !form.submitted)"> Name is required and should be minimum 4 characters. </p> </div> <div> <input type="submit" value="submit"> </div> </form>
هدف از بحث
ارائه راه حلی جهت تزریق یک وهله از واحد کار تشکیل شده (همان شیء سشن در RavenDB) به کلیه کلاسهای لایه سرویس برنامه و همچنین زنده نگه داشتن شیء document store آن در طول عمر برنامه است. ایجاد شیء document store که کار اتصال به بانک اطلاعاتی را مدیریت میکند، بسیار پرهزینه است. به همین جهت این شیء تنها یکبار باید در طول عمر برنامه ایجاد شود.
ابزارها و پیشنیازهای لازم
ابتدا یک برنامه جدید ASP.NET MVC را آغاز کنید. سپس ارجاعات لازم را به کلاینت RavenDB، سرور درون پروسهای آن (RavenDB.Embedded) و همچنین StructureMap با استفاده از نیوگت، اضافه نمائید:
PM> Install-Package RavenDB.Client PM> Install-Package RavenDB.Embedded -Pre PM> Install-Package structuremap
دریافت کدهای کامل این مثال
این مثال، به همراه فایلهای باینری ارجاعات یاد شده، نیست (جهت کاهش حجم 100 مگابایتی آن). برای بازیابی آنها میتوانید به مطلبی در اینباره در سایت مراجعه کنید.
این پروژه از چهار قسمت مطابق شکل زیر تشکیل شده است:
الف) لایه سرویسهای برنامه
using RavenDB25Mvc4Sample.Models; using System.Collections.Generic; namespace RavenDB25Mvc4Sample.Services.Contracts { public interface IUsersService { User AddUser(User user); IList<User> GetUsers(int page, int count = 20); } } using System.Collections.Generic; using System.Linq; using Raven.Client; using RavenDB25Mvc4Sample.Models; using RavenDB25Mvc4Sample.Services.Contracts; namespace RavenDB25Mvc4Sample.Services { public class UsersService : IUsersService { private readonly IDocumentStore _documentStore; private readonly IDocumentSession _documentSession; public UsersService(IDocumentStore documentStore, IDocumentSession documentSession) { _documentStore = documentStore; _documentSession = documentSession; } public User AddUser(User user) { _documentSession.Store(user); return user; } public IList<User> GetUsers(int page, int count = 20) { return _documentSession.Query<User>() .Skip(page * count) .Take(count) .ToList(); } //todo: سایر متدهای مورد نیاز در اینجا } }
هر کلاس لایه سرویس با یک اینترفیس مشخص شده و اعمال آنها از طریق این اینترفیسها در اختیار کنترلرهای برنامه قرار میگیرند.
ب) لایه Infrastructure برنامه
در این لایه کدهای اتصالات IoC Container مورد استفاده قرار میگیرند. کدهایی که به برنامه جاری وابستهاند، اما حالت عمومی و مشترکی ندارند تا در سایر پروژههای مشابه استفاده شوند.
using Raven.Client; using Raven.Client.Embedded; using RavenDB25Mvc4Sample.Services; using RavenDB25Mvc4Sample.Services.Contracts; using StructureMap; namespace RavenDB25Mvc4Sample.Infrastructure { public static class IoCConfig { public static void ApplicationStart() { ObjectFactory.Initialize(x => { // داکیومنت استور سینگلتون تعریف شده چون باید در طول عمر برنامه زنده نگه داشته شود x.ForSingletonOf<IDocumentStore>().Use(() => { return new EmbeddableDocumentStore { DataDirectory = "App_Data" }.Initialize(); }); // سشن در برنامه وب هیبرید تعریف شده تا در طول عمر یک درخواست زنده نگه داشته شود // در برنامههای ویندوزی حالت هیبرید را حذف کنید x.For<IDocumentSession>().HybridHttpOrThreadLocalScoped().Use(context => { return context.GetInstance<IDocumentStore>().OpenSession(); }); // اتصالات لایه سرویس در اینجا x.For<IUsersService>().Use<UsersService>(); // ... }); } public static void ApplicationEndRequest() { ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects(); } } }
IDocumentStore و IDocumentSession، دو اینترفیس تعریف شده در کلاینت RavenDB هستند. اولی کار اتصال به بانک اطلاعاتی را مدیریت خواهد کرد و دومی کار مدیریت الگوی واحد کار را انجام میدهد. IDocumentStore به صورت Singleton تعریف شده است؛ چون باید در طول عمر برنامه زنده نگه داشته شود. اما IDocumentStore در ابتدای هر درخواست رسیده، وهله سازی شده و سپس در پایان هر درخواست در متد ApplicationEndRequest به صورت خودکار Dispose خواهد شد.
اگر به فایل Global.asax.cs پروژه وب برنامه مراجعه کنید، نحوه استفاده از این کلاس را مشاهده خواهید کرد:
using System; using System.Globalization; using System.Web.Mvc; using System.Web.Routing; using RavenDB25Mvc4Sample.Infrastructure; using StructureMap; namespace RavenDB25Mvc4Sample { public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { IoCConfig.ApplicationStart(); AreaRegistration.RegisterAllAreas(); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); //Set current Controller factory as StructureMapControllerFactory ControllerBuilder.Current.SetControllerFactory(new StructureMapControllerFactory()); } protected void Application_EndRequest(object sender, EventArgs e) { IoCConfig.ApplicationEndRequest(); } } public class StructureMapControllerFactory : DefaultControllerFactory { protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType) { if (controllerType == null) throw new InvalidOperationException(string.Format("Page not found: {0}", requestContext.HttpContext.Request.Url.AbsoluteUri.ToString(CultureInfo.InvariantCulture))); return ObjectFactory.GetInstance(controllerType) as Controller; } } }
ج) استفاده از کلاسهای لایه سرویس در کنترلرهای برنامه
using System.Web.Mvc; using Raven.Client; using RavenDB25Mvc4Sample.Models; using RavenDB25Mvc4Sample.Services.Contracts; namespace RavenDB25Mvc4Sample.Controllers { public class HomeController : Controller { private readonly IDocumentSession _documentSession; private readonly IUsersService _usersService; public HomeController(IDocumentSession documentSession, IUsersService usersService) { _documentSession = documentSession; _usersService = usersService; } [HttpGet] public ActionResult Index() { return View(); //نمایش صفحه ثبت } [HttpPost] public ActionResult Index(User user) { if (this.ModelState.IsValid) { _usersService.AddUser(user); _documentSession.SaveChanges(); return RedirectToAction("Index"); } return View(user); } } }
public class BaseEntity : IBaseEntity { [JsonIgnore] int Id { get; set; } [JsonIgnore] string? Audit { get; set; } }
public class AuditSourceValues { [JsonProperty("hn")] public string? HostName { get; set; } [JsonProperty("mn")] public string? MachineName { get; set; } [JsonProperty("rip")] public string? RemoteIpAddress { get; set; } [JsonProperty("lip")] public string? LocalIpAddress { get; set; } [JsonProperty("ua")] public string? UserAgent { get; set; } [JsonProperty("an")] public string? ApplicationName { get; set; } [JsonProperty("av")] public string? ApplicationVersion { get; set; } [JsonProperty("cn")] public string? ClientName { get; set; } [JsonProperty("cv")] public string? ClientVersion { get; set; } [JsonProperty("o")] public string? Other { get; set; } }
public class EntityAudit<TEntity> { [JsonProperty("type")] [JsonConverter(typeof(StringEnumConverter))] public EntityEventType EventType { get; set; } [JsonProperty("user", NullValueHandling = NullValueHandling.Include)] public int? ActorUserId { get; set; } [JsonProperty("at")] public DateTime ActDateTime { get; set; } [JsonProperty("sources")] public AuditSourceValues? AuditSourceValues { get; set; } [JsonProperty("newValues", NullValueHandling = NullValueHandling.Include)] public TEntity NewEntity { get; set; } = default!; public string? SerializeJson() { return JsonSerializer.Serialize(this, options: new JsonSerializerOptions { WriteIndented = false, IgnoreNullValues = true }); } }
دقت کنید که این کلاس به صورت جنریک تعریف شده است تا اگر بعدا بخواهیم آن را Deserialize کنیم و مثلا از آن API بسازیم، یا استفادهی خاصی را از آن داشته باشیم، بهراحتی به Entity مد نظر تبدیل شود. در این مقاله فقط به ذخیرهی آن پرداخته میشود و استفاده از این فیلد که به راحتی و با کمک DbFunctionها در Entity Framework قابل انجام است به خواننده واگذار میشود.
public enum EntityEventType { Create = 0, Update = 1, Delete = 2 }
public interface IAuditSourcesProvider { AuditSourceValues GetAuditSourceValues(); }
public class AuditSourcesProvider : IAuditSourcesProvider { protected readonly IHttpContextAccessor HttpContextAccessor; public AuditSourcesProvider(IHttpContextAccessor httpContextAccessor) { HttpContextAccessor = httpContextAccessor; } public virtual AuditSourceValues GetAuditSourceValues() { var httpContext = HttpContextAccessor.HttpContext; return new AuditSourceValues { HostName = GetHostName(httpContext), MachineName = GetComputerName(httpContext), LocalIpAddress = GetLocalIpAddress(httpContext), RemoteIpAddress = GetRemoteIpAddress(httpContext), UserAgent = GetUserAgent(httpContext), ApplicationName = GetApplicationName(httpContext), ClientName = GetClientName(httpContext), ClientVersion = GetClientVersion(httpContext), ApplicationVersion = GetApplicationVersion(httpContext), Other = GetOther(httpContext) }; } protected virtual string? GetUserAgent(HttpContext httpContext) { return httpContext.Request?.Headers["User-Agent"].ToString(); } protected virtual string? GetRemoteIpAddress(HttpContext httpContext) { return httpContext.Connection?.RemoteIpAddress?.ToString(); } protected virtual string? GetLocalIpAddress(HttpContext httpContext) { return httpContext.Connection?.LocalIpAddress?.ToString(); } protected virtual string GetHostName(HttpContext httpContext) { return httpContext.Request.Host.ToString(); } protected virtual string GetComputerName(HttpContext httpContext) { return Environment.MachineName; } protected virtual string? GetApplicationName(HttpContext httpContext) { return Assembly.GetEntryAssembly()?.GetName().Name; } protected virtual string? GetApplicationVersion(HttpContext httpContext) { return Assembly.GetEntryAssembly()?.GetName().Version.ToString(); } protected virtual string? GetClientVersion(HttpContext httpContext) { return httpContext.Request?.Headers["client-version"]; } protected virtual string? GetClientName(HttpContext httpContext) { return httpContext.Request?.Headers["client-name"]; } protected virtual string? GetOther(HttpContext httpContext) { return null; } }
حالا برای تامین اطلاعات کلاس EntityAudit کار مشابهی میکنیم. ابتدا اینترفیس IEntityAuditProvider را به صورت زیر تعریف میکنیم:
public interface IEntityAuditProvider { string? GetAuditValues(EntityEventType eventType, object? entity, string? previousJsonAudit = null); }
و سپس کلاس EntityAuditProvider را ایجاد میکنیم:
public class EntityAuditProvider : IEntityAuditProvider { private readonly IHttpContextAccessor _httpContextAccessor; private readonly IAuditSourcesProvider _auditSourcesProvider; #region Constructor Injections public EntityAuditProvider(IHttpContextAccessor httpContextAccessor, IAuditSourcesProvider auditSourcesProvider) { _httpContextAccessor = httpContextAccessor; _auditSourcesProvider = auditSourcesProvider; } #endregion public virtual string? GetAuditValues(EntityEventType eventType, object? newEntity, string? previousJsonAudit = null) { var httpContext = _httpContextAccessor.HttpContext; int? userId; var user = httpContext.User; if (!user.Identity.IsAuthenticated) userId = null; else userId = user.Claims.Where(x => x.Type == "UserID").Select(x => x.Value).First().ToInt(); var auditSourceValues = _auditSourcesProvider.GetAuditSourceValues(); var auditJArray = new JArray(); // Update & Delete if (eventType == EntityEventType.Update || eventType == EntityEventType.Delete) { auditJArray = JArray.Parse(previousJsonAudit!); } // Delete => No NewValues if (eventType == EntityEventType.Delete) { newEntity = null; } JObject newAuditJObject = JObject.FromObject(new EntityAudit<object?> { EventType = eventType, ActorUserId = userId, ActDateTime = DateTime.Now, AuditSourceValues = auditSourceValues, NewEntity = newEntity }, new JsonSerializer { NullValueHandling = NullValueHandling.Ignore, Formatting = Formatting.None }); auditJArray.Add(newAuditJObject); return auditJArray.SerializeToJson(true); } }
public class AuditSaveChangesInterceptor : SaveChangesInterceptor { private readonly IEntityAuditProvider _entityAuditProvider; #region Constructor Injections public AuditSaveChangesInterceptor(IEntityAuditProvider entityAuditProvider) { _entityAuditProvider = entityAuditProvider; } #endregion public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result) { ApplyAudits(eventData.Context.ChangeTracker); return base.SavingChanges(eventData, result); } public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = new CancellationToken()) { ApplyAudits(eventData.Context.ChangeTracker); return base.SavingChangesAsync(eventData, result, cancellationToken); } private void ApplyAudits(ChangeTracker changeTracker) { ApplyCreateAudits(changeTracker); ApplyUpdateAudits(changeTracker); ApplyDeleteAudits(changeTracker); } private void ApplyCreateAudits(ChangeTracker changeTracker) { var addedEntries = changeTracker.Entries() .Where(x => x.State == EntityState.Added); foreach (var addedEntry in addedEntries) { if (addedEntry.Entity is IBaseEntity entity) { entity.Audit = _entityAuditProvider.GetAuditValues(EntityEventType.Create, entity); } } } private void ApplyUpdateAudits(ChangeTracker changeTracker) { var modifiedEntries = changeTracker.Entries() .Where(x => x.State == EntityState.Modified); foreach (var modifiedEntry in modifiedEntries) { if (modifiedEntry.Entity is IBaseEntity entity) { var eventType = entity.IsArchived ? EntityEventType.Delete : EntityEventType.Update; // Maybe Soft Delete entity.Audit = _entityAuditProvider.GetAuditValues(eventType, entity, entity.Audit); } } } private void ApplyDeleteAudits(ChangeTracker changeTracker) { var deletedEntries = changeTracker.Entries() .Where(x => x.State == EntityState.Deleted); foreach (var modifiedEntry in deletedEntries) { if (modifiedEntry.Entity is IBaseEntity entity) { entity.Audit = _entityAuditProvider.GetAuditValues(EntityEventType.Delete, entity, entity.Audit); } } } }
و سپس آن را به سیستم معرفی میکنیم:
services.AddDbContext<ATADbContext>((serviceProvider, options) => { options .UseSqlServer(...) // Interceptors var entityAuditProvider = serviceProvider.GetRequiredService<IEntityAuditProvider>(); options.AddInterceptors(new AuditSaveChangesInterceptor(entityAuditProvider)); });
نمونهی کامل فیلد Audit که در JsonFormatter قرار داده شده است، بعد از ایجاد شدن و یکبار آپدیت و سپس حذف نرم رکورد:
[ { "type":"Create", "user":1, "at":"2020-11-24T23:05:54.2692711+03:30", "sources":{ "hn":"localhost:44398", "mn":"DESKTOP-N1GAV2U", "rip":"::1", "lip":"::1", "ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36", "an":"Server.Api", "av":"1.0.0.0" }, "newValues":{ "Name":"Farshad" } }, { "type":"Update", "user":1, "at":"2020-11-24T23:06:20.0838188+03:30", "sources":{ "hn":"localhost:44398", "mn":"DESKTOP-N1GAV2U", "rip":"::1", "lip":"::1", "ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36", "an":"Server.Api", "av":"1.0.0.0" }, "newValues":{ "Name":"Edited Farshad" } }, { "type":"Delete", "user":null, "at":"2020-11-24T23:06:28.601837+03:30", "sources":{ "hn":"localhost:44398", "mn":"DESKTOP-N1GAV2U", "rip":"::1", "lip":"::1", "ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36", "an":"Server.Api", "av":"1.0.0.0" }, "newValues":null } ]
ولی روش گفته شده در این مقاله، همین عملیات را به صورت کاملتری و فقط بر روی یک ستون همان جدول انجام میدهد که باعث ذخیرهی دیتای کمتر، یکپارچگی بهتر و دسترسیپذیری و راحتی استفاده از آن میشود.
سری معروف فیبوناچی که معرف حضور شما هست. سری از اعداد است که هر عدد آن مساوی حاصل جمع دو عدد ماقبل آن است. دو عدد اول این سری هم 0 و 1 هستند.
اگر بخواهیم این الگوریتم را به صورت یک متد بازگشتی نمایش دهیم به صورت زیر خواهد بود:
public static int Fibonacci(int x)
{
if (x <= 1)
return 1;
return Fibonacci(x - 1) + Fibonacci(x - 2);
}
این الگوریتم چند مشکل دارد:
الف) برای اعداد بزرگ حتی با بکارگیری Int64 و یا double و امثال آن هم باز به جواب نخواهیم رسید (برای مثال 1500 را بررسی کنید).
ب) بسیار کند است.
در دات نت 4 برای کار با اعداد بزرگ، فضای نام System.Numerics معرفی شده است که حاوی نوع جدیدی از اعداد به نام BigInteger است.
اکنون اگر الگوریتم سری فیبوناچی را بر اساس این نوع داده جدید بازنویسی کنیم خواهیم داشت:
using System;
using System.Collections.Generic;
using System.Numerics; //needs a ref. to this assembly
namespace Fibonaci
{
public class CFibonacci
{
public static int Fibonacci(int x)
{
if (x <= 1)
return 1;
return Fibonacci(x - 1) + Fibonacci(x - 2);
}
public static IEnumerable<BigInteger> BigFib(Int64 toNumber)
{
BigInteger previous = 0;
BigInteger current = 1;
for (Int64 y = 1; y <= toNumber; y++)
{
var auxiliar = current;
current += previous;
previous = auxiliar;
yield return current;
}
}
}
}
using System;
using System.Linq;
namespace Fibonaci
{
class Program
{
static void Main()
{
foreach (var i in CFibonacci.BigFib(10))
{
Console.WriteLine("{0}", i);
}
var num = 12000;
var fib = CFibonacci.BigFib(num).Last();
Console.WriteLine("fib({0})={1}", num, fib);
Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
برای نمونه با عدد 12000 خروجی برنامه در کسری از ثانیه (و نه چند دقیقه یا ساعت) به شرح زیر خواهد بود:
fib(12000)=514263424911336592579396579289954520826834443526829600435873863248622
65414020714013892551476261070010099275571144059579167356039437242089427136323689
02207956221569622791450891447905907668251232675988098246382902426783148546665404
47372384043164600945249911273857878346679362876357499204290285069442042444471200
52292329349103672302428662317285015525888210397583707071480178840772972692357054
71823998861896761687119434646250991702691100894769561810834542099577336821493905
41651658937860506067011215222435859797671748514023462634575877112541265857011723
31453990415231608729534781720381122965899871018532003735284559342372552627132300
63895825396012087948050855095233633638445668687440232926253620457459973889510838
23542785159371236389909470974738599166720611351903568781845409425624666559791912
02212289710838873334773835118391287956725504426150461421914844191810523257658770
99885492757927034409234340065928400769741802132203888929463702342324148343605275
28928280472094493359682662519127203581813404104542972181231076224891404730611459
03321942693225066038987483163709402601230467054944349111055850348779989058517069
96087626795709205215727843443054577680024507650678240240742421270422674907476927
22422733945450760323640619100021663675080870429299040891840880753646474330069332
72320218334582268219906763463261387161318500503970491314781100556494361063341371
43577787961183154125538371204296752028496084633103476783071177779604042581017888
28257784920659671082363171157289668904381254080676855815524987553372657063695970
39668109161449140707240711279859427919912443872405284305891366802954763421905970
15206311458187449420118838775707435857999310870199585760807680179258273461000460
97527064929564528474349547038178370043823628944670926601955537657427194815893365
88494863101667547896798728140224921584809355334379707156342620570496834086358692
30946467203330676206265047960072392991634456381998479411463182171816379650120684
35082399788137090460167819041845511951296934273988759169877839532492294430334328
46972905198131530224288922834125154211248159843609629469051889033085360540770480
25633451201705370447586177546577777759300410144166197439355903631773088812515215
09638377918595294747887970034209028019490210394392422302403687059119407005858379
52137098994457236290005745735420803758853723206992134642997705010940581386168427
47382973672816710014652632509888958851675894223117421829434728942878605569971512
65291783384910157203679779458354245579846973830472593370160977523707902575129803
072039857524154149354311250529579592001