@@ -55,7 +55,7 @@ protected JasperFxAggregationProjectionBase(AggregationScope scope)
5555 base . Version = att . Version ;
5656 }
5757
58- NaturalKeyDefinition = discoverNaturalKey ( ) ;
58+ NaturalKeyDefinition = discoverNaturalKey ( GetType ( ) ) ;
5959 }
6060
6161 public NaturalKeyDefinition ? NaturalKeyDefinition { get ; }
@@ -402,7 +402,7 @@ public virtual Task EnrichEventsAsync(SliceGroup<TDoc, TId> group, TQuerySession
402402 return ( lastEvent , aggregate ) ;
403403 }
404404
405- private static NaturalKeyDefinition ? discoverNaturalKey ( )
405+ private static NaturalKeyDefinition ? discoverNaturalKey ( Type projectionType )
406406 {
407407 var docType = typeof ( TDoc ) ;
408408
@@ -414,19 +414,87 @@ public virtual Task EnrichEventsAsync(SliceGroup<TDoc, TId> group, TQuerySession
414414
415415 var definition = new NaturalKeyDefinition ( docType , naturalKeyProp ) ;
416416
417- // Discover [NaturalKeySource] methods on the aggregate to build event mappings
418- var methods = docType . GetMethods ( BindingFlags . Public | BindingFlags . Instance )
417+ // Discover [NaturalKeySource] methods on the aggregate type (instance methods)
418+ discoverNaturalKeySourceMethods ( definition , naturalKeyProp , docType ,
419+ BindingFlags . Public | BindingFlags . Instance ) ;
420+
421+ // Also discover [NaturalKeySource] methods on the projection type itself
422+ // (static methods on the projection class, e.g., Create/Apply methods)
423+ if ( projectionType != docType )
424+ {
425+ discoverNaturalKeySourceMethods ( definition , naturalKeyProp , projectionType ,
426+ BindingFlags . Public | BindingFlags . Instance | BindingFlags . Static ) ;
427+ }
428+
429+ return definition ;
430+ }
431+
432+ private static void discoverNaturalKeySourceMethods (
433+ NaturalKeyDefinition definition ,
434+ PropertyInfo naturalKeyProp ,
435+ Type searchType ,
436+ BindingFlags bindingFlags )
437+ {
438+ var docType = typeof ( TDoc ) ;
439+ var methods = searchType . GetMethods ( bindingFlags )
419440 . Where ( m => m . GetCustomAttribute < NaturalKeySourceAttribute > ( ) != null ) ;
420441
421442 foreach ( var method in methods )
422443 {
423444 var parameters = method . GetParameters ( ) ;
424445 if ( parameters . Length == 0 ) continue ;
425446
426- var eventType = parameters [ 0 ] . ParameterType ;
447+ // Determine the event type from the first parameter.
448+ // It can be the raw event type or IEvent<T>.
449+ var firstParamType = parameters [ 0 ] . ParameterType ;
450+ Type eventType ;
451+ if ( firstParamType . IsGenericType &&
452+ firstParamType . GetGenericTypeDefinition ( ) == typeof ( IEvent < > ) )
453+ {
454+ eventType = firstParamType . GetGenericArguments ( ) [ 0 ] ;
455+ }
456+ else if ( typeof ( IEvent ) . IsAssignableFrom ( firstParamType ) )
457+ {
458+ eventType = firstParamType ;
459+ }
460+ else
461+ {
462+ eventType = firstParamType ;
463+ }
464+
465+ // Skip if we already have a mapping for this event type
466+ if ( definition . EventMappings . Any ( m => m . EventType == eventType ) )
467+ continue ;
468+
469+ try
470+ {
471+ var extractor = buildExtractor ( method , naturalKeyProp , docType , parameters ) ;
472+ if ( extractor != null )
473+ {
474+ definition . EventMappings . Add ( new NaturalKeyEventMapping ( eventType , extractor ) ) ;
475+ }
476+ }
477+ catch
478+ {
479+ // Silently skip methods we can't build extractors for
480+ }
481+ }
482+ }
483+
484+ private static Func < object , object ? > ? buildExtractor (
485+ MethodInfo method ,
486+ PropertyInfo naturalKeyProp ,
487+ Type docType ,
488+ ParameterInfo [ ] parameters )
489+ {
490+ var eventParam = Expression . Parameter ( typeof ( object ) , "e" ) ;
491+ var firstParamType = parameters [ 0 ] . ParameterType ;
427492
428- // Build a delegate that: creates aggregate, calls the method, reads the natural key property
429- var eventParam = Expression . Parameter ( typeof ( object ) , "e" ) ;
493+ // For instance methods on the aggregate (the original working pattern):
494+ // Create a new TDoc, call the method, read the natural key property
495+ if ( ! method . IsStatic && method . DeclaringType == docType )
496+ {
497+ var eventType = firstParamType ;
430498 var docParam = Expression . Variable ( docType , "doc" ) ;
431499
432500 var body = Expression . Block (
@@ -436,10 +504,42 @@ public virtual Task EnrichEventsAsync(SliceGroup<TDoc, TId> group, TQuerySession
436504 Expression . Convert ( Expression . Property ( docParam , naturalKeyProp ) , typeof ( object ) )
437505 ) ;
438506
439- var extractor = Expression . Lambda < Func < object , object ? > > ( body , eventParam ) . Compile ( ) ;
440- definition . EventMappings . Add ( new NaturalKeyEventMapping ( eventType , extractor ) ) ;
507+ return Expression . Lambda < Func < object , object ? > > ( body , eventParam ) . Compile ( ) ;
441508 }
442509
443- return definition ;
510+ // For static methods on the projection class, we can't safely call them
511+ // (they may need IEvent with StreamKey, etc.). Instead, find a matching
512+ // property on the event data type and read it directly.
513+ Type eventDataType ;
514+ if ( firstParamType . IsGenericType && firstParamType . GetGenericTypeDefinition ( ) == typeof ( IEvent < > ) )
515+ {
516+ eventDataType = firstParamType . GetGenericArguments ( ) [ 0 ] ;
517+ }
518+ else if ( firstParamType == docType && parameters . Length >= 2 )
519+ {
520+ var secondType = parameters [ 1 ] . ParameterType ;
521+ eventDataType = secondType . IsGenericType && secondType . GetGenericTypeDefinition ( ) == typeof ( IEvent < > )
522+ ? secondType . GetGenericArguments ( ) [ 0 ]
523+ : secondType ;
524+ }
525+ else
526+ {
527+ eventDataType = firstParamType ;
528+ }
529+
530+ // Search for a property on the event data that matches the natural key by type
531+ var eventKeyProp = eventDataType
532+ . GetProperties ( BindingFlags . Public | BindingFlags . Instance )
533+ . FirstOrDefault ( p => p . PropertyType == naturalKeyProp . PropertyType ) ;
534+
535+ if ( eventKeyProp == null ) return null ;
536+
537+ var body2 = Expression . Convert (
538+ Expression . Property (
539+ Expression . Convert ( eventParam , eventDataType ) ,
540+ eventKeyProp ) ,
541+ typeof ( object ) ) ;
542+
543+ return Expression . Lambda < Func < object , object ? > > ( body2 , eventParam ) . Compile ( ) ;
444544 }
445545}
0 commit comments