طراحی افزونه پذیر با ASP.NET MVC 4.x/5.x - قسمت سوم
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: هفت دقیقه

پس از بررسی ساختار یک پروژه‌ی افزونه پذیر و همچنین بهبود توزیع فایل‌های استاتیک آن، اکنون نوبت به کار با داده‌ها است. هدف اصلی آن نیز داشتن مدل‌های اختصاصی و مستقل Entity framework code-first به ازای هر افزونه است و سپس بارگذاری و تشخیص خودکار آن‌ها در Context مرکزی برنامه.

پیشنیازها
- آشنایی با مباحث Migrations در EF Code first
- آشنایی با مباحث الگوی واحد کار
- چگونه مدل‌های EF را به صورت خودکار به Context اضافه کنیم؟
- چگونه تنظیمات مدل‌های EF را به صورت خودکار به Context اضافه کنیم؟


کدهایی را که در این قسمت مشاهده خواهید کرد، در حقیقت همان برنامه‌ی توسعه یافته «آشنایی با مباحث الگوی واحد کار» است و از ذکر قسمت‌های تکراری آن جهت طولانی نشدن مبحث، صرفنظر خواهد شد. برای مثال Context و مدل‌های محصولات و گروه‌های آن‌ها به همراه کلاس‌های لایه سرویس برنامه‌ی اصلی، دقیقا همان کدهای مطلب «آشنایی با مباحث الگوی واحد کار» است.


تعریف domain classes مخصوص افزونه‌ها

در ادامه‌ی پروژه‌ی افزونه پذیر فعلی، پروژه‌ی class library جدیدی را به نام MvcPluginMasterApp.Plugin1.DomainClasses اضافه خواهیم کرد. از آن جهت تعریف کلاس‌های مدل افزونه‌ی یک استفاده می‌کنیم. برای مثال کلاس News را به همراه تنظیمات Fluent آن به این پروژه‌ی جدید اضافه کنید:
using System.Data.Entity.ModelConfiguration;
 
namespace MvcPluginMasterApp.Plugin1.DomainClasses
{
    public class News
    {
        public int Id { set; get; }
 
        public string Title { set; get; }
 
        public string Body { set; get; }
    }
 
    public class NewsConfig : EntityTypeConfiguration<News>
    {
        public NewsConfig()
        {
            this.ToTable("Plugin1_News");
            this.HasKey(news => news.Id);
            this.Property(news => news.Title).IsRequired().HasMaxLength(500);
            this.Property(news => news.Body).IsOptional().IsMaxLength();
        }
    }
}
این پروژه برای کامپایل شدن نیاز به بسته‌ی نیوگت ذیل دارد:
 PM> install-package EntityFramework

مشکل! برنامه‌ی اصلی، همانند مطلب «آشنایی با مباحث الگوی واحد کار» دارای domain classes خاص خودش است به همراه تنظیمات Context ایی که صریحا در آن مدل‌های متناظر با این پروژه در معرض دید EF قرار گرفته‌اند:
public class MvcPluginMasterAppContext : DbContext, IUnitOfWork
{
    public DbSet<Category> Categories { set; get; }
    public DbSet<Product> Products { set; get; }
اکنون برنامه‌ی اصلی چگونه باید مدل‌ها و تنظیمات سایر افزونه‌ها را یافته و به صورت خودکار به این Context اضافه کند؟ با توجه به اینکه این برنامه هیچ ارجاع مستقیمی را به افزونه‌ها ندارد.


تغییرات اینترفیس Unit of work جهت افزونه پذیری

در ادامه، اینترفیس بهبود یافته‌ی IUnitOfWork را جهت پذیرش DbSetهای پویا و همچنین EntityTypeConfigurationهای پویا، ملاحظه می‌کنید:
namespace MvcPluginMasterApp.PluginsBase
{
    public interface IUnitOfWork : IDisposable
    {
        IDbSet<TEntity> Set<TEntity>() where TEntity : class;
        int SaveAllChanges();
        void MarkAsChanged<TEntity>(TEntity entity) where TEntity : class;
        IList<T> GetRows<T>(string sql, params object[] parameters) where T : class;
        IEnumerable<TEntity> AddThisRange<TEntity>(IEnumerable<TEntity> entities) where TEntity : class;
        void SetDynamicEntities(Type[] dynamicTypes);
        void ForceDatabaseInitialize();
        void SetConfigurationsAssemblies(Assembly[] assembly);
    }
}
متدهای جدید آن:
SetDynamicEntities : توسط این متد در ابتدای برنامه، نوع‌های مدل‌های جدید افزونه‌ها به صورت خودکار به Context اضافه خواهند شد.
SetConfigurationsAssemblies : کار افزودن اسمبلی‌های حاوی تعاریف EntityTypeConfigurationهای جدید و پویا را به عهده دارد.
ForceDatabaseInitialize: سبب خواهد شد تا مباحث migrations، پیش از شروع به کار برنامه، اعمال شوند.

در کلاس Context ذیل، نحوه‌ی پیاده سازی این متدهای جدید را ملاحظه می‌کنید:
namespace MvcPluginMasterApp.DataLayer.Context
{
    public class MvcPluginMasterAppContext : DbContext, IUnitOfWork
    {
        private readonly IList<Assembly> _configurationsAssemblies = new List<Assembly>();
        private readonly IList<Type[]> _dynamicTypes = new List<Type[]>(); 
 
        public void ForceDatabaseInitialize()
        {
            Database.Initialize(force: true);
        }
 
        public void SetConfigurationsAssemblies(Assembly[] assemblies)
        {
            if (assemblies == null) return;
 
            foreach (var assembly in assemblies)
            {
                _configurationsAssemblies.Add(assembly);
            }
        }
 
        public void SetDynamicEntities(Type[] dynamicTypes)
        {
            if (dynamicTypes == null) return;
            _dynamicTypes.Add(dynamicTypes);
        }
 
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            addConfigurationsFromAssemblies(modelBuilder);
            addPluginsEntitiesDynamically(modelBuilder);
            base.OnModelCreating(modelBuilder);
        }
 
        private void addConfigurationsFromAssemblies(DbModelBuilder modelBuilder)
        {
            foreach (var assembly in _configurationsAssemblies)
            {
                modelBuilder.Configurations.AddFromAssembly(assembly);
            }
        }
 
        private void addPluginsEntitiesDynamically(DbModelBuilder modelBuilder)
        {
            foreach (var types in _dynamicTypes)
            {
                foreach (var type in types)
                {
                    modelBuilder.RegisterEntityType(type);
                }
            }
        }
    }
}
در متد استاندارد OnModelCreating، فرصت افزودن نوع‌های پویا و همچنین تنظیمات پویای آن‌ها وجود دارد. برای این منظور می‌توان از متدهای modelBuilder.RegisterEntityType و modelBuilder.Configurations.AddFromAssembly کمک گرفت.


بهبود اینترفیس IPlugin جهت پذیرش نوع‌های پویای EF

در قسمت اول، با اینترفیس IPlugin آشنا شدیم. هر افزونه باید دارای کلاسی باشد که این اینترفیس را پیاده سازی می‌کند. از آن جهت دریافت تنظیمات و یا ثبت تنظیمات مسیریابی و امثال آن استفاده می‌شود.
در اینجا متد GetEfBootstrapper آن کار دریافت تنظیمات EF هر افزونه را به عهد دارد.
namespace MvcPluginMasterApp.PluginsBase
{
    public interface IPlugin
    {
        EfBootstrapper GetEfBootstrapper();
        //...به همراه سایر متدهای مورد نیاز
    }
 
    public class EfBootstrapper
    {
        /// <summary>
        /// Assemblies containing EntityTypeConfiguration classes.
        /// </summary>
        public Assembly[] ConfigurationsAssemblies { get; set; }
 
        /// <summary>
        /// Domain classes.
        /// </summary>
        public Type[] DomainEntities { get; set; }
 
        /// <summary>
        /// Custom Seed method.
        /// </summary>
        public Action<IUnitOfWork> DatabaseSeeder { get; set; }
    } 
}
ConfigurationsAssemblies مشخص کننده‌ی اسمبلی‌هایی است که حاوی تعاریف EntityTypeConfigurationهای افزونه‌ی جاری هستند.
DomainEntities بیانگر لیست مدل‌ها و موجودیت‌های هر افزونه است.
DatabaseSeeder کار دریافت منطق متد Seed را بر عهده دارد. برای مثال اگر افزونه‌ای نیاز است در آغاز کار تشکیل جداول آن، دیتای پیش فرض و خاصی را در بانک اطلاعاتی ثبت کند، می‌توان از این متد استفاده کرد. اگر دقت کنید این Action یک وهله از IUnitOfWork را به افزونه ارسال می‌کند. بنابراین در این طراحی جدید، اینترفیس IUnitOfWork به پروژه‌ی MvcPluginMasterApp.PluginsBase منتقل می‌شود. به این ترتیب دیگر نیازی نیست تا تک تک افزونه‌ها ارجاع مستقیمی را به DataLayer پروژه‌ی اصلی پیدا کنند.


تکمیل متد GetEfBootstrapper در افزونه‌ها

اکنون جهت معرفی مدل‌ها و تنظیمات EF آن‌ها، تنها کافی است متد GetEfBootstrapper هر افزونه را تکمیل کنیم:
namespace MvcPluginMasterApp.Plugin1
{
    public class Plugin1 : IPlugin
    {
        public EfBootstrapper GetEfBootstrapper()
        {
            return new EfBootstrapper
            {
                DomainEntities = new[] { typeof(News) },
                ConfigurationsAssemblies = new[] { typeof(NewsConfig).Assembly },
                DatabaseSeeder = uow =>
                {
                    var news = uow.Set<News>();
                    if (news.Any())
                    {
                        return;
                    }
 
                    news.Add(new News
                    {
                        Title = "News 1",
                        Body = "news 1 news 1 news 1 ...."
                    });
 
                    news.Add(new News
                    {
                        Title = "News 2",
                        Body = "news 2 news 2 news 2 ...."
                    });
                }
            };
        }
در اینجا نحوه‌ی معرفی مدل‌های جدید را توسط خاصیت DomainEntities و تنظیمات متناظر را به کمک خاصیت ConfigurationsAssemblies مشاهده می‌کنید. باید دقت داشت که هر اسمبلی فقط باید یکبار معرفی شود و مهم نیست که چه تعداد تنظیمی در آن وجود دارند. کار یافتن کلیه‌ی تنظیمات از نوع EntityTypeConfigurationها به صورت خودکار توسط EF صورت می‌گیرد.
همچنین توسط delegate ایی به نام DatabaseSeeder، نحوه‌ی دسترسی به متد Set واحد کار و سپس استفاده‌ی از آن، برای تعریف متد Seed سفارشی نیز تکمیل شده‌است.


تدارک یک راه انداز EF، پیش از شروع به کار برنامه

در پوشه‌ی App_Start پروژه‌ی اصلی یا همان MvcPluginMasterApp، کلاس جدید EFBootstrapperStart را با کدهای ذیل اضافه کنید:
[assembly: PreApplicationStartMethod(typeof(EFBootstrapperStart), "Start")]
namespace MvcPluginMasterApp
{
    public static class EFBootstrapperStart
    {
        public static void Start()
        {
            var plugins = SmObjectFactory.Container.GetAllInstances<IPlugin>().ToList();
            using (var uow = SmObjectFactory.Container.GetInstance<IUnitOfWork>())
            {
                initDatabase(uow, plugins);
                runDatabaseSeeders(uow, plugins);
            }
        }
 
        private static void initDatabase(IUnitOfWork uow, IEnumerable<IPlugin> plugins)
        {
            foreach (var plugin in plugins)
            {
                var efBootstrapper = plugin.GetEfBootstrapper();
                if (efBootstrapper == null) continue;
 
                uow.SetDynamicEntities(efBootstrapper.DomainEntities);
                uow.SetConfigurationsAssemblies(efBootstrapper.ConfigurationsAssemblies);
            }
 
            Database.SetInitializer(new MigrateDatabaseToLatestVersion<MvcPluginMasterAppContext, Configuration>());
            uow.ForceDatabaseInitialize();
        }
 
        private static void runDatabaseSeeders(IUnitOfWork uow, IEnumerable<IPlugin> plugins)
        {
            foreach (var plugin in plugins)
            {
                var efBootstrapper = plugin.GetEfBootstrapper();
                if (efBootstrapper == null || efBootstrapper.DatabaseSeeder == null) continue;
 
                efBootstrapper.DatabaseSeeder(uow);
                uow.SaveAllChanges();
            }
        }
    }
}
در اینجا یک راه انداز سفارشی از نوع PreApplicationStartMethod تهیه شده‌است. Pre بودن آن به معنای اجرای کدهای متد Start این کلاس، پیش از آغاز به کار برنامه و پیش از فراخوانی متد Application_Start فایل Global.asax.cs است.
همانطور که ملاحظه می‌کنید، ابتدا لیست تمام افزونه‌های موجود، به کمک StructureMap دریافت می‌شوند. سپس می‌توان در متد initDatabase به متد GetEfBootstrapper هر افزونه دسترسی یافت و توسط آن تنظیمات مدل‌ها را یافته و به Context اصلی برنامه اضافه کرد. سپس با فراخوانی ForceDatabaseInitialize تمام این موارد به صورت خودکار به بانک اطلاعاتی اعمال خواهند شد.
کار متد runDatabaseSeeders، یافتن DatabaseSeeder هر افزونه، اجرای آن‌ها و سپس فراخوانی متد SaveAllChanges در آخر کار است.



کدهای کامل این سری را از اینجا می‌توانید دریافت کنید:
MvcPlugin
  • #
    ‫۹ سال و ۵ ماه قبل، جمعه ۲۸ فروردین ۱۳۹۴، ساعت ۲۰:۱۲
    با تشکر.
    اگر لازم باشد پلاگین ما به مدل موجود در پروژه اصلی دسترسی داشته باشد باید به چه شکلی عمل کرد؟ 
    • #
      ‫۹ سال و ۵ ماه قبل، جمعه ۲۸ فروردین ۱۳۹۴، ساعت ۲۰:۵۳
      - در عمل کل برنامه و تمام افزونه‌های آن از یک IUnitOfWork استفاده می‌کنند؛ یعنی تمام آن‌ها به تمام مدل‌های اضافه شده‌ی به Context اصلی برنامه دسترسی دارند. بنابراین هر پلاگین در صورت نیاز امکان دسترسی به مدل‌های برنامه‌ی اصلی یا سایر افزونه‌ها را دارا است. تمام این افزونه‌ها در کنار هم یک سیستم را تشکیل می‌دهند و مانند شکل انتهای بحث، از یک بانک اطلاعاتی استفاده می‌کنند.
      - به همین جهت تنها کاری که باید انجام داد، افزودن ارجاعی به کلاس‌های مدل مورد نظر هست. پس از آن شبیه به کاری که در DatabaseSeeder انجام شده، می‌توان با استفاده از متد Set، به کلیه امکانات مدلی خاص دسترسی یافت:
      DatabaseSeeder = uow =>
      {
           var news = uow.Set<News>();
      اگر نمی‌خواهید ارجاعی را به کلاس‌های مدل مورد نظر اضافه کنید، با توجه به اینکه این کلاس‌ها هم اکنون جزئی از وهله‌ی Context ارائه شده‌ی توسط IUnitOfWork هستند، باید متوسل به Reflection و تدارک متد Set ویژه‌ای شوید که بجای News، معادل رشته‌ای آن‌را دریافت کند.
      ولی در کل افزودن ارجاعی به کلاس‌های مدل دیگر، مشکل ساز نیست؛ چون این کلاس‌ها عملا منطق خاصی را پیاده سازی نمی‌کنند و همچنین وابستگی خاصی هم به پروژه‌ی خاصی ندارند. یک سری کلاس دارای خاصیت‌های get/set دار معمولی هستند به همراه تنظیمات آن‌ها.
      • #
        ‫۹ سال و ۵ ماه قبل، شنبه ۵ اردیبهشت ۱۳۹۴، ساعت ۲۲:۰۷
        من در قسمت کلاس اهراز هویت به کمی مشکل برخوردم .
        کلاس زیر رو دارم که البته در پلاگینی جدا نوشته شده  :
            public class CmsUser : IdentityUser
            {
                public string DisplayName { get; set; }
        
            }
        هر پست باید دارای یک نویسنده باشد که می‌خوام از CmsUser  استفاده کنم .
        این وابستگی و ارتباط باید کجا نوشته شود ؟ 
         public class Post
            {
        
                private IList<string> _tags = new List<string>();
                public int id { get; set; }
                public string name { get; set; }
                public string slug { get; set; }
                public string description { get; set; }
                public DateTime? publishTime { get; set; }
                public string content { get; set; }
                public IList<string> tags
                {
        
                    get { return _tags; }
                    set { _tags = value; }
                }
                public string CombineTags
                {
                    get { return string.Join(",", _tags); }
                    set { _tags = value.Split(',').Select(x => x.Trim()).ToList(); }
                }
        
                public string AuthorID { get; set; }
        
              //  [ForeignKey("AuthorID")]
              //  public CmsUser Author { get; set; }
            }
        هرجا می‌نویسم برای کلید‌های خارجیش اخطار میده  و این که زیاد نمی‌خوام پلاگین پست از وجود CmsUser  با خبر باشه
        • #
          ‫۹ سال و ۵ ماه قبل، یکشنبه ۶ اردیبهشت ۱۳۹۴، ساعت ۰۰:۱۹
          - موجودیت‌های مشترک بین افزونه‌ها را در یک پروژه‌ی مجزا قرار دهید؛ مانند: CommonEntities
          - از این پروژه‌ی مشترک، ارجاعی را به افزونه‌های مورد نظر اضافه کنید.

          پروژه‌ی جاری جهت افزودن کلید خارجی به کاربران مشترک بین تمام افزونه‌ها به روز شد، با این تغییرات و با این خروجی (که در آن در هر دو افزونه‌ی تعریف شده، ارجاعی به کلاس User مشترک هست):

          • #
            ‫۹ سال و ۵ ماه قبل، دوشنبه ۷ اردیبهشت ۱۳۹۴، ساعت ۲۰:۵۲
            برای ارتباط‌های n به n به چه شکلی باید عمل کرد؟ منظورم موقعی است که باید لیستی از کلاس دیگر در کلاس یوزر ما موجود باشد.
            با تشکر
  • #
    ‫۹ سال و ۵ ماه قبل، یکشنبه ۶ اردیبهشت ۱۳۹۴، ساعت ۱۵:۲۴
    برای استفاده از Identity چون دوتا Context ایجاد میشه ، کلی دردسر میشه 
    چطوری این رو بسازیم که تداخل هم ایجاد نشه :
    public class Cmscontex : IdentityDbContext<CmsUser>
    identity  به کلاسی که از  IdentityDbContext ارث برده شده نیاز داره . در صورتی که ما قبلا 
    public class UIAppContext : DbContext, IUnitOfWork
    اگه دوتا Context ایجاد کنیم ارتباط کلاس‌ها خیلی بد میشه .
    لطفا اگر میشه در رابط با identity هم مثالی بزنید
  • #
    ‫۹ سال و ۴ ماه قبل، سه‌شنبه ۲۲ اردیبهشت ۱۳۹۴، ساعت ۱۳:۳۸

    سلام

    پس از لود شدن dll‌های پلاگین امکان استفاده از روش  ^ و  ^  برای اضافه کردن و بارگذاری اطلاعات در Context وجود ندارد ؟

    • #
      ‫۹ سال و ۴ ماه قبل، سه‌شنبه ۲۲ اردیبهشت ۱۳۹۴، ساعت ۱۴:۳۳
      قسمت «تغییرات اینترفیس Unit of work جهت افزونه پذیری » در متن فوق از همین مطالب استفاده می‌کند (متدهای addConfigurationsFromAssemblies و addPluginsEntitiesDynamically).
      • #
        ‫۹ سال و ۴ ماه قبل، سه‌شنبه ۲۲ اردیبهشت ۱۳۹۴، ساعت ۱۵:۵۴

        ولی در عمل در EfBootstrapper باید برای کلیه کلاس‌های Ef  مقدار DomainEntities و برای کلیه کلاس‌های Fluent Api مقدار ConfigurationsAssemblies مشخص شود . در حالی که در لینک هایی که ^ و  ^  کلیه کلاس هایی که از BaseEntity مشتق شده اند به Context اضافه شده و نیازی به معرفی ندارند و  کلاس‌ها تنظیمات FluentApi‌ها نیز  از روی nameSpace مشخص شده اضافه می‌شوند.

        بهتر نیست از BaseEntity استفاده بشود؟

        • #
          ‫۹ سال و ۴ ماه قبل، سه‌شنبه ۲۲ اردیبهشت ۱۳۹۴، ساعت ۱۶:۰۱
          - این روش دقیق‌تر هست، با احتمال اشتباه و سعی و خطای کمتر و سریعتر.
          - modelBuilder.Configurations.AddFromAssembly متد توکار خود EF هست(در نگارش‌های اخیر آن) و به صورت خودکار کلاس‌های مشتق شده‌ی از EntityTypeConfiguration را اسکن می‌کند.
          - در کل هر طور که صلاح می‌دانید روش اسکن آن‌را تغییر دهید. اصل ماجرا، یعنی متدهای addConfigurationsFromAssemblies و addPluginsEntitiesDynamically تفاوتی نخواهند کرد.
          • #
            ‫۹ سال و ۴ ماه قبل، دوشنبه ۱۱ خرداد ۱۳۹۴، ساعت ۲۱:۰۹
            سلام
            آیا میشه با این روش، PartialView رو هم لود کرد ؟ یعنی PartialView داخل Plugin به عنوان EmbedeResource باشه و از پروژه اصلی لودش کرد ؟
            • #
              ‫۹ سال و ۴ ماه قبل، دوشنبه ۱۱ خرداد ۱۳۹۴، ساعت ۲۱:۱۵
              - به چه مشکلی برخوردید دقیقا؟ اجرا نشد؟ خطا گرفتید؟
              - partial view هم مانند یک view معمولی باید custom tools اش به razor generator تنظیم شود. بعد از آن کار کردن با آن معمولی و مانند قبل خواهد بود.
              • #
                ‫۹ سال و ۴ ماه قبل، سه‌شنبه ۱۲ خرداد ۱۳۹۴، ساعت ۱۵:۱۵
                سلام 
                مشکل نبود فایل RazorGeneratorMvcStart.cs  بود که پس از افزودن رفع شد 
  • #
    ‫۹ سال و ۱ ماه قبل، چهارشنبه ۲۸ مرداد ۱۳۹۴، ساعت ۱۶:۵۱
    با سلام و احترام
    من با این قسمت مشکل دارم 
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
            {
                AddConfigurationsFromAssemblies(modelBuilder);
                AddPluginsEntitiesDynamically(modelBuilder);
                base.OnModelCreating(modelBuilder);
    
                modelBuilder.Entity<AppUser>().ToTable("Users");
                modelBuilder.Entity<CustomRole>().ToTable("Roles");
                modelBuilder.Entity<CustomUserClaim>().ToTable("UserClaims");
                modelBuilder.Entity<CustomUserRole>().ToTable("UserRoles");
                modelBuilder.Entity<CustomUserLogin>().ToTable("UserLogins");
            }
    در خط سوم متد  AddPluginsEntitiesDynamically(modelBuilder);
    اجرا نمیشه و خطای زیر رو میده
    Additional information: Method not found: 'Void System.Data.Entity.DbModelBuilder.RegisterEntityType(System.Type)'.
    جالبه وقتی داخل این متد breakpoint میزارم اصلا واردش نمیشه!
    • #
      ‫۹ سال و ۱ ماه قبل، چهارشنبه ۲۸ مرداد ۱۳۹۴، ساعت ۱۷:۲۷
      «Method not found» یعنی مشکل تداخل نگارش‌های مختلف EF را با هم دارید (در یک نگارش و یک پروژه، این متد هست و در دیگری خیر). نحوه‌ی رفع مشکل (با این فرض که EF در هیچ پروژه‌ای به صورت دستی اضافه نشده و حتما از طریق نیوگت اضافه شده‌است):
      PM> update-package
      اجرای این دستور، تمام تداخلات و عدم هماهنگی‌ها را به صورت یکجا در تمام پروژه‌ها برطرف می‌کند.
  • #
    ‫۹ سال و ۱ ماه قبل، چهارشنبه ۲۸ مرداد ۱۳۹۴، ساعت ۱۹:۰۴
    اگر در تزریق وابستگی به مثال رسمی Identity در ساختار پروژه جاری که به صورت یک پلاگین جدا در نظر گرفته شود ظاهرا با مشکلاتی مواجه خواهیم شد .
    همه مباحث گفته شده در این بحث و همچنین بخش تزریق وابستگی به مثال رسمی Identity  در نظر گرفته شده است ولی با پیغام خطای زیر مواجه شده ام.
    No default Instance is registered and cannot be automatically determined for type 'Microsoft.Owin.Security.DataProtection.IDataProtectionProvider'
    
    There is no configuration specified for Microsoft.Owin.Security.DataProtection.IDataProtectionProvider
    
    1.) new ApplicationUserManager(*Default of IUserStore<AppUser, Int32>*, *Default of IApplicationRoleManager*, *Default of IDataProtectionProvider*, *Default of IIdentityMessageService*, *Default of IIdentityMessageService*, *Default of IUserService*)
    2.) PPU.Plugin.Accounting.Service.ApplicationUserManager
    3.) Instance of PPU.Plugin.Accounting.Service.Contract.IApplicationUserManager (PPU.Plugin.Accounting.Service.ApplicationUserManager)
    4.) new HomeController(*Default of IApplicationUserManager*, *Default of IApplicationSignInManager*, *Default of IAuthenticationManager*, *Default of IProfileService*, *Default of IUserService*, *Default of IUnitOfWork*)
    5.) PPU.Plugin.Accounting.Areas.Account.Controllers.HomeController
    6.) Instance of PPU.Plugin.Accounting.Areas.Account.Controllers.HomeController
    7.) Container.GetInstance(PPU.Plugin.Accounting.Areas.Account.Controllers.HomeController)

    • #
      ‫۹ سال و ۱ ماه قبل، چهارشنبه ۲۸ مرداد ۱۳۹۴، ساعت ۱۹:۱۸
      «IDataProtectionProvider» در فایل Startup تنظیم می‌شود. مکان این فایل آغازین هم در فایل web.config با کلید owin:AppStartup باید دقیقا مشخص شود. در غیر اینصورت مقداری برای IDataProtectionProvider درنظر گرفته نخواهد شد و به خطای فوق می‌رسید.
      • #
        ‫۹ سال و ۱ ماه قبل، چهارشنبه ۲۸ مرداد ۱۳۹۴، ساعت ۲۰:۰۹
        ممنون حق با شما بود و من به این نکته توجه نکردم. البته به نظر میشه بدون تنظیم کلید مربویه در WebConfig با استفاده از 
        [assembly: OwinStartupAttribute(typeof(PPU.WebUi.Startup))]
        انجام داد. با توجه به اینکه هر پلاگین قراره به صورت مستقل باشه، خوب من سیستم Accounting رو یه پلاگین مستقل در نظر گرفتم. سوال اینجاست آیا باید فایل StartUp مربوطه رو در پروژه اصلی قرار داد و یا اینکه در همون پروژه پلاگین گذاشت تا بشه ازش به عنوان یه سیستم مستقل در دیگر پروژه‌ها استفاده کرد.
        • #
          ‫۹ سال و ۱ ماه قبل، چهارشنبه ۲۸ مرداد ۱۳۹۴، ساعت ۲۰:۲۶
          مباحث authentication و authorization سراسری هستند. بنابراین باید در برنامه‌ی اصلی قرارگیرند تا تمام افزونه‌ها را تغذیه کنند.
  • #
    ‫۹ سال و ۱ ماه قبل، یکشنبه ۸ شهریور ۱۳۹۴، ساعت ۱۶:۲۲
    در پروژه مثال شما در این بخش به عنوان نمونه پلاگین 1 که دارای کلاس News هست رو در نظر بگیرید شما برای ارتباط با کلاس User اومدید سیم کشی مورد نظر رو انجام دادید
      [ForeignKey("UserId")]
      public virtual User User { set; get; }
      public int UserId { set; get; }
    به همین خاطر رفرنسی رو به پروژه CommonEntity ارجاع دادید.
    خوب حالا اگه قرار باشه  که من Icollection مربوطه به News رو در کلاس User قرار بدم نیاز دارم ارجاعی به پلاگین 1 داشته باشم که باعث خطای Circular Dependency میشه.
    چه پیشنهادی برای حل این مشکل وجود داره و چه باید کرد؟ 
    • #
      ‫۹ سال و ۱ ماه قبل، یکشنبه ۸ شهریور ۱۳۹۴، ساعت ۱۶:۴۷
      راه حل: نباید این‌کار را انجام دهید.
      علت:
      - اگر افزونه‌ای قرار هست برنامه‌ی اصلی را تغدیه کند - مثلا اعتبارسنجی - نام آن افزونه نیست و نباید به صورت افزونه تعریف شود. برنامه‌ی اصلی بجز بارگذاری افزونه‌ها هیچ کار دیگری قرار نیست با جزئیات آن‌ها به صورت مستقیم انجام دهد.
      - اگر افزونه‌ای وابسته‌است به افزونه‌ی دیگر، نام اینکار افزونه نویسی نیست.
      - شما قبل از اینکه بخواهید وارد این مبحث شوید، نیاز است کمی در مورد برنامه‌های افزونه پذیر موجود (در حالت کلی) مطالعه کنید و بررسی کنید که مثلا اگر یک برنامه‌ی پخش music افزونه پذیر است، افزونه‌ی A آن که توسط فرد X تهیه شده، آیا قرار است از امکانات افزونه‌ی B که توسط فرد Y تهیه شده‌است، استفاده کند؟ چنین کاری اساسا بی‌مفهوم است و طراحی افزونه پذیر نام ندارد. آیا افزونه‌ی A فایرفاکس از افزونه‌ی B آن استفاده می‌کند و به آن وابسته‌‌است؟ خیر.
      - اگر قرار هست افزونه‌ها به یک سری اطلاعات مشترک دسترسی پیدا کنند، این اطلاعات باید مشترک باشند و مستقل از هر کدام از افزونه‌ها.

      در مثالی که ارائه شد، اگر هدف کوئری گرفتن از لیست خبرهای یک کاربر است، این کار فقط باید در افزونه‌ی News انجام شود (چون اگر قرار باشد سایر افزونه‌ها به ریز اطلاعات news دسترسی داشته باشند که ضرورتی به افزونه تعریف کردن آن نبود) و به این صورت:
       var userNewsList =  _news.Include(x=>x.User).Where(x=>x.UserId == 1).ToList();
  • #
    ‫۹ سال قبل، یکشنبه ۲۲ شهریور ۱۳۹۴، ساعت ۱۲:۵۷
    فقط برای globalization از مقاله شما استفاده کردم (مقاله ) برای مستر کار میدهد اما در پلاگین‌ها کار نمی‌کند و در حقیقت همان ریسورس اصلی کار می‌کند و بقیه ریسورس‌ها (فارسی) کار نمی‌کنند حال آنکه در مستر همه چی درست است
    • #
      ‫۹ سال قبل، یکشنبه ۲۲ شهریور ۱۳۹۴، ساعت ۱۸:۰۸
      پروژه برای اضافه کردن مباحث بومی سازی به افزونه‌ها، به روز شد.


      تنها نکته‌ی مهم آن تغییر دستور کپی کردن فایل‌ها به پوشه‌ی bin پروژه‌ی اصلی به صورت زیر است تا در اینجا زیر پوشه‌ی bin\fa یک افزونه هم به پوشه‌ی bin پروژه‌ی اصلی کپی شود:
      XCopy "$(ProjectDir)$(OutDir)*" "$(SolutionDir)MvcPluginMasterApp\bin\" /S /Y
  • #
    ‫۸ سال و ۱۰ ماه قبل، جمعه ۲۲ آبان ۱۳۹۴، ساعت ۰۵:۲۶
    من برای جستجو این تغییراتو به سورس شما اضافه کردم؛ اما در کنترلر Search برای دسترسی به متد SearchNews  یا SearchArticle (اضافه کردم)، چطوری باید انجام بدم! 
    اصلا این روش درستیه؟
    • #
      ‫۸ سال و ۱۰ ماه قبل، شنبه ۲۳ آبان ۱۳۹۴، ساعت ۱۳:۰۰
      مقدار دریافتی را به صورت کوئری استرینگ، به کنترلر و اکشن متد ویژه‌ی جستجوی یک افزونه، هدایت کنید. به این صورت:
      var pluginUrl = plugin.GetMenuItem(this.Request.RequestContext).Url;
      return Redirect(pluginUrl + "/path1/path2/path3?queryStringValue1=" + Server.UrlEncode(searchTerm));
  • #
    ‫۸ سال و ۸ ماه قبل، دوشنبه ۵ بهمن ۱۳۹۴، ساعت ۲۲:۰۶
    در نسخه نهایی پروژه شما دو پلاگین قرار دادید. پلاگینی برای اخبار و مقالات و یک پروژه هم برای مدل‌های مشترک که کاربر در این قسمت قرار داره. حالا سوال من اینه که فرضا مدل Tags برای هر دو جدول اخبار و مقالات یکی هست. و ممکنه اصلا ما یک افزونه دیگری هم داشته باشیم که بخواد برچسب بگیره. برای این جور روابط باید به چه شکلی عمل کرد ؟ رابطه‌ی بین جدول اخبار و برچسب‌ها و رابطه بین جدول مقالات و برچست‌ها همگی چند به چند هستند.
    • #
      ‫۸ سال و ۸ ماه قبل، دوشنبه ۵ بهمن ۱۳۹۴، ساعت ۲۳:۳۸
      مدل Tag هم میبایست در بین مدل‌های مشترک قرار گیرد . در ایجاد روابط چند به چند اگر با Fluent API‌ها کار کنید نیاز نیست حتما در هر دو طرف لیست‌های مورد نظر را تعریف کنید. برای مثال در افزونه اخبار میتوانید همچین تعریفی داشته باشید.
       public class NewsItemConfig : EntityTypeConfiguration<NewsItem>
          {
              public NewsItemConfig()
              {
                  HasMany(a => a.Tags).WithMany().Map(m =>
                  {
                      m.MapLeftKey("TagId");
                      m.MapRightKey("NewsItemId");
                      m.ToTable("NewsItemTag");
                  });
            
              }
          }
      برای کوئری زدن هم میتوانید به شکل زیر اقدام کنید.
      var news= from p in ctx.NewsItems
                       from t in p.Tags
                       where t.Name == "Tag1"
                       select p;


  • #
    ‫۸ سال و ۳ ماه قبل، یکشنبه ۶ تیر ۱۳۹۵، ساعت ۱۷:۵۶
    سلام
    میخواستم بدونم این سیستم افزونه پذیر رو میشه با Injection خود asp.net core پیاده سازی کرد یا حتما بایستی structure map باشه؟
    اگر میشه، کدوم بهتره؟
    • #
      ‫۸ سال و ۳ ماه قبل، یکشنبه ۶ تیر ۱۳۹۵، ساعت ۱۸:۲۶
      این پروژه ارتباطی به ASP.NET Core  ندارد و مربوط هست به ASP.NET MVC 5. اخیرا یک سری تغییرنام صورت گرفته. ASP.NET 5 و ASP.NET MVC 6 الان هر دو به نام ASP.NET Core 1.0 معرفی شدند. EF 7 هم به نام EF Core 1.0 معرفی شده‌است. پایه این‌ها NET Core. است که با دات نت اصلی (و متداول) یکی نیست.
  • #
    ‫۸ سال و ۲ ماه قبل، پنجشنبه ۲۴ تیر ۱۳۹۵، ساعت ۰۵:۱۹
    سلام
    یه مشکلی در پروژه‌ی من وجود داره که انتقال پروژه‌های افزونه به پروژه‌ی اصلی ناقص انجام میشه، یعنی dll خود پروژه‌ی افزونه و resource آن انتقال پیدا میکنه ولی سایر dll‌های افزونه انتقال پیدا میکنه (مثل DomainClasses و Services  ). در حالی که در نمونه سمپل شما این مشکل وجود نداره  
    بارها rebiuld و حتی مجددا پروژه‌ها رو اضافه کردم ولی مشکل حل نشد
    • #
      ‫۸ سال و ۲ ماه قبل، پنجشنبه ۲۴ تیر ۱۳۹۵، ساعت ۰۵:۳۰
      قسمت post build برای کپی فایل‌ها مهم است. اگر این قسمت اجرا نمی‌شود، یعنی یا مسیرها را اشتباه وارد کرده‌اید و یا پروژه‌ی مدنظر build نمی‌شود.
      - مسیرها را بررسی کنید.
      - کل solution را build کنید.
      - روی پروژه اصلی کلیک راست کنید و گزینه‌ی project dependencies را انتخاب کنید. در اینجا می‌شود مشخص کرد، زمانیکه این پروژه build می‌شود، چه پروژه‌هایی حتما باید build شوند (در قسمت depends on آن، تیک همه‌ی موارد را قرار دهید).
  • #
    ‫۸ سال و ۲ ماه قبل، چهارشنبه ۶ مرداد ۱۳۹۵، ساعت ۲۳:۳۰
    با سلام ؛ فرض کنید سیستمی قرار است طراحی کنیم که شامل ماژول یا پلاگین پیامک و ماژول اشخاص می‌باشد و این دو ماژول کاملا از هم مستقل هستند؛ یکی از امکاناتی که میخواهیم به سیستم اضافه کنیم امکال ارسال پیامک به اشخاص از طریق ماژول پیامک می‌باشد؛ حال با توجه به این معماری که پلاگینها از همدیگر جدا عمل میکنند و اینکه پلاگینها از همدیگر هیچ اطلاعی ندارند چطور باید این کار را انجام دهیم. با سپاس.
    • #
      ‫۸ سال و ۲ ماه قبل، چهارشنبه ۶ مرداد ۱۳۹۵، ساعت ۲۳:۳۶
      کمی بالاتر توضیح دادم  «... در عمل کل برنامه و تمام افزونه‌های آن از یک IUnitOfWork استفاده می‌کنند؛ یعنی تمام آن‌ها به تمام مدل‌های اضافه شده‌ی به Context اصلی برنامه دسترسی دارند ...»
      + «...  موجودیت‌های مشترک بین افزونه‌ها را در یک پروژه‌ی مجزا قرار دهید؛ مانند: CommonEntities  ...»
      • #
        ‫۸ سال و ۲ ماه قبل، پنجشنبه ۷ مرداد ۱۳۹۵، ساعت ۰۰:۱۴
        بله درسته پلاگینها به تمام مدل‌های برنامه دسترسی دارند ولی به امکانات ماژول‌های دیگر دسترسی ندارند و نمیتوانند از امکانات ماژول‌های دیگر استفاده ببرند فرض کنید کاربر داخل صفحه پلاگین اشخاص است و از همینجا میخواهد به یکی از اشخاص پیامک بدهد با کلیک  روی گزینه ارسال پیامک طبیعتا باید پنجره ارسال پیامک که داخل پلاگین پیامک است باز شود و بقیه مراحل در حقیقت منظورم ارتباط بین پلاگین‌های مختلف برنامه است با سپاس.
        • #
          ‫۸ سال و ۲ ماه قبل، پنجشنبه ۷ مرداد ۱۳۹۵، ساعت ۰۰:۳۳
          از مطالبی مانند «Messaging Patterns with Postal.NET» ایده بگیرید. ایده اصلی آن ارتباط loosely coupled  قسمت‌های مختلف برنامه با هم است. شبیه به همین ایده در برنامه‌های دسکتاپ WPF که از الگوی MVVM استفاده می‌کنند، با مفهومی به نام Messenger وجود دارد. توسط Messenger دو قسمت مختلف برنامه با هم کار می‌کنند، بدون اینکه هیچ ارجاع مستقیمی از یکدیگر را داشته باشند.
  • #
    ‫۸ سال قبل، جمعه ۱۹ شهریور ۱۳۹۵، ساعت ۱۸:۱۳
    من اگر بخوام یک لیست در صفحه‌ی اصلی داشته باشم که آخرین مطالب سایت رو نشون بده، که شامل آخرین مطالب هر پلاگین مانند اخبار و مقالات و ... باشه، چطور میتونم اطلاعات رو از پلاگین‌ها بگیرم تا اصول پلاگین بیس به هم نریزه؟
    آیا میشه توی این کلاس NewsPlugin، مانند این کد که با GetMenuItem   آدرس پلاگین رو بدست آوردیم، بتونیم به بانک متصل شده و لیست اطلاعات پلاگین رو هم داشته باشیم!    
    @using MvcPluginMasterApp.IoCConfig
    @using MvcPluginMasterApp.PluginsBase
    @{
        var plugins = SmObjectFactory.Container.GetAllInstances<IPlugin>().ToList();
    }
    
    @foreach (var plugin in plugins)
    {
        var menuItem = plugin.GetMenuItem(this.Request.RequestContext);
        <li>
            <a href="@menuItem.Url">@menuItem.Name</a>
        </li>
    }

    • #
      ‫۸ سال قبل، جمعه ۱۹ شهریور ۱۳۹۵، ساعت ۱۸:۳۵
      «... اصول پلاگین بیس به هم نریزه ...»
      اصولش را تکمیل کنید. بسط دهید؛ مانند همین GetMenuItem. اسمش را مثلا بگذارید، GetLatestInfo. پیاده سازی آن در هر پلاگین به صورت مستقل انجام خواهد شد. در این طرف مانند همین روش نمایش منو، این لیست را نمایش دهید.
      • #
        ‫۸ سال قبل، شنبه ۲۰ شهریور ۱۳۹۵، ساعت ۰۰:۰۰
        اگر من به همین کلاس NewsPlugin  سرویس خبر (INewsService) رو Inject کنم، موردی نداره، اصولی هست؟
        • #
          ‫۸ سال قبل، شنبه ۲۰ شهریور ۱۳۹۵، ساعت ۰۰:۲۲
          مشکلی ندارد. سرویس ایجاد می‌کنید، که سرویس دهد. فقط در تعاریف پلاگین پایه باید نوع خروجی GetLatestInfo را مشخص کنید. یک کلاس مشخص که تمام پلاگین‌های برنامه باید اطلاعاتی را که دارند، مطابق این فرمت خاص «نگاشت» کنند. این نگاشت هم در سرویس‌های مرتبط صورت خواهد گرفت و خروجی را از طریق GetLatestInfo، با فرمت کاملا مشخصی در اختیار مصرف کننده‌ی اصلی قرار می‌دهند. 
  • #
    ‫۷ سال و ۳ ماه قبل، دوشنبه ۸ خرداد ۱۳۹۶، ساعت ۱۸:۳۴
    ممنون میشم راهنمایی کنید، راه حلی که بشه مانند متد Seed (که میتونیم دیتای پیش فرض برای هر پلاگین اضافه کنیم)، در هنگام اجرا توی ساختار دیتابیس تغییراتی ایجاد کنیم. مثلا برای هر پلاگین یک Function داشته باشیم؟
    • #
      ‫۷ سال و ۳ ماه قبل، دوشنبه ۸ خرداد ۱۳۹۶، ساعت ۱۹:۰۴
      برای ساختار هر افزونه خاصیت زیر را هم اضافه کنید:
      string CreateSqlSchema { get;}
      بعد در زمان بارگذاری آن، متد context.Database.ExecuteSqlCommand را بر روی آن فراخوانی کنید.
      • #
        ‫۷ سال و ۳ ماه قبل، دوشنبه ۸ خرداد ۱۳۹۶، ساعت ۱۹:۵۹
        ممنون، برای ایجاد Function جواب داد. علاوه براین من میخوام برای ساخت FulltextSearch، اسکریپتشو هنگام اجرای برنامه برای هر پلاگین اجرا کنم. ولی این خطا رو گرفتم .
        An exception of type 'System.Data.SqlClient.SqlException' occurred in EntityFramework.SqlServer.dll but was not handled in user code
        
        Additional information: CREATE FULLTEXT CATALOG statement cannot be used inside a user transaction.
        برای این حالت چه راه حلی وجود داره؟
        • #
          ‫۷ سال و ۳ ماه قبل، دوشنبه ۸ خرداد ۱۳۹۶، ساعت ۲۱:۵۷
          برای این حالت‌های خاص، تراکنش را خاموش کنید:
          context.Database.ExecuteSqlCommand(TransactionalBehavior.DoNotEnsureTransaction, sql, parameters);
  • #
    ‫۶ سال و ۹ ماه قبل، یکشنبه ۲۶ آذر ۱۳۹۶، ساعت ۱۵:۰۳
    سلام؛  من کامل آموزش رو پیاده سازی کردم فقط  HybridHttpOrThreadLocalScoped در کلاس SmObjectFactory خطا می‌ده

    • #
      ‫۶ سال و ۹ ماه قبل، یکشنبه ۲۶ آذر ۱۳۹۶، ساعت ۱۵:۳۲
      در اینجا توضیح دادم:
      PM> Install-Package structuremap.web -Version 4.0.0.315