مطالب
Persist ، Load و Bookmark در Workflow

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

قصد دارم با این مثال، طریقه persist شدن یک workflow در زمانیکه نیاز به انتظار برای تایید دارد و فراخوانی آن از همان نقطه پس از تایید مربوطه را توضیح دهم.

ساختار اینترفیس کاربری ما WPF می‌باشد. پس در ابتدا یک پروژه از نوع WPF ایجاد می‌کنیم. اسم solution  را PersistWF و اسم Project را PersistWF.UI انتخاب می‌کنیم.

در پروژه  UI نام فایل MainWindow.xaml  را به AddRequest.xaml تغییر می‌دهیم. همچنین اسم کلاس مربوطه را در codebehind 

همین طور مقدار StartupUri را هم در app.xaml اصلاح می‌کنیم

StartupUri="AddRequest.xaml"

Reference ‌های زیر رو هم به پروژه اضافه می‌کنیم 

•System.Activities
•System.Activities.DurableInstancing
•System.Configuration
•System.Data.Linq
•System.Runtime.DurableInstancing
•System.ServiceModel
•System.ServiceModel.Activities
•System.Workflow.ComponentModel
•System.Runtime.DurableInstancing
•System.Activities.DurableInstancing

قرار است کاربری ثبت نام کند، در فرایند ثبت، منتظر تایید یکی از مدیران قرار می‌گیرد. مدیر، لیست کاربران جدید را می‌بینید، یک کاربر را انتخاب می‌کند؛ مقادیر لازم را وارد می‌کند و سپس پروسه تایید را انجام می‌دهد که فراخوانی فرآیند مربوطه از همان قسمتی‌است که منتظر تایید مانده است.

برای Persist کردن workflow از کلاس SqlWorkflowInstanceStore   استفاده می‌کنم. این شی به connection ای به یک دیتابیس با یک ساختار معین احتیاج دارد. خوشبختانه اسکریپت‌های مورد نیاز این ساختار در پوشه [Drive]:\Windows\Microsoft.NET\Framework\v4.0.30319\SQL\en وجود دارند. دو اسکریپت با نام‌های SqlWorkflowInstanceStoreSchema و SqlWorkflowInstanceStoreLogic باید به ترتیب در دیتابیس اجرا شوند.

من یک دیتابیس با نام PersistWF ایجاد می‌کنم و اسکریپت‌ها را بر روی آن اجرا می‌کنم. یک جدول هم برای نگهداری کاربران ثبت شده در همین دیتابیس ایجاد می‌کنم.

و شمایل دیتابیس ما پس از اجرا کردن اسکریپت‌ها و ساختن جدول User  بدین شکل است: 

XAML زیر، ساختار فرم AddRequest می‌باشد که قرار است نقش UI برنامه را ایفا کند. آن را با XAML‌های پیش فرض عوض کنید. 

<Window x:Class="PersistWF.UI.AddRequest"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="520" Width="550" Loaded="Window_Loaded">
    <Grid MinWidth="300" MinHeight="100" Width="514">
        <Label Height="30" Margin="5,10,10,10" Name="lblName"  VerticalAlignment="Top" HorizontalAlignment="Left" Width="90"  HorizontalContentAlignment="Right">Name:</Label>
        <Label Height="30" Margin="270,10,10,10" Name="lblPhone"  VerticalAlignment="Top" HorizontalAlignment="Left" Width="90"  HorizontalContentAlignment="Right">Phone Number:</Label>
        <Label Height="30" Margin="5,40,10,10" Name="lblEmail"  VerticalAlignment="Top" HorizontalAlignment="Left" Width="90"  HorizontalContentAlignment="Right">Email:</Label>
        <TextBox Height="25" Margin="100,10,10,10" Name="txtName"  VerticalAlignment="Top" HorizontalAlignment="Left" Width="170" />
        <TextBox Height="25" Margin="365,10,10,10" Name="txtPhone"  VerticalAlignment="Top" HorizontalAlignment="Left" Width="100" />
        <TextBox Height="25" Margin="100,40,10,10" Name="txtEmail"  VerticalAlignment="Top" HorizontalAlignment="Left" Width="300" />
        <Button Height="23" Margin="100,86,0,0" Name="brnRegister"  VerticalAlignment="Top" HorizontalAlignment="Left" Width="70"  Click="brnRegister_Click">Register</Button>
        <ListView x:Name="lstUsers" Margin="10,125,10,10" Height="145"  VerticalAlignment="Top" ItemsSource="{Binding}"  HorizontalContentAlignment="Center"  SelectionChanged="lstUsers_SelectionChanged" >
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="Current User" Width="480">
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <StackPanel Orientation="Horizontal">
                                    <TextBlock Text="{Binding Name}"  Width="110"/>
                                    <TextBlock Text="{Binding Phone}"  Width="70"/>
                                    <TextBlock Text="{Binding Email}"  Width="130"/>
                                    <TextBlock Text="{Binding Status}"  Width="70"/>
                                    <TextBlock Text="{Binding AcceptedBy}"  Width="100"/>
                                </StackPanel>
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>
                </GridView>
            </ListView.View>
        </ListView>
        <Label Height="37" HorizontalAlignment="Stretch" Margin="10,272,5,10"  Name="lblSelectedNotes" VerticalAlignment="Top" Visibility="Hidden" />
        <Label Height="30" Margin="10,0,0,140" Name="lblAgent"  VerticalAlignment="Bottom" HorizontalAlignment="Left" Width="40"  HorizontalContentAlignment="Left" Visibility="Hidden">Admin Name:</Label>
        <TextBox Height="25" Margin="60,0,0,140" Name="txtAcceptedBy"  VerticalAlignment="Bottom" HorizontalAlignment="Left" Width="190"  Visibility="Hidden" />
        <Button Height="25" Margin="270,0,0,140" Name="btnAccept"  VerticalAlignment="Bottom" HorizontalAlignment="Left" Width="90"  Click="btnAccept_Click" Visibility="Hidden">Accept</Button>
        <Label Height="27" HorizontalAlignment="Left" Margin="10,0,0,110"  Name="lblEvent" VerticalAlignment="Bottom" Width="76">Event Log</Label>
        <ListBox Margin="12,0,5,12" Name="lstEvents" Height="100"  VerticalAlignment="Bottom" FontStretch="Condensed" FontSize="10" />
    </Grid>
