NHibernate

NHibernate registration in net core is tricky as the we are not using a Fluent NHibernate. To solve this issue, a specific nhibernate.cfg.xml file was created. Session management in a Kesler application we need to use async_local.

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
  <session-factory>
    <property name="dialect">NHibernate.Dialect.MsSql2012Dialect</property>
    <property name="connection.driver_class">EdFi.Ods.Api.Common.Infrastructure.Architecture.SqlServer.EdFiSql2008ClientDriver, EdFi.Ods.Api.Common</property>
    <property name="connection.isolation">ReadCommitted</property>
    <property name="default_schema">edfi</property>
    <property name="current_session_context_class">async_local</property>
    <property name="adonet.batch_size">100</property>
    <!--
        Disable the hbm2ddl keywords feature.
         - The keywords feature causes the database to be hit as soon as the session factory is created.
         - If tracing is enabled and a trace listener is added (for instance, in Azure), then controllers are instantiated outside of the request.
         - Since we are calculating connection information based on headers in the request, we have no connection information during trace time, and NHibernate throws an exception.
      -->
    <property name="hbm2ddl.auto">none</property>
    <property name="hbm2ddl.keywords">none</property>
  </session-factory>
</hibernate-configuration>

New interfaces where created to inject the connection string. Each interface was created specific to the database that it connects to:

#if NETSTANDARD
namespace EdFi.Ods.Common.Database
{
    public interface IOdsDatabaseConnectionStringProvider : IDatabaseConnectionStringProvider { }
}
#endif
#if NETSTANDARD
namespace EdFi.Ods.Common.Database
{
    public interface IAdminDatabaseConnectionStringProvider : IDatabaseConnectionStringProvider { }
}
#endif
#if NETSTANDARD
namespace EdFi.Ods.Common.Database
{
    public interface ISecurityDatabaseConnectionStringProvider : IDatabaseConnectionStringProvider { }
}
#endif

For NHibernate we use the IOdsDatabaseConnectionStringProvider and it is implemented as:

using System;
using System.Collections.Concurrent;
using System.Configuration;
using EdFi.Ods.Common.Configuration;
using EdFi.Ods.Common.Extensions;

namespace EdFi.Ods.Common.Database
{
    /// <summary>
    /// Gets the connection string using a configured named connection string as a prototype for the connection string
    /// with an injected <see cref="IDatabaseNameReplacementTokenProvider"/> to replace token in database name.
    /// </summary>
    public class PrototypeWithDatabaseNameTokenReplacementConnectionStringProvider : IOdsDatabaseConnectionStringProvider
    {
        private readonly IConfigConnectionStringsProvider _configConnectionStringsProvider;

        private readonly ConcurrentDictionary<Tuple<string, string>, string> _connectionStringsByNames
            = new ConcurrentDictionary<Tuple<string, string>, string>();
        private readonly IDatabaseNameReplacementTokenProvider _databaseNameReplacementTokenProvider;
        private readonly IDbConnectionStringBuilderAdapterFactory _dbConnectionStringBuilderAdapterFactory;
        private readonly string _prototypeConnectionStringName;

        private string _prototypeConnectionString;

        /// <summary>
        /// Initializes a new instance of the <see cref="PrototypeWithDatabaseNameTokenReplacementConnectionStringProvider"/> class using
        /// the specified "prototype" named connection string from the application configuration file and the supplied database name replacement token provider.
        /// </summary>
        /// <param name="prototypeConnectionStringName">The named connection string to use as the basis for building the connection string.</param>
        /// <param name="databaseNameReplacementTokenProvider">The provider that builds the database name replacement token for use in the resulting connection string.</param>
        /// <param name="configConnectionStringsProvider"></param>
        /// <param name="dbConnectionStringBuilderAdapterFactory"></param>
        public PrototypeWithDatabaseNameTokenReplacementConnectionStringProvider(
            string prototypeConnectionStringName,
            IDatabaseNameReplacementTokenProvider databaseNameReplacementTokenProvider,
            IConfigConnectionStringsProvider configConnectionStringsProvider,
            IDbConnectionStringBuilderAdapterFactory dbConnectionStringBuilderAdapterFactory)
        {
            _prototypeConnectionStringName = prototypeConnectionStringName;
            _databaseNameReplacementTokenProvider = databaseNameReplacementTokenProvider;
            _configConnectionStringsProvider = configConnectionStringsProvider;
            _dbConnectionStringBuilderAdapterFactory = dbConnectionStringBuilderAdapterFactory;
        }

        /// <summary>
        /// Gets the connection string using a configured named connection string with the database replaced using the specified database name replacement token provider.
        /// </summary>
        /// <returns>The connection string.</returns>
        public string GetConnectionString()
        {
            var connectionStringBuilder = _dbConnectionStringBuilderAdapterFactory.Get();
            connectionStringBuilder.ConnectionString = PrototypeConnectionString();

            // Override the Database Name, format if string coming in has a format replacement token,
            // otherwise use database name set in the Initial Catalog.
            connectionStringBuilder.DatabaseName = connectionStringBuilder.DatabaseName.IsFormatString()
                ? string.Format(
                    connectionStringBuilder.DatabaseName,
                    _databaseNameReplacementTokenProvider.GetReplacementToken())
                : connectionStringBuilder.DatabaseName;

            return _connectionStringsByNames.GetOrAdd(
                Tuple.Create(_prototypeConnectionStringName, connectionStringBuilder.DatabaseName),
                x => connectionStringBuilder.ConnectionString);
        }

        private string PrototypeConnectionString()
        {
            if (_prototypeConnectionString != null)
            {
                return _prototypeConnectionString;
            }

            if (_configConnectionStringsProvider.Count == 0)
            {
                throw new ConfigurationErrorsException("No connection strings were found in the configuration file.");
            }

            if (string.IsNullOrWhiteSpace(_prototypeConnectionStringName))
            {
                throw new ArgumentNullException("prototypeConnectionStringName");
            }

            string connectionString = _configConnectionStringsProvider.GetConnectionString(_prototypeConnectionStringName);

            _prototypeConnectionString = connectionString ?? throw new ConfigurationErrorsException(
                $"No connection string named '{_prototypeConnectionStringName}' was found in the 'connectionStrings' section of the application configuration file.");

            return _prototypeConnectionString;
        }
    }
}

The NHibernateConfigurator is modified to load the configuration file

using System;
using System.Collections.Generic;
using System.Linq;
using EdFi.Ods.Api.Common.Infrastructure.Extensibility;
using EdFi.Ods.Api.Common.Infrastructure.Filtering;
using EdFi.Ods.Api.Common.Providers;
using EdFi.Ods.Api.Common.Providers.Criteria;
using EdFi.Ods.Common;
using EdFi.Ods.Common.Database;
using EdFi.Ods.Common.Extensions;
using EdFi.Ods.Common.Utils.Extensions;
using NHibernate;
using NHibernate.Cfg;
using NHibernate.Cfg.MappingSchema;
using NHibernate.Mapping;

namespace EdFi.Ods.Api.Common.Infrastructure.Configuration
{
    public class NHibernateConfigurator : INHibernateConfigurator
    {
        private const string EntityExtensionMemberName = "Extensions";
        private const string AggregateExtensionMemberName = "AggregateExtensions";

        private readonly IDictionary<string, HbmBag[]> _aggregateExtensionHbmBagsByEntityName;
        private readonly INHibernateFilterConfigurator[] _authorizationStrategyConfigurators;
        private readonly INHibernateBeforeBindMappingActivity[] _beforeBindMappingActivities;
        private readonly INHibernateConfigurationActivity[] _configurationActivities;
        private readonly IDictionary<string, HbmBag[]> _entityExtensionHbmBagsByEntityName;
        private readonly IExtensionNHibernateConfigurationProvider[] _extensionConfigurationProviders;
        private readonly IDictionary<string, HbmSubclass[]> _extensionDerivedEntityByEntityName;
        private readonly IDictionary<string, HbmJoinedSubclass[]> _extensionDescriptorByEntityName;
        private readonly IFilterCriteriaApplicatorProvider _filterCriteriaApplicatorProvider;
        private readonly IOrmMappingFileDataProvider _ormMappingFileDataProvider;

#if NETFRAMEWORK
        public NHibernateConfigurator(IExtensionNHibernateConfigurationProvider[] extensionConfigurationProviders,
            INHibernateBeforeBindMappingActivity[] beforeBindMappingActivities,
            INHibernateFilterConfigurator[] authorizationStrategyConfigurators,
            IFilterCriteriaApplicatorProvider filterCriteriaApplicatorProvider,
            INHibernateConfigurationActivity[] configurationActivities,
            IOrmMappingFileDataProvider ormMappingFileDataProvider)
#elif NETSTANDARD
        private IOdsDatabaseConnectionStringProvider _connectionStringProvider;
        public NHibernateConfigurator(IEnumerable<IExtensionNHibernateConfigurationProvider> extensionConfigurationProviders,
            IEnumerable<INHibernateBeforeBindMappingActivity> beforeBindMappingActivities,
            IEnumerable<INHibernateFilterConfigurator> authorizationStrategyConfigurators,
            IFilterCriteriaApplicatorProvider filterCriteriaApplicatorProvider,
            IEnumerable<INHibernateConfigurationActivity> configurationActivities,
            IOrmMappingFileDataProvider ormMappingFileDataProvider, 
            IOdsDatabaseConnectionStringProvider connectionStringProvider)
#endif
        {
#if NETSTANDARD
            _connectionStringProvider = connectionStringProvider;
#endif
            _ormMappingFileDataProvider = Preconditions.ThrowIfNull(
                ormMappingFileDataProvider, nameof(ormMappingFileDataProvider));

            _extensionConfigurationProviders = Preconditions.ThrowIfNull(
                extensionConfigurationProviders.ToArray(), nameof(extensionConfigurationProviders));

            _beforeBindMappingActivities = Preconditions.ThrowIfNull(
                beforeBindMappingActivities.ToArray(), nameof(beforeBindMappingActivities));

            _authorizationStrategyConfigurators = Preconditions.ThrowIfNull(
                authorizationStrategyConfigurators.ToArray(), nameof(authorizationStrategyConfigurators));

            _configurationActivities = Preconditions.ThrowIfNull(
                configurationActivities.ToArray(), nameof(configurationActivities));

            _filterCriteriaApplicatorProvider = Preconditions.ThrowIfNull(
                filterCriteriaApplicatorProvider, nameof(filterCriteriaApplicatorProvider));

            //Resolve all extensions to include in core mapping
            _entityExtensionHbmBagsByEntityName = _extensionConfigurationProviders
                .SelectMany(x => x.EntityExtensionHbmBagByEntityName)
                .GroupBy(x => x.Key)
                .ToDictionary(
                    x => x.Key,
                    x => x.Select(y => y.Value)
                        .ToArray());

            _aggregateExtensionHbmBagsByEntityName = _extensionConfigurationProviders
                .SelectMany(x => x.AggregateExtensionHbmBagsByEntityName)
                .GroupBy(x => x.Key)
                .ToDictionary(
                    x => x.Key,
                    x => x.SelectMany(y => y.Value)
                        .ToArray());

            _extensionDescriptorByEntityName = _extensionConfigurationProviders
                .SelectMany(x => x.NonDiscriminatorBasedHbmJoinedSubclassesByEntityName)
                .GroupBy(x => x.Key)
                .ToDictionary(
                    x => x.Key,
                    x => x.SelectMany(y => y.Value)
                        .ToArray());

            _extensionDerivedEntityByEntityName = _extensionConfigurationProviders
                .SelectMany(x => x.DiscriminatorBasedHbmSubclassesByEntityName)
                .GroupBy(x => x.Key)
                .ToDictionary(k => k.Key, v => v.SelectMany(y => y.Value).ToArray());
        }

        public NHibernate.Cfg.Configuration Configure()
        {
            var configuration = new NHibernate.Cfg.Configuration();
#if NETSTANDARD
            // NOTE: the NHibernate documentation states that this file would be automatically loaded, however in testings this was not the case.
            // The expectation is that this file will be in the binaries location.
            configuration.Configure("hibernate.cfg.xml");

            // NOTE: since we are using the connection string provider instead we just need to configure the connection string.
            configuration.DataBaseIntegration(c => c.ConnectionString = _connectionStringProvider.GetConnectionString());
#endif

            // Add the configuration to the container
            configuration.BeforeBindMapping += Configuration_BeforeBindMapping;

            // Get all the filter definitions from all the configurators
            var allFilterDetails = _authorizationStrategyConfigurators
                .SelectMany(c => c.GetFilters())
                .Distinct()
                .ToList();

            // Group the filters by name first (there can only be 1 "default" filter, but flexibility 
            // to apply same filter name with same parameters to different entities should be supported
            // (and is in fact supported below when filters are applied to individual entity mappings)
            var allFilterDetailsGroupedByName = allFilterDetails
                .GroupBy(f => f.FilterDefinition.FilterName)
                .Select(g => g);

            // Add all the filter definitions to the NHibernate configuration
            foreach (var filterDetails in allFilterDetailsGroupedByName)
            {
                configuration.AddFilterDefinition(
                    filterDetails.First()
                        .FilterDefinition);
            }

            // Configure the mappings
            var ormMappingFileData = _ormMappingFileDataProvider.OrmMappingFileData();
            configuration.AddResources(ormMappingFileData.MappingFileFullNames, ormMappingFileData.Assembly);

            //Resolve all extension assemblies and add to NHibernate configuration
            _extensionConfigurationProviders.ForEach(
                e => configuration.AddResources(e.OrmMappingFileData.MappingFileFullNames, e.OrmMappingFileData.Assembly));

            // Invoke configuration activities
            foreach (var configurationActivity in _configurationActivities)
            {
                configurationActivity.Execute(configuration);
            }

            // Apply the previously defined filters to the mappings
            foreach (var mapping in configuration.ClassMappings)
            {
                Type entityType = mapping.MappedClass;
                var properties = entityType.GetProperties();

                var applicableFilters = allFilterDetails
                    .Where(filterDetails => filterDetails.ShouldApply(entityType, properties))
                    .ToList();

                foreach (var filter in applicableFilters)
                {
                    var filterDefinition = filter.FilterDefinition;

                    // Save the filter criteria applicators
                    _filterCriteriaApplicatorProvider.AddCriteriaApplicator(
                        filterDefinition.FilterName,
                        entityType,
                        filter.CriteriaApplicator);

                    mapping.AddFilter(
                        filterDefinition.FilterName,
                        filterDefinition.DefaultFilterCondition);

                    var metaAttribute = new MetaAttribute(filterDefinition.FilterName);
                    metaAttribute.AddValue(filter.HqlConditionFormatString);

                    mapping.MetaAttributes.Add(
                        "HqlFilter_" + filterDefinition.FilterName,
                        metaAttribute);
                }
            }

            configuration.AddCreateDateHooks();

            return configuration;
        }

        private void Configuration_BeforeBindMapping(object sender, BindMappingEventArgs e)
        {
            // When core mapping file loaded, attach any extensions to their core entity counterpart
            if (IsEdFiStandardMappingEvent())
            {
                var classMappingByEntityName = e.Mapping.Items.OfType<HbmClass>()
                    .ToDictionary(
                        x => x.Name.Split('.')
                            .Last(),
                        x => x);

                var joinedSubclassMappingByEntityName = e.Mapping.Items.OfType<HbmClass>()
                    .SelectMany(i => i.JoinedSubclasses)
                    .ToDictionary(
                        x => x.Name.Split('.')
                            .Last(),
                        x => x);

                var subclassJoinMappingByEntityName = e.Mapping.Items.OfType<HbmClass>()
                    .SelectMany(i => i.Subclasses)
                    .Where(sc => sc.Joins.Count() == 1)
                    .ToDictionary(
                        x => x.Name.Split('.')
                            .Last(),
                        x => x.Joins.Single());

                MapExtensionsToCoreEntity(
                    classMappingByEntityName, joinedSubclassMappingByEntityName, subclassJoinMappingByEntityName);

                MapJoinedSubclassesToCoreEntity(
                    classMappingByEntityName, joinedSubclassMappingByEntityName, subclassJoinMappingByEntityName);

                MapDescriptorToCoreDescriptorEntity(classMappingByEntityName);

                MapDerivedEntityToCoreEntity(classMappingByEntityName);
            }

            foreach (var beforeBindMappingActivity in _beforeBindMappingActivities)
            {
                beforeBindMappingActivity.Execute(sender, e);
            }

            void MapDerivedEntityToCoreEntity(Dictionary<string, HbmClass> classMappingByEntityName)
            {
                foreach (string entityName in _extensionDerivedEntityByEntityName.Keys)
                {
                    if (!classMappingByEntityName.TryGetValue(entityName, out HbmClass classMapping))
                    {
                        throw new MappingException(
                            $"The subclass extension to entity '{entityName}' could not be applied because the class mapping could not be found.");
                    }

                    var hbmSubclasses = _extensionDerivedEntityByEntityName[entityName].Select(x => (object) x).ToArray();

                    classMapping.Items1 = (classMapping.Items1 ?? new object[0]).Concat(hbmSubclasses).ToArray();
                }
            }

            void MapDescriptorToCoreDescriptorEntity(Dictionary<string, HbmClass> classMappingByEntityName)
            {
                // foreach entity name, look in core mapping file (e.mapping) for core entity mapping and if found
                // concat new extension HbmJoinedSubclass to current set of Ed-Fi entity HbmJoinedSubclasses.
                foreach (string entityName in _extensionDescriptorByEntityName.Keys)
                {
                    if (!classMappingByEntityName.TryGetValue(entityName, out HbmClass classMapping))
                    {
                        throw new MappingException(
                            $"The subclass extension to entity '{entityName}' could not be applied because the class mapping could not be found.");
                    }

                    var hbmJoinedSubclasses = _extensionDescriptorByEntityName[entityName]
                        .Select(x => (object) x)
                        .ToArray();

                    classMapping.Items1 = (classMapping.Items1 ?? new object[0]).Concat(hbmJoinedSubclasses)
                        .ToArray();
                }
            }

            void MapJoinedSubclassesToCoreEntity(Dictionary<string, HbmClass> classMappingByEntityName,
                Dictionary<string, HbmJoinedSubclass> joinedSubclassMappingByEntityName,
                Dictionary<string, HbmJoin> subclassJoinMappingByEntityName)
            {
                // foreach entity name, look in core mapping file (e.mapping) for core entity mapping and if found
                // concat new extension HbmDynamicComponent to current set of items.
                foreach (string entityName in _aggregateExtensionHbmBagsByEntityName.Keys)
                {
                    HbmJoinedSubclass joinedSubclassMapping = null;
                    HbmJoin subclassJoinMapping = null;

                    if (!classMappingByEntityName.TryGetValue(entityName, out HbmClass classMapping)
                        && !joinedSubclassMappingByEntityName.TryGetValue(entityName, out joinedSubclassMapping)
                        && !subclassJoinMappingByEntityName.TryGetValue(entityName, out subclassJoinMapping))
                    {
                        throw new MappingException(
                            $"The aggregate extensions to entity '{entityName}' could not be applied because the class mapping could not be found.");
                    }

                    var extensionComponent = new HbmDynamicComponent
                    {
                        name = AggregateExtensionMemberName,
                        Items = _aggregateExtensionHbmBagsByEntityName[entityName]
                            .Select(x => (object) x)
                            .ToArray()
                    };

                    if (classMapping != null)
                    {
                        classMapping.Items = classMapping.Items.Concat(extensionComponent)
                            .ToArray();
                    }
                    else if (joinedSubclassMapping != null)
                    {
                        joinedSubclassMapping.Items = joinedSubclassMapping.Items.Concat(extensionComponent)
                            .ToArray();
                    }
                    else if (subclassJoinMapping != null)
                    {
                        subclassJoinMapping.Items = subclassJoinMapping.Items.Concat(extensionComponent)
                            .ToArray();
                    }
                }
            }

            void MapExtensionsToCoreEntity(Dictionary<string, HbmClass> classMappingByEntityName,
                Dictionary<string, HbmJoinedSubclass> joinedSubclassMappingByEntityName,
                Dictionary<string, HbmJoin> subclassJoinMappingByEntityName)
            {
                // foreach entity name, look in core mapping file (e.mapping) for core entity mapping and if found
                // concat new extension HbmDynamicComponent to current set of items.
                foreach (string entityName in _entityExtensionHbmBagsByEntityName.Keys)
                {
                    HbmJoinedSubclass joinedSubclassMapping = null;
                    HbmJoin subclassJoinMapping = null;

                    if (!classMappingByEntityName.TryGetValue(entityName, out HbmClass classMapping)
                        && !joinedSubclassMappingByEntityName.TryGetValue(entityName, out joinedSubclassMapping)
                        && !subclassJoinMappingByEntityName.TryGetValue(entityName, out subclassJoinMapping))
                    {
                        throw new MappingException(
                            $"The entity extension to entity '{entityName}' could not be applied because the class mapping could not be found.");
                    }

                    var extensionComponent = new HbmDynamicComponent
                    {
                        name = EntityExtensionMemberName,
                        Items = _entityExtensionHbmBagsByEntityName[entityName]
                            .Select(x => (object) x)
                            .ToArray()
                    };

                    if (classMapping != null)
                    {
                        classMapping.Items = classMapping.Items.Concat(extensionComponent)
                            .ToArray();
                    }
                    else if (joinedSubclassMapping != null)
                    {
                        joinedSubclassMapping.Items = joinedSubclassMapping.Items.Concat(extensionComponent)
                            .ToArray();
                    }
                    else if (subclassJoinMapping != null)
                    {
                        subclassJoinMapping.Items = subclassJoinMapping.Items.Concat(extensionComponent)
                            .ToArray();
                    }
                }
            }

            bool IsEdFiStandardMappingEvent()
            {
                return e.Mapping.@namespace.Equals(Namespaces.Entities.NHibernate.BaseNamespace)
                       && e.Mapping.assembly.Equals(Namespaces.Standard.BaseNamespace);
            }
        }
    }
}

NHibernate is then registered into the container using:

using System;
using Autofac;
using EdFi.Ods.Api.Common.Constants;
using EdFi.Ods.Api.Common.Infrastructure.Configuration;
using EdFi.Ods.Api.Common.Providers;
using NHibernate;

namespace EdFi.Ods.Api.NetCore.Container.Modules
{
    public class NHibernateConfigurationModule : Module
    {
        protected override void Load(ContainerBuilder builder)
        {
            builder.RegisterType<OrmMappingFileDataProvider>()
                .WithParameter(new NamedParameter("assemblyName", OrmMappingFileConventions.OrmMappingAssembly))
                .As<IOrmMappingFileDataProvider>()
                .SingleInstance();

            builder.RegisterType<NHibernateConfigurator>()
                .As<INHibernateConfigurator>()
                .SingleInstance();

            builder.Register(
                    c => c.Resolve<INHibernateConfigurator>()
                        .Configure())
                .As<NHibernate.Cfg.Configuration>()
                .AsSelf()
                .SingleInstance();

            builder.Register(
                    c => c.Resolve<NHibernate.Cfg.Configuration>()
                        .BuildSessionFactory())
                .As<ISessionFactory>()
                .SingleInstance();

            // ----------------------------------------------------------------------------------------------------
            // NOTE: Sometimes ISessionFactory cannot be injected, so we're injecting a Func<IStatelessSession> rather
            // than the ISessionFactory or IStatelessSession (the latter of which can result in a memory leak).
            // Read on for more details.
            //
            // If the container manages the creation of the IStatelessSession (or ISession) as a transient instance,
            // it will track the session instance until it is explicitly released by calling the container's Release
            // method. This is because these interfaces also implement IDisposable. Usually Release is invoked
            // automatically by the using a lifecycle other than transient (e.g. per web request), but in our case this
            // is not always possible as the code needing to open a session is sometimes running outside of the context
            // of the current ASP.NET web request (e.g. background cache initialization) and because of a runtime
            // cyclical dependency exception, an ISessionFactory cannot be resolved and injected.
            //
            // By using a singleton factory method of type Func<IStatelessSession>, we can keep the management of the
            // session instance out of the container's hands, avoid the cyclical dependency exception, and we only need
            // to dispose of the session when we're done.
            // ----------------------------------------------------------------------------------------------------

            // The function is a singleton, not the session
            // Autofac needs to first resolve the context into a variable before it can assign the function.
            builder.Register<Func<IStatelessSession>>(
                    c =>
                    {
                        var ctx = c.Resolve<IComponentContext>();

                        return () => ctx.Resolve<ISessionFactory>()
                            .OpenStatelessSession();
                    })
                .SingleInstance();

            builder.Register<Func<ISession>>(
                    c =>
                    {
                        var ctx = c.Resolve<IComponentContext>();

                        return () => ctx.Resolve<ISessionFactory>()
                            .OpenSession();
                    })
                .SingleInstance();
        }
    }
}