RSCG – ErrorOrX

RSCG – ErrorOrX
 
 

name ErrorOrX
nuget https://www.nuget.org/packages/ErrorOrX/
link https://github.com/ANcpLua/ErrorOrX
author Alexander Nachtmanns

API results from Functional returns of ErroOrX

 

This is how you can use ErrorOrX .

The code that you start with is


<Project Sdk="Microsoft.NET.Sdk.Web">

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

  <ItemGroup>
    <PackageReference Include="ErrorOrX" Version="3.5.0" />
    <PackageReference Include="ErrorOrX.Generators" Version="3.5.0" />
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.2" />
    <PackageReference Include="OpenAPISwaggerUI" Version="9.2024.1215.2209" />
  </ItemGroup>

	<PropertyGroup>
		<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
		<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath>
	</PropertyGroup>
</Project>


The code that you will use is


using OpenAPISwaggerUI;

var builder = WebApplication.CreateBuilder(args);

//instead of this
//builder.Services.AddOpenApi();
builder.Services.AddErrorOrOpenApi();
builder.Services.AddErrorOrEndpoints();
var app = builder.Build();

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

var summaries = new[]
{
    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
})
.WithName("GetWeatherForecast");

app.Run();

internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}



using ErrorOr;

namespace DemoFuncAPI;

public static class PersonAPI
{

    [Get("/todos/{id}")]
    public static ErrorOr<Person> GetById(int id)
    {
        try
        {
            return GetPersonById(id).OrNotFound();
        }
        catch (Exception ex)
        {
            return Error.Failure(description: ex.Message);
        }
    }

    static Person? GetPersonById(int id) =>
    
        id switch
        {
            1 => new Person(1, "John Doe"),
            2 => throw new Exception("person does not exists"),
            _ => null
        };
    
}

public record Person(int Id, string Name)   ;


 

The code that is generated is

// <auto-generated/>
#nullable enable

namespace ErrorOr
{
    /// <summary>
    /// Marks a static method as an ErrorOr endpoint with explicit HTTP method and route.
    /// Prefer using [Get], [Post], [Put], [Delete], or [Patch] for standard HTTP methods.
    /// </summary>
    [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = false)]
    public sealed class ErrorOrEndpointAttribute : global::System.Attribute
    {
        public ErrorOrEndpointAttribute(string httpMethod, string route)
        {
            HttpMethod = httpMethod;
            Route = route;
        }
        public string HttpMethod { get; }
        public string Route { get; }
    }

    [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = false)]
    public sealed class GetAttribute : global::System.Attribute
    {
        public GetAttribute(string route) => Route = route;
        public string Route { get; }
    }

    [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = false)]
    public sealed class PostAttribute : global::System.Attribute
    {
        public PostAttribute(string route) => Route = route;
        public string Route { get; }
    }

    [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = false)]
    public sealed class PutAttribute : global::System.Attribute
    {
        public PutAttribute(string route) => Route = route;
        public string Route { get; }
    }

    [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = false)]
    public sealed class DeleteAttribute : global::System.Attribute
    {
        public DeleteAttribute(string route) => Route = route;
        public string Route { get; }
    }

    [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = false)]
    public sealed class PatchAttribute : global::System.Attribute
    {
        public PatchAttribute(string route) => Route = route;
        public string Route { get; }
    }

    [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)]
    public sealed class ProducesErrorAttribute : global::System.Attribute
    {
        public ProducesErrorAttribute(int statusCode, string errorType)
        {
            StatusCode = statusCode;
            ErrorType = errorType;
        }
        public int StatusCode { get; }
        public string ErrorType { get; }
    }

    [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = false)]
    public sealed class AcceptedResponseAttribute : global::System.Attribute { }

    [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)]
    public sealed class ReturnsErrorAttribute : global::System.Attribute
    {
        public ReturnsErrorAttribute(global::ErrorOr.ErrorType errorType, string errorCode)
        {
            ErrorType = errorType;
            ErrorCode = errorCode;
        }
        public ReturnsErrorAttribute(int statusCode, string errorCode)
        {
            StatusCode = statusCode;
            ErrorCode = errorCode;
            ErrorType = null;
        }
        public global::ErrorOr.ErrorType? ErrorType { get; }
        public int? StatusCode { get; }
        public string ErrorCode { get; }
    }

    /// <summary>
    /// Marks a class as a route group for versioned API endpoints.
    /// All endpoints in the class will be mapped under the specified path prefix
    /// using the eShop-style NewVersionedApi() pattern when combined with [ApiVersion].
    /// </summary>
    [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = false)]
    public sealed class RouteGroupAttribute : global::System.Attribute
    {
        public RouteGroupAttribute(string path) => Path = path;
        public string Path { get; }
        public string? ApiName { get; set; }
    }
}
// <auto-generated/>
// This file was auto-generated by ErrorOr.Generators.
// Do not modify this file directly.

#nullable enable
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;

namespace ErrorOr.Generated
{
    /// <summary>
    /// Generated endpoint mappings for all [ErrorOrEndpoint] handlers in this assembly.
    /// </summary>
    public static class ErrorOrEndpointMappings
    {
        /// <summary>
        /// Maps all ErrorOr endpoints to the application's routing table.
        /// </summary>
        /// <param name="app">The endpoint route builder to add mappings to.</param>
        /// <returns>A convention builder for applying global conventions to all endpoints.</returns>
        /// <exception cref="InvalidOperationException">
        /// Thrown when AddErrorOrEndpoints() was not called during service registration.
        /// </exception>
        /// <remarks>
        /// This follows ASP.NET Core's convention builder pattern, enabling global
        /// endpoint configuration like RequireAuthorization() or RequireRateLimiting().
        /// </remarks>
        /// <example>
        /// <code>
        /// app.MapErrorOrEndpoints()
        ///    .RequireAuthorization()
        ///    .RequireRateLimiting("api");
        /// </code>
        /// </example>
        public static IEndpointConventionBuilder MapErrorOrEndpoints(this IEndpointRouteBuilder app)
        {
            // Validate that AddErrorOrEndpoints() was called
            var marker = app.ServiceProvider.GetService<ErrorOrEndpointsMarkerService>();
            if (marker is null)
            {
                throw new InvalidOperationException(
                    "Unable to find the required services. " +
                    "Please add all the required services by calling 'IServiceCollection.AddErrorOrEndpoints()' " +
                    "in the application startup code.");
            }

            var __endpointBuilders = new System.Collections.Generic.List<IEndpointConventionBuilder>();

            // GET /todos/{id} -> global::DemoFuncAPI.PersonAPI.GetById
            var __ep0 = app.MapGet(@"/todos/{id}", (Delegate)Invoke_Ep0)
                .WithName("DemoFuncAPI_PersonAPI_GetById")
                .WithTags("PersonAPI")
                .WithMetadata(new global::Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata(200, typeof(global::DemoFuncAPI.Person), new[] { "application/json" }))
                .WithMetadata(new global::Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata(400, typeof(global::Microsoft.AspNetCore.Http.HttpValidationProblemDetails), new[] { "application/problem+json" }))
                .WithMetadata(new global::Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata(500, typeof(global::Microsoft.AspNetCore.Mvc.ProblemDetails), new[] { "application/problem+json" }))
                ;
            __endpointBuilders.Add(__ep0);


            return new CompositeEndpointConventionBuilder(__endpointBuilders);
        }

        /// <summary>
        /// Registers ErrorOr endpoint services and returns a builder for configuration.
        /// </summary>
        /// <param name="services">The service collection to configure.</param>
        /// <returns>A builder for further configuration.</returns>
        /// <remarks>
        /// This follows ASP.NET Core's builder pattern (like AddRazorComponents())
        /// enabling fluent extension method chaining without callback nesting.
        /// </remarks>
        /// <example>
        /// <code>
        /// builder.Services.AddErrorOrEndpoints()
        ///     .UseJsonContext&lt;AppJsonSerializerContext&gt;()
        ///     .WithCamelCase()
        ///     .WithIgnoreNulls();
        /// </code>
        /// </example>
        public static IErrorOrEndpointsBuilder AddErrorOrEndpoints(this IServiceCollection services)
        {
            // Register marker service for validation in MapErrorOrEndpoints()
            services.AddSingleton<ErrorOrEndpointsMarkerService>();
            return new ErrorOrEndpointsBuilder(services);
        }

