-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Description
Context
I am playing with preview2 to determine if it could be used for my project.
The app should use event sourcing, so I wrote the method to spawn new core/versioned entity alongside with the event specific to that kind of operation.
I branched the method to accept params TEntity[]
and use Add
if the count is 1 and AddRange
otherwise to test single vs bulk spawning performance.
With the very same entity type, I am getting success on AddRange
and failing on Add
.
ExceptionMessage: "The property 'ID' on entity type 'Title' has a temporary value while attempting to change the entity's state to 'Unchanged'. Either set a permanent value explicitly or ensure that the database is configured to generate values for this property."
StackTrace: " в Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState oldState, EntityState newState, Boolean acceptChanges)
в Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState entityState, Boolean acceptChanges)
в Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.AcceptChanges()
в Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.AcceptAllChanges(IEnumerable`1 changedEntries)
в Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.<SaveChangesAsync>d__58.MoveNext()
--- Конец трассировка стека из предыдущего расположения, где возникло исключение ---
в System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
в System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
в Microsoft.EntityFrameworkCore.DbContext.<SaveChangesAsync>d__44.MoveNext()
--- Конец трассировка стека из предыдущего расположения, где возникло исключение ---
в System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
в System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
в System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
в DbPerf.Entities.ContextBase`1.<SpawnImmutableOrVersionedEntityWithEvent>d__38`3.MoveNext()
--- Конец трассировка стека из предыдущего расположения, где возникло исключение ---
в System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
в System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
в System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
в Utils.Pipe.Pipe.<AwaitThenAsync>d__8`2.MoveNext()
--- Конец трассировка стека из предыдущего расположения, где возникло исключение ---
в System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
в System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
в System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
в Utils.AsyncLinq.AsyncEnumerator.SelectAsyncEnumerable`2.<GetNextAsync>d__3.MoveNext()
--- Конец трассировка стека из предыдущего расположения, где возникло исключение ---
в System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
в System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
в System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
в Utils.AsyncLinq.AsyncEnumerator.<Execute>d__12`1.MoveNext()
--- Конец трассировка стека из предыдущего расположения, где возникло исключение ---
в System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
в System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
в System.Runtime.CompilerServices.ConfiguredTaskAwaitable.ConfiguredTaskAwaiter.GetResult()
в Utils.AsyncLinq.AsyncEnumerator.<ToList>d__36`1.MoveNext()
--- Конец трассировка стека из предыдущего расположения, где возникло исключение ---
в System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
в System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
в System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
в Utils.AsyncLinq.AsyncEnumerator.<ToArray>d__33`1.MoveNext()
--- Конец трассировка стека из предыдущего расположения, где возникло исключение ---
в System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
в System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
в System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
в DbPerf.DbBenchmark`1.<DoSafeAsync>d__21`1.MoveNext()"
Code
Core Entity
public interface ITitle
{
int ClientID { get; set; }
}
public class Title : ITitle
{
// events
public virtual TitleEventCreated Created { get; set; }
public virtual ICollection<TitleEventMetadataVersionChanged> MetadataVersions { get; set; } = new List<TitleEventMetadataVersionChanged>();
// /events
public long ID { get; set; }
public int ClientID { get; set; }
public virtual ICollection<Page> Pages { get; set; } = new List<Page>();
internal Title() { }
public Title( ITitle mock )
=> new Title
{
ClientID = mock.ClientID,
};
}
Event to be spawned
public interface IConcreteEvent
{
long ID { get; set; }
Event Base { get; set; } // Used to be inherited with TPT, but TPT is not supported by EF Core
EventType Type { get; }
}
public interface ITitleContextualEvent : IConcreteEvent
{
long TitleID { get; set; }
Title Title { get; set; }
}
public class Event // Used to be base class for all of the events, but TPT is not supported by EF Core
{
public long ID { get; set; }
public Guid UserID { get; set; }
public DateTime Spawned { get; set; }
public EventType EventType { get; set; }
public virtual TitleEventCreated TitleEventCreated { get; set; }
public virtual TitleEventMetadataVersionChanged TitleEventMetadataVersionChanged { get; set; }
public virtual PageEventFound PageEventFound { get; set; }
public virtual PageEventMetadataVersionChanged PageEventMetadataVersionChanged { get; set; }
protected internal Event() { }
internal Event( Guid userId, EventType type )
: this( userId, type, DateTime.UtcNow ) { }
internal Event( Guid userId, EventType type, DateTime spawned )
: this()
{
UserID = userId;
EventType = type;
Spawned = spawned;
}
}
public class TitleEventCreated : ITitleContextualEvent
{
public long ID { get; set; }
public virtual Event Base { get; set; }
public EventType Type => EventType.TitleEventCreated;
public long TitleID { get; set; }
public virtual Title Title { get; set; }
protected internal TitleEventCreated() { }
internal TitleEventCreated( Event @base, Title title )
: this()
{
Base = @base;
Title = title;
}
}
Mappings
// IMap defines only void Map( ModelBuilder @base ) and all concrete implementors are created and called with reflection in OnModelConfiguring
public abstract class EntityTypeConfiguration<T> : IMapping where T : class
{
private ModelBuilder _modelBuilder;
private EntityTypeBuilder<T> _entityBuilder;
void IMapping.Map( ModelBuilder @base )
{
_modelBuilder = @base;
_entityBuilder = _modelBuilder.Entity<T>();
Map( _entityBuilder );
}
protected abstract void Map( EntityTypeBuilder<T> builder );
protected PropertyBuilder<long> MapToNpgsqlBigSequence( Expression<Func<T, long>> propertyExpression, long start = 1, long end = Int64.MaxValue, int increment = 1 )
=> MapToNpgsqlSequence( propertyExpression, start, end, increment );
protected PropertyBuilder<int> MapToNpgsqlSequence( Expression<Func<T, int>> propertyExpression, long start = 1, long end = Int32.MaxValue, int increment = 1 )
=> MapToNpgsqlSequence( propertyExpression, start, end, increment );
private PropertyBuilder<TSequence> MapToNpgsqlSequence<TSequence>( Expression<Func<T, TSequence>> propertyExpression, long start, long end, int increment )
where TSequence : struct
{
PropertyBuilder<TSequence> property = _entityBuilder.Property( propertyExpression );
string sequenceName = $"{property.Metadata.DeclaringEntityType.Relational().TableName}_{property.Metadata.Relational().ColumnName}_seq";
_modelBuilder.HasSequence( typeof( TSequence ), sequenceName, cfg
=> cfg
.HasMin( start )
.HasMax( end )
.IncrementsBy( increment )
.StartsAt( start )
.IsCyclic( false )
);
property
.ValueGeneratedOnAdd()
.HasDefaultValueSql( $"(nextval('\"{sequenceName}\"'))" );
return property;
}
}
public class TitleMapping : EntityTypeConfiguration<Title>
{
protected override void Map( EntityTypeBuilder<Title> builder )
{
builder.ToTable( nameof( Title ) );
builder.HasKey( t => t.ID );
MapToNpgsqlBigSequence( t => t.ID );
builder.HasOne( p => p.Created )
.WithOne( e => e.Title )
.HasForeignKey<TitleEventCreated>( e => e.TitleID )
.OnDelete( DeleteBehavior.Restrict );
builder.HasMany( t => t.MetadataVersions )
.WithOne( e => e.Title )
.HasForeignKey( e => e.TitleID )
.OnDelete( DeleteBehavior.Restrict );
}
}
public class EventMapping : EntityTypeConfiguration<Event>
{
protected override void Map( EntityTypeBuilder<Event> builder )
{
builder.ToTable( nameof( Event ) );
builder.HasKey( e => e.ID );
builder.HasIndex( e => new { e.EventType, e.ID } ).HasName( "IK_Event_EventType_ID" ).IsUnique();
builder.HasIndex( e => new { e.ID } ).IsUnique();
MapToNpgsqlBigSequence( e => e.ID );
builder.Property( e => e.Spawned )
.IsRequired();
}
}
public abstract class EventInheritorMapping<TConcreteEvent> : EntityTypeConfiguration<TConcreteEvent>
where TConcreteEvent: class, IConcreteEvent
{
protected sealed override void Map( EntityTypeBuilder<TConcreteEvent> builder )
{
string name = typeof( TConcreteEvent ).Name;
builder.ToTable( name );
builder.HasKey( e => e.ID );
builder.Ignore( e => e.Type );
builder.HasIndex( e => e.ID ).IsUnique();
builder.Property( e => e.ID )
.ValueGeneratedNever();
builder
.HasOne( e => e.Base )
.WithOne( EventNavigationProperty )
.HasPrincipalKey<Event>( e => e.ID )
.HasForeignKey<TConcreteEvent>( e => e.ID )
.OnDelete( DeleteBehavior.Restrict );
MapConcrete( builder );
}
protected abstract Expression<Func<Event, TConcreteEvent>> EventNavigationProperty { get; }
protected abstract void MapConcrete( EntityTypeBuilder<TConcreteEvent> builder );
}
public abstract class EventInheritorTitleContextualMapping<TEvent> : EventInheritorMapping<TEvent>
where TEvent : class, ITitleContextualEvent
{
protected sealed override void MapConcrete( EntityTypeBuilder<TEvent> builder )
{
if ( NavigationProperty != null )
builder.HasOne( e => e.Title )
.WithOne( NavigationProperty )
.HasForeignKey<TEvent>( e => e.TitleID )
.OnDelete( DeleteBehavior.Restrict );
else
builder.HasOne( e => e.Title )
.WithMany( NavigationCollection )
.HasForeignKey( e => e.TitleID )
.OnDelete( DeleteBehavior.Restrict );
MapContextual( builder );
}
protected abstract Expression<Func<Title, TEvent>> NavigationProperty { get; }
protected abstract Expression<Func<Title, IEnumerable<TEvent>>> NavigationCollection { get; }
protected abstract void MapContextual( EntityTypeBuilder<TEvent> builder );
}
public class TitleEventCreatedMapping : EventInheritorTitleContextualMapping<TitleEventCreated>
{
protected override Expression<Func<Event, TitleEventCreated>> EventNavigationProperty => @event => @event.TitleEventCreated;
protected override Expression<Func<Title, TitleEventCreated>> NavigationProperty => title => title.Created;
protected override Expression<Func<Title, IEnumerable<TitleEventCreated>>> NavigationCollection => throw new NotImplementedException();
protected override void MapContextual( EntityTypeBuilder<TitleEventCreated> builder )
{ }
}
Call Stack
pop()
// defined in public abstract class ContextBase<TContext> : DbContext where TContext: ContextBase<TContext>
private async Task<TEntity[]> SpawnImmutableOrVersionedEntityWithEvent<TEntity, TEntityInterface, TSubEvent>
(
EventType toBeSpawned,
Guid userId,
Func<TEntityInterface, TEntity> entityFactory,
Func<Event, TEntity, TSubEvent> eventFactory,
params TEntityInterface[] toBeCreated
)
where TEntity : class, TEntityInterface
where TSubEvent : class, IConcreteEvent
{
if ( toBeCreated.Length == 0 )
return Array.Empty<TEntity>();
TEntity[] entities = toBeCreated.Select( e => entityFactory( e ) ).ToArray();
AddSingleOrRange( entities, entities.Length );
(Event, TSubEvent)[] events = entities
.Select( entity => (entity, new Event( userId, toBeSpawned )) )
.Select( ( (TEntity, Event) t, int index ) => (t.Item2, eventFactory( t.Item2, t.Item1 )) )
.ToArray();
if ( events[ 0 ].Item2.Type != toBeSpawned )
throw new InvalidCastException( $"Can't cast TSubEvent ({ typeof( TSubEvent ).ToPrettyString() }) to { toBeSpawned }!" );
AddSingleOrRange( events.Select( e => e.Item1 ), events.Length );
AddSingleOrRange( events.Select( e => e.Item2 ), events.Length );
await SaveChangesAsync().ConfigureAwait( false ); // exception is thrown here
return entities;
void AddSingleOrRange<T>( IEnumerable<T> toAdd, int length ) where T : class
=> Set<T>().Then( set =>
{
if ( length > 1 )
set.AddRange( toAdd );
else
set.Add( toAdd.First() ); // to be able to benchmark AddRange vs multiple Add insertions
} );
}
pop()
// defined in the same class
private Task<Title[]> AnySpawnTitleCreatedAsync( Guid userId, params ITitle[] toBeCreated )
=> SpawnImmutableOrVersionedEntityWithEvent
(
EventType.TitleEventCreated,
userId,
mock => new Title( mock ),
( @event, entity ) => new TitleEventCreated( @event, entity ),
toBeCreated
);
pop()
// defined in the same class
public Task<Title> SpawnTitleCreatedAsync( Guid userId, ITitle toBeCreated )
=> AnySpawnTitleCreatedAsync( userId, toBeCreated )
.AwaitThen( titles => titles[ 0 ] ); // awaits the task and asynchronously returns results from applying func
// or
internal Task<Title[]> BulkSpawnTitleCreatedAsync( Guid userId, params ITitle[] toBeCreated )
=> AnySpawnTitleCreatedAsync( userId, toBeCreated ); // is here just for test. In real environment bulk spawning is rare
pop(); pop(); pop()
private Task<T[]> DoSafeAsync<T>( int counter, string template, Func<int, Task<T>[]> func )
=> DoSafeAsync( counter, template, n => func( n ).AsAsyncEnumerator().SelectAsync( a => a ).ToArray() ); // transform here makes thing similar to the iterating through Task<T> array with foreach, awaiting task and adding it to the resulting array
private Task<T> DoSafeAsync<T>( int counter, string template, Func<int, Task<T>> func )
=> return DoSafeAsync( String.Format( template, counter ), () => func( counter ) );
private async Task<T> DoSafeAsync<T>( string activityName /* use is amended */, Func<Task<T>> func )
{
try
{
return await func().ConfigureAwait( false );
}
catch ( Exception e ) when ( LogAndCatch( e ) )
{
return default( T );
}
bool LogAndCatch( Exception e )
{
LogManager.GetLogger( _logger.Name + ".exceptions" ).Log( LogLevel.Error, e );
_logger.Warn( "ERROR" );
return true;
}
}
pop()
IEnumerable<T> MakeN<T>( int count, Func<int, T> factory ) => Enumerable.Range( 1, count ).Select( factory );
T[] MakeNArray<T>( int count, Func<int, T> factory ) => MakeN( count, factory ).ToArray();
await DoSafeAsync( 7500, "insert {0} titles with AddRange", n =>
{
ITitle[] t = MakeNArray( n, id => new TitleModel( id ) ); // TitleModel is simpliest property bag implementing ITitle
return _context.BulkSpawnTitleCreatedAsync( _fakeUser, t );
} ).ConfigureAwait( false );
await DoSafeAsync( 750, "insert {0} titles with Add", n =>
MakeN( n, id => new TitleModel( id ) )
.Select( t => _context.SpawnTitleCreatedAsync( _fakeUser, t ) )
.ToArray()
).ConfigureAwait( false );
above gets called immedeately after context creation (the method accepts context) on app start.
Further technical details
EF Core version: 2.0.0-preview2-final
Database Provider: Npgsql.EntityFrameworkCore.PostgreSQL 2.0.0-preview2-final
Operating system: Windows 7
IDE: Visual Studio 2017 15.3.0 Preview 5.0