Tutto le soluzioni che ho visto finora hanno uno svantaggio: mentre cambiano il nome del controllore o dell'azione, non garantiscono la coerenza tra queste due entità. Si può specificare un'azione da un controller diverso:
public class HomeController : Controller
{
public ActionResult HomeAction() { ... }
}
public class AnotherController : Controller
{
public ActionResult AnotherAction() { ... }
private void Process()
{
Url.Action(nameof(AnotherAction), nameof(HomeController));
}
}
per renderlo ancora peggio, questo approccio non può tenere conto dei numerosi attributi si può applicare ai controllori e/o azioni di cambiare il routing, per esempio RouteAttribute
e RoutePrefixAttribute
, quindi qualsiasi modifica al routing basato sugli attributi potrebbe passare inosservata.
Infine, il Url.Action()
sé non garantisce la coerenza tra metodo di azione e dei suoi parametri che costituiscono l'URL:
public class HomeController : Controller
{
public ActionResult HomeAction(int id, string name) { ... }
private void Process()
{
Url.Action(nameof(HomeAction), new { identity = 1, title = "example" });
}
}
La mia soluzione è basata su Expression
e metadati:
public static class ActionHelper<T> where T : Controller
{
public static string GetUrl(Expression<Func<T, Func<ActionResult>>> action)
{
return GetControllerName() + '/' + GetActionName(GetActionMethod(action));
}
public static string GetUrl<U>(
Expression<Func<T, Func<U, ActionResult>>> action, U param)
{
var method = GetActionMethod(action);
var parameters = method.GetParameters();
return GetControllerName() + '/' + GetActionName(method) +
'?' + GetParameter(parameters[0], param);
}
public static string GetUrl<U1, U2>(
Expression<Func<T, Func<U1, U2, ActionResult>>> action, U1 param1, U2 param2)
{
var method = GetActionMethod(action);
var parameters = method.GetParameters();
return GetControllerName() + '/' + GetActionName(method) +
'?' + GetParameter(parameters[0], param1) +
'&' + GetParameter(parameters[1], param2);
}
private static string GetControllerName()
{
const string SUFFIX = nameof(Controller);
string name = typeof(T).Name;
return name.EndsWith(SUFFIX) ? name.Substring(0, name.Length - SUFFIX.Length) : name;
}
private static MethodInfo GetActionMethod(LambdaExpression expression)
{
var unaryExpr = (UnaryExpression)expression.Body;
var methodCallExpr = (MethodCallExpression)unaryExpr.Operand;
var methodCallObject = (ConstantExpression)methodCallExpr.Object;
var method = (MethodInfo)methodCallObject.Value;
Debug.Assert(method.IsPublic);
return method;
}
private static string GetActionName(MethodInfo info)
{
return info.Name;
}
private static string GetParameter<U>(ParameterInfo info, U value)
{
return info.Name + '=' + Uri.EscapeDataString(value.ToString());
}
}
Ciò impedisce dal passaggio di parametri errati per generare un URL:
ActionHelper<HomeController>.GetUrl(controller => controller.HomeAction, 1, "example");
Poiché è un'espressione lambda, l'azione è sempre vincolata al relativo controller. (E hai anche Intellisense!) Una volta scelta l'azione, ti costringe a specificare tutti i suoi parametri di tipo corretto.
Il codice indicato continua a non risolvere il problema di routing, tuttavia è almeno possibile fissarlo, poiché sono disponibili sia Type.Attributes
sia MethodInfo.Attributes
del controller.
EDIT:
Come @CarterMedlin sottolineato, parametri di azione di tipo non-primitivo non possono avere uno-a-uno vincolante per interrogare parametri. Attualmente, questo viene risolto chiamando ToString()
che può essere sovrascritto nella classe parametro appositamente per questo scopo. Tuttavia, l'approccio potrebbe non essere sempre applicabile, né controlla il nome del parametro.
Per risolvere il problema, è possibile dichiarare la seguente interfaccia:
public interface IUrlSerializable
{
Dictionary<string, string> GetQueryParams();
}
e implementarlo nella classe di parametri:
public class HomeController : Controller
{
public ActionResult HomeAction(Model model) { ... }
}
public class Model : IUrlSerializable
{
public int Id { get; set; }
public string Name { get; set; }
public Dictionary<string, string> GetQueryParams()
{
return new Dictionary<string, string>
{
[nameof(Id)] = Id,
[nameof(Name)] = Name
};
}
}
E rispettive modifiche ActionHelper
:
public static class ActionHelper<T> where T : Controller
{
...
private static string GetParameter<U>(ParameterInfo info, U value)
{
var serializableValue = value as IUrlSerializable;
if (serializableValue == null)
return GetParameter(info.Name, value.ToString());
return String.Join("&",
serializableValue.GetQueryParams().Select(param => GetParameter(param.Key, param.Value)));
}
private static string GetParameter(string name, string value)
{
return name + '=' + Uri.EscapeDataString(value);
}
}
Come potete vedere, ha ancora un fallback per ToString()
, quando la classe parametro non implementa l'interfaccia.
Usage:
ActionHelper<HomeController>.GetUrl(controller => controller.HomeAction, new Model
{
Id = 1,
Name = "example"
});
Suppongo che si possa fare un extensionmethod in poi 'classe Controller' whhich potrebbe tagliare la parte "Controller" lontano da voi. Ma non ho ancora visto una raccomandazione specifica su come usarla. –
Sembra che [T4MVC] (https://t4mvc.codeplex.com/) sia più adatto per questo T4 utilizzato da –
. Ora con nameof come funzione di una lingua, suppongo che sia meglio rimanere in C# ogni volta che è possibile. – Mikeon