Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Modular and Extendable Applications Based on ASP.NET 5 and ExtCore Framework

0.00/5 (No votes)
16 Dec 2015 1  
Using ExtCore framework to create modular and extendable ASP.NET 5 applications

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:

  1. 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.
  2. 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:

  1. 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.
  2. 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

  1. 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.
  2. 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.
  3. 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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here