Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

InvalidOperationException thrown when saving changes: 'The value of xxx is unknown when attempting to save changes'. Regresion between 3.1 and 5.0 #23507

Closed
jotatoledo opened this issue Nov 26, 2020 · 4 comments

Comments

@jotatoledo
Copy link

jotatoledo commented Nov 26, 2020

The following code worked with Microsoft.EntityFrameworkCore 3.1.10 and Pomelo.EntityFrameworkCore.MySql 3.2.4. After updating to Microsoft.EntityFrameworkCore 5.0.0 and Pomelo.EntityFrameworkCore.MySql 5.0.0-alpha.2 InvalidOperationException is thrown.

Seems related to #20002 and #21206

// 'model' here is of type ProfileForm
var profile = new UserProfile
{
	Id = id
};
this.dbContext.Add(profile);
this.dbContext.Entry(profile).CurrentValues.SetValues(model); 
var clientCulture = this.GetClientCulture();
var searchintTopics = await this.FindTopicsAsync(model.Searching, clientCulture);
var offeringTopics = await this.FindTopicsAsync(model.Offering, clientCulture);
profile.UpdateOfferingForCulture(offeringTopics, clientCulture)
	.UpdateSearchingForCulture(searchintTopics, clientCulture);
// saving will throw
await this.dbContext.SaveChangesAsync();

private async Task<IList<InterestTopic>> FindTopicsAsync(IEnumerable<int> topicIds, CultureInfo clientCulture)
{
	var cultureName = clientCulture.Name;
	var rawResult = await this.dbContext
		.InterestTopics
		.Where(t => topicIds.Contains(t.Id))
		.ToListAsync();
	// NOTE: currently not possible to translate this into SQL syntax, therefore we do it in memory
	// See https://github.com/dotnet/efcore/issues/10434
	return rawResult.Where(t => t.SpecificCulture == null
		|| t.SpecificCulture == cultureName)
		.ToList();
}

public class ProfileForm
{
	public bool IsCompart { get; set; }

	public List<int> Offering { get; set; } = default!;

	public List<int> Searching { get; set; } = default!;

	public string? Company { get; set; }

	public string? Job { get; set; }

	public string? Bio { get; set; }

	public string? LinkedInUrl { get; set; }
}

The data model:

public class ApplicationDbContext : DbContext
{
	public virtual DbSet<UserProfile> UserProfiles => Set<UserProfile>();

	public virtual DbSet<InterestTopic>  InterestTopics => Set<InterestTopic>();

	public virtual DbSet<UserInterestOffering> UserInterestOfferings => Set<UserInterestOffering>();

	public virtual DbSet<UserInterestSearching> UserInterestSearchings => Set<UserInterestSearching>();

	public ApplicationDbContext(DbContextOptions options) : base(options)
	{
	}

	protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
	{
		base.OnConfiguring(optionsBuilder);
		optionsBuilder.UseLazyLoadingProxies();
	}

	protected override void OnModelCreating(ModelBuilder builder)
	{
		base.OnModelCreating(builder);
		builder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
	}
}

public class DataConstants
{
	public const string PoidName = "Poid";
	public const string CreatedAtStampName = "CreatedAt";
}

public class UserProfileEntityTypeConfiguration : IEntityTypeConfiguration<UserProfile>
{
	public void Configure(EntityTypeBuilder<UserProfile> builder)
	{
		builder.Property<int>(DataConstants.PoidName);
		builder.Property<DateTime>(DataConstants.CreatedAtStampName)
			.HasColumnType("DATETIME")
			.HasDefaultValueSql("CURRENT_TIMESTAMP");
		builder.Property(t => t.Id)
			.IsRequired();
		builder.Property(t => t.Company)
			.IsRequired(false)
			.HasMaxLength(120);
		builder.Property(t => t.Job)
			.IsRequired(false)
			.HasMaxLength(100);
		builder.Property(t => t.Bio)
			.IsRequired(false)
			.HasMaxLength(500);
		builder.Property(t => t.IsCompart)
			.IsRequired();
		builder.Property(t => t.LinkedInUrl)
			.IsRequired(false)
			.HasMaxLength(50)
			.StoreEmptyAsNull();

		builder.HasKey(DataConstants.PoidName);
		builder.HasIndex(t => t.Id);
	}
}

public class UserInterestSearchingEntityTypeConfiguration : IEntityTypeConfiguration<UserInterestSearching>
{
	public static readonly string TopicFkName = $"{nameof(InterestTopic)}{nameof(InterestTopic.Id)}";
	public static readonly string UserFkName = $"{nameof(UserProfile)}{DataConstants.PoidName}";

	public void Configure(EntityTypeBuilder<UserInterestSearching> builder)
	{
		builder.Property<int>(UserFkName);
		builder.Property<int>(TopicFkName);

		builder.HasOne(t => t.User)
			.WithMany(t => t.Searching)
			.HasForeignKey(UserFkName);
		builder.HasOne(t => t.Topic)
			.WithMany(t => t.AsSearch)
			.HasForeignKey(TopicFkName);

		builder.HasKey(UserFkName, TopicFkName);
	}
}

public class UserInterestOfferingEntityTypeConfiguration : IEntityTypeConfiguration<UserInterestOffering>
{
	public static readonly string TopicFkName = $"{nameof(InterestTopic)}{nameof(InterestTopic.Id)}";
	public static readonly string UserFkName = $"{nameof(UserProfile)}{DataConstants.PoidName}";

	public void Configure(EntityTypeBuilder<UserInterestOffering> builder)
	{
		builder.Property<int>(UserFkName);
		builder.Property<int>(TopicFkName);

		builder.HasOne(t => t.User)
			.WithMany(t => t.Offering)
			.HasForeignKey(UserFkName);
		builder.HasOne(t => t.Topic)
			.WithMany(t => t.AsOffer)
			.HasForeignKey(TopicFkName);

		builder.HasKey(UserFkName, TopicFkName);
	}
}

public class InterestTopicEntityTypeConfiguration : IEntityTypeConfiguration<InterestTopic>
{
	public void Configure(EntityTypeBuilder<InterestTopic> builder)
	{
		builder.Property(t => t.Id)
			.IsRequired()
			.ValueGeneratedNever();
		builder.Property(t => t.DisplayName)
			.IsRequired()
			.HasMaxLength(100);
		builder.Property(t => t.SpecificCulture)
			.IsRequired(false)
			// BCP-47 language tag seem to be up to 35 chars. Most likely wont be rquired, but better be safe
			// See https://stackoverflow.com/a/17863380/5394220
			.HasMaxLength(40);
		builder.HasKey(t => t.Id);
		builder.HasIndex(t => new { t.DisplayName, t.SpecificCulture });
	}
}

The entity types:

public class UserInterestOffering
{
	public virtual UserProfile User { get; set; } = default!;

	public virtual InterestTopic Topic { get; set; } = default!;
}

public class UserInterestSearching
{
	public virtual UserProfile User { get; set; } = default!;

	public virtual InterestTopic Topic { get; set; } = default!;
}

public class InterestTopic
{
	public int Id { get; set; }

	public string DisplayName { get; set; } = default!;

	public string? SpecificCulture { get; set; }

	public virtual ICollection<UserInterestOffering> AsOffer { get; set; } = default!;

	public virtual ICollection<UserInterestSearching> AsSearch { get; set; } = default!;

	public bool IsForCulture(CultureInfo cultureInfo)
	{
		return this.SpecificCulture == null
			|| this.SpecificCulture == cultureInfo.Name;
	}
}

public class UserProfile
{
	public UserProfile()
	{
		this.Offering = new HashSet<UserInterestOffering>();
		this.Searching = new HashSet<UserInterestSearching>();
	}

	public Guid Id { get; set; }

	public bool IsCompart { get; set; }

	public string? Company { get; set; }

	public string? Job { get; set; }

	public string? Bio { get; set; }

	public string? LinkedInUrl { get; set; }

	public virtual ICollection<UserInterestOffering> Offering { get; set; }

	public virtual ICollection<UserInterestSearching> Searching { get; set; }

	public UserProfile UpdateOfferingForCulture(IEnumerable<InterestTopic> values, CultureInfo cultureToUpdate)
	{
		if(values.Any(v => !v.IsForCulture(cultureToUpdate)))
		{
			throw new InvalidOperationException(string.Format("At least one element in '{0}' is not for '{1}'", nameof(values), cultureToUpdate));
		}

		var otherCultureTopics = this.Offering
			   .Where(s => !s.Topic.IsForCulture(cultureToUpdate))
			   .ToList();
		var newTopics = values
			.Select(val => new UserInterestOffering { Topic = val, User = this })
			.ToList();
		this.Offering.Clear();
		this.Offering = otherCultureTopics.Concat(newTopics).ToList();
		return this;
	}

	public UserProfile UpdateSearchingForCulture(IEnumerable<InterestTopic> values, CultureInfo cultureToUpdate)
	{
		if (values.Any(v => !v.IsForCulture(cultureToUpdate)))
		{
			throw new InvalidOperationException(string.Format("At least one element in '{0}' is not for '{1}'", nameof(values), cultureToUpdate));
		}

		var otherCultureTopics = this.Searching
			.Where(s => !s.Topic.IsForCulture(cultureToUpdate))
			.ToList();
		var newTopics = values
			.Select(val => new UserInterestSearching { Topic = val, User = this })
			.ToList();
		this.Searching.Clear();
		this.Searching = otherCultureTopics.Concat(newTopics).ToList();
		return this;
	}
}

The full stack trace:

System.InvalidOperationException: The value of 'UserInterestOffering.InterestTopicId' is unknown when attempting to save changes. This is because the property is also part of a foreign key for which the principal entity in the relationship is not known.
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.PrepareToSave()
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.GetEntriesToSave(Boolean cascadeChanges)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(DbContext _, Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
   at Pomelo.EntityFrameworkCore.MySql.Storage.Internal.MySqlExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
   at SAI.InterestNetwork.Web.Features.Profile.ProfileController.CreateProfileAsync(ProfileForm model) in C:\Users\jtoled\source\repos\SAI.InterestNetwork\SAI.InterestNetwork.Web\Features\Profile\ProfileController.cs:line 98
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Localization.RequestLocalizationMiddleware.Invoke(HttpContext context)
   at Serilog.AspNetCore.RequestLoggingMiddleware.Invoke(HttpContext httpContext)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

Environment

EF Core version: 5.0.0
Database provider: Pomelo.EntityFrameworkCore.MySql 5.0.0-alpha.2
Target framework: netcoreapp3.1
Operating system: win10-x64
IDE: Visual Studio 2019 Version 16.8.2

@ajcvickers
Copy link
Member

@jotatoledo Unfortunately, there is too much missing code here for me to be able to run this on my machine in order to reproduce and investigate the issue. Please attach a small, runnable project or post a small, runnable code listing that reproduces what you are seeing so that we can investigate.

@jotatoledo
Copy link
Author

@ajcvickers thanks for the reply. Ill setup a minimal repro and notify you

@ajcvickers
Copy link
Member

EF Team Triage: Closing this issue as the requested additional details have not been provided and we have been unable to reproduce it.

BTW this is a canned response and may have info or details that do not directly apply to this particular issue. While we'd like to spend the time to uniquely address every incoming issue, we get a lot traffic on the EF projects and that is not practical. To ensure we maximize the time we have to work on fixing bugs, implementing new features, etc. we use canned responses for common triage decisions.

@yitzchokneuhaus1
Copy link

@jotatoledo Were you able to figure anything out about this issue, We are currently having the same issue while upgrading to EF Core 5

@ajcvickers ajcvickers reopened this Oct 16, 2022
@ajcvickers ajcvickers closed this as not planned Won't fix, can't repro, duplicate, stale Oct 16, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants