Skip to content

TeamComponent.RemoveUserFromTeam throws on duplicate TeamMember rows (.Single predicate) #59

@poxet

Description

@poxet

Symptom

When the in-memory team-member list of a TeamComponent<T> instance contains two or more rows matching the predicate that RemoveUserFromTeam uses to find the row being removed, the click-handler throws System.InvalidOperationException: Sequence contains more than one matching element and the row stays in the UI. Every subsequent click on a different row in the same team also throws (the duplicates remain in _teamMembers), so once a team has duplicates the team-management page is effectively unusable for that team until either the page is reloaded or the duplicates are deleted from the database.

Where

Tharga.Team.Blazor.Features.Team.TeamComponent<T>.RemoveUserFromTeam (line numbers obscured by Razor compilation; the <RemoveUserFromTeam>d__35.MoveNext async state machine is the giveaway).

Stack

System.Linq.ThrowHelper.ThrowMoreThanOneMatchException
System.Linq.Enumerable.TryGetSingle
System.Linq.Enumerable.Single
Tharga.Team.Blazor.Features.Team.TeamComponent`1+<RemoveUserFromTeam>d__35.MoveNext

How we noticed

We monitor production exceptions for Quilt4Net.Server (consumer of Tharga.Team.Blazor 2.0.16). On v4.2.16.0 we saw 5 occurrences of this exception in a 24-hour window — all from the same call site, all from a single team that had a duplicate TeamMember row in our Mongo instance.

Root cause

RemoveUserFromTeam uses .Single(predicate) on the in-memory list. The list is loaded from the database, and the database can carry duplicate rows (in our case from a migration glitch — your repository layer doesn't prevent duplicate (TeamKey, UserKey) rows). One bad row in the database becomes a hard 500 in the UI.

Suggested fix

Same pattern we just landed downstream for our analogous UserService.GetUserAsync duplicate-Identity case:

  1. .FirstOrDefault(predicate) instead of .Single(predicate).
  2. If > 1 matches existed (compute once, log once), _logger.LogWarning(...) with the team key + user key so operators can find the bad rows for cleanup.
  3. Continue removing the picked row; the leftover duplicates can be cleaned up on the next page load (or, if you're feeling ambitious, the remove operation could opportunistically delete all matching rows so the duplicate is gone after one click).

Defence layer

If the upstream root cause is acceptable to leave (duplicates are operator-fixable in seconds), at minimum surface a friendly error in the UI instead of letting the exception propagate to the boundary — currently it bricks the whole team-management page until reload.

Notes

Author of Tharga.Team.Blazor and the consumer that hit this is the same person, so no upstream-vs-downstream confusion — just filing here for visibility / so it doesn't get lost in the prod-exception-cleanup writeup. Happy to PR the fix if you want; just didn't want to push to your repo unannounced.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions