نظرات مطالب
آشنایی با TransactionScope
زمانی که از تراکنش‌های مربوط به DbConnection استفاده می‌کنید اتمام آن توسط متد Commit موجود در این کلاس مشخص خواهد شد نه متد SaveChanges . در نتیجه اگر تمامی SaveChanges‌ها فراخوانی شده  به درستی انجام شوند کل عملیات Commit در غیر این صورت عملیات RollBack می‌شود. در مورد TransactionScope‌ها نیز به همین صورت است. اگر قبل از رسیدن اجرای برنامه به دستور transcation.Complete هر گونه خطایی صادر شود عملیات RollBack خواهد شد.
بازخوردهای دوره
تزریق وابستگی‌ها در حالتی‌که از یک اینترفیس چندین کلاس مشتق شده‌اند
با سلام 
 کلاس UsersManagerService  که داره اینترفیس IUsersManagerService رو پیاده سازی می‌کنه ، اگر روزی فرضا یه سرویس دیگه مثل Sms و ایمیلی که الان توی کلاس هست به سیستم اضافه شد به نظر میاد که باید اون رو هم مثل اینها توی سازنده کلاس اضافه کرد و در هر حال کلاس ما دچار تغییر میشه .این آیا نقض OpenClose Responsibility و SRP نیست ؟
مطالب
UnitTest برای کلاس های Abstract با استفاده از Rhino Mocks
در این پست قصد دارم کلاس زیر رو براتون آزمایش کنم:
public abstract class myabstractclass
{
    public abstract string dosomething( string input );
 
    public double round( double number , int decimals )
    {
        return math.round( number , decimals );
    }
}
در کلاس بالا که abstract هستش، متدی دارم که abstract است و بدنه‌ای نداره و از متد بعدی به اسم round برای گرد کردن اعداد استفاده می‌شه. برای تست کلاس بالا و اطمینان از درست بودن متد‌ها باید به روش زیر عمل نمود:

روش اول:
در این روش ابتدا باید کلاسی نوشت تا کلاس abstract بالا رو پیاده سازی کنه:
   public class mynewclass : myabstractclass
   {
       public override string dosomething( string input )
       {
           return input;
       }
   }
بعد می‌شه خیلی راحت برای کلاس دوم متد‌های تست رو نوشت. به روش زیر:
    [testclass]
    public class mytest
    {
        [testmethod]
        public void testround()
        {
            mynewclass mynewclass = new mynewclass();
 
            var result = mynewclass.round( 5.55 , 1 );
 
            assert.areequal( 5.6 , result );
        }
    }
که بعد از اجرا نتیجه زیر رو خواهید دید


البته روش بالا خیلی مورد پسند من نیست. 

در روش دوم که من خیلی بیشتر بهش علاقه دارم دیگه نیازی به استفاده از یک کلاس دوم برای پیاده سازی کلاس abstract نیست. بلکه در این روش از ابزار rhinomocks  برای این کار استفاده می‌کنیم . استفاده از rhino mocks به چندین روش امکان پذیره که امروز 2 روش اونو براتون توضیح میدم:
در روش اول از mockrepository استفاده می‌کنیم  و در روش دوم از روش aaa یا arrange-act-assert

استفاده از mockrepository :
ابتدا کد‌های مربوطه رو می‌نویسم:
       [testmethod]
       public void testwithmockrepository()
       {
           var mockrepository = new rhino.mocks.mockrepository();
           var mock = mockrepository.partialmock<myabstractclass>();
 
           using ( mockrepository.record() )
           {
               expect.call( mock.dosomething( arg<string>.is.anything ) ).return( "hi..." ).repeat.once();
           }
           using ( mockrepository.playback() )
           {
               assert.areequal( "hi..." , mock.dosomething( "salam" ) );             
           }
       }
همانطور که در کد‌های نوشته شده بالا می‌بینید ابتدا یک mockrepository ساخته شده، سپس از نمونه اون کلاس برای ساخت partialmock کلاس myabstractclass استفاده کردم. در این روش متد‌های expect حتما باید بین بلاک record نوشته شوند تا بتونیم از اون‌ها در بلاک playback استفاده کنیم. نتیجه اجرای این متد تست هم مثل متد تست قبلی درست است.
در مورد expect.call باید بگم که از این کلاس برای شبیه سازی رفتار یک متد استفاده میشه (مثلا در مواقعی که یک متد برای انجام عملیات باید به دیتا بیس وصل شده و یک query را اجرا کنه) برای اینکه در تست، از این عملیات صرف نظر بشه از mock‌ها استفاده کرده و رفتار متد رو به روش بالا شبیه سازی می‌کنیم. البته کار کردن با rhino mocks به صورت بالا دیگه از مد افتاده و جدیدا از روش aaa استفاده میشه که اونو در پایین توضیح می‌دم:
      [testmethod]
      public void testwithaaa()
      {
          var mock = mockrepository.generatepartialmock<myabstractclass>();
 
          mock.expect( x => x.dosomething( arg<string>.is.anything ) ).return( "hi..." ).repeat.once();//arange
 
          var result = mock.dosomething( "salam" );//act
 
          assert.areequal( "hi..." , result );//assert
      }
توی این روش دیگه خبری از record و playback نیست و همانطور که مشخصه از سه مرحله arrange-act-assert تشکیل شده که هر مرحله رو براتون مشخص کردم. مزیت استفاده از این روش اینه که اولا تعداد خطوط کمتری برای کد نویسی نیاز داره و دوما سرعت اجراش از روش قبلی خیلی بیشتره. در مورد repeat.once هم بگم که این دستور نشون می‌ده فقط یک بار اجازه انجام عملیات act رو دارید. یعنی اگر کد هارو به صورت زیر تغییر بدیم با خطا روبرو می‌شیم:
       [testmethod]
       public void testwithaaa()
       {
           var mock = mockrepository.generatepartialmock<myabstractclass>();
 
           mock.expect( x => x.dosomething( arg<string>.is.anything ) ).return( "hi..." ).repeat.once();//arange
 
           var result = mock.dosomething( "salam" );//act
           var result2 = mock.dosomething( "bye" );//act
           assert.areequal( "hi..." , result );//assert
       }



خطای آن هم واضح داره میگه که expected#1 هستش در حالی که actual#2 (تعداد دفعات حقیقی از دفعات مورد انتظار بیشتره)
توی پست‌های بعدی (اگه وقت بشه) حتما در مورد rhino mocks بیشتر توضیح میدم 
مطالب
ایجاد توالی‌ها در Reactive extensions
در مطلب «معرفی Reactive extensions» با نحوه‌ی تبدیل IEnumerable‌ها به نمونه‌های Observable آشنا شدیم. اما سایر حالات چطور؟ آیا Rx صرفا محدود است به کار با IEnumerableها؟ در ادامه نگاهی خواهیم داشت به نحوه‌ی تبدیل بسیاری از منابع داده دیگر به توالی‌های Observable قابل استفاده در Rx.


روش‌های متفاوت ایجاد توالی (sequence) در Rx

الف) استفاده از متدهای Factory

1) Observable.Create
نمونه‌ای از استفاده از آن‌را در مطلب «معرفی Reactive extensions» مشاهده کردید.
 var query = Enumerable.Range(1, 5).Select(number => number);
var observableQuery = query.ToObservable();
var observer = Observer.Create<int>(onNext: number => Console.WriteLine(number));
observableQuery.Subscribe(observer);
کار آن، تدارک delegate ایی است که توسط متد Subscribe، به ازای هربار پردازش مقدار موجود در توالی معرفی شده به آن، فراخوانی می‌گردد و هدف اصلی از آن این است که به صورت دستی اینترفیس IObservable را پیاده سازی نکنید (امکان پیاده سازی inline یک اینترفیس توسط Actionها).
البته در این مثال فقط delegate مربوط به onNext را ملاحظه می‌کند. توسط سایر overloadهای آن امکان ذکر delegate‌های OnError/OnCompleted نیز وجود دارد.

2) Observable.Return
برای ایجاد یک خروجی Observable از یک مقدار مشخص، می‌توان از متد جنریک Observable.Return استفاده کرد. برای مثال:
 var observableValue1 = Observable.Return("Value");
var observableValue2 = Observable.Return(2);
در ادامه نحوه‌ی پیاده سازی این متد را توسط Observable.Create مشاهده می‌کنید:
        public static IObservable<T> Return<T>(T value)
        {
            return Observable.Create<T>(o =>
            {
                o.OnNext(value);
                o.OnCompleted();
                return Disposable.Empty;
            });
        }
البته دو سطر نوشته شده در اصل معادل هستند با سطرهای ذیل؛ که ذکر نوع جنریک آن‌ها ضروری نیست. زیرا به صورت خودکار از نوع آرگومان معرفی شده، تشخیص داده می‌شود:
 var observableValue1 = Observable.Return<string>("Value");
var observableValue2 = Observable.Return<int>(2);

3) Observable.Empty
برای بازگشت یک توالی خالی که تنها کار اطلاع رسانی onCompleted  را انجام می‌دهد.
 var emptyObservable = Observable.Empty<string>();
