@@ -15,7 +15,31 @@ abstract class SqlServerDialect
1515 /// </summary>
1616 protected static string Table < TEntity > ( ServiceControlDbContext dbContext ) => SchemaQualifiedTableName . For < TEntity > ( dbContext ) ;
1717
18- protected static async Task Execute ( ServiceControlDbContext dbContext , string sql , IEnumerable < object ? [ ] > rows , CancellationToken cancellationToken = default )
18+ /// <summary>
19+ /// How to declare each column of a statement's rows, taken from the properties the columns are
20+ /// mapped to. Pass the property names in the order the values sit in each row. A row may be
21+ /// shorter than this list, but not longer. A text column gets its length as its size, or -1 for nvarchar(max).
22+ /// </summary>
23+ protected static ColumnType [ ] ColumnTypes < TEntity > ( ServiceControlDbContext dbContext , params string [ ] properties )
24+ {
25+ var entityType = dbContext . Model . FindEntityType ( typeof ( TEntity ) )
26+ ?? throw new InvalidOperationException ( $ "{ typeof ( TEntity ) . Name } is not part of the model.") ;
27+
28+ return [ .. properties . Select ( name =>
29+ {
30+ var property = entityType . FindProperty ( name )
31+ ?? throw new InvalidOperationException ( $ "{ typeof ( TEntity ) . Name } has no property { name } .") ;
32+
33+ var dbType = property . GetRelationalTypeMapping ( ) . DbType
34+ ?? throw new InvalidOperationException ( $ "{ typeof ( TEntity ) . Name } .{ name } has no DbType in its type mapping.") ;
35+
36+ return new ColumnType ( dbType , dbType == DbType . String ? property . GetMaxLength ( ) ?? - 1 : null ) ;
37+ } ) ] ;
38+ }
39+
40+ protected readonly record struct ColumnType ( DbType DbType , int ? Size ) ;
41+
42+ protected static async Task Execute ( ServiceControlDbContext dbContext , string sql , ColumnType [ ] columnTypes , IEnumerable < object ? [ ] > rows , CancellationToken cancellationToken = default )
1943 {
2044 await using var command = dbContext . Database . GetDbConnection ( ) . CreateCommand ( ) ;
2145 command . Transaction = ( dbContext . Database . CurrentTransaction
@@ -25,18 +49,36 @@ protected static async Task Execute(ServiceControlDbContext dbContext, string sq
2549 var index = 0 ;
2650 foreach ( var row in rows )
2751 {
28- foreach ( var value in row )
52+ for ( var column = 0 ; column < row . Length ; column ++ )
2953 {
54+ var value = row [ column ] ;
3055 var parameter = command . CreateParameter ( ) ;
3156 parameter . ParameterName = $ "@p{ index ++ } ";
3257 parameter . Value = value ?? DBNull . Value ;
3358
34- // Attempt de-duplication compares LastAttemptedAt for equality, so datetime
35- // parameters must keep datetime2 precision instead of the datetime default .
59+ // Attempt de-duplication compares LastAttemptedAt for equality, so a datetime keeps
60+ // datetime2 precision wherever it sits, including the lone "now" that ResolveRetriedMessages sends first .
3661 if ( value is DateTime )
3762 {
3863 parameter . DbType = DbType . DateTime2 ;
3964 }
65+ else
66+ {
67+ // SQL Server reuses a plan only when the declarations match, so every value, null or not,
68+ // is declared by its column. A string longer than its column keeps its own length,
69+ // because SqlClient would silently cut it and SQL Server must refuse it instead.
70+ var ( dbType , size ) = columnTypes [ column ] ;
71+ if ( value is string && dbType != DbType . String )
72+ {
73+ throw new InvalidOperationException ( $ "Parameter { parameter . ParameterName } holds text, but its column is declared as { dbType } .") ;
74+ }
75+
76+ parameter . DbType = dbType ;
77+ if ( size is { } columnSize )
78+ {
79+ parameter . Size = value is string text && columnSize != - 1 && text . Length > columnSize ? text . Length : columnSize ;
80+ }
81+ }
4082
4183 command . Parameters . Add ( parameter ) ;
4284 }
0 commit comments