Skip to content

Commit 9ef2b37

Browse files
authored
Ensure dates read from the database are treated as local when constructing entities (#18989)
* Ensure dates read from the database are treated as local when constructing entities. * Fixed typos in comments.
1 parent 516d62a commit 9ef2b37

File tree

7 files changed

+65
-30
lines changed

7 files changed

+65
-30
lines changed

src/Umbraco.Cms.Api.Management/Factories/DocumentVersionPresentationFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public async Task<DocumentVersionItemResponseModel> CreateAsync(ContentVersionMe
2626
new ReferenceByIdModel(_entityService.GetKey(contentVersion.ContentTypeId, UmbracoObjectTypes.DocumentType)
2727
.Result),
2828
new ReferenceByIdModel(await _userIdKeyResolver.GetAsync(contentVersion.UserId)),
29-
new DateTimeOffset(contentVersion.VersionDate, TimeSpan.Zero), // todo align with datetime offset rework
29+
new DateTimeOffset(contentVersion.VersionDate),
3030
contentVersion.CurrentPublishedVersion,
3131
contentVersion.CurrentDraftVersion,
3232
contentVersion.PreventCleanup);

src/Umbraco.Core/Models/ContentVersionMeta.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public ContentVersionMeta(
3737

3838
public int UserId { get; }
3939

40-
public DateTime VersionDate { get; }
40+
public DateTime VersionDate { get; private set; }
4141

4242
public bool CurrentPublishedVersion { get; }
4343

@@ -47,5 +47,7 @@ public ContentVersionMeta(
4747

4848
public string? Username { get; }
4949

50+
public void SpecifyVersionDateKind(DateTimeKind kind) => VersionDate = DateTime.SpecifyKind(VersionDate, kind);
51+
5052
public override string ToString() => $"ContentVersionMeta(versionId: {VersionId}, versionDate: {VersionDate:s}";
5153
}

src/Umbraco.Infrastructure/Persistence/Factories/ContentBaseFactory.cs

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,11 @@ public static Content BuildEntity(DocumentDto dto, IContentType? contentType)
3939

4040
content.CreatorId = nodeDto.UserId ?? Constants.Security.UnknownUserId;
4141
content.WriterId = contentVersionDto.UserId ?? Constants.Security.UnknownUserId;
42-
content.CreateDate = nodeDto.CreateDate;
43-
content.UpdateDate = contentVersionDto.VersionDate;
42+
43+
// Dates stored in the database are local server time, but for SQL Server, will be considered
44+
// as DateTime.Kind = Utc. Fix this so we are consistent when later mapping to DataTimeOffset.
45+
content.CreateDate = DateTime.SpecifyKind(nodeDto.CreateDate, DateTimeKind.Local);
46+
content.UpdateDate = DateTime.SpecifyKind(contentVersionDto.VersionDate, DateTimeKind.Local);
4447

4548
content.Published = dto.Published;
4649
content.Edited = dto.Edited;
@@ -52,7 +55,7 @@ public static Content BuildEntity(DocumentDto dto, IContentType? contentType)
5255
content.PublishedVersionId = publishedVersionDto.Id;
5356
if (dto.Published)
5457
{
55-
content.PublishDate = publishedVersionDto.ContentVersionDto.VersionDate;
58+
content.PublishDate = DateTime.SpecifyKind(publishedVersionDto.ContentVersionDto.VersionDate, DateTimeKind.Local);
5659
content.PublishName = publishedVersionDto.ContentVersionDto.Text;
5760
content.PublisherId = publishedVersionDto.ContentVersionDto.UserId;
5861
}
@@ -71,7 +74,7 @@ public static Content BuildEntity(DocumentDto dto, IContentType? contentType)
7174
}
7275

7376
/// <summary>
74-
/// Builds an IMedia item from a dto and content type.
77+
/// Builds a Media item from a dto and content type.
7578
/// </summary>
7679
public static Core.Models.Media BuildEntity(ContentDto dto, IMediaType? contentType)
7780
{
@@ -97,8 +100,8 @@ public static Core.Models.Media BuildEntity(ContentDto dto, IMediaType? contentT
97100

98101
content.CreatorId = nodeDto.UserId ?? Constants.Security.UnknownUserId;
99102
content.WriterId = contentVersionDto.UserId ?? Constants.Security.UnknownUserId;
100-
content.CreateDate = nodeDto.CreateDate;
101-
content.UpdateDate = contentVersionDto.VersionDate;
103+
content.CreateDate = DateTime.SpecifyKind(nodeDto.CreateDate, DateTimeKind.Local);
104+
content.UpdateDate = DateTime.SpecifyKind(contentVersionDto.VersionDate, DateTimeKind.Local);
102105

103106
// reset dirty initial properties (U4-1946)
104107
content.ResetDirtyProperties(false);
@@ -111,7 +114,7 @@ public static Core.Models.Media BuildEntity(ContentDto dto, IMediaType? contentT
111114
}
112115

113116
/// <summary>
114-
/// Builds an IMedia item from a dto and content type.
117+
/// Builds a Member item from a dto and member type.
115118
/// </summary>
116119
public static Member BuildEntity(MemberDto dto, IMemberType? contentType)
117120
{
@@ -126,7 +129,9 @@ public static Member BuildEntity(MemberDto dto, IMemberType? contentType)
126129

127130
content.Id = dto.NodeId;
128131
content.SecurityStamp = dto.SecurityStampToken;
129-
content.EmailConfirmedDate = dto.EmailConfirmedDate;
132+
content.EmailConfirmedDate = dto.EmailConfirmedDate.HasValue
133+
? DateTime.SpecifyKind(dto.EmailConfirmedDate.Value, DateTimeKind.Local)
134+
: null;
130135
content.PasswordConfiguration = dto.PasswordConfig;
131136
content.Key = nodeDto.UniqueId;
132137
content.VersionId = contentVersionDto.Id;
@@ -140,14 +145,20 @@ public static Member BuildEntity(MemberDto dto, IMemberType? contentType)
140145

141146
content.CreatorId = nodeDto.UserId ?? Constants.Security.UnknownUserId;
142147
content.WriterId = contentVersionDto.UserId ?? Constants.Security.UnknownUserId;
143-
content.CreateDate = nodeDto.CreateDate;
144-
content.UpdateDate = contentVersionDto.VersionDate;
148+
content.CreateDate = DateTime.SpecifyKind(nodeDto.CreateDate, DateTimeKind.Local);
149+
content.UpdateDate = DateTime.SpecifyKind(contentVersionDto.VersionDate, DateTimeKind.Local);
145150
content.FailedPasswordAttempts = dto.FailedPasswordAttempts ?? default;
146151
content.IsLockedOut = dto.IsLockedOut;
147152
content.IsApproved = dto.IsApproved;
148-
content.LastLoginDate = dto.LastLoginDate;
149-
content.LastLockoutDate = dto.LastLockoutDate;
150-
content.LastPasswordChangeDate = dto.LastPasswordChangeDate;
153+
content.LastLockoutDate = dto.LastLockoutDate.HasValue
154+
? DateTime.SpecifyKind(dto.LastLockoutDate.Value, DateTimeKind.Local)
155+
: null;
156+
content.LastLoginDate = dto.LastLoginDate.HasValue
157+
? DateTime.SpecifyKind(dto.LastLoginDate.Value, DateTimeKind.Local)
158+
: null;
159+
content.LastPasswordChangeDate = dto.LastPasswordChangeDate.HasValue
160+
? DateTime.SpecifyKind(dto.LastPasswordChangeDate.Value, DateTimeKind.Local)
161+
: null;
151162

152163
// reset dirty initial properties (U4-1946)
153164
content.ResetDirtyProperties(false);
@@ -186,7 +197,7 @@ public static DocumentDto BuildDto(IContent entity, Guid objectType)
186197
new ContentScheduleDto
187198
{
188199
Action = x.Action.ToString(),
189-
Date = x.Date,
200+
Date = DateTime.SpecifyKind(x.Date, DateTimeKind.Local),
190201
NodeId = entity.Id,
191202
LanguageId = languageRepository.GetIdByIsoCode(x.Culture, false),
192203
Id = x.Id,
@@ -261,7 +272,7 @@ private static NodeDto BuildNodeDto(IContentBase entity, Guid objectType)
261272
UserId = entity.CreatorId,
262273
Text = entity.Name,
263274
NodeObjectType = objectType,
264-
CreateDate = entity.CreateDate,
275+
CreateDate = DateTime.SpecifyKind(entity.CreateDate, DateTimeKind.Local),
265276
};
266277

267278
return dto;
@@ -275,7 +286,7 @@ private static ContentVersionDto BuildContentVersionDto(IContentBase entity, Con
275286
{
276287
Id = entity.VersionId,
277288
NodeId = entity.Id,
278-
VersionDate = entity.UpdateDate,
289+
VersionDate = DateTime.SpecifyKind(entity.UpdateDate, DateTimeKind.Local),
279290
UserId = entity.WriterId,
280291
Current = true, // always building the current one
281292
Text = entity.Name,

src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,25 @@ public static IUser BuildEntity(
3939
user.Language = dto.UserLanguage;
4040
user.SecurityStamp = dto.SecurityStampToken;
4141
user.FailedPasswordAttempts = dto.FailedLoginAttempts ?? 0;
42-
user.LastLockoutDate = dto.LastLockoutDate;
43-
user.LastLoginDate = dto.LastLoginDate;
44-
user.LastPasswordChangeDate = dto.LastPasswordChangeDate;
45-
user.CreateDate = dto.CreateDate;
46-
user.UpdateDate = dto.UpdateDate;
4742
user.Avatar = dto.Avatar;
4843
user.EmailConfirmedDate = dto.EmailConfirmedDate;
4944
user.InvitedDate = dto.InvitedDate;
5045
user.Kind = (UserKind)dto.Kind;
5146

47+
// Dates stored in the database are local server time, but for SQL Server, will be considered
48+
// as DateTime.Kind = Utc. Fix this so we are consistent when later mapping to DataTimeOffset.
49+
user.LastLockoutDate = dto.LastLockoutDate.HasValue
50+
? DateTime.SpecifyKind(dto.LastLockoutDate.Value, DateTimeKind.Local)
51+
: null;
52+
user.LastLoginDate = dto.LastLoginDate.HasValue
53+
? DateTime.SpecifyKind(dto.LastLoginDate.Value, DateTimeKind.Local)
54+
: null;
55+
user.LastPasswordChangeDate = dto.LastPasswordChangeDate.HasValue
56+
? DateTime.SpecifyKind(dto.LastPasswordChangeDate.Value, DateTimeKind.Local)
57+
: null;
58+
user.CreateDate = DateTime.SpecifyKind(dto.CreateDate, DateTimeKind.Local);
59+
user.UpdateDate = DateTime.SpecifyKind(dto.UpdateDate, DateTimeKind.Local);
60+
5261
// reset dirty initial properties (U4-1946)
5362
user.ResetDirtyProperties(false);
5463

src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public IEnumerable<IAuditItem> Get(AuditType type, IQuery<IAuditItem> query)
2929

3030
List<LogDto>? dtos = Database.Fetch<LogDto>(sql);
3131

32-
return dtos.Select(x => new AuditItem(x.NodeId, Enum<AuditType>.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters, x.Datestamp)).ToList();
32+
return dtos.Select(x => new AuditItem(x.NodeId, Enum<AuditType>.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters, DateTime.SpecifyKind(x.Datestamp, DateTimeKind.Local))).ToList();
3333
}
3434

3535
public void CleanLogs(int maximumAgeOfLogsInMinutes)
@@ -104,12 +104,12 @@ public IEnumerable<IAuditItem> GetPagedResultsByQuery(
104104
totalRecords = page.TotalItems;
105105

106106
var items = page.Items.Select(
107-
dto => new AuditItem(dto.NodeId, Enum<AuditType>.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters, dto.Datestamp)).ToList();
107+
dto => new AuditItem(dto.NodeId, Enum<AuditType>.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters, DateTime.SpecifyKind(dto.Datestamp, DateTimeKind.Local))).ToList();
108108

109109
// map the DateStamp
110110
for (var i = 0; i < items.Count; i++)
111111
{
112-
items[i].CreateDate = page.Items[i].Datestamp;
112+
items[i].CreateDate = DateTime.SpecifyKind(page.Items[i].Datestamp, DateTimeKind.Local);
113113
}
114114

115115
return items;
@@ -149,7 +149,7 @@ protected override void PersistUpdatedItem(IAuditItem entity) =>
149149
LogDto? dto = Database.First<LogDto>(sql);
150150
return dto == null
151151
? null
152-
: new AuditItem(dto.NodeId, Enum<AuditType>.Parse(dto.Header), dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters, dto.Datestamp);
152+
: new AuditItem(dto.NodeId, Enum<AuditType>.Parse(dto.Header), dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters, DateTime.SpecifyKind(dto.Datestamp, DateTimeKind.Local));
153153
}
154154

155155
protected override IEnumerable<IAuditItem> PerformGetAll(params int[]? ids) => throw new NotImplementedException();
@@ -162,7 +162,7 @@ protected override IEnumerable<IAuditItem> PerformGetByQuery(IQuery<IAuditItem>
162162

163163
List<LogDto>? dtos = Database.Fetch<LogDto>(sql);
164164

165-
return dtos.Select(x => new AuditItem(x.NodeId, Enum<AuditType>.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters, x.Datestamp)).ToList();
165+
return dtos.Select(x => new AuditItem(x.NodeId, Enum<AuditType>.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters, DateTime.SpecifyKind(x.Datestamp, DateTimeKind.Local))).ToList();
166166
}
167167

168168
protected override Sql<ISqlContext> GetBaseQuery(bool isCount)

src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -400,15 +400,17 @@ private void SetVariations(Content? content, IDictionary<int, List<ContentVariat
400400
{
401401
foreach (ContentVariation v in contentVariation)
402402
{
403-
content.SetCultureInfo(v.Culture, v.Name, v.Date);
403+
content.SetCultureInfo(v.Culture, v.Name, DateTime.SpecifyKind(v.Date, DateTimeKind.Local));
404404
}
405405
}
406406

407+
// Dates stored in the database are local server time, but for SQL Server, will be considered
408+
// as DateTime.Kind = Utc. Fix this so we are consistent when later mapping to DataTimeOffset.
407409
if (content.PublishedState is PublishedState.Published && content.PublishedVersionId > 0 && contentVariations.TryGetValue(content.PublishedVersionId, out contentVariation))
408410
{
409411
foreach (ContentVariation v in contentVariation)
410412
{
411-
content.SetPublishInfo(v.Culture, v.Name, v.Date);
413+
content.SetPublishInfo(v.Culture, v.Name, DateTime.SpecifyKind(v.Date, DateTimeKind.Local));
412414
}
413415
}
414416

src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentVersionRepository.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Data;
12
using NPoco;
23
using Umbraco.Cms.Core;
34
using Umbraco.Cms.Core.Models;
@@ -98,6 +99,16 @@ public DocumentVersionRepository(IScopeAccessor scopeAccessor) =>
9899
Page<ContentVersionMeta>? page =
99100
_scopeAccessor.AmbientScope?.Database.Page<ContentVersionMeta>(pageIndex + 1, pageSize, query);
100101

102+
// Dates stored in the database are local server time, but for SQL Server, will be considered
103+
// as DateTime.Kind = Utc. Fix this so we are consistent when later mapping to DataTimeOffset.
104+
if (page is not null)
105+
{
106+
foreach (ContentVersionMeta item in page.Items)
107+
{
108+
item.SpecifyVersionDateKind(DateTimeKind.Local);
109+
}
110+
}
111+
101112
totalRecords = page?.TotalItems ?? 0;
102113

103114
return page?.Items;

0 commit comments

Comments
 (0)