This project is read-only.

Generic repository implementation registration for multiple types

Aug 25, 2012 at 5:34 PM
Edited Aug 25, 2012 at 5:34 PM

I have the following interface definition

public interface IContentRepository<T>

then an interface implementation (provider):

public sealed class JsonContentProvider<T> : IContentRepository<T> where T : Base

 I need to register JsonContentProvider<T> for interface IContentRepository<T> for all types that inherit from Base. There are more than a dozens of such types.

This is a bit different from multiple registration samples where you have multiple provider implementing one interface. My example has only one provider implementation but for generic type.

Is there a shortcut I can use rather than wiring up each and every one separately?

 

 

 


Coordinator
Aug 25, 2012 at 6:53 PM
Edited Aug 25, 2012 at 6:55 PM

There absolutely is. You can simply use the following registration:

container.RegisterOpenGeneric(
    typeof(IContentRepository<>),
    typeof(JsonContentProvider<>));

This will map every closed generic version of IContentRepository<T> to the applicable JsonContentProvider<T> implementation (using unregistered type resolution), if the T matches all the type constraints. This means that if a IContentRepository<T> is requested that is not explicitly registered by the container, and that T matches the type constraints of JsonContentProvider<T> (in your case the where T : base constraint), a new instance of that JsonContentProvider<T> will be returned. However, when there is an explicit registration, that registration will be used. This allows you to mix these open generic registrations with normal or batch registrations. For instance, you can have a default registration for most types, but have a few specific registrations that you explicitly registrer. For instance:

container.Register<IContentRepository<Person>, XmlContentRepository<Person>>();

Or perhaps you defined a set of non-generic classes that implement IContentRepository<T>, for instance:

public class PersonContentRepository  : IContentRepository<Person>
{
    // implement IContentRepository<Person> here
}

With the RegisterManyForOpenGeneric extension method can find all those classes in your application and register them at once with the following line of code:

container.RegisterManyForOpenGeneric(
    typeof(IContentRepository<>),
    AppDomain.CurrentDomain.GetAssemblies());

RegisterManyForOpenGeneric simply calls container.Register<TService, TImplementation>() under the covers and the behavior is the same as registering them manually. Since they are registered explicitly, the container will rather return them instead of a JsonContentProvider<T>.

Since the RegisterOpenGeneric extension method takes generic type constraints into consideration, you can use this as a form of conditional registration. For instance:

public interface IValidator<T> { }

public class ClassValidator<T> : IValidator<T> where T : class

public class EnumValidator<T> : IValidator<T> where T : struct

container.RegisterOpenGeneric(
    typeof(IValidator<>),
    typeof(ClassValidator<>));

container.RegisterOpenGeneric(
    typeof(IValidator<>),
    typeof(EnumValidator<>));

// resolves a ClassValidator<object>
container.GetInstance<IValidator<object>>();

// resolves a EnumValidator<int>
container.GetInstance<IValidator<int>>();

Isn't that cool?

The following Wiki pages give more information about this:

Marked as answer by dot_NET_Junkie on 11/4/2013 at 2:00 AM
Aug 25, 2012 at 8:02 PM
Edited Aug 25, 2012 at 8:03 PM

Yeah I was looking at the Batch registration docs but didn't find the explanation for RegisterOpenGeneric (besides what's in the Composites section).

Thank you for such informative answers, this is awesome.;)

However, I must have done something wrong, because it's not working for me. The app stops with an exception at

container.Verify();

and it only says "Null Reference Exception: Object reference not set to an instance of an object":

This is from the stack trace:

[NullReferenceException: Object reference not set to an instance of an object.]
   SimpleInjector.Extensions.TransientOpenGenericResolver.Register(Type closedGenericImplementation, UnregisteredTypeEventArgs e) +18
   SimpleInjector.Extensions.OpenGenericResolver.ResolveOpenGeneric(Object sender, UnregisteredTypeEventArgs e) +93
   System.EventHandler`1.Invoke(Object sender, TEventArgs e) +0
   SimpleInjector.Container.BuildInstanceProducerThroughUnregisteredTypeResolution(Type serviceType) +44
   SimpleInjector.Container.BuildInstanceProducerForType(Type serviceType, Func`1 buildInstanceProducerForConcreteType) +13
   SimpleInjector.Container.BuildInstanceProducerForType(Type serviceType) +77
   SimpleInjector.<>c__DisplayClass13.b__12() +13
   SimpleInjector.Container.GetInstanceProducerForType(Type serviceType, Func`1 buildInstanceProducer) +55
   SimpleInjector.Container.GetRegistrationEvenIfInvalid(Type serviceType) +77
   SimpleInjector.Advanced.DefaultConstructorInjectionBehavior.BuildParameterExpression(ParameterInfo parameter) +47
   SimpleInjector.InstanceProducers.<>c__DisplayClass1.b__0(ParameterInfo parameter) +11
   System.Linq.WhereSelectArrayIterator`2.MoveNext() +66
   System.Linq.Buffer`1..ctor(IEnumerable`1 source) +216
   System.Linq.Enumerable.ToArray(IEnumerable`1 source) +77

And this is how my registrations look like:

		private static void InitializeContainer(Container container) {
			string dataProvider = ConfigurationManager.AppSettings["DataProvider"];

			// custom when condition
			//Func<IRequest, bool> adminAreaRequest = new Func<IRequest, bool>(r => r.Target.Member.ReflectedType.FullName.Contains("Areas.Admin"));

			if (!string.IsNullOrWhiteSpace(dataProvider)) {
				switch (dataProvider) {
					case "xml":
						#region XML provider bindings

						// 1) entities repositories
						container.RegisterOpenGeneric(typeof(IContentRepository<>), typeof(XmlContentProvider<>));

						// 2) settings and states repositories
						container.Register<ISettingsRepository, XmlSettingsProvider>();
						container.Register<ICulturesRepository, XmlCulturesProvider>();
						container.Register<IWidgetFrameworkRepository, WidgetFrameworkProvider>();
						container.Register<IContentItemsSearch, XmlContentItemsSearch>();

						// 3) various
						container.RegisterOpenGeneric(typeof(ICollectionsRepository<,>), typeof(XmlCollectionsProvider<,>));
						container.RegisterOpenGeneric(typeof(ICategorizationRepository<>), typeof(XmlCategorizationProvider<>));
						container.Register<IContentManager, XmlContentManager>();
						#endregion

						break;

					default:
						#region JSON provider bindings

						// 1) entities repositories
						container.RegisterOpenGeneric(typeof(IContentRepository<>), typeof(JsonContentProvider<>));

						// 2) settings and states repositories
						container.Register<ISettingsRepository, JsonSettingsProvider>();
						container.Register<ICulturesRepository>(() => { return new JsonCulturesProvider(PendingInitializer.JsonDataStorePhysicalPath); });
						//container.Register<IWidgetFrameworkRepository, WidgetFrameworkProvider>();
						container.Register<IContentItemsSearch, JsonContentItemsSearch>();

						// 3) various
						container.RegisterOpenGeneric(typeof(ICollectionsRepository<,>), typeof(JsonCollectionsProvider<,>));
						container.RegisterOpenGeneric(typeof(ICategorizationRepository<>), typeof(JsonCategorizationProvider<>));
						container.Register<IContentManager, JsonContentManager>();

						#endregion

						break;
				}
			}
		}

 

I am not using EF so it doesn't have anything to do with that, I guess. What could be wrong in there (or in my repositories)?

Coordinator
Aug 25, 2012 at 8:25 PM

It's very unfortunate that the framework is throwing a NullReferenceException. That is actually a bug, since it should be throwing a descriptive exception. I will try to fix this soon.

There seems to be a problem while resolving one of the openGenericImplementations that you registered using the RegisterOpenGeneric method. What you can try to do is resolving them directly. For instance:

container.GetInstance<XmlContentProvider<SomeType>>();
container.GetInstance<XmlCollectionsProvider<SomeType, SomethingElse>>();
container.GetInstance<XmlCategorizationProvider<SomeType>>();
container.GetInstance<JsonContentProvider<SomeType>>();
container.GetInstance<JsonCollectionsProvider<SomeType, SomethingElse>>();
container.GetInstance<JsonContentProvider<JsonCategorizationProvider>>();

Replace the SomeType and SomethingElse with actual types that could be resolved by your application, and call these lines after the registration. One of them will probably fail with an exception, and thise will probably the reason why the RegisterOpenGeneric registration is failing.

Again, the reason you don't see what's wrong is because of a bug in the framework. I'll fix this soon.

Aug 25, 2012 at 9:01 PM
Edited Aug 25, 2012 at 9:01 PM

I figured it out. It fails because my repository implementation has 2 constructors. I did check the other questions&answers here, specifically the "default constructor" one about multiple constructor being an antipattern.

Now the dependency for those constructor in my case is not some external service but a simple string variable (as a constructor parameter). Now I apologize if this is going too far in describing my code and design decisions but I think it is a valid discussion, that's why I will show how the JsonContentProvider constructor look like, for you to comment (maybe):

		private readonly string _dataStoreFolderPath, _contentFolderPath, _dataFilePath, _fileName;

		public JsonContentProvider() {
			_dataStoreFolderPath = JsonProvider.JsonDataStorePhysicalPath;
			_contentFolderPath = Path.Combine(_dataStoreFolderPath, typeof(T).Name);

			if (!Directory.Exists(_contentFolderPath))
				Directory.CreateDirectory(_contentFolderPath);
		}

		public JsonContentProvider(string dataStoreFolderPath) {
			_dataStoreFolderPath = dataStoreFolderPath;
			_contentFolderPath = Path.Combine(_dataStoreFolderPath, typeof(T).Name);

			if (!Directory.Exists(_contentFolderPath))
				Directory.CreateDirectory(_contentFolderPath);
		}

 

As you can see I have this JsonProvider static class somewhere which provides the root data folder if it wasn't provided via the constructor. Now my question is this: wouldn't it be an overkill to remove the default parameterless constructor and leave just the one with string parameter and the provide that string parameter via the container registration?


Coordinator
Aug 25, 2012 at 9:17 PM

Removing one of the constructors (and removing ambiguity) would definitely be a good thing, especially if you don't need it. Still (without doing any special registrations) you can't resolve a type that has a constructor argument of type string, since string by itself is a ambiguous dependency: string is not a service.

Under 'normal' conditions I would probably advice to use a factory delegate to register this type, such as:

container.Register<IContentProvider>(() => new JsonContentProvider("path"));

However, this doesn't apply in your situation, since you are registering this as an open generic type. There is no RegisterOpenGeneric extension method overload that accepts a Func<T> delegate, simply because RegisterOpenGeneric is not a generic method, because it has to take Type arguments.

Although it is possible to change the default constructor injection behavior of the container, and allow it injecting a string in every JsonContextProvider<T> it creates, I wouldn't walk this path (yet). This would be too much magic. Instead, there is a simple solution to this problem. Create a derived type of JsonContextProvider<T>, let it have a single constructor, and place it inside your composition root. You can than wire this type instead of the JsonContextProvider<T> itself:

public class SingleCtorJsonContextProvider<T> : JsonContextProvider<T>
{
    public SingleCtorJsonContextProvider() : base("somePath") { }
}

container.RegisterOpenGeneric(typeof(IContextProvider<>), typeof(SingleCtorJsonContextProvider));

Aug 26, 2012 at 9:44 AM

To be completely fair, I have to admit I'm having second thoughts on switching to SI. I did want to switch initially mainly because of the performance (and Ninject's performance is really terrible) but this seems like a too far stretch, especially this last solution with introducing another class to cover the requirements for having only one controller.

Another thing is, this inheritance assumes I can provide the "somePath" in the derived constructor, however, the path should be provided by the container with constructor argument registration (because I initialize the paths in there - they are read from the configuration settings in Web.Config, constructed together and provided to the repositories).

I did see in another thread there is support for constructor parameters (http://simpleinjector.codeplex.com/discussions/390508) but as you said it doesn't expand to the RegisterOpenGeneric() method.

If you explain whether you plan to expand on the whole constructor support "thing" in the future then I might temporarily go with your solution and later change to something cleaner (though I am not sure if it will work in the first place).

Thank you so far for all the explanation provided!

Coordinator
Aug 26, 2012 at 6:12 PM

The features of the Simple Injector are a bit limited (it is called ‘Simple’ for a reason). However, I think it’s feature set will be sufficient in most cases. I found out that most of the time I thought I needed some complex registration that was not supported, the problem was in my design. Either is was a violation of one of the SOLID principles, or a violation of a DI best practice.

When I look at the JsonContentProvider<T> there are two possible things in your design that might cause this friction. I notice that you check the existence of some directory and create it if it doesn't. There are two problems with this. First of all, you might be violating the Single Responsiblity Principle (SRP) here, since this class probably now has two responsibilities: the responsibility of providing Json content, and the responsibility of managing directories (and possibly writing to disk). Or let me put it in another perspective. If I understand correctly, the JsonContentProvider seems to be interested in caching the content somewhere. Does it care whether this content is cached on disk, memory, or somewhere else? Should it care? Probably not. In that case, we're missing an abstraction here. Besides, how would you be unit testing the JsonContentProvider, when it writes to disk?

The other issue is that your constructor does too much. That's the 4th law of IoC . Constructors of services (that are wired up by the container) should do nothing except storing the dependencies it is given. You should not have any logic in your constructor, since they are just meant for building up the object graph and everything else slows building the object graph down and might cause strange side effects (remember that building the object graph is done at another moment in time, than using it).

A solution can be to first of all move this logic out of the constructor. The SRP states that a class should have a single responsibility, which means that this logic should be moved to another class. In that case, the JsonContentProvider<T> can take a dependency on a new abstraction:

public interface IContentCache
{
    string this[Type type] { get; set; }
}

The JsonContentProvider will just be storing and retrieving content from the case, and will not be bothered with the existing of that folder on disk (and even that it writes data to disk). An implementation of the IContentCache might look like this:

public class DiskContentCache : IContentCache
{
    private readonly string dataStoreFolderPath;

    public DiskContentCache(string dataStoreFolderPath)
    {
        this.dataStoreFolderPath = dataStoreFolderPath;
    }

    public string this[Type type]
    {
        get
        {
            string file = GetPath(type) + "\\content.txt";
        
            if (File.Exists(file))
            {
                return File.ReadText(file);
            }
            
            return null;
        }
        
        set
        {
            string path = GetPath(type);
            
            if (!Directory.Exists(path))
            {
                Directory.CreateDirectory(path);
            }
            
            lock (this)
            {
                File.WriteText(path + "\\content.txt", value);
            }
        }
    }
    
    private string GetPath(Type type)
    {
        return Path.Combine(this.dataStoreFolderPath, type.Name);
    }
}

Now your JsonContentProvider<T> can simply depend on the non-generic IContentCache and call it as follows:

string content = this.contentCache[typeof(T))];

if (content == null)
{
    this.contentCache[typeof(T)] = content = BuildContent();
}

The IContentCache can be registered as follows:

container.RegisterSingle<IContentCache>(new DiskContentCache(
    ConfigurationManager.AppSettings["dataStoreFolderPath"]));

 
Of course I'm just guessing about what your code does so my examples could be unusable to you, but perhaps these tips could help you.

When you map an open generic interface to an open generic implementation, the container must do the creation of this type. If you were allowed to pass in a delegate, you’d be blessed with creating that type yourself using reflection (which is pretty nasty when it comes to creating generic types), which would be an awful experience and pretty slow. By letting the container do this, we ensure a good user experience and optimal performance. One of the 'limitations' of the Simple Injector is that there is no built-in way to override a constructor argument. This forces you to have a clean application design. This can be quite annoying of course when trying to use Simple Injector on an existing application :-) There are ways btw to extend the Simple Injector to allow overriding constructor argument. You can read this in this blog post. However, even if you feel that you’re not violating any SOLID principle, you can probably still break up that class and move logic to a non-generic dependency. Because it is possible to create non-generic classes manually (as I shown above) you can solve this problem elegantly, without any customizations to the container.

I hope this helps.