CachedTopicRepository wraps SqlTopicRepository and is registered as a singleton (e.g., via the activator), so its in-memory tree persists across requests rather than being reloaded per request. If a Save() fails or only partially persists (see #155), the cache's tree may no longer reflect what actually made it to the database. A subsequent Save() against that same cached tree then walks a Children collection that's missing topics SQL already has rows for, so TopicFactory.Create() adds new topics with the same keys instead of detecting the existing ones. Since there's no unique constraint on (ParentID, TopicKey) in the Topics table, these duplicate siblings persist silently until the application is restarted.
Implementation Notes
On a Save() failure (or, once #155 is deployed, whenever the wrapped SqlTopicRepository.Save() throws), CachedTopicRepository should invalidate the cached subtree it attempted to save rather than leaving it in an uncertain state—either by evicting the affected topics so the next Load() re-reads from SQL, or by re-running Load() itself against the wrapped repository to resynchronize before returning the caller.
Affected Files
OnTopic.Data.Caching/CachedTopicRepository.cs
Tasks
CachedTopicRepositorywrapsSqlTopicRepositoryand is registered as a singleton (e.g., via the activator), so its in-memory tree persists across requests rather than being reloaded per request. If aSave()fails or only partially persists (see #155), the cache's tree may no longer reflect what actually made it to the database. A subsequentSave()against that same cached tree then walks aChildrencollection that's missing topics SQL already has rows for, soTopicFactory.Create()adds new topics with the same keys instead of detecting the existing ones. Since there's no unique constraint on(ParentID, TopicKey)in theTopicstable, these duplicate siblings persist silently until the application is restarted.Implementation Notes
On a
Save()failure (or, once #155 is deployed, whenever the wrappedSqlTopicRepository.Save()throws),CachedTopicRepositoryshould invalidate the cached subtree it attempted to save rather than leaving it in an uncertain state—either by evicting the affected topics so the nextLoad()re-reads from SQL, or by re-runningLoad()itself against the wrapped repository to resynchronize before returning the caller.Affected Files
OnTopic.Data.Caching/CachedTopicRepository.csTasks
Save()failures from the wrapped repository inCachedTopicRepositorySave()failsSave()followed by aLoad()reflects the wrapped repository's actual state, not stale cached dataSqlTopicRepository.Save(): Wrap recursive in a single transaction #155, since a non-transactional partial save is the scenario most likely to trigger this divergence between the cache and the database