در کدهای ذیل، پیاده سازی این متد را توسط Observable.Create مشاهده می‌کنید:
        public static IObservable<T> Empty<T>()
        {
            return Observable.Create<T>(o =>
            {
                o.OnCompleted();
                return Disposable.Empty;
            });
        }

4) Observable.Never
برای بازگشت یک توالی بدون قابلیت اطلاع رسانی و notification
 var neverObservable = Observable.Never<string>();
این متد به نحو زیر توسط Observable.Create پیاده سازی شده‌است:
        public static IObservable<T> Never<T>()
        {
            return Observable.Create<T>(o =>
            {
                return Disposable.Empty;
            });
        }

5) Observable.Throw
برای ایجاد یک توالی که صرفا کار اطلاع رسانی OnError را توسط استثنای معرفی شده به آن انجام می‌دهد.
 var throwObservable = Observable.Throw<string>(new Exception());
در ادامه نحوه‌ی پیاده سازی این متد را توسط Observable.Create مشاهده می‌کنید:
        public static IObservable<T> Throws<T>(Exception exception)
        {
            return Observable.Create<T>(o =>
            {
                o.OnError(exception);
                return Disposable.Empty;
            });
        }

6) توسط Observable.Range
به سادگی می‌توان بازه‌ی Observable ایی را ایجاد کرد:
 var range = Observable.Range(10, 15);
range.Subscribe(Console.WriteLine, () => Console.WriteLine("Completed"));

7) Observable.Generate
اگر بخواهیم عملیات Observable.Range را پیاده سازی کنیم، می‌توان از متد Observable.Generate استفاده کرد:
        public static IObservable<int> Range(int start, int count)
        {
            var max = start + count;
            return Observable.Generate(
                initialState: start,
                condition: value => value < max,
                iterate: value => value + 1,
                resultSelector: value => value);
        }
توسط پارامتر initialState، مقدار آغازین را دریافت می‌کند. پارامتر condition، مشخص می‌کند که توالی چه زمانی باید خاتمه یابد. در پارامتر iterate، مقدار جاری دریافت شده و مقدار بعدی تولید می‌شود. resultSelector کار تبدیل و بازگشت مقدار خروجی را به عهده دارد.

8) Observable.Interval
عموما از انواع و اقسام تایمرهای موجود در دات نت مانند System.Timers.Timer ، System.Threading.Timer و System.Windows.Threading.DispatcherTimer برای ایجاد یک توالی از رخ‌دادها استفاده می‌شود. تمام این‌ها را به سادگی می‌توان توسط متد Observable.Interval‌، که قابل انتقال به تمام پلتفرم‌هایی است که Rx برای آن‌ها تهیه شده‌است، جایگزین کرد:
 var interval = Observable.Interval(period: TimeSpan.FromMilliseconds(250));
interval.Subscribe(Console.WriteLine, () => Console.WriteLine("completed"));
در اینجا تایمر تهیه شده، هر 450 میلی‌ثانیه یکبار اجرا می‌شود. برای خاتمه‌ی آن باید شیء interval را Dispose کنید.
Overload دوم این متد، امکان معرفی scheduler و اجرای بر روی تردی دیگر را نیز میسر می‌کند.

9) Observable.Timer
تفاوت Observable.Timer با Observable.Interval در مفهوم پارامتر ارسالی به آن‌ها است:
 var timer = Observable.Timer(dueTime: TimeSpan.FromSeconds(1));
 timer.Subscribe(Console.WriteLine, () => Console.WriteLine("completed"));
یکی due time دارد (مدت زمان صبر کردن تا تولید اولین خروجی) و دیگری period (به صورت متوالی تکرار می‌شود).  
خروجی Observable.Interval مثال زده شده به نحو زیر است و خاتمه‌‌ای ندارد:
0
1
2
3
4
5

اما خروجی Observable.Timer به نحو ذیل  بوده و پس از یک ثانیه، خاتمه می‌یابد:
0
completed

متد Observable.Timer دارای هفت overload متفاوت است که توسط آن‌ها dueTime (مدت زمان صبر کردن تا تولید اولین خروجی)، period (کار Observable.Timer را به صورت متوالی در بازه‌ی زمانی مشخص شده تکرار می‌کند) و scheduler (تعیین ترد اجرایی عملیات) قابل مقدار دهی هستند.
اگر می‌خواهید Observable.Timer بلافاصله شروع به کار کند، مقدار dueTime آن‌را مساوی TimeSpan.Zero قرار دهید. به این ترتیب یک Observable.Interval را به وجود آورده‌اید که بلافاصله شروع به کار کرده است و تا مدت زمان مشخص شده‌ای جهت اجرای اولین callback خود صبر نمی‌کند.



ب) تبدیلگرهایی که خروجی IObservable ایجاد می‌کنند

برای تبدیل مدل‌های برنامه نویسی Async قدیمی دات نت مانند APM، رخدادها و امثال آن به معادل‌های Rx، متدهای الحاقی خاصی تهیه شده‌اند.

1) تبدیل delegates به معادل Observable
متد Observable.Start، امکان تبدیل یک Func یا Action زمانبر را به یک توالی observable میسر می‌کند. در این حالت به صورت پیش فرض، پردازش عملیات بر روی یکی از تردهای ThreadPool انجام می‌شود.
        static void StartAction()
        {
            var start = Observable.Start(() =>
            {
                Console.Write("Observable.Start");
                for (int i = 0; i < 10; i++)
                {
                    Thread.Sleep(100);
                    Console.Write(".");
                }
            });
            start.Subscribe(
               onNext: unit => Console.WriteLine("published"),
               onCompleted: () => Console.WriteLine("completed"));
        }

        static void StartFunc()
        {
            var start = Observable.Start(() =>
            {
                Console.Write("Observable.Start");
                for (int i = 0; i < 10; i++)
                {
                    Thread.Sleep(100);
                    Console.Write(".");
                }
                return "value";
            });
            start.Subscribe(
               onNext: Console.WriteLine,
               onCompleted: () => Console.WriteLine("completed"));
        }
در اینجا دو مثال از بکارگیری Action و Func‌ها را توسط Observable.Start مشاهده می‌کنید.
زمانیکه از Func استفاده می‌شود، تابع یک خروجی را ارائه داده و سپس توالی خاتمه می‌یابد. اگر از Action استفاده شود، نوع Observable بازگشت داده شده از نوع Unit است که در برنامه نویسی functional معادل void است و هدف از آن مشخص سازی پایان عملیات Action می‌باشد. Unit دارای مقداری نبوده و صرفا سبب اجرای اطلاع رسانی OnNext می‌شود.
تفاوت مهم Observable.Start و Observable.Return در این است که Observable.Start مقدار تابع را به صورت تنبل (lazily) پردازش می‌کند، اما Observable.Return پردازش حریصانه‌ای (eagrly) را به همراه خواهد داشت. به این ترتیب Observable.Start بسیار شبیه به یک Task (پردازش‌های غیرهمزمان) عمل می‌کند.
در اینجا شاید این سؤال مطرح شود که استفاده از قابلیت‌های Async سی‌شارپ 5 برای اینگونه کارها مناسب است یا Rx؟ قابلیت‌های Async بیشتر به اعمال مخصوص IO bound مانند کار با شبکه، دریافت فایل از اینترنت، کار با یک بانک اطلاعاتی خارج از مرزهای سیستم، مرتبط می‌شوند؛ اما اعمال CPU bound مانند محاسبات سنگین حاصل از توالی‌های observable را به خوبی می‌توان توسط Rx مدیریت کرد.


2) تبدیل Events به معادل Observable

دات نت از روزهای اول خود به همراه یک event driven programming model بوده‌است. Rx متدهایی را برای دریافت یک رخداد و تبدیل آن به یک توالی Observable ارائه داده‌است. برای نمونه ObservableCollection زیر را درنظر بگیرید
 var items = new System.Collections.ObjectModel.ObservableCollection<string>
  {
          "Item1", "Item2", "Item3"
  };
اگر بخواهیم مانند روش‌های متداول، حذف شدن آیتم‌های آن‌را تحت نظر قرار دهیم، می‌توان نوشت:
            items.CollectionChanged += (sender, ea) =>
            {
                if (ea.Action == NotifyCollectionChangedAction.Remove)
                {
                    foreach (var oldItem in ea.OldItems.Cast<string>())
                    {
                        Console.WriteLine("Removed {0}", oldItem);
                    }
                }
            };
این نوع کدها در WPF زیاد کاربرد دارند. اکنون معادل کدهای فوق با Rx به صورت زیر هستند:
            var removals =
                Observable.FromEventPattern<NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs>
                (
                    addHandler: handler => items.CollectionChanged += handler,
                    removeHandler: handler => items.CollectionChanged -= handler
                )
                .Where(e => e.EventArgs.Action == NotifyCollectionChangedAction.Remove)
                .SelectMany(c => c.EventArgs.OldItems.Cast<string>());

            var disposable = removals.Subscribe(onNext: item => Console.WriteLine("Removed {0}", item));
