More diagnostics, I have INPC wired in my domain model, which communicates to our WPF view model, view, bindings, and so forth. Some debugging there. Can see the Id being set properly. Changed here, changing snipped for brevity, but you can see the progression, things appear to be properly set.
Although it is curious the IQueryable failure lands during the InternalPositions (ObservableCollection<Location>) INPC EH. No discernable way why that should be the case. And without explanation why the TimeSpan Duration user type is failing.
Property changed: WhalleyBotEnhanced.Tracks.TrackMeetLane; root: WhalleyBotEnhanced.Tracks.TrackMeetLane; property: Id
NullSafeGet: fields: 18, names: [lowvalueduetime7_10_0_], owner class: 'WhalleyBotEnhanced.Tracks.TrackMeetLane'.
NullSafeGet: fields: 18, names: [lowvalueperiod8_10_0_], owner class: 'WhalleyBotEnhanced.Tracks.TrackMeetLane'.
NullSafeGet: fields: 18, names: [lowvalueduration9_10_0_], owner class: 'WhalleyBotEnhanced.Tracks.TrackMeetLane'.
NullSafeGet: fields: 18, names: [highvalueduetime11_10_0_], owner class: 'WhalleyBotEnhanced.Tracks.TrackMeetLane'.
NullSafeGet: fields: 18, names: [highvalueperiod12_10_0_], owner class: 'WhalleyBotEnhanced.Tracks.TrackMeetLane'.
NullSafeGet: fields: 18, names: [highvalueduration13_10_0_], owner class: 'WhalleyBotEnhanced.Tracks.TrackMeetLane'.
Property changed: WhalleyBotEnhanced.Tracks.TrackMeetLane; root: WhalleyBotEnhanced.Tracks.TrackMeetLane; property: Enabled
Property changed: WhalleyBotEnhanced.Tracks.TrackMeetLane; root: WhalleyBotEnhanced.Tracks.TrackMeetLane; property: IsReady
Property changed: WhalleyBotEnhanced.Tracks.TrackMeetLane; root: WhalleyBotEnhanced.Tracks.TrackMeetLane; property: TrackMeetEvent
Property changed: WhalleyBotEnhanced.Tracks.TrackMeetLane; root: WhalleyBotEnhanced.Tracks.TrackMeetLane; property: Name
Property changed: WhalleyBotEnhanced.Tracks.TrackMeetLane; root: WhalleyBotEnhanced.Tracks.TrackMeetLane; property: Color
Property changed: WhalleyBotEnhanced.Tracks.TrackMeetLane; root: WhalleyBotEnhanced.Tracks.TrackMeetLane; property: LowValue
Property changed: WhalleyBotEnhanced.Tracks.TrackMeetLane; root: WhalleyBotEnhanced.Tracks.TrackMeetLane; property: HighValue
Property changed: WhalleyBotEnhanced.Tracks.TrackMeetLane; root: WhalleyBotEnhanced.Tracks.TrackMeetLane; property: PackCategories
Property changed: WhalleyBotEnhanced.Tracks.TrackMeetLane; root: WhalleyBotEnhanced.Tracks.TrackMeetLane; property: InternalPackCategories
Property changed: WhalleyBotEnhanced.Tracks.TrackMeetLane; root: WhalleyBotEnhanced.Tracks.TrackMeetLane; property: PackIds
Property changed: WhalleyBotEnhanced.Tracks.TrackMeetLane; root: WhalleyBotEnhanced.Tracks.TrackMeetLane; property: InternalPackIds
Property changed: WhalleyBotEnhanced.Tracks.TrackMeetLane; root: WhalleyBotEnhanced.Tracks.TrackMeetLane; property: Positions
Property changed: WhalleyBotEnhanced.Tracks.TrackMeetLane; root: WhalleyBotEnhanced.Tracks.TrackMeetLane; property: InternalPositions
Exception thrown: 'NHibernate.Exceptions.GenericADOException' in NHibernate.dll
An exception of type 'NHibernate.Exceptions.GenericADOException' occurred in NHibernate.dll but was not handled in user code
could not initialize a collection: [WhalleyBotEnhanced.Tracks.TrackMeetEvent.Lanes#c0832da0-d2f9-412c-8364-059f7ed205b5][SQL: SELECT lanes0_.TrackMeetEventId as trackmeeteventid3_10_1_, lanes0_.Id as id1_10_1_, lanes0_.Id as id1_10_0_, lanes0_.Enabled as enabled2_10_0_, lanes0_.TrackMeetEventId as trackmeeteventid3_10_0_, lanes0_.Name as name4_10_0_, lanes0_.Color as color5_10_0_, lanes0_.lowValuePlayerCount as lowvalueplayercount6_10_0_, lanes0_.lowValueDueTime as lowvalueduetime7_10_0_, lanes0_.lowValuePeriod as lowvalueperiod8_10_0_, lanes0_.lowValueDuration as lowvalueduration9_10_0_, lanes0_.highValuePlayerCount as highvalueplayercount10_10_0_, lanes0_.highValueDueTime as highvalueduetime11_10_0_, lanes0_.highValuePeriod as highvalueperiod12_10_0_, lanes0_.highValueDuration as highvalueduration13_10_0_, lanes0_.packCategoriesJson as packcategoriesjson14_10_0_, lanes0_.packIdsJson as packidsjson15_10_0_, lanes0_.positionsJson as positionsjson16_10_0_ FROM public.efcore_wbe_trackmeetlane lanes0_ WHERE lanes0_.TrackMeetEventId=?]
The core of the user type for us is TryAssemble and TryDisassemble, basically from (database) and to (database) directions. Our approach thus far is to handle the edge cases, null, intercepting the TPrimitive (P) and TUser (U), and failing those, throw an exception. Then in the derived class, override the two Try methods only focused on the specific types we want to convert ONLY. Which for the most part seems to be working well. Wondering however if there are some issues with Fluent and NH components not using this for whatever reason, which would be muy no bueno.
protected override bool TryAssemble(object cached, object owner, out object assembled)
{
assembled = null;
switch (cached)
{
case not null when cached is Duration duration:
assembled = duration;
break;
case not null when cached is TimeSpan timeSpan:
assembled = Duration.FromTicks(timeSpan.Ticks);
break;
}
return assembled is not null;
}
protected override bool TryDisassemble(object value, out object disassembled)
{
disassembled = null;
switch (value)
{
case not null when value is Duration duration:
// TODO: for now assuming TimeSpan is the intermediate record set type
disassembled = duration.ToTimeSpan();
break;
case not null when value is TimeSpan timeSpan:
disassembled = timeSpan;
break;
case not null:
// TODO: just in the event we may need a DateTimeOffset use case
throw new InvalidOperationException($"Unable to disassemble value type '{value.GetType()}'.");
}
return disassembled is not null;
}
And from the base class perspective. Snipped for brevity... effectively you can say the Try methods are both core to the Assemble and Disassemble methods, which are in turn central to the user type. And as long as we have primitive P and returned R types correctly aligned, it works out pretty well. P being with respect to the ADO recordset, and R being the returned or sometimes user (U) type. In this specific circumstance, TimeSpan to NodaTime.Duration.
public virtual object NullSafeGet(DbDataReader rs, string[] names, ISessionImplementor session, object owner)
{
// after lifting the index ordinal named field...
return value switch
{
DBNull or null => null,
R r => Assemble(Disassemble(r), owner),
P p => Assemble(p, owner),
_ => throw new InvalidOperationException($"Unable to null safe get value type '{value.GetType()}', names [{string.Join(", ", names)}].")
};
}
public virtual void NullSafeSet(DbCommand cmd, object value, int index, ISessionImplementor session)
{
// after finding cmd, arg (parameter) is the correct pattern matched type and so on...
arg.Value = value switch
{
DBNull or null => DBNull.Value,
P p => p,
R r => Disassemble(r),
_ => throw new InvalidOperationException($"Unable to null safe set value type '{value.GetType()}', index {index}.")
};
// ...
}
I put breakpoints in the Try methods, and I do not see anything that jumps out as being unusual or errant there. But for whatever reason obviously a convertion is being missed.
Our component part handler is this:
protected static void OnMapSchedulablePlayerComponentData(ComponentPart<SchedulablePlayerComponentData> componentPart)
{
componentPart.Map(x => x.PlayerCount).Not.Nullable();
componentPart.Map(x => x.DueTime).CustomType<NpgsqlNodaTimeDurationUserType>().Not.Nullable();
componentPart.Map(x => x.Period).CustomType<NpgsqlNodaTimeDurationUserType>().Not.Nullable();
componentPart.Map(x => x.Duration).CustomType<NpgsqlNodaTimeDurationUserType>().Not.Nullable();
}
And in the lane map. CamelCaseColumnPrefix is something I added to camel case the prefix. Technically perhaps a nice to have.
Component(x => x.LowValue, OnMapSchedulablePlayerComponentData).CamelCaseColumnPrefix(nameof(TrackMeetLane.LowValue));
Component(x => x.HighValue, OnMapSchedulablePlayerComponentData).CamelCaseColumnPrefix(nameof(TrackMeetLane.HighValue));
As far as the map itself and the queries, verified the queries are good and returning the data they should, including in the specific ID use case.
Totaly mystery at this point why the custom types are not being handled correctly. And it would be a massive issue if we had to somehow break open the component, that would be bad.
Failing fluent NH components, we could perhaps rethink whether component is the right answer, and do an alternate JSON based approach for the column(s) itself. We are doing that in other places and it seems to be working correctly.
Open to other suggestions.
Best and thank you!
Michael W. Powell