مطالب
شروع به کار با DNTFrameworkCore - قسمت 4 - پیاده‌سازی CRUD API موجودیت‌ها
پس از معرفی DNTFrameworkCore، طراحی موجودیت‌های سیستم و پیاده‌سازی DTOها، اعتبارسنج‌ها و سرویس‌های متناظر آنها، در این مطلب روش پیاده سازی CRUD API یکسری موجودیت فرضی را با استفاده از امکانات این زیرساخت بررسی خواهیم کرد.

برای شروع لازم است بسته نیوگت زیر را نصب کنید:
PM> Install-Package DNTFrameworkCore.Web

همچنین برای اعمال خودکار مهاجرت‌های بانک اطلاعاتی، بسته نیوگت زیر را نصب کنید:
PM> Install-Package DNTFrameworkCore.Web.EntityFramework

سپس با استفاده از متد الحاقی MigrateDbContext به شکل زیر می‌توان فرآیند مذکور را خودکار کرد:
public class Program
{
    public static void Main(string[] args)
    {
        CreateWebHostBuilder(args).Build()
            .MigrateDbContext<ProjectDbContext>()
            .Run();
    }

//...
}

مثال اول: پیاده سازی CRUD API یک موجودیت ساده
[Route("api/[controller]")]
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;
}
کار با ارث‌بری از CrudController جنریک شروع می‌شود؛ سپس نیاز است نوع سرویس، نوع مدل و همچنین نوع شناسه مرتبط با موجودیت مورد‎‌نظر را از طریق Type Parameter مشخص کنید. این کنترلر پایه تعاریف مختلفی دارد که برحسب نیاز خود می‌توانید از آنها استفاده کنید. در ادامه نیز برای اعمال دسترسی خاصی برای عملیات CRUD، نیاز است نام دسترسی‌ها را مشخص کنید. کار تمام است؛ برای استفاده از آن می‌توانید با اجرای پروژه DNTFrameworkCore.TestAPI به شکل زیر عمل کنید:
ابتدا نیاز است با استفاده از افزونه‌ی Postman یک Environment جدید ایجاد کنید.

در اینجا دو متغیر endpoint و token ‌برای سرعت بخشیدن به فرآیند تست API تولیدی ایجاد شده‌اند. مقدار endpoint آن از ابتدا مشخص می‌باشد؛ ولی برای مقداردهی token، از ترفند زیر می‌توان استفاده کرد:

در برگه Tests آن می‌توان متغیر تعریف شده در Environment ایجاد شده را با قطعه کد زیر مقداردهی کرد:
 var data = JSON.parse(responseBody)
pm.environment.set("token", data.token);
و برای استفاده از این متغیر به شکل زیر عمل کنید:

حال برای ارسال درخواست‌های HTTP به BlogsController به شکل زیر عمل کنید:

پشت صحنه اکشن GET مربوط به BlogsController از متد سرویس ReadPagedListAsync استفاده می‌شود که خروجی آن در صورت مشخص نکردن TReadModel، برای یک موجودیت ساده مانند واحد سنجش، از همان TModel استفاده خواهد شد. در تصویر بالا لیست صفحه بندی شده موجودیت Blog را مشاهده می‌کنید. برای درخواست صفحه دیگر و جستجوی پویا می‌توان به شکل زیر عمل کرد:
query={"page":1,"pageSize":100,"filter":{"logic":"and","filters":[{"field":"title","value":"Blog1","operator":"startswith"}]}}
همانطور که در مطالب گذشته اشاره شد، ورودی متدهای Read موجود در سرویس‌ها از نوع IFilteredPagedQueryModel می‌باشد. یک ModelBinder سفارشی هم برای بایند خودکار این کوئری استرینک با محتوای یک شیء JSON، در زیرساخت طراحی شده است.

در پشت صحنه اکشن POST از متد CreateAsync سرویس مرتبط استفاده می‌شود و همانطور که در قسمت قبلی عنوان شد، Id و RowVersion مدل ارسالی، مقداردهی خواهد شد. 

در پشت صحنه اکشن ‎‎‎‎‎‎GET/{id}‎ از متد FindAsync سرویس مرتبط استفاده می‌شود و خروجی آن از نوع TModel می‌باشد. 

در پشت صحنه اکشن PUT از متد EditAsync سرویس مرتبط استفاده می‌شود که ورودی آن نوع TModel می‌باشد. همانطور که قبلا اشاره شده بود و در خروجی حاصل از درخواست ویرایش بالا مشخص می‎‌باشد، مکانیزم مدیریت استثناهای حاصل از مباحث همزمانی نیز به درستی انجام شده است.
برای حذف یک Blog می‌توان با ارسال درخواست DELETE به آدرس زیر به این هدف رسید:
{{endpoint}}/blogs/10
در پشت صحنه این اکشن نیز از متد DeleteAsync سرویس مرتبط استفاده می‌شود.
‌‌‌
مثال دوم: پیاده سازی و استفاده از CRUD API در سناریوهای Master-Detail
[Route("api/[controller]")]
public class UsersController : CrudController<IUserService, long, UserReadModel, UserModel>
{
    private readonly ILookupService _lookupService;

    public UsersController(IUserService service, ILookupService lookupService) : base(service)
    {
        _lookupService = lookupService ?? throw new ArgumentNullException(nameof(lookupService));
    }

    protected override string CreatePermissionName => PermissionNames.Users_Create;
    protected override string EditPermissionName => PermissionNames.Users_Edit;
    protected override string ViewPermissionName => PermissionNames.Users_View;
    protected override string DeletePermissionName => PermissionNames.Users_Delete;

    [HttpGet("[action]")]
    [PermissionAuthorize(PermissionNames.Users_Create, PermissionNames.Users_Edit)]
    public async Task<IActionResult> RoleList()
    {
        var result = await _lookupService.ReadRolesAsync();
        return Ok(result);
    }
}
‌‌
موجودیت User از جمله موجودیت‌هایی می‌باشد که نیاز است ReadModel مجزایی برای آن درنظر گرفت؛ چرا که در زمان نمایش لیستی کاربران، نیاز به واکشی گروه‌های کاربری متصل و دسترسی‌های خاص آن، نمی‌باشد. همچنین اکشن متد RoleList برای دریافت لیست گروه‌های کاربری موجود در سیستم نیز پیاده‌سازی شده است. باتوجه به اینکه نیاز است از این اکشن متد در عملیات ثبت و ویرایش استفاده کرد، بر روی آن با استفاده از فیلتر سفارشی PermissionAuthorize، بررسی شده است که کاربر جاری یکی از دسترسی‌های Users_Create یا Users_Edit را داشته باشد.
‎‎‎‎‎
درخواست‌های GET و DELETE مشابه مثال اول می‌باشد؛ برای درخواست‌های POST و PUT آن می‌توان به شکل زیر عمل کرد:

باتوجه به اینکه UserRole به عنوان یکی از وابستگی‌های موجودیت User محسوب می‌شود، در پاسخ درخواست GET مرتبط با کاربری با شناسه 2، roles آن، لیستی از UserRoleModel هستند که به عنوان یک DetailModel طراحی شده است. به عنوان مثال برای حذف اتصال یک گروه کاربری باید درخواست PUT را به شکل زیر ارسال کنید:

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

{
    "userName": "rabbal",
    "displayName": "غلامرضا ربال",
    "password": null,
    "isActive": false,
    "roles": [],
    "permissions": [],
    "ignoredPermissions": [],
    "rowVersion": "AAAAAAACGxI=",
    "id": 2
}


برای بررسی بیشتر، پیشنهاد می‌کنم پروژه  DNTFrameworkCore.TestAPI  موجود در مخزن این زیرساخت را بازبینی کنید.  
مطالب
ASP.NET MVC #18

اعتبار سنجی کاربران در ASP.NET MVC

دو مکانیزم اعتبارسنجی کاربران به صورت توکار در ASP.NET MVC در دسترس هستند: Forms authentication و Windows authentication.
در حالت Forms authentication، برنامه موظف به نمایش فرم لاگین به کاربر‌ها و سپس بررسی اطلاعات وارده توسط آن‌ها است. برخلاف آن، Windows authentication حالت یکپارچه با اعتبار سنجی ویندوز است. برای مثال زمانیکه کاربری به یک دومین ویندوزی وارد می‌شود، از همان اطلاعات ورود او به شبکه داخلی، به صورت خودکار و یکپارچه جهت استفاده از برنامه کمک گرفته خواهد شد و بیشترین کاربرد آن در برنامه‌های نوشته شده برای اینترانت‌های داخلی شرکت‌ها است. به این ترتیب کاربران یک بار به دومین وارد شده و سپس برای استفاده از برنامه‌های مختلف ASP.NET، نیازی به ارائه نام کاربری و کلمه عبور نخواهند داشت. Forms authentication بیشتر برای برنامه‌هایی که از طریق اینترنت به صورت عمومی و از طریق انواع و اقسام سیستم عامل‌ها قابل دسترسی هستند، توصیه می‌شود (و البته منعی هم برای استفاده در حالت اینترانت ندارد).
ضمنا باید به معنای این دو کلمه هم دقت داشت: هدف از Authentication این است که مشخص گردد هم اکنون چه کاربری به سایت وارد شده است. Authorization، سطح دسترسی کاربر وارد شده به سیستم و اعمالی را که مجاز است انجام دهد، مشخص می‌کند.


فیلتر Authorize در ASP.NET MVC

یکی دیگر از فیلترهای امنیتی ASP.NET MVC به نام Authorize، کار محدود ساختن دسترسی به متدهای کنترلرها را انجام می‌دهد. زمانیکه اکشن متدی به این فیلتر یا ویژگی مزین می‌شود، به این معنا است که کاربران اعتبارسنجی نشده، امکان دسترسی به آن‌را نخواهند داشت. فیلتر Authorize همواره قبل از تمامی فیلترهای تعریف شده دیگر اجرا می‌شود.
فیلتر Authorize با پیاده سازی اینترفیس System.Web.Mvc.IAuthorizationFilter توسط کلاس System.Web.Mvc.AuthorizeAttribute در دسترس می‌باشد. این کلاس علاوه بر پیاده سازی اینترفیس یاد شده، دارای دو خاصیت مهم زیر نیز می‌باشد:

public string Roles { get; set; } // comma-separated list of role names
public string Users { get; set; } // comma-separated list of usernames

زمانیکه فیلتر Authorize به تنهایی بکارگرفته می‌شود، هر کاربر اعتبار سنجی شده‌ای در سیستم قادر خواهد بود به اکشن متد مورد نظر دسترسی پیدا کند. اما اگر همانند مثال زیر، از خواص Roles و یا Users نیز استفاده گردد، تنها کاربران اعتبار سنجی شده مشخصی قادر به دسترسی به یک کنترلر یا متدی در آن خواهند شد:

[Authorize(Roles="Admins")]
public class AdminController : Controller
{
  [Authorize(Users="Vahid")]
  public ActionResult DoSomethingSecure()
   {
  }
}

در این مثال، تنها کاربرانی با نقش Admins قادر به دسترسی به کنترلر جاری Admin خواهند بود. همچنین در بین این کاربران ویژه، تنها کاربری به نام Vahid قادر است متد DoSomethingSecure را فراخوانی و اجرا کند.

اکنون سؤال اینجا است که فیلتر Authorize چگونه از دو مکانیزم اعتبار سنجی یاد شده استفاده می‌کند؟ برای پاسخ به این سؤال، فایل web.config برنامه را باز نموده و به قسمت authentication آن دقت کنید:

<authentication mode="Forms">
<forms loginUrl="~/Account/LogOn" timeout="2880" />
</authentication>

به صورت پیش فرض، برنامه‌های ایجاد شده توسط VS.NET جهت استفاده از حالت Forms یا همان Forms authentication تنظیم شده‌اند. در اینجا کلیه کاربران اعتبار سنجی نشده، به کنترلری به نام Account و متد LogOn در آن هدایت می‌شوند.
برای تغییر آن به حالت اعتبار سنجی یکپارچه با ویندوز، فقط کافی است مقدار mode را به Windows تغییر داد و تنظیمات forms آن‌را نیز حذف کرد.


یک نکته: اعمال تنظیمات اعتبار سنجی اجباری به تمام صفحات سایت
تنظیم زیر نیز در فایل وب کانفیگ برنامه، همان کار افزودن ویژگی Authorize را انجام می‌دهد با این تفاوت که تمام صفحات سایت را به صورت خودکار تحت پوشش قرار خواهد داد (البته منهای loginUrl ایی که در تنظیمات فوق مشاهده نمودید):

<authorization>
<deny users="?" />
</authorization>

در این حالت دسترسی به تمام آدرس‌های سایت تحت تاثیر قرار می‌گیرند، منجمله دسترسی به تصاویر و فایل‌های CSS و غیره. برای اینکه این موارد را برای مثال در حین نمایش صفحه لاگین نیز نمایش دهیم، باید تنظیم زیر را پیش از تگ system.web به فایل وب کانفیگ برنامه اضافه کرد:

<!-- we don't want to stop anyone seeing the css and images -->
<location path="Content">
<system.web>
<authorization>
<allow users="*" />
</authorization>
</system.web>
</location>

در اینجا پوشه Content از سیستم اعتبارسنجی اجباری خارج می‌شود و تمام کاربران به آن دسترسی خواهند داشت.
به علاوه امکان امن ساختن تنها قسمتی از سایت نیز میسر است؛ برای مثال:

<location path="secure">
  <system.web>
    <authorization>
      <allow roles="Administrators" />
      <deny users="*" />
    </authorization>
  </system.web>
</location>

در اینجا مسیری به نام secure، نیاز به اعتبارسنجی اجباری دارد. به علاوه تنها کاربرانی در نقش Administrators به آن دسترسی خواهند داشت.


نکته: به تنظیمات انجام شده در فایل Web.Config دقت داشته باشید
همانطور که می‌شود دسترسی به یک مسیر را توسط تگ location بازگذاشت، امکان بستن آن هم فراهم است (بجای allow از deny استفاده شود). همچنین در ASP.NET MVC به سادگی می‌توان تنظیمات مسیریابی را در فایل global.asax.cs تغییر داد. برای مثال اینبار مسیر دسترسی به صفحات امن سایت، Admin خواهد بود نه Secure. در این حالت چون از فیلتر Authorize استفاده نشده و همچنین فایل web.config نیز تغییر نکرده، این صفحات بدون محافظت رها خواهند شد.
بنابراین اگر از تگ location برای امن سازی قسمتی از سایت استفاده می‌کنید، حتما باید پس از تغییرات مسیریابی، فایل web.config را هم به روز کرد تا به مسیر جدید اشاره کند.
به همین جهت در ASP.NET MVC بهتر است که صریحا از فیلتر Authorize بر روی کنترلرها (جهت اعمال به تمام متدهای آن) یا بر روی متدهای خاصی از کنترلرها استفاده کرد.
امکان تعریف AuthorizeAttribute در فایل global.asax.cs و متد RegisterGlobalFilters آن به صورت سراسری نیز وجود دارد. اما در این حالت حتی صفحه لاگین سایت هم دیگر در دسترس نخواهد بود. برای رفع این مشکل در ASP.NET MVC 4 فیلتر دیگری به نام AllowAnonymousAttribute معرفی شده است تا بتوان قسمت‌هایی از سایت را مانند صفحه لاگین، از سیستم اعتبارسنجی اجباری خارج کرد تا حداقل کاربر بتواند نام کاربری و کلمه عبور خودش را وارد نماید:

[System.Web.Mvc.AllowAnonymous]
public ActionResult Login()
{
return View();
}

بنابراین در ASP.NET MVC 4.0، فیلتر AuthorizeAttribute را سراسری تعریف کنید. سپس در کنترلر لاگین برنامه از فیلتر AllowAnonymous استفاده نمائید.
البته نوشتن فیلتر سفارشی AllowAnonymousAttribute در ASP.NET MVC 3.0 نیز میسر است. برای مثال:

public class LogonAuthorize : AuthorizeAttribute {
public override void OnAuthorization(AuthorizationContext filterContext) {
if (!(filterContext.Controller is AccountController))
base.OnAuthorization(filterContext);
}
}

در این فیلتر سفارشی، اگر کنترلر جاری از نوع AccountController باشد، از سیستم اعتبار سنجی اجباری خارج خواهد شد. مابقی کنترلرها همانند سابق پردازش می‌شوند. به این معنا که اکنون می‌توان LogonAuthorize را به صورت یک فیلتر سراسری در فایل global.asax.cs معرفی کرد تا به تمام کنترلرها، منهای کنترلر Account اعمال شود.



مثالی جهت بررسی حالت Windows Authentication

یک پروژه جدید خالی ASP.NET MVC را آغاز کنید. سپس یک کنترلر جدید را به نام Home نیز به آن اضافه کنید. در ادامه متد Index آن‌را با ویژگی Authorize، مزین نمائید. همچنین بر روی نام این متد کلیک راست کرده و یک View خالی را برای آن ایجاد کنید:

using System.Web.Mvc;

namespace MvcApplication15.Controllers
{
public class HomeController : Controller
{
[Authorize]
public ActionResult Index()
{
return View();
}
}
}

محتوای View متناظر با متد Index را هم به شکل زیر تغییر دهید تا نام کاربر وارد شده به سیستم را نمایش دهد:

@{
ViewBag.Title = "Index";
}

<h2>Index</h2>
Current user: @User.Identity.Name

به علاوه در فایل Web.config برنامه، حالت اعتبار سنجی را به ویندوز تغییر دهید:

<authentication mode="Windows" />

اکنون اگر برنامه را اجرا کنید و وب سرور آزمایشی انتخابی هم IIS Express باشد، پیغام HTTP Error 401.0 - Unauthorized نمایش داده می‌شود. علت هم اینجا است که Windows Authentication به صورت پیش فرض در این وب سرور غیرفعال است. برای فعال سازی آن به مسیر My Documents\IISExpress\config مراجعه کرده و فایل applicationhost.config را باز نمائید. تگ windowsAuthentication را یافته و ویژگی enabled آن‌را که false است به true تنظیم نمائید. اکنون اگر برنامه را مجددا اجرا کنیم، در محل نمایش User.Identity.Name، نام کاربر وارد شده به سیستم نمایش داده خواهد شد.
همانطور که مشاهده می‌کنید در اینجا همه چیز یکپارچه است و حتی نیازی نیست صفحه لاگین خاصی را به کاربر نمایش داد. همینقدر که کاربر توانسته به سیستم ویندوزی وارد شود، بر این اساس هم می‌تواند از برنامه‌های وب موجود در شبکه استفاده کند.



بررسی حالت Forms Authentication

برای کار با Forms Authentication نیاز به محلی برای ذخیره سازی اطلاعات کاربران است. اکثر مقالات را که مطالعه کنید شما را به مباحث membership مطرح شده در زمان ASP.NET 2.0 ارجاع می‌دهند. این روش در ASP.NET MVC هم کار می‌کند؛ اما الزامی به استفاده از آن نیست.

برای بررسی حالت اعتبار سنجی مبتنی بر فرم‌ها، یک برنامه خالی ASP.NET MVC جدید را آغاز کنید. یک کنترلر Home ساده را نیز به آن اضافه نمائید.
سپس نیاز است نکته «تنظیمات اعتبار سنجی اجباری تمام صفحات سایت» را به فایل وب کانفیگ برنامه اعمال نمائید تا نیازی نباشد فیلتر Authorize را در همه جا معرفی کرد. سپس نحوه معرفی پیش فرض Forms authentication تعریف شده در فایل web.config نیز نیاز به اندکی اصلاح دارد:

<authentication mode="Forms">
<!--one month ticket-->
<forms name=".403MyApp"
cookieless="UseCookies"
loginUrl="~/Account/LogOn"
defaultUrl="~/Home"
slidingExpiration="true"
protection="All"
path="/"
timeout="43200"/>
</authentication>

در اینجا استفاده از کوکی‌ها اجباری شده است. loginUrl به کنترلر و متد لاگین برنامه اشاره می‌کند. defaultUrl مسیری است که کاربر پس از لاگین به صورت خودکار به آن هدایت خواهد شد. همچنین نکته‌ی مهم دیگری را که باید رعایت کرد، name ایی است که در این فایل config عنوان می‌‌کنید. اگر بر روی یک وب سرور، چندین برنامه وب ASP.Net را در حال اجرا دارید، باید برای هر کدام از این‌ها نامی جداگانه و منحصربفرد انتخاب کنید، در غیراینصورت تداخل رخ داده و گزینه مرا به خاطر بسپار شما کار نخواهد کرد.
کار slidingExpiration که در اینجا تنظیم شده است نیز به صورت زیر می‌باشد:
اگر لاگین موفقیت آمیزی ساعت 5 عصر صورت گیرد و timeout شما به عدد 10 تنظیم شده باشد، این لاگین به صورت خودکار در 5:10‌ منقضی خواهد شد. اما اگر در این حین در ساعت 5:05 ، کاربر، یکی از صفحات سایت شما را مرور کند، زمان منقضی شدن کوکی ذکر شده به 5:15 تنظیم خواهد شد(مفهوم تنظیم slidingExpiration). لازم به ذکر است که اگر کاربر پیش از نصف زمان منقضی شدن کوکی (مثلا در 5:04)، یکی از صفحات را مرور کند، تغییری در این زمان نهایی منقضی شدن رخ نخواهد داد.
اگر timeout ذکر نشود، زمان منقضی شدن کوکی ماندگار (persistent) مساوی زمان جاری + زمان منقضی شدن سشن کاربر که پیش فرض آن 30 دقیقه است، خواهد بود.

سپس یک مدل را به نام Account به پوشه مدل‌های برنامه با محتوای زیر اضافه نمائید:

using System.ComponentModel.DataAnnotations;

namespace MvcApplication15.Models
{
public class Account
{
[Required(ErrorMessage = "Username is required to login.")]
[StringLength(20)]
public string Username { get; set; }

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

public bool RememberMe { get; set; }
}
}

همچنین مطابق تنظیمات اعتبار سنجی مبتنی بر فرم‌های فایل وب کانفیگ، نیاز به یک AccountController نیز هست:

using System.Web.Mvc;
using MvcApplication15.Models;

namespace MvcApplication15.Controllers
{
public class AccountController : Controller
{
[HttpGet]
public ActionResult LogOn()
{
return View();
}

[HttpPost]
public ActionResult LogOn(Account loginInfo, string returnUrl)
{
return View();
}
}
}

در اینجا در حالت HttpGet فرم لاگین نمایش داده خواهد شد. بنابراین بر روی این متد کلیک راست کرده و گزینه Add view را انتخاب کنید. سپس در صفحه باز شده گزینه Create a strongly typed view را انتخاب کرده و مدل را هم بر روی کلاس Account قرار دهید. قالب scaffolding را هم Create انتخاب کنید. به این ترتیب فرم لاگین برنامه ساخته خواهد شد.
اگر به متد HttpPost فوق دقت کرده باشید، علاوه بر دریافت وهله‌ای از شیء Account، یک رشته را به نام returnUrl نیز تعریف کرده است. علت هم اینجا است که سیستم Forms authentication، صفحه بازگشت را به صورت خودکار به شکل یک کوئری استرینگ به انتهای Url جاری اضافه می‌کند. مثلا:

http://localhost/Account/LogOn?ReturnUrl=something

بنابراین اگر یکی از پارامترهای متد تعریف شده به نام returnUrl باشد، به صورت خودکار مقدار دهی خواهد شد.

تا اینجا زمانیکه برنامه را اجرا کنیم، ابتدا بر اساس تعاریف مسیریابی پیش فرض برنامه، آدرس کنترلر Home و متد Index آن فراخوانی می‌گردد. اما چون در وب کانفیگ برنامه authorization را فعال کرده‌ایم، برنامه به صورت خودکار به آدرس مشخص شده در loginUrl قسمت تعاریف اعتبارسنجی مبتنی بر فرم‌ها هدایت خواهد شد. یعنی آدرس کنترلر Account و متد LogOn آن درخواست می‌گردد. در این حالت صفحه لاگین نمایان خواهد شد.

مرحله بعد، اعتبار سنجی اطلاعات وارد شده کاربر است. بنابراین نیاز است کنترلر Account را به نحو زیر بازنویسی کرد:

using System.Web.Mvc;
using System.Web.Security;
using MvcApplication15.Models;

namespace MvcApplication15.Controllers
{
public class AccountController : Controller
{
[HttpGet]
public ActionResult LogOn(string returnUrl)
{
if (User.Identity.IsAuthenticated) //remember me
{
if (shouldRedirect(returnUrl))
{
return Redirect(returnUrl);
}
return Redirect(FormsAuthentication.DefaultUrl);
}

return View(); // show the login page
}

[HttpGet]
public void LogOut()
{
FormsAuthentication.SignOut();
}

private bool shouldRedirect(string returnUrl)
{
// it's a security check
return !string.IsNullOrWhiteSpace(returnUrl) &&
Url.IsLocalUrl(returnUrl) &&
returnUrl.Length > 1 &&
returnUrl.StartsWith("/") &&
!returnUrl.StartsWith("//") &&
!returnUrl.StartsWith("/\\");
}

[HttpPost]
public ActionResult LogOn(Account loginInfo, string returnUrl)
{
if (this.ModelState.IsValid)
{
if (loginInfo.Username == "Vahid" && loginInfo.Password == "123")
{
FormsAuthentication.SetAuthCookie(loginInfo.Username, loginInfo.RememberMe);
if (shouldRedirect(returnUrl))
{
return Redirect(returnUrl);
}
FormsAuthentication.RedirectFromLoginPage(loginInfo.Username, loginInfo.RememberMe);
}
}
this.ModelState.AddModelError("", "The user name or password provided is incorrect.");
ViewBag.Error = "Login faild! Make sure you have entered the right user name and password!";
return View(loginInfo);
}
}
}

در اینجا با توجه به گزینه «مرا به خاطر بسپار»، اگر کاربری پیشتر لاگین کرده و کوکی خودکار حاصل از اعتبار سنجی مبتنی بر فرم‌های او نیز معتبر باشد، مقدار User.Identity.IsAuthenticated مساوی true خواهد بود. بنابراین نیاز است در متد LogOn از نوع HttpGet به این مساله دقت داشت و کاربر اعتبار سنجی شده را به صفحه پیش‌فرض تعیین شده در فایل web.config برنامه یا returnUrl هدایت کرد.
در متد LogOn از نوع HttpPost، کار اعتبارسنجی اطلاعات ارسالی به سرور انجام می‌شود. در اینجا فرصت خواهد بود تا اطلاعات دریافتی، با بانک اطلاعاتی مقایسه شوند. اگر اطلاعات مطابقت داشتند، ابتدا کوکی خودکار FormsAuthentication تنظیم شده و سپس به کمک متد RedirectFromLoginPage کاربر را به صفحه پیش فرض سیستم هدایت می‌کنیم. یا اگر returnUrl ایی وجود داشت، آن‌را پردازش خواهیم کرد.
برای پیاده سازی خروج از سیستم هم تنها کافی است متد FormsAuthentication.SignOut فراخوانی شود تا تمام اطلاعات سشن و کوکی‌های مرتبط، به صورت خودکار حذف گردند.

تا اینجا فیلتر Authorize بدون پارامتر و همچنین در حالت مشخص سازی صریح کاربران به نحو زیر را پوشش دادیم:

[Authorize(Users="Vahid")]

اما هنوز حالت استفاده از Roles در فیلتر Authorize باقی مانده است. برای فعال سازی خودکار بررسی نقش‌های کاربران نیاز است یک Role provider سفارشی را با پیاده سازی کلاس RoleProvider، طراحی کنیم. برای مثال:

using System;
using System.Web.Security;

namespace MvcApplication15.Helper
{
public class CustomRoleProvider : RoleProvider
{
public override bool IsUserInRole(string username, string roleName)
{
if (username.ToLowerInvariant() == "ali" && roleName.ToLowerInvariant() == "User")
return true;
// blabla ...
return false;
}

public override string[] GetRolesForUser(string username)
{
if (username.ToLowerInvariant() == "ali")
{
return new[] { "User", "Helpdesk" };
}

if(username.ToLowerInvariant()=="vahid")
{
return new [] { "Admin" };
}

return new string[] { };
}

public override void AddUsersToRoles(string[] usernames, string[] roleNames)
{
throw new NotImplementedException();
}

public override string ApplicationName
{
get
{
throw new NotImplementedException();
}
set
{
throw new NotImplementedException();
}
}

public override void CreateRole(string roleName)
{
throw new NotImplementedException();
}

public override bool DeleteRole(string roleName, bool throwOnPopulatedRole)
{
throw new NotImplementedException();
}

public override string[] FindUsersInRole(string roleName, string usernameToMatch)
{
throw new NotImplementedException();
}

public override string[] GetAllRoles()
{
throw new NotImplementedException();
}

public override string[] GetUsersInRole(string roleName)
{
throw new NotImplementedException();
}

public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames)
{
throw new NotImplementedException();
}

public override bool RoleExists(string roleName)
{
throw new NotImplementedException();
}
}
}

در اینجا حداقل دو متد IsUserInRole و GetRolesForUser باید پیاده سازی شوند و مابقی اختیاری هستند.
بدیهی است در یک برنامه واقعی این اطلاعات باید از یک بانک اطلاعاتی خوانده شوند؛ برای نمونه به ازای هر کاربر تعدادی نقش وجود دارد. به ازای هر نقش نیز تعدادی کاربر تعریف شده است (یک رابطه many-to-many باید تعریف شود).
در مرحله بعد باید این Role provider سفارشی را در فایل وب کانفیگ برنامه در قسمت system.web آن تعریف و ثبت کنیم:

<roleManager>
<providers>
<clear />
<add name="CustomRoleProvider" type="MvcApplication15.Helper.CustomRoleProvider"/>
</providers>
</roleManager>


همین مقدار برای راه اندازی بررسی نقش‌ها در ASP.NET MVC کفایت می‌کند. اکنون امکان تعریف نقش‌ها، حین بکارگیری فیلتر Authorize میسر است:

[Authorize(Roles = "Admin")]
public class HomeController : Controller



نظرات مطالب
SQL Antipattern #2
نیازی به استفاده از Id نیست. مسیر زیر را در نظر بگیرید:
/// Example: "00001.00042.00005".
مسیر بالا متناظر با نودی در درخت می‌باشد که در عمق 2 بوده و فرزند 5 ام مربوط به نود 00001.00042 می‌باشد. اگر نیاز باشد فرزند جدیدی به نود 00001.00042 اضافه شود، باید ابتدا مسیر آخرین فرزند آن یعنی الگوی بالایی واکشی شده و سپس مسیر جدیدی برای نود جدید به شکل زیر تشکیل شود:
/// Example: "00001.00042.00006".
دقیقا مشابه به کاری می‌باشد که نوع داده hierarchyid موجود در Sql Server انجام می‌دهد. با این روش دقیقا مشخص می‌باشد که نود x در چه مکانی قرار داد.

مدیریت واحدهای سازمانی
یکسری متد کمکی هم برای مدیریت فیلد Path در نظر گرفته شده است.
    public class OrganizationalUnit : TrackableEntity<User>, IHasRowVersion, IPassivable
    {
        #region Constants

        /// <summary>
        /// Maximum depth of an UO hierarchy.
        /// </summary>
        public const int MaxDepth = 16;

        /// <summary>
        /// Length of a code unit between dots.
        /// </summary>
        public const int PathUnitLength = 5;

        /// <summary>
        /// Maximum length of the <see cref="Path"/> property.
        /// </summary>
        public const int MaxPathLength = MaxDepth * (PathUnitLength + 1) - 1;

        public const char HierarchicalDisplayNameSeperator = '»';

        #endregion

        #region Properties

        public string Name { get; set; }
        public string NormalizedName { get; set; }
        public string HierarchicalDisplayName { get; set; }
        /// <summary>
        /// Hierarchical Path of this organization unit.
        /// Example: "00001.00042.00005".
        /// It's changeable if OU hierarch is changed.
        /// </summary>
        public string Path { get; set; }
        public bool IsActive { get; set; } = true;
        public byte[] RowVersion { get; set; }

        #endregion

        #region Navigation Properties

        public OrganizationalUnit Parent { get; set; }
        public long? ParentId { get; set; }
        public ICollection<OrganizationalUnit> Children { get; set; } = new HashSet<OrganizationalUnit>();
        public ICollection<UserOrganizationalUnit> UserOrganizationalUnits { get; set; } =
            new HashSet<UserOrganizationalUnit>();

        #endregion

        #region Public Methods

        /// <summary>
        /// Creates path for given numbers.
        /// Example: if numbers are 4,2 then returns "00004.00002";
        /// </summary>
        /// <param name="numbers">Numbers</param>
        public static string CreatePath(params int[] numbers)
        {
            if (numbers.IsNullOrEmpty())
            {
                return null;
            }

            return numbers.Select(number => number.ToString(new string('0', PathUnitLength))).JoinAsString(".");
        }

        /// <summary>
        /// Appends a child path to a parent path. 
        /// Example: if parentPath = "00001", childPath = "00042" then returns "00001.00042".
        /// </summary>
        /// <param name="parentPath">Parent path. Can be null or empty if parent is a root.</param>
        /// <param name="childPath">Child path.</param>
        public static string AppendPath(string parentPath, string childPath)
        {
            if (childPath.IsNullOrEmpty())
            {
                throw new ArgumentNullException(nameof(childPath), "childPath can not be null or empty.");
            }

            if (parentPath.IsNullOrEmpty())
            {
                return childPath;
            }

            return parentPath + "." + childPath;
        }

        /// <summary>
        /// Gets relative path to the parent.
        /// Example: if path = "00019.00055.00001" and parentPath = "00019" then returns "00055.00001".
        /// </summary>
        /// <param name="path">The path.</param>
        /// <param name="parentPath">The parent path.</param>
        public static string GetRelativePath(string path, string parentPath)
        {
            if (path.IsNullOrEmpty())
            {
                throw new ArgumentNullException(nameof(path), "Path can not be null or empty.");
            }

            if (parentPath.IsNullOrEmpty())
            {
                return path;
            }

            if (path.Length == parentPath.Length)
            {
                return null;
            }

            return path.Substring(parentPath.Length + 1);
        }

        /// <summary>
        /// Calculates next path for given path.
        /// Example: if code = "00019.00055.00001" returns "00019.00055.00002".
        /// </summary>
        /// <param name="path">The path.</param>
        public static string CalculateNextPath(string path)
        {
            if (path.IsNullOrEmpty())
            {
                throw new ArgumentNullException(nameof(path), "Path can not be null or empty.");
            }

            var parentPath = GetParentPath(path);
            var lastUnitPath = GetLastUnitPath(path);

            return AppendPath(parentPath, CreatePath(Convert.ToInt32(lastUnitPath) + 1));
        }

        /// <summary>
        /// Gets the last unit path.
        /// Example: if path = "00019.00055.00001" returns "00001".
        /// </summary>
        /// <param name="path">The path.</param>
        public static string GetLastUnitPath(string path)
        {
            if (path.IsNullOrEmpty())
            {
                throw new ArgumentNullException(nameof(path), "Path can not be null or empty.");
            }

            var splittedPath = path.Split('.');
            return splittedPath[splittedPath.Length - 1];
        }

        /// <summary>
        /// Gets parent path.
        /// Example: if path = "00019.00055.00001" returns "00019.00055".
        /// </summary>
        /// <param name="path">The path.</param>
        public static string GetParentPath(string path)
        {
            if (path.IsNullOrEmpty())
            {
                throw new ArgumentNullException(nameof(path), "Path can not be null or empty.");
            }

            var splittedPath = path.Split('.');
            if (splittedPath.Length == 1)
            {
                return null;
            }

            return splittedPath.Take(splittedPath.Length - 1).JoinAsString(".");
        }

        #endregion
    }

البته یک ویو نمایشی برای حالت درختی هم بهتر است داشته باشید.


