Skip to content

Commit ff9c287

Browse files
author
Bart Koelman
committed
Added injectable ISystemClock to AppDbContext, which is used by User resource to set LastPasswordChange
1 parent 29e6edd commit ff9c287

32 files changed

+407
-99
lines changed

benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.ComponentModel.Design;
34
using BenchmarkDotNet.Attributes;
45
using JsonApiDotNetCore.Configuration;
56
using JsonApiDotNetCore.Internal.Contracts;
@@ -37,7 +38,7 @@ public JsonApiDeserializerBenchmarks()
3738
IResourceGraph resourceGraph = DependencyFactory.CreateResourceGraph(options);
3839
var targetedFields = new TargetedFields();
3940

40-
_jsonApiDeserializer = new RequestDeserializer(resourceGraph, targetedFields);
41+
_jsonApiDeserializer = new RequestDeserializer(resourceGraph, new ServiceContainer(), targetedFields);
4142
}
4243

4344
[Benchmark]

src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
using System;
12
using JsonApiDotNetCoreExample.Models;
3+
using Microsoft.AspNetCore.Authentication;
24
using Microsoft.EntityFrameworkCore;
35

46
namespace JsonApiDotNetCoreExample.Data
57
{
68
public sealed class AppDbContext : DbContext
79
{
10+
public ISystemClock SystemClock { get; }
11+
812
public DbSet<TodoItem> TodoItems { get; set; }
913
public DbSet<Passport> Passports { get; set; }
1014
public DbSet<Person> People { get; set; }
@@ -17,7 +21,10 @@ public sealed class AppDbContext : DbContext
1721
public DbSet<ArticleTag> ArticleTags { get; set; }
1822
public DbSet<Tag> Tags { get; set; }
1923

20-
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
24+
public AppDbContext(DbContextOptions<AppDbContext> options, ISystemClock systemClock) : base(options)
25+
{
26+
SystemClock = systemClock ?? throw new ArgumentNullException(nameof(systemClock));
27+
}
2128

2229
protected override void OnModelCreating(ModelBuilder modelBuilder)
2330
{

src/Examples/JsonApiDotNetCoreExample/Models/User.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
using System;
22
using JsonApiDotNetCore.Models;
3+
using JsonApiDotNetCoreExample.Data;
4+
using Microsoft.AspNetCore.Authentication;
35

46
namespace JsonApiDotNetCoreExample.Models
57
{
68
public class User : Identifiable
79
{
10+
private readonly ISystemClock _systemClock;
811
private string _password;
912

1013
[Attr] public string Username { get; set; }
@@ -18,16 +21,25 @@ public string Password
1821
if (value != _password)
1922
{
2023
_password = value;
21-
LastPasswordChange = DateTime.Now;
24+
LastPasswordChange = _systemClock.UtcNow.LocalDateTime;
2225
}
2326
}
2427
}
2528

2629
[Attr] public DateTime LastPasswordChange { get; set; }
30+
31+
public User(AppDbContext appDbContext)
32+
{
33+
_systemClock = appDbContext.SystemClock;
34+
}
2735
}
2836

2937
public sealed class SuperUser : User
3038
{
3139
[Attr] public int SecurityLevel { get; set; }
40+
41+
public SuperUser(AppDbContext appDbContext) : base(appDbContext)
42+
{
43+
}
3244
}
3345
}

src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
using Microsoft.Extensions.DependencyInjection;
55
using JsonApiDotNetCoreExample.Data;
66
using Microsoft.EntityFrameworkCore;
7-
using JsonApiDotNetCore.Extensions;
87
using System;
98
using JsonApiDotNetCore;
109
using JsonApiDotNetCore.Configuration;
1110
using JsonApiDotNetCore.Query;
1211
using JsonApiDotNetCoreExample.Services;
12+
using Microsoft.AspNetCore.Authentication;
1313
using Newtonsoft.Json.Converters;
1414

1515
namespace JsonApiDotNetCoreExample
@@ -30,6 +30,7 @@ public Startup(IWebHostEnvironment env)
3030

3131
public virtual void ConfigureServices(IServiceCollection services)
3232
{
33+
services.AddSingleton<ISystemClock, SystemClock>();
3334
services.AddScoped<SkipCacheQueryParameterService>();
3435
services.AddScoped<IQueryParameterService>(sp => sp.GetService<SkipCacheQueryParameterService>());
3536

src/JsonApiDotNetCore/Extensions/TypeExtensions.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Collections;
44
using System.Collections.Generic;
55
using System.Linq;
6+
using System.Reflection;
67
using JsonApiDotNetCore.Models;
78

89
namespace JsonApiDotNetCore.Extensions
@@ -75,5 +76,12 @@ public static bool IsOrImplementsInterface(this Type source, Type interfaceType)
7576

7677
return source == interfaceType || source.GetInterfaces().Any(type => type == interfaceType);
7778
}
79+
80+
public static bool HasSingleConstructorWithoutParameters(this Type type)
81+
{
82+
ConstructorInfo[] constructors = type.GetConstructors();
83+
84+
return constructors.Length == 1 && constructors[0].GetParameters().Length == 0;
85+
}
7886
}
7987
}

src/JsonApiDotNetCore/Internal/TypeHelper.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Linq.Expressions;
77
using JsonApiDotNetCore.Extensions;
88
using JsonApiDotNetCore.Models;
9+
using Microsoft.Extensions.DependencyInjection;
910