        private static async Task<global::Microsoft.AspNetCore.Http.HttpResults.Results<global::Microsoft.AspNetCore.Http.HttpResults.Ok<global::DemoFuncAPI.Person>, global::Microsoft.AspNetCore.Http.HttpResults.BadRequest<global::Microsoft.AspNetCore.Mvc.ProblemDetails>, global::Microsoft.AspNetCore.Http.HttpResults.InternalServerError<global::Microsoft.AspNetCore.Mvc.ProblemDetails>>> Invoke_Ep0(HttpContext ctx)
        {
            return await Invoke_Ep0_Core(ctx);
        }

        private static Task<global::Microsoft.AspNetCore.Http.HttpResults.Results<global::Microsoft.AspNetCore.Http.HttpResults.Ok<global::DemoFuncAPI.Person>, global::Microsoft.AspNetCore.Http.HttpResults.BadRequest<global::Microsoft.AspNetCore.Mvc.ProblemDetails>, global::Microsoft.AspNetCore.Http.HttpResults.InternalServerError<global::Microsoft.AspNetCore.Mvc.ProblemDetails>>> Invoke_Ep0_Core(HttpContext ctx)
        {
            static global::Microsoft.AspNetCore.Mvc.ProblemDetails CreateBindProblem(string param, string reason) => new()
            {
                Title = "Bad Request",
                Detail = $"Parameter '{param}' {reason}.",
                Status = 400,
                Type = "https://httpstatuses.io/400",
            };

            static Task<global::Microsoft.AspNetCore.Http.HttpResults.Results<global::Microsoft.AspNetCore.Http.HttpResults.Ok<global::DemoFuncAPI.Person>, global::Microsoft.AspNetCore.Http.HttpResults.BadRequest<global::Microsoft.AspNetCore.Mvc.ProblemDetails>, global::Microsoft.AspNetCore.Http.HttpResults.InternalServerError<global::Microsoft.AspNetCore.Mvc.ProblemDetails>>> BindFail(string param, string reason)
                => Task.FromResult<global::Microsoft.AspNetCore.Http.HttpResults.Results<global::Microsoft.AspNetCore.Http.HttpResults.Ok<global::DemoFuncAPI.Person>, global::Microsoft.AspNetCore.Http.HttpResults.BadRequest<global::Microsoft.AspNetCore.Mvc.ProblemDetails>, global::Microsoft.AspNetCore.Http.HttpResults.InternalServerError<global::Microsoft.AspNetCore.Mvc.ProblemDetails>>>(global::Microsoft.AspNetCore.Http.TypedResults.BadRequest(CreateBindProblem(param, reason)));

            if (!TryGetRouteValue(ctx, "id", out var p0Raw) || !int.TryParse(p0Raw, out var p0)) return BindFail("id", "has invalid format");
            var result = global::DemoFuncAPI.PersonAPI.GetById(p0);
            if (result.IsError)
            {
                if (result.Errors.Count is 0) return Task.FromResult<global::Microsoft.AspNetCore.Http.HttpResults.Results<global::Microsoft.AspNetCore.Http.HttpResults.Ok<global::DemoFuncAPI.Person>, global::Microsoft.AspNetCore.Http.HttpResults.BadRequest<global::Microsoft.AspNetCore.Mvc.ProblemDetails>, global::Microsoft.AspNetCore.Http.HttpResults.InternalServerError<global::Microsoft.AspNetCore.Mvc.ProblemDetails>>>(global::Microsoft.AspNetCore.Http.TypedResults.InternalServerError(new global::Microsoft.AspNetCore.Mvc.ProblemDetails { Title = "Error", Detail = "An error occurred but no details were provided.", Status = 500 }));
                var first = result.Errors[0];
                var problem = new global::Microsoft.AspNetCore.Mvc.ProblemDetails
                {
                    Title = first.Code,
                    Detail = first.Description,
                    Status = first.Type switch { global::ErrorOr.ErrorType.Validation => 400, global::ErrorOr.ErrorType.Unauthorized => 401, global::ErrorOr.ErrorType.Forbidden => 403, global::ErrorOr.ErrorType.NotFound => 404, global::ErrorOr.ErrorType.Conflict => 409, global::ErrorOr.ErrorType.Failure => 500, global::ErrorOr.ErrorType.Unexpected => 500, _ => (int)first.Type is >= 100 and <= 599 ? (int)first.Type : 500 }
                };
                problem.Type = $"https://httpstatuses.io/{problem.Status}";

                switch (first.Type)
                {
                    case global::ErrorOr.ErrorType.Failure:
                        return Task.FromResult<global::Microsoft.AspNetCore.Http.HttpResults.Results<global::Microsoft.AspNetCore.Http.HttpResults.Ok<global::DemoFuncAPI.Person>, global::Microsoft.AspNetCore.Http.HttpResults.BadRequest<global::Microsoft.AspNetCore.Mvc.ProblemDetails>, global::Microsoft.AspNetCore.Http.HttpResults.InternalServerError<global::Microsoft.AspNetCore.Mvc.ProblemDetails>>>(global::Microsoft.AspNetCore.Http.TypedResults.InternalServerError(problem));
                    default:
                        return Task.FromResult<global::Microsoft.AspNetCore.Http.HttpResults.Results<global::Microsoft.AspNetCore.Http.HttpResults.Ok<global::DemoFuncAPI.Person>, global::Microsoft.AspNetCore.Http.HttpResults.BadRequest<global::Microsoft.AspNetCore.Mvc.ProblemDetails>, global::Microsoft.AspNetCore.Http.HttpResults.InternalServerError<global::Microsoft.AspNetCore.Mvc.ProblemDetails>>>(global::Microsoft.AspNetCore.Http.TypedResults.InternalServerError(problem));
                }
            }

            return Task.FromResult<global::Microsoft.AspNetCore.Http.HttpResults.Results<global::Microsoft.AspNetCore.Http.HttpResults.Ok<global::DemoFuncAPI.Person>, global::Microsoft.AspNetCore.Http.HttpResults.BadRequest<global::Microsoft.AspNetCore.Mvc.ProblemDetails>, global::Microsoft.AspNetCore.Http.HttpResults.InternalServerError<global::Microsoft.AspNetCore.Mvc.ProblemDetails>>>(global::Microsoft.AspNetCore.Http.TypedResults.Ok(result.Value));
        }

