نظرات مطالب
ایجاد سرویس چندلایه‎ی WCF با Entity Framework در قالب پروژه - 9
- در قسمت هفتم، تنظیمات برنامه‌های وب آن بحث شده. پابلیش آن کپی و پیست پروژه در یک دایرکتوری مجازی در IIS است (یعنی فرقی با راه اندازی یک وب سایت معمولی ASP.NET نداره در اساس).
- اگر به خطایی برخوردید در این بین، عین خطا را ارسال کنید تا بیشتر بشود بحث کرد.
مطالب
یک دست سازی ی و ک دریافتی در صفحات وب

با استفاده از jQuery ، تحت نظر قرار دادن ورودی‌های کاربران در تمام فیلدهای ورودی صفحه کار ساده‌ای است؛ اما جایگزینی مثلا ی فارسی با ی عربی و برعکس درست در لحظه‌ی تایپ آن‌ها کار ساده‌ای نیست و هر مرورگر روش خاص خودش را دارد و بعضی‌ها هم اصلا اجازه‌ی تغییر رخدادهای رسیده را نمی‌دهند.
اسکریپت زیر کار یک دست سازی ی و ک دریافتی در صفحات وب را انجام می‌دهد (برای مثال اگر کاربر ی تایپ کند به صورت خودکار به ی تبدیل می‌شود):
// <![CDATA[
function substituteCharInFireFox(charCode, e) {
var keyEvt = document.createEvent("KeyboardEvent");
keyEvt.initKeyEvent("keypress", true, true, null, false, false, false, false, 0, charCode);
e.target.dispatchEvent(keyEvt);
e.preventDefault();
}

function substituteCharInChrome(charCode, e) {
//it does not work yet! /*$.browser.webkit*/
//https://bugs.webkit.org/show_bug.cgi?id=16735
var keyEvt = document.createEvent("KeyboardEvent");
keyEvt.initKeyboardEvent("keypress", true, true, null, false, false, false, false, 0, charCode);
e.target.dispatchEvent(keyEvt);
e.preventDefault();
}

function insertAtCaret(myValue, e) {
var obj = e.target;
var startPos = obj.selectionStart;
var endPos = obj.selectionEnd;
var scrollTop = obj.scrollTop;
obj.value = obj.value.substring(0, startPos) + myValue + obj.value.substring(endPos, obj.value.length);
obj.focus();
obj.selectionStart = startPos + myValue.length;
obj.selectionEnd = startPos + myValue.length;
obj.scrollTop = scrollTop;
e.preventDefault();
}

$(document).ready(function () {
$(document).keypress(function (e) {

var keyCode = e.keyCode ? e.keyCode : e.which;
var arabicYeCharCode = 1610;
var persianYeCharCode = 1740;
var arabicKeCharCode = 1603;
var persianKeCharCode = 1705;

if ($.browser.msie) {
switch (keyCode) {
case arabicYeCharCode:
event.keyCode = persianYeCharCode;
break;
case arabicKeCharCode:
event.keyCode = persianKeCharCode;
break;
}
}
else if ($.browser.mozilla) {
switch (keyCode) {
case arabicYeCharCode:
substituteCharInFireFox(persianYeCharCode, e);
break;
case arabicKeCharCode:
substituteCharInFireFox(persianKeCharCode, e);
break;
}
}
else {
switch (keyCode) {
case arabicYeCharCode:
insertAtCaret(String.fromCharCode(persianYeCharCode), e);
break;
case arabicKeCharCode:
insertAtCaret(String.fromCharCode(persianKeCharCode), e);
break;
}
}
});
});
// ]]>
تابع substituteCharInChrome قرار است در نگارش‌های آتی گوگل کروم کار کند! کروم فعلا هر نوع شبیه سازی فشرده شدن کلیدهای صفحه کلید را به صفر ترجمه می‌کند. به همین جهت از روش insertAtCaret در مورد آن استفاده شد. هر دو تابع substituteChar ذکر شده در مورد فایرفاکس و کروم و یا روش ساده IE (با توجه به اینکه keyCode در IE فقط خواندنی نیست)، با اپرا کار نمی‌کنند!

  • دریافت این اسکریپت: (+)
  • نسخه‌ی فشرده شده آن: (+)
  • یک پروژه‌ی ساده ASP.NET نمونه در مورد استفاده از آن: (+)

این اسکریپت با IE، فایرفاکس، اپرا ، کروم گوگل و Safari شرکت اپل سازگار است و تفاوتی هم نمی‌کند که در یک html ساده استفاده شود یا در صفحات ASP ، PHP ، ASP.NET ، JSP یا هر چی!


مطالب مشابه:

مطالب
Blazor 5x - قسمت 25 - تهیه API مخصوص Blazor WASM - بخش 2 - تامین پایه‌ی اعتبارسنجی و احراز هویت
در این قسمت می‌خواهیم پایه‌ی اعتبارسنجی و احراز هویت سمت سرور برنامه‌ی کلاینت Blazor WASM را بر اساس JWT یکپارچه با ASP.NET Core Identity تامین کنیم. اگر با JWT آشنایی ندارید، نیاز است مطالب زیر را در ابتدا مطالعه کنید:
- «معرفی JSON Web Token»

توسعه‌ی IdentityUser

در قسمت‌های 21 تا 23، روش نصب و یکپارچگی ASP.NET Core Identity را با یک برنامه‌ی Blazor Server بررسی کردیم. در پروژه‌ی Web API جاری هم از قصد داریم از ASP.NET Core Identity استفاده کنیم؛ البته بدون نصب UI پیش‌فرض آن. به همین جهت فقط از ApplicationDbContext آن برنامه که از IdentityDbContext مشتق شده و همچنین قسمتی از تنظیمات سرویس‌های ابتدایی آن که در قسمت قبل بررسی کردیم، در اینجا استفاده خواهیم کرد.
IdentityUser پیش‌فرض که معرف موجودیت کاربران یک سیستم مبتنی بر ASP.NET Core Identity است، برای ثبت نام یک کاربر، فقط به ایمیل و کلمه‌ی عبور او نیاز دارد که نمونه‌ای از آن‌را در حین معرفی «ثبت کاربر ادمین Identity» بررسی کردیم. اکنون می‌خواهیم این موجودیت پیش‌فرض را توسعه داده و برای مثال نام کاربر را نیز به آن اضافه کنیم. برای اینکار فایل جدید BlazorServer\BlazorServer.Entities\ApplicationUser .cs را به پروژه‌ی Entities با محتوای زیر اضافه می‌کنیم:
using Microsoft.AspNetCore.Identity;

namespace BlazorServer.Entities
{
    public class ApplicationUser : IdentityUser
    {
        public string Name { get; set; }
    }
}
برای توسعه‌ی IdentityUser پیش‌فرض، فقط کافی است از آن ارث‌بری کرده و خاصیت جدیدی را به خواص موجود آن اضافه کنیم. البته برای شناسایی IdentityUser، نیاز است بسته‌ی نیوگت Microsoft.AspNetCore.Identity.EntityFrameworkCore را نیز در این پروژه نصب کرد.
اکنون که یک ApplicationUser سفارشی را ایجاد کردیم، نیازی نیست تا DbSet خاص آن‌را به ApplicationDbContext برنامه اضافه کنیم. برای معرفی آن به برنامه ابتدا باید به فایل BlazorServer\BlazorServer.DataAccess\ApplicationDbContext.cs مراجعه کرده و نوع IdentityUser را به IdentityDbContext، از طریق آرگومان جنریکی که می‌پذیرد، معرفی کنیم:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
زمانیکه این آرگومان جنریک قید نشود، از همان نوع IdentityUser پیش‌فرض خودش استفاده می‌کند.
پس از این تغییر، در فایل BlazorWasm\BlazorWasm.WebApi\Startup.cs نیز باید ApplicationUser را به عنوان نوع جدید کاربران، معرفی کرد:
namespace BlazorWasm.WebApi
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddIdentity<ApplicationUser, IdentityRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();
  
            // ...

پس از این تغییرات، باید از طریق خط فرمان به پوشه‌ی BlazorServer.DataAccess وارد شد و دستورات زیر را جهت ایجاد و اعمال Migrations متناظر با تغییرات فوق، اجرا کرد. چون در این دستورات اینبار پروژه‌ی آغازین، به پروژه‌ی Web API اشاره می‌کند، باید بسته‌ی نیوگت Microsoft.EntityFrameworkCore.Design را نیز به پروژه‌ی آغازین اضافه کرد، تا بتوان آن‌ها را با موفقیت به پایان رساند:
dotnet tool update --global dotnet-ef --version 5.0.4
dotnet build
dotnet ef migrations --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ add AddNameToAppUser --context ApplicationDbContext
dotnet ef --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ database update --context ApplicationDbContext


ایجاد مدل‌های ثبت نام

در ادامه می‌خواهیم کنترلری را ایجاد کنیم که کار ثبت نام و لاگین را مدیریت می‌کند. برای این منظور باید بتوان از کاربر، اطلاعاتی مانند نام کاربری و کلمه‌ی عبور او را دریافت کرد و پس از پایان عملیات نیز نتیجه‌ی آن‌را بازگشت داد. به همین جهت دو مدل زیر را جهت مدیریت قسمت ثبت نام، به پروژه‌ی BlazorServer.Models اضافه می‌کنیم:
using System.ComponentModel.DataAnnotations;

namespace BlazorServer.Models
{
    public class UserRequestDTO
    {
        [Required(ErrorMessage = "Name is required")]
        public string Name { get; set; }

        [Required(ErrorMessage = "Email is required")]
        [RegularExpression("^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$",
                ErrorMessage = "Invalid email address")]
        public string Email { get; set; }

        public string PhoneNo { get; set; }

        [Required(ErrorMessage = "Password is required.")]
        [DataType(DataType.Password)]
        public string Password { get; set; }

        [Required(ErrorMessage = "Confirm password is required")]
        [DataType(DataType.Password)]
        [Compare("Password", ErrorMessage = "Password and confirm password is not matched")]
        public string ConfirmPassword { get; set; }
    }
}
و مدل پاسخ عملیات ثبت نام:
    public class RegistrationResponseDTO
    {
        public bool IsRegistrationSuccessful { get; set; }

        public IEnumerable<string> Errors { get; set; }
    }


ایجاد و تکمیل کنترلر Account، جهت ثبت نام کاربران

در ادامه نیاز داریم تا جهت ارائه‌ی امکانات اعتبارسنجی و احراز هویت کاربران، کنترلر جدید Account را به پروژه‌ی Web API اضافه کنیم:
using System;
using BlazorServer.Entities;
using BlazorServer.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
using System.Linq;
using BlazorServer.Common;

namespace BlazorWasm.WebApi.Controllers
{
    [Route("api/[controller]/[action]")]
    [ApiController]
    [Authorize]
    public class AccountController : ControllerBase
    {
        private readonly SignInManager<ApplicationUser> _signInManager;
        private readonly UserManager<ApplicationUser> _userManager;
        private readonly RoleManager<IdentityRole> _roleManager;

        public AccountController(SignInManager<ApplicationUser> signInManager,
            UserManager<ApplicationUser> userManager,
            RoleManager<IdentityRole> roleManager)
        {
            _roleManager = roleManager ?? throw new ArgumentNullException(nameof(roleManager));
            _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager));
            _signInManager = signInManager ?? throw new ArgumentNullException(nameof(signInManager));
        }

        [HttpPost]
        [AllowAnonymous]
        public async Task<IActionResult> SignUp([FromBody] UserRequestDTO userRequestDTO)
        {
            var user = new ApplicationUser
            {
                UserName = userRequestDTO.Email,
                Email = userRequestDTO.Email,
                Name = userRequestDTO.Name,
                PhoneNumber = userRequestDTO.PhoneNo,
                EmailConfirmed = true
            };
            var result = await _userManager.CreateAsync(user, userRequestDTO.Password);
            if (!result.Succeeded)
            {
                var errors = result.Errors.Select(e => e.Description);
                return BadRequest(new RegistationResponseDTO { Errors = errors, IsRegistrationSuccessful = false });
            }

            var roleResult = await _userManager.AddToRoleAsync(user, ConstantRoles.Customer);
            if (!roleResult.Succeeded)
            {
                var errors = result.Errors.Select(e => e.Description);
                return BadRequest(new RegistationResponseDTO { Errors = errors, IsRegistrationSuccessful = false });
            }

            return StatusCode(201); // Created
        }
    }
}
- در اینجا اولین اکشن متد کنترلر Account را مشاهده می‌کنید که کار ثبت نام یک کاربر را انجام می‌دهد. نمونه‌‌ای از این کدها پیشتر در قسمت 23 این سری، زمانیکه کاربر جدیدی را با نقش ادمین تعریف کردیم، مشاهده کرده‌اید.
- در تعریف ابتدایی این کنترلر، ویژگی‌های زیر ذکر شده‌اند:
[Route("api/[controller]/[action]")]
[ApiController]
[Authorize]
می‌خواهیم مسیریابی آن با api/ شروع شود و به صورت خودکار بر اساس نام کنترلر و نام اکشن متدها، تعیین گردد. همچنین نمی‌خواهیم مدام کدهای بررسی معتبر بودن ModelState را در کنترلرها قرار دهیم. به همین جهت از ویژگی ApiController استفاده شده تا اینکار را به صورت خودکار انجام دهد. قرار دادن فیلتر Authorize بر روی یک کنترلر سبب می‌شود تا تمام اکشن متدهای آن به کاربران اعتبارسنجی شده محدود شوند؛ مگر اینکه عکس آن به صورت صریح توسط فیلتر AllowAnonymous مشخص گردد. نمونه‌ی آن‌را در اکشن متد عمومی SignUp در اینجا مشاهده می‌کنید.

