private static DocumentFilterContext GetDocumentFilterContext(ApiVersionModel versionModel) { var actionDescriptor = new ActionDescriptor(); actionDescriptor.SetProperty(versionModel); var apiDescription = new ApiDescription { ActionDescriptor = actionDescriptor }; var apiDescriptions = new List <ApiDescription> { apiDescription }; var schemaGeneratorOptions = new SchemaGeneratorOptions(); var jsonSerializerOptions = new JsonSerializerOptions(); var jsonSerializerDataContractResolver = new JsonSerializerDataContractResolver(jsonSerializerOptions); var schemaGenerator = new SchemaGenerator(schemaGeneratorOptions, jsonSerializerDataContractResolver); var schemaRepository = new SchemaRepository(); return(new DocumentFilterContext(apiDescriptions, schemaGenerator, schemaRepository)); }
public void is_deprecated_should_match_model(int majorVersion, int minorVersion, bool expected) { // arrange var apiVersion = new ApiVersion(majorVersion, minorVersion); var model = new ApiVersionModel( supportedVersions: new[] { new ApiVersion(1, 0) }, deprecatedVersions: new[] { new ApiVersion(0, 9) }); var description = new ApiDescription { ActionDescriptor = new ActionDescriptor() { Properties = { [typeof(ApiVersionModel)] = model }, }, Properties = { [typeof(ApiVersion)] = apiVersion, } }; // act var deprecated = description.IsDeprecated(); // assert deprecated.Should().Be(expected); }
public void api_descriptions_should_ignore_api_for_direct_route_action() { // arrange var configuration = new HttpConfiguration(); var routeTemplate = "api/values"; var model = new ApiVersionModel(new ApiVersion(1, 0)); var controller = new HttpControllerDescriptor(configuration, "ApiExplorerValues", typeof(ApiExplorerValuesController)); var actions = new ReflectedHttpActionDescriptor[] { new ReflectedHttpActionDescriptor(controller, typeof(ApiExplorerValuesController).GetMethod("Get")) { Properties = { [typeof(ApiVersionModel)] = model }, }, new ReflectedHttpActionDescriptor(controller, typeof(ApiExplorerValuesController).GetMethod("Post")) { Properties = { [typeof(ApiVersionModel)] = model }, }, }; configuration.Routes.Add("Route", CreateDirectRoute(routeTemplate, actions)); IApiExplorer apiExplorer = new VersionedApiExplorer(configuration); // act var descriptions = apiExplorer.ApiDescriptions; // assert descriptions.Single().Should().Should().BeEquivalentTo( new { HttpMethod = Get, RelativePath = routeTemplate }, options => options.ExcludingMissingMembers()); }
public void api_descriptions_should_recognize_composite_routes() { // arrange var configuration = new HttpConfiguration(); var routeTemplate = "api/values"; var model = new ApiVersionModel(new ApiVersion(1, 0)); var controllerDescriptor = new HttpControllerDescriptor(configuration, "AttributeApiExplorerValues", typeof(AttributeApiExplorerValuesController)); var action = new ReflectedHttpActionDescriptor(controllerDescriptor, typeof(AttributeApiExplorerValuesController).GetMethod("Action")) { Properties = { [typeof(ApiVersionModel)] = model }, }; var actions = new ReflectedHttpActionDescriptor[] { action }; var routeCollection = new List <IHttpRoute>() { CreateDirectRoute(routeTemplate, actions) }; var route = NewRouteCollectionRoute(); route.EnsureInitialized(() => routeCollection); configuration.Routes.Add("Route", route); IApiExplorer apiExplorer = new VersionedApiExplorer(configuration); // act var descriptions = apiExplorer.ApiDescriptions; // assert descriptions.Single().Should().Should().BeEquivalentTo( new { HttpMethod = Get, RelativePath = routeTemplate, ActionDescriptor = action }, options => options.ExcludingMissingMembers()); }
/// <summary> /// Applies the filter to the specified operation using the given context. /// </summary> /// <param name="operation">The operation to apply the filter to.</param> /// <param name="context">The current operation filter context.</param> public void Apply(OpenApiOperation operation, OperationFilterContext context) { ApiDescription apiDescription = context.ApiDescription; ApiVersion apiVersion = apiDescription.GetApiVersion(); ApiVersionModel model = apiDescription.ActionDescriptor.GetApiVersionModel(Explicit | Implicit); operation.Deprecated = model.DeprecatedApiVersions.Contains(apiVersion); if (operation.Parameters == null) { return; } foreach (OpenApiParameter parameter in operation.Parameters) { ApiParameterDescription description = apiDescription.ParameterDescriptions.First(p => p.Name == parameter.Name); if (parameter.Description == null) { parameter.Description = description.ModelMetadata?.Description; } parameter.Required |= description.IsRequired; } }
void ApplyInheritedActionConventions(IReadOnlyList <HttpActionDescriptor> actions) { var noInheritedApiVersions = SupportedVersions.Count == 0 && DeprecatedVersions.Count == 0 && AdvertisedVersions.Count == 0 && DeprecatedAdvertisedVersions.Count == 0; if (noInheritedApiVersions) { return; } for (var i = 0; i < actions.Count; i++) { var action = actions[i]; var model = action.GetProperty <ApiVersionModel>(); if (model == null || model.IsApiVersionNeutral) { continue; } var supportedVersions = model.SupportedApiVersions.Union(SupportedVersions); var deprecatedVersions = model.DeprecatedApiVersions.Union(DeprecatedVersions); model = new ApiVersionModel( declaredVersions: model.DeclaredApiVersions, supportedVersions, deprecatedVersions, AdvertisedVersions, DeprecatedAdvertisedVersions); action.SetProperty(model); } }
/// <summary> /// Aggregates the current version information with other version information. /// </summary> /// <param name="version">The <see cref="ApiVersionModel">API version information</see> that is the basis /// of the aggregation.</param> /// <param name="otherVersions">A <see cref="IEnumerable{T}">sequence</see> of other /// <see cref="ApiVersionModel">API version information</see> to aggregate.</param> /// <returns>A new <see cref="ApiVersionModel"/> that is the aggregated result of the /// <paramref name="otherVersions">other version information</paramref> and the current version information.</returns> public static ApiVersionModel Aggregate(this ApiVersionModel version, IEnumerable <ApiVersionModel> otherVersions) { if (version == null) { throw new ArgumentNullException(nameof(version)); } if (otherVersions == null) { throw new ArgumentNullException(nameof(otherVersions)); } var implemented = new HashSet <ApiVersion>(); var supported = new HashSet <ApiVersion>(); var deprecated = new HashSet <ApiVersion>(); implemented.UnionWith(version.ImplementedApiVersions); supported.UnionWith(version.SupportedApiVersions); deprecated.UnionWith(version.DeprecatedApiVersions); foreach (var otherVersion in otherVersions) { implemented.UnionWith(otherVersion.ImplementedApiVersions); supported.UnionWith(otherVersion.SupportedApiVersions); deprecated.UnionWith(otherVersion.DeprecatedApiVersions); } deprecated.ExceptWith(supported); return(new ApiVersionModel(version, implemented.ToSortedReadOnlyList(), supported.ToSortedReadOnlyList(), deprecated.ToSortedReadOnlyList())); }
public async Task on_action_executing_should_add_version_headers() { // arrange var supported = new[] { new ApiVersion(1, 0), new ApiVersion(2, 0) }; var deprecated = new[] { new ApiVersion(0, 5) }; var model = new ApiVersionModel(supported, deprecated); var onStartResponse = new List <(Func <object, Task>, object)>(); var context = CreateContext(model, onStartResponse); var attribute = new ReportApiVersionsAttribute(); // act attribute.OnActionExecuting(context); for (var i = 0; i < onStartResponse.Count; i++) { var(callback, state) = onStartResponse[i]; await callback(state); } // assert var headers = context.HttpContext.Response.Headers; headers["api-supported-versions"].Single().Should().Be("1.0, 2.0"); headers["api-deprecated-versions"].Single().Should().Be("0.5"); }
/// <summary> /// Selects the controller for OData requests. /// </summary> /// <param name="odataPath">The OData path.</param> /// <param name="request">The request.</param> /// <returns>The name of the selected controller or <c>null</c> if the request isn't handled by this convention.</returns> public virtual string?SelectController(ODataPath odataPath, HttpRequestMessage request) { if (odataPath == null) { throw new ArgumentNullException(nameof(odataPath)); } if (odataPath.PathTemplate != "~" && odataPath.PathTemplate != "~/$metadata") { return(null); } var properties = request.ApiVersionProperties(); // the service document and metadata endpoints are special, but they are not neutral. if the client doesn't // specify a version, they may not know to. assume a default version by policy, but it's always allowed. // a client might also send an OPTIONS request to determine which versions are available (ex: tooling) if (string.IsNullOrEmpty(properties.RawRequestedApiVersion)) { var modelSelector = request.GetRequestContainer().GetRequiredService <IEdmModelSelector>(); var versionSelector = request.GetApiVersioningOptions().ApiVersionSelector; var model = new ApiVersionModel(modelSelector.ApiVersions, Enumerable.Empty <ApiVersion>()); properties.RequestedApiVersion = versionSelector.SelectVersion(request, model); } return("VersionedMetadata"); }
public void is_deprecated_should_return_expected_result_for_deprecated_version(int majorVersion, bool expected) { // arrange var provider = new DefaultApiVersionDescriptionProvider( new Mock <IActionDescriptorCollectionProvider>().Object, new Mock <IApiVersionGroupNameFormatter>().Object, new OptionsWrapper <ApiVersioningOptions>(new ApiVersioningOptions())); var action = new ActionDescriptor(); var controller = new ControllerModel(typeof(Controller).GetTypeInfo(), new object[0]); var model = new ApiVersionModel( declaredVersions: new[] { new ApiVersion(1, 0), new ApiVersion(2, 0) }, supportedVersions: new[] { new ApiVersion(2, 0) }, deprecatedVersions: new[] { new ApiVersion(1, 0) }, advertisedVersions: Empty <ApiVersion>(), deprecatedAdvertisedVersions: Empty <ApiVersion>()); controller.SetProperty(model); action.SetProperty(controller); // act var result = provider.IsDeprecated(action, new ApiVersion(majorVersion, 0)); // assert result.Should().Be(expected); }
public void aggregate_should_merge_api_version_info_sequence() { // arrange var model = new ApiVersionModel(new[] { new ApiVersion(1, 0) }, new[] { new ApiVersion(0, 9) }); var otherModels = new[] { new ApiVersionModel(new[] { new ApiVersion(2, 0) }, Enumerable.Empty <ApiVersion>()), new ApiVersionModel(new[] { new ApiVersion(3, 0) }, new[] { new ApiVersion(3, 0, "Alpha") }) }; var expected = new ApiVersionModel( new[] { new ApiVersion(1, 0), new ApiVersion(2, 0), new ApiVersion(3, 0) }, new[] { new ApiVersion(0, 9), new ApiVersion(3, 0, "Alpha") }); // act var aggregatedModel = model.Aggregate(otherModels); // assert aggregatedModel.Should().BeEquivalentTo( new { expected.IsApiVersionNeutral, expected.DeclaredApiVersions, expected.ImplementedApiVersions, expected.SupportedApiVersions, expected.DeprecatedApiVersions }); }
public ApiVersion SelectVersion(HttpRequest request, ApiVersionModel model) { Random random = new Random(); int randomNumber = random.Next(0, model.SupportedApiVersions.Count()); ApiVersion version = model.SupportedApiVersions[randomNumber]; return(version); }
/// <summary> /// Aggregates the current version information with other version information. /// </summary> /// <param name="version">The <see cref="ApiVersionModel">API version information</see> that is the basis /// of the aggregation.</param> /// <param name="otherVersion">The other <see cref="ApiVersionModel">API version information</see> to aggregate.</param> /// <returns>A new <see cref="ApiVersionModel"/> that is the aggregated result of the /// <paramref name="otherVersion">other version information</paramref> and the current version information.</returns> public static ApiVersionModel Aggregate(this ApiVersionModel version, ApiVersionModel otherVersion) { Arg.NotNull(version, nameof(version)); Arg.NotNull(otherVersion, nameof(otherVersion)); Contract.Ensures(Contract.Result <ApiVersionModel>() != null); return(version.Aggregate(new[] { otherVersion })); }
public void Report(HttpResponseHeaders headers, ApiVersionModel apiVersionModel) { Arg.NotNull(headers, nameof(headers)); Arg.NotNull(apiVersionModel, nameof(apiVersionModel)); AddApiVersionHeader(headers, ApiSupportedVersions, apiVersionModel.SupportedApiVersions); AddApiVersionHeader(headers, ApiDeprecatedVersions, apiVersionModel.DeprecatedApiVersions); }
public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary <string, object> values, HttpRouteDirection routeDirection) { if (routeDirection == UriGeneration) { return(true); } if (!MatchAnyVersion && apiVersion != request.GetRequestedApiVersion()) { return(false); } var properties = request.ApiVersionProperties(); // determine whether this constraint can match any api version and no api version has otherwise been matched if (MatchAnyVersion && properties.RequestedApiVersion == null) { var options = request.GetApiVersioningOptions(); // is implicitly matching an api version allowed? if (options.AssumeDefaultVersionWhenUnspecified || IsServiceDocumentOrMetadataRoute(values)) { var odata = request.ODataApiVersionProperties(); var model = new ApiVersionModel(odata.MatchingRoutes.Keys, Enumerable.Empty <ApiVersion>()); var selector = options.ApiVersionSelector; var requestedApiVersion = properties.RequestedApiVersion = selector.SelectVersion(request, model); // if an api version is selected, determine if it corresponds to a route that has been previously matched if (requestedApiVersion != null && odata.MatchingRoutes.TryGetValue(requestedApiVersion, out var routeName)) { // create a new versioned path constraint on the fly and evaluate it. this sets up the underlying odata // infrastructure such as the container, edm, etc. this has no bearing the action selector which will // already select the correct action. without this the response may be incorrect, even if the correct // action is selected and executed. var constraint = new VersionedODataPathRouteConstraint(routeName, requestedApiVersion); return(constraint.Match(request, route, parameterName, values, routeDirection)); } } } else if (!MatchAnyVersion && properties.RequestedApiVersion != apiVersion) { return(false); } request.DeleteRequestContainer(true); // by evaluating the remaining unversioned constraints, this will ultimately determine whether 400 or 404 // is returned for an odata request foreach (var constraint in innerConstraints) { if (constraint.Match(request, route, parameterName, values, routeDirection)) { return(true); } } return(false); }
void ApplyControllerConventions(HttpControllerDescriptor controllerDescriptor) { Contract.Requires(controllerDescriptor != null); MergeAttributesWithConventions(controllerDescriptor); var model = new ApiVersionModel(VersionNeutral, supportedVersions, deprecatedVersions, advertisedVersions, deprecatedAdvertisedVersions); controllerDescriptor.SetConventionsApiVersionModel(model); }
public ApiEntityListPage <ApiVersionModel> Get() { ApiVersionModel v1 = new ApiVersionModel("v1", true, true); ApiEntityListPage <ApiVersionModel> result = new ApiEntityListPage <ApiVersionModel>(new List <ApiVersionModel> { v1 }, HttpContext.Request.Path.ToString()); return(result); }
public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection) { if (routeDirection == UrlGeneration) { return(true); } var feature = httpContext.Features.Get <IApiVersioningFeature>(); // determine whether this constraint can match any api version and no api version has otherwise been matched if (MatchAnyVersion && feature.RequestedApiVersion == null) { var options = httpContext.RequestServices.GetRequiredService <IOptions <ApiVersioningOptions> >().Value; // is implicitly matching an api version allowed? if (options.AssumeDefaultVersionWhenUnspecified || IsServiceDocumentOrMetadataRoute(values)) { var odata = httpContext.ODataVersioningFeature(); var model = new ApiVersionModel(odata.MatchingRoutes.Keys, Array.Empty <ApiVersion>()); var selector = httpContext.RequestServices.GetRequiredService <IApiVersionSelector>(); var requestedApiVersion = feature.RequestedApiVersion = selector.SelectVersion(httpContext.Request, model); // if an api version is selected, determine if it corresponds to a route that has been previously matched if (requestedApiVersion != null && odata.MatchingRoutes.TryGetValue(requestedApiVersion, out var routeName)) { // create a new versioned path constraint on the fly and evaluate it. this sets up the underlying odata // infrastructure such as the container, edm, etc. this has no bearing the action selector which will // already select the correct action. without this the response may be incorrect, even if the correct // action is selected and executed. var constraint = new VersionedODataPathRouteConstraint(routeName, requestedApiVersion); return(constraint.Match(httpContext, route, routeKey, values, routeDirection)); } } } else if (!MatchAnyVersion && feature.RequestedApiVersion != apiVersion) { return(false); } httpContext.Request.DeleteRequestContainer(true); // by evaluating the remaining unversioned constraints, this will ultimately determine whether 400 or 404 // is returned for an odata request foreach (var constraint in innerConstraints) { if (constraint.Match(httpContext, route, routeKey, values, routeDirection)) { return(true); } } return(false); }
private ControllerVersionInfo ApplyControllerConventions(ControllerModel controllerModel) { Contract.Requires(controllerModel != null); Contract.Ensures(Contract.Result <ControllerVersionInfo>() != null); MergeAttributesWithConventions(controllerModel); var model = new ApiVersionModel(VersionNeutral, supportedVersions, deprecatedVersions, advertisedVersions, deprecatedAdvertisedVersions); controllerModel.SetProperty(model); return(new ControllerVersionInfo(supportedVersions, deprecatedVersions, advertisedVersions, deprecatedAdvertisedVersions)); }
public void action_should_be_mapped_to_specific_api_version() { // arrange var action = new ActionDescriptor(); var version = new ApiVersion(42, 0); var model = new ApiVersionModel(version); // act action.SetProperty(model); // assert action.IsMappedTo(version).Should().BeTrue(); }
public void action_should_be_api_versionX2Dneutral_with_a_versioned_model() { // arrange var action = new ActionDescriptor(); var model = new ApiVersionModel(new ApiVersion(42, 0)); action.SetProperty(model); // act var result = action.IsApiVersionNeutral(); // assert result.Should().BeFalse(); }
static ActionExecutedContext CreateContext(ApiVersionModel model) { var headers = new HeaderDictionary(); var response = new Mock <HttpResponse>(); var httpContext = new Mock <HttpContext>(); var action = new ActionDescriptor(); var actionContext = new ActionContext(httpContext.Object, new RouteData(), action); response.SetupGet(r => r.Headers).Returns(headers); httpContext.SetupGet(c => c.Response).Returns(response.Object); action.SetProperty(model); return(new ActionExecutedContext(actionContext, new IFilterMetadata[0], null)); }
public virtual ApiVersion SelectVersion(HttpRequest request, ApiVersionModel model) { switch (model.ImplementedApiVersions.Count) { case 0: return(options.DefaultApiVersion); case 1: var version = model.ImplementedApiVersions[0]; return(version.Status == null ? version : options.DefaultApiVersion); } return(model.ImplementedApiVersions.Where(v => v.Status == null).Min(v => v) ?? options.DefaultApiVersion); }
public void convention_should_apply_api_version_model() { // arrange var supported = new[] { new ApiVersion(1, 0), new ApiVersion(2, 0), new ApiVersion(3, 0) }; var deprecated = new[] { new ApiVersion(0, 9) }; var model = new ApiVersionModel(supported, deprecated); var type = typeof(object); var attributes = new object[] { new ApiVersionAttribute("1.0"), new ApiVersionAttribute("2.0"), new ApiVersionAttribute("3.0"), new ApiVersionAttribute("0.9") { Deprecated = true } }; var actionMethod = type.GetRuntimeMethod(nameof(object.ToString), EmptyTypes); var controller = new ControllerModel(type.GetTypeInfo(), attributes) { Actions = { new ActionModel(actionMethod, attributes) } }; var convention = (ApiVersionAttribute)attributes[0]; // act convention.Apply(controller); // assert controller.GetProperty <ApiVersionModel>().ShouldBeEquivalentTo( new { IsApiVersionNeutral = false, DeclaredApiVersions = deprecated.Union(supported).ToArray(), ImplementedApiVersions = deprecated.Union(supported).ToArray(), SupportedApiVersions = supported, DeprecatedApiVersions = deprecated }); controller.Actions.Single().GetProperty <ApiVersionModel>().ShouldBeEquivalentTo( new { IsApiVersionNeutral = false, DeclaredApiVersions = deprecated.Union(supported).ToArray(), ImplementedApiVersions = deprecated.Union(supported).ToArray(), SupportedApiVersions = supported, DeprecatedApiVersions = deprecated }); }
public void action_should_not_be_implicitly_versioned_when_api_versions_have_been_mapped() { // arrange var version = new ApiVersion(42, 0); var action = new ActionDescriptor(); var model = new ApiVersionModel(version); action.SetProperty(model); // act var result = action.IsImplicitlyMappedTo(version); // assert result.Should().BeFalse(); }
public void action_should_be_mapped_to_specific_api_version() { // arrange var action = new ActionDescriptor(); var version = new ApiVersion(42, 0); var model = new ApiVersionModel(version); action.SetProperty(model); // act var result = action.MappingTo(version); // assert result.Should().Be(Explicit); }
public virtual ApiVersion SelectVersion( HttpRequest request, ApiVersionModel model ) { Arg.NotNull( request, nameof( request ) ); Arg.NotNull( model, nameof( model ) ); switch ( model.ImplementedApiVersions.Count ) { case 0: return options.DefaultApiVersion; case 1: var version = model.ImplementedApiVersions[0]; return version.Status == null ? version : options.DefaultApiVersion; } return model.ImplementedApiVersions.Where( v => v.Status == null ).Max( v => v ) ?? options.DefaultApiVersion; }
public void on_action_executing_should_add_version_headers() { // arrange var supported = new[] { new ApiVersion(1, 0), new ApiVersion(2, 0) }; var deprecated = new[] { new ApiVersion(0, 5) }; var model = new ApiVersionModel(supported, deprecated); var context = CreateContext(model); var attribute = new ReportApiVersionsAttribute(); // act attribute.OnActionExecuting(context); // assert context.HttpContext.Response.Headers["api-supported-versions"].Single().Should().Be("1.0, 2.0"); context.HttpContext.Response.Headers["api-deprecated-versions"].Single().Should().Be("0.5"); }
public virtual void ApplyTo(HttpActionDescriptor actionDescriptor) { if (actionDescriptor == null) { throw new ArgumentNullException(nameof(actionDescriptor)); } var attributes = new List <object>(); attributes.AddRange(actionDescriptor.GetCustomAttributes <IApiVersionNeutral>(inherit: true)); attributes.AddRange(actionDescriptor.GetCustomAttributes <IApiVersionProvider>(inherit: false)); MergeAttributesWithConventions(attributes); if (VersionNeutral) { actionDescriptor.SetProperty(ApiVersionModel.Neutral); return; } ApiVersionModel?versionModel; if (MappedVersions.Count == 0) { var declaredVersions = SupportedVersions.Union(DeprecatedVersions); versionModel = new ApiVersionModel( declaredVersions, SupportedVersions, DeprecatedVersions, AdvertisedVersions, DeprecatedAdvertisedVersions); } else { var emptyVersions = Empty <ApiVersion>(); versionModel = new ApiVersionModel( declaredVersions: MappedVersions, supportedVersions: emptyVersions, deprecatedVersions: emptyVersions, advertisedVersions: emptyVersions, deprecatedAdvertisedVersions: emptyVersions); } actionDescriptor.SetProperty(versionModel); }
static ActionExecutingContext CreateContext(ApiVersionModel model) { var headers = new HeaderDictionary(); var response = new Mock <HttpResponse>(); var httpContext = new Mock <HttpContext>(); var action = new ActionDescriptor(); var actionContext = new ActionContext(httpContext.Object, new RouteData(), action); var filters = new IFilterMetadata[0]; var actionArguments = new Dictionary <string, object>(); var controller = default(object); response.SetupGet(r => r.Headers).Returns(headers); httpContext.SetupGet(c => c.Response).Returns(response.Object); action.SetProperty(model); return(new ActionExecutingContext(actionContext, filters, actionArguments, controller)); }