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;
|
namespace MicCheck.Api.Audit;
|
||||||
|
|
||||||
public class AuditLog
|
public record AuditLog
|
||||||
{
|
{
|
||||||
public int Id { get; init; }
|
public int Id { get; init; }
|
||||||
public required string ResourceType { get; init; }
|
public required string ResourceType { get; init; }
|
||||||
|
|||||||
@@ -6,29 +6,25 @@ namespace MicCheck.Api.Audit;
|
|||||||
|
|
||||||
public class AuditLogQueryService(IMicCheckDbContext db)
|
public class AuditLogQueryService(IMicCheckDbContext db)
|
||||||
{
|
{
|
||||||
public async Task<PagedResult<AuditLogResponse>> ListByOrganizationAsync(
|
public async Task<PagedResult<AuditLogResponse>> ListByOrganizationAsync(int organizationId, AuditLogFilter filter, CancellationToken ct = default)
|
||||||
int organizationId, AuditLogFilter filter, CancellationToken ct = default)
|
|
||||||
{
|
{
|
||||||
var query = db.AuditLogs.Where(l => l.OrganizationId == organizationId);
|
var query = db.AuditLogs.Where(l => l.OrganizationId == organizationId);
|
||||||
return await ApplyFilterAndPageAsync(query, filter, ct);
|
return await ApplyFilterAndPageAsync(query, filter, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PagedResult<AuditLogResponse>> ListByProjectAsync(
|
public async Task<PagedResult<AuditLogResponse>> ListByProjectAsync(int projectId, AuditLogFilter filter, CancellationToken ct = default)
|
||||||
int projectId, AuditLogFilter filter, CancellationToken ct = default)
|
|
||||||
{
|
{
|
||||||
var query = db.AuditLogs.Where(l => l.ProjectId == projectId);
|
var query = db.AuditLogs.Where(l => l.ProjectId == projectId);
|
||||||
return await ApplyFilterAndPageAsync(query, filter, ct);
|
return await ApplyFilterAndPageAsync(query, filter, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PagedResult<AuditLogResponse>> ListByEnvironmentAsync(
|
public async Task<PagedResult<AuditLogResponse>> ListByEnvironmentAsync(int environmentId, AuditLogFilter filter, CancellationToken ct = default)
|
||||||
int environmentId, AuditLogFilter filter, CancellationToken ct = default)
|
|
||||||
{
|
{
|
||||||
var query = db.AuditLogs.Where(l => l.EnvironmentId == environmentId);
|
var query = db.AuditLogs.Where(l => l.EnvironmentId == environmentId);
|
||||||
return await ApplyFilterAndPageAsync(query, filter, ct);
|
return await ApplyFilterAndPageAsync(query, filter, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<PagedResult<AuditLogResponse>> ApplyFilterAndPageAsync(
|
private async Task<PagedResult<AuditLogResponse>> ApplyFilterAndPageAsync(IQueryable<AuditLog> query, AuditLogFilter filter, CancellationToken ct)
|
||||||
IQueryable<AuditLog> query, AuditLogFilter filter, CancellationToken ct)
|
|
||||||
{
|
{
|
||||||
if (filter.From.HasValue)
|
if (filter.From.HasValue)
|
||||||
query = query.Where(l => l.CreatedAt >= filter.From.Value);
|
query = query.Where(l => l.CreatedAt >= filter.From.Value);
|
||||||
@@ -66,4 +62,4 @@ public class AuditLogQueryService(IMicCheckDbContext db)
|
|||||||
|
|
||||||
return new PagedResult<AuditLogResponse>(total, items);
|
return new PagedResult<AuditLogResponse>(total, items);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ public class AuditService(IMicCheckDbContext db, IHttpContextAccessor httpContex
|
|||||||
}, JsonOptions);
|
}, JsonOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
var actorUserId = ResolveActorUserId();
|
var actorUserId = GetCurrentUserId();
|
||||||
|
|
||||||
var log = new AuditLog
|
var log = new AuditLog
|
||||||
{
|
{
|
||||||
@@ -78,7 +78,7 @@ public class AuditService(IMicCheckDbContext db, IHttpContextAccessor httpContex
|
|||||||
string? changes = null,
|
string? changes = null,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var actorUserId = ResolveActorUserId();
|
var actorUserId = GetCurrentUserId();
|
||||||
|
|
||||||
var log = new AuditLog
|
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;
|
var claim = httpContextAccessor.HttpContext?.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
||||||
return int.TryParse(claim, out var id) ? id : null;
|
return int.TryParse(claim, out var id) ? id : null;
|
||||||
|
|||||||
@@ -2,9 +2,4 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
|
|
||||||
namespace MicCheck.Api.Common.Security.Authorization;
|
namespace MicCheck.Api.Common.Security.Authorization;
|
||||||
|
|
||||||
public class ProjectPermissionRequirement : IAuthorizationRequirement
|
public record ProjectPermissionRequirement(ProjectPermission Permission) : IAuthorizationRequirement;
|
||||||
{
|
|
||||||
public ProjectPermission Permission { get; }
|
|
||||||
|
|
||||||
public ProjectPermissionRequirement(ProjectPermission permission) => Permission = permission;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,12 +3,7 @@ using MicCheck.Api.Common.Validation;
|
|||||||
|
|
||||||
namespace MicCheck.Api.Features;
|
namespace MicCheck.Api.Features;
|
||||||
|
|
||||||
public record CreateFeatureRequest(
|
public record CreateFeatureRequest(string Name, FeatureType Type, string? InitialValue, string? Description);
|
||||||
string Name,
|
|
||||||
FeatureType Type,
|
|
||||||
string? InitialValue,
|
|
||||||
string? Description
|
|
||||||
);
|
|
||||||
|
|
||||||
public class CreateFeatureRequestValidator : IModelValidator<CreateFeatureRequest>
|
public class CreateFeatureRequestValidator : IModelValidator<CreateFeatureRequest>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace MicCheck.Api.Features;
|
namespace MicCheck.Api.Features;
|
||||||
|
|
||||||
public class FeatureStateResult
|
public record FeatureStateResult
|
||||||
{
|
{
|
||||||
public required Feature Feature { get; init; }
|
public required Feature Feature { get; init; }
|
||||||
public bool Enabled { get; init; }
|
public bool Enabled { get; init; }
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
namespace MicCheck.Api.Features;
|
namespace MicCheck.Api.Features;
|
||||||
|
|
||||||
public class Tag
|
public record Tag
|
||||||
{
|
{
|
||||||
public int Id { get; init; }
|
public int Id { get; init; }
|
||||||
public required string Label { get; set; }
|
public required string Label { get; init; }
|
||||||
public required string Color { get; set; }
|
public required string Color { get; init; }
|
||||||
public int ProjectId { get; init; }
|
public int ProjectId { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,23 +8,24 @@ namespace MicCheck.Api.Features;
|
|||||||
[ApiController]
|
[ApiController]
|
||||||
[Authorize(Policy = AuthorizationPolicies.AdminApiAccess)]
|
[Authorize(Policy = AuthorizationPolicies.AdminApiAccess)]
|
||||||
[EnableRateLimiting("AdminApi")]
|
[EnableRateLimiting("AdminApi")]
|
||||||
|
[Route("api/v1/project/{projectId}")]
|
||||||
public class TagsController(TagService tagService) : ControllerBase
|
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)
|
public async Task<ActionResult<IReadOnlyList<TagResponse>>> List(int projectId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var tags = await tagService.ListByProjectAsync(projectId, ct);
|
var tags = await tagService.ListByProjectAsync(projectId, ct);
|
||||||
return Ok(tags.Select(TagResponse.From).ToList());
|
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)
|
public async Task<ActionResult<TagResponse>> Create(int projectId, CreateTagRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var tag = await tagService.CreateAsync(projectId, request.Label, request.Color, ct);
|
var tag = await tagService.CreateAsync(projectId, request.Label, request.Color, ct);
|
||||||
return CreatedAtAction(nameof(List), new { projectId }, TagResponse.From(tag));
|
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)
|
public async Task<IActionResult> Delete(int projectId, int id, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var tag = await tagService.FindByIdAsync(id, ct);
|
var tag = await tagService.FindByIdAsync(id, ct);
|
||||||
|
|||||||
@@ -3,10 +3,7 @@ using MicCheck.Api.Common.Validation;
|
|||||||
|
|
||||||
namespace MicCheck.Api.Features;
|
namespace MicCheck.Api.Features;
|
||||||
|
|
||||||
public record UpdateFeatureRequest(
|
public record UpdateFeatureRequest(string Name, string? Description);
|
||||||
string Name,
|
|
||||||
string? Description
|
|
||||||
);
|
|
||||||
|
|
||||||
public class UpdateFeatureRequestValidator : IModelValidator<UpdateFeatureRequest>
|
public class UpdateFeatureRequestValidator : IModelValidator<UpdateFeatureRequest>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
namespace MicCheck.Api.Features.Usage;
|
namespace MicCheck.Api.Features.Usage;
|
||||||
|
|
||||||
public class FeatureUsageDaily
|
public record FeatureUsageDaily
|
||||||
{
|
{
|
||||||
public int Id { get; init; }
|
public int Id { get; init; }
|
||||||
public int EnvironmentId { get; init; }
|
public int EnvironmentId { get; init; }
|
||||||
public int FeatureId { 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 DateOnly UsageDate { get; init; }
|
||||||
public long Count { get; set; }
|
public long Count { get; init; }
|
||||||
public DateTimeOffset UpdatedAt { get; set; }
|
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 DailyUsage(DateOnly Date, long TotalCount, IReadOnlyList<TopFeatureUsage> Features);
|
||||||
|
|
||||||
public record DashboardUsageResponse(
|
public record DashboardUsageResponse(IReadOnlyList<TopFeatureUsage> TopFeaturesLastDay, IReadOnlyList<DailyUsage> DailyUsage);
|
||||||
IReadOnlyList<TopFeatureUsage> TopFeaturesLastDay,
|
|
||||||
IReadOnlyList<DailyUsage> DailyUsage);
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace MicCheck.Api.Webhooks;
|
namespace MicCheck.Api.Webhooks;
|
||||||
|
|
||||||
public class WebhookEvent
|
public record WebhookEvent
|
||||||
{
|
{
|
||||||
public required string EventType { get; init; }
|
public required string EventType { get; init; }
|
||||||
public int? EnvironmentId { get; init; }
|
public int? EnvironmentId { get; init; }
|
||||||
|
|||||||
Reference in New Issue
Block a user