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
7 changes: 7 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ updates:
labels:
- "dependencies"
- "dotnet"
# Cooldown: wait 7 days after a release before proposing an update, so freshly
# published packages have time to have any release-day issues surfaced by the
# ecosystem before they land in a PR here.
cooldown:
default-days: 7
ignore:
# The FsCheck.Xunit 3.x migration is done (#175). Keep holding *future major*
# bumps (4.x+) so a breaking API change to the published contract-test bases
Expand All @@ -27,6 +32,8 @@ updates:
labels:
- "dependencies"
- "github-actions"
cooldown:
default-days: 7
groups:
github-actions:
patterns:
Expand Down
54 changes: 54 additions & 0 deletions AuditTrail.slnx.DotSettings
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<!--
ReSharper InspectCode noise-floor (see issue #231).

Purpose: silence InspectCode inspections that are either (a) already covered
and gated by the build's Roslyn analyzer stack (Meziantou, SonarAnalyzer,
Roslynator, AsyncFixer, VS.Threading, BannedApiAnalyzers, .NET SDK analyzers)
— double-reporting them here adds no signal — or (b) known false positives on
this repo's multi-package / multi-TFM layout. The goal is 0 noise findings on
a clean main so the first error after the job lands is always actionable.

Only error-severity findings gate merge (pr.yaml inspectcode job); these
entries drop the corresponding warnings from Code Scanning entirely.

Follows the canonical noise-floor profile proven out in Extensions-Logging-Data
(repo-template#431 rollout).
-->

<!-- CheckNamespace: all AuditTrail library code deliberately declares the flat
root namespace `Wolfgang.AuditTrail` (plus `Wolfgang.AuditTrail.Entities` /
`.Schema` / `.Serializers`), independent of which package (Abstractions,
EntityFrameworkCore, TestKit.Xunit) the physical folder lives in. The
Polyfills/ folder additionally declares the polyfill types under their
runtime `System.*` namespace on TFMs that lack them. R# expects namespace
to mirror the folder path — a valid check on a single-project solution,
but noise for this multi-package fleet layout. -->
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CheckNamespace/@EntryIndexedValue">DO_NOT_SHOW</s:String>

<!-- Redundant-code style rules — covered in-build by Roslynator / IDE00xx and
.editorconfig. RedundantUsingDirective in particular is a FALSE POSITIVE on
src library projects: <ImplicitUsings> is enabled only for net10.0 (and
net8.0 on some projects), so explicit System usings are REQUIRED on
netstandard2.0 / net6.0 but look redundant to InspectCode (which analyses
a single TFM). -->
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantUsingDirective/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantNameQualifier/@EntryIndexedValue">DO_NOT_SHOW</s:String>

<!-- Nullability — owned by the Roslyn nullable reference-type analysis, not R#.
The *AccordingTo(Nullable)APIContract rules flag defensive null-checks on
non-null-annotated parameters; those checks are INTENTIONAL in a library
that is called from nullable-oblivious code where callers can still pass
null despite the annotation. RedundantSuppressNullableWarningExpression
duplicates SonarAnalyzer S8969, which is the rule we actually gate on. -->
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantSuppressNullableWarningExpression/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract/@EntryIndexedValue">DO_NOT_SHOW</s:String>

<!-- Public-API false positives: R# cannot see external consumers of a library's
public auto-properties or record positional properties, so it reports them
as unused (.Global variant). The NuGet-published types are the whole point
of the library — treat both rules as noise. -->
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedAutoPropertyAccessor_002EGlobal/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=NotAccessedPositionalProperty_002EGlobal/@EntryIndexedValue">DO_NOT_SHOW</s:String>
</wpf:ResourceDictionary>
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ private static IHostBuilder AddSingleConfigFile
bool reloadOnChange
)
{
builder.ConfigureAppConfiguration((context, configurationBuilder) =>
builder.ConfigureAppConfiguration((_, configurationBuilder) =>
{
configurationBuilder
.SetBasePath(AppContext.BaseDirectory)
Expand Down
2 changes: 1 addition & 1 deletion src/Wolfgang.AuditTrail.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ private static async Task<int> Main(string[] args)
.ReadFrom.Configuration(context.Configuration)
.Enrich.WithProperty("Version", Assembly.GetEntryAssembly()?.GetName().Version);
})
.ConfigureServices((context, services) =>
.ConfigureServices((_, services) =>
{
services
.AddSingleton<IReporter, ConsoleReporter>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public static void SetItem(this DbContext context, string key, object? value)

public static T? GetItem<T>(this DbContext context, string key)
{
if (_state.TryGetValue(context, out var state) && state!.TryGetValue(key, out var value))
if (_state.TryGetValue(context, out var state) && state.TryGetValue(key, out var value))
{
return (T?)value;
}
Expand All @@ -47,7 +47,7 @@ public static void RemoveItem(this DbContext context, string key)
{
if (_state.TryGetValue(context, out var state))
{
state!.Remove(key);
state.Remove(key);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public async Task DropTablesAsync(DbContext context, CancellationToken cancellat
var headerTable = EnsureSafeIdentifier(_options.HeaderTableName, nameof(_options.HeaderTableName));
var schema = string.IsNullOrWhiteSpace(_options.Schema)
? null
: EnsureSafeIdentifier(_options.Schema!, nameof(_options.Schema));
: EnsureSafeIdentifier(_options.Schema, nameof(_options.Schema));

var detailFqn = QuoteIdentifier(context.Database.ProviderName, schema, detailTable);
var headerFqn = QuoteIdentifier(context.Database.ProviderName, schema, headerTable);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ public void Null_string_round_trips()
[Fact]
public void Empty_byte_array_round_trips()
{
var decoded = RoundTrip<byte[]>(Array.Empty<byte>());
var decoded = RoundTrip(Array.Empty<byte>());
Assert.Equal(Array.Empty<byte>(), decoded);
}

Expand Down
2 changes: 1 addition & 1 deletion tests/Wolfgang.AuditTrail.Cli.Tests.Unit/MigrateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public async Task OnExecuteAsync_when_env_var_set_uses_resolved_value()

Assert.Equal(ExitCode.Success, exit);
Assert.NotNull(runner.CapturedOptions);
Assert.Equal("Server=.;Database=fromenv", runner.CapturedOptions!.ConnectionString);
Assert.Equal("Server=.;Database=fromenv", runner.CapturedOptions.ConnectionString);
}
finally
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ public async Task Update_excludes_NotAudited_columns_from_detail_rows()
.Select(d => d.ColumnName)
.ToListAsync();

Assert.Contains(nameof(Customer.Name), columns);
Assert.DoesNotContain(nameof(Customer.Notes), columns);
Assert.Contains(nameof(Customer.Name), columns, StringComparer.Ordinal);
Assert.DoesNotContain(nameof(Customer.Notes), columns, StringComparer.Ordinal);
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,16 @@ public MappedColumnContext(DbContextOptions<MappedColumnContext> options, IAudit



// EF hydrates these via reflection; R# can't see the runtime usage.
// ReSharper disable UnusedMember.Local
// ReSharper disable UnusedAutoPropertyAccessor.Local
private sealed class MappedItem
{
public int Id { get; set; }

[Column("item_display")]
public string DisplayName { get; set; } = string.Empty;
}
// ReSharper restore UnusedAutoPropertyAccessor.Local
// ReSharper restore UnusedMember.Local
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ public async Task Delete_captures_OriginalValue_from_pre_save_snapshot()

// ── Fixture and entity isolated from the shared TestDbContext ───────────

// EF hydrates these via reflection; R# can't see the runtime usage.
// ReSharper disable UnusedAutoPropertyAccessor.Local
[ExcludeFromCodeCoverage]
private sealed class Widget
{
Expand All @@ -129,6 +131,7 @@ private sealed class Widget

public int RowVersion { get; set; }
}
// ReSharper restore UnusedAutoPropertyAccessor.Local



Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public async Task SaveChangesAsync_when_inserting_a_customer_writes_header_and_d
var detailsByColumn = header.Details.ToDictionary(d => d.ColumnName, StringComparer.Ordinal);
Assert.Equal("Alice", detailsByColumn["Name"].ValueText);
Assert.Equal("alice@example.com", detailsByColumn["Email"].ValueText);
Assert.DoesNotContain("Notes", detailsByColumn.Keys);
Assert.DoesNotContain("Notes", detailsByColumn.Keys, StringComparer.Ordinal);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,15 @@ private TestDbContext BuildContext(string? schema)
// testing can't attribute those mutants to these assertions. Force a
// fresh model per build so each test actually exercises the config.
.ReplaceService<IModelCacheKeyFactory, UncachedModelCacheKeyFactory>();
return new TestDbContext(builder.Options, new StaticAuditUserProvider("u", null), options);
return new TestDbContext(builder.Options, new StaticAuditUserProvider("u"), options);
}

// Interface-mandated signatures; parameters are intentionally unused because a
// unique key per call is exactly what disables the model cache.
// Interface-mandated signature; the parameters are intentionally unused because
// a unique key per call is exactly what disables the model cache.
#pragma warning disable RCS1163, S1172 // Unused parameter — intentional (unique key per call)
private sealed class UncachedModelCacheKeyFactory : IModelCacheKeyFactory
{
public object Create(DbContext context, bool designTime) => new object();

public object Create(DbContext context) => new object();
}
#pragma warning restore RCS1163, S1172

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public void Serialize_distinct_byte_array_keys_produce_distinct_strings()
var a = sut.Serialize(new object?[] { new byte[] { 0x01, 0x02, 0x03 } });
var b = sut.Serialize(new object?[] { new byte[] { 0x04, 0x05, 0x06 } });

Assert.NotEqual(a, b);
Assert.NotEqual(a, b, StringComparer.Ordinal);
Assert.Equal("010203", a);
Assert.Equal("040506", b);
}
Expand Down Expand Up @@ -61,7 +61,7 @@ public void Serialize_DateTime_with_fractional_seconds_does_not_truncate()
var sa = sut.Serialize(new object?[] { a });
var sb = sut.Serialize(new object?[] { b });

Assert.NotEqual(sa, sb);
Assert.NotEqual(sa, sb, StringComparer.Ordinal);
// "o" (round-trip) format preserves full tick precision + Z kind suffix.
Assert.EndsWith("Z", sa, StringComparison.Ordinal);
}
Expand All @@ -79,8 +79,8 @@ public void Serialize_DateTimeOffset_preserves_offset_and_fractional_seconds()
var c = new DateTimeOffset(2026, 1, 15, 14, 30, 45, TimeSpan.FromHours(2));
var d = new DateTimeOffset(2026, 1, 15, 12, 30, 45, TimeSpan.Zero);

Assert.NotEqual(sut.Serialize(new object?[] { a }), sut.Serialize(new object?[] { b }));
Assert.NotEqual(sut.Serialize(new object?[] { c }), sut.Serialize(new object?[] { d }));
Assert.NotEqual(sut.Serialize(new object?[] { a }), sut.Serialize(new object?[] { b }), StringComparer.Ordinal);
Assert.NotEqual(sut.Serialize(new object?[] { c }), sut.Serialize(new object?[] { d }), StringComparer.Ordinal);
}


Expand Down Expand Up @@ -115,7 +115,7 @@ public void Serialize_primitive_types_use_invariant_formatting(object value, str
{
var sut = new PipeDelimitedEntityKeySerializer();

Assert.Equal(expected, sut.Serialize(new object?[] { value }));
Assert.Equal(expected, sut.Serialize(new[] { value }));
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ public void AuditMigrationsDbContext_when_schema_is_set_routes_version_table_und
using var ctx = new AuditMigrationsDbContext(dbOptions, auditOptions);
var entity = ctx.Model.FindEntityType(typeof(AuditSchemaVersion));
Assert.NotNull(entity);
Assert.Equal("myaudit", entity!.GetSchema());
Assert.Equal("myaudit", entity.GetSchema());
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ public sealed class StringAuditValueSerializerExactFormatTests
{
private enum Color
{
// Kept so the enum has more than one member — the encoding test only
// exercises Green, but a single-member enum would be an odd shape.
// ReSharper disable once UnusedMember.Local
Red,
Green,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public void UseAuditing_adds_AuditSaveChangesInterceptor_to_DbContextOptions()
?.Interceptors;

Assert.NotNull(interceptors);
Assert.Contains(interceptors!, i => i is AuditSaveChangesInterceptor);
Assert.Contains(interceptors, i => i is AuditSaveChangesInterceptor);
}


Expand Down