From 9e0ad2c9b90431d50bce5ebda6f156aff22eaaa1 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Tue, 23 Feb 2021 13:02:51 +0100 Subject: [PATCH 1/2] Corrected link rendering for hosting inside an IIS application virtual directory --- .../Middleware/JsonApiMiddleware.cs | 5 + .../HostingInIIS/ArtGalleriesController.cs | 16 +++ .../HostingInIIS/ArtGallery.cs | 15 +++ .../HostingInIIS/HostingDbContext.cs | 15 +++ .../HostingInIIS/HostingFakers.cs | 22 ++++ .../HostingInIIS/HostingStartup.cs | 32 +++++ .../HostingInIIS/HostingTests.cs | 114 ++++++++++++++++++ .../IntegrationTests/HostingInIIS/Painting.cs | 14 +++ .../HostingInIIS/PaintingsController.cs | 19 +++ .../KebabCasingConventionStartup.cs | 1 - 10 files changed, 252 insertions(+), 1 deletion(-) create mode 100644 test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/ArtGalleriesController.cs create mode 100644 test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/ArtGallery.cs create mode 100644 test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingDbContext.cs create mode 100644 test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingFakers.cs create mode 100644 test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingStartup.cs create mode 100644 test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingTests.cs create mode 100644 test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/Painting.cs create mode 100644 test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/PaintingsController.cs diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs index 026e50682a..7a5149ce25 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs @@ -208,6 +208,11 @@ private static string GetBasePath(string resourceName, IJsonApiOptions options, builder.Append(httpRequest.Host); } + if (httpRequest.PathBase.HasValue) + { + builder.Append(httpRequest.PathBase); + } + string customRoute = GetCustomRoute(resourceName, options.Namespace, httpRequest.HttpContext); if (!string.IsNullOrEmpty(customRoute)) { diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/ArtGalleriesController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/ArtGalleriesController.cs new file mode 100644 index 0000000000..d7383df1eb --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/ArtGalleriesController.cs @@ -0,0 +1,16 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.Logging; + +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.HostingInIIS +{ + public sealed class ArtGalleriesController : JsonApiController + { + public ArtGalleriesController(IJsonApiOptions options, ILoggerFactory loggerFactory, + IResourceService resourceService) + : base(options, loggerFactory, resourceService) + { + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/ArtGallery.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/ArtGallery.cs new file mode 100644 index 0000000000..470b7c4e63 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/ArtGallery.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.HostingInIIS +{ + public sealed class ArtGallery : Identifiable + { + [Attr] + public string Theme { get; set; } + + [HasMany] + public ISet Paintings { get; set; } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingDbContext.cs new file mode 100644 index 0000000000..03795fe1cf --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingDbContext.cs @@ -0,0 +1,15 @@ +using Microsoft.EntityFrameworkCore; + +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.HostingInIIS +{ + public sealed class HostingDbContext : DbContext + { + public DbSet ArtGalleries { get; set; } + public DbSet Paintings { get; set; } + + public HostingDbContext(DbContextOptions options) + : base(options) + { + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingFakers.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingFakers.cs new file mode 100644 index 0000000000..428c27ad69 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingFakers.cs @@ -0,0 +1,22 @@ +using System; +using Bogus; +using TestBuildingBlocks; + +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.HostingInIIS +{ + internal sealed class HostingFakers : FakerContainer + { + private readonly Lazy> _lazyArtGalleryFaker = new Lazy>(() => + new Faker() + .UseSeed(GetFakerSeed()) + .RuleFor(artGallery => artGallery.Theme, f => f.Lorem.Word())); + + private readonly Lazy> _lazyPaintingFaker = new Lazy>(() => + new Faker() + .UseSeed(GetFakerSeed()) + .RuleFor(painting => painting.Title, f => f.Lorem.Sentence())); + + public Faker ArtGallery => _lazyArtGalleryFaker.Value; + public Faker Painting => _lazyPaintingFaker.Value; + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingStartup.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingStartup.cs new file mode 100644 index 0000000000..8d3da5f0e3 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingStartup.cs @@ -0,0 +1,32 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCoreExampleTests.Startups; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; + +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.HostingInIIS +{ + public sealed class HostingStartup : TestableStartup + where TDbContext : DbContext + { + public HostingStartup(IConfiguration configuration) : base(configuration) + { + } + + protected override void SetJsonApiOptions(JsonApiOptions options) + { + base.SetJsonApiOptions(options); + + options.Namespace = "public-api"; + options.IncludeTotalResourceCount = true; + } + + public override void Configure(IApplicationBuilder app, IWebHostEnvironment environment) + { + app.UsePathBase("/iis-application-virtual-directory"); + + base.Configure(app, environment); + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingTests.cs new file mode 100644 index 0000000000..a9efda9bba --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingTests.cs @@ -0,0 +1,114 @@ +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Serialization.Objects; +using TestBuildingBlocks; +using Xunit; + +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.HostingInIIS +{ + public sealed class HostingTests + : IClassFixture, HostingDbContext>> + { + private const string HostPrefix = "http://localhost"; + + private readonly ExampleIntegrationTestContext, HostingDbContext> _testContext; + private readonly HostingFakers _fakers = new HostingFakers(); + + public HostingTests(ExampleIntegrationTestContext, HostingDbContext> testContext) + { + _testContext = testContext; + } + + [Fact] + public async Task Get_primary_resources_with_include_returns_links() + { + // Arrange + var gallery = _fakers.ArtGallery.Generate(); + gallery.Paintings = _fakers.Painting.Generate(1).ToHashSet(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + await dbContext.ClearTableAsync(); + dbContext.ArtGalleries.Add(gallery); + await dbContext.SaveChangesAsync(); + }); + + const string route = "/iis-application-virtual-directory/public-api/artGalleries?include=paintings"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Related.Should().BeNull(); + responseDocument.Links.First.Should().Be(HostPrefix + route); + responseDocument.Links.Last.Should().Be(HostPrefix + route); + responseDocument.Links.Prev.Should().BeNull(); + responseDocument.Links.Next.Should().BeNull(); + + string galleryLink = HostPrefix + $"/iis-application-virtual-directory/public-api/artGalleries/{gallery.StringId}"; + + responseDocument.ManyData.Should().HaveCount(1); + responseDocument.ManyData[0].Links.Self.Should().Be(galleryLink); + responseDocument.ManyData[0].Relationships["paintings"].Links.Self.Should().Be(galleryLink + "/relationships/paintings"); + responseDocument.ManyData[0].Relationships["paintings"].Links.Related.Should().Be(galleryLink + "/paintings"); + + // TODO: The next link is wrong: it should use the custom route. + string paintingLink = HostPrefix + $"/iis-application-virtual-directory/public-api/paintings/{gallery.Paintings.ElementAt(0).StringId}"; + + responseDocument.Included.Should().HaveCount(1); + responseDocument.Included[0].Links.Self.Should().Be(paintingLink); + responseDocument.Included[0].Relationships["exposedAt"].Links.Self.Should().Be(paintingLink + "/relationships/exposedAt"); + responseDocument.Included[0].Relationships["exposedAt"].Links.Related.Should().Be(paintingLink + "/exposedAt"); + } + + [Fact] + public async Task Get_primary_resources_with_include_on_custom_route_returns_links() + { + // Arrange + var painting = _fakers.Painting.Generate(); + painting.ExposedAt = _fakers.ArtGallery.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + await dbContext.ClearTableAsync(); + dbContext.Paintings.Add(painting); + await dbContext.SaveChangesAsync(); + }); + + const string route = "/iis-application-virtual-directory/custom/path/to/paintings?include=exposedAt"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Related.Should().BeNull(); + responseDocument.Links.First.Should().Be(HostPrefix + route); + responseDocument.Links.Last.Should().Be(HostPrefix + route); + responseDocument.Links.Prev.Should().BeNull(); + responseDocument.Links.Next.Should().BeNull(); + + string paintingLink = HostPrefix + $"/iis-application-virtual-directory/custom/path/to/paintings/{painting.StringId}"; + + responseDocument.ManyData.Should().HaveCount(1); + responseDocument.ManyData[0].Links.Self.Should().Be(paintingLink); + responseDocument.ManyData[0].Relationships["exposedAt"].Links.Self.Should().Be(paintingLink + "/relationships/exposedAt"); + responseDocument.ManyData[0].Relationships["exposedAt"].Links.Related.Should().Be(paintingLink + "/exposedAt"); + + // TODO: The next link is wrong: it should not use the custom route. + string galleryLink = HostPrefix + $"/iis-application-virtual-directory/custom/path/to/artGalleries/{painting.ExposedAt.StringId}"; + + responseDocument.Included.Should().HaveCount(1); + responseDocument.Included[0].Links.Self.Should().Be(galleryLink); + responseDocument.Included[0].Relationships["paintings"].Links.Self.Should().Be(galleryLink + "/relationships/paintings"); + responseDocument.Included[0].Relationships["paintings"].Links.Related.Should().Be(galleryLink + "/paintings"); + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/Painting.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/Painting.cs new file mode 100644 index 0000000000..75979bf30c --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/Painting.cs @@ -0,0 +1,14 @@ +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.HostingInIIS +{ + public sealed class Painting : Identifiable + { + [Attr] + public string Title { get; set; } + + [HasOne] + public ArtGallery ExposedAt { get; set; } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/PaintingsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/PaintingsController.cs new file mode 100644 index 0000000000..ced2427169 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/PaintingsController.cs @@ -0,0 +1,19 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Controllers.Annotations; +using JsonApiDotNetCore.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.HostingInIIS +{ + [DisableRoutingConvention, Route("custom/path/to/paintings")] + public sealed class PaintingsController : JsonApiController + { + public PaintingsController(IJsonApiOptions options, ILoggerFactory loggerFactory, + IResourceService resourceService) + : base(options, loggerFactory, resourceService) + { + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/KebabCasingConventionStartup.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/KebabCasingConventionStartup.cs index d871dac25e..b7980fcb10 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/KebabCasingConventionStartup.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/KebabCasingConventionStartup.cs @@ -17,7 +17,6 @@ protected override void SetJsonApiOptions(JsonApiOptions options) { base.SetJsonApiOptions(options); - options.IncludeExceptionStackTraceInErrors = true; options.Namespace = "public-api"; options.UseRelativeLinks = true; options.IncludeTotalResourceCount = true; From 02496277f072ccecd574449126700bbfa17ed34d Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Tue, 23 Feb 2021 13:54:49 +0100 Subject: [PATCH 2/2] Added link to open issue --- .../IntegrationTests/HostingInIIS/HostingTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingTests.cs index a9efda9bba..e3e35b7f72 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingTests.cs @@ -58,6 +58,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.ManyData[0].Relationships["paintings"].Links.Related.Should().Be(galleryLink + "/paintings"); // TODO: The next link is wrong: it should use the custom route. + // https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/956 string paintingLink = HostPrefix + $"/iis-application-virtual-directory/public-api/paintings/{gallery.Paintings.ElementAt(0).StringId}"; responseDocument.Included.Should().HaveCount(1); @@ -103,6 +104,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.ManyData[0].Relationships["exposedAt"].Links.Related.Should().Be(paintingLink + "/exposedAt"); // TODO: The next link is wrong: it should not use the custom route. + // https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/956 string galleryLink = HostPrefix + $"/iis-application-virtual-directory/custom/path/to/artGalleries/{painting.ExposedAt.StringId}"; responseDocument.Included.Should().HaveCount(1);