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¶m2=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}");
}
2 Answers 2
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.
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}");
}
}