20

Background:
We have a project with many modules. We're using EntityFramework 4.2 with FluentAPI (CodeFirst).

There is a central project named Diverto.ORM.EntityFramework.SQLServer which contains partial classes that build the context using the FluentAPI (and which has references to every other project on the solution).

Recently we received a request from the customer to implement many other features and the solution will need several other projects. Some of these projects will come from another system (Human Resources) and some will be created. The core of the existing solution is a Finance system.

We want to enable and disable these new projects (and GUI, business logic and all) "on the fly" using MEF. They will interact as plugins and the main menu from the application will get populated also using MEF.
However we don't really have a clue on how to enable/disable these modules/projects (new ones and HR ones) because of data that they must share.

Consider this:
- DivertoContext (main context) with DbSet<ClassA> and DbSet<ClassB>.
- PluginContext (from a plugin) with DbSet<ClassC>.

Now, consider that inside the GUI I must have access to data from ClassA, ClassB and ClassC (if the plugin is there).

Solution found! See bellow

HEY, YOU THERE, READ THIS BEFORE THE ANSWER!

I've noticed some people checking this out and marking this as favorite or upvoting. Please, bear in mind that this answer dates back to 2012 and EntityFramework has changed a lot since that.

Also, please, please, PLEASE, remember that each project has it's very own needs. I needed this feature, this way, at that time. Your project might not need this at all, or just some parts of this!

Finally, just to make sure everything is covered up, yes, it's possible to make this work with EF 6.1 and EF Migrations and it might be possible with other ORM and migration framework as well.

You might need some other interfaces, as one for the migration to load, and properly handle specific plugin migration (don't mix it with other plugins so try to implement some sort of unique token for each plugin).

Kishore Kumar
  • 12,675
  • 27
  • 97
  • 154
Anderson Matos
  • 3,132
  • 1
  • 23
  • 33
  • Where does "features" entail? Entities, entity members, GUI functionality..? "disable features from these new projects": is "these new projects" the projects from HR or the ones for the customer? A picture would help. ;p – Jake Berger Jan 05 '12 at 18:19
  • By features I mean entities, entity members, GUI and business logic that will come from HR into the Finance project. Some of HR entities inherit from basic entities that are common to all projects but there is no straight link from HR to Finance. "These new projects" are the ones that come from the customer request (some will come from existing HR projects, some will be created, but none of them are present in the existing Finance solution). – Anderson Matos Jan 06 '12 at 12:42

1 Answers1

29

Solution found!

Well, I'll try to explain here since I couldn't find this elsewhere. This is interesting for people that have to create a single base software that will receive multiple plugins and these plugins must interact with existing data within a single database.

First of all, I'll be working with Entity Framework with CodeFirst API and all. So if you're going into this I recomend reading of EntityTypeConfiguration from MSDN and of Code First Fluent API also from MSDN.

Now, let's understang some things:

  • You must have only one context for everything to work properly. I'll go into that and show a way to have classes from plugins inside the context from the application, but for that to work you MUST understand the Generic Repository Pattern. I'll show only a bit here but I recomend you to study hard on that and try to create the best interface for your app.
  • MEF will be our friend here. I'll consider that you already know how to create a simple plugin with MEF and how to access a method inside that plugin. Also, I'll try to avoid going deep into MEF because it's not the case here and because you can use other solution. In fact, I'm using MEF just because I'm already familiar with it somehow.
  • If you're going into "Oh, I'll need to handle multiple context creation that will point into a single database and all" you're doing it wrong. This is all about simple configs and just some fluent API. Believe in me: I've searched through the Internet for a week and finally after talking to a friend we found this solution and it's really easy =).


First things first

Solution: MEFTest
Projects:

  • Base.ORM (that will hold interfaces for ORM)
  • Base.ORM.EntityFramework.SQLServer (will hold base classes for EF)
  • SampleApplication.ORM.EntityFramework.SQLServer (will hold the context for the application)
  • SampleApplication (executable)
  • MEFPlugin (will hold our plugin)

Now, the coding

Inside the Base.ORM project create your Generic Repository interface with methods as you see fit, but the interface is not typed. It will be similar to this:

public interface IRepository
{
   bool Add<T>(T item);
}

From now on I'll just call it IRepository to keep things simple.
I'll consider one method called Add(T item) for sample coding.

Inside the Base.ORM.EntityFramework.SQLServer create a BaseContext class that inherits from DbContext and that implements IRepository. It should look like this:

public class BaseContext : IRepository
{
   public bool Add<T>(T item)
   {
      try { Set<T>().Add(item); return true; }
      catch { return false; }
   }
}

You might add a custom IDatabaseInitializer base-implementation here for database versioning. I've done it with SQL files winthin a standard folder but this is old coding as EF now supports migrations.

If you'll still up to handling this manually, remember to set the database to single user mode BEFORE and reverting to multi user mode AFTER. Remember: try...catch...finally will help here because you can revert to multi user inside the finally so even on error there will be no problems left behind.

Inside the SampleApplication project, add:
ClassA (int Id, string Name) and ClassB (int Id, DateTime TestDate).

Inside the SampleApplication.ORM.EntityFramework.SQLServer create your standard context.
I'll use three classes here with very interesting names: ClassA, ClassB and ClassC.
Both ClassA and ClassB are referenced from this project and it will be like this:

public class Context : BaseContext
{
   public DbSet<ClassA> ClassA { get; set; }
   public DbSet<ClassB> ClassB { get; set; }

   protected override void OnModelCreating(DbModelBuilder modelBuilder)
   {
      /* I'll talk about this later. Just override the OnModelCreating and leave it */
      base.OnModelCreating(modelBuilder);
   }
}

Now the funny part: The plugin will have a method like this:

public void Setup(DbModelBuilder modelBuilder)
{
   modelBuilder.Entity<ClassC>().ToTable("ClassC");
   modelBuilder.Entity<ClassC>().HasKey(_classC => _classC.Id);
   modelBuilder.Entity<ClassC>().Property(_classC => _classC.Date2).HasColumnType("datetime2").HasPrecision(7);
}

Of course ClassC is inside the plugin project. You don't have any reference to it from the main project.
You will have to find this method (Setup) using MEF and of course, interfaces. I'll just show WHERE to place this and how to make it work =)

Back to the Context class, the OnModelCreating method will be like this:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
   modelBuilder.Entity<ClassA>().ToTable("ClassA");
   modelBuilder.Entity<ClassA>().HasKey(_classA => _classA.Id);

   modelBuilder.Entity<ClassB>().ToTable("ClassB");
   modelBuilder.Entity<ClassB>().HasKey(_classB => _classB.Id);
   modelBuilder.Entity<ClassB>().Property(_classB => _classB.TestDate).HasColumnType("datetime2").HasPrecision(7);

   /* Use MEF to load all plugins. I'll use the mock interface IPlugin */
   foreach (IPlugin plugin in MefLoadedPlugins)
      plugin.Setup(modelBuilder);
}

Usage

Inside your APP you will have just one Context. This context inherits from BaseContext which implements IRepository. With that in mind you need to code your GUI and business layer to use IRepository (from Base.ORM) and the specific class (inside the business-specific dll).

It works!

Well, it's working here.

I think I've shown every relevant part here.
Of course there is more code inside the classes but it's not the case. I'm trying to show only what you really need to create/implement to get it done.

Don't forget:

  1. Don't forget that you will have to write your own code to seed the database. For my case here inside the same interface for the plugin I have something like Seed(IRepository) and I just handle everything there.
  2. Don't forget that you don't have references from the main project to the plugins. This means that you MUST find a way to load menu, GUI, business and data all through interfaces and providers. I've managed to solve that using something like IPlugin (business), IFormPlugin (GUI - Winforms) and IPluginRepository (data). You will have to find your own names and methods that might fit your needs but this should be a good start point.
  3. Don't forget that if you load a plugin you must create the tables inside the database or EF CodeFirst will fail to initialize. Remember that you might need SQL files and manually run them to create tables as needed.
  4. Don't forget that if you unload a plugin you must remove tables too or EF will fail too.
  5. Don't forget that you REALLY NEED BACKUPS. I didn't do this yet but I've already marked where this will be done (before and after DDL commands).
  6. This is MY solution for MY case. This should be a good start for new projects but just that. Don't think that by following what I've done here it will work 100% for every case.

Thanks

Thanks to people from SO and from MSDN that helped my a lot with comments and other posts that I've found.
Thanks to Caio Garcia (BR) that helped me with some instructions about other plugin-based systems.

Sample codes (full)

Here follows some sample codes:

FOR THE BASIC ITEMS (solution predefined items)

public class ClassA
{
   public int Id { get; set; }
   public string Name { get; set; }
}

public class ClassB
{
   public int Id { get; set; }
   public string OtherName { get; set; }
}

public interface IRepository
{
   bool Add<T>(T item);
   bool Save();
}

public class BaseContext : DbContext, IRepository
{
   public bool Add<T>(T item)
   { 
      try { Set<T>().Add(item); return true; } catch { return false; }
   }
   public bool Save()
   {
      try { SaveChanges(); return true; } catch { return false; }
   }
}

public class Context : BaseContext
{
   // Fill this list using MEF - check for the IPluginContext interface on assemblies
   public List<IPluginContext> MefLoadedPlugins;

