Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions JsonApiDotNetCore.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,7 @@
<s:Boolean x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/UseRoslynLogicForEvidentTypes/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/EditorConfig/EnableClangFormatSupport/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/EditorConfig/EnableEditorConfigSupport/@EntryValue">False</s:Boolean>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IP/@EntryIndexedValue">IP</s:String>
<s:Boolean x:Key="/Default/CodeStyle/Naming/CSharpNaming/ApplyAutoDetectedRules/@EntryValue">False</s:Boolean>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /&gt;</s:String>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace DapperExample.TranslationToSql;
/// </summary>
internal sealed class ParameterFormatter
{
private static readonly HashSet<Type> NumericTypes =
private static readonly HashSet<Type> SimpleTypes =
[
typeof(bool),
typeof(int),
Expand Down Expand Up @@ -53,7 +53,7 @@ private void WriteValue(object? parameterValue, StringBuilder builder)
{
string value = (string)RuntimeTypeConverter.ConvertType(parameterValue, typeof(string))!;

if (NumericTypes.Contains(parameterValue.GetType()))
if (SimpleTypes.Contains(parameterValue.GetType()))
{
builder.Append(value);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public static class RuntimeTypeConverter
{
if (!CanContainNull(type))
{
string targetTypeName = type.GetFriendlyTypeName();
string targetTypeName = GetFriendlyTypeName(type);
throw new FormatException($"Failed to convert 'null' to type '{targetTypeName}'.");
}

Expand All @@ -67,7 +67,9 @@ public static class RuntimeTypeConverter
return value;
}

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

if (string.IsNullOrEmpty(stringValue))
{
Expand Down Expand Up @@ -115,6 +117,11 @@ public static class RuntimeTypeConverter
return isNullableTypeRequested ? (TimeOnly?)convertedValue : convertedValue;
}

if (nonNullableType == typeof(Uri))
{
return new Uri(stringValue);
}

if (nonNullableType.IsEnum)
{
object convertedValue = Enum.Parse(nonNullableType, stringValue);
Expand All @@ -128,8 +135,8 @@ public static class RuntimeTypeConverter
}
catch (Exception exception) when (exception is FormatException or OverflowException or InvalidCastException or ArgumentException)
{
string runtimeTypeName = runtimeType.GetFriendlyTypeName();
string targetTypeName = type.GetFriendlyTypeName();
string runtimeTypeName = GetFriendlyTypeName(runtimeType);
string targetTypeName = GetFriendlyTypeName(type);

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

return type.IsValueType ? DefaultTypeCache.GetOrAdd(type, Activator.CreateInstance) : null;
}

/// <summary>
/// Gets the name of a type, including the names of its generic type arguments, without any namespaces.
/// <example>
/// <code><![CDATA[
/// KeyValuePair<TimeSpan, Nullable<DateTimeOffset>>
/// ]]></code>
/// </example>
/// </summary>
public static string GetFriendlyTypeName(Type type)
{
ArgumentNullException.ThrowIfNull(type);

// Based on https://stackoverflow.com/questions/2581642/how-do-i-get-the-type-name-of-a-generic-type-argument.

if (type.IsGenericType)
{
string typeArguments = type.GetGenericArguments().Select(GetFriendlyTypeName).Aggregate((firstType, secondType) => $"{firstType}, {secondType}");
return $"{type.Name[..type.Name.IndexOf('`')]}<{typeArguments}>";
}

return type.Name;
}
}
23 changes: 0 additions & 23 deletions src/JsonApiDotNetCore.Annotations/TypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,27 +30,4 @@ private static bool AreTypesEqual(Type left, Type right, bool isLeftGeneric)
{
return isLeftGeneric ? right.IsGenericType && right.GetGenericTypeDefinition() == left : left == right;
}

/// <summary>
/// Gets the name of a type, including the names of its generic type arguments.
/// <example>
/// <code><![CDATA[
/// KeyValuePair<TimeSpan, Nullable<DateTimeOffset>>
/// ]]></code>
/// </example>
/// </summary>
public static string GetFriendlyTypeName(this Type type)
{
ArgumentNullException.ThrowIfNull(type);

// Based on https://stackoverflow.com/questions/2581642/how-do-i-get-the-type-name-of-a-generic-type-argument.

if (type.IsGenericType)
{
string typeArguments = type.GetGenericArguments().Select(GetFriendlyTypeName).Aggregate((firstType, secondType) => $"{firstType}, {secondType}");
return $"{type.Name[..type.Name.IndexOf('`')]}<{typeArguments}>";
}

return type.Name;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ public LiteralConstantExpression(object typedValue, string stringValue)
{
ArgumentNullException.ThrowIfNull(typedValue);

return typedValue is IFormattable cultureAwareValue ? cultureAwareValue.ToString(null, CultureInfo.InvariantCulture) : typedValue.ToString();
return typedValue is IFormattable cultureAwareValue
? cultureAwareValue.ToString(typedValue is DateTime or DateTimeOffset or DateOnly or TimeOnly ? "O" : null, CultureInfo.InvariantCulture)
: typedValue.ToString();
}

public override TResult Accept<TArgument, TResult>(QueryExpressionVisitor<TArgument, TResult> visitor, TArgument argument)
Expand Down
3 changes: 2 additions & 1 deletion src/JsonApiDotNetCore/Queries/Parsing/FilterParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,8 @@ protected virtual ConstantValueConverter GetConstantValueConverterForType(Type d
}
catch (FormatException exception)
{
throw new QueryParseException($"Failed to convert '{stringValue}' of type 'String' to type '{destinationType.Name}'.", position, exception);
string destinationTypeName = RuntimeTypeConverter.GetFriendlyTypeName(destinationType);
throw new QueryParseException($"Failed to convert '{stringValue}' of type 'String' to type '{destinationTypeName}'.", position, exception);
}
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,15 +214,15 @@ public override ResourceObject Read(ref Utf8JsonReader reader, Type typeToConver
{
attributeValue = JsonSerializer.Deserialize(ref reader, property.PropertyType, options);
}
catch (JsonException)
catch (JsonException exception)
{
// Inside a JsonConverter there is no way to know where in the JSON object tree we are. And the serializer
// is unable to provide the correct position either. So we avoid an exception and postpone producing an error
// response to the post-processing phase, by setting a sentinel value.
var jsonElement = ReadSubTree<JsonElement>(ref reader, options);

attributeValue = new JsonInvalidAttributeInfo(attributeName, property.PropertyType, jsonElement.ToString(),
jsonElement.ValueKind);
jsonElement.ValueKind, exception);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,14 @@ private static void AssertNoInvalidAttribute(object? attributeValue, RequestAdap
{
if (info == JsonInvalidAttributeInfo.Id)
{
throw new ModelConversionException(state.Position, "Resource ID is read-only.", null);
throw new ModelConversionException(state.Position, "Resource ID is read-only.", null, innerException: info.InnerException);
}

string typeName = info.AttributeType.GetFriendlyTypeName();
string typeName = RuntimeTypeConverter.GetFriendlyTypeName(info.AttributeType);

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ namespace JsonApiDotNetCore.Serialization.Request;
/// </summary>
internal sealed class JsonInvalidAttributeInfo
{
public static readonly JsonInvalidAttributeInfo Id = new("id", typeof(string), "-", JsonValueKind.Undefined);
public static readonly JsonInvalidAttributeInfo Id = new("id", typeof(string), "-", JsonValueKind.Undefined, null);

public string AttributeName { get; }
public Type AttributeType { get; }
public string? JsonValue { get; }
public JsonValueKind JsonType { get; }
public Exception? InnerException { get; }

public JsonInvalidAttributeInfo(string attributeName, Type attributeType, string? jsonValue, JsonValueKind jsonType)
public JsonInvalidAttributeInfo(string attributeName, Type attributeType, string? jsonValue, JsonValueKind jsonType, Exception? innerException)
{
ArgumentNullException.ThrowIfNull(attributeName);
ArgumentNullException.ThrowIfNull(attributeType);
Expand All @@ -23,5 +24,6 @@ public JsonInvalidAttributeInfo(string attributeName, Type attributeType, string
AttributeType = attributeType;
JsonValue = jsonValue;
JsonType = jsonType;
InnerException = innerException;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ public sealed class ModelConversionException : Exception
public HttpStatusCode? StatusCode { get; }
public string? SourcePointer { get; }

public ModelConversionException(RequestAdapterPosition position, string? genericMessage, string? specificMessage, HttpStatusCode? statusCode = null)
: base(genericMessage)
public ModelConversionException(RequestAdapterPosition position, string? genericMessage, string? specificMessage, HttpStatusCode? statusCode = null,
Exception? innerException = null)
: base(genericMessage, innerException)
{
ArgumentNullException.ThrowIfNull(position);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Numerics;
using JsonApiDotNetCore.Queries.Expressions;
using JsonApiDotNetCore.Queries.Parsing;
using JsonApiDotNetCore.QueryStrings.FieldChains;
Expand All @@ -11,21 +12,6 @@ internal sealed class SumFilterParser(IResourceFactory resourceFactory)
{
private static readonly FieldChainPattern SingleToManyRelationshipChain = FieldChainPattern.Parse("M");

private static readonly HashSet<Type> NumericTypes =
[
typeof(sbyte),
typeof(byte),
typeof(short),
typeof(ushort),
typeof(int),
typeof(uint),
typeof(long),
typeof(ulong),
typeof(float),
typeof(double),
typeof(decimal)
];

protected override bool IsFunction(string name)
{
if (name == SumExpression.Keyword)
Expand Down Expand Up @@ -103,6 +89,6 @@ private QueryExpression ParseSumSelector()
private static bool IsNumericType(Type type)
{
Type innerType = Nullable.GetUnderlyingType(type) ?? type;
return NumericTypes.Contains(innerType);
return innerType.GetInterfaces().Any(@interface => @interface.Name == typeof(INumber<>).Name && @interface.Namespace == typeof(INumber<>).Namespace);
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using System.Net;
using System.Numerics;
using System.Text;
using FluentAssertions;
using JsonApiDotNetCore.Resources;
using Xunit;
Expand All @@ -17,13 +20,22 @@ public sealed class RuntimeTypeConverterTests
[InlineData(typeof(uint))]
[InlineData(typeof(long))]
[InlineData(typeof(ulong))]
[InlineData(typeof(Int128))]
[InlineData(typeof(UInt128))]
[InlineData(typeof(BigInteger))]
[InlineData(typeof(Half))]
[InlineData(typeof(float))]
[InlineData(typeof(double))]
[InlineData(typeof(decimal))]
[InlineData(typeof(Guid))]
[InlineData(typeof(DateTime))]
[InlineData(typeof(Complex))]
[InlineData(typeof(Rune))]
[InlineData(typeof(DateTimeOffset))]
[InlineData(typeof(DateTime))]
[InlineData(typeof(DateOnly))]
[InlineData(typeof(TimeOnly))]
[InlineData(typeof(TimeSpan))]
[InlineData(typeof(Guid))]
[InlineData(typeof(IPNetwork))]
[InlineData(typeof(DayOfWeek))]
public void Cannot_convert_null_to_value_type(Type type)
{
Expand All @@ -45,13 +57,22 @@ public void Cannot_convert_null_to_value_type(Type type)
[InlineData(typeof(uint?))]
[InlineData(typeof(long?))]
[InlineData(typeof(ulong?))]
[InlineData(typeof(Int128?))]
[InlineData(typeof(UInt128?))]
[InlineData(typeof(BigInteger?))]
[InlineData(typeof(Half?))]
[InlineData(typeof(float?))]
[InlineData(typeof(double?))]
[InlineData(typeof(decimal?))]
[InlineData(typeof(Guid?))]
[InlineData(typeof(DateTime?))]
[InlineData(typeof(Complex?))]
[InlineData(typeof(Rune?))]
[InlineData(typeof(DateTimeOffset?))]
[InlineData(typeof(DateTime?))]
[InlineData(typeof(DateOnly?))]
[InlineData(typeof(TimeOnly?))]
[InlineData(typeof(TimeSpan?))]
[InlineData(typeof(Guid?))]
[InlineData(typeof(IPNetwork?))]
[InlineData(typeof(DayOfWeek?))]
[InlineData(typeof(string))]
[InlineData(typeof(IFace))]
Expand Down Expand Up @@ -129,12 +150,18 @@ public void Returns_same_instance_for_interface()
[InlineData(typeof(long?), null)]
[InlineData(typeof(ulong), 0)]
[InlineData(typeof(ulong?), null)]
[InlineData(typeof(Int128?), null)]
[InlineData(typeof(UInt128?), null)]
[InlineData(typeof(BigInteger?), null)]
[InlineData(typeof(Half?), null)]
[InlineData(typeof(float), 0)]
[InlineData(typeof(float?), null)]
[InlineData(typeof(double), 0)]
[InlineData(typeof(double?), null)]
[InlineData(typeof(decimal), 0)]
[InlineData(typeof(decimal?), null)]
[InlineData(typeof(Complex?), null)]
[InlineData(typeof(Rune?), null)]
[InlineData(typeof(DayOfWeek), DayOfWeek.Sunday)]
[InlineData(typeof(DayOfWeek?), null)]
[InlineData(typeof(string), "")]
Expand Down
Loading