Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -401,4 +401,11 @@ dotnet_naming_style.camelcase.capitalization = camel_case
dotnet_naming_style.s_camelcase.required_prefix = s_
dotnet_naming_style.s_camelcase.required_suffix =
dotnet_naming_style.s_camelcase.word_separator =
dotnet_naming_style.s_camelcase.capitalization = camel_case
dotnet_naming_style.s_camelcase.capitalization = camel_case

[tests/**/*.cs]
# Tests intentionally use xUnit-style method names with underscores.
dotnet_diagnostic.CA1707.severity = none
# Test code deliberately exercises sync validators and uses xUnit-created fixtures.
dotnet_diagnostic.CA1849.severity = none
dotnet_diagnostic.CA1515.severity = none
87 changes: 87 additions & 0 deletions docs/build-log.txt
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,93 @@ REMAINING BLOCKER
None found for the SharpCompress / NU1902 issue after the package pin was
applied and verified.

ADDENDUM: ISSUE #280 RELEASE ANALYZER WARNING CLEANUP
----------------------------------------------------
Generated: 2026-05-10
Branch: squad/280-cleanup-release-build-warnings
Scope: DevOps/infra-side warning cleanup after refreshing the preserved
issue branch with `dev`.

BASELINE
--------
- Preserved worktree branch was 4 commits ahead and 1 commit behind `dev`.
Merged `dev` into the worktree first so the baseline included PR #279 and
the SharpCompress transitive pin.
- Initial deduplicated Release analyzer baseline after the merge:
- 89 warnings
- 0 source-code errors
- 1 local worktree environment error: Tailwind CLI was not resolvable because
the sibling worktree lacked `node_modules`
- Top warning IDs before cleanup:
- CA2007 = 37
- CA1707 = 34
- CA1849 = 8
- CA1515 = 4
- CA2225 = 2
- CA1062 = 2
- CA1724 = 1
- CA1865 = 1
- Highest-leverage files before cleanup:
- tests/Domain.Tests/Behaviors/ValidationBehaviorTests.cs
- tests/Domain.Tests/Entities/BlogPostTests.cs
- tests/Domain.Tests/Abstractions/ResultTests.cs
- src/Web/Data/MongoDbBlogPostRepository.cs
- src/Web/Infrastructure/Caching/BlogPostCacheService.cs
- src/Web/Components/Theme/ThemeProvider.razor.cs

FIXES APPLIED
-------------
- .editorconfig
- Added centralized `[tests/**/*.cs]` suppressions for CA1707, CA1849, and
CA1515 so test-only analyzer noise stays managed in one repo-wide location.
These warnings are xUnit naming / focused-sync-validator patterns in test
code, not production defects.
- src/ServiceDefaults/Extensions.cs
- Renamed the extension container type to `ServiceDefaultsExtensions` to
remove CA1724.
- Added `ArgumentNullException.ThrowIfNull(app)` to remove CA1062.
- src/Domain/Behaviors/ValidationBehavior.cs
- Added `ConfigureAwait(false)` to both async handler awaits.
- src/Web/Data/MongoDbBlogPostRepository.cs
- Added `ConfigureAwait(false)` to repository async calls.
- src/Web/Infrastructure/Caching/BlogPostCacheService.cs
- Added `ConfigureAwait(false)` to distributed-cache and fetch awaits.
- src/Web/Data/BlogDbContext.cs
- Added `ArgumentNullException.ThrowIfNull(modelBuilder)` for CA1062.
- Added a justified CA1515 suppression because the public DbContext type is
part of the composition root and shared test infrastructure.
- Local worktree-only environment fix
- Added an untracked local `node_modules` symlink back to the primary checkout
so Tailwind could build inside the sibling worktree. No tracked files
changed for this environment repair.

RESULT
------
- `dotnet build MyBlog.slnx --configuration Release --no-restore`
- Status: ✅ SUCCESS
- Final baseline: 0 warnings, 0 errors

VERIFICATION
------------
- `dotnet test tests/Architecture.Tests/Architecture.Tests.csproj --configuration Release --no-build`
- ✅ Passed 16/16
- `dotnet test tests/Domain.Tests/Domain.Tests.csproj --configuration Release --no-build`
- ✅ Passed 42/42
- `dotnet test tests/Web.Tests/Web.Tests.csproj --configuration Release --no-build`
- ✅ Passed 153/153
- `dotnet test tests/Web.Tests.Bunit/Web.Tests.Bunit.csproj --configuration Release --no-build`
- ✅ Passed 69/69
- `dotnet test tests/Web.Tests.Integration/Web.Tests.Integration.csproj --configuration Release --no-build`
- ✅ Passed 12/12
- `dotnet test tests/AppHost.Tests/AppHost.Tests.csproj --configuration Release --no-build`
- ✅ Passed 48/49, Skipped 1 documented AppHost theme skip

REMAINING WARNINGS / HANDOFF
---------------------------
- None in the final Release build baseline.
- No natural handoff to Legolas is required from this pass because the build is
warning-clean after the infra/backend and test-project cleanup.

================================================================================
END OF BUILD LOG
================================================================================
6 changes: 6 additions & 0 deletions src/AppHost/AppHost.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@
<PackageReference Include="Snappier" />
</ItemGroup>

<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>AppHost.Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
Expand Down
10 changes: 10 additions & 0 deletions src/AppHost/MongoDbResourceBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ internal static class MongoDbResourceBuilderExtensions
// Shared semaphore — guards all three dev commands (Clear, Seed, Stats) so only one runs at a time.
private static readonly SemaphoreSlim _dbMutex = new(1, 1);

/// <summary>
/// Test-only hook used by <c>AppHost.Tests</c> to hold the seed command inside the shared mutex
/// so overlapping invocations can be asserted deterministically.
/// </summary>
internal static Func<CancellationToken, ValueTask>? SeedCommandAfterMutexAcquiredAsync { get; set; }

public static IResourceBuilder<MongoDBServerResource> WithMongoDbDevCommands(
this IResourceBuilder<MongoDBServerResource> builder,
string databaseName)
Expand Down Expand Up @@ -180,6 +186,10 @@ private static void WithSeedDataCommand(

try
{
var afterMutexAcquired = SeedCommandAfterMutexAcquiredAsync;
if (afterMutexAcquired is not null)
await afterMutexAcquired(context.CancellationToken);

context.Logger.LogInformation(
"Seed MyBlog data invoked on {ResourceName} — inserting seed data into '{Database}'.",
context.ResourceName, databaseName);
Expand Down
6 changes: 6 additions & 0 deletions src/Domain/Abstractions/Result.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
// Project Name : Domain
// =======================================================

using System.Diagnostics.CodeAnalysis;

namespace MyBlog.Domain.Abstractions;

public enum ResultErrorCode
Expand Down Expand Up @@ -140,6 +142,9 @@ public static Result<T> FromValue(T? value)
}
#pragma warning restore CA1000 // Do not declare static members on generic types

// CA2225 does not recognize Result<T>.ToValue()/FromValue() as valid alternates for
// these generic implicit conversions, so suppress the warning only on the operators.
[SuppressMessage("Usage", "CA2225:Operator overloads have named alternates", Justification = "Result<T> already exposes ToValue()/FromValue() named conversion APIs; the implicit conversions are kept intentionally for application ergonomics.")]
public static implicit operator T?(Result<T>? result)
{
if (result is null)
Expand All @@ -152,6 +157,7 @@ public static Result<T> FromValue(T? value)
return result.Value;
}

[SuppressMessage("Usage", "CA2225:Operator overloads have named alternates", Justification = "Result<T> already exposes ToValue()/FromValue() named conversion APIs; the implicit conversions are kept intentionally for application ergonomics.")]
public static implicit operator Result<T>(T? value)
{
return Ok(value);
Expand Down
4 changes: 2 additions & 2 deletions src/Domain/Behaviors/ValidationBehavior.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public async Task<TResponse> Handle(
ArgumentNullException.ThrowIfNull(next);

if (!validators.Any())
return await next(cancellationToken);
return await next(cancellationToken).ConfigureAwait(false);

var context = new ValidationContext<TRequest>(request);
var failures = validators
Expand All @@ -43,7 +43,7 @@ public async Task<TResponse> Handle(
return (TResponse)CreateFailResult(typeof(TResponse), errorMessage);
}

return await next(cancellationToken);
return await next(cancellationToken).ConfigureAwait(false);
}

private static object CreateFailResult(Type resultType, string errorMessage)
Expand Down
4 changes: 3 additions & 1 deletion src/ServiceDefaults/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ namespace MyBlog.ServiceDefaults;
// This project should be referenced by each service project in your solution.
// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults
[ExcludeFromCodeCoverage(Justification = "Aspire infrastructure bootstrap — not business logic")]
public static class Extensions
public static class ServiceDefaultsExtensions
{
Comment on lines 26 to 30
private const string HealthEndpointPath = "/health";
private const string AlivenessEndpointPath = "/alive";
Expand Down Expand Up @@ -121,6 +121,8 @@ public static TBuilder AddDefaultHealthChecks<TBuilder>(this TBuilder builder) w

public static WebApplication MapDefaultEndpoints(this WebApplication app)
{
ArgumentNullException.ThrowIfNull(app);

// Adding health checks endpoints to applications in non-development environments has security implications.
// See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments.
if (app.Environment.IsDevelopment() || app.Environment.IsEnvironment("Testing"))
Expand Down
5 changes: 5 additions & 0 deletions src/Web/Data/BlogDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,21 @@
//Project Name : Web
//=======================================================

using System.Diagnostics.CodeAnalysis;

using MongoDB.EntityFrameworkCore.Extensions;

namespace MyBlog.Web.Data;

[SuppressMessage("Design", "CA1515:Consider making public types internal", Justification = "The DbContext type is part of the web composition root and shared test infrastructure.")]
public sealed class BlogDbContext(DbContextOptions<BlogDbContext> options) : DbContext(options)
{
public DbSet<BlogPost> BlogPosts => Set<BlogPost>();

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
ArgumentNullException.ThrowIfNull(modelBuilder);

var entity = modelBuilder.Entity<BlogPost>();
entity.ToCollection("blogposts");
entity.HasKey(p => p.Id);
Expand Down
24 changes: 12 additions & 12 deletions src/Web/Data/MongoDbBlogPostRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,45 +14,45 @@
{
public async Task<BlogPost?> GetByIdAsync(Guid id, CancellationToken ct = default)
{
await using var ctx = await contextFactory.CreateDbContextAsync(ct);
await using var ctx = await contextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);

Check warning on line 17 in src/Web/Data/MongoDbBlogPostRepository.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

Consider calling ConfigureAwait on the awaited task (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2007)

Check warning on line 17 in src/Web/Data/MongoDbBlogPostRepository.cs

View workflow job for this annotation

GitHub Actions / Build Solution

Consider calling ConfigureAwait on the awaited task (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2007)

Check warning on line 17 in src/Web/Data/MongoDbBlogPostRepository.cs

View workflow job for this annotation

GitHub Actions / Web.Tests

Consider calling ConfigureAwait on the awaited task (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2007)

Check warning on line 17 in src/Web/Data/MongoDbBlogPostRepository.cs

View workflow job for this annotation

GitHub Actions / Web.Tests.Bunit

Consider calling ConfigureAwait on the awaited task (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2007)

Check warning on line 17 in src/Web/Data/MongoDbBlogPostRepository.cs

View workflow job for this annotation

GitHub Actions / AppHost.Tests (Aspire + Playwright E2E)

Consider calling ConfigureAwait on the awaited task (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2007)
return await ctx.BlogPosts.AsNoTracking()
.FirstOrDefaultAsync(p => p.Id == id, ct);
.FirstOrDefaultAsync(p => p.Id == id, ct).ConfigureAwait(false);
}

public async Task<IReadOnlyList<BlogPost>> GetAllAsync(CancellationToken ct = default)
{
await using var ctx = await contextFactory.CreateDbContextAsync(ct);
await using var ctx = await contextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
return await ctx.BlogPosts.AsNoTracking()
.OrderByDescending(p => p.CreatedAt)
.ToListAsync(ct);
.ToListAsync(ct).ConfigureAwait(false);
}

public async Task AddAsync(BlogPost post, CancellationToken ct = default)
{
await using var ctx = await contextFactory.CreateDbContextAsync(ct);
await ctx.BlogPosts.AddAsync(post, ct);
await ctx.SaveChangesAsync(ct);
await using var ctx = await contextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
await ctx.BlogPosts.AddAsync(post, ct).ConfigureAwait(false);
await ctx.SaveChangesAsync(ct).ConfigureAwait(false);
}

public async Task UpdateAsync(BlogPost post, CancellationToken ct = default)
{
await using var ctx = await contextFactory.CreateDbContextAsync(ct);
await using var ctx = await contextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
var entry = ctx.Attach(post);
// Version was incremented by post.Update(); the original value in the DB is Version - 1.
// EF Core uses OriginalValue in the WHERE filter to detect concurrent modifications.
entry.Property(p => p.Version).OriginalValue = post.Version - 1;
entry.State = EntityState.Modified;
await ctx.SaveChangesAsync(ct);
await ctx.SaveChangesAsync(ct).ConfigureAwait(false);
}

public async Task DeleteAsync(Guid id, CancellationToken ct = default)
{
await using var ctx = await contextFactory.CreateDbContextAsync(ct);
var post = await ctx.BlogPosts.FindAsync([id], ct);
await using var ctx = await contextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
var post = await ctx.BlogPosts.FindAsync([id], ct).ConfigureAwait(false);
if (post is not null)
{
ctx.BlogPosts.Remove(post);
await ctx.SaveChangesAsync(ct);
await ctx.SaveChangesAsync(ct).ConfigureAwait(false);
}
}
}
20 changes: 10 additions & 10 deletions src/Web/Infrastructure/Caching/BlogPostCacheService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public async ValueTask<IReadOnlyList<BlogPostDto>> GetOrFetchAllAsync(
return cached;

// L2 hit
var bytes = await distributedCache.GetAsync(BlogPostCacheKeys.All, ct);
var bytes = await distributedCache.GetAsync(BlogPostCacheKeys.All, ct).ConfigureAwait(false);
if (bytes is not null)
{
try
Expand All @@ -49,19 +49,19 @@ public async ValueTask<IReadOnlyList<BlogPostDto>> GetOrFetchAllAsync(
catch (JsonException)
{
// Stale or corrupt bytes — remove and fall through to the DB
await distributedCache.RemoveAsync(BlogPostCacheKeys.All, CancellationToken.None);
await distributedCache.RemoveAsync(BlogPostCacheKeys.All, CancellationToken.None).ConfigureAwait(false);
}
}

// DB via caller-supplied fetch
var result = await fetch();
var result = await fetch().ConfigureAwait(false);
var list = result as List<BlogPostDto> ?? result.ToList();
localCache.Set(BlogPostCacheKeys.All, list, LocalOpts);
await distributedCache.SetAsync(
BlogPostCacheKeys.All,
JsonSerializer.SerializeToUtf8Bytes(list, JsonOpts),
RedisOpts,
ct);
ct).ConfigureAwait(false);
return result;
}

Expand All @@ -77,7 +77,7 @@ await distributedCache.SetAsync(
return cached;

// L2 hit
var bytes = await distributedCache.GetAsync(key, ct);
var bytes = await distributedCache.GetAsync(key, ct).ConfigureAwait(false);
if (bytes is not null)
{
try
Expand All @@ -92,12 +92,12 @@ await distributedCache.SetAsync(
catch (JsonException)
{
// Stale or corrupt bytes — remove and fall through to the DB
await distributedCache.RemoveAsync(key, CancellationToken.None);
await distributedCache.RemoveAsync(key, CancellationToken.None).ConfigureAwait(false);
}
}

// DB via caller-supplied fetch
var result = await fetch();
var result = await fetch().ConfigureAwait(false);
if (result is null)
return null;

Expand All @@ -106,22 +106,22 @@ await distributedCache.SetAsync(
key,
JsonSerializer.SerializeToUtf8Bytes(result, JsonOpts),
RedisOpts,
ct);
ct).ConfigureAwait(false);
return result;
}

public async Task InvalidateAllAsync(CancellationToken ct = default)
{
localCache.Remove(BlogPostCacheKeys.All);
// CancellationToken.None: the DB write already committed — must not be cancelled
await distributedCache.RemoveAsync(BlogPostCacheKeys.All, CancellationToken.None);
await distributedCache.RemoveAsync(BlogPostCacheKeys.All, CancellationToken.None).ConfigureAwait(false);
}

public async Task InvalidateByIdAsync(Guid id, CancellationToken ct = default)
{
var key = BlogPostCacheKeys.ById(id);
localCache.Remove(key);
// CancellationToken.None: the DB write already committed — must not be cancelled
await distributedCache.RemoveAsync(key, CancellationToken.None);
await distributedCache.RemoveAsync(key, CancellationToken.None).ConfigureAwait(false);
}
}
Loading
Loading