Skip to content

Commit 5091f0a

Browse files
fixing empty metadata value database commit issue
1 parent 7e01968 commit 5091f0a

3 files changed

Lines changed: 187 additions & 49 deletions

File tree

ECommerce.BusinessEvents.Tests/Services/EventTrackingServiceTests.cs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,123 @@ public async Task GetAllEventsAsync_ReturnsEmptyListWhenNoEvents()
212212
Assert.Empty(events);
213213
}
214214

215+
[Fact]
216+
public async Task TrackEventAsync_WithNullAndEmptyFields_OnlySavesMetadataForFieldsWithData()
217+
{
218+
// Arrange - Create a schema with metadata extraction configuration
219+
string customerSchemaWithMetadata = @"{
220+
""$schema"": ""https://json-schema.org/draft/2020-12/schema"",
221+
""$id"": ""https://example.com/schemas/customer-with-metadata/v1"",
222+
""title"": ""Customer With Metadata"",
223+
""type"": ""object"",
224+
""properties"": {
225+
""Id"": {
226+
""type"": ""string"",
227+
""x-metadata"": true
228+
},
229+
""Name"": {
230+
""type"": ""string"",
231+
""x-metadata"": true
232+
},
233+
""Email"": {
234+
""type"": ""string"",
235+
""format"": ""email"",
236+
""x-metadata"": true
237+
},
238+
""Phone"": {
239+
""type"": [""string"", ""null""],
240+
""x-metadata"": true
241+
},
242+
""CompanyName"": {
243+
""type"": [""string"", ""null""],
244+
""x-metadata"": true
245+
},
246+
""Status"": {
247+
""type"": ""string"",
248+
""x-metadata"": true
249+
}
250+
},
251+
""required"": [""Id"", ""Name"", ""Email""]
252+
}";
253+
254+
await _schemaRegistry.AddSchemaAsync("CustomerWithMetadata", 1, customerSchemaWithMetadata);
255+
256+
// Create test data with some null/empty fields
257+
// Filtering happens in ExtractMetadataFromJson during JSON parsing stage
258+
var customerData = new
259+
{
260+
Id = "test-123",
261+
Name = "John Doe",
262+
Email = "john.doe@example.com",
263+
Phone = (string?)null, // null field - should be filtered out
264+
CompanyName = "", // empty string - should be filtered out
265+
Status = "Active" // valid field - should be included
266+
};
267+
268+
_schemaValidatorMock
269+
.Setup(v => v.Validate(It.IsAny<string>(), It.IsAny<string>()))
270+
.Returns(ECommerce.Common.Result<ECommerce.Common.Unit, string>.Success(new ECommerce.Common.Unit()));
271+
272+
var dto = new BusinessEventDto
273+
{
274+
EntityType = "CustomerWithMetadata",
275+
EntityId = "test-123",
276+
EventType = IBusinessEventService.EventType.Created,
277+
SchemaVersion = 1,
278+
EventTimestamp = DateTimeOffset.UtcNow,
279+
CorrelationId = Guid.NewGuid().ToString(),
280+
ActorId = "test-user",
281+
ActorType = IBusinessEventService.ActorType.User,
282+
EntityData = customerData
283+
};
284+
285+
// Act
286+
var result = await _eventTracker.TrackEventAsync(dto);
287+
288+
// Assert
289+
Assert.True(result.IsSuccess, result.Error);
290+
291+
// Verify the main event was saved
292+
var savedEvent = await _context.BusinessEvents.FirstOrDefaultAsync();
293+
Assert.NotNull(savedEvent);
294+
Assert.Equal("CustomerWithMetadata", savedEvent.EntityType);
295+
Assert.Equal("test-123", savedEvent.EntityId);
296+
297+
// Verify metadata - should only have records for fields with actual data
298+
// Null and empty fields are filtered out in ExtractMetadataFromJson method
299+
var metadataRecords = await _context.BusinessEventMetadata
300+
.Where(m => m.EventId == savedEvent.EventId)
301+
.ToListAsync();
302+
303+
// Should have metadata for: Id, Name, Email, Status (4 fields)
304+
// Should NOT have metadata for: Phone (null), CompanyName (empty string)
305+
Assert.Equal(4, metadataRecords.Count);
306+
307+
// Verify specific metadata entries exist with correct values
308+
var idMetadata = metadataRecords.FirstOrDefault(m => m.MetadataKey == "Id");
309+
Assert.NotNull(idMetadata);
310+
Assert.Equal("test-123", idMetadata.MetadataValue);
311+
312+
var nameMetadata = metadataRecords.FirstOrDefault(m => m.MetadataKey == "Name");
313+
Assert.NotNull(nameMetadata);
314+
Assert.Equal("John Doe", nameMetadata.MetadataValue);
315+
316+
var emailMetadata = metadataRecords.FirstOrDefault(m => m.MetadataKey == "Email");
317+
Assert.NotNull(emailMetadata);
318+
Assert.Equal("john.doe@example.com", emailMetadata.MetadataValue);
319+
320+
var statusMetadata = metadataRecords.FirstOrDefault(m => m.MetadataKey == "Status");
321+
Assert.NotNull(statusMetadata);
322+
Assert.Equal("Active", statusMetadata.MetadataValue);
323+
324+
// Verify null and empty fields are NOT saved as metadata
325+
var phoneMetadata = metadataRecords.FirstOrDefault(m => m.MetadataKey == "Phone");
326+
Assert.Null(phoneMetadata);
327+
328+
var companyMetadata = metadataRecords.FirstOrDefault(m => m.MetadataKey == "CompanyName");
329+
Assert.Null(companyMetadata);
330+
}
331+
215332
public void Dispose()
216333
{
217334
_context.Dispose();

ECommerce.BusinessEvents/Services/EventTrackingService.cs

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ private async Task<Result<Unit, string>> ExtractAndSaveMetadataAsync(
108108
{
109109
try
110110
{
111+
logger.LogInformation("Adding metadata for event {EventId}, entity type {EntityType}, entity ID {EntityId}",
112+
eventId, entityType, entityId);
113+
111114
// Get metadata configuration from schema
112115
var metadataConfig = await schemaRegistry.GetMetadataConfigAsync(entityType, schemaVersion);
113116

@@ -121,16 +124,17 @@ private async Task<Result<Unit, string>> ExtractAndSaveMetadataAsync(
121124
// Extract metadata fields from JSON
122125
var metadataEntries = ExtractMetadataFromJson(jsonData, metadataConfig);
123126

124-
// Create BusinessEventMetadata entries
125-
var businessEventMetadata = metadataEntries.Select(entry => new BusinessEventMetadata
126-
{
127-
EventId = eventId,
128-
EntityType = entityType,
129-
EntityId = entityId,
130-
MetadataKey = entry.Key,
131-
MetadataValue = entry.Value.Value?.ToString() ?? string.Empty,
132-
DataType = entry.Value.DataType
133-
}).ToList();
127+
// Create BusinessEventMetadata entries (filtering already done in ExtractMetadataFromJson)
128+
var businessEventMetadata = metadataEntries
129+
.Select(entry => new BusinessEventMetadata
130+
{
131+
EventId = eventId,
132+
EntityType = entityType,
133+
EntityId = entityId,
134+
MetadataKey = entry.Key,
135+
MetadataValue = entry.Value.Value!.ToString()!,
136+
DataType = entry.Value.DataType
137+
}).ToList();
134138

135139
if (businessEventMetadata.Any())
136140
{
@@ -153,6 +157,7 @@ private async Task<Result<Unit, string>> ExtractAndSaveMetadataAsync(
153157
/// <summary>
154158
/// Extracts metadata values from JSON data based on configured field paths.
155159
/// Supports nested field access using dot notation.
160+
/// Only includes fields that have actual data (not null or empty).
156161
/// </summary>
157162
private Dictionary<string, (object? Value, string DataType)> ExtractMetadataFromJson(
158163
string jsonData,
@@ -168,9 +173,13 @@ private async Task<Result<Unit, string>> ExtractAndSaveMetadataAsync(
168173
foreach (var fieldPath in config.FieldsToExtract)
169174
{
170175
var value = GetValueFromJsonPath(root, fieldPath);
171-
var dataType = config.FieldTypes.GetValueOrDefault(fieldPath, "string");
172176

173-
metadata[fieldPath] = (value, dataType);
177+
// Only include fields that have actual data
178+
if (value != null && !string.IsNullOrWhiteSpace(value.ToString()))
179+
{
180+
var dataType = config.FieldTypes.GetValueOrDefault(fieldPath, "string");
181+
metadata[fieldPath] = (value, dataType);
182+
}
174183
}
175184
}
176185
catch (Exception ex)

ECommerce.BusinessEvents/Services/SchemaRegistryService.cs

Lines changed: 49 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -103,75 +103,87 @@ private void ExtractMetadataFromSchema(JsonElement schemaElement, MetadataExtrac
103103
var propertySchema = property.Value;
104104
var currentPath = string.IsNullOrEmpty(fieldPath) ? propertyName : $"{fieldPath}.{propertyName}";
105105

106-
// Check if this property has x-metadata annotation
107-
if (HasMetadataAnnotation(propertySchema))
106+
// Check if this property is marked for metadata extraction
107+
if (propertySchema.TryGetProperty("x-metadata", out var metadataFlag) &&
108+
metadataFlag.ValueKind == JsonValueKind.True)
108109
{
109110
config.FieldsToExtract.Add(currentPath);
110-
config.FieldTypes[currentPath] = MapSchemaTypeToDataType(propertySchema);
111+
112+
// Determine data type from schema
113+
var dataType = GetDataTypeFromSchema(propertySchema);
114+
config.FieldTypes[currentPath] = dataType;
111115
}
112116

113117
// Recursively process nested objects
114-
if (IsObjectType(propertySchema) && propertySchema.TryGetProperty("properties", out _))
118+
if (propertySchema.TryGetProperty("type", out var typeElement) &&
119+
typeElement.ValueKind == JsonValueKind.String &&
120+
typeElement.GetString() == "object")
115121
{
116122
ExtractMetadataFromSchema(propertySchema, config, currentPath);
117123
}
118124
}
119125
}
120126

121127
/// <summary>
122-
/// Checks if a JSON schema property has the x-metadata custom annotation set to true.
128+
/// Determines the data type for metadata storage based on JSON schema type definition.
123129
/// </summary>
124-
private bool HasMetadataAnnotation(JsonElement propertySchema)
130+
private string GetDataTypeFromSchema(JsonElement propertySchema)
125131
{
126-
if (propertySchema.TryGetProperty("x-metadata", out var metadataElement))
132+
if (!propertySchema.TryGetProperty("type", out var typeElement))
133+
return "string";
134+
135+
// Handle array of types (e.g., ["string", "null"])
136+
if (typeElement.ValueKind == JsonValueKind.Array)
137+
{
138+
foreach (var arrayElement in typeElement.EnumerateArray())
139+
{
140+
if (arrayElement.ValueKind == JsonValueKind.String)
141+
{
142+
var typeString = arrayElement.GetString();
143+
if (typeString != "null")
144+
{
145+
return MapSchemaTypeToDataType(typeString, propertySchema);
146+
}
147+
}
148+
}
149+
return "string"; // Default fallback
150+
}
151+
152+
// Handle single type
153+
if (typeElement.ValueKind == JsonValueKind.String)
127154
{
128-
return metadataElement.ValueKind == JsonValueKind.True;
155+
var typeString = typeElement.GetString();
156+
return MapSchemaTypeToDataType(typeString, propertySchema);
129157
}
130-
return false;
158+
159+
return "string"; // Default fallback
131160
}
132161

133162
/// <summary>
134-
/// Maps JSON schema types to BusinessEventMetadata DataType values.
135-
/// Supports string, number, boolean, and date types.
163+
/// Maps JSON schema types to metadata storage types.
136164
/// </summary>
137-
private string MapSchemaTypeToDataType(JsonElement propertySchema)
165+
private string MapSchemaTypeToDataType(string schemaType, JsonElement propertySchema)
138166
{
139-
if (!propertySchema.TryGetProperty("type", out var typeElement))
140-
return "string"; // Default fallback
141-
142-
var schemaType = typeElement.GetString();
143-
144-
return schemaType switch
167+
return schemaType?.ToLower() switch
145168
{
146-
"string" when IsDateTimeFormat(propertySchema) => "date",
147-
"string" => "string",
148-
"number" or "integer" => "number",
169+
"string" => IsDateTimeFormat(propertySchema) ? "date" : "string",
170+
"number" => "number",
171+
"integer" => "number",
149172
"boolean" => "boolean",
150-
_ => "string" // Default fallback
173+
_ => "string"
151174
};
152175
}
153176

154177
/// <summary>
155-
/// Determines if a string property represents a date/time value based on format annotations.
178+
/// Checks if a string property has a date/time format annotation.
156179
/// </summary>
157180
private bool IsDateTimeFormat(JsonElement propertySchema)
158181
{
159-
if (propertySchema.TryGetProperty("format", out var formatElement))
182+
if (propertySchema.TryGetProperty("format", out var formatElement) &&
183+
formatElement.ValueKind == JsonValueKind.String)
160184
{
161185
var format = formatElement.GetString();
162-
return format == "date-time" || format == "date" || format == "time";
163-
}
164-
return false;
165-
}
166-
167-
/// <summary>
168-
/// Determines if a JSON schema element represents an object type.
169-
/// </summary>
170-
private bool IsObjectType(JsonElement propertySchema)
171-
{
172-
if (propertySchema.TryGetProperty("type", out var typeElement))
173-
{
174-
return typeElement.GetString() == "object";
186+
return format is "date" or "date-time" or "time";
175187
}
176188
return false;
177189
}

0 commit comments

Comments
 (0)