Skip to content

Commit 6f09f8d

Browse files
ramonsmitshazzik
authored andcommitted
NH-2520 - Add TimestampUtc type
1 parent cb67239 commit 6f09f8d

File tree

10 files changed

+333
-6
lines changed

10 files changed

+333
-6
lines changed

src/NHibernate.Test/NHibernate.Test.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1397,6 +1397,8 @@
13971397
<Compile Include="TransformTests\ImplementationOfEqualityTests.cs" />
13981398
<Compile Include="TypesTest\CharClass.cs" />
13991399
<Compile Include="TypesTest\CharClassFixture.cs" />
1400+
<Compile Include="TypesTest\TimestampUtcClass.cs" />
1401+
<Compile Include="TypesTest\TimestampUtcTypeFixture.cs" />
14001402
<Compile Include="TypesTest\StringTypeWithLengthFixture.cs" />
14011403
<Compile Include="TypesTest\DateTimeClass.cs" />
14021404
<Compile Include="TypesTest\LocalDateTimeTypeFixture.cs" />
@@ -3413,6 +3415,7 @@
34133415
<EmbeddedResource Include="NHSpecificTest\AccessAndCorrectPropertyName\DogMapping.hbm.xml" />
34143416
<EmbeddedResource Include="NHSpecificTest\AccessAndCorrectPropertyName\PersonMapping.hbm.xml" />
34153417
<EmbeddedResource Include="NHSpecificTest\NH2507\Mappings.hbm.xml" />
3418+
<EmbeddedResource Include="TypesTest\TimestampUtcClass.hbm.xml" />
34163419
<EmbeddedResource Include="NHSpecificTest\EntityNameAndInheritance\Mappings.hbm.xml" />
34173420
<EmbeddedResource Include="NHSpecificTest\NH2467\Mappings.hbm.xml" />
34183421
<EmbeddedResource Include="NHSpecificTest\NH2459\Mappings.hbm.xml" />
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.TypesTest
4+
{
5+
public class TimestampUtcClass
6+
{
7+
public int Id { get; set; }
8+
public DateTime Value { get; set; }
9+
public DateTime Revision { get; protected set; }
10+
}
11+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
3+
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" default-lazy="false">
4+
<class
5+
name="NHibernate.Test.TypesTest.TimestampUtcClass, NHibernate.Test"
6+
table="bc_timestamputc">
7+
<id name="Id" column="id">
8+
<generator class="assigned" />
9+
</id>
10+
<version name="Revision" column="Revision" type="TimestampUtc" />
11+
<property name="Value" type="TimestampUtc" />
12+
</class>
13+
</hibernate-mapping>
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
using System;
2+
using NHibernate.Type;
3+
using NUnit.Framework;
4+
5+
namespace NHibernate.Test.TypesTest
6+
{
7+
/// <summary>
8+
/// Test fixture for type <see cref="TimestampUtcType"/>.
9+
/// </summary>
10+
[TestFixture]
11+
public class TimestampUtcTypeFixture : TypeFixtureBase
12+
{
13+
readonly TimestampUtcType _type = NHibernateUtil.TimestampUtc;
14+
readonly DateTime _utc = new DateTime(1976, 11, 30, 10, 0, 0, 300, DateTimeKind.Utc);
15+
readonly DateTime _local = new DateTime(1976, 11, 30, 10, 0, 0, 300, DateTimeKind.Local);
16+
readonly DateTime _unspecified = new DateTime(1976, 11, 30, 10, 0, 0, 300, DateTimeKind.Unspecified);
17+
18+
/// <summary>
19+
/// 1976-11-30T10:00:00.3000000
20+
/// </summary>
21+
const long DateInTicks = 623537928003000000;
22+
23+
protected override string TypeName => "TimestampUtc";
24+
25+
[Test]
26+
public void Next()
27+
{
28+
var current = DateTime.Parse("2004-01-01");
29+
var next = (DateTime)_type.Next(current, null);
30+
31+
Assert.AreEqual(DateTimeKind.Utc, next.Kind, "Kind is not Utc");
32+
Assert.IsTrue(next > current, "next should be greater than current (could be equal depending on how quickly this occurs)");
33+
}
34+
35+
/// <summary>
36+
/// Perform a 'seed' and check if the result is a datetime with kind set to Utc.
37+
/// </summary>
38+
[Test]
39+
public void Seed()
40+
{
41+
var type = NHibernateUtil.TimestampUtc;
42+
Assert.IsTrue(type.Seed(null) is DateTime, "Seed should be DateTime");
43+
44+
var value = (DateTime)type.Seed(null);
45+
Assert.AreEqual(DateTimeKind.Utc, value.Kind, "Kind should be Utc");
46+
}
47+
48+
/// <summary>
49+
/// Perform a basis write with a DateTime value where Kind is Local which should fail.
50+
/// </summary>
51+
[Test]
52+
[TestCase(DateTimeKind.Unspecified)]
53+
[TestCase(DateTimeKind.Local)]
54+
public void LocalReadWrite_Fail(DateTimeKind kind)
55+
{
56+
var entity = new TimestampUtcClass
57+
{
58+
Id = 1,
59+
Value = DateTime.SpecifyKind(DateTime.Now, kind)
60+
};
61+
62+
using(var session = OpenSession())
63+
using(var tx = session.BeginTransaction())
64+
{
65+
session.Save(entity);
66+
Assert.That(() => session.Flush(), Throws.TypeOf<PropertyValueException>());
67+
tx.Rollback();
68+
}
69+
}
70+
71+
/// <summary>
72+
/// Create two session. Write entity in the first and read it in the second and compare if
73+
/// the retrieved timestamp value still equals the original value.
74+
/// </summary>
75+
/// <remarks> This test takes the database precision into consideration.</remarks>
76+
[Test]
77+
public void UtcReadWrite_Success()
78+
{
79+
TimestampUtcClass entity;
80+
81+
// Save
82+
using(var session = OpenSession())
83+
using(var tx = session.BeginTransaction())
84+
{
85+
// Create a new datetime value and round it to the precision that the database supports. This
86+
// code basically the same as in the implementation but here to guard posible changes.
87+
var resolution = session.GetSessionImplementation().Factory.Dialect.TimestampResolutionInTicks;
88+
var next = DateTime.UtcNow;
89+
next = next.AddTicks(-(next.Ticks % resolution));
90+
91+
entity = new TimestampUtcClass
92+
{
93+
Id = 1,
94+
Value = next
95+
};
96+
97+
session.Save(entity);
98+
tx.Commit();
99+
session.Close();
100+
}
101+
102+
// Retrieve and compare
103+
using (var session = OpenSession())
104+
using (var tx = session.BeginTransaction())
105+
{
106+
var result = session.Get<TimestampUtcClass>(entity.Id);
107+
Assert.IsNotNull(result, "Entity not saved or cannot be retrieved by its key.");
108+
109+
// Property: Value
110+
Assert.AreEqual(DateTimeKind.Utc, result.Value.Kind, "Kind is NOT Utc");
111+
Assert.AreEqual(entity.Value.Ticks, result.Value.Ticks, "Value should be the same.");
112+
113+
// Property: Revision
114+
var revision = result.Revision;
115+
Assert.AreEqual(DateTimeKind.Utc, revision.Kind, "Kind is NOT Utc");
116+
117+
var differenceInSeconds = Math.Abs((revision - DateTime.UtcNow).TotalSeconds);
118+
Assert.IsTrue(differenceInSeconds < 1d, "Difference should be less then 1 second.");
119+
120+
tx.Commit();
121+
session.Close();
122+
}
123+
124+
// Delete
125+
using (var session = OpenSession())
126+
using (var tx = session.BeginTransaction())
127+
{
128+
var result = session.Get<TimestampUtcClass>(entity.Id);
129+
session.Delete(result);
130+
tx.Commit();
131+
session.Close();
132+
}
133+
}
134+
135+
/// <summary>
136+
/// Tests if the type FromStringValue implementation behaves as expected.
137+
/// </summary>
138+
/// <param name="timestampValue"></param>
139+
[Test]
140+
[TestCase("2011-01-27T15:50:59.6220000+02:00")]
141+
[TestCase("2011-01-27T14:50:59.6220000+01:00")]
142+
[TestCase("2011-01-27T13:50:59.6220000Z")]
143+
public void FromStringValue_ParseValidValues(string timestampValue)
144+
{
145+
var timestamp = DateTime.Parse(timestampValue);
146+
147+
Assert.AreEqual(DateTimeKind.Local, timestamp.Kind, "Kind is NOT Local. dotnet framework parses datetime values with kind set to Local and time correct to local timezone.");
148+
149+
timestamp = timestamp.ToUniversalTime();
150+
151+
var value = (DateTime)_type.FromStringValue(timestampValue);
152+
153+
Assert.AreEqual(timestamp, value, timestampValue);
154+
Assert.AreEqual(DateTimeKind.Utc, value.Kind, "Kind is NOT Utc");
155+
}
156+
157+
/// <summary>
158+
/// Test the framework tostring behavior. If the test fails then the <see cref="TimestampType"/> and <see cref="TimestampUtcType"/> implemention could not work propertly at run-time.
159+
/// </summary>
160+
[Test, Category("Expected framework behavior")]
161+
[TestCase(623537928003000000, DateTimeKind.Utc, ExpectedResult = "1976-11-30T10:00:00.3000000Z")]
162+
[TestCase(623537928003000000, DateTimeKind.Unspecified, ExpectedResult = "1976-11-30T10:00:00.3000000")]
163+
[TestCase(623537928003000000, DateTimeKind.Local, ExpectedResult = "1976-11-30T10:00:00.3000000+01:00",
164+
Ignore = "Offset depends on which system this test is run and can currently now be influenced via the .net framework",
165+
Description ="This test will ONLY succeed when the test is run on a system which if currently in a timezone with offset +01:00")]
166+
public string ExpectedToStringDotnetFrameworkBehavior(long ticks, DateTimeKind kind)
167+
{
168+
return new DateTime(ticks, kind).ToString("o");
169+
}
170+
171+
/// <summary>
172+
/// Test the framework tostring behavior. If the test fails then the <see cref="TimestampType"/> and <see cref="TimestampUtcType"/> implemention could not work propertly at run-time.
173+
/// </summary>
174+
[Test, Category("Expected framework behavior")]
175+
public void ExpectedIsEqualDotnetFrameworkBehavior()
176+
{
177+
const string assertMessage = "Values should be equal dotnet framework ignores Kind value.";
178+
Assert.AreEqual(_utc, _local, assertMessage);
179+
Assert.AreEqual(_utc, _unspecified, assertMessage);
180+
Assert.AreEqual(_unspecified, _local, assertMessage);
181+
}
182+
}
183+
}

src/NHibernate/NHibernate.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,7 @@
733733
<Compile Include="Type\UInt32Type.cs" />
734734
<Compile Include="Type\UInt64Type.cs" />
735735
<Compile Include="Type\PrimitiveType.cs" />
736+
<Compile Include="Type\TimestampUtcType.cs" />
736737
<Compile Include="Type\YesNoType.cs" />
737738
<Compile Include="UnresolvableObjectException.cs" />
738739
<Compile Include="Util\ADOExceptionReporter.cs" />

src/NHibernate/NHibernateUtil.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,11 @@ public static IType GuessType(System.Type type)
237237

238238
public static readonly DbTimestampType DbTimestamp = new DbTimestampType();
239239

240+
/// <summary>
241+
/// NHibernate timestamp utc type.
242+
/// </summary>
243+
public static readonly TimestampUtcType TimestampUtc = new TimestampUtcType();
244+
240245
/// <summary>
241246
/// NHibernate TrueFalse type
242247
/// </summary>

src/NHibernate/Type/TimestampType.cs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,25 @@ namespace NHibernate.Type
3030
[Serializable]
3131
public class TimestampType : PrimitiveType, IVersionType, ILiteralType
3232
{
33+
/// <summary>
34+
/// Retrieve the current system Local time.
35+
/// </summary>
36+
/// <value>DateTime.Now</value>
37+
protected virtual DateTime Now => DateTime.Now;
38+
39+
/// <summary>
40+
/// Returns the DateTimeKind for type <see cref="TimestampUtcType"/>.
41+
/// </summary>
42+
/// <value>Returns DateTimeKind.Unspecified</value>
43+
protected virtual DateTimeKind Kind => DateTimeKind.Unspecified;
44+
3345
public TimestampType() : base(SqlTypeFactory.DateTime)
3446
{
3547
}
3648

3749
public override object Get(DbDataReader rs, int index)
3850
{
39-
return Convert.ToDateTime(rs[index]);
51+
return DateTime.SpecifyKind(Convert.ToDateTime(rs[index]), Kind);
4052
}
4153

4254
public override object Get(DbDataReader rs, string name)
@@ -60,7 +72,7 @@ public override System.Type ReturnedClass
6072
/// </remarks>
6173
public override void Set(DbCommand st, object value, int index)
6274
{
63-
st.Parameters[index].Value = (value is DateTime) ? value:DateTime.Now;
75+
st.Parameters[index].Value = (value is DateTime) ? value : DateTime.SpecifyKind(Now, Kind);
6476
}
6577

6678
public override string Name
@@ -94,9 +106,9 @@ public virtual object Seed(ISessionImplementor session)
94106
{
95107
if (session == null)
96108
{
97-
return DateTime.Now;
109+
return Now;
98110
}
99-
return Round(DateTime.Now, session.Factory.Dialect.TimestampResolutionInTicks);
111+
return Round(Now, session.Factory.Dialect.TimestampResolutionInTicks);
100112
}
101113

102114
public IComparer Comparator
@@ -108,7 +120,7 @@ public IComparer Comparator
108120

109121
public object StringToObject(string xml)
110122
{
111-
return DateTime.Parse(xml);
123+
return FromStringValue(xml);
112124
}
113125

114126
public override System.Type PrimitiveClass
@@ -126,4 +138,4 @@ public override string ObjectToSQLString(object value, Dialect.Dialect dialect)
126138
return '\'' + value.ToString() + '\'';
127139
}
128140
}
129-
}
141+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using System;
2+
using System.Data.Common;
3+
4+
namespace NHibernate.Type
5+
{
6+
[Serializable]
7+
public class TimestampUtcType : TimestampType
8+
{
9+
/// <summary>
10+
/// Returns the DateTimeKind for type <see cref="TimestampUtcType"/>.
11+
/// </summary>
12+
/// <value>Returns DateTimeKind.Utc</value>
13+
protected override DateTimeKind Kind => DateTimeKind.Utc;
14+
15+
/// <summary>
16+
/// Retrieve the current system Utc time.
17+
/// </summary>
18+
/// <value>DateTime.UtcNow</value>
19+
protected override DateTime Now => DateTime.UtcNow;
20+
21+
public override string Name => "TimestampUtc";
22+
23+
/// <summary>
24+
/// Parse the value by the base implementation and convert it to Utc as the .net framework by default parse to a DateTime value with Kind set to Local.
25+
/// </summary>
26+
/// <returns>DateTime value where Kind is Utc</returns>
27+
public override object FromStringValue(string xml)
28+
{
29+
return ((DateTime) base.FromStringValue(xml)).ToUniversalTime();
30+
}
31+
32+
/// <summary>
33+
/// Validate the passed DateTime value if Kind is set to Utc and passes value to base implementation (<see cref="TimestampType.Set(DbCommand, object, int )"/>).
34+
/// </summary>
35+
/// <exception cref="ArgumentException">Thrown when Kind is NOT Utc.</exception>
36+
public override void Set(DbCommand st, object value, int index)
37+
{
38+
if (value is DateTime)
39+
{
40+
var v = (DateTime) value;
41+
if (v.Kind != DateTimeKind.Utc) throw new ArgumentException("Kind is NOT Utc", nameof(value));
42+
}
43+
44+
base.Set(st, value, index);
45+
}
46+
47+
/// <summary>
48+
/// Compares two DateTime object and also compare its Kind which is not used by the .net framework DateTime.Equals implementation.
49+
/// </summary>
50+
/// <param name="x"></param>
51+
/// <param name="y"></param>
52+
/// <returns></returns>
53+
public override bool IsEqual(object x, object y)
54+
{
55+
return base.IsEqual(x, y) && ((DateTime) x).Kind == ((DateTime) y).Kind;
56+
}
57+
58+
/// <summary>
59+
/// Retrieve the string representation of the timestamp object. This is in the following format:
60+
/// <code>
61+
/// 2011-01-27T14:50:59.6220000Z
62+
/// </code>
63+
/// </summary>
64+
public override string ToString(object val)
65+
{
66+
return ((DateTime) val).ToString("o");
67+
}
68+
}
69+
}

src/NHibernate/Type/TypeFactory.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ private static void RegisterBuiltInTypes()
262262

263263
RegisterType(NHibernateUtil.Date, new[] { "date" });
264264
RegisterType(NHibernateUtil.Timestamp, new[] { "timestamp" });
265+
RegisterType(NHibernateUtil.TimestampUtc, new[] { "timestamputc" });
265266
RegisterType(NHibernateUtil.DbTimestamp, new[] { "dbtimestamp" });
266267
RegisterType(NHibernateUtil.Time, new[] { "time" });
267268
RegisterType(NHibernateUtil.TrueFalse, new[] { "true_false" });

0 commit comments

Comments
 (0)