Entity Framework in an Dynamics Nav (Navision) environment (or using EF to access tables with unknown table names)

Summary
In this post I should how to use dynamic compilation to allow Entity Framework to reference tables whose names are not known at compile time.

Details
Entity Framework works great for the majority of scenarios; it handles tables in one database or spread across multiple databases very well.  Let’s say you have a site that sells products online for small companies you would need tables like company, customer, order, product, invoice etc, you would probably put all your data into a single set of tables and EF will work just fine.

However, if you have ever worked Microsoft Dynamics Nav (Navision) you know that it does not follow this standard structure.

Dynamics segregates the data differently. Each company had its own set of customer, order, product, invoice, etc, tables which were prefixed with the company name. For example, if you had two companies called ACME and SuperCorp the table structure would be like the following –

[ACME$Customer]
[ACME$Order]
[ACME$Product]
[ACME$Invoice]
[SuperCorp$Customer]
[SuperCorp$Order]
[SuperCorp$Product]
[SuperCorp$Invoice]

EF does not play nicely with this.

My first attempt was to pass in the table prefix to my DbContext when mapping, this worked for the first company passed in, but when I create a new DbContext for the second company, it would give model errors because somewhere deep in the workings of EF, something was being set statically and the mapping was failing (it was still pointing to the first company). If I created another new DbContext for the first company it worked fine again.

My second attempt was to pass in a DbCompiledModel for the company when constructing  the DbContext

DbContext(string nameOrConnectionString, System.Data.Entity.Infrastructure.DbCompiledModel model)

This worked a bit better than the first attempt, I could create a DbContext for the first and second and first (again) companies. I could retrieve data, but as soon as a there was a join in the query it failed, again it looked like something was being set the first time I created the DbContext and this was not changing when for the second company. I wasn’t happy with this approach anyway as it required a significant step away from well known EF practices of mapping and constructing a context.
My third approach was to create a context file specifically for each company which would inherit from a base context that contained all the DbSets and column mappings, table mapping was done in each individual context. This approached worked for all database queries, but has the huge issue of requiring a recompile and redeployment for each new company; a working, but impractical approach.

Building on this I came up with my solution. I created a CommonDBContext, which has the DbSets and the OnModelCreating where the table prefix is passed to the Mapping classes.
A DynamicContext class that is used to dynamically compile and construct DbContexts for each of the companies as needed. It also caches the compiled DbContext so you only take the compilation hit once for the lifetime of the application.

public abstract class CommonDBContext : DbContext
{

private readonly string tablePrefix;

protected CommonDBContext(string connectionString, string tablePrefix) :base(connectionString)
    {
        this.tablePrefix = tablePrefix;
    }

    public DbSet Customer { get; set; }
    public DbSet Order { get; set; }
    public DbSet Product { get; set; }
    public DbSet Invoice { get; set; }
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Configurations.Add(new CustomerMap(tablePrefix));
        modelBuilder.Configurations.Add(new OrderMap(tablePrefix));
        modelBuilder.Configurations.Add(new ProductMap(tablePrefix));
        modelBuilder.Configurations.Add(new InvoiceMap(tablePrefix));
    }
}

class DynamicContext
{
    private readonly static Dictionary<string, Type> CompiledContexts = new Dictionary<string, Type>();
    private readonly string _classNamePrefix;
    private static readonly object LockObject = new object();
    private readonly string _sourceCode =
            @"
            using System.Data.Entity;
            namespace Implementation.Context
            {{
                public class {0}_Context : CommonDBContext
                {{
                  public {0}_Context(string connectionString, string tablePrefix)
                        : base(connectionString, tablePrefix)
                    {{
                        Database.SetInitializer<{0}_Context>(null);
                    }}
                }}
            }}";

    #region Constructors

    public DynamicContext(string classNamePrefix)
    {
        _sourceCode = string.Format(_sourceCode, classNamePrefix);
        _classNamePrefix = classNamePrefix;
    }

    #endregion

    #region Public Methods

    public CommonDBContext GetContext(string connectionString, string tablePrefix)
    {
        Type[] constructorTypes = new Type[2] { typeof(string), typeof(string) };

        Type compiledType;
        //Double lock to prevent multiple compilations of same code
        if (!CompiledContexts.TryGetValue(_classNamePrefix, out compiledType))
        {
            lock (LockObject)
            {
                if (!CompiledContexts.TryGetValue(_classNamePrefix, out compiledType))
                {
                    compiledType = CompileContext(_classNamePrefix);
                    CompiledContexts.Add(_classNamePrefix, compiledType);
                }
            }
        }

        CommonDBContext contextObject = GetInstanceOfCompiledContext(connectionString, tablePrefix, compiledType, constructorTypes);
        return contextObject;
    }

    #endregion

    #region Private Methods

    private CommonDBContext GetInstanceOfCompiledContext(string connectionString, string tablePrefix, Type compiledContext, Type[] constructorTypes)
    {
        ConstructorInfo contextConstructorInfo = compiledContext.GetConstructor(constructorTypes);
        object contextObject = contextConstructorInfo.Invoke(new object[] { connectionString, tablePrefix });
        return contextObject as CommonDBContext;
    }

    ///
<summary>
    /// This is a one time hit for each class compiled.
    /// </summary>

    /// <param name="assemblyNames"></param>
    /// <returns></returns>
    private string[] GetAssembly(string[] assemblyNames)
    {
        string [] locations = new string[assemblyNames.Length];

        for (int loop = 0; loop <= assemblyNames.Length - 1; loop++) { locations[loop] = AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic && a.ManifestModule.Name == assemblyNames[loop]).Select(a => a.Location).First();
        }
        return locations;
    }

    private Type CompileContext(string classNamePrefix)
    {
        var compilerParameters = new CompilerParameters { GenerateInMemory = true };

        compilerParameters.ReferencedAssemblies.AddRange(GetAssembly(new string[] { "Implementation.dll", "EntityFramework.dll" }));
        var provider = new CSharpCodeProvider();
        var compiled = provider.CompileAssemblyFromSource(compilerParameters, _sourceCode);

        Type compliedType = compiled.CompiledAssembly.GetType(string.Format("Implementation.Context.{0}_Context", classNamePrefix));
        return compliedType;
    }

    #endregion

}

public class CustomerMap : EntityTypeConfiguration
{
    public CustomerMap(string tablePrefix)
    {
        // Primary Key
        this.HasKey(t => t.CustomerID);
        //snip

        // Map the table name
        string tableName = string.Format("{0}$Customer", tablePrefix);
        this.ToTable(tableName)
    }
}

If you have trouble getting this working, get in touch.

14 thoughts on “Entity Framework in an Dynamics Nav (Navision) environment (or using EF to access tables with unknown table names)

  1. Hello Byran,

    First of all great post. Thanks for it.
    Now, I have a different scenario, where Table with unique names
    i.e.,
    Table 1 -> 123-asd-45
    Table2 -> 676-bvs-98

    are added dynamically
    to DB when i execute a store procedure to create Table. So how can i achieve this.
    Any help appreciated !!!

    Regards

    Rizwan

    • Glad it was helpful to you.

      If the columns in the tables are known ahead of time you should be able to follow my example with tablePrefix, of course you will pass in the whole table name instead of just the prefix.

      If the columns change, you will probably need dynamic class compilation and construction. You can follow my example for that too.

      Hope that helps.

      Bryan

  2. Hi Bryan,

    I am getting exception in GetAssembly function

    locations[loop] = AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic && a.ManifestModule.Name == assemblyNames[loop]).Select(a => a.Location).First();

    Exception:
    System.InvalidOperationException occurred
    HResult=0x80131509
    Message=Sequence contains no elements
    Source=MoviesExample
    StackTrace:
    at MoviesExample.Models.DynamicContext.GetAssembly(String[] assemblyNames) in C:\Users\hyarlagadda\Documents\Visual Studio 2017\Projects\MoviesExample\MoviesExample\Models\CommonDBContext.cs:line 108
    at MoviesExample.Models.DynamicContext.CompileContext(String classNamePrefix) in C:\Users\hyarlagadda\Documents\Visual Studio 2017\Projects\MoviesExample\MoviesExample\Models\CommonDBContext.cs:line 117
    at MoviesExample.Models.DynamicContext.GetContext(String connectionString, String tablePrefix) in C:\Users\hyarlagadda\Documents\Visual Studio 2017\Projects\MoviesExample\MoviesExample\Models\CommonDBContext.cs:line 75
    at MoviesExample.Controllers.MoviesController..ctor() in C:\Users\hyarlagadda\Documents\Visual Studio 2017\Projects\MoviesExample\MoviesExample\Controllers\MoviesController.cs:line 15

    If I change First() to FirstOrDefault() then it is failing at

    var compiled = provider.CompileAssemblyFromSource(compilerParameters, _sourceCode); with null reference exception.

    Can you please help me?

Leave a Reply

Your email address will not be published. Required fields are marked *