I'm trying to build a dynamic Web interface where I can dynamically point at a folder and serve Web content out of that folder with ASP.NET Core. This works fairly easily by using FileProviders in ASP.NET Core to re-route the Web root folder. This works both for StaticFiles and For RazorPages.
However, for RazorPages the problem is that once you do this you can't dynamically add references for additional types. I'd like to be able to optionally add a folder (PrivateBin) which on startup I can loop through, load the assemblies and then have those assemblies visible in Razor.
Unfortunately it doesn't work as Razor does not appear to see the loaded assemblies even when using runtime compilation.
I use the following during startup to load assemblies. Note the folder that these are loaded from are not in the default ContentRoot or WebRoot but in the new redirected WebRoot.
// WebRoot is a user chosen Path here specified via command line --WebRoot c:\temp\web
private void LoadPrivateBinAssemblies()
{
var binPath = Path.Combine(WebRoot, "PrivateBin");
if (Directory.Exists(binPath))
{
var files = Directory.GetFiles(binPath);
foreach (var file in files)
{
if (!file.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase) &&
!file.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase))
continue;
try
{
var asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(file);
Console.WriteLine("Additional Assembly: " + file);
}
catch (Exception ex)
{
Console.WriteLine("Failed to load private assembly: " + file);
}
}
}
}
The assembly loads into the AssemblyLoadContext() and I can - using Reflection and Type.GetType("namespace.class,assembly")
- access the type.
However, when I try to access the type in RazorPages - even with Runtime Compilation enabled - the types are not available. I get the following error:
To make sure that the type is indeed available, I checked that I can do the following inside of Razor:
@{
var md = Type.GetType("Westwind.AspNetCore.Markdown.Markdown,Westwind.AspNetCore.Markdown");
var mdText = md.InvokeMember("Parse", BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static, null,
null, new object[] { "**asdasd**", false, false, false });
}
@mdText
and that works fine. So the assembly is loaded and the type is accessible, but Razor doesn't appear to be aware of it.
So the question is:
Is it possible to load assemblies at runtime and make them available to Razor with Runtime Compilation, and use it like you normally would use a type via direct declarative access?
Enable runtime compilation at project creationSelect either the Web Application or the Web Application (Model-View-Controller) project template. Select the Enable Razor runtime compilation checkbox.
Notes: The runtime looks for Razor Pages files in the Pages folder by default. Index is the default page when a URL doesn't include a page.
Adding the Web API In order to add a Web API Controller you will need to Right Click the Project in the Solution Explorer and click on Add and then New Item. Now from the Add New Item window, choose the API Controller – Empty option as shown below. Then give it a suitable name and click Add.
It turns out the solution to this is via the Razor Runtime Compilation Options which allow adding of extra 'ReferencePaths', and then explicitly loading assemblies.
In ConfigureServices():
services.AddRazorPages(opt => { opt.RootDirectory = "/"; })
.AddRazorRuntimeCompilation(
opt =>
{
opt.FileProviders.Add(new PhysicalFileProvider(WebRoot));
LoadPrivateBinAssemblies(opt);
});
then:
private void LoadPrivateBinAssemblies(MvcRazorRuntimeCompilationOptions opt)
{
var binPath = Path.Combine(WebRoot, "PrivateBin");
if (Directory.Exists(binPath))
{
var files = Directory.GetFiles(binPath);
foreach (var file in files)
{
if (!file.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase) &&
!file.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase))
continue;
try
{
var asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(file);
opt.AdditionalReferencePaths.Add(file);
}
catch (Exception ex)
{
...
}
}
}
}
The key is:
opt.AdditionalReferencePaths.Add(file);
which makes the assembly visible to Razor, but doesn't actually load it. To load it you then have to explicitly load it with:
AssemblyLoadContext.Default.LoadFromAssemblyPath(file);
which loads the assembly from a path. Note that any dependencies that this assembly has have to be available either in the application's startup path or in the same folder you're loading from.
Note: Load order for dependencies may be important here or a not previously added assembly may not be found as a dependency (untested).
A Quick look into the ASP.NET Core source code reveals:
All Razor view Compilations start at:
RuntimeViewCompiler.CreateCompilation(..)
which uses: CSharpCompiler.Create(.., .., references: ..)
which uses: RazorReferenceManager.CompilationReferences
which uses: see code on github
// simplyfied
var referencePaths = ApplicationPartManager.ApplicationParts
.OfType<ICompilationReferencesProvider>()
.SelectMany(_ => _.GetReferencePaths())
which uses: ApplicationPartManager.ApplicationParts
So we need somehow register our own ICompilationReferencesProvider
and this is how..
While it's search for Application parts does the ApplicationPartManager
a few things:
[assembly: ApplicationPartAttribute(assemblyName:"..")] // Specifies an assembly to be added as an ApplicationPart
[assembly: RelatedAssemblyAttribute(assemblyFileName:"..")] // Specifies a assembly to load as part of MVC's assembly discovery mechanism.
// plus `Assembly.GetEntryAssembly()` gets added automaticly behind the scenes.
Then it loops throuth all found Assemblies and uses ApplicationPartFactory.GetApplicationPartFactory(assembly) (as seen in line 69) to find types which extend ApplicationPartFactory
.
Then it invokes the method GetApplicationParts(assembly)
on all found ApplicationPartFactory
s.
All Assemblies without ApplicationPartFactory
get the DefaultApplicationPartFactory
which returns new AssemblyPart(assembly)
in GetApplicationParts
.
public abstract IEnumerable<ApplicationPart> GetApplicationParts(Assembly assembly);
GetApplicationPartFactory searches for [assembly: ProvideApplicationPartFactory(typeof(SomeType))]
then it uses SomeType
as factory.
public abstract class ApplicationPartFactory {
public abstract IEnumerable<ApplicationPart> GetApplicationParts(Assembly assembly);
public static ApplicationPartFactory GetApplicationPartFactory(Assembly assembly)
{
// ...
var provideAttribute = assembly.GetCustomAttribute<ProvideApplicationPartFactoryAttribute>();
if (provideAttribute == null)
{
return DefaultApplicationPartFactory.Instance; // this registers `assembly` as `new AssemblyPart(assembly)`
}
var type = provideAttribute.GetFactoryType();
// ...
return (ApplicationPartFactory)Activator.CreateInstance(type);
}
}
This means we can create and register (using ProvideApplicationPartFactoryAttribute
) our own ApplicationPartFactory
which returns a custom ApplicationPart
implementation which implements ICompilationReferencesProvider
and then returns our references in GetReferencePaths
.
[assembly: ProvideApplicationPartFactory(typeof(MyApplicationPartFactory))]
namespace WebApplication1 {
public class MyApplicationPartFactory : ApplicationPartFactory {
public override IEnumerable<ApplicationPart> GetApplicationParts(Assembly assembly)
{
yield return new CompilationReferencesProviderAssemblyPart(assembly);
}
}
public class CompilationReferencesProviderAssemblyPart : AssemblyPart, ICompilationReferencesProvider {
private readonly Assembly _assembly;
public CompilationReferencesProviderAssemblyPart(Assembly assembly) : base(assembly)
{
_assembly = assembly;
}
public IEnumerable<string> GetReferencePaths()
{
// your `LoadPrivateBinAssemblies()` method needs to be called before the next line executes!
// So you should load all private bin's before the first RazorPage gets requested.
return AssemblyLoadContext.GetLoadContext(_assembly).Assemblies
.Where(_ => !_.IsDynamic)
.Select(_ => new Uri(_.CodeBase).LocalPath);
}
}
}
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Content Remove="Pages\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.0.0" />
</ItemGroup>
</Project>
services
.AddRazorPages()
.AddRazorRuntimeCompilation();
AssemblyLoadContext.Default.LoadFromAssemblyPath(@"C:\path\to\ClassLibrary1.dll");
// plus the MyApplicationPartFactory and attribute from above.
~/Pages/Index.cshtml
@page
<pre>
output: [
@(
new ClassLibrary1.Class1().Method1()
)
]
</pre>
And it shows the expected output:
output: [
Hallo, World!
]
Have a nice day.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With