پس از بررسی ساختار یک پروژهی افزونه پذیرو همچنین بهبود توزیع فایلهای استاتیک آن، اکنون نوبت به کار با دادهها است. هدف اصلی آن نیز داشتن مدلهای اختصاصی و مستقل Entity framework code-first به ازای هر افزونه است و سپس بارگذاری و تشخیص خودکار آنها در Context مرکزی برنامه.
پیشنیازها
- آشنایی با مباحث Migrations در EF Code first
- آشنایی با مباحث الگوی واحد کار
- چگونه مدلهای EF را به صورت خودکار به Context اضافه کنیم؟
- چگونه تنظیمات مدلهای EF را به صورت خودکار به Context اضافه کنیم؟
کدهایی را که در این قسمت مشاهده خواهید کرد، در حقیقت همان برنامهی توسعه یافته «آشنایی با مباحث الگوی واحد کار» است و از ذکر قسمتهای تکراری آن جهت طولانی نشدن مبحث، صرفنظر خواهد شد. برای مثال Context و مدلهای محصولات و گروههای آنها به همراه کلاسهای لایه سرویس برنامهی اصلی، دقیقا همان کدهای مطلب «آشنایی با مباحث الگوی واحد کار» است.
تعریف domain classes مخصوص افزونهها
در ادامهی پروژهی افزونه پذیر فعلی، پروژهی class library جدیدی را به نام MvcPluginMasterApp.Plugin1.DomainClasses اضافه خواهیم کرد. از آن جهت تعریف کلاسهای مدل افزونهی یک استفاده میکنیم. برای مثال کلاس News را به همراه تنظیمات Fluent آن به این پروژهی جدید اضافه کنید:
این پروژه برای کامپایل شدن نیاز به بستهی نیوگت ذیل دارد:
مشکل!برنامهی اصلی، همانند مطلب «آشنایی با مباحث الگوی واحد کار» دارای domain classes خاص خودش است به همراه تنظیمات Context ایی که صریحا در آن مدلهای متناظر با این پروژه در معرض دید EF قرار گرفتهاند:
اکنون برنامهی اصلی چگونه باید مدلها و تنظیمات سایر افزونهها را یافته و به صورت خودکار به این Context اضافه کند؟ با توجه به اینکه این برنامه هیچ ارجاع مستقیمی را به افزونهها ندارد.
تغییرات اینترفیس Unit of work جهت افزونه پذیری
در ادامه، اینترفیس بهبود یافتهی IUnitOfWork را جهت پذیرش DbSetهای پویا و همچنین EntityTypeConfigurationهای پویا، ملاحظه میکنید:
متدهای جدید آن:
SetDynamicEntities : توسط این متد در ابتدای برنامه، نوعهای مدلهای جدید افزونهها به صورت خودکار به Context اضافه خواهند شد.
SetConfigurationsAssemblies : کار افزودن اسمبلیهای حاوی تعاریف EntityTypeConfigurationهای جدید و پویا را به عهده دارد.
ForceDatabaseInitialize: سبب خواهد شد تا مباحث migrations، پیش از شروع به کار برنامه، اعمال شوند.
در کلاس Context ذیل، نحوهی پیاده سازی این متدهای جدید را ملاحظه میکنید:
در متد استاندارد OnModelCreating، فرصت افزودن نوعهای پویا و همچنین تنظیمات پویای آنها وجود دارد. برای این منظور میتوان از متدهای modelBuilder.RegisterEntityType و modelBuilder.Configurations.AddFromAssembly کمک گرفت.
بهبود اینترفیس IPlugin جهت پذیرش نوعهای پویای EF
در قسمت اول، با اینترفیس IPlugin آشنا شدیم. هر افزونه باید دارای کلاسی باشد که این اینترفیس را پیاده سازی میکند. از آن جهت دریافت تنظیمات و یا ثبت تنظیمات مسیریابی و امثال آن استفاده میشود.
در اینجا متد GetEfBootstrapper آن کار دریافت تنظیمات EF هر افزونه را به عهد دارد.
ConfigurationsAssemblies مشخص کنندهی اسمبلیهایی است که حاوی تعاریف EntityTypeConfigurationهای افزونهی جاری هستند.
DomainEntities بیانگر لیست مدلها و موجودیتهای هر افزونه است.
DatabaseSeeder کار دریافت منطق متد Seed را بر عهده دارد. برای مثال اگر افزونهای نیاز است در آغاز کار تشکیل جداول آن، دیتای پیش فرض و خاصی را در بانک اطلاعاتی ثبت کند، میتوان از این متد استفاده کرد. اگر دقت کنید این Action یک وهله از IUnitOfWork را به افزونه ارسال میکند. بنابراین در این طراحی جدید، اینترفیس IUnitOfWork به پروژهی MvcPluginMasterApp.PluginsBase منتقل میشود. به این ترتیب دیگر نیازی نیست تا تک تک افزونهها ارجاع مستقیمی را به DataLayer پروژهی اصلی پیدا کنند.
تکمیل متد GetEfBootstrapper در افزونهها
اکنون جهت معرفی مدلها و تنظیمات EF آنها، تنها کافی است متد GetEfBootstrapper هر افزونه را تکمیل کنیم:
در اینجا نحوهی معرفی مدلهای جدید را توسط خاصیت DomainEntities و تنظیمات متناظر را به کمک خاصیت ConfigurationsAssemblies مشاهده میکنید. باید دقت داشت که هر اسمبلی فقط باید یکبار معرفی شود و مهم نیست که چه تعداد تنظیمی در آن وجود دارند. کار یافتن کلیهی تنظیمات از نوع EntityTypeConfigurationها به صورت خودکار توسط EF صورت میگیرد.
همچنین توسط delegate ایی به نام DatabaseSeeder، نحوهی دسترسی به متد Set واحد کار و سپس استفادهی از آن، برای تعریف متد Seed سفارشی نیز تکمیل شدهاست.
تدارک یک راه انداز EF، پیش از شروع به کار برنامه
در پوشهی App_Start پروژهی اصلی یا همان MvcPluginMasterApp، کلاس جدید EFBootstrapperStart را با کدهای ذیل اضافه کنید:
در اینجا یک راه انداز سفارشی از نوع PreApplicationStartMethod تهیه شدهاست. Pre بودن آن به معنای اجرای کدهای متد Start این کلاس، پیش از آغاز به کار برنامه و پیش از فراخوانی متد Application_Start فایل Global.asax.cs است.
همانطور که ملاحظه میکنید، ابتدا لیست تمام افزونههای موجود، به کمک StructureMap دریافت میشوند. سپس میتوان در متد initDatabase به متد GetEfBootstrapper هر افزونه دسترسی یافت و توسط آن تنظیمات مدلها را یافته و به Context اصلی برنامه اضافه کرد. سپس با فراخوانی ForceDatabaseInitialize تمام این موارد به صورت خودکار به بانک اطلاعاتی اعمال خواهند شد.
کار متد runDatabaseSeeders، یافتن DatabaseSeeder هر افزونه، اجرای آنها و سپس فراخوانی متد SaveAllChanges در آخر کار است.
کدهای کامل این سری را از اینجا میتوانید دریافت کنید:
MvcPlugin
پیشنیازها
- آشنایی با مباحث 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; }
تغییرات اینترفیس 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); } } } } }
بهبود اینترفیس 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; } } }
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 ...." }); } }; }
همچنین توسط 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(); } } } }
همانطور که ملاحظه میکنید، ابتدا لیست تمام افزونههای موجود، به کمک StructureMap دریافت میشوند. سپس میتوان در متد initDatabase به متد GetEfBootstrapper هر افزونه دسترسی یافت و توسط آن تنظیمات مدلها را یافته و به Context اصلی برنامه اضافه کرد. سپس با فراخوانی ForceDatabaseInitialize تمام این موارد به صورت خودکار به بانک اطلاعاتی اعمال خواهند شد.
کار متد runDatabaseSeeders، یافتن DatabaseSeeder هر افزونه، اجرای آنها و سپس فراخوانی متد SaveAllChanges در آخر کار است.
کدهای کامل این سری را از اینجا میتوانید دریافت کنید:
MvcPlugin