仅针对 webHttpBinding 更改 XML 和 JSON 中的响应输出格式
Change the response output format in XML and JSON only for webHttpBinding
我已经研究这个问题很长时间了,以下是我的发现和要求:
我们有两个端点:
- TCP 端点
- WebHttp 端点
通过 WebHttp 端点,我们需要支持 JSON 和 XML 但具有自定义响应格式。这是所需的格式(为清楚起见,仅显示 JSON):
{
"status": "success",
"data" : {}
}
我们需要的是让每个对象returned正常序列化,在层级中放到data下。假设我们有这个 OperationContract
:
ObjectToBeReturned test();
和ObjectToBeReturned
是:
[DataContract]
class ObjectToBeReturned {
[DataMember]
public string A {get; set;}
[DataMember]
public string B {get; set;}
}
现在,我们希望通过 TCP
直接交换 ObjectToBeReturned
对象,但通过 WebHttp
具有以下格式作为响应:
{
"status": "success",
"data": {
"A": "atest",
"B": "btest"
}
}
可能性 1
我们考虑了两种可能性。第一个是让一个名为 Response 的对象成为我们所有 OperationContract
的 return 对象,它将包含以下内容:
[DataContract]
class Response<T> {
[DataMember]
public string Status {get; set;}
[DataMember]
public T Data {get; set;}
}
问题是我们也需要通过 TCP 协议交换这个对象,但这不是我们理想的场景。
可能性2
我们已尝试添加自定义 EndpointBehavior
和自定义 IDispatchMessageFormatter
,该自定义 IDispatchMessageFormatter
只会出现在 WebHttp
端点。
在这个class中,我们实现了以下方法:
public Message SerializeReply(
MessageVersion messageVersion,
object[] parameters,
object result)
{
var clientAcceptType = WebOperationContext.Current.IncomingRequest.Accept;
Type type = result.GetType();
var genericResponseType = typeof(Response<>);
var specificResponseType = genericResponseType.MakeGenericType(result.GetType());
var response = Activator.CreateInstance(specificResponseType, result);
Message message;
WebBodyFormatMessageProperty webBodyFormatMessageProperty;
if (clientAcceptType == "application/json")
{
message = Message.CreateMessage(messageVersion, "", response, new DataContractJsonSerializer(specificResponseType));
webBodyFormatMessageProperty = new WebBodyFormatMessageProperty(WebContentFormat.Json);
}
else
{
message = Message.CreateMessage(messageVersion, "", response, new DataContractSerializer(specificResponseType));
webBodyFormatMessageProperty = new WebBodyFormatMessageProperty(WebContentFormat.Xml);
}
var responseMessageProperty = new HttpResponseMessageProperty
{
StatusCode = System.Net.HttpStatusCode.OK
};
message.Properties.Add(HttpResponseMessageProperty.Name, responseMessageProperty);
message.Properties.Add(WebBodyFormatMessageProperty.Name, webBodyFormatMessageProperty);
return message;
}
这看起来很有希望。该方法的问题在于,在使用 DataContractSerializer
进行序列化时,我们会收到以下错误:
Consider using a DataContractResolver if you are using
DataContractSerializer or add any types not known statically to the
list of known types - for example, by using the KnownTypeAttribute
attribute or by adding them to the list of known types passed to the
serializer.
我们真的不想在响应 class 上方列出所有已知类型,因为数量太多,维护将是一场噩梦(当我们列出已知类型时,我们能够获得数据)。请注意,传递给响应的所有对象都将使用 DataContract
属性进行修饰。
我必须指出,我们不关心更改消息格式是否会导致 WebHttp
端点无法通过另一个 C# 项目中的 ServiceReference
访问,他们应该使用 TCP
为此。
问题
基本上,我们只想为 WebHttp 自定义 return 格式,所以,问题是:
- 有没有比我们正在做的更简单的方法来实现它?
- 有没有办法根据
SerializeReply
方法中 result
参数的类型告诉序列化程序已知类型?
- 我们是否应该实施将在
MessageDispatcherFormatter
中调用的自定义 Serializer
以调整格式以适应我们的格式?
我们觉得我们走在正确的道路上,但有些地方缺失了。
您几乎走在正确的轨道上 - 拥有仅适用于 JSON 端点的端点行为肯定是正确的方法。但是,您可以使用消息检查器,它比格式化程序简单一些。在检查器上,您可以获取现有响应,如果它是 JSON 响应,并用您的包装对象包装内容。
请注意,WCF 内部结构都是基于 XML 的,因此您需要使用 Mapping Between JSON and XML,但这并不太复杂。
下面的代码显示了这个场景的实现。
public class Whosebug_36918281
{
[DataContract] public class ObjectToBeReturned
{
[DataMember]
public string A { get; set; }
[DataMember]
public string B { get; set; }
}
[ServiceContract]
public interface ITest
{
[OperationContract, WebGet(ResponseFormat = WebMessageFormat.Json)]
ObjectToBeReturned Test();
}
public class Service : ITest
{
public ObjectToBeReturned Test()
{
return new ObjectToBeReturned { A = "atest", B = "btest" };
}
}
public class MyJsonWrapperInspector : IEndpointBehavior, IDispatchMessageInspector
{
public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
{
}
public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
{
return null;
}
public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
{
endpointDispatcher.DispatchRuntime.MessageInspectors.Add(this);
}
public void BeforeSendReply(ref Message reply, object correlationState)
{
object propValue;
if (reply.Properties.TryGetValue(WebBodyFormatMessageProperty.Name, out propValue) &&
((WebBodyFormatMessageProperty)propValue).Format == WebContentFormat.Json)
{
XmlDocument doc = new XmlDocument();
doc.Load(reply.GetReaderAtBodyContents());
var newRoot = doc.CreateElement("root");
SetTypeAttribute(doc, newRoot, "object");
var status = doc.CreateElement("status");
SetTypeAttribute(doc, status, "string");
status.AppendChild(doc.CreateTextNode("success"));
newRoot.AppendChild(status);
var newData = doc.CreateElement("data");
SetTypeAttribute(doc, newData, "object");
newRoot.AppendChild(newData);
var data = doc.DocumentElement;
var toCopy = new List<XmlNode>();
foreach (XmlNode child in data.ChildNodes)
{
toCopy.Add(child);
}
foreach (var child in toCopy)
{
newData.AppendChild(child);
}
Console.WriteLine(newRoot.OuterXml);
var newReply = Message.CreateMessage(reply.Version, reply.Headers.Action, new XmlNodeReader(newRoot));
foreach (var propName in reply.Properties.Keys)
{
newReply.Properties.Add(propName, reply.Properties[propName]);
}
reply = newReply;
}
}
private void SetTypeAttribute(XmlDocument doc, XmlElement element, string value)
{
var attr = element.Attributes["type"];
if (attr == null)
{
attr = doc.CreateAttribute("type");
attr.Value = value;
element.Attributes.Append(attr);
}
else
{
attr.Value = value;
}
}
public void Validate(ServiceEndpoint endpoint)
{
}
}
public static void Test()
{
string baseAddress = "http://" + Environment.MachineName + ":8000/Service";
string baseAddressTcp = "net.tcp://" + Environment.MachineName + ":8888/Service";
ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddress), new Uri(baseAddressTcp));
var ep1 = host.AddServiceEndpoint(typeof(ITest), new NetTcpBinding(), "");
var ep2 = host.AddServiceEndpoint(typeof(ITest), new WebHttpBinding(), "");
ep2.EndpointBehaviors.Add(new WebHttpBehavior());
ep2.EndpointBehaviors.Add(new MyJsonWrapperInspector());
host.Open();
Console.WriteLine("Host opened");
Console.WriteLine("TCP:");
ChannelFactory<ITest> factory = new ChannelFactory<ITest>(new NetTcpBinding(), new EndpointAddress(baseAddressTcp));
ITest proxy = factory.CreateChannel();
Console.WriteLine(proxy.Test());
((IClientChannel)proxy).Close();
factory.Close();
Console.WriteLine();
Console.WriteLine("Web:");
WebClient c = new WebClient();
Console.WriteLine(c.DownloadString(baseAddress + "/Test"));
Console.Write("Press ENTER to close the host");
Console.ReadLine();
host.Close();
}
}
以下是我如何能够完成我想要的。可能不是完美的解决方案,但对我来说效果很好。
可能性 2 是可行的方法。但我不得不将其更改为:
public Message SerializeReply(
MessageVersion messageVersion,
object[] parameters,
object result)
{
// In this sample we defined our operations as OneWay, therefore, this method
// will not get invoked.
var clientAcceptType = WebOperationContext.Current.IncomingRequest.Accept;
Type type = result.GetType();
var genericResponseType = typeof(Response<>);
var specificResponseType = genericResponseType.MakeGenericType(result.GetType());
var response = Activator.CreateInstance(specificResponseType, result);
Message message;
WebBodyFormatMessageProperty webBodyFormatMessageProperty;
if (clientAcceptType == "application/json")
{
message = Message.CreateMessage(messageVersion, "", response, new DataContractJsonSerializer(specificResponseType));
webBodyFormatMessageProperty = new WebBodyFormatMessageProperty(WebContentFormat.Json);
}
else
{
message = Message.CreateMessage(messageVersion, "", response, new DataContractSerializer(specificResponseType));
webBodyFormatMessageProperty = new WebBodyFormatMessageProperty(WebContentFormat.Xml);
}
var responseMessageProperty = new HttpResponseMessageProperty
{
StatusCode = System.Net.HttpStatusCode.OK
};
message.Properties.Add(HttpResponseMessageProperty.Name, responseMessageProperty);
message.Properties.Add(WebBodyFormatMessageProperty.Name, webBodyFormatMessageProperty);
return message;
}
这里的关键是,由于 Response 是一个泛型类型,WCF 需要知道所有已知类型并且不可能手动列出它们。我决定我所有的 return 类型都将实现自定义 IDataContract class(是的,空的):
public interface IDataContract
{
}
然后,我在 Response 中所做的是实现一个 GetKnownTypes
方法,并在其中循环遍历所有在程序集中实现 IDataContract 的 classes 和 return 它们在一个数组中。这是我的响应对象:
[DataContract(Name = "ResponseOf{0}")]
[KnownType("GetKnownTypes")]
public class Response<T>
where T : class
{
public static Type[] GetKnownTypes()
{
var type = typeof(IDataContract);
var types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => type.IsAssignableFrom(p));
return types.ToArray();
}
[DataMember(Name = "status")]
public ResponseStatus ResponseStatus { get; set; }
[DataMember(Name = "data")]
public object Data { get; set; }
public Response()
{
ResponseStatus = ResponseStatus.Success;
}
public Response(T data) : base()
{
Data = data;
}
}
这使我能够通过 TCP 连接并直接交换对象,并在 JSON 或 XML 中通过 WebHTTP 进行很好的序列化。
我已经研究这个问题很长时间了,以下是我的发现和要求:
我们有两个端点:
- TCP 端点
- WebHttp 端点
通过 WebHttp 端点,我们需要支持 JSON 和 XML 但具有自定义响应格式。这是所需的格式(为清楚起见,仅显示 JSON):
{
"status": "success",
"data" : {}
}
我们需要的是让每个对象returned正常序列化,在层级中放到data下。假设我们有这个 OperationContract
:
ObjectToBeReturned test();
和ObjectToBeReturned
是:
[DataContract]
class ObjectToBeReturned {
[DataMember]
public string A {get; set;}
[DataMember]
public string B {get; set;}
}
现在,我们希望通过 TCP
直接交换 ObjectToBeReturned
对象,但通过 WebHttp
具有以下格式作为响应:
{
"status": "success",
"data": {
"A": "atest",
"B": "btest"
}
}
可能性 1
我们考虑了两种可能性。第一个是让一个名为 Response 的对象成为我们所有 OperationContract
的 return 对象,它将包含以下内容:
[DataContract]
class Response<T> {
[DataMember]
public string Status {get; set;}
[DataMember]
public T Data {get; set;}
}
问题是我们也需要通过 TCP 协议交换这个对象,但这不是我们理想的场景。
可能性2
我们已尝试添加自定义 EndpointBehavior
和自定义 IDispatchMessageFormatter
,该自定义 IDispatchMessageFormatter
只会出现在 WebHttp
端点。
在这个class中,我们实现了以下方法:
public Message SerializeReply(
MessageVersion messageVersion,
object[] parameters,
object result)
{
var clientAcceptType = WebOperationContext.Current.IncomingRequest.Accept;
Type type = result.GetType();
var genericResponseType = typeof(Response<>);
var specificResponseType = genericResponseType.MakeGenericType(result.GetType());
var response = Activator.CreateInstance(specificResponseType, result);
Message message;
WebBodyFormatMessageProperty webBodyFormatMessageProperty;
if (clientAcceptType == "application/json")
{
message = Message.CreateMessage(messageVersion, "", response, new DataContractJsonSerializer(specificResponseType));
webBodyFormatMessageProperty = new WebBodyFormatMessageProperty(WebContentFormat.Json);
}
else
{
message = Message.CreateMessage(messageVersion, "", response, new DataContractSerializer(specificResponseType));
webBodyFormatMessageProperty = new WebBodyFormatMessageProperty(WebContentFormat.Xml);
}
var responseMessageProperty = new HttpResponseMessageProperty
{
StatusCode = System.Net.HttpStatusCode.OK
};
message.Properties.Add(HttpResponseMessageProperty.Name, responseMessageProperty);
message.Properties.Add(WebBodyFormatMessageProperty.Name, webBodyFormatMessageProperty);
return message;
}
这看起来很有希望。该方法的问题在于,在使用 DataContractSerializer
进行序列化时,我们会收到以下错误:
Consider using a DataContractResolver if you are using DataContractSerializer or add any types not known statically to the list of known types - for example, by using the KnownTypeAttribute attribute or by adding them to the list of known types passed to the serializer.
我们真的不想在响应 class 上方列出所有已知类型,因为数量太多,维护将是一场噩梦(当我们列出已知类型时,我们能够获得数据)。请注意,传递给响应的所有对象都将使用 DataContract
属性进行修饰。
我必须指出,我们不关心更改消息格式是否会导致 WebHttp
端点无法通过另一个 C# 项目中的 ServiceReference
访问,他们应该使用 TCP
为此。
问题
基本上,我们只想为 WebHttp 自定义 return 格式,所以,问题是:
- 有没有比我们正在做的更简单的方法来实现它?
- 有没有办法根据
SerializeReply
方法中result
参数的类型告诉序列化程序已知类型? - 我们是否应该实施将在
MessageDispatcherFormatter
中调用的自定义Serializer
以调整格式以适应我们的格式?
我们觉得我们走在正确的道路上,但有些地方缺失了。
您几乎走在正确的轨道上 - 拥有仅适用于 JSON 端点的端点行为肯定是正确的方法。但是,您可以使用消息检查器,它比格式化程序简单一些。在检查器上,您可以获取现有响应,如果它是 JSON 响应,并用您的包装对象包装内容。
请注意,WCF 内部结构都是基于 XML 的,因此您需要使用 Mapping Between JSON and XML,但这并不太复杂。
下面的代码显示了这个场景的实现。
public class Whosebug_36918281
{
[DataContract] public class ObjectToBeReturned
{
[DataMember]
public string A { get; set; }
[DataMember]
public string B { get; set; }
}
[ServiceContract]
public interface ITest
{
[OperationContract, WebGet(ResponseFormat = WebMessageFormat.Json)]
ObjectToBeReturned Test();
}
public class Service : ITest
{
public ObjectToBeReturned Test()
{
return new ObjectToBeReturned { A = "atest", B = "btest" };
}
}
public class MyJsonWrapperInspector : IEndpointBehavior, IDispatchMessageInspector
{
public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
{
}
public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
{
return null;
}
public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
{
endpointDispatcher.DispatchRuntime.MessageInspectors.Add(this);
}
public void BeforeSendReply(ref Message reply, object correlationState)
{
object propValue;
if (reply.Properties.TryGetValue(WebBodyFormatMessageProperty.Name, out propValue) &&
((WebBodyFormatMessageProperty)propValue).Format == WebContentFormat.Json)
{
XmlDocument doc = new XmlDocument();
doc.Load(reply.GetReaderAtBodyContents());
var newRoot = doc.CreateElement("root");
SetTypeAttribute(doc, newRoot, "object");
var status = doc.CreateElement("status");
SetTypeAttribute(doc, status, "string");
status.AppendChild(doc.CreateTextNode("success"));
newRoot.AppendChild(status);
var newData = doc.CreateElement("data");
SetTypeAttribute(doc, newData, "object");
newRoot.AppendChild(newData);
var data = doc.DocumentElement;
var toCopy = new List<XmlNode>();
foreach (XmlNode child in data.ChildNodes)
{
toCopy.Add(child);
}
foreach (var child in toCopy)
{
newData.AppendChild(child);
}
Console.WriteLine(newRoot.OuterXml);
var newReply = Message.CreateMessage(reply.Version, reply.Headers.Action, new XmlNodeReader(newRoot));
foreach (var propName in reply.Properties.Keys)
{
newReply.Properties.Add(propName, reply.Properties[propName]);
}
reply = newReply;
}
}
private void SetTypeAttribute(XmlDocument doc, XmlElement element, string value)
{
var attr = element.Attributes["type"];
if (attr == null)
{
attr = doc.CreateAttribute("type");
attr.Value = value;
element.Attributes.Append(attr);
}
else
{
attr.Value = value;
}
}
public void Validate(ServiceEndpoint endpoint)
{
}
}
public static void Test()
{
string baseAddress = "http://" + Environment.MachineName + ":8000/Service";
string baseAddressTcp = "net.tcp://" + Environment.MachineName + ":8888/Service";
ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddress), new Uri(baseAddressTcp));
var ep1 = host.AddServiceEndpoint(typeof(ITest), new NetTcpBinding(), "");
var ep2 = host.AddServiceEndpoint(typeof(ITest), new WebHttpBinding(), "");
ep2.EndpointBehaviors.Add(new WebHttpBehavior());
ep2.EndpointBehaviors.Add(new MyJsonWrapperInspector());
host.Open();
Console.WriteLine("Host opened");
Console.WriteLine("TCP:");
ChannelFactory<ITest> factory = new ChannelFactory<ITest>(new NetTcpBinding(), new EndpointAddress(baseAddressTcp));
ITest proxy = factory.CreateChannel();
Console.WriteLine(proxy.Test());
((IClientChannel)proxy).Close();
factory.Close();
Console.WriteLine();
Console.WriteLine("Web:");
WebClient c = new WebClient();
Console.WriteLine(c.DownloadString(baseAddress + "/Test"));
Console.Write("Press ENTER to close the host");
Console.ReadLine();
host.Close();
}
}
以下是我如何能够完成我想要的。可能不是完美的解决方案,但对我来说效果很好。
可能性 2 是可行的方法。但我不得不将其更改为:
public Message SerializeReply(
MessageVersion messageVersion,
object[] parameters,
object result)
{
// In this sample we defined our operations as OneWay, therefore, this method
// will not get invoked.
var clientAcceptType = WebOperationContext.Current.IncomingRequest.Accept;
Type type = result.GetType();
var genericResponseType = typeof(Response<>);
var specificResponseType = genericResponseType.MakeGenericType(result.GetType());
var response = Activator.CreateInstance(specificResponseType, result);
Message message;
WebBodyFormatMessageProperty webBodyFormatMessageProperty;
if (clientAcceptType == "application/json")
{
message = Message.CreateMessage(messageVersion, "", response, new DataContractJsonSerializer(specificResponseType));
webBodyFormatMessageProperty = new WebBodyFormatMessageProperty(WebContentFormat.Json);
}
else
{
message = Message.CreateMessage(messageVersion, "", response, new DataContractSerializer(specificResponseType));
webBodyFormatMessageProperty = new WebBodyFormatMessageProperty(WebContentFormat.Xml);
}
var responseMessageProperty = new HttpResponseMessageProperty
{
StatusCode = System.Net.HttpStatusCode.OK
};
message.Properties.Add(HttpResponseMessageProperty.Name, responseMessageProperty);
message.Properties.Add(WebBodyFormatMessageProperty.Name, webBodyFormatMessageProperty);
return message;
}
这里的关键是,由于 Response 是一个泛型类型,WCF 需要知道所有已知类型并且不可能手动列出它们。我决定我所有的 return 类型都将实现自定义 IDataContract class(是的,空的):
public interface IDataContract
{
}
然后,我在 Response 中所做的是实现一个 GetKnownTypes
方法,并在其中循环遍历所有在程序集中实现 IDataContract 的 classes 和 return 它们在一个数组中。这是我的响应对象:
[DataContract(Name = "ResponseOf{0}")]
[KnownType("GetKnownTypes")]
public class Response<T>
where T : class
{
public static Type[] GetKnownTypes()
{
var type = typeof(IDataContract);
var types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => type.IsAssignableFrom(p));
return types.ToArray();
}
[DataMember(Name = "status")]
public ResponseStatus ResponseStatus { get; set; }
[DataMember(Name = "data")]
public object Data { get; set; }
public Response()
{
ResponseStatus = ResponseStatus.Success;
}
public Response(T data) : base()
{
Data = data;
}
}
这使我能够通过 TCP 连接并直接交换对象,并在 JSON 或 XML 中通过 WebHTTP 进行很好的序列化。