نظرات مطالب
EF Code First #1

سلام ممنون از  مطالب مفیدتون

من تازه 1 روزه شروع کردم code first کار کنم

تو یه مقاله  داشتم می‌خوندم  برای تعریف entity ها این association ‌ها رو تو هر دو طرف virtual   تعریف کرده بود

مثلا برای blog این طور نوشته بود

public virtual IList<post> posts { set; get; }

خودم هم این طور نوشتم تا این جا که فرقی ندیدم می‌شه توضیح بدید چه فرقی دارن

باز هم ممنونم

نظرات مطالب
Blazor 5x - قسمت ششم - مبانی Blazor - بخش 3 - چرخه‌های حیات کامپوننت‌ها
یک نکته‌ی تکمیلی: تنظیم پویای عنوان صفحات در برنامه‌های Blazor

برای تنظیم پویای عنوان یک صفحه‌ی وب، نیاز است با DOM API مرورگر به صورت مستقیم کار کرد. برای مثال فایل wwwroot\main.js را که مدخل آن به کامپوننت Host_ و یا صفحه‌ی index.html اضافه می‌شود، به صورت زیر تکمیل می‌کنیم:
window.JsFunctionHelper = {
  blazorSetTitle: function (title) {
    document.title = title;
  }
};
اکنون می‌خواهیم این متد جاوااسکریپتی را که مستقیما با شیء document کار می‌کند، در کامپوننت جدید Client\Shared\PageTitle.razor استفاده کنیم:
@inject IJSRuntime JSRuntime

@code
{
   [Parameter]
   public string Title { get; set; }

   protected override async Task OnParametersSetAsync()
   {
      await JSRuntime.InvokeVoidAsync("JsFunctionHelper.blazorSetTitle", Title);
   }
}
در اینجا کامپوننت جدیدی تعریف شده‌است که به محض تنظیم مقدار پارامتر عنوان آن، سبب فراخوانی متد جاوا اسکریپتی blazorSetTitle می‌شود. برای نمونه روش استفاده‌ی از آن در کامپوننت Counter، جهت نمایش عنوانی پویا، به محض تغییر مقدار شمارشگر، به صورت زیر می‌تواند باشد:
@page "/counter"

<PageTitle Title="@GetPageTitle()" />

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }

    private string GetPageTitle() => $"Counter ({currentCount})";
}
این روش در برنامه‌های Blazor Server کار نخواهد کرد و در حین فراخوانی متد InvokeVoidAsync یک NullReferenceException مشاهده می‌شود؛ چون این نوع برنامه‌های Blazor Server به همراه یک مرحله‌ی pre-render در سمت سرور هستند که ابتدا، کار تهیه‌ی HTML ای را که باید به سمت مرورگر ارسال کنند، به پایان می‌رسانند. در این مرحله خبری از DOM نیست که بتوان به آن دسترسی یافت و تغییری را در آن ایجاد کرد.
برای رفع این مشکل همانطور که در مطلب جاری نیز عنوان شد، باید از روال رویدادگردان OnAfterRenderAsync استفاده کرد. در این حالت کدهای کامپوننت PageTitle.razor به صورت زیر تغییر می‌کنند:
@inject IJSRuntime JSRuntime

@code
{
   [Parameter]
   public string Title { get; set; }

   protected override async Task OnAfterRenderAsync(bool firstRender)
   {
      await JSRuntime.InvokeVoidAsync("JsFunctionHelper.blazorSetTitle", Title);
   }
}
روال رویدادگردان OnAfterRenderAsync پس از اینکه کار بارگذاری و تشکیل کامل DOM در مرورگر انجام شد، فراخوانی می‌شود. به همین جهت دیگر دسترسی به شیء document.title، سبب بروز یک NullReferenceException نخواهد شد.


یک نکته: قرار است در Blazor 6x، کامپوننت‌های جدید Title، Link و Meta جهت تنظیم اطلاعات تگ head صفحه، به صورت استاندارد اضافه شوند:
<Title Value="@title" />
<Meta name="description" content="Modifying the head from a Blazor component." />
<Link href="main.css" rel="stylesheet" />
مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت نهم- مدیریت طول عمر توکن‌ها
توکن‌های صادر شده‌ی توسط IdentityServer به دلایل امنیتی، طول عمر محدودی دارند. بنابراین اولین سؤالی که در اینجا مطرح خواهد شد، این است: «اگر توکنی منقضی شد، چه باید کرد؟» و یا «اگر خواستیم به صورت دستی طول عمر توکنی را پایان دهیم، چه باید کرد؟»


بررسی طول عمر توکن‌ها

اگر مرورگر خود را پس از لاگین به سیستم، برای مدتی به حال خود رها کنید، پس از شروع به کار مجدد، مشاهده خواهید کرد که دیگر نمی‌توانید به API دسترسی پیدا کنید. علت اینجا است که Access token صادر شده، منقضی شده‌است. تمام توکن‌ها، دارای طول عمر مشخصی هستند و پس از سپری شدن این زمان، دیگر اعتبارسنجی نخواهند شد. زمان انقضای توکن، در خاصیت یا claim ویژه‌ای به نام exp ذخیره می‌شود.
در اینجا ما دو نوع توکن را داریم: Identity token و Access token
از Identity token برای ورود به سیستم کلاینت استفاده می‌شود و به صورت پیش‌فرض طول عمر کوتاه آن به 5 دقیقه تنظیم شده‌است. علت کوتاه بودن این زمان این است که این توکن‌ها تنها یکبار مورد استفاده قرار می‌گیرد و پس از ارائه‌ی آن به کلاینت، از طریق آن Claim Identity تولید می‌شود. پس از آن طول عمر Claim Identity تولید شده صرفا به تنظیمات برنامه‌ی کلاینت مرتبط است و می‌تواند از تنظیمات IDP کاملا مجزا باشد؛ مانند پیاده سازی sliding expiration. در این حالت تا زمانیکه کاربر در برنامه فعال است، در حالت logged in باقی خواهد ماند.

Access tokenها متفاوت هستند. طول عمر پیش‌فرض آن‌ها به یک ساعت تنظیم شده‌است و نسبت به Identity token طول عمر بیشتری دارند. پس از اینکه این زمان سپری شد، تنها با داشتن یک Access token جدید است که دسترسی ما مجددا به Web API برقرار خواهد شد. بنابراین در اینجا ممکن است هنوز در برنامه‌ی کلاینت در حالت logged in قرار داشته باشیم، چون هنوز طول عمر Claim Identity آن به پایان نرسیده‌است، اما نتوانیم با قسمت‌های مختلف برنامه کار کنیم، چون نمی‌توانیم از یک Access token منقضی شده جهت دسترسی به منابع محافظت شده‌ی سمت Web API استفاده نمائیم. در اینجا دیگر برنامه‌ی کلاینت هیچ نقشی بر روی تعیین طول عمر یک Access token ندارد و این طول عمر صرفا توسط IDP به تمام کلاینت‌های آن دیکته می‌شود.
در اینجا برای دریافت یک Access token جدید، نیاز به یک Refresh token داریم که صرفا برای «کلاینت‌های محرمانه» که در قسمت سوم این سری آن‌ها را بررسی کردیم، توصیه می‌شود.


چگونه می‌توان زمان انقضای توکن‌ها را صریحا تنظیم کرد؟

برای تنظیم زمان انقضای توکن‌ها، از کلاس src\IDP\DNT.IDP\Config.cs سمت IDP شروع می‌کنیم.
namespace DNT.IDP
{
    public static class Config
    {
        public static IEnumerable<Client> GetClients()
        {
            return new List<Client>
            {
                new Client
                {
                    ClientName = "Image Gallery",
                    // IdentityTokenLifetime = ... // defaults to 300 seconds / 5 minutes
                    // AuthorizationCodeLifetime = ... // defaults to 300 seconds / 5 minutes
                    // AccessTokenLifetime = ... // defaults to 3600 seconds / 1 hour
                }
             };
        }
    }
}
- در اینجا در تنظیمات یک کلاینت جدید، خاصیت IdentityTokenLifetime آن، به طول عمر Identity token تولید شده اشاره می‌کند که مقدار پیش‌فرض آن عدد صحیح 300 ثانیه است یا معادل 5 دقیقه.
- مقدار خاصیت AuthorizationCodeLifetime تنظیمات یک کلاینت، عدد صحیحی است با مقدار پیش‌فرض 300 ثانیه یا معادل 5 دقیقه که طول عمر AuthorizationCode را تعیین می‌کند. این مورد، طول عمر توکن خاصی نیست و در حین فراخوانی Token Endpoint مبادله می‌شود و در طی Hybrid flow رخ می‌دهد. بنابراین مقدار پیش‌فرض آن بسیار مناسب بوده و نیازی به تغییر آن نیست.
- مقدار خاصیت AccessTokenLifetime تنظیمات یک کلاینت، عدد صحیحی است با مقدار پیش‌فرض 3600 ثانیه و یا معادل 1 ساعت و طول عمر Access token تولید شده‌ی توسط این IDP را مشخص می‌کند.
البته باید درنظر داشت اگر طول عمر این توکن دسترسی را برای مثال به 120 یا 2 دقیقه تنظیم کنید، پس از سپری شدن این 2 دقیقه ... هنوز هم برنامه‌ی کلاینت قادر است به Web API دسترسی داشته باشد. علت آن وجود بازه‌ی 5 دقیقه‌ای است که در طی آن، انجام این عملیات مجاز شمرده می‌شود و برای کلاینت‌هایی درنظر گرفته شده‌است که ساعت سیستم آن‌ها ممکن است اندکی با ساعت سرور IDP تفاوت داشته باشند.


درخواست تولید یک Access Token جدید با استفاده از Refresh Tokens

زمانیکه توکنی منقضی می‌شود، کاربر باید مجددا به سیستم لاگین کند تا توکن جدیدی برای او صادر گردد. برای بهبود این تجربه‌ی کاربری، می‌توان در کلاینت‌های محرمانه با استفاده از Refresh token، در پشت صحنه عملیات دریافت توکن جدید را انجام داد و در این حالت دیگر کاربر نیازی به لاگین مجدد ندارد. در این حالت برنامه‌ی کلاینت یک درخواست از نوع POST را به سمت IDP ارسال می‌کند. در این حالت عملیات Client Authentication نیز صورت می‌گیرد. یعنی باید مشخصات کامل کلاینت را به سمت IDP ارسال کرد. در اینجا اطلاعات هویت کلاینت در هدر درخواست و Refresh token در بدنه‌ی درخواست به سمت سرور IDP ارسال خواهند شد. پس از آن IDP اطلاعات رسیده را تعیین اعتبار کرده و در صورت موفقیت آمیز بودن عملیات، یک Access token جدید را به همراه Identity token و همچنین یک Refresh token جدید دیگر، صادر می‌کند.
برای صدور مجوز درخواست یک Refresh token، نیاز است scope جدیدی را به نام offline_access معرفی کنیم. به این معنا که امکان دسترسی به برنامه حتی در زمانیکه offline است، وجود داشته باشد. بنابراین offline در اینجا به معنای عدم لاگین بودن شخص در سطح IDP است.
بنابراین اولین قدم پیاده سازی کار با Refresh token، مراجعه‌ی به کلاس src\IDP\DNT.IDP\Config.cs و افزودن خاصیت AllowOfflineAccess با مقدار true به خواص یک کلاینت است:
namespace DNT.IDP
{
    public static class Config
    {
        public static IEnumerable<Client> GetClients()
        {
            return new List<Client>
            {
                new Client
                {
                    ClientName = "Image Gallery",
                    // IdentityTokenLifetime = ... // defaults to 300 seconds / 5 minutes
                    // AuthorizationCodeLifetime = ... // defaults to 300 seconds / 5 minutes
                    // AccessTokenLifetime = ... // defaults to 3600 seconds / 1 hour
                    AllowOfflineAccess = true,
                    // AbsoluteRefreshTokenLifetime = ... // Defaults to 2592000 seconds / 30 days
                    // RefreshTokenExpiration = TokenExpiration.Sliding
                    UpdateAccessTokenClaimsOnRefresh = true,
                    // ...
                }
             };
        }
    }
}
- در اینجا می‌توان خاصیت AbsoluteRefreshTokenLifetime را که بیانگر طول عمر Refresh token است، تنظیم کرد. مقدار پیش‌فرض آن 2592000  ثانیه و یا معادل 30 روز است.
- البته RefreshToken ضرورتی ندارد که طول عمر Absolute و یا کاملا تعیین شده‌ای را داشته باشد. این رفتار را توسط خاصیت RefreshTokenExpiration می‌توان به TokenExpiration.Sliding نیز تنظیم کرد. البته حالت پیش‌فرض آن بسیار مناسب است.
- در اینجا می‌توان خاصیت UpdateAccessTokenClaimsOnRefresh را نیز به true تنظیم کرد. فرض کنید یکی از Claims کاربر مانند آدرس او تغییر کرده‌است. به صورت پیش‌فرض با درخواست مجدد توکن توسط RefreshToken، این Claims به روز رسانی نمی‌شوند. با تنظیم این خاصیت به true این مشکل برطرف خواهد شد.


پس از تنظیم IDP جهت صدور RefreshToken، اکنون کلاس ImageGallery.MvcClient.WebApp\Startup.cs برنامه‌ی MVC Client را به صورت زیر تکمیل می‌کنیم:
ابتدا در متد تنظیمات AddOpenIdConnect، نیاز است صدور درخواست scope جدید offline_access را صادر کنیم:
options.Scope.Add("offline_access");
همین اندازه تنظیم در سمت برنامه‌ی کلاینت برای دریافت refresh token و ذخیره سازی آن جهت استفاده‌های آتی کفایت می‌کند.

در ادامه نیاز است به سرویس ImageGalleryHttpClient مراجعه کرده و کدهای آن‌را به صورت زیر تغییر داد:
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using IdentityModel.Client;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

namespace ImageGallery.MvcClient.Services
{
    public interface IImageGalleryHttpClient
    {
        Task<HttpClient> GetHttpClientAsync();
    }

    /// <summary>
    /// A typed HttpClient.
    /// </summary>
    public class ImageGalleryHttpClient : IImageGalleryHttpClient
    {
        private readonly HttpClient _httpClient;
        private readonly IConfiguration _configuration;
        private readonly IHttpContextAccessor _httpContextAccessor;
        private readonly ILogger<ImageGalleryHttpClient> _logger;

        public ImageGalleryHttpClient(
            HttpClient httpClient,
            IConfiguration configuration,
            IHttpContextAccessor httpContextAccessor,
            ILogger<ImageGalleryHttpClient> logger)
        {
            _httpClient = httpClient;
            _configuration = configuration;
            _httpContextAccessor = httpContextAccessor;
            _logger = logger;
        }

        public async Task<HttpClient> GetHttpClientAsync()
        {
            var accessToken = string.Empty;

            var currentContext = _httpContextAccessor.HttpContext;
            var expires_at = await currentContext.GetTokenAsync("expires_at");
            if (string.IsNullOrWhiteSpace(expires_at)
                || ((DateTime.Parse(expires_at).AddSeconds(-60)).ToUniversalTime() < DateTime.UtcNow))
            {
                accessToken = await RenewTokens();
            }
            else
            {
                accessToken = await currentContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
            }

            if (!string.IsNullOrWhiteSpace(accessToken))
            {
                _logger.LogInformation($"Using Access Token: {accessToken}");
                _httpClient.SetBearerToken(accessToken);
            }

            _httpClient.BaseAddress = new Uri(_configuration["WebApiBaseAddress"]);
            _httpClient.DefaultRequestHeaders.Accept.Clear();
            _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            return _httpClient;
        }

        private async Task<string> RenewTokens()
        {
            // get the current HttpContext to access the tokens
            var currentContext = _httpContextAccessor.HttpContext;

            // get the metadata
            var discoveryClient = new DiscoveryClient(_configuration["IDPBaseAddress"]);
            var metaDataResponse = await discoveryClient.GetAsync();

            // create a new token client to get new tokens
            var tokenClient = new TokenClient(
                metaDataResponse.TokenEndpoint,
                _configuration["ClientId"],
                _configuration["ClientSecret"]);

            // get the saved refresh token
            var currentRefreshToken = await currentContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);

            // refresh the tokens
            var tokenResult = await tokenClient.RequestRefreshTokenAsync(currentRefreshToken);
            if (tokenResult.IsError)
            {
                throw new Exception("Problem encountered while refreshing tokens.", tokenResult.Exception);
            }

            // update the tokens & expiration value
            var updatedTokens = new List<AuthenticationToken>();
            updatedTokens.Add(new AuthenticationToken
            {
                Name = OpenIdConnectParameterNames.IdToken,
                Value = tokenResult.IdentityToken
            });
            updatedTokens.Add(new AuthenticationToken
            {
                Name = OpenIdConnectParameterNames.AccessToken,
                Value = tokenResult.AccessToken
            });
            updatedTokens.Add(new AuthenticationToken
            {
                Name = OpenIdConnectParameterNames.RefreshToken,
                Value = tokenResult.RefreshToken
            });

            var expiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(tokenResult.ExpiresIn);
            updatedTokens.Add(new AuthenticationToken
            {
                Name = "expires_at",
                Value = expiresAt.ToString("o", CultureInfo.InvariantCulture)
            });

            // get authenticate result, containing the current principal & properties
            var currentAuthenticateResult = await currentContext.AuthenticateAsync("Cookies");

            // store the updated tokens
            currentAuthenticateResult.Properties.StoreTokens(updatedTokens);

            // sign in
            await currentContext.SignInAsync("Cookies",
             currentAuthenticateResult.Principal, currentAuthenticateResult.Properties);

            // return the new access token
            return tokenResult.AccessToken;
        }
    }
}
تفاوت این کلاس با نمونه‌ی قبلی آن در اضافه شدن متد RenewTokens آن است.
پیشتر در قسمت ششم، روش کار مستقیم با DiscoveryClient و TokenClient را در حین کار با UserInfo Endpoint جهت دریافت دستی اطلاعات claims از IDP بررسی کردیم. در اینجا به همین ترتیب با TokenEndpoint کار می‌کنیم. به همین جهت توسط DiscoveryClient، متادیتای IDP را که شامل آدرس TokenEndpoint است، استخراج کرده و توسط آن TokenClient را به همراه اطلاعات کلاینت تشکیل می‌دهیم.
سپس مقدار refresh token فعلی را نیاز داریم. زیرا توسط آن است که می‌توانیم درخواست دریافت یکسری توکن جدید را ارائه دهیم. پس از آن با فراخوانی tokenClient.RequestRefreshTokenAsync(currentRefreshToken)، تعدادی توکن جدید را از سمت IDP دریافت می‌کنیم. لیست آن‌ها را تهیه کرده و توسط آن کوکی جاری را به روز رسانی می‌کنیم. در این حالت نیاز است مجددا SignInAsync فراخوانی شود تا کار به روز رسانی کوکی نهایی گردد.
خروجی این متد، مقدار access token جدید است.
پس از آن در متد GetHttpClientAsync بررسی می‌کنیم که آیا نیاز است کار refresh token صورت گیرد یا خیر؟ برای این منظور مقدار expires_at را دریافت و با زمان جاری با فرمت UTC مقایسه می‌کنیم. 60 ثانیه پیش از انقضای توکن، متد RenewTokens فراخوانی شده و توسط آن access token جدیدی برای استفاده‌ی در برنامه صادر می‌شود. مابقی این متد مانند قبل است و این توکن دسترسی را به همراه درخواست از Web API به سمت آن ارسال می‌کنیم.


معرفی Reference Tokens

تا اینجا با توکن‌هایی از نوع JWT کار کردیم. این نوع توکن‌ها، به همراه تمام اطلاعات مورد نیاز جهت اعتبارسنجی آن‌ها در سمت کلاینت، بدون نیاز به فراخوانی مجدد IDP به ازای هر درخواست هستند. اما این نوع توکن‌ها به همراه یک مشکل نیز هستند. زمانیکه صادر شدند، دیگر نمی‌توان طول عمر آن‌ها را کنترل کرد. اگر طول عمر یک Access token به مدت 20 دقیقه تنظیم شده باشد، می‌توان مطمئن بود که در طی این 20 دقیقه حتما می‌توان از آن استفاده کرد و دیگر نمی‌توان در طی این بازه‌ی زمانی دسترسی آن‌را بست و یا آن‌را برگشت زد. اینجاست که Reference Tokens معرفی می‌شوند. بجای قرار دادن تمام اطلاعات در یک JWT متکی به خود، این نوع توکن‌های مرجع، فقط یک Id هستند که به توکن اصلی ذخیره شده‌ی در سطح IDP لینک می‌شوند و به آن اشاره می‌کنند. در این حالت هربار که نیاز به دسترسی منابع محافظت شده‌ی سمت API را با یک چنین توکن دسترسی لینک شده‌ای داشته باشیم، Reference Token در پشت صحنه (back channel) به IDP ارسال شده و اعتبارسنجی می‌شود. سپس محتوای اصلی آن به سمت API ارسال می‌شود. این عملیات از طریق endpoint ویژه‌ای در IDP به نام token introspection endpoint انجام می‌شود. به این ترتیب می‌توان طول عمر توکن صادر شده را کاملا کنترل کرد؛ چون تنها تا زمانیکه در data store مربوط به IDP وجود خارجی داشته باشند، قابل استفاده خواهند بود. بنابراین نسبت به حالت استفاده‌ی از JWTهای متکی به خود، تنها عیب آن زیاد شدن ترافیک به سمت IDP جهت اعتبارسنجی Reference Token‌ها به ازای هر درخواست به سمت Web API است.


چگونه از Reference Token‌ها بجای JWTهای متکی به خود استفاده کنیم؟

برای استفاده‌ی از Reference Tokenها بجای JWTها، ابتدا نیاز به مراجعه‌ی به کلاس src\IDP\DNT.IDP\Config.cs و تغییر مقدار خاصیت AccessTokenType هر کلاینت است:
namespace DNT.IDP
{
    public static class Config
    {
        public static IEnumerable<Client> GetClients()
        {
            return new List<Client>
            {
                new Client
                {
                    ClientName = "Image Gallery",
// ...
                    AccessTokenType = AccessTokenType.Reference
                }
             };
        }
    }
}
مقدار پیش‌فرض AccessTokenType، همان Jwt یا توکن‌های متکی به خود است که در اینجا به Reference Token تغییر یافته‌است.
اینبار اگر برنامه را اجرا کنید و در کلاس ImageGalleryHttpClient برنامه‌ی کلاینت، بر روی سطر httpClient.SetBearerToken یک break-point قرار دهید، مشاهده خواهید کرد فرمت این توکن ارسالی به سمت Web API تغییر یافته و اینبار تنها یک Id ساده‌است که دیگر قابل decode شدن و استخراج اطلاعات دیگری از آن نیست. با ادامه جریان برنامه و رسیدن این توکن به سمت Web API، درخواست رسیده برگشت خواهد خورد و اجرا نمی‌شود.
علت اینجا است که هنوز تنظیمات کار با token introspection endpoint انجام نشده و این توکن رسیده‌ی در سمت Web API قابل اعتبارسنجی و استفاده نیست. برای تنظیم آن نیاز است یک ApiSecret را در سطح Api Resource مربوط به IDP تنظیم کنیم:
namespace DNT.IDP
{
    public static class Config
    {
        // api-related resources (scopes)
        public static IEnumerable<ApiResource> GetApiResources()
        {
            return new List<ApiResource>
            {
                new ApiResource(
                    name: "imagegalleryapi",
                    displayName: "Image Gallery API",
                    claimTypes: new List<string> {"role" })
                {
                  ApiSecrets = { new Secret("apisecret".Sha256()) }
                }
            };
        }
اکنون فایل startup در سطح API را جهت معرفی این تغییرات به صورت زیر ویرایش می‌کنیم:
namespace ImageGallery.WebApi.WebApp
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(defaultScheme: IdentityServerAuthenticationDefaults.AuthenticationScheme)
               .AddIdentityServerAuthentication(options =>
               {
                   options.Authority = Configuration["IDPBaseAddress"];
                   options.ApiName = "imagegalleryapi";
                   options.ApiSecret = "apisecret";
               });
در اینجا نیاز است ApiSecret تنظیم شده‌ی در سطح IDP معرفی شود.

اکنون اگر برنامه را اجرا کنید، ارتباط با token introspection endpoint به صورت خودکار برقرار شده، توکن رسیده اعتبارسنجی گردیده و برنامه بدون مشکل اجرا خواهد شد.


چگونه می‌توان Reference Tokenها را از IDP حذف کرد؟

هدف اصلی استفاده‌ی از Reference Tokenها به دست آوردن کنترل بیشتری بر روی طول عمر آن‌ها است و حذف کردن آن‌ها می‌تواند به روش‌های مختلفی رخ دهد. برای مثال یک روش آن تدارک یک صفحه‌ی Admin و ارائه‌ی رابط کاربری برای حذف توکن‌ها از منبع داده‌ی IDP است. روش دیگر آن حذف این توکن‌ها از طریق برنامه‌ی کلاینت با برنامه نویسی است؛ برای مثال در زمان logout شخص. برای این منظور، endpoint ویژه‌ای به نام token revocation endpoint در نظر گرفته شده‌است. فراخوانی آن از سمت برنامه‌ی کلاینت، امکان حذف توکن‌های ذخیره شده‌ی در سمت IDP را میسر می‌کند.
به همین جهت به کنترلر ImageGallery.MvcClient.WebApp\Controllers\GalleryController.cs مراجعه کرده و متد Logout آن‌را تکمیل می‌کنیم:
namespace ImageGallery.MvcClient.WebApp.Controllers
{
    [Authorize]
    public class GalleryController : Controller
    {
        public async Task Logout()
        {
            await revokeTokens();
            // Clears the  local cookie ("Cookies" must match the name of the scheme)
            await HttpContext.SignOutAsync("Cookies");
            await HttpContext.SignOutAsync("oidc");
        }

        private async Task revokeTokens()
        {
            var discoveryClient = new DiscoveryClient(_configuration["IDPBaseAddress"]);
            var metaDataResponse = await discoveryClient.GetAsync();
            var tokenRevocationClient = new TokenRevocationClient(
                metaDataResponse.RevocationEndpoint,
                _configuration["ClientId"],
                _configuration["ClientSecret"]
            );

            var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
            if (!string.IsNullOrWhiteSpace(accessToken))
            {
                var response = await tokenRevocationClient.RevokeAccessTokenAsync(accessToken);
                if (response.IsError)
                {
                    throw new Exception("Problem accessing the TokenRevocation endpoint.", response.Exception);
                }
            }

            var refreshToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);
            if (!string.IsNullOrWhiteSpace(refreshToken))
            {
                var response = await tokenRevocationClient.RevokeRefreshTokenAsync(refreshToken);
                if (response.IsError)
                {
                    throw new Exception("Problem accessing the TokenRevocation endpoint.", response.Exception);
                }
            }
        }
در اینجا در متد جدید revokeTokens، ابتدا توسط DiscoveryClient، به آدرس RevocationEndpoint دسترسی پیدا می‌کنیم. سپس توسط آن، TokenRevocationClient را تشکیل می‌دهیم. اکنون می‌توان توسط این کلاینت حذف توکن‌ها، دو متد RevokeAccessTokenAsync و RevokeRefreshTokenAsync آن‌را بر اساس مقادیر فعلی این توکن‌ها در سیستم، فراخوانی کرد تا سبب حذف آن‌ها در سمت IDP شویم.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشه‌ی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشه‌ی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آن‌را اجرا کنید تا برنامه‌ی IDP راه اندازی شود.
- در آخر به پوشه‌ی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحه‌ی login نام کاربری را User 1 و کلمه‌ی عبور آن‌را password وارد کنید.
نظرات مطالب
T4MVC : یکی از الزامات مدیریت پروژه‌های ASP.NET MVC
با سلام.
من T4MVC را بر روی پروژه اصلی قرار دادم.
فایل‌های resource من در یک پروژه جدا قرار دارند. مدلهای پروژه من هم در یک پروژه جدا قرار دارند. حال برای تعریف خاصیت اعتبارسنجی روی یکی از خاصیت‌های ویومدل آیا می‌شود T4MVC را بکار برد؟ (بجای استفاده از magic string)
 [StringLength(25, ErrorMessageResourceType = typeof(ValidationErrorsResource), ErrorMessageResourceName = "InvalidStringLength")]
public string FatherName { get; set; }

مطالب
Functional Programming - قسمت پنجم - وسواس استفاده از نوع های اولیه
در ادامه سری مقالات مرتبط با برنامه نویسی تابعی ، قصد دارم به استفاده کردن یا نکردن از نوع‌های داده اولیه (Primitive Types) را بررسی کنیم. پیشنهاد میکنم در صورتی که قسمت‌های قبلی را مطالعه نکرده اید ابتدا قسمت‌های قبل را بخوانید.

در طراحی مدل دامین، بیشتر مواقع از نوع‌های اولیه مانند int , string,… استفاده میکنیم و به عبارتی میتوانیم بگوییم در استفاده از این نوع داده وسواس داریم. قطعه کد زیر را در نظر بگیرید:
public class UserFactory
{
    public User CreateUser(string email) {
        return new User(email);
    }
}
کلاس UserFactory، یک متد به نام CreateUser دارد که یک رشته را به عنوان ورودی میگیرد و یک شیء از کلاس User را بر می‌گرداند. خوب مشکل این متد کجاست؟
اگر به خاطر داشته باشید، در قسمت‌های قبلی در مورد مفهومی به نام Honesty صحبت کردیم. به طور ساده باید بتوانیم از روی امضای تابع، کاری را که تابع انجام میدهد و خروجی آن را ببینیم. این تابع Honest نیست؛ شرایطی که string می‌تواند درست نباشد، خالی باشد، طول غیر مجاز داشته باشد و ... را نمیتوانیم از امضای تابع حدس بزنیم.

برای روشن‌تر شدن بحث، مثال بالا را همیشه در ذهن خود داشته باشید. در این مثال، در تابع Divide که عمل تقسیم را انجام می‌دهد، پارامتر y که یک عدد از نوع int است، میتواند مقدار صفر را داشته باشد و باعث یک exception شود.و از آنجائیکه نوع خروجی این متد هم int است، انتظار دریافت یک exception را نداریم. در مورد exception‌ها به طول مفصل در قسمت قبلی صحبت کردیم. در مثال بالا تصور کنید که بجای یک ایمیل، از چند ایمیل به عنوان ورودی می‌خواهید استفاده کنید. آیا منطق Validation را به ازای هر پارامتر ورودی باید تکرار کنید؟

به طور کلی استفاده‌ی نابجا و بیش از حد از نوع‌های داده‌ی اولیه، باعث می‌شود تا Honesty متد‌ها را از دست بدهیم و قاعده‌ی DRY را نقض کنیم.

صحبت در مورد استفاده کردن یا نکردن، جنبه‌های زیادی دارد و یکی از مواردی است که در معماری DDD تحت عنوان Value Object به آن پرداخته شده. هدف ما در این قسمت از مقاله، صرفا پرداختن به گوشه‌ای از این مورد هست. ولی شما میتوانید برای مطالعه بیشتر و اطلاعات تکمیلی کتاب Domain-Driven Design: Tackling Complexity in the Heart of Software نوشته Eric Evans را مطالعه کنید.


به جای نوع‌های اولیه از چی استفاده کنیم؟

جواب خیلی ساده‌است؛ شما نیاز دارید تا یک Type اختصاصی را ایجاد کنید. برای مثال بجای استفاده از نوع string برای یک ایمیل، می‌توانید یک کلاس را به عنوان Email ایجاد کنید که مشخصه‌ای به نام Value دارد. این کار به روش‌های مختلفی قابل انجام است؛ اما پیشنهاد من استفاده از این روش هست:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace ValueOf
{
    public class ValueOf<TValue, TThis> where TThis : ValueOf<TValue, TThis>, new()
    {
        private static readonly Func<TThis> Factory;

        /// <summary>
        /// WARNING - THIS FEATURE IS EXPERIMENTAL. I may change it to do
        /// validation in a different way.
        /// Right now, override this method, and throw any exceptions you need to.
        /// Access this.Value to check the value
        /// </summary>
        protected virtual void Validate()
        {
        }

        static ValueOf()
        {
            ConstructorInfo ctor = typeof(TThis)
                .GetTypeInfo()
                .DeclaredConstructors
                .First();

            var argsExp = new Expression[0];
            NewExpression newExp = Expression.New(ctor, argsExp);
            LambdaExpression lambda = Expression.Lambda(typeof(Func<TThis>), newExp);

            Factory = (Func<TThis>)lambda.Compile();
        }

        public TValue Value { get; protected set; }

        public static TThis From(TValue item)
        {
            TThis x = Factory();
            x.Value = item;
            x.Validate();

            return x;
        }

        protected virtual bool Equals(ValueOf<TValue, TThis> other)
        {
            return EqualityComparer<TValue>.Default.Equals(Value, other.Value);
        }

        public override bool Equals(object obj)
        {
            if (obj is null)
                return false;

            if (ReferenceEquals(this, obj))
                return true;

            return obj.GetType() == GetType() && Equals((ValueOf<TValue, TThis>)obj);
        }

        public override int GetHashCode()
        {
            return EqualityComparer<TValue>.Default.GetHashCode(Value);
        }

        public static bool operator ==(ValueOf<TValue, TThis> a, ValueOf<TValue, TThis> b)
        {
            if (a is null && b is null)
                return true;

            if (a is null || b is null)
                return false;

            return a.Equals(b);
        }

        public static bool operator !=(ValueOf<TValue, TThis> a, ValueOf<TValue, TThis> b)
        {
            return !(a == b);
        }

        public override string ToString()
        {
            return Value.ToString();
        }
    }
}
در این روش، یک کلاس را به عنوان Value Object ایجاد کرده‌ایم. این کلاس، نوع اولیه‌ای را که با آن سر و کار داریم، در بر خواهد گرفت و منطق مربوط به مقایسه، همچنین عملگرهای == و != را هم از طریق Equals و GetHashCode، پیاده سازی کرده. برای مثال جهت کلاس ایمیل می‌توانیم به صورت زیر عمل کنیم:
public class EmailAddress : ValueOf<string, EmailAddress> { }
همچنین برای مقدار دهی این کلاس میتوانید به صورت زیر عمل کنید:
EmailAddress emailAddress = EmailAddress.From("foo@bar.com");
برای مثال‌های پیچیده‌تر مانند آدرس، که شامل آدرس، کد پستی و … می‌باشد، میتوانید با استفاده از امکان Tuple‌ها که از سی شارپ 7 به بعد معرفی شده، مانند مثال زیر عمل کنید:
public class Address : ValueOf<(string firstLine, string secondLine, Postcode postcode), Address> { }
و در نهایت برای نوشتن منطق مربوط به validation می‌توانید متد Validate را Override کنید و قاعده‌ی DRY را هم نقض نکنید.

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

در صورتیکه مشکلی در پیاده سازی داشتید، می‌توانید مشکل خود را زیر همین مطلب و یا بر روی gist آن کامنت کنید.
مطالب
مدیریت طول عمر DbContext در برنامه‌های Blazor SSR

فرض کنید یک صفحه‌ی Blazor SSR، از سه کامپوننت منوی سمت راست، محتوای اصلی صفحه و فوتر سایت که به همراه متنی است، تشکیل شده‌است. منوی سمت راست، به همراه لینک‌هایی‌است که آمار آن‌ها را نیز نمایش می‌دهد و این اطلاعات را از بانک اطلاعاتی، به کمک EF-Core دریافت می‌کند. فوتر صفحه، سال شروع به کار و نام برنامه را از بانک اطلاعاتی دریافت می‌کند و محتوای اصلی صفحه نیز از بانک اطلاعاتی دریافت می‌شود. پس از تکمیل این سه کامپوننت مجزا، اگر برنامه را اجرا کنید، بلافاصله با خطای زیر مواجه می‌شوید:

A second operation started on this context before a previous operation completed

مشکل کجاست؟! مشکل اینجاست که تنها یک نمونه از DbContext، در طول درخواست جاری رسیده، بین سه کامپوننت جاری برنامه به اشتراک گذاشته می‌شود (به سازنده‌ی سرویس‌های مرتبط تزریق می‌شود) و ... در Blazor SSR، پردازش کامپوننت‌های یک صفحه، به صورت موازی و همزمان انجام می‌شوند؛ یعنی ترتیبی نیست. اگر ابتدا کامپوننت منو، بعد محتوای صفحه و در آخر فوتر، رندر می‌شدند، هیچگاه پیام فوق را مشاهده نمی‌کردیم؛ اما ... هر سه کامپوننت، با هم و همزمان رندر می‌شوند و سپس نتیجه‌ی نهایی در Response درج خواهد شد. یعنی یک DbContext بین چندین ترد به اشتراک گذاشته می‌شود که چنین حالتی توسط EF-Core پشتیبانی نمی‌شود و مجاز نیست.

روش مواجه شدن با یک چنین حالت‌هایی، نمونه سازی مجزای DbContext به ازای هر کامپوننت است که نمونه‌ای از آن‌را پیشتر در مطلب «نکات ویژه‌ی کار با EF-Core در برنامه‌های Blazor Server» مشاهده کرده‌اید. در این مطلب، راه‌حل دیگری برای اینکار ارائه می‌شود که ساده‌تر است و نیازی به تغییرات آنچنانی در کدهای کامپوننت‌ها و کل برنامه ندارد.

استفاده از کلاس پایه‌ی OwningComponentBase برای نمونه سازی مجدد DbContext به‌ازای هر کامپوننت

زمانیکه در برنامه‌های Blazor SSR از روش استاندارد زیر برای دسترسی به سرویس‌های مختلف برنامه استفاده می‌کنیم:

@inject IHotelRoomService HotelRoomService

طول عمر دریافتی سرویس، دقیقا بر اساس طول عمر اصلی تعریف شده‌ی آن عمل می‌کند (شبیه به برنامه‌های ASP.NET Core). یعنی برای مثال اگر Scoped باشد، DbContext تزریق شده‌ی در آن هم Scoped است و این DbContext، بین تمام کامپوننت‌های در حال پردازش موازی در طول یک درخواست، به‌اشتراک گذاشته می‌شود که مطلوب ما نیست. ما می‌خواهیم بتوانیم به ازای هر کامپوننت مجزای صفحه، یک DbContext جدید داشته باشیم. یعنی باید بتوانیم خودمان این سرویس Scoped را نمونه سازی کنیم و نه اینکه آن‌را مستقیما از سیستم تزریق وابستگی‌ها دریافت کنیم.

بنابراین اگر بخواهیم قسمت‌های مختلف برنامه را تغییر ندهیم و همان تعاریف ابتدایی services.AddDbContext و Scoped تعریف کردن سرویس‌های برنامه بدون تغییر باقی بمانند (و از IDbContextFactory و موارد مشابه دیگر مطلب «نکات ویژه‌ی کار با EF-Core در برنامه‌های Blazor Server» هم استفاده نکنیم)، باید جایگزینی را برای نمونه سازی سرویس‌ها ارائه دهیم. به همین جهت در ابتدا، یک ویژگی جدیدی را به صورت زیر تعریف می‌کنیم:

[AttributeUsage(AttributeTargets.Property)]
public sealed class InjectComponentScopedAttribute : Attribute
{
}

تا بتوانیم بجای:

@inject IHotelRoomService HotelRoomService

بنویسیم:

[InjectComponentScoped] internal IHotelRoomService HotelRoomService { set; get; } = null!;

مرحله‌ی بعد، نوبت به نمونه سازی خودکار این سرویس‌های درخواستی علامتگذاری شده‌ی با InjectComponentScoped است. برای این منظور، تمام کامپوننت‌های برنامه را از کلاس پایه و استاندارد OwningComponentBase ارث‌بری می‌کنیم. مزیت اینکار، امکان دسترسی به خاصیتی به نام ScopedServices در تمام کامپوننت‌های برنامه است که توسط آن می‌توان به متد ScopedServices.GetRequiredService آن دسترسی یافت. یعنی با ارث‌بری از کلاس پایه‌ی OwningComponentBase به ازای هر کامپوننت، به صورت خودکار Scope جدیدی شروع می‌شود که توسط آن می‌توان به نمونه‌ی جدیدی از سرویس مدنظر دسترسی یافت و نه به نمونه‌ی اشتراکی در طی درخواست جاری.

اکنون اگر از این مزیت به صورت زیر استفاده کنیم، می‌توان تمام سرویس‌های درخواستی مزین به InjectComponentScopedAttribute یک کامپوننت را به صورت خودکار یافته و با استفاده از ScopedServices.GetRequiredService، مقدار دهی کرد:

public class BlazorScopedComponentBase : OwningComponentBase
{
    private static readonly ConcurrentDictionary<Type, Lazy<List<PropertyInfo>>> CachedProperties = new();

