Remove FluentValidation; validator/DI cleanup #7

Merged
wamplerj merged 6 commits from fluent-validator-gone into main 2026-07-05 22:54:31 -07:00
14 changed files with 235 additions and 96 deletions
Showing only changes of commit 7b51100ab6 - Show all commits

View File

@@ -0,0 +1,12 @@
namespace MicCheck.Api.Audit;
public static class DependencyRegistration
{
public static IServiceCollection AddAuditServices(this IServiceCollection services)
{
services.AddScoped<IAuditService, AuditService>();
services.AddScoped<AuditLogQueryService>();
return services;
}
}

View File

@@ -0,0 +1,65 @@
using System.Text;
using MicCheck.Api.Common.Security.ApiKeys;
using MicCheck.Api.Common.Security.Authentication;
using MicCheck.Api.Common.Security.Authorization;
using MicCheck.Api.Common.Validation;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
namespace MicCheck.Api.Common;
public static class DependencyRegistration
{
public static IServiceCollection AddCommonServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddAuthentication()
.AddScheme<AuthenticationSchemeOptions, EnvironmentKeyAuthenticationHandler>(
EnvironmentKeyAuthenticationHandler.SchemeName, _ => { })
.AddScheme<AuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(
ApiKeyAuthenticationHandler.SchemeName, _ => { })
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateIssuerSigningKey = true,
ValidIssuer = configuration["Jwt:Issuer"],
ValidAudience = configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(configuration["Jwt:SecretKey"]!))
};
});
services.AddAuthorization(options =>
{
options.AddPolicy(AuthorizationPolicies.FlagsApiAccess, policy =>
policy.AddAuthenticationSchemes(EnvironmentKeyAuthenticationHandler.SchemeName)
.RequireClaim("EnvironmentId"));
options.AddPolicy(AuthorizationPolicies.AdminApiAccess, policy =>
policy.AddAuthenticationSchemes(ApiKeyAuthenticationHandler.SchemeName, JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser());
options.AddPolicy(AuthorizationPolicies.OrganizationAdmin, policy =>
policy.AddAuthenticationSchemes(ApiKeyAuthenticationHandler.SchemeName, JwtBearerDefaults.AuthenticationScheme)
.RequireClaim("OrganizationRole", "Admin"));
});
services.AddScoped<ITokenService, TokenService>();
services.AddScoped<AuthService>();
services.AddScoped<ApiKeyService>();
services.AddScoped<IAuthorizationHandler, ProjectPermissionRequirementHandler>();
services.AddModelValidatorsFromAssemblyContaining<Program>();
services.Configure<ApiBehaviorOptions>(options =>
{
options.InvalidModelStateResponseFactory = context => ValidationProblemResponseFactory.Create(context.ModelState);
});
return services;
}
}

View File

@@ -0,0 +1,21 @@
using Microsoft.EntityFrameworkCore;
namespace MicCheck.Api.Data;
public static class DependencyRegistration
{
public static IServiceCollection AddDataServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddScoped<DatabaseSeeder>();
var connectionString = configuration.GetConnectionString("miccheck")
?? (System.Environment.GetEnvironmentVariable("DATABASE_URL") is { } databaseUrl
? DatabaseUrlParser.ToNpgsqlConnectionString(databaseUrl)
: configuration.GetConnectionString("DefaultConnection")!);
services.AddDbContext<MicCheckDbContext>(options => options.UseNpgsql(connectionString));
services.AddScoped<IMicCheckDbContext>(sp => sp.GetRequiredService<MicCheckDbContext>());
return services;
}
}

View File

@@ -0,0 +1,12 @@
namespace MicCheck.Api.Environments;
public static class DependencyRegistration
{
public static IServiceCollection AddEnvironmentsServices(this IServiceCollection services)
{
services.AddScoped<EnvironmentService>();
services.AddScoped<EnvironmentDocumentService>();
return services;
}
}

View File

@@ -0,0 +1,20 @@
using MicCheck.Api.Features.Usage;
namespace MicCheck.Api.Features;
public static class DependencyRegistration
{
public static IServiceCollection AddFeaturesServices(this IServiceCollection services)
{
services.AddScoped<FeatureEvaluationService>();
services.AddSingleton<FlagCache>();
services.AddScoped<FeatureService>();
services.AddScoped<FeatureStateService>();
services.AddScoped<FeatureSegmentService>();
services.AddScoped<TagService>();
services.AddFeatureUsageServices();
return services;
}
}

View File

@@ -12,11 +12,9 @@ public class FlagCache(IMemoryCache cache)
return flags;
}
public void Set(int environmentId, IReadOnlyList<FeatureStateResult> flags)
=> cache.Set(CacheKey(environmentId), flags, CacheDuration);
public void Set(int environmentId, IReadOnlyList<FeatureStateResult> flags) => cache.Set(CacheKey(environmentId), flags, CacheDuration);
public void Invalidate(int environmentId)
=> cache.Remove(CacheKey(environmentId));
public void Invalidate(int environmentId) => cache.Remove(CacheKey(environmentId));
private static string CacheKey(int environmentId) => $"flags:{environmentId}";
}

View File

@@ -0,0 +1,13 @@
namespace MicCheck.Api.Features.Usage;
public static class DependencyRegistration
{
public static IServiceCollection AddFeatureUsageServices(this IServiceCollection services)
{
services.AddSingleton<FeatureUsageMetrics>();
services.AddScoped<FeatureUsageQueryService>();
services.AddHostedService<FeatureUsageFlushBackgroundService>();
return services;
}
}

View File

@@ -0,0 +1,12 @@
namespace MicCheck.Api.Identities;
public static class DependencyRegistration
{
public static IServiceCollection AddIdentitiesServices(this IServiceCollection services)
{
services.AddScoped<IdentityResolutionService>();
services.AddScoped<AdminIdentityService>();
return services;
}
}

View File

@@ -0,0 +1,11 @@
namespace MicCheck.Api.Organizations;
public static class DependencyRegistration
{
public static IServiceCollection AddOrganizationsServices(this IServiceCollection services)
{
services.AddScoped<OrganizationService>();
return services;
}
}

View File

@@ -1,28 +1,19 @@
using System.Text;
using System.Threading.RateLimiting;
using MicCheck.Api.Common.Security.ApiKeys;
using MicCheck.Api.Audit;
using MicCheck.Api.Common.Security.Authentication;
using MicCheck.Api.Common;
using MicCheck.Api.Common.Security.ApiKeys;
using MicCheck.Api.Common.Security.Authorization;
using MicCheck.Api.Common.Validation;
using MicCheck.Api.Data;
using MicCheck.Api.Environments;
using MicCheck.Api.Features;
using MicCheck.Api.Features.Usage;
using MicCheck.Api.Identities;
using MicCheck.Api.Organizations;
using MicCheck.Api.Projects;
using MicCheck.Api.Segments;
using MicCheck.Api.Users;
using MicCheck.Api.Webhooks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Scalar.AspNetCore;
using Serilog;
@@ -47,39 +38,7 @@ try
options.JsonSerializerOptions.Converters.Add(
new System.Text.Json.Serialization.JsonStringEnumConverter()));
builder.Services.AddAuthentication()
.AddScheme<AuthenticationSchemeOptions, EnvironmentKeyAuthenticationHandler>(
EnvironmentKeyAuthenticationHandler.SchemeName, _ => { })
.AddScheme<AuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(
ApiKeyAuthenticationHandler.SchemeName, _ => { })
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:SecretKey"]!))
};
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy(AuthorizationPolicies.FlagsApiAccess, policy =>
policy.AddAuthenticationSchemes(EnvironmentKeyAuthenticationHandler.SchemeName)
.RequireClaim("EnvironmentId"));
options.AddPolicy(AuthorizationPolicies.AdminApiAccess, policy =>
policy.AddAuthenticationSchemes(ApiKeyAuthenticationHandler.SchemeName, JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser());
options.AddPolicy(AuthorizationPolicies.OrganizationAdmin, policy =>
policy.AddAuthenticationSchemes(ApiKeyAuthenticationHandler.SchemeName, JwtBearerDefaults.AuthenticationScheme)
.RequireClaim("OrganizationRole", "Admin"));
});
builder.Services.AddCommonServices(builder.Configuration);
builder.Services.AddHttpContextAccessor();
@@ -92,58 +51,20 @@ try
limiter.QueueLimit = 0;
}));
builder.Services.AddModelValidatorsFromAssemblyContaining<Program>();
builder.Services.Configure<ApiBehaviorOptions>(options =>
{
options.InvalidModelStateResponseFactory = context => ValidationProblemResponseFactory.Create(context.ModelState);
});
builder.Services.AddMemoryCache();
builder.Services.AddMetrics();
builder.Services.AddScoped<ITokenService, TokenService>();
builder.Services.AddScoped<AuthService>();
builder.Services.AddScoped<ApiKeyService>();
builder.Services.AddScoped<DatabaseSeeder>();
builder.Services.AddScoped<IPasswordHasher<User>, PasswordHasher<User>>();
builder.Services.AddScoped<IAuthorizationHandler, ProjectPermissionRequirementHandler>();
builder.Services.AddScoped<FeatureEvaluationService>();
builder.Services.AddScoped<IdentityResolutionService>();
builder.Services.AddScoped<EnvironmentDocumentService>();
builder.Services.AddSingleton<SegmentEvaluator>();
builder.Services.AddSingleton<FlagCache>();
builder.Services.AddScoped<IAuditService, AuditService>();
builder.Services.AddScoped<AuditLogQueryService>();
builder.Services.AddScoped<OrganizationService>();
builder.Services.AddScoped<ProjectService>();
builder.Services.AddScoped<EnvironmentService>();
builder.Services.AddScoped<FeatureService>();
builder.Services.AddScoped<FeatureStateService>();
builder.Services.AddScoped<FeatureSegmentService>();
builder.Services.AddScoped<SegmentService>();
builder.Services.AddScoped<TagService>();
builder.Services.AddScoped<WebhookService>();
builder.Services.AddScoped<WebhookDispatcher>();
builder.Services.AddScoped<AdminIdentityService>();
builder.Services.AddScoped<UserService>();
builder.Services.AddSingleton<WebhookQueue>();
builder.Services.AddHostedService<WebhookBackgroundService>();
builder.Services.AddHostedService<WebhookRetryBackgroundService>();
builder.Services.AddSingleton<FeatureUsageMetrics>();
builder.Services.AddScoped<FeatureUsageQueryService>();
builder.Services.AddHostedService<FeatureUsageFlushBackgroundService>();
builder.Services.AddHttpClient("Webhooks", client =>
client.DefaultRequestHeaders.Add("User-Agent", "MicCheck-Webhook/1.0"));
builder.Services.AddAuditServices();
builder.Services.AddIdentitiesServices();
builder.Services.AddEnvironmentsServices();
builder.Services.AddSegmentsServices();
builder.Services.AddOrganizationsServices();
builder.Services.AddProjectsServices();
builder.Services.AddFeaturesServices();
builder.Services.AddUsersServices();
builder.Services.AddWebhooksServices();
var connectionString = builder.Configuration.GetConnectionString("miccheck")
?? (System.Environment.GetEnvironmentVariable("DATABASE_URL") is { } databaseUrl
? DatabaseUrlParser.ToNpgsqlConnectionString(databaseUrl)
: builder.Configuration.GetConnectionString("DefaultConnection")!);
builder.Services.AddDbContext<MicCheckDbContext>(options =>
options.UseNpgsql(connectionString));
builder.Services.AddScoped<IMicCheckDbContext>(sp => sp.GetRequiredService<MicCheckDbContext>());
builder.Services.AddDataServices(builder.Configuration);
var app = builder.Build();

View File

@@ -0,0 +1,11 @@
namespace MicCheck.Api.Projects;
public static class DependencyRegistration
{
public static IServiceCollection AddProjectsServices(this IServiceCollection services)
{
services.AddScoped<ProjectService>();
return services;
}
}

View File

@@ -0,0 +1,12 @@
namespace MicCheck.Api.Segments;
public static class DependencyRegistration
{
public static IServiceCollection AddSegmentsServices(this IServiceCollection services)
{
services.AddSingleton<SegmentEvaluator>();
services.AddScoped<SegmentService>();
return services;
}
}

View File

@@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Identity;
namespace MicCheck.Api.Users;
public static class DependencyRegistration
{
public static IServiceCollection AddUsersServices(this IServiceCollection services)
{
services.AddScoped<UserService>();
services.AddScoped<IPasswordHasher<User>, PasswordHasher<User>>();
return services;
}
}

View File

@@ -0,0 +1,17 @@
namespace MicCheck.Api.Webhooks;
public static class DependencyRegistration
{
public static IServiceCollection AddWebhooksServices(this IServiceCollection services)
{
services.AddScoped<WebhookService>();
services.AddScoped<WebhookDispatcher>();
services.AddSingleton<WebhookQueue>();
services.AddHostedService<WebhookBackgroundService>();
services.AddHostedService<WebhookRetryBackgroundService>();
services.AddHttpClient("Webhooks", client =>
client.DefaultRequestHeaders.Add("User-Agent", "MicCheck-Webhook/1.0"));
return services;
}
}