ASP.NET MVC Enrutamiento a través de atributos de método [cerrado]

80

En el StackOverflow Podcast # 54 , Jeff menciona que registran sus rutas URL en la base de código StackOverflow a través de un atributo sobre el método que maneja la ruta. Suena como un buen concepto (con la salvedad que planteó Phil Haack sobre las prioridades de ruta).

¿Alguien podría proporcionar alguna muestra para que esto suceda?

Además, ¿alguna "mejor práctica" para usar este estilo de enrutamiento?

TorgoGuy
fuente

Respuestas:

62

ACTUALIZACIÓN : Esto ha sido publicado en codeplex . El código fuente completo, así como el ensamblado precompilado, están disponibles para descargar. Todavía no he tenido tiempo de publicar la documentación en el sitio, por lo que esta publicación SO tendrá que ser suficiente por ahora.

ACTUALIZACIÓN : agregué algunos atributos nuevos para manejar 1) orden de ruta, 2) restricciones de parámetro de ruta y 3) valores predeterminados de parámetro de ruta. El texto a continuación refleja esta actualización.

De hecho, hice algo como esto para mis proyectos MVC (no tengo idea de cómo lo está haciendo Jeff con stackoverflow). Definí un conjunto de atributos personalizados: UrlRoute, UrlRouteParameterConstraint, UrlRouteParameterDefault. Se pueden adjuntar a los métodos de acción del controlador MVC para hacer que las rutas, restricciones y valores predeterminados se vinculen a ellos automáticamente.

Uso de ejemplo:

(Tenga en cuenta que este ejemplo es algo artificial pero demuestra la función)

public class UsersController : Controller
{
    // Simple path.
    // Note you can have multiple UrlRoute attributes affixed to same method.
    [UrlRoute(Path = "users")]
    public ActionResult Index()
    {
        return View();
    }

    // Path with parameter plus constraint on parameter.
    // You can have multiple constraints.
    [UrlRoute(Path = "users/{userId}")]
    [UrlRouteParameterConstraint(Name = "userId", Regex = @"\d+")]
    public ActionResult UserProfile(int userId)
    {
        // ...code omitted

        return View();
    }

    // Path with Order specified, to ensure it is added before the previous
    // route.  Without this, the "users/admin" URL may match the previous
    // route before this route is even evaluated.
    [UrlRoute(Path = "users/admin", Order = -10)]
    public ActionResult AdminProfile()
    {
        // ...code omitted

        return View();
    }

    // Path with multiple parameters and default value for the last
    // parameter if its not specified.
    [UrlRoute(Path = "users/{userId}/posts/{dateRange}")]
    [UrlRouteParameterConstraint(Name = "userId", Regex = @"\d+")]
    [UrlRouteParameterDefault(Name = "dateRange", Value = "all")]
    public ActionResult UserPostsByTag(int userId, string dateRange)
    {
        // ...code omitted

        return View();
    }

Definición de UrlRouteAttribute:

/// <summary>
/// Assigns a URL route to an MVC Controller class method.
/// </summary>
[AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class UrlRouteAttribute : Attribute
{
    /// <summary>
    /// Optional name of the route.  If not specified, the route name will
    /// be set to [controller name].[action name].
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    /// Path of the URL route.  This is relative to the root of the web site.
    /// Do not append a "/" prefix.  Specify empty string for the root page.
    /// </summary>
    public string Path { get; set; }

    /// <summary>
    /// Optional order in which to add the route (default is 0).  Routes
    /// with lower order values will be added before those with higher.
    /// Routes that have the same order value will be added in undefined
    /// order with respect to each other.
    /// </summary>
    public int Order { get; set; }
}

Definición de UrlRouteParameterConstraintAttribute:

/// <summary>
/// Assigns a constraint to a route parameter in a UrlRouteAttribute.
/// </summary>
[AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class UrlRouteParameterConstraintAttribute : Attribute
{
    /// <summary>
    /// Name of the route parameter on which to apply the constraint.
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    /// Regular expression constraint to test on the route parameter value
    /// in the URL.
    /// </summary>
    public string Regex { get; set; }
}

Definición de UrlRouteParameterDefaultAttribute:

/// <summary>
/// Assigns a default value to a route parameter in a UrlRouteAttribute
/// if not specified in the URL.
/// </summary>
[AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class UrlRouteParameterDefaultAttribute : Attribute
{
    /// <summary>
    /// Name of the route parameter for which to supply the default value.
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    /// Default value to set on the route parameter if not specified in the URL.
    /// </summary>
    public object Value { get; set; }
}

Cambios en Global.asax.cs:

Reemplace las llamadas a MapRoute, con una sola llamada a la función RouteUtility.RegisterUrlRoutesFromAttributes:

    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        RouteUtility.RegisterUrlRoutesFromAttributes(routes);
    }

Definición de RouteUtility.RegisterUrlRoutesFromAttributes:

La fuente completa está disponible en el codeplex . Vaya al sitio si tiene comentarios o informes de errores.

DSO
fuente
Supongo que hacerlo con atributos evita el uso de rutas predeterminadas y restricciones de ruta ...
Nicolas Cadilhac
Con este enfoque, nunca he necesitado rutas predeterminadas porque estás vinculando cada ruta a un método específico. Tiene razón sobre las limitaciones. Busqué la posibilidad de agregar restricciones como una propiedad de atributo, pero encontré un inconveniente porque las restricciones de MVC se especifican mediante objetos anónimos y las propiedades de atributo solo pueden ser tipos simples. Creo que todavía es posible hacer restricciones como atributo (con más codificación), pero aún no me he molestado con eso porque realmente no he necesitado restricciones en mi trabajo MVC hasta este punto (tiendo a validar los valores de ruta en el controlador).
DSO
3
¡Muy agradable! Nuestro RouteAttribute es muy similar a esto, solo que agrega un poco de funcionalidad de ayuda adicional. Tendré que agregar una respuesta detallando las diferencias.
Jarrod Dixon
1
Esto es fantástico. Me encanta.
BowserKingKoopa
1
¡Esto es genial! He estado usando MvcContrib por un tiempo y no tenía idea de que esto estaba allí. Mencionaste en tu publicación original que no tuviste tiempo para documentarlo. ¿Sigue siendo el caso? Parece que al menos una mención de él en los documentos de MvcContrib sería muy útil para que los desarrolladores al menos sepan que está ahí. ¡Gracias!
Todd Menier
44

También puede probar AttributeRouting , que está disponible en github o mediante nuget .

Este es un enchufe descarado, ya que soy el autor del proyecto. Pero maldición si no estoy muy feliz usándolo. Puede que tú también lo estés. Hay mucha documentación y código de muestra en la wiki del repositorio de github .

Con esta biblioteca, puede hacer mucho:

  • Decora tus acciones con los atributos GET, POST, PUT y DELETE.
  • Asigne múltiples rutas a una sola acción, ordenándolas con una propiedad Order.
  • Especifique los valores predeterminados y las restricciones de la ruta mediante atributos.
  • Especifique parámetros opcionales con un simple? token antes del nombre del parámetro.
  • Especifique el nombre de la ruta para admitir rutas con nombre.
  • Defina áreas MVC en un controlador o controlador base.
  • Agrupe o anide sus rutas usando prefijos de ruta aplicados a un controlador o controlador base.
  • Admite URL heredadas.
  • Establezca la precedencia de rutas entre las rutas definidas para una acción, dentro de un controlador y entre controladores y controladores base.
  • Genere urls salientes en minúsculas automáticamente.
  • Defina sus propias convenciones de ruta personalizadas y aplíquelas en un controlador para generar las rutas para acciones dentro del controlador sin atributos repetitivos (piense en el estilo RESTful).
  • Depura tus rutas usando un HttpHandler proporcionado.

Estoy seguro de que me estoy olvidando de otras cosas. Echale un vistazo. Es fácil de instalar a través de nuget.

NOTA: A partir del 16 de abril de 2012, AttributeRouting también es compatible con la nueva infraestructura de API web. En caso de que esté buscando algo que pueda manejar eso. ¡Gracias subkamran !

Mancha
fuente
10
Este proyecto parece más maduro (mejor documentación, más funciones, conjunto de pruebas completo) que las otras opciones mencionadas
David Laing
3
Estoy de acuerdo, esto parece hacer todo lo que pueda desear, y con una buena documentación de ejemplo.
Mike Chamberlain
3
Muchas gracias. Estoy felizmente usando esta solución y ha resuelto todos mis conflictos de enrutamiento, ambigüedad y confusión.
Valamas
3
Oye, deberías escribir las viñetas anteriores en tu página de github, ya que encontré esta publicación SO cuando estaba buscando más detalles :)
GONeale
2
Solo para jugar al defensor del diablo, ¿hay algún beneficio en declarar todas sus rutas en un solo lugar? ¿Perdemos algo o estamos limitados de alguna manera al cambiar a este método?
GONeale
9

1. Descargue RiaLibrary.Web.dll y haga referencia a él en su proyecto de sitio web ASP.NET MVC

2. Decore los métodos del controlador con los atributos [Url]:

public SiteController : Controller
{
    [Url("")]
    public ActionResult Home()
    {
        return View();
    }

    [Url("about")]
    public ActionResult AboutUs()
    {
        return View();
    }

    [Url("store/{?category}")]
    public ActionResult Products(string category = null)
    {
        return View();
    }
}

Por cierto, '?' Iniciar sesión El parámetro '{? category}' significa que es opcional. No necesitará especificar esto explícitamente en los valores predeterminados de ruta, que es igual a esto:

routes.MapRoute("Store", "store/{category}",
new { controller = "Store", action = "Home", category = UrlParameter.Optional });

3. Actualice el archivo Global.asax.cs

public class MvcApplication : System.Web.HttpApplication
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        routes.MapRoutes(); // This does the trick
    }

