نظرات مطالب
نوشتن TagHelperهای سفارشی برای ASP.NET Core
یک نکته: برای تبدیل محتوای یک TagBuilder به رشته می‌توان از متد زیر استفاده کرد:
using System.IO;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Html;

namespace Sample
{
    /// <summary>
    /// Html Helper Extensions
    /// </summary>
    public static class HtmlHelperExtensions
    {
        /// <summary>
        /// Convert IHtmlContent/TagBuilder to string
        /// </summary>
        public static string GetString(this IHtmlContent content)
        {
            using (var writer = new StringWriter())
            {
                content.WriteTo(writer, HtmlEncoder.Default);
                return writer.ToString();
            }
        }
    }
}
مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت هشتم- تعریف سطوح دسترسی پیچیده
تعریف نقش‌ها، ساده‌ترین روش محدود کردن دسترسی به منابع است؛ برای نمونه مشخص می‌کنیم که کاربران دارای نقش PayingUser، امکان سفارش دادن نگارش قاب شده‌ی تصاویر را داشته باشند. اما می‌خواهیم منطق دسترسی به منابع مختلف را پیچیده‌تر کنیم. برای مثال می‌خواهیم بررسی کنیم اگر منبعی واقعا متعلق به کاربر جاری سیستم است، به آن دسترسی داشته باشد. برای مثال هرچند کاربر جاری دارای نقش PayingUser است، اما آیا باید اجازه‌ی ویرایش تصاویر تمام کاربران، برای او صادر شده باشد؟ برای پیاده سازی یک چنین موارد پیچیده‌ای که فراتر از مفهوم نقش‌ها هستند، ویژگی جدیدی به نام Authorization policies به ASP.NET Core اضافه شده‌است که آن‌را در این قسمت بر اساس امکانات IdentityServer 4 بررسی می‌کنیم.


مقایسه تعریف سطوح دسترسی «مبتنی بر نقش‌ها» با سطوح دسترسی «مبتنی بر سیاست‌های امنیتی»

- در سطوح دسترسی «مبتنی بر نقش‌ها»
یک‌سری نقش از پیش تعریف شده وجود دارند؛ مانند PayingUser و یا FreeUser که کاربر توسط هر نقش، به یکسری دسترسی‌های خاص نائل می‌شود. برای مثال PayingUser می‌تواند نگارش قاب شده‌ی تصاویر را سفارش دهد و یا تصویری را به سیستم اضافه کند.

- در سطوح دسترسی «مبتنی بر سیاست‌های امنیتی»
سطوح دسترسی بر اساس یک سری سیاست که بیانگر ترکیبی از منطق‌های دسترسی هستند، اعطاء می‌شوند. این منطق‌ها نیز از طریق ترکیب User Claims حاصل می‌شوند و می‌توانند منطق‌های پیچیده‌تری را به همراه داشته باشند. برای مثال اگر کاربری از کشور A است و نوع اشتراک او B است و اگر در بین یک بازه‌ی زمانی خاصی متولد شده باشد، می‌تواند به منبع خاصی دسترسی پیدا کند. به این ترتیب حتی می‌توان نیاز به ترکیب چندین نقش را با تعریف یک سیاست امنیتی جدید جایگزین کرد. به همین جهت نسبت به روش بکارگیری مستقیم کار با نقش‌ها ترجیح داده می‌شود.


جایگزین کردن بررسی سطوح دسترسی توسط نقش‌ها با روش بکارگیری سیاست‌های دسترسی

در ادامه می‌خواهیم بجای بکارگیری مستقیم نقش‌ها جهت محدود کردن دسترسی به قسمت‌های خاصی از برنامه‌ی کلاینت، تنها کاربرانی که از کشور خاصی وارد شده‌اند و نیز سطح اشتراک خاصی را دارند، بتوانند دسترسی‌های ویژه‌ای داشته باشند؛ چون برای مثال امکان ارسال مستقیم تصاویر قاب شده را به کشور دیگری نداریم.

