How to create a DbContext inside Visual Studio, or "What if you want something strange?"



    Starting with version 14.1, XtraReports has built-in support for the ORM Entity Framework. If earlier a developer had to use the standard BindingSource component to bind report elements to data and then manually write code to load data from the EF model, now he just needs to select a specific context (from the current project or assembly specified in the project's References) and specify the line to use connectivity. The EFDataSource component itself will create a context with the desired connection string and return data to the report.

    What does this give the developer, besides convenience:
    Firstly, it makes it easy to get started with XtraReports. There’s no need to think: “But how can we use data from the Entity Framework here?”. There is a simple wizard where it is enough to answer a couple of questions from the series “What exactly do you need”.
    Secondly, this makes it possible to see the real data in the Preview report in Visual Studio, which facilitates the actual creation of the report, since you can always control the result without running a separate application.
    Thirdly, the developer can now let the end users of his application create reports themselves using data from the EntityFramework model.



    Well, now that you’ve finished with the necessary preface, you can move on to more interesting things, namely, how it works and how it works.

    (Hereinafter, italics highlight some personal impressions designed to dilute boring and boring technical details) It would seem that making such a component does not cost anything. Draw a number of forms, come up with a simple API. However, there is a nuance - you need to get real data from the model inside Visual Studio. As Boromir said, “You can't just take and create an instance of a custom DbContext in the VisualStudio process.”

    By default, the Entity Framework saves the database connection string in app / web.config. Accordingly, when you try to create context from the Visual Studio process, this immediately leads to an error, since studio devenv.exe.config does not contain the connection string with which the context was created. This problem could be circumvented by forcing the report designer to create the desired default constructor for the data model, but this is not our way. It is desirable that in the simplest case (namely, this is the case when the data context was created as a result of Visual Studio and no changes were made to it), the developer did not need any additional actions.

    In addition, the Entity Framework supports a wide variety of DBMSs through third-party data providers. To use a data provider other than the default MS SQL, the Entity Framework must be correctly configured (via app.config or in code), and all the necessary assemblies must be accessible by putting them in the GAC or next to the project being launched. In the case of starting from Visual Studio, this is also not so easy to provide:
    Firstly, the already mentioned devenv.exe.config problem.

    Secondly, the assemblies of third-party DBMS providers are often downloaded by NuGet and stored locally in the project, and not in the GAC, which also makes it impossible to directly create the user-specified DbContext.

    So we need:
    1. Find the connection string with the given name in the user's project
    2. Create a custom DbContext, provided that there may not be the necessary constructor that accepts the connection string, and the GAC does not have the assemblies on which it depends (primarily EntityFramework.dll)
    3. Configure EntityFramework to work with a custom DBMS if it is different from the standard Microsoft SQL Server.


    By default, a connection string is created with the same name as the data model. However, its name can be easily changed and it is impossible to find out which connection string the developer wanted to use. There is no way out - you can only ask the developer himself. In general, when developing components, you should try to minimize the number of assumptions and assumptions. The less your component decides something for the developer, the better. It is always better to ask than to do wrong.



    Next, you need to get a specific connection string from the configuration file of the current project - ConfigurationManager will not help with this. But, the VisualStudio EnvDTE object model of automation will help us , and in particular the Microsoft.VSDesigner.VSDesignerPackage.IGlobalConnectionService interface (unfortunately, not documented):

    public interface IGlobalConnectionService    {
            DataConnection[] GetConnections(System.IServiceProvider serviceProvider, Project project);
            bool AddConnectionToServerExplorer(System.IServiceProvider serviceProvider, DataConnection connection);
            bool AddConnectionToAppSettings(System.IServiceProvider serviceProvider, Project project, DataConnection connection);
            bool RemoveConnectionFromAppSettings(System.IServiceProvider serviceProvider, Project project, DataConnection connection);
            bool UpdateConnectionInAppSettings(System.IServiceProvider serviceProvider, Project project, DataConnection oldConnection, DataConnection newConnection);
            bool IsValidPropertyName(System.IServiceProvider serviceProvider, Project project, string propertyName);
            bool RefreshApplicationSettings(System.IServiceProvider serviceProvider, Project project);    
    }
    


    Here we are interested in the GetConnections method, which returns an array of DataConnection objects. Moreover, this method finds strings not only in app.config, but also in Server Explorer and in machine.config. More information about the IGlobalConnectionService can be obtained from the reflector and assembly Microsoft.VSDesigner.

    Some kind of reflector (most often I use free ILSpy ) when developing components or plug-ins for the studio is an irreplaceable thing - as a rule, to do something, you have to “peek” with a debugger how similar functionality is implemented by Microsoft and then analyze the source codes of “peeped ”Assemblies. In our case, the requested service was suggested by the studio masters Add New DataSet and Add New ADO.NET Entity Data Model.

    And so, we have a connection string. But what to do with it if the user model does not have a constructor that accepts a connection string? The answer is both simple and complex at the same time - using Reflection.Emit, you need to make a dynamic assembly, create your own descendant of the user data model in it and make the necessary constructor in it. And then an attentive reader may ask a question: How do we create our own constructor in a descendant class if there is no constructor with the necessary parameters in the base class? The answer is again simple - in IL, calling the base class constructor is optional, and you can call any constructor of any ancestor in the hierarchy.

    .class public auto ansi DevExpress.DataAccess.Tests.Entity.AdventureWorksCFEntities
        extends [DevExpress.DataAccess.v14.2.Tests.MsSqlEF6]DevExpress.DataAccess.Tests.Entity.AdventureWorksCFEntities
    {
        .method public specialname rtspecialname 
            instance void .ctor (
                string ''
            ) cil managed 
        {
            .maxstack 2
            IL_0000: ldarg.0
            IL_0001: ldarg.1
            IL_0002: call instance void [EntityFramework]System.Data.Entity.DbContext::.ctor(string)
            IL_0007: ret
        } 
    } 
    

    Yes, it looks like a “dirty hack” - but nonetheless it works and is allowed by IL. Actually, an alternative way to “slip” the required connection string to EntityFramework is not a much more “clean” hack with the ConfigurationManager, described for example here .


    Creating a dynamic assembly, in addition to actually creating a descendant of the user model, also allows you to solve the problem with the reference to EntityFramework.dll. To create a call to System.Data.Entity.DbContext ::. Ctor (string), in any case, you need to load the EntityFramework.dll assembly and get the DbContext type from there. Looking for it in the GAC or in the current directory is ungrateful due to the fact that most likely it lies locally somewhere in the NuGet repository. Therefore, you have to use the studio automation object model again, in particular ITypesDiscoveryService , and look for EntityFramework.dll in the project references. Looking ahead - there you can find the assembly of a custom data provider for EntityFramework, if necessary.

    So, two of the three problems have been resolved. It remains the simplest, but time-consuming of all - registering an arbitrary data provider. As I already wrote, the Entity Framework is able to work with a variety of DBMSs through arbitrary data providers. The easiest way to use them is to register the necessary settings in app.config. Another way is to use the DBConfiguration class , which is an implementation of the Chain-of-Responsibility pattern and stores a list of resolvers IDbDependencyResolver . Each of them, in turn, implements the Service Locator pattern. Entity Framework during initialization procedure searches for DBConfiguration descendant in the same assembly as the data model, and if it finds one, it asks for the name of the used data provider, the DbProviderServices  and DbProviderFactory factories , and so on.

    Even the EF developers themselves justify themselves in the documentation - “Yes, we know that the Service Locator is an antipattern, but we know what we are doing and in our case its use is justified.”

    Here is an example of setting the Entity Framework to use SqlCE:

    
    public class SqlCEConfiguration : DbConfiguration {
        public SqlCEConfiguration() {
            SetProviderServices(  SqlCeProviderServices.ProviderInvariantName,
                SqlCeProviderServices.Instance);
            SetDefaultConnectionFactory(
                new SqlCeConnectionFactory(SqlCeProviderServices.ProviderInvariantName));
        }
    }
    


    Since the descendant of the DbConfiguration class  must be in the same assembly as the DbContext, it must therefore be created in the same dynamic assembly in which we created the descendant of the user model a little earlier. Here you have to write a little more complex code, different for different data providers. And this will require types from the assemblies of the corresponding data providers - they can be found through the same ITypesDiscoveryService , provided that the necessary assemblies are in the project references.

    Writing code in Reflection.Emit that creates the assembly with the required IL is a tedious task - however, the ReflectionEmitLanguage plugin can make it very easy to the Reflector. It does not create 100% working code, but it helps to avoid “stupid" errors when rewriting IL instructions.

    To summarize: Retrieving data from an arbitrary EF model inside the Visual Studio process is not easy, because for this you need to “slip” the connection string to it and configure the Entity Framework to work with an arbitrary data provider. If you really want to do this, you will have to :
    It is clear that within the framework of the article it is impossible to cover the entire experience of working with EF in our company. Various problems arose (and arise) and not all of them were solved, so not everything depends on us as component developers. But I believe that this approach, although it does not work in absolutely all cases, still improved the life for our users - software developers.

    There is another opinion - that components should not allow the developer to work with real data. There are no fundamental reasons for this, and Visual Studio itself allows you to do this (for example, when creating datasets). As I think, this is based on just such experience and understanding that it just won’t work out, since there is a high probability that you will encounter a problem in any of the areas beyond your control - inside Visual Studio, .NET Framework or Entity Framework.

    And finally, a final note: the described mechanism for creating DbContext inside Visual Studio first appeared in our WPF controls in the Scaffolding mechanism. It was not intended to receive data, but it originally came up with the idea of ​​temporarily assembling and generating a descendant of DbContext in it.

    That is all I wanted to tell in this article. Ready to answer any questions in the comments.

    PS. The author of the Corgi title photo is vk.com/kudma .

    Also popular now: