Skip to content

Commit 7722500

Browse files
committed
refactor(de-serialization): introduce new interfaces and composition root at JsonApiInputFormatter
removes logic from the input formatter and into JsonApiReader. Significantly reduces coupling of concrete types.
1 parent 34431fa commit 7722500

17 files changed

+181
-107
lines changed

src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public class DefaultEntityRepository<TEntity, TId>
3232
private readonly DbSet<TEntity> _dbSet;
3333
private readonly ILogger _logger;
3434
private readonly IJsonApiContext _jsonApiContext;
35+
private readonly IGenericProcessorFactory _genericProcessorFactory;
3536

3637
public DefaultEntityRepository(
3738
DbContext context,
@@ -42,6 +43,7 @@ public DefaultEntityRepository(
4243
_dbSet = context.GetDbSet<TEntity>();
4344
_jsonApiContext = jsonApiContext;
4445
_logger = loggerFactory.CreateLogger<DefaultEntityRepository<TEntity, TId>>();
46+
_genericProcessorFactory = _jsonApiContext.GenericProcessorFactory;
4547
}
4648

4749
public virtual IQueryable<TEntity> Get()
@@ -110,7 +112,7 @@ public virtual async Task<TEntity> UpdateAsync(TId id, TEntity entity)
110112

111113
public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds)
112114
{
113-
var genericProcessor = GenericProcessorFactory.GetProcessor(relationship.Type, _context);
115+
var genericProcessor = _genericProcessorFactory.GetProcessor(relationship.Type);
114116
await genericProcessor.UpdateRelationshipsAsync(parent, relationship, relationshipIds);
115117
}
116118

src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ public static void AddJsonApiInternals<TContext>(this IServiceCollection service
6161
services.AddScoped<IDocumentBuilder, DocumentBuilder>();
6262
services.AddScoped<IJsonApiSerializer, JsonApiSerializer>();
6363
services.AddScoped<IJsonApiWriter, JsonApiWriter>();
64+
services.AddScoped<IJsonApiDeSerializer, JsonApiDeSerializer>();
65+
services.AddScoped<IJsonApiReader, JsonApiReader>();
66+
services.AddScoped<IGenericProcessorFactory, GenericProcessorFactory>();
67+
services.AddScoped(typeof(GenericProcessor<>));
6468
}
6569

6670
public static void SerializeAsJsonApi(this MvcOptions options, JsonApiOptions jsonApiOptions)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using System.Threading.Tasks;
2+
using Microsoft.AspNetCore.Mvc.Formatters;
3+
4+
namespace JsonApiDotNetCore.Formatters
5+
{
6+
public interface IJsonApiReader
7+
{
8+
Task<InputFormatterResult> ReadAsync(InputFormatterContext context);
9+
}
10+
}
Lines changed: 3 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
11
using System;
2-
using System.IO;
32
using System.Threading.Tasks;
4-
using JsonApiDotNetCore.Serialization;
5-
using JsonApiDotNetCore.Services;
63
using Microsoft.AspNetCore.Mvc.Formatters;
74
using Microsoft.Extensions.DependencyInjection;
8-
using Microsoft.Extensions.Logging;
9-
using Newtonsoft.Json;
10-
using Microsoft.EntityFrameworkCore;
115

126
namespace JsonApiDotNetCore.Formatters
137
{
@@ -23,55 +17,10 @@ public bool CanRead(InputFormatterContext context)
2317
return contentTypeString == "application/vnd.api+json";
2418
}
2519

26-
public Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
20+
public async Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
2721
{
28-
if (context == null)
29-
throw new ArgumentNullException(nameof(context));
30-
31-
var request = context.HttpContext.Request;
32-
33-
if (request.ContentLength == 0)
34-
{
35-
return InputFormatterResult.SuccessAsync(null);
36-
}
37-
38-
var loggerFactory = GetService<ILoggerFactory>(context);
39-
var logger = loggerFactory?.CreateLogger<JsonApiInputFormatter>();
40-
41-
var dbContext = GetService<DbContext>(context);
42-
43-
try
44-
{
45-
var body = GetRequestBody(context.HttpContext.Request.Body);
46-
var jsonApiContext = GetService<IJsonApiContext>(context);
47-
var model = jsonApiContext.IsRelationshipPath ?
48-
JsonApiDeSerializer.DeserializeRelationship(body, jsonApiContext) :
49-
JsonApiDeSerializer.Deserialize(body, jsonApiContext, dbContext);
50-
51-
if(model == null)
52-
logger?.LogError("An error occurred while de-serializing the payload");
53-
54-
return InputFormatterResult.SuccessAsync(model);
55-
}
56-
catch (JsonSerializationException ex)
57-
{
58-
logger?.LogError(new EventId(), ex, "An error occurred while de-serializing the payload");
59-
context.HttpContext.Response.StatusCode = 422;
60-
return InputFormatterResult.FailureAsync();
61-
}
62-
}
63-
64-
private string GetRequestBody(Stream body)
65-
{
66-
using (var reader = new StreamReader(body))
67-
{
68-
return reader.ReadToEnd();
69-
}
70-
}
71-
72-
private TService GetService<TService>(InputFormatterContext context)
73-
{
74-
return context.HttpContext.RequestServices.GetService<TService>();
22+
var reader = context.HttpContext.RequestServices.GetService<IJsonApiReader>();
23+
return await reader.ReadAsync(context);
7524
}
7625
}
7726
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using System;
2+
using System.IO;
3+
using System.Threading.Tasks;
4+
using JsonApiDotNetCore.Serialization;
5+
using JsonApiDotNetCore.Services;
6+
using Microsoft.AspNetCore.Mvc.Formatters;
7+
using Microsoft.EntityFrameworkCore;
8+
using Microsoft.Extensions.Logging;
9+
using Newtonsoft.Json;
10+
11+
namespace JsonApiDotNetCore.Formatters
12+
{
13+
public class JsonApiReader : IJsonApiReader
14+
{
15+
private readonly IJsonApiDeSerializer _deSerializer;
16+
private readonly IJsonApiContext _jsonApiContext;
17+
private readonly ILogger<JsonApiReader> _logger;
18+
19+
20+
public JsonApiReader(IJsonApiDeSerializer deSerializer, IJsonApiContext jsonApiContext, ILoggerFactory loggerFactory)
21+
{
22+
_deSerializer = deSerializer;
23+
_jsonApiContext = jsonApiContext;
24+
_logger = loggerFactory.CreateLogger<JsonApiReader>();
25+
}
26+
27+
public Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
28+
{
29+
if (context == null)
30+
throw new ArgumentNullException(nameof(context));
31+
32+
var request = context.HttpContext.Request;
33+
if (request.ContentLength == 0)
34+
return InputFormatterResult.SuccessAsync(null);
35+
36+
try
37+
{
38+
var body = GetRequestBody(context.HttpContext.Request.Body);
39+
var model = _jsonApiContext.IsRelationshipPath ?
40+
_deSerializer.DeserializeRelationship(body) :
41+
_deSerializer.Deserialize(body);
42+
43+
if(model == null)
44+
_logger?.LogError("An error occurred while de-serializing the payload");
45+
46+
return InputFormatterResult.SuccessAsync(model);
47+
}
48+
catch (JsonSerializationException ex)
49+
{
50+
_logger?.LogError(new EventId(), ex, "An error occurred while de-serializing the payload");
51+
context.HttpContext.Response.StatusCode = 422;
52+
return InputFormatterResult.FailureAsync();
53+
}
54+
}
55+
56+
private string GetRequestBody(Stream body)
57+
{
58+
using (var reader = new StreamReader(body))
59+
{
60+
return reader.ReadToEnd();
61+
}
62+
}
63+
}
64+
}

src/JsonApiDotNetCore/Internal/Generics/GenericProcessorFactory.cs

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,22 @@
33

44
namespace JsonApiDotNetCore.Internal
55
{
6-
/// <summary>
7-
/// Used to generate a generic operations processor when the types
8-
/// are not know until runtime. The typical use case would be for
9-
/// accessing relationship data.
10-
/// </summary>
11-
public static class GenericProcessorFactory
6+
public class GenericProcessorFactory : IGenericProcessorFactory
127
{
13-
public static IGenericProcessor GetProcessor(Type type, DbContext dbContext)
8+
private readonly DbContext _dbContext;
9+
private readonly IServiceProvider _serviceProvider;
10+
11+
public GenericProcessorFactory(DbContext dbContext,
12+
IServiceProvider serviceProvider)
13+
{
14+
_dbContext = dbContext;
15+
_serviceProvider = serviceProvider;
16+
}
17+
18+
public IGenericProcessor GetProcessor(Type type)
1419
{
15-
var repositoryType = typeof(GenericProcessor<>).MakeGenericType(type);
16-
return (IGenericProcessor)Activator.CreateInstance(repositoryType, dbContext);
20+
var processorType = typeof(GenericProcessor<>).MakeGenericType(type);
21+
return (IGenericProcessor)_serviceProvider.GetService(processorType);
1722
}
1823
}
1924
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System;
2+
3+
namespace JsonApiDotNetCore.Internal
4+
{
5+
/// <summary>
6+
/// Used to generate a generic operations processor when the types
7+
/// are not know until runtime. The typical use case would be for
8+
/// accessing relationship data.
9+
/// </summary>
10+
public interface IGenericProcessorFactory
11+
{
12+
IGenericProcessor GetProcessor(Type type);
13+
}
14+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System.Collections.Generic;
2+
3+
namespace JsonApiDotNetCore.Serialization
4+
{
5+
public interface IJsonApiDeSerializer
6+
{
7+
object Deserialize(string requestBody);
8+
object DeserializeRelationship(string requestBody);
9+
List<TEntity> DeserializeList<TEntity>(string requestBody);
10+
}
11+
}

src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,33 @@
88
using JsonApiDotNetCore.Services;
99
using Newtonsoft.Json;
1010
using Newtonsoft.Json.Linq;
11-
using System.Collections;
12-
using JsonApiDotNetCore.Data;
1311
using Microsoft.EntityFrameworkCore;
1412

1513
namespace JsonApiDotNetCore.Serialization
1614
{
17-
public static class JsonApiDeSerializer
15+
public class JsonApiDeSerializer : IJsonApiDeSerializer
1816
{
19-
public static object Deserialize(string requestBody, IJsonApiContext context,
20-
DbContext dbContext)
17+
private readonly DbContext _dbContext;
18+
private readonly IJsonApiContext _jsonApiContext;
19+
private readonly IGenericProcessorFactory _genericProcessorFactor;
20+
21+
public JsonApiDeSerializer(DbContext dbContext,
22+
IJsonApiContext jsonApiContext,
23+
IGenericProcessorFactory genericProcessorFactory)
24+
{
25+
_dbContext = dbContext;
26+
_jsonApiContext = jsonApiContext;
27+
_genericProcessorFactor = genericProcessorFactory;
28+
}
29+
30+
public object Deserialize(string requestBody)
2131
{
2232
var document = JsonConvert.DeserializeObject<Document>(requestBody);
23-
var entity = DataToObject(document.Data, context, dbContext);
33+
var entity = DataToObject(document.Data);
2434
return entity;
2535
}
2636

27-
public static object DeserializeRelationship(string requestBody, IJsonApiContext context)
37+
public object DeserializeRelationship(string requestBody)
2838
{
2939
var data = JToken.Parse(requestBody)["data"];
3040

@@ -35,34 +45,31 @@ public static object DeserializeRelationship(string requestBody, IJsonApiContext
3545
}
3646

3747

38-
public static List<TEntity> DeserializeList<TEntity>(string requestBody, IJsonApiContext context,
39-
DbContext dbContext)
48+
public List<TEntity> DeserializeList<TEntity>(string requestBody)
4049
{
4150
var documents = JsonConvert.DeserializeObject<Documents>(requestBody);
4251

4352
var deserializedList = new List<TEntity>();
4453
foreach (var data in documents.Data)
4554
{
46-
var entity = DataToObject(data, context, dbContext);
55+
var entity = DataToObject(data);
4756
deserializedList.Add((TEntity)entity);
4857
}
4958

5059
return deserializedList;
5160
}
5261

53-
private static object DataToObject(DocumentData data,
54-
IJsonApiContext context,
55-
DbContext dbContext)
62+
private object DataToObject(DocumentData data)
5663
{
5764
var entityTypeName = data.Type.ToProperCase();
5865

59-
var contextEntity = context.ContextGraph.GetContextEntity(entityTypeName);
60-
context.RequestEntity = contextEntity;
66+
var contextEntity = _jsonApiContext.ContextGraph.GetContextEntity(entityTypeName);
67+
_jsonApiContext.RequestEntity = contextEntity;
6168

6269
var entity = Activator.CreateInstance(contextEntity.EntityType);
6370

6471
entity = _setEntityAttributes(entity, contextEntity, data.Attributes);
65-
entity = _setRelationships(entity, contextEntity, data.Relationships, dbContext);
72+
entity = _setRelationships(entity, contextEntity, data.Relationships);
6673

6774
var identifiableEntity = (IIdentifiable)entity;
6875

@@ -72,7 +79,7 @@ private static object DataToObject(DocumentData data,
7279
return identifiableEntity;
7380
}
7481

75-
private static object _setEntityAttributes(
82+
private object _setEntityAttributes(
7683
object entity, ContextEntity contextEntity, Dictionary<string, object> attributeValues)
7784
{
7885
if (attributeValues == null || attributeValues.Count == 0)
@@ -98,11 +105,10 @@ private static object _setEntityAttributes(
98105
return entity;
99106
}
100107

101-
private static object _setRelationships(
108+
private object _setRelationships(
102109
object entity,
103110
ContextEntity contextEntity,
104-
Dictionary<string, RelationshipData> relationships,
105-
DbContext context)
111+
Dictionary<string, RelationshipData> relationships)
106112
{
107113
if (relationships == null || relationships.Count == 0)
108114
return entity;
@@ -114,13 +120,13 @@ private static object _setRelationships(
114120
if (attr.IsHasOne)
115121
entity = _setHasOneRelationship(entity, entityProperties, attr, contextEntity, relationships);
116122
else
117-
entity = _setHasManyRelationship(entity, entityProperties, attr, contextEntity, relationships, context);
123+
entity = _setHasManyRelationship(entity, entityProperties, attr, contextEntity, relationships);
118124
}
119125

120126
return entity;
121127
}
122128

123-
private static object _setHasOneRelationship(object entity,
129+
private object _setHasOneRelationship(object entity,
124130
PropertyInfo[] entityProperties,
125131
RelationshipAttribute attr,
126132
ContextEntity contextEntity,
@@ -147,12 +153,11 @@ private static object _setHasOneRelationship(object entity,
147153
return entity;
148154
}
149155

150-
private static object _setHasManyRelationship(object entity,
156+
private object _setHasManyRelationship(object entity,
151157
PropertyInfo[] entityProperties,
152158
RelationshipAttribute attr,
153159
ContextEntity contextEntity,
154-
Dictionary<string, RelationshipData> relationships,
155-
DbContext context)
160+
Dictionary<string, RelationshipData> relationships)
156161
{
157162
var entityProperty = entityProperties.FirstOrDefault(p => p.Name == attr.InternalRelationshipName);
158163

@@ -167,7 +172,7 @@ private static object _setHasManyRelationship(object entity,
167172

168173
if (data == null) return entity;
169174

170-
var genericProcessor = GenericProcessorFactory.GetProcessor(attr.Type, context);
175+
var genericProcessor = _genericProcessorFactor.GetProcessor(attr.Type);
171176
var ids = relationshipData.ManyData.Select(r => r["id"]);
172177
genericProcessor.SetRelationships(entity, attr, ids);
173178
}

src/JsonApiDotNetCore/Services/IJsonApiContext.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@ public interface IJsonApiContext
1919
bool IsRelationshipPath { get; }
2020
PageManager PageManager { get; set; }
2121
IMetaBuilder MetaBuilder { get; set; }
22+
IGenericProcessorFactory GenericProcessorFactory { get; set; }
2223
}
2324
}

0 commit comments

Comments
 (0)