Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to dynamically load pages from plugins in Razor Pages application?

I'm trying to deal with plugins using Razor Pages application.

Solution consists of 3 projects: one Razor Pages application and two Razor Class Libraries (RCL). Application must not refer RCL projects statically, they must be loaded as plugins:

enter image description here

There's nothing special inside pages. Feature pages just produce simple HTML. Index page builds a sort of menu.

Index page model:

public class IndexModel : PageModel
{
    public IEnumerable<MenuItem> MenuItems { get; private set; }

    public void OnGet()
    {
        MenuItems = new List<MenuItem>
        {
            new MenuItem { Route = "FeatureA", Title = "Feature A" },
            new MenuItem { Route = "FeatureB", Title = "Feature B" }
        };
    }
}

Index page:

@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}

<div class="text-center">
    <div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
        <ul class="navbar-nav flex-grow-1">
            @foreach (var item in Model.MenuItems)
            {
                <li class="nav-item">
                    <a class="nav-link text-dark" asp-area="" asp-page="/@item.Route">@item.Title</a>
                </li>
            }
        </ul>
    </div>
</div>

When I run the app, there are menu items, but their hrefs are empty:

<div class="text-center">
    <div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
        <ul class="navbar-nav flex-grow-1">
                <li class="nav-item">
                    <a class="nav-link text-dark" href="">Feature A</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link text-dark" href="">Feature B</a>
                </li>
        </ul>
    </div>
</div>

Of course, all assemblies (app and feature ones) are in the same directory.

Menu works in two following cases:

  • either if I refer RCL projects in App project, which kills plugins idea;
  • or if I put App.deps.json with FeatureLib_A and FeatureLib_B as dependecies (just save deps file from first case, remove references, rebuild all, copy saved deps file).

Also, I've tried to eagerly load RCL assemblies in Startup class. Assemblies are being loaded, but Index page behaves the same.

Is there any way to tell ASP infrastructure to use RCL assemblies without modifying deps file? What am I missing?

like image 515
Dennis Avatar asked Dec 28 '18 07:12

Dennis


People also ask

What is a dynamic razor page?

Razor Pages allow for self-contained .cshtml Razor pages on disk to be served including dynamic content via it's built-in support for C# Razor syntax. Essentially you can create something like hello.cshtml and then serve that as https://localhost:5200/hello.

What is Razor pages in MVC?

Introduction to Razor Pages in ASP.NET Core. Razor Pages is a new aspect of ASP.NET Core MVC that makes coding page-focused scenarios easier and more productive. If you're looking for a tutorial that uses the Model-View-Controller approach, see Get started with ASP.NET Core MVC. This document provides an introduction to Razor Pages.

Why can't I add assemblies to Razor pages at runtime?

Because runtime compiled Razor Pages are not pre-compiled you can't easily add assemblies to access at runtime. All that's available by default is what is compiled into the application when the static server was built originally - it doesn't look for other assemblies in the startup folder or elsewhere at least not automatically.

How do I add Razor pages to a new web app?

Run dotnet new webapp from the command line. See Get started with Razor Pages for detailed instructions on how to create a Razor Pages project. Razor Pages is enabled in program.cs: AddRazorPages adds services for Razor Pages to the app. MapRazorPages adds endpoints for Razor Pages to the IEndpointRouteBuilder. Consider a basic page:


2 Answers

I've figured it out.

The basic idea is to give ApplicationPartManager appropriate application parts.
It's important to note that:

  • "code" assemblies (e.g. FeatureLib_A.dll) must be added as AssemblyPart;
  • "view" assemblies (e.g. FeatureLib_A.Views.dll) must be added as CompiledRazorAssemblyPart.

Sample code:

public class Startup
{
    // ...

    public void ConfigureServices(IServiceCollection services)
    {
        var assemblyLoader = new DotNetCoreAssemblyLoader(searchPattern: "FeatureLib*.dll");

        services.AddMvc()
            .ConfigureApplicationPartManager(_ =>
            {
                foreach (var assembly in assemblyLoader.Assemblies)
                {
                    if (assembly.FullName.Contains("Views"))
                    {
                        _.ApplicationParts.Add(new CompiledRazorAssemblyPart(assembly));
                    }
                    else
                    {
                        _.ApplicationParts.Add(new AssemblyPart(assembly));
                    }
                }
            })
            .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    }

    // ...
}

DotNetCoreAssemblyLoader is a custom class, which looks for assembly files using given search pattern, and loads assemblies via AssemblyLoadContext.Default.LoadFromAssemblyPath.

like image 59
Dennis Avatar answered Sep 22 '22 15:09

Dennis


public class Startup
{

    public Startup( IHostingEnvironment hostingEnvironment)
    {

        _hostingEnvironment = hostingEnvironment;
    }
    private readonly IHostingEnvironment _hostingEnvironment;



    public void ConfigureServices(IServiceCollection services)
    {
        ...
        services.AddMvc()
                      .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
                      .ConfigureApplicationPartManager(ConfigureApplicationParts); ;
    }



    private void ConfigureApplicationParts(ApplicationPartManager apm)
    {
        string rootPath = _hostingEnvironment.ContentRootPath;
        var pluginsPath = Path.Combine(rootPath, "Plugins");

        var assemblyFiles = Directory.GetFiles(pluginsPath, "Plugin*.dll", SearchOption.AllDirectories);
        foreach (var assemblyFile in assemblyFiles)
        {
            try
            {
                var assembly = Assembly.LoadFrom(assemblyFile);
                if (assemblyFile.EndsWith(".Views.dll"))
                    apm.ApplicationParts.Add(new 
                           CompiledRazorAssemblyPart(assembly));
                else
                    apm.ApplicationParts.Add(new AssemblyPart(assembly));
            }
            catch (Exception e) { }
        }
    }
}
like image 23
Khaled M.Ramadan Avatar answered Sep 20 '22 15:09

Khaled M.Ramadan