Skip to content

Calling AddOpenApiForJsonApi before AddJsonApi regenerates/re-registers controllers, causing an error #1734

Closed
@isaaclyman

Description

@isaaclyman

DESCRIPTION

In a simple project that uses JsonApiDotNetCore.OpenApi.Swashbuckle, when I create a single resource and do something like:

// Program.cs

var builder = WebApplication.CreateBuilder(args);

// BEGIN FOOTGUN
builder.Services.AddOpenApiForJsonApi();
builder.Services.AddJsonApi(opts => {
  opts.Namespace = "api";
}, discovery: discovery => discovery.AddCurrentAssembly());
// END FOOTGUN

// ...

var app = builder.Build();
app.UseJsonApi();

I repeatedly get the error described in #1179:

JsonApiDotNetCore.Errors.InvalidConfigurationException: Multiple controllers found for resource type 'myResources': 'MyApp.Api.Library.MyResource.MyResourceController' and 'MyApp.Api.Library.MyResource.MyResourceController'

  • This happens whether I use the [Resource] attribute or define an explicit controller.
  • It happens whether I use automatic discovery or manually register the resource.

Any of the following changes stops the error from occurring:

  • Removing the call to app.UseJsonApi();
  • Removing JsonApiDotNetCore.OpenApi.Swashbuckle from the project
  • Putting the AddOpenApiForJsonApi() call after AddJsonApi()

STEPS TO REPRODUCE

  1. Use both JsonApiDotNetCore and JsonApiDotNetCore.OpenApi.Swashbuckle in a project.
  2. Call Services.AddOpenApiForJsonApi() before Services.AddJsonApi().
  3. Start the project.

EXPECTED BEHAVIOR

I would expect the package to handle this gracefully or surface an error telling me that I need to call those methods in a specific order.

ACTUAL BEHAVIOR

I see an error for which the only matching issue in Github tells me it's a problem with source generation in .NET 6.

VERSIONS USED

  • JsonApiDotNetCore version: 5.7.1
  • .NET Core version: 8.0.410
  • Entity Framework Core version: N/A
  • Database provider: N/A

Activity

bkoelman

bkoelman commented on May 29, 2025

@bkoelman
Member

Thanks for reporting this. It is true that OpenAPI must be registered afterwards. Would you like to create a PR that throws an exception with a helpful message?

isaaclyman

isaaclyman commented on May 29, 2025

@isaaclyman
SponsorAuthor

I would like to, but unfortunately don't have the time. If my schedule opens up in a couple months I might be able to take a stab at it.

bkoelman

bkoelman commented on Jun 2, 2025

@bkoelman
Member

No worries, I can take care of it.

I'm curious about your experience with our OpenAPI support. I'd appreciate any feedback; anything you'd like to share? Are you interested only in a documentation website, or also in generating typed client libraries?

isaaclyman

isaaclyman commented on Jun 2, 2025

@isaaclyman
SponsorAuthor

For the moment it's just for documentation. It works pretty well, though I have noticed that if I create a controller that only supports certain methods (e.g. injects a getAll service and leaves the others null), all the other methods still show up in Swagger even though they're not allowed. (There are workarounds, of course.)

I've also had a difficult time figuring out how to generate dual documentation for both endpoints generated/owned by this library and our own custom endpoints. I imagine there's a way, if I invest more time into it, though it looks like the code does some service replacement so I'm not sure if that makes things more difficult.

In general, though, I'm happy there's a well-maintained library with so much extensibility. For our use case we mostly care about conformance to the JSON:API spec from an external client perspective, and the application in question won't be doing any direct database querying, so it's valuable to be able to swap out functionality at whatever level is most convenient.

One thing I'd like to see in the documentation (and may come back to add later on, once I've got a handle on it) is some rules of thumb for which level of extensibility to plug into: BaseJsonApiController, JsonApiController, Service, Repository, or Resource Definition. Not all the boundaries between those things are clear to me yet.

Anyway that's what comes to mind after diving in for a few days. Thanks for your work building and maintaining this and I hope to be able to give some time back to the library later on.

bkoelman

bkoelman commented on Jun 2, 2025

@bkoelman
Member

It works pretty well, though I have noticed that if I create a controller that only supports certain methods (e.g. injects a getAll service and leaves the others null), all the other methods still show up in Swagger even though they're not allowed. (There are workarounds, of course.)

I'm aware of that limitation, but I'm not sure what we can do to improve that, other than document better. Because the endpoints are exposed (they just throw when called), scanning for action methods will detect them. We can't know what their implementation does. See the comments at:

private static bool IsEndpointAvailable(JsonApiEndpoints endpoint, ResourceType resourceType)
{
JsonApiEndpoints availableEndpoints = GetGeneratedControllerEndpoints(resourceType);
if (availableEndpoints == JsonApiEndpoints.None)
{
// Auto-generated controllers are disabled, so we can't know what to hide.
// It is assumed that a handwritten JSON:API controller only provides action methods for what it supports.
// To accomplish that, derive from BaseJsonApiController instead of JsonApiController.
return true;
}
// For an overridden JSON:API action method in a partial class to show up, it's flag must be turned on in [Resource].
// Otherwise, it is considered to be an action method that throws because the endpoint is unavailable.
return IncludesEndpoint(endpoint, availableEndpoints);
}

This also answers your question on whether to choose JsonApiController or BaseJsonApiController.

BaseJsonApiController contains only logic, without exposing any endpoints. Deriving from it gives flexibility to specify which endpoints to expose (using [HttpGet] and similar attributes). Alternatively, switch to auto-generated controllers, because then we know what's being exposed to get it right.

I've also had a difficult time figuring out how to generate dual documentation for both endpoints generated/owned by this library and our own custom endpoints. I imagine there's a way, if I invest more time into it, though it looks like the code does some service replacement so I'm not sure if that makes things more difficult.

Yes, we do lots of replacements to compensate for the fact that our controller action method signatures don't match the JSON:API request/response structure. Basically, there are two use cases:

  1. Extra action methods on JsonApiDotNetCore controllers (they derive from one of our base types and return a JSON:API response): We currently hide them because support hasn't been implemented yet. Use cases are welcome; please indicate what kind of extensibility you need.
  2. Non-JSON:API controllers: We ignore them, so the default Swashbuckle handling kicks in. This is typically used for RPC-style or file transfers.

Which of the two are you referring to?

Endpoint expansion happens in JsonApiActionDescriptorCollectionProvider. This turns a route such as /article/{id}/{relationshipName} into /article/{id}/author and /article/{id}/revisions.

One thing I'd like to see in the documentation (and may come back to add later on, once I've got a handle on it) is some rules of thumb for which level of extensibility to plug into: BaseJsonApiController, JsonApiController, Service, Repository, or Resource Definition. Not all the boundaries between those things are clear to me yet.

This is documented a bit at https://www.jsonapi.net/usage/faq.html#whats-the-best-place-to-put-my-custom-businessvalidation-logic and the intro text at https://www.jsonapi.net/usage/extensibility/services.html. The essence is that resource definitions are resource-oriented, whereas controllers, resource services and repositories are pipeline-oriented. So it depends on your needs as to where is best to plug in. For example, to handle "any filter anywhere on an article" use a resource definition. To handle "an article on a primary endpoint," use the pipeline extensibility. The latter doesn't kick in when an article appears in a secondary endpoint or in an include at another endpoint.

Anyway that's what comes to mind after diving in for a few days. Thanks for your work building and maintaining this and I hope to be able to give some time back to the library later on.

Thanks for your kind words. Your feedback is greatly appreciated.

isaaclyman

isaaclyman commented on Jun 3, 2025

@isaaclyman
SponsorAuthor

Thanks, this is really helpful commentary.

To answer your question about additional endpoints in Swagger, my use case is option 2 (non-JSON:API controllers). I'm probably making some kind of configuration mistake on my end, but they simply don't show up.

bkoelman

bkoelman commented on Jun 3, 2025

@bkoelman
Member

Non-JSON:API controllers: We ignore them, so the default Swashbuckle handling kicks in. This is typically used for RPC-style or file transfers.

I was wrong about this. They are currently blocked to avoid downstream crashes. This is tracked at #1066.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Participants

      @isaaclyman@bkoelman

      Issue actions

        Calling AddOpenApiForJsonApi before AddJsonApi regenerates/re-registers controllers, causing an error · Issue #1734 · json-api-dotnet/JsonApiDotNetCore