Skip to content

Commit e305b67

Browse files
Improve exception on user types lacking some interfaces (nhibernate#1988)
1 parent cbea12e commit e305b67

File tree

6 files changed

+305
-8
lines changed

6 files changed

+305
-8
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System;
2+
3+
namespace NHibernate.Test.NHSpecificTest.GH1963
4+
{
5+
public class Entity
6+
{
7+
public virtual Guid Id { get; set; }
8+
public virtual string Name { get; set; }
9+
public virtual bool Flag { get; set; }
10+
}
11+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using System;
2+
using System.Linq;
3+
using NHibernate.UserTypes;
4+
using NUnit.Framework;
5+
6+
namespace NHibernate.Test.NHSpecificTest.GH1963
7+
{
8+
[TestFixture]
9+
public class Fixture : BugTestCase
10+
{
11+
protected override void OnSetUp()
12+
{
13+
using (var session = OpenSession())
14+
using (var transaction = session.BeginTransaction())
15+
{
16+
var e1 = new Entity {Name = "Bob", Flag = true};
17+
session.Save(e1);
18+
19+
var e2 = new Entity {Name = "Sally"};
20+
session.Save(e2);
21+
22+
transaction.Commit();
23+
}
24+
}
25+
26+
protected override void OnTearDown()
27+
{
28+
using (var session = OpenSession())
29+
using (var transaction = session.BeginTransaction())
30+
{
31+
// The HQL delete does all the job inside the database without loading the entities, but it does
32+
// not handle delete order for avoiding violating constraints if any. Use
33+
// session.Delete("from System.Object");
34+
// instead if in need of having NHibernate ordering the deletes, but this will cause
35+
// loading the entities in the session.
36+
session.CreateQuery("delete from System.Object").ExecuteUpdate();
37+
38+
transaction.Commit();
39+
}
40+
}
41+
42+
[Test]
43+
public void LinqFilterOnNonLiteralCustomType()
44+
{
45+
using (var session = OpenSession())
46+
using (session.BeginTransaction())
47+
{
48+
var result = from e in session.Query<Entity>()
49+
where e.Flag
50+
select e;
51+
52+
Assert.That(
53+
result.ToList,
54+
Throws
55+
.InnerException.TypeOf<InvalidOperationException>()
56+
.And.InnerException.Message.Contains(nameof(IEnhancedUserType)));
57+
}
58+
}
59+
}
60+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="NHibernate.Test"
3+
namespace="NHibernate.Test.NHSpecificTest.GH1963">
4+
5+
<class name="Entity">
6+
<id name="Id" generator="guid.comb"/>
7+
<property name="Name"/>
8+
<property name="Flag" type="NHibernate.Test.NHSpecificTest.GH1963.SqlBoolean, NHibernate.Test" />
9+
</class>
10+
11+
</hibernate-mapping>
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
using System;
2+
using System.Data;
3+
using System.Data.Common;
4+
using NHibernate.Engine;
5+
using NHibernate.SqlTypes;
6+
using NHibernate.UserTypes;
7+
8+
namespace NHibernate.Test.NHSpecificTest.GH1963
9+
{
10+
[Serializable]
11+
public class SqlBoolean : IUserType
12+
// Uncomment for checking implementing this solves the query exception
13+
//, IEnhancedUserType
14+
{
15+
/// <summary>
16+
/// Compare two instances of the class mapped by this type for persistent "equality"
17+
/// ie. equality of persistent state
18+
/// </summary>
19+
/// <param name="x"/><param name="y"/>
20+
/// <returns/>
21+
public new bool Equals(object x, object y)
22+
{
23+
if (x == y) return true;
24+
if (x == null || y == null) return false;
25+
26+
return x.Equals(y);
27+
}
28+
29+
/// <summary>
30+
/// Get a hashcode for the instance, consistent with persistence "equality"
31+
/// </summary>
32+
public int GetHashCode(object x)
33+
{
34+
return x.GetHashCode();
35+
}
36+
37+
/// <summary>
38+
/// Retrieve an instance of the mapped class from a JDBC resultset.
39+
/// Implementors should handle possibility of null values.
40+
/// </summary>
41+
/// <param name="rs">a IDataReader</param><param name="names">column names</param>
42+
/// <param name="session"></param>
43+
/// <param name="owner">the containing entity</param>
44+
/// <returns/>
45+
/// <exception cref="T:NHibernate.HibernateException">HibernateException</exception>
46+
public object NullSafeGet(DbDataReader rs, string[] names, ISessionImplementor session, object owner)
47+
{
48+
object result = NHibernateUtil.String.NullSafeGet(rs, names[0], session, owner);
49+
50+
if (result == null)
51+
return default(bool?);
52+
else
53+
return result.ToString() == "1";
54+
}
55+
56+
/// <summary>
57+
/// Write an instance of the mapped class to a prepared statement.
58+
/// Implementors should handle possibility of null values.
59+
/// A multi-column type should be written to parameters starting from index.
60+
/// </summary>
61+
/// <param name="cmd">a IDbCommand</param><param name="value">the object to write</param>
62+
/// <param name="index">command parameter index</param>
63+
/// <param name="session"></param>
64+
/// <exception cref="T:NHibernate.HibernateException">HibernateException</exception>
65+
public void NullSafeSet(DbCommand cmd, object value, int index, ISessionImplementor session)
66+
{
67+
var result = value != null ? Convert.ToInt16(bool.Parse(value.ToString())) : default(short?);
68+
69+
NHibernateUtil.Int16.NullSafeSet(cmd, result, index, session);
70+
}
71+
72+
/// <summary>
73+
/// Return a deep copy of the persistent state, stopping at entities and at collections.
74+
/// </summary>
75+
/// <param name="value">generally a collection element or entity field</param>
76+
/// <returns>
77+
/// a copy
78+
/// </returns>
79+
public object DeepCopy(object value)
80+
{
81+
return value;
82+
}
83+
84+
/// <summary>
85+
/// During merge, replace the existing (<paramref name="target"/>) value in the entity
86+
/// we are merging to with a new (<paramref name="original"/>) value from the detached
87+
/// entity we are merging. For immutable objects, or null values, it is safe to simply
88+
/// return the first parameter. For mutable objects, it is safe to return a copy of the
89+
/// first parameter. For objects with component values, it might make sense to
90+
/// recursively replace component values.
91+
/// </summary>
92+
/// <param name="original">the value from the detached entity being merged</param>
93+
/// <param name="target">the value in the managed entity</param>
94+
/// <param name="owner">the managed entity</param>
95+
/// <returns>
96+
/// the value to be merged
97+
/// </returns>
98+
public object Replace(object original, object target, object owner)
99+
{
100+
return DeepCopy(original);
101+
}
102+
103+
/// <summary>
104+
/// Reconstruct an object from the cacheable representation. At the very least this
105+
/// method should perform a deep copy if the type is mutable. (optional operation)
106+
/// </summary>
107+
/// <param name="cached">the object to be cached</param><param name="owner">the owner of the cached object</param>
108+
/// <returns>
109+
/// a reconstructed object from the cachable representation
110+
/// </returns>
111+
public object Assemble(object cached, object owner)
112+
{
113+
return DeepCopy(cached);
114+
}
115+
116+
/// <summary>
117+
/// Transform the object into its cacheable representation. At the very least this
118+
/// method should perform a deep copy if the type is mutable. That may not be enough
119+
/// for some implementations, however; for example, associations must be cached as
120+
/// identifier values. (optional operation)
121+
/// </summary>
122+
/// <param name="value">the object to be cached</param>
123+
/// <returns>
124+
/// a cacheable representation of the object
125+
/// </returns>
126+
public object Disassemble(object value)
127+
{
128+
return DeepCopy(value);
129+
}
130+
131+
/// <summary>
132+
/// The SQL types for the columns mapped by this type.
133+
/// </summary>
134+
public SqlType[] SqlTypes
135+
{
136+
get { return new[] { new SqlType(DbType.Int16) }; }
137+
}
138+
139+
/// <summary>
140+
/// The type returned by <c>NullSafeGet()</c>
141+
/// </summary>
142+
public System.Type ReturnedType
143+
{
144+
get { return typeof(bool?); }
145+
}
146+
147+
/// <summary>
148+
/// Are objects of this type mutable?
149+
/// </summary>
150+
public bool IsMutable
151+
{
152+
get { return false; }
153+
}
154+
155+
public object FromXMLString(string xml)
156+
{
157+
if (bool.TryParse(xml, out var value))
158+
return value;
159+
return null;
160+
}
161+
162+
public string ObjectToSQLString(object value)
163+
{
164+
var val = value as bool?;
165+
switch (val)
166+
{
167+
case true:
168+
return "1";
169+
case false:
170+
return "0";
171+
default:
172+
return "null";
173+
}
174+
}
175+
176+
public string ToXMLString(object value)
177+
{
178+
return value?.ToString();
179+
}
180+
}
181+
}

src/NHibernate/Async/Type/CustomType.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ public override async Task<bool> IsDirtyAsync(object old, object current, bool[]
9292

9393
public Task<object> NextAsync(object current, ISessionImplementor session, CancellationToken cancellationToken)
9494
{
95+
if (!(userType is IUserVersionType userVersionType))
96+
throw new InvalidOperationException(
97+
$"User type {userType} does not implement {nameof(IUserVersionType)}, Either implement it, or " +
98+
$"avoid using this user type as a version type.");
9599
if (cancellationToken.IsCancellationRequested)
96100
{
97101
return Task.FromCanceled<object>(cancellationToken);
@@ -108,6 +112,10 @@ public Task<object> NextAsync(object current, ISessionImplementor session, Cance
108112

109113
public Task<object> SeedAsync(ISessionImplementor session, CancellationToken cancellationToken)
110114
{
115+
if (!(userType is IUserVersionType userVersionType))
116+
throw new InvalidOperationException(
117+
$"User type {userType} does not implement {nameof(IUserVersionType)}, Either implement it, or " +
118+
$"avoid using this user type as a version type.");
111119
if (cancellationToken.IsCancellationRequested)
112120
{
113121
return Task.FromCanceled<object>(cancellationToken);

src/NHibernate/Type/CustomType.cs

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,11 @@ public override string ToLoggableString(object value, ISessionFactoryImplementor
115115
return "null";
116116
}
117117

118-
if (userType is IEnhancedUserType eut)
118+
if (userType is IEnhancedUserType enhancedUserType)
119119
{
120120
// 6.0 TODO: remove warning disable/restore
121121
#pragma warning disable 618
122-
return eut.ToXMLString(value);
122+
return enhancedUserType.ToXMLString(value);
123123
#pragma warning restore 618
124124
}
125125
return value.ToString();
@@ -172,9 +172,13 @@ public override bool IsDirty(object old, object current, bool[] checkable, ISess
172172
/// <inheritdoc />
173173
public object StringToObject(string xml)
174174
{
175+
if (!(userType is IEnhancedUserType enhancedUserType))
176+
throw new InvalidOperationException(
177+
$"User type {userType} does not implement {nameof(IEnhancedUserType)}, Either implement it, or " +
178+
$"avoid using this user type as an identifier or a discriminator.");
175179
// 6.0 TODO: remove warning disable/restore
176180
#pragma warning disable 618
177-
return ((IEnhancedUserType) userType).FromXMLString(xml);
181+
return enhancedUserType.FromXMLString(xml);
178182
#pragma warning restore 618
179183
}
180184

@@ -183,30 +187,52 @@ public object StringToObject(string xml)
183187
/// <inheritdoc cref="IVersionType.FromStringValue"/>
184188
public object FromStringValue(string xml)
185189
{
190+
if (!(userType is IEnhancedUserType enhancedUserType))
191+
throw new InvalidOperationException(
192+
$"User type {userType} does not implement {nameof(IEnhancedUserType)}, Either implement it, or " +
193+
$"avoid using this user type as an identifier or a discriminator.");
186194
// 6.0 TODO: remove warning disable/restore
187195
#pragma warning disable 618
188-
return ((IEnhancedUserType) userType).FromXMLString(xml);
196+
return enhancedUserType.FromXMLString(xml);
189197
#pragma warning restore 618
190198
}
191199

192200
public virtual string ObjectToSQLString(object value, Dialect.Dialect dialect)
193201
{
194-
return ((IEnhancedUserType)userType).ObjectToSQLString(value);
202+
if (!(userType is IEnhancedUserType enhancedUserType))
203+
throw new InvalidOperationException(
204+
$"User type {userType} does not implement {nameof(IEnhancedUserType)}, its SQL literal value " +
205+
$"cannot be resolved. Either implement it, or avoid using this user type as an identifier, a " +
206+
$"discriminator, or with queries requiring its literal value.");
207+
return enhancedUserType.ObjectToSQLString(value);
195208
}
196209

197210
public object Next(object current, ISessionImplementor session)
198211
{
199-
return ((IUserVersionType) userType).Next(current, session);
212+
if (!(userType is IUserVersionType userVersionType))
213+
throw new InvalidOperationException(
214+
$"User type {userType} does not implement {nameof(IUserVersionType)}, Either implement it, or " +
215+
$"avoid using this user type as a version type.");
216+
return userVersionType.Next(current, session);
200217
}
201218

202219
public object Seed(ISessionImplementor session)
203220
{
204-
return ((IUserVersionType) userType).Seed(session);
221+
if (!(userType is IUserVersionType userVersionType))
222+
throw new InvalidOperationException(
223+
$"User type {userType} does not implement {nameof(IUserVersionType)}, Either implement it, or " +
224+
$"avoid using this user type as a version type.");
225+
return userVersionType.Seed(session);
205226
}
206227

207228
public IComparer Comparator
208229
{
209-
get { return (IComparer) userType; }
230+
get
231+
{
232+
return userType as IComparer ?? throw new InvalidOperationException(
233+
$"User type {userType} does not implement {nameof(IUserVersionType)}, Either implement it, or " +
234+
$"avoid using this user type as a version type.");
235+
}
210236
}
211237

212238
public override object Replace(object original, object current, ISessionImplementor session, object owner,

0 commit comments

Comments
 (0)