مطالب
Attribute Routing در ASP.NET MVC 5
Routing مکانیزم مسیریابی ASP.NET MVC است، که یک URI را به یک اکشن متد نگاشت می‌کند. MVC 5 نوع جدیدی از مسیر یابی را پشتیبانی میکند که Attribute Routing یا مسیریابی نشانه ای نام دارد. همانطور که از نامش پیداست، مسیریابی نشانه ای از Attribute‌ها برای این امر استفاده میکند. این روش به شما کنترل بیشتری روی URI‌های اپلیکیشن تان می‌دهد.
مدل قبلی مسیریابی (conventional-routing) هنوز کاملا پشتیبانی می‌شود. در واقع می‌توانید هر دو تکنیک را بعنوان مکمل یکدیگر در یک پروژه استفاده کنید.
در این پست قابلیت‌ها و گزینه‌های اساسی مسیریابی نشانه ای را بررسی میکنیم.
  • چرا مسیریابی نشانه ای؟
  • فعال سازی مسیریابی نشانه ای
  • پارامتر‌های اختیاری URI و مقادیر پیش فرض
  • پیشوند مسیر ها
  • مسیر پیش فرض
  • محدودیت‌های مسیر ها
      • محدودیت‌های سفارشی
  • نام مسیر ها
  • ناحیه‌ها (Areas)


چرا مسیریابی نشانه ای

برای مثال یک وب سایت تجارت آنلاین بهینه شده اجتماعی، می‌تواند مسیرهایی مانند لیست زیر داشته باشد:
  • {productId:int}/{productTitle}
نگاشت می‌شود به: (ProductsController.Show(int id
  • {username}
نگاشت می‌شود به: (ProfilesController.Show(string username
  • {username}/catalogs/{catalogId:int}/{catalogTitle}
نگاشت می‌شود به: (CatalogsController.Show(string username, int catalogId
در نسخه قبلی ASP.NET MVC، قوانین مسیریابی در فایل RouteConfig.cs تعریف می‌شدند، و اشاره به اکشن‌های کنترلرها به نحو زیر انجام می‌شد:
routes.MapRoute(
    name: "ProductPage",
    url: "{productId}/{productTitle}",
    defaults: new { controller = "Products", action = "Show" },
    constraints: new { productId = "\\d+" }
);
هنگامی که قوانین مسیریابی در کنار اکشن متدها تعریف می‌شوند، یعنی در یک فایل سورس و نه در یک کلاس پیکربندی خارجی، درک و فهم نگاشت URI‌ها به اکشن‌ها واضح‌تر و راحت می‌شود. تعریف مسیر قبلی، می‌تواند توسط یک attribute ساده بدین صورت نگاشت شود:
[Route("{productId:int}/{productTitle}")]
public ActionResult Show(int productId) { ... }

فعال سازی Attribute Routing

برای فعال سازی مسیریابی نشانه ای، متد MapMvcAttributeRoutes را هنگام پیکربندی فراخوانی کنید.
public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
        routes.MapMvcAttributeRoutes();
    }
}
همچنین می‌توانید مدل قبلی مسیریابی را با تکنیک جدید تلفیق کنید.
public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
    routes.MapMvcAttributeRoutes();
 
    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
    );
}

پارامترهای اختیاری URI و مقادیر پیش فرض

می توانید با اضافه کردن یک علامت سوال به پارامترهای مسیریابی، آنها را optional یا اختیاری کنید. برای تعیین مقدار پیش فرض هم از فرمت parameter=value استفاده می‌کنید.
public class BooksController : Controller
{
    // eg: /books
    // eg: /books/1430210079
    [Route("books/{isbn?}")]
    public ActionResult View(string isbn)
    {
        if (!String.IsNullOrEmpty(isbn))
        {
            return View("OneBook", GetBook(isbn));
        }
        return View("AllBooks", GetBooks());
    }
 
    // eg: /books/lang
    // eg: /books/lang/en
    // eg: /books/lang/he
    [Route("books/lang/{lang=en}")]
    public ActionResult ViewByLanguage(string lang)
    {
        return View("OneBook", GetBooksByLanguage(lang));
    }
}
در این مثال، هر دو مسیر books/ و books/1430210079/ به اکشن متد "View" نگاشت می‌شوند، مسیر اول تمام کتاب‌ها را لیست میکند، و مسیر دوم جزئیات کتابی مشخص را لیست می‌کند. هر دو مسیر books/lang/ و books/lang/en/ به یک شکل نگاشت می‌شوند، چرا که مقدار پیش فرض این پارامتر en تعریف شده.



پیشوند مسیرها (Route Prefixes)

برخی اوقات، تمام مسیرها در یک کنترلر با یک پیشوند شروع می‌شوند. بعنوان مثال:
public class ReviewsController : Controller
{
    // eg: /reviews
    [Route("reviews")]
    public ActionResult Index() { ... }
    // eg: /reviews/5
    [Route("reviews/{reviewId}")]
    public ActionResult Show(int reviewId) { ... }
    // eg: /reviews/5/edit
    [Route("reviews/{reviewId}/edit")]
    public ActionResult Edit(int reviewId) { ... }
}
همچنین می‌توانید با استفاده از خاصیت [RoutePrefix] یک پیشوند عمومی برای کل کنترلر تعریف کنید:
[RoutePrefix("reviews")]
public class ReviewsController : Controller
{
    // eg.: /reviews
    [Route]
    public ActionResult Index() { ... }
    // eg.: /reviews/5
    [Route("{reviewId}")]
    public ActionResult Show(int reviewId) { ... }
    // eg.: /reviews/5/edit
    [Route("{reviewId}/edit")]
    public ActionResult Edit(int reviewId) { ... }
}
در صورت لزوم، می‌توانید برای بازنویسی (override) پیشوند مسیرها از کاراکتر ~ استفاده کنید:
[RoutePrefix("reviews")]
public class ReviewsController : Controller
{
    // eg.: /spotlight-review
    [Route("~/spotlight-review")]
    public ActionResult ShowSpotlight() { ... }
 
    ...
}

مسیر پیش فرض

می توانید خاصیت [Route] را روی کنترلر اعمال کنید، تا اکشن متد را بعنوان یک پارامتر بگیرید. این مسیر سپس روی تمام اکشن متدهای این کنترلر اعمال می‌شود، مگر آنکه یک [Route] بخصوص روی اکشن‌ها تعریف شده باشد.
[RoutePrefix("promotions")]
[Route("{action=index}")]
public class ReviewsController : Controller
{
    // eg.: /promotions
    public ActionResult Index() { ... }
 
    // eg.: /promotions/archive
    public ActionResult Archive() { ... }
 
    // eg.: /promotions/new
    public ActionResult New() { ... }
 
    // eg.: /promotions/edit/5
    [Route("edit/{promoId:int}")]
    public ActionResult Edit(int promoId) { ... }
}

محدودیت‌های مسیر ها

با استفاده از Route Constraints می‌توانید نحوه جفت شدن پارامتر‌ها در قالب مسیریابی را محدود و کنترل کنید. فرمت کلی {parameter:constraint} است. بعنوان مثال:
// eg: /users/5
[Route("users/{id:int}"]
public ActionResult GetUserById(int id) { ... }
 
// eg: users/ken
[Route("users/{name}"]
public ActionResult GetUserByName(string name) { ... }
در اینجا، مسیر اول تنها در صورتی انتخاب می‌شود که قسمت id در URI یک مقدار integer باشد. در غیر اینصورت مسیر دوم انتخاب خواهد شد.
جدول زیر constraint‌ها یا محدودیت هایی که پشتیبانی می‌شوند را لیست می‌کند.
 مثال  توضیحات  محدودیت
 {x:alpha}  کاراکترهای الفبای لاتین را تطبیق (match) می‌دهد (a-z, A-Z).  alpha
 {x:bool}  یک مقدار منطقی را تطبیق می‌دهد.  bool
 {x:datetime}  یک مقدار DateTime را تطبیق می‌دهد.  datetime
 {x:decimal}  یک مقدار پولی را تطبیق می‌دهد.  decimal
 {x:double}  یک مقدار اعشاری 64 بیتی را تطبیق می‌دهد.  double
 {x:float}  یک مقدار اعشاری 32 بیتی را تطبیق می‌دهد.  float
 {x:guid}  یک مقدار GUID را تطبیق می‌دهد.  guid
 {x:int}  یک مقدار 32 بیتی integer را تطبیق می‌دهد.  int
 {(x:length(6}
{(x:length(1,20}
 رشته ای با طول تعیین شده را تطبیق می‌دهد.  length
 {x:long}  یک مقدار 64 بیتی integer را تطبیق می‌دهد.  long
 {(x:max(10}  یک مقدار integer با حداکثر مجاز را تطبیق می‌دهد.  max
 {(x:maxlength(10}  رشته ای با حداکثر طول تعیین شده را تطبیق می‌دهد.  maxlength
 {(x:min(10}  مقداری integer با حداقل مقدار تعیین شده را تطبیق می‌دهد.  min
 {(x:minlength(10}  رشته ای با حداقل طول تعیین شده را تطبیق می‌دهد.  minlength
 {(x:range(10,50}  مقداری integer در بازه تعریف شده را تطبیق می‌دهد.  range
 {(${x:regex(^\d{3}-\d{3}-\d{4}  یک عبارت با قاعده را تطبیق می‌دهد.  regex

توجه کنید که بعضی از constraint ها، مانند "min" آرگومان‌ها را در پرانتز دریافت می‌کنند.
می توانید محدودیت‌های متعددی روی یک پارامتر تعریف کنید، که باید با دونقطه جدا شوند. بعنوان مثال:
// eg: /users/5
// but not /users/10000000000 because it is larger than int.MaxValue,
// and not /users/0 because of the min(1) constraint.
[Route("users/{id:int:min(1)}")]
public ActionResult GetUserById(int id) { ... }
مشخص کردن اختیاری بودن پارامتر ها، باید در آخر لیست constraints تعریف شود:
// eg: /greetings/bye
// and /greetings because of the Optional modifier,
// but not /greetings/see-you-tomorrow because of the maxlength(3) constraint.
[Route("greetings/{message:maxlength(3)?}")]
public ActionResult Greet(string message) { ... }

محدودیت‌های سفارشی

با پیاده سازی قرارداد IRouteConstraint می‌توانید محدودیت‌های سفارشی بسازید. بعنوان مثال، constraint زیر یک پارامتر را به لیستی از مقادیر قابل قبول محدود می‌کند:
public class ValuesConstraint : IRouteConstraint
{
    private readonly string[] validOptions;
    public ValuesConstraint(string options)
    {
        validOptions = options.Split('|');
    }
 
    public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
        object value;
        if (values.TryGetValue(parameterName, out value) && value != null)
        {
            return validOptions.Contains(value.ToString(), StringComparer.OrdinalIgnoreCase);
        }
        return false;
    }
}
قطعه کد زیر نحوه رجیستر کردن این constraint را نشان می‌دهد:
public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
        var constraintsResolver = new DefaultInlineConstraintResolver();
 
        constraintsResolver.ConstraintMap.Add("values", typeof(ValuesConstraint));
 
        routes.MapMvcAttributeRoutes(constraintsResolver);
    }
}
حالا می‌توانید این محدودیت سفارشی را روی مسیرها اعمال کنید:
public class TemperatureController : Controller
{
    // eg: temp/celsius and /temp/fahrenheit but not /temp/kelvin
    [Route("temp/{scale:values(celsius|fahrenheit)}")]
    public ActionResult Show(string scale)
    {
        return Content("scale is " + scale);
    }
}

نام مسیر ها

می توانید به مسیرها یک نام اختصاص دهید، با این کار تولید URI‌ها هم راحت‌تر می‌شوند. بعنوان مثال برای مسیر زیر:
[Route("menu", Name = "mainmenu")]
public ActionResult MainMenu() { ... }
می‌توانید لینکی با استفاده از Url.RouteUrl تولید کنید:
<a href="@Url.RouteUrl("mainmenu")">Main menu</a>

ناحیه‌ها (Areas)

برای مشخص کردن ناحیه ای که کنترلر به آن تعلق دارد می‌توانید از خاصیت [RouteArea] استفاده کنید. هنگام استفاده از این خاصیت، می‌توانید با خیال راحت کلاس AreaRegistration را از ناحیه مورد نظر حذف کنید.
[RouteArea("Admin")]
[RoutePrefix("menu")]
[Route("{action}")]
public class MenuController : Controller
{
    // eg: /admin/menu/login
    public ActionResult Login() { ... }
 
    // eg: /admin/menu/show-options
    [Route("show-options")]
    public ActionResult Options() { ... }
 
    // eg: /stats
    [Route("~/stats")]
    public ActionResult Stats() { ... }
}
با این کنترلر، فراخوانی تولید لینک زیر، رشته "Admin/menu/show-options/" را بدست میدهد:
Url.Action("Options", "Menu", new { Area = "Admin" })
به منظور تعریف یک پیشوند سفارشی برای یک ناحیه، که با نام خود ناحیه مورد نظر متفاوت است می‌توانید از پارامتر AreaPrefix استفاده کنید. بعنوان مثال:
[RouteArea("BackOffice", AreaPrefix = "back-office")]
اگر از ناحیه‌ها هم بصورت مسیریابی نشانه ای، و هم بصورت متداول (که با کلاس‌های AreaRegistration پیکربندی می‌شوند) استفاده می‌کنید باید مطمئن شوید که رجیستر کردن نواحی اپلیکیشن پس از مسیریابی نشانه ای پیکربندی می‌شود. به هر حال رجیستر کردن ناحیه‌ها پیش از تنظیم مسیرها بصورت متداول باید صورت گیرد. دلیل آن هم مشخص است، برای اینکه درخواست‌های ورودی بدرستی با مسیرهای تعریف شده تطبیق داده شوند، باید ابتدا attribute routes، سپس area registration و در آخر default route رجیستر شوند. بعنوان مثال:
public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
    routes.MapMvcAttributeRoutes();
 
    AreaRegistration.RegisterAllAreas();
 
    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
    );
}

مطالب
نمایش اشیاء موجود در View بر اساس دسترسی‌ها در ASP.NET MVC
سیستم دسترسی در یک سیستم، همیشه برای من چالش برانگیز بوده است. با دیدن کدهای مختلف از افراد مختلف، شیوه‌های گوناگونی از کدنویسی را دیده‌ام؛ ولی یکی از نکاتی که در بین آن‌ها بررسی نشده بود و یا از آن غافل مانده بودند، بررسی بعضی از عناصر موجود در ویو بود که باید با توجه به نقش کاربر سیستم، وضعیت آن بررسی میشد. 
برای مثال تصور کنید که شما دو کاربر دارید که هر دو سطح دسترسی به پروفایل کاربران دیگر را دارند. ولی یکی از کاربرها این توانایی را دارد تا با کلیک بر روی دکمه‌ای، لاگین کاربر مورد نظر را مسدود نماید، ولی دیگری نمی‌تواند این دکمه را ببیند، یا برای او به صورت غیرفعال نمایش داده شود. در اکثر این مواقع کاری که برنامه نویسان انجام میدهند، نوشتن توابع به شیوه‌های گوناگون در لابلای انبوهی از کدهای View هست تا بتوانند این مسئله را پیاده سازی کنند. ولی با اضافه شدن هر چه بیشتر این عناصر به صفحه و با توجه به انبوهی از ویووها و ویووکامپوننت‌ها کار بسیار سخت‌تر میشود.
 به همین جهت من از فیلترها در MVC کمک گرفتم و این موضوع را با توجه به خروجی نهایی صفحه بررسی میکنم. به این معنا که وقتی صفحه بدون هیچ گونه اعتبارسنجی در سطح ویو آماده شد، با استفاده از فیلتر مورد نظر که به صورت سراسری اضافه شده است، بررسی میکنم که آیا این کاربر حق دارد بعضی از المان‌ها را ببیند یا خیر؟ در صورتیکه برای هر المان موجود در صفحه اعتباری نداشته باشد، آن المان از صفحه حذف و یا غیرفعال میشود.
ابتدا یک صفحه را با المان‌های زیر میسازیم. از آنجا که بیشتر تگ‌های عملیاتی از نوع لینک و دکمه هستند، از هر کدام، سه عنصر به صفحه اضافه میکنیم:
<button data-perm="true" data-controller="c" data-action="r" data-method="post" data-type="disable">Test 1</button>
<button data-perm="true" data-controller="c" data-action="t" data-method="post">Test 2</button>
<button data-perm="false" data-controller="c" data-action="r" data-method="post">Test 3</button>
<a data-perm="true" data-controller="c" data-action="m" data-method="post">Test 4</a>
<a data-perm="false" data-controller="c" data-action="m" data-method="post">Test 5</a>
<a data-perm="true" data-controller="c" data-action="t" data-method="post">Test 6</a>
در اینجا ما از همان ساختار آشنای *-data استفاده میکنیم و معنی هر کدام از ویژگی‌ها برابر زیر است:
ویژگی
توضیحات
 perm   آیا نیاز به اعتبارسنجی دارد یا خیر؟ در صورتی که المانی مقدار Perm آن با مقدار true پر گردد، اعتبارسنجی روی آن اعمال خواهد شد.   
 controller   نام کنترلری که به آن دسترسی دارد.  
 action  نام اکشنی که به آن در کنترلر ذکر شده دسترسی دارد. 
 method   در صورتیکه دسترسی get و post و ... هر یک متفاوت باشد.  
 type  نحوه برخورد با المان غیرمجاز. در صورتیکه با disable مقداردهی شود، المان غیرفعال و در غیر اینصورت، از روی صفحه حذف میشود.

سپس یک کلاس جدید ساخته و با ارث بری از ActionFilterAttribute، کار ساخت فیلتر را آغاز میکنیم:

    public class AuthorizePage: ActionFilterAttribute
    {
        private HtmlTextWriter _htmlTextWriter;
        private StringWriter _stringWriter;
        private StringBuilder _stringBuilder;
        private HttpWriter _output;
        IAuthorization _auth;
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {        
            _stringBuilder = new StringBuilder();
            _stringWriter = new StringWriter(_stringBuilder);
            _htmlTextWriter = new HtmlTextWriter(_stringWriter);
            _output = (HttpWriter) filterContext.RequestContext.HttpContext.Response.Output;
            filterContext.RequestContext.HttpContext.Response.Output = _htmlTextWriter;
            _auth = new Auth();
        }

        public override void OnResultExecuted(ResultExecutedContext filterContext)
        {
            var response = _stringBuilder.ToString();

            response = AuthorizeTags(response);
            _output.Write(response);
        }

        public string AuthorizeTags(string response)
        {

            var doc = GetHtmlDocument(response);
            var nodes=doc.DocumentNode.SelectNodes("//*[@data-perm]");

            if (nodes == null)
                return response;

            foreach(var node in nodes)
            {
                var dataPermission = node.Attributes["data-perm"];
                if(!dataPermission.Value.TryBooleanParse())
                {
                    continue;
                }

                var controller = node.Attributes["data-controller"].Value;
                var action = node.Attributes["data-action"].Value;
                var method = node.Attributes["data-method"].Value;

                var access=_auth.Authorize(HttpContext.Current.User.Identity.Name , controller, action, method);

                if (access)
                    continue;

                var removeElm = true;
                var type = node.Attributes["data-type"]?.Value;
                if (type!=null && type.ToLower()== "disable")
                {
                    removeElm = false;
                }

                if(removeElm)
                {
                    node.Remove();
                    continue;
                }


                node.Attributes.Add("disabled", "true");
            }

            return doc.DocumentNode.OuterHtml;
        }

        private HtmlDocument GetHtmlDocument(string htmlContent)
        {
            var doc = new HtmlDocument
            {
                OptionOutputAsXml = true,
                OptionDefaultStreamEncoding = Encoding.UTF8
            };
            doc.LoadHtml(htmlContent);

            return doc;
        }

    }
در خطوط اولیه، از متد OnActionExecuting به عنوان یک سازنده برای پر کردن در لحظه هر درخواست استفاده میکنیم. htmlTextWriter که یک پیاده سازی از TextWriter هست، برای نوشتن بهینه کدهای HTML سودمند میباشد و از آن جهت که نیاز زیادی برای کار با رشته‌ها دارد، از StringBuilder در پشت صحنه استفاده می‌کند. جهت آشنایی بیشتر با این کلاس‌ها، مطلب StringBuilder و قسمت نظراتش را مطالعه بفرمایید. همچنین یک httpWriter هم برای ایجاد خروجی در اکشن مورد نظر نیازمندیم و مورد آخر یک اینترفیس میباشد که جهت اعتبارسنجی مورد استفاده قرار میگیرد و این اینترفیس میتواند از طریق تزریق وابستگی‌ها پر شود:
  public interface IAuthorization
    {
        bool Authorize(string userId, string controller, string action, string method);
    }

ادامه کد در متد OnResultExecuted قرار دارد و متد اصلی کار ما میباشد. این متد بعد از صدور خروجی از اکشن، صدا زده شده اجرا میشود و شامل خروجی اکشن میباشد. خروجی اکشن را به متدی به نام AuthorizeResponse داده و با استفاده از بسته htmlagilitypack که یک HTML Parser میباشد، کدهای HTML را تحلیل میکنیم. قاعده فیلترسازی المان‌ها در این کتابخانه بر اساس قواعد تعریف شده در XPath میباشد. بر اساس این قاعده ما گفتیم هر نوع تگی که دارای ویژگی data-perm میباشد، باید به عنوان گره‌های فیلتر شده برگشت داده شود. سپس مقادیر نام کنترلر و اکشن و ... از المان دریافت شده و با استفاده از اینترفیسی که ما اینجا تعریف کرده‌ایم، بررسی میکنیم که آیا این کاربر به این موارد دسترسی دارد یا خیر. در صورتیکه پاسخ برگشتی، از عدم اعتبار کاربر بگوید، گره مورد نظر حذف و یا در صورتیکه ویژگی data-type وجود داشته و مقدارش برابر disable باشد، آن المان غیرفعال خواهد شد. در نهایت کد تولیدی سند را به رشته تبدیل کرده و جایگزین خروجی فعلی میکنیم.
جهت تعریف سراسری آن در Global.asax داریم:
protected void Application_Start()
{
  GlobalFilters.Filters.Add(new AuthorizePage());
}
مطالب
مقدمه ای بر Docker، قسمت چهارم
در قسمت قبلی  در مورد ساخت و اجرای یک image درون container صحبت کردیم. اما در سناریو‌های واقعی، عملا سیستم تک بعدی نخواهد بود و حاوی دیتابیس‌های مختلف،  message broker ،Caching server و غیره نیز خواهد بود. بنابراین احتیاج به imageهای مختلفی داریم که راه اندازی شده و با یکدیگر ارتباط مستقیم داشته باشند.
از نسخه‌های اولیه‌ی داکر از مفهومی به نام Linking، برای ایجاد کردن Container‌های مختلفی درون یک شبکه استفاده می‌شد. از آنجائیکه این روش منسوخ شده محسوب گردیده و دیگر استفاده‌ی چندانی نمی‌شود، از آن می‌گذریم.
برای مطالعه‌ی بیشتر راجع به این موضوع، از این لینک  استفاده نمایید. نکته‌ای که باید مورد توجه قرار بگیرد این است که استفاده کردن از container linking بسیار ساده‌است؛ اما باید در نظر داشته باشید که در نسخه‌های آتی docker، به احتمال خیلی زیاد این ویژگی حذف خواهد شد و استفاده‌ی از آن توصیه نمی‌شود.
اما روش دیگری که معرفی شده است، Container networks میباشد که با ساخت یک شبکه‌ی مصنوعی و اجرا کردن imageهای مختلفی درون شبکه‌ی بخصوص آن، محقق می‌شود و containerهای مختلف به راحتی می‌توانند با همدیگر ارتباط برقرار نمایند.
Docker Host این قابلیت را می‌دهد تا containerهای مختلف بتوانند توسط شبکه‌ای ایزوله، با هم در ارتباط باشند. طبق شکل زیر می‌توان شبکه‌های ایزوله‌ی متفاوتی را که درونشان containerهای مختلفی هستند، ایجاد نمود.


برای ایجاد یک Container network ابتدا نیاز است یک Custom network bridge را ساخته و container‌های مختلفی را دورن آن اجرا کنیم.

بنابراین برای ایجاد یک شبکه، از دستور زیر استفاده میکنیم:

docker network create --driver bridge isolated-network

isolated-network نام این شبکه است و می‌توان به دلخواه آن را تغییر داد که در اینجا از نوع bridge در نظر گرفته شده‌است.

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

docker network ls

حال میخواهیم containerهای خود را درون این شبکه اجرا نماییم؛ بطور مثال تصویر mongo را در نظر بگیرید. نیاز داریم container آن درون این شبکه‌ی از نوع bridge اجرا شده و بقیه‌ی containerهای درون این شبکه نیز بدان دسترسی داشته باشند.

دستور زیر را برای اجرا کردن container درون شبکه‌ای که ساخته‌ایم، فراخوانی می‌کنیم:

docker run -d --net=isolated-network --name mongodb mongo

با استفاده از نام mongodb که به این container داده‌ایم، توسط بقیه‌ی containerهای درون این شبکه، قابل دسترسی است.

برای کامل شدن این مثال کافی‌است به ادامه‌ی imageی که در مقاله‌ی قسمت قبلی انجام دادیم رفته و تغییراتی را در آن اعمال نماییم. برنامه‌ی nodejsی را که دفعه‌ی قبل نوشتیم، اینگونه بازنویسی می‌نماییم:

const express = require('express')
const mongoClient = require('mongodb').MongoClient;
const app = express()

const PORT = 3000;

app.get('/', async (req, res) => {
  let db = null;
  try {
    db = await mongoClient.connect("mongodb://mongodb/");
    let database = db.db("myDb");
    await database.collection('users').insertOne({ firstName: 'Ali', lastName: 'Kh' });
  }
  catch (e) { }
  finally {
    if(db) db.close();
    res.send('Hello World')
  }
});


app.listen(PORT, () => {
  console.log(`listening on port ${PORT}!`)
})

تغییرات صورت گرفته فقط اضافه شدن پکیج mongodb است و بقیه‌ی کد‌ها هم منطق وصل شدن به دیتابیس و اضافه کردن رکوردی به یک مجموعه‌ی خاص میباشد که به صورت ساده‌ای در نظر گرفته شده است.

نکته: اگر از nodejs ورژن نسخه‌های پایین‌تر استفاده می‌کنید، ممکن است برای نوشتن async/await به خطا برخورد کنید. قاعدتا راه حلش یا بروزرسانی است و یا تغییر دادن کد‌های فوق از async/await به callback معمولی جاوااسکریپت.

نکته: همانطور که می‌بینید در صورت عادی، برای وصل شدن به دیتابیس mongo بطور مثال از localhost:27017 استفاده میکردیم، اما از آنجایی که برنامه‌ی ما درون یک container و شبکه‌ای که ایجاد کردیم، قرار است اجرا شود، توانستیم با استفاده از نام containerی که برای mongo ایجاد کردیم، بدان وصل شویم.

حال کافی‌است dockerfile قبلی را هم اضافه کرده و تصویر خود را build نماییم و بعد از آن هم با استفاده از دستور زیر تصویر خود را بر روی container اجرا کنیم:

دستور build

docker build -f dockerfile -t alikhll/nodemongo .

دستور اجرا

docker run -d --net=isolated-network -p 3000:3000 alikhll/nodemongo

همانطور که میبینید containerهای مختلفی را در یک شبکه اجرا کرده‌ایم و قابلیت فراخوانی آنها فراهم هست. اما شاید استفاده‌ی از این روش برای تست مناسب باشد، ولی اگر قرار باشد روند توسعه نرم‌افزار با استفاده از این روش انجام شود، سختی و تکرار‌ها در فرآیند این کار شاید زیاد‌تر از حد بوده و روند توسعه را کاهش دهد. برای حل این مشکل از ترکیب containerها در کنار هم استفاده میکنیم و مطالب بیشتری در قسمت بعدی آورده خواهد شد.

مطالب
طراحی گردش کاری با استفاده از State machines - قسمت سوم
در این قسمت، یک سری مثال گردش کاری سازگار با Stateless Designer را بررسی خواهیم کرد. خروجی‌های XML زیر را می‌توانید در Stateless Designer وارد کرده و تبدیل به کدهای معادل کنید. اگر نمونه‌ای را هم خودتان طراحی کرده‌اید می‌توانید در قسمت نظرات مطلب جاری به اشتراک بگذارید.


الف) طراحی گردش کاری یک سیستم ردیابی خطاها (Bug tracking system)

در ادامه رویدادها، حالات و انتقالات یک ماشین حالت ردیابی خطاها را مشاهده می‌کنید:

<statemachine xmlns="http://statelessdesigner.codeplex.com/Schema">
  <settings>
    <itemname>BugTrackingStateMachine</itemname>
    <namespace>StatelessTests</namespace>
    <class>public</class>
  </settings>
  <triggers>     
    <trigger>Assign</trigger>
    <trigger>Defer</trigger>
    <trigger>Resolve</trigger>
    <trigger>Close</trigger>
  </triggers>
  <states>     
    <state start="yes">Open</state>
    <state>Assigned</state>
    <state>Deferred</state>
    <state>Resolved</state>
    <state>Closed</state>
  </states>
  <transitions>
    <transition trigger="Assign" from="Open" to="Assigned" />
    <transition trigger="Assign" from="Assigned" to="Assigned" />
    <transition trigger="Close" from="Assigned" to="Closed" />
    <transition trigger="Defer" from="Assigned" to="Deferred" />
    <transition trigger="Assign" from="Deferred" to="Assigned" />
    <transition trigger="Resolve" from="Assigned" to="Resolved" />
  </transitions>
</statemachine>
با گرافی معادل:


توضیحات:
یک گزارش خطا حداقل پنج حالت آغاز (Open)، انتساب به شخص، جهت رفع مشکل (Assign)، به تاخیر افتادن/درحال بررسی (Deffered)، برطرف شده (Resolved) و خاتمه یافته/برطرف نخواهد شد (Closed) را می‌تواند داشته باشد.
برای حرکت (Transition) از هر حالت به حالتی دیگر نیاز به یک سری رویداد (Trigger) است که لیست آن‌ها را در بالا مشاهده می‌کنید.
در ابتدا سیستم در حالت انتساب به شخص قرار می‌گیرد. سپس در همین حالت شخص می‌تواند یکی از سه حالت رفع شده، بستن موضوع و یا ارجاع به زمانی دیگر را انتخاب کند. حتی در حالت ارجاع به شخص، شخص می‌تواند مساله را به شخصی دیگر ارجاع دهد. یا در حالت به تاخیر افتادن حل مساله، می‌توان مشکل را به شخصی دیگر انتساب داد.


ب) طراحی گردش کاری درخواست ارتقاء در یک شرکت

مراحل درخواست ارتقاء شغلی را در یک سازمان فرضی، در ذیل مشاهده می‌کنید:
<statemachine xmlns="http://statelessdesigner.codeplex.com/Schema">
  <settings>
    <itemname>RequestPromotionStateMachine</itemname>
    <namespace>StatelessTests</namespace>
    <class>public</class>
  </settings>
  <triggers>     
    <trigger>Complete</trigger>
    <trigger>RequestInfo</trigger>
    <trigger>Deny</trigger>
    <trigger>Approve</trigger>
    <trigger>ManagerJustify</trigger>
  </triggers>
  <states>     
    <state start="yes">RequestPromotionForm</state>
    <state>ManagerReview</state>
    <state>PromotionDenied</state>
    <state>VicePresidentApprove</state>
    <state>Promoted</state>
  </states>
  <transitions>
    <transition trigger="Complete" from="RequestPromotionForm" to="ManagerReview" />

    <transition trigger="RequestInfo" from="ManagerReview" to="RequestPromotionForm" />
    <transition trigger="Deny" from="ManagerReview" to="PromotionDenied" />
    <transition trigger="Approve" from="ManagerReview" to="VicePresidentApprove" />

    <transition trigger="ManagerJustify" from="VicePresidentApprove" to="ManagerReview" />
    <transition trigger="Deny" from="VicePresidentApprove" to="PromotionDenied" />
    <transition trigger="Approve" from="VicePresidentApprove" to="Promoted" />
  </transitions>
</statemachine>
با گرافی معادل:


توضیحات:
کارمند فرم درخواست ارتقاء را تکمیل می‌کند. این فرم به مسئول او ارسال می‌شود. مسئول می‌تواند درخواست را یک ضرب رد کند؛ یا تائید کند که سپس برای مدیرعامل شرکت ارسال می‌شود و یا مجددا به شخص برای تکمیل نواقص فرم ارجاع دهد. مدیرعامل شرکت می‌تواند درخواست را تائید کند که در اینجا کار خاتمه می‌یابد و شخص ارتقاء خواهد یافت. یا می‌تواند درخواست را رد کند و یا برای بررسی بیشتر مجددا به مسئول شخص ارجاع دهد.


تمرین! توضیحات زیر را تبدیل به یک State machine کنید!

چند سال قبل به اداره‌ی بیمه تامین اجتماعی منطقه مراجعه کردم. جهت دریافت ریز سوابق و انتقال آن‌ها به این مرکز ابتدا یک برگه دریافت شد. پر شد، بعد به صورت دستی (توسط بنده) به یک نفر دیگر ارجاع شد تا امضاء کند. سپس به صورت دستی به مسئول قسمت ارجاع شد تا امضاء کند. مجددا به صورت دستی به مدیر کل مجموعه ارجاع شد تا امضاء کند. سپس به صورت دستی به دبیرخانه برای پیگیری ارجاع شد. قرار است ظرف یک ماه تا 45 روز این سوابق از یک واحد دیگر به این واحد منتقل شوند!
بعد از 45 روز:
مراجعه به دبیرخانه: دریافت شماره پرونده رسیده.
گفته شد که به قسمت دریافت شماره مراجعه کنید. مراجعه شد، گفتند برو پرونده‌ات را بگیر بیار. رفتم زیر زمین، گفت که ما اینطوری پرونده نمی‌دیم! برو فرمش رو هم پر کن بیار. مراجعه شد به کارمند مربوطه، ایشان پس از مشورت با سایر همکاران به این نتیجه رسیدند که در این مرحله نیازی به مراجعه به زیر زمین نبوده! و باید به قسمت ثبت نام مجدد مراجعه کنید! چشم!  
اینجا هم مجددا فرم پر شد،‌ارجاع داده شده به معاون قسمت، امضاء کرد گفت برو دبیرخانه شماره بگیر. شماره گرفته شده بعد مجددا به همان قسمت ثبت نام مراجعه کردم، گفتند برو پرونده‌ات را از زیر زمین بگیر بیار! بعد از آوردن پرونده، ارجاع شد به صورت دستی به یک قسمت دیگر که سوابق وارد سیستم شود (هنوز نشده بود!). بعد از ثبت (نیم ساعت یا بیشتر ...)، مجددا به همان قسمت ثبت نام مراجعه کردم، گفت حالا برو یک شماره بگیر بیار. شماره گرفته شد از قسمتی دیگر و مراجعه مجدد به قسمت ثبت نام، یک نامه دیگر تهیه کرد، به سه نفر دیگر + دبیرخانه برای امضاء و شماره گرفتن ارجاع داده شد. اینجا تمام شد. بعد این موارد ارجاع شد به قسمت دیگری از شهر برای دریافت قبض پرداخت بیمه.
مطلب مرتبط
بازخوردهای دوره
افزونه‌ای برای کپسوله سازی نکات ارسال یک فرم ASP.NET MVC به سرور توسط jQuery Ajax
- Ajax.BeginForm هم در پشت صحنه از jQuery Ajax استفاده می‌کنه. فقط پیاده سازی نهائیش از شما مخفی شده.
- در نمونه بحث جاری کنترل بیشتری بر روی رویدادها خواهید داشت. مثلا قسمت xhr.status == 403 و هدایت کاربر به صفحه لاگین در صورت منقضی شدن اعتبارسنجی آن.
- Ajax.BeginForm برای کار کردن حتما نیاز به submit button داره. در مطلب جاری از یک span هم می‌تونید استفاده کنید و مشکلی نداره.
- در Ajax.BeginForm آنچنان کنترلی بر روی پردازش نهایی خروجی اکشن متد ندارید. مثلا در اینجا عنوان شد که اگر خروجی JSON بود و اگر دارای فیلد مشخصی با مقدار مشخصی بود نیاز است کار خاصی انجام شود. در حالت jQuery Ajax مستقیم، پردازش JSON ساده‌تر است.
نظرات مطالب
ایجاد alert,confirm,prompt هایی متفاوت با jQuery Impromptu
ممنون،
یه سوال بنده یه فرم ثبت نام دارم که با کلیک بر روی Botton اطلاعات در دیتابیس درج می‌شود حالا می‌خوام بعد از ثبت اطلاعات یه alert به کاربر نشون بده که اطلاعات با موفقیت ثبت شد ولی مشکل اینجاست که وقتی کاربر روی Button کلیک میکنه چون صفحه PostBack میشه دیگه پیغام به کاربر نمایش داده نمیشه،اگه امکان داره بنده رو راهنمایی بفرمائید.
دقیقا مثل همین قسمت ارسال نظر سایت.
نظرات مطالب
تبدیل فایل‌های pfx به snk
مطلب خوبی بود وحید جان. ممنونم.
البته من بعد از اینکه مطلب شما رو خوندم و متوجه شدم که دو نوع فایل pfx و snk هست که با اون میشه sign کرد اندکی تو اینترنت گشتم و  متوجه یک نکته شدم که گفتم بد نیست اینجا مطرح کنم.
هر چند مطلب شما درباره تبدیل فایل pfx به snk است اما متنی که نوشتید این موضوع را القا میکند که نمیشود به سادگی این فایل رو ساخت.
به هر روی، میتوان فایل snk را از طریق فایل زبانه signing در خواص پروژه ساخت. برای این کار کافیست که گزینه Protect my key file with a password  را آنتیک کرد و در این حالت به جای اینکه فایل pfx ساخته شود فایل snk ساخته میشود.
مطلب دیگر اینکه من پروژه‌های متن باز دیگری را دیده ام که الان حضور ذهن ندارم بگم(احتمالاً یکیشون RavenDB بود) که از طریق خواص پروژه ویژوال استودیو کار signing را انجام نمیدهند یعنی در آنجا گزینه sign کردن را انتخاب نکرده اند. چون فایل snk را اگر منتشر کنیم همه میتونند با اون اسمبلی‌ها را sign کنند و معنای strong name بودن اسمبلی به طور کلی میره زیر سوال. در عوض از یک customized build استفاده میکنند که فقط توسط خودشون(مالکان پروژه) قابل فراخوانی است و توسط اون اسمبلی‌های release را میسازند. البته در اینباره باید بیشتر بررسی کنم و شاید دقیقاً ماجرا 100 درصد به این شکل که گفتم نیست.
مطالب دوره‌ها
نحوه برقراری ارتباطات بین صفحات، سیستم راهبری و ViewModelها در قالب پروژه WPF Framework
هدف از قالب پروژه WPF Framework ایجاد یک پایه، برای شروع سریع یک برنامه تجاری WPF جدید است. بنابراین فرض کنید که این قالب، هم اکنون در اختیار شما است و قصد دارید یک صفحه جدید، مثلا تغییر مشخصات کاربری را به آن اضافه کنید. کدهای کامل این قابلیت هم اکنون در قالب پروژه موجود است و به این ترتیب توضیح جزئیات روابط آن در اینجا ساده‌تر خواهد بود.

1) ایجاد صفحه تغییر مشخصات کاربر
کلیه Viewهای برنامه، در پروژه ریشه، ذیل پوشه Views اضافه خواهند شد. همچنین چون در آینده تعداد این فایل‌ها افزایش خواهند یافت، بهتر است جهت مدیریت آن‌ها، به ازای هر گروه از قابلیت‌ها، یک پوشه جدید را ذیل پوشه Views اضافه کرد.


همانطور که ملاحظه می‌کنید در اینجا پوشه UserInfo به همراه یک فایل جدید XAML به نام ChangeProfile.xaml، ذیل پوشه Views پروژه ریشه اصلی اضافه شده‌اند.
ChangeProfile.xaml از نوع Page است؛ از این جهت که اگر به فایل MainWindow.xaml که سیستم راهبری برنامه در آن تعبیه شده است مراجعه کنید، یک چنین تعریفی را ملاحظه خواهید نمود:
<CustomControls:FrameFactory
                    x:Name="ActiveScreen"            
                    HorizontalContentAlignment="Stretch"
                    VerticalContentAlignment="Stretch"     
                    NavigationUIVisibility="Hidden"            
                    Grid.Column="1" 
                    Margin="0" />
سورس کامل کنترل سفارشی FrameFactory.cs را در پروژه Infrastructure برنامه می‌توانید مشاهده کنید. FrameFactory در حقیقت یک کنترل Frame استاندارد است که مباحث تزریق وابستگی‌ها و همچنین راهبری خودکار سیستم در آن تعریف شده‌اند.
مرحله بعد، تعریف محتویات فایل ChangeProfile.xaml است. در این فایل اطلاعات انقیاد داده‌ها از ViewModel مرتبط که در ادامه توضیح داده خواهد شد دریافت می‌گردد. مثلا مقدار خاصیت ChangeProfileData.Password، از ViewModel به صورت خودکار تغذیه خواهد شد.
در این فایل یک سری DynamicResource را هم برای تعریف دکمه‌های سبک مترو ملاحظه می‌کنید. کلیدهای متناظر با آن در فایل Icons.xaml که در فایل App.xaml برای کل برنامه ثبت شده است، تامین می‌گردد.


2) تنظیم اعتبارسنجی صفحه اضافه شده
پس از اینکه صفحه جدید اضافه شد، نیاز است وضعیت دسترسی به آن مشخص شود:
/// <summary>
/// تغییر مشخصات کاربر جاری
/// </summary>
[PageAuthorization(AuthorizationType.FreeForAuthenticatedUsers)]
public partial class ChangeProfile
برای این منظور به فایل code behind این صفحه یعنی ChangeProfile.xaml.cs مراجعه و تنها، ویژگی فوق را به آن اضافه خواهیم کرد. ویژگی PageAuthorization به صورت خودکار توسط فریم ورک تهیه شده خوانده و اعمال خواهد شد. برای نمونه در اینجا کلیه کاربران اعتبارسنجی شده در سیستم می‌توانند مشخصات کاربری خود را تغییر دهند.
در مورد نحوه تعیین نقش‌های متفاوت در صورت نیاز، در قسمت قبل بحث گردید.


3) تغییر منوی برنامه جهت اشاره به صفحه جدید
خوب، ما تا اینجا یک صفحه جدید را تهیه کرده‌ایم. در مرحله بعد باید مدخلی را در منوی برنامه جهت اشاره به آن تهیه کنیم.
منوی برنامه در فایل MainMenu.xaml قرار دارد. اطلاعات متناظر با دکمه ورود به صفحه تغییر مشخصات کاربری نیز به شکل ذیل تعریف شده است:
                <Button Style="{DynamicResource MetroCircleButtonStyle}"
                        Height="55" Width="55"  
                        Command="{Binding DoNavigate}"
                        CommandParameter="\Views\UserInfo\ChangeProfile.xaml"
                        Margin="2">
                    <Rectangle Width="28" Height="17.25">
                        <Rectangle.Fill>
                            <VisualBrush Stretch="Fill" Visual="{StaticResource appbar_user_tie}" />
                        </Rectangle.Fill>
                    </Rectangle>
                </Button>
به ازای هر صفحه جدیدی که تعریف می‌شود تنها کافی است CommandParameter ایی مساوی مسیر فایل XAML مورد نظر، در فایل منوی برنامه قید شود. منوی اصلی دارای ViewModel ایی است به نام MainMenuViewModel.cs که در پروژه Infrastructure پیشتر تهیه شده است.
در این ViewModel تعاریف DoNavigate و پردازش پارامتر دریافتی به صورت خودکار صورت خواهد گرفت و سورس کامل آن در اختیار شما است. بنابراین تنها کافی است CommandParameter را مشخص کنید، DoNavigate کار هدایت به آن‌را انجام خواهد داد.


4) ایجاد ViewModel متناظر با صفحه
مرحله آخر افزودن یک صفحه، تعیین ViewModel متناظر با آن است. عنوان شد که اطلاعات مورد نیاز جهت عملیات Binding در این فایل قرار می‌گیرند و اگر به فایل ChangeProfileViewModel.cs مراجعه کنید (نام آن مطابق قرارداد، یک کلمه ViewModel را نسبت به نام View متناظر بیشتر دارد)، چنین خاصیت عمومی را در آن خواهید یافت.


مطابق قراردادهای توکار قالب تهیه شده:
- نیاز است ViewModel تعریف شده از کلاس پایه BaseViewModel مشتق شود تا علاوه بر تامین یک سری کدهای تکراری مانند:
 public abstract class BaseViewModel : DataErrorInfoBase, INotifyPropertyChanged, IViewModel
سبب شناسایی این کلاس به عنوان ViewModel و برقرار تزریق وابستگی‌های خودکار در سازنده آن نیز گردد.
- پس از اضافه شدن کلاس پایه BaseViewModel نیاز است تکلیف خاصیت public override bool ViewModelContextHasChanges را نیز مشخص کنید. در اینجا به سیستم راهبری اعلام می‌کنید که آیا در ViewModel جاری تغییرات ذخیره نشده‌ای وجود دارند؟ فقط باید true یا false را بازگردانید. برای مثال خاصیت uow.ContextHasChanges برای این منظور بسیار مناسب است و از طریق پیاده سازی الگوی واحد کار به صورت خودکار چنین اطلاعاتی را در اختیار برنامه قرار می‌دهد.

در ViewModelها هرجایی که نیاز به اطلاعات کاربر وارد شده به سیستم داشتید، از اینترفیس IAppContextService در سازنده کلاس ViewModel جاری استفاده کنید. اینترفیس IUnitOfWork امکانات ذخیره سازی اطلاعات و همچنین مشخص سازی وضعیت Context جاری را در اختیار شما قرار می‌دهد.
کلیه کدهای کار کردن با یک موجودیت باید در کلاس سرویس متناظر با آن قرار گیرند و این کلاس سرویس توسط اینترفیس آن مانند IUsersService در اینجا باید توسط سازنده کلاس در اختیار ViewModel قرار گیرد.
تزریق وابستگی‌ها در اینجا خودکار بوده و تنظیمات آن در فایل IocConfig.cs پروژه Infrastructure قرار دارد. این کلاس آنچنان نیازی به تغییر ندارد؛ اگر پیش فرض‌های نامگذاری آن‌را مانند کلاس‌های Test و اینترفیس‌های ITest، در لایه سرویس برنامه رعایت شوند.
مطالب
ساخت یک سایت ساده‌‌ی نمایش لیست فیلم با استفاده از Vue.js - قسمت اول
Vue.js  یکی از محبوب ترین فریم ورک‌های  SPA است و سایت جاری نیز دارای مقالات خوبی درباره‌ی Vue.js می‌باشد. قصد داریم طی چند مقاله با استفاده از Vue.js و چندین پلاگین مطرح آن، یک سایت ساده‌ی نمایش فیلم را ایجاد کنیم. ابتدا Node.js  را بر روی سیستم خود نصب کنید (پیشنهاد ما نسخه‌ی LTS می‌باشد). مراحل نصب آن ساده است و بصورت Nextهایی پی در پی می‌باشد؛ بصورت پیش فرض npm نیز همراه آن نصب میشود. سپس دو دستور زیر را جهت صحت انجام مراحل نصب، تست نمایید.

در این مقاله با ادیتور VS Code کار میکنیم. بعد از نصب آن، از منوی Terminal، گزینه‌ی New Terminal را کلیک کنید تا پنجره‌ی PowerShell نمایش داده شود؛ برای سرعت و دقت بیشتر در برنامه‌های  vue.js ای. با دستور زیر vue cli را  نصب میکنیم  (فقط یک مرتبه و برای برنامه‌های بعدی vue.jsای، نیازی به اجرای این دستور نداریم):

npm install -g @vue/cli

جهت راه اندازی یک برنامه‌ی پیش فرض Vue.js ای، کافیست دستور زیر را اجرا نماییم تا پکیج‌های مورد نیاز، به همراه کانفیگ اولیه (Zero config) برای ما ایجاد شوند:

vue create movie-app

بعد از ایجاد برنامه در vs code، از طریق منوی File، گزینه Open Folder را کلیک کرده و پوشه برنامه‌ای را که ایجاد کردیم، Select Folder میکنیم. ساختار اولیه‌ی برنامه‌ی ایجاد شده، به شکل زیر می‌باشد:

نیازمندیهای مثال جاری

A) برای گرفتن اطلاعات مورد نمایش در مثال جاری، از سایت omdbapi.com استفاده میکنیم که با دریافت یک api key آن بصورت رایگان، میتوانیم web serviceهای آن را Call نماییم.

B) از  vuetify برای ui استفاده میکنیم که بصورت Material Design و دارای کامپوننت‌های غنی می‌باشد؛ ضمن اینکه RTL را هم پشتیبانی میکند.

برای نصب آن در Terminal دستور زیر را اجرا میکنیم:

vue add vuetify

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

npm run serve

بعد از اجرای دستور فوق، روی گزینه زیر ctrl+click میکنیم تا نتیجه کار در مرورگر قابل رویت باشد:

نمایش صفحه زیر نشان دهنده‌ی درستی انجام کار تا اینجا است:


نکته: جهت استفاده از امکان RTL کافیست در فایل vuetify.js واقع در پوشه‌ی plugins، تغییرات زیر را انجام دهیم. در مثال جاری بدلیل اینکه اطلاعات انگلیسی می‌باشند، از نسخه LTR آن استفاده میکنیم؛ هر چند یکسری api فارسی نیز موجود می‌باشد که میتوان از آنها استفاده نمود.

import Vue from 'vue'
import Vuetify from 'vuetify/lib'
import 'vuetify/src/stylus/app.styl'

Vue.use(Vuetify, {
  iconfont: 'md',
  rtl: true
})


C) نصب  vue-router : جهت انجام routeهای تودرتو ، مپ کردن کامپوننت ها با آدرسی مشخص، کار با پارامتر و  HTML5 History API  مورد استفاده قرار میگیرد. برای نصب آن، دستور زیر را اجرا میکنیم:

npm install vue-router

برای نوشتن routeهای مورد نیاز، یک فولدر را با نام router، در پوشه src برنامه ایجاد میکنیم و یک فایل جاوا اسکریپتی را در آن با نام index.js، میسازیم (این ساختار برای مدیریت بهتر پروژه می‌باشد):

درون فایل  index.js، محتویات زیر را طبق مستندات آن قرار میدهیم:

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

جهت استفاده از این router، نیاز است تا در نمونه‌ی وهله سازی شده‌ی vue برنامه بکار گرفته شود. فایل  main.js  را باز کنید و خط زیر را در قسمت بالای برنامه وارد کنید:

import router from './router'

اکنون محتویات فایل  main.js بشکل زیر می‌باشد:

import Vue from 'vue'
import './plugins/vuetify'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
  router
}).$mount('#app')


D) نصب axios : برای انجام  درخواستهای  HTTP  و عملیات ا‌ی‌جکس در vue.js  ترجیحا بهتر است از axios که یک کتابخانه‌ی محبوب می‌باشد و کار با آن ساده است، استفاده شود. برای نصب آن، دستور زیر را اجرا میکنیم:

npm install axios


E) نصب vuex : کتابخانه‌ای جهت مدیریت حالت (state management) برای  vue.js میباشد و مشابه آن Flux و Redux برای React می‌باشند. برای  نصب، دستور زیر را اجرا میکنیم:

npm install vuex


برای بکارگیری آن یک فولدر را با نام store در پوشه‌ی src برنامه ایجاد میکنیم و یک فایل جاوا اسکریپتی را در آن با نام index.js میسازیم (این ساختار برای مدیریت بهتر پروژه می‌باشد). درون فایل  index.js، محتویات زیر را طبق مستندات آن و ^ قرار میدهیم. 

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export const store = new Vuex.Store()

برای استفاده و کانفیگ آن، محتویات فایل  main.js را بشکل زیر تغییر دهید:

import Vue from 'vue'
import './plugins/vuetify'
import App from './App.vue'
import router from './router'
import {store} from './store'

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
  store,
  router
}).$mount('#app')



دریافت کد قسمت اول 

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

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


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

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


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


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

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

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

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

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

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

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

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

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

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

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

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

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


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

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


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

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


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

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

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

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

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

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

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

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

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


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

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

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


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

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

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

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

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

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

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

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


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


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


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



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