Files
semantic-kernel/dotnet/src/SemanticKernel.Core/Functions/KernelFunctionFromMethod.cs
T
SergeyMenshykh 4b0172d867 .Net: Implicit JSON to target type conversion (#4757)
### Motivation and Context
It would be very convenient and beneficial for both SK consumer code and
SK itself if there was an implicit mechanism for deserializing function
arguments provided as JSON into their actual types specified in the
function signature, similar to the implicit data type conversion
mechanism SK currently has.

Closes https://github.com/microsoft/semantic-kernel/issues/4696,
https://github.com/microsoft/semantic-kernel/issues/3126

### Description
The change adds the 'TryToDeserializeValue' fallback, which attempts to
deserialize the argument value into the target parameter type if all the
methods to convert the argument to its actual type have been exhausted.
2024-02-06 13:20:59 +00:00

762 lines
34 KiB
C#

// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.SemanticKernel.Text;
namespace Microsoft.SemanticKernel;
/// <summary>
/// Provides factory methods for creating <see cref="KernelFunction"/> instances backed by a .NET method.
/// </summary>
[DebuggerDisplay("{DebuggerDisplay,nq}")]
internal sealed class KernelFunctionFromMethod : KernelFunction
{
/// <summary>
/// Creates a <see cref="KernelFunction"/> instance for a method, specified via an <see cref="MethodInfo"/> instance
/// and an optional target object if the method is an instance method.
/// </summary>
/// <param name="method">The method to be represented via the created <see cref="KernelFunction"/>.</param>
/// <param name="target">The target object for the <paramref name="method"/> if it represents an instance method. This should be null if and only if <paramref name="method"/> is a static method.</param>
/// <param name="functionName">The name to use for the function. If null, it will default to one derived from the method represented by <paramref name="method"/>.</param>
/// <param name="description">The description to use for the function. If null, it will default to one derived from the method represented by <paramref name="method"/>, if possible (e.g. via a <see cref="DescriptionAttribute"/> on the method).</param>
/// <param name="parameters">Optional parameter descriptions. If null, it will default to one derived from the method represented by <paramref name="method"/>.</param>
/// <param name="returnParameter">Optional return parameter description. If null, it will default to one derived from the method represented by <paramref name="method"/>.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> to use for logging. If null, no logging will be performed.</param>
/// <returns>The created <see cref="KernelFunction"/> wrapper for <paramref name="method"/>.</returns>
public static KernelFunction Create(
MethodInfo method,
object? target = null,
string? functionName = null,
string? description = null,
IEnumerable<KernelParameterMetadata>? parameters = null,
KernelReturnParameterMetadata? returnParameter = null,
ILoggerFactory? loggerFactory = null)
{
Verify.NotNull(method);
if (!method.IsStatic && target is null)
{
throw new ArgumentNullException(nameof(target), "Target must not be null for an instance method.");
}
MethodDetails methodDetails = GetMethodDetails(functionName, method, target);
var result = new KernelFunctionFromMethod(
methodDetails.Function,
methodDetails.Name,
description ?? methodDetails.Description,
parameters?.ToList() ?? methodDetails.Parameters,
returnParameter ?? methodDetails.ReturnParameter);
if (loggerFactory?.CreateLogger(method.DeclaringType ?? typeof(KernelFunctionFromPrompt)) is ILogger logger &&
logger.IsEnabled(LogLevel.Trace))
{
logger.LogTrace("Created KernelFunction '{Name}' for '{MethodName}'", result.Name, method.Name);
}
return result;
}
/// <inheritdoc/>
protected override ValueTask<FunctionResult> InvokeCoreAsync(
Kernel kernel,
KernelArguments arguments,
CancellationToken cancellationToken)
{
return this._function(kernel, this, arguments, cancellationToken);
}
/// <inheritdoc/>
protected override async IAsyncEnumerable<TResult> InvokeStreamingCoreAsync<TResult>(
Kernel kernel,
KernelArguments arguments,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
FunctionResult functionResult = await this.InvokeCoreAsync(kernel, arguments, cancellationToken).ConfigureAwait(false);
if (functionResult.Value is TResult result)
{
yield return result;
yield break;
}
// If the function returns an IAsyncEnumerable<T>, we can stream the results directly.
// This helps to enable composition, with a KernelFunctionFromMethod that returns an
// Invoke{Prompt}StreamingAsync and returns its result enumerable directly.
if (functionResult.Value is IAsyncEnumerable<TResult> asyncEnumerable)
{
await foreach (TResult item in asyncEnumerable.WithCancellation(cancellationToken).ConfigureAwait(false))
{
yield return item;
}
yield break;
}
// Supports the following provided T types for Method streaming
if (typeof(TResult) == typeof(StreamingKernelContent) ||
typeof(TResult) == typeof(StreamingMethodContent))
{
if (functionResult.Value is not null)
{
yield return (TResult)(object)new StreamingMethodContent(functionResult.Value, functionResult.Metadata);
}
yield break;
}
throw new NotSupportedException($"Streaming function {this.Name} does not support type {typeof(TResult)}");
// We don't invoke the hook here as the InvokeCoreAsync will do that for us
}
/// <summary>
/// JSON serialized string representation of the function.
/// </summary>
public override string ToString() => JsonSerializer.Serialize(this, JsonOptionsCache.WriteIndented);
#region private
/// <summary>Delegate used to invoke the underlying delegate.</summary>
private delegate ValueTask<FunctionResult> ImplementationFunc(
Kernel kernel,
KernelFunction function,
KernelArguments arguments,
CancellationToken cancellationToken);
private static readonly object[] s_cancellationTokenNoneArray = new object[] { CancellationToken.None };
private readonly ImplementationFunc _function;
private record struct MethodDetails(string Name, string Description, ImplementationFunc Function, List<KernelParameterMetadata> Parameters, KernelReturnParameterMetadata ReturnParameter);
private KernelFunctionFromMethod(
ImplementationFunc implementationFunc,
string functionName,
string description,
IReadOnlyList<KernelParameterMetadata> parameters,
KernelReturnParameterMetadata returnParameter) :
base(functionName, description, parameters, returnParameter)
{
Verify.ValidFunctionName(functionName);
this._function = implementationFunc;
}
private static MethodDetails GetMethodDetails(string? functionName, MethodInfo method, object? target)
{
ThrowForInvalidSignatureIf(method.IsGenericMethodDefinition, method, "Generic methods are not supported");
if (functionName is null)
{
// Get the name to use for the function. If the function has a KernelFunction attribute and it contains a name, we use that.
// Otherwise, we use the name of the method, but strip off any "Async" suffix if it's {Value}Task-returning.
// We don't apply any heuristics to the value supplied by KernelFunction's Name so that it can always be used
// as a definitive override.
functionName = method.GetCustomAttribute<KernelFunctionAttribute>(inherit: true)?.Name?.Trim();
if (string.IsNullOrEmpty(functionName))
{
functionName = SanitizeMetadataName(method.Name!);
if (IsAsyncMethod(method) &&
functionName.EndsWith("Async", StringComparison.Ordinal) &&
functionName.Length > "Async".Length)
{
functionName = functionName.Substring(0, functionName.Length - "Async".Length);
}
}
}
Verify.ValidFunctionName(functionName);
// Build up a list of KernelParameterMetadata for the parameters we expect to be populated
// from arguments. Some arguments are populated specially, not from arguments, and thus
// we don't want to advertize their metadata, e.g. CultureInfo, ILoggerFactory, etc.
List<KernelParameterMetadata> argParameterViews = new();
// Get marshaling funcs for parameters and build up the parameter metadata.
var parameters = method.GetParameters();
var parameterFuncs = new Func<KernelFunction, Kernel, KernelArguments, CancellationToken, object?>[parameters.Length];
bool sawFirstParameter = false;
for (int i = 0; i < parameters.Length; i++)
{
(parameterFuncs[i], KernelParameterMetadata? parameterView) = GetParameterMarshalerDelegate(method, parameters[i], ref sawFirstParameter);
if (parameterView is not null)
{
argParameterViews.Add(parameterView);
}
}
// Check for param names conflict
Verify.ParametersUniqueness(argParameterViews);
// Get the return type and a marshaling func for the return value.
(Type returnType, Func<Kernel, KernelFunction, object?, ValueTask<FunctionResult>> returnFunc) = GetReturnValueMarshalerDelegate(method);
if (Nullable.GetUnderlyingType(returnType) is Type underlying)
{
// Unwrap the U from a Nullable<U> since everything is going through object, at which point Nullable<U> and a boxed U are indistinguishable.
returnType = underlying;
}
// Create the func
ValueTask<FunctionResult> Function(Kernel kernel, KernelFunction function, KernelArguments arguments, CancellationToken cancellationToken)
{
// Create the arguments.
object?[] args = parameterFuncs.Length != 0 ? new object?[parameterFuncs.Length] : Array.Empty<object?>();
for (int i = 0; i < args.Length; i++)
{
args[i] = parameterFuncs[i](function, kernel, arguments, cancellationToken);
}
// Invoke the method.
object? result = Invoke(method, target, args);
// Extract and return the result.
return returnFunc(kernel, function, result);
}
// And return the details.
return new MethodDetails
{
Function = Function,
Name = functionName!,
Description = method.GetCustomAttribute<DescriptionAttribute>(inherit: true)?.Description ?? "",
Parameters = argParameterViews,
ReturnParameter = new KernelReturnParameterMetadata()
{
ParameterType = returnType,
Description = method.ReturnParameter.GetCustomAttribute<DescriptionAttribute>(inherit: true)?.Description,
}
};
}
/// <summary>Gets whether a method has a known async return type.</summary>
private static bool IsAsyncMethod(MethodInfo method)
{
Type t = method.ReturnType;
if (t == typeof(Task) || t == typeof(ValueTask))
{
return true;
}
if (t.IsGenericType)
{
t = t.GetGenericTypeDefinition();
if (t == typeof(Task<>) || t == typeof(ValueTask<>) || t == typeof(IAsyncEnumerable<>))
{
return true;
}
}
return false;
}
/// <summary>
/// Gets a delegate for handling the marshaling of a parameter.
/// </summary>
private static (Func<KernelFunction, Kernel, KernelArguments, CancellationToken, object?>, KernelParameterMetadata?) GetParameterMarshalerDelegate(
MethodInfo method, ParameterInfo parameter, ref bool sawFirstParameter)
{
Type type = parameter.ParameterType;
// Handle special types.
// These are not reported as part of KernelParameterMetadata because they're not satisfied from arguments.
if (type == typeof(KernelFunction))
{
return (static (KernelFunction func, Kernel _, KernelArguments _, CancellationToken _) => func, null);
}
if (type == typeof(Kernel))
{
return (static (KernelFunction _, Kernel kernel, KernelArguments _, CancellationToken _) => kernel, null);
}
if (type == typeof(KernelArguments))
{
return (static (KernelFunction _, Kernel _, KernelArguments arguments, CancellationToken _) => arguments, null);
}
if (type == typeof(ILoggerFactory))
{
return ((KernelFunction _, Kernel kernel, KernelArguments _, CancellationToken _) => kernel.LoggerFactory, null);
}
if (type == typeof(ILogger))
{
return ((KernelFunction _, Kernel kernel, KernelArguments _, CancellationToken _) => kernel.LoggerFactory.CreateLogger(method?.DeclaringType ?? typeof(KernelFunctionFromPrompt)) ?? NullLogger.Instance, null);
}
if (type == typeof(IAIServiceSelector))
{
return ((KernelFunction _, Kernel kernel, KernelArguments _, CancellationToken _) => kernel.ServiceSelector, null);
}
if (type == typeof(CultureInfo) || type == typeof(IFormatProvider))
{
return (static (KernelFunction _, Kernel kernel, KernelArguments _, CancellationToken _) => kernel.Culture, null);
}
if (type == typeof(CancellationToken))
{
return (static (KernelFunction _, Kernel _, KernelArguments _, CancellationToken cancellationToken) => cancellationToken, null);
}
// Handle the special FromKernelServicesAttribute, which indicates that the parameter should be sourced from the kernel's services.
// As with the above, these are not reported as part of KernelParameterMetadata because they're not satisfied from arguments.
if (parameter.GetCustomAttribute<FromKernelServicesAttribute>() is FromKernelServicesAttribute fromKernelAttr)
{
return ((KernelFunction _, Kernel kernel, KernelArguments _, CancellationToken _) =>
{
// Try to resolve the service from kernel.Services, using the attribute's key if one was provided.
object? service = kernel.Services is IKeyedServiceProvider keyedServiceProvider ?
keyedServiceProvider.GetKeyedService(type, fromKernelAttr.ServiceKey) :
kernel.Services.GetService(type);
if (service is not null)
{
return service;
}
// The service wasn't available. If the parameter has a default value (typically null), use that.
if (parameter.HasDefaultValue)
{
return parameter.DefaultValue;
}
// Otherwise, fail.
throw new KernelException($"Missing service for function parameter '{parameter.Name}'",
new ArgumentException("Missing service for function parameter", parameter.Name));
}, null);
}
// Handle parameters to be satisfied from KernelArguments.
string name = SanitizeMetadataName(parameter.Name ?? "");
ThrowForInvalidSignatureIf(string.IsNullOrWhiteSpace(name), method, $"Parameter {parameter.Name}'s attribute defines an invalid name.");
var converter = GetConverter(type);
object? parameterFunc(KernelFunction _, Kernel kernel, KernelArguments arguments, CancellationToken __)
{
// 1. Use the value of the variable if it exists.
if (arguments.TryGetValue(name, out object? value))
{
return Process(value);
}
// 2. Otherwise, use the default value if there is one, sourced either from an attribute or the parameter's default.
if (parameter.HasDefaultValue)
{
return parameter.DefaultValue;
}
// 3. Otherwise, fail.
throw new KernelException($"Missing argument for function parameter '{name}'",
new ArgumentException("Missing argument for function parameter", name));
object? Process(object? value)
{
if (!type.IsAssignableFrom(value?.GetType()))
{
if (converter is not null)
{
try
{
return converter(value, kernel.Culture);
}
catch (Exception e) when (!e.IsCriticalException())
{
throw new ArgumentOutOfRangeException(name, value, e.Message);
}
}
if (value is not null && TryToDeserializeValue(value, type, out var deserializedValue))
{
return deserializedValue;
}
}
return value;
}
}
sawFirstParameter = true;
var parameterView = new KernelParameterMetadata(name)
{
Description = parameter.GetCustomAttribute<DescriptionAttribute>(inherit: true)?.Description,
DefaultValue = parameter.DefaultValue?.ToString(),
IsRequired = !parameter.IsOptional,
ParameterType = type,
};
return (parameterFunc, parameterView);
}
/// <summary>
/// Tries to deserialize the given value into an object of the specified target type.
/// </summary>
/// <param name="value">The value to be deserialized.</param>
/// <param name="targetType">The type of the object to deserialize the value into.</param>
/// <param name="deserializedValue">The deserialized object if the method succeeds; otherwise, null.</param>
/// <returns>true if the value is successfully deserialized; otherwise, false.</returns>
private static bool TryToDeserializeValue(object value, Type targetType, out object? deserializedValue)
{
try
{
deserializedValue = value switch
{
JsonDocument document => document.Deserialize(targetType),
JsonNode node => node.Deserialize(targetType),
JsonElement element => element.Deserialize(targetType),
// The JSON can be represented by other data types from various libraries. For example, JObject, JToken, and JValue from the Newtonsoft.Json library.
// Since we don't take dependencies on these libraries and don't have access to the types here,
// the only way to deserialize those types is to convert them to a string first by calling the 'ToString' method.
// Attempting to use the 'JsonSerializer.Serialize' method, instead of calling the 'ToString' directly on those types, can lead to unpredictable outcomes.
// For instance, the JObject for { "id": 28 } JSON is serialized into the string "{ "Id": [] }", and the deserialization fails with the
// following exception - "The JSON value could not be converted to System.Int32. Path: $.Id | LineNumber: 0 | BytePositionInLine: 7."
_ => JsonSerializer.Deserialize(value.ToString(), targetType)
};
return true;
}
catch (NotSupportedException)
{
// There is no compatible JsonConverter for targetType or its serializable members.
}
catch (JsonException)
{
// The JSON is invalid.
}
deserializedValue = null;
return false;
}
/// <summary>
/// Gets a delegate for handling the result value of a method, converting it into the <see cref="Task{FunctionResult}"/> to return from the invocation.
/// </summary>
private static (Type ReturnType, Func<Kernel, KernelFunction, object?, ValueTask<FunctionResult>> Marshaler) GetReturnValueMarshalerDelegate(MethodInfo method)
{
// Handle each known return type for the method
Type returnType = method.ReturnType;
// No return value, either synchronous (void) or asynchronous (Task / ValueTask).
if (returnType == typeof(void))
{
return (typeof(void), (static (_, function, _) =>
new ValueTask<FunctionResult>(new FunctionResult(function))));
}
if (returnType == typeof(Task))
{
return (typeof(void), async static (_, function, result) =>
{
await ((Task)ThrowIfNullResult(result)).ConfigureAwait(false);
return new FunctionResult(function);
}
);
}
if (returnType == typeof(ValueTask))
{
return (typeof(void), async static (_, function, result) =>
{
await ((ValueTask)ThrowIfNullResult(result)).ConfigureAwait(false);
return new FunctionResult(function);
}
);
}
// string (which is special as no marshaling is required), either synchronous (string) or asynchronous (Task<string> / ValueTask<string>)
if (returnType == typeof(string))
{
return (typeof(string), static (kernel, function, result) =>
{
var resultString = (string?)result;
return new ValueTask<FunctionResult>(new FunctionResult(function, resultString, kernel.Culture));
}
);
}
if (returnType == typeof(Task<string>))
{
return (typeof(string), async static (kernel, function, result) =>
{
var resultString = await ((Task<string>)ThrowIfNullResult(result)).ConfigureAwait(false);
return new FunctionResult(function, resultString, kernel.Culture);
}
);
}
if (returnType == typeof(ValueTask<string>))
{
return (typeof(string), async static (kernel, function, result) =>
{
var resultString = await ((ValueTask<string>)ThrowIfNullResult(result)).ConfigureAwait(false);
return new FunctionResult(function, resultString, kernel.Culture);
}
);
}
if (returnType == typeof(FunctionResult))
{
return (typeof(object), static (_, function, result) =>
{
var functionResult = (FunctionResult?)result;
return new ValueTask<FunctionResult>(functionResult ?? new FunctionResult(function));
}
);
}
if (returnType == typeof(Task<FunctionResult>))
{
return (typeof(object), async static (_, _, result) =>
{
var functionResult = await ((Task<FunctionResult>)ThrowIfNullResult(result)).ConfigureAwait(false);
return functionResult;
}
);
}
if (returnType == typeof(ValueTask<FunctionResult>))
{
return (typeof(object), async static (_, _, result) =>
{
var functionResult = await ((ValueTask<FunctionResult>)ThrowIfNullResult(result)).ConfigureAwait(false);
return functionResult;
}
);
}
// All other synchronous return types T.
if (!returnType.IsGenericType || returnType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
return (returnType, (kernel, function, result) =>
{
return new ValueTask<FunctionResult>(new FunctionResult(function, result, kernel.Culture));
}
);
}
// All other asynchronous return types
// Task<T>
if (returnType.GetGenericTypeDefinition() is Type genericTask &&
genericTask == typeof(Task<>) &&
returnType.GetProperty("Result", BindingFlags.Public | BindingFlags.Instance)?.GetGetMethod() is MethodInfo taskResultGetter)
{
return (taskResultGetter.ReturnType, async (kernel, function, result) =>
{
await ((Task)ThrowIfNullResult(result)).ConfigureAwait(false);
var taskResult = Invoke(taskResultGetter, result, Array.Empty<object>());
return new FunctionResult(function, taskResult, kernel.Culture);
}
);
}
// ValueTask<T>
if (returnType.GetGenericTypeDefinition() is Type genericValueTask &&
genericValueTask == typeof(ValueTask<>) &&
returnType.GetMethod("AsTask", BindingFlags.Public | BindingFlags.Instance) is MethodInfo valueTaskAsTask &&
valueTaskAsTask.ReturnType.GetProperty("Result", BindingFlags.Public | BindingFlags.Instance)?.GetGetMethod() is MethodInfo asTaskResultGetter)
{
return (asTaskResultGetter.ReturnType, async (kernel, function, result) =>
{
Task task = (Task)Invoke(valueTaskAsTask, ThrowIfNullResult(result), Array.Empty<object>())!;
await task.ConfigureAwait(false);
var taskResult = Invoke(asTaskResultGetter, task, Array.Empty<object>());
return new FunctionResult(function, taskResult, kernel.Culture);
}
);
}
// IAsyncEnumerable<T>
if (returnType.GetGenericTypeDefinition() is Type genericAsyncEnumerable && genericAsyncEnumerable == typeof(IAsyncEnumerable<>))
{
Type elementType = returnType.GetGenericArguments()[0];
MethodInfo? getAsyncEnumeratorMethod = typeof(IAsyncEnumerable<>)
.MakeGenericType(elementType)
.GetMethod("GetAsyncEnumerator");
if (getAsyncEnumeratorMethod is not null)
{
return (returnType, (kernel, function, result) =>
{
var asyncEnumerator = Invoke(getAsyncEnumeratorMethod, result, s_cancellationTokenNoneArray);
if (asyncEnumerator is not null)
{
return new ValueTask<FunctionResult>(new FunctionResult(function, asyncEnumerator, kernel.Culture));
}
return new ValueTask<FunctionResult>(new FunctionResult(function));
}
);
}
}
// Unrecognized return type.
throw GetExceptionForInvalidSignature(method, $"Unknown return type {returnType}");
// Throws an exception if a result is found to be null unexpectedly
static object ThrowIfNullResult(object? result) =>
result ??
throw new KernelException("Function returned null unexpectedly.");
}
/// <summary>Invokes the MethodInfo with the specified target object and arguments.</summary>
private static object? Invoke(MethodInfo method, object? target, object?[]? arguments)
{
object? result = null;
try
{
const BindingFlags BindingFlagsDoNotWrapExceptions = (BindingFlags)0x02000000; // BindingFlags.DoNotWrapExceptions on .NET Core 2.1+, ignored before then
result = method.Invoke(target, BindingFlagsDoNotWrapExceptions, binder: null, arguments, culture: null);
}
catch (TargetInvocationException e) when (e.InnerException is not null)
{
// If we're targeting .NET Framework, such that BindingFlags.DoNotWrapExceptions
// is ignored, the original exception will be wrapped in a TargetInvocationException.
// Unwrap it and throw that original exception, maintaining its stack information.
ExceptionDispatchInfo.Capture(e.InnerException).Throw();
}
return result;
}
/// <summary>Gets an exception that can be thrown indicating an invalid signature.</summary>
[DoesNotReturn]
private static Exception GetExceptionForInvalidSignature(MethodInfo method, string reason) =>
throw new KernelException($"Function '{method.Name}' is not supported by the kernel. {reason}");
/// <summary>Throws an exception indicating an invalid KernelFunctionFactory signature if the specified condition is not met.</summary>
private static void ThrowForInvalidSignatureIf([DoesNotReturnIf(true)] bool condition, MethodInfo method, string reason)
{
if (condition)
{
throw GetExceptionForInvalidSignature(method, reason);
}
}
/// <summary>
/// Gets a converter for type to ty conversion. For example, string to int, string to Guid, double to int, CustomType to string, etc.
/// </summary>
/// <param name="targetType">Specifies the target type into which a source type should be converted.</param>
/// <returns>The converter function if the target type is supported; otherwise, null.</returns>
/// <remarks>
/// The conversion function uses whatever TypeConverter is registered for the target type.
/// Conversion is first attempted using the current culture, and if that fails, it tries again
/// with the invariant culture. If both fail, an exception is thrown.
/// </remarks>
private static Func<object?, CultureInfo, object?>? GetConverter(Type targetType) =>
s_parsers.GetOrAdd(targetType, static targetType =>
{
// For nullables, parse as the inner type. We then just need to be careful to treat null as null,
// as the underlying parser might not be expecting null.
bool wasNullable = !targetType.IsValueType;
if (!wasNullable && targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
wasNullable = true;
targetType = Nullable.GetUnderlyingType(targetType)!;
}
// Finally, look up and use a type converter. Again, special-case null if it was actually Nullable<T>.
if (TypeConverterFactory.GetTypeConverter(targetType) is TypeConverter converter)
{
return (input, cultureInfo) =>
{
// This if block returns null if the target ValueType is nullable, or if the target type is a ReferenceType, which is inherently nullable.
// This prevents null from being handled by converters below, which may fail when converting from nulls or to the target type from nulls.
if (input is null && wasNullable)
{
return null;
}
object? Convert(CultureInfo culture)
{
if (input?.GetType() is Type type && converter.CanConvertFrom(type))
{
// This line performs string to type conversion
return converter.ConvertFrom(context: null, culture, input);
}
// This line performs implicit type conversion, e.g., int to long, byte to int, Guid to string, etc.
if (converter.CanConvertTo(targetType))
{
return converter.ConvertTo(context: null, culture, input, targetType);
}
// EnumConverter cannot convert integer, so we verify manually
if (targetType.IsEnum &&
(input is int ||
input is uint ||
input is long ||
input is ulong ||
input is short ||
input is ushort ||
input is byte ||
input is sbyte))
{
return Enum.ToObject(targetType, input);
}
throw new InvalidOperationException($"No converter found to convert from {targetType} to {input?.GetType()}.");
}
// First try to parse using the supplied culture (or current if none was supplied).
// If that fails, try with the invariant culture and allow any exception to propagate.
try
{
return Convert(cultureInfo);
}
catch (Exception e) when (!e.IsCriticalException() && cultureInfo != CultureInfo.InvariantCulture)
{
return Convert(CultureInfo.InvariantCulture);
}
};
}
// Unsupported type.
return null;
});
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private string DebuggerDisplay => string.IsNullOrWhiteSpace(this.Description) ? this.Name : $"{this.Name} ({this.Description})";
/// <summary>
/// Remove characters from method name that are valid in metadata but invalid for SK.
/// </summary>
private static string SanitizeMetadataName(string methodName) =>
s_invalidNameCharsRegex.Replace(methodName, "_");
/// <summary>Regex that flags any character other than ASCII digits or letters or the underscore.</summary>
private static readonly Regex s_invalidNameCharsRegex = new("[^0-9A-Za-z_]");
/// <summary>Parser functions for converting strings to parameter types.</summary>
private static readonly ConcurrentDictionary<Type, Func<object?, CultureInfo, object?>?> s_parsers = new();
#endregion
}