dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.28k stars 9.96k forks source link

Microsoft.Extensions.ApiDescription.Server Unable to find service type 'Microsoft.Extensions.ApiDescriptions.IDocumentProvider' in dependency injection container. #57939

Closed JTeeuwissen closed 1 week ago

JTeeuwissen commented 2 weeks ago

Is there an existing issue for this?

Describe the bug

I ran into this bug while trying to use Microsoft.Extensions.ApiDescription.Server for openapi file generation.

Given this code (see repro for .sln)

using NSwag.AspNetCore;

var builder = WebApplication.CreateBuilder(args);

MethodThatBreaksBuild(builder);

var app = builder.Build();

app.MapGet("/hello", () => "Hello World!");

app.UseSwaggerUi(settings => settings.SwaggerRoutes.Add(new SwaggerUiRoute("v1", "/openapi/v1.json")));
app.MapOpenApi();

app.Run();

void MethodThatBreaksBuild(WebApplicationBuilder webApplicationBuilder)
{
  webApplicationBuilder.Services.AddOpenApi();
}

and dependencies

<ItemGroup>
  <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0-rc.1.24452.1" />
  <PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="9.0.0-rc.1.24452.1">
    <PrivateAssets>all</PrivateAssets>
    <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
  </PackageReference>
  <PackageReference Include="NSwag.AspNetCore" Version="14.0.2" />
</ItemGroup>

the generation during build fails on resolving the IDocumentProvider, although it is registered using AddOpenApi. Whenever I inline MethodThatBreaksBuild generation works as expected. Similar behavior occurs for MapOpenApi and conditional execution.

Expected Behavior

I expect an Example.json to be generated during build, without any errors.

Steps To Reproduce

Build this solution

Example.zip

Exceptions (if any)

dotnet build Example.sln

Restore complete (1,4s)
You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy
  Example failed with 2 error(s) (9,3s) → Example\bin\Debug\net9.0\Example.dll
    C:\Users\x\.nuget\packages\microsoft.extensions.apidescription.server\9.0.0-rc.1.24452.1\build\Microsoft.Extensions.ApiDescription.Server.targets(68,5): error : Unable to find service type 'Microsoft.Extensions.ApiDescriptions.IDocumentProvider' in dependency injection container. Update the 'Startup' class to register a document.
    C:\Users\x\.nuget\packages\microsoft.extensions.apidescription.server\9.0.0-rc.1.24452.1\build\Microsoft.Extensions.ApiDescription.Server.targets(68,5): error MSB3073: The command "dotnet "C:\Users\x\.nuget\packages\microsoft.extensions.apidescription.server\9.0.0-rc.1.24452.1\build\../tools/dotnet-getdocument.dll" --assembly "C:\Users\x\Desktop\Example
\Example\bin\Debug\net9.0\Example.dll" --file-list "obj\Example.OpenApiFiles.cache" --framework ".NETCoreApp,Version=v9.0" --output "C:\Users\x\Desktop\Example\Example" --project "Example" --assets-file "C:\Users\x\Desktop\Example\Example\obj\project.assets.json" --platform "AnyCPU" " exited with code 10.

Build failed with 2 error(s) in 11,7s

.NET Version

9.0.100-rc.1.24452.12

Anything else?

No response

PaskalSunari commented 2 weeks ago

downgrade to previous version if needed

captainsafia commented 2 weeks ago

@JTeeuwissen Thanks for reporting this issue!

I believe the issue here might actually be related to the fact that you reference both NSwag.AspNetCore and Microsoft.AspNetCore.OpenApi in your project.

The IDocumentProvider interface is internal and it's up to each OpenAPI implementation to define it in their sources. The lookup logic that we use to resolve the IDocumentProvider interface through private reflection does so by querying for it across all assemblies that have been loaded in tho the application domain:

https://github.com/dotnet/aspnetcore/blob/d05f3586adc0439595436ff6b8677548302fd64d/src/Tools/GetDocumentInsider/src/Commands/GetDocumentCommandWorker.cs#L193-L201

I suspect that when the AddOpenApi method is moved to its own method, it's shifted lower in the list of loaded assemblies and so the codepath above ends up resolving the IDocumentProvider interface from NSwag's assemblies. Since you never called NSwag's registration method, it fails to resolve one that has been registered.

If you're only using NSwag to serve Swagger UI, I would recommend using Swashbuckle's Swashhbuckle.AspNetCore.SwaggerUi package if you're able to. It splits up the Swagger UI-related APIs into their own assembly so you don't run into any issues with IDocumentProvider discovery failing.

Can you try swapping out Swashbuckle in your sample app and verifying if that solves the issue for you?

P.S.: I realize the lookup logic in ApiDescription.Server is super spooky given that you can run into issues like this. I'm hoping we can do something drastic to improve the package in the future. See my comment on this issue for more info.

JTeeuwissen commented 1 week ago

Thank you @captainsafia for the elaborate explanation. Using Swashbuckle I cannot replicate the same issue.

I'm hoping we can do something drastic to improve the package in the future

Is that before the release of dotnet 9?

captainsafia commented 1 week ago

Is that before the release of dotnet 9?

Unfortunately not....we're already in the release candidate for .NET 9 so the opportunity to merge in major changes isn't there.

JTeeuwissen commented 1 week ago

Is that before the release of dotnet 9?

Unfortunately not....we're already in the release candidate for .NET 9 so the opportunity to merge in major changes isn't there.

After thinking about it a bit more. Would it be possible to move the

var service = services.GetService(serviceType);
if (service == null)
{
    _reporter.WriteError(Resources.FormatServiceNotFound(DocumentService));
    return false;
}

part up, so it can be included in the assembly scanning?

Type serviceType = null;
object? service = null;
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
    // Try to find the IDocumentProvider in the assembly
    var assemblyServiceType = assembly.GetType(DocumentService, throwOnError: false);
    if (assemblyServiceType == null) {
        continue;
    }

    serviceType = assemblyServiceType;

    // Try to find the IDocumentProvider in the IServiceProvider.
    // Multiple assemblies might define an IDocumentProvider but they might not be registered.
    service = services.GetService(serviceType);
    if (service != null)
    {
        break;
    }
}

if (serviceType == null)
{
    _reporter.WriteError(Resources.FormatServiceTypeNotFound(DocumentService));
    return false;
}

if (service == null)
{
    _reporter.WriteError(Resources.FormatServiceNotFound(DocumentService));
    return false;
}

This should have no other impact other than fixing the problem as described in this issue.

JTeeuwissen commented 1 week ago

@captainsafia ping instead closed issues don't give notifications