Skip to content

Commit 56c5cd5

Browse files
Replace Newtonsoft.Json with System.Text.Json (#206)
1 parent d099da5 commit 56c5cd5

File tree

31 files changed

+821
-340
lines changed

31 files changed

+821
-340
lines changed

.github/README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# Directory Services Internals<br/>PowerShell Module and Framework
55

66
[![MIT License](https://img.shields.io/badge/License-MIT-green.svg)](../LICENSE.md)
7-
[![PowerShell 5.1 | 7](https://badgen.net/badge/icon/5.1%20|%207?icon=terminal&label=PowerShell)](#)
7+
[![PowerShell 5.1 | 7](https://badgen.net/badge/icon/5.1%20|%207?icon=terminal&label=PowerShell)](#)
88
[![Windows Server 2008 R2 | 2012 R2 | 2016 | 2019 | 2022 | 2025](https://badgen.net/badge/icon/2008%20R2%20|%202012%20R2%20|%202016%20|%202019%20|%202022%20|%202025?icon=windows&label=Windows%20Server)](#)
99

1010
[![.NET Framework 4.8+](https://img.shields.io/badge/Framework-4.8%2B-007FFF.svg?logo=.net)](#)
@@ -160,8 +160,7 @@ This project utilizes the following 3<sup>rd</sup> party copyrighted material:
160160
- [ManagedEsent](https://github.com/Microsoft/ManagedEsent) - Provides managed access to esent.dll, the embeddable database engine native to Windows.
161161
- [NDceRpc](https://github.com/OpenSharp/NDceRpc) - Integration of WCF and .NET with MS-RPC and binary serialization.
162162
- [PBKDF2.NET](https://github.com/therealmagicmike/PBKDF2.NET) - Provides PBKDF2 for .NET Framework.
163-
- [Bouncy Castle](https://www.bouncycastle.org/csharp/index.html) - A lightweight cryptography API for Java and C#.
164-
- [Json.NET](https://github.com/JamesNK/Newtonsoft.Json) - Popular high-performance JSON framework for .NET.
163+
- [Bouncy Castle](https://www.bouncycastle.org/csharp/index.html) - A lightweight cryptography API for Java and C#.
165164
- [Peter O. CBOR](https://github.com/peteroupc/CBOR) - A C# implementation of Concise Binary Object Representation (RFC 7049).
166165

167166
## Related Projects

Scripts/Update-Licenses.ps1

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,6 @@ $products = @(
1919
}, @{
2020
Name = 'Bouncy Castle';
2121
LicenseUrl = 'https://raw.githubusercontent.com/bcgit/bc-csharp/master/crypto/License.html'
22-
}, @{
23-
Name = 'Json.NET';
24-
LicenseUrl = 'https://raw.githubusercontent.com/JamesNK/Newtonsoft.Json/master/LICENSE.md'
2522
}, @{
2623
Name = 'Peter O. CBOR Library';
2724
LicenseUrl = 'https://raw.githubusercontent.com/peteroupc/CBOR/master/LICENSE.md'

Src/DSInternals.Common.Test/DSInternals.Common.Test.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<Project Sdk="MSTest.Sdk">
22
<PropertyGroup>
33
<TargetFrameworks>net48;net8.0-windows</TargetFrameworks>
4+
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
45
</PropertyGroup>
56
<ItemGroup>
67
<ProjectReference Include="..\DSInternals.Common\DSInternals.Common.csproj" />

Src/DSInternals.Common.Test/KeyCredentialTester.cs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Security.Cryptography.X509Certificates;
44
using DSInternals.Common.Data;
55
using Microsoft.VisualStudio.TestTools.UnitTesting;
6+
using System.Text.Json;
67

78
namespace DSInternals.Common.Test
89
{
@@ -11,6 +12,64 @@ public class KeyCredentialTester
1112
{
1213
private const string DummyDN = "CN=Account,DC=contoso,DC=com";
1314

15+
private static KeyCredential CreateSampleKey()
16+
{
17+
byte[] publicKey = "525341310008000003000000000100000000000000000000010001C1A78914457758B0B13C70C710C7F8548F3F9ED56AD4640B6E6A112655C98ECAC1CBD68A298F5686C08439428A97FE6FDF58D78EA481905182BAD684C2D9C5CDE1CDE34AA19742E8BBF58B953EAC4C562FCF598CC176B02DBE9FFFEF5937A65815C236F92892F7E511A1FEDD5483CB33F1EA715D68106180DED2432A293367114A6E325E62F93F73D7ECE4B6A2BCDB829D95C8645C3073B94BA7CB7515CD29042F0967201C6E24A77821E92A6C756DF79841ACBAAE11D90CA03B9FCD24EF9E304B5D35248A7BD70557399960277058AE3E99C7C7E2284858B7BF8B08CDD286964186A50A7FCBCC6A24F00FEE5B9698BBD3B1AEAD0CE81FEA461C0ABD716843A5".HexToBinary();
18+
Guid deviceId = Guid.Parse("47f577e3-d2d0-4a0a-8aca-e0501098bde4");
19+
DateTime creationTime = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc);
20+
return new KeyCredential(publicKey, deviceId, DummyDN, creationTime);
21+
}
22+
23+
[TestMethod]
24+
public void KeyCredential_RoundTrip_DoubleQuoted()
25+
{
26+
KeyCredential key = CreateSampleKey();
27+
string json = key.ToJson();
28+
KeyCredential result = KeyCredential.ParseJson(json);
29+
Assert.IsNotNull(result);
30+
Assert.AreEqual(key.Identifier, result.Identifier);
31+
Assert.AreEqual(key.DeviceId, result.DeviceId);
32+
Assert.AreEqual(key.CreationTime, result.CreationTime);
33+
}
34+
35+
[TestMethod]
36+
public void KeyCredential_Deserialize_SingleQuoted_Input()
37+
{
38+
KeyCredential key = CreateSampleKey();
39+
string json = key.ToJson().Replace('"', '\'');
40+
KeyCredential result = KeyCredential.ParseJson(json);
41+
Assert.IsNotNull(result);
42+
Assert.AreEqual(key.DeviceId, result.DeviceId);
43+
}
44+
45+
[TestMethod]
46+
public void KeyCredential_Deserialize_TrailingComma_And_Comments()
47+
{
48+
KeyCredential key = CreateSampleKey();
49+
string json = key.ToJson();
50+
string jsonWithComment = json.Insert(1, "\n // comment\n");
51+
jsonWithComment = jsonWithComment.Substring(0, jsonWithComment.Length - 1) + ",\n}";
52+
KeyCredential result = KeyCredential.ParseJson(jsonWithComment);
53+
Assert.IsNotNull(result);
54+
Assert.AreEqual(key.DeviceId, result.DeviceId);
55+
}
56+
57+
[TestMethod]
58+
public void Parse_SingleQuoted_WithEscapedApostrophe_Works()
59+
{
60+
var jsonSingleQuoted = "{ 'OwnerDN':'CN=O\\'Connor,DC=contoso,DC=com', 'IsComputerKey': false }";
61+
var obj = KeyCredential.ParseJson(jsonSingleQuoted);
62+
Assert.IsNotNull(obj);
63+
}
64+
65+
[TestMethod]
66+
[ExpectedException(typeof(JsonException))]
67+
public void Parse_BadJson_StillThrows()
68+
{
69+
var bad = "{ \"OwnerDN\": \"CN=User,DC=contoso,DC=com\" ";
70+
_ = KeyCredential.ParseJson(bad);
71+
}
72+
1473
[TestMethod]
1574
public void KeyCredential_Parse_NonMFAKey()
1675
{

Src/DSInternals.Common.Test/SearchableDeviceKeyTester.cs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
using System;
22
using DSInternals.Common.Data;
3+
using DSInternals.Common.Serialization;
34
using Microsoft.VisualStudio.TestTools.UnitTesting;
4-
using Newtonsoft.Json;
5-
using Newtonsoft.Json.Linq;
5+
using System.Text.Json;
66

77
namespace DSInternals.Common.Test
88
{
@@ -38,7 +38,10 @@ public void SearchableDeviceKey_Parse_FIDO_Input1()
3838
Assert.AreEqual("cb69481e-8ff7-4039-93ec-0a2729a154a8", keyCredential.FidoKeyMaterial.AuthenticatorData.AttestedCredentialData.AaGuid.ToString());
3939

4040
// Serialize the object again and compare with the original
41-
Assert.AreEqual(JToken.Parse(jsonData).ToString(Formatting.None), keyCredential.ToJson());
41+
string normalized = JsonSerializer.Serialize(
42+
DsiJson.DeserializeLenient<JsonElement>(jsonData),
43+
DsiJson.Options);
44+
Assert.AreEqual(normalized, keyCredential.ToJson());
4245
}
4346

4447
[TestMethod]
@@ -91,7 +94,10 @@ public void SearchableDeviceKey_Serialize_FIDO_Input2()
9194
Assert.AreEqual(2, keyCredential.FidoKeyMaterial.AttestationCertificates.Count);
9295

9396
// Serialize the object again and compare with the original
94-
Assert.AreEqual(JToken.Parse(jsonData).ToString(Formatting.None), keyCredential.ToJson());
97+
string normalized = JsonSerializer.Serialize(
98+
DsiJson.DeserializeLenient<JsonElement>(jsonData),
99+
DsiJson.Options);
100+
Assert.AreEqual(normalized, keyCredential.ToJson());
95101
}
96102

97103
[TestMethod]
@@ -119,7 +125,10 @@ public void SearchableDeviceKey_Parse_NGC()
119125
Assert.AreEqual("cbad3c94-b480-4fa6-9187-ff1ed42c4479", parsedKey.DeviceId.Value.ToString().ToLowerInvariant());
120126

121127
// Serialize the object again and compare with the original
122-
Assert.AreEqual(JToken.Parse(jsonData).ToString(Formatting.None), parsedKey.ToJson());
128+
string normalized = JsonSerializer.Serialize(
129+
DsiJson.DeserializeLenient<JsonElement>(jsonData),
130+
DsiJson.Options);
131+
Assert.AreEqual(normalized, parsedKey.ToJson());
123132

124133
// Re-generate the identifier and check that it matches the value in AAD.
125134
var generatedKey = new KeyCredential(
@@ -131,7 +140,7 @@ public void SearchableDeviceKey_Parse_NGC()
131140
Assert.AreEqual(parsedKey.Identifier, generatedKey.Identifier);
132141

133142
// Serialize the generated object and compare with the original
134-
Assert.AreEqual(JToken.Parse(jsonData).ToString(Formatting.None), generatedKey.ToJson());
143+
Assert.AreEqual(normalized, generatedKey.ToJson());
135144
}
136145

137146
[TestMethod]

Src/DSInternals.Common.Test/packages.lock.json

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@
7373
},
7474
"Microsoft.Bcl.AsyncInterfaces": {
7575
"type": "Transitive",
76-
"resolved": "6.0.0",
77-
"contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==",
76+
"resolved": "9.0.8",
77+
"contentHash": "mdq9WaHnRJBvmhbDvoEk9aIEjpoW1cmA6wGuE0/eV49NT/0Z/d+NauB4jzw2Dyi/TndebYfjAYHCOXeB0c/Qhg==",
7878
"dependencies": {
7979
"System.Threading.Tasks.Extensions": "4.5.4"
8080
}
@@ -172,11 +172,6 @@
172172
"resolved": "3.10.2",
173173
"contentHash": "WS9GHohjOzf653bqCSxplq3T25LAwFVeVrgLuotTiPDu+bO5bD7RgvXbkLqRqZGE2Qyuk/dbQpqa18PYAMXjMg=="
174174
},
175-
"Newtonsoft.Json": {
176-
"type": "Transitive",
177-
"resolved": "13.0.3",
178-
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
179-
},
180175
"PeterO.Cbor": {
181176
"type": "Transitive",
182177
"resolved": "4.5.5",
@@ -229,6 +224,16 @@
229224
"System.ValueTuple": "4.5.0"
230225
}
231226
},
227+
"System.IO.Pipelines": {
228+
"type": "Transitive",
229+
"resolved": "9.0.8",
230+
"contentHash": "6vPmJt73mgUo1gzc/OcXlJvClz/2jxZ4TQPRfriVaLoGRH2mye530D9WHJYbFQRNMxF3PWCoeofsFdCyN7fLzA==",
231+
"dependencies": {
232+
"System.Buffers": "4.5.1",
233+
"System.Memory": "4.5.5",
234+
"System.Threading.Tasks.Extensions": "4.5.4"
235+
}
236+
},
232237
"System.Memory": {
233238
"type": "Transitive",
234239
"resolved": "4.5.5",
@@ -265,25 +270,25 @@
265270
},
266271
"System.Text.Encodings.Web": {
267272
"type": "Transitive",
268-
"resolved": "6.0.1",
269-
"contentHash": "E5M5AE2OUTlCrf4omZvzzziUJO9CofBl+lXHaN5IKePPJvHqYFYYpaDPgCpR4VwaFbEebfnjOxxEBtPtsqAxpQ==",
273+
"resolved": "9.0.8",
274+
"contentHash": "W+LotQsM4wBJ4PG7uRkyul4wqL4Fz7R4ty6uXrCNZUhbaHYANgrPaYR2ZpMVpdCjQEJ17Jr1NMN8hv4SHaHY4A==",
270275
"dependencies": {
271276
"System.Buffers": "4.5.1",
272-
"System.Memory": "4.5.4",
277+
"System.Memory": "4.5.5",
273278
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
274279
}
275280
},
276281
"System.Text.Json": {
277282
"type": "Transitive",
278-
"resolved": "6.0.11",
279-
"contentHash": "xqC1HIbJMBFhrpYs76oYP+NAskNVjc6v73HqLal7ECRDPIp4oRU5pPavkD//vNactCn9DA2aaald/I5N+uZ5/g==",
283+
"resolved": "9.0.8",
284+
"contentHash": "mIQir9jBqk0V7X0Nw5hzPJZC8DuGdf+2DS3jAVsr6rq5+/VyH5rza0XGcONJUWBrZ+G6BCwNyjWYd9lncBu48A==",
280285
"dependencies": {
281-
"Microsoft.Bcl.AsyncInterfaces": "6.0.0",
286+
"Microsoft.Bcl.AsyncInterfaces": "9.0.8",
282287
"System.Buffers": "4.5.1",
283-
"System.Memory": "4.5.4",
284-
"System.Numerics.Vectors": "4.5.0",
288+
"System.IO.Pipelines": "9.0.8",
289+
"System.Memory": "4.5.5",
285290
"System.Runtime.CompilerServices.Unsafe": "6.0.0",
286-
"System.Text.Encodings.Web": "6.0.1",
291+
"System.Text.Encodings.Web": "9.0.8",
287292
"System.Threading.Tasks.Extensions": "4.5.4",
288293
"System.ValueTuple": "4.5.0"
289294
}
@@ -304,11 +309,11 @@
304309
"dsinternals.common": {
305310
"type": "Project",
306311
"dependencies": {
307-
"Newtonsoft.Json": "[13.0.3, )",
308312
"PeterO.Cbor": "[4.5.5, )",
309313
"System.Buffers": "[4.5.1, )",
310314
"System.Formats.Asn1": "[9.0.8, )",
311315
"System.Memory": "[4.5.5, )",
316+
"System.Text.Json": "[9.0.8, )",
312317
"System.ValueTuple": "[4.6.1, )"
313318
}
314319
}
@@ -486,8 +491,8 @@
486491
},
487492
"Newtonsoft.Json": {
488493
"type": "Transitive",
489-
"resolved": "13.0.3",
490-
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
494+
"resolved": "13.0.1",
495+
"contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A=="
491496
},
492497
"PeterO.Cbor": {
493498
"type": "Transitive",
@@ -559,7 +564,6 @@
559564
"dsinternals.common": {
560565
"type": "Project",
561566
"dependencies": {
562-
"Newtonsoft.Json": "[13.0.3, )",
563567
"PeterO.Cbor": "[4.5.5, )",
564568
"System.DirectoryServices": "[9.0.8, )"
565569
}

Src/DSInternals.Common/AzureAD/AzureADClient.cs

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
using System;
2-
using System.Collections;
2+
using System.Collections.Generic;
33
using System.Globalization;
44
using System.IO;
55
using System.Net;
66
using System.Net.Http;
77
using System.Net.Http.Headers;
88
using System.Text;
9+
using System.Text.Json;
910
using System.Threading.Tasks;
1011
using DSInternals.Common.Data;
1112
using DSInternals.Common.Exceptions;
12-
using Newtonsoft.Json;
13+
using DSInternals.Common.Serialization;
1314

1415
namespace DSInternals.Common.AzureAD
1516
{
@@ -31,7 +32,6 @@ public class AzureADClient : IDisposable
3132
private string _tenantId;
3233
private HttpClient _httpClient;
3334
private readonly string _batchSizeParameter;
34-
private JsonSerializer _jsonSerializer = JsonSerializer.CreateDefault();
3535

3636
public AzureADClient(string accessToken, Guid? tenantId = null, int batchSize = MaxBatchSize)
3737
{
@@ -117,17 +117,17 @@ public async Task SetUserAsync(string userPrincipalName, KeyCredential[] keyCred
117117
// Vaidate the input
118118
Validator.AssertNotNullOrEmpty(userPrincipalName, nameof(userPrincipalName));
119119

120-
var properties = new Hashtable() { { KeyCredentialAttributeName, keyCredentials } };
120+
var properties = new Dictionary<string, object> { { KeyCredentialAttributeName, keyCredentials } };
121121
await SetUserAsync(userPrincipalName, properties).ConfigureAwait(false);
122122
}
123123

124124
public async Task SetUserAsync(Guid objectId, KeyCredential[] keyCredentials)
125125
{
126-
var properties = new Hashtable() { { KeyCredentialAttributeName, keyCredentials } };
126+
var properties = new Dictionary<string, object> { { KeyCredentialAttributeName, keyCredentials } };
127127
await SetUserAsync(objectId.ToString(), properties).ConfigureAwait(false);
128128
}
129129

130-
private async Task SetUserAsync(string userIdentifier, Hashtable properties)
130+
private async Task SetUserAsync(string userIdentifier, Dictionary<string, object> properties)
131131
{
132132
// Build the request uri
133133
var url = new StringBuilder();
@@ -137,7 +137,7 @@ private async Task SetUserAsync(string userIdentifier, Hashtable properties)
137137
// TODO: Switch to HttpMethod.Patch after migrating to .NET Standard 2.1 / .NET 5
138138
using (var request = new HttpRequestMessage(new HttpMethod("PATCH"), url.ToString()))
139139
{
140-
request.Content = new StringContent(JsonConvert.SerializeObject(properties), Encoding.UTF8, JsonContentType);
140+
request.Content = new StringContent(JsonSerializer.Serialize(properties, DsiJson.Options), Encoding.UTF8, JsonContentType);
141141
await SendODataRequest<object>(request).ConfigureAwait(false);
142142
}
143143
}
@@ -155,30 +155,26 @@ private async Task<T> SendODataRequest<T>(HttpRequestMessage request)
155155
}
156156

157157
using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
158-
using (var streamReader = new StreamReader(responseStream))
159158
{
160159
if (s_odataContentType.MediaType.Equals(response.Content.Headers.ContentType.MediaType, StringComparison.InvariantCultureIgnoreCase))
161160
{
162-
// The response is a JSON document
163-
using (var jsonTextReader = new JsonTextReader(streamReader))
161+
if (response.StatusCode == HttpStatusCode.OK)
164162
{
165-
if (response.StatusCode == HttpStatusCode.OK)
166-
{
167-
return _jsonSerializer.Deserialize<T>(jsonTextReader);
168-
}
169-
else
170-
{
171-
// Translate OData response to an exception
172-
var error = _jsonSerializer.Deserialize<OdataErrorResponse>(jsonTextReader);
173-
throw error.GetException();
174-
}
163+
return await JsonSerializer.DeserializeAsync<T>(responseStream, DsiJson.Options).ConfigureAwait(false);
164+
}
165+
else
166+
{
167+
var error = await JsonSerializer.DeserializeAsync<OdataErrorResponse>(responseStream, DsiJson.Options).ConfigureAwait(false);
168+
throw error.GetException();
175169
}
176170
}
177171
else
178172
{
179-
// The response is not a JSON document, so we parse its first line as message text
180-
string message = await streamReader.ReadLineAsync().ConfigureAwait(false);
181-
throw new GraphApiException(message, response.StatusCode.ToString());
173+
using (var streamReader = new StreamReader(responseStream))
174+
{
175+
string message = await streamReader.ReadLineAsync().ConfigureAwait(false);
176+
throw new GraphApiException(message, response.StatusCode.ToString());
177+
}
182178
}
183179
}
184180
}

0 commit comments

Comments
 (0)