By chance, I found an article written by José F. Romanello that shows how to do this for the WCF web API and adapt it. The source code is at the end of the answer.
Assuming I have four services, registering a routing registration changes the use of the ServiceRoute subclass, 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")); } }
WorkspaceService.SetLocationHeader now looks like this:
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 workspace uri from other services, for example 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 lines, and it is unlikely that changing the method name, service name, or routing table prefix will cause a system breakdown.
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 DefaultLinker constructor requires some changes to get the base uri of the web application, given that HTTPS can be stopped on the load balancer. This is beyond the scope of this answer.