Skip to content

Commit 88cbb3d

Browse files
committed
Fix PositionSnapshotHolder returning live reference instead of copy
TryGetSnapshot and Process returned the internal dictionary reference, allowing external code to mutate internal snapshot state. Now returns TypedClone() like OrderSnapshotHolder does.
1 parent 97c5a33 commit 88cbb3d

File tree

2 files changed

+75
-33
lines changed

2 files changed

+75
-33
lines changed

Messages/PositionSnapshotHolder.cs

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ namespace StockSharp.Messages;
44
/// <see cref="PositionChangeMessage"/> snapshots holder.
55
/// </summary>
66
/// <remarks>
7-
/// Returns the same reference for the same position key (PortfolioName + SecurityId + StrategyId + Side + ClientCode + DepoName + LimitType).
7+
/// Returns a copy of the snapshot for the same position key (PortfolioName + SecurityId + StrategyId + Side + ClientCode + DepoName + LimitType).
88
/// </remarks>
99
public class PositionSnapshotHolder : BaseLogReceiver
1010
{
@@ -32,7 +32,15 @@ public PositionSnapshotHolder()
3232
public bool TryGetSnapshot(string portfolioName, SecurityId securityId, string strategyId, Sides? side, string clientCode, string depoName, TPlusLimits? limitType, out PositionChangeMessage snapshot)
3333
{
3434
var key = CreateKey(portfolioName, securityId, strategyId, side, clientCode, depoName, limitType);
35-
return _snapshots.TryGetValue(key, out snapshot);
35+
36+
if (_snapshots.TryGetValue(key, out var stored))
37+
{
38+
snapshot = stored.TypedClone();
39+
return true;
40+
}
41+
42+
snapshot = null;
43+
return false;
3644
}
3745

3846
/// <summary>
@@ -46,15 +54,22 @@ public bool TryGetSnapshot(PositionChangeMessage posMsg, out PositionChangeMessa
4654
if (posMsg is null)
4755
throw new ArgumentNullException(nameof(posMsg));
4856

49-
return _snapshots.TryGetValue(CreateKey(posMsg), out snapshot);
57+
if (_snapshots.TryGetValue(CreateKey(posMsg), out var stored))
58+
{
59+
snapshot = stored.TypedClone();
60+
return true;
61+
}
62+
63+
snapshot = null;
64+
return false;
5065
}
5166

5267
/// <summary>
5368
/// Process <see cref="PositionChangeMessage"/> change.
5469
/// </summary>
5570
/// <param name="posMsg"><see cref="PositionChangeMessage"/> change.</param>
5671
/// <returns>
57-
/// Position snapshot. Returns the same reference for the same position key.
72+
/// Position snapshot copy.
5873
/// </returns>
5974
public PositionChangeMessage Process(PositionChangeMessage posMsg)
6075
{
@@ -68,13 +83,13 @@ public PositionChangeMessage Process(PositionChangeMessage posMsg)
6883
if (_snapshots.TryGetValue(key, out var snapshot))
6984
{
7085
ApplyChanges(snapshot, posMsg);
71-
return snapshot;
86+
return snapshot.TypedClone();
7287
}
7388
else
7489
{
75-
snapshot = (PositionChangeMessage)posMsg.Clone();
90+
snapshot = posMsg.TypedClone();
7691
_snapshots.Add(key, snapshot);
77-
return snapshot;
92+
return snapshot.TypedClone();
7893
}
7994
}
8095
}

Tests/SnapshotHolderTests.cs

Lines changed: 53 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2196,7 +2196,7 @@ public void Position_TryGetSnapshot_NoSnapshot_ReturnsFalse()
21962196
}
21972197

21982198
[TestMethod]
2199-
public void Position_Process_ReturnsSameReference()
2199+
public void Position_Process_ReturnsCopy()
22002200
{
22012201
var holder = new PositionSnapshotHolder();
22022202
var msg1 = new PositionChangeMessage
@@ -2219,8 +2219,13 @@ public void Position_Process_ReturnsSameReference()
22192219

22202220
var result2 = holder.Process(msg2);
22212221

2222-
// Must return same reference
2223-
result1.AssertSame(result2);
2222+
// Must return different references (copies)
2223+
result1.AssertNotSame(result2);
2224+
2225+
// result1 has state from first Process call
2226+
result1.Changes[PositionChangeTypes.CurrentValue].AssertEqual(100m);
2227+
// result2 has updated state
2228+
result2.Changes[PositionChangeTypes.CurrentValue].AssertEqual(150m);
22242229
}
22252230

22262231
[TestMethod]
@@ -2237,9 +2242,7 @@ public void Position_Process_UpdatesExistingSnapshot()
22372242
.TryAdd(PositionChangeTypes.CurrentValue, 100m)
22382243
.TryAdd(PositionChangeTypes.AveragePrice, 50m);
22392244

2240-
var snapshot = holder.Process(msg1);
2241-
snapshot.Changes[PositionChangeTypes.CurrentValue].AssertEqual(100m);
2242-
snapshot.Changes[PositionChangeTypes.AveragePrice].AssertEqual(50m);
2245+
holder.Process(msg1);
22432246

22442247
var msg2 = new PositionChangeMessage
22452248
{
@@ -2250,9 +2253,9 @@ public void Position_Process_UpdatesExistingSnapshot()
22502253
.TryAdd(PositionChangeTypes.CurrentValue, 150m)
22512254
.TryAdd(PositionChangeTypes.UnrealizedPnL, 25m);
22522255

2253-
holder.Process(msg2);
2256+
var snapshot = holder.Process(msg2);
22542257

2255-
// Same snapshot should be updated
2258+
// Returned copy should have merged state
22562259
snapshot.Changes[PositionChangeTypes.CurrentValue].AssertEqual(150m);
22572260
snapshot.Changes[PositionChangeTypes.AveragePrice].AssertEqual(50m); // unchanged
22582261
snapshot.Changes[PositionChangeTypes.UnrealizedPnL].AssertEqual(25m); // new field
@@ -2472,7 +2475,7 @@ public void Position_KeyIsCaseInsensitive()
24722475
}
24732476
.TryAdd(PositionChangeTypes.CurrentValue, 100m);
24742477

2475-
var snap1 = holder.Process(msg1);
2478+
holder.Process(msg1);
24762479

24772480
var msg2 = new PositionChangeMessage
24782481
{
@@ -2486,9 +2489,12 @@ public void Position_KeyIsCaseInsensitive()
24862489

24872490
var snap2 = holder.Process(msg2);
24882491

2489-
// Should be same reference due to case-insensitive key
2490-
snap1.AssertSame(snap2);
2491-
snap1.Changes[PositionChangeTypes.CurrentValue].AssertEqual(200m);
2492+
// Case-insensitive key — same internal snapshot updated, returned as copy
2493+
snap2.Changes[PositionChangeTypes.CurrentValue].AssertEqual(200m);
2494+
2495+
// Verify via TryGetSnapshot — should also return 200
2496+
holder.TryGetSnapshot(msg1, out var retrieved);
2497+
retrieved.Changes[PositionChangeTypes.CurrentValue].AssertEqual(200m);
24922498
}
24932499

24942500
#endregion
@@ -2774,10 +2780,10 @@ public void Order_TryGetSnapshot_ReturnsCopy()
27742780
}
27752781

27762782
/// <summary>
2777-
/// BUG #3: Position holder also returns live reference.
2783+
/// Position TryGetSnapshot must return a copy, not the internal reference.
27782784
/// </summary>
27792785
[TestMethod]
2780-
public void Position_LiveReference_CanBeMutatedExternally_Bug()
2786+
public void Position_TryGetSnapshot_ReturnsCopy()
27812787
{
27822788
var holder = new PositionSnapshotHolder();
27832789

@@ -2789,23 +2795,44 @@ public void Position_LiveReference_CanBeMutatedExternally_Bug()
27892795
}
27902796
.TryAdd(PositionChangeTypes.CurrentValue, 100m);
27912797

2792-
var snapshot = holder.Process(msg1);
2798+
holder.Process(msg1);
27932799

2794-
// Get snapshot via TryGetSnapshot
2795-
holder.TryGetSnapshot(msg1, out var retrieved);
2800+
// Get snapshot twice
2801+
holder.TryGetSnapshot(msg1, out var snap1);
2802+
holder.TryGetSnapshot(msg1, out var snap2);
2803+
2804+
// Should be different references (copies)
2805+
snap1.AssertNotSame(snap2);
27962806

2797-
// BUG: Same reference returned
2798-
snapshot.AssertSame(retrieved);
2807+
// But with same data
2808+
snap1.Changes[PositionChangeTypes.CurrentValue].AssertEqual(snap2.Changes[PositionChangeTypes.CurrentValue]);
2809+
}
27992810

2800-
// External code can mutate the internal state!
2801-
retrieved.Changes[PositionChangeTypes.CurrentValue] = 999m;
2811+
/// <summary>
2812+
/// Position Process must return a copy, not the internal reference.
2813+
/// External mutation must not affect internal state.
2814+
/// </summary>
2815+
[TestMethod]
2816+
public void Position_Process_ReturnsCopy_IsolatedFromInternal()
2817+
{
2818+
var holder = new PositionSnapshotHolder();
28022819

2803-
// This affects the internal snapshot
2804-
holder.TryGetSnapshot(msg1, out var afterMutation);
2805-
afterMutation.Changes[PositionChangeTypes.CurrentValue].AssertEqual(999m);
2820+
var msg1 = new PositionChangeMessage
2821+
{
2822+
PortfolioName = "Portfolio1",
2823+
SecurityId = _secId1,
2824+
ServerTime = _now,
2825+
}
2826+
.TryAdd(PositionChangeTypes.CurrentValue, 100m);
28062827

2807-
// Original 'snapshot' variable is also affected (same reference)
2808-
snapshot.Changes[PositionChangeTypes.CurrentValue].AssertEqual(999m);
2828+
var snapshot = holder.Process(msg1);
2829+
2830+
// Mutate the returned copy
2831+
snapshot.Changes[PositionChangeTypes.CurrentValue] = 999m;
2832+
2833+
// Internal state must NOT be affected
2834+
holder.TryGetSnapshot(msg1, out var afterMutation);
2835+
afterMutation.Changes[PositionChangeTypes.CurrentValue].AssertEqual(100m);
28092836
}
28102837

28112838
/// <summary>

0 commit comments

Comments
 (0)