تا اینجا اگر برنامه را اجرا کنیم، می‌توان با استفاده از Swagger UI، آن‌را آزمایش کرد:


که با اجرای آن، برای نمونه به خروجی زیر می‌رسیم:


که عنوان می‌کند کلمه‌ی عبور باید حداقل دارای یک عدد و یک حرف بزرگ باشد. پس از اصلاح آن، status-code=201 را دریافت خواهیم کرد.
و اگر سعی کنیم همین کاربر را مجددا ثبت نام کنیم، با خطای زیر مواجه خواهیم شد:



ایجاد مدل‌های ورود به سیستم

در پروژه‌ی Web API، از UI پیش‌فرض ASP.NET Core Identity استفاده نمی‌کنیم. به همین جهت نیاز است مدل‌های قسمت لاگین را به صورت زیر تعریف کنیم:
using System.ComponentModel.DataAnnotations;

namespace BlazorServer.Models
{
    public class AuthenticationDTO
    {
        [Required(ErrorMessage = "UserName is required")]
        [RegularExpression("^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$",
            ErrorMessage = "Invalid email address")]
        public string UserName { get; set; }

        [Required(ErrorMessage = "Password is required.")]
        [DataType(DataType.Password)]
        public string Password { get; set; }
    }
}
به همراه مدل پاسخ ارائه شده‌ی حاصل از عملیات لاگین:
using System.Collections.Generic;

namespace BlazorServer.Models
{
    public class AuthenticationResponseDTO
    {
        public bool IsAuthSuccessful { get; set; }

        public string ErrorMessage { get; set; }

        public string Token { get; set; }

        public UserDTO UserDTO { get; set; }
    }

    public class UserDTO
    {
        public string Id { get; set; }
        public string Name { get; set; }
        public string Email { get; set; }
        public string PhoneNo { get; set; }
    }
}
که در اینجا اگر عملیات لاگین با موفقیت به پایان برسد، یک توکن، به همراه اطلاعاتی از کاربر، به سمت کلاینت ارسال خواهد شد؛ در غیر اینصورت، متن خطای مرتبط بازگشت داده می‌شود.


ایجاد مدل مشخصات تولید JSON Web Token

پس از لاگین موفق، نیاز است یک JWT را تولید کرد و در اختیار کلاینت قرار داد. مشخصات ابتدایی تولید این توکن، توسط مدل زیر تعریف می‌شود:
namespace BlazorServer.Models
{
    public class BearerTokensOptions
    {
        public string Key { set; get; }

        public string Issuer { set; get; }

        public string Audience { set; get; }

        public int AccessTokenExpirationMinutes { set; get; }
    }
}
که شامل کلید امضای توکن، مخاطبین، صادر کننده و مدت زمان اعتبار آن به دقیقه‌است و به صورت زیر در فایل BlazorWasm\BlazorWasm.WebApi\appsettings.json تعریف می‌شود:
{
  "BearerTokens": {
    "Key": "This is my shared key, not so secret, secret!",
    "Issuer": "https://localhost:5001/",
    "Audience": "Any",
    "AccessTokenExpirationMinutes": 20
  }
}
پس از این تعاریف، جهت دسترسی به مقادیر آن توسط سیستم تزریق وابستگی‌ها، مدخل آن‌را به صورت زیر به کلاس آغازین Web API اضافه می‌کنیم:
namespace BlazorWasm.WebApi
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddOptions<BearerTokensOptions>().Bind(Configuration.GetSection("BearerTokens"));
            // ...

ایجاد سرویسی برای تولید JSON Web Token

سرویس زیر به کمک سرویس توکار UserManager مخصوص Identity و مشخصات ابتدایی توکنی که معرفی کردیم، کار تولید یک JWT را انجام می‌دهد:
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using BlazorServer.Entities;
using BlazorServer.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;

namespace BlazorServer.Services
{

    public interface ITokenFactoryService
    {
        Task<string> CreateJwtTokensAsync(ApplicationUser user);
    }

    public class TokenFactoryService : ITokenFactoryService
    {
        private readonly UserManager<ApplicationUser> _userManager;
        private readonly BearerTokensOptions _configuration;

        public TokenFactoryService(
                UserManager<ApplicationUser> userManager,
                IOptionsSnapshot<BearerTokensOptions> bearerTokensOptions)
        {
            _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager));
            if (bearerTokensOptions is null)
            {
                throw new ArgumentNullException(nameof(bearerTokensOptions));
            }
            _configuration = bearerTokensOptions.Value;
        }

        public async Task<string> CreateJwtTokensAsync(ApplicationUser user)
        {
            var signingCredentials = new SigningCredentials(
                new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.Key)),
                SecurityAlgorithms.HmacSha256);
            var claims = await getClaimsAsync(user);
            var now = DateTime.UtcNow;
            var tokenOptions = new JwtSecurityToken(
                issuer: _configuration.Issuer,
                audience: _configuration.Audience,
                claims: claims,
                notBefore: now,
                expires: now.AddMinutes(_configuration.AccessTokenExpirationMinutes),
                signingCredentials: signingCredentials);
            return new JwtSecurityTokenHandler().WriteToken(tokenOptions);
        }

        private async Task<List<Claim>> getClaimsAsync(ApplicationUser user)
        {
            string issuer = _configuration.Issuer;
            var claims = new List<Claim>
            {
                // Issuer
                new Claim(JwtRegisteredClaimNames.Iss, issuer, ClaimValueTypes.String, issuer),
                // Issued at
                new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64, issuer),
                new Claim(ClaimTypes.Name, user.Email, ClaimValueTypes.String, issuer),
                new Claim(ClaimTypes.Email, user.Email, ClaimValueTypes.String, issuer),
                new Claim("Id", user.Id, ClaimValueTypes.String, issuer),
                new Claim("DisplayName", user.Name, ClaimValueTypes.String, issuer),
            };

            var roles = await _userManager.GetRolesAsync(user);
            foreach (var role in roles)
            {
                claims.Add(new Claim(ClaimTypes.Role, role, ClaimValueTypes.String, issuer));
            }
            return claims;
        }
    }
}
کار افزودن نقش‌های یک کاربر به توکن او، به کمک متد userManager.GetRolesAsync انجام شده‌است. نمونه‌ای از این سرویس را پیشتر در مطلب «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» مشاهده کرده‌اید. البته در آنجا از سیستم Identity برای تامین نقش‌های کاربران استفاده نمی‌شود و مستقل از آن عمل می‌کند.

در آخر، این سرویس را به صورت زیر به لیست سرویس‌های ثبت شده‌ی پروژه‌ی Web API، اضافه می‌کنیم:
namespace BlazorWasm.WebApi
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<ITokenFactoryService, TokenFactoryService>();
            // ...


تکمیل کنترلر Account جهت لاگین کاربران

پس از ثبت نام کاربران، اکنون می‌خواهیم امکان لاگین آن‌ها را نیز فراهم کنیم:
namespace BlazorWasm.WebApi.Controllers
{
    [Route("api/[controller]/[action]")]
    [ApiController]
    [Authorize]
    public class AccountController : ControllerBase
    {
        private readonly SignInManager<ApplicationUser> _signInManager;
        private readonly UserManager<ApplicationUser> _userManager;
        private readonly ITokenFactoryService _tokenFactoryService;

        public AccountController(
            SignInManager<ApplicationUser> signInManager,
            UserManager<ApplicationUser> userManager,
            ITokenFactoryService tokenFactoryService)
        {
            _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager));
            _signInManager = signInManager ?? throw new ArgumentNullException(nameof(signInManager));
            _tokenFactoryService = tokenFactoryService;
        }

        [HttpPost]
        [AllowAnonymous]
        public async Task<IActionResult> SignIn([FromBody] AuthenticationDTO authenticationDTO)
        {
            var result = await _signInManager.PasswordSignInAsync(
                    authenticationDTO.UserName, authenticationDTO.Password,
                    isPersistent: false, lockoutOnFailure: false);
            if (!result.Succeeded)
            {
                return Unauthorized(new AuthenticationResponseDTO
                {
                    IsAuthSuccessful = false,
                    ErrorMessage = "Invalid Authentication"
                });
            }

            var user = await _userManager.FindByNameAsync(authenticationDTO.UserName);
            if (user == null)
            {
                return Unauthorized(new AuthenticationResponseDTO
                {
                    IsAuthSuccessful = false,
                    ErrorMessage = "Invalid Authentication"
                });
            }

            var token = await _tokenFactoryService.CreateJwtTokensAsync(user);
            return Ok(new AuthenticationResponseDTO
            {
                IsAuthSuccessful = true,
                Token = token,
                UserDTO = new UserDTO
                {
                    Name = user.Name,
                    Id = user.Id,
                    Email = user.Email,
                    PhoneNo = user.PhoneNumber
                }
            });
        }
    }
}
در اکشن متد جدید لاگین، اگر عملیات ورود به سیستم با موفقیت انجام شود، با استفاده از سرویس Token Factory که آن‌را پیشتر ایجاد کردیم، توکن مخصوصی را به همراه اطلاعاتی از کاربر، به سمت برنامه‌ی کلاینت بازگشت می‌دهیم.

تا اینجا اگر برنامه را اجرا کنیم، می‌توان در قسمت ورود به سیستم، برای نمونه مشخصات کاربر ادمین را وارد کرد:


و پس از اجرای درخواست، به خروجی زیر می‌رسیم:


که در اینجا JWT تولید شده‌ی به همراه قسمتی از مشخصات کاربر، در خروجی نهایی مشخص است. می‌توان محتوای این توکن را در سایت jwt.io مورد بررسی قرار داد که به این خروجی می‌رسیم و حاوی claims تعریف شده‌است:
{
  "iss": "https://localhost:5001/",
  "iat": 1616396383,
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "vahid@dntips.ir",
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": "vahid@dntips.ir",
  "Id": "582855fb-e95b-45ab-b349-5e9f7de40c0c",
  "DisplayName": "vahid@dntips.ir",
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin",
  "nbf": 1616396383,
  "exp": 1616397583,
  "aud": "Any"
}


تنظیم Web API برای پذیرش و پردازش JWT ها

تا اینجا پس از لاگین، یک JWT را در اختیار کلاینت قرار می‌دهیم. اما اگر کلاینت این JWT را به سمت سرور ارسال کند، اتفاق خاصی رخ نخواهد داد و توسط آن، شیء User قابل دسترسی در یک اکشن متد، به صورت خودکار تشکیل نمی‌شود. برای رفع این مشکل، ابتدا بسته‌ی جدید نیوگت Microsoft.AspNetCore.Authentication.JwtBearer را به پروژه‌ی Web API اضافه می‌کنیم، سپس به کلاس آغازین پروژه‌ی Web API مراجعه کرده و آن‌را به صورت زیر تکمیل می‌کنیم:
namespace BlazorWasm.WebApi
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            var bearerTokensSection = Configuration.GetSection("BearerTokens");
            services.AddOptions<BearerTokensOptions>().Bind(bearerTokensSection);
            // ...

            var apiSettings = bearerTokensSection.Get<BearerTokensOptions>();
            var key = Encoding.UTF8.GetBytes(apiSettings.Key);
            services.AddAuthentication(opt =>
            {
                opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                opt.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(cfg =>
            {
                cfg.RequireHttpsMetadata = false;
                cfg.SaveToken = true;
                cfg.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(key),
                    ValidateAudience = true,
                    ValidateIssuer = true,
                    ValidAudience = apiSettings.Audience,
                    ValidIssuer = apiSettings.Issuer,
                    ClockSkew = TimeSpan.Zero,
                    ValidateLifetime = true
                };
            });

            // ...
در اینجا در ابتدا اعتبارسنجی از نوع Jwt تعریف شده‌است و سپس پردازش کننده و وفق دهنده‌ی آن‌را به سیستم اضافه کرده‌ایم تا توکن‌های دریافتی از هدرهای درخواست‌های رسیده را به صورت خودکار پردازش و تبدیل به Claims شیء User یک اکشن متد کند.


افزودن JWT به تنظیمات Swagger

هر کدام از اکشن متدهای کنترلرهای Web API برنامه که مزین به فیلتر Authorize باشد‌، در Swagger UI با یک قفل نمایش داده می‌شود. در این حالت می‌توان این UI را به نحو زیر سفارشی سازی کرد تا بتواند JWT را دریافت و به سمت سرور ارسال کند:
namespace BlazorWasm.WebApi
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            // ...

            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "BlazorWasm.WebApi", Version = "v1" });
                c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
                {
                    In = ParameterLocation.Header,
                    Description = "Please enter the token in the field",
                    Name = "Authorization",
                    Type = SecuritySchemeType.ApiKey
                });
                c.AddSecurityRequirement(new OpenApiSecurityRequirement {
                    {
                        new OpenApiSecurityScheme
                        {
                            Reference = new OpenApiReference
                            {
                                Type = ReferenceType.SecurityScheme,
                                Id = "Bearer"
                            }
                        },
                        new string[] { }
                    }
                });
            });
        }

        // ...


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-25.zip
مطالب
خلاصه اشتراک‌های روز پنج شنبه 1390/06/31
اشتراک‌ها
یک Business Apps ساخته شده با Asp.net Core Mvc و , TypeScript

یک CMS تجاری بزرگ با قابلیت‌های زیر

A modular, service based web application model
Code generator to produce initial services / user interface code for an SQL table
T4 based code generation on server to reference script widgets with intellisense / compile time validation
T4 based code generation to provide compile time type safety and intellisense while calling AJAX services from script side.
An attribute based form definition system (prepare UI in server side with a simple C# class)
Automatic seamless data-binding through form definitions (form <-> entity <-> service).
Caching Helpers (Local / Distributed)
Automatic cache validation
Configuration System (storage medium independent. store settings in database, file, whatever...)
Simple Logging
Reporting (reports just provide data, has no dependency on rendering, similar to MVC)
Script bundling, minification (making use of Node / UglifyJS / CleanCSS) and content versioning (no more F5 / clear browser cache)
Fluent SQL Builder (SELECT/INSERT/UPDATE/DELETE)
Micro ORM (also Dapper is integrated)
Customizable handlers for REST like services that work by reusing information in entity classes and do automatic validation.
Attribute based navigation menu
UI Localization (store localized texts in json files, embedded resource, database, in memory class, anywhere)
Data Localization (using an extension table mechanism helps to localize even data entered by users, like lookup tables)
Script widget system (inspired by jQueryUI but more suitable for C# code)
Client side and server side validation (based on jQuery validate plugin, but abstracts dependency)
Audit logging (where CDC is not available)
System for data based integration tests
Dynamic scripts
Script side templates 
یک Business Apps ساخته شده با Asp.net Core Mvc و , TypeScript
مطالب
Globalization در ASP.NET MVC - قسمت ششم
در قسمت قبل ساختار اصلی و پیاده‌سازی ابتدایی یک پرووایدر سفارشی دیتابیسی شرح داده شد. در این قسمت ادامه بحث و مطالب پیشرفته‌تر آورده شده است.

تولید یک پرووایدر منابع دیتابیسی - بخش دوم
در بخش دوم این سری مطلب، ساختار دیتابیس و مباحث پیشرفته پیاده‌سازی کلاس‌های نشان داده‌شده در بخش اول در قسمت قبل شرح داده می‌شود. این مباحث شامل نحوه کش صحیح و بهینه داده‌های دریافتی از دیتابیس، پیاده‌سازی فرایند fallback، و پیاده‌سازی مناسب کلاس DbResourceManager برای مدیریت کل عملیات است.
 
ساختار دیتابیس
برای پیاده‌سازی منابع دیتابیسی روش‌های مختلفی برای آرایش جداول جهت ذخیره انواع ورودی‌ها می‌توان درنظر گرفت.
مثلا درصورتی‌که حجم و تعداد منابع بسیار باشد و نیز منابع دیتابیسی به اندازه کافی در دسترس باشد، می‌توان به ازای هر منبع یک جدول درنظر گرفت.
یا درصورتیکه منابع داده‌ای محدودتر باشند می‌توان به ازای هر کالچر یک جدول درنظر گرفت و تمام منابع مربوط به یک کالچر را درون یک جدول ذخیره کرد. درهرصورت نحوه انتخاب آرایش جداول منابع کاملا بستگی به شرایط کاری و سلایق برنامه‌نویسی دارد.
برای مطلب جاری به عنوان یک راه‌حل ساده و کارآمد برای پروژه‌های کوچک و متوسط، تمام ورودی‌های منابع درون یک جدول با ساختاری مانند زیر ذخیره می‌شود:


نام این جدول را با درنظر گرفتن شرایط موجود می‌توان Resources گذاشت.

ستون Name برای ذخیره نام منبع درنظر گرفته شده است. این نام برابر نام منابع درخواستی در سیستم مدیریت منابع ASP.NET است که درواقع برابر همان نام فایل منبع اما بدون پسوند resx. است.

ستون Key برای نگهداری کلید ورودی منبع استفاده می‌شود که دقیقا برابر همان مقداری است که درون فایلهای resx. ذخیره می‌شود. 

ستون Culture برای ذخیره کالچر ورودی منبع به کار می‌رود. این مقدار می‌تواند برای کالچر پیش‌فرض برنامه برابر رشته خالی باشد. 

ستون Value نیز برای نگهداری مقدار ورودی منبع استفاده می‌شود. 

برای ستون Id می‌توان از GUID نیز استفاده کرد. در اینجا برای راحتی کار از نوع داده bigint و خاصیت Identity برای تولید خودکار آن در Sql Server استفاده شده است.

نکته: برای امنیت بیشتر می‌توان یک Unique Constraint بر روی سه فیلد Name و Key و Culture اعمال کرد.

برای نمونه به تصویر زیر که ذخیره تعدای ورودی منبع را درون جدول Resources نمایش می‌دهد دقت کنید:

 

اصلاح کلاس DbResourceProviderFactory

برای ذخیره منابع محلی، جهت اطمینان از یکسان بودن نام منبع، متد مربوطه در کلاس DbResourceProviderFactory باید به‌صورت زیر تغییر کند:

public override IResourceProvider CreateLocalResourceProvider(string virtualPath)
{
  if (!string.IsNullOrEmpty(virtualPath))
  {
    virtualPath = virtualPath.Remove(0, virtualPath.IndexOf('/') + 1); // removes everything from start to the first '/'
  }
  return new LocalDbResourceProvider(virtualPath);
}
با این تغییر مسیرهای درخواستی چون "Default.aspx/~" و یا "Default.aspx/" هر دو به صورت "Default.aspx" در می‌آیند تا با نام ذخیره شده در دیتابیس یکسان شوند.
 

ارتباط با دیتابیس

خوشبختانه برای تبادل اطلاعات با جدول بالا امروزه راه‌های زیادی وجود دارد. برای پیاده‌سازی آن مثلا می‌توان از یک اینترفیس استفاده کرد. سپس با استفاده از سازوکارهای موجود مثلا به‌کارگیری IoC، نمونه مناسبی از پیاده‌سازی اینترفیس مذبور را در اختیار برنامه قرار داد.
اما برای جلوگیری از پیچیدگی بیش از حد و دور شدن از مبحث اصلی، برای پیاده‌سازی فعلی از EF Code First به صورت مستقیم در پروژه استفاده شده است که سری آموزشی کاملی از آن در همین سایت وجود دارد.

پس از پیاده‌سازی کلاس‌های مرتبط برای استفاده از EF Code First، از کلاس ResourceData که در بخش اول نیز نشان داده شده بود، برای کپسوله کردن ارتباط با داده‌ها استفاده می‌شود که نمونه‌ای ابتدایی از آن در زیر آورده شده است:

using System.Collections.Generic;
using System.Linq;
using DbResourceProvider.Models;

namespace DbResourceProvider.Data
{
  public class ResourceData
  {
    private readonly string _resourceName;
    public ResourceData(string resourceName)
    {
      _resourceName = resourceName;
    }
    public Resource GetResource(string resourceKey, string culture)
    {
      using (var data = new TestContext())
      {
        return data.Resources.SingleOrDefault(r => r.Name == _resourceName && r.Key == resourceKey && r.Culture == culture);
      }
    }
    public List<Resource> GetResources(string culture)
    {
      using (var data = new TestContext())
      {
        return data.Resources.Where(r => r.Name == _resourceName && r.Culture == culture).ToList();
      }
    }
  }
}
کلاس فوق نسبت به نمونه‌ای که در قسمت قبل نشان داده شد کمی فرق دارد. بدین صورت که برای راحتی بیشتر نام منبع درخواستی به جای پارامتر متدها، در اینجا به عنوان پارامتر کانستراکتور وارد می‌شود.

نکته: درصورتی‌که این کلاس‌ها در پروژه‌ای جداگانه قرار دارند، باید ConnectionString مربوطه در فایل کانفیگ برنامه مقصد نیز تنظیم شود.

کش کردن ورودی‌ها
برای کش کردن ورودی‌ها این نکته را که قبلا هم به آن اشاره شده بود باید درنظر داشت:
پس از اولین درخواست برای هر منبع، نمونه تولیدشده از پرووایدر مربوطه در حافظه سرور کش خواهد شد.
یعنی متدهای کلاس DbResourceProviderFactory به‌ازای هر منبع تنها یکبار فراخوانی می‌شود. نمونه‌های کش‌شده از پروایدرهای کلی و محلی به همراه تمام محتویاتشان (مثلا نمونه تولیدی از کلاس DbResourceManager) تا زمان Unload شدن سایت در حافظه سرور باقی می‌مانند. بنابراین عملیات کشینگ ورودی‌ها را می‌توان درون خود کلاس DbResourceManager به ازای هر منبع انجام داد.
برای کش کردن ورودی‌های هر منبع می‌توان چند روش را درپیش گرفت. روش اول این است که به ازای هر کلید درخواستی تنها ورودی مربوطه از دیتابیس فراخوانی شده و در برنامه کش شود. این روش برای حالاتی که تعداد ورودی‌ها یا تعداد درخواست‌های کلیدهای هر منبع کم باشد مناسب خواهد بود.
یکی از پیاده‌سازی این روش این است که ورودی‌ها به ازای هر کالچر ذخیره شوند. پیاده‌سازی اولیه این نوع فرایند کشینگ در کلاس DbResourceManager به صورت زیر است:
using System.Collections.Generic;
using System.Globalization;
using DbResourceProvider.Data;
namespace DbResourceProvider
{
  public class DbResourceManager
  {
    private readonly string _resourceName;
    private readonly Dictionary<string, Dictionary<string, object>> _resourceCacheByCulture;
    public DbResourceManager(string resourceName)
    {
      _resourceName = resourceName;
      _resourceCacheByCulture = new Dictionary<string, Dictionary<string, object>>();
    }
    public object GetObject(string resourceKey, CultureInfo culture)
    {
      return GetCachedObject(resourceKey, culture.Name);
    }
    private object GetCachedObject(string resourceKey, string cultureName)
    {
      if (!_resourceCacheByCulture.ContainsKey(cultureName))
        _resourceCacheByCulture.Add(cultureName, new Dictionary<string, object>());
      var cachedResource = _resourceCacheByCulture[cultureName];
      lock (this)
      {
        if (!cachedResource.ContainsKey(resourceKey))
        {
          var data = new ResourceData(_resourceName);
          var dbResource = data.GetResource(resourceKey, cultureName);
          if (dbResource == null) return null;
          var cachedResources = _resourceCacheByCulture[cultureName];
          cachedResources.Add(dbResource.Key, dbResource.Value);
        }
      }
      return cachedResource[resourceKey];
    }
  }
}
همانطور که قبلا توضیح داده شد کشِ پرووایدرهای منابع به ازای هر منبع درخواستی (و به تبع آن نمونه‌های موجود در آن مثل DbResourceManager) برعهده خود ASP.NET است. بنابراین برای کش کردن ورودی‌های درخواستی هر منبع در کلاس DbResourceManager تنها کافی است آن‌ها را درون یک متغیر محلی در سطح کلاس (فیلد) ذخیره کرد. کاری که در کد بالا در متغیر resourceCacheByCulture_ انجام شده است. در این متغیر که از نوع دیکشنری تعریف شده است کلیدهای هر عضو آن برابر نام کالچر مربوطه است. مقادیر هر عضو این دیکشنری نیز خود یک دیکشنری است که ورودی‌های منابع مربوط به کالچر مربوطه در آن ذخیره می‌شوند.
عملیات در متد GetCachedObject انجام می‌شود. همان‌طور که می‌بینید ابتدا وجود ورودی موردنظر در متغیر کشینگ بررسی می‌شود و درصورت عدم وجود، مقدار آن مستقیما از دیتابیس درخواست می‌شود. سپس این مقدار درخواستی ابتدا درون متغیر کشینگ ذخیره شده (به همراه بلاک lock) و درنهایت برگشت داده می‌شود.

نکته: کل فرایند بررسی وجود کلید در متغیر کشینگ (شرط دوم در متد GetCachedObject) درون بلاک lock قرار داده شده است تا در درخواست‌های همزمان احتمال افزودن چندباره یک کلید ازبین برود.

پیاده‌سازی دیگر این فرایند کشینگ، ذخیره ورودی‌ها براساس نام کلید به جای نام کالچر است. یعنی کلید دیکشنری اصلی نام کلید و کلید دیکشنری داخلی نام کالچر است که این روش زیاد جالب نیست.
روش دوم که بیشتر برای برنامه‌های بزرگ با ورودی‌ها و درخواست‌های زیاد به‌کار می‌رود این است که درهر بار درخواست به دیتابیس به جای دریافت تنها همان ورودی درخواستی، تمام ورودی‌های منبع و کالچر درخواستی استخراج شده و کش می‌شود تا تعداد درخواست‌های به سمت دیتابیس کاهش یابد. برای پیاده‌سازی این روش کافی است تغییرات زیر در متد GetCachedObject اعمال شود:
private object GetCachedObject(string resourceKey, string cultureName)
{
  lock (this)
  {
    if (!_resourceCacheByCulture.ContainsKey(cultureName))
    {
      _resourceCacheByCulture.Add(cultureName, new Dictionary<string, object>());
      var cachedResources = _resourceCacheByCulture[cultureName];
      var data = new ResourceData(_resourceName);
      var dbResources = data.GetResources(cultureName);
      foreach (var dbResource in dbResources)
      {
        cachedResources.Add(dbResource.Key, dbResource.Value);
      }
    }
  }
  var cachedResource = _resourceCacheByCulture[cultureName];
  return !cachedResource.ContainsKey(resourceKey) ? null : cachedResource[resourceKey];
}
دراینجا هم می‌توان به جای استفاده از نام کالچر برای کلید دیکشنری اصلی از نام کلید ورودی منبع استفاده کرد که چندان توصیه نمی‌شود.

نکته: انتخاب یکی از دو روش فوق برای فرایند کشینگ کاملا به شرایط موجود و سلیقه برنامه نویس بستگی دارد.

فرایند Fallback
درباره فرایند fallback به اندازه کافی در قسمت‌های قبلی توضیح داده شده است. برای پیاده‌سازی این فرایند ابتدا باید به نوعی به سلسله مراتب کالچرهای موجود از کالچر جاری تا کالچر اصلی و پیش فرض سیستم دسترسی پیدا کرد. برای اینکار ابتدا باید با استفاده از روشی کالچر والد یک کالچر را بدست آورد. کالچر والد کالچری است که عمومیت بیشتری نسبت به کالچر موردنظر دارد. مثلا کالچر fa، کالچر والد fa-IR است. همچنین کالچر Invariant به عنوان والد تمام کالچرها شناخته می‌شود.
خوشبختانه در کلاس CultureInfo (که در قسمت‌های قبلی شرح داده شده است) یک پراپرتی با عنوان Parent وجود دارد که کالچر والد را برمی‌گرداند.
برای رسیدن به سلسله مراتب مذبور در کلاس ResourceManager دات نت، از کلاسی با عنوان ResourceFallbackManager استفاده می‌شود. هرچند این کلاس با سطح دسترسی internal تعریف شده است اما نام‌گذاری نامناسبی دارد زیرا کاری که می‌کند به عنوان Manager هیچ ربطی ندارد. این کلاس با استفاده از یک کالچر ورودی، یک enumerator از سلسله مراتب کالچرها که در بالا صحبت شد تهیه می‌کند.
با استفاده پیاده‌سازی موجود در کلاس ResourceFallbackManager کلاسی با عنوان CultureFallbackProvider تهیه کردم که به صورت زیر است:
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
namespace DbResourceProvider
{
  public class CultureFallbackProvider : IEnumerable<CultureInfo>
  {
    private readonly CultureInfo _startingCulture;
    private readonly CultureInfo _neutralCulture;
    private readonly bool _tryParentCulture;
    public CultureFallbackProvider(CultureInfo startingCulture = null, 
                                   CultureInfo neutralCulture = null, 
                                   bool tryParentCulture = true)
    {
      _startingCulture = startingCulture ?? CultureInfo.CurrentUICulture;
      _neutralCulture = neutralCulture;
      _tryParentCulture = tryParentCulture;
    }
    #region Implementation of IEnumerable<CultureInfo>
    public IEnumerator<CultureInfo> GetEnumerator()
    {
      var reachedNeutralCulture = false;
      var currentCulture = _startingCulture;
      do
      {
        if (_neutralCulture != null && currentCulture.Name == _neutralCulture.Name)
        {
          yield return CultureInfo.InvariantCulture;
          reachedNeutralCulture = true;
          break;
        }
        yield return currentCulture;
        currentCulture = currentCulture.Parent;
      } while (_tryParentCulture && !HasInvariantCultureName(currentCulture));
      if (!_tryParentCulture || HasInvariantCultureName(_startingCulture) || reachedNeutralCulture)
        yield break;
      yield return CultureInfo.InvariantCulture;
    }
    #endregion
    #region Implementation of IEnumerable
    IEnumerator IEnumerable.GetEnumerator()
    {
      return GetEnumerator();
    }
    #endregion
    private bool HasInvariantCultureName(CultureInfo culture)
    {
      return culture.Name == CultureInfo.InvariantCulture.Name;
    }
  }
}
این کلاس که اینترفیس <IEnumerable<CultureInfo را پیاده‎سازی کرده است، سه پارامتر کانستراکتور دارد.
اولین پارامتر، کالچر جاری یا آغازین را مشخص می‌کند. این کالچری است که تولید enumerator مربوطه از آن آغاز می‌شود. درصورتی‌که این پارامتر نال باشد مقدار کالچر UI در ثرد جاری برای آن درنظر گرفته می‌شود. مقدار پیش‌فرضی که برای این پارامتر درنظر گرفته شده است، null است.
پارامتر بعدی کالچر خنثی موردنظر کاربر است. این کالچری است که درصورت رسیدن enumerator به آن کار پایان خواهد یافت. درواقع کالچر پایانی enumerator است. این پارامتر می‌تواند نال باشد. مقدار پیش‌فرضی که برای این پارامتر درنظر گرفته شده است، null است.
پارمتر آخر هم تعیین می‌کند که آیا enumerator از کالچرهای والد استفاده بکند یا خیر. مقدار پیش‌فرضی که برای این پارامتر درنظر گرفته شده است، true است. 
کار اصلی کلاس فوق در متد GetEnumerator انجام می‌شود. در این کلاس یک حلقه do-while وجود دارد که enumerator را با استفاده از کلمه کلیدی yield تولید می‌کند. در این متد ابتدا درصورت نال نبودن کالچر خنثی ورودی، بررسی می‌شود که آیا نام کالچر جاری حلقه (که در متغیر محلی currentCulture ذخیره شده است) برابر نام کالچر خنثی است یا خیر. درصورت برقراری شرط، کار این حلقه با برگشت CultureInfo.InvariantCulture پایان می‌‌یابد. InvariantCulture کالچر بدون زبان و فرهنگ و موقعیت مکانی است که درواقع به عنوان کالچر والد تمام کالچرها درنظر گرفته می‌شود. پراپرتی Name این کالچر برابر string.Empty است.
کار حلقه با برگشت مقدار کالچر جاری enumerator ادامه می‌یابد. سپس کالچر جاری با کالچر والدش مقداردهی می‌شود. شرط قسمت while حلقه تعیین می‌کند که درصورتی‌که کلاس برای استفاده از کالچرهای والد تنظیم شده باشد، تا زمانی که نام کالچر جاری برابر نام کالچر Invariant نباشد، تولید اعضای enumerator ادامه یابد.
درانتها نیز درصورتی‌که با شرایط موجود، قبلا کالچر Invariant برگشت داده نشده باشد این کالچر نیز yield می‌شود. درواقع درصورتی‌که استفاده از کالچرهای والد اجازه داده نشده باشد یا کالچر آغازین برابر کالچر Invariant باشد و یا قبلا به دلیل رسیدن به کالچر خنثی ورودی، مقدار کالچر Invariant برگشت داده شده باشد، enumerator قطع شده و عملیات پایان می‌یابد. در غیر اینصورت کالچر Invariant به عنوان کالچر پایانی برگشت داده می‌شود.
 
استفاده از CultureFallbackProvider
با استفاده از کلاس CultureFallbackProvider می‌توان عملیات جستجوی ورودی‌های درخواستی را با ترتیبی مناسب بین تمام کالچرهای موجود به انجام رسانید.
برای استفاده از این کلاس باید تغییراتی در متد GetObject کلاس DbResourceManager به صورت زیر اعمال کرد:
public object GetObject(string resourceKey, CultureInfo culture)
{
  foreach (var currentCulture in new CultureFallbackProvider(culture))
  {
    var value = GetCachedObject(resourceKey, currentCulture.Name);
    if (value != null) return value;
  }
  throw new KeyNotFoundException("The specified 'resourceKey' not found.");
}
با استفاده از یک حلقه foreach درون enumerator کلاس CultureFallbackProvider، کالچرهای موردنیاز برای fallback یافته می‌شوند. در اینجا از مقادیر پیش‌فرض دو پارامتر دیگر کانستراکتور کلاس CultureFallbackProvider استفاده شده است.
سپس به ازای هر کالچر یافته شده مقدار ورودی درخواستی بدست آمده و درصورتی‌که نال نباشد (یعنی ورودی موردنظر برای کالچر جاری یافته شود) آن مقدار برگشت داده می‌شود و درصورتی‌که نال باشد عملیات برای کالچر بعدی ادامه می‌یابد.
درصورتی‌که ورودی درخواستی یافته نشود (خروج از حلقه بدون برگشت مقداری برای ورودی منبع درخواستی) استثنای KeyNotFoundException صادر می‌شود تا کاربر را از اشتباه رخداده مطلع سازد.

آزمایش پرووایدر سفارشی

ابتدا تنظیمات موردنیاز فایل کانفیگ را که در قسمت قبل نشان داده شد، در برنامه خود اعمال کنید.

داده‌های نمونه نشان داده شده در ابتدای این مطلب را درنظر بگیرید. حال اگر در یک برنامه وب اپلیکیشن، صفحه Default.aspx در ریشه سایت حاوی دو کنترل زیر باشد:

<asp:Label ID="Label1" runat="server" meta:resourcekey="Label1" />
<asp:Label ID="Label2" runat="server" meta:resourcekey="Label2" />
خروجی برای کالچر "en-US" (معمولا پیش‌فرض، اگر تنظیمات سیستم عامل تغییر نکرده باشد) چیزی شبیه تصویر زیر خواهد بود:

سپس تغییر زیر را در فایل web.config اعمال کنید تا کالچر UI سایت به fa تغییر یابد (به بخش "uiCulture="fa دقت کنید):

<globalization uiCulture="fa" resourceProviderFactoryType = "DbResourceProvider.DbResourceProviderFactory, DbResourceProvider" />
بنابراین صفحه Default.aspx با همان داده‌های نشان داده شده در بالا به صورت زیر تغییر خواهد کرد:

می‌بینید که با توجه به عدم وجود مقداری برای Label2.Text برای کالچر fa، عملیات fallback اتفاق افتاده است.

بحث و نتیجه‎‌گیری

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

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

در ادامه درباره روش‌های مختلف نحوه پیاده‌سازی قابلیت به‌روزرسانی ورودی‌های منابع در زمان اجرا با استفاده از پرووایدرهای منابع سفارشی بحث خواهد شد. همچنین راه‌حل‌های مختلف استفاده از این پرووایدرهای سفارشی در جاهای مختلف پروژه‌های MVC شرح داده می‌شود.

البته مباحث پیشرفته‌تری چون تزریق وابستگی برای پیاده‌سازی لایه ارتباط با دیتابیس در بیرون و یا تولید یک Factory برای تزریق کامل پرووایدر منابع از بیرون نیز جای بحث و بررسی دارد.

منابع

http://weblogs.asp.net/thangchung/archive/2010/06/25/extending-resource-provider-for-soring-resources-in-the-database.aspx

http://msdn.microsoft.com/en-us/library/aa905797.aspx

http://msdn.microsoft.com/en-us/library/system.web.compilation.resourceproviderfactory.aspx

http://www.dotnetframework.org/default.aspx/.../ResourceFallbackManager@cs

http://www.codeproject.com/Articles/14190/ASP-NET-2-0-Custom-SQL-Server-ResourceProvider

http://www.west-wind.com/presentations/wwdbresourceprovider

مطالب
اعمال تزریق وابستگی‌ها به مثال رسمی ASP.NET Identity
پروژه‌ی ASP.NET Identity که نسل جدید سیستم Authentication و Authorization مخصوص ASP.NET است، دارای دو سری مثال رسمی است:
الف) مثال‌های کدپلکس
 ب) مثال نیوگت

