Skip to content

Commit 6508259

Browse files
committed
Add OpenAPI tests for using various data types in attributes that behave like values, such as Int128/BigInteger/Half/Uri/IPAddress/Version and various others
- Publicly exposes RuntimeTypeConverter.GetFriendlyTypeName(), which expands generic type arguments - Improved conversion from string to date/time values in RuntimeTypeConverter - Add conversion from string to System.Uri in RuntimeTypeConverter - Improved LiteralConstantExpression.GetStringValue for date/time values - Include original request body parse exception in meta stack trace - Use generic math in SumFilterParser example
1 parent 91c9392 commit 6508259

File tree

65 files changed

+9357
-148
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+9357
-148
lines changed

JsonApiDotNetCore.sln.DotSettings

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,7 @@
583583
<s:Boolean x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/UseRoslynLogicForEvidentTypes/@EntryValue">False</s:Boolean>
584584
<s:Boolean x:Key="/Default/CodeStyle/EditorConfig/EnableClangFormatSupport/@EntryValue">False</s:Boolean>
585585
<s:Boolean x:Key="/Default/CodeStyle/EditorConfig/EnableEditorConfigSupport/@EntryValue">False</s:Boolean>
586+
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IP/@EntryIndexedValue">IP</s:String>
586587
<s:Boolean x:Key="/Default/CodeStyle/Naming/CSharpNaming/ApplyAutoDetectedRules/@EntryValue">False</s:Boolean>
587588
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /&gt;</s:String>
588589
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /&gt;</s:String>

src/Examples/DapperExample/TranslationToSql/ParameterFormatter.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace DapperExample.TranslationToSql;
88
/// </summary>
99
internal sealed class ParameterFormatter
1010
{
11-
private static readonly HashSet<Type> NumericTypes =
11+
private static readonly HashSet<Type> SimpleTypes =
1212
[
1313
typeof(bool),
1414
typeof(int),
@@ -53,7 +53,7 @@ private void WriteValue(object? parameterValue, StringBuilder builder)
5353
{
5454
string value = (string)RuntimeTypeConverter.ConvertType(parameterValue, typeof(string))!;
5555

56-
if (NumericTypes.Contains(parameterValue.GetType()))
56+
if (SimpleTypes.Contains(parameterValue.GetType()))
5757
{
5858
builder.Append(value);
5959
}

src/JsonApiDotNetCore.Annotations/Resources/RuntimeTypeConverter.cs

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public static class RuntimeTypeConverter
5353
{
5454
if (!CanContainNull(type))
5555
{
56-
string targetTypeName = type.GetFriendlyTypeName();
56+
string targetTypeName = GetFriendlyTypeName(type);
5757
throw new FormatException($"Failed to convert 'null' to type '{targetTypeName}'.");
5858
}
5959

@@ -67,7 +67,9 @@ public static class RuntimeTypeConverter
6767
return value;
6868
}
6969

70-
string? stringValue = value is IFormattable cultureAwareValue ? cultureAwareValue.ToString(null, cultureInfo) : value.ToString();
70+
string? stringValue = value is IFormattable cultureAwareValue
71+
? cultureAwareValue.ToString(value is DateTime or DateTimeOffset or DateOnly or TimeOnly ? "O" : null, cultureInfo)
72+
: value.ToString();
7173

7274
if (string.IsNullOrEmpty(stringValue))
7375
{
@@ -115,6 +117,11 @@ public static class RuntimeTypeConverter
115117
return isNullableTypeRequested ? (TimeOnly?)convertedValue : convertedValue;
116118
}
117119

120+
if (nonNullableType == typeof(Uri))
121+
{
122+
return new Uri(stringValue);
123+
}
124+
118125
if (nonNullableType.IsEnum)
119126
{
120127
object convertedValue = Enum.Parse(nonNullableType, stringValue);
@@ -128,8 +135,8 @@ public static class RuntimeTypeConverter
128135
}
129136
catch (Exception exception) when (exception is FormatException or OverflowException or InvalidCastException or ArgumentException)
130137
{
131-
string runtimeTypeName = runtimeType.GetFriendlyTypeName();
132-
string targetTypeName = type.GetFriendlyTypeName();
138+
string runtimeTypeName = GetFriendlyTypeName(runtimeType);
139+
string targetTypeName = GetFriendlyTypeName(type);
133140

134141
throw new FormatException($"Failed to convert '{value}' of type '{runtimeTypeName}' to type '{targetTypeName}'.", exception);
135142
}
@@ -157,4 +164,27 @@ public static bool CanContainNull(Type type)
157164

158165
return type.IsValueType ? DefaultTypeCache.GetOrAdd(type, Activator.CreateInstance) : null;
159166
}
167+
168+
/// <summary>
169+
/// Gets the name of a type, including the names of its generic type arguments, without any namespaces.
170+
/// <example>
171+
/// <code><![CDATA[
172+
/// KeyValuePair<TimeSpan, Nullable<DateTimeOffset>>
173+
/// ]]></code>
174+
/// </example>
175+
/// </summary>
176+
public static string GetFriendlyTypeName(Type type)
177+
{
178+
ArgumentNullException.ThrowIfNull(type);
179+
180+
// Based on https://stackoverflow.com/questions/2581642/how-do-i-get-the-type-name-of-a-generic-type-argument.
181+
182+
if (type.IsGenericType)
183+
{
184+
string typeArguments = type.GetGenericArguments().Select(GetFriendlyTypeName).Aggregate((firstType, secondType) => $"{firstType}, {secondType}");
185+
return $"{type.Name[..type.Name.IndexOf('`')]}<{typeArguments}>";
186+
}
187+
188+
return type.Name;
189+
}
160190
}

src/JsonApiDotNetCore.Annotations/TypeExtensions.cs

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -30,27 +30,4 @@ private static bool AreTypesEqual(Type left, Type right, bool isLeftGeneric)
3030
{
3131
return isLeftGeneric ? right.IsGenericType && right.GetGenericTypeDefinition() == left : left == right;
3232
}
33-
34-
/// <summary>
35-
/// Gets the name of a type, including the names of its generic type arguments.
36-
/// <example>
37-
/// <code><![CDATA[
38-
/// KeyValuePair<TimeSpan, Nullable<DateTimeOffset>>
39-
/// ]]></code>
40-
/// </example>
41-
/// </summary>
42-
public static string GetFriendlyTypeName(this Type type)
43-
{
44-
ArgumentNullException.ThrowIfNull(type);
45-
46-
// Based on https://stackoverflow.com/questions/2581642/how-do-i-get-the-type-name-of-a-generic-type-argument.
47-
48-
if (type.IsGenericType)
49-
{
50-
string typeArguments = type.GetGenericArguments().Select(GetFriendlyTypeName).Aggregate((firstType, secondType) => $"{firstType}, {secondType}");
51-
return $"{type.Name[..type.Name.IndexOf('`')]}<{typeArguments}>";
52-
}
53-
54-
return type.Name;
55-
}
5633
}

src/JsonApiDotNetCore/Queries/Expressions/LiteralConstantExpression.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ public LiteralConstantExpression(object typedValue, string stringValue)
3535
{
3636
ArgumentNullException.ThrowIfNull(typedValue);
3737

38-
return typedValue is IFormattable cultureAwareValue ? cultureAwareValue.ToString(null, CultureInfo.InvariantCulture) : typedValue.ToString();
38+
return typedValue is IFormattable cultureAwareValue
39+
? cultureAwareValue.ToString(typedValue is DateTime or DateTimeOffset or DateOnly or TimeOnly ? "O" : null, CultureInfo.InvariantCulture)
40+
: typedValue.ToString();
3941
}
4042

4143
public override TResult Accept<TArgument, TResult>(QueryExpressionVisitor<TArgument, TResult> visitor, TArgument argument)

src/JsonApiDotNetCore/Queries/Parsing/FilterParser.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -532,7 +532,8 @@ protected virtual ConstantValueConverter GetConstantValueConverterForType(Type d
532532
}
533533
catch (FormatException exception)
534534
{
535-
throw new QueryParseException($"Failed to convert '{stringValue}' of type 'String' to type '{destinationType.Name}'.", position, exception);
535+
string destinationTypeName = RuntimeTypeConverter.GetFriendlyTypeName(destinationType);
536+
throw new QueryParseException($"Failed to convert '{stringValue}' of type 'String' to type '{destinationTypeName}'.", position, exception);
536537
}
537538
};
538539
}

