riskfirst / riskfirst.hateoas

Powerful HATEOAS functionality for .NET web api
MIT License
78 stars 25 forks source link

Unable to transform links with .Net Core 3.1 and Microsoft.AspNetCore.Mvc.Versioning #35

Open doowruc opened 4 years ago

doowruc commented 4 years ago

I have been adding links successfully to a .Net Core 3.1 API project.

Today I have tried to add versioning to my API via the Microsoft.AspNetCore.Mvc.Versioning package and am now getting errors in LinkTransformationBuilderExtensions.AddRoutePath.

Specifically, ctx.LinkGenerator.GetPathByRouteValues() returns null for insert and delete links.

System.InvalidOperationException: Invalid path when adding route 'InsertValueRoute'. RouteValues: action=Get,controller=Values,version=1
   at RiskFirst.Hateoas.LinkTransformationBuilderExtensions.<>c.<AddRoutePath>b__2_0(LinkTransformationContext ctx) in C:\Source\Doowruc\GitHub\Doowruc\riskfirst.hateoas\src\RiskFirst.Hateoas\LinkTransformationBuilderExtensions.cs:line 33
   at RiskFirst.Hateoas.BuilderLinkTransformation.<>c__DisplayClass2_0.<Transform>b__0(StringBuilder sb, Func`2 transform) in C:\Source\Doowruc\GitHub\Doowruc\riskfirst.hateoas\src\RiskFirst.Hateoas\BuilderLinkTransformation.cs:line 21
   at System.Linq.Enumerable.Aggregate[TSource,TAccumulate](IEnumerable`1 source, TAccumulate seed, Func`3 func)
   at RiskFirst.Hateoas.BuilderLinkTransformation.Transform(LinkTransformationContext context) in C:\Source\Doowruc\GitHub\Doowruc\riskfirst.hateoas\src\RiskFirst.Hateoas\BuilderLinkTransformation.cs:line 19
   at RiskFirst.Hateoas.DefaultLinksEvaluator.BuildLinks(IEnumerable`1 links, ILinkContainer container) in C:\Source\Doowruc\GitHub\Doowruc\riskfirst.hateoas\src\RiskFirst.Hateoas\DefaultLinksEvaluator.cs:line 25

I have added a .Net Core 3.1 sample project to my fork (https://github.com/doowruc/riskfirst.hateoas) which demonstrates the issue. This is a copy of the existing classes in the BasicSimple sample

doowruc commented 4 years ago

It appears to be because the HttpContext RouteValues now contains a "version" which is not in the LinkSpec RouteValues.

The following code inserted prior to var path = ctx.LinkGenerator.GetPathByRouteValues(ctx.HttpContext, ctx.LinkSpec.RouteName, ctx.LinkSpec.RouteValues); fixes it, however this is probably not the best way to include it in the LinkSpec!

var contextRouteValues = ctx.HttpContext.Features.Get<IRouteValuesFeature>()?.RouteValues;

if (contextRouteValues != null && contextRouteValues.ContainsKey("version") && !ctx.LinkSpec.RouteValues.ContainsKey("version"))
{
    var version = contextRouteValues["version"];

    ctx.LinkSpec.RouteValues.Add("version", version);
}
doowruc commented 4 years ago

Further debugging suggests it is because of the lack of getValues Func being passed on RequireRoutedLink

If I add in version as the function, then it works, without the need to ament the extension as per my previous comment:

config.AddPolicy<ItemsLinkContainer<ValueInfo>>(policy =>
{
    policy.RequireSelfLink()
        .RequireRoutedLink("insert", "InsertValueRoute", x => new { version = "1" });
});
doowruc commented 4 years ago

I have figured out how to sort this with a LinksHandler

public class VersionLinkRequirement<TResource> : LinksHandler<VersionLinkRequirement<TResource>>, ILinksRequirement
{
    public string Id { get; set; }
    public string RouteName { get; set; }
    public Func<TResource, RouteValueDictionary> GetRouteValues { get; set; }

    protected override Task HandleRequirementAsync(LinksHandlerContext context, VersionLinkRequirement<TResource> requirement)
    {
        if (string.IsNullOrEmpty(requirement.RouteName))
        {
            context.Skipped(requirement, LinkRequirementSkipReason.Error, $"Requirement did not have a RouteName specified for link: {requirement.Id}");

            return Task.CompletedTask;
        }

        var route = context.RouteMap.GetRoute(requirement.RouteName);

        if (route == null)
        {
            context.Skipped(requirement, LinkRequirementSkipReason.Error, $"No route was found for route name: {requirement.RouteName}");

            return Task.CompletedTask;
        }

        var values = new RouteValueDictionary();

        if (requirement.GetRouteValues != null)
        {
            values = requirement.GetRouteValues((TResource)context.Resource);
        }

        var link = new LinkSpec(requirement.Id, route, values);

        if (context.ActionContext.RouteData.Values.TryGetValue("version", out var version))
        {
            link.RouteValues.Add("version", version);
        }

        context.Links.Add(link);

        context.Handled(requirement);

        return Task.CompletedTask;
    }
}
jamiecoh commented 4 years ago

This is great info. Thanks for posting it, hopefully help anyone in future.

vpetkovic commented 4 years ago

Sweet thanks! Any word when this will be pushed to a package?