5

I am using ILMerge and Quartz.NET in a C# .NET 4.0 Windows Service application. The app runs fine without using ILMerge, but now that we're nearing shipping release, I wanted to combine all DLLs into a single executable.

Problem is, that ILMerge seems to work fine, but when I run the combined executable, it throws this exception:

Unhandled Exception: Quartz.SchedulerException: ThreadPool type 'Quartz.Simpl.SimpleThreadPool' could not be instantiated. ---> System.InvalidCastException: Unable to cast object of type 'Quartz.Simpl.SimpleThreadPool' to type 'Quartz.Spi.IThreadPool'.
at Quartz.Util.ObjectUtils.InstantiateType[T](Type type) in :line 0
at Quartz.Impl.StdSchedulerFactory.Instantiate() in :line 0
--- End of inner exception stack trace ---
at Quartz.Impl.StdSchedulerFactory.Instantiate() in :line 0
at Quartz.Impl.StdSchedulerFactory.GetScheduler() in :line 0

Does anyone have any idea why this is? I have been wasting over 4 hours already and I can't figure it out. If I don't combine with ILMerge, then everything runs fine (with the Quartz.dll and Common.Logging.dll in the same directory).

I'm sure someone must have tried packaging Quartz.net up like this before, any ideas?

James Stone
  • 487
  • 7
  • 16
  • Is this the first time you tried to combine it with ILMerge? Or did it work prior to recent changes? – Ryan Gates May 01 '13 at 18:13
  • 1
    First time I tried to utilize ILMerge, ran it, didn't work anymore. Figured it must have been ILMerge, tried the internalize flag, didn't change anything. Remove ILMerge, compiled normally (like I used to before trying this out), all works (if DLLs are in same directory). – James Stone May 01 '13 at 18:23
  • One of the things that ILMerge doesn't handle is type loading from an external assembly (which might be the case based on a glancing over the stacktrace). Maybe look also into one of the alternatives found [here](http://chrisghardwick.blogspot.nl/2012/01/ilmerge-getting-started-merging-and.html) – rene May 01 '13 at 18:24
  • @rene would that mean I should try to load these external assemblies (not sure which) at startup manually? could that solve the issue? – James Stone May 01 '13 at 18:25
  • @rene what part of the stracktrace would tell you that an assembly is missing? It's complaining about an invalid cast? – James Stone May 01 '13 at 18:53
  • Don't use ilmerge. Instead use Jeffrey Richter's solution see http://blogs.msdn.com/b/microsoft_press/archive/2010/02/03/jeffrey-richter-excerpt-2-from-clr-via-c-third-edition.aspx – sgmoore May 04 '13 at 17:29
  • sometimes ILmerge doesn't work. You could try something like SmartAssembly, to see if you get better results. – Peter Ritchie May 04 '13 at 18:15

2 Answers2

1

You could try creating your own ISchedulerFactory and avoid using reflection to load all of your types. The StdSchedulerFactory uses this code to creat a threadpool. It's where your error is happening and would be the place to start looking at making changes:

        Type tpType = loadHelper.LoadType(cfg.GetStringProperty(PropertyThreadPoolType)) ?? typeof(SimpleThreadPool);

        try
        {
            tp = ObjectUtils.InstantiateType<IThreadPool>(tpType);
        }
        catch (Exception e)
        {
            initException = new SchedulerException("ThreadPool type '{0}' could not be instantiated.".FormatInvariant(tpType), e);
            throw initException;
        }

The ObjectUtils.InstantiateType method that is called is this one, and the last line is the one throwing your exception:

    public static T InstantiateType<T>(Type type)
    {
        if (type == null)
        {
            throw new ArgumentNullException("type", "Cannot instantiate null");
        }
        ConstructorInfo ci = type.GetConstructor(Type.EmptyTypes);
        if (ci == null)
        {
            throw new ArgumentException("Cannot instantiate type which has no empty constructor", type.Name);
        }
        return (T) ci.Invoke(new object[0]);
    }

Right after this section in the factory, datasources are loaded using the same pattern and then the jobs themselves are also loaded dynamically which means you'd also have to write your own JobFactory. Since Quartz.Net loads a bunch of bits and pieces dynamically at runtime going down this road means you might end up rewriting a fair amount of things.

jvilalta
  • 6,679
  • 1
  • 28
  • 36
1

Disclaimer: I don't know Quartz.NET at all, although I spent some time struggling with ILMerge. When I finally understood its limitations... I stopped using it.

ILMerge'd application tends to have problems with everything which contains the word "reflection". I can guess (I've never used Quartz.NET) that some classes are resolved using reflection and driven by configuration files.

Class is not only identified by its name (with namespace) but also by assembly it is coming from (unfortunatelly it doesn't get displayed in exception message). So, let's assume you had (before ILMerging) two assemblies A (for you Application) and Q (for Quartz.NET). Assembly 'A' was referencing assembly 'Q' and was using a class 'Q:QClass' which was implementing 'Q:QIntf'. After merging, those classes became 'A:QClass' and 'A:QIntf' (they were moved from assembly Q to A) and all the references in code has been replaced to use those (completely) new classes/interfaces, so "A:QClass" is implementing "A:QIntf" now. But, it did not change any config files/embedded strings which may still reference "Q:QClass".

So when application is reading those not-updated config files it still loads "Q:QClass" (why it CAN find it is a different question, maybe you left assembly 'Q' in current folder or maybe it is in GAC - see 1). Anyway, "Q:QClass" DOES NOT implement "A:QIntf", it still implements "Q:QIntf" even if they are binary identical - so you can't cast 'Q:QClass' to 'A:QIntf'.

The not-ideal-but-working solution is to "embed" assemblies instead of "merging" them. I wrote a open-source tool which does it (embedding instead of merging) but it is not related to this question. So if you decide to embed just ask me.

  1. You can test it by removing (hiding, whatever works for you) every single instance of Q.dll on your PC. If I'm right, the exception should say now 'FileNotFound'.
Milosz Krajewski
  • 1,160
  • 1
  • 12
  • 19
  • I ended up not using ILMERGE, it just wouldn't work and seemed like it required a lot more work than the benefit of having just a file files less in deployment runs. I also tried embedding, loading the assemblies dynamically, which helped me understand a lot more of the behind the scenes stuff, but it just wouldn't work for the hell of it. I loaded the assemblies at the earliest point, but had lots of static classes accessing them and they needed them to be re-loaded, but even static constructor was too late, strangely. always failed. your answer had most background, so thanks! – James Stone May 08 '13 at 11:04
  • What do you mean by 'loading the assemblies at the earliest point'? I have successfully used the method Jeffrey Richter described on his blog (see my earlier comment). It was very straight forward and the only to watch for is, you need to ensure that your AssemblyResolve event is initialized before anything else, which in my case meant creating a new entry point that setup the AssemblyResolve event and then called the old entry point. – sgmoore May 08 '13 at 12:43
  • @RomanMittermayr: I'm also puzzled "earliest" point. Although static constructor is quite early, the earlies point is module initializer. Unfortunately, module initializer cannot be altered with C# but can be altered with some minor IL manipulation (see: https://github.com/Fody/ModuleInit). When you say "reloaded" I have a feeling that Quartz.NET is creating separate AppDomains. That might be, in fact, a problem if you can't access them and inject you AssemblyResolver into them. You can try https://libz.codeplex.com/ as well. – Milosz Krajewski May 08 '13 at 13:20