WCF message:How to remove the SOAP Header element? - c#

I try to delete the whole SOAP header from a WCF message, just only want to leave the envelope body. Anybody can give me an idea how can do that?
Create a WCF message like this:
**string response = "Hello World!";
Message msg = Message.CreateMessage(MessageVersion.Soap11, "*", new TextBodyWriter(response));
msg.Headers.Clear();**
The sending SOAP message will be:
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header />
<s:Body>
<Binary>Hello World!</Binary>
</s:Body>
</s:Envelope>
But I don't want to the SOAP header element, which I just only need the envelop body.How to remove the header element from a WCF message?

Option 1: Use bacicHttpBinding, it will not add content to the header (when not configured for security)
Option 2: Implement a custom mesaage encoder and strip the header there. anywhere before that there is a chance wcf will add the header back. See sample encoder here.

That question is a tricky one: let's take it step by step
Some Context
The Message class writes its headers in its ToString() method. ToString() then calls an internal overload ToString(XmlDictionaryWriter writer) which then starts writing:
// System.ServiceModel.Channels.Message
internal void ToString(XmlDictionaryWriter writer)
{
if (this.IsDisposed)
{
throw TraceUtility.ThrowHelperError(this.CreateMessageDisposedException(), this);
}
if (this.Version.Envelope != EnvelopeVersion.None)
{
this.WriteStartEnvelope(writer);
this.WriteStartHeaders(writer);
MessageHeaders headers = this.Headers;
for (int i = 0; i < headers.Count; i++)
{
headers.WriteHeader(i, writer);
}
writer.WriteEndElement();
MessageDictionary arg_60_0 = XD.MessageDictionary;
this.WriteStartBody(writer);
}
this.BodyToString(writer);
if (this.Version.Envelope != EnvelopeVersion.None)
{
writer.WriteEndElement();
writer.WriteEndElement();
}
}
The this.WriteStartHeaders(writer); code writes the header tag regardless of the number of headers. It is matched by the writer.WriteEndElement() after the for loop. This writer.WriteEndElement() must be matched with the header tag being written, else the Xml document will be invalid.
So there is no way we can override a virtual method to get rid of the headers: WriteStartHeaders calls the virtual method OnWriteStartHeaders but the tag closing prevents simply shutting it off). We have to change the whole ToString() method in order to remove any header-related structure, to arrive at:
- write start of envelope
- write start of body
- write body
- write end of body
- write end of envelope
Solutions
In the above pseudocode, we have control on everything but the "write body" part. All methods called in the initial ToString(XmlDictionaryWriter writer) are public except BodyToString. So we will need to call it through reflection or whichever method fits your needs. Writing a message without its headers simply becomes:
private void ProcessMessage(Message msg, XmlDictionaryWriter writer)
{
msg.WriteStartEnvelope(writer); // start of envelope
msg.WriteStartBody(writer); // start of body
var bodyToStringMethod = msg.GetType()
.GetMethod("BodyToString", BindingFlags.Instance | BindingFlags.NonPublic);
bodyToStringMethod.Invoke(msg, new object[] {writer}); // write body
writer.WriteEndElement(); // write end of body
writer.WriteEndElement(); // write end of envelope
}
Now we have a way to get our message content without the headers. But how should this method be invoked?
We only want the message without headers as a string
Great, we don't need to care about overriding the ToString() method that then calls the initial writing of the message. Just create a method in your program that takes a Message and an XmlDictionaryWriter and call it to get the message without its headers.
We want the ToString() method to return the message without headers
This one is a bit more complicated. We cannot easily inherit from the Message class because we would need to pull out a lot of dependencies out of the System.ServiceModel assembly. I won't go there in this answer.
What we can do is use the capabilities of some frameworks to create a proxy around an existing object and to intercept some calls to the original object in order to replace/enhance its behavior: I'm used to Castle Dynamic proxy so let's use that.
We want to intercept the ToString() method so we create a proxy around the Message object we are using and add an interceptor to replace the ToString method of the Message with our implementation:
var msg = Message.CreateMessage(MessageVersion.Soap11, "*");
msg.Headers.Clear();
var proxyGenerator = new Castle.DynamicProxy.ProxyGenerator();
var proxiedMessage = proxyGenerator.CreateClassProxyWithTarget(msg, new ProxyGenerationOptions(),
new ToStringInterceptor());
The ToStringInterceptor needs to do almost the same thing as the initial ToString() method, we will however use our ProcessMessage method defined above:
public class ToStringInterceptor : IInterceptor
{
public void Intercept(IInvocation invocation)
{
if (invocation.Method.Name != "ToString")
{
invocation.Proceed();
}
else
{
var result = string.Empty;
var msg = invocation.InvocationTarget as Message;
StringWriter stringWriter = new StringWriter(CultureInfo.InvariantCulture);
XmlDictionaryWriter xmlDictionaryWriter =
XmlDictionaryWriter.CreateDictionaryWriter(new XmlTextWriter(stringWriter));
try
{
ProcessMessage(msg, xmlDictionaryWriter);
xmlDictionaryWriter.Flush();
result = stringWriter.ToString();
}
catch (XmlException ex)
{
result = "ErrorMessage";
}
invocation.ReturnValue = result;
}
}
private void ProcessMessage(Message msg, XmlDictionaryWriter writer)
{
// same method as above
}
}
And here we are: calls to the ToString() method of the message will now return a envelope without headers. We can pass the message to other parts of the framework and know it should mostly work: direct calls to some of the internal plumbing of Message can still produce the initial output but short of a full reimplementation we cannot control that.
Points of note
This is the shortest way to removing the headers I found. The fact that the header serialisation in the writer was not handled in one virtual function only was a big problem. The code doesn't give you much wriggle room.
This implementation doesn't use the same XmlWriter as the one used in the original implementation of ToString() in the Message, EncodingFallbackAwareXmlTextWriter. This class is internal in System.ServiceModel and pulling it out is left as an exercice to the reader. As a result, the output differs slightly since the xml is not formatted with the simple XmlTextWriter I use.
The interceptor could simply have parsed the xml returned from the initial ToString() call and removed the headers node before letting the value bubble up. This is another viable solution.
Raw code
public class ToStringInterceptor : IInterceptor
{
public void Intercept(IInvocation invocation)
{
if (invocation.Method.Name != "ToString")
{
invocation.Proceed();
}
else
{
var result = string.Empty;
var msg = invocation.InvocationTarget as Message;
StringWriter stringWriter = new StringWriter(CultureInfo.InvariantCulture);
XmlDictionaryWriter xmlDictionaryWriter =
XmlDictionaryWriter.CreateDictionaryWriter(new XmlTextWriter(stringWriter));
try
{
ProcessMessage(msg, xmlDictionaryWriter);
xmlDictionaryWriter.Flush();
result = stringWriter.ToString();
}
catch (XmlException ex)
{
result = "ErrorMessage";
}
invocation.ReturnValue = result;
}
}
private void ProcessMessage(Message msg, XmlDictionaryWriter writer)
{
msg.WriteStartEnvelope(writer);
msg.WriteStartBody(writer);
var bodyToStringMethod = msg.GetType()
.GetMethod("BodyToString", BindingFlags.Instance | BindingFlags.NonPublic);
bodyToStringMethod.Invoke(msg, new object[] { writer });
writer.WriteEndElement();
writer.WriteEndElement();
}
}
internal class Program
{
private static void Main(string[] args)
{
var msg = Message.CreateMessage(MessageVersion.Soap11, "*");
msg.Headers.Clear();
var proxyGenerator = new Castle.DynamicProxy.ProxyGenerator();
var proxiedMessage = proxyGenerator.CreateClassProxyWithTarget(msg, new ProxyGenerationOptions(),
new ToStringInterceptor());
var initialResult = msg.ToString();
var proxiedResult = proxiedMessage.ToString();
Console.WriteLine("Initial result");
Console.WriteLine(initialResult);
Console.WriteLine();
Console.WriteLine("Proxied result");
Console.WriteLine(proxiedResult);
Console.ReadLine();
}
}

I did not have your XmlBodyWriter but you could use data contract serializer or your xml body writer
But the trick is to use msg.WriteBody. this will omit the headers
var response = "Hello";
Message msg = Message.CreateMessage(MessageVersion.Soap11, "*",response, new DataContractSerializer(response.GetType()));
msg.Headers.Clear();
var sb = new StringBuilder();
var xmlWriter = new XmlTextWriter(new StringWriter(sb));
msg.WriteBody(xmlWriter);

Should be something like this:
XmlDocument xml = new XmlDocument();
xml.LoadXml(myXmlString); // suppose that myXmlString contains "<Body>...</Body>"
XmlNodeList xnList = xml.SelectNodes("/Envelope/Body");
foreach (XmlNode xn in xnList)
{
string binary1 = xn["Binary1"].InnerText;
string binary2 = xn["Binary2"].InnerText;
Console.WriteLine("Binary: {0} {1}", binary1 , binary2);
}

Related

IRS-A2A BulkRequestTransmitter message not formmatted properly and/or cannot be interpreted

I am receiving the following error when attempting to submit through the BulkRequestTransmitter Web Service. The Composition Guide is less than helpful as far as this message goes, and when I compare my SOAP XML with the SOAP from the Composition Guide, they seem to be apples-to-apples. I'm hoping that another set of eyes may be able to see where the problem is.
The message was not formatted properly and/or cannot be interpreted. Please review the XML standards outlined in Section 3 of the AIR Submission Composition and Reference Guide located at https://www.irs.gov/for-Tax-Pros/Software-Developers/Information-Returns/Affordable-Care-Act-Information-Return-AIR-Program, correct any issues, and try again.
What I've Tried:
Attempted to submit with (and without) whitespace in the SOAP Envelope.
Attempted to submit with the Form Data XML in XML format.
Attempted to submit with the Form Data in base64string format (as this submission was).
Added the ds prefix to the Signature elements. Used this SO post in order to add the prefix to the Signature elements.
Added the Form Data in "Pretty Print" format and as according to the updated Composition Guide (v4.2).
Copied the formatting of the MIME for the BulkTransmitterService request outlined in section 10.3 of the Composition Guide.
Created two solutions: 1.) Manually creating the XML necessary for the SOAP requests and sending via HttpWebRequest object; 2.) Sending a submission request via the WSDL imported to the project as a Service Reference, using custom encoders for GZip and Mtom Encoding and manually creating the XML necessary for the SOAP Status Request (sent via HttpWebRequest).
Update #1
Updated the request based on some new additions.
Added the ds prefix to the Signature elements.
Added the Form Data in "Pretty Print" format and as according to the updated Composition Guide (v4.2: Section 5.4.2).
Update #2
I began to manually create the SOAP .xml file within a new instance of Visual Studio importing the schema references as necessary. I'm doing this outside of any sort of application creation.
In doing so, I was able to find some additional bugs in the SOAP I was creating through my application (thank you for intellisense!). The bugs that I found were within the Manifest XML, as they didn't conform to the IRS schema.
I will be looking into these in the next 24 hours and update accordingly.
The urn:MailingAddressGrp should have a child of either urn:USAddressGrp or urn:ForeignAddressGrp. That child should then contain the proper address elements. My code is currently missing the direct child of the urn:MailingAddressGrp.
The value for urn1:DocumentSystemFileNm of Form1094C_Request_[TCC]_yyyyMMddThhmmssfffZ.xml is incorrect. I'm not entirely sure what it should be just yet.
The urn1:BulkExchangeFile element, is having an issue related to the xop:Include element I have within. The schema wants a base64Binary type.
Update #2.5
Updated my XML generation process to include the USAddressGrp element.
Discovered that I had one extra character in the milliseconds (four instead of three). Once I corrected this, along with removing the string "Form" from the beginning of the file name, the value for the urn1:DocumentSystemFileNm was able to validate against the schema successfully.
Update #3
Updated the Full Request based on the updates I have made. At this
point, I am unable to deduce what is wrong with my request. If
anyone sees anything glaring, please help!
Update #4
Updated the Full Request based on additional updates made. Removed
the ds prefix from the Signature based on another SO user's
feedback. This user has gotten these requests to work without having
to append the ds prefix to the Signature after the fact and
re-compute the signature.
The SO user also confirmed that his requests are working with an
<inc:Include> element being setup as a child element of the
<BulkExchangeFile> element.
Confirmed the MIME headers are correct as per the sample in section 10.3 of the Composition Guide.
Update #5
I currently have two solutions: one which is sending manually creating the XML necessary for the SOAP requests and sending via HttpWebRequest; and one which is using the WSDL Service Reference for the Submission Request, using the custom encoders outlined below, and manually creating the XML necessary for the SOAP Request of the Status.
As of this update, Solution 1 gives me the error above when making a Submission Request, and gives me the error below when making the Status Request. However, when using Solution 2, both requests (Submission and Status) give me the error below.
I am looking into possible certificate issues to see if they make any progress with either of these solutions.
Update #6
There were a number of issues I ran into which caused me to be delayed. I'll spare you the nitty-gritty details, however, the short of it is that we did not have the Security Certificate registered with the IRS system, nor did we have the Certificate installed properly so that I could access the information through the X509Store. Finally these things got done, and I was able to test submitting data to the IRS from the server (vs. my localmachine which did not have the proper certificate). Unfortunately, I am still receiving the WS-Security error detailed below. I have updated the Full Request with what I am currently sending.
An Error Occurred with message: The WS Security Header in the message is invalid. Please review the transmission instructions outlined in Section 5 of the AIR Submission Composition and Reference Guide located at https://www.irs.gov/for-Tax-Pros/Software-Developers/Information-Returns/Affordable-Care-Act-Information-Return-AIR-Program, correct any issues, and try again.
All line breaks in the MIME headers are as-is, and I believe the line breaks are what is expected.
The FormData attachment is being sent as Pretty Print while the SOAP Envelope is not; The SOAP Envelope in this post is formatted for readability.
UPDATE #7:
Thanks to users: jstill and fatherOfWine with what they have posted below, and to Bon for earlier assistance on this project. I have broken through one wall in getting the Submission to work. It is now working. The Status request is also working. However, I need to figure out how to process it in order to pull the status and the attachment (error data file) out of it.
Full Request:
Content-Encoding: gzip
Accept-Encoding: gzip, deflate
Content-Type: multipart/related; type="application/xop+xml"; start="<rootpart>"; start-info="text/xml"; boundary="MIME_boundary"
SOAPAction: BulkRequestTransmitter
MIME-Version: 1.0
Host: la.www4.irs.gov
--MIME_Boundary
Content-Type: application/xop+xml; charset=UTF-8; type="text/xml"
Content-Transfer-Encoding: 8bit
Content-Id: <root_part>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header>
<Security xmlns:h="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Signature Id="SIG-E77c57b78ebc54e989bfc9e43604a04a4" xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#WithComments" />
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
<Reference URI="#TS-Eb4799bee41bb4df0a72f52832d283ef7">
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
<DigestValue>[TimestampDigestValue]</DigestValue>
</Reference>
<Reference URI="#id-E5f1ed32aab8f4578adeee5debd851a62">
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
<DigestValue>[ACABusinessHeaderDigestValue]</DigestValue>
</Reference>
<Reference URI="#id-E4a71164001994d7f865fc7ddb8055350">
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
<DigestValue>[ManifestDigestValue]</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>[SignatureValue]</SignatureValue>
<KeyInfo Id="KI-E2309cb142e1a4076a2e71373e6e6b75f">
<SecurityTokenReference d6p1:Id="STR-E2751169ee468470290fe5e8bfb34589e" xmlns:d6p1="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<KeyIdentifier EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary" ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3">[KeyIdentifier]</KeyIdentifier>
</SecurityTokenReference>
</KeyInfo>
</Signature>
<a:Timestamp a:Id="TS-Eb4799bee41bb4df0a72f52832d283ef7" xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns:a="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<a:Created>2016-05-18T09:51:05.856Z</a:Created>
<a:Expires>2016-05-18T10:01:05.856Z</a:Expires>
</a:Timestamp>
</Security>
<ACATransmitterManifestReqDtl a:Id="id-E4a71164001994d7f865fc7ddb8055350" xmlns:h="urn:us:gov:treasury:irs:ext:aca:air:7.0" xmlns:a="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns="urn:us:gov:treasury:irs:ext:aca:air:7.0">
<PaymentYr>2015</PaymentYr>
<PriorYearDataInd>0</PriorYearDataInd>
<EIN xmlns="urn:us:gov:treasury:irs:common">000000301</EIN>
<TransmissionTypeCd>O</TransmissionTypeCd>
<TestFileCd>T</TestFileCd>
<OriginalReceiptId />
<TransmitterNameGrp>
<BusinessNameLine1Txt />
</TransmitterNameGrp>
<CompanyInformationGrp>
<CompanyNm>Selitestthree</CompanyNm>
<MailingAddressGrp>
<USAddressGrp>
<AddressLine1Txt>6689 Willow Court</AddressLine1Txt>
<CityNm xmlns="urn:us:gov:treasury:irs:common">Beverly Hills</CityNm>
<USStateCd>CA</USStateCd>
<USZIPCd xmlns="urn:us:gov:treasury:irs:common">90211</USZIPCd>
</USAddressGrp>
</MailingAddressGrp>
<ContactNameGrp>
<PersonFirstNm>Rose</PersonFirstNm>
<PersonLastNm>Lincoln</PersonLastNm>
</ContactNameGrp>
<ContactPhoneNum>5559876543</ContactPhoneNum>
</CompanyInformationGrp>
<VendorInformationGrp>
<VendorCd>I</VendorCd>
<ContactNameGrp>
<PersonFirstNm>ContactFirstName</PersonFirstNm>
<PersonLastNm>ContactLastName</PersonLastNm>
</ContactNameGrp>
<ContactPhoneNum>ContactPhoneNumber</ContactPhoneNum>
</VendorInformationGrp>
<TotalPayeeRecordCnt>3</TotalPayeeRecordCnt>
<TotalPayerRecordCnt>1</TotalPayerRecordCnt>
<SoftwareId>PPACA</SoftwareId>
<FormTypeCd>1094/1095C</FormTypeCd>
<BinaryFormatCd xmlns="urn:us:gov:treasury:irs:common">application/xml</BinaryFormatCd>
<ChecksumAugmentationNum xmlns="urn:us:gov:treasury:irs:common">6b2512ce28f603f76261923d297738e5</ChecksumAugmentationNum>
<AttachmentByteSizeNum xmlns="urn:us:gov:treasury:irs:common">14076</AttachmentByteSizeNum>
<DocumentSystemFileNm>1094C_Request_[TCC]_20160518T215105716Z.xml</DocumentSystemFileNm>
</ACATransmitterManifestReqDtl>
<ACABusinessHeader a:Id="id-E5f1ed32aab8f4578adeee5debd851a62" xmlns:h="urn:us:gov:treasury:irs:msg:acabusinessheader" xmlns:a="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns="urn:us:gov:treasury:irs:msg:acabusinessheader">
<UniqueTransmissionId xmlns="urn:us:gov:treasury:irs:ext:aca:air:7.0">51958882-c653-4eab-8dfb-287ecc555aaa:SYS12:[TCC]::T</UniqueTransmissionId>
<Timestamp xmlns="urn:us:gov:treasury:irs:common">2016-05-18T14:51:05.8568594-07:00</Timestamp>
</ACABusinessHeader>
</s:Header>
<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<ACABulkRequestTransmitter xmlns="urn:us:gov:treasury:irs:msg:irsacabulkrequesttransmitter" version="1.0">
<BulkExchangeFile xmlns="urn:us:gov:treasury:irs:common">
<inc:Include href="cid:1094C_Request_BB0S4_20160518T215105716Z.xml" xmlns:inc="http://www.w3.org/2004/08/xop/include" />
</BulkExchangeFile>
</ACABulkRequestTransmitter>
</s:Body>
</s:Envelope>
--MIME_Boundary
Content-Type: text/xml; charset=us-ascii
Content-Transfer-Encoding: 7bit
Content-Id: <1094C_Request_[TCC]_20160518T215105716Z.xml>
Content-Disposition: attachment; name="1094C_Request_[TCC]_20160518T215105716Z.xml"
[PrettyPrintFormDataXml]
--MIME_boundary--
Don't know if it will resolve your issue, but nevertheless i give it a shot. Sometimes help comes from very unexpected sources :)
First of all timestamp fields are in a wrong format: one in
businessheader should NOT contain milliseconds at all. I know it for
a fact.
In security header timestamp only 3 digits are allowed to represent
milliseconds.
Remove empty elements like "OriginalReceiptId" from ACATransmitterManifestReqDtl element: they don't like those.
I hope you are providing them with proper software id, because you have it empty in the payload, but I am sure they would love to have it, imho.:)
And I think the message you've got in the response also have something to do with Signature element. I think they want Signature element to have some prefix("ds" preferably, I guess). But here I am not sure on 100%.
You see, I am battling same battle as you. And my message security timestamp has prefix "u" and they do not complain about it. Though they didn't like binarysecuritytoken ever.:) I am struggling to generate signature to the IRS liking. WCF is very secretive and does not allow easy prefix changing on soap envelope or allow to choose CanonicalizationMethod algorithm for a signature.
UPDATE: Been able to successfully send request to the service. Tell you at once: prefixes are unimportant. What was important: CorrectedInd tag must be present in Form1095BUpstreamDetail and attributes recordType="String" lineNum="0" also must be present.
UPDATE2:
Another thing that I've changed I've placed ACABusinessHeader before ManifestDtl.
Here are my settings: I am using WCF as carrier and SignedXml to generate signature. Also I am using custom gZip encoder(for obvious reasons0 and custom MtomEncoder to read response from service(yes, yes it's MTOMed:)) can you believe those pokemons?!?!?) and that's not all: they send response as multipart document with only 1 part!:)) I had to adjust my encoder to handle that. And voilĂ , service started to behave. Hope it might help.
UPDATE3
First of all make sure data in attachment file correspond to the test scenario you are using as guinea pig. I, probably, sound like a broken record, but that's REALLY important.
Now I'll cut the stuff and present what I have. It's a bit crude, but it does the trick.:)
1.Here is config file portion:
1.1.Make sure system.serviceModel element contains following portion:
<extensions>
<bindingElementExtensions>
<add name="gzipMessageEncoding" type="<namespaceWhereEncoderLives>.GZipMessageEncodingElement, GZipEncoder, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null" />
</bindingElementExtensions>
</extensions>
1.2. Make sure binding element contains this:
<customBinding>
<binding name="BulkRequestTransmitterBinding">
<gzipMessageEncoding innerMessageEncoding="textMessageEncoding" />
<httpsTransport />
</binding>
</customBinding>
1.3. Change binding of BulkRequestTransmitterPort endpoit under client element to "customBinding"(and change binding name to the name of the custom binding as well) and make sure it contains following portion:
<identity>
<dns value="domain from cert" />
</identity>
Also client element should contain following portion:
<metadata>
<policyImporters>
<extension type="NamespaceToToTheLocationOf.GZipMessageEncodingBindingElementImporter, GZipMessageEncoder, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</policyImporters>
</metadata>
GZip encoder you could get from following link:
https://msdn.microsoft.com/en-us/library/cc138373(v=vs.90).aspx
Just download WCF example and dully move whole GZipMessageEncoder project under your project.
Get MTOMEncoder(which I renamed from SwaEncoder for clarity reasons) from this link:
Soap-with-Attachments
Move following classes into GZipMessageEncoder project:
MimeContent, MimeParser, MimePart, MTOMEncoder
Modify GZipMessageEncoder class like this:
4.1. Add following code at the beginning of the class:
//------------------- MTOM related stuff. Begin. ---------------------
const string ATTCHMNT_PROP = "attachment_file_content";
const string ATTCHMNT_CONTENT_ID = "Here goes content id";
private string _ContentType;
private string _MediaType;
protected MimeContent _MyContent;
protected MimePart _SoapMimeContent;
protected MimePart _AttachmentMimeContent;
protected GZipMessageEncoderFactory _Factory;
protected MimeParser _MimeParser;
private void SetupMTOM(GZipMessageEncoderFactory factory)
{
//
_ContentType = "multipart/related";
_MediaType = _ContentType;
//
// Create owned objects
//
_Factory = factory;
_MimeParser = new MimeParser();
//
// Create object for the mime content message
//
_SoapMimeContent = new MimePart()
{
ContentTypeStart = "application/xop+xml",
ContentType = "text/xml",
ContentId = "Here goes envelope MIME id from HTTP Content-Type header", // TODO: make content id dynamic or configurable?
CharSet = "UTF-8", // TODO: make charset configurable?
TransferEncoding = "8bit" // TODO: make transfer-encoding configurable?
};
_AttachmentMimeContent = new MimePart()
{
ContentType = "application/xml", // TODO: AttachmentMimeContent.ContentType configurable?
ContentId = ATTCHMNT_CONTENT_ID, // TODO: AttachmentMimeContent.ContentId configurable/dynamic?
TransferEncoding = "7bit" // TODO: AttachmentMimeContent.TransferEncoding dynamic/configurable?
};
_MyContent = new MimeContent()
{
Boundary = "here goes boundary id" // TODO: MimeContent.Boundary configurable/dynamic?
};
_MyContent.Parts.Add(_SoapMimeContent);
_MyContent.Parts.Add(_AttachmentMimeContent);
_MyContent.SetAsStartPart(_SoapMimeContent);
}
//------------------- MTOM related stuff. End. ----------------------
4.2. Modify Method WriteMessage(Message message, int maxMessageSize, BufferManager bufferManager, int messageOffset) like this:
public override ArraySegment<byte> WriteMessage(Message message, int maxMessageSize, BufferManager bufferManager, int messageOffset)
{
ArraySegment<byte> buffer = innerEncoder.WriteMessage(message, maxMessageSize, bufferManager, 0);
var requestSOAPEnvelopeXml = System.Text.Encoding.UTF8.GetString(buffer.Array);
//Here you create Security node and sign the request. For ex:
requestSOAPEnvelopeXml = SigngEnvelope(requestSOAPEnvelopeXml);
//Here you are getting 1094\1095 forms xml payload.
string fileContent = GetAttachmentFileContent();
//Here comes the MTOMing...
_SoapMimeContent.Content = System.Text.Encoding.UTF8.GetBytes(requestSOAPEnvelopeXml);
_AttachmentMimeContent.Content = System.Text.Encoding.UTF8.GetBytes(fileContent);
_MyContent.Parts.Where(m=> m.ContentId!=null && m.ContentId.Equals(ATTCHMNT_CONTENT_ID)).Single().ContentDisposition = GetFileName(envelope);
// Now create the message content for the stream
byte[] MimeContentBytes = _MimeParser.SerializeMimeContent(_MyContent);
int MimeContentLength = MimeContentBytes.Length;
// Write the mime content into the section of the buffer passed into the method
byte[] TargetBuffer = bufferManager.TakeBuffer(MimeContentLength + messageOffset);
Array.Copy(MimeContentBytes, 0, TargetBuffer, messageOffset, MimeContentLength);
// Return the segment of the buffer to the framework
return CompressBuffer(new ArraySegment<byte>(TargetBuffer, messageOffset, MimeContentLength), bufferManager, messageOffset);
}
4.3. Override couple more methods like this:
public override Message ReadMessage(ArraySegment<byte> buffer, BufferManager bufferManager, string contentType)
{
ArraySegment<byte> decompressedBuffer = DecompressBuffer(buffer, bufferManager);
MtomEncoder mtomEncoder = new MtomEncoder(innerEncoder, _Factory);
Message returnMessage = mtomEncoder.ReadMessage(buffer, bufferManager, contentType);
returnMessage.Properties.Encoder = mtomEncoder;
return returnMessage;
}
public override bool IsContentTypeSupported(string contentType)
{
return true;
}
4.4. Make sure GZipMessage constructor looks like this:
internal GZipMessageEncoder(MessageEncoder messageEncoder, GZipMessageEncoderFactory factory)
: base()
{
if (messageEncoder == null)
throw new ArgumentNullException("messageEncoder", "A valid message encoder must be passed to the GZipEncoder");
innerEncoder = messageEncoder;
SetupMTOM(factory);
}
5. Make sure GZipMessageEncodingBindingElement class has following method:
public override void ApplyConfiguration(BindingElement bindingElement)
{
GZipMessageEncodingBindingElement binding = (GZipMessageEncodingBindingElement)bindingElement;
PropertyInformationCollection propertyInfo = this.ElementInformation.Properties;
if (propertyInfo["innerMessageEncoding"].ValueOrigin != PropertyValueOrigin.Default)
{
switch (this.InnerMessageEncoding)
{
case "textMessageEncoding":
binding.InnerMessageEncodingBindingElement =
new TextMessageEncodingBindingElement(MessageVersion.Soap11, Encoding.UTF8);
break;
case "binaryMessageEncoding":
binding.InnerMessageEncodingBindingElement = new BinaryMessageEncodingBindingElement();
break;
}
}
}
Modify MTOMEncoder class. Make sure that following method looks like this:
public override Message ReadMessage(System.IO.Stream stream, int maxSizeOfHeaders, string contentType)
{
VerifyOperationContext();
if (contentType.ToLower().StartsWith("multipart/related"))
{
byte[] ContentBytes = new byte[stream.Length];
stream.Read(ContentBytes, 0, ContentBytes.Length);
MimeContent Content = _MimeParser.DeserializeMimeContent(contentType, ContentBytes);
if (Content.Parts.Count >= 1)
{
MemoryStream ms = new MemoryStream(Content.Parts[0].Content);
//At least for now IRS is sending SOAP envelope as 1st part(and only part(sic!) of MULTIpart response) as xml.
Message Msg = ReadMessage(ms, int.MaxValue, "text/xml");//Content.Parts[0].ContentType);
if( Content.Parts.Count>1 )
Msg.Properties.Add(ATTCHMNT_PROP, Content.Parts[1].Content);
return Msg;
}
else
{
throw new ApplicationException("Invalid mime message sent! Soap with attachments makes sense, only, with at least 2 mime message content parts!");
}
}
else if (contentType.ToLower().StartsWith("text/xml"))
{
XmlReader Reader = XmlReader.Create(stream);
return Message.CreateMessage(Reader, maxSizeOfHeaders, MessageVersion);
}
else
{
throw new ApplicationException(
string.Format(
"Invalid content type for reading message: {0}! Supported content types are multipart/related and text/xml!",
contentType));
}
}
GZipMessageEncoderFactory class constructor should look like this:
public GZipMessageEncoderFactory(MessageEncoderFactory messageEncoderFactory)
{
if (messageEncoderFactory == null)
throw new ArgumentNullException("messageEncoderFactory", "A valid message encoder factory must be passed to the GZipEncoder");
encoder = new GZipMessageEncoder(messageEncoderFactory.Encoder, this);
}
This is how I call the service:
var requestClient = new BulkRequestTransmitterPortTypeClient("BulkRequestTransmitterPort");
requestClient.Endpoint.Contract.ProtectionLevel = System.Net.Security.ProtectionLevel.None;
#if DEBUG
var vs = requestClient.Endpoint.Behaviors.Where((i) => i.GetType().Namespace.Contains("VisualStudio"));
if( vs!=null )
requestClient.Endpoint.Behaviors.Remove((System.ServiceModel.Description.IEndpointBehavior)vs.Single());
#endif
using (var scope = new OperationContextScope(requestClient.InnerChannel))
{
//Adding proper HTTP Header to an outgoing requqest.
HttpRequestMessageProperty requestMessage = new HttpRequestMessageProperty();
requestMessage.Headers["Content-Encoding"] = "gzip";
requestMessage.Headers["Content-Type"] = #"multipart/related; type=""application/xop+xml"";start=""<Here goes envelope boundary id>"";start-info=""text/xml"";boundary=""here goes boundary id""";
OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = requestMessage;
response = requestClient.BulkRequestTransmitter(request.ACASecurityHeader,
request.Security, ref request.ACABusinessHeader,
request.ACATransmitterManifestReqDtl,
request.ACABulkRequestTransmitter);
}
Modify Mime Part:
9.1. Add new method:
public void GetHeader(StringBuilder Builder)
{
if (string.IsNullOrEmpty(ContentId) && string.IsNullOrEmpty(ContentType) && string.IsNullOrEmpty(TransferEncoding))
return;
if (!string.IsNullOrEmpty(ContentTypeStart))
{
Builder.Append(string.Format("Content-Type: {0}", ContentTypeStart));
Builder.Append(string.Format("; type=\"{0}\"", ContentType));
}
else
Builder.Append(string.Format("Content-Type: {0}", ContentType));
if (!string.IsNullOrEmpty(CharSet)) Builder.Append(string.Format("; charset={0}", CharSet));
Builder.Append(new char[] { '\r', '\n' });
Builder.Append(string.Format("Content-Transfer-Encoding: {0}", TransferEncoding));
Builder.Append(new char[] { '\r', '\n' });
Builder.Append(string.Format("Content-Id: {0}", ContentId));
Builder.Append(new char[] { '\r', '\n' });
if (!string.IsNullOrEmpty(ContentDisposition))
Builder.Append(string.Format("Content-Disposition: attachment; filename=\"{0}\"", ContentDisposition));
}
9.2. Add property:
public string ContentDisposition { get; set; }
Modify MimeParser SerializeMimeContent() method:
replace this block of code:
Builder.Append(string.Format("Content-Type: {0}", item.ContentType));
if (!string.IsNullOrEmpty(item.CharSet)) Builder.Append(string.Format("; charset={0}", item.CharSet));
Builder.Append(new char[] { '\r', '\n' });
Builder.Append(string.Format("Content-Transfer-Encoding: {0}", item.TransferEncoding));
Builder.Append(new char[] { '\r', '\n' });
Builder.Append(string.Format("Content-Id: {0}", item.ContentId));
with this:
item.GetHeader(Builder);
And that's should be it! Kick off your shoes and dig the blues!:)))
First off, a quick disclaimer. This answer was made possible by the great stuff provided by fatherOfWine, Russ, and Bon across this and other SO questions. All I really did was combine a bunch of stuff from them them and hack through the issues I still had until it worked. More importantly, the code provided here is BAD and should probably not be used as-is. I plan on cleaning this up quite a bit now that I know what works, and I'd recommend anyone making use of it to do the same. A big thing that will likely jump out to anyone looking at this is the plethora of static variables I used as a quick hack to get to things all through the pipeline. Seriously, don't use this as-is in production, it is the product of many hours of just throwing things at the wall until something stuck, but it should provide a good starting point to get something better going.
There's too much code to really include it all here, so I'll just go through some highlights and general discoveries then include a link to the VS solution.
Make sure you have your TCC and various other IDs setup already, that you've purchased the right kind of certificate (Page 41 of this doc) and that you've registered the cert properly (see this doc)
I found that removing all the CR-LFs in the soap envelope was necessary to get the message to be accepted. With them in there I would gets faults on some elements of "unexpected child elements" or something like that.
The documentation contradicts itself in several place (see pages 74 and 84 and look at what they say the BulkExchangeFile element should contain for an example) and the wsdl/xsds are just straight up wrong as well. Maybe I just had old ones somehow, but I had to make changes and try things until I found what the service on their side would actually accept.
It is very important that you add the keyinfo to the signedxml section properly, that all of your references there are built right and include the proper InclusiveNamespaces lists, and that once you call ComputeSignature the only change you make to your envelope is to add the signature element to it.
Speaking of the signature element, if it appears after the timestamp element inside the security element the IRS system will return a fault. It has to be first.
Because the namespace prefixes were so important when it came to generating the signature references, I went the route of building the envelope xml by hand so I could be certain that everything matched up exactly with what they wanted. Even then, there were several elements whose prefixes I had to change as I tested because what the XSD or some page in the docs said it should be was not what their service actually wanted. Luckily the faults returned by the service actually provided some help by indicating which namespace it was expecting a value from.
Outside of getting all the gzip and MTOM stuff setup (again, thanks a million to fatherOfWine for that help) the bulk of what finally worked for me is done in single general-use class (which I cleverly called "General"). Again, this is bad code and was the product of just needing to get something (anything!) to work properly. I'll go ahead and include it here in the answer though in case it provides a quick "ah ha!" to anyone else working this problem.
using System;
using System.IO;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography.Xml;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Text;
using System.Xml;
using IrsAcaClient.ACABulkRequestTransmitterService;
namespace IrsAcaClient
{
public class General
{
/*****************************************************
*
* What I'm doing here (with static vars) is VERY BAD but this whole thing is just a dirty hack for now.
* Hopefully I can clean this up later.
* - JRS 2016-05-10
*
*****************************************************/
public const string SecurityTimestampStringFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffZ";
public const string EnvelopeContentID = "<rootpart>";
public static string AttachmentFilePath;
public static string AttachmentFileName { get { return Path.GetFileName(General.AttachmentFilePath); } }
public static string AttachmentContentID {get { return string.Format("<{0}>", General.AttachmentFileName); }}
public const string MIMEBoundary = "MIME_boundary";
public static string TCCode;
public static Guid TransmissionGuid;
public static string UniqueTransmissionId
{
get { return string.Format("{0}:SYS12:{1}::T", TransmissionGuid, TCCode); }
}
public static string SecurityTimeStampWsuId;
public static string ManifestWsuId;
public static string BusinessHeaderWsuId;
public static string SignatureWsuId;
public static string CertificatePath;
public static string CertificatePassword;
public static DateTime SecurityTimestampUTC;
private static string _replacementSoapEnvelope;
public static string ReplacementSoapEnvelope{get { return _replacementSoapEnvelope; }}
private static void GenerateReference(string elementID, string inclusivePrefixList, SignedXmlWithId xSigned)
{
var reference = new Reference()
{
Uri = "#" + elementID
};
XmlDsigExcC14NTransform env = new XmlDsigExcC14NTransform();
env.InclusiveNamespacesPrefixList = inclusivePrefixList;
reference.AddTransform(env);
xSigned.AddReference(reference);
}
public static string GetAttachmentFileContent()
{
//probably not ideal
return File.ReadAllText(AttachmentFilePath);
}
public static string GetFileName()
{
//TODO: this may need to be tweaked slightly from the real filename
return General.AttachmentFileName;
}
public static string GenerateWsuId(string prefix)
{
return string.Format("{0}-{1}", prefix, Guid.NewGuid().ToString().Replace("-", "").ToUpper());
}
internal static void GenerateReplacementSoapEnvelope(ACABulkRequestTransmitterService.SecurityHeaderType securityHeader, ACABulkRequestTransmitterService.ACABulkBusinessHeaderRequestType businessHeader, ACABulkRequestTransmitterService.ACATrnsmtManifestReqDtlType manifest, ACABulkRequestTransmitterService.ACABulkRequestTransmitterType bulkTrans)
{
//load the base envelope xml
var doc = new XmlDocument();
doc.PreserveWhitespace = false;
doc.Load("BaseSoapEnvelope.xml");
/* Need a bunch of namespaces defined
* xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
* xmlns:urn="urn:us:gov:treasury:irs:ext:aca:air:7.0"
* xmlns:urn1="urn:us:gov:treasury:irs:common"
* xmlns:urn2="urn:us:gov:treasury:irs:msg:acabusinessheader"
* xmlns:urn3="urn:us:gov:treasury:irs:msg:irsacabulkrequesttransmitter"
* xmlns:wsa="http://www.w3.org/2005/08/addressing"
* xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
* xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
* xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
* xmlns:xop="http://www.w3.org/2004/08/xop/include"
*/
XmlNamespaceManager nsMgr = new XmlNamespaceManager(doc.NameTable);
nsMgr.AddNamespace("soapenv", "http://schemas.xmlsoap.org/soap/envelope/");
nsMgr.AddNamespace("urn", "urn:us:gov:treasury:irs:ext:aca:air:7.0");
nsMgr.AddNamespace("urn1", "urn:us:gov:treasury:irs:common");
nsMgr.AddNamespace("urn2", "urn:us:gov:treasury:irs:msg:acabusinessheader");
nsMgr.AddNamespace("urn3", "urn:us:gov:treasury:irs:msg:irsacabulkrequesttransmitter");
nsMgr.AddNamespace("wsa", "http://www.w3.org/2005/08/addressing");
nsMgr.AddNamespace("wsse", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
nsMgr.AddNamespace("wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd");
nsMgr.AddNamespace("ds", "http://www.w3.org/2000/09/xmldsig#");
nsMgr.AddNamespace("xop","http://www.w3.org/2004/08/xop/include");
//start replacing values in it
//for securityHeader, should have the following
/*
* securityHeader.Signature.Id
* securityHeader.Timestamp.Id
* securityHeader.Timestamp.Created.Value
* securityHeader.Timestamp.Expires.Value
*/
//doc.SelectSingleNode("//wsse:Security/ds:Signature", nsMgr).Attributes["Id"].Value = securityHeader.Signature.Id;
doc.SelectSingleNode("//wsse:Security/wsu:Timestamp", nsMgr).Attributes["wsu:Id"].Value = securityHeader.Timestamp.Id;
doc.SelectSingleNode("//wsse:Security/wsu:Timestamp/wsu:Created", nsMgr).InnerText = securityHeader.Timestamp.Created.Value;
doc.SelectSingleNode("//wsse:Security/wsu:Timestamp/wsu:Expires", nsMgr).InnerText = securityHeader.Timestamp.Expires.Value;
//for businessHeader, should have the following
/*
* businessHeader.UniqueTransmissionId
* businessHeader.Timestamp
* businessHeader.Id
*/
doc.SelectSingleNode("//urn2:ACABusinessHeader", nsMgr).Attributes["wsu:Id"].Value = businessHeader.Id;
doc.SelectSingleNode("//urn2:ACABusinessHeader/urn:UniqueTransmissionId", nsMgr).InnerText = businessHeader.UniqueTransmissionId;
doc.SelectSingleNode("//urn2:ACABusinessHeader/urn1:Timestamp", nsMgr).InnerText = businessHeader.Timestamp.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ssZ");
//for manifest, should have the following, some of which will need some conversions
/*
* manifest.Id
* manifest.BinaryFormatCd - convert from enum
* manifest.PaymentYr
* manifest.PriorYearDataInd - convert from enum
* manifest.EIN
* manifest.TransmissionTypeCd - convert from enum
* manifest.TestFileCd
* manifest.TransmitterNameGrp.BusinessNameLine1Txt
* manifest.CompanyInformationGrp.CompanyNm
* manifest.CompanyInformationGrp.MailingAddressGrp.Item.AddressLine1Txt
* manifest.CompanyInformationGrp.MailingAddressGrp.Item.CityNm
* manifest.CompanyInformationGrp.MailingAddressGrp.Item.USStateCd - convert from enum
* manifest.CompanyInformationGrp.MailingAddressGrp.Item.USZIPCd
* manifest.CompanyInformationGrp.ContactNameGrp.PersonFirstNm
* manifest.CompanyInformationGrp.ContactNameGrp.PersonLastNm
* manifest.CompanyInformationGrp.ContactPhoneNum
* manifest.VendorInformationGrp.VendorCd
* manifest.VendorInformationGrp.ContactNameGrp.PersonFirstNm
* manifest.VendorInformationGrp.ContactNameGrp.PersonLastNm
* manifest.VendorInformationGrp.ContactPhoneNum
* manifest.TotalPayeeRecordCnt
* manifest.TotalPayerRecordCnt
* manifest.SoftwareId
* manifest.FormTypeCd - convert from enum
* manifest.ChecksumAugmentationNum
* manifest.AttachmentByteSizeNum
* manifest.DocumentSystemFileNm
*/
doc.SelectSingleNode("//urn:ACATransmitterManifestReqDtl", nsMgr).Attributes["wsu:Id"].Value = manifest.Id;
doc.SelectSingleNode("//urn:ACATransmitterManifestReqDtl/urn:PaymentYr", nsMgr).InnerText = manifest.PaymentYr;
doc.SelectSingleNode("//urn:ACATransmitterManifestReqDtl/urn:PriorYearDataInd", nsMgr).InnerText = manifest.PriorYearDataInd.GetXmlEnumAttributeValueFromEnum();
doc.SelectSingleNode("//urn:ACATransmitterManifestReqDtl/urn1:EIN", nsMgr).InnerText = manifest.EIN;
doc.SelectSingleNode("//urn:ACATransmitterManifestReqDtl/urn:TransmissionTypeCd", nsMgr).InnerText = manifest.TransmissionTypeCd.ToString();
doc.SelectSingleNode("//urn:ACATransmitterManifestReqDtl/urn:TestFileCd", nsMgr).InnerText = manifest.TestFileCd;
doc.SelectSingleNode("//urn:ACATransmitterManifestReqDtl/urn:TransmitterNameGrp/urn:BusinessNameLine1Txt", nsMgr).InnerText = manifest.TransmitterNameGrp.BusinessNameLine1Txt;
doc.SelectSingleNode("//urn:ACATransmitterManifestReqDtl/urn:CompanyInformationGrp/urn:CompanyNm", nsMgr).InnerText = manifest.CompanyInformationGrp.CompanyNm;
doc.SelectSingleNode("//urn:ACATransmitterManifestReqDtl/urn:CompanyInformationGrp/urn:MailingAddressGrp/urn:USAddressGrp/urn:AddressLine1Txt", nsMgr).InnerText = ((USAddressGrpType)manifest.CompanyInformationGrp.MailingAddressGrp.Item).AddressLine1Txt;
doc.SelectSingleNode("//urn:ACATransmitterManifestReqDtl/urn:CompanyInformationGrp/urn:MailingAddressGrp/urn:USAddressGrp/urn1:CityNm", nsMgr).InnerText = ((USAddressGrpType)manifest.CompanyInformationGrp.MailingAddressGrp.Item).CityNm;
doc.SelectSingleNode("//urn:ACATransmitterManifestReqDtl/urn:CompanyInformationGrp/urn:MailingAddressGrp/urn:USAddressGrp/urn:USStateCd", nsMgr).InnerText = ((USAddressGrpType)manifest.CompanyInformationGrp.MailingAddressGrp.Item).USStateCd.ToString();
doc.SelectSingleNode("//urn:ACATransmitterManifestReqDtl/urn:CompanyInformationGrp/urn:MailingAddressGrp/urn:USAddressGrp/urn1:USZIPCd", nsMgr).InnerText = ((USAddressGrpType)manifest.CompanyInformationGrp.MailingAddressGrp.Item).USZIPCd;
doc.SelectSingleNode("//urn:ACATransmitterManifestReqDtl/urn:CompanyInformationGrp/urn:ContactNameGrp/urn:PersonFirstNm", nsMgr).InnerText = manifest.CompanyInformationGrp.ContactNameGrp.PersonFirstNm;
doc.SelectSingleNode("//urn:ACATransmitterManifestReqDtl/urn:CompanyInformationGrp/urn:ContactNameGrp/urn:PersonLastNm", nsMgr).InnerText = manifest.CompanyInformationGrp.ContactNameGrp.PersonLastNm;
doc.SelectSingleNode("//urn:ACATransmitterManifestReqDtl/urn:CompanyInformationGrp/urn:ContactPhoneNum", nsMgr).InnerText = manifest.CompanyInformationGrp.ContactPhoneNum;
doc.SelectSingleNode("//urn:ACATransmitterManifestReqDtl/urn:VendorInformationGrp/urn:VendorCd", nsMgr).InnerText = manifest.VendorInformationGrp.VendorCd;
doc.SelectSingleNode("//urn:ACATransmitterManifestReqDtl/urn:VendorInformationGrp/urn:ContactNameGrp/urn:PersonFirstNm", nsMgr).InnerText = manifest.VendorInformationGrp.ContactNameGrp.PersonFirstNm;
doc.SelectSingleNode("//urn:ACATransmitterManifestReqDtl/urn:VendorInformationGrp/urn:ContactNameGrp/urn:PersonLastNm", nsMgr).InnerText = manifest.VendorInformationGrp.ContactNameGrp.PersonLastNm;
doc.SelectSingleNode("//urn:ACATransmitterManifestReqDtl/urn:VendorInformationGrp/urn:ContactPhoneNum", nsMgr).InnerText = manifest.VendorInformationGrp.ContactPhoneNum;
doc.SelectSingleNode("//urn:ACATransmitterManifestReqDtl/urn:TotalPayeeRecordCnt", nsMgr).InnerText = manifest.TotalPayeeRecordCnt;
doc.SelectSingleNode("//urn:ACATransmitterManifestReqDtl/urn:TotalPayerRecordCnt", nsMgr).InnerText = manifest.TotalPayerRecordCnt;
doc.SelectSingleNode("//urn:ACATransmitterManifestReqDtl/urn:SoftwareId", nsMgr).InnerText = manifest.SoftwareId;
doc.SelectSingleNode("//urn:ACATransmitterManifestReqDtl/urn:FormTypeCd", nsMgr).InnerText = manifest.FormTypeCd.GetXmlEnumAttributeValueFromEnum();
doc.SelectSingleNode("//urn:ACATransmitterManifestReqDtl/urn1:BinaryFormatCd", nsMgr).InnerText = manifest.BinaryFormatCd.GetXmlEnumAttributeValueFromEnum();
doc.SelectSingleNode("//urn:ACATransmitterManifestReqDtl/urn1:ChecksumAugmentationNum", nsMgr).InnerText = manifest.ChecksumAugmentationNum;
doc.SelectSingleNode("//urn:ACATransmitterManifestReqDtl/urn1:AttachmentByteSizeNum", nsMgr).InnerText = manifest.AttachmentByteSizeNum;
doc.SelectSingleNode("//urn:ACATransmitterManifestReqDtl/urn:DocumentSystemFileNm", nsMgr).InnerText = manifest.DocumentSystemFileNm;
//for bulkTrans, should have the following
/*
* bulkTrans.BulkExchangeFile.Include.href
*/
doc.SelectSingleNode("//urn3:ACABulkRequestTransmitter/urn1:BulkExchangeFile/xop:Include", nsMgr).Attributes["href"].Value = bulkTrans.BulkExchangeFile.Include.href;
//now do some more security setup
var cert = new X509Certificate2(CertificatePath, CertificatePassword, X509KeyStorageFlags.MachineKeySet);
var exported = cert.Export(X509ContentType.Cert, CertificatePassword);
var base64 = Convert.ToBase64String(exported);
//now compute all the signing stuff
var xSigned = new SignedXmlWithId(doc);
xSigned.Signature.Id = securityHeader.Signature.Id;
// Add the key to the SignedXml document.
xSigned.SigningKey = cert.PrivateKey;
xSigned.Signature.Id = SignatureWsuId;
xSigned.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NWithCommentsTransformUrl;
var keyInfo = new KeyInfo
{
Id = GenerateWsuId("KI")
};
//need to get the keyinfo into the signed xml stuff before we compute sigs, and because it is using some stuff that
//doesn't appear to be supported out of the box we'll work around it by adding a node directly
var sbKeyInfo = new StringBuilder();
sbKeyInfo.Append("<root xmlns:wsse=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd\" xmlns:wsu=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\">");
sbKeyInfo.Append("<wsse:SecurityTokenReference wsu:Id=\"" + GenerateWsuId("STR") + "\">");
sbKeyInfo.Append("<wsse:KeyIdentifier EncodingType=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary\" ValueType=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3\">" + base64.ToString());
sbKeyInfo.Append("</wsse:KeyIdentifier>");
sbKeyInfo.Append("</wsse:SecurityTokenReference>");
sbKeyInfo.Append("</root>");
XmlDocument tempDoc = new XmlDocument();
tempDoc.LoadXml(sbKeyInfo.ToString());
keyInfo.AddClause(new KeyInfoNode((XmlElement)tempDoc.FirstChild.FirstChild));
xSigned.KeyInfo = keyInfo;
GenerateReference(SecurityTimeStampWsuId, "wsse wsa soapenv urn urn1 urn2 urn3", xSigned);
GenerateReference(BusinessHeaderWsuId, "wsa soapenv urn urn1 urn3", xSigned);
GenerateReference(ManifestWsuId, "wsa soapenv urn1 urn2 urn3", xSigned);
// Compute the Signature.
xSigned.ComputeSignature();
//signing stuff must come before the timestamp or the IRS service complains
doc.SelectSingleNode("//wsse:Security", nsMgr).InsertBefore(xSigned.GetXml(), doc.SelectSingleNode("//wsse:Security", nsMgr).FirstChild);
//
_replacementSoapEnvelope = doc.OuterXml;
}
public static ACABulkRequestTransmitterResponseType Run(ACABulkRequestTransmitterService.SecurityHeaderType securityHeader, ACABulkRequestTransmitterService.ACABulkBusinessHeaderRequestType businessHeader, ACABulkRequestTransmitterService.ACATrnsmtManifestReqDtlType manifest, ACABulkRequestTransmitterService.ACABulkRequestTransmitterType bulkTrans)
{
//had some issues early on with the cert on the IRS server, this should probably be removed and retested without it
ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, errors) => true;
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Ssl3 |
SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
var acaSecurityHeader = new ACABulkRequestTransmitterService.TransmitterACASecurityHeaderType(); //leave this empty for transmitting via ISS-A2A
var requestClient = new ACABulkRequestTransmitterService.BulkRequestTransmitterPortTypeClient("BulkRequestTransmitterPort");
requestClient.Endpoint.Contract.ProtectionLevel = System.Net.Security.ProtectionLevel.None;
//var vs = requestClient.Endpoint.Behaviors.Where((i) => i.GetType().Namespace.Contains("VisualStudio"));
//if (vs != null)
// requestClient.Endpoint.Behaviors.Remove((System.ServiceModel.Description.IEndpointBehavior)vs.Single());
//generate the real envelope we want
GenerateReplacementSoapEnvelope(securityHeader, businessHeader, manifest, bulkTrans);
using (var scope = new OperationContextScope(requestClient.InnerChannel))
{
//Adding proper HTTP Header to an outgoing requqest.
HttpRequestMessageProperty requestMessage = new HttpRequestMessageProperty();
requestMessage.Headers["Content-Encoding"] = "gzip";
requestMessage.Headers["Content-Type"] = string.Format(#"multipart/related; type=""application/xop+xml"";start=""{0}"";start-info=""text/xml"";boundary=""{1}""", General.EnvelopeContentID, General.MIMEBoundary);
OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = requestMessage;
var response = requestClient.BulkRequestTransmitter(acaSecurityHeader,
securityHeader,
ref businessHeader,
manifest,
bulkTrans);
//we got a response! now do something with it
return response;
}
}
}
Here is the complete solution, just needs all of your own data supplied (including the complete attachment file with all the payee and payer records, which is outside the scope of this but should be pretty easy to generate). Also note that this is submission of forms only, not status checks. When I get that working I'll try to remember to return and update this answer (but if someone else already has it and wants to share, that'd be pretty rad as well).
Edit for Status Service
I've combined a cleaned up version of the classes generated from the wsdl and my own junk code to get messages through and process the responses. Note that this isn't 100% tested yet, needs sanity checks, etc. but like the previous stuff should at least help anyone else struggling with this mess. Usage here is pretty straightforward:
var statusResponse = StatusService.CheckStatus(receipt, tCCode, certificatePath, certificatePassword, "https://la.www4.irs.gov/airp/aca/a2a/1095BC_Status_Request_AATS2016");
And here is the full class (with bonus generated classes namespace):
See my second answer for the status service code
Seconds answer to include status service, rather than just another link that could disappear.
Here is the main class:
using System;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography.Xml;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Serialization;
namespace IrsAcaClient
{
public class StatusService
{
private const string SecurityTimestampStringFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffZ";
public static ACABulkRequestStatusService.ACABulkRequestTransmitterStatusDetailResponseType CheckStatus(string receiptID, string tCCode, string certificatePath, string certificatePassword, string statusServiceUrl)
{
//go ahead and generate some of the ids and timestamps we'll need
var securityTimeStampWsuId = GenerateWsuId("TS");
var businessHeaderWsuId = GenerateWsuId("id");
var detailRequestWsuId = GenerateWsuId("id");
var signatureWsuId = GenerateWsuId("SIG");
var securityTimestampUTC = DateTime.UtcNow;
var securityTimestampCreated = securityTimestampUTC.ToString(SecurityTimestampStringFormat);
var securityTimestampExpires = securityTimestampUTC.AddMinutes(10).ToString(SecurityTimestampStringFormat);
//build the envelope
//load the base envelope xml
var doc = new XmlDocument();
doc.PreserveWhitespace = false;
doc.Load("BaseStatusRequestEnvelope.xml");
/* Need a bunch of namespaces defined
* xmlns:oas1="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
* xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
* xmlns:urn="urn:us:gov:treasury:irs:msg:irstransmitterstatusrequest"
* xmlns:urn1="urn:us:gov:treasury:irs:ext:aca:air:7.0"
* xmlns:urn2="urn:us:gov:treasury:irs:common"
* xmlns:urn3="urn:us:gov:treasury:irs:msg:acasecurityheader"
* xmlns:wsa="http://www.w3.org/2005/08/addressing"
* xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
* xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
* xmlns:ds="http://www.w3.org/2000/09/xmldsig#");
*/
XmlNamespaceManager nsMgr = new XmlNamespaceManager(doc.NameTable);
nsMgr.AddNamespace("soapenv", "http://schemas.xmlsoap.org/soap/envelope/");
nsMgr.AddNamespace("urn", "urn:us:gov:treasury:irs:msg:irstransmitterstatusrequest");
nsMgr.AddNamespace("urn1", "urn:us:gov:treasury:irs:ext:aca:air:7.0");
nsMgr.AddNamespace("urn2", "urn:us:gov:treasury:irs:common");
nsMgr.AddNamespace("urn3", "urn:us:gov:treasury:irs:msg:acasecurityheader");
nsMgr.AddNamespace("wsa", "http://www.w3.org/2005/08/addressing");
nsMgr.AddNamespace("wsse", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
nsMgr.AddNamespace("wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd");
nsMgr.AddNamespace("oas1", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
nsMgr.AddNamespace("ds", "http://www.w3.org/2000/09/xmldsig#");
//start replacing values in it
//for securityHeader, should have the following
/*
* securityHeader.Timestamp.Id
* securityHeader.Timestamp.Created.Value
* securityHeader.Timestamp.Expires.Value
*/
doc.SelectSingleNode("//wsse:Security/wsu:Timestamp", nsMgr).Attributes["wsu:Id"].Value = securityTimeStampWsuId;
doc.SelectSingleNode("//wsse:Security/wsu:Timestamp/wsu:Created", nsMgr).InnerText = securityTimestampCreated;
doc.SelectSingleNode("//wsse:Security/wsu:Timestamp/wsu:Expires", nsMgr).InnerText = securityTimestampExpires;
//for businessHeader, should have the following
/*
* businessHeader.UniqueTransmissionId
* businessHeader.Timestamp
* businessHeader.Id
*/
doc.SelectSingleNode("//urn:ACABusinessHeader", nsMgr).Attributes["wsu:Id"].Value = businessHeaderWsuId;
doc.SelectSingleNode("//urn:ACABusinessHeader/urn1:UniqueTransmissionId", nsMgr).InnerText = GetUniqueTransmissionId(Guid.NewGuid(), tCCode);
doc.SelectSingleNode("//urn:ACABusinessHeader/urn2:Timestamp", nsMgr).InnerText = securityTimestampUTC.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ssZ");
//for ACABulkRequestTransmitterStatusDetailRequest, should have the following
/*
* ACABulkRequestTransmitterStatusDetailRequest.Id
* ACABulkRequestTransmitterStatusDetailRequest.ACABulkReqTrnsmtStsReqGrpDtl.ReceiptId
*/
doc.SelectSingleNode("//urn:ACABulkRequestTransmitterStatusDetailRequest", nsMgr).Attributes["wsu:Id"].Value = detailRequestWsuId;
doc.SelectSingleNode("//urn:ACABulkRequestTransmitterStatusDetailRequest/urn1:ACABulkReqTrnsmtStsReqGrpDtl/urn2:ReceiptId", nsMgr).InnerText = receiptID;
//now do some more security setup
var cert = new X509Certificate2(certificatePath, certificatePassword, X509KeyStorageFlags.MachineKeySet);
var exported = cert.Export(X509ContentType.Cert, certificatePassword);
var base64 = Convert.ToBase64String(exported);
//now compute all the signing stuff
var xSigned = new SignedXmlWithId(doc);
// Add the key to the SignedXml document.
xSigned.SigningKey = cert.PrivateKey;
xSigned.Signature.Id = signatureWsuId;
xSigned.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NWithCommentsTransformUrl;
var keyInfo = new KeyInfo
{
Id = GenerateWsuId("KI")
};
//need to get the keyinfo into the signed xml stuff before we compute sigs, and because it is using some stuff that
//doesn't appear to be supported out of the box we'll work around it by adding a node directly
var sbKeyInfo = new StringBuilder();
sbKeyInfo.Append("<root xmlns:wsse=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd\" xmlns:wsu=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\">");
sbKeyInfo.Append("<wsse:SecurityTokenReference wsu:Id=\"" + GenerateWsuId("STR") + "\">");
sbKeyInfo.Append("<wsse:KeyIdentifier EncodingType=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary\" ValueType=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3\">" + base64.ToString());
sbKeyInfo.Append("</wsse:KeyIdentifier>");
sbKeyInfo.Append("</wsse:SecurityTokenReference>");
sbKeyInfo.Append("</root>");
XmlDocument tempDoc = new XmlDocument();
tempDoc.LoadXml(sbKeyInfo.ToString());
keyInfo.AddClause(new KeyInfoNode((XmlElement)tempDoc.FirstChild.FirstChild));
xSigned.KeyInfo = keyInfo;
GenerateReference(securityTimeStampWsuId, "wsse wsa oas1 soapenv urn urn1 urn2 urn3", xSigned);
GenerateReference(businessHeaderWsuId, "wsa oas1 soapenv urn1 urn2 urn3", xSigned);
GenerateReference(detailRequestWsuId, "oas1 soapenv urn1 urn2 urn3", xSigned);
// Compute the Signature.
xSigned.ComputeSignature();
//signing stuff must come before the timestamp or the IRS service complains
doc.SelectSingleNode("//wsse:Security", nsMgr).InsertBefore(xSigned.GetXml(), doc.SelectSingleNode("//wsse:Security", nsMgr).FirstChild);
//get the completed envelope
var envelope = doc.OuterXml;
//start the webrequest
//get the request object
var request = CreateWebRequest(statusServiceUrl);
//get the request stream and then get a writer on it
using (var stream = request.GetRequestStream())
using (var gz = new GZipStream(stream, CompressionMode.Compress))
using (var writer = new StreamWriter(gz))
{
//start by writing the soap envelope to the stream
writer.WriteLine(envelope);
writer.Close();
stream.Close();
}
//get the response
WebResponse response;
//let an exception get thrown up the stack
response = request.GetResponse();
//get the response stream, get a reader on it, and read the response as text
using (var responseStream = response.GetResponseStream())
using (var reader = new StreamReader(responseStream, Encoding.UTF8))
{
var responseText = reader.ReadToEnd();
//rip the one element (and children) we need out
var match = Regex.Match(responseText, #"<(?'prefix'[\w\d]*):ACABulkRequestTransmitterStatusDetailResponse.*<\/\k<prefix>:ACABulkRequestTransmitterStatusDetailResponse>");
return Deserialize<ACABulkRequestStatusService.ACABulkRequestTransmitterStatusDetailResponseType>(match.ToString());
}
}
private static string GetUniqueTransmissionId(Guid transmissionGuid, string tCCode)
{
return string.Format("{0}:SYS12:{1}::T", transmissionGuid, tCCode);
}
private static string GenerateWsuId(string prefix)
{
return string.Format("{0}-{1}", prefix, Guid.NewGuid().ToString().Replace("-", "").ToUpper());
}
private static void GenerateReference(string elementID, string inclusivePrefixList, SignedXmlWithId xSigned)
{
var reference = new Reference()
{
Uri = "#" + elementID
};
XmlDsigExcC14NTransform env = new XmlDsigExcC14NTransform();
env.InclusiveNamespacesPrefixList = inclusivePrefixList;
reference.AddTransform(env);
xSigned.AddReference(reference);
}
/// <summary>
/// creates a webrequest object and prefills some required headers and such
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
private static HttpWebRequest CreateWebRequest(string url)
{
//setup a web request with all the headers and such that the service requires
var webRequest = (HttpWebRequest)WebRequest.Create(url);
webRequest.Method = "POST";
webRequest.ProtocolVersion = HttpVersion.Version11;
webRequest.Headers.Add(HttpRequestHeader.ContentEncoding, "gzip");
webRequest.Headers.Add(HttpRequestHeader.AcceptEncoding, "gzip, deflate");
webRequest.ContentType = "text/xml;charset=UTF-8";
webRequest.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
webRequest.Headers.Add("SOAPAction", "RequestSubmissionStatusDetail");
webRequest.KeepAlive = true;
return webRequest;
}
/// <summary>
/// deserializes the xml string into an object
/// </summary>
/// <param name="xmlString"></param>
/// <returns></returns>
public static T Deserialize<T>(string xmlString) where T : class
{
//if the string is empty, just return null
if (xmlString.Length <= 0)
{
return null;
}
//create a serializer
var serializer = new System.Xml.Serialization.XmlSerializer(typeof(T));
T output;
//create the reader that the serializer will read from, passing it the string
using (var reader = new System.IO.StringReader(xmlString))
{
//rebuild the list object
output = (T)serializer.Deserialize(reader);
}
//return the list
return output;
}
}
}
Here is the relevant base xml:
<?xml version="1.0" encoding="utf-8" ?>
<soapenv:Envelope
xmlns:oas1="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:urn="urn:us:gov:treasury:irs:msg:irstransmitterstatusrequest"
xmlns:urn1="urn:us:gov:treasury:irs:ext:aca:air:7.0" xmlns:urn2="urn:us:gov:treasury:irs:common"
xmlns:urn3="urn:us:gov:treasury:irs:msg:acasecurityheader">
<soapenv:Header xmlns:wsa="http://www.w3.org/2005/08/addressing">
<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<wsu:Timestamp wsu:Id="XXXXXXXXXXXXXXXXXX">
<wsu:Created>XXXXXXXXXXXXXXXXXX</wsu:Created>
<wsu:Expires>XXXXXXXXXXXXXXXXXX</wsu:Expires>
</wsu:Timestamp>
</wsse:Security>
<urn:ACABusinessHeader wsu:Id="XXXXXXXXXXXXXXXXXX" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<urn1:UniqueTransmissionId>
XXXXXXXXXXXXXXXXXX
</urn1:UniqueTransmissionId>
<urn2:Timestamp>XXXXXXXXXXXXXXXXXX</urn2:Timestamp>
</urn:ACABusinessHeader>
<urn3:ACASecurityHeader />
<wsa:Action>RequestSubmissionStatusDetail</wsa:Action>
</soapenv:Header>
<soapenv:Body>
<urn:ACABulkRequestTransmitterStatusDetailRequest version="1.0" wsu:Id="XXXXXXXXXXXXXXXXXX" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<urn1:ACABulkReqTrnsmtStsReqGrpDtl>
<urn2:ReceiptId>XXXXXXXXXXXXXXXXXX</urn2:ReceiptId>
</urn1:ACABulkReqTrnsmtStsReqGrpDtl>
</urn:ACABulkRequestTransmitterStatusDetailRequest>
</soapenv:Body>
</soapenv:Envelope>
For this one, the main change I needed to make to the WSDL-generated classes was the following:
[System.SerializableAttribute()]
[XmlRoot("ACABulkRequestTransmitterStatusDetailResponse", Namespace = "urn:us:gov:treasury:irs:msg:irstransmitterstatusrequest")]
public class ACABulkRequestTransmitterStatusDetailResponseType
{
private ACABulkRequestTransmitterResponseType aCABulkRequestTransmitterResponseField;
private ACABulkReqTrnsmtStsRespGrpDtlType aCABulkReqTrnsmtStsRespGrpDtlField;
private string versionField;
public ACABulkRequestTransmitterStatusDetailResponseType()
{
this.versionField = "1.0";
}
[System.Xml.Serialization.XmlElementAttribute(Namespace = "urn:us:gov:treasury:irs:ext:aca:air:7.0", Order = 0)]
public ACABulkRequestTransmitterResponseType ACABulkRequestTransmitterResponse
{
get
{
return this.aCABulkRequestTransmitterResponseField;
}
set
{
this.aCABulkRequestTransmitterResponseField = value;
}
}
[System.Xml.Serialization.XmlElementAttribute(Namespace = "urn:us:gov:treasury:irs:ext:aca:air:7.0", Order = 1)]
public ACABulkReqTrnsmtStsRespGrpDtlType ACABulkReqTrnsmtStsRespGrpDtl
{
get
{
return this.aCABulkReqTrnsmtStsRespGrpDtlField;
}
set
{
this.aCABulkReqTrnsmtStsRespGrpDtlField = value;
}
}
[System.Xml.Serialization.XmlAttributeAttribute()]
public string version
{
get
{
return this.versionField;
}
set
{
this.versionField = value;
}
}
}
Adding the ds prefix is actually what is breaking this. The SignatureValue element contains the x509 hash of the serialized SignedInfo element (this is where having consistent whitespace matters).
By modifying the SignedInfo element, the authentication will fail when the IRS compares the SignatureValue hash you send against their computation of the SignedInfo hash using the certificate you uploaded and associated with the provided TCC.
Just remove your modification of the SignedInfo element and all should be good. It works for me.
This was an edit made to the above post, which added more information to jstill's post. Unfortunately, peer reviewers rejected it.
In addition to the changes jstill made to the Status' Reference.cs file, I also had to include the BulkExchangeFileType and IncludeFileType updates he made to the Submission's Reference.cs file in order to get the Deserializer method to function partially.
The Deserializer method will return the TransmissionStatusCd and the ReceiptId elements as expected, however, it will not populate the ErrorDataFile element properly.
Since, at this time, I am unable to get the ErrorDataFile object populated properly, I am not utilizing the ACABulkRequestTransmitterStatusDetailResponseType object to capture the response returned from the Status Web Service. Instead, I have chosen to read the ResponseStream into a string object and parse the (up to) two MIME parts of the response, and process those as necessary.
Additional changes to the Status' Reference.cs
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.0.30319.34283")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(Namespace = "urn:us:gov:treasury:irs:common")]
public partial class BulkExchangeFileType : object, System.ComponentModel.INotifyPropertyChanged
{
private IncludeType includeField;
/// <remarks/>
[System.Xml.Serialization.XmlElement(Order = 0, Namespace = "http://www.w3.org/2004/08/xop/include")]
public IncludeType Include
{
get { return this.includeField; }
set
{
this.includeField = value;
this.RaisePropertyChanged("Include");
}
}
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged;
if (propertyChanged != null)
{
propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
}
}
}
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.0.30319.34283")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(Namespace = "http://www.w3.org/2004/08/xop/include")]
public partial class IncludeType : object, System.ComponentModel.INotifyPropertyChanged
{
private System.Xml.XmlNode[] anyField;
private string hrefField;
/// <remarks/>
[System.Xml.Serialization.XmlTextAttribute()]
[System.Xml.Serialization.XmlAnyElementAttribute(Order = 0)]
public System.Xml.XmlNode[] Any
{
get { return this.anyField; }
set
{
this.anyField = value;
this.RaisePropertyChanged("Any");
}
}
/// <remarks/>
[System.Xml.Serialization.XmlAttributeAttribute(DataType = "string")]
public string href
{
get { return this.hrefField; }
set
{
this.hrefField = value;
this.RaisePropertyChanged("href");
}
}
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged;
if (propertyChanged != null)
{
propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
}
}
}
For those running into the following error:
AIRMF3002 Rejected transmission - Unable to process your request because validation failure occurs in the Attachment Byte Size Number
It appears there is an issue on the IRS side with the size they are expecting (as per the Documentation) and the size they actually accept. Originally, I had the following code:
// Size in Bytes of File: This code returns the "Size" located on the File's Property Page.
// Result: TRANSMISSION REJECTED ON INCORRECT FILE SIZE!
manifestHeader.AttachmentByteSizeNum = new FileInfo(FormDataFilePath).Length.ToString();
I replaced the above code with the following, and the error I was receiving was resolved.
// Read the contents of the file, and retrieve the length of the content of the file itself..
// Result: TRANSMISSION WAS ACCEPTED USING THIS FILE SIZE.
manifestHeader.AttachmentByteSizeNum = File.ReadAllText(FormDataFilePath).Length.ToString();
It appears as though the Web Service is actually expecting the size of the file content and not the size of the actual file. The difference in size pertaining to the test scenarios was approximately 3 bytes. I assume that is because retreiving the size of the file adds some additional file-related information not belonging to the actual content.
I have notified the IRS about this issue regarding their documentation.

How to use XmlWriterSettings() when using override void WriteEndElement()?

I am working with a legacy application that does not import abbreviated empty xml elements. For example:
BAD empty:
<foo />
GOOD empty:
<foo></foo>
I know the solution to achieve this, which I will present now:
public class XmlTextWriterFull : XmlTextWriter
{
public XmlTextWriterFull(Stream stream, Encoding enc) : base(stream, enc)
{
}
public XmlTextWriterFull(String str, Encoding enc) : base(str, enc)
{
}
public override void WriteEndElement()
{
base.WriteFullEndElement();
}
}
and the client code:
var x_settings = new XmlWriterSettings();
x_settings.NewLineChars = Environment.NewLine;
x_settings.NewLineOnAttributes = true;
x_settings.NewLineHandling = NewLineHandling.Replace;
x_settings.CloseOutput = true;
x_settings.Indent = true;
x_settings.NewLineOnAttributes = true;
//var memOut = new MemoryStream();
var writer = new XmlTextWriterFull(outputFilename, Encoding.UTF8); //Or the encoding of your choice
var x_serial = new XmlSerializer(typeof(YOUR_OBJECT_TYPE));
x_serial.Serialize(writer, YOUR_OBJECT_INSTANCE);
writer.Close();
However, if you observed carefully the XmlWriterSettings are never used in the client code. Therefore the xml output is terribly formatted. My questions is this: how do I adapt the above code to accept XmlWriterSettings?
The use of factory creation methods and sealed/internal/abstract classes makes this difficult to implement an override.
I will accept an alternative solution, I am not married to my above solution.
WORKAROUND SOLUTION
Step 1: create the following class in your solution:
public class XmlTextWriterFull : XmlTextWriter
{
public XmlTextWriterFull(TextWriter sink) : base(sink)
{
Formatting = Formatting.Indented;
}
public override void WriteEndElement()
{
base.WriteFullEndElement();
}
}
Step 2: Add the following client code. Make sure to replace YOUR_OBJECT_TYPE and YOUR_OBJECT_INSTANCE with the class and instance your are working with:
TextWriter streamWriter = new StreamWriter(outputFilename);
var writer = new XmlTextWriterFull(streamWriter);
var x_serial = new XmlSerializer(typeof (YOUR_OBJECT_TYPE));
x_serial.Serialize(writer, YOUR_OBJECT_INSTANCE);
writer.Close();
The workaround above will produce the following empty xml element formatting:
<foo>
</foo>
The issue with this workaround is that it adds a line feed (notice the elements are on separate lines). This may be acceptable for you but causes issues with my legacy application.
How about this.
Grab the awesome XmlWrappingWriter class from http://www.tkachenko.com/blog/archives/000585.html (I have omitted the code for the sake of brevity).
With that, we can create a sub-class as follows (very similar to your original one):
public class XmlTextWriterFull2 : XmlWrappingWriter
{
public XmlTextWriterFull2(XmlWriter baseWriter)
: base(baseWriter)
{
}
public override void WriteEndElement()
{
base.WriteFullEndElement();
}
}
It can then be invoked like this (again very similar):
var x_settings = new XmlWriterSettings();
x_settings.NewLineChars = Environment.NewLine;
x_settings.NewLineOnAttributes = true;
x_settings.NewLineHandling = NewLineHandling.None;
x_settings.CloseOutput = true;
x_settings.Indent = true;
x_settings.NewLineOnAttributes = true;
using (XmlWriter writer = XmlWriter.Create(outputFilename, x_settings))
{
using (XmlTextWriterFull2 xmlTextWriterFull = new XmlTextWriterFull2(writer))
{
var x_serial = new XmlSerializer(typeof(YOUR_OBJECT_TYPE));
x_serial.Serialize(xmlTextWriterFull, YOUR_OBJECT_INSTANCE);
}
}
In my case, an element that had previously been rendered as
<Foo>
</Foo>
became
<Foo></Foo>
As you alluded to in your question, this is actually quite a tricky problem due to everything being sealed/internal etc., making overrides rather difficult. I think my biggest problem was trying to get an XmlWriter to accept XmlWriterSettings: beyond this approach, I could find no way of getting the original XmlTextWriterFull to respect the given XmlWriterSettings.
MSDN states that this method:
XmlWriter.Create(XmlWriter, XmlWriterSettings)
Can be used to apply the XmlWriterSettings to the XmlWriter. I couldn't get this to work like I wanted (the indentation never worked, for example), and upon decompiling the code, it does not appear that all the settings are used with this particular method, hence why my invocation code just passes in the outputFile (a stream of some sort would work just as well).
The workaround solution you gave in your question adds extra line breaks (when indenting is enabled) because we're telling the writer to treat this element as if it had children.
Here is how I modified your workaround to manipulate the indenting dynamically so as to avoid those extra line breaks.
public class XmlTextWriterFull : XmlTextWriter
{
public XmlTextWriterFull(TextWriter sink)
: base(sink)
{
Formatting = Formatting.Indented;
}
private bool inElement = false;
public override void WriteStartElement(string prefix, string localName, string ns)
{
base.WriteStartElement(prefix, localName, ns);
// Remember that we're in the process of defining an element.
// As soon as a child element is closed, this flag won't be true anymore and we'll know to avoid messing with the indenting.
this.inElement = true;
}
public override void WriteEndElement()
{
if (!this.inElement)
{
// The element being closed has child elements, so we should just let the writer use it's default behavior.
base.WriteEndElement();
}
else
{
// It looks like the element doesn't have children, and we want to avoid emitting a self-closing tag.
// First, let's temporarily disable any indenting, then force the full closing element tag.
var prevFormat = this.Formatting;
this.Formatting = Formatting.None;
base.WriteFullEndElement();
this.Formatting = prevFormat;
this.inElement = false;
}
}
}
Following code snippet force printing of closing tag on the same line (sorry for vb version, it should be easy to rewrite the same using C#):
Imports System.Xml
Imports System.IO
Public Class CustomXmlTextWriter
Inherits XmlTextWriter
Public Sub New(ByRef baseWriter As TextWriter)
MyBase.New(baseWriter)
Formatting = Xml.Formatting.Indented
End Sub
Public Overrides Sub WriteEndElement()
If Not (Me.WriteState = Xml.WriteState.Element) Then
MyBase.WriteEndElement()
Else
Formatting = Xml.Formatting.None
MyBase.WriteFullEndElement()
Formatting = Xml.Formatting.Indented
End If
End Sub
End Class
Another option.
public class XmlCustomTextWriter : XmlTextWriter
{
private TextWriter _tw = null;
public XmlCustomTextWriter(TextWriter sink)
: base(sink)
{
_tw = sink;
Formatting = Formatting.Indented;
Indentation = 0;
}
public void OutputElement(string name, string value)
{
WriteStartElement(name);
string nl = _tw.NewLine;
_tw.NewLine = "";
WriteString(value);
WriteFullEndElement();
_tw.NewLine = nl;
}
}
Leaving this here in case someone needs it; since none of the answers above solved it for me, or seemed like overkill.
FileStream fs = new FileStream("file.xml", FileMode.Create);
XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
XmlWriter w = XmlWriter.Create(fs, settings);
w.WriteStartDocument();
w.WriteStartElement("tag1");
w.WriteStartElement("tag2");
w.WriteAttributeString("attr1", "val1");
w.WriteAttributeString("attr2", "val2");
w.WriteFullEndElement();
w.WriteEndElement();
w.WriteEndDocument();
w.Flush();
fs.Close();
The trick was to set the XmlWriterSettings.Indent = true and add it to the XmlWriter.
Edit:
Alternatively you can also use
w.Formatting = Formatting.Indented;
instead of adding an XmlWriterSettings.

Validate XML against XSD in a single method

I need to implement a C# method that needs to validate an XML against an external XSD and return a Boolean result indicating whether it was well formed or not.
public static bool IsValidXml(string xmlFilePath, string xsdFilePath);
I know how to validate using a callback. I would like to know if it can be done in a single method, without using a callback. I need this purely for cosmetic purposes: I need to validate up to a few dozen types of XML documents so I would like to make is something as simple as below.
if(!XmlManager.IsValidXml(
#"ProjectTypes\ProjectType17.xml",
#"Schemas\Project.xsd"))
{
throw new XmlFormatException(
string.Format(
"Xml '{0}' is invalid.",
xmlFilePath));
}
There are a couple of options I can think of depending on whether or not you want to use exceptions for non-exceptional events.
If you pass a null as the validation callback delegate, most of the built-in validation methods will throw an exception if the XML is badly formed, so you can simply catch the exception and return true/false depending on the situation.
public static bool IsValidXml(string xmlFilePath, string xsdFilePath, XNamespace namespaceName)
{
var xdoc = XDocument.Load(xmlFilePath);
var schemas = new XmlSchemaSet();
schemas.Add(namespaceName, xsdFilePath);
try
{
xdoc.Validate(schemas, null);
}
catch (XmlSchemaValidationException)
{
return false;
}
return true;
}
The other option that comes to mind pushes the limits of your without using a callback criterion. Instead of passing a pre-defined callback method, you could instead pass an anonymous method and use it to set a true/false return value.
public static bool IsValidXml(string xmlFilePath, string xsdFilePath, XNamespace namespaceName)
{
var xdoc = XDocument.Load(xmlFilePath);
var schemas = new XmlSchemaSet();
schemas.Add(namespaceName, xsdFilePath);
Boolean result = true;
xdoc.Validate(schemas, (sender, e) =>
{
result = false;
});
return result;
}

How does one implement a .Net WebService that does not encapsulate response in XML?

I am writing an series of web interfaces to some data. I have WebMethods to return the data in DataSet and XmlDataDocument format (The XmlDataDocument removes all the schema overhead.)
[WebMethod]
public XmlDataDocument Search_XML( string query ) {
return new XmlDataDocument( Search_DataSet( query ) );
}
[WebMethod]
public DataSet Search_DataSet( string query ) {
DataSet result = new DataSet( "SearchResults" );
//... Populate DataSet here
return result;
}
I have also created a function that accepts an XSL formatting string and returns the formated results, allowing the client to format an HTML response they can inject right into their webpage:
public string Search_XSL( string query, string xsl ) {
string result = "";
XmlDataDocument resultxml = Search_XML( query );
XslCompiledTransform transform = new XslCompiledTransform();
using ( StringReader xslstringreader = new StringReader( xsl ) ) {
using ( XmlReader xslxmlreader = XmlReader.Create( xslstringreader ) ) {
using ( MemoryStream transformedmemorystream = new MemoryStream() ) {
using ( StreamWriter transformedstreamwriter = new StreamWriter( transformedmemorystream ) ) {
try {
transform.Load( xslxmlreader );
transform.Transform( resultxml, null, transformedstreamwriter );
transformedstreamwriter.Flush();
transformedmemorystream.Position = 0;
using ( StreamReader transformedreader = new StreamReader( transformedmemorystream ) ) {
result = transformedreader.ReadToEnd();
}
}
catch ( Exception ex ) {
result = ex.InnerException.ToString();
}
}
}
}
}
return result;
}
My question is, how do I implement a WebMethod-like interface for this Search_XSL() function so that I can return the resulting string exactly as the function does, without the XML encoding the WebMethod puts around it? Would that be a new Web Form? How do I implement a Web Form with no actual HTML, just accepting form parameters? Not sure where to start here.
Edit: It looks like a "Generic Handler" .ashx file is the way to go. Is this the right approach?
If you need an HTTP endpoint that processes an HttpContext and returns a custom response, then using IHttpHandler via a Generic Web handler (*.ashx) would be the correct approach to take.
You would read the values from the request query string and then process the request. Your generic handler would use the HttpContext.Response to set the content type of the output stream to text/html and would write the resulting HTML you wish to inject.
WCF is the way to go in .net. You can configure your methods to return json or any number of of other type of serializations. While a generic handler could work there is much better support for wcf. Check out this question for more info.
WebMethods use SOAP, which means you can't remove the SOAP envelope at the service layer. Your original idea is right, here's what you need to finish:
protected void Page_Load(object sender, EventArgs e)
{
Response.ContentType = "text/html";
Response.Write("Hello world");
}
protected override void Render(HtmlTextWriter writer)
{
}
It's a quick and dirty solution, but it works.

How to deserialize WCF message using OperationContract

I succeeded in building a WCF client generated by svcutil.exe from the WSDL. Using the generated client proxy class I can call the web service of an external service supplier. I also succeeded in coding a message inspector, as I need to log both raw XML request and response as full SOAP message to the database.
For an emergency scenario I also need to be able to "import" a raw XML response. I found many hints on using XMLSerializer or deserializing WCF messages based on the message contract.
But how can I deserialize a raw XML response based on an operation contract? For a first test I use one of the logged raw responses, save it to a file and now try to deserialize it to the response type as generated in the client proxy. Somehow I must succeed in calling DeserializeReply() from class ClientOperation. But how to get there?
I happily accept any help as I'm quite new to WCF...
TIA,
Stefan
This is what I tried after Marc's answer:
public static RatingResult DeserializeResponseFromFile(string path)
{
var xmlReader = XmlReader.Create(path);
var message = Message.CreateMessage(xmlReader, int.MaxValue, MessageVersion.Soap11);
var readerAtBodyContents = message.GetReaderAtBodyContents();
var dcs = new DataContractSerializer(typeof(RatingResult), "RatingResponse", "http://rating.webservice.xxx.de");
// Error in line 6 position 7. 'EndElement' 'RatingResponse' from namespace
// 'http://rating.webservice.xxx.de' is not expected.
// Expecting element 'commonDataField'.
var wsResult = (RatingResult)dcs.ReadObject(readerAtBodyContents);
return wsResult;
}
This is part of the logged XML response file, that I'm trying to deserialize to type RatingResponse:
<soapenv:Envelope xmlns:soapenv="..." xmlns:soapenc="..." xmlns:xsd="..." xmlns:xsi="...">
<soapenv:Header soapenv:encodingStyle="..." />
<soapenv:Body soapenv:encodingStyle="...">
<p933:RatingResponse xmlns:p933="http://rating.webservice.xxx.de">
<RatingReturn href="#id0" />
</p933:RatingResponse>
<multiRef id="id0" soapenc:root="0" soapenv:encodingStyle="..." xsi:type="p878:RatingResult" xmlns:p878="http://output.rating.webservice.xxx.de">
<commonData href="#id1" />
<acctData href="#id2" />
<resultData href="#id3" />
</multiRef>
<multiRef id="id1" soapenc:root="0" soapenv:encodingStyle="..." xsi:type="p719:RatingCommonData" xmlns:p719="http://input.rating.webservice.xxx.de">
<requestdate xsi:type="xsd:dateTime">2010-12-24T09:45:09.531Z</requestdate>
...
I guess that the data contract serializer has problems deserializing the href's. Please note that the message I try to deserialize "by hand" was captured using my injected message inspector. In a "normal" call of the web service this message get deserialized without problems.
I don't really understand what you're trying to ask and to do.... based on an operation contract ?? The operation contract is just an attribute you put on an operation / method call to mark it as a service method .... the operation contract doesn't do anything even remotely involved with serialization or deserialization..... do you mean how to deserialize an XML message using the DataContractSerializer which is the WCF default serializer??
Assuming you do really mean HOWTO: deserialize a WCF message using the DataContractSerializer, then try this: if you have the response XML from a service call that used the default WCF DataContractSerializer, you should be able to deserialize it like this (assuming you have your XML serialized response in a xmlResponse variable):
using(MemoryStream memStm = new MemoryStream())
using(StreamWriter stw = new StreamWriter(memStm))
{
// write your response to the memory stream
stw.Write(xmlResponse);
stw.Flush();
// "reset" memory stream
memStm.Seek(0, SeekOrigin.Begin);
// setup DataContractSerializer
DataContractSerializer dcs = new DataContractSerializer(typeof(YourDataType));
// deserialize result XML into an instance of "YourDataType"
var result = dcs.ReadObject(memStm);
}
For anyone in the future doing this. I had to manually read a WCF message out of the MSMSQ, and get the request object out of the MSMQ/WCF message envelope. Here's how:
Root code:
var q = new MessageQueue(#".\Private$\VishalQ;poison");
var allMessages = q.GetAllMessages().ToList();
var wcfRequests = allMessages.Select(ConvertToWcfRequest<ObjectChangedRequest>);
My contract:
[ServiceContract]
public interface IWish
{
[OperationContract(IsOneWay = true)]
void ObjectChanged(ObjectChangedRequest request);
}
My Data Contract:
[DataContract(Namespace = "http://x.namespaces.x-x.com/")]
public class ObjectChangedRequest
{
[DataMember]
public OperationType OperationType { get; set; }
}
My message deserialization code:
/// <summary>
/// Converts a WCF MSMQ message to a WCF request object.
/// </summary>
public static T ConvertToWcfRequest<T>(Message msmqMessage)
{
var buffer = new byte[msmqMessage.BodyStream.Length];
msmqMessage.BodyStream.Read(buffer, 0, (int)msmqMessage.BodyStream.Length);
var envelopeStart = FindEnvelopeStart(buffer);
using var msmqStream = new MemoryStream(buffer, envelopeStart, buffer.Length - envelopeStart);
var encodingElement = new BinaryMessageEncodingBindingElement();
var wcfMessage = encodingElement.CreateMessageEncoderFactory().Encoder.ReadMessage(msmqStream, int.MaxValue);
var document = new XmlDocument();
document.Load(wcfMessage.GetReaderAtBodyContents());
var realRoot = document.FirstChild.FirstChild;
using var wcfStream = new MemoryStream();
using var xmlWriter = XmlWriter.Create(wcfStream);
realRoot.WriteTo(xmlWriter);
xmlWriter.Flush();
wcfStream.Seek(0, SeekOrigin.Begin);
var wcfSerializer = new DataContractSerializer(typeof(T), realRoot.Name, "http://tempuri.org/"); //No idea why this has to be temp uri and not our namespace...
return (T)wcfSerializer.ReadObject(wcfStream);
}
/// <summary>
/// Locates the start of a WCF message within a MSMQ message.
/// </summary>
private static int FindEnvelopeStart(byte[] stream)
{
var position = 0;
var previousByte = stream[position];
for (position = 0; position < stream.Length; position++)
{
var currentByte = stream[position];
//Some magic numbers that define the start of the WCF message envelope
if (currentByte == 0x02 && previousByte == 0x56)
break;
previousByte = currentByte;
}
return position - 1;
}

Categories

Resources