مطالب
الگوهای طراحی API - مکانیزم جلوگیری از پردازش تکراری درخواست ها - Request Deduplication

در فضایی که همواره هیچ تضمینی وجود ندارد که درخواست ارسال شده‌ی به یک API، همواره مسیر خود را همانطور که انتظار می‌رود طی کرده و پاسخ مورد نظر را در اختیار ما قرار می‌دهد، بی‌شک تلاش مجدد برای پردازش درخواست مورد نظر، به دلیل خطاهای گذرا، یکی از راهکارهای مورد استفاده خواهد بود. تصور کنید قصد طراحی یک مجموعه API عمومی را دارید، به‌نحوی که مصرف کنندگان بدون نگرانی از ایجاد خرابی یا تغییرات ناخواسته، امکان تلاش مجدد در سناریوهای مختلف مشکل در ارتباط با سرور را داشته باشند. حتما توجه کنید که برخی از متدهای HTTP مانند GET، به اصطلاح Idempotent هستند و در طراحی آنها همواره باید این موضوع مدنظر قرار بگیرد و خروجی مشابهی برای درخواست‌های تکراری همانند، مهیا کنید.

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

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

Idempotent and Safe HTTP Methods

طبق HTTP RFC، متدهایی که پاسخ یکسانی را برای درخواست‌های همسان مهیا می‌کنند، به اصطلاح Idempotent هستند. همچنین متدهایی که باعث نشوند تغییری در وضعیت سیستم در سمت سرور ایجاد شود، به اصطلاح Safe در نظر گرفته خواهند شد. برای هر دو خصوصیت عنوان شده، سناریوهای استثناء و قابل بحثی وجود دارند؛ به‌عنوان مثال در مورد خصوصیت Safe بودن، درخواست GET ای را تصور کنید که یکسری لاگ آماری هم ثبت می‌کند یا عملیات بازنشانی کش را نیز انجام می‌دهد که در خیلی از موارد به عنوان یک قابلیت شناسایی خواهد شد. در این سناریوها و طبق RFC، باتوجه به اینکه هدف مصرف کننده، ایجاد Side-effect نبوده‌است، هیچ مسئولیتی در قبال این تغییرات نخواهد داشت. لیست زیر شامل متدهای مختلف HTTP به همراه دو خصوصیت ذکر شده می باشد:

HTTP MethodSafeIdempotent
GETYesYes
HEADYesYes
OPTIONSYesYes
TRACEYesYes
PUTNoYes
DELETENoYes
POSTNoNo
PATCHNoNo

Request Identifier as a Solution

راهکاری که عموما مورد استفاده قرار می‌گیرد، استفاده از یک شناسه‌ی یکتا برای درخواست ارسالی و ارسال آن به سرور از طریق هدر HTTP می باشد. تصویر زیر از کتاب API Design Patterns، روش استفاده و مراحل جلوگیری از پردازش درخواست تکراری با شناسه‌ای همسان را نشان می‌دهد:

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

Implementation in .NET

ممکن است پیاده‌سازی‌های مختلفی را از این الگوی طراحی در اینترنت مشاهده کنید که به پیاده سازی یک Middleware بسنده کرده‌اند و صرفا بررسی این مورد که درخواست جاری قبلا دریافت شده‌است یا خیر را جواب می دهند که ناقص است. برای اینکه اطمینان حاصل کنیم درخواست مورد نظر دریافت و پردازش شده‌است، باید در منطق عملیات مورد نظر دست برده و تغییراتی را اعمال کنیم. برای این منظور فرض کنید در بستری هستیم که می توانیم از مزایای خصوصیات ACID دیتابیس رابطه‌ای مانند SQLite استفاده کنیم. ایده به این شکل است که شناسه درخواست دریافتی را در تراکنش مشترک با عملیات اصلی ذخیره کنیم و در صورت بروز هر گونه خطا در اصل عملیات، کل تغییرات برگشت خورده و کلاینت امکان تلاش مجدد با شناسه‌ی مورد نظر را داشته باشد. برای این منظور مدل زیر را در نظر بگیرید:

public class IdempotentId(string id, DateTime time)
{
    public string Id { get; private init; } = id;
    public DateTime Time { get; private init; } = time;
}

هدف از این موجودیت ثبت و نگهداری شناسه‌های درخواست‌های دریافتی می‌باشد. در ادامه واسط IIdempotencyStorage را برای مدیریت نحوه ذخیره سازی و پاکسازی شناسه‌های دریافتی خواهیم داشت:

public interface IIdempotencyStorage
{
    Task<bool> TryPersist(string idempotentId, CancellationToken cancellationToken);
    Task CleanupOutdated(CancellationToken cancellationToken);
    bool IsKnownException(Exception ex);
}

در اینجا متد TryPersist سعی می‌کند با شناسه دریافتی یک رکورد را ثبت کند و اگر تکراری باشد، خروجی false خواهد داشت. متد CleanupOutdated برای پاکسازی شناسه‌هایی که زمان مشخصی (مثلا ۱۲ ساعت) از دریافت آنها گذشته است، استفاده خواهد شد که توسط یک وظیفه‌ی زمان‌بندی شده می تواند اجرا شود؛ به این صورت، امکان استفاده‌ی مجدد از آن شناسه‌ها برای کلاینت‌ها مهیا خواهد شد. پیاده سازی واسط تعریف شده، به شکل زیر خواهد بود:

/// <summary>
/// To prevent from race-condition, this default implementation relies on primary key constraints.
/// </summary>
file sealed class IdempotencyStorage(
    AppDbContext dbContext,
    TimeProvider dateTime,
    ILogger<IdempotencyStorage> logger) : IIdempotencyStorage
{
    private const string ConstraintName = "PK_IdempotentId";

    public Task CleanupOutdated(CancellationToken cancellationToken)
    {
        throw new NotImplementedException(); //TODO: cleanup the outdated ids based on configurable duration
    }

    public bool IsKnownException(Exception ex)
    {
        return ex is UniqueConstraintException e && e.ConstraintName.Contains(ConstraintName);
    }

    // To tackle race-condition issue, the implementation relies on storage capabilities, such as primary constraint for given IdempotentId.
    public async Task<bool> TryPersist(string idempotentId, CancellationToken cancellationToken)
    {
        try
        {
            dbContext.Add(new IdempotentId(idempotentId, dateTime.GetUtcNow().UtcDateTime));
            await dbContext.SaveChangesAsync(cancellationToken);

            return true;
        }
        catch (UniqueConstraintException e) when (e.ConstraintName.Contains(ConstraintName))
        {
            logger.LogInformation(e, "The given idempotentId [{IdempotentId}] already exists in the storage.", idempotentId);
            return false;
        }
    }
}

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

در این پیاده سازی از کتابخانه MediatR استفاده می کنیم؛ در همین راستا برای مدیریت تراکنش ها به صورت زیر می توان TransactionBehavior را پیاده سازی کرد:

internal sealed class TransactionBehavior<TRequest, TResponse>(
    AppDbContext dbContext,
    ILogger<TransactionBehavior<TRequest, TResponse>> logger) :
    IPipelineBehavior<TRequest, TResponse>
    where TRequest : IBaseCommand
    where TResponse : IErrorOr
{
    public async Task<TResponse> Handle(
        TRequest command,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        string commandName = typeof(TRequest).Name;
        await using var transaction = await dbContext.Database.BeginTransactionAsync(IsolationLevel.ReadCommitted, cancellationToken);

        TResponse? result;
        try
        {
            logger.LogInformation("Begin transaction {TransactionId} for handling {CommandName} ({@Command})", transaction.TransactionId, commandName, command);

            result = await next();
            if (result.IsError)
            {
                await transaction.RollbackAsync(cancellationToken);

                logger.LogInformation("Rollback transaction {TransactionId} for handling {CommandName} ({@Command}) due to failure result.", transaction.TransactionId, commandName, command);

                return result;
            }

            await transaction.CommitAsync(cancellationToken);

            logger.LogInformation("Commit transaction {TransactionId} for handling {CommandName} ({@Command})", transaction.TransactionId, commandName, command);
        }
        catch (Exception ex)
        {
            await transaction.RollbackAsync(cancellationToken);

            logger.LogError(ex, "An exception occured within transaction {TransactionId} for handling {CommandName} ({@Command})", transaction.TransactionId, commandName, command);

            throw;
        }

        return result;
    }
}

در اینجا مستقیما AppDbContext تزریق شده و با استفاده از خصوصیت Database آن، کار مدیریت تراکنش انجام شده‌است. همچنین باتوجه به اینکه برای مدیریت خطاها از کتابخانه‌ی ErrorOr استفاده می کنیم و خروجی همه‌ی Command های سیستم، حتما یک وهله از کلاس ErrorOr است که واسط IErrorOr را پیاده سازی کرده‌است، یک محدودیت روی تایپ جنریک اعمال کردیم که این رفتار، فقط برروی IBaseCommand ها اجرا شود. تعریف واسط IBaseCommand به شکل زیر می‌باشد:

 
/// <summary>
/// This is marker interface which is used as a constraint of behaviors.
/// </summary>
public interface IBaseCommand
{
}

public interface ICommand : IBaseCommand, IRequest<ErrorOr<Unit>>
{
}

public interface ICommand<T> : IBaseCommand, IRequest<ErrorOr<T>>
{
}

public interface ICommandHandler<in TCommand> : IRequestHandler<TCommand, ErrorOr<Unit>>
    where TCommand : ICommand
{
    Task<ErrorOr<Unit>> IRequestHandler<TCommand, ErrorOr<Unit>>.Handle(TCommand request, CancellationToken cancellationToken)
    {
        return Handle(request, cancellationToken);
    }

    new Task<ErrorOr<Unit>> Handle(TCommand command, CancellationToken cancellationToken);
}

public interface ICommandHandler<in TCommand, T> : IRequestHandler<TCommand, ErrorOr<T>>
    where TCommand : ICommand<T>
{
    Task<ErrorOr<T>> IRequestHandler<TCommand, ErrorOr<T>>.Handle(TCommand request, CancellationToken cancellationToken)
    {
        return Handle(request, cancellationToken);
    }

    new Task<ErrorOr<T>> Handle(TCommand command, CancellationToken cancellationToken);
}

در ادامه برای پیاده‌سازی IdempotencyBehavior و محدود کردن آن، واسط IIdempotentCommand را به شکل زیر خواهیم داشت:

/// <summary>
/// This is marker interface which is used as a constraint of behaviors.
/// </summary>
public interface IIdempotentCommand
{
    string IdempotentId { get; }
}

public abstract class IdempotentCommand : ICommand, IIdempotentCommand
{
    public string IdempotentId { get; init; } = string.Empty;
}

public abstract class IdempotentCommand<T> : ICommand<T>, IIdempotentCommand
{
    public string IdempotentId { get; init; } = string.Empty;
}

در اینجا یک پراپرتی، برای نگهداری شناسه‌ی درخواست دریافتی با نام IdempotentId در نظر گرفته شده‌است. این پراپرتی باید از طریق مقداری که از هدر درخواست HTTP دریافت می‌کنیم مقداردهی شود. به عنوان مثال برای ثبت کاربر جدید، به شکل زیر باید عمل کرد:

[HttpPost]
public async Task<ActionResult<long>> Register(
     [FromBody] RegisterUserCommand command,
     [FromIdempotencyToken] string idempotentId,
     CancellationToken cancellationToken)
{
     command.IdempotentId = idempotentId;
     var result = await sender.Send(command, cancellationToken);

     return result.ToActionResult();
}

در اینجا از همان Command به عنوان DTO ورودی استفاده شده‌است که وابسته به سطح Backward compatibility مورد نیاز، می توان از DTO مجزایی هم استفاده کرد. سپس از طریق FromIdempotencyToken سفارشی، شناسه‌ی درخواست، دریافت شده و بر روی command مورد نظر، تنظیم شده‌است.

رفتار سفارشی IdempotencyBehavior از ۲ بخش تشکیل شده‌است؛ در قسمت اول سعی می شود، قبل از اجرای هندلر مربوط به command مورد نظر، شناسه‌ی دریافتی را در storage تعبیه شده ثبت کند:

internal sealed class IdempotencyBehavior<TRequest, TResponse>(
    IIdempotencyStorage storage,
    ILogger<IdempotencyBehavior<TRequest, TResponse>> logger) :
    IPipelineBehavior<TRequest, TResponse>
    where TRequest : IIdempotentCommand
    where TResponse : IErrorOr
{
    public async Task<TResponse> Handle(
        TRequest command,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        string commandName = typeof(TRequest).Name;

        if (string.IsNullOrWhiteSpace(command.IdempotentId))
        {
            logger.LogWarning(
                "The given command [{CommandName}] ({@Command}) marked as idempotent but has empty IdempotentId",
                commandName, command);
            return await next();
        }

        if (await storage.TryPersist(command.IdempotentId, cancellationToken) == false)
        {
            return (dynamic)Error.Conflict(
                $"The given command [{commandName}] with idempotent-id [{command.IdempotentId}] has already been received and processed.");
        }

        return await next();
    }
}

در اینجا IIdempotencyStorage تزریق شده و در صورتی که امکان ذخیره سازی وجود نداشته باشد، خطای Confilict که به‌خطای 409 ترجمه خواهد شد، برگشت داده می‌شود. در غیر این صورت ادامه‌ی عملیات اصلی باید اجرا شود. پس از آن اگر به هر دلیلی در زمان پردازش عملیات اصلی،‌ درخواست همزمانی با همان شناسه، توسط سرور دریافت شده و پردازش شود، عملیات جاری با خطای UniqueConstaint برروی PK_IdempotentId در زمان نهایی سازی تراکنش جاری، مواجه خواهد شد. برای این منظور بخش دوم این رفتار به شکل زیر خواهد بود:

internal sealed class IdempotencyExceptionBehavior<TRequest, TResponse>(IIdempotencyStorage storage) :
    IPipelineBehavior<TRequest, TResponse>
    where TRequest : IIdempotentCommand
    where TResponse : IErrorOr
{
    public async Task<TResponse> Handle(
        TRequest command,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(command.IdempotentId)) return await next();

        string commandName = typeof(TRequest).Name;
        try
        {
            return await next();
        }
        catch (Exception ex) when (storage.IsKnownException(ex))
        {
            return (dynamic)Error.Conflict(
                $"The given command [{commandName}] with idempotent-id [{command.IdempotentId}] has already been received and processed.");
        }
    }
}

در اینجا عملیات اصلی در بدنه try اجرا شده و در صورت بروز خطایی مرتبط با Idempotency، خروجی Confilict برگشت داده خواهد شد. باید توجه داشت که نحوه ثبت رفتارهای تعریف شده تا اینجا باید به ترتیب زیر انجام شود:

services.AddMediatR(config =>
{
   config.RegisterServicesFromAssemblyContaining(typeof(DependencyInjection));

   // maintaining the order of below behaviors is crucial.
   config.AddOpenBehavior(typeof(LoggingBehavior<,>));
   config.AddOpenBehavior(typeof(IdempotencyExceptionBehavior<,>));
   config.AddOpenBehavior(typeof(TransactionBehavior<,>));
   config.AddOpenBehavior(typeof(IdempotencyBehavior<,>));
});

به این ترتیب بدنه اصلی هندلرهای موجود در سیستم هیچ تغییری نخواهند داشت و به صورت ضمنی و انتخابی، امکان تعیین command هایی که نیاز است به صورت Idempotent اجرا شوند را خواهیم داشت.

References

https://www.mscharhag.com/p/rest-api-design

https://www.manning.com/books/api-design-patterns

https://codeopinion.com/idempotent-commands/

مطالب
بهینه سازی فایلهای js و css در برنامه‌های ASP.NET با استفاده از Combres - قسمت اول
 یکی از مواردی که در توسعه وب نقش مهمی دارد، بهینه سازی فایلهای js  و css است که با فشرده سازی و کش کردن آنها می‌توان سرعت بارگذاری را تا حد چشمگیری افزایش داد. برای درک بهتر، به مثال زیر توجه کنید.
یک پروژه ساده را ایجاد می‌کنیم و فایل‌های CSS و js  را مانند شکل زیر، به آن اضافه می‌کنیم:


طبق تصویر فایل‌ها را به صفحه‌ای که ساختیم اضافه می‌کنیم:


پروژه را اجرا کرده و توسط افزونه‌ی firebug درخواست‌هایی را که از سرور شده‌است، بررسی می‌کنیم. مشاهده خواهید کرد که به ازای هر فایل، یک درخواست به سرور ارسال شده و هیچکدام از فایل‌ها توسط  وب سرور فشرده سازی نشده‌اند و اطلاعاتی در مورد کش، به هدر آنها اضافه نشده است.

برای رفع این موارد، روشهای گوناگونی وجود دارد که امروز قصد داریم این کار را توسط  کتابخانه Combress انجام دهیم !
نصب کتابخانه Combres
شما می‌توانید با استفاده از nuget این کتابخانه را به پروژه خود اضافه کنید. 
ایجاد فایل تنظیمات
پس از نصب کتابخانه، فایلی با نام combres.xml در فولدر app_data ساخته می‌شود که تمامی فعالیت‌های کتابخانه براساس آن انجام می‌شود و ساختار آن بصورت زیر است:
<?xml version="1.0" encoding="utf-8" ?>
<combres xmlns='urn:combres'>
  <filters>
    <filter type="Combres.Filters.FixUrlsInCssFilter, Combres" />
  </filters>
  <resourceSets url="~/combres.axd"
                defaultDuration="30"
                defaultVersion="auto"
                defaultDebugEnabled="false"
                defaultIgnorePipelineWhenDebug="true"
                localChangeMonitorInterval="30"
                remoteChangeMonitorInterval="60">
    <resourceSet name="siteCss" type="css">
      <resource path="~/content/Site.css" />
      <resource path="~/content/anotherCss.css" />
      <resource path="~/scripts/yetAnotherCss.css" />
    </resourceSet>
    <resourceSet name="siteJs" type="js">
      <resource path="~/scripts/jquery-1.4.4.js" />
      <resource path="~/scripts/anotherJs.js" />
      <resource path="~/scripts/yetAnotherJs.js" />
    </resourceSet>
  </resourceSets>
</combres>
ResourceSet: با استفاده از هر ResourceSet می‌توانید آن مجموعه فایل را در یک درخواست از سرور دریافت کنید.
پارامتر url : برای تولید لینک فایل‌ها از آن استفاده میکند.
پارامتر defaultDuration : این عدد به تعداد روزهای پیشفرض برای کش کردن فایلها اشاره میکند.
پارامتر defaultVersion :در صورتی که مقدار آن auto باشد به ازای هر تغییر، آدرس جدیدی برای فایل موردنظر ایجاد میشود.
پارامتر defaultDebugEnabled :با استفاده از آن میتوانید debug mode را مشخص کنید. در صورتیکه مقدار آن auto باشد، این مقدار مستقیما از وب‌کانفیگ خوانده میشود.
مقادیر پیش فرض برای تمامی ResourceSet‌ها استفاده می‌شود و در صورت نیاز میتوان این مقادیر را برای هر ResourceSet بازنویسی کرد. فیلترها برای اعمال تغییراتی در فایل js و CSS استفاده می‌شوند که باید به این شکل معرفی شوند. در قسمت بعد با فیلترها بیشتر آشنا میشویم.
فایل cobmres.xml  را به منظور استفاده در پروژه به صورت زیر تغییر می‌دهیم.
<?xml version="1.0" encoding="utf-8" ?>

<combres xmlns='urn:combres'>
  <filters>
    <filter type="Combres.Filters.FixUrlsInCssFilter, Combres" />
  </filters>
  <resourceSets url="~/combres.axd"
                defaultDuration="30"
                defaultVersion="auto"
                defaultDebugEnabled="false"
                defaultIgnorePipelineWhenDebug="true"
                localChangeMonitorInterval="30"
                remoteChangeMonitorInterval="60">
    <resourceSet name="siteCss" type="css">
      <resource path="~/Styles/Site.css" />
    </resourceSet>
    <resourceSet name="siteJs" type="js">
      <resource path="~/Scripts/jquery-1.10.2.js" />
      <resource path="~/Scripts/jquery-1.10.2.min.js" />
    </resourceSet>
  </resourceSets>
