عنوان:

‫طراحی گردش کاری با استفاده از State machines - قسمت دوم


نویسنده: وحید نصیری
تاریخ: ۱۳۹۱/۱۰/۱۱ ۲۳:۱۰
آدرس: www.dntips.ir
معرفی کتابخانه stateless به عنوان جایگزین سبک وزنی برای Windows workflow foundation

کتابخانه سورس باز Stateless، برای طراحی و پیاده سازی «ماشین‌های حالت گردش کاری مانند» تهیه شده و مزایای زیر را نسبت به Windows workflow foundation دارا است:
- جمعا  30 کیلوبایت است!
- تمام اجزای آن سورس باز است.
- دارای API روان و ساده‌ای است.
- امکان تبدیل UML state diagrams، به نمونه معادل Stateless بسیار ساده و سریع است.
- به دلیل code first بودن، کار کردن با آن برای برنامه نویس‌ها ساده‌تر بوده و افزودن یا تغییر اجزای آن با کدنویسی به سادگی میسر است.

دریافت کتابخانه Stateless از Google code و یا از NuGet


پیاده سازی مثال کلید برق با Stateless

در ادامه همان مثال ساده  کلید برق قسمت قبل را با Stateless پیاده سازی خواهیم کرد:
using System;
using Stateless;

namespace StatelessTests
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                string on = "On", off = "Off";
                var space = ' ';

                var onOffSwitch = new StateMachine<string, char>(initialState: off);

                onOffSwitch.Configure(state: off).Permit(trigger: space, destinationState: on);
                onOffSwitch.Configure(state: on).Permit(trigger: space, destinationState: off);

                Console.WriteLine("Press <space> to toggle the switch. Any other key will raise an error.");

                while (true)
                {
                    Console.WriteLine("Switch is in state: " + onOffSwitch.State);
                    var pressed = Console.ReadKey(true).KeyChar;
                    onOffSwitch.Fire(trigger: pressed);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Exception: " + ex.Message);
                Console.WriteLine("Press any key to continue...");
                Console.ReadKey(true);
            }
        }
    }
}
کار با ایجاد یک وهله از ماشین حالت (new StateMachine) آغاز می‌شود. حالت آغازین آن (initialState) مطابق مثال قسمت قبل، مساوی off است.
امضای کلاس StateMachine را در ذیل مشاهده می‌کنید؛ جهت توضیح آرگومان‌های جنریک string و char معرفی شده در مثال:
public class StateMachine<TState, TTrigger>
که اولی بیانگر نوع حالات قابل تعریف است و دومی نوع رویداد قابل دریافت را مشخص می‌کند.
برای مثال در اینجا حالات روشن و خاموش، با رشته‌های on و off مشخص شده‌اند و رویداد قابل قبول دریافتی، کاراکتر فاصله است.
سپس نیاز است این ماشین حالت را برای معرفی رویدادهایی (trigger در اینجا) که سبب تغییر حالت آن می‌شوند، تنظیم کنیم. اینکار توسط متدهای Configure و Permit انجام خواهد شد. متد Configure، یکی از حالات از پیش تعیین شده را جهت تنظیم، مشخص می‌کند و سپس در متد Permit تعیین خواهیم کرد که بر اساس رخدادی مشخص (برای مثال در اینجا فشرده شدن کلید space) وضعیت حالت جاری، به وضعیت جدیدی (destinationState) منتقل شود.
نهایتا این ماشین حالت در یک حلقه بی‌نهایت مشغول به کار خواهد شد. برای نمونه یک Thread پس زمینه (BackgroundWorker) نیز می‌تواند همین کار را در برنامه‌های ویندوزی انجام دهد.


یک نکته
علاوه بر روش‌های یاد شده‌ی تشخیص الگوی ماشین حالت که در قسمت قبل بررسی شدند، مورد refactoring انبوهی از if و elseها و یا switchهای بسیار طولانی را نیز می‌توان به این لیست افزود.



استفاده از Stateless Designer برای تولید کدهای ماشین حالت

کتابخانه Stateless دارای یک طراح و Code generator بصری سورس باز است که آن‌را به شکل افزونه‌ای برای VS.NET می‌توانید در سایت Codeplex دریافت کنید. این طراح از کتابخانه GLEE برای رسم گراف استفاده می‌کند.

کار مقدماتی با آن به نحو زیر است:
الف) فایل StatelessDesignerPackage.vsix را از سایت کدپلکس دریافت و نصب کنید. البته نگارش فعلی آن فقط با VS 2012 سازگار است.
ب) ارجاعی را به اسمبلی stateless به پروژه خود اضافه نمائید (به یک پروژه جدید یا از پیش موجود).
ج) از منوی پروژه، گزینه Add new item را انتخاب کرده و سپس در صفحه ظاهر شده، گزینه جدید Stateless state machine را انتخاب و به پروژه اضافه نمائید.
کار با این طراح، با ادیت XML آن شروع می‌شود. برای مثال گردش کاری ارسال و تائید یک مطلب جدید را در بلاگی فرضی، به نحو زیر وارد نمائید:
<statemachine xmlns="http://statelessdesigner.codeplex.com/Schema">
  <settings>
    <itemname>BlogPostStateMachine</itemname>
    <namespace>StatelessTests</namespace>
    <class>public</class>
  </settings>
  <triggers>     
    <trigger>Save</trigger>
    <trigger>RequireEdit</trigger>
    <trigger>Accept</trigger>
    <trigger>Reject</trigger>
  </triggers>
  <states>     
    <state start="yes">Begin</state>
    <state>InProgress</state>     
    <state>Published</state>      
    <state>Rejected</state>      
  </states>
  <transitions>
    <transition trigger="Save" from="Begin" to="InProgress" />

    <transition trigger="Accept" from="InProgress" to="Published" />
    <transition trigger="Reject" from="InProgress" to="Rejected" />

    <transition trigger="Save" from="InProgress" to="InProgress" />

    <transition trigger="RequireEdit" from="Published" to="InProgress" />
    <transition trigger="RequireEdit" from="Rejected" to="InProgress" />
  </transitions>
</statemachine>
حاصل آن گراف زیر خواهد بود:


به علاوه کدهای زیر که به صورت خودکار تولید شده‌اند:
using Stateless;

namespace StatelessTests
{
  public class BlogPostStateMachine
  {
    public delegate void UnhandledTriggerDelegate(State state, Trigger trigger);
    public delegate void EntryExitDelegate();
    public delegate bool GuardClauseDelegate();

    public enum Trigger
    {
      Save,
      RequireEdit,
      Accept,
      Reject,
    }

    public enum State
    {
      Begin,
      InProgress,
      Published,
      Rejected,
    }

    private readonly StateMachine<State, Trigger> stateMachine = null;

    public EntryExitDelegate OnBeginEntry = null;
    public EntryExitDelegate OnBeginExit = null;
    public EntryExitDelegate OnInProgressEntry = null;
    public EntryExitDelegate OnInProgressExit = null;
    public EntryExitDelegate OnPublishedEntry = null;
    public EntryExitDelegate OnPublishedExit = null;
    public EntryExitDelegate OnRejectedEntry = null;
    public EntryExitDelegate OnRejectedExit = null;
    public GuardClauseDelegate GuardClauseFromBeginToInProgressUsingTriggerSave = null;
    public GuardClauseDelegate GuardClauseFromInProgressToPublishedUsingTriggerAccept = null;
    public GuardClauseDelegate GuardClauseFromInProgressToRejectedUsingTriggerReject = null;
    public GuardClauseDelegate GuardClauseFromInProgressToInProgressUsingTriggerSave = null;
    public GuardClauseDelegate GuardClauseFromPublishedToInProgressUsingTriggerRequireEdit = null;
    public GuardClauseDelegate GuardClauseFromRejectedToInProgressUsingTriggerRequireEdit = null;
    public UnhandledTriggerDelegate OnUnhandledTrigger = null;

    public BlogPost()
    {
      stateMachine = new StateMachine<State, Trigger>(State.Begin);
      stateMachine.Configure(State.Begin)
        .OnEntry(() => { if (OnBeginEntry != null) OnBeginEntry(); })
        .OnExit(() => { if (OnBeginExit != null) OnBeginExit(); })
        .PermitIf(Trigger.Save, State.InProgress , () => { if (GuardClauseFromBeginToInProgressUsingTriggerSave != null) return GuardClauseFromBeginToInProgressUsingTriggerSave(); return true; } )
      ;
      stateMachine.Configure(State.InProgress)
        .OnEntry(() => { if (OnInProgressEntry != null) OnInProgressEntry(); })
        .OnExit(() => { if (OnInProgressExit != null) OnInProgressExit(); })
        .PermitIf(Trigger.Accept, State.Published , () => { if (GuardClauseFromInProgressToPublishedUsingTriggerAccept != null) return GuardClauseFromInProgressToPublishedUsingTriggerAccept(); return true; } )
        .PermitIf(Trigger.Reject, State.Rejected , () => { if (GuardClauseFromInProgressToRejectedUsingTriggerReject != null) return GuardClauseFromInProgressToRejectedUsingTriggerReject(); return true; } )
        .PermitReentryIf(Trigger.Save , () => { if (GuardClauseFromInProgressToInProgressUsingTriggerSave != null) return GuardClauseFromInProgressToInProgressUsingTriggerSave(); return true; } )
      ;
      stateMachine.Configure(State.Published)
        .OnEntry(() => { if (OnPublishedEntry != null) OnPublishedEntry(); })
        .OnExit(() => { if (OnPublishedExit != null) OnPublishedExit(); })
        .PermitIf(Trigger.RequireEdit, State.InProgress , () => { if (GuardClauseFromPublishedToInProgressUsingTriggerRequireEdit != null) return GuardClauseFromPublishedToInProgressUsingTriggerRequireEdit(); return true; } )
      ;
      stateMachine.Configure(State.Rejected)
        .OnEntry(() => { if (OnRejectedEntry != null) OnRejectedEntry(); })
        .OnExit(() => { if (OnRejectedExit != null) OnRejectedExit(); })
        .PermitIf(Trigger.RequireEdit, State.InProgress , () => { if (GuardClauseFromRejectedToInProgressUsingTriggerRequireEdit != null) return GuardClauseFromRejectedToInProgressUsingTriggerRequireEdit(); return true; } )
      ;
      stateMachine.OnUnhandledTrigger((state, trigger) => { if (OnUnhandledTrigger != null) OnUnhandledTrigger(state, trigger); });
    }

    public bool TryFireTrigger(Trigger trigger)
    {
      if (!stateMachine.CanFire(trigger))
      {
        return false;
      }
      stateMachine.Fire(trigger);
      return true;
    }

    public State GetState
    {
      get
      {
        return stateMachine.State;
      }
    }
  }
}
توضیحات:

ماشین حالت فوق دارای چهار حالت شروع، در حال بررسی، منتشر شده و رد شده است. معمول است که این چهار حالت را به شکل یک enum معرفی کنند که در کدهای تولیدی فوق نیز به همین نحو عمل گردیده و public enum State معرف چهار حالت ذکر شده است. همچنین رویدادهای ذخیره، نیاز به ویرایش، ویرایش، تائید و رد نیز توسط public enum Trigger معرفی شده‌اند.
در قسمت Transitions، بر اساس یک رویداد (Trigger در اینجا)، انتقال از یک حالت به حالتی دیگر را سبب خواهیم شد.
تعاریف اصلی تنظیمات ماشین حالت، در سازنده کلاس BlogPostStateMachine انجام شده است. این تعاریف نیز بسیار ساده هستند. به ازای هر حالت، یک Configure داریم. در متدهای OnEntry و OnExit هر حالت، یک سری callback function فراخوانی خواهند شد. برای مثال در حالت Rejected یا Approved می‌توان ایمیلی را به ارسال کننده مطلب جهت یادآوری وضعیت رخ داده، ارسال نمود.
متدهای PermitIf سبب انتقال شرطی، به حالتی دیگر خواهند شد. برای مثال رد یا تائید یک مطلب نیاز به دسترسی مدیریتی خواهد داشت. این نوع موارد را توسط delgateهای Guard ایی که برای مدیریت شرط‌ها ایجاد کرده است، می‌توان تنظیم کرد. PermitReentryIf سبب بازگشت مجدد به همان حالت می‌گردد. برای مثال ویرایش و ذخیره یک مطلب در حال انتشار، سبب تائید یا رد آن نخواهد شد؛ صرفا عملیات ذخیره صورت گرفته و ماشین حالت مجددا در همان مرحله باقی خواهد ماند.

نحوه استفاده از ماشین حالت تولیدی:
همانطور که عنوان شد، حداقل استفاده از ماشین‌های حالت، refactoing انبوهی از if و else‌ها است که در حالت مدیریت یک چنین گردش‌های کاری باید تدارک دید.
namespace StatelessTests
{
    public class BlogPostManager
    {
        private BlogPostStateMachine _stateMachine;
        public BlogPostManager()
        {
            configureWorkflow();
        }

        private void configureWorkflow()
        {
            _stateMachine = new BlogPostStateMachine();

            _stateMachine.GuardClauseFromBeginToInProgressUsingTriggerSave = () => { return UserCanPost; };
            _stateMachine.OnBeginExit = () => { /* save data + save state + send an email to admin */ };

            _stateMachine.GuardClauseFromInProgressToPublishedUsingTriggerAccept = () => { return UserIsAdmin; };
            _stateMachine.GuardClauseFromInProgressToRejectedUsingTriggerReject = () => { return UserIsAdmin; };
            _stateMachine.GuardClauseFromInProgressToInProgressUsingTriggerSave = () => { return UserHasEditRights; };
            _stateMachine.OnInProgressExit = () => { /* save data + save state + send an email to user */ };

            _stateMachine.OnPublishedExit = () => { /* save data + save state + send an email to admin */ };
            _stateMachine.GuardClauseFromPublishedToInProgressUsingTriggerRequireEdit = () => { return UserHasEditRights; };

            _stateMachine.OnRejectedExit = () => { /* save data + save state + send an email to admin */ };
            _stateMachine.GuardClauseFromRejectedToInProgressUsingTriggerRequireEdit = () => { return UserHasEditRights; };
        }

        public bool UserIsAdmin
        {
            get
            {
                return true; // TODO: Evaluate if user is an admin.
            }
        }

        public bool UserCanPost
        {
            get
            {
                return true; // TODO: Evaluate if user is authenticated
            }
        }

        public bool UserHasEditRights
        {
            get
            {
                return true; // TODO: Evaluate if user is owner or admin
            }
        }

        // User actions
        public void Save() { _stateMachine.TryFireTrigger(BlogPostStateMachine.Trigger.Save); }
        public void RequireEdit() { _stateMachine.TryFireTrigger(BlogPostStateMachine.Trigger.RequireEdit); }

        // Admin actions        
        public void Accept() { _stateMachine.TryFireTrigger(BlogPostStateMachine.Trigger.Accept); }
        public void Reject() { _stateMachine.TryFireTrigger(BlogPostStateMachine.Trigger.Reject); }
    }
}
در کلاس فوق، نحوه استفاده از ماشین حالت تولیدی را مشاهده می‌کنید. در delegateهای Guard، سطوح دسترسی انجام عملیات بررسی خواهند شد. برای مثال، از بانک اطلاعاتی بر اساس اطلاعات کاربر جاری وارد شده به سیستم اخذ می‌گردند. در متدهای Exit هر مرحله، کارهای ذخیره سازی اطلاعات در بانک اطلاعاتی، ذخیره سازی حالت (مثلا در یک فیلد که بعدا قابل بازیابی باشد) صورت می‌گیرد و در صورت نیاز ایمیلی به اشخاص مختلف ارسال خواهد شد.
برای به حرکت درآوردن این ماشین، نیاز به یک سری اکشن متد نیز می‌باشد. تعدادی از این موارد را در انتهای کلاس فوق ملاحظه می‌کنید. کد نویسی آن‌ها در حد فراخوانی متد TryFireTrigger ماشین حالت است.

یک نکته:
ماشین حالت تولیدی به صورت پیش فرض در حالت State.Begin قرار دارد. می‌توان این مورد را از بانک اطلاعاتی خواند و سپس مقدار دهی نمود تا با هر بار وهله سازی ماشین حالت دقیقا مشخص باشد که در چه مرحله‌ای قرار داریم و TryFireTrigger بتواند بر این اساس تصمیم‌گیری کند که آیا مجاز است عملیاتی را انجام دهد یا خیر.

نظرات

  • مهمان در ۱۳۹۱/۱۰/۱۴ ۱۰:۲۵
    استفاده از یک Dictionary از نوع string و Action، چه مشکلی نسبت به این روش داره؟
    • وحید نصیری در ۱۳۹۱/۱۰/۱۴ ۱۰:۴۶
      مشکلی نداره. شما در هر زمانی می‌تونید دست به اختراع مجدد چرخ بزنید. با یک Dictionary از نوع string و Action فقط قسمت حالات و رویدادها رو طراحی کردید. مابقی قسمت‌ها مانند انتقال‌ها رو هم که اضافه کنید می‌شود کتابخانه Stateless.
  • امیر هاشم زاده در ۱۳۹۳/۰۶/۱۶ ۲۳:۲۶
    طبق کد زیر:
    _stateMachine.OnPublishedExit = () => { /* save data + save state + send an email to admin */ };
    _stateMachine.GuardClauseFromPublishedToInProgressUsingTriggerRequireEdit = () => { return UserHasEditRights };
    آیا درست متوجه شدم که: بازهم ابتدا سطح دسترسی بررسی می‌شود و سپس عملیات ذخیره سازی صورت می‌پذیرد؟!
    • وحید نصیری در ۱۳۹۳/۰۶/۱۶ ۲۳:۴۲
      این‌ها فقط یک سری تنظیم اولیه سیستم هستند. مهم نیست ترتیب معرفی آن‌ها به چه صورتی است. اجرای آن‌ها اینجا انجام نمی‌شود.
  • امیر هاشم زاده در ۱۳۹۳/۰۶/۱۷ ۰:۱۶
    1. ظاهرا در کلاس BlogPostStateMachine، متد سازنده آن به اشتباه BlogPost درج شده است.
    2. برای تغییر حالت و مقدار دهی آن از بانک اطلاعاتی، باید کد کلاس BlogPostStateMachine را مانند زیر تغییر دهیم:
    public BlogPostStateMachine(State state)
        {
          stateMachine = new StateMachine<State, Trigger>(state);


  • مجتبی آزاد در ۱۳۹۴/۰۸/۱۱ ۱۵:۶
    سلام؛ یک سوال برای من ایجاد شده، برای مواقعی که نیاز هست تا وضعیت state machine از دیتابیس خوانده شود، پراپرتی _stateMachine.State فقط‌خواندنی هست و  امکان اینکه از این طریق حالت جاری را اعلام کنیم وجود ندارد، آیا حالت جاری را از طریق سازنده کلاس state machine اعلام می‌کنیم؟
    • وحید نصیری در ۱۳۹۴/۰۸/۱۱ ۱۵:۲۰
      «... امکان اینکه از این طریق حالت جاری را اعلام کنیم وجود ندارد ...»
      این کار سورس باز هست. مطابق نیاز خودتان تغییرش دهید.
  • سالار ربال در ۱۳۹۴/۱۱/۳۰ ۲۱:۵۱
    با تشکر؛
    کل بحث جا افتاده است برایم  ولی در مثال BlogPostManager ، کمی غیر قابل درک میباشد که وقتی بحث سطح دسترسی مطرح است ، Guard‌ها چه استفاده ای خواهند داشت؟ منظور بنده این است که وقتی در یک برنامه فرضا Asp.net برای هر یک از Trigger‌ها یک اکشن متد خاص (برای ویرایش فیلد State رکورد) داشته باشیم ، همین چک کردن این مورد که آیا کاربر، نویسنده پست یا مدیر است هم در ابتدای اکشن باید صورت گیرد و اجازه دسترسی به ادامه کار داده نشود(Forbidden)  . اگر برداشت بنده صحیح نیست هم لطفا راهنمایی کنید.
    ممنون میشوم بحث را با مثال واقعی تحت وب یا ویندوز مطرح کنید. 
    • امیر هاشم زاده در ۱۳۹۴/۱۱/۳۰ ۲۲:۱۳
      جهت انتقال از یک وضعیت به وضعیت دیگر، بازگشت مقدار True در هر Guard مربوطه الزامی است حتی اگر یک اکشن متد خاص برای ویرایش فیلد وضعیت ماشین در نظر گرفته باشید.
      • سالار ربال در ۱۳۹۴/۱۱/۳۰ ۲۳:۶
        لطفا دلیل این که الزامی است رو بفرمایید.
        این که Guard‌ها جز اصول پیاده سازی State Machine‌ها میباشد شکی در آن نیست ولی واقعا در این سناریو استفاده از State Machine فقط کار را اضافه کرده. بنده چند مدت است دنبال این موضوع هستم که خودم را متقاعد کنم تا در همچین سناریو هایی از State Machine‌ها استفاده کنم ولی... 
        • امیر هاشم زاده در ۱۳۹۴/۱۱/۳۰ ۲۳:۳۰
          مقداردهی Guardها بدلیل وجود PermitIfها الزامی است ولی در Permitها خیر.
          • سالار ربال در ۱۳۹۴/۱۱/۳۰ ۲۳:۴۸
            اصلا بحث این نیست. فکر میکنم منظور بنده را متوجه نشده اید. عرض کردم ، اینکه این کتابخانه چه امکاناتی دارد و ... را متوجه هستم. صرفا دلیل استفاده از State Machine در همچین سناریویی بحث مورد نظر من بود. بنده قصد دارم از این مورد استفاده کنم ولی در سناریوهایی شبیه مثال مقاله (البته متوجه هستم که مثال هست صرفا) علاوه بر زیاد شدن کار، چه امکان دیگری دارد؟
    • وحید نصیری در ۱۳۹۴/۱۲/۰۱ ۰:۳
      - همیشه باید احتمال نفوذ پذیر بودن قسمت‌های مختلف برنامه را داد. وجود یک سطح امنیتی بیشتر به هیچ عنوان ضرری ندارد.
      - اگر اطلاعات دسترسی را در ابتدای کار در دسترس دارید، مابقی کار تزریق اطلاعات و پر کردن جاهای خالی است.
      - ممکن است دسترسی حرکت از یک مرحله به مرحله‌ی بعد، نیازمند محاسبات خاصی فراتر از سطح دسترسی‌های معمولی سیستم باشد. مثلا به یک کاربر خاص بدون دسترسی به پنل مدیریتی سیستم، امکان برگشت زدن یا تائید را داد. در یک چنین حالتی یک گردش کاری، سطح دسترسی محاسباتی خاص خودش را نیاز دارد.
      - یک گردش کاری را در یک scheduled job پس زمینه هم می‌شود اجرا کرد. بنابراین طراحی اصولی آن به همین شکل است (و غیر وابسته به فناوری خاصی).
    • وحید نصیری در ۱۳۹۴/۱۲/۰۱ ۱۲:۷
      پروژه‌ی دیگری هست از همین گروه به نام mass transit. این پروژه، یک زیر شاخه‌ی ساخت گردش کاری به نام « Automatonymous » هم دارد؛ با این مستندات. به تست‌های سورس آن نگاه کنید، مثال NHibernate هم در آن لحاظ شده‌است.
  • سالار ربال در ۱۳۹۷/۱۰/۰۴ ۱۴:۱۹
    - آیا چنین انتقال هایی به شکل زیر کار اصولی می‌باشد؟ 

    به عنوان مثال اگر Trigger1 همان رویداد Save ما بوده که با توجه به Domain ای که در آن قرار داریم انجام آن در حالت‌های مشخص شده رویداد معتبری باشد، ماشین حالت بالا صحیح می‌باشد یا اینکه باید با یک رویداد دیگری به حالت State1 برگشته و دوباره انتقال به حالت State2 را انجام دهیم. البته متوجه هستم که به ازای تک تک رویدادها همیشه و نه لزوما باید دکمه متناظری را باتوجه به حالت فعلی شیء مورد، توسط کاربر قابل مشاهده باشد. با توجه به اینکه اگر کاربر نهایی به ازای تک تک رویدادهای موجود، لزوما دکمه‌های متناظری را در فرم مشاهده نکند و نیاز باشد یک دکمه Shortcut مانندی قرار بگیرد که احتمال دارد باعث چندین انتقال شود، Fire کردن چندین رویداد در پشت صحنه، کار اصولی می‌باشد؟

    - نحوه استفاده از ماشین حالت ایجاد شده در لایه Application/ServiceLayer به چه شکل خواهد بود؟ حدس و سعی بنده به شکل زیر می‌باشد:
     public InvoiceStateMachine(InvoiceModel model)
            {
                _stateMachine =
                    new StateMachine<InvoiceStatus, Trigger>(() => model.Status, state => model.Status = state);
    سپس تعریف فیلدی به شکل زیر به منظور داشتن یک رویداد Parameterised، در دل ماشین حالت ایجاد شده:
    public Func<InvoiceModel, Task> OnSaveAsync = null;
    private readonly StateMachine<InvoiceStatus, Trigger>.TriggerWithParameters<InvoiceModel> _saveTrigger;

    کانفیگ آن به شکل زیر:
     _stateMachine.Configure(InvoiceStatus.Pending)
                    .OnEntryFromAsync(_saveTrigger, async invoice => await OnSaveAsync(invoice))
    سپس در InvoiceService به شکل زیر عمل کرده:
     private void InitializeStateMachine(InvoiceModel model)
            {
                _stateMachine = new InvoiceStateMachine(model)
                {
                    OnSaveAsync = async invoiceModel =>
                    {
                        var result = model.IsNew() ? await CreateAsync(model) : await EditAsync(model);
                        if (!result.Succeeded)
                            throw new BusinessRuleValidationException(result.Message);
                    }
                };
            }

    و در نهایت متدی که از بیرون فراخوانی خواهد شد:
    [Transactional]
    public async Task<Result> SaveAsync(InvoiceModel model)
    {
        try
        {
            InitializeStateMachine(model);
            await _stateMachine.TryFireSaveAsync(model);
            return Success();
        }
        catch (BusinessRuleValidationException e)
        {
            return Failed(e.Message);
        }
    }

    برای انتقال خطاهای ایجاد شده در زمان انتقال، راه حلی به غیر از صدور یک استثناء مشخص و گرفتن آن و ساخت خروجی مورد نظر متد سرویس به ذهنم نمی‌رسد.
    • وحید نصیری در ۱۳۹۷/۱۰/۰۴ ۱۵:۲۷
      - بله. برای نمونه تریگر Save بر روی حالت InProgress، در مثال جاری وجود دارد.
      - آیا برنامه‌ی وب یا چند ریسمانی هست؟ اگر بله از روش‌های قفل گذاری روی قسمتی از کد، برای جلوگیری از دسترسی همزمان به آن (حتی چند کلیک پشت سر هم کاربر هم در عمل همین معنا را پیدا می‌کند) استفاده کنید:
      // فقط یک ترد امکان دسترسی به کد را داشته باشد
      private static readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1,1);
      
      public async Task RunAsync()
      {
         // شروع تمام تردها معلق خواهند شد تا این سمافور به پایان برسد
         // پس از پایان کار ترد جاری، فقط یک ترد در حال انتظار، مجوز دسترسی به قطعه کد بعدی را خواهد یافت و به همین ترتیب برای سایر تردها
         await _semaphoreSlim.WaitAsync(); 
         try
         {
             await Task.Delay(20000); // Your code here
         }
         finally
         {
            _semaphoreSlim.Release();
         }
      }
      - خطاها هم می‌توانند یک state جدید مانند Rejected مثال این مطلب باشند.