I have a C# Solution containing two projects : Client (parent) and RestAPI (child). From the client project, I'm loading a ASP.NET server. Currently, my RestAPI is successfully loading controllers from the client project using services.AddControllersWithViews().AddApplicationPart(assembly);. Currently, only the controllers can be loaded, though, I want to use some views too.
To summarize, what I'm trying to do is to use views that are located into my Client project by the RestAPI project.
What I've tried to do :
services.AddControllersWithViews().AddApplicationPart(assembly); => It loads only the controllers
services.AddMvc().AddApplicationPart(assembly); => Same results
Returning a static path to the view file (i.e. : return View("C:\Simon\...");)
Project structure :
In the controller DevicesController.cs, I have to following function:
[Route("show")]
[HttpGet]
public ViewResult Show()
{
return View("Home");
}
I want to return the view in the Views folder located under Client project. However, It only returns the one located under RestAPI. If I remove the one in RestAPI, the application crashes with the following error logs :
System.InvalidOperationException: The view 'Home' was not found. The following locations were searched:
/Views/Devices/Home.cshtml
/Views/Shared/Home.cshtml
I think you have to consider adding relatedAssemblies.
private static void AddApplicationPart(IMvcBuilder mvcBuilder, Assembly assembly)
{
var partFactory = ApplicationPartFactory.GetApplicationPartFactory(assembly);
foreach (var part in partFactory.GetApplicationParts(assembly))
{
mvcBuilder.PartManager.ApplicationParts.Add(part);
}
var relatedAssemblies = RelatedAssemblyAttribute.GetRelatedAssemblies(assembly, throwOnError: false);
foreach (var relatedAssembly in relatedAssemblies)
{
partFactory = ApplicationPartFactory.GetApplicationPartFactory(relatedAssembly);
foreach (var part in partFactory.GetApplicationParts(relatedAssembly))
{
mvcBuilder.PartManager.ApplicationParts.Add(part);
}
}
}
Related
I have a Razor Pages project (no Controller) with structure like this:
From the main Index.cshtml, I would render a partial view for its content depend on the Theme name, for example:
#* Default will be replaced with theme name *#
<partial name="Themes\Default\HomeContent" />
In HomeContent.cshtml, I would like to render many other partial views within its folder. However this wouldn't work:
<p>Content</p>
<partial name="_DefaultThemePartial" />
The engine only searches these locations (correct according to the documentation):
InvalidOperationException: The partial view '_DefaultThemePartial' was
not found. The following locations were searched:
/Pages/_DefaultThemePartial.cshtml
/Pages/Shared/_DefaultThemePartial.cshtml
/Views/Shared/_DefaultThemePartial.cshtml
I have also tried <partial name="./_DefaultThemePartial" /> or <partial name=".\_DefaultThemePartial" /> or try putting them in a subfolder named Shared (within the Default folder). None of them works, only the above 3 locations are searched.
Is there anyway to render those partials without specifying the full path?
I posted a proposal here. In the meantime, I found out you can expand the discovery mechanism by using IViewLocationExpander but I barely found any useful documentation on it.
public class PartialViewLocationExpander : IViewLocationExpander
{
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
if (!context.Values.TryGetValue("FromView", out var fromView))
{
return viewLocations;
}
var folder = Path.GetDirectoryName(fromView) ?? "/";
var name = context.ViewName;
if (!name.EndsWith(".cshtml", StringComparison.OrdinalIgnoreCase))
{
name += ".cshtml";
}
var path = Path.Combine(folder, name)
.Replace('\\', '/');
return viewLocations.Concat(new[] { path });
}
public void PopulateValues(ViewLocationExpanderContext context)
{
var ctx = context.ActionContext as ViewContext;
if (ctx == null) { return; }
var path = ctx.ExecutingFilePath;
if (!string.IsNullOrEmpty(path))
{
context.Values["FromView"] = path;
context.Values["ViewName"] = context.ViewName;
}
}
}
// Register
services.Configure<RazorViewEngineOptions>(options =>
{
options.ViewLocationExpanders.Add(new PartialViewLocationExpander());
});
From a response from my GitHub issue, there's a simpler solution without any extra code by adding the .cshtml at the end:
[...] This is by design. Partial view lookup is done in two ways:
By name
By file path
You've done the lookup by name (without file extension), if you want
to use a relative path, make sure you specify the file extension.
<partial name="_DefaultThemePartial.cshtml" />
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?
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..
ApplicationPartManager
While it's search for Application parts does the ApplicationPartManager a few things:
it searchs for hidden Assemblies reading attributes like:
[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 ApplicationPartFactorys.
All Assemblies without ApplicationPartFactory get the DefaultApplicationPartFactory which returns new AssemblyPart(assembly) in GetApplicationParts.
public abstract IEnumerable<ApplicationPart> GetApplicationParts(Assembly assembly);
GetApplicationPartFactory
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);
}
}
One Solution
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);
}
}
}
My Working Test Setup:
ASP.NET Core 3 WebApplication
ASP.NET Core 3 ClassLibrary
Both Projects have no reference to each other.
<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.
I have ASP.NET Web API project and I want to add a Help page, but I want it to be in a separate project.
Is it possible ?
You can re-write XmlDocumentationProvider constructor to something like that:
public XmlDocumentationProvider(string appDataPath)
{
if (appDataPath == null)
{
throw new ArgumentNullException(nameof(appDataPath));
}
var files = new[] { "MyWebApiProject.xml" /*, ... any other projects */ };
foreach (var file in files)
{
var xpath = new XPathDocument(Path.Combine(appDataPath, file));
_documentNavigators.Add(xpath.CreateNavigator());
}
}
It will include all the xml documentation files that you list in the array. You should also make sure your target Web API project (and all the model projects if needed) generates documentation on build and copies it to the right location.
You should call WebApiConfig.Register from your Web API project in your help project:
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RouteConfig.RegisterRoutes(RouteTable.Routes);
GlobalConfiguration.Configure(MyApiProjectNamespace.WebApiConfig.Register);
}
I have this code in a class library:
string.Format("{0}://{1}", Current.Request.Url.Scheme, Current.Request.Url.Authority);
This works fine if the application is deployed in the root domain and not a subdomain.
I would like to adapt the above to work for a sub domain as well. In the razor code I can just use:
Url.Content("~/")
Is there an equivalent for this for class libraries ('web independent' C# code)?
This little function will get the application root folder, e.g. '/' or '/sub-folder/':
string GetAppRootFolder()
{
var appRootFolder = HttpContext.Current.Request.ApplicationPath.ToLower();
if (!appRootFolder.EndsWith("/"))
{
appRootFolder += "/";
}
return appRootFolder;
}
I am playing around with Sharepoint 2007. I have a virtual machine (win server 2k3) with an instance of sharepoint server 2007 running on it. I am now working on creating web parts. I have successfully created simple ones, such as this one that displays text:
public class SimpleWebPart : WebPart
{
private string _displayText = "Hello World!";
[WebBrowsable(true), Personalizable(true)]
public string DisplayText
{
get { return _displayText; }
set { _displayText = value; }
}
protected override void Render(System.Web.UI.HtmlTextWriter writer)
{
writer.Write(_displayText);
}
}
I have this one (and a few test ones) inside of a Class Library, which I put into the _app_bin folder inside of C:\Inetpub\wwwroot\wss\VirtualDirectories\80.
The latest one I added utilizes LINQ to get data from a table I added (not part of Sharepoint):
public class SimpleDBWebPart : WebPart
{
protected override void Render(System.Web.UI.HtmlTextWriter writer)
{
var oDB = new SPWebPartDataClassesDataContext();
var oRes = oDB.GetAllFirstTable();
foreach(var item in oRes)
{
writer.Write("<div>Item Name: {0}</div>",item.text);
writer.Write("<div>Item ID: {0}</div>", item.id);
}
}
}
The GetAllFirstTable() is a stored procedure that gets all the data from my test table:
ALTER PROCEDURE dbo.GetAllFirstTable
AS
SELECT * FROM FirstTable
RETURN
When I try to add the WebPart to a page, I get this error:
The "SimpleDBWebPart" Web Part appears to be causing a problem. Could not load file or assembly 'System.Data.Linq, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' or one of its dependencies. The system cannot find the file specified.
I used Reflector to make sure I have the assembly inside the DLL:
And that appears to be the case. Do I have to add the assembly to the web.config file of the sharepoint site? Or is there something else that I am missing?
Thanks guys!
To use LINQ or .NET 3.5 feature you need to first configure the SharePoint to run in 3.5 Mode.
Refer to these links on how to do that
Simplest way
AnotherOne