From 3de1b878d98c1d7ad5da6aedd7ea04a496ef6478 Mon Sep 17 00:00:00 2001 From: Jesse Squire Date: Tue, 8 Jun 2021 12:02:16 -0400 Subject: [PATCH] [Azure.Core.Amqp] Lazy Allocation for All Sections The focus of these changes is to enable lazy allocation for all sections of the `AmqpAnnotatedMessage` type, to match the AMQP spec definition. (see: http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#section-message-format) In order to allow downstream consumers to test if a section is populated without triggering allocation for the property, the `HasSection` member has been added. --- sdk/core/Azure.Core.Amqp/Azure.Core.Amqp.sln | 31 +++++++ sdk/core/Azure.Core.Amqp/CHANGELOG.md | 12 +-- .../api/Azure.Core.Amqp.netstandard2.0.cs | 11 +++ .../src/AmqpAnnotatedMessage.cs | 46 +++++++++- .../Azure.Core.Amqp/src/AmqpMessageSection.cs | 56 ++++++++++++ .../tests/AmqpAnnotatedMessageTests.cs | 89 +++++++++++++++++++ 6 files changed, 235 insertions(+), 10 deletions(-) create mode 100644 sdk/core/Azure.Core.Amqp/Azure.Core.Amqp.sln create mode 100644 sdk/core/Azure.Core.Amqp/src/AmqpMessageSection.cs diff --git a/sdk/core/Azure.Core.Amqp/Azure.Core.Amqp.sln b/sdk/core/Azure.Core.Amqp/Azure.Core.Amqp.sln new file mode 100644 index 0000000000000..97afa0ebc76df --- /dev/null +++ b/sdk/core/Azure.Core.Amqp/Azure.Core.Amqp.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31205.134 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.Core.Amqp", "src\Azure.Core.Amqp.csproj", "{B5AD38C8-DAB6-40E6-AB79-41A8ED9D297B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.Core.Amqp.Tests", "tests\Azure.Core.Amqp.Tests.csproj", "{61ABA08F-FA50-4DEA-8C97-00B7B057BDEF}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B5AD38C8-DAB6-40E6-AB79-41A8ED9D297B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B5AD38C8-DAB6-40E6-AB79-41A8ED9D297B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5AD38C8-DAB6-40E6-AB79-41A8ED9D297B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B5AD38C8-DAB6-40E6-AB79-41A8ED9D297B}.Release|Any CPU.Build.0 = Release|Any CPU + {61ABA08F-FA50-4DEA-8C97-00B7B057BDEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {61ABA08F-FA50-4DEA-8C97-00B7B057BDEF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {61ABA08F-FA50-4DEA-8C97-00B7B057BDEF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {61ABA08F-FA50-4DEA-8C97-00B7B057BDEF}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1DC074C7-A8F0-4778-B791-323040BDF4C5} + EndGlobalSection +EndGlobal diff --git a/sdk/core/Azure.Core.Amqp/CHANGELOG.md b/sdk/core/Azure.Core.Amqp/CHANGELOG.md index 0b4adb1ed855a..9d54c418574d3 100644 --- a/sdk/core/Azure.Core.Amqp/CHANGELOG.md +++ b/sdk/core/Azure.Core.Amqp/CHANGELOG.md @@ -2,14 +2,10 @@ ## 1.2.0-beta.1 (Unreleased) -### Features Added - -### Breaking Changes - -### Key Bugs Fixed - -### Fixed - +### Added +- All section properties of the `AmqpAnnotatedMessage` are now lazily allocated to reflect that they are defined as optional in the AMQP specification, section 3.2. + +- The `HasSection` method has been added to `AmqpAnnotatedMessage` to allow inspecting the property for a section to determine if it is populated without triggering an allocation. ## 1.1.0 (2021-06-16) diff --git a/sdk/core/Azure.Core.Amqp/api/Azure.Core.Amqp.netstandard2.0.cs b/sdk/core/Azure.Core.Amqp/api/Azure.Core.Amqp.netstandard2.0.cs index 58954deb428b7..758ad6f168a52 100644 --- a/sdk/core/Azure.Core.Amqp/api/Azure.Core.Amqp.netstandard2.0.cs +++ b/sdk/core/Azure.Core.Amqp/api/Azure.Core.Amqp.netstandard2.0.cs @@ -26,6 +26,7 @@ public AmqpAnnotatedMessage(Azure.Core.Amqp.AmqpMessageBody body) { } public Azure.Core.Amqp.AmqpMessageHeader Header { get { throw null; } } public System.Collections.Generic.IDictionary MessageAnnotations { get { throw null; } } public Azure.Core.Amqp.AmqpMessageProperties Properties { get { throw null; } } + public bool HasSection(Azure.Core.Amqp.AmqpMessageSection section) { throw null; } } public partial class AmqpMessageBody { @@ -87,4 +88,14 @@ internal AmqpMessageProperties() { } public Azure.Core.Amqp.AmqpAddress? To { get { throw null; } set { } } public System.ReadOnlyMemory? UserId { get { throw null; } set { } } } + public enum AmqpMessageSection + { + Header = 0, + DeliveryAnnotations = 1, + MessageAnnotations = 2, + Properties = 3, + ApplicationProperties = 4, + Body = 5, + Footer = 6, + } } diff --git a/sdk/core/Azure.Core.Amqp/src/AmqpAnnotatedMessage.cs b/sdk/core/Azure.Core.Amqp/src/AmqpAnnotatedMessage.cs index e1d51aac912aa..3bdc31c1bab19 100644 --- a/sdk/core/Azure.Core.Amqp/src/AmqpAnnotatedMessage.cs +++ b/sdk/core/Azure.Core.Amqp/src/AmqpAnnotatedMessage.cs @@ -27,7 +27,19 @@ public AmqpAnnotatedMessage(AmqpMessageBody body) /// Gets the header of the AMQP message. /// /// - public AmqpMessageHeader Header { get; } = new AmqpMessageHeader(); + public AmqpMessageHeader Header + { + get + { + if (_header == null) + { + _header = new AmqpMessageHeader(); + } + return _header; + } + } + + private AmqpMessageHeader? _header; /// /// Gets the footer of the AMQP message. @@ -86,7 +98,19 @@ public IDictionary MessageAnnotations /// /// Gets the properties of the AMQP message. /// - public AmqpMessageProperties Properties { get; } = new AmqpMessageProperties(); + public AmqpMessageProperties Properties + { + get + { + if (_properties == null) + { + _properties = new AmqpMessageProperties(); + } + return _properties; + } + } + + private AmqpMessageProperties? _properties; /// /// Gets the application properties of the AMQP message. @@ -111,5 +135,23 @@ public IDictionary ApplicationProperties /// /// public AmqpMessageBody Body { get; set; } + + /// + /// Determines whether the specified section is present for the AMQP message. + /// + /// The section to consider. + /// true if the specified is populated for the AMQP message; otherwise, false. + public bool HasSection(AmqpMessageSection section) => + section switch + { + AmqpMessageSection.Header => (_header != null), + AmqpMessageSection.DeliveryAnnotations => (_deliveryAnnotations != null), + AmqpMessageSection.MessageAnnotations => (_messageAnnotations != null), + AmqpMessageSection.Properties => (_properties != null), + AmqpMessageSection.ApplicationProperties => (_applicationProperties != null), + AmqpMessageSection.Body => (Body != null), + AmqpMessageSection.Footer => (_footer != null), + _ => throw new ArgumentException($"Unknown AMQP message section: { section }.", nameof(section)) + }; } } diff --git a/sdk/core/Azure.Core.Amqp/src/AmqpMessageSection.cs b/sdk/core/Azure.Core.Amqp/src/AmqpMessageSection.cs new file mode 100644 index 0000000000000..d7d74d5574f55 --- /dev/null +++ b/sdk/core/Azure.Core.Amqp/src/AmqpMessageSection.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Azure.Core.Amqp +{ + /// + /// Represents the sections of an AMQP message. + /// + /// + public enum AmqpMessageSection + { + /// + /// The header section of the message. + /// + /// + /// + Header, + + /// + /// The delivery annotations section of the message. + /// + /// + DeliveryAnnotations, + + /// + /// The message annotations section of the message. + /// + /// + MessageAnnotations, + + /// + /// The properties section of the message. + /// + /// + /// + Properties, + + /// + /// The application properties section of the message. + /// + /// + ApplicationProperties, + + /// + /// The body of the message, representing one specific sections. + /// + /// + Body, + + /// + /// The footer section of the message. + /// + /// + Footer + } +} diff --git a/sdk/core/Azure.Core.Amqp/tests/AmqpAnnotatedMessageTests.cs b/sdk/core/Azure.Core.Amqp/tests/AmqpAnnotatedMessageTests.cs index 6d23d07f1b94f..4c9407e32f02a 100644 --- a/sdk/core/Azure.Core.Amqp/tests/AmqpAnnotatedMessageTests.cs +++ b/sdk/core/Azure.Core.Amqp/tests/AmqpAnnotatedMessageTests.cs @@ -11,6 +11,8 @@ namespace Azure.Core.Amqp.Tests { public class AmqpAnnotatedMessageTests { + private static readonly AmqpMessageBody EmptyDataBody = AmqpMessageBody.FromData(new ReadOnlyMemory[] { Array.Empty() }); + [Test] public void CanCreateAnnotatedMessage() { @@ -65,5 +67,92 @@ public void CanCreateAnnotatedMessage() Assert.AreEqual("to", message.Properties.To.ToString()); Assert.AreEqual("userId", Encoding.UTF8.GetString(message.Properties.UserId.Value.ToArray())); } + + [Test] + public void HeaderIsCreatedOnDemand() + { + var message = new AmqpAnnotatedMessage(EmptyDataBody); + Assert.False(message.HasSection(AmqpMessageSection.Header)); + + message.Header.DeliveryCount = 99; + Assert.True(message.HasSection(AmqpMessageSection.Header)); + Assert.NotNull(message.Header); + } + + [Test] + public void DeliveryAnnotationsAreCreatedOnDemand() + { + var message = new AmqpAnnotatedMessage(EmptyDataBody); + Assert.False(message.HasSection(AmqpMessageSection.DeliveryAnnotations)); + + message.DeliveryAnnotations.Add("test", new object()); + Assert.True(message.HasSection(AmqpMessageSection.DeliveryAnnotations)); + Assert.NotNull(message.DeliveryAnnotations); + } + + [Test] + public void MessageAnnotationsAreCreatedOnDemand() + { + var message = new AmqpAnnotatedMessage(EmptyDataBody); + Assert.False(message.HasSection(AmqpMessageSection.MessageAnnotations)); + + message.MessageAnnotations.Add("test", new object()); + Assert.True(message.HasSection(AmqpMessageSection.MessageAnnotations)); + Assert.NotNull(message.MessageAnnotations); + } + + [Test] + public void PropertiesAreCreatedOnDemand() + { + var message = new AmqpAnnotatedMessage(EmptyDataBody); + Assert.False(message.HasSection(AmqpMessageSection.Properties)); + + message.Properties.ContentType = "test/unit"; + Assert.True(message.HasSection(AmqpMessageSection.Properties)); + Assert.NotNull(message.Properties); + } + + [Test] + public void ApplicationPropertiesAreCreatedOnDemand() + { + var message = new AmqpAnnotatedMessage(EmptyDataBody); + Assert.False(message.HasSection(AmqpMessageSection.ApplicationProperties)); + + message.ApplicationProperties.Add("test", new object()); + Assert.True(message.HasSection(AmqpMessageSection.ApplicationProperties)); + Assert.NotNull(message.ApplicationProperties); + } + + [Test] + public void FooterIsCreatedOnDemand() + { + var message = new AmqpAnnotatedMessage(EmptyDataBody); + Assert.False(message.HasSection(AmqpMessageSection.Footer)); + + message.Footer.Add("test", new object()); + Assert.True(message.HasSection(AmqpMessageSection.Footer)); + Assert.NotNull(message.Footer); + } + + [Test] + public void BodyIsDetectedByHasSection() + { + var message = new AmqpAnnotatedMessage(EmptyDataBody); + + message.Body = null; + Assert.False(message.HasSection(AmqpMessageSection.Body)); + + message.Body = AmqpMessageBody.FromValue("this is a string value"); + Assert.True(message.HasSection(AmqpMessageSection.Body)); + } + + [Test] + public void HasSectionValidatesTheSection() + { + var invalidSection = (AmqpMessageSection)int.MinValue; + var message = new AmqpAnnotatedMessage(EmptyDataBody); + + Assert.Throws(() => message.HasSection(invalidSection)); + } } }