Skip to content

Commit b610ee0

Browse files
authored
Stored json gets corrupted on SqlServer after an update with a smaller sized payload (#1332)
* Stored json gets corrupted on SqlServer after an update with a smaller sized payload (#1331) * Fix issue where sql parameter optimalisation was applied for INSERT/UPDATE. Now only applied for `ExecuteReaderAsync` and `ExecuteReaderAsync`. * Fix failing CI tests due to new test using incorrect table for testing * Updated the test name to better express its intent * Version still supports NETFRAMEWORK and await using isn't supported in NETFX
1 parent bb6a57a commit b610ee0

File tree

4 files changed

+123
-12
lines changed

4 files changed

+123
-12
lines changed
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
namespace NServiceBus.PersistenceTesting.Sagas
2+
{
3+
using System;
4+
using System.Linq;
5+
using System.Threading.Tasks;
6+
using NUnit.Framework;
7+
8+
public class When_updating_saga_with_smaller_state : SagaPersisterTests
9+
{
10+
[Test]
11+
public async Task It_should_truncate_the_stored_state()
12+
{
13+
// When updating an existing saga where the serialized state is smaller in length than the previous the column value should not have any left over data from the previous value.
14+
// The deserializer ignores any trailing
15+
16+
var sqlVariant = (SqlTestVariant)param.Values[0];
17+
18+
if (sqlVariant.Dialect is not SqlDialect.MsSqlServer)
19+
{
20+
Assert.Ignore("Only relevant for SQL Server");
21+
return; // Satisfy compiler
22+
}
23+
24+
var sagaData = new SagaWithCorrelationPropertyData
25+
{
26+
CorrelatedProperty = Guid.NewGuid().ToString(),
27+
Payload = "very long state"
28+
};
29+
30+
await SaveSaga(sagaData);
31+
32+
SagaWithCorrelationPropertyData retrieved;
33+
var context = configuration.GetContextBagForSagaStorage();
34+
var persister = configuration.SagaStorage;
35+
36+
using (var completeSession = configuration.CreateStorageSession())
37+
{
38+
await completeSession.Open(context);
39+
40+
retrieved = await persister.Get<SagaWithCorrelationPropertyData>(nameof(sagaData.CorrelatedProperty), sagaData.CorrelatedProperty, completeSession, context);
41+
42+
retrieved.Payload = "short";
43+
44+
await persister.Update(retrieved, completeSession, context);
45+
await completeSession.CompleteAsync();
46+
}
47+
48+
var retrieved2 = await GetById<SagaWithCorrelationPropertyData>(sagaData.Id);
49+
50+
Assert.LessOrEqual(retrieved.Payload, sagaData.Payload); // No real need, but here to prevent accidental updates
51+
Assert.AreEqual(retrieved.Payload, retrieved2.Payload);
52+
#if NETFRAMEWORK
53+
using var con = sqlVariant.Open();
54+
#else
55+
await using var con = sqlVariant.Open();
56+
#endif
57+
await con.OpenAsync();
58+
var cmd = con.CreateCommand();
59+
cmd.CommandText = $"SELECT Data FROM [PersistenceTests_SWCP] WHERE Id = '{retrieved.Id}'";
60+
var data = (string)await cmd.ExecuteScalarAsync();
61+
62+
// Payload should only have a single closing bracket, if there are more that means there is trailing data
63+
var countClosingBrackets = data.ToCharArray().Count(x => x == '}');
64+
65+
Assert.AreEqual(1, countClosingBrackets);
66+
}
67+
68+
public class SagaWithCorrelationProperty : Saga<SagaWithCorrelationPropertyData>, IAmStartedByMessages<StartMessage>
69+
{
70+
public Task Handle(StartMessage message, IMessageHandlerContext context)
71+
{
72+
throw new NotImplementedException();
73+
}
74+
75+
protected override void ConfigureHowToFindSaga(SagaPropertyMapper<SagaWithCorrelationPropertyData> mapper)
76+
{
77+
mapper.ConfigureMapping<StartMessage>(msg => msg.SomeId).ToSaga(saga => saga.CorrelatedProperty);
78+
}
79+
}
80+
81+
public class SagaWithCorrelationPropertyData : ContainSagaData
82+
{
83+
public string CorrelatedProperty { get; set; }
84+
public string Payload { get; set; }
85+
}
86+
87+
public class StartMessage
88+
{
89+
public string SomeId { get; set; }
90+
}
91+
92+
public When_updating_saga_with_smaller_state(TestVariant param) : base(param)
93+
{
94+
}
95+
}
96+
}

src/SqlPersistence/CommandWrapper.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,14 @@ public Task<int> ExecuteNonQueryAsync(CancellationToken cancellationToken = defa
6464

6565
public Task<DbDataReader> ExecuteReaderAsync(CancellationToken cancellationToken = default)
6666
{
67+
dialect.OptimizeForReads(command);
6768
return command.ExecuteReaderAsync(cancellationToken);
6869
}
6970

7071
public Task<DbDataReader> ExecuteReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken = default)
7172
{
7273
var resultingBehavior = dialect.ModifyBehavior(command.Connection, behavior);
74+
dialect.OptimizeForReads(command);
7375
return command.ExecuteReaderAsync(resultingBehavior, cancellationToken);
7476
}
7577

src/SqlPersistence/Config/SqlDialect.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,9 @@ internal virtual void ValidateTablePrefix(string tablePrefix)
6262
}
6363

6464
internal abstract object GetCustomDialectDiagnosticsInfo();
65+
66+
internal virtual void OptimizeForReads(DbCommand command)
67+
{
68+
}
6569
}
6670
}

src/SqlPersistence/Config/SqlDialect_MsSqlServer.cs

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,7 @@ internal override void SetParameterValue(DbParameter parameter, object value)
3434
if (value is ArraySegment<char> charSegment)
3535
{
3636
parameter.Value = charSegment.Array;
37-
38-
// Set to 4000 or -1 to improve query execution plan reuse
39-
// Must be set when exceeding 4000 characters for nvarchar(max) https://stackoverflow.com/a/973269/199551
40-
parameter.Size = charSegment.Count > 4000 ? -1 : 4000;
41-
}
42-
else if (value is string stringValue)
43-
{
44-
parameter.Value = stringValue;
45-
46-
// Set to 4000 or -1 to improve query execution plan reuse
47-
// Must be set when exceeding 4000 characters for nvarchar(max) https://stackoverflow.com/a/973269/199551
48-
parameter.Size = stringValue.Length > 4000 ? -1 : 4000;
37+
parameter.Size = charSegment.Count;
4938
}
5039
else
5140
{
@@ -84,9 +73,29 @@ internal override object GetCustomDialectDiagnosticsInfo()
8473
};
8574
}
8675

76+
internal override void OptimizeForReads(DbCommand command)
77+
{
78+
foreach (DbParameter parameter in command.Parameters)
79+
{
80+
if (parameter.Value is ArraySegment<char> charSegment)
81+
{
82+
// Set to 4000 or -1 to improve query execution plan reuse
83+
// Must be set when exceeding 4000 characters for nvarchar(max) https://stackoverflow.com/a/973269/199551
84+
parameter.Size = charSegment.Count > 4000 ? -1 : 4000;
85+
}
86+
else if (parameter.Value is string stringValue)
87+
{
88+
// Set to 4000 or -1 to improve query execution plan reuse
89+
// Must be set when exceeding 4000 characters for nvarchar(max) https://stackoverflow.com/a/973269/199551
90+
parameter.Size = stringValue.Length > 4000 ? -1 : 4000;
91+
}
92+
}
93+
}
94+
8795
internal string Schema { get; set; }
8896
bool hasConnectionBeenInspectedForEncryption;
8997
bool isConnectionEncrypted;
9098
}
99+
91100
}
92101
}

0 commit comments

Comments
 (0)