بررسی Hybrid Flow جهت امن سازی Web API
این Flow را پیشتر نیز مرور کرده بودیم. تفاوت آن با قسمتهای قبل، در استفاده از توکن دومی است به نام access token که به همراه identity token از طرف IDP صادر میشود و تا این قسمت از آن بجز در قسمت «دریافت اطلاعات بیشتری از کاربران از طریق UserInfo Endpoint» استفاده نکرده بودیم.
در اینجا، ابتدا برنامهی وب، یک درخواست اعتبارسنجی را به سمت IDP ارسال میکند که response type آن از نوع code id_token است (یا همان مشخصهی Hybrid Flow) و همچنین تعدادی scope نیز جهت دریافت claims متناظر با آنها در این درخواست ذکر شدهاند. در سمت IDP، کاربر با ارائهی مشخصات خود، اعتبارسنجی شده و پس از آن IDP صفحهی اجازهی دسترسی به اطلاعات کاربر (صفحهی consent) را ارائه میدهد. پس از آن IDP اطلاعات code و id_token را به سمت برنامهی وب ارسال میکند. در ادامه کلاینت وب، توکن هویت رسیده را اعتبارسنجی میکند. پس از موفقیت آمیز بودن این عملیات، اکنون کلاینت درخواست دریافت یک access token را از IDP ارائه میدهد. اینکار در پشت صحنه و بدون دخالت کاربر صورت میگیرد که به آن استفادهی از back channel هم گفته میشود. یک چنین درخواستی به token endpoint، شامل اطلاعات code و مشخصات دقیق کلاینت جاری است. به عبارتی نوعی اعتبارسنجی هویت برنامهی کلاینت نیز میباشد. در پاسخ، دو توکن جدید را دریافت میکنیم: identity token و access token. در اینجا access token توسط خاصیت at_hash موجود در id_token به آن لینک میشود. سپس هر دو توکن اعتبارسنجی میشوند. در این مرحله، میانافزار اعتبارسنجی، هویت کاربر را از identity token استخراج میکند. به این ترتیب امکان وارد شدن به برنامهی کلاینت میسر میشود. در اینجا همچنین access token ای نیز صادر شدهاست.
اکنون علاقمند به کار با Web API برنامهی کلاینت MVC خود هستیم. برای این منظور access token که اکنون در برنامهی MVC Client در دسترس است، به صورت یک Bearer token به هدر ویژهای با کلید Authorization اضافه میشود و به همراه هر درخواست، به سمت API ارسال خواهد شد. در سمت Web API این access token رسیده، اعتبارسنجی میشود و در صورت موفقیت آمیز بودن عملیات، دسترسی به منابع Web API صادر خواهد شد.
امن سازی دسترسی به Web API
تنظیمات برنامهی IDP
برای امن سازی دسترسی به Web API از کلاس src\IDP\DNT.IDP\Config.cs در سطح IDP شروع میکنیم. در اینجا باید یک scope جدید مخصوص دسترسی به منابع Web API را تعریف کنیم:
namespace DNT.IDP { public static class Config { // api-related resources (scopes) public static IEnumerable<ApiResource> GetApiResources() { return new List<ApiResource> { new ApiResource( name: "imagegalleryapi", displayName: "Image Gallery API", claimTypes: new List<string> {"role" }) }; }
پس از آن در قسمت تعریف کلاینت، مجوز درخواست این scope جدید imagegalleryapi را نیز صادر میکنیم:
AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.Address, "roles", "imagegalleryapi" },
namespace DNT.IDP { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddIdentityServer() .AddDeveloperSigningCredential() .AddTestUsers(Config.GetUsers()) .AddInMemoryIdentityResources(Config.GetIdentityResources()) .AddInMemoryApiResources(Config.GetApiResources()) .AddInMemoryClients(Config.GetClients()); }
تنظیمات برنامهی MVC Client
اکنون نوبت انجام تنظیمات برنامهی MVC Client در فایل ImageGallery.MvcClient.WebApp\Startup.cs است. در اینجا در متد AddOpenIdConnect، درخواست scope جدید imagegalleryapi را صادر میکنیم:
options.Scope.Add("imagegalleryapi");
تنظیمات برنامهی Web API
اکنون میخواهیم مطمئن شویم که Web API، به access token ای که قسمت Audience آن درست مقدار دهی شدهاست، دسترسی خواهد داشت.
برای این منظور به پوشهی پروژهی Web API در مسیر src\WebApi\ImageGallery.WebApi.WebApp وارد شده و دستور زیر را صادر کنید تا بستهی نیوگت AccessTokenValidation نصب شود:
dotnet add package IdentityServer4.AccessTokenValidation
using IdentityServer4.AccessTokenValidation; namespace ImageGallery.WebApi.WebApp { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(defaultScheme: IdentityServerAuthenticationDefaults.AuthenticationScheme) .AddIdentityServerAuthentication(options => { options.Authority = Configuration["IDPBaseAddress"]; options.ApiName = "imagegalleryapi"; });
سپس متد AddIdentityServerAuthentication فراخوانی شدهاست که به آدرس IDP اشاره میکند که مقدار آنرا در فایل appsettings.json قرار دادهایم. از این آدرس برای بارگذاری متادیتای IDP استفاده میشود. کار دیگر این میانافزار، اعتبارسنجی access token رسیدهی به آن است. مقدار خاصیت ApiName آن، به نام API resource تعریف شدهی در سمت IDP اشاره میکند. هدف این است که بررسی شود آیا خاصیت aud موجود در access token رسیده به مقدار imagegalleryapi تنظیم شدهاست یا خیر؟
پس از تنظیم این میانافزار، اکنون نوبت به افزودن آن به ASP.NET Core request pipeline است:
namespace ImageGallery.WebApi.WebApp { public class Startup { public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseAuthentication();
اکنون میتوانیم اجبار به Authorization را در تمام اکشن متدهای این Web API در فایل ImageGallery.WebApi.WebApp\Controllers\ImagesController.cs فعالسازی کنیم:
namespace ImageGallery.WebApi.WebApp.Controllers { [Route("api/images")] [Authorize] public class ImagesController : Controller {
ارسال Access Token به همراه هر درخواست به سمت Web API
تا اینجا اگر مراحل اجرای برنامهها را طی کنید، مشاهده خواهید کرد که برنامهی MVC Client دیگر کار نمیکند و نمیتواند از فیلتر Authorize فوق رد شود. علت اینجا است که در حال حاضر، تمامی درخواستهای رسیدهی به Web API، فاقد Access token هستند. بنابراین اعتبارسنجی آنها با شکست مواجه میشود.
برای رفع این مشکل، سرویس ImageGalleryHttpClient را به نحو زیر اصلاح میکنیم تا در صورت وجود Access token، آنرا به صورت خودکار به هدرهای ارسالی توسط HttpClient اضافه کند:
using System; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Protocols.OpenIdConnect; namespace ImageGallery.MvcClient.Services { public interface IImageGalleryHttpClient { Task<HttpClient> GetHttpClientAsync(); } /// <summary> /// A typed HttpClient. /// </summary> public class ImageGalleryHttpClient : IImageGalleryHttpClient { private readonly HttpClient _httpClient; private readonly IConfiguration _configuration; private readonly IHttpContextAccessor _httpContextAccessor; public ImageGalleryHttpClient( HttpClient httpClient, IConfiguration configuration, IHttpContextAccessor httpContextAccessor) { _httpClient = httpClient; _configuration = configuration; _httpContextAccessor = httpContextAccessor; } public async Task<HttpClient> GetHttpClientAsync() { var currentContext = _httpContextAccessor.HttpContext; var accessToken = await currentContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken); if (!string.IsNullOrWhiteSpace(accessToken)) { _httpClient.SetBearerToken(accessToken); } _httpClient.BaseAddress = new Uri(_configuration["WebApiBaseAddress"]); _httpClient.DefaultRequestHeaders.Accept.Clear(); _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); return _httpClient; } } }
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.1.1.0" /> <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.1.1.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.Abstractions" Version="2.1.1.0" /> <PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="5.2.0.0" /> <PackageReference Include="IdentityModel" Version="3.9.0" /> </ItemGroup> </Project>
البته پس از این تغییرات نیاز است به کنترلر گالری مراجعه و از متد جدید GetHttpClientAsync بجای خاصیت HttpClient قبلی استفاده کرد.
اکنون اگر برنامه را اجرا کنیم، پس از لاگین، دسترسی به Web API امن شده، برقرار شده و برنامه بدون مشکل کار میکند.
بررسی محتوای Access Token
اگر بر روی سطر if (!string.IsNullOrWhiteSpace(accessToken)) در سرویس ImageGalleryHttpClient یک break-point را قرار دهیم و محتویات Access Token را در حافظه ذخیره کنیم، میتوانیم با مراجعهی به سایت jwt.io، محتویات آنرا بررسی نمائیم:
که در حقیقت این محتوا را به همراه دارد:
{ "nbf": 1536394771, "exp": 1536398371, "iss": "https://localhost:6001", "aud": [ "https://localhost:6001/resources", "imagegalleryapi" ], "client_id": "imagegalleryclient", "sub": "d860efca-22d9-47fd-8249-791ba61b07c7", "auth_time": 1536394763, "idp": "local", "role": "PayingUser", "scope": [ "openid", "profile", "address", "roles", "imagegalleryapi" ], "amr": [ "pwd" ] }
همچنین اگر دقت کنید، Id کاربر جاری در خاصیت sub آن قرار دارد.
مدیریت صفحهی عدم دسترسی به Web API
با اضافه شدن scope جدید دسترسی به API در سمت IDP، این مورد در صفحهی دریافت رضایت کاربر نیز ظاهر میشود:
در این حالت اگر کاربر این گزینه را انتخاب نکند، پس از هدایت به برنامهی کلاینت، در سطر response.EnsureSuccessStatusCode استثنای زیر ظاهر خواهد شد:
An unhandled exception occurred while processing the request. HttpRequestException: Response status code does not indicate success: 401 (Unauthorized). System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()
public async Task<IActionResult> Index() { var httpClient = await _imageGalleryHttpClient.GetHttpClientAsync(); var response = await httpClient.GetAsync("api/images"); if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || response.StatusCode == System.Net.HttpStatusCode.Forbidden) { return RedirectToAction("AccessDenied", "Authorization"); } response.EnsureSuccessStatusCode();
فیلتر کردن تصاویر نمایش داده شده بر اساس هویت کاربر وارد شدهی به سیستم
تا اینجا هرچند دسترسی به API امن شدهاست، اما هنوز کاربر وارد شدهی به سیستم میتواند تصاویر سایر کاربران را نیز مشاهده کند. بنابراین قدم بعدی امن سازی API، عکس العمل نشان دادن به هویت کاربر جاری سیستم است.
برای این منظور به کنترلر ImageGallery.WebApi.WebApp\Controllers\ImagesController.cs سمت API مراجعه کرده و Id کاربر جاری را از لیست Claims او استخراج میکنیم:
namespace ImageGallery.WebApi.WebApp.Controllers { [Route("api/images")] [Authorize] public class ImagesController : Controller { [HttpGet()] public async Task<IActionResult> GetImages() { var ownerId = this.User.Claims.FirstOrDefault(claim => claim.Type == "sub").Value;
مرحلهی بعد، مراجعه به ImageGallery.WebApi.Services\ImagesService.cs و تغییر متد GetImagesAsync است تا صرفا بر اساس ownerId دریافت شده کار کند:
namespace ImageGallery.WebApi.Services { public class ImagesService : IImagesService { public Task<List<Image>> GetImagesAsync(string ownerId) { return _images.Where(image => image.OwnerId == ownerId).OrderBy(image => image.Title).ToListAsync(); }
namespace ImageGallery.WebApi.WebApp.Controllers { [Route("api/images")] [Authorize] public class ImagesController : Controller { [HttpGet()] public async Task<IActionResult> GetImages() { var ownerId = this.User.Claims.FirstOrDefault(claim => claim.Type == "sub").Value; var imagesFromRepo = await _imagesService.GetImagesAsync(ownerId); var imagesToReturn = _mapper.Map<IEnumerable<ImageModel>>(imagesFromRepo); return Ok(imagesToReturn); }
هنوز یک مشکل دیگر باقی است: سایر اکشن متدهای این کنترلر Web API همچنان محدود به کاربر جاری نشدهاند. یک روش آن تغییر دستی تمام کدهای آن است. در این حالت متد IsImageOwnerAsync زیر، جهت بررسی اینکه آیا رکورد درخواستی متعلق به کاربر جاری است یا خیر، به سرویس تصاویر اضافه میشود:
namespace ImageGallery.WebApi.Services { public class ImagesService : IImagesService { public Task<bool> IsImageOwnerAsync(Guid id, string ownerId) { return _images.AnyAsync(i => i.Id == id && i.OwnerId == ownerId); }
اما روش بهتر انجام این عملیات را که در قسمت بعدی بررسی میکنیم، بر اساس بستن دسترسی ورود به اکشن متدها بر اساس Authorization policy است. در این حالت اگر کاربری مجوز انجام عملیاتی را نداشت، اصلا وارد کدهای یک اکشن متد نخواهد شد.
ارسال سایر User Claims مانند نقشها به همراه یک Access Token
برای تکمیل قسمت ارسال تصاویر میخواهیم تنها کاربران نقش خاصی قادر به انجام اینکار باشند. اما اگر به محتوای access token ارسالی به سمت Web API دقت کرده باشید، حاوی Identity claims نیست. البته میتوان مستقیما در برنامهی Web API با UserInfo Endpoint، برای دریافت اطلاعات بیشتر، کار کرد که نمونهای از آنرا در قسمت قبل مشاهده کردید، اما مشکل آن زیاد شدن تعداد رفت و برگشتهای به سمت IDP است. همچنین باید درنظر داشت که فراخوانی مستقیم UserInfo Endpoint جهت برنامهی MVC client که درخواست دریافت access token را از IDP میدهد، متداول است و نه برنامهی Web API.
برای رفع این مشکل باید در حین تعریف ApiResource، لیست claim مورد نیاز را هم ذکر کرد:
namespace DNT.IDP { public static class Config { // api-related resources (scopes) public static IEnumerable<ApiResource> GetApiResources() { return new List<ApiResource> { new ApiResource( name: "imagegalleryapi", displayName: "Image Gallery API", claimTypes: new List<string> {"role" }) }; }
سپس کار با اکشن متد CreateImage در سمت API را به نقش PayingUser محدود میکنیم:
namespace ImageGallery.WebApi.WebApp.Controllers { [Route("api/images")] [Authorize] public class ImagesController : Controller { [HttpPost] [Authorize(Roles = "PayingUser")] public async Task<IActionResult> CreateImage([FromBody] ImageForCreationModel imageForCreation) {
var ownerId = User.Claims.FirstOrDefault(c => c.Type == "sub").Value; imageEntity.OwnerId = ownerId; // add and save. await _imagesService.AddImageAsync(imageEntity);
نکتهی مهم: در اینجا نباید این OwnerId را از سمت برنامهی کلاینت MVC به سمت برنامهی Web API ارسال کرد. برنامهی Web API باید این اطلاعات را از access token اعتبارسنجی شدهی رسیده استخراج و استفاده کند؛ از این جهت که دستکاری اطلاعات اعتبارسنجی نشدهی ارسالی به سمت Web API سادهاست؛ اما access tokenها دارای امضای دیجیتال هستند.
در سمت کلاینت نیز در فایل ImageGallery.MvcClient.WebApp\Views\Shared\_Layout.cshtml نمایش لینک افزودن تصویر را نیز محدود به PayingUser میکنیم:
@if(User.IsInRole("PayingUser")) { <li><a asp-area="" asp-controller="Gallery" asp-action="AddImage">Add an image</a></li> <li><a asp-area="" asp-controller="Gallery" asp-action="OrderFrame">Order a framed picture</a></li> }
namespace ImageGallery.MvcClient.WebApp.Controllers { [Authorize] public class GalleryController : Controller { [Authorize(Roles = "PayingUser")] public IActionResult AddImage() { return View(); }
[HttpPost] [Authorize(Roles = "PayingUser")] [ValidateAntiForgeryToken] public async Task<IActionResult> AddImage(AddImageViewModel addImageViewModel)
برای آزمایش این قسمت یکبار از برنامه خارج شده و سپس با اکانت User 1 که PayingUser است به سیستم وارد شوید. در ادامه از منوی بالای سایت، گزینهی Add an image را انتخاب کرده و تصویری را آپلود کنید. پس از آن، این تصویر آپلود شده را در لیست تصاویر صفحهی اول سایت، مشاهده خواهید کرد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشهی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آنرا اجرا کنید تا برنامهی IDP راه اندازی شود.
- در آخر به پوشهی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحهی login نام کاربری را User 1 و کلمهی عبور آنرا password وارد کنید.
Bad Code Smell ها
درخواست ایده برای برای پیاده سازی منوی چند سطحی
لینکی که شما معرفی کردید با خواسته من متفاوت است . به طور مثال گوشی LG V10 میتواند در گروههای زیر قرار گیرد :
1-بر اساس سیستم عامل ( اندروید)
2-شبکه ارتباطی : 4G
3- تعداد سیم کارت : یک
و الی آخر
ولی در لینکی که شما قرار داید "درب آهنی " نمیتواند در زیر شاخه " سقف" قرار گیرد .
در فایل ProductController.ts کدهای زیر را کپی نمایید:
module Product { export interface Scope { message: string; } export class Controller { constructor($scope: Scope) { $scope.message = "Hello from Masoud"; } } }
ابتدا یک ماژول به نام Product ایجاد میکنیم. سپس یک اینترفیس برای پیاده سازی آبجکت Scope که جهت مقید سازی عناصر DOM به آبجکتهای کنترلر مورد استفاده قرار میگیرد، ایجاد میکنیم. در داخل این اینترفیس متغیری به نام message از نوع string داریم. قصد داریم این متغیر را به یک عنصر مقید کنیم. حال یک کلاس به نام کنترلر ایجاد میکنیم که در تابع سازنده آن تزریق وابستگی برای scope$ از نوع اینترفیس Scope تعیین شده است. در نتیجه در بدنه سازنده میتوانیم به متغیر message مقدار مورد نظر را نسبت دهیم .
کلمه کلیدی
export برای تعریف عمومی کلاس استفاده شده است .
یک View ایجاد و کدهای
زیر را در آن کپی کنید :
<script type="text/javascript" src="~/scripts/app/ProductController.js"></script> <div ng-app> <div ng-controller="Product.Controller"> <p>{{message}}</p> </div> </div>
اولین نکته در تگ script است که فراخوانی فایل TypeScript باید با پسوند js. انجام گیرد. به دلیل اینکه فایلهای TypeScript بعد از کامپایل تبدیل به فایلهای JavaScript خواهند شد؛ در نتیجه پسوند آن نیز js. است. دومین نکته در فراخوانی کنترلر مورد نظر است که از ترکیب نام ماژول و نام کلاس است. بعد از اجرای پروژه خروجی به صورت زیر خواهد بود :
PM> Install-Package DNTFrameworkCore.Web
- Refactor کردن فرمهای ثبت و ویرایش مرتبط با یک Aggregate، به یک PartialView که با یک ViewModel کار میکند. برای موجودیتهای ساده و پایه، همان Model/DTO، به عنوان Model متناظر با یک ویو یا به اصطلاح ViewModel استفاده میشود؛ ولی برای سایر موارد، از مدلی که نام آن با نام موجودیت + کلمه ModalViewModel یا FormViewModel تشکیل میشود، استفاده خواهیم کرد.
- یک فرم، در قالب یک پارشالویو، به صورت Ajaxای با استفاده از افزونه jquery-unobtrusive-ajax بارگذاری شده و به سرور ارسال خواهد شد.
- یک فرم براساس طراحی خود میتواند در قالب یک مودال باز شود، یا به منظور inline-editing آن را بارگذاری و به قسمتی از صفحه که مدنظرتان میباشد اضافه شود.
- وجود ویو Index به همراه پارشالویو _List برای نمایش لیستی و یک پارشالویو برای عملیات ثبت و ویرایش الزامی میباشد. البته اگر از مکانیزمی که در مطلب « طراحی یک گرید با jQuery Ajax و ASP.NET MVC به همراه پیاده سازی عملیات CRUD» مطرح شد، استفاده نمیکنید و نیاز دارید تا اطلاعات صفحهبندی شده، مرتب شده و فیلتر شدهای را در قالب JSON دریافت کنید، از اکشنمتد ReadPagedList کنترلر پایه استفاده کنید.
public class BlogsController : CrudController<IBlogService, int, BlogModel> { public BlogsController(IBlogService service) : base(service) { } protected override string CreatePermissionName => PermissionNames.Blogs_Create; protected override string EditPermissionName => PermissionNames.Blogs_Edit; protected override string ViewPermissionName => PermissionNames.Blogs_View; protected override string DeletePermissionName => PermissionNames.Blogs_Delete; protected override string ViewName => "_BlogModal"; }
<form asp-action="@(Model.IsNew() ? "Create" : "Edit")" asp-controller="Blogs" asp-modal-form="BlogForm"> <div> <input type="hidden" name="save-continue" value="true"/> <input asp-for="RowVersion" type="hidden"/> <input asp-for="Id" type="hidden"/> <div> <div> <label asp-for="Title"></label> <input asp-for="Title" autocomplete="off"/> <span asp-validation-for="Title"></span> </div> </div> <div> <div> <label asp-for="Url"></label> <input asp-for="Url" type="url"/> <span asp-validation-for="Url"></span> </div> </div> </div> ... </form>
<div> <a asp-modal-delete-link asp-model-id="@Model.Id" asp-modal-toggle="false" asp-controller="Blogs" asp-action="Delete" asp-if="!Model.IsNew()" asp-permission="@PermissionNames.Blogs_Delete" title="Delete Blog"> <i></i> </a> <a title="Refresh Blog" asp-if="!Model.IsNew()" asp-modal-link asp-modal-toggle="false" asp-controller="Blogs" asp-action="Edit" asp-route-id="@Model.Id"> <i></i> </a> <a title="New Blog" asp-modal-link asp-modal-toggle="false" asp-controller="Blogs" asp-action="Create"> <i></i> </a> <button type="button" data-dismiss="modal"> <i></i> Cancel </button> <button type="submit"> <i></i> Save Changes </button> </div>
public class RoleModalViewModel : RoleModel { public IReadOnlyList<LookupItem> PermissionList { get; set; } }
protected override IActionResult RenderView(RoleModel role) { var model = _mapper.Map<RoleModalViewModel>(role); model.PermissionList = ReadPermissionList(); return PartialView(ViewName, model); }
برای مدیریت سناریوهای Master-Detail به مانند قسمت مدیریت دسترسیها در تب Permissions فرم بالا، امکاناتی در زیرساخت تعبیه شده است ولی پیادهسازی آن را به عنوان یک تمرین و با توجه به سری مطالب «Editing Variable Length Reorderable Collections in ASP.NET MVC» به شما واگذار میکنم.
نکته تکمیلی: برای ارسال اطلاعات اضافی به ویو Index متناظر با یک موجودیت میتوانید متد RenderIndex را به شکل زیر بازنویسی کنید:
protected override IActionResult RenderIndex(IPagedQueryResult<RoleReadModel> model) { var indexModel = new RoleIndexViewModel { Items = model.Items, TotalCount = model.TotalCount, Permissions = ReadPermissionList() }; return Request.IsAjaxRequest() ? (IActionResult) PartialView(indexModel) : View(indexModel); }
مدل RoleIndexViewModel استفاده شده در تکه کد بالا نیز به شکل زیر خواهد بود:
public class RoleIndexViewModel : PagedQueryResult<RoleReadModel> { public IReadOnlyList<LookupItem> Permissions { get; set; } }
فرآیند بارگذاری یک پارشالویو در مودال
به عنوان مثال برای استفاده از مودالهای بوت استرپ، ایده کار به این شکل است که یک مودال را به شکل زیر در فایل Layout قرار دهید:
<div class="modal fade" @*tabindex="-1"*@ id="main-modal" data-keyboard="true" data-backdrop="static" role="dialog" aria-hidden="true"> <div class="modal-dialog modal-dialog-centered" role="document"> <div class="modal-content"> <div class="modal-body"> Loading... </div> </div> </div> </div>
سپس در زمان کلیک بروی یک دکمه Ajaxای، ابتدا main-modal را نمایش داده و بعد از دریافت پارشالویو از سرور، آن را با محتوای modal-content جایگزین میکنیم. به همین دلیل Tag Halperهای مطرح شده در مطلب جاری، callbackهای failure/complete/success متناظر با unobtrusive-ajax را نیز مقداردهی میکنند. برای این منظور نیاز است تا متدهای جاوااسکریپتی زیر نیز در سطح شیء window تعریف شده باشند:
/*---------------------------------- asp-modal-link ---------------------------*/ window.handleModalLinkLoaded = function (data, status, xhr) { prepareForm('#main-modal.modal form'); }; window.handleModalLinkFailed = function (xhr, status, error) { //.... }; /*---------------------------------- asp-modal-form ---------------------------*/ window.handleModalFormBegin = function (xhr) { $('#main-modal a').addClass('disabled'); $('#main-modal button').attr('disabled', 'disabled'); }; window.handleModalFormComplete = function (xhr, status) { $('#main-modal a').removeClass('disabled'); $('#main-modal button').removeAttr('disabled'); }; window.handleModalFormSucceeded = function (data, status, xhr) { if (xhr.getResponseHeader('Content-Type') === 'text/html; charset=utf-8') { prepareForm('#main-modal.modal form'); } else { hideMainModal(); } }; window.handleModalFormFailed = function (xhr, status, error, formId) { if (xhr.status === 400) { handleBadRequest(xhr, formId); } };
برای بررسی بیشتر، پیشنهاد میکنم پروژه DNTFrameworkCore.TestWebApp موجود در مخزن این زیرساخت را بازبینی کنید.
AngularJS #1
پر نکردن فیلد های PDF با استفاده از iTextSharp
using System; using System.Diagnostics; using System.IO; using iTextSharp.text; using iTextSharp.text.pdf; namespace Delete { class Program { //روش صحیح ثبت و معرفی فونت در این کتابخانه public static iTextSharp.text.Font GetTahoma() { var fontName = "Tahoma"; if (!FontFactory.IsRegistered(fontName)) { var fontPath = Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf"; FontFactory.Register(fontPath); } return FontFactory.GetFont(fontName, BaseFont.IDENTITY_H, BaseFont.EMBEDDED); } static void Main(string[] args) { string fileNameExisting = @"name.pdf"; string fileNameNew = @"newform.pdf"; using (var existingFileStream = new FileStream(fileNameExisting, FileMode.Open)) using (var newFileStream = new FileStream(fileNameNew, FileMode.Create)) { var pdfReader = new PdfReader(existingFileStream); using (var stamper = new PdfStamper(pdfReader, newFileStream)) { //نکته مهم جهت کار با اطلاعات فارسی //در غیراینصورت شاهد ثبت اطلاعات نخواهید بود stamper.AcroFields.AddSubstitutionFont(GetTahoma().BaseFont); //form.Fields.Keys = تمام فیلدهای موجود در فرم var form = stamper.AcroFields; //مقدار دهی فیلدهای فرم form.SetField("name3", "مقدار1"); form.SetField("name2", "مقدار2"); // "Yes" and "Off" are valid values here //form.SetField("Check Box 1", "Yes"); // "" and "Off" are valid values here //form.SetField("Option Button 1", ""); // نحوه مقدار دهی لیست //form.SetListOption("ListBox1", new[] { "1مقدار یک", "مقدار دو1" }, null); //form.SetField("ListBox1", null); // به این ترتیب فرم دیگر توسط کاربر قابل ویرایش نخواهد بود //stamper.PartialFormFlattening --> جهت غیرقابل ویرایش نمودن فیلدی مشخص stamper.FormFlattening = true; stamper.Close(); pdfReader.Close(); } } //Process.Start("newform.pdf"); } } }
که اگر بخوام این مشکل را بر طرف بشه کافی:
به جای این کد
PdfStamper pdfStamper = new PdfStamper(pdfReader, stream);
PdfStamper stamper = new PdfStamper(pdfReader, stream, '\0', true);
pdfStamper.FormFlattening = false;
در مباحث اعتبارسنجی و احراز هویت Blazor در قسمتهای بعدی، به قطعه کد context@ داری در داخل یک RenderFragment خواهیم رسید:
<AuthorizeView> <Authorized> Hello, @context.User.Identity.Name! </Authorized> </AuthorizeView>
برای پاسخ به این سؤال نیاز است با مفهوم «Templated components» در برنامههای Blazor آشنا شد. تا اینجا از RenderFragmentها صرفا جهت فراهم آوردن قسمت ثابتی از قالب کامپوننت جاری، توسط استفاده کنندهی از آن، کمک گرفتیم. اما در همان سمت استفاده کننده، امکان دسترسی به اطلاعات مهیای داخل آن فرگمنت نیز وجود دارد. برای نمونه به کدهای کامپوننت TableTemplate.razor دقت کنید:
@typeparam Titem <table class="table"> <thead> <tr>@TableHeader</tr> </thead> <tbody> @foreach (var item in Items) { <tr>@RowTemplate(item)</tr> } </tbody> </table> @code { [Parameter] public RenderFragment TableHeader { get; set; } [Parameter] public RenderFragment<TItem> RowTemplate { get; set; } [Parameter] public IReadOnlyList<TItem> Items { get; set; } }
- همچنین یک RenderFragment جنریک را نیز مشاهده میکنید که قالب ردیفهای جدول را تامین میکند. نوع جنریک قابل دسترسی در این کامپوننت، توسط دایرکتیو typeparam Titem@ تعریف شدهی در ابتدای آن، مشخص شدهاست.
- بنابراین هربار که ردیفی از بدنهی جدول در حال رندر است، یک شیء item از نوع TItem را به قالب سفارشی تامین شدهی توسط بکارگیرندهی خود ارسال میکند.
اکنون این سؤال مطرح میشود که چگونه میتوان به شیء item، در سمت والد یا بکارگیرندهی کامپوننت TableTemplate فوق دسترسی یافت؟
برای اینکار میتوان از پارامتر ویژهای به نام context که یک implicit parameter است و به صورت پیشفرض تعریف شده و مهیا است، در سمت کدهای بکارگیرندهی کامپوننت، استفاده کرد:
@page "/pets" <h1>Pets</h1> <TableTemplate Items="pets"> <TableHeader> <th>ID</th> <th>Name</th> </TableHeader> <RowTemplate> <td>@context.PetId</td> <td>@context.Name</td> </RowTemplate> </TableTemplate> @code { private List<Pet> pets = new() { new Pet { PetId = 2, Name = "Mr. Bigglesworth" }, new Pet { PetId = 4, Name = "Salem Saberhagen" }, new Pet { PetId = 7, Name = "K-9" } }; private class Pet { public int PetId { get; set; } public string Name { get; set; } } }
- همچنین در اینجا پارامتر ویژهای به نام context@ را نیز مشاهده میکنید. این پارامتر همان شیء item ای است که در حین رندر هر ردیف جدول، به فرگمنت RowTemplate ارسال میشود. به این ترتیب کامپوننت والد میتواند از اطلاعات در حال رندر توسط کامپوننت فرگمنت دار، مطلع شود و از آن استفاده کند. در این مثال، نوع context@، از نوع کلاس Pet است که سعی شده بر اساس نوع پارامتر Items ارسالی به آن، توسط کامپایلر تشخیص داده شود. حتی میتوان این نوع را به صورت صریحی نیز مشخص کرد:
<TableTemplate Items="pets" TItem="Pet">
<SomeGenericComponent TParam1="Person" TParam2="Supplier" TItem=etc/>
<TableTemplate Items="pets" Context="pet"> <TableHeader> <th>ID</th> <th>Name</th> </TableHeader> <RowTemplate> <td>@pet.PetId</td> <td>@pet.Name</td> </RowTemplate> </TableTemplate>
<TableTemplate Items="pets"> <TableHeader> <th>ID</th> <th>Name</th> </TableHeader> <RowTemplate Context="pet"> <td>@pet.PetId</td> <td>@pet.Name</td> </RowTemplate> </TableTemplate>
در نهایت میخواهیم نگاشتها را اینچنین تنظیم کنیم:
[MapFrom(typeof (Student), ignoreAllNonExistingProperty: true, alsoCopyMetadata: true)] public class AdminStudentViewModel { // [IgnoreMap] public int Id { set; get; } [MapForMember("Name")] public string FirstName { set; get; } [MapForMember("Family")] public string LastName { set; get; } public string Email { set; get; } [MapForMember("RegisterDateTime")] public string RegisterDateTimePersian { set; get; } [UseValueResolver(typeof (BookCountValueResolver))] public int BookCounts { set; get; } [UseValueResolver(typeof (BookPriceValueResolver))] public decimal BookPrice { set; get; } };
به تعریف و توضیح صفتهای (ویژگیها یا Attributes) مورد نیاز میپردازم:
صفت MapFromAttribute
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] public class MapFromAttribute : Attribute { public Type SourceType { get; private set; } public bool IgnoreAllNonExistingProperty { get; private set; } public bool AlsoCopyMetadata { get; private set; } //Go to: https://www.dntips.ir/courses/topic/16/cb36bc2e-4263-431e-86a5-236322cb5576 public MapFromAttribute(Type sourceType, bool ignoreAllNonExistingProperty = false, bool alsoCopyMetadata = false) { SourceType = sourceType; IgnoreAllNonExistingProperty = ignoreAllNonExistingProperty; AlsoCopyMetadata = alsoCopyMetadata; } };
صفت IgnoreMapAttribute
[AttributeUsage(AttributeTargets.Property)] public class IgnoreMapAttribute : Attribute {};
صفت MapForMemberAttribute
[AttributeUsage(AttributeTargets.Property)] public class MapForMemberAttribute : Attribute { public string MemberToMap { get; private set; } public MapForMemberAttribute(string memberToMap) { MemberToMap = memberToMap; } };
صفت UseValueResolverAttribute
[AttributeUsage(AttributeTargets.Property)] public class UseValueResolverAttribute : Attribute { public IValueResolver ValueResolver { get; private set; } public UseValueResolverAttribute(Type valueResolver) { ValueResolver = valueResolver.GetConstructors()[0].Invoke(new object[] {}) as IValueResolver; } };
تا اینجا صفات پیش نیاز کار فراهم شدند. حال باید این صفتها را به نگاشت متناسبی در automapper تبدیل کنیم.
دریافت کدها
ادامه دارد...