Closed Mike-E-angelo closed 5 months ago
Thanks for reporting the issue. Can you provide a (small) sample project that I can use to reproduce the error? That would help me a lot.
Ah I wish I could, but it would take some time with my code/model. The best I can do at the moment is provide the model and configuration:
public sealed class DisbursementDetails
{
public Guid Id { get; set; }
public bool Enabled { get; set; } = true;
public DisbursementBeneficiaryAccount Owner { get; set; } = default!;
public CreationReason Reason { get; set; } = default!;
public DateTimeOffset? Acknowledged { get; set; }
public DateTimeOffset? Notified { get; set; }
// ReSharper disable once EntityFramework.ModelValidation.UnlimitedStringLength // TODO
public string? Comments { get; set; }
}
public abstract class CreationReason
{
public Guid Id { get; set; }
// ReSharper disable once EntityFramework.ModelValidation.UnlimitedStringLength
public string? Notes { get; set; }
}
public abstract class SystemCreationReason : CreationReason;
public sealed class CurrentUserReason : SystemCreationReason;
public sealed class CurrentUserNameReason : SystemCreationReason;
sealed class AccountingConfigurations : ICommand<ModelBuilder>
{
public static AccountingConfigurations Default { get; } = new();
AccountingConfigurations() {}
public void Execute(ModelBuilder parameter)
{
parameter.Entity<SystemCreationReason>();
parameter.Entity<CurrentUserReason>();
parameter.Entity<CurrentUserNameReason>();
parameter.Entity<DisbursementDetails>(x => x.HasOne(y => y.Reason)
.WithOne()
.HasForeignKey<CreationReason>("DisbursementAccountId")
.OnDelete(DeleteBehavior.Cascade));
}
}
Awesome! Thank you for your efforts here @Giorgi πππ
@Mike-E-angelo It's now live on NuGet.
Looks like it's working now, I see your interceptor in a test exception that I have thrown:
---> EntityFramework.Exceptions.Common.MaxLengthExceededException: Maximum length exceeded
---> Microsoft.Data.SqlClient.SqlException (0x80131904): String or binary data would be truncated in table 'starbeam.dbo.Vendor', column 'Identifier'. Truncated value: 'asdfasdf'.
at void Microsoft.Data.SqlClient.SqlConnection.OnError(SqlException exception, bool breakConnection, Action<Action> wrapCloseInAction)
at void Microsoft.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, bool breakConnection, Action<Action> wrapCloseInAction)
at void Microsoft.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, bool callerHasConnectionLock, bool asyncClose)
at bool Microsoft.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, out bool dataReady)
at bool Microsoft.Data.SqlClient.SqlDataReader.TryHasMoreRows(out bool moreRows)
at bool Microsoft.Data.SqlClient.SqlDataReader.TryHasMoreResults(out bool moreResults)
at bool Microsoft.Data.SqlClient.SqlDataReader.TryNextResult(out bool more)
at Task<bool> Microsoft.Data.SqlClient.SqlDataReader.NextResultAsyncExecute(Task task, object state)
at Task<T> Microsoft.Data.SqlClient.SqlDataReader.InvokeAsyncCall<T>(SqlDataReaderBaseAsyncCallContext<T> context)
at async Task Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ConsumeAsync(RelationalDataReader reader, CancellationToken cancellationToken)
ClientConnectionId:...
Error Number:2628,State:1,Class:16
--- End of inner exception stack trace ---
at Task EntityFramework.Exceptions.Common.ExceptionProcessorInterceptor<T>.SaveChangesFailedAsync(DbContextErrorEventData eventData, CancellationToken cancellationToken)
at Task Microsoft.EntityFrameworkCore.Diagnostics.CoreLoggerExtensions.SaveChangesFailedAsync(IDiagnosticsLogger<Update> diagnostics, DbContext context, Exception exception, CancellationToken
Looks good. π Thank you once again. π
The indexes
dictionary is populated first time unique constraint is violated, so if you tested with max length exceeded error, the dictionary would not be populated. You need to cause unique constraint violation to validate the fix.
Ah! Good point. Glad I checked in with you on that. :)
Now we have a new problem. π
Microsoft.Azure.WebJobs.Host.FunctionInvocationException: Exception while executing function: starbeam-one-Authentications
---> System.ArgumentException: An item with the same key has already been added. Key: IX_ExternalProcess_ResultId
at bool System.Collections.Generic.Dictionary<TKey, TValue>.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
at void System.Collections.Generic.Dictionary<TKey, TValue>.Add(TKey key, TValue value)
at Dictionary<TKey, TElement> System.Linq.Enumerable.ToDictionary<TSource, TKey, TElement>(IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector, IEqualityComparer<TKey> comparer) x 2
at void EntityFramework.Exceptions.Common.ExceptionProcessorInterceptor<T>.SetConstraintDetails(DbContext context, UniqueConstraintException exception, Exception providerException)
at Task EntityFramework.Exceptions.Common.ExceptionProcessorInterceptor<T>.SaveChangesFailedAsync(DbContextErrorEventData eventData, CancellationToken cancellationToken)
at Task Microsoft.EntityFrameworkCore.Diagnostics.CoreLoggerExtensions.SaveChangesFailedAsync(IDiagnosticsLogger<Update> diagnostics, DbContext context, Exception exception, CancellationToken cancellationToken)
at async Task<int> Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken)
I've emitted the pertinent details below in the debugger:
Model:
public abstract class ExternalProcess
{
public Guid Id { get; set; }
public bool Enabled { get; set; } = true;
public DateTimeOffset Created { get; set; }
public DateTimeOffset? Completed { get; set; }
public ICollection<CompletedStep> CompletedSteps { get; init; } = default!;
public ICollection<ProcessUpdate> Updates { get; set; } = default!;
public ProcessState State { get; set; } = default!;
}
public sealed class DepositOrder : ExternalProcess
{
public Checkout Subject { get; set; } = default!;
public Deposit? Result { get; set; }
}
Configuration:
parameter.Entity<DepositOrder>(x =>
{
x.HasOne(y => y.Subject)
.WithOne()
.HasForeignKey<DepositOrder>()
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
x.HasOne(y => y.Result)
.WithOne(y => y.Order)
.HasForeignKey<DepositOrder>();
x.HasIndex("ResultId").HasFilter("[DepositOrder_ResultId] IS NOT NULL");
x.HasIndex("SubjectId").HasFilter("[DepositOrder_SubjectId] IS NOT NULL");
});
When I try to run app with this model I get this error on line x.HasIndex("ResultId").HasFilter("[DepositOrder_ResultId] IS NOT NULL");
System.InvalidOperationException: 'The property 'ResultId' cannot be added to the type 'DepositOrder' because no property type was specified and there is no corresponding CLR property or field. To add a shadow state property, the property type must be specified.'
Ah, maybe you need the Deposit
entity? π€
public class Deposit : Credit
{
public DepositOrder Order { get; set; } = default!;
}
public abstract class Credit : Transaction;
[Index(nameof(Created))]
public abstract class Transaction
{
public Guid Id { get; set; }
public DateTimeOffset Created { get; set; }
}
Same error.
I think you should have ResultId
and SubjectId
in your model otherwise EF can't know what type they should be in the database.
That's weird @Giorgi I will investigate further. EF knows the type because of the Id
property of the respective types, and uses these shadow properties accordingly.
One thing I thought of without looking into this is that you should make sure the base tables are defined as entities. I use TBH:
parameter.Entity<ExternalProcess>();
parameter.Entity<Transaction>();
That's right, the exception is gone, but I don't get the exception you are encountering:
Ah! OK now add the following:
public abstract class PurchaseOrder : ExternalProcess
{
public MarketplaceTransaction? Result { get; set; }
}
public sealed class SaleOrder : PurchaseOrder;
public sealed class MarketplaceTransaction
{
public Guid Id { get; init; }
public DateTimeOffset Created { get; init; }
public decimal Value { get; init; }
}
Configuration:
parameter.Entity<PurchaseOrder>();
That should be enough to have two entities with two indexes resulting in the same name. If not I can look into this further when I have some more time here. I appreciate your patience in walking through this with me. π
Same. Can you provide a Minimal reproducible example?
Sure... are you able to provide what you have so far as a starting point by chance? It would greatly assist me. π
I have whatever you pasted here today.
OK I have a failing test for you here @Giorgi ... needed to add one more entity with a Result
property. I have 4 of them in my model. :D
https://github.com/DragonSpark/Framework/commit/c58bf02725ca18c02ae91310d8a80b50b85e1fdc
Thanks @Mike-E-angelo I think I fixed the issue finally. I'm now able to get the real names and it should avoid duplicate keys.
@NickStrupat You can check commit d280318a9f37830099c989b5ff317c2eeec210eb to see how to retrieve the real names.
@Mike-E-angelo The fixed version is on the NuGet now.
Hooray! I can confirm this is working as expected now:
---> EntityFramework.Exceptions.Common.UniqueConstraintException: Unique constraint violation
---> Microsoft.Data.SqlClient.SqlException (0x80131904): Cannot insert duplicate key row in object 'dbo.Authenticator' with unique index 'IX_Authenticator_Identifier'. The duplicate key value is (twitter).
The statement has been terminated.
at Task<DbDataReader> Microsoft.Data.SqlClient.SqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)+(Task<SqlDataReader> result) => { }
at void System.Threading.Tasks.ContinuationResultTaskFromResultTask<TAntecedentResult, TResult>.InnerInvoke()
at async Task<RelationalDataReader> Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken) x 2
at async Task Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
ClientConnectionId:...
Error Number:2601,State:1,Class:14
--- End of inner exception stack trace ---
at Task EntityFramework.Exceptions.Common.ExceptionProcessorInterceptor<T>.SaveChangesFailedAsync(DbContextErrorEventData eventData, CancellationToken cancellationToken)
at Task Microsoft.EntityFrameworkCore.Diagnostics.CoreLoggerExtensions.SaveChangesFailedAsync(IDiagnosticsLogger<Update> diagnostics, DbContext context, Exception exception, CancellationToken cancellationToken)
at async Task<int> Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken)
Thank you again for all your efforts out there! πππ
Thanks for testing it so thoroughly! In this case, the Constraint
property of UniqueConstraintException
should be IX_Authenticator_Identifier
if you need that in your code.
This error also occurs when there are multiple tables with the same name and indexes in separate SQL Server schemas.
I have a table [inventory].[Category]
with index IX_Category_Name
and a table [incidents].[Category]
with index IX_Category_Name
.
This is valid in SQL Server because index names are scoped to their associated tables.
Can you upload a repro project? Just the DbContext code is enough.
I've created a minimal example here: https://github.com/zefubachs/EFExceptionSchema
I'll have a look at it in a couple of days.
@zefubachs This is now fixed in the latest commit.
Hi, I have the same problem with EntityFrameworkCore.Exceptions.PostgreSQL 8.1.2 but with foreign key !!
This line : (in ExceptionProcessorInterceptor, line 118)
foreignKeys = mappedConstraints.ToDictionary(arg => arg.constraint.Name, arg => arg.Properties);
generate this error :
System.ArgumentException: An item with the same key has already been added. Key: fk_montants_pieces_pieces_piece_entreprise_id_piece_id
at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable`1 source, Func`2 keySelector, Func`2 elementSelector, IEqualityComparer`1 comparer)
at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable`1 source, Func`2 keySelector, Func`2 elementSelector)
at EntityFramework.Exceptions.Common.ExceptionProcessorInterceptor`1.SetConstraintDetails(DbContext context, ReferenceConstraintException exception, Exception providerException)
at EntityFramework.Exceptions.Common.ExceptionProcessorInterceptor`1.SaveChangesFailedAsync(DbContextErrorEventData eventData, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Diagnostics.CoreLoggerExtensions.SaveChangesFailedAsync(IDiagnosticsLogger`1 diagnostics, DbContext context, Exception exception, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
I have two foreign key with the same constraint name. (efcore generate this case when using inheritance)
Could you check and apply same fix to foreign key and generate a new nuget package ? EntityFrameworkCore.Exceptions.PostgreSQL version 8.1.2 does not have this commit.
Thanks.
@Eric-Ans Can you share the context and configuration that causes this error?
@Giorgi https://github.com/Eric-Ans/TestEntityFrameworkExceptions
This sample throw the error I describe previously when context.SaveChanges(); is called in main.
If you look at file : 20240731133414_InitialCreate.Designer.cs: you will see two entries with the same foreign key name : HasConstraintName("fk_montants_pieces_piece_id");
and in ExceptionProcessorInterceptor class line 118, mappedConstraints contains the same two constraint name, so ToDictionary fail.
Hello,
Thank you so much for your efforts on this project. π
I seem to be experiencing an exception with this line here:
https://github.com/Giorgi/EntityFramework.Exceptions/blob/main/EntityFramework.Exceptions.Common/ExceptionProcessorInterceptor.cs#L94
In my case I have an abstract class that serves as the table for the hierarchy of implementations. It appears it's going through each of the implementations and repeating the abstract class as the index owner, as seen here in the results from the debugger:
This results in the following exception:
Please let me know if there is any further information I can provide and I will assist.