Как установить заголовок местоположения для UriTemplate другого сервиса в WCF 4.0 REST без магических строк? - PullRequest
4 голосов
/ 16 февраля 2012

Рассмотрим следующие две службы 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 будет неверным.Если бы я изменил UriTemplate на DocumentService.GetDocument, то полученный 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, для возврата Uri для операции GetDocument DocumentService?

Я использую простой старый WCF 4.0 (не веб-API WCF).

Ответы [ 2 ]

1 голос
/ 24 февраля 2012

Случайно я обнаружил статью , написанную Хосе Ф. Романиелло, в которой показано, как это сделать для веб-API WCF, и адаптировал ее.Исходный код находится в конце ответа.

Предполагая, что у меня есть четыре службы, регистрация маршрутизации изменяется, чтобы использовать подкласс 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);
}

Этот же фрагмент кода можно использовать для установки 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;
}

При таком подходе нет волшебных строк, и маловероятно, что изменение имени метода, имени службы, префикса таблицы маршрутизации нарушит работу системы.

Вот реализация, адаптированная из статьи :

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));
    }
}

Конструктор по умолчанию ResourceLinker требует некоторых изменений для подбора базового URI веб-приложения, принимаяс учетом того, что HTTPS может быть прекращен на балансировщике нагрузки.Это выходит за рамки этого ответа.

1 голос
/ 23 февраля 2012

Вы используете это:

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

(вот углубленная статья о маршрутизации asp.net вне mvc http://msdn.microsoft.com/en-us/library/ie/dd329551.aspx) (и вот документация для функции: http://msdn.microsoft.com/en-us/library/cc680260.aspx)

Кроме того, чтобы устранить проблему магической строки, вы можете использовать константы, которые содержат строки. Это позволяет легко выполнять рефакторинг.

...