How to dynamically load pages from plugins in Razo

2019-04-11 14:42发布

问题:

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:

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?

回答1:

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.