        private static bool TryGetRouteValue(HttpContext ctx, string name, out string? value)
        {
            if (!ctx.Request.RouteValues.TryGetValue(name, out var raw) || raw is null) { value = null; return false; }
            value = raw.ToString(); return value is not null;
        }

        private static bool TryGetQueryValue(HttpContext ctx, string name, out string? value)
        {
            if (!ctx.Request.Query.TryGetValue(name, out var raw) || raw.Count is 0) { value = null; return false; }
            value = raw.ToString(); return value is not null;
        }

        private static global::Microsoft.AspNetCore.Http.IResult ToProblem(global::System.Collections.Generic.IReadOnlyList<global::ErrorOr.Error> errors)
        {
            if (errors.Count is 0) return global::Microsoft.AspNetCore.Http.TypedResults.Problem();
            var hasValidation = false;
            for (var i = 0; i < errors.Count; i++) if (errors&#91;i&#93;.Type == global::ErrorOr.ErrorType.Validation) { hasValidation = true; break; }
            if (hasValidation)
            {
                var dict = new global::System.Collections.Generic.Dictionary<string, string&#91;&#93;>();
                foreach (var e in errors)
                {
                    if (e.Type != global::ErrorOr.ErrorType.Validation) continue;
                    if (!dict.TryGetValue(e.Code, out var existing))
                        dict[e.Code] = new[] { e.Description };
                    else
                    {
                        var arr = new string[existing.Length + 1];
                        existing.CopyTo(arr, 0);
                        arr[existing.Length] = e.Description;
                        dict[e.Code] = arr;
                    }
                }
                return global::Microsoft.AspNetCore.Http.TypedResults.ValidationProblem(dict);
            }
            var first = errors[0];
            var problem = new global::Microsoft.AspNetCore.Mvc.ProblemDetails
            {
                Title = first.Code,
                Detail = first.Description,
                Status = first.Type switch { global::ErrorOr.ErrorType.Validation => 400, global::ErrorOr.ErrorType.Unauthorized => 401, global::ErrorOr.ErrorType.Forbidden => 403, global::ErrorOr.ErrorType.NotFound => 404, global::ErrorOr.ErrorType.Conflict => 409, global::ErrorOr.ErrorType.Failure => 500, global::ErrorOr.ErrorType.Unexpected => 500, _ => (int)first.Type is >= 100 and <= 599 ? (int)first.Type : 500 }
            };
            problem.Type = $"https://httpstatuses.io/{problem.Status}";
            return problem.Status switch
            {
                400 => global::Microsoft.AspNetCore.Http.TypedResults.BadRequest(problem),
                401 => global::Microsoft.AspNetCore.Http.TypedResults.Unauthorized(),
                403 => global::Microsoft.AspNetCore.Http.TypedResults.Forbid(),
                404 => global::Microsoft.AspNetCore.Http.TypedResults.NotFound(problem),
                409 => global::Microsoft.AspNetCore.Http.TypedResults.Conflict(problem),
                422 => global::Microsoft.AspNetCore.Http.TypedResults.UnprocessableEntity(problem),
                500 => global::Microsoft.AspNetCore.Http.TypedResults.InternalServerError(problem),
                _ => global::Microsoft.AspNetCore.Http.TypedResults.Problem(detail: first.Description, statusCode: problem.Status ?? 500, title: first.Code, type: problem.Type)
            };
        }
    }
}

// <auto-generated>
// This file was generated by ErrorOr.Generators source generator.
// </auto-generated>

#nullable enable

namespace ErrorOr.Generated
{
    /// <summary>
    /// Marker service to verify that AddErrorOrEndpoints() was called.
    /// </summary>
    /// <remarks>
    /// This follows the ASP.NET Core pattern used by RazorComponentsMarkerService
    /// to provide clear error messages when the service registration is missing.
    /// </remarks>
    internal sealed class ErrorOrEndpointsMarkerService { }

