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:
- 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.
- 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.
- 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.
- Don't forget that if you unload a plugin you must remove tables too or EF will fail too.
- 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).
- 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();
}