0
votes

I have a simple question about dependecy registration.

I'm developing a brand new web application that use Engine Context paradigm with Autofac container. For any library on the solution I have one class implementing IDependencyRegistrar that implement a common Register method, due to add one the container some specific implementation of some interfaces and components.

In this way, a base Core library (running at application startup) provide a RegisterDependencies method that lookup on every Executing Assembly to discover all the DDL's used by the application and registering them on Autofac Container.

The code that provide this behavior is:

 builder = new ContainerBuilder();
        var drTypes = typeFinder.FindClassesOfType<IDependencyRegistrar>();
        var drInstances = new List<IDependencyRegistrar>();
        foreach (var drType in drTypes)
            drInstances.Add((IDependencyRegistrar) Activator.CreateInstance(drType));
        //sort
        drInstances = drInstances.AsQueryable().OrderBy(t => t.Order).ToList();
        foreach (var dependencyRegistrar in drInstances)
            dependencyRegistrar.Register(builder, typeFinder, config);
        builder.Update(container);

Where the FindClassOfType<IDependencyRegistrar> works thanks to a Method implementation like that:

        public virtual IList<Assembly> GetAssemblies()
    {
        var addedAssemblyNames = new List<string>();
        var assemblies = new List<Assembly>();

        if (LoadAppDomainAssemblies)
            AddAssembliesInAppDomain(addedAssemblyNames, assemblies);
        AddConfiguredAssemblies(addedAssemblyNames, assemblies);

        return assemblies;
    }

And, AddAssemblyInAppDomain is:

private void AddAssembliesInAppDomain(List<string> addedAssemblyNames, List<Assembly> assemblies)
    {
        foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
        {
            if (Matches(assembly.FullName))
            {
                if (!addedAssemblyNames.Contains(assembly.FullName))
                {
                    assemblies.Add(assembly);
                    addedAssemblyNames.Add(assembly.FullName);
                }
            }
        }
    }

The problem is: when I end up on adding in mysolution the MVC project (the front-end), I've referenced on it only direct accessing library (service layer and some infrastructure components) but no DataLayer components and some other DLL. Due to the fact that MVC not referencing directly some libraries of deep layers, my Engine Context doesn't see the others sub-components and not registering them on the Autofac container, causing a

'no registered services'

exception when execution make explicit request on them.

The whole system just works if I add reference to any library from the MVC project but, for layered architectured application, this is not a best practice: my MVC need to know nothing about DataLayer or others low-layered services.

However, in this way, no ExecutingAssembly are discovered, so, not dependency are registered anymore.

Wich is the best approch to resolve this situation without referencing all assemblies directly from main MVC project?

2
I've found that the problem may be caused by another issue. My DAL library was not referenced by Model library (a mix of model and service classes) but, this has configured as reverse dependency. My Model library has an DAL interface and the DAL Project reference the Model, implementing these interfaces. So, dependency are: MVC --> Model <-- DAL. I think that this is the problem: no ones directly referencing the DAL so, if I compile MVC, DAL library was not copied into BIN Dir. May be this the problem? If it is, what I can do to workaround it without change my dependency architecture? - Daniel Grandis
I had this exact same problem with autofac and a console app. With web projects it seems to be easier but non-web projects I couldn't find a nice way of getting all transitive dependencies to be detected and loaded. - IbrarMumtaz

2 Answers

0
votes

What you are trying to do is described in Autofac documentation as Assembly Scanning, take a look here. Basically, to get all assemblies in IIS-hosted application you need this piece of code:

var assemblies = BuildManager.GetReferencedAssemblies().Cast<Assembly>();

EDIT:

Ok, so I understand the situation is like this:

Project Web is a MVC web app.

Project Model is a class library where you have your contracts (interfaces) defined, e.g. for DAL, but also for Web.

Project DAL contains some implementations of contracts from Model.

There might be some additional class libraries, but they all uses Model for contracts.

So to sum up - all projects have reference to Model, but they have no references to each other.

I think for every library (except Model) you should create a module. To do so, create a class implementing Module type from Autofac library and override Load method - put all your module registration in there. Then, in Web app start you should load all assemblies and register their modules. But, as you mentioned, assemblies other than Web are not present in bin directory; you should copy them there "manually", for example in Post-Build action (Project Properties -> Build Events -> Post-Build action). The following command should do the work:

xcopy /Y "$(TargetDir)*.dll" "$(ProjectDir)..\{Your Web App}\bin"

Also, in your solution properties you should set, that Web project "depends" on all other projects. It would assure all other libraries would be build before Web. It does not add any reference between these assemblies.

Then, during application startup, you should search for you assemblies in bin folder and register each assembly module, like this:

var containerBuilder = new ContainerBuilder();
containerBuilder.RegisterControllers(typeof(MvcApplication).Assembly);

var libFolder = new DirectoryInfo(HostingEnvironment.MapPath("~/bin"));
var libFiles = libFolder.GetFiles("*.dll", SearchOption.AllDirectories);
foreach (var lib in libFiles)
{
     var asm = Assembly.LoadFrom(lib.FullName);
     containerBuilder.RegisterAssemblyModules(asm);
}

var container = containerBuilder.Build();

You might want to add some filter to libFolder.GetFiles() to retreive only your assemblies, not all from bin.

If your other assemblies contains Mvc Controllers, you should take a look how to manage the situation here (see Initializer class). Basically, in pre-start of application you would need to add assemblies to BuildManager. Otherwise, the code above should work just fine.

0
votes

If you are working on a non-web project then my answer might help?

To your Ioc class add a method i.e:

    public static void SetIocForTesting(bool forUnitTesting)
    {
        _testContext = forUnitTesting;
    }

Sample container set-up code, delegate out the responsibility of getting the assemblies to load into the builder. i.e GetModules():

    public static IContainer Container
    {
        get
        {
            if (_container != null)
            {
                return _container;
            }

            var builder = new ContainerBuilder();

            foreach (var lib in GetModules())
            {
                builder.RegisterAssemblyModules(lib);
            }

            _container = builder.Build();
            return _container;
        }
    }

When scanning for Assemblies, switch on the testContext variable:

    private static IEnumerable<Assembly> GetModules()
    {
        if (_testContext)
        {
            return AppDomain.CurrentDomain.GetAssemblies();
        }

        var currentPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
        if (currentPath == null)
        {
            throw new NullReferenceException("Unable to build the container because currentPath variable is null.");
        }
        // XXXX = assign a wild card
        var libFolder = new DirectoryInfo(currentPath);
        var libFiles = libFolder.GetFiles("XXXX.*.dll", SearchOption.TopDirectoryOnly);
        return libFiles.Select(lib => Assembly.LoadFrom(lib.FullName)).ToList();
    }

When unit testing your IoC provider and a registration:

    protected virtual void GivenThat()
    {
        IocProvider.SetIocForTesting(true);
    }

.. you have a method that switches the IoC to ensure it works correctly with all assemblies referenced and loaded by your test project. The above method lives inside an abstract base class I use for BDD style unit testing.

Your test project usually ends up referencing a lot of assemblies which means resolving services have a higher success rate.

Finally, for non UnitTesting code add a static constructor:

static IocProvider()
{
    _testContext = false;
}

This will ensure a default work flow for production code.

Feel free to play with the above format to suit your needs; I hope it helps someone in the way the above question and answer helped me.