Wednesday, 3 November 2010

Colin McRae Dirt 2 – Choppy sound and crashes in multiplayer

I like my driving games; always have done since the original Mario Kart on the SNES way back when.

So the other day I bought Dirt 2 on Steam as I needed some driving action (my previous driving squeeze was Grid – and I can highly recommend that too).  Very cool, especially with XBox 360 wireless controllers.

After clocking up 20 hours or so on the solo missions I decided to give the online game a go.  Whilst in the trailer I navigated to the Multiplayer ‘board’ and hit ‘online’.

The instant I hit the button on the pad, the sound started to crackle & stutter; then I lost everything except the menu sound effects (only later for them to return for about 10 seconds). I’ve had this before (during the videos on Streetfighter 4, for example) and I just thought ‘well if the game still plays, I’ll put up with it for now’.

Only it didn’t play – not exactly anyway.  The first race went okay (apart from getting battered by the opposition!), and then I went back to the menu ready for the next race to begin.  When the next race began to load, the game just froze.

So I fired it up again and had another go.  Got through the first race again, only on the return to the menu, it got stuck when the camera zooms out from the ‘photo’ that’s taken at the end.

All the while, the sound’s doing crazy things.  Oh and the framerate in-race was not as good as it should have been either (of course it can drop slightly in Multiplayer given the extra work going on to do the networking).

Now my machine isn’t top-end (at least not until the year’s bonus comes in ;) ) but it’s still pretty beefy:

Intel Q9650 on an NVidia 780i Ultra Quad Sli board (can’t remember the manufacturer any more, possibly ASUS – but it cost a bit).
4Gb Corsair Ram as 2x2Gbs using stock timings
Two BFG GTX 280s (not OCd) in SLI
SB x-Fi Pci-ex card.

So there’s really no excuse there for the kind of crap that was going down!

I did a google search for the problem and lots of people have been reporting similar problems, but seemingly not to any decent conclusion.  Lots of talk of onboard hardware, out of date drivers etc.  One common thread seemed to be Win7 x64 (which I run), but ultimately my machine couldn’t be any more up-to-date driver-wise (I’m a habitual upgrader).

Lots of people were moaning about the Rapture sound system that the game uses as well – well, mine was setup at it’s defaults (and still is).

So I gave up.

A couple of days later I was mucking about on the Games for Windows (GfW) screens in-game and stumbled across the Voice settings.  Guess what – as soon as the UI for that settings panel was displayed, the exact-same sound interference kicked in again!  I exited that screen and it went away straight away too.

Now, I don’t use a headset with a microphone (don’t play any games where verbally abusing everyone else will either help or add anything); so what I’m about to say will NOT help those of you that do.

I tried disabling the voice stuff in GfW, but it didn’t make any difference.

So instead I pressed ALT+ENTER to get out of full screen and opened the Windows Recording Devices UI from the Volume Control.  I went through and disabled all the recording devices.

Went back to the game window and back into fullscreen (ALT+ENTER again) and all of a sudden the sound was clean again on the Voice UI in the GfW overlay.

So, immediately I went to the multiplayer menu again – hit ‘Online’ as I was before and this time – no crackle, no stuttering or anything: everything working exactly as it should.

I then went on to play (and lose!) a bunch of online races, no pauses between loading, frame-rate nice and crisp as in Single Player and, best of all, sound stayed intact all the way through.

So – there you have it – disable the recording devices (probably only have to disable one or two of them, but I don’t do any audio recording on my machine anyway, so I’m happy with them all switched off) and have your problem fixed!

There’s clearly an underlying bug here – possibly at the Dirt 2 level, or equally at the GfW level, so that needs to be fixed; although the game is nearly a year old now so I doubt it ever will be.

In the meantime I can now get back to getting my ass handed to me by supafly teenagers with too much time on their hands; like I used to be 15 years ago.

Tuesday, 21 September 2010

Map System.TimeSpan to xs:duration for the DataContractSerializer

I’m working on a new RESTful service (I’m using Asp.Net MVC for this) and I’m keen to make it as easy to integrate with as across as many client platforms as possible.

I’m going to be using XML exclusively, as I it enables me to produce schemas that our service clients will be able to download and then see the shape of each object that the service operations will expect.

I also want to employ XML schema validation on the data coming in from the request.  Doing this will trap most data errors it gets further down the request pipeline, thus protecting my code; but will also ensure that the caller gets nice verbose error messages – XML Schema Validation is pretty explicit on what’s gone wrong!

Thus, I’ve wired up an MVC action to produce a schema for the relevant objects using the XsdDataContractExporter class.  I will simply point users at this, alongside documentation for each of the operations; which will include schema type names from that ‘live’ schema.

While testing out one of the types, I noticed that a TimeSpan member was being serialized as xs:string and not as xs:duration.  I consulted an MSDN topic I should now know by heart (given how many times I’ve looked at it) to check the support for TimeSpan in the DataContractSerializer and sure enough it’s there; but I couldn’t understand why, if DateTime is indeed mapped to the XML DateTime type, it’s not mapped to the XML Duration type.

So I’ve written a TimeSpan type called XmlDuration that is implicitly convertible to System.TimeSpan but which, when you expose it as a member on a Data Contract, presents itself as xs:duration.  This then means that if you enable schema validation on your incoming XML, the input string will be validated against the rules attached to the XML Duration type, instead of being simply a string that allows any content.

