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
6 changes: 3 additions & 3 deletions samples/TinyHelpers.EntityFrameworkCore.Sample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@

//await dataContext.SaveChangesAsync();

//await dataContext.ExecuteTransactionAsync(async () =>
//await dataContext.ExecuteTransactionAsync(async (cancellationToken) =>
//{
// var posts = await dataContext.Posts.ToListAsync();
// var posts = await dataContext.Posts.ToListAsync(cancellationToken);
// var post = posts.First();
// post.Reviews.First().User = "Topolino";
// await dataContext.SaveChangesAsync();
// await dataContext.SaveChangesAsync(cancellationToken);
//});

// Exception
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.9" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@

namespace TinyHelpers.AspNetCore.Swagger.Filters;

/// <summary>
/// Adds an <c>Accept-Language</c> header parameter to an OpenAPI operation when the application
/// exposes a fixed set of supported cultures.
/// </summary>
/// <remarks>
/// This keeps the generated Swagger document aligned with <see cref="RequestLocalizationOptions" />.
/// </remarks>
internal class AcceptLanguageHeaderOperationFilter(IOptions<RequestLocalizationOptions> requestLocalizationOptions) : IOperationFilter
{
private readonly List<JsonNode>? supportedLanguages = requestLocalizationOptions.Value
Expand All @@ -16,6 +23,7 @@ internal class AcceptLanguageHeaderOperationFilter(IOptions<RequestLocalizationO

private readonly JsonNode defaultLanguage = JsonValue.Create(requestLocalizationOptions.Value.DefaultRequestCulture.Culture.Name);

/// <inheritdoc />
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
if (supportedLanguages?.Count > 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@

namespace TinyHelpers.AspNetCore.Swagger.Filters;

/// <summary>
/// Adds a default <c>application/problem+json</c> response to an OpenAPI operation.
/// </summary>
/// <remarks>
/// The filter ensures <see cref="ProblemDetails" /> is present in the schema repository so error
/// responses are documented consistently across endpoints.
/// </remarks>
internal class DefaultResponseOperationFilter : IOperationFilter
{
/// <inheritdoc />
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
// Ensure ProblemDetails schema is generated.
context.SchemaGenerator.GenerateSchema(typeof(ProblemDetails), context.SchemaRepository);

operation.Responses ??= [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,25 @@

namespace TinyHelpers.AspNetCore.Swagger.Filters;

/// <summary>
/// Describes additional OpenAPI parameters that should be attached to every operation.
/// </summary>
/// <remarks>
/// Instances are created through dependency injection and consumed by
/// <see cref="OpenApiParametersOperationFilter" /> to avoid duplicating parameter definitions in
/// multiple Swagger configuration points.
/// </remarks>
public class OpenApiOperationOptions
{
/// <summary>
/// Initializes a new instance of the <see cref="OpenApiOperationOptions" /> class.
/// </summary>
internal OpenApiOperationOptions()
{
}

/// <summary>
/// Gets the parameters that should be appended to generated OpenAPI operations.
/// </summary>
public IList<OpenApiParameter> Parameters { get; } = [];
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,16 @@

namespace TinyHelpers.AspNetCore.Swagger.Filters;

/// <summary>
/// Adds application-defined OpenAPI parameters to each generated operation.
/// </summary>
/// <remarks>
/// The filter centralizes shared parameters, such as custom headers or query strings, so Swagger
/// output stays consistent without repeating the same definitions on every controller action.
/// </remarks>
internal class OpenApiParametersOperationFilter(OpenApiOperationOptions options) : IOperationFilter
{
/// <inheritdoc />
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
if (options.Parameters.Count > 0)
Expand Down
44 changes: 40 additions & 4 deletions src/TinyHelpers.AspNetCore.Swashbuckle/OpenApiSchemaHelper.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,37 @@
using System.Text.Json.Nodes;
using System.Globalization;
using System.Text.Json.Nodes;
using Microsoft.OpenApi;

namespace TinyHelpers.AspNetCore.Swagger;

/// <summary>
/// Provides factory helpers for building reusable <see cref="OpenApiSchema" /> instances.
/// </summary>
public static class OpenApiSchemaHelper
{
/// <summary>
/// Creates a string schema with an optional default value..
/// </summary>
/// <param name="defaultValue">The optional default value.</param>
/// <returns>A schema configured for <see cref="JsonSchemaType.String" />.</returns>
public static OpenApiSchema CreateStringSchema(string? defaultValue = null)
{
var schema = new OpenApiSchema
{
Type = JsonSchemaType.String,
Default = defaultValue is not null ? JsonValue.Create(defaultValue.ToString()) : null
Default = defaultValue is not null ? JsonValue.Create(defaultValue) : null
};

return schema;
}

/// <summary>
/// Creates a schema for a primitive OpenAPI type.
/// </summary>
/// <typeparam name="TValue">The underlying type used by the schema.</typeparam>
/// <param name="type">The OpenAPI schema type to emit.</param>
/// <param name="format">The optional OpenAPI format name.</param>
/// <returns>A schema configured with the supplied type and format.</returns>
public static OpenApiSchema CreateSchema<TValue>(JsonSchemaType type, string? format = null)
{
var schema = new OpenApiSchema
Expand All @@ -27,30 +43,50 @@ public static OpenApiSchema CreateSchema<TValue>(JsonSchemaType type, string? fo
return schema;
}

/// <summary>
/// Creates a schema for a primitive OpenAPI type with a typed default value.
/// </summary>
/// <typeparam name="TValue">The struct type used for the default value.</typeparam>
/// <param name="type">The OpenAPI schema type to emit.</param>
/// <param name="format">The optional OpenAPI format name.</param>
/// <param name="defaultValue">The default value to expose in the generated schema, if any.</param>
/// <returns>A schema configured with the supplied type, format, and default value.</returns>
public static OpenApiSchema CreateSchema<TValue>(JsonSchemaType type, string? format, TValue? defaultValue = null) where TValue : struct
{
var schema = new OpenApiSchema
{
Type = type,
Format = format,
Default = defaultValue is not null ? JsonValue.Create(defaultValue.ToString()) : null
Default = defaultValue is not null ? JsonValue.Create(Convert.ToString(defaultValue, CultureInfo.InvariantCulture)) : null
};

return schema;
}

/// <summary>
/// Creates a string schema whose allowed values are limited to the supplied set.
/// </summary>
/// <param name="values">The values to expose in the schema enumeration.</param>
/// <param name="defaultValue">The default value to expose in the generated schema, if any.</param>
/// <returns>A schema configured for <see cref="JsonSchemaType.String" /> with the specified enumeration.</returns>
public static OpenApiSchema CreateSchema(IEnumerable<string> values, string? defaultValue = null)
{
var schema = new OpenApiSchema
{
Type = JsonSchemaType.String,
Enum = values.Select(v => JsonValue.Create(v)).Cast<JsonNode>().ToList(),
Default = defaultValue is not null ? JsonValue.Create(defaultValue.ToString()) : null
Default = defaultValue is not null ? JsonValue.Create(defaultValue) : null
};

return schema;
}

/// <summary>
/// Creates a string schema for an enum type by exposing all enum names as OpenAPI values.
/// </summary>
/// <typeparam name="TEnum">The enum type to represent.</typeparam>
/// <param name="defaultValue">The default enum value to expose in the generated schema, if any.</param>
/// <returns>A schema configured for <see cref="JsonSchemaType.String" /> with all enum members.</returns>
public static OpenApiSchema CreateSchema<TEnum>(TEnum? defaultValue = null) where TEnum : struct, Enum
{
var schema = new OpenApiSchema
Expand Down
148 changes: 135 additions & 13 deletions src/TinyHelpers.AspNetCore.Swashbuckle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,166 @@
[![Lint Code Base](https://github.com/marcominerva/TinyHelpers/actions/workflows/linter.yml/badge.svg)](https://github.com/marcominerva/TinyHelpers/actions/workflows/linter.yml)
[![CodeQL](https://github.com/marcominerva/TinyHelpers/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/marcominerva/TinyHelpers/actions/workflows/github-code-scanning/codeql)
[![NuGet](https://img.shields.io/nuget/v/TinyHelpers.AspNetCore.Swashbuckle.svg?style=flat-square)](https://www.nuget.org/packages/TinyHelpers.AspNetCore.Swashbuckle)
[![Nuget](https://img.shields.io/nuget/dt/TinyHelpers.AspNetCore.Swashbuckle)](https://www.nuget.org/packages/TinyHelpers.AspNetCore.Swashbuckle)
[![NuGet](https://img.shields.io/nuget/dt/TinyHelpers.AspNetCore.Swashbuckle)](https://www.nuget.org/packages/TinyHelpers.AspNetCore.Swashbuckle)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/marcominerva/TinyHelpers/blob/master/LICENSE)

A collection of helper methods and classes for Swashbuckle ASP.NET Core that I use every day. I have packed them in a single library to avoid code duplication.
TinyHelpers.AspNetCore.Swashbuckle is a small collection of practical helpers for Swashbuckle ASP.NET Core applications.
It keeps common Swagger configuration in one place so applications can reuse the same OpenAPI conventions without
duplicating setup code across startup files.

**Installation**
## Compatibility

The library is available on [NuGet](https://www.nuget.org/packages/TinyHelpers.AspNetCore.Swashbuckle). Just search for *TinyHelpers.AspNetCore* in the **Package Manager GUI** or run the following command in the **.NET CLI**:
The package targets:

- .NET 8
- .NET 9
- .NET 10

It is designed to be used with [Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore)
and `AddSwaggerGen(...)`.

## Installation

Install the package from NuGet:

```shell
dotnet add package TinyHelpers.AspNetCore.Swashbuckle
```

**Usage**
Or search for `TinyHelpers.AspNetCore.Swashbuckle` in the Visual Studio Package Manager.

## Contents

- [Swagger and OpenAPI helpers](#swagger-and-openapi-helpers)
- [Schema helpers](#schema-helpers)
- [Quick examples](#quick-examples)

The library provides some useful extension methods for Swashbuckle ASP.NET Core:
## Swagger and OpenAPI helpers

- `AddAcceptLanguageHeader`: an extension method for the Swagger implementation provided by [Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore). It adds the _Accept-Language_ header to **swagger.json** definition.
### `SwaggerExtensions`

| Method | What it does | When to use it |
| --- | --- | --- |
| `AddAcceptLanguageHeader()` | Adds the `Accept-Language` header to documented operations when the app has supported cultures. | When your API uses request localization and you want consumers to discover the supported culture values. |
| `AddDefaultProblemDetailsResponse()` | Adds a default `application/problem+json` response to operations. | When you want a consistent error contract in Swagger UI and generated documents. |
| `AddTimeSpanTypeMapping(bool useCurrentTimeAsExample = false)` | Maps `TimeSpan` to a string schema and optionally adds a readable example. | When your API exposes `TimeSpan` values and you want a clearer schema. |
| `AddTimeSpanTypeMapping(string? example)` | Maps `TimeSpan` to a string schema using a custom example value. | When you want a precise sample that matches your API format. |
| `AddSwaggerOperationParameters(Action<OpenApiOperationOptions> setupAction)` | Registers reusable OpenAPI parameters in the dependency injection container. | When you want to define shared parameters once and reuse them in Swagger generation. |
| `AddOperationParameters()` | Adds the parameters configured through `OpenApiOperationOptions`. | When you want the shared parameters to appear in the generated Swagger operations. |

### Example

```csharp
using Microsoft.OpenApi;
using TinyHelpers.AspNetCore.Swagger;
Comment thread
Copilot marked this conversation as resolved.

builder.Services.AddSwaggerGen(options =>
{
options.AddAcceptLanguageHeader();
options.AddDefaultProblemDetailsResponse();
options.AddTimeSpanTypeMapping(useCurrentTimeAsExample: true);
options.AddOperationParameters();
});

builder.Services.AddSwaggerOperationParameters(parameters =>
{
parameters.Parameters.Add(new OpenApiParameter
{
Name = "X-Correlation-Id",
In = ParameterLocation.Header,
Required = false,
Description = "Identifier used to correlate requests"
});
});
```

- `AddDefaultProblemDetailsResponse()`: adds a default (error) response to all endpoints in the OpenAPI definition:
## Schema helpers

### `OpenApiSchemaHelper`

This helper provides ready-to-use schema fragments that can be reused when building custom Swagger filters or other
OpenAPI customizations.

| Method | What it does | When to use it |
| --- | --- | --- |
| `CreateStringSchema(string? defaultValue = null)` | Creates a string schema with an optional default value. | When you want a simple reusable string schema. |
| `CreateSchema<TValue>(JsonSchemaType type, string? format = null)` | Creates a schema with an explicit OpenAPI type and format. | When you want a primitive schema and prefer the typed helper overload. |
| `CreateSchema<TValue>(JsonSchemaType type, string? format, TValue? defaultValue = null)` | Creates a schema with a typed default value. | When you want to document a primitive value and its default. |
| `CreateSchema(IEnumerable<string> values, string? defaultValue = null)` | Creates a string schema with an enumeration of allowed values. | When the field is constrained to a fixed set of strings. |
| `CreateSchema<TEnum>(TEnum? defaultValue = null)` | Creates a string schema from an enum type. | When you want the enum names to appear as OpenAPI values. |

### Example

```csharp
using TinyHelpers.AspNetCore.Swagger;

var cultureSchema = OpenApiSchemaHelper.CreateSchema(["it-IT", "en-US"], "it-IT");
var durationSchema = OpenApiSchemaHelper.CreateStringSchema("00:30:00");
```

### Example with an enum

```csharp
builder.Services.AddOpenApi(options =>
public enum ExportFormat
{
Csv,
Json,
Xml
}

var schema = OpenApiSchemaHelper.CreateSchema<ExportFormat>(ExportFormat.Json);
```

## Quick examples

### Minimal API with Swagger helpers

```csharp
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
using TinyHelpers.AspNetCore.Swagger;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSwaggerGen(options =>
{
options.AddAcceptLanguageHeader();
options.AddDefaultProblemDetailsResponse();
options.AddTimeSpanTypeMapping("00:15:00");
options.AddOperationParameters();
});

builder.Services.AddSwaggerOperationParameters(parameters =>
{
parameters.Parameters.Add(new OpenApiParameter
{
Name = "X-Request-Id",
In = ParameterLocation.Header,
Required = false,
Description = "Optional request correlation identifier"
});
});

var app = builder.Build();

app.UseSwagger();
app.UseSwaggerUI();

app.Run();
```

### Building a reusable schema

```csharp
using Microsoft.OpenApi;
using TinyHelpers.AspNetCore.Swagger;

var schema = OpenApiSchemaHelper.CreateSchema<string>(JsonSchemaType.String, "uuid");
```

**Contribute**
## Contribute

The project is constantly evolving. Contributions are welcome. Feel free to file issues and pull requests on the repo and we'll address them as we can.
The project is continuously evolving. Contributions, issues, and pull requests are welcome.

> **Warning**
Remember to work on the **develop** branch, don't use the **master** branch directly. Create Pull Requests targeting **develop**.
> [!WARNING]
> Work on the **develop** branch, not on **master**. Pull requests should target **develop**.
Loading
Loading