Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
0574bca
remove unused class
Fresa Mar 12, 2026
5e8920f
add generator for sequential json enumerators
Fresa Mar 12, 2026
af4da6d
feat(request): generate sequential json request content
Fresa Mar 12, 2026
3258669
test: move unit tests to separate project as they cause dependency co…
Fresa Mar 12, 2026
f5a8772
include missing dependencies
Fresa Mar 12, 2026
e49bc77
ignore casing when looking for leaf schema nodes
Fresa Mar 12, 2026
f573467
add missing implicit dependency nodatime
Fresa Mar 12, 2026
dc04385
add validation method for sequential json enumerators
Fresa Mar 12, 2026
c78091e
fix(schema): request content schema render duplicates due to not reso…
Fresa Mar 12, 2026
a8e2d17
test(jsonl): add integration test for jsonl
Fresa Mar 13, 2026
d0ddd59
add cancellation token support for the sequential media type enumerators
Fresa Mar 13, 2026
b0c6157
bump item position when an item has been read
Fresa Mar 13, 2026
2fb8778
advanceto the consumed data which need to include the delimiter
Fresa Mar 13, 2026
2f52dac
handle sequential media types that doesn't require the sequence to st…
Fresa Mar 14, 2026
83e0b96
test(request): assert all sequential json objects was read
Fresa Mar 14, 2026
c963f97
refactor sequential media type generator to produce an enumerable ins…
Fresa Mar 15, 2026
5af450e
return validation context directly from the sequential media type
Fresa Mar 16, 2026
719dbc1
expose private method on operations that can be called to create a re…
Fresa Mar 18, 2026
fffe40d
refactor(response): let response implementations handle validation an…
Fresa Mar 18, 2026
0c6378c
write response content last to make sure metadata and headers are inc…
Fresa Mar 18, 2026
77b640b
refactor(response): simplify writing to response
Fresa Mar 18, 2026
7fa3e5e
expose method for creating validation context for response objects
Fresa Mar 19, 2026
d57949f
create validation context should be private if the response class is …
Fresa Mar 19, 2026
234fe00
fix captured local parameter
Fresa Mar 23, 2026
b24dcb1
feat(response): support sequential media types jsonl and json-seq
Fresa Mar 23, 2026
2656edb
test(response): json-seq sequential media type
Fresa Mar 23, 2026
9b37673
use consistent naming convention for all request sequential media types
Fresa Mar 24, 2026
25ebd95
doc: describe sequential media types in README
Fresa Mar 24, 2026
de03572
fix(response): content class name could be mixed pascal case where it…
Fresa Mar 24, 2026
be795d6
explicitly implement all supported sequential media type classes usin…
Fresa Mar 24, 2026
920a813
doc: add missing comments on sequential media type writer base class
Fresa Mar 24, 2026
d8cc58d
add + to default delimiters for converting a string to camel and pasc…
Fresa Mar 24, 2026
620ff8a
fix: json-seq enumerable detect record separator incorrectly
Fresa Mar 24, 2026
7bf32cd
test(request): send different sequential media types
Fresa Mar 24, 2026
0425f7b
test(request): let last character always be a new line as supported b…
Fresa Mar 24, 2026
985fb9b
test(response): verify all supported sequential media types
Fresa Mar 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions OpenAPI.WebApiGenerator.sln
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example.OpenApi", "tests\Example.OpenApi\Example.OpenApi.csproj", "{4E274740-E49C-4E56-9B69-C33D9409C119}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenAPI.WebApiGenerator.UnitTests", "tests\OpenAPI.WebApiGenerator.UnitTests\OpenAPI.WebApiGenerator.UnitTests.csproj", "{2CED6DCB-B934-438D-98F0-21C784B353EB}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -188,6 +192,18 @@ Global
{4E274740-E49C-4E56-9B69-C33D9409C119}.Release|x64.Build.0 = Release|Any CPU
{4E274740-E49C-4E56-9B69-C33D9409C119}.Release|x86.ActiveCfg = Release|Any CPU
{4E274740-E49C-4E56-9B69-C33D9409C119}.Release|x86.Build.0 = Release|Any CPU
{2CED6DCB-B934-438D-98F0-21C784B353EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2CED6DCB-B934-438D-98F0-21C784B353EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2CED6DCB-B934-438D-98F0-21C784B353EB}.Debug|x64.ActiveCfg = Debug|Any CPU
{2CED6DCB-B934-438D-98F0-21C784B353EB}.Debug|x64.Build.0 = Debug|Any CPU
{2CED6DCB-B934-438D-98F0-21C784B353EB}.Debug|x86.ActiveCfg = Debug|Any CPU
{2CED6DCB-B934-438D-98F0-21C784B353EB}.Debug|x86.Build.0 = Debug|Any CPU
{2CED6DCB-B934-438D-98F0-21C784B353EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2CED6DCB-B934-438D-98F0-21C784B353EB}.Release|Any CPU.Build.0 = Release|Any CPU
{2CED6DCB-B934-438D-98F0-21C784B353EB}.Release|x64.ActiveCfg = Release|Any CPU
{2CED6DCB-B934-438D-98F0-21C784B353EB}.Release|x64.Build.0 = Release|Any CPU
{2CED6DCB-B934-438D-98F0-21C784B353EB}.Release|x86.ActiveCfg = Release|Any CPU
{2CED6DCB-B934-438D-98F0-21C784B353EB}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,26 @@ switch (request.TryMatchAcceptMediaType<Response.OK200>(out var matchedMediaType
}
```

## Sequential Media Types
OpenAPI 3.2 added support for [sequential media types](https://spec.openapis.org/oas/v3.2.0.html#sequential-media-types). The following sequential media types are supported for both request and response media content:
- application/jsonl
- application/x-ndjson
- application/x-jsonlines
- application/json-seq
- application/geo+json-seq

Other sequential media types can be implemented by simply following the expected naming convention and placing the implementations in the expected namespace, see the compilation error of any missing media type class.

### Request Content
Inherit from `SequentialJsonEnumerable<T>` using the following naming convention:
- application/jsonl (lower case) -> `ApplicationJsonlEnumerable<T>`

### Response Content
Inherit from `SequentialJsonWriter<T>` using the following naming convention:
- application/jsonl (lower case) -> `ApplicationJsonlWriter<T>`

See the [OpenAPI 3.2 examples](#examples) for further details how to consume and produce sequential media types.

## Authentication and Authorization
OpenAPI defines [security scheme objects](https://spec.openapis.org/oas/latest#security-scheme-object) for authentication and authorization mechanisms. The generator implement endpoint filters that corresponds to the security declaration of each operation. Do _not_ call `UseAuthentication` or similar when configuring the application.

Expand Down
20 changes: 11 additions & 9 deletions src/OpenAPI.WebApiGenerator/ApiGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,10 @@ private static void GenerateCode(SourceProductionContext context,

var validationExtensionsGenerator = new ValidationExtensionsGenerator(rootNamespace);
validationExtensionsGenerator.GenerateClass().AddTo(context);
var sequentialJsonEnumeratorsGenerator = new SequentialMediaTypesGenerator(rootNamespace);
sequentialJsonEnumeratorsGenerator.GenerateClasses().AddTo(context);

var operations = new List<(string Namespace, KeyValuePair<HttpMethod, OpenApiOperation> Operation)>();
var securityParameterGenerators = new ConcurrentDictionary<IOpenApiSecurityScheme, List<ParameterGenerator>>();
foreach (var path in openApi.Paths)
{
var pathExpression = path.Key;
Expand Down Expand Up @@ -137,9 +138,10 @@ private static void GenerateCode(SourceProductionContext context,
var schemaReference = openApiOperationVisitor.GetSchemaReference(mediaType);
var typeDeclaration = schemaGenerator.Generate(schemaReference);
return new RequestBodyContentGenerator(
pair.Key,
pair,
typeDeclaration,
httpRequestExtensionsGenerator);
httpRequestExtensionsGenerator,
sequentialJsonEnumeratorsGenerator);
}).ToList();
requestBodyGenerator = new RequestBodyGenerator(
body,
Expand All @@ -164,14 +166,14 @@ private static void GenerateCode(SourceProductionContext context,
var responseContent =
// OpenAPI.NET is incorrectly adding content where there is none defined.
// No content definition means NO content.
response.Content?.Where(content =>
openApiResponseVisitor.HasContent(content.Value)) ?? [];
var responseBodyGenerators = responseContent.Select(valuePair =>
response.Content?.Where(responseContent =>
openApiResponseVisitor.HasContent(responseContent.Value)) ?? [];
var responseBodyGenerators = responseContent.Select(mediaContent =>
{
var content = valuePair.Value;
var contentSchemaReference = openApiResponseVisitor.GetSchemaReference(content);
var contentMediaType = mediaContent.Value;
var contentSchemaReference = openApiResponseVisitor.GetSchemaReference(contentMediaType);
var typeDeclaration = schemaGenerator.Generate(contentSchemaReference);
return new ResponseBodyContentGenerator(valuePair.Key, typeDeclaration);
return new ResponseBodyContentGenerator(mediaContent, typeDeclaration);
}).ToList();

var responseHeaderGenerators = response.Headers?.Select(valuePair =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ internal abstract class BaseSecurityRequirementsFilter(WebApiConfiguration confi
break;
}

authorized &= ClaimContainsScopes(authenticateResult.Principal, configuration.SecuritySchemeOptions.GetScopeOptions(scheme), scopes);
authorized &= ClaimContainsScopes(authenticateResult.Principal, Configuration.SecuritySchemeOptions.GetScopeOptions(scheme), scopes);
if (!authorized)
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,18 @@ internal partial class Operation
private Func<Request, ImmutableList<ValidationResult>, Response> HandleRequestValidationError { get; } = (_, validationResult) =>
{{jsonValidationExceptionGenerator.CreateThrowJsonValidationExceptionInvocation("Request is not valid", "validationResult")}};

/// <summary>
/// Create a request validation error response.
/// <exception cref="JsonValidationException"></exception>
/// <param name="request">The invalid request</param>
/// <param name="ValidationContext">The validation context describing the validation errors</param>
/// </summary>
private Response CreateRequestValidationErrorResponse(Request request, ValidationContext validationContext)
{
var configuration = request.HttpContext.RequestServices.GetRequiredService<WebApiConfiguration>();
return HandleRequestValidationError(request, validationContext.Results.WithLocation(configuration.OpenApiSpecificationUri));
}

{{authGenerator.GenerateAuthFilters(operation.Operation, parameters, out var requiresAuth).Indent(4)}}
{{(requiresAuth ?
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -1,39 +1,51 @@
using System.Net.Http.Headers;
using System.Collections.Generic;
using System.Net.Http.Headers;
using Corvus.Json.CodeGeneration;
using Corvus.Json.CodeGeneration.CSharp;
using Microsoft.OpenApi;
using OpenAPI.WebApiGenerator.Extensions;

namespace OpenAPI.WebApiGenerator.CodeGeneration;

internal sealed class RequestBodyContentGenerator(
string contentType,
KeyValuePair<string, IOpenApiMediaType> contentMediaType,
TypeDeclaration typeDeclaration,
HttpRequestExtensionsGenerator httpRequestExtensionsGenerator)
HttpRequestExtensionsGenerator httpRequestExtensionsGenerator,
SequentialMediaTypesGenerator sequentialMediaTypesGenerator)
{
private string FullyQualifiedTypeName =>
$"{FullyQualifiedTypeDeclarationIdentifier}?";

private string FullyQualifiedTypeDeclarationIdentifier => typeDeclaration.FullyQualifiedDotnetTypeName();

internal string PropertyName { get; } = contentType.ToPascalCase();

internal MediaTypeWithQualityHeaderValue ContentType { get; } = MediaTypeWithQualityHeaderValue.Parse(contentType);
private readonly bool _isSequentialMediaType = contentMediaType.Value.ItemSchema != null;

internal string PropertyName { get; } = contentMediaType.Key.ToPascalCase();
internal bool IsPropertyStruct => !_isSequentialMediaType;

internal MediaTypeWithQualityHeaderValue ContentType { get; } = MediaTypeWithQualityHeaderValue.Parse(contentMediaType.Key);

internal string SchemaLocation => typeDeclaration.RelativeSchemaLocation;
internal string GenerateRequestBindingDirective() =>
$"""
{PropertyName} =
({httpRequestExtensionsGenerator.CreateBindBodyInvocation(
"request",
FullyQualifiedTypeDeclarationIdentifier)
.Indent(8).Trim()})
{PropertyName} = {(_isSequentialMediaType ?
$"{sequentialMediaTypesGenerator.GenerateConstructorInstance(
ContentType,
typeDeclaration,
"request.Body")}" :
$"({httpRequestExtensionsGenerator.CreateBindBodyInvocation(
"request",
FullyQualifiedTypeDeclarationIdentifier).Indent(8).Trim()})")}
""";


public string GenerateRequestProperty() =>
$$"""
/// <summary>
/// Request content for {{contentType}}
/// </summary>
internal {{FullyQualifiedTypeName}} {{PropertyName}} { get; private set; }
""";
public string GenerateRequestProperty()
{
var fullyQualifiedTypeName = _isSequentialMediaType
? sequentialMediaTypesGenerator.GetFullyQualifiedTypeName(ContentType, typeDeclaration)
: FullyQualifiedTypeDeclarationIdentifier;
return
$$"""
/// <summary>
/// Request content for {{contentMediaType.Key}}
/// </summary>
internal {{fullyQualifiedTypeName}}? {{PropertyName}} { get; private set; }
""";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ internal ValidationContext Validate(ValidationContext validationContext, Validat
{{{_contentGenerators.AggregateToString(content =>
$"""
true when {content.PropertyName} is not null =>
{content.PropertyName}!.Value.Validate("{content.SchemaLocation}", true, validationContext, validationLevel),
{content.PropertyName}!.{(content.IsPropertyStruct ? "Value." : "")}Validate("{content.SchemaLocation}", true, validationContext, validationLevel),
""")}}
true when requestContentType is null =>
{{(_body.Required ? """validationContext.WithResult(false, "Request content is missing")""" : "validationContext")}},
Expand Down
Loading
Loading