ASP.NET MVC: Get all controllers - asp.net-mvc

Is it possible to get all controllers available to a ControllerFactory?
What I want to do is get a list of all controller types in application, but in a consistent way.
So that all controllers I get are the same ones default request resolution is using.
(The actual task is to find all action methods that have a given attribute).

You can use reflection to enumerate all classes in an assembly, and filter only classes inherit from Controller class.
The best reference is asp.net mvc source code. Take a look of the implementations of ControllerTypeCache and ActionMethodSelector class.
ControllerTypeCache shows how to get all controller classes available.
internal static bool IsControllerType(Type t) {
return
t != null &&
t.IsPublic &&
t.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) &&
!t.IsAbstract &&
typeof(IController).IsAssignableFrom(t);
}
public void EnsureInitialized(IBuildManager buildManager) {
if (_cache == null) {
lock (_lockObj) {
if (_cache == null) {
List<Type> controllerTypes = GetAllControllerTypes(buildManager);
var groupedByName = controllerTypes.GroupBy(
t => t.Name.Substring(0, t.Name.Length - "Controller".Length),
StringComparer.OrdinalIgnoreCase);
_cache = groupedByName.ToDictionary(
g => g.Key,
g => g.ToLookup(t => t.Namespace ?? String.Empty, StringComparer.OrdinalIgnoreCase),
StringComparer.OrdinalIgnoreCase);
}
}
}
}
And ActionMethodSelector shows how to check if a method has desired attribute.
private static List<MethodInfo> RunSelectionFilters(ControllerContext controllerContext, List<MethodInfo> methodInfos) {
// remove all methods which are opting out of this request
// to opt out, at least one attribute defined on the method must return false
List<MethodInfo> matchesWithSelectionAttributes = new List<MethodInfo>();
List<MethodInfo> matchesWithoutSelectionAttributes = new List<MethodInfo>();
foreach (MethodInfo methodInfo in methodInfos) {
ActionMethodSelectorAttribute[] attrs = (ActionMethodSelectorAttribute[])methodInfo.GetCustomAttributes(typeof(ActionMethodSelectorAttribute), true /* inherit */);
if (attrs.Length == 0) {
matchesWithoutSelectionAttributes.Add(methodInfo);
}
else if (attrs.All(attr => attr.IsValidForRequest(controllerContext, methodInfo))) {
matchesWithSelectionAttributes.Add(methodInfo);
}
}
// if a matching action method had a selection attribute, consider it more specific than a matching action method
// without a selection attribute
return (matchesWithSelectionAttributes.Count > 0) ? matchesWithSelectionAttributes : matchesWithoutSelectionAttributes;
}

I don't think it's possible to give a simple answer to this question, because it depends on a lot of different things, including the implementation of IControllerFactory.
For instance, if you have a completely custom-built IControllerFactory implementation, all bets are off, because it may use any sort of mechanism to create Controller instances.
However, the DefaultControllerFactory looks after the appropriate Controller type in all the assemblies defined in the RouteCollection (configured in global.asax).
In this case, you could loop through all the assemblies associated with the RouteCollection, and look for Controllers in each.
Finding Controllers in a given assembly is relatively easy:
var controllerTypes = from t in asm.GetExportedTypes()
where typeof(IController).IsAssignableFrom(t)
select t;
where asm is an Assembly instance.

Related

Attribute.IsDefined returns false, Attribute.GetCustomAttribute returns null for defined ActionNameAttribute

I'm trying to set up some unit tests to ensure that URLs will be mapped to the appropriate controllers and actions according to a route-table, and that the target action-method and controller exists within the relevant assembly.
The only remaining problem I'm having is testing the existence of an action-method where an ActionNameAttribute has been applied to enable dash-separated action-name mappings, e.g., a "Contact Us" form url: /contact-us maps to the ContactUs method on a Forms controller because the ContactUs method signature is defined thusly:
[ActionName("contact-us")]
public ActionResult ContactUs()
I've set up the following method, which I am running inside each test, and works for all cases where the action-method name is not redefined with ActionNameAttribute:
private static bool ActionIsDefinedOnController(string expectedActionName, string controllerName, string assemblyName)
{
var thisControllerType = Type.GetType(AssemblyQualifiedName(controllerName, assemblyName), false, true);
if (thisControllerType == null)
return false;
var allThisControllersActions = thisControllerType.GetMethods().Select(m => m.Name.ToLower());
if( allThisControllersActions.Contains(expectedActionName.ToLower()))
return true;
var methods = thisControllerType.GetMethods();
//If we've so far failed to find the method, look for methods with ActionName attributes, and check in those values:
foreach (var method in methods)
{
if (Attribute.IsDefined(method, typeof(ActionNameAttribute))
{
var a = (ActionNameAttribute) Attribute.GetCustomAttribute(method, typeof (ActionNameAttribute));
if (a.Name == expectedActionName)
return true;
}
}
return false;
}
...but whenever a method's name is redifined with ActionNameAttribute, the check Attribute.IsDefined(method, typeof(ActionNameAttribute) is failing (returns false), even when I can see the attribute in the list of custom-attributes in my debugging session:
Why is this check failing, when it should be passing?
I've been able to construct a different check:
UPDATE I had pasted in the wrong code here initially, here's the revised:
List<string> customAttributes = method.GetCustomAttributes(false).Select(a => a.ToString()).ToList();
if (customAttributes.Contains("System.Web.Mvc.ActionNameAttribute"))
{
var a = (ActionNameAttribute) Attribute.GetCustomAttribute(method, typeof (ActionNameAttribute));
if (a.Name == expectedActionName)
return true;
}
...and now my condition is catching the cases where ActionNameAttribute is applied, but now Attribute.GetCustomAttribute() returns null. So I can't check the value of the action name to compare against the expected value... arrrrgh!
I would just have:
//If we've so far failed to find the method, look for methods with ActionName attributes, and check in those values:
foreach (var method in methods)
{
var attr = method.GetCustomAttribute<System.Web.Mvc.ActionNameAttribute>();
if (attr!=null && attr.Name == expectedActionName)
{
return true;
}
}
As I said in comment, I suspect that you're picking up the wrong ActionNameAttribute in your typeof calls, so I've been explicit

ASP.NET MVC 3 - Pass Session data to Layout

I want to show some data on every page with the layout (_Layout.cshtml), so I made a parent controller class, and the database access executes in its constructor. This works well except the case when I want to reach the session data, because when I try to check if the session variable exists, an exception (NullReferenceException) is thrown:
if (Session["UserId"] != null)
System.NullReferenceException: Object reference not set to an instance of an object.
I think that's because the Session object doesn't exist yet in the parent class. I can't find out another soluton to pass session related data to the layout, only when I copy the code to all action controllers. Any ideas?
Update:
Dave A, here is the parent class:
public class PCMarketController : Controller
{
protected PCMarketContext db = new PCMarketContext();
public PCMarketController()
{
int numberOfCartItems = 0;
if (Session["UserId"] != null) //Throws NullReferenceException in parent, works in action method
{
string UserId = HttpContext.Session["UserId"].ToString();
List<CartItem> CartItems = db.CartItems.Where(i => i.UserId == UserId).ToList();
foreach (var item in CartItems)
{
int count = item.Count;
numberOfCartItems += count;
}
}
ViewBag.NumberOfCartItems = " (" + numberOfCartItems + ")";
List<Category> Categories = db.Categories.ToList();
ViewBag.Categories = Categories;
}
}
You are correct in assuming the reason for Session to be null. The session HttpContext.Session injected at a later time in the page life cycle by the ControllerBuilder.
I would usually override OnActionExecuting method of the controller for such a case (http://msdn.microsoft.com/en-au/library/system.web.mvc.controller.onactionexecuting(v=vs.98).aspx).
One word of caution, using sessions may hinder the testability of your controller via standard unit tests
Cheers

How to check record ownership in controllers?

An application permits users to create records. For our purposes, let's call those records Goals.
One user should not be able to see Goals created by another user.
What is the best method for preventing UserA from accessing UserB's Goal?
I can do it like this:
//using asp.net membership
Guid uId = (Guid)System.Web.Security.Membership.GetUser().ProviderUserKey;
//goal records contain a foreign key to users, so I know who owns what
Goal theGoal = db.Goals.SingleOrDefault(g => g.GoalId == goalId
&& g.UserId == uId);
if (null == theGoal)
{
ViewData["error"] = "Can't find that goal.";
return View("Error");
}
else
{
return View(theGoal);
}
This works fine. The problem is that I've got similar code littered in every action that accesses goals.
Is there a more re-usable way of accomplishing this?
I thought of implementing it as an Authorization Filter. 2 problems with that scheme:
1) Requires the filter to know about and use the DB.
2) Requires 2 queries(1 in the filter, another in the action) instead of just the 1 query in the action that I have now.
What's a more DRY way of accomplishing this?
A custom model binder is a great place to do this:
public class GoalModelBinder : DefaultModelBinder
{
private readonly IGoalRepository _repository;
public GoalModelBinder(IGoalRepository repository)
{
_repository = repository;
}
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
// Here the default model binder does his job of binding stuff
// like the goal id which you would use in the repository to check
var goal = base.CreateModel(controllerContext, bindingContext, modelType) as Goal;
var user = controllerContext.HttpContext.User;
var theGoal = _repository.GetGoal(user, goal);
if (theGoal == null)
{
throw new HttpException(403, "Not authorized");
}
// It's OK, we've checked that the Goal belongs to the user
// => return it
return theGoal;
}
}
and then in your Application_Start register this model binder:
// some implementation of your repo
var sqlRepo = new SqlGoalRepository();
ModelBinders.Binders.Add(typeof(Goal), new GoalModelBinder(sqlRepo));
Now your controller action becomes less littered:
[Authorize]
public ActionResult Edit(Goal goal)
{
// if we get that far we are fine => we've got our goal
// and we are sure that it belongs to the currently logged user
return View(goal);
}

Plugin controllers, StructureMap and ASP.NET MVC

I'm using ASP.NET MVC (1.0) and StructureMap (2.5.3), I'm doing a plugin feature where dll's with controller are to be picked up in a folder. I register the controllers with SM (I am able to pick it up afterwards, so I know it's in there)
foreach (string file in path)
{
var assy = System.Reflection.Assembly.LoadFile(file);
Scan(x =>{
x.Assembly(assy);
x.AddAllTypesOf<IController>();
});
}
My problem is with the GetControllerInstance method of my override of DefaultControllerFactory. Everytime I send in enything else than a valid controller (valid in the sense that it is a part of the web project) I get the input Type parameter as null.
I've tried setting up specific routes for it.
I've done a test with Castle.Windsor and there it is not a problem.
Can anyone point me in the right direction? I'd appreciate it.
[Edit]
Here is the code:
-> Controller factory for Windsor
public WindsorControllerFactory()
{
container = new WindsorContainer(new XmlInterpreter(
new ConfigResource("castle")));
// Register all the controller types as transient
// This is for the regular controllers
var controllerTypes =
from t in
Assembly.GetExecutingAssembly().GetTypes()
where typeof(IController).IsAssignableFrom(t)
select t;
foreach (Type t in controllerTypes)
{
container.AddComponentLifeStyle(t.FullName, t,
LifestyleType.Transient);
}
/* Now the plugin controllers */
foreach (string file in Plugins() )
{
var assy = System.Reflection.Assembly.LoadFile(file);
var pluginContr =
from t in assy.GetTypes()
where typeof(IController).IsAssignableFrom(t)
select t;
foreach (Type t in pluginContr)
{
AddToPlugins(t);
/* This is the only thing I do, with regards to Windsor,
for the plugin Controllers */
container.AddComponentLifeStyle(t.FullName, t,
LifestyleType.Transient);
}
}
}
-> StructureMap; adding the controllers:
public class PluginRegistry : Registry
{
public PluginRegistry()
{
foreach (string file in Plugins() ) // Plugins return string[] of assemblies in the plugin folder
{
var assy = System.Reflection.Assembly.LoadFile(file);
Scan(x =>
{
x.Assembly(assy);
//x.AddAllTypesOf<IController>().
// NameBy(type => type.Name.Replace("Controller", ""));
x.AddAllTypesOf<IController>();
});
}
}
}
-> Controller factory for SM version
Not really doing much, as I'm registering the controllers with SM in the earlier step
public SMControllerFactory()
: base()
{
foreach (string file in Plugins() )
{
var assy = System.Reflection.Assembly.LoadFile(file);
var pluginContr =
from t in assy.GetTypes()
where typeof(IController).IsAssignableFrom(t)
select t;
foreach (Type t in pluginContr)
{
AddPlugin();
}
}
}
Can you post your controller factory?
I don't understand why Castle would work since I would think you would also get null passed in for the Type param of GetControllerInstance regardless of the DI framework you use inside that method. MVC is in charge of matching up the string name of the controller in the URL with a real type (unless you overrode those methods too). So I'm guessing it isn't the DI framework, but that MVC can't find your controller classes for some reason.

Unit testing my controller method results in an empty ViewName?

I'm doing some simple MS unit tests on my standard, nothing special controller.
When I check the ViewName proprty, from the returned ViewResult object, it's "" (empty).
I'm under the impression that the ViewName is implied by the name of the View (as suggested by this MS article on ASP.NET MVC controller testing).
BTW, when I test the ViewData, it's all there and correct.
Here's the code I have...
public ActionResult Index(int? page, string tag)
{
if (page == null || page <= 0)
{
page = 1;
}
var viewData = new IndexViewData
{
... my property setters, etc ...
};
return View(viewData);
}
[TestMethod]
public void Index_Action_Should_Return_Index_View_For_Default_HomePage()
{
// Arrange.
var controller = PostController; // Wrapper, cause I use D.I.
// Act.
ViewResult viewResult = controller.Index(null, null) as ViewResult;
// Assert.
Assert.IsNotNull(viewResult);
Assert.AreEqual("Index", viewResult.ViewName); // This is false/fails.
var indexViewData = viewResult.ViewData.Model as IndexViewData;
Assert.IsNotNull(indexViewData); // This is true.
}
The ViewName is only present when you set it in the ViewResult. If your View name matches your controller name, then I would check to ensure that the ViewName is null or empty as that would be (IMO) the correct behavior since you wouldn't want to set a name on the view. I only check that the ViewName is set when I intend that the View to be returned does not match the action -- say, when returning the "Error" view, for example.
EDIT: The following is the source for ExecuteResult in ViewResultBase.cs (from RC1, I don't have the source for RTW on my Macintosh). As you can see it checks to see if the ViewName has been set directly and if not, it pulls it from the action in the controller context's route data. This only happens in ExecuteResult, which is invoked AFTER your controller's action has completed.
public override void ExecuteResult(ControllerContext context) {
if (context == null) {
throw new ArgumentNullException("context");
}
if (String.IsNullOrEmpty(ViewName)) {
ViewName = context.RouteData.GetRequiredString("action");
}
ViewEngineResult result = null;
if (View == null) {
result = FindView(context);
View = result.View;
}
ViewContext viewContext = new ViewContext(context, View, ViewData, TempData);
View.Render(viewContext, context.HttpContext.Response.Output);
if (result != null) {
result.ViewEngine.ReleaseView(context, View);
}
}
I personally found the testing facilities provided by MVC2 to be somewhat clumsy. I'm guessing there is something better already extant, but I ended up creating a simple class to test actions. I modeled the interface (the implementation is another story) on a class provided by the excellent open source Java MVC framework Stripes called MockRoundTrip.
Here is the method used to get the action destination page when testing actions, called getTripDestination(). It returns the correct result irrespective of whether the viewname is explicitly set or not
//Get the destination page of the request, using Runtime execution pattern of MVC, namely
//if no ViewName is explicitly set in controller, ViewResult will have an empty ViewName
//Instead, current action name will be used in its place
public string getTripDestination()
{
RouteData routeData = getRouteData();
ViewResult viewResult = (result is ViewResult) ? (ViewResult)result : null;
string tripDestination = (viewResult != null) ? viewResult.ViewName : "";
return (tripDestination != "") ? tripDestination : (String)routeData.Values["action"];
}
private RouteData getRouteData()
{
HttpContextBase context = controller.ControllerContext.RequestContext.HttpContext;
return RouteTable.Routes.GetRouteData(context);
}
The viewname is set automatically by the framework. But when we unit test, we short-circuit the framework and there is nothing left to set the name.
So our actions need to set the viewname explicitly when we unit test. We could also check for null or empty if we really, really want to lean on the convention.
The documentation for Controller.View() states:
This method overload of the View class returns a ViewResult object
that has an empty ViewName property. If you are writing unit tests for
controller actions, take into account the empty ViewName property for
unit tests that do not take a string view name.
At run time, if the ViewName property is empty, the current action
name is used in place of the ViewName property.
So when expecting a view with the same name as the current action we can just test that it's an empty string.
Alternatively, the Controller.View(String) method will set the ViewName.

Resources