Reputation: 151
O Great Ones of StackOverflow, hear my plea:
I'm writing .NET 4.5 library code to talk to Oracle SalesCloud services, and I'm having problems with the SOAP requests that have null string values on the C# object.
The XSD for the attributes are specified as follows:
<xsd:element minOccurs="0" name="OneOfTheStringProperties" nillable="true" type="xsd:string" />
Using the "Add Service Reference..." utility in VS2013 and writing a request where I'm updating something other than OneOfTheStringProperties, the output is
<OneOfTheStringProperties xsi:nil="true></OneOfTheStringProperties>
On the server end, this causes two problems. First, since read-only properties are also specified this way, the server rejects the entire request. Second, it means that I could be unintentionally blanking out values that I want to keep (unless I send every property back every time...inefficient and a pain to code.)
My Google-fu is weak on this one, and I don't want to dig into writing a custom XmlSerializer (and all the testing/edge cases that go along with it) unless it's the best route.
So far, the best I could find is to follow the the [Property]Specified pattern. Doing this, then, for each string property available means that I have to add the following to the definition in Reference.cs
[System.Xml.Serialization.XmlIgnoreAttribute()]
public bool OneOfTheStringPropertiesSpecified
{
get { return !String.IsNullOrEmpty(OneOfTheStringProperties); }
set { }
}
It's a LOT of typing, but it works, and the log traces for the SOAP messages are correct.
I'm hoping for advice on one of three avenues of approach:
A config switch, specific XmlSerializer override, or some other fix that will suppress the .NET 4.5 XmlSerializer output for null strings
Something like the same secret formula that will put out "proper" XML such as <OneOfTheStringProperties xsi:nil="true" />
A targeted tutorial to create an extension (or an existing VS2013 extension) that will allow me to right-click on a string property and insert the the following pattern:
[System.Xml.Serialization.XmlIgnoreAttribute()]
public bool [$StringProperty]Specified
{
get { return !String.isNullOrEmpty([$StringProperty]); }
set { }
}
I'm also open to any other suggestions. If it's a matter of just using the proper search terms (which, apparently, I'm not), that would be greatly appreciated, too.
In furtherance of this request, O Keepers of the Knowledge, I offer this sacrificial goat.
Added For Clarification
Just to make sure, I'm not looking for a one-click magic bullet. As a developer, and especially one who works on a team where the underlying structure often changes due to requirements, I know that it takes a lot of work to keep things up.
What I am looking for, though, is a reasonable reduction in the workload every time I have to do a refresh on the structure (and for others, a simplified recipe to achieve the same thing.) For example, using the *Specified means typing in approximately 165+ characters for the given example. For a contract with 45 string fields, that means I would have to type over 7,425 characters each and every time the model changes--and that's for one service object! There are about 10-20 service objects up for grabs.
The right-click idea would reduce it down to 45 right-click-click operations...better.
A custom attribute put on the class would be even better, as it only would have to be done once per refresh.
Ideally, a runtime setting in app.config would be a one-and-done--doesn't matter how hard it is to implement the first time, since that goes into the library.
I think the true answer is somewhere better than almost 7500 characters/class and probably not as good as a simple app.config setting, but it's either out there or I believe it can be made.
Upvotes: 3
Views: 1373
Reputation: 4284
Here's how to add a custom behaviour to the WCF client which can be used to inspect the message and skip the properties.
This is a combination of:
Full Code:
void Main()
{
var endpoint = new Uri("http://somewhere/");
var behaviours = new List<IEndpointBehavior>()
{
new SkipConfiguredPropertiesBehaviour(),
};
var channel = Create<IRemoteService>(endpoint, GetBinding(endpoint), behaviours);
channel.SendData(new Data()
{
SendThis = "This should appear in the HTTP request.",
DoNotSendThis = "This should not appear in the HTTP request.",
});
}
[ServiceContract]
public interface IRemoteService
{
[OperationContract]
int SendData(Data d);
}
public class Data
{
public string SendThis { get; set; }
public string DoNotSendThis { get; set; }
}
public class SkipConfiguredPropertiesBehaviour : IEndpointBehavior
{
public void AddBindingParameters(
ServiceEndpoint endpoint,
BindingParameterCollection bindingParameters)
{
}
public void ApplyClientBehavior(
ServiceEndpoint endpoint,
ClientRuntime clientRuntime)
{
clientRuntime.MessageInspectors.Add(new SkipConfiguredPropertiesInspector());
}
public void ApplyDispatchBehavior(
ServiceEndpoint endpoint,
EndpointDispatcher endpointDispatcher)
{
}
public void Validate(
ServiceEndpoint endpoint)
{
}
}
public class SkipConfiguredPropertiesInspector : IClientMessageInspector
{
public void AfterReceiveReply(
ref Message reply,
object correlationState)
{
Console.WriteLine("Received the following reply: '{0}'", reply.ToString());
}
public object BeforeSendRequest(
ref Message request,
IClientChannel channel)
{
Console.WriteLine("Was going to send the following request: '{0}'", request.ToString());
request = TransformMessage(request);
return null;
}
private Message TransformMessage(Message oldMessage)
{
Message newMessage = null;
MessageBuffer msgbuf = oldMessage.CreateBufferedCopy(int.MaxValue);
XPathNavigator nav = msgbuf.CreateNavigator();
//load the old message into xmldocument
var ms = new MemoryStream();
using(var xw = XmlWriter.Create(ms))
{
nav.WriteSubtree(xw);
xw.Flush();
xw.Close();
}
ms.Position = 0;
XDocument xdoc = XDocument.Load(XmlReader.Create(ms));
//perform transformation
var elementsToRemove = xdoc.Descendants().Where(d => d.Name.LocalName.Equals("DoNotSendThis")).ToArray();
foreach(var e in elementsToRemove)
{
e.Remove();
}
// have a cheeky read...
StreamReader sr = new StreamReader(ms);
Console.WriteLine("We're really going to write out: " + xdoc.ToString());
//create the new message
newMessage = Message.CreateMessage(xdoc.CreateReader(), int.MaxValue, oldMessage.Version);
return newMessage;
}
}
public static T Create<T>(Uri endpoint, Binding binding, List<IEndpointBehavior> behaviors = null)
{
var factory = new ChannelFactory<T>(binding);
if (behaviors != null)
{
behaviors.ForEach(factory.Endpoint.Behaviors.Add);
}
return factory.CreateChannel(new EndpointAddress(endpoint));
}
public static BasicHttpBinding GetBinding(Uri uri)
{
var binding = new BasicHttpBinding()
{
MaxBufferPoolSize = 524288000, // 10MB
MaxReceivedMessageSize = 524288000,
MaxBufferSize = 524288000,
MessageEncoding = WSMessageEncoding.Text,
TransferMode = TransferMode.Buffered,
Security = new BasicHttpSecurity()
{
Mode = uri.Scheme == "http" ? BasicHttpSecurityMode.None : BasicHttpSecurityMode.Transport,
}
};
return binding;
}
Here's a link to the LinqPad script: http://share.linqpad.net/kgg8st.linq
If you run it, the output will be something like:
Was going to send the following request: '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header>
<Action s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">http://tempuri.org/IRemoteService/SendData</Action>
</s:Header>
<s:Body>
<SendData xmlns="http://tempuri.org/">
<d xmlns:d4p1="http://schemas.datacontract.org/2004/07/" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<d4p1:DoNotSendThis>This should not appear in the HTTP request.</d4p1:DoNotSendThis>
<d4p1:SendThis>This should appear in the HTTP request.</d4p1:SendThis>
</d>
</SendData>
</s:Body>
</s:Envelope>'
We're really going to write out: <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header>
<Action a:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none" xmlns:a="http://schemas.xmlsoap.org/soap/envelope/">http://tempuri.org/IRemoteService/SendData</Action>
</s:Header>
<s:Body>
<SendData xmlns="http://tempuri.org/">
<d xmlns:a="http://schemas.datacontract.org/2004/07/" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<a:SendThis>This should appear in the HTTP request.</a:SendThis>
</d>
</SendData>
</s:Body>
</s:Envelope>
Upvotes: 3
Reputation: 820
This isn't the perfect solution but along the 45 right click line, you could use a T4 Text Template to generate the XXXSpecified properties in a partial class declaration separated from the generated web service code.
It would then be a single right click -> run custom tool to regenerate the XXXSpecified code when the service reference is updated.
Here is an example template, which generates code for all string properties of classes in a given namespace:
<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="$(SolutionDir)<Path to assembly containing service objects>" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Reflection" #>
<#@ output extension=".cs" #>
<#
string serviceObjectNamespace = "<Namespace containing service objects>";
#>
namespace <#= serviceObjectNamespace #> {
<#
foreach (Type type in AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(t => t.GetTypes())
.Where(t => t.IsClass && t.Namespace == serviceObjectNamespace)) {
var properties = type.GetProperties().Where(p => p.PropertyType == typeof(string));
if (properties.Count() > 0) {
#>
public partial class <#= type.Name #> {
<#
foreach (PropertyInfo prop in properties) {
#>
[System.Xml.Serialization.XmlIgnoreAttribute()]
public bool <#= prop.Name#>Specified
{
get { return <#= prop.Name#> != null; }
set { }
}
<#
}
#>
}
<#
} }
#>
}
Upvotes: 3