در ادامه قصد داریم مثال نیوگت آن‌را که مثال کاملی است از نحوه‌ی استفاده از ASP.NET Identity در ASP.NET MVC، جهت اعمال الگوی واحد کار و تزریق وابستگی‌ها، بازنویسی کنیم.


پیشنیازها
- برای درک مطلب جاری نیاز است ابتدا دور‌ه‌ی مرتبطی را در سایت مطالعه کنید و همچنین با نحوه‌ی پیاده سازی الگوی واحد کار در EF Code First آشنا باشید.
- به علاوه فرض بر این است که یک پروژه‌ی خالی ASP.NET MVC 5 را نیز آغاز کرده‌اید و توسط کنسول پاور شل نیوگت، فایل‌های مثال Microsoft.AspNet.Identity.Samples را به آن افزوده‌اید:
 PM> Install-Package Microsoft.AspNet.Identity.Samples -Pre


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

همانند مطلب پیاده سازی الگوی واحد کار در EF Code First، این پروژه‌ی جدید را با چهار اسمبلی class library دیگر به نام‌های
AspNetIdentityDependencyInjectionSample.DataLayer
AspNetIdentityDependencyInjectionSample.DomainClasses
AspNetIdentityDependencyInjectionSample.IocConfig
AspNetIdentityDependencyInjectionSample.ServiceLayer
تکمیل می‌کنیم.


ساختار پروژه‌ی AspNetIdentityDependencyInjectionSample.DomainClasses

مثال Microsoft.AspNet.Identity.Samples بر مبنای primary key از نوع string است. برای نمونه کلاس کاربران آن‌را به نام ApplicationUser در فایل Models\IdentityModels.cs می‌توانید مشاهده کنید. در مطلب جاری، این نوع پیش فرض، به نوع متداول int تغییر خواهد یافت. به همین جهت نیاز است کلاس‌های ذیل را به پروژه‌ی DomainClasses اضافه کرد:
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.AspNet.Identity.EntityFramework;
 