1011
namespace JsonApiDotNetCore.Internal
1112
{
@@ -237,5 +238,39 @@ public static object CreateInstance(Type type)
237238
throw new InvalidOperationException($"Failed to create an instance of '{type.FullName}' using its default constructor.", exception);
238239
}
239240
}
241+
242+
public static T CreateEntityInstance<T>(IServiceProvider serviceProvider)
243+
{
244+
return (T) CreateEntityInstance(typeof(T), serviceProvider);
245+
}
246+
247+
public static object CreateEntityInstance(Type type, IServiceProvider serviceProvider)
248+
{
249+
if (type == null)
250+
{
251+
throw new ArgumentNullException(nameof(type));
252+
}
253+
254+
if (serviceProvider == null)
255+
{
256+
throw new ArgumentNullException(nameof(serviceProvider));
257+
}
258+
259+
bool hasSingleConstructorWithoutParameters = type.HasSingleConstructorWithoutParameters();
260+
261+
try
262+
{
263+
return hasSingleConstructorWithoutParameters
264+
? Activator.CreateInstance(type)
265+
: ActivatorUtilities.CreateInstance(serviceProvider, type);
266+
}
267+
catch (Exception exception)
268+
{
269+
throw new InvalidOperationException(hasSingleConstructorWithoutParameters
270+
? $"Failed to create an instance of '{type.FullName}' using its default constructor."
271+
: $"Failed to create an instance of '{type.FullName}' using injected constructor parameters.",
272+
exception);
273+
}
274+
}
240275
}
241276
}

src/JsonApiDotNetCore/Serialization/Client/ResponseDeserializer.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace JsonApiDotNetCore.Serialization.Client
1313
/// </summary>
1414
public class ResponseDeserializer : BaseDocumentParser, IResponseDeserializer
1515
{
16-
public ResponseDeserializer(IResourceContextProvider provider) : base(provider) { }
16+
public ResponseDeserializer(IResourceContextProvider contextProvider, IServiceProvider serviceProvider) : base(contextProvider, serviceProvider) { }
1717

1818
/// <inheritdoc/>
1919
public DeserializedSingleResponse<TResource> DeserializeSingle<TResource>(string body) where TResource : class, IIdentifiable
@@ -87,7 +87,7 @@ private IIdentifiable ParseIncludedRelationship(RelationshipAttribute relationsh
8787
if (includedResource == null)
8888
return relatedInstance;
8989

90-
var resourceContext = _provider.GetResourceContext(relatedResourceIdentifier.Type);
90+
var resourceContext = _contextProvider.GetResourceContext(relatedResourceIdentifier.Type);
9191
if (resourceContext == null)
9292
throw new InvalidOperationException($"Included type '{relationshipAttr.RightType}' is not a registered json:api resource.");
9393

src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@ namespace JsonApiDotNetCore.Serialization
2121
/// </summary>
2222
public abstract class BaseDocumentParser
2323
{
24-
protected readonly IResourceContextProvider _provider;
24+
protected readonly IResourceContextProvider _contextProvider;
25+
private readonly IServiceProvider _serviceProvider;
2526
protected Document _document;
2627

27-
protected BaseDocumentParser(IResourceContextProvider provider)
28+
protected BaseDocumentParser(IResourceContextProvider contextProvider, IServiceProvider serviceProvider)
2829
{
29-
_provider = provider;
30+
_contextProvider = contextProvider;
31+
_serviceProvider = serviceProvider;
3032
}
3133

3234
/// <summary>
@@ -131,7 +133,7 @@ private JToken LoadJToken(string body)
131133
/// <returns>The parsed entity</returns>
132134
private IIdentifiable ParseResourceObject(ResourceObject data)
133135
{
134-
var resourceContext = _provider.GetResourceContext(data.Type);
136+
var resourceContext = _contextProvider.GetResourceContext(data.Type);
135137
if (resourceContext == null)
136138
{
137139
throw new InvalidRequestBodyException("Payload includes unknown resource type.",
@@ -140,7 +142,7 @@ private IIdentifiable ParseResourceObject(ResourceObject data)
140142
"If you have manually registered the resource, check that the call to AddResource correctly sets the public name.", null);
141143
}
142144

143-
var entity = (IIdentifiable)TypeHelper.CreateInstance(resourceContext.ResourceType);
145+
var entity = (IIdentifiable)TypeHelper.CreateEntityInstance(resourceContext.ResourceType, _serviceProvider);
144146

145147
entity = SetAttributes(entity, data.Attributes, resourceContext.Attributes);
146148
entity = SetRelationships(entity, data.Relationships, resourceContext.Relationships);

src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ public class RequestDeserializer : BaseDocumentParser, IJsonApiDeserializer
1111
{
1212
private readonly ITargetedFields _targetedFields;
1313

14-
public RequestDeserializer(IResourceContextProvider provider,
15-
ITargetedFields targetedFields) : base(provider)
14+
public RequestDeserializer(IResourceContextProvider contextProvider, IServiceProvider serviceProvider, ITargetedFields targetedFields)
15+
: base(contextProvider, serviceProvider)
1616
{
1717
_targetedFields = targetedFields;
1818
}

test/IntegrationTests/Data/EntityRepositoryTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.Collections.Generic;
1111
using System.Linq;
1212
using System.Threading.Tasks;
13+
using IntegrationTests;
1314
using JsonApiDotNetCore.Configuration;
1415
using Microsoft.Extensions.Logging.Abstractions;
1516
using Xunit;
@@ -174,7 +175,7 @@ private AppDbContext GetContext(Guid? seed = null)
174175
var options = new DbContextOptionsBuilder<AppDbContext>()
175176
.UseInMemoryDatabase(databaseName: $"IntegrationDatabaseRepository{actualSeed}")
176177
.Options;
177-
var context = new AppDbContext(options);
178+
var context = new AppDbContext(options, new FrozenSystemClock());
178179

179180
context.TodoItems.RemoveRange(context.TodoItems);
180181
return context;

0 commit comments

Comments
 (0)