diff --git a/Weasel.slnx b/Weasel.slnx index 3298e914..b03dd61c 100644 --- a/Weasel.slnx +++ b/Weasel.slnx @@ -54,6 +54,7 @@ + diff --git a/src/Weasel.Core.AotSmoke/Program.cs b/src/Weasel.Core.AotSmoke/Program.cs new file mode 100644 index 00000000..ebc24af9 --- /dev/null +++ b/src/Weasel.Core.AotSmoke/Program.cs @@ -0,0 +1,297 @@ +// AOT smoke test (weasel#263 / JasperFx/jasperfx#213). +// +// This program touches a representative cross-section of the AOT-clean +// Weasel.Core surface. The csproj sets IsAotCompatible=true, TrimMode=full, +// and promotes the AOT analyzer warning codes to errors, so any change that +// adds [RequiresDynamicCode] / [RequiresUnreferencedCode] to an API +// exercised here — or any change to this file that calls into a reflective +// Weasel.Core surface — fails the build in CI. +// +// The consolidation surfaces from #270 are the primary target: +// - SchemaObjectBase / SequenceBase / FunctionBase / ViewBase (covered +// indirectly via TableBase, which extends SchemaObjectBase) +// - TableBase +// - ForeignKeyBase (Parse, LinkColumns, Equals/GetHashCode) +// - IDdlSyntaxStrategy +// - DbObjectName, CascadeAction, EnumStorage, CreationStyle, +// SchemaPatchDifference, SqlFormatting +// +// Intentionally *not* exercised here (those carry AOT annotations by design): +// - AssertCommand.Execute is [RequiresDynamicCode] (Spectre.Console +// ExceptionFormatter dependency). +// - CommandBuilderBase.AddParameters(object) wraps an unconditional +// IL2075 suppression for the parameters→GetType()→GetProperties chain +// (the parameter is annotated [DynamicallyAccessedMembers] as the +// caller-facing contract). + +using System.Data.Common; +using JasperFx; +using Weasel.Core; +using Weasel.Core.Migrations; +using DbCommandBuilder = Weasel.Core.DbCommandBuilder; + +// --- DbObjectName -------------------------------------------------------- +// Pure value object; ToString / QualifiedName should be deterministic. + +var id = new DbObjectName("public", "smoke_test"); +if (id.QualifiedName != "public.smoke_test") +{ + Console.Error.WriteLine($"DbObjectName.QualifiedName regression: {id.QualifiedName}"); + return 1; +} + +// --- Enums round-trip --------------------------------------------------- +// Touch each enum surface that consumers commonly use. + +if (CascadeAction.Cascade.ToString() != "Cascade" || + EnumStorage.AsInteger.ToString() != "AsInteger" || + CreationStyle.CreateIfNotExists.ToString() != "CreateIfNotExists" || + SchemaPatchDifference.None.ToString() != "None" || + SqlFormatting.Concise.ToString() != "Concise" || + BulkInsertMode.InsertsOnly.ToString() != "InsertsOnly" || + AutoCreate.None.ToString() != "None") +{ + Console.Error.WriteLine("Enum ToString regression."); + return 1; +} + +// --- ForeignKeyBase shared parsing helpers ------------------------------ +// ForeignKeyBase.Parse and ParseCascadeClause are consolidation outputs +// of #270 step 4. Exercise via a minimal subclass. + +var fk = new SmokeForeignKey("smoke_fkey"); +fk.Parse("FOREIGN KEY (state_id) REFERENCES states(id) ON DELETE CASCADE ON UPDATE NO ACTION"); +if (fk.ColumnNames.Length != 1 || fk.ColumnNames[0].Trim() != "state_id" || + fk.LinkedNames.Length != 1 || fk.LinkedNames[0].Trim() != "id" || + fk.LinkedTable?.QualifiedName != "smoke.states" || + fk.DeleteAction != CascadeAction.Cascade || + fk.UpdateAction != CascadeAction.NoAction) +{ + Console.Error.WriteLine($"ForeignKeyBase.Parse regression: " + + $"cols={string.Join(",", fk.ColumnNames)} " + + $"linked={string.Join(",", fk.LinkedNames)} " + + $"table={fk.LinkedTable?.QualifiedName} " + + $"del={fk.DeleteAction} upd={fk.UpdateAction}"); + return 1; +} + +// LinkColumns appends; structural Equals across same-provider-root FKs. +var fk2 = new SmokeForeignKey("smoke_fkey"); +fk2.LinkColumns("state_id", "id"); +fk2.LinkedTable = new DbObjectName("smoke", "states"); +fk2.DeleteAction = CascadeAction.Cascade; +fk2.UpdateAction = CascadeAction.NoAction; +if (!fk.Equals(fk2)) +{ + Console.Error.WriteLine("ForeignKeyBase.Equals regression: structurally-equal FKs compared unequal."); + return 1; +} + +// --- TableBase consolidation surface ------------------------------------ +// Build a Table via the abstract base, exercise the column / index / PK +// helpers and the explicit ITable interface implementations. + +ITable table = new SmokeTable(id); +table.AddColumn("id", typeof(int)); +table.AddPrimaryKeyColumn("tenant_id", typeof(string)); +var added = (table as SmokeTable)!; +if (!added.HasColumn("id") || added.ColumnFor("tenant_id") is null) +{ + Console.Error.WriteLine("TableBase HasColumn/ColumnFor regression."); + return 1; +} + +// PrimaryKeyName auto-default via DefaultPrimaryKeyName hook. +if (added.PrimaryKeyName != "pk_smoke_test_tenant_id") +{ + Console.Error.WriteLine($"TableBase.PrimaryKeyName regression: {added.PrimaryKeyName}"); + return 1; +} + +// ITable.AddForeignKey routes through the abstract CreateForeignKey hook. +var fkBase = table.AddForeignKey("fk_smoke_states", new DbObjectName("smoke", "states"), + new[] { "state_id" }, new[] { "id" }); +if (fkBase.Name != "fk_smoke_states") +{ + Console.Error.WriteLine("ITable.AddForeignKey regression."); + return 1; +} + +// RemoveColumn is case-insensitive on every provider. +added.RemoveColumn("ID"); +if (added.HasColumn("id")) +{ + Console.Error.WriteLine("TableBase.RemoveColumn (case-insensitive) regression."); + return 1; +} + +added.IgnoreIndex("smoke_ignored_idx"); +if (!added.HasIgnoredIndex("smoke_ignored_idx")) +{ + Console.Error.WriteLine("TableBase.IgnoreIndex regression."); + return 1; +} + +// --- IDdlSyntaxStrategy -------------------------------------------------- +// #270 step 8 — pluggable per-provider DDL syntax decisions. Exercise the +// interface via a minimal implementation; consumer code must be able to +// call the strategy methods without AOT-hostile reflection. + +IDdlSyntaxStrategy syntax = new SmokeSyntax(); +var w = new StringWriter(); +syntax.WriteDropTable(w, id); +syntax.WriteCreateTableHeader(w, id, CreationStyle.CreateIfNotExists); +if (syntax.QuoteIdentifier("x") != "\"x\"" || + syntax.InlineForeignKeyConstraints || + syntax.AutoIncrementToken != "SMOKE_INCR" || + syntax.StatementTerminator != ";") +{ + Console.Error.WriteLine("IDdlSyntaxStrategy regression."); + return 1; +} + +Console.WriteLine($"Weasel.Core AOT smoke OK — exercised {nameof(DbObjectName)}, " + + $"{nameof(ForeignKeyBase)}.Parse, {nameof(TableBase)}, " + + $"{nameof(IDdlSyntaxStrategy)}."); +return 0; + + +// =========================================================================== +// Minimal stubs — implement just enough of each abstract base to instantiate +// it from this consumer project. Bodies that aren't exercised throw, since +// the smoke test only cares whether the surface compiles cleanly under +// IsAotCompatible=true + TrimMode=full. +// =========================================================================== + +internal sealed class SmokeColumn(string name, string type): ITableColumn +{ + public string Name { get; } = name; + public bool AllowNulls { get; set; } = true; + public string? DefaultExpression { get; set; } + public string Type { get; set; } = type; + public bool IsPrimaryKey { get; set; } +} + +internal sealed class SmokeIndex(string name): INamed +{ + public string Name { get; } = name; +} + +internal sealed class SmokeForeignKey(string name): ForeignKeyBase(name) +{ + private string[] _columnNames = Array.Empty(); + private string[] _linkedNames = Array.Empty(); + + public override string[] ColumnNames + { + get => _columnNames; + set => _columnNames = value; + } + + public override string[] LinkedNames + { + get => _linkedNames; + set => _linkedNames = value; + } + + // The Parse helper in ForeignKeyBase falls back to the supplied default + // schema when the catalog row's table name is unqualified. The smoke + // test passes "states" (unqualified), so we expect "smoke.states". + public void Parse(string definition) => base.Parse(definition, defaultSchema: "smoke"); + + protected override DbObjectName ParseLinkedTable(string tableName) + => new DbObjectName(tableName.Contains('.') ? tableName.Split('.')[0] : "smoke", + tableName.Contains('.') ? tableName.Split('.')[1] : tableName); +} + +internal sealed class SmokeTable(DbObjectName identifier) + : TableBase(identifier) +{ + public override IReadOnlyList PrimaryKeyColumns + => _columns.Where(c => c.IsPrimaryKey).Select(c => c.Name).ToList(); + + protected override string DefaultPrimaryKeyName() + => $"pk_{Identifier.Name}_{string.Join("_", PrimaryKeyColumns)}"; + + protected override SmokeForeignKey CreateForeignKey(string name) => new(name); + + protected override ITableColumn AddColumnAndReturn(string name, string columnType) + { + var col = new SmokeColumn(name, columnType); + _columns.Add(col); + return col; + } + + protected override ITableColumn AddPrimaryKeyColumnAndReturn(string name, string columnType) + { + var col = new SmokeColumn(name, columnType) { IsPrimaryKey = true }; + _columns.Add(col); + return col; + } + + protected override string GetDatabaseTypeFor(Type dotnetType) => dotnetType.Name; + + protected override Migrator GetDefaultMigratorForBasicSql() => new SmokeMigrator(); + + public override void WriteCreateStatement(Migrator migrator, TextWriter writer) + => writer.Write($"CREATE TABLE {Identifier};"); + + public override void WriteDropStatement(Migrator rules, TextWriter writer) + => writer.Write($"DROP TABLE {Identifier};"); + + public override void ConfigureQueryCommand(DbCommandBuilder builder) + { + // No-op — the smoke test never executes a catalog query. + } +} + +internal sealed class SmokeMigrator(): Migrator("smoke") +{ + public override IDatabaseProvider Provider + => throw new NotSupportedException("Smoke migrator has no provider."); + + public override IDatabaseWithTables CreateDatabase(DbConnection connection, string? identifier = null) + => throw new NotSupportedException(); + + public override bool MatchesConnection(DbConnection connection) => false; + + public override ITable CreateTable(DbObjectName identifier) => new SmokeTable(identifier); + + public override void WriteScript(TextWriter writer, Action writeStep) + => writeStep(this, writer); + + public override void WriteSchemaCreationSql(IEnumerable schemaNames, TextWriter writer) { } + public override void WriteSchemaDropSql(IEnumerable schemaNames, TextWriter writer) { } + public override string ToExecuteScriptLine(string scriptName) => string.Empty; + public override void AssertValidIdentifier(string name) { } + public override string GenerateDeleteAllSql(IReadOnlyList tables, bool resetIdentity = true) + => string.Empty; + + protected override Task executeDelta(SchemaMigration migration, DbConnection conn, AutoCreate autoCreate, + IMigrationLogger logger, CancellationToken ct = default) => Task.CompletedTask; +} + +internal sealed class SmokeSyntax: IDdlSyntaxStrategy +{ + public string QuoteIdentifier(string name) => $"\"{name}\""; + + public void WriteDropTable(TextWriter writer, DbObjectName identifier) + => writer.WriteLine($"DROP TABLE IF EXISTS {identifier};"); + + public void WriteCreateTableHeader(TextWriter writer, DbObjectName identifier, CreationStyle style) + { + if (style == CreationStyle.DropThenCreate) + { + writer.WriteLine($"CREATE TABLE {identifier} ("); + } + else + { + writer.WriteLine($"CREATE TABLE IF NOT EXISTS {identifier} ("); + } + } + + public bool InlineForeignKeyConstraints => false; + public string AutoIncrementToken => "SMOKE_INCR"; + public string StatementTerminator => ";"; +} + diff --git a/src/Weasel.Core.AotSmoke/Weasel.Core.AotSmoke.csproj b/src/Weasel.Core.AotSmoke/Weasel.Core.AotSmoke.csproj new file mode 100644 index 00000000..6e828306 --- /dev/null +++ b/src/Weasel.Core.AotSmoke/Weasel.Core.AotSmoke.csproj @@ -0,0 +1,39 @@ + + + + Exe + false + + + true + full + IL2026;IL2046;IL2055;IL2065;IL2067;IL2070;IL2072;IL2075;IL2090;IL2091;IL2111;IL3050;IL3051 + + + + + + + diff --git a/src/Weasel.Core/CommandBuilderBase.cs b/src/Weasel.Core/CommandBuilderBase.cs index b1e86a90..6c6c45ec 100644 --- a/src/Weasel.Core/CommandBuilderBase.cs +++ b/src/Weasel.Core/CommandBuilderBase.cs @@ -275,21 +275,26 @@ public void AppendParameter(string value) /// For each public property of the parameters object, adds a new parameter /// to the command with the name of the property and the current value of the property /// on the parameters object. Does *not* affect the command text. - /// The on - /// declares to the trimmer that the runtime type of the passed object must have its - /// preserved (see #266 / - /// JasperFx/jasperfx#213). The trimmer's flow analysis doesn't currently propagate that - /// annotation through object.GetType() at the call site below, so an - /// documents that the IL2075 - /// warning is expected — the DAM on the parameter is the caller-facing contract, and the - /// long-term fix is the source-generator path (path 2 in #266). + /// + /// This overload reflects over the parameters object's public properties via + /// , which the trimmer can't statically analyse. + /// The below is the caller-facing + /// contract — AOT-trim-clean consumers should prefer the + /// / + /// overloads, which don't + /// reflect. The silences the + /// IL2075 warning at the GetType().GetProperties() call site below; the + /// runtime contract is documented in the + /// message. Surfaced by Weasel.Core.AotSmoke (weasel#263 / weasel#266 / + /// JasperFx/jasperfx#213); the long-term fix is the source-generator path + /// (path 2 in #266). + /// /// /// [RequiresUnreferencedCode("AddParameters(object) reflects on the parameters object's public properties via Type.GetProperties(). Use the IDictionary overload when publishing AOT-trim-clean.")] [UnconditionalSuppressMessage("Trimming", "IL2075", - Justification = "Runtime contract is documented via the DynamicallyAccessedMembers attribute on the parameter; the trimmer's flow analysis through object.GetType() doesn't see it but the contract holds. See #266.")] - public void AddParameters( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] object parameters) + Justification = "Reflective property enumeration is gated by RequiresUnreferencedCode on this method; the IL2075 at the GetType().GetProperties() call site is the cost of that path. See weasel#266.")] + public void AddParameters(object parameters) { if (parameters == null) { diff --git a/src/Weasel.Core/CommandLine/AssertCommand.cs b/src/Weasel.Core/CommandLine/AssertCommand.cs index 34d7b749..056e3548 100644 --- a/src/Weasel.Core/CommandLine/AssertCommand.cs +++ b/src/Weasel.Core/CommandLine/AssertCommand.cs @@ -16,18 +16,22 @@ public class AssertCommand: JasperFxAsyncCommand /// on Weasel.Core. /// /// This is a dev-time CLI tool (the db-assert command), not on any - /// hot path — annotating the entry point so the warning propagates to AOT- - /// publishing consumers as a precise diagnostic is the right minimum-blast- - /// radius fix (per JasperFx/jasperfx#213). End users targeting AOT can - /// either avoid this command, or substitute a non-Spectre exception - /// formatter in their own host. + /// hot path. Earlier passes tried [RequiresDynamicCode] on this + /// override to propagate the diagnostic; the analyzer rejects that with + /// IL3051 because the base member + /// (JasperFx.CommandLine.JasperFxAsyncCommand<T>.Execute) + /// doesn't carry the same annotation. The + /// below silences the + /// underlying IL3050 with a Justification — end users targeting AOT can + /// either avoid this command or substitute a non-Spectre exception + /// formatter in their own host. Surfaced by Weasel.Core.AotSmoke + /// (weasel#263 / JasperFx/jasperfx#213). /// /// - [RequiresDynamicCode("Uses Spectre.Console.AnsiConsole.WriteException, whose ExceptionFormatter requires runtime IL generation that isn't available under PublishAot.")] [UnconditionalSuppressMessage( "AOT", "IL3050", - Justification = "AnsiConsole.WriteException is only reached on the dev-time db-assert command path. weasel#265 / JasperFx/jasperfx#213.")] + Justification = "AnsiConsole.WriteException's ExceptionFormatter needs runtime IL generation, but this is the dev-time db-assert command path — never reached in an AOT-published consumer. weasel#265.")] public override async Task Execute(WeaselInput input) { AnsiConsole.Write( diff --git a/src/Weasel.EntityFrameworkCore/Batching/BatchedQuery.cs b/src/Weasel.EntityFrameworkCore/Batching/BatchedQuery.cs index 8d197f64..3e1f8fff 100644 --- a/src/Weasel.EntityFrameworkCore/Batching/BatchedQuery.cs +++ b/src/Weasel.EntityFrameworkCore/Batching/BatchedQuery.cs @@ -29,7 +29,15 @@ public BatchedQuery(DbContext context) /// /// Queues a query that returns a list of entities. /// The query is compiled to SQL immediately but not executed until . + /// + /// + /// reflects over the entity type's members; EF Core itself isn't AOT-ready upstream + /// (tracked as dotnet/efcore#29761). Suppressed locally — Weasel.EntityFrameworkCore + /// consumers targeting AOT inherit EF Core's overall AOT limitations. weasel#263. + /// /// + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2087", + Justification = "EF Core's FindEntityType reflects on the entity type; EF Core upstream is not AOT-ready (dotnet/efcore#29761). weasel#263.")] public Task> Query(IQueryable queryable) where T : class, new() { var entityType = _context.Model.FindEntityType(typeof(T)) @@ -47,6 +55,8 @@ public BatchedQuery(DbContext context) /// /// Queues a query that returns a single entity or null. /// + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2087", + Justification = "EF Core's FindEntityType reflects on the entity type; EF Core upstream is not AOT-ready (dotnet/efcore#29761). weasel#263.")] public Task QuerySingle(IQueryable queryable) where T : class, new() { var entityType = _context.Model.FindEntityType(typeof(T)) diff --git a/src/Weasel.EntityFrameworkCore/DatabaseCleanerExtensions.cs b/src/Weasel.EntityFrameworkCore/DatabaseCleanerExtensions.cs index cbb77ab7..aa96d540 100644 --- a/src/Weasel.EntityFrameworkCore/DatabaseCleanerExtensions.cs +++ b/src/Weasel.EntityFrameworkCore/DatabaseCleanerExtensions.cs @@ -20,7 +20,17 @@ public static IServiceCollection AddDatabaseCleaner(this IServiceColle /// Registers an implementation that seeds data /// after . /// Multiple seeders execute in registration order. + /// + /// + /// requires + /// on TImplementation; propagating that to TData here would cascade to + /// every caller. Suppressed with Justification — AOT consumers that register seeders + /// should ensure each seeder type is rooted (e.g. via a DynamicDependency or + /// by referencing the constructor directly). weasel#263. + /// /// + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", + Justification = "AddTransient needs DAM.PublicConstructors on TImplementation. Caller is expected to root each IInitialData implementation via DynamicDependency or direct reference. weasel#263.")] public static IServiceCollection AddInitialData(this IServiceCollection services) where TContext : DbContext where TData : class, IInitialData diff --git a/src/Weasel.EntityFrameworkCore/DbContextExtensions.cs b/src/Weasel.EntityFrameworkCore/DbContextExtensions.cs index 158fab03..96a6419a 100644 --- a/src/Weasel.EntityFrameworkCore/DbContextExtensions.cs +++ b/src/Weasel.EntityFrameworkCore/DbContextExtensions.cs @@ -132,7 +132,16 @@ public static (DbConnection conn, Migrator? migrator) FindMigratorForDbContext(t /// /// Finds the DbDataSource configured on a DbContext via EF Core options extensions. /// Returns null if the context was not configured with a data source. + /// + /// Reflects on the runtime type of each IDbContextOptionsExtension looking for + /// a DataSource property — the canonical extension carrying the data source isn't + /// uniformly typed across EF Core provider packages. Suppressed locally; AOT consumers + /// should rely on EF Core's typed configuration APIs instead of this reflective fallback. + /// weasel#263. + /// /// + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2075", + Justification = "Reflective lookup of the DataSource property on an arbitrary IDbContextOptionsExtension; EF Core upstream is not AOT-ready (dotnet/efcore#29761). weasel#263.")] internal static DbDataSource? FindDataSource(DbContext context) { var dbContextOptions = context.GetService(); diff --git a/src/Weasel.EntityFrameworkCore/Weasel.EntityFrameworkCore.csproj b/src/Weasel.EntityFrameworkCore/Weasel.EntityFrameworkCore.csproj index 71a28758..6cc9b9a9 100644 --- a/src/Weasel.EntityFrameworkCore/Weasel.EntityFrameworkCore.csproj +++ b/src/Weasel.EntityFrameworkCore/Weasel.EntityFrameworkCore.csproj @@ -9,6 +9,9 @@ true true true + + true diff --git a/src/Weasel.MySql/Weasel.MySql.csproj b/src/Weasel.MySql/Weasel.MySql.csproj index 46d10f9d..4e42a17a 100644 --- a/src/Weasel.MySql/Weasel.MySql.csproj +++ b/src/Weasel.MySql/Weasel.MySql.csproj @@ -3,6 +3,9 @@ MySql Support for Weasel mysql;ddl;schema;migration + + true diff --git a/src/Weasel.Oracle/Weasel.Oracle.csproj b/src/Weasel.Oracle/Weasel.Oracle.csproj index 8bcf4851..d183e539 100644 --- a/src/Weasel.Oracle/Weasel.Oracle.csproj +++ b/src/Weasel.Oracle/Weasel.Oracle.csproj @@ -11,6 +11,9 @@ true true true + + true diff --git a/src/Weasel.Postgresql/BatchBuilder.cs b/src/Weasel.Postgresql/BatchBuilder.cs index 6f362d33..abed02ae 100644 --- a/src/Weasel.Postgresql/BatchBuilder.cs +++ b/src/Weasel.Postgresql/BatchBuilder.cs @@ -189,6 +189,15 @@ public void StartNewCommand() _current = appendCommand(); } + /// + /// Reflective property-enumeration overload — see + /// + /// for the equivalent caller-contract on the base. weasel#263 / weasel#266. + /// + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode( + "AddParameters(object) reflects on the parameters object's public properties via Type.GetProperties(). Use the IDictionary overload when publishing AOT-trim-clean.")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2075", + Justification = "Reflective property enumeration is gated by RequiresUnreferencedCode on this method; the IL2075 at the GetType().GetProperties() call site is the cost of that path. See weasel#266.")] public void AddParameters(object parameters) { _current ??= appendCommand(); diff --git a/src/Weasel.Postgresql/ICommandBuilder.cs b/src/Weasel.Postgresql/ICommandBuilder.cs index 60801826..ca6b4974 100644 --- a/src/Weasel.Postgresql/ICommandBuilder.cs +++ b/src/Weasel.Postgresql/ICommandBuilder.cs @@ -49,9 +49,17 @@ public interface ICommandBuilder /// /// Use an anonymous type to add named parameters. - /// If a dictionary is passed in then its key-value pairs will be used as named parameters + /// If a dictionary is passed in then its key-value pairs will be used as named parameters. + /// + /// Annotated to + /// match + /// — both reflect over the parameters object's public properties. AOT-trim-clean + /// consumers should prefer the dictionary overloads below. + /// /// /// + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode( + "AddParameters(object) reflects on the parameters object's public properties via Type.GetProperties(). Use the IDictionary overload when publishing AOT-trim-clean.")] void AddParameters(object parameters); /// diff --git a/src/Weasel.Postgresql/PostgresqlProvider.cs b/src/Weasel.Postgresql/PostgresqlProvider.cs index 6b259f01..2aafe976 100644 --- a/src/Weasel.Postgresql/PostgresqlProvider.cs +++ b/src/Weasel.Postgresql/PostgresqlProvider.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Reflection; using ImTools; using JasperFx.Core; @@ -175,6 +176,14 @@ public string ConvertSynonyms(string type) return type; } + // Reflective interface enumeration on `type.ImplementedInterfaces` is gated by + // earlier memo / IsNullable / IsArray / IsEnum branches — only types Npgsql doesn't + // ship a built-in mapping for and that aren't directly enumerable hit this path. + // The base contract for determineParameterType in DatabaseProvider<,,> is a + // cold-path resolver, so AOT consumers that target only the pre-mapped types + // never traverse this. weasel#263 audit. + [UnconditionalSuppressMessage("Trimming", "IL2070", + Justification = "Reflective IEnumerable detection on an arbitrary CLR type. Only reached for types not in the memo / not arrays / not enums; AOT consumers that pre-register their types via storeMappings never hit this path. weasel#263.")] protected override bool determineParameterType(Type type, out NpgsqlDbType dbType) { var npgsqlDbType = ResolveNpgsqlDbType(type); @@ -293,6 +302,14 @@ public bool HasTypeMapping(Type memberType) return ResolveDatabaseType(memberType) != null || memberType.IsEnum; } + /// + /// Construct a closed over the supplied value type. + /// Uses at runtime, which AOT can't generate + /// code for ahead of time — AOT consumers that pre-register their types via + /// bypass this helper and never JIT a new closed + /// Nullable<T>. weasel#263 audit. + /// + [RequiresDynamicCode("Type.MakeGenericType(Nullable<>) requires runtime IL generation. Pre-register types via storeMappings to avoid this path when publishing AOT.")] private Type GetNullableType(Type type) { type = Nullable.GetUnderlyingType(type) ?? type; @@ -304,6 +321,20 @@ private Type GetNullableType(Type type) return type; } + /// + /// Register additional CLR types to be treated as PostgreSQL timestamp / + /// timestamptz. Internally calls which + /// constructs closed generics via + /// . overrides an + /// abstract base and can't be annotated with + /// without IL3051 against DatabaseProvider<,,>.storeMappings, so we + /// suppress the IL3050 here + /// with a Justification. AOT consumers that pre-register the nullable form alongside + /// the value type via bypass this helper entirely. + /// weasel#263. + /// + [UnconditionalSuppressMessage("AOT", "IL3050", + Justification = "Calls GetNullableType which uses Type.MakeGenericType for closed Nullable construction. Pre-registering both T and Nullable via RegisterMapping avoids this path; documented in the AOT audit (weasel#263).")] public void AddTimespanTypes(NpgsqlDbType npgsqlDbType, params Type[] types) { var timespanTypesList = npgsqlDbType == NpgsqlDbType.Timestamp ? TimespanTypes : TimespanZTypes; diff --git a/src/Weasel.Postgresql/SqlGeneration/CustomizableWhereFragment.cs b/src/Weasel.Postgresql/SqlGeneration/CustomizableWhereFragment.cs index c08b4149..16b00642 100644 --- a/src/Weasel.Postgresql/SqlGeneration/CustomizableWhereFragment.cs +++ b/src/Weasel.Postgresql/SqlGeneration/CustomizableWhereFragment.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Diagnostics.CodeAnalysis; using JasperFx.Core.Reflection; namespace Weasel.Postgresql.SqlGeneration; @@ -16,6 +17,21 @@ public CustomizableWhereFragment(string sql, string paramReplacementToken, param _token = paramReplacementToken.ToCharArray()[0]; } + /// + /// Calls when the first parameter + /// looks like an anonymous type or IDictionary with string keys. That + /// overload reflects over the object's public properties — annotated upstream with + /// . has + /// wide internal implementation, so propagating [RequiresUnreferencedCode] here + /// would force every implementor to declare it too (IL2046 cascade). Instead we + /// suppress the IL2026 + /// diagnostic at the two call sites below with a Justification — AOT-trim-clean + /// consumers should construct this fragment with explicit + /// entries (the array-of-parameters path, not the + /// anonymous-type path). weasel#263. + /// + [UnconditionalSuppressMessage("Trimming", "IL2026", + Justification = "ICommandBuilder.AddParameters(object) is only reached on the anonymous-type / IDictionary input shape; AOT consumers pass explicit CommandParameter[] entries which take the lower branch. weasel#263.")] public void Apply(ICommandBuilder builder) { // backwards compatibility, old version of this class accepted CommandParameter[] diff --git a/src/Weasel.Postgresql/Weasel.Postgresql.csproj b/src/Weasel.Postgresql/Weasel.Postgresql.csproj index ebd8c167..e0c795e6 100644 --- a/src/Weasel.Postgresql/Weasel.Postgresql.csproj +++ b/src/Weasel.Postgresql/Weasel.Postgresql.csproj @@ -11,6 +11,10 @@ true true true + + true diff --git a/src/Weasel.SqlServer/BatchBuilder.cs b/src/Weasel.SqlServer/BatchBuilder.cs index 65e6c26a..590176c3 100644 --- a/src/Weasel.SqlServer/BatchBuilder.cs +++ b/src/Weasel.SqlServer/BatchBuilder.cs @@ -213,6 +213,15 @@ public void StartNewCommand() _current = appendCommand(); } + /// + /// Reflective property-enumeration overload — see + /// + /// for the equivalent caller-contract on the base. weasel#263 / weasel#266. + /// + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode( + "AddParameters(object) reflects on the parameters object's public properties via Type.GetProperties(). Use the IDictionary overload when publishing AOT-trim-clean.")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2075", + Justification = "Reflective property enumeration is gated by RequiresUnreferencedCode on this method; the IL2075 at the GetType().GetProperties() call site is the cost of that path. See weasel#266.")] public void AddParameters(object parameters) { _current ??= appendCommand(); diff --git a/src/Weasel.SqlServer/ICommandBuilder.cs b/src/Weasel.SqlServer/ICommandBuilder.cs index 4243f9d9..4989192e 100644 --- a/src/Weasel.SqlServer/ICommandBuilder.cs +++ b/src/Weasel.SqlServer/ICommandBuilder.cs @@ -48,9 +48,17 @@ public interface ICommandBuilder /// /// Use an anonymous type to add named parameters. - /// If a dictionary is passed in then its key-value pairs will be used as named parameters + /// If a dictionary is passed in then its key-value pairs will be used as named parameters. + /// + /// Annotated to + /// match + /// — both reflect over the parameters object's public properties. AOT-trim-clean + /// consumers should prefer the dictionary overloads below. + /// /// /// + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode( + "AddParameters(object) reflects on the parameters object's public properties via Type.GetProperties(). Use the IDictionary overload when publishing AOT-trim-clean.")] void AddParameters(object parameters); /// diff --git a/src/Weasel.SqlServer/Weasel.SqlServer.csproj b/src/Weasel.SqlServer/Weasel.SqlServer.csproj index f185daab..2abf93b4 100644 --- a/src/Weasel.SqlServer/Weasel.SqlServer.csproj +++ b/src/Weasel.SqlServer/Weasel.SqlServer.csproj @@ -11,6 +11,9 @@ true true true + + true diff --git a/src/Weasel.Sqlite/Weasel.Sqlite.csproj b/src/Weasel.Sqlite/Weasel.Sqlite.csproj index f964adc7..55f2cff8 100644 --- a/src/Weasel.Sqlite/Weasel.Sqlite.csproj +++ b/src/Weasel.Sqlite/Weasel.Sqlite.csproj @@ -11,6 +11,9 @@ true true true + + true