    protected void Application_Start()
    {
        RegisterRoutes(RouteTable.Routes);
    }
}

¿Cómo establecer valores predeterminados y restricciones? Ejemplo:

public SiteController : Controller
{
    [Url("admin/articles/edit/{id}", Constraints = @"id=\d+")]
    public ActionResult ArticlesEdit(int id)
    {
        return View();
    }

    [Url("articles/{category}/{date}_{title}", Constraints =
         "date=(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])")]
    public ActionResult Article(string category, DateTime date, string title)
    {
        return View();
    }
}

¿Cómo configurar el pedido? Ejemplo:

[Url("forums/{?category}", Order = 2)]
public ActionResult Threads(string category)
{
    return View();
}

[Url("forums/new", Order = 1)]
public ActionResult NewThread()
{
    return View();
}
Konstantin Tarkus
fuente
1
¡Muy agradable! Me gusta especialmente la {?param}nomenclatura de los parámetros opcionales.
Jarrod Dixon
3

Esta publicación es solo para extender la respuesta de DSO.

Al convertir mis rutas en atributos, necesitaba manejar el atributo ActionName. Entonces en GetRouteParamsFromAttribute:

ActionNameAttribute anAttr = methodInfo.GetCustomAttributes(typeof(ActionNameAttribute), false)
    .Cast<ActionNameAttribute>()
    .SingleOrDefault();

// Add to list of routes.
routeParams.Add(new MapRouteParams()
{
    RouteName = routeAttrib.Name,
    Path = routeAttrib.Path,
    ControllerName = controllerName,
    ActionName = (anAttr != null ? anAttr.Name : methodInfo.Name),
    Order = routeAttrib.Order,
    Constraints = GetConstraints(methodInfo),
    Defaults = GetDefaults(methodInfo),
});

También encontré que el nombre de la ruta no era adecuado. El nombre se crea dinámicamente con controllerName.RouteName. Pero los nombres de mis rutas son cadenas constantes en la clase del controlador y las utilizo para llamar también a Url.RouteUrl. Es por eso que realmente necesito que el nombre de la ruta en el atributo sea el nombre real de la ruta.

Otra cosa que haré es convertir los atributos predeterminados y de restricción a AttributeTargets.Parameter para poder pegarlos a params.

Nicolás Cadilhac
fuente
Sí, me incliné por el comportamiento de denominación de rutas. Probablemente sea mejor hacer lo que hizo, simplemente use lo que está en el atributo tal como está o hágalo nulo. Buena idea poner las restricciones por defecto en los propios parámetros. Probablemente publique esto en el codeplex en algún momento para administrar mejor los cambios.
DSO
0

He combinado estos dos enfoques en una versión frankensteiniana para cualquiera que lo desee. (Me gustó la notación param opcional, pero también pensé que deberían ser atributos separados de los valores predeterminados / restricciones en lugar de todos mezclados en uno).

http://github.com/djMax/AlienForce/tree/master/Utilities/Web/

Max Metral
fuente
0

Necesitaba que el enrutamiento de ITCloud funcionara en asp.net mvc 2 usando un AsyncController; para hacerlo, simplemente edite la clase RouteUtility.cs en la fuente y vuelva a compilar. Tienes que quitar "Completado" del nombre de la acción en la línea 98

// Add to list of routes.
routeParams.Add(new MapRouteParams()
{
    RouteName = String.IsNullOrEmpty(routeAttrib.Name) ? null : routeAttrib.Name,
    Path = routeAttrib.Path,
    ControllerName = controllerName,
    ActionName = methodInfo.Name.Replace("Completed", ""),
    Order = routeAttrib.Order,
    Constraints = GetConstraints(methodInfo),
    Defaults = GetDefaults(methodInfo),
    ControllerNamespace = controllerClass.Namespace,
});

Luego, en AsyncController, decore el ActionResult XXXXCompleted con los atributos UrlRoutey familiares UrlRouteParameterDefault:

[UrlRoute(Path = "ActionName/{title}")]
[UrlRouteParameterDefault(Name = "title", Value = "latest-post")]
public ActionResult ActionNameCompleted(string title)
{
    ...
}

Espero que ayude a alguien con el mismo problema.

TimDog
fuente
Para su información, la convención es tener los atributos relacionados con MVC en el método ActionNameAsync y no en el método ActionNameCompleted.
Erv Walter
Gracias - no me di cuenta de eso.
TimDog