با استفاده از متد Observable.FromEventPattern می‌توان معادل Observable رخ‌داد CollectionChanged را تهیه کرد. پارامتر اول جنریک آن، نوع رخداد است و پارامتر اختیاری دوم آن، EventArgs این رخداد. همچنین با توجه به قسمت Where نوشته شده، در این بین مواردی را که Action مساوی حذف شدن را دارا هستند، فیلتر کرده و نهایتا لیست Observable آن‌ها بازگشت داده می‌شوند. اکنون می‌توان با استفاده از متد Subscribe، این تغییرات را دریافت کرد. برای مثال با فراخوانی
 items.Remove("Item1");
بلافاصله خروجی Removed item1 ظاهر می‌شود.


3) تبدیل Task به معادل Observable

متد ToObservable واقع در فضای نام System.Reactive.Threading.Tasks را بر روی یک Task نیز می‌توان فراخوانی کرد:
 var task = Task.Factory.StartNew(() => "Test");
var source = task.ToObservable();
source.Subscribe(Console.WriteLine, () => Console.WriteLine("completed"));
البته باید دقت داشت استفاده از Task دات نت 4.5 که بیشتر جهت پردازش‌های async اعمال I/O-bound طراحی شده‌است، بر IObservable مقدم است. صرفا اگر نیاز است این Task را با سایر observables ادغام کنید از متد ToObservable برای کار با آن استفاده نمائید.


4) تبدیل IEnumerable به معادل Observable
با این مورد تاکنون آشنا شده‌اید. فقط کافی است متد ToObservable را بر روی یک IEnumerable، جهت تهیه خروجی Observable فراخوانی کرد.


5) تبدیل APM به معادل Observable

APM یا Asynchronous programming model، همان روش کار با متدهای Async با نام‌های BeginXXX و EndXXX است که از نگارش‌های آغازین دات نت به همراه آن بوده‌اند. کار کردن با آن مشکل است و مدیریت آن به همراه پراکندگی‌های بسیاری جهت کار با callbacks آن است. برای تبدیل این نوع روش برنامه نویسی به روش Rx نیز متدهایی پیش بینی شده‌است؛ مانند Observable.FromAsyncPattern.

یک نکته
کتابخانه‌ای به نام Rxx بسیاری از این محصور کننده‌ها را تهیه کرده‌است:
http://Rxx.codeplex.com

ابتدا بسته‌ی نیوگت آن‌را نصب کنید:
 PM> Install-Package Rxx
سپس برای نمونه، برای کار با یک فایل استریم خواهیم داشت:
 using (new FileStream("file.txt", FileMode.Open)
                 .ReadToEndObservable()
                 .Subscribe(x => Console.WriteLine(x.Length)))
{
         Console.ReadKey();
}
متد ReadToEndObservable یکی از متدهای الحاقی کتابخانه‌ی Rxx است.
مطالب
پَرباد - راهنمای اتصال و پیاده‌سازی درگاه‌های پرداخت اینترنتی (شبکه شتاب)

پَرباد چیست؟

همانطور که همه ما میدانیم، اتصال و راه اندازی درگاه‌های پرداخت اینترنتی (شبکه شتاب)، از همان ابتدا کاری مشکل و  پر دردسر برای برنامه نویسان بود. هر بانک، سیستم متفاوت و مخصوص به خود را دارد و این بدان معنا است که برنامه نویسان باید کدهای کاملا متفاوت و همچنین پیاده سازی‌های متفاوتی را از روی فایل‌های PDF راهنمای بانکی، که در نهایت منجر به بی نظمی در پروژه‌ها می‌شود، بنویسند و البته مشکل بزرگتر آن است که پس از پیاده سازی هم اطمینان کاملی از صحت کدهای نوشته شده وجود ندارد؛ چه بسا که واحد‌های پشتیبانی درگاه‌های پرداخت هم افراد حرفه‌ای و آشنا با توسعه نرم افزار نیستند و اکثر اوقات نمی‌توان به آنها تکیه کرد.
برای راحتی کار برنامه نویسان حوضه فریم ورک دات نت، سیستمی جامع، اوپن سورس و کاملا رایگان، بدون نیاز به اضافه کردن هیچ گونه وب سرویسی تهیه شده است که به برنامه نویسان اجازه می‌دهد تنها با نوشتن چند خط کد، وب سایت خود را به پرداخت اینترنتی مجهز کنند. لطفا پیشنهادات، بحث‌ها و نظرات خود را در صفحه مخصوص این پروژه ارسال کنید.  
این سیستم در حال حاضر متشکل از درگاه‌های پرداخت اینترنتی بانک‌های ملت، سامان، پارسیان، تجارت و پاسارگاد است.
همچنین این سیستم در قالب یک Nuget Package برای نصب راحت در اپلیکیشن آماده شده است.


آنچه که شما در این مطلب یاد خواهید گرفت:

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


طریقه نصب

PM> Install-Package Parbad

برای وب سایت‌های بر پایه فریم ورک MVC

PM> Install-Package Parbad.MVC5


ایجاد صورتحساب و ارسال کاربر به درگاه پرداخت

ابتدا یک شیٔ صورتحساب را به صورت زیر ایجاد کنید
var invoice = new Invoice( [Order Number], [Amount], [Verify URL]);

- Order Number شماره صورتحساب است و باید همیشه یک عدد یکتا باشد (تکراری نباشد).
- Amount مبلغ قابل پرداخت به ریال است.
- Verify URL یک آدرس در وب سایت شما، برای بازگشت مشتری پس از پرداخت و تأیید صورتحساب است.
برای مثال:
var invoice = new Invoice(1, 30000, "http://www.mywebsite.com/payment/verify" );
سپس صورتحساب را به درگاه مورد نظر ارسال میکنیم.
var result = Payment.Request(Gateways.Mellat, invoice);

شیٔ result حاوی شماره یکتا رجوع و وضعیت درخواست (موفقیت یا عدم موفقیت درخواست) است.
if (result.Status == RequestResultStatus.Success)
{
    // این متد، کاربر را به سمت وب سایت درگاه پرداخت هدایت میکند
    result.Process(Context);
}
else
{
    // در صورت تمایل می‌توانید پیغام مورد نظر از درگاه پرداخت را نمایش دهید
    var msg = result.Message;
}

در وب سایت‌های MVC می‌توانید به روش زیر عمل کنید

if (result.Status == RequestResultStatus.Success)
{
   // کاربر را به سمت وب سایت درگاه پرداخت هدایت میکند 
   return new RequestActionResult(result);
}
else
{
   return View("Error");
}


تأیید صورتحساب

پس از بازگشت کاربر از وب سایت بانک، باید از پرداخت صورتحساب توسط کاربر اطمینان حاصل کنید. کد زیر را باید در آدرسی که هنگام ساخت صورتحساب ذکر کرده بودید، قرار دهید.
var result = Payment.Verify(System.Web.HttpContext.Current);

شیٔ result در اینجا حاوی اطلاعاتی مانند: درگاه بانکی (که کاربر در آن صورتحساب را پرداخت کرده)، شماره رجوع، شماره تراکنش یکتای بانکی، وضعیت پرداخت و پیام درگاه است.
شما می‌توانید با بررسی این شیٔ، تصمیمات لازم را بگیرید.
if(result.Status == VerifyResultStatus.Success)
{
    // کاربر، صورتحساب را پرداخت کرده است و شما میتوانید ادامه عملیات خرید را انجام دهید
}
else
{
    // کاربر بنا به دلایلی صورتحساب را پرداخت نکرده است
    // شما همچنین میتوانید علت را در قالب یک پیام از پراپرتی پیام مشاهده کنید

    // بنابراین شما میتوانید این صورتحساب را در پایگاه داده خود مردود اعلام کنید
}


مردود کردن صورتحساب قبل از انتقال وجه از مشتری به فروشنده

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



همانطور که در تصویر می‌بینید، در هنگام بازگشت مشتری به وب سایت شما و تأیید کردن صورتحساب، شما می‌توانید اطلاعات تراکنش مورد نظر را که شامل، درگاه پرداخت بانکی، شماره سفارش و شماره رجوع است را دریافت کنید و سپس با استفاده از این اطلاعات، پایگاه داده خود را بررسی کرده و در صورت لزوم، متد Cancel را فراخوانی کنید. به این ترتیب به درگاه بانکی، هیچگونه تأییدیه ای اعلام نمی‌شود و این بدان معناست که اگر وجهی به حساب فروشگاه واریز شده باشد، پس از چند دقیقه (معمولا ۱۵ دقیقه) به حساب مشتری برگشت داده خواهد شد.


برگشت وجه به حساب مشتری پس از تأیید صورتحساب

var refundResult = Payment.Refund(new RefundInvoice([Order Number], [Amount]));
در اینجا، Order Number همان شماره سفارش صورتحساب و Amount مقداری از وجه و یا کل وجه برای برگشت به حساب مشتری است.
پس از آن شما می‌توانید نتیجه این عملیات را در شیٔ refundResult بررسی کنید.


درگاه مجازی پرداخت

درصورتیکه شما نیاز به تست عملکرد اپلیکیشن خود داشته باشید، نیازی به داشتن یک حساب واقعی در بانک‌های اینترنتی ندارید و می‌توانید اپلیکیشن خود را با یک درگاه مجازی بسیار ساده تست کنید. برای انجام این کار در هنگام ارسال صورتحساب، از میان درگاه‌های بانکی، درگاه مجازی پَرباد را انتخاب کنید.
var result = Payment.Request(Gateways.ParbadVirtualGateway, invoice);


