اگر نظرات رو مطالعه کنید، چندین نگارش از این افزونه موجود هست که خروجیهای متفاوتی رو نیاز دارند. ضمنا بهتره این افزونه رو با یک نمونهای که مدام داره به روز میشه تعویض کنید.
آیا پیاده سازی تابع matcher افزونه typeahead با استفاده از Htmlhelper امکان پذیر است؟
یا به عبارت دیگر: آیا امکان پیاده سازی کامل این افزونه در سمت سرور وجود دارد؟
سلام ... خیلی ممنون بابت این افزونه ...
گویا این افزونه روی vs 2012 کار نمیکنه! ... راهی هست که بشه کار کنه؟!
نظرات مطالب
ارسال ایمیل در ASP.NET Core
از کدهای زیر در پروژه core 5 استفاده شده است و افزونه دیگری نصب نشده است و فقط از smtpClient استفاده شده است و مشکلی در اجرا نداشته اند و ایمیل به خوبی ارسال شد:
var smtp = new SmtpClient { Host = StaticValues.HostEmail, Port = StaticValues.PortEmail, EnableSsl = false, //StaticValues.SslEmail, DeliveryMethod = SmtpDeliveryMethod.Network, UseDefaultCredentials = false, Credentials = new NetworkCredential(fromEmail, EmailPassword) }; using(var message = new MailMessage(fromEmail, toEmail, subject, body) { BodyEncoding = Encoding.UTF8, IsBodyHtml = true, HeadersEncoding = Encoding.UTF8, DeliveryNotificationOptions = DeliveryNotificationOptions.OnFailure }) { smtp.Send(message); }
اشتراکها
پیاده سازی AOP در TypeScript
مطالب دورهها
اصول طراحی یک سیستم افزونه پذیر به کمک StructureMap
قصد داریم سیستمی را طراحی کنیم که افزونههای خود را در زمان اجرا از مسیری خاص خوانده و سپس وهلههای آنهارا جهت استفاده در دسترس قرار دهد. برنامهای که در اینجا مورد بررسی قرار میگیرد، یک برنامهی WinForms ساده است؛ به نام WinFormsWithPluginSupport. اما اصول کلی مطرح شده، در تمام فناوریهای دیگر دات نتی نیز کاربرد دارد و یکسان است.
تهیه قرارداد
یک پروژهی Class library به نام PluginsBase را به Solution جاری اضافه کنید. به آن اینترفیس قرار داد پلاگینهای برنامه خود را اضافه نمائید. برای مثال:
هر پلاگین دارای یک نام یا توضیح خاص خود خواهد بود به همراه متدی برای اجرای منطق مرتبط با آن.
تهیه سه پلاگین جدید
به Solution جاری سه پروژهی مجزای Class library با نامهای plugin1 تا 3 را اضافه کنید. در ادامه به هر پلاگین، ارجاعی را به اسمبلی PluginsBase، برای دریافت قرارداد پیاده سازی منطق پلاگین، اضافه نمائید. هدف این است که اینترفیس IPlugin، در این اسمبلیها قابل دسترسی شود.
هر پلاگین هم دارای برای مثال کدهایی مانند کد ذیل خواهد بود که در آن صرفا نام آنها به 2 و 3 تنظیم میشود.
کپی خودکار پلاگینها به پوشهی مخصوص آنها
به پروژهی WinFormsWithPluginSupport مراجعه کنید. در پوشهی bin\debug آن یک پوشهی جدید به نام Plugins ایجاد نمائید. بدیهی است هربار که پلاگینهای برنامه تغییر کنند نیاز است اسمبلیهای نهایی آنها را به این پوشه کپی نمائیم. اما راه بهتری نیز وجود دارد. به خواص هر کدام از پروژههای پلاگین مراجعه کرده و برگهی Build events را باز کنید.
در اینجا قسمت post-build event را به نحو ذیل تغییر دهید:
این کار را برای هر سه پلاگین تکرار کنید.
به این ترتیب هربار که پلاگین جاری کامپایل شود، پس از آن به صورت خودکار به پوشهی plugins تعیین شده، کپی میشود و دیگر نیازی به کپی دستی نخواهد بود.
تنظیم فوق، تنها اسمبلی اصلی پروژه را به پوشهی bin\debug\plugins کپی میکند. اگر میخواهید تمام فایلها کپی شوند، از تنظیم ذیل استفاده کنید:
اضافه کردن وابستگیهای اصلی پروژهی WinForms
در ادامه بستهی نیوگت StructureMap را به پروژهی WinForms از طریق دستور ذیل اضافه کنید:
همچنین این پروژه تنها نیاز دارد ارجاع مستقیمی را به اسمبلی PluginsBase ابتدای مطلب داشته باشد. از آن، جهت تنظیمات اولیه یافتن افزونهها استفاده میکنیم.
تعریف محل ثبت پلاگینها
روشهای متفاوتی برای کار با StructureMap وجود دارد. یکی از آنها تعریف کلاسی است مشتق شده از کلاس Registry آن به نحو ذیل:
در اینجا مشخص کردهایم که اسمبلیهای پوشه plugins را که یک سطح پایینتر از پوشهی اجرایی برنامه قرار میگیرند، خوانده و در این بین آنهایی را که پیاده سازی از اینترفیس IPlugin دارند، در دسترس قرار دهد.
یک نکتهی مهم
در قسمت assemblyFilter تعیین کردهایم که اسمبلی تکراری PluginBase بارگذاری نشود. چون این اسمبلی هم اکنون به برنامهی WinForms ارجاع دارد. رعایت این نکته جهت رفع تداخلات آتی بسیار مهم است. همچنین این فایل در پوشهی Plugins نیز نباید حضور داشته باشد وگرنه شاهد بارگذاری افزونهها نخواهید بود.
سپس نیاز به وهله سازی Container آن و معرفی این کلاس PluginsRegistry میباشد:
تنظیمات ابتدایی WinForms برای دسترسی به امکانات StructureMap
به فرم اصلی برنامه مراجعه کرده و به سازندهی آن IContainer را اضافه کنید. از این اینترفیس جهت دسترسی به پلاگینهای برنامه استفاده خواهیم کرد.
اکنون برنامه دیگر کامپایل نخواهد شد؛ چون در فایل Program.cs وهله سازی مستقیمی از FrmMain وجود دارد. این وهله سازی را اکنون به StructureMap محول میکنیم تا مشکل برطرف شود:
زمانیکه از متد IocConfig.Container.GetInstance استفاده میشود، تا هر تعداد سطحی که تعریف شده، سازندههای کلاسهای مرتبط وهله سازی میشوند. در اینجا نیاز است سازندهی کلاس FrmMain وهله سازی شود. چون IContainer اینترفیس اصلی خود StructureMap است، آنرا شناخته و به صورت خودکار وهله سازی میکند. اگر اینترفیس دیگری را ذکر کنید، نیاز است مطابق معمول آنرا در کلاس IocConfig و متد defaultContainer آن معرفی نمائید.
بارگذاری و اجرای افزونهها
دو دکمهی Run و ReLoad را به فرم اصلی برنامه با کدهای ذیل اضافه کنید:
در اینجا توسط متد container.GetAllInstances میتوان به تمامی وهلههای پلاگینهای بارگذاری شده، دسترسی یافت و سپس آنها را اجرا کرد.
همچنین در متد ReLoad نحوهی بارگذاری مجدد این پلاگینها را در صورت نیاز مشاهده میکنید.
اگر برنامه را اجرا کردید و پلاگینی بارگذاری نشد، به دنبال اسمبلیهای تکراری بگردید. برای مثال PluginsBase نباید هم در پوشهی اصلی اجرایی برنامه حضور داشته باشد و هم در پوشهی پلاگینها.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید
WinFormsWithPluginSupport.zip
تهیه قرارداد
یک پروژهی Class library به نام PluginsBase را به Solution جاری اضافه کنید. به آن اینترفیس قرار داد پلاگینهای برنامه خود را اضافه نمائید. برای مثال:
namespace PluginsBase { public interface IPlugin { string Name { get; } void Run(); } }
تهیه سه پلاگین جدید
به Solution جاری سه پروژهی مجزای Class library با نامهای plugin1 تا 3 را اضافه کنید. در ادامه به هر پلاگین، ارجاعی را به اسمبلی PluginsBase، برای دریافت قرارداد پیاده سازی منطق پلاگین، اضافه نمائید. هدف این است که اینترفیس IPlugin، در این اسمبلیها قابل دسترسی شود.
هر پلاگین هم دارای برای مثال کدهایی مانند کد ذیل خواهد بود که در آن صرفا نام آنها به 2 و 3 تنظیم میشود.
using PluginsBase; namespace Plugin1 { public class Plugin1Main : IPlugin { public string Name { get { return "Test 1"; } } public void Run() { // todo: ... } } }
کپی خودکار پلاگینها به پوشهی مخصوص آنها
به پروژهی WinFormsWithPluginSupport مراجعه کنید. در پوشهی bin\debug آن یک پوشهی جدید به نام Plugins ایجاد نمائید. بدیهی است هربار که پلاگینهای برنامه تغییر کنند نیاز است اسمبلیهای نهایی آنها را به این پوشه کپی نمائیم. اما راه بهتری نیز وجود دارد. به خواص هر کدام از پروژههای پلاگین مراجعه کرده و برگهی Build events را باز کنید.
در اینجا قسمت post-build event را به نحو ذیل تغییر دهید:
Copy "$(ProjectDir)$(OutDir)$(TargetName).*" "$(SolutionDir)WinFormsWithPluginSupport\bin\debug\Plugins"
به این ترتیب هربار که پلاگین جاری کامپایل شود، پس از آن به صورت خودکار به پوشهی plugins تعیین شده، کپی میشود و دیگر نیازی به کپی دستی نخواهد بود.
تنظیم فوق، تنها اسمبلی اصلی پروژه را به پوشهی bin\debug\plugins کپی میکند. اگر میخواهید تمام فایلها کپی شوند، از تنظیم ذیل استفاده کنید:
Copy "$(ProjectDir)$(OutDir)*.*" "$(SolutionDir)WinFormsWithPluginSupport\bin\debug\Plugins"
اضافه کردن وابستگیهای اصلی پروژهی WinForms
در ادامه بستهی نیوگت StructureMap را به پروژهی WinForms از طریق دستور ذیل اضافه کنید:
PM> install-package structuremap
تعریف محل ثبت پلاگینها
روشهای متفاوتی برای کار با StructureMap وجود دارد. یکی از آنها تعریف کلاسی است مشتق شده از کلاس Registry آن به نحو ذیل:
using System.IO; using System.Windows.Forms; using PluginsBase; using StructureMap.Configuration.DSL; using StructureMap.Graph; namespace WinFormsWithPluginSupport.Core { public class PluginsRegistry : Registry { public PluginsRegistry() { this.Scan(scanner => { scanner.AssembliesFromPath( path: Path.Combine(Application.StartupPath, "plugins"), // یک اسمبلی نباید دوبار بارگذاری شود assemblyFilter: assembly => { return !assembly.FullName.Equals(typeof(IPlugin).Assembly.FullName); }); scanner.AddAllTypesOf<IPlugin>().NameBy(item => item.FullName); }); } } }
یک نکتهی مهم
در قسمت assemblyFilter تعیین کردهایم که اسمبلی تکراری PluginBase بارگذاری نشود. چون این اسمبلی هم اکنون به برنامهی WinForms ارجاع دارد. رعایت این نکته جهت رفع تداخلات آتی بسیار مهم است. همچنین این فایل در پوشهی Plugins نیز نباید حضور داشته باشد وگرنه شاهد بارگذاری افزونهها نخواهید بود.
سپس نیاز به وهله سازی Container آن و معرفی این کلاس PluginsRegistry میباشد:
using System; using System.Threading; using StructureMap; namespace WinFormsWithPluginSupport { public static class IocConfig { private static readonly Lazy<Container> _containerBuilder = new Lazy<Container>(defaultContainer, LazyThreadSafetyMode.ExecutionAndPublication); public static IContainer Container { get { return _containerBuilder.Value; } } private static Container defaultContainer() { return new Container(x => { x.AddRegistry<PluginsRegistry>(); }); } } }
تنظیمات ابتدایی WinForms برای دسترسی به امکانات StructureMap
به فرم اصلی برنامه مراجعه کرده و به سازندهی آن IContainer را اضافه کنید. از این اینترفیس جهت دسترسی به پلاگینهای برنامه استفاده خواهیم کرد.
using System.Windows.Forms; using StructureMap; namespace WinFormsWithPluginSupport { public partial class FrmMain : Form { private readonly IContainer _container; public FrmMain(IContainer container) { _container = container; InitializeComponent(); } } }
using System; using System.Windows.Forms; namespace WinFormsWithPluginSupport { static class Program { [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(IocConfig.Container.GetInstance<FrmMain>()); } } }
بارگذاری و اجرای افزونهها
دو دکمهی Run و ReLoad را به فرم اصلی برنامه با کدهای ذیل اضافه کنید:
using System.Linq; using System.Windows.Forms; using PluginsBase; using StructureMap; using WinFormsWithPluginSupport.Core; namespace WinFormsWithPluginSupport { public partial class FrmMain : Form { private readonly IContainer _container; public FrmMain(IContainer container) { _container = container; InitializeComponent(); } private void BtnRun_Click(object sender, System.EventArgs e) { var plugins = _container.GetAllInstances<IPlugin>().ToList(); foreach (var plugin in plugins) { plugin.Run(); } } private void BtnReload_Click(object sender, System.EventArgs e) { _container.EjectAllInstancesOf<IPlugin>(); _container.Configure(x => x.AddRegistry<PluginsRegistry>() ); } } }
همچنین در متد ReLoad نحوهی بارگذاری مجدد این پلاگینها را در صورت نیاز مشاهده میکنید.
اگر برنامه را اجرا کردید و پلاگینی بارگذاری نشد، به دنبال اسمبلیهای تکراری بگردید. برای مثال PluginsBase نباید هم در پوشهی اصلی اجرایی برنامه حضور داشته باشد و هم در پوشهی پلاگینها.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید
WinFormsWithPluginSupport.zip
در قسمت قبل راجع به مدل پیشفرض پرووایدر منابع در ASP.NET بحث نسبتا مفصلی شد. در این قسمت تولید یک پرووایدر سفارشی برای استفاده از دیتابیس به جای فایلهای resx. به عنوان منبع نگهداری دادهها بحث میشود.
قبلا هم اشاره شده بود که در پروژههای بزرگ ذخیره تمام ورودیهای منابع درون فایلهای resx. بازدهی مناسبی نخواهد داشت. همچنین به مرور زمان و با افزایش تعداد این فایلها، کار مدیریت آنها بسیار دشوار و طاقتفرسا خواهد شد. درضمن بهدلیل رفتار سیستم کشینگ این منابع در ASP.NET، که محتویات کل یک فایل را بلافاصله پس از اولین درخواست یکی از ورودیهای آن در حافظه سرور کش میکند، در صورت وجود تعداد زیادی فایل منبع و با ورودیهای بسیار، با گذشت زمان بازدهی کلی سایت به شدت تحت تاثیر قرار خواهد گرفت.
بنابراین استفاده از یک منبع مثل دیتابیس برای چنین شرایطی و نیز کنترل مدیریت دسترسی به ورودیهای آن به صورت سفارشی، میتواند به بازدهی بهتر برنامه کمک زیادی کند. درضمن فرایند بهروزرسانی مقادیر این ورودیها در صورت استفاده از یک دیتابیس میتواند سادهتر از حالت استفاده از فایلهای resx. انجام شود.
تولید یک پرووایدر منابع دیتابیسی - بخش اول
در بخش اول این مطلب با نحوه پیادهسازی کلاسهای اصلی و اولیه موردنیاز آشنا خواهیم شد. مفاهیم پیشرفتهتر (مثل کشکردن ورودیها و عملیات fallback) و نیز ساختار مناسب جدول یا جداول موردنیاز در دیتابیس و نحوه ذخیره ورودیها برای انواع منابع در دیتابیس در مطلب بعدی آورده میشود.
با توجه به توضیحاتی که در قسمت قبل داده شد، میتوان از طرح اولیهای به صورت زیر برای سفارشیسازی یک پرووایدر منابع دیتابیسی استفاده کرد:
اگر مطالب قسمت قبل را خوب مطالعه کرده باشید، پیاده سازی اولیه طرح بالا نباید کار سختی باشد. در ادامه یک نمونه از پیادهسازیهای ممکن نشان داده شده است.
برای آغاز کار ابتدا یک پروژه ClassLibrary جدید مثلا با نام DbResourceProvider ایجاد کنید و ریفرنسی از اسمبلی System.Web به این پروژه اضافه کنید. سپس کلاسهایی که در ادامه شرح داده شدهاند را به آن اضافه کنید.
کلاس DbResourceProviderFactory
همه چیز از یک ResourceProviderFactory شروع میشود. نسخه سفارشی نشان داده شده در زیر برای منابع محلی و کلی از کلاسهای پرووایدر سفارشی استفاده میکند که در ادامه آورده شدهاند.
using System.Web.Compilation; namespace DbResourceProvider { public class DbResourceProviderFactory : ResourceProviderFactory { #region Overrides of ResourceProviderFactory public override IResourceProvider CreateGlobalResourceProvider(string classKey) { return new GlobalDbResourceProvider(classKey); } public override IResourceProvider CreateLocalResourceProvider(string virtualPath) { return new LocalDbResourceProvider(virtualPath); } #endregion } }
درباره اعضای کلاس ResourceProviderFactory در قسمت قبل توضیحاتی داده شد. در نمونه سفارشی بالا دو متد این کلاس برای برگرداندن پرووایدرهای سفارشی منابع محلی و کلی بازنویسی شدهاند. سعی شده است تا نمونههای سفارشی در اینجا رفتاری همانند نمونههای پیشفرض در ASP.NET داشته باشند، بنابراین برای پرووایدر منابع کلی (GlobalDbResourceProvider) نام منبع درخواستی (className) و برای پرووایدر منابع محلی (LocalDbResourceProvider) مسیر مجازی درخواستی (virtualPath) به عنوان پارامتر کانستراکتور ارسال میشود.
نکته: برای استفاده از این کلاس به جای کلاس پیشفرض ASP.NET باید یکسری تنظیمات در فایل کانفیگ برنامه مقصد اعمال کرد که در ادامه آورده شده است.
کلاس BaseDbResourceProvider
برای پیادهسازی راحتتر کلاسهای موردنظر، بخشهای مشترک بین دو پرووایدر محلی و کلی در یک کلاس پایه به صورت زیر قرار داده شده است. این طرح دقیقا مشابه نمونه پیشفرض ASP.NET است.
using System.Globalization; using System.Resources; using System.Web.Compilation; namespace DbResourceProvider { public abstract class BaseDbResourceProvider : IResourceProvider { private DbResourceManager _resourceManager; protected abstract DbResourceManager CreateResourceManager(); private void EnsureResourceManager() { if (_resourceManager != null) return; _resourceManager = CreateResourceManager(); } #region Implementation of IResourceProvider public object GetObject(string resourceKey, CultureInfo culture) { EnsureResourceManager(); if (_resourceManager == null) return null; if (culture == null) culture = CultureInfo.CurrentUICulture; return _resourceManager.GetObject(resourceKey, culture); } public virtual IResourceReader ResourceReader { get { return null; } } #endregion } }
کلاس بالا چون یک کلاس صرفا پایه است بنابراین به صورت abstract تعریف شده است. در این کلاس، از نمونه سفارشی DbResourceManager برای بازیابی دادهها از دیتابیس استفاده شده است که در ادامه شرح داده شده است.
در اینجا، از متد CreateResourceManager برای تولید نمونه مناسب از کلاس DbResourceManager استفاده میشود. این متد به صورت abstract و protected تعریف شده است بنابراین پیادهسازی آن باید در کلاسهای مشتق شده که در ادامه آورده شدهاند انجام شود.
در متد EnsureResourceManager کار بررسی نال نبودن resouceManager_ انجام میشود تا درصورت نال بودن آن، بلافاصله نمونهای تولید شود.
نکته: ازآنجاکه نقطه آغازین فرایند یعنی تولید نمونهای از کلاس DbResourceProviderFactory توسط خود ASP.NET انجام خواهد شد، بنابراین مدیریت تمام نمونههای ساخته شده از کلاسهایی که در این مطلب شرح داده میشوند درنهایت عملا برعهده ASP.NET است. در ASP.NET درطول عمر یک برنامه تنها یک نمونه از کلاس Factory تولید خواهد شد، و متدهای موجود در آن در حالت عادی تنها یکبار به ازای هر منبع درخواستی (کلی یا محلی) فراخوانی میشوند. درنتیجه به ازای هر منبع درخواستی (کلی یا محلی) هر یک از کلاسهای پرووایدر منابع تنها یکبار نمونهسازی خواهد شد. بنابراین بررسی نال نبودن این متغیر و تولید نمونهای جدید تنها در صورت نال بودن آن، کاری منطقی است. این نمونه بعدا توسط ASP.NET به ازای هر منبع یا صفحه درخواستی کش میشود تا در درخواستهای بعدی تنها از این نسخه کششده استفاده شود.
در متد GetObject نیز کار استخراج ورودی منابع انجام میشود. ابتدا با استفاده از متد EnsureResourceManager از وجود نمونهای از کلاس DbResourceManager اطمینان حاصل میشود. سپس درصورتیکه مقدار این کلاس همچنان نال باشد مقدار نال برگشت داده میشود. این حالت وقتی پیش میآید که نتوان با استفاده از دادههای موجود نمونهای مناسب از کلاس DbResourceManager تولید کرد.
سپس مقدار کالچر ورودی بررسی میشود و درصورتیکه نال باشد مقدار کالچر UI ثرد جاری که در CultureInfo.CurrentUICulture قرار دارد برای آن درنظر گرفته میشود. درنهایت با فراخوانی متد GetObject از DbResourceManager تولیدی برای کلید و کالچر مربوطه کار استخراج ورودی درخواستی پایان میپذیرد.
پراپرتی ResourceReader در این کلاس به صورت virtual تعریف شده است تا بتوان پیادهسازی مناسب آن را در هر یک از کلاسهای مشتقشده اعمال کرد. فعلا برای این کلاس پایه مقدار نال برگشت داده میشود.
کلاس GlobalDbResourceProvider
برای پرووایدر منابع کلی از این کلاس استفاده میشود. نحوه پیادهسازی آن نیز دقیقا همانند طرح نمونه پیشفرض ASP.NET است.
using System; using System.Resources; namespace DbResourceProvider { public class GlobalDbResourceProvider : BaseDbResourceProvider { private readonly string _classKey; public GlobalDbResourceProvider(string classKey) { _classKey = classKey; } #region Implementation of BaseDbResourceProvider protected override DbResourceManager CreateResourceManager() { return new DbResourceManager(_classKey); } public override IResourceReader ResourceReader { get { throw new NotSupportedException(); } } #endregion } }
GlobalDbResourceProvider از کلاس پایهای که در بالا شرح داده شد مشتق شده است. بنابراین تنها بخشهای موردنیاز یعنی متد CreateResourceManager و پراپرتی ResourceReader در این کلاس پیادهسازی شده است.
در اینجا نمونه مخصوص کلاس ResourceManager (همان DbResourceManager) با توجه به نام فایل مربوط به منبع کلی تولید میشود. نام فایل در اینجا همان چیزی است که در دیتابیس برای نام منبع مربوطه ذخیره میشود. ساختار آن بعدا بحث میشود.
همانطور که میبینید برای پراپرتی ResourceReader خطای عدم پشتیبانی صادر میشود. دلیل آن در قسمت قبل و نیز بهصورت کمی دقیقتر در ادامه آورده شده است.
کلاس LocalDbResourceProvider
برای منابع محلی نیز از طرحی مشابه نمونه پیشفرض ASP.NET که در قسمت قبل نشان داده شد، استفاده شده است.
using System.Resources; namespace DbResourceProvider { public class LocalDbResourceProvider : BaseDbResourceProvider { private readonly string _virtualPath; public LocalDbResourceProvider(string virtualPath) { _virtualPath = virtualPath; } #region Implementation of BaseDbResourceProvider protected override DbResourceManager CreateResourceManager() { return new DbResourceManager(_virtualPath); } public override IResourceReader ResourceReader { get { return new DbResourceReader(_virtualPath); } } #endregion } }
این کلاس نیز از کلاس پایهای BaseDbResourceProvider مشتق شده و پیادهسازیهای مخصوص منابع محلی برای متد CreateResourceManager و پراپرتی ResourceReader در آن انجام شده است.
در متد CreateResourceManager کار تولید نمونهای از DbResourceManager با استفاده از مسیر مجازی صفحه درخواستی انجام میشود. این فرایند شبیه به پیادهسازی پیشفرض ASP.NET است. در واقع در پیادهسازی جاری، نام منابع محلی همنام با مسیر مجازی متناظر آنها در دیتابیس ذخیره میشود. درباره ساختار جدول دیتابیس بعدا بحث میشود.
در این کلاس کار بازخوانی کلیدهای موجود برای پراپرتیهای موجود در یک صفحه از طریق نمونهای از کلاس DbResourceReader انجام شده است. شرح این کلاس در ادامه آمده است.
نکته: همانطور که در قسمت قبل هم اشاره کوتاهی شده بود، از خاصیت ResourceReader در پرووایدر منابع برای تعیین تمام پراپرتیهای موجود در منبع استفاده میشود تا کار جستجوی کلیدهای موردنیاز در عبارات بومیسازی ضمنی برای رندر صفحه وب راحتتر انجام شود. بنابراین از این پراپرتی تنها در پرووایدر منابع محلی استفاده میشود. ازآنجاکه در عبارات بومیسازی ضمنی تنها قسمت اول نام کلید ورودی منبع آورده میشود، بنابراین قسمت دوم (و یا قسمتهای بعدی) کلید موردنظر که همان نام پراپرتی کنترل متناظر است از جستجو میان ورودیهای یافته شده توسط این پراپرتی بدست میآید تا ASP.NET بداند که برای رندر صفحه چه پراپرتیهایی نیاز به رجوع به پرووایدر منبع محلی مربوطه دارد (برای آشنایی بیشتر با عبارت بومیسازی ضمنی رجوع شود به قسمت قبل).
نکته: دقت کنید که پس از اولین درخواست، خروجی حاصل از enumerator این ResourceReader کش میشود تا در درخواستهای بعدی از آن استفاده شود. بنابراین در حالت عادی، به ازای هر صفحه تنها یکبار این پراپرتی فراخوانده میشود. درباره این enumerator در ادامه بحث شده است.
کلاس DbResourceManager
کار اصلی مدیریت و بازیابی ورودیهای منابع از دیتابیس از طریق کلاس DbResourceManager انجام میشود. نمونهای بسیار ساده و اولیه از این کلاس را در زیر مشاهده میکنید:
using System.Globalization; using DbResourceProvider.Data; namespace DbResourceProvider { public class DbResourceManager { private readonly string _resourceName; public DbResourceManager(string resourceName) { _resourceName = resourceName; } public object GetObject(string resourceKey, CultureInfo culture) { var data = new ResourceData(); return data.GetResource(_resourceName, resourceKey, culture.Name).Value; } } }
کار استخراج ورودیهای منابع با استفاده از نام منبع درخواستی در این کلاس مدیریت خواهد شد. این کلاس با استفاده نام منیع درخواستی به عنوان پارامتر کانستراکتور ساخته میشود. با استفاده از متد GetObject که نام کلید ورودی موردنظر و کالچر مربوطه را به عنوان پارامتر ورودی دریافت میکند فرایند استخراج انجام میشود.
برای کپسولهسازی عملیات از کلاس جداگانهای (ResourceData) برای تبادل با دیتابیس استفاده شده است. شرح بیشتر درباره این کلاس و نیز پیاده سازی کاملتر کلاس DbResourceManager به همراه مدیریت کش ورودیهای منابع و نیز عملیات fallback در مطلب بعدی آورده میشود.کلاس DbResourceReader
این کلاس که درواقع پیادهسازی اینترفیس IResourceReader است برای یافتن تمام کلیدهای تعریف شده برای یک منبع بهکار میرود، پیادهسازی آن نیز به صورت زیر است:
using System.Collections; using System.Resources; using System.Security; using DbResourceProvider.Data; namespace DbResourceProvider { public class DbResourceReader : IResourceReader { private readonly string _resourceName; private readonly string _culture; public DbResourceReader(string resourceName, string culture = "") { _resourceName = resourceName; _culture = culture; } #region Implementation of IResourceReader public void Close() { } public IDictionaryEnumerator GetEnumerator() { return new DbResourceEnumerator(new ResourceData().GetResources(_resourceName, _culture)); } #endregion #region Implementation of IEnumerable IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } #endregion #region Implementation of IDisposable public void Dispose() { Close(); } #endregion } }
این کلاس تنها با استفاده از نام منبع و عنوان کالچر موردنظر کار بازخوانی ورودیهای موجود را انجام میدهد.
تنها نکته مهم در کد بالا متد GetEnumerator است که نمونهای از اینترفیس IDictionaryEnumerator را برمیگرداند. در اینجا از کلاس DbResourceEnumerator که برای کار با دیتابیس طراحی شده، استفاده شده است. همانطور که قبلا هم اشاره شده بود، هر یک از اعضای این enumerator از نوع DictionaryEntry هستند که یک struct است. این کلاس در ادامه شرح داده شده است.
متد Close برای بستن و از بین بردن منابعی است که در تهیه enumerator موردبحث نقش داشتهاند. مثل منایع شبکهای یا فایلی که باید قبل از اتمام کار با این کلاس به صورت کامل بسته شوند. هرچند در نمونه جاری چنین موردی وجود ندارد و بنابراین این متد بلااستفاده است.
در کلاس فوق نیز برای دریافت اطلاعات از ResourceData استفاده شده است که بعدا به همراه ساختار مناسب جدول دیتابیس شرح داده میشود.نکته: دقت کنید که در پیادهسازی نشان داده شده برای کلاس LocalDbResourceProvider برای یافتن ورودیهای موجود از مقدار پیشفرض (یعنی رشته خالی) برای کالچر استفاده شده است تا از ورودیهای پیشفرض که در حالت عادی باید شامل تمام موارد تعریف شده موجود هستند استفاده شود (قبلا هم شرح داده شد که منبع اصلی و پیشفرض یعنی همانی که برای زبان پیشفرض برنامه درنظر گرفته میشود و بدون نام کالچر مربوطه است، باید شامل حداکثر ورودیهای تعریف شده باشد. منابع مربوطه به سایر کالچرها میتوانند همه این ورودیهای تعریفشده در منبع اصلی و یا قسمتی از آن را شامل شوند. عملیات fallback تضمین میدهد که درنهایت نزدیکترین گزینه متناظر با درخواست جاری را برگشت دهد).
کلاس DbResourceEnumerator
کلاس دیگری که در اینجا استفاده شده است، DbResourceEnumerator است. این کلاس در واقع پیاده سازی اینترفیس IDictionaryEnumerator است. محتوای این کلاس در زیر آورده شده است:
using System.Collections; using System.Collections.Generic; using DbResourceProvider.Models; namespace DbResourceProvider { public sealed class DbResourceEnumerator : IDictionaryEnumerator { private readonly List<Resource> _resources; private int _dataPosition; public DbResourceEnumerator(List<Resource> resources) { _resources = resources; Reset(); } public DictionaryEntry Entry { get { var resource = _resources[_dataPosition]; return new DictionaryEntry(resource.Key, resource.Value); } } public object Key { get { return Entry.Key; } } public object Value { get { return Entry.Value; } } public object Current { get { return Entry; } } public bool MoveNext() { if (_dataPosition >= _resources.Count - 1) return false; ++_dataPosition; return true; } public void Reset() { _dataPosition = -1; } } }
تفاوت این اینترفیس با اینترفیس IEnumerable در سه عضو اضافی است که برای استفاده در سیستم مدیریت منابع ASP.NET نیاز است. همانطور که در کد بالا مشاهده میکنید این سه عضو عبارتند از پراپرتیهای Entry و Key و Value. پراپرتی Entry که ورودی جاری در enumerator را مشخص میکند از نوع DictionaryEntry است. پراپرتیهای Key و Value هم که از نوع object تعریف شدهاند برای کلید و مقدار ورودی جاری استفاده میشوند.
این کلاس لیستی از Resource به عنوان پارامتر کانستراکتور برای تولید enumerator دریافت میکند. کلاس Resource مدل تولیدی از ساختار جدول دیتابیس برای ذخیره ورودیهای منابع است که در مطلب بعدی شرح داده میشود. بقیه قسمتهای کد فوق هم پیادهسازی معمولی یک enumerator است.
تنظیمات فایل کانفیگ
برای اجبار کردن ASP.NET به استفاده از Factory موردنظر باید تنظیمات زیر را در فایل web.config اعمال کرد:
<system.web> ... <globalization resourceProviderFactoryType=" نام کامل اسمبلی مربوطه ,نام پرووایدر فکتوری به همراه فضای نام آن " /> ... </system.web>
روش نشان داده شده در بالا حالت کلی تعریف و تنظیم یک نوع داده در فایل کانفیگ را نشان میدهد. درباره نام کامل اسمبلی در اینجا شرح داده شده است.
مثلا برای پیادهسازی نشان داده شده در این مطلب خواهیم داشت:<globalization resourceProviderFactoryType="DbResourceProvider.DbResourceProviderFactory, DbResourceProvider" />
در مطلب بعدی درباره ساختار مناسب جدول یا جداول دیتابیس برای ذخیره ورودهای منابع و نیز پیادهسازی کاملتر کلاسهای مورداستفاده بحث خواهد شد.
منابع:
سلام؛ با بررسیهای که انجام دادم و آپلود سایت بر روی دو سرور مجزا متوجه شدم مشکل از هاست است. فکر میکنم نیاز به تنظیماتی در iis وجود داشته که هاست من این تنظیمات را ندارد(به گفته پشتیبانی هاست). آیا واقعا چنین تنظیماتی وجود دارد که باید انجام شود؟
NHibernate کتابخانهی تبدیل شده پروژه بسیار محبوب Hibernate جاوا به سی شارپ است و یکی از ORM های بسیار موفق، به شمار میرود. در طی تعدادی مقاله قصد آشنایی با این فریم ورک را داریم.
چرا نیاز است تا از یک ORM استفاده شود؟
تهیه قسمت و یا لایه دسترسی به دادهها در یک برنامه عموما تا 30 درصد زمان کل تهیه یک محصول را تشکیل میدهد. اما باید در نظر داشت که این پروسهی تکراری هیچ کار خارق العادهای نبوده و ارزش افزودهی خاصی را به یک برنامه اضافه نمیکند. تقریبا تمام برنامههای تجاری نیاز به لایه دسترسی به دادهها را دارند. پس چرا ما باید به ازای هر پروژه، این کار تکراری و کسل کننده را بارها و بارها تکرار کنیم؟
هدف NHibernate ، کاستن این بار از روی شانههای یک برنامه نویس است. با کمک این کتابخانه، دیگر رویه ذخیره شدهای را نخواهید نوشت. دیگر هیچگاه با ADO.Net سر و کار نخواهید داشت. به این صورت میتوان عمده وقت خود را صرف قسمتهای اصلی و طراحی برنامه کرد تا کد نویسی یک لایه تکراری. همچنین عدهای از بزرگان اینگونه ابزارها اعتقاد دارند که برنامه نویسهایی که لایه دسترسی به دادهها را خود طراحی میکنند، مشغول کلاهبرداری از مشتریهای خود هستند! (صرف زمان بیشتر برای تهیه یک محصول و همچنین وجود باگهای احتمالی در لایه دسترسی به دادههای طراحی شده توسط یک برنامه نویس نه چندان حرفهای)
برای مشاهده سایر مزایای استفاده از یک ORM لطفا به مقاله "5 دلیل برای استفاده از یک ابزار ORM" مراجعه نمائید.
در ادامه برای معرفی این کتابخانه یک سیستم ثبت سفارشات را با هم مرور خواهیم کرد.
بررسی مدل سیستم ثبت سفارشات
در این مدل سادهی ما، مشتریها (customers) امکان ثبت سفارشات (orders) را دارند. سفارشات توسط یک کارمند (employee) که مسؤول ثبت آنها است به سیستم وارد میشود. هر سفارش میتواند شامل یک یا چند (one-to-many) آیتم (order items) باشد و هر آیتم معرف یک محصول (product) است که قرار است توسط یک مشتری (customer) خریداری شود. کلاس دیاگرام این مدل به صورت زیر میتواند باشد.
نگاشت مدل
زمانیکه مدل سیستم مشخص شد، اکنون نیاز است تا حالات (دادهها) آنرا در مکانی ذخیره کنیم. عموما اینکار با کمک سیستمهای مدیریت پایگاههای داده مانند SQL Server، Oracle، IBM DB2 ، MySql و امثال آنها صورت میگیرد. زمانیکه از NHibernate استفاده کنید اهمیتی ندارد که برنامه شما قرار است با چه نوع دیتابیسی کار کند؛ زیرا این کتابخانه اکثر دیتابیسهای شناخته شده موجود را پشتیبانی میکند و برنامه از این لحاظ مستقل از نوع دیتابیس عمل خواهد کرد و اگر نیاز بود روزی بجای اس کیوال سرور از مای اس کیوال استفاده شود، تنها کافی است تنظیمات ابتدایی NHibernate را تغییر دهید (بجای بازنویسی کل برنامه).
اگر برای ذخیره سازی دادهها و حالات سیستم از دیتابیس استفاده کنیم، نیاز است تا اشیاء مدل خود را به جداول دیتابیس نگاشت نمائیم. این نگاشت عموما یک به یک نیست (لزومی ندارد که حتما یک شیء به یک جدول نگاشت شود). در گذشتهی نچندان دور کتابخانهی NHibernate ، این نگاشت عموما توسط فایلهای XML ایی به نام hbm صورت میگرفت. این روش هنوز هم پشتیبانی شده و توسط بسیاری از برنامه نویسها بکار گرفته میشود. روش دیگری که برای تعریف این نگاشت مرسوم است، مزین سازی اشیاء و خواص آنها با یک سری از ویژگیها میباشد که فریم ورک برتر این عملیات Castle Active Record نام دارد.
اخیرا کتابخانهی دیگری برای انجام این نگاشت تهیه شده به نام Fluent NHibernate که بسیار مورد توجه علاقمندان به این فریم ورک واقع گردیده است. با کمک کتابخانهی Fluent NHibernate عملیات نگاشت اشیاء به جداول، بجای استفاده از فایلهای XML ، توسط کدهای برنامه صورت خواهند گرفت. این مورد مزایای بسیاری را همانند استفاده از یک زبان برنامه نویسی کامل برای تعریف نگاشتها، بررسی خودکار نوعهای دادهای و حتی امکان تعریف منطقی خاص برای قسمت نگاشت برنامه، به همراه خواهد داشت.
آماده سازی سیستم برای استفاده از NHibernate
در ادامه بجای دریافت پروژه سورس باز NHibernate از سایت سورس فورج، پروژه سورس باز Fluent NHibernate را از سایت گوگل کد دریافت خواهیم کرد که بر فراز کتابخانهی NHibernate بنا شده است و آنرا کاملا پوشش میدهد. سورس این کتابخانه را با checkout مسیر زیر توسط TortoiseSVN میتوان دریافت کرد.
البته احتمالا برای دریافت آن از گوگل کد با توجه به تحریم موجود نیاز به پروکسی خواهد بود. برای تنظیم پروکسی در TortoiseSVN به قسمت تنظیمات آن مطابق تصویر ذیل مراجعه کنید:
همچنین جهت سهولت کار، آخرین نگارش موجود در زمان نگارش این مقاله را از این آدرس نیز میتوانید دریافت نمائید.
پس از دریافت پروژه، باز کردن فایل solution آن در VS و سپس build کل مجموعه، اگر به پوشههای آن مراجعه نمائید، فایلهای زیر قابل مشاهده هستند:
Nhibernate.dll : اسمبلی فریم ورک NHibernate است.
NHibernate.Linq.dll : اسمبلی پروایدر LINQ to NHibernate میباشد.
FluentNHibernate.dll : اسمبلی فریم ورک Fluent NHibernate است.
Iesi.Collections.dll : یک سری مجموعههای ویژه مورد استفاده NHibernate را ارائه میدهد.
Log4net.dll : فریم ورک لاگ کردن اطلاعات NHibernate میباشد. (این فریم ورک نیز جهت عملیات logging بسیار معروف و محبوب است)
Castle.Core.dll : کتابخانه پایه Castle.DynamicProxy2.dll است.
Castle.DynamicProxy2.dll : جهت اعمال lazy loading در فریم ورک NHibernate بکار میرود.
System.Data.SQLite.dll : پروایدر دیتابیس SQLite است.
Nunit.framework.dll : نیز یکی از فریم ورکهای بسیار محبوب آزمون واحد در دات نت فریم ورک است.
برای سادگی مراجعات بعدی، این فایلها را یافته و در پوشهای به نام lib کپی نمائید.
برپایی یک پروژه جدید
پس از دریافت Fluent NHibernate ، یک پروژه Class Library جدید را در VS.Net آغاز کنید (برای مثال به نام NHSample1 ). سپس یک پروژه دیگر را نیز از نوع Class Library به نام UnitTests به این solution ایجاد شده جهت انجام آزمونهای واحد برنامه اضافه نمائید.
اکنون به پروژه NHSample1 ، ارجاع هایی را به فایلهای FluentNHibernate.dll و سپس NHibernate.dll در که پوشه lib ایی که در قسمت قبل ساختیم، قرار دارند، اضافه نمائید.
در ادامه یک پوشه جدید به پروژه NHSample1 به نام Domain اضافه کنید. سپس به این پوشه، کلاس Customer را اضافه نمائید:
namespace NHSample1.Domain
{
public class Customer
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string AddressLine1 { get; set; }
public string AddressLine2 { get; set; }
public string PostalCode { get; set; }
public string City { get; set; }
public string CountryCode { get; set; }
}
}
using FluentNHibernate.Mapping;
namespace NHSample1.Domain
{
class CustomerMapping : ClassMap<Customer>
{
}
}
using FluentNHibernate.Mapping;
namespace NHSample1.Domain
{
class CustomerMapping : ClassMap<Customer>
{
public CustomerMapping()
{
Not.LazyLoad();
Id(c => c.Id).GeneratedBy.HiLo("1000");
Map(c => c.FirstName).Not.Nullable().Length(50);
Map(c => c.LastName).Not.Nullable().Length(50);
Map(c => c.AddressLine1).Not.Nullable().Length(50);
Map(c => c.AddressLine2).Length(50);
Map(c => c.PostalCode).Not.Nullable().Length(10);
Map(c => c.City).Not.Nullable().Length(50);
Map(c => c.CountryCode).Not.Nullable().Length(2);
}
}
}
سپس وضعیت نگاشت تک تک خواص کلاس Customer را مشخص میکنیم. توسط Id(c => c.Id).GeneratedBy.HiLo به سیستم اعلام خواهیم کرد که فیلد Id از نوع identity است که از 1000 شروع خواهد شد. مابقی موارد هم بسیار واضح هستند. تمامی خواص کلاس Customer ذکر شده، نال را نمیپذیرند (منهای AddressLine2) و طول آنها نیز مشخص گردیده است.
با کمک Fluent NHibernate ، بحث بررسی نوعهای دادهای و همچنین یکی بودن موارد مطرح شده در نگاشت با کلاس اصلی Customer به سادگی توسط کامپایلر بررسی شده و خطاهای آتی کاهش خواهند یافت.
برای آشنایی بیشتر با lambda expressions میتوان به مقاله زیر مراجعه کرد:
Step-by-step Introduction to Delegates and Lambda Expressions
ادامه دارد...
در این پست قصد دارم کلاس زیر رو براتون آزمایش کنم:
در کلاس بالا که abstract هستش، متدی دارم که abstract است و بدنهای نداره و از متد بعدی به اسم round برای گرد کردن اعداد استفاده میشه. برای تست کلاس بالا و اطمینان از درست بودن متدها باید به روش زیر عمل نمود:
روش اول:
در این روش ابتدا باید کلاسی نوشت تا کلاس abstract بالا رو پیاده سازی کنه:
بعد میشه خیلی راحت برای کلاس دوم متدهای تست رو نوشت. به روش زیر:
که بعد از اجرا نتیجه زیر رو خواهید دید
البته روش بالا خیلی مورد پسند من نیست.
در روش دوم که من خیلی بیشتر بهش علاقه دارم دیگه نیازی به استفاده از یک کلاس دوم برای پیاده سازی کلاس abstract نیست. بلکه در این روش از ابزار rhinomocks برای این کار استفاده میکنیم . استفاده از rhino mocks به چندین روش امکان پذیره که امروز 2 روش اونو براتون توضیح میدم:
در روش اول از mockrepository استفاده میکنیم و در روش دوم از روش aaa یا arrange-act-assert
استفاده از mockrepository :
ابتدا کدهای مربوطه رو مینویسم:
همانطور که در کدهای نوشته شده بالا میبینید ابتدا یک mockrepository ساخته شده، سپس از نمونه اون کلاس برای ساخت partialmock کلاس myabstractclass استفاده کردم. در این روش متدهای expect حتما باید بین بلاک record نوشته شوند تا بتونیم از اونها در بلاک playback استفاده کنیم. نتیجه اجرای این متد تست هم مثل متد تست قبلی درست است.
در مورد expect.call باید بگم که از این کلاس برای شبیه سازی رفتار یک متد استفاده میشه (مثلا در مواقعی که یک متد برای انجام عملیات باید به دیتا بیس وصل شده و یک query را اجرا کنه) برای اینکه در تست، از این عملیات صرف نظر بشه از mockها استفاده کرده و رفتار متد رو به روش بالا شبیه سازی میکنیم. البته کار کردن با rhino mocks به صورت بالا دیگه از مد افتاده و جدیدا از روش aaa استفاده میشه که اونو در پایین توضیح میدم:
توی این روش دیگه خبری از record و playback نیست و همانطور که مشخصه از سه مرحله arrange-act-assert تشکیل شده که هر مرحله رو براتون مشخص کردم. مزیت استفاده از این روش اینه که اولا تعداد خطوط کمتری برای کد نویسی نیاز داره و دوما سرعت اجراش از روش قبلی خیلی بیشتره. در مورد repeat.once هم بگم که این دستور نشون میده فقط یک بار اجازه انجام عملیات act رو دارید. یعنی اگر کد هارو به صورت زیر تغییر بدیم با خطا روبرو میشیم:
خطای آن هم واضح داره میگه که expected#1 هستش در حالی که actual#2 (تعداد دفعات حقیقی از دفعات مورد انتظار بیشتره)
توی پستهای بعدی (اگه وقت بشه) حتما در مورد 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 بالا رو پیاده سازی کنه:
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" ) ); } }
در مورد 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 }
[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 بیشتر توضیح میدم