Ran into a bit of an issue today when an application I developed stopped working. Fortunately I built a pretty awesome tracing framework that I often drop into all my applications that lets me get at important runtime information from within the application. In this case I was able to see the WCF Data Services query that was being sent. It looked fine. The problem was that the response couldn’t be deserialized.

So I pasted the query into IE and saw this.

2010-07-16_1101

Ahh great so the XML is malformed. That explains why the WCF Data Services client library can’t give me any useful information. Now I could go to the trouble of using Fiddler or FiddlerCap but in this case there’s a much simpler way.

Start Notepad » File » Open » Paste the URL

2010-07-16_1027

I admit I only found this out recently that our old friend Notepad could be used to open HTTP URL’s.

In this case the error was due to the fact that the database I am working with is a train wreck that lacks any constraints so the integrity of the data was violated. That’s a different issue altogether and one that’s out of my hands, unfortunately.

In the OData specification, the $format parameter can be passed on the query string of the request to tell the server that you would like the response to be serialized as JSON. Normally, to get JSON-formatted data, you have to specify "application/json" in your "Accept" header. The query string feature is handy in situations when it’s not easy or possible to modify the request headers.

Unfortunately, if you try to pass $format on a WCF Data Services query, you will get a response that looks like:

<error xmlns="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
    <code /> 
    <message xml:lang="en-US">
        The query parameter '$format' begins with a system-reserved 
        '$' character but is not recognized.
    </message> 
</error>

Unfortunately, WCF Data Services doesn’t support this OData convention. But with a little bit of trickery, you can make it work by modifying the request in an ASP.NET HTTP module.

The trick is to check for the query string parameter before the request gets to WCF Data Services and modify the request headers accordingly. Normally, the Request.Headers collection is read-only. You’ll need to use some simple reflection to make it writable but once you do, it’s just a matter of setting the appropriate Accept header, then rewriting the URL to remove the $format parameter so WCF Data Services doesn’t bitch and moan.

The entire HTTP module is shown below. Like it? Hate it? Let me know in the comments.

<!-- add to web.config -->
<system.webServer>
    <modules runAllManagedModulesForAllRequests="true">
        <add name="ODataFormatModule" type="YourNamespace.ODataFormatModule, YourAssembly" />
    </modules>
</system.webServer>
/// <summary>
/// Intercepts WCF Data Services requests that include a $format parameter on the query
/// string and alters the request headers according to the OData specification.
/// </summary>
public sealed class ODataFormatModule : IHttpModule
{

    #region Constructors

    /// <summary>
    /// Initializes a new instance of the <see cref="T:ODataFormatModule"/> class.
    /// </summary>
    public ODataFormatModule( )
    {
    }

    #endregion

    #region Properties

    /// <summary>
    /// Gets the application instance.
    /// </summary>
    public HttpApplication Application
    {
        get;
        private set;
    }

    /// <summary>
    /// Gets the current HTTP context.
    /// </summary>
    public HttpContext Context
    {
        get
        {
            return HttpContext.Current;
        }
    }

    /// <summary>
    /// Gets the current request.
    /// </summary>
    public HttpRequest Request
    {
        get
        {
            return HttpContext.Current.Request;
        }
    }

    #endregion

    #region Methods

    /// <summary>
    /// Initializes a module and prepares it to handle requests.
    /// </summary>
    /// <param name="context">An <see cref="T:HttpApplication"/> that provides access to the methods, 
    /// properties, and events common to all application objects within an ASP.NET application</param>
    public void Init( HttpApplication context )
    {

        Application = context;
        Application.BeginRequest += Application_BeginRequest;

    }

    /// <summary>
    /// Disposes of the resources (other than memory) used by the module that implements
    /// <see cref="T:IHttpModule"/>.
    /// </summary>
    public void Dispose( )
    {
        Application.BeginRequest -= Application_BeginRequest;
        Application = null;
    }

    /// <summary>
    /// Forces the <see cref="T:NameValueCollection"/> to allow modifications by using reflection to
    /// set the IsReadOnly property to false.
    /// </summary>
    /// <param name="collection">The collection to make writable.</param>
    /// <returns>The original collection after the IsReadOnly property has been hacked.</returns>
    private static NameValueCollection MakeWritable( NameValueCollection collection )
    {

        var collectionType = collection.GetType( );
        var isReadOnlyProperty = collectionType.GetProperty(
            "IsReadOnly",
            BindingFlags.Instance |
            BindingFlags.IgnoreCase |
            BindingFlags.NonPublic
        );

        isReadOnlyProperty.SetValue( collection, false, null );

        return collection;

    }

    /// <summary>
    /// Gets a content type for the corresponding format parameter according to the OData specification
    /// http://www.odata.org/developers/protocols/uri-conventions#FormatSystemQueryOption
    /// </summary>
    /// <param name="format">The value of the $format querystring parameter.</param>
    /// <returns>The corresponding Accept request header value.</returns>
    private static string MapToMediaType( string format )
    {

        Contract.Requires( format != null );

        switch ( format.ToLowerInvariant() ) {
            case "atom":
            return "application/atom+xml";
            case "xml":
            return "application/xml";
            case "json":
            return "application/json";
            default:
            return format;
        }   // switch

    }

    #endregion

    #region Event Handlers

    /// <summary>
    /// Handles the BeginRequest event of the Application.
    /// </summary>
    /// <param name="sender">The source of the event.</param>
    /// <param name="e">The <see cref="T:EventArgs"/> instance containing the event data.</param>
    private void Application_BeginRequest( object sender, EventArgs e )
    {

        var format = Request.QueryString["$format"];

        if ( !String.IsNullOrWhiteSpace(format) ) {

            // Ordinarily, Request.Headers is read-only so we need to
            // use some reflection to get around that. Ugly, I know.
            var requestQuery = MakeWritable( Request.QueryString );
            var requestHeaders = MakeWritable( Request.Headers );

            // Set the Accept header the way a well-behaved json client
            // would have done, which WCF Data Services does support
            requestHeaders["Accept"] = MapToMediaType( format );

            // Use URL-rewriting to remove the $format part of the querystring
            // Otherwise, if it gets to WCF Data Services, it barfs
            requestQuery.Remove( "$format" );
            Context.RewritePath( Request.FilePath, Request.PathInfo, requestQuery.ToString( ) );

        }   // if

    }

    #endregion

}   // class