diff --git a/src/Microsoft.EntityFrameworkCore.SqlServer/ValueGeneration/Internal/SqlServerSequenceHiLoValueGenerator.cs b/src/Microsoft.EntityFrameworkCore.SqlServer/ValueGeneration/Internal/SqlServerSequenceHiLoValueGenerator.cs index a4e23de7e66..7664239f3df 100644 --- a/src/Microsoft.EntityFrameworkCore.SqlServer/ValueGeneration/Internal/SqlServerSequenceHiLoValueGenerator.cs +++ b/src/Microsoft.EntityFrameworkCore.SqlServer/ValueGeneration/Internal/SqlServerSequenceHiLoValueGenerator.cs @@ -3,6 +3,8 @@ using System; using System.Globalization; +using System.Threading; +using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage; @@ -56,6 +58,18 @@ protected override long GetNewLowValue() typeof(long), CultureInfo.InvariantCulture); + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + protected override async Task GetNewLowValueAsync(CancellationToken cancellationToken = default(CancellationToken)) + => (long)Convert.ChangeType( + await _rawSqlCommandBuilder + .Build(_sqlGenerator.GenerateNextSequenceValueOperation(_sequence.Name, _sequence.Schema)) + .ExecuteScalarAsync(_connection, cancellationToken: cancellationToken), + typeof(long), + CultureInfo.InvariantCulture); + /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. diff --git a/src/Microsoft.EntityFrameworkCore/ChangeTracking/Internal/IKeyPropagator.cs b/src/Microsoft.EntityFrameworkCore/ChangeTracking/Internal/IKeyPropagator.cs index 4bda034358f..0a851f0f706 100644 --- a/src/Microsoft.EntityFrameworkCore/ChangeTracking/Internal/IKeyPropagator.cs +++ b/src/Microsoft.EntityFrameworkCore/ChangeTracking/Internal/IKeyPropagator.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Threading; +using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Metadata; @@ -17,5 +19,14 @@ public interface IKeyPropagator /// directly from your code. This API may change or be removed in future releases. /// void PropagateValue([NotNull] InternalEntityEntry entry, [NotNull] IProperty property); + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + Task PropagateValueAsync( + [NotNull] InternalEntityEntry entry, + [NotNull] IProperty property, + CancellationToken cancellationToken = default(CancellationToken)); } } diff --git a/src/Microsoft.EntityFrameworkCore/ChangeTracking/Internal/IValueGenerationManager.cs b/src/Microsoft.EntityFrameworkCore/ChangeTracking/Internal/IValueGenerationManager.cs index 4155468c23a..f2caad82364 100644 --- a/src/Microsoft.EntityFrameworkCore/ChangeTracking/Internal/IValueGenerationManager.cs +++ b/src/Microsoft.EntityFrameworkCore/ChangeTracking/Internal/IValueGenerationManager.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Threading; +using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Metadata; @@ -18,6 +20,14 @@ public interface IValueGenerationManager /// void Generate([NotNull] InternalEntityEntry entry); + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + Task GenerateAsync( + [NotNull] InternalEntityEntry entry, + CancellationToken cancellationToken = default(CancellationToken)); + /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. diff --git a/src/Microsoft.EntityFrameworkCore/ChangeTracking/Internal/InternalEntityEntry.cs b/src/Microsoft.EntityFrameworkCore/ChangeTracking/Internal/InternalEntityEntry.cs index 08d066f7af5..32b80482605 100644 --- a/src/Microsoft.EntityFrameworkCore/ChangeTracking/Internal/InternalEntityEntry.cs +++ b/src/Microsoft.EntityFrameworkCore/ChangeTracking/Internal/InternalEntityEntry.cs @@ -8,6 +8,8 @@ using System.Diagnostics; using System.Linq; using System.Reflection; +using System.Threading; +using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata; @@ -74,6 +76,25 @@ public virtual void SetEntityState(EntityState entityState, bool acceptChanges = SetEntityState(oldState, entityState, acceptChanges); } + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual async Task SetEntityStateAsync( + EntityState entityState, + bool acceptChanges, + CancellationToken cancellationToken = default(CancellationToken)) + { + var oldState = _stateData.EntityState; + + if (PrepareForAdd(entityState)) + { + await StateManager.ValueGeneration.GenerateAsync(this, cancellationToken); + } + + SetEntityState(oldState, entityState, acceptChanges); + } + private bool PrepareForAdd(EntityState newState) { if (newState != EntityState.Added diff --git a/src/Microsoft.EntityFrameworkCore/ChangeTracking/Internal/KeyPropagator.cs b/src/Microsoft.EntityFrameworkCore/ChangeTracking/Internal/KeyPropagator.cs index 729923e6a66..4d1189ee1d1 100644 --- a/src/Microsoft.EntityFrameworkCore/ChangeTracking/Internal/KeyPropagator.cs +++ b/src/Microsoft.EntityFrameworkCore/ChangeTracking/Internal/KeyPropagator.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata; @@ -48,6 +50,29 @@ public virtual void PropagateValue(InternalEntityEntry entry, IProperty property } } + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual async Task PropagateValueAsync( + InternalEntityEntry entry, + IProperty property, + CancellationToken cancellationToken = default(CancellationToken)) + { + Debug.Assert(property.IsForeignKey()); + + if (!TryPropagateValue(entry, property) + && property.IsKey()) + { + var valueGenerator = TryGetValueGenerator(property); + + if (valueGenerator != null) + { + entry[property] = await valueGenerator.NextAsync(new EntityEntry(entry), cancellationToken); + } + } + } + private bool TryPropagateValue(InternalEntityEntry entry, IProperty property) { var entityType = entry.EntityType; diff --git a/src/Microsoft.EntityFrameworkCore/ChangeTracking/Internal/ValueGenerationManager.cs b/src/Microsoft.EntityFrameworkCore/ChangeTracking/Internal/ValueGenerationManager.cs index d524bdc9e63..a6c43a8222e 100644 --- a/src/Microsoft.EntityFrameworkCore/ChangeTracking/Internal/ValueGenerationManager.cs +++ b/src/Microsoft.EntityFrameworkCore/ChangeTracking/Internal/ValueGenerationManager.cs @@ -1,7 +1,10 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System.Diagnostics; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata; @@ -10,7 +13,7 @@ namespace Microsoft.EntityFrameworkCore.ChangeTracking.Internal { /// - /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// public class ValueGenerationManager : IValueGenerationManager @@ -19,7 +22,7 @@ public class ValueGenerationManager : IValueGenerationManager private readonly IKeyPropagator _keyPropagator; /// - /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// public ValueGenerationManager( @@ -31,13 +34,67 @@ public ValueGenerationManager( } /// - /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// public virtual void Generate(InternalEntityEntry entry) { var entityEntry = new EntityEntry(entry); + foreach (var propertyTuple in FindProperties(entry)) + { + var property = propertyTuple.Item1; + + if (propertyTuple.Item2) + { + _keyPropagator.PropagateValue(entry, property); + } + else + { + var valueGenerator = GetValueGenerator(entry, property); + + SetGeneratedValue( + entry, + property, + valueGenerator.Next(entityEntry), + valueGenerator.GeneratesTemporaryValues); + } + } + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual async Task GenerateAsync( + InternalEntityEntry entry, + CancellationToken cancellationToken = default(CancellationToken)) + { + var entityEntry = new EntityEntry(entry); + + foreach (var propertyTuple in FindProperties(entry)) + { + var property = propertyTuple.Item1; + + if (propertyTuple.Item2) + { + _keyPropagator.PropagateValue(entry, property); + } + else + { + var valueGenerator = GetValueGenerator(entry, property); + + SetGeneratedValue( + entry, + property, + await valueGenerator.NextAsync(entityEntry, cancellationToken), + valueGenerator.GeneratesTemporaryValues); + } + } + } + + private IEnumerable> FindProperties(InternalEntityEntry entry) + { foreach (var property in entry.EntityType.GetProperties()) { var isForeignKey = property.IsForeignKey(); @@ -45,26 +102,18 @@ public virtual void Generate(InternalEntityEntry entry) if ((property.RequiresValueGenerator || isForeignKey) && property.ClrType.IsDefaultValue(entry[property])) { - if (isForeignKey) - { - _keyPropagator.PropagateValue(entry, property); - } - else - { - var valueGenerator = _valueGeneratorSelector.Select(property, property.IsKey() - ? property.DeclaringEntityType - : entry.EntityType); - - Debug.Assert(valueGenerator != null); - - SetGeneratedValue(entry, property, valueGenerator.Next(entityEntry), valueGenerator.GeneratesTemporaryValues); - } + yield return Tuple.Create(property, isForeignKey); } } } + private ValueGenerator GetValueGenerator(InternalEntityEntry entry, IProperty property) + => _valueGeneratorSelector.Select(property, property.IsKey() + ? property.DeclaringEntityType + : entry.EntityType); + /// - /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// public virtual bool MayGetTemporaryValue(IProperty property, IEntityType entityType) diff --git a/src/Microsoft.EntityFrameworkCore/DbContext.cs b/src/Microsoft.EntityFrameworkCore/DbContext.cs index a496985c914..47e9d0442a7 100644 --- a/src/Microsoft.EntityFrameworkCore/DbContext.cs +++ b/src/Microsoft.EntityFrameworkCore/DbContext.cs @@ -104,7 +104,7 @@ private IStateManager StateManager => _stateManager ?? (_stateManager = InternalServiceProvider.GetRequiredService()); - internal IAsyncQueryProvider QueryProvider + internal IAsyncQueryProvider QueryProvider => _queryProvider ?? (_queryProvider = this.GetService()); private IServiceProvider InternalServiceProvider @@ -440,6 +440,41 @@ private void SetEntityState(InternalEntityEntry entry, EntityState entityState) public virtual EntityEntry Add([NotNull] TEntity entity) where TEntity : class => SetEntityState(Check.NotNull(entity, nameof(entity)), EntityState.Added); + /// + /// + /// Begins tracking the given entity, and any other reachable entities that are + /// not already being tracked, in the state such that they will + /// be inserted into the database when is called. + /// + /// + /// This method is async only to allow special value generators, such as the one used by + /// 'Microsoft.EntityFrameworkCore.Metadata.SqlServerValueGenerationStrategy.SequenceHiLo', + /// to access the database asynchronously. For all other cases the non async method should be used. + /// + /// + /// The type of the entity. + /// The entity to add. + /// A to observe while waiting for the task to complete. + /// + /// A task that represents the asynchronous Add operation. The task result contains the + /// for the entity. The entry provides access to change tracking + /// information and operations for the entity. + /// + public virtual async Task> AddAsync( + [NotNull] TEntity entity, + CancellationToken cancellationToken = default(CancellationToken)) + where TEntity : class + { + var entry = EntryWithoutDetectChanges(entity); + + await entry.GetInfrastructure().SetEntityStateAsync( + EntityState.Added, + acceptChanges: true, + cancellationToken: cancellationToken); + + return entry; + } + /// /// Begins tracking the given entity, and any other reachable entities that are /// not already being tracked, in the state such that no @@ -544,6 +579,39 @@ private EntityEntry SetEntityState( public virtual EntityEntry Add([NotNull] object entity) => SetEntityState(Check.NotNull(entity, nameof(entity)), EntityState.Added); + /// + /// + /// Begins tracking the given entity, and any other reachable entities that are + /// not already being tracked, in the state such that they will + /// be inserted into the database when is called. + /// + /// + /// This method is async only to allow special value generators, such as the one used by + /// 'Microsoft.EntityFrameworkCore.Metadata.SqlServerValueGenerationStrategy.SequenceHiLo', + /// to access the database asynchronously. For all other cases the non async method should be used. + /// + /// + /// The entity to add. + /// A to observe while waiting for the task to complete. + /// + /// A task that represents the asynchronous Add operation. The task result contains the + /// for the entity. The entry provides access to change tracking + /// information and operations for the entity. + /// + public virtual async Task AddAsync( + [NotNull] object entity, + CancellationToken cancellationToken = default(CancellationToken)) + { + var entry = EntryWithoutDetectChanges(entity); + + await entry.GetInfrastructure().SetEntityStateAsync( + EntityState.Added, + acceptChanges: true, + cancellationToken: cancellationToken); + + return entry; + } + /// /// Begins tracking the given entity, and any other reachable entities that are /// not already being tracked, in the state such that no @@ -638,6 +706,23 @@ private EntityEntry SetEntityState(object entity, EntityState entityState) public virtual void AddRange([NotNull] params object[] entities) => AddRange((IEnumerable)entities); + /// + /// + /// Begins tracking the given entity, and any other reachable entities that are + /// not already being tracked, in the state such that they will + /// be inserted into the database when is called. + /// + /// + /// This method is async only to allow special value generators, such as the one used by + /// 'Microsoft.EntityFrameworkCore.Metadata.SqlServerValueGenerationStrategy.SequenceHiLo', + /// to access the database asynchronously. For all other cases the non async method should be used. + /// + /// + /// The entities to add. + /// A task that represents the asynchronous operation. + public virtual Task AddRangeAsync([NotNull] params object[] entities) + => AddRangeAsync((IEnumerable)entities); + /// /// Begins tracking the given entities, and any other reachable entities that are /// not already being tracked, in the state such that no @@ -702,6 +787,38 @@ private void SetEntityStates(IEnumerable entities, EntityState entitySta public virtual void AddRange([NotNull] IEnumerable entities) => SetEntityStates(Check.NotNull(entities, nameof(entities)), EntityState.Added); + /// + /// + /// Begins tracking the given entity, and any other reachable entities that are + /// not already being tracked, in the state such that they will + /// be inserted into the database when is called. + /// + /// + /// This method is async only to allow special value generators, such as the one used by + /// 'Microsoft.EntityFrameworkCore.Metadata.SqlServerValueGenerationStrategy.SequenceHiLo', + /// to access the database asynchronously. For all other cases the non async method should be used. + /// + /// + /// The entities to add. + /// A to observe while waiting for the task to complete. + /// + /// A task that represents the asynchronous operation. + /// + public virtual async Task AddRangeAsync( + [NotNull] IEnumerable entities, + CancellationToken cancellationToken = default(CancellationToken)) + { + var stateManager = StateManager; + + foreach (var entity in entities) + { + await stateManager.GetOrCreateEntry(entity).SetEntityStateAsync( + EntityState.Added, + acceptChanges: true, + cancellationToken: cancellationToken); + } + } + /// /// Begins tracking the given entities, and any other reachable entities that are /// not already being tracked, in the state such that no diff --git a/src/Microsoft.EntityFrameworkCore/DbSet`.cs b/src/Microsoft.EntityFrameworkCore/DbSet`.cs index ebcf07070b6..2234d4edd88 100644 --- a/src/Microsoft.EntityFrameworkCore/DbSet`.cs +++ b/src/Microsoft.EntityFrameworkCore/DbSet`.cs @@ -103,6 +103,32 @@ public virtual EntityEntry Add([NotNull] TEntity entity) throw new NotImplementedException(); } + /// + /// + /// Begins tracking the given entity, and any other reachable entities that are + /// not already being tracked, in the state such that they will + /// be inserted into the database when is called. + /// + /// + /// This method is async only to allow special value generators, such as the one used by + /// 'Microsoft.EntityFrameworkCore.Metadata.SqlServerValueGenerationStrategy.SequenceHiLo', + /// to access the database asynchronously. For all other cases the non async method should be used. + /// + /// + /// The entity to add. + /// A to observe while waiting for the task to complete. + /// + /// A task that represents the asynchronous Add operation. The task result contains the + /// for the entity. The entry provides access to change tracking + /// information and operations for the entity. + /// + public virtual Task> AddAsync( + [NotNull] TEntity entity, + CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + /// /// Begins tracking the given entity, and any other reachable entities that are /// not already being tracked, in the state such that no @@ -177,6 +203,25 @@ public virtual void AddRange([NotNull] params TEntity[] entities) throw new NotImplementedException(); } + /// + /// + /// Begins tracking the given entities, and any other reachable entities that are + /// not already being tracked, in the state such that they will + /// be inserted into the database when is called. + /// + /// + /// This method is async only to allow special value generators, such as the one used by + /// 'Microsoft.EntityFrameworkCore.Metadata.SqlServerValueGenerationStrategy.SequenceHiLo', + /// to access the database asynchronously. For all other cases the non async method should be used. + /// + /// + /// The entities to add. + /// A task that represents the asynchronous operation. + public virtual Task AddRangeAsync([NotNull] params TEntity[] entities) + { + throw new NotImplementedException(); + } + /// /// Begins tracking the given entities, and any other reachable entities that are /// not already being tracked, in the state such that no @@ -239,6 +284,28 @@ public virtual void AddRange([NotNull] IEnumerable entities) throw new NotImplementedException(); } + /// + /// + /// Begins tracking the given entities, and any other reachable entities that are + /// not already being tracked, in the state such that they will + /// be inserted into the database when is called. + /// + /// + /// This method is async only to allow special value generators, such as the one used by + /// 'Microsoft.EntityFrameworkCore.Metadata.SqlServerValueGenerationStrategy.SequenceHiLo', + /// to access the database asynchronously. For all other cases the non async method should be used. + /// + /// + /// The entities to add. + /// A to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. + public virtual Task AddRangeAsync( + [NotNull] IEnumerable entities, + CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + /// /// Begins tracking the given entities, and any other reachable entities that are /// not already being tracked, in the state such that no diff --git a/src/Microsoft.EntityFrameworkCore/Internal/AsyncLock.cs b/src/Microsoft.EntityFrameworkCore/Internal/AsyncLock.cs new file mode 100644 index 00000000000..27d12331afa --- /dev/null +++ b/src/Microsoft.EntityFrameworkCore/Internal/AsyncLock.cs @@ -0,0 +1,78 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Microsoft.EntityFrameworkCore.Internal +{ + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public sealed class AsyncLock + { + private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1); + private readonly Releaser _releaser; + private readonly Task _releaserTask; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public AsyncLock() + { + _releaser = new Releaser(this); + _releaserTask = Task.FromResult(_releaser); + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public Task LockAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + var wait = _semaphore.WaitAsync(cancellationToken); + + return wait.IsCompleted ? + _releaserTask : + wait.ContinueWith((_, state) => ((AsyncLock)state)._releaser, + this, CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public Releaser Lock() + { + _semaphore.Wait(); + + return _releaser; + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public struct Releaser : IDisposable + { + private readonly AsyncLock _toRelease; + + internal Releaser([NotNull] AsyncLock toRelease) + { + _toRelease = toRelease; + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public void Dispose() + => _toRelease._semaphore.Release(); + } + } +} diff --git a/src/Microsoft.EntityFrameworkCore/Internal/InternalDbSet.cs b/src/Microsoft.EntityFrameworkCore/Internal/InternalDbSet.cs index fa22449057b..48704ed58ba 100644 --- a/src/Microsoft.EntityFrameworkCore/Internal/InternalDbSet.cs +++ b/src/Microsoft.EntityFrameworkCore/Internal/InternalDbSet.cs @@ -48,14 +48,9 @@ public InternalDbSet([NotNull] DbContext context) } /// - /// Finds an entity with the given primary key values. If an entity with the given primary key values - /// is being tracked by the context, then it is returned immediately without making a request to the - /// database. Otherwise, a query is made to the dataabse for an entity with the given primary key values - /// and this entity, if found, is attached to the context and returned. If no entity is found, then - /// null is returned. + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. /// - /// The values of the primary key for the entity to be found. - /// The entity found, or null. public override TEntity Find(params object[] keyValues) { Check.NotNull(keyValues, nameof(keyValues)); @@ -66,27 +61,16 @@ public override TEntity Find(params object[] keyValues) } /// - /// Finds an entity with the given primary key values. If an entity with the given primary key values - /// is being tracked by the context, then it is returned immediately without making a request to the - /// database. Otherwise, a query is made to the dataabse for an entity with the given primary key values - /// and this entity, if found, is attached to the context and returned. If no entity is found, then - /// null is returned. + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. /// - /// The values of the primary key for the entity to be found. - /// The entity found, or null. public override Task FindAsync(params object[] keyValues) => FindAsync(keyValues, default(CancellationToken)); /// - /// Finds an entity with the given primary key values. If an entity with the given primary key values - /// is being tracked by the context, then it is returned immediately without making a request to the - /// database. Otherwise, a query is made to the dataabse for an entity with the given primary key values - /// and this entity, if found, is attached to the context and returned. If no entity is found, then - /// null is returned. + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. /// - /// The values of the primary key for the entity to be found. - /// A to observe while waiting for the task to complete. - /// The entity found, or null. public override Task FindAsync(object[] keyValues, CancellationToken cancellationToken) { Check.NotNull(keyValues, nameof(keyValues)); @@ -165,28 +149,38 @@ private static Expression> BuildPredicate(IReadOnlyList public override EntityEntry Add(TEntity entity) - => _context.Add(Check.NotNull(entity, nameof(entity))); + => _context.Add(entity); + + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public override Task> AddAsync( + TEntity entity, + CancellationToken cancellationToken = default(CancellationToken)) + => _context.AddAsync(entity, cancellationToken); /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// public override EntityEntry Attach(TEntity entity) - => _context.Attach(Check.NotNull(entity, nameof(entity))); + => _context.Attach(entity); /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// public override EntityEntry Remove(TEntity entity) - => _context.Remove(Check.NotNull(entity, nameof(entity))); + => _context.Remove(entity); /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// public override EntityEntry Update(TEntity entity) - => _context.Update(Check.NotNull(entity, nameof(entity))); + => _context.Update(entity); /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used @@ -194,7 +188,15 @@ public override EntityEntry Update(TEntity entity) /// public override void AddRange(params TEntity[] entities) // ReSharper disable once CoVariantArrayConversion - => _context.AddRange(Check.NotNull(entities, nameof(entities))); + => _context.AddRange(entities); + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public override Task AddRangeAsync(params TEntity[] entities) + // ReSharper disable once CoVariantArrayConversion + => _context.AddRangeAsync(entities); /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used @@ -202,7 +204,7 @@ public override void AddRange(params TEntity[] entities) /// public override void AttachRange(params TEntity[] entities) // ReSharper disable once CoVariantArrayConversion - => _context.AttachRange(Check.NotNull(entities, nameof(entities))); + => _context.AttachRange(entities); /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used @@ -210,7 +212,7 @@ public override void AttachRange(params TEntity[] entities) /// public override void RemoveRange(params TEntity[] entities) // ReSharper disable once CoVariantArrayConversion - => _context.RemoveRange(Check.NotNull(entities, nameof(entities))); + => _context.RemoveRange(entities); /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used @@ -218,35 +220,44 @@ public override void RemoveRange(params TEntity[] entities) /// public override void UpdateRange(params TEntity[] entities) // ReSharper disable once CoVariantArrayConversion - => _context.UpdateRange(Check.NotNull(entities, nameof(entities))); + => _context.UpdateRange(entities); /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// public override void AddRange(IEnumerable entities) - => _context.AddRange(Check.NotNull(entities, nameof(entities))); + => _context.AddRange(entities); + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public override Task AddRangeAsync( + IEnumerable entities, + CancellationToken cancellationToken = default(CancellationToken)) + => _context.AddRangeAsync(entities, cancellationToken); /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// public override void AttachRange(IEnumerable entities) - => _context.AttachRange(Check.NotNull(entities, nameof(entities))); + => _context.AttachRange(entities); /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// public override void RemoveRange(IEnumerable entities) - => _context.RemoveRange(Check.NotNull(entities, nameof(entities))); + => _context.RemoveRange(entities); /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// public override void UpdateRange(IEnumerable entities) - => _context.UpdateRange(Check.NotNull(entities, nameof(entities))); + => _context.UpdateRange(entities); IEnumerator IEnumerable.GetEnumerator() => _entityQueryable.Value.GetEnumerator(); diff --git a/src/Microsoft.EntityFrameworkCore/Microsoft.EntityFrameworkCore.csproj b/src/Microsoft.EntityFrameworkCore/Microsoft.EntityFrameworkCore.csproj index 729b9994c5a..3ac7b510177 100644 --- a/src/Microsoft.EntityFrameworkCore/Microsoft.EntityFrameworkCore.csproj +++ b/src/Microsoft.EntityFrameworkCore/Microsoft.EntityFrameworkCore.csproj @@ -186,6 +186,7 @@ + diff --git a/src/Microsoft.EntityFrameworkCore/ValueGeneration/HiLoValueGenerator.cs b/src/Microsoft.EntityFrameworkCore/ValueGeneration/HiLoValueGenerator.cs index 7d669b1a8c9..d798b7f33ed 100644 --- a/src/Microsoft.EntityFrameworkCore/ValueGeneration/HiLoValueGenerator.cs +++ b/src/Microsoft.EntityFrameworkCore/ValueGeneration/HiLoValueGenerator.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Threading; +using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Utilities; @@ -42,10 +44,26 @@ protected HiLoValueGenerator([NotNull] HiLoValueGeneratorState generatorState) /// The value to be assigned to a property. public override TValue Next(EntityEntry entry) => _generatorState.Next(GetNewLowValue); + /// + /// Gets a value to be assigned to a property. + /// + /// The change tracking entry of the entity for which the value is being generated. + /// The value to be assigned to a property. + public override Task NextAsync( + EntityEntry entry, CancellationToken cancellationToken = default(CancellationToken)) + => _generatorState.NextAsync(GetNewLowValueAsync); + /// /// Gets the low value for the next block of values to be used. /// /// The low value for the next block of values to be used. protected abstract long GetNewLowValue(); + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + protected virtual Task GetNewLowValueAsync(CancellationToken cancellationToken = default(CancellationToken)) + => Task.FromResult(GetNewLowValue()); } } diff --git a/src/Microsoft.EntityFrameworkCore/ValueGeneration/HiLoValueGeneratorState.cs b/src/Microsoft.EntityFrameworkCore/ValueGeneration/HiLoValueGeneratorState.cs index 3b02b694161..32ee82603b0 100644 --- a/src/Microsoft.EntityFrameworkCore/ValueGeneration/HiLoValueGeneratorState.cs +++ b/src/Microsoft.EntityFrameworkCore/ValueGeneration/HiLoValueGeneratorState.cs @@ -4,6 +4,7 @@ using System; using System.Globalization; using System.Threading; +using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Utilities; @@ -15,7 +16,7 @@ namespace Microsoft.EntityFrameworkCore.ValueGeneration /// public class HiLoValueGeneratorState { - private readonly object _lock; + private readonly AsyncLock _asyncLock = new AsyncLock(); private HiLoValue _currentValue; private readonly int _blockSize; @@ -35,7 +36,6 @@ public HiLoValueGeneratorState(int blockSize) _blockSize = blockSize; _currentValue = new HiLoValue(-1, 0); - _lock = new object(); } /// @@ -57,7 +57,7 @@ public virtual TValue Next([NotNull] Func getNewLowValue) // gets a chance to use the new new value, so use a while here to do it all again. while (newValue.Low >= newValue.High) { - lock (_lock) + using (_asyncLock.Lock()) { // Once inside the lock check to see if another thread already got a new block, in which // case just get a value out of the new block instead of requesting one. @@ -74,9 +74,54 @@ public virtual TValue Next([NotNull] Func getNewLowValue) } } - return (TValue)Convert.ChangeType(newValue.Low, typeof(TValue), CultureInfo.InvariantCulture); + return ConvertResult(newValue); } + /// + /// Gets a value to be assigned to a property. + /// + /// The type of values being generated. + /// + /// A function to get the next low value if needed. + /// + /// A to observe while waiting for the task to complete. + /// The value to be assigned to a property. + public virtual async Task NextAsync( + [NotNull] Func> getNewLowValue, + CancellationToken cancellationToken = default(CancellationToken)) + { + Check.NotNull(getNewLowValue, nameof(getNewLowValue)); + + var newValue = GetNextValue(); + + // If the chosen value is outside of the current block then we need a new block. + // It is possible that other threads will use all of the new block before this thread + // gets a chance to use the new new value, so use a while here to do it all again. + while (newValue.Low >= newValue.High) + { + using (await _asyncLock.LockAsync()) + { + // Once inside the lock check to see if another thread already got a new block, in which + // case just get a value out of the new block instead of requesting one. + if (newValue.High == _currentValue.High) + { + var newCurrent = await getNewLowValue(cancellationToken); + newValue = new HiLoValue(newCurrent, newCurrent + _blockSize); + _currentValue = newValue; + } + else + { + newValue = GetNextValue(); + } + } + } + + return ConvertResult(newValue); + } + + private static TValue ConvertResult(HiLoValue newValue) + => (TValue)Convert.ChangeType(newValue.Low, typeof(TValue), CultureInfo.InvariantCulture); + private HiLoValue GetNextValue() { HiLoValue originalValue; diff --git a/src/Microsoft.EntityFrameworkCore/ValueGeneration/ValueGenerator.cs b/src/Microsoft.EntityFrameworkCore/ValueGeneration/ValueGenerator.cs index 869e8562b15..352f5047062 100644 --- a/src/Microsoft.EntityFrameworkCore/ValueGeneration/ValueGenerator.cs +++ b/src/Microsoft.EntityFrameworkCore/ValueGeneration/ValueGenerator.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Threading; +using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.ChangeTracking; @@ -17,7 +19,8 @@ public abstract class ValueGenerator /// /// The change tracking entry of the entity for which the value is being generated. /// The value to be assigned to a property. - public virtual object Next([NotNull] EntityEntry entry) => NextValue(entry); + public virtual object Next([NotNull] EntityEntry entry) + => NextValue(entry); /// /// Template method to be overridden by implementations to perform value generation. @@ -26,6 +29,26 @@ public abstract class ValueGenerator /// The generated value. protected abstract object NextValue([NotNull] EntityEntry entry); + /// + /// Gets a value to be assigned to a property. + /// + /// The change tracking entry of the entity for which the value is being generated. + /// The value to be assigned to a property. + public virtual Task NextAsync( + [NotNull] EntityEntry entry, + CancellationToken cancellationToken = default(CancellationToken)) + => NextValueAsync(entry, cancellationToken); + + /// + /// Template method to be overridden by implementations to perform value generation. + /// + /// The change tracking entry of the entity for which the value is being generated. + /// The generated value. + protected virtual Task NextValueAsync( + [NotNull] EntityEntry entry, + CancellationToken cancellationToken = default(CancellationToken)) + => Task.FromResult(NextValue(entry)); + /// /// /// Gets a value indicating whether the values generated are temporary (i.e they should be replaced diff --git a/src/Microsoft.EntityFrameworkCore/ValueGeneration/ValueGenerator`.cs b/src/Microsoft.EntityFrameworkCore/ValueGeneration/ValueGenerator`.cs index 859db91a7da..c54c275e977 100644 --- a/src/Microsoft.EntityFrameworkCore/ValueGeneration/ValueGenerator`.cs +++ b/src/Microsoft.EntityFrameworkCore/ValueGeneration/ValueGenerator`.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Threading; +using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.ChangeTracking; @@ -18,11 +20,32 @@ public abstract class ValueGenerator : ValueGenerator /// The generated value. public new abstract TValue Next([NotNull] EntityEntry entry); + /// + /// Template method to be overridden by implementations to perform value generation. + /// + /// The change tracking entry of the entity for which the value is being generated. + /// The generated value. + public new virtual Task NextAsync( + [NotNull] EntityEntry entry, + CancellationToken cancellationToken = default(CancellationToken)) + => Task.FromResult(Next(entry)); + + /// + /// Gets a value to be assigned to a property. + /// + /// The change tracking entry of the entity for which the value is being generated. + /// The value to be assigned to a property. + protected override object NextValue(EntityEntry entry) + => Next(entry); + /// /// Gets a value to be assigned to a property. /// /// The change tracking entry of the entity for which the value is being generated. /// The value to be assigned to a property. - protected override object NextValue(EntityEntry entry) => Next(entry); + protected override Task NextValueAsync( + EntityEntry entry, + CancellationToken cancellationToken = default(CancellationToken)) + => Task.FromResult((object)Next(entry)); } } diff --git a/test/Microsoft.EntityFrameworkCore.SqlServer.FunctionalTests/SequenceEndToEndTest.cs b/test/Microsoft.EntityFrameworkCore.SqlServer.FunctionalTests/SequenceEndToEndTest.cs index f0c5b4a837f..aaf1369e83a 100644 --- a/test/Microsoft.EntityFrameworkCore.SqlServer.FunctionalTests/SequenceEndToEndTest.cs +++ b/test/Microsoft.EntityFrameworkCore.SqlServer.FunctionalTests/SequenceEndToEndTest.cs @@ -73,8 +73,8 @@ public async Task Can_use_sequence_end_to_end_async() using (var context = new BronieContext(serviceProvider, "BroniesAsync")) { - context.Database.EnsureDeleted(); - context.Database.EnsureCreated(); + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); } await AddEntitiesAsync(serviceProvider, "BroniesAsync"); @@ -106,8 +106,8 @@ private static async Task AddEntitiesAsync(IServiceProvider serviceProvider, str { for (var i = 0; i < 10; i++) { - context.Add(new Pegasus { Name = "Rainbow Dash " + i }); - context.Add(new Pegasus { Name = "Fluttershy " + i }); + await context.AddAsync(new Pegasus { Name = "Rainbow Dash " + i }); + await context.AddAsync(new Pegasus { Name = "Fluttershy " + i }); } await context.SaveChangesAsync(); @@ -115,7 +115,6 @@ private static async Task AddEntitiesAsync(IServiceProvider serviceProvider, str } [ConditionalFact] - [PlatformSkipCondition(TestPlatform.Mac | TestPlatform.Linux, SkipReason = "Test is flaky on OSX/Linux. See https://github.com/dotnet/corefx/issues/8701")] public async Task Can_use_sequence_end_to_end_from_multiple_contexts_concurrently_async() { var serviceProvider = new ServiceCollection() @@ -124,8 +123,8 @@ public async Task Can_use_sequence_end_to_end_from_multiple_contexts_concurrentl using (var context = new BronieContext(serviceProvider, "ManyBronies")) { - context.Database.EnsureDeleted(); - context.Database.EnsureCreated(); + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); } const int threadCount = 50; diff --git a/test/Microsoft.EntityFrameworkCore.SqlServer.Tests/SqlServerSequenceValueGeneratorTest.cs b/test/Microsoft.EntityFrameworkCore.SqlServer.Tests/SqlServerSequenceValueGeneratorTest.cs index f63c7cbcff6..85b664184f2 100644 --- a/test/Microsoft.EntityFrameworkCore.SqlServer.Tests/SqlServerSequenceValueGeneratorTest.cs +++ b/test/Microsoft.EntityFrameworkCore.SqlServer.Tests/SqlServerSequenceValueGeneratorTest.cs @@ -20,31 +20,47 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Tests { public class SqlServerSequenceValueGeneratorTest { - [Fact] - public void Generates_sequential_int_values() => Generates_sequential_values(); - - [Fact] - public void Generates_sequential_long_values() => Generates_sequential_values(); - - [Fact] - public void Generates_sequential_short_values() => Generates_sequential_values(); - - [Fact] - public void Generates_sequential_byte_values() => Generates_sequential_values(); - - [Fact] - public void Generates_sequential_uint_values() => Generates_sequential_values(); - - [Fact] - public void Generates_sequential_ulong_values() => Generates_sequential_values(); - - [Fact] - public void Generates_sequential_ushort_values() => Generates_sequential_values(); - - [Fact] - public void Generates_sequential_sbyte_values() => Generates_sequential_values(); - - public void Generates_sequential_values() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Generates_sequential_int_values(bool async) => await Generates_sequential_values(async); + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Generates_sequential_long_values(bool async) => await Generates_sequential_values(async); + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Generates_sequential_short_values(bool async) => await Generates_sequential_values(async); + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Generates_sequential_byte_values(bool async) => await Generates_sequential_values(async); + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Generates_sequential_uint_values(bool async) => await Generates_sequential_values(async); + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Generates_sequential_ulong_values(bool async) => await Generates_sequential_values(async); + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Generates_sequential_ushort_values(bool async) => await Generates_sequential_values(async); + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Generates_sequential_sbyte_values(bool async) => await Generates_sequential_values(async); + + public async Task Generates_sequential_values(bool async) { const int blockSize = 4; @@ -60,17 +76,21 @@ public void Generates_sequential_values() for (var i = 1; i <= 27; i++) { - Assert.Equal(i, (int)Convert.ChangeType(generator.Next(null), typeof(int), CultureInfo.InvariantCulture)); + var value = async + ? await generator.NextAsync(null) + : generator.Next(null); + + Assert.Equal(i, (int)Convert.ChangeType(value, typeof(int), CultureInfo.InvariantCulture)); } } [Fact] - public void Multiple_threads_can_use_the_same_generator_state() + public async Task Multiple_threads_can_use_the_same_generator_state() { const int threadCount = 50; const int valueCount = 35; - var generatedValues = GenerateValuesInMultipleThreads(threadCount, valueCount); + var generatedValues = await GenerateValuesInMultipleThreads(threadCount, valueCount); // Check that each value was generated once and only once var checks = new bool[threadCount * valueCount]; @@ -86,7 +106,7 @@ public void Multiple_threads_can_use_the_same_generator_state() Assert.True(checks.All(c => c)); } - private IEnumerable> GenerateValuesInMultipleThreads(int threadCount, int valueCount) + private async Task>> GenerateValuesInMultipleThreads(int threadCount, int valueCount) { const int blockSize = 10; @@ -99,25 +119,34 @@ private IEnumerable> GenerateValuesInMultipleThreads(int threadCount, var executor = new FakeRawSqlCommandBuilder(blockSize); var sqlGenerator = new SqlServerUpdateSqlGenerator(new SqlServerSqlGenerationHelper(), new SqlServerTypeMapper()); - var tests = new Action[threadCount]; + var tests = new Func[threadCount]; var generatedValues = new List[threadCount]; for (var i = 0; i < tests.Length; i++) { var testNumber = i; generatedValues[testNumber] = new List(); - tests[testNumber] = () => + tests[testNumber] = async () => { for (var j = 0; j < valueCount; j++) { var connection = CreateConnection(serviceProvider); var generator = new SqlServerSequenceHiLoValueGenerator(executor, sqlGenerator, state, connection); - generatedValues[testNumber].Add(generator.Next(null)); + var value = j % 2 == 0 + ? await generator.NextAsync(null) + : generator.Next(null); + + generatedValues[testNumber].Add(value); } }; } - Parallel.Invoke(tests); + var tasks = tests.Select(Task.Run).ToArray(); + + foreach (var t in tasks) + { + await t; + } return generatedValues; } diff --git a/test/Microsoft.EntityFrameworkCore.Tests/DbContextTest.cs b/test/Microsoft.EntityFrameworkCore.Tests/DbContextTest.cs index af74b717bbc..627d34b81c5 100644 --- a/test/Microsoft.EntityFrameworkCore.Tests/DbContextTest.cs +++ b/test/Microsoft.EntityFrameworkCore.Tests/DbContextTest.cs @@ -383,32 +383,46 @@ public virtual void Resume() } [Fact] - public void Can_add_existing_entities_to_context_to_be_deleted() + public async Task Can_add_existing_entities_to_context_to_be_deleted() { - TrackEntitiesTest((c, e) => c.Remove(e), (c, e) => c.Remove(e), EntityState.Deleted); + await TrackEntitiesTest((c, e) => c.Remove(e), (c, e) => c.Remove(e), EntityState.Deleted); } [Fact] - public void Can_add_new_entities_to_context_with_graph_method() + public async Task Can_add_new_entities_to_context_with_graph_method() { - TrackEntitiesTest((c, e) => c.Add(e), (c, e) => c.Add(e), EntityState.Added); + await TrackEntitiesTest((c, e) => c.Add(e), (c, e) => c.Add(e), EntityState.Added); } [Fact] - public void Can_add_existing_entities_to_context_to_be_attached_with_graph_method() + public async Task Can_add_new_entities_to_context_with_graph_method_async() { - TrackEntitiesTest((c, e) => c.Attach(e), (c, e) => c.Attach(e), EntityState.Unchanged); + await TrackEntitiesTest((c, e) => c.AddAsync(e), (c, e) => c.AddAsync(e), EntityState.Added); } [Fact] - public void Can_add_existing_entities_to_context_to_be_updated_with_graph_method() + public async Task Can_add_existing_entities_to_context_to_be_attached_with_graph_method() { - TrackEntitiesTest((c, e) => c.Update(e), (c, e) => c.Update(e), EntityState.Modified); + await TrackEntitiesTest((c, e) => c.Attach(e), (c, e) => c.Attach(e), EntityState.Unchanged); } - private static void TrackEntitiesTest( + [Fact] + public async Task Can_add_existing_entities_to_context_to_be_updated_with_graph_method() + { + await TrackEntitiesTest((c, e) => c.Update(e), (c, e) => c.Update(e), EntityState.Modified); + } + + private static Task TrackEntitiesTest( Func> categoryAdder, Func> productAdder, EntityState expectedState) + => TrackEntitiesTest( + (c, e) => Task.FromResult(categoryAdder(c, e)), + (c, e) => Task.FromResult(productAdder(c, e)), + expectedState); + + private static async Task TrackEntitiesTest( + Func>> categoryAdder, + Func>> productAdder, EntityState expectedState) { using (var context = new EarlyLearningCenter(TestHelpers.Instance.CreateServiceProvider())) { @@ -417,10 +431,10 @@ private static void TrackEntitiesTest( var product1 = new Product { Id = 1, Name = "Marmite", Price = 7.99m }; var product2 = new Product { Id = 2, Name = "Bovril", Price = 4.99m }; - var categoryEntry1 = categoryAdder(context, category1); - var categoryEntry2 = categoryAdder(context, category2); - var productEntry1 = productAdder(context, product1); - var productEntry2 = productAdder(context, product2); + var categoryEntry1 = await categoryAdder(context, category1); + var categoryEntry2 = await categoryAdder(context, category2); + var productEntry1 = await productAdder(context, product1); + var productEntry2 = await productAdder(context, product2); Assert.Same(category1, categoryEntry1.Entity); Assert.Same(category2, categoryEntry2.Entity); @@ -445,32 +459,54 @@ private static void TrackEntitiesTest( } [Fact] - public void Can_add_multiple_new_entities_to_context() + public async Task Can_add_multiple_new_entities_to_context() + { + await TrackMultipleEntitiesTest((c, e) => c.AddRange(e[0], e[1]), (c, e) => c.AddRange(e[0], e[1]), EntityState.Added); + } + + [Fact] + public async Task Can_add_multiple_new_entities_to_context_async() { - TrackMultipleEntitiesTest((c, e) => c.AddRange(e[0], e[1]), (c, e) => c.AddRange(e[0], e[1]), EntityState.Added); + await TrackMultipleEntitiesTest((c, e) => c.AddRangeAsync(e[0], e[1]), (c, e) => c.AddRangeAsync(e[0], e[1]), EntityState.Added); } [Fact] - public void Can_add_multiple_existing_entities_to_context_to_be_attached() + public async Task Can_add_multiple_existing_entities_to_context_to_be_attached() { - TrackMultipleEntitiesTest((c, e) => c.AttachRange(e[0], e[1]), (c, e) => c.AttachRange(e[0], e[1]), EntityState.Unchanged); + await TrackMultipleEntitiesTest((c, e) => c.AttachRange(e[0], e[1]), (c, e) => c.AttachRange(e[0], e[1]), EntityState.Unchanged); } [Fact] - public void Can_add_multiple_existing_entities_to_context_to_be_updated() + public async Task Can_add_multiple_existing_entities_to_context_to_be_updated() { - TrackMultipleEntitiesTest((c, e) => c.UpdateRange(e[0], e[1]), (c, e) => c.UpdateRange(e[0], e[1]), EntityState.Modified); + await TrackMultipleEntitiesTest((c, e) => c.UpdateRange(e[0], e[1]), (c, e) => c.UpdateRange(e[0], e[1]), EntityState.Modified); } [Fact] - public void Can_add_multiple_existing_entities_to_context_to_be_deleted() + public async Task Can_add_multiple_existing_entities_to_context_to_be_deleted() { - TrackMultipleEntitiesTest((c, e) => c.RemoveRange(e[0], e[1]), (c, e) => c.RemoveRange(e[0], e[1]), EntityState.Deleted); + await TrackMultipleEntitiesTest((c, e) => c.RemoveRange(e[0], e[1]), (c, e) => c.RemoveRange(e[0], e[1]), EntityState.Deleted); } - private static void TrackMultipleEntitiesTest( + private static Task TrackMultipleEntitiesTest( Action categoryAdder, Action productAdder, EntityState expectedState) + => TrackMultipleEntitiesTest( + (c, e) => + { + categoryAdder(c, e); + return Task.FromResult(0); + }, + (c, e) => + { + productAdder(c, e); + return Task.FromResult(0); + }, + expectedState); + + private static async Task TrackMultipleEntitiesTest( + Func categoryAdder, + Func productAdder, EntityState expectedState) { using (var context = new EarlyLearningCenter(TestHelpers.Instance.CreateServiceProvider())) { @@ -479,8 +515,8 @@ private static void TrackMultipleEntitiesTest( var product1 = new Product { Id = 1, Name = "Marmite", Price = 7.99m }; var product2 = new Product { Id = 2, Name = "Bovril", Price = 4.99m }; - categoryAdder(context, new[] { category1, category2 }); - productAdder(context, new[] { product1, product2 }); + await categoryAdder(context, new[] { category1, category2 }); + await productAdder(context, new[] { product1, product2 }); Assert.Same(category1, context.Entry(category1).Entity); Assert.Same(category2, context.Entry(category2).Entity); @@ -500,41 +536,55 @@ private static void TrackMultipleEntitiesTest( } [Fact] - public void Can_add_existing_entities_with_default_value_to_context_to_be_deleted() + public async Task Can_add_existing_entities_with_default_value_to_context_to_be_deleted() { - TrackEntitiesDefaultValueTest((c, e) => c.Remove(e), (c, e) => c.Remove(e), EntityState.Deleted); + await TrackEntitiesDefaultValueTest((c, e) => c.Remove(e), (c, e) => c.Remove(e), EntityState.Deleted); } [Fact] - public void Can_add_new_entities_with_default_value_to_context_with_graph_method() + public async Task Can_add_new_entities_with_default_value_to_context_with_graph_method() { - TrackEntitiesDefaultValueTest((c, e) => c.Add(e), (c, e) => c.Add(e), EntityState.Added); + await TrackEntitiesDefaultValueTest((c, e) => c.Add(e), (c, e) => c.Add(e), EntityState.Added); } [Fact] - public void Can_add_existing_entities_with_default_value_to_context_to_be_attached_with_graph_method() + public async Task Can_add_new_entities_with_default_value_to_context_with_graph_method_async() { - TrackEntitiesDefaultValueTest((c, e) => c.Attach(e), (c, e) => c.Attach(e), EntityState.Unchanged); + await TrackEntitiesDefaultValueTest((c, e) => c.AddAsync(e), (c, e) => c.AddAsync(e), EntityState.Added); } [Fact] - public void Can_add_existing_entities_with_default_value_to_context_to_be_updated_with_graph_method() + public async Task Can_add_existing_entities_with_default_value_to_context_to_be_attached_with_graph_method() { - TrackEntitiesDefaultValueTest((c, e) => c.Update(e), (c, e) => c.Update(e), EntityState.Modified); + await TrackEntitiesDefaultValueTest((c, e) => c.Attach(e), (c, e) => c.Attach(e), EntityState.Unchanged); } - // Issue #3890 - private static void TrackEntitiesDefaultValueTest( + [Fact] + public async Task Can_add_existing_entities_with_default_value_to_context_to_be_updated_with_graph_method() + { + await TrackEntitiesDefaultValueTest((c, e) => c.Update(e), (c, e) => c.Update(e), EntityState.Modified); + } + + private static Task TrackEntitiesDefaultValueTest( Func> categoryAdder, Func> productAdder, EntityState expectedState) + => TrackEntitiesDefaultValueTest( + (c, e) => Task.FromResult(categoryAdder(c, e)), + (c, e) => Task.FromResult(productAdder(c, e)), + expectedState); + + // Issue #3890 + private static async Task TrackEntitiesDefaultValueTest( + Func>> categoryAdder, + Func>> productAdder, EntityState expectedState) { using (var context = new EarlyLearningCenter(TestHelpers.Instance.CreateServiceProvider())) { var category1 = new Category { Id = 0, Name = "Beverages" }; var product1 = new Product { Id = 0, Name = "Marmite", Price = 7.99m }; - var categoryEntry1 = categoryAdder(context, category1); - var productEntry1 = productAdder(context, product1); + var categoryEntry1 = await categoryAdder(context, category1); + var productEntry1 = await productAdder(context, product1); Assert.Same(category1, categoryEntry1.Entity); Assert.Same(product1, productEntry1.Entity); @@ -551,41 +601,63 @@ private static void TrackEntitiesDefaultValueTest( } [Fact] - public void Can_add_multiple_new_entities_with_default_values_to_context() + public async Task Can_add_multiple_new_entities_with_default_values_to_context() { - TrackMultipleEntitiesDefaultValuesTest((c, e) => c.AddRange(e[0]), (c, e) => c.AddRange(e[0]), EntityState.Added); + await TrackMultipleEntitiesDefaultValuesTest((c, e) => c.AddRange(e[0]), (c, e) => c.AddRange(e[0]), EntityState.Added); } [Fact] - public void Can_add_multiple_existing_entities_with_default_values_to_context_to_be_attached() + public async Task Can_add_multiple_new_entities_with_default_values_to_context_async() { - TrackMultipleEntitiesDefaultValuesTest((c, e) => c.AttachRange(e[0]), (c, e) => c.AttachRange(e[0]), EntityState.Unchanged); + await TrackMultipleEntitiesDefaultValuesTest((c, e) => c.AddRangeAsync(e[0]), (c, e) => c.AddRangeAsync(e[0]), EntityState.Added); } [Fact] - public void Can_add_multiple_existing_entities_with_default_values_to_context_to_be_updated() + public async Task Can_add_multiple_existing_entities_with_default_values_to_context_to_be_attached() { - TrackMultipleEntitiesDefaultValuesTest((c, e) => c.UpdateRange(e[0]), (c, e) => c.UpdateRange(e[0]), EntityState.Modified); + await TrackMultipleEntitiesDefaultValuesTest((c, e) => c.AttachRange(e[0]), (c, e) => c.AttachRange(e[0]), EntityState.Unchanged); } [Fact] - public void Can_add_multiple_existing_entities_with_default_values_to_context_to_be_deleted() + public async Task Can_add_multiple_existing_entities_with_default_values_to_context_to_be_updated() { - TrackMultipleEntitiesDefaultValuesTest((c, e) => c.RemoveRange(e[0]), (c, e) => c.RemoveRange(e[0]), EntityState.Deleted); + await TrackMultipleEntitiesDefaultValuesTest((c, e) => c.UpdateRange(e[0]), (c, e) => c.UpdateRange(e[0]), EntityState.Modified); } - // Issue #3890 - private static void TrackMultipleEntitiesDefaultValuesTest( + [Fact] + public async Task Can_add_multiple_existing_entities_with_default_values_to_context_to_be_deleted() + { + await TrackMultipleEntitiesDefaultValuesTest((c, e) => c.RemoveRange(e[0]), (c, e) => c.RemoveRange(e[0]), EntityState.Deleted); + } + + private static Task TrackMultipleEntitiesDefaultValuesTest( Action categoryAdder, Action productAdder, EntityState expectedState) + => TrackMultipleEntitiesDefaultValuesTest( + (c, e) => + { + categoryAdder(c, e); + return Task.FromResult(0); + }, + (c, e) => + { + productAdder(c, e); + return Task.FromResult(0); + }, + expectedState); + + // Issue #3890 + private static async Task TrackMultipleEntitiesDefaultValuesTest( + Func categoryAdder, + Func productAdder, EntityState expectedState) { using (var context = new EarlyLearningCenter(TestHelpers.Instance.CreateServiceProvider())) { var category1 = new Category { Id = 0, Name = "Beverages" }; var product1 = new Product { Id = 0, Name = "Marmite", Price = 7.99m }; - categoryAdder(context, new[] { category1 }); - productAdder(context, new[] { product1 }); + await categoryAdder(context, new[] { category1 }); + await productAdder(context, new[] { product1 }); Assert.Same(category1, context.Entry(category1).Entity); Assert.Same(product1, context.Entry(product1).Entity); @@ -604,6 +676,17 @@ public void Can_add_no_new_entities_to_context() TrackNoEntitiesTest(c => c.AddRange(), c => c.AddRange()); } + [Fact] + public async Task Can_add_no_new_entities_to_context_async() + { + using (var context = new EarlyLearningCenter(TestHelpers.Instance.CreateServiceProvider())) + { + await context.AddRangeAsync(); + await context.AddRangeAsync(); + Assert.Empty(context.ChangeTracker.Entries()); + } + } + [Fact] public void Can_add_no_existing_entities_to_context_to_be_attached() { @@ -633,32 +716,46 @@ private static void TrackNoEntitiesTest(Action categoryAdder, Action< } [Fact] - public void Can_add_existing_entities_to_context_to_be_deleted_non_generic() + public async Task Can_add_existing_entities_to_context_to_be_deleted_non_generic() + { + await TrackEntitiesTestNonGeneric((c, e) => c.Remove(e), (c, e) => c.Remove(e), EntityState.Deleted); + } + + [Fact] + public async Task Can_add_new_entities_to_context_non_generic_graph() { - TrackEntitiesTestNonGeneric((c, e) => c.Remove(e), (c, e) => c.Remove(e), EntityState.Deleted); + await TrackEntitiesTestNonGeneric((c, e) => c.AddAsync(e), (c, e) => c.AddAsync(e), EntityState.Added); } [Fact] - public void Can_add_new_entities_to_context_non_generic_graph() + public async Task Can_add_new_entities_to_context_non_generic_graph_async() { - TrackEntitiesTestNonGeneric((c, e) => c.Add(e), (c, e) => c.Add(e), EntityState.Added); + await TrackEntitiesTestNonGeneric((c, e) => c.Add(e), (c, e) => c.Add(e), EntityState.Added); } [Fact] - public void Can_add_existing_entities_to_context_to_be_attached_non_generic_graph() + public async Task Can_add_existing_entities_to_context_to_be_attached_non_generic_graph() { - TrackEntitiesTestNonGeneric((c, e) => c.Attach(e), (c, e) => c.Attach(e), EntityState.Unchanged); + await TrackEntitiesTestNonGeneric((c, e) => c.Attach(e), (c, e) => c.Attach(e), EntityState.Unchanged); } [Fact] - public void Can_add_existing_entities_to_context_to_be_updated_non_generic_graph() + public async Task Can_add_existing_entities_to_context_to_be_updated_non_generic_graph() { - TrackEntitiesTestNonGeneric((c, e) => c.Update(e), (c, e) => c.Update(e), EntityState.Modified); + await TrackEntitiesTestNonGeneric((c, e) => c.Update(e), (c, e) => c.Update(e), EntityState.Modified); } - private static void TrackEntitiesTestNonGeneric( + private static Task TrackEntitiesTestNonGeneric( Func categoryAdder, Func productAdder, EntityState expectedState) + => TrackEntitiesTestNonGeneric( + (c, e) => Task.FromResult(categoryAdder(c, e)), + (c, e) => Task.FromResult(productAdder(c, e)), + expectedState); + + private static async Task TrackEntitiesTestNonGeneric( + Func> categoryAdder, + Func> productAdder, EntityState expectedState) { using (var context = new EarlyLearningCenter(TestHelpers.Instance.CreateServiceProvider())) { @@ -667,10 +764,10 @@ private static void TrackEntitiesTestNonGeneric( var product1 = new Product { Id = 1, Name = "Marmite", Price = 7.99m }; var product2 = new Product { Id = 2, Name = "Bovril", Price = 4.99m }; - var categoryEntry1 = categoryAdder(context, category1); - var categoryEntry2 = categoryAdder(context, category2); - var productEntry1 = productAdder(context, product1); - var productEntry2 = productAdder(context, product2); + var categoryEntry1 = await categoryAdder(context, category1); + var categoryEntry2 = await categoryAdder(context, category2); + var productEntry1 = await productAdder(context, product1); + var productEntry2 = await productAdder(context, product2); Assert.Same(category1, categoryEntry1.Entity); Assert.Same(category2, categoryEntry2.Entity); @@ -695,32 +792,54 @@ private static void TrackEntitiesTestNonGeneric( } [Fact] - public void Can_add_multiple_existing_entities_to_context_to_be_deleted_Enumerable() + public async Task Can_add_multiple_existing_entities_to_context_to_be_deleted_Enumerable() { - TrackMultipleEntitiesTestEnumerable((c, e) => c.RemoveRange(e), (c, e) => c.RemoveRange(e), EntityState.Deleted); + await TrackMultipleEntitiesTestEnumerable((c, e) => c.RemoveRange(e), (c, e) => c.RemoveRange(e), EntityState.Deleted); } [Fact] - public void Can_add_multiple_new_entities_to_context_Enumerable_graph() + public async Task Can_add_multiple_new_entities_to_context_Enumerable_graph() { - TrackMultipleEntitiesTestEnumerable((c, e) => c.AddRange(e), (c, e) => c.AddRange(e), EntityState.Added); + await TrackMultipleEntitiesTestEnumerable((c, e) => c.AddRange(e), (c, e) => c.AddRange(e), EntityState.Added); } [Fact] - public void Can_add_multiple_existing_entities_to_context_to_be_attached_Enumerable_graph() + public async Task Can_add_multiple_new_entities_to_context_Enumerable_graph_async() { - TrackMultipleEntitiesTestEnumerable((c, e) => c.AttachRange(e), (c, e) => c.AttachRange(e), EntityState.Unchanged); + await TrackMultipleEntitiesTestEnumerable((c, e) => c.AddRangeAsync(e), (c, e) => c.AddRangeAsync(e), EntityState.Added); } [Fact] - public void Can_add_multiple_existing_entities_to_context_to_be_updated_Enumerable_graph() + public async Task Can_add_multiple_existing_entities_to_context_to_be_attached_Enumerable_graph() { - TrackMultipleEntitiesTestEnumerable((c, e) => c.UpdateRange(e), (c, e) => c.UpdateRange(e), EntityState.Modified); + await TrackMultipleEntitiesTestEnumerable((c, e) => c.AttachRange(e), (c, e) => c.AttachRange(e), EntityState.Unchanged); } - private static void TrackMultipleEntitiesTestEnumerable( + [Fact] + public async Task Can_add_multiple_existing_entities_to_context_to_be_updated_Enumerable_graph() + { + await TrackMultipleEntitiesTestEnumerable((c, e) => c.UpdateRange(e), (c, e) => c.UpdateRange(e), EntityState.Modified); + } + + private static Task TrackMultipleEntitiesTestEnumerable( Action> categoryAdder, Action> productAdder, EntityState expectedState) + => TrackMultipleEntitiesTestEnumerable( + (c, e) => + { + categoryAdder(c, e); + return Task.FromResult(0); + }, + (c, e) => + { + productAdder(c, e); + return Task.FromResult(0); + }, + expectedState); + + private static async Task TrackMultipleEntitiesTestEnumerable( + Func, Task> categoryAdder, + Func, Task> productAdder, EntityState expectedState) { using (var context = new EarlyLearningCenter(TestHelpers.Instance.CreateServiceProvider())) { @@ -729,8 +848,8 @@ private static void TrackMultipleEntitiesTestEnumerable( var product1 = new Product { Id = 1, Name = "Marmite", Price = 7.99m }; var product2 = new Product { Id = 2, Name = "Bovril", Price = 4.99m }; - categoryAdder(context, new List { category1, category2 }); - productAdder(context, new List { product1, product2 }); + await categoryAdder(context, new List { category1, category2 }); + await productAdder(context, new List { product1, product2 }); Assert.Same(category1, context.Entry(category1).Entity); Assert.Same(category2, context.Entry(category2).Entity); @@ -750,41 +869,55 @@ private static void TrackMultipleEntitiesTestEnumerable( } [Fact] - public void Can_add_existing_entities_with_default_value_to_context_to_be_deleted_non_generic() + public async Task Can_add_existing_entities_with_default_value_to_context_to_be_deleted_non_generic() { - TrackEntitiesDefaultValuesTestNonGeneric((c, e) => c.Remove(e), (c, e) => c.Remove(e), EntityState.Deleted); + await TrackEntitiesDefaultValuesTestNonGeneric((c, e) => c.Remove(e), (c, e) => c.Remove(e), EntityState.Deleted); } [Fact] - public void Can_add_new_entities_with_default_value_to_context_non_generic_graph() + public async Task Can_add_new_entities_with_default_value_to_context_non_generic_graph() { - TrackEntitiesDefaultValuesTestNonGeneric((c, e) => c.Add(e), (c, e) => c.Add(e), EntityState.Added); + await TrackEntitiesDefaultValuesTestNonGeneric((c, e) => c.Add(e), (c, e) => c.Add(e), EntityState.Added); } [Fact] - public void Can_add_existing_entities_with_default_value_to_context_to_be_attached_non_generic_graph() + public async Task Can_add_new_entities_with_default_value_to_context_non_generic_graph_async() { - TrackEntitiesDefaultValuesTestNonGeneric((c, e) => c.Attach(e), (c, e) => c.Attach(e), EntityState.Unchanged); + await TrackEntitiesDefaultValuesTestNonGeneric((c, e) => c.AddAsync(e), (c, e) => c.AddAsync(e), EntityState.Added); } [Fact] - public void Can_add_existing_entities_with_default_value_to_context_to_be_updated_non_generic_graph() + public async Task Can_add_existing_entities_with_default_value_to_context_to_be_attached_non_generic_graph() { - TrackEntitiesDefaultValuesTestNonGeneric((c, e) => c.Update(e), (c, e) => c.Update(e), EntityState.Modified); + await TrackEntitiesDefaultValuesTestNonGeneric((c, e) => c.Attach(e), (c, e) => c.Attach(e), EntityState.Unchanged); } - // Issue #3890 - private static void TrackEntitiesDefaultValuesTestNonGeneric( + [Fact] + public async Task Can_add_existing_entities_with_default_value_to_context_to_be_updated_non_generic_graph() + { + await TrackEntitiesDefaultValuesTestNonGeneric((c, e) => c.Update(e), (c, e) => c.Update(e), EntityState.Modified); + } + + private static Task TrackEntitiesDefaultValuesTestNonGeneric( Func categoryAdder, Func productAdder, EntityState expectedState) + => TrackEntitiesDefaultValuesTestNonGeneric( + (c, e) => Task.FromResult(categoryAdder(c, e)), + (c, e) => Task.FromResult(productAdder(c, e)), + expectedState); + + // Issue #3890 + private static async Task TrackEntitiesDefaultValuesTestNonGeneric( + Func> categoryAdder, + Func> productAdder, EntityState expectedState) { using (var context = new EarlyLearningCenter(TestHelpers.Instance.CreateServiceProvider())) { var category1 = new Category { Id = 0, Name = "Beverages" }; var product1 = new Product { Id = 0, Name = "Marmite", Price = 7.99m }; - var categoryEntry1 = categoryAdder(context, category1); - var productEntry1 = productAdder(context, product1); + var categoryEntry1 = await categoryAdder(context, category1); + var productEntry1 = await productAdder(context, product1); Assert.Same(category1, categoryEntry1.Entity); Assert.Same(product1, productEntry1.Entity); @@ -801,41 +934,63 @@ private static void TrackEntitiesDefaultValuesTestNonGeneric( } [Fact] - public void Can_add_multiple_existing_entities_with_default_values_to_context_to_be_deleted_Enumerable() + public async Task Can_add_multiple_existing_entities_with_default_values_to_context_to_be_deleted_Enumerable() { - TrackMultipleEntitiesDefaultValueTestEnumerable((c, e) => c.RemoveRange(e), (c, e) => c.RemoveRange(e), EntityState.Deleted); + await TrackMultipleEntitiesDefaultValueTestEnumerable((c, e) => c.RemoveRange(e), (c, e) => c.RemoveRange(e), EntityState.Deleted); } [Fact] - public void Can_add_multiple_new_entities_with_default_values_to_context_Enumerable_graph() + public async Task Can_add_multiple_new_entities_with_default_values_to_context_Enumerable_graph() { - TrackMultipleEntitiesDefaultValueTestEnumerable((c, e) => c.AddRange(e), (c, e) => c.AddRange(e), EntityState.Added); + await TrackMultipleEntitiesDefaultValueTestEnumerable((c, e) => c.AddRange(e), (c, e) => c.AddRange(e), EntityState.Added); } [Fact] - public void Can_add_multiple_existing_entities_with_default_values_to_context_to_be_attached_Enumerable_graph() + public async Task Can_add_multiple_new_entities_with_default_values_to_context_Enumerable_graph_async() { - TrackMultipleEntitiesDefaultValueTestEnumerable((c, e) => c.AttachRange(e), (c, e) => c.AttachRange(e), EntityState.Unchanged); + await TrackMultipleEntitiesDefaultValueTestEnumerable((c, e) => c.AddRangeAsync(e), (c, e) => c.AddRangeAsync(e), EntityState.Added); } [Fact] - public void Can_add_multiple_existing_entities_with_default_values_to_context_to_be_updated_Enumerable_graph() + public async Task Can_add_multiple_existing_entities_with_default_values_to_context_to_be_attached_Enumerable_graph() { - TrackMultipleEntitiesDefaultValueTestEnumerable((c, e) => c.UpdateRange(e), (c, e) => c.UpdateRange(e), EntityState.Modified); + await TrackMultipleEntitiesDefaultValueTestEnumerable((c, e) => c.AttachRange(e), (c, e) => c.AttachRange(e), EntityState.Unchanged); } - // Issue #3890 - private static void TrackMultipleEntitiesDefaultValueTestEnumerable( + [Fact] + public async Task Can_add_multiple_existing_entities_with_default_values_to_context_to_be_updated_Enumerable_graph() + { + await TrackMultipleEntitiesDefaultValueTestEnumerable((c, e) => c.UpdateRange(e), (c, e) => c.UpdateRange(e), EntityState.Modified); + } + + private static Task TrackMultipleEntitiesDefaultValueTestEnumerable( Action> categoryAdder, Action> productAdder, EntityState expectedState) + => TrackMultipleEntitiesDefaultValueTestEnumerable( + (c, e) => + { + categoryAdder(c, e); + return Task.FromResult(0); + }, + (c, e) => + { + productAdder(c, e); + return Task.FromResult(0); + }, + expectedState); + + // Issue #3890 + private static async Task TrackMultipleEntitiesDefaultValueTestEnumerable( + Func, Task> categoryAdder, + Func, Task> productAdder, EntityState expectedState) { using (var context = new EarlyLearningCenter(TestHelpers.Instance.CreateServiceProvider())) { var category1 = new Category { Id = 0, Name = "Beverages" }; var product1 = new Product { Id = 0, Name = "Marmite", Price = 7.99m }; - categoryAdder(context, new List { category1 }); - productAdder(context, new List { product1 }); + await categoryAdder(context, new List { category1 }); + await productAdder(context, new List { product1 }); Assert.Same(category1, context.Entry(category1).Entity); Assert.Same(product1, context.Entry(product1).Entity); @@ -860,6 +1015,17 @@ public void Can_add_no_new_entities_to_context_Enumerable_graph() TrackNoEntitiesTestEnumerable((c, e) => c.AddRange(e), (c, e) => c.AddRange(e)); } + [Fact] + public async Task Can_add_no_new_entities_to_context_Enumerable_graph_async() + { + using (var context = new EarlyLearningCenter(TestHelpers.Instance.CreateServiceProvider())) + { + await context.AddRangeAsync(new HashSet()); + await context.AddRangeAsync(new HashSet()); + Assert.Empty(context.ChangeTracker.Entries()); + } + } + [Fact] public void Can_add_no_existing_entities_to_context_to_be_attached_Enumerable_graph() { @@ -884,21 +1050,27 @@ private static void TrackNoEntitiesTestEnumerable( } } - [Fact] - public void Can_add_new_entities_to_context_with_key_generation_graph() - { - TrackEntitiesWithKeyGenerationTest((c, e) => c.Add(e).Entity); - } - - private static void TrackEntitiesWithKeyGenerationTest(Func adder) + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Can_add_new_entities_to_context_with_key_generation_graph(bool async) { using (var context = new EarlyLearningCenter(TestHelpers.Instance.CreateServiceProvider())) { var gu1 = new TheGu { ShirtColor = "Red" }; var gu2 = new TheGu { ShirtColor = "Still Red" }; - Assert.Same(gu1, adder(context, gu1)); - Assert.Same(gu2, adder(context, gu2)); + if (async) + { + Assert.Same(gu1, (await context.AddAsync(gu1)).Entity); + Assert.Same(gu2, (await context.AddAsync(gu2)).Entity); + } + else + { + Assert.Same(gu1, context.Add(gu1).Entity); + Assert.Same(gu2, context.Add(gu2).Entity); + } + Assert.NotEqual(default(Guid), gu1.Id); Assert.NotEqual(default(Guid), gu2.Id); Assert.NotEqual(gu1.Id, gu2.Id); @@ -914,46 +1086,71 @@ private static void TrackEntitiesWithKeyGenerationTest(Func c.Remove(e), EntityState.Detached, EntityState.Deleted); - ChangeStateWithMethod((c, e) => c.Remove(e), EntityState.Unchanged, EntityState.Deleted); - ChangeStateWithMethod((c, e) => c.Remove(e), EntityState.Deleted, EntityState.Deleted); - ChangeStateWithMethod((c, e) => c.Remove(e), EntityState.Modified, EntityState.Deleted); - ChangeStateWithMethod((c, e) => c.Remove(e), EntityState.Added, EntityState.Detached); + await ChangeStateWithMethod((c, e) => c.Remove(e), EntityState.Detached, EntityState.Deleted); + await ChangeStateWithMethod((c, e) => c.Remove(e), EntityState.Unchanged, EntityState.Deleted); + await ChangeStateWithMethod((c, e) => c.Remove(e), EntityState.Deleted, EntityState.Deleted); + await ChangeStateWithMethod((c, e) => c.Remove(e), EntityState.Modified, EntityState.Deleted); + await ChangeStateWithMethod((c, e) => c.Remove(e), EntityState.Added, EntityState.Detached); } [Fact] - public void Can_use_graph_Add_to_change_entity_state() + public async Task Can_use_graph_Add_to_change_entity_state() { - ChangeStateWithMethod((c, e) => c.Add(e), EntityState.Detached, EntityState.Added); - ChangeStateWithMethod((c, e) => c.Add(e), EntityState.Unchanged, EntityState.Added); - ChangeStateWithMethod((c, e) => c.Add(e), EntityState.Deleted, EntityState.Added); - ChangeStateWithMethod((c, e) => c.Add(e), EntityState.Modified, EntityState.Added); - ChangeStateWithMethod((c, e) => c.Add(e), EntityState.Added, EntityState.Added); + await ChangeStateWithMethod((c, e) => c.Add(e), EntityState.Detached, EntityState.Added); + await ChangeStateWithMethod((c, e) => c.Add(e), EntityState.Unchanged, EntityState.Added); + await ChangeStateWithMethod((c, e) => c.Add(e), EntityState.Deleted, EntityState.Added); + await ChangeStateWithMethod((c, e) => c.Add(e), EntityState.Modified, EntityState.Added); + await ChangeStateWithMethod((c, e) => c.Add(e), EntityState.Added, EntityState.Added); } [Fact] - public void Can_use_graph_Attach_to_change_entity_state() + public async Task Can_use_graph_Add_to_change_entity_state_async() { - ChangeStateWithMethod((c, e) => c.Attach(e), EntityState.Detached, EntityState.Unchanged); - ChangeStateWithMethod((c, e) => c.Attach(e), EntityState.Unchanged, EntityState.Unchanged); - ChangeStateWithMethod((c, e) => c.Attach(e), EntityState.Deleted, EntityState.Unchanged); - ChangeStateWithMethod((c, e) => c.Attach(e), EntityState.Modified, EntityState.Unchanged); - ChangeStateWithMethod((c, e) => c.Attach(e), EntityState.Added, EntityState.Unchanged); + await ChangeStateWithMethod((c, e) => c.AddAsync(e), EntityState.Detached, EntityState.Added); + await ChangeStateWithMethod((c, e) => c.AddAsync(e), EntityState.Unchanged, EntityState.Added); + await ChangeStateWithMethod((c, e) => c.AddAsync(e), EntityState.Deleted, EntityState.Added); + await ChangeStateWithMethod((c, e) => c.AddAsync(e), EntityState.Modified, EntityState.Added); + await ChangeStateWithMethod((c, e) => c.AddAsync(e), EntityState.Added, EntityState.Added); } [Fact] - public void Can_use_graph_Update_to_change_entity_state() + public async Task Can_use_graph_Attach_to_change_entity_state() { - ChangeStateWithMethod((c, e) => c.Update(e), EntityState.Detached, EntityState.Modified); - ChangeStateWithMethod((c, e) => c.Update(e), EntityState.Unchanged, EntityState.Modified); - ChangeStateWithMethod((c, e) => c.Update(e), EntityState.Deleted, EntityState.Modified); - ChangeStateWithMethod((c, e) => c.Update(e), EntityState.Modified, EntityState.Modified); - ChangeStateWithMethod((c, e) => c.Update(e), EntityState.Added, EntityState.Modified); + await ChangeStateWithMethod((c, e) => c.Attach(e), EntityState.Detached, EntityState.Unchanged); + await ChangeStateWithMethod((c, e) => c.Attach(e), EntityState.Unchanged, EntityState.Unchanged); + await ChangeStateWithMethod((c, e) => c.Attach(e), EntityState.Deleted, EntityState.Unchanged); + await ChangeStateWithMethod((c, e) => c.Attach(e), EntityState.Modified, EntityState.Unchanged); + await ChangeStateWithMethod((c, e) => c.Attach(e), EntityState.Added, EntityState.Unchanged); } - private void ChangeStateWithMethod(Action action, EntityState initialState, EntityState expectedState) + [Fact] + public async Task Can_use_graph_Update_to_change_entity_state() + { + await ChangeStateWithMethod((c, e) => c.Update(e), EntityState.Detached, EntityState.Modified); + await ChangeStateWithMethod((c, e) => c.Update(e), EntityState.Unchanged, EntityState.Modified); + await ChangeStateWithMethod((c, e) => c.Update(e), EntityState.Deleted, EntityState.Modified); + await ChangeStateWithMethod((c, e) => c.Update(e), EntityState.Modified, EntityState.Modified); + await ChangeStateWithMethod((c, e) => c.Update(e), EntityState.Added, EntityState.Modified); + } + + private Task ChangeStateWithMethod( + Action action, + EntityState initialState, + EntityState expectedState) + => ChangeStateWithMethod((c, e) => + { + action(c, e); + return Task.FromResult(0); + }, + initialState, + expectedState); + + private async Task ChangeStateWithMethod( + Func action, + EntityState initialState, + EntityState expectedState) { using (var context = new EarlyLearningCenter(TestHelpers.Instance.CreateServiceProvider())) { @@ -962,7 +1159,7 @@ private void ChangeStateWithMethod(Action action, EntityState entry.State = initialState; - action(context, entity); + await action(context, entity); Assert.Equal(expectedState, entry.State); } @@ -4813,7 +5010,7 @@ public void Auto_DetectChanges_for_Entry_can_be_switched_off(bool useGenericOver } [Fact] - public void Add_Attach_Remove_Update_do_not_call_DetectChanges() + public async Task Add_Attach_Remove_Update_do_not_call_DetectChanges() { var provider = TestHelpers.Instance.CreateServiceProvider(new ServiceCollection().AddScoped()); using (var context = new ButTheHedgehogContext(provider)) @@ -4830,6 +5027,12 @@ public void Add_Attach_Remove_Update_do_not_call_DetectChanges() context.AddRange(new Product { Id = id++, Name = "Little Hedgehogs" }); context.AddRange(new List { new Product { Id = id++, Name = "Little Hedgehogs" } }); context.AddRange(new List { new Product { Id = id++, Name = "Little Hedgehogs" } }); + await context.AddAsync(new Product { Id = id++, Name = "Little Hedgehogs" }); + await context.AddAsync((object)new Product { Id = id++, Name = "Little Hedgehogs" }); + await context.AddRangeAsync(new Product { Id = id++, Name = "Little Hedgehogs" }); + await context.AddRangeAsync(new Product { Id = id++, Name = "Little Hedgehogs" }); + await context.AddRangeAsync(new List { new Product { Id = id++, Name = "Little Hedgehogs" } }); + await context.AddRangeAsync(new List { new Product { Id = id++, Name = "Little Hedgehogs" } }); context.Attach(new Product { Id = id++, Name = "Little Hedgehogs" }); context.Attach((object)new Product { Id = id++, Name = "Little Hedgehogs" }); context.AttachRange(new Product { Id = id++, Name = "Little Hedgehogs" }); @@ -4901,9 +5104,10 @@ public async void It_throws_object_disposed_exception() Assert.Throws(() => context.Remove(new object())); Assert.Throws(() => context.SaveChanges()); await Assert.ThrowsAsync(() => context.SaveChangesAsync()); + await Assert.ThrowsAsync(() => context.AddAsync(new object())); var methodCount = typeof(DbContext).GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly).Count(); - var expectedMethodCount = 27; + var expectedMethodCount = 31; Assert.True( methodCount == expectedMethodCount, userMessage: $"Expected {expectedMethodCount} methods on DbContext but found {methodCount}. " + diff --git a/test/Microsoft.EntityFrameworkCore.Tests/DbSetTest.cs b/test/Microsoft.EntityFrameworkCore.Tests/DbSetTest.cs index c4562ffffc3..198bed1e2c9 100644 --- a/test/Microsoft.EntityFrameworkCore.Tests/DbSetTest.cs +++ b/test/Microsoft.EntityFrameworkCore.Tests/DbSetTest.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Specification.Tests; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -15,32 +16,46 @@ namespace Microsoft.EntityFrameworkCore.Tests public class DbSetTest { [Fact] - public void Can_add_existing_entities_to_context_to_be_deleted() + public async Task Can_add_existing_entities_to_context_to_be_deleted() { - TrackEntitiesTest((c, e) => c.Remove(e), (c, e) => c.Remove(e), EntityState.Deleted); + await TrackEntitiesTest((c, e) => c.Remove(e), (c, e) => c.Remove(e), EntityState.Deleted); } [Fact] - public void Can_add_new_entities_to_context_graph() + public async Task Can_add_new_entities_to_context_graph() { - TrackEntitiesTest((c, e) => c.Add(e), (c, e) => c.Add(e), EntityState.Added); + await TrackEntitiesTest((c, e) => c.Add(e), (c, e) => c.Add(e), EntityState.Added); } [Fact] - public void Can_add_existing_entities_to_context_to_be_attached_graph() + public async Task Can_add_new_entities_to_context_graph_async() { - TrackEntitiesTest((c, e) => c.Attach(e), (c, e) => c.Attach(e), EntityState.Unchanged); + await TrackEntitiesTest((c, e) => c.AddAsync(e), (c, e) => c.AddAsync(e), EntityState.Added); } [Fact] - public void Can_add_existing_entities_to_context_to_be_updated_graph() + public async Task Can_add_existing_entities_to_context_to_be_attached_graph() { - TrackEntitiesTest((c, e) => c.Update(e), (c, e) => c.Update(e), EntityState.Modified); + await TrackEntitiesTest((c, e) => c.Attach(e), (c, e) => c.Attach(e), EntityState.Unchanged); } - private static void TrackEntitiesTest( + [Fact] + public async Task Can_add_existing_entities_to_context_to_be_updated_graph() + { + await TrackEntitiesTest((c, e) => c.Update(e), (c, e) => c.Update(e), EntityState.Modified); + } + + private static Task TrackEntitiesTest( Func, Category, EntityEntry> categoryAdder, Func, Product, EntityEntry> productAdder, EntityState expectedState) + => TrackEntitiesTest( + (c, e) => Task.FromResult(categoryAdder(c, e)), + (c, e) => Task.FromResult(productAdder(c, e)), + expectedState); + + private static async Task TrackEntitiesTest( + Func, Category, Task>> categoryAdder, + Func, Product, Task>> productAdder, EntityState expectedState) { using (var context = new EarlyLearningCenter()) { @@ -49,10 +64,10 @@ private static void TrackEntitiesTest( var product1 = new Product { Id = 1, Name = "Marmite", Price = 7.99m }; var product2 = new Product { Id = 2, Name = "Bovril", Price = 4.99m }; - var categoryEntry1 = categoryAdder(context.Categories, category1); - var categoryEntry2 = categoryAdder(context.Categories, category2); - var productEntry1 = productAdder(context.Products, product1); - var productEntry2 = productAdder(context.Products, product2); + var categoryEntry1 = await categoryAdder(context.Categories, category1); + var categoryEntry2 = await categoryAdder(context.Categories, category2); + var productEntry1 = await productAdder(context.Products, product1); + var productEntry2 = await productAdder(context.Products, product2); Assert.Same(category1, categoryEntry1.Entity); Assert.Same(category2, categoryEntry2.Entity); @@ -77,32 +92,69 @@ private static void TrackEntitiesTest( } [Fact] - public void Can_add_multiple_new_entities_to_set() + public async Task Can_add_multiple_new_entities_to_set() + { + await TrackMultipleEntitiesTest( + (c, e) => c.Categories.AddRange(e[0], e[1]), + (c, e) => c.Products.AddRange(e[0], e[1]), + EntityState.Added); + } + + [Fact] + public async Task Can_add_multiple_new_entities_to_set_async() { - TrackMultipleEntitiesTest((c, e) => c.Categories.AddRange(e[0], e[1]), (c, e) => c.Products.AddRange(e[0], e[1]), EntityState.Added); + await TrackMultipleEntitiesTest( + (c, e) => c.Categories.AddRangeAsync(e[0], e[1]), + (c, e) => c.Products.AddRangeAsync(e[0], e[1]), + EntityState.Added); } [Fact] - public void Can_add_multiple_existing_entities_to_set_to_be_attached() + public async Task Can_add_multiple_existing_entities_to_set_to_be_attached() { - TrackMultipleEntitiesTest((c, e) => c.Categories.AttachRange(e[0], e[1]), (c, e) => c.Products.AttachRange(e[0], e[1]), EntityState.Unchanged); + await TrackMultipleEntitiesTest( + (c, e) => c.Categories.AttachRange(e[0], e[1]), + (c, e) => c.Products.AttachRange(e[0], e[1]), + EntityState.Unchanged); } [Fact] - public void Can_add_multiple_existing_entities_to_set_to_be_updated() + public async Task Can_add_multiple_existing_entities_to_set_to_be_updated() { - TrackMultipleEntitiesTest((c, e) => c.Categories.UpdateRange(e[0], e[1]), (c, e) => c.Products.UpdateRange(e[0], e[1]), EntityState.Modified); + await TrackMultipleEntitiesTest( + (c, e) => c.Categories.UpdateRange(e[0], e[1]), + (c, e) => c.Products.UpdateRange(e[0], e[1]), + EntityState.Modified); } [Fact] - public void Can_add_multiple_existing_entities_to_set_to_be_deleted() + public async Task Can_add_multiple_existing_entities_to_set_to_be_deleted() { - TrackMultipleEntitiesTest((c, e) => c.Categories.RemoveRange(e[0], e[1]), (c, e) => c.Products.RemoveRange(e[0], e[1]), EntityState.Deleted); + await TrackMultipleEntitiesTest( + (c, e) => c.Categories.RemoveRange(e[0], e[1]), + (c, e) => c.Products.RemoveRange(e[0], e[1]), + EntityState.Deleted); } - private static void TrackMultipleEntitiesTest( + private static Task TrackMultipleEntitiesTest( Action categoryAdder, Action productAdder, EntityState expectedState) + => TrackMultipleEntitiesTest( + (c, e) => + { + categoryAdder(c, e); + return Task.FromResult(0); + }, + (c, e) => + { + productAdder(c, e); + return Task.FromResult(0); + }, + expectedState); + + private static async Task TrackMultipleEntitiesTest( + Func categoryAdder, + Func productAdder, EntityState expectedState) { using (var context = new EarlyLearningCenter()) { @@ -111,8 +163,8 @@ private static void TrackMultipleEntitiesTest( var product1 = new Product { Id = 1, Name = "Marmite", Price = 7.99m }; var product2 = new Product { Id = 2, Name = "Bovril", Price = 4.99m }; - categoryAdder(context, new[] { category1, category2 }); - productAdder(context, new[] { product1, product2 }); + await categoryAdder(context, new[] { category1, category2 }); + await productAdder(context, new[] { product1, product2 }); Assert.Same(category1, context.Entry(category1).Entity); Assert.Same(category2, context.Entry(category2).Entity); @@ -137,6 +189,17 @@ public void Can_add_no_new_entities_to_set() TrackNoEntitiesTest(c => c.Categories.AddRange(), c => c.Products.AddRange()); } + [Fact] + public async Task Can_add_no_new_entities_to_set_async() + { + using (var context = new EarlyLearningCenter()) + { + await context.Categories.AddRangeAsync(); + await context.Products.AddRangeAsync(); + Assert.Empty(context.ChangeTracker.Entries()); + } + } + [Fact] public void Can_add_no_existing_entities_to_set_to_be_attached() { @@ -166,32 +229,69 @@ private static void TrackNoEntitiesTest(Action categoryAdde } [Fact] - public void Can_add_multiple_existing_entities_to_set_to_be_deleted_Enumerable() + public async Task Can_add_multiple_existing_entities_to_set_to_be_deleted_Enumerable() { - TrackMultipleEntitiesTestEnumerable((c, e) => c.Categories.RemoveRange(e), (c, e) => c.Products.RemoveRange(e), EntityState.Deleted); + await TrackMultipleEntitiesTestEnumerable( + (c, e) => c.Categories.RemoveRange(e), + (c, e) => c.Products.RemoveRange(e), + EntityState.Deleted); } [Fact] - public void Can_add_multiple_new_entities_to_set_Enumerable_graph() + public async Task Can_add_multiple_new_entities_to_set_Enumerable_graph() { - TrackMultipleEntitiesTestEnumerable((c, e) => c.Categories.AddRange(e), (c, e) => c.Products.AddRange(e), EntityState.Added); + await TrackMultipleEntitiesTestEnumerable( + (c, e) => c.Categories.AddRange(e), + (c, e) => c.Products.AddRange(e), + EntityState.Added); } [Fact] - public void Can_add_multiple_existing_entities_to_set_to_be_attached_Enumerable_graph() + public async Task Can_add_multiple_new_entities_to_set_Enumerable_graph_async() { - TrackMultipleEntitiesTestEnumerable((c, e) => c.Categories.AttachRange(e), (c, e) => c.Products.AttachRange(e), EntityState.Unchanged); + await TrackMultipleEntitiesTestEnumerable( + (c, e) => c.Categories.AddRangeAsync(e), + (c, e) => c.Products.AddRangeAsync(e), + EntityState.Added); } [Fact] - public void Can_add_multiple_existing_entities_to_set_to_be_updated_Enumerable_graph() + public async Task Can_add_multiple_existing_entities_to_set_to_be_attached_Enumerable_graph() { - TrackMultipleEntitiesTestEnumerable((c, e) => c.Categories.UpdateRange(e), (c, e) => c.Products.UpdateRange(e), EntityState.Modified); + await TrackMultipleEntitiesTestEnumerable( + (c, e) => c.Categories.AttachRange(e), + (c, e) => c.Products.AttachRange(e), + EntityState.Unchanged); } - private static void TrackMultipleEntitiesTestEnumerable( + [Fact] + public async Task Can_add_multiple_existing_entities_to_set_to_be_updated_Enumerable_graph() + { + await TrackMultipleEntitiesTestEnumerable( + (c, e) => c.Categories.UpdateRange(e), + (c, e) => c.Products.UpdateRange(e), + EntityState.Modified); + } + + private static Task TrackMultipleEntitiesTestEnumerable( Action> categoryAdder, Action> productAdder, EntityState expectedState) + => TrackMultipleEntitiesTestEnumerable( + (c, e) => + { + categoryAdder(c, e); + return Task.FromResult(0); + }, + (c, e) => + { + productAdder(c, e); + return Task.FromResult(0); + }, + expectedState); + + private static async Task TrackMultipleEntitiesTestEnumerable( + Func, Task> categoryAdder, + Func, Task> productAdder, EntityState expectedState) { using (var context = new EarlyLearningCenter()) { @@ -200,8 +300,8 @@ private static void TrackMultipleEntitiesTestEnumerable( var product1 = new Product { Id = 1, Name = "Marmite", Price = 7.99m }; var product2 = new Product { Id = 2, Name = "Bovril", Price = 4.99m }; - categoryAdder(context, new List { category1, category2 }); - productAdder(context, new List { product1, product2 }); + await categoryAdder(context, new List { category1, category2 }); + await productAdder(context, new List { product1, product2 }); Assert.Same(category1, context.Entry(category1).Entity); Assert.Same(category2, context.Entry(category2).Entity); @@ -232,6 +332,17 @@ public void Can_add_no_new_entities_to_set_Enumerable_graph() TrackNoEntitiesTestEnumerable((c, e) => c.Categories.AddRange(e), (c, e) => c.Products.AddRange(e)); } + [Fact] + public async Task Can_add_no_new_entities_to_set_Enumerable_graph_async() + { + using (var context = new EarlyLearningCenter()) + { + await context.Categories.AddRangeAsync(new HashSet()); + await context.Products.AddRangeAsync(new HashSet()); + Assert.Empty(context.ChangeTracker.Entries()); + } + } + [Fact] public void Can_add_no_existing_entities_to_set_to_be_attached_Enumerable_graph() { @@ -257,46 +368,71 @@ private static void TrackNoEntitiesTestEnumerable( } [Fact] - public void Can_use_Add_to_change_entity_state() + public async Task Can_use_Add_to_change_entity_state() { - ChangeStateWithMethod((c, e) => c.Categories.Add(e), EntityState.Detached, EntityState.Added); - ChangeStateWithMethod((c, e) => c.Categories.Add(e), EntityState.Unchanged, EntityState.Added); - ChangeStateWithMethod((c, e) => c.Categories.Add(e), EntityState.Deleted, EntityState.Added); - ChangeStateWithMethod((c, e) => c.Categories.Add(e), EntityState.Modified, EntityState.Added); - ChangeStateWithMethod((c, e) => c.Categories.Add(e), EntityState.Added, EntityState.Added); + await ChangeStateWithMethod((c, e) => c.Categories.Add(e), EntityState.Detached, EntityState.Added); + await ChangeStateWithMethod((c, e) => c.Categories.Add(e), EntityState.Unchanged, EntityState.Added); + await ChangeStateWithMethod((c, e) => c.Categories.Add(e), EntityState.Deleted, EntityState.Added); + await ChangeStateWithMethod((c, e) => c.Categories.Add(e), EntityState.Modified, EntityState.Added); + await ChangeStateWithMethod((c, e) => c.Categories.Add(e), EntityState.Added, EntityState.Added); } [Fact] - public void Can_use_Attach_to_change_entity_state() + public async Task Can_use_Add_to_change_entity_state_async() { - ChangeStateWithMethod((c, e) => c.Categories.Attach(e), EntityState.Detached, EntityState.Unchanged); - ChangeStateWithMethod((c, e) => c.Categories.Attach(e), EntityState.Unchanged, EntityState.Unchanged); - ChangeStateWithMethod((c, e) => c.Categories.Attach(e), EntityState.Deleted, EntityState.Unchanged); - ChangeStateWithMethod((c, e) => c.Categories.Attach(e), EntityState.Modified, EntityState.Unchanged); - ChangeStateWithMethod((c, e) => c.Categories.Attach(e), EntityState.Added, EntityState.Unchanged); + await ChangeStateWithMethod((c, e) => c.Categories.AddAsync(e), EntityState.Detached, EntityState.Added); + await ChangeStateWithMethod((c, e) => c.Categories.AddAsync(e), EntityState.Unchanged, EntityState.Added); + await ChangeStateWithMethod((c, e) => c.Categories.AddAsync(e), EntityState.Deleted, EntityState.Added); + await ChangeStateWithMethod((c, e) => c.Categories.AddAsync(e), EntityState.Modified, EntityState.Added); + await ChangeStateWithMethod((c, e) => c.Categories.AddAsync(e), EntityState.Added, EntityState.Added); } [Fact] - public void Can_use_Update_to_change_entity_state() + public async Task Can_use_Attach_to_change_entity_state() { - ChangeStateWithMethod((c, e) => c.Categories.Update(e), EntityState.Detached, EntityState.Modified); - ChangeStateWithMethod((c, e) => c.Categories.Update(e), EntityState.Unchanged, EntityState.Modified); - ChangeStateWithMethod((c, e) => c.Categories.Update(e), EntityState.Deleted, EntityState.Modified); - ChangeStateWithMethod((c, e) => c.Categories.Update(e), EntityState.Modified, EntityState.Modified); - ChangeStateWithMethod((c, e) => c.Categories.Update(e), EntityState.Added, EntityState.Modified); + await ChangeStateWithMethod((c, e) => c.Categories.Attach(e), EntityState.Detached, EntityState.Unchanged); + await ChangeStateWithMethod((c, e) => c.Categories.Attach(e), EntityState.Unchanged, EntityState.Unchanged); + await ChangeStateWithMethod((c, e) => c.Categories.Attach(e), EntityState.Deleted, EntityState.Unchanged); + await ChangeStateWithMethod((c, e) => c.Categories.Attach(e), EntityState.Modified, EntityState.Unchanged); + await ChangeStateWithMethod((c, e) => c.Categories.Attach(e), EntityState.Added, EntityState.Unchanged); } [Fact] - public void Can_use_Remove_to_change_entity_state() + public async Task Can_use_Update_to_change_entity_state() { - ChangeStateWithMethod((c, e) => c.Categories.Remove(e), EntityState.Detached, EntityState.Deleted); - ChangeStateWithMethod((c, e) => c.Categories.Remove(e), EntityState.Unchanged, EntityState.Deleted); - ChangeStateWithMethod((c, e) => c.Categories.Remove(e), EntityState.Deleted, EntityState.Deleted); - ChangeStateWithMethod((c, e) => c.Categories.Remove(e), EntityState.Modified, EntityState.Deleted); - ChangeStateWithMethod((c, e) => c.Categories.Remove(e), EntityState.Added, EntityState.Detached); + await ChangeStateWithMethod((c, e) => c.Categories.Update(e), EntityState.Detached, EntityState.Modified); + await ChangeStateWithMethod((c, e) => c.Categories.Update(e), EntityState.Unchanged, EntityState.Modified); + await ChangeStateWithMethod((c, e) => c.Categories.Update(e), EntityState.Deleted, EntityState.Modified); + await ChangeStateWithMethod((c, e) => c.Categories.Update(e), EntityState.Modified, EntityState.Modified); + await ChangeStateWithMethod((c, e) => c.Categories.Update(e), EntityState.Added, EntityState.Modified); } - private void ChangeStateWithMethod(Action action, EntityState initialState, EntityState expectedState) + [Fact] + public async Task Can_use_Remove_to_change_entity_state() + { + await ChangeStateWithMethod((c, e) => c.Categories.Remove(e), EntityState.Detached, EntityState.Deleted); + await ChangeStateWithMethod((c, e) => c.Categories.Remove(e), EntityState.Unchanged, EntityState.Deleted); + await ChangeStateWithMethod((c, e) => c.Categories.Remove(e), EntityState.Deleted, EntityState.Deleted); + await ChangeStateWithMethod((c, e) => c.Categories.Remove(e), EntityState.Modified, EntityState.Deleted); + await ChangeStateWithMethod((c, e) => c.Categories.Remove(e), EntityState.Added, EntityState.Detached); + } + + private Task ChangeStateWithMethod( + Action action, + EntityState initialState, + EntityState expectedState) + => ChangeStateWithMethod((c, e) => + { + action(c, e); + return Task.FromResult(0); + }, + initialState, + expectedState); + + private async Task ChangeStateWithMethod( + Func action, + EntityState initialState, + EntityState expectedState) { using (var context = new EarlyLearningCenter()) { @@ -305,27 +441,33 @@ private void ChangeStateWithMethod(Action action, entry.State = initialState; - action(context, entity); + await action(context, entity); Assert.Equal(expectedState, entry.State); } } - [Fact] - public void Can_add_new_entities_to_context_with_key_generation() - { - TrackEntitiesWithKeyGenerationTest((c, e) => c.Add(e).Entity); - } - - private static void TrackEntitiesWithKeyGenerationTest(Func, TheGu, TheGu> adder) + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Can_add_new_entities_to_context_with_key_generation(bool async) { using (var context = new EarlyLearningCenter()) { var gu1 = new TheGu { ShirtColor = "Red" }; var gu2 = new TheGu { ShirtColor = "Still Red" }; - Assert.Same(gu1, adder(context.Gus, gu1)); - Assert.Same(gu2, adder(context.Gus, gu2)); + if (async) + { + Assert.Same(gu1, (await context.Gus.AddAsync(gu1)).Entity); + Assert.Same(gu2, (await context.Gus.AddAsync(gu2)).Entity); + } + else + { + Assert.Same(gu1, context.Gus.Add(gu1).Entity); + Assert.Same(gu2, context.Gus.Add(gu2).Entity); + } + Assert.NotEqual(default(Guid), gu1.Id); Assert.NotEqual(default(Guid), gu2.Id); Assert.NotEqual(gu1.Id, gu2.Id);