How to show a view when it is in other assembly? .NET Core Web Application - Stack Overflow

admin2025-05-01  0

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.UIassembly), however, the view from Modules.AccessControl.UI is trying to be viewed.

About this I know 2 things:

  1. I can point to the right view by using the full path in the call to return View()
  2. Since 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.UIassembly), however, the view from Modules.AccessControl.UI is trying to be viewed.

About this I know 2 things:

  1. I can point to the right view by using the full path in the call to return View()
  2. Since 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)
    {
        
    }
}
Share Improve this question edited Jan 6 at 2:05 jstuardo asked Jan 2 at 18:14 jstuardojstuardo 4,30614 gold badges83 silver badges173 bronze badges 0
Add a comment  | 

2 Answers 2

Reset to default 0

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:

转载请注明原文地址:http://anycun.com/QandA/1746103364a91712.html