Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to retrieve a MetadataWorkspace without having a connection to a database?

I am writing a test library that needs to traverse the Entity Framework MetadataWorkspace for a given DbContext type. However, as this is a test library I would rather not have a connection to the database - it introduces dependencies that may not be available from the test environment.

When I try to get a reference to the MetadataWorkspace like so:

var metadata = ((IObjectContextAdapter)context).ObjectContext.MetadataWorkspace;

I get a SqlException:

An exception of type 'System.Data.SqlClient.SqlException' occurred in System.Data.dll but was not handled in user code

Additional information: A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections. (provider: SQL Network Interfaces, error: 26 - Error Locating Server/Instance Specified)

Is it possible to do what I want without a connection string?

like image 202
Alex Avatar asked Feb 24 '15 18:02

Alex


1 Answers

Yes you can do this by feeding context a dummy connection string. Note that usually when you call parameterless constructor of DbContext, it will look for connection string with the name of your context class in app.config file of main application. If that is the case and you cannot change this behavior (like you don't own the source code of context in question) - you will have to update app.config with that dummy conneciton string (can be done in runtime too). If you can call DbContext constructor with connection string, then:

var cs = String.Format("metadata=res://*/{0}.csdl|res://*/{0}.ssdl|res://*/{0}.msl;provider=System.Data.SqlClient;provider connection string=\"\"", "TestModel");
using (var ctx = new TestDBEntities(cs)) {
    var metadata = ((IObjectContextAdapter)ctx).ObjectContext.MetadataWorkspace;
    // no throw here
    Console.WriteLine(metadata);                
}

So you providing only parameters important to obtain metadata workspace, and providing empty connection string.

UPDATE: after more thought, you don't need to use such hacks and instantiate context at all.

public static MetadataWorkspace GetMetadataWorkspaceOf<T>(string modelName) where T:DbContext {
    return new MetadataWorkspace(new[] { $"res://*/{modelName}.csdl", $"res://*/{modelName}.ssdl", $"res://*/{modelName}.msl" }, new[] {typeof(T).Assembly});
}

Here you just use constructor of MetadataWorkspace class directly, passing it paths to target metadata elements and also assembly to inspect. Note that this method makes some assumptions: that metadata artifacts are embedded into resources (usually they are, but can be external, or embedded under another paths) and that everything needed is in the same assembly as Context class itself (you might in theory have context in one assembly and entity classes in another, or something). But I hope you get the idea.

UPDATE2: to get metadata workspace of code-first model is somewhat more complicated, because edmx file for that model is generated at runtime. Where and how it is generated is implementation detail. However, you can still get metadata workspace with some efforts:

    public static MetadataWorkspace GetMetadataWorkspaceOfCodeFirst<T>() where T : DbContext {
        // require constructor which accepts connection string
        var constructor = typeof (T).GetConstructor(new[] {typeof (string)});
        if (constructor == null)
            throw new Exception("Constructor with one string argument is required.");
        // pass dummy connection string to it. You cannot pass empty one, so use some parameters there
        var ctx = (DbContext) constructor.Invoke(new object[] {"App=EntityFramework"});
        try {                
            var ms = new MemoryStream();
            var writer = new XmlTextWriter(ms, Encoding.UTF8);
            // here is first catch - generate edmx file yourself and save to xml document
            EdmxWriter.WriteEdmx(ctx, writer);
            ms.Seek(0, SeekOrigin.Begin);
            var rawEdmx = XDocument.Load(ms);
            // now we are crude-parsing edmx to get to the elements we need
            var runtime = rawEdmx.Root.Elements().First(c => c.Name.LocalName == "Runtime");                
            var cModel = runtime.Elements().First(c => c.Name.LocalName == "ConceptualModels").Elements().First();
            var sModel = runtime.Elements().First(c => c.Name.LocalName == "StorageModels").Elements().First();
            var mModel = runtime.Elements().First(c => c.Name.LocalName == "Mappings").Elements().First();

            // now we build a list of stuff needed for constructor of MetadataWorkspace
            var cItems = new EdmItemCollection(new[] {XmlReader.Create(new StringReader(cModel.ToString()))});
            var sItems = new StoreItemCollection(new[] {XmlReader.Create(new StringReader(sModel.ToString()))});
            var mItems = new StorageMappingItemCollection(cItems, sItems, new[] {XmlReader.Create(new StringReader(mModel.ToString()))});
            // and done
            return new MetadataWorkspace(() => cItems, () => sItems, () => mItems);
        }
        finally {
            ctx.Dispose();
        }
    }
like image 69
Evk Avatar answered Sep 28 '22 06:09

Evk