تنظیم User Claims جدید در برنامه‌ی IDP
برای تنظیم این سیاست امنیتی جدید، ابتدا دو claim جدید subscriptionlevel و country را به خواص کاربران در کلاس src\IDP\DNT.IDP\Config.cs در سطح IDP اضافه می‌کنیم:
namespace DNT.IDP
{
    public static class Config
    {
        public static List<TestUser> GetUsers()
        {
            return new List<TestUser>
            {
                new TestUser
                {
                    Username = "User 1",
                    // ...

                    Claims = new List<Claim>
                    {
    // ...
                        new Claim("subscriptionlevel", "PayingUser"),
                        new Claim("country", "ir")
                    }
                },
                new TestUser
                {
                    Username = "User 2",
// ...

                    Claims = new List<Claim>
                    {
    // ...
                        new Claim("subscriptionlevel", "FreeUser"),
                        new Claim("country", "be")
                    }
                }
            };
        }
سپس باید تعاریف این claims جدید را به متد GetIdentityResources افزود تا به صورت scopeهای جدید از طرف کلاینت‌ها قابل درخواست باشند و چون این claimها استاندارد نیستند، برای تعریف آن‌ها از IdentityResource استفاده می‌کنیم:
namespace DNT.IDP
{
    public static class Config
    {
        // identity-related resources (scopes)
        public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            return new List<IdentityResource>
            {
   // ...     
                new IdentityResource(
                    name: "country",
                    displayName: "The country you're living in",
                    claimTypes: new List<string> { "country" }),
                new IdentityResource(
                    name: "subscriptionlevel",
                    displayName: "Your subscription level",
                    claimTypes: new List<string> { "subscriptionlevel" })
            };
        }
همچنین باید مطمئن شد که کلاینت مدنظر ما قادر است این scopeهای تعریف شده را درخواست کند و IDP مجاز است تا آن‌ها را بازگشت دهد. برای این منظور آن‌ها را به لیست AllowedScopes تعریف کلاینت، اضافه می‌کنیم:
namespace DNT.IDP
{
    public static class Config
    {
        public static IEnumerable<Client> GetClients()
        {
            return new List<Client>
            {
                new Client
                {
                    ClientName = "Image Gallery",
// ...
                    AllowedScopes =
                    {
    // ...
                        "country",
                        "subscriptionlevel"
                    }
// ...
                }
             };
        }
    }

استفاده‌ی از User Claims جدید در برنامه‌ی MVC Client
در ادامه به کلاس ImageGallery.MvcClient.WebApp\Startup.cs برنامه‌ی MVC Client مراجعه کرده و دو scope جدیدی را که در سمت IDP تعریف کردیم، در اینجا در تنظیمات متد AddOpenIdConnect، درخواست می‌دهیم:
options.Scope.Add("subscriptionlevel");
options.Scope.Add("country");
به این ترتیب برنامه‌ی کلاینت می‌تواند دسترسی به این دو claim جدید را از طریق IDP، پیدا کند.
البته همانطور که در قسمت‌های قبل نیز ذکر شد، اگر claim ای در لیست نگاشت‌های تنظیمات میان‌افزار OpenID Connect مایکروسافت نباشد، آن‌را در لیست this.User.Claims ظاهر نمی‌کند. به همین جهت همانند claim role که پیشتر MapUniqueJsonKey را برای آن تعریف کردیم، نیاز است برای این دو claim نیز نگاشت‌های لازم را به سیستم افزود:
options.ClaimActions.MapUniqueJsonKey(claimType: "role", jsonKey: "role");
options.ClaimActions.MapUniqueJsonKey(claimType: "subscriptionlevel", jsonKey: "subscriptionlevel");
options.ClaimActions.MapUniqueJsonKey(claimType: "country", jsonKey: "country");

ایجاد سیاست‌های دسترسی در برنامه‌ی MVC Client

برای تعریف یک سیاست دسترسی جدید در کلاس ImageGallery.MvcClient.WebApp\Startup.cs برنامه‌ی MVC Client، به متد ConfigureServices آن مراجعه کرده و آن‌را به صورت زیر تکمیل می‌کنیم:
namespace ImageGallery.MvcClient.WebApp
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthorization(options =>
            {
                options.AddPolicy(
                   name: "CanOrderFrame",
                   configurePolicy: policyBuilder =>
                    {
                        policyBuilder.RequireAuthenticatedUser();
                        policyBuilder.RequireClaim(claimType: "country", requiredValues: "ir");
                        policyBuilder.RequireClaim(claimType: "subscriptionlevel", requiredValues: "PayingUser");
                    });
            });
در اینجا نحوه‌ی تعریف یک Authorization Policy جدید را مشاهده می‌کنید. ابتدا یک نام برای آن تعریف می‌شود که در قسمت‌های دیگر برنامه جهت ارجاع به آن مورد استفاده قرار می‌گیرد. سپس تنظیمات این سیاست دسترسی جدید را مشاهده می‌کنید که در آن نیاز است کاربر مدنظر حتما اعتبارسنجی شده باشد. از کشور ir بوده و همچنین سطح اشتراک او PayingUser باشد. در اینجا پارامتر requiredValues، یک آرایه را می‌پذیرد. بنابراین اگر برای مثال کشورهای دیگری نیز مدنظر هستند، می‌توان لیست آن‌ها را در اینجا اضافه کرد.
به علاوه policyBuilder شامل متد RequireRole نیز هست. به همین جهت است که این روش تعریف سطوح دسترسی، روش قدیمی مبتنی بر نقش‌ها را جایگزین کرده و در برگیرنده‌ی آن نیز می‌شود؛ چون در این سیستم، role نیز تنها یک claim است، مانند country و یا subscriptionlevel فوق.


بررسی نحوه‌ی استفاده‌ی از Authorization Policy تعریف شده و جایگزین کردن آن با روش بررسی نقش‌ها

تا کنون از روش بررسی سطوح دسترسی‌ها بر اساس نقش‌های کاربران در دو قسمت استفاده کرده‌ایم:
الف) اصلاح Views\Shared\_Layout.cshtml برای استفاده‌ی از Authorization Policy
در فایل Layout با بررسی نقش PayingUser، منوهای مرتبط با این نقش را فعال می‌کنیم:
@if(User.IsInRole("PayingUser"))
{
  <li><a asp-area="" asp-controller="Gallery" asp-action="AddImage">Add an image</a></li>
  <li><a asp-area="" asp-controller="Gallery" asp-action="OrderFrame">Order a framed picture</a></li>
}
برای جایگزین کردن آن جهت استفاده‌ی از سیاست دسترسی جدید CanOrderFrame، ابتدا نیاز است در این View به سرویس IAuthorizationService دسترسی پیدا کنیم که روش تزریق آن‌را در ذیل مشاهده می‌کنید:
@using Microsoft.AspNetCore.Authorization
@inject IAuthorizationService AuthorizationService
پس از آن، روش استفاده‌ی از این سرویس را در ذیل مشاهده می‌کنید:
@if (User.IsInRole("PayingUser"))
{
  <li><a asp-area="" asp-controller="Gallery" asp-action="AddImage">Add an image</a></li>
}
@if ((await AuthorizationService.AuthorizeAsync(User, "CanOrderFrame")).Succeeded)
{
  <li><a asp-area="" asp-controller="Gallery" asp-action="OrderFrame">Order a framed picture</a></li>
}
اکنون لینک منوی درخواست نگارش قاب شده‌ی یک تصویر، صرفا به کاربران تامین کننده‌ی سیاست دسترسی CanOrderFrame نمایش داده می‌شود.

ب) اصلاح کنترلر ImageGallery.MvcClient.WebApp\Controllers\GalleryController.cs برای استفاده‌ی از Authorization Policy
namespace ImageGallery.MvcClient.WebApp.Controllers
{
    [Authorize]
    public class GalleryController : Controller
    {
        [Authorize(Policy = "CanOrderFrame")]
        public async Task<IActionResult> OrderFrame()
        {
در اینجا فیلتر Authorize امکان پذیرش نام یک Policy را نیز به همراه دارد.

اکنون برای آزمایش برنامه یکبار از آن خارج شده و سپس توسط اکانت User 1 که از نوع PayingUser در کشور ir است، به آن وارد شوید.
ابتدا به قسمت IdentityInformation آن وارد شوید. در اینجا لیست claims جدید را می‌توانید مشاهده کنید. همچنین لینک سفارش تصویر قاب شده نیز نمایان است و می‌توان به آدرس آن نیز وارد شد.


استفاده از سیاست‌های دسترسی در سطح برنامه‌ی Web API

در سمت برنامه‌ی Web API، در حال حاضر کاربران می‌توانند به متدهای Get ،Put و Delete ای که رکوردهای آن‌ها الزاما متعلق به آن‌ها نیست دسترسی داشته باشند. بنابراین نیاز است از ورود کاربران به متدهای تغییرات رکوردهایی که OwnerID آن‌ها با هویت کاربری آن‌ها تطابقی ندارد، جلوگیری کرد. در این حالت Authorization Policy تعریف شده نیاز دارد تا با سرویس کاربران و بانک اطلاعاتی کار کند. همچنین نیاز به دسترسی به اطلاعات مسیریابی جاری را برای دریافت ImageId دارد. پیاده سازی یک چنین سیاست دسترسی پیچیده‌ای توسط متدهای RequireClaim و RequireRole میسر نیست. خوشبختانه امکان بسط سیستم Authorization Policy با پیاده سازی یک IAuthorizationRequirement سفارشی وجود دارد. RequireClaim و RequireRole، جزو Authorization Requirementهای پیش‌فرض و توکار هستند. اما می‌توان نمونه‌های سفارشی آن‌ها را نیز پیاده سازی کرد:
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;

namespace ImageGallery.WebApi.Services
{
    public class MustOwnImageRequirement : IAuthorizationRequirement
    {
    }

    public class MustOwnImageHandler : AuthorizationHandler<MustOwnImageRequirement>
    {
        private readonly IImagesService _imagesService;
        private readonly ILogger<MustOwnImageHandler> _logger;

        public MustOwnImageHandler(
            IImagesService imagesService,
            ILogger<MustOwnImageHandler> logger)
        {
            _imagesService = imagesService;
            _logger = logger;
        }

        protected override async Task HandleRequirementAsync(
            AuthorizationHandlerContext context, MustOwnImageRequirement requirement)
        {
            var filterContext = context.Resource as AuthorizationFilterContext;
            if (filterContext == null)
            {
                context.Fail();
                return;
            }

            var imageId = filterContext.RouteData.Values["id"].ToString();
            if (!Guid.TryParse(imageId, out Guid imageIdAsGuid))
            {
                _logger.LogError($"`{imageId}` is not a Guid.");
                context.Fail();
                return;
            }

            var subClaim = context.User.Claims.FirstOrDefault(c => c.Type == "sub");
            if (subClaim == null)
            {
                _logger.LogError($"User.Claims don't have the `sub` claim.");
                context.Fail();
                return;
            }

            var ownerId = subClaim.Value;
            if (!await _imagesService.IsImageOwnerAsync(imageIdAsGuid, ownerId))
            {
                _logger.LogError($"`{ownerId}` is not the owner of `{imageIdAsGuid}` image.");
                context.Fail();
                return;
            }

            // all checks out
            context.Succeed(requirement);
        }
    }
}
در پروژه‌ی ImageGallery.WebApi.Services ابتدا یک Authorization Requirement و سپس پیاده سازی کننده‌ی آن که Authorization Handler نام دارد را تعریف کرده‌ایم. این پروژه نیاز به وابستگی‌های ذیل را دارد تا کامپایل شود.
<Project Sdk="Microsoft.NET.Sdk">
  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.1.0" />
    <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="2.1.1.0" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.1.1.0" />
  </ItemGroup>
</Project>

پیاده سازی سیاست‌های پویای دسترسی شامل مراحل ذیل است:
1- تعریف یک نیازمندی دسترسی جدید
public class MustOwnImageRequirement : IAuthorizationRequirement
{
}
ابتدا نیاز است یک نیازمندی دسترسی جدید را با پیاده سازی اینترفیس IAuthorizationRequirement ارائه دهیم. این نیازمندی، خالی است و صرفا به عنوان نشانه‌ای جهت یافت AuthorizationHandler استفاده کننده‌ی از آن استفاده می‌شود. در اینجا در صورت نیاز می‌توان یک سری خاصیت اضافه را تعریف کرد تا آن‌ها را به صورت پارامترهایی ثابت به AuthorizationHandler ارسال کند.

2- پیاده سازی یک AuthorizationHandler استفاده کننده‌ی از نیازمندی دسترسی تعریف شده
که کدهای کامل آن‌را در کلاس MustOwnImageHandler مشاهده می‌کنید. کار آن با ارث بری از AuthorizationHandler شروع شده و آرگومان جنریک آن، همان نیازمندی است که پیشتر تعریف کردیم. از این آرگومان جنریک جهت یافتن خودکار AuthorizationHandler متناظر با آن توسط ASP.NET Core استفاده می‌شود. بنابراین در اینجا MustOwnImageRequirement تهیه شده صرفا کارکرد علامتگذاری را دارد.
در کلاس تهیه شده باید متد HandleRequirementAsync آن‌را بازنویسی کرد و اگر در این بین، منطق سفارشی ما context.Succeed را فراخوانی کند، به معنای برآورده شدن سیاست دسترسی بوده و کاربر جاری می‌تواند به منبع درخواستی بلافاصله دسترسی یابد و اگر context.Fail فراخوانی شود، در همینجا دسترسی کاربر قطع شده و HTTP status code مساوی 401 (عدم دسترسی) را دریافت می‌کند.
در این پیاده سازی از filterContext.RouteData برای یافتن Id تصویر مورد نظر استفاده شده‌است. همچنین Id شخص جاری نیز از sub claim موجود استخراج گردیده‌است. اکنون این اطلاعات را به سرویس تصاویر ارسال می‌کنیم تا توسط متد IsImageOwnerAsync آن مشخص شود که آیا کاربر جاری سیستم، همان کاربری است که تصویر را در بانک اطلاعاتی ثبت کرده‌است؟ اگر بله، با فراخوانی context.Succeed به سیستم Authorization اعلام خواهیم کرد که این سیاست دسترسی و نیازمندی مرتبط با آن با موفقیت پشت سر گذاشته شده‌است.

3- معرفی سیاست دسترسی پویای تهیه شده به سیستم
معرفی سیاست کاری پویا و سفارشی تهیه شده، شامل دو مرحله‌ی زیر است:
مراجعه‌ی به کلاس ImageGallery.WebApi.WebApp\Startup.cs و افزودن نیازمندی آن:
namespace ImageGallery.WebApi.WebApp
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthorization(authorizationOptions =>
            {
                authorizationOptions.AddPolicy(
                   name: "MustOwnImage",
                   configurePolicy: policyBuilder =>
                    {
                        policyBuilder.RequireAuthenticatedUser();
                        policyBuilder.AddRequirements(new MustOwnImageRequirement());
                    });
            });
            services.AddScoped<IAuthorizationHandler, MustOwnImageHandler>();
ابتدا باید MustOwnImageHandler تهیه شده را به سیستم تزریق وابستگی‌ها معرفی کنیم.
سپس یک Policy جدید را با نام دلخواه MustOwnImage تعریف کرده و نیازمندی علامتگذار خود را به عنوان یک policy.Requirements جدید، اضافه می‌کنیم. همانطور که ملاحظه می‌کنید یک وهله‌ی جدید از MustOwnImageRequirement در اینجا ثبت شده‌است. همین وهله به متد HandleRequirementAsync نیز ارسال می‌شود. بنابراین اگر نیاز به ارسال پارامترهای بیشتری به این متد وجود داشت، می‌توان خواص مرتبطی را به کلاس MustOwnImageRequirement نیز اضافه کرد.
همانطور که مشخص است، در اینجا یک نیازمندی را می‌توان ثبت کرد و نه Handler آن‌را. این Handler از سیستم تزریق وابستگی‌ها بر اساس آرگومان جنریک AuthorizationHandler پیاده سازی شده، به صورت خودکار یافت شده و اجرا می‌شود (بنابراین اگر Handler شما اجرا نشد، مطمئن شوید که حتما آن‌را به سیستم تزریق وابستگی‌ها معرفی کرده‌اید).

پس از آن هر کنترلر یا اکشن متدی که از این سیاست دسترسی پویای تهیه شده استفاده کند:
[Authorize(Policy ="MustOwnImage")]
به صورت خودکار توسط MustOwnImageHandler مدیریت می‌شود.


اعمال سیاست دسترسی پویای تعریف شده به Web API

پس از تعریف سیاست دسترسی MustOwnImage که پویا عمل می‌کند، اکنون نوبت به استفاده‌ی از آن در کنترلر ImageGallery.WebApi.WebApp\Controllers\ImagesController.cs است:
namespace ImageGallery.WebApi.WebApp.Controllers
{
    [Route("api/images")]
    [Authorize]
    public class ImagesController : Controller
    {
        [HttpGet("{id}", Name = "GetImage")]
        [Authorize("MustOwnImage")]
        public async Task<IActionResult> GetImage(Guid id)
        {
        }

        [HttpDelete("{id}")]
        [Authorize("MustOwnImage")]
        public async Task<IActionResult> DeleteImage(Guid id)
        {
        }

        [HttpPut("{id}")]
        [Authorize("MustOwnImage")]
        public async Task<IActionResult> UpdateImage(Guid id, [FromBody] ImageForUpdateModel imageForUpdate)
        {
        }
    }
}
در اینجا در سه قسمت GetImage ،DeleteImage و UpdateImage با اعمال سیاست دسترسی پویای MustOwnImage، اگر کاربر جاری همان Owner تصویر درخواستی نباشد، دسترسی او به اجرای کدهای داخل این اکشن متدها به صورت خودکار بسته خواهد شد.




کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشه‌ی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشه‌ی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آن‌را اجرا کنید تا برنامه‌ی IDP راه اندازی شود.
- در آخر به پوشه‌ی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحه‌ی login نام کاربری را User 1 و کلمه‌ی عبور آن‌را password وارد کنید.
مطالب
الگوی شیء نال Null Object Pattern
این الگو شاید به نظر ساده برسد، ولی در بعضی موارد می‌تواند در سطوح بالاتر، کدهای تمیزتر و خلوت‌تری را در اختیار شما بگذارد. در مورد این الگو، در کتاب توسعه چابک عمو باب نیز آمده است. بسیاری ممکن است نسبت به این الگو جبهه بگیرند و بگویند که بررسی نال بودن یک شیء بهتر است و یا حتی رخ دادن خطای Null Pointer Exception در برنامه باعث می‌شود بتوانیم باگ‌ها را پیدا کنیم. در جواب باید گفت که این الگو قرار نیست در همه جا مورد استفاده قرار گیرد. در مثال زیر می‌توانید تا حدی به جایگاه استفاده از این الگو برسید. اینکه چگونه و در کجا از یک الگو استفاده کنید، به عهده برنامه نویس است.
کار این الگو در یک جمله این است که اگر متدی نتواند خروجی مناسبی را بدهد و به جای آن قرار باشد نال را برگشت دهد، به جای برگشت دادن نال، از یک شیء که هیچ رفتاری ندارد استفاده می‌کند و آن شیء را برمی‌گرداند تا در ادامه کد، بررسی نال بودن، یا خطای NPE رخ ندهد.

به عنوان مثال فرض کنید قرار است یک کاربر با نام کاربری Ali به سیستم وارد شود؛ در اینجا سه حالت وجود دارد:
  1. این کاربر یافت شده و اجازه دسترسی دارد.
  2. این کاربر یافت شده و اجازه دسترسی ندارد.
  3. این کاربر یافت نمی‌شود.

اگر در حالتیکه کاربر یافت نشود، بخواهیم نال برگردانیم، در ادامه‌ی کد باید بررسی نال بودن و یا گاها انتظار خطای NPE را داشته باشیم؛ یا اینکه در عوض از الگوی شیء نال بهره ببریم.

بدون استفاده از الگو
در این مثال ابتدا کلاس یوزر را می‌سازیم:
 public class User
    {
        public String Usernam { get; set; }
        public bool Authenticated { get; set; }
    }
در لایه سرویس هم خروجی را برمی‌گردانیم:
 public User GeUser(string uname)
        {
            if (uname == "Ali")
            {
                return new User()
                {
                    Usernam = "Ali",
                    Authenticated = true
                };
            }
            else if (uname == "Reza")
            {
                return new User()
                {
                    Usernam = "Reza",
                    Authenticated = false
                };
            }
            else
            {
                return null;
            }
        }
در این حالت کد بعدی شما باید اینگونه باشد:
 var userServices=new UserServices();
            var user = userServices.GeUser("Ali");
            if (user != null && user.Authenticated)
            {
                Console.WriteLine("You are Authorized");
            }
همانطور که می‌بینید یک خط کد شرطی به سیستم اضافه شد و در یک سیستم بزرگتر این بررسی‌ها بیشتر شده و حتی در بعضی نقاط ممکن است با یک عدم بررسی، وقوع خطای NPE را افزایش دهید. حالا همین مثال بالا را با همین الگو جلو می‌بریم.

استفاده از الگو

ابتدا یک کلاس جدید را با ارث بری از کلاس یوزر می‌سازیم:
public class NullUser:User
    {
        public NullUser()
        {
            Authenticated = false;
        }
    }
این کلاس همان شیء نالی است که قرار است به جای خود عبارت Null برگشت دهیم:
  public User GeUser(string uname)
        {
            if (uname == "Ali")
            {
                return new User()
                {
                    Usernam = "Ali",
                    Authenticated = true
                };
            }
            else if (uname == "Reza")
            {
                return new User()
                {
                    Usernam = "Reza",
                    Authenticated = false
                };
            }
                return new NullUser();
        }
بدین ترتیب در ادامه کد الزامی به بررسی نال نیست:
var userServices=new UserServices();
            var user = userServices.GeUser("xxx");
            if (user.Authenticated)
                Console.WriteLine("You are Authorized");

یک نکته اضافه تر اینکه، در صورتی که قصد دارید متدی را در کلاس پدر تحریف کنید، بهتر است یک اینترفیس یا کلاس انتزاعی را تعریف و هر دو کلاس را از آن ارث بری کنید که برای مثال بالا می‌شود اینترفیس IUser و  دو کلاس User و NullUser هم مشتقات آن.
مطالب
C# 7.1 - default Literals
Literal چیزی است مانند null و در حقیقت یک واژه‌ی کلیدی‌است که دارای مقداری مشخص می‌باشد. واژه کلیدی default نیز مفهوم مشابهی را به همراه دارد. تا پیش از C# 7.1 برای دسترسی به مقدار پیش‌فرض value types به صورت ذیل عمل می‌شد:
 int a = default(int);
در اینجا مقدار پیش‌فرض نوعی که بین پرانتزها ذکر می‌شود، بازگشت داده خواهد شد. اگر int ذکر شود، صفر و اگر bool ذکر شود، مقدار false را بازگشت می‌دهد. همچنین در اینجا اگر یک reference type مانند string ذکر شود، مقدار null بازگشت داده خواهد شد.
var number = default(int); // 0
var date = default(DateTime); // DateTime.MinValue
var obj = default(object); // null
در C# 7.1 با بهبود کامپایلر، مفهوم type inference پیاده سازی شده‌است. به این معنا که در مثال فوق مشخص است که a نوع int دارد. بنابراین نیازی نیست تا default به همراه ذکر صریح int باشد و می‌توان int را از آن حذف کرد:
int a = default; // 0
Guid guid = default; // 00000000-0000-0000-0000-000000000000


مثال‌هایی از default Literals در C# 7.1

 C# 7.1
 C# 7.0
 
 int i = default;
 int i = default(int);
 Local Variable Defaults  
 Person Create() => default;
 Person Create() => default(Person);
 Local function 
 Person Create(string name, int age = default)
 Person Create(string name, int age = default(int))
 Optional Parameter Default Value 
 (string Name, int Age) person = ("User 1", default);
 (string Name, int Age) person = ("User 1", default(int));
 Tuple Element Default Value 
Person p = new Person
{
  Name = default,
  Age = default
};
Person p = new Person
{
  Name = default(string),
  Age = default(int)
};
 Object Initializer Default Value 
var people = new[]
{
  new Person(),
  default,
  new Person()
};

var ages = new[] {18, default, 50};
var people = new[]
{
  new Person(),
  default(Person),
  new Person()
};

var ages = new[] {18, default(int), 50};
 Array Initializer Default Value 
 int i = default;
Console.WriteLine(i is default);
 int i = default(int);
Console.WriteLine(i is default(int)); // true
 Is Operator 
 // Local method returning a tuple
(T, T) CreateTwo<T>() => (default, default);
 // Local method returning a tuple
(T, T) CreateTwo<T>() => (default(T), default(T));
 Generic Defaults 
 bool IsAnswerKnown()=> false;
int? p = IsAnswerKnown() ? 42 : default;
 bool IsAnswerKnown()=> false;
int? p = IsAnswerKnown() ? 42 : (int?)null;
 Conditional Operator Defaults 

در این مثال‌ها مفهوم type inference را بهتر می‌توان مشاهده کرد. برای مثال در آرایه‌ی ذیل چون اعضای آن int هست، مقدار default نیز به همان مقدار پیش‌فرض int اشاره می‌کند و همچنین نوع آرایه نیز int درنظر گرفته می‌شود و نیازی به ذکر آن نیست:
 var ages = new[] {18, default, 50};
اما اگر در اینجا اعداد را حذف کنیم و default باقی بماند:
 var ages = new[] { default };
دیگر تشخیص نوع پیش‌فرض میسر نبوده و این قطعه از کد کامپایل نخواهد شد.
نمونه‌ی دیگر آن قطعه کد ذیل است:
string s = default;
if(s == default)
{

}
در اینجا s از نوع string است و مقایسه‌ی انجام شدهی در قطعه کد if، بر اساس مقدار پیش فرض string یا همان null صورت خواهد گرفت.
و یا در مقایسه‌ی ذیل 1.5 یک عدد double است. بنابراین default در اینجا به مقدار پیش‌فرض double و یا 0.0 اشاره می‌کند:
int a = default;
var x = a > 0 ? default : 1.5;
مطالب
آموزش TypeScript #4
در پست‌های قبل با کلیات و primitive types در زبان TypeScript آشنا شدیم:

در این پست به مفاهیم شی گرایی در این زبان می‌پردازیم.

ماژول ها:
تعریف یک ماژول: برای تعریف یک ماژول باید از کلمه کلیدی module استفاده کنید. یک ماژول معادل یک ظرف است برای نگهداری کلاس‌ها و اینترفیس‌ها و سایر ماژول ها. کلاس‌ها و اینترفیس‌ها در TypeScript می‌توانند به صورت internal یا public باشند(به صورت پیش فرض internal است؛ یعنی فقط در همان ماژول قابل استفاده و فراخوانی است). هر چیزی که در داخل یک ماژول تعریف می‌شود محدوده آن در داخل آن ماژول خواهد بود. اگر قصد توسعه یک پروژه در مقیاس بزرگ را دارید می‌توانید همانند دات نت که در آن امکان تعریف فضای نام‌های تودرتو امکان پذیر است در TypeScript نیز، ماژول‌های تودرتو تعریف کنید.  برای مثال:
module MyModule1 {
    module  MyModule2 {
     }
}
اما به صورت معمول سعی می‌شود هر ماژول در یک فایل جداگانه تعریف شود. استفاده از چند ماژول در یک فایل به مرور، درک پروژه را سخت خواهد کرد و در هنگام توسعه امکان برخورد با مشکل وجود خواهد داشت. برای مثال اگر یک فایل به نام MyModule.ts داشته باشیم که یک ماژول به این نام را شامل شود بعد از کامپایل یک فایل به نام  MyModule.js ایجاد خواهد شد. 

کلاس ها:
برای تعریف یک کلاس می‌توانیم همانند دات نت از کلمه کلیدی class استفاده کنیم. بعد از تعریف کلاس می‌توانیم متغیر‌ها و توابع مورد نظر را در این کلاس قرار داده و تعریف کنیم.  
module Utilities {
   export class Logger {
      log(message: string): void{
       if(typeofwindow.console !== 'undefined') {
           window.console.log(message);
        }
      }
   }    
}
نکته مهم و جالب قسمت بالا کلمه export است. export معادل public در دات نت است و کلاس  logger را قابل دسترس در خارج ماژول Utilities خواهد کرد. اگر از export در هنگام تعریف کلاس استفاده نکنیم این کلاس فقط در سایر کلاس‌های تعریف شده در داخل همان ماژول قابل دسترس است.
تابع log  که در کلاس بالا تعریف کردیم به صورت پیش فرض public یا عمومی است و نیاز به استفاده export نیست.
برای استفاده از کلاس بالا باید این کلمه کلیدی new استفاده کنیم.  
window.onload = function() {
  varlogger = new Utilities.Logger();
  logger.log('Logger is loaded'); 
};
برای تعریف سازنده برای کلاس بالا باید از کلمه کلیدی constructor استفاده نماییم:
export class Logger{
constructor(private num: number) { 
}
با کمی دقت متوجه تعریف متغیر num به صورت private خواهید شد که برخلاف انتظار ما در زبان‌های دات نتی است. بر خلاف دات نت در زبان TypeScript، دسترسی به متغیر تعریف شده در سازنده با کمک اشاره گر this  در هر جای کلاس ممکن می‌باشد. در نتیجه نیازی به تعریف متغیر جدید و  پاس دادن مقادیر این متغیر‌ها به این فیلدها نمی‌باشد.
اگر به تابع log دقت کنید خواهید دید که یک پارامتر ورودی به نام message دارد که نوع آن string است. در ضمن Typescript از پارامتر‌های اختیاری( پارامتر با مقدار پیش فرض) نیز پشتیبانی می‌کند. مثال:

pad(num: number, len: number= 2, char: string= '0')
استفاده از پارامترهای Rest
منظور از پارامترهای Rest یعنی در هنگام فراخوانی توابع محدودیتی برای تعداد پارامتر‌ها نیست که معادل params در دات نت است. برای تعریف این گونه پارامترهاکافیست به جای params از ... استفاده نماییم.
function addManyNumbers(...numbers: number[]) {
  var sum = 0;
  for(var i = 0; i < numbers.length; i++) {
    sum += numbers[i];
 }
  returnsum;
}
var result = addManyNumbers(1,2,3,5,6,7,8,9);
تعریف توابع خصوصی
در TypeScript امکان توابع خصوصی با کلمه کلیدی private امکان پذیر است. همانند دات نت با استفاده از کلمه کلیدی private می‌توانیم کلاسی تعریف کنیم که فقط برای همان کلاس قابل دسترس باشد(به صورت پیش فرض توابع به صورت عمومی هستند).
module Utilities {
    Export class Logger {  
     log(message: string): void{
                 if(typeofwindow.console !== 'undefined') {   
                    window.console.log(this.getTimeStamp() + ' -'+ message);
                    window.console.log(this.getTimeStamp() + ' -'+ message); 
                }
        }
  private getTimeStamp(): string{
      var now = newDate();
      return now.getHours() + ':'+
      now.getMinutes() + ':'+
      now.getSeconds() + ':'+
      now.getMilliseconds();
  }
 }
}
از آن جا که تابع getTimeStamp به صورت خصوصی تعریف شده است در نتیجه امکان استفاده از آن در خارج کلاس وجود ندارد. اگر سعی بر استفاده این تابع داشته باشیم توسط کامپایلر با یک warning مواجه خواهیم شد.

یک نکته مهم این است که کلمه private فقط برای توابع و متغیر‌ها قابل استفاده است.

تعریف توابع static:

در TypeScript امکان تعریف توابع static وجود دارد. همانند دات نت باید از کلمه کلیدی static استفاده کنیم.

classFormatter {
static pad(num: number, len: number, char: string): string{
      var output = num.toString();
         while(output.length < len) {
         output = char + output;
      }
   returnoutput;
   }
  }
}
و استفاده از این تابع بدون وهله سازی از کلاس :
Formatter.pad(now.getSeconds(), 2, '0') +
Function Overload
همان گونه که در دات نت امکان overload کردن توابع میسر است در TypeScript هم این امکان وجود دارد.
static pad(num: number, len?: number, char?: string);
static pad(num: string, len?: number, char?: string);
static pad(num: any, len: number= 2, char: string= '0') {
 var output = num.toString();
 while(output.length < len) {
 output = char + output;
 }
 returnoutput;
}

ادامه دارد...
مطالب
امکان داشتن خروجی‌های Covariant در C# 9.0
در زبان #C، زمانیکه از کلاسی ارث‌بری می‌شود، امکان بازنویسی متدهای کلاس پایه، در صورت معرفی آن‌ها به صورت virtual و یا abstract، وجود دارد؛ اما در این بازنویسی‌ها، تغییر نوع خروجی این متدها مجاز نیست. این محدودیت در C# 9.0 با معرفی Covariant returns برداشته شده‌است؛ با یک شرط: نوع جدید این خروجی، باید covariant نوع اصلی متدی باشد که از کلاس پایه‌ی آن ارث‌بری شده‌است.


وضعیت خروجی متدهای بازنویسی شده تا پیش از C# 9.0

برای توضیح بهتر Covariant returns، نیاز است مثال زیر را بررسی کنیم:
public abstract class Product
{
  public string Name { get; set; }
  public abstract ProductOrder Order(int quantity);
}

public class Book : Product
{
  public string ISBN { get; set; }
  public override ProductOrder Order(int quantity) =>
new BookOrder { Quantity = quantity, Product = this };
}

public class ProductOrder
{
  public int Quantity { get; set; }
}

public class BookOrder : ProductOrder
{
  public Book Product { get; set; }
}
در اینجا یک کلاس abstract و پایه‌ی Product وجود دارد که می‌توان متد abstract سفارش دهی آن‌را در کلاس‌های مشتق شده‌ی از آن، مانند Book، بازنویسی کرد.
همانطور که مشاهده می‌کنید، در کلاس Book، تنها خروجی که برای متد Order بازنویسی شده می‌توان درنظر گرفت، همانی است که در کلاس پایه‌ی Product تعریف شده‌است و قابل تغییر نیست؛ یعنی همان ProductOrder.
همچنین در حین استفاده‌ی از این کلاس‌ها، تبدیل خروجی متد Order، به BookOrder ضروری است:
var book = new Book
{
  Name = "My book",
  ISBN = "11-1-12-22-0"
};
BookOrder orderBook = (BookOrder)book.Order(1);


امکان تغییر خروجی متدهای بازنویسی شده در C# 9.0

در C# 9.0 با مجاز اعلام شدن خروجی‌های covariant، می‌توان تغییرات زیر را به کدهای فوق اعمال کرد:
public class Book : Product
{
  public string ISBN { get; set; }
  public override BookOrder Order(int quantity) =>
      new BookOrder { Quantity = quantity, Product = this };
}
چون کلاس BookOrder از ProductOrder تعریف شده‌ی در کلاس پایه مشتق شده‌است (مفهوم covariant بودن خروجی متد)، می‌توان در C# 9.0 آن‌را به عنوان خروجی متد Order بازنویسی شده‌ی در کلاس Book، تنظیم کرد.
مزایای این ویژگی:
- داشتن یک خروجی مختص و متناسب با کلاس کتاب، مانند BookOrder؛ بجای ارائه‌ی یک خروجی بسیار عمومی ProductOrder.
- نیاز به کار با Generics را برای اینگونه اختصاصی سازی‌ها منتفی می‌کند.
- با این تغییر، دیگر نیازی به تبدیل نوع خروجی متد Order یک کتاب نیست و سطر سفارش دهی را می‌توان به صورت زیر خلاصه کرد:
BookOrder orderBook = book.Order(1);
مطالب
استفاده از لوسین برای برجسته سازی عبارت جستجو شده در نتایج حاصل
قسمت جستجوی سایت جاری رو با استفاده از لوسین بازنویسی کردم. خلاصه‌ای از نحوه انجام این‌کار رو در ادامه ملاحظه خواهید کرد:

1) دریافت کتابخانه‌های لازم
نیاز به کتابخانه‌های Lucene.NET و همچنین Lucene.Net Contrib است که هر دو مورد را به سادگی توسط NuGet می‌توانید دریافت و نصب کنید.
Highlighter استفاده شده، در کتابخانه Lucene.Net Contrib قرار دارد. به همین جهت این مورد را نیز باید جداگانه دریافت کرد.


2) تهیه منبع داده
در اینجا جهت سادگی کار فرض کنید که لیستی از مطالب را به فرمت زیر دراختیار داریم:
public class Post
{
    public int Id { set; get; }
    public string Title { set; get; }
    public string Body { set; get; }
}
تفاوتی نمی‌کند که از چه منبع داده‌ای استفاده می‌کنید. آیا قرار است یک سری فایل متنی ساده موجود در یک پوشه را ایندکس کنید یا تعدادی رکورد بانک اطلاعاتی؛ از NHibernate استفاده می‌کنید یا از Entity framework و یا از ADO.NET. کتابخانه Lucene مستقل است از منبع داده مورد استفاده و تنها اطلاعاتی با فرمت شیء Document معرفی شده به آن‌را می‌شناسد.


3) تبدیل اطلاعات به فرمت Lucene.NET
همانطور که عنوان شد نیاز است هر رکورد از اطلاعات خود را به شیء Document نگاشت کنیم. نمونه‌ای از اینکار را در متد ذیل مشاهده می‌نمائید:
static Document MapPostToDocument(Post post)
{
    var postDocument = new Document();
    postDocument.Add(new Field("Id", post.Id.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED));
    postDocument.Add(new Field("Title", post.Title, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS));
    postDocument.Add(new Field("Body", post.Body, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS));
    return postDocument;
}
این متد وهله‌ای از شیء Post را دریافت کرده و آن‌را تبدیل به یک سند Lucene می‌کند.
کار با ایجاد یک وهله از شیء Document شروع شده و سپس اطلاعات به صوت فیلدهایی به این سند اضافه می‌شوند.

توضیحات آرگومان‌های مختلف سازنده کلاس Field:
- در ابتدا نام فیلد مورد نظر ذکر می‌گردد.
- سپس مقدار متناظر با آن فیلد، به صورت رشته باید معرفی شود.
- آرگومان سوم آن مشخص می‌کند که اصل اطلاعات نیز علاوه بر ایندکس شدن باید در فایل‌های Lucene ذخیره شوند یا خیر. توسط Field.Store.YES مشخص می‌کنیم که بله؛ علاقمندیم تا اصل اطلاعات نیز از طریق Lucene قابل بازیابی باشند. این مورد جهت نمایش سریع نتایج جستجوها می‌تواند مفید باشد. اگر قرار نیست اطلاعاتی را از این فیلد خاص به کاربر نمایش دهید می‌توانید از گزینه Field.Store.NO استفاده کنید. همچنین امکان فشرده سازی اطلاعات ذخیره شده با انتخاب گزینه Field.Store.COMPRESS نیز میسر است.
- توسط آرگومان چهارم آن تعیین خواهیم کرد که اطلاعات فیلد مورد نظر ایندکس شوند یا خیر. مقدار Field.Index.NOT_ANALYZED سبب عدم ایندکس شدن فیلد Id می‌شوند (چون قرار نیست روی id در قسمت جستجوی عمومی سایت، جستجویی صورت گیرد). به کمک مقدار Field.Index.ANALYZED، مقدار معرفی شده، ایندکس خواهد شد.
- پارامتر پنجم آن‌را جهت سرعت عمل در نمایان سازی/برجسته کردن و highlighting عبارات جستجو شده در متن‌های یافت شده معرفی کرده‌ایم. الگوریتم‌های متناظر با این روش در فایل‌های Lucene.Net Contrib قرار دارند.


یک نکته
اگر اطلاعاتی را که قرار است ایندکس کنید از نوع HTML می‌باشند، بهتر است تمام تگ‌های آن‌را پیش از افزودن به لوسین حذف کنید. به این ترتیب نتایج جستجوی دقیق‌تری را می‌توان شاهد بود. برای این منظور می‌توان از متد ذیل کمک گرفت:
public static string RemoveHtmlTags(string text)
{
    return string.IsNullOrEmpty(text) ? string.Empty : Regex.Replace(text, @"<(.|\n)*?>", string.Empty);
}


4) تهیه Full text index به کمک Lucene.NET
تا اینجا توانستیم اطلاعات خود را به فرمت اسناد لوسین تبدیل کنیم. اکنون ثبت و تبدیل آن‌ها به فایل‌های Full text search لوسین به سادگی زیر است:
static readonly Lucene.Net.Util.Version _version = Lucene.Net.Util.Version.LUCENE_29;
public static void CreateIdx(IEnumerable<Post> dataList)
{
    var directory = FSDirectory.Open(new DirectoryInfo(Environment.CurrentDirectory + "\\LuceneIndex"));
    var analyzer = new StandardAnalyzer(_version);
    using (var writer = new IndexWriter(directory, analyzer, create: true, mfl: IndexWriter.MaxFieldLength.UNLIMITED))
    {
         foreach (var post in dataList)
        {
            writer.AddDocument(MapPostToDocument(post));
        }

        writer.Optimize();
        writer.Commit();
        writer.Close();
        directory.Close();
    }
}
ابتدا محل ذخیره سازی فایل‌های full text search مشخص می‌شوند. سپس آنالیز کننده اطلاعات باید معرفی شود. در ادامه به کمک این اطلاعات، شیء IndexWriter ایجاد و مستندات لوسین به آن اضافه می‌شوند. در آخر، این اطلاعات بهینه سازی شده و ثبت نهایی صورت خواهد گرفت.
ذکر version در اینجا ضروری است؛ از این جهت که اگر ایندکسی با فرمت مثلا LUCENE_29 تهیه شود ممکن است با نگارش بعدی این کتابخانه سازگار نباشد و در صورت ارتقاء، نتایج جستجوی انجام شده، کاملا بی‌ربط نمایش داده شوند. با ذکر صریح نگارش، دیگر این اتفاق رخ نخواهد داد.


نکته
StandardAnalyzer توکار لوسین، امکان دریافت لیستی از واژه‌هایی که نباید ایندکس شوند را نیز دارا است. اطلاعات بیشتر در اینجا.


5) به روز رسانی ایندکس‌ها
به کمک سه متد ذیل می‌توان اطلاعات ایندکس‌های موجود را به روز یا حذف کرد:
public static void UpdateIndex(Post post)
{
        var directory = FSDirectory.Open(new DirectoryInfo(Environment.CurrentDirectory + "\\LuceneIndex"));
        var analyzer = new StandardAnalyzer(_version);
        using (var indexWriter = new IndexWriter(directory, analyzer, create: false, mfl: IndexWriter.MaxFieldLength.UNLIMITED))
        {
            var newDoc = MapPostToDocument(post);

             indexWriter.UpdateDocument(new Term("Id", post.Id.ToString()), newDoc);
             indexWriter.Commit();
             indexWriter.Close();
             directory.Close();
         }
}

public static void DeleteIndex(Post post)
{
         var directory = FSDirectory.Open(new DirectoryInfo(Environment.CurrentDirectory + "\\LuceneIndex"));
         var analyzer = new StandardAnalyzer(_version);
         using (var indexWriter = new IndexWriter(directory, analyzer, create: false, mfl: IndexWriter.MaxFieldLength.UNLIMITED))
         {
             indexWriter.DeleteDocuments(new Term("Id", post.Id.ToString()));
             indexWriter.Commit();
             indexWriter.Close();
             directory.Close();
          }
}

public static void AddIndex(Post post)
{
      var directory = FSDirectory.Open(new DirectoryInfo(Environment.CurrentDirectory + "\\LuceneIndex"));
      var analyzer = new StandardAnalyzer(_version, getStopWords());
      using (var indexWriter = new IndexWriter(directory, analyzer, create: false, mfl: IndexWriter.MaxFieldLength.UNLIMITED))
      {
           var searchQuery = new TermQuery(new Term("Id", post.Id.ToString()));
           indexWriter.DeleteDocuments(searchQuery);

            var newDoc = MapPostToDocument(post);
            indexWriter.AddDocument(newDoc);
            indexWriter.Commit();
            indexWriter.Close();
            directory.Close();
        }
}
تنها نکته مهم این متدها، استفاده از متد IndexWriter با پارامتر create مساوی false است. به این ترتیب فایل‌های موجود بجای از نو ساخته شدن، به روز خواهند شد.
محل فراخوانی این متدها هم می‌تواند در کنار متدهای به روز رسانی اطلاعات اصلی در بانک اطلاعاتی برنامه باشند. اگر رکوردی اضافه یا حذف شده، ایندکس متناظر نیز باید به روز شود.


