Skip to content

Commit 871f87d

Browse files
committed
Replace ISystemClock with TimeProvider
1 parent 5d8badf commit 871f87d

34 files changed

+181
-161
lines changed

src/Examples/DapperExample/Definitions/TodoItemDefinition.cs

+6-6
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ namespace DapperExample.Definitions;
1111
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
1212
public sealed class TodoItemDefinition : JsonApiResourceDefinition<TodoItem, long>
1313
{
14-
private readonly IClock _clock;
14+
private readonly TimeProvider _timeProvider;
1515

16-
public TodoItemDefinition(IResourceGraph resourceGraph, IClock clock)
16+
public TodoItemDefinition(IResourceGraph resourceGraph, TimeProvider timeProvider)
1717
: base(resourceGraph)
1818
{
19-
ArgumentNullException.ThrowIfNull(clock);
19+
ArgumentNullException.ThrowIfNull(timeProvider);
2020

21-
_clock = clock;
21+
_timeProvider = timeProvider;
2222
}
2323

2424
public override SortExpression OnApplySort(SortExpression? existingSort)
@@ -38,11 +38,11 @@ public override Task OnWritingAsync(TodoItem resource, WriteOperationKind writeO
3838
{
3939
if (writeOperation == WriteOperationKind.CreateResource)
4040
{
41-
resource.CreatedAt = _clock.UtcNow;
41+
resource.CreatedAt = _timeProvider.GetUtcNow();
4242
}
4343
else if (writeOperation == WriteOperationKind.UpdateResource)
4444
{
45-
resource.LastModifiedAt = _clock.UtcNow;
45+
resource.LastModifiedAt = _timeProvider.GetUtcNow();
4646
}
4747

4848
return Task.CompletedTask;

src/Examples/DapperExample/IClock.cs

-6
This file was deleted.

src/Examples/DapperExample/Program.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,12 @@
1111
using JsonApiDotNetCore.Repositories;
1212
using Microsoft.EntityFrameworkCore;
1313
using Microsoft.EntityFrameworkCore.Diagnostics;
14-
using Microsoft.Extensions.DependencyInjection.Extensions;
1514

1615
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
1716

1817
// Add services to the container.
1918

20-
builder.Services.TryAddSingleton<IClock, SystemClock>();
19+
builder.Services.AddSingleton<TimeProvider>();
2120

2221
DatabaseProvider databaseProvider = GetDatabaseProvider(builder.Configuration);
2322
string? connectionString = builder.Configuration.GetConnectionString($"DapperExample{databaseProvider}");

src/Examples/DapperExample/SystemClock.cs

-6
This file was deleted.

src/Examples/JsonApiDotNetCoreExample/Definitions/TodoItemDefinition.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace JsonApiDotNetCoreExample.Definitions;
1212
public sealed class TodoItemDefinition(IResourceGraph resourceGraph, TimeProvider timeProvider)
1313
: JsonApiResourceDefinition<TodoItem, long>(resourceGraph)
1414
{
15-
private readonly Func<DateTimeOffset> _getUtcNow = timeProvider.GetUtcNow;
15+
private readonly TimeProvider _timeProvider = timeProvider;
1616

1717
public override SortExpression OnApplySort(SortExpression? existingSort)
1818
{
@@ -31,11 +31,11 @@ public override Task OnWritingAsync(TodoItem resource, WriteOperationKind writeO
3131
{
3232
if (writeOperation == WriteOperationKind.CreateResource)
3333
{
34-
resource.CreatedAt = _getUtcNow();
34+
resource.CreatedAt = _timeProvider.GetUtcNow();
3535
}
3636
else if (writeOperation == WriteOperationKind.UpdateResource)
3737
{
38-
resource.LastModifiedAt = _getUtcNow();
38+
resource.LastModifiedAt = _timeProvider.GetUtcNow();
3939
}
4040

4141
return Task.CompletedTask;

test/DapperTests/IntegrationTests/DapperTestContext.cs

+3-8
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,17 @@
44
using DapperExample.Models;
55
using DapperExample.Repositories;
66
using DapperExample.TranslationToSql.DataModel;
7-
using FluentAssertions.Common;
8-
using FluentAssertions.Extensions;
97
using JetBrains.Annotations;
108
using JsonApiDotNetCore.Configuration;
119
using Microsoft.AspNetCore.Hosting;
1210
using Microsoft.AspNetCore.Mvc.Testing;
1311
using Microsoft.EntityFrameworkCore;
1412
using Microsoft.EntityFrameworkCore.Metadata;
1513
using Microsoft.Extensions.DependencyInjection;
14+
using Microsoft.Extensions.DependencyInjection.Extensions;
1615
using Microsoft.Extensions.Logging;
1716
using TestBuildingBlocks;
1817
using Xunit.Abstractions;
19-
using IClock = DapperExample.IClock;
2018

2119
namespace DapperTests.IntegrationTests;
2220

@@ -29,7 +27,7 @@ public sealed class DapperTestContext : IntegrationTest
2927
EXEC sp_MSForEachTable 'ALTER TABLE ? CHECK CONSTRAINT ALL';
3028
""";
3129

32-
public static readonly DateTimeOffset FrozenTime = 29.September(2018).At(16, 41, 56).AsUtc().ToDateTimeOffset();
30+
public static readonly DateTimeOffset FrozenTime = DefaultDateTimeUtc;
3331

3432
private readonly Lazy<WebApplicationFactory<TodoItem>> _lazyFactory;
3533
private ITestOutputHelper? _testOutputHelper;
@@ -82,10 +80,7 @@ private WebApplicationFactory<TodoItem> CreateFactory()
8280

8381
builder.ConfigureServices(services =>
8482
{
85-
services.AddSingleton<IClock>(new FrozenClock
86-
{
87-
UtcNow = FrozenTime
88-
});
83+
services.Replace(ServiceDescriptor.Singleton<TimeProvider>(new FrozenTimeProvider(FrozenTime)));
8984

9085
ServiceDescriptor scopedCaptureStore = services.Single(descriptor => descriptor.ImplementationType == typeof(SqlCaptureStore));
9186
services.Remove(scopedCaptureStore);

test/DapperTests/IntegrationTests/FrozenClock.cs

-11
This file was deleted.

test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs

-2
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ public AtomicCreateResourceTests(IntegrationTestContext<TestableStartup<Operatio
2626
testContext.UseController<MusicTracksController>();
2727
testContext.UseController<PlaylistsController>();
2828

29-
testContext.ConfigureServices(services => services.AddSingleton<ISystemClock, FrozenSystemClock>());
30-
3129
var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService<IJsonApiOptions>();
3230
options.AllowUnknownFieldsInRequestBody = false;
3331
}

test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs

-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ public AtomicCreateResourceWithClientGeneratedIdTests(IntegrationTestContext<Tes
2828
services.AddResourceDefinition<AssignIdToTextLanguageDefinition>();
2929

3030
services.AddSingleton<ResourceDefinitionHitCounter>();
31-
services.AddSingleton<ISystemClock, FrozenSystemClock>();
3231
});
3332
}
3433

test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/DateMustBeInThePastAttribute.cs

+4-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
using System.Reflection;
33
using JsonApiDotNetCore.Resources;
44
using Microsoft.Extensions.DependencyInjection;
5-
using TestBuildingBlocks;
65

76
namespace JsonApiDotNetCoreTests.IntegrationTests.AtomicOperations;
87

@@ -20,9 +19,11 @@ internal sealed class DateMustBeInThePastAttribute : ValidationAttribute
2019
if (propertyInfo.PropertyType == typeof(DateTimeOffset) || propertyInfo.PropertyType == typeof(DateTimeOffset?))
2120
{
2221
var typedValue = (DateTimeOffset?)propertyInfo.GetValue(validationContext.ObjectInstance);
23-
var systemClock = validationContext.GetRequiredService<ISystemClock>();
2422

25-
if (typedValue >= systemClock.UtcNow)
23+
var timeProvider = validationContext.GetRequiredService<TimeProvider>();
24+
DateTimeOffset utcNow = timeProvider.GetUtcNow();
25+
26+
if (typedValue >= utcNow)
2627
{
2728
return new ValidationResult($"{validationContext.MemberName} must be in the past.");
2829
}

test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResourceMetaTests.cs

-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ public AtomicResourceMetaTests(IntegrationTestContext<TestableStartup<Operations
2727
services.AddResourceDefinition<TextLanguageMetaDefinition>();
2828

2929
services.AddSingleton<ResourceDefinitionHitCounter>();
30-
services.AddSingleton<ISystemClock, FrozenSystemClock>();
3130
});
3231

3332
var hitCounter = _testContext.Factory.Services.GetRequiredService<ResourceDefinitionHitCounter>();

test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ModelStateValidation/AtomicModelStateValidationTests.cs

+3-4
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@ public AtomicModelStateValidationTests(IntegrationTestContext<TestableStartup<Op
1717
{
1818
_testContext = testContext;
1919

20-
_testContext.ConfigureServices(services => services.AddSingleton<ISystemClock, FrozenSystemClock>());
21-
2220
testContext.UseController<OperationsController>();
2321
}
2422

@@ -74,7 +72,8 @@ public async Task Cannot_create_resource_with_multiple_violations()
7472
public async Task Cannot_create_resource_when_violation_from_custom_ValidationAttribute()
7573
{
7674
// Arrange
77-
var clock = _testContext.Factory.Services.GetRequiredService<ISystemClock>();
75+
var timeProvider = _testContext.Factory.Services.GetRequiredService<TimeProvider>();
76+
DateTimeOffset utcNow = timeProvider.GetUtcNow();
7877

7978
var requestBody = new
8079
{
@@ -90,7 +89,7 @@ public async Task Cannot_create_resource_when_violation_from_custom_ValidationAt
9089
{
9190
title = "some",
9291
lengthInSeconds = 120,
93-
releasedAt = clock.UtcNow.AddDays(1)
92+
releasedAt = utcNow.AddDays(1)
9493
}
9594
}
9695
}

test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/QueryStrings/AtomicQueryStringTests.cs

+6-10
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,7 @@ public AtomicQueryStringTests(IntegrationTestContext<TestableStartup<OperationsD
2020
testContext.UseController<OperationsController>();
2121
testContext.UseController<MusicTracksController>();
2222

23-
testContext.ConfigureServices(services =>
24-
{
25-
services.AddResourceDefinition<MusicTrackReleaseDefinition>();
26-
27-
services.AddSingleton<ISystemClock, FrozenSystemClock>();
28-
});
23+
testContext.ConfigureServices(services => services.AddResourceDefinition<MusicTrackReleaseDefinition>());
2924
}
3025

3126
[Fact]
@@ -272,12 +267,13 @@ public async Task Cannot_use_sparse_fieldset_at_operations_endpoint()
272267
public async Task Can_use_Queryable_handler_at_resource_endpoint()
273268
{
274269
// Arrange
275-
var clock = _testContext.Factory.Services.GetRequiredService<ISystemClock>();
270+
var timeProvider = _testContext.Factory.Services.GetRequiredService<TimeProvider>();
271+
DateTimeOffset utcNow = timeProvider.GetUtcNow();
276272

277273
List<MusicTrack> musicTracks = _fakers.MusicTrack.GenerateList(3);
278-
musicTracks[0].ReleasedAt = clock.UtcNow.AddMonths(5);
279-
musicTracks[1].ReleasedAt = clock.UtcNow.AddMonths(-5);
280-
musicTracks[2].ReleasedAt = clock.UtcNow.AddMonths(-1);
274+
musicTracks[0].ReleasedAt = utcNow.AddMonths(5);
275+
musicTracks[1].ReleasedAt = utcNow.AddMonths(-5);
276+
musicTracks[2].ReleasedAt = utcNow.AddMonths(-1);
281277

282278
await _testContext.RunOnDatabaseAsync(async dbContext =>
283279
{

test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/QueryStrings/MusicTrackReleaseDefinition.cs

+6-6
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,20 @@
22
using JsonApiDotNetCore.Configuration;
33
using JsonApiDotNetCore.Resources;
44
using Microsoft.Extensions.Primitives;
5-
using TestBuildingBlocks;
65

76
namespace JsonApiDotNetCoreTests.IntegrationTests.AtomicOperations.QueryStrings;
87

98
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
109
public sealed class MusicTrackReleaseDefinition : JsonApiResourceDefinition<MusicTrack, Guid>
1110
{
12-
private readonly ISystemClock _systemClock;
11+
private readonly TimeProvider _timeProvider;
1312

14-
public MusicTrackReleaseDefinition(IResourceGraph resourceGraph, ISystemClock systemClock)
13+
public MusicTrackReleaseDefinition(IResourceGraph resourceGraph, TimeProvider timeProvider)
1514
: base(resourceGraph)
1615
{
17-
ArgumentNullException.ThrowIfNull(systemClock);
16+
ArgumentNullException.ThrowIfNull(timeProvider);
1817

19-
_systemClock = systemClock;
18+
_timeProvider = timeProvider;
2019
}
2120

2221
public override QueryStringParameterHandlers<MusicTrack> OnRegisterQueryableHandlersForQueryStringParameters()
@@ -33,7 +32,8 @@ private IQueryable<MusicTrack> FilterOnRecentlyReleased(IQueryable<MusicTrack> s
3332

3433
if (bool.Parse(parameterValue.ToString()))
3534
{
36-
tracks = tracks.Where(musicTrack => musicTrack.ReleasedAt < _systemClock.UtcNow && musicTrack.ReleasedAt > _systemClock.UtcNow.AddMonths(-3));
35+
DateTimeOffset utcNow = _timeProvider.GetUtcNow();
36+
tracks = tracks.Where(musicTrack => musicTrack.ReleasedAt < utcNow && musicTrack.ReleasedAt > utcNow.AddMonths(-3));
3737
}
3838

3939
return tracks;

test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs

-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ public AtomicUpdateResourceTests(IntegrationTestContext<TestableStartup<Operatio
2929
services.AddResourceDefinition<ImplicitlyChangingTextLanguageDefinition>();
3030

3131
services.AddSingleton<ResourceDefinitionHitCounter>();
32-
services.AddSingleton<ISystemClock, FrozenSystemClock>();
3332
});
3433

3534
var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService<IJsonApiOptions>();

test/JsonApiDotNetCoreTests/IntegrationTests/ContentNegotiation/CustomExtensions/CustomExtensionsContentTypeTests.cs

+10-3
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
using System.Net;
22
using System.Net.Http.Headers;
33
using FluentAssertions;
4+
using FluentAssertions.Extensions;
45
using JsonApiDotNetCore.Configuration;
56
using JsonApiDotNetCore.Middleware;
67
using JsonApiDotNetCore.Serialization.Objects;
78
using JsonApiDotNetCore.Serialization.Request.Adapters;
89
using JsonApiDotNetCore.Serialization.Response;
910
using Microsoft.Extensions.DependencyInjection;
11+
using Microsoft.Extensions.DependencyInjection.Extensions;
1012
using TestBuildingBlocks;
1113
using Xunit;
1214

1315
namespace JsonApiDotNetCoreTests.IntegrationTests.ContentNegotiation.CustomExtensions;
1416

1517
public sealed class CustomExtensionsContentTypeTests : IClassFixture<IntegrationTestContext<TestableStartup<PolicyDbContext>, PolicyDbContext>>
1618
{
19+
private static readonly DateTimeOffset CurrentTime = 31.December(2024).At(21, 53, 40).AsUtc();
1720
private readonly IntegrationTestContext<TestableStartup<PolicyDbContext>, PolicyDbContext> _testContext;
1821

1922
public CustomExtensionsContentTypeTests(IntegrationTestContext<TestableStartup<PolicyDbContext>, PolicyDbContext> testContext)
@@ -39,6 +42,9 @@ public CustomExtensionsContentTypeTests(IntegrationTestContext<TestableStartup<P
3942
});
4043
});
4144

45+
testContext.PostConfigureServices(services => services.Replace(
46+
ServiceDescriptor.Singleton<TimeProvider>(new FrozenTimeProvider(CurrentTime, TimeZoneInfo.FindSystemTimeZoneById("Tokyo Standard Time")))));
47+
4248
var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService<IJsonApiOptions>();
4349
options.IncludeExtensions(ServerTimeMediaTypeExtension.ServerTime, ServerTimeMediaTypeExtension.RelaxedServerTime);
4450
}
@@ -108,7 +114,8 @@ public async Task Permits_JsonApi_ContentType_header_with_ServerTime_extension()
108114
httpResponse.Content.Headers.ContentType.ShouldNotBeNull();
109115
httpResponse.Content.Headers.ContentType.ToString().Should().Be(ServerTimeMediaTypes.ServerTime.ToString());
110116

111-
responseDocument.Meta.ShouldContainKey("localServerTime");
117+
responseDocument.Meta.ShouldContainKey("localServerTime").With(time =>
118+
time.ShouldNotBeNull().ToString().Should().Be("2025-01-01T06:53:40.0000000+09:00"));
112119
}
113120

114121
[Fact]
@@ -143,7 +150,7 @@ public async Task Permits_JsonApi_ContentType_header_with_relaxed_ServerTime_ext
143150
httpResponse.Content.Headers.ContentType.ShouldNotBeNull();
144151
httpResponse.Content.Headers.ContentType.ToString().Should().Be(ServerTimeMediaTypes.RelaxedServerTime.ToString());
145152

146-
responseDocument.Meta.ShouldContainKey("utcServerTime");
153+
responseDocument.Meta.ShouldContainKey("utcServerTime").With(time => time.ShouldNotBeNull().ToString().Should().Be("2024-12-31T21:53:40.0000000Z"));
147154
}
148155

149156
[Fact]
@@ -185,7 +192,7 @@ public async Task Permits_JsonApi_ContentType_header_with_AtomicOperations_and_S
185192
httpResponse.Content.Headers.ContentType.ShouldNotBeNull();
186193
httpResponse.Content.Headers.ContentType.ToString().Should().Be(ServerTimeMediaTypes.AtomicOperationsWithServerTime.ToString());
187194

188-
responseDocument.Meta.ShouldContainKey("utcServerTime");
195+
responseDocument.Meta.ShouldContainKey("utcServerTime").With(time => time.ShouldNotBeNull().ToString().Should().Be("2024-12-31T21:53:40.0000000Z"));
189196
}
190197

191198
[Fact]

test/JsonApiDotNetCoreTests/IntegrationTests/ContentNegotiation/CustomExtensions/ServerTimeResponseMeta.cs

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
1+
using System.Globalization;
12
using JsonApiDotNetCore.Middleware;
23
using JsonApiDotNetCore.Serialization.Response;
34

45
namespace JsonApiDotNetCoreTests.IntegrationTests.ContentNegotiation.CustomExtensions;
56

6-
internal sealed class ServerTimeResponseMeta(IJsonApiRequest request, RequestDocumentStore documentStore) : IResponseMeta
7+
internal sealed class ServerTimeResponseMeta(IJsonApiRequest request, RequestDocumentStore documentStore, TimeProvider timeProvider) : IResponseMeta
78
{
9+
private readonly IJsonApiRequest _request = request;
810
private readonly RequestDocumentStore _documentStore = documentStore;
11+
private readonly TimeProvider _timeProvider = timeProvider;
912

1013
public IDictionary<string, object?>? GetMeta()
1114
{
12-
if (request.Extensions.Contains(ServerTimeMediaTypeExtension.ServerTime) || request.Extensions.Contains(ServerTimeMediaTypeExtension.RelaxedServerTime))
15+
if (_request.Extensions.Contains(ServerTimeMediaTypeExtension.ServerTime) ||
16+
_request.Extensions.Contains(ServerTimeMediaTypeExtension.RelaxedServerTime))
1317
{
1418
if (_documentStore.Document is not { Meta: not null } || !_documentStore.Document.Meta.TryGetValue("useLocalTime", out object? useLocalTimeValue) ||
1519
useLocalTimeValue == null || !bool.TryParse(useLocalTimeValue.ToString(), out bool useLocalTime))
@@ -20,11 +24,11 @@ internal sealed class ServerTimeResponseMeta(IJsonApiRequest request, RequestDoc
2024
return useLocalTime
2125
? new Dictionary<string, object?>
2226
{
23-
["localServerTime"] = DateTime.Now.ToString("O")
27+
["localServerTime"] = _timeProvider.GetLocalNow().ToString("O", CultureInfo.InvariantCulture)
2428
}
2529
: new Dictionary<string, object?>
2630
{
27-
["utcServerTime"] = DateTime.UtcNow.ToString("O")
31+
["utcServerTime"] = _timeProvider.GetUtcNow().UtcDateTime.ToString("O", CultureInfo.InvariantCulture)
2832
};
2933
}
3034

0 commit comments

Comments
 (0)