    private List<PropertyInfo> InjectComponentScopedPropertiesList => CachedProperties.GetOrAdd(GetType(),
            type => new Lazy<List<PropertyInfo>>(
                () => type.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Instance |
                                         BindingFlags.NonPublic | BindingFlags.Public)
                    .Where(p => p.GetCustomAttribute<InjectComponentScopedAttribute>() is not null)
                    .ToList(), LazyThreadSafetyMode.ExecutionAndPublication)).Value;

    protected override void OnInitialized()
    {
        foreach (var propertyInfo in InjectComponentScopedPropertiesList)
        {
            propertyInfo.SetValue(this, ScopedServices.GetRequiredService(propertyInfo.PropertyType));
        }
    }
}

این سرویس، اینبار طول عمری، محدود به کامپوننت جاری را خواهد داشت و بین سایر کامپوننت‌های درحال پردازش درخواست جاری، به اشتراک گذاشته نمی‌شود و همچنین به صورت خودکار هم در پایان درخواست، Dispose می‌شود.

فعالسازی ارث‌بری خودکار در تمام کامپوننت‌های برنامه

مرحله‌ی بعد، ارث‌بری خودکار تمام کامپوننت‌های برنامه از OwningComponentBase سفارشی فوق است و در اینجا قصد نداریم تمام کامپوننت‌ها را جهت معرفی آن، به صورت دستی تغییر دهیم. برای اینکار فقط کافی است به فایل Imports.razor_ مراجعه و یک سطر زیر را در آن درج کنیم:

@inherits BlazorScopedComponentBase

با اینکار یک ارث‌بری سراسری در کل برنامه رخ می‌دهد و تمام کامپوننت‌ها، از BlazorScopedComponentBase مشتق خواهند شد. یعنی پس از این تغییر، اگر سرویسی را به صورت زیر معرفی و با ویژگی InjectComponentScoped علامتگذاری کردیم:

[InjectComponentScoped] internal IHotelRoomService HotelRoomService { set; get; } = null!;

به صورت خودکار یافت شده و نمونه سازی Scoped محدود به طول عمر همان کامپوننت می‌شود که بین سایر کامپوننت‌ها، به اشتراک گذاشته نخواهد شد.

یک نکته: اگر کامپوننت شما متد OnInitialized را بازنویسی می‌کند، ‌فراموش نکنید که در ابتدای آن باید ()base.OnInitialized را هم فراخوانی کنید تا متد OnInitialized کامپوننت پایه‌ی BlazorScopedComponentBase نیز فراخوانی شود. البته این مورد در حین بازنویسی نمونه‌ی async آن مهم نیست؛ چون همیشه OnInitialized غیر async در ابتدا فراخوانی می‌شود و سپس نمونه‌ی async آن اجرا خواهد شد.

مطالب
آشنایی با Refactoring - قسمت 6

در ادامه بحث «حذف کدهای تکراری»، روش Refactoring دیگری به نام "Extract Superclass" وجود دارد که البته در بین برنامه نویس‌های دات نت به نام Base class بیشتر مشهور است تا Superclass. هدف آن هم انتقال کدهای تکراری بین چند کلاس، به یک کلاس پایه و سپس ارث بری از آن می‌باشد.

یک مثال:
در WPF و Silverlight جهت مطلع سازی رابط کاربری از تغییرات حاصل شده در مقادیر داده‌ها، نیاز است کلاس مورد نظر، اینترفیس INotifyPropertyChanged را پیاده سازی کند:

using System.ComponentModel;

namespace Refactoring.Day6.ExtractSuperclass.Before
{
public class User : INotifyPropertyChanged
{
string _name;
public string Name
{
get { return _name; }
set
{
if (_name == value) return;
_name = value;
raisePropertyChanged("Name");
}
}

public event PropertyChangedEventHandler PropertyChanged;
void raisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler == null) return;
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}


و نکته‌ی مهم این است که اگر 100 کلاس هم داشته باشید، باید این کدهای تکراری اجباری مرتبط با raisePropertyChanged را در آن‌ها قرار دهید. به همین جهت مرسوم است برای کاهش حجم کدهای تکرای، قسمت‌های تکراری کد فوق را در یک کلاس پایه قرار می‌دهند:

using System.ComponentModel;

namespace Refactoring.Day6.ExtractSuperclass.After
{
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler == null) return;
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}

و سپس از آن ارث بری می‌کنند:

namespace Refactoring.Day6.ExtractSuperclass.After
{
public class User : ViewModelBase
{
string _name;
public string Name
{
get { return _name; }
set
{
if (_name == value) return;
_name = value;
RaisePropertyChanged("Name");
}
}
}
}


به این ترتیب این کلاس پایه در ده‌ها و صدها کلاس قابل استفاده خواهد بود، بدون اینکه مجبور شویم مرتبا یک سری کد تکراری «اجباری» را copy/paste کنیم.

مثالی دیگر:
اگر با ORM های Code first کار کنید، نیاز است تا ابتدا طراحی کار توسط کلاس‌های ساده دات نتی انجام شود؛ که اصطلاحا به آن‌ها POCO یا Plain old CLR objects یا Plain old .NET Classes هم گفته می‌شود. در بین این کلاس‌ها، متداول است که یک سری از خصوصیات، تکراری و مشترک باشد؛ مثلا تمام کلاس‌ها تاریخ ثبت رکورد را هم داشته باشند به همراه نام کاربر و مشخصاتی از این دست. اینجا هم برای حذف کدهای تکراری، یک Base class طراحی می‌شود: (+)

نظرات مطالب
Blazor 5x - قسمت دهم - مبانی Blazor - بخش 7 - مسیریابی
تعدادی پرسش و پاسخ تکمیلی مباحث مسیریابی

چگونه می‌توان DateTime را به عنوان پارامتر مسیریابی ارسال کرد؟
Blazor از پارامترهایی از نوع DateTime، در حین مسیریابی پشتیبانی می‌کند؛ با این شرط که قید datetime در حین مسیریابی صریحا ذکر شود:
@page "/route/{parameter:datetime}"
یک مثال: کامپوننت index.razor که سبب هدایت به سمت کامپوننت دیگری به همراه ارسال پارامتری از نوع DateTime می‌شود:
@page "/"
@inject NavigationManager NavManager
<button @onclick="CurrentTime">Current Time</button>
@code {
    public void CurrentTime()
    {
        NavManager.NavigateTo("/time/" + DateTime.Now);
    }
}
و کامپوننت Time.razor که قادر است این DateTime را دریافت کرده و به یک پارامتر، Bind کند:
@page "/time/{param:datetime}"
<h3>Time</h3>
<p>@Param</p>
@code {
    [Parameter]
    public DateTime Param { get; set; }
}


چگونه می‌توان به عنوان صفحه‌ی جاری دسترسی یافت؟
برای اینکار نیاز است از JavaScript interop استفاده کرد. ابتدا برای مثال تابع عمومی getTitle را که بر اساس DOM API مرورگر کار می‌کند، تهیه می‌کنیم:
window.getTitle = () => {
   return document.title;
};
سپس می‌توان از آن در یک کامپوننت Blazor به صورت زیر استفاده کرد:
@page "/"
@inject IJSRuntime jsRuntime
<h2>Page Title: @Title</h2>
<button class="btn btn-primary" @onclick="@GetTitle">Get Title</button>
@code {
public string Title = "";
    
    public async void GetTitle()
    {
            Title = await jsRuntime.InvokeAsync<string>("getTitle");
     }
}

چگونه می‌توان مسیری را از طریق کدهای برنامه در یک برگه‌ی مجزای مرورگر باز کرد؟
در اینجا نیز می‌توان با استفاده از JavaScript interop، متد استاندارد open مرورگر را فراخوانی کرد و پارامتر اول آن‌را به url مدنظر و پارامتر بعدی آن‌را به blank_ تنظیم کرد تا مرورگر آدرس درخواستی را در یک برگه‌ی جدید باز کند:
@inject IJSRuntime jsRuntime

<button @onclick="NavigateToNewTab">New Tab Navigation</button>
@code {
public async Task NavigateToNewTab()
    {
        string url = "/counter";
        await jsRuntime.InvokeAsync<object>("open", url, "_blank");
    }
}
نظرات مطالب
چگونگی رسیدگی به Null property در AutoMapper
public class TestModel
    {
        public byte[] MProperty { get; set; }
    }

 public class TestViewModel
    {
       public byte[] VMProperty { get; set; }
    }

class Program
    {
        static void Main(string[] args)
        {
            Mapper.CreateMap<TestModel,TestViewModel>().ForMember(tv=>tv.VMProperty,m=>m.MapFrom(t=>t.MProperty));
            TestModel tm = new TestModel();
            tm.MProperty = new byte[]  { 1, 2, 3 ,4};

            TestViewModel tvm = Mapper.Map<TestModel, TestViewModel>(tm);
            foreach (var item in tvm.VMProperty)
            {
                Console.WriteLine(item.ToString());
            }
            Console.ReadKey();
        }
    }
در صورتی که MProperty، مقدار داشته باشه مشکلی پیش نمیاد اگر هم null باشه بازم مشکلی پیش نمیاد و VMProperty برابر با null هست.
مطالب
آشنایی و استفاده از WCF Data Services در Visualstudio 2012
مقدمه:

WCF Data Services جزئی از NET Framework است که امکان ایجاد سرویس دهنده‌های با قرارداد OData را به روی وب یا Intranet با استفاده از REST مهیا می‌سازد. OData از داده هایی که با Url آدرس پذیر هستند استفاده می‌نماید. دسترسی و تغییر داده‌ها با استفاده از استاندارد HTTP و کلمات GET، PUT، POST و DELETE صورت می‌پذیرد. برای اینکه درک بهتری داشته باشید به یک مثال می‌پردازیم.

ایجاد یک برنامه سرویس دهنده WCF Data Service در 2012 VisualStudio
  1. یک ASP.NET Web Application با نام NorthwindService ایجاد نمایید و بر روی پروژه راست کلیک کنید و از منوی Add گزینه New Item را انتخاب نمایید  از پنجره باز شده از دسته Data گزینه ADO.NET Entity Data Model  را انتخاب و نام ان را Northwind بگذارید.
  2. از پنجره باز شده Generate from Databaseرا انتخاب و با انتخاب کانکشن از نوع Sql Server Compact 4  اتصال به فایل Northwind.sdf را انتخاب تا کلاس‌های لازم تولید شود.
  3. برای تولید data service بر روی پروژه راست کلیک کنید و از منوی Add گزینه New Item را انتخاب نمایید از پنجره باز شده گزینه  WCF Data Service را انتخاب و نام آن را Northwind.svc بگذارید. کد زیر خودکار تولید می‌شود
     public class Northwind : DataService< /* TODO: put your data source class name here */ >
        {
            // This method is called only once to initialize service-wide policies.
            public static void InitializeService(DataServiceConfiguration config)
            {
                // TODO: set rules to indicate which entity sets and service operations are visible, updatable, etc.
                // Examples:
                // config.SetEntitySetAccessRule("MyEntityset", EntitySetRights.AllRead);
                // config.SetServiceOperationAccessRule("MyServiceOperation", ServiceOperationRights.All);
                config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V3;
            }
        }
  4. برای دسترسی به موجودیت‌های Northwind بجای عبارت put your data source نام مدل را تایپ کنید
    public class Northwind : DataService<NorthwindEntities>
  5. برای فعال کردن دسترسی به منابع data source متغیر config کلاس DataServiceConfiguration را بصورت زیر تنظیم نمایید. تابع SetEntitySetAccessRule با گرفتن نام موجودیت و نحوه دسترسی امکان استفاده از این موجودیت را با استفاده از WCF Data Service فزاهم می‌نمایید. مثلا در زیر امکان دسترسی به موجودیت Orders را با امکان خواندن همه، نوشتن ادقامی و جایگزین فراهم نموده است.
    config.SetEntitySetAccessRule("Orders", EntitySetRights.AllRead 
         | EntitySetRights.WriteMerge 
         | EntitySetRights.WriteReplace );
    config.SetEntitySetAccessRule("Customers", EntitySetRights.AllRead);
  6. اگر بخواهیم امکان خواندن همه موجودیت‌ها را فراهم کنیم از کد زیر می‌توانیم استفاده نمایید که * به معنای همه موجودیت‌های data model می‌باشد
    config.SetEntitySetAccessRule("*", EntitySetRights.AllRead);

دسترسی به WCF Data Service بوسیله مرورگر وب
برای دسترسی به وب سرویس برنامه را اجرا نمایید تا آدرس http://localhost:8358/Northwind.svc مشخصات وب سرویس را نمایش دهد
<service xmlns="http://www.w3.org/2007/app" xmlns:atom="http://www.w3.org/2005/Atom" xml:base="http://localhost:8358/Northwind.svc/">
<workspace>
<atom:title>Default</atom:title>
<collection href="Categories">
<atom:title>Categories</atom:title>
</collection>
<collection href="Customers">
<atom:title>Customers</atom:title>
</collection>
<collection href="Employees">
<atom:title>Employees</atom:title>
</collection>
<collection href="Order_Details">
<atom:title>Order_Details</atom:title>
</collection>
<collection href="Orders">
<atom:title>Orders</atom:title>
</collection>
<collection href="Products">
<atom:title>Products</atom:title>
</collection>
<collection href="Shippers">
<atom:title>Shippers</atom:title>
</collection>
<collection href="Suppliers">
<atom:title>Suppliers</atom:title>
</collection>
</workspace>
</service>
 حال اگر آدرس را به http://localhost:8358/Northwind.svc/Products وارد نمایید لیست کالا‌ها بصورت Atom xml قابل دسترس می‌باشد.
ایجاد یک برنامه گیرنده WCF Data Service در Visual Studio 2012
  1. بر روی Solution پروژه جاری راست کلیک و از منوی Add گزینه New Project را انتخاب و یک پروژه از نوع WPF Application با نام NorthwindClient ایجاد نمایید.
  2. در پنجره MainWindow مانند کد زیر از یک Combobox و DataGrid برای نمایش اطلاعات استفاده نمایید
        <Window x:Class="MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Northwind Orders" Height="335" Width="425" 
            Name="OrdersWindow" Loaded="Window1_Loaded">
        <Grid Name="orderItemsGrid">
            <ComboBox DisplayMemberPath="Order_ID" ItemsSource="{Binding}"
                      IsSynchronizedWithCurrentItem="true" 
                      Height="23" Margin="92,12,198,0" Name="comboBoxOrder" VerticalAlignment="Top"/>
            <DataGrid ItemsSource="{Binding Path=Order_Details}"  
                      CanUserAddRows="False" CanUserDeleteRows="False"  
                      Name="orderItemsDataGrid" Margin="34,46,34,50"
                      AutoGenerateColumns="False">
                <DataGrid.Columns>
                    <DataGridTextColumn  Header="Product" Binding="{Binding Product_ID, Mode=OneWay}" />
                    <DataGridTextColumn  Header="Quantity" Binding="{Binding Quantity, Mode=TwoWay}" />
                    <DataGridTextColumn  Header="Price" Binding="{Binding UnitPrice, Mode=TwoWay}" />
                    <DataGridTextColumn  Header="Discount" Binding="{Binding Discount, Mode=TwoWay}" />                
                </DataGrid.Columns>     
            </DataGrid>
            <Label Height="28" Margin="34,12,0,0" Name="orderLabel" VerticalAlignment="Top" 
                   HorizontalAlignment="Left" Width="65">Order:</Label>
            <StackPanel Name="Buttons" Orientation="Horizontal" HorizontalAlignment="Right" 
                        Height="40" Margin="0,257,22,0">
                <Button Height="23" HorizontalAlignment="Right" Margin="0,0,12,12" 
                    Name="buttonSave" VerticalAlignment="Bottom" Width="75" 
                        Click="buttonSaveChanges_Click">Save Changes
                </Button>
                <Button Height="23" Margin="0,0,12,12" 
                    Name="buttonClose" VerticalAlignment="Bottom" Width="75" 
                        Click="buttonClose_Click">Close</Button>
            </StackPanel>
        </Grid>
    </Window>

