Skip to content

Commit 8926d6b

Browse files
author
aligneddev
committed
csharpier format all
1 parent a49ee1d commit 8926d6b

20 files changed

Lines changed: 330 additions & 155 deletions

.specify/memory/constitution.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ Domain logic isolated from infrastructure concerns via layered architecture alig
4747

4848
### II. Functional Programming (Pure & Impure Sandwich)
4949

50-
Core calculations and business logic implemented as pure functions: distance-to-distance conversions, expense-to-savings transformations, weather-to-recommendation mappings. Pure functions have no side effects—given the same input, always return the same output. Impure edges (database reads/writes, external API calls, user input, system time) explicitly isolated at application boundaries. Handlers orchestrate pure logic within impure I/O boundaries. **F# discriminated unions and active patterns preferred for domain modeling** (domain layer uses F#); Railway Oriented Programming (Result<'T> type) for error handling; C# records used in API surface for interop.
50+
Core calculations and business logic implemented as pure functions: distance-to-distance conversions, expense-to-savings transformations, weather-to-recommendation mappings. Pure functions have no side effects—given the same input, always return the same output. Use immutable data structures. Impure edges (database reads/writes, external API calls, user input, system time) explicitly isolated at application boundaries. Handlers orchestrate pure logic within impure I/O boundaries. **F# discriminated unions and active patterns preferred for domain modeling** (domain layer uses F#); Railway Oriented Programming (Result<'T> type) for error handling; C# records used in API surface for interop.
5151

5252
**Rationale**: Pure functions are trivially testable, deterministic, and composable. Side effect isolation makes dataflow explicit and reduces debugging complexity. Immutable data structures preferred where practical. F# enforces immutability and pattern matching, reducing entire categories of bugs. Discriminated unions make invalid states unrepresentable.
5353

@@ -168,10 +168,14 @@ Example: "User records a bike ride" slice includes:
168168
- Background function listening to CES to update RideProjection
169169
- Aspire AppHost configuration for frontend + API + database orchestration; Azure CLI deployment scripts for Static Web Apps (frontend) and Container Apps (API)
170170

171-
run `dotnet format .` to enforce code style; `dotnet test` to run tests; `dotnet run --project src/BikeTracking.AppHost` to start local stack; GitHub Actions for CI/CD to Azure.
171+
run `csharpier format .` to enforce code formatting (document in readme.md, `dotnet tool install csharpier -g` is needed); run `dotnet format .` (built-in .NET tool) uses .editorconfig for formatting rules and supports more granular control (e.g., namespace matching, file-scoped namespaces).
172172

173+
Best Practice: Use CSharpier for consistent formatting and dotnet format for linting and code style enforcement (e.g., dotnet_diagnostic.IDE0130.severity=error in .editorconfig).
173174
run Typescript linting and formatting via `npm run lint` and `npm run format` in the frontend directory.
174175