در نتیجه در هنگام هدایت کاربر به درگاه پرداخت، کاربر به درگاه مجازی هدایت خواهد شد.

اما قبل از کار با درگاه مجازی باید در فایل web.config وب اپلیکیشن خود، تنظیمات زیر را قرار دهید:
<system.webServer>
  <handlers>
   <add name="ParbadGatewayPage" verb="*" path="Parbad.axd" type="Parbad.Web.Gateway.ParbadVirtualGatewayHandler" />
  </handlers>
</system.webServer>
در اینجا، درگاه مجازی به عنوان یک HttpHandler معرفی شده است. مقداری که در مشخصه path ذکر شده، در واقع آدرس درگاه مجازی است که شما می‌توانید به دلخواه خود آن را وارد کنید. ما در این مثال از آدرس parbad.axd استفاده کرده ایم.
و در نهایت در وب اپلیکیشن خود، مسیر ذکر شده را به صورت زیر معرفی کنید:
ParbadConfiguration.Gateways.ConfigureParbadVirtualGateway(new ParbadVirtualGatewayConfiguration("Parbad.axd"));
در نتیجه در هنگام هدایت کاربر به درگاه مجازی، شما باید در نوار آدرس مرورگر خود، مقداری را که تنظیم کرده اید مشاهده کنید.


نکته مهم: فراموش نکنید، قبل از انتشار نهایی وب سایت بر روی سرور (نمایش عمومی)، تنظیمات HttpHandler مربوط به این درگاه مجازی را از درون فایل web.config حذف کنید. بدین صورت، این درگاه از دسترس عموم خارج خواهد بود.

تنظیمات پَرباد

بهترین مکان برای درج این تنظیمات در اپلیکیشن‌های ASP.NET WebForms فایل Global.asax.cs و در اپلیکیشن‌های ASP.NET MVC فایل Startup.cs است.
ASP.NET Web Forms
public class Global : HttpApplication
{
    void Application_Start(object sender, EventArgs e)
    {
        // configurations
    }
}

ASP.NET MVC
public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        // configurations
    }
}

تنظیمات درگاه‌های پرداخت

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


تنظیمات ذخیره سازی اطلاعات پرداخت

پَرباد برای ذخیره و بازیابی اطلاعات پرداخت، نیاز به یک منبع ذخیره سازی دارد.
منبع پیش فرض پَرباد، کلاس TemporaryMemoryStorage است که همانطور که از نام آن پیداست، اطلاعات را به صورت موقت در حافظه رَم سرور ذخیره میکند. اگر شما خودتان اطلاعات پرداخت را در پایگاه داده ذخیره میکنید، این منبع، گزینه مناسبی است به دلیل سرعت بسیار بالای حافظه رَم.
توجه: در نظر داشته باشید که اگر به هر دلیلی سرور و یا وب سایت شما، ری‌استارت شود، کلیه اطلاعات موجود در این منبع هم از بین خواهد رفت.
ذخیره و بازیابی توسط SQL Server
برای این منظور در قسمت تنظیمات، کد زیر را قرار داده و رشته اتصال و نام جدول پرداخت را معرفی کنید.
ParbadConfiguration.Storage = new SqlServerStorage("Connection String", "MyPaymentTableName");

فیلد‌های مورد نیاز در این جدول:

ذخیره و بازیابی اطلاعات توسط روش مورد نظر شما:
در صورتیکه مایلید ذخیره و بازیابی را به روش خود انجام دهید، کلاس Storage را پیاده سازی کنید
public class MyStorage : Storage
{
    // Implement methods here...
}

و کلاس مورد نظر را در تنظیمات به عنوان منبع، معرفی کنید.
ParbadConfiguration.Storage = new MyStorage();

لازم به ذکر است که این کلاس شامل متد‌های synchronous و همچنین asynchronous است. بنابراین در صورتیکه برای مثال در هنگام ارسال درخواست به بانک، از متد‌های async استفاده می‌کنید، نیازی به پیاده سازی کردن متد‌های synchronous نیست.
در صورتیکه هر گونه پیشنهاد یا انتقاد نسبت به کارکرد این سیستم دارید، صمیمانه منتظر شنیدن آن در راستای توسعه این سیستم هستم.
همچنین در صورت تمایل به توسعه آن، می‌توانید آن را در گیت هاب دنبال کنید و یا لطفا پیشنهادات، بحث‌ها و نظرات خود را در صفحه مخصوص این پروژه ارسال کنید. 
با تشکر.
مطالب
Serialization #2

مطابق آنچه در قسمت قبل  گفته شد برای آن‌که بتوان از مدل News برای سریالی‌کردن استفاده کرد، باید آن را به شکل ذیل پیاده‌سازی کرد:

[DataContract]
public class News
{
     [DataMember] public int Id;
     [DataMember] public string Body;
     [DataMember] public DateTime NewsDate;
}

با Override  کردن  [DataContract]به صورت [("DataContract(Name=”MyCustomNews] می‌توان نام ریشه XML فایل را به MyCustomNews  تغییر داد. همچنین با Override  کردن [DataMember]  بصورت [("DataMember(Name=”MyCustomFieldName]  می‌شود به هر فیلدی عنوان دلخواهی داد و همچنین  با تعیین عبارت  NameSpace به صورت [("DataContract(Name = "MyCustomNews", Namespace = "http://www.my.com] می‌شود فضای نام را تغییر داد که با این تغییرات، خروجی زیر حاصل می‌شود: 

<?xml version="1.0" encoding="utf-8"?>
<MyCustomNews xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.my.com">
  <Body>NewsBody</Body>
  <MyCustomFieldName>111</MyCustomFieldName>
  <NewsDate>2012-10-04T00:00:00</NewsDate>
</MyCustomNews>


ویژگی [DataMember] هم ازفیلدها و هم از propertyها، پشتیبانی می‌کند، خواه عمومی باشند یا خصوصی و  نوع فیلد یا Property  می‌تواند به یکی از اشکال زیر باشد:

  1. انواع اولیه .
  2. انواع DateTime ،TimeSpan، Guid ،Uri و انواع Enum
  3. انواع پوچ پذیر هر کدام از موارد بالا
  4. نوع byte[]
  5. انواع تعریف شده توسط کاربر که توسط صفت [DataContract] محصور شده‌اند.
  6. هر نوع IEnumerable
  7. هر نوعی که با صفت [Serializable] محصور شود و یا اینترفیس ISerializable را پیاده سازی کند.
  8. هر نوعی که اینترفیس IXmlSerializble را پیاده سازی نماید.

تعیین فرمت باینری برای سریالی‌کردن:

برای سریالی کننده‌های  DataContractSerializer و NetDataContractSerializer می‌توان به روش زیر  فرمت خروجی را به شکل فرمت باینری درآورد که خروجی آن تاحد زیادی کوچک‌تر و کم حجم‌تر می‌شود:

var s = new MemoryStream();
using (XmlDictionaryWriter w=XmlDictionaryWriter.CreateBinaryWriter(s))
{
         ds.WriteObject(w,news);
}

و برای Deserialize کردن آن به شیوه زیر عمل می‌کنیم:

var s2 = new MemoryStream(s.ToArray());
News deserializednews;
using (XmlDictionaryReader r=XmlDictionaryReader.CreateBinaryReader(s2,XmlDictionaryReaderQuotas.Max))
{
     deserializednews = (News)ds.ReadObject(r);
}

که در آن از ویژگی Max  کلاس XmlDictionaryReaderQuotas  برای به دست آوردن حداکثر سهمیه فضای دیسک مربوط به  XmlDictionaryReaders  استفاده می‌شود. 

مطالب
بررسی وجود نام کاربر با استفاده از jQuery Ajax در ASP.Net

شاید بعضی از سایت‌ها را دیده باشید که در حین ثبت نام، پس از وارد کردن یک نام کاربری و سپس مشغول شدن به پر کردن فیلد کلمه‌ی عبور، در قسمت نام کاربری شروع به جستجو در مورد آزاد بودن نام کاربری درخواستی می‌کنند یا نمونه‌ای دیگر، فرم پرداخت الکترونیکی بانک سامان. پس از اینکه شماره قبض را وارد کردید، بلافاصله بدون ریفرش صفحه به شما پیغام می‌دهد که این شماره معتبر است یا خیر. امروز قصد داریم این قابلیت را با استفاده از کتابخانه‌ی Ajax مجموعه jQuery در ASP.Net پیاده سازی کنیم (بدون استفاده از ASP.Net Ajax مایکروسافت).
ابتدا سورس کامل را ملاحظه نمائید:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="AjaxTest.aspx.cs" Inherits="testWebForms87.AjaxTest" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>jQuery Ajax Text</title>

<script src="jquery.min.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function() {
$("#<%= TextBox1.ClientID %>").blur(function(event) {
$.ajax({
type: "POST",
url: "AjaxTest.aspx/IsUserAvailable",
data: "{'username': '" + $('#<%= TextBox1.ClientID %>').val() + "'}",
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function(msg) {
$('#valid').html("<img src='ajaxImages/waiting.gif' alt='لطفا کمی تامل کنید'>");
var delay = function() {
AjaxSucceeded(msg);
};

setTimeout(delay, 2000); //remove this
},
error: AjaxFailed
});
});
});
function AjaxSucceeded(result) {
if (result.d == true)
$('#msg').html("<img src='ajaxImages/available.gif' alt='نام کاربری درخواستی موجود است'>");
else
$('#msg').html("<img src='ajaxImages/taken.gif' alt='متاسفانه نام کاربری مورد نظر پیشتر دریافت شده‌است'>");
}
function AjaxFailed(result) {
alert(result.status + ' ' + result.statusText);
}
</script>

</head>
<body>
<form id="form1" runat="server">
<div>
user name:
<asp:TextBox ID="TextBox1" runat="server"></asp:TextBox>
<span id="msg"></span>
<br />
pass:
<asp:TextBox ID="TextBox2" TextMode="Password" runat="server"></asp:TextBox>
</div>
<!-- preload -->
<div style="display: none">
<img src="ajaxImages/available.gif" alt="available" />
<img src="ajaxImages/taken.gif" alt="taken" />
<img src="ajaxImages/waiting.gif" alt="waiting" />
</div>
</form>
</body>
</html>


using System;
using System.Web.Services;

namespace testWebForms87
{
public partial class AjaxTest : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
}

[WebMethod]
public static bool IsUserAvailable(string username)
{
// این مورد را با خواندن اطلاعات از دیتابیس می‌شود تعویض کرد
return username != "test";
}
}
}

توضیحات:
همانطور که ملاحظه می‌کنید صفحه‌ی ASP.Net ما بسیار ساده است و از دو تکست باکس استاندارد تشکیل می‌شود، به همراه تصاویر مربوط به Ajax که یک سری تصاویر ساده چرخان معروف منتظر بمانید ، یافت شد یا موجود نیست می‌باشند. این تصاویر در یک div مخفی (display: none) در صفحه قرار گرفته‌اند و در هنگام بارگذاری صفحه، این‌ها نیز بارگذاری شده و حاضر و آماده خواهند بود. بنابراین هنگام استفاده از آن‌ها، کاربر تاخیری را مشاهده نخواهد کرد. همچنین یک span با id مساوی msg‌ را هم پس از تکست باکس اضافه کرده‌ایم تا تصاویر مربوط به رخدادهای Ajax را با استفاده از توانایی‌های jQuery به آن اضافه کنیم.

اسکریپت Ajax ما با دراختیار گرفتن روال رخداد گردان blur شیء textBox1 شروع می‌شود. همانطور که در مقالات پیشین سایت نیز ذکر شد، روش صحیح دریافت ID یک کنترل ASP.Net در کدهای سمت کلاینت جاوا اسکریپتی، بر اساس خاصیت ClientID آن است که در اولین سطر کدهای ما مشخص است (زیرا در ASP.Net نام و ID یک کنترل در هنگام رندر شدن به همراه ID کنترل‌های دربرگیرنده آن نیز خواهد بود، بنابراین بهتر است این مورد را داینامیک کرد).

کار بررسی موجود بودن نام کاربری (یا مثلا یک شماره قبض و امثال آن) توسط WebMethod ایی به نام IsUserAvailable در code behind صفحه انجام می‌شود که پیاده سازی آن‌را ملاحظه می‌کنید. بدیهی است در این مثال ساده، تنها نام کاربری از پیش رزرو شده، کلمه‌ی test است و در یک کد واقعی این مورد با مقایسه‌ی نام کاربری با اطلاعات موجود در دیتابیس باید صورت گیرد (و حملات تزریق اس کیوال را هم فراموش نکنید. برای رهایی از آن‌ها "حتما" باید از پارامترهای ADO.Net استفاده کرد و گرنه کد شما مستعد به این نوع حملات خواهد بود).

سؤال: چرا از web method استفاده شد و همچنین چرا این متد static است؟
زمانیکه یک متد با کلمه کلیدی static‌ مشخص می‌شود حالت state less پیدا می‌کند یعنی مستقل از وهله‌ی کلاس عمل می‌کند. در این حالت نیازی به ارسال ViewState نبوده (بنابراین در کوئری مورد نظر ما بسیار بهینه و سبک عمل می‌کنند) و همچنین نیازی به ایجاد یک وهله‌ای از کلاس صفحه‌ی ما نیز نخواهد بود. برای توضیحات بیشتر به این مقاله مراجعه نمائید. (به صورت خلاصه، دلیل اصلی، کارآیی بالا و بهینه بودن این روش در این مساله ویژه است و در ASP.Net Ajax مایکروسافت به صورت گسترده‌ای در پشت صحنه مورد استفاده قرار می‌گیرد)
استفاده از ویژگی WebMethod عملکرد صفحه‌ی ما را شبیه به یک وب سرویس خواهد کرد و امکان دسترسی به آن در متدهای استاندارد POST به صورت ارسال دیتا به آدرس WebService.asmx/WebMethodName‌ خواهد بود. یک مثال ساده و عملی

بررسی تابع Ajax بکار رفته:
این تابع هنگام فراخوانی رخداد blur تکست‌باکس ما (مطابق کد فوق) فراخوانی می‌شود. ساختار ساده‌ای دارد که به شرح زیر است:
type: "POST"
نحوه‌ی ارسال داده را به متد وب سرویس ما مشخص می‌کند.

url: "AjaxTest.aspx/IsUserAvailable"
اطلاعات پست شده، به صفحه‌ی AjaxTest.aspx و وب متد IsUserAvailable ارسال خواهد شد

data: "{'username': '" + $('#<%= TextBox1.ClientID %>').val() + "'}",
داده‌ای که به آرگومان username ما ارسال می‌شود، همان مقدار تایپ شده در TextBox1 است (که باز هم دریافت ID آن به صورت داینامیک صورت گرفته تا مشکل زا نشود)

contentType: "application/json; charset=utf-8",
dataType: "json",
در این دو سطر از نوع داده‌ی json استفاده شده است که فرمت بسیار سبک و بهینه‌ای برای تبادل اطلاعات در وب به‌شمار می‌آید و توسط کتابخانه‌های جاوا اسکریپتی به سادگی پردازش شده و تبدیل به اشیاء مورد نظر خواهند شد. برای مثال اگر خروجی یک وب سرویس در حالت xml به صورت زیر باشد:

<xx yy="nn"></xx>
معادل json آن به شرح زیر است:
{ "xx": {"yy":"nn"} }
success: function(msg)
Success ، پس از موفقیت آمیز بودن عملیات ajax در jQuery فراخوانی می‌شود
error: AjaxFailed
و اگر در این بین خطایی رخ داده باشد، قسمت error فراخوانی می‌شود.

در این مثال برای نمایش بهتر عملیات، یک وقفه‌ی 2 ثانیه‌ای توسط setTimeout ایجاد شده و بدیهی است در یک مثال واقعی باید آن‌را حذف نمود.

نکته: با استفاده از افزونه‌ی فایرباگ فایرفاکس، می‌توان جزئیات این عملیات را بهتر مشاهده نمود:





مطالب
اصول طراحی شی‌ء گرا: OO Design Principles - قسمت دوم

اصل چهارم: Starve for loosely coupled designs

"به دنبال طراحی با اتصال سست بین اجزا باش"

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

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

public class StrongCoupledConcreteA
    {
        public string GenerateString(string s) { return s + " from" + this.GetType().ToString(); }
    }

    public class StrongCoupledConcreteB
    {
        public void GenerateString(ref string s) { s += " from" + this.GetType().ToString(); }
    }

    public class Printer
    {
        bool condition;
        public Printer(bool cond)
        {
            condition = cond;
        }

        public void SetCondition(bool value) { condition = value; }

        public void Print()
        {
            string result;
            string input = " this message is";
            if (condition)
            {
                var stringGenerator = new StrongCoupledConcreteA();
                result = stringGenerator.GenerateString(input);
            }
            else
            {
                var stringGenerator = new StrongCoupledConcreteB();
                result = input;
                stringGenerator.GenerateString(ref result);
            }
            Console.WriteLine(result);
        }

    }
    public class Context
    {
        Printer printer;
        public void DoWork()
        {
            printer = new Printer(true);
            printer.Print();

            printer.SetCondition(false);
            printer.Print();
        }

    }

حال کد بازنویسی شده را با آن مقایسه کنید:

public interface IStringGenerator
        {
            string GenerateString(string s);
        }
        public class LooslyCoupledConcreteA : IStringGenerator
        {
            public string GenerateString(string s)
            {
                return s + " from " + this.GetType().ToString();
            }
        }
        public class LooslyCoupledConcreteB : IStringGenerator
        {
            public string GenerateString(string s)
            {
                return s + " from " + this.GetType().ToString();
            }
        }

           public class Printer
           {
               bool condition;
               public Printer(bool cond)
               {
                   condition = cond;
               }

               public void SetCondition(bool value) { condition = value; }

               public void Print()
               {
                   string result;
                   string input = " this message is";
                   IStringGenerator generator;
                   if (condition)
                   {
                       generator = new LooslyCoupledConcreteA();
                   }
                   else
                   {
                       generator = new LooslyCoupledConcreteB();
                   }
                   
                   result = generator.GenerateString(input);
                   Console.WriteLine(result);

               }

           }

با کمی دقت مشاهده میکنیم که در کلاس‌های strongly coupled با اینکه هدف هر دو کلاس تولید یک رشته است، ولی عدم وجود پروتکل باعث شده است نحوه‌ی گرفتن ورودی و برگرداندن خروجی متفاوت شود و در نتیجه نیازمند به اضافه کردن پیچیدگی در کلاس فراخوانی کننده‌ی آن‌ها می‌شویم. این در حالی است که در روش loosely coupled با ایجاد یک پروتکل (واسط IStringGenerator ) این پیچیدگی از بین رفته است. در اینجا نوع اتصال (وابستگی) از جنس اتصال (وابستگی) قوی به تعریف (prototype) و شاید به نوعی نحوه‌ی پیاده سازی متد می‌باشد.


SOLID Principles *

پنج اصل بعدی به اصول SOLID معروف هستند.

S: Single Responsibility

O: Open/Closed

L: Liskov’s Substitution

I: Interface Segregation

D: Dependency Injection


اصل پنجم: Single responsibility

"به دنبال ماژول‌های تک مسئولیتی باش"

در این قسمت مقصود از مسئولیت، «دلیلی است که کلاس باید تغییر کند» بدین معنا که اگر کلاسی با چند دلیل متفاوت مجبور به تغییر شود، آن کلاس چند مسئولیتی است. کلاس‌های چند مسئولیتی عموما کد حجیمی دارند؛ نام آنها تعریف دقیقی را از مسئولیتشان ارائه نمی‌دهد و با عنوانی بسیار کلی نامگذاری میشوند و اشکال زدایی آنها بسیار طاقت فرساست. از طرفی، چند مسئولیتی بودن یک کلاس، باعث از بین رفتن مزایای توارث می‌شود. مثلا فرض کنید دو مسئولیت A,B در واسطی بیان می‌شوند که به یکدیگر مرتبط نبوده و مستقلند. برای  مسئولیت A دو پیاده سازی و برای مسئولیت B،   سه پیاده سازی در نظر گرفته شده است و جمعا برای پشتیبانی از تمامی حالات باید شش کلاس پیاده ساز، در نظر گرفته شود که  توارث را سخت و بی معنی میکند زیرا قابلیت استفاده مجدد را از توارث سلب کرده است. با این وجود عملا رعایت همچین نکته‌ای در دنیای واقعی کار سختی است.

مثال زیر این مشکل را بیان می‌دارد: 

// single responsibility principle - bad example

    interface IEmail
    {
        void SetSender(string sender);
        void SetReceiver(string receiver);
        void SetContent(string content);
    }

    class Email : IEmail
    {
        public void SetSender(string sender)
        {
            throw new NotImplementedException();
        }
        public void SetReceiver(string receiver)
        {
            throw new NotImplementedException();
        }

        public void SetContent(string content)
        {
            throw new NotImplementedException();
        }
    }

در این مثال کلاس Email دارای دو مسئولیت (دلیل برای تغییر) است: الف- نحوه مقداردهی فرستنده و گیرنده براساس پروتکل‌های مختلف مانند IMAP, POP3 ، بدین معنا که با تغییر پروتکل نیاز به تغییر پیاده سازی خواهیم شد. ب- تعریف محتوای پیام، بدین معنا که برای پشتیبانی از محتوای html, xml   نیاز به تغییر کلاس Email داریم.

با تغییر طراحی خواهیم داشت: 

// single responsibility principle - good example
    public interface IMessage
    {
        void SetSender(string sender);
        void SetReceiver(string receiver);
        void SetContent(IContent content);
    }

    public interface IContent
    {
        string GetAsString(); // used for serialization
    }

    public class Email : IMessage
    {        
        public void SetSender(string sender)
        {
            throw new NotImplementedException();
        }

        public void SetReceiver(string receiver)
        {
            throw new NotImplementedException();
        }

        public void SetContent(IContent content)
        {
            throw new NotImplementedException();
        }
    }

در اینجا واسط IContent مسئولیت پشتیبانی از xml, html را خواهد داشت و نیازی به تغییر کلاس Email برای پشتیبانی از این فرمت‌های محتوای پیام را نخواهیم داشت.


اصل ششم: Open for extension, close for modification :  Open/Closed Principle

"پذیرای توسعه و بازدارنده از تغییر هر آنچه که هست، باش"

ا ین اصل می‌گوید طراحی باید به گونه‌ای باشد که با اضافه شدن یک ویژگی، کد‌های قبلی تغییری نکنند و فقط کدهای جدید برای پیاده سازی ویژگی جدید نوشته شوند.  برای درک بهتر به مثال زیر توجه کنید:

public class AreaCalculator
        {
            public double Area(object[] shapes)
            {
                double area = 0;

                foreach (var shape in shapes)
                {

                    if (shape is Square)
                    {
                        Square square = (Square)shape;
                        area += Math.Sqrt(square.Height);
                    }

                    if (shape is Triangle)
                    {
                        Triangle triangle = (Triangle)shape;
                        double TotalHalf = (triangle.FirstSide + triangle.SecondSide + triangle.ThirdSide) / 2;
                        area += Math.Sqrt(TotalHalf * (TotalHalf - triangle.FirstSide) *
                        (TotalHalf - triangle.SecondSide) * (TotalHalf - triangle.ThirdSide));
                    }

                    if (shape is Circle)
                    {
                        Circle circle = (Circle)shape;
                        area += circle.Radius * circle.Radius * Math.PI;
                    }

                }
                return area;
            }
        }
        public class Square
        {
            public double Height { get; set; }
        }
        public class Circle
        {
            public double Radius { get; set; }
        }
        public class Triangle
        {
            public double FirstSide { get; set; }
            public double SecondSide { get; set; }
            public double ThirdSide { get; set; }
        }

در اینجا کلاس AreaCalculator برای محاسبه مساحت تمام اشیاء ورودی، مساحت تک تک اشیاء را محاسبه میکند و نتیجه را برمی‌گرداند. در این مثال با اضافه شدن شکل هندسی جدید، باید کد این کلاس تغییر کند که با اصل Open/Closed مغایر است. برای بهبود این کد طراحی زیر پیشنهاد شده است:

public class AreaCalculator
{
    public double Area(Shape[] shapes)
    {
        double area = 0;

        foreach (var shape in shapes)
        {
            area += shape.Area();
        }

        return area;
    }
}
public abstract class Shape
{
    public abstract double Area();
}
public class Square : Shape
{
    public double Height { get { return _height; } }
    private double _height;

    public Square(double Height)
    {
        _height = Height;
    }

    public override double Area()
    {
        return Math.Sqrt(_height);
    }
}
public class Circle : Shape
{
    public double Radius { get { return _radius; } }

    private double _radius;

    public Circle(double Radius)
    {
        _radius = Radius;
    }

    public override double Area()
    {
        return _radius * _radius * Math.PI;
    }
}
public class Triangle : Shape
{
    public double FirstSide { get { return _firstSide; } }
    public double SecondSide { get { return _secondSide; } }
    public double ThirdSide { get { return _thirdSide; } }

    private double _firstSide;
    private double _secondSide;
    private double _thirdSide;

    public Triangle(double FirstSide, double SecondSide, double ThirdSide)
    {
        _firstSide = FirstSide;
        _secondSide = SecondSide;
        _thirdSide = ThirdSide;
    }

    public override double Area()
    {
        double TotalHalf = (_firstSide + _secondSide + _thirdSide) / 2;
        return Math.Sqrt(TotalHalf * (TotalHalf - _firstSide) * (TotalHalf - _secondSide) * (TotalHalf - _thirdSide));
    }
}

در این طراحی، پیچیدگی محاسبه مساحت هر شکل به کلاس آن شکل منتقل شده است و با اضافه شدن شکل جدید نیازی به تغییر کلاس AreaCalculator نداریم.

در مقاله‌ی بعدی به سه اصل دیگر اصول SOLID خواهم پرداخت.

مطالب دوره‌ها
بررسی Semantic Search و FTS Table-valued functions
Semantic Search جزو تازه‌های SQL Server 2012 است و مقدمات نصب و فعال سازی آن‌را در قسمت اول بررسی کردیم.
توابع Predicates مختص به FTS مانند Contains و Freetext، تنها ردیف‌های متناظر با جستجوی انجام شده را باز می‌گردانند و رتبه‌ای به نتایج جستجو اعمال نمی‌گردد. برای مثال، مشخص نیست اولین ردیف بازگشت داده شده بهترین تطابق را با جستجوی انجام شده دارد یا بدترین نتیجه‌ی ممکن است. برای رفع این مشکل FTS table-valued functions معرفی شده‌اند. حاصل این‌ها یک جدول با دو ستون است. ستون اول کلید متناظر با جدول تطابق یافته بوده و ستون دوم، Rank نام دارد که بیانگر میزان مفید بودن و درجه‌ی اعتبار ردیف بازگشت داده شده‌است.
Semantic Search نیز به کمک سه table-valued functions پیاده سازی می‌شود. همچنین باید دقت داشت که تمام زبان‌های پشتیبانی شده توسط FTS در حالت Semantic Search پشتیبانی نمی‌شوند. برای بررسی این مورد، دو کوئری ذیل را اجرا نمائید:
 -- Full text Languages
