Extend AspNetCore View Discovery - asp.net-mvc

I'm looking for a way to extend the AspNetCore MVC view discovery logic. I want to be able to inherit from a controller and have the new controller have access to the Actions of the base Controller. Is there a way to extend the view discovery logic so that you can tell a controller where to look for its vies, to look in the folder of the controller, look in a folder based on the name of the base controller, or even look in a folder based on the namespace of the controller?
~/Controllers/UserAccountController.cs
namespace App.Controllers.UserAccount
{
public class UserAccountController {
public virtual async Task<IActionResult> Action1()
{
return View();
}
}
}
~/Controllers/UserAccountExtController.cs
namespace App.Controllers.UserAccount
{
public class UserAccountExtController : UserAccountController {
public override async Task<IActionResult> Action1()
{
return View();
}
}
}
Is there a way that I can extend the view discovery logic so that it if it does not find the view in the view folder with the same name as the Controller name, that it will look in the folder based on an Attribute of the controller, or the folder of the inherited controller, the folder that the controller exists in, or a folder based on the namespace of the controller?

I ended up going with a IViewLocationExpander to solve the issue thanks to RandyBuchholz for the tip on casting the ActionContext to a ControllerActionContext, which allowed me to identify the BaseType of the controller. This allowed be to add the convention of checking the default location of the BaseController if a view didn't exist in the default location for the Controller.
public class MyViewLocationExpander : IViewLocationExpander
{
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
// list used for future extension
var alternateLocations = new List<string>();
if (context.ActionContext.ActionDescriptor is ControllerActionDescriptor descriptor)
{
var baseType = descriptor.ControllerTypeInfo.BaseType.Name;
if (!baseType.StartsWith("Controller"))
{
var baseLocation = baseType.Replace("Controller", string.Empty);
alternateLocations.Add("/Views/" + baseLocation + "/{0}.cshtml");
}
}
var locations = viewLocations.ToList();
locations.InsertRange(locations.IndexOf("/Views/Shared/{0}.cshtml") - 1, alternateLocations);
return locations;
}
public void PopulateValues(ViewLocationExpanderContext context)
{
}
}
Then just register the IViewLocationExpander in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
//...
services.Configure<RazorViewEngineOptions>(options =>
{
var expander = new MyViewLocationExpander();
options.ViewLocationExpanders.Add(expander);
});
//...
}

Related

Asp.Net MVC How to log all actions being called

I need to be able to log all actions that are called from my asp.net mvc application. How and what would be the best way to achieve this? Where I log it to whether it be the console or log file doesn't matter.
You could create your own class which inherits from ActionFilterAttribute and then override the OnActionExecuting method.
Example
public class LogActionAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var controller = filterContext.RequestContext.RouteData.Values["Controller"];
var action = filterContext.RequestContext.RouteData.Values["Action"];
//
// Perform logging here
//
base.OnActionExecuting(filterContext);
}
}
public class HomeController : Controller
{
[LogAction]
public ActionResult Index()
{
return View();
}
}
Hope this helps!
Credit HeyMega for their answer. Here's an example of an expanded implementation I arrived at in MVC5.
public class LogActionAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var controller = filterContext.RequestContext.RouteData.Values.ContainsKey("Controller") ? filterContext.RequestContext.RouteData.Values["Controller"].ToString() : null;
var action = filterContext.RequestContext.RouteData.Values.ContainsKey("Action") ? filterContext.RequestContext.RouteData.Values["Action"].ToString() : null;
var area = filterContext.RequestContext.RouteData.DataTokens.ContainsKey("Area") ? filterContext.RequestContext.RouteData.DataTokens["Area"].ToString() : null;
var user = filterContext.RequestContext.HttpContext.User.Identity.GetUserId();
Task.Run(() => Generic().AreaActionLog(user, area, controller, action));
base.OnActionExecuting(filterContext);
}
}
I chose to separate the method doing the actual logging into a separate process, if anything goes wrong with the Database interaction, or the DB interaction takes several seconds, the UI is uninterrupted.
You can then decorate the entire controller with [LogAction] attribute like so.
[LogAction]
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
public ActionResult Contact()
{
return View();
}
}
Or selectively apply the attribute by decorating individual methods:
public class HomeController : Controller
{
[LogAction]
public ActionResult Index_Logs_Things()
{
return View();
}
}
Hope this helps someone.
You could try Audit.NET library with its Audit.MVC and the different data providers to store the logs on files, eventlog, sql, redis, mongo, and much more.
With the MVC extension you just need to decorate your controllers or actions with an attribute:
[Audit]
public class HomeController : Controller
{ ... }
Execute a static configuration to set the output of your logs:
Audit.Core.Configuration.Setup()
.UseFileLogProvider(_ => _
.Directory(#"C:\Logs"));
And it will provide the infrastructure to log the interactions with your MVC application.

ViewBag in static method of controller

I am new to mvc and I load ViewBag in a method of controller as,
HomeController: Controller
{
Public ActionResult Index()
{
loadViewBag();
return View();
}
public void loadViewBag()
{
ViewBag.aaa = "something";
}
}
It works fine.
What is my problem is, Now I want to call loadViewBag() method form another controller( say Account) so that I can reuse same method and need to make loadViewBag() method static due to some static variables as:
public static void loadViewBag()
If I make loadViewBag method static, there appear error on ViewBag " An object reference is required for the non-static field, method, or property 'System.Web.Mvc.ControllerBase.ViewBag.get' ".
Is there any solution/suggestion.
Thank You.
Just make it an extension method of ControllerBase e.g.
public static void ControllerExt
{
public static void LoadViewBag(this ControllerBase controller)
{
controller.ViewBag.aaa = "something";
...
}
}
That way you can use it in any controller
public class HomeController : Controller
{
public ActionResult Index()
{
this.LoadViewBag();
return View();
}
}
public class AccountController : Controller
{
public ActionResult Index()
{
this.LoadViewBag();
return View();
}
}
If its only specific to some controllers then it would be more flexible to pass the ViewBag property in e.g.
public static class ControllerHelper
{
public static void LoadViewBag(dynamic viewBag)
{
viewBag.aaa = "something";
}
}
public class HomeController : Controller
{
public ActionResult Index()
{
ControllerHelper.LoadViewBag(ViewBag);
return View();
}
}
ViewBag is a property of your controller (more specifically of ControllerBase), and since a static method has no knowledge of a class instance, you can't access it.
You could pass the controller instance to the method if you want to use a static method or even make it an extension method, but depending on your problem, this solution could be sub-optimal. You may be able to get a better answer if you add more details to your question.
Public ActionResult Index()
{
this.loadViewBag();
return View();
}
public static void loadViewBag(this ControllerBase target)
{
target.ViewBag.aaa = "something";
}
Do you need that to allow different controllers/views to use some common properties?
Then I'd rather recommend a common base controller, while also wrapping ViewBag code into type safe properties (to let the compiler control the data consistency - as you know, ViewBag is not type safe, so any typos and data mismatches won't be noticed until the code gets executed).
1. Introduce a common controller with those wrapper properties
public abstract class MyBaseController : Controller
{
internal long CurrentUserId
{
get { return ViewBag.CurrentUserId; }
set { ViewBag.CurrentUserId = value; }
}
internal Role CurrentUserRole
{
get { return ViewBag.CurrentUserRole; }
set { ViewBag.CurrentUserRole = value; }
}
...
}
Thus, your inherited controllers could simply set the properties - or, with lots of common code just introduce a method in your base controller - similar to what you already have.
2. Introduce a common view class with those wrapper properties
public abstract class MyBaseViewPage<T> : WebViewPage<T>
{
public string Title
{
get { return (string)ViewBag.Title; }
set { ViewBag.Title = value; }
}
public long CurrentUserId
{
get { return (long)ViewBag.CurrentUserId; }
}
public Role CurrentUserRole
{
get { return ViewBag.CurrentUserRole; }
}
}
public abstract class MyBaseViewPage : MyBaseViewPage<dynamic>
{
}
and update web.config to let MVC know you're using a custom base view:
<configuration>
...
<system.web.webPages.razor>
...
<pages pageBaseType="MyRootNamespace.Views.MyBaseViewPage">
...
</pages>
</system.web.webPages.razor>
Now you can use them as normal properties in your controllers and views.

Accessing Service Layer from Controller in MVC Project

I previously had a controller that had code like this:
public ActionResult Method(int Id)
{
var foo = doThis(Id)
return View("Error");
}
doThis() is a method that exists in the controller, and performs some logic. I'm now trying to relocate all business logic to a Services project that contains a bunch of classes.
To start I added a class library Project.Services and then added a class FooServices which contains the following:
namespace Project.Services
{
class FooServices
{
public List<Bar> doThis(int Id)
{
//Do stuff
return parentSets;
}
}
}
I've added a reference to this project from my MVC project, and a reference from this Services project to my data model project, but I'm not sure how to proceed now. How can I access these methods from controllers?
How can I access these methods from controllers?
In order to access an instance method you need an instance of the object:
public ActionResult Method(int Id)
{
var foo = new FooServices().doThis(Id)
return View("Error");
}
Of course by doing this you are now strongly coupling your controller logic with a specific implementation of your service making it very difficult to unit test your controllers in isolation.
So to weaken the coupling start by introducing an abstraction:
public interface IFooServices
{
List<Bar> DoThis(int id)
}
and then have your service layer implement this interface:
public class FooServices: IFooServices
{
public List<Bar> DoThis(int id)
{
//Do stuff
return parentSets;
}
}
Alright, now your controller could work with this abstraction:
public class HomeController: Controller
{
private readonly IFooServices service;
public HomeController(IFooServices service)
{
this.sevrice = service;
}
public ActionResult Method(int id)
{
var foo = this.service.DoThis(id)
return View("Error");
}
}
Great, at this stage we really have a weak coupling between your controller and the service layer. All that's left now is to configure your favorite dependency Injection framework to inject the specific service into your controller.

Can I use method hiding on an ActionResult that has the same signature as the base controller?

I have 2 controllers, one inheriting the other. I need to override an ActionResult from the base controller because I need to change to code to implement pagination in the plugin for nopCommerce. However, I get an AmbiguousMatchException because of the new ActionResult.
Base Controller:
public class BaseController : Controller {
public ActionResult Category(int categoryId, CatalogPagingFilteringModel command)
{
//original action result code
}
}
Customer Controller w/ inheritance
public class CustomController : BaseController {
public new ActionResult Category(int categoryId, CatalogPagingFilteringModel command)
{
// my pagination code with different model/view
}
}
Route Info:
Here I remove the route for the base controller and add a new route to use the CustomCatalog controller.
routes.Remove(routes["OriginalCategory"]);
routes.MapLocalizedRoute(
"OriginalCategory",
"Category/{categoryId}/{SeName}",
new { controller = "CustomCatalog", action = "Category", SeName = UrlParameter.Optional },
new { categoryId = #"\d+" },
new[] { "Nop.Plugin.Common.Web.Controllers" });
I then get an AmbiguousMatchException
[AmbiguousMatchException: The current request for action 'Category' on
controller type 'CustomCatalogController' is ambiguous between the
following action methods: System.Web.Mvc.ActionResult Category(Int32,
Nop.Web.Models.Catalog.CatalogPagingFilteringModel) on type
Nop.Plugin.Common.Web.Controllers.CustomCatalogController
System.Web.Mvc.ActionResult Category(Int32,
Nop.Web.Models.Catalog.CatalogPagingFilteringModel) on type
Nop.Web.Controllers.CatalogController]
EDIT
The base controller resides in the core of the application where as the CustomController is in the plugin meaning that I cannot modify the base controller's type.
How about using virtual in the base controller and override in the derived controller instead of new?
base:
public virtual ActionResult Category(...) { }
derived:
public override ActionResult Category(...) { }
You cannot override a method which has not been declared virtual.
You can either define a new method with a different signature or you can encapsulate the functionality of the original class by maintaining a private reference to it in a wrapper class. It helps if you have an interface that you can implement from the base library (because this allows you to substitute the wrapper class where the base class that implements the same interface would be used), but you can do it without the interface, too.
// Base class source code is not modifiable
class BaseClass {
public ActionResult Category(...) {}
public ActionResult Other() {}
}
// Wrapper class can modify the behavior
class Wrapper {
private BaseClass baseClass = new BaseClass(); // Instantiate appropriately
public ActionResult Category(...) {
// do some stuff
}
public ActionResult Other() {
return baseClass.Other();
}
}

Cannot resolve view of the parent controller

Create a controller:
public abstract class MyBaseController : Controller
{
public ActionResult MyAction(string id)
{
return View();
}
}
Than create another specific controller that inherit from MyBaseController:
public class MyController : MyBaseController
{
}
There is a view called MyAction.aspx in the Views/MyBaseController folder
Then, call MyController/MyAction method. Following exception will be generated:
The view 'MyAction' or its master
could not be found. The following
locations were searched:
~/Views/MyController/MyAction.aspx
~/Views/MyController/MyAction.ascx
~/Views/Shared/MyAction.aspx
~/Views/Shared/MyAction.ascx
Can I make MVC.NET to use the view from Views/MyBaseController folder?
you should wait for a more finesse answer but this work:
Create a new view engine based on the default one and override the FindViewMethod this way:
public class MyNewViewEngine : WebFormViewEngine
{
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
var type = controllerContext.Controller.GetType();
//Retrieve all the applicable views.
var applicableViews = from m in type.GetMethods()
where typeof(ActionResult).IsAssignableFrom(m.ReturnType) & m.Name == viewName
select m;
//Save the original location formats.
var cacheLocations = ViewLocationFormats;
var tempLocations = cacheLocations.ToList();
//Iterate over applicable views and check if they have been declared in the given controller.
foreach(var view in applicableViews)
{
//If not, add a new format location to the ones at the default engine.
if (view.DeclaringType != type)
{
var newLocation = "~/Views/" + view.DeclaringType.Name.Substring(0, view.DeclaringType.Name.LastIndexOf("Controller")) + "/{0}.aspx";
if (!tempLocations.Contains(newLocation))
tempLocations.Add(newLocation);
}
}
//Change the location formats.
ViewLocationFormats = tempLocations.ToArray();
//Redirected to the default implementation
var result = base.FindView(controllerContext, viewName, masterName, useCache);
//Restore the location formats
ViewLocationFormats = cacheLocations;
return result;
}
}
Add the new view engine:
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new MyNewViewEngine());
RegisterRoutes(RouteTable.Routes);
}
}
hope this helps
You need to add it to shared because you are in the context of the subcontroller. If you want different behavior for different controllers, then you'll want to put a MyAction view in each of your subcontroller view folders.
To answer your question though, you probably could make it look in base controller folder, but it would require you to write your own request handler which looks in base controller folders. The default implementation only looks in the view folder for the current controller context, then it looks in the shared folder. It sounds like your view is shared however, so the shared folder seems like a good place for it anyway.
It is possible, but not very clean.
public class MyController : MyBaseController
{
public ActionResult MyAction(string id)
{
return View("~/Views/MyBaseController/MyAction.aspx");
}
}
However if your View (MyAction.aspx) contains a reference to a Partial View, ASP.NET MVC will look for it in the Views/MyController folder (and not find it there!).
If your view is shared across controllers, its best to place it in the Views/Shared folder as recommended by NickLarsen.

Resources