/
NHibernate
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(); } } }
, multiple selections available,
Related content
Combining Bulk Load Assemblies and Classes
Combining Bulk Load Assemblies and Classes
More like this
Platform Dev. Guide - Configuration
Platform Dev. Guide - Configuration
More like this
Platform Dev. Guide - Configuration
Platform Dev. Guide - Configuration
More like this
Platform Dev. Guide - Configuration
Platform Dev. Guide - Configuration
More like this
EdFi.Ods.WebApi.Netcore
EdFi.Ods.WebApi.Netcore
More like this
Authentication
Authentication
More like this