1
\$\begingroup\$

I would like Autofac to instantiate components based on connection strings in the form of the URI:

autofac://assembly-name/full-class-name?param1=arg1&param2=arg2 

So the following test will work:

[TestClass]
public class Locator_Should
{
    [TestMethod]
    public void Resolve()
    {
        var builder = new ContainerBuilder();
        builder.RegisterType<DiskFolder>();
        var container = builder.Build();
        var folder = container.Resolve<IFolder>(
            "autofac://LocatorTest/LocatorTest.DiskFolder?path=\\Windows");

        Assert.IsTrue(folder.Any());
    }
}

Where:

public interface IFolder : IEnumerable<string>
{
}

public class DiskFolder : ReadOnlyCollection<string>, IFolder
{
    public DiskFolder(string path)
        : base(Directory.GetFiles(path))
    {
    }
}

The library code is:

public static class AutofacLocator
{
    public static T Resolve<T>(this ILifetimeScope scope, string uri) =>
        scope.Resolve<T>(new Uri(uri));

    public static object Resolve(this ILifetimeScope scope, string uri) =>
        scope.Resolve(new Uri(uri));

    public static T Resolve<T>(this ILifetimeScope scope, Uri uri) =>
        (T)scope.Resolve(uri);

    public static object Resolve(this ILifetimeScope scope, Uri uri)
    {
        if (uri.Scheme != "autofac")
            throw new NotSupportedException("Schema not supported.");

        var factory = (Delegate)scope.Resolve(GetFactory(uri));
        return factory.DynamicInvoke(GetArguments(uri));
    }

    static Type GetFactory(Uri uri)
    {
        Func<Type[], Type> getType = Expression.GetFuncType;
        var types = GetParameters(uri)
            .Select(p => p.ParameterType)
            .Append(GetReturnType(uri));

        return getType(types.ToArray());
    }

    static object[] GetArguments(Uri uri)
    {
        var arguments = uri.Query
            .TrimStart('?')
            .Split('&')
            .Select(p => p.Split('='))
            .ToDictionary(nv => nv[0], nv => Uri.UnescapeDataString(nv[1]));

        return GetParameters(uri)
            .Select(p => Convert.ChangeType(arguments[p.Name], p.ParameterType))
            .ToArray();
    }

    static ParameterInfo[] GetParameters(Uri uri) =>
        GetReturnType(uri)
            .GetConstructors()
            .First()
            .GetParameters();

    static Type GetReturnType(Uri uri) =>
        Type.GetType($"{uri.Segments[1]}, {uri.Host}");
}
\$\endgroup\$

2 Answers 2

3
\$\begingroup\$

It appears the goal here is to provide parameters to a resolve operation using a different format, something string-based. This appears to be primarily useful in a service location (manually executing Resolve) scenario rather than true DI, so I would caution you to be careful.

Since this started as an Autofac solution, I'll post an answer based on Autofac.

My first recommendation would be to implement a new Parameter type rather than entirely separate handling of strings. This would make the solution more interoperable with other parameter types and Autofac on the whole. You can see how other parameter types like ResolvedParameter and NamedParameter are implemented to give you examples.

My second recommendation would be to look at how Autofac.Configuration handles parsing the string values into objects. Convert.ChangeType doesn't work on a lot of things and having a more robust mechanism will be valuable.

Finally... if possible, I might consider looking at the design that requires this and see if it can be changed. As I mentioned, this appears to be primarily useful in service location and it'd be better if you can switch your solution to use actual dependency injection. That'd mean the parameters need to be available either during registration time (e.g., read them from config and include their values when registering things); or during runtime, in which case you'd likely resolve a Func<Xy, Y, B> or something similar in your consuming class and not use a lifetime scope. In either case, the utility of this mechanism once you switch to DI is much more limited. It could be that just using Autofac.Configuration will suffice.

\$\endgroup\$
1
\$\begingroup\$

GitHub and NuGet

The right way would be to do not depend on Autofac but support it through AutofacSeriveProvider as many other IoC containers. So now I have this for ioc://:

namespace System
{
    public static class ServiceLocator
    {
        public static T GetService<T>(this IServiceProvider provider, string uri) =>
            provider.GetService<T>(new Uri(uri));

        public static object GetService(this IServiceProvider provider, string uri) =>
            provider.GetService(new Uri(uri));

        public static T GetService<T>(this IServiceProvider provider, Uri uri) =>
            (T)provider.GetService(uri);

        public static object GetService(this IServiceProvider provider, Uri uri)
        {
            if (uri.Scheme != "ioc")
                throw new NotSupportedException("Schema not supported.");

            var factory = (Delegate)provider.GetService(GetFactory(uri));
            return factory.DynamicInvoke(GetArguments(uri));
        }

        static Type GetFactory(Uri uri)
        {
            Func<Type[], Type> getType = Expression.GetFuncType;
            var types = GetParameters(uri)
                .Select(p => p.ParameterType)
                .Append(GetReturnType(uri));

            return getType(types.ToArray());
        }

        static object[] GetArguments(Uri uri)
        {
            var arguments = uri.Query
                .TrimStart('?')
                .Split('&')
                .Select(p => p.Split('='))
                .ToDictionary(nv => nv[0], nv => Uri.UnescapeDataString(nv[1]));

            return GetParameters(uri)
                .Select(p => Convert.ChangeType(arguments[p.Name], p.ParameterType))
                .ToArray();
        }

        static ParameterInfo[] GetParameters(Uri uri) =>
            GetReturnType(uri)
                .GetConstructors()
                .First()
                .GetParameters();

        static Type GetReturnType(Uri uri) =>
            Type.GetType($"{uri.Segments[1]}, {uri.Host}");
    }
}

And the following for clr://:

namespace System
{
    public static class Activator<T>
    {
        public static T CreateInstance(string uri) =>
            CreateInstance(new Uri(uri));

        public static T CreateInstance(Uri uri) =>
            uri.Scheme != "clr"
            ? throw new NotSupportedException()
            : (T)Activator.CreateInstance(GetReturnType(uri), GetArguments(uri));

        static object[] GetArguments(Uri uri)
        {
            var arguments = uri.Query
                .TrimStart('?')
                .Split('&')
                .Select(p => p.Split('='))
                .ToDictionary(nv => nv[0], nv => Uri.UnescapeDataString(nv[1]));

            return GetParameters(uri)
                .Select(p => Convert.ChangeType(arguments[p.Name], p.ParameterType))
                .ToArray();
        }

        static ParameterInfo[] GetParameters(Uri uri) =>
            GetReturnType(uri)
                .GetConstructors()
                .First()
                .GetParameters();

        static Type GetReturnType(Uri uri) =>
            Type.GetType($"{uri.Segments[1]}, {uri.Host}");
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.