پیاده سازی UnitOfWork به وسیله MEF
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: چهار دقیقه

در این پست قصد دارم یک UnitOfWork به روش MEF پیاده سازی کنم. ORM مورد نظر EntityFramework CodeFirst است. در صورتی که با UnitOfWork , MEF آشنایی ندارید از لینک‌های زیر استفاده کنید:
 برای شروع ابتدا مدل برنامه رو به صورت زیر تعریف کنید.
 public class Category
    {
        public int Id { get; set; }

        public string Title { get; set; }
    }
سپس فایل Map  رو برای مدل بالا به صورت زیر تعریف کنید.
 public class CategoryMap : EntityTypeConfiguration<Entity.Category>
    {
        public CategoryMap()
        {
            ToTable( "Category" );

            HasKey( _field => _field.Id );

            Property( _field => _field.Title )
            .IsRequired();            
        }
    }
برای پیاده سازی الگوی واحد کار ابتدا باید یک اینترفیس به صورت زیر تعریف کنید.
using System.Data.Entity;
using System.Data.Entity.Infrastructure;

namespace DataAccess
{
    public interface IUnitOfWork
    {
        DbSet<TEntity> Set<TEntity>() where TEntity : class;
        DbEntityEntry<TEntity> Entry<TEntity>() where TEntity : class;
        void SaveChanges();     
        void Dispose();
    }
}
DbContext مورد نظر باید اینترفیس مورد نظر را پیاده سازی کند و برای اینکه بتونیم اونو در CompositionContainer اضافه کنیم باید از Export Attribute استفاده کنیم.
چون کلاس DatabaseContext از اینترفیس IUnitOfWork ارث برده است برای همین از InheritedExport استفاده می‌کنیم.
[InheritedExport( typeof( IUnitOfWork ) )]
    public class DatabaseContext : DbContext, IUnitOfWork
    {
        private DbTransaction transaction = null;

        public DatabaseContext()           
        {
            this.Configuration.AutoDetectChangesEnabled = false;
            this.Configuration.LazyLoadingEnabled = true;
        }

        protected override void OnModelCreating( DbModelBuilder modelBuilder )
        {
            modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();

            modelBuilder.AddFormAssembly( Assembly.GetAssembly( typeof( Entity.Map.CategoryMap ) ) );
        }

        public DbEntityEntry<TEntity> Entry<TEntity>() where TEntity : class
        {
            return this.Entry<TEntity>();
        }      
    }
نکته قابل ذکر در قسمت OnModelCreating این است که یک Extension Methodبه نام AddFromAssembly (همانند NHibernate) اضافه شده است که از Assembly  مورد نظر تمام کلاس‌های Map رو پیدا می‌کنه و اونو به ModelBuilder اضافه می‌کنه. کد متد به صورت زیر است:
 public static class ModelBuilderExtension
    {
        public static void AddFormAssembly( this DbModelBuilder modelBuilder, Assembly assembly )
        {
            Array.ForEach<Type>( assembly.GetTypes().Where( type => type.BaseType != null && type.BaseType.IsGenericType && type.BaseType.GetGenericTypeDefinition() == typeof( EntityTypeConfiguration<> ) ).ToArray(), delegate( Type type )
            {
                dynamic instance = Activator.CreateInstance( type );
                modelBuilder.Configurations.Add( instance );
            } );
        }
    }

برای پیاده سازی قسمت BusinessLogic ابتدا کلاس BusiessBase را در آن قرار دهید:
public class BusinessBase<TEntity> where TEntity : class
    {        
        public BusinessBase( IUnitOfWork unitOfWork )
        {
            this.UnitOfWork = unitOfWork;
        }

        [Import]
        public IUnitOfWork UnitOfWork
        {
            get;
            private set;
        }

        public virtual IEnumerable<TEntity> GetAll()
        {
            return UnitOfWork.Set<TEntity>().AsNoTracking();
        }

        public virtual void Add( TEntity entity )
        {
            try
            {             
                UnitOfWork.Set<TEntity>().Add( entity );
                UnitOfWork.SaveChanges();
            }
            catch
            {              
                throw;
            }
            finally
            {
                UnitOfWork.Dispose();
            }
        }
    }

تمام متد‌های پایه مورد نظر را باید در این کلاس قرار داد که برای مثال من متد Add , GetAll را براتون پیاده سازی کردم. UnitOfWork توسط ImportAttribute مقدار دهی می‌شود و نیاز به وهله سازی از آن نیست 
کلاس Category رو هم باید به صورت زیر اضافه کنید.
 public class Category : BusinessBase<Entity.Category>
    {      
        [ImportingConstructor]
        public Category( [Import( typeof( IUnitOfWork ) )] IUnitOfWork unitOfWork )
            : base( unitOfWork )
        {
        }
    }
.در انتها باید UI مورد نظر طراحی شود که من در اینجا از Console Application استفاده کردم. یک کلاس به نام Plugin ایجاد کنید  و کد‌های زیر را در آن قرار دهید.
public class Plugin
    {        
        public void Run()
        {
            AggregateCatalog catalog = new AggregateCatalog();

            Container = new CompositionContainer( catalog );

            CompositionBatch batch = new CompositionBatch();

            catalog.Catalogs.Add( new AssemblyCatalog( Assembly.GetExecutingAssembly() ) );

            batch.AddPart( this );

            Container.Compose( batch );
        }

        public CompositionContainer Container 
        {
            get; 
            private set;
        }
    }
در کلاس Plugin  توسط AssemblyCatalog تمام Export Attribute‌های موجود جستجو می‌شود و بعد به عنوان کاتالوگ مورد نظر به Container اضافه می‌شود. انواع Catalog در MEF به شرح زیر است:
  • AssemblyCatalog : در اسمبلی مورد نظر به دنبال تمام Export Attribute‌ها می‌گردد و آن‌ها را به عنوان ExportedValue در Container اضافه می‌کند.
  • TypeCatalog: فقط یک نوع مشخص را به عنوان ExportAttribute در نظر می‌گیرد.
  • DirectoryCatalog :  در یک مسیر مشخص تمام Assembly مورد نظر را از نظر Export Attribute جستجو می‌کند و آن‌ها را به عنوان ExportedValue در Container اضافه می‌کند. 
  • ApplicationCatalog :  در اسمبلی  و فایل‌های (EXE) مورد نظر به دنبال تمام Export Attribute‌ها می‌گردد و آن‌ها را به عنوان ExportedValue در Container اضافه می‌کند. 
  • AggregateCatalog : تمام موارد فوق را Support می‌کند.
کلاس Program  رو به صورت زیر بازنویسی کنید.
  class Program
    {
        static void Main( string[] args )
        {
            Plugin plugin = new Plugin();
            plugin.Run();

            Category category = new Category(plugin.Container.GetExportedValue<IUnitOfWork>());
            category.GetAll().ToList().ForEach( _record => Console.Write( _record.Title ) );
        }
    }
پروژه اجرا کرده و نتیجه رو مشاهده کنید.
  • #
    ‫۱۱ سال و ۷ ماه قبل، جمعه ۲۵ اسفند ۱۳۹۱، ساعت ۰۵:۳۵
    سلام
    سپاس از مطلبتون
    در قسمت زیر شما نام کلاس CategoriMap رو ذکر کردید 
    در این صورت به ازای هر کلاس باید در این قسمت نام Map اون ذکر شود؟
    خوب اگر قرار باشه به ازای هر کلاس نام Map آن ذکر شود دیگر نیازی به AddFormAssembly  نبود مستقیمآ نام    .CategoryMap  در modelBuilder  اضافه میکردیم

       protected override void OnModelCreating( DbModelBuilder modelBuilder )
            {
                modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
     
                modelBuilder.AddFormAssembly( Assembly.GetAssembly( typeof( Entity.Map.CategoryMap ) ) );
            }

    • #
      ‫۱۱ سال و ۷ ماه قبل، جمعه ۲۵ اسفند ۱۳۹۱، ساعت ۱۱:۵۵
      اگر به پیاده سازی AddFromAsm دقت کنید یک ForEach داره. یعنی فقط شما یک اسمبلی رو بهش می‌دی، خودش مابقی رو پیدا می‌کنه. حتی اضافه کردن DbSetها هم قابلیت خودکار سازی داره .
      • #
        ‫۱۱ سال و ۷ ماه قبل، جمعه ۲۵ اسفند ۱۳۹۱، ساعت ۱۴:۵۵
        بله در جریانم
        اما در اینجا به صورت مستقیم نام MaP مستقیم ذکر شده
        برای این این سوال برام پیش امد . چون اگر قرار بود خود کار ذکر بشن پس دیگه به ذکر Entity.Map.CategoryMap  نبود
        • #
          ‫۱۱ سال و ۷ ماه قبل، جمعه ۲۵ اسفند ۱۳۹۱، ساعت ۱۵:۳۳

          مهم نیست. همینقدر که ایده اینکار مطرح شده مابقی‌اش هنر Reflection مصرف کننده است. مثلا از یک رشته (ذخیره شده در تنظیمات برنامه) هم می‌شود این نام‌ها را دریافت کرد: Assembly.Load

  • #
    ‫۱۱ سال و ۷ ماه قبل، جمعه ۲۵ اسفند ۱۳۹۱، ساعت ۱۴:۴۱
    یک نکته که در مورد پیاده سازی بالا وجود داره اینه که متد save رو در خود توابع مربوط به repository‌ها قرار داده اید و این با الگوی unitOfWork همخوانی نداره .
    • #
      ‫۱۱ سال و ۷ ماه قبل، شنبه ۲۶ اسفند ۱۳۹۱، ساعت ۰۳:۱۶
      متد Savechange در interface IUnitOfWork  قرار دارد .اما  در DatabaseContext  پیاده سازی نشده  که اون هم احتمالا یادشون رفته باشه.
      • #
        ‫۱۱ سال و ۷ ماه قبل، شنبه ۲۶ اسفند ۱۳۹۱، ساعت ۰۳:۵۶
        کلاس پایه DbContext پیاده سازی SaveChanges رو داره.
    • #
      ‫۱۱ سال و ۷ ماه قبل، شنبه ۲۶ اسفند ۱۳۹۱، ساعت ۰۳:۲۳
      در مورد سوال اول که چرا CategoryMap رو به متد AddFromAssembly پاس دادم.
      متد AddFromAssembly نیاز به یک Assembly دارد تا بتونه تمام کلاس هایی رو که از کلاس EntityTypeConfiguration ارث برده اند رو پیدا کنه و اونها رو به صورت خودکار به ModelBuilder اضافه کنه. به همین دلیل من Assembly کلاس CategoryMap رو به اون پاس دادم. دقت کنید که اگر من n تا کلاس Map دیگه هم توی این ClassLibrary داشتم باز توسط همین دستور این کار به صورت خودکار انجام می‌شد. (پیشنهاد می‌کنم تست کنید)
      نکته: این متد برگرفته شده از متد AddFromAssembly در NHibernate Session Configuration   است.
      البته بهتر است که یک کلاس پایه برای این کار بسازید و اون کلاس رو به AddFromAssembly پاس بدید.

      در مورد سوال دوست عزیز مبنی بر اینکه متد Save رو در خود توابع Repository قرار دادم.
      اگر به کد‌های نوشته شده دقت کنید من اصلا مفهومی به نام Repository رو پیاده سازی نکردم. به این دلیل که خود DbContext ترکیبی از Repository Pattern , UnitOfWork است. متد SaveChange صدا زده شده همان متد SaveChange در DbContext است.
      فرض کنید من یک کلاس Business دیگر به صورت زیر داشتم.

      public class MyBusiness : BusinessBase<Entity.MyEntity>
          {
              [ImportingConstructor]
              public MyBusiness  ( [Import( typeof( IUnitOfWork ) )] IUnitOfWork unitOfWork )
                  : base( unitOfWork )
              {
              }
              public void Add(Entity.MyEntity entity)
              {
                  UnitOfWork.Set<MyEntity>().Add(entity);
              }
          }
      حالا کد کلاس Business Category به صورت زیر تغییر می‌کنه.
      public class Category : BusinessBase<Entity.Category>
          {      
              [ImportingConstructor]
              public Category( [Import( typeof( IUnitOfWork ) )] IUnitOfWork unitOfWork )
                  : base( unitOfWork )
              {
              }
      
              public override void Add( Entity.Category entity )
              {
                  new MyBusiness().Add( new Entity.MyEntity() );
                  UnitOfWork.Set<Entity.Category>().Add( entity );
                  UnitOfWork.SaveChanges();
              }
          }
      همان طور که می‌بینید فقط یک بار متد SaveChange فراخوانی شده است. Virtual کردن متد BusinessBase دقیقا به همین دلیل است.

  • #
    ‫۱۱ سال و ۷ ماه قبل، یکشنبه ۲۷ اسفند ۱۳۹۱، ساعت ۰۷:۱۹
    تشکر میکنم بابت مقاله خوب و کاملتون.
  • #
    ‫۱۱ سال و ۷ ماه قبل، یکشنبه ۲۷ اسفند ۱۳۹۱، ساعت ۲۲:۱۶
    اگه  به جای category   که شما تعریف کردین . موجودیتهای مثل خبر‌ها یا آخرین نظرات و ... داشتیم چطور میتونیم اینا رو گروه بندی کنیم جوری که بشه هر کدوم رو در صفحه اصلی  نشون داد؟مثلا همه موجودیت‌ها رو برسی کنه و بر اساس گروهشون اونایی که گروه خاصی دارن در قسمت منوی صفحه اصلی نمایش بده.ایا این امکان در MEF هست؟
  • #
    ‫۱۱ سال و ۶ ماه قبل، یکشنبه ۱۸ فروردین ۱۳۹۲، ساعت ۰۲:۲۹
    مرسی از مقاله خیلی خوبتون .
    چه جور می‌تونم این روش روی local تست کنم .که اثرش رو مشاهده کنم .
  • #
    ‫۱۰ سال و ۱۲ ماه قبل، یکشنبه ۱۴ مهر ۱۳۹۲، ساعت ۱۵:۴۵
    مهندس سلام
    من از MEF برای تزریق کامپوننت هام به یک الگوی کار در MVC استفاده کردم، کار به درستی انجام میشه اما Attribute‌های کلاس‌ها مثل پیام‌های خطا، طول فیلد و ... روی خروجی نهایی اعمال نمیشه و دیتابیس طبق پیش فرض‌ها ساخته میشه.
    البته اگر از یک فایل کانفیگ (fluent API) جدا استفاده کنم مشکل حل میشه اما در این فایلها نمی‌توان پیام خطا برای حالت‌های مختلف تعریف کرد (البته به نظرم)
      • #
        ‫۱۰ سال و ۱۲ ماه قبل، دوشنبه ۱۵ مهر ۱۳۹۲، ساعت ۰۰:۴۷
        اما این پست‌ها ربطی به سوال من نداره قبلا همش بررسی کردم مهندس. مشکل توی عدم تزریق Metadata‌های کلاس مانند DisplayName، ErrorMessageها و ... است که در FluentApi ظاهرا قابل پیاده سازی نیست
        • #
          ‫۱۰ سال و ۱۲ ماه قبل، دوشنبه ۱۵ مهر ۱۳۹۲، ساعت ۰۲:۱۴

          من الان یک ویژگی StringLength به طول 30 رو روی خاصیت Title کلاس Category مقاله جاری اضافه کردم. با همین کدهای فوق، این فیلد با طول 30 الان در دیتابیس قابل مشاهده است. آغاز دیتابیس اصلا کاری به MEF نداره.

          این هم فایل مثالی که در آن ویژگی طول رشته اعمال شده برای آزمایش:

          MefSample.cs

          • #
            ‫۱۰ سال و ۱۲ ماه قبل، دوشنبه ۱۵ مهر ۱۳۹۲، ساعت ۱۲:۳۹
            من کلاسهام به این شکله:
            کلاس کانتکس‌های من
             public class VegaContext : DbContext, IUnitOfWork, IDbContext
                {
            #region Constructors (2) 
            
                    /// <summary>
                    /// Initializes the <see cref="VegaContext" /> class.
                    /// </summary>
                    static VegaContext()
                    {
                        Database.SetInitializer<VegaContext>(null);
                    }
            
                    /// <summary>
                    /// Initializes a new instance of the <see cref="VegaContext" /> class.
                    /// </summary>
                    public VegaContext() : base("LocalSqlServer") { }
            
            #endregion Constructors 
            
            #region Properties (2) 
            
                    /// <summary>
                    /// Gets or sets the languages.
                    /// </summary>
                    /// <value>
                    /// The languages.
                    /// </value>
                    public DbSet<Language> Languages { get; set; }
            
                    /// <summary>
                    /// Gets or sets the resources.
                    /// </summary>
                    /// <value>
                    /// The resources.
                    /// </value>
                    public DbSet<Resource> Resources { get; set; }
            
            #endregion Properties 
            
            #region Methods (2) 
            
            // Public Methods (1) 
            
                    /// <summary>
                    /// Setups the specified model builder.
                    /// </summary>
                    /// <param name="modelBuilder">The model builder.</param>
                    public void Setup(DbModelBuilder modelBuilder)
                    {
                        //todo
                        modelBuilder.Configurations.Add(new ResourceMap());
                        modelBuilder.Configurations.Add(new LanguageMap());
                        modelBuilder.Entity<Resource>().ToTable("Vega_Languages_Resources");
                        modelBuilder.Entity<Language>().ToTable("Vega_Languages_Languages");
                        //base.OnModelCreating(modelBuilder);
                    }
            // Protected Methods (1) 
            
                    /// <summary>
                    /// This method is called when the model for a derived context has been initialized, but
                    /// before the model has been locked down and used to initialize the context.  The default
                    /// implementation of this method does nothing, but it can be overridden in a derived class
                    /// such that the model can be further configured before it is locked down.
                    /// </summary>
                    /// <param name="modelBuilder">The builder that defines the model for the context being created.</param>
                    /// <remarks>
                    /// Typically, this method is called only once when the first instance of a derived context
                    /// is created.  The model for that context is then cached and is for all further instances of
                    /// the context in the app domain.  This caching can be disabled by setting the ModelCaching
                    /// property on the given ModelBuidler, but note that this can seriously degrade performance.
                    /// More control over caching is provided through use of the DbModelBuilder and DbContextFactory
                    /// classes directly.
                    /// </remarks>
                    protected override void OnModelCreating(DbModelBuilder modelBuilder)
                    {
                        modelBuilder.Configurations.Add(new ResourceMap());
                        modelBuilder.Configurations.Add(new LanguageMap());
                        modelBuilder.Entity<Resource>().ToTable("Vega_Languages_Resources");
                        modelBuilder.Entity<Language>().ToTable("Vega_Languages_Languages");
                        base.OnModelCreating(modelBuilder);
                    }
            
            #endregion Methods 
            
                    #region IUnitOfWork Members
                    /// <summary>
                    /// Sets this instance.
                    /// </summary>
                    /// <typeparam name="TEntity">The type of the entity.</typeparam>
                    /// <returns></returns>
                    public new IDbSet<TEntity> Set<TEntity>() where TEntity : class
                    {
                        return base.Set<TEntity>();
                    }
                    #endregion
                }
            در تعاریف کلاسهایی که از IDBContext ارث می‌برن اکسپورت شدن (این یک نمونه از کلاس‌های منه)
            در طرف دیگر برای لود کردن کلاس زیر نوشتم
            public class LoadContexts
                {
                    public LoadContexts()
                    {
                        var directoryPath = HttpRuntime.BinDirectory;//AppDomain.CurrentDomain.BaseDirectory; //"Dll folder path";
            
                        var directoryCatalog = new DirectoryCatalog(directoryPath, "*.dll");
            
                        var aggregateCatalog = new AggregateCatalog();
                        aggregateCatalog.Catalogs.Add(directoryCatalog);
            
                        var container = new CompositionContainer(aggregateCatalog);
                        container.ComposeParts(this);
                    }
            
                    //[Import]
                    //public IPlugin Plugin { get; set; }
            
                    [ImportMany]
                    public IEnumerable<IDbContext> Contexts { get; set; }
                }
            و در کانتکس اصلی برنامه این پلاگین هارو لود می‌کنم
            public class MainContext : DbContext, IUnitOfWork
                {
                    public MainContext() : base("LocalSqlServer") { }
            
                    protected override void OnModelCreating(DbModelBuilder modelBuilder)
                    {
                        base.OnModelCreating(modelBuilder);
                        var contextList = new LoadContexts(); //ObjectFactory.GetAllInstances<IDbContext>();
                        foreach (var context in contextList.Contexts)
                            context.Setup(modelBuilder);
            
                        Database.SetInitializer(new MigrateDatabaseToLatestVersion<MainContext, Configuration>());
                        //Database.SetInitializer(new DropCreateDatabaseAlways<MainContext>());
                    }
            
                    /// <summary>
                    /// Sets this instance.
                    /// </summary>
                    /// <typeparam name="TEntity">The type of the entity.</typeparam>
                    /// <returns></returns>
                    public IDbSet<TEntity> Set<TEntity>() where TEntity : class
                    {
                        return base.Set<TEntity>();
                    }
                }
            با موفقیت همه پلاگین‌ها لود میشه و مشکلی در عملیات نیست. اما Attribute‌های کلاس هارو نمیشناسه. مثلا پیام خطا تعریف شده در MVC نمایش داده نمیشه چون وجود نداره ولی وقتی کلاس مورد نظر از IValidatableObject  ارث میبره خطای‌های من نمایش داده میشه. می‌خوام از خود متادیتاهای استاندارد استفاده کنم.



            • #
              ‫۱۰ سال و ۱۲ ماه قبل، دوشنبه ۱۵ مهر ۱۳۹۲، ساعت ۱۳:۲۱

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

              -  عمل نکردن خطاهای اعتبارسنجی به بود و نبود یک سری از تعاریف لازم در View هم بر می‌گرده. توضیح شما یعنی عمل نکردن اعتبارسنجی سمت کلاینت ولی عمل کردن اعتبارسنجی سمت سرور. به ترتیب باید jquery.min.js ، jquery.validate.min.js و jquery.validate.unobtrusive.min.js به View الحاق شده باشند. تنظیمات ClientValidationEnabled و UnobtrusiveJavaScriptEnabled در وب کانفیگ فعال باشند. از متدهایی مانند ValidationMessageFor استفاده شده باشد. این متدها یک سری ویژگی‌های خاص unobtrusive رو به عناصر HTML برای شناسایی توسط jquery.validate اضافه می‌کنند و بدون این‌ها عملا اعتبارسنجی سمت کاربر رخ نمی‌ده.

              • #
                ‫۱۰ سال و ۱۲ ماه قبل، سه‌شنبه ۱۶ مهر ۱۳۹۲، ساعت ۰۰:۰۵
                ممنون از پاسخ شما.
                اما مهندس توی کامنت قبلی گفتم "با موفقیت همه پلاگین‌ها لود میشه و مشکلی در عملیات نیست. اما Attribute‌های کلاس هارو نمیشناسه. مثلا پیام خطا تعریف شده در MVC نمایش داده نمیشه چون وجود نداره ولی وقتی کلاس مورد نظر از IValidatableObject  ارث میبره خطای‌های من نمایش داده میشه. می‌خوام از خود متادیتاهای استاندارد استفاده کنم.  "

                پس خطا نمایش داده میشه و مشکلی توی طرف کلاینت ندارم.
                در هر صورت ممنون از اینکه وقت گذاشتید و پاسخ دادید.
                • #
                  ‫۱۰ سال و ۱۲ ماه قبل، سه‌شنبه ۱۶ مهر ۱۳۹۲، ساعت ۰۱:۲۳
                  پردازش IValidatableObject سمت سرور هست. فقط نمایش نتیجه این نوع اعتبار سنجی سمت سرور، در سمت کلاینت بعد از post back کامل نمایش داده میشه.
                  • #
                    ‫۱۰ سال و ۱۲ ماه قبل، سه‌شنبه ۱۶ مهر ۱۳۹۲، ساعت ۱۴:۲۸
                    بله درسته
                    بعد از تست متوجه شدم وقتی خودم متا دیتا تعریف می‌کنم(ارث بری از متادیتای استاندارد) خطای طرف کلاینت عمل نمی‌کنه اما وقتی از متادیتای استاندارد خود دات نت استفاده میکنم خطای طرف کلاینت فعال نمیشه
                    • #
                      ‫۱۰ سال و ۱۲ ماه قبل، سه‌شنبه ۱۶ مهر ۱۳۹۲، ساعت ۱۴:۳۴
                      مطلب چطور باید سؤال پرسید رو اگر از ابتدا رعایت کرده بودید بحث به درازا نمی‌کشید. (سؤالی که در هر مرحله داره صورت مساله توضیح داده نشده‌اش عوض میشه؛ مثالی که نمی‌تونی از راه دور سریع تستش کنی و جزئیات متغیرش مشخص نیست)
  • #
    ‫۱۰ سال و ۱۰ ماه قبل، دوشنبه ۱۸ آذر ۱۳۹۲، ساعت ۲۰:۴۴
    اگر امکان دارد سورس مثال را در سایت قرار دهید.
     با تشکر