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;