📜 ⬆️ ⬇️

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 the 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 all he needs is to select a specific context (from the current project or assembly specified in the project's References) and specify the string used connect. The EFDataSource component itself creates a context with the necessary connection string and returns the data to the report.

What does it give to the developer, besides convenience:
First, it facilitates an initial acquaintance with XtraReports. 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 “What exactly do you need” series.
Secondly, it gives the opportunity to see the real data in the Preview report in Visual Studio, which facilitates the actual creation of the report itself, since you can always monitor the result without launching a separate application.
Third, the developer can now let the end users of his application generate reports using data from the EntityFramework model.


')
And now, when the preface is complete, you can move on to more interesting things, namely, how it all works and how it works.

(Hereinafter, some personal impressions are given in italics, designed to dilute the boring and boring technical details) It would seem that this component is worthless. Draw a number of forms, come up with a simple API. However, there is a nuance here - you need to get real data from the model inside Visual Studio. As Boromir said, “You cannot just create and create an instance of a custom DbContext in the VisualStudio process”.

By default, the Entity Framework stores the database connection string in app / web.config. Accordingly, when attempting to create a context from the Visual Studio process, this immediately leads to an error, since the 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 the work of Visual Studio and no changes were made to it), no additional actions were required from the developer.

In addition, the Entity Framework supports a wide variety of databases through third-party data providers. To use any data provider other than the default MS SQL, the Entity Framework needs to be properly configured (via app.config or in code) and make available all necessary assemblies by putting them in the GAC or next to the project being launched. If launched from Visual Studio, this is also not so easy to ensure:
First, the already mentioned problem devenv.exe.config.

Secondly, third-party database provider assemblies are often swung by NuGet and are 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 in the user project a connection string with the specified name
  2. Create a custom DbContext, provided that the necessary constructor does not accept the connection string, and the GAC does not have the assemblies on which it depends (primarily EntityFramework.dll)
  3. Configure EntityFramework to work with the user 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 exactly 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 solves 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 - the ConfigurationManager will not help with this. But, the object automation model VisualStudio EnvDTE will help us, and in particular the interface Microsoft.VSDesigner.VSDesignerPackage.IGlobalConnectionService (unfortunately, undocumented):

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 machine.config. More information about the IGlobalConnectionService can be found in the reflector and Microsoft.VSDesigner assembly.

Any reflector (most often I use the free ILSpy ) when developing components or plug-ins to the studio is irreplaceable - as a rule, to do something, you have to “pry” the debugger, as Microsoft has implemented similar functionality and then analyze the source code of “peeped ”Builds. In our case, the required 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 - with the help of Reflection.Emit you need to make a dynamic assembly, create your descendant of a custom data model in it and make the necessary constructor in it. And here the attentive reader may ask the question: How will we create our constructor in the descendant class, if the constructor with the necessary parameters is not 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 necessary connection string for EntityFramework is not a much more “clean” hack with the Configuration Manager, for example, described here .


Creating a dynamic assembly, in addition to actually creating a descendant of a user model, also allows you to solve the problem with reference to EntityFramework.dll. To create a call to System.Data.Entity.DbContext ::. Ctor (string), in any case, you need to load the assembly EntityFramework.dll and get the type DbContext from there. Looking for it in the GAC or in the current directory is a thankless task because 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 reference. Looking ahead - there you can also find an assembly of a custom data provider for EntityFramework, if necessary.

So, two of the three problems solved. The simplest but most laborious of all is to register an arbitrary data provider. As I already wrote, the Entity Framework is able to work with a variety of DBMS through arbitrary data providers. The easiest way to use them is to register the necessary settings in the app.config. Another way is to use the DBConfiguration class, which is an implementation of the Chain-of-Responsibility pattern and storing the resolver list IDbDependencyResolver . Each of them, in turn, implements the Service Locator pattern. During the initialization procedure, the Entity Framework searches for a DBConfiguration descendant in the same assembly as the data model, and if it finds it, it requests the name of the data provider used, the DbProviderServices and DbProviderFactory factories , and so on.

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

Here is an example of setting up 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 lie in the same assembly with DbContext, accordingly, it must 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 for this, you will need types from the assemblies of the respective data providers - they can be found through the same ITypesDiscoveryService , provided that the necessary assemblies are in the project references.

Writing code on Reflection.Emit, which creates an assembly with the required IL, is quite a chore - however, the ReflectionEmitLanguage plugin for Reflector can greatly facilitate it. It does not create a 100% working code, but it helps to avoid “silly” mistakes when rewriting IL instructions.

To summarize: Retrieving data from an arbitrary EF model within the Visual Studio process is not easy, because for this you need to “slip” the required connection string 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 this article it is impossible to highlight 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 it to be done (for example, when creating datasets). As I think, this is based on just such experience and an understanding of the fact that it will not work out just like that, since there is a rather high probability that you will encounter a problem in any of the areas uncontrolled by yourself - inside Visual Studio, .NET Framework or Entity Framework.

And finally, the last remark: 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 had an idea with a temporary assembly and generation of a descendant of DbContext in it.

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

Ps. The author of the title photo with corgi - vk.com/kudma .

Source: https://habr.com/ru/post/244707/


All Articles