</combres>
اگر از نیوگت برای نصب کتابخانه استفاده کرده باشید تغییرات مورد نیاز در فایل وب کانفیگ به صورت خودکار اعمال می‌شود؛ در غیر اینصورت باید قسمتهای زیر را به آن اضافه کنید.
<configuration>
  <configSections>
    <section name="combres" type="Combres.ConfigSectionSetting, Combres, Version=2.2, Culture=neutral, PublicKeyToken=1ca6b37997dd7536" />
  </configSections>
  <system.web>
    <pages>
      <namespaces>
        <add namespace="Combres" />
      </namespaces>
    </pages>
  </system.web>
  <combres definitionUrl="~/App_Data/combres.xml" />
  <appSettings>
    <add key="CombresSectionName" value="combres" />
  </appSettings>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="AjaxMin" publicKeyToken="21ef50ce11b5d80f" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-4.84.4790.14405" newVersion="4.84.4790.14405" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
</configuration>
حال باید Route  مربوط به Combres را به RouteTable اضافه کنیم. در صورتیکه از نیوگت استفاده کرده باشید، کلاسی به فولدر app_start  اضافه شده است که با استفاده از WebActivator اینکار را در Application_Start انجام میدهد؛ در غیر اینصورت باید به صورت زیر این کار را انجام دهیم.
protected void Application_Start(object sender, EventArgs e)
        {
            RouteTable.Routes.AddCombresRoute("Combres Route");
        }
بعد از انجام مراحل قبل، نوبت به آن رسیده است که از لینکهای combres در صفحات خود استفاده کنیم. شیوه استفاده از آن در وب فرم به این صورت است:
<%@ Import Namespace="Combres" %>
<head runat="server">
    <%= WebExtensions.CombresLink("siteCss") %>
    <%= WebExtensions.CombresLink("siteJs") %>
</head>
و در MVC به صورت زیر می‌باشد:
<%= Url.CombresLink("siteCss") %>
<%= Url.CombresLink("siteJs") %>
در هر دو مورد نام ResourceSet برای تولید لینک به متد CombresLink ارسال میشود. پس از اجرای مجدد برنامه و با استفاده از firebug خواهیم دید که به ازای هر ResourceSet، یک درخواست به سرور ارسال شده است و حجم فایلها به صورت چشمگیری کاهش یافته است و اطلاعات مربوط به کش هم به آن اضافه شده است.

در ادامه می‌توانید فایل site.css قبلی و فعلی را مقایسه کنید!

در قسمت بعد با سازوکار combres و روش استفاده از فیلترها، بیشتر آشنا میشویم.

CombresProject-67bd3b7299f24c5484edc35537fa990b.zip 

نظرات مطالب
اثر وجود سشن بر پردازش موازی در ASP.NET
- در سایت جاری ماژول سشن در وب کانفیگ کلا حذف شده. در هیچ‌جایی از آن از سشن استفاده نشده. (یک نمونه عملی)
- به کوکی سمت کلاینت در سمت سرور دسترسی وجود دارد. مانند سیستم Forms Authentication که اساس آن استفاده از کوکی‌های رمزنگاری شده‌است؛ چه برای نگهداری اطلاعات کاربر لاگین شده به سیستم و چه برای نگهداری اطلاعات نقش‌های منتسب به او.
- تفاوتی ندارند. یک هسته‌ی اصلی ASP.NET وجود دارد که حداقل دو فریم ورک وب فرمز و MVC از آن استفاده می‌کنند.
پروژه‌ها
طراحی فریمورک برای کار با ASP.NET MVC و EF به صورت N-Layer

هدف اصلی بنده ساخت یک قالب  آماده بر اساس مقالاتی که تا الان در سایت جاری مطالعه کردم  و با امکان سطح دسترسی داینامیک، بود.همانطور که در مقاله مربوط به  چک لیست تهیه یک برنامه Asp.net MVC ، در نکات تکمیلی پیشنهاد شد:

" تهیه قالب‌های سفارشی VS.NET و لحاظ موارد فوق در آن جهت استفاده‌های بعدی نیز وجود دارد"


کاربر سیستمی
نام کاربری :SystemAdmin
کلمه عبور: Admin1234@gmail.com 

راه اندازی پروژه
از این مقاله کمک بگیرید و ورژن جدید را از مخزن پروژه دانلود کنید

موارد اضافه شده در ورژن آخر

  • بهبود سیستم فیلترینگ و مرتب سازی لیست کاربران
  • استفاده از  noty  به جای  toastr 
  • یکپارچه شده با Asp.net Web Api
  • یکپارچه شده با Asp.net SignalR
مطالب
توسعه اپلیکیشن های ASP.NET با Windows Azure Active Directory
ابزار ASP.NET برای Windows Azure Active Directory فعال کردن احراز هویت در وب اپلیکیشن هایی که روی  Windows Azure Web Sites  میزبانی شده اند را ساده‌تر می‌کند. می‌توانید از Windows Azure Authentication برای احراز هویت کاربران Office 365 استفاده کنید، حساب‌های کاربری را از On-Premise Active Directory خود همگام سازی (Sync) کنید و یا از یک دامنه سفارشی Windows Azure Active Directory بهره ببرید. فعال سازی Windows Azure Authentication، اپلیکیشن شما را طوری پیکربندی می‌کند تا تمامی کاربران را با استفاده از یک  Windows Azure Active Directory tenant  احراز هویت کند.

این مقاله ساختن یک اپلیکیشن ASP.NET را که بر اساس  organizational accounts  پیکربندی شده و بر روی  Windows Azure Active Directory  میزبانی می‌شود، مرور می‌کند.

پیش نیاز ها

  1. Visual Studio Express 2013 RC for Web یا Visual Studio 2013 RC 
  2. یک حساب کاربری در Windows Azure. می‌توانید یک حساب رایگان بسازید.