The code is as follows.  There’s one really long line in there which is a Regex that I’ve split into multiple string additions purely for this post; you can join the strings back up again if you so desire:

  1. /// <summary>
  2. /// This type is a TimeSpan in the .Net world but, when
  3. /// serialized as Xml it behaves like an XML duration type.  
  4. /// For more about the duration data type in XML - see
  5. /// http://www.w3.org/TR/xmlschema-2/#duration
  6. ///
  7. /// You should use this on types that intend to send a
  8. /// timespan to ensure clients can read the data in a
  9. /// conformant manner.
  10. ///
  11. /// Note that when the type writes out to XML, it starts
  12. /// with days; not years.  That is because .Net timespan
  13. /// only expresses days.
  14. ///
  15. /// If the original duration string is preserved from
  16. /// the input XML, then the same duration instance will
  17. /// serialize out using that same string.
  18. /// </summary>
  19. [XmlSchemaProvider("GetTypeSchema")]
  20. public sealed class XmlDuration : IXmlSerializable
  21. {
  22.     /// <summary>
  23.     /// When this instance is loaded from XML, this is
  24.     /// the original string.
  25.     /// </summary>
  26.     public string ValueString { get; private set; }
  27.  
  28.     /// <summary>
  29.     /// The inner .Net TimeSpan value for this instance
  30.     /// </summary>
  31.     public TimeSpan Value { get; private set; }
  32.  
  33.     public XmlDuration() { }
  34.  
  35.     public XmlDuration(TimeSpan duration)
  36.     {
  37.         Value = duration;
  38.     }
  39.  
  40.     public XmlDuration(XmlDuration duration)
  41.     {
  42.         Value = duration.Value;
  43.         ValueString = duration.ValueString;
  44.     }
  45.  
  46.     public XmlDuration(string duration)
  47.     {
  48.         try
  49.         {
  50.             Value = TimeSpanFromDurationString(duration);
  51.             ValueString = duration;
  52.         }
  53.         catch (ArgumentException aex)
  54.         {
  55.             throw new ArgumentException
  56.                 ("Invalid duration (see inner exception", "duration",
  57.                 aex);
  58.         }
  59.     }
  60.  
  61.     public static implicit operator XmlDuration(TimeSpan source)
  62.     {
  63.         return new XmlDuration(source);
  64.     }
  65.  
  66.     public static implicit operator TimeSpan(XmlDuration source)
  67.     {
  68.         return source.Value;
  69.     }
  70.  
  71.     private static Regex RxRead = new Regex(@"^(?<ISNEGATIVE>-)?" +
  72.         @"P((?<YEARS>[0-9]+)Y)?((?<MONTHS>([0-9])+)M)?((?<DAYS>([0-9])+)D)?" +
  73.         @"(T((?<HOURS>([0-9])+)H)?((?<MINUTES>([0-9])+)M)?((?<SECONDS>([0-9]" +
  74.         @")+(\.[0-9]{1,3})?)S)?)?$", RegexOptions.Compiled);
  75.  
  76.     /// <summary>
  77.     /// Constructs a new TimeSpan instance from the pass XML
  78.     /// duration string (see summary on this type for a link
  79.     /// that describes the format).
  80.     ///
  81.     /// Note that if the input string is not a valid XML
  82.     /// duration, an argument exception will occur.
  83.     /// </summary>
  84.     /// <param name="value"></param>
  85.     /// <returns></returns>
  86.     public static TimeSpan TimeSpanFromDurationString(string value)
  87.     {
  88.         TimeSpan toReturn = TimeSpan.MinValue;
  89.         var match = RxRead.Match(value);
  90.  
  91.         match.ThrowIf(m => m.Success == false, "value",
  92.             "The string {0} is not a valid XML duration".FormatWith(value));
  93.  
  94.         bool isNegative = false;
  95.         int years = 0, months = 0, days = 0, hours = 0, minutes = 0;
  96.         double seconds = 0;
  97.  
  98.         var group = match.Groups["ISNEGATIVE"];
  99.         isNegative = group.Success;
  100.  
  101.         group = match.Groups["YEARS"];
  102.         if (group.Success)
  103.             years = int.Parse(group.Value);
  104.         group = match.Groups["MONTHS"];
  105.         if (group.Success)
  106.             months = int.Parse(group.Value);
  107.         group = match.Groups["DAYS"];
  108.         if (group.Success)
  109.             days = int.Parse(group.Value);
  110.         group = match.Groups["HOURS"];
  111.         if (group.Success)
  112.             hours = int.Parse(group.Value);
  113.         group = match.Groups["MINUTES"];
  114.         if (group.Success)
  115.             minutes = int.Parse(group.Value);
  116.         group = match.Groups["SECONDS"];
  117.         if (group.Success)
  118.             seconds = double.Parse(group.Value);
  119.  
  120.         //now have to split the seconds into whole and fractional.
  121.         //note - there is clearly a potential for a loss of fidelity
  122.         //here given that we're expanding years and months to 365 and
  123.         //30 days respectively. There's no perfect solution - although
  124.         //you can simply ask your web service clients to express all
  125.         //durations in terms of days, hours, minutes and seconds.
  126.         int wholeSeconds = (int)seconds;
  127.         seconds -= wholeSeconds;
  128.         toReturn = new TimeSpan((years * 365) + (months * 30) + days,
  129.             hours, minutes, wholeSeconds, (int)(seconds * 1000));
  130.         if (isNegative)
  131.             toReturn = toReturn.Negate();
  132.  
  133.         return toReturn;
  134.     }
  135.  
  136.     #region IXmlSerializable Members
  137.  
  138.     /// <summary>
  139.     /// Returns a qualified name of
  140.     /// http://www.w3.org/2001/XMLSchema:duration
  141.     /// </summary>
  142.     /// <param name="xs"></param>
  143.     /// <returns></returns>
  144.     public static XmlQualifiedName GetTypeSchema(XmlSchemaSet xs)
  145.     {
  146.         return new XmlQualifiedName
  147.             ("duration", "http://www.w3.org/2001/XMLSchema");
  148.     }
  149.  
  150.     public System.Xml.Schema.XmlSchema GetSchema()
  151.     {
  152.         //see the static GetTypeSchema method.
  153.         return null;
  154.     }
  155.  
  156.     public void ReadXml(System.Xml.XmlReader reader)
  157.     {
  158.         string s = reader.ReadElementContentAsString();
  159.         if (s.IsNotWhitespaceOrNull())
  160.         {
  161.             Value = TimeSpanFromDurationString(s);
  162.             ValueString = s;
  163.         }
  164.         else
  165.             Value = TimeSpan.MinValue;
  166.     }
  167.  
  168.     public void WriteXml(System.Xml.XmlWriter writer)
  169.     {
  170.         StringBuilder sb = new StringBuilder();
  171.  
  172.         //if we have the original duration string then we write that back out.
  173.         if (ValueString.IsNotWhitespaceOrNull())
  174.             writer.WriteValue(ValueString);
  175.         else
  176.         {
  177.             if (Value.Ticks < 0)
  178.                 sb.Append('-');
  179.  
  180.             bool isFractionalSeconds =
  181.                 ((double)((int)Value.TotalSeconds)) != Value.TotalSeconds;
  182.  
  183.             sb.AppendFormat("P{0}D", (int)Value.TotalDays);
  184.             sb.AppendFormat("T{0}H", Value.Hours);
  185.             sb.AppendFormat("{0}M", Value.Minutes);
  186.             sb.AppendFormat("{0}S",
  187.                 isFractionalSeconds ?
  188.                     "{0}.{1}".FormatWith(Value.Seconds, Value.Milliseconds)
  189.                 : Value.Seconds.ToString());
  190.  
  191.             writer.WriteValue(sb.ToString());
  192.         }
  193.     }
  194.  
  195.     #endregion
  196. }

I’ve taken this class and merged it into the System.Xml namespace – because clearly this will also work with the XmlSerializer as well as for the DataContractSerializer.

A few notes.

The nifty part of this class is in the use of the XmlSchemaProviderAttribute.  This is the .Net framework’s preferred mechanism for mapping types to Xml schema.  In theory, if you were writing a more complex custom type for which Schema simply cannot be auto-generated, you could manually inject the schema into the XmlSchemaSet passed into the GetTypeSchema method (the name of which is determined by the parameter you pass to the attribute constructor).  You would then return the XmlQualifiedName of this schema type to satisfy the framework.

In this case all we have to do is to return the well-known qualified name of the xml duration type: ‘duration’ from the namespace ‘http://www.w3.org/2001/XmlSchema’.  We should be able to rely on anyone working with XML to have mapped this namespace already, and we know that a schema exporter will be doing the same since most of the .Net fundamental types are mapped to the same namespace.

Under the hood I’ve written a simple regex parser based on the format for the Duration data type.  It will recognise all valid strings, but it also lets one or two invalid ones through (notably ‘PT’).  However, if you are also using schema validation then this will not trouble you.

As the comments state - years and months are a problem; since the .Net TimeSpan chickens out and doesn’t encode years/months (presumably because it makes it much easier to calculate them from the difference of two DateTimes, as well as to add one onto a DateTime).  Of course neither have a fixed number of days; so I’ve gone for a reasonable average.  You could be more clever and take the current month’s number of days plus a strict average of 365.25 days per year – it depends on how accurate you really need it to be.

If you’re writing a new web service, you can simply make sure that all your clients express durations starting with the number of days – e.g. ‘P400D’ which will be deserialized exactly into a .Net TimeSpan representing 400 days.

In situations where a duration is received from a client and might need to be sent back to them – the original input string is preserved (but it will be up to you to persist that server side).

So now you can change a DataContract class like this:

  1. public class Class1
  2. {
  3.     public TimeSpan Duration { get; set; }
  4. }

And change it over to this:

  1. public class Class1
  2. {
  3.     public XmlDuration Duration { get; set; }
  4. }

In this case these types aren’t annotated of course (whereas in my case all my exported types are).

If you’re publishing a data contract for a type that must also implement some internal interface that exposes a TimeSpan like this:

  1. public interface IClassInternal
  2. {
  3.     TimeSpan Duration { get; set; }
  4. }

Then your best policy is to write the DataContract class as follows:

  1. public class Class1 : IClassInternal
  2. {
  3.     public XmlDuration Duration { get; set; }
  4.  
  5.     #region IClassInternal Members
  6.  
  7.     TimeSpan IClassInternal.Duration
  8.     {
  9.         get
  10.         {
  11.             //use implicit casting operator
  12.             return Duration;
  13.         }
  14.         set
  15.         {
  16.             if(value!=null)
  17.                 Duration = value; //and here
  18.         }
  19.     }
  20.  
  21.     #endregion
  22. }

Obviously, there is an argument here that the XmlDuration should, in fact, be a value type and not a class.  I’ll leave that up to you to decide.

Thursday, 16 September 2010

MVC Bug: The virtual path '[path]' maps to another application, which is not allowed

I’ve being doing a lot of work on Asp.Net MVC (now v2) over the past few months and, firstly, I have to say that it totally rocks.

I will not go into any real detail about MVC here – this post is about the method System.Web.Mvc.HtmlHelper.AntiForgerytoken and that it highlights a bug in an internal class TokenPersister that you’re potentially going to get if you ever use spaces in your virtual directory names.  Ironically, the class in question is marked with a comment that says it’s difficult to unit test because of the way it fakes Asp.Net requests – ironic that it should be such a class in which we find a bug!  A better argument for unit-testing you’re unlikely to find.

Detail – Reproduce that bug

Using MVC it’s very easy to reproduce.  I should start by declaring my system configuration:

  • Windows 2008 R2
  • IIS 7.5 installed and configured
  • Visual Studio 2008 with all service packs etc
  • .Net 3.5 sp1 plus security patches

VS2010 and .Net 4 are also on this machine – and I would welcome anybody following the same steps to confirm that i:

  1. Create a new Asp.Net MVC2 Web Application (from the template) called ‘Asp Net Bug 2008’
    • Don’t bother with the unit tests project
  2. Open the project web properties, and set the project to use the Local IIS Web Server, the location should automatically be set to http://localhost/Asp Net Bug 2008/ (you might need to add the trailing slash here) – the spaces here are important so leave them in.
    • Create the Virtual Directory
    • Don’t forget to save the project file afterwards (like I just did as I created this walkthrough!)
  3. Open the view Views/Account/Register.aspx
  4. Just after the line
    1. <% using (Html.BeginForm()) { %>
    Add the following:
    1. <%= Html.AntiForgeryToken() %>
  5. Compile and run
  6. Navigate to the [Log On] link at the top of the page
  7. Hit the ‘Register’ link

You will see this exception helper:

 mvcexception

And then on the Asp.Net error page, you’ll see this stack trace:

[ArgumentException: The virtual path '/Asp%20Net%20Bug%202008/Account/Register' maps to another application, which is not allowed.]
System.Web.CachedPathData.GetVirtualPathData(VirtualPath virtualPath, Boolean permitPathsOutsideApp) +11193138
System.Web.HttpContext.GetFilePathData() +61
System.Web.Configuration.HttpCapabilitiesBase.GetBrowserCapabilities(HttpRequest request) +124
System.Web.HttpRequest.get_Browser() +168
System.Web.UI.Page.SetIntrinsics(HttpContext context, Boolean allowAsync) +207
System.Web.UI.Page.ProcessRequest(HttpContext context) +232
System.Web.Mvc.TokenPersister.CreateFormatterGenerator() +459
System.Web.Mvc.FormatterGenerator..cctor() +10

[TypeInitializationException: The type initializer for 'FormatterGenerator' threw an exception.]
System.Web.Mvc.AntiForgeryDataSerializer.get_Formatter() +38
System.Web.Mvc.AntiForgeryDataSerializer.Serialize(AntiForgeryData token) +181
System.Web.Mvc.HtmlHelper.GetAntiForgeryTokenAndSetCookie(String salt, String domain, String path) +405
System.Web.Mvc.HtmlHelper.AntiForgeryToken(String salt, String domain, String path) +13
System.Web.Mvc.HtmlHelper.AntiForgeryToken() +17
ASP.views_account_register_aspx.__RenderregisterContent(HtmlTextWriter __w, Control parameterContainer) in c:\Users\andras.zoltan\documents\visual studio 2008\projects\aspnetbug2008\aspnetbug2008\Views\Account\Register.aspx:17
System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children) +115
ASP.views_shared_site_master.__Render__control1(HtmlTextWriter __w, Control parameterContainer) in c:\Users\andras.zoltan\documents\visual studio 2008\projects\aspnetbug2008\aspnetbug2008\Views\Shared\Site.Master:26
System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children) +115
System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children) +240
System.Web.UI.Page.Render(HtmlTextWriter writer) +38
System.Web.Mvc.ViewPage.Render(HtmlTextWriter writer) +89
System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +4240

A closer look at this, you’ll see that the method GetBrowserCapabilities, called from the accessor of the HttpRequest.Browser is the culprit.  Opening Reflector, we see that this method is attempting to load the site configuration (web.config presumably) – which is where GetVirtualPathData comes in.  When I was confronted with this issue, it was obvious that the check which determines if the current request is in the current application root was failing.  So the first thing I thought was to change our app root to be something more friendly.


The Fix


Take the spaces out of the virtual directory name; perhaps replacing with hyphens, or just zeroing them down completely.  It’ll now work perfectly.


The Cause


Well, of course I haven’t analysed down to the last IL opcode here, but the first thing I thought was whether or not this was an Asp.Net bug, since the last few items in the call stack are from there.  So you can try another test, if you simply add a standard WebForm to the project (making sure it’s hosted in a vdir again that has spaces in it), and put in the following:





  1. <%= (Request.Browser!=null).ToString() %>

Compile and run that, and you’ll correctly get ‘True’ output in the page.  So, when the current HttpRequest is produced from the normal Asp.Net pipeline, everything works correctly.


So, let’s take another look at that stack trace – we have the call to

System.Web.Mvc.TokenPersister.CreateFormatterGenerator
Which the yields a call to
System.Web.UI.Page.ProcessRequest

But we’re already in that method further down, so why?


The reason can be found if you open CreateFormatterGenerator, either by getting the source code from CodePlex or by opening reflector.  Let’s take a look at the method body (which I have ripped straight out of the published project from CodePlex):





  1. public static Func<IStateFormatter> CreateFormatterGenerator()
  2. {
  3.     // This code instantiates a page and tricks it into thinking
  4.     // that it's servicing a postback scenario with encrypted
  5.     // ViewState, which is required to make the StateFormatter
  6.     // properly decrypt data. Specifically, this code sets the
  7.     // internal Page.ContainsEncryptedViewState flag.
  8.     TextWriter writer = TextWriter.Null;
  9.     HttpResponse response = new HttpResponse(writer);
  10.     HttpRequest request = new HttpRequest("DummyFile.aspx",
  11.         HttpContext.Current.Request.Url.ToString(), "__EVENTTARGET=true&__VIEWSTATEENCRYPTED=true");
  12.     HttpContext context = new HttpContext(request, response);
  13.  
  14.     Page page = new Page()
  15.     {
  16.         EnableViewStateMac = true,
  17.         ViewStateEncryptionMode = ViewStateEncryptionMode.Always
  18.     };
  19.     page.ProcessRequest(context);
  20.  
  21.     return () => new TokenPersister(page).StateFormatter;
  22. }

Well, the embedded comment there explains what’s going on – and it’s this fake request that’s causing the problem, clearly.  Whatever it is that Asp.Net does to make sure we don’t get this error on a standard WebForm is not being done properly – although I’m not sure what that could be.  My initial guess is that the %20 is being compared to the space in the path name, rather than being url-decoded first – and therefore a simple fix might be to change the call to HttpContext.Current.Request.Url.ToString() on the fourth code line above to pass in the url-decoded string.  If I was feeling ambitious, I’d hack into the project and pursue a fix.  However, my primary feeling is to report it to the Mvc team – which I have done here.

Serializing to attributes in WCF with DataContractSerializer

It’s a common problem – you want to return an object from a WCF service as XML, but you either want, or need, to deliver some or all of the property values as XML Attributes instead of XML Elements; but you can’t because the DataContractSerializer doesn’t support attributes (you’re most likely to have seen this StackOverflow QA if you’ve done a web search).  Most likely you’ve then migrated all your WCF service code to using the XmlSerializer (with all the XmlElement/XmlAttribute/XmlType attributes et al) – and you’ve cursed loudly.

