مطالب
روش استفاده‌ی از jQuery در برنامه‌های AngularJS 2.0
استفاده‌ی گسترده‌ی از jQuery به همراه برنامه‌های AngularJS 2.0 ایده‌ی خوبی نیست؛ زیرا jQuery و کدهای آن، ارتباط تنگاتنگی را بین DOM و JavaScript برقرار می‌کنند که برخلاف رویه‌ی فریم‌ورک‌های MVC است. اما با این حال استفاده‌ی از افزونه‌های بی‌شمار jQuery در برنامه‌های AngularJS 2.0، جهت غنا بخشیدن به ظاهر و همچنین کاربردپذیری برنامه، یکی از استفاده‌های پذیرفته شده‌ی آن است.
روش استفاده‌ی از jQuery نیز در حالت کلی همانند مطلب «استفاده از کتابخانه‌های ثالث جاوا اسکریپتی در برنامه‌های AngularJS 2.0» است؛ اما به همراه چند نکته‌ی اضافه‌تر مانند محل فراخوانی و دسترسی به DOM، در کدهای یک کامپوننت.


هدف: استفاده از کتابخانه‌ی Chosen

می‌خواهیم جهت غنی‌تر کردن ظاهر یک دراپ داون در برنامه‌های AngularJS 2.0، از یک افزونه‌ی بسیار معروف jQuery به نام Chosen استفاده کنیم.


تامین پیشنیازهای اولیه

می‌توان فایل‌های این کتابخانه را مستقیما از GitHub دریافت و به پروژه اضافه کرد. اما بهتر است این‌کار را توسط bower مدیریت کنیم. این کتابخانه هنوز دارای بسته‌ی رسمی npm نیست (و بسته‌ی chosen-npm که در مخزن npm وجود دارد، توسط این تیم ایجاد نشده‌است). اما همانطور که در مستندات آن نیز آمده‌است، توسط دستور ذیل نصب می‌شود:
bower install chosen
برای مدیریت این مساله در ویژوال استودیو، به منوی project و گزینه‌ی add new item مراجعه کرده و bower را جستجو کنید:


در اینجا نام پیش فرض bower.json را پذیرفته و سپس محتوای فایل ایجاد شده را به نحو ذیل تغییر دهید:
{
    "name": "asp-net-mvc5x-angular2x",
    "version": "1.0.0",
    "authors": [
        "DNT"
    ],
    "license": "MIT",
    "ignore": [
        "node_modules",
        "bower_components"
    ],
    "dependencies": {
        "chosen": "1.4.2"
    },
    "devDependencies": {
    }
}
ساختار این فایل هم بسیار شبیه‌است به ساختار package.json مربوط به npm. در اینجا کتابخانه‌ی chosen در قسمت لیست وابستگی‌ها اضافه شده‌است. با ذخیره کردن این فایل، کار دریافت خودکار بسته‌های تعریف شده، آغاز می‌شود و یا کلیک راست بر روی فایل bower.json دارای گزینه‌ی restore packages نیز هست.
پس از دریافت خودکار chosen، بسته‌ی آن‌را در مسیر bower_components\chosen واقع در ریشه‌ی پروژه می‌توانید مشاهده کنید.


استفاده از jQuery و chosen به صورت untyped

ساده‌ترین و متداول‌ترین روش استفاده از jQuery و افزونه‌های آن شامل موارد زیر هستند:
الف) تعریف مداخل مرتبط با آن‌ها در فایل index.html
<script src="~/node_modules/jquery/dist/jquery.min.js"></script>
<script src="~/node_modules/bootstrap/dist/js/bootstrap.min.js"></script>

<script src="~/bower_components/chosen/chosen.jquery.min.js"></script>
<link href="~/bower_components/chosen/chosen.min.css" rel="stylesheet" type="text/css" />
که در اینجا jQuery و بوت استرپ و chosen به همراه فایل css آن اضافه شده‌اند.
ب) تعریف jQuery به صورت untyped
declare var jQuery: any;
همینقدر که این یک سطر را به ابتدای ماژول خود اضافه کنید، امکان دسترسی به تمام امکانات جی‌کوئری را خواهید یافت. البته بدون intellisense و بررسی نوع‌های پارامترها؛ چون نوع آن، any یا همان حالت پیش فرض جاوا اسکریپت تعریف شده‌است.


محل صحیح فراخوانی متدهای مرتبط با jQuery

در تصویر ذیل، چرخه‌ی حیات یک کامپوننت را مشاهده می‌کنید که با تعدادی از آن‌ها پیشتر آشنا شده‌‌ایم:

روش متداول استفاده از jQuery، فراخوانی آن پس از رخداد document ready است. در اینجا معادل این رخداد، hook ویژ‌ه‌ای به نام ngAfterViewInit است. بنابراین تمام فراخوانی‌های jQuery را باید در این متد انجام داد.
همچنین جی‌کوئری نیاز دارد تا بداند هم اکنون قرار است با چه المانی کار کنیم و کامپوننت بارگذاری شده کدام است. برای این منظور، یکی از سرویس‌های توکار AngularJS 2.0 را به نام ElementRef، به سازنده‌ی کلاس تزریق می‌کنیم. توسط خاصیت this._el.nativeElement آن می‌توان به المان ریشه‌ی کامپوننت جاری دسترسی یافت.
constructor(private _el: ElementRef) {
}


تهیه‌ی کامپوننت نمایش یک دراپ داون مزین شده با chosen

در ادامه قصد داریم نکاتی را که تاکنون مرور کردیم، به صورت یک مثال پیاده سازی کنیم. به همین جهت فایل جدید using-jquery-addons.component.ts را به پروژه اضافه کنید به همراه فایل قالب آن به نام using-jquery-addons.component.html؛ با این محتوا:
الف) کامپوننت using-jquery-addons.component.ts
import { Component, ElementRef, AfterViewInit } from "@angular/core";

declare var jQuery: any; // untyped

@Component({
    templateUrl: "app/using-jquery-addons/using-jquery-addons.component.html"
})
export class UsingJQueryAddonsComponent implements AfterViewInit {

    dropDownItems = ["First", "Second", "Third"];
    selectedValue = "Second";

    constructor(private _el: ElementRef) {
    }

    ngAfterViewInit() {
        jQuery(this._el.nativeElement)
            .find("select")
            .chosen()
            .on("change", (e, args) => {
                this.selectedValue = args.selected;
            });
    }
}

ب) قالب using-jquery-addons.component.html
<select>
    <option *ngFor="let item of dropDownItems"
            [value]="item"
            [selected]="item == selectedValue">
        {{item}} option
    </option>
</select>

<h4> {{selectedValue}}</h4>
با این خروجی


توضیحات

کلاس UsingJQueryAddonsComponent، اینترفیس AfterViewInit را پیاده سازی کرده‌است؛ تا توسط متد ngAfterViewInit بتوانیم با عناصر DOM کار کنیم. هرچند در کل اینکار باید صرفا محدود شود به مواردی مانند مثال جاری و در حد آغاز یک افزونه‌ی jQuery و اگر قرار است تغییراتی دیگری صورت گیرند بهتر است از همان روش binding توکار AngularJS 2.0 استفاده کرد.
در سازنده‌ی کلاس، سرویس ElementRef تزریق شده‌است تا توسط خاصیت this._el.nativeElement آن بتوان به المان ریشه‌ی کامپوننت جاری دسترسی یافت. به همین جهت است که پس از آن از متد find، برای یافتن دراپ داون استفاده شده‌است و سپس chosen به نحو متداولی به آن اعمال گردیده‌است.
در اینجا هر زمانیکه یکی از آیتم‌های دراپ داون انتخاب شوند، مقدار آن به خاصیت selectedValue انتساب داده شده و این انتساب سبب فعال سازی binding و نمایش مقدار آن در ذیل دراپ داون می‌گردد.
در قالب این کامپوننت هم با استفاده از ngFor، عناصر دراپ داون از آرایه‌ی dropDownItems تعریف شده در کلاس کامپوننت، تامین می‌شوند. متغیر محلی item تعریف شده‌ی در اینجا، در محدودی همین المان قابل دسترسی است. برای مثال از آن جهت تنظیم دومین آیتم لیست به صورت انتخاب شده، در حین اولین بار نمایش view استفاده شده‌است.


استفاده از jQuery و chosen به صورت typed

کتابخانه‌ی jQuery در مخزن کد https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/jquery دارای فایل d.ts. خاص خود است. برای نصب آن می‌توان از روش ذیل استفاده کرد:
npm install -g typings
typings install jquery --save --ambient
پس از افزودن فایل typings این کتابخانه، در ابتدای فایل main.ts یک سطر ذیل را اضافه کنید:
 /// <reference path="../typings/browser/ambient/jquery/index.d.ts" />
اینکار سبب خواهد شد تا intellisense درون ویژوال استودیو بتواند مداخل متناظر را یافته و راهنمای مناسبی را ارائه دهد.

در ادامه به فایل using-jquery-addons.component.ts مراجعه کرده و تغییر ذیل را اعمال کنید:
// declare var jQuery: any; // untyped
declare var jQuery: JQueryStatic; // typed
فایل d.ts. اضافه شده، امکان استفاده‌ی از نوع جدید JQueryStatic را مهیا می‌کند. پس از آن در کدهای تعریف شده، chosen به رنگ قرمز در می‌آید، چون در تعاریف نوع دار jQuery، این افزونه وجود خارجی ندارد. برای رفع این مشکل، فایل typings/browser/ambient/jquery/index.d.ts را گشوده و سپس به انتهای آن، تعریف chosen را به صورت دستی اضافه کنید:
interface JQuery {
   //...
   chosen(options?:any):JQuery;
}
declare module "jquery" {
   export = $;
}
به این ترتیب این متد به عنوان جزئی از متدهای زنجیروار jQuery، شناسایی شده و قابل استفاده خواهد شد.


کدهای کامل این پروژه را از اینجا می‌توانید دریافت کنید.
نظرات مطالب
بررسی روش آپلود فایل‌ها در ASP.NET Core
چند نکته‌ی تکمیلی
چگونه فایل دریافتی را تبدیل به آرایه‌ای از بایت‌ها کنیم؟
public async Task<IActionResult> FileUpload(IFormFile file)
        {
            if (file == null || file.Length == 0)
            {
                return BadRequest();
            }

            using (var memoryStream = new MemoryStream())
            {
                await file.CopyToAsync(memoryStream);
                var fileBytes = memoryStream.ToArray();
                // ... save it
            }
        }

چگونه فایل تصویر دریافتی را تبدیل به یک شیء تصویر کنیم؟
public async Task<IActionResult> FileUpload(IFormFile file)
        {
            if (file == null || file.Length == 0)
            {
                return BadRequest();
            }

            using (var memoryStream = new MemoryStream())
            {
                await file.CopyToAsync(memoryStream);
                using (var img = Image.FromStream(memoryStream))
                {
                  // TODO: ResizeImage(img, 100, 100);
                }
            }
        }
مطالب
چگونه از دو نگارش مختلف یک اسمبلی در برنامه‌های NET Core. استفاده کنیم؟
فرض کنید اسمبلی A که System.Drawing.Common، نام دارد، فضای نام System.Drawing.Common را ارائه می‌دهد و اسمبلی B که CoreCompact.System.Drawing نام دارد، باز هم دقیقا همان فضای نام را ارائه می‌دهد. سؤال: آیا می‌توان از این دو اسمبلی مختلف در برنامه‌ی خود استفاده کرد؟ یا مثال دیگر آن داشتن دو اسمبلی با نگارش‌های مختلف، از یک کتابخانه است. برای مثال یکی بر اساس netcoreapp2.1 تهیه شده‌است و دیگری بر اساس netstandard2.0 و به هر دلیلی نیاز است که بتوان از هر دو اسمبلی در برنامه‌ی خود استفاده کرد. برای یک چنین مواردی در زبان #C ویژگی به نام extern alias وجود دارد که با مثالی نحوه‌ی کار با آن‌را بررسی خواهیم کرد.


ایجاد پروژه‌ی Globe1

پروژه‌ی کتابخانه‌ی Globe1 را بر اساس netcoreapp2.1 به این صورت توسط فایل‌های زیر ایجاد می‌کنیم:

الف) فایل Globe1.csproj
<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <TargetFramework>netcoreapp2.1</TargetFramework>
    </PropertyGroup>
</Project>

ب) فایل Globe.cs
using System;

namespace Space
{
    public class Globe
    {
        public string GetColor() => "Blue";
    }
}


ایجاد پروژه‌ی Globe2

پروژه‌ی کتابخانه‌ی Globe2 را بر اساس netstandard2.0 به این صورت توسط فایل‌های زیر ایجاد می‌کنیم:

الف) فایل Globe2.csproj
<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>
    </PropertyGroup>
</Project>

ب) فایل Globe.cs
using System;

namespace Space
{
    public class Globe
    {
        public string GetColor() => "Green";
    }
}


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

همانطور که ملاحظه می‌کنید اگر این دو پروژه‌ی class library را کامپایل کنیم، به دو فایل dll یا اسمبلی خواهیم رسید که هر دو دارای فضای نام Space و همچنین کلاس Globe هستند. اما نگارش‌های آن‌ها و یا TargetFrameworkهای آن‌ها متفاوت است. اکنون می‌خواهیم از هر دوی این‌ها در یک پروژه‌ی Console استفاده کنیم. بنابراین ابتدا این پروژه را با ایجاد فایل csproj آن شروع می‌کنیم:

الف) فایل Owl.csproj
<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>netcoreapp2.1</TargetFramework>
    </PropertyGroup>
    <ItemGroup>
        <Reference Include="Globe1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
            <HintPath>..\Globe1\bin\$(Configuration)\netcoreapp2.1\Globe1.dll</HintPath>
            <Aliases>Lib1</Aliases>
        </Reference>

        <Reference Include="Globe2, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
            <HintPath>..\Globe2\bin\$(Configuration)\netstandard2.0\Globe2.dll</HintPath>
            <Aliases>Lib2</Aliases>
        </Reference>
    </ItemGroup>
</Project>
روش ارجاع دهی به این اسمبلی‌ها، اندکی متفاوت است. در اینجا نام هر کدام به همراه شماره نگارش آن‌ها ذکر شده‌است. همچنین مسیر یافتن فایل‌های Globe1.dll و Globe2.dll نیز ذکر شده‌اند؛ بجای اینکه ارجاعی را به فایل‌های csproj این class libraryها اضافه کنیم (از این جهت که ممکن است مانند مثال ابتدای بحث، صرفا دو فایل dll را بیشتر در اختیار نداشته باشیم). به علاوه یک Alias نیز برای هر کدام تعریف شده‌است تا توسط آن بتوان به امکانات هرکدام دسترسی یافت. وجود این Alias از این جهت ضروری است که هرچند دو فایل dll مختلف را داریم، اما فضاهای نام و کلاس‌های آن‌ها، نام‌های مشابهی دارند و قابل تمیز از یکدیگر نیستند.

ب) فایل Program.cs

اکنون قسمت مهم این فایل، همان extern aliasهایی هستند که پیشتر تعریف کردیم:
extern alias Lib1;
extern alias Lib2;
using System;
using SpaceOne = Lib1::Space;
using SpaceTwo = Lib2::Space;

namespace Owl
{
    class Program
    {
        static void Main(string[] args)
        {
            var owl = new SuperOwl();
            owl.IntegrateGlobe(new SpaceOne.Globe());
            owl.IntegrateGlobe(new SpaceTwo.Globe());

            Console.WriteLine(owl.GetGLobeColors());
        }
    }

    public class SuperOwl
    {
        private SpaceOne.Globe _firstGlobe;
        private SpaceTwo.Globe _secondGlobe;

        public void IntegrateGlobe(SpaceOne.Globe globe) => _firstGlobe = globe;
        public void IntegrateGlobe(SpaceTwo.Globe globe) => _secondGlobe = globe;
        public string GetGLobeColors() => $"First: {_firstGlobe.GetColor()}, Second: {_secondGlobe.GetColor()}";
    }
}
ابتدا extern aliasها تعریف می‌شوند:
extern alias Lib1;
extern alias Lib2;
 سپس بر اساس هر Alias می‌توان یک فضای نام جدید را بجای فضای نام Space فعلی هرکدام تعریف کرد:
using SpaceOne = Lib1::Space;
using SpaceTwo = Lib2::Space;
 تا بتوان به متدهای GetColor متفاوت این‌ها دسترسی یافت.

اگر این برنامه را اجرا کنیم، چنین خروجی حاصل می‌شود:
First: Blue, Second: Green

کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید: alias.zip
مطالب
استفاده از Web API در ASP.NET Web Forms
گرچه ASP.NET Web API بهمراه ASP.NET MVC بسته بندی شده و استفاده می‌شود، اما اضافه کردن آن به اپلیکیشن‌های ASP.NET Web Forms کار ساده ای است. در این مقاله مراحل لازم را بررسی می‌کنیم.

برای استفاده از Web API در یک اپلیکیشن ASP.NET Web Forms دو قدم اصلی باید برداشته شود:

  • اضافه کردن یک کنترلر Web API که از کلاس ApiController مشتق می‌شود.
  • اضافه کردن مسیرهای جدید به متد Application_Start.


یک پروژه Web Forms بسازید

ویژوال استودیو را اجرا کنید و پروژه جدیدی از نوع ASP.NET Web Forms Application ایجاد کنید.


کنترلر و مدل اپلیکیشن را ایجاد کنید

کلاس جدیدی با نام Product بسازید و خواص زیر را به آن اضافه کنید.

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public string Category { get; set; }
}
همانطور که مشاهده می‌کنید مدل مثال جاری نمایانگر یک محصول است. حال یک کنترلر Web API به پروژه اضافه کنید. کنترلر‌های Web API درخواست‌های HTTP را به اکشن متدها نگاشت می‌کنند. در پنجره Solution Explorer روی نام پروژه کلیک راست کنید و گزینه Add, New Item را انتخاب کنید.

در دیالوگ باز شده گزینه Web را از پانل سمت چپ کلیک کنید و نوع آیتم جدید را Web API Controller Class انتخاب نمایید. نام این کنترلر را به "ProductsController" تغییر دهید و OK کنید.

کنترلر ایجاد شده شامل یک سری متد است که بصورت خودکار برای شما اضافه شده اند، آنها را حذف کنید و کد زیر را به کنترلر خود اضافه کنید.

namespace WebForms
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net;
    using System.Net.Http;
    using System.Web.Http;

    public class ProductsController : ApiController
    {

        Product[] products = new Product[] 
        { 
            new Product { Id = 1, Name = "Tomato Soup", Category = "Groceries", Price = 1 }, 
            new Product { Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M }, 
            new Product { Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M } 
        };

        public IEnumerable<Product> GetAllProducts()
        {
            return products;
        }

        public Product GetProductById(int id)
        {
            var product = products.FirstOrDefault((p) => p.Id == id);
            if (product == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
            return product;
        }

        public IEnumerable<Product> GetProductsByCategory(string category)
        {
            return products.Where(
                (p) => string.Equals(p.Category, category,
                    StringComparison.OrdinalIgnoreCase));
        }
    }
}
کنترلر جاری لیستی از محصولات را بصورت استاتیک در حافظه محلی نگهداری می‌کند. متدهایی هم برای دریافت لیست محصولات تعریف شده اند.


اطلاعات مسیریابی را اضافه کنید

مرحله بعدی اضافه کردن اطلاعات مسیریابی (routing) است. در مثال جاری می‌خواهیم آدرس هایی مانند "api/products/" به کنترلر Web API نگاشت شوند. فایل Global.asax را باز کنید و عبارت زیر را به بالای آن اضافه نمایید.

using System.Web.Http;
حال کد زیر را به متد Application_Start اضافه کنید.
RouteTable.Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = System.Web.Http.RouteParameter.Optional }
    );

برای اطلاعات بیشتر درباره مسیریابی در Web API به این لینک مراجعه کنید.


دریافت اطلاعات بصورت آژاکسی در کلاینت

تا اینجا شما یک API دارید که کلاینت‌ها می‌توانند به آن دسترسی داشته باشند. حال یک صفحهHTML خواهیم ساخت که با استفاده از jQuery سرویس را فراخوانی می‌کند. صفحه Default.aspx را باز کنید و کدی که بصورت خودکار در قسمت Content تولید شده است را حذف کرده و کد زیر را به این قسمت اضافه کنید:

<%@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.Master" 
    AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="WebForms._Default" %>

<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
</asp:Content>

<asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">
    <h2>Products</h2>
    <table>
    <thead>
        <tr><th>Name</th><th>Price</th></tr>
    </thead>
    <tbody id="products">
    </tbody>
    </table>
</asp:Content>
حال در قسمت HeaderContent کتابخانه jQuery را ارجاع دهید.
<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
    <script src="Scripts/jquery-1.7.1.min.js" type="text/javascript"></script>
</asp:Content>

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

نکته: برای ارجاع دادن اسکریپت‌ها می‌توانید بسادگی فایل مورد نظر را با drag & drop به کد خود اضافه کنید.

زیر تگ jQuery اسکریپت زیر را اضافه کنید.

<script type="text/javascript">
    function getProducts() {
        $.getJSON("api/products",
            function (data) {
                $('#products').empty(); // Clear the table body.

                // Loop through the list of products.
                $.each(data, function (key, val) {
                    // Add a table row for the product.
                    var row = '<td>' + val.Name + '</td><td>' + val.Price + '</td>';
                    $('<tr/>', { text: row })  // Append the name.
                        .appendTo($('#products'));
                });
            });
        }

        $(document).ready(getProducts);
</script>

هنگامی که سند جاری (document) بارگذاری شد این اسکریپت یک درخواست آژاکسی به آدرس "api/products/" ارسال می‌کند. سرویس ما لیستی از محصولات را با فرمت JSON بر می‌گرداند، سپس این اسکریپت لیست دریافت شده را به جدول HTML اضافه می‌کند.

اگر اپلیکیشن را اجرا کنید باید با نمایی مانند تصویر زیر مواجه شوید:

مطالب
روش صحیح تعریف قلم در iTextSharp

روش متداول تعریف فونت در iTextSharp به صورت زیر است:

public static iTextSharp.text.Font Tahoma()

{
var fontPath = Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf";
var baseFont = BaseFont.CreateFont(fontPath, BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
return new Font(baseFont);
}

از آنجائیکه خصوصا برای متون فارسی نیاز است تا به ازای هر المان کوچکی این فونت تنظیم شود و‌ در غیر اینصورت متنی نمایش داده نخواهد شد، با سربار بالایی مواجه خواهیم شد. بنابراین به نظر می‌رسد که بهتر باشد این تولید اشیاء فونت را کش کنیم. خوشبختانه iTextSharp سیستم کش کردن تعریف قلم‌های متفاوت را هم به صورت توکار دارا است:

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);
}

کلاس FontFactory کار ثبت و بازیابی قلم‌های متفاوت را به عهده دارد. تنها کافی است یکبار قلمی در آن ثبت شود (FontFactory.Register)، بار دیگر اطلاعات قلم به سادگی از کش FontFactory خوانده خواهد شد (FontFactory.GetFont).

اشتراک‌ها
آینده‌ی IdentityServer
  • IdentityServer نگارش 4، آخرین نگارش سورس باز و رایگان آن است و تا زمان پشتیبانی NET Core 3.1. که سال 2022 است، پشتیبانی خواهد شد.
  • نگارش بعدی آن که Duende IdentityServer نام دارد، سورس باز است، اما رایگان نیست؛ چیزی شبیه به مجوز iTextSharp. برای کارهای تجاری باید مجوز آن خریده شود و برای کارهای کاملا سورس باز، رایگان است.
آینده‌ی IdentityServer
نظرات مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت اول - نیاز به تامین کننده‌ی هویت مرکزی
  • IdentityServer نگارش 4، آخرین نگارش سورس باز و رایگان آن است و تا زمان پشتیبانی NET Core 3.1. که سال 2022 است، پشتیبانی خواهد شد.
  • نگارش بعدی آن که Duende IdentityServer نام دارد، سورس باز است، اما رایگان نیست؛ چیزی شبیه به مجوز iTextSharp. برای کارهای تجاری باید مجوز آن خریده شود و برای کارهای کاملا سورس باز، رایگان است.
مطالب
بررسی مقدمات کتابخانه‌ی JSON.NET
چرا JSON.NET؟
JSON.NET یک کتابخانه‌ی سورس باز کار با اشیاء JSON در دات نت است. تاریخچه‌ی آن به 8 سال قبل بر می‌گردد و توسط یک برنامه نویس نیوزیلندی به نام James Newton King تهیه شده‌است. اولین نگارش آن در سال 2006 ارائه شد؛ مقارن با زمانی که اولین استاندارد JSON نیز ارائه گردید.
این کتابخانه از آن زمان تا کنون، 6 میلیون بار دانلود شده‌است و به علت کیفیت بالای آن، این روزها پایه اصلی بسیاری از کتابخانه‌ها و فریم ورک‌های دات نتی می‌باشد؛ مانند RavenDB تا ASP.NET Web API و SignalR مایکروسافت و همچنین گوگل نیز از آن جهت تدارک کلاینت‌های کار با API خود استفاده می‌کنند.
هرچند دات نت برای نمونه در نگارش سوم آن جهت مصارف WCF کلاسی را به نام DataContractJsonSerializer ارائه کرد، اما کار کردن با آن محدود است به فرمت خاص WCF به همراه عدم انعطاف پذیری و سادگی کار با آن. به علاوه باید درنظر داشت که JSON.NET از دات نت 2 به بعد تا مونو، Win8 و ویندوز فون را نیز پشتیبانی می‌کند.

برای نصب آن نیز کافی است دستور ذیل را در کنسول پاورشل نیوگت اجرا کنید:
 PM> install-package Newtonsoft.Json

معماری JSON.NET

کتابخانه‌ی JSON.NET از سه قسمت عمده تشکیل شده‌است:
الف) JsonSerializer
ب) LINQ to JSON
ج) JSON Schema


الف) JsonSerializer
کار JsonSerializer تبدیل اشیاء دات نتی به JSON و برعکس است. مزیت مهم آن امکانات قابل توجه تنظیم عملکرد و خروجی آن می‌باشد که این تنظیمات را به شکل ویژگی‌های خواص نیز می‌توان اعمال نمود. به علاوه امکان سفارشی سازی هر کدام نیز توسط کلاسی به نام JsonConverter، پیش بینی شده‌است.
یک مثال:
 var roles = new List<string>
{
   "Admin",
   "User"
};
string json = JsonConvert.SerializeObject(roles, Formatting.Indented);
در اینجا نحوه‌ی استفاده از JSON.NET را جهت تبدیل یک شیء دات نتی، به معادل JSON آن مشاهده می‌کنید. اعمال تنظیم Formatting.Indented سبب خواهد شد تا خروجی آن دارای Indentation باشد. برای نمونه اگر در برنامه‌ی خود قصد دارید فرمت JSON تو در تویی را به نحو زیبا و خوانایی نمایش دهید یا چاپ کنید، همین تنظیم ساده کافی خواهد بود.
و یا در مثال ذیل استفاده از یک anonymous object را مشاهده می‌کنید:
 var jsonString = JsonConvert.SerializeObject(new
{
   Id =1,
   Name = "Test"
}, Formatting.Indented);
به صورت پیش فرض تنها خواص عمومی کلاس‌ها توسط JSON.NET تبدیل خواهند شد.


تنظیمات پیشرفته‌تر JSON.NET

مزیت مهم JSON.NET بر سایر کتابخانه‌ها‌ی موجود مشابه، قابلیت‌های سفارشی سازی قابل توجه آن است. در مثال ذیل نحوه‌ی معرفی JsonSerializerSettings را مشاهده می‌نمائید:
var jsonData = JsonConvert.SerializeObject(new
{
   Id = 1,
   Name = "Test",
   DateTime = DateTime.Now
}, new JsonSerializerSettings
{
   Formatting = Formatting.Indented,
   Converters =
   {
      new JavaScriptDateTimeConverter()
   }
});
در اینجا با استفاده از تنظیم JavaScriptDateTimeConverter، می‌توان خروجی DateTime استانداردی را به مصرف کنندگان جاوا اسکریپتی سمت کاربر ارائه داد؛ با خروجی ذیل:
 {
  "Id": 1,
  "Name": "Test",
  "DateTime": new Date(1409821985245)
}


نوشتن خروجی JSON در یک استریم

خروجی متد JsonConvert.SerializeObject یک رشته‌است که در صورت نیاز به سادگی توسط متد File.WriteAllText در یک فایل قابل ذخیره می‌باشد. اما برای رسیدن به حداکثر کارآیی و سرعت می‌توان از استریم‌ها نیز استفاده کرد:
using (var stream = File.CreateText(@"c:\output.json"))
{
    var jsonSerializer = new JsonSerializer
   {
      Formatting = Formatting.Indented
   };
   jsonSerializer.Serialize(stream, new
   {
     Id = 1,
     Name = "Test",
     DateTime = DateTime.Now
   });
}
کلاس JsonSerializer و متد Serialize آن یک استریم را نیز جهت نوشتن خروجی می‌پذیرند. برای مثال response.Output برنامه‌های وب نیز یک استریم است و در اینجا نوشتن مستقیم در استریم بسیار سریعتر است از تبدیل شیء به رشته و سپس ارائه خروجی آن؛ زیرا سربار تهیه رشته JSON از آن حذف می‌گردد و نهایتا GC کار کمتری را باید انجام دهد.


تبدیل JSON رشته‌ای به اشیاء دات نت

اگر رشته‌ی jsonData ایی را که پیشتر تولید کردیم، بخواهیم تبدیل به نمونه‌ای از شیء User ذیل کنیم:
public class User
{
   public int Id { set; get; }
   public string Name { set; get; }
   public DateTime DateTime { set; get; }
}
خواهیم داشت:
 var user = JsonConvert.DeserializeObject<User>(jsonData);
در اینجا از متد DeserializeObject به همراه مشخص سازی صریح نوع شیء نهایی استفاده شده‌است.
البته در اینجا با توجه به استفاده از JavaScriptDateTimeConverter برای تولید jsonData، نیاز است چنین تنظیمی را نیز در حالت DeserializeObject مشخص کنیم:
var user = JsonConvert.DeserializeObject<User>(jsonData, new JsonSerializerSettings
{
   Converters = {  new JavaScriptDateTimeConverter() }
});


مقدار دهی یک نمونه یا وهله‌ی از پیش موجود

متد JsonConvert.DeserializeObject یک شیء جدید را ایجاد می‌کند. اگر قصد دارید صرفا تعدادی از خواص یک وهله‌ی موجود، توسط JSON.NET مقدار دهی شوند از متد PopulateObject استفاده کنید:
 JsonConvert.PopulateObject(jsonData, user);


کاهش حجم JSON تولیدی

زمانیکه از متد JsonConvert.SerializeObject استفاده می‌کنیم، تمام خواص عمومی تبدیل به معادل JSON آن‌ها خواهند شد؛ حتی خواصی که مقدار ندارند. این خواص در خروجی JSON، با مقدار null مشخص می‌شوند. برای حذف این خواص از خروجی JSON نهایی تنها کافی است در تنظیمات JsonSerializerSettings، مقدار NullValueHandling = NullValueHandling.Ignore مشخص گردد.
var jsonData = JsonConvert.SerializeObject(object, new JsonSerializerSettings
{
   NullValueHandling = NullValueHandling.Ignore,
   Formatting = Formatting.Indented
});
مورد دیگری که سبب کاهش حجم خروجی نهایی خواهد شد، تنظیم DefaultValueHandling = DefaultValueHandling.Ignore است. در این حالت کلیه خواصی که دارای مقدار پیش فرض خودشان هستند، در خروجی JSON ظاهر نخواهند شد. مثلا مقدار پیش فرض خاصیت int مساوی صفر است. در این حالت کلیه خواص از نوع int که دارای مقدار صفر می‌باشند، در خروجی قرار نمی‌گیرند.
به علاوه حذف Formatting = Formatting.Indented نیز توصیه می‌گردد. در این حالت فشرده‌ترین خروجی ممکن حاصل خواهد شد.