یک مدیر کلی به حساب کاربری Active Directory خود اضافه کنید

  1. وارد Windows Azure Portal شوید.
  2. یک حساب کاربری (Windows Azure Active Directory (AD انتخاب یا ایجاد کنید. اگر قبلا حساب کاربری ساخته اید از همان استفاده کنید در غیر اینصورت یک حساب جدید ایجاد کنید. مشترکین Windows Azure یک AD پیش فرض با نام Default Directory خواهند داشت.
  3. در حساب کاربری AD خود یک کاربر جدید در نقش Global Administrator بسازید. اکانت AD خود را انتخاب کنید و Add User را کلیک کنید. برای اطلاعات کامل‌تر به Managing Windows Azure AD from the Windows Azure Portal 1– Sign Up with an Organizational Account مراجعه کنید.

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

نام کاربری را وارد کنید و نقش Global Administrator را به آن اختصاص دهید. مدیران کلی به یک آدرس ایمیل متناوب هم نیاز دارند. به مرحله بعد بروید.

بر روی Create کلیک کنید و کلمه‌ی عبور موقتی را کپی کنید. پس از اولین ورود باید کلمه عبور را تغییر دهید.


یک اپلیکیشن ASP.NET بسازید

در ویژوال استودیو یک پروژه جدید ASP.NET Web Forms یا MVC بسازید و روی Change Authentication  کلیک کنید. 

گزینه Organizational Accounts را انتخاب کنید. نام دامنه خود را وارد کنید و سپس گزینه Single Sign On, Read directory data را انتخاب کنید. به مرحله بعد بروید.

نکته: در قسمت More Options می توانید قلمرو اپلیکیشن (Application ID URI) را تنظیم کنید. تنظیمات پیش فرض برای اکثر کاربران مناسب است اما در صورت لزوم می‌توانید آنها را ویرایش کنید، مثلا از طریق Windows Azure Portal دامنه‌های سفارشی خودتان را تنظیم کنید.

اگر گزینه Overwrite را انتخاب کنید اپلیکیشن جدیدی در Windows Azure برای شما ساخته خواهد شد. در غیر اینصورت فریم ورک سعی می‌کند اپلیکیشنی با شناسه یکسان پیدا کند (در پست متدهای احراز هویت در VS 2013 به تنظیمات این قسمت پرداخته شده).

اطلاعات مدیر کلی دامنه در Active Directory خود را وارد کنید (Credentials) و پروژه را با کلیک کردن بر روی Create Project بسازید.

با کلیدهای ترکیبی Ctrl + F5، اپلیکیشن را اجرا کنید. مرورگر شما باید یک اخطار SSL Certificate به شما بدهد. این بدین دلیل است که مدرک استفاده شده توسط IIS Express مورد اعتماد (trusted) نیست. این اخطار را بپذیرید و اجازه اجرا را به آن بدهید. پس از آنکه اپلیکیشن خود را روی Windows Azure منتشر کردید، این پیغام دیگر تولید نمی‌شود؛ چرا که Certificate‌های استفاده شده trusted هستند.

با حساب کاربری سازمانی (organizational account) که ایجاد کرده‌اید، وارد شوید.

همانطور که مشاهده می‌کنید هم اکنون به سایت وارد شده اید.



توزیع اپلیکیشن روی Windows Azure

در Windows Azure Portal یک وب سایت را به‌همراه یک دیتابیس، ایجاد کنید. در پانل سمت چپ صفحه روی Websites کلیک کنید و بعد New را انتخاب کنید. سپس گزینه Custom Create را برگزینید.

اپلیکیشن را روی Windows Azure منتشر کنید. روی پروژه کلیک راست کرده و Publish را انتخاب کنید. در مرحله تنظیمات (Settings) مشاهده می‌کنید که احراز هویت حساب‌های سازمانی (organizational accounts) فعال است. همچنین سطح دسترسی به خواندن تنظیم شده است. در قسمت Database رشته اتصال دیتابیس را تنظیم کنید.

حال به وب سایت Windows Azure خود بروید و توسط حساب کاربری ایجاد شده وارد سایت شوید. در این مرحله دیگر نباید خطای امنیتی SSL را دریافت کنید.


خواندن اطلاعات پروفایل کاربران توسط Graph API

قالب پروژه ویژوال استودیو برای organizational accounts یک متد و نما بنام UserProfile به پروژه اضافه کرده است.
[Authorize]
public async Task<ActionResult> UserProfile()
{
    string tenantId = ClaimsPrincipal.Current.FindFirst(TenantSchema).Value;

    // Get a token for calling the Windows Azure Active Directory Graph
    AuthenticationContext authContext = new AuthenticationContext(String.Format(CultureInfo.InvariantCulture, LoginUrl, tenantId));
    ClientCredential credential = new ClientCredential(AppPrincipalId, AppKey);
    AuthenticationResult assertionCredential = authContext.AcquireToken(GraphUrl, credential);
    string authHeader = assertionCredential.CreateAuthorizationHeader();
    string requestUrl = String.Format(
        CultureInfo.InvariantCulture,
        GraphUserUrl,
        HttpUtility.UrlEncode(tenantId),
        HttpUtility.UrlEncode(User.Identity.Name));

    HttpClient client = new HttpClient();
    HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
    request.Headers.TryAddWithoutValidation("Authorization", authHeader);
    HttpResponseMessage response = await client.SendAsync(request);
    string responseString = await response.Content.ReadAsStringAsync();
    UserProfile profile = JsonConvert.DeserializeObject<UserProfile>(responseString);

    return View(profile);
}

کلیک کردن لینک UserProfile  اطلاعات پروفایل کاربر جاری را نمایش می‌دهد.



اطلاعات بیشتر

مطالب دوره‌ها
شی گرایی در #F
برنامه نویسی شی گرای سومین نسل از الگوهای اصلی برنامه نویسی است. در توضیحات فصل اول گفته شد که #F یک زبان تابع گرا است ولی این بدان معنی نیست که #F از مفاهیمی نظیر کلاس و یا interface پشتیبانی نکند. برعکس در #F امکان تعریف کلاس و interface و هم چنین پیاده سازی مفاهیم شی گرایی وجود دارد.

*با توجه به این موضوع که فرض است دوستان با مفاهیم شی گرایی آشنایی دارند از توضیح و تشریح این مفاهیم خودداری می‌کنم.

Classes
کلاس چارچوبی از اشیا است برای نگهداری خواص(Properties) و رفتار ها(Methods) و رخدادها(Events). کلاس پایه ای‌ترین مفهوم در برنامه نویسی شی گراست. ساختار کلی تعربف کلاس در #F به صورت زیر است:
type [access-modifier] type-name [type-params] [access-modifier] ( parameter-list ) [ as identifier ] =
   [ class ]
     [ inherit base-type-name(base-constructor-args) ]
     [ let-bindings ]
     [ do-bindings ]
     member-list
      ...
   [ end ]

type [access-modifier] type-name1 ...
and [access-modifier] type-name2 ...
...
همان طور که در ساختار بالا می‌بینید مفاهیم access-modifier و inherit و constructor هم در #F وجود دارد.

انواع access-modifier در #F
  • public : دسترسی برای تمام فراخوان‌ها امکان پذیر است
  • internal : دسترسی برای تمام فراخوان هایی که در همین assembly هستند امکان پذیر است
  • private : دسترسی فقط برای فراخوان‌های موجود در همین ماژول امکان پذیر است

نکته : protected access modifier در #F پشتیبانی نمی‌شود.

مثالی از تعریف کلاس:

type Account(number : int, name : string) = class
    let mutable amount = 0m
   
end
کلاس بالا دارای یک سازنده است که دو پارامتر ورودی می‌گیرد. کلمه end به معنای انتهای کلاس است. برای استفاده کلاس باید به صورت زیر عمل کنید:
let myAccount = new Account(123456, "Masoud")
توابع و خواص در کلاس ها
برای تعریف خاصیت در #F باید از کلمه کلیدی member استفاده کنید. در مثال بعدی برای کلاس بالا تابع و خاصیت تعریف خواهیم کرد.
type Account(number : int, name: string) = class
    let mutable amount = 0m
 
    member x.Number = number
    member x.Name= name
    member x.Amount = amount
 
    member x.Deposit(value) = amount <- amount + value
    member x.Withdraw(value) = amount <- amount - value
end
کلاس بالا دارای سه خاصیت به نام‌های Number و Name و Amount است و دو تابع به نام‌های Deposit و Withdraw دارد. اما x استفاده شده قبل از هر member به معنی this در #C  است. در #F شما برای اشاره به شناسه‌های یک محدوده خودتون باید یک نام رو برای اشاره گر مربوطه تعیین کنید.
open System
 
type Account(number : int, name: string) = class
    let mutable amount = 0m
 
    member x.Number = number
    member x.Name= name
    member x.Amount = amount
 
    member x.Deposit(value) = amount <- amount + value
    member x.Withdraw(value) = amount <- amount - value
end
 let masoud= new Account(12345, "Masoud") let saeed = new Account(67890, "Saeed") let transfer amount (source : Account) (target : Account) = source.Withdraw amount target.Deposit amount let printAccount (x : Account) = printfn "x.Number: %i, x.Name: %s, x.Amount: %M" x.Number x.Name x.Amount let main() = let printAccounts() = [masoud; saeed] |> Seq.iter printAccount printfn "\nInializing account" homer.Deposit 50M marge.Deposit 100M printAccounts() printfn "\nTransferring $30 from Masoud to Saeed" transfer 30M masoud saeed
 printAccounts() printfn "\nTransferring $75 from Saeed to Masoud" transfer 75M saeed masoud printAccounts() main()
استفاده از کلمه do
در #F زمانی که قصد داشته باشیم در بعد از وهله سازی از کلاس و فراخوانی سازنده، عملیات خاصی انجام شود(مثل انجام برخی عملیات متداول در سازنده‌های کلاس‌های دات نت) باید از کلمه کلیدی do به همراه یک بلاک از کد استفاده کنیم.
open System
open System.Net
 
type Stock(symbol : string) = class

    let mutable _symbol = String.Empty
    do
     //کد مورد نظر در این جا نوشته  میشود
end
یک مثال در این زمینه:

open System

type MyType(a:int, b:int) as this =
    inherit Object()
    let x = 2*a
    let y = 2*b
    do printfn "Initializing object %d %d %d %d %d %d"
               a b x y (this.Prop1) (this.Prop2)
    static do printfn "Initializing MyType." 
    member this.Prop1 = 4*x
    member this.Prop2 = 4*y
    override this.ToString() = System.String.Format("{0} {1}", this.Prop1, this.Prop2)

let obj1 = new MyType(1, 2)
در مثال بالا دو عبارت do  یکی به صورت static و دیگری به صورت غیر static تعریف شده اند. استفاده از do  به صورت غیر static این امکان را به ما می‌دهد که بتوانیم به تمام شناسه‌ها و توابع تعریف شده در کلاس استفاده کنیم ولی do به صورت static فقط به خواص و توابع از نوع static در کلاس دسترسی دارد.
خروجی مثال بالا:
Initializing MyType.
Initializing object 1 2 2 4 8 16
خواص static:
برای تعریف خواص به صورت استاتیک مانند #C از کلمه کلیدی static استفاده کنید.مثالی در این زمینه:
type SomeClass(prop : int) = class
    member x.Prop = prop
    static member SomeStaticMethod = "This is a static method"
end
SomeStaticMethod به صورت استاتیک تعریف شده در حالی که x.Prop به صورت غیر استاتیک. دسترسی به متد‌ها یا خواص static باید بدون وهله سازی از کلاس انجام بگیرد در غیر این صورت با خطای کامپایلر روبرو خواهید شد.
let instance = new SomeClass(5);;
instance.SomeStaticMethod;; 

output:
stdin(81,1): error FS0191: property 'SomeStaticMethod' is static.
روش استفاده درست:
SomeClass.SomeStaticMethod;; (* invoking static method *)
متد‌های get , set در خاصیت ها:
همانند #C و سایر زبان‌های دات نت امکان تعریف متد‌های get و set برای خاصیت‌های یک کلاس وجود دارد.
ساختار کلی:
 member alias.PropertyName
        with get() = some-value
        and set(value) = some-assignment
مثالی در این زمینه:
type MyClass() = class
   let mutable num = 0 
    member x.Num
        with get() = num
        and set(value) = num <- value
end;;
کد متناظر در #C:
public int Num
{
   get{return num;}
   set{num=value;}
}
یا به صورت:
type MyClass() = class
    let mutable num = 0
 
    member x.Num
        with get() = num
        and set(value) =
            if value > 10 || value < 0 then
                raise (new Exception("Values must be between 0 and 10"))
            else
                num <- value
end

Interface ها
اینترفیس به تمامی خواص و توابع عمومی اشئایی که آن را پیاده سازی کرده اند اشاره می‌کند. (توضیحات بیشتر (^ ) و (^ ))ساختار کلی برای تعریف آن به صورت زیر است:
type type-name = 
   interface
       inherits-decl 
       member-defns 
   end
مثال:
type IPrintable =
   abstract member Print : unit -> unit
استفاده از حرف I برای شروع نام اینترفیس طبق قوانین تعریف شده (اختیاری) برای نام گذاری است.
نکته: در هنگام تعریف توابع و خاصیت در interface‌ها باید از کلمه abstract استفاده کنیم. هر کلاسی که از یک یا چند تا اینترفیس ارث ببرد باید تمام خواص و توابع اینتریس‌ها را پیاده سازی کند. در مثال بعدی کلاس SomeClass1 اینترفیس بالا را پیاده سازی می‌کند. دقت کنید که کلمه this توسط من به عنوان اشاره گر به اشیای کلاس تعیین شده و شما می‌تونید از هر کلمه یا حرف دیگری استفاده کنید.
type SomeClass1(x: int, y: float) =
   interface IPrintable with 
      member this.Print() = printfn "%d %f" x y
نکته مهم: اگر قصد فراخوانی متد Print را در کلاس بالا دارید نمی‌تونید به صورت مستقیم متد بالا را فراخوانی کنید. بلکه حتما باید کلاس به اینترفیس مربوطه cast شود.
روش نادرست:
let instance = new SomeClass1(10,20)
instance.Print//فراخوانی این متد باعث ایجاد خطای کامپایلری می‌شود.
روش درست:
let instance = new SomeClass1(10,20) 
let instanceCast = instance :> IPrintable// استفاده از (<:)  برای عملیات تبدیل کلاس به اینترفیس
instanceCast.Print
برای عملیات cast ازاستفاده کنید.
در مثال بعدی کلاسی خواهیم داشت که از سه اینترفیس ارث می‌برد. در نتیجه باید تمام متد‌های هر سه اینترفیس را پیاده سازی کند.
type Interface1 =
    abstract member Method1 : int -> int

type Interface2 =
    abstract member Method2 : int -> int

type Interface3 =
    inherit Interface1
    inherit Interface2
    abstract member Method3 : int -> int

type MyClass() =
    interface Interface3 with 
        member this.Method1(n) = 2 * n
        member this.Method2(n) = n + 100
        member this.Method3(n) = n / 10
فراخوانی این متد‌ها نیز به صورت زیر خواهد بود:
let instance = new MyClass()
let instanceToCast = instance :> Interface3
instanceToCast.Method3 10
کلاس‌های Abstract
#F از کلاس‌های abstract هم پشتیبانی می‌کند. اگر با کلاس‌های abstract در #C آشنایی ندارید می‌تونید مطالب مورد نظر رو در  (^ ) و (^ ) مطالعه کنید. به صورت خلاصه کلاس‌های abstract به عنوان کلاس‌های پایه در برنامه نویسی شی گرا استفاده می‌شوند. این کلاس‌ها دارای خواص و متد‌های پیاده سازی شده و نشده هستند. خواص و متد هایی که در کلاس پایه abstract پیاده سازی نشده اند باید توسط کلاس هایی که از این کلاس پایه ارث می‌برند حتما پیاده سازی شوند.
ساختار کلی تعریف کلاس‌های abstract:
[<AbstractClass>]
type [ accessibility-modifier ] abstract-class-name =
    [ inherit base-class-or-interface-name ]
    [ abstract-member-declarations-and-member-definitions ]

    abstract member member-name : type-signature
در #F برای این که مشخص کنیم که یک کلاس abstract است حتما باید [<AbstractClass>] در بالای کلاس تعریف شود.
[<AbstractClass>]
type Shape(x0 : float, y0 : float) =
    let mutable x, y = x0, y0
    let mutable rotAngle = 0.0

    abstract Area : float with get
    abstract Perimeter : float  with get
    abstract Name : string with get
کلاس بالا تعریفی از کلاس abstract است که سه خصوصیت abstract دارد (برای تعیین خصوصیت‌ها و متد هایی که در کلاس پایه پیاده سازی نمی‌شوند از کلمه کلیدی abstract در هنگام تعریف آن‌ها استفاده می‌کنیم). حال دو کلاس ایجاد می‌کنیم که این کلاس پایه را پیاده سازی کنند.

#1 کلاس اول
type Square(x, y,SideLength) =
    inherit Shape(x, y)
  override this.Area = this.SideLength * this.SideLength override this.Perimeter = this.SideLength * 4. override this.Name = "Square"
#2 کلاس دوم
type Circle(x, y, radius) =
    inherit Shape(x, y)
 let PI = 3.141592654 member this.Radius = radius override this.Area = PI * this.Radius * this.Radius override this.Perimeter = 2. * PI * this.Radius
Structures
structure‌ها در #F دقیقا معال struct در #C هستند. توضیحات بیشتر درباره struct در #C (^ ) و (^ )). اما به طور خلاصه باید ذکر کنم که strucure‌ها تقریبا دارای مفهوم کلاس هستند با اندکی تفاوت که شامل موارد زیر است:
  • structure‌ها از نوع مقداری هستند و این بدین معنی است مستقیما درون پشته ذخیره می‌شوند.
  • ارجاع به structure‌ها از نوع ارجاع با مقدار است بر خلاف کلاس‌ها که از نوع ارجاع به منبع هستند.(^ )
  • structure‌ها دارای خواص ارث بری نیستند.
  • عموما از structure برای ذخیره مجموعه ای از داده‌ها با حجم و اندازه کم استفاده می‌شود.

ساختار کلی تعریف structure

[ attributes ]
type [accessibility-modifier] type-name =
   struct
      type-definition-elements
   end

//یا به صورت زیر

[ attributes ]
[<StructAttribute>]
type [accessibility-modifier] type-name =
   type-definition-elements
یک نکته مهم هنگام کار با struct‌ها در #F این است که امکان استفاده از let و Binding در struct‌ها وجود ندارد. به جای آن باید از val استفاده کنید.
type Point3D =
   struct 
      val x: float
      val y: float
      val z: float
   end
تفاوت اصلی بین val و let در این است که هنگام تعریف شناسه با val امکان مقدار دهی اولیه به شناسه وجود ندارد. در مثال بالا مقادیر برای x و y و z برابر 0.0 است که توسط کامپایلر انجام می‌شود. در ادامه یک struct به همراه سازنده تعریف می‌کنیم:
type Point2D =
   struct 
      val X: float
      val Y: float
      new(x: float, y: float) = { X = x; Y = y }
   end
توسط سازنده struct بالا مقادیر اولیه x و y دریافت می‌شود به متغیر‌های متناظر انتساب می‌شود.

  در پایان یک مثال مشترک رو در #C و #F پیاده سازی می‌کنیم:


نظرات مطالب
آموزش سیلورلایت 4 - قسمت‌های 21 تا 27
MVVM فقط یک الگوی جدا سازی منطق برنامه از لایه نمایشی آن است. سؤال شما مربوط به multi-tier architecture است؛ جائیکه قسمت نمایشی کار از قسمت نگهداری اطلاعات از قسمت مدیریت کار با اطلاعات جدا شده است و اتفاقا سیلورلایت به صورت ذاتی چند tier است (حداقل دو tiers آن روی دو کامپیوتر مجزا قرار دارند؛ قسمت سیلورلایتی سمت کاربر و قسمت سرویس‌ها در سروری مجزا).
در یک پروژه WCF RIA Services کلیه اعمال کار با بانک اطلاعاتی در همان سمت سرور و توسط سرویس‌هایی که اضافه می‌کنید انجام خواهد شد. فقط صدا زدن متدهای این سرویس‌ها است که در سمت کاربر و برنامه سیلولایتی صورت خواهد گرفت (در همان ViewModel).
از آنجائیکه قسمت عمده مدل سیستم حین نمایش از همان جداول بانک اطلاعاتی شما تشکیل خواهد شد که در سمت سرور در سرویس‌های تعریف شده قابل دسترسی می‌شود، روش WCF RIA Services تعریف مجدد این‌ها را در سمت کاربر به صورت خودکار انجام می‌دهد. به همراه replicate کردن تمام مسایل اعتبار سنجی و غیره به سمت کاربر.
مطالب
آغاز کار با الکترون
در مقاله «آشنایی با الکترون» با نحوه نصب و راه اندازی آن آشنا شدیم. در این مقاله با تعدادی اصطلاح، آشنا شده و یک برنامه ساده را برای نوشتن و خواندن فایل‌ها، می‌نویسیم.
فرآیندها (Processes) در الکترون به دو بخش تقسیم می‌شوند:

یک. فرآیند اصلی (Main Process) که همان فایل جاوااسکریپتی است و توسط main، در فایل package.json مشخص شده‌است .فرآیند اصلی تنها فرآیندی است که قابلیت دسترسی به امکانات گرافیکی سیستم عامل را از قبیل نوتیفیکشن ها، دیالوگ‌ها ،Tray و ... دارد. فرآیند اصلی می‌تواند با استفاده از شیء BrowserWindow که در قسمت قبلی کاربرد آن را مشاهده کردیم، render process را ایجاد کند. با هر بار ایجاد یک نمونه از این شیء، یک Render Process ایجاد می‌شود.

دو. فرآیند رندر (Render Process): از آنجا که الکترون از کرومیوم استفاده می‌کند و کرومیوم شامل معماری چند پردازشی است، هر صفحه‌ی وب می‌تواند پردازش خود را داشته باشد که به آن Render Process می‌گویند. به طور معمول در مرورگرها، صفحات وب در محیطی به نام SandBox اجرا می‌شوندکه اجازه دسترسی به منابع بومی را ندارند. ولی از آنجا که الکترون می‌تواند از Node.js استفاده کند، قابلیت دسترسی به تعاملات سطح پایین سیستم عامل را نیز داراست.

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


در ابتدا قصد داریم یک منو برای برنامه‌ی خود درست کنیم. برای ساخت منو، راه‌های متفاوتی وجود دارند که فعلا ما راه استفاده از template را بر می‌گزینیم که به صورت یک آرایه نوشته می‌شود. کدهای زیر را در فایل index.js یا هر اسمی که برای آن انتخاب کرده‌اید بنویسید:
const electron = require('electron');
const {app,dialog,BrowserWindow,Menu,shell} = electron;

let win;

app.on('ready', function () {
  win = new BrowserWindow({width: 800, height: 600});
  win.loadURL(`file://${__dirname}/index.html`);

var app_menu=[
  {
    label:'پرونده',
    submenu:[
      {
        label:'باز کردن',
        accelerator:'CmdOrCtrl+O',
        click:()=>{
        }
      },
      {
        label:'ذخیره',
        accelerator:'CmdOrCtrl+S',
        click:()=>{
        }
      }
    ]
  },
  {
    label:'سیستم',
    submenu:[
        {
        label:'درباره ما',
        click:()=>
        {
                   shell.openExternal('https://www.dntips.ir');
        }
      },
      {
        label:'خروج',
        accelerator:'CmdOrCtrl+X',
        click:()=>
        {
          win=null;
          app.quit();
        }
      }
    ]
  }
];
تا به اینجای کار، بیشتر کدها برای شما آشناست و فقط تغییرات اندکی در آن‌ها ایجاد شده‌است. مثلا شیء app و سایر اشیاء به طور خلاصه‌تری نوشته شده‌اند. در اینجا دو شیء menu و dialogو shell برای شما جدید هستند. بعد از آن ما یک آرایه را برای منو تدارک دیده‌ایم که نحوه ساخت آن و تعاریفی مثل عنوان، کلید میانبر یا ترکیبی و نحوه انتساب رویدادها را می‌بینید.
 
در خطوط بعدی، یک کار اضافه‌تر را جهت آشنایی بیشتر انجام می‌دهیم. قصد داریم اگر سیستم عامل مکینتاش بود، نام برنامه هم در ابتدای نوار منو نمایش داده شود. به همین جهت در ادامه خطوط زیر را اضافه می‌کنیم:
  if(process.platform=="darwin")
  {
    const app_name=app.getName();
    app_menu.unshift({
      label:app_name
    })
  }
با استفاده از process.platform در node.js می‌توانیم نوع پلتفرم جاری را دریافت کنیم. مقادیر زیر، مقادیری هستند که بازگردانده می‌شوند:

ویندوز
win32 حتی اگر 64 بیتی باشد.
 لینوکس  linux
 مک  darwin
 فری بی اس دی
 freebsd
سولاریس
 sunos
سپس نام برنامه را از شیء app دریافت می‌کنیم و با استفاده از متد unshift، مقادیر داده شده را به ابتدای آرایه اضافه می‌کنیم.

دستو shell در بالا به شما اجازه می‌دهد با محیط دسکتاپ، یکپارچگی خود را حفظ کنید و دستوراتی از قبیل باز کردن url، باز کردن یک مسیر دایرکتوری، باز کردن یک فایل، انتقال فایل به سطل آشغال یا بازیافت و صدای بوق سیستم (بیپ) را به شما می‌دهد. مستندات این شیء را در اینجا مطالعه فرمایید.

دستور app.quit همانطور که از نامش پیداست، باعث خاتمه برنامه می‌شود. ولی یک نکته در اینجا وجود دارد که الزامی به نوشتن کدی برای اینکار نیست. می‌توانید زیرمنوی بالا را به شکل زیر هم بنویسید:
{
        label:'خروج',
        accelerator:'CmdOrCtrl+X',
        role:'close'
 }
خصوصیت role شامل چندین نوع اکشن مانند minimize,close,undo,redo و... می‌باشد که لیست کاملتر آن در اینجا قرار دارد. اگر خصوصیت کلیک و role را همزمان استفاده کنید، خصوصیت role نادیده گرفته خواهد شد.

در انتها با اجرای دو دستور زیر، منو ساخته می‌شود:
  var menu=Menu.buildFromTemplate(app_menu);
  Menu.setApplicationMenu(menu);
در خط اول، منو توسط قالبی که با آرایه‌ها ایجاد کردیم ساخته می‌شود و در خط دوم، منو به برنامه ست می‌شود.
حال قصد داریم برای زیرمنوی «باز کردن فایل» یک دیالوگ open درخواست کنیم. برای این کار از شیء dialog استفاده می‌کنیم. پس خطوط زیر را به رویداد کلیک این زیرمنو اضافه می‌کنیم:
 dialog.showOpenDialog({
             title:'باز کردن فایل متنی',
              properties: [ 'openFile']//[ 'openFile', 'openDirectory', 'multiSelections' ]
             ,filters:[
             {name:'فایل‌های نوشتاری' , extensions:['txt','text']},
             {name:'جهت تست' , extensions:['doc','docx']}
              ]
           },
             (filename)=>{
               if(filename===undefined)
                  return;
               dialog.showMessageBox({title:'پیام اطلاعاتی',type:"info",buttons:['تایید'],message:`the name of file is [${filename}]`});
            });
این متد سه پارامتر دارد که اولین و آخرین پارامتر آن اختیاری می‌باشد. اولین پارامتر آن شیء پنجره است. دومین پارامتر آن، تنظیم یک سری خصوصیات که شامل (پسوند‌های قابل قبول، عنوان، مسیر پیش فرض، قابلیت انتخاب چندگانه، قابلیت باز کردن دایرکتوری و...) می‌شود که لیست کامل آن را می‌توانید در این صفحه ببینید. سومین پارامتر هم که در کد بالا ذکر شده است، callback می‌باشد که خروجی آن، مسیر فایل مورد نظر است و اگر انتخاب چندگانه باشد، آرایه‌ای با نام فایل‌هاست، که همگی آن‌ها به همراه مسیرشان می‌باشند. در صورتیکه کاربر از دیالوگ انصراف بدهد، پارامتر دریافتی با خروجی undefined همراه است.  آخرین دیالوگ هم نمایش یک پیام ساده است که نام فایل جاری را بر میگرداند. اگر خصوصیت buttons را با آرایه خالی مقداردهی کنید، دکمه Ok نمایش داده می‌شود و اگر هم مقداردهی نکنید با خطا روبرو خواهید شد.
برای قسمت ذخیره هم کد زیر را می‌نویسیم:
    dialog.showSaveDialog({
            title:'باز کردن فایل متنی',
             properties: [ 'openFile']//[ 'openFile', 'openDirectory', 'multiSelections' ]
            ,filters:[
            {name:'فایل‌های نوشتاری' , extensions:['txt','text']}
             ]
          },
            (filename)=>{
              if(filename===undefined)
                 return;

           });

حال بهتر است این دیالوگ‌های جاری را هدفمند کنیم و بتوانیم فایل‌های متنی را به کاربر نمایش دهیم، یا آن‌ها را ذخیره کنیم. به همین علت فایل html زیر را نوشته و طبق دستوری که در مقاله «آشنایی با الکترون» فرا گرفتیم، آن را نمایش می‌دهیم:
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title></title>
  </head>
  <body>
    Fie Content:<br/>
    <textarea id="TextFile" cols="100" rows="50"></textarea>
 
  </body>
</html>
برای تشکیل ساختار HTML می‌توانید عبارت HTML را تایپ نمایید تا بعد از زدن Enter، ساختار آن به طور خودکار تشکیل شود. سپس محتوا را مثل بالا به شکل دلخواه تغییر می‌دهیم.

کاری که می‌خواهیم انجام دهیم این است که فایل متنی را باز کرده و محتوای آن را در کادر متنی نشان دهیم و موقع ذخیره نیز محتوای نوشته شده در کادر متنی را در فایلی ذخیره کنیم. از آنجا که main Process به المان‌های DOM یا Render Process دسترسی ندارد، باید از طریقی، ارتباط آن را برقرار کنیم. یکی از راه‌های برقراری این ارتباط، IPC است. IPC در واقع یک فرستنده و یک شنونده است که هر کدام در یک سمت قرار گرفته اند. فرستنده پیام را تحت یک عنوان ارسال می‌کند و شنونده منتظر دریافت پیامی تحت همان عنوان میماند و پیام دریافتی را پاسخ می‌دهد. در این مقاله، ما فقط قسمتی از این نوع ارتباطات را بررسی میکنیم.

در نتیجه محتوای callback کدهای دیالوگ open و save را به شکل زیر تغییر می‌دهیم:
Open
 dialog.showOpenDialog({
                 title:'باز کردن فایل متنی',
                  properties: [ 'openFile']//[ 'openFile', 'openDirectory', 'multiSelections' ]
                 ,filters:[
                 {name:'فایل‌های نوشتاری' , extensions:['txt','text']},
                 {name:'جهت تست' , extensions:['doc','docx']}
                  ]
               },
                 (filename)=>{
                   if(filename===undefined)
                      return;

                      win.webContents.send('openFile',filename);
                  // dialog.showMessageBox({title:'پیام اطلاعاتی',type:"info",buttons:['تایید'],message:`the name of file is [${filename}]`});
                });
Save
  dialog.showSaveDialog({
                title:'باز کردن فایل متنی',
                 properties: [ 'openFile']//[ 'openFile', 'openDirectory', 'multiSelections' ]
                ,filters:[
                {name:'فایل‌های نوشتاری' , extensions:['txt','text']}
                 ]
              },
                (filename)=>{
                  if(filename===undefined)
                     return;
                       win.webContents.send('saveFile',filename);
               });
دستور win.webContents.send یک پیام را به صورت Async به سمت RenderProcess مربوطه ارسال میکند. پارامتر اول، عنوان IPC است و پارامتر دوم، پیام IPC است.
برای ایجاد شنونده هم کد زیر را به فایل index.html اضافه می‌کنیم:
  <script>
    const {ipcRenderer} = require('electron');
    var fs=require('fs');

    ipcRenderer.on('openFile', (event, arg) => {
      var content=  fs.readFileSync(String(arg),'utf8');
      document.getElementById("TextFile").value=content;
    });

    ipcRenderer.on('saveFile', (event, arg) =>{
      var content=document.getElementById("TextFile").value;
      fs.writeFileSync(String(arg),content,'utf8');
      alert('ذخیره شد');
    });
    </script>

در اینجا شونده‌هایی را از نوع ipcRenderer ایجاد می‌کنیم و با استفاده از متد on، به پیام‌هایی تحت عنوان‌های مشخص شده گوش فرا می‌دهیم. پیام‌های ارسالی را که حاوی آدرس فایل می‌باشند، به شیءای که از نوع fs می‌باشد، می‌دهند و آن‌ها را می‌خوانند یا می‌نویسند. خواندن و نوشتن فایل، به صورت همزمان صورت میگیرد. ولی اگر دوست دارید که به صورت غیر همزمان پیامی را بخوانید یا بنویسید، باید عبارت Sync را از نام متدها حذف کنید و یک callback را به عنوان پارامتر دوم قرار دهید و محتوای آن را از طریق نوشتن یک پارامتر در سازنده دریافت کنید.

فایل‌های پروژه
 

مطالب
Minimal API's در دات نت 6 - قسمت سوم - ایجاد endpoints مقدماتی
در دو قسمت قبل، ساختار ابتدایی برنامه‌ی Minimal API's بلاگ دهی را ایجاد کردیم. در این قسمت می‌خواهیم بررسی کنیم، معادل‌های کنترلرهای MVC و اکشن متدهای آن‌ها در سیستم جدید Minimal API، به چه صورتی ایجاد می‌شوند.


ایجاد اولین endpoint از نوع Get مبتنی بر Minimal API

برای افزودن اولین endpoint برنامه، به فایل Program.cs برنامه مراجعه کرده و آن‌را به صورت زیر تکمیل می‌کنیم:
// ...

app.UseHttpsRedirection();

app.MapGet("/api/authors", async (MinimalBlogDbContext ctx) =>
{
    var authors = await ctx.Authors.ToListAsync();
    return authors;
});

app.Run();
app.MapGet، معادل یک اکشن متد کنترلرهای MVC را که از نوع HttpGet هستند، ارائه می‌دهد. در همینجا می‌توان آدرس دقیق این endpoint را به عنوان پارامتر اول، مشخص کرد که پس از فراخوانی آن در مرورگر، یک Delegate که هندلر نام دارد (پارامتر دوم این متد)، اجرا می‌شود تا Response ای را ارائه دهد.
همانطور که مشاهده می‌کنید می‌توان در اینجا، این Delegate را از نوع Lambda expressions تعریف کرد و با ذکر MinimalBlogDbContext به صورت یک پارامتر آن، کار تزریق وابستگی‌های خودکار آن نیز صورت می‌گیرد. شبیه به حالتی که می‌توان یک سرویس را به عنوان پارامتر یک اکشن متد، با ذکر ویژگی [FromServices] در کنترلرهای MVC معرفی کرد؛ البته در اینجا بدون نیاز به ذکر این ویژگی (هرچند هنوز هم قابل ذکر است). مزیت آن این است که هر endpoint، تنها سرویس‌های مورد نیاز خودش را دریافت می‌کند و نه یک لیست قابل توجه از تمام سرویس‌هایی که قرار است در قسمت‌های مختلف یک کنترلر استفاده شوند.
پس از آن می‌توان با Context ای که در اختیار داریم، عملیات مدنظر را پیاده سازی کرده و یک خروجی را ارائه دهیم. در اینجا دیگر نیازی به تعریف IActionResult‌ها و امثال آن نیست و همه چیز ساده شده‌است.


ایجاد اولین endpoint از نوع Post مبتنی بر Minimal API

app.MapPost، معادل یک اکشن متد کنترلرهای MVC را که از نوع HttpPost هستند، ارائه می‌دهد:
//...

app.UseHttpsRedirection();

//...

app.MapPost("/api/authors", async (MinimalBlogDbContext ctx, AuthorDto authorDto) =>
{
    var author = new Author();
    author.FirstName = authorDto.FirstName;
    author.LastName = authorDto.LastName;
    author.Bio = authorDto.Bio;
    author.DateOfBirth = authorDto.DateOfBirth;

    ctx.Authors.Add(author);
    await ctx.SaveChangesAsync();

    return author;
});

app.Run();

internal record AuthorDto(string FirstName, string LastName, DateTime DateOfBirth, string? Bio);
در ابتدا یک Dto را که حاوی اطلاعات نویسنده‌ی جدیدی است، معادل خواص مدل Author دومین برنامه، تعریف می‌کنیم. سپس می‌توان این Dto را نیز به صورت یک پارامتر جدید به Lambda Expression متد app.MapPost معرفی کرد تا کار نگاشت اطلاعات دریافتی به آن، به صورت خودکار انجام شود (حالت پیش‌فرض آن [FromBody] است که نیازی به ذکر آن نیست).
سعی شده‌است تا این مثال در ساده‌ترین شکل ممکن خودش ارائه شود. در ادامه کار نگاشت خواص Dto را به مدل دومین برنامه، توسط AutoMapper انجام خواهیم داد.
مابقی نکات متد app.MapPost نیز مانند متد app.MapGet است؛ برای مثال در اینجا نیز تعریف مسیر endpoint، توسط اولین پارامتر این متد صورت می‌گیرد و نحوه‌ی تزریق سرویس DbContext برنامه نیز یکی است.


آزمایش برنامه‌ی Minimal API's

برنامه‌ی Minimal API's تهیه شده، به همراه یک Swagger از پیش تنظیم شده نیز هست. به همین جهت برای کار با این API الزاما نیازی به استفاده‌ی از مثلا برنامه‌ی Postman یا راه حل‌های مشابه نیست. بنابراین فقط کافی است تا برنامه‌ی API را اجرا کرده و در رابط کاربری ظاهر شده در آدرس https://localhost:7085/swagger/index.html، بر روی دکمه‌ی Try it out هر کدام از endpointها کلیک کنیم. برای مثال اگر چنین کاری را در قسمت Post انجام دهیم، به تصویر زیر می‌رسیم:



در اینجا پس از ویرایش اطلاعات شیء JSON ای که برای ما تدارک دیده‌است، فقط کافی است بر روی دکمه‌ی execute ذیل آن کلیک کنیم تا اطلاعات این Dto را به app.MapPost متناظر فوق ارسال کند و برای نمونه خروجی بازگشتی از سرور را نیز در همینجا نمایش می‌دهد که در آن، Id رکورد نیز پس از ثبت در بانک اطلاعاتی، مشخص است:



شروع به Refactoring و خلوت کردن فایل Program.cs

اگر بخواهیم به همین نحو تمام endpoints و dtoها را داخل فایل Program.cs اضافه کنیم، پس از مدتی به یک فایل بسیار حجیم و غیرقابل نگهداری خواهیم رسید. بنابراین در مرحله‌ی اول، تنظیمات سرویس‌ها و میان افزارها را به خارج از آن منتقل می‌کنیم. برای این منظور پوشه‌ی جدید Extensions را به همراه دو کلاس زیر ایجاد می‌کنیم:
using Microsoft.EntityFrameworkCore;
using MinimalBlog.Dal;

namespace MinimalBlog.Api.Extensions;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddApplicationServices(this IServiceCollection services,
        WebApplicationBuilder builder)
    {
        if (builder == null)
        {
            throw new ArgumentNullException(nameof(builder));
        }

        builder.Services.AddEndpointsApiExplorer();
        builder.Services.AddSwaggerGen();

        var connectionString = builder.Configuration.GetConnectionString("Default");
        builder.Services.AddDbContext<MinimalBlogDbContext>(opt => opt.UseSqlServer(connectionString));

        return services;
    }
}
کار این متد الحاقی، خارج کردن تنظیمات سرویس‌های برنامه از کلاس Program است.

همچنین نیاز به متد الحاقی دیگری برای خارج کردن تنظیمات میان‌افزارها داریم:
namespace MinimalBlog.Api.Extensions;

public static class WebApplicationExtensions
{
    public static WebApplication ConfigureApplication(this WebApplication app)
    {
        if (app == null)
        {
            throw new ArgumentNullException(nameof(app));
        }

        if (app.Environment.IsDevelopment())
        {
            app.UseSwagger();
            app.UseSwaggerUI();
        }

        app.UseHttpsRedirection();

        return app;
    }
}
پس از این تغییرات، اکنون ابتدای کلاس Program برنامه‌ی Api به صورت زیر تغییر می‌کند و خلاصه می‌شود:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddApplicationServices(builder);

var app = builder.Build();
app.ConfigureApplication();

در قسمت بعد، endpoints را از این کلاس آغازین برنامه خارج می‌کنیم.
مطالب
آشنایی با OWIN و بررسی نقش آن در ASP.NET Core
در این مطلب می‌خواهیم نگاهی به قسمت‌های کلیدی OWIN و همچنین پروژه‌ی Katana بیندازیم و در نهایت نیز نقش OWIN را در ASP.NET Core بررسی خواهیم کرد.



OWIN چیست؟

همانطور که می‌دانید OWIN یک specification است که استانداری را بین وب‌سرور و وب‌اپلیکیشن‌ها تعریف کرده است. در واقع OWIN یکسری لایه‌ی انتزاعی را جهت ایجاد اپلیکیشن‌هایی که نحوه‌ی میزبانی آنها اهمیتی ندارد، تعریف خواهد کرد. به صورت خلاصه توسط این لایه‌ی انتزاعی می‌توانیم وابستگی بین وب‌سرور و وب‌اپلیکیشن را حذف کنیم. در این specification منظور از وب‌سرور یک delegate و همچنین یک دیکشنری است. در واقع هدف این است که وقتی درخواستی به این وب‌سرور ارسال شد، درخواست به قسمت‌های کوچکی تقسیم‌بندی شده و درون این دیکشنری قرار خواهند گرفت (این دیکشنری حاوی کلیدهای از پیش‌تعریف شده‌ای است که توسط OWIN تعریف شده‌اند). سپس این دیکشنری از طریق یک application function به درون pipeline ارسال خواهد شد و از یکسری middleware عبور خواهد کرد. در اینحالت می‌توانیم کنترلی را بر روی درخواست‌های وارده و صادره داشته باشیم. ایده‌ی middleware خیلی شبیه به HTTP moduleها در IIS است؛ اما تفاوت آن این است که middlewareها وابستگی‌ایی به IIS ندارند و همچنین مبتنی بر رویداد نیستند. هر middleware بعد از انجام تغییرات بر روی درخواست، تا زمان رسیدن دیکشنری به آخرین middleware، آن را به middleware بعدی ارسال خواهد کرد. در این حین می‌توانیم به response streams اطلاعاتی را append کنیم. وقتی دیکشنری از تمامی middlewareها عبور کرد، سرور مطلع خواهد شد و نتیجه را به کلاینت ارسال می‌کند.


استاندارد OWIN تعدادی کلید را درون یک دیکشنری تعریف کرده است که بعد از ورود به هر middleware مقداردهی خواهند شد. این کلیدها را می‌توانیم در دو دسته‌ی Request و Response بررسی کنیم.

کلیدهای مربوط به Request

ضروری؟

نام کلید

مقدار

بله

"owin.RequestBody"

یک Stream همراه با request body. اگر body برای request وجود نداشته باشد، Stream.Null به عنوان placeholder قابل استفاده است.

بله

"owin.RequestHeaders"

یک دیکشنری به صورت IDictionary<string, string[]> از هدرهای درخواست.

بله

"owin.RequestMethod"

رشته‌ایی حاوی نوع فعل متد HTTP مربوط به درخواست (مانند GET and POST )

بله

"owin.RequestPath"

path درخواست شده به صورت string

بله

"owin.RequestPathBase"

قسمتی از path درخواست به صورت string

بله

"owin.RequestProtocol"

نام و نسخه‌ی پروتکل (مانند HTTP/1.0 or HTTP/1.1 )

بله

"owin.RequestQueryString"

رشته‌ای حاوی query string ؛ بدون علامت ? (مانند foo=bar&baz=quux )

بله

"owin.RequestScheme"

رشته‌ایی حاوی URL scheme استفاده شده در درخواست (مانند HTTP or HTTPS )



کلیدهای مربوط به Response

ضروری؟

نام کلید

مقدار

بله

"owin.ResponseBody"

یک Stream جهت نوشتن response body در خروجی

بله

"owin.ResponseHeaders"

یک دیکشنری به صورت IDictionary<string, string[]> از هدرهای response

خیر

"owin.ResponseStatusCode"

یک عدد صحیح حاوی کد وضعیت HTTP response ؛ حالت پیش‌فرض 200 است.

خیر

"owin.ResponseReasonPhrase"

یک رشته حاوی reason phrase مربوط به status code ؛ اگر خالی باشد در نتیجه سرور بهتر است آن را مقداردهی کند.

خیر

"owin.ResponseProtocol"

یک رشته حاوی نام و نسخه‌ی پروتکل (مانند HTTP/1.0 or HTTP/1.1 )؛ اگر خالی باشد؛ “owin.RequestProtocol” به عنوان مقدار پیش‌فرض در نظر گرفته خواهد شد.


Katana
پروژه‌ی Katana یک پیاده‌سازی از استاندارد OWIN است که توسط مایکروسافت ایجاد شده است. مایکروسافت علاوه بر پیاده‌سازی OWIN، یکسری قابلیت دیگر را نیز به آن اضافه کرده است. برای شروع کار با Katana یک پروژه خالی از نوع ASP.NET Web Application را ایجاد کنید. در ادامه لازم است پکیج Microsoft.Owin.Host.SystemWeb را نیز نصب کنیم. همراه با نصب این پکیج، دو وابستگی دیگر نیز نصب خواهند شد؛ زیرا پیاده‌سازی OWIN درون پکیج Microsoft.Owin قرار دارد:
<package id="Microsoft.Owin" version="3.0.1" targetFramework="net461" />
<package id="Microsoft.Owin.Host.SystemWeb" version="3.0.1" targetFramework="net461" />
<package id="Owin" version="1.0" targetFramework="net461" />
در ادامه نیاز به یک نقطه‌ی شروع برای اپلیکیشن‌مان داریم. طبق convention باید یک فایل را با نام Startup.cs با محتویات زیر ایجاد کنیم:
using Owin;
namespace SimpleOwinWebApp
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {

        } 
    }
}
توسط IAppBuilder می‌توانیم middlewareها را به pipeline اضافه کنیم:
using Owin;
namespace SimpleOwinWebApp
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.Use(async (ctx, next) =>
            {
                await ctx.Response.WriteAsync("Hello");
            });
        } 
    }
توسط متد Use، یک middleware را به صورت inline تعریف کرده‌ایم. متد Use یک delegate را از ورودی دریافت خواهد کرد و امضای آن به اینصورت است:
Func<IOwinContext, Func<Task>, Task> handler

IOwinContext در واقع یک wrapper برای environment dictionaryایی است که در ابتدا به آن اشاره کردیم. در مثال قبل، از پراپرتی Response، جهت ارسال خروجی به کلاینت استفاده شده است. این پراپرتی در واقع معادل کلید owin.ResponseBody درون دیکشنری است. اما در اینجا به صورت strongly-typed و ساده به آن دسترسی داریم؛ هر چند که امکان کار با دیکشنری خام نیز وجود دارد. به عنوان مثال معادل مثال قبل بدون استفاده از پراپرتی Response، اینچنین خواهد بود:
app.Use(async (ctx, next) =>
{
   var response = ctx.Environment["owin.ResponseBody"] as Stream;
   using (var writer = new StreamWriter(response))
   {
      await writer.WriteAsync("Hello");
   }
});
اکنون اگر پروژه را اجرا کنید، با وارد کردن هر آدرسی، پیام Hello درون مرورگر برایتان نمایش داده خواهد شد:


به هر تعداد middleware که خواستید می‌توانید به pipeline اضافه کنید؛ اما باید دقت داشته باشید که ترتیب قرار دادن آنها اهمیت دارد.