یکسری متد DomainService

       public virtual async Task<string> GetNextChildPathAsync(long? parentId)
        {
            var lastChild = await GetLastChildOrNullAsync(parentId).ConfigureAwait(false);
            if (lastChild == null)
            {
                var parentPath = parentId != null ? await GetPathAsync(parentId.Value).ConfigureAwait(false) : null;
                return OrganizationalUnit.AppendPath(parentPath, OrganizationalUnit.CreatePath(1));
            }

            return OrganizationalUnit.CalculateNextPath(lastChild.Path);
        }

        public async Task<string> GetNextChildHierarchicalDisplayNameAsync(string name, long? parentId)
        {
            var parent = parentId != null
                ? await _organizationalUnits.SingleOrDefaultAsync(a => a.Id == parentId.Value).ConfigureAwait(false)
                : null;

            return parent == null
                ? name
                : $"{parent.HierarchicalDisplayName} {OrganizationalUnit.HierarchicalDisplayNameSeperator} {name}";
        }

        public virtual async Task<OrganizationalUnit> GetLastChildOrNullAsync(long? parentId)
        {
            return await _organizationalUnits.OrderByDescending(c => c.Path)
                .FirstOrDefaultAsync(ou => ou.ParentId == parentId).ConfigureAwait(false);
        }

        public virtual async Task<string> GetPathAsync(long id)
        {
            Guard.ArgumentNotZero(id, nameof(id));
            var organizationalUnit = await _organizationalUnits.SingleOrDefaultAsync(ou => ou.Id == id).ConfigureAwait(false);
            if (organizationalUnit == null)
            {
                throw new KeyNotFoundException();
            }
            return organizationalUnit.Path;
        }

        public async Task<List<OrganizationalUnit>> FindChildrenAsync(long? parentId, bool recursive = false)
        {
            if (!recursive)
            {
                return await _organizationalUnits.Where(ou => ou.ParentId == parentId).ToListAsync().ConfigureAwait(false);
            }

            if (!parentId.HasValue)
            {
                return await _organizationalUnits.ToListAsync().ConfigureAwait(false);
            }

            var path = await GetPathAsync(parentId.Value).ConfigureAwait(false);

            return await _organizationalUnits.Where(
                ou => ou.Path.StartsWith(path) && ou.Id != parentId.Value).ToListAsync().ConfigureAwait(false);
        }

        public virtual async Task MoveAsync(long id, long? parentId)
        {
            Guard.ArgumentNotZero(id, nameof(id));
            var organizationalUnit = await _organizationalUnits.SingleOrDefaultAsync(ou => ou.Id == id).ConfigureAwait(false);
            if (organizationalUnit == null || organizationalUnit.ParentId == parentId)
            {
                return;
            }

            //Should find children before Path change
            var children = await FindChildrenAsync(id, true).ConfigureAwait(false);

            //Store old Path of OU
            var oldPath = organizationalUnit.Path;

            //Move OU
            organizationalUnit.Path = await GetNextChildPathAsync(parentId).ConfigureAwait(false);
            organizationalUnit.ParentId = parentId;

            //Update Children Paths
            foreach (var child in children)
            {
                child.Path = OrganizationalUnit.AppendPath(organizationalUnit.Path, OrganizationalUnit.GetRelativePath(child.Path, oldPath));
            }
        }



مطالب
Functional Programming یا برنامه نویسی تابعی - قسمت سوم – Immutability

در ادامه مطالب مربوط به برنامه نویسی تابعی، قصد دارم بیشتر وارد کد شویم و مباحث عنوان شده را در دنیای کد پیاده سازی کنیم. هدف این قسمت، refactor کردن کد موجود به یک معماری immutable هست.  پیشتر درباره immutable ‌ها صحبت کردیم. ابتدا برای یکسان سازی ادبیات مورد استفاده، چند کلمه را مجددا تعریف خواهیم کرد:

  • Immutability: عدم توانایی تغییر داده
  • State: داده‌هایی که در طول زمان تغییر می‌کنند
  • Side Effect: تغییری که روی داده‌ها اتفاق می‌افتد

در قطعه کد زیر سعی شده‌است تفاوت یک کلاس Stateless و stateful را به سادگی نشان دهیم:

    //Stateful
    public class UserProfile
    {
        private User _user;
        private string _address;

        public void UpdateUser(int userId, string name)
        {
            _user = new User(userId, name);
        }
    }

    //Stateless
    public class User
    {
        public User(int id, string name)
        {
            Id = id;
            Name = name;
        }

        public int Id { get; }
        public string Name { get; }
    }


چرا Immutable بودن مهم است؟ 

هر عمل mutable  معادل کدی غیر شفاف است. در واقع وابستگی هر عملی که انجام می‌دهیم به state، باعث می‌شود که شرایط ناپایداری را در کد داشته باشیم. به طور مثال در یک عملیات چند نخی تصور کنید که چندین نخ به طور همزمان می‌توانند state را تغییر دهند و مدیریت این قضیه باعث به وجود آمدن کد‌هایی ناخوانا و تحمیل پیچیدگی بیشتر به کد خواهد شد. 

در واقع انتظار داریم که به ازای یک ورودی بر اساس بدنه‌ی متد، یک خروجی داشته باشیم؛ ولی در واقعیت تاثیری که اجرای متد بر روی state کل کلاس خواهد گذاشت، از دید ما پنهان است و باعث به وجود آمدن مشکلات بعدی خواهد شد. برای مثال قطعه کد بالا را به صورت Honest بازنویسی میکنیم: 

    public class UserProfile
    {
        private readonly User _user;
        private readonly string _address;

        public UserProfile(User user,string address)
        {
            _user = user;
            _address = address;
        }
        public UserProfile UpdateUser(int userId, string name)
        {
            var newUser = new User(userId, name);
            return  new UserProfile(newUser,_address);
        }
    }

    public class User
    {
        public User(int id, string name)
        {
            Id = id;
            Name = name;
        }

        public int Id { get; }
        public string Name { get; }
    }

در این مثال متد UpdateUser به جای  void، یک شی از جنس کلاس UserProfile را بر می‌گرداند. کلاس UserProfile هم برای وهله سازی نیاز به یک شیء از جنس User و Address را دارد. بنابراین مطمئن هستیم که مقدار دهی شده‌اند. نکته دیگر در قطعه کد بالا این است که به ازای هر بار فراخوانی متد، یک شیء جدید بدون وابستگی به وهله سازی اشیاء دیگر، برگردانده میشود.


Immutable بودن باعث می‌شود: 

  • خوانایی کد افزایش پیدا کند
  • جای واحدی برای Validate کردن داشته باشیم
  • به صورت ذاتی Thread Safe باشیم


در مورد محدودیت‌هایی که در کار با اشیاء Immutable باید در نظر داشته باشیم، می‌توان به مصرف بالای رم و سی پی یو، اشاره کرد. در واقع به نسبت حالت mutate، تعداد اشیاء بیشتری ساخته خواهند شد. در فریمورک دات نت برای کار با اشیا immutable امکاناتی در نظر گرفته شده که این هزینه را کاهش می‌دهند. به طور مثال می‌توانیم از کلاس ImmutableList استفاده کنیم و از ایجاد اشیاء اضافه‌تر و تحمیل بار اضافی به GC جلوگیری کنیم. یک مثال: 

//Create Immutable List
ImmutableList<string> list = ImmutableList.Create<string>();
ImmutableList<string> list2 = list.Add("Salam");

//Builder
ImmutableList<string>.Builder builder = ImmutableList.CreateBuilder<string>();
builder.Add("avali");
builder.Add("dovomi");
builder.Add("sevomi");

ImmutableList<string> immutableList = builder.ToImmutable();


چطور با side effect کنار بیایم؟ 

یکی از الگوهای رایج برای این کار، مفهوم جدا سازی Command/Query است. به طور ساده تمامی عملیاتی را که تاثیر گذار هستند، به صورت Command در نظر میگیریم. Command ‌ها معمولا هیچ نوعی را بازگشت نمیدهند و همینطور بر عکس این قضیه برای Query ‌ها صادق است. اشتباه رایج درباره این الگو، محدود کردن این الگو به معماری‌های خاصی مانند Domain Driven می‌باشد؛ در صورتیکه الزامی برای رعایت این الگو در سایر معماری‌ها وجود ندارد. 

به مثال زیر دقت کنید. سعی کردم قسمت‌های Command و Query را از هم جدا کنم: 

در واقع هر برنامه می‌تواند شامل دو قسمت باشد:

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

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

برای مسائلی که در بالا صحبت شد، نمونه‌‌ای را آماده کرده‌ام. این نمونه به طور ساده یک سیستم مدیریت نوبت است که نوبت‌ها را در فایلی ذخیره و بازیابی میکند ( mutate ) و منطق مربوط به نوبت‌ها و زمان ویزیت آن میتواند به صورت immutable پیاده سازی شود. این کد در دو حالت functional و غیر functional پیاده سازی شده تا به خوبی تفاوت آن را در حالت قبل و بعد از برنامه نویسی تابعی بتوانیم درک کنیم. به جهت خوانایی بیشتر و دسترسی به کد‌ها، آن‌ها را روی گیت‌هاب قرار داده و شما میتوانید از اینجا سورس کد مورد نظر را بررسی کنید. سعی شده در این مثال تمامی مواردی که در این قسمت ذکر شد را پیاده سازی کنیم. امیدوارم که مطالب مربوط به برنامه نویسی تابعی یا functional programming توانسته باشد دیدگاه جدیدی را به کدهایی که مینویسیم بدهد. در  قسمت‌های بعدی به مواردی مانند مدیریت exception ‌ها و کار با null ‌ها و ... خواهیم پرداخت.

مطالب
رمزنگاری و رمزگشایی خودکار خواص مدل‌ها در ASP.NET Core
فرض کنید قصد دارید خاصیت Id مدل مورد استفاده‌ی در یک View را رمزنگاری کنید تا در سمت کلاینت به سادگی قابل تغییر نباشد. همچنین این Id زمانیکه به سمت سرور ارسال شد، به صورت خودکار رمزگشایی شود و بدون نیاز به تغییرات خاصی در کدهای متداول اکشن متدها، اطلاعات نهایی آن قابل استفاده باشند. برای این منظور در ASP.NET Core می‌توان یک Action Result رمزنگاری کننده و یک Model binder رمزگشایی کننده را طراحی کرد.


نیاز به علامتگذاری خواصی که باید رمزنگاری شوند

می‌خواهیم خاصیت یا خاصیت‌های مشخصی، از یک مدل را رمزنگاری شده به سمت کلاینت ارسال کنیم. به همین جهت ویژگی خالی زیر را به پروژه اضافه می‌کنیم تا از آن تنها جهت علامتگذاری این نوع خواص، استفاده کنیم:
using System;

namespace EncryptedModelBinder.Utils
{
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class EncryptedFieldAttribute : Attribute { }
}


رمزنگاری خودکار مدل خروجی از یک اکشن متد

در ادامه کدهای کامل یک ResultFilter را مشاهده می‌کنید که مدل ارسالی به سمت کلاینت را یافته و سپس خواصی از آن‌را که با ویژگی EncryptedField مزین شده‌اند، به صورت خودکار رمزنگاری می‌کند:
namespace EncryptedModelBinder.Utils
{
    public class EncryptedFieldResultFilter : ResultFilterAttribute
    {
        private readonly IProtectionProviderService _protectionProviderService;
        private readonly ILogger<EncryptedFieldResultFilter> _logger;
        private readonly ConcurrentDictionary<Type, bool> _modelsWithEncryptedFieldAttributes = new ConcurrentDictionary<Type, bool>();

        public EncryptedFieldResultFilter(
            IProtectionProviderService protectionProviderService,
            ILogger<EncryptedFieldResultFilter> logger)
        {
            _protectionProviderService = protectionProviderService;
            _logger = logger;
        }

        public override void OnResultExecuting(ResultExecutingContext context)
        {
            var model = context.Result switch
            {
                PageResult pageResult => pageResult.Model, // For Razor pages
                ViewResult viewResult => viewResult.Model, // For MVC Views
                ObjectResult objectResult => objectResult.Value, // For Web API results
                _ => null
            };

            if (model is null)
            {
                return;
            }

            if (typeof(IEnumerable).IsAssignableFrom(model.GetType()))
            {
                foreach (var item in model as IEnumerable)
                {
                    encryptProperties(item);
                }
            }
            else
            {
                encryptProperties(model);
            }
        }

        private void encryptProperties(object model)
        {
            var modelType = model.GetType();
            if (_modelsWithEncryptedFieldAttributes.TryGetValue(modelType, out var hasEncryptedFieldAttribute)
                && !hasEncryptedFieldAttribute)
            {
                return;
            }

            foreach (var property in modelType.GetProperties())
            {
                var attribute = property.GetCustomAttributes(typeof(EncryptedFieldAttribute), false).FirstOrDefault();
                if (attribute == null)
                {
                    continue;
                }

                hasEncryptedFieldAttribute = true;

                var value = property.GetValue(model);
                if (value is null)
                {
                    continue;
                }

                if (value.GetType() != typeof(string))
                {
                    _logger.LogWarning($"[EncryptedField] should be applied to `string` proprties, But type of `{property.DeclaringType}.{property.Name}` is `{property.PropertyType}`.");
                    continue;
                }

                var encryptedData = _protectionProviderService.Encrypt(value.ToString());
                property.SetValue(model, encryptedData);
            }

            _modelsWithEncryptedFieldAttributes.TryAdd(modelType, hasEncryptedFieldAttribute);
        }
    }
}
توضیحات:
- در اینجا برای رمزنگاری از IProtectionProviderService استفاده شده‌است که در بسته‌ی DNTCommon.Web.Core تعریف شده‌است. این سرویس در پشت صحنه از سیستم Data Protection استفاده می‌کند.
- سپس رخ‌داد OnResultExecuting، بازنویسی شده‌است تا بتوان به مدل ارسالی به سمت کلاینت، پیش از ارسال نهایی آن، دسترسی یافت.
- context.Result می‌تواند از نوع PageResult صفحات Razor باشد و یا از نوع ViewResult مدل‌های متداول Viewهای پروژه‌های MVC و یا از نوع ObjectResult که مرتبط است به پروژه‌های Web Api بدون هیچ نوع View سمت سروری. هر کدام از این نوع‌ها، دارای خاصیت مدل هستند که در اینجا قصد بررسی آن‌را داریم.
- پس از مشخص شدن شیء Model، اکنون حلقه‌ای را بر روی خواص آن تشکیل داده و خواصی را که دارای ویژگی EncryptedFieldAttribute هستند، یافته و آن‌ها را رمزنگاری می‌کنیم.

روش اعمال این فیلتر باید به صورت سراسری باشد:
namespace EncryptedModelBinder
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDNTCommonWeb();
            services.AddControllersWithViews(options =>
            {
                options.Filters.Add(typeof(EncryptedFieldResultFilter));
            });
        }
از این پس مدل‌های تمام خروجی‌های ارسالی به سمت کلاینت، بررسی شده و در صورت لزوم، خواص آن‌ها رمزنگاری می‌شود.


رمزگشایی خودکار مدل دریافتی از سمت کلاینت

تا اینجا موفق شدیم خواص ویژه‌ای از مدل‌ها را رمزنگاری کنیم. مرحله‌ی بعد، رمزگشایی خودکار این اطلاعات در سمت سرور است. به همین جهت نیاز داریم تا در سیستم Model Binding پیش‌فرض ASP.NET Core مداخله کرده و منطق سفارشی خود را تزریق کنیم. بنابراین در ابتدا یک IModelBinderProvider سفارشی را تهیه می‌کنیم تا در صورتیکه خاصیت جاری در حال بررسی توسط سیستم Model Binding دارای ویژگی EncryptedFieldAttribute بود، از EncryptedFieldModelBinder برای پردازش آن استفاده کند:
namespace EncryptedModelBinder.Utils
{
    public class EncryptedFieldModelBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (context.Metadata.IsComplexType)
            {
                return null;
            }

            var propName = context.Metadata.PropertyName;
            if (string.IsNullOrWhiteSpace(propName))
            {
                return null;
            }

            var propInfo = context.Metadata.ContainerType.GetProperty(propName);
            if (propInfo == null)
            {
                return null;
            }

            var attribute = propInfo.GetCustomAttributes(typeof(EncryptedFieldAttribute), false).FirstOrDefault();
            if (attribute == null)
            {
                return null;
            }

            return new BinderTypeModelBinder(typeof(EncryptedFieldModelBinder));
        }
    }
}
که این EncryptedFieldModelBinder به صورت زیر تعریف می‌شود:
namespace EncryptedModelBinder.Utils
{
    public class EncryptedFieldModelBinder : IModelBinder
    {
        private readonly IProtectionProviderService _protectionProviderService;

        public EncryptedFieldModelBinder(IProtectionProviderService protectionProviderService)
        {
            _protectionProviderService = protectionProviderService;
        }

        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
            {
                throw new ArgumentNullException(nameof(bindingContext));
            }

            var logger = bindingContext.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>();
            var fallbackBinder = new SimpleTypeModelBinder(bindingContext.ModelType, logger);
            var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
            if (valueProviderResult == ValueProviderResult.None)
            {
                return fallbackBinder.BindModelAsync(bindingContext);
            }

            bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);

            var valueAsString = valueProviderResult.FirstValue;
            if (string.IsNullOrWhiteSpace(valueAsString))
            {
                return fallbackBinder.BindModelAsync(bindingContext);
            }

            var decryptedResult = _protectionProviderService.Decrypt(valueAsString);
            bindingContext.Result = ModelBindingResult.Success(decryptedResult);
            return Task.CompletedTask;
        }
    }
}
در اینجا مقدار ارسالی به سمت سرور به صورت یک رشته دریافت شده و سپس رمزگشایی می‌شود و بجای مقدار فعلی خاصیت، مورد استفاده قرار می‌گیرد. به این ترتیب دیگر نیازی به تغییر کدهای اکشن متدها برای رمزگشایی اطلاعات نیست.

پس از این تعاریف نیاز است EncryptedFieldModelBinderProvider را به صورت زیر به سیستم معرفی کرد:
namespace EncryptedModelBinder
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDNTCommonWeb();
            services.AddControllersWithViews(options =>
            {
                options.ModelBinderProviders.Insert(0, new EncryptedFieldModelBinderProvider());
                options.Filters.Add(typeof(EncryptedFieldResultFilter));
            });
        }


یک مثال

فرض کنید مدل‌های زیر تعریف شده‌اند:
namespace EncryptedModelBinder.Models
{
    public class ProductInputModel
    {
        [EncryptedField]
        public string Id { get; set; }

        [EncryptedField]
        public int Price { get; set; }

        public string Name { get; set; }
    }
}

namespace EncryptedModelBinder.Models
{
    public class ProductViewModel
    {
        [EncryptedField]
        public string Id { get; set; }

        [EncryptedField]
        public int Price { get; set; }

        public string Name { get; set; }
    }
}
که بعضی از خواص آن‌ها با ویژگی EncryptedField مزین شده‌اند.
اکنون کنترلر زیر زمانیکه رندر شود، View متناظر با اکشن متد Index آن، یکسری لینک را به اکشن متد Details، جهت مشاهده‌ی جزئیات محصول، تولید می‌کند. همچنین اکشن متد Products آن هم فقط یک خروجی JSON را به همراه دارد:
namespace EncryptedModelBinder.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            var model = getProducts();
            return View(model);
        }

        public ActionResult<string> Details(ProductInputModel model)
        {
            return model.Id;
        }

        public ActionResult<List<ProductViewModel>> Products()
        {
            return getProducts();
        }

        private static List<ProductViewModel> getProducts()
        {
            return new List<ProductViewModel>
            {
                new ProductViewModel { Id = "1", Name = "Product 1"},
                new ProductViewModel { Id = "2", Name = "Product 2"},
                new ProductViewModel { Id = "3", Name = "Product 3"}
            };
        }
    }
}
کدهای View اکشن متد Index به صورت زیر است:
@model List<ProductViewModel>

<h3>Home</h3>

<ul>
    @foreach (var item in Model)
    {
        <li><a asp-action="Details" asp-route-id="@item.Id">@item.Name</a></li>
    }
</ul>
در ادامه اگر برنامه را اجرا کنیم، می‌توان مشاهده کرد که تمام asp-route-id‌ها که به خاصیت ویژه‌ی Id اشاره می‌کنند، به صورت خودکار رمزنگاری شده‌اند:


و اگر یکی از لینک‌ها را درخواست کنیم، خروجی model.Id، به صورت معمولی و رمزگشایی شده‌ای مشاهده می‌شود (این خروجی یک رشته‌است که هیچ ویژگی خاصی به آن اعمال نشده‌است. به همین جهت، اینبار این خروجی معمولی مشاهده می‌شود). هدف از اکشن متد Details، نمایش رمزگشایی خودکار اطلاعات است.


و یا اگر اکشن متدی که همانند اکشن متدهای Web API، فقط یک شیء JSON را باز می‌گرداند، فراخوانی کنیم نیز می‌توان به خروجی رمزنگاری شده‌ی زیر رسید:



کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: EncryptedModelBinder.zip
مطالب
الگوی Composite
الگوی Composite یکی دیگر از الگوهای ساختاری می‌باشد که قصد داریم در این مقاله آن را بررسی نماییم.

الگوی Composite در عمل یک Collection Pattern (الگوی مجموعه ای) است. که می‌توان در درون آن ترکیبی از زیر مجموعه‌های مختلف را قرار داد و سپس هر زیر مجموعه را به نوبه خود فراخوانی نمود.به بیان دیگر الگوی Composite به ما کمک می‌کند که در یک ساختار درختی بتوانیم مجموعه ای (Collection ی)،از بخشی از آبجکتهای سلسله مراتبی را نمایش دهیم. این الگو به Client اجازه می‌دهد، که رفتار یکسانی نسبت به یک Collection ی از آبجکتها یا یک آبجکت تنها داشته باشد.

مثالهای متعددی می‌توان از الگوی Composite زد، که در ذیل به چند نمونه از آنها می‌پردازیم:

نمونه اول: همانطور که می‌دانیم یک سازمان از بخشهای مختلفی تشکیل شده است، که بصورت سلسله مراتبی با یکدیگر در ارتباط می‌باشند، چنانچه بخواهیم بخشها و زیر مجموعه‌های تابعه آنها را بصورت آبجکت نگهداری نماییم، یکی از بهترین الگوهای پیشنهاد شده الگوی Composite می‌باشد.

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

نمونه سوم: یک File System را در نظر بگیرید،که ساختارش از File و Folder تشکیل شده است. و می‌تواند یک ساختار سلسله مراتبی داشته باشد.بطوریکه درون هر Folder می‌تواند یک یا چند File یا Folder قرار گیرد. و در درون Folder‌های زیر مجموعه می‌توان چندین File یا Folder دیگر قرار داد.اگر بخواهیم به عنوان نمونه شکل ساختار درختی File و فولدر را نمایش دهیم بصورت زیر خواهد بود:

در ساختار درختی به Folder شاخه یا Branch گویند، چون می‌تواند زیر شاخه‌های دیگری نیز در خود داشته باشد. و به File برگ یا Leaf گویند.برگ نمی‌تواند زیر مجموعه ای داشته باشد. در واقع برگ (Leaf) بیانگر انتهای یک شاخه می‌باشد.

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

الگوی Composite از سه Component اصلی تشکیل شده است،که یکایک آنها را بررسی می‌کنیم:

  • Component: کلاس پایه ای است که در آن متدها یا Functionality‌های مشترک تعریف می‌گردد. Component می‌تواند یک Abstract Class یا Interface باشد.
  • Leaf : به آبجکتهای گفته می‌شود که هیچ Child ی ندارند. و فقط یک آبجکت مستقل تنها می‌باشد. کلاس Leaf متدهای مشترک تعریف شده در Component  را پیاده سازی می‌کند.اگر مثال File و Folder را بخاطر آورید،File یک آبجکت از نوع Leaf است چون نمی‌تواند هیچ فرزندی داشته باشد و یک آبجکت تنها می‌باشد.
  • Composite: کلاس فوق Collection ی از آبجکتها را در خود نگهداری می‌کند، به عبارتی در Composite می‌توان بخشی از ساختار درختی را قرار داد، که این ساختار می‌تواند ترکیبی از آبجکتهای Leaf و Composite باشد. در مثال File و Folder، یک Folder را می‌توان به عنوان Composite در نظر گرفت،زیرا که یک Folder می‌تواند چندین File یا Folder را در خود جای دهد. در کلاس Composite معمولا متدهایی همچون Add (افزودن Remove،( Child (حذف یک Child) و غیرو... وجود دارد. 
کلاس Leaf و کلاس Composite از کلاس Component ارث بری (Inherit) می‌شوند.
شکل زیر بیانگر الگوی Composite می‌باشد:


توصیف شکل: طبق تعاریف گفته شده، دو کلاس Leaf و Composite از Inherit ،Component شده اند. و Client نیز فقط متد‌های مشترک تعریف شده در Component را مشاهده می‌کند، به عبارتی Client رفتار یکسانی نسبت به Leaf و Composite خواهد داشت.
برای درک بیشتر الگوی Composite مثالی را بررسی می‌کنیم، فرض کنید در کلاس Component متدی به نام Display را تعریف می‌کنیم،بطوریکه نام آبجکت را نمایش دهد.بنابراین خواهیم داشت:
اینترفیسی را برای Component در نظر می‌گیریم، و متد Display را در آن تعریف می‌کنیم:
public interface Icomponent
    {
        void Display(int depth);
    }
در کلاس Leaf، اینترفیس IComponent را پیاده سازی می‌نماییم:
 public class Leaf:Icomponent
    {
        private String name = string.Empty;
        public Leaf(string name)
        {
            this.name = name;
        }

        public void Display(int depth)
        {

            Console.WriteLine(new String('-', depth) + ' ' + name);

        }
    }
در کلاس Composite نیز اینترفیس IComponent را پیاده سازی می‌نماییم، با این تفاوت که متد‌های Add و Remove را نیز در کلاس Composite اضافه می‌کنیم، چون قبلا هم گفته بودیم، Composite در حکم یک Collection می‌باشد، بنابراین می‌بایست قابلیت حذف و اضافه نمود آبجکت در خود را داشته باشد. پیاده سازی متد Display در آن بصورت Recursive (بازگشتی) میباشد. و علتش این است که بتوانیم ساختار سلسله مراتبی را بازیابی نماییم.
 public class Composite:Icomponent
    {
        private List<Icomponent> _children = new List<Icomponent>();
        private String name = String.Empty;

        public Composite(String sname)
        {
            this.name = sname;
        }

        public void Add(Icomponent component)
        {

            _children.Add(component);

        }


        public void Remove(Icomponent component)
        {

            _children.Remove(component);

        }


        public void Display(int depth)
        {

            Console.WriteLine(new String('-', depth) + ' ' + name);



            // Recursively display child nodes

            foreach (Icomponent component in _children)
            {

                component.Display(depth + 2);

            }

        }
    }
در ادامه بوسیله چندین آبجکت Leaf و Composite یک ساختار درختی را ایجاد می‌کنیم.
class Program
    {
        static void Main(string[] args)
        {
            // Create a tree structure

            Composite root = new Composite("root");

            root.Add(new Leaf("Leaf A"));

            root.Add(new Leaf("Leaf B"));



            Composite comp = new Composite("Composite X");

            comp.Add(new Leaf("Leaf XA"));

            comp.Add(new Leaf("Leaf XB"));



            root.Add(comp);

            root.Add(new Leaf("Leaf C"));



            // Add and remove a leaf

            Leaf leaf = new Leaf("Leaf D");

            root.Add(leaf);

            root.Remove(leaf);



            // Recursively display tree

            root.Display(1);

            Console.ReadKey();
        }
    }
در ابتدا یک آبجکت Composite ایجاد می‌کنیم و آن را به عنوان ریشه در نظر گرفته و نام آن را Root قرار می‌دهیم. سپس دو آبجکت LeafA و LeafB را به آن می‌افزاییم، در ادامه آبجکت Composite دیگری به نام Comp ایجاد می‌کنیم، که خود دارای دو فرزند به نامهای LeafXA و LeafXB  می باشد. و سر آخر هم یک آبجکت LeafC ایجاد می‌کنیم.
آبجکت LeafD صرفا جهت نمایش افزودن و حذف کردن آن در یک آبجکت Composite نوشته شده است. برای این که بتوانیم ساختار سلسله مراتبی کد بالا را مشاهده نماییم، متد Root.Display را اجرا می‌کنیم و خروجی آن بصورت زیر خواهد بود:

اگر بخواهیم،شکل درختی آن را تصور کنیم بصورت زیر خواهد بود:


درپایان باید بگویم،که نمونه کد بالا را می‌توان به ساختار File و Folder نیز تعمیم داد، بطوریکه متدهای مشترک بین File و Folder را در اینترفیس IComponent تعریف می‌کنیم و بطور جداگانه در کلاسهای Composite و Leaf پیاده سازی می‌کنیم.
امیدوارم توضیحات داده شده در مورد الگوی Composite مفید واقع شود.

نظرات مطالب
بررسی روش آپلود فایل‌ها در ASP.NET Core
اکثرا از base64 استفاده میکنم. برای برنامه نویس‌های موبایل و فرانت قابل قبول‌تر است :)
نمونه کد تبدیل base64 به iformfile:
public static async Task<ResponsePayload<string>> SaveBase64(this string imgBase64, string filePath, FileSizeType fileSizeType)
    {
        if (string.IsNullOrWhiteSpace(imgBase64))
            return new ResponsePayload<string>(false, "فایل را وارد کنید.", null);

        string data;
        if (imgBase64.StartsWith("data:"))
        {
            string[] base64Arr = imgBase64.Split(',');
            if (base64Arr.Length == 0)
                return new ResponsePayload<string>(false, "فایل را وارد کنید.", null);
            data = base64Arr[1];
        }
        else
        {
            data = imgBase64;
        }

        byte[] bytes = Convert.FromBase64String(data);
        var fileType = GetFileExtension(imgBase64);
        if (string.IsNullOrEmpty(fileType))
            return new ResponsePayload<string>(false, "فایل وارد شده صحیح نمی‌باشد.", null);

        using var stream = new MemoryStream(bytes);
        IFormFile file = new FormFile(stream, 0, bytes.Length, filePath, "." + fileType);

        string fileName = Guid.NewGuid().ToString().Replace("-", "");
        return await UploadFile(file, filePath + fileName, fileSizeType);
    }
private static string GetFileExtension(string base64String)
    {
        string data;

        if (base64String.StartsWith("data:"))
        {
            string[] base64Arr = base64String.Split(',');
            if (base64Arr.Length == 0)
                return "";
            data = base64Arr[1];
        }
        else
        {
            data = base64String;
        }
        return data.Substring(0, 5).ToUpper() switch
        {
            "IVBOR" => "png",
            "/9J/4" => "jpg",
            "AAAAF" => "mp4",
            "JVBER" => "pdf",
            "AAABA" => "ico",
            "UMFYI" => "rar",
            "E1XYD" => "rtf",
            "U1PKC" => "txt",
            "MQOWM" => "srt",
            "77U/M" => "srt",
            "UESDB" => "",
            "" => "docx",
            _ => string.Empty,
        };
    }
}

public class FileSizeType
{
    public int Size { get; set; }
}

مطالب
بررسی روش آپلود فایل‌ها در ASP.NET Core
مدیریت پردازش آپلود فایل‌ها در ASP.NET Core نسبت به ASP.NET MVC 5.x به طور کامل تغییر کرده‌است و اینبار بجای ذکر نوع System.Web.HttpPostedFileBase باید از اینترفیس جدید IFormFile واقع در فضای نام Microsoft.AspNetCore.Http کمک گرفت.


مراحل فعال سازی آپلود فایل‌ها در ASP.NET Core

مرحله‌ی اول فعال سازی آپلود فایل‌ها در ASP.NET Core، شامل افزودن ویژگی "enctype="multipart/form-data به یک فرم تعریف شده‌است:
<form method="post"
      asp-action="Index"
      asp-controller="TestFileUpload"
      enctype="multipart/form-data">
    <input type="file" name="files" multiple />
    <input type="submit" value="Upload" />
</form>
در اینجا همچنین ذکر ویژگی multiple در input از نوع file، امکان ارسال چندین فایل با هم را نیز میسر می‌کند.
در سمت سرور، امضای اکشن متد دریافت کننده‌ی این فایل‌ها به صورت ذیل خواهد بود:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Index(IList<IFormFile> files)
در اینجا نام پارامتر تعریف شده، باید دقیقا مساوی نام input از نوع file باشد. همچنین از آنجائیکه ویژگی multiple را نیز در سمت کلاینت قید کرده‌ایم، این پارامتر سمت سرور از نوع یک لیست، تعریف شده‌است. اگر ویژگی multiple را حذف کنیم می‌توان آن‌را به صورت ساده‌ی IFormFile files نیز تعریف کرد.


یافتن جایگزینی برای Server.MapPath در ASP.NET Core

زمانیکه فایل ارسالی، در سمت سرور دریافت شد، مرحله‌ی بعد، ذخیره سازی آن بر روی سرور است و از آنجائیکه ما دقیقا نمی‌دانیم ریشه‌ی سایت در کدام پوشه‌ی سرور واقع شده‌است، می‌شد از متد Server.MapPath برای یافتن دقیق آن کمک گرفت. با حذف این متد در ASP.NET Core، روش یافتن ریشه‌ی سایت یا همان پوشه‌ی wwwroot در اینجا شامل مراحل ذیل است:
public class TestFileUploadController : Controller
{
    private readonly IHostingEnvironment _environment;
    public TestFileUploadController(IHostingEnvironment environment)
    {
        _environment = environment;
    }
ابتدا اینترفیس توکار IHostingEnvironment را در سازنده‌ی کلاس تزریق می‌کنیم. سرویس HostingEnvironment جزو سرویس‌های از پیش تعریف شده‌ی ASP.NET Core است و نیازی به تنظیمات اضافه‌تری ندارد. همینقدر که ذکر شود، به صورت خودکار توسط ASP.NET Core مقدار دهی و تامین می‌گردد.
پس از آن خاصیت environment.WebRootPath_ به ریشه‌ی پوشه‌ی wwwroot برنامه، بر روی سرور اشاره می‌کند. به این ترتیب می‌توان مسیر دقیقی را جهت ذخیره سازی فایل‌های رسیده، مشخص کرد.


امکان ذخیره سازی async فایل‌ها در ASP.NET Core

عملیات کار با فایل‌ها، عملیاتی است که از مرزهای IO سیستم عبور می‌کند. به همین جهت یکی از بهترین مثال‌های پیاده سازی async، جهت رها سازی تردهای برنامه و بالا بردن میزان پاسخ‌دهی آن با بالا بردن تعداد تردهای آزاد بیشتر است. در ASP.NET Core، نوشتن async محتوای فایل رسیده در یک stream پشتیبانی می‌شود و این stream می‌تواند یک FileStream و یا MemoryStream باشد. در ذیل نحوه‌ی کار async با یک FileStream را مشاهده می‌کنید:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Index(IList<IFormFile> files)
{
    var uploadsRootFolder = Path.Combine(_environment.WebRootPath, "uploads");
    if (!Directory.Exists(uploadsRootFolder))
    {
        Directory.CreateDirectory(uploadsRootFolder);
    }
 
    foreach (var file in files)
    {
        if (file == null || file.Length == 0)
        {
            continue;
        }
 
        var filePath = Path.Combine(uploadsRootFolder, file.FileName);
        using (var fileStream = new FileStream(filePath, FileMode.Create))
        {
            await file.CopyToAsync(fileStream).ConfigureAwait(false);
        }
    }
    return View();
}
در اینجا کدهای کامل متد دریافت فایل‌ها را در سمت سرور مشاهده می‌کنید. ابتدا با استفاده از خاصیت environment.WebRootPath_، به مسیر ریشه‌ی wwwroot دسترسی و سپس پوشه‌ی uploads را در آن جهت ذخیره سازی فایل‌های دریافتی، تعیین کرده‌ایم.
چون برنامه‌های ASP.NET Core قابلیت اجرای بر روی لینوکس را نیز دارند، تا حد امکان باید از Path.Combine جهت جمع زدن اجزای مختلف یک میسر، استفاده کرد. از این جهت که در لینوکس، جداکننده‌ی اجزای مسیرها، / است بجای \ در ویندوز و متد Path.Combine به صورت خودکار این مسایل را لحاظ خواهد کرد.
در آخر با استفاده از متد file.CopyToAsync کار نوشتن غیرهمزمان محتوای فایل دریافتی در یک FileStream انجام می‌شود؛ به همین جهت در امضای متد فوق، <async Task<IActionResult را نیز ملاحظه می‌کنید.


پشتیبانی کامل از Model Binding آپلود فایل‌ها در ASP.NET Core

در ASP.NET MVC 5.x اگر ویژگی Required را بر روی یک خاصیت از نوع HttpPostedFileBase قرار دهید ... کار نمی‌کند و در سمت کلاینت تاثیری را به همراه نخواهد داشت؛ مگر اینکه تنظیمات سمت کلاینت آن‌را به صورت دستی انجام دهیم. این مشکلات در ASP.NET Core، کاملا برطرف شده‌اند:
public class UserViewModel
{
    [Required(ErrorMessage = "Please select a file.")]
    [DataType(DataType.Upload)]
    public IFormFile Photo { get; set; }
}
در اینجا یک خاصیت از نوع IFormFile، با دو ویژگی Required و DataType خاص آن در یک ViewModel تعریف شده‌اند. فرم معادل آن در ASP.NET Core به صورت ذیل خواهد بود:
@model UserViewModel
 
<form method="post"
      asp-action="UploadPhoto"
      asp-controller="TestFileUpload"
      enctype="multipart/form-data">
    <div asp-validation-summary="ModelOnly" class="text-danger"></div>
 
    <input asp-for="Photo" />
    <span asp-validation-for="Photo" class="text-danger"></span>
    <input type="submit" value="Upload"/>
</form>
در اینجا ابتدا نوع مدل View تعیین شده‌است و سپس با استفاده از Tag Helpers، صرفا یک input را به خاصیت Photo مدل View جاری متصل کرده‌ایم. همین اتصال سبب فعال سازی مباحث اعتبارسنجی سمت سرور و کاربر نیز می‌شود.
اینبار جهت فعال سازی و استفاده‌ی از قابلیت‌های Model Binding می‌توان از ModelState نیز بهره گرفت:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadPhoto(UserViewModel userViewModel)
{
    if (ModelState.IsValid)
    {
        var formFile = userViewModel.Photo;
        if (formFile == null || formFile.Length == 0)
        {
            ModelState.AddModelError("", "Uploaded file is empty or null.");
            return View(viewName: "Index");
        }
 
        var uploadsRootFolder = Path.Combine(_environment.WebRootPath, "uploads");
        if (!Directory.Exists(uploadsRootFolder))
        {
            Directory.CreateDirectory(uploadsRootFolder);
        }
 
        var filePath = Path.Combine(uploadsRootFolder, formFile.FileName);
        using (var fileStream = new FileStream(filePath, FileMode.Create))
        {
            await formFile.CopyToAsync(fileStream).ConfigureAwait(false);
        }
 
        RedirectToAction("Index");
    }
    return View(viewName: "Index");
}
اگر ModelState معتبر باشد، کار ذخیره سازی تک فایل رسیده را انجام می‌دهیم. سایر نکات این متد، با اکشن متد Index که پیشتر بررسی شد، یکی هستند.


بررسی پسوند فایل‌های رسیده‌ی به سرور

ASP.NET Core دارای ویژگی است به نام FileExtensions که ... هیچ ارتباطی به خاصیت‌هایی از نوع IFormFile ندارد:
 [FileExtensions(Extensions = ".png,.jpg,.jpeg,.gif", ErrorMessage = "Please upload an image file.")]
ویژگی FileExtensions صرفا جهت درج بر روی خواصی از نوع string طراحی شده‌است. بنابراین قرار دادن این ویژگی بر روی خاصیت‌هایی از نوع IFormFile، سبب فعال سازی اعتبارسنجی سمت سرور پسوندهای فایل‌های رسیده، نخواهد شد.
در ادامه جهت بررسی پسوندهای فایل‌های رسیده، می‌توان یک ویژگی اعتبارسنجی سمت سرور جدید را طراحی کرد:
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class UploadFileExtensionsAttribute : ValidationAttribute
{
    private readonly IList<string> _allowedExtensions;
    public UploadFileExtensionsAttribute(string fileExtensions)
    {
        _allowedExtensions = fileExtensions.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();
    }
 
    public override bool IsValid(object value)
    {
        var file = value as IFormFile;
        if (file != null)
        {
            return isValidFile(file);
        }
 
        var files = value as IList<IFormFile>;
        if (files == null)
        {
            return false;
        }
 
        foreach (var postedFile in files)
        {
            if (!isValidFile(postedFile)) return false;
        }
 
        return true;
    }
 
    private bool isValidFile(IFormFile file)
    {
        if (file == null || file.Length == 0)
        {
            return false;
        }
 
        var fileExtension = Path.GetExtension(file.FileName);
        return !string.IsNullOrWhiteSpace(fileExtension) &&
               _allowedExtensions.Any(ext => fileExtension.Equals(ext, StringComparison.OrdinalIgnoreCase));
    }
}
در اینجا با ارث بری از کلاس پایه ValidationAttribute و بازنویسی متد IsValid آن، کار اعتبارسنجی پسوند فایل‌ها و یا فایل رسیده را انجام داده‌ایم. این ویژگی جدید اگر بر روی خاصیتی از نوع IFormFile قرار بگیرد، پارامتر object value متد IsValid آن حاوی اطلاعات فایل و یا فایل‌های رسیده، خواهد بود. بر این اساس می‌توان تصمیم گیری کرد که آیا پسوند این فایل، مجاز است یا خیر.
public class UserViewModel
{
    [Required(ErrorMessage = "Please select a file.")]
    //`FileExtensions` needs to be applied to a string property. It doesn't work on IFormFile properties, and definitely not on IEnumerable<IFormFile> properties.
    //[FileExtensions(Extensions = ".png,.jpg,.jpeg,.gif", ErrorMessage = "Please upload an image file.")]
    [UploadFileExtensions(".png,.jpg,.jpeg,.gif", ErrorMessage = "Please upload an image file.")]
    [DataType(DataType.Upload)]
    public IFormFile Photo { get; set; }
}
در اینجا روش استفاده‌ی از این ویژگی اعتبارسنجی جدید را نیز با تکمیل ViewModel کاربر، مشاهده می‌کنید. پس از آن تنها بررسی if (ModelState.IsValid) در یک اکشن متد، نتیجه‌ی دریافتی از اعتبارسنج جدید UploadFileExtensions را در اختیار ما قرار می‌دهد و بر این اساس می‌توان تصمیم‌گیری کرد که آیا باید فایل رسیده را ذخیره کرد یا خیر.
مطالب
C# 7 - Ref Returns and Ref Locals
C# 7 به همراه تغییرات قابل توجهی در مورد نحوه‌ی دریافت خروجی از متدها است که نمونه‌هایی از آن‌ها را مانند tuples و out variable، پیشتر بررسی کردیم. در ادامه تغییرات جدید دیگری را به نام ref locals و ref returns نیز بررسی خواهیم کرد و هدف از آن، کاهش تعداد بار کپی کردن مقادیر و یا اعمال dereferencing جهت بالا بردن کارآیی برنامه هستند.


انتقال توسط مقدار

اگر پارامتری به صورت value type تعریف شود، این مقدار درون متد، ایجاد شده و حافظه‌ای به آن اختصاص داده می‌شود و سپس در انتهای متد تخریب خواهد شد. بنابراین تغییری در مقدار آن، سبب انعکاس آن به فراخوان متد، نخواهد شد.
static void PassByValueSample()
{
    int a = 1;
    PassByValue(a);
    Console.WriteLine($"after the invocation of {nameof(PassByValue)}, {nameof(a)} = {a}");
}
 
static void PassByValue(int x)
{
    x = 2;
}
در این مثال متد PassByValue تنها یک کپی از مقدار متغیر a را دریافت می‌کند. بنابراین هر تغییری که درون متد PassByValue بر روی این مقدار دریافتی رخ دهد، به فراخوان آن منتقل نخواهد شد.


انتقال توسط ارجاع

برای بازگشت مقدار تغییر داده شده‌ی توسط یک متد، می‌توان یک نوع خروجی را برای آن تعریف کرد. راه دیگر آن تعریف یک پارامتر توسط واژه‌ی کلیدی ref است. پارامتری که به این روش تعریف شود، هم ارسال کننده‌ی مقدار و هم دریافت کننده‌ی تغییرات خواهد بود.
static void PassByReferenceSample()
{
    int a = 1;
    PassByReference(ref a);
    Console.WriteLine($"after the invocation of {nameof(PassByReference)}, {nameof(a)} = {a}");
}
 
static void PassByReference(ref int x)
{
    x = 2;
}
در این مثال متغیر x به مقدار متغیر a اشاره می‌کند. بنابراین هر تغییری که بر روی آن صورت گیرد، به متغیر a هم اعمال و منعکس خواهد شد.


متغیرهای out

با استفاده از واژه‌ی کلیدی ref، می‌توان یک مقدار را هم به تابع ارسال کرد و هم از آن دریافت نمود. اما اگر تنها قرار است مقداری از تابع بازگشت داده شود، می‌توان از متغیرهای out استفاده کرد.
static void OutSample()
{
    Out(out int a);
    Console.WriteLine($"after the invocation of {nameof(Out)}, {nameof(a)} = {a}");
}
 
static void Out(out int x)
{
    x = 2;
}
در حالت استفاده‌ی از واژه‌ی کلیدی out، نیازی به مقدار دهی اولیه‌ی متغیر ارسالی به متد نیست و در اینجا روش جدید تعریف متغیرهای out را در C# 7 نیز مشاهده می‌کنید که نیازی نیست تا ابتدا int a را تعریف کرد و سپس در متد Out آن‌را فراخوانی نمود. می‌توان کل عملیات را به صورت خلاصه در یک سطر ذکر کرد.
البته اگر تنها قرار است یک مقدار را از یک متد دریافت کنیم، بهتر است نوع خروجی متد را مشخص کرد و از آن استفاده نمود؛ بجای استفاده‌ی از متغیرهای out. یک نمونه کاربرد مفید متغیرهای out در خود فریم ورک و توسط متد TryParse وجود دارد:
if (int.TryParse("42", out var result))
{
   Console.WriteLine($"the result is {result}");
}


Ref Locals در C# 7

در C# 7، امکان تعریف متغیرهای محلی از نوع ref نیز وجود دارد:
 int x = 3;
ref int x1 = ref x;
x1 = 2;
Console.WriteLine($"local variable {nameof(x)} after the change: {x}");
در اینجا متغیر x1 دارای ارجاعی است به متغیر x. بنابراین تغییر مقدار x1، به متغیر x نیز منعکس خواهد شد.

باید دقت داشت که نمی‌توان یک مقدار را به یک ref variable نسبت داد:
 ref int i = sequence.Count();
به این ترتیب امکان اشتباه گرفتن بین value variables و reference variables توسط کامپایلر گوشزد خواهد شد:
 ref int number1 = null; // ERROR
ref int number2 = 42; // ERROR


Ref Returns در C# 7

در C# 7، واژه‌ی کلیدی ref را به همراه نوع خروجی نیز می‌توان بکار برد:
 static ref int ReturnByReference()
{
    int[] arr = { 1 };
    ref int x = ref arr[0];
    return ref x;
}
در این مثال ارجاعی به اولین عضو آرایه‌ی تعریف شده، به عنوان خروجی متد بازگشت داده می‌شود. همچنین می‌توان این کد را به صورت ساده‌تر ذیل نیز نوشت:
static ref int ReturnByReference2()
{
   int[] arr = { 1 };
   return ref arr[0];
}
باید دقت داشت که یک متغیر int محلی را نمی‌توان به صورت ref بازگشت داد. علت اینجا است که متغیر int یک value type است و در انتهای متد تخریب خواهد شد. بنابراین امکان بازگشت آن به صورت ref میسر نیست. اما آرایه‌ها reference type بوده و بر روی heap تشکیل می‌شوند. در این حالت متغیر int داخل آرایه را می‌توان به صورت ref بازگشت داد.

هرچند امکان بازگشت یک متغیر محلی int به صورت ref وجود ندارد، اما اگر این متغیر به صورت ref به تابع ارسال شده باشد، این امکان میسر است:
static ref int ReturnByReference3(ref int x)
{
    x = 2;
    return ref x;
}

چند نکته: امکان تعریف فیلد ref و یا خاصیت get;set دار از نوع ref وجود ندارد. اما تعریف خواصی که یک ref را بازگشت می‌دهند، میسر هستند:
class Thing1
{
    ref string _Text1; /* Error */
 
    ref string Text2 { get; set; } /* Error */
 
 
    string _text = "Text";
    ref string Text3 { get { return ref _text; } } // Properties that return a reference are allowed
}


مزیت کار با ref returns

ref return‌ها شاید آنچنان در کدهای روزمره‌ی #C بکارگرفته نشوند، اما نیاز به کدهای unsafe و کار مستقیم با pointers را به حداقل می‌رسانند و به آن می‌توان لقب safe pointer را اطلاق کرد؛ از این جهت که این کد هنوز هم یک managed code است و نه یک unsafe code.
private MyBigStruct[] array = new MyBigStruct[10];
private int current;
 
public ref MyBigStruct GetCurrentItem()
{
   return ref array[current];
}
مهم‌ترین مزیت این قابلیت جدید را در قطعه کد فوق ملاحظه می‌کنید. در طراحی بازی‌ها عموما استفاده‌ی از آرایه‌های بزرگ از پیش تخصیص داده شده‌ی structها بسیار مرسوم است (چون میزان مصرف حافظه‌ی کمتری را نسبت به نوع‌های ارجاعی دارند و فشار کمتری را به GC وارد می‌کنند). اکنون با معرفی این قابلیت، دیگر نیازی نیست تا مدام آرایه‌های بزرگی از structها را از قسمتی به قسمت دیگر کپی کرد و سپس بر روی عناصری از آن‌ها عملیاتی را انجام داد و مجددا این حاصل را به مکان مدنظر کپی کرد. در اینجا بدون کپی کردن value types می‌توان با ایجاد ارجاعی به آن‌ها، تغییرات مدنظر را به آن‌ها اعمال کرد.
مطالب
استخراج متن از فایل‌های PDF توسط iTextSharp
پیشنیاز
نحوه ذخیره شدن متن در فایل‌های PDF

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

دو کلاس متفاوت برای استخراج متن از فایل‌های PDF در iTextSharp وجود دارند:
الف) SimpleTextExtractionStrategy

using System.Diagnostics;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;
using iTextSharp.text.pdf.parser;

namespace TestReaders
{
    class Program
    {
        private static void readPdf1()
        {
            var reader = new PdfReader("test.pdf");
            int intPageNum = reader.NumberOfPages;
            for (int i = 1; i <= intPageNum; i++)
            {
               var text = PdfTextExtractor.GetTextFromPage(reader, i, new SimpleTextExtractionStrategy());
                File.WriteAllText("page-" + i + "-text.txt", text);
            }
            reader.Close();
        }

        static void Main(string[] args)
        {
            readPdf1();
        }
    }
}
مثال فوق، متن موجود در تمام صفحات یک فایل PDF را در فایل‌های txt جداگانه‌ای ثبت می‌کند. برای نمونه اگر از PDF پیشنیاز یاد شده استفاده کنیم، خروجی آن به نحو زیر خواهد بود:
 Test
ld Wor llo He
Hello People
علت آن نیز پیشتر بررسی گردید. متن، در این فایل ویژه در مختصات خاصی ترسیم شده است. حاصل از دیدگاه خواننده نهایی بسیار خوانا است؛ اما خروجی hello world متنی جالبی از آن استخراج نمی‌شود. SimpleTextExtractionStrategy دقیقا بر اساس همان عملگر‌های Tj و همچنین منابع صفحه، عبارات را یافته و سر هم می‌کند.


ب) LocationTextExtractionStrategy

همان مثال قبل را درنظر بگیرید، اینبار به شکل زیر:
        private static void readPdf2()
        {
            var reader = new PdfReader("test.pdf");
            int intPageNum = reader.NumberOfPages;
            for (int i = 1; i <= intPageNum; i++)
            {
                var text = PdfTextExtractor.GetTextFromPage(reader, i, new LocationTextExtractionStrategy());
                File.WriteAllText("page-" + i + "-text.txt", text);
            }
            reader.Close();
        }
کلاس LocationTextExtractionStrategy هوشمند‌تر عمل کرده و بر اساس عملگرهای هندسی یک فایل PDF، سعی می‌کند جملات و حروف را کنار هم قرار دهد و در نهایت خروجی متنی بهتری را تولید کند. برای نمونه اینبار خروجی متنی حاصل به صورت زیر خواهد بود:
 Test
Hello World
Hello People
این خروجی با آنچه که در صفحه نمایش داده می‌شود تطابق دارد.


استخراج متون فارسی از فایل‌های PDF توسط iTextSharp

روش‌های فوق با PDFهای فارسی هم کار می‌کنند اما خروجی حاصل آن مفهوم نیست و نیاز به پردازش ثانوی دارد. ابتدا مثال زیر را درنظر بگیرید:
        static void writePdf2()
        {
            using (var document = new Document(PageSize.A4))
            {
                var writer = PdfWriter.GetInstance(document, new FileStream("test.pdf", FileMode.Create));
                document.Open();

                FontFactory.Register("c:\\windows\\fonts\\tahoma.ttf");
                var tahoma = FontFactory.GetFont("tahoma", BaseFont.IDENTITY_H);

                ColumnText.ShowTextAligned(
                            canvas: writer.DirectContent,
                            alignment: Element.ALIGN_CENTER,
                            phrase: new Phrase("تست می‌شود", tahoma),
                            x: 100,
                            y: 100,
                            rotation: 0,
                            runDirection: PdfWriter.RUN_DIRECTION_RTL,
                            arabicOptions: 0);                
            }

            Process.Start("test.pdf");
        }
از متد فوق، برای تولید یک فایل PDF که متنی فارسی را نمایش می‌دهد استفاده خواهیم کرد. اگر متد readPdf2 را که به همراه LocationTextExtractionStrategy تعریف شده است، بر روی فایل حاصل فراخوانی کنیم، خروجی آن به صورت زیر خواهد بود:
ﺩﻮﺷﻲﻣ ﺖﺴﺗ
برای تبدیل آن به یونیکد خواهیم داشت:
        private static void readPdf2()
        {
            var reader = new PdfReader("test.pdf");
            int intPageNum = reader.NumberOfPages;
            for (int i = 1; i <= intPageNum; i++)
            {
                var text = PdfTextExtractor.GetTextFromPage(reader, i, new LocationTextExtractionStrategy());                
                text = Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(text));
                File.WriteAllText("page-" + i + "-text.txt", text, Encoding.UTF8);
            }
            reader.Close();
        }
اکنون خروجی ثبت شده در فایل متنی حاصل به صورت زیر است:
 ﺩﻮﺷﻲﻣ ﺖﺴﺗ
دقیقا به همان نحوی است که iTextSharp و اکثر تولید کننده‌های PDF فارسی از آن استفاده می‌کنند و اصطلاحا چرخاندن حروف یا تولید Glyph mirrors صورت می‌گیرد. روش‌های زیادی برای چرخاندن حروف وجود دارند. در ادامه از روشی استفاده خواهیم کرد که خود ویندوز در کارهای داخلی‌اش از آن استفاده می‌کند:
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security;

namespace TestReaders
{
    [SuppressUnmanagedCodeSecurity]
    class GdiMethods
    {
        [DllImport("GDI32.dll")]
        public static extern bool DeleteObject(IntPtr hgdiobj);

        [DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern uint GetCharacterPlacement(IntPtr hdc, string lpString, int nCount, int nMaxExtent, [In, Out] ref GcpResults lpResults, uint dwFlags);

        [DllImport("GDI32.dll")]
        public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
    }

    [StructLayout(LayoutKind.Sequential)]
    struct GcpResults
    {
        public uint lStructSize;
        [MarshalAs(UnmanagedType.LPTStr)]
        public string lpOutString;
        public IntPtr lpOrder;
        public IntPtr lpDx;
        public IntPtr lpCaretPos;
        public IntPtr lpClass;
        public IntPtr lpGlyphs;
        public uint nGlyphs;
        public int nMaxFit;
    }

    public class UnicodeCharacterPlacement
    {
        const int GcpReorder = 0x0002;
        GCHandle _caretPosHandle;
        GCHandle _classHandle;
        GCHandle _dxHandle;
        GCHandle _glyphsHandle;
        GCHandle _orderHandle;

        public Font Font { set; get; }

        public string Apply(string lines)
        {
            if (string.IsNullOrWhiteSpace(lines))
                return string.Empty;

            return Apply(lines.Split('\n')).Aggregate((s1, s2) => s1 + s2);
        }

        public IEnumerable<string> Apply(IEnumerable<string> lines)
        {
            if (Font == null)
                throw new ArgumentNullException("Font is null.");

            if (!hasUnicodeText(lines))
                return lines;

            var graphics = Graphics.FromHwnd(IntPtr.Zero);
            var hdc = graphics.GetHdc();
            try
            {
                var font = (Font)Font.Clone();
                var hFont = font.ToHfont();
                var fontObject = GdiMethods.SelectObject(hdc, hFont);
                try
                {
                    var results = new List<string>();
                    foreach (var line in lines)
                        results.Add(modifyCharactersPlacement(line, hdc));
                    return results;
                }
                finally
                {
                    GdiMethods.DeleteObject(fontObject);
                    GdiMethods.DeleteObject(hFont);
                    font.Dispose();
                }
            }
            finally
            {
                graphics.ReleaseHdc(hdc);
                graphics.Dispose();
            }
        }

        void freeResources()
        {
            _orderHandle.Free();
            _dxHandle.Free();
            _caretPosHandle.Free();
            _classHandle.Free();
            _glyphsHandle.Free();
        }

        static bool hasUnicodeText(IEnumerable<string> lines)
        {
            return lines.Any(line => line.Any(chr => chr >= '\u00FF'));
        }

        void initializeResources(int textLength)
        {
            _orderHandle = GCHandle.Alloc(new int[textLength], GCHandleType.Pinned);
            _dxHandle = GCHandle.Alloc(new int[textLength], GCHandleType.Pinned);
            _caretPosHandle = GCHandle.Alloc(new int[textLength], GCHandleType.Pinned);
            _classHandle = GCHandle.Alloc(new byte[textLength], GCHandleType.Pinned);
            _glyphsHandle = GCHandle.Alloc(new short[textLength], GCHandleType.Pinned);
        }

        string modifyCharactersPlacement(string text, IntPtr hdc)
        {
            var textLength = text.Length;
            initializeResources(textLength);
            try
            {
                var gcpResult = new GcpResults
                {
                    lStructSize = (uint)Marshal.SizeOf(typeof(GcpResults)),
                    lpOutString = new String('\0', textLength),
                    lpOrder = _orderHandle.AddrOfPinnedObject(),
                    lpDx = _dxHandle.AddrOfPinnedObject(),
                    lpCaretPos = _caretPosHandle.AddrOfPinnedObject(),
                    lpClass = _classHandle.AddrOfPinnedObject(),
                    lpGlyphs = _glyphsHandle.AddrOfPinnedObject(),
                    nGlyphs = (uint)textLength,
                    nMaxFit = 0
                };
                var result = GdiMethods.GetCharacterPlacement(hdc, text, textLength, 0, ref gcpResult, GcpReorder);
                return result != 0 ? gcpResult.lpOutString : text;
            }
            finally
            {
                freeResources();
            }
        }
    }
}
از کلاس فوق در هر برنامه‌ای که راست به چپ را به نحو صحیحی پشتیبانی نمی‌کند، می‌توان استفاده کرد؛ خصوصا برنامه‌های گرافیکی.
در اینجا برای اصلاح متد readPdf2 خواهیم داشت:
        private static void readPdf2()
        {
            var reader = new PdfReader("test.pdf");
            int intPageNum = reader.NumberOfPages;
            for (int i = 1; i <= intPageNum; i++)
            {
                var text = PdfTextExtractor.GetTextFromPage(reader, i, new LocationTextExtractionStrategy());
                text = Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(text));
                text = new UnicodeCharacterPlacement
                {
                    Font = new System.Drawing.Font("Tahoma", 12)
                }.Apply(text);
                File.WriteAllText("page-" + i + "-text.txt", text, Encoding.UTF8);
            }
            reader.Close();
        }
اگر خروجی متد اصلاح شده فوق را بررسی کنیم، دقیقا به «تست می‌شود» خواهیم رسید.

سؤال: آیا این روش با تمام PDFهای فارسی کار می‌کند؟
پاسخ: خیر! همانطور که در پیشنیاز مطلب جاری عنوان شد، در یک حالت خاص، PDF writer می‌تواند شماره Glyphها را کاملا عوض کرده و در فایل PDF نهایی ثبت کند. خروجی حاصل در برنامه Adobe reader خوانا است، چون نمایش را بر اساس اطلاعات هندسی Glyphها انجام می‌دهد؛ اما خروجی متنی آن به نوعی obfuscated است چون مثلا حرف A آن به کاراکتر مرسوم دیگری نگاشت شده است.