@@ -360,6 +360,24 @@ public virtual async Task SaveChanges_too_large_entry_after_smaller_throws_after
360360 Assert . Equal ( "1" , ( await assertContext . Customers . FirstAsync ( ) ) . Id ) ;
361361 }
362362
363+ [ ConditionalFact ]
364+ public virtual async Task SaveChanges_transaction_behaviour_always_payload_exactly_2_mib ( )
365+ {
366+ var contextFactory = await InitializeAsync < TransactionalBatchContext > ( ) ;
367+
368+ using var context = contextFactory . CreateContext ( ) ;
369+ context . Database . AutoTransactionBehavior = AutoTransactionBehavior . Always ;
370+
371+ context . Customers . Add ( new Customer { Id = "1" , Name = new string ( 'x' , 1048291 ) , PartitionKey = "1" } ) ;
372+ context . Customers . Add ( new Customer { Id = "2" , Name = new string ( 'x' , 1048291 ) , PartitionKey = "1" } ) ;
373+
374+ await context . SaveChangesAsync ( ) ;
375+
376+ using var assertContext = contextFactory . CreateContext ( ) ;
377+ var customersCount = await assertContext . Customers . CountAsync ( ) ;
378+ Assert . Equal ( 2 , customersCount ) ;
379+ }
380+
363381 [ ConditionalFact ]
364382 public virtual async Task SaveChanges_transaction_behaviour_always_payload_larger_than_cosmos_limit_throws ( )
365383 {
@@ -381,80 +399,106 @@ public virtual async Task SaveChanges_transaction_behaviour_always_payload_large
381399 Assert . Equal ( 0 , customersCount ) ;
382400 }
383401
384- // The tests below will fail if the cosmos db sdk is updated and the serialization logic for transactional batches has changed
402+ private const int nameLengthToExceed2MiBWithSpecialCharIdOnUpdate = 1046358 ;
385403
386404 [ ConditionalTheory , InlineData ( true ) , InlineData ( false ) ]
387- public virtual async Task SaveChanges_transaction_behaviour_always_single_entity_payload_can_be_exactly_cosmos_limit_and_throws_when_1byte_over ( bool oneByteOver )
405+ public virtual async Task SaveChanges_update_id_contains_special_chars_which_makes_request_larger_than_2_mib_splits_into_2_batches ( bool isIdSpecialChar )
388406 {
389407 var contextFactory = await InitializeAsync < TransactionalBatchContext > ( ) ;
390-
408+
391409 using var context = contextFactory . CreateContext ( ) ;
392- context . Database . AutoTransactionBehavior = AutoTransactionBehavior . Always ;
393410
394- var customer = new Customer { Id = new string ( 'x' , 1_000 ) , PartitionKey = new string ( 'x' , 1_000 ) } ;
411+ var id1 = isIdSpecialChar ? new string ( '€' , 341 ) : new string ( 'x' , 341 ) ;
412+ var id2 = isIdSpecialChar ? new string ( 'Ω' , 341 ) : new string ( 'y' , 341 ) ;
413+
414+ var customer1 = new Customer { Id = id1 , PartitionKey = new string ( '€' , 341 ) } ;
415+ var customer2 = new Customer { Id = id2 , PartitionKey = new string ( '€' , 341 ) } ;
416+
417+ context . Customers . Add ( customer1 ) ;
418+ context . Customers . Add ( customer2 ) ;
395419
396- context . Customers . Add ( customer ) ;
397420 await context . SaveChangesAsync ( ) ;
421+ ListLoggerFactory . Clear ( ) ;
398422
399- // Total document size will be: 2_097_503. Total request size will be: 2_098_541
400- // Normally 2MiB is 2_097_152, but cosmos appears to allow ~1Kib (1389 bytes) extra
401- var str = new string ( 'x' , 2_095_228 ) ;
402- customer . Name = str ;
423+ customer1 . Name = new string ( 'x' , nameLengthToExceed2MiBWithSpecialCharIdOnUpdate ) ;
424+ customer2 . Name = new string ( 'x' , nameLengthToExceed2MiBWithSpecialCharIdOnUpdate ) ;
403425
404- if ( oneByteOver )
426+ await context . SaveChangesAsync ( ) ;
427+ using var assertContext = contextFactory . CreateContext ( ) ;
428+ Assert . Equal ( 2 , ( await context . Customers . ToListAsync ( ) ) . Count ) ;
429+
430+ // The id being a special character should make the difference whether this fits in 1 batch.
431+ if ( isIdSpecialChar )
405432 {
406- customer . Name += 'x' ;
407- var ex = await Assert . ThrowsAsync < DbUpdateException > ( ( ) => context . SaveChangesAsync ( ) ) ;
408- Assert . IsType < CosmosException > ( ex . InnerException ) ;
433+ Assert . Equal ( 2 , ListLoggerFactory . Log . Count ( x => x . Id == CosmosEventId . ExecutedTransactionalBatch ) ) ;
409434 }
410435 else
411436 {
412- await context . SaveChangesAsync ( ) ;
413-
414- using var assertContext = contextFactory . CreateContext ( ) ;
415- var dbCustomer = await assertContext . Customers . FirstAsync ( ) ;
416- Assert . Equal ( dbCustomer . Name , str ) ;
437+ Assert . Equal ( 1 , ListLoggerFactory . Log . Count ( x => x . Id == CosmosEventId . ExecutedTransactionalBatch ) ) ;
417438 }
418439 }
419440
420441 [ ConditionalTheory , InlineData ( true ) , InlineData ( false ) ]
421- public virtual async Task SaveChanges_update_id_contains_special_chars_which_makes_request_larger_than_2_mib_splits_into_2_batches ( bool isIdSpecialChar )
442+ public virtual async Task SaveChanges_create_id_contains_special_chars_which_would_make_request_larger_than_2_mib_on_update_does_not_split_into_2_batches_for_create ( bool isIdSpecialChar )
422443 {
423444 var contextFactory = await InitializeAsync < TransactionalBatchContext > ( ) ;
424445
425446 using var context = contextFactory . CreateContext ( ) ;
426447
427- string id1 = isIdSpecialChar ? new string ( '€' , 341 ) : new string ( 'x' , 341 ) ;
428- string id2 = isIdSpecialChar ? new string ( 'Ω' , 341 ) : new string ( 'y' , 341 ) ;
448+ var id1 = isIdSpecialChar ? new string ( '€' , 341 ) : new string ( 'x' , 341 ) ;
449+ var id2 = isIdSpecialChar ? new string ( 'Ω' , 341 ) : new string ( 'y' , 341 ) ;
429450
430- var customer1 = new Customer { Id = id1 , PartitionKey = new string ( '€' , 341 ) } ;
431- var customer2 = new Customer { Id = id2 , PartitionKey = new string ( '€' , 341 ) } ;
451+ var customer1 = new Customer { Id = id1 , Name = new string ( 'x' , nameLengthToExceed2MiBWithSpecialCharIdOnUpdate ) , PartitionKey = new string ( '€' , 341 ) } ;
452+ var customer2 = new Customer { Id = id2 , Name = new string ( 'x' , nameLengthToExceed2MiBWithSpecialCharIdOnUpdate ) , PartitionKey = new string ( '€' , 341 ) } ;
432453
433454 context . Customers . Add ( customer1 ) ;
434455 context . Customers . Add ( customer2 ) ;
435456
436457 await context . SaveChangesAsync ( ) ;
437- ListLoggerFactory . Clear ( ) ;
458+ using var assertContext = contextFactory . CreateContext ( ) ;
459+ Assert . Equal ( 2 , ( await context . Customers . ToListAsync ( ) ) . Count ) ;
460+
461+ // The id being a special character should not make the difference whether this fits in 1 batch, as id is duplicated in the payload on create.
462+ Assert . Equal ( 1 , ListLoggerFactory . Log . Count ( x => x . Id == CosmosEventId . ExecutedTransactionalBatch ) ) ;
463+ }
464+
465+ [ ConditionalTheory , InlineData ( true ) , InlineData ( false ) ]
466+ [ CosmosCondition ( CosmosCondition . IsNotEmulator ) ]
467+ public virtual async Task SaveChanges_transaction_behaviour_always_single_entity_payload_can_be_exactly_cosmos_limit_and_throws_when_1byte_over ( bool oneByteOver )
468+ {
469+ var contextFactory = await InitializeAsync < TransactionalBatchContext > ( ) ;
470+
471+ using var context = contextFactory . CreateContext ( ) ;
472+ context . Database . AutoTransactionBehavior = AutoTransactionBehavior . Always ;
438473
439- customer1 . Name = new string ( 'x' , 1046358 ) ;
440- customer2 . Name = new string ( 'x' , 1046358 ) ;
474+ var customer = new Customer { Id = new string ( 'x' , 1_000 ) , PartitionKey = new string ( 'x' , 1_000 ) } ;
441475
476+ context . Customers . Add ( customer ) ;
442477 await context . SaveChangesAsync ( ) ;
443- using var assertContext = contextFactory . CreateContext ( ) ;
444- Assert . Equal ( 2 , ( await context . Customers . ToListAsync ( ) ) . Count ) ;
445478
446- // The id being a special character should make the difference whether this fits in 1 batch.
447- if ( isIdSpecialChar )
479+ // Total document size will be: 2_097_510. Total request size will be: 2_098_548
480+ // Normally 2MiB is 2_097_152, but cosmos appears to allow ~1Kib (1396 bytes) extra
481+ var str = new string ( 'x' , 2_095_235 ) ;
482+ customer . Name = str ;
483+
484+ if ( oneByteOver )
448485 {
449- Assert . Equal ( 2 , ListLoggerFactory . Log . Count ( x => x . Id == CosmosEventId . ExecutedTransactionalBatch ) ) ;
486+ customer . Name += 'x' ;
487+ var ex = await Assert . ThrowsAsync < DbUpdateException > ( ( ) => context . SaveChangesAsync ( ) ) ;
488+ Assert . IsType < CosmosException > ( ex . InnerException ) ;
450489 }
451490 else
452491 {
453- Assert . Equal ( 1 , ListLoggerFactory . Log . Count ( x => x . Id == CosmosEventId . ExecutedTransactionalBatch ) ) ;
492+ await context . SaveChangesAsync ( ) ;
493+
494+ using var assertContext = contextFactory . CreateContext ( ) ;
495+ var dbCustomer = await assertContext . Customers . FirstAsync ( ) ;
496+ Assert . Equal ( dbCustomer . Name , str ) ;
454497 }
455498 }
456499
457500 [ ConditionalTheory , InlineData ( true ) , InlineData ( false ) ]
501+ [ CosmosCondition ( CosmosCondition . IsNotEmulator ) ]
458502 public virtual async Task SaveChanges_transaction_behaviour_always_update_entities_payload_can_be_exactly_cosmos_limit_and_throws_when_1byte_over ( bool oneByteOver )
459503 {
460504 var contextFactory = await InitializeAsync < TransactionalBatchContext > ( ) ;
@@ -470,8 +514,8 @@ public virtual async Task SaveChanges_transaction_behaviour_always_update_entiti
470514
471515 await context . SaveChangesAsync ( ) ;
472516
473- customer1 . Name = new string ( 'x' , 1097582 ) ;
474- customer2 . Name = new string ( 'x' , 1097583 ) ;
517+ customer1 . Name = new string ( 'x' , 1097589 ) ;
518+ customer2 . Name = new string ( 'x' , 1097590 ) ;
475519
476520 if ( oneByteOver )
477521 {
@@ -486,6 +530,7 @@ public virtual async Task SaveChanges_transaction_behaviour_always_update_entiti
486530 }
487531
488532 [ ConditionalTheory , InlineData ( true ) , InlineData ( false ) ]
533+ [ CosmosCondition ( CosmosCondition . IsNotEmulator ) ]
489534 public virtual async Task SaveChanges_id_counts_double_toward_request_size_on_update ( bool oneByteOver )
490535 {
491536 var contextFactory = await InitializeAsync < TransactionalBatchContext > ( ) ;
@@ -495,14 +540,14 @@ public virtual async Task SaveChanges_id_counts_double_toward_request_size_on_up
495540
496541 var customer1 = new Customer { Id = new string ( 'x' , 1 ) , PartitionKey = new string ( 'x' , 1_023 ) } ;
497542 var customer2 = new Customer { Id = new string ( 'y' , 1_023 ) , PartitionKey = new string ( 'x' , 1_023 ) } ;
498-
543+
499544 context . Customers . Add ( customer1 ) ;
500545 context . Customers . Add ( customer2 ) ;
501546
502547 await context . SaveChangesAsync ( ) ;
503548
504- customer1 . Name = new string ( 'x' , 1097582 + 1_022 * 2 + 1 ) ;
505- customer2 . Name = new string ( 'x' , 1097583 ) ;
549+ customer1 . Name = new string ( 'x' , 1097590 + 1_022 * 2 ) ;
550+ customer2 . Name = new string ( 'x' , 1097590 ) ;
506551
507552 if ( oneByteOver )
508553 {
@@ -517,15 +562,16 @@ public virtual async Task SaveChanges_id_counts_double_toward_request_size_on_up
517562 }
518563
519564 [ ConditionalTheory , InlineData ( true ) , InlineData ( false ) ]
565+ [ CosmosCondition ( CosmosCondition . IsNotEmulator ) ]
520566 public virtual async Task SaveChanges_transaction_behaviour_always_create_entities_payload_can_be_exactly_cosmos_limit_and_throws_when_1byte_over ( bool oneByteOver )
521567 {
522568 var contextFactory = await InitializeAsync < TransactionalBatchContext > ( ) ;
523569
524570 using var context = contextFactory . CreateContext ( ) ;
525571 context . Database . AutoTransactionBehavior = AutoTransactionBehavior . Always ;
526572
527- var customer1 = new Customer { Id = new string ( 'x' , 1_023 ) , Name = new string ( 'x' , 1098841 ) , PartitionKey = new string ( 'x' , 1_023 ) } ;
528- var customer2 = new Customer { Id = new string ( 'y' , 1_023 ) , Name = new string ( 'x' , 1098841 ) , PartitionKey = new string ( 'x' , 1_023 ) } ;
573+ var customer1 = new Customer { Id = new string ( 'x' , 1_023 ) , Name = new string ( 'x' , 1098848 ) , PartitionKey = new string ( 'x' , 1_023 ) } ;
574+ var customer2 = new Customer { Id = new string ( 'y' , 1_023 ) , Name = new string ( 'x' , 1098848 ) , PartitionKey = new string ( 'x' , 1_023 ) } ;
529575 if ( oneByteOver )
530576 {
531577 customer1 . Name += 'x' ;
@@ -545,15 +591,16 @@ public virtual async Task SaveChanges_transaction_behaviour_always_create_entiti
545591 }
546592
547593 [ ConditionalTheory , InlineData ( true ) , InlineData ( false ) ]
594+ [ CosmosCondition ( CosmosCondition . IsNotEmulator ) ]
548595 public virtual async Task SaveChanges_id_does_not_count_double_toward_request_size_on_create ( bool oneByteOver )
549596 {
550597 var contextFactory = await InitializeAsync < TransactionalBatchContext > ( ) ;
551598
552599 using var context = contextFactory . CreateContext ( ) ;
553600 context . Database . AutoTransactionBehavior = AutoTransactionBehavior . Always ;
554601
555- var customer1 = new Customer { Id = new string ( 'x' , 1 ) , Name = new string ( 'x' , 1098841 + 1_022 ) , PartitionKey = new string ( 'x' , 1_023 ) } ;
556- var customer2 = new Customer { Id = new string ( 'y' , 1_023 ) , Name = new string ( 'x' , 1098841 ) , PartitionKey = new string ( 'x' , 1_023 ) } ;
602+ var customer1 = new Customer { Id = new string ( 'x' , 1 ) , Name = new string ( 'x' , 1098848 + 1_022 ) , PartitionKey = new string ( 'x' , 1_023 ) } ;
603+ var customer2 = new Customer { Id = new string ( 'y' , 1_023 ) , Name = new string ( 'x' , 1098848 ) , PartitionKey = new string ( 'x' , 1_023 ) } ;
557604 if ( oneByteOver )
558605 {
559606 customer1 . Name += 'x' ;
0 commit comments