From df08b248a494409698aa82753f8a91f4c232e234 Mon Sep 17 00:00:00 2001 From: Maxwell Weru Date: Tue, 21 Nov 2023 18:36:06 +0300 Subject: [PATCH] Simplify models for IoTHub support hence support trimming --- samples/AzureIotHub/AzureIotEventsConsumer.cs | 52 ++-- samples/AzureIotHub/MyIotHubEvent.cs | 10 +- .../VehicleTelemetryEvent.cs | 10 +- .../VehicleTelemetryEventsConsumer.cs | 5 +- ...ureEventHubsEventRegistrationExtensions.cs | 4 +- .../EventDataExtensions.cs | 13 + .../IotHub/IotHubEvent.cs | 291 ++++++++++++++---- .../IotHub/IotHubEventMessageSource.cs | 17 - .../IotHub/IotHubEventSerializer.cs | 122 ++------ .../IotHub/IotHubJsonSerializerContext.cs | 1 + .../IotHub/IotHubOperationalEvent.cs | 124 -------- .../IotHub/IotHubOperationalEventType.cs | 23 -- .../IotHub/TypeExtensions.cs | 23 -- .../MessageStrings.cs | 4 +- .../IotHubEventSerializerTests.cs | 99 +++--- 15 files changed, 337 insertions(+), 461 deletions(-) delete mode 100644 src/Tingle.EventBus.Transports.Azure.EventHubs/IotHub/IotHubEventMessageSource.cs delete mode 100644 src/Tingle.EventBus.Transports.Azure.EventHubs/IotHub/IotHubOperationalEvent.cs delete mode 100644 src/Tingle.EventBus.Transports.Azure.EventHubs/IotHub/IotHubOperationalEventType.cs delete mode 100644 src/Tingle.EventBus.Transports.Azure.EventHubs/IotHub/TypeExtensions.cs diff --git a/samples/AzureIotHub/AzureIotEventsConsumer.cs b/samples/AzureIotHub/AzureIotEventsConsumer.cs index 3d9546a5..4a0c6d88 100644 --- a/samples/AzureIotHub/AzureIotEventsConsumer.cs +++ b/samples/AzureIotHub/AzureIotEventsConsumer.cs @@ -19,49 +19,35 @@ public AzureIotEventsConsumer(ILogger logger) public Task ConsumeAsync(EventContext context, CancellationToken cancellationToken) { var evt = context.Event; - var source = evt.Source; - if (source == IotHubEventMessageSource.Telemetry) + if (evt.IsTelemetry) { - var telemetry = evt.Telemetry!; + var telemetry = evt.GetTelemetry(); var deviceId = context.GetIotHubDeviceId(); var enqueued = context.GetIotHubEnqueuedTime(); - logger.LogInformation("Received {Source} from {DeviceId}\r\nEnqueued: {EnqueuedTime}\r\nTimestamped: {Timestamp}\r\nTelemetry:{Telemetry}", - source, + logger.LogInformation("Received Telemetry from {DeviceId}\r\nEnqueued: {EnqueuedTime}\r\nTimestamped: {Timestamp}\r\nTelemetry:{Telemetry}", deviceId, enqueued, telemetry.Timestamp, JsonSerializer.Serialize(telemetry, serializerOptions)); } - else if (source == IotHubEventMessageSource.TwinChangeEvents) + else { - var @tce = evt.TwinEvent!; - logger.LogInformation("TwinChange event received of type '{Type}' from '{DeviceId}{ModuleId}' in '{HubName}'.\r\nEvent:{Event}", - tce.Type, - tce.DeviceId, - tce.ModuleId, - tce.HubName, - JsonSerializer.Serialize(tce.Event, serializerOptions)); - } - else if (source == IotHubEventMessageSource.DeviceLifecycleEvents) - { - var lce = evt.LifecycleEvent!; - logger.LogInformation("Device Lifecycle event received of type '{Type}' from '{DeviceId}{ModuleId}' in '{HubName}'.\r\nEvent:{Event}", - lce.Type, - lce.DeviceId, - lce.ModuleId, - lce.HubName, - JsonSerializer.Serialize(lce.Event, serializerOptions)); - } - else if (source == IotHubEventMessageSource.DeviceConnectionStateEvents) - { - var cse = evt.ConnectionStateEvent!; - logger.LogInformation("Device connection state event received of type '{Type}' from '{DeviceId}{ModuleId}' in '{HubName}'.\r\nEvent:{Event}", - cse.Type, - cse.DeviceId, - cse.ModuleId, - cse.HubName, - JsonSerializer.Serialize(cse.Event, serializerOptions)); + var prefix = evt.Source switch + { + IotHubEventMessageSource.TwinChangeEvents => "TwinChange", + IotHubEventMessageSource.DeviceLifecycleEvents => "Device Lifecycle", + IotHubEventMessageSource.DeviceConnectionStateEvents => "Device connection state", + _ => throw new InvalidOperationException($"Unknown event source '{evt.Source}'."), + }; + var ope = evt.Event; + logger.LogInformation("{Prefix} event received of type '{Type}' from '{DeviceId}{ModuleId}' in '{HubName}'.\r\nEvent:{Event}", + prefix, + ope.Type, + ope.DeviceId, + ope.ModuleId, + ope.HubName, + JsonSerializer.Serialize(ope.Payload, serializerOptions)); } return Task.CompletedTask; diff --git a/samples/AzureIotHub/MyIotHubEvent.cs b/samples/AzureIotHub/MyIotHubEvent.cs index c9e09780..12173094 100644 --- a/samples/AzureIotHub/MyIotHubEvent.cs +++ b/samples/AzureIotHub/MyIotHubEvent.cs @@ -3,15 +3,7 @@ namespace AzureIotHub; -public record MyIotHubEvent : IotHubEvent -{ - public MyIotHubEvent(IotHubEventMessageSource source, - MyIotHubTelemetry? telemetry, - IotHubOperationalEvent? twinEvent, - IotHubOperationalEvent? lifecycleEvent, - IotHubOperationalEvent? connectionStateEvent) - : base(source, telemetry, twinEvent, lifecycleEvent, connectionStateEvent) { } -} +public record MyIotHubEvent : IotHubEvent { } public class MyIotHubTelemetry { diff --git a/samples/MultipleDifferentTransports/VehicleTelemetryEvent.cs b/samples/MultipleDifferentTransports/VehicleTelemetryEvent.cs index a7a0be85..691a869c 100644 --- a/samples/MultipleDifferentTransports/VehicleTelemetryEvent.cs +++ b/samples/MultipleDifferentTransports/VehicleTelemetryEvent.cs @@ -3,15 +3,7 @@ namespace MultipleDifferentTransports; -internal record VehicleTelemetryEvent : IotHubEvent -{ - public VehicleTelemetryEvent(IotHubEventMessageSource source, - VehicleTelemetry? telemetry, - IotHubOperationalEvent? twinEvent, - IotHubOperationalEvent? lifecycleEvent, - IotHubOperationalEvent? connectionStateEvent) - : base(source, telemetry, twinEvent, lifecycleEvent, connectionStateEvent) { } -} +internal record VehicleTelemetryEvent : IotHubEvent { } internal class VehicleTelemetry { diff --git a/samples/MultipleDifferentTransports/VehicleTelemetryEventsConsumer.cs b/samples/MultipleDifferentTransports/VehicleTelemetryEventsConsumer.cs index e424165f..34d5112d 100644 --- a/samples/MultipleDifferentTransports/VehicleTelemetryEventsConsumer.cs +++ b/samples/MultipleDifferentTransports/VehicleTelemetryEventsConsumer.cs @@ -16,10 +16,9 @@ public VehicleTelemetryEventsConsumer(ILogger lo public async Task ConsumeAsync(EventContext context, CancellationToken cancellationToken) { var evt = context.Event; - var source = evt.Source; - if (source != IotHubEventMessageSource.Telemetry) return; + if (!evt.IsTelemetry) return; - var telemetry = evt.Telemetry!; + var telemetry = evt.GetTelemetry(); var action = telemetry.Action; if (action is not "door-status-changed") { diff --git a/src/Tingle.EventBus.Transports.Azure.EventHubs/AzureEventHubsEventRegistrationExtensions.cs b/src/Tingle.EventBus.Transports.Azure.EventHubs/AzureEventHubsEventRegistrationExtensions.cs index 72952285..e3949e0d 100644 --- a/src/Tingle.EventBus.Transports.Azure.EventHubs/AzureEventHubsEventRegistrationExtensions.cs +++ b/src/Tingle.EventBus.Transports.Azure.EventHubs/AzureEventHubsEventRegistrationExtensions.cs @@ -32,13 +32,11 @@ public static EventRegistration ConfigureAsIotHubEvent(this EventRegistration re } /// - /// Use the serializer that supports . + /// Use the serializer that supports . /// /// The to configure. /// /// - [RequiresDynamicCode(MessageStrings.JsonSerializationRequiresDynamicCodeMessage)] - [RequiresUnreferencedCode(MessageStrings.JsonSerializationUnreferencedCodeMessage)] public static EventRegistration UseIotHubEventSerializer(this EventRegistration registration) { if (registration is null) throw new ArgumentNullException(nameof(registration)); diff --git a/src/Tingle.EventBus.Transports.Azure.EventHubs/EventDataExtensions.cs b/src/Tingle.EventBus.Transports.Azure.EventHubs/EventDataExtensions.cs index 6280241a..a63f8c16 100644 --- a/src/Tingle.EventBus.Transports.Azure.EventHubs/EventDataExtensions.cs +++ b/src/Tingle.EventBus.Transports.Azure.EventHubs/EventDataExtensions.cs @@ -65,4 +65,17 @@ public static bool TryGetPropertyValue(this EventData data, string key, [NotN { return data.TryGetPropertyValue(key, out var value) ? value : default; } + + /// + /// Gets the required property value that is associated with the specified key from + /// or . + /// + /// + /// The instance to use. + /// The key to locate. + /// + public static T GetRequiredPropertyValue(this EventData data, string key) where T : IConvertible + { + return data.GetPropertyValue(key) ?? throw new InvalidOperationException($"The property '{key}' could not be found."); + } } diff --git a/src/Tingle.EventBus.Transports.Azure.EventHubs/IotHub/IotHubEvent.cs b/src/Tingle.EventBus.Transports.Azure.EventHubs/IotHub/IotHubEvent.cs index ce83651d..50ed356c 100644 --- a/src/Tingle.EventBus.Transports.Azure.EventHubs/IotHub/IotHubEvent.cs +++ b/src/Tingle.EventBus.Transports.Azure.EventHubs/IotHub/IotHubEvent.cs @@ -1,82 +1,249 @@ -namespace Tingle.EventBus.Transports.Azure.EventHubs.IotHub; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; + +namespace Tingle.EventBus.Transports.Azure.EventHubs.IotHub; /// Represents an event from Azure IoT Hub. -public record IotHubEvent : IotHubEvent - where TDeviceTelemetry : class +public record IotHubEvent { - /// - /// Creates an instance of . - /// - /// - /// The telemetry data. - /// The twin change event. - /// The lifecycle event. - /// The connection state event. - public IotHubEvent(IotHubEventMessageSource source, - TDeviceTelemetry? telemetry, - IotHubOperationalEvent? twinEvent, - IotHubOperationalEvent? lifecycleEvent, - IotHubOperationalEvent? connectionStateEvent) - : base(source, telemetry, twinEvent, lifecycleEvent, connectionStateEvent) { } -} + /// The source of the event. + public IotHubEventMessageSource Source { get; internal set; } -/// -/// Represents the event from Azure IoT Hub -/// -public record IotHubEvent - where TDeviceTelemetry : class - where TDeviceTwinChange : IotHubDeviceTwinChangeEvent - where TDeviceLifecycle : IotHubDeviceLifecycleEvent -{ - /// - /// Creates an instance of . - /// - /// - /// The telemetry data. - /// The twin change event. - /// The lifecycle event. - /// The connection state event. - public IotHubEvent(IotHubEventMessageSource source, - TDeviceTelemetry? telemetry, - IotHubOperationalEvent? twinEvent, - IotHubOperationalEvent? lifecycleEvent, - IotHubOperationalEvent? connectionStateEvent) - { - Source = source; - Telemetry = telemetry; - TwinEvent = twinEvent; - LifecycleEvent = lifecycleEvent; - ConnectionStateEvent = connectionStateEvent; - } + /// Whether the event is a telemetry event. + [MemberNotNullWhen(true, nameof(Telemetry))] + [MemberNotNullWhen(false, nameof(Event))] + public bool IsTelemetry => Source is IotHubEventMessageSource.Telemetry; - /// The source of the event. - public IotHubEventMessageSource Source { get; } + /// Whether the event is a twin change event. + [MemberNotNullWhen(false, nameof(Telemetry))] + [MemberNotNullWhen(true, nameof(Event))] + public bool IsTwinEvent => Source is IotHubEventMessageSource.TwinChangeEvents; + + /// Whether the event is a lifecycle event. + [MemberNotNullWhen(false, nameof(Telemetry))] + [MemberNotNullWhen(true, nameof(Event))] + public bool IsLifecycleEvent => Source is IotHubEventMessageSource.DeviceLifecycleEvents; + + /// Whether the event is a connection state event. + [MemberNotNullWhen(false, nameof(Telemetry))] + [MemberNotNullWhen(true, nameof(Event))] + public bool IsConnectionStateEvent => Source is IotHubEventMessageSource.DeviceConnectionStateEvents; /// /// The telemetry data. /// Only populate when is set to /// . /// - public TDeviceTelemetry? Telemetry { get; } + public JsonNode? Telemetry { get; internal set; } /// - /// The twin change event. + /// The connection state event. /// Only populate when is set to - /// . + /// . /// - public IotHubOperationalEvent? TwinEvent { get; } + public IotHubOperationalEvent? Event { get; internal set; } - /// - /// The lifecycle event. - /// Only populate when is set to - /// . - /// - public IotHubOperationalEvent? LifecycleEvent { get; } + #region Telemetry to other types + + /// Create a from the template's backing object. + /// The type to deserialize the template into. + /// Options to control the conversion behavior. + /// A representation of the template. + /// + /// The model does not container a backing object. + /// + /// + /// There is no compatible + /// for or its serializable members. + /// + [RequiresUnreferencedCode(MessageStrings.SerializationUnreferencedCodeMessage)] + [RequiresDynamicCode(MessageStrings.SerializationRequiresDynamicCodeMessage)] + public TValue GetTelemetry(JsonSerializerOptions? options = null) + { + if (Telemetry is null) throw new InvalidOperationException("Telemetry is null. This method can only be called when the source is telemetry"); + + return JsonSerializer.Deserialize(Telemetry, options: options) ?? throw new InvalidOperationException($"The telemetry could not be deserialized to '{typeof(TValue)}'."); + } + + /// Create a from the template's backing object. + /// The type to deserialize the template into. + /// Metadata about the type to convert. + /// + /// The model does not container a backing object. + /// + /// + /// is . + /// + /// + /// is not compatible with the JSON. + /// + /// + /// There is no compatible + /// for or its serializable members. + /// + public TValue GetTelemetry(JsonTypeInfo jsonTypeInfo) + { + if (Telemetry is null) throw new InvalidOperationException("Telemetry is null. This method can only be called when the source is telemetry"); + + return JsonSerializer.Deserialize(Telemetry, jsonTypeInfo: jsonTypeInfo) ?? throw new InvalidOperationException($"The telemetry could not be deserialized to '{typeof(TValue)}'."); + } /// - /// The connection state event. - /// Only populate when is set to - /// . + /// Converts the template's backing object into a . /// - public IotHubOperationalEvent? ConnectionStateEvent { get; } + /// The type of the object to convert to and return. + /// A metadata provider for serializable types. + /// A representation of the JSON value. + /// + /// is . + /// + /// -or- + /// + /// is . + /// + /// + /// The JSON is invalid. + /// + /// -or- + /// + /// is not compatible with the JSON. + /// + /// -or- + /// + /// There is remaining data in the string beyond a single JSON value. + /// + /// There is no compatible + /// for or its serializable members. + /// + /// + /// The method of the provided + /// returns for the type to convert. + /// + public object GetTelemetry(Type returnType, JsonSerializerContext context) + { + if (Telemetry is null) throw new InvalidOperationException("Telemetry is null. This method can only be called when the source is telemetry"); + + return JsonSerializer.Deserialize(Telemetry, returnType: returnType, context: context) ?? throw new InvalidOperationException($"The telemetry could not be deserialized to '{returnType}'."); + } + + #endregion +} + +/// The kind of source for an Azure IoT Hub Event. +public enum IotHubEventMessageSource +{ + /// + Telemetry, + + /// + TwinChangeEvents, + + /// + DeviceLifecycleEvents, + + /// + DeviceConnectionStateEvents, +} + +/// Represents an operational event from Azure IoT Hub. +public sealed record IotHubOperationalEvent +{ + /// The name of the hub where the event happened. + public required string HubName { get; init; } + + /// Unique identifier of the device. + public required string DeviceId { get; init; } + + /// Unique identifier of the module within the device. + public string? ModuleId { get; init; } + + /// Type of operational event. + public required IotHubOperationalEventType Type { get; init; } + + /// Time when the operation was done. + public string? OperationTimestamp { get; init; } + + /// The actual event. + public required IotHubOperationalEventPayload Payload { get; init; } +} + +/// Abstractions for an Azure IoT Hub Event. +public record IotHubOperationalEventPayload +{ + /// Unique identifier of the device. + [JsonPropertyName("deviceId")] + public string? DeviceId { get; set; } + + /// Unique identifier of the module within the device. + [JsonPropertyName("moduleId")] + public string? ModuleId { get; set; } + + /// + [JsonPropertyName("etag")] + public string? Etag { get; set; } + + /// The version. + [JsonPropertyName("version")] + public long Version { get; set; } + + /// The twin properties. + [JsonPropertyName("properties")] + public IotHubTwinPropertiesCollection? Properties { get; set; } + + /// + [JsonPropertyName("sequenceNumber")] + public string? SequenceNumber { get; set; } + + /// + [JsonExtensionData] + public Dictionary? Extras { get; set; } +} + +/// Properties of the device twin. +public record IotHubTwinPropertiesCollection +{ + /// + [JsonPropertyName("desired")] + public IotHubTwinProperties? Desired { get; set; } + + /// + [JsonPropertyName("reported")] + public IotHubTwinProperties? Reported { get; set; } +} + +/// Properties of the device twin. +public record IotHubTwinProperties +{ + /// The version of the twin. + [JsonPropertyName("$version")] + public long Version { get; set; } + + /// + [JsonExtensionData] + public Dictionary? Extras { get; set; } +} + +/// The type of an operational event on Azure IoT Hub. +public enum IotHubOperationalEventType +{ + /// + UpdateTwin, + + /// + ReplaceTwin, + + /// + CreateDeviceIdentity, + + /// + DeleteDeviceIdentity, + + /// + DeviceDisconnected, + + /// + DeviceConnected, } diff --git a/src/Tingle.EventBus.Transports.Azure.EventHubs/IotHub/IotHubEventMessageSource.cs b/src/Tingle.EventBus.Transports.Azure.EventHubs/IotHub/IotHubEventMessageSource.cs deleted file mode 100644 index f258b7a6..00000000 --- a/src/Tingle.EventBus.Transports.Azure.EventHubs/IotHub/IotHubEventMessageSource.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Tingle.EventBus.Transports.Azure.EventHubs.IotHub; - -/// The kind of source for an Azure IoT Hub Event. -public enum IotHubEventMessageSource -{ - /// - Telemetry, - - /// - TwinChangeEvents, - - /// - DeviceLifecycleEvents, - - /// - DeviceConnectionStateEvents, -} diff --git a/src/Tingle.EventBus.Transports.Azure.EventHubs/IotHub/IotHubEventSerializer.cs b/src/Tingle.EventBus.Transports.Azure.EventHubs/IotHub/IotHubEventSerializer.cs index 79ea5e96..0dcd8058 100644 --- a/src/Tingle.EventBus.Transports.Azure.EventHubs/IotHub/IotHubEventSerializer.cs +++ b/src/Tingle.EventBus.Transports.Azure.EventHubs/IotHub/IotHubEventSerializer.cs @@ -2,19 +2,16 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; using System.Text.Json; +using System.Text.Json.Nodes; using Tingle.EventBus.Serialization; +using SC = Tingle.EventBus.Transports.Azure.EventHubs.IotHub.IotHubJsonSerializerContext; namespace Tingle.EventBus.Transports.Azure.EventHubs.IotHub; -[RequiresDynamicCode(MessageStrings.JsonSerializationRequiresDynamicCodeMessage)] -[RequiresUnreferencedCode(MessageStrings.JsonSerializationUnreferencedCodeMessage)] internal class IotHubEventSerializer : AbstractEventSerializer { - private static readonly Type BaseType = typeof(IotHubEvent<,,>); - private static readonly ConcurrentDictionary typeMaps = new(); + private static readonly Type BaseType = typeof(IotHubEvent); public IotHubEventSerializer(IOptionsMonitor optionsAccessor, ILoggerFactory loggerFactory) @@ -29,75 +26,56 @@ public IotHubEventSerializer(IOptionsMonitor optio CancellationToken cancellationToken = default) { var targetType = typeof(T); - if (!BaseType.IsAssignableFromGeneric(targetType)) + if (!BaseType.IsAssignableFrom(targetType)) { throw new NotSupportedException($"Only events that inherit from '{BaseType.FullName}' are supported for deserialization."); } - var mapped = typeMaps.GetOrAdd(targetType, GetTargetTypes(targetType)); + var data = context.RawTransportData as EventData ?? throw new InvalidOperationException($"{nameof(context.RawTransportData)} cannot be null and must be an {nameof(EventData)}"); - var serializerOptions = OptionsAccessor.CurrentValue.SerializerOptions; - var data = (EventData)context.RawTransportData!; - object? telemetry = null, twinChange = null, lifecycle = null, connectionState = null; + JsonNode? telemetry = null; + IotHubOperationalEvent? opevent = null; var source = Enum.Parse(data.GetIotHubMessageSource()!, ignoreCase: true); if (source == IotHubEventMessageSource.Telemetry) { - telemetry = await JsonSerializer.DeserializeAsync(utf8Json: stream, - returnType: mapped.TelemetryType, - options: serializerOptions, - cancellationToken: cancellationToken).ConfigureAwait(false); + telemetry = await JsonNode.ParseAsync(utf8Json: stream, cancellationToken: cancellationToken).ConfigureAwait(false); + + //var @event = new IotHubEvent { Source = source, Telemetry = telemetry, }; + //return new EventEnvelope { Event = @event, }; } else if (source is IotHubEventMessageSource.TwinChangeEvents or IotHubEventMessageSource.DeviceLifecycleEvents or IotHubEventMessageSource.DeviceConnectionStateEvents) { - var hubName = data.GetPropertyValue("hubName"); - var deviceId = data.GetPropertyValue("deviceId"); + var hubName = data.GetRequiredPropertyValue("hubName"); + var deviceId = data.GetRequiredPropertyValue("deviceId"); var moduleId = data.GetPropertyValue("moduleId"); var operationType = data.GetPropertyValue("opType")!; var type = Enum.Parse(operationType, ignoreCase: true); var operationTimestamp = data.GetPropertyValue("operationTimestamp"); - if (source == IotHubEventMessageSource.TwinChangeEvents) - { - var twinChangeEvent = await JsonSerializer.DeserializeAsync(utf8Json: stream, - returnType: mapped.TwinChangeEventType, - options: serializerOptions, - cancellationToken: cancellationToken).ConfigureAwait(false); - - var twinChangeOpEventType = typeof(IotHubOperationalEvent<>).MakeGenericType(mapped.TwinChangeEventType); - twinChange = Activator.CreateInstance(twinChangeOpEventType, new[] { hubName, deviceId, moduleId, type, operationTimestamp, twinChangeEvent, }); - } - else if (source == IotHubEventMessageSource.DeviceLifecycleEvents) - { - var lifecycleEvent = await JsonSerializer.DeserializeAsync(utf8Json: stream, - returnType: mapped.LifecycleEventType, - options: serializerOptions, - cancellationToken: cancellationToken).ConfigureAwait(false); + var payload = await JsonSerializer.DeserializeAsync(stream, SC.Default.IotHubOperationalEventPayload, cancellationToken).ConfigureAwait(false) + ?? throw new InvalidOperationException($"The payload of the event could not be deserialized to '{nameof(IotHubOperationalEventPayload)}'."); - var lifecycleOpEventType = typeof(IotHubOperationalEvent<>).MakeGenericType(mapped.LifecycleEventType); - lifecycle = Activator.CreateInstance(lifecycleOpEventType, new[] { hubName, deviceId, moduleId, type, operationTimestamp, lifecycleEvent, }); - } - else if (source == IotHubEventMessageSource.DeviceConnectionStateEvents) + opevent = new IotHubOperationalEvent { - var connectionStateEvent = await JsonSerializer.DeserializeAsync( - utf8Json: stream, - options: serializerOptions, - cancellationToken: cancellationToken).ConfigureAwait(false); - - connectionState = new IotHubOperationalEvent( - hubName: hubName, - deviceId: deviceId, - moduleId: moduleId, - type: type, - operationTimestamp: operationTimestamp, - @event: connectionStateEvent!); - } + HubName = hubName, + DeviceId = deviceId, + ModuleId = moduleId, + Type = type, + OperationTimestamp = operationTimestamp, + Payload = payload, + }; + + //var @event = new IotHubEvent { Source = source, Event = opevent, }; } - var args = new object?[] { source, telemetry, twinChange, lifecycle, connectionState, }; - var @event = (T?)Activator.CreateInstance(targetType, args); + var @event = (T?)Activator.CreateInstance(targetType); + var ihe = @event as IotHubEvent ?? throw new InvalidOperationException($"The event of type '{targetType.FullName}' could not be cast to '{BaseType.FullName}'."); + ihe.Source = source; + ihe.Telemetry = telemetry; + ihe.Event = opevent; return new EventEnvelope { Event = @event, }; } @@ -108,44 +86,4 @@ protected override Task SerializeEnvelopeAsync(Stream stream, { throw new NotSupportedException("Serialization of IotHub events is not allowed."); } - - private static MappedTypes GetTargetTypes([NotNull] Type givenType) - { - if (givenType is null) throw new ArgumentNullException(nameof(givenType)); - - var baseType = givenType.BaseType; - while (baseType is not null) - { - if (baseType.IsGenericType && baseType.GetGenericTypeDefinition() == BaseType) - { - var abstractOne = baseType.GenericTypeArguments.FirstOrDefault(t => t.IsAbstract); - if (abstractOne is not null) - { - throw new InvalidOperationException($"Abstract type '{abstractOne.FullName}' on '{givenType.FullName}' is not supported for IotHub events."); - } - - return new(telemetryType: baseType.GenericTypeArguments[0], - twinChangeEventType: baseType.GenericTypeArguments[1], - lifecycleEventType: baseType.GenericTypeArguments[2]); - } - - baseType = baseType.BaseType; - } - - throw new InvalidOperationException("Reached the end but could not get the inner types. This should not happen. Report it."); - } - - private record MappedTypes - { - public MappedTypes(Type telemetryType, Type twinChangeEventType, Type lifecycleEventType) - { - TelemetryType = telemetryType ?? throw new ArgumentNullException(nameof(telemetryType)); - TwinChangeEventType = twinChangeEventType ?? throw new ArgumentNullException(nameof(twinChangeEventType)); - LifecycleEventType = lifecycleEventType ?? throw new ArgumentNullException(nameof(lifecycleEventType)); - } - - public Type TelemetryType { get; } - public Type TwinChangeEventType { get; } - public Type LifecycleEventType { get; } - } } diff --git a/src/Tingle.EventBus.Transports.Azure.EventHubs/IotHub/IotHubJsonSerializerContext.cs b/src/Tingle.EventBus.Transports.Azure.EventHubs/IotHub/IotHubJsonSerializerContext.cs index 8c46c0f3..5ee11353 100644 --- a/src/Tingle.EventBus.Transports.Azure.EventHubs/IotHub/IotHubJsonSerializerContext.cs +++ b/src/Tingle.EventBus.Transports.Azure.EventHubs/IotHub/IotHubJsonSerializerContext.cs @@ -3,4 +3,5 @@ namespace Tingle.EventBus.Transports.Azure.EventHubs.IotHub; [JsonSerializable(typeof(IotHubConnectionAuthMethod))] +[JsonSerializable(typeof(IotHubOperationalEventPayload))] internal partial class IotHubJsonSerializerContext : JsonSerializerContext { } diff --git a/src/Tingle.EventBus.Transports.Azure.EventHubs/IotHub/IotHubOperationalEvent.cs b/src/Tingle.EventBus.Transports.Azure.EventHubs/IotHub/IotHubOperationalEvent.cs deleted file mode 100644 index 3c24431b..00000000 --- a/src/Tingle.EventBus.Transports.Azure.EventHubs/IotHub/IotHubOperationalEvent.cs +++ /dev/null @@ -1,124 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Tingle.EventBus.Transports.Azure.EventHubs.IotHub; - -/// -/// Details about an operational event from Azure IoT Hub. -/// -public sealed record IotHubOperationalEvent -{ - /// - /// Creates an instance of . - /// - /// The name of the hub where the event happened. - /// Unique identifier of the device. - /// Unique identifier of the module within the device. - /// Type of operational event. - /// Time when the operation was done. - /// The actual event. - public IotHubOperationalEvent(string? hubName, string? deviceId, string? moduleId, IotHubOperationalEventType type, string? operationTimestamp, T @event) - { - HubName = hubName; - DeviceId = deviceId; - ModuleId = moduleId; - Type = type; - OperationTimestamp = operationTimestamp; - Event = @event; - } - - /// The name of the hub where the event happened. - public string? HubName { get; } - - /// Unique identifier of the device. - public string? DeviceId { get; } - - /// Unique identifier of the module within the device. - public string? ModuleId { get; } - - /// Type of operational event. - public IotHubOperationalEventType Type { get; } - - /// Time when the operation was done. - public string? OperationTimestamp { get; } - - /// The actual event. - public T Event { get; } -} - -/// -/// Basics of a device lifecycle event. -/// -public record IotHubDeviceLifecycleEvent: AbstractIotHubEvent -{ - /// - [JsonExtensionData] - public Dictionary? Extras { get; set; } -} - -/// -/// Basics of a device twin change event. -/// -public record IotHubDeviceTwinChangeEvent : AbstractIotHubEvent -{ - /// - [JsonExtensionData] - public Dictionary? Extras { get; set; } -} - -/// -/// Basics of a device twin change event. -/// -public record IotHubDeviceConnectionStateEvent -{ - /// - [JsonPropertyName("sequenceNumber")] - public string? SequenceNumber { get; set; } -} - -/// Abstractions for an Azure IoT Hub Event. -public abstract record AbstractIotHubEvent -{ - /// Unique identifier of the device. - [JsonPropertyName("deviceId")] - public string? DeviceId { get; set; } - - /// Unique identifier of the module within the device. - [JsonPropertyName("moduleId")] - public string? ModuleId { get; set; } - - /// - [JsonPropertyName("etag")] - public string? Etag { get; set; } - - /// The version. - [JsonPropertyName("version")] - public long Version { get; set; } - - /// The twin properties. - [JsonPropertyName("properties")] - public IotHubTwinPropertiesCollection? Properties { get; set; } -} - -/// Properties of the device twin. -public record IotHubTwinPropertiesCollection -{ - /// - [JsonPropertyName("desired")] - public IotHubTwinProperties? Desired { get; set; } - - /// - [JsonPropertyName("reported")] - public IotHubTwinProperties? Reported { get; set; } -} - -/// Properties of the device twin. -public record IotHubTwinProperties -{ - /// The version of the twin. - [JsonPropertyName("$version")] - public long Version { get; set; } - - /// - [JsonExtensionData] - public Dictionary? Extras { get; set; } -} diff --git a/src/Tingle.EventBus.Transports.Azure.EventHubs/IotHub/IotHubOperationalEventType.cs b/src/Tingle.EventBus.Transports.Azure.EventHubs/IotHub/IotHubOperationalEventType.cs deleted file mode 100644 index db4db9e0..00000000 --- a/src/Tingle.EventBus.Transports.Azure.EventHubs/IotHub/IotHubOperationalEventType.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Tingle.EventBus.Transports.Azure.EventHubs.IotHub; - -/// The type of an operational event on Azure IoT Hub. -public enum IotHubOperationalEventType -{ - /// - UpdateTwin, - - /// - ReplaceTwin, - - /// - CreateDeviceIdentity, - - /// - DeleteDeviceIdentity, - - /// - DeviceDisconnected, - - /// - DeviceConnected, -} diff --git a/src/Tingle.EventBus.Transports.Azure.EventHubs/IotHub/TypeExtensions.cs b/src/Tingle.EventBus.Transports.Azure.EventHubs/IotHub/TypeExtensions.cs deleted file mode 100644 index 239f1557..00000000 --- a/src/Tingle.EventBus.Transports.Azure.EventHubs/IotHub/TypeExtensions.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace System; - -internal static class TypeExtensions -{ - public static bool IsAssignableFromGeneric(this Type genericType, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type givenType) - { - var interfaceTypes = givenType.GetInterfaces(); - - foreach (var it in interfaceTypes) - { - if (it.IsGenericType && it.GetGenericTypeDefinition() == genericType) - return true; - } - - if (givenType.IsGenericType && givenType.GetGenericTypeDefinition() == genericType) - return true; - - var baseType = givenType.BaseType; - return baseType != null && genericType.IsAssignableFromGeneric(baseType); - } -} diff --git a/src/Tingle.EventBus.Transports.Azure.EventHubs/MessageStrings.cs b/src/Tingle.EventBus.Transports.Azure.EventHubs/MessageStrings.cs index 1a39cf8a..26223333 100644 --- a/src/Tingle.EventBus.Transports.Azure.EventHubs/MessageStrings.cs +++ b/src/Tingle.EventBus.Transports.Azure.EventHubs/MessageStrings.cs @@ -2,6 +2,6 @@ internal class MessageStrings { - public const string JsonSerializationUnreferencedCodeMessage = "JSON serialization and deserialization might require types that cannot be statically analyzed. Use the serialziers and types that takes a JsonSerializerContext, or make sure all of the required types are preserved."; - public const string JsonSerializationRequiresDynamicCodeMessage = "JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications."; + public const string SerializationUnreferencedCodeMessage = "JSON serialization and deserialization might require types that cannot be statically analyzed. Use the serialziers and types that takes a JsonSerializerContext, or make sure all of the required types are preserved."; + public const string SerializationRequiresDynamicCodeMessage = "JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications."; } diff --git a/tests/Tingle.EventBus.Transports.Azure.EventHubs.Tests/IotHubEventSerializerTests.cs b/tests/Tingle.EventBus.Transports.Azure.EventHubs.Tests/IotHubEventSerializerTests.cs index a158ab4d..b44b4650 100644 --- a/tests/Tingle.EventBus.Transports.Azure.EventHubs.Tests/IotHubEventSerializerTests.cs +++ b/tests/Tingle.EventBus.Transports.Azure.EventHubs.Tests/IotHubEventSerializerTests.cs @@ -15,6 +15,8 @@ namespace Tingle.EventBus.Transports.Azure.EventHubs.Tests; public class IotHubEventSerializerTests { + private static readonly JsonSerializerOptions serializerOptions = new(JsonSerializerDefaults.Web); + private const string DeviceId = "1234567890"; private const string HubName = "iothub-route-test-weu-ih"; @@ -53,10 +55,8 @@ await TestSerializerAsync(async (provider, _, serializer) => var envelope = await serializer.DeserializeAsync(ctx); Assert.NotNull(envelope); Assert.NotNull(envelope.Event); - Assert.Null(envelope.Event.TwinEvent); - Assert.Null(envelope.Event.LifecycleEvent); - Assert.Null(envelope.Event.ConnectionStateEvent); - var telemetry = Assert.IsType(envelope.Event.Telemetry); + Assert.Null(envelope.Event.Event); + var telemetry = envelope.Event.GetTelemetry(serializerOptions); Assert.Equal(DateTimeOffset.Parse("2022-12-22T12:10:51Z"), telemetry.Timestamp); Assert.Equal(JsonSerializer.SerializeToNode(new { door = "frontLeft" })!.ToJsonString(), JsonSerializer.Serialize(telemetry.Extras)); }); @@ -81,19 +81,18 @@ await TestSerializerAsync(async (provider, _, serializer) => Assert.NotNull(envelope); Assert.NotNull(envelope.Event); Assert.Null(envelope.Event.Telemetry); - Assert.Null(envelope.Event.LifecycleEvent); - Assert.Null(envelope.Event.ConnectionStateEvent); - var twinEvent = Assert.IsType>(envelope.Event.TwinEvent); - Assert.Equal(HubName, twinEvent.HubName); - Assert.Equal(DeviceId, twinEvent.DeviceId); - Assert.Null(twinEvent.ModuleId); - Assert.Equal(IotHubOperationalEventType.UpdateTwin, twinEvent.Type); - Assert.Equal("2022-01-16T16:36:53.8146535Z", twinEvent.OperationTimestamp); - Assert.Null(twinEvent.Event.DeviceId); - Assert.Null(twinEvent.Event.ModuleId); - Assert.Null(twinEvent.Event.Etag); - Assert.Equal(3, twinEvent.Event.Version); - Assert.Equal(2, twinEvent.Event.Properties?.Desired?.Version); + var opevent = envelope.Event.Event; + Assert.NotNull(opevent); + Assert.Equal(HubName, opevent.HubName); + Assert.Equal(DeviceId, opevent.DeviceId); + Assert.Null(opevent.ModuleId); + Assert.Equal(IotHubOperationalEventType.UpdateTwin, opevent.Type); + Assert.Equal("2022-01-16T16:36:53.8146535Z", opevent.OperationTimestamp); + Assert.Null(opevent.Payload.DeviceId); + Assert.Null(opevent.Payload.ModuleId); + Assert.Null(opevent.Payload.Etag); + Assert.Equal(3, opevent.Payload.Version); + Assert.Equal(2, opevent.Payload.Properties?.Desired?.Version); }); } @@ -116,18 +115,17 @@ await TestSerializerAsync(async (provider, _, serializer) => Assert.NotNull(envelope); Assert.NotNull(envelope.Event); Assert.Null(envelope.Event.Telemetry); - Assert.Null(envelope.Event.TwinEvent); - Assert.Null(envelope.Event.ConnectionStateEvent); - var lifecycleEvent = Assert.IsType>(envelope.Event.LifecycleEvent); - Assert.Equal(HubName, lifecycleEvent.HubName); - Assert.Equal(DeviceId, lifecycleEvent.DeviceId); - Assert.Null(lifecycleEvent.ModuleId); - Assert.Equal(IotHubOperationalEventType.CreateDeviceIdentity, lifecycleEvent.Type); - Assert.Equal("2022-01-16T16:36:53.8146535Z", lifecycleEvent.OperationTimestamp); - Assert.Equal(DeviceId, lifecycleEvent.Event.DeviceId); - Assert.Null(lifecycleEvent.Event.ModuleId); - Assert.Equal("AAAAAAAAAAE=", lifecycleEvent.Event.Etag); - Assert.Equal(2, lifecycleEvent.Event.Version); + var opevent = envelope.Event.Event; + Assert.NotNull(opevent); + Assert.Equal(HubName, opevent.HubName); + Assert.Equal(DeviceId, opevent.DeviceId); + Assert.Null(opevent.ModuleId); + Assert.Equal(IotHubOperationalEventType.CreateDeviceIdentity, opevent.Type); + Assert.Equal("2022-01-16T16:36:53.8146535Z", opevent.OperationTimestamp); + Assert.Equal(DeviceId, opevent.Payload.DeviceId); + Assert.Null(opevent.Payload.ModuleId); + Assert.Equal("AAAAAAAAAAE=", opevent.Payload.Etag); + Assert.Equal(2, opevent.Payload.Version); }); } @@ -150,15 +148,14 @@ await TestSerializerAsync(async (provider, _, serializer) => Assert.NotNull(envelope); Assert.NotNull(envelope.Event); Assert.Null(envelope.Event.Telemetry); - Assert.Null(envelope.Event.TwinEvent); - Assert.Null(envelope.Event.LifecycleEvent); - var connectionStateEvent = Assert.IsType>(envelope.Event.ConnectionStateEvent); - Assert.Equal(HubName, connectionStateEvent.HubName); - Assert.Equal(DeviceId, connectionStateEvent.DeviceId); - Assert.Null(connectionStateEvent.ModuleId); - Assert.Equal(IotHubOperationalEventType.DeviceConnected, connectionStateEvent.Type); - Assert.Equal("2022-01-16T16:36:53.8146535Z", connectionStateEvent.OperationTimestamp); - Assert.Equal("000000000000000001D7F744182052190000000C000000000000000000000001", connectionStateEvent.Event.SequenceNumber); + var opevent = envelope.Event.Event; + Assert.NotNull(opevent); + Assert.Equal(HubName, opevent.HubName); + Assert.Equal(DeviceId, opevent.DeviceId); + Assert.Null(opevent.ModuleId); + Assert.Equal(IotHubOperationalEventType.DeviceConnected, opevent.Type); + Assert.Equal("2022-01-16T16:36:53.8146535Z", opevent.OperationTimestamp); + Assert.Equal("000000000000000001D7F744182052190000000C000000000000000000000001", opevent.Payload.SequenceNumber); }); } @@ -170,19 +167,7 @@ await TestSerializerAsync(async (provider, _, serializer) => var ereg = new EventRegistration(typeof(DummyEvent1)); var ctx = new DeserializationContext(BinaryData.FromString(""), ereg); var ex = await Assert.ThrowsAsync(() => serializer.DeserializeAsync(ctx)); - Assert.Equal("Only events that inherit from 'Tingle.EventBus.Transports.Azure.EventHubs.IotHub.IotHubEvent`3' are supported for deserialization.", ex.Message); - }); - } - - [Fact] - public async Task DeserializeAsync_Throws_NotSupportedException_For_AbstractTypes() - { - await TestSerializerAsync(async (provider, _, serializer) => - { - var ereg = new EventRegistration(typeof(DummyEvent2)); - var ctx = new DeserializationContext(BinaryData.FromString(""), ereg); - var ex = await Assert.ThrowsAsync(() => serializer.DeserializeAsync(ctx)); - Assert.Equal($"Abstract type '{typeof(DummyTelemetry1).FullName}' on '{typeof(DummyEvent2).FullName}' is not supported for IotHub events.", ex.Message); + Assert.Equal("Only events that inherit from 'Tingle.EventBus.Transports.Azure.EventHubs.IotHub.IotHubEvent' are supported for deserialization.", ex.Message); }); } @@ -192,7 +177,7 @@ public async Task SerializeAsync_Throws_NotSupportedException() await TestSerializerAsync(async (provider, publisher, serializer) => { var ereg = new EventRegistration(typeof(MyIotHubEvent)); - var context = new EventContext(publisher, new MyIotHubEvent(IotHubEventMessageSource.Telemetry, null, null, null, null)); + var context = new EventContext(publisher, new()); var ctx = new SerializationContext(context, ereg); var ex = await Assert.ThrowsAsync(() => serializer.SerializeAsync(ctx)); Assert.Equal("Serialization of IotHub events is not allowed.", ex.Message); @@ -231,13 +216,5 @@ private async Task TestSerializerAsync(Func - { - public DummyEvent2(IotHubEventMessageSource source, - DummyTelemetry1? telemetry, - IotHubOperationalEvent? twinEvent, - IotHubOperationalEvent? lifecycleEvent, - IotHubOperationalEvent? connectionStateEvent) - : base(source, telemetry, twinEvent, lifecycleEvent, connectionStateEvent) { } - } + record DummyEvent2 : IotHubEvent { } }