SELECT *
FROM sys.fulltext_languages
ORDER BY name;

-- Semantic Search Languages
SELECT *
FROM sys.fulltext_semantic_languages
ORDER BY name;
GO

بررسی table-valued functions مختص به FTS

دو متد ویژه‌ی CONTAINSTABLE و FREETEXTTABLE خروجی از نوع جدول دارند؛ با ستون‌هایی به نام‌های key و rank. اگر قسمت ایجاد کاتالوگ FTS و ایندکس آن‌را بخاطر داشته باشید، در حین ایجاد ایندکس FTS می‌بایستی KEY INDEX PK_Documents را نیز ذکر کرد. کاربرد آن در همین table-valued functions است.
مقدار rank، عددی است بین 0 و 1000 که هر چقدر مقدار آن بیشتر باشد، یعنی نتیجه‌ای نزدیک‌تر، به عبارت جستجو شده، یافت گردیده‌است. باید دقت داشت که این عدد فقط در زمینه‌ی یک کوئری معنا پیدا می‌کند و مقایسه‌ی rank دو کوئری مختلف با هم، بی‌معنا است.
عملکرد CONTAINSTABLE بسیار شبیه به متد Contains است با این تفاوت که قابلیت‌های بیشتری دارد. برای مثال در اینجا می‌توان برای قسمتی از جستجو، وزن و اهمیت بیشتری را قائل شد و این حالت تنها زمانی معنا پیدا می‌کند که خروجی جستجو، دارای rank باشد.
متد FREETEXTTABLE نیز بسیار شبیه به FREETEXT عمل کرده و نسبت به CONTAINSTABLE بسیار ساده‌تر است. برای نمونه امکان تعریف وزن، formsof، near و غیره در اینجا وجود ندارد. به علاوه عملگرهای منطقی مانند and و or نیز در اینجا کاربردی نداشته و صرفا یک noise word درنظر گرفته می‌شوند.


چند مثال جهت بررسی عملکرد دو متد CONTAINSTABLE و FREETEXTTABLE

استفاده از متد CONTAINSTABLE
 -- Rank with CONTAINSTABLE
SELECT D.id, D.title, CT.[RANK], D.docexcerpt
FROM CONTAINSTABLE(dbo.Documents, docexcerpt,
N'data OR level') AS CT
 INNER JOIN dbo.Documents AS D
  ON CT.[KEY] = D.id
ORDER BY CT.[RANK] DESC;
چون متد CONTAINSTABLE خروجی از نوع table دارد، در قسمت from ذکر شده‌است.
این متد ابتدا نام جدول مورد بررسی را دریافت می‌کند. سپس ستونی که باید جستجو بر روی آن انجام شود و در ادامه عبارت جستجو شونده، مشخص می‌گردد. اگر این متد را به تنهایی اجرا کنیم:
 SELECT * FROM CONTAINSTABLE(dbo.Documents, docexcerpt, N'data OR level')
همانطور که عنوان شد، صرفا یک سری ردیف اشاره کننده به id و rank را بازگشت می‌دهد. به همین جهت join نوشته شده‌است تا بتوان رکوردهای اصلی را نیز در همینجا به همراه rank متناظر، نمایش داد.


استفاده از متد FREETEXTTABLE

 -- Rank with FREETEXTTABLE
SELECT D.id, D.title, FT.[RANK], D.docexcerpt
FROM FREETEXTTABLE (dbo.Documents, docexcerpt,
N'data level') AS FT
 INNER JOIN dbo.Documents AS D
  ON FT.[KEY] = D.id
ORDER BY FT.[RANK] DESC;
کلیات عملکرد متد FREETEXTTABLE بسیار شبیه است به متد CONTAINSTABLE؛ با این تفاوت که ساده‌تر بوده و بسیاری از قابلیت‌های پیشرفته و سفارشی CONTAINSTABLE را به صورت خودکار و یکجا اعمال می‌کند. به همین جهت دقت آن، اندکی کمتر بوده و عمومی‌تر عمل می‌کند.
در اینجا اگر نیاز باشد تا تعداد نتایج را شبیه به کوئری‌های top n محدود نمود، می‌توان از پارامتر عددی بعدی که برای نمونه به 2 تنظیم شده‌است، استفاده کرد:
 -- Rank with FREETEXTTABLE and top_n_by_rank
SELECT D.id, D.title, FT.[RANK], D.docexcerpt
FROM FREETEXTTABLE (dbo.Documents, docexcerpt,
N'data level', 2) AS FT
 INNER JOIN dbo.Documents AS D
  ON FT.[KEY] = D.id
ORDER BY FT.[RANK] DESC;
در این کوئری تنها 2 ردیف بازگشت داده می‌شود.

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

 -- Weighted terms
SELECT D.id, D.title, CT.[RANK], D.docexcerpt
FROM CONTAINSTABLE
(dbo.Documents, docexcerpt,
N'ISABOUT(data weight(0.8), level weight(0.2))') AS CT
 INNER JOIN dbo.Documents AS D
  ON CT.[KEY] = D.id
ORDER BY CT.[RANK] DESC;
با استفاده از واژه کلیدی ISABOUT، امکان تعیین وزن، برای واژه‌های در حال جستجو ممکن می‌شوند. در این کوئری اهمیت واژه data بیشتر از اهمیت واژه level تعیین شده‌است.


انجام جستجو‌های Proximity
 -- Proximity term
SELECT D.id, D.title, CT.[RANK]
FROM CONTAINSTABLE (dbo.Documents, doccontent,
N'NEAR((data, row), 30)') AS CT
 INNER JOIN dbo.Documents AS D
  ON CT.[KEY] = D.id
ORDER BY CT.[RANK] DESC;
GO
در اینجا مانند متد CONTAINS، امکان انجام جستجوهای Proximity نیز وجود دارد. برای مثال در کوئری فوق به دنبال رکوردهایی هستیم که در آن‌ها واژه‌های data و row وجود دارند، با فاصله‌ای کمتر از 30 کلمه.


بررسی Semantic Search key valued functions

متد SEMANTICKEYPHRASETABLE کار بازگشت واژه‌های کلیدی آنالیز شده توسط FTS را انجام داده و جدولی حاوی 4 ستون را باز می‌گرداند. این چهار ستون عبارتند از:
- column_id: شماره ستون واژه کلیدی یافت شده‌است. تفسیر آن نیاز به استفاده از تابع سیستمی COL_NAME دارد (مانند مثال زیر).
- document_key: متناظر است با کلید اصلی جدولی که بر روی آن کوئری گرفته می‌شود.
- keyphrase: همان واژه کلیدی است.
- score: رتبه‌ی واژه کلیدی است در بین سایر واژه‌هایی که بازگشت داده شده و عددی است بین صفر تا یک.

مثالی از آن‌را در ادامه ملاحظه می‌کنید:
 -- Top 100 semantic key phrases
SELECT TOP (100)
 D.id, D.title,
 SKT.column_id,
 COL_NAME(OBJECT_ID(N'dbo.Documents'), SKT.column_id) AS column_name,
 SKT.document_key,
 SKT.keyphrase, SKT.score
FROM SEMANTICKEYPHRASETABLE
(dbo.Documents, doccontent) AS SKT
 INNER JOIN dbo.Documents AS D
  ON SKT.document_key = D.id
ORDER BY SKT.score DESC;


در متد جدولی SEMANTICKEYPHRASETABLE، ابتدا جدول مورد نظر و سپس ستونی که نیاز است واژه‌های کلیدی آنالیز شده‌ی آن بازگشت داده شوند، قید می‌گردند. document_key آن به تنهایی شاید مفید نباشد. به همین جهت join شده‌است به جدول اصلی، تا بتوان رکوردهای متناظر را نیز بهتر تشخیص داد.
به این ترتیب مهم‌ترین واژه‌های کلیدی ستون doccontent را به همراه درجه‌ی اهمیت و رتبه‌ی آن‌ها، می‌توان گزارش گرفت.


متد SEMANTICSIMILARITYTABLE برای یافتن سندهای مشابه با یک سند مشخص بکار می‌روند؛ چیزی شبیه به گزارش «مقالات مشابه مطلب جاری» در بسیاری از سایت‌های ارائه‌ی محتوا. ستون‌های خروجی آن عبارتند از:
- source_column_id: شماره ستون منبع انجام کوئری.
- matched_column_id: شماره ستون سند مشابه یافت شده.
- matched_document_key: متناظر است با کلید اصلی جدولی که بر روی آن کوئری گرفته می‌شود.
- score: رتبه‌ی نسبی سند مشابه یافت شده.

 -- Documents that are similar to document 1
SELECT S.source_document_title,
 SST.matched_document_key,
 D.title AS matched_document_title,
 SST.score
FROM
 (SEMANTICSIMILARITYTABLE
  (dbo.Documents, doccontent, 1) AS SST
  INNER JOIN dbo.Documents AS D
ON SST.matched_document_key = D.id)
 CROSS JOIN
  (SELECT title FROM dbo.Documents WHERE id=1)
AS S(source_document_title)
ORDER BY SST.score DESC;



در این کوئری، اسناد مشابه با سند شماره 1 یافت شده‌اند. مبنای جستجو نیز ستون doccontent، جدول dbo.Documents است. از join بر روی matched_document_key و id جدول اصلی، مشخصات سند یافت شده را می‌توان استخراج کرد. کار CROSS JOIN تعریف شده، صرفا افزودن یک ستون مشخص به نتیجه‌ی خروجی کوئری است.
همانطور که در تصویر مشخص است، سند شماره 4 بسیار شبیه است به سند شماره 1. در ادامه قصد داریم بررسی کنیم که علت این شباهت چه بوده‌است؟


متد SEMANTICSIMILARITYDETAILSTABLE واژه‌های کلیدی مهم مشترک بین دو سند را بازگشت می‌دهد (سند منبع و سند مقصد). به این ترتیب می‌توان دریافت، چه واژه‌های کلیدی سبب شده‌اند تا این دو سند به هم شبیه باشند. ستون‌های خروجی آن عبارتند از:
- keyphrase: واژه‌ی کلیدی
- score: رتبه‌ی نسبی واژه‌ی کلیدی

 -- Key phrases that are common across two documents
SELECT SSDT.keyphrase, SSDT.score
FROM SEMANTICSIMILARITYDETAILSTABLE
(dbo.Documents, doccontent, 1,
doccontent, 4) AS SSDT
ORDER BY SSDT.score DESC;


در کوئری فوق قصد داریم بررسی کنیم چه واژه‌های کلیدی، سبب مشابهت سندهای شماره 1 و 4 شده‌اند و بین آن‌ها مشترک می‌باشند.
مطالب
بهینه سازی برنامه‌های وب ASP.NET برای موتورهای جستجو (SEO)
می‌دانیم بهینه‌سازی موتورهای جستجو (به انگلیسی: ‎Search engine optimization (SEO)‎)‏ که گاهی در فارسی به آن سئو نیز گفته می‌شود، عملیاتی است برای بهبود دید یک وب‌گاه یا یک صفحهٔ وب، در صفحه نتایج موتورهای جستجو که می‌تواند طبیعی و یا الگوریتمی باشد. این عملیات برای وبمسترها یکی از عوامل مهم و حیاتی بدست آوردن کاربران جدید از موتورهای جستجو است.

اگر چک لیست‌های SEO وب سایت ها را مشاهده کنیم، می‌توانیم آن‌ها را در دو دسته‌ی کلی بهینه سازی درونی و برونی وب سایت در نظر بگیریم:
Off-Page Optimization
یا برونی ، که بیشتر بر دوش مشاوران سئو و خود مدیران وب سایت است.(link building ، فعالیت در شبکه اجتماعی و ...)
و اما در حوزه On-Page Optimization یا درونی که بخش‌های مهمی از آن وظیفه‌ی مابرنامه نویس‌ها است.(H1 Tag ، URL Naming ، Meta Tags ، عنوان صفحه و ...)
[البته عامل درونی بهینه سازی محتوا (Content Optimization)  که مهمترین عامل در الگوریتم‌های نسل جدید موتورهای جستجو و همچنین الگوریتم جدید گوگل+) به حساب می‌آید بر عهده مشاوران سئو و خود مدیران وب سایت می‌باشد]


در ادامه به ارائه چند راهکار جهت بهینه سازی برنامه‌های وب ASP.NET مان برای موتورهای جستجو می‌پردازیم:


1.متدی برای ایجاد عنوان سایت
    private const string SeparatorTitle = " - ";
    private const int MaxLenghtTitle = 60;
    public static string GeneratePageTitle(params string[] crumbs)
    {
        var title = "";

        for (int i = 0; i < crumbs.Length; i++)
        {
            title += string.Format
                        (
                            "{0}{1}",
                            crumbs[i],
                            (i < crumbs.Length - 1) ? SeparatorTitle : string.Empty
                        );
        }

        title = title.Substring(0, title.Length <= MaxLenghtTitle ? title.Length : MaxLenghtTitle).Trim();

        return title;
    }
نکته :
  • MaxLenghtTitle پیشنهادی برای عنوان سایت 60 می‌باشد.

2.متدی برای ایجاد متاتگ صفحات سایت
public enum CacheControlType
{
    [Description("public")]
    _public,
    [Description("private")]
    _private,
    [Description("no-cache")]
    _nocache,
    [Description("no-store")]
    _nostore
}  
private const int MaxLenghtTitle = 60; private const int MaxLenghtDescription = 170; private const string FaviconPath = "~/cdn/ui/favicon.ico"; public static string GenerateMetaTag(string title, string description, bool allowIndexPage, bool allowFollowLinks, string author = "", string lastmodified = "", string expires = "never", string language = "fa", CacheControlType cacheControlType = CacheControlType._private) { title = title.Substring(0, title.Length <= MaxLenghtTitle ? title.Length : MaxLenghtTitle).Trim(); description = description.Substring(0, description.Length <= MaxLenghtDescription ? description.Length : MaxLenghtDescription).Trim(); var meta = ""; meta += string.Format("<title>{0}</title>\n", title); meta += string.Format("<link rel=\"shortcut icon\" href=\"{0}\"/>\n", FaviconPath); meta += string.Format("<meta http-equiv=\"content-language\" content=\"{0}\"/>\n", language); meta += string.Format("<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\"/>\n"); meta += string.Format("<meta charset=\"utf-8\"/>\n"); meta += string.Format("<meta name=\"description\" content=\"{0}\"/>\n", description); meta += string.Format("<meta http-equiv=\"Cache-control\" content=\"{0}\"/>\n", EnumExtensions.EnumHelper<CacheControlType>.GetEnumDescription(cacheControlType.ToString())); meta += string.Format("<meta name=\"robots\" content=\"{0}, {1}\" />\n", allowIndexPage ? "index" : "noindex", allowFollowLinks ? "follow" : "nofollow"); meta += string.Format("<meta name=\"expires\" content=\"{0}\"/>\n", expires); if (!string.IsNullOrEmpty(lastmodified)) meta += string.Format("<meta name=\"last-modified\" content=\"{0}\"/>\n", lastmodified); if (!string.IsNullOrEmpty(author)) meta += string.Format("<meta name=\"author\" content=\"{0}\"/>\n", author); //------------------------------------Google & Bing Doesn't Use Meta Keywords ... //meta += string.Format("<meta name=\"keywords\" content=\"{0}\"/>\n", keywords); return meta; }
چند نکته :

3.متدی برای ایجاد Slug ( اسلاگ آدرسی با مفهوم برای بکار بردن در URL ها است که دوست‌دار موتورهای جستجو می‌باشد)
private const int MaxLenghtSlug = 45;
public static string GenerateSlug(string title)
{
        var slug = RemoveAccent(title).ToLower();
        slug = Regex.Replace(slug, @"[^a-z0-9-\u0600-\u06FF]", "-");
        slug = Regex.Replace(slug, @"\s+", "-").Trim();
        slug = Regex.Replace(slug, @"-+", "-");
        slug = slug.Substring(0, slug.Length <= MaxLenghtSlug ? slug.Length : MaxLenghtSlug).Trim();

        return slug;
}
    
private static string RemoveAccent(string text)
{
        var bytes = Encoding.GetEncoding("UTF-8").GetBytes(text);
        return Encoding.UTF8.GetString(bytes);
}
نکته :
  • MaxLenghtSlug پیشنهادی برای عنوان سایت 45 می‌باشد. 

نمونه ای از کاربرد توابع :
   Head.InnerHtml = SEO.GenerateMetaTag
                            (
                                title: SEO.GeneratePageTitle(".NET Tips", "آرشیو مطالب", "ASP.NET MVC #1"),
                                description: "چرا ASP.NET MVC با وجود فریم ورک پخته‌ای به نام ASP.NET web forms، اولین سؤالی که حین سوئیچ به ASP.NET MVC مطرح می‌شود این است: «برای چی؟». بنابراین تا به این سؤال پاسخ داده نشود، هر نوع بحث فنی در این مورد بی فایده است.",
                                allowIndexPage: true,
                                allowFollowLinks: true,
                                author: "وحید نصیری",
                                cacheControlType: SEO.CacheControlType._private
                            );
و خروجی در Page Source :
<title>.NET Tips - آرشیو مطالب - ASP.NET MVC #1</title>
<link rel="shortcut icon" href="../../cdn/images/ui/favicon.ico"/>
<meta http-equiv="content-language" content="fa"/>
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
<meta charset="utf-8"/>
<meta name="description" content="چرا ASP.NET MVC ؟با وجود فریم ورک پخته‌ای به نام ASP.NET web forms، اولین سؤالی که حین سوئیچ به ASP.NET MVC مطرح می‌شود این است: &#171;برای چی؟&#187;. بن ..."/>
<meta http-equiv="Cache-control" content="private"/>
<meta name="robots" content="index, follow" />
<meta name="expires" content="never"/>
<meta name="author" content="وحید نصیری"/>
موفق باشید