Skip to content

Commit fe476ba

Browse files
committed
feat: support Microsoft.AspNetCore.OpenApi
1 parent 869fc04 commit fe476ba

File tree

12 files changed

+279
-49
lines changed

12 files changed

+279
-49
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## Features
44

5+
- Added support for .NET 9.
6+
- `IntelliTect.Coalesce.Swashbuckle` is now deprecated in favor of `Microsoft.AspNetCore.OpenApi` for .NET 9+ projects. To upgrade:
7+
- Replace `services.AddSwaggerGen(...)` with `services.AddOpenApi()`.
8+
- Replace `app.MapSwagger()` with `app.MapOpenApi()`.
9+
- Replace `app.UseSwaggerUI()` with `app.MapScalarApiReference()` from package `Scalar.AspNetCore`. The OpenAPI UI maps to `/scalar/v1` by default.
510
- Added a `reset` method to all [API caller objects](https://intellitect.github.io/Coalesce/stacks/vue/layers/api-clients.html#api-callers). This method resets all stateful fields on the object to default values.
611
- Template: Added username/password auth option (aka individual user accounts, aka local accounts)
712

Directory.Packages.props

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<PackageVersion Include="McMaster.Extensions.CommandLineUtils" Version="2.3.0" />
1818
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="$(DotNetPackageVersionSpec)" />
1919
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="$(DotNetPackageVersionSpec)" />
20+
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
2021
<PackageVersion Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="$(DotNetPackageVersionSpec)" />
2122
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.8.0" />
2223
<PackageVersion Include="Microsoft.Data.SqlClient" Version="5.2.2" />
@@ -28,13 +29,15 @@
2829
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="$(DotNetPackageVersionSpec)" />
2930
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="$(DotNetPackageVersionSpec)" />
3031
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
32+
<PackageVersion Include="Microsoft.OpenApi" Version="1.6.22" />
3133
<PackageVersion Include="Microsoft.OpenApi.Readers" Version="1.6.22" />
3234
<PackageVersion Include="Moq" Version="4.20.72" />
3335
<PackageVersion Include="Moq.AutoMock" Version="3.5.0" />
3436
<PackageVersion Include="NETStandard.Library" Version="2.0.3" />
3537
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
3638
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
3739
<PackageVersion Include="Roslynator.Analyzers" Version="4.12.7" />
40+
<PackageVersion Include="Scalar.AspNetCore" Version="1.2.36" />
3841
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.8.1" />
3942
<PackageVersion Include="System.Net.Http.Json" Version="8.0.1" />
4043
<PackageVersion Include="xunit" Version="2.9.2" />

playground/Coalesce.Domain/Case.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Microsoft.EntityFrameworkCore;
99
using System;
1010
using System.Collections.Generic;
11+
using System.ComponentModel;
1112
using System.ComponentModel.DataAnnotations;
1213
using System.ComponentModel.DataAnnotations.Schema;
1314
using System.IO;
@@ -222,6 +223,7 @@ public class AllOpenCases : StandardDataSource<Case, AppDbContext>
222223
public AllOpenCases(CrudContext<AppDbContext> context) : base(context) { }
223224

224225
[Coalesce]
226+
[Description("Only include cases opened on or after this date")]
225227
public DateTimeOffset? MinDate { get; set; }
226228

227229
public override IQueryable<Case> GetQuery(IDataSourceParameters parameters) => Db.Cases

playground/Coalesce.Web.Vue3/Api/Generated/CaseController.g.cs

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

playground/Coalesce.Web.Vue3/Coalesce.Web.Vue3.csproj

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@
99
</PropertyGroup>
1010

1111
<ItemGroup>
12-
<!--<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.2" />
13-
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.3" />
14-
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.1" />-->
12+
<PackageReference Include="Scalar.AspNetCore" />
1513
</ItemGroup>
1614
<ItemGroup>
1715
<ProjectReference Include="..\Coalesce.Domain\Coalesce.Domain.csproj" />

playground/Coalesce.Web.Vue3/Program.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
using Microsoft.AspNetCore.Authentication;
55
using Microsoft.AspNetCore.Authentication.Cookies;
66
using Microsoft.EntityFrameworkCore;
7+
using Microsoft.Extensions.DependencyInjection;
78
using Microsoft.Extensions.Logging.Console;
89
using Microsoft.Net.Http.Headers;
910
using Microsoft.OpenApi.Models;
11+
using Scalar.AspNetCore;
1012
using System.Security.Claims;
1113
using System.Text.Json;
1214
using System.Text.Json.Serialization;
@@ -59,7 +61,7 @@
5961
});
6062

6163

62-
//services.AddOpenApi(); // net9
64+
services.AddOpenApi();
6365

6466
services.AddSwaggerGen(c =>
6567
{
@@ -119,7 +121,10 @@
119121

120122
app.UseCors(c => c.AllowAnyOrigin().AllowAnyHeader());
121123
app.MapControllers();
122-
//app.MapOpenApi(); // net9
124+
125+
app.MapOpenApi();
126+
app.MapScalarApiReference();
127+
123128
app.MapSwagger();
124129
app.UseSwaggerUI(c =>
125130
{

playground/Coalesce.Web.Vue3/src/App.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
<v-btn variant="text" to="/test">Test</v-btn>
2121
<v-btn variant="text" to="/test-setup">Test2</v-btn>
2222
<v-btn variant="text" to="/audit-logs">Audit</v-btn>
23-
<v-btn variant="text" to="/swagger">OpenAPI</v-btn>
23+
<v-btn variant="text" href="/swagger">Swagger</v-btn>
24+
<v-btn variant="text" href="/scalar/v1">OpenAPI</v-btn>
2425
<v-btn variant="text" href="/coalesce-security">Security Overview</v-btn>
2526

2627
<v-menu offset-y>

src/IntelliTect.Coalesce.Swashbuckle/CoalesceApiOperationFilter.cs

Lines changed: 23 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Swashbuckle.AspNetCore.Swagger;
99
using Swashbuckle.AspNetCore.SwaggerGen;
1010
using System;
11+
using System.Collections.Generic;
1112
using System.Linq;
1213

1314
namespace IntelliTect.Coalesce.Swashbuckle
@@ -126,46 +127,40 @@ private void ProcessStandardParameters(OpenApiOperation operation, MethodViewMod
126127

127128
private void ProcessDataSources(OpenApiOperation operation, OperationFilterContext context, MethodViewModel method)
128129
{
129-
// In all reality, there will only ever be one data source parameter per action,
130-
// but might as well not make assumptions if we don't have to.
131-
foreach (var paramVm in method.Parameters.Where(p => p.Type.IsA(typeof(IDataSource<>))))
132-
{
133-
var declaredFor =
134-
paramVm.GetAttributeValue<DeclaredForAttribute>(a => a.DeclaredFor)
135-
?? paramVm.Type.GenericArgumentsFor(typeof(IDataSource<>))!.Single();
130+
var iDataSourceParam = method.Parameters.FirstOrDefault(p => p.Type.IsA(typeof(IDataSource<>)));
131+
if (iDataSourceParam is null) return;
132+
133+
var declaredFor =
134+
iDataSourceParam.GetAttributeValue<DeclaredForAttribute>(a => a.DeclaredFor)
135+
?? iDataSourceParam.Type.GenericArgumentsFor(typeof(IDataSource<>))!.Single();
136136

137-
var dataSources = declaredFor.ClassViewModel.ClientDataSources(reflectionRepository);
137+
var dataSources = declaredFor.ClassViewModel!.ClientDataSources(reflectionRepository);
138138

139-
var dataSourceParam = new OpenApiParameter
139+
var dataSourceNameParam = operation.Parameters.FirstOrDefault(p => p.Name == nameof(IDataSourceParameters.DataSource));
140+
if (dataSourceNameParam is not null)
141+
{
142+
dataSourceNameParam.Schema = new OpenApiSchema
140143
{
141-
In = ParameterLocation.Query,
142-
Name = paramVm.Name,
143-
Required = false,
144-
Schema = new OpenApiSchema
145-
{
146-
Type = "string",
147-
Enum = (new string[] { IntelliTect.Coalesce.Api.DataSources.DataSourceFactory.DefaultSourceName })
144+
Type = "string",
145+
Enum = (new string[] { IntelliTect.Coalesce.Api.DataSources.DataSourceFactory.DefaultSourceName })
148146
.Concat(dataSources.Select(ds => ds.ClientTypeName))
149147
.Select(n => new OpenApiString(n) as IOpenApiAny)
150148
.ToList()
151-
},
152149
};
153-
operation.Parameters.Add(dataSourceParam);
154150

155-
foreach (var dataSource in dataSources)
151+
foreach (var param in dataSources.SelectMany(ds => ds.DataSourceParameters).GroupBy(ds => ds.Name))
156152
{
157-
foreach (var dsParam in dataSource.DataSourceParameters)
153+
var openApiParam = operation.Parameters.FirstOrDefault(p =>
154+
p.Name.Equals($"{dataSourceNameParam.Name}.{param.Key}", StringComparison.OrdinalIgnoreCase));
155+
156+
if (openApiParam is not null)
158157
{
159-
var schema = context.SchemaGenerator.GenerateSchema(dsParam.Type.TypeInfo, context.SchemaRepository);
158+
var dataSourceNames = string.Join(", ", param.Select(p => p.EffectiveParent.ClientTypeName));
160159

161-
operation.Parameters.Add(new OpenApiParameter
160+
openApiParam.Description = string.Join(". \n", (new List<string>(param.Select(p => p.Description))
162161
{
163-
In = ParameterLocation.Query,
164-
Name = $"{paramVm.Name}.{dsParam.Name}",
165-
Required = false,
166-
Description = $"Used by Data Source {dataSource.ClientTypeName}",
167-
Schema = schema,
168-
});
162+
$"Used by data source{(param.Count() == 1 ? "" : "s")} {dataSourceNames}."
163+
}).Where(s => !string.IsNullOrWhiteSpace(s)).Distinct());
169164
}
170165
}
171166
}

src/IntelliTect.Coalesce/Api/OpenApi/CoalesceApiDescriptionProvider.cs

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,16 @@
1-
using IntelliTect.Coalesce.Models;
2-
using IntelliTect.Coalesce.TypeDefinition;
3-
using Microsoft.AspNetCore.Mvc;
1+
using IntelliTect.Coalesce.TypeDefinition;
42
using Microsoft.AspNetCore.Mvc.ApiExplorer;
53
using Microsoft.AspNetCore.Mvc.Controllers;
64
using Microsoft.AspNetCore.Mvc.ModelBinding;
7-
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
8-
using System;
9-
using System.Collections.Generic;
105
using System.Linq;
11-
using System.Text;
12-
using System.Threading.Tasks;
136

147
namespace IntelliTect.Coalesce.Api
158
{
169
/// <summary>
1710
/// Performs adjustments to the API metadata so that it doesn't cause .NET 9's OpenAPI generation to implode.
1811
/// In particular, we have to remove the parameters that are bound with Coalesce's custom model binders,
1912
/// since these cause the OpenAPI generator to throw exceptions. We re-add these definitions with
20-
/// CoalesceApiOperationFilter (for Swashbuckle), or CLASS_TBD for the native aspnetcore OpenApi gen.
13+
/// CoalesceApiOperationFilter.
2114
/// </summary>
2215
internal class CoalesceApiDescriptionProvider(ReflectionRepository reflectionRepository) : IApiDescriptionProvider
2316
{
@@ -50,7 +43,6 @@ private void ProcessStandardParameters(ApiDescription operation, MethodViewModel
5043
// We add them back in in CoalesceApiOperationFilter.
5144
// They break the new Microsoft.AspNetCore.OpenApi package in .NET 9
5245
// if we leave them present in the API descriptions.
53-
5446
foreach (var paramVm in method.Parameters.Where(p =>
5547
p.Type.IsA(typeof(IBehaviors<>)) ||
5648
p.Type.IsA(typeof(IDataSource<>))
@@ -72,14 +64,35 @@ private void ProcessStandardParameters(ApiDescription operation, MethodViewModel
7264
// Remove params that have no setter.
7365
// Honestly I'm clueless why this isn't default behavior, but OK.
7466
!p.PropViewModel.HasSetter
75-
76-
// Remove the string "DataSource" parameter that is redundant with our data source model binding functionality.
77-
|| (p.PropViewModel.Name == nameof(IDataSourceParameters.DataSource) && p.OperationParam.Type == typeof(string))
7867
))
7968
{
8069
parameters.Remove(noSetterProp.OperationParam);
8170
}
8271
}
72+
73+
foreach (var paramVm in method.Parameters.Where(p => p.Type.IsA(typeof(IDataSource<>))))
74+
{
75+
var declaredFor =
76+
paramVm.GetAttributeValue<DeclaredForAttribute>(a => a.DeclaredFor)
77+
?? paramVm.Type.GenericArgumentsFor(typeof(IDataSource<>))!.Single();
78+
79+
var dataSources = declaredFor.ClassViewModel!.ClientDataSources(reflectionRepository);
80+
81+
foreach (var dataSource in dataSources)
82+
{
83+
foreach (var dsParam in dataSource.DataSourceParameters)
84+
{
85+
operation.ParameterDescriptions.Add(new ApiParameterDescription()
86+
{
87+
Source = BindingSource.Query,
88+
Name = $"{paramVm.Name}.{dsParam.Name}",
89+
IsRequired = false,
90+
Type = dsParam.Type.TypeInfo,
91+
ModelMetadata = operation.ParameterDescriptions.First().ModelMetadata.GetMetadataForProperty(dataSource.Type.TypeInfo, dsParam.Name)
92+
});
93+
}
94+
}
95+
}
8396
}
8497
}
8598
}

0 commit comments

Comments
 (0)