Skip to content

Commit

Permalink
Support MessageHeaderArrayAttribute and MessagePropertyAttribute
Browse files Browse the repository at this point in the history
Adds these types to public API.
Adds implementation code to support them.
Adds tests for them.

Fixes dotnet#1410
  • Loading branch information
roncain committed Nov 4, 2016
1 parent 2dfce75 commit a803605
Show file tree
Hide file tree
Showing 16 changed files with 925 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ internal class TypeLoader
private static Type[] s_messageContractMemberAttributes = {
typeof(MessageHeaderAttribute),
typeof(MessageBodyMemberAttribute),
typeof(MessagePropertyAttribute)
};

private static Type[] s_formatterAttributes = {
Expand Down Expand Up @@ -1331,9 +1332,10 @@ internal MessageDescription CreateTypedMessageDescription(Type typedMessageType,
}
}


if (memberInfo.IsDefined(typeof(MessageBodyMemberAttribute), false) ||
memberInfo.IsDefined(typeof(MessageHeaderAttribute), false)
memberInfo.IsDefined(typeof(MessageHeaderAttribute), false) ||
memberInfo.IsDefined(typeof(MessageHeaderArrayAttribute), false) ||
memberInfo.IsDefined(typeof(MessagePropertyAttribute), false)
)
{
contractMembers.Add(memberInfo);
Expand All @@ -1360,7 +1362,8 @@ internal MessageDescription CreateTypedMessageDescription(Type typedMessageType,
memberType = ((FieldInfo)memberInfo).FieldType;
}

if (memberInfo.IsDefined(typeof(MessageHeaderAttribute), false))
if (memberInfo.IsDefined(typeof(MessageHeaderArrayAttribute), false) ||
memberInfo.IsDefined(typeof(MessageHeaderAttribute), false))
{
headerPartDescriptionList.Add(CreateMessageHeaderDescription(memberType,
memberInfo,
Expand All @@ -1369,6 +1372,12 @@ internal MessageDescription CreateTypedMessageDescription(Type typedMessageType,
i,
-1));
}
else if (memberInfo.IsDefined(typeof(MessagePropertyAttribute), false))
{
messageDescription.Properties.Add(CreateMessagePropertyDescription(memberInfo,
new XmlName(memberInfo.Name),
i));
}
else
{
bodyPartDescriptionList.Add(CreateMessagePartDescription(memberType,
Expand Down Expand Up @@ -1455,7 +1464,15 @@ private MessageHeaderDescription CreateMessageHeaderDescription(Type headerParam
headerDescription = new MessageHeaderDescription(headerName.EncodedName, headerNs);
headerDescription.UniquePartName = defaultName.EncodedName;

// check on MessageHeaderArrayAttribute is omitted.
if (headerAttr is MessageHeaderArrayAttribute)
{
if (!headerParameterType.IsArray || headerParameterType.GetArrayRank() != 1)
{
throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR.Format(SR.SFxInvalidMessageHeaderArrayType, defaultName)));
}
headerDescription.Multiple = true;
headerParameterType = headerParameterType.GetElementType();
}

headerDescription.Type = TypedHeaderManager.GetHeaderType(headerParameterType);
headerDescription.TypedHeader = (headerParameterType != headerDescription.Type);
Expand Down Expand Up @@ -1490,7 +1507,8 @@ private MessagePropertyDescription CreateMessagePropertyDescription(CustomAttrib
XmlName defaultName,
int parameterIndex)
{
XmlName propertyName = defaultName;
MessagePropertyAttribute attr = ServiceReflector.GetSingleAttribute<MessagePropertyAttribute>(attrProvider);
XmlName propertyName = attr.IsNameSetExplicit ? new XmlName(attr.Name) : defaultName;
MessagePropertyDescription propertyDescription = new MessagePropertyDescription(propertyName.EncodedName);
propertyDescription.Index = parameterIndex;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ public static string HttpBaseAddress_Basic
get { return GetEndpointAddress("BasicHttp.svc//Basic"); }
}

// Endpoint that relies on post-1.1.0 features
public static string HttpBaseAddress_4_4_0_Basic
{
get { return GetEndpointAddress("BasicHttp_4_4_0.svc//Basic"); }
}

public static string HttpBaseAddress_NetHttp
{
get { return GetEndpointAddress("NetHttp.svc//NetHttp"); }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,5 @@ public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRu
public void DefaultApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
}
}
}

70 changes: 70 additions & 0 deletions src/System.Private.ServiceModel/tests/Common/Unit/TestHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,15 @@ public static string ValidateContractDescription(ContractDescription contract, T
prefix, messageData.Action, messageData.MessageType, messageDesc.MessageType));
}
}

// Validate the Body parts of the MessageDescription
ValidatePartDescriptionCollection(messageData.Action, "Body", errorBuilder, messageDesc.Body.Parts.ToArray(), messageData.Body);

// Validate the Header parts of the MessageDescription
ValidatePartDescriptionCollection(messageData.Action, "Header", errorBuilder, messageDesc.Headers.ToArray(), messageData.Headers);

// Validate the Property parts of the MessageDescription
ValidatePartDescriptionCollection(messageData.Action, "Properties", errorBuilder, messageDesc.Properties.ToArray(), messageData.Properties);
}
}
}
Expand All @@ -208,6 +217,53 @@ public static string ValidateContractDescription(ContractDescription contract, T
return errorBuilder.Length == 0 ? null : errorBuilder.ToString();
}

private static void ValidatePartDescriptionCollection(string action, string section, StringBuilder errorBuilder, MessagePartDescription[] desc, PartDescriptionData[] data)
{
if (data != null)
{
if (desc.Length != data.Length)
{
errorBuilder.AppendLine(String.Format("action {0}, section {1}, expected part count = {2}, actual = {3}",
action, section, data.Length, desc.Length));
}

// MessagePartDescriptions are keyed collections, so their order is unpredictable
foreach (PartDescriptionData dataPart in data)
{
MessagePartDescription descPart = desc.SingleOrDefault((d) => String.Equals(dataPart.Name, d.Name));
ValidatePartDescription(action, section, errorBuilder, descPart, dataPart);
}
}
}

private static void ValidatePartDescription(string action, string section, StringBuilder errorBuilder, MessagePartDescription desc, PartDescriptionData data)
{
if (desc == null)
{
errorBuilder.AppendLine(String.Format("action {0}, section {1}, expected part Name = {2} but did not find it.",
action, section, data.Name));
return;
}

if (!String.Equals(desc.Name, data.Name))
{
errorBuilder.AppendLine(String.Format("action {0}, section {1}, expected part Name = {2}, actual = {3}",
action, section, data.Name, desc.Name));
}

if (desc.Type != data.Type)
{
errorBuilder.AppendLine(String.Format("action {0}, section {1}, name {2}, expected Type = {3}, actual = {4}",
action, section, desc.Name, data.Type, desc.Type));
}

if (desc.Multiple != data.Multiple)
{
errorBuilder.AppendLine(String.Format("action {0}, section {1}, name {2}, expected Multiple = {3}, actual = {4}",
action, section, desc.Name, data.Multiple, desc.Multiple));
}
}

#if DEBUGGING
private static void DumpContractDescription(ContractDescription contract)
{
Expand Down Expand Up @@ -259,8 +315,22 @@ public class MessageDescriptionData

// If using MessageContract, the type of the "typed message"
public Type MessageType { get; set; }

public PartDescriptionData[] Body { get; set; }

public PartDescriptionData[] Headers { get; set; }

public PartDescriptionData[] Properties { get; set; }
}

public class PartDescriptionData
{
public string Name { get; set; }
public Type Type { get; set; }
public bool Multiple { get; set; }
}


public class CustomBodyWriter : BodyWriter
{
private string _bodyContent;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.ServiceModel;
using Infrastructure.Common;
using WcfService;
using Xunit;

public static class MessageContractTests_4_4_0
{
[WcfFact]
[OuterLoop]
public static void Message_With_MessageHeaders_RoundTrips()
{
BasicHttpBinding binding = null;
IWcfService_4_4_0 clientProxy = null;
RequestBankingData_4_4_0 requestData = null;
ChannelFactory<IWcfService_4_4_0> factory = null;

// *** SETUP *** \\
try
{
binding = new BasicHttpBinding();
factory = new ChannelFactory<IWcfService_4_4_0>(binding, new EndpointAddress(Endpoints.HttpBaseAddress_4_4_0_Basic));
clientProxy = factory.CreateChannel();

requestData = new RequestBankingData_4_4_0();
requestData.accountName = "Michael Jordan";
requestData.transactionDate = DateTime.Now;
requestData.amount = 100.0M;

// post-1.1.0 features
requestData.requestSingleValue = "test single value";
requestData.requestMultipleValues = "test,multiple,value".Split(',');
requestData.requestArrayMultipleValues = "test,array,multiple,value".Split(',');

// *** EXECUTE *** \\
ReplyBankingData_4_4_0 replyData = clientProxy.MessageContractRequestReply(requestData);

// *** VALIDATE *** \\
Assert.True(String.Equals(requestData.accountName, replyData.accountName),
String.Format("Expected Customer = '{0}', actual = '{1}'",
requestData.accountName, replyData.accountName));

Assert.True(requestData.amount == replyData.amount,
String.Format("Expected Amount = '{0}', actual = '{1}'",
requestData.amount, requestData.amount));

Assert.True(String.Equals(requestData.requestSingleValue, replyData.replySingleValue),
String.Format("Expected RequestSingleValue = '{0}', actual = '{1}",
requestData.requestSingleValue, replyData.replySingleValue));

ValidateArray("MultipleValue", requestData.requestMultipleValues, replyData.replyMultipleValues);
ValidateArray("ArrayMultipleValue", requestData.requestArrayMultipleValues, replyData.replyArrayMultipleValues);

// *** CLEANUP *** \\
factory.Close();
((ICommunicationObject)clientProxy).Close();
}
finally
{
// *** ENSURE CLEANUP *** \\
ScenarioTestHelpers.CloseCommunicationObjects((ICommunicationObject)clientProxy, factory);
}
}

private static void ValidateArray(string elementName, string[] array1, string[] array2)
{
Assert.True(array2 != null,
String.Format("The {0} element returned a null array", elementName));

Assert.True(array1.Length == array2.Length,
String.Format("The {0} element was expected to return {1} items, actual = {2}",
elementName, array1.Length, array2.Length));

for (int i = 0; i < array1.Length; ++i)
{
Assert.True(array1[i] == array2[i],
String.Format("Array item {0} of element {1} was expected to be {2}, actual was {3}",
i, elementName, array1[i], array2[i]));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.


using System;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
using System.Xml;

namespace WcfService
{
// These classes exist for features added post-1.1.0
[ServiceContract]
public interface IWcfService_4_4_0
{
[OperationContract(Action = "http://tempuri.org/IWcfService_4_4_0/MessageContractRequestReply",
ReplyAction = "http://tempuri.org/IWcfService_4_4_0/MessageContractRequestReplyResponse")]
ReplyBankingData_4_4_0 MessageContractRequestReply(RequestBankingData_4_4_0 bt);
}

[MessageContract(IsWrapped = true,
WrapperName = "CustomWrapperName",
WrapperNamespace = "http://www.contoso.com")]
public class RequestBankingData_4_4_0
{
[MessageBodyMember(Order = 1,
Name = "Date_of_Request")]
public DateTime transactionDate;

[MessageBodyMember(Name = "Customer_Name",
Namespace = "http://www.contoso.com",
Order = 3)]
public string accountName;

[MessageBodyMember(Order = 2,
Name = "Transaction_Amount")]
public decimal amount;

// The following rely on features added post-1.1.0
[MessageProperty(Name = "TestProperty")]
public string requestProperty;

[MessageHeader(Name = "SingleElement")]
public string requestSingleValue;

[MessageHeader(Name = "MultipleElement")]
public string[] requestMultipleValues;

[MessageHeaderArray(Name = "ArrayMultipleElement")]
public string[] requestArrayMultipleValues;
}

[MessageContract(IsWrapped = true,
WrapperName = "CustomWrapperName",
WrapperNamespace = "http://www.contoso.com")]
public class ReplyBankingData_4_4_0
{
[MessageBodyMember(Order = 1, Name = "Date_of_Request")]
public DateTime transactionDate;

[MessageBodyMember(Name = "Customer_Name",
Namespace = "http://www.contoso.com",
Order = 3)]
public string accountName;

[MessageBodyMember(Order = 2,
Name = "Transaction_Amount")]
public decimal amount;

// The following rely on features added post-1.1.0
[MessageProperty(Name = "TestProperty")]
public string replyProperty;

[MessageHeader(Name = "SingleElement")]
public string replySingleValue;

[MessageHeader(Name = "MultipleElement")]
public string[] replyMultipleValues;

[MessageHeaderArray(Name = "ArrayMultipleElement")]
public string[] replyArrayMultipleValues;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.


using System;
using System.Collections.Generic;
using System.IO;
using System.ServiceModel;
using System.ServiceModel.Channels;

namespace WcfService
{
// This contract relies on features added post-1.1.0
[ServiceContract]
public interface IWcfService_4_4_0
{
[OperationContract(Action = "http://tempuri.org/IWcfService_4_4_0/MessageContractRequestReply")]
ReplyBankingData_4_4_0 MessageContractRequestReply(RequestBankingData_4_4_0 bt);
}
}
Loading

0 comments on commit a803605

Please sign in to comment.