Configuration API - quandis/qbo3-Documentation GitHub Wiki

QBO can be extensively configured via the web browser. From Design > Configuration, the following pages are available:

  • Modules: maintain module-specific dropdown lists, templates, statements, filters, services and application settings
  • Logging: maintain logging sources and listeners
  • Queuing: maintain queues, monitor items being processed asynchronously
  • File Objects: maintain locations to store files (FTPs sites, SAN folders, Amazon S3, client imaging systems)
  • Schedules: maintain the Schedule table, leveraged by many other modules
  • Calendars: maintain the Calendar and Holiday tables
  • Packages: install business-specific functionality with a single click
  • Data Tuning: optimize QBO configuration to improve database performance

Logging

QBO leverages the Microsoft Enterprise Library (ETL)'s Logging Application block. This does several things, including:

  • buffered asynchronous logging: the code does not wait around for the log write
  • configurable logging sinks: log to a text file, a database, or a centralized error repository
  • ignore logging: code can write to a log with no listeners configured, ignoring the logged item
  • tracing within the log (correlate multiple log entries with a single activity)

For performance logging of a call to Valuation.ashx/Summary?ID=X, this is what happens:

  • Valuation.ashx's ProcessRequest calls HttpHandler.ProcessRequest
    • HttpHandler.ProcessRequest calls HttpHandler.ProcessQuery
    • HttpHandler.ProcessQuery
      • issues a TraceManager.StartTrace("Performance"), writing a unique string to the Performance logging sink
      • calls ProcessQueryXhtml
      • calls AbstractObject.InvokeXmlReader
        • AbstractObject.InvokeXmlReader calls ExecuteXmlReader
        • ExecuteXmlReader marks a start time, and once the database has returned data,
        • ExecuteXmlReader calls LogPerformance, writing the execution time to the performance log
      • calls the Transform property, to transform the results against an XSLT
        • Transform calls LogPerformance, writing the XSLT load time to the performance log

This creates entries in the performance log akin to:

Message: Start Trace: Activity  '28ffd1c2-f95e-4dfe-ad6e-6e47f0f86a2b' in method 'qbo.Application.HttpHandler.ProcessQuery' at 681496160415 ticks
Message: qbo.Mortgage.ValuationObject/ObjectTree took 9 ms to execute
Message: qbo.Mortgage.ValuationObject/ObjectData took 2 ms to execute
Message: qbo.Mortgage.ValuationObject/Navigation took 2 ms to execute
Message: Xslt Templates/Mortgage//Valuation.Summary.xslt load time took 206 ms to execute
Message: End Trace: Activity '28ffd1c2-f95e-4dfe-ad6e-6e47f0f86a2b' in method 'qbo.Application.HttpHandler.ProcessQuery' at 681632296083 ticks (elapsed time: 0.01 

Navigating to Valuation.ashx/Summary?ID=X again will produce similar results, minus the loading of Valuation.Summary.xslt (assuming that Xslt caching is turned on).

Log Filtering

Logging can be extensive. For example, logging all performance data is will generate a lot of data. Often, we're interested only in slow-running operations. The qbo.Logging.PerformanceFilter class can be used to limit logging to only those operations that take more than a given amount of time to execute. If used, the default minimum execution time to log is 2500 ms, but this is adjustable in web.config.

<add enabled="true" type="qbo.Logging.PerformanceFilter, qbo.Logging" name="Performance Filter" MinDuration="100"/>

Configuration Monitoring

QBO is designed to run on a server farm, and enables power users to alter configuration settings which, in some cases, need to be propagated to all servers in a server farm. Such propagation is accomplished via Configuration Monitoring. Specifically, a ConfigurationMonitor class does the following:

  • scans the bin folder for DLLs that include a ConfigurationMonitorAttribute attribute (pointing to some method to run), and registers the monitoring method
  • starts a timer
  • each time it's timer elapses, executes each registered monitoring method

For websites, initiating configuration monitoring is done via the Application_Start method found in Global.asax.

As of this writing, there are three monitoring methods in the QBO standard deployment:

  • ConfigurationEntry: enables overrides of any .config file based on the BaseConfiguration class
    • scans the ConfigurationEntry table for recent changes, and loads the configuration data into the application domain's memory
  • SystemDefault: enables overrides of application settings
    • scans the SystemDefault table for application setting changes, and writes any application settings to web.config
  • CachedFile: enables overrides of standard XSLTs
    • scans the ConfiguationEntry table for 'FileOverride' entries, and copies them from a configured FileObject (e.g. Amazon S3, Azure, etc.)

Configuration Entry

Upon detecting a ConfigurationEntry table change, the ConfigurationEntry corresponding collection is updated to contain the ConfigurationXml value that was saved to the database. Consider the following example:

  • A new statement called SCRASelect is added under the Contact module resulting in a record being added to ConfigurationEntry. The ConfigurationEntry record contains:
    • Source = Contact.config`
    • ConfigurationType = qbo.Application.Configuration.DbStatementCollection
    • ConfigurationKey = SCRASelect
    • ConfigurationXml = {Statement Xml}
    • UpdatedDate = {Date Added}
  • The ConfigurationMonitor will detect the new record based on last poll date.
  • ConfiguraitonMonitor will invoke the Update method. The Update method must be implement in each class that inherits from BaseConfiguration. In this case it will deserialize the ConfigurationXml which updates Contact statement collection and then wires the statement into the Contact configuration)

Targeting Specific Servers

In some case, including multi-tenant installations and code roll-outs, it is useful to have configuration entries that apply to some servers, but not others. This can be accomplished by using the qbo.Application.Properties.Settings.ConfigurationFilter application setting. For example:

<setting name="ConfigurationFilter" serializeAs="String">
  <value>PROD-2016-09-15</value>
</setting>

When a web.config file includes this setting, the server's ConfigurationEntry/Monitor method will include ConfigurationEntry rows where:

  • ConfigurationEntry.Filter is NULL, or
  • ConfigurationEntry.Filter = qbo.Application.Properties.Settings.ConfigurationFilter

For example, assume:

  • you are deploying a new OCR plugin (qbo.Attachment.GoogleDrive) as part of a code deployment.
  • machines P1, P2, P3 and P4 contain your 'old' code base (without the OCR plugin)
  • machines P5, P6, P7 and P8 contain your 'new' code base (with the OCR plugin)
  • you intend to deploy a setup package (Setup.OCR.xml)
    • this includes an IService entry for Attachment.config so users can call Attachment/OCR

If you run the Setup.OCR.xml package as-is, all machines (P1..P8) will attempt to wire an IService entry to qbo.Attachment.GoogleDrive.dll. Machines P1..P4 don't have qbo.Attachment.GoogleDrive.dll, so they will encounter an error.

This error can be avoid as follows:

  • In the web.config file for P5..P8, set the ConfigurationFilter to some value that is different than P1..P4 (e.g. PROD-2016-09-15)
  • Edit Setup.OCR.xml manually, adding a Filter node to each ConfigurationEntry: <Filter>PROD-2016-09-15</Filter>
  • Run the manually edited setup package against any server

This will create ConfigurationEntry rows that will only be read by servers P5..P8. Servers P1..P4 will ignore those rows. It also means that servers P1..P4 will not be able to call any newly created Attachment/OCR method, but that's true prior to the code deployment anyway.

Once machines P1..P4 have been 'decommissioned', you should run the following SQL to ensure you keep Attachment/OCR available for future code deployments:

UPDATE ConfigurationEntry SET Filter = NULL WHERE Filter = 'PROD-2016-09-15'

System Defaults and Application Settings

.NET application settings are a useful tool to drive settings via configuration. They are defined in compiled code with default values, but can be overridden in app.config or web.config files. For example, the qbo.Application.Properties.Settings.SiteName is used to configuration-drive the name of a QBO site; it defaults to 'Quandis Business Object'. To override such a setting in web.config, one can add:

<applicationSettings>
  <qbo.Application.Properties.Settings>
    <setting name="SiteName" serializeAs="String">
      <value>Acme Financial Services</value>
    </setting>
  <qbo.Application.Properties.Settings>
<applicationSettings>

For continuous deployment purposes, QBO's SystemDefault/Monitor method enables you to data-drive such configuration from the QBO SystemDefault table. For example from SQL:

INSERT INTO SystemDefault (SystemDefault, Value, UpdatedDate) 
VALUES ('qbo.Application.Properties.Settings.SiteName', 'Acme Financial Services', GETDATE())

or from a setup package:

<SystemDefaultCollection>
  <SystemDefaultItem>
    <SystemDefault>qbo.Application.Properties.Settings.SiteName</SystemDefault>
    <Value>Acme Financial Services</Value>
  </SystemDefaultItem>
</SystemDefaultCollection>

will cause SystemDefault/Monitor on each web and application server to detect the setting, and modify the server's web.config file to contain the overridden setting.

This way, a single virtual machine image can be deployed to different environments with the same web.config file, and the environment's setting (from the database) will be automatically overwritten.

Feature Switching

System Defaults are frequently use for feature switching. When Quandis introduces a new QBO feature that runs a risk of breaking existing QBO functionality, the new functionality can be toggled by an application setting that acts as a feature switch. For example, the original functionality of the Task class included a Pause method that would save the task, and a Save method that would call:

if (!ActualCompletion.HasValue) ActualCompletion = DateTime.Now;
Save();

We later encountered use cases where we wished to call Save without triggering the completion of the task. Rather than break existing functionality, a feature switch was introduced:

if (Properties.Settings.Default.ImportFormCompleteOnSave && !ActualCompletion.HasValue) ActualCompletion = DateTime.Now;
Save();

The ImportFormCompleteOnSave application setting determines whether a QBO system will implement the 'old' or the 'new' functionality, enabling clients to choose their logic.

Startup

QBO's extensible nature means that there's a lot of dependency injection and reflection going on. We've worked hard to minimize the impact of this in overall system performance, generally by ensuring that operations involving reflection are done once - when the application domain starts. The first request to a QBO instance is very often slow, because none of the reflection operations have been cached yet. This has two implications:

  • Production systems should be configure to keep the application domain alive, and
  • Development systems that have the application domain reset frequently are slow to respond following each app domain restart

We recommend loader.io for performance and load testing QBO installations. We're confident you'll be very happy with post-startup performance!

Packages

Packages are QBO 'add-ons' that provide pre-configured functionality in QBO. Package files are simply QBO-compliant XML files that contain data and configuration entries to rapidly set up functionality. For example, the Compliance package will install the data and configuration required to manage compliance audits for attorneys in the mortgage space.

To view packages available for install, navigate to Design > Configuration > Packages (Import/ImportFile.ashx/PackageList).

To install a packages, click on the name of the package.

Creating packages

Developers can create packages by:

  • Creating a theme (empty ASP.NET web project)
  • Adding the required XSLTs, javascript, and css files to the project
  • Creating an import-framework compliant XML documents, placed in Config/Setup.{Package Name}.xml
  • Deploying the project to the target QBO system.

Note that custom configuration components (statements, filters, file objects, etc.) should be part of the package file, so they are stored in the ConfigurationEntry table instead of cluttering the core configuration files.

See Source: Theme.Compliance.csproj for an example package.

Data Tuning

QBO attempts to assist with keeping the database highly tuned as follows:

  • Data Retention Policy: control what data is audited, and schedule purges of data that is no longer needed
  • Entity View: maintain how tables participate in the Entity view
  • Index Tuning: maintain indexes, including removing unused indexes and creating indexes specific to a site's particular usage
  • Extranet Tuning: analyze a site's usage, and auto-configure triggers and object configuration to optimize performance

Data Retention Policy

All QBO tables have the option of being audited. Auditing comprises a history table to which changing data is copied, and a trigger that copies the data that is being changed. For example, the Organization table may have an OrganizationHistory trigger that copies data to an OrganizationHistory table. Every time an Organization row is updated or deleted, a copy of the row being updated or deleted (along with the user and time stamp of the change) is copied to the OrganizationHistory table. These history table can grow quite large quite rapidly, especially where there are 'portfolio updates' be passed into QBO on a daily basis.

The Data Retention Policy dashboard will display, for each installed module:

  • Module: the name of the module (which is the same as the database table)
  • History: reflects whether History information is being tracked
    • Not installed: there is no {Module}History table for the module
    • Not configured: there is a {Module}History table, but the trigger to populate it is disabled
    • Enabled: there is a {Module}History table, and a trigger to populate it is enabled
  • Purge: reflects whether there is a schedule job to purge data from the table
    • Not scheduled: there is no job to purge data from the table
    • X months, Run {some schedule}: data older than X months will be purged according to {some schedule}
      • e.g. 120 months, Run every 1 day(s) @ 03:00:00am will delete data more than 10 years old daily at 3 am
  • History purge: reflect whether there is a scheduled job to purge data from the history table
    • these parameters are the same as Purge, except they apply to the History table only

Data Tuning Best practices:

  • Logging tables (E.g. QueueLog, ImportLog, SecurityLog)
    • should not have history installed
    • should purge data after a reasonable auditing time frame (e.g. 3 months)
  • Rarely changing tables that have little business impact (e.g. MessageRecipient, HolidayInstance)
    • should not have history installed
  • High volume tables (E.g. Decision, DecisionStep, Attachment, Message)
    • should purge history data after a reasonable auditing time frame (e.g. 3 months)

Entity and EntityParent Views

The Entity and EntityParent Views are used by QBO to establish the relationships between rows in the system. It is the key to the Summary statement, which is leveraged by all Summary pages, and most data-driven templates (Decision, Attachment, Message). As a result the performance of the Entity view is critical to the overall performance of a QBO system.

The Entity must contains all tables for which a Summary statement will be run, which is almost all tables.

The EntityParent should contain only tables which have a parent object, and can include a record more than once. For example, the Valuation table may have 'two' parents: a Property, and the Valuation.Object/ID. The EntityParent may include both for the Valuation.

When a Summary statement is run, a recursive common table expression (CTE) is used to calculate the ObjectTree: a table representing all ancestors and descendants of a record. Each object's Summary statement may be configured with optional parameters, including:

  • Ancestors: a space-delimited list of tables that should be considered as ancestors
  • Descendants: a space-delimited list of tables that should be considered as descendants
  • AncestorDepth: number of generations to calculate ancestors to (a generation is 1 pass through the recursive CTE querying the Entity view)
  • DescendantDepth: number of generations to calculate descendants to (a generation is 1 pass through the recursive CTE querying the EntityParent view)
  • MaxCount: the most recent items are usually the most relevant; setting the MaxCount parameter to 100 or so will limit descendant data to the 100 most recent rows per table
    • e.g. a Loan/Summary?ID=X&MaxCount=100 limits the number of Attachments to 100, ImportForms to 100, ImportFileQueues to 100, etc.

The Summary statement performance can be impacted significantly by growth in tables, usually tables that participate in the EntityParent view. For example, the ImportFileQueue and ImportLog tables may grow very quickly in systems that are importing external data. Thus, as time passes, what was a performant Summary statement may become slow.

The Design > Configuration > Data Tuning > Entity View > Options > Optimize Summary command will automatically configure the Summary parameters for a given class based on data actually in the system. It scans the Entity and EntityParent views to determine which ancestor and descendant records actually exist for a given class, and sets the Ancestors and Descendants parameter to the results so that future calls to the Summary statement do not need to query tables that won't return rows anyway.

This OptimizeSummary statement is certainly helpful, but manual input by an experienced power user can contribute to even more tuning. For example, a given system may hang thousands of Attachment or Ledger/LedgerItem records off an Organization. Nominally these records should be part of Organization/Summary. However, they do not have to be present in the Summary output if they are not being used by the UI or templated configurations like workflows. A power user can manually 'tune' the Organization/Summary statement, removing Attachment or Ledger, with the understanding that future UI or templated configurations won't have Attachment or Ledger data output by the Summary statement.

Summary Tuning

As noted above, the Summary method is intended to return all information related to an object, so that power users don't need to define custom queries for:

  • Document generation,
  • Workflow if/then statements,
  • Parameter substitution,
  • etc.

where "all information" includes:

  • the object itself
  • ancestors
  • descendants
  • siblings (foreign keys)

Thus, the Summary method becomes a useful construct for many use cases, including rendering the basic UI for most objects. Ironically, the UI rarely needs to have descendants included in the Summary output, because descendants are rendered in panels via AJAX calls. Thus, most calls to the Summary method don't use most of the data returned by Summary.

The application setting qbo.Application.Properties.Settings.SummaryDefaultOptions allows QBO to limit the default behavior of Summary for UI operations by intercepting calls to Summary from base web handlers, and appending the SummaryDefaultOptions to the parameters being passed to Summary. Configuring a target system's SummaryDefaultOptions to be DescendantDepth=0 will thus remove descendants from the Summary web page, lightening the load on the database for UI calls.

Note that Summary parameters can be overridden by creating a Summary statement for any given class. If a custom Summary statement exists and includes any of the same parameters included in SummaryDefaultOptions, the custom Summary statement's parameters 'win'.

For example, assume that the Foreclosure/Summary UI needed to include key tasks from associated workflows in Foreclosure.Summary.xslt. To optimize the system:

  • From Design > Configuration > Modules > Object Status > Settings, configure SummaryDefaultOptions to be DescendantDepth=0
  • From Design > Configuration > Modules > Mortgage > Statements, add a Summary statement with a parameter DescendantDepth=10

Other considerations:

  • Effective data retention policies: see above
  • Tailored repeatability: for templated items, consider one-per-parent instead of many-per-parent when you can
  • Set SelectGenericEntityColumns to false: this will omit ParentLabel, GrandParent, and GrandParentID from {Select.ForeignKey} and {From.ForeignKey}, but is much faster than including extra joins to Entity by default.
    • This is done from Design > Configuration > Modules > Matrix > Settings tab

Index Tuning

The ConfigurationEntry/IndexList method signature provides a list of indexes recommended for a given system. Recommended indexes include:

  • A clustered primary key index on a table's identity column
  • A non-clustered index on each foreign key (assuming the foreign key contains, or will contain, non-null values)
  • A non-clustered index on ObjectID/Object for tables that follow QBO's GenericObject pattern
  • A non-clustered index on each Dimension, as defined by the C# configuration file (for dashboard statistics)
  • A non-clustered conditional index on UpdatedDate, for calculation of deltas and use by the data listener pattern
  • A non-clustered index on each SmartSearch term used by the table's configuration file

Extranet Tuning

The ObjectAccess/Analysis method (and corresponding statement) will analyze current data, and recommend extranet configuration settings for a given class. Recommendations are defined as:

  • Recommended: this means a foreign key includes non-null data, and there are PersonAccess records exist mapping users to these foreign key values
  • Maybe: this means a foreign key includes non-null data, but there are no PersonAccess records mapping users to any of these foreign key values
  • Disable: this means a foreign key does not include any non-null data, meaning there is no possibility of users being mapped to values based on this data

Secrets Management

Quandis provides a key management infrastructure (backed by AWS Key Management Service) to centralize key management, encryption and decryption of secrets, such as usernames and passwords.

Unit Tests

Unit tests that need to include credentials (and other secrets) should store them in Quandis' infrastructure. In order to use this infrastructure, you must obtain credentials from Quandis. When we issue these credentials, we will provide:

  • A 'secrets table': the name of the table we use to store a client's secrets.
  • A 'crypto key': the name of an encryption key used to encrypt and decrypt secrets.

The credentials we issue have access to the secrets table and the crypto key. Do not share these credentials with others; we can issue a credential per developer, if you wish.

Once you've received your credentials, secrets table and crypto key from Quandis, you're ready to add keys. From Visual Studio, open the Package Manager Console (which is a Powershell window) and issue the following:

# Do this once, when you are provided credentials
Set-AWSCredentials -StoreAs '{your email}' -AccessKey {Quandis provided username} -SecretKey {Quandis provided password}
# Set the default AWS credential profile
Initialize-AWSDefaults -ProfileName '[email protected]'
Import-module c:\source\trunk\qbo.3\publish\keyManager.ps1
Set-Secrets -table "AcmeFinancial" -application "devacme.quandis.net" -key "AcmeFinancial"

Once the code above has been executed, you may create as many secrets as you'd like:

Set-Secret -keyName "AzureBlobSecretKey" -keyValue "blahblahblahblah"

QBO plugins that leverage third party infrastructure, including AWS and Azure resources, often need credential information in order to run tests. Quandis' practice is to leverage our key management infrastructure for this. For example, the qbo.Azure solution includes a BlobStorageTests class that includes:

MockConfiguration(CredentialConfiguration.CredentialCache, new Credential()
{
  UriPrefix = "https://samplebucket.core.windows.net",
  AuthType = "Basic",
  Username = UserSecret.Get("AzureStorageUsername"),
  Password = UserSecret.Get("AzureStoragePassword")
});

If you run this test without setting up secrets management with Quandis, you will receive an error message along these lines:

System.ArgumentNullException: To run a unit test that depends on user secrets, you must set the following environment variables: qbo_SecretsTable, qbo_ApplicationName, qbo_CryptoKey. 

If you have already run Set-Secrets and run this test, you will instead receive an error message along these lines:

System.ArgumentOutOfRangeException: The key 'AzureStorageUsername' was not found in the secrets table 'AcmeFinancial' for the application 'devacme.quandis.net'. 
Use Quandis' keyManager.ps1 PowerShell script to set the key before running this test:
Set-Secret -keyName 'Foo' -KeyValue 'Bar'
Parameter name: keyName

To get this test to pass, you must set an AzureStorageUsername secret:

Set-Secret -keyName 'AzureStorageUsername' -keyValue '{username from your Azure account}'
Set-Secret -keyName 'AzureStoragePassword' -keyValue '{password from your Azure account}'

Reflection

Each QBO instance may have different sets of modules and plugins. It is frequently useful to determine the set of installed modules and plugins programmatically. The following API calls assist with such discovery.

Type List

Calling ObjectConfiguration.ashx/TypeList?TypeName={some base type} will serve up a list of all non-abstract classes that are assignable from that type. For example:

-- Get a List of IFileObject plugins installed
Application/ObjectConfiguration.ashx/TypeList?TypeName=qbo.Attachment.Interfaces.IFileObject,qbo.Attachment

with results similar to:

<ClassCollection>
  <KeyValuePairOfstringstring xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/System.Collections.Generic">
    <key>BlobStorage</key>
    <value>qbo.Attachment.Azure.BlobStorage, qbo.Attachment.Azure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null</value>
  </KeyValuePairOfstringstring>
  <KeyValuePairOfstringstring xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/System.Collections.Generic">
    <key>CachedFile</key>
    <value>qbo.Attachment.FileObjects.CachedFile, qbo.Attachment, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null</value>
  </KeyValuePairOfstringstring>
  <KeyValuePairOfstringstring xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/System.Collections.Generic">
    <key>DistributedFile</key>
    <value>qbo.Attachment.FileObjects.DistributedFile, qbo.Attachment, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null</value>
  </KeyValuePairOfstringstring>
  <KeyValuePairOfstringstring xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/System.Collections.Generic">
    <key>S3FileObject</key>
    <value>qbo.Attachment.Amazon.S3FileObject, qbo.Attachment.Amazon, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null</value>
  </KeyValuePairOfstringstring>
  <KeyValuePairOfstringstring xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/System.Collections.Generic">
    <key>sFtpFile</key>
    <value>qbo.Attachment.Rebex.sFtpFile, qbo.Attachment.Rebex, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null</value>
  </KeyValuePairOfstringstring>
  <KeyValuePairOfstringstring xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/System.Collections.Generic">
    <key>TemplateFile</key>
    <value>qbo.Attachment.FileObjects.TemplateFile, qbo.Attachment, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null</value>
  </KeyValuePairOfstringstring>
  <KeyValuePairOfstringstring xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/System.Collections.Generic">
    <key>UNCFile</key>
    <value>qbo.Attachment.FileObjects.UNCFile, qbo.Attachment, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null</value>
  </KeyValuePairOfstringstring>
</ClassCollection>

This can be used with a dropdown behavior:

<select name="Handler" data-behavior="Dropdown" data-dropdown-options="{{'url': 
  'Application/ObjectConfiguration.ashx/TypeList?Output=Json', 
  'jsonEval': 'ClassNameCollection.ClassNameItem', 
  'value': 'value', 
  'text': 'key', 
  'selected': '{@Handler}', 
  'data': {{ 'TypeName': 'qbo.Attachment.Interfaces.IFileObject,qbo.Attachment' }} }}">
  <option value="">-- None --</option>
</select>

Commonly used calls include:

// Get a list of all IService plugins
Application/ObjectConfiguration.ashx/TypeList?Type=qbo.Application.Interfaces.IService
// Get a list of all IQueue plugins
Application/ObjectConfiguration.ashx/TypeList?Type=qbo.Application.Interfaces.IQueue
// Get a list of all IMethodListener plugins
Application/ObjectConfiguration.ashx/TypeList?Type=qbo.Application.Interfaces.IMethodListener

// Get a list of all IFileObject plugins
Application/ObjectConfiguration.ashx/TypeList?Type=qbo.Attachment.Interfaces.IFileObject
// Get a list of all IGenerator plugins
Application/ObjectConfiguration.ashx/TypeList?Type=qbo.Attachment.Interfaces.IGenerator

// Get a list of all IScore plugins
Application/ObjectConfiguration.ashx/TypeList?Type=qbo.Score.Interfaces.IScore
⚠️ **GitHub.com Fallback** ⚠️