Well, I’m here to rescue you, because it is possible – and the answer to the problem is actually inferred from the MSDN article entitled ‘Types supported by the Data Contract Serializer’.

The example I’m going to give is purely for illustration purposes only.  I don’t have a lot of time, so work with me!

  • Create a new Asp.Net WCF service application, you can use Cassini as your web server (probably easier – otherwise you might have to enable Asp.Net compatibility mode).
  • Open the web.config and delete the <service> element that was created for the new service.
  • The interface and implementation model for this example is overkill.  Move the [ServiceContract] and [OperationContract] declarations from the interface that was created for you new service to the class that was also created.  Delete the interface.
  • Open the .svc markup file and add the following at the end: Factory="System.ServiceModel.Activation.WebServiceHostFactory" – this enables the zero-configuration WCF model for this service (we’re going to create a RESTful service).
  • Paste the following class declarations into your svc codebehind:
public interface IExampleData
{
	string Description { get; set; }
	string Name { get; set; }
	int ID { get; set; }
}
public class ExampleData : IExampleData
{
	public string Description { get; set; }
	public string Name { get; set; }
	public int ID { get; set; }
}
public class ExampleDataAttributed : ExampleData, IXmlSerializable
{
	#region IXmlSerializable Members
	public System.Xml.Schema.XmlSchema GetSchema()
	{
		return null;
	}
	public void ReadXml(System.Xml.XmlReader reader)
	{
		//implement if remote callers are going to pass your object in
	}
	public void WriteXml(System.Xml.XmlWriter writer)
	{
		writer.WriteAttributeString("id", ID.ToString());
		writer.WriteAttributeString("name", Name);
		//we'll keep the description as an element as it could be long.
		writer.WriteElementString("description", Description);
	}
	#endregion
}

Just to demonstrate the point, the class that will be part-serialized to attributes simply derives from one that will be serialized as normal.



  • Now add the following two methods to your service class:
[OperationContract]
[WebGet(UriTemplate = "/test1")]
public ExampleData Test1()
{
	return new ExampleData() { ID = 1, 
Name = "Element-centric",
Description =
"The contents of this item are entirely serialized to elements - as normal" };
}
[OperationContract]
[WebGet(UriTemplate = "/test2")]
public ExampleDataAttributed Test2()
{
	return new ExampleData_Attributed() { ID = 2, 
Name = "Mixed",
Description =
"Everything except this description will be serialized to attributes" };
}

Cover, and bake for 40 minutes (that is – Build it).


If you left your service as Service1.svc, then run it and open up IE and browse to http://localhost:[port of cassini]/test1


The result should look something like this:

<JSLabs.ExampleData 
 xmlns="http://schemas.datacontract.org/2004/07/ExampleNamespace" 
 xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
	<Description>
		The contents of this item are entirely serialized to elements - as normal
	</Description>
	<ID>
		1
	</ID>
	<Name>
		Element-centric
	</Name>
</JSLabs.ExampleData>

Now browse to http://localhost:[port of cassini]/test2

<JSLabs.ExampleDataAttributed id="2" name="Mixed" 
  xmlns="http://schemas.datacontract.org/2004/07/JobServe.Labs.Web">
	<description>Everything except this description will be 
	serialized to attributes</description>
</JSLabs.ExampleDataAttributed>

It’s made a little less impressive by that nasty ‘orrible “xmlns=” attribute that the WCF data contract serializer automatically puts on the type – but, as you can see, the ‘ID’ and ‘Name’ properties have indeed been pushed out as attributes!


We could have made both methods return IExampleData and then used the KnownType attribute on that interface in order to get it to support either (according to what the code of the methods returned).


To support deserializing an object from the attributes, all you have to do is to implement the IXmlSerializable.ReadXml method.


Finally, as the aforementioned MSDN article says about the supported types – you should also be able to use XmlElement/XmlNode types as a way of representing XML directly – the DataContractSerializer, like in this case, take the short route and simply gets the Xml.


This also shouldn’t affect JSON formatting if you’re dual-outputting objects for either XML or JSON clients.