try { applyCorrectYeKe();
auditFields();
//and another methods ...
Return base.SaveChanges(); } catch (DbEntityValidationException validationException) { //... } catch (DbUpdateConcurrencyException concurrencyException) { //... } catch (DbUpdateException updateException) { //... }
در کتابخانهی 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); }
در کدها و افزونهای که در ادامه ارائه خواهند شد، این مسایل درنظر گرفته شده است:
- چگونه اعتبار سنجی سمت کاربر را در حین استفاده از Ajax فعال کنیم.
- چگونه از چندبار کلیک کاربر در حین ارسال فرم به سرور جلوگیری نمائیم.
- چگونه Complex Types قابل تعریف در EF Code first را نیز در اینجا مدیریت کنیم.
- نحوه تعریف صحیح آدرسهای کنترلرها چگونه باید باشد.
- نحوه اعلام وضعیت لاگین شخص به او، در صورت بروز مشکل.
- ارسال صحیح anti forgery token در حین اعمال Ajax ایی.
- بررسی Ajax بودن درخواست رسیده و تهیه یک فیلتر سفارشی مخصوص آن.
- از کش شدن اطلاعات Ajax ایی جلوگیری شود.
ابتدا معرفی مدل برنامه
using System.ComponentModel; using System.ComponentModel.DataAnnotations; namespace jQueryMvcSample01.Models { public class User { [Required(ErrorMessage = "(*)"), DisplayName("نام")] public string Name { set; get; } public PhoneInfo PhoneInfo { set; get; } } public class PhoneInfo { [Required(ErrorMessage = "(*)"), DisplayName("تلفن")] public string Phone { get; set; } [Required(ErrorMessage = "(*)"), DisplayName("پیش شماره")] public string Ext { get; set; } } }
کدهای کنترلر برنامه
using System.Web.Mvc; using jQueryMvcSample01.Models; using jQueryMvcSample01.Security; namespace jQueryMvcSample01.Controllers { public class HomeController : Controller { [HttpGet] public ActionResult Index() { return View(); //نمایش فرم } [HttpPost] [AjaxOnly] //فقط در حالت ایجکس قابل دسترسی باشد [ValidateAntiForgeryToken] public ActionResult Index(User user) { if (this.ModelState.IsValid) { // ذخیره سازی در بانک اطلاعاتی ... System.Threading.Thread.Sleep(3000); return Content("ok");//اعلام موفقیت آمیز بودن کار } return Content(null);//ارسال خطا } } }
چند نکته در اینجا حائز اهمیت هستند:
الف) استفاده از ویژگی AjaxOnly (که کدهای آنرا در پروژه پیوست میتوانید مشاهده نمائید)، جهت صرفا پردازش درخواستهای Ajaxایی.
ب) استفاده از ویژگی ValidateAntiForgeryToken در حین اعمال اجکسی. اگر سایتهای مختلف را در اینباره جستجو کنید، عموما برای پردازش آن در حین استفاده از jQuery Ajax بسیار مشکل دارند.
ج) استفاده از return Content برای اعلام نتیجه کار. اگر اطلاعات ثبت شد، یک ok یا هر عبارت دیگری که علاقمند بودید ارسال گردیده و در غیراینصورت null بازگشت داده میشود.
کدهای افزونه PostMvcFormAjax
// <![CDATA[ (function ($) { $.fn.PostMvcFormAjax = function (options) { var defaults = { postUrl: '/', loginUrl: '/login', beforePostHandler: null, completeHandler: null, errorHandler: null }; var options = $.extend(defaults, options); var validateForm = function (form) { //فعال سازی دستی اعتبار سنجی جیکوئری var val = form.validate(); val.form(); return val.valid(); }; return this.each(function () { var form = $(this); //اگر فرم اعتبار سنجی نشده، اطلاعات آن ارسال نشود if (!validateForm(form)) return; //در اینجا میتوان مثلا دکمهای را غیرفعال کرد if (options.beforePostHandler) options.beforePostHandler(this); //اطلاعات نباید کش شوند $.ajaxSetup({ cache: false }); $.ajax({ type: "POST", url: options.postUrl, data: form.serialize(), //تمام فیلدهای فرم منجمله آنتی فرجری توکن آنرا ارسال میکند complete: function (xhr, status) { var data = xhr.responseText; if (xhr.status == 403) { window.location = options.loginUrl; //در حالت لاگین نبودن شخص اجرا میشود } else if (status === 'error' || !data) { if (options.errorHandler) options.errorHandler(this); } else { if (options.completeHandler) options.completeHandler(this); } } }); }); }; })(jQuery); // ]]>
الف) فعال سازی دستی اعتبار سنجی جیکوئری، از این جهت که این نوع اعتبار سنجی به صورت پیش فرض تنها در حالت postback و ارسال کامل صفحه به سرور فعال میشود.
ب) استفاده از متد serialize جهت پردازش یکباره کل اطلاعات و فیلدهای یک فرم.
نکته مهم این متد ارسال فیلد مخفی anti forgery token نیز میباشد. فقط باید دقت داشت که این فیلد در حالتی که dataType به json تنظیم شود و همچنین از متد serialize استفاده گردد، در ASP.NET MVC پردازش نمیگردد (خیلی مهم!). به همین جهت در اینجا dataType تنظیمات jQuery Ajax حذف شده است.
ج) تنظیم cache به false در تنظیمات ابتدایی jQuery Ajax تا اطلاعات ارسالی و دریافتی کش نشوند و مشکل ساز نگردند.
د) بررسی xhr.status == 403 که توسط SiteAuthorizeAttribute (جایگزین بهتر فیلتر Authorize توکار ASP.NET MVC که کدهای آن در پروژه پیوست قابل دریافت است) و هدایت کاربر به صفحه لاگین
تعریف View ایی که از اشیاء تو در تو استفاده میکند و همچنین از افزونه فوق برای ارسال اطلاعات بهره خواهد برد:
@model jQueryMvcSample01.Models.User @{ ViewBag.Title = "تعریف کاربر"; var postUrl = Url.Action(actionName: "Index", controllerName: "Home"); } @using (Html.BeginForm(actionName: "Index", controllerName: "Home", method: FormMethod.Post, htmlAttributes: new { id = "UserForm" })) { @Html.ValidationSummary(true) @Html.AntiForgeryToken() <fieldset> <legend>تعریف کاربر</legend> <div class="editor-label"> @Html.LabelFor(model => model.Name) </div> <div class="editor-field"> @Html.EditorFor(model => model.Name) @Html.ValidationMessageFor(model => model.Name) </div> <div class="editor-label"> @Html.LabelFor(model => model.PhoneInfo.Ext) </div> <div class="editor-field"> @Html.EditorFor(model => model.PhoneInfo.Ext) @Html.ValidationMessageFor(model => model.PhoneInfo.Ext) </div> <div class="editor-label"> @Html.LabelFor(model => model.PhoneInfo.Phone) </div> <div class="editor-field"> @Html.EditorFor(model => model.PhoneInfo.Phone) @Html.ValidationMessageFor(model => model.PhoneInfo.Phone) </div> <p> <input type="submit" id="btnSave" value="ارسال" /> </p> </fieldset> } @section JavaScript { <script type="text/javascript"> $(document).ready(function () { $("#btnSave").click(function (event) { //جلوگیری از پست بک به سرور event.preventDefault(); var button = $(this); $("#UserForm").PostMvcFormAjax({ postUrl: '@postUrl', loginUrl: '/login', beforePostHandler: function () { //غیرفعال سازی دکمه ارسال button.attr('disabled', 'disabled'); button.val("..."); }, completeHandler: function () { //فعال سازی مجدد دکمه ارسال alert('انجام شد'); button.removeAttr('disabled'); button.val("ارسال"); }, errorHandler: function () { alert('خطایی رخ داده است'); } }); }); }); </script> }
@Html.EditorFor(model => model.PhoneInfo.Phone)
در ادامه نحوه استفاده از افزونه PostMvcFormAjax را مشاهده میکنید. چند نکته نیز در اینجا حائز اهمیت هستند:
الف) توسط htmlAttributes یک id برای فرم تعریف کردهایم تا در افزونه PostMvcFormAjax مورد استفاده قرار گیرد.
ب) postUrl و loginUrl را همانند متغیر تعریف شده در ابتدای View توسط Url.Action باید تعریف کرد تا در صورتیکه سایت ما در ریشه اصلی قرار نداشت، باز هم به صورت خودکار مسیر صحیحی محاسبه و ارائه گردد.
ج) نحوه غیرفعال سازی و فعال سازی دکمه submit را در روالهای beforePostHandler و completeHandler ملاحظه میکنید. این مساله برای جلوگیری از کلیکهای مجدد یک کاربر ناشکیبا و جلوگیری از ثبت اطلاعات تکراری بسیار مهم است.
د) کل این اطلاعات، در یک section به نام JavaScript ثبت شده است. این section در فایل layout برنامه به صورت زیر مورد استفاده قرار خواهد گرفت و به این ترتیب مقدار دهی خواهد شد:
<head> <title>@ViewBag.Title</title> <link href="@Url.Content("Content/Site.css")" rel="stylesheet" type="text/css" /> <script src="@Url.Content("~/Scripts/jquery-1.9.1.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery.PostMvcFormAjax.js")" type="text/javascript"></script> @RenderSection("JavaScript", required: false) </head>
دریافت کدهای کامل این قسمت
jQueryMvcSample01.zip
sudo apt-get update -y sudo apt-get upgrade -y
sudo java -version
sudo add-apt-repository -y ppa:webupd8team/java
gpg: keyring `/tmp/tmpkjrm4mnm/secring.gpg' created gpg: keyring `/tmp/tmpkjrm4mnm/pubring.gpg' created gpg: requesting key EEA14886 from hkp server keyserver.ubuntu.com gpg: /tmp/tmpkjrm4mnm/trustdb.gpg: trustdb created gpg: key EEA14886: public key "Launchpad VLC" imported gpg: no ultimately trusted keys found gpg: Total number processed: 1 gpg: imported: 1 (RSA: 1) OK
sudo apt-get update
sudo apt-get install oracle-java8-installer -y
sudo java -version
java version "1.8.0_66" Java(TM) SE Runtime Environment (build 1.8.0_66-b17) Java HotSpot(TM) 64-Bit Server VM (build 25.66-b17, mixed mode)
پس از دریافت Apache Kafka، از طریق یک terminal جدید وارد پوشه Downloads شوید. سپس فایل مورد نظر را از حالت فشرده خارج کنید و وارد پوشه اصلی آن شوید. برای این کار از دستورات زیر استفاده کنید:
tar -xzf kafka_2.11-1.0.0.tgz cd kafka_2.11-1.0.0
سپس با استفاده از دستور "ls " لیست تمامی فایلها و فولدرهای موجود در فولدر kafka_2.11-1.0.0 و را نمایش دهید:
در لیست نمایش داده شده دو فولد اصلی bin و config وجود دارند که به ترتیب فایلهای اجرایی و کانفیگهای پیشفرض و مورد نیاز، در آنها قرار دارند.
اجرای Apache ZooKeeper:
همانطور که در بخش قبل توضیح داده شد، Apache Kafka هیچ Stateی را در خود ذخیره و مدیریت نمیکند و اصطلاحا Stateless میباشد و مدیریت تمامی این Stateها را بر عهده Apache ZooKeeper قرار میدهد. بنابراین قبل از اینکه بخواهیم Kafka را اجرا کنیم، ابتدا باید Apache ZooKeeper را اجرا کنیم. برای اجرای Apache ZooKeeper در terminalی که قبلا باز کردهاید، دستور زیر را اجرا کنید:
bin/zookeeper-server-start.sh config/zookeeper.properties
با اجرای فایل zookeeper-server-start.sh و ارسال کانفیگ پیشفرضش در فولدر config با نام zookeeper.properties، قسمت مدیریت کننده Stateهای Kafka اجرا میشود.
اجرای Apache Kafka:
پس از اجرای Apache ZooKeeper، باید یک terminal جدید را باز کنید و با استفاده از دستور زیر kafka-server را با استفاده از کانفیگ پیشفرضش اجرا کنید:
bin/kafka-server-start.sh config/server.properties
به همین راحتی Kafka Server اجرا شده و آمادهی استفاده است. برای ادامه و نمایش دادن سایر قابلیتهای Kafka باید یک Topic جدید را ایجاد کنیم.
ایجاد Topic:
همانطور که میدانید تمامی پیامهای شما در Partitionهای Topic ذخیره میشوند؛ پس قبل از اینکه بخواهیم توسط یک Producer پیامی را ارسال کنیم یا اینکه بخواهیم توسط Consumer پیامی را دریافت کنیم، ابتدا باید Topic مربوطه را ایجاد کنیم. بهترین و عمومیترین مثال برای نمایش قابلیتهای Publish-Subscribe در Kafka، مثال چت بین کاربران است که در آن یک کاربر به عنوان Producer عمل میکند و پیامهایی را برای سایر کاربران که نقش Consumer را دارند ارسال میکند. kafka-topics
.sh
در Kafka ابزاریست برای مدیریت Topic ها که با استفاده از آن میتوانید Topicهای مورد نیاز را تعریف، ویرایش و یا حذف کنید.
یک terminal جدید را آغاز و با استفاده از دستور زیر یک Topic را با نام userChat ایجاد کنید:
bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic userChat
با این دستور یک Topic در Kafka با نام userChat ایجاد میشود و مشخصات آن (منظور stateهای مرتبط با آن است)، در zookeeper نیز ثبت میشود.
توضیحات Optionها و آرگومانهایی که در دستور ایجاد Topic استفاده شدند به شرح زیر است:
create-- :
مشخص کننده این است که شما میخواهید یک Topic جدید را ایجاد کنید.
zookeeper localhost:2181--:
مشخص کننده آدرس سرور zookeeper است (سرور zookeeper بصورت پیشفرض از پورت 2181 استفاده میکند).
replication-factor 1--:
مشخص کننده این است که تنها یک کپی از این Topic در سرور ایجاد شود. البته در این مثال به دلیل اینکه تنها یک سرور از Kafka را اجرا کردهایم در صورتیکه این مقدار را بیش از عدد 1 در نظر بگیریم، با خطا مواجه میشویم.
partitions 1--
تعداد Partitionهای این Topic را مشخص میکند. مقدار Partition در حالتیکه حتی یک سرور را نیز اجرا کردهایم، میتواند بیش از 1 باشد؛ اما در این حالت Primary Broker برای تمام Partitionهای همین سرور در نظر گرفته میشود.
topic userChat--
نام Topic را مشخص میکند.
پس از اینکه Topic مورد نظر ایجاد شد، با استفاده از دستور زیر میتوانیم لیست Topicهای ایجاد شده را مشاهده کنیم:
bin/kafka-topics.sh --list --zookeeper localhost:2181
توضیحات Optionها و آرگومانهایی که در دستور نمایش لیست Topicها استفاده شدند به شرح زیر است:
list--:
شما میخواهید لیست topicها را مشاهده کنید.
zookeeper localhost:2181--:
همانند دستور ایجاد، مشخص کننده سرور zookeeper میباشد.
تا اینجا برای ارسال و دریافت پیام در بین Applicationهای مختلف همه چیز آمادهاست. البته همانطور که در بخشهای بعدی این سری مقالات میبینیم، در عمل Producerها و Consumerها زیرسیستمهایی هستند که خود ما با استفاده از هر زبان برنامه نویسی پیاده سازی میکنیم. اما در این قسمت برای نمایش و تکمیل مثالمان، از ابزارهایی که خود Kafka در اختیار ما قرار داده استفاده میکنیم.
اجرای یک Producer و ارسال پیام به Kafka:
kafka-console-producer.sh ابزاریست که با استفاده از آن میتوانید به راحتی یک Producer را ایجاد کنید. در terminalی که قبلا برای مدیریت Topicها باز کردهاید با استفاده از دستور زیر، Producer را اجرا میکنیم:
bin/kafka-console-producer.sh --broker-list localhost:9092 --topic userChat
توضیحات Optionها و آرگومانهایی که در دستور ایجاد Producer استفاده شدند به شرح زیر است:
broker-list localhost:9092--:
نشان دهنده لیست Brokerها میباشد و در صورتیکه تعداد آنها بیش از یک باشد، میتوان آنها را با "," از هم جدا کرد (پورت پیشفرض Kafka server برای دریافت دستورات، 9092 میباشد).
topic userChat--:
Topicی از Kafka server را مشخص میکند که این Producer میخواهد پیامهای خود را برایش ارسال کند.
پس از اجرای دستور فوق، با تایپ کردن و زدن کلید Enter، پیام مورد نظر به Broker موردنظر یعنی localhost:9092 ارسال و در تنها Partition تاپیک userChat ذخیره میشود.
اجرای یک Consumer و دریافت پیامهای موجود در Topic مورد نظر:
kafka-console-consumer.sh ابزاریست که خود Kafka در اختیار ما قرار داده است و با استفاده از آن میتوانیم به راحتی یک Consumer برای userChat ،Topic ایجاد کنیم.
در یک terminal جدید با استفاده از دستور زیر یک Consumer را ایجاد کنید که userChat ،Topic را Subscribe میکند:
bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic userChat --from-beginning
توضیحات Optionها و آرگومانهایی که در دستور ایجاد یک Consumer استفاده شدند به شرح زیر است:
bootstrap-server localhost:9092-- :
مشخص کننده Kafka serverی است که میخواهیم از طریق آن به تمامی اعضای Cluster دسترسی داشته باشیم. در صورتیکه بیش از یک سرور را بخواهیم وارد کنیم، باید آنها را با "," از هم جدا کنیم.
topic userChat --:
مشخص کننده Topicی است که این Consumer میخواهد روی آن Subscribe کند.
from-beginning--:
مشخص کننده این است که Consumer ایجاد شده میخواهد تمامی پیامهای موجود در userChat ، Topic را از اولین offset تا آخرین offset دریافت کند.
پس از اجرای کد فوق مشاهده میکنید که پیامهایی که قبلا Producer در تاپیک userChat ثبت کردهاست، برای این Consumer ارسال میشوند.
از اینجا به بعد هر لحظه که در terminal ارسال کننده یا Producer پیامی تایپ کنید و کلید Enter را بزنید، بلافاصله پیام مورد نظر برای Consumer ارسال میشود و در terminal آن نمایش داده میشود. حتی میتوانیم چندین Consumer را در terminal های مختلفی اجرا کنیم. در این صورت با ارسال هر پیام از طرف Producer، تمامی Consumerها آن را نمایش میدهند.
با استفاده از سادگی راه اندازی و قابلیتهای بسیار زیادی که Apache Kafka در مدیریت جریان دادهای بین سیستمهایمان به ما میدهد، میتوانیم به سادگی در سیستمهایمان قابلیتهای مقیاس پذیری افقی، تحمل در برابر خطا، در دسترس بودن، کارآیی بالا و سادگی مدیریت ارتباطات بخشهای مختلف را اضافه کنیم. در بخشهای بعدی به نحوه ایجاد یک Cluster و اینکه چگونه میتوان از این بستر در Net. استفاده کرد، میپردازیم.
render() { fetch('https://jsonplaceholder.typicode.com/users') .then(response => response.json()) .then(data => this.setState({ users: data })) return ( <ul> {this.state.users.map(user => <li key={user.id}>{user.name}</li>)} </ul> ); }
- constructor
- render
- componentDidMount
//App.js import React, { Component } from 'react'; import { Users } from "./Users"; export default class App extends Component { constructor(props) { super(props); this.state = { toggle: false }; } handleChange = () => { this.setState({ toggle: !this.state.toggle }); } render() { return <div className="container text-center"> <div className="row p-2"> <div className="form-check"> <input type="checkbox" className="form-check-input" checked={this.state.toggle} onChange={this.handleChange} /> <label className="form-check-label">Toggle</label> </div> <div className="row p-2"> {this.state.toggle && <Users />} </div> </div> </div> } } //Users import React, { Component } from 'react'; export class Users extends Component { constructor(props) { super(props); this.state = { users: [] } } componentDidMount() { fetch('https://jsonplaceholder.typicode.com/users') .then(response => response.json()) .then(data => { this.setState({ users: data }); console.log(data) }) } render() { return ( <div className="bg-info text-white p-2"> {this.state.users && <p>{JSON.stringify(this.state.users, null, 2)}</p> } </div> ); } }
import React, { Component } from 'react'; export class Users extends Component { constructor(props) { // as before } componentDidMount() { // as before } componentDidUpdate() { console.log("componentDidUpdate Users Component"); } render() { // as before } }
import React, { Component } from 'react'; export class Users extends Component { constructor(props) { // as before } componentDidMount() { // as before } componentDidUpdate() { console.log("componentDidUpdate Users Component"); } componentWillUnmount() { console.log("componentWillUnmount Users Component"); } render() { // as before } }
import React, { useState, useEffect } from 'react'; export const Users = () => { const [users, setUsers] = useState([]); useEffect(() => { console.log("Same as componentDidMount") fetch('https://jsonplaceholder.typicode.com/users') .then(response => response.json()) .then(data => { setUsers({ users: data }); }) }, []); useEffect(() => { console.log("Same as componentDidUpdate") }); useEffect(() => { return () => { console.log("Same as componentWillUnmount") } }, []); return ( <div className="bg-info text-white p-2"> {users && <p>{JSON.stringify(users, null, 2)}</p> } </div> ); }
برای بهبود این وضعیت میتوان مرحلهی دومی را نیز به این فرآیند لاگین افزود؛ پس از اینکه مشخص شد کاربر وارد شدهی به سایت، دارای اکانتی در IDP ما است، کدی را به آدرس ایمیل او ارسال میکنیم. اگر این ایمیل واقعا متعلق به این شخص است، بنابراین قادر به دسترسی به آن، خواندن و ورود آن به برنامهی ما نیز میباشد. این اعتبارسنجی دو مرحلهای را میتوان به عملیات لاگین متداول از طریق ورود نام کاربری و کلمهی عبور در IDP ما نیز اضافه کرد.
تنظیم میانافزار Cookie Authentication
مرحلهی اول ایجاد گردش کاری اعتبارسنجی دو مرحلهای، فعالسازی میانافزار Cookie Authentication در برنامهی IDP است. برای این منظور به کلاس Startup آن مراجعه کرده و AddCookie را اضافه میکنیم:
namespace DNT.IDP { public class Startup { public const string TwoFactorAuthenticationScheme = "idsrv.2FA"; public void ConfigureServices(IServiceCollection services) { // ... services.AddAuthentication() .AddCookie(authenticationScheme: TwoFactorAuthenticationScheme) .AddGoogle(authenticationScheme: "Google", configureOptions: options => { options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; options.ClientId = Configuration["Authentication:Google:ClientId"]; options.ClientSecret = Configuration["Authentication:Google:ClientSecret"]; }); }
اصلاح اکشن متد Login برای هدایت کاربر به صفحهی ورود اطلاعات کد موقتی
تا این مرحله، در اکشن متد Login کنترلر Account، اگر کاربر، اطلاعات هویتی خود را صحیح وارد کند، به سیستم وارد خواهد شد. برای لغو این عملکرد پیشفرض، کدهای HttpContext.SignInAsync آنرا حذف کرده و با Redirect به اکشن متد نمایش صفحهی ورود کد موقتی ارسال شدهی به آدرس ایمیل کاربر، جایگزین میکنیم.
namespace DNT.IDP.Controllers.Account { [SecurityHeaders] [AllowAnonymous] public class AccountController : Controller { [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Login(LoginInputModel model, string button) { // ... if (ModelState.IsValid) { if (await _usersService.AreUserCredentialsValidAsync(model.Username, model.Password)) { var user = await _usersService.GetUserByUsernameAsync(model.Username); var id = new ClaimsIdentity(); id.AddClaim(new Claim(JwtClaimTypes.Subject, user.SubjectId)); await HttpContext.SignInAsync(scheme: Startup.TwoFactorAuthenticationScheme, principal: new ClaimsPrincipal(id)); await _twoFactorAuthenticationService.SendTemporaryCodeAsync(user.SubjectId); var redirectToAdditionalFactorUrl = Url.Action("AdditionalAuthenticationFactor", new { returnUrl = model.ReturnUrl, rememberLogin = model.RememberLogin }); // request for a local page if (Url.IsLocalUrl(model.ReturnUrl)) { return Redirect(redirectToAdditionalFactorUrl); } if (string.IsNullOrEmpty(model.ReturnUrl)) { return Redirect("~/"); } // user might have clicked on a malicious link - should be logged throw new Exception("invalid return URL"); } await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials")); ModelState.AddModelError("", AccountOptions.InvalidCredentialsErrorMessage); } // something went wrong, show form with error var vm = await BuildLoginViewModelAsync(model); return View(vm); }
- سپس بر اساس Id این کاربر، یک ClaimsIdentity تشکیل میشود.
- در ادامه با فراخوانی متد SignInAsync بر روی این ClaimsIdentity، یک کوکی رمزنگاری شده را با scheme تعیین شده که با authenticationScheme تنظیم شدهی در کلاس آغازین برنامه تطابق دارد، ایجاد میکنیم.
await HttpContext.SignInAsync(scheme: Startup.TwoFactorAuthenticationScheme, principal: new ClaimsPrincipal(id));
public interface ITwoFactorAuthenticationService { Task SendTemporaryCodeAsync(string subjectId); Task<bool> IsValidTemporaryCodeAsync(string subjectId, string code); }
- متد IsValidTemporaryCodeAsync، کد دریافت شدهی از کاربر را با نمونهی موجود در بانک اطلاعاتی مقایسه و اعتبار آنرا اعلام میکند.
ایجاد اکشن متد AdditionalAuthenticationFactor و View مرتبط با آن
پس از ارسال کد موقتی به کاربر، کاربر را به صورت خودکار به اکشن متد جدید AdditionalAuthenticationFactor هدایت میکنیم تا این کد موقتی را که به صورت ایمیل (و یا در اینجا با مشاهدهی لاگ برنامه)، دریافت کردهاست، وارد کند. همچنین returnUrl را نیز به این اکشن متد جدید ارسال میکنیم تا بدانیم پس از ورود موفق کد موقتی توسط کاربر، او را باید در ادامهی این گردش کاری به کجا هدایت کنیم. بنابراین قسمت بعدی کار، ایجاد این اکشن متد و تکمیل View آن است.
ViewModel ای که بیانگر ساختار View مرتبط است، چنین تعریفی را دارد:
using System.ComponentModel.DataAnnotations; namespace DNT.IDP.Controllers.Account { public class AdditionalAuthenticationFactorViewModel { [Required] public string Code { get; set; } public string ReturnUrl { get; set; } public bool RememberLogin { get; set; } } }
سپس اکشن متد AdditionalAuthenticationFactor در حالت Get، این View را نمایش میدهد و در حالت Post، اطلاعات آنرا از کاربر دریافت خواهد کرد:
namespace DNT.IDP.Controllers.Account { public class AccountController : Controller { [HttpGet] public IActionResult AdditionalAuthenticationFactor(string returnUrl, bool rememberLogin) { // create VM var vm = new AdditionalAuthenticationFactorViewModel { RememberLogin = rememberLogin, ReturnUrl = returnUrl }; return View(vm); } [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> AdditionalAuthenticationFactor( AdditionalAuthenticationFactorViewModel model) { if (!ModelState.IsValid) { return View(model); } // read identity from the temporary cookie var info = await HttpContext.AuthenticateAsync(Startup.TwoFactorAuthenticationScheme); var tempUser = info?.Principal; if (tempUser == null) { throw new Exception("2FA error"); } // ... check code for user if (!await _twoFactorAuthenticationService.IsValidTemporaryCodeAsync(tempUser.GetSubjectId(), model.Code)) { ModelState.AddModelError("code", "2FA code is invalid."); return View(model); } // login the user AuthenticationProperties props = null; if (AccountOptions.AllowRememberLogin && model.RememberLogin) { props = new AuthenticationProperties { IsPersistent = true, ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration) }; } // issue authentication cookie for user var user = await _usersService.GetUserBySubjectIdAsync(tempUser.GetSubjectId()); await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username)); await HttpContext.SignInAsync(user.SubjectId, user.Username, props); // delete temporary cookie used for 2FA await HttpContext.SignOutAsync(Startup.TwoFactorAuthenticationScheme); if (_interaction.IsValidReturnUrl(model.ReturnUrl) || Url.IsLocalUrl(model.ReturnUrl)) { return Redirect(model.ReturnUrl); } return Redirect("~/"); }
- فراخوانی HttpContext.SignInAsync با اسکیمای مشخص شده، یک کوکی رمزنگاری شده را در اکشن متد Login ایجاد میکند. اکنون در اینجا با استفاده از متد HttpContext.AuthenticateAsync و ذکر همان اسکیما، میتوانیم به محتوای این کوکی رمزنگاری شده دسترسی داشته باشیم و از طریق آن، Id کاربر را استخراج کنیم.
- اکنون که این Id را داریم و همچنین Code موقتی نیز از طرف کاربر ارسال شدهاست، آنرا به متد IsValidTemporaryCodeAsync که پیشتر در مورد آن توضیح دادیم، ارسال کرده و اعتبارسنجی میکنیم.
- در آخر این کوکی رمزنگاری شده را با فراخوانی متد HttpContext.SignOutAsync، حذف و سپس یک کوکی جدید را بر اساس اطلاعات هویت کاربر، توسط متد HttpContext.SignInAsync ایجاد و ثبت میکنیم تا کاربر بتواند بدون مشکل وارد سیستم شود.
View متناظر با آن نیز در فایل src\IDP\DNT.IDP\Views\Account\AdditionalAuthenticationFactor.cshtml، به صورت زیر تعریف شدهاست تا کد موقتی را به همراه آدرس بازگشت پس از ورود آن، به سمت سرور ارسال کند:
@model AdditionalAuthenticationFactorViewModel <div> <div class="page-header"> <h1>2-Factor Authentication</h1> </div> @Html.Partial("_ValidationSummary") <div class="row"> <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title">Input your 2FA code</h3> </div> <div class="panel-body"> <form asp-route="Login"> <input type="hidden" asp-for="ReturnUrl" /> <input type="hidden" asp-for="RememberLogin" /> <fieldset> <div class="form-group"> <label asp-for="Code"></label> <input class="form-control" placeholder="Code" asp-for="Code" autofocus> </div> <div class="form-group"> <button class="btn btn-primary">Submit code</button> </div> </fieldset> </form> </div> </div> </div> </div>
آزمایش برنامه جهت بررسی اعتبارسنجی دو مرحلهای
پس از طی این مراحل، اعتبارسنجی دو مرحلهای در برنامه فعال شدهاست. اکنون برای آزمایش آن، برنامهها را اجرا میکنیم. پس از لاگین، صفحهی زیر نمایش داده میشود:
همچنین کد موقتی این مرحله را نیز در لاگهای برنامه مشاهده میکنید:
پس از ورود آن، کار اعتبارسنجی نهایی آن انجام شده و سپس بلافاصله به برنامهی MVC Client هدایت میشویم.
اضافه کردن اعتبارسنجی دو مرحلهای به قسمت ورود از طریق تامین کنندههای هویت خارجی
دقیقا همین مراحل را نیز به اکشن متد Callback کنترلر ExternalController اضافه میکنیم. در این اکشن متد، تا قسمت کدهای مشخص شدن user آن که از اکانت خارجی وارد شدهاست، با قبل یکی است. پس از آن تمام کدهای لاگین شخص به برنامه را از اینجا حذف و به اکشن متد جدید AdditionalAuthenticationFactor در همین کنترلر منتقل میکنیم.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی 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 وارد کنید.
یکی از سؤالاتی که ممکن است در مصاحبهها با آن روبرو شوید، عنوان این مطلب است. در این مقاله قصد داریم تفاوت بین این دو را بررسی کنیم.
در علم کامپیوتر، یک call stack، یک ساختار دادهای پشته میباشد که اطلاعات جزئی را راجع به زیرروالهای فعال یک برنامه، ذخیره میکند. این نوع پشته با اسامی مختلفی از جمله Execution Stack (ES)، Program Stack (PS)، Control Stack، Runtime Stack یا Machine Stack شناخته میشود و یا اینکه به صورت کلی به آن The Stack یا همان پشته هم میگویند. با استفاده از صفحه Call Stack میتوانیم توابع و پروسیجرهایی را که فراخوانی شدهاند، ببینیم. Call Stack به ما میگوید که کدام متدها و توابع، با چه ترتیبی اجرا شدهاند. Call Stack یک راه بسیار خوب، برای فهم درست نحوهی اجرای یک برنامه است. با رهگیری یا Track کردن Call Stack میتوانیم بفهمیم که مکانیزم کار داخلی برنامه چگونه است و برای بهتر رفع کردن مشکلات، از آن استفاده کنیم. در سی شارپ، Stack Trace یک پشتهی اجرایی یا Execution Stack است که تمامی متدهای درحال اجرا را رهگیری (Track) میکند. Stack Trace راهی است که با استفاده از آن میتوانیم Call Stack را بررسی کنیم، تا شماره خط متدی را که Exception درآن رخ داده است، ببینیم. از این به بعد برای سادگی مطلب، Stack Trace را به صورت ST بیان میکنیم. برای دسترسی به ST از فضای نام System.Diagnostics مانند زیر استفاده میکنیم.
System.Diagnostics.StackTrace myTrack = new System.Diagnostics.StackTrace();
و یا برای مشاهدهی آن در کنسول از کد زیر استفاده میکنیم:
static void Main(string[] args) { Console.WriteLine("Stack Trace: {0}", Environment.StackTrace); }
نتیجه:
Stack Trace:at System.Environment.get_StackTrace() at DiffThrowAndThrowException.Program.Main(String[] args) in Program.cs:line 9
استفاده از ST در Try Catch :
static void Main(string[] args) { try { throw new Exception(); } catch (Exception ex) { Console.WriteLine("Stack Trace: {0}", Environment.StackTrace); } }
نتیجه:
Stack Trace:at System.Environment.get_StackTrace() at DiffThrowAndThrowException.Program.Main(String[] args) in Program.cs:line 11
حالا که مفهوم Stack Trace و نحوه کار و نمایش آن را بررسی کردیم، به راحتی میتوان تفاوت بین Throw و Throw Exception را درک کرد. به طور کلی میتوان اینطور گفت، در حالتیکه در داخل بلاک Catch، از Throw استفاده کنیم، این کار باعث میشود استثنائی که در اینجا رخ داده، به ابتدای ST افزوده شده و در واقع سلسله مراتب اجرای برنامه تا جایی که Throw نوشته شده، در ST نگهداری شود. اما در صورتیکه به جای Throw از Throw Exception استفاده کنیم، اتفاقی که رخ میدهد این است که ST تا اینجای کار که throw exception را استفاده میکنیم، نگهداری میشود و اطلاعات متدهایی که بعد از throw exception اجرا شدهاند، از آن حذف میشود. در نهایت در این حالت ST شامل اطلاعات متدهای اجرا شده در فرآیند جاری، از ابتدا تا رسیدن به throw exception میباشد.
در زیر، نمونه کدی را برای استفاده از Throw، میبینید:
class Program { static void Main(string[] args) { FirstExceptionMethod firstException = new FirstExceptionMethod(); try { firstException.Method1(); } catch (Exception ex) { Console.WriteLine(ex.StackTrace); } } } class FirstExceptionMethod { public void Method1() { try { SecondExceptionMethod secondException = new SecondExceptionMethod(); secondException.Method2(); } catch (Exception ex) { throw; } } } class SecondExceptionMethod { public void Method2() { try { ThrowExMethod(); } catch (Exception ex) { throw; } } public void ThrowExMethod() { throw new Exception(); } }
نتیجه:
at DiffThrowAndThrowException.SecondExceptionMethod.ThrowExMethod() in Program.cs:line 51 at DiffThrowAndThrowException.SecondExceptionMethod.Method2() in Program.cs:line 41 at DiffThrowAndThrowException.FirstExceptionMethod.Method1() in Program.cs:line 27 at DiffThrowAndThrowException.Program.Main(String[] args) in Program.cs:line 12
همانطور که میبینید اطلاعات متدهایی که در این فرآیند اجرا شدهاند، در داخل Stack Trace رهگیری (track) شدهاند.
این دفعه برای نشان دادن تفاوت محتویات ST، کدهای Method1 را به شکل زیر تغییر میدهیم که در بخش catch آن، از throw exception استفاده کردهایم:
public void Method1() { try { SecondExceptionMethod secondException = new SecondExceptionMethod(); secondException.Method2(); } catch (Exception ex) { throw new Exception("Some Text ..."); } }
نتیجه:
at DiffThrowAndThrowException.FirstExceptionMethod.Method1() in Program.cs:line 31 at DiffThrowAndThrowException.Program.Main(String[] args) in Program.cs:line 12
در اینجا اطلاعات متدهای برنامه از شروع فرآیند تا جائیکه از throw exception استفاده کرده، در ST نوشته میشود.
با دیدن خروجیهای بالا میتوان دریافت که استفاده از throw exception بجای throw باعث میشود تا اطلاعات کمتری از فرآیند اجرا شده در ST ذخیره شود و در واقع رهگیری متدهای فرآیند از ابتدا تا جائیکه throw exception استفاده میشود، پیش میرود و بعد از آن اطلاعاتی را ثبت نمیکند.
_categoryService.AddNewCategory(category); _uow.SaveAllChanges(); throw new InvalidOperationException(); return RedirectToAction("Index");
public interface IRunAtInit { void Execute(); } public interface IRunAfterEachRequest { void Execute(); } public interface IRunAtStartUp { void Execute(); } public interface IRunOnEachRequest { void Execute(); } public interface IRunOnError { void Execute(); }
public class TaskRegistry : StructureMap.Configuration.DSL.Registry { public TaskRegistry() { Scan(scan => { scan.Assembliy("yourAssemblyName"); scan.AddAllTypesOf<IRunAtInit>(); scan.AddAllTypesOf<IRunAtStartUp>(); scan.AddAllTypesOf<IRunOnEachRequest>(); scan.AddAllTypesOf<IRunOnError>(); scan.AddAllTypesOf<IRunAfterEachRequest>(); }); } }
ioc.AddRegistry(new TaskRegistry());
protected void Application_Start() { // other code foreach (var task in SmObjectFactory.Container.GetAllInstances<IRunAtInit>()) { task.Execute(); } } protected void Application_BeginRequest() { foreach (var task in SmObjectFactory.Container.GetAllInstances<IRunOnEachRequest>()) { task.Execute(); } } protected void Application_EndRequest(object sender, EventArgs e) { try { foreach (var task in SmObjectFactory.Container.GetAllInstances<IRunAfterEachRequest>()) { task.Execute(); } } finally { HttpContextLifecycle.DisposeAndClearAll(); MiniProfiler.Stop(); } } protected void Application_Error() { foreach (var task in SmObjectFactory.Container.GetAllInstances<IRunOnError>()) { task.Execute(); } }
public class TransactionPerRequest : IRunOnEachRequest, IRunOnError, IRunAfterEachRequest { private readonly IUnitOfWork _uow; private readonly HttpContextBase _httpContext; public TransactionPerRequest(IUnitOfWork uow, HttpContextBase httpContext) { _uow = uow; _httpContext = httpContext; } void IRunOnEachRequest.Execute() { _httpContext.Items["_Transaction"] = _uow.Database.BeginTransaction(System.Data.IsolationLevel.ReadCommitted); } void IRunOnError.Execute() { _httpContext.Items["_Error"] = true; } void IRunAfterEachRequest.Execute() { var transaction = (DbContextTransaction) _httpContext.Items["_Transaction"]; if (_httpContext.Items["_Error"] != null) { transaction.Rollback(); } else { transaction.Commit(); } } }
_categoryService.AddNewCategory(category); _uow.SaveAllChanges(); throw new InvalidOperationException(); return RedirectToAction("Index");
یک try/catch بذار، تا بتونی تاریخ مشکل دار رو پیدا کنی:
var pers = new PersianCalendar(); var date = pers.ToDateTime(this.Year, this.Month, this.Day, this.Hour, this.Minute, this.Second, 0).ToString(); try { return SqlDateTime.Parse(date); } catch(Exception ex) { throw new InvalidOperationException("Can't parse "+ date); }
نحوه کار با ftp - بخش اول
مواردی هم که اعلام کزدید کاملا صحیح میباشد.فقط بخش تابعی که درون آن از try/catch استفاده شده رو میخواستم ابتدا در تیکه کد دیگری از یک صفحه aspx بنویسم که وقتی برای بررسی مجدد تابع پیدا نکردم وگرنه اونجا try/catch بلا استفاده است.
موارد متذکر شده برای سهولت بیشتر کاربران در کدهای فوق لحاظ و ویرایش گردید.
با تشکر.