# Accept the Official Hack: Build-Time OpenAPI Detection in .NET 10 Minimal APIs

It is straightforward to configure a minimal API to [produce an OpenAPI document at build time](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/aspnetcore-openapi?view=aspnetcore-10.0&tabs=net-cli%2Cnetcore-cli#generate-openapi-documents-at-build-time). This runs the API during build, requests the OpenAPI document from it, and saves it to disk.

The slightly trickier part is to put checks in `Program.cs` to exclude any startup code that cannot run at build time. This is typically done because configuration key/value pairs are not available at that time. For example:

```csharp
if (!isBuildTime)
{
    connString = builder.Configuration.GetConnectionString("AppDB") ?? 
    throw new InvalidOperationException("Connection string 'AppDB' is not configured.");

    builder.Services.AddDbContext<AppDbContext>(
        options =>
        {
            options.UseNpgsql(connString);
        }
    );
}
```

The question is how to deduce that the API has been launched at build time, i.e. `isBuildTime` should be true?

The [official way](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/aspnetcore-openapi?view=aspnetcore-10.0&tabs=net-cli%2Cnetcore-cli#customize-runtime-behavior-during-build-time-document-generation) of doing this is to check that the assembly that invoked the API is `"GetDocument.Insider"`:

```csharp
var isBuildTime = 
	Assembly.GetEntryAssembly()?.GetName().Name == "GetDocument.Insider";
```

[`GetDocument.Insider.dll`](https://github.com/dotnet/aspnetcore/blob/main/src/Tools/GetDocumentInsider/README.md) is the command line tool that automatically runs during build of the API if the `.csproj` includes the following reference:

```xml
<PackageReference Include="Microsoft.Extensions.ApiDescription.Server"
	 Version="10.0.7">
      ...
</PackageReference>
```

This package is a shim. It only provides build targets and props and hooks into the build of the API to [run the command-line tool `dotnet-getdocument`](https://github.com/dotnet/dotnet/blob/551f471e3c3df1828061c6faaa9dc9161fdbb154/src/aspnetcore/src/Tools/Extensions.ApiDescription.Server/src/build/Microsoft.Extensions.ApiDescription.Server.targets#L55-L69). This tool in turn [runs the command line tool `GetDocument.Insider`](https://github.com/dotnet/aspnetcore/blob/main/src/Tools/dotnet-getdocument/README.md) that we check for.

This is a pretty convoluted sequence:

1.  `Microsoft.Extensions.ApiDescription.Server` provides targets that run during build of the API.
    
2.  One of those targets runs the command line tool `dotnet-getdocument`
    
3.  That in turn runs the command line tool `GetDocument.Insider`
    
4.  That in turn runs the API and fetches the `/openapi/v1/json` (or other configured endpoint) to get the OpenAPI document and saves it to disk.
    

Checking in Program.cs if the API was invoked by the assembly `GetDocument.Insider.dll` to determine if it is running during build bothers me. It is a *hack*, and an uncomfortable one, for two reasons.

First, the name of the tool could change. If the sequence above gets cleaned up or modified in the future, the assembly `GetDocument.Insider` might vanish. This means we would need to change the check we do to compute if the API is running at build time for OpenAPI document generation.

Second, there is a standard, time-worn way for checking in ASP.NET if `Program.cs` is running in a specific environment and executing code conditionally based on that: [runtime environments](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/environments?view=aspnetcore-10.0).

We could define a custom environment name, say `BuildTime`, that sits alongside the predefined environments `Production`, `Development` and `Staging`, and check for it in `Program.cs`:

```plaintext
isBuildTime = builder.Environment.IsEnvironment("BuildTime")
```

This is the canonical solution to the problem of determining if the API was launched at build time. The environment name is not going to change either - unlike the assembly name used in the official solution - because this is a custom environment name that we have handpicked for our build.

Unfortunately, there is no official way of declaring the environment name under which the API should be launched during build. Nor would you have any success with setting the environment variables `ASPNETCORE_ENVIRONMENT` or `DOTNET_ENVIRONMENT` on the command line before building the API. In other words, this would not work:

```bash
export ASPNETCORE_ENVIRONMENT=BuildTime
dotnet build
```

The environment simply doesn't get picked up and passed to the API by the tooling that generates the OpenAPI document at build time.

However, I did succeed in setting the environment name so that it gets picked up by the API when it runs during build.

I show my solution in the next section. But my conclusion is that, even though it sounds like the right thing to do, actually doing it is even more fragile and *hacky* than the official solution.

So, until .NET provides an official way of providing a custom environment name to an API during build-time OpenAPI document generation - and there is an [open issue in `dotnet/aspnetcore` repo](https://github.com/dotnet/aspnetcore/issues/54698) requesting exactly this feature - I would just (grit my teeth and) use the official solution:

```csharp
var isBuildTime = 
	Assembly.GetEntryAssembly()?.GetName().Name == "GetDocument.Insider";

if (!isBuildTime) { 
	// load services and middlewares that require 
	// configuration key values that are not available at build time
	
}
```

## Providing a custom environment to an API for build-time for OpenAPI generation - DON'T DO THIS

I define a custom .NET environment named `BuildTime` in my minimal API project.

While you can pass command-line arguments to the `dotnet-getdocument` tool using property `<OpenApiGenerateDocumentsOptions>` in the API's `csproj`:

```xml
<OpenApiGenerateDocumentsOptions>--file-name openapi_v1</OpenApiGenerateDocumentsOptions>
```

none of these corresponds to the environment name of the API that would be launched.

Also, you cannot pass `--environment Development` command line argument for `dotnet run` (`dotnet build` does not have an `--environment` argument):

```bash
dotnet run --environment BuildTime
```

using the `<OpenApiGenerateDocumentsOptions>` csproj property.

Nor can you set `ASPNETCORE_ENVIRONMENT` or `DOTNET_ENVIRONMENT` environment variable to `"BuildTime"` in such a way that it would be available to the API that would be launched during build (as explained in the previous section).

So to get the `GetDocument.Insider` tool to launch the API in environment `"BuildTime"`, you essentially have to first construct the command line for running the `GetDocument.Insider` yourself then execute the command with environment variable `ASPNETCORE_ENVIRONMENT` set to `BuildTime`. This is done like this:

1.  add a reference to `Microsoft.Extensions.ApiDescription.Server` as before using `dotnet add package Microsoft.Extensions.ApiDescription.Server` which reflects in the `csproj` like this:
    
    ```xml
    <PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="9.0.6">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    ```
    
2.  But DISABLE automatic OpenAPI document generation on build to via these properties in the csproj:
    
    ```xml
    <PropertyGroup>
    	<OpenApiGenerateDocuments>false</OpenApiGenerateDocuments>     <OpenApiGenerateDocumentsOnBuild>false</OpenApiGenerateDocumentsOnBuild>   
    </PropertyGroup>
    ```
    
    Also REMOVE the following properties we added originally. They are not going to be used any more:
    
    ```xml
    <PropertyGroup>
    <OpenApiDocumentsDirectory>.</OpenApiDocumentsDirectory>
    <OpenApiGenerateDocumentsOptions>--file-name openapi</OpenApiGenerateDocumentsOptions>
    ```
    

\`\`\`

3.  Construct the command line for running `GetDocument.Insider` tool yourself, then execute this command line, by putting these targets in your `csproj`:
    
    ```xml
    <Target Name="ResolveApiDescriptionPackage">
        <ItemGroup>
          <_ApiDescriptionPackage Include="@(PackageReference)" Condition="'%(Identity)' == 'Microsoft.Extensions.ApiDescription.Server'" />
        </ItemGroup>
      </Target>
    <Target Name="GenerateOpenApiDocumentsAfterBuild" DependsOnTargets="ResolveReferences;ResolveApiDescriptionPackage" AfterTargets="Build">
        <PropertyGroup>
          <_DetectedApiVersion>%(_ApiDescriptionPackage.Version)</_DetectedApiVersion>
          <_DotNetGetDocumentCommand>dotnet "$(NuGetPackageRoot)microsoft.extensions.apidescription.server/$(_DetectedApiVersion)/tools/dotnet-getdocument.dll" --assembly "$(TargetPath)" --file-list "$(MSBuildProjectDirectory)/obj/cloudcartapi.OpenApiFiles.cache" --framework "$(TargetFrameworkIdentifier),Version=$(TargetFrameworkVersion)" --output "$(MSBuildProjectDirectory)" --project "$(MSBuildProjectFullPath)" --assets-file "$(ProjectAssetsFile)" --platform "$(Platform)" --file-name openapi_v1</_DotNetGetDocumentCommand>
        </PropertyGroup>
        <Exec Command="$(_DotNetGetDocumentCommand)" EnvironmentVariables="ASPNETCORE_ENVIRONMENT=BuildTime;DOTNET_ENVIRONMENT=BuildTime" LogStandardErrorAsError="true" />
      </Target>
    ```
    
4.  Finally, for robustness, I add this flourish to the `csproj`:
    
    ```xml
    <!-- 
    The command run in GenerateOpenApiDocumentsAfterBuild target above is a little bit brittle and breaks if the major version of the ApiDescription package does not match the major version of .NET set in <TargetFramework> property a the top.
    
    While this is a very unlikely scenario, because the major version of every .NET package should be/would be the same as the major version of .NET framework that the project compiles against, still f the scenario does arise, instead of getting to the point where GenerateOpenApiDocumentsAfterBuild target runs and fails with an unedifying error, in this task we detect the situation early, show an informative error message and stop the build.
    -->
      <Target Name="ValidateApiDescriptionVersion" BeforeTargets="Build" DependsOnTargets="ResolveApiDescriptionPackage">
        <PropertyGroup>
          <_TFMajorVersion>$([System.Text.RegularExpressions.Regex]::Match('$(TargetFramework)', 'net(\d+)').Groups[1].Value)</_TFMajorVersion>
          <_ApiDescriptionVersion>@(_ApiDescriptionPackage->'%(Version)')</_ApiDescriptionVersion>
          <_ApiDescriptionMajorVersion>$([System.Text.RegularExpressions.Regex]::Match('$(_ApiDescriptionVersion)', '^(\d+)').Groups[1].Value)</_ApiDescriptionMajorVersion>
        </PropertyGroup>
        <Error Condition="'$(_TFMajorVersion)' != '$(_ApiDescriptionMajorVersion)'" Text="Version mismatch: TargetFramework '$(TargetFramework)' has major version $(_TFMajorVersion) but Microsoft.Extensions.ApiDescription.Server version '$(_ApiDescriptionVersion)' has major version $(_ApiDescriptionMajorVersion). Update this package to a version whose major version number is same as that of the target framework (specified in TargetFramework property in this csproj file)." />
      </Target>
    ```
    

**The real issue isn't that this solution is a bit messy, it is that it is even more fragile**: if in a future version Microsoft change the name of the file `dotnet-getdocument.dll` or its location within the package `microsoft.extensions.apidescription.server` or names of any of the numerous arguments we are passing to it, this logic would break.

On the other hand, in the solution given in MS Docs (and excerpted from there in the article above):

```csharp
var isBuildTime = Assembly.GetEntryAssembly()?.GetName().Name == "GetDocument.Insider";
    
if (!isBuildTime) { 
	// load services and middlewares that DO require configuration key values that are not available at build time
	
}
```

if the name of assembly changes we would know about it through the Release Notes of the .NET version in which this happens, the new official solution would again be documented/updated, and we can update our code.

Hence why the official, if fragile-looking, solution for conditionally excluding code in `Program.cs` that requires configuration data is the one that should be used.
