📜 ⬆️ ⬇️

Modular ASP.NET 5 Application

For quite a long time, when developing websites for my clients, I used my own simple CMS (Platform). It was written on ASP.NET + MVC and had closed source code. With the advent of the first beta of the new ASP.NET 5, I decided to rewrite my system on this technology to make it cross-platform and, ultimately, put it on GitHub. Since the technology is very new, there was practically no information on this issue, so the solution of some problems was found either by chance or in the process of studying the source codes of ASP.NET 5 itself.

To simplify, I prepared and also posted on GitHub a special test solution - AspNet5ModularApp . Basically, I will rely on him in this article, but I will also touch on some of the techniques and ideas that I used in Platformus (partly in the hope of getting any comments on them).



Solving the main tasks


If we omit specific questions specifically for developing CMS, the main task for me was (and still remains, by the way) a competent modular architecture of the project.
')
I decided that the main web application should not have any controllers, views or resources (scripts, styles, images, etc.), should not know about the data or how to store them, but should know about the directory with extensions (assembly set ). In this case, his only task is to find, load and initialize all extensions.

Extensions, in turn, must implement some common interface and may contain, in fact, anything. (But, since almost every extension in my case will work with data, I introduced an additional level of abstraction describing this mechanism. Thanks to this, all extensions can work with data unified and in a single context, which is quite important. I will describe in detail it's lower.)

In most cases, extensions should contain controllers and views, so first of all I tried to make the controller and view work from a dynamically loaded assembly (i.e., from an assembly that is not explicitly referenced in the project.json main web application and which loaded from the directory with extensions after it starts).

There were no problems with controllers. Simply implement the IAssemblyProvider interface, copy the assemblies from DefaultAssemblyProvider and add those assemblies that are dynamically loaded from the directory with extensions (see AspNet5ModularApp.ExtensionAssemblyProvider ), and then simply register the new implementation in ConfigureServices (see AspNet5ModularApp.Startup ). The only point: by default, MVC is looking for controllers in assemblies that refer to something like Microsoft.AspNet.Mvc. Since in my case almost all projects refer to this build (this is bad, but I described below why so far this is the case), the search takes place in all of them, which probably negatively affects the speed, therefore, in any case, it is necessary to fix it (although, of course, in our test solution there are no performance problems).

I had to tinker with the performances. As far as I know now, you can either make representations resources (by adding, for example, the string “resource”: “Views / **” in project.json), or use the preliminary compilation of views (by adding the RazorPreCompilation class inherited from RazorPreCompileModule). I prefer the second option, because in this case, you can, first, use views typed by your own types (I mean types defined inside the assembly, which contains the view itself — in the case of resource views, these types are not will be found at runtime during compilation, although, as far as I understand from the answer to my question on GitHub ASP.NET, this problem can also be solved), and secondly, they simply do not require runtime compilation and therefore load faster on the first access. . Well, all the errors in views also become apparent at the compilation stage.

The main web application of our AspNet5ModularApp test solution simultaneously supports both of these options, the corresponding behavior is set using the AddPrecompiledRazorViews and AddRazorOptions functions (see Startup.cs ).

If it is enough to simply install assemblies containing them for precompiled views, you must implement the IFileProvider interface with resource representations (see AspNet5ModularApp.CompositeFileProvider ) and specify the main web application as the file sources (by default this is the case) and, in addition, all dynamically loaded assemblies containing views. By the way, resources can be not only views, but also scripts, styles, images, and so on. After the assemblies that contain them are added to the CompositeFileProvider, they will be available along the paths along which they are located in their assemblies.

ExtensionA illustrates the first option (with resource views), and ExtensionB the second option (with precompiled views).

Here it is also necessary to tell about a couple of problems that I have not found a solution to.

First, different parts of ASP.NET 5 use different versions of the System. * Builds, so if you connect Microsoft.AspNet.Mvc in one project and try to connect System.Runtime in another project, you can get an error that is used different versions of the same library. At the moment, AspNet5ModularApp is built on the basis of the 8th ASP.NET 5 beta, so I think that by the release this will be fixed. In the meantime, I simply prescribed Microsoft.AspNet.Mvc in all projects (except those that work with the Entity Framework - the Entity Framework itself is enough) to get the same set of dependencies. I agree, this is very bad, but it allowed us not to waste time on trifles.

The second (more serious, perhaps) problem is that I copy the extension assemblies into a directory with extensions and, if the assembly uses something that the main web application does not use, I have to copy the corresponding dependencies there as well. For example, I had to put it in the directory with the System.Reflection.dll and System.Reflection.TypeExtensions.dll extensions (otherwise I get an exception when I try to load assemblies that have the dependencies mentioned above). But the worst thing is that I could not find such a set of assemblies, which would allow EntityFramework.Sqlite to earn. Accordingly, I had to include an explicit link to EntityFramework.Sqlite in the project.json of the main application (and I wrote above that I don’t want it to know about the data at all, not to mention a specific implementation), which really annoys me. (By the way, everything will be fine if you register the assemblies in .dnx in the GAC, but it seems to me that this is wrong.)

Next, I began to deal with the data and its storage. I wanted the extensions to work with different data sources, and for defining a specific implementation it was enough just to copy the necessary assemblies into the directory with the extensions.

In the AspNet5ModularApp.Models.Abstractions project , I defined the base interface for the model - IEntity. In the AspNet5ModularApp.Data.Abstractions project , I defined 3 basic interfaces - IStorageContext, IStorage and IRepository. Their purpose is best illustrated by the AspNet5ModularApp.Data.EF.Sqlite project, which contains implementations of these interfaces for working with the Sqlite database using the Entity Framework 7. Also, this project defines the IModelRegistrar interface, which allows extensions to register their models in a single context (see AspNet5ModularApp.Data.EF.Sqlite.StorageContext.OnModelCreating ).

The general principle of operation is as follows. An extension can consist of several projects: a project with controllers and views, a project with models, a project with abstractions of repositories for working with a data source (one for each model) and a project with implementations of these abstractions (one project for each data source). A project with controllers and views knows about repository models and abstractions, but does not know about their implementations for specific data sources. Thus, in the controller's constructor, you can ask the built-in ASP.NET 5 DI to provide an accessible instance of IStorage and, further, request an available implementation of a certain repository from it via its interface. (Of course, in order for the built-in DI to find an available IStorage implementation, it needs to be told about it, which is done in AspNet5ModularApp.ExtensionB.ExtensionB.ConfigureServices . In order not to do this in each extension, in Platformus I rendered the general functionality into a separate extension - Barebone. )

Result


What happened as a result. If you close your eyes to the problems with dependencies, about which I wrote and which, most likely, will be solved soon, then, in my opinion, I have found answers to all my questions. I can extend the capabilities of an application by simply copying assemblies to a directory with extensions, extensions can have strongly typed representations and a single context for working with a data source. It is not difficult to make it possible to add and remove extensions directly in the process of the application, if necessary. You can also significantly improve performance by adding caching to the type search engines.

I would be happy to comment and comment, I will also be happy to explain those points that I could not well describe in the article.

Links and thanks


Link to AspNet5ModularApp.

I would like to thank the user GitHub github.com/leo9223 who really helped me by showing this project . This project, in turn, helped me deal with resource-ideas, although I could not get it to work. Now I know why. When creating EmbeddedFileProvider from assemblies with extensions, basic namespaces were not specified there, therefore no representations could be found. I found a clue here . Also, the answers to my questions on GitHub were helped by the ASP.NET developers, thanks for their patience and attention.

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


All Articles