UrlHelper injection #371

Open
rlightner opened this Issue Feb 2, 2017 · 7 comments

Projects

None yet

2 participants

@rlightner

I've searched but come up with nothing. Is there a way to Inject UrlHelper into some service classes I'm writing like AutoFac does?

@dotnetjunkie
Collaborator

You need to provide us with more information about what it is you need. What do you want to achieve, and why doesn't this work? What have you tried, why doesn't that work?

@rlightner

Ya, my bad.
This is what I'm currently doing which works:
container.Register<UrlHelper>(()=> new UrlHelper(HttpContext.Current.Request.RequestContext), Lifestyle.Scoped);

But I wonder if I'm doing it the correct way.

@dotnetjunkie
Collaborator

I would say this is not the correct way, because your object composition depends on runtime data (e.g. the availability of HttpContext.Current). This article goes into great details why this is a bad idea. One result of this is that it will be impossible for you to verify the container's configuration using container.Verify().

Besides that, it's always good to hide framework components, like UrlHelper, behind application-specific abstractions (as the Dependency Inversion Principle teaches us). Such application-specific abstraction can be tailored to your code's needs and narrow. Doing so allows you to easily replace the MVC specific implementation with a fake one when doing unit or integration testing.

For instance, such abstraction might look like this:

public interface IUrlProvider
{
    string UrlFor<TController>(Expression<Action<TController>> actionSelector) 
        where TController : Controller;
}

Now you can create an adapter for MVC that uses RouteTable and HttpContext.Current.Request.RequestContext under the covers, while for unit testing, you can have a simple implementation that uses some simple logic that verifies whether the supplied expression is sane.

This way you can simply register your MvcUrlProvider as follows:

container.RegisterSingleton<IUrlProvider>(new MvcUrlProvider());

And use it as follows:

var shipOrderUrl = this.urlProvider.UrlFor<ShipOrderController>(c => c.ShipOrder(orderId));

The shape of this abstraction might vary, based upon your needs. This is obviously just an example.

@rlightner

Thanks for the detailed response. I'll read the included links and see where that gets me. I'm trying to use the T4MVC for url templating in some class modules that aren't privy to the current Context which is why this has become an issue.

@dotnetjunkie
Collaborator

I stopped using T4MVC for a long time ago. In my last MVC project (some years back) we actually used this IUrlProvider abstraction ourselves.

@rlightner

I'd love to walk away from it as well. I'd love any links you have pointing me towards what you might have implemented!

@dotnetjunkie
Collaborator

Well, this was a long time ago. I don't recall whether we'd had anything published online, and I don't recall the exact implementation. It might have looks a bit like this though:

// Use this as real implementation
public class MvcUrlProvider : BaseUrlProvider, IUrlProvider
{
    protected override string GetVirtualPath(RouteValueDictionary route) =>
        RouteTable.Routes.GetVirtualPath(HttpContext.Current.Request.RequestContext, route).VirtualPath;
}

// Use this as stub inside your tests
public class StubUrlProvider : BaseUrlProvider, IUrlProvider
{
    protected override string GetVirtualPath(RouteValueDictionary route) => "/Test/Value";
}

// Abstract class to ensure that invalid ForUrl requests will fail during unit tests as well.
public abstract class BaseUrlProvider
{
    public string UrlFor<TController>(Expression<Action<TController>> actionSelector) where TController : Controller =>
        GetVirtualPath<TController>(GetActionMethod(actionSelector), (MethodCallExpression)actionSelector.Body);

    protected abstract string GetVirtualPath(RouteValueDictionary route);

    private MethodInfo GetActionMethod<T>(Expression<Action<T>> actionSelector) =>
        actionSelector.Body is MethodCallExpression
            ? GetActionMethod(actionSelector.Body as MethodCallExpression)
            : ThrowExpressionNotSupported(actionSelector);

    private static MethodInfo GetActionMethod<T, TActionResult>(Expression<Func<T, TActionResult>> selector) =>
        selector.Body is MethodCallExpression
            ? GetActionMethod(selector.Body as MethodCallExpression)
            : ThrowExpressionNotSupported(selector);

    private static MethodInfo GetActionMethod(MethodCallExpression actionSelector)
    {
        return !actionSelector.Method.IsConstructor && actionSelector.Method.IsPublic
            ? actionSelector.Method
            : ThrowExpressionNotSupported(actionSelector);
    }

    private string GetVirtualPath<T>(MethodInfo actionMethod, MethodCallExpression expression) =>
        GetVirtualPath(BuildRouteValueDictionary<T>(actionMethod, expression));

    private static RouteValueDictionary BuildRouteValueDictionary<TController>(
        MethodInfo actionMethod, MethodCallExpression expression)
    {
        var route = new RouteValueDictionary(new
        {
            Controller = typeof(TController).Name.Replace(typeof(Controller).Name, string.Empty),
            Action = actionMethod.Name,
        });

        IncludeParameters(expression, route);
        return route;
    }

    public static RouteValueDictionary GetRouteValues<T>(Expression<Action<T>> actionSelector)
    {
        var expression = (MethodCallExpression)actionSelector.Body;
        var method = expression.Method;

        if (!method.IsConstructor && method.IsPublic)
        {
            var routes = new RouteValueDictionary(new
            {
                Controller = typeof(T).Name.Replace(typeof(Controller).Name, string.Empty),
                Action = method.Name,
            });

            IncludeParameters(expression, routes);

            return routes;
        }

        throw new ArgumentException($"The expression '{expression}' is not supported. ");
    }

    private static void IncludeParameters(MethodCallExpression expression, RouteValueDictionary routes)
    {
        foreach (var pair in expression.Arguments.Zip(expression.Method.GetParameters(), 
            (arg, param) => new { arg, param }))
        {
            object value = GetValue(pair.arg);
            string parameterName = GetMethodParameterName(pair.param);
            IncludeParameter(value, parameterName, routes);
        }
    }

    private static void IncludeParameter(object value, string parameterName, RouteValueDictionary routes)
    {
        if (value == null)
        {
            return;
        }

        Type instanceType = value.GetType();

        if (instanceType == typeof(string) || instanceType.IsValueType)
        {
            IncludePrimitiveParameter(value, parameterName, routes);
        }
        else if (typeof(IEnumerable).IsAssignableFrom(instanceType))
        {
            IncludeCollectionParameter(value, parameterName, routes);
        }
        else
        {
            IncludeComplexObjectParameter(value, parameterName, routes, instanceType);
        }
    }

    private static void IncludePrimitiveParameter(object value, string parameterName, RouteValueDictionary routes) =>
        routes.Add(parameterName, value);

    private static void IncludeCollectionParameter(object value, string parameterName, RouteValueDictionary routes)
    {
        var parameters = (IEnumerable)value;

        int index = 0;

        foreach (object parameter in parameters)
        {
            routes.Add($"{parameterName}[{index++}]", parameter);
        }
    }

    private static void IncludeComplexObjectParameter(object value, string parameterName,
        RouteValueDictionary routes, Type instanceType)
    {
        foreach (var property in instanceType.GetProperties())
        {
            object propertyValue = property.GetValue(value, null);

            IncludeParameter(propertyValue, parameterName + "." + property.Name, routes);
        }
    }

    private static string GetMethodParameterName(ParameterInfo methodParameter)
    {
        var bindAttributes = methodParameter.GetCustomAttributes<BindAttribute>();

        return bindAttributes.Any()
            ? bindAttributes.First().Prefix
            : methodParameter.Name;
    }

    private static object GetValue(Expression argumentExpression) =>
        argumentExpression is ConstantExpression
            ? ((ConstantExpression)argumentExpression).Value
            : Expression.Lambda(argumentExpression).Compile().DynamicInvoke();

    private static MethodInfo ThrowExpressionNotSupported(LambdaExpression actionSelector)
    {
        throw new ArgumentException(
            $"Expression {actionSelector} has body of type {actionSelector.Body.GetType().Name}, which is not supported.");
    }

    private static MethodInfo ThrowExpressionNotSupported(MethodCallExpression actionSelector)
    {
        throw new ArgumentException($"The expression '{actionSelector}' is not supported. ");
    }
}

I'm not sure, but this might work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment