Quino 2: Starting up an application, in detail

Published by Marco on

Updated by Marco on

As part of the final release process for Quino 2, we’ve upgraded 5 solutions[1] from Quino 1.13 to the latest API in order to shake out any remaining API inconsistencies or even just inelegant or clumsy calls or constructs. A lot of questions came up during these conversions, so I wrote the following blog to provide detail on the exact workings and execution order of a Quino application.

I’ve discussed the design of Quino’s configuration before, most recently in API Design: Running an Application (Part I) and API Design: To Generic or not Generic? (Part II) as well as the three-part series that starts with Encodo’s configuration library for Quino: part I.

Quino Execution Stages

The life-cycle of a Quino 2.0 application breaks down into roughly the following stages:

  1. Build Application: Register services with the IOC, add objects needed during configuration and add actions to the startup and shutdown lists
  2. Load User Configuration: Use non-IOC objects to bootstrap configuration from the command line and configuration files; IOC is initialized and can no longer be modified after action ServicesInitialized
  3. Apply Application Configuration: Apply code-based configuration to IOC objects; ends with the ServicesConfigured action
  4. Execute: execute the loop, event-handler, etc.
  5. Shut Down: dispose of the application, shutting down services in the IOC, setting the exit code, etc.

Stage 1

The first stage is all about putting the application together with calls to Use various services and features. This stage is covered in detail in three parts, starting with Encodo’s configuration library for Quino: part I.

Stage 2

Let’s tackle this one last because it requires a bit more explanation.

Stage 3

Technically, an application can add code to this stage by adding an IApplicationAction before the ServicesConfigured action. Use the Configure<TService>() extension method in stage 1 to configure individual services, as shown below.

application.Configure<IFileLogSettings>(
  s => s.Behavior = FileLogBehavior.MultipleFiles
);

Stage 4

The execution stage is application-specific. This stage can be short or long, depending on what your application does.

For desktop applications or single-user utilities, stage 4 is executed in application code, as shown below, in the Run method, which called by the ApplicationManager after the application has started.

var transcript = new ApplicationManager().Run(CreateApplication, Run);

IApplication CreateApplication() { … }
void Run(IApplication application) { … }

If your application is a service, like a daemon or a web server or whatever, then you’ll want to execute stages 1–3 and then let the framework send requests to your application’s running services. When the framework sends the termination signal, execute stage 5 by disposing of the application. Instead of calling Run, you’ll call CreateAndStartupUp.

var application = new ApplicationManager().CreateAndStartUp(CreateApplication);

IApplication CreateApplication() { … }

Stage 5

Every application has certain tasks to execute during shutdown. For example, an application will want to close down any open connections to external resources, close file (especially log files) and perhaps inform the user of shutdown.

Instead of exposing a specific “shutdown” method, a Quino 2.0 application can simply be disposed to shut it down.

If you use ApplicationManager.Run() as shown above, then you’re already sorted—the application will be disposed and the user will be informed in case of catastrophic failure; otherwise, you can shut down and get the final application transcript from the disposed object.

application.Dispose();
var transcript = application.GetTranscript();
// Do something with the transcript…

Stage 2 Redux

We’re finally ready to discuss stage 2 in detail.

An IOC has two phases: in the first phase, the application registers services with the IOC; in the second phase, the application uses services from the IOC.

An application should use the IOC as much as possible, so Quino keeps stage 2 as short as possible. Because it can’t use the IOC during the registration phase, code that runs in this stage shares objects via a poor-man’s IOC built into the IApplication that allows modification and only supports singletons. Luckily, very little end-developer application code will ever need to run in this stage. It’s nevertheless interesting to know how it works.

Obviously, any code in this stage that uses the IOC will cause it to switch from phase one to phase two and subsequent attempts to register services will fail. Therefore, while application code in stage 2 has to be careful, you don’t have to worry about not knowing you’ve screwed up.

Why would we have this stage? Some advocates of using an IOC claim that everything should be configured in code. However, it’s not uncommon for applications to want to run very differently based on command-line or other configuration parameters. The Quino startup handles this by placing the following actions in stage 2:

  • Parse and apply command-line
  • Import and apply external configuration (e.g. from file)

An application is free to insert more actions before the ServicesInitialized action, but they have to play by the rules outlined above.

“Single” objects

Code in stage 2 shares objects by calling SetSingle() and GetSingle(). There are only a few objects that fall into this category.

The calls UseCore() and UseApplication() register most of the standard objects used in stage 2. Actually, while they’re mostly used during stage 2, some of them are also added to the poor man’s IOC in case of catastrophic failure, in which case the IOC cannot be assumed to be available. A good example is the IApplicationCrashReporter.

Executing Stages

Before listing all of the objects, let’s take a rough look at how a standard application is started. The following steps outline what we consider to be a good minimum level of support for any application. Of course, the Quino configuration is modular, so you can take as much or as little as you like, but while you can use a naked Application—which has absolutely nothing registered—and you can call UseCore() to have a bit more—it registers a handful of low-level services but no actions—we recommend calling at least UseApplication() to adds most of the functionality outlined below.

  1. Create application: This involves creating the IOC and most of the IOC registration as well as adding most of the application startup actions (stage 1)
  2. Set debug mode: Get the final value of RunMode from the IRunSettings to determine if the application should catch all exceptions or let them go to the debugger. This involves getting the IRunSettings from the application and getting the final value using the IApplicationManagerPreRunFinalizer. This is commonly an implementation that can allows setting the value of RunMode from the command-line in debug builds. This further depends on the ICommandSetManager (which depends on the IValueTools) and possibly the ICommandLineSettings (to set the CommandLineConfigurationFilename if it was set by the user).
  3. Process command line: Set the ICommandProcessingResult, possibly setting other values and adding other configuration steps to the list of startup actions (e.g. many command-line options are switches that are handled by calling Configure<TSettings>() where TSettings is the configuration object in the IOC to modify).
  4. Read configuration file: Load the configuration data into the IConfigurationDataSettings, involving the ILocationManager to find configuration files and the ITextValueNodeReader to read them.
  5. The ILogger is used throughout by various actions to log application behavior
  6. If there is an unhandled error, the IApplicationCrashReporter uses the IFeedback or the ILogger to notify the user and log the error
  7. The IInMemoryLogger is used to include all in-memory messages in the IApplicationTranscript

The next section provides detail to each of the individual objects referenced in the workflow above.

Available Objects

You can get any one of these objects from the IApplication in at least two ways, either by using GetSingle<TService>() (safe in all situations) or GetInstance<TService>() (safe only in stage 3 or later) or there’s almost always a method which starts with “Use” and ends in the service name.

The example below shows how to get the ICommandSetManager[2] if you need it.

application.GetCommandSetManager();
application.GetSingle<ICommandSetManager>(); // Prefer the one above
application.GetInstance<ICommandSetManager>();

All three calls return the exact same object, though. The first two from the poor-man’s IOC; the last from the real IOC.

Only applications that need access to low-level objects or need to mess around in stage 2 need to know which objects are available where and when. Most applications don’t care and will just always use GetInstance().

The objects in the poor-man’s IOC are listed below.

Core

  • IValueTools: converts values; used by the command-line parser, mostly to translate enumerate values and flags
  • ILocationManager: an object that manages aliases for file-system locations, like “Configuration”, from which configuration files should be loaded or “UserConfiguration” where user-specific overlay configuration files are stored; used by the configuration loader
  • ILogger: a reference to the main logger for the application
  • IInMemoryLogger: a reference to an in-memory message store for the logger (used by the ApplicationManager to retrieve the message log from a crashed application)
  • IMessageFormatter: a reference to the object that formats messages for the logger

Command line

  • ICommandSetManager: sets the schema for a command line; used by the command-line parser
  • ICommandProcessingResult: contains the result of having processed the command line
  • ICommandLineSettings: defines the properties needed to process the command line (e.g. the Arguments and CommandLineConfigurationFilename, which indicates the optional filename to use for configuration in addition to the standard ones)

Configuration

  • IConfigurationDataSettings: defines the ConfigurationData which is the hierarchical representation of all configuration data for the application as well as the MainConfigurationFilename from which this data is read; used by the configuration-loader
  • ITextValueNodeReader: the object that knows how to read ConfigurationData from the file formats supported by the application[3]; used by the configuration-loader

Run

  • IRunSettings: an object that manages the RunMode (“release” or “debug”), which can be set from the command line and is used by the ApplicationManager to determine whether to use global exception-handling
  • IApplicationManagerPreRunFinalizer: a reference to an object that applies any options from the command line before the decision of whether to execute in release or debug mode is taken.
  • IApplicationCrashReporter: used by the ApplicationManager in the code surrounding the entire application execution and therefore not guaranteed to have a usable IOC available
  • IApplicationDescription: used together with the ILocationManager to set application-specific aliases to user-configuration folders (e.g. AppData\{CompanyTitle}\{ApplicationTitle})
  • IApplicationTranscript: an object that records the last result of having run the application; returned by the ApplicationManager after Run() has completed, but also available through the application object returned by CreateAndStartUp() to indicate the state of the application after startup.

Each of these objects has a very compact interface and has a single responsibility. An application can easily replace any of these objects by calling UseSingle() during stage 1 or 2. This call sets the object in both the poor-man’s IOC as well as the real one. For those rare cases where a non-IOC singleton needs to be set after the IOC has been finalized, the application can call SetSingle(), which does not touch the IOC. This feature is currently used only to set the IApplicationTranscript, which needs to happen even after the IOC registration is complete.


[1]

Two large customer solutions, two medium-sized internal solutions (Punchclock and JobVortex) as well as the Demo/Sandbox solution. These solutions include the gamut of application types:

  • 3 ASP.NET MVC applications
  • 2 ASP.NET WebAPI applications
  • 2 Windows services
  • 3 Winform/DevExpress applications
  • 2 Winform/DevExpress utilities
  • 4 Console applications and utilities
[2]

I originally used ITextValueNodeReader as an example, but that’s one case where the recommended call doesn’t match 1-to-1 with the interface name.

application.GetSingle<ITextValueNodeReader>();
application.GetInstance<ITextValueNodeReader>();
application.GetConfigurationDataReader(); // Recommended
[3] Currently only XML, but JSON is on the way when someone gets a free afternoon.