Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
namespace MyTested.AspNetCore.Mvc.Internal
{
using System.Linq;
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

internal class ApiVersionActionConstraint : IActionConstraint
{
public int Order => 0;

public bool Accept(ActionConstraintContext context)
{
var requestedVersion = GetRequestedApiVersion(context.RouteContext);

if (requestedVersion == null)
{
return true;
}

var metadata = context.CurrentCandidate.Action.GetApiVersionMetadata();

if (metadata == ApiVersionMetadata.Empty || metadata.IsApiVersionNeutral)
{
return true;
}

if (!metadata.IsMappedTo(requestedVersion))
{
return false;
}

if (context.Candidates.Count <= 1)
{
return true;
}

var mapping = metadata.MappingTo(requestedVersion);
if (mapping == ApiVersionMapping.Explicit)
{
return true;
}

var hasExplicitCandidate = context.Candidates.Any(c =>
{
if (c.Action == context.CurrentCandidate.Action)
{
return false;
}

var otherMetadata = c.Action.GetApiVersionMetadata();
return otherMetadata.MappingTo(requestedVersion) == ApiVersionMapping.Explicit;
});

return !hasExplicitCandidate;
}

private static ApiVersion GetRequestedApiVersion(RouteContext context)
{
var parser = ApiVersionParser.Default;

if (context.RouteData.Values.TryGetValue("version", out var routeVersion)
&& routeVersion is string versionString
&& parser.TryParse(versionString, out var parsedVersion))
{
return parsedVersion;
}

var reader = context.HttpContext.RequestServices
?.GetService<IOptions<ApiVersioningOptions>>()
?.Value
?.ApiVersionReader;

if (reader != null)
{
var rawVersions = reader.Read(context.HttpContext.Request);

if (rawVersions.Count > 0
&& parser.TryParse(rawVersions[0], out var parsedReaderVersion))
{
return parsedReaderVersion;
}
}

return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace MyTested.AspNetCore.Mvc.Internal
{
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ActionConstraints;

internal class ApiVersionActionConstraintProvider : IActionConstraintProvider
{
private static readonly ApiVersionActionConstraint SharedConstraint = new();

public int Order => -1000;

public void OnProvidersExecuting(ActionConstraintProviderContext context)
{
var metadata = context.Action.GetApiVersionMetadata();

if (metadata != ApiVersionMetadata.Empty && !metadata.IsApiVersionNeutral)
{
context.Results.Add(new ActionConstraintItem(SharedConstraint)
{
Constraint = SharedConstraint,
IsReusable = true
});
}
}

public void OnProvidersExecuted(ActionConstraintProviderContext context)
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
namespace MyTested.AspNetCore.Mvc.Internal
{
using System.Collections.Generic;
using System.Linq;
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

internal class ApiVersionAwareActionSelector : IActionSelector
{
private readonly IActionSelector inner;

public ApiVersionAwareActionSelector(IActionSelector inner)
{
this.inner = inner;
}

public IReadOnlyList<ActionDescriptor> SelectCandidates(RouteContext context)
=> this.inner.SelectCandidates(context);

public ActionDescriptor SelectBestCandidate(
RouteContext context,
IReadOnlyList<ActionDescriptor> candidates)
{
var requestedVersion = GetRequestedApiVersion(context);

if (requestedVersion == null)
{
return this.inner.SelectBestCandidate(context, candidates);
}

var versionedCandidates = new List<ActionDescriptor>();

foreach (var candidate in candidates)
{
var metadata = candidate.GetApiVersionMetadata();

if (metadata == ApiVersionMetadata.Empty
|| metadata.IsApiVersionNeutral
|| metadata.IsMappedTo(requestedVersion))
{
versionedCandidates.Add(candidate);
}
}

if (versionedCandidates.Count == 0)
{
return null;
}

if (versionedCandidates.Count > 1)
{
var explicitlyMapped = versionedCandidates
.Where(c => c.GetApiVersionMetadata().MappingTo(requestedVersion) == ApiVersionMapping.Explicit)
.ToList();

if (explicitlyMapped.Count > 0)
{
versionedCandidates = explicitlyMapped;
}
}

return this.inner.SelectBestCandidate(context, versionedCandidates);
}

private static ApiVersion GetRequestedApiVersion(RouteContext context)
{
var parser = ApiVersionParser.Default;

if (context.RouteData.Values.TryGetValue("version", out var routeVersion)
&& routeVersion is string versionString
&& parser.TryParse(versionString, out var parsedVersion))
{
return parsedVersion;
}

var reader = context.HttpContext.RequestServices
?.GetService<IOptions<ApiVersioningOptions>>()
?.Value
?.ApiVersionReader;

if (reader != null)
{
var rawVersions = reader.Read(context.HttpContext.Request);

if (rawVersions.Count > 0
&& parser.TryParse(rawVersions[0], out var parsedReaderVersion))
{
return parsedReaderVersion;
}
}

return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="5.1.0" />
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Asp.Versioning.Mvc" Version="8.1.1" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,49 @@
namespace MyTested.AspNetCore.Mvc.Plugins
namespace MyTested.AspNetCore.Mvc.Plugins
{
using System;
using System.Linq;
using Asp.Versioning;
using Internal;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Versioning;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection;

public class VersioningTestPlugin : IHttpFeatureRegistrationPlugin
public class VersioningTestPlugin
: IHttpFeatureRegistrationPlugin,
IServiceRegistrationPlugin,
IRoutingServiceRegistrationPlugin
{
public Action<HttpContext> HttpFeatureRegistrationDelegate
=> httpContext => httpContext
.Features
.Set<IApiVersioningFeature>(new ApiVersioningFeature(httpContext));

public Func<ServiceDescriptor, bool> ServiceSelectorPredicate
=> serviceDescriptor
=> serviceDescriptor.ServiceType == typeof(IActionConstraintProvider);

public Action<IServiceCollection> ServiceRegistrationDelegate
=> serviceCollection
=> serviceCollection.AddSingleton<IActionConstraintProvider, ApiVersionActionConstraintProvider>();

public Action<IServiceCollection> RoutingServiceRegistrationDelegate
=> serviceCollection =>
{
var selectorDescriptor = serviceCollection
.FirstOrDefault(d => d.ServiceType == typeof(IActionSelector));

if (selectorDescriptor != null)
{
serviceCollection.Remove(selectorDescriptor);
serviceCollection.AddSingleton<IActionSelector>(sp =>
{
var innerSelector = (IActionSelector)ActivatorUtilities
.CreateInstance(sp, selectorDescriptor.ImplementationType);

return new ApiVersionAwareActionSelector(innerSelector);
});
}
};
}
}
Loading