Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 9d17ce1

Browse files
committed
Add support for DateOnly/TimeOnly
1 parent f82892d commit 9d17ce1

File tree

8 files changed

+265
-2
lines changed

8 files changed

+265
-2
lines changed

‎src/JsonApiDotNetCore.Annotations/Resources/Internal/RuntimeTypeConverter.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,18 @@ public static class RuntimeTypeConverter
8282
return isNullableTypeRequested ? (TimeSpan?)convertedValue : convertedValue;
8383
}
8484

85+
if (nonNullableType == typeof(DateOnly))
86+
{
87+
DateOnly convertedValue = DateOnly.Parse(stringValue, cultureInfo);
88+
return isNullableTypeRequested ? (DateOnly?)convertedValue : convertedValue;
89+
}
90+
91+
if (nonNullableType == typeof(TimeOnly))
92+
{
93+
TimeOnly convertedValue = TimeOnly.Parse(stringValue, cultureInfo);
94+
return isNullableTypeRequested ? (TimeOnly?)convertedValue : convertedValue;
95+
}
96+
8597
if (nonNullableType.IsEnum)
8698
{
8799
object convertedValue = Enum.Parse(nonNullableType, stringValue);

‎test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/ModelStateFakers.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Globalization;
12
using Bogus;
23
using TestBuildingBlocks;
34

@@ -8,6 +9,12 @@ namespace JsonApiDotNetCoreTests.IntegrationTests.InputValidation.ModelState;
89

910
internal sealed class ModelStateFakers : FakerContainer
1011
{
12+
private static readonly DateOnly MinCreatedOn = DateOnly.Parse("2000年01月01日", CultureInfo.InvariantCulture);
13+
private static readonly DateOnly MaxCreatedOn = DateOnly.Parse("2050年01月01日", CultureInfo.InvariantCulture);
14+
15+
private static readonly TimeOnly MinCreatedAt = TimeOnly.Parse("09:00:00", CultureInfo.InvariantCulture);
16+
private static readonly TimeOnly MaxCreatedAt = TimeOnly.Parse("17:30:00", CultureInfo.InvariantCulture);
17+
1118
private readonly Lazy<Faker<SystemVolume>> _lazySystemVolumeFaker = new(() =>
1219
new Faker<SystemVolume>()
1320
.UseSeed(GetFakerSeed())
@@ -18,7 +25,9 @@ internal sealed class ModelStateFakers : FakerContainer
1825
.UseSeed(GetFakerSeed())
1926
.RuleFor(systemFile => systemFile.FileName, faker => faker.System.FileName())
2027
.RuleFor(systemFile => systemFile.Attributes, faker => faker.Random.Enum(FileAttributes.Normal, FileAttributes.Hidden, FileAttributes.ReadOnly))
21-
.RuleFor(systemFile => systemFile.SizeInBytes, faker => faker.Random.Long(0, 1_000_000)));
28+
.RuleFor(systemFile => systemFile.SizeInBytes, faker => faker.Random.Long(0, 1_000_000))
29+
.RuleFor(systemFile => systemFile.CreatedOn, faker => faker.Date.BetweenDateOnly(MinCreatedOn, MaxCreatedOn))
30+
.RuleFor(systemFile => systemFile.CreatedAt, faker => faker.Date.BetweenTimeOnly(MinCreatedAt, MaxCreatedAt)));
2231

2332
private readonly Lazy<Faker<SystemDirectory>> _lazySystemDirectoryFaker = new(() =>
2433
new Faker<SystemDirectory>()

‎test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/ModelStateValidationTests.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Net;
22
using FluentAssertions;
33
using JsonApiDotNetCore.Serialization.Objects;
4+
using Microsoft.Extensions.DependencyInjection;
45
using TestBuildingBlocks;
56
using Xunit;
67

@@ -17,6 +18,12 @@ public ModelStateValidationTests(IntegrationTestContext<TestableStartup<ModelSta
1718

1819
testContext.UseController<SystemDirectoriesController>();
1920
testContext.UseController<SystemFilesController>();
21+
22+
testContext.ConfigureServicesBeforeStartup(services =>
23+
{
24+
// Polyfill for missing DateOnly/TimeOnly support in .NET 6 ModelState validation.
25+
services.AddDateOnlyTimeOnlyStringConverters();
26+
});
2027
}
2128

2229
[Fact]
@@ -123,6 +130,53 @@ public async Task Cannot_create_resource_with_invalid_attribute_value()
123130
error.Source.Pointer.Should().Be("/data/attributes/directoryName");
124131
}
125132

133+
[Fact]
134+
public async Task Cannot_create_resource_with_invalid_DateOnly_TimeOnly_attribute_value()
135+
{
136+
// Arrange
137+
SystemFile newFile = _fakers.SystemFile.Generate();
138+
139+
var requestBody = new
140+
{
141+
data = new
142+
{
143+
type = "systemFiles",
144+
attributes = new
145+
{
146+
fileName = newFile.FileName,
147+
attributes = newFile.Attributes,
148+
sizeInBytes = newFile.SizeInBytes,
149+
createdOn = DateOnly.MinValue,
150+
createdAt = TimeOnly.MinValue
151+
}
152+
}
153+
};
154+
155+
const string route = "/systemFiles";
156+
157+
// Act
158+
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync<Document>(route, requestBody);
159+
160+
// Assert
161+
httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity);
162+
163+
responseDocument.Errors.ShouldHaveCount(2);
164+
165+
ErrorObject error1 = responseDocument.Errors[0];
166+
error1.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
167+
error1.Title.Should().Be("Input validation failed.");
168+
error1.Detail.Should().StartWith("The field CreatedAt must be between ");
169+
error1.Source.ShouldNotBeNull();
170+
error1.Source.Pointer.Should().Be("/data/attributes/createdAt");
171+
172+
ErrorObject error2 = responseDocument.Errors[1];
173+
error2.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
174+
error2.Title.Should().Be("Input validation failed.");
175+
error2.Detail.Should().StartWith("The field CreatedOn must be between ");
176+
error2.Source.ShouldNotBeNull();
177+
error2.Source.Pointer.Should().Be("/data/attributes/createdOn");
178+
}
179+
126180
[Fact]
127181
public async Task Can_create_resource_with_valid_attribute_value()
128182
{

‎test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/SystemFile.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,12 @@ public sealed class SystemFile : Identifiable<int>
2020
[Attr]
2121
[Range(typeof(long), "1", "9223372036854775807")]
2222
public long SizeInBytes { get; set; }
23+
24+
[Attr]
25+
[Range(typeof(DateOnly), "2000年01月01日", "2050年01月01日")]
26+
public DateOnly CreatedOn { get; set; }
27+
28+
[Attr]
29+
[Range(typeof(TimeOnly), "09:00:00", "17:30:00")]
30+
public TimeOnly CreatedAt { get; set; }
2331
}

‎test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterDataTypeTests.cs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,62 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
247247
responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("someTimeSpan").With(value => value.Should().Be(resource.SomeTimeSpan));
248248
}
249249

250+
[Fact]
251+
public async Task Can_filter_equality_on_type_DateOnly()
252+
{
253+
// Arrange
254+
var resource = new FilterableResource
255+
{
256+
SomeDateOnly = DateOnly.FromDateTime(27.January(2003))
257+
};
258+
259+
await _testContext.RunOnDatabaseAsync(async dbContext =>
260+
{
261+
await dbContext.ClearTableAsync<FilterableResource>();
262+
dbContext.FilterableResources.AddRange(resource, new FilterableResource());
263+
await dbContext.SaveChangesAsync();
264+
});
265+
266+
string route = $"/filterableResources?filter=equals(someDateOnly,'{resource.SomeDateOnly:O}')";
267+
268+
// Act
269+
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync<Document>(route);
270+
271+
// Assert
272+
httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK);
273+
274+
responseDocument.Data.ManyValue.ShouldHaveCount(1);
275+
responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("someDateOnly").With(value => value.Should().Be(resource.SomeDateOnly));
276+
}
277+
278+
[Fact]
279+
public async Task Can_filter_equality_on_type_TimeOnly()
280+
{
281+
// Arrange
282+
var resource = new FilterableResource
283+
{
284+
SomeTimeOnly = new TimeOnly(23, 59, 59, 999)
285+
};
286+
287+
await _testContext.RunOnDatabaseAsync(async dbContext =>
288+
{
289+
await dbContext.ClearTableAsync<FilterableResource>();
290+
dbContext.FilterableResources.AddRange(resource, new FilterableResource());
291+
await dbContext.SaveChangesAsync();
292+
});
293+
294+
string route = $"/filterableResources?filter=equals(someTimeOnly,'{resource.SomeTimeOnly:O}')";
295+
296+
// Act
297+
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync<Document>(route);
298+
299+
// Assert
300+
httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK);
301+
302+
responseDocument.Data.ManyValue.ShouldHaveCount(1);
303+
responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("someTimeOnly").With(value => value.Should().Be(resource.SomeTimeOnly));
304+
}
305+
250306
[Fact]
251307
public async Task Cannot_filter_equality_on_incompatible_value()
252308
{
@@ -291,6 +347,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
291347
[InlineData(nameof(FilterableResource.SomeNullableDateTime))]
292348
[InlineData(nameof(FilterableResource.SomeNullableDateTimeOffset))]
293349
[InlineData(nameof(FilterableResource.SomeNullableTimeSpan))]
350+
[InlineData(nameof(FilterableResource.SomeNullableDateOnly))]
351+
[InlineData(nameof(FilterableResource.SomeNullableTimeOnly))]
294352
[InlineData(nameof(FilterableResource.SomeNullableEnum))]
295353
public async Task Can_filter_is_null_on_type(string propertyName)
296354
{
@@ -311,6 +369,8 @@ public async Task Can_filter_is_null_on_type(string propertyName)
311369
SomeNullableDateTime = 1.January(2001).AsUtc(),
312370
SomeNullableDateTimeOffset = 1.January(2001).AsUtc(),
313371
SomeNullableTimeSpan = TimeSpan.FromHours(1),
372+
SomeNullableDateOnly = DateOnly.FromDateTime(1.January(2001)),
373+
SomeNullableTimeOnly = new TimeOnly(1, 0),
314374
SomeNullableEnum = DayOfWeek.Friday
315375
};
316376

@@ -345,6 +405,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
345405
[InlineData(nameof(FilterableResource.SomeNullableDateTime))]
346406
[InlineData(nameof(FilterableResource.SomeNullableDateTimeOffset))]
347407
[InlineData(nameof(FilterableResource.SomeNullableTimeSpan))]
408+
[InlineData(nameof(FilterableResource.SomeNullableDateOnly))]
409+
[InlineData(nameof(FilterableResource.SomeNullableTimeOnly))]
348410
[InlineData(nameof(FilterableResource.SomeNullableEnum))]
349411
public async Task Can_filter_is_not_null_on_type(string propertyName)
350412
{
@@ -361,6 +423,8 @@ public async Task Can_filter_is_not_null_on_type(string propertyName)
361423
SomeNullableDateTime = 1.January(2001).AsUtc(),
362424
SomeNullableDateTimeOffset = 1.January(2001).AsUtc(),
363425
SomeNullableTimeSpan = TimeSpan.FromHours(1),
426+
SomeNullableDateOnly = DateOnly.FromDateTime(1.January(2001)),
427+
SomeNullableTimeOnly = new TimeOnly(1, 0),
364428
SomeNullableEnum = DayOfWeek.Friday
365429
};
366430

‎test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterOperatorTests.cs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@ public sealed class FilterOperatorTests : IClassFixture<IntegrationTestContext<T
3535
private const string TimeSpanInTheRange = "2:15:51:42.397";
3636
private const string TimeSpanUpperBound = "2:16:22:41.736";
3737

38+
private const string IsoDateOnlyLowerBound = "2000年10月22日";
39+
private const string IsoDateOnlyInTheRange = "2000年11月22日";
40+
private const string IsoDateOnlyUpperBound = "2000年12月22日";
41+
42+
private const string InvariantDateOnlyLowerBound = "10/22/2000";
43+
private const string InvariantDateOnlyInTheRange = "11/22/2000";
44+
private const string InvariantDateOnlyUpperBound = "12/22/2000";
45+
46+
private const string TimeOnlyLowerBound = "15:28:54.997";
47+
private const string TimeOnlyInTheRange = "15:51:42.397";
48+
private const string TimeOnlyUpperBound = "16:22:41.736";
49+
3850
private readonly IntegrationTestContext<TestableStartup<FilterDbContext>, FilterDbContext> _testContext;
3951

4052
public FilterOperatorTests(IntegrationTestContext<TestableStartup<FilterDbContext>, FilterDbContext> testContext)
@@ -556,6 +568,96 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
556568
responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("someTimeSpan").With(value => value.Should().Be(resource.SomeTimeSpan));
557569
}
558570

571+
[Theory]
572+
[InlineData(IsoDateOnlyLowerBound, IsoDateOnlyUpperBound, ComparisonOperator.LessThan, IsoDateOnlyInTheRange)]
573+
[InlineData(IsoDateOnlyLowerBound, IsoDateOnlyUpperBound, ComparisonOperator.LessThan, IsoDateOnlyUpperBound)]
574+
[InlineData(IsoDateOnlyLowerBound, IsoDateOnlyUpperBound, ComparisonOperator.LessOrEqual, IsoDateOnlyInTheRange)]
575+
[InlineData(IsoDateOnlyLowerBound, IsoDateOnlyUpperBound, ComparisonOperator.LessOrEqual, IsoDateOnlyLowerBound)]
576+
[InlineData(IsoDateOnlyUpperBound, IsoDateOnlyLowerBound, ComparisonOperator.GreaterThan, IsoDateOnlyInTheRange)]
577+
[InlineData(IsoDateOnlyUpperBound, IsoDateOnlyLowerBound, ComparisonOperator.GreaterThan, IsoDateOnlyLowerBound)]
578+
[InlineData(IsoDateOnlyUpperBound, IsoDateOnlyLowerBound, ComparisonOperator.GreaterOrEqual, IsoDateOnlyInTheRange)]
579+
[InlineData(IsoDateOnlyUpperBound, IsoDateOnlyLowerBound, ComparisonOperator.GreaterOrEqual, IsoDateOnlyUpperBound)]
580+
[InlineData(InvariantDateOnlyLowerBound, InvariantDateOnlyUpperBound, ComparisonOperator.LessThan, InvariantDateOnlyInTheRange)]
581+
[InlineData(InvariantDateOnlyLowerBound, InvariantDateOnlyUpperBound, ComparisonOperator.LessThan, InvariantDateOnlyUpperBound)]
582+
[InlineData(InvariantDateOnlyLowerBound, InvariantDateOnlyUpperBound, ComparisonOperator.LessOrEqual, InvariantDateOnlyInTheRange)]
583+
[InlineData(InvariantDateOnlyLowerBound, InvariantDateOnlyUpperBound, ComparisonOperator.LessOrEqual, InvariantDateOnlyLowerBound)]
584+
[InlineData(InvariantDateOnlyUpperBound, InvariantDateOnlyLowerBound, ComparisonOperator.GreaterThan, InvariantDateOnlyInTheRange)]
585+
[InlineData(InvariantDateOnlyUpperBound, InvariantDateOnlyLowerBound, ComparisonOperator.GreaterThan, InvariantDateOnlyLowerBound)]
586+
[InlineData(InvariantDateOnlyUpperBound, InvariantDateOnlyLowerBound, ComparisonOperator.GreaterOrEqual, InvariantDateOnlyInTheRange)]
587+
[InlineData(InvariantDateOnlyUpperBound, InvariantDateOnlyLowerBound, ComparisonOperator.GreaterOrEqual, InvariantDateOnlyUpperBound)]
588+
public async Task Can_filter_comparison_on_DateOnly(string matchingValue, string nonMatchingValue, ComparisonOperator filterOperator, string filterValue)
589+
{
590+
// Arrange
591+
var resource = new FilterableResource
592+
{
593+
SomeDateOnly = DateOnly.Parse(matchingValue, CultureInfo.InvariantCulture)
594+
};
595+
596+
var otherResource = new FilterableResource
597+
{
598+
SomeDateOnly = DateOnly.Parse(nonMatchingValue, CultureInfo.InvariantCulture)
599+
};
600+
601+
await _testContext.RunOnDatabaseAsync(async dbContext =>
602+
{
603+
await dbContext.ClearTableAsync<FilterableResource>();
604+
dbContext.FilterableResources.AddRange(resource, otherResource);
605+
await dbContext.SaveChangesAsync();
606+
});
607+
608+
string route = $"/filterableResources?filter={filterOperator.ToString().Camelize()}(someDateOnly,'{filterValue}')";
609+
610+
// Act
611+
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync<Document>(route);
612+
613+
// Assert
614+
httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK);
615+
616+
responseDocument.Data.ManyValue.ShouldHaveCount(1);
617+
responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("someDateOnly").With(value => value.Should().Be(resource.SomeDateOnly));
618+
}
619+
620+
[Theory]
621+
[InlineData(TimeOnlyLowerBound, TimeOnlyUpperBound, ComparisonOperator.LessThan, TimeOnlyInTheRange)]
622+
[InlineData(TimeOnlyLowerBound, TimeOnlyUpperBound, ComparisonOperator.LessThan, TimeOnlyUpperBound)]
623+
[InlineData(TimeOnlyLowerBound, TimeOnlyUpperBound, ComparisonOperator.LessOrEqual, TimeOnlyInTheRange)]
624+
[InlineData(TimeOnlyLowerBound, TimeOnlyUpperBound, ComparisonOperator.LessOrEqual, TimeOnlyLowerBound)]
625+
[InlineData(TimeOnlyUpperBound, TimeOnlyLowerBound, ComparisonOperator.GreaterThan, TimeOnlyInTheRange)]
626+
[InlineData(TimeOnlyUpperBound, TimeOnlyLowerBound, ComparisonOperator.GreaterThan, TimeOnlyLowerBound)]
627+
[InlineData(TimeOnlyUpperBound, TimeOnlyLowerBound, ComparisonOperator.GreaterOrEqual, TimeOnlyInTheRange)]
628+
[InlineData(TimeOnlyUpperBound, TimeOnlyLowerBound, ComparisonOperator.GreaterOrEqual, TimeOnlyUpperBound)]
629+
public async Task Can_filter_comparison_on_TimeOnly(string matchingValue, string nonMatchingValue, ComparisonOperator filterOperator, string filterValue)
630+
{
631+
// Arrange
632+
var resource = new FilterableResource
633+
{
634+
SomeTimeOnly = TimeOnly.Parse(matchingValue, CultureInfo.InvariantCulture)
635+
};
636+
637+
var otherResource = new FilterableResource
638+
{
639+
SomeTimeOnly = TimeOnly.Parse(nonMatchingValue, CultureInfo.InvariantCulture)
640+
};
641+
642+
await _testContext.RunOnDatabaseAsync(async dbContext =>
643+
{
644+
await dbContext.ClearTableAsync<FilterableResource>();
645+
dbContext.FilterableResources.AddRange(resource, otherResource);
646+
await dbContext.SaveChangesAsync();
647+
});
648+
649+
string route = $"/filterableResources?filter={filterOperator.ToString().Camelize()}(someTimeOnly,'{filterValue}')";
650+
651+
// Act
652+
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync<Document>(route);
653+
654+
// Assert
655+
httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK);
656+
657+
responseDocument.Data.ManyValue.ShouldHaveCount(1);
658+
responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("someTimeOnly").With(value => value.Should().Be(resource.SomeTimeOnly));
659+
}
660+
559661
[Theory]
560662
[InlineData("The fox jumped over the lazy dog", "Other", TextMatchKind.Contains, "jumped")]
561663
[InlineData("The fox jumped over the lazy dog", "the fox...", TextMatchKind.Contains, "The")]

‎test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterableResource.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,18 @@ public sealed class FilterableResource : Identifiable<int>
7777
[Attr]
7878
public TimeSpan? SomeNullableTimeSpan { get; set; }
7979

80+
[Attr]
81+
public DateOnly SomeDateOnly { get; set; }
82+
83+
[Attr]
84+
public DateOnly? SomeNullableDateOnly { get; set; }
85+
86+
[Attr]
87+
public TimeOnly SomeTimeOnly { get; set; }
88+
89+
[Attr]
90+
public TimeOnly? SomeNullableTimeOnly { get; set; }
91+
8092
[Attr]
8193
public DayOfWeek SomeEnum { get; set; }
8294

‎test/JsonApiDotNetCoreTests/JsonApiDotNetCoreTests.csproj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<TargetFramework>$(TargetFrameworkName)</TargetFramework>
44
</PropertyGroup>
@@ -11,9 +11,11 @@
1111

1212
<ItemGroup>
1313
<PackageReference Include="coverlet.collector" Version="$(CoverletVersion)" PrivateAssets="All" />
14+
<PackageReference Include="DateOnlyTimeOnly.AspNet" Version="2.0.*" />
1415
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="$(AspNetVersion)" />
1516
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="$(EFCoreVersion)" />
1617
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="$(EFCoreVersion)" />
1718
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(TestSdkVersion)" />
19+
<PackageReference Include="System.Text.Json" Version="7.0.*" />
1820
</ItemGroup>
1921
</Project>

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /