Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

EF: In search of a design strategy for DatabaseFirst DbContext of a Modular Application

I'm looking for suggestions on how to approach using an ORM (in this case, EF5) in the design of modular Non-Monolithic applications, with a Core part and 3rd party Modules, where the Core has no direct Reference to the 3rd party Modules, and Modules only have a reference to Core/Common tables and classes.

For arguments sake, a close enough analogy would be DNN.

CodeFirst:

With CodeFirst, the approach I used was to build up the model of the Db was via reflection: in the Core's DbContext's DbInitialation phase, I used Reflection to find any class in any dll (eg Core or various Modules) decorated with IDbInitializer (a custom contract containing an Execute() method) to define just the dll's structure. Each dll added to the DbModel what it knew about itself. Any subsequent Seeding was also handled in the same wa (searching for a specific IDbSeeder contract, and executing it).

Pro: * the approach works for now. * The same core DbContext can be used across all respositories, as long as each repo uses dbContext.GetSet(), rather than expecting it to be a property of the dbContext. No biggie. Cons: * it only works at startup (ie, adding new modules would require an AppPool refresh). * CodeFirst is great for a POC. But in EF5, it's not mature enough for Enterprise work yet (and I can't wait for EF6 for StoredProcs and other features to be added). * My DBA hates CodeFirst, at least for the Core, wanting to optimize that part with Stored Procs as much as as possible...We're a team, so I have to try to find a way to please him, if I can find a way...

Database-first:

The DbModel phase appears to be happening prior to the DbContext's constructor (reading from embedded *.edmx resource file). DbInitialization is never invoked (as model is deemed complete), so I can't add more tables than what the Core knows about.

If I can't add elements to the Model, dynamically, as one can with CodeFirst, it means that * either the Core DbContext's Model has to have knowledge of every table in the Db -- Core AND every 3rd party module. Making the application Monolithic and highly coupled, defeating the very thing I am trying to achieve. * Or each 3rd party has to create their own DbContext, importing Core tables, leading to * versioning issues (module not updating their *.edmx's when Core's *.edmx is updated, etc.) * duplication everywhere, in different memory contexts = hard to track down concurrency issues.

At this point, it seems to me that the CodeFirst approach is the only way that Modular software can be achieved with EF. But hopefully someone else know's how to make DatabaseFirst shine -- is there any way of 'appending' DbSet's to the model created from the embedded *.edmx file?

Or any other ideas?

like image 958
Ciel Avatar asked Sep 29 '12 10:09

Ciel


2 Answers

I would consider using a sort of plugin architecture, since that's your overall design for the application itself.

You can accomplish the basics of this by doing something like the following (note that this example uses StructureMap - here is a link to the StructureMap Documentation):

  1. Create an interface from which your DbContext objects can derive.

    public interface IPluginContext {
        IDictionary<String, DbSet> DataSets { get; }
    }
    
  2. In your Dependency Injection set-up (using StructureMap) - do something like the following:

    Scan(s => {
        s.AssembliesFromApplicationBaseDirectory();
        s.AddAllTypesOf<IPluginContext>();
        s.WithDefaultConventions();
    });
    For<IEnumerable<IPluginContext>>().Use(x =>
        x.GetAllInstances<IPluginContext>()
    );
    
  3. For each of your plugins, either alter the {plugin}.Context.tt file - or add a partial class file which causes the DbContext being generated to derive from IPluginContext.

    public partial class FooContext : IPluginContext { }
    
  4. Alter the {plugin}.Context.tt file for each plugin to expose something like:

    public IDictionary<String, DbSet> DataSets {
        get {
            // Here is where you would have the .tt file output a reference
            // to each property, keyed on its property name as the Key - 
            // in the form of an IDictionary.
        }
    }
    
  5. You can now do something like the following:

    // This could be inside a service class, your main Data Context, or wherever
    // else it becomes convenient to call.
    public DbSet DataSet(String name) {
        var plugins = ObjectFactory.GetInstance<IEnumerable<IPluginContext>>();
        var dataSet = plugins.FirstOrDefault(p =>
            p.DataSets.Any(ds => ds.Key.Equals(name))
        );
    
        return dataSet;
    }
    

Forgive me if the syntax isn't perfect - I'm doing this within the post, not within the compiler.

The end result gives you the flexibility to do something like:

    // Inside an MVC controller...
    public JsonResult GetPluginByTypeName(String typeName) {
        var dataSet = container.DataSet(typeName);
        if (dataSet != null) {
            return Json(dataSet.Select());
        } else {
            return Json("Unable to locate that object type.");
        }
    }

Clearly, in the long-run - you would want the control to be inverted, where the plugin is the one actually tying into the architecture, rather than the server expecting a type. You can accomplish the same kind of thing using this sort of lazy-loading, however - where the main application exposes an endpoint that all of the Plugins tie to.

That would be something like:

public interface IPlugin : IDisposable {
    void EnsureDatabase();
    void Initialize();
}

You now can expose this interface to any application developers who are going to create plugins for your architecture (DNN style) - and your StructureMap configuration works something like:

Scan(s => {
    s.AssembliesFromApplicationBaseDirectory(); // Upload all plugin DLLs here
    // NOTE: Remember that this gives people access to your system!!! 
    //       Given what you are developing, though, I am assuming you
    //       already get that.
    s.AddAllTypesOf<IPlugin>();
    s.WithDefaultConventions();
});
For<IEnumerable<IPlugin>>().Use(x => x.GetAllInstances<IPlugin>());

Now, when you initialize your application, you can do something like:

// Global.asax
public static IEnumerable<IPlugin> plugins = 
    ObjectFactory.GetInstance<IEnumerable<IPlugin>>();

public void Application_Start() {
    foreach(IPlugin plugin in plugins) {
        plugin.EnsureDatabase();
        plugin.Initialize();
    }
}

Each of your IPlugin objects can now contain its own database context, manage the process of installing (if necessary) its own database instance / tables, and dispose of itself gracefully.

Clearly this isn't a complete solution - but I hope it starts you off in a useful direction. :) If I can help clarify anything herein, please let me know.

like image 174
Troy Alford Avatar answered Oct 29 '22 11:10

Troy Alford


While it's a CodeFirst approach, and not the cleanest solution, what about forcing initialization to run even after start up, as in the answer posted here: Forcing code-first to always initialize a non-existent database? (I know precisely nothing about CodeFirst, but seeing as this is a month old with no posts it's worth a shot)

like image 36
RelicScoth Avatar answered Oct 29 '22 11:10

RelicScoth