Introduction
While working on Platformus project (it is free, open source and cross-platform CMS based on ASP.NET 5), one of the most difficult tasks for me is modular structure of the project. I want it to be possible to add/remove extensions just by copying them into the extensions folder. Also, my extensions may consist of controllers, view models, views, storage models and repositories, etc. At some point, I decided to extract this functionality into a separate ExtCore framework project to be able to reuse it later in other projects.
What is ExtCore Framework
ExtCore framework consists of 2 required NuGet packages:
ExtCore.WebApplication
ExtCore.Infrastructure
ExtCore.WebApplication
Contains classes for discovering and loading of extensions, controllers, views, resources, etc.
ExtCore.Infrastructure
Describes an extension with IExtension
interface. All extensions must implement this interface in order to be discovered.
Also, ExtCore has one optional extension to work with storage (now it supports only SQLite and MS SQL Server but it is very easy to add some other storage support). 5 more NuGet packages:
ExtCore.Data
ExtCore.Data.Models.Abstractions
ExtCore.Data.Abstractions
ExtCore.Data.EntityFramework.Sqlite
ExtCore.Data.EntityFramework.SqlServer
ExtCore.Data.Models.Abstractions
Describes a storage entity with IEntity
interface. All entities must implement this interface.
ExtCore.Data.Abstractions
Describes basic storage context, storage and repository with IStorageContext
, IStorage
and IRepository
interfaces.
ExtCore.Data.EntityFramework.Sqlite and ExtCore.Data.EntityFramework.SqlServer
Implement IStorageContext
, IStorage
and IRepository
for appropriate storage. Implementation of IStorage
interface will search through all the assemblies for given IRepository
implementation. If implementation is found, it will instantiate a repository instance and return.
ExtCore.Data
The main part of the ExtCore.Data
extension. It contains implementation of IExtension
, which automatically searches for available implementation of IStorage
and injects it using built-in ASP.NET 5 DI. So every controller will be able to get an instance of the IStorage
implementation to work with storage.
More About Extension Structure
When you create your own extension, you may (and you should if you want to have the unified architecture), follow the next extension structure (where X
is name of the extension):
YourApplication.X
YourApplication.X.Data.Models
YourApplication.X.Data.Abstractions
YourApplication.X.Data.SpecificStorageA
YourApplication.X.Data.SpecificStorageB
YourApplication.X.Data.SpecificStorageC
YourApplication.X.Frontend
YourApplication.X.Backend
- etc.
As you can see, the structure is very similar to ExtCore.Data
extension’s one but we also can see some YourApplication.X.Frontend
and YourApplication.X.Backend
here. These parts of YourApplication.X
extension contain its UI (controllers, views, js, CSS, etc) for frontend and backend (but you can have different layer or layers in your application).
It is important to know how you can store views and static resources (as js, CSS, images, etc) in your extensions and how you can use them later.
There are two options for storing views:
- You can store the views as compiled as resources. In this case, you can’t have views strongly typed if the type is defined inside anything that the main web application doesn’t have dependency to. In other words, you can only use standard types as
string
or IEnumerable
for view models, otherwise it will not be possible to compile the views at runtime. - You can store the views as precompiled ones. In this case, you can use any existing type for view models. Also, it will not take time to compile the views at runtime. It is the preferred option for me.
There is only one option for storing static resources. It is compiling static resources as assembly resources by adding something like this in your project.json:
"resource": "Your/Static/Content/Path/**"
After that, you can use URL like /resource?name=your.static.content.path.someimagename.png to get the resource by HTTP using ExtCore
(in future releases, I will make it possible to get resources like regular files by their names).
How It Works
Please take a look at ExtCore.WebApplication.Startup class.
First of all, in ConfigureServices
method, we load all the assemblies from the extensions folder of the application:
IEnumerable<Assembly> assemblies = AssemblyManager.LoadAssemblies(
this.applicationBasePath.Substring
(0, this.applicationBasePath.LastIndexOf("src")) + "artifacts\\bin\\Extensions",
this.assemblyLoaderContainer,
this.assemblyLoadContextAccessor
);
After assemblies are loaded, we store them into the global cache:
ExtensionManager.SetAssemblies(assemblies);
Now using ExtensionManager
class, we can access the array of the available assemblies and extensions from any place, so all extensions can have information about each other.
Next, we have to do is to allow Razor to resolve views stored in the extensions. As I described above, there are two options to store the views inside the extensions. ExtCore
supports both of them.
This is to include views compiled as resources:
.AddPrecompiledRazorViews(ExtensionManager.Assemblies.ToArray());
This is to include precompiled views:
services.Configure<RazorViewEngineOptions>(options =>
{
options.FileProvider = this.GetFileProvider(this.applicationBasePath);
}
);
After that, we have to call SetConfigurationRoot
and ConfigureServices
methods of all the extensions:
foreach (IExtension extension in ExtensionManager.Extensions)
{
extension.SetConfigurationRoot(this.configurationRoot);
extension.ConfigureServices(services);
}
And the last one, we should tell MVC how to discover our controllers inside the extensions:
services.AddTransient<DefaultAssemblyProvider>();
services.AddTransient<IAssemblyProvider, ExtensionAssemblyProvider>();
ExtensionAssemblyProvider
will copy all the assemblies found by DefaultAssemblyProvider
and add assemblies stored in our ExtensionManager
.
In Configure
method, we call Configure
and RegisterRoutes
methods of all the extensions:
foreach (IExtension extension in ExtensionManager.Extensions)
extension.Configure(applicationBuilder);
applicationBuilder.UseMvc(routeBuilder =>
{
routeBuilder.MapRoute(name: "Resource", template: "resource",
defaults: new { controller = "Resource", action = "Index" });
foreach (IExtension extension in ExtensionManager.Extensions)
extension.RegisterRoutes(routeBuilder);
}
);
How to Use
To use ExtCore
, it is enough to add reference to ExtCore.WebApplication
to your main app’s project.json, make your main app’s Startup
class inherited from ExtCore.WebApplication.Startup
and add reference to ExtCore.Infrastructure
to your extension’s project.json. After that, you can put your extension’s DLL file to /artifacts/bin/extensions folder and it will be automatically discovered next time application is started.
I have prepared the sample application, you can use: https://github.com/ExtCore/ExtCore-Sample. It contains 2 extensions:
- Extension A. It shows how to add views to assembly as resources (and also, it shows that the main web application will find this views). Also, it shows how to get and display names of all available extensions.
- Extension B. It shows how to use precompiled strongly typed views with custom view model classes defined inside the extension. Also, it shows how to work with the storage (Sqlite in this case).
You can also download my ready to use sample project. It contains everything you need to run ExtCore
-based web application from Visual Studio 2015, including SQLite database with the test data.
Known Issues
- Different AspNet5 projects use different versions of System.Xxx, I decided just to put
Microsoft.AspNet.Mvc
as dependency in all the projects to have the same set of the dependencies in all projects, but this is WRONG. So Microsoft.AspNet.Mvc
should be replaced with, for example, some version of System.Linq
, etc., but in this case, I got compilation errors because of different versions of System.Xxx in different projects. I will fix this later and appreciate any help. - Because the main web application doesn’t have some dependencies the modules need, I had to put, for example, System.Reflection.dll and System.Reflection.TypeExtensions.dll to the folder with extensions. I really don’t like it and have to solve it too.
- The biggest problem is that I couldn’t find the correct set of assemblies that
EntityFramework.Sqlite
needs to run (I tried to copy a lot of them to the extensions folder but with no luck), so I decided just to add EntityFramework.Sqlite
as dependency to the main web application, but I really don’t like it very much, but it works now. So this is what I have to solve too.
Afterword
I will be happy if my work will be useful for you and feel free to contact me with questions or ideas. Also, you can write to me in Gitter: https://gitter.im/ExtCore/ExtCore.