c# - route - select asp-for asp-items
Implementación de un IRouter personalizado en ASP.NET 5(vNext) MVC 6 (2)
Como dijo @opiants, el problema es que no está haciendo nada en su método RouteAsync
.
Si su intención es terminar llamando a un método de acción del controlador, podría usar el siguiente enfoque que las rutas MVC predeterminadas:
Por defecto, MVC usa un
TemplateRoute
con unIRouter
destinoIRouter
. En RouteAsync, el TemplateRoute delegará al IRouter interno. Este enrutador interno se está configurando comoMvcRouteHandler
por las extensiones de constructor predeterminadas. En su caso, comience agregando unIRouter
como su objetivo interno:
public class CustomRoute : ICustomRoute
{
private readonly IMemoryCache cache;
private readonly IRouter target;
private object synclock = new object();
public CustomRoute(IMemoryCache cache, IRouter target)
{
this.cache = cache;
this.target = target;
}
Luego actualice su inicio para establecer ese objetivo como el MvcRouteHandler
, que ya se ha establecido como routes.DefaultHandler
:
app.UseMvc(routes =>
{
routes.Routes.Add(
new CustomRoute(routes.ServiceProvider.GetRequiredService<IMemoryCache>(),
routes.DefaultHandler));
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
// Uncomment the following line to add a route for porting Web API 2 controllers.
// routes.MapWebApiRoute("DefaultApi", "api/{controller}/{id?}");
});
Finalmente, actualice su método AsyncRoute para llamar al IRouter
interno, que sería el MvcRouteHandler
. Puede usar la implementación de ese método en TemplateRoute
como guía. Rápidamente he usado este enfoque y he modificado su método de la siguiente manera:
public async Task RouteAsync(RouteContext context)
{
var requestPath = context.HttpContext.Request.Path.Value;
if (!string.IsNullOrEmpty(requestPath) && requestPath[0] == ''/'')
{
// Trim the leading slash
requestPath = requestPath.Substring(1);
}
// Get the page that matches.
var page = GetPageList()
.Where(x => x.VirtualPath.Equals(requestPath))
.FirstOrDefault();
// If we got back a null value set, that means the URI did not match
if (page == null)
{
return;
}
//Invoke MVC controller/action
var oldRouteData = context.RouteData;
var newRouteData = new RouteData(oldRouteData);
newRouteData.Routers.Add(this.target);
// TODO: You might want to use the page object (from the database) to
// get both the controller and action, and possibly even an area.
// Alternatively, you could create a route for each table and hard-code
// this information.
newRouteData.Values["controller"] = "CustomPage";
newRouteData.Values["action"] = "Details";
// This will be the primary key of the database row.
// It might be an integer or a GUID.
newRouteData.Values["id"] = page.Id;
try
{
context.RouteData = newRouteData;
await this.target.RouteAsync(context);
}
finally
{
// Restore the original values to prevent polluting the route data.
if (!context.IsHandled)
{
context.RouteData = oldRouteData;
}
}
}
Actualizar RC2
Parece que TemplateRoute
ya no existe en RC2 aspnet Routing.
Investigué la historia, y se renombró a RouteBase
en commit 36180ab como parte de una refactorización más grande.
Estoy intentando convertir esta implementación de RouteBase de muestra para trabajar con MVC 6. He trabajado la mayor parte de la misma siguiendo el ejemplo en el proyecto de Enrutamiento , pero me estoy preguntando cómo devolver la Task
asíncrona desde el método. Realmente no me importa si en realidad es asíncrono (saludos a cualquiera que pueda brindar esa respuesta), por ahora solo quiero que funcione.
Tengo las rutas salientes funcionando (lo que significa que ActionLink
funciona bien cuando pongo los valores de ruta). El problema está con el método RouteAsync
.
public Task RouteAsync(RouteContext context)
{
var requestPath = context.HttpContext.Request.Path.Value;
if (!string.IsNullOrEmpty(requestPath) && requestPath[0] == ''/'')
{
// Trim the leading slash
requestPath = requestPath.Substring(1);
}
// Get the page that matches.
var page = GetPageList()
.Where(x => x.VirtualPath.Equals(requestPath))
.FirstOrDefault();
// If we got back a null value set, that means the URI did not match
if (page != null)
{
var routeData = new RouteData();
// This doesn''t work
//var routeData = new RouteData(context.RouteData);
// This doesn''t work
//routeData.Routers.Add(this);
// This doesn''t work
//routeData.Routers.Add(new MvcRouteHandler());
// TODO: You might want to use the page object (from the database) to
// get both the controller and action, and possibly even an area.
// Alternatively, you could create a route for each table and hard-code
// this information.
routeData.Values["controller"] = "CustomPage";
routeData.Values["action"] = "Details";
// This will be the primary key of the database row.
// It might be an integer or a GUID.
routeData.Values["id"] = page.Id;
context.RouteData = routeData;
// When there is a match, the code executes to here
context.IsHandled = true;
// This test works
//await context.HttpContext.Response.WriteAsync("Hello there");
// This doesn''t work
//return Task.FromResult(routeData);
// This doesn''t work
//return Task.FromResult(context);
}
// This satisfies the return statement, but
// I''m not sure it is the right thing to return.
return Task.FromResult(0);
}
Todo el método se ejecuta hasta el final cuando hay una coincidencia. Pero cuando termina de ejecutarse, no llama al método de Details
del controlador CustomPage
, como debería. Acabo de recibir una página en blanco en el navegador.
WriteAsync
línea WriteAsync
como se hizo en esta publicación y escribe Hello there
en la página en blanco, pero no puedo entender por qué MVC no está llamando a mi controlador (en versiones anteriores esto funcionó sin problemas). Desafortunadamente, esa publicación cubrió cada parte del enrutamiento, excepto cómo implementar un IRouter
o INamedRouter
.
¿Cómo puedo hacer que el método RouteAsync
funcione?
Implementación completa de CustomRoute
using Microsoft.AspNet.Routing;
using Microsoft.Framework.Caching.Memory;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
public class PageInfo
{
// VirtualPath should not have a leading slash
// example: events/conventions/mycon
public string VirtualPath { get; set; }
public int Id { get; set; }
}
public interface ICustomRoute : IRouter
{ }
public class CustomRoute : ICustomRoute
{
private readonly IMemoryCache cache;
private object synclock = new object();
public CustomRoute(IMemoryCache cache)
{
this.cache = cache;
}
public Task RouteAsync(RouteContext context)
{
var requestPath = context.HttpContext.Request.Path.Value;
if (!string.IsNullOrEmpty(requestPath) && requestPath[0] == ''/'')
{
// Trim the leading slash
requestPath = requestPath.Substring(1);
}
// Get the page that matches.
var page = GetPageList()
.Where(x => x.VirtualPath.Equals(requestPath))
.FirstOrDefault();
// If we got back a null value set, that means the URI did not match
if (page != null)
{
var routeData = new RouteData();
// TODO: You might want to use the page object (from the database) to
// get both the controller and action, and possibly even an area.
// Alternatively, you could create a route for each table and hard-code
// this information.
routeData.Values["controller"] = "CustomPage";
routeData.Values["action"] = "Details";
// This will be the primary key of the database row.
// It might be an integer or a GUID.
routeData.Values["id"] = page.Id;
context.RouteData = routeData;
context.IsHandled = true;
}
return Task.FromResult(0);
}
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
VirtualPathData result = null;
PageInfo page = null;
// Get all of the pages from the cache.
var pages = GetPageList();
if (TryFindMatch(pages, context.Values, out page))
{
result = new VirtualPathData(this, page.VirtualPath);
context.IsBound = true;
}
return result;
}
private bool TryFindMatch(IEnumerable<PageInfo> pages, IDictionary<string, object> values, out PageInfo page)
{
page = null;
int id;
object idObj;
object controller;
object action;
if (!values.TryGetValue("id", out idObj))
{
return false;
}
id = Convert.ToInt32(idObj);
values.TryGetValue("controller", out controller);
values.TryGetValue("action", out action);
// The logic here should be the inverse of the logic in
// GetRouteData(). So, we match the same controller, action, and id.
// If we had additional route values there, we would take them all
// into consideration during this step.
if (action.Equals("Details") && controller.Equals("CustomPage"))
{
page = pages
.Where(x => x.Id.Equals(id))
.FirstOrDefault();
if (page != null)
{
return true;
}
}
return false;
}
private IEnumerable<PageInfo> GetPageList()
{
string key = "__CustomPageList";
IEnumerable<PageInfo> pages;
// Only allow one thread to poplate the data
if (!this.cache.TryGetValue(key, out pages))
{
lock (synclock)
{
if (!this.cache.TryGetValue(key, out pages))
{
// TODO: Retrieve the list of PageInfo objects from the database here.
pages = new List<PageInfo>()
{
new PageInfo() { Id = 1, VirtualPath = "somecategory/somesubcategory/content1" },
new PageInfo() { Id = 2, VirtualPath = "somecategory/somesubcategory/content2" },
new PageInfo() { Id = 3, VirtualPath = "somecategory/somesubcategory/content3" }
};
this.cache.Set(key, pages,
new MemoryCacheEntryOptions()
{
Priority = CacheItemPriority.NeverRemove,
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15)
});
}
}
}
return pages;
}
}
CustomRoute DI Registro
services.AddTransient<ICustomRoute, CustomRoute>();
Configuración de ruta MVC
// Add MVC to the request pipeline.
app.UseMvc(routes =>
{
routes.Routes.Add(routes.ServiceProvider.GetService<ICustomRoute>());
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
// Uncomment the following line to add a route for porting Web API 2 controllers.
// routes.MapWebApiRoute("DefaultApi", "api/{controller}/{id?}");
});
En caso de que importe, estoy usando Beta 5
, DNX 4.5.1
y DNX Core 5
.
Solución
Creé una solución genérica que se puede usar para una clave primaria simple para la asignación de URL de 2 vías en esta respuesta, en base a la información que aprendí aquí. El controlador, la acción, el proveedor de datos y el tipo de datos de la clave principal se pueden especificar al cablearlo en el enrutamiento MVC 6.
La razón principal por la que eso no funciona es porque no estás haciendo nada en el método RouteAsync
. Otra razón es que la forma en que funciona el enrutamiento en MVC 6 es muy diferente a la forma en que funcionó el enrutamiento anterior de MVC, por lo que probablemente sea mejor escribirlo desde cero utilizando el código fuente como referencia, ya que hay muy pocos artículos que abordan MVC 6 en el momento.
EDITAR: la respuesta de @Daniel JG tiene mucho más sentido que esto, así que úselo si es posible. Esto podría encajar en el caso de uso de otra persona, así que lo dejo aquí.
Aquí hay una implementación muy simple de IRouter
usando beta7 . Esto debería funcionar pero es probable que tengas que rellenar los huecos. Deberá eliminar la page != null
y reemplazarla con el código a continuación y reemplazar los controladores y las acciones:
if (page == null)
{
// Move to next router
return;
}
// TODO: Replace with correct controller
var controllerType = typeof(HomeController);
// TODO: Replace with correct action
var action = nameof(HomeController.Index);
// This is used to locate the razor view
// Remove the trailing "Controller" string
context.RouteData.Values["Controller"] = controllerType.Name.Substring(0, controllerType.Name.Length - 10);
var actionInvoker = context.HttpContext.RequestServices.GetRequiredService<IActionInvokerFactory>();
var descriptor = new ControllerActionDescriptor
{
Name = action,
MethodInfo = controllerType.GetTypeInfo().DeclaredMethods.Single(m => m.Name == action),
ControllerTypeInfo = controllerType.GetTypeInfo(),
// Setup filters
FilterDescriptors = new List<FilterDescriptor>(),
// Setup DI properties
BoundProperties = new List<ParameterDescriptor>(0),
// Setup action arguments
Parameters = new List<ParameterDescriptor>(0),
// Setup route constraints
RouteConstraints = new List<RouteDataActionConstraint>(0),
// This router will work fine without these props set
//ControllerName = "Home",
//DisplayName = "Home",
};
var accessor = context.HttpContext.RequestServices.GetRequiredService<IActionContextAccessor>();
accessor.ActionContext = new ActionContext(context.HttpContext, context.RouteData, descriptor);
var actionInvokerFactory = context.HttpContext.RequestServices.GetRequiredService<IActionInvokerFactory>();
var invoker = actionInvokerFactory.CreateInvoker(accessor.ActionContext);
// Render the page
await invoker.InvokeAsync();
// Don''t execute the next IRouter
context.IsHandled = true;
return;
Asegúrese de agregar una referencia al espacio de nombres Microsoft.Framework.DependencyInjection
para resolver la extensión GetRequiredService
.
Después de eso, registre el IRouter como se muestra a continuación:
app.UseMvc(routes =>
{
// Run before any default IRouter implementation
// or use .Add to run after all the default IRouter implementations
routes.Routes.Insert(0, routes.ServiceProvider.GetRequiredService<CustomRoute>());
// .. more code here ...
});
Entonces simplemente registra eso en tu COI,
services.AddSingleton<CustomRoute>();
Otro enfoque ''más limpio'' probablemente sería crear una implementación diferente de IActionSelector
.