Convert logic-free classes to records per CLAUDE.md
WebhookEvent, FeatureStateResult, ProjectPermissionRequirement, AuditLog, Tag, and FeatureUsageDaily are plain data holders with no behavior, so switch them to records. EF-mapped entities with identity semantics (User, Organization, Project, Feature, Segment, etc.) stay classes, since record value-equality and deep ToString fight EF's change tracker. Also restore `virtual` on AuditService.RecordAsync/LogAsync, which had been dropped and broke Mock<AuditService>-based test setups.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
namespace MicCheck.Api.Audit;
|
||||
|
||||
public class AuditLog
|
||||
public record AuditLog
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public required string ResourceType { get; init; }
|
||||
|
||||
@@ -6,29 +6,25 @@ namespace MicCheck.Api.Audit;
|
||||
|
||||
public class AuditLogQueryService(IMicCheckDbContext db)
|
||||
{
|
||||
public async Task<PagedResult<AuditLogResponse>> ListByOrganizationAsync(
|
||||
int organizationId, AuditLogFilter filter, CancellationToken ct = default)
|
||||
public async Task<PagedResult<AuditLogResponse>> ListByOrganizationAsync(int organizationId, AuditLogFilter filter, CancellationToken ct = default)
|
||||
{
|
||||
var query = db.AuditLogs.Where(l => l.OrganizationId == organizationId);
|
||||
return await ApplyFilterAndPageAsync(query, filter, ct);
|
||||
}
|
||||
|
||||
public async Task<PagedResult<AuditLogResponse>> ListByProjectAsync(
|
||||
int projectId, AuditLogFilter filter, CancellationToken ct = default)
|
||||
public async Task<PagedResult<AuditLogResponse>> ListByProjectAsync(int projectId, AuditLogFilter filter, CancellationToken ct = default)
|
||||
{
|
||||
var query = db.AuditLogs.Where(l => l.ProjectId == projectId);
|
||||
return await ApplyFilterAndPageAsync(query, filter, ct);
|
||||
}
|
||||
|
||||
public async Task<PagedResult<AuditLogResponse>> ListByEnvironmentAsync(
|
||||
int environmentId, AuditLogFilter filter, CancellationToken ct = default)
|
||||
public async Task<PagedResult<AuditLogResponse>> ListByEnvironmentAsync(int environmentId, AuditLogFilter filter, CancellationToken ct = default)
|
||||
{
|
||||
var query = db.AuditLogs.Where(l => l.EnvironmentId == environmentId);
|
||||
return await ApplyFilterAndPageAsync(query, filter, ct);
|
||||
}
|
||||
|
||||
private async Task<PagedResult<AuditLogResponse>> ApplyFilterAndPageAsync(
|
||||
IQueryable<AuditLog> query, AuditLogFilter filter, CancellationToken ct)
|
||||
private async Task<PagedResult<AuditLogResponse>> ApplyFilterAndPageAsync(IQueryable<AuditLog> query, AuditLogFilter filter, CancellationToken ct)
|
||||
{
|
||||
if (filter.From.HasValue)
|
||||
query = query.Where(l => l.CreatedAt >= filter.From.Value);
|
||||
@@ -66,4 +62,4 @@ public class AuditLogQueryService(IMicCheckDbContext db)
|
||||
|
||||
return new PagedResult<AuditLogResponse>(total, items);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ public class AuditService(IMicCheckDbContext db, IHttpContextAccessor httpContex
|
||||
}, JsonOptions);
|
||||
}
|
||||
|
||||
var actorUserId = ResolveActorUserId();
|
||||
var actorUserId = GetCurrentUserId();
|
||||
|
||||
var log = new AuditLog
|
||||
{
|
||||
@@ -78,7 +78,7 @@ public class AuditService(IMicCheckDbContext db, IHttpContextAccessor httpContex
|
||||
string? changes = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var actorUserId = ResolveActorUserId();
|
||||
var actorUserId = GetCurrentUserId();
|
||||
|
||||
var log = new AuditLog
|
||||
{
|
||||
@@ -112,7 +112,7 @@ public class AuditService(IMicCheckDbContext db, IHttpContextAccessor httpContex
|
||||
});
|
||||
}
|
||||
|
||||
private int? ResolveActorUserId()
|
||||
private int? GetCurrentUserId()
|
||||
{
|
||||
var claim = httpContextAccessor.HttpContext?.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
||||
return int.TryParse(claim, out var id) ? id : null;
|
||||
|
||||
@@ -2,9 +2,4 @@ using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace MicCheck.Api.Common.Security.Authorization;
|
||||
|
||||
public class ProjectPermissionRequirement : IAuthorizationRequirement
|
||||
{
|
||||
public ProjectPermission Permission { get; }
|
||||
|
||||
public ProjectPermissionRequirement(ProjectPermission permission) => Permission = permission;
|
||||
}
|
||||
public record ProjectPermissionRequirement(ProjectPermission Permission) : IAuthorizationRequirement;
|
||||
|
||||
@@ -3,12 +3,7 @@ using MicCheck.Api.Common.Validation;
|
||||
|
||||
namespace MicCheck.Api.Features;
|
||||
|
||||
public record CreateFeatureRequest(
|
||||
string Name,
|
||||
FeatureType Type,
|
||||
string? InitialValue,
|
||||
string? Description
|
||||
);
|
||||
public record CreateFeatureRequest(string Name, FeatureType Type, string? InitialValue, string? Description);
|
||||
|
||||
public class CreateFeatureRequestValidator : IModelValidator<CreateFeatureRequest>
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace MicCheck.Api.Features;
|
||||
|
||||
public class FeatureStateResult
|
||||
public record FeatureStateResult
|
||||
{
|
||||
public required Feature Feature { get; init; }
|
||||
public bool Enabled { get; init; }
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
namespace MicCheck.Api.Features;
|
||||
|
||||
public class Tag
|
||||
public record Tag
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public required string Label { get; set; }
|
||||
public required string Color { get; set; }
|
||||
public required string Label { get; init; }
|
||||
public required string Color { get; init; }
|
||||
public int ProjectId { get; init; }
|
||||
}
|
||||
|
||||
@@ -8,23 +8,24 @@ namespace MicCheck.Api.Features;
|
||||
[ApiController]
|
||||
[Authorize(Policy = AuthorizationPolicies.AdminApiAccess)]
|
||||
[EnableRateLimiting("AdminApi")]
|
||||
[Route("api/v1/project/{projectId}")]
|
||||
public class TagsController(TagService tagService) : ControllerBase
|
||||
{
|
||||
[HttpGet("api/v1/project/{projectId}/tags")]
|
||||
[HttpGet("tags")]
|
||||
public async Task<ActionResult<IReadOnlyList<TagResponse>>> List(int projectId, CancellationToken ct)
|
||||
{
|
||||
var tags = await tagService.ListByProjectAsync(projectId, ct);
|
||||
return Ok(tags.Select(TagResponse.From).ToList());
|
||||
}
|
||||
|
||||
[HttpPost("api/v1/project/{projectId}/tags")]
|
||||
[HttpPost("tags")]
|
||||
public async Task<ActionResult<TagResponse>> Create(int projectId, CreateTagRequest request, CancellationToken ct)
|
||||
{
|
||||
var tag = await tagService.CreateAsync(projectId, request.Label, request.Color, ct);
|
||||
return CreatedAtAction(nameof(List), new { projectId }, TagResponse.From(tag));
|
||||
}
|
||||
|
||||
[HttpDelete("api/v1/project/{projectId}/tag/{id}")]
|
||||
[HttpDelete("tag/{id}")]
|
||||
public async Task<IActionResult> Delete(int projectId, int id, CancellationToken ct)
|
||||
{
|
||||
var tag = await tagService.FindByIdAsync(id, ct);
|
||||
|
||||
@@ -3,10 +3,7 @@ using MicCheck.Api.Common.Validation;
|
||||
|
||||
namespace MicCheck.Api.Features;
|
||||
|
||||
public record UpdateFeatureRequest(
|
||||
string Name,
|
||||
string? Description
|
||||
);
|
||||
public record UpdateFeatureRequest(string Name, string? Description);
|
||||
|
||||
public class UpdateFeatureRequestValidator : IModelValidator<UpdateFeatureRequest>
|
||||
{
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
namespace MicCheck.Api.Features.Usage;
|
||||
|
||||
public class FeatureUsageDaily
|
||||
public record FeatureUsageDaily
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public int EnvironmentId { get; init; }
|
||||
public int FeatureId { get; init; }
|
||||
public required string FeatureName { get; set; }
|
||||
public required string FeatureName { get; init; }
|
||||
public DateOnly UsageDate { get; init; }
|
||||
public long Count { get; set; }
|
||||
public DateTimeOffset UpdatedAt { get; set; }
|
||||
public long Count { get; init; }
|
||||
public DateTimeOffset UpdatedAt { get; init; }
|
||||
}
|
||||
|
||||
@@ -4,6 +4,4 @@ public record TopFeatureUsage(int FeatureId, string FeatureName, long Count);
|
||||
|
||||
public record DailyUsage(DateOnly Date, long TotalCount, IReadOnlyList<TopFeatureUsage> Features);
|
||||
|
||||
public record DashboardUsageResponse(
|
||||
IReadOnlyList<TopFeatureUsage> TopFeaturesLastDay,
|
||||
IReadOnlyList<DailyUsage> DailyUsage);
|
||||
public record DashboardUsageResponse(IReadOnlyList<TopFeatureUsage> TopFeaturesLastDay, IReadOnlyList<DailyUsage> DailyUsage);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace MicCheck.Api.Webhooks;
|
||||
|
||||
public class WebhookEvent
|
||||
public record WebhookEvent
|
||||
{
|
||||
public required string EventType { get; init; }
|
||||
public int? EnvironmentId { get; init; }
|
||||
|
||||
Reference in New Issue
Block a user