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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -350,3 +350,5 @@ MigrationBackup/
.ionide/

/src/TinyHelpers/TinyHelpers.xml
/src/TinyHelpers.AspNetCore/TinyHelpers.AspNetCore.xml
/src/TinyHelpers.AspNetCore.Swashbuckle/TinyHelpers.AspNetCore.Swashbuckle.xml
33 changes: 17 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
[![NuGet](https://img.shields.io/nuget/dt/TinyHelpers)](https://www.nuget.org/packages/TinyHelpers)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/marcominerva/TinyHelpers/blob/master/LICENSE)

TinyHelpers is a small .NET utility library that groups common helpers into a single package. It is designed to reduce repeated boilerplate in application code while keeping each helper focused and easy to discover.
TinyHelpers is a small .NET utility library that groups common helpers into a single package. It is designed to reduce repeated boilerplate in application code while keeping each helper focused, discoverable, and explicit about the contract it supports.

## Compatibility

Expand Down Expand Up @@ -48,20 +48,21 @@ Additional documentation is available for these packages:
- [HTTP helpers](#http-helpers)
- [JSON serialization](#json-serialization)
- [Threading](#threading)
- [Compatibility helpers](#compatibility-helpers)
- [Quick examples](#quick-examples)

## Collections and sequences

The collection helpers keep common null-safety, indexing, asynchronous projection, and conditional filtering patterns reusable. They are intended for code that repeatedly composes LINQ queries or optional collections and should avoid scattering small guard clauses across the codebase.

### `CollectionExtensions`

| Method | What it does | When to use it |
| --- | --- | --- |
| `EmptyIfNull()` | Returns an empty sequence when the source is `null`. | When you want to avoid null checks before enumeration. |
| `ForEach(Action<T>)` | Executes an action for each item in a sequence. | When you want a simple side-effect loop. |
| `ForEachAsync(Func<T, Task>)` | Executes an asynchronous action for each item. | When each item requires asynchronous work. |
| `SelectAsync(Func<T, Task<TResult>>)` | Projects each item asynchronously. | When the projection itself is asynchronous. |
| `ToListAsync()` | Materializes an asynchronous sequence into a list. | When you need a list from an async source. |
| `ForEach(Action<T>)` | Executes an action for each item and returns the original sequence. | When a side-effect step should remain in a fluent pipeline. |
| `ForEachAsync(Func<T, Task>)` | Executes an asynchronous action for each item and returns the original sequence after all actions complete. | When each item requires asynchronous work but the original values are still needed. |
| `SelectAsync(Func<T, Task<TResult>>)` | Projects each item asynchronously while preserving source order in the materialized result. | When the projection itself is asynchronous. |
| `ToListAsync()` | Materializes an asynchronous sequence into an in-memory sequence. | When later code needs synchronous enumeration from an async source. |
| `Remove(predicate)` | Removes matching items from a collection. | When you want to filter in place. |
| `IsEmpty()` / `IsNotEmpty()` | Checks whether a collection contains items. | When you want readable emptiness checks. |
| `IsNullOrEmpty()` / `IsNotNullOrEmpty()` | Checks for `null` or empty sequences. | When the source may be missing entirely. |
Expand Down Expand Up @@ -149,8 +150,8 @@ var insideRange = 5.IsBetween(1, 10);

| Method | What it does |
| --- | --- |
| `ToDateOnly()` | Converts a `DateTimeOffset` to `DateOnly`. |
| `ToTimeOnly()` | Converts a `DateTimeOffset` to `TimeOnly`. |
| `ToDateOnly(TimeZoneInfo? zone = null)` | Converts a `DateTimeOffset` to `DateOnly` after applying the specified time zone, or UTC when no zone is provided. |
| `ToTimeOnly(TimeZoneInfo? zone = null)` | Converts a `DateTimeOffset` to `TimeOnly` after applying the specified time zone, or UTC when no zone is provided. |

### `DateOnlyExtensions`

Expand Down Expand Up @@ -195,7 +196,7 @@ var timeOnly = createdAt.ToTimeOnly();
| `IsEmpty()` | Checks whether a GUID is `Guid.Empty`. | When validating identifiers. |
| `IsNotEmpty()` | Checks whether a GUID is not empty. | When you need a positive validation. |
| `HasValue()` | Checks whether a GUID is different from `null` and `Guid.Empty`. | When handling optional identifiers. |
| `GetValueOrCreateNew()` | Returns the existing GUID when it is different from `null` and `Guid.Empty`; otherwise, it creates a new one automatically. On .NET 9+, you can also specify the GUID version to generate. | When you want to assign an identifier lazily. |
| `GetValueOrCreateNew()` | Returns the existing GUID when it is different from `null` and `Guid.Empty`; otherwise, it creates a new one automatically. On .NET 9+, you can also specify the GUID version to generate. | When you want to assign an identifier lazily at application boundaries, persistence boundaries, or outbound messages. |
| `GetValueOrDefault()` | Returns the current GUID when it is different from `Guid.Empty`; otherwise, it returns a default value. | When null-safe defaults are useful. |

### Example
Expand All @@ -215,7 +216,7 @@ var defaultId = id.GetValueOrDefault();

Adds an access token to outgoing requests through a delegate that can inspect the current `HttpRequestMessage`. If a request returns `401 Unauthorized` and a refresh delegate is configured, the handler can refresh the token and retry the request once. The authorization scheme is configurable and defaults to `Bearer`.

Use this handler when the token depends on the outgoing request or when you need a lightweight refresh flow around a protected API.
Use this handler when token acquisition depends on the outgoing request, such as per-tenant or per-resource tokens, or when you need a lightweight refresh flow around a protected API.

### Example

Expand All @@ -232,7 +233,7 @@ var client = new HttpClient(handler);

Adds headers to outgoing requests by invoking a delegate that returns a dictionary of header names and values. Each returned header is applied with `TryAddWithoutValidation`, so the handler is suitable for custom or preformatted header values.

Use this handler when headers such as correlation IDs, tenant identifiers, or custom API metadata need to be injected per request.
Use this handler when headers such as correlation IDs, tenant identifiers, or custom API metadata need to be injected per request without repeating header setup at every call site.

### Example

Expand All @@ -251,7 +252,7 @@ var client = new HttpClient(handler);

Adds query string parameters to outgoing requests by merging the values returned by a delegate into the current request URI. Existing query string values are preserved, and new values are appended to the request URL.

Use this handler when the query string depends on the current request context and you want to centralize URL composition.
Use this handler when the query string depends on the current request context and you want to centralize URL composition for pagination, tenant selection, feature flags, or similar cross-cutting values.

### Example

Expand All @@ -273,19 +274,19 @@ var client = new HttpClient(handler)

### `ShortDateConverter`

Serializes a `DateTime` using only the date portion.
Serializes and deserializes a `DateTime` using only the date portion when time-of-day information is not part of the JSON contract.

### `UtcDateTimeConverter`

Serializes and deserializes `DateTime` values in UTC format.
Serializes and deserializes `DateTime` values in UTC format so the JSON boundary normalizes date-time values instead of preserving local offsets or unspecified kinds.

### `TimeSpanTicksConverter`

Serializes a `TimeSpan` as ticks.
Serializes and deserializes a `TimeSpan` as ticks so durations round-trip without string-format ambiguity.

### `StringTrimmingConverter`

Trims whitespace from JSON strings during read and write operations.
Trims leading and trailing whitespace from JSON strings during read and write operations when the JSON boundary should normalize user-entered text.

### `StringEnumMemberConverter`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
namespace TinyHelpers.AspNetCore.Swagger.Filters;

/// <summary>
/// Describes additional OpenAPI parameters that should be attached to every operation.
/// Collects reusable Swagger operation metadata that should be applied consistently across multiple endpoints.
/// </summary>
/// <remarks>
/// Instances are created through dependency injection and consumed by
/// <see cref="OpenApiParametersOperationFilter" /> to avoid duplicating parameter definitions in
/// multiple Swagger configuration points.
/// The library uses this options object to register shared parameter definitions once and copy them into the
/// generated OpenAPI document wherever they are needed. This keeps endpoint setup centralized and avoids drift between
/// route metadata and the generated client contract.
/// </remarks>
public class OpenApiOperationOptions
{
Expand All @@ -20,7 +20,11 @@ internal OpenApiOperationOptions()
}

/// <summary>
/// Gets the parameters that should be appended to generated OpenAPI operations.
/// Gets the parameter definitions that should be merged into generated Swagger operations.
/// </summary>
/// <remarks>
/// Parameters are intentionally stored here rather than declared inline on each route so callers can reuse metadata
/// for cross-cutting inputs such as headers or query values while keeping the generated contract consistent.
/// </remarks>
public IList<OpenApiParameter> Parameters { get; } = [];
}
61 changes: 33 additions & 28 deletions src/TinyHelpers.AspNetCore.Swashbuckle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

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.
duplicating setup code across startup files, endpoint metadata, or custom filters.

## Compatibility

Expand Down Expand Up @@ -43,27 +43,19 @@ Or search for `TinyHelpers.AspNetCore.Swashbuckle` in the Visual Studio Package

| 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. |
| `AddAcceptLanguageHeader()` | Adds the `Accept-Language` header to documented operations when the app has supported cultures. | When your API uses request localization and consumers need to discover supported culture values. |
| `AddDefaultProblemDetailsResponse()` | Adds a default `application/problem+json` response to generated 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 the schema to show the wire format clearly. |
| `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. |
| `AddSwaggerOperationParameters(Action<OpenApiOperationOptions> setupAction)` | Registers reusable Swagger operation parameters in the dependency injection container. | When cross-cutting headers or query values must be declared once and reused during Swagger generation. |
| `AddOperationParameters()` | Adds the operation filter that copies registered shared parameters into generated Swagger operations. | When you want the generated contract to include the parameters registered with `AddSwaggerOperationParameters()`. |

### Example

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

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

builder.Services.AddSwaggerOperationParameters(parameters =>
{
parameters.Parameters.Add(new OpenApiParameter
Expand All @@ -74,22 +66,35 @@ builder.Services.AddSwaggerOperationParameters(parameters =>
Description = "Identifier used to correlate requests"
});
});

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

Register shared parameters with `AddSwaggerOperationParameters()` during service registration, then enable
`AddOperationParameters()` in `AddSwaggerGen(...)`. The options object stores the shared parameter definitions, and the
operation filter copies them into generated operations. This prevents duplicated endpoint metadata while preserving a
complete contract for Swagger UI and generated clients.

## Schema helpers

### `OpenApiSchemaHelper`

This helper provides ready-to-use schema fragments that can be reused when building custom Swagger filters or other
OpenAPI customizations.
OpenAPI customizations. It keeps default values, formats, and enum choices consistent across generated documents.

| 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. |
| `CreateStringSchema(string? defaultValue = null)` | Creates a reusable string schema with an optional default value. | When you want text-based contract metadata without rebuilding the same schema each time. |
| `CreateSchema<TValue>(JsonSchemaType type, string? format = null)` | Creates a primitive schema with explicit OpenAPI type and format metadata. | When a filter needs to describe a primitive OpenAPI shape consistently. |
| `CreateSchema<TValue>(JsonSchemaType type, string? format, TValue? defaultValue = null)` | Creates a primitive schema and includes the default value that clients should display or assume. | When you want to document both the value shape and its fallback. |
| `CreateSchema(IEnumerable<string> values, string? defaultValue = null)` | Creates a string schema with an enumeration of externally defined allowed values. | When the field is constrained to a fixed set of strings that is not represented by a CLR enum. |
| `CreateSchema<TEnum>(TEnum? defaultValue = null)` | Creates a string schema from an enum type so every declared value is documented. | When you want CLR enum names to appear as OpenAPI values. |

### Example

Expand Down Expand Up @@ -124,14 +129,6 @@ 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
Expand All @@ -143,6 +140,14 @@ builder.Services.AddSwaggerOperationParameters(parameters =>
});
});

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

var app = builder.Build();

app.UseSwagger();
Expand Down
19 changes: 16 additions & 3 deletions src/TinyHelpers.AspNetCore.Swashbuckle/SwaggerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,32 @@ public void AddTimeSpanTypeMapping(string? example)
}

/// <summary>
/// Adds shared OpenAPI parameter definitions so they are automatically applied to every generated operation.
/// Adds the operation filter that copies registered shared parameters into each generated Swagger operation.
/// </summary>
/// <remarks>
/// Use this in Swagger configuration after registering parameters with
/// <see cref="AddSwaggerOperationParameters(IServiceCollection, Action{OpenApiOperationOptions})" />. Keeping
/// parameter definitions in dependency injection and applying them through a filter prevents duplicated route
/// metadata while preserving a complete contract for generated clients.
/// </remarks>
/// <seealso cref="AddSwaggerOperationParameters(IServiceCollection, Action{OpenApiOperationOptions})"/>
public void AddOperationParameters()
=> options.OperationFilter<OpenApiParametersOperationFilter>();
}

/// <summary>
/// Registers OpenAPI parameter definitions that can be automatically applied to every operation.
/// Registers shared Swagger operation parameters that can later be merged into generated operations.
/// </summary>
/// <param name="services">The service collection to extend.</param>
/// <param name="setupAction">The configuration callback used to populate shared parameters.</param>
/// <param name="setupAction">
/// A callback that adds reusable parameter definitions to an <see cref="OpenApiOperationOptions" /> instance.
/// </param>
/// <returns>The same <see cref="IServiceCollection" /> instance so calls can be chained.</returns>
/// <remarks>
/// Call this during service registration when a parameter, such as a tenant, correlation, or feature header,
/// must appear consistently in the Swagger contract without being repeated on every endpoint. The registered
/// options are consumed by <see cref="AddOperationParameters(SwaggerGenOptions)" /> when the document is generated.
/// </remarks>
/// <seealso cref="AddOperationParameters(SwaggerGenOptions)"/>
public static IServiceCollection AddSwaggerOperationParameters(this IServiceCollection services, Action<OpenApiOperationOptions> setupAction)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>TinyHelpers.AspNetCore.Swagger</RootNamespace>
<DocumentationFile>TinyHelpers.AspNetCore.Swashbuckle.xml</DocumentationFile>
<Authors>Marco Minerva</Authors>
<Company>Marco Minerva</Company>
<Product>Tiny Helpers for Swashbuckle ASP.NET Core</Product>
Expand All @@ -21,6 +22,10 @@
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>

<ItemGroup>
<None Remove="TinyHelpers.AspNetCore.Swashbuckle.xml" />
</ItemGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
Expand Down
Loading
Loading