Class CacheInvalidatingUserServiceDecorator
Decorator over IUserService that drops the cached copy of a user after any call that changes them — whoever implemented the write.
public sealed class CacheInvalidatingUserServiceDecorator : IUserService
- Inheritance
-
CacheInvalidatingUserServiceDecorator
- Implements
- Inherited Members
Remarks
UserServiceBase caches resolved users and invalidates in the paths it owns. A host overriding
one of those to supply persistence replaces the path that invalidates, so the write commits
and every later read is served stale. That is the toolkit dropping a responsibility it owns, not a
host concern, so the fix belongs here rather than in a note telling every host to remember.
Why a decorator and not a template method. The obvious fix is to make the mutating members
non-virtual and have them call a protected abstract hook, which would make it impossible to
get wrong. That is a breaking change — it stops every existing override compiling — so it is
4.0 work. A decorator invalidates regardless of who implemented the member, needs no consumer
change, and can ship in a minor.
SetUserLastSeenAsync(string, DateTime) is deliberately not invalidated. It runs on every authenticated resolve (throttled), so invalidating there would empty the cache continuously and defeat the thing this class exists to keep correct. A cached LastSeen is therefore up to one resolve stale, which is the behaviour that already shipped.
Invalidation runs only after the inner call returns. A throwing write has changed nothing, and dropping a valid entry because a write failed would trade a stale read for a needless one.
Constructors
CacheInvalidatingUserServiceDecorator(IUserService)
public CacheInvalidatingUserServiceDecorator(IUserService inner)
Parameters
innerIUserServiceThe store to delegate to.
Remarks
A store that does not implement IUserCacheInvalidator has no cache to drop, so this decorator becomes a pass-through rather than an error.
Methods
ClearOwnIconAsync()
public Task ClearOwnIconAsync()
Returns
Remarks
The self-service icon members take no key, so the caller is resolved first to learn which entry to drop. That read is a cache hit in the case that matters — the caller is signed in, which is what put them in the cache — so it costs a dictionary lookup, not a round trip.
ClearUserIconAsync(string)
Clears a specific user's icon (administrative). Requires Manage.
public Task ClearUserIconAsync(string userKey)
Parameters
userKeystring
Returns
DeleteUserAsync(string)
public Task DeleteUserAsync(string userKey)
Parameters
userKeystring
Returns
GetAsync()
All users. Cross-user enumeration — requires Manage.
public IAsyncEnumerable<IUser> GetAsync()
Returns
GetCurrentUserAsync(ClaimsPrincipal)
public Task<IUser> GetCurrentUserAsync(ClaimsPrincipal claimsPrincipal = null)
Parameters
claimsPrincipalClaimsPrincipal
Returns
GetCurrentUserAsync(ClaimsPrincipal, bool)
The user for claimsPrincipal, resolved as GetCurrentUserAsync(ClaimsPrincipal)
does, saying whether the resolve is the user being active.
public Task<IUser> GetCurrentUserAsync(ClaimsPrincipal claimsPrincipal, bool recordActivity)
Parameters
claimsPrincipalClaimsPrincipalThe caller; null for the one on the current circuit.
recordActivityboolWhether this resolve counts towards LastSeen. Pass
falsefrom code that reads the user on a schedule rather than because they did something, such as claim revalidation on an idle tab.
Returns
Remarks
Only the last-seen stamp depends on it; keeping the record right from the sign-in (the directory id, a trusted profile refresh) happens either way. The default forwards to GetCurrentUserAsync(ClaimsPrincipal), so a host's own store keeps compiling and records whatever it recorded before.
GetTeamMemberUsersAsync()
The users who share at least one team with the caller, plus the caller themselves. Self-service: an authenticated caller is required but no scope, because the result is derived entirely from the caller's own team memberships and takes no argument that could widen it.
public Task<IReadOnlyList<IUser>> GetTeamMemberUsersAsync()
Returns
Remarks
This is the identity source for the team member list when the caller lacks
Manage. A member row's email, display name and icon live on the user
record rather than on ITeamMember — accepting an invitation clears the per-team name
override and promotes it to Name — so without this projection a team owner would
see their own accepted members as unidentified.
The default implementation returns an empty list; AuthorizationUserServiceDecorator supplies
the real projection, as it holds the undecorated store.
GetUserByKeyAsync(string)
The user with the given key, or null. The default implementation scans GetAsync(); storage-backed services override it with a direct read.
public Task<IUser> GetUserByKeyAsync(string userKey)
Parameters
userKeystring
Returns
SeedUserNameAsync(string, string)
Sets the user's display name only if it is currently null/empty. Used by the invitation-accept flow to promote the admin-entered invitation name into the new user's identity without clobbering an IdP-provided name.
public Task SeedUserNameAsync(string userKey, string name)
Parameters
Returns
SetOwnIconAsync(byte[], string)
public Task SetOwnIconAsync(byte[] data, string contentType)
Parameters
Returns
Remarks
The self-service icon members take no key, so the caller is resolved first to learn which entry to drop. That read is a cache hit in the case that matters — the caller is signed in, which is what put them in the cache — so it costs a dictionary lookup, not a round trip.
SetUserDirectoryIdAsync(string, string)
Links the user to their external-directory id (DirectoryId). Called by the oid backfill (internal self-call) and by directory verification on an email-fallback match (relink). Default is a no-op — stores that track the directory id override it.
public Task SetUserDirectoryIdAsync(string userKey, string directoryId)
Parameters
Returns
SetUserDisabledAsync(string, DateTime?, string)
Deletes the user record from the store, with no team-membership cleanup — call through DeleteUserAsync(string, bool, CancellationToken), which removes team memberships and audits.
public Task SetUserDisabledAsync(string userKey, DateTime? disabledAt, string disabledBy)
Parameters
Returns
Remarks
Throws rather than no-opping when unimplemented, for the same reason DeleteUserAsync(string) does: silently skipping a requested disable would hide the missing implementation behind an apparently successful containment.
SetUserEmailAsync(string, string)
Sets the user's email address (administrative). Call through SetUserEmailAsync(string, string, CancellationToken), which validates the address and audits the change; requires Manage.
public Task SetUserEmailAsync(string userKey, string email)
Parameters
Returns
Remarks
Throws rather than no-opping when unimplemented, as SetUserDisabledAsync(string, DateTime?, string) does: silently skipping the change would report an address that was never set.
SetUserIconAsync(string, byte[], string)
Sets a specific user's icon (administrative). The mechanism is the same as
SetOwnIconAsync(byte[], string) but targets userKey; requires
Manage.
public Task SetUserIconAsync(string userKey, byte[] data, string contentType)
Parameters
Returns
SetUserLastSeenAsync(string, DateTime)
Stamps when the user last made an authenticated request. The automatic throttled stamping is an internal self-call that bypasses the authorization decorator; calling this member from outside requires Manage. The default is a no-op — stores that track LastSeen override it.
public Task SetUserLastSeenAsync(string userKey, DateTime lastSeen)
Parameters
Returns
SetUserNameAsync(string, string)
Always sets the user's display name. Used by the user self-edit flow where the caller has explicitly chosen a name for themselves.
public Task SetUserNameAsync(string userKey, string name)