EFCore.BulkExtensions
EFCore.BulkExtensions copied to clipboard
BulkDelete attempts to read non-EF properties
Hi folks,
we have identified an issue with BulkExtensions which we cannot work properly around. It seems that bulk delete attempts to read non- EF properties. This is an issue for us because certain properties (navigation/ logical properties) cannot be read from a slim object which subsequently leads to a crash. We attempted to exclude these properties through a bulk configuration, but this just leads to an InvalidOperationException that the property is not in all-properties (which is correct).
Example: Lets say we have an alert which has none or multiple state transitions:
class Alert
{
IEnumerable<StateTransition> AlertStateTransitions {get;set;}
StateTransition LatestStateTransition => AlertStateTransitions.OrderBy(t=> CreatedOn).FirstOrDefault();
}
--> the property LatestStateTransition is calculated. In case we want to delete the alert, we use a slim object. these don't have/load navigation properties, i.e. AlertStateTransitions is null and leads to some exception (correctly).
The db context is properly understanding:
public void DbContext_Experiment()
{
var dbContext = GetService<MyDbContext>();
var entityType= dbContext.Model.FindEntityType(typeof(Alert));
var entityProperties = entityType.GetProperties();
entityProperties.Should().NotContain(ep => ep.Name == nameof(Alert.LatestStateTransition ), because: "this is a calculated property not visible to EF");
}
Excluding the property leads to an exception similar to this:

Not excluding leads to an exception similar to this:
(it seems that GenericsHelpers is operating on an invalid instance of the model (i.e. newly created, not passed by us) -so basically all properties are unpopulated (outside of the default constructor?).
The only mitigation we have in the moment is to not let any logical property run into any exception which ain't fine for the reason being that BulkExtension wants always to read them (on delete).
Is there another trick we can apply?
Aron
Can you write a test where the issue would be reproducible.
I am not sure how to write a unit test for this because the existing ones are too complicated to enter quickly. But I think I can call out the issue in code:
When we use BulkDelete (only?), it arrives sooner or later in TableInfo.cs:210
var allProperties = new List<IProperty>();
foreach (var entityProperty in entityType.GetProperties())
This seems to define the all properties from which exclusions can happen. It is derived from context.Model.FindEntityType(type) which is ok in my view. It makes sense to care only about properties which EF is also caring about.
The processing down the line arrives at some point on GenericsHelpers.GetPropertiesDefaultValue which however ignores the db context/ model and subsequently all properties seen in line 16:
var arrayPropertyInfos = type.GetProperties();
var result = new List<string>();
This is pure dotnet reflection on public-instance properties. For the same reason, it can easily decorrelate with all-properties, for instance, if a public property is ignored in the model. This is where it goes wrong in my view. It should be referring to the same list of properties, i.e. the model.
After having found that, I could mitigate the issue by changing the visibility of our property to internal (luckily it was falsely public). Because it became internal, GenericsHelpers stopped to see it and all went fine.
I've made some change that should fix this, sent tableInfo arg to GetPropertiesDefaultValue
if (!tableInfo.PropertyColumnNamesDict.ContainsKey(name)) { continue; } // skip non-EF properties
It is Commited, but new nuget not yet published. Can you download the latest source into your project and try it to check if it's fixed.
Hi Boris,
doesn't work; it seems the check is too late; field.GetValue is called before the tableInfo is checked:
if (field.GetIndexParameters().Any()) // Skip Indexer: public string this[string pPropertyName] => string.Empty;
{
continue;
}
var name = field.Name;
**var temp = field.GetValue(value);**
object? defaultValue = null;
if (!tableInfo.PropertyColumnNamesDict.ContainsKey(name)) // skip non-EF properties
{
continue;
}
Cheers, Aron
Fixed with latest v.