namespace AspNetIdentityDependencyInjectionSample.DomainClasses
{
  public class ApplicationUser : IdentityUser<int, CustomUserLogin, CustomUserRole, CustomUserClaim>
  {
   // سایر خواص اضافی در اینجا
 
   [ForeignKey("AddressId")]
   public virtual Address Address { get; set; }
   public int? AddressId { get; set; }
  }
}

using System.Collections.Generic;
 
namespace AspNetIdentityDependencyInjectionSample.DomainClasses
{
  public class Address
  {
   public int Id { get; set; }
   public string City { get; set; }
   public string State { get; set; }
 
   public virtual ICollection<ApplicationUser> ApplicationUsers { set; get; }
  }
}

using Microsoft.AspNet.Identity.EntityFramework;
 
namespace AspNetIdentityDependencyInjectionSample.DomainClasses
{
  public class CustomRole : IdentityRole<int, CustomUserRole>
  {
   public CustomRole() { }
   public CustomRole(string name) { Name = name; }
 
 
  }
}

using Microsoft.AspNet.Identity.EntityFramework;
 
namespace AspNetIdentityDependencyInjectionSample.DomainClasses
{
  public class CustomUserClaim : IdentityUserClaim<int>
  {
 
  }
}

using Microsoft.AspNet.Identity.EntityFramework;
 
namespace AspNetIdentityDependencyInjectionSample.DomainClasses
{
  public class CustomUserLogin : IdentityUserLogin<int>
  {
 
  }
}

using Microsoft.AspNet.Identity.EntityFramework;
 
namespace AspNetIdentityDependencyInjectionSample.DomainClasses
{
  public class CustomUserRole : IdentityUserRole<int>
  {
 
  }
}
در اینجا نحوه‌ی تغییر primary key از نوع string را به نوع int، مشاهده می‌کنید. این تغییر نیاز به اعمال به کلاس‌های کاربران و همچنین نقش‌های آن‌ها نیز دارد. به همین جهت صرفا تغییر کلاس ابتدایی ApplicationUser کافی نیست و باید کلاس‌های فوق را نیز اضافه کرد و تغییر داد.
بدیهی است در اینجا کلاس پایه کاربران را می‌توان سفارشی سازی کرد و خواص دیگری را نیز به آن افزود. برای مثال در اینجا یک کلاس جدید آدرس تعریف شده‌است که ارجاعی از آن در کلاس کاربران نیز قابل مشاهده است.
سایر کلاس‌های مدل‌های اصلی برنامه که جداول بانک اطلاعاتی را تشکیل خواهند داد نیز در آینده به همین اسمبلی DomainClasses اضافه می‌شوند.


ساختار پروژه‌ی AspNetIdentityDependencyInjectionSample.DataLayer جهت اعمال الگوی واحد کار

اگر به همان فایل Models\IdentityModels.cs ابتدایی پروژه که اکنون کلاس ApplicationUser آن‌را به پروژه‌ی DomainClasses منتقل کرده‌ایم، مجددا مراجعه کنید، کلاس DbContext مخصوص ASP.NET Identity نیز در آن تعریف شده‌است:
 public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
این کلاس را به پروژه‌ی DataLayer منتقل می‌کنیم و از آن به عنوان DbContext اصلی برنامه استفاده خواهیم کرد. بنابراین دیگر نیازی نیست چندین DbContext در برنامه داشته باشیم. IdentityDbContext، در اصل از DbContext مشتق شده‌است.
اینترفیس IUnitOfWork برنامه، در پروژه‌ی DataLayer چنین شکلی را دارد که نمونه‌ای از آن‌را در مطلب آشنایی با نحوه‌ی پیاده سازی الگوی واحد کار در EF Code First، پیشتر ملاحظه کرده‌اید.
using System.Collections.Generic;
using System.Data.Entity;
 
namespace AspNetIdentityDependencyInjectionSample.DataLayer.Context
{
  public interface IUnitOfWork
  {
   IDbSet<TEntity> Set<TEntity>() where TEntity : class;
   int SaveAllChanges();
   void MarkAsChanged<TEntity>(TEntity entity) where TEntity : class;
   IList<T> GetRows<T>(string sql, params object[] parameters) where T : class;
   IEnumerable<TEntity> AddThisRange<TEntity>(IEnumerable<TEntity> entities) where TEntity : class;
   void ForceDatabaseInitialize();
  }
}
اکنون کلاس ApplicationDbContext منتقل شده به DataLayer یک چنین امضایی را خواهد یافت:
public class ApplicationDbContext :
  IdentityDbContext<ApplicationUser, CustomRole, int, CustomUserLogin, CustomUserRole, CustomUserClaim>,
  IUnitOfWork
{
  public DbSet<Category> Categories { set; get; }
  public DbSet<Product> Products { set; get; }
  public DbSet<Address> Addresses { set; get; }
تعریف آن باید جهت اعمال کلاس‌های سفارشی سازی شده‌ی کاربران و نقش‌های آن‌ها برای استفاده از primary key از نوع int به شکل فوق، تغییر یابد. همچنین در انتهای آن مانند قبل، IUnitOfWork نیز ذکر شده‌است. پیاده سازی کامل این کلاس را از پروژه‌ی پیوست انتهای بحث می‌توانید دریافت کنید.
کار کردن با این کلاس، هیچ تفاوتی با DbContext‌های متداول EF Code First ندارد و تمام اصول آن‌ها یکی است.

در ادامه اگر به فایل App_Start\IdentityConfig.cs مراجعه کنید، کلاس ذیل در آن قابل مشاهده‌است:
 public class ApplicationDbInitializer : DropCreateDatabaseIfModelChanges<ApplicationDbContext>
نیازی به این کلاس به این شکل نیست. آن‌را حذف کنید و در پروژه‌ی DataLayer، کلاس جدید ذیل را اضافه نمائید:
using System.Data.Entity.Migrations;
 
namespace AspNetIdentityDependencyInjectionSample.DataLayer.Context
{
  public class Configuration : DbMigrationsConfiguration<ApplicationDbContext>
  {
   public Configuration()
   {
    AutomaticMigrationsEnabled = true;
    AutomaticMigrationDataLossAllowed = true;
   }
  }
}
در این مثال، بحث migrations به حالت خودکار تنظیم شده‌است و تمام تغییرات در پروژه‌ی DomainClasses را به صورت خودکار به بانک اطلاعاتی اعمال می‌کند. تا همینجا کار تنظیم DataLayer به پایان می‌رسد.


ساختار پروژ‌ه‌ی AspNetIdentityDependencyInjectionSample.ServiceLayer

در ادامه مابقی کلاس‌‌های موجود در فایل App_Start\IdentityConfig.cs را به لایه سرویس برنامه منتقل خواهیم کرد. همچنین برای آن‌ها یک سری اینترفیس جدید نیز تعریف می‌کنیم، تا تزریق وابستگی‌ها به نحو صحیحی صورت گیرد. اگر به فایل‌های کنترلر این مثال پیش فرض مراجعه کنید (پیش از تغییرات بحث جاری)، هرچند به نظر در کنترلرها، کلاس‌های موجود در فایل App_Start\IdentityConfig.cs تزریق شده‌اند، اما به دلیل عدم استفاده از اینترفیس‌ها، وابستگی کاملی بین جزئیات پیاده سازی این کلاس‌ها و نمونه‌های تزریق شده به کنترلرها وجود دارد و عملا معکوس سازی واقعی وابستگی‌ها رخ نداده‌است. بنابراین نیاز است این مسایل را اصلاح کنیم.

الف) انتقال کلاس ApplicationUserManager به لایه سرویس برنامه
کلاس ApplicationUserManager فایل App_Start\IdentityConfig.c را به لایه سرویس منتقل می‌کنیم:
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using AspNetIdentityDependencyInjectionSample.DomainClasses;
using AspNetIdentityDependencyInjectionSample.ServiceLayer.Contracts;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.DataProtection;
 
namespace AspNetIdentityDependencyInjectionSample.ServiceLayer
{
  public class ApplicationUserManager
   : UserManager<ApplicationUser, int>, IApplicationUserManager
  {
   private readonly IDataProtectionProvider _dataProtectionProvider;
   private readonly IIdentityMessageService _emailService;
   private readonly IApplicationRoleManager _roleManager;
   private readonly IIdentityMessageService _smsService;
   private readonly IUserStore<ApplicationUser, int> _store;
 
   public ApplicationUserManager(IUserStore<ApplicationUser, int> store,
    IApplicationRoleManager roleManager,
    IDataProtectionProvider dataProtectionProvider,
    IIdentityMessageService smsService,
    IIdentityMessageService emailService)
    : base(store)
   {
    _store = store;
    _roleManager = roleManager;
    _dataProtectionProvider = dataProtectionProvider;
    _smsService = smsService;
    _emailService = emailService;
 
    createApplicationUserManager();
   }
 
 
   public void SeedDatabase()
   {
   }
 
   private void createApplicationUserManager()
   {
    // Configure validation logic for usernames
    this.UserValidator = new UserValidator<ApplicationUser, int>(this)
    {
      AllowOnlyAlphanumericUserNames = false,
      RequireUniqueEmail = true
    };
 
    // Configure validation logic for passwords
    this.PasswordValidator = new PasswordValidator
    {
      RequiredLength = 6,
      RequireNonLetterOrDigit = true,
      RequireDigit = true,
      RequireLowercase = true,
      RequireUppercase = true,
    };
 
    // Configure user lockout defaults
    this.UserLockoutEnabledByDefault = true;
    this.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
    this.MaxFailedAccessAttemptsBeforeLockout = 5;
 
    // Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user
    // You can write your own provider and plug in here.
    this.RegisterTwoFactorProvider("PhoneCode", new PhoneNumberTokenProvider<ApplicationUser, int>
    {
      MessageFormat = "Your security code is: {0}"
    });
    this.RegisterTwoFactorProvider("EmailCode", new EmailTokenProvider<ApplicationUser, int>
    {
      Subject = "SecurityCode",
      BodyFormat = "Your security code is {0}"
    });
    this.EmailService = _emailService;
    this.SmsService = _smsService;
 
    if (_dataProtectionProvider != null)
    {
      var dataProtector = _dataProtectionProvider.Create("ASP.NET Identity");
      this.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser, int>(dataProtector);
    }
   } 
  }
}
تغییراتی که در اینجا اعمال شده‌اند، به شرح زیر می‌باشند:
- متد استاتیک Create این کلاس حذف و تعاریف آن به سازنده‌ی کلاس منتقل شده‌اند. به این ترتیب با هربار وهله سازی این کلاس توسط IoC Container به صورت خودکار این تنظیمات نیز به کلاس پایه UserManager اعمال می‌شوند.
- اگر به کلاس پایه UserManager دقت کنید، به آرگومان‌های جنریک آن یک int هم اضافه شده‌است. این مورد جهت استفاده از primary key از نوع int ضروری است.
- در کلاس پایه UserManager تعدادی متد وجود دارند. تعاریف آن‌ها را به اینترفیس IApplicationUserManager منتقل خواهیم کرد. نیازی هم به پیاده سازی این متدها در کلاس جدید ApplicationUserManager نیست؛ زیرا کلاس پایه UserManager پیشتر آن‌ها را پیاده سازی کرده‌است. به این ترتیب می‌توان به یک تزریق وابستگی واقعی و بدون وابستگی به پیاده سازی خاص UserManager رسید. کنترلری که با IApplicationUserManager بجای ApplicationUserManager کار می‌کند، قابلیت تعویض پیاده سازی آن‌را جهت آزمون‌های واحد خواهد یافت.
- در کلاس اصلی ApplicationDbInitializer پیش فرض این مثال، متد Seed هم قابل مشاهده‌است. این متد را از کلاس جدید Configuration اضافه شده به DataLayer حذف کرده‌ایم. از این جهت که در آن از متدهای کلاس ApplicationUserManager مستقیما استفاده شده‌است. متد Seed اکنون به کلاس جدید اضافه شده به لایه سرویس منتقل شده و در آغاز برنامه فراخوانی خواهد شد. DataLayer نباید وابستگی به لایه سرویس داشته باشد. لایه سرویس است که از امکانات DataLayer استفاده می‌کند.
- اگر به سازنده‌ی کلاس جدید ApplicationUserManager دقت کنید، چند اینترفیس دیگر نیز به آن تزریق شده‌اند. اینترفیس IApplicationRoleManager را ادامه تعریف خواهیم کرد. سایر اینترفیس‌های تزریق شده مانند IUserStore، IDataProtectionProvider و IIdentityMessageService جزو تعاریف اصلی ASP.NET Identity بوده و نیازی به تعریف مجدد آن‌ها نیست. فقط کلاس‌های EmailService و SmsService فایل App_Start\IdentityConfig.c را نیز به لایه سرویس منتقل کرده‌ایم. این کلاس‌ها بر اساس تنظیمات IoC Container مورد استفاده، در اینجا به صورت خودکار ترزیق خواهند شد. حالت پیش فرض آن، وهله سازی مستقیم است که مطابق کدهای فوق به حالت تزریق وابستگی‌ها بهبود یافته‌است.


