Umbraco: Create custom tree without separate project/DLL? - umbraco

I need to add a custom tree to a custom section of a "Web Site" (ie. a folder in the inetpub directory of a server)
I could do it by creating a new project (hence a new DLL), and copying the DLL to the bin folder of the website. But that will create a dependency to another project outside the website, which is not acceptable in my case.
Is there a way to create a custom tree without creating a separate DLL (eg. by just creating a Class in the AppCode folder)?
Is there a way to put in a Class name (instead of DLL name) in the treeHandlerAssembly column in the umbracoAppTree table?
Thanks in advance!

I solved it!
This article helped me http://www.shazwazza.com/post/Dynamically-registering-custom-trees-without-writing-to-UmbracoAppTree.aspx
Other than that I couldn't find any help anywhere else, hope this will help someone.
static object _locker = new object();
/// <summary>
/// Adds a custom tree to a custom section in Umbraco
/// </summary>
/// <param name="customTreeType">Your custom tree that is inherited from BaseTree</param>
/// <param name="alias">Your custom section alias</param>
public static void RenderCustomTree(Type customTreeType, string alias)
{
// Check if customTree is already registered
if (TreeDefinitionCollection.Instance.Count(x => x.TreeType == customTreeType) == 0)
{
lock (_locker)
{
// Double check
if (TreeDefinitionCollection.Instance.Count(x => x.TreeType == customTreeType) == 0)
{
Application customApp = new Application(alias, alias, ".traycontent");
// Create the tree definition
var myCustomTree = new TreeDefinition(customTreeType,
new umbraco.BusinessLogic.ApplicationTree(true, true, 0,
alias, //applicationAlias,
alias, //alias
alias, //title
".sprTreeFolder", //iconClosed
".sprTreeFolder_o", //iconOpened
"uComponents.Core", //assemblyName
customTreeType.ToString(), //type
null), //action
customApp);
// Add our tree definition to the collection at runtime
TreeDefinitionCollection.Instance.Add(myCustomTree);
}
}
}
}
And in your own class inherited from IHttpModule:
public void Init(HttpApplication application)
{
RenderCustomTree(typeof(YourCustomTree), "yourCustomSection");
}

Related

How to dynamically add a controller in a ASP.NET Core 6 MVC application

I need to dynamically creates controllers in a ASP.NET Core 6 MVC application.
I found some way to somewhat achieve this but not quite.
I'm able to dynamically add my controller but somehow it reflects only on the second request.
So here is what I do: first I initialize my console app as follows:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Mvc.Infrastructure;
namespace DynamicControllerServer
{
internal class Program
{
static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
ApplicationPartManager partManager = builder.Services.AddMvc().PartManager;
// Store thePartManager in my Middleware to be able to add controlelr after initialization is done
MyMiddleware._partManager = partManager;
// Register controller change event
builder.Services.AddSingleton<IActionDescriptorChangeProvider>(MyActionDescriptorChangeProvider.Instance);
builder.Services.AddSingleton(MyActionDescriptorChangeProvider.Instance);
var app = builder.Build();
app.UseAuthorization();
app.MapControllers();
// Add Middleware which is responsible to cactn the request and dynamically add the missing controller
app.UseMiddleware<MyMiddleware>();
app.RunAsync();
Console.WriteLine("Server has been started successfully ...");
Console.ReadLine();
}
}
}
Then my middleware looks like this: it basically detects that there is the "dynamic" keyword in the url. If so, it will load the assembly containing the DynamicController:
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using System;
using System.Reflection;
namespace DynamicControllerServer
{
public class MyMiddleware
{
public RequestDelegate _next { get; }
private string dllName = "DynamicController1.dll";
static public ApplicationPartManager _partManager = null;
public MyMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
if (httpContext.Request.Path.HasValue)
{
var queryParams = httpContext.Request.Path.Value;
if(httpContext.Request.Path.Value.Contains("api/dynamic"))
{
// Dynamically load assembly
Assembly assembly = assembly = Assembly.LoadFrom(#"C:\Temp\" + dllName);
// Add controller to the application
AssemblyPart _part = new AssemblyPart(assembly);
_partManager.ApplicationParts.Add(_part);
// Notify change
MyActionDescriptorChangeProvider.Instance.HasChanged = true;
MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel();
}
}
await _next(httpContext); // calling next middleware
}
}
}
The ActionDescriptorChange provider looks like this:
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Primitives;
namespace DynamicControllerServer
{
public class MyActionDescriptorChangeProvider : IActionDescriptorChangeProvider
{
public static MyActionDescriptorChangeProvider Instance { get; } = new MyActionDescriptorChangeProvider();
public CancellationTokenSource TokenSource { get; private set; }
public bool HasChanged { get; set; }
public IChangeToken GetChangeToken()
{
TokenSource = new CancellationTokenSource();
return new CancellationChangeToken(TokenSource.Token);
}
}
}
Dynamic controller is in separate dll and is very simple:
using Microsoft.AspNetCore.Mvc;
namespace DotNotSelfHostedOwin
{
[Route("api/[controller]")]
[ApiController]
public class DynamicController : ControllerBase
{
public string[] Get()
{
return new string[] { "dynamic1", "dynamic1", DateTime.Now.ToString() };
}
}
}
Here are the packages used in that project:
<PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
This works "almost" fine ... when first request is made to:
https://localhost:5001/api/dynamic
then it goes in the middleware and load the assembly, but returns a 404 error.
Then second request will actually work as expected:
Second request returns the expected result:
I must doing it wrong and probably my middleware is executed too late in the flow to reflect the dynamic controller right away.
Question is: what should be the proper way to achieve this?
Second question I have is say now the external dll holding our dynamic controller is updated.
How can I reload that controller to get the new definition?
Any help would be appreciated
Thanks in advance
Nick
Here is the answer to my own question in case it can help somebody out there.
It seems building and loading the controller from the middleware will always end up with failure on the first call.
This makes sense since we are already in the http pipeline.
I end up doing same thing from outside the middleware.
Basically my application detect a change in the controller assembly, unload the original assembly and load the new one.
You cannot use the Default context since it will not allow reloading different dll for same assembly:
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath); // Produce an exception on updates
To be able to reload new dll for same assembly, I’m loading each controller in its own assembly context. To do that you need to create your own class deriving from AssemblyLoadContext and managing assembly load:
public class MyOwnContext: AssemblyLoadContext
{
// You can find lots of example in the net
}
When you want to unload the assembly, you just unload the context:
MyOwnContextObj.Unload();
Now to add or remove the controller on the fly, you need to keep reference of the PartManager and the ApplicationPart.
To add controller
ApplicationPart part = new AssemblyPart(assembly);
_PartManager.ApplicationParts.Add(part);
To remove:
_PartManager.ApplicationParts.Remove(part);
On course once done, still use following piece of code to acknowledge the change:
MyActionDescriptorChangeProvider.Instance.HasChanged = true;
MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel();
That allow updating controller on the fly with no interruption of service.
Hope this helps people out there.
I have done a similar solution (used for managing a web app plugins) with some differences that may help you:
List all the external assemblies in a config file or appsettings.json so all the dll names and/or addresses are known at startup
Instead of registering controllers when they are called, register them at program.cs/start up :
//Foreah dllName from settings file
var assembly = Assembly.LoadFrom(#"Base address" + dllNameLoadedFromSettings);
var part = new AssemblyPart(assembly);
services.AddControllersWithViews()
.ConfigureApplicationPartManager(apm => apm.ApplicationParts.Add(part));
// Any other configuration based on the usage you want
Second: I usually keep plugin dlls in the bin folder so when using IIS as soon as a dll file in bin is changed the upper-level app is automatically reset. So your second question would be solved too.

MapMvcAttributeRoutes: This method cannot be called during the application's pre-start initialization phase

I have a very simple test in a test project in a solution using ASP MVC V5 and attribute routing. Attribute routing and the MapMvcAttributeRoutes method are part of ASP MVC 5.
[Test]
public void HasRoutesInTable()
{
var routes = new RouteCollection();
routes.MapMvcAttributeRoutes();
Assert.That(routes.Count, Is.GreaterThan(0));
}
This results in:
System.InvalidOperationException :
This method cannot be called during the applications pre-start initialization phase.
Most of the answers to this error message involve configuring membership providers in the web.config file. This project has neither membership providers or a web.config file so the error seems be be occurring for some other reason. How do I move the code out of this "pre-start" state so that the tests can run?
The equivalent code for attributes on ApiController works fine after HttpConfiguration.EnsureInitialized() is called.
I recently upgraded my project to ASP.NET MVC 5 and experienced the exact same issue. When using dotPeek to investigate it, I discovered that there is an internal MapMvcAttributeRoutes extension method that has a IEnumerable<Type> as a parameter which expects a list of controller types. I created a new extension method that uses reflection and allows me to test my attribute-based routes:
public static class RouteCollectionExtensions
{
public static void MapMvcAttributeRoutesForTesting(this RouteCollection routes)
{
var controllers = (from t in typeof(HomeController).Assembly.GetExportedTypes()
where
t != null &&
t.IsPublic &&
t.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) &&
!t.IsAbstract &&
typeof(IController).IsAssignableFrom(t)
select t).ToList();
var mapMvcAttributeRoutesMethod = typeof(RouteCollectionAttributeRoutingExtensions)
.GetMethod(
"MapMvcAttributeRoutes",
BindingFlags.NonPublic | BindingFlags.Static,
null,
new Type[] { typeof(RouteCollection), typeof(IEnumerable<Type>) },
null);
mapMvcAttributeRoutesMethod.Invoke(null, new object[] { routes, controllers });
}
}
And here is how I use it:
public class HomeControllerRouteTests
{
[Fact]
public void RequestTo_Root_ShouldMapTo_HomeIndex()
{
// Arrange
var routes = new RouteCollection();
// Act - registers traditional routes and the new attribute-defined routes
RouteConfig.RegisterRoutes(routes);
routes.MapMvcAttributeRoutesForTesting();
// Assert - uses MvcRouteTester to test specific routes
routes.ShouldMap("~/").To<HomeController>(x => x.Index());
}
}
One problem now is that inside RouteConfig.RegisterRoutes(route) I cannot call routes.MapMvcAttributeRoutes() so I moved that call to my Global.asax file instead.
Another concern is that this solution is potentially fragile since the above method in RouteCollectionAttributeRoutingExtensions is internal and could be removed at any time. A proactive approach would be to check to see if the mapMvcAttributeRoutesMethod variable is null and provide an appropriate error/exceptionmessage if it is.
NOTE: This only works with ASP.NET MVC 5.0. There were significant changes to attribute routing in ASP.NET MVC 5.1 and the mapMvcAttributeRoutesMethod method was moved to an internal class.
In ASP.NET MVC 5.1 this functionality was moved into its own class called AttributeRoutingMapper.
(This is why one shouldn't rely on code hacking around in internal classes)
But this is the workaround for 5.1 (and up?):
public static void MapMvcAttributeRoutes(this RouteCollection routeCollection, Assembly controllerAssembly)
{
var controllerTypes = (from type in controllerAssembly.GetExportedTypes()
where
type != null && type.IsPublic
&& type.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase)
&& !type.IsAbstract && typeof(IController).IsAssignableFrom(type)
select type).ToList();
var attributeRoutingAssembly = typeof(RouteCollectionAttributeRoutingExtensions).Assembly;
var attributeRoutingMapperType =
attributeRoutingAssembly.GetType("System.Web.Mvc.Routing.AttributeRoutingMapper");
var mapAttributeRoutesMethod = attributeRoutingMapperType.GetMethod(
"MapAttributeRoutes",
BindingFlags.Public | BindingFlags.Static,
null,
new[] { typeof(RouteCollection), typeof(IEnumerable<Type>) },
null);
mapAttributeRoutesMethod.Invoke(null, new object[] { routeCollection, controllerTypes });
}
Well, it's really ugly and I'm not sure if it'll be worth the test complexity, but here's how you can do it without modifying your RouteConfig.Register code:
[TestClass]
public class MyTestClass
{
[TestMethod]
public void MyTestMethod()
{
// Move all files needed for this test into a subdirectory named bin.
Directory.CreateDirectory("bin");
foreach (var file in Directory.EnumerateFiles("."))
{
File.Copy(file, "bin\\" + file, overwrite: true);
}
// Create a new ASP.NET host for this directory (with all the binaries under the bin subdirectory); get a Remoting proxy to that app domain.
RouteProxy proxy = (RouteProxy)ApplicationHost.CreateApplicationHost(typeof(RouteProxy), "/", Environment.CurrentDirectory);
// Call into the other app domain to run route registration and get back the route count.
int count = proxy.RegisterRoutesAndGetCount();
Assert.IsTrue(count > 0);
}
private class RouteProxy : MarshalByRefObject
{
public int RegisterRoutesAndGetCount()
{
RouteCollection routes = new RouteCollection();
RouteConfig.RegisterRoutes(routes); // or just call routes.MapMvcAttributeRoutes() if that's what you want, though I'm not sure why you'd re-test the framework code.
return routes.Count;
}
}
}
Mapping attribute routes needs to find all the controllers you're using to get their attributes, which requires accessing the build manager, which only apparently works in app domains created for ASP.NET.
What are you testing here? Looks like you are testing a 3rd party extension method. You shouldn't be using your unit tests to test 3rd party code.

Hook Unity into Web API Filter Attributes

I have Unity running great for all the controllers in my ASP.NET Web API project - just using the default set up that comes out of the NuGet box. I have also managed to hook it up to MVC Filter Attributes - but can't seem to do the same for ASP.NET Web API filter attributes.
How to I extend this default implementation to inject a dependency into an ActionFilterAttribute, for example...
public class BasicAuthenticationAttribute : ActionFilterAttribute
{
[Dependency]
public IMyService myService { get; set; }
public BasicAuthenticationAttribute()
{
}
}
This filter is applied to controllers using attributes:
[BasicAuthentication]
I'm pretty sure I need to hook up the Unity container so it handles the creation of the attribute class, but need some clues about where to start as it does not use the same extensibility points as the MVC filters.
I just wanted to add, other things I have tried include service location rather than dependency injection, but the DependencyResolver you get back is not the same one you configure.
// null
var service = actionContext.Request.GetDependencyScope().GetService(typeof(IMyService));
Or
// null
var service = GlobalConfiguration.Configuration.DependencyResolver.GetService(typeof(IApiUserService));
The problem is that the Attribute class is created by .NET and not by the WebAPI framework.
Before reading farther, did you forget to configure your DependencyResolver with your IApiUserService?
(IUnityContainer)container;
container.RegisterType<IApiUserService, MyApiUserServiceImpl>();
...
var service = GlobalConfiguration.Configuration.DependencyResolver.GetService(typeof(IApiUserService));
I created an App_Start\UnityConfig class that holds my UnityContainer:
public class UnityConfig {
#region Unity Container
private static Lazy<IUnityContainer> container = new Lazy<IUnityContainer>(() => {
var container = new UnityContainer();
RegisterTypes(container);
return container;
});
/// <summary>
/// Gets the configured Unity container.
/// </summary>
public static IUnityContainer GetConfiguredContainer() {
return container.Value;
}
#endregion
public static void Configure(HttpConfiguration config) {
config.DependencyResolver = new UnityDependencyResolver(UnityConfig.GetConfiguredContainer());
}
/// <summary>Registers the type mappings with the Unity container.</summary>
/// <param name="container">The unity container to configure.</param>
/// <remarks>There is no need to register concrete types such as controllers or API controllers (unless you want to
/// change the defaults), as Unity allows resolving a concrete type even if it was not previously registered.</remarks>
private static void RegisterTypes(IUnityContainer container) {
// NOTE: To load from web.config uncomment the line below. Make sure to add a Microsoft.Practices.Unity.Configuration to the using statements.
// container.LoadConfiguration();
// TODO: Register your types here
// container.RegisterType<IProductRepository, ProductRepository>();
container.RegisterType<MyClass>(new PerRequestLifetimeManager(), new InjectionConstructor("connectionStringName"));
}
}
The UnityDependencyResolver and PerRequestLifetimeManager came from this blog post and Unity.WebApi (Project/Nuget Package) which I internalized. (since it's a bootstrap)
When I need to make use of the UnityContainer in my other code, I passed it into the constructor:
config.Filters.Add(new MyFilterAttribute(UnityConfig.GetConfiguredContainer()));

2 razor partial views with the same name in different projects using razorgenerator

In our project we're using the razorgenerator of David Ebbo. This allowed us to move some of our cshtml files to a class library.
What we would like to achieve now is the following:
MyCommonViews has a "MyView.cshtml" in its Views folder.
MyWebProject ALSO has a "MyView.cshtml" in its Views folder.
MyOtherWebProject DOES NOT have a "MyView.cshtml" in its Views folder.
When MyOtherWebProject needs to load MyView.cshtml, it will pick the one which is in the compiled MyCommonViews project. That is what we want.
BUT when MyWebProject needs to load MyView.cshtml, we would like it to pick up the "overridden" MyView.cshtml file which is in the MyWebProject itself.
Is what we want possible and how?
Manu.
I wrote up a hacky solution for our problem. It hacks into the razorgenerators viewengine and removes all appropriate entries from the (private readonly) Dictionary it has.
The code is ran on application start.
Talk is cheap, show me the code:
private static void HackRazorGeneratorToAllowViewOverriding()
{
// first we search for the PrecompiledMvcEngine
var razorGeneratorViewEngine = ViewEngines.Engines.ToList().FirstOrDefault(ve => ve.GetType().Name.Contains("PrecompiledMvcEngine"));
if (razorGeneratorViewEngine == null)
return;
// retrieve the dictionary where it keeps the mapping between a view path and the (view object) type to instantiate
var razorMappings = (IDictionary<string, Type>)ReflectionUtils.GetPrivateReadonly("_mappings", razorGeneratorViewEngine);
// retrieve a list of all our cshtml files in our 'concrete' web project
var files = Directory.GetFiles(Path.Combine(WebConfigSettings.RootPath, "Views"), "*.cshtml", SearchOption.AllDirectories);
// do some kungfu on those file paths so that they are in the same format as in the razor mapping dictionary
var concreteViewPaths = files.Select(fp => string.Format("~{0}", fp.Replace(WebConfigSettings.RootPath, "").Replace(#"\", "/"))).ToList();
// loop through each of the cshtml paths (of our 'concrete' project) and remove it from the razor mappings if it's there
concreteViewPaths.ForEach(vp =>
{
if (razorMappings.ContainsKey(vp))
razorMappings.Remove(vp);
});
}
WebConfigSettings.RootPath contains the path on HD to the root of our web application.
This is a part of our static ReflectionUtils class:
/// <summary>
/// Get a field that is 'private readonly'.
/// </summary>
public static object GetPrivateReadonly(string readonlyPropName, object instance)
{
var field = instance.GetType().GetField(readonlyPropName, BindingFlags.Instance | BindingFlags.NonPublic);
if (field == null)
throw new NullReferenceException(string.Format("private readonly field '{0}' not found in '{1}'", readonlyPropName, instance));
return field.GetValue(instance);
}
This did the trick. We basically force the PrecompiledMvcEngine to "forget" any view which we have in our concrete project.
You can also try CompositePrecompiledMvcEngine from RazorGenerator.Mvc 2.1.0. It was designed for correct support of view overriding within multiple assemblies. Piece of code:
var engine = new CompositePrecompiledMvcEngine(
/*1*/ PrecompiledViewAssembly.OfType<MyCommonViewsSomeClass>(),
/*2*/ PrecompiledViewAssembly.OfType<MyWebProjectSomeClass>(
usePhysicalViewsIfNewer: HttpContext.Current.IsDebuggingEnabled));
ViewEngines.Engines.Insert(0, engine);
VirtualPathFactoryManager.RegisterVirtualPathFactory(engine);
The first line will register all views from the MyCommonViews assembly (~/Views/MyView.cshtml), the second line will register all views from the MyWebProject or MyOtherWebProject assembly.
When it encounters the virtual path, that already has been registered (~/Views/MyView.cshtml from the MyWebProject assembly), it overrides an old mapping with a new view type mapping.
If another project doesn't has view with the same virtual path (MyOtherWebProject) it leaves source mapping unchanged.
There is flag PreemptPhysicalFiles = false which does the magic.
Full sample:
[assembly: WebActivator.PostApplicationStartMethod(typeof(Application.Web.Common.App_Start.RazorGeneratorMvcStart), "Start")]
namespace Application.Web.Common.App_Start
{
public static class RazorGeneratorMvcStart
{
public static void Start()
{
var engine = new PrecompiledMvcEngine2(typeof (RazorGeneratorMvcStart).Assembly)
{
UsePhysicalViewsIfNewer = true, //compile if file changed
PreemptPhysicalFiles = false //use local file if exist
};
ViewEngines.Engines.Add(engine);//Insert(0,engine) ignores local partial views
// StartPage lookups are done by WebPages.
VirtualPathFactoryManager.RegisterVirtualPathFactory(engine);
}
}
}
However there is maybe a small bug:
http://razorgenerator.codeplex.com/workitem/100

Entity Framework 4: PropertyChanged event is raised too often

the generated code from EF for a property of an entity looks like this:
/// <summary>
/// No Metadata Documentation available.
/// </summary>
[EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=false)]
[DataMemberAttribute()]
public global::System.DateTime DateCreated
{
get
{
return _DateCreated;
}
set
{
OnDateCreatedChanging(value);
ReportPropertyChanging("DateCreated");
_DateCreated = StructuralObject.SetValidValue(value);
ReportPropertyChanged("DateCreated");
OnDateCreatedChanged();
}
}
private global::System.DateTime _DateCreated;
partial void OnDateCreatedChanging(global::System.DateTime value);
partial void OnDateCreatedChanged();
This code doesn't check if the value has actually changed (in the setter). Therefore the PropertyChanged event is raised even if you set a value that is equal to the current value. But in this case nothing would have changed, so I wouldn't want this event...
For EntityKey properties they do check this:
/// <summary>
/// No Metadata Documentation available.
/// </summary>
[EdmScalarPropertyAttribute(EntityKeyProperty=true, IsNullable=false)]
[DataMemberAttribute()]
public global::System.Guid Id
{
get
{
return _Id;
}
set
{
if (_Id != value)
{
OnIdChanging(value);
ReportPropertyChanging("Id");
_Id = StructuralObject.SetValidValue(value);
ReportPropertyChanged("Id");
OnIdChanged();
}
}
}
private global::System.Guid _Id;
partial void OnIdChanging(global::System.Guid value);
partial void OnIdChanged();
I would expect this behavior from all properties.
Am I missing a setting in the model designer, or is there another solution?
Thanx!
It is point of T4 templates to allow you modifications you need. It is absolutely wrong approach to say:
But I would rather not use a custom template in my project!
It is like throwing all advantages of T4 templates away and going back to hardcoded custom tools for code generating.
I did, as I knew it was possible and Ladislav also stated, include the T4 template file into the project and made the following changes to the "Write PrimitiveType Properties." part of the template:
if (!Object.Equals(<#=code.FieldName(primitiveProperty)#>, value))
{
<#=ChangingMethodName(primitiveProperty)#>(value);
ReportPropertyChanging("<#=primitiveProperty.Name#>");
<#=code.FieldName(primitiveProperty)#> = StructuralObject.SetValidValue(value<#=OptionalNullableParameterForSetValidValue(primitiveProperty, code)#>);
ReportPropertyChanged("<#=primitiveProperty.Name#>");
<#=ChangedMethodName(primitiveProperty)#>();
}
Hope that will be helpfull to others.

Resources