Skip to content

Commit 975d721

Browse files
authored
Merge pull request #522 from Inxton/fix-duplicate-identities-false-positives
fix duplicate identities false positives
2 parents 8bbd6e4 + 68b5371 commit 975d721

4 files changed

Lines changed: 109 additions & 22 deletions

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Twin Identity System
2+
3+
## Overview
4+
5+
The identity system in AXSharp allows twin objects to be looked up by a unique numeric identity (`ulong`). This is used by components that need to resolve references between twin objects at runtime (e.g. the `[MemberByIdentity]` attribute).
6+
7+
The core class is [`TwinIdentityProvider`](~/api/AXSharp.Connector.Identity.TwinIdentityProvider.yml), which is responsible for assigning, writing, and sorting identity values.
8+
9+
## How identities work
10+
11+
1. During twin construction, each object that implements `ITwinIdentity` is registered via `AddIdentity`.
12+
2. When the application starts, `ConstructIdentitiesAsync` is called to assign identity values, write them to the PLC, and build a sorted lookup dictionary.
13+
3. Other parts of the system can then resolve objects by their identity using `GetTwinByIdentity`.
14+
15+
## Constructing identities
16+
17+
```csharp
18+
await connector.IdentityProvider.ConstructIdentitiesAsync();
19+
```
20+
21+
`ConstructIdentitiesAsync` performs the following steps:
22+
23+
1. **Assign** -- Each identity tag is assigned a `ulong` value using the provided `identityProvider` function. If no function is supplied, a default provider based on `string.GetHashCode()` of the symbol is used.
24+
2. **Write** -- The assigned values are written to the PLC via `WriteBatchAsync`. Values are always written fresh, regardless of what was previously stored on the PLC.
25+
3. **Sort** -- The identities are sorted into a `SortedDictionary<ulong, ITwinIdentity>` using the locally assigned values (`Cyclic`), not values read back from the PLC.
26+
27+
This ensures that stale or inconsistent identity values left on the PLC from prior sessions cannot cause errors.
28+
29+
## The `failOnDuplicate` parameter
30+
31+
```csharp
32+
await connector.IdentityProvider.ConstructIdentitiesAsync(failOnDuplicate: false);
33+
```
34+
35+
By default, `ConstructIdentitiesAsync` throws a `DuplicateIdentityException` when two symbols resolve to the same identity value. You can set `failOnDuplicate` to `false` to log a warning and skip the duplicate entry instead.
36+
37+
## Custom identity providers
38+
39+
The default identity provider uses `string.GetHashCode()`, which has two important limitations:
40+
41+
- **Not deterministic across processes** -- In .NET Core/.NET 5+, `string.GetHashCode()` is randomized per process. Identity values will differ between application restarts.
42+
- **Collision risk** -- `GetHashCode()` returns a 32-bit value. For large PLC programs with many symbols, hash collisions become increasingly likely (birthday paradox).
43+
44+
For production use, supply a custom identity provider that guarantees uniqueness:
45+
46+
```csharp
47+
await connector.IdentityProvider.ConstructIdentitiesAsync(
48+
identityProvider: tag => tag.Cyclic = MyStableHashFunction(tag.Symbol)
49+
);
50+
```
51+
52+
A good custom provider should:
53+
- Produce deterministic values for the same symbol across process restarts.
54+
- Guarantee uniqueness (or near-uniqueness) across all symbols in the program.
55+
- Use the full 64-bit `ulong` range to minimize collision probability.
56+
57+
## Troubleshooting
58+
59+
### `DuplicateIdentityException`
60+
61+
This exception is thrown when two different symbols are assigned the same identity value. Common causes:
62+
63+
- **Hash collision** with the default `GetHashCode()`-based provider. Solution: use a custom identity provider with better distribution, or set `failOnDuplicate: false` if identity-based lookups are not critical for your application.
64+
- **Stale PLC values** (resolved in current version) -- Previously, the identity system would read back values from the PLC after writing. If the PLC returned stale data from a prior session, this could cause spurious duplicate errors. The system now uses the locally assigned values directly, avoiding this issue.

docfx/articles/toc.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
href: ~/articles/connectors/Dummy.md
3232
- name: WebAPI
3333
href: ~/articles/connectors/WebAPI.md
34+
- name: Identity
35+
href: ~/articles/connectors/IDENTITY.md
3436
- name: Blazor rendering
3537
href: ~/articles/blazor/README.md
3638
items:

src/AXSharp.connectors/src/AXSharp.Connector/Identity/TwinIdentityProvider.cs

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ internal async Task<IEnumerable<OnlinerULInt>> ReadIdentitiesAsync()
206206
/// </summary>
207207
/// <param name="identities">Identities</param>
208208
/// <param name="identityProvider">Identity creator.</param>
209-
/// <returns></returns>
209+
/// <returns>Assigned identities.</returns>
210210
public IEnumerable<OnlinerULInt> AssignIdentities(IEnumerable<OnlinerULInt> identities, Func<OnlinerULInt, ulong> identityProvider = null)
211211
{
212212
// If no identity provider is given, use default one based on hash code of the symbol.
@@ -234,45 +234,63 @@ public async Task WriteIdentities(IEnumerable<OnlinerULInt> identitiesToWrite)
234234
}
235235

236236
/// <summary>
237-
/// Refreshes and sorts identities.
237+
/// Constructs identities by assigning them locally, writing to PLC, and sorting by the assigned values.
238+
/// Identity values are always written fresh to the PLC regardless of any previously stored values.
239+
/// This ensures that stale or inconsistent identity values from prior sessions do not cause duplicate identity errors.
238240
/// </summary>
239-
public async Task ConstructIdentitiesAsync(Func<OnlinerULInt, ulong> identityProvider = null)
241+
/// <param name="identityProvider">
242+
/// Function that assigns and returns an identity value for each <see cref="OnlinerULInt"/> tag.
243+
/// When <c>null</c>, a default provider based on <see cref="string.GetHashCode()"/> of the symbol is used.
244+
/// Note: <see cref="string.GetHashCode()"/> is not deterministic across process restarts in .NET Core/.NET 5+,
245+
/// so identity values will differ between sessions. For stable identities, supply a custom provider.
246+
/// </param>
247+
/// <param name="failOnDuplicate">
248+
/// When <c>true</c>, throws <see cref="DuplicateIdentityException"/> if two symbols resolve to the same identity value.
249+
/// When <c>false</c>, logs a warning and skips the duplicate entry.
250+
/// </param>
251+
public async Task ConstructIdentitiesAsync(Func<OnlinerULInt, ulong> identityProvider = null, bool failOnDuplicate = true)
240252
{
241-
await WriteIdentities(AssignIdentities(_identitiesTags, identityProvider));
242-
await SortIdentitiesAsync();
253+
await WriteIdentities(AssignIdentities(_identitiesTags, identityProvider));
254+
await SortIdentitiesAsync(failOnDuplicate);
243255
}
244256

245257
/// <summary>
246-
/// Sorts identities.
258+
/// Sorts identities using the locally assigned Cyclic values, without reading back from PLC.
247259
/// </summary>
248-
internal async Task SortIdentitiesAsync()
260+
internal async Task SortIdentitiesAsync(bool failOnDuplicate = true)
249261
{
250-
await Task.Run(async () =>
262+
_connector?.Logger.Information("Sorting identities from assigned values...");
263+
_sortedIdentities.Clear();
264+
foreach (var identity in _identities)
251265
{
252-
_connector?.Logger.Information("Sorting identities...");
253-
if (_connector != null)
266+
var key = identity.Key.Cyclic;
267+
if (!_sortedIdentities.ContainsKey(key) && key != 0)
254268
{
255-
await _connector?.ReadBatchAsync(_identities.Select(p => p.Key), eAccessPriority.High);
269+
_sortedIdentities.Add(key, identity.Value);
256270
}
257-
_sortedIdentities.Clear();
258-
foreach (var identity in _identities)
271+
else
259272
{
260-
var key = identity.Key.LastValue;
261-
if (!_sortedIdentities.ContainsKey(key))
262-
{
263-
_sortedIdentities.Add(key, identity.Value);
264-
}
265-
else
273+
if (failOnDuplicate)
266274
{
267275
throw new DuplicateIdentityException("There is a duplicate identity: " +
268-
$"{identity.Value.Symbol} : {key}." +
276+
$"'{identity.Value.Symbol} : {key}.'" +
277+
$" and '{_sortedIdentities[key].Symbol} : {key}.'" +
278+
$" share the same identity value." +
269279
$"The algorithm for assigning identities needs to be adjusted." +
270280
$"Use an algorithm that guarantees unique identities and is less prone to collisions.");
271281
}
282+
else
283+
{
284+
_connector?.Logger.Warning($"Duplicate identity detected: '{identity.Value.Symbol} : {key}' and '{_sortedIdentities[key].Symbol} : {key}' share the same identity value. " +
285+
$"This entry will be ignored."+
286+
$"The algorithm for assigning identities needs to be adjusted." +
287+
$"Use an algorithm that guarantees unique identities and is less prone to collisions." +
288+
$"Ignoring this warning may lead to unexpected behavior in those parts of the system that rely on unique identities.");
289+
}
272290
}
291+
}
273292

274-
_connector?.Logger.Information("Sorting identities done.");
275-
});
293+
_connector?.Logger.Information("Sorting identities done.");
276294
}
277295
}
278296

src/AXSharp.connectors/tests/AXSharp.ConnectorTests/AXSharp.ConnectorTests/Identity/TwinIdentityProviderTests.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,16 +220,19 @@ public async void CanCallSortIdentities()
220220

221221
var identityVar_2 = Substitute.For<OnlinerULInt, IOnline<ulong>>();
222222
identityVar_2.LastValue.Returns(2ul);
223+
identityVar_2.Cyclic.Returns(2ul);
223224
var obj2 = Substitute.For<ITwinIdentity>();
224225
obj2.Identity.Returns(identityVar_2);
225226

226227
var identityVar_1 = Substitute.For<OnlinerULInt, IOnline<ulong>>();
227228
var obj1 = Substitute.For<ITwinIdentity>();
228229
obj1.Identity.Returns(identityVar_1);
229230
identityVar_1.LastValue.Returns(1ul);
231+
identityVar_1.Cyclic.Returns(1ul);
230232

231233
var identityVar_3 = Substitute.For<OnlinerULInt, IOnline<ulong>>();
232234
identityVar_3.LastValue.Returns(3ul);
235+
identityVar_3.Cyclic.Returns(3ul);
233236
var obj3 = Substitute.For<ITwinIdentity>();
234237
obj3.Identity.Returns(identityVar_3);
235238

0 commit comments

Comments
 (0)