Preparing ASP.NET Core: more about working with a modular framework

    We continue our column on ASP.NET Core with another publication by Dmitry Sikorsky ( DmitrySikorsky ) - the head of the Ubranians company from Ukraine. This time, Dmitry continues the story of his experience in developing a modular cross-platform framework based on ASP.NET Core. Previous articles from the column can always be read at #aspnetcolumn - Vladimir Yunev
    In a previous article, I already talked about ExtCore, a small framework for developing modular and extensible applications on ASP.NET Core. In this article I will try to dwell in more detail on the process of developing an application based on it.

    Main application


    First, create a new empty project on ASP.NET Core 1.0:



    As a result, we get a ready-to-use project. It remains only to delete the file Project_Readme.html. Our solution explorer should now look something like this:



    aspnetcolumngithubAdvice! You can try it all by yourself or by downloading the source code from GitHub https://github.com/ExtCore/ExtCore-Sample .
    To connect the ExtCore framework to our project, you need to add links to the ExtuCore.Infrastructure and ExtCore.WebApplication NuGet packages in project.json. Also, since in this example we will work with the database, we will add links to the ExtCore.Data extension components there: (ExtCore.Data, ExtCore.Data.Abstractions, ExtCore.Data.EntityFramework.Sqlite, ExtCore.Data. Models.Abstractions). (We also need links to familiar packages for MVC applications, such as Microsoft.AspNet.Mvc.) As a result, our project.json should look like this:

    {
      "commands": {
        "web": "Microsoft.AspNet.Server.Kestrel"
      },
      "dependencies": {
        "EntityFramework.Sqlite": "7.0.0-rc1-final",
        "ExtCore.Data": "1.0.0-alpha7",
        "ExtCore.Data.Abstractions": "1.0.0-alpha7",
        "ExtCore.Data.EntityFramework.Sqlite": "1.0.0-alpha7",
        "ExtCore.Data.Models.Abstractions": "1.0.0-alpha7",
        "ExtCore.Infrastructure": "1.0.0-alpha7",
        "ExtCore.WebApplication": "1.0.0-alpha7",
        "Microsoft.AspNet.Diagnostics": "1.0.0-rc1-final",
        "Microsoft.AspNet.Diagnostics.Entity": "7.0.0-rc1-final",
        "Microsoft.AspNet.Mvc": "6.0.0-rc1-final",
        "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-rc1-final",
        "Microsoft.AspNet.Server.Kestrel": "1.0.0-rc1-final",
        "Microsoft.Extensions.Configuration.Abstractions": "1.0.0-rc1-final",
        "Microsoft.Extensions.Configuration.Json": "1.0.0-rc1-final",
        "Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.0.0-rc1-final"
      },
      "exclude": [
        "wwwroot"
      ],
      "frameworks": {
        "dnx451": { },
        "dnxcore50": { }
      },
      "publishExclude": [
        "**.user",
        "**.vspscc"
      ],
      "version": "1.0.0-*",
      "webroot": "wwwroot"
    }
    

    Now it remains only to inherit the Startup class from ExtCore.WebApplication.Startup:

    public class Startup : ExtCore.WebApplication.Startup
    {
      public Startup(IHostingEnvironment hostingEnvironment, IApplicationEnvironment applicationEnvironment, IAssemblyLoaderContainer assemblyLoaderContainer, IAssemblyLoadContextAccessor assemblyLoadContextAccessor, ILibraryManager libraryManager)
        : base(hostingEnvironment, applicationEnvironment, assemblyLoaderContainer, assemblyLoadContextAccessor, libraryManager)
      {
        IConfigurationBuilder configurationBuilder = new ConfigurationBuilder()
          .AddJsonFile("config.json");
        this.configurationRoot = configurationBuilder.Build();
      }
      public override void ConfigureServices(IServiceCollection services)
      {
        base.ConfigureServices(services);
      }
      public override void Configure(IApplicationBuilder applicationBuilder, IHostingEnvironment hostingEnvironment)
      {
        if (hostingEnvironment.IsEnvironment("Development"))
        {
          applicationBuilder.UseBrowserLink();
          applicationBuilder.UseDeveloperExceptionPage();
          applicationBuilder.UseDatabaseErrorPage();
        }
        else
        {
          applicationBuilder.UseExceptionHandler("/");
        }
        base.Configure(applicationBuilder, hostingEnvironment);
      }
    }
    

    In the constructor of the Startup class, we initialize the configurationRoot variable defined in the base class ExtCore.WebApplication.Startup. This is necessary to provide the ExtCore framework with access to configuration parameters (in our case, the only source of configuration parameters is the config.json file). For example, the ExtCore.Data extension thus receives the parameter Data: DefaultConnection: ConnectionString (database connection string). You can also configure other extensions (including your own).

    Let's create the config.json file in the root of the project:

    {
      "Data": {
        "DefaultConnection": {
          "ConnectionString": "Data Source=../db.sqlite"
        }
      },
      "Extensions": {
        "Path": "artifacts\\bin\\Extensions"
      }
    }
    

    The Extensions: Path parameter determines the path along which the folder with the extensions is located in the file system (relative to the application root).

    That's all, at this point we can build and run our application. We will get error 404 and it will be correct, because we do not have any routes or controllers yet.

    Extensions


    Now let's create 2 extensions. The first extension (ExtensionA) on its only page (the main page of our application) will simply display a list of all available extensions. Also in it, we test the use of static content in the form of resources using an example CSS file. The second extension (ExtensionB) will display the records described by the model from the database. Everything is simple.

    Extension ExtensionA


    Let's create another WebApplication.ExtensionA project (note that this time it is a class library):


    In order to conveniently separate the projects in the solution related to various extensions, we will move our project to the solution folder with the name ExtensionA, having previously created it.
    First, edit project.json again. Add a link to ExtCore.Infrastructure (contains a description of the IExtension interface; in addition, ExtCore downloads and uses only those assemblies that have a link to this package) and to Microsoft.AspNet.Mvc. In this extension, we will use the view and CSS file added as resources (I described this in more detail in the previous article, which is referenced above), so you must also add the corresponding entry. Here's what you should get:

    {
      "dependencies": {
        "ExtCore.Infrastructure": "1.0.0-alpha7",
        "Microsoft.AspNet.Mvc": "6.0.0-rc1-final"
      },
      "frameworks": {
        "dnx451": { },
        "dnxcore50": { }
      },
      "resource": [ "Styles/**", "Views/**" ],
      "version": "1.0.0-*"
    }
    

    Next, we implement the IExtension interface:

    public class ExtensionA : IExtension
    {
      private IConfigurationRoot configurationRoot;
      public string Name
      {
        get
        {
          return "Extension A";
        }
      }
      public void SetConfigurationRoot(IConfigurationRoot configurationRoot)
      {
        this.configurationRoot = configurationRoot;
      }
      public void ConfigureServices(IServiceCollection services)
      {
      }
      public void Configure(IApplicationBuilder applicationBuilder)
      {
      }
      public void RegisterRoutes(IRouteBuilder routeBuilder)
      {
        routeBuilder.MapRoute(name: "Extension A", template: "", defaults: new { controller = "ExtensionA", action = "Index" });
      }
    }
    

    In the RegisterRoutes method, we add a route for the main page of our application.
    Now we’ll add a controller with a single Index method that will pass to the view a set of names of all the ExtCore extensions loaded, for which the ExtensionManager class is used:

    public class ExtensionAController : Controller
    {
      public ActionResult Index()
      {
        return this.View(ExtensionManager.Extensions.Select(e => e.Name));
      }
    }
    

    In turn, the view displays this set as follows:

      @foreach (var item in this.Model) {
    • @item
    • }

    The last thing to do is add the typography.css styles file to the Styles folder. Above, in the project.json file, we specified that the entire contents of the Styles and Views folders will be added to the assembly as resources. ExtCore will detect these resources and make it possible to use them in a manner similar to using physical files. That is, we can connect our CSS file in any extension in this way:


    It should only be borne in mind that the tree structure of the file system turns into a “flat” structure of text names (case matters!).

    Our ExtensionA extension is ready. To test its operation, it is enough to either add a link to it in project.json of the main application, or collect it as a dll file and copy it to the folder with extensions (we specified it in config.json earlier).

    ExtensionB


    Here we need 4 whole new projects: WebApplication.ExtensionB, WebApplication.ExtensionB.Data.Abstractions, WebApplication.ExtensionB.Data.EntityFramework.Sqlite and WebApplication.ExtensionB.Data.Models. As in the first extension, we group them in the solution folder (named ExtensionB).

    WebApplication.ExtensionB
    In this project we will place the IExtension interface implementation, controller, view models and views.
    The implementation of the IExtension interface is similar to that of the previous extension. Let's go straight to the controller:

    public class ExtensionBController : Controller
    {
      private IStorage storage;
      public ExtensionBController(IStorage storage)
      {
        this.storage = storage;
      }
      public ActionResult Index()
      {
        return this.View(new IndexViewModelBuilder().Build(this.storage.GetRepository().All()));
      }
    }
    

    Since in this extension we need to get some records from the database, we will use the capabilities of the ExtCore.Data extension for this. In the controller’s constructor, we’ll query the built-in ASP.NET DI for the available implementation of the IStorage interface (which the ExtCore.Data extension detected and registered earlier). Next, we’ll ask for our own implementation of our own IItemRepository interface for a specific repository (in our case, this is a SQLite database) and call the All method to get all the records. Next, we transform the models from the database into view models for display in the view.
    Instead of using resource views, in this extension we will use precompiled views. To do this, add the RazorPreCompilation class to the / Compiler / PreProcess folder:

    public class RazorPreCompilation : RazorPreCompileModule
    {
      protected override bool EnablePreCompilation(BeforeCompileContext context) => true;
    }
    

    This will give us the opportunity to use our own (i.e. declared within our extension) classes for view models. (For more information on precompiled views, see the previous article.)
    WebApplication. ExtensionB. Data Abstractions
    This project contains the interface of a single repository for working with models of the Item type (see below):

    public interface IItemRepository : IRepository
    {
      IEnumerable All();
    }
    

    In our example, the interface describes just one method for retrieving all records.
    WebApplication. ExtensionB. Data EntityFramework. Sqlite
    In this project, we implement the IItemRepository interface for a specific repository - SQLite databases:

    public class ItemRepository : RepositoryBase, IItemRepository
    {
      public IEnumerable All()
      {
        return this.dbSet.OrderBy(i => i.Name);
      }
    }
    

    Since the extension does not work directly with a specific implementation, but uses only abstractions, we can support several types of storages at the same time and add new ones without the need to change the code of the extension itself.
    Also, registration of the models used in the extension and storage settings are also performed here. To do this, use a class that implements the IModelRegistrar interface:

    public class ModelRegistrar : IModelRegistrar
    {
      public void RegisterModels(ModelBuilder modelbuilder)
      {
        modelbuilder.Entity(etb =>
        {
          etb.HasKey(e => e.Id);
          etb.Property(e => e.Id);
          etb.ForSqliteToTable("Items");
        }
        );
      }
    }
    

    WebApplication.ExtensionB.Data.Models
    In this project, we describe our only model - Item:

    public class Item : IEntity
    {
      public int Id { get; set; }
      public string Name { get; set; }
    }
    

    Each model must implement the ExtCore.Data.Models.Abstractions.IEntity interface.
    We will test the work of our new extension exactly as we did with ExtensionA.

    Launch and testing


    Our application with two extensions is ready. Running it, we should see something like this:



    conclusions


    At the moment, we (I and a few interested guys) are actively developing this project and several others are already based on it. We will be very happy for ideas, advice and criticism. Thanks!

    Link to the sources: https://github.com/ExtCore/ExtCore-Sample .

    For authors


    Friends, if you are interested in supporting the column with your own material, please write to me at vyunev@microsoft.com in order to discuss all the details. We are looking for authors who can interestingly talk about ASP.NET and other topics.

    about the author


    Sikorsky Dmitry Aleksandrovich
    Ubreinians Company ( http://ubrainians.com/ )
    Owner, Director
    DmitrySikorsky

    Also popular now: