From f568654c7c7a9a0baebb70e380058706a1077439 Mon Sep 17 00:00:00 2001 From: Bart Koelman <10324372+bkoelman@users.noreply.github.com> Date: Sat, 23 Mar 2024 10:00:33 +0100 Subject: [PATCH 1/3] Clarify that it's fine to use explicit controllers --- docs/usage/extensibility/controllers.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/usage/extensibility/controllers.md b/docs/usage/extensibility/controllers.md index 7e54d3fb9c..68e1d86ea3 100644 --- a/docs/usage/extensibility/controllers.md +++ b/docs/usage/extensibility/controllers.md @@ -2,6 +2,8 @@ To expose API endpoints, ASP.NET controllers need to be defined. +## Auto-generated controllers + _since v5_ Controllers are auto-generated (using [source generators](https://docs.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview)) when you add `[Resource]` on your model class: @@ -14,7 +16,12 @@ public class Article : Identifiable } ``` -## Resource Access Control +> [!NOTE] +> Auto-generated controllers are convenient to get started, but may not work as expected with certain customizations. +> For example, when model classes are defined in a separate project, the controllers are generated in that project as well, which is probably not what you want. +> In such cases, it's perfectly fine to use [explicit controllers](#explicit-controllers) instead. + +### Resource Access Control It is often desirable to limit which endpoints are exposed on your controller. A subset can be specified too: @@ -52,7 +59,7 @@ DELETE http://localhost:14140/articles/1 HTTP/1.1 } ``` -## Augmenting controllers +### Augmenting controllers Auto-generated controllers can easily be augmented because they are partial classes. For example: @@ -91,9 +98,9 @@ partial class ArticlesController In case you don't want to use auto-generated controllers and define them yourself (see below), remove `[Resource]` from your models or use `[Resource(GenerateControllerEndpoints = JsonApiEndpoints.None)]`. -## Earlier versions +## Explicit controllers -In earlier versions of JsonApiDotNetCore, you needed to create controllers that inherit from `JsonApiController`. For example: +To define your own controller class, inherit from `JsonApiController`. For example: ```c# public class ArticlesController : JsonApiController From 2fcfc65867f96b8e153851dca326c72610b2725a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 23 Mar 2024 16:02:09 +0000 Subject: [PATCH 2/3] Bump dotnet-reportgenerator-globaltool from 5.2.3 to 5.2.4 (#1510) --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 12fd6de2b4..b255005e33 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -15,7 +15,7 @@ ] }, "dotnet-reportgenerator-globaltool": { - "version": "5.2.3", + "version": "5.2.4", "commands": [ "reportgenerator" ] From 86ff1966e9d2ab26c7a6b5132e4c4fe8d4d50593 Mon Sep 17 00:00:00 2001 From: Bart Koelman <10324372+bkoelman@users.noreply.github.com> Date: Sat, 23 Mar 2024 17:25:16 +0100 Subject: [PATCH 3/3] Add support for configuring the visibility of the "describedby" link --- .../Resources/Annotations/LinkTypes.shared.cs | 13 ++++++----- .../Annotations/RelationshipAttribute.cs | 2 +- .../RelationshipAttribute.netstandard.cs | 2 +- .../ResourceLinksAttribute.shared.cs | 6 ++--- .../Serialization/Response/LinkBuilder.cs | 13 +++++++---- .../UnitTests/Links/LinkInclusionTests.cs | 23 ++++++++++++++++++- 6 files changed, 42 insertions(+), 17 deletions(-) diff --git a/src/JsonApiDotNetCore.Annotations/Resources/Annotations/LinkTypes.shared.cs b/src/JsonApiDotNetCore.Annotations/Resources/Annotations/LinkTypes.shared.cs index 7e996828b9..632b8b9ed3 100644 --- a/src/JsonApiDotNetCore.Annotations/Resources/Annotations/LinkTypes.shared.cs +++ b/src/JsonApiDotNetCore.Annotations/Resources/Annotations/LinkTypes.shared.cs @@ -3,10 +3,11 @@ namespace JsonApiDotNetCore.Resources.Annotations; [Flags] public enum LinkTypes { - Self = 1 << 0, - Related = 1 << 1, - Pagination = 1 << 2, - NotConfigured = 1 << 3, - None = 1 << 4, - All = Self | Related | Pagination + NotConfigured = 0, + None = 1 << 0, + Self = 1 << 1, + Related = 1 << 2, + DescribedBy = 1 << 3, + Pagination = 1 << 4, + All = Self | Related | DescribedBy | Pagination } diff --git a/src/JsonApiDotNetCore.Annotations/Resources/Annotations/RelationshipAttribute.cs b/src/JsonApiDotNetCore.Annotations/Resources/Annotations/RelationshipAttribute.cs index 0b4848ada1..492af08c60 100644 --- a/src/JsonApiDotNetCore.Annotations/Resources/Annotations/RelationshipAttribute.cs +++ b/src/JsonApiDotNetCore.Annotations/Resources/Annotations/RelationshipAttribute.cs @@ -70,7 +70,7 @@ internal set /// Configures which links to write in the relationship-level links object for this relationship. Defaults to , /// which falls back to and then falls back to RelationshipLinks in global options. /// - public LinkTypes Links { get; set; } = LinkTypes.NotConfigured; + public LinkTypes Links { get; set; } /// /// Whether or not this relationship can be included using the include query string parameter. This is true by default. diff --git a/src/JsonApiDotNetCore.Annotations/Resources/Annotations/RelationshipAttribute.netstandard.cs b/src/JsonApiDotNetCore.Annotations/Resources/Annotations/RelationshipAttribute.netstandard.cs index d7af592564..054d7b1af3 100644 --- a/src/JsonApiDotNetCore.Annotations/Resources/Annotations/RelationshipAttribute.netstandard.cs +++ b/src/JsonApiDotNetCore.Annotations/Resources/Annotations/RelationshipAttribute.netstandard.cs @@ -9,7 +9,7 @@ namespace JsonApiDotNetCore.Resources.Annotations; public abstract class RelationshipAttribute : ResourceFieldAttribute { /// - public LinkTypes Links { get; set; } = LinkTypes.NotConfigured; + public LinkTypes Links { get; set; } /// [Obsolete("Use AllowInclude in Capabilities instead.")] diff --git a/src/JsonApiDotNetCore.Annotations/Resources/Annotations/ResourceLinksAttribute.shared.cs b/src/JsonApiDotNetCore.Annotations/Resources/Annotations/ResourceLinksAttribute.shared.cs index 010f87db5e..2c2c353f3a 100644 --- a/src/JsonApiDotNetCore.Annotations/Resources/Annotations/ResourceLinksAttribute.shared.cs +++ b/src/JsonApiDotNetCore.Annotations/Resources/Annotations/ResourceLinksAttribute.shared.cs @@ -13,18 +13,18 @@ public sealed class ResourceLinksAttribute : Attribute /// Configures which links to write in the top-level links object for this resource type. Defaults to , which falls /// back to TopLevelLinks in global options. /// - public LinkTypes TopLevelLinks { get; set; } = LinkTypes.NotConfigured; + public LinkTypes TopLevelLinks { get; set; } /// /// Configures which links to write in the resource-level links object for this resource type. Defaults to , which /// falls back to ResourceLinks in global options. /// - public LinkTypes ResourceLinks { get; set; } = LinkTypes.NotConfigured; + public LinkTypes ResourceLinks { get; set; } /// /// Configures which links to write in the relationship-level links object for all relationships of this resource type. Defaults to /// , which falls back to RelationshipLinks in global options. This can be overruled per relationship by setting /// . /// - public LinkTypes RelationshipLinks { get; set; } = LinkTypes.NotConfigured; + public LinkTypes RelationshipLinks { get; set; } } diff --git a/src/JsonApiDotNetCore/Serialization/Response/LinkBuilder.cs b/src/JsonApiDotNetCore/Serialization/Response/LinkBuilder.cs index 7740141002..ffb8b8e9b7 100644 --- a/src/JsonApiDotNetCore/Serialization/Response/LinkBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/LinkBuilder.cs @@ -100,12 +100,15 @@ private static string NoAsyncSuffix(string actionName) SetPaginationInTopLevelLinks(resourceType!, links); } - string? documentDescriptionUrl = _documentDescriptionLinkProvider.GetUrl(); - - if (!string.IsNullOrEmpty(documentDescriptionUrl)) + if (ShouldIncludeTopLevelLink(LinkTypes.DescribedBy, resourceType)) { - var requestUri = new Uri(HttpContext.Request.GetEncodedUrl()); - links.DescribedBy = UriNormalizer.Normalize(documentDescriptionUrl, _options.UseRelativeLinks, requestUri); + string? documentDescriptionUrl = _documentDescriptionLinkProvider.GetUrl(); + + if (!string.IsNullOrEmpty(documentDescriptionUrl)) + { + var requestUri = new Uri(HttpContext.Request.GetEncodedUrl()); + links.DescribedBy = UriNormalizer.Normalize(documentDescriptionUrl, _options.UseRelativeLinks, requestUri); + } } return links.HasValue() ? links : null; diff --git a/test/JsonApiDotNetCoreTests/UnitTests/Links/LinkInclusionTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/Links/LinkInclusionTests.cs index 94b9cb9386..9b8890618a 100644 --- a/test/JsonApiDotNetCoreTests/UnitTests/Links/LinkInclusionTests.cs +++ b/test/JsonApiDotNetCoreTests/UnitTests/Links/LinkInclusionTests.cs @@ -22,36 +22,49 @@ public sealed class LinkInclusionTests [InlineData(LinkTypes.NotConfigured, LinkTypes.None, LinkTypes.None)] [InlineData(LinkTypes.NotConfigured, LinkTypes.Self, LinkTypes.Self)] [InlineData(LinkTypes.NotConfigured, LinkTypes.Related, LinkTypes.Related)] + [InlineData(LinkTypes.NotConfigured, LinkTypes.DescribedBy, LinkTypes.DescribedBy)] [InlineData(LinkTypes.NotConfigured, LinkTypes.Pagination, LinkTypes.Pagination)] [InlineData(LinkTypes.NotConfigured, LinkTypes.All, LinkTypes.All)] [InlineData(LinkTypes.None, LinkTypes.NotConfigured, LinkTypes.None)] [InlineData(LinkTypes.None, LinkTypes.None, LinkTypes.None)] [InlineData(LinkTypes.None, LinkTypes.Self, LinkTypes.None)] [InlineData(LinkTypes.None, LinkTypes.Related, LinkTypes.None)] + [InlineData(LinkTypes.None, LinkTypes.DescribedBy, LinkTypes.None)] [InlineData(LinkTypes.None, LinkTypes.Pagination, LinkTypes.None)] [InlineData(LinkTypes.None, LinkTypes.All, LinkTypes.None)] [InlineData(LinkTypes.Self, LinkTypes.NotConfigured, LinkTypes.Self)] [InlineData(LinkTypes.Self, LinkTypes.None, LinkTypes.Self)] [InlineData(LinkTypes.Self, LinkTypes.Self, LinkTypes.Self)] [InlineData(LinkTypes.Self, LinkTypes.Related, LinkTypes.Self)] + [InlineData(LinkTypes.Self, LinkTypes.DescribedBy, LinkTypes.Self)] [InlineData(LinkTypes.Self, LinkTypes.Pagination, LinkTypes.Self)] [InlineData(LinkTypes.Self, LinkTypes.All, LinkTypes.Self)] [InlineData(LinkTypes.Related, LinkTypes.NotConfigured, LinkTypes.Related)] [InlineData(LinkTypes.Related, LinkTypes.None, LinkTypes.Related)] [InlineData(LinkTypes.Related, LinkTypes.Self, LinkTypes.Related)] [InlineData(LinkTypes.Related, LinkTypes.Related, LinkTypes.Related)] + [InlineData(LinkTypes.Related, LinkTypes.DescribedBy, LinkTypes.Related)] [InlineData(LinkTypes.Related, LinkTypes.Pagination, LinkTypes.Related)] [InlineData(LinkTypes.Related, LinkTypes.All, LinkTypes.Related)] + [InlineData(LinkTypes.DescribedBy, LinkTypes.NotConfigured, LinkTypes.DescribedBy)] + [InlineData(LinkTypes.DescribedBy, LinkTypes.None, LinkTypes.DescribedBy)] + [InlineData(LinkTypes.DescribedBy, LinkTypes.Self, LinkTypes.DescribedBy)] + [InlineData(LinkTypes.DescribedBy, LinkTypes.Related, LinkTypes.DescribedBy)] + [InlineData(LinkTypes.DescribedBy, LinkTypes.DescribedBy, LinkTypes.DescribedBy)] + [InlineData(LinkTypes.DescribedBy, LinkTypes.Pagination, LinkTypes.DescribedBy)] + [InlineData(LinkTypes.DescribedBy, LinkTypes.All, LinkTypes.DescribedBy)] [InlineData(LinkTypes.Pagination, LinkTypes.NotConfigured, LinkTypes.Pagination)] [InlineData(LinkTypes.Pagination, LinkTypes.None, LinkTypes.Pagination)] [InlineData(LinkTypes.Pagination, LinkTypes.Self, LinkTypes.Pagination)] [InlineData(LinkTypes.Pagination, LinkTypes.Related, LinkTypes.Pagination)] + [InlineData(LinkTypes.Pagination, LinkTypes.DescribedBy, LinkTypes.Pagination)] [InlineData(LinkTypes.Pagination, LinkTypes.Pagination, LinkTypes.Pagination)] [InlineData(LinkTypes.Pagination, LinkTypes.All, LinkTypes.Pagination)] [InlineData(LinkTypes.All, LinkTypes.NotConfigured, LinkTypes.All)] [InlineData(LinkTypes.All, LinkTypes.None, LinkTypes.All)] [InlineData(LinkTypes.All, LinkTypes.Self, LinkTypes.All)] [InlineData(LinkTypes.All, LinkTypes.Related, LinkTypes.All)] + [InlineData(LinkTypes.All, LinkTypes.DescribedBy, LinkTypes.All)] [InlineData(LinkTypes.All, LinkTypes.Pagination, LinkTypes.All)] [InlineData(LinkTypes.All, LinkTypes.All, LinkTypes.All)] public void Applies_cascading_settings_for_top_level_links(LinkTypes linksInResourceType, LinkTypes linksInOptions, LinkTypes expected) @@ -88,7 +101,7 @@ public void Applies_cascading_settings_for_top_level_links(LinkTypes linksInReso var linkGenerator = new FakeLinkGenerator(); var controllerResourceMapping = new FakeControllerResourceMapping(); var paginationParser = new PaginationParser(); - var documentDescriptionLinkProvider = new NoDocumentDescriptionLinkProvider(); + var documentDescriptionLinkProvider = new NonEmptyDocumentDescriptionLinkProvider(); var linkBuilder = new LinkBuilder(options, request, paginationContext, httpContextAccessor, linkGenerator, controllerResourceMapping, paginationParser, documentDescriptionLinkProvider); @@ -435,4 +448,12 @@ public override string GetUriByAddress(TAddress address, RouteValueDic throw new NotImplementedException(); } } + + private sealed class NonEmptyDocumentDescriptionLinkProvider : IDocumentDescriptionLinkProvider + { + public string GetUrl() + { + return "openapi.yaml"; + } + } }