- Home
- LLBLGen Pro
- Bugs & Issues
DateTime? and IsChanged property with null values
Joined: 11-Sep-2008
My Environment: LLBLGen: 5.11.1 - 19-Dec-2023 (I checked the log and there didn't appear to be a fix to the issue I 'think' I see) Runtime: 5.11.0.0 No exception, just unexpected behavior Adapter Template with .NET 8 (also appears in 6) Azure SQL DB (though this concern is in code, after Entity Initialization and setting fields in memory)
The story: I'm looking to evaluate the IsChanged property after assigning some values from a dto. I have a new entity that I initialize. I then take a dto and copy the value from its DateTime? field to the entity's DateTime? field. In the case where the dto field is null, I would expect that when the code sets the entity field to null, that IsChanged would remain false. However, I am getting IsChanged == true.
I wrote a unit test to show that this behavior works as I am expecting with a string field, but not with a DateTime? field.
[Fact]
public void NullableDateTimeIsChangedWorks()
{
var facilityConfig = new FacilityConfigEntity {};
var fieldIndexToUpdate = (int)FacilityConfigFieldIndex.UpdateBy; // USING A STRING FIELD
Assert.Null(facilityConfig.Fields[fieldIndexToUpdate].DbValue);
Assert.Null(facilityConfig.Fields[fieldIndexToUpdate].CurrentValue);
Assert.False(facilityConfig.Fields[fieldIndexToUpdate].IsChanged);
facilityConfig.UpdateBy = null; // SET STRING TO NULL
Assert.Null(facilityConfig.Fields[fieldIndexToUpdate].DbValue);
Assert.Null(facilityConfig.Fields[fieldIndexToUpdate].CurrentValue);
Assert.False(facilityConfig.Fields[fieldIndexToUpdate].IsChanged); // PASSES!
fieldIndexToUpdate = (int)FacilityConfigFieldIndex.EndDate; // USING NULLABLE DATETIME FIELD
Assert.Null(facilityConfig.Fields[fieldIndexToUpdate].DbValue);
Assert.Null(facilityConfig.Fields[fieldIndexToUpdate].CurrentValue);
Assert.False(facilityConfig.Fields[fieldIndexToUpdate].IsChanged);
facilityConfig.EndDate = null; // SET DATETIME? TO NULL
Assert.Null(facilityConfig.Fields[fieldIndexToUpdate].DbValue);
Assert.Null(facilityConfig.Fields[fieldIndexToUpdate].CurrentValue);
Assert.False(facilityConfig.Fields[fieldIndexToUpdate].IsChanged); // FAILS!
}
In the code above, the IsChanged is still false after setting the value to null (as expected). However, for the DateTime? it fails. I would expect the same behavior as the string.
In reality, we are trying to update entities that are fetched from the database, but I thought this simplified the reproduction of the problem just using a new entity. Of course, the same problem occurs when we do that.
Looking at this thread: https://www.llblgen.com/tinyforum/Thread/16049/1 I think my expectations are right that when setting the DateTime? field to a null value, that IsChanged should remain false just like the string field does.
Joined: 17-Aug-2003
I can't reproduce the difference between string and datetime: both change
This is on Northwind, Customer.City is a nullable string field. Order.OrderDate is a nullable DateTime. I think I've rebuilt your test correctly? If not, please correct me. I've also added checking for IsNull which are false, because they check on the DB Value and on a new entity there are no db values so they report false.
var customer = new CustomerEntity { };
Assert.AreEqual(string.Empty, customer.City);
Assert.IsNull(customer.Fields[(int)CustomerFieldIndex.City].DbValue);
Assert.IsNull(customer.Fields[(int)CustomerFieldIndex.City].CurrentValue);
Assert.IsNull(customer.Fields.GetDbValue((int)CustomerFieldIndex.City));
Assert.IsNull(customer.Fields.GetCurrentValue((int)CustomerFieldIndex.City));
Assert.IsFalse(customer.Fields[(int)CustomerFieldIndex.City].IsChanged);
Assert.IsFalse(customer.Fields.GetIsNull((int)CustomerFieldIndex.City));
Assert.IsFalse(customer.Fields[(int)CustomerFieldIndex.City].IsNull);
customer.City = null;
Assert.AreEqual(string.Empty, customer.City);
Assert.IsNull(customer.Fields[(int)CustomerFieldIndex.City].DbValue);
Assert.IsNull(customer.Fields[(int)CustomerFieldIndex.City].CurrentValue);
Assert.IsNull(customer.Fields.GetDbValue((int)CustomerFieldIndex.City));
Assert.IsNull(customer.Fields.GetCurrentValue((int)CustomerFieldIndex.City));
Assert.IsTrue(customer.Fields[(int)CustomerFieldIndex.City].IsChanged); // SUCCEEDS
Assert.IsFalse(customer.Fields.GetIsNull((int)CustomerFieldIndex.City)); // because no db data is present
Assert.IsFalse(customer.Fields[(int)CustomerFieldIndex.City].IsNull); // because no db data is present
var order = new OrderEntity { };
Assert.IsNull(order.OrderDate);
Assert.IsNull(order.Fields[(int)OrderFieldIndex.OrderDate].DbValue);
Assert.IsNull(order.Fields[(int)OrderFieldIndex.OrderDate].CurrentValue);
Assert.IsNull(order.Fields.GetDbValue((int)OrderFieldIndex.OrderDate));
Assert.IsNull(order.Fields.GetCurrentValue((int)OrderFieldIndex.OrderDate));
Assert.IsFalse(order.Fields[(int)OrderFieldIndex.OrderDate].IsChanged);
Assert.IsFalse(order.Fields.GetIsNull((int)OrderFieldIndex.OrderDate));
Assert.IsFalse(order.Fields[(int)OrderFieldIndex.OrderDate].IsNull);
order.OrderDate = null;
Assert.IsNull(order.OrderDate);
Assert.IsNull(order.Fields[(int)OrderFieldIndex.OrderDate].DbValue);
Assert.IsNull(order.Fields[(int)OrderFieldIndex.OrderDate].CurrentValue);
Assert.IsNull(order.Fields.GetDbValue((int)OrderFieldIndex.OrderDate));
Assert.IsNull(order.Fields.GetCurrentValue((int)OrderFieldIndex.OrderDate));
Assert.IsTrue(order.Fields[(int)OrderFieldIndex.OrderDate].IsChanged); // SUCCEEDS
Assert.IsFalse(order.Fields.GetIsNull((int)OrderFieldIndex.OrderDate)); // because no db data is present
Assert.IsFalse(order.Fields[(int)OrderFieldIndex.OrderDate].IsNull); // because no db data is present
The reason this enforces the field to be marked changed is that null is a valid value for the fields, as they're nullable. So we do mark them as changed. This is done to make sure they're updated in save (as you might want to update an entity which currently holds a value for the field and now receives 'NULL' for it so a default constraint might run). As the initial state of the field is null not changed, it won't be persisted in an update so we mark it as changed. The practical value the field has (null) didn't effectively change, but the initial state of the field is really 'undefined', not 'null': if you see the initial entity state as undefined, it might be more clear why setting a nullable field to null in that case marks the field as changed.
Why your string field isn't marked as changed however is weird. I can't reproduce that.
Joined: 19-Nov-2024
Assert.IsTrue(order.Fields[(int)OrderFieldIndex.OrderDate].IsChanged); // SUCCEEDS <-- this seems like a bug
Joined: 17-Aug-2003
thargenediad wrote:
Assert.IsTrue(order.Fields[(int)OrderFieldIndex.OrderDate].IsChanged); // SUCCEEDS <-- this seems like a bug
No, as I explained above: the field has changed from undefined to null, as null is a valid value and by having it marked as changed it's included in an update statement. The field isn't seen as having a null value in a new entity, mind you.
Also, the string field in your test behaves differently than in mine, so something's not the same in my test wrt that field compared to yours.
Joined: 11-Sep-2008
Hi Frans, I see what you mean. Yes, I think you reproduced my test correctly; I think the problem is that I thought the new (unfetched) entity had the same behavior as a fetched entity. If so, sorry about that confusion. I was hoping to simplify my test without having to save because my test entity had a lot of dependencies.
I wrote a new test with a different entity, but same concept, with an entity that didn't have as many dependencies (I excluded the string field because I think I have a separate issue going on there since we're using a string type converter; I can either create a separate post for that, or we can move onto it once I determine what to expect with my DateTime? field):
The story here is that I am initializing an entity, immediately saving to the database (with a refetch).
The DueDate field is nullable and after refetch is still null.
I set the DueDate to null, and IsChanged becomes true.
Is that still expected here?
EDIT Added a comment in the last line of code below. EDIT
[Fact]
public void NullableDateTimeIsChangedWorks()
{
var instrument = new IngestInstrumentEntity { };
using DataAccessAdapter adapter = new DataAccessAdapter();
adapter.SaveEntity(instrument, true);
var dueDateField = instrument.Fields[(int)IngestInstrumentFieldIndex.DueDate];
Assert.Null(dueDateField.DbValue);
Assert.Null(dueDateField.CurrentValue);
Assert.False(dueDateField.IsChanged);
instrument.DueDate = null;
Assert.Null(dueDateField.DbValue);
Assert.Null(dueDateField.CurrentValue);
Assert.False(dueDateField.IsChanged); // THIS FAILS. Expecting IsChanged == false since the fetched value was already null.
}
Note: I'm not sure if it makes a difference, but this field is a DateTime2(3) datatype in the database.
Joined: 17-Aug-2003
I can't reproduce it. (with 5.11.4. You could pull the package from nuget and see if it makes a difference, but as you said earlier, there haven't been fixes in this area)
[Test]
public void NullCheckOnRefetchedNullableField()
{
using(var adapter = new DataAccessAdapter())
{
var newCustomer = EntityCreator.CreateNewCustomer(1);
newCustomer.TestRunId = _testRunID;
AddressEntity newAddress = EntityCreator.CreateNewAddress(1);
newAddress.TestRunId = _testRunID;
newCustomer.VisitingAddress = newAddress;
newCustomer.BillingAddress = newAddress;
var newOrder = new OrderEntity();
newOrder.OrderDate = DateTime.Now;
newOrder.Customer = newCustomer;
newOrder.TestRunId = _testRunID;
bool result = adapter.SaveEntity(newCustomer, true);
Assert.AreEqual(true, result);
Assert.AreEqual(EntityState.Fetched, newOrder.Fields.State);
var shippedDateField = newOrder.Fields[(int)OrderFieldIndex.ShippedDate];
Assert.Null(shippedDateField.DbValue);
Assert.Null(shippedDateField.CurrentValue);
Assert.False(shippedDateField.IsChanged);
newOrder.ShippedDate = null;
Assert.Null(shippedDateField.DbValue);
Assert.Null(shippedDateField.CurrentValue);
Assert.False(shippedDateField.IsChanged);
}
}
Works, so it doesn't fail on the last line. ShippedDate is a DateTime2 field here too, but that doesn't really matter, changing the fields doesn't interact with the mapping data.
Would it be possible to debug the call path of setting the field's value in your case? The interesting code is in EntityCore<T>.SetValue, around line 3105:
if(FieldUtilities.DetermineIfFieldShouldBeSet(_fields.GetIsChanged(fieldIndex), originalValue, _fields.GetDbValue(fieldIndex), _isNew, valueToSet))
if this succeeds, it'll set the field and mark it as dirty. If not, it'll skip it. In my test, FieldUtilities.DetermineIfFieldShouldBeSet
returns false.
Regarding the type converter problem with your string field, let's first get this sorted, it might be the same issue...