Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 6 additions & 4 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
<Project>
<PropertyGroup>
<!-- Custom properties, which are used in the main projects -->
<OptionalValuesLibraryTargetFrameworks>net8.0;net9.0</OptionalValuesLibraryTargetFrameworks>
<OptionalValuesTestsTargetFrameworks>net8.0;net9.0</OptionalValuesTestsTargetFrameworks>
<OptionalValuesLibraryTargetFrameworks>net8.0;net9.0;net10.0</OptionalValuesLibraryTargetFrameworks>
<OptionalValuesSwashbuckleTargetFrameworks>net8.0;net9.0</OptionalValuesSwashbuckleTargetFrameworks>
<OptionalValuesOpenApiTargetFrameworks>net10.0</OptionalValuesOpenApiTargetFrameworks>
<OptionalValuesTestsTargetFrameworks>net8.0;net9.0;net10.0</OptionalValuesTestsTargetFrameworks>

<CommonPackageTags>optional partial json undefined jsonpatch jsonmergepatch patch System.Text.Json Api
unspecified</CommonPackageTags>
<CommonPackageTags>optional partial json undefined jsonpatch jsonmergepatch patch
System.Text.Json Api unspecified</CommonPackageTags>
</PropertyGroup>

<PropertyGroup>
Expand Down
4 changes: 3 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@
<!-- Package dependency versions -->
<ItemGroup>
<PackageVersion Include="FluentValidation" Version="$(FluentValidationVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
<PackageVersion Include="NJsonSchema" Version="$(NJsonSchemaVersion)" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerGen" Version="$(SwashbuckleVersion)" />
</ItemGroup>
<!-- Examples dependency versions -->
<ItemGroup>
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="8.0.11" />
<PackageVersion Include="Microsoft.Extensions.ApiDescription.Server" Version="10.0.0" />
<PackageVersion Include="NSwag.AspNetCore" Version="$(NSwagVersion)" />
<PackageVersion Include="NSwag.MSBuild" Version="$(NSwagVersion)" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="$(SwashbuckleVersion)" />
Expand All @@ -25,6 +26,7 @@
<ItemGroup>
<PackageVersion Include="BenchmarkDotNet" Version="0.15.4" />
<PackageVersion Include="DotNet.ReproducibleBuilds" Version="1.2.39" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="4.14.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
Expand Down
6 changes: 5 additions & 1 deletion OptionalValues.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@
<Folder Name="/examples/">
<Project Path="examples/OptionalValues.Examples.NSwag/OptionalValues.Examples.NSwag.csproj" />
<Project Path="examples/OptionalValues.Examples.Swashbuckle/OptionalValues.Examples.Swashbuckle.csproj" />
<Project Path="examples/OptionalValues.Examples.OpenApi/OptionalValues.Examples.OpenApi.csproj" Type="Classic C#" />
</Folder>
<Folder Name="/src/">
<Project Path="src/OptionalValues.DataAnnotations/OptionalValues.DataAnnotations.csproj" />
<Project Path="src/OptionalValues.FluentValidation/OptionalValues.FluentValidation.csproj" />
<Project Path="src/OptionalValues.NSwag/OptionalValues.NSwag.csproj" />
<Project Path="src/OptionalValues.Swashbuckle/OptionalValues.Swashbuckle.csproj" />
<Project Path="src/OptionalValues/OptionalValues.csproj" />
<Project Path="src/OptionalValues.OpenApi/OptionalValues.OpenApi.csproj" Type="Classic C#" />
</Folder>
<Folder Name="/test/">
<Project Path="test/OptionalValues.Benchmarks/OptionalValues.Benchmarks.csproj" />
Expand All @@ -27,5 +29,7 @@
<Project Path="test/OptionalValues.NSwag.Tests/OptionalValues.NSwag.Tests.csproj" />
<Project Path="test/OptionalValues.Swashbuckle.Tests/OptionalValues.Swashbuckle.Tests.csproj" />
<Project Path="test/OptionalValues.Tests/OptionalValues.Tests.csproj" />
<Project Path="test\OptionalValues.OpenApi.TestApp\OptionalValues.OpenApi.TestApp.csproj" Type="Classic C#" />
<Project Path="test/OptionalValues.OpenApi.Tests/OptionalValues.OpenApi.Tests.csproj" Type="Classic C#" />
</Folder>
</Solution>
</Solution>
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>

<IsPackable>false</IsPackable>
<IsExampleProject>true</IsExampleProject>

<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

<PropertyGroup>
<OpenApiDocumentsDirectory>.</OpenApiDocumentsDirectory>
<OpenApiGenerateDocumentsOptions>--file-name openapi</OpenApiGenerateDocumentsOptions>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
<PackageReference Include="Microsoft.Extensions.ApiDescription.Server">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\OptionalValues.DataAnnotations\OptionalValues.DataAnnotations.csproj" />
<ProjectReference Include="..\..\src\OptionalValues.OpenApi\OptionalValues.OpenApi.csproj" />
<ProjectReference Include="..\..\src\OptionalValues\OptionalValues.csproj" />
</ItemGroup>

</Project>
116 changes: 116 additions & 0 deletions examples/OptionalValues.Examples.OpenApi/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
using System.Text.Json.Serialization;

using OptionalValues;
using OptionalValues.DataAnnotations;
using OptionalValues.OpenApi;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi(options =>
{
options.AddOptionalValueSupport();
});

// Add OptionalValue support to the JSON serializer
builder.Services.ConfigureHttpJsonOptions(jsonOptions =>
{
jsonOptions.SerializerOptions.NumberHandling = JsonNumberHandling.Strict;
jsonOptions.SerializerOptions.AddOptionalValueSupport();
});

WebApplication app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}

app.UseHttpsRedirection();

// This is an example of how to use OptionalValues with NSwag
// It also shows what the JSON Serializer will do with the OptionalValues
// You can play with the values in the Company object to see how the JSON Serializer will handle them
// Play around by omitting properties or whole objects
app.MapPost("/company", (Company company) => company)
.WithName("Example Post")
.WithDescription("This directly returns the posted object")
.WithTags("Example");

app.Run();

class NullableProperty
{
[Specified]
public OptionalValue<string?> Name { get; init; }
}

/// <summary>
/// This is the main example company model.
/// </summary>
class Company
{
public Guid Id { get; init; } = Guid.NewGuid();

/// <summary>
/// Name of the company.
/// </summary>
[RequiredValue]
public OptionalValue<string> Name { get; init; }

[OptionalLength(0, 50)]
public OptionalValue<string?> Summary { get; init; }

/// <summary>
/// The contact person for the company.
/// </summary>
[Specified]
public OptionalValue<Person?> Contact { get; init; }
}

/// <summary>
/// This is a person.
/// </summary>
class Person
{
/// <summary>
/// The full name of the person.
/// </summary>
[Specified]
public OptionalValue<string?> Name { get; init; } = "John Doe";

[OptionalRange(0, 120)]
public OptionalValue<int> Age { get; init; }

/// <summary>
/// Contact address for the person.
/// </summary>
public OptionalValue<Address> Address { get; init; }

/// <summary>
/// Billing address for the person.
/// </summary>
public OptionalValue<Address?> BillingAddress { get; init; }
}

/// <summary>
/// A mailing address.
/// </summary>
class Address
{
[Specified]
public OptionalValue<string?> Street { get; init; }

public OptionalValue<string> City { get; init; }

[OptionalRegularExpression("^[a-zA-Z ]+$")]
public OptionalValue<string> State { get; init; }

public OptionalValue<string?> Zip { get; init; }
}

/// <summary>
/// Entry point for the OptionalValues.Examples.OpenApi application.
/// </summary>
public partial class Program {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7022;http://localhost:5138",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
9 changes: 9 additions & 0 deletions examples/OptionalValues.Examples.OpenApi/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
150 changes: 150 additions & 0 deletions examples/OptionalValues.Examples.OpenApi/openapi.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
{
"openapi": "3.1.1",
"info": {
"title": "OptionalValues.Examples.OpenApi | v1",
"version": "1.0.0"
},
"paths": {
"/company": {
"post": {
"tags": [
"Example"
],
"description": "This directly returns the posted object",
"operationId": "Example Post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Company"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Company"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"Address": {
"required": [
"street"
],
"type": "object",
"properties": {
"street": {
"type": [
"null",
"string"
]
},
"city": {
"type": "string"
},
"state": {
"type": "string"
},
"zip": {
"type": [
"null",
"string"
]
}
},
"description": "A mailing address."
},
"Company": {
"required": [
"name",
"contact"
],
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"name": {
"type": "string",
"description": "Name of the company."
},
"summary": {
"maxLength": 50,
"minLength": 0,
"type": [
"null",
"string"
]
},
"contact": {
"oneOf": [
{
"type": "null"
},
{
"description": "The contact person for the company.",
"$ref": "#/components/schemas/Person"
}
]
}
},
"description": "This is the main example company model."
},
"Person": {
"required": [
"name"
],
"type": "object",
"properties": {
"name": {
"type": [
"null",
"string"
],
"description": "The full name of the person."
},
"age": {
"maximum": 120,
"minimum": 0,
"type": "integer",
"format": "int32"
},
"address": {
"description": "Contact address for the person.",
"$ref": "#/components/schemas/Address"
},
"billingAddress": {
"oneOf": [
{
"type": "null"
},
{
"description": "Billing address for the person.",
"$ref": "#/components/schemas/Address"
}
]
}
},
"description": "This is a person."
}
}
},
"tags": [
{
"name": "Example"
}
]
}
Loading