@@ -132,21 +132,31 @@ public async Task SaveSessionAsync(UAuthSession session, long expectedVersion, C
132132 projection . Version ++ ;
133133 }
134134
135- public Task CreateSessionAsync ( UAuthSession session , CancellationToken ct = default )
135+ public async Task CreateSessionAsync ( UAuthSession session , CancellationToken ct = default )
136136 {
137137 ct . ThrowIfCancellationRequested ( ) ;
138138
139+ if ( session . Tenant != _tenant )
140+ throw new InvalidOperationException ( "Tenant mismatch." ) ;
141+
139142 if ( ! _inExecution )
140143 throw new InvalidOperationException ( "Must be called inside ExecuteAsync" ) ;
141144
142- var projection = session . ToProjection ( ) ;
143-
144145 if ( session . Version != 0 )
145146 throw new InvalidOperationException ( "New session must have version 0." ) ;
146147
147- DbSetSession . Add ( projection ) ;
148+ var exists = await _db . Set < SessionProjection > ( )
149+ . AnyAsync (
150+ x => x . Tenant == _tenant &&
151+ x . SessionId == session . SessionId ,
152+ ct ) ;
148153
149- return Task . CompletedTask ;
154+ if ( exists )
155+ throw new UAuthConcurrencyException (
156+ "session_already_exists" ) ;
157+
158+ var projection = session . ToProjection ( ) ;
159+ DbSetSession . Add ( projection ) ;
150160 }
151161
152162 public async Task < bool > RevokeSessionAsync ( AuthSessionId sessionId , DateTimeOffset at , CancellationToken ct = default )
@@ -165,6 +175,19 @@ public async Task<bool> RevokeSessionAsync(AuthSessionId sessionId, DateTimeOffs
165175 domain . UpdateProjection ( projection ) ;
166176 projection . Version ++ ;
167177
178+ var chain = await DbSetChain
179+ . SingleOrDefaultAsync (
180+ x => x . Tenant == _tenant &&
181+ x . ChainId == projection . ChainId ,
182+ ct ) ;
183+
184+ if ( chain ? . ActiveSessionId == sessionId )
185+ {
186+ chain . ActiveSessionId = null ;
187+ chain . LastSeenAt = at ;
188+ chain . Version ++ ;
189+ }
190+
168191 return true ;
169192 }
170193
@@ -314,22 +337,38 @@ public async Task SaveChainAsync(UAuthSessionChain chain, long expectedVersion,
314337 projection . Version ++ ;
315338 }
316339
317- public Task CreateChainAsync ( UAuthSessionChain chain , CancellationToken ct = default )
340+ public async Task CreateChainAsync ( UAuthSessionChain chain , CancellationToken ct = default )
318341 {
319342 ct . ThrowIfCancellationRequested ( ) ;
320343
344+ if ( chain . Tenant != _tenant )
345+ throw new InvalidOperationException ( "Tenant mismatch." ) ;
346+
321347 if ( ! _inExecution )
322348 throw new InvalidOperationException ( "Must be called inside ExecuteAsync" ) ;
323349
324350 if ( chain . Version != 0 )
325351 throw new InvalidOperationException ( "New chain must have version 0." ) ;
326352
327- var projection = chain . ToProjection ( ) ;
353+ var exists =
354+ DbSetChain . Local . Any ( x =>
355+ x . Tenant == _tenant &&
356+ x . ChainId == chain . ChainId ) ;
328357
329- DbSetChain . Add ( projection ) ;
330- _db . Entry ( projection ) . State = EntityState . Added ;
358+ if ( ! exists )
359+ {
360+ exists = await DbSetChain
361+ . AsNoTracking ( )
362+ . AnyAsync (
363+ x => x . Tenant == _tenant &&
364+ x . ChainId == chain . ChainId ,
365+ ct ) ;
366+ }
331367
332- return Task . CompletedTask ;
368+ if ( exists )
369+ throw new UAuthConcurrencyException ( "chain_already_exists" ) ;
370+
371+ DbSetChain . Add ( chain . ToProjection ( ) ) ;
333372 }
334373
335374 public async Task RevokeChainAsync ( SessionChainId chainId , DateTimeOffset at , CancellationToken ct = default )
@@ -475,6 +514,9 @@ public async Task SaveRootAsync(UAuthSessionRoot root, long expectedVersion, Can
475514 {
476515 ct . ThrowIfCancellationRequested ( ) ;
477516
517+ if ( root . Tenant != _tenant )
518+ throw new InvalidOperationException ( "Tenant mismatch." ) ;
519+
478520 if ( ! _inExecution )
479521 throw new InvalidOperationException ( "Must be called inside ExecuteAsync" ) ;
480522
@@ -494,21 +536,40 @@ public async Task SaveRootAsync(UAuthSessionRoot root, long expectedVersion, Can
494536 projection . Version ++ ;
495537 }
496538
497- public Task CreateRootAsync ( UAuthSessionRoot root , CancellationToken ct = default )
539+ public async Task CreateRootAsync ( UAuthSessionRoot root , CancellationToken ct = default )
498540 {
499541 ct . ThrowIfCancellationRequested ( ) ;
500542
543+ if ( root . Tenant != _tenant )
544+ throw new InvalidOperationException ( "Tenant mismatch." ) ;
545+
501546 if ( ! _inExecution )
502547 throw new InvalidOperationException ( "Must be called inside ExecuteAsync" ) ;
503548
504549 if ( root . Version != 0 )
505550 throw new InvalidOperationException ( "New root must have version 0." ) ;
506551
552+ var exists = DbSetRoot . Local . Any ( x =>
553+ x . Tenant == _tenant &&
554+ x . UserKey == root . UserKey ) ;
555+
556+ if ( ! exists )
557+ {
558+ exists = await DbSetRoot
559+ . AsNoTracking ( )
560+ . AnyAsync (
561+ x =>
562+ x . Tenant == _tenant &&
563+ x . UserKey == root . UserKey ,
564+ ct ) ;
565+ }
566+
567+ if ( exists )
568+ throw new UAuthConcurrencyException ( "root_already_exists" ) ;
569+
507570 var projection = root . ToProjection ( ) ;
508571
509572 DbSetRoot . Add ( projection ) ;
510-
511- return Task . CompletedTask ;
512573 }
513574
514575 public async Task RevokeRootAsync ( UserKey userKey , DateTimeOffset at , CancellationToken ct = default )
@@ -604,6 +665,21 @@ public async Task RemoveSessionAsync(AuthSessionId sessionId, CancellationToken
604665 if ( projection is null )
605666 return ;
606667
668+ var chain = await DbSetChain
669+ . SingleOrDefaultAsync (
670+ x => x . Tenant == _tenant &&
671+ x . ChainId == projection . ChainId ,
672+ ct ) ;
673+
674+ if ( chain ? . ActiveSessionId == sessionId )
675+ {
676+ chain . ActiveSessionId = null ;
677+
678+ // Remove has no DateTimeOffset parameter.
679+ // Don't introduce DateTimeOffset.UtcNow here merely to update LastSeenAt.
680+ chain . Version ++ ;
681+ }
682+
607683 DbSetSession . Remove ( projection ) ;
608684 }
609685
0 commit comments