4

Consider the following two WCF 4.0 REST services:

[ServiceContract]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
public class WorkspaceService
{
    [WebInvoke(UriTemplate = "{id}/documents/{name}", Method = "POST")]
    public Document CreateWorkspaceDocument(Stream stream, string id, string name) 
    {
        /* CreateDocument is omitted as it isn't relevant to the question */
        Document response = CreateDocument(id, name, stream);

        /* set the location header */
        SetLocationHeader(response.Id);
    }

    private void SetLocationHeader(string id)
    {   
        Uri uri = new Uri("https://example.com/documents/" + id);
        WebOperationContext.Current.OutgoingResponse.SetStatusAsCreated(uri);
    }

    /* methods to delete, update etc */
}

[ServiceContract]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
public class DocumentService
{

    [WebGet(UriTemplate = "{id}")]
    public Document GetDocument(string id)
    {
    }

    /* methods to delete, update etc */
}

In essence, when someone creates a document in a workspace, the Location header is set to the location of the document, which is essentially is the same as invoking the DocumentService.GetDocument operation.

My global.asax looks as follows:

public class Global : HttpApplication
{
    private void Application_Start(object sender, EventArgs e)
    {
        RegisterRoutes();
    }

    private void RegisterRoutes()
    {
        var webServiceHostFactory = new WebServiceHostFactory();
        RouteTable.Routes.Add(new ServiceRoute("workspaces", webServiceHostFactory, typeof (WorkspaceService)));
        RouteTable.Routes.Add(new ServiceRoute("documents", webServiceHostFactory, typeof (DocumentService)));
        /* other services */
    }
}

The implementation of WorkspaceService.SetLocationHeader as it makes some assumptions about how routing was setup. If I was to change the route of DocumentService then the resulting Uri will be incorrect. If I changed the UriTemplate of DocumentService.GetDocument, then the resulting Uri will be incorrect too.

If WorkspaceService and DocumentService was merged into one service, I could have written SetLocationHeader as follows:

var itemTemplate = WebOperationContext.Current.GetUriTemplate("GetDocument");
var uri = itemTemplate.BindByPosition(WebOperationContext.Current.IncomingRequest.UriTemplateMatch.BaseUri, id);
WebOperationContext.Current.OutgoingResponse.SetStatusAsCreated(uri);

How would one write WorkspaceService.SetLocationHeader such that it will use the routing table defined in Global.asax and UriTemplates to return the Uri for the GetDocument operation of the DocumentService?

I'm using plain old WCF 4.0 (not the WCF Web API).

bloudraak
  • 5,902
  • 5
  • 37
  • 52

2 Answers2

1

You use this:

  RouteTable.Routes.GetVirtualPath(null,"route_name",null)

(here's an in depth article on asp.net routing outside of mvc http://msdn.microsoft.com/en-us/library/ie/dd329551.aspx) (and here's the documentation for the function: http://msdn.microsoft.com/en-us/library/cc680260.aspx)

Also, in order to eliminate the magic string issue, you can use constants that hold the strings. This allows for easy refactoring.

linkerro
  • 5,318
  • 3
  • 25
  • 29
  • This would help for ASP.NET MVC applications where almost each route has a route name. However, I fail to see how this is applicable to WCF 4.0 REST services which also uses an UriTemplate to further describe the URI. The idea here is to get rid of "magic strings" so that a change to the routing table, or UriTemplate doesn't invalidate the code written in SetLocationHeader. – bloudraak Feb 23 '12 at 20:19
  • Then I must have missed your point. I've edited my answer to include the magic strings issue. Still, I can't escape the feeling that there's something wrong with the thinking process on this one. Downvote the answer if it's not correct. – linkerro Feb 23 '12 at 22:23
  • this morning I stumbled on a way to do it. See if my answer makes any sense. – bloudraak Feb 23 '12 at 22:25
1

By accident, I found the an article written by José F. Romaniello which shows how to do it for the WCF Web API and adapted it. The source code is at the end of the answer.

Assuming I have four services, the routing registration changes to use a subclass of ServiceRoute which we later use to "evaluate" when scanning the routing table.

using System;
using System.Web;
using System.Web.Routing;

public class Global : HttpApplication
{
    private void Application_Start(object sender, EventArgs e)
    {
        RegisterRoutes();
    }

    private void RegisterRoutes()
    {
        RouteTable.Routes.Add(new ServiceRoute<Service1>("s1"));
        RouteTable.Routes.Add(new ServiceRoute<Service2>("s2"));
        RouteTable.Routes.Add(new ServiceRoute<Service3>("s3"));
        RouteTable.Routes.Add(new ServiceRoute<Service4>("s4"));
    }
}

The WorkspaceService.SetLocationHeader now looks as follows:

private void SetLocationHeader(string id)
{   
    ResourceLinker resourceLinker = new ResourceLinker();

    Uri uri = resourceLinker.GetUri<WorkspaceService>(s => s.Get(id));
    WebOperationContext.Current.OutgoingResponse.SetStatusAsCreated(uri);
}

The same code snippet can be used to set the uri of a workspace from other services, such as DocumentService.Get

[WebGet("{id}")]
public Document Get(string id)
{
    // can be static
    ResourceLinker resourceLinker = new ResourceLinker();

    DocumentEntity entity = _repository.FindById(id);
    Document document = new Document();
    document.Name = entity.Name; 
    // map other properties
    document.Workspace.Name = entity.Workspace.Name;
    document.Workspace.Uri = resourceLinker.GetUri<WorkspaceService>(s => s.Get("0"));
    // map other properties
    return document;
}

With this approach there are no magic strings and its unlikely that a change to a method name, service name, routing table prefix will break the system.

Here is the implementation adapted from the article :

using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
using System.Web.Routing;

public interface IServiceRoute
{
    Type ServiceType
    {
        get;
    }

    string RoutePrefix
    {
        get;
        set;
    }
}

public class ServiceRoute<T> : ServiceRoute, IServiceRoute
{
    public ServiceRoute(string routePrefix) : this(routePrefix, new WebServiceHostFactory())
    {
    }

    public ServiceRoute(string routePrefix, ServiceHostFactoryBase serviceHostFactory)
        : base(routePrefix, serviceHostFactory, typeof (T))
    {
        RoutePrefix = routePrefix;
        ServiceType = typeof (T);
    }

    #region IServiceRoute Members

    public string RoutePrefix
    {
        get;
        set;
    }

    public Type ServiceType
    {
        get;
        private set;
    }

    #endregion
}

public static class RouteTableExtensions
{
    public static void AddService<T>(this RouteCollection routeCollection, string routePrefix)
    {
        routeCollection.Add(new ServiceRoute<T>(routePrefix));
    }

    public static string GetRoutePrefixForType<T>(this RouteCollection routeCollection)
    {
        var routeServiceType = routeCollection
            .OfType<IServiceRoute>()
            .FirstOrDefault(r => r.ServiceType == typeof (T));
        if (routeServiceType != null)
        {
            return routeServiceType.RoutePrefix;
        }
        return null;
    }
}

public interface IResourceLinker
{
    Uri GetUri<T>(Expression<Action<T>> restMethod);
}

public class ResourceLinker : IResourceLinker
{
    private readonly Uri _baseUri;

    public ResourceLinker()
        : this("http://localhost:53865")
    {
    }

    public ResourceLinker(string baseUri)
    {
        _baseUri = new Uri(baseUri, UriKind.Absolute);
    }

    #region IResourceLinker Members

    public Uri GetUri<T>(Expression<Action<T>> restMethod)
    {
        var methodCallExpression = (MethodCallExpression) restMethod.Body;
        var uriTemplateForMethod = GetUriTemplateForMethod(methodCallExpression.Method);

        var args = methodCallExpression.Method
            .GetParameters()
            .Where(p => uriTemplateForMethod.Contains("{" + p.Name + "}"))
            .ToDictionary(p => p.Name, p => ValuateExpression(methodCallExpression, p));

        var prefix = RouteTable.Routes.GetRoutePrefixForType<T>();
        var newBaseUri = new Uri(_baseUri, prefix);
        var uriMethod = new UriTemplate(uriTemplateForMethod, true);
        return uriMethod.BindByName(newBaseUri, args);
    }

    #endregion

    private static string ValuateExpression(MethodCallExpression methodCallExpression, ParameterInfo p)
    {
        var argument = methodCallExpression.Arguments[p.Position];
        var constantExpression = argument as ConstantExpression;
        if (constantExpression != null)
        {
            return constantExpression.Value.ToString();
        }

        //var memberExpression = (argument as MemberExpression);
        var lambdaExpression = Expression.Lambda(argument, Enumerable.Empty<ParameterExpression>());
        var result = lambdaExpression.Compile().DynamicInvoke().ToString();
        return result;
    }

    private static string GetUriTemplateForMethod(MethodInfo method)
    {
        var webGet = method.GetCustomAttributes(true).OfType<WebGetAttribute>().FirstOrDefault();
        if (webGet != null)
        {
            return webGet.UriTemplate ?? method.Name;
        }

        var webInvoke = method.GetCustomAttributes(true).OfType<WebInvokeAttribute>().FirstOrDefault();
        if (webInvoke != null)
        {
            return webInvoke.UriTemplate ?? method.Name;
        }

        throw new InvalidOperationException(string.Format("The method {0} is not a web method.", method.Name));
    }
}

The default constructor of ResourceLinker requires some changes to pick up the base uri of the web application, taking into account that HTTPS may be terminated at the load balancer. That falls outside of this answer.

bloudraak
  • 5,902
  • 5
  • 37
  • 52