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