【问题标题】:How does one set the location header to a UriTemplate of another service in WCF 4.0 REST without magic strings?如何在没有魔术字符串的 WCF 4.0 REST 中将位置标头设置为另一个服务的 UriTemplate?
【发布时间】:2012-03-08 05:48:38
【问题描述】:

考虑以下两个 WCF 4.0 REST 服务:

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

本质上,当有人在工作空间中创建文档时,Location 标头设置为文档的位置,这与调用DocumentService.GetDocument 操作基本相同。

我的 global.asax 如下所示:

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

WorkspaceService.SetLocationHeader 的实现,因为它对路由的设置方式做了一些假设。如果我要更改 DocumentService 的路线,那么生成的 Uri 将不正确。如果我更改了DocumentService.GetDocument 的UriTemplate,那么生成的Uri 也会不正确。

如果 WorkspaceService 和 DocumentService 合并为一个服务,我可以写成 SetLocationHeader 如下:

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

如何编写WorkspaceService.SetLocationHeader 以便它使用在 Global.asax 和 UriTemplates 中定义的路由表来返回用于 DocumentService 的 GetDocument 操作的 Uri?

我使用的是普通的旧 WCF 4.0(不是 WCF Web API)。

【问题讨论】:

    标签: wcf c#-4.0 wcf-rest


    【解决方案1】:

    我偶然发现了由 José F. Romaniello 编写的 an article,它展示了如何为 WCF Web API 执行此操作并对其进行了改编。源码在答案的最后。

    假设我有四个服务,路由注册更改为使用 ServiceRoute 的子类,我们稍后在扫描路由表时使用它来“评估”。

    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 现在如下所示:

    private void SetLocationHeader(string id)
    {   
        ResourceLinker resourceLinker = new ResourceLinker();
    
        Uri uri = resourceLinker.GetUri<WorkspaceService>(s => s.Get(id));
        WebOperationContext.Current.OutgoingResponse.SetStatusAsCreated(uri);
    }
    

    同样的代码sn-p可以用来设置来自其他服务的工作区的uri,比如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;
    }
    

    使用这种方法没有魔法字符串,更改方法名称、服务名称、路由表前缀不太可能破坏系统。

    这是改编自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));
        }
    }
    

    考虑到 HTTPS 可能在负载均衡器处终止,ResourceLinker 的默认构造函数需要进行一些更改才能获取 Web 应用程序的基本 uri。这不在此答案范围内。

    【讨论】:

      【解决方案2】:

      你用这个:

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

      (这是一篇关于 mvc 之外的 asp.net 路由的深入文章 http://msdn.microsoft.com/en-us/library/ie/dd329551.aspx) (这里是函数的文档:http://msdn.microsoft.com/en-us/library/cc680260.aspx

      此外,为了消除魔术字符串问题,您可以使用保存字符串的常量。这样可以轻松重构。

      【讨论】:

      • 这将有助于 ASP.NET MVC 应用程序,其中几乎每个路由都有一个路由名称。但是,我看不出这如何适用于 WCF 4.0 REST 服务,它也使用 UriTemplate 来进一步描述 URI。这里的想法是摆脱“魔术字符串”,以便对路由表或 UriTemplate 的更改不会使 SetLocationHeader 中编写的代码无效。
      • 那我一定错过了你的意思。我已经编辑了我的答案以包括魔术字符串问题。尽管如此,我还是无法摆脱这种思考过程有问题的感觉。如果答案不正确,请投反对票。
      • 今天早上我偶然发现了一种方法。看看我的回答是否有意义。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-09-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-04-22
      • 1970-01-01
      • 2016-02-08
      相关资源
      最近更新 更多