Skip to content

Commit 0ac09b7

Browse files
committed
Custom parsable and nullable
1 parent 178dd7a commit 0ac09b7

File tree

2 files changed

+194
-9
lines changed

2 files changed

+194
-9
lines changed

Diff for: src/Components/Endpoints/src/Binding/FormDataSerializerOptions.cs

+14-9
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,26 @@ internal bool IsSingleValueConverter(Type type)
2828

2929
internal FormDataConverter<T> ResolveConverter<T>()
3030
{
31-
if (!_converters.TryGetValue(typeof(T), out var converter))
31+
return (FormDataConverter<T>)_converters.GetOrAdd(typeof(T), CreateConverter, this);
32+
}
33+
34+
private static FormDataConverter CreateConverter(Type type, FormDataSerializerOptions options)
35+
{
36+
FormDataConverter? converter;
37+
foreach (var factory in options._factories)
3238
{
33-
throw new InvalidOperationException($"No converter registered for type '{typeof(T).FullName}'.");
39+
converter = factory(type, options);
40+
if (converter != null)
41+
{
42+
return converter;
43+
}
3444
}
3545

36-
return (FormDataConverter<T>)converter;
46+
throw new InvalidOperationException($"No converter registered for type '{type.FullName}'.");
3747
}
3848

3949
internal FormDataConverter ResolveConverter(Type type)
4050
{
41-
if (!_converters.TryGetValue(type, out var converter))
42-
{
43-
throw new InvalidOperationException($"No converter registered for type '{type.FullName}'.");
44-
}
45-
46-
return converter;
51+
return _converters.GetOrAdd(type, CreateConverter, this);
4752
}
4853
}

Diff for: src/Components/Endpoints/test/Binding/FormDataDeserializerTests.cs

+180
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics.CodeAnalysis;
45
using System.Globalization;
56
using Microsoft.Extensions.Primitives;
7+
using Newtonsoft.Json.Linq;
68

79
namespace Microsoft.AspNetCore.Components.Endpoints.Binding;
810

@@ -59,6 +61,56 @@ public void CanDeserialize_NullValues(Type type)
5961
Assert.Null(result);
6062
}
6163

64+
[Fact]
65+
public void CanDeserialize_CustomParsableTypes()
66+
{
67+
// Arrange
68+
var expected = new Point { X = 1, Y = 1 };
69+
var collection = new Dictionary<string, StringValues>() { ["value"] = new StringValues("(1,1)") };
70+
var reader = new FormDataReader(collection, CultureInfo.InvariantCulture);
71+
reader.PushPrefix("value");
72+
var options = new FormDataSerializerOptions();
73+
74+
// Act
75+
var result = FormDataDeserializer.Deserialize<Point>(reader, options);
76+
77+
// Assert
78+
Assert.Equal(expected, result);
79+
}
80+
81+
[Fact]
82+
public void CanDeserialize_NullableCustomParsableTypes()
83+
{
84+
// Arrange
85+
var expected = new ValuePoint { X = 1, Y = 1 };
86+
var collection = new Dictionary<string, StringValues>() { ["value"] = new StringValues("(1,1)") };
87+
var reader = new FormDataReader(collection, CultureInfo.InvariantCulture);
88+
reader.PushPrefix("value");
89+
var options = new FormDataSerializerOptions();
90+
91+
// Act
92+
var result = FormDataDeserializer.Deserialize<ValuePoint?>(reader, options);
93+
94+
// Assert
95+
Assert.Equal(expected, result);
96+
}
97+
98+
[Fact]
99+
public void CanDeserialize_NullableCustomParsableTypes_NullValue()
100+
{
101+
// Arrange
102+
var collection = new Dictionary<string, StringValues>() { };
103+
var reader = new FormDataReader(collection, CultureInfo.InvariantCulture);
104+
reader.PushPrefix("value");
105+
var options = new FormDataSerializerOptions();
106+
107+
// Act
108+
var result = FormDataDeserializer.Deserialize<ValuePoint?>(reader, options);
109+
110+
// Assert
111+
Assert.Null(result);
112+
}
113+
62114
public static TheoryData<string, Type, object> NullableBasicTypes
63115
{
64116
get
@@ -195,3 +247,131 @@ private object CallDeserialize(FormDataReader reader, FormDataSerializerOptions
195247
return method.MakeGenericMethod(type).Invoke(null, new object[] { reader, options })!;
196248
}
197249
}
250+
251+
internal class Point : IParsable<Point>, IEquatable<Point>
252+
{
253+
public int X { get; set; }
254+
public int Y { get; set; }
255+
256+
public static Point Parse(string s, IFormatProvider provider)
257+
{
258+
// Parses points. Points start with ( and end with ).
259+
// Points define two components, X and Y, separated by a comma.
260+
var components = s.Trim('(', ')').Split(',');
261+
if (components.Length != 2)
262+
{
263+
throw new FormatException("Invalid point format.");
264+
}
265+
var result = new Point();
266+
result.X = int.Parse(components[0], provider);
267+
result.Y = int.Parse(components[1], provider);
268+
return result;
269+
}
270+
271+
public static bool TryParse([NotNullWhen(true)] string s, IFormatProvider provider, [MaybeNullWhen(false)] out Point result)
272+
{
273+
// Try parse points is similar to Parse, but returns a bool to indicate success.
274+
// It also uses the out parameter to return the result.
275+
try
276+
{
277+
result = Parse(s, provider);
278+
return true;
279+
}
280+
catch (FormatException)
281+
{
282+
result = null;
283+
return false;
284+
}
285+
}
286+
287+
public override bool Equals(object obj)
288+
{
289+
return Equals(obj as Point);
290+
}
291+
292+
public bool Equals(Point other)
293+
{
294+
return other is not null &&
295+
X == other.X &&
296+
Y == other.Y;
297+
}
298+
299+
public override int GetHashCode()
300+
{
301+
return HashCode.Combine(X, Y);
302+
}
303+
304+
public static bool operator ==(Point left, Point right)
305+
{
306+
return EqualityComparer<Point>.Default.Equals(left, right);
307+
}
308+
309+
public static bool operator !=(Point left, Point right)
310+
{
311+
return !(left == right);
312+
}
313+
}
314+
315+
internal class ValuePoint : IParsable<ValuePoint>, IEquatable<ValuePoint>
316+
{
317+
public int X { get; set; }
318+
public int Y { get; set; }
319+
320+
public static ValuePoint Parse(string s, IFormatProvider provider)
321+
{
322+
// Parses points. Points start with ( and end with ).
323+
// Points define two components, X and Y, separated by a comma.
324+
var components = s.Trim('(', ')').Split(',');
325+
if (components.Length != 2)
326+
{
327+
throw new FormatException("Invalid point format.");
328+
}
329+
var result = new ValuePoint();
330+
result.X = int.Parse(components[0], provider);
331+
result.Y = int.Parse(components[1], provider);
332+
return result;
333+
}
334+
335+
public static bool TryParse([NotNullWhen(true)] string s, IFormatProvider provider, [MaybeNullWhen(false)] out ValuePoint result)
336+
{
337+
// Try parse points is similar to Parse, but returns a bool to indicate success.
338+
// It also uses the out parameter to return the result.
339+
try
340+
{
341+
result = Parse(s, provider);
342+
return true;
343+
}
344+
catch (FormatException)
345+
{
346+
result = null;
347+
return false;
348+
}
349+
}
350+
351+
public override bool Equals(object obj)
352+
{
353+
return Equals(obj as ValuePoint);
354+
}
355+
356+
public bool Equals(ValuePoint other)
357+
{
358+
return other is not null &&
359+
X == other.X &&
360+
Y == other.Y;
361+
}
362+
363+
public override int GetHashCode()
364+
{
365+
return HashCode.Combine(X, Y);
366+
}
367+
368+
public static bool operator ==(ValuePoint left, ValuePoint right)
369+
{
370+
return EqualityComparer<ValuePoint>.Default.Equals(left, right);
371+
}
372+
373+
public static bool operator !=(ValuePoint left, ValuePoint right)
374+
{
375+
return !(left == right);
376+
}
377+
}

0 commit comments

Comments
 (0)