Scott Hanselman

ASMX SoapExtension to Strip out Whitespace and New Lines

September 17, '07 Comments [4] Posted in ASP.NET | Web Services | XML
Sponsored By

Someone asked...

[I've got] a WebService with a WebMethod of the form.  Very Simple.

[WebMethod]
public XmlNode HelloWorld () {
                XmlDocument document = new XmlDocument();
                document.LoadXml(“<a><b><c><d>Hello World</d></c></b></a>”);
                return document;
}

What comes back is something in the response is something like

<a>
                <b>
                                <c>
                                                <d>Hello World</d>
                                </c>
                </b>
</a>

Where each level of indentation is actually only 2 characters.   I would like it to come back just like it is entered in the LoadXml call [no unneeded whitespace and no unneeded new lines.]

This is an old problem. Basically if you look at SoapServerProtocol.GetWriterForMessage, they...

return new XmlTextWriter(new StreamWriter(message.Stream, new UTF8Encoding(false), bufferSize));

...just make one. You don't get to change the settings. Of course, in WCF this is easy, but this person was using ASMX.

Enter the SoapExtension. In the web.config I'll register one.

<system.web>
    <webServices>
      <soapExtensionTypes>
        <add type="ASMXStripWhitespace.ASMXStripWhitespaceExtension, ASMXStripWhitespace"
             priority="1"
             group="High" />
      </soapExtensionTypes>
    </webServices>
...

And my class will derive from SoapExtension. There's lots of good details in this MSDN article by George Shepard a while back.

Here's my quicky implementation. Basically we're just reading in the stream of XML that was just output by the ASMX (specifically the XmlSerializer that used that XmlTextWriter we saw above) infrastructure.

using System;
using System.IO;
using System.Web.Services.Protocols;
using System.Text;
using System.Xml;

namespace ASMXStripWhitespace
{
    public class ASMXStripWhitespaceExtension : SoapExtension
    {
        // Fields
        private Stream newStream;
        private Stream oldStream;

        public MemoryStream YankIt(Stream streamToPrefix)
        {
            streamToPrefix.Position = 0L;
            XmlTextReader reader = new XmlTextReader(streamToPrefix);

            XmlWriterSettings settings = new XmlWriterSettings();
            settings.Indent = false;
            settings.NewLineChars = "";
            settings.NewLineHandling = NewLineHandling.None;
            settings.Encoding = Encoding.UTF8;
            MemoryStream outStream = new MemoryStream();
            using(XmlWriter writer = XmlWriter.Create(outStream, settings))
            {
                do
                {
                    writer.WriteNode(reader, true);
                }
                while (reader.Read());
                writer.Flush();
            }
           
            ////debug
            //outStream.Seek(0, SeekOrigin.Begin);
            ////outStream.Position = 0L;
            //StreamReader reader2 = new StreamReader(outStream);
            //string s = reader2.ReadToEnd();
            //System.Diagnostics.Debug.WriteLine(s);

            //outStream.Position = 0L;
            outStream.Seek(0, SeekOrigin.Begin);
            return outStream;
        }

        // Methods
        private void StripWhitespace()
        {
            this.newStream.Position = 0L;
            this.newStream = this.YankIt(this.newStream);
            this.Copy(this.newStream, this.oldStream);
        }

        private void Copy(Stream from, Stream to)
        {
            TextReader reader = new StreamReader(from);
            TextWriter writer = new StreamWriter(to);
            writer.WriteLine(reader.ReadToEnd());
            writer.Flush();
        }

        public override void ProcessMessage(SoapMessage message)
        {
            switch (message.Stage)
            {
                case SoapMessageStage.BeforeSerialize:
                case SoapMessageStage.AfterDeserialize:
                    return;

                case SoapMessageStage.AfterSerialize:
                    this.StripWhitespace();
                    return;
                case SoapMessageStage.BeforeDeserialize:
                    this.GetReady();
                    return;
            }
            throw new Exception("invalid stage");
        }

        public override Stream ChainStream(Stream stream)
        {
            this.oldStream = stream;
            this.newStream = new MemoryStream();
            return this.newStream;
        }

        private void GetReady()
        {
            this.Copy(this.oldStream, this.newStream);
            this.newStream.Position = 0L;
        }

        public override object GetInitializer(Type t)
        {
            return typeof(ASMXStripWhitespaceExtension);
        }

        public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)
        {
            return attribute;
        }

        public override void Initialize(object initializer)
        {
            //You'd usually get the attribute here and pull whatever you need off it.
            ASMXStripWhitespaceAttribute attr = initializer as ASMXStripWhitespaceAttribute;
        }
    }

    [AttributeUsage(AttributeTargets.Method)]
    public class ASMXStripWhitespaceAttribute : SoapExtensionAttribute
    {
        // Fields
        private int priority;

        // Properties
        public override Type ExtensionType
        {
            get { return typeof(ASMXStripWhitespaceExtension); }
        }

        public override int Priority
        {
            get { return this.priority; }
            set { this.priority = value;}
        }
    }
}