ب) انتقال کلاس ApplicationSignInManager به لایه سرویس برنامه
کلاس ApplicationSignInManager فایل App_Start\IdentityConfig.c را نیز به لایه سرویس منتقل می‌کنیم.
using AspNetIdentityDependencyInjectionSample.DomainClasses;
using AspNetIdentityDependencyInjectionSample.ServiceLayer.Contracts;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin.Security;
 
namespace AspNetIdentityDependencyInjectionSample.ServiceLayer
{
  public class ApplicationSignInManager :
   SignInManager<ApplicationUser, int>, IApplicationSignInManager
  {
   private readonly ApplicationUserManager _userManager;
   private readonly IAuthenticationManager _authenticationManager;
 
   public ApplicationSignInManager(ApplicationUserManager userManager,
              IAuthenticationManager authenticationManager) :
    base(userManager, authenticationManager)
   {
    _userManager = userManager;
    _authenticationManager = authenticationManager;
   }
  }
}
در اینجا نیز اینترفیس جدید IApplicationSignInManager را برای مخفی سازی پیاده سازی کلاس پایه توکار SignInManager، اضافه کرده‌ایم. این اینترفیس دقیقا حاوی تعاریف متدهای کلاس پایه SignInManager است و نیازی به پیاده سازی مجدد در کلاس ApplicationSignInManager نخواهد داشت.


ج) انتقال کلاس ApplicationRoleManager به لایه سرویس برنامه
کلاس ApplicationRoleManager فایل App_Start\IdentityConfig.c را نیز به لایه سرویس منتقل خواهیم کرد:
using AspNetIdentityDependencyInjectionSample.DomainClasses;
using AspNetIdentityDependencyInjectionSample.ServiceLayer.Contracts;
using Microsoft.AspNet.Identity;
 
namespace AspNetIdentityDependencyInjectionSample.ServiceLayer
{
  public class ApplicationRoleManager : RoleManager<CustomRole, int>, IApplicationRoleManager
  {
   private readonly IRoleStore<CustomRole, int> _roleStore;
   public ApplicationRoleManager(IRoleStore<CustomRole, int> roleStore)
    : base(roleStore)
   {
    _roleStore = roleStore;
   }
 
 
   public CustomRole FindRoleByName(string roleName)
   {
    return this.FindByName(roleName); // RoleManagerExtensions
   }
 
   public IdentityResult CreateRole(CustomRole role)
   {
    return this.Create(role); // RoleManagerExtensions
   }
  }
}
روش کار نیز در اینجا همانند دو کلاس قبل است. اینترفیس جدید IApplicationRoleManager را که حاوی تعاریف متدهای کلاس پایه توکار RoleManager است، به لایه سرویس اضافه می‌کنیم. کنترلرهای برنامه با این اینترفیس بجای استفاده مستقیم از کلاس ApplicationRoleManager کار خواهند کرد.

تا اینجا کار تنظیمات لایه سرویس برنامه به پایان می‌رسد.


ساختار پروژه‌ی AspNetIdentityDependencyInjectionSample.IocConfig 

پروژه‌ی IocConfig جایی است که تنظیمات StructureMap را به آن منتقل کرده‌ایم:
using System;
using System.Data.Entity;
using System.Threading;
using System.Web;
using AspNetIdentityDependencyInjectionSample.DataLayer.Context;
using AspNetIdentityDependencyInjectionSample.DomainClasses;
using AspNetIdentityDependencyInjectionSample.ServiceLayer;
using AspNetIdentityDependencyInjectionSample.ServiceLayer.Contracts;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.Owin.Security;
using StructureMap;
using StructureMap.Web;
 
namespace AspNetIdentityDependencyInjectionSample.IocConfig
{
  public static class SmObjectFactory
  {
   private static readonly Lazy<Container> _containerBuilder =
    new Lazy<Container>(defaultContainer, LazyThreadSafetyMode.ExecutionAndPublication);
 
   public static IContainer Container
   {
    get { return _containerBuilder.Value; }
   }
 
   private static Container defaultContainer()
   {
    return new Container(ioc =>
    {
      ioc.For<IUnitOfWork>()
        .HybridHttpOrThreadLocalScoped()
        .Use<ApplicationDbContext>();
 
      ioc.For<ApplicationDbContext>().HybridHttpOrThreadLocalScoped().Use<ApplicationDbContext>();
      ioc.For<DbContext>().HybridHttpOrThreadLocalScoped().Use<ApplicationDbContext>();
 
      ioc.For<IUserStore<ApplicationUser, int>>()
       .HybridHttpOrThreadLocalScoped()
       .Use<UserStore<ApplicationUser, CustomRole, int, CustomUserLogin, CustomUserRole, CustomUserClaim>>();
 
      ioc.For<IRoleStore<CustomRole, int>>()
       .HybridHttpOrThreadLocalScoped()
       .Use<RoleStore<CustomRole, int, CustomUserRole>>();
 
      ioc.For<IAuthenticationManager>()
        .Use(() => HttpContext.Current.GetOwinContext().Authentication);
 
      ioc.For<IApplicationSignInManager>()
        .HybridHttpOrThreadLocalScoped()
        .Use<ApplicationSignInManager>();
 
      ioc.For<IApplicationUserManager>()
        .HybridHttpOrThreadLocalScoped()
        .Use<ApplicationUserManager>();
 
      ioc.For<IApplicationRoleManager>()
        .HybridHttpOrThreadLocalScoped()
        .Use<ApplicationRoleManager>();
 
      ioc.For<IIdentityMessageService>().Use<SmsService>();
      ioc.For<IIdentityMessageService>().Use<EmailService>();
      ioc.For<ICustomRoleStore>()
        .HybridHttpOrThreadLocalScoped()
        .Use<CustomRoleStore>();
 
      ioc.For<ICustomUserStore>()
        .HybridHttpOrThreadLocalScoped()
        .Use<CustomUserStore>();
 
      //config.For<IDataProtectionProvider>().Use(()=> app.GetDataProtectionProvider()); // In Startup class
 
      ioc.For<ICategoryService>().Use<EfCategoryService>();
      ioc.For<IProductService>().Use<EfProductService>();
    });
   }
  }
}
در اینجا نحوه‌ی اتصال اینترفیس‌های برنامه را به کلاس‌ها و یا نمونه‌هایی که آن‌ها را می‌توانند پیاده سازی کنند، مشاهده می‌کنید. برای مثال IUnitOfWork به ApplicationDbContext مرتبط شده‌است و یا دوبار تعاریف متناظر با DbContext را مشاهده می‌کنید. از این تعاریف به صورت توکار توسط ASP.NET Identity زمانیکه قرار است UserStore و RoleStore را وهله سازی کند، استفاده می‌شوند و ذکر آن‌ها الزامی است.
در تعاریف فوق یک مورد را به فایل Startup.cs موکول کرده‌ایم. برای مشخص سازی نمونه‌ی پیاده سازی کننده‌ی IDataProtectionProvider نیاز است به IAppBuilder کلاس Startup برنامه دسترسی داشت. این کلاس آغازین Owin اکنون به نحو ذیل بازنویسی شده‌است و در آن، تنظیمات IDataProtectionProvider را به همراه وهله سازی CreatePerOwinContext مشاهده می‌کنید:
using System;
using AspNetIdentityDependencyInjectionSample.IocConfig;
using AspNetIdentityDependencyInjectionSample.ServiceLayer.Contracts;
using Microsoft.AspNet.Identity;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.DataProtection;
using Owin;
using StructureMap.Web;
 
namespace AspNetIdentityDependencyInjectionSample
{
  public class Startup
  {
   public void Configuration(IAppBuilder app)
   {
    configureAuth(app);
   }
 
   private static void configureAuth(IAppBuilder app)
   {
    SmObjectFactory.Container.Configure(config =>
    {
      config.For<IDataProtectionProvider>()
        .HybridHttpOrThreadLocalScoped()
        .Use(()=> app.GetDataProtectionProvider());
    });
    SmObjectFactory.Container.GetInstance<IApplicationUserManager>().SeedDatabase();
 
    // Configure the db context, user manager and role manager to use a single instance per request
    app.CreatePerOwinContext(() => SmObjectFactory.Container.GetInstance<IApplicationUserManager>());
 
    // Enable the application to use a cookie to store information for the signed in user
    // and to use a cookie to temporarily store information about a user logging in with a third party login provider
    // Configure the sign in cookie
    app.UseCookieAuthentication(new CookieAuthenticationOptions
    {
      AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
      LoginPath = new PathString("/Account/Login"),
      Provider = new CookieAuthenticationProvider
      {
       // Enables the application to validate the security stamp when the user logs in.
       // This is a security feature which is used when you change a password or add an external login to your account.
       OnValidateIdentity = SmObjectFactory.Container.GetInstance<IApplicationUserManager>().OnValidateIdentity()
      }
    });
    app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
 
    // Enables the application to temporarily store user information when they are verifying the second factor in the two-factor authentication process.
    app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));
 
    // Enables the application to remember the second login verification factor such as phone or email.
    // Once you check this option, your second step of verification during the login process will be remembered on the device where you logged in from.
    // This is similar to the RememberMe option when you log in.
    app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie); 
   }
 
  }
}
این تعاریف از فایل پیش فرض Startup.Auth.cs پوشه‌ی App_Start دریافت و جهت کار با IoC Container برنامه، بازنویسی شده‌اند.


تنظیمات برنامه‌ی اصلی ASP.NET MVC، جهت اعمال تزریق وابستگی‌ها

الف) ابتدا نیاز است فایل Global.asax.cs را به نحو ذیل بازنویسی کنیم:
using System;
using System.Data.Entity;
using System.Web;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using AspNetIdentityDependencyInjectionSample.DataLayer.Context;
using AspNetIdentityDependencyInjectionSample.IocConfig;
using StructureMap.Web.Pipeline;
 
namespace AspNetIdentityDependencyInjectionSample
{
  public class MvcApplication : HttpApplication
  {
   protected void Application_Start()
   {
    AreaRegistration.RegisterAllAreas();
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);
 
 
    setDbInitializer();
    //Set current Controller factory as StructureMapControllerFactory
    ControllerBuilder.Current.SetControllerFactory(new StructureMapControllerFactory());
   }
 
   protected void Application_EndRequest(object sender, EventArgs e)
   {
    HttpContextLifecycle.DisposeAndClearAll();
   }
 
   public class StructureMapControllerFactory : DefaultControllerFactory
   {
    protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
    {
      if (controllerType == null)
       throw new InvalidOperationException(string.Format("Page not found: {0}", requestContext.HttpContext.Request.RawUrl));
      return SmObjectFactory.Container.GetInstance(controllerType) as Controller;
    }
   }
 
   private static void setDbInitializer()
   {
    Database.SetInitializer(new MigrateDatabaseToLatestVersion<ApplicationDbContext, Configuration>());
    SmObjectFactory.Container.GetInstance<IUnitOfWork>().ForceDatabaseInitialize();
   }
  }
}
در اینجا در متد setDbInitializer، نحوه‌ی استفاده و تعریف فایل Configuration لایه Data را ملاحظه می‌کنید؛ به همراه متد آغاز بانک اطلاعاتی و اعمال تغییرات لازم به آن در ابتدای کار برنامه. همچنین ControllerFactory برنامه نیز به StructureMapControllerFactory تنظیم شده‌است تا کار تزریق وابستگی‌ها به کنترلرهای برنامه به صورت خودکار میسر شود. در پایان کار هر درخواست نیز منابع Disposable رها می‌شوند.

ب) به پوشه‌ی Models برنامه مراجعه کنید. در اینجا در هر کلاسی که Id از نوع string وجود داشت، باید تبدیل به نوع int شوند. چون primary key برنامه را به نوع int تغییر داده‌ایم. برای مثال کلاس‌های EditUserViewModel و RoleViewModel باید تغییر کنند.

ج) اصلاح کنترلرهای برنامه جهت اعمال تزریق وابستگی‌ها

اکنون اصلاح کنترلرها جهت اعمال تزریق وابستگی‌ها ساده‌است. در ادامه نحوه‌ی تغییر امضای سازنده‌های این کنترلرها را جهت استفاده از اینترفیس‌های جدید مشاهده می‌کنید:
  [Authorize]
public class AccountController : Controller
{
  private readonly IAuthenticationManager _authenticationManager;
  private readonly IApplicationSignInManager _signInManager;
  private readonly IApplicationUserManager _userManager;
  public AccountController(IApplicationUserManager userManager,
          IApplicationSignInManager signInManager,
          IAuthenticationManager authenticationManager)
  {
   _userManager = userManager;
   _signInManager = signInManager;
   _authenticationManager = authenticationManager;
  }

  [Authorize]
public class ManageController : Controller
{
  // Used for XSRF protection when adding external logins
  private const string XsrfKey = "XsrfId";
 
  private readonly IAuthenticationManager _authenticationManager;
  private readonly IApplicationUserManager _userManager;
  public ManageController(IApplicationUserManager userManager, IAuthenticationManager authenticationManager)
  {
   _userManager = userManager;
   _authenticationManager = authenticationManager;
  }

  [Authorize(Roles = "Admin")]
public class RolesAdminController : Controller
{
  private readonly IApplicationRoleManager _roleManager;
  private readonly IApplicationUserManager _userManager;
  public RolesAdminController(IApplicationUserManager userManager,
           IApplicationRoleManager roleManager)
  {
   _userManager = userManager;
   _roleManager = roleManager;
  }


  [Authorize(Roles = "Admin")]
public class UsersAdminController : Controller
{
  private readonly IApplicationRoleManager _roleManager;
  private readonly IApplicationUserManager _userManager;
  public UsersAdminController(IApplicationUserManager userManager,
           IApplicationRoleManager roleManager)
  {
   _userManager = userManager;
   _roleManager = roleManager;
  }
پس از این تغییرات، فقط کافی است بجای خواص برای مثال RoleManager سابق از فیلدهای تزریق شده در کلاس، مثلا roleManager_ جدید استفاده کرد. امضای متدهای یکی است و تنها به یک search و replace نیاز دارد.
البته تعدادی اکشن متد نیز در اینجا وجود دارند که از string id استفاده می‌کنند. این‌ها را باید به int? Id تغییر داد تا با نوع primary key جدید مورد استفاده تطابق پیدا کنند.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید:
AspNetIdentityDependencyInjectionSample


معادل این پروژه جهت ASP.NET Core Identity : «سفارشی سازی ASP.NET Core Identity - قسمت اول - موجودیت‌های پایه و DbContext برنامه »
مطالب
تکمیل کلاس DelegateCommand

مدت‌ها از کلاس DelegateCommand معرفی شده در این آدرس استفاده می‌کردم. این کلاس یک مشکل جزئی دارد و آن هم عدم بررسی مجدد قسمت canExecute به صورت خودکار هست.

خلاصه‌ای برای کسانی که بار اول هست با این مباحث برخورد می‌کنند؛ یا MVVM به زبان بسیار ساده:

در برنامه نویسی متداول سیستم مایکروسافتی، در هر سیستمی که ایجاد کرده و در هر فناوری که ارائه داده از زمان VB6 تا امروز، شما روی یک دکمه مثلا دوبار کلیک می‌کنید و در فایل اصطلاحا code behind این فرم و در روال رخدادگردان آن شروع به کد نویسی خواهید کرد. این مورد تقریبا در همه جا صادق است؛ از WinForms تا WPF تا Silverlight تا حتی ASP.NET Webforms . به عمد هم این طراحی صورت گرفته تا برنامه نویس‌ها در این محیط‌ها زیاد احساس غریبی نکنند. اما این روش یک مشکل مهم دارد و آن هم «توهم» جداسازی رابط کاربر از کدهای برنامه است. به ظاهر یک فایل فرم وجود دارد و یک فایل جدای code behind ؛ اما در عمل هر دوی این‌ها یک partial class یا به عبارتی «یک کلاس» بیشتر نیستند. «فکر می‌کنیم» که از هم جدا شدند اما واقعا یکی هستند. شما در code behind صفحه به صورت مستقیم با عناصر رابط کاربری سروکار دارید و کدهای شما به این عناصر گره خورده‌اند.
شاید بپرسید که چه اهمیتی دارد؟
مشکل اول: امکان نوشتن آزمون‌ها واحد برای این متدها وجود ندارد یا بسیار سخت است. این متدها فقط با وجود فرم و رابط کاربری متناظر با آن‌ها هست که معنا پیدا می‌کنند و تک تک عناصر آن‌ها وهله سازی می‌شوند.
مشکل دوم: کد نوشته فقط برای همین فرم جاری آن قابل استفاده است؛ چون به صورت صریح به عناصر موجود در فرم اشاره می‌کند. نمی‌تونید این فایل code behind رو بردارید بدون هیچ تغییری برای فرم دیگری استفاده کنید.
مشکل سوم: نمی‌تونید طراحی فرم رو بدید به یک نفر، کد نویسی اون رو به شخصی دیگر. چون ایندو لازم و ملزوم یکدیگرند.

این سیستم کد نویسی دهه 90 است.
چند سالی است که طراحان سعی کرده‌اند این سیستم رو دور بزنند و روش‌هایی رو ارائه بدن که در آن‌ها فرم‌های برنامه و فایل‌های پیاده سازی کننده‌ی منطق آن هیچگونه ارتباط مستقیمی باهم نداشته باشند؛ به هم گره نخورده باشند؛ ارجاعی به هیچیک از عناصر بصری فرم را در خود نداشته باشند. به همین دلیل ASP.NET MVC به وجود آمده و در همان سال‌ها مثلا MVVM .

سؤال:
الان که رابط کاربری از فایل پیاده سازی کننده منطق آن جدا شده و دیگر Code behind هم نیست (همان partial class های متداول)، این فایل‌ها چطور متوجه می‌شوند که مثلا روی یک فرم، شیءایی قرار گرفته؟ از کجا متوجه خواهند شد که روی دکمه‌ای کلیک شده؟ این‌ها که ارجاعی از فرم را در درون خود ندارند.
در الگوی MVVM این سیم کشی توسط امکانات قوی Binding موجود در WPF میسر می‌شود. در ASP.NET MVC چیزی شبیه به آن به نام Model binder و همان مکانیزم‌های استاندارد HTTP این کار رو می‌کنه. در MVVM شما بجای code behind خواهید داشت ViewModel (اسم جدید آن). در ASP.NET MVC این اسم شده Controller. بنابراین اگر این اسامی رو شنیدید زیاد تعجب نکنید. این‌ها همان Code behind قدیمی هستند اما ... بدون داشتن ارجاعی از رابط کاربری در خود که ... اطلاعات موجود در فرم به نحوی به آن‌ها Bind و ارسال می‌شوند.
این سیم کشی‌ها هم نامرئی هستند. یعنی فایل ViewModel یا فایل Controller نمی‌دونند که دقیقا از چه کنترلی در چه فرمی این اطلاعات دریافت شده.
این ایده هم جدید نیست. شاید بد نباشه به دوران طلایی Win32 برگردیم. همان توابع معروف PostMessage و SendMessage را به خاطر دارید؟ شما در یک ترد می‌تونید با مثلا PostMessage شیءایی رو به یک فرم که در حال گوش فرا دادن به تغییرات است ارسال کنید (این سیم کشی هم نامرئی است). بنابراین پیاده سازی این الگوها حتی در Win32 و کلیه فریم ورک‌های ساخته شده بر پایه آن‌ها مانند VCL ، VB6 ، WinForms و غیره ... «از روز اول» وجود داشته و می‌تونستند بعد از 10 سال نیان بگن که اون روش‌های RAD ایی رو که ما پیشنهاد دادیم، می‌شد خیلی بهتر از همان ابتدا، طور دیگری پیاده سازی بشه.

ادامه بحث!
این سیم کشی یا اصطلاحا Binding ، در مورد رخدادها هم در WPF وجود داره و اینبار به نام Commands معرفی شده‌است. به این معنا که بجای اینکه بنویسید:
<Button  Click="btnClick_Event">Last</Button>

بنویسید:
<Button Command="{Binding GoLast}">Last</Button>

حالا باید مکانیزمی وجود داشته باشه تا این پیغام رو به ViewModel برنامه برساند. اینکار با پیاده سازی اینترفیس ICommand قابل انجام است که معرفی یک کلاس عمومی از پیاده سازی آن‌را در ابتدای بحث مشاهده نمودید.
در یک DelegateCommand،‌ توسط متد منتسب به executeAction، مشخص خواهیم کرد که اگر این سیم کشی برقرار شد (که ما دقیقا نمی‌دانیم و نمی‌خواهیم که بدانیم از کجا و کدام فرم دقیقا)، لطفا این اعمال را انجام بده و توسط متد منتسب به canExecute به سیستم Binding خواهیم گفت که آیا مجاز هستی این اعمال را انجام دهی یا خیر. اگر این متد false برگرداند، مثلا دکمه یاد شده به صورت خودکار غیرفعال می‌شود.
اما مشکل کلاس DelegateCommand ذکر شده هم دقیقا همینجا است. این دکمه تا ابد غیرفعال خواهد ماند. در WPF کلاسی وجود دارد به نام CommandManager که حاوی متدی استاتیکی است به نام InvalidateRequerySuggested. اگر این متد به صورت دستی فراخوانی شود، یکبار دیگر کلیه متدهای منتسب به تمام canExecute های تعریف شده، به صورت خودکار اجرا می‌شوند و اینجا است که می‌توان دکمه‌ای را که باید مجددا بر اساس شرایط جاری تغییر وضعیت پیدا کند، فعال کرد. بنابراین فراخوانی متد InvalidateRequerySuggested یک راه حل کلی رفع نقیصه‌ی ذکر شده است.
راه حل دومی هم برای حل این مشکل وجود دارد. می‌توان از رخدادگردان CommandManager.RequerySuggested استفاده کرد. روال منتسب به این رخدادگردان هر زمانی که احساس کند تغییری در UI رخ داده، فراخوانی می‌شود. بنابراین پیاده سازی بهبود یافته کلاس DelegateCommand به صورت زیر خواهد بود:

using System;
using System.Windows.Input;

namespace MvvmHelpers
{
// Ref.
// - http://johnpapa.net/silverlight/5-simple-steps-to-commanding-in-silverlight/
// - http://joshsmithonwpf.wordpress.com/2008/06/17/allowing-commandmanager-to-query-your-icommand-objects/
public class DelegateCommand<T> : ICommand
{
readonly Func<T, bool> _canExecute;
bool _canExecuteCache;
readonly Action<T> _executeAction;

public DelegateCommand(Action<T> executeAction, Func<T, bool> canExecute = null)
{
if (executeAction == null)
throw new ArgumentNullException("executeAction");

_executeAction = executeAction;
_canExecute = canExecute;
}

public event EventHandler CanExecuteChanged
{
add { if (_canExecute != null) CommandManager.RequerySuggested += value; }
remove { if (_canExecute != null) CommandManager.RequerySuggested -= value; }
}

public bool CanExecute(object parameter)
{
return _canExecute == null ? true : _canExecute((T)parameter);
}

public void Execute(object parameter)
{
_executeAction((T)parameter);
}
}
}

استفاده از آن هم در ViewModel ساده است. یکبار خاصیتی به این نام تعریف می‌شود. سپس در سازنده کلاس مقدار دهی شده و متدهای متناظر آن تعریف خواهند شد:

public DelegateCommand<string> GoLast { set; get; }

//in ctor
GoLast = new DelegateCommand<string>(goLast, canGoLast);

private bool canGoLast(string data)
{
//ex.
return ListViewGuiData.CurrentPage != ListViewGuiData.TotalPage - 1;
}

private void goLast(string data)
{
//do something
}

مزیت کلاس DelegateCommand جدید هم این است که مثلا متد canGoLast فوق، به صورت خودکار با به روز رسانی UI ، فراخوانی و تعیین اعتبار مجدد می‌شود.


مطالب
معرفی کتابخانه PdfReport
مدتی هست که در حال تهیه کتابخانه‌ی گزارش سازی مبتنی بر iTextSharp هستم و عمده‌ی استفاده از آن برای من تاکنون، تهیه گزارشات باکیفیت PDF فارسی تحت وب بوده؛ هر چند این کتابخانه وابستگی خاصی به فناوری مورد استفاده ندارد و با WinForms، WPF، مشتقات ASP.NET ، سرویس‌های WCF و کلا هرجایی که دات نت فریم ورک کامل در دسترس باشد، قابل استفاده است. همچنین به منبع داده خاصی گره نخورده است و حتی می‌توانید از یک anonymously typed list عدم متصل به بانک اطلاعاتی خاصی نیز به عنوان منبع داده آن استفاده کنید.
کتابخانه PdfReport به عمد جهت دات نت 3.5 تهیه شده است تا بازه وسیعی از سیستم عامل‌ها را پوشش دهد.
این کتابخانه علاوه بر تبدیل اطلاعات شما به گزارشات مبتنی بر PDF، امکان تهیه خروجی خودکار اکسل (2007 به بعد) را نیز دارد. فایل خروجی آن، به صورت پیوست درون فایل PDF تهیه شده قرار می‌گیرد و جزئی از آن می‌شود.
بدیهی است اینبار با کتابخانه گزارش سازی روبرو هستید که با راست به چپ مشکلی ندارد!
کتابخانه PdfReport بر پایه کتابخانه‌های معروف سورس باز iTextSharp و EPPlus تهیه شده است. حداقل مزیت استفاده از آن، صرفه جویی در وقت شما جهت آموختن ریزه کاری‌های مرتبط با هر کدام از کتابخانه‌های یاده شده است. برای نمونه جهت فراگیری کار با iTextSharp نیاز است یک کتاب 600 صفحه‌ای به نام iText in action را مطالعه و تمرین کنید. این مورد منهای مسایل و نکات متعدد مرتبط با زبان فارسی است که در این کتاب به آن‌ها اشاره‌ای نشده است.
برای تهیه آن، مشکلات متداولی که کاربران مدام در انجمن‌های برنامه نویسی مطرح می‌کنند و ابزارهای موجود عاجز از ارائه راه حلی ساده برای حل آن‌ها هستند، مد نظر بوده و امید است نگارش یک این کتابخانه بتواند بسیاری از این دردها را کاهش دهد.
کار با این کتابخانه صرفا با کدنویسی میسر است (code first) و همین مساله انعطاف پذیری قابل توجهی را ایجاد کرده که در طی روزهای آینده با جزئیات بیشتر آن آشنا خواهید شد.


اما چرا PDF؟

استفاده از قالب PDF برای تهیه گزارشات، این مزایا را به همراه دارد:
- دقیقا همان چیزی که مشاهده می‌شود، در هر مکانی قابل چاپ است. با همان کیفیت، همان اندازه صفحه، همان فونت و غیره. به این ترتیب به صفحه بندی بسیار مناسب و بهینه‌ای می‌توان رسید و مشکلات گزارشات HTML ایی وب را ندارد.
- امکان استفاد از فونت‌های شکیل‌تر در آن بدون مشکل و بدون نیاز به نصب بر روی کامپیوتری میسر است؛ چون فونت را می‌توان در فایل PDF نیز قرار داد.
- این فایل در تمام سیستم عامل‌ها پشتیبانی می‌شود. خصوصا اینکه فایل نهایی در تمام کامپیوترها و در انواع و اقسام سیستم عامل‌ها به یک شکل و اندازه نمایش داده خواهد شد. برای مثال اینطور نیست که در ویندوز XP ،‌اعداد آن فارسی نمایش داده شوند و در ویندوز 7 با تنظیمات محلی خاصی، دیگر اینطور نباشد. حتی تحت لینوکس هم اعداد آن فارسی نمایش داده خواهد شد چون فونت مخصوص بکار رفته، در خود فایل PDF قابل قرار دادن است.
- برنامه معروف و رایگان Adobe reader برای خواندن و مشاهده آن کفایت می‌کند و البته کلاینت یکبار باید این برنامه را نصب کند. همچنین از این نوع برنامه‌های رایگان برای مشاهده فایل‌های PDF زیاد است.
- تمام صفحات گزارش را در یک فایل می‌توان داشت و به یکباره تمام آن نیز به سادگی قابل چاپ است. این مشکلی است که با گزارشات تحت وب وجود دارد که نمی‌شود مثلا یک گزارش 100 صفحه‌ای را به یکباره به چاپگر ارسال کرد. به همین جهت عموما کاربران درخواست می‌دهند تا کل گزارش را در یک صفحه HTML نمایش دهید تا ما راحت آن‌را چاپ کنیم یا راحت از آن خروجی بگیریم. اما زمانیکه فایل PDF تهیه می‌شود این مشکلات وجود نخواهد داشت و جهت Print بسیار بهینه سازی شده است. تا حدی که الان فرمت برگزیده تهیه کتاب‌های الکترونیکی نیز PDF است. مثلا سایت معروف آمازون امکان فروش نسخه PDF کتاب‌ها را هم پیش بینی کرده است.
-امکان صفحه بندی دقیق به همراه مشخص سازی landscape یا portrait بودن صفحه نهایی میسر است. چیزی که در گزارشات صفحات وب آنچنان معنایی ندارد.
- امکان رمزنگاری اطلاعات در آن پیش بینی شده است. همچنین می‌توان به فایل‌های PDF امضای دیجیتال نیز اضافه کرد. به این ترتیب هرگونه تغییری در محتوای فایل توسط برنامه‌های PDF خوان معتبر گزارش داده شده و می‌توان از صحت اطلاعات ارائه شده توسط آن اطمینان حاصل کرد.
- از فشرده سازی مطالب، فایل‌ها و تصاویر قرار داده شده در آن پشتیبانی می‌کند.
- از گرافیک برداری پشتیبانی می‌کند.


مجوز استفاده از این کتابخانه:
کار من مبتنی بر LGPL است. به این معنا که به صورت باینری (فایل dll) در هر نوع پروژه‌ای قابل استفاده است.
اما ... PdfReport از دو کتابخانه دیگر نیز استفاده می‌کند:
- کتابخانه iTextSharp که دارای مجوز AGPL است. این مجوز رایگان نیست.
- کتابخانه EPPlus برای تولید فایل‌های اکسل با کیفیت. مجوز استفاده از این کتابخانه LGPL است و تا زمانیکه به صورت باینری از آن استفاده می‌کنید، محدودیتی را برای شما ایجاد نخواهد کرد.


کتابخانه PdfReport به صورت سورس باز در CodePlex قرار گرفته ؛ اما جهت پرسیدن سؤالات، پیشنهادات، ارائه بهبود و غیره می‌توانید (و بهتر است) از قسمت مدیریت پروژه مرتبط در سایت جاری نیز استفاده کنید.


نحوه تهیه اولین گزارش، با کتابخانه PdfReport

الف) یک پروژه Class library جدید را شروع کنید. از این جهت که گزارشات PdfReport در انواع و اقسام پروژه‌های VS.NET قابل استفاده است، می‌توان از این پروژه Class library به عنوان کلاس‌های پایه قابل استفاده در انواع و اقسام پروژه‌های مختلف، بدون نیاز به تغییری در کدهای آن استفاده کرد.

ب) آخرین نگارش فایل‌های مرتبط با PdfReport را از اینجا دریافت کنید و سپس ارجاعاتی را به اسمبلی‌های موجود در بسته آن به پروژه خود اضافه نمائید (ارجاعاتی به PdfReport، iTextSharp و EPPlus). فایل XML راهنمای کتابخانه نیز به همراه بسته آن می‌باشد که در حین استفاده از متدها و خواص PdfReport کمک بزرگی خواهد بود.

ج) کلاس‌های زیر را به آن اضافه کنید:
using System.Web;
using System.Windows.Forms;

namespace PdfReportSamples
{
    public static class AppPath
    {
        public static string ApplicationPath
        {
            get
            {
                if (isInWeb)
                    return HttpRuntime.AppDomainAppPath;

                return Application.StartupPath;
            }
        }

