Skip to content

Commit

Permalink
Add ApiVersion v1.0-prerelease (#836)
Browse files Browse the repository at this point in the history
* base functionality for api versioning and swagger docs

* add v1 prerelease version to all api endpoints

* update health shared package version

* add versioned url to e2e tests

* keep the original known route names unchanges

* add e2e tests for new api paths

* update version url in tests

* remove ApiController attribute; add VersiontedRoute attribute to simplify versioned routes

* remove api version setup code from web project

* update formatting

* update E2E tests to reuse test parameter data in shared class

* consolidate VersionAPIData
  • Loading branch information
jnlycklama authored Jun 18, 2021
1 parent 8c13d1e commit 6fb1527
Show file tree
Hide file tree
Showing 20 changed files with 268 additions and 98 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

namespace Microsoft.Health.Dicom.Api.Controllers
{
[ApiVersion("1.0-prerelease")]
[QueryModelStateValidator]
[ServiceFilter(typeof(DicomAudit.AuditLoggingFilterAttribute))]
public class ChangeFeedController : Controller
Expand All @@ -39,6 +40,7 @@ public ChangeFeedController(IMediator mediator, ILogger<ChangeFeedController> lo
[ProducesResponseType(typeof(JsonResult), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.Unauthorized)]
[ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)]
[VersionedRoute(KnownRoutes.ChangeFeed)]
[Route(KnownRoutes.ChangeFeed)]
[AuditEventType(AuditEventSubType.ChangeFeed)]
public async Task<IActionResult> GetChangeFeed([FromQuery] long offset = 0, [FromQuery] int limit = 10, [FromQuery] bool includeMetadata = true)
Expand All @@ -58,6 +60,7 @@ public async Task<IActionResult> GetChangeFeed([FromQuery] long offset = 0, [Fro
[ProducesResponseType(typeof(ChangeFeedEntry), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.Unauthorized)]
[ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)]
[VersionedRoute(KnownRoutes.ChangeFeedLatest)]
[Route(KnownRoutes.ChangeFeedLatest)]
[AuditEventType(AuditEventSubType.ChangeFeed)]
public async Task<IActionResult> GetChangeFeedLatest([FromQuery] bool includeMetadata = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

namespace Microsoft.Health.Dicom.Api.Controllers
{
[ApiVersion("1.0-prerelease")]
[QueryModelStateValidator]
[ServiceFilter(typeof(DicomAudit.AuditLoggingFilterAttribute))]
public class DeleteController : Controller
Expand All @@ -39,6 +40,7 @@ public DeleteController(IMediator mediator, ILogger<DeleteController> logger)
[ProducesResponseType((int)HttpStatusCode.NoContent)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[VersionedRoute(KnownRoutes.StudyRoute)]
[Route(KnownRoutes.StudyRoute)]
[AuditEventType(AuditEventSubType.Delete)]
public async Task<IActionResult> DeleteStudyAsync(string studyInstanceUid)
Expand All @@ -55,6 +57,7 @@ public async Task<IActionResult> DeleteStudyAsync(string studyInstanceUid)
[ProducesResponseType((int)HttpStatusCode.NoContent)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[VersionedRoute(KnownRoutes.SeriesRoute)]
[Route(KnownRoutes.SeriesRoute)]
[AuditEventType(AuditEventSubType.Delete)]
public async Task<IActionResult> DeleteSeriesAsync(string studyInstanceUid, string seriesInstanceUid)
Expand All @@ -71,6 +74,7 @@ public async Task<IActionResult> DeleteSeriesAsync(string studyInstanceUid, stri
[ProducesResponseType((int)HttpStatusCode.NoContent)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[VersionedRoute(KnownRoutes.InstanceRoute)]
[Route(KnownRoutes.InstanceRoute)]
[AuditEventType(AuditEventSubType.Delete)]
public async Task<IActionResult> DeleteInstanceAsync(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

namespace Microsoft.Health.Dicom.Api.Controllers
{
[ApiVersion("1.0-prerelease")]
[ServiceFilter(typeof(DicomAudit.AuditLoggingFilterAttribute))]
public class ExtendedQueryTagController : Controller
{
Expand All @@ -46,6 +47,7 @@ public ExtendedQueryTagController(IMediator mediator, ILogger<ExtendedQueryTagCo
[BodyModelStateValidator]
[ProducesResponseType(typeof(JsonResult), (int)HttpStatusCode.Accepted)]
[HttpPost]
[VersionedRoute(KnownRoutes.ExtendedQueryTagRoute)]
[Route(KnownRoutes.ExtendedQueryTagRoute)]
[AuditEventType(AuditEventSubType.AddExtendedQueryTag)]
public async Task<IActionResult> PostAsync([FromBody] IEnumerable<AddExtendedQueryTagEntry> extendedQueryTags)
Expand All @@ -61,6 +63,7 @@ public async Task<IActionResult> PostAsync([FromBody] IEnumerable<AddExtendedQue

[ProducesResponseType(typeof(JsonResult), (int)HttpStatusCode.NoContent)]
[HttpDelete]
[VersionedRoute(KnownRoutes.DeleteExtendedQueryTagRoute)]
[Route(KnownRoutes.DeleteExtendedQueryTagRoute)]
[AuditEventType(AuditEventSubType.RemoveExtendedQueryTag)]
public async Task<IActionResult> DeleteAsync(string tagPath)
Expand All @@ -83,6 +86,7 @@ public async Task<IActionResult> DeleteAsync(string tagPath)
[ProducesResponseType(typeof(JsonResult), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
[HttpGet]
[VersionedRoute(KnownRoutes.ExtendedQueryTagRoute)]
[Route(KnownRoutes.ExtendedQueryTagRoute)]
[AuditEventType(AuditEventSubType.GetAllExtendedQueryTags)]
public async Task<IActionResult> GetAllTagsAsync()
Expand All @@ -108,6 +112,7 @@ public async Task<IActionResult> GetAllTagsAsync()
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
[HttpGet]
[VersionedRoute(KnownRoutes.GetExtendedQueryTagRoute)]
[Route(KnownRoutes.GetExtendedQueryTagRoute)]
[AuditEventType(AuditEventSubType.GetExtendedQueryTag)]
public async Task<IActionResult> GetTagAsync(string tagPath)
Expand Down
7 changes: 7 additions & 0 deletions src/Microsoft.Health.Dicom.Api/Controllers/QueryController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

namespace Microsoft.Health.Dicom.Api.Controllers
{
[ApiVersion("1.0-prerelease")]
[QueryModelStateValidator]
[ServiceFilter(typeof(DicomAudit.AuditLoggingFilterAttribute))]
public class QueryController : Controller
Expand All @@ -45,6 +46,7 @@ public QueryController(IMediator mediator, ILogger<QueryController> logger)
[ProducesResponseType(typeof(IEnumerable<DicomDataset>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
[ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)]
[VersionedRoute(KnownRoutes.QueryAllStudiesRoute)]
[Route(KnownRoutes.QueryAllStudiesRoute)]
[AuditEventType(AuditEventSubType.Query)]
public async Task<IActionResult> QueryForStudyAsync()
Expand All @@ -64,6 +66,7 @@ public async Task<IActionResult> QueryForStudyAsync()
[ProducesResponseType(typeof(IEnumerable<DicomDataset>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
[ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)]
[VersionedRoute(KnownRoutes.QueryAllSeriesRoute)]
[Route(KnownRoutes.QueryAllSeriesRoute)]
[AuditEventType(AuditEventSubType.Query)]
public async Task<IActionResult> QueryForSeriesAsync()
Expand All @@ -83,6 +86,7 @@ public async Task<IActionResult> QueryForSeriesAsync()
[ProducesResponseType(typeof(IEnumerable<DicomDataset>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
[ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)]
[VersionedRoute(KnownRoutes.QuerySeriesInStudyRoute)]
[Route(KnownRoutes.QuerySeriesInStudyRoute)]
[AuditEventType(AuditEventSubType.Query)]
public async Task<IActionResult> QueryForSeriesInStudyAsync(string studyInstanceUid)
Expand All @@ -103,6 +107,7 @@ public async Task<IActionResult> QueryForSeriesInStudyAsync(string studyInstance
[ProducesResponseType(typeof(IEnumerable<DicomDataset>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
[ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)]
[VersionedRoute(KnownRoutes.QueryAllInstancesRoute)]
[Route(KnownRoutes.QueryAllInstancesRoute)]
[AuditEventType(AuditEventSubType.Query)]
public async Task<IActionResult> QueryForInstancesAsync()
Expand All @@ -122,6 +127,7 @@ public async Task<IActionResult> QueryForInstancesAsync()
[ProducesResponseType(typeof(IEnumerable<DicomDataset>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
[ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)]
[VersionedRoute(KnownRoutes.QueryInstancesInStudyRoute)]
[Route(KnownRoutes.QueryInstancesInStudyRoute)]
[AuditEventType(AuditEventSubType.Query)]
public async Task<IActionResult> QueryForInstancesInStudyAsync(string studyInstanceUid)
Expand All @@ -142,6 +148,7 @@ public async Task<IActionResult> QueryForInstancesInStudyAsync(string studyInsta
[ProducesResponseType(typeof(IEnumerable<DicomDataset>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
[ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)]
[VersionedRoute(KnownRoutes.QueryInstancesInSeriesRoute)]
[Route(KnownRoutes.QueryInstancesInSeriesRoute)]
[AuditEventType(AuditEventSubType.Query)]
public async Task<IActionResult> QueryForInstancesInSeriesAsync(string studyInstanceUid, string seriesInstanceUid)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

namespace Microsoft.Health.Dicom.Api.Controllers
{
[ApiVersion("1.0-prerelease")]
[QueryModelStateValidator]
[ServiceFilter(typeof(DicomAudit.AuditLoggingFilterAttribute))]
public class RetrieveController : Controller
Expand All @@ -51,6 +52,7 @@ public RetrieveController(IMediator mediator, ILogger<RetrieveController> logger
[ProducesResponseType((int)HttpStatusCode.NotFound)]
[ProducesResponseType((int)HttpStatusCode.NotAcceptable)]
[HttpGet]
[VersionedRoute(KnownRoutes.StudyRoute, Name = KnownRouteNames.VersionedRetrieveStudy)]
[Route(KnownRoutes.StudyRoute, Name = KnownRouteNames.RetrieveStudy)]
[AuditEventType(AuditEventSubType.Retrieve)]
public async Task<IActionResult> GetStudyAsync(string studyInstanceUid)
Expand All @@ -69,6 +71,7 @@ public async Task<IActionResult> GetStudyAsync(string studyInstanceUid)
[ProducesResponseType((int)HttpStatusCode.NotAcceptable)]
[ProducesResponseType((int)HttpStatusCode.NotModified)]
[HttpGet]
[VersionedRoute(KnownRoutes.StudyMetadataRoute)]
[Route(KnownRoutes.StudyMetadataRoute)]
[AuditEventType(AuditEventSubType.RetrieveMetadata)]
public async Task<IActionResult> GetStudyMetadataAsync([FromHeader(Name = IfNoneMatch)] string ifNoneMatch, string studyInstanceUid)
Expand All @@ -85,6 +88,7 @@ public async Task<IActionResult> GetStudyMetadataAsync([FromHeader(Name = IfNone
[ProducesResponseType((int)HttpStatusCode.NotFound)]
[ProducesResponseType((int)HttpStatusCode.NotAcceptable)]
[HttpGet]
[VersionedRoute(KnownRoutes.SeriesRoute)]
[Route(KnownRoutes.SeriesRoute)]
[AuditEventType(AuditEventSubType.Retrieve)]
public async Task<IActionResult> GetSeriesAsync(
Expand All @@ -106,6 +110,7 @@ public async Task<IActionResult> GetSeriesAsync(
[ProducesResponseType((int)HttpStatusCode.NotAcceptable)]
[ProducesResponseType((int)HttpStatusCode.NotModified)]
[HttpGet]
[VersionedRoute(KnownRoutes.SeriesMetadataRoute)]
[Route(KnownRoutes.SeriesMetadataRoute)]
[AuditEventType(AuditEventSubType.RetrieveMetadata)]
public async Task<IActionResult> GetSeriesMetadataAsync([FromHeader(Name = IfNoneMatch)] string ifNoneMatch, string studyInstanceUid, string seriesInstanceUid)
Expand All @@ -123,6 +128,7 @@ public async Task<IActionResult> GetSeriesMetadataAsync([FromHeader(Name = IfNon
[ProducesResponseType((int)HttpStatusCode.NotFound)]
[ProducesResponseType((int)HttpStatusCode.NotAcceptable)]
[HttpGet]
[VersionedRoute(KnownRoutes.InstanceRoute, Name = KnownRouteNames.VersionedRetrieveInstance)]
[Route(KnownRoutes.InstanceRoute, Name = KnownRouteNames.RetrieveInstance)]
[AuditEventType(AuditEventSubType.Retrieve)]
public async Task<IActionResult> GetInstanceAsync(
Expand All @@ -148,6 +154,7 @@ public async Task<IActionResult> GetInstanceAsync(
[ProducesResponseType((int)HttpStatusCode.NotAcceptable)]
[ProducesResponseType((int)HttpStatusCode.NotModified)]
[HttpGet]
[VersionedRoute(KnownRoutes.InstanceMetadataRoute)]
[Route(KnownRoutes.InstanceMetadataRoute)]
[AuditEventType(AuditEventSubType.RetrieveMetadata)]
public async Task<IActionResult> GetInstanceMetadataAsync(
Expand All @@ -170,6 +177,7 @@ public async Task<IActionResult> GetInstanceMetadataAsync(
[ProducesResponseType((int)HttpStatusCode.NotFound)]
[ProducesResponseType((int)HttpStatusCode.NotAcceptable)]
[HttpGet]
[VersionedRoute(KnownRoutes.FrameRoute)]
[Route(KnownRoutes.FrameRoute)]
[AuditEventType(AuditEventSubType.Retrieve)]
public async Task<IActionResult> GetFramesAsync(
Expand Down
2 changes: 2 additions & 0 deletions src/Microsoft.Health.Dicom.Api/Controllers/StoreController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

namespace Microsoft.Health.Dicom.Api.Controllers
{
[ApiVersion("1.0-prerelease")]
[QueryModelStateValidator]
[ServiceFilter(typeof(DicomAudit.AuditLoggingFilterAttribute))]
public class StoreController : Controller
Expand All @@ -47,6 +48,7 @@ public StoreController(IMediator mediator, ILogger<StoreController> logger)
[ProducesResponseType(typeof(DicomDataset), (int)HttpStatusCode.Conflict)]
[ProducesResponseType((int)HttpStatusCode.UnsupportedMediaType)]
[HttpPost]
[VersionedRoute(KnownRoutes.StoreRoute)]
[Route(KnownRoutes.StoreRoute)]
[AuditEventType(AuditEventSubType.Store)]
public async Task<IActionResult> PostAsync(string studyInstanceUid = null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ namespace Microsoft.Health.Dicom.Api.Features.Routing
{
internal class KnownRouteNames
{
internal const string VersionedRetrieveStudy = nameof(VersionedRetrieveStudy);
internal const string RetrieveStudy = nameof(RetrieveStudy);

internal const string VersionedRetrieveInstance = nameof(VersionedRetrieveInstance);
internal const string RetrieveInstance = nameof(RetrieveInstance);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,10 @@ public UrlResolver(
public Uri ResolveRetrieveStudyUri(string studyInstanceUid)
{
EnsureArg.IsNotNull(studyInstanceUid, nameof(studyInstanceUid));
var hasVersion = _httpContextAccessor.HttpContext.Request.RouteValues.ContainsKey("version");

return RouteUri(
KnownRouteNames.RetrieveStudy,
hasVersion ? KnownRouteNames.VersionedRetrieveStudy : KnownRouteNames.RetrieveStudy,
new RouteValueDictionary()
{
{ KnownActionParameterNames.StudyInstanceUid, studyInstanceUid },
Expand All @@ -59,9 +60,10 @@ public Uri ResolveRetrieveStudyUri(string studyInstanceUid)
public Uri ResolveRetrieveInstanceUri(InstanceIdentifier instanceIdentifier)
{
EnsureArg.IsNotNull(instanceIdentifier, nameof(instanceIdentifier));
var hasVersion = _httpContextAccessor.HttpContext.Request.RouteValues.ContainsKey("version");

return RouteUri(
KnownRouteNames.RetrieveInstance,
hasVersion ? KnownRouteNames.VersionedRetrieveInstance : KnownRouteNames.RetrieveInstance,
new RouteValueDictionary()
{
{ KnownActionParameterNames.StudyInstanceUid, instanceIdentifier.StudyInstanceUid },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using Microsoft.AspNetCore.Mvc;

namespace Microsoft.Health.Dicom.Api.Features.Routing
{
public sealed class VersionedRouteAttribute : RouteAttribute
{
public VersionedRouteAttribute(string template)
: base("v{version:apiVersion}/" + template)
{
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Description>Common components, such as controllers, for Microsoft's DICOMweb APIs using ASP.NET Core.</Description>
Expand All @@ -16,6 +16,7 @@
<PackageReference Include="MediatR" Version="9.0.0" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.17.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="$(SdkPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="5.0.0" />
<PackageReference Include="Microsoft.Health.Abstractions" Version="$(HealthcareSharedPackageVersion)" />
<PackageReference Include="Microsoft.Health.Api" Version="$(HealthcareSharedPackageVersion)" />
<PackageReference Include="Microsoft.Health.Core" Version="$(HealthcareSharedPackageVersion)" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Dicom.Serialization;
using EnsureThat;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
Expand Down Expand Up @@ -94,6 +95,14 @@ public static IDicomServerBuilder AddDicomServer(
jsonOptions.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});

services.AddApiVersioning(c =>
{
c.AssumeDefaultVersionWhenUnspecified = true;
c.DefaultApiVersion = new ApiVersion(1, 0, "prerelease");
c.ReportApiVersions = true;
c.UseApiBehavior = false;
});

services.AddSingleton<IUrlResolver, UrlResolver>();

services.RegisterAssemblyModules(typeof(DicomMediatorExtensions).Assembly, dicomServerConfiguration.Features);
Expand Down
Loading

0 comments on commit 6fb1527

Please sign in to comment.