Skip to content

Commit 2d27190

Browse files
authored
Merge pull request #2286 from Cratis:fix/child-relationship
Fixing a misunderstanding related to how the projection engine discovers children
2 parents 5c3a921 + 2b2901f commit 2d27190

9 files changed

+118
-33
lines changed

Source/Clients/DotNET.Specs/Projections/ModelBound/for_ModelBoundProjectionBuilder/when_building_model_with_children_from_and_child_has_id_by_convention.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ [Fact] void should_use_id_property_as_identified_by()
3030
var childrenDef = _result.Children[nameof(OrderWithChildHavingIdByConvention.Items)];
3131
childrenDef.IdentifiedBy.ShouldEqual(nameof(ItemWithIdByConvention.Id));
3232
}
33+
34+
[Fact] void should_apply_naming_policy_to_identified_by()
35+
{
36+
var childrenDef = _result.Children[nameof(OrderWithChildHavingIdByConvention.Items)];
37+
childrenDef.IdentifiedBy.ShouldEqual(naming_policy.GetPropertyName(new Properties.PropertyPath(nameof(ItemWithIdByConvention.Id))));
38+
}
3339
}
3440

3541
[EventType]

Source/Clients/DotNET.Specs/Projections/ModelBound/for_ModelBoundProjectionBuilder/when_building_model_with_children_from_and_child_has_key_attribute.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ [Fact] void should_use_key_property_as_identified_by()
3030
var childrenDef = _result.Children[nameof(OrderWithChildHavingKeyAttribute.Items)];
3131
childrenDef.IdentifiedBy.ShouldEqual(nameof(ItemWithKeyAttribute.ItemId));
3232
}
33+
34+
[Fact] void should_apply_naming_policy_to_identified_by()
35+
{
36+
var childrenDef = _result.Children[nameof(OrderWithChildHavingKeyAttribute.Items)];
37+
childrenDef.IdentifiedBy.ShouldEqual(naming_policy.GetPropertyName(new Properties.PropertyPath(nameof(ItemWithKeyAttribute.ItemId))));
38+
}
3339
}
3440

3541
[EventType]

Source/Clients/DotNET.Specs/Projections/ModelBound/for_ModelBoundProjectionBuilder/when_building_model_with_children_from_using_parent_id_as_key.cs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,29 @@ [Fact] void should_use_warehouse_id_from_event_as_key()
3333
fromDef.Key.ShouldEqual(nameof(WarehouseAddedToSimulation.Id));
3434
}
3535

36+
[Fact] void should_map_warehouse_id_property_to_event_id_not_event_source_id()
37+
{
38+
var eventType = event_types.GetEventTypeFor(typeof(WarehouseAddedToSimulation)).ToContract();
39+
var childrenDef = _result.Children[nameof(WarehousesForSimulation.Warehouses)];
40+
var fromDef = childrenDef.From.Single(kvp => kvp.Key.IsEqual(eventType)).Value;
41+
fromDef.Properties[nameof(Warehouse.Id)].ShouldEqual(nameof(WarehouseAddedToSimulation.Id));
42+
}
43+
3644
[Fact] void should_use_parent_id_property_as_parent_key()
3745
{
38-
// This test verifies the issue where the builder defaults to EventSourceId
39-
// instead of using the parent's Id property (WarehousesForSimulation.Id)
40-
// when no explicit parentKey is specified in the ChildrenFrom attribute
46+
// This test verifies the builder discovers the event property that identifies the parent
47+
// by matching the property type with the parent's Id property type (WarehouseSimulationId)
48+
// The event has SimulationId property which matches the parent's Id property type
4149
var eventType = event_types.GetEventTypeFor(typeof(WarehouseAddedToSimulation)).ToContract();
4250
var childrenDef = _result.Children[nameof(WarehousesForSimulation.Warehouses)];
4351
var fromDef = childrenDef.From.Single(kvp => kvp.Key.IsEqual(eventType)).Value;
44-
fromDef.ParentKey.ShouldEqual(nameof(WarehousesForSimulation.Id));
52+
fromDef.ParentKey.ShouldEqual(nameof(WarehouseAddedToSimulation.SimulationId));
53+
}
54+
55+
[Fact] void should_apply_naming_policy_to_identified_by()
56+
{
57+
var childrenDef = _result.Children[nameof(WarehousesForSimulation.Warehouses)];
58+
childrenDef.IdentifiedBy.ShouldEqual(naming_policy.GetPropertyName(new Properties.PropertyPath(nameof(Warehouse.Id))));
4559
}
4660
}
4761

Source/Clients/DotNET.Specs/Projections/ModelBound/for_ModelBoundProjectionBuilder/when_building_model_with_children_from_with_auto_map.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,20 @@ [Fact] void should_auto_map_price()
6363
var fromDef = childrenDef.From.Single(kvp => kvp.Key.IsEqual(eventType)).Value;
6464
fromDef.Properties.Keys.ShouldContain(nameof(OrderLineItem.Price));
6565
}
66+
67+
[Fact] void should_apply_naming_policy_to_identified_by()
68+
{
69+
var childrenDef = _result.Children[nameof(OrderWithAutoMappedChildren.Items)];
70+
childrenDef.IdentifiedBy.ShouldEqual(naming_policy.GetPropertyName(new Properties.PropertyPath(nameof(OrderLineItem.Id))));
71+
}
72+
73+
[Fact] void should_apply_naming_policy_to_key()
74+
{
75+
var eventType = event_types.GetEventTypeFor(typeof(LineItemAdded)).ToContract();
76+
var childrenDef = _result.Children[nameof(OrderWithAutoMappedChildren.Items)];
77+
var fromDef = childrenDef.From.Single(kvp => kvp.Key.IsEqual(eventType)).Value;
78+
fromDef.Key.ShouldEqual(naming_policy.GetPropertyName(new Properties.PropertyPath(nameof(LineItemAdded.ItemId))));
79+
}
6680
}
6781

6882
[EventType]

Source/Clients/DotNET.Specs/Projections/ModelBound/for_ModelBoundProjectionBuilder/when_building_model_with_children_having_id_by_convention_should_default_to_event_source_id.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ [Fact] void should_auto_map_license_plate_property()
5252
var fromDef = childrenDef.From.Single(kvp => kvp.Key.IsEqual(eventType)).Value;
5353
fromDef.Properties.Keys.ShouldContain(nameof(Vehicle.LicensePlate));
5454
}
55+
56+
[Fact] void should_apply_naming_policy_to_identified_by()
57+
{
58+
var childrenDef = _result.Children[nameof(VehicleFleet.Vehicles)];
59+
childrenDef.IdentifiedBy.ShouldEqual(naming_policy.GetPropertyName(new Properties.PropertyPath(nameof(Vehicle.Id))));
60+
}
5561
}
5662

5763
[EventType]

Source/Clients/DotNET.Specs/Projections/ModelBound/for_ModelBoundProjectionBuilder/when_building_model_with_children_having_key_attribute_should_default_to_event_source_id.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ [Fact] void should_auto_map_name_property()
5353
var fromDef = childrenDef.From.Single(kvp => kvp.Key.IsEqual(eventType)).Value;
5454
fromDef.Properties.Keys.ShouldContain(nameof(Employee.Name));
5555
}
56+
57+
[Fact] void should_apply_naming_policy_to_identified_by()
58+
{
59+
var childrenDef = _result.Children[nameof(Department.Employees)];
60+
childrenDef.IdentifiedBy.ShouldEqual(naming_policy.GetPropertyName(new Properties.PropertyPath(nameof(Employee.EmployeeNumber))));
61+
}
5662
}
5763

5864
[EventType]

Source/Clients/DotNET.Specs/Projections/ModelBound/for_ModelBoundProjectionBuilder/when_building_model_with_children_having_set_from_context.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ [Fact] void should_auto_map_co2_per_km_property()
6060
var fromDef = childrenDef.From.Single(kvp => kvp.Key.IsEqual(eventType)).Value;
6161
fromDef.Properties.Keys.ShouldContain(nameof(TransportType.Co2PerKm));
6262
}
63+
64+
[Fact] void should_apply_naming_policy_to_identified_by()
65+
{
66+
var childrenDef = _result.Children[nameof(TransportTypesForSimulation.TransportTypes)];
67+
childrenDef.IdentifiedBy.ShouldEqual(naming_policy.GetPropertyName(new Properties.PropertyPath(nameof(TransportType.Id))));
68+
}
6369
}
6470

6571
[EventType]

Source/Clients/DotNET.Specs/Projections/ModelBound/for_ModelBoundProjectionBuilder/when_building_model_with_from_event_with_custom_key.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,11 @@ [Fact] void should_use_custom_key_from_event()
4444
var fromDefinition = _result.From.Single(kvp => kvp.Key.IsEqual(eventType)).Value;
4545
fromDefinition.Key.ShouldEqual(nameof(UserRegisteredWithCustomId.UserId));
4646
}
47+
48+
[Fact] void should_apply_naming_policy_to_custom_key()
49+
{
50+
var eventType = event_types.GetEventTypeFor(typeof(UserRegisteredWithCustomId)).ToContract();
51+
var fromDefinition = _result.From.Single(kvp => kvp.Key.IsEqual(eventType)).Value;
52+
fromDefinition.Key.ShouldEqual(naming_policy.GetPropertyName(new Properties.PropertyPath(nameof(UserRegisteredWithCustomId.UserId))));
53+
}
4754
}

Source/Clients/DotNET/Projections/ModelBound/ChildrenDefinitionExtensions.cs

Lines changed: 49 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ internal static void ProcessChildrenFromAttribute(
4949

5050
var key = keyProperty?.GetValue(attr) as string ?? WellKnownExpressions.EventSourceId;
5151
var explicitParentKey = parentKeyProperty?.GetValue(attr) as string;
52-
var discoveredParentKey = explicitParentKey is null ? DiscoverParentKey(parentModelType) : null;
52+
var discoveredParentKey = explicitParentKey is null ? DiscoverEventPropertyForParentId(eventType, parentModelType, namingPolicy) : null;
5353
var parentKey = explicitParentKey ?? discoveredParentKey ?? WellKnownExpressions.EventSourceId;
5454
var autoMap = autoMapProperty?.GetValue(attr) as bool? ?? true;
5555

@@ -60,11 +60,16 @@ internal static void ProcessChildrenFromAttribute(
6060
identifiedBy = DiscoverKeyPropertyName(childType);
6161
}
6262

63+
// Apply naming policy to identifiedBy to ensure consistent casing
64+
var identifiedByWithNaming = string.IsNullOrEmpty(identifiedBy) || identifiedBy == WellKnownExpressions.EventSourceId
65+
? identifiedBy
66+
: namingPolicy.GetPropertyName(new PropertyPath(identifiedBy));
67+
6368
if (!definition.Children.TryGetValue(propertyName, out var childrenDef))
6469
{
6570
childrenDef = new ChildrenDefinition
6671
{
67-
IdentifiedBy = identifiedBy,
72+
IdentifiedBy = identifiedByWithNaming,
6873
From = new Dictionary<EventType, FromDefinition>(),
6974
Join = new Dictionary<EventType, JoinDefinition>(),
7075
All = new FromEveryDefinition(),
@@ -128,21 +133,26 @@ internal static void ProcessChildrenFromAttribute(
128133
a.GetType().GetGenericTypeDefinition() == typeof(AddFromAttribute<>) ||
129134
a.GetType().GetGenericTypeDefinition() == typeof(SubtractFromAttribute<>)));
130135

131-
// Only auto-map Id/Key to EventSourceId if autoMap is enabled and there's no explicit mapping
132-
if (autoMap && !hasExplicitMapping)
136+
// If this is the identified property and has no explicit mapping, map it to the key
137+
if (autoMap && !hasExplicitMapping && parameter.Name!.Equals(identifiedBy, StringComparison.OrdinalIgnoreCase))
133138
{
134-
// If this is the identified property and has no explicit mapping, default to EventSourceId
135-
if (parameter.Name!.Equals(identifiedBy, StringComparison.OrdinalIgnoreCase))
139+
// If key is EventSourceId, use event context, otherwise use the key property from the event
140+
if (key == WellKnownExpressions.EventSourceId)
136141
{
137142
fromDefinition.Properties[paramPropertyName] = "$eventContext(EventSourceId)";
138143
}
139-
140-
// Check if parameter has [Key] attribute and no explicit mapping
141-
if (parameter.GetCustomAttribute<KeyAttribute>() is not null)
144+
else
142145
{
143-
fromDefinition.Properties[paramPropertyName] = "$eventContext(EventSourceId)";
146+
// Map to the key from the event (keyExpression already has naming policy applied)
147+
fromDefinition.Properties[paramPropertyName] = keyExpression;
144148
}
145149
}
150+
151+
// Check if parameter has [Key] attribute and no explicit mapping and key is EventSourceId
152+
if (autoMap && !hasExplicitMapping && key == WellKnownExpressions.EventSourceId && parameter.GetCustomAttribute<KeyAttribute>() is not null)
153+
{
154+
fromDefinition.Properties[paramPropertyName] = "$eventContext(EventSourceId)";
155+
}
146156
}
147157
}
148158

@@ -207,40 +217,50 @@ static string DiscoverKeyPropertyName(Type? childType)
207217
return WellKnownExpressions.EventSourceId;
208218
}
209219

210-
static string? DiscoverParentKey(Type? parentModelType)
220+
static string? DiscoverEventPropertyForParentId(Type eventType, Type? parentModelType, INamingPolicy namingPolicy)
211221
{
212222
if (parentModelType is null)
213223
{
214224
return null;
215225
}
216226

217-
// Look for a property or parameter named "Id" (case-insensitive)
218-
var idProperty = parentModelType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
227+
// First, find the parent model's Id property and its type
228+
var parentIdProperty = parentModelType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
219229
.FirstOrDefault(p => p.Name.Equals("Id", StringComparison.OrdinalIgnoreCase));
220230

221-
if (idProperty is not null)
231+
if (parentIdProperty is null)
222232
{
223-
return idProperty.Name;
224-
}
225-
226-
// Check primary constructor parameters for Id
227-
var primaryConstructor = parentModelType.GetConstructors(BindingFlags.Public | BindingFlags.Instance)
228-
.OrderByDescending(c => c.GetParameters().Length)
229-
.FirstOrDefault();
233+
// Check constructor parameters for record types
234+
var constructor = parentModelType.GetConstructors(BindingFlags.Public | BindingFlags.Instance)
235+
.OrderByDescending(c => c.GetParameters().Length)
236+
.FirstOrDefault();
230237

231-
if (primaryConstructor is not null)
232-
{
233-
var idParameter = primaryConstructor.GetParameters()
238+
var idParameter = constructor?.GetParameters()
234239
.FirstOrDefault(p => p.Name?.Equals("Id", StringComparison.OrdinalIgnoreCase) == true);
235240

236-
if (idParameter is not null)
241+
if (idParameter is null)
237242
{
238-
// Convert parameter name to property name (capitalize first letter for records)
239-
var paramName = idParameter.Name!;
240-
return char.ToUpperInvariant(paramName[0]) + paramName.Substring(1);
243+
return null;
241244
}
245+
246+
// Get the type from constructor parameter
247+
var parentIdType = idParameter.ParameterType;
248+
249+
// Search event for a property with matching type
250+
var matchingEventProperty = eventType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
251+
.FirstOrDefault(p => p.PropertyType == parentIdType);
252+
253+
return matchingEventProperty is not null
254+
? namingPolicy.GetPropertyName(new PropertyPath(matchingEventProperty.Name))
255+
: null;
242256
}
243257

244-
return null;
258+
// Search event for a property with matching type
259+
var eventProperty = eventType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
260+
.FirstOrDefault(p => p.PropertyType == parentIdProperty.PropertyType);
261+
262+
return eventProperty is not null
263+
? namingPolicy.GetPropertyName(new PropertyPath(eventProperty.Name))
264+
: null;
245265
}
246266
}

0 commit comments

Comments
 (0)