using System; using System.Collections.Concurrent; using System.Threading; namespace LazyDic { public class LazyConcurrentDictionary<TKey, TValue> { private readonly ConcurrentDictionary<TKey, Lazy<TValue>> _concurrentDictionary; public LazyConcurrentDictionary() { _concurrentDictionary = new ConcurrentDictionary<TKey, Lazy<TValue>>(); } public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory) { var lazyResult = _concurrentDictionary.GetOrAdd(key, k => new Lazy<TValue>(() => valueFactory(k), LazyThreadSafetyMode.ExecutionAndPublication)); return lazyResult.Value; } public TValue AddOrUpdate(TKey key, TValue addValue, Func<TKey, TValue, TValue> updateValueFactory) { var lazyResult = _concurrentDictionary.AddOrUpdate( key, new Lazy<TValue>(() => addValue), (k, currentValue) => new Lazy<TValue>(() => updateValueFactory(k, currentValue.Value), LazyThreadSafetyMode.ExecutionAndPublication)); return lazyResult.Value; } public TValue AddOrUpdate(TKey key, Func<TKey, TValue> addValueFactory, Func<TKey, TValue, TValue> updateValueFactory) { var lazyResult = _concurrentDictionary.AddOrUpdate( key, k => new Lazy<TValue>(() => addValueFactory(k)), (k, currentValue) => new Lazy<TValue>(() => updateValueFactory(k, currentValue.Value), LazyThreadSafetyMode.ExecutionAndPublication)); return lazyResult.Value; } public int Count => _concurrentDictionary.Count; } }
NUnit:
using NUnit.Framework; using NUnit.Framework.SyntaxHelpers; namespace TestLibrary { [TestFixture] public class MyTest { [Test] public void Test1() { var expectedValue = 2; Assert.That(expectedValue , Is.EqualTo(2)); } } }
Microsoft UnitTesting :
using Microsoft.VisualStudio.TestTools.UnitTesting ; namespace TestLibrary { [TesClass] public class MyTest { [TestMethod] public void Test1() { var expectedValue = 2; Assert.AreEqual (expectedValue , 2); } } }
نکته: مورد استفاده این کتابخانه فقط در قسمت Assert کدهای تست است و استفاده از سایر کتابخانههای جانبی الزامی است.
»Standard که باید از Should.dll استفاده نمایید؛
»Fluent که باید از Should.Fluent.dll استفاده نمایید؛(پیاده سازی همان فریم ورک Should به صورت Static Reflection)
نصب کتابخانه Should با استفاده از nuget (آخرین نسخه آن در حال حاضر 1.1.20 است ) :
Install-Package Should
Install-Package ShouldFluent
در ابتدا همان مثال قبلی را با این کتابخانه بررسی خواهیم کرد:
using Microsoft.VisualStudio.TestTools.UnitTesting; namespace TestLibrary { [TesClass] public class MyTest { [TestMethod] public void Test1() { var expectedValue = 2; expectedValue.Should().Equal( 2 ); } } }
مثال:
[TestMethod] public void AccountConstructorTest() { const int expectedBalance = 1000; Account bankAccount = new Account(); // Assert.IsNotNull(bankAccount, "Account was null."); // Assert.AreEqual(expectedBalance, bankAccount.AccountBalance, "Account balance not mathcing");
bankAccount.ShouldNotBeNull("Account was null"); bankAccount.AccountBalance.ShouldEqual(expectedBalance, "Account balance not matching"); }
#Should.Fluent
public void Should_fluent_assertions() { object obj = null; obj.Should().Be.Null(); obj = new object(); obj.Should().Be.OfType(typeof(object)); obj.Should().Equal(obj); obj.Should().Not.Be.Null(); obj.Should().Not.Be.SameAs(new object()); obj.Should().Not.Be.OfType<string>(); obj.Should().Not.Equal("foo"); obj = "x"; obj.Should().Not.Be.InRange("y", "z"); obj.Should().Be.InRange("a", "z"); obj.Should().Be.SameAs("x"); "This String".Should().Contain("This"); "This String".Should().Not.Be.Empty(); "This String".Should().Not.Contain("foobar"); false.Should().Be.False(); true.Should().Be.True(); var list = new List<object>(); list.Should().Count.Zero(); list.Should().Not.Contain.Item(new object()); var item = new object(); list.Add(item); list.Should().Not.Be.Empty(); list.Should().Contain.Item(item); };
#مثالهای استفاده از متغیرهای DateTime و Guid
public void Should_fluent_assertions() { var id = new Guid(); id.Should().Be.Empty(); id = Guid.NewGuid(); id.Should().Not.Be.Empty(); var date = DateTime.Now; date1.Should().Be.Today(); var str = ""; str.Should().Be.NullOrEmpty(); var one = "1"; one.Should().Be.ConvertableTo<int>(); var idString = Guid.NewGuid().ToString(); idString.Should().Be.ConvertableTo<Guid>(); }
Blazor 5x - قسمت 25 - تهیه API مخصوص Blazor WASM - بخش 2 - تامین پایهی اعتبارسنجی و احراز هویت
- «معرفی 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; } } }
اکنون که یک ApplicationUser سفارشی را ایجاد کردیم، نیازی نیست تا DbSet خاص آنرا به ApplicationDbContext برنامه اضافه کنیم. برای معرفی آن به برنامه ابتدا باید به فایل BlazorServer\BlazorServer.DataAccess\ApplicationDbContext.cs مراجعه کرده و نوع IdentityUser را به IdentityDbContext، از طریق آرگومان جنریکی که میپذیرد، معرفی کنیم:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
پس از این تغییر، در فایل 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 } } }
- در تعریف ابتدایی این کنترلر، ویژگیهای زیر ذکر شدهاند:
[Route("api/[controller]/[action]")] [ApiController] [Authorize]
تا اینجا اگر برنامه را اجرا کنیم، میتوان با استفاده از 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; } } }
{ "BearerTokens": { "Key": "This is my shared key, not so secret, secret!", "Issuer": "https://localhost:5001/", "Audience": "Any", "AccessTokenExpirationMinutes": 20 } }
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; } } }
در آخر، این سرویس را به صورت زیر به لیست سرویسهای ثبت شدهی پروژهی 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 } }); } } }
تا اینجا اگر برنامه را اجرا کنیم، میتوان در قسمت ورود به سیستم، برای نمونه مشخصات کاربر ادمین را وارد کرد:
و پس از اجرای درخواست، به خروجی زیر میرسیم:
که در اینجا 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 به تنظیمات 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
روش تعریف یک module initializer
در مثال زیر، قالب ابتدایی یک ModuleInitializer را مشاهده میکنید:
namespace CS9Features { using System.Runtime.CompilerServices; internal static class TestModuleInitializer { [ModuleInitializer] public static void MyModuleInitializer() { // put your module initializer here } } }
- باید استاتیک باشد.
- باید بدون پارامتر باشد.
- باید خروجی آن void باشد.
- نباید به صورت جنریک تعریف شود.
- این متد باید در همان اسمبلی، قابل دسترسی باشد؛ یعنی سطح دسترسی آن باید یا public و یا internal باشد.
- نباید local function باشد.
میتوان بیش از یک ModuleInitializer را در یک اسمبلی تعریف کرد
به مثال زیر دقت کنید:
namespace CS9Features { using System.Runtime.CompilerServices; internal static class TestModuleInitializer { [ModuleInitializer] public static void MyModuleInitializer1() { // put your module initializer here } [ModuleInitializer] public static void MyModuleInitializer2() { // put your module initializer here } } }
این مورد یکی از مهمترین تفاوتهای module initializerها با سازندههای static است. ترتیب اجرای سازندههای static مشخص نیست و بر اساس کدهای کلاینت و زمان دسترسی به کلاسهای مختلف، سازندهی استاتیک کلاس A میتواند پس از سازندهی استاتیک کلاس B اجرا شود و یا برعکس. اما همواره نحوهی اجرای module initializerها مشخص و ترتیبی است و همچنین نیازی به فراخوانی آنها توسط هیچ کلاینتی نیست.
موارد کاربرد module initializerها
نمونهی بسیار پرکاربرد module initializer ها، اجرای کدهایی پیش از شروع به اجرای آزمونهای خودکار یک برنامهاست؛ مانند کدهایی که یک بانک اطلاعاتی را ایجاد و مقدار دهی اولیه میکنند و پس از آن قرار است آزمایشهای برنامه بر روی این بانک اطلاعاتی مشخص، اجرا شوند.
هدف ارائه راه حلی برای مدیریت Transactionها به عنوان یک Cross Cutting Concern، توسط ApplicationServiceها میباشد.
- دوره Aspect oriented programming
- بررسی مفاهیم معکوس سازی وابستگیها و ابزارهای مرتبط با آن
- طراحی و پیاده سازی ServiceLayer به همراه خودکارسازی Business Validationها
پیش فرض ما این است که شما از EF به عنوان OR-Mapper استفاده میکنید و الگوی Context Per Request را پیاده سازی کرده اید یا از طریق پیاده سازی الگوی Container Per Request به داشتن Context یکتا برای هر درخواست رسیده اید.
کتابخانه StructureMap.Mvc5 پیاده سازی از الگوی Container Per Request را با استفاده از امکانات Nested Container مربوط به StructureMap ارائه میدهد. اشیاء موجود در Nested Container طول عمر Singleton دارند.
واسط ITransaction
public interface ITransaction : IDisposable { void Commit(); void Rollback(); }
واسط بالا 3 متد را که برای مدیریت تراکنش لازم میباشد، در اختیار استفاده کننده قرار میدهد.
واسط IUnitOfWork
public interface IUnitOfWork : IDisposable { ... ITransaction BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.Snapshot); ITransaction Transaction { get; } IDbConnection Connection { get; } bool HasTransaction { get; } }
اعضای جدید واسط IUnitOfWork کاملا مشخص هستند.
پیاده سازی واسط ITransaction، توسط یک Nested Type در دل کلاس DbContextBase انجام میگیرد.
public abstract class DbContextBase : DbContext { ... #region Fields private ITransaction _currenTransaction; #endregion #region NestedTypes private class DbContextTransactionAdapter : ITransaction { private DbContextTransaction _transaction; public DbContextTransactionAdapter(DbContextTransaction transaction) { Guard.NotNull(transaction, nameof(transaction)); _transaction = transaction; } public void Commit() { _transaction?.Commit(); } public void Rollback() { if (_transaction?.UnderlyingTransaction.Connection != null) _transaction.Rollback(); } public void Dispose() { _transaction?.Dispose(); _transaction = null; } } #endregion #region Public Methods ... public ITransaction BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted) { if (_currenTransaction != null) return _currenTransaction; return _currenTransaction = new DbContextTransactionAdapter(Database.BeginTransaction(isolationLevel)); } #endregion #region Properties ... public ITransaction Transaction => _currenTransaction; public IDbConnection Connection => Database.Connection; public bool HasTransaction => _currenTransaction != null; #endregion } public class ApplicationDbContext : DbContextBase, IUnitOfWork, ITransientDependency { }
کلاس DbContextTransactionAdapter همانطور که از نام آن مشخص میباشد، پیاده سازی از الگوی Adapter برای وفق دادن DbContextTransaction با واسط ITransaction، میباشد. متد BeginTransaction در صورتی که تراکنشی برای وهله جاری DbContext ایجاد نشده باشد، تراکنشی را ایجاد کرده و فیلد currentTransaction_ را نیز مقدار دهی میکند.
TransactionalAttribute
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] public sealed class TransactionalAttribute : Attribute { public IsolationLevel IsolationLevel { get; set; } = IsolationLevel.ReadCommitted; public TimeSpan? Timeout { get; set; } }
TransactionInterceptor
public class TransactionInterceptor : ISyncInterceptionBehavior { private readonly IUnitOfWork _unitOfWork; public TransactionInterceptor(IUnitOfWork unitOfWork) { _unitOfWork = unitOfWork; } public IMethodInvocationResult Intercept(ISyncMethodInvocation methodInvocation) { var transactionAttribute = GetTransactionaAttributeOrNull(methodInvocation.InstanceMethodInfo); if (transactionAttribute == null || _unitOfWork.HasTransaction) return methodInvocation.InvokeNext(); using (var transaction = _unitOfWork.BeginTransaction(transactionAttribute.IsolationLevel)) { var result = methodInvocation.InvokeNext(); if (result.Successful) transaction.Commit(); else { transaction.Rollback(); } return result; } } private static TransactionalAttribute GetTransactionaAttributeOrNull(MemberInfo methodInfo) { var transactionalAttribute = ReflectionHelper.GetAttributesOfMemberAndDeclaringType<TransactionalAttribute>( methodInfo ).FirstOrDefault(); return transactionalAttribute; } }
واسط ISyncInterceptionBehavior، مربوط میشود به کتابخانه جانبی دیگری که برای AOP توسط تیم StructureMap به نام StructureMap.DynamicInterception ارائه شدهاست. در متد Intercept، ابتدا چک میشود که که آیا این متد با TransactionAttribute تزئین شده و طی درخواست جاری برای Context جاری تراکنشی ایجاد نشده باشد؛ سپس تراکنش جدیدی ایجاد شده و بدنه اصلی متد اجرا میشود و نهایتا در صورت موفقیت آمیز بودن عملیات، تراکنش مورد نظر Commit میشود.
در آخر لازم است این Interceptor در تنظیمات اولیه StructureMap به شکل زیر معرفی شود:
Policies.Interceptors(new DynamicProxyInterceptorPolicy( type => typeof(IApplicationService).IsAssignableFrom(type), typeof(AuthorizationInterceptor), typeof(TransactionInterceptor), typeof(ValidationInterceptor)));
نکته: فرض کنید در بدنه اکشن متد یک کنترلر ASP.NET MVC یا ASP.NET Core، دو متد تراکنشی فراخوانی شود؛ در این صورت شاید لازم باشد که این دو متد طی یک تراکنش واحد به جای تراکنشهای مجزا، اجرا شوند؛ بنابراین نیاز است از الگوی Transaction Per Request استفاده شود. برای این کار میتوان یک ActionFilterAttribute سفارشی ایجاد کرد که ایجاد کننده تراکنش باشد و متدهای داخلی که هر کدام جدا تراکنشی بودند، نیز از تراکنش ایجاد شده استفاده کنند.
private enum VisiteType { FirstVisite = 2, Reviste = 4, }
if (httpContext.Cache[ipAddress] != null) { hits++; if (hits == (int)VisiteType.FirstVisite) { httpContext.Cache.Insert(key: ipAddress, value: hits, dependencies: null, absoluteExpiration: DateTime.UtcNow.AddSeconds(1), slidingExpiration: Cache.NoSlidingExpiration); return; }
using System; using System.Net; using System.Web; using System.Web.Caching; namespace IpBlocker { public class IpBlocker : IHttpModule { private int hits = 0; /// <summary> /// Define enum to specify visite type /// </summary> private enum VisiteType { FirstVisite = 2, Reviste = 4, } public void Init(HttpApplication context) { context.BeginRequest += OnBeginRequest; } public void Dispose() { } public void OnBeginRequest(object sender, EventArgs e) { var httpApplication = sender as HttpApplication; var httpContext = httpApplication?.Context; ProcessRequest(httpApplication, httpContext); } private void ProcessRequest(HttpApplication application, HttpContext httpContext) { //Checke if browser is a search engine web crawler if (httpContext.Request.Browser.Crawler) return; var ipAddress = application.Context.Request.UserHostAddress; if (httpContext.Cache[ipAddress] == null) { // Reset hits for new request after blocking ip if (hits > 0) hits = 0; hits++; httpContext.Cache.Insert(key: ipAddress, value: hits, dependencies: null, absoluteExpiration: DateTime.UtcNow.AddSeconds(1), slidingExpiration: Cache.NoSlidingExpiration); return; } else { if (httpContext.Cache[ipAddress] != null) { hits++; if (hits == (int)VisiteType.FirstVisite) { httpContext.Cache.Insert(key: ipAddress, value: hits, dependencies: null, absoluteExpiration: DateTime.UtcNow.AddSeconds(1), slidingExpiration: Cache.NoSlidingExpiration); return; } if (hits == (int)VisiteType.Reviste) { httpContext.Cache.Insert(key: ipAddress, value: hits, dependencies: null, absoluteExpiration: DateTime.UtcNow.AddSeconds(1), slidingExpiration: Cache.NoSlidingExpiration); return; } if (hits > (int)VisiteType.Reviste) { httpContext.Cache.Insert(key: ipAddress, value: hits, dependencies: null, absoluteExpiration: DateTime.UtcNow.AddMinutes(1), slidingExpiration: Cache.NoSlidingExpiration); httpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; httpContext.Response.SuppressContent = true; httpContext.Response.End(); } } } } } }
معرفی System.Text.Json در NET Core 3.0.
public class IntConverter : JsonConverter<int> { public override int Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var value = reader.GetString (); if (int.TryParse (value, out _)) return int.Parse (value); throw new NotSupportedException ($"`{value}` can't be converted to `int`."); } public override void Write (Utf8JsonWriter writer, int value, JsonSerializerOptions options) { writer.WriteNumberValue (value); } }
واژهی کلیدی جدید in
C# 7.2، واژهی کلیدی جدیدی را به نام in جهت تعریف پارامترها، معرفی کردهاست. زمانیکه از آن استفاده میشود به این معنا است که value type ارسالی به آن، توسط ارجاعی از آن، در اختیار متد قرار میگیرد و نه توسط مقدار کپی شدهی آن (حالت پیشفرض) و همچنین متد استفاده کنندهی از آن، مقدار این شیء را تغییر نمیدهد.
واژهی کلیدی in مکمل واژههای کلیدی ref و out است که پیشتر به همراه زبان #C ارائه شده بودند:
- واژهی کلیدی out: مقدار آرگومان مزین شدهی توسط آن، باید درون متد تنظیم شود و صرفا کاربرد ارائهی یک خروجی اضافهتر توسط آن متد را دارد.
- واژهی کلیدی ref: مقدار آرگومان مزین شدهی توسط آن، ممکن است درون متد تنظیم شود، یا خیر و همچنین توسط ارجاع به آن منتقل میشود.
- واژهی کلیدی in: مقدار آرگومان مزین شدهی توسط آن، درون متد تغییر نخواهد کرد و همچنین توسط ارجاع به آن منتقل میشود.
برای مثال اگر پارامترهای value type متد زیر را از نوع in معرفی کنیم، امکان تغییر مقدار آنها درون متد وجود نخواهد داشت:
public static int Add(in int number1, in int number2) { number1 = 5; // Cannot assign to variable 'in int' because it is a readonly variable return number1 + number2; }
واژهی کلیدی جدید in تا چه اندازهای بر روی کارآیی برنامه تاثیر دارد؟
زمانیکه یک value type را به متدی ارسال میکنیم، ابتدا به مکان جدیدی از حافظه کپی شده و سپس مقدار clone شدهی آن، به متد ارسال میشود. با استفاده از واژهی کلیدی in، دقیقا همان ارجاع به مقدار اولیه، به متد ارسال خواهد شد؛ بدون ایجاد کپی اضافهتری از آن. برای بررسی تاثیر این عملیات بر روی کارآیی برنامه، میتوان از BenchmarkDotNet استفاده کرد. برای این منظور ابتدا ارجاعی را به BenchmarkDotNet اضافه میکنیم:
<ItemGroup> <PackageReference Include="BenchmarkDotNet" Version="0.10.12" /> </ItemGroup>
using BenchmarkDotNet.Attributes; namespace CS72Tests { public struct Input { public decimal Number1; public decimal Number2; } [MemoryDiagnoser] public class InBenchmarking { const int loops = 50000000; Input inputInstance = new Input(); [Benchmark(Baseline = true)] public decimal RunNormalLoop_Pass_By_Value() { decimal result = 0M; for (int i = 0; i < loops; i++) { result = Run(inputInstance); } return result; } [Benchmark] public decimal RunInLoop_Pass_By_Reference() { decimal result = 0M; for (int i = 0; i < loops; i++) { result = RunIn(in inputInstance); } return result; } public decimal Run(Input input) { return input.Number1; } public decimal RunIn(in Input input) { return input.Number1; } } }
static void Main(string[] args) { var summary = BenchmarkRunner.Run<InBenchmarking>();
با این خروجی نهایی:
Method | Mean | Error | StdDev | Scaled | Allocated | ---------------------------- |----------:|---------:|---------:|-------:|----------:| RunNormalLoop_Pass_By_Value | 280.04 ms | 2.219 ms | 1.733 ms | 1.00 | 0 B | RunInLoop_Pass_By_Reference | 91.75 ms | 1.733 ms | 1.780 ms | 0.33 | 0 B |
امکان استفادهی از واژهی کلیدی in در حین تعریف متدهای الحاقی
در حین تعریف متدهای الحاقی، واژهی کلیدی in باید پیش از واژهی کلیدی this ذکر شود:
public static class Factorial { public static int Calculate(in this int num) { int result = 1; for (int i = num; i > 1; i--) result *= i; return result; } }
int num = 3; Console.WriteLine($"(in num) -> {Factorial.Calculate(in num)}"); Console.WriteLine($"(num) -> {Factorial.Calculate(num)}"); Console.WriteLine($"num. -> {num.Calculate()}");
(in num) -> 6 (num) -> 6 num. -> 6
و به طور کلی استفادهی از in در مکانهای ذیل مجاز است:
• methods
• delegates
• lambdas
• local functions
• indexers
• operators
محدودیتهای استفادهی از پارامترهای in
الف) محدودیت استفاده از پارامترهای in در تعریف overloads
مثال زیر را در نظر بگیرید:
public class CX { public void A(Input a) { Console.WriteLine("int a"); } public void A(in Input a) { Console.WriteLine("in int a"); } }
اگر سعی کنیم وهلهای از این کلاس را ایجاد کرده و از متدهای A آن استفاده کنیم:
public class Y { public void Test() { var inputInstance = new Input(); var cx = new CX(); cx.A(inputInstance); // The call is ambiguous between the following methods or properties: 'CX.A(Input)' and 'CX.A(in Input)' } }
ب) پارامترهای از نوع in را در متدهای iterator نمیتوان استفاده کرد:
public IEnumerable<int> B(in int a) // Iterators cannot have ref or out parameters { Console.WriteLine("in int a"); yield return 1; }
ج) پارامترهای از نوع in را در متدهای async نمیتوان استفاده کرد:
public async Task C(in int a) // Async methods cannot have ref or out parameters { await Task.Delay(1000); }
تاثیر کار با متدهای داخلی تغییر دهندهی وضعیت یک struct
مثال زیر را درنظر بگیرید. به نظر شما خروجی آن چیست؟
using System; namespace CS72Tests { struct MyStruct { public int MyValue { get; set; } public void UpdateMyValue(int value) { MyValue = value; } } public static class TestInStructs { public static void Run() { var myStruct = new MyStruct(); myStruct.UpdateMyValue(1); UpdateMyValue(myStruct); Console.WriteLine(myStruct.MyValue); } static void UpdateMyValue(in MyStruct myStruct) { myStruct.UpdateMyValue(5); } } }
در ابتدا مقدار struct را به 1 تنظیم و سپس ارجاع آنرا به متدی دیگر که مقدار آنرا به 5 تنظیم میکند، ارسال کردیم. در این حالت برنامه بدون مشکل کامپایل و اجرا میشود. علت اینجا است که کامپایلر #C زمانیکه متدی را در داخل یک struct فراخوانی میکند، یک clone از آن struct را ایجاد کرده و متد را بر روی آن clone اجرا میکند؛ چون نمیداند که آیا این متد وضعیت و مقدار این struct را تغییر میدهد یا خیر. در این حالت کپی اصلی بدون تغییر باقی میماند (در نهایت عدد 1 را مشاهده خواهیم کرد)، اما در آخر فراخوان، ارجاعی از struct را دریافت نکرده و بر روی کپی آن کار میکند. بنابراین مزیت بهبود کارآیی، از دست خواهد رفت.
البته در اینجا اگر میخواستیم مقدار MyValue را مستقیما تغییر دهیم، کامپایلر از آن جلوگیری میکرد و این کد هیچگاه کامپایل نمیشد:
static void UpdateMyValue(in MyStruct myStruct) { myStruct.MyValue = 5; // Cannot assign to a member of variable 'in MyStruct' because it is a readonly variable myStruct.UpdateMyValue(5); }
مجوز استفاده از فایلهای جاوا اسکریپتی آن MIT است؛ به این معنا که در هر نوع پروژهای قابل استفاده است. مجوز استفاده از کامپوننتهای سمت سرور آن که برای نمونه جهت ASP.NET MVC یک سری HTML Helper را تدارک دیدهاند، تجاری میباشد. در ادامه قصد داریم صرفا از فایلهای JS عمومی آن استفاده کنیم.
دریافت jqGrid
برای دریافت jqGrid میتوانید به مخزن کد آن، در آدرس https://github.com/tonytomov/jqGrid/releases و یا از طریق NuGet اقدام کنید:
PM> Install-Package Trirand.jqGrid
از jQuery UI برای تولید صفحات جستجوی بر روی رکوردها و همچنین تولید خودکار صفحات ویرایش و یا افزودن رکوردها استفاده میکند. به علاوه آیکنها، قالب و رنگ خود را نیز از jQuery UI دریافت میکند. بنابراین اگر قصد تغییر قالب آنرا داشتید تنها کافی است یک قالب استاندارد دیگر jQuery UI را مورد استفاده قرار دهید.
تنظیمات اولیه فایل Layout سایت
پس از دریافت بستهی نیوگت jqGrid، نیاز است فایلهای مورد نیاز اصلی آنرا به شکل زیر به فایل layout پروژه اضافه کرد:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>@ViewBag.Title - My ASP.NET Application</title> <link href="~/Content/themes/base/jquery.ui.all.css" rel="stylesheet" /> <link href="~/Content/jquery.jqGrid/ui.jqgrid.css" rel="stylesheet" /> <link href="~/Content/Site.css" rel="stylesheet" type="text/css" /> </head> <body> <div> @RenderBody() </div> <script src="~/Scripts/jquery-1.7.2.min.js"></script> <script src="~/Scripts/jquery-ui-1.8.11.min.js"></script> <script src="~/Scripts/i18n/grid.locale-fa.js"></script> <script src="~/Scripts/jquery.jqGrid.min.js"></script> @RenderSection("Scripts", required: false) </body> </html>
این گرید به همراه فایل زبان فارسی grid.locale-fa.js نیز میباشد که در کدهای فوق پیوست شدهاست. البته اگر فرصت کردید نیاز است کمی ترجمههای آن بهبود پیدا کنند.
تنظیمات ثانویه site.css
.ui-widget { } /*how to move jQuery dialog close (X) button from right to left*/ .ui-jqgrid .ui-jqgrid-caption-rtl { text-align: center !important; } .ui-dialog .ui-dialog-titlebar-close { left: .3em !important; } .ui-dialog .ui-dialog-title { margin: .1em 0 .1em .8em !important; direction: rtl !important; float: right !important; }
همچنین محل قرار گیری دکمهی بسته شدن دیالوگها و راست به چپ کردن عناوین آنها نیز در اینجا قید شدهاند.
مدل برنامه
در ادامه قصد داریم لیستی از محصولات را با ساختار ذیل، توسط jqGrid نمایش دهیم:
namespace jqGrid01.Models { public class Product { public int Id { set; get; } public string Name { set; get; } public decimal Price { set; get; } public bool IsAvailable { set; get; } } }
ساختار دادهای مورد نیاز توسط jqGrid
jqGrid مستقل است از فناوری سمت سرور. بنابراین هر چند در عنوان بحث ASP.NET MVC ذکر شدهاست، اما از ASP.NET MVC صرفا جهت بازگرداندن خروجی JSON استفاده خواهیم کرد و این مورد در هر فناوری سمت سرور دیگری نیز میتواند انجام شود.
using System.Collections.Generic; namespace jqGrid01.Models { public class JqGridData { public int Total { get; set; } public int Page { get; set; } public int Records { get; set; } public IList<JqGridRowData> Rows { get; set; } public object UserData { get; set; } } public class JqGridRowData { public int Id { set; get; } public IList<string> RowCells { set; get; } } }
Total، نمایانگر تعداد صفحات اطلاعات است. عدد Page، شماره صفحهی جاری است. عدد Records، تعداد کل رکوردهای گزارش را مشخص میکند. ساختار ردیفهای آن نیز تشکیل شدهاست از یک Id به همراه سلولهایی که باید با فرمت string، بازگشت داده شوند.
UserData اختیاری است. برای مثال اگر خواستید جمع کل صفحه را در ذیل گرید نمایش دهید، میتوانید یک anonymous object را در اینجا مقدار دهی کنید. خاصیتهای آن دقیقا باید با نام خاصیتهای ستونهای متناظر، یکی باشند. برای مثال اگر میخواهید عددی را در ستون Id، در فوتر گرید نمایش دهید، باید نام خاصیت را Id ذکر کنید.
کدهای سمت کلاینت گرید
در اینجا کدهای کامل سمت کلاینت گرید را ملاحظه میکنید:
@{ ViewBag.Title = "Index"; } <div dir="rtl" align="center"> <div id="rsperror"></div> <table id="list" cellpadding="0" cellspacing="0"></table> <div id="pager" style="text-align:center;"></div> </div> @section Scripts { <script type="text/javascript"> $(document).ready(function () { $('#list').jqGrid({ caption: "آزمایش اول", //url from wich data should be requested url: '@Url.Action("GetProducts","Home")', //type of data datatype: 'json', jsonReader: { root: "Rows", page: "Page", total: "Total", records: "Records", repeatitems: true, userdata: "UserData", id: "Id", cell: "RowCells" }, //url access method type mtype: 'GET', //columns names colNames: ['شماره', 'نام محصول', 'موجود است', 'قیمت'], //columns model colModel: [ { name: 'Id', index: 'Id', align: 'right', width: 50, sorttype: "number" }, { name: 'Name', index: 'Name', align: 'right', width: 300 }, { name: 'IsAvailable', index: 'IsAvailable', align: 'center', width: 100, formatter: 'checkbox' }, { name: 'Price', index: 'Price', align: 'center', width: 100, sorttype: "number" } ], //pager for grid pager: $('#pager'), //number of rows per page rowNum: 10, rowList: [10, 20, 50, 100], //initial sorting column sortname: 'Id', //initial sorting direction sortorder: 'asc', //we want to display total records count viewrecords: true, altRows: true, shrinkToFit: true, width: 'auto', height: 'auto', hidegrid: false, direction: "rtl", gridview: true, rownumbers: true, footerrow: true, userDataOnFooter: true, loadComplete: function() { //change alternate rows color $("tr.jqgrow:odd").css("background", "#E0E0E0"); }, loadError: function(xhr, st, err) { jQuery("#rsperror").html("Type: " + st + "; Response: " + xhr.status + " " + xhr.statusText); } //, loadonce: true }) .jqGrid('navGrid', "#pager", { edit: false, add: false, del: false, search: false, refresh: true }) .jqGrid('navButtonAdd', '#pager', { caption: "تنظیم نمایش ستونها", title: "Reorder Columns", onClickButton: function() { jQuery("#list").jqGrid('columnChooser'); } }); }); </script> }
Div سومی با id مساوی rsperror نیز تعریف شدهاست که از آن جهت نمایش خطاهای بازگشت داده شده از سرور استفاده کردهایم.
- در ادامه نحوهی فراخوانی افزونهی jqGrid را بر روی جدول list ملاحظه میکنید.
- خاصیت caption، عنوان نمایش داده شده در بالای گرید را مقدار دهی میکند:
- خاصیت url، به آدرسی اشاره میکند که قرار است ساختار JqGridData ایی را که پیشتر در مورد آن بحث کردیم، با فرمت JSON بازگشت دهد. در اینجا برای مثال به یک اکشن متد کنترلری در یک پروژهی ASP.NET MVC اشاره میکند.
- datatype را برابر json قرار دادهایم. از نوع xml نیز پشتیبانی میکند.
- شیء jsonReader را از این جهت مقدار دهی کردهایم تا بتوانیم شیء JqGridData را با اصول نامگذاری دات نت، هماهنگ کنیم. برای درک بهتر این موضوع، فایل jquery.jqGrid.src.js را باز کنید و در آن به دنبال تعریف jsonReader بگردید. به یک چنین مقادیر پیش فرضی خواهید رسید:
ts.p.jsonReader = $.extend(true,{ root: "rows", page: "page", total: "total", records: "records", repeatitems: true, cell: "cell", id: "id", userdata: "userdata", subgrid: {root:"rows", repeatitems: true, cell:"cell"} },ts.p.jsonReader);
- در ادامه mtype به GET تنظیم شدهاست. در اینجا مشخص میکنیم که عملیات Ajax ایی دریافت اطلاعات از سرور توسط GET انجام شود یا برای مثال توسط POST.
- خاصیت colNames، معرف نام ستونهای گرید است. برای اینکه این نامها از راست به چپ نمایش داده شوند، باید خاصیت direction به rtl تنظیم شود.
- colModel آرایهای است که تعاریف ستونها را در بر دارد. مقدار name آن باید یک نام منحصربفرد باشد. از این نام در حین جستجو یا ویرایش اطلاعات استفاده میشود. مقدار index نامی است که جهت مرتب سازی اطلاعات، به سرور ارسال میشود. تنظیم sorttype در اینجا مشخص میکند که آیا به صورت پیش فرض، ستون جاری رشتهای مرتب شود یا اینکه برای مثال عددی پردازش گردد. مقادیر مجاز آن text (مقدار پیش فرض)، float، number، currency، numeric، int ، integer، date و datetime هستند.
- در ستون IsAvailable، مقدار formatter نیز تنظیم شدهاست. در اینجا توسط formatter، نوع bool دریافتی با یک checkbox نمایش داده خواهد شد.
- خاصیت pager به id متناظری در صفحه اشاره میکند.
- توسط rowNum مشخص میکنیم که در هر صفحه چه تعداد رکورد باید نمایش داده شوند.
- تعداد رکوردهای نمایش داده شده را میتوان توسط rowList پویا کرد. در اینجا آرایهای را ملاحظه میکنید که توسط اعداد آن، کاربر امکان انتخاب صفحاتی مثلا 100 ردیفه را نیز پیدا میکند. rowList به صورت یک dropdown در کنار عناصر راهبری صفحه در فوتر گرید ظاهر میشود.
- خاصیت sortname، نحوهی مرتب سازی اولیه گرید را مشخص میکند.
- خاصیت sortorder، جهت مرتب سازی اولیهی گردید را تنظیم میکند.
- viewrecords: تعداد رکوردها را در نوار ابزار پایین گرید نمایش میدهد.
- altRows: سبب میشود رنگ متن ردیفها یک در میان متفاوت باشد.
- shrinkToFit: به معنای تنظیم خودکار اندازهی سلولها بر اساس اندازهی دادهای است که دریافت میکنند.
- width: عرض گرید، که در اینجا به auto تنظیم شدهاست.
- height: طول گرید، که در اینجا به auto جهت محاسبهی خودکار، تنظیم شدهاست.
- gridview: برای بالا بردن سرعت نمایشی به true تنظیم شدهاست. در این حالت کل ردیف یکباره درج میشود. اگر از subgird یا حالت نمایش درختی استفاده شود، باید این خاصیت را false کرد.
- rownumbers: ستون سمت راست شماره ردیفهای خودکار را نمایش میدهد.
- footerrow: سبب نمایش ردیف فوتر میشود.
- userDataOnFooter: سبب خواهد شد تا خاصیت UserData مقدار دهی شده، در ردیف فوتر ظاهر شود.
- loadComplete : یک callback است که زمان پایان بارگذاری صفحهی جاری را مشخص میکند. در اینجا با استفاده از jQuery سبب شدهایم تا رنگ پس زمینهی ردیفها یک در میان تغییر کند.
- loadError: اگر از سمت سرور خطایی صادر شود، در این callback قابل دریافت خواهد بود.
- در ادامه توسط فراخوانی متد jqGrid با پارامتر navGrid، در ناحیه pager سبب نمایش دکمه refresh شدهایم. این دکمه سبب بارگذاری مجدد اطلاعات گردید از سرور میشود.
- همچنین به کمک متد jqGrid با پارامتر navButtonAdd در ناحیه pager، سبب نمایش دکمهای که صفحهی انتخاب ستونها را ظاهر میکند، خواهیم شد.
پیشنیاز کدهای سمت سرور jqGrid
اگر به تنظیمات گرید دقت کرده باشید، خاصیت index ستونها، نامی است که به سرور، جهت اطلاع رسانی در مورد فیلتر اطلاعات و مرتب سازی مجدد آنها ارسال میگردد. این نام، بر اساس کلیک کاربر بر روی ستونهای موجود، هر بار میتوان متفاوت باشد. بنابراین بجای if و else نوشتنهای طولانی جهت مرتب سازی اطلاعات، میتوان از کتابخانهی معروفی به نام dynamic LINQ استفاده کرد.
PM> Install-Package DynamicQuery
کدهای سمت سرور بازگشت اطلاعات به فرمت JSON
در کدهای سمت کلاینت، به اکشن متد GetProducts اشاره شده بود. تعاریف کامل آنرا در ذیل مشاهده میکنید:
using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Web.Mvc; using jqGrid01.Models; using jqGrid01.Extensions; // for dynamic OrderBy namespace jqGrid01.Controllers { public class HomeController : Controller { public ActionResult Index() { return View(); } public ActionResult GetProducts(string sidx, string sord, int page, int rows) { var list = ProductDataSource.LatestProducts; var pageIndex = page - 1; var pageSize = rows; var totalRecords = list.Count; var totalPages = (int)Math.Ceiling(totalRecords / (float)pageSize); var products = list.AsQueryable() .OrderBy(sidx + " " + sord) .Skip(pageIndex * pageSize) .Take(pageSize) .ToList(); var jqGridData = new JqGridData { UserData = new // نمایش در فوتر { Name = "جمع صفحه", Price = products.Sum(x => x.Price) }, Total = totalPages, Page = page, Records = totalRecords, Rows = (products.Select(product => new JqGridRowData { Id = product.Id, RowCells = new List<string> { product.Id.ToString(CultureInfo.InvariantCulture), product.Name, product.IsAvailable.ToString(), product.Price.ToString(CultureInfo.InvariantCulture) } })).ToList() }; return Json(jqGridData, JsonRequestBehavior.AllowGet); } } }
- امضای متد GetProducts نیز مهم است. دقیقا همین پارامترها با همین نامها از طرف jqGrid به سرور ارسال میشوند که توسط آنها ستون مرتب سازی، جهت مرتب سازی، صفحهی جاری و تعداد ردیفی که باید بازگشت داده شوند، قابل دریافت است.
- در این کدها دو قسمت مهم وجود دارند:
الف) متد OrderBy نوشته شده، به صورت پویا عمل میکند و از کتابخانهی Dynamic LINQ مایکروسافت بهره میبرد.
به علاوه توسط Take و Skip کار صفحه بندی و بازگشت تنها بازهای از اطلاعات مورد نیاز، انجام میشود.
ب) لیست جنریک محصولات، در نهایت باید با فرمت JqGridData به صورت JSON بازگشت داده شود. نحوهی این Projection را در اینجا میتوانید ملاحظه کنید.
هر ردیف این لیست، باید تبدیل شود به ردیفی از جنس JqGridRowData، تا توسط jqGrid قابل پردازش گردد.
- توسط مقدار دهی UserData، برچسبی را در ذیل ستون Name و مقداری را در ذیل ستون Price نمایش خواهیم داد.
برای مطالعهی بیشتر
بهترین راهنمای جزئیات این Grid، مستندات آنلاین آن هستند: http://www.trirand.com/jqgridwiki/doku.php?id=wiki:jqgriddocs
همچنین این مستندات را با فرمت PDF نیز میتوانید مطالعه کنید: http://www.trirand.com/blog/jqgrid/downloads/jqgriddocs.pdf
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید
jqGrid01.zip
مثالهای سری jqGrid تغییرات زیادی داشتند. برای دریافت آنها به این مخزن کد مراجعه کنید.
تغییرات کتابخانه DNTFrameworkCore
public interface ICreationTracking { DateTime CreatedDateTime { get; set; } } public interface IModificationTracking { DateTime? ModifiedDateTime { get; set; } }
public interface IHasRowIntegrity { string Hash { get; set; } } public interface IHasRowVersion { byte[] Version { get; set; } }
public abstract class AggregateRoot<TKey> : Entity<TKey>, IAggregateRoot where TKey : IEquatable<TKey> { private readonly List<IDomainEvent> _events = new List<IDomainEvent>(); public IReadOnlyCollection<IDomainEvent> Events => _events.AsReadOnly(); protected virtual void AddDomainEvent(IDomainEvent newEvent) { _events.Add(newEvent); } public virtual void ClearEvents() { _events.Clear(); } }
public static class EventBusExtensions { public static Task TriggerAsync(this IEventBus bus, IEnumerable<IDomainEvent> events) { var tasks = events.Select(async domainEvent => await bus.TriggerAsync(domainEvent)); return Task.WhenAll(tasks); } public static async Task PublishAsync(this IEventBus bus, IAggregateRoot aggregateRoot) { await bus.TriggerAsync(aggregateRoot.Events); aggregateRoot.ClearEvents(); } }
public interface IKeyValueService : IApplicationService { Task SetValueAsync(string key, string value); Task<Maybe<string>> LoadValueAsync(string key); Task<bool> IsTamperedAsync(string key); } public class KeyValue : Entity, IModificationTracking, ICreationTracking, IHasRowIntegrity { public string Key { get; set; } [Encrypted] public string Value { get; set; } public string Hash { get; set; } public DateTime CreatedDateTime { get; set; } public DateTime? ModifiedDateTime { get; set; } }
public class ExceptionOptions { public List<ExceptionMapItem> Mappings { get; } = new List<ExceptionMapItem>(); [Required] public string DbException { get; set; } [Required] public string DbConcurrencyException { get; set; } [Required] public string InternalServerIssue { get; set; } public bool TryFindMapping(DbException dbException, out ExceptionMapItem mapping) { mapping = null; var words = new HashSet<string>(Regex.Split(dbException.ToStringFormat(), @"\W")); var mappingItem = Mappings.FirstOrDefault(a => a.Keywords.IsProperSubsetOf(words)); if (mappingItem == null) { return false; } mapping = mappingItem; return true; } }
"Exception": { "Mappings": [ { "Message": "به دلیل وجود اطلاعات وابسته امکان حذف وجود ندارد", "Keywords": [ "DELETE", "REFERENCE" ] }, { "Message": "یک تسک با این عنوان قبلا در سیستم ثبت شده است", "MemberName": "Title", "Keywords": [ "Task", "UIX_Task_NormalizedTitle" ] } ], "DbException": "امکان ذخیرهسازی اطلاعات وجود ندارد؛ دوباره تلاش نمائید", "DbConcurrencyException": "اطلاعات توسط کاربری دیگر در شبکه تغییر کرده است", "InternalServerIssue": "متأسفانه مشکلی در فرآیند انجام درخواست شما پیش آمده است!" }
_session.UserId _session.UserId<long>() _session.UserId<int>() _session.UserId<Guid>()
public static class UserClaimTypes { public const string UserName = ClaimTypes.Name; public const string UserId = ClaimTypes.NameIdentifier; public const string SerialNumber = ClaimTypes.SerialNumber; public const string Role = ClaimTypes.Role; public const string DisplayName = nameof(DisplayName); public const string BranchId = nameof(BranchId); public const string BranchName = nameof(BranchName); public const string IsHeadOffice = nameof(IsHeadOffice); public const string TenantId = nameof(TenantId); public const string TenantName = nameof(TenantName); public const string IsHeadTenant = nameof(IsHeadTenant); public const string Permission = nameof(Permission); public const string PackedPermission = nameof(PackedPermission); public const string ImpersonatorUserId = nameof(ImpersonatorUserId); public const string ImpersonatorTenantId = nameof(ImpersonatorTenantId); }
/// <summary> /// Adds a file logger named 'File' to the factory. /// </summary> /// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param> public static ILoggingBuilder AddFile(this ILoggingBuilder builder) { builder.Services.AddSingleton<ILoggerProvider, FileLoggerProvider>(); return builder; } /// <summary> /// Adds a file logger named 'File' to the factory. /// </summary> /// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param> /// <param name="configure">Configure an instance of the <see cref="FileLoggerOptions" /> to set logging options</param> public static ILoggingBuilder AddFile(this ILoggingBuilder builder, Action<FileLoggerOptions> configure) { builder.AddFile(); builder.Services.Configure(configure); return builder; }
public interface ITenantResolutionStrategy { string TenantId(); } public interface ITenantStore { Task<Tenant> FindTenantAsync(string tenantId); }
public interface ITenantSession : IScopedDependency { /// <summary> /// Gets current TenantId or null. /// This TenantId should be the TenantId of the <see cref="IUserSession.UserId" />. /// It can be null if given <see cref="IUserSession.UserId" /> is a head-tenant user or no user logged in. /// </summary> string TenantId { get; } /// <summary> /// Gets current TenantName or null. /// This TenantName should be the TenantName of the <see cref="IUserSession.UserId" />. /// It can be null if given <see cref="IUserSession.UserId" /> is a head-tenant user or no user logged in. /// </summary> string TenantName { get; } /// <summary> /// Represents current tenant is head-tenant. /// </summary> bool IsHeadTenant { get; } /// <summary> /// TenantId of the impersonator. /// This is filled if a user with <see cref="IUserSession.ImpersonatorUserId" /> performing actions behalf of the /// <see cref="IUserSession.UserId" />. /// </summary> string ImpersonatorTenantId { get; } }
public static class SystemTime { public static Func<DateTime> Now = () => DateTime.UtcNow; public static Func<DateTime, DateTime> Normalize = (dateTime) => DateTime.SpecifyKind(dateTime, DateTimeKind.Utc); }
public interface IClock : ITransientDependency { DateTime Now { get; } DateTime Normalize(DateTime dateTime); } internal sealed class Clock : IClock { public DateTime Now => SystemTime.Now(); public DateTime Normalize(DateTime dateTime) { return SystemTime.Normalize(dateTime); } }
public class Result { private static readonly Result _ok = new Result(false, string.Empty); private readonly List<ValidationFailure> _failures; protected Result(bool failed, string message) : this(failed, message, Enumerable.Empty<ValidationFailure>()) { Failed = failed; Message = message; } protected Result(bool failed, string message, IEnumerable<ValidationFailure> failures) { Failed = failed; Message = message; _failures = failures.ToList(); } public bool Failed { get; } public string Message { get; } public IEnumerable<ValidationFailure> Failures => _failures.AsReadOnly(); [DebuggerStepThrough] public static Result Ok() => _ok; [DebuggerStepThrough] public static Result Fail(string message) { return new Result(true, message); } //... }
services.AddFramework() .WithModelValidation() .WithFluentValidation() .WithMemoryCache() .WithSecurityService() .WithBackgroundTaskQueue() .WithRandomNumber();
تغییرات کتابخانه DNTFrameworkCore.EFCore
public abstract class TrackableEntity<TKey> : Entity<TKey>, ITrackable where TKey : IEquatable<TKey> { [NotMapped] public TrackingState TrackingState { get; set; } [NotMapped] public ICollection<string> ModifiedProperties { get; set; } }
public static class ConfigurationBuilderExtensions { public static IConfigurationBuilder AddEFCore(this IConfigurationBuilder builder, IServiceProvider provider) { return builder.Add(new EFConfigurationSource(provider)); } }
protected DbContextCore(DbContextOptions options, IEnumerable<IHook> hooks) : base(options) { _hooks = hooks ?? throw new ArgumentNullException(nameof(hooks)); }
public void IgnoreHook(string hookName) { _ignoredHookList.Add(hookName); }
internal sealed class RowIntegrityHook : PostActionHook<IHasRowIntegrity> { public override string Name => HookNames.RowIntegrity; public override int Order => int.MaxValue; public override EntityState HookState => EntityState.Unchanged; protected override void Hook(IHasRowIntegrity entity, HookEntityMetadata metadata, IUnitOfWork uow) { metadata.Entry.Property(EFCore.Hash).CurrentValue = uow.EntityHash(entity); } }
//DbContextCore : IUnitOfWork public string EntityHash<TEntity>(TEntity entity) where TEntity : class { var row = Entry(entity).ToDictionary(p => p.Metadata.Name != EFCore.Hash && !p.Metadata.ValueGenerated.HasFlag(ValueGenerated.OnUpdate) && !p.Metadata.IsShadowProperty()); return EntityHash<TEntity>(row); } protected virtual string EntityHash<TEntity>(Dictionary<string, object> row) where TEntity : class { var json = JsonConvert.SerializeObject(row, Formatting.Indented); using (var hashAlgorithm = SHA256.Create()) { var byteValue = Encoding.UTF8.GetBytes(json); var byteHash = hashAlgorithm.ComputeHash(byteValue); return Convert.ToBase64String(byteHash); } }
IsTamperedAsync HasTamperedEntryAsync TamperedEntryListAsync
public const string CreatedDateTime = nameof(ICreationTracking.CreatedDateTime); public const string CreatedByUserId = nameof(CreatedByUserId); public const string CreatedByBrowserName = nameof(CreatedByBrowserName); public const string CreatedByIP = nameof(CreatedByIP); public const string ModifiedDateTime = nameof(IModificationTracking.ModifiedDateTime); public const string ModifiedByUserId = nameof(ModifiedByUserId); public const string ModifiedByBrowserName = nameof(ModifiedByBrowserName); public const string ModifiedByIP = nameof(ModifiedByIP);
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.NormalizeDateTime(); modelBuilder.NormalizeDecimalPrecision(precision: 20, scale: 6); base.OnModelCreating(modelBuilder); }
MigrateDbContext<TContext>(this IHost host)
services.AddEFCore<ProjectDbContext>() .WithTrackingHook<long>() .WithDeletedEntityHook() .WithRowIntegrityHook() .WithNumberingHook(options => { options.NumberedEntityMap[typeof(Task)] = new NumberedEntityOption { Prefix = "Task", FieldNames = new[] {nameof(Task.BranchId)} }; });
تغییرات کتابخانه DNTFrameworkCore.Web.Tenancy
services.AddTenancy() .WithTenantSession() .WithStore<InMemoryTenantStore>() .WithResolutionStrategy<HostResolutionStrategy>();
app.UseTenancy();
سایر کتابخانهها تغییرات خاصی نداشتند و صرفا نحوه معرفی سرویسهای آنها ممکن است تغییر کند و یا وابستگیهای آنها به آخرین نسخه موجود ارتقاء داده شده باشند که در پروژه DNTFrameworkCore.TestAPI اعمال شدهاند.
PM> Install-Package DNTFrameworkCore PM> Install-Package DNTFrameworkCore.EFCore PM> Install-Package DNTFrameworkCore.EFCore.SqlServer PM> Install-Package DNTFrameworkCore.Web PM> Install-Package DNTFrameworkCore.FluentValidation PM> Install-Package DNTFrameworkCore.Web.Tenancy PM> Install-Package DNTFrameworkCore.Licensing