</Window>

اگر همه چیز مرتب باشد؛ ساختار فرم شما باید به این شکل باشد 

اکثر workflow‌ها از activity معروف  WrteLine استفاده می‌کنند که برای نمایش یک رشته به کار می‌رود. ما هم در workflow مثالمان از این Activity استفاده می‌کنیم. اما برای اینکه مقادیری که توسط این Activity ایجاد می‌شوند در کادر event log فرم خودمان نمایش داده شود؛ احتیاج داریم که یک TextWriter سفارشی برای خودمان ایجاد کنیم. اما قبل از آن یک کلاس static در پروژه ایجاد می‌کنیم که بتوانیم در هر قسمتی، به فرم دسترسی داشته باشیم.

کلاسی را با نام ApplicationInterface به پروژه اضافه کرده و یک  Property استاتیک از جنس فرم AddRequest هم برای آن تعریف می‌کنیم:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PersistWF.UI
{
    public static class ApplicationInterface
    {
        public static AddRequest _app { get; set; }
    }
}

به Constructor کلاس موجود در فایل AddRequest.xaml.cs  این خط کد رو اضافه می‌کنم

        public AddRequest()
        {
            InitializeComponent();
            ApplicationInterface._app = this;
        }
این دو متد را هم به این کلاس اضافه می‌کنیم  
private void AddEvent(string szText)
        {
            lstEvents.Items.Add(szText);
        }
        public ListBox GetEventListBox()
        {
            return this.lstEvents;
        }

متد اول برای اضافه کردن یک event Log و متد دوم هم که کنسول لاگ را در اختیار درخواست کننده‌اش قرار می‌دهد.

و حالا کلاس TextWriter سفارشی‌امان را می‌نویسیم. یک کلاس به نام ListBoxTextWriter به پروژه اضافه می‌کنیم که از TextWriter مشتق می‌شود و محتویات آن‌را در زیر می‌بینید: 

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;

namespace PersistWF.UI
{
    public class ListBoxTextWriter : TextWriter 
    { 
         const string textClosed = "This TextWriter must be opened before use"; 
         private Encoding _encoding; 
         private bool _isOpen = false; 
         private ListBox _listBox; 
         public ListBoxTextWriter() 
         { 
             // Get the static list box 
             _listBox = ApplicationInterface._app.GetEventListBox(); 
             if (_listBox != null) 
             _isOpen = true; 
         } 
         public ListBoxTextWriter(ListBox listBox) 
         { 
             this._listBox = listBox; 
             this._isOpen = true; 
         } 
         public override Encoding Encoding 
         { 
             get 
             { 
                if (_encoding == null) 
                { 
                    _encoding = new UnicodeEncoding(false, false); 
                } 
                return _encoding; 
             } 
         }
         public override void Close()
         {
             this.Dispose(true);
         }
         protected override void Dispose(bool disposing)
         {
             this._isOpen = false;
             base.Dispose(disposing);
         }
         public override void Write(char value)
         {
             if (!this._isOpen)
                 throw new ApplicationException(textClosed); ;
             this._listBox.Dispatcher.BeginInvoke(new Action(() => this._listBox.Items.Add(value.ToString())));
         }
         public override void Write(string value)
         {
             if (!this._isOpen)
                 throw new ApplicationException(textClosed); 
             if (value != null)
                 this._listBox.Dispatcher.BeginInvoke(new Action(() => this._listBox.Items.Add(value)));
         }
         public override void Write(char[] buffer, int index, int count)
         {
             String toAdd = "";
             if (!this._isOpen)
                 throw new ApplicationException(textClosed); ;
             if (buffer == null || index < 0 || count < 0)
                 throw new ArgumentOutOfRangeException("buffer");
             if ((buffer.Length - index) < count)
                 throw new ArgumentException("The buffer is too small");
             for (int i = 0; i < count; i++)
                 toAdd += buffer[i];
             this._listBox.Dispatcher.BeginInvoke(new Action(() => this._listBox.Items.Add(toAdd)));
         }
    }
}

همان طور که می‌بینید کلاس ListBoxTextWriter از کلاس abstract  TextWriter  مشتق شده و پیاده سازی از متد Write را فراهم می‌کند تا یک رشته را به کنترل ListBox اضافه کنه. (البته سه تا از این متد‌ها را Override می‌کنیم تا بتوانیم یک رشته، یک کاراکتر و یا آرایه ای از کاراکتر‌ها را به ListBox اضافه کنیم) در constructor  پیشفرض از کلاس ApplicationInterface استفاده کردیم تا بتوانیم کنترل lstEvents را از فرم اصلی برنامه به دست بیاوریم. برای Add کردن از Dispatcher و متد BeginInvoke مرتبط با آن استفاده کردیم . این کار، متد را قادر می‌سازد حتی وقتی‌که از یک thread متفاوت فراخوانی می‌شود، کار کند.

حالا می‌توانیم از این کلاس، به عنوان مقدار خاصیت TextWriter برای WriteLine استفاده کنیم.

به کلاس ApplicationInterface برگردیم تا متد زیر را هم به آن اضافه کنیم 

public static void AddEvent(String status)
        {
            if (_app != null)
            {
                new ListBoxTextWriter(_app.GetEventListBox()).WriteLine(status);
            }
        }

این هم از constructor دومی استفاده می‌کنه برای معرفی ListBox.

برای ارتباط با دیتابیس از LINQ to SQL استفاده می‌کنیم تا User رو ذخیره و بازیابی کنیم. به پروژه یک آیتم از نوع LINQ to SQL با نام UserData.dbml اضافه می‌کنیم. به دیتابیس متصل شده و جدول User رو به محیط Design می‌کشیم. در ادامه برای شی کلاس SQLWorkflowInstanceStore هم از همین Connectionstring استفاده می‌کنیم. 

برای ایجاد workflow مورد نظر، به دو Activity سفارشی احتیاج داریم که باید خودمان ایجاد نماییم. یک پوشه با نام Activities به پروژه اضافه می‌کنم تا کلاس‌های مورد نظر را آن‌جا ایجاد کنیم.

