How to set location header on UriTemplate of another service in WCF 4.0 REST without magic lines?

Consider the following two REST services for WCF 4.0:

[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 the workspace, the Location header is set to the position of the document, which is essentially the same as when the DocumentService.GetDocument operation is called.

My global.asax looks like this:

 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 */ } } 

Implementation of WorkspaceService.SetLocationHeader as it makes some assumptions about how routing was configured. If I changed the route of the DocumentService , then the resulting Uri would be wrong. If I changed the UriTemplate DocumentService.GetDocument , then the resulting Uri will also be incorrect.

If the WorkspaceService and DocumentService were combined into one service, I could write 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 to write WorkspaceService.SetLocationHeader so that it uses the routing table defined in Global.asax and UriTemplates to return the Uri for the GetDocument DocumentService operation?

I am using the plain old WCF 4.0 (and not the WCF Web API).

+4
source share
2 answers

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.

+1
source

You use this:

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

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

Also, to fix the magic string problem, you can use constants that contain strings. This allows easy refactoring.

+1
source

Source: https://habr.com/ru/post/1396911/


All Articles