6) جستجو در اطلاعات ایندکس شده و نمایش آن‌ها به همراه نمایان/برجسته سازی عبارات جستجو شده
قسمت نهایی کار با لوسین و اطلاعات ایندکس‌های تهیه شده، کوئری گرفتن از آن‌ها است. متدهای کامل مورد نیاز را در ذیل مشاهده می‌کنید:

public static void Query(string term)
{
     var directory = FSDirectory.Open(new DirectoryInfo(Environment.CurrentDirectory + "\\LuceneIndex"));
     using (var searcher = new IndexSearcher(directory, readOnly: true))
     {
          var analyzer = new StandardAnalyzer(_version);
          var parser = new MultiFieldQueryParser(_version, new[] { "Body", "Title" }, analyzer);
          var query = parseQuery(term, parser);
          var hits = searcher.Search(query, 10).ScoreDocs;

          if (hits.Length == 0)
          {
               term = searchByPartialWords(term);
               query = parseQuery(term, parser);
               hits = searcher.Search(query, 10).ScoreDocs;
           }

           FastVectorHighlighter fvHighlighter = new FastVectorHighlighter(true, true);
           foreach (var scoreDoc in hits)
           {
               var doc = searcher.Doc(scoreDoc.doc);
               string bestfragment = fvHighlighter.GetBestFragment(
                                fvHighlighter.GetFieldQuery(query),
                                searcher.GetIndexReader(),
                                docId: scoreDoc.doc,
                                fieldName: "Body",
                                fragCharSize: 400);
                var id = doc.Get("Id");
                var title = doc.Get("Title");
                var score = scoreDoc.score;
                Console.WriteLine(bestfragment);
            }

            searcher.Close();
            directory.Close();
      }
   }

   private static Query parseQuery(string searchQuery, QueryParser parser)
   {
       Query query;
        try
        {
            query = parser.Parse(searchQuery.Trim());
        }
        catch (ParseException)
        {
            query = parser.Parse(QueryParser.Escape(searchQuery.Trim()));
        }
        return query;
   }

   private static string searchByPartialWords(string bodyTerm)
   {
       bodyTerm = bodyTerm.Replace("*", "").Replace("?", "");
       var terms = bodyTerm.Trim().Replace("-", " ").Split(' ')
                                .Where(x => !string.IsNullOrEmpty(x))
                                .Select(x => x.Trim() + "*");
       bodyTerm = string.Join(" ", terms);
       return bodyTerm;
   }
توضیحات:
اکثر سایت‌ها را که بررسی کنید، جستجوی بر روی یک فیلد را توضیح داده‌اند. در اینجا نحوه جستجو بر روی چند فیلد را به کمک MultiFieldQueryParser ملاحظه می‌کنید.
نکته‌ی مهمی را هم که در اینجا باید به آن دقت داشت، حساس بودن لوسین به کوچکی و بزرگی نام فیلدهای معرفی شده است و در صورت عدم رعایت این مساله، جستجوی شما نتیجه‌ای را دربر نخواهد داشت.
در ادامه برای parse اطلاعات، از متد کمکی parseQuery استفاده شده است. ممکن است به ParseException بخاطر یک سری حروف خاص بکارگرفته شده در عبارات مورد جستجو برسیم. در اینجا می‌توان توسط متد QueryParser.Escape، اطلاعات دریافتی را اصلاح کرد.
سپس نحوه استفاده از کوئری تهیه شده و متد Search را ملاحظه می‌کنید. در اینجا بهتر است تعداد رکوردهای بازگشت داده شده را تعیین کرد (به کمک آرگومان دوم متد جستجو) تا بی‌جهت سرعت عملیات را پایین نیاورده و همچنین مصرف حافظه سیستم را نیز بالا نبریم.
ممکن است تعداد hits یا نتایج حاصل صفر باشد؛ بنابراین بد نیست خودمان دست به کار شده و به کمک متد searchByPartialWords، ورودی کاربر را بر اساس زبان جستجوی ویژه لوسین اندکی بهینه کنیم تا بتوان به نتایج بهتری دست یافت.
در آخر نحوه کار با  ScoreDocs یافت شده را ملاحظه می‌کنید. اگر محتوای فیلد را در حین ایندکس سازی ذخیره کرده باشیم، به کمک متد doc.Get می‌توان به اطلاعات کامل آن نیز دست یافت.
همچنین نکته دیگری را که در اینجا می‌توان ملاحظه کرد استفاده از FastVectorHighlighter می‌باشد. به کمک این Highlighter ویژه می‌توان نتایج جستجو را شبیه به نتایج نمایش داده شده توسط موتور جستجوی گوگل درآورد. برای مثال اگر شخصی ef code first را جستجو کرد، توسط متد GetBestFragment، بهترین جزئی که شامل بیشترین تعداد حروف جستجو شده است، یافت گردیده و همچنین به کمک تگ‌های B، ضخیم نمایش داده خواهند شد.

 
مطالب
آشنایی با NHibernate - قسمت چهارم

در این قسمت یک مثال ساده از insert ، load و delete را بر اساس اطلاعات قسمت‌های قبل با هم مرور خواهیم کرد. برای سادگی کار از یک برنامه Console استفاده خواهد شد (هر چند مرسوم شده است که برای نوشتن آزمایشات از آزمون‌های واحد بجای این نوع پروژه‌ها استفاده شود). همچنین فرض هم بر این است که database schema برنامه را مطابق قسمت قبل در اس کیوال سرور ایجاد کرده اید (نکته آخر بحث قسمت سوم).

یک پروژه جدید از نوع کنسول را به solution برنامه (همان NHSample1 که در قسمت‌های قبل ایجاد شد)، اضافه نمائید.
سپس ارجاعاتی را به اسمبلی‌های زیر به آن اضافه کنید:
FluentNHibernate.dll
NHibernate.dll
NHibernate.ByteCode.Castle.dll
NHSample1.dll : در قسمت‌های قبل تعاریف موجودیت‌ها و نگاشت‌ آن‌ها را در این پروژه class library ایجاد کرده بودیم و اکنون قصد استفاده از آن را داریم.

اگر دیتابیس قسمت قبل را هنوز ایجاد نکرده‌اید، کلاس CDb را به برنامه افزوده و سپس متد CreateDb آن‌را به برنامه اضافه نمائید.

using FluentNHibernate;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHSample1.Mappings;

namespace ConsoleTestApplication
{
class CDb
{
public static void CreateDb(IPersistenceConfigurer dbType)
{
var cfg = Fluently.Configure().Database(dbType);

PersistenceModel pm = new PersistenceModel();
pm.AddMappingsFromAssembly(typeof(CustomerMapping).Assembly);
var sessionSource = new SessionSource(
cfg.BuildConfiguration().Properties,
pm);

var session = sessionSource.CreateSession();
sessionSource.BuildSchema(session, true);
}
}
}
اکنون برای ایجاد دیتابیس اس کیوال سرور بر اساس نگاشت‌های قسمت قبل، تنها کافی است دستور ذیل را صادر کنیم:

CDb.CreateDb(
MsSqlConfiguration
.MsSql2008
.ConnectionString("Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true")
.ShowSql());

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

در ادامه یک کلاس جدید به نام Config را به برنامه کنسول ایجاد شده اضافه کنید:

using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using NHSample1.Mappings;

namespace ConsoleTestApplication
{
class Config
{
public static ISessionFactory CreateSessionFactory(IPersistenceConfigurer dbType)
{
return
Fluently.Configure().Database(dbType
).Mappings(m => m.FluentMappings.AddFromAssembly(typeof(CustomerMapping).Assembly))
.BuildSessionFactory();
}
}
}
اگر بحث را دنبال کرده باشید، این کلاس را پیشتر در کلاس FixtureBase آزمون واحد خود، به نحوی دیگر دیده بودیم. برای کار با NHibernate‌ نیاز به یک سشن مپ شده به موجودیت‌های برنامه می‌باشد که توسط متد CreateSessionFactory کلاس فوق ایجاد خواهد شد. این متد را به این جهت استاتیک تعریف کرده‌ایم که هیچ نوع وابستگی به کلاس جاری خود ندارد. در آن نوع دیتابیس مورد استفاده ( برای مثال اس کیوال سرور 2008 یا هر مورد دیگری که مایل بودید)، به همراه اسمبلی حاوی اطلاعات نگاشت‌های برنامه معرفی شده‌اند.

اکنون سورس کامل مثال برنامه را در نظر بگیرید:

کلاس CDbOperations جهت اعمال ثبت و حذف اطلاعات:

using System;
using NHibernate;
using NHSample1.Domain;

namespace ConsoleTestApplication
{
class CDbOperations
{
ISessionFactory _factory;

public CDbOperations(ISessionFactory factory)
{
_factory = factory;
}

public int AddNewCustomer()
{
using (ISession session = _factory.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
Customer vahid = new Customer()
{
FirstName = "Vahid",
LastName = "Nasiri",
AddressLine1 = "Addr1",
AddressLine2 = "Addr2",
PostalCode = "1234",
City = "Tehran",
CountryCode = "IR"
};

Console.WriteLine("Saving a customer...");

session.Save(vahid);
session.Flush();//چندین عملیات با هم و بعد

transaction.Commit();

return vahid.Id;
}
}
}

public void DeleteCustomer(int id)
{
using (ISession session = _factory.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
Customer customer = session.Load<Customer>(id);
Console.WriteLine("Id:{0}, Name: {1}", customer.Id, customer.FirstName);

Console.WriteLine("Deleting a customer...");
session.Delete(customer);

session.Flush();//چندین عملیات با هم و بعد

transaction.Commit();
}
}
}
}
}
و سپس استفاده از آن در برنامه

using System;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using NHSample1.Domain;

namespace ConsoleTestApplication
{
class Program
{
static void Main(string[] args)
{
//CDb.CreateDb(SQLiteConfiguration.Standard.ConnectionString("data source=sample.sqlite").ShowSql());
//return;

//todo: Read ConnectionString from app.config or web.config
using (ISessionFactory session = Config.CreateSessionFactory(
MsSqlConfiguration
.MsSql2008
.ConnectionString("Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true")
.ShowSql()
))
{
CDbOperations db = new CDbOperations(session);
int id = db.AddNewCustomer();
Console.WriteLine("Loading a customer and delete it...");
db.DeleteCustomer(id);
}

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
توضیحات:
نیاز است تا ISessionFactory را برای ساخت سشن‌های دسترسی به دیتابیس ذکر شده در تنظمیات آن جهت استفاده در تمام تردهای برنامه، ایجاد نمائیم. لازم به ذکر است که تا قبل از فراخوانی BuildSessionFactory این تنظیمات باید معرفی شده باشند و پس از آن دیگر اثری نخواهند داشت.
ایجاد شیء ISessionFactory هزینه بر است و گاهی بر اساس تعداد کلاس‌هایی که باید مپ شوند، ممکن است تا چند ثانیه به طول انجامد. به همین جهت نیاز است تا یکبار ایجاد شده و بارها مورد استفاده قرار گیرد. در برنامه به کرات از using استفاده شده تا اشیاء IDisposable را به صورت خودکار و حتمی، معدوم نماید.

بررسی متد AddNewCustomer :
در ابتدا یک سشن را از ISessionFactory موجود درخواست می‌کنیم. سپس یکی از بهترین تمرین‌های کاری جهت کار با دیتابیس‌ها ایجاد یک تراکنش جدید است تا اگر در حین اجرای کوئری‌ها مشکلی در سیستم، سخت افزار و غیره پدید آمد، دیتابیسی ناهماهنگ حاصل نشود. زمانیکه از تراکنش استفاده شود، تا هنگامیکه دستور transaction.Commit آن با موفقیت به پایان نرسیده باشد، اطلاعاتی در دیتابیس تغییر نخواهد کرد و از این لحاظ استفاده از تراکنش‌ها جزو الزامات یک برنامه اصولی است.
در ادامه یک وهله از شیء Customer را ایجاد کرده و آن‌را مقدار دهی می‌کنیم (این شیء در قسمت‌های قبل ایجاد گردید). سپس با استفاده از session.Save دستور ثبت را صادر کرده، اما تا زمانیکه transaction.Commit فراخوانی و به پایان نرسیده باشد، اطلاعاتی در دیتابیس ثبت نخواهد شد.
نیازی به ذکر سطر فلاش در این مثال نبود و NHibernate اینکار را به صورت خودکار انجام می‌دهد و فقط از این جهت عنوان گردید که اگر چندین عملیات را با هم معرفی کردید، استفاده از session.Flush سبب خواهد شد که رفت و برگشت‌ها به دیتابیس حداقل شود و فقط یکبار صورت گیرد.
در پایان این متد، Id ثبت شده در دیتابیس بازگشت داده می‌شود.

چون در متد CreateSessionFactory ، متد ShowSql را نیز ذکر کرده بودیم، هنگام اجرای برنامه، عبارات SQL ایی که در پشت صحنه توسط NHibernate تولید می‌شوند را نیز می‌توان مشاهده نمود:



بررسی متد DeleteCustomer :
ایجاد سشن و آغاز تراکنش آن همانند متد AddNewCustomer است. سپس در این سشن، یک شیء از نوع Customer با Id ایی مشخص load‌ خواهد گردید. برای نمونه، نام این مشتری نیز در کنسول نمایش داده می‌شود. سپس این شیء مشخص و بارگذاری شده را به متد session.Delete ارسال کرده و پس از فراخوانی transaction.Commit ، این مشتری از دیتابیس حذف می‌شود.

برای نمونه خروجی SQL پشت صحنه این عملیات که توسط NHibernate مدیریت می‌شود، به صورت زیر است:

Saving a customer...
NHibernate: select next_hi from hibernate_unique_key with (updlock, rowlock)
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 17, @p1 = 16
NHibernate: INSERT INTO [Customer] (FirstName, LastName, AddressLine1, AddressLine2, PostalCode, City, CountryCode, Id) VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7);@p0 = 'Vahid', @p1 = 'Nasiri', @p2 = 'Addr1', @p3 = 'Addr2', @p4 = '1234', @p5 = 'Tehran', @p6 = 'IR', @p7 = 16016
Loading a customer and delete it...
NHibernate: SELECT customer0_.Id as Id2_0_, customer0_.FirstName as FirstName2_0_, customer0_.LastName as LastName2_0_, customer0_.AddressLine1 as AddressL4_2_0_, customer0_.AddressLine2 as AddressL5_2_0_, customer0_.PostalCode as PostalCode2_0_, customer0_.City as City2_0_, customer0_.CountryCode as CountryC8_2_0_ FROM [Customer] customer0_ WHERE customer0_.Id=@p0;@p0 = 16016
Id:16016, Name: Vahid
Deleting a customer...
NHibernate: DELETE FROM [Customer] WHERE Id = @p0;@p0 = 16016
Press a key...
استفاده از دیتابیس SQLite بجای SQL Server در مثال فوق:

فرض کنید از هفته آینده قرار شده است که نسخه سبک و تک کاربره‌ای از برنامه ما تهیه شود. بدیهی است SQL server برای این منظور انتخاب مناسبی نیست (هزینه بالا برای یک مشتری، مشکلات نصب، مشکلات نگهداری و امثال آن برای یک کاربر نهایی و نه یک سازمان بزرگ که حتما ادمینی برای این مسایل در نظر گرفته می‌شود).
اکنون چه باید کرد؟ باید برنامه را از صفر بازنویسی کرد یا قسمت دسترسی به داده‌های آن‌را کلا مورد باز بینی قرار داد؟ اگر برنامه اسپاگتی ما اصلا لایه دسترسی به داده‌ها را نداشت چه؟! همه جای برنامه پر است از SqlCommand و Open و Close ! و عملا استفاده از یک دیتابیس دیگر یعنی باز نویسی کل برنامه.
همانطور که ملاحظه می‌کنید، زمانیکه با NHibernate کار شود، مدیریت لایه دسترسی به داده‌ها به این فریم ورک محول می‌شود و اکنون برای استفاده از دیتابیس SQLite تنها باید تغییرات زیر صورت گیرد:
ابتدا ارجاعی را به اسمبلی System.Data.SQLite.dll اضافه نمائید (تمام این اسمبلی‌های ذکر شده به همراه مجموعه FluentNHibernate ارائه می‌شوند). سپس:
الف) ایجاد یک دیتابیس خام بر اساس کلاس‌های domain و mapping تعریف شده در قسمت‌های قبل به صورت خودکار

CDb.CreateDb(SQLiteConfiguration.Standard.ConnectionString("data source=sample.sqlite").ShowSql());
ب) تغییر آرگومان متد CreateSessionFactory

//todo: Read ConnectionString from app.config or web.config
using (ISessionFactory session = Config.CreateSessionFactory(
SQLiteConfiguration.Standard.ConnectionString("data source=sample.sqlite").ShowSql()
))
{
...

نمایی از دیتابیس SQLite تشکیل شده پس از اجرای متد قسمت الف ، در برنامه Lita :




دریافت سورس برنامه تا این قسمت

نکته:
در سه قسمت قبل، تمام خواص پابلیک کلاس‌های پوشه domain را به صورت معمولی و متداول معرفی کردیم. اگر نیاز به lazy loading در برنامه وجود داشت، باید تمامی کلاس‌ها را ویرایش کرده و واژه کلیدی virtual را به کلیه خواص پابلیک آن‌ها اضافه کرد. علت هم این است که برای عملیات lazy loading ، فریم ورک NHibernate باید یک سری پروکسی را به صورت خودکار جهت کلاس‌های برنامه ایجاد نماید و برای این امر نیاز است تا بتواند این خواص را تحریف (override) کند. به همین جهت باید آن‌ها را به صورت virtual تعریف کرد. همچنین تمام سطرهای Not.LazyLoad نیز باید حذف شوند.

ادامه دارد ...


اشتراک‌ها
نحوه ی کار GC در دات نت

تو این ویدیو سعی کردم کلا در مورد GC توضیح بدم که چطور کارمیکنه و مباحثی مثل

  • Close Connection vs Dispose
  • Try finally block
  • Using keyword
  • When GC starts to work
  • How garbage collection works
  • Generation 0, Generation 1, Generation 2
  • Finalizer
  • Object Resurrection
رو کامل توضیح بدم که چی هستند.

نحوه ی کار GC در دات نت
نظرات مطالب
نوشتن Middleware سفارشی در ASP.NET Core
یکی از روش‌های مقابله با مشکل فوق استفاده از کلاس SemaphoreSlim می باشد که در NET Framework 4.0 معرفی شده و در فضای نام  System.Threading در دسترس می‌باشد.
اگر اکشن متد‌های شما به صورت async await ایجاد کرده اید بهتر هست  ابتدا کلاس زیر را ایجاد نمایید:
using System;
using System.Threading;
using System.Threading.Tasks;
 
namespace MyApp
{
    public class AsyncLock : IDisposable
    {
        private SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1, 1);
 
        public async Task<AsyncLock> LockAsync()
        {
            await _semaphoreSlim.WaitAsync();
            return this;
        }
 
        public void Dispose()
        {
            _semaphoreSlim.Release();
        }
    }
}
سپس به صورت زیر از آن استفاده کنید:
private static readonly AsyncLock _mutex = new AsyncLock();
 
using(await _mutex.LockAsync())
{
    // Critical section... You can await here!
}
در این صورت تمامی درخواست‌های به سمت سرور به ترتیب اجرا خواهند شد و دیگر مشکل فوق را نخواهیم داشت.