مطالب
C# 8.0 - Pattern Matching
در نگارش‌های پیشین #C، بهبودهایی در زمینه‌ی Pattern matching وجود داشتند. در نگارش 8 نیز این بهبودها ادامه پیدا کرده‌اند که نتیجه‌ی آن به‌وجود آمدن روش جدیدی برای نوشتن عبارات switch است.


معرفی روش جدید نوشتن عبارات switch در C#8.0

فرض کنید یک enum که معرف تعدادی رنگ است را تعریف کرده‌ایم:
    public enum Rainbow
    {
        Red,
        Orange,
        Yellow,
        Green,
        Blue,
        Indigo,
        Violet
    }
همچنین کلاسی را نیز جهت تشکیل اشیاء رنگ مبتنی بر RGB تدارک دیده‌ایم:
    class RGBColor
    {
        internal byte Red { get; }
        internal byte Green { get; }
        internal byte Blue { get; }

        internal RGBColor(byte red, byte green, byte blue)
        {
            Red = red;
            Green = green;
            Blue = blue;
        }

        public override string ToString() => $"rgb({Red}, {Green}, {Blue})";
    }
اکنون هدف ما این است که اگر یکی از اعضای این enum را انتخاب کردیم، بتوانیم معادل رنگ RGB آن‌را نیز داشته باشیم. برای این منظور می‌توان switch ساده‌ی زیر را تشکیل داد:
        internal static RGBColor FromRainbow(Rainbow rainbowBolor)
        {
            switch (rainbowBolor)
            {
                case Rainbow.Red:
                    return new RGBColor(0xFF, 0x00, 0x00);
                case Rainbow.Orange:
                    return new RGBColor(0xFF, 0x7F, 0x00);
                case Rainbow.Yellow:
                    return new RGBColor(0xFF, 0xFF, 0x00);
                case Rainbow.Green:
                    return new RGBColor(0x00, 0xFF, 0x00);
                case Rainbow.Blue:
                    return new RGBColor(0x00, 0x00, 0xFF);
                case Rainbow.Indigo:
                    return new RGBColor(0x4B, 0x00, 0x82);
                case Rainbow.Violet:
                    return new RGBColor(0x94, 0x00, 0xD3);
                default:
                    throw new ArgumentException(message: "invalid enum value", paramName: nameof(rainbowBolor));
            };
        }
این کاری است که تا پیش از C# 8.0 به صورت متداولی انجام می‌شود. اکنون در C# 8.0 می‌توان عبارت switch فوق را به صورت زیر خلاصه کرد:
        internal static RGBColor TasteTheRainbow(Rainbow rainbowColor) =>
            rainbowColor switch
        {
            Rainbow.Red => new RGBColor(0xFF, 0x00, 0x00),
            Rainbow.Orange => new RGBColor(0xFF, 0x7F, 0x00),
            Rainbow.Yellow => new RGBColor(0xFF, 0xFF, 0x00),
            Rainbow.Green => new RGBColor(0x00, 0xFF, 0x00),
            Rainbow.Blue => new RGBColor(0x00, 0x00, 0xFF),
            Rainbow.Indigo => new RGBColor(0x4B, 0x00, 0x82),
            Rainbow.Violet => new RGBColor(0x94, 0x00, 0xD3),
            _ => throw new ArgumentException(message: "invalid enum value", paramName: nameof(rainbowColor)),
        };
- در این روش جدید، بجای اینکه با ذکر switch و سپس، مقداری/نوعی شروع شود، ابتدا با نوع شروع می‌شود و سپس واژه‌ی کلیدی switch ذکر خواهد شد.
- در ادامه تمام caseها حذف می‌شوند و بجای آن‌ها صرفا مقادیر مدنظر باقی می‌ماند. در اینجا <= به صورت expressed as خوانده می‌شود.
- caseهای مختلف با کاما از هم جدا می‌شوند.
- همچنین در سطر آخر آن نیز از یک discard استفاده شده‌است که معادل همان حالت default یا حالتی است که هیچ تطابقی صورت نگرفته باشد.
- به علاوه اگر دقت کنید، نتیجه‌ی نهایی این switch جدید، به صورت یک مقدار، توسط متد TasteTheRainbow، بازگشت داده شده‌است. بنابراین نوشتن یک چنین عباراتی در C# 8.0، مجاز است:
var operation = "+";
int a = 1, b = 2;
var result = operation switch
{
   "+" => a + b,
   "-" => a - b,
   "/" => a / b,
     _ => throw new NotSupportedException()
};


معرفی Property Patterns در C# 8.0

کلاس زیر را درنظر بگیرید که از تعدادی خاصیت عمومی تشکیل شده‌است:
    class Address
    {
        public string AddressLine1 { get; set; }
        public string AddressLine2 { get; set; }
        public string City { get; set; }
        public string State { get; set; }
        public string PostalCode { get; set; }
        public string CountryRegion { get; set; }
    }
اکنون فرض کنید که می‌خواهیم مالیات فروش را بر اساس آدرس و محل آن، محاسبه کنیم. در C# 8.0 با معرفی قابلیت الگوهای خواص، می‌توان بر روی آدرس، یک switch را تشکیل داد و سپس تک تک خواص آن‌را ارزیابی کرد:
    static class PropertyPatterns
    {
        internal static decimal ComputeSalesTax(
            Address location,
            decimal salePrice) =>
            location switch
        {
            { State: "Fars" } => salePrice * 0.06m,
            { State: "Tehran", City: "Tehran" } => salePrice * 0.056m,

            // Other cases removed for brevity...
            _ => 0M
        };
    }
در اینجا، سمت چپ هر case، داخل یک {} قرار می‌گیرد و در آن می‌توان مقادیر چندین خاصیت شیء location دریافتی را بررسی کرد. برای نمونه در سطر دوم آن، روش ارزیابی بیش از یک خاصیت را نیز مشاهده می‌کنید که روش ذکر آن شبیه به تعریف شیء‌های JSON است. در آخر نیز توسط یک discard، حالت default ذکر شده‌است.


معرفی Tuple Patterns در C# 8.0

در switch‌های C# 8.0، می‌توان از tuples نیز برای تشکیل قسمت case و همچنین مقداری که قرار است switch بر روی آن صورت گیرد، استفاده کرد:
    static class TuplePatterns
    {
        internal static string RockPaperScissors(
            string first,
            string second)
            => (first, second) switch
        {
            ("rock", "paper") => "Rock is covered by Paper. Paper wins!",
            ("rock", "scissors") => "Rock breaks Scissors. Rock wins!",
            ("paper", "rock") => "Paper covers Rock. Paper wins!",
            ("paper", "scissors") => "Paper is cut by Scissors. Scissors wins!",
            ("scissors", "rock") => "Scissors is broken by Rock. Rock wins!",
            ("scissors", "paper") => "Scissors cuts Paper. Scissors wins!",
            (_, _) => "tie"
        };
    }
در اینجا بر روی tuple ای که به صورت (first, second) تعریف شده، یک switch تعریف می‌شود. سپس برای نمونه 6 حالت مختلف برای آن پیش‌بینی شده و یک حالت default که آن نیز توسط discards معرفی می‌شود.


بهبودهای Pattern Matching بر روی اشیاء در C# 8.0

فرض کنید شیء پایه‌ی Shape را تعریف و بر اساس آن دو شیء جدید دایره و مستطیل را ایجاد کرده‌ایم:
    class Shape
    {
        protected internal double Height { get; }
        protected internal double Length { get; }

        protected Shape(double height = 0, double length = 0)
        {
            Height = height;
            Length = length;
        }
    }

    class Circle : Shape
    {
        internal double Radius => Height / 2;
        internal double Diameter => Radius * 2;
        internal double Circumference => 2 * Math.PI * Radius;

        internal Circle(double height = 10, double length = 10)
            : base(height, length) { }
    }

    class Rectangle : Shape
    {
        internal bool IsSquare => Height == Length;

        internal Rectangle(double height = 10, double length = 10)
            : base(height, length) { }
    }
امکان Pattern Matching بر روی اشیاء، در C# 7x نیز وجود دارد؛ اما در C# 8.0 می‌توان از روش جدید بیان عبارت switch آن به صورت زیر نیز در این حالت استفاده کرد:
    static class ObjectPatterns
    {
        internal static string ShapeDetails(this Shape shape)
            => shape switch
        {
            Circle c => $"circle with (C): {c.Circumference}",
            Rectangle s when s.IsSquare => $"L:{s.Length} H:{s.Height}, square",
            Rectangle r => $"L:{r.Length} H:{r.Height}, rectangle",
            _ => "Unknown shape!" // Discard
        };
    }
در اینجا یک شیء، به متد ShapeDetails ارسال شده و سپس جزئیاتی از آن دریافت می‌شود. مطابق روش C# 8.0، در اینجا نیز کار با ذکر نوع و سپس عبارت switch، شروع می‌شود. در ادامه روش بررسی نوع‌ها را در caseهای این سوئیچ ملاحظه می‌کنید. اگر در قسمت case آن Circle c ذکر شد، یعنی نوع shape از نوع دایره بوده و همچنین در همینجا می‌توان متغیر c را بر این اساس تعریف کرد و از آن استفاده نمود و یا می‌توان به کمک واژه‌ی کلیدی when، بر روی این متغیری که جدید تعریف شده، شرطی را نیز بررسی کرد. حالت default آن هم توسط discards معرفی می‌شود.


معرفی Positional Patterns در C# 8.0

در اینجا یک Point را داریم که می‌خواهیم بر اساس آن یک Quadrant را استخراج کنیم:
    class Point
    {
        public int X { get; }

        public int Y { get; }

        public Point(int x, int y) => (X, Y) = (x, y);

        public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
    }

    enum Quadrant
    {
        Unknown,
        Origin,
        One,
        Two,
        Three,
        Four,
        OnBorder
    }
برای این منظور می‌توان از الگوهای موقعیتی C# 8.0 استفاده کرد:
    static class PositionalPatterns
    {
        internal static Quadrant AsQuadrant(Point point) => point switch
        {
            (0, 0) => Quadrant.Origin,
            var (x, y) when x > 0 && y > 0 => Quadrant.One,
            var (x, y) when x < 0 && y > 0 => Quadrant.Two,
            var (x, y) when x < 0 && y < 0 => Quadrant.Three,
            var (x, y) when x > 0 && y < 0 => Quadrant.Four,
            (_, _) => Quadrant.OnBorder, // Either are 0, but not both
            _ => Quadrant.Unknown
        };
    }
اگر به کلاس Point دقت کنید، یک قسمت Deconstruct هم دارد. به همین جهت در قسمت‌های case این switch، زمانیکه برای مثال (0,0) ذکر می‌شود (که یک tuple literal است)، به صورت خودکار یک شیء Point متناظر را با مقادیر X و Y آن، تشکیل می‌دهد. همچنین روش‌های مختلف مقایسه‌ی مقادیر x و y این tuple را نیز در caseهای مختلف آن مشاهده می‌کنید.
در اینجا اگر دقت کنید و case مخصوص discards معرفی شده‌است. اولی برای حالت‌هایی است که هیچکدام از شرایط پیش از آن را برآورده نمی‌کند، مانند حالت (1,0)، در غیراینصورت سطر بعد از آن بازگشت داده می‌شود.
مطالب
الگوهای طراحی 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/

اشتراک‌ها
NET Core SDK 3.1.403. منتشر شد

This .NET Core SDK release includes the following released .NET Core and ASP.NET Core Runtimes.

  • .NET Core SDK 3.1.403
  • .NET Core Runtime 3.1.9
  • ASP.NET Core 3.1.9 
NET Core SDK 3.1.403. منتشر شد
اشتراک‌ها
NetBeans IDE 8.1 Beta منتشر شد

The latest version of the NetBeans IDE features a range of new tools for HTML5/JavaScript and supports mixed Java/C++ development. It is now available for download in beta.

NetBeans IDE 8.1 Beta منتشر شد
اشتراک‌ها
ASP.NET 5 Beta7 منتشر شد

ASP.NET 5 beta7 is now available both on NuGet and as a tooling update to Visual Studio 2015! This release also includes the first public preview of the .NET Execution Environment (DNX) for Mac and Linux based on .NET Core – no Mono required.

ASP.NET 5 Beta7 منتشر شد
اشتراک‌ها
Visual Studio 2022 17.2 منتشر شد

This release brings continued improvements to the C# and .NET experiences, new Git performance and experiences, updates for C++ developers, and new Azure tools for local development and deployment. We also continue to address your direct feedback submitted via Developer Community, addressing over 400 feedback items in this release! You can see the broader list of community feedback addressed in releases by visiting the fixes page on Developer Community. 

Visual Studio 2022 17.2 منتشر شد
اشتراک‌ها
NET Core 2.0. منتشر شد

 .NET Core 2.0 is available today as a final release. You can start developing with it at the command line, in your favorite text editor, in Visual Studio 2017 15.3, Visual Studio Code or Visual Studio for Mac. It is ready for production workloads, on your own hardware or your favorite cloud, like Microsoft Azure.

NET Core 2.0. منتشر شد
اشتراک‌ها
اولین بتای Bootstrap 4 منتشر شد

Two years in the making, we finally have our first beta release of Bootstrap 4. In that time, we’ve broken all the things at least twenty-seven times over with nearly 5,000 commits, 650+ files changed, 67,000 lines added, and 82,000 lines deleted. We also shipped six major alpha releases, a trio of official Themes, and even a job board for good measure.  

اولین بتای Bootstrap 4 منتشر شد