简体   繁体   English

将责任从另一个控制器中的短URL重定向到完整URL

[英]Redirecting responsibility from short to full URL in another controller

As a homework I have to do a simple URL shortener, where I can add full link to list, which is processed by Hashids.net library , and I get short version of an URL. 作为一个家庭作业,我必须做一个简单的URL缩短,我可以添加完整的链接到列表,由Hashids.net库处理,我得到一个URL的简短版本。

CLICK

I've got something like this now, but I got stuck on redirecting it back to full link. 我现在有这样的东西,但我被困在重定向到完整的链接。

I would like to add a new controller, which will take the responsibility of redirecting short URL to full URL. 我想添加一个新的控制器,它将负责将短URL重定向到完整的URL。 After clicking short URL it should go to localhost:xxxx/ShortenedUrl and then redirect to full link. 点击短网址后,应转到localhost:xxxx/ShortenedUrl ,然后重定向到完整链接。 Any tips how can I create this? 任何提示我如何创建这个?

I was trying to do it by @Html.ActionLink(@item.ShortenedLink, "Index", "Redirect") and return Redirect(fullLink) in Redirect controller but it didn't work as I expect. 我试图通过@Html.ActionLink(@item.ShortenedLink, "Index", "Redirect")它并在Redirect控制器中return Redirect(fullLink) ,但它没有按照我的预期工作。

And one more question about routes, how can I achieve that after clicking short URL it will give me localhost:XXXX/ShortenedURL (ie localhost:XXXX/FSIAOFJO2@ ). 还有一个关于路由的问题,如何在点击短网址后实现这一点,它会给我localhost:XXXX/ShortenedURL (即localhost:XXXX/FSIAOFJO2@ )。 Now I've got 现在我有了

<a href="@item.ShortenedLink">@Html.DisplayFor(model => item.ShortenedLink)</a> 

and

app.UseMvc(routes =>
{ 
    routes.MapRoute("default", "{controller=Link}/{action=Index}");
});

but it gives me localhost:XXXX/Link/ShortenedURL , so I would like to omit this Link in URL. 但它给了我localhost:XXXX/Link/ShortenedURL ,所以我想在URL中省略这个链接。

View (part with Short URL): 查看 (部分短网址):

 <td>@Html.ActionLink(item.ShortenedLink,"GoToFull","Redirect", new { target = "_blank" }))</td>

Link controller: 链接控制器:

public class LinkController : Controller
{
    private ILinksRepository _repository;

    public LinkController(ILinksRepository linksRepository)
    {
        _repository = linksRepository;
    }

    [HttpGet]
    public IActionResult Index()
    {
        var links = _repository.GetLinks();
        return View(links);
    }

    [HttpPost]
    public IActionResult Create(Link link)
    {
        _repository.AddLink(link);
        return Redirect("Index");
    }

    [HttpGet]
    public IActionResult Delete(Link link)
    {
        _repository.DeleteLink(link);
        return Redirect("Index");
    }
}

Redirect controller which I am trying to do: 我正在尝试重定向控制器:

private ILinksRepository _repository;

public RedirectController(ILinksRepository linksRepository)
{
    _repository = linksRepository;
}

public IActionResult GoToFull()
{
    var links = _repository.GetLinks();
    return Redirect(links[0].FullLink);
}

Is there a better way to get access to links list in Redirect Controller? 有没有更好的方法来访问Redirect Controller中的链接列表?

This is my suggestion, trigger the link via AJAX, here is working example: 这是我的建议,通过AJAX触发链接,这是工作示例:

This is the HTML element binded through model: 这是通过模型绑定的HTML元素:

@Html.ActionLink(Model.ShortenedLink, "", "", null, 
new { onclick = "fncTrigger('" + "http://www.google.com" + "');" })

This is the javascript ajax code: 这是javascript ajax代码:

function fncTrigger(id) {

            $.ajax({
                url: '@Url.Action("TestDirect", "Home")',
                type: "GET",
                data: { id: id },
                success: function (e) {
                },
                error: function (err) {
                    alert(err);
                },
            });
    }

Then on your controller to receive the ajax click: 然后在您的控制器上接收ajax点击:

 public ActionResult TestDirect(string id)
    {
        return JavaScript("window.location = '" + id + "'");
    }

Basically what I am doing here is that, after I click the link, it will call the TestDirect action, then redirect it to using the passed url parameter. 基本上我在这里做的是,在我点击链接后,它将调用TestDirect操作,然后使用传递的url参数将其重定向到。 You can do the conversion inside this action. 您可以在此操作中执行转换。

To create dynamic data-driven URLs, you need to create a custom IRouter . 要创建动态数据驱动的URL,您需要创建自定义IRouter Here is how it can be done: 以下是它的完成方式:

CachedRoute<TPrimaryKey>

This is a reusable generic class that maps a set of dynamically provided URLs to a single action method. 这是一个可重用的泛型类,它将一组动态提供的URL映射到单个操作方法。 You can inject an ICachedRouteDataProvider<TPrimaryKey> to provide the data (a URL to primary key mapping). 您可以注入ICachedRouteDataProvider<TPrimaryKey>来提供数据(主键映射的URL)。

The data is cached to prevent multiple simultaneous requests from overloading the database (routes run on every request). 缓存数据以防止多个同时请求重载数据库(每个请求都运行路由)。 The default cache time is for 15 minutes, but you can adjust as necessary for your requirements. 默认缓存时间为15分钟,但您可以根据需要进行调整。

If you want it to act "immediate", you could build a more advanced cache that is updated just after a successful database update of one of the records. 如果您希望它“立即”执行,您可以构建一个更高级的缓存,在其中一个记录成功进行数据库更新后立即更新。 That is, the same action method would update both the database and the cache. 也就是说,相同的操作方法将更新数据库和缓存。

using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Caching.Memory;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

public class CachedRoute<TPrimaryKey> : IRouter
{
    private readonly string _controller;
    private readonly string _action;
    private readonly ICachedRouteDataProvider<TPrimaryKey> _dataProvider;
    private readonly IMemoryCache _cache;
    private readonly IRouter _target;
    private readonly string _cacheKey;
    private object _lock = new object();

    public CachedRoute(
        string controller,
        string action,
        ICachedRouteDataProvider<TPrimaryKey> dataProvider,
        IMemoryCache cache,
        IRouter target)
    {
        if (string.IsNullOrWhiteSpace(controller))
            throw new ArgumentNullException("controller");
        if (string.IsNullOrWhiteSpace(action))
            throw new ArgumentNullException("action");
        if (dataProvider == null)
            throw new ArgumentNullException("dataProvider");
        if (cache == null)
            throw new ArgumentNullException("cache");
        if (target == null)
            throw new ArgumentNullException("target");

        _controller = controller;
        _action = action;
        _dataProvider = dataProvider;
        _cache = cache;
        _target = target;

        // Set Defaults
        CacheTimeoutInSeconds = 900;
        _cacheKey = "__" + this.GetType().Name + "_GetPageList_" + _controller + "_" + _action;
    }

    public int CacheTimeoutInSeconds { get; set; }

    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 id that matches.
        TPrimaryKey id;

        //If this returns false, that means the URI did not match
        if (!GetPageList().TryGetValue(requestPath, out id))
        {
            return;
        }

        //Invoke MVC controller/action
        var routeData = context.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"] = _controller;
        routeData.Values["action"] = _action;

        // This will be the primary key of the database row.
        // It might be an integer or a GUID.
        routeData.Values["id"] = id;

        await _target.RouteAsync(context);
    }

    public VirtualPathData GetVirtualPath(VirtualPathContext context)
    {
        VirtualPathData result = null;
        string virtualPath;

        if (TryFindMatch(GetPageList(), context.Values, out virtualPath))
        {
            result = new VirtualPathData(this, virtualPath);
        }

        return result;
    }

    private bool TryFindMatch(IDictionary<string, TPrimaryKey> pages, IDictionary<string, object> values, out string virtualPath)
    {
        virtualPath = string.Empty;
        TPrimaryKey id;
        object idObj;
        object controller;
        object action;

        if (!values.TryGetValue("id", out idObj))
        {
            return false;
        }

        id = SafeConvert<TPrimaryKey>(idObj);
        values.TryGetValue("controller", out controller);
        values.TryGetValue("action", out action);

        // The logic here should be the inverse of the logic in 
        // RouteAsync(). 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(_action) && controller.Equals(_controller))
        {
            // The 'OrDefault' case returns the default value of the type you're 
            // iterating over. For value types, it will be a new instance of that type. 
            // Since KeyValuePair<TKey, TValue> is a value type (i.e. a struct), 
            // the 'OrDefault' case will not result in a null-reference exception. 
            // Since TKey here is string, the .Key of that new instance will be null.
            virtualPath = pages.FirstOrDefault(x => x.Value.Equals(id)).Key;
            if (!string.IsNullOrEmpty(virtualPath))
            {
                return true;
            }
        }
        return false;
    }

    private IDictionary<string, TPrimaryKey> GetPageList()
    {
        IDictionary<string, TPrimaryKey> pages;

        if (!_cache.TryGetValue(_cacheKey, out pages))
        {
            // Only allow one thread to poplate the data
            lock (_lock)
            {
                if (!_cache.TryGetValue(_cacheKey, out pages))
                {
                    pages = _dataProvider.GetPageToIdMap();

                    _cache.Set(_cacheKey, pages,
                        new MemoryCacheEntryOptions()
                        {
                            Priority = CacheItemPriority.NeverRemove,
                            AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(this.CacheTimeoutInSeconds)
                        });
                }
            }
        }

        return pages;
    }

    private static T SafeConvert<T>(object obj)
    {
        if (typeof(T).Equals(typeof(Guid)))
        {
            if (obj.GetType() == typeof(string))
            {
                return (T)(object)new Guid(obj.ToString());
            }
            return (T)(object)Guid.Empty;
        }
        return (T)Convert.ChangeType(obj, typeof(T));
    }
}

LinkCachedRouteDataProvider

