我搜索了几个小时没有结果。我发现最好的方法是使用 Catch All 和一个 ErrorController 循环遍历映射的路由并以正则表达式的方式测试它们。因此,当一个请求没有匹配的路由时,它会进入 ErrorController,它会针对所有现有路由进行测试(所以 没有重复的路由),如果匹配,则调用该操作。这是代码。
最后一条路线:
routes.MapRoute(
"NotFound",
"{*url}",
new { controller = "Error", action = "PageNotFound" }
);
ErrorController:PageNotFound 动作由路由调用。如果匹配,则调用私有 Run 操作以运行带参数的操作。
public class ErrorController : Controller
{
private static readonly List<string> UrlNotContains = new List<string>{ "images/" };
// GET: Error
public ActionResult PageNotFound(string url)
{
if (url == null)
return HttpNotFound();
var routes = System.Web.Routing.RouteTable.Routes; /* Get routes */
foreach (var route in routes.Take(routes.Count - 3).Skip(2)) /* iterate excluding 2 firsts and 3 lasts (in my case) */
{
var r = (Route)route;
// Replace parameters by regex catch groups
var pattern = Regex.Replace(r.Url, @"{[^{}]*}", c =>
{
var parameterName = c.Value.Substring(1, c.Value.Length - 2);
// If parameter's constaint is a string, use it as the body
var body = r.Constraints.ContainsKey(parameterName) && r.Constraints[parameterName] is string
? r.Constraints[parameterName]
: @".+";
return $@"(?<{parameterName}>{body})";
});
// Test regex !
var regex = new Regex(pattern);
Match match = regex.Match(url);
if (!match.Success)
continue;
// If match, call the controller
var controllerName = r.Defaults["controller"].ToString();
var actionName = r.Defaults["action"].ToString();
var parameters = new Dictionary<string, object>();
foreach(var groupName in regex.GetGroupNames().Skip(1)) /* parameters are the groups catched by the regex */
parameters.Add(groupName, match.Groups[groupName].Value);
return Run(controllerName, actionName, parameters);
}
return HttpNotFound();
}
private ActionResult Run(string controllerName, string actionName, Dictionary<string, object> parameters)
{
// get the controller
var ctrlFactory = ControllerBuilder.Current.GetControllerFactory();
var ctrl = ctrlFactory.CreateController(this.Request.RequestContext, controllerName) as Controller;
var ctrlContext = new ControllerContext(this.Request.RequestContext, ctrl);
var ctrlDesc = new ReflectedControllerDescriptor(ctrl.GetType());
// get the action
var actionDesc = ctrlDesc.FindAction(ctrlContext, actionName);
// Change the route data so the good default view will be called in time
foreach (var parameter in parameters)
if (!ctrlContext.RouteData.Values.ContainsKey(parameter.Key))
ctrlContext.RouteData.Values.Add(parameter.Key, parameter.Value);
ctrlContext.RouteData.Values["controller"] = controllerName;
ctrlContext.RouteData.Values["action"] = actionName;
// To call the action in the controller, the parameter dictionary needs to have a value for each parameter, even the one with a default
var actionParameters = actionDesc.GetParameters();
foreach (var actionParameter in actionParameters)
{
if (parameters.ContainsKey(actionParameter.ParameterName)) /* If we already have a value for the parameter, change it's type */
parameters[actionParameter.ParameterName] = Convert.ChangeType(parameters[actionParameter.ParameterName], actionParameter.ParameterType);
else if (actionParameter.DefaultValue != null) /* If we have no value for it but it has a default value, use it */
parameters[actionParameter.ParameterName] = actionParameter.DefaultValue;
else if (actionParameter.ParameterType.IsClass) /* If the expected parameter is a class (like a ViewModel) */
{
var obj = Activator.CreateInstance(actionParameter.ParameterType); /* Instanciate it */
foreach (var propertyInfo in actionParameter.ParameterType.GetProperties()) /* Feed the properties */
{
// Get the property alias (If you have a custom model binding, otherwise, juste use propertyInfo.Name)
var aliasName = (propertyInfo.GetCustomAttributes(typeof(BindAliasAttribute), true).FirstOrDefault() as BindAliasAttribute)?.Alias ?? propertyInfo.Name;
var matchingKey = parameters.Keys.FirstOrDefault(k => k.Equals(aliasName, StringComparison.OrdinalIgnoreCase));
if (matchingKey != null)
propertyInfo.SetValue(obj, Convert.ChangeType(parameters[matchingKey], propertyInfo.PropertyType));
}
parameters[actionParameter.ParameterName] = obj;
}
else /* Parameter missing to call the action! */
return HttpNotFound();
}
// Set culture on the thread (Because my BaseController.BeginExecuteCore won't be called)
CultureHelper.SetImplementedCulture();
// Return the other action result as the current action result
return actionDesc.Execute(ctrlContext, parameters) as ActionResult;
}
}
相信我,这是我发现的唯一好方法。所以这里成功的关键是这个系统使用了参数约束来匹配。所以在这种情况下,问题会得到解决,因为你的 noteId 参数有一个约束。
限制:
- 如果存在多个无法通过正则表达式定义的模棱两可的参数