I have an ASP.NET Core MVC web application with different assemblies (modules). Each module is defined with the corresponding URL.
I have created a new route prefix to add the module name to the URL. That prefix is called module
.
On the other hand, I have 2 modules, called AccessControl
and TimeAttendance
. Both of them have the following controllers (notice they have the same name)
namespace Modules.AccessControl.UI.Controllers
{
[Route("[module]/[controller]")]
public class AreaController : Controller
{
[HttpGet]
public IActionResult Index()
{
return View();
}
}
}
and
namespace Modules.TimeAttendance.UI.Controllers
{
[Route("[module]/[controller]")]
public class AreaController : Controller
{
[HttpGet]
public IActionResult Index()
{
return View();
}
}
}
Both have the corresponding views called Index
:
and
Well.... when I load this in the browser: https://localhost:44375/TimeAttendance/Area
, the right Index
action is executed (from Modules.TimeAttendance.UI
assembly), however, the view from Modules.AccessControl.UI
is trying to be viewed.
About this I know 2 things:
return View()
AccessControl
module is the first assembly (module) to be loaded, I think that is why the view from that assembly is shown.Is there a way to use the full assembly to address the view when calling return View()
by default?
I had the same problem with Swagger before, however, with that I could set to use the full assembly name. I hope I can do the same when addressing the right view.
EDIT:
I created a custom IViewLocationExpander
to modify the view search path. Now this error is shown:
How can I address the view using full path? I have tried /Modules.TimeAttendance.UI/Views/Area/Index.cshtml
also but it did not work either.
EDIT 2: the routing
public class Module(string routePrefix, IModuleStartup startup)
{
/// <summary>
/// Gets the route prefix to all controller and endpoints in the module.
/// </summary>
public string RoutePrefix { get; } = routePrefix;
/// <summary>
/// Gets the startup class of the module.
/// </summary>
public IModuleStartup Startup { get; } = startup;
/// <summary>
/// Gets the assembly of the module.
/// </summary>
public Assembly Assembly => Startup.GetType().Assembly;
}
public class ModuleRoutingConvention(IEnumerable<Module> modules) : IActionModelConvention
{
private readonly IEnumerable<Module> _modules = modules;
public void Apply(ActionModel action)
{
var module = _modules.FirstOrDefault(m => action.Controller.ControllerType.Assembly.FullName?.StartsWith($"Modules.{m.RoutePrefix}") ?? false);
if (module == null)
{
return;
}
action.RouteValues.Add("module", module.RoutePrefix);
}
}
public class ModuleRoutingMvcOptionsPostConfigure : IPostConfigureOptions<MvcOptions>
{
private readonly IEnumerable<Module> _modules;
public ModuleRoutingMvcOptionsPostConfigure(IEnumerable<Module> modules)
{
_modules = modules;
}
public void PostConfigure(string? name, MvcOptions options)
{
options.Conventions.Add(new ModuleRoutingConvention(_modules));
}
}
/// <summary>
/// Adds a module.
/// </summary>
/// <param name="services"></param>
/// <param name="routePrefix">The prefix of the routes to the module.</param>
/// <typeparam name="TStartup">The type of the startup class of the module.</typeparam>
/// <returns></returns>
public static class ModuleServiceCollection
{
/// <summary>
/// Adds a module.
/// </summary>
/// <param name="services"></param>
/// <param name="routePrefix">The prefix of the routes to the module.</param>
/// <typeparam name="TStartup">The type of the startup class of the module.</typeparam>
/// <returns></returns>
public static IServiceCollection AddModule<TStartup>(this IServiceCollection services, string routePrefix, IConfiguration configuration)
where TStartup : IModuleStartup, new()
{
// Register assembly in MVC so it can find controllers of the module
services.AddControllers().ConfigureApplicationPartManager(manager =>
manager.ApplicationParts.Add(new AssemblyPart(typeof(TStartup).Assembly)));
var startup = new TStartup();
startup.ConfigureServices(services, configuration);
services.AddSingleton(new Module(routePrefix, startup));
return services;
}
}
EDIT 3:
This is how I am modifyng the view search location:
public class ModuleViewLocationMapper : IViewLocationExpander
{
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
List<string> locations = new List<string>(viewLocations);
var controllerActionDescriptor = (ControllerActionDescriptor)context.ActionContext.ActionDescriptor;
if (controllerActionDescriptor != null)
locations.Insert(0, string.Concat("~/", controllerActionDescriptor.ControllerTypeInfo.Assembly.GetName().Name, "/Views/{1}/{0}", RazorViewEngine.ViewExtension));
locations.RemoveAt(1);
return locations;
}
public void PopulateValues(ViewLocationExpanderContext context)
{
}
}
I have an ASP.NET Core MVC web application with different assemblies (modules). Each module is defined with the corresponding URL.
I have created a new route prefix to add the module name to the URL. That prefix is called module
.
On the other hand, I have 2 modules, called AccessControl
and TimeAttendance
. Both of them have the following controllers (notice they have the same name)
namespace Modules.AccessControl.UI.Controllers
{
[Route("[module]/[controller]")]
public class AreaController : Controller
{
[HttpGet]
public IActionResult Index()
{
return View();
}
}
}
and
namespace Modules.TimeAttendance.UI.Controllers
{
[Route("[module]/[controller]")]
public class AreaController : Controller
{
[HttpGet]
public IActionResult Index()
{
return View();
}
}
}
Both have the corresponding views called Index
:
and
Well.... when I load this in the browser: https://localhost:44375/TimeAttendance/Area
, the right Index
action is executed (from Modules.TimeAttendance.UI
assembly), however, the view from Modules.AccessControl.UI
is trying to be viewed.
About this I know 2 things:
return View()
AccessControl
module is the first assembly (module) to be loaded, I think that is why the view from that assembly is shown.Is there a way to use the full assembly to address the view when calling return View()
by default?
I had the same problem with Swagger before, however, with that I could set to use the full assembly name. I hope I can do the same when addressing the right view.
EDIT:
I created a custom IViewLocationExpander
to modify the view search path. Now this error is shown:
How can I address the view using full path? I have tried /Modules.TimeAttendance.UI/Views/Area/Index.cshtml
also but it did not work either.
EDIT 2: the routing
public class Module(string routePrefix, IModuleStartup startup)
{
/// <summary>
/// Gets the route prefix to all controller and endpoints in the module.
/// </summary>
public string RoutePrefix { get; } = routePrefix;
/// <summary>
/// Gets the startup class of the module.
/// </summary>
public IModuleStartup Startup { get; } = startup;
/// <summary>
/// Gets the assembly of the module.
/// </summary>
public Assembly Assembly => Startup.GetType().Assembly;
}
public class ModuleRoutingConvention(IEnumerable<Module> modules) : IActionModelConvention
{
private readonly IEnumerable<Module> _modules = modules;
public void Apply(ActionModel action)
{
var module = _modules.FirstOrDefault(m => action.Controller.ControllerType.Assembly.FullName?.StartsWith($"Modules.{m.RoutePrefix}") ?? false);
if (module == null)
{
return;
}
action.RouteValues.Add("module", module.RoutePrefix);
}
}
public class ModuleRoutingMvcOptionsPostConfigure : IPostConfigureOptions<MvcOptions>
{
private readonly IEnumerable<Module> _modules;
public ModuleRoutingMvcOptionsPostConfigure(IEnumerable<Module> modules)
{
_modules = modules;
}
public void PostConfigure(string? name, MvcOptions options)
{
options.Conventions.Add(new ModuleRoutingConvention(_modules));
}
}
/// <summary>
/// Adds a module.
/// </summary>
/// <param name="services"></param>
/// <param name="routePrefix">The prefix of the routes to the module.</param>
/// <typeparam name="TStartup">The type of the startup class of the module.</typeparam>
/// <returns></returns>
public static class ModuleServiceCollection
{
/// <summary>
/// Adds a module.
/// </summary>
/// <param name="services"></param>
/// <param name="routePrefix">The prefix of the routes to the module.</param>
/// <typeparam name="TStartup">The type of the startup class of the module.</typeparam>
/// <returns></returns>
public static IServiceCollection AddModule<TStartup>(this IServiceCollection services, string routePrefix, IConfiguration configuration)
where TStartup : IModuleStartup, new()
{
// Register assembly in MVC so it can find controllers of the module
services.AddControllers().ConfigureApplicationPartManager(manager =>
manager.ApplicationParts.Add(new AssemblyPart(typeof(TStartup).Assembly)));
var startup = new TStartup();
startup.ConfigureServices(services, configuration);
services.AddSingleton(new Module(routePrefix, startup));
return services;
}
}
EDIT 3:
This is how I am modifyng the view search location:
public class ModuleViewLocationMapper : IViewLocationExpander
{
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
List<string> locations = new List<string>(viewLocations);
var controllerActionDescriptor = (ControllerActionDescriptor)context.ActionContext.ActionDescriptor;
if (controllerActionDescriptor != null)
locations.Insert(0, string.Concat("~/", controllerActionDescriptor.ControllerTypeInfo.Assembly.GetName().Name, "/Views/{1}/{0}", RazorViewEngine.ViewExtension));
locations.RemoveAt(1);
return locations;
}
public void PopulateValues(ViewLocationExpanderContext context)
{
}
}
It is important that we consult the framework documentation before implementing something: Microsoft Learn - Routing - Area
I suggest you use Area
, which is also supported in routing as default and can vastly simplify your implementation.
Additionally, use an enum
to specify the Areas so that they are easier to manage and will avoid spelling mistakes or the like.
[ApiController]
[Area(nameof(MyAreas.Geo))]
[Route("[area]/[controller]")]
public class VolcanicController : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
return Ok("Geo Volcanic");
}
}
[ApiController]
[Area(nameof(MyAreas.Weather))]
[Route("[area]/[controller]")]
public class ForecastController : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
return Ok("Weather Forecast");
}
}
public enum MyAreas
{
Geo,
Weather
}
The error indicates your controller would be hit but it failed to find view in /Modules.TimeAttendace.UI.Views/Area
folder
Remove your ViewExpander,configure as below:
builder.Services.AddControllersWithViews(op => op.Conventions.Add(new ModuleRoutingConvention()));
builder.Services.Configure<RazorViewEngineOptions>(op =>
{
op.AreaViewLocationFormats.Insert(0, "/Views/{2}/{1}/{0}.cshtml");
});
Modify your ActionModelConvention:
public class ModuleRoutingConvention() : IActionModelConvention
{
.......
action.RouteValues.Add("module", module);
action.RouteValues.Add("area", module);
}
}
add a folder same with your assembly name:
Now it would work: