Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MEF Plugins and EF CodeFirst - How?

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).

like image 838
Anderson Matos Avatar asked Jan 04 '12 17:01

Anderson Matos


1 Answers

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();
}
like image 60
Anderson Matos Avatar answered Oct 27 '22 23:10

Anderson Matos