  3. برای ارجاع به wcf data service بر روی پروژه راست کلیک و گزینه Add Service Reference را انتخاب نمایید در پنجره باز شده گزینه Discover را انتخاب تا سرویس را یافته و نام Namespase را Northwind بگذارید.
  4. حال مانند کد زیر یک شی از مدل NorthwindEntities با آدرس وب سرویس ایجاد نموده ایم و نتیحه کوئری با استفاده از کلاس DataServiceCollection به DataContext گرید انتصاب داده ایم که البته پیش فرض آن آشنایی با DataBinding در WPF است.
            private NorthwindEntities context;
            private string customerId = "ALFKI";
            private Uri svcUri = new Uri("http://localhost:8358/Northwind.svc");
    
            private void Window1_Loaded(object sender, RoutedEventArgs e)
            {
                try
                {
                    context = new NorthwindEntities(svcUri);
                    var ordersQuery = from o in context.Orders.Expand("Order_Details")
                                      where o.Customers.Customer_ID == customerId
                                      select o;
                    DataServiceCollection<Orders> customerOrders = new DataServiceCollection<Orders>(ordersQuery);
                    this.orderItemsGrid.DataContext = customerOrders;
                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.ToString());
                }
            }
    
  5. با صدا زدن تابع SaveChanges مدل می‌توانید تغییرات را در پایگاه داده ذخیره نمایید.
    private void buttonSaveChanges_Click(object sender, RoutedEventArgs e)
    {
        try
        {
            context.SaveChanges();
        }
        catch (DataServiceRequestException ex)
        {
            MessageBox.Show(ex.ToString());
    
        }
    }

  1. برنامه را اجرا نمایید تا خروجی کار را مشاهده نمایید. مقادیر Quantity را تغییر دهید و دکمه Save Changes را انتخاب تا تغییرات دخیره شود.
در اینجا در یک برنامه ویندوزی استفاده از WCF Data Service را تست نمودیم اما براحتی به همین شیوه در یک برنامه وب نیز قابل استفاده است.