    /// <summary>
    /// Builder interface for configuring ErrorOr endpoints.
    /// </summary>
    /// <remarks>
    /// This pattern follows ASP.NET Core's IRazorComponentsBuilder design,
    /// enabling fluent extension method chaining without callback nesting.
    /// </remarks>
    public interface IErrorOrEndpointsBuilder
    {
        /// <summary>
        /// Gets the service collection being configured.
        /// </summary>
        global::Microsoft.Extensions.DependencyInjection.IServiceCollection Services { get; }
    }

    /// <summary>
    /// Default implementation of <see cref="IErrorOrEndpointsBuilder"/>.
    /// </summary>
    internal sealed class ErrorOrEndpointsBuilder : IErrorOrEndpointsBuilder
    {
        public ErrorOrEndpointsBuilder(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services)
        {
            Services = services;
        }

        public global::Microsoft.Extensions.DependencyInjection.IServiceCollection Services { get; }
    }

    /// <summary>
    /// Extension methods for <see cref="IErrorOrEndpointsBuilder"/>.
    /// </summary>
    public static class ErrorOrEndpointsBuilderExtensions
    {
        /// <summary>
        /// Registers a JsonSerializerContext for AOT-compatible JSON serialization.
        /// </summary>
        /// <typeparam name="TContext">The JsonSerializerContext type.</typeparam>
        /// <param name="builder">The builder instance.</param>
        /// <returns>The builder instance for chaining.</returns>
        public static IErrorOrEndpointsBuilder UseJsonContext<TContext>(this IErrorOrEndpointsBuilder builder)
            where TContext : global::System.Text.Json.Serialization.JsonSerializerContext, new()
        {
            builder.Services.ConfigureHttpJsonOptions(options =>
            {
                options.SerializerOptions.TypeInfoResolverChain.Insert(0, new TContext());
            });
            return builder;
        }

        /// <summary>
        /// Uses camelCase for JSON property names.
        /// </summary>
        /// <param name="builder">The builder instance.</param>
        /// <param name="enabled">Whether to enable camelCase (default: true).</param>
        /// <returns>The builder instance for chaining.</returns>
        public static IErrorOrEndpointsBuilder WithCamelCase(this IErrorOrEndpointsBuilder builder, bool enabled = true)
        {
            if (enabled)
            {
                builder.Services.ConfigureHttpJsonOptions(options =>
                {
                    options.SerializerOptions.PropertyNamingPolicy = global::System.Text.Json.JsonNamingPolicy.CamelCase;
                });
            }
            return builder;
        }