1. یک Activity برای ایجاد User

این Activity تعدادی پارامتر از نوع InArgument دارد که توسط آن‌ها یک Instance از کلاس User ایجاد می‌کند و در حقیقت آن را به دیتابیس می‌فرستد و دخیره می‌کند. Connectionstring را هم می‌شود توسط یک آرگومان ورودی دیگر مقدار دهی کرد. یک آرگومان خروجی هم برای این Activity در نظر می‌گیریم تا User ایجاد شده را برگردانیم. روی پوشه‌ی Activities کلیک راست می‌کنیم و Add - NewItem را انتخاب می‌کنیم. از لیست workflow‌ها Template مربوط به CodeActivity را انتخاب کرده و یک CodeActivity با نام CreateUser ایجاد می‌کنیم 

محتویات این کلاس را هم مانند زیر کامل می‌کنیم 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Activities;

namespace PersistWF.UI.Activities
{

    public sealed class CreateUser : CodeActivity
    {


        public InArgument<string> Name { get; set; }
        public InArgument<string> Email { get; set; }
        public InArgument<string> Phone { get; set; }
        public InArgument<string> ConnectionString { get; set; }

        public OutArgument<User> User { get; set; }

        protected override void Execute(CodeActivityContext context)
        {
            // ایجاد کاربر
            User user = new User();
            user.Email = Email.Get(context);
            user.Name = Name.Get(context);
            user.Phone = Phone.Get(context);
            user.Status = "New";
     user.WorkflowID = context.WorkflowInstanceId;
            UserDataDataContext db = new UserDataDataContext(ConnectionString.Get(context));
            db.Users.InsertOnSubmit(user);
            db.SubmitChanges();
            User.Set(context, user);
        }
    }
}

متد Execute، توسط مقادیری که به عنوان پارامتر دریافت شده، یک شی از کلاس User ایجاد می‌کند و به کمک DataContext آن‌را در دیتابیس دخیره کرده و در آخر User ذخیره شده را در اختیار پارامتر خروجی قرار می‌دهد.

1. یک Activity برای انتظار دریافت تایید

این Activity قرار است Workflow را Idle کند تا زمانیکه مدیر دستور تایید را با فراخوانی مجدد workflow از این همین قسمت صادر نماید.

این Activity باید از NativeActivity مشتق شده و برای اینکه workflow را وادرا به معلق شدن کند کافی‌است خاصیت CanInduceIdle را با مقدار برگشتی true , override کنیم.

مثل قسمت قبل یک CodeActivity ایجاد می‌کنیم. اینبار با نام WaitForAccept که محتویاتش را هم مانند زیر تغییر می‌دهیم. 

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Activities;
using System.Workflow.ComponentModel;

namespace PersistWF.UI.Activities
{

    public sealed class WaitForAccept<T> : NativeActivity<T>
    {
        public WaitForAccept()
            :base()
        {

        }
        public string BookmarkName { get; set; }
        public OutArgument<T> Input { get; set; } 

        protected override void Execute(NativeActivityContext context)
        {
            context.CreateBookmark(BookmarkName, new BookmarkCallback(this.Continue));
        }

        private void Continue(NativeActivityContext context, Bookmark bookmark, object value)
        {
            Input.Set(context, (T)value); 
        }
        protected override bool CanInduceIdle
        {
            get
            {
                return true;
            }
        }
    }
}
این کلاس را generic نوشتم تا به جای User بشود هر پارامتر دیگه‌ای را به آن ارسال کرد. در واقع وقتی workflow به این Activity می‌رسد، Idle می‌شود. این activity  یک bookmark هم ایجاد می‌کند. ما وقتی workflow را با این bookmark فراخوانی کنیم؛ workflow از همینجا ادامه می‌یابد. فراخوانیbookmark می‌تواند همراه با وارد کردن یک  object باشد. متد Continue آن object را به آرگومان خروجی می‌دهد تا مسیر workflow را طی کند.
ما User  هایی را که به این نقطه رسیدنْ نمایش می‌دهیم. مدیر اونها را دیده و با مقدار دهی فیلد AcceptedBy، آن User را از اینجا به workflow می‌فرستد و ما user وارد شده را در ادامه‌ی فرآیند Accept می‌کنیم.
 
برای ایجاد workflow هم می‌توانید از designer استفاده کنید و هم می‌توانید کد مربوط به workflow را پیاده سازی کنید.

برای پیاده سازی از طریق کد، یک کلاس با نام UserWF ایجاد می‌کنیم و محتویات workflow را مانند زیر پیاده سازی خواهیم کرد:

using PersistWF.UI.Activities;
using System;
using System.Activities;
using System.Activities.Statements;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PersistWF.UI
{
     public sealed class UserWF : Activity
    {
        public InArgument<string> Name { get; set; }
        public InArgument<string> Email { get; set; }
        public InArgument<string> Phone { get; set; }
        public InArgument<string> ConnectionString { get; set; }
        public InArgument<TextWriter> Writer { get; set; }

        public UserWF()
        {
            Variable<User> User = new Variable<User> { Name = "User" };
            this.Implementation = () => new Sequence
            {
                DisplayName = "EnterUser",
                Variables = { User },
                Activities = { 
                     new CreateUser   //  1. ایجاد کاربر با ورود پارامتر‌های ورودی  
                    {
                        ConnectionString = new InArgument<string>(c=> ConnectionString.Get(c)),
                        Email = new InArgument<string>(c=> Email.Get(c)),
                        Name = new InArgument<string>(c=> Name.Get(c)),
                        Phone = new InArgument<string>(c=> Phone.Get(c)),
                        User = new OutArgument<User>(c=> User.Get(c))
                    },
                    new WriteLine // 2. لاگ مربوط به دخیره کاربر
                    {
                        TextWriter = new InArgument<TextWriter>(c=> Writer.Get(c)),
                        Text = new InArgument<string>(c=> string.Format("User {0} Registered and waiting for Accept", Name.Get(c) ) )
                    },
                    new InvokeMethod 
                     { 
                         TargetType = typeof(ApplicationInterface),  // 3. برای به روزرسانی لیست کاربران ثبت شده در نمایش فرم
                         MethodName = "NewUser", 
                         Parameters = 
                         { 
                            new InArgument<User>(env => User.Get(env)) 
                         } 
                     }, 
                     new WaitForAccept<User>  // 4. اینجا فرایند متوقف می‌شود و منتظر تایید مدیر می‌ماند
                     {  
                        BookmarkName = "GetAcceptes",
                        Input = new OutArgument<User>(env => User.Get(env))
                     },
                     new WriteLine // 5. لاگ مربوط به تایید شدن کاربر
                     {
                         TextWriter = new InArgument<TextWriter>(c=> Writer.Get(c)),
                         Text = new InArgument<string>(c=> string.Format("User {0} Accepter by {1}",Name.Get(c),User.Get(c).AcceptedBy))
                     }
                }

            };

        }

    }
}

