bloudraak
bloudraak

Reputation: 6002

How does one set the location header to a UriTemplate of another service in WCF 4.0 REST without magic strings?

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).

Upvotes: 4

Views: 1956

Answers (2)

linkerro
linkerro

Reputation: 5458

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.

Upvotes: 1

bloudraak
bloudraak

Reputation: 6002

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.

Upvotes: 1

Related Questions