src/JsonApiDotNetCore/Serialization/JsonConverters/ResourceObjectConverter.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,15 +214,15 @@ public override ResourceObject Read(ref Utf8JsonReader reader, Type typeToConver
214214
{
215215
attributeValue = JsonSerializer.Deserialize(ref reader, property.PropertyType, options);
216216
}
217-
catch (JsonException)
217+
catch (JsonException exception)
218218
{
219219
// Inside a JsonConverter there is no way to know where in the JSON object tree we are. And the serializer
220220
// is unable to provide the correct position either. So we avoid an exception and postpone producing an error
221221
// response to the post-processing phase, by setting a sentinel value.
222222
var jsonElement = ReadSubTree<JsonElement>(ref reader, options);
223223

224224
attributeValue = new JsonInvalidAttributeInfo(attributeName, property.PropertyType, jsonElement.ToString(),
225-
jsonElement.ValueKind);
225+
jsonElement.ValueKind, exception);
226226
}
227227
}
228228

src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceObjectAdapter.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,14 @@ private static void AssertNoInvalidAttribute(object? attributeValue, RequestAdap
8686
{
8787
if (info == JsonInvalidAttributeInfo.Id)
8888
{
89-
throw new ModelConversionException(state.Position, "Resource ID is read-only.", null);
89+
throw new ModelConversionException(state.Position, "Resource ID is read-only.", null, innerException: info.InnerException);
9090
}
9191

92-
string typeName = info.AttributeType.GetFriendlyTypeName();
92+
string typeName = RuntimeTypeConverter.GetFriendlyTypeName(info.AttributeType);
9393

9494
throw new ModelConversionException(state.Position, "Incompatible attribute value found.",
95-
$"Failed to convert attribute '{info.AttributeName}' with value '{info.JsonValue}' of type '{info.JsonType}' to type '{typeName}'.");
95+
$"Failed to convert attribute '{info.AttributeName}' with value '{info.JsonValue}' of type '{info.JsonType}' to type '{typeName}'.",
96+
innerException: info.InnerException);
9697
}
9798
}
9899

src/JsonApiDotNetCore/Serialization/Request/JsonInvalidAttributeInfo.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ namespace JsonApiDotNetCore.Serialization.Request;
77
/// </summary>
88
internal sealed class JsonInvalidAttributeInfo
99
{
10-
public static readonly JsonInvalidAttributeInfo Id = new("id", typeof(string), "-", JsonValueKind.Undefined);
10+
public static readonly JsonInvalidAttributeInfo Id = new("id", typeof(string), "-", JsonValueKind.Undefined, null);
1111

1212
public string AttributeName { get; }
1313
public Type AttributeType { get; }
1414
public string? JsonValue { get; }
1515
public JsonValueKind JsonType { get; }
16+
public Exception? InnerException { get; }
1617

17-
public JsonInvalidAttributeInfo(string attributeName, Type attributeType, string? jsonValue, JsonValueKind jsonType)
18+
public JsonInvalidAttributeInfo(string attributeName, Type attributeType, string? jsonValue, JsonValueKind jsonType, Exception? innerException)
1819
{
1920
ArgumentNullException.ThrowIfNull(attributeName);
2021
ArgumentNullException.ThrowIfNull(attributeType);
@@ -23,5 +24,6 @@ public JsonInvalidAttributeInfo(string attributeName, Type attributeType, string
2324
AttributeType = attributeType;
2425
JsonValue = jsonValue;
2526
JsonType = jsonType;
27+
InnerException = innerException;
2628
}
2729
}

src/JsonApiDotNetCore/Serialization/Request/ModelConversionException.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ public sealed class ModelConversionException : Exception
1515
public HttpStatusCode? StatusCode { get; }
1616
public string? SourcePointer { get; }
1717

18-
public ModelConversionException(RequestAdapterPosition position, string? genericMessage, string? specificMessage, HttpStatusCode? statusCode = null)
19-
: base(genericMessage)
18+
public ModelConversionException(RequestAdapterPosition position, string? genericMessage, string? specificMessage, HttpStatusCode? statusCode = null,
19+
Exception? innerException = null)
20+
: base(genericMessage, innerException)
2021
{
2122
ArgumentNullException.ThrowIfNull(position);
2223

0 commit comments

Comments
 (0)