diff --git a/.gitignore b/.gitignore index 0cc6cf6..33cbf6b 100644 --- a/.gitignore +++ b/.gitignore @@ -350,3 +350,5 @@ MigrationBackup/ .ionide/ /src/TinyHelpers/TinyHelpers.xml +/src/TinyHelpers.AspNetCore/TinyHelpers.AspNetCore.xml +/src/TinyHelpers.AspNetCore.Swashbuckle/TinyHelpers.AspNetCore.Swashbuckle.xml diff --git a/README.md b/README.md index e95e0ec..023f198 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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)` | Executes an action for each item in a sequence. | When you want a simple side-effect loop. | -| `ForEachAsync(Func)` | Executes an asynchronous action for each item. | When each item requires asynchronous work. | -| `SelectAsync(Func>)` | 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)` | Executes an action for each item and returns the original sequence. | When a side-effect step should remain in a fluent pipeline. | +| `ForEachAsync(Func)` | 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>)` | 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. | @@ -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` @@ -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 @@ -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 @@ -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 @@ -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 @@ -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` diff --git a/src/TinyHelpers.AspNetCore.Swashbuckle/Filters/OpenApiOperationOptions.cs b/src/TinyHelpers.AspNetCore.Swashbuckle/Filters/OpenApiOperationOptions.cs index baf5cde..f1f4cef 100644 --- a/src/TinyHelpers.AspNetCore.Swashbuckle/Filters/OpenApiOperationOptions.cs +++ b/src/TinyHelpers.AspNetCore.Swashbuckle/Filters/OpenApiOperationOptions.cs @@ -3,12 +3,12 @@ namespace TinyHelpers.AspNetCore.Swagger.Filters; /// -/// Describes additional OpenAPI parameters that should be attached to every operation. +/// Collects reusable Swagger operation metadata that should be applied consistently across multiple endpoints. /// /// -/// Instances are created through dependency injection and consumed by -/// 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. /// public class OpenApiOperationOptions { @@ -20,7 +20,11 @@ internal OpenApiOperationOptions() } /// - /// Gets the parameters that should be appended to generated OpenAPI operations. + /// Gets the parameter definitions that should be merged into generated Swagger operations. /// + /// + /// 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. + /// public IList Parameters { get; } = []; } diff --git a/src/TinyHelpers.AspNetCore.Swashbuckle/README.md b/src/TinyHelpers.AspNetCore.Swashbuckle/README.md index 097a7d8..6728708 100644 --- a/src/TinyHelpers.AspNetCore.Swashbuckle/README.md +++ b/src/TinyHelpers.AspNetCore.Swashbuckle/README.md @@ -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 @@ -43,12 +43,12 @@ 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 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 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 @@ -56,14 +56,6 @@ Or search for `TinyHelpers.AspNetCore.Swashbuckle` in the Visual Studio Package 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 @@ -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(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(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 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? 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(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(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 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? 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 @@ -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 @@ -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(); diff --git a/src/TinyHelpers.AspNetCore.Swashbuckle/SwaggerExtensions.cs b/src/TinyHelpers.AspNetCore.Swashbuckle/SwaggerExtensions.cs index 92fa88d..447b493 100644 --- a/src/TinyHelpers.AspNetCore.Swashbuckle/SwaggerExtensions.cs +++ b/src/TinyHelpers.AspNetCore.Swashbuckle/SwaggerExtensions.cs @@ -49,19 +49,32 @@ public void AddTimeSpanTypeMapping(string? example) } /// - /// 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. /// + /// + /// Use this in Swagger configuration after registering parameters with + /// . Keeping + /// parameter definitions in dependency injection and applying them through a filter prevents duplicated route + /// metadata while preserving a complete contract for generated clients. + /// /// public void AddOperationParameters() => options.OperationFilter(); } /// - /// 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. /// /// The service collection to extend. - /// The configuration callback used to populate shared parameters. + /// + /// A callback that adds reusable parameter definitions to an instance. + /// /// The same instance so calls can be chained. + /// + /// 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 when the document is generated. + /// /// public static IServiceCollection AddSwaggerOperationParameters(this IServiceCollection services, Action setupAction) { diff --git a/src/TinyHelpers.AspNetCore.Swashbuckle/TinyHelpers.AspNetCore.Swashbuckle.csproj b/src/TinyHelpers.AspNetCore.Swashbuckle/TinyHelpers.AspNetCore.Swashbuckle.csproj index 50552eb..74f140e 100644 --- a/src/TinyHelpers.AspNetCore.Swashbuckle/TinyHelpers.AspNetCore.Swashbuckle.csproj +++ b/src/TinyHelpers.AspNetCore.Swashbuckle/TinyHelpers.AspNetCore.Swashbuckle.csproj @@ -6,6 +6,7 @@ enable enable TinyHelpers.AspNetCore.Swagger + TinyHelpers.AspNetCore.Swashbuckle.xml Marco Minerva Marco Minerva Tiny Helpers for Swashbuckle ASP.NET Core @@ -21,6 +22,10 @@ README.md + + + + diff --git a/src/TinyHelpers.AspNetCore/DataAnnotations/AllowedExtensionsAttribute.cs b/src/TinyHelpers.AspNetCore/DataAnnotations/AllowedExtensionsAttribute.cs index 306b545..0b9d76f 100644 --- a/src/TinyHelpers.AspNetCore/DataAnnotations/AllowedExtensionsAttribute.cs +++ b/src/TinyHelpers.AspNetCore/DataAnnotations/AllowedExtensionsAttribute.cs @@ -10,13 +10,15 @@ namespace TinyHelpers.AspNetCore.DataAnnotations; /// The allowed extensions, with or without the *. prefix. /// /// This attribute is useful when the extension is part of the contract, such as restricting uploads to image -/// formats that downstream processing or security policies can safely handle. +/// formats that downstream processing or security policies can safely handle. It complements content-type checks by +/// documenting and enforcing the file-name contract exposed to users and API clients. /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] public class AllowedExtensionsAttribute(params string[] extensions) : ValidationAttribute("Only files with the following extensions are supported: {0}") { private readonly IEnumerable extensions = extensions.Select(e => e.Replace("*.", string.Empty)); + /// protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) { if (value is IFormFile file) @@ -32,7 +34,7 @@ public class AllowedExtensionsAttribute(params string[] extensions) : Validation } /// - /// Formats the validation error using the configured extension list. + /// Formats the validation error with the configured extension list so failed validation tells the caller which suffixes are accepted. /// /// The validated member name. /// A localized error message that lists the allowed extensions. diff --git a/src/TinyHelpers.AspNetCore/DataAnnotations/ContentTypeAttribute.cs b/src/TinyHelpers.AspNetCore/DataAnnotations/ContentTypeAttribute.cs index f742d3e..9b6bb9a 100644 --- a/src/TinyHelpers.AspNetCore/DataAnnotations/ContentTypeAttribute.cs +++ b/src/TinyHelpers.AspNetCore/DataAnnotations/ContentTypeAttribute.cs @@ -10,8 +10,19 @@ namespace TinyHelpers.AspNetCore.DataAnnotations; /// public enum FileType { + /// + /// Represents image MIME types for upload scenarios where downstream processing expects visual media. + /// Image, + + /// + /// Represents video MIME types for upload scenarios where downstream processing expects moving-picture media. + /// Video, + + /// + /// Represents audio MIME types for upload scenarios where downstream processing expects sound media. + /// Audio } @@ -20,7 +31,8 @@ public enum FileType /// /// /// Use this attribute when the server depends on a known set of MIME types to avoid accepting files that cannot -/// be rendered, transcoded, or safely processed later in the pipeline. +/// be rendered, transcoded, or safely processed later in the pipeline. The accepted values become part of the +/// validation contract, which helps clients discover failures before storage or media-processing work begins. /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] public class ContentTypeAttribute : ValidationAttribute @@ -34,7 +46,7 @@ public class ContentTypeAttribute : ValidationAttribute private const string DefaultErrorMessage = "The {0} field should have one of the following Content-Types: {1}"; /// - /// Creates a new instance that accepts the specified MIME types. + /// Creates a new instance that accepts explicit MIME types when the built-in media categories are not precise enough. /// /// The accepted content types, such as image/png. public ContentTypeAttribute(params string[] validContentTypes) @@ -44,7 +56,7 @@ public ContentTypeAttribute(params string[] validContentTypes) } /// - /// Creates a new instance using one of the built-in content type groups. + /// Creates a new instance using one of the built-in content type groups for common upload scenarios. /// /// The predefined file category to accept. public ContentTypeAttribute(FileType fileType) @@ -59,6 +71,7 @@ public ContentTypeAttribute(FileType fileType) }; } + /// protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) { if (value is IFormFile formFile && !validContentTypes.Contains(formFile.ContentType, StringComparer.OrdinalIgnoreCase)) @@ -70,7 +83,7 @@ public ContentTypeAttribute(FileType fileType) } /// - /// Formats the validation error using the configured content types. + /// Formats the validation error with the configured content types. /// /// The validated member name. /// A localized error message that lists the allowed content types. diff --git a/src/TinyHelpers.AspNetCore/DataAnnotations/FileSizeAttribute.cs b/src/TinyHelpers.AspNetCore/DataAnnotations/FileSizeAttribute.cs index 6ccaedf..da2f568 100644 --- a/src/TinyHelpers.AspNetCore/DataAnnotations/FileSizeAttribute.cs +++ b/src/TinyHelpers.AspNetCore/DataAnnotations/FileSizeAttribute.cs @@ -10,11 +10,13 @@ namespace TinyHelpers.AspNetCore.DataAnnotations; /// The maximum accepted file size in bytes. /// /// This is intended for request boundaries where rejecting oversized payloads early is cheaper and clearer than -/// allowing the file to progress into later validation or storage stages. +/// allowing the file to progress into later validation or storage stages. The limit also documents the upload +/// contract that clients should honor before sending content. /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] public class FileSizeAttribute(int maxFileSizeInBytes) : ValidationAttribute("The {0} field size cannot be bigger than {1} bytes") { + /// protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) { if (value is IFormFile formFile && formFile.Length > maxFileSizeInBytes) @@ -26,7 +28,7 @@ public class FileSizeAttribute(int maxFileSizeInBytes) : ValidationAttribute("Th } /// - /// Formats the validation error using the configured byte limit. + /// Formats the validation error with the configured byte limit so rejected uploads report the exact contract boundary. /// /// The validated member name. /// A localized error message that includes the maximum file size. diff --git a/src/TinyHelpers.AspNetCore/Extensions/RouteHandlerBuilderExtensions.cs b/src/TinyHelpers.AspNetCore/Extensions/RouteHandlerBuilderExtensions.cs index 99225b0..a5ee376 100644 --- a/src/TinyHelpers.AspNetCore/Extensions/RouteHandlerBuilderExtensions.cs +++ b/src/TinyHelpers.AspNetCore/Extensions/RouteHandlerBuilderExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; #if NET9_0_OR_GREATER using Microsoft.OpenApi; #endif @@ -7,17 +8,25 @@ namespace TinyHelpers.AspNetCore.Extensions; /// -/// Adds minimal OpenAPI-focused helpers to so endpoint metadata stays close to the route mapping code. +/// Adds endpoint metadata helpers to so route declarations and their OpenAPI behavior stay together. /// +/// +/// Keeping response and header metadata next to the route mapping makes minimal APIs easier to review and helps the +/// generated OpenAPI document remain synchronized with the endpoint behavior. +/// /// public static class RouteHandlerBuilderExtensions { /// - /// Declares a set of problem responses on the endpoint. + /// Declares a set of responses on the endpoint for expected failure status codes. /// /// The route configuration being extended. /// The HTTP status codes that should be described as responses. /// The same so calls can be chained. + /// + /// Use this helper when an endpoint can fail with several documented status codes and all of them share the same + /// problem-details payload shape. + /// public static RouteHandlerBuilder ProducesDefaultProblem(this RouteHandlerBuilder builder, params int[] statusCodes) { foreach (var statusCode in statusCodes) diff --git a/src/TinyHelpers.AspNetCore/Extensions/ServiceCollectionExtensions.cs b/src/TinyHelpers.AspNetCore/Extensions/ServiceCollectionExtensions.cs index b6d558d..775b198 100644 --- a/src/TinyHelpers.AspNetCore/Extensions/ServiceCollectionExtensions.cs +++ b/src/TinyHelpers.AspNetCore/Extensions/ServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Localization; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -12,8 +13,12 @@ namespace TinyHelpers.AspNetCore.Extensions; /// -/// Registers common application services and configuration helpers that the library reuses across samples and packages. +/// Registers common ASP.NET Core services and configuration helpers that reduce repeated setup in applications using the library. /// +/// +/// These helpers intentionally keep the built-in dependency injection and options patterns visible while providing +/// one-line registration for common localization, replacement, and problem-details conventions. +/// public static class ServiceCollectionExtensions { extension(IServiceCollection services) @@ -35,21 +40,27 @@ public static class ServiceCollectionExtensions } /// - /// Registers request localization with a culture list. + /// Registers request localization with a culture list when the default provider order is sufficient. /// /// The supported culture names. /// The same so additional registrations can continue fluently. - /// The first culture becomes the default. + /// + /// The first culture becomes the default so applications can define their fallback culture in the same order + /// they declare supported cultures. + /// public IServiceCollection AddRequestLocalization(params string[] cultures) => services.AddRequestLocalization(cultures, null); /// - /// Registers request localization and allows the caller to adjust the culture-provider chain. + /// Registers request localization and allows the caller to adjust the culture-provider chain used during negotiation. /// /// The supported culture names. /// A callback that can reorder or replace the request culture providers. /// The same so additional registrations can continue fluently. - /// The first culture becomes the default. + /// + /// The first culture becomes the default. The provider callback exists for applications that need a specific + /// precedence, such as preferring route values over cookies or the Accept-Language header. + /// public IServiceCollection AddRequestLocalization(IEnumerable cultures, Action>? providersConfiguration) { var supportedCultures = cultures.Select(c => new CultureInfo(c)).ToList(); @@ -66,12 +77,16 @@ public IServiceCollection AddRequestLocalization(IEnumerable cultures, A } /// - /// Replaces one registered service with another implementation while preserving the desired lifetime. + /// Replaces one registered service with another implementation when an application needs to override a library or framework default. /// /// The service contract to replace. /// The implementation to register. /// The lifetime that should be used for the replacement registration. /// The same so additional registrations can continue fluently. + /// + /// This wraps the standard replacement pattern so callers can state the intended service contract and + /// implementation without manually creating a . + /// public IServiceCollection Replace(ServiceLifetime lifetime = ServiceLifetime.Scoped) where TService : class where TImplementation : class, TService @@ -84,8 +99,11 @@ public IServiceCollection Replace(ServiceLifetime lif /// /// Registers a predictable pipeline so the application emits consistent error payloads. /// - /// The service collection being configured. /// The same so additional registrations can continue fluently. + /// + /// The customization fills common problem-details fields and a trace identifier, which gives clients and logs a + /// stable shape to correlate errors without requiring each application to repeat the same setup. + /// public IServiceCollection AddDefaultProblemDetails() { services.AddProblemDetails(options => @@ -100,6 +118,10 @@ public IServiceCollection AddDefaultProblemDetails() /// Registers the library default exception handler so uncaught failures are shaped into problem details before they leave the pipeline. /// /// The same so additional registrations can continue fluently. + /// + /// Registering the handler with the problem-details service keeps unexpected failures consistent with explicit + /// validation and endpoint errors, which simplifies client error handling. + /// public IServiceCollection AddDefaultExceptionHandler() { // Ensures that the ProblemDetails service is registered. diff --git a/src/TinyHelpers.AspNetCore/Middlewares/ApplicationBuilderExtensions.cs b/src/TinyHelpers.AspNetCore/Middlewares/ApplicationBuilderExtensions.cs index 124a30a..7187c3e 100644 --- a/src/TinyHelpers.AspNetCore/Middlewares/ApplicationBuilderExtensions.cs +++ b/src/TinyHelpers.AspNetCore/Middlewares/ApplicationBuilderExtensions.cs @@ -3,7 +3,7 @@ namespace TinyHelpers.AspNetCore.Middlewares; /// -/// Provides middleware registration helpers. +/// Provides middleware registration helpers for request-body behaviors that are otherwise easy to forget in pipeline setup. /// public static class ApplicationBuilderExtensions { @@ -12,7 +12,6 @@ public static class ApplicationBuilderExtensions /// /// Enables request buffering so downstream middleware and services can re-read the body after an earlier component has inspected it. /// - /// The application pipeline being configured. /// The builder instance. public IApplicationBuilder UseRequestRewind() => app.UseMiddleware(); diff --git a/src/TinyHelpers.AspNetCore/OpenApi/OpenApiExtensions.cs b/src/TinyHelpers.AspNetCore/OpenApi/OpenApiExtensions.cs index 9a2f237..6a04365 100644 --- a/src/TinyHelpers.AspNetCore/OpenApi/OpenApiExtensions.cs +++ b/src/TinyHelpers.AspNetCore/OpenApi/OpenApiExtensions.cs @@ -1,23 +1,36 @@ #if NET9_0_OR_GREATER using Microsoft.AspNetCore.OpenApi; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using TinyHelpers.AspNetCore.OpenApi.Transformers; namespace TinyHelpers.AspNetCore.OpenApi; /// -/// Adds OpenAPI configuration helpers that keep the generated contract aligned with the library's conventions. +/// Adds opinionated and service-registration helpers used to keep generated OpenAPI +/// documents aligned with the runtime behavior configured by this library. /// +/// +/// These helpers centralize reusable OpenAPI conventions, such as shared operation parameters and default error +/// responses, so applications do not need to repeat the same transformer setup for every document or endpoint. +/// public static class OpenApiExtensions { extension(IServiceCollection services) { /// - /// Registers OpenAPI parameter definitions that can be automatically applied to every operation. + /// Registers shared OpenAPI operation parameters that can later be merged into generated operations. /// - /// A callback that fills the parameter options object. - /// The same for fluent registration. + /// + /// A callback that adds reusable parameter definitions to an instance. + /// + /// The same instance so registrations can continue fluently. + /// + /// Call this during service registration when a parameter, such as a tenant, correlation, or feature header, + /// must appear consistently in the OpenAPI contract without being repeated on every endpoint. The registered + /// options are consumed by when the OpenAPI document is generated. + /// /// public IServiceCollection AddOpenApiOperationParameters(Action setupAction) { @@ -57,9 +70,15 @@ public OpenApiOptions AddDefaultProblemDetailsResponse() } /// - /// Adds shared OpenAPI parameter definitions so they are automatically applied to every generated operation. + /// Adds the operation transformer that copies registered shared parameters into each generated OpenAPI operation. /// - /// The same for fluent configuration. + /// + /// Use this in the OpenAPI document configuration after registering parameters with + /// . Keeping + /// parameter definitions in dependency injection and applying them through a transformer prevents duplicated + /// route metadata while preserving a complete contract for generated clients. + /// + /// The same instance so OpenAPI configuration can continue fluently. /// public OpenApiOptions AddOperationParameters() => options.AddOperationTransformer(); @@ -104,7 +123,6 @@ public OpenApiOptions EnableEnumSupport() /// /// Configures schema reference IDs to include the namespace so types with the same name do not collide in large models. /// - /// The to configure. /// The same instance for further customization. /// /// OpenAPI defaults to the short type name, which is easy to read but can produce duplicate schema IDs when a diff --git a/src/TinyHelpers.AspNetCore/OpenApi/OpenApiSchemaHelper.cs b/src/TinyHelpers.AspNetCore/OpenApi/OpenApiSchemaHelper.cs index 30a0ac0..ead37ee 100644 --- a/src/TinyHelpers.AspNetCore/OpenApi/OpenApiSchemaHelper.cs +++ b/src/TinyHelpers.AspNetCore/OpenApi/OpenApiSchemaHelper.cs @@ -9,13 +9,17 @@ namespace TinyHelpers.AspNetCore.OpenApi; /// /// Creates reusable schema fragments so OpenAPI transformers can express the same contract details without duplicating schema setup. /// +/// +/// These factory methods keep schema construction consistent across transformers that need to describe default values, +/// formats, and enum choices for generated clients. +/// public static class OpenApiSchemaHelper { /// - /// Creates a string schema with an optional default value. + /// Creates a string schema with an optional default value for reusable text-based contract metadata. /// /// The optional default value. - /// A schema configured for . + /// A schema configured for the OpenAPI string type. public static OpenApiSchema CreateStringSchema(string? defaultValue = null) { var schema = new OpenApiSchema @@ -28,7 +32,7 @@ public static OpenApiSchema CreateStringSchema(string? defaultValue = null) } /// - /// Creates a schema with the specified type and format. + /// Creates a typed schema with an optional format when a transformer needs to describe a primitive OpenAPI shape. /// /// The type associated with the schema. /// The OpenAPI type name. @@ -46,7 +50,7 @@ public static OpenApiSchema CreateSchema(string type, string? format = n } /// - /// Creates a typed schema with a default value. + /// Creates a typed schema with a default value so generated clients can display the same fallback used by the API contract. /// /// The value type associated with the schema. /// The OpenAPI type name. @@ -66,7 +70,7 @@ public static OpenApiSchema CreateSchema(string type, string? format, TV } /// - /// Creates a string enum schema from a known list of values. + /// Creates a string enum schema from a known list of values when the valid choices are defined outside a CLR enum. /// /// The allowed values to expose in the schema. /// The optional default value. @@ -84,7 +88,7 @@ public static OpenApiSchema CreateSchema(IEnumerable values, string? def } /// - /// Creates an enum schema from an type. + /// Creates an enum schema from an type so the generated document exposes every declared value. /// /// The enumeration type to describe. /// The optional default enum value. @@ -113,10 +117,14 @@ namespace TinyHelpers.AspNetCore.OpenApi; /// /// Creates reusable schema fragments so OpenAPI transformers can express the same contract details without duplicating schema setup. /// +/// +/// These factory methods keep schema construction consistent across transformers that need to describe default values, +/// formats, and enum choices for generated clients. +/// public static class OpenApiSchemaHelper { /// - /// Creates a string schema with an optional default value. + /// Creates a string schema with an optional default value for reusable text-based contract metadata. /// /// The optional default value. /// A schema configured for . @@ -132,7 +140,7 @@ public static OpenApiSchema CreateStringSchema(string? defaultValue = null) } /// - /// Creates a schema with the specified type and format. + /// Creates a typed schema with an optional format when a transformer needs to describe a primitive OpenAPI shape. /// /// The type associated with the schema. /// The OpenAPI type value. @@ -150,7 +158,7 @@ public static OpenApiSchema CreateSchema(JsonSchemaType type, string? fo } /// - /// Creates a typed schema with a default value. + /// Creates a typed schema with a default value so generated clients can display the same fallback used by the API contract. /// /// The type associated with the schema. /// The OpenAPI type value. @@ -170,7 +178,7 @@ public static OpenApiSchema CreateSchema(JsonSchemaType type, string? fo } /// - /// Creates a string enum schema from a known list of values. + /// Creates a string enum schema from a known list of values when the valid choices are defined outside a CLR enum. /// /// The allowed values to expose in the schema. /// The optional default value. @@ -188,7 +196,7 @@ public static OpenApiSchema CreateSchema(IEnumerable values, string? def } /// - /// Creates an enum schema from an type. + /// Creates an enum schema from an type so the generated document exposes every declared value. /// /// The enumeration type to describe. /// The optional default enum value. diff --git a/src/TinyHelpers.AspNetCore/OpenApi/Transformers/CamelCaseQueryParametersOperationTransformer.cs b/src/TinyHelpers.AspNetCore/OpenApi/Transformers/CamelCaseQueryParametersOperationTransformer.cs index d6bf95d..063fe8c 100644 --- a/src/TinyHelpers.AspNetCore/OpenApi/Transformers/CamelCaseQueryParametersOperationTransformer.cs +++ b/src/TinyHelpers.AspNetCore/OpenApi/Transformers/CamelCaseQueryParametersOperationTransformer.cs @@ -6,6 +6,9 @@ namespace TinyHelpers.AspNetCore.OpenApi.Transformers; +/// +/// Normalizes generated query-parameter names to camel case so the OpenAPI document matches common ASP.NET Core JSON naming conventions. +/// public class CamelCaseQueryParametersOperationTransformer : IOpenApiOperationTransformer { /// @@ -38,6 +41,9 @@ public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransform namespace TinyHelpers.AspNetCore.OpenApi.Transformers; +/// +/// Normalizes generated query-parameter names to camel case so the OpenAPI document matches common ASP.NET Core JSON naming conventions. +/// public class CamelCaseQueryParametersOperationTransformer : IOpenApiOperationTransformer { /// diff --git a/src/TinyHelpers.AspNetCore/OpenApi/Transformers/DefaultResponseOperationTransformer.cs b/src/TinyHelpers.AspNetCore/OpenApi/Transformers/DefaultResponseOperationTransformer.cs index 2fe0607..b76d7c6 100644 --- a/src/TinyHelpers.AspNetCore/OpenApi/Transformers/DefaultResponseOperationTransformer.cs +++ b/src/TinyHelpers.AspNetCore/OpenApi/Transformers/DefaultResponseOperationTransformer.cs @@ -51,10 +51,19 @@ public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransform namespace TinyHelpers.AspNetCore.OpenApi; +/// +/// Adds a conventional default problem-details response to operations so API clients can discover a consistent error contract. +/// public class DefaultResponseOperationTransformer : IOpenApiOperationTransformer { + /// + /// Gets or sets the response key used for the fallback error response when a specific status code is not documented. + /// public string DefaultResponseCode { get; set; } = "default"; + /// + /// Gets or sets the description applied to the fallback error response so generated documents have meaningful failure metadata. + /// public string DefaultDescription { get; set; } = "Error"; /// diff --git a/src/TinyHelpers.AspNetCore/OpenApi/Transformers/OpenApiOperationOptions.cs b/src/TinyHelpers.AspNetCore/OpenApi/Transformers/OpenApiOperationOptions.cs index b716717..42c6fae 100644 --- a/src/TinyHelpers.AspNetCore/OpenApi/Transformers/OpenApiOperationOptions.cs +++ b/src/TinyHelpers.AspNetCore/OpenApi/Transformers/OpenApiOperationOptions.cs @@ -9,7 +9,8 @@ namespace TinyHelpers.AspNetCore.OpenApi; /// /// /// The library uses this options object to register shared parameter definitions once and copy them into the -/// generated OpenAPI document wherever they are needed, which keeps endpoint setup centralized and avoids drift. +/// generated OpenAPI document wherever they are needed. This keeps endpoint setup centralized and avoids drift between +/// route metadata and the generated client contract. /// public class OpenApiOperationOptions { @@ -18,11 +19,11 @@ internal OpenApiOperationOptions() } /// - /// Gets the parameter definitions that should be merged into matching OpenAPI operations. + /// Gets the parameter definitions that should be merged into generated OpenAPI operations. /// /// - /// Parameters are intentionally stored here rather than declared inline on each route so callers can reuse the - /// same metadata across multiple endpoints and keep the generated contract consistent. + /// 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. /// public IList Parameters { get; } = []; } @@ -33,12 +34,27 @@ internal OpenApiOperationOptions() namespace TinyHelpers.AspNetCore.OpenApi; +/// +/// Collects reusable OpenAPI operation metadata that should be applied consistently across multiple endpoints. +/// +/// +/// 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. +/// public class OpenApiOperationOptions { internal OpenApiOperationOptions() { } + /// + /// Gets the parameter definitions that should be merged into generated OpenAPI operations. + /// + /// + /// 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. + /// public IList Parameters { get; } = []; } diff --git a/src/TinyHelpers.AspNetCore/OpenApi/Transformers/TimeExampleSchemaTransformer.cs b/src/TinyHelpers.AspNetCore/OpenApi/Transformers/TimeExampleSchemaTransformer.cs index 0307c26..474f942 100644 --- a/src/TinyHelpers.AspNetCore/OpenApi/Transformers/TimeExampleSchemaTransformer.cs +++ b/src/TinyHelpers.AspNetCore/OpenApi/Transformers/TimeExampleSchemaTransformer.cs @@ -6,6 +6,9 @@ namespace TinyHelpers.AspNetCore.OpenApi.Transformers; +/// +/// Adds representative examples to time-based schemas so generated OpenAPI documents communicate the expected wire format. +/// public class TimeExampleSchemaTransformer : IOpenApiSchemaTransformer { /// @@ -35,6 +38,9 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext namespace TinyHelpers.AspNetCore.OpenApi.Transformers; +/// +/// Adds representative examples to time-based schemas so generated OpenAPI documents communicate the expected wire format. +/// public class TimeExampleSchemaTransformer : IOpenApiSchemaTransformer { /// diff --git a/src/TinyHelpers.AspNetCore/README.md b/src/TinyHelpers.AspNetCore/README.md index 1829e05..a698267 100644 --- a/src/TinyHelpers.AspNetCore/README.md +++ b/src/TinyHelpers.AspNetCore/README.md @@ -23,9 +23,9 @@ The package targets: - .NET 9 - .NET 10 -OpenAPI features are available when the consuming project targets .NET 9 or .NET 10. Some extensions are also framework-specific: +OpenAPI features are available when the consuming project targets .NET 9 or .NET 10. Some extensions are framework-specific: -- `EnableEnumSupport()` is available only on .NET 9+ +- `EnableEnumSupport()` is available only on .NET 9 - `UseStrictNumericSchemas()` is available only on .NET 10+ - `WithResponseDescription()` and `WithLocationHeader()` are available only on .NET 10+ @@ -110,11 +110,11 @@ var hasName = principal.HasClaim(ClaimTypes.Name); | --- | --- | --- | | `ConfigureAndGet(configuration, sectionName)` | Binds a section and also registers the options in the container, returning the created instance. | When you want to configure and read the same options during startup. | | `Replace(lifetime)` | Replaces a registered service with another implementation. | When you want to swap a service without rewriting the whole registration. | -| `AddDefaultProblemDetails()` | Registers `ProblemDetails` with a consistent and centralized configuration. | When you want uniform RFC 7807 responses across the app. | -| `AddDefaultExceptionHandler()` | Registers the library's default `IExceptionHandler` and ensures `ProblemDetails` is available too. | When you want unhandled exceptions to become a standard payload. | -| `UseDefaults(ProblemDetailsContext context)` | Applies the library's default values to a `ProblemDetailsContext`. | When you customize `CustomizeProblemDetails` but still want the same base behavior. | -| `AddRequestLocalization(params string[] cultures)` | Registers localization using only the list of supported cultures. The first culture becomes the default. | When you do not need to configure providers manually. | -| `AddRequestLocalization(IEnumerable cultures, Action>? providersConfiguration)` | Registers localization and lets you customize the culture-provider chain. The first culture becomes the default. | When you want to change the provider order or composition. | +| `AddDefaultProblemDetails()` | Registers `ProblemDetails` with defaults for common fields and a trace identifier. | When you want uniform RFC 7807 responses that are easy to correlate in clients and logs. | +| `AddDefaultExceptionHandler()` | Registers the library's default `IExceptionHandler` and ensures `ProblemDetails` is available too. | When you want unhandled exceptions to use the same payload shape as explicit errors. | +| `UseDefaults(ProblemDetailsContext context)` | Applies the library's default values to a `ProblemDetailsContext`. | When you customize `CustomizeProblemDetails` but still want the same base error contract. | +| `AddRequestLocalization(params string[] cultures)` | Registers localization using only the list of supported cultures. The first culture becomes the default. | When the default provider order is enough. | +| `AddRequestLocalization(IEnumerable cultures, Action>? providersConfiguration)` | Registers localization and lets you customize the culture-provider chain. The first culture becomes the default. | When you want a specific precedence, such as route values before cookies or the `Accept-Language` header. | ### Example @@ -138,7 +138,7 @@ builder.Services.AddDefaultExceptionHandler(); | Method | What it does | When to use it | | --- | --- | --- | -| `UseRequestRewind()` | Enables request-body buffering so the body can be read more than once. | When a middleware or filter must inspect the body without consuming it permanently. | +| `UseRequestRewind()` | Enables request-body buffering so the body can be read more than once. | When middleware or filters must inspect the body for validation, auditing, or request-signature checks without consuming it permanently. | ### `EnableRequestRewindMiddleware` @@ -159,14 +159,14 @@ if (navManager.TryGetQueryString("page", out var page)) ### `AllowedExtensionsAttribute` -Validates that an `IFormFile` uses one of the allowed file extensions. +Validates that an `IFormFile` uses one of the allowed file extensions. Use it when the file-name suffix is part of the upload contract and downstream processing or policy checks require known formats. - Constructor: `AllowedExtensionsAttribute(params string[] extensions)` - `FormatErrorMessage(string name)`: generates the error message with the allowed extensions ### `ContentTypeAttribute` -Validates the `Content-Type` of an `IFormFile`. +Validates the `Content-Type` of an `IFormFile`. Use it when the server accepts only MIME types that can be rendered, transcoded, stored, or otherwise processed safely. - `ContentTypeAttribute(params string[] validContentTypes)`: uses an explicit MIME type list - `ContentTypeAttribute(FileType fileType)`: uses a predefined group @@ -174,7 +174,7 @@ Validates the `Content-Type` of an `IFormFile`. ### `FileType` -Supporting enum for `ContentTypeAttribute`: +Supporting enum for the built-in `ContentTypeAttribute` MIME type groups: - `Image` - `Video` @@ -182,14 +182,14 @@ Supporting enum for `ContentTypeAttribute`: ### `FileSizeAttribute` -Validates that an `IFormFile` does not exceed a maximum size. +Validates that an `IFormFile` does not exceed a maximum size. Use it to reject oversized uploads at the request boundary before later validation, storage, or media-processing work starts. - Constructor: `FileSizeAttribute(int maxFileSizeInBytes)` - `FormatErrorMessage(string name)`: generates the error message with the limit in bytes ### `RoleAuthorizeAttribute` -Automatically builds the `Roles` list of `AuthorizeAttribute` from one or more roles. +Automatically builds the `Roles` list of `AuthorizeAttribute` from one or more role values, keeping role-based authorization declarations readable without manually composing a comma-delimited string. - Constructor: `RoleAuthorizeAttribute(params string[] roles)` @@ -219,11 +219,11 @@ public IActionResult SecretArea() | Method | What it does | Availability | | --- | --- | --- | -| `ProducesDefaultProblem(params int[] statusCodes)` | Adds `ProblemDetails` responses to the route metadata for the specified status codes. | .NET 8+ | -| `WithResponseDescription(int statusCode, string description)` | Updates the description of an existing OpenAPI response. | .NET 10+ | -| `WithLocationHeader(string description, int statusCode)` | Adds a `Location` header to the specified creation response. | .NET 10+ | +| `ProducesDefaultProblem(params int[] statusCodes)` | Adds `ProblemDetails` responses to the route metadata for expected failure status codes. | .NET 8+ | +| `WithResponseDescription(int statusCode, string description)` | Updates the description of an existing OpenAPI response when the generated text is too generic. | .NET 10+ | +| `WithLocationHeader(string description, int statusCode)` | Adds a required `Location` header to the specified creation response. | .NET 10+ | -These helpers keep Minimal API configuration close to the route instead of scattering OpenAPI metadata in separate places. +These helpers keep Minimal API behavior and OpenAPI metadata close to the route mapping. This makes endpoint declarations easier to review and helps the generated document stay synchronized with runtime behavior. ### Example @@ -242,16 +242,16 @@ app.MapPost("/orders", () => Results.Created("/orders/1", new { Id = 1 })) ## OpenAPI -The OpenAPI extensions are available when the consuming project uses the package on .NET 9 or .NET 10. +The OpenAPI extensions are available when the consuming project uses the package on .NET 9 or .NET 10. They centralize reusable OpenAPI conventions so shared parameters, error responses, schema IDs, and schema transformations do not need to be repeated for every endpoint or document. ### `OpenApiExtensions` | Method | What it does | Notes | | --- | --- | --- | -| `AddOpenApiOperationParameters(setupAction)` | Registers reusable OpenAPI parameters inside `OpenApiOperationOptions.Parameters`. | Lets you declare common parameters once. | +| `AddOpenApiOperationParameters(setupAction)` | Registers reusable OpenAPI operation parameters inside `OpenApiOperationOptions.Parameters`. | Declare cross-cutting headers or query values once during service registration. | | `AddAcceptLanguageHeader()` | Adds the `Accept-Language` header to documented operations. | Useful when the app uses request localization. | | `AddDefaultProblemDetailsResponse()` | Adds a default error response based on `ProblemDetails`. | In .NET 9 it also adds the document transformer. | -| `AddOperationParameters()` | Adds the parameters configured through `OpenApiOperationOptions`. | The bridge between the options bag and the generated document. | +| `AddOperationParameters()` | Adds the operation transformer that copies parameters registered with `AddOpenApiOperationParameters()` into generated operations. | Use it in OpenAPI configuration so the generated contract includes those shared inputs. | | `RemoveServerList()` | Removes the server list from the OpenAPI document. | Useful when you want a more portable document across environments. | | `WriteNumberAsString()` | Aligns the schema with numbers serialized as strings. | Useful when runtime JSON uses `JsonNumberHandling.WriteAsString`. | | `DescribeAllParametersInCamelCase()` | Converts query parameter names to camel case in the document. | Keeps documentation consistent with JSON naming. | @@ -262,7 +262,7 @@ The OpenAPI extensions are available when the consuming project uses the package ### `OpenApiSchemaHelper` -This utility creates ready-to-use schema fragments that can be reused in transformers or other OpenAPI customizations. +This utility creates ready-to-use schema fragments that can be reused in transformers or other OpenAPI customizations. It keeps default values, formats, and enum choices consistent across generated documents. #### .NET 9 @@ -282,11 +282,11 @@ This utility creates ready-to-use schema fragments that can be reused in transfo ### What they do in practice -- `CreateStringSchema()` creates a simple string schema, optionally with a default. -- `CreateSchema(..., format)` creates a schema with explicit type and format. -- `CreateSchema(..., defaultValue)` also adds a default to the contract. -- `CreateSchema(IEnumerable values, ...)` turns a list of values into a string-based OpenAPI enum. -- `CreateSchema(...)` builds an enum schema directly from a CLR enum. +- `CreateStringSchema()` creates a reusable string schema, optionally with a default value. +- `CreateSchema(..., format)` creates a primitive schema with explicit type and format metadata. +- `CreateSchema(..., defaultValue)` also adds the default value that clients should display or assume. +- `CreateSchema(IEnumerable values, ...)` turns externally defined choices into a string-based OpenAPI enum. +- `CreateSchema(...)` builds an enum schema directly from a CLR enum so every declared value is documented. ### Example @@ -312,6 +312,8 @@ builder.Services.AddOpenApi(options => ### Example `AddOpenApiOperationParameters()` +Register shared parameters during service registration, then enable `AddOperationParameters()` in OpenAPI configuration. This keeps cross-cutting inputs centralized while still exposing them in the generated client contract. + ```csharp builder.Services.AddOpenApiOperationParameters(parameters => { @@ -323,6 +325,11 @@ builder.Services.AddOpenApiOperationParameters(parameters => Description = "Identifier used to correlate requests" }); }); + +builder.Services.AddOpenApi(options => +{ + options.AddOperationParameters(); +}); ``` ### Example `OpenApiSchemaHelper` diff --git a/src/TinyHelpers.AspNetCore/TinyHelpers.AspNetCore.csproj b/src/TinyHelpers.AspNetCore/TinyHelpers.AspNetCore.csproj index d3b4728..fb987b3 100644 --- a/src/TinyHelpers.AspNetCore/TinyHelpers.AspNetCore.csproj +++ b/src/TinyHelpers.AspNetCore/TinyHelpers.AspNetCore.csproj @@ -5,6 +5,7 @@ latest enable enable + TinyHelpers.AspNetCore.xml Marco Minerva Marco Minerva Tiny Helpers for ASP.NET Core @@ -20,6 +21,10 @@ README.md + + + + diff --git a/src/TinyHelpers/Extensions/CollectionExtensions.cs b/src/TinyHelpers/Extensions/CollectionExtensions.cs index e2ac15d..3617291 100644 --- a/src/TinyHelpers/Extensions/CollectionExtensions.cs +++ b/src/TinyHelpers/Extensions/CollectionExtensions.cs @@ -5,8 +5,12 @@ namespace TinyHelpers.Extensions; /// -/// Contains extension methods for collections. +/// Provides collection and sequence helpers that keep common null-safety, indexing, asynchronous projection, and conditional filtering patterns reusable. /// +/// +/// These extensions are intended for application code that repeatedly composes LINQ queries or optional collections and +/// should avoid scattering small guard clauses and loop helpers across the codebase. +/// public static class CollectionExtensions { #if NETSTANDARD2_0 @@ -45,7 +49,7 @@ public static IEnumerable Chunk(this IEnumerable so #endif /// - /// Returns the elements of an , or an empty singleton collection if the sequence is . + /// Returns the source sequence or an empty sequence when the source is so callers can enumerate safely. /// /// The type of the data in the source. /// The sequence to return a default value for if it is . @@ -54,7 +58,7 @@ public static IEnumerable EmptyIfNull(this IEnumerable source ?? []; /// - /// Returns the elements of an , or an empty singleton collection if the sequence is . + /// Returns the source query or an empty query when the source is so query composition can continue safely. /// /// The type of the data in the source. /// The sequence to return a default value for if it is . @@ -63,12 +67,12 @@ public static IQueryable EmptyIfNull(this IQueryable? => source ?? Array.Empty().AsQueryable(); /// - /// Performs the specified action on each element of the . + /// Performs an action for each item and returns the original sequence so side-effect steps can remain in a fluent pipeline. /// /// The type of the data in the source. /// The sequence on whose elements apply the action to. /// The delegate to perform on each element of the collection. - /// An whose elements are the result of invoking the action on each element of source. + /// The original sequence after has been invoked for each item. public static IEnumerable ForEach(this IEnumerable source, Action action) { foreach (var item in source) @@ -80,13 +84,13 @@ public static IEnumerable ForEach(this IEnumerable so } /// - /// Asynchronously performs the specified action on each element of the . + /// Performs an asynchronous action for each item and returns the original sequence after every action completes. /// /// The type of the data in the source. /// The sequence on whose elements apply the action to. /// An asynchronous delegate that is invoked once per element in the data source. /// A token that can be used to request cancellation of the asynchronous operation. - /// An whose elements are the result of invoking the action on each element of source. + /// The original sequence after every asynchronous action has completed. public static async Task> ForEachAsync(this IEnumerable source, Func action, CancellationToken cancellationToken = default) { foreach (var item in source) @@ -99,12 +103,12 @@ public static async Task> ForEachAsync(this IEnume } /// - /// Asynchronously projects each element of a sequence into a new form. + /// Projects each item with an asynchronous selector while preserving the source order in the materialized result. /// /// The type of the elements of source. /// The type of the value returned by selector. /// A sequence of values to invoke a transform function on. - /// A transform function to apply to each source element; the second parameter of the function represents the index of the source element. + /// A transform function to apply to each source element. /// A token that can be used to request cancellation of the asynchronous operation. /// An whose elements are the result of invoking the transform function on each element of source. /// source is null @@ -250,7 +254,7 @@ public static long GetLongCount(this IEnumerable? source, Func => (predicate is null ? source?.LongCount() : source?.LongCount(predicate)) ?? 0; /// - /// Filters a sequence of values based on a condition. + /// Applies a filter only when is so optional filters can remain in a fluent pipeline. /// /// The type of the elements. /// The to check. @@ -261,7 +265,7 @@ public static IEnumerable WhereIf(this IEnumerable so => condition ? source.Where(predicate) : source; /// - /// Filters a sequence of values based on a condition. + /// Applies a queryable filter only when is so optional query predicates remain composable. /// /// The type of the elements. /// The to check. diff --git a/src/TinyHelpers/Extensions/DateTimeOffsetExtensions.cs b/src/TinyHelpers/Extensions/DateTimeOffsetExtensions.cs index ad2ca35..0fd304f 100644 --- a/src/TinyHelpers/Extensions/DateTimeOffsetExtensions.cs +++ b/src/TinyHelpers/Extensions/DateTimeOffsetExtensions.cs @@ -11,8 +11,8 @@ public static class DateTimeOffsetExtensions /// Constructs a object that is set to the date part of the specified . /// /// The object to extract the date part from. - /// The optional to apply to the resulting value. - /// A object representing date of the day specified in the object. + /// The optional to apply to the resulting value; UTC is used when no zone is specified. + /// A value for the date in the selected time zone. /// /// /// @@ -26,15 +26,15 @@ public static DateOnly ToDateOnly(this DateTimeOffset dateTimeOffset, TimeZoneIn /// Constructs a object from a representing the time of the day in this object. /// /// The object to extract the time of the day from. - /// The optional to apply to the resulting value. - /// A object representing time of the day specified in the object. + /// The optional to apply to the resulting value; UTC is used when no zone is specified. + /// A value for the time in the selected time zone. /// /// /// public static TimeOnly ToTimeOnly(this DateTimeOffset dateTimeOffset, TimeZoneInfo? zone = null) { var inTargetZone = TimeZoneInfo.ConvertTime(dateTimeOffset, zone ?? TimeZoneInfo.Utc); - return TimeOnly.FromDateTime(dateTimeOffset.DateTime); + return TimeOnly.FromDateTime(inTargetZone.DateTime); } #endif } diff --git a/src/TinyHelpers/Extensions/GuidExtensions.cs b/src/TinyHelpers/Extensions/GuidExtensions.cs index 8e088a4..2fb87ff 100644 --- a/src/TinyHelpers/Extensions/GuidExtensions.cs +++ b/src/TinyHelpers/Extensions/GuidExtensions.cs @@ -4,7 +4,7 @@ namespace TinyHelpers.Extensions; /// -/// Contains extensions methods for the type. +/// Provides helpers for treating as a missing identifier and creating fallback values consistently. /// public static class GuidExtensions { @@ -63,15 +63,15 @@ public static bool HasValue([NotNullWhen(true)] this Guid? input) => !input.IsEmpty(); /// - /// Gets the actual value of this instance, if it is different from Guid.Empty; otherwise, creates a new using . + /// Returns the current identifier when it is not ; otherwise, creates a new random identifier. /// /// The to test. - /// The actual value of this instance, if it is different from Guid.Empty; otherwise, a new created with . + /// The current identifier when it is not empty; otherwise, a new value created with . public static Guid GetValueOrCreateNew(this Guid input) => input.IsEmpty() ? Guid.NewGuid() : input; /// - /// Gets the actual value of this instance, if it is different from Guid.Empty; otherwise, returns the specified default value. + /// Returns the current identifier when it is not ; otherwise, returns a caller-provided fallback value. /// /// The to test. /// The default to return if the input is Guid.Empty. @@ -80,7 +80,7 @@ public static Guid GetValueOrDefault(this Guid input, Guid defaultValue) => input.IsEmpty() ? defaultValue : input; /// - /// Gets the actual value of this instance, if it is different from and Guid.Empty; otherwise, creates a new using . + /// Returns the current identifier when it is not or ; otherwise, creates a new random identifier. /// /// The to test. /// The actual value of this instance, if it is different from and Guid.Empty; otherwise, a new created with . @@ -89,11 +89,11 @@ public static Guid GetValueOrCreateNew(this Guid? input) #if NET9_0_OR_GREATER /// - /// Gets the actual value of this instance, if it is different from Guid.Empty; otherwise, creates a new using . + /// Returns the current identifier when it is not ; otherwise, creates a new identifier using the requested GUID version. /// /// The to test. /// The version of the to create if the input is Guid.Empty. - /// The actual value of this instance, if it is different from Guid.Empty; otherwise, a new . + /// The current identifier when it is not empty; otherwise, a new identifier using . public static Guid GetValueOrCreateNew(this Guid input, GuidVersion guidVersion) => input.IsEmpty() ? guidVersion switch { @@ -102,11 +102,11 @@ public static Guid GetValueOrCreateNew(this Guid input, GuidVersion guidVersion) } : input; /// - /// Gets the actual value of this instance, if it is different from and Guid.Empty; otherwise, creates a new . + /// Returns the current identifier when it is not or ; otherwise, creates a new identifier using the requested GUID version. /// /// The to test. /// The version of the to create if the input is or Guid.Empty. - /// The actual value of this instance, if it is different from and Guid.Empty; otherwise, a new . + /// The current identifier when it has a value; otherwise, a new identifier using . public static Guid GetValueOrCreateNew(this Guid? input, GuidVersion guidVersion) => input.IsEmpty() ? guidVersion switch { diff --git a/src/TinyHelpers/Http/AuthenticatedParameterizedHttpClientHandler.cs b/src/TinyHelpers/Http/AuthenticatedParameterizedHttpClientHandler.cs index fc320de..1d4ba14 100644 --- a/src/TinyHelpers/Http/AuthenticatedParameterizedHttpClientHandler.cs +++ b/src/TinyHelpers/Http/AuthenticatedParameterizedHttpClientHandler.cs @@ -4,12 +4,16 @@ namespace TinyHelpers.Http; /// -/// Represents a handler to authenticate HTTP requests using Bearer token. +/// Adds an authorization token to outgoing HTTP requests and can refresh the token once after an unauthorized response. /// +/// +/// Use this handler when token acquisition depends on the current , such as per-tenant +/// or per-resource tokens, and the client needs a lightweight retry path for expired credentials. +/// public class AuthenticatedParameterizedHttpClientHandler : DelegatingHandler { /// - /// Scheme for Bearer authorization. + /// The default authorization scheme used when the request does not already specify one. /// public const string BearerAuthorizationScheme = "Bearer"; @@ -19,11 +23,11 @@ public class AuthenticatedParameterizedHttpClientHandler : DelegatingHandler private readonly string authorizationScheme; /// - /// Initializes a new instance of the class. + /// Initializes a new handler that can add request-specific authorization tokens and optionally refresh them. /// - /// Delegate function to get access token for authentication. - /// Optional delegate function to refresh access token. - /// Indicates if there should be an Authorization header. + /// Delegate used to get the access token for the current request. + /// Optional delegate used to refresh the access token after a response. + /// Indicates whether a token should only be added when the request already has an authorization header. /// The authorization scheme used for the HTTP request. /// is . public AuthenticatedParameterizedHttpClientHandler(Func> getToken, Func? refreshToken = null, bool checkAuthorizationHeader = true, string authorizationScheme = BearerAuthorizationScheme) @@ -35,10 +39,10 @@ public AuthenticatedParameterizedHttpClientHandler(Func - /// Constructor for AuthenticatedParameterizedHttpClientHandler. + /// Initializes a new handler with a custom inner handler and request-specific token provider. /// - /// Delegate function to get access token for authentication. - /// Inner handler to send request to. + /// Delegate used to get the access token for the current request. + /// The next handler in the HTTP pipeline. /// The authorization scheme used for the HTTP request. /// is . public AuthenticatedParameterizedHttpClientHandler(Func> getToken, HttpMessageHandler innerHandler, string authorizationScheme = BearerAuthorizationScheme) @@ -47,11 +51,11 @@ public AuthenticatedParameterizedHttpClientHandler(Func - /// Constructor for AuthenticatedParameterizedHttpClientHandler. + /// Initializes a new handler with a custom inner handler, request-specific token provider, and token refresh callback. /// - /// Delegate function to get access token for authentication. - /// Delegate function to refresh access token. - /// Inner handler to send request to. + /// Delegate used to get the access token for the current request. + /// Delegate used to refresh the access token after a response. + /// The next handler in the HTTP pipeline. /// The authorization scheme used for the HTTP request. /// is . public AuthenticatedParameterizedHttpClientHandler(Func> getToken, Func? refreshToken, HttpMessageHandler innerHandler, string authorizationScheme = BearerAuthorizationScheme) @@ -60,12 +64,12 @@ public AuthenticatedParameterizedHttpClientHandler(Func - /// Constructor for AuthenticatedParameterizedHttpClientHandler. + /// Initializes a new handler with full control over token refresh, authorization-header behavior, and the inner handler. /// - /// Delegate function to get access token for authentication. - /// Delegate function to refresh access token. - /// Indicates if there should be an Authorization header. - /// Inner handler for HTTP message request to be sent to. + /// Delegate used to get the access token for the current request. + /// Delegate used to refresh the access token after a response. + /// Indicates whether a token should only be added when the request already has an authorization header. + /// The next handler in the HTTP pipeline. /// The authorization scheme used for the HTTP request. /// is . public AuthenticatedParameterizedHttpClientHandler(Func> getToken, Func? refreshToken, bool checkAuthorizationHeader, HttpMessageHandler innerHandler, string authorizationScheme = BearerAuthorizationScheme) @@ -78,7 +82,7 @@ public AuthenticatedParameterizedHttpClientHandler(Func - /// Calls the function to automatically add the Bearer token and then sends an HTTP request to the inner handler to send to the server as an asynchronous operation. If the response is 401 (Unauthorized), it will try to refresh the token using the handler specified in the constructor and try again. + /// Adds the configured authorization token before sending the request and retries once after refreshing the token when the response is unauthorized. /// /// The HTTP request message to send to the server. /// A cancellation token to cancel operation. diff --git a/src/TinyHelpers/Http/HeaderInjectorHttpClientHandler.cs b/src/TinyHelpers/Http/HeaderInjectorHttpClientHandler.cs index 402296f..35ebae0 100644 --- a/src/TinyHelpers/Http/HeaderInjectorHttpClientHandler.cs +++ b/src/TinyHelpers/Http/HeaderInjectorHttpClientHandler.cs @@ -1,8 +1,12 @@ namespace TinyHelpers.Http; /// -/// Represents a handler for injecting headers in an HTTP request message. +/// Adds request-specific headers to outgoing HTTP requests before they continue through the client pipeline. /// +/// +/// Use this handler to centralize cross-cutting headers such as correlation identifiers, tenant identifiers, or custom +/// API metadata without repeating header setup at every call site. +/// public class HeaderInjectorHttpClientHandler : DelegatingHandler { private readonly Func>> getHeaders; @@ -30,7 +34,7 @@ public HeaderInjectorHttpClientHandler(Func - /// Calls the function to get headers and then sends an HTTP request to the inner handler to send to the server as an asynchronous operation. + /// Resolves headers for the current request, adds them without strict validation, and sends the request to the next handler. /// /// The HTTP request message to send to the server. /// A cancellation token to cancel operation. diff --git a/src/TinyHelpers/Http/QueryStringInjectorHttpClientHandler.cs b/src/TinyHelpers/Http/QueryStringInjectorHttpClientHandler.cs index b3d6f30..4f51e4e 100644 --- a/src/TinyHelpers/Http/QueryStringInjectorHttpClientHandler.cs +++ b/src/TinyHelpers/Http/QueryStringInjectorHttpClientHandler.cs @@ -3,8 +3,12 @@ namespace TinyHelpers.Http; /// -/// Represents a handler for adding query string parameters to an HTTP request message. +/// Adds request-specific query string parameters to outgoing HTTP requests before they continue through the client pipeline. /// +/// +/// Use this handler when query values depend on the current request context and URL composition should be centralized, +/// such as pagination, tenant selection, or feature flags. +/// public class QueryStringInjectorHttpClientHandler : DelegatingHandler { private readonly Func>> getQueryString; @@ -32,7 +36,7 @@ public QueryStringInjectorHttpClientHandler(Func - /// Calls the function to get query string parameters and then sends an HTTP request to the inner handler to send to the server as an asynchronous operation. + /// Merges query string parameters for the current request into the URI and sends the request to the next handler. /// /// The HTTP request message to send to the server. /// A cancellation token to cancel operation. diff --git a/src/TinyHelpers/Json/Serialization/ShortDateConverter.cs b/src/TinyHelpers/Json/Serialization/ShortDateConverter.cs index e1c0a19..e85eb28 100644 --- a/src/TinyHelpers/Json/Serialization/ShortDateConverter.cs +++ b/src/TinyHelpers/Json/Serialization/ShortDateConverter.cs @@ -5,11 +5,12 @@ namespace TinyHelpers.Json.Serialization; /// -/// Converts a value to or from JSON, keeping only the date part. +/// Converts a value to or from JSON while preserving only the date portion of the contract. /// /// /// -/// Initializes a new instance of the class with a specified serialization format. +/// Use this converter when time-of-day information is not part of the JSON contract and clients should exchange a +/// stable date-only representation. /// /// The serialization format to use. The default is yyyy-MM-dd. public class ShortDateConverter(string? serializationFormat) : JsonConverter diff --git a/src/TinyHelpers/Json/Serialization/StringTrimmingConverter.cs b/src/TinyHelpers/Json/Serialization/StringTrimmingConverter.cs index 00926b4..313748a 100644 --- a/src/TinyHelpers/Json/Serialization/StringTrimmingConverter.cs +++ b/src/TinyHelpers/Json/Serialization/StringTrimmingConverter.cs @@ -4,8 +4,12 @@ namespace TinyHelpers.Json.Serialization; /// -/// A converter to trim the whitespace from JSON strings during serialization and deserialization. +/// Trims leading and trailing whitespace from JSON string values during serialization and deserialization. /// +/// +/// Use this converter when the JSON boundary should normalize user-entered text before it reaches the domain model or +/// before values are written back to clients. +/// public class StringTrimmingConverter : JsonConverter { /// diff --git a/src/TinyHelpers/Json/Serialization/TimeSpanTicksConverter.cs b/src/TinyHelpers/Json/Serialization/TimeSpanTicksConverter.cs index f6a4c8b..7b9fff5 100644 --- a/src/TinyHelpers/Json/Serialization/TimeSpanTicksConverter.cs +++ b/src/TinyHelpers/Json/Serialization/TimeSpanTicksConverter.cs @@ -4,7 +4,7 @@ namespace TinyHelpers.Json.Serialization; /// -/// A converter for serializing and deserializing as ticks. +/// Converts values to JSON ticks so durations can be round-tripped without format ambiguity. /// /// public class TimeSpanTicksConverter : JsonConverter diff --git a/src/TinyHelpers/Json/Serialization/UtcDateTimeConverter.cs b/src/TinyHelpers/Json/Serialization/UtcDateTimeConverter.cs index 9b0cab1..7f1b6a5 100644 --- a/src/TinyHelpers/Json/Serialization/UtcDateTimeConverter.cs +++ b/src/TinyHelpers/Json/Serialization/UtcDateTimeConverter.cs @@ -4,11 +4,12 @@ namespace TinyHelpers.Json.Serialization; /// -/// A converter for serializing and deserializing values converting them to UTC, if needed. +/// Converts values to UTC during JSON serialization and deserialization. /// /// /// -/// Initializes a new instance of the class with a specified serialization format. +/// Use this converter when a JSON contract should normalize date-time values to a UTC wire format instead of preserving +/// local offsets or unspecified kinds. /// /// The serialization format to use. The default is yyyy-MM-ddTHH:mm:ss.fffffffZ. /// diff --git a/src/TinyHelpers/Threading/AsyncLock.cs b/src/TinyHelpers/Threading/AsyncLock.cs index 6530f71..393bab8 100644 --- a/src/TinyHelpers/Threading/AsyncLock.cs +++ b/src/TinyHelpers/Threading/AsyncLock.cs @@ -1,8 +1,12 @@ namespace TinyHelpers.Threading; /// -/// Provides a lock that can be used asynchronously. +/// Provides an asynchronous mutual-exclusion primitive for protecting shared state without blocking worker threads. /// +/// +/// Await and dispose the returned instance when the critical section ends. +/// Timed overloads return so callers can handle contention without exceptions. +/// public sealed class AsyncLock : IDisposable #if !NETSTANDARD2_0 , IAsyncDisposable diff --git a/src/TinyHelpers/Threading/LockResult.cs b/src/TinyHelpers/Threading/LockResult.cs index ce8cdea..c22aa6a 100644 --- a/src/TinyHelpers/Threading/LockResult.cs +++ b/src/TinyHelpers/Threading/LockResult.cs @@ -1,17 +1,21 @@ namespace TinyHelpers.Threading; /// -/// Represents the result of an asynchronous lock operation. +/// Represents the outcome of a timed acquisition attempt. /// +/// +/// Timed lock attempts need to distinguish between successful acquisition and timeout without throwing. This value +/// carries both the ownership flag and the lock instance that must be disposed when ownership is granted. +/// public readonly struct LockResult { /// - /// Gets the object if successfully acquired. + /// Gets the instance to dispose when the lock was acquired. /// public AsyncLock? AsyncLock { get; } /// - /// Gets a boolean indicating if the lock was acquired or not. + /// Gets a value indicating whether the lock was acquired before the timeout elapsed. /// public bool IsOwned { get; }