Here we have a simple service that retrieves the data from the database and loads it into a Dictionary. 这里我们有一个简单的服务,它从数据库中检索数据并将其加载到Dictionary中。 The most complicated part is the scope that needs to be setup in order to use DbContext from within the service. 最复杂的部分是需要设置的范围,以便在服务中使用DbContext

public interface ICachedRouteDataProvider<TPrimaryKey>
{
    IDictionary<string, TPrimaryKey> GetPageToIdMap();
}

public class LinkCachedRouteDataProvider : ICachedRouteDataProvider<int>
{
    private readonly IServiceProvider serviceProvider;

    public LinkCachedRouteDataProvider(IServiceProvider serviceProvider)
    {
        this.serviceProvider = serviceProvider
            ?? throw new ArgumentNullException(nameof(serviceProvider));
    }

    public IDictionary<string, int> GetPageToIdMap()
    {
        using (var scope = serviceProvider.CreateScope())
        {
            var dbContext = scope.ServiceProvider.GetService<ApplicationDbContext>();

            return (from link in dbContext.Links
                    select new KeyValuePair<string, int>(
                        link.ShortenedLink.Trim('/'),
                        link.Id)
                    ).ToDictionary(pair => pair.Key, pair => pair.Value);
        }
    }
}

RedirectController

Our redirect controller accepts the primary key as an id parameter and then looks up the database record to get the URL to redirect to. 我们的重定向控制器接受主键作为id参数,然后查找数据库记录以获取要重定向到的URL。

public class RedirectController
{
    private readonly ApplicationDbContext dbContext;

    public RedirectController(ApplicationDbContext dbContext)
    {
        this.dbContext = dbContext
            ?? throw new ArgumentNullException(nameof(dbContext));
    }

    public IActionResult GoToFull(int id)
    {
        var link = dbContext.Links.FirstOrDefault(x => x.Id == id);
        return new RedirectResult(link.FullLink);
    }
}

In a production scenario, you would probably want to make this a permanent redirect return new RedirectResult(link.FullLink, true) , but those are automatically cached by browsers which makes testing difficult. 在生产场景中,您可能希望将此永久重定向return new RedirectResult(link.FullLink, true) ,但这些会被浏览器自动缓存,这使得测试变得困难。

Startup.cs

We setup the DbContext , the memory cache, and the LinkCachedRouteDataProvider in our DI container for use later. 我们在DI容器中设置DbContext ,内存缓存和LinkCachedRouteDataProvider供以后使用。

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddMvc();

    services.AddMemoryCache();
    services.AddSingleton<LinkCachedRouteDataProvider>();
}

And then we setup our routing using the CachedRoute<TPrimaryKey> , providing all dependencies. 然后我们使用CachedRoute<TPrimaryKey>设置路由,提供所有依赖项。

app.UseMvc(routes =>
{
    routes.Routes.Add(new CachedRoute<int>(
        controller: "Redirect",
        action: "GoToFull",
        dataProvider: app.ApplicationServices.GetService<LinkCachedRouteDataProvider>(),
        cache: app.ApplicationServices.GetService<IMemoryCache>(),
        target: routes.DefaultHandler)
        // Set to 60 seconds of caching to make DB updates refresh quicker
        { CacheTimeoutInSeconds = 60 });

    routes.MapRoute(
        name: "default",
        template: "{controller=Home}/{action=Index}/{id?}");
});

To build these short URLs on the user interface, you can use tag helpers (or HTML helpers) the same way you would with any other route: 要在用户界面上构建这些短URL,您可以像使用任何其他路径一样使用标记助手(或HTML助手):

<a asp-area="" asp-controller="Redirect" asp-action="GoToFull" asp-route-id="1">
    @Url.Action("GoToFull", "Redirect", new { id = 1 })
</a>

Which is generated as: 生成为:

<a href="/M81J1w0A">/M81J1w0A</a>

You can of course use a model to pass the id parameter into your view when it is generated. 您当然可以使用模型在生成时将id参数传递到视图中。

<a asp-area="" asp-controller="Redirect" asp-action="GoToFull" asp-route-id="@Model.Id">
    @Url.Action("GoToFull", "Redirect", new { id = Model.Id })
</a>

I have made a Demo on GitHub . 在GitHub上做了一个Demo If you enter the short URLs into the browser, they will be redirected to the long URLs. 如果您在浏览器中输入短网址,则会将其重定向到长网址。

  • M81J1w0A -> https://maps.google.com/ M81J1w0A - > https://maps.google.com/
  • r33NW8K -> https://stackoverflow.com/ r33NW8K - > https://stackoverflow.com/

I didn't create any of the views to update the URLs in the database, but that type of thing is covered in several tutorials such as Get started with ASP.NET Core MVC and Entity Framework Core using Visual Studio , and it doesn't look like you are having issues with that part. 我没有创建任何视图来更新数据库中的URL,但是这些类型的东西包含在几个教程中,例如使用Visual Studio开始使用ASP.NET Core MVC和Entity Framework Core ,它没有看起来你遇到了那个问题。

References: 参考文献:

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM