Search code examples
c#.netnested-generics

TypeLoadException thrown when dynamically instantiating an object with a nested type arg


Whilst contributing to an OSS project for work, I'm running into a TypeLoadException. I'm working on creating a seam whereby a developer could inject their own Repository class to remove a concrete dependency on EF so I can isolate my new code and run some tests.

It appears that running Activator.CreateInstance() against a type that has a nested type arg throws a wrench in creating it at runtime. I've used this pattern many times before, but this time the difference is that I'm using it to dynamically inject a generic Repository pattern implementation. The problem really seems linked to that type arg. I'm currently stumped, so any help would be greatly appreciated.

Here's the error I'm getting:

System.TypeLoadException: Could not load type 'Rock.Tests.Fakes.FakeRepository' from assembly 'Rock.Tests, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.
    at System.Reflection.RuntimeAssembly.GetType(RuntimeAssembly assembly, String name, Boolean throwOnError, Boolean ignoreCase, ObjectHandleOnStack type)
    at System.Reflection.RuntimeAssembly.GetType(String name, Boolean throwOnError, Boolean ignoreCase)
    at System.Activator.CreateInstance(String assemblyName, String typeName, Boolean ignoreCase, BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture, Object[] activationAttributes, Evidence securityInfo, ref StackCrawlMark stackMark)
    at System.Activator.CreateInstance(String assemblyName, String typeName)
    at Rock.Data.RepositoryFactory`1.FindRepository() in RepositoryFactory.cs: line 30
    at Rock.Data.Service`1..ctor() in Service.cs: line 34
    at Rock.Core.AttributeService..ctor()
    at Rock.Attribute.Helper.LoadAttributes(IHasAttributes item) in Helper.cs: line 165
    at Rock.Data.ModelWithAttributes`1.get_Attributes() in ModelWithAttributes.cs: line 38
    at Rock.CMS.Page.MapPagesRecursive(Page page) in Page.cs: line 422
    at Rock.CMS.Page.ExportObject() in Page.cs: line 410
    at Rock.Tests.CMS.PageTests.TheExportObjectMethod.ShouldCopyPropertiesOfDTO() in PageTests.cs: line 16

Here are some of the relevant (annotated) code snippets from the Rock.Data namespace:

IRepository.cs

public interface IRepository<T> where T : class
{
    // Very basic CRUD repository contract...
}

EFRepository.cs

public class EFRepository<T> where T : Model<T>
{
    // Concrete implementation of IRepository<T> specific to Entity Framework 4.3
}

Service.cs

public class Service<T> where T : Model<T>
{
    private readonly IRepository<T> _repository;

    // Inside this constructor are my changes...
    public Service() // : this(new EFRepository<T>())
    {
        // Instead of hard-coding the use of EFRepository here, I
        // thought it might be worthwhile to add a call out to a
        // factory method implementation.

        var factory = new RepositoryFactory<T>();
        _repository = factory.FindRepository();
    }

    // This constructor never really appears to be called.
    // From my test code's entry point, there's no way for me to
    // explicitly call this constructor, hence the factory implemenation.
    public Service(IRepository<T> repository)
    {
        _repository = repository;
    }
}

RepositoryFactory.cs

// Here's my quick/dirty factory method implementation to try to generically
// instantiate an IRepository of my choosing for testing purposes...
public class RepositoryFactory<T> where T : Model<T>
{
    public IRepository<T> FindRepository()
    {
        var repositoryTypeSetting = ConfiguraitonManager.AppSettings["RepositoryType"];

        if (string.IsNullOrEmpty(repositoryTypeSetting))
        {
            return new EFRepository<T>();
        }

        var settingArray = repositoryTypeSetting.Split(new[] { ',' });

        // I'm aware that Trim() is superfluous here, but this will be part of a development
        // framework, so I'm trying to take whitespace/developer error into account.
        var className = settingArray[0].Trim();
        var assemblyName = settingArray[1].Trim();

        // I've tried with and without Unwrap(), the exception originates from Activator.CreateInstance()
        return (IRepository<T>) Activator.CreateInstance(assemblyName, className).Unwrap();
    }
}

And here's some of the fake objects and test code snippets I'm using in my separate Rock.Tests project...

FakeRepository.cs

// Basic fake/stub implementation
public class FakeRepository<T> : IRepository<T> where T : Model<T>
{
    // Nothing here yet other than `throw new NotImplementedException()` for each method in the contract
    // We never make it here...
}

PageTests.cs

// Very basic XUnit test example...
public class PageTests
{
    public class TheExportMethod
    {
        [Fact]
        public void ShouldNotBeEmpty()
        {
            var page = new Page { Name = "FooPage" };
            var result = page.Export();
            Assert.NotEmpty(result);
        }
    }
}

App.config

<configuration>
    <appSettings>
        <clear/>
        <add key="RepositoryType" value="Rock.Tests.Fakes.FakeRepository,Rock.Tests"/>
    </appSettings>
</configuration>

Hopefully that's covers it pretty thoroughly. Thanks in advance!


Solution

  • It looks like you're trying to instantiate an open generic type, since your config file doesn't specify the generic type parameter for FakeRepository<T>. You'd need to do something more like:

    <add key="RepositoryType" value="Rock.Tests.Fakes.FakeRepository`1[[Some.ModelType,Some.ModelAssembly]],Rock.Tests"/>