176+
Test` to run tests; `dotnet run --project src/BikeTracking.AppHost` to start local stack; GitHub Actions for CI/CD to Azure.
177+
178+
175179
### Vertical Slice Implementation Strategy: Minimal-First Approach
176180

177181
After the application structure is built, implementation proceeds in **vertical slices with minimal functionality first**:

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ Local-first Bike Tracking application built with .NET Aspire orchestration, .NET
2222

2323
- .NET SDK 10.x
2424
- Node.js 20+ and npm
25+
- CSharpier global tool (required for formatting checks):
26+
27+
```powershell
28+
dotnet tool install csharpier -g
29+
```
30+
31+
run it with `dotnet csharpier format .` from the repo root to format all C# code.
32+
33+
- Helpful editor integration: VS Code CSharpier extension (`csharpier.csharpier-vscode`)
2534

2635
## Quick Start
2736

src/BikeTracking.Api/Application/Events/EfOutboxStore.cs

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,39 +5,57 @@ namespace BikeTracking.Api.Application.Events;
55

66
public sealed class EfOutboxStore(IServiceScopeFactory scopeFactory) : IOutboxStore
77
{
8-
public async Task<IReadOnlyList<OutboxEventEntity>> LoadPendingAsync(int maxBatchSize, DateTime utcNow, CancellationToken cancellationToken)
8+
public async Task<IReadOnlyList<OutboxEventEntity>> LoadPendingAsync(
9+
int maxBatchSize,
10+
DateTime utcNow,
11+
CancellationToken cancellationToken
12+
)
913
{
1014
using var scope = scopeFactory.CreateScope();
1115
var dbContext = scope.ServiceProvider.GetRequiredService<BikeTrackingDbContext>();
1216

13-
return await dbContext.OutboxEvents
14-
.Where(x => x.PublishedAtUtc == null && x.NextAttemptUtc <= utcNow)
17+
return await dbContext
18+
.OutboxEvents.Where(x => x.PublishedAtUtc == null && x.NextAttemptUtc <= utcNow)
1519
.OrderBy(x => x.OutboxEventId)
1620
.Take(maxBatchSize)
1721
.AsNoTracking()
1822
.ToListAsync(cancellationToken);
1923
}
2024

21-
public async Task MarkPublishedAsync(long outboxEventId, DateTime publishedAtUtc, CancellationToken cancellationToken)
25+
public async Task MarkPublishedAsync(
26+
long outboxEventId,
27+
DateTime publishedAtUtc,
28+
CancellationToken cancellationToken
29+
)
2230
{
2331
using var scope = scopeFactory.CreateScope();
2432
var dbContext = scope.ServiceProvider.GetRequiredService<BikeTrackingDbContext>();
2533

26-
var eventEntity = await dbContext.OutboxEvents
27-
.SingleAsync(x => x.OutboxEventId == outboxEventId, cancellationToken);
34+
var eventEntity = await dbContext.OutboxEvents.SingleAsync(
35+
x => x.OutboxEventId == outboxEventId,
36+
cancellationToken
37+
);
2838

2939
eventEntity.PublishedAtUtc = publishedAtUtc;
3040
eventEntity.LastError = null;
3141
await dbContext.SaveChangesAsync(cancellationToken);
3242
}
3343

34-
public async Task ScheduleRetryAsync(long outboxEventId, int retryCount, DateTime nextAttemptUtc, string? lastError, CancellationToken cancellationToken)
44+
public async Task ScheduleRetryAsync(
45+
long outboxEventId,
46+
int retryCount,
47+
DateTime nextAttemptUtc,
48+
string? lastError,
49+
CancellationToken cancellationToken
50+
)
3551
{
3652
using var scope = scopeFactory.CreateScope();
3753
var dbContext = scope.ServiceProvider.GetRequiredService<BikeTrackingDbContext>();
3854

39-
var eventEntity = await dbContext.OutboxEvents
40-
.SingleAsync(x => x.OutboxEventId == outboxEventId, cancellationToken);
55+
var eventEntity = await dbContext.OutboxEvents.SingleAsync(
56+
x => x.OutboxEventId == outboxEventId,
57+
cancellationToken
58+
);
4159

4260
eventEntity.RetryCount = retryCount;
4361
eventEntity.NextAttemptUtc = nextAttemptUtc;

src/BikeTracking.Api/Application/Events/IOutboxStore.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,21 @@ namespace BikeTracking.Api.Application.Events;
44

55
public interface IOutboxStore
66
{
7-
Task<IReadOnlyList<OutboxEventEntity>> LoadPendingAsync(int maxBatchSize, DateTime utcNow, CancellationToken cancellationToken);
8-
Task MarkPublishedAsync(long outboxEventId, DateTime publishedAtUtc, CancellationToken cancellationToken);
9-
Task ScheduleRetryAsync(long outboxEventId, int retryCount, DateTime nextAttemptUtc, string? lastError, CancellationToken cancellationToken);
7+
Task<IReadOnlyList<OutboxEventEntity>> LoadPendingAsync(
8+
int maxBatchSize,
9+
DateTime utcNow,
10+
CancellationToken cancellationToken
11+
);
12+
Task MarkPublishedAsync(
13+
long outboxEventId,
14+
DateTime publishedAtUtc,
15+
CancellationToken cancellationToken
16+
);
17+
Task ScheduleRetryAsync(
18+
long outboxEventId,
19+
int retryCount,
20+
DateTime nextAttemptUtc,
21+
string? lastError,
22+
CancellationToken cancellationToken
23+
);
1024
}

src/BikeTracking.Api/Application/Events/OutboxPublisherService.cs

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ public sealed class OutboxPublisherService(
99
IOutboxStore outboxStore,
1010
IUserRegisteredPublisher userRegisteredPublisher,
1111
IOptions<IdentityOptions> identityOptions,
12-
ILogger<OutboxPublisherService> logger) : BackgroundService
12+
ILogger<OutboxPublisherService> logger
13+
) : BackgroundService
1314
{
1415
private readonly OutboxOptions _outboxOptions = identityOptions.Value.Outbox;
1516

@@ -45,20 +46,38 @@ private async Task PublishPendingBatchAsync(CancellationToken cancellationToken)
4546
{
4647
try
4748
{
48-
if (!string.Equals(outboxEvent.EventType, UserRegisteredEventPayload.EventTypeName, StringComparison.Ordinal))
49+
if (
50+
!string.Equals(
51+
outboxEvent.EventType,
52+
UserRegisteredEventPayload.EventTypeName,
53+
StringComparison.Ordinal
54+
)
55+
)
4956
{
50-
await outboxStore.MarkPublishedAsync(outboxEvent.OutboxEventId, DateTime.UtcNow, cancellationToken);
57+
await outboxStore.MarkPublishedAsync(
58+
outboxEvent.OutboxEventId,
59+
DateTime.UtcNow,
60+
cancellationToken
61+
);
5162
continue;
5263
}
5364

54-
var payload = JsonSerializer.Deserialize<UserRegisteredEventPayload>(outboxEvent.EventPayloadJson);
65+
var payload = JsonSerializer.Deserialize<UserRegisteredEventPayload>(
66+
outboxEvent.EventPayloadJson
67+
);
5568
if (payload is null)
5669
{
57-
throw new InvalidOperationException($"Unable to deserialize payload for outbox event {outboxEvent.OutboxEventId}.");
70+
throw new InvalidOperationException(
71+
$"Unable to deserialize payload for outbox event {outboxEvent.OutboxEventId}."
72+
);
5873
}
5974

6075
await userRegisteredPublisher.PublishAsync(payload, cancellationToken);
61-
await outboxStore.MarkPublishedAsync(outboxEvent.OutboxEventId, DateTime.UtcNow, cancellationToken);
76+
await outboxStore.MarkPublishedAsync(
77+
outboxEvent.OutboxEventId,
78+
DateTime.UtcNow,
79+
cancellationToken
80+
);
6281
}
6382
catch (Exception ex)
6483
{
@@ -71,14 +90,16 @@ await outboxStore.ScheduleRetryAsync(
7190
retryCount,
7291
nextAttemptUtc,
7392
ex.Message,
74-
cancellationToken);
93+
cancellationToken
94+
);
7595

7696
logger.LogWarning(
7797
ex,
7898
"Failed to publish outbox event {OutboxEventId}. Retrying at {NextAttemptUtc} (retry #{RetryCount}).",
7999
outboxEvent.OutboxEventId,
80100
nextAttemptUtc,
81-
retryCount);
101+
retryCount
102+
);
82103
}
83104
}
84105
}

src/BikeTracking.Api/Application/Events/UserRegisteredPublisher.cs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,27 +14,36 @@ public sealed class UserRegisteredPublisher : IUserRegisteredPublisher
1414
private readonly ILogger<UserRegisteredPublisher> _logger;
1515
private int _remainingForcedFailures;
1616

17-
public UserRegisteredPublisher(IOptions<IdentityOptions> options, ILogger<UserRegisteredPublisher> logger)
17+
public UserRegisteredPublisher(
18+
IOptions<IdentityOptions> options,
19+
ILogger<UserRegisteredPublisher> logger
20+
)
1821
{
1922
_logger = logger;
2023
_remainingForcedFailures = Math.Max(0, options.Value.Outbox.FailFirstPublishAttempts);
2124
}
2225

23-
public Task PublishAsync(UserRegisteredEventPayload payload, CancellationToken cancellationToken)
26+
public Task PublishAsync(
27+
UserRegisteredEventPayload payload,
28+
CancellationToken cancellationToken
29+
)
2430
{
2531
cancellationToken.ThrowIfCancellationRequested();
2632

2733
if (_remainingForcedFailures > 0)
2834
{
2935
Interlocked.Decrement(ref _remainingForcedFailures);
30-
throw new InvalidOperationException("Simulated publish failure for resilience verification.");
36+
throw new InvalidOperationException(
37+
"Simulated publish failure for resilience verification."
38+
);
3139
}
3240

3341
_logger.LogInformation(
3442
"Published UserRegistered event. EventId: {EventId}, UserId: {UserId}, UserName: {UserName}",
3543
payload.EventId,
3644
payload.UserId,
37-
payload.UserName);
45+
payload.UserName
46+
);
3847

3948
return Task.CompletedTask;
4049
}

src/BikeTracking.Api/Application/Users/IdentifyService.cs

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@ public sealed class IdentifyService(
1010
BikeTrackingDbContext dbContext,
1111
PinPolicyValidator pinPolicyValidator,
1212
IPinHasher pinHasher,
13-
IOptions<IdentityOptions> identityOptions)
13+
IOptions<IdentityOptions> identityOptions
14+
)
1415
{
1516
private readonly ThrottleOptions _throttleOptions = identityOptions.Value.Throttle;
1617

17-
public async Task<IdentifyResult> IdentifyAsync(IdentifyRequest request, CancellationToken cancellationToken)
18+
public async Task<IdentifyResult> IdentifyAsync(
19+
IdentifyRequest request,
20+
CancellationToken cancellationToken
21+
)
1822
{
1923
var validationErrors = ValidateRequest(request);
2024
if (validationErrors.Count > 0)
@@ -24,8 +28,8 @@ public async Task<IdentifyResult> IdentifyAsync(IdentifyRequest request, Cancell
2428

2529
var normalizedName = UserNameNormalizer.Normalize(request.Name);
2630

27-
var user = await dbContext.Users
28-
.Include(x => x.Credential)
31+
var user = await dbContext
32+
.Users.Include(x => x.Credential)
2933
.Include(x => x.AuthAttemptState)
3034
.SingleOrDefaultAsync(x => x.NormalizedName == normalizedName, cancellationToken);
3135

@@ -51,15 +55,19 @@ public async Task<IdentifyResult> IdentifyAsync(IdentifyRequest request, Cancell
5155

5256
if (attemptState.DelayUntilUtc is not null && attemptState.DelayUntilUtc > now)
5357
{
54-
var retryAfterSeconds = Math.Max(1, (int)Math.Ceiling((attemptState.DelayUntilUtc.Value - now).TotalSeconds));
58+
var retryAfterSeconds = Math.Max(
59+
1,
60+
(int)Math.Ceiling((attemptState.DelayUntilUtc.Value - now).TotalSeconds)
61+
);
5562
return IdentifyResult.Throttled(retryAfterSeconds);
5663
}
5764

5865
var pinMatches = pinHasher.Verify(
5966
request.Pin,
6067
user.Credential.PinSalt,
6168
user.Credential.PinHash,
62-
user.Credential.IterationCount);
69+
user.Credential.IterationCount
70+
);
6371

6472
if (pinMatches)
6573
{
@@ -68,7 +76,9 @@ public async Task<IdentifyResult> IdentifyAsync(IdentifyRequest request, Cancell
6876
attemptState.DelayUntilUtc = null;
6977
await dbContext.SaveChangesAsync(cancellationToken);
7078

71-
return IdentifyResult.Success(new IdentifySuccessResponse(user.UserId, user.DisplayName, true));
79+
return IdentifyResult.Success(
80+
new IdentifySuccessResponse(user.UserId, user.DisplayName, true)
81+
);
7282
}
7383

7484
attemptState.ConsecutiveWrongCount += 1;
@@ -112,7 +122,8 @@ public sealed record IdentifyResult(
112122
IdentifyResultType ResultType,
113123
IdentifySuccessResponse? Response,
114124
ErrorResponse? Error,
115-
int RetryAfterSeconds = 0)
125+
int RetryAfterSeconds = 0
126+
)
116127
{
117128
public static IdentifyResult Success(IdentifySuccessResponse response)
118129
{
@@ -124,15 +135,17 @@ public static IdentifyResult ValidationFailure(IReadOnlyList<string> errors)
124135
return new IdentifyResult(
125136
IdentifyResultType.ValidationFailed,
126137
null,
127-
new ErrorResponse(UsersErrorCodes.ValidationFailed, "Validation failed.", errors));
138+
new ErrorResponse(UsersErrorCodes.ValidationFailed, "Validation failed.", errors)
139+
);
128140
}
129141

130142
public static IdentifyResult Unauthorized()
131143
{
132144
return new IdentifyResult(
133145
IdentifyResultType.Unauthorized,
134146
null,
135-
new ErrorResponse(UsersErrorCodes.InvalidCredentials, "Invalid name or PIN."));
147+
new ErrorResponse(UsersErrorCodes.InvalidCredentials, "Invalid name or PIN.")
148+
);
136149
}
137150

138151
public static IdentifyResult Throttled(int retryAfterSeconds)
@@ -141,7 +154,8 @@ public static IdentifyResult Throttled(int retryAfterSeconds)
141154
IdentifyResultType.Throttled,
142155
null,
143156
new ErrorResponse(UsersErrorCodes.Throttled, "Too many attempts. Try again later."),
144-
retryAfterSeconds);
157+
retryAfterSeconds
158+
);
145159
}
146160
}
147161

src/BikeTracking.Api/Application/Users/PinPolicyValidator.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ public IReadOnlyList<string> Validate(string? pin)
1818

1919
if (pin.Length < _pinPolicy.MinLength || pin.Length > _pinPolicy.MaxLength)
2020
{
21-
errors.Add($"PIN must be between {_pinPolicy.MinLength} and {_pinPolicy.MaxLength} characters.");
21+
errors.Add(
22+
$"PIN must be between {_pinPolicy.MinLength} and {_pinPolicy.MaxLength} characters."
23+
);
2224
}
2325

2426
if (_pinPolicy.NumericOnly && pin.Any(ch => !char.IsDigit(ch)))

0 commit comments

Comments
 (0)