The order that things happen is important. The overridden call to ChainStream is where we get a new copy of the stream. The ProcessMessage switch is our opportunity to "get ready" and where we "strip whitespace."

If you want a method to use this, you have to add the attribute, in this case "ASMXStripWhitespace" to that method. Notice the attribute class just above. You can pass things into it if you like or override standard properties also.

public class Service1 : System.Web.Services.WebService
{
   [WebMethod]
   [ASMXStripWhitespace]
   public XmlNode HelloWorld () {
         XmlDocument document = new XmlDocument();
         document.LoadXml("<a><b><c><d>Hello World</d></c></b></a>");
         return document;
   }
}

The real work happens in YankIt where we just setup our own XmlSerializer and spin through the reader and writing out to the memory stream using the new settings of no new line chars and no indentation. Notice that the reader.Read() is a Do/While and not just a While. We don't want to lose the root node.

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by SherWeb

Changing where XmlSerializer Outputs Temporary Assemblies

September 16, '07 Comments [9] Posted in ASP.NET | Learning .NET | XML | XmlSerializer
Sponsored By

With this tip, I couldn't find any documentation, so you're on your own. That means no help/support from me or anywhere. YMMV - Your Mileage May Vary. No warranties. Enjoy.

Someone asked:

"When using the XmlSerializer from ASP.NET there are permissions issues can can be solved by granting the user account read/write permissions on the %SYSTEMROOT%\Temp folder but I would much rather have the temporary files created in a different folder"

So I poked around. If you want the XmlSerializer to output it's assemblies elsewhere, here's how. Note that I used the previous tip on How To Debug into a .NET XmlSeraizlizer Generated Assembly just to prove this was working. It's not needed for this tip!

To start, let's see where the temporary files end up usually.

First, I'll add this to my web.config (or whatever.exe.config if you like). This is just to visualize and confirm. It's not needed for this tip.

<configuration>
   <system.diagnostics>
      <switches>
         <add name="XmlSerialization.Compilation" value="1" />
      </switches>
   </system.diagnostics>
</configuration>

I'll debug my application, but I'll use SysInternal's Process Monitor and set the filter to only show processes whose name contains the string "WebDev" and see what files the VS WebDev Server writes to. If you're using IIS, you would search for W3WP or ASPNET_WP.

Process Monitor - Sysinternals www.sysinternals.com

Here I can see it writing to C:\Users\Scott\AppData\Local\Temp. The file names are auto-generated...note their names, in my case 6txrbdy.0.*. To prove these are the real generated XmlSerializers, I'll put a breakpoint on my app just before I call CreateSerializer and then drag the .cs file over from the TEMP folder into VS.NET. Note that the .PDB will get loaded when I hit F11 to step into.

XmlSerializerAlternateLocation (Debugging) - Microsoft Visual Studio (Administrator) (2)

Make note of the Call Stack. See the name of the assembly and the name of the .cs file? So, we're sure now where this XmlSerializer came from; it came from C:\Users\Scott\AppData\Local\Temp. If we were running IIS, it'd have been in %SYSTEMROOT%\Temp. The point being, it's automatic and it's temporary and your process needs WRITE access to that folder.

Now, poking around in Reflector with the not-used-often-enough Analyze method shows that XmlSerializerFactory.CreateSerializer eventually ends up in XmlSerializerCompilerParameters.Create which looks for a Configuration Section called “xmlSerializer” and a key called TempFilesLocation.

Lutz Roeders .NET Reflector (3)

That actually means it's looking for a section called System.Xml.Serializer and an element called xmlSerializer and an attribute called tempFilesLocation.

I'll add this to my web.config now:

<system.xml.serialization> 
  <xmlSerializer tempFilesLocation="c:\\foo"/> 
</system.xml.serialization> 

...and create a c:\\foo directory. Make sure your hosting process has write access to the directory.

I'll run my app again, and check out that new folder.

Administrator CWindowssystem32cmd.exe (2)

I've got newly generated XmlSerializer temporary assemblies in there. Undocumented, yes, but it does the job. Note, stuff like this can totally go away at any minute, so don't base your whole design on things like this. It's your tush, not mine, on the line.

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by SherWeb

Clever: XML to Schema Inference Wizard for Visual Studio 2008

September 15, '07 Comments [4] Posted in LINQ | Microsoft
Sponsored By

VB9 support for XML is pretty sexy, as I've mentioned before. Specifically, the XML Literals may have me using VB more often, or, more likely, mixing VB9 and C# together. More on that soon.

Here's some C# LINQ to XML:

XDocument booksXML = XDocument.Load(Server.MapPath("books.xml"));        
var books = from book in 
                    booksXML.Descendants("{http://example.books.com}book")
                    select book.Element("{http://example.books.com}title").Value;
Response.Write(String.Format("Found {0} books!", books.Count()));

Here's the same code in VB:

Dim booksXML = XDocument.Load(Server.MapPath("books.xml"))
Dim books = From book In booksXML...<b:book> Select book.<b:title>.Value
Response.Write(String.Format("Found {0} books!", books.Count()))

What's significant is the intellisense for your XML, created via schema, within your VB code.

To make it clearer:

VB9LINQXML

I've just pressed "." after typing "book.<" and I've got intellisense for my XSD-described XML document. Slick.

However, this assumes that you actually HAVE a schema, either having written one, or inferred one. I would hazard to guess that many (most?) projects haven't created an XSD. Heck, I see a lot of XML with no namespace at all! Without an XSD, Visual Basic can't give you this experience.

The teams have just snuck a new Project Item Template up onto Microsoft Download Center for use in Orcas. It's the XML to Schema Inference Wizard. Here's the deal.

You've got some XML somewhere without a schema and you're working on it in a project. You want intellisense for XML in VB9, so you right click on your project and select Add | New Item and see this dialog. (NOTE: you might need to SCROLL DOWN to see it!)

Add New Item - CUsersScottDesktopASP.NET OrcasNew CodeChapter 9VBListing9-10q_app

See the new item, Xml To Schema? Name your new schema and select Add and you'll see this dialog:

Infer XML Schema from XML documents

What's cool here is that you can Add from File, Add from Web or even Paste in an example XML document. You can add as many as you like, then click OK.

The new schemas will be inferred using the XML inference APIs (you could have done this one by one with xsd.exe from the command line, but that can be tedious, and also command-lines tend to squash Joe Public's fun if they are required.) and added to your project.

Now, in your VB code, type "Imports <xmlns:yourprefix=" and you'll get Intellisense with all the namespaces that you want to make available for your LINQ to XML expressions.

ImportsXMLNamespace

And that's it. At this point, you've associated the fully qualified namespace with whatever prefix you chose, and you'll have Intellisense for all your XML within VB9. Note that the inference isn't perfect, as we're "gleaning" a lot, so the more representative of reality your instance document, the better. You're welcome to hand-edit them also if they get to 80% and you want to take the XSD all the way.

Be sure to check out Beth Massi's Blog's LINQ Category for more detail on VB9 and LINQ.

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by SherWeb

Hanselminutes Podcast 81 - Vista x64 Redo for Developers

September 15, '07 Comments [7] Posted in Podcast
Sponsored By

My eighty-first podcast is up. I didn't totally love the last show. It was good, but not great. I wanted to cover a few other things, but you know, you get to talking and then 20 minutes has gone by. Some listeners agreed, so, I was sitting in the hotel room on the Sunday night before I started my New Employee Orientation (NEO) at Microsoft, sick to my stomach and scared out of my gourd, and figured I'd record a solo show, with a prepped outline beforehand, and fill in the gaps in Show 80. Here it is, I hope it doesn't suck.

Subscribe: Subscribe to Hanselminutes Subscribe to my Podcast in iTunes

If you have trouble downloading, or your download is slow, do try the torrent with µtorrent or another BitTorrent Downloader.

Links from the Show

Do also remember the complete archives are always up and they have PDF Transcripts, a little known feature that show up a few weeks after each show.

Telerik is our sponsor for this show.

Check out their UI Suite of controls for ASP.NET. It's very hardcore stuff. One of the things I appreciate about Telerik is their commitment to completeness. For example, they have a page about their Right-to-Left support while some vendors have zero support, or don't bother testing. They also are committed to XHTML compliance and publish their roadmap. It's nice when your controls vendor is very transparent.

As I've said before this show comes to you with the audio expertise and stewardship of Carl Franklin. The name comes from Travis Illig, but the goal of the show is simple. Avoid wasting the listener's time. (and make the commute less boring)

Enjoy. Who knows what'll happen in the next show?

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by SherWeb

Forcing Google Apps for your Domain (GAFYD) into Mobile Mode on your iPhone

September 13, '07 Comments [33] Posted in Musings
Sponsored By

The Hanselman Family - Windows Internet Explorer I'm thinking I'm going to take the iPhone back. I want to like it, I really do, but there's just so many things that don't fit into my life's workflow. For example, I just couldn't get the contacts and calendar to sync with Outlook for the life of me so I ended up going Outlook->Plaxo->Google Calendar->Spanning Sync->iCal->iPhone. Rube Goldberg would be proud. It's ridiculous. If it only had Exchange support...it'd be perfect. Anyway, I'm 85% going to take it back next week and pay the $40 restocking fee.

While I've been trying to get it to work I've also been totally hating the Gmail support. How can they possibly call POP3 support for Gmail support for Gmail?

The whole POINT of Gmail is that it's a view on this massive amount of email. POP3 support for Gmail apply filters (how could it?) and oddly, your own sent items come back at you as new inbox items. It's untenable if I want to keep a tidy mailbox. If Google added IMAP support, again, the problem would largely go away.

The Gmail Web Interface is yummy and wonderful in a real browser. It's utterly unusable and craptastic in the iPhone browser. The screen is too small.

Side bar: iGmail is a great compromise that Apple should take note of. It's a little Rails app that will give you an iPhone-looking interface that's really a web-proxied-face over Gmail. However, I haven't figured out how to make it point to my Google Apps Hosted Domain (where mail.hanselman.com is hosted) as GAFYD (Google Apps For Your Domain) is a fork of Gmail...it's not the same code.

So, since the Gmail Web Interface isn't usable and the POP3 solution isn't usable, what could be? Welll, Google Apps have a "mobile mode" that it'll switch into automatically when it detects a mobile device like a Windows Mobile Device or smaller phone. However, it doesn't see the iPhone this way and gives the desktop experience.

calendar - Windows Internet ExplorerTurns out you can add /x at the end of a Gmail URL to force mobile mode like:

https://mail.google.com/mail/x/

It's great if you're on a slow link in Africa. However, for hosted GAFYD sites the URL is different and less obvious. Also, the Gmail browser sniffer on the server side appears to really want to prevent you from "hurting yourself" and will stay in standard mode anyway.

However, try this

http://mail.google.com/a/YOURDOMAIN.com/x/?btmpl=mobile

The trick appears to be the "?btmpl=mobile" and it works great on an iPhone, but also in IE7.

In Google Calendar for your Domain you'd add /m to the URL:

http://www.google.com/calendar/hosted/YOURDOMAIN.com/m

Both of these, in my opinion, make Google Apps for your Domain more usable on the iPhone until Google decides to detect the iPhone as a mobile device and not only make the experience better but also conserve a lot of bandwidth and make for a snappier experience.

image

I'm still taking the iPhone back though. Actually, I think this is the my first truly failed technology purchase in recent memory. I should have paid more attention to the WAF (Wife Acceptance Factor) numbers. ;)

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by SherWeb

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.