@@ -1744,16 +1744,16 @@ endpoint.Cached is true &&
17441744 }
17451745 }
17461746
1747- // Set user context BEFORE upload so that upload row commands can access user claims
1748- if (
1749- ( endpoint . RequestHeadersMode == RequestHeadersMode . Context && headers is not null && Options . RequestHeadersContextKey is not null )
1750- ||
1751- ( endpoint . UserContext is true && Options . AuthenticationOptions . IpAddressContextKey is not null )
1752- ||
1753- ( endpoint . UserContext is true && context . User ? . Identity ? . IsAuthenticated is true &&
1754- ( Options . AuthenticationOptions . ClaimsJsonContextKey is not null || Options . AuthenticationOptions . ContextKeyClaimsMapping . Count > 0 )
1755- )
1756- )
1747+ // Set user context BEFORE upload so that upload row commands can access user claims.
1748+ // Also runs BeforeRoutineCommands and (when WrapInTransaction is true) opens the request transaction here.
1749+ bool willSetHeadersContext = endpoint . RequestHeadersMode == RequestHeadersMode . Context && headers is not null && Options . RequestHeadersContextKey is not null ;
1750+ bool willSetUserContext = endpoint . UserContext is true && (
1751+ Options . AuthenticationOptions . IpAddressContextKey is not null
1752+ || ( context . User ? . Identity ? . IsAuthenticated is true &&
1753+ ( Options . AuthenticationOptions . ClaimsJsonContextKey is not null || Options . AuthenticationOptions . ContextKeyClaimsMapping . Count > 0 ) )
1754+ ) ;
1755+ bool willRunBeforeRoutineCommands = Options . BeforeRoutineCommands . Length > 0 ;
1756+ if ( willSetHeadersContext || willSetUserContext || willRunBeforeRoutineCommands || Options . WrapInTransaction )
17571757 {
17581758 if ( connection . State != ConnectionState . Open )
17591759 {
@@ -1763,47 +1763,90 @@ endpoint.Cached is true &&
17631763 }
17641764 await connection . OpenRetryAsync ( Options . ConnectionRetryOptions , cancellationToken ) ;
17651765 }
1766- await using var batch = NpgsqlRestBatch . Create ( connection ) ;
17671766
1768- if ( endpoint . RequestHeadersMode == RequestHeadersMode . Context && headers is not null && Options . RequestHeadersContextKey is not null )
1767+ if ( Options . WrapInTransaction && transaction is null )
17691768 {
1770- var cmd = new NpgsqlBatchCommand ( Consts . SetContext ) ;
1771- cmd . Parameters . Add ( NpgsqlRestParameter . CreateTextParam ( Options . RequestHeadersContextKey ) ) ;
1772- cmd . Parameters . Add ( NpgsqlRestParameter . CreateTextParam ( headers ) ) ;
1773- batch . BatchCommands . Add ( cmd ) ;
1769+ transaction = await connection . BeginTransactionAsync ( cancellationToken ) ;
17741770 }
17751771
1776- if ( endpoint . UserContext is true )
1772+ string setContextSql = transaction is not null ? Consts . SetContextLocal : Consts . SetContext ;
1773+
1774+ if ( willSetHeadersContext || willSetUserContext || willRunBeforeRoutineCommands )
17771775 {
1778- claimsDict ??= context . User . BuildClaimsDictionary ( Options . AuthenticationOptions ) ;
1776+ await using var batch = NpgsqlRestBatch . Create ( connection ) ;
17791777
1780- if ( Options . AuthenticationOptions . IpAddressContextKey is not null )
1778+ if ( willSetHeadersContext )
17811779 {
1782- var cmd = new NpgsqlBatchCommand ( Consts . SetContext ) ;
1783- cmd . Parameters . Add ( NpgsqlRestParameter . CreateTextParam ( Options . AuthenticationOptions . IpAddressContextKey ) ) ;
1784- cmd . Parameters . Add ( NpgsqlRestParameter . CreateTextParam ( context . Request . GetClientIpAddressDbParam ( ) ) ) ;
1780+ var cmd = new NpgsqlBatchCommand ( setContextSql ) ;
1781+ cmd . Parameters . Add ( NpgsqlRestParameter . CreateTextParam ( Options . RequestHeadersContextKey ) ) ;
1782+ cmd . Parameters . Add ( NpgsqlRestParameter . CreateTextParam ( headers ) ) ;
17851783 batch . BatchCommands . Add ( cmd ) ;
17861784 }
1787- if ( context . User ? . Identity ? . IsAuthenticated is true && Options . AuthenticationOptions . ClaimsJsonContextKey is not null )
1785+
1786+ if ( endpoint . UserContext is true )
17881787 {
1789- var cmd = new NpgsqlBatchCommand ( Consts . SetContext ) ;
1790- cmd . Parameters . Add ( NpgsqlRestParameter . CreateTextParam ( Options . AuthenticationOptions . ClaimsJsonContextKey ) ) ;
1791- cmd . Parameters . Add ( NpgsqlRestParameter . CreateTextParam ( claimsDict ? . GetUserClaimsDbParam ( ) ?? DBNull . Value ) ) ;
1792- batch . BatchCommands . Add ( cmd ) ;
1788+ claimsDict ??= context . User . BuildClaimsDictionary ( Options . AuthenticationOptions ) ;
1789+
1790+ if ( Options . AuthenticationOptions . IpAddressContextKey is not null )
1791+ {
1792+ var cmd = new NpgsqlBatchCommand ( setContextSql ) ;
1793+ cmd . Parameters . Add ( NpgsqlRestParameter . CreateTextParam ( Options . AuthenticationOptions . IpAddressContextKey ) ) ;
1794+ cmd . Parameters . Add ( NpgsqlRestParameter . CreateTextParam ( context . Request . GetClientIpAddressDbParam ( ) ) ) ;
1795+ batch . BatchCommands . Add ( cmd ) ;
1796+ }
1797+ if ( context . User ? . Identity ? . IsAuthenticated is true && Options . AuthenticationOptions . ClaimsJsonContextKey is not null )
1798+ {
1799+ var cmd = new NpgsqlBatchCommand ( setContextSql ) ;
1800+ cmd . Parameters . Add ( NpgsqlRestParameter . CreateTextParam ( Options . AuthenticationOptions . ClaimsJsonContextKey ) ) ;
1801+ cmd . Parameters . Add ( NpgsqlRestParameter . CreateTextParam ( claimsDict ? . GetUserClaimsDbParam ( ) ?? DBNull . Value ) ) ;
1802+ batch . BatchCommands . Add ( cmd ) ;
1803+ }
1804+ if ( context . User ? . Identity ? . IsAuthenticated is true )
1805+ {
1806+ foreach ( var mapping in Options . AuthenticationOptions . ContextKeyClaimsMapping )
1807+ {
1808+ var cmd = new NpgsqlBatchCommand ( setContextSql ) ;
1809+ cmd . Parameters . Add ( NpgsqlRestParameter . CreateTextParam ( mapping . Key ) ) ;
1810+ cmd . Parameters . Add ( NpgsqlRestParameter . CreateTextParam ( claimsDict ! . GetClaimDbContextParam ( mapping . Value ) ) ) ;
1811+ batch . BatchCommands . Add ( cmd ) ;
1812+ }
1813+ }
17931814 }
1794- if ( context . User ? . Identity ? . IsAuthenticated is true )
1815+
1816+ if ( willRunBeforeRoutineCommands )
17951817 {
1796- foreach ( var mapping in Options . AuthenticationOptions . ContextKeyClaimsMapping )
1818+ foreach ( var beforeCmd in Options . BeforeRoutineCommands )
17971819 {
1798- var cmd = new NpgsqlBatchCommand ( Consts . SetContext ) ;
1799- cmd . Parameters . Add ( NpgsqlRestParameter . CreateTextParam ( mapping . Key ) ) ;
1800- cmd . Parameters . Add ( NpgsqlRestParameter . CreateTextParam ( claimsDict ! . GetClaimDbContextParam ( mapping . Value ) ) ) ;
1820+ var cmd = new NpgsqlBatchCommand ( beforeCmd . Sql ) ;
1821+ foreach ( var p in beforeCmd . Parameters )
1822+ {
1823+ object value ;
1824+ switch ( p . Source )
1825+ {
1826+ case BeforeRoutineCommandParameterSource . Claim :
1827+ claimsDict ??= context . User . BuildClaimsDictionary ( Options . AuthenticationOptions ) ;
1828+ value = p . Name is null ? DBNull . Value : claimsDict ! . GetClaimDbContextParam ( p . Name ) ;
1829+ break ;
1830+ case BeforeRoutineCommandParameterSource . RequestHeader :
1831+ value = p . Name is not null && context . Request . Headers . TryGetValue ( p . Name , out var headerValue )
1832+ ? ( object ) headerValue . ToString ( )
1833+ : DBNull . Value ;
1834+ break ;
1835+ case BeforeRoutineCommandParameterSource . IpAddress :
1836+ value = context . Request . GetClientIpAddressDbParam ( ) ;
1837+ break ;
1838+ default :
1839+ value = DBNull . Value ;
1840+ break ;
1841+ }
1842+ cmd . Parameters . Add ( NpgsqlRestParameter . CreateTextParam ( value ) ) ;
1843+ }
18011844 batch . BatchCommands . Add ( cmd ) ;
18021845 }
18031846 }
1804- }
18051847
1806- await batch . ExecuteBatchWithRetryAsync ( endpoint . RetryStrategy , cancellationToken ) ;
1848+ await batch . ExecuteBatchWithRetryAsync ( endpoint . RetryStrategy , cancellationToken ) ;
1849+ }
18071850 }
18081851
18091852 object ? uploadMetadata = null ;
@@ -1817,7 +1860,7 @@ endpoint.Cached is true &&
18171860 }
18181861 await connection . OpenRetryAsync ( Options . ConnectionRetryOptions , cancellationToken ) ;
18191862 }
1820- if ( uploadHandler . RequiresTransaction is true )
1863+ if ( uploadHandler . RequiresTransaction is true && transaction is null )
18211864 {
18221865 transaction = await connection . BeginTransactionAsync ( cancellationToken ) ;
18231866 }
@@ -1845,7 +1888,7 @@ uploadHandler is not null &&
18451888 await connection . OpenRetryAsync ( Options . ConnectionRetryOptions , cancellationToken ) ;
18461889 }
18471890 await using var batch = NpgsqlRestBatch . Create ( connection ) ;
1848- var cmd = new NpgsqlBatchCommand ( Consts . SetContext ) ;
1891+ var cmd = new NpgsqlBatchCommand ( transaction is not null ? Consts . SetContextLocal : Consts . SetContext ) ;
18491892 cmd . Parameters . Add ( NpgsqlRestParameter . CreateTextParam ( Options . UploadOptions . DefaultUploadMetadataContextKey ) ) ;
18501893 cmd . Parameters . Add ( NpgsqlRestParameter . CreateTextParam ( uploadMetadata ) ) ;
18511894 batch . BatchCommands . Add ( cmd ) ;
@@ -3088,7 +3131,18 @@ await Results.Problem(
30883131 uploadHandler ? . OnError ( connection , context , exception ) ;
30893132 }
30903133
3091- if ( context . Response . StatusCode != 200 && context . Response . StatusCode != 205 && context . Response . StatusCode != 400 )
3134+ if ( context . Response . StatusCode == 400 )
3135+ {
3136+ if ( shouldLog && cmdLog is not null )
3137+ {
3138+ Logger ? . LogWarning ( "Client error (400) executing command: {commandText} mapped to endpoint: {Url}: {message}{NewLine}{cmdLog}" , commandText , endpoint . Path , exception . Message , Environment . NewLine , cmdLog . ToString ( ) ) ;
3139+ }
3140+ else
3141+ {
3142+ Logger ? . LogWarning ( "Client error (400) executing command: {commandText} mapped to endpoint: {Url}: {message}" , commandText , endpoint . Path , exception . Message ) ;
3143+ }
3144+ }
3145+ else if ( context . Response . StatusCode != 200 && context . Response . StatusCode != 205 )
30923146 {
30933147 if ( shouldLog && cmdLog is not null )
30943148 {
0 commit comments