مدیریت ارث بری توسط JSON.NET

در مثال ذیل کلاس کارمند و کلاس مدیر را که خود نیز در اصل یک کارمند می‌باشد، ملاحظه می‌کنید:
public class Employee
{
    public string Name { set; get; }
}

public class Manager : Employee
{
    public IList<Employee> Reports { set; get; }
}
در اینجا هر مدیر لیست کارمندانی را که به او گزارش می‌دهند نیز به همراه دارد. در ادامه نمونه‌ای از مقدار دهی این اشیاء ذکر شده‌اند:
 var employee = new Employee { Name = "User1" };
var manager1 = new Manager { Name = "User2" };
var manager2 = new Manager { Name = "User3" };
manager1.Reports = new[] { employee, manager2 };
manager2.Reports = new[] { employee };
با فراخوانی
 var list = JsonConvert.SerializeObject(manager1, Formatting.Indented);
یک چنین خروجی JSON ایی حاصل می‌شود:
{
  "Reports": [
    {
      "Name": "User1"
    },
    {
      "Reports": [
        {
          "Name": "User1"
        }
      ],
      "Name": "User3"
    }
  ],
  "Name": "User2"
}
این خروجی JSON جهت تبدیل به نمونه‌ی معادل دات نتی خود، برای مثال جهت رسیدن به manager1 در کدهای فوق، چندین مشکل را به همراه دارد:
- در اینجا مشخص نیست که این اشیاء، کارمند هستند یا مدیر. برای مثال مشخص نیست User2 چه نوعی دارد و باید به کدام شیء نگاشت شود.
- مشکل دوم در مورد کاربر User1 است که در دو قسمت تکرار شده‌است. این شیء JSON اگر به نمونه‌ی معادل دات نتی خود نگاشت شود، به دو وهله از User1 خواهیم رسید و نه یک وهله‌ی اصلی که سبب تولید این خروجی JSON شده‌است.

برای حل این دو مشکل، تغییرات ذیل را می‌توان به JSON.NET اعمال کرد:
var list = JsonConvert.SerializeObject(manager1, new JsonSerializerSettings
{
   Formatting = Formatting.Indented,
   TypeNameHandling = TypeNameHandling.Objects,
   PreserveReferencesHandling = PreserveReferencesHandling.Objects
});
با این خروجی:
{
  "$id": "1",
  "$type": "JsonNetTests.Manager, JsonNetTests",
  "Reports": [
    {
      "$id": "2",
      "$type": "JsonNetTests.Employee, JsonNetTests",
      "Name": "User1"
    },
    {
      "$id": "3",
      "$type": "JsonNetTests.Manager, JsonNetTests",
      "Reports": [
        {
          "$ref": "2"
        }
      ],
      "Name": "User3"
    }
  ],
  "Name": "User2"
}
- با تنظیم TypeNameHandling = TypeNameHandling.Objects سبب خواهیم شد تا خاصیت اضافه‌ای به نام $type به خروجی JSON اضافه شود. این نوع، در حین فراخوانی متد JsonConvert.DeserializeObject جهت تشخیص صحیح نگاشت اشیاء بکار گرفته خواهد شد و اینبار مشخص است که کدام شیء، کارمند است و کدامیک مدیر.
- با تنظیم PreserveReferencesHandling = PreserveReferencesHandling.Objects شماره Id خودکاری نیز به خروجی JSON اضافه می‌گردد. اینبار اگر به گزارش دهنده‌ها با دقت نگاه کنیم، مقدار $ref=2 را خواهیم دید. این مورد سبب می‌شود تا در حین نگاشت نهایی، دو وهله متفاوت از شیء با Id=2 تولید نشود.

باید دقت داشت که در حین استفاده از JsonConvert.DeserializeObject نیز باید JsonSerializerSettings یاد شده، تنظیم شوند.


ویژگی‌های قابل تنظیم در JSON.NET

علاوه بر JsonSerializerSettings که از آن صحبت شد، در JSON.NET امکان تنظیم یک سری از ویژگی‌ها به ازای خواص مختلف نیز وجود دارند.
- برای نمونه ویژگی JsonIgnore معروفترین آن‌ها است:
public class User
{
   public int Id { set; get; }

   [JsonIgnore]
   public string Name { set; get; }

   public DateTime DateTime { set; get; }
}
JsonIgnore سبب می‌شود تا خاصیتی در خروجی نهایی JSON تولیدی حضور نداشته باشد و از آن صرفنظر شود.

- با استفاده از ویژگی JsonProperty اغلب مواردی را که پیشتر بحث کردیم مانند NullValueHandling، TypeNameHandling و غیره، می‌توان تنظیم نمود. همچنین گاهی از اوقات کتابخانه‌های جاوا اسکریپتی سمت کاربر، از اسامی خاصی که از روش‌های نامگذاری دات نتی پیروی نمی‌کنند، در طراحی خود استفاده می‌کنند. در اینجا می‌توان نام خاصیت نهایی را که قرار است رندر شود نیز صریحا مشخص کرد. برای مثال:
[JsonProperty(PropertyName = "m_name", NullValueHandling = NullValueHandling.Ignore)]
public string Name { set; get; }
همچنین در اینجا امکان تنظیم Order نیز وجود دارد. برای مثال مشخص کنیم که خاصیت X در ابتدا قرار گیرد و پس از آن خاصیت Y رندر شود.

- استفاده از ویژگی JsonObject به همراه مقدار OptIn آن به این معنا است که از کلیه خواصی که دارای ویژگی JsonProperty نیستند، صرفنظر شود. حالت پیش فرض آن OptOut است؛ یعنی تمام خواص عمومی در خروجی JSON حضور خواهند داشت منهای مواردی که با JsonIgnore مزین شوند.
[JsonObject(MemberSerialization.OptIn)]
public class User
{
    public int Id { set; get; }

    [JsonProperty]
    public string Name { set; get; }
 
    public DateTime DateTime { set; get; }
}

- با استفاده از ویژگی JsonConverter می‌توان نحوه‌ی رندر شدن مقدار خاصیت را سفارشی سازی کرد. برای مثال:
[JsonConverter(typeof(JavaScriptDateTimeConverter))]
public DateTime DateTime { set; get; }


تهیه یک JsonConverter سفارشی

با استفاده از JsonConverterها می‌توان کنترل کاملی را بر روی اعمال serialization و deserialization مقادیر خواص اعمال کرد. مثال زیر را در نظر بگیرید:
public class HtmlColor
{
   public int Red { set; get; }
   public int Green { set; get; }
   public int Blue { set; get; }
}

var colorJson = JsonConvert.SerializeObject(new HtmlColor
{
  Red = 255,
  Green = 0,
  Blue = 0
}, Formatting.Indented);
در اینجا علاقمندیم، در حین عملیات serialization، بجای اینکه مقادیر اجزای رنگ تهیه شده به صورت int نمایش داده شوند، کل رنگ با فرمت hex رندر شوند. برای اینکار نیاز است یک JsonConverter سفارشی را تدارک دید:
    public class HtmlColorConverter : JsonConverter
    {

        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(HtmlColor);
        }

        public override object ReadJson(JsonReader reader, Type objectType,
                                        object existingValue, JsonSerializer serializer)
        {
            throw new NotSupportedException();
        }

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            var color = value as HtmlColor;
            if (color == null)
                return;

            writer.WriteValue("#" + color.Red.ToString("X2")
                + color.Green.ToString("X2") + color.Blue.ToString("X2"));
        }
    }
کار با ارث بری از کلاس پایه JsonConverter شروع می‌شود. سپس باید تعدادی از متدهای این کلاس پایه را بازنویسی کرد. در متد CanConvert اعلام می‌کنیم که تنها اشیایی از نوع کلاس HtmlColor را قرار است پردازش کنیم. سپس در متد WriteJson منطق سفارشی خود را می‌توان پیاده سازی کرد.
از آنجائیکه این تبدیلگر صرفا قرار است برای حالت serialization استفاده شود، قسمت ReadJson آن پیاده سازی نشده‌است.

در آخر برای استفاده از آن خواهیم داشت:
var colorJson = JsonConvert.SerializeObject(new HtmlColor
{
  Red = 255,
  Green = 0,
  Blue = 0
},  new JsonSerializerSettings
    {
      Formatting = Formatting.Indented,
      Converters = { new HtmlColorConverter() }
    });   
مطالب
روش استفاده از لوسین 4.8 در دات‌نت

مطالب پیشین مرتبط با لوسین را در اینجا می‌توانید پیگیری کنید. آخرین نگارش آن که تا این تاریخ، 4.8 بتا است، با ‌دات‌نت(Core) سازگار است و روش برپایی آغازین آن ... تغییرات قابل توجهی داشته‌است که خلاصه‌ی آن‌ها را در این مطلب بررسی خواهیم کرد.

1) بسته‌های جدید مورد نیاز

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

<PackageReference Include="Lucene.Net" Version="4.8.0-beta00016"/>
<PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00016"/>
<PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00016"/>

2) تهیه نگاشت‌های لازم

فرض کنید شیء اصلی ما چنین ساختاری را دارد:

public class WhatsNewItemModel
{
    public required int Id { set; get; }

    public required string OriginalTitle { set; get; }
}

مرحله‌ی بعد کار با لوسین، تبدیل اشیاء سفارشی خود به شیء Document لوسین و برعکس است. به همین جهت به دو مپر برای این کارها نیاز است:

الف) نگاشت‌گر یک شیء سفارشی، به شیء Document

public static class LuceneDocumentMapper
{
    public static Document MapToLuceneDocument(this WhatsNewItemModel post)
    {
        ArgumentNullException.ThrowIfNull(post);

        return
        [
            new TextField(nameof(WhatsNewItemModel.OriginalTitle), post.OriginalTitle, Field.Store.YES),

            // Document StringField instances are sort of keywords, they are not analyzed, they indexed as is (in its original case).
            new StringField(nameof(WhatsNewItemModel.Id), post.Id.ToString(CultureInfo.InvariantCulture),
                Field.Store.YES),
        ];
    }
}

در اینجا یک متدالحاقی را تهیه کرده‌ایم تا شیءای از نوع WhatsNewItemModel ما را به یک شیء Document لوسین، تبدیل کند.

چند نکته در اینجا حائز اهمیت هستند:

- در نگارش جدید لوسین، با اشیاء TextField و StringField جدید سروکار داریم و شیء قدیمی Field نگارش‌های قبلی لوسین، منسوخ شده درنظر گرفته می‌شود.

- زمانی از شیء TextField استفاده می‌کنیم که قرار است توسط لوسین، تحلیل شده و در جستجوهای پیچیده استفاده شود.

- اگر فقط قرار است، مقداری را در این ایندکس ذخیره کنیم و قصد تحلیل آن‌ها را نداریم و حداکثر یک کوئری ساده‌ی یافتن اصل آن‌ها، مدنظر ما است، باید از اشیاء StringField برای معرفی و نگاشت آن‌ها استفاده کنیم (شبیه به کار با واژه‌های کلیدی).

- پرچم Field.Store.YES به این معنا است که اصل محتوای تحلیل شده نیز در ایندکس لوسین، درج شود. اگر این پرچم را به NO تنظیم کنیم، فقط تحلیل آن صورت گرفته و نتیجه‌ی آن ذخیره می‌شود، که برای جستجوها مفید است؛ اما مقدار این فیلد دیگر قابل بازیابی نخواهد بود.

ب) نگاشت‌گر یک شیء Document لوسین، به یک شیء سفارشی

در زمان کوئری گرفتن از لوسین، خروجی نهایی یک شیء Document آن است که باید به شیء سفارشی مدنظر ما نگاشت شود:

public static class LuceneDocumentMapper
{
    public static LuceneSearchResult MapToLuceneSearchResult(this Document document)
    {
        ArgumentNullException.ThrowIfNull(document);

        return new LuceneSearchResult
        {
            Id = document.Get(nameof(WhatsNewItemModel.Id), CultureInfo.InvariantCulture).ToInt(),
            OriginalTitle = document.Get(nameof(WhatsNewItemModel.OriginalTitle), CultureInfo.InvariantCulture)
        };
    }
}

نمونه‌ای از این نگاشت را در متد الحاقی فوق مشاهده می‌کنید که توسط متد Get شیء Document قابل انجام است. بدیهی است خروجی این متد، یک رشته‌است و در صورت نیاز باید توسط ما کار تبدیلات ثانویه آن‌ها انجام شود.

3) نیاز به یک تحلیل‌گر مناسب

لوسین برای تولید ایندکس‌های جستجوی تمام متنی خود، از یک سری Analyzer استفاده می‌کنید که اگر سری پیشین مطالب مرتبط را مطالعه کنید، به نمونه‌ی StandardAnalyzer آن خواهید رسید که هنوز هم معتبر و قابل استفاده‌است و یا می‌توان همانند سایت جاری، از یک LowerCaseHtmlStripAnalyzer استفاده کرد که این کارها را همزمان انجام می‌دهد:

الف) از یک لیست PersianStopwords.List برای حذف واژه‌های کم اهمیت زبان فارسی استفاده می‌کند. برای مثال ما نمی‌خواهیم که واژه‌ی «ما» را با اهمیت شمرده و ایندکس کند و امثال آن.

ب) LowerCaseFilter را به متون دریافتی اعمال می‌کند. این کار در پشت صحنه‌ی StandardAnalyzer توکار لوسین هم اعمال می‌شود. اگر با این موضوع آشنا نباشید، ممکن است در حین کوئری گرفتن، به نتیجه‌ای نرسید! چون متن ارسالی به لوسین را ابتدا باید lower-case کنید و سپس آن‌را کوئری بگیرید.

ج) HTMLStripCharFilter توکار لوسین هم به آن اعمال شده‌است. از این جهت که متن مقالات ما به همراه تگ‌های HTML ای هم هستند. این فیلتر کار حذف کردن آن‌ها را در حین تحلیل، انجام می‌دهد و دیگر نیازی نیست تا ما خودمان متن ارسالی به لوسین را تمیز کنیم.

نکته‌ی مهم: این تحلیل‌گر ویژه، فقط باید به فیلدهایی از نوع TextField اعمال شود. اگر آن‌را به StringField ها اعمال کنیم، دیگر قادر به کوئری گرفتن از آن‌ها نخواهیم بود! چون تحلیل‌گر StringFieldها باید از نوع توکار KeywordAnalyzer ثبت و معرفی شود. این نوع فیلدها، حالت واژه‌های کلیدی را دارند (به همان صورتی که هست ثبت می‌شوند) و قرارنیست که توسط لوسین تحلیل ویژه‌ای شوند. به همین جهت برای رسیدن به یک تحلیل‌گر ترکیبی که بتواند این دو نوع فیلد را با هم پوشش دهد و کار معرفی چندین نوع تحلیل‌گر را یکجا انجام دهد، نیاز به یک PerFieldAnalyzerWrapper جدید داریم:

_keywordAnalyzer = new KeywordAnalyzer();

        _lowerCaseHtmlStripAnalyzer = new LowerCaseHtmlStripAnalyzer(LuceneVersion);

        _analyzer = new PerFieldAnalyzerWrapper(_lowerCaseHtmlStripAnalyzer, new Dictionary<string, Analyzer>
        {
            {
                nameof(WhatsNewItemModel.Id), _keywordAnalyzer
            }
        });

PerFieldAnalyzerWrapper در حقیقت برای تمام فیلدهایی که در قسمت دیکشنری فوق، ذکر نشده‌اند، از LowerCaseHtmlStripAnalyzer استفاده می‌کند. برای مابقی موارد از KeywordAnalyzer کمک خواهد گرفت.

4) روش صحیح راه اندازی reader و writer های ایندکس لوسین جدید

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

نکته‌ی مهمی که این مثال‌ها به آن توجهی نکرده‌اند، «thread-safe» بودن نویسنده و خواننده‌ی ایندکس لوسین است. یعنی می‌توان یک نمونه از این‌ها را در ابتدای کار برنامه ایجاد کرد و تا آخر کار برنامه، بدون نیاز به نمونه سازی مجدد و باز و بسته کردن آن‌ها، بارها مورد استفاده‌ی مجدد قرار داد و هیچ تداخلی هم ندارند و از قسمت‌های مختلف برنامه هم قابل دسترسی هستند.

به همین جهت باید یک سرویس مرکزی را برای اینکار تدارک دید که طول عمر آن، حتما Singleton باشد تا بتواند نویسنده و خواننده‌ی ایندکس لوسین را فقط یکبار نمونه سازی و ایجاد کرده و تا پایان کار برنامه، زنده نگه دارد (کدهای کامل این کلاس را در اینجا می‌توانید مطالعه کنید):

public class FullTextSearchService : IFullTextSearchService
{
    private const LuceneVersion LuceneVersion = Lucene.Net.Util.LuceneVersion.LUCENE_48;
    private readonly Analyzer _analyzer;

    private readonly IAppFoldersService _appFoldersService;
    private readonly FSDirectory _fsDirectory;

    //  IndexWriter instances are completely thread safe, meaning multiple threads can call any of its methods, concurrently.
    private readonly IndexWriter _indexWriter;

    private readonly KeywordAnalyzer _keywordAnalyzer;
    private readonly ILogger<FullTextSearchService> _logger;
    private readonly LowerCaseHtmlStripAnalyzer _lowerCaseHtmlStripAnalyzer;

    // Safely shares IndexSearcher instances across multiple threads, while periodically reopening.
    private readonly SearcherManager _searcherManager;

    private bool _isDisposed;

    public FullTextSearchService(IAppFoldersService appFoldersService, ILogger<FullTextSearchService> logger)
    {
        _appFoldersService = appFoldersService ?? throw new ArgumentNullException(nameof(appFoldersService));
        _logger = logger;

        _keywordAnalyzer = new KeywordAnalyzer();

        _lowerCaseHtmlStripAnalyzer = new LowerCaseHtmlStripAnalyzer(LuceneVersion);

        _analyzer = new PerFieldAnalyzerWrapper(_lowerCaseHtmlStripAnalyzer, new Dictionary<string, Analyzer>
        {
            // Document StringField instances are sort of keywords, they are not analyzed, they indexed as is (in its original case).
            // But StandardAnalyzer applies lower case filter to a query.
            // We can fix this by using KeywordAnalyzer with our query parser.
            {
                nameof(WhatsNewItemModel.Id), _keywordAnalyzer
            },
            {
                nameof(WhatsNewItemModel.DocumentTypeIdHash), _keywordAnalyzer
            },
            {
                nameof(WhatsNewItemModel.DocumentContentHash), _keywordAnalyzer
            }
        });

        _fsDirectory = FSDirectory.Open(_appFoldersService.LuceneIndexFolderPath);

        _indexWriter = new IndexWriter(_fsDirectory, new IndexWriterConfig(LuceneVersion, _analyzer));
        _searcherManager = new SearcherManager(_indexWriter, applyAllDeletes: true, searcherFactory: null);
    }

این سرویس، یک سرویس Singleton است که نحوه‌ی آغاز و شروع به کار با اشیاء لوسین را در سازنده‌ی آن مشاهده می‌کنید.

توضیحات:

الف) در اینجا، روش نمونه سازی PerFieldAnalyzerWrapper را که پیشتر در مورد آن بحث شد، مشاهده می‌کنید.

ب) سپس یک IndexWriter، نمونه سازی می‌شود که از تحلیل‌گر ترکیبی ما استفاده می‌کند.

ج) در ادامه یک SearcherManager جدید را مشاهده می‌کنید که با IndexWriter برنامه هماهنگ است و هر زمانیکه سندی به لوسین اضافه می‌شود، قادر به کوئری گرفتن از آن هم خواهیم بود.

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

5) روش افزودن یک سند به ایندکس لوسین و سپس به روز رسانی آن

اکنون با استفاده از نگاشت‌گرهایی که در ابتدای بحث تهیه کردیم و همچنین شیء IndexWriter فوق، به صورت زیر می‌توان یک شیء سفارشی خود را به ایندکس لوسین اضافه کنیم:

_indexWriter.AddDocument(post.MapToLuceneDocument());
_indexWriter.Flush(triggerMerge: true, applyAllDeletes: true);
_indexWriter.Commit();

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

_indexWriter.UpdateDocument(new Term(nameof(WhatsNewItemModel.Id), post.Id.ToString()),
                post.MapToLuceneDocument());

new Term، در حقیقت یک کوئری جدید را سبب می‌شود که توسط آن سندی یافت شده، در پشت صحنه حذف می‌شود و سپس سند جدیدی بجای آن درج خواهد شد. در اینجا باید دقت داشت که چون Id ثبت شده از نوع StringField است، نباید حالت lower-case آن‌را جستجو کرد و باید دقیقا به همان نحوی که ثبت شده، جستجو شود.

6) روش کار با searcherManager جدید لوسین

همانطور که عنوان شد، لوسین جدید به همراه یک searcherManager هم هست که کار آن، ارائه‌ی thread-safe دسترسی به خواننده‌ی ایندکس‌ لوسین است. نحوه‌ی عمومی کار با آن را در ادامه مشاهده می‌کنید:

private TResult DoSearch<TResult>(Func<IndexSearcher, TResult> action, TResult defaultValue)
    {
        _searcherManager.MaybeRefreshBlocking();
        var indexSearcher = _searcherManager.Acquire();

        try
        {
            return action(indexSearcher);
        }
        catch (FileNotFoundException)
        {
            // It's not indexed yet.
            return defaultValue;
        }
        finally
        {
            _searcherManager.Release(indexSearcher);
        }
    }

با استفاده از searcherManager، در طول مدت زمان کوتاهی، بر روی ایندکس قفل‌گذاری شده و یک indexSearcher امن، در اختیار متدهای استفاده کننده‌ی از آن قرار می‌گیرند و در پایان کار، این قفل رها می‌شود.

برای مثال یک نمونه روش استفاده از این indexSearcher امن، به صورت زیر است:

public int GetNumberOfDocuments() => DoSearch(indexSearcher => indexSearcher.IndexReader.NumDocs, defaultValue: 0);

مابقی مثال‌های آن‌را می‌توانید در کلاس FullTextSearchService مشاهده کنید که به همراه یافتن «مطالب مشابه»، جستجوهای صفحه بندی شده، جستجوهای مرتب شده‌ی بر اساس یک فیلد، امکان دسترسی به تمام اسناد ذخیره شده‌ی در ایندکس لوسین و امثال آن است که کلیات آن با قبل تفاوتی نکرده‌است و مطالب و نکات آن‌را پیشتر در مقالات سری لوسین بررسی کرده‌ایم. تنها تفاوت مهمی که در اینجا وجود دارد، نحوه‌ی برپایی و راه اندازی تحلیل‌گر، خواننده و نویسنده‌ی ایندکس آن است که در این مطلب بررسی شدند؛ وگرنه کلیات جستجوی پیشرفته‌ی آن، مانند قبل است و تفاوت خاصی نکرده‌است.

مطالب
معرفی System.Text.Json در NET Core 3.0.
معروفترین کتابخانه‌ی کار با JSON در دات نت، Json.NET است که این روزها، جزء جدایی ناپذیر حداقل، تمام برنامه‌های وب مبتنی بر دات نت می‌باشد. برای مثال ASP.NET Core 2x/1x و همچنین ASP.NET Web API پیش از NET Core.، به صورت پیش‌فرض از این کتابخانه برای کار با JSON استفاده می‌کنند. این کتابخانه 10 سال پیش ایجاد شد و در طول زمان، قابلیت‌های زیادی به آن اضافه شده‌است. همین حجم بالای کار صورت گرفته سبب شده‌است که برای مثال شروع به استفاده‌ی از <Span<T در آن برای بالابردن کارآیی، بسیار مشکل شده و نیاز به تغییرات اساسی در آن داشته باشد. به همین جهت خود تیم CoreFX دات نت Core گزینه‌ی دیگری را برای کار با JSON در فضای نام جدید System.Text.Json ارائه داده‌است که برای کار با آن نیاز به نصب وابستگی ثالثی نیست و همچنین کارآیی آن به علت استفاده‌ی از ویژگی‌های جدید زبان، مانند ref struct و Span، به طور میانگین دو برابر کتابخانه‌ی Json.NET است. برای مثال استفاده‌ی از string (حالت پیش‌فرض کتابخانه‌ی Json.NET) یعنی کار با رشته‌هایی از نوع UTF-16؛ اما کار با Span، امکان دسترسی مستقیم به رشته‌هایی از نوع UTF-8 را میسر می‌کند که نیازی به تبدیل به رشته‌هایی از نوع UTF-16 را ندارند.


ASP.NET Core 3x دیگر به صورت پیش‌فرض به همراه Json.NET ارائه نمی‌شود

در برنامه‌های ASP.NET Core 3x، وابستگی ثالث Json.NET حذف شده‌است و از این پس هر نوع خروجی JSON آن، مانند بازگشت مقادیر مختلف از اکشن متدهای کنترلرها، به صورت خودکار در پشت صحنه از امکانات ارائه شده‌ی در System.Text.Json استفاده می‌کند و دیگر Json.NET، کتابخانه‌ی پیش‌فرض کار با JSON آن نیست. بنابراین برای کار با آن نیاز به تنظیم خاصی نیست. همینقدر که یک پروژه‌ی جدید ASP.NET Core 3x را ایجاد کنید، یعنی در حال استفاده‌ی از System.Text.Json هستید.


روش بازگشت به Json.NET در ASP.NET Core 3x

اگر به هر دلیلی هنوز نیاز به استفاده‌ی از کتابخانه‌ی Json.NET را دارید، آداپتور ویژه‌ی آن نیز تدارک دیده شده‌است. برای اینکار:
الف) ابتدا باید بسته‌ی نیوگت Microsoft.AspNetCore.Mvc.NewtonsoftJson را نصب کنید.
ب) سپس در کلاس Startup، باید این کتابخانه را به صورت یک سرویس جدید، با فراخوانی متد AddNewtonsoftJson، معرفی کرد:
 public void ConfigureServices(IServiceCollection services)
 {
     services.AddControllers()
            .AddNewtonsoftJson()
     // ...
}
یکی از دلایل بازگشت به Json.NET می‌تواند عدم پشتیبانی از OpenAPI / Swagger در حین کار با System.Text.Json باشد و این مورد قرار نیست در نگارش نهایی 3.0، حضور داشته باشد و انطباق با آن به نگارش‌های بعدی موکول شده‌است.


روش کار مستقیم با System.Text.Json

اگر در قسمتی از برنامه‌ی خود نیاز به کار مستقیم با اشیاء JSON را داشته باشید و یا حتی بخواهید از این قابلیت در برنامه‌های کنسول و یا کتابخانه‌ها نیز استفاده کنید، روش انتقال کدهایی که از Json.NET استفاده می‌کنند به System.Text.Json، به صورت زیر است:
public class Person
{
   public string FirstName { get; set; }
   public string LastName { get; set; }
   public DateTime? BirthDay { get; set; }
}
تبدیل رشته‌ی JSON حاوی اطلاعات شخص، به شیء متناظر با آن و یا حالت عکس آن:
using System;
using System.Text.Json.Serialization;

namespace ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Person person = JsonSerializer.Parse<Person>(...);
            string json = JsonSerializer.ToString(person);
        }
    }
}
در اینجا از کلاس System.Text.Json.Serialization.JsonSerializer، روش کار با دو متد Parse را برای Deserialization و ToString را برای Serialization مشاهده می‌کنید.
کلاس JsonSerializer دارای overloadهای زیر برای کار با متدهای Parse و ToString است:
namespace System.Text.Json.Serialization
{
    public static class JsonSerializer
    {
        public static object Parse(ReadOnlySpan<byte> utf8Json, Type returnType, JsonSerializerOptions options = null);
        public static object Parse(string json, Type returnType, JsonSerializerOptions options = null);
        public static TValue Parse<TValue>(ReadOnlySpan<byte> utf8Json, JsonSerializerOptions options = null);
        public static TValue Parse<TValue>(string json, JsonSerializerOptions options = null);

        public static string ToString(object value, Type type, JsonSerializerOptions options = null);
        public static string ToString<TValue>(TValue value, JsonSerializerOptions options = null);
    }
}
یک نکته: کارآیی متد Parse با امضای ReadOnlySpan<byte> utf8Json، بیشتر است از نمونه‌ای که string json را می‌پذیرد. از این جهت که چون با آرایه‌ای از بایت‌های رشته‌ای از نوع UTF-8 کار می‌کند، نیاز به تبدیل به UTF-16 را مانند متدی که string را می‌پذیرد، ندارد. برای تولید آرایه‌ی بایت‌های utf8Json از روی یک شیء، می‌توانید از متد JsonSerializer.ToUtf8Bytes استفاده کنید و یا برای تولید آن از روی یک رشته، از متد Encoding.UTF8.GetBytes استفاده کنید.


سفارشی سازی JsonSerializer جدید

اگر به امضای متدهای Parse و ToString کلاس JsonSerializer دقت کنید، دارای یک پارامتر اختیاری از نوع JsonSerializerOptions نیز هستند که به صورت زیر تعریف شده‌است:
public sealed class JsonSerializerOptions
{
   public bool AllowTrailingCommas { get; set; }
   public int DefaultBufferSize { get; set; }
   public JsonNamingPolicy DictionaryKeyPolicy { get; set; }
   public bool IgnoreNullValues { get; set; }
   public bool IgnoreReadOnlyProperties { get; set; }
   public int MaxDepth { get; set; }
   public bool PropertyNameCaseInsensitive { get; set; }
   public JsonNamingPolicy PropertyNamingPolicy { get; set; }
   public JsonCommentHandling ReadCommentHandling { get; set; }
   public bool WriteIndented { get; set; }
}
برای نمونه معادل تنظیم NullValueHandling در Json.NET:
// Json.NET:
var settings = new JsonSerializerSettings
{
    NullValueHandling = NullValueHandling.Ignore
};
string json = JsonConvert.SerializeObject(person, settings);
اینبار توسط خاصیت IgnoreNullValues صورت می‌گیرد:
// JsonSerializer:
var options = new JsonSerializerOptions
{
    IgnoreNullValues = true
};
string json = JsonSerializer.ToString(person, options);

در برنامه‌های ASP.NET Core که این نوع متدها در پشت صحنه فراخوانی می‌شوند، روش تنظیم JsonSerializerOptions به صورت زیر است:
services.AddControllers()
   .AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true);


نگاشت نام ویژه‌ی خواص در حین عملیات deserialization

در مثال فوق، فرض شده‌است که نام خاصیت BirthDay، دقیقا با اطلاعاتی که از رشته‌ی JSON دریافتی پردازش می‌شود، تطابق دارد. اگر این نام در اطلاعات دریافتی متفاوت است، می‌توان از ویژگی JsonPropertyName برای تعریف این نگاشت استفاده کرد:
[JsonPropertyName("birthdate")]
public DateTime? BirthDay { get; set; }
روش دیگر اینکار، برای نمونه تنظیم PropertyNamingPolicy به حالت CamelCase است:
var options = new JsonSerializerOptions
{
   PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
string json = JsonSerializer.ToString(person, options);
و یا اگر می‌خواهید حساسیت به بزرگی و کوچکی حروف را ندید بگیرید (مانند حالت پیش‌فرض JSON.NET) از تنظیم JsonSerializerOptions.PropertyNameCaseInsensitive استفاده کنید.

در این بین اگر نمی‌خواهید خاصیتی در عملیات serialization و یا برعکس آن پردازش شود، می‌توان از تعریف ویژگی [JsonIgnore] بر روی آن استفاده کرد.