Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
252 views
in Technique[技术] by (71.8m points)

c# - Imlementing a Custom IRouter in ASP.NET 5 (vNext) MVC 6

I am attempting to convert this sample RouteBase implementation to work with MVC 6. I have worked out most of it by following the example in the Routing project, but I am getting tripped up on how to return the asynchronous Task from the method. I really don't care if it actually is asynchronous (cheers to anyone who can provide that answer), for now I just want to get it functioning.

I have the outgoing routes functioning (meaning ActionLink works fine when I put in the route values). The problem is with the RouteAsync method.

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);
}

The entire method runs all the way through to the end when there is a match. But when it is done executing, it doesn't call the Details method of the CustomPage controller, as it should. I just get a blank white page in the browser.

I added the WriteAsync line as was done in this post and it writes Hello there to the blank page, but I can't understand why MVC isn't calling my controller (in previous versions this worked without a hitch). Unfortunately, that post covered every part of routing except for how to implement an IRouter or INamedRouter.

How can I make the RouteAsync method function?

Entire CustomRoute Implementation

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 Registration

services.AddTransient<ICustomRoute, CustomRoute>();

MVC Route Configuration

// 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?}");
});

In case it matters I am using Beta 5, DNX 4.5.1 and DNX Core 5.

Solution

I created a generic solution that can be used for a simple primary key to URL 2-way mapping in this answer based on the information I learned here. The controller, action, data provider, and datatype of the primary key can be specified when wiring it into MVC 6 routing.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

As @opiants said, the problem is that you are doing nothing in your RouteAsync method.

If your intention is to end up calling a controller action method, you could use the following approach than the default MVC routes:

By default MVC uses a TemplateRoute with an inner target IRouter. In RouteAsync, the TemplateRoute will delegate to the inner IRouter. This inner router is being set as the MvcRouteHandler by the default builder extensions. In your case, start by adding an IRouter as your inner target:

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;
    }

Then update your startup to set that target as the MvcRouteHandler, which has already been set as 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?}");
});

Finally, update your AsyncRoute method to call the inner IRouter, which would be the MvcRouteHandler. You can use the implementation of that method in TemplateRoute as a guide. I have quickly used this approach and modified your method as follows:

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;
        }
    }
}

Update RC2

Looks like TemplateRoute is no longer around in RC2 aspnet Routing.

I investigated the history, and it was renamed RouteBase in commit 36180ab as part of a bigger refactoring.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...