Class TeamOwnership
Decides who may be made the owner of a team, and which sitting owners that displaces.
public static class TeamOwnership
- Inheritance
-
TeamOwnership
- Inherited Members
Remarks
Pure and static so the rules are testable without a store. They are the whole safety argument for SetOwner, so they should not live inside a service method where only an integration test can reach them.
A team has exactly one owner. That is the invariant every rule here serves.
SetMemberRoleAsync refuses to grant or revoke Owner in either direction, so no ordinary
path can add a second owner or remove the last one. Setting an owner is the only operation that moves
the role, and it always leaves exactly one — the promotion is applied first and the displaced owners
demoted after, so the team is never momentarily ownerless.
Two states violate the invariant, and a team arrives at both from outside this class rather than
through it: RemoveUserFromAllTeamsAsync removes the owner along with everyone else, leaving
none; and a team synced from a legacy system whose model permits several owners arrives carrying
more than one. Setting an owner is the repair for both, which is why it does not require the team
to be in any particular state first.
Methods
CanSetOwner(IEnumerable<ITeamMember>, string)
Whether candidateUserKey may be made the sole owner of this team.
public static bool CanSetOwner(IEnumerable<ITeamMember> members, string candidateUserKey)
Parameters
membersIEnumerable<ITeamMember>candidateUserKeystring
Returns
Remarks
One condition: the candidate must be an existing member. That is what keeps this a repair rather than a way to inject an outsider into a team the caller does not belong to — and the caller holds a system scope precisely because they are not a member.
Deliberately not conditioned on the current owner count. Refusing on a team that already has an owner would rule out the two cases this exists to serve: reducing a legacy team's several owners to one, and moving ownership when the sitting owner cannot do it themselves.
IsOwnerless(IEnumerable<ITeamMember>)
Whether the team currently has no member at Owner.
public static bool IsOwnerless(IEnumerable<ITeamMember> members)
Parameters
membersIEnumerable<ITeamMember>
Returns
Remarks
A null or empty roster counts as ownerless. Setting an owner still refuses on one, because there is nobody to promote — see CanSetOwner(IEnumerable<ITeamMember>, string).
IsSoleOwner(IEnumerable<ITeamMember>, string)
Whether userKey is already the only owner, so setting them owner would change
nothing.
public static bool IsSoleOwner(IEnumerable<ITeamMember> members, string userKey)
Parameters
membersIEnumerable<ITeamMember>userKeystring
Returns
Remarks
The caller is expected to be a repeated process — a sync reconciling teams from another system — so "already correct" is the common case rather than a mistake. It returns without writing and without an audit entry, instead of throwing and forcing every such caller to catch.
OwnersToDemote(IEnumerable<ITeamMember>, string)
The sitting owners displaced by making newOwnerUserKey owner — every member at
Owner except the incoming one.
public static IReadOnlyList<ITeamMember> OwnersToDemote(IEnumerable<ITeamMember> members, string newOwnerUserKey)
Parameters
membersIEnumerable<ITeamMember>newOwnerUserKeystring
Returns
Remarks
Returns all of them, not just one. A team synced from a system that permits several owners is reduced to exactly one in a single operation; demoting one at a time would leave the invariant broken between calls and give the audit log a partial story.