diff --git a/doc/reference/modules/basic_mapping.xml b/doc/reference/modules/basic_mapping.xml index 839a3880ff5..b2407efade5 100644 --- a/doc/reference/modules/basic_mapping.xml +++ b/doc/reference/modules/basic_mapping.xml @@ -2633,7 +2633,7 @@ types, System.Object types, and System.Object types for large objects. Just like Columns for System.ValueType types can handle null values only if the entity property is properly typed with a Nullable<T>. Otherwise null will be replaced by the default - value for the type when reading, and when be overwritten by it when persisting the entity, potentially leading to + value for the type when reading, and then will be overwritten by it when persisting the entity, potentially leading to phantom updates. @@ -3025,6 +3025,12 @@ NHibernate.Type.TypeFactory. + + Default NHibernate types used when no type attribute is specified can be overridden by using + the NHibernate.Type.TypeFactory.RegisterType static method before configuring and building + session factories. + + diff --git a/src/NHibernate.Test/TypesTest/ChangeDefaultTypeClass.cs b/src/NHibernate.Test/TypesTest/ChangeDefaultTypeClass.cs new file mode 100644 index 00000000000..1dca978091e --- /dev/null +++ b/src/NHibernate.Test/TypesTest/ChangeDefaultTypeClass.cs @@ -0,0 +1,11 @@ +using System; + +namespace NHibernate.Test.TypesTest +{ + public class ChangeDefaultTypeClass + { + public int Id { get; set; } + + public DateTime NormalDateTimeValue { get; set; } = DateTime.Today; + } +} diff --git a/src/NHibernate.Test/TypesTest/ChangeDefaultTypeClass.hbm.xml b/src/NHibernate.Test/TypesTest/ChangeDefaultTypeClass.hbm.xml new file mode 100644 index 00000000000..f2cfaf24dec --- /dev/null +++ b/src/NHibernate.Test/TypesTest/ChangeDefaultTypeClass.hbm.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/src/NHibernate.Test/TypesTest/ChangeDefaultTypeFixture.cs b/src/NHibernate.Test/TypesTest/ChangeDefaultTypeFixture.cs new file mode 100644 index 00000000000..d54f3876f33 --- /dev/null +++ b/src/NHibernate.Test/TypesTest/ChangeDefaultTypeFixture.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using NHibernate.Cfg; +using NHibernate.Engine; +using NHibernate.Impl; +using NHibernate.Type; +using NUnit.Framework; + +namespace NHibernate.Test.TypesTest +{ + /// + /// TestFixtures for changing a default .Net type. + /// + [TestFixture] + public class ChangeDefaultTypeFixture : TypeFixtureBase + { + protected override string TypeName => "ChangeDefaultType"; + + private IType _originalDefaultDateTimeType; + private IType _testDefaultDateTimeType; + + protected override void Configure(Configuration configuration) + { + _originalDefaultDateTimeType = TypeFactory.GetDefaultTypeFor(typeof(DateTime)); + Assert.That(_originalDefaultDateTimeType, Is.Not.Null); + _testDefaultDateTimeType = NHibernateUtil.DateTime.Equals(_originalDefaultDateTimeType) + ? (IType) NHibernateUtil.DateTimeNoMs + : NHibernateUtil.DateTime; + TypeFactory.RegisterType(typeof(DateTime), _testDefaultDateTimeType, TypeFactory.EmptyAliases); + base.Configure(configuration); + } + + protected override void DropSchema() + { + base.DropSchema(); + if (_originalDefaultDateTimeType != null) + TypeFactory.RegisterType(typeof(DateTime), _originalDefaultDateTimeType, TypeFactory.EmptyAliases); + } + + [Test] + public void DefaultType() + { + Assert.That(TypeFactory.GetDefaultTypeFor(typeof(DateTime)), Is.EqualTo(_testDefaultDateTimeType)); + } + + [Test] + public void PropertyType() + { + Assert.That( + Sfi.GetClassMetadata(typeof(ChangeDefaultTypeClass)) + .GetPropertyType(nameof(ChangeDefaultTypeClass.NormalDateTimeValue)), + Is.EqualTo(_testDefaultDateTimeType)); + } + + [Test] + public void GuessType() + { + Assert.That(NHibernateUtil.GuessType(typeof(DateTime)), Is.EqualTo(_testDefaultDateTimeType)); + } + + [Test] + public void ParameterType() + { + var namedParametersField = typeof(AbstractQueryImpl) + .GetField("namedParameters", BindingFlags.Instance | BindingFlags.NonPublic); + Assert.That(namedParametersField, Is.Not.Null, "Missing internal field"); + + using (var s = OpenSession()) + { + // Query where the parameter type cannot be deducted from compared entity property + var q = s.CreateQuery($"from {nameof(ChangeDefaultTypeClass)} where :date1 = :date2 or :date1 = :date3") + .SetParameter("date1", DateTime.Now) + .SetDateTime("date2", DateTime.Now) + .SetDateTimeNoMs("date3", DateTime.Now); + + var namedParameters = namedParametersField.GetValue(q) as Dictionary; + Assert.That(namedParameters, Is.Not.Null, "Unable to retrieve parameters internal field"); + Assert.That(namedParameters["date1"].Type, Is.EqualTo(_testDefaultDateTimeType)); + Assert.That(namedParameters["date2"].Type, Is.EqualTo(NHibernateUtil.DateTime)); + Assert.That(namedParameters["date3"].Type, Is.EqualTo(NHibernateUtil.DateTimeNoMs)); + } + } + } +} diff --git a/src/NHibernate/Type/TypeFactory.cs b/src/NHibernate/Type/TypeFactory.cs index 9d14ce6812f..cfa5e405e47 100644 --- a/src/NHibernate/Type/TypeFactory.cs +++ b/src/NHibernate/Type/TypeFactory.cs @@ -6,6 +6,7 @@ using System.Xml; using System.Xml.Linq; using NHibernate.Bytecode; +using NHibernate.Cfg; using NHibernate.Classic; using NHibernate.SqlTypes; using NHibernate.UserTypes; @@ -33,8 +34,9 @@ private enum TypeClassification PrecisionScale } + public static readonly string[] EmptyAliases = System.Array.Empty(); + private static readonly INHibernateLogger _log = NHibernateLogger.For(typeof(TypeFactory)); - private static readonly string[] EmptyAliases= System.Array.Empty(); private static readonly char[] PrecisionScaleSplit = { '(', ')', ',' }; private static readonly char[] LengthSplit = { '(', ')' }; @@ -95,7 +97,15 @@ private enum TypeClassification private delegate NullableType NullableTypeCreatorDelegate(SqlType sqlType); - private static void RegisterType(System.Type systemType, IType nhibernateType, IEnumerable aliases) + /// + /// Defines which NHibernate type should be chosen by default for handling a given .Net type. + /// This must be done before any operation on NHibernate, including building its + /// and building session factory. Otherwise the behavior will be undefined. + /// + /// The .Net type. + /// The NHibernate type. + /// The additional aliases to map to the type. Use if none. + public static void RegisterType(System.Type systemType, IType nhibernateType, IEnumerable aliases) { var typeAliases = new List(aliases); typeAliases.AddRange(GetClrTypeAliases(systemType));