Skip to content

Commit 19cc14d

Browse files
committed
Added client library with tests to project; made adjustmets to the models to make the client test pass without introducing any structural adjustments to the test codes
1 parent 09ab5c0 commit 19cc14d

26 files changed

+2811
-284
lines changed

JsonApiDotNetCore.sln

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCore.OpenApi",
4848
EndProject
4949
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApiTests", "test\OpenApiTests\OpenApiTests.csproj", "{B693DE14-BB28-496F-AB39-B4E674ABCA80}"
5050
EndProject
51+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApiClient", "src\Examples\OpenApiClient\OpenApiClient.csproj", "{63C2C6C1-0967-4439-BB63-A55DA22869AB}"
52+
EndProject
53+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCore.OpenApiClient", "src\JsonApiDotNetCore.OpenApiClient\JsonApiDotNetCore.OpenApiClient.csproj", "{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}"
54+
EndProject
5155
Global
5256
GlobalSection(SolutionConfigurationPlatforms) = preSolution
5357
Debug|Any CPU = Debug|Any CPU
@@ -238,6 +242,30 @@ Global
238242
{B693DE14-BB28-496F-AB39-B4E674ABCA80}.Release|x64.Build.0 = Release|Any CPU
239243
{B693DE14-BB28-496F-AB39-B4E674ABCA80}.Release|x86.ActiveCfg = Release|Any CPU
240244
{B693DE14-BB28-496F-AB39-B4E674ABCA80}.Release|x86.Build.0 = Release|Any CPU
245+
{63C2C6C1-0967-4439-BB63-A55DA22869AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
246+
{63C2C6C1-0967-4439-BB63-A55DA22869AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
247+
{63C2C6C1-0967-4439-BB63-A55DA22869AB}.Debug|x64.ActiveCfg = Debug|Any CPU
248+
{63C2C6C1-0967-4439-BB63-A55DA22869AB}.Debug|x64.Build.0 = Debug|Any CPU
249+
{63C2C6C1-0967-4439-BB63-A55DA22869AB}.Debug|x86.ActiveCfg = Debug|Any CPU
250+
{63C2C6C1-0967-4439-BB63-A55DA22869AB}.Debug|x86.Build.0 = Debug|Any CPU
251+
{63C2C6C1-0967-4439-BB63-A55DA22869AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
252+
{63C2C6C1-0967-4439-BB63-A55DA22869AB}.Release|Any CPU.Build.0 = Release|Any CPU
253+
{63C2C6C1-0967-4439-BB63-A55DA22869AB}.Release|x64.ActiveCfg = Release|Any CPU
254+
{63C2C6C1-0967-4439-BB63-A55DA22869AB}.Release|x64.Build.0 = Release|Any CPU
255+
{63C2C6C1-0967-4439-BB63-A55DA22869AB}.Release|x86.ActiveCfg = Release|Any CPU
256+
{63C2C6C1-0967-4439-BB63-A55DA22869AB}.Release|x86.Build.0 = Release|Any CPU
257+
{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
258+
{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Debug|Any CPU.Build.0 = Debug|Any CPU
259+
{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Debug|x64.ActiveCfg = Debug|Any CPU
260+
{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Debug|x64.Build.0 = Debug|Any CPU
261+
{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Debug|x86.ActiveCfg = Debug|Any CPU
262+
{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Debug|x86.Build.0 = Debug|Any CPU
263+
{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Release|Any CPU.ActiveCfg = Release|Any CPU
264+
{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Release|Any CPU.Build.0 = Release|Any CPU
265+
{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Release|x64.ActiveCfg = Release|Any CPU
266+
{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Release|x64.Build.0 = Release|Any CPU
267+
{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Release|x86.ActiveCfg = Release|Any CPU
268+
{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Release|x86.Build.0 = Release|Any CPU
241269
EndGlobalSection
242270
GlobalSection(SolutionProperties) = preSolution
243271
HideSolutionNode = FALSE
@@ -258,6 +286,8 @@ Global
258286
{210FD61E-FF5D-4CEE-8E0D-C739ECCCBA21} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F}
259287
{71287D6F-6C3B-44B4-9FCA-E78FE3F02289} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF}
260288
{B693DE14-BB28-496F-AB39-B4E674ABCA80} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F}
289+
{63C2C6C1-0967-4439-BB63-A55DA22869AB} = {026FBC6C-AF76-4568-9B87-EC73457899FD}
290+
{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF}
261291
EndGlobalSection
262292
GlobalSection(ExtensibilityGlobals) = postSolution
263293
SolutionGuid = {A2421882-8F0A-4905-928F-B550B192F9A4}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
using System.Runtime.CompilerServices;
2+
3+
[assembly: InternalsVisibleTo("JsonApiDotNetCore.OpenApiClient")]

src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,7 @@ public DataContract GetDataContractForType(Type type)
4444

4545
IList<DataProperty> replacementProperties = null;
4646

47-
if (type.IsSubclassOfOpenGeneric(typeof(Identifiable<>)))
48-
{
49-
replacementProperties = dataContract.ObjectProperties.Where(IsIdentity).ToArray();
50-
}
51-
else if (type.IsAssignableTo(typeof(IIdentifiable)))
47+
if (type.IsAssignableTo(typeof(IIdentifiable)))
5248
{
5349
replacementProperties = GetDataPropertiesThatExistInResourceContext(type, dataContract);
5450
}
@@ -61,6 +57,16 @@ public DataContract GetDataContractForType(Type type)
6157
return dataContract;
6258
}
6359

60+
private static bool IsIdentifiableBaseType(Type type)
61+
{
62+
if (type.IsGenericType)
63+
{
64+
return type.GetGenericTypeDefinition() == typeof(Identifiable<>);
65+
}
66+
67+
return type == typeof(Identifiable);
68+
}
69+
6470
private static bool IsIdentity(DataProperty property)
6571
{
6672
return property.MemberInfo.Name == nameof(Identifiable.Id);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using System;
2+
using System.Linq.Expressions;
3+
4+
namespace JsonApiDotNetCore.OpenApiClient
5+
{
6+
public interface IJsonApiClient
7+
{
8+
/// <summary>
9+
/// Ensures correct serialization of attributes in a POST/PATCH Resource request body. In JSON:API, an omitted attribute indicates to ignore it, while an
10+
/// attribute that is set to "null" means to clear it. This poses a problem because the serializer cannot distinguish between "you have explicitly set
11+
/// this .NET property to null" vs "you didn't touch it, so it is null by default" when converting an instance to JSON. Therefore, calling this method
12+
/// treats all attributes that contain their default value (<c>null</c> for reference types, <c>0</c> for integers, <c>false</c> for booleans, etc) as
13+
/// omitted unless explicitly listed to include them using <paramref name="alwaysIncludedAttributeSelectors" />.
14+
/// </summary>
15+
/// <param name="requestDocument">
16+
/// The request document instance for which this registration applies.
17+
/// </param>
18+
/// <param name="alwaysIncludedAttributeSelectors">
19+
/// Optional. A list of expressions to indicate which properties to unconditionally include in the JSON request body. For example:
20+
/// <code><![CDATA[
21+
/// video => video.Title, video => video.Summary
22+
/// ]]></code>
23+
/// </param>
24+
/// <typeparam name="TRequestDocument">
25+
/// The type of the request document.
26+
/// </typeparam>
27+
/// <typeparam name="TAttributesObject">
28+
/// The type of the attributes object inside <typeparamref name="TRequestDocument" />.
29+
/// </typeparam>
30+
/// <returns>
31+
/// An <see cref="IDisposable" /> to clear the current registration. For efficient memory usage, it is recommended to wrap calls to this method in a
32+
/// <c>using</c> statement, so the registrations are cleaned up after executing the request.
33+
/// </returns>
34+
IDisposable RegisterAttributesForRequestDocument<TRequestDocument, TAttributesObject>(TRequestDocument requestDocument,
35+
params Expression<Func<TAttributesObject, object>>[] alwaysIncludedAttributeSelectors)
36+
where TRequestDocument : class;
37+
}
38+
}
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Linq.Expressions;
5+
using System.Reflection;
6+
using JetBrains.Annotations;
7+
using JsonApiDotNetCore.OpenApi;
8+
using Newtonsoft.Json;
9+
using Newtonsoft.Json.Serialization;
10+
11+
namespace JsonApiDotNetCore.OpenApiClient
12+
{
13+
/// <summary>
14+
/// Base class to inherit auto-generated client from. Enables to mark fields to be explicitly included in a request body, even if they are null or
15+
/// default.
16+
/// </summary>
17+
[PublicAPI]
18+
public abstract class JsonApiClient : IJsonApiClient
19+
{
20+
private readonly JsonApiJsonConverter _jsonApiJsonConverter = new();
21+
22+
protected void SetSerializerSettingsForJsonApi(JsonSerializerSettings settings)
23+
{
24+
ArgumentGuard.NotNull(settings, nameof(settings));
25+
26+
settings.Converters.Add(_jsonApiJsonConverter);
27+
}
28+
29+
/// <inheritdoc />
30+
public IDisposable RegisterAttributesForRequestDocument<TRequestDocument, TAttributesObject>(TRequestDocument requestDocument,
31+
params Expression<Func<TAttributesObject, object>>[] alwaysIncludedAttributeSelectors)
32+
where TRequestDocument : class
33+
{
34+
ArgumentGuard.NotNull(requestDocument, nameof(requestDocument));
35+
36+
var attributeNames = new HashSet<string>();
37+
38+
foreach (Expression<Func<TAttributesObject, object>> selector in alwaysIncludedAttributeSelectors)
39+
{
40+
if (RemoveConvert(selector.Body) is MemberExpression selectorBody)
41+
{
42+
attributeNames.Add(selectorBody.Member.Name);
43+
}
44+
else
45+
{
46+
throw new ArgumentException($"The expression '{selector}' should select a single property. For example: 'article => article.Title'.");
47+
}
48+
}
49+
50+
_jsonApiJsonConverter.RegisterRequestDocument(requestDocument, new AttributeNamesContainer(attributeNames, typeof(TAttributesObject)));
51+
52+
return new AttributesRegistrationScope(_jsonApiJsonConverter, requestDocument);
53+
}
54+
55+
private static Expression RemoveConvert(Expression expression)
56+
{
57+
Expression innerExpression = expression;
58+
59+
while (true)
60+
{
61+
if (innerExpression is UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression)
62+
{
63+
innerExpression = unaryExpression.Operand;
64+
}
65+
else
66+
{
67+
return innerExpression;
68+
}
69+
}
70+
}
71+
72+
private sealed class JsonApiJsonConverter : JsonConverter
73+
{
74+
private readonly Dictionary<object, AttributeNamesContainer> _alwaysIncludedAttributesPerRequestDocumentInstance = new();
75+
private readonly Dictionary<Type, ISet<object>> _requestDocumentInstancesPerRequestDocumentType = new();
76+
private bool _isSerializing;
77+
78+
public override bool CanRead => false;
79+
80+
public void RegisterRequestDocument(object requestDocument, AttributeNamesContainer attributes)
81+
{
82+
_alwaysIncludedAttributesPerRequestDocumentInstance[requestDocument] = attributes;
83+
84+
Type requestDocumentType = requestDocument.GetType();
85+
86+
if (!_requestDocumentInstancesPerRequestDocumentType.ContainsKey(requestDocumentType))
87+
{
88+
_requestDocumentInstancesPerRequestDocumentType[requestDocumentType] = new HashSet<object>();
89+
}
90+
91+
_requestDocumentInstancesPerRequestDocumentType[requestDocumentType].Add(requestDocument);
92+
}
93+
94+
public void RemoveAttributeRegistration(object requestDocument)
95+
{
96+
if (_alwaysIncludedAttributesPerRequestDocumentInstance.ContainsKey(requestDocument))
97+
{
98+
_alwaysIncludedAttributesPerRequestDocumentInstance.Remove(requestDocument);
99+
100+
Type requestDocumentType = requestDocument.GetType();
101+
_requestDocumentInstancesPerRequestDocumentType[requestDocumentType].Remove(requestDocument);
102+
103+
if (!_requestDocumentInstancesPerRequestDocumentType[requestDocumentType].Any())
104+
{
105+
_requestDocumentInstancesPerRequestDocumentType.Remove(requestDocumentType);
106+
}
107+
}
108+
}
109+
110+
public override bool CanConvert(Type objectType)
111+
{
112+
ArgumentGuard.NotNull(objectType, nameof(objectType));
113+
114+
return !_isSerializing && _requestDocumentInstancesPerRequestDocumentType.ContainsKey(objectType);
115+
}
116+
117+
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
118+
{
119+
throw new UnreachableCodeException();
120+
}
121+
122+
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
123+
{
124+
ArgumentGuard.NotNull(writer, nameof(writer));
125+
ArgumentGuard.NotNull(value, nameof(value));
126+
ArgumentGuard.NotNull(serializer, nameof(serializer));
127+
128+
if (_alwaysIncludedAttributesPerRequestDocumentInstance.ContainsKey(value))
129+
{
130+
AttributeNamesContainer attributeNamesContainer = _alwaysIncludedAttributesPerRequestDocumentInstance[value];
131+
serializer.ContractResolver = new JsonApiDocumentContractResolver(attributeNamesContainer);
132+
}
133+
134+
try
135+
{
136+
_isSerializing = true;
137+
serializer.Serialize(writer, value);
138+
}
139+
finally
140+
{
141+
_isSerializing = false;
142+
}
143+
}
144+
}
145+
146+
private sealed class AttributeNamesContainer
147+
{
148+
private readonly ISet<string> _attributeNames;
149+
private readonly Type _containerType;
150+
151+
public AttributeNamesContainer(ISet<string> attributeNames, Type containerType)
152+
{
153+
ArgumentGuard.NotNull(attributeNames, nameof(attributeNames));
154+
ArgumentGuard.NotNull(containerType, nameof(containerType));
155+
156+
_attributeNames = attributeNames;
157+
_containerType = containerType;
158+
}
159+
160+
public bool ContainsAttribute(string name)
161+
{
162+
return _attributeNames.Contains(name);
163+
}
164+
165+
public bool ContainerMatchesType(Type type)
166+
{
167+
return _containerType == type;
168+
}
169+
}
170+
171+
private sealed class AttributesRegistrationScope : IDisposable
172+
{
173+
private readonly JsonApiJsonConverter _jsonApiJsonConverter;
174+
private readonly object _requestDocument;
175+
176+
public AttributesRegistrationScope(JsonApiJsonConverter jsonApiJsonConverter, object requestDocument)
177+
{
178+
ArgumentGuard.NotNull(jsonApiJsonConverter, nameof(jsonApiJsonConverter));
179+
ArgumentGuard.NotNull(requestDocument, nameof(requestDocument));
180+
181+
_jsonApiJsonConverter = jsonApiJsonConverter;
182+
_requestDocument = requestDocument;
183+
}
184+
185+
public void Dispose()
186+
{
187+
_jsonApiJsonConverter.RemoveAttributeRegistration(_requestDocument);
188+
}
189+
}
190+
191+
private sealed class JsonApiDocumentContractResolver : DefaultContractResolver
192+
{
193+
private readonly AttributeNamesContainer _attributeNamesContainer;
194+
195+
public JsonApiDocumentContractResolver(AttributeNamesContainer attributeNamesContainer)
196+
{
197+
ArgumentGuard.NotNull(attributeNamesContainer, nameof(attributeNamesContainer));
198+
199+
_attributeNamesContainer = attributeNamesContainer;
200+
}
201+
202+
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
203+
{
204+
JsonProperty property = base.CreateProperty(member, memberSerialization);
205+
206+
if (_attributeNamesContainer.ContainerMatchesType(property.DeclaringType))
207+
{
208+
if (_attributeNamesContainer.ContainsAttribute(property.UnderlyingName))
209+
{
210+
property.NullValueHandling = NullValueHandling.Include;
211+
property.DefaultValueHandling = DefaultValueHandling.Include;
212+
}
213+
else
214+
{
215+
property.NullValueHandling = NullValueHandling.Ignore;
216+
property.DefaultValueHandling = DefaultValueHandling.Ignore;
217+
}
218+
}
219+
220+
return property;
221+
}
222+
}
223+
}
224+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<VersionPrefix>$(JsonApiDotNetCoreVersionPrefix)</VersionPrefix>
4+
<TargetFramework>$(NetCoreAppVersion)</TargetFramework>
5+
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
6+
</PropertyGroup>
7+
8+
<PropertyGroup>
9+
<PackageTags>jsonapidotnetcore;jsonapi;json:api;dotnet;asp.net;openapi;swagger;client;nswag</PackageTags>
10+
<Description>TODO</Description>
11+
<Authors>json-api-dotnet</Authors>
12+
<PackageProjectUrl>https://www.jsonapi.net/</PackageProjectUrl>
13+
<PackageLicenseExpression>MIT</PackageLicenseExpression>
14+
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
15+
<PublishRepositoryUrl>true</PublishRepositoryUrl>
16+
<EmbedUntrackedSources>true</EmbedUntrackedSources>
17+
<DebugType>embedded</DebugType>
18+
</PropertyGroup>
19+
20+
<ItemGroup>
21+
<ProjectReference Include="..\JsonApiDotNetCore.OpenApi\JsonApiDotNetCore.OpenApi.csproj" />
22+
<ProjectReference Include="..\JsonApiDotNetCore\JsonApiDotNetCore.csproj" />
23+
</ItemGroup>
24+
25+
<ItemGroup>
26+
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0">
27+
<PrivateAssets>all</PrivateAssets>
28+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
29+
</PackageReference>
30+
<PackageReference Include="Swashbuckle.AspNetCore" Version="$(SwashbuckleVersion)" />
31+
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="$(SwashbuckleVersion)" />
32+
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="$(SwashbuckleVersion)" />
33+
</ItemGroup>
34+
35+
</Project>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
using System.Runtime.CompilerServices;
22

33
[assembly: InternalsVisibleTo("JsonApiDotNetCore.OpenApi")]
4+
[assembly: InternalsVisibleTo("JsonApiDotNetCore.OpenApiClient")]
45
[assembly: InternalsVisibleTo("Benchmarks")]
56
[assembly: InternalsVisibleTo("JsonApiDotNetCoreTests")]
67
[assembly: InternalsVisibleTo("UnitTests")]
78
[assembly: InternalsVisibleTo("DiscoveryTests")]
9+
[assembly: InternalsVisibleTo("OpenApiTests")]
810
[assembly: InternalsVisibleTo("TestBuildingBlocks")]

0 commit comments

Comments
 (0)