Skip to content

2.0.0-preview2-final AddRange success, but similar Add fails tith "The property on entity type has a temporary value while attempting to change the entity's state to 'Unchanged'" #9263

@bessgeor

Description

@bessgeor

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      pFad - Phonifier reborn

      Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

      Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


      Alternative Proxies:

      Alternative Proxy

      pFad Proxy

      pFad v3 Proxy

      pFad v4 Proxy