   protected override void OnModelCreating(DbModelBuilder modelBuilder)
   {
      modelBuilder.Entity<ClassA>().ToTable("TableB", "Schema_1");
      modelBuilder.Entity<ClassA>().HasKey(_a => _a.Id);

      modelBuilder.Entity<ClassB>().ToTable("TableB", "Schema_1");
      modelBuilder.Entity<ClassB>().HasKey(_b => _b.Id);

      if (MefLoadedPlugins != null)
         foreach (var pluginContext in MefLoadedPlugins)
            pluginContext.Setup(modelBuilder);
   }
}

FOR THE PLUGIN

public class ClassC
{
   public int Id { get; set; }
   public string Description { get; set; }
}

public interface IPluginContext
{
   void Setup(DbModelBuilder modelBuilder);
}

public class Just_A_Sample_Plugin_Context : IPluginContext
{
   public void Setup(DbModelBuilder modelBuilder)
   {
      modelBuilder.Entity<ClassC>().ToTable("TableC", "Schema_2");
      modelBuilder.Entity<ClassC>().HasKey(_c => _c.Id);
   }
}

ON YOUR REGULAR CODE

public void DoSomething(IRepository repo)
{
   var classA = new ClassA() { Name = "First Name" };
   repo.Add(classA);
   repo.Save();
}
Anderson Matos
  • 3,132
  • 1
  • 23
  • 33
  • This is exactly what I need but I don't quite seem to able to get it working. Can you clarify what your BaseContext and BaseReposiory looks like? You say BaseRepository inherits DbContext and implements IRepository and BaseContext also implements IRepository? Do you need to declare a DBSset for Class C anywhere? – GraemeMiller Aug 21 '12 at 10:51
  • @GraemeMiller I've already updated this item. It was my mistake and I'm sorry for that. It's the same class: BaseContext. It's a class that inherits from DbContext and implements IRepository (Generic Repository Pattern). There is no need for DbSet, just by setting modelBuilder.Entity on the Setup() method inside the plugin context the entity will be created and will be available. – Anderson Matos Aug 22 '12 at 12:24
  • Thanks for update but still struggling. I'm lost on what your BaseContext look like.I've never combined a GenericRepository and a context before. I have created my normal generic IRepository where T: class it is then implemented by BaseContext as a result require it to be of type then when my application Context inherits BaseContext I can't make it look like your Context as mine requires a type T. Just wondering why we are combining context and repository? – GraemeMiller Aug 22 '12 at 20:53
  • @GraemeMiller Damn! I forgot to set this explicitly and update this item along with the other part. The problem is that you should not create the IRepository. You should have a plain regular interface and methods will be like: "void Add(T item) where T : class;". The BaseContext will then implement the interface with methods similar to this: "public void Add(T item) where T : class { Set().Add(item); }". My bad, once again. I'll review this post, wait for an update. – Anderson Matos Aug 23 '12 at 16:17
  • I got it working in the end and did what you said and made it Add. I also got it working with just have a separate Generic Repository and providing it with the context. Thanks for the great reference. I hoped I would find some way to create relationships in the context between dynamically added entities and core entities but it doesn't seem possible :( – GraemeMiller Aug 23 '12 at 17:18
  • Depends on what you mean by relationship. If you're talking about an array or object reference on the plugin to a class on core-modules, you can have that (plugins can know about core-modules but core-modules cannot know about plugins) HOWEVER if you're talking about having a relation on the core-class to the plugin, I can't say if it's possible. Never done before. Might be possible if you find a way to do it using interfaces and custom-code to enhance EF capabilites. Might be worth a question on SO. – Anderson Matos Aug 24 '12 at 19:15
  • How would one go about migrations with this pattern? – MrJD Nov 15 '12 at 01:11
  • Since I'm talking here about manual/coded items, migrations would be just like that. Migration classes will have to be available togather within a single directory/namespace. Nothing more that I can remember (it's working here already). – Anderson Matos Nov 16 '12 at 22:41
  • @AndersonMatos - How did you manage with Add-migration doing this? Did it look to the plugins and create the migration files for you? – webnoob Nov 19 '15 at 15:16
  • @webnoob its 2021 now hope you have solved the issue with migration, this answer was of great help btw, i am stuck at migrations part i want to use ef core update-database and add-migrations command to handle migrations automagically – Alok Feb 27 '21 at 09:49
  • @alok @webnoob It's been a while since I went through this, but considering that plugins are loaded dynamically, I don't think it would be feasible to leverage that using `update-database`. But maybe setup a component (executable project) that helps you? – Anderson Matos Mar 03 '21 at 23:13
  • @AndersonMatos thanks i solved the issue just today itself :) apparently ef design tools is smart enough to run all plugins before generating migrations – Alok Mar 04 '21 at 11:31