diff --git a/src/Examples/GettingStarted/Startup.cs b/src/Examples/GettingStarted/Startup.cs index 5a59ba2514..2560e8d6f0 100644 --- a/src/Examples/GettingStarted/Startup.cs +++ b/src/Examples/GettingStarted/Startup.cs @@ -9,7 +9,7 @@ namespace GettingStarted { public sealed class Startup { - // This method gets called by the runtime. Use this method to add services to the container. + // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddDbContext( @@ -19,7 +19,7 @@ public void ConfigureServices(IServiceCollection services) options => options.Namespace = "api"); } - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, SampleDbContext context) { context.Database.EnsureDeleted(); diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/NoNamespaceStartup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/NoNamespaceStartup.cs new file mode 100644 index 0000000000..a6fade4548 --- /dev/null +++ b/src/Examples/JsonApiDotNetCoreExample/Startups/NoNamespaceStartup.cs @@ -0,0 +1,23 @@ +using JsonApiDotNetCore.Configuration; +using Microsoft.AspNetCore.Hosting; + +namespace JsonApiDotNetCoreExample.Startups +{ + /// + /// This should be in JsonApiDotNetCoreExampleTests project but changes in .net core 3.0 + /// do no longer allow that. See https://github.com/aspnet/AspNetCore/issues/15373. + /// + public class NoNamespaceStartup : TestStartup + { + public NoNamespaceStartup(IWebHostEnvironment env) : base(env) + { + } + + protected override void ConfigureJsonApiOptions(JsonApiOptions options) + { + base.ConfigureJsonApiOptions(options); + + options.Namespace = null; + } + } +} diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs index d2952010e2..c4106c91b9 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Net; using System.Net.Http.Headers; +using System.Text; using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Extensions; @@ -29,11 +30,11 @@ public JsonApiMiddleware(RequestDelegate next) _next = next; } - public async Task Invoke(HttpContext httpContext, - IControllerResourceMapping controllerResourceMapping, - IJsonApiOptions options, - ICurrentRequest currentRequest, - IResourceGraph resourceGraph) + public async Task Invoke(HttpContext httpContext, + IControllerResourceMapping controllerResourceMapping, + IJsonApiOptions options, + ICurrentRequest currentRequest, + IResourceGraph resourceGraph) { var routeValues = httpContext.GetRouteData().Values; @@ -171,18 +172,28 @@ private static string GetBaseId(RouteValueDictionary routeValues) private static string GetBasePath(string resourceName, IJsonApiOptions options, HttpRequest httpRequest) { - if (options.RelativeLinks) + var builder = new StringBuilder(); + + if (!options.RelativeLinks) { - return options.Namespace; + builder.Append(httpRequest.Scheme); + builder.Append("://"); + builder.Append(httpRequest.Host); } - var customRoute = GetCustomRoute(httpRequest.Path.Value, resourceName, options.Namespace); - var toReturn = $"{httpRequest.Scheme}://{httpRequest.Host}/{options.Namespace}"; - if (customRoute != null) + string customRoute = GetCustomRoute(httpRequest.Path.Value, resourceName, options.Namespace); + if (!string.IsNullOrEmpty(customRoute)) + { + builder.Append('/'); + builder.Append(customRoute); + } + else if (!string.IsNullOrEmpty(options.Namespace)) { - toReturn += $"/{customRoute}"; + builder.Append('/'); + builder.Append(options.Namespace); } - return toReturn; + + return builder.ToString(); } private static string GetCustomRoute(string path, string resourceName, string apiNamespace) diff --git a/src/JsonApiDotNetCore/Serialization/Server/Builders/LinkBuilder.cs b/src/JsonApiDotNetCore/Serialization/Server/Builders/LinkBuilder.cs index 140f3e31b9..9cd27888a9 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/Builders/LinkBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Server/Builders/LinkBuilder.cs @@ -92,7 +92,7 @@ private void SetPageLinks(ResourceContext resourceContext, TopLevelLinks links) private string GetSelfTopLevelLink(ResourceContext resourceContext) { var builder = new StringBuilder(); - builder.Append(GetBasePath()); + builder.Append(_currentRequest.BasePath); builder.Append("/"); builder.Append(resourceContext.ResourceName); @@ -127,7 +127,7 @@ private string GetPageLink(ResourceContext resourceContext, int pageOffset, int parameters["page[number]"] = pageOffset.ToString(); }); - return $"{GetBasePath()}/{resourceContext.ResourceName}" + queryString; + return $"{_currentRequest.BasePath}/{resourceContext.ResourceName}" + queryString; } private string BuildQueryString(Action> updateAction) @@ -175,17 +175,17 @@ public RelationshipLinks GetRelationshipLinks(RelationshipAttribute relationship private string GetSelfRelationshipLink(string parent, string parentId, string navigation) { - return $"{GetBasePath()}/{parent}/{parentId}/relationships/{navigation}"; + return $"{_currentRequest.BasePath}/{parent}/{parentId}/relationships/{navigation}"; } private string GetSelfResourceLink(string resource, string resourceId) { - return $"{GetBasePath()}/{resource}/{resourceId}"; + return $"{_currentRequest.BasePath}/{resource}/{resourceId}"; } private string GetRelatedRelationshipLink(string parent, string parentId, string navigation) { - return $"{GetBasePath()}/{parent}/{parentId}/{navigation}"; + return $"{_currentRequest.BasePath}/{parent}/{parentId}/{navigation}"; } /// @@ -221,15 +221,5 @@ private bool ShouldAddRelationshipLink(ResourceContext resourceContext, Relation return _options.RelationshipLinks.HasFlag(link); } - - protected string GetBasePath() - { - if (_options.RelativeLinks) - { - return string.Empty; - } - - return _currentRequest.BasePath; - } } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/LinksWithNamespaceTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/LinksWithNamespaceTests.cs new file mode 100644 index 0000000000..d3944d2464 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/LinksWithNamespaceTests.cs @@ -0,0 +1,68 @@ +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCoreExample.Models; +using Newtonsoft.Json; +using Xunit; + +namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec.DocumentTests +{ + public sealed class LinksWithNamespaceTests : FunctionalTestCollection + { + public LinksWithNamespaceTests(StandardApplicationFactory factory) : base(factory) + { + } + + [Fact] + public async Task GET_RelativeLinks_True_With_Namespace_Returns_RelativeLinks() + { + // Arrange + var person = new Person(); + + _dbContext.People.Add(person); + _dbContext.SaveChanges(); + + var route = "/api/v1/people/" + person.StringId; + var request = new HttpRequestMessage(HttpMethod.Get, route); + + var options = (JsonApiOptions) _factory.GetService(); + options.RelativeLinks = true; + + // Act + var response = await _factory.Client.SendAsync(request); + var responseString = await response.Content.ReadAsStringAsync(); + var document = JsonConvert.DeserializeObject(responseString); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("/api/v1/people/" + person.StringId, document.Links.Self); + } + + [Fact] + public async Task GET_RelativeLinks_False_With_Namespace_Returns_AbsoluteLinks() + { + // Arrange + var person = new Person(); + + _dbContext.People.Add(person); + _dbContext.SaveChanges(); + + var route = "/api/v1/people/" + person.StringId; + var request = new HttpRequestMessage(HttpMethod.Get, route); + + var options = (JsonApiOptions) _factory.GetService(); + options.RelativeLinks = false; + + // Act + var response = await _factory.Client.SendAsync(request); + var responseString = await response.Content.ReadAsStringAsync(); + var document = JsonConvert.DeserializeObject(responseString); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal($"http://localhost/api/v1/people/" + person.StringId, document.Links.Self); + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/LinksWithoutNamespaceTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/LinksWithoutNamespaceTests.cs new file mode 100644 index 0000000000..d3914c2e12 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/LinksWithoutNamespaceTests.cs @@ -0,0 +1,68 @@ +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using JsonApiDotNetCore.Models; +using Newtonsoft.Json; +using Xunit; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCoreExample.Models; + +namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec.DocumentTests +{ + public sealed class LinksWithoutNamespaceTests : FunctionalTestCollection + { + public LinksWithoutNamespaceTests(NoNamespaceApplicationFactory factory) : base(factory) + { + } + + [Fact] + public async Task GET_RelativeLinks_True_Without_Namespace_Returns_RelativeLinks() + { + // Arrange + var person = new Person(); + + _dbContext.People.Add(person); + _dbContext.SaveChanges(); + + var route = "/people/" + person.StringId; + var request = new HttpRequestMessage(HttpMethod.Get, route); + + var options = (JsonApiOptions) _factory.GetService(); + options.RelativeLinks = true; + + // Act + var response = await _factory.Client.SendAsync(request); + var responseString = await response.Content.ReadAsStringAsync(); + var document = JsonConvert.DeserializeObject(responseString); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("/people/" + person.StringId, document.Links.Self); + } + + [Fact] + public async Task GET_RelativeLinks_False_Without_Namespace_Returns_AbsoluteLinks() + { + // Arrange + var person = new Person(); + + _dbContext.People.Add(person); + _dbContext.SaveChanges(); + + var route = "/people/" + person.StringId; + var request = new HttpRequestMessage(HttpMethod.Get, route); + + var options = (JsonApiOptions) _factory.GetService(); + options.RelativeLinks = false; + + // Act + var response = await _factory.Client.SendAsync(request); + var responseString = await response.Content.ReadAsStringAsync(); + var document = JsonConvert.DeserializeObject(responseString); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal($"http://localhost/people/" + person.StringId, document.Links.Self); + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/Factories/NoNamespaceApplicationFactory.cs b/test/JsonApiDotNetCoreExampleTests/Factories/NoNamespaceApplicationFactory.cs new file mode 100644 index 0000000000..e13326ffcf --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Factories/NoNamespaceApplicationFactory.cs @@ -0,0 +1,13 @@ +using JsonApiDotNetCoreExample.Startups; +using Microsoft.AspNetCore.Hosting; + +namespace JsonApiDotNetCoreExampleTests +{ + public class NoNamespaceApplicationFactory : CustomApplicationFactoryBase + { + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.UseStartup(); + } + } +}