اگر بخوایم از Designer استفاده کنیم.  فرایندمان چیزی شبیه شکل زیر خواهد بود 

به Application بر می‌گردیم تا آن را پیاده سازی کنیم. ابتدا به app.config که اتوماتیک ایجاد شده رفته تا اسم Connectionstring  رو به UserGenerator تغییر دهیم. محتویات درون app.config به شکل زیر است. 

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <configSections>
    </configSections>
    <connectionStrings>
        <add name="UserGenerator"
            connectionString="Data Source=.;Initial Catalog=PersistWF;Integrated Security=True"
            providerName="System.Data.SqlClient" />
    </connectionStrings>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
    </startup>
</configuration>

در کلاس AddRequest کد زیر را اضافه می‌کنم. برای نگهداری مقدار connectionstring 

private string _connectionString = "";

همچنین کد‌های زیر را به رویداد Load فرم اضافه می‌کنم تا مقدار ConnectionString را از Config بخوانم: 

Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
            ConnectionStringsSection css = (ConnectionStringsSection)config.GetSection("connectionStrings");
            _connectionString =  css.ConnectionStrings["UserGenerator"].ConnectionString;

خط زیر را هم به کلاس AddRequest اضافه نمایید.  

private InstanceStore _instanceStore;

این ارجاعیه  به کلاس InstanceStore که برای Persist و Load کردن workflow از آن استفاده می‌کنیم و کد‌های زیر را هم به رویداد Load فرم اضافه می‌کنیم.  

_instanceStore = new SqlWorkflowInstanceStore(_connectionString);
            InstanceView view = _instanceStore.Execute(_instanceStore.CreateInstanceHandle(), new CreateWorkflowOwnerCommand(), TimeSpan.FromSeconds(30));
            _instanceStore.DefaultInstanceOwner = view.InstanceOwner;

InstanceStore یک کلاس abstract  می باشد که همه‌ی Provider‌های مربوط به persistence از آن مشتق می‌شوند. در این پروژه من از کلاس SqlWorkflowInstanceStore استفاده کردم تا workflow‌ها را در دیتابیسSQL Server ذخیره کنم.

برای ایجاد یک Request مقادیر را از فرم دریافت کرده، یک User ایجاد می‌کنیم و آن را در فرآیند به جریان می‌اندازیم. این کار را در رویداد کلیک دکمه Register انجام می‌دهیم 

private void brnRegister_Click(object sender, RoutedEventArgs e)
        {
            Dictionary<string, object> parameters = new Dictionary<string, object>();
            parameters.Add("Name", txtName.Text);
            parameters.Add("Phone", txtPhone.Text);
            parameters.Add("Email", txtEmail.Text);
            parameters.Add("ConnectionString", _connectionString);
            parameters.Add("Writer", new ListBoxTextWriter(lstEvents));
            WorkflowApplication i = new WorkflowApplication
            (new UserWF(), parameters);
            // Setup persistence 
            i.InstanceStore = _instanceStore;
            i.PersistableIdle = (waiea) => PersistableIdleAction.Unload;
            i.Run(); 
        }

پارامتر‌های ورودی را از روی فرم مقدار دهی می‌کنیم. یک شی از کلاس WorkflowApplication ایجاد می‌کنیم. خاصیت InstanceStore آن را با Store ای که ایجاد کردیم مقدار دهی می‌کنیم. توسط رویداد PersistableIdle فرآیند رو مجبور می‌کنیم به Persist شدن و Unload شدن.

و سپس فرایند را اجرا می‌کنم.

اگر یادتان باشد، در فرآیند، از یک InvoceMethod استفاده کردیم. متد مورد نظر را هم در کلاس ApplicationInterface.cs ایجاد می‌کنیم. 

public static void NewUser(User l)
        {
            if (_app != null)
                _app.AddNewUser(l);
        }

همین طور که می‌بینید، یک متد هم در کلاس AddRequest ایجاد می‌شود؛ با این محتوا 

public void AddNewUser(User l)
        {
            this.lstUsers.Dispatcher.BeginInvoke(new Action(() => this.lstUsers.Items.Add(l)));
        }

این متد فقط یک کاربر را به لیست کاربران اضافه می‌کند. این لیست همه کاربران را نشان می‌دهد. توسط رویداد SelectionChanged این کنترل، کاربر انتخاب شده را بررسی کرده در صورتی که کاربر جدید باشد، امکان تایید شدن را برایش فراهم می‌کنیم؛ که نمایش دکمه تایید است. 

private void lstUsers_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (lstUsers.SelectedIndex >= 0)
            {
                User l = (User)lstUsers.Items[lstUsers.SelectedIndex];
                lblSelectedNotes.Visibility = Visibility.Visible;
                if (l.Status == "New")
                {
                    lblAgent.Visibility = Visibility.Visible;
                    txtAcceptedBy.Visibility = Visibility.Visible;
                    btnAccept.Visibility = Visibility.Visible;
                }
                else
                {
                    lblAgent.Visibility = Visibility.Hidden;
                    txtAcceptedBy.Visibility = Visibility.Hidden;
                    btnAccept.Visibility = Visibility.Hidden;
                }
            }
            else
            {
                lblSelectedNotes.Content = "";
                lblSelectedNotes.Visibility = Visibility.Hidden;
                lblAgent.Visibility = Visibility.Hidden;
                txtAcceptedBy.Visibility = Visibility.Hidden;
                btnAccept.Visibility = Visibility.Hidden;
            } 
        }

و برای رویداد کلیک دکمه تایید کاربر : 

private void btnAccept_Click(object sender, RoutedEventArgs e)
        {
            if (lstUsers.SelectedIndex >= 0) 
             { 
                 User u = (User)lstUsers.Items[lstUsers.SelectedIndex]; 
                 Guid id = u.WorkflowID.Value;
                 UserDataDataContext dc = new UserDataDataContext(_connectionString); 
                 dc.Refresh(RefreshMode.OverwriteCurrentValues, dc.Users);
                 u = dc.Users.SingleOrDefault<User>(x => x.WorkflowID == id); 
                 if (u != null) 
                 { 
                     u.AcceptedBy = txtAcceptedBy.Text; 
                     u.Status = "Assigned"; 
                     dc.SubmitChanges();
                     // Clear the input 
                     txtAcceptedBy.Text = "";
                 }
                 // Update the grid 
                 lstUsers.Items[lstUsers.SelectedIndex] = u;
                 lstUsers.Items.Refresh();
                 WorkflowApplication i = new WorkflowApplication(new UserWF());
                 i.InstanceStore = _instanceStore;
                 i.PersistableIdle = (waiea) => PersistableIdleAction.Unload;
                 i.Load(id);
                 try
                 {
                     i.ResumeBookmark("GetAcceptes", u);
                 }
                 catch (Exception e2)
                 {
                     AddEvent(e2.Message);
                 }
             } 
        }

کاربر را انتخاب می‌کنم مقادیرش را تنظیم می‌کنیم. آن را دخیره کرده و workflow را از روی guid مربوط به آن که قبلا در فرآیند به Entity دادیم، Load می‌کنیم و همانطور که می‌بینید توسط متد ResumeBookmark فرآیند رو از جایی که می‌خواهیم ادامه می‌دهیم. البته می‌توان تایید کاربر را هم در خود فرآیند انجام داد و چون نوشتن Activity  مرتبط با آن تقریبا تکراری است با اجازه‌ی شما من اون رو ننوشتم و زحمتش با خودتونه.

حالا فقط مانده‌است که همه کاربران را در ابتدای نمایش فرم از دیتابیس فراخوانی کنیم و در لیست نمایش دهیم:

private void LoadExistingLeads()
        {
            UserDataDataContext dc = new UserDataDataContext(_connectionString);
            dc.Refresh(RefreshMode.OverwriteCurrentValues, dc.Users);
            IEnumerable<User> q = dc.Users;
            foreach (User u in q)
            {
                AddNewUser(u);
            }
        }

و فراخوانی این متد را به انتهای رویداد Load صفحه واگذار می‌کنیم.

پروژه رو اجرا کرده و یک کاربر را اضافه می‌کنم. همانطور که می‌دانید این کاربر در فرآیند ایجاد و در دیتابیس ذخیره می‌شود

برنامه را می‌بندم و دوباره اجرا می‌کنم. کاربر را انتخاب می‌کنم و یک نام برای admin انتخاب و آن را تایید می‌کنم. فرآیند را از bookmark مورد نظر اجرا کرده و به پایان می‌رسد. با بسته شدن برنامه، فرایند Idle و Unload می‌شود و ذخیره آن در sqlserver صورت می‌گیرد. 

اشتراک‌ها
استفاده از default و check در SQL

SQL is unusual is that data is not passively stored. Instead you use declarative SQL to specify the rules that underlie the data and its integrity. When used properly, constraints can avoid having to provide a lot of logic elsewhere. CHECK() and DEFAULT can do a lot to ensure that your data is correct 

استفاده از default و check در SQL
اشتراک‌ها
مفهوم زبان مشترک در DDD

Account

A bank account allows us to send and receive money and has its unique number. Anytime we tell about an account in a bank, an account is always a bank account. In the other hand, an account in an information system is used to authorize a user. We have the term "account" meaning something absolutely different in two different domains. Domain has an impact on what we imagine when someone says a concrete term. So we have to learn and specify domain terms first.

Price

Let's speak about e-shop domain. What is a price? For us, as customers, it is how much we pay. A manager can think about price as an amount that his company pays to the supplier. For an accountant, a price is just a number. And e-shop programmer is now confused.

Language is crucial because customers and experts are telling their stories in their language. But it is also natural language, inaccurate, ambiguous, context-aware. And as we can see, language can be tricky even within one domain. 


مفهوم زبان مشترک در DDD
اشتراک‌ها
زبان Rust در حال از هم پاشیدن است!

While Rust has earned a dedicated following, it's not without its fair share of controversies and problems. In this video I'll talk about the negative response and backlash from the Rust community in the wake of the most recent controversy, its connections to past incidents, and give my personal thoughts.  

زبان Rust در حال از هم پاشیدن است!
اشتراک‌ها
الگوهای طراحی در قرن 21 ام
 What do you want from me? I want you to stop using design patterns. Um…OK, let me rephrase that. I want you to stop using design patterns like it’s 1999. 
الگوهای طراحی در قرن 21 ام
مطالب
CoffeeScript #7

اصطلاحات عمومی CoffeeScript

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

Each

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

for (var i=0; i < array.length; i++)
  myFunction(array[i]);

array.forEach(function(item, i){
  myFunction(item)
});
اگرچه تابع ()forEach مختصر و خواناتر است ولی یک مشکل دارد؛ به دلیل فراخوانی تابع callback در هر بار اجرای حلقه، بسیار کندتر از حلقه for اجرا می‌شود.
حال به نحوه‌ی کارکرد CoffeeScript دقت کنید.
myFunction(item) for item in array
که پس از کامپایل می‌شود:
var i, item, len;

for (i = 0, len = array.length; i < len; i++) {
  item = array[i];
  myFunction(item);
}
همانطوری که مشاهده می‌کنید، از نظر syntax بسیار ساده و با خوانایی بالا است و مطمئن هستم شما هم با من موافق هستید و نکته‌ی مهمی که وجود دارد، کامپایل حلقه‌ی با ظاهر forEach به حلقه‌ی for، توسط CoffeeScript و حفظ سرعت اجرای آن است.

Map

همانند تابع forEach که در استاندارد ES5 قرار داشت، تابع دیگری به نام ()map وجود دارد که از نظر syntax بسیار خلاصه‌تر از حلقه‌ی for می‌باشد. ولی متاسفانه همانند تابع forEach، این تابع نیز به دلیل فراخوانی تابع، بسیار کندتر از for اجرا می‌شود.

var result = []
for (var i=0; i < array.length; i++)
  result.push(array[i].name)

var result = array.map(function(item, i){
  return item.name;
});
همانطور که مشاهده می‌کنید در اینجا طریقه‌ی استفاده از تابع map و پیاده سازی آن بدون استفاده از تابع map نشان داده شده است. حال به مثال زیر توجه کنید:
result = (item.name for item in array)
با استفاده از ساختار حلقه‌ها که در قسمت 4 گفتیم و تنها با قراردادن () در اطراف آن می‌توان تابع map را به راحتی پیاده سازی کرد.
نتیجه‌ی کامپایل مثال بالا می‌شود:
var item, result;

result = (function() {
  var i, len, results;
  results = [];
  for (i = 0, len = array.length; i < len; i++) {
    item = array[i];
    results.push(item.name);
  }
  return results;
})();

Select

یکی دیگر از توابع ES5، تابع ()filter است که برای کاهش خانه‌های آرایه استفاده می‌شود.

var result = []
for (var i=0; i < array.length; i++)
  if (array[i].name == "test")
    result.push(array[i])

result = array.filter(function(item, i){
  return item.name == "test"
});
CoffeeScript با استفاده از کلمه‌ی کلیدی when، عمل فیلتر کردن آیتم‌هایی را که نمی‌خواهیم در آرایه باشند، انجام می‌دهد و در پشت صحنه، با استفاده از یک حلقه‌ی for این عمل را انجام می‌دهد.
result = (item for item in array when item.name is "test")
در اینجا نیز همانند تابع map برای جلوگیری از تداخل متغیرها از یک تابع بی‌نام استفاده می‌کند.
var item, result;

result = (function() {
  var i, len, results;
  results = [];
  for (i = 0, len = array.length; i < len; i++) {
    item = array[i];
    if (item.name === "test") {
      results.push(item);
    }
  }
  return results;
})();

نکته‌ی مهم: در صورت فراموشی اضافه کردن () در اطراف حلقه‌ی نوشته شده، نتیجه‌ی صحیحی تولید نخواهد شد و نتها آخرین عضو از خروجی را باز می‌گرداند.
var i, item, len, result;

for (i = 0, len = array.length; i < len; i++) {
  item = array[i];
  if (item.name === "test") {
    result = item;
  }
}

قوه‌ی درک CoffeeScript بسیار بالا و انعطاف پذیر است، به مثال زیر توجه کنید:
passed = []
failed = []
(if score > 60 then passed else failed).push score for score in [49, 58, 76, 82, 88, 90]

# Or
passed = (score for score in scores when score > 60)
و یا در صورتیکه طول خط نوشته شده زیاد باشد می‌توانید به صورت چند خطی آن را بنویسید:
passed = []
failed = []
for score in [49, 58, 76, 82, 88, 90]
  (if score > 60 then passed else failed).push score
و نتیجه‌ی کامپایل مثال آخر می‌شود:
var failed, i, len, passed, ref, score;

passed = [];

failed = [];

ref = [49, 58, 76, 82, 88, 90];
for (i = 0, len = ref.length; i < len; i++) {
  score = ref[i];
  (score > 60 ? passed : failed).push(score);
}
مطالب
C# 8.0 - Ranges & Indices
نوع Span به همراه NET Core 2.1. ارائه شد. یکی از مهم‌ترین مزایای آن امکان دسترسی به قسمتی از حافظه (توسط متد Split آن)، بدون ایجاد سربار کپی یا تخصیص مجدد حافظه‌ای برای دسترسی به آن است. قدم بعدی، بسط این قابلیت به امکانات ذاتی زبان #C است؛ تحت عنوان ویژگی Ranges که امکان دسترسی مستقیم به بازه‌ای/قسمتی از آرایه‌ها، رشته‌ها و یا Spanها را میسر می‌کند.


معرفی عملگر Hat

برای دسترسی به آخرین عضو یک آرایه عموما از روش زیر استفاده می‌شود:
var integerArray = new int[3];
var lastItem = integerArray[integerArray.Length - 1];
یعنی از آخر شروع به شمارش کرده و 1 واحد از آن کم می‌کنیم (این عدد 1 را به‌خاطر داشته باشید) و یا اگر بخواهیم از LINQ استفاده کنیم می‌توان از متد Last آن استفاده کرد:
var integerList = integerArray.ToList();
integerList.Last();
همچنین اگر بخواهیم دومین عنصر از آخر آن‌را دریافت کنیم:
var secondToLast = integerArray[integerArray.Length - 2];
نیز مجددا از آخر شروع به شمارش کرده و 2 واحد، از آن کم می‌کنیم (این عدد 2 را نیز به‌خاطر داشته باشید).

این شمردن‌های از آخر در C# 8.0 توسط ارائه‌ی عملگر hat یا همان ^ که پیشتر کار xor را انجام می‌داد (و البته هنوز هم در جای خودش همین مفهوم را دارد)، میسر شده‌است:
var lastItem = integerArray[^1];
این قطعه کد یعنی به دنبال ایندکس X، از آخر آرایه هستیم.

نکته‌ی مهم: کسانیکه شروع به آموزش برنامه نویسی می‌کنند، مدتی طول می‌کشد تا عادت کنند که اولین ایندکس یک آرایه از صفر شروع می‌شود. در اینجا باید درنظر داشت که با بکارگیری «عملگر کلاه»، آخرین ایندکس یک آرایه از «یک» شروع می‌شود و نه از صفر. برای نمونه در مثال زیر به خوبی تفاوت بین ایندکس از ابتدا و ایندکس از انتها را می‌توانید مشاهده کنید:
var words = new string[]
{
                // index from start    index from end
    "The",      // 0                   ^9
    "quick",    // 1                   ^8
    "brown",    // 2                   ^7
    "fox",      // 3                   ^6
    "jumped",   // 4                   ^5
    "over",     // 5                   ^4
    "the",      // 6                   ^3
    "lazy",     // 7                   ^2
    "dog"       // 8                   ^1
};              // 9 (or words.Length) ^0
آرایه‌ی فوق، 9 عضو دارد. در این حالت اولین عنصر آن با ایندکس صفر قابل دسترسی است. در همین حالت همین ایندکس را اگر از آخر محاسبه کنیم، 9 خواهد بود و همینطور برای مابقی.
در حالت کلی ایندکس n^ معادل sequence.Length - n است. بنابراین sequence[^0] به معنای sequence[sequence.Length] است و هر دو مورد یک index out of range exception را صادر می‌کنند.

IDE نیز با فعال سازی C# 8.0، زمانیکه به قطعه کد زیر می‌رسد، زیر words.Length - 1 خط کشیده و پیشنهاد می‌دهد که بهتر است از 1^ استفاده کنید:
Console.WriteLine($"The last word is {words[words.Length - 1]}");



معرفی نوع جدید Index

در C# 8.0 زمانیکه می‌نویسم 1^، در حقیقت قطعه کد زیر را ایجاد کرده‌ایم:
var index = new Index(value: 1, fromEnd: true);
Index indexStruct = ^1;
var indexShortHand = ^1;
Index یک struct و نوع جدید در C# 8.0 می‌باشد که در فضای نام System قرار گرفته‌است. سه سطر فوق دقیقا به یک معنا هستند و هر کدام خلاصه شده و ساده شده‌ی سطر قبلی است.
در سطر اول، پارامتر fromEnd نیز قابل تعریف است. این fromEnd با مقدار true، همان عملگر ^ در اینجا است و عدم ذکر این عملگر به معنای false بودن آن است:
int[] a = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
Console.WriteLine(a[a.Length – 2]); // will write 8 on console.
Console.WriteLine(a[^2]); // will write 8 on console.

Index i5 = 5;
Console.WriteLine(a[i5]);        //will write 5 on console.

Index i2fromEnd = ^2;
Console.WriteLine(a[i2fromEnd]); // will write 8 on console.
در این مثال دو نمونه کاربرد fromEnd با مقدار false و سپس true را ملاحظه می‌کنید. در حالتیکه Index i5 = 5 تعریف شده‌است، دسترسی به عناصر آرایه همانند قبل از ایندکس صفر و از آغاز شروع می‌شود و نه از ایندکس یک.


روش دسترسی به بازه‌ای از اعضای یک آرایه تا پیش از C# 8.0

فرض کنید آرایه‌ای از اعداد بین 1 تا 10 را به صورت زیر ایجاد کرده‌اید:
var numbers = Enumerable.Range(1, 10).ToArray();
اکنون اگر بخواهیم به بازه‌ی مشخصی درون این آرایه دسترسی پیدا کنیم، می‌توان حداقل به یکی از دو روش زیر عمل کرد:
var (start, end) = (1, 7); 
var length = end - start; 

// Using LINQ 
var subset1 = numbers.Skip(start).Take(length);
 
// Or using Array.Copy 
var subset2 = new int[length];
Array.Copy(numbers, start, subset2, 0, length);
یا می‌توان برای مثال توسط LINQ، از متدهای Skip و Take آن برای جدا کردن بازه‌ای از آرایه‌ی numbers استفاده کرد و یا حتی می‌توان از روش کپی کردن آرایه‌ها به آرایه‌ای جدید نیز کمک گرفت که در هر دو حالت، مراحلی که باید طی شوند قابل توجه است. با ارائه‌ی C# 8.0، این نوع دسترسی‌ها جزئی از قابلیت‌های زبان شده‌اند.


روش دسترسی به بازه‌ای از اعضای یک آرایه در C# 8.0

در C# 8.0 برای دسترسی به بازه‌ای از عناصر یک آرایه می‌توان از range expression که به صورت x..y نوشته می‌شود، استفاده کرد. در ادامه، مثال‌هایی را از کاربردهای عبارت .. ملاحظه می‌کنید:
var myArray = new string[] { "Item1", "Item2", "Item3", "Item4", "Item5" };
3..1 به معنای انتخاب بازه‌ای از المان 2 تا المان 3 است. در اینجا به واژه‌ی «المان» دقت کنید که معادل ایندکس آن در آرایه نیست. یعنی عدد ابتدای یک بازه دقیقا به ایندکس آن در آرایه اشاره می‌کند و عدد انتهای بازه، به ایندکس ماقبل آن (از این جهت که بتوان توسط 0^، انتهای بازه را مشخص کرد؛ بدون دریافت استثنای index out of range). به همین جهت به ابتدای بازه می‌گویند inclusive و به انتهای آن exclusive:
 var fromIndexToX = myArray[1..3]; // = [Item2, Item3]
1^..1 به معنای انتخاب بازه‌ای از المان 2، تا المان یکی مانده به آخر است:
var fromIndexToXFromTheEnd = myArray[1..^1]; // = [ "Item2", "Item3", "Item4" ]

ذکر انتهای بازه اجباری نیست و اگر ذکر نشود به معنای 0^ است. برای مثال ..1 به معنای انتخاب بازه‌ای از المان 2، تا آخرین المان است:
var fromAnIndexToTheEnd = myArray[1..]; // = [ "Item2", "Item3", "Item4", "Item5" ]

ذکر ابتدای بازه نیز اجباری نیست و اگر ذکر نشود به معنای عدد صفر است. برای مثال 3.. به معنای انتخاب بازه‌ای از اولین المان، تا سومین المان است:
 var fromTheStartToAnIndex = myArray[..3]; // = [ "Item1", "Item2", "Item3" ]

اگر ابتدا و انتهای بازه ذکر نشوند، کل بازه و تمام عناصر آن بازگشت داده می‌شوند:
 var entireRange = myArray[..]; // = [ "Item1", "Item2", "Item3", "Item4", "Item5" ]
همچنین [0^..0] نیز به معنای کل بازه است.

مثالی دیگر: بازنویسی یک حلقه‌ی for با foreach
حلقه‌ی for زیر را
var myArray = new string[] { "Item1", "Item2", "Item3", "Item4", "Item5" };
for (int i = 1; i <= 3; i++)
{
  Console.WriteLine(myArray[i]);
}
توسط range expression می‌توان به صورت زیر بازنویسی کرد:
foreach (var item in myArray[1..4]) // = [ "Item2", "Item3", "Item4" ]
{
  Console.WriteLine(item);
}
بنابراین همانطور که مشاهده می‌کنید، ذکر بازه‌ی 4..1 به صورت حلقه‌ی for (int i = 1; i < 4; i++) تفسیر می‌شود و نه حلقه‌ی for (int i = 1; i <= 4; i++)
یعنی ابتدای آن inclusive است و انتهای آن exclusive


چند مثال کاربردی و متداول از بازه‌ها

using System;
using System.Linq;

namespace ConsoleApp
{
    class Program
    {
        private static readonly int[] _numbers = Enumerable.Range(1, 10).ToArray();

        static void Main()
        {
            var skip2CharactersAndTake2Characters = _numbers[2..4]; // صرفنظر کردن از دو عنصر اول و سپس انتخاب دو عنصر
            var skipFirstAndLastCharacter = _numbers[1..^1]; // صرفنظر کردن از دو عنصر اول و آخر
            var last3Characters = _numbers[^3..]; // انتخاب بازه‌ای شامل سه عنصر آخر
            var first4Characters = _numbers[0..4]; // دریافت بازه‌ای از 4 عنصر اول
            var rangeStartFrom2 = _numbers[2..]; // دریافت بازه‌ای شروع شده از المان دوم تا آخر
            var skipLast3Characters = _numbers[..^3]; // صرفنظر کردن از سه المان آخر
            var rangeAll = _numbers[..]; // انتخاب کل بازه
        }
    }
}


معرفی نوع جدید Range

در C# 8.0 زمانیکه می‌نویسم 4..1، در حقیقت قطعه کد زیر را ایجاد کرده‌ایم:
var range = new Range(1, 4);
Range rangeStruct = 1..4;
var rangeShortHand = 1..4;
Range نیز یک struct و نوع جدید در C# 8.0 می‌باشد که در فضای نام System قرار گرفته‌است. سه سطر فوق دقیقا به یک معنا هستند و هر کدام خلاصه شده و ساده شده‌ی سطر قبلی است.

یک مثال: استفاده از نوع جدید Range به عنوان پارامتر یک متد
using System;
using System.Linq;

namespace ConsoleApp
{
    class Program
    {
        private static readonly int[] _numbers = Enumerable.Range(1, 10).ToArray();
        static void Print(Range range) => Console.WriteLine($"{range} => {string.Join(", ", _numbers[range])}");

        static void Main()
        {
            Print(1..3); // 1..3 => 2, 3
            Print(..3);      // 0..3 => 1, 2, 3
            Print(3..);      // 3..^0 => 4, 5, 6, 7, 8, 9, 10
            Print(1..^1);    // 1..^1 => 2, 3, 4, 5, 6, 7, 8, 9
            Print(^2..^1);   // ^2..^1 => 9
        }
    }
}
همانطور که ملاحظه می‌کنید، Range را می‌توان به عنوان پارامتر متدها نیز استفاده و بر روی آرایه‌ها اعمال کرد؛ اما با <List<T سازگار نیست.

مثالی دیگر: استفاده از Range به عنوان جایگزینی برای متد String.Substring

از Range می‌توان برای کار بر روی رشته‌ها و انتخاب قسمتی از آن‌ها نیز استفاده کرد:
Console.WriteLine("123456789"[1..4]); // Would output 234
چند مثال دیگر:
var helloWorldStr = "Hello, World!";

var hello = helloWorldStr[..5];
Console.WriteLine(hello); // Output: Hello

var world = helloWorldStr[7..];
Console.WriteLine(world); // Output: World!

var world2 = helloWorldStr[^6..]; // Take the last 6 characters
Console.WriteLine(world); // Output: World!


سؤال: زمانیکه بازه‌ای از یک آرایه را انتخاب می‌کنیم، آیا یک آرایه‌ی جدید ایجاد می‌شود، یا هنوز به همان آرایه‌ی قبلی اشاره می‌کند؟

پاسخ: یک آرایه‌ی جدید ایجاد می‌شود؛ اما می‌توان با فراخوانی متد ()array.AsSpan پیش از انتخاب یک بازه، بازه‌ای را تولید کرد که دقیقا به همان آرایه‌ی اصلی اشاره می‌کند و یک کپی جدید نیست:
var arr = (new[] { 1, 4, 8, 11, 19, 31 }).AsSpan();
var range = arr[2..5];

ref int elt1 = ref range[1];
elt1 = -1;

int copiedElement = range[2];
copiedElement = -2;

Console.WriteLine($"range[1]: {range[1]}, range[2]: {range[2]}"); // output: range[1]: -1, range[2]: 19
Console.WriteLine($"arr[3]: {arr[3]}, arr[4]: {arr[4]}"); // output: arr[3]: -1, arr[4]: 19
در این مثال، آرایه‌ی اصلی را ابتدا تبدیل به یک Span کرده‌ایم و سپس بازه‌ای از روی آن انتخاب شده‌است. به همین جهت است که زمانیکه از ref locals برای تغییر عضوی از این بازه استفاده می‌شود، این تغییر بر روی آرایه‌ی اصلی نیز تاثیر می‌گذارد.
اشتراک‌ها
بررسی <IAsyncEnumerable<T در NET Core 3.

One of the most exciting features of .NET Core 3 and C# 8.0 has been the addition of IAsyncEnumerable<T> (aka async streams). But what's so special about it? What can we do now that wasn't possible before? 

بررسی <IAsyncEnumerable<T در NET Core 3.
اشتراک‌ها
AutoMapper 4.0 منتشر شد

There’s a ton of small bug fixes in this release, quite a few enhancements and a few larger new features. Configuration performance went up quite a bit, and I’ve laid the groundwork to make in-memory mapping a lot faster in the future. LINQ projection has gotten to the point where you can do anything that the major query providers support.

AutoMapper 4.0 منتشر شد