22

I need to change the storage schema of the entities on runtime. I've followed a wonderful post, available here: http://blogs.microsoft.co.il/blogs/idof/archive/2008/08/22/change-entity-framework-storage-db-schema-in-runtime.aspx?CommentPosted=true#commentmessage

This works perfectly, but only for queries, not for modifications.

Any idea why?

Daniel Mann
  • 57,011
  • 13
  • 100
  • 120
nirpi
  • 715
  • 1
  • 9
  • 24

6 Answers6

25

Well, I was looking for this piece of code all around the Internet. In the end I had to do it myself. It's based on Brandon Haynes adapter, but this function is all you need to change the schema on runtime - and you don't need to replace the autogenerated context constructors.

public static EntityConnection Create(
    string schema, string connString, string model)
{
    XmlReader[] conceptualReader = new XmlReader[]
    {
        XmlReader.Create(
            Assembly
                .GetExecutingAssembly()
                .GetManifestResourceStream(model + ".csdl")
        )
    };
    XmlReader[] mappingReader = new XmlReader[]
    {
        XmlReader.Create(
            Assembly
                .GetExecutingAssembly()
                .GetManifestResourceStream(model + ".msl")
        )
    };

    var storageReader = XmlReader.Create(
        Assembly
            .GetExecutingAssembly()
            .GetManifestResourceStream(model + ".ssdl")
    );
    XNamespace storageNS = "http://schemas.microsoft.com/ado/2009/02/edm/ssdl";

    var storageXml = XElement.Load(storageReader);

    foreach (var entitySet in storageXml.Descendants(storageNS + "EntitySet"))
    {   
        var schemaAttribute = entitySet.Attributes("Schema").FirstOrDefault();
        if (schemaAttribute != null)
        {
            schemaAttribute.SetValue(schema);
        }
    }
    storageXml.CreateReader();

    StoreItemCollection storageCollection =
        new StoreItemCollection(
            new XmlReader[] { storageXml.CreateReader() }
        );
    EdmItemCollection conceptualCollection = new EdmItemCollection(conceptualReader);
    StorageMappingItemCollection mappingCollection =
        new StorageMappingItemCollection(
            conceptualCollection, storageCollection, mappingReader
        );

    var workspace = new MetadataWorkspace();
    workspace.RegisterItemCollection(conceptualCollection);
    workspace.RegisterItemCollection(storageCollection);
    workspace.RegisterItemCollection(mappingCollection);

    var connectionData = new EntityConnectionStringBuilder(connString);
    var connection = DbProviderFactories
        .GetFactory(connectionData.Provider)
        .CreateConnection();
    connection.ConnectionString = connectionData.ProviderConnectionString;

    return new EntityConnection(workspace, connection);
}

The resulting EntityConnection should be passed as a parameter when instantiating the context. You can modify it, so all ssdl models are modified by this function, not only the specified one.

JesseBuesking
  • 6,496
  • 4
  • 44
  • 89
Jan Matousek
  • 956
  • 3
  • 13
  • 15
  • Awesome! This solution did the trick for me. I am working with a DB2 database, which apparently considers schemas to be more on the level of SQLServer databases. Working with EF5. – Rick Arthur Aug 13 '13 at 23:08
  • 1
    I noticed that creating instances of those three collections is very expensive (time), so I store the modified Worspaces in a Dictionary, so I don't need to modify it for every Context instantiation. – Jan Matousek Aug 15 '13 at 08:59
  • I was already wrapping the EntityConnection creation so I only needed to add the part which overwrites the schema. Too bad a similar feature is not supported out of the box. Thanks. – JCallico Feb 24 '14 at 18:33
  • 1
    Not sure why my namespace was different (using EF 5 and Oracle), but this worked for me once I did: XNamespace StorageNS = "http://schemas.microsoft.com/ado/2009/11/edm/ssdl"; – Lee Richardson Apr 11 '14 at 19:52
  • See my note how to use this from a DbContext. – Dave Jul 08 '14 at 14:30
  • This works with my DbContext (see above answer) but I get compiler warnings saying that RegisterItemCollection method is obsolete and to "Construct MetadataWorkspace using constructor that accepts metadata loading delegates." I'm not sure how to do that. Has anyone else? – Dave Jul 16 '14 at 13:30
  • I had a problem with this but found a solution at http://stackoverflow.com/a/16888970/1396331. My XmlNamespace was causing an XmlException "The ':' character, hexadecimal value 0x3A, cannot be included in a name." My problem was resolved by adding curly braces around the url. – JDennis May 05 '15 at 02:14
  • If you are using stored procedures the Schema in the Function element needs to be modified too, so after the code to change the EntitySet elements, add this code to change the Function elements. ` // Change Schema name for Function foreach (var function in storageXml.Descendants(storageNS + "Function")) { var schemaAttribute = function.Attributes("Schema").FirstOrDefault(); if (schemaAttribute != null) { schemaAttribute.SetValue(schema); } }` – Robert Tanenbaum Sep 18 '15 at 14:56
  • Do you know how well this play with custom mappings? I have custom mappings in place to avoid mapping NUMBER(1,0) to Boolean and instead map it to byte. It all works fine at design time but fails when creating the mappingCollection: StorageMappingItemCollection mappingCollection = new StorageMappingItemCollection( conceptualCollection, storageCollection, mappingReader ); This is a link to the custom mappings: https://community.oracle.com/message/10145961#10145961 – Raciel R. Oct 14 '16 at 18:12
  • @RacielR. hey, did you manage to fix the '2019 error' that would rise with custom mappings when creating `StorageMappingItemCollection`? – Bassel Shmali Jul 19 '17 at 12:00
  • `XNamespace storageNS = storageXml.Attribute("xmlns").Value;` This allows a dynamic namespace to be used rather than a fixed one as in the example. – Richard Ockerby Feb 13 '18 at 17:06
  • http://chriseelmaa.com/entity-framework-dynamic-schema-changes-using-database-first-approach/ – Simple Fellow Jul 08 '18 at 18:36
11

I've managed to resolve this issue by using a brilliant library, located in CodePlex (courtesy of Brandon Haynes), named "Entity Framework Runtime Model Adapter", available here: http://efmodeladapter.codeplex.com/

I've tweaked it a bit, to fit our needs and without the need of replacing the designer code at all.

So, I'm good.

Thanks anyways, and especially to Brandon, amazing job!

nirpi
  • 715
  • 1
  • 9
  • 24
5

I need import data from postgres database. It by default use schema "public". So I use Entity Framework CTP 4 "Code first". It by default use schema "dbo". To change it in runtime I use:

public class PublicSchemaContext : DbContext
{
    protected override void OnModelCreating(System.Data.Entity.ModelConfiguration.ModelBuilder builder)
    {
        builder.Entity<series_categories>().MapSingleType().ToTable("[public].[series_categories]");
    }

    public DbSet<series_categories> series_categories { get; set; }
}

It work for select, insert, update and delete data. So next test in pass:

[Test]
        public void AccessToPublicSchema()
        {
            // Select
            var db = new PublicSchemaContext();
            var list = db.series_categories.ToList();
            Assert.Greater(list.Count, 0);
            Assert.IsNotNull(list.First().series_category);

            // Delete
            foreach (var item in db.series_categories.Where(c => c.series_category == "Test"))
                db.series_categories.Remove(item);
            db.SaveChanges();

            // Insert
            db.series_categories.Add(new series_categories { series_category = "Test", series_metacategory_id = 1 });
            db.SaveChanges();

            // Update
            var test = db.series_categories.Single(c => c.series_category == "Test");
            test.series_category = "Test2";
            db.SaveChanges();

            // Delete
            foreach (var item in db.series_categories.Where(c => c.series_category == "Test2"))
                db.series_categories.Remove(item);
            db.SaveChanges();
        }
  • This solution worked perfectly for me. In my case, all I needed to do was switch through table prefixes. However, the syntax for me was slightly different: modelBuilder.Entity().Map(x => x.ToTable(_tablePrefix + tableName)); Thanks Serg! – Adam Apr 06 '11 at 23:49
  • Glad to hear! Syntax will changing, since my code was write and tested on Entity Framework CTP4. – Sergey Makridenkov Apr 20 '11 at 14:26
2

Not an answer per se but a followup on Jan Matousek's Create[EntityConnection] method showing how to use from a DbContext. Note DB is the DbContext type passed to the generic repository.

 public TxRepository(bool pUseTracking, string pServer, string pDatabase, string pSchema="dbo")
{
    // make our own EF database connection string using server and database names
    string lConnectionString = BuildEFConnectionString(pServer, pDatabase);

    // do nothing special for dbo as that is the default
    if (pSchema == "dbo")
    {
        // supply dbcontext with our connection string
        mDbContext = Activator.CreateInstance(typeof(DB), lConnectionString) as DB;
    }
    else // change the schema in the edmx file before we use it!
    {
        // Create an EntityConnection and use that to create an ObjectContext,
        // then that to create a DbContext with a different default schema from that specified for the edmx file.
        // This allows us to have parallel tables in the database that we can make available using either schema or synonym renames.
        var lEntityConnection = CreateEntityConnection(pSchema, lConnectionString, "TxData");

        // create regular ObjectContext
        ObjectContext lObjectContext = new ObjectContext(lEntityConnection);

        // create a DbContext from an existing ObjectContext
        mDbContext = Activator.CreateInstance(typeof(DB), lObjectContext, true) as DB;
    }

    // finish EF setup
    SetupAndOpen(pUseTracking);
}
Dave
  • 1,822
  • 2
  • 27
  • 36
0

I was able to convert the solution from Jan Matousek to work in vb.net 2013 with entity framework 6. I will also try to explain how to use the code in vb.net.

We have a JD Edwards Database which uses different Schema's for each environment (TESTDTA, CRPDTA, PRODDTA). This makes switching between environments cumbersome as you have to manually modify the .edmx file if you want to change environments.

First step is to create a partial class that allows you to pass a value to the constructor of your entities, by default it uses the values from your config file.

Partial Public Class JDE_Entities
    Public Sub New(ByVal myObjectContext As ObjectContext)
        MyBase.New(myObjectContext, True)
    End Sub
End Class

Next create the function that will modify your store schema .ssdl file in memory.

 Public Function CreateObjectContext(ByVal schema As String, ByVal connString As String, ByVal model As String) As ObjectContext

    Dim myEntityConnection As EntityConnection = Nothing

    Try

        Dim conceptualReader As XmlReader = XmlReader.Create(Me.GetType().Assembly.GetManifestResourceStream(model + ".csdl"))
        Dim mappingReader As XmlReader = XmlReader.Create(Me.GetType().Assembly.GetManifestResourceStream(model + ".msl"))
        Dim storageReader As XmlReader = XmlReader.Create(Me.GetType().Assembly.GetManifestResourceStream(model + ".ssdl"))

        Dim storageNS As XNamespace = "http://schemas.microsoft.com/ado/2009/11/edm/ssdl"

        Dim storageXml = XDocument.Load(storageReader)
        Dim conceptualXml = XDocument.Load(conceptualReader)
        Dim mappingXml = XDocument.Load(mappingReader)

        For Each myItem As XElement In storageXml.Descendants(storageNS + "EntitySet")
            Dim schemaAttribute = myItem.Attributes("Schema").FirstOrDefault

            If schemaAttribute IsNot Nothing Then
                schemaAttribute.SetValue(schema)
            End If

        Next

        storageXml.Save("storage.ssdl")
        conceptualXml.Save("storage.csdl")
        mappingXml.Save("storage.msl")

        Dim storageCollection As StoreItemCollection = New StoreItemCollection("storage.ssdl")
        Dim conceptualCollection As EdmItemCollection = New EdmItemCollection("storage.csdl")

        Dim mappingCollection As StorageMappingItemCollection = New StorageMappingItemCollection(conceptualCollection, storageCollection, "storage.msl")


        Dim workspace = New MetadataWorkspace()
        workspace.RegisterItemCollection(conceptualCollection)
        workspace.RegisterItemCollection(storageCollection)
        workspace.RegisterItemCollection(mappingCollection)

        Dim connectionData = New EntityConnectionStringBuilder(connString)
        Dim connection = DbProviderFactories.GetFactory(connectionData.Provider).CreateConnection()

        connection.ConnectionString = connectionData.ProviderConnectionString

        myEntityConnection = New EntityConnection(workspace, connection)

        Return New ObjectContext(myEntityConnection)

    Catch ex As Exception

    End Try

End Function

Make sure that the storageNS namespace hardcoded value matches the one used in your code, you can view this by debugging the code and examining the storageXML variable to see what was actually used.

Now you can pass a new schema name, and different database connection info at runtime when you create your entities. No more manual .edmx changes required.

Using Context As New JDE_Entities(CreateObjectContext("NewSchemaNameHere", ConnectionString_EntityFramework("ServerName", "DatabaseName", "UserName", "Password"), "JDE_Model"))

            Dim myWO = From a In Context.F4801 Where a.WADOCO = 400100

            If myWO IsNot Nothing Then
                For Each r In myWO
                    Me.Label1.Text = r.WADL01
                Next
            End If
        End Using

These were the .net libraries used:

Imports System.Data.Entity.Core.EntityClient
Imports System.Xml
Imports System.Data.Common
Imports System.Data.Entity.Core.Metadata.Edm
Imports System.Reflection
Imports System.Data.Entity.Core.Mapping
Imports System.Data.Entity.Core.Objects
Imports System.Data.Linq
Imports System.Xml.Linq

Hope that helps anyone out there with the same issues.

0

I had a lot of problems getting this to work when using EF6 with an OData Data Service, so I had to find an alternate solution. In my case, I didn't really need to do it on the fly. I could get away with changing the schema when deploying to some test environments, and in the installer.

Use Mono.Cecil to rewrite the embedded .ssdl resources straight in the DLLs. This works just fine in my case.

Here is a simplified example of how you can do this:

var filename = "/path/to/some.dll"
var replacement = "Schema=\"new_schema\"";

var module = ModuleDefinition.ReadModule(filename);
var ssdlResources = module.Resources.Where(x => x.Name.EndsWith(".ssdl"));

foreach (var resource in ssdlResources)
{
    var item = (EmbeddedResource)resource;
    string rewritten;

    using (var reader = new StreamReader(item.GetResourceStream()))
    {
        var text = reader.ReadToEnd();
        rewritten = Regex.Replace(text, "Schema=\"old_schema\"", replacement);
    }

    var bytes = Encoding.UTF8.GetBytes(rewritten);
    var newResource = new EmbeddedResource(item.Name, item.Attributes, bytes);

    module.Resources.Remove(item);
    module.Resources.Add(newResource);
}
ogrim
  • 968
  • 9
  • 17