        private static bool isInWeb
        {
            get
            {
                return HttpContext.Current != null;
            }
        }
    }
}
از این کلاس برای مشخص سازی محل ذخیره سازی فایل‌های نهایی PDF تولیدی استفاده خواهیم کرد.
همانطور که مشاهده می‌کنید ارجاعاتی را به System.Windows.Forms.dll و System.Web.dll نیاز دارد.

در ادامه کلاس User را جهت ساخت یک منبع داده درون حافظه‌ای تعریف خواهیم کرد:
using System;

namespace PdfReportSamples.IList
{
    public class User
    {
        public int Id { set; get; }
        public string Name { set; get; }
        public string LastName { set; get; }
        public long Balance { set; get; }
        public DateTime RegisterDate { set; get; }
    }
}
اکنون کلاس اصلی گزارش ما به صورت زیر خواهد بود:
using System;
using System.Collections.Generic;
using PdfRpt.Core.Contracts;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.IList
{
    public class IListPdfReport
    {
        public IPdfReportData CreatePdfReport()
        {
            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.RightToLeft);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" });
            })
            .DefaultFonts(fonts =>
            {
                fonts.Path(Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf",
                                  Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
            })
            .PagesFooter(footer =>
            {
                footer.DefaultFooter(DateTime.Now.ToString("MM/dd/yyyy"));
            })
            .PagesHeader(header =>
            {
                header.DefaultHeader(defaultHeader =>
                {
                    defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png");
                    defaultHeader.Message("گزارش جدید ما");
                });
            })
            .MainTableTemplate(template =>
            {
                template.BasicTemplate(BasicTemplate.ClassicTemplate);
            })
            .MainTablePreferences(table =>
            {
                table.ColumnsWidthsType(TableColumnWidthType.Relative);
                table.NumberOfDataRowsPerPage(5);
            })
            .MainTableDataSource(dataSource =>
            {
                var listOfRows = new List<User>();
                for (int i = 0; i < 200; i++)
                {
                    listOfRows.Add(new User { Id = i, LastName = "نام خانوادگی " + i, Name = "نام " + i, Balance = i + 1000 });
                }
                dataSource.StronglyTypedList<User>(listOfRows);
            })
            .MainTableSummarySettings(summarySettings =>
            {
                summarySettings.OverallSummarySettings("جمع کل");
                summarySettings.PerviousPageSummarySettings("نقل از صفحه قبل");
                summarySettings.PageSummarySettings("جمع صفحه");
            })
            .MainTableColumns(columns =>
            {
                columns.AddColumn(column =>
                {
                    column.PropertyName("rowNo");
                    column.IsRowNumber(true);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(0);
                    column.Width(1);
                    column.HeaderCell("#");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.Id);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(1);
                    column.Width(2);
                    column.HeaderCell("شماره");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.Name);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(2);
                    column.Width(3);
                    column.HeaderCell("نام");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.LastName);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(3);
                    column.Width(3);
                    column.HeaderCell("نام خانوادگی");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.Balance);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(4);
                    column.Width(2);
                    column.HeaderCell("موجودی");
                    column.ColumnItemsTemplate(template =>
                    {
                        template.TextBlock();
                        template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });
                    column.AggregateFunction(aggregateFunction =>
                    {
                        aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum);
                        aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });
                });

            })
            .MainTableEvents(events =>
            {
                events.DataSourceIsEmpty(message: "رکوردی یافت نشد.");
            })
            .Export(export =>
            {
                export.ToExcel();
                export.ToCsv();
                export.ToXml();
            })
            .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\RptIListSample.pdf"));
        }
    }
}
و برای استفاده از آن:
var rpt = new IListPdfReport().CreatePdfReport();
// rpt.FileName


برای نمونه، جهت مشاهده نمایش این خروجی در یک برنامه ویندوزی، به مثال‌های همراه سورس پروژه در مخزن کد آن مراجعه نمائید.

توضیحات بیشتر:

- در قسمت  DocumentPreferences، جهت راست به چپ (PdfRunDirection)، اندازه صفحه (PdfPageSize)، جهت صفحه (PageOrientation) و امثال آن تنظیم می‌شوند.
- سپس نیاز است قلم‌های مورد استفاده در گزارش مشخص شوند. در متد DefaultFonts باید دو  قلم را معرفی کنید. قلم اول، قلم پیش فرض خواهد بود و قلم دوم برای رفع نواقص قلم اول مورد استفاده قرار می‌گیرد. برای مثال اگر قلم اول فاقد حروف انگلیسی است، به صورت خودکار به قلم دوم رجوع خواهد شد.
- در ادامه در متد PagesFooter، تاریخ درج شده در پایین تمام صفحات مشخص می‌شود. در مورد ساخت Footer سفارشی در قسمت‌های بعدی بحث خواهد شد.
- در متد PagesHeader، متن و تصویر قرار گرفته در Header تمام صفحات گزارش قابل تنظیم است. این مورد نیز قابل سفارشی سازی است که در قسمت‌های بعد به آن خواهیم پرداخت.
- توسط MainTableTemplate، قالب ظاهری ردیف‌های گزارش مشخص می‌شود. یک سری قالب پیش فرض در کتابخانه PdfReport موجود است که توسط متد BasicTemplate آن قابل دسترسی است. در مورد نحوه تعریف قالب‌‌های سفارشی به مرور در قسمت‌های بعد، بحث خواهد شد.
- در قسمت MainTablePreferences تنظیمات جدول اصلی گزارش تعیین می‌شود. برای مثال چه تعداد ردیف در صفحه نمایش داده شود. اگر این مورد را تنظیم نکنید، به صورت خودکار محاسبه خواهد شد. نحوه تعیین عرض ستون‌های گزارش به کمک متد ColumnsWidthsType مشخص می‌شود که در اینجا حالت نسبی درنظر گرفته شده است.
- منبع داده مورد استفاده توسط متد MainTableDataSource مشخص می‌شود که در اینجا یک لیست جنریک تعیین شده و سپس توسط متد StronglyTypedList در اختیار گزارش ساز جاری قرار می‌گیرد. تعدادی منبع داده پیش فرض در PdfReport وجود دارند که هر کدام را در قسمت‌های بعدی بررسی خواهیم کرد. همچنین امکان تعریف منابع داده سفارشی نیز وجود دارد.
- با کمک متد MainTableSummarySettings، برچسب‌های جمع‌های پایین صفحات مشخص می‌شود.
- در قسمت MainTableColumns، ستون‌هایی را که علاقمندیم در گزارش ظاهر شوند، قید می‌کنیم. هر ستون باید با یک فیلد یا خاصیت منبع داده متناظر باشد. همچنین همانطور که مشاهده می‌کنید امکان تعیین Visibility، عرض و غیره آن نیز مهیا است (قابلیت ساخت گزارشاتی که به انتخاب کاربر، ستون‌های آن ظاهر یا مخفی شوند). در اینجا توسط callbackهایی که در متد ColumnItemsTemplate قابل دسترسی هستند، می‌توان اطلاعات را پیش از نمایش فرمت کرد. برای مثال سه رقم جدا کننده به اعداد اضافه کرد (برای نمونه در خاصیت موجودی فوق) و یا توسط متد AggregateFunction، می‌توان متد تجمعی مناسبی را جهت ستون جاری مشخص کرد.
- توسط متد MainTableEvents به بسیاری از رخدادهای داخلی PdfReport دسترسی خواهیم یافت. برای مثال اگر در اینجا رکوردی موجود نباشد، رخداد DataSourceIsEmpty صادر خواهد شد.
- به کمک متد Export، خروجی‌های دلخواه مورد نظر را می‌توان مشخص کرد. تعدادی خروجی، مانند اکسل، XML و CSV در این کتابخانه موجود است. امکان سفارشی سازی آن‌ها نیز پیش بینی شده است.
- و نهایتا توسط متد Generate مشخص خواهیم کرد که فایل گزارش کجا ذخیره شود.

 لطفا برای طرح مشکلات و سؤالات خود در رابطه با کتابخانه PdfReport از این قسمت سایت استفاده کنید.