diff --git a/NuGet.Server.Common.sln b/NuGet.Server.Common.sln index 5d261453..003ad1c4 100644 --- a/NuGet.Server.Common.sln +++ b/NuGet.Server.Common.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.27005.2 +VisualStudioVersion = 15.0.26730.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8415FED7-1BED-4227-8B4F-BB7C24E041CD}" EndProject @@ -162,7 +162,6 @@ Global {C1E36A2C-1C1B-4521-B256-AD42505D9EFB} = {8415FED7-1BED-4227-8B4F-BB7C24E041CD} {E29F54DF-DFB8-4E27-940D-21ECCB9B6FC1} = {7783A106-0F4C-4055-9AB4-413FB2C7B8F0} {79F72C83-E94D-4D04-B904-5A4DA161168E} = {7783A106-0F4C-4055-9AB4-413FB2C7B8F0} - {FF5CA51A-CD6A-463F-AE9A-5737FF0FCFA7} = {7783A106-0F4C-4055-9AB4-413FB2C7B8F0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {AA413DB0-5475-4B5D-A3AF-6323DA8D538B} diff --git a/src/NuGet.Services.ServiceBus/BrokeredMessageSerializer.cs b/src/NuGet.Services.ServiceBus/BrokeredMessageSerializer.cs new file mode 100644 index 00000000..fc0d155b --- /dev/null +++ b/src/NuGet.Services.ServiceBus/BrokeredMessageSerializer.cs @@ -0,0 +1,95 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Newtonsoft.Json; + +namespace NuGet.Services.ServiceBus +{ + /// + /// Serializes objects into Service Bus . This serializer will + /// throw if the message does not contain a message with the expected + /// type and schema version. + /// + /// A type decorated with a . + public class BrokeredMessageSerializer + { + private const string SchemaNameKey = "SchemaName"; + private const string SchemaVersionKey = "SchemaVersion"; + + private static readonly string SchemaName; + private static readonly int SchemaVersion; + + static BrokeredMessageSerializer() + { + var attributes = typeof(TMessage).GetCustomAttributes(typeof(SchemaAttribute), inherit: false); + + if (attributes.Length != 1) + { + throw new InvalidOperationException($"{typeof(TMessage)} must have exactly one {nameof(SchemaAttribute)}"); + } + + var schemaAttribute = (SchemaAttribute)attributes[0]; + + SchemaName = schemaAttribute.Name; + SchemaVersion = schemaAttribute.Version; + } + + public IBrokeredMessage Serialize(TMessage message) + { + var json = JsonConvert.SerializeObject(message); + var brokeredMessage = new BrokeredMessageWrapper(json); + + brokeredMessage.Properties[SchemaNameKey] = SchemaName; + brokeredMessage.Properties[SchemaVersionKey] = SchemaVersion; + + return brokeredMessage; + } + + public TMessage Deserialize(IBrokeredMessage message) + { + AssertTypeAndSchemaVersion(message, SchemaName, SchemaVersion); + + return JsonConvert.DeserializeObject(message.GetBody()); + } + + private static void AssertTypeAndSchemaVersion(IBrokeredMessage message, string type, int schemaVersion) + { + if (GetType(message) != type) + { + throw new FormatException($"The provided message should have {SchemaNameKey} property '{type}'."); + } + + if (GetSchemaVersion(message) != schemaVersion) + { + throw new FormatException($"The provided message should have {SchemaVersionKey} property '{schemaVersion}'."); + } + } + + private static int GetSchemaVersion(IBrokeredMessage message) + { + return GetProperty(message, SchemaVersionKey, "an integer"); + } + + private static string GetType(IBrokeredMessage message) + { + return GetProperty(message, SchemaNameKey, "a string"); + } + + private static T GetProperty(IBrokeredMessage message, string key, string typeLabel) + { + object value; + if (!message.Properties.TryGetValue(key, out value)) + { + throw new FormatException($"The provided message does not have a {key} property."); + } + + if (!(value is T)) + { + throw new FormatException($"The provided message contains a {key} property that is not {typeLabel}."); + } + + return (T)value; + } + } +} diff --git a/src/NuGet.Services.ServiceBus/NuGet.Services.ServiceBus.csproj b/src/NuGet.Services.ServiceBus/NuGet.Services.ServiceBus.csproj index bca77ca6..ace33025 100644 --- a/src/NuGet.Services.ServiceBus/NuGet.Services.ServiceBus.csproj +++ b/src/NuGet.Services.ServiceBus/NuGet.Services.ServiceBus.csproj @@ -40,9 +40,11 @@ + + diff --git a/src/NuGet.Services.ServiceBus/SchemaAttribute.cs b/src/NuGet.Services.ServiceBus/SchemaAttribute.cs new file mode 100644 index 00000000..25b99adc --- /dev/null +++ b/src/NuGet.Services.ServiceBus/SchemaAttribute.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace NuGet.Services.ServiceBus +{ + /// + /// The attribute used to define a schema. + /// + public class SchemaAttribute : Attribute + { + /// + /// The name of a message's schema. This should NEVER change for a single message. + /// + public string Name { get; set; } + + /// + /// The schema's version. This should be bumped whenever a schema's property + /// is added, removed, or modified. + /// + public int Version { get; set; } + } +} diff --git a/src/NuGet.Services.ServiceBus/project.json b/src/NuGet.Services.ServiceBus/project.json index bcb06aaa..458aa054 100644 --- a/src/NuGet.Services.ServiceBus/project.json +++ b/src/NuGet.Services.ServiceBus/project.json @@ -1,5 +1,6 @@ { "dependencies": { + "Newtonsoft.Json": "9.0.1", "WindowsAzure.ServiceBus": "4.1.3" }, "frameworks": { @@ -8,4 +9,4 @@ "runtimes": { "win": {} } -} +} \ No newline at end of file diff --git a/src/NuGet.Services.Validation/ServiceBusMessageSerializer.cs b/src/NuGet.Services.Validation/ServiceBusMessageSerializer.cs index 9a4ace22..bf3874d7 100644 --- a/src/NuGet.Services.Validation/ServiceBusMessageSerializer.cs +++ b/src/NuGet.Services.Validation/ServiceBusMessageSerializer.cs @@ -2,38 +2,29 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using Newtonsoft.Json; using NuGet.Services.ServiceBus; namespace NuGet.Services.Validation { public class ServiceBusMessageSerializer : IServiceBusMessageSerializer { - private const string SchemaVersionKey = "SchemaVersion"; - private const string TypeKey = "Type"; - private const string PackageValidationType = nameof(PackageValidationMessageData); - private const int SchemaVersion1 = 1; + private const string PackageValidationSchemaName = "PackageValidationMessageData"; + + private static readonly BrokeredMessageSerializer _serializer = new BrokeredMessageSerializer(); public IBrokeredMessage SerializePackageValidationMessageData(PackageValidationMessageData message) { - var body = new PackageValidationMessageData1 + return _serializer.Serialize(new PackageValidationMessageData1 { PackageId = message.PackageId, PackageVersion = message.PackageVersion, ValidationTrackingId = message.ValidationTrackingId, - }; - - var brokeredMessage = Serialize(body, PackageValidationType, SchemaVersion1); - - return brokeredMessage; + }); } public PackageValidationMessageData DeserializePackageValidationMessageData(IBrokeredMessage message) { - AssertTypeAndSchemaVersion(message, PackageValidationType, SchemaVersion1); - - var json = message.GetBody(); - var data = Deserialize(json); + var data = _serializer.Deserialize(message); return new PackageValidationMessageData( data.PackageId, @@ -41,60 +32,7 @@ public PackageValidationMessageData DeserializePackageValidationMessageData(IBro data.ValidationTrackingId); } - private static void AssertTypeAndSchemaVersion(IBrokeredMessage message, string type, int schemaVersion) - { - if (GetType(message) != type) - { - throw new FormatException($"The provided message should have {TypeKey} property '{type}'."); - } - - if (GetSchemaVersion(message) != schemaVersion) - { - throw new FormatException($"The provided message should have {SchemaVersionKey} property '{schemaVersion}'."); - } - } - - private static int GetSchemaVersion(IBrokeredMessage message) - { - return GetProperty(message, SchemaVersionKey, "an integer"); - } - - private static string GetType(IBrokeredMessage message) - { - return GetProperty(message, TypeKey, "a string"); - } - - private static T GetProperty(IBrokeredMessage message, string key, string typeLabel) - { - object value; - if (!message.Properties.TryGetValue(key, out value)) - { - throw new FormatException($"The provided message does not have a {key} property."); - } - - if (!(value is T)) - { - throw new FormatException($"The provided message contains a {key} property that is not {typeLabel}."); - } - - return (T)value; - } - - private static IBrokeredMessage Serialize(T data, string type, int schemaVersion) - { - var json = JsonConvert.SerializeObject(data); - var brokeredMessage = new BrokeredMessageWrapper(json); - brokeredMessage.Properties[TypeKey] = type; - brokeredMessage.Properties[SchemaVersionKey] = schemaVersion; - - return brokeredMessage; - } - - private static T Deserialize(string json) - { - return JsonConvert.DeserializeObject(json); - } - + [Schema(Name = PackageValidationSchemaName, Version = 1)] private class PackageValidationMessageData1 { public string PackageId { get; set; } diff --git a/tests/NuGet.Services.ServiceBus.Tests/BrokeredMessageSerializerFacts.cs b/tests/NuGet.Services.ServiceBus.Tests/BrokeredMessageSerializerFacts.cs new file mode 100644 index 00000000..cfca8565 --- /dev/null +++ b/tests/NuGet.Services.ServiceBus.Tests/BrokeredMessageSerializerFacts.cs @@ -0,0 +1,185 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Moq; +using Xunit; + +namespace NuGet.Services.ServiceBus.Tests +{ + public class BrokeredMessageSerializerFacts + { + private const string SchemaNameKey = "SchemaName"; + private const string SchemaVersionKey = "SchemaVersion"; + + private const string SchemaName = "Foo"; + private const int SchemaVersion23 = 23; + + private const string JsonSerializedContent = "{\"A\":\"Hello World\"}"; + + [Schema(Name = "Foo", Version = 23)] + public class SchematizedType + { + public string A { get; set; } + } + + public class UnSchematizedType { } + + public class TheConstructor : Base + { + [Fact] + public void ThrowsIfSchemaDoesntHaveSchemaVersionAttribute() + { + Action runConstructor = () => new BrokeredMessageSerializer(); + var exception = Assert.Throws(runConstructor); + + Assert.Equal(typeof(InvalidOperationException), exception.InnerException.GetType()); + Assert.Contains($"{nameof(UnSchematizedType)} must have exactly one {nameof(SchemaAttribute)}", exception.InnerException.Message); + } + } + + public class TheSerializeMethod : Base + { + [Fact] + public void ProducesExpectedMessage() + { + // Arrange + var input = new SchematizedType { A = "Hello World" }; + + // Act + var output = _target.Serialize(input); + + // Assert + Assert.Contains(SchemaVersionKey, output.Properties.Keys); + Assert.Equal(SchemaVersion23, output.Properties[SchemaVersionKey]); + Assert.Contains(SchemaNameKey, output.Properties.Keys); + Assert.Equal(SchemaName, output.Properties[SchemaNameKey]); + var body = output.GetBody(); + Assert.Equal(JsonSerializedContent, body); + } + } + + public class TheDeserializePackageValidationMessageDataMethod : Base + { + private const string TypeValue = "PackageValidationMessageData"; + + [Fact] + public void ProducesExpectedMessage() + { + // Arrange + var brokeredMessage = GetBrokeredMessage(); + + // Act + var output = _target.Deserialize(brokeredMessage.Object); + + // Assert + Assert.Equal("Hello World", output.A); + } + + [Fact] + public void RejectsInvalidType() + { + // Arrange + var brokeredMessage = GetBrokeredMessage(); + brokeredMessage.Object.Properties[SchemaNameKey] = "bad"; + + // Act & Assert + var exception = Assert.Throws(() => + _target.Deserialize(brokeredMessage.Object)); + Assert.Contains($"The provided message should have {SchemaNameKey} property '{SchemaName}'.", exception.Message); + } + + [Fact] + public void RejectsInvalidSchemaVersion() + { + // Arrange + var brokeredMessage = GetBrokeredMessage(); + brokeredMessage.Object.Properties[SchemaVersionKey] = -1; + + // Act & Assert + var exception = Assert.Throws(() => + _target.Deserialize(brokeredMessage.Object)); + Assert.Contains($"The provided message should have {SchemaVersionKey} property '23'.", exception.Message); + } + + [Fact] + public void RejectsMissingType() + { + // Arrange + var brokeredMessage = GetBrokeredMessage(); + brokeredMessage.Object.Properties.Remove(SchemaNameKey); + + // Act & Assert + var exception = Assert.Throws(() => + _target.Deserialize(brokeredMessage.Object)); + Assert.Contains($"The provided message does not have a {SchemaNameKey} property.", exception.Message); + } + + [Fact] + public void RejectsMissingSchemaVersion() + { + // Arrange + var brokeredMessage = GetBrokeredMessage(); + brokeredMessage.Object.Properties.Remove(SchemaVersionKey); + + // Act & Assert + var exception = Assert.Throws(() => + _target.Deserialize(brokeredMessage.Object)); + Assert.Contains($"The provided message does not have a {SchemaVersionKey} property.", exception.Message); + } + + [Fact] + public void RejectsInvalidTypeType() + { + // Arrange + var brokeredMessage = GetBrokeredMessage(); + brokeredMessage.Object.Properties[SchemaNameKey] = -1; + + // Act & Assert + var exception = Assert.Throws(() => + _target.Deserialize(brokeredMessage.Object)); + Assert.Contains($"The provided message contains a {SchemaNameKey} property that is not a string.", exception.Message); + } + + [Fact] + public void RejectsInvalidSchemaVersionType() + { + // Arrange + var brokeredMessage = GetBrokeredMessage(); + brokeredMessage.Object.Properties[SchemaVersionKey] = "bad"; + + // Act & Assert + var exception = Assert.Throws(() => + _target.Deserialize(brokeredMessage.Object)); + Assert.Contains($"The provided message contains a {SchemaVersionKey} property that is not an integer.", exception.Message); + } + + private static Mock GetBrokeredMessage() + { + var brokeredMessage = new Mock(); + brokeredMessage + .Setup(x => x.GetBody()) + .Returns(JsonSerializedContent); + brokeredMessage + .Setup(x => x.Properties) + .Returns(new Dictionary + { + { SchemaNameKey, SchemaName }, + { SchemaVersionKey, SchemaVersion23 } + }); + return brokeredMessage; + } + } + + public abstract class Base + { + protected readonly BrokeredMessageSerializer _target; + + public Base() + { + _target = new BrokeredMessageSerializer(); + } + } + } +} diff --git a/tests/NuGet.Services.ServiceBus.Tests/NuGet.Services.ServiceBus.Tests.csproj b/tests/NuGet.Services.ServiceBus.Tests/NuGet.Services.ServiceBus.Tests.csproj index 67f1efaf..776a8ac8 100644 --- a/tests/NuGet.Services.ServiceBus.Tests/NuGet.Services.ServiceBus.Tests.csproj +++ b/tests/NuGet.Services.ServiceBus.Tests/NuGet.Services.ServiceBus.Tests.csproj @@ -42,6 +42,7 @@ + @@ -50,6 +51,10 @@ + + {6674b4b4-2d0e-4840-8cf0-2356acde8863} + NuGet.Services.Contracts + {9337000b-ea3b-40be-9a33-38bc28dfd0cb} NuGet.Services.ServiceBus diff --git a/tests/NuGet.Services.ServiceBus.Tests/Properties/AssemblyInfo.cs b/tests/NuGet.Services.ServiceBus.Tests/Properties/AssemblyInfo.cs index e394674b..3829a867 100644 --- a/tests/NuGet.Services.ServiceBus.Tests/Properties/AssemblyInfo.cs +++ b/tests/NuGet.Services.ServiceBus.Tests/Properties/AssemblyInfo.cs @@ -1,4 +1,7 @@ -using System.Reflection; +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Reflection; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following diff --git a/tests/NuGet.Services.ServiceBus.Tests/project.json b/tests/NuGet.Services.ServiceBus.Tests/project.json index 42de3faf..ebf75baf 100644 --- a/tests/NuGet.Services.ServiceBus.Tests/project.json +++ b/tests/NuGet.Services.ServiceBus.Tests/project.json @@ -11,4 +11,4 @@ "runtimes": { "win": {} } -} \ No newline at end of file +} diff --git a/tests/NuGet.Services.Validation.Tests/ServiceBusMessageSerializerTests.cs b/tests/NuGet.Services.Validation.Tests/ServiceBusMessageSerializerTests.cs index 4b428d5f..c4df231e 100644 --- a/tests/NuGet.Services.Validation.Tests/ServiceBusMessageSerializerTests.cs +++ b/tests/NuGet.Services.Validation.Tests/ServiceBusMessageSerializerTests.cs @@ -11,8 +11,8 @@ namespace NuGet.Services.Validation.Tests { public class ServiceBusMessageSerializerTests { + private const string SchemaName = "SchemaName"; private const string SchemaVersionKey = "SchemaVersion"; - private const string TypeKey = "Type"; private const string PackageId = "NuGet.Versioning"; private const string PackageVersion = "4.3.0"; private static readonly Guid ValidationTrackingId = new Guid("14b4c1b8-40e2-4d60-9db7-4b7195e807f5"); @@ -33,8 +33,8 @@ public void ProducesExpectedMessage() // Assert Assert.Contains(SchemaVersionKey, output.Properties.Keys); Assert.Equal(SchemaVersion1, output.Properties[SchemaVersionKey]); - Assert.Contains(TypeKey, output.Properties.Keys); - Assert.Equal(PackageValidationMessageDataType, output.Properties[TypeKey]); + Assert.Contains(SchemaName, output.Properties.Keys); + Assert.Equal(PackageValidationMessageDataType, output.Properties[SchemaName]); var body = output.GetBody(); Assert.Equal(TestData.SerializedPackageValidationMessageData1, body); } @@ -64,12 +64,12 @@ public void RejectsInvalidType() { // Arrange var brokeredMessage = GetBrokeredMessage(); - brokeredMessage.Object.Properties[TypeKey] = "bad"; + brokeredMessage.Object.Properties[SchemaName] = "bad"; // Act & Assert var exception = Assert.Throws(() => _target.DeserializePackageValidationMessageData(brokeredMessage.Object)); - Assert.Contains($"The provided message should have {TypeKey} property '{PackageValidationMessageDataType}'.", exception.Message); + Assert.Contains($"The provided message should have {SchemaName} property '{PackageValidationMessageDataType}'.", exception.Message); } [Fact] @@ -90,12 +90,12 @@ public void RejectsMissingType() { // Arrange var brokeredMessage = GetBrokeredMessage(); - brokeredMessage.Object.Properties.Remove(TypeKey); + brokeredMessage.Object.Properties.Remove(SchemaName); // Act & Assert var exception = Assert.Throws(() => _target.DeserializePackageValidationMessageData(brokeredMessage.Object)); - Assert.Contains($"The provided message does not have a {TypeKey} property.", exception.Message); + Assert.Contains($"The provided message does not have a {SchemaName} property.", exception.Message); } [Fact] @@ -116,12 +116,12 @@ public void RejectsInvalidTypeType() { // Arrange var brokeredMessage = GetBrokeredMessage(); - brokeredMessage.Object.Properties[TypeKey] = -1; + brokeredMessage.Object.Properties[SchemaName] = -1; // Act & Assert var exception = Assert.Throws(() => _target.DeserializePackageValidationMessageData(brokeredMessage.Object)); - Assert.Contains($"The provided message contains a {TypeKey} property that is not a string.", exception.Message); + Assert.Contains($"The provided message contains a {SchemaName} property that is not a string.", exception.Message); } [Fact] @@ -147,7 +147,7 @@ private static Mock GetBrokeredMessage() .Setup(x => x.Properties) .Returns(new Dictionary { - { TypeKey, PackageValidationMessageDataType }, + { SchemaName, PackageValidationMessageDataType }, { SchemaVersionKey, SchemaVersion1 } }); return brokeredMessage;