        /// <summary>
        /// Ignores null values when serializing JSON.
        /// </summary>
        /// <param name="builder">The builder instance.</param>
        /// <param name="enabled">Whether to ignore nulls (default: true).</param>
        /// <returns>The builder instance for chaining.</returns>
        public static IErrorOrEndpointsBuilder WithIgnoreNulls(this IErrorOrEndpointsBuilder builder, bool enabled = true)
        {
            if (enabled)
            {
                builder.Services.ConfigureHttpJsonOptions(options =>
                {
                    options.SerializerOptions.DefaultIgnoreCondition = global::System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull;
                });
            }
            return builder;
        }
    }

    /// <summary>
    /// Composite convention builder that applies conventions to multiple endpoints.
    /// </summary>
    /// <remarks>
    /// This follows the ASP.NET Core pattern for applying global conventions
    /// to all endpoints registered by MapErrorOrEndpoints().
    /// </remarks>
    internal sealed class CompositeEndpointConventionBuilder : global::Microsoft.AspNetCore.Builder.IEndpointConventionBuilder
    {
        private readonly global::System.Collections.Generic.List<global::Microsoft.AspNetCore.Builder.IEndpointConventionBuilder> _builders;

        public CompositeEndpointConventionBuilder(global::System.Collections.Generic.List<global::Microsoft.AspNetCore.Builder.IEndpointConventionBuilder> builders)
        {
            _builders = builders;
        }

        public void Add(global::System.Action<global::Microsoft.AspNetCore.Builder.EndpointBuilder> convention)
        {
            foreach (var builder in _builders)
            {
                builder.Add(convention);
            }
        }

        public void Finally(global::System.Action<global::Microsoft.AspNetCore.Builder.EndpointBuilder> finallyConvention)
        {
            foreach (var builder in _builders)
            {
                builder.Finally(finallyConvention);
            }
        }
    }
}
// <auto-generated/>
global using ErrorOr.Generated;
// <auto-generated/>
#nullable enable

using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi;

namespace ErrorOr.Generated;

/// <summary>
/// Document transformer for tag: PersonAPI
/// Generated from: [ErrorOrEndpoint] attribute on *PersonAPIEndpoints class
/// </summary>
file sealed class Tag_PersonAPI_Transformer : IOpenApiDocumentTransformer
{
    public Task TransformAsync(
        OpenApiDocument document,
        OpenApiDocumentTransformerContext context,
        CancellationToken cancellationToken)
    {
        document.Tags ??= new HashSet<OpenApiTag>();
        document.Tags.Add(new OpenApiTag { Name = "PersonAPI" });
        return Task.CompletedTask;
    }
}

/// <summary>
/// Operation transformer that applies XML documentation and parameter definitions to operations.
/// Each entry is a strict 1:1 mapping from handler signature to operation metadata.
/// </summary>
file sealed class XmlDocOperationTransformer : IOpenApiOperationTransformer
{
    // Pre-computed metadata from XML docs (compile-time extraction)
    private static readonly FrozenDictionary<string, (string? Summary, string? Description)> OperationDocs =
        new Dictionary<string, (string? Summary, string? Description)>
        {
        }.ToFrozenDictionary(StringComparer.Ordinal);

    // Pre-computed parameter descriptions from XML <param> tags
    private static readonly FrozenDictionary<string, FrozenDictionary<string, string>> ParameterDocs =
        new Dictionary<string, FrozenDictionary<string, string>>
        {
        }.ToFrozenDictionary(StringComparer.Ordinal);

    // Pre-computed parameter definitions from handler signatures
    private static readonly FrozenDictionary<string, (string Name, ParameterLocation Location, bool Required, JsonSchemaType SchemaType, string? SchemaFormat)&#91;&#93;> ParameterDefs =
        new Dictionary<string, (string, ParameterLocation, bool, JsonSchemaType, string?)&#91;&#93;>
        {
            ["DemoFuncAPI_PersonAPI_GetById"] = [("id", ParameterLocation.Path, true, JsonSchemaType.Integer, "int32")],
        }.ToFrozenDictionary(StringComparer.Ordinal);

    public Task TransformAsync(
        OpenApiOperation operation,
        OpenApiOperationTransformerContext context,
        CancellationToken cancellationToken)
    {
        string? operationId = null;
        var metadata = context.Description.ActionDescriptor?.EndpointMetadata;
        if (metadata is not null)
        {
            for (var i = 0; i < metadata.Count; i++)
            {
                if (metadata&#91;i&#93; is IEndpointNameMetadata nameMetadata)
                {
                    operationId = nameMetadata.EndpointName;
                    break;
                }
            }
        }

        if (operationId is null)
            return Task.CompletedTask;

        // Apply summary and description
        if (OperationDocs.TryGetValue(operationId, out var docs))
        {
            if (docs.Summary is not null)
                operation.Summary ??= docs.Summary;
            if (docs.Description is not null)
                operation.Description ??= docs.Description;
        }

        // Add parameter definitions from handler signatures
        if (ParameterDefs.TryGetValue(operationId, out var paramDefs))
        {
            operation.Parameters ??= &#91;&#93;;
            foreach (var (pName, pLocation, pRequired, pSchemaType, pSchemaFormat) in paramDefs)
            {
                var schema = new OpenApiSchema { Type = pSchemaType };
                if (pSchemaFormat is not null) schema.Format = pSchemaFormat;
                operation.Parameters.Add(new OpenApiParameter
                {
                    Name = pName,
                    In = pLocation,
                    Required = pRequired,
                    Schema = schema
                });
            }
        }

        // Apply parameter descriptions
        if (ParameterDocs.TryGetValue(operationId, out var paramDocs) && operation.Parameters is not null)
        {
            foreach (var param in operation.Parameters)
            {
                if (param.Name is not null && paramDocs.TryGetValue(param.Name, out var paramDesc))
                {
                    param.Description ??= paramDesc;
                }
            }
        }

        return Task.CompletedTask;
    }
}

/// <summary>
/// Schema transformer that applies type XML documentation to schemas.
/// Each entry is a strict 1:1 mapping from XML doc to schema description.
/// AOT-safe: Uses Type as dictionary key (no runtime reflection).
/// </summary>
file sealed class XmlDocSchemaTransformer : IOpenApiSchemaTransformer
{
    // Pre-computed type descriptions from XML docs (AOT-safe: Type keys resolved at compile-time)
    private static readonly FrozenDictionary<Type, string> TypeDescriptions =
        new Dictionary<Type, string>
        {
            [typeof(global::ErrorOr.ErrorOrEndpointAttribute)] = "Marks a static method as an ErrorOr endpoint with explicit HTTP method and route.     Prefer using [Get], [Post], [Put], [Delete], or [Patch] for standard HTTP methods.",
            [typeof(global::ErrorOr.RouteGroupAttribute)] = "Marks a class as a route group for versioned API endpoints.     All endpoints in the class will be mapped under the specified path prefix     using the eShop-style NewVersionedApi() pattern when combined with [ApiVersion].",
        }.ToFrozenDictionary();

    public Task TransformAsync(
        OpenApiSchema schema,
        OpenApiSchemaTransformerContext context,
        CancellationToken cancellationToken)
    {
        var type = context.JsonTypeInfo.Type;
        // For generic types, lookup the generic type definition
        var lookupType = type.IsGenericType ? type.GetGenericTypeDefinition() : type;
        if (TypeDescriptions.TryGetValue(lookupType, out var description))
        {
            schema.Description ??= description;
        }
        return Task.CompletedTask;
    }
}

/// <summary>
/// Extension methods for registering generated OpenAPI transformers.
/// </summary>
public static class GeneratedOpenApiExtensions
{
    /// <summary>
    /// Adds OpenAPI with generated transformers for ErrorOr endpoints.
    /// Each transformer is registered following the strict 1:1 mapping rule.
    /// </summary>
    public static IServiceCollection AddErrorOrOpenApi(
        this IServiceCollection services,
        string documentName = "v1")
    {
        services.AddOpenApi(documentName, options =>
        {
            // Tag: PersonAPI
            options.AddDocumentTransformer(new Tag_PersonAPI_Transformer());

            // XML doc summaries → operation metadata
            options.AddOperationTransformer(new XmlDocOperationTransformer());

            // XML doc summaries → schema descriptions
            options.AddSchemaTransformer(new XmlDocSchemaTransformer());
        });

        return services;
    }
}

Code and pdf at

https://ignatandrei.github.io/RSCG_Examples/v2/docs/ErrorOrX


Posted

in

, ,

by

Tags: