From 4df903e45621ac96c749fa63635ed1033ebef260 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Wed, 3 Jun 2026 00:20:46 +0200 Subject: [PATCH] Added shared api feature --- LiteCharms.Features/Abstractions/IEndpoint.cs | 6 ++ .../Api/ApiVersionTargetAttribute.cs | 7 ++ .../OpenApiBearerSecuritySchemeTransformer.cs | 16 ++++ .../Email/Models/EmailEnquiryModel.cs | 8 +- LiteCharms.Features/Extensions/Api.cs | 96 +++++++++++++++++++ .../LiteCharms.Features.csproj | 23 +++++ 6 files changed, 152 insertions(+), 4 deletions(-) create mode 100644 LiteCharms.Features/Abstractions/IEndpoint.cs create mode 100644 LiteCharms.Features/Api/ApiVersionTargetAttribute.cs create mode 100644 LiteCharms.Features/Api/OpenApiBearerSecuritySchemeTransformer.cs create mode 100644 LiteCharms.Features/Extensions/Api.cs diff --git a/LiteCharms.Features/Abstractions/IEndpoint.cs b/LiteCharms.Features/Abstractions/IEndpoint.cs new file mode 100644 index 0000000..25bb977 --- /dev/null +++ b/LiteCharms.Features/Abstractions/IEndpoint.cs @@ -0,0 +1,6 @@ +namespace LiteCharms.Features.Abstractions; + +public interface IEndpoint +{ + void Map(IEndpointRouteBuilder builder); +} diff --git a/LiteCharms.Features/Api/ApiVersionTargetAttribute.cs b/LiteCharms.Features/Api/ApiVersionTargetAttribute.cs new file mode 100644 index 0000000..63d1598 --- /dev/null +++ b/LiteCharms.Features/Api/ApiVersionTargetAttribute.cs @@ -0,0 +1,7 @@ +namespace LiteCharms.Features.Api; + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +public sealed class ApiVersionTargetAttribute(int majorVersion) : Attribute +{ + public int MajorVersion { get; } = majorVersion; +} diff --git a/LiteCharms.Features/Api/OpenApiBearerSecuritySchemeTransformer.cs b/LiteCharms.Features/Api/OpenApiBearerSecuritySchemeTransformer.cs new file mode 100644 index 0000000..41ef59a --- /dev/null +++ b/LiteCharms.Features/Api/OpenApiBearerSecuritySchemeTransformer.cs @@ -0,0 +1,16 @@ +namespace LiteCharms.Features.Api; + +public sealed class OpenApiBearerSecuritySchemeTransformer : IOpenApiDocumentTransformer +{ + public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken) + { + var bearerScheme = new OpenApiSecurityScheme + { + Type = SecuritySchemeType.Http, + Scheme = "bearer", + Description = "JWT Authorization header using the Bearer scheme. Example: \"Bearer {token}\"", + }; + + document.AddComponent("Bearer", bearerScheme); + } +} diff --git a/LiteCharms.Features/Email/Models/EmailEnquiryModel.cs b/LiteCharms.Features/Email/Models/EmailEnquiryModel.cs index b13e3a7..499f9c1 100644 --- a/LiteCharms.Features/Email/Models/EmailEnquiryModel.cs +++ b/LiteCharms.Features/Email/Models/EmailEnquiryModel.cs @@ -7,25 +7,25 @@ public sealed class EmailEnquiryModel [Required] [MinLength(2)] [MaxLength(255)] - [Display(Name = "Full Name")] + [System.ComponentModel.DataAnnotations.Display(Name = "Full Name")] public string? FullName { get; set; } [Required] [EmailAddress] [MinLength(5)] [MaxLength(255)] - [Display(Name = "Email Address")] + [System.ComponentModel.DataAnnotations.Display(Name = "Email Address")] public string? EmailAddress { get; set; } [Required] [MinLength(2)] [MaxLength(255)] - [Display(Name = "Subject")] + [System.ComponentModel.DataAnnotations.Display(Name = "Subject")] public string? EmailSubject { get; set; } [Required] [MinLength(2)] [MaxLength(2000)] - [Display(Name = "Message")] + [System.ComponentModel.DataAnnotations.Display(Name = "Message")] public string? Message { get; set; } } diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs new file mode 100644 index 0000000..df8fe23 --- /dev/null +++ b/LiteCharms.Features/Extensions/Api.cs @@ -0,0 +1,96 @@ +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.Api; + +namespace LiteCharms.Features.Extensions; + +public static class Api +{ + public const string Books = nameof(Books); + public const string Payments = nameof(Payments); + + public static IApplicationBuilder MapEndpoints(this WebApplication app, IDictionary versionGroups) + { + var endpoints = app.Services.GetRequiredService>(); + + foreach (var endpoint in endpoints) + { + var versionAttributes = endpoint.GetType().GetCustomAttributes().ToList(); + + if (versionAttributes.Count != 0) + { + foreach (var attr in versionAttributes) + if (versionGroups.TryGetValue(attr.MajorVersion, out var targetGroup)) + endpoint.Map(targetGroup); + } + else + endpoint.Map(app); + } + + return app; + } + + public static IServiceCollection AddEndpoints(this IServiceCollection services, Assembly assembly) + { + ServiceDescriptor[] discriptors = [.. assembly.DefinedTypes + .Where(t => t is { IsInterface: false, IsAbstract: false }) + .Where(t => t.IsAssignableTo(typeof(IEndpoint))) + .Select(t => ServiceDescriptor.Transient(typeof(IEndpoint), t))]; + + services.TryAddEnumerable(discriptors); + + return services; + } + + public static string ToEndpointName(this Type target, string? annotation = "") => + $"{target.Name.Replace("Endpoint", string.Empty)}{annotation}".ToLower(CultureInfo.CurrentCulture); + + public static IServiceCollection AddApiServices(this IServiceCollection services, IConfiguration configuration) + { + services.AddHttpClient(); + + services.AddApiVersioning(options => + { + options.DefaultApiVersion = new ApiVersion(1); + options.ReportApiVersions = true; + options.AssumeDefaultVersionWhenUnspecified = true; + options.ApiVersionReader = ApiVersionReader.Combine(new UrlSegmentApiVersionReader(), + new QueryStringApiVersionReader("version"), + new QueryStringApiVersionReader("version"), + new MediaTypeApiVersionReader("version")); + }) + .AddApiExplorer(options => + { + options.GroupNameFormat = "'v'VVV"; + options.SubstituteApiVersionInUrl = true; + }); + + var urls = configuration["ASPNETCORE_URLS"] ?? configuration["Urls"]; + var healthUrl = "http://localhost:8080/health"; + + if (!string.IsNullOrWhiteSpace(urls)) + { + string firstUrl = urls.Split(';').FirstOrDefault(s => s.Contains("http://"))! + .Replace("*", "localhost").Replace("+", "localhost"); + + healthUrl = $"{firstUrl.TrimEnd('/')}/health"; + } + + services.AddHealthChecksUI(setup => + { + setup.SetNotifyUnHealthyOneTimeUntilChange(); + setup.AddHealthCheckEndpoint("primary, heal", healthUrl); + setup.SetHeaderText("Midrand Books"); + }) + .AddInMemoryStorage(); + + services.AddOutputCache(options => + { + options.AddBasePolicy(builder => builder.Cache()); + options.DefaultExpirationTimeSpan = TimeSpan.FromSeconds(10); + }); + + services.AddOpenApi(options => options.AddDocumentTransformer()); + + return services; + } +} diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index 8e12ef0..dd9ff14 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -29,6 +29,26 @@ + + + + + + + + + + + + + + + + + + + + @@ -147,6 +167,9 @@ + + +