Self-hosting OWIN
در مثال قبلی، اپلیکیشن توسط IIS Express اجرا می‌شد. برای میزبانی درون یک کنسول اپلیکیشن، ابتدا یک پروژه‌ی Console Application را ایجاد کرده و پکیج Microsoft.Owin.SelfHost را نصب کنید. سپس کلاس Startup موردنظرتان را ایجاد کرده و در نهایت درون متد Main، کار راه‌اندازی سرور را انجام خواهیم داد:
using System;
using Microsoft.Owin.Hosting;

namespace SimpleOwinConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            using (WebApp.Start<Startup>("http://localhost:12345"))
            {
                Console.WriteLine("Listening to port 12345");
                Console.WriteLine("Press Enter to end...");
                Console.ReadLine();
            }
        }
    }
}

OWIN در ASP.NET Core
ASP.NET Core دارای مفهومی با عنوان pipeline است. این pipeline خیلی شبیه به OWIN است اما OWIN نیست؛ بلکه عملکرد آن شبیه به OWIN است. به عنوان مثال اینبار به جای دیکشنری، شیء HttpContext را داریم. در ادامه یک پروژه‌ی ASP.NET Core Web Application از نوع Empty را شروع خواهیم کرد. اگر دقت کنید اینبار برای کلاس Startup باید دو متد را پیاده‌سازی کنیم:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace SimpleOwinCoreApp
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.Run(async (context) =>
            {
                await context.Response.WriteAsync("Hello World!");
            });
        }
    }
}

متد Configure: همانطور که در ابتدای مطلب مشاهده کردید این متد قبلاً در پروژه‌های مبتنی بر کاتانا Configuration نام داشت؛ همچنین به جای IAppBuilder اینبار IApplicationBuilder را داریم. مزیت ASP.NET Core این است که در هر جایی از اپلیکیشن می‌توانیم از سیستم DI توکار آن استفاده کنیم؛ در نتیجه علاوه بر IApplicationBuilder وابستگی‌های دیگری مانند IHostingEnvironment و ILoggerFactory را نیز می‌توانیم تزریق کنیم.
متد ConfigureServices: در اینجا می‌توانیم سرویس‌های موردنیاز درون اپلیکیشن را برای IoC ریجستر کنیم.
در کد فوق استفاده از متد Use به معنای آخرین نقطه در pipeline است. یعنی جایی که response برگردانده خواهد شد و چیزی بعد از آن اجرا نخواهد شد؛ در واقع ارجاعی به middleware بعدی وجود ندارد.

ایجاد یک Middleware جدید
تا اینجا تمامی کدها را به صورت inline نوشتیم. اما اگر بخواهیم middlewareمان قابلیت استفاده‌ی مجدد داشته باشد می‌توانیم تعاریف آن را به یک کلاس با ساختار زیر منتقل نمائیم:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

namespace SimpleOwinCoreApp.Middlewares
{
    public class SimpleMiddleware
    {
        private readonly RequestDelegate _next;

        public SimpleMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task Invoke(HttpContext ctx)
        {
            // قبل از فراخوانی میان‌افزار بعدی

            await ctx.Response.WriteAsync("Hello DNT!");

            await _next(ctx);

            // بعد از فراخوانی میان‌افزار بعدی
        }
    }
}

درون متد Invoke بعد از پردازش درخواست باید متد middleware بعدی را همراه با context جاری فراخوانی کنیم. در نتیجه قبل و بعد از فراخوانی middleware بعدی فرصت این را خواهیم داشت تا درخواست را پردازش کنیم. در نهایت برای استفاده از middleware فوق می‌توانیم از متد الحاقی UseMiddleware استفاده کنیم:
app.UseMiddleware<SimpleMiddleware>();

استفاده از middlewareهای مبتنی بر Katana در ASP.NET Core
middlewareهایی را که برای Katana نوشته‌اید، درون یک اپلیکیشن ASP.NET Core نیز قابل استفاده هستند. برای اینکار با مراجعه به فایل project.json می‌توانید پکیج زیر را نصب کنید:
"Microsoft.AspNetCore.Owin": "1.0.0"
سپس درون متد Configure می‌توانید Owin را به pipeline اضافه کرده و middleware خود را ریجستر کنید:
app.UseOwin(pipeline =>
{
pipeline(next => new MyKatanaBasedMiddleware(next).Invoke)
});

مثال تکمیلی:
در ادامه می‌خواهیم ماژول مطرح شده در این مطلب  را به صورت یک middleware با قابلیت پذیرفتن تنظیمات، نوشته و سپس درون pipeline استفاده کنیم. برای شروع یک کلاس با نام IpBlockerMiddleware با محتویات زیر ایجاد خواهیم کرد:
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

namespace SimpleOwinAspNetCore.Middleware
{
    public class IpBlockerMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly IpBlockerOptions _options;

        public IpBlockerMiddleware(RequestDelegate next, IpBlockerOptions options)
        {
            _next = next;
            _options = options;
        }

        public async Task Invoke(HttpContext context)
        {
            var ipAddress = context.Request.Host.Host;
            if (IsBlockedIpAddress(ipAddress))
            {
                context.Response.StatusCode = 403;
                await context.Response.WriteAsync("Forbidden : The server understood the request, but It is refusing to fulfill it.");
                return;
            }
            await _next.Invoke(context);
        }

        private bool IsBlockedIpAddress(string ipAddress)
        {
            return _options.Ips.Any(ip => ip == ipAddress);
        }
    }
}
در کدهای فوق لیست Ipها از پراپرتی Ips درون کلاس IpBlockerOptions دریافت خواهد شد:
using System.Collections.Generic;

namespace SimpleOwinAspNetCore.Middleware
{
    public class IpBlockerOptions
    {
        public IpBlockerOptions()
        {
            Ips = new[] { "192.168.1.1" };
        }
        public IList<string> Ips { get; set; }
    }
}
همچنین برای استفاده راحت‌تر از middleware، یک متد الحاقی را برای آن ایجاد کرده‌ایم و سپس پراپرتی Ips را توسط اینترفیس IConfigurationRoot دریافت کرده‌ایم:
using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;

namespace SimpleOwinAspNetCore.Middleware
{
    public static class IpBlockerExtensions
    {
        public static IApplicationBuilder UseIpBlocker(this IApplicationBuilder builder, IConfigurationRoot configuration, IpBlockerOptions options = null)
        {
            return builder.UseMiddleware<IpBlockerMiddleware>(options ?? new IpBlockerOptions
            {
                Ips = configuration.GetSection("block_list").GetChildren().Select(p => p.Value).ToArray()
            });
        }
    }
}
قبلاً در رابطه با فایل‌های کانفیگ مطلبی را مطالعه کرده‌اید؛ در نتیجه نیازی به توضیح اضافه‌تری ندارد. تنها کاری که در اینجا انجام شده است، دریافت محتویات کلید block_list از فایل کانفیگ است. 
محتویات فایل blockedIps.json:
{
  "block_list": [
    "192.168.1.1",
    "localhost",
    "127.0.0.1",
    "172.16.132.151"
  ]
}

برای خواندن فایل فوق در برنامه نیز خواهیم داشت:
public IConfigurationRoot Configuration { set; get; }

public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("blockedIps.json");
Configuration = builder.Build();
}
در نهایت برای استفاده از middleware فوق خواهیم داشت:
app.UseIpBlocker(Configuration);
اکنون هر درخواستی که با آدرس‌های تعیین شده درون فایل blockedIps.json وارد pipeline شود، امکان استفاده‌ی از سایت را نخواهد داشت.

کدهای این مطلب را می‌توانید از اینجا دریافت کنید.