Tuesday 7 August 2012

WCF Data Services NullReferenceException browsing metadata

I have recently discovered an issue with malformed Uri base addresses and WCF Data Services when querying the metadata for the service.
The issue causes an exception in the System.Data.Services.HttpContextServiceHost class which causes a http 500 in the client browser. Searching the internet did not yield much additional information in this problem.


wcfdataservicesexception

The offending line in the HttpContextServiceHost is the when the code accesses the UriTemplateMatch property, which is null, a NullReferenceException is thrown.

internal void VerifyQueryParameters()
       {
           HashSet<string> set = new HashSet<string>(StringComparer.Ordinal);
          NameValueCollection queryParameters = this.operationContext.IncomingRequest.UriTemplateMatch.QueryParameters;

To discover the reason why this property is null, I need to debug much further back in the request processing phase of the WCF Data Services stack.

Problem has its root in the the public string SelectOperation(ref Message message) of the System.ServiceModel.Dispatcher.WebHttpDispatchOperationSelector class. This operation is called during the http request that is made to the base address. You can see from the call stack below:

image

From what I understand from stepping through the code the method public string SelectOperation(ref Message message) is attempting to determine the operation to invoke from the request message. The matched operation is then assigned to the message properties collection which is accessed later, via the OperationContext.IncomingRequest.UriTemplateMatch. The method public string SelectOperation(ref Message message) makes a call to the method  protected virtual string SelectOperation(ref Message message, out bool uriMatched) which in turn calls an internal private method private bool CanUriMatch(UriTemplateTable methodSpecificTable, Uri to, HttpRequestMessageProperty prop, Message message, out string operationName) it is this method where the failure to match occurs and thus the OperationContext.IncomingRequest.UriTemplateMatch property is null.
public string SelectOperation(ref Message message)
{
    bool flag;
    if (message == null)
    {
        throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("message");
    }
    string property = this.SelectOperation(ref message, out flag);
    message.Properties.Add("UriMatched", flag);
    if (property != null)
    {
        message.Properties.Add("HttpOperationName", property);
        if (DiagnosticUtility.ShouldTraceInformation)
        {
            TraceUtility.TraceEvent(TraceEventType.Information, 0xf0025, SR2.GetString(SR2.TraceCodeWebRequestMatchesOperation, new object[] { message.Headers.To, property }));
        }
    }
    return property;
}
protected virtual string SelectOperation(ref Message message, out bool uriMatched)
{
    if (message == null)
    {
        throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("message");
    }
    uriMatched = false;
    if (this.methodSpecificTables != null)
    {
        UriTemplateTable table;
        if (!message.Properties.ContainsKey(HttpRequestMessageProperty.Name))
        {
            return this.catchAllOperationName;
        }
        HttpRequestMessageProperty requestProp = message.Properties[HttpRequestMessageProperty.Name] as HttpRequestMessageProperty;
        if (requestProp == null)
        {
            return this.catchAllOperationName;
        }
        string method = requestProp.Method;
        Uri to = message.Headers.To;
        if (to == null)
        {
            return this.catchAllOperationName;
        }
        if (this.helpUriTable != null)
        {
            UriTemplateMatch match = this.helpUriTable.MatchSingle(to);
            if (match != null)
            {
                uriMatched = true;
                this.AddUriTemplateMatch(match, requestProp, message);
                if (method == "GET")
                {
                    return "HelpPageInvoke";
                }
                WebHttpDispatchOperationSelectorData property = new WebHttpDispatchOperationSelectorData {
                    AllowedMethods = new List<string> { "GET" }
                };
                message.Properties.Add("HttpOperationSelectorData", property);
                return this.catchAllOperationName;
            }
        }
        if (this.methodSpecificTables.TryGetValue(method, out table))
        {
            string str2;
            uriMatched = this.CanUriMatch(table, to, requestProp, message, out str2);
            if (uriMatched)
            {
                return str2;
            }
        }
        if (this.wildcardTable != null)
        {
            string str3;
            uriMatched = this.CanUriMatch(this.wildcardTable, to, requestProp, message, out str3);
            if (uriMatched)
            {
                return str3;
            }
        }
        if (this.ShouldRedirectToUriWithSlashAtTheEnd(table, message, to))
        {
            return "";
        }
        List<string> list2 = null;
        foreach (KeyValuePair<string, UriTemplateTable> pair in this.methodSpecificTables)
        {
            if (((pair.Key != method) && (pair.Key != "*")) && (pair.Value.MatchSingle(to) != null))
            {
                if (list2 == null)
                {
                    list2 = new List<string>();
                }
                if (!list2.Contains(pair.Key))
                {
                    list2.Add(pair.Key);
                }
            }
        }
        if (list2 != null)
        {
            uriMatched = true;
            WebHttpDispatchOperationSelectorData data2 = new WebHttpDispatchOperationSelectorData {
                AllowedMethods = list2
            };
            message.Properties.Add("HttpOperationSelectorData", data2);
        }
    }
    return this.catchAllOperationName;
}
private void AddUriTemplateMatch(UriTemplateMatch match, HttpRequestMessageProperty requestProp, Message message)
{
    match.SetBaseUri(match.BaseUri, requestProp);
    message.Properties.Add("UriTemplateMatchResults", match);
}
private bool CanUriMatch(UriTemplateTable methodSpecificTable, Uri to, HttpRequestMessageProperty prop, Message message, out string operationName)
{
    operationName = null;
    UriTemplateMatch match = methodSpecificTable.MatchSingle(to);
    if (match != null)
    {
        operationName = match.Data as string;
        this.AddUriTemplateMatch(match, prop, message);
        return true;
    }
    return false;
}

When the Uri is matched the matched property is assigned to the “UriTemplateMatchResults” property of the request message. It is this property that is accessed by the OperationContext.IncomingRequest.UriTemplateMatch property and is the reason that it is returned as null and the cause of the exception.