Without digging too far into the issue, I seem to recall having run into 
the question in a SO or other forum some issues around fluent NH 
components, custom types, and so forth.

My take aways, I know for a FACT that as long as my custom type, if also 
dialect mapped custom type, sees the TimeSpan properly, or DateTimeOffset 
in the case of NodaTime.Instant, that the mapping happens as expected.

Something about components is ignoring custom types.

After some careful consideration and planning, decided it would be easier 
instead fo migrate the component columns to an all emcompassing JSON object 
column. The update was relatively painless. And we know how to map JSON 
oriented columns in a custom type, no problem.

So for now, that is the postgres pivot.

However, if anyone has any further background, insights, etc, would be glad 
to hear them, or whether there are any plans to address the matter moving 
fortward, in either NH an/or fluent.

Best,

Michael W. Powell

On Friday, June 27, 2025 at 3:50:50 PM UTC-4 Michael W Powell wrote:

> 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
> On Friday, June 27, 2025 at 12:27:11 PM UTC-4 Michael W Powell wrote:
>
>> With a strategically placed Debug write, the component low and high 
>> values are properly being set, but the failure is happening after the owner 
>> component instance has been set, as far as I can determine.
>>
>> 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'.
>>
>> Which there is some property setter logic going on there, but I wonder if 
>> that should not be a simple receiver of the NH negotiated component 
>> instance. One example:
>>
>> public virtual SchedulablePlayerComponentData HighValue
>> {
>>     get => _highValue;
>>     set
>>     {
>>         var highValue = _highValue;
>>
>>         if (SetProperty(ref highValue, value ?? new()) && 
>> !ReferenceEquals(highValue, _highValue))
>>         {
>>             _highValue.AcceptValue(highValue);
>>         }
>>     }
>> }
>>
>> So instead we just do: set => SetProperty(ref _highValue, value), and if 
>> we need to connect any business logic after that, we do that, but let NH 
>> handle the component property itself.
>>
>> Not dissimilar in approach from IList<T> collections, adding model or 
>> view model observability, ObservableCollection<T> around those source 
>> collections, for proper parent-child referencing.
>>
>> Thoughts?
>> On Friday, June 27, 2025 at 12:04:25 PM UTC-4 Michael W Powell wrote:
>>
>>> Hello,
>>>
>>> I have the dialect arranging a map between TimeSpan and 
>>> NodaTime.Duration. I step through several such user type conversions from 
>>> and to, assembled and disassembled, so I know that at least it should be 
>>> able to handle the use case.
>>>
>>> However, still receiving an exception, not sure quite as to why, maybe 
>>> something TimeSpan? Nullable is appearing in the ADO recordset somehow, I'm 
>>> not sure yet.
>>>
>>> And sorry for the mess here, the excptions, messages, and the query... 
>>> The data is all there as expected, and have traced up the callstack, up to 
>>> and including the repository pattern IQueryable query provider, everything 
>>> else being equal, the values SHOULD be landing in the mapped property(ies) 
>>> correctly.
>>>
>>> Trouble is in an exception like this, I do not know per se "which" 
>>> properties are failing. I'll look at possibly doing some Debug writes at 
>>> strategic IUserType moments when those details, alias, etc, are better 
>>> known. Although I'm not sure exactly that will tell me property names, more 
>>> like the query alias names, which is also not especially helpful.
>>>
>>> BTW, if it matters, the columns in question, are properly mapped in the 
>>> context of a fluent Component. This is intentionally properly the case. 
>>> I'll also have to review the component code, because I'm not hundred 
>>> percent certain those properties are not internally or privately set, with 
>>> their values either calculated or ctor provided.
>>>
>>> Thanks!
>>>
>>> NHibernate.Exceptions.GenericADOException: '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=?]'
>>>
>>> - $exception {"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 
>>> lane..."} NHibernate.Exceptions.GenericADOException
>>>
>>> + InnerException {"Unable to cast object of type 'System.TimeSpan' to 
>>> type 'NodaTime.Duration'."} System.Exception 
>>> {System.InvalidCastException}
>>>
>>>

-- 
You received this message because you are subscribed to the Google Groups 
"Fluent NHibernate" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion visit 
https://groups.google.com/d/msgid/fluent-nhibernate/911d80c1-8e60-48ee-95af-fba3b38f21ban%40googlegroups.com.

Reply via email to