diff --git a/Sample/Sample.csproj b/Sample/Sample.csproj index b98e6e0..9790a9e 100644 --- a/Sample/Sample.csproj +++ b/Sample/Sample.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/GraphQL.DI/AutoInputObjectGraphType.cs b/src/GraphQL.DI/AutoInputObjectGraphType.cs new file mode 100644 index 0000000..0255fb6 --- /dev/null +++ b/src/GraphQL.DI/AutoInputObjectGraphType.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Text; +using GraphQL.Types; + +namespace GraphQL.DI +{ + /// + /// An automatically-generated graph type for public properties on the specified input model. + /// + public class AutoInputObjectGraphType : InputObjectGraphType + { + private const string ORIGINAL_EXPRESSION_PROPERTY_NAME = nameof(ORIGINAL_EXPRESSION_PROPERTY_NAME); + + /// + /// Creates a GraphQL type from . + /// + public AutoInputObjectGraphType() + { + var classType = typeof(TSourceType); + + //allow default name / description / obsolete tags to remain if not overridden + var nameAttribute = classType.GetCustomAttribute(); + if (nameAttribute != null) + Name = nameAttribute.Name; + else { + var name = GetDefaultName(); + if (name != null) + Name = name; + } + + var descriptionAttribute = classType.GetCustomAttribute(); + if (descriptionAttribute != null) + Description = descriptionAttribute.Description; + var obsoleteAttribute = classType.GetCustomAttribute(); + if (obsoleteAttribute != null) + DeprecationReason = obsoleteAttribute.Message; + //pull metadata + foreach (var metadataAttribute in classType.GetCustomAttributes()) + Metadata.Add(metadataAttribute.Key, metadataAttribute.Value); + + foreach (var property in GetRegisteredProperties()) { + if (property != null) { + var fieldType = ProcessProperty(property); + if (fieldType != null) + AddField(fieldType); + + } + } + } + + /// + /// Returns the default name assigned to the graph, or to leave the default setting set by the constructor. + /// + protected virtual string? GetDefaultName() + { + //if this class is inherited, do not set default name + if (GetType() != typeof(AutoInputObjectGraphType)) + return null; + + //without this code, the name would default to AutoInputObjectGraphType_1 + var name = typeof(TSourceType).Name.Replace('`', '_'); + if (name.EndsWith("Model", StringComparison.InvariantCulture)) + name = name.Substring(0, name.Length - "Model".Length); + return name; + } + + /// + /// Returns a list of properties that should have fields created for them. + /// + protected virtual IEnumerable GetRegisteredProperties() + => typeof(TSourceType).GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(x => x.CanWrite); + + /// + /// Processes the specified property and returns a + /// + /// + /// + protected virtual FieldType? ProcessProperty(PropertyInfo property) + { + //get the field name + string fieldName = property.Name; + var fieldNameAttribute = property.GetCustomAttribute(); + if (fieldNameAttribute != null) { + fieldName = fieldNameAttribute.Name; + } + if (fieldName == null) + return null; //ignore field if set to null (or Ignore is specified) + + //determine the graphtype of the field + var graphTypeAttribute = property.GetCustomAttribute(); + Type? graphType = graphTypeAttribute?.Type; + //infer the graphtype if it is not specified + if (graphType == null) { + graphType = InferGraphType(ApplyAttributes(GetTypeInformation(property))); + } + //load the description + string? description = property.GetCustomAttribute()?.Description; + //load the deprecation reason + string? obsoleteDescription = property.GetCustomAttribute()?.Message; + //load the default value + object? defaultValue = property.GetCustomAttribute()?.Value; + //create the field + var fieldType = new FieldType() { + Type = graphType, + Name = fieldName, + Description = description, + DeprecationReason = obsoleteDescription, + DefaultValue = defaultValue, + }; + //set name of property + fieldType.WithMetadata(ORIGINAL_EXPRESSION_PROPERTY_NAME, property.Name); + //load the metadata + foreach (var metaAttribute in property.GetCustomAttributes()) + fieldType.WithMetadata(metaAttribute.Key, metaAttribute.Value); + //return the field + return fieldType; + } + + /// + protected virtual IEnumerable<(Type Type, Nullability Nullable)> GetNullabilityInformation(PropertyInfo parameter) + { + return parameter.GetNullabilityInformation(); + } + + private static readonly Type[] _listTypes = new Type[] { + typeof(IEnumerable<>), + typeof(IList<>), + typeof(List<>), + typeof(ICollection<>), + typeof(IReadOnlyCollection<>), + typeof(IReadOnlyList<>), + typeof(HashSet<>), + typeof(ISet<>), + }; + + /// + /// Analyzes a property and returns a + /// struct containing type information necessary to select a graph type. + /// + protected virtual TypeInformation GetTypeInformation(PropertyInfo propertyInfo) + { + var isList = false; + var isNullableList = false; + var typeTree = GetNullabilityInformation(propertyInfo); + foreach (var type in typeTree) { + if (type.Type.IsArray) { + //unwrap type and mark as list + isList = true; + isNullableList = type.Nullable != Nullability.NonNullable; + continue; + } + if (type.Type.IsGenericType) { + var g = type.Type.GetGenericTypeDefinition(); + if (_listTypes.Contains(g)) { + //unwrap type and mark as list + isList = true; + isNullableList = type.Nullable != Nullability.NonNullable; + continue; + } + } + if (type.Type == typeof(IEnumerable) || type.Type == typeof(ICollection)) { + //assume list of nullable object + isList = true; + isNullableList = type.Nullable != Nullability.NonNullable; + break; + } + //found match + var nullable = type.Nullable != Nullability.NonNullable; + return new TypeInformation(propertyInfo, true, type.Type, nullable, isList, isNullableList, null); + } + //unknown type + return new TypeInformation(propertyInfo, true, typeof(object), true, isList, isNullableList, null); + } + + /// + /// Apply , , , + /// , and over + /// the supplied . + /// Override this method to enforce specific graph types for specific CLR types, or to implement custom + /// attributes to change graph type selection behavior. + /// + protected virtual TypeInformation ApplyAttributes(TypeInformation typeInformation) + => typeInformation.ApplyAttributes(typeInformation.MemberInfo); + + /// + /// Returns a GraphQL input type for a specified CLR type + /// + protected virtual Type InferGraphType(TypeInformation typeInformation) + => typeInformation.InferGraphType(); + + } +} diff --git a/src/GraphQL.DI/DIGraphAttribute.cs b/src/GraphQL.DI/DIGraphAttribute.cs new file mode 100644 index 0000000..9727b2b --- /dev/null +++ b/src/GraphQL.DI/DIGraphAttribute.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace GraphQL.DI +{ + /// + /// Marks a method's return graph type to be a specified DI graph type. + /// Useful when the return type cannot be inferred (often when it is of type ). + /// + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + public class DIGraphAttribute : Attribute + { + /// + /// Marks a method's return graph type to be a specified DI graph type. + /// + /// A type that inherits . + public DIGraphAttribute(Type graphBaseType) + { + GraphBaseType = graphBaseType; + } + + /// + /// The DI graph type that inherits . + /// + public Type GraphBaseType { get; } + } +} diff --git a/src/GraphQL.DI/DIObjectGraphBase.cs b/src/GraphQL.DI/DIObjectGraphBase.cs index 487a445..cd61d07 100644 --- a/src/GraphQL.DI/DIObjectGraphBase.cs +++ b/src/GraphQL.DI/DIObjectGraphBase.cs @@ -12,26 +12,21 @@ namespace GraphQL.DI /// This is a required base type of all DI-created graph types. may be /// used if the type is . /// - //this class is a placeholder for future support of properties or methods on the base class public abstract class DIObjectGraphBase : IDIObjectGraphBase, IResolveFieldContext { - //this would be an ideal spot to put public readonly fields for the resolving query, such as Schema, Metrics, Executor, and so on, rather than being inside the ResolveFieldContext instance. - //this could only contain fields that are not unique to a resolving field (such as Source), so as to not break multithreading support - //with DI, any objects necessary could be brought in via dependency injection (such as Schema), so they really don't need to be in here - /// /// The for the current field. /// public IResolveFieldContext Context { get; private set; } = null!; /// - public TSource Source => (TSource)Context.Source; + public TSource Source => (TSource)Context.Source!; /// public CancellationToken RequestAborted => Context.CancellationToken; /// - public IDictionary UserContext => Context.UserContext; + public IDictionary UserContext => Context.UserContext; /// public Metrics Metrics => Context.Metrics; @@ -40,10 +35,10 @@ public abstract class DIObjectGraphBase : IDIObjectGraphBase, Field IResolveFieldContext.FieldAst => Context.FieldAst; FieldType IResolveFieldContext.FieldDefinition => Context.FieldDefinition; IObjectGraphType IResolveFieldContext.ParentType => Context.ParentType; - IResolveFieldContext IResolveFieldContext.Parent => Context.Parent; - IDictionary IResolveFieldContext.Arguments => Context.Arguments; - object IResolveFieldContext.RootValue => Context.RootValue; - object IResolveFieldContext.Source => Context.Source; + IResolveFieldContext IResolveFieldContext.Parent => Context.Parent!; + IDictionary? IResolveFieldContext.Arguments => Context.Arguments; + object? IResolveFieldContext.RootValue => Context.RootValue; + object IResolveFieldContext.Source => Context.Source!; ISchema IResolveFieldContext.Schema => Context.Schema; Document IResolveFieldContext.Document => Context.Document; Operation IResolveFieldContext.Operation => Context.Operation; @@ -52,9 +47,9 @@ public abstract class DIObjectGraphBase : IDIObjectGraphBase, ExecutionErrors IResolveFieldContext.Errors => Context.Errors; IEnumerable IResolveFieldContext.Path => Context.Path; IEnumerable IResolveFieldContext.ResponsePath => Context.ResponsePath; - Dictionary IResolveFieldContext.SubFields => Context.SubFields; - IDictionary IResolveFieldContext.Extensions => Context.Extensions; - IServiceProvider IResolveFieldContext.RequestServices => Context.RequestServices; + Dictionary? IResolveFieldContext.SubFields => Context.SubFields; + IDictionary IResolveFieldContext.Extensions => Context.Extensions; + IServiceProvider IResolveFieldContext.RequestServices => Context.RequestServices!; IExecutionArrayPool IResolveFieldContext.ArrayPool => Context.ArrayPool; } @@ -62,7 +57,7 @@ public abstract class DIObjectGraphBase : IDIObjectGraphBase, /// This is a required base type of all DI-created graph types. may be /// used when the type is not . /// - public abstract class DIObjectGraphBase : DIObjectGraphBase, IDIObjectGraphBase + public abstract class DIObjectGraphBase : DIObjectGraphBase { } } diff --git a/src/GraphQL.DI/DIObjectGraphType.cs b/src/GraphQL.DI/DIObjectGraphType.cs index ec46544..27f4763 100644 --- a/src/GraphQL.DI/DIObjectGraphType.cs +++ b/src/GraphQL.DI/DIObjectGraphType.cs @@ -21,7 +21,24 @@ namespace GraphQL.DI /// Wraps a graph type for use with GraphQL. This class should be registered as a singleton /// within your dependency injection provider. /// - public class DIObjectGraphType : DIObjectGraphType where TDIGraph : IDIObjectGraphBase { } + public class DIObjectGraphType : DIObjectGraphType where TDIGraph : IDIObjectGraphBase + { + /// + protected override string? GetDefaultName() + { + //if this class is inherited, do not set default name + var thisType = GetType(); + if (thisType != typeof(DIObjectGraphType)) + return null; + + //without this code, the name would default to DIObjectGraphType_1 + var name = typeof(TDIGraph).Name.Replace('`', '_'); + if (name.EndsWith("Graph", StringComparison.InvariantCulture)) + name = name.Substring(0, name.Length - "Graph".Length); + return name; + } + } + /// /// Wraps a graph type for use with GraphQL. This class should be registered as a singleton /// within your dependency injection provider. @@ -39,7 +56,12 @@ public DIObjectGraphType() var nameAttribute = classType.GetCustomAttribute(); if (nameAttribute != null) Name = nameAttribute.Name; - //note: should probably take the default name from TDIGraph's name, rather than this type's name + else { + var name = GetDefaultName(); + if (name != null) + Name = name; + } + var descriptionAttribute = classType.GetCustomAttribute(); if (descriptionAttribute != null) Description = descriptionAttribute.Description; @@ -63,6 +85,23 @@ public DIObjectGraphType() AddField(fieldType); } + /// + /// Returns the default name assigned to the graph, or to leave the default setting set by the constructor. + /// + protected virtual string? GetDefaultName() + { + //if this class is inherited, do not set default name + var thisType = GetType(); + if (thisType != typeof(DIObjectGraphType)) + return null; + + //without this code, the name would default to DIObjectGraphType_1 + var name = typeof(TDIGraph).Name.Replace('`', '_'); + if (name.EndsWith("Graph", StringComparison.InvariantCulture)) + name = name.Substring(0, name.Length - "Graph".Length); + return name; + } + //grab some methods via reflection for us to use later private static readonly MethodInfo _getRequiredServiceMethod = typeof(ServiceProviderServiceExtensions).GetMethods(BindingFlags.Public | BindingFlags.Static).Single(x => x.Name == nameof(ServiceProviderServiceExtensions.GetRequiredService) && !x.IsGenericMethod); private static readonly MethodInfo _getOrCreateServiceMethod = typeof(ActivatorUtilities).GetMethods(BindingFlags.Public | BindingFlags.Static).Single(x => x.Name == nameof(ActivatorUtilities.GetServiceOrCreateInstance) && !x.IsGenericMethod); @@ -233,141 +272,19 @@ protected virtual IEnumerable GetMethodsToProcess() } - private (Nullability, IList?) GetMethodParameterNullability(ParameterInfo parameter) - { - Nullability nullableContext = GetMethodDefaultNullability(parameter.Member); - - foreach (var attribute in parameter.CustomAttributes) { - if (attribute.AttributeType.FullName == "System.Runtime.CompilerServices.NullableAttribute" && attribute.ConstructorArguments.Count == 1) { - var argType = attribute.ConstructorArguments[0].ArgumentType; - if (argType == typeof(byte)) { - return ((Nullability)(byte)attribute.ConstructorArguments[0].Value, null); - } else if (argType == typeof(byte[])) { - return (nullableContext, (IList)attribute.ConstructorArguments[0].Value); - } else { - throw new ApplicationException($"Could not interpret NullableAttribute on {parameter.Member.Name}.{parameter.Name}."); - } - } - } - return (nullableContext, null); - } - - /// - /// Returns a boolean indicating if the return value of a method is nullable. - /// - /// See . - /// - /// - protected virtual IEnumerable<(Type Type, Nullability Nullable)> GetNullability(ParameterInfo parameter) - { - var (nullableContext, nullabilityBytes) = GetMethodParameterNullability(parameter); - var list = new List<(Type, Nullability)>(); - var index = 0; - Consider(parameter.ParameterType, false); - if (nullabilityBytes != null && nullabilityBytes.Count != index) - throw new ApplicationException($"Unable to interpret nullability attributes for {parameter.Member.Name}.{parameter.Name}."); - return list; - - void Consider(Type t, bool nullableValueType) - { - if (t.IsValueType) { - if (t.IsGenericType) { - var g = t.GetGenericTypeDefinition(); - if (g == typeof(Nullable<>)) { - //do not increment index for Nullable - //do not add Nullable to the list but rather just add underlying type - Consider(t.GenericTypeArguments[0], true); - } else { - //generic structs that are not Nullable will contain a 0 in the array - index++; - list.Add((t, nullableValueType ? Nullability.Nullable : Nullability.NonNullable)); - for (int i = 0; i < t.GenericTypeArguments.Length; i++) { - Consider(t.GenericTypeArguments[i], false); - } - } - } else { - //do not increment index for concrete value types - list.Add((t, nullableValueType ? Nullability.Nullable : Nullability.NonNullable)); - } - return; - } - //pull the nullability flag from the array and increment index - var nullable = nullabilityBytes != null ? (Nullability)(byte)nullabilityBytes[index++].Value : nullableContext; - list.Add((t, nullable)); - - if (t.IsArray) { - Consider(t.GetElementType(), false); - } else if (t.IsGenericType) { - for (int i = 0; i < t.GenericTypeArguments.Length; i++) { - Consider(t.GenericTypeArguments[i], false); - } - } - } - } - - private Nullability GetMethodDefaultNullability(MemberInfo method) - { - var nullable = Nullability.Unknown; - - // check the parent type first to see if there's a nullable context attribute set for it - CheckDeclaringType(method.DeclaringType); - - // now check the method to see if there's a nullable context attribute set for it - var attribute = method.CustomAttributes.FirstOrDefault(x => - x.AttributeType.FullName == "System.Runtime.CompilerServices.NullableContextAttribute" && - x.ConstructorArguments.Count == 1 && - x.ConstructorArguments[0].ArgumentType == typeof(byte)); - if (attribute != null) { - nullable = (Nullability)(byte)attribute.ConstructorArguments[0].Value; - } - - return nullable; - - void CheckDeclaringType(Type parentType) - { - if (parentType.DeclaringType != null) - CheckDeclaringType(parentType.DeclaringType); - var attribute = parentType.CustomAttributes.FirstOrDefault(x => - x.AttributeType.FullName == "System.Runtime.CompilerServices.NullableContextAttribute" && - x.ConstructorArguments.Count == 1 && - x.ConstructorArguments[0].ArgumentType == typeof(byte)); - if (attribute != null) { - nullable = (Nullability)(byte)attribute.ConstructorArguments[0].Value; - } - } - } - /// /// Apply , , , - /// and over the supplied . + /// , and over + /// the supplied . /// Override this method to enforce specific graph types for specific CLR types, or to implement custom /// attributes to change graph type selection behavior. /// protected virtual TypeInformation ApplyAttributes(TypeInformation typeInformation) - { - var member = typeInformation.IsInputType ? (ICustomAttributeProvider)typeInformation.ParameterInfo : typeInformation.ParameterInfo.Member; - if (typeInformation.IsNullable) { - if (member.IsDefined(typeof(RequiredAttribute), false)) - typeInformation.IsNullable = false; - if (member.IsDefined(typeof(System.ComponentModel.DataAnnotations.RequiredAttribute), false)) - typeInformation.IsNullable = false; - } else { - if (member.IsDefined(typeof(OptionalAttribute), false)) - typeInformation.IsNullable = true; - } - if (typeInformation.IsList) { - if (typeInformation.ListIsNullable) { - if (member.IsDefined(typeof(RequiredListAttribute), false)) - typeInformation.ListIsNullable = false; - } else { - if (member.IsDefined(typeof(OptionalListAttribute), false)) - typeInformation.ListIsNullable = true; - } - } - if (member.IsDefined(typeof(IdAttribute), false)) - typeInformation.GraphType = typeof(IdGraphType); - return typeInformation; - } + => typeInformation.ApplyAttributes(typeInformation.IsInputType ? (ICustomAttributeProvider)typeInformation.ParameterInfo! : typeInformation.MemberInfo); + + /// + protected virtual IEnumerable<(Type Type, Nullability Nullable)> GetNullabilityInformation(ParameterInfo parameter) + => parameter.GetNullabilityInformation(); private static readonly Type[] _listTypes = new Type[] { typeof(IEnumerable<>), @@ -389,7 +306,7 @@ protected virtual TypeInformation GetTypeInformation(ParameterInfo parameterInfo var isOptionalParameter = parameterInfo.IsOptional; var isList = false; var isNullableList = false; - var typeTree = GetNullability(parameterInfo); + var typeTree = GetNullabilityInformation(parameterInfo); foreach (var type in typeTree) { if (type.Type == typeof(IDataLoaderResult)) //assume type is nullable object @@ -439,21 +356,7 @@ protected virtual TypeInformation GetTypeInformation(ParameterInfo parameterInfo /// Returns a GraphQL input type for a specified CLR type /// protected virtual Type InferGraphType(TypeInformation typeInformation) - { - var t = typeInformation.GraphType; - if (t != null) { - if (!typeInformation.IsNullable) - t = typeof(NonNullGraphType<>).MakeGenericType(t); - } else { - t = typeInformation.Type.GetGraphTypeFromType(typeInformation.IsNullable, typeInformation.IsInputType ? TypeMappingMode.InputType : TypeMappingMode.OutputType); - } - if (typeInformation.IsList) { - t = typeof(ListGraphType<>).MakeGenericType(t); - if (!typeInformation.ListIsNullable) - t = typeof(NonNullGraphType<>).MakeGenericType(t); - } - return t; - } + => typeInformation.InferGraphType(); /// /// Returns an expression that gets/creates an instance of from a . diff --git a/src/GraphQL.DI/DISchemaTypes.cs b/src/GraphQL.DI/DISchemaTypes.cs new file mode 100644 index 0000000..5775ff4 --- /dev/null +++ b/src/GraphQL.DI/DISchemaTypes.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using GraphQL.Types; + +namespace GraphQL.DI +{ + /// + public class DISchemaTypes : SchemaTypes + { + /// + public DISchemaTypes(ISchema schema, IServiceProvider serviceProvider) : base(schema, serviceProvider) + { + } + + /// + protected override Type? GetGraphTypeFromClrType(Type clrType, bool isInputType, List<(Type ClrType, Type GraphType)> typeMappings) + { + var type = base.GetGraphTypeFromClrType(clrType, isInputType, typeMappings); + if (type == null && isInputType && clrType.IsClass) { + return typeof(AutoInputObjectGraphType<>).MakeGenericType(clrType); + } + return type; + } + } +} diff --git a/src/GraphQL.DI/GraphQLBuilderExtensions.cs b/src/GraphQL.DI/GraphQLBuilderExtensions.cs new file mode 100644 index 0000000..17d9bae --- /dev/null +++ b/src/GraphQL.DI/GraphQLBuilderExtensions.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using GraphQL.Types; + +namespace GraphQL.DI +{ + /// + /// Provides extension methods to configure GraphQL.NET services within a dependency injection framework. + /// + public static class GraphQLBuilderExtensions + { + /// + /// Registers , and + /// as generic types. + /// + public static IGraphQLBuilder AddDIGraphTypes(this IGraphQLBuilder builder) + { + builder.TryRegister(typeof(AutoInputObjectGraphType<>), typeof(AutoInputObjectGraphType<>), ServiceLifetime.Transient); + builder.TryRegister(typeof(DIObjectGraphType<>), typeof(DIObjectGraphType<>), ServiceLifetime.Transient); + builder.TryRegister(typeof(DIObjectGraphType<,>), typeof(DIObjectGraphType<,>), ServiceLifetime.Transient); + return builder; + } + + /// + /// Scans the calling assembly for classes that implement and + /// registers clr type mappings on the schema between that + /// (constructed from that class and its source type), and the source type. + /// Skips classes where the source type is , or where the class is marked with + /// the , or where another graph type would be automatically mapped + /// to the specified type, or where a graph type has already been registered to the specified clr type. + /// + public static IGraphQLBuilder AddDIClrTypeMappings(this IGraphQLBuilder builder) + => AddDIClrTypeMappings(builder, Assembly.GetCallingAssembly()); + + /// + /// Scans the specified assembly for classes that implement and + /// registers clr type mappings on the schema between that + /// (constructed from that class and its source type), and the source type. + /// Skips classes where the source type is , or where the class is marked with + /// the , or where another graph type would be automatically mapped + /// to the specified type, or where a graph type has already been registered to the specified clr type. + /// + public static IGraphQLBuilder AddDIClrTypeMappings(this IGraphQLBuilder builder, Assembly assembly) + { + var typesAlreadyMapped = new HashSet( + assembly.GetDefaultClrTypeMappings() + .Where(x => x.GraphType.IsOutputType()) + .Select(x => x.ClrType)); + + var types = assembly.GetTypes() + .Where(x => x.IsClass && !x.IsAbstract && typeof(IDIObjectGraphBase).IsAssignableFrom(x)) + .Select(x => { + var iface = x.GetInterfaces().FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IDIObjectGraphBase<>)); + return (x, iface?.GetGenericArguments()[0]); + }) + .Where(x => x.SourceType != null && x.SourceType != typeof(object) && !x.DIGraphType.IsDefined(typeof(DoNotMapClrTypeAttribute)) && !typesAlreadyMapped.Contains(x.SourceType)) + .Select<(Type DIGraphType, Type? SourceType), (Type ClrType, Type GraphType)>(x => (x.SourceType!, typeof(DIObjectGraphType<,>).MakeGenericType(x.DIGraphType, x.SourceType!))) + .ToList(); + + if (types.Count == 0) + return builder; + + builder.ConfigureSchema(schema => { + var existingMappings = new HashSet(schema.TypeMappings.Where(x => x.graphType.IsOutputType()).Select(x => x.clrType)); + foreach (var type in types) { + if (!existingMappings.Contains(type.ClrType)) + schema.RegisterTypeMapping(type.ClrType, type.GraphType); + } + }); + + return builder; + } + + /// + /// Contains a list of types that are scanned for, from which a clr type mapping will be matched + /// + private static readonly Type[] _typesToRegister = new Type[] + { + typeof(ObjectGraphType<>), + typeof(InputObjectGraphType<>), + typeof(EnumerationGraphType<>), + }; + + /// + /// Scans the specified assembly for classes that inherit from , + /// , or , and + /// returns a list of mappings between matched classes and the source type or underlying enum type. + /// Skips classes where the source type is , or where the class is marked with + /// the . + /// + private static List<(Type ClrType, Type GraphType)> GetDefaultClrTypeMappings(this Assembly assembly) + { + //create a list of type mappings + var typeMappings = new List<(Type clrType, Type graphType)>(); + + //loop through each type in the specified assembly + foreach (var graphType in assembly.GetTypes()) { + //skip types that are not graph types + if (!typeof(IGraphType).IsAssignableFrom(graphType)) + continue; + + //skip abstract types and interfaces + if (graphType.IsAbstract || graphType.IsInterface) + continue; + + //skip types marked with the DoNotRegister attribute + if (graphType.GetCustomAttributes(false).Any(y => y.GetType() == typeof(DoNotMapClrTypeAttribute))) + continue; + + //start with the base type + var baseType = graphType.BaseType; + while (baseType != null) { + //skip types marked with the DoNotRegister attribute + if (baseType.GetCustomAttributes(false).Any(y => y.GetType() == typeof(DoNotMapClrTypeAttribute))) + break; + + //look for generic types that match our list above + if (baseType.IsConstructedGenericType && _typesToRegister.Contains(baseType.GetGenericTypeDefinition())) { + //get the base type + var clrType = baseType.GetGenericArguments()[0]; + + //as long as it's not of type 'object', register it + if (clrType != typeof(object)) + typeMappings.Add((clrType, graphType)); + + //skip to the next type + break; + } + + //look up the inheritance chain for a match + baseType = baseType.BaseType; + } + } + + //return the list of type mappings + return typeMappings; + } + } +} diff --git a/src/GraphQL.DI/GraphTypeAttribute.cs b/src/GraphQL.DI/GraphTypeAttribute.cs index 1da3f5c..43989b1 100644 --- a/src/GraphQL.DI/GraphTypeAttribute.cs +++ b/src/GraphQL.DI/GraphTypeAttribute.cs @@ -8,7 +8,7 @@ namespace GraphQL.DI /// marks a parameter's (query argument's) input value to be the specified GraphQL type. /// //perhaps this should apply to ReturnValue rather than Method - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] public class GraphTypeAttribute : Attribute { /// diff --git a/src/GraphQL.DI/IdAttribute.cs b/src/GraphQL.DI/IdAttribute.cs index 23a6c78..92a6586 100644 --- a/src/GraphQL.DI/IdAttribute.cs +++ b/src/GraphQL.DI/IdAttribute.cs @@ -9,7 +9,7 @@ namespace GraphQL.DI /// marks a parameter's (query argument's) input value to be an ID graph type. /// //perhaps this should apply to ReturnValue rather than Method - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] public class IdAttribute : Attribute { /// diff --git a/src/GraphQL.DI/IgnoreAttribute.cs b/src/GraphQL.DI/IgnoreAttribute.cs new file mode 100644 index 0000000..8969127 --- /dev/null +++ b/src/GraphQL.DI/IgnoreAttribute.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace GraphQL.DI +{ + /// + /// Marks a method (field) to be skipped when constructing the graph type. + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + public class IgnoreAttribute : NameAttribute + { + /// + public IgnoreAttribute() : base(null!) + { + } + } +} diff --git a/src/GraphQL.DI/MetadataAttribute.cs b/src/GraphQL.DI/MetadataAttribute.cs index e367a51..6ca2300 100644 --- a/src/GraphQL.DI/MetadataAttribute.cs +++ b/src/GraphQL.DI/MetadataAttribute.cs @@ -5,7 +5,7 @@ namespace GraphQL.DI /// /// Marks a class (graph type), method (field) or parameter (query argument) with additional metadata. /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Parameter, AllowMultiple = true, Inherited = false)] + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = true, Inherited = false)] public class MetadataAttribute : Attribute { /// diff --git a/src/GraphQL.DI/NameAttribute.cs b/src/GraphQL.DI/NameAttribute.cs index e4aefb2..559effe 100644 --- a/src/GraphQL.DI/NameAttribute.cs +++ b/src/GraphQL.DI/NameAttribute.cs @@ -7,7 +7,7 @@ namespace GraphQL.DI /// Marks a class (graph type), method (field) or parameter (query argument) with a specified GraphQL name. /// Note that the specified name will be translated by the schema's . /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] public class NameAttribute : Attribute { /// diff --git a/src/GraphQL.DI/Nullability.cs b/src/GraphQL.DI/Nullability.cs index 452a8d5..9b82dac 100644 --- a/src/GraphQL.DI/Nullability.cs +++ b/src/GraphQL.DI/Nullability.cs @@ -1,13 +1,21 @@ -using System; -using System.Collections.Generic; -using System.Text; - namespace GraphQL.DI { + /// + /// Nullable reference annotation for a member. + /// public enum Nullability : byte { + /// + /// The member is a reference type not marked with nullable reference annotations. + /// Unknown = 0, + /// + /// The member is a non-nullable value type or is a nullable refernce type marked as non-nullable. + /// NonNullable = 1, + /// + /// The member is a nullable value type or is a nullable reference type marked as nullable. + /// Nullable = 2, } } diff --git a/src/GraphQL.DI/NullabilityInformation.cs b/src/GraphQL.DI/NullabilityInformation.cs deleted file mode 100644 index a003593..0000000 --- a/src/GraphQL.DI/NullabilityInformation.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace GraphQL.DI -{ - internal struct NullabilityInformation - { - public Nullability DefaultNullability; - public Nullability[] Nullability; - } -} diff --git a/src/GraphQL.DI/NullabilityInterpretationException.cs b/src/GraphQL.DI/NullabilityInterpretationException.cs new file mode 100644 index 0000000..91b6275 --- /dev/null +++ b/src/GraphQL.DI/NullabilityInterpretationException.cs @@ -0,0 +1,25 @@ +using System; +using System.Reflection; + +namespace GraphQL.DI +{ + /// + /// Exception when nullable reference annotations cannot be read from a parameter. + /// + public class NullabilityInterpretationException : Exception + { + /// + /// Initializes a new instance for the specified . + /// + public NullabilityInterpretationException(ParameterInfo parameter) : base($"Unable to interpret nullability attributes for argument '{parameter.Name}' on method '{parameter.Member.DeclaringType.Name}.{parameter.Member.Name}'.") + { + } + + /// + /// Initializes a new instance for the specified . + /// + public NullabilityInterpretationException(MemberInfo member) : base($"Unable to interpret nullability attributes for '{member.DeclaringType.Name}.{member.Name}'.") + { + } + } +} diff --git a/src/GraphQL.DI/OptionalAttribute.cs b/src/GraphQL.DI/OptionalAttribute.cs index caff82b..e408431 100644 --- a/src/GraphQL.DI/OptionalAttribute.cs +++ b/src/GraphQL.DI/OptionalAttribute.cs @@ -7,7 +7,7 @@ namespace GraphQL.DI /// marks a parameter's (query argument's) input value to be optional. /// //perhaps this should apply to ReturnValue instead of Method - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] public class OptionalAttribute : Attribute { } diff --git a/src/GraphQL.DI/OptionalListAttribute.cs b/src/GraphQL.DI/OptionalListAttribute.cs index bfecf83..3acae28 100644 --- a/src/GraphQL.DI/OptionalListAttribute.cs +++ b/src/GraphQL.DI/OptionalListAttribute.cs @@ -7,7 +7,7 @@ namespace GraphQL.DI /// marks a parameter's (query argument's) input value to be optional. /// //perhaps this should apply to ReturnValue instead of Method - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] public class OptionalListAttribute : Attribute { } diff --git a/src/GraphQL.DI/ReflectionExtensions.cs b/src/GraphQL.DI/ReflectionExtensions.cs new file mode 100644 index 0000000..18705e5 --- /dev/null +++ b/src/GraphQL.DI/ReflectionExtensions.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace GraphQL.DI +{ + /// + /// Provides extension methods to and instances to read nullabile type reference annotations. + /// + public static class ReflectionExtensions + { + private static (Nullability, IList?) GetMethodParameterNullability(ParameterInfo parameter) + { + Nullability nullableContext = GetMethodDefaultNullability(parameter.Member); + + foreach (var attribute in parameter.CustomAttributes) { + if (attribute.AttributeType.FullName == "System.Runtime.CompilerServices.NullableAttribute" && attribute.ConstructorArguments.Count == 1) { + var argType = attribute.ConstructorArguments[0].ArgumentType; + if (argType == typeof(byte)) { + return ((Nullability)(byte)attribute.ConstructorArguments[0].Value, null); + } else if (argType == typeof(byte[])) { + return (nullableContext, (IList)attribute.ConstructorArguments[0].Value); + } else { + throw new NullabilityInterpretationException(parameter); + } + } + } + return (nullableContext, null); + } + + private static (Nullability, IList?) GetMemberNullability(MemberInfo member) + { + Nullability nullableContext = GetMethodDefaultNullability(member); + + foreach (var attribute in member.CustomAttributes) { + if (attribute.AttributeType.FullName == "System.Runtime.CompilerServices.NullableAttribute" && attribute.ConstructorArguments.Count == 1) { + var argType = attribute.ConstructorArguments[0].ArgumentType; + if (argType == typeof(byte)) { + return ((Nullability)(byte)attribute.ConstructorArguments[0].Value, null); + } else if (argType == typeof(byte[])) { + return (nullableContext, (IList)attribute.ConstructorArguments[0].Value); + } else { + throw new NullabilityInterpretationException(member); + } + } + } + return (nullableContext, null); + } + + /// + /// Examines the parameter type and walks the type definitions, for each returning the nullability information for the type. + /// + /// For instance, for Test<string, int>, first returns nullability for Test<string, int>, then string, then int. + /// + /// + /// See . + /// + /// + public static IEnumerable<(Type Type, Nullability Nullable)> GetNullabilityInformation(this ParameterInfo parameter) + { + if (parameter == null) + throw new ArgumentNullException(nameof(parameter)); + + var (nullableContext, nullabilityBytes) = GetMethodParameterNullability(parameter); + var list = new List<(Type, Nullability)>(); + var index = 0; + Consider(parameter.ParameterType, false, list, nullabilityBytes, nullableContext, ref index); + if (nullabilityBytes != null && nullabilityBytes.Count != index) + throw new NullabilityInterpretationException(parameter); + return list; + } + + /// + /// Examines the property type and walks the type definitions, for each returning the nullability information for the type. + /// + /// For instance, for Test<string, int>, first returns nullability for Test<string, int>, then string, then int. + /// + /// + /// See . + /// + /// + public static IEnumerable<(Type Type, Nullability Nullable)> GetNullabilityInformation(this PropertyInfo property) + { + if (property == null) + throw new ArgumentNullException(nameof(property)); + + var (nullableContext, nullabilityBytes) = GetMemberNullability(property); + var list = new List<(Type, Nullability)>(); + var index = 0; + Consider(property.PropertyType, false, list, nullabilityBytes, nullableContext, ref index); + if (nullabilityBytes != null && nullabilityBytes.Count != index) + throw new NullabilityInterpretationException(property); + return list; + } + + private static void Consider(Type t, bool nullableValueType, List<(Type, Nullability)> list, IList? nullabilityBytes, Nullability nullableContext, ref int index) + { + if (t.IsValueType) { + if (t.IsGenericType) { + var g = t.GetGenericTypeDefinition(); + if (g == typeof(Nullable<>)) { + //do not increment index for Nullable + //do not add Nullable to the list but rather just add underlying type + Consider(t.GenericTypeArguments[0], true, list, nullabilityBytes, nullableContext, ref index); + } else { + //generic structs that are not Nullable will contain a 0 in the array + index++; + list.Add((t, nullableValueType ? Nullability.Nullable : Nullability.NonNullable)); + for (int i = 0; i < t.GenericTypeArguments.Length; i++) { + Consider(t.GenericTypeArguments[i], false, list, nullabilityBytes, nullableContext, ref index); + } + } + } else { + //do not increment index for concrete value types + list.Add((t, nullableValueType ? Nullability.Nullable : Nullability.NonNullable)); + } + return; + } + //pull the nullability flag from the array and increment index + var nullable = nullabilityBytes != null ? (Nullability)(byte)nullabilityBytes[index++].Value : nullableContext; + list.Add((t, nullable)); + + if (t.IsArray) { + Consider(t.GetElementType(), false, list, nullabilityBytes, nullableContext, ref index); + } else if (t.IsGenericType) { + for (int i = 0; i < t.GenericTypeArguments.Length; i++) { + Consider(t.GenericTypeArguments[i], false, list, nullabilityBytes, nullableContext, ref index); + } + } + } + + private static Nullability GetMethodDefaultNullability(MemberInfo method) + { + var nullable = Nullability.Unknown; + + // check the parent type first to see if there's a nullable context attribute set for it + CheckDeclaringType(method.DeclaringType); + + // now check the method to see if there's a nullable context attribute set for it + var attribute = method.CustomAttributes.FirstOrDefault(x => + x.AttributeType.FullName == "System.Runtime.CompilerServices.NullableContextAttribute" && + x.ConstructorArguments.Count == 1 && + x.ConstructorArguments[0].ArgumentType == typeof(byte)); + if (attribute != null) { + nullable = (Nullability)(byte)attribute.ConstructorArguments[0].Value; + } + + return nullable; + + void CheckDeclaringType(Type parentType) + { + if (parentType.DeclaringType != null) + CheckDeclaringType(parentType.DeclaringType); + var attribute = parentType.CustomAttributes.FirstOrDefault(x => + x.AttributeType.FullName == "System.Runtime.CompilerServices.NullableContextAttribute" && + x.ConstructorArguments.Count == 1 && + x.ConstructorArguments[0].ArgumentType == typeof(byte)); + if (attribute != null) { + nullable = (Nullability)(byte)attribute.ConstructorArguments[0].Value; + } + } + } + } +} diff --git a/src/GraphQL.DI/RequiredAttribute.cs b/src/GraphQL.DI/RequiredAttribute.cs index 71b3931..0f3310b 100644 --- a/src/GraphQL.DI/RequiredAttribute.cs +++ b/src/GraphQL.DI/RequiredAttribute.cs @@ -7,7 +7,7 @@ namespace GraphQL.DI /// marks a parameter's (query argument's) input value to be required. /// //perhaps this should apply to ReturnValue instead of Method - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] public class RequiredAttribute : Attribute { } diff --git a/src/GraphQL.DI/RequiredListAttribute.cs b/src/GraphQL.DI/RequiredListAttribute.cs index 51068d1..e8b7398 100644 --- a/src/GraphQL.DI/RequiredListAttribute.cs +++ b/src/GraphQL.DI/RequiredListAttribute.cs @@ -7,7 +7,7 @@ namespace GraphQL.DI /// marks a parameter's (query argument's) input value to be required. /// //perhaps this should apply to ReturnValue instead of Method - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] public class RequiredListAttribute : Attribute { } diff --git a/src/GraphQL.DI/Shane32.GraphQL.DI.csproj b/src/GraphQL.DI/Shane32.GraphQL.DI.csproj index ccf9920..96e54ef 100644 --- a/src/GraphQL.DI/Shane32.GraphQL.DI.csproj +++ b/src/GraphQL.DI/Shane32.GraphQL.DI.csproj @@ -10,8 +10,8 @@ - - + + diff --git a/src/GraphQL.DI/TypeInformation.cs b/src/GraphQL.DI/TypeInformation.cs index a14bc5a..3ec7b92 100644 --- a/src/GraphQL.DI/TypeInformation.cs +++ b/src/GraphQL.DI/TypeInformation.cs @@ -1,7 +1,8 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; -using System.Text; +using GraphQL.Types; namespace GraphQL.DI { @@ -13,7 +14,12 @@ public struct TypeInformation /// /// The argument or return parameter of the method being inspected. /// - public ParameterInfo ParameterInfo; + public ParameterInfo? ParameterInfo; + + /// + /// The member being inspected. + /// + public MemberInfo MemberInfo; /// /// Indicates that this is an input type (an argument); false for output types. @@ -59,6 +65,29 @@ public struct TypeInformation public TypeInformation(ParameterInfo parameterInfo, bool isInputType, Type type, bool isNullable, bool isList, bool listIsNullable, Type? graphType) { ParameterInfo = parameterInfo; + MemberInfo = parameterInfo.Member; + IsInputType = isInputType; + Type = type; + IsNullable = isNullable; + IsList = isList; + ListIsNullable = listIsNullable; + GraphType = graphType; + } + + /// + /// Initializes an instance with the specified properties. + /// + /// The member being inspected. + /// Indicates that this is an input type (an argument); false for output types. + /// The underlying type. + /// Indicates that the underlying type is nullable. + /// Indicates that this represents a list of elements. + /// Indicates that the list is nullable. + /// The graph type of the underlying CLR type; null if not specified. + public TypeInformation(MemberInfo memberInfo, bool isInputType, Type type, bool isNullable, bool isList, bool listIsNullable, Type? graphType) + { + ParameterInfo = null; + MemberInfo = memberInfo; IsInputType = isInputType; Type = type; IsNullable = isNullable; @@ -66,5 +95,62 @@ public TypeInformation(ParameterInfo parameterInfo, bool isInputType, Type type, ListIsNullable = listIsNullable; GraphType = graphType; } + + internal Type InferGraphType() + { + var t = GraphType; + if (t != null) { + if (!IsNullable) + t = typeof(NonNullGraphType<>).MakeGenericType(t); + } else { + t = Type.GetGraphTypeFromType(IsNullable, IsInputType ? TypeMappingMode.InputType : TypeMappingMode.OutputType); + } + if (IsList) { + t = typeof(ListGraphType<>).MakeGenericType(t); + if (!ListIsNullable) + t = typeof(NonNullGraphType<>).MakeGenericType(t); + } + return t; + } + + /// + /// Returns a new instance with , , , + /// , and + /// applied as necessary. + /// + internal TypeInformation ApplyAttributes(ICustomAttributeProvider member) + { + var typeInformation = this; //copy struct + //var member = examineParent ? (ICustomAttributeProvider)typeInformation.ParameterInfo.Member : typeInformation.ParameterInfo; + if (typeInformation.IsNullable) { + if (member.IsDefined(typeof(RequiredAttribute), false)) + typeInformation.IsNullable = false; + if (member.IsDefined(typeof(System.ComponentModel.DataAnnotations.RequiredAttribute), false)) + typeInformation.IsNullable = false; + } else { + if (member.IsDefined(typeof(OptionalAttribute), false)) + typeInformation.IsNullable = true; + } + if (typeInformation.IsList) { + if (typeInformation.ListIsNullable) { + if (member.IsDefined(typeof(RequiredListAttribute), false)) + typeInformation.ListIsNullable = false; + } else { + if (member.IsDefined(typeof(OptionalListAttribute), false)) + typeInformation.ListIsNullable = true; + } + } + if (member.IsDefined(typeof(IdAttribute), false)) + typeInformation.GraphType = typeof(IdGraphType); + else if (member.GetCustomAttributes(typeof(DIGraphAttribute), false).SingleOrDefault() is DIGraphAttribute diGraphAttribute) { + var iface = diGraphAttribute.GraphBaseType.GetInterfaces().FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IDIObjectGraphBase<>)); + if (iface == null) { + throw new InvalidOperationException($"Member '{typeInformation.MemberInfo.DeclaringType.Name}.{typeInformation.MemberInfo.Name}' is marked with [DIGraph] specifying type '{diGraphAttribute.GraphBaseType.Name}' which does not inherit {nameof(IDIObjectGraphBase)}."); + } + typeInformation.GraphType = typeof(DIObjectGraphType<,>).MakeGenericType(diGraphAttribute.GraphBaseType, iface.GetGenericArguments()[0]); + } + return typeInformation; + } + } } diff --git a/src/Tests/AutoInputObjectGraphTypeTests/Graph.cs b/src/Tests/AutoInputObjectGraphTypeTests/Graph.cs new file mode 100644 index 0000000..e729252 --- /dev/null +++ b/src/Tests/AutoInputObjectGraphTypeTests/Graph.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Reflection; +using GraphQL.DI; +using GraphQL.Types; +using Shouldly; +using Xunit; + +namespace AutoInputObjectGraphTypeTests +{ + public class Graph + { + private IInputObjectGraphType _graphType; + + private IInputObjectGraphType Configure() + { + _graphType = new AutoInputObjectGraphType(); + return _graphType; + } + + [Fact] + public void GraphName() + { + Configure().Name.ShouldBe("TestGraphName"); + } + + [Name("TestGraphName")] + public class CGraphName { } + + [Fact] + public void DefaultGraphName() + { + Configure().Name.ShouldBe("CDefault"); + } + + public class CDefault { } + + [Fact] + public void DefaultGraphNameInputModel() + { + Configure().Name.ShouldBe("CDefaultInput"); + } + + public class CDefaultInputModel { } + + [Fact] + public void GraphDescription() + { + Configure().Description.ShouldBe("TestGraphDescription"); + } + + [Description("TestGraphDescription")] + public class CGraphDescription { } + + [Fact] + public void GraphObsolete() + { +#pragma warning disable CS0618 // Member is obsolete + Configure().DeprecationReason.ShouldBe("TestDeprecationReason"); +#pragma warning restore CS0618 // Member is obsolete + } + + [Obsolete("TestDeprecationReason")] + public class CGraphObsolete { } + + [Fact] + public void GraphMetadata() + { + Configure().GetMetadata("test").ShouldBe("value"); + } + + [Metadata("test", "value")] + public class CGraphMetadata { } + + [Fact] + public void CanOverrideMembers() + { + var test = new CCanOverrideMembersGraphType(); + test.Fields.Count.ShouldBe(0); + } + + public class CCanOverrideMembersGraphType : AutoInputObjectGraphType + { + protected override IEnumerable GetRegisteredProperties() => Array.Empty(); + } + + public class CCanOverrideMembers + { + public static string Field1() => "hello"; + } + } +} diff --git a/src/Tests/AutoInputObjectGraphTypeTests/Property.cs b/src/Tests/AutoInputObjectGraphTypeTests/Property.cs new file mode 100644 index 0000000..9e465b4 --- /dev/null +++ b/src/Tests/AutoInputObjectGraphTypeTests/Property.cs @@ -0,0 +1,180 @@ +#nullable enable + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using GraphQL; +using GraphQL.DI; +using GraphQL.Types; +using Moq; +using Shouldly; +using Xunit; + +namespace AutoInputObjectGraphTypeTests +{ + public class Property + { + private readonly IInputObjectGraphType _graphType = new AutoInputObjectGraphType(); + + public class CDefault + { + public string? Field1 { get; set; } + public string Field2 { get; set; } = null!; + public int? Field3 { get; set; } + public int Field4 { get; set; } + public List Field5 { get; set; } = null!; + public List? Field6 { get; set; } + public string[] Field7 { get; set; } = null!; + [Required] + public string? Field8 { get; set; } + [Optional] + public string Field9 { get; set; } = null!; + [OptionalList] + public List Field10 { get; set; } = null!; + [RequiredList] + public List? Field11 { get; set; } + [GraphType(typeof(GuidGraphType))] + public object? Field12 { get; set; } + [Name("Field14")] + public string? Field13 { get; set; } + [Ignore] + public string? Field15 { get; set; } + [DefaultValue(5)] + public int Field16 { get; set; } + [Description("Example")] + public int Field17 { get; set; } + [Obsolete("Example")] + public int Field18 { get; set; } + [Id] + public int Field19 { get; set; } + public int Field20 => 3; + [Name(null!)] + public int Field21 { get; set; } + public int Field22() => 3; + [Metadata("test", 3)] + [Metadata("test2", 5)] + public int Field23 { get; set; } + public IEnumerable? Field24 { get; set; } + public ICollection Field25 { get; set; } = null!; + public Class1 Field26 { get; set; } = null!; + public int?[]? Field27 { get; set; } + } + + public class Class1 { } + + [Theory] + [InlineData("Field1", typeof(string), true)] + [InlineData("Field2", typeof(string), false)] + [InlineData("Field3", typeof(int), true)] + [InlineData("Field4", typeof(int), false)] + [InlineData("Field5", typeof(string), true, true, false)] + [InlineData("Field6", typeof(string), false, true, true)] + [InlineData("Field7", typeof(string), false, true, false)] + [InlineData("Field8", typeof(string), false)] + [InlineData("Field9", typeof(string), true)] + [InlineData("Field10", typeof(string), false, true, true)] + [InlineData("Field11", typeof(string), true, true, false)] + [InlineData("Field12", null, true, false, false, typeof(GuidGraphType))] + [InlineData("Field13", null, true)] + [InlineData("Field14", typeof(string), true)] + [InlineData("Field15", null, true)] + [InlineData("Field16", typeof(int), false)] + [InlineData("Field17", typeof(int), false)] + [InlineData("Field18", typeof(int), false)] + [InlineData("Field19", null, true, false, false, typeof(NonNullGraphType))] + [InlineData("Field20", null, true)] + [InlineData("Field21", null, true)] + [InlineData("Field22", null, true)] + [InlineData("Field23", typeof(int), false)] + [InlineData("Field24", typeof(object), true, true, true)] + [InlineData("Field25", typeof(object), true, true, false)] + [InlineData("Field26", typeof(Class1), false)] + [InlineData("Field27", typeof(int), true, true, true)] + public void Types(string name, Type? fieldType, bool isNullable, bool isList = false, bool listIsNullable = false, Type? graphType = null) + { + if (graphType == null && fieldType != null) { + graphType = fieldType.GetGraphTypeFromType(isNullable, TypeMappingMode.InputType); + if (isList) { + graphType = typeof(ListGraphType<>).MakeGenericType(graphType); + if (!listIsNullable) { + graphType = typeof(NonNullGraphType<>).MakeGenericType(graphType); + } + } + } + var field = _graphType.Fields.Find(name); + if (graphType == null) { + field.ShouldBeNull(); + return; + } + field.ShouldNotBeNull(); + field.Type.ShouldBe(graphType); + field.Resolver.ShouldBe(null); + } + + [Fact] + public void DefaultValue() + { + var field = _graphType.Fields.Find("Field16"); + field.ShouldNotBeNull(); + field.DefaultValue.ShouldBeOfType().ShouldBe(5); + } + + [Fact] + public void Description() + { + var field = _graphType.Fields.Find("Field17"); + field.ShouldNotBeNull(); + field.Description.ShouldBe("Example"); + } + + [Fact] + public void Obsolete() + { + var field = _graphType.Fields.Find("Field18"); + field.ShouldNotBeNull(); + field.DeprecationReason.ShouldBe("Example"); + } + + [Fact] + public void Metadata() + { + var field = _graphType.Fields.Find("Field23"); + field.ShouldNotBeNull(); + field.Metadata.ShouldNotBeNull(); + field.Metadata.ShouldContainKeyAndValue("test", 3); + field.Metadata.ShouldContainKeyAndValue("test2", 5); + } + + [Theory] + [InlineData("Field1", "Field1")] + [InlineData("Field14", "Field13")] + public void Expression(string graphFieldName, string classPropertyName) + { + var field = _graphType.Fields.Find(graphFieldName); + field.ShouldNotBeNull(); + field.Metadata.ShouldNotBeNull(); + field.Metadata.ShouldContainKey("ORIGINAL_EXPRESSION_PROPERTY_NAME"); + var actual = field.Metadata["ORIGINAL_EXPRESSION_PROPERTY_NAME"] + .ShouldBeOfType(); + actual.ShouldBe(classPropertyName); + } + + [Fact] + public void ExpressionCorrectlyResolves() + { + var graph = new AutoInputObjectGraphType(); + var dic = new Dictionary { + { "Field2", "value" } + }; + var actual = (Class2)graph.ParseDictionary(dic); + actual.Field1.ShouldBe("value"); + } + + public class Class2 + { + [Name("Field2")] + public string? Field1 { get; set; } + } + } +} diff --git a/src/Tests/DIObjectGraphTypeTests/Field.cs b/src/Tests/DIObjectGraphTypeTests/Field.cs index f83eb19..5437e42 100644 --- a/src/Tests/DIObjectGraphTypeTests/Field.cs +++ b/src/Tests/DIObjectGraphTypeTests/Field.cs @@ -274,6 +274,22 @@ public class CRequired : DIObjectGraphBase public static string Field1() => "hello"; } + [Fact] + public void InheritedRequired() + { + Configure(); + VerifyField("Field1", false, false, "hello"); + Verify(false); + } + + public class CInheritedRequired : DIObjectGraphBase + { + [MyRequired] + public static string Field1() => "hello"; + } + + public class MyRequiredAttribute : RequiredAttribute { } + [Fact] public async Task RequiredTask() { @@ -349,6 +365,79 @@ public class CIdTypeNonNull : DIObjectGraphBase public static int Field1() => 2; } + [Fact] + public void IdListType() + { + Configure(); + VerifyField("Field1", typeof(ListGraphType), false, new[] { "hello" }); + Verify(false); + } + + public class CIdListType : DIObjectGraphBase + { + [Id] + public static string[] Field1() => new[] { "hello" }; + } + + [Fact] + public void DIGraphType() + { + Configure(); + VerifyField("Field1", typeof(DIObjectGraphType), false, "hello"); + Verify(false); + } + + public class CDIGraphType2 : DIObjectGraphBase + { + public static string Field1() => "hello"; + } + + public class CDIGraphType : DIObjectGraphBase + { + [DIGraph(typeof(CDIGraphType2))] + public static string Field1() => "hello"; + } + + [Fact] + public void DIGraphTypeNonNull() + { + Configure(); + VerifyField("Field1", typeof(NonNullGraphType>), false, 2); + Verify(false); + } + + public class CDIGraphTypeNonNull : DIObjectGraphBase + { + [DIGraph(typeof(CDIGraphType2))] + public static int Field1() => 2; + } + + [Fact] + public void DIGraphListType() + { + Configure(); + VerifyField("Field1", typeof(ListGraphType>), false, new[] { "hello" }); + Verify(false); + } + + public class CDIGraphListType : DIObjectGraphBase + { + [DIGraph(typeof(CDIGraphType2))] + public static string[] Field1() => new[] { "hello" }; + } + + [Fact] + public void DIGraphTypeInvalid() + { + Should.Throw(() => Configure()); + } + + public class CDIGraphTypeInvalid : DIObjectGraphBase + { + [DIGraph(typeof(string))] + public static string Field1() => "hello"; + } + [Fact] public async Task Concurrent() { @@ -480,12 +569,14 @@ public void SkipVoidMembers() Configure(); _graphType.Fields.Find("Field1").ShouldBeNull(); _graphType.Fields.Find("Field2").ShouldBeNull(); + _graphType.Fields.Find("Field3").ShouldNotBeNull(); } public class CSkipVoidMembers : DIObjectGraphBase { public static Task Field1() => Task.CompletedTask; public static void Field2() { } + public static string Field3() => null!; } [Fact] @@ -493,12 +584,29 @@ public void SkipNullName() { Configure(); _graphType.Fields.Find("Field1").ShouldBeNull(); + _graphType.Fields.Find("Field2").ShouldNotBeNull(); } public class CSkipNullName : DIObjectGraphBase { [Name(null)] public static string Field1() => "hello"; + public static string Field2() => "hello"; + } + + [Fact] + public void Ignore() + { + Configure(); + _graphType.Fields.Find("Field1").ShouldBeNull(); + _graphType.Fields.Find("Field2").ShouldNotBeNull(); + } + + public class CIgnore : DIObjectGraphBase + { + [Ignore] + public static string Field1() => "hello"; + public static string Field2() => "hello"; } [Fact] diff --git a/src/Tests/DIObjectGraphTypeTests/Graph.cs b/src/Tests/DIObjectGraphTypeTests/Graph.cs index 0a18e04..9268798 100644 --- a/src/Tests/DIObjectGraphTypeTests/Graph.cs +++ b/src/Tests/DIObjectGraphTypeTests/Graph.cs @@ -19,6 +19,33 @@ public void GraphName() [Name("TestGraphName")] public class CGraphName : DIObjectGraphBase { } + [Fact] + public void GraphDefaultName() + { + Configure().Name.ShouldBe("CGraphDefaultName"); + new DIObjectGraphType().Name.ShouldBe("CGraphDefaultName"); + } + + public class CGraphDefaultName : DIObjectGraphBase { } + + [Fact] + public void GraphDefaultName2() + { + Configure().Name.ShouldBe("CGraphDefaultName2"); + } + + [Fact] + public void GraphDefaultName3() + { + Configure().Name.ShouldBe("CGraphDefaultName"); + new DIObjectGraphType().Name.ShouldBe("CGraphDefaultName"); + } + + public class CGraphDefaultNameGraph : DIObjectGraphBase { } + + public class CGraphDefaultName2 : DIObjectGraphBase { } + public class Class1 { } + [Fact] public void GraphDescription() { diff --git a/src/Tests/DIObjectGraphTypeTests/Nullable.cs b/src/Tests/DIObjectGraphTypeTests/Nullable.cs deleted file mode 100644 index c5e1961..0000000 --- a/src/Tests/DIObjectGraphTypeTests/Nullable.cs +++ /dev/null @@ -1,539 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using GraphQL.DataLoader; -using GraphQL.DI; -using GraphQL.Types; -using Shouldly; -using Tests.NullabilityTestClasses; -using Xunit; - -namespace DIObjectGraphTypeTests -{ - public class Nullable - { - //Verify... methods verify that .NET is building the classes - // with the expected attributes on them. Failure is not an - // error, but simply indicates that the Method and Argument - // tests may not be testing the anticipated scenarios - //Method and Argument should always pass - [Theory] - [InlineData(typeof(NullableClass1), 1, 0)] //default not nullable - [InlineData(typeof(NullableClass2), 2, 0)] //default nullable - [InlineData(typeof(NullableClass5), null, null)] - [InlineData(typeof(NullableClass6), null, null)] - [InlineData(typeof(NullableClass7), 1, 0)] //default not nullable - [InlineData(typeof(NullableClass8), 2, 0)] //default nullable - [InlineData(typeof(NullableClass9), null, null)] - [InlineData(typeof(NullableClass10), null, null)] - [InlineData(typeof(NullableClass11), 1, 0)] //default not nullable - [InlineData(typeof(NullableClass12), null, null)] - [InlineData(typeof(NullableClass13), 1, 0)] //default not nullable - [InlineData(typeof(NullableClass14), 2, 0)] //default nullable - [InlineData(typeof(NullableClass15), null, null)] - [InlineData(typeof(NullableClass16), 1, 0)] - [InlineData(typeof(NullableClass16.NestedClass1), null, 0)] - [InlineData(typeof(NullableClass17), 1, 0)] - [InlineData(typeof(NullableClass18<>), null, null)] - public void VerifyTestClass(Type type, int? nullableContext, int? nullable) - { - var actualHasNullableContext = type.CustomAttributes.FirstOrDefault( - x => x.AttributeType.Name == "NullableContextAttribute"); - if (nullableContext == null) { - actualHasNullableContext.ShouldBeNull(); - } else { - actualHasNullableContext.ShouldNotBeNull(); - actualHasNullableContext.ConstructorArguments[0].Value.ShouldBe(nullableContext); - } - - var actualHasNullable = type.CustomAttributes.FirstOrDefault( - x => x.AttributeType.Name == "NullableAttribute"); - if (nullable == null) { - actualHasNullable.ShouldBeNull(); - } else { - actualHasNullable.ShouldNotBeNull(); - actualHasNullable.ConstructorArguments[0].ArgumentType.ShouldBe(typeof(byte)); - actualHasNullable.ConstructorArguments[0].Value.ShouldBe(nullable); - } - } - - [Theory] - [InlineData(typeof(NullableClass1), "Field1", 2, null)] - [InlineData(typeof(NullableClass1), "Field2", 2, null)] - [InlineData(typeof(NullableClass1), "Field3", null, null)] - [InlineData(typeof(NullableClass1), "Field4", null, null)] - [InlineData(typeof(NullableClass1), "Field5", null, null)] - [InlineData(typeof(NullableClass1), "Field6", null, null)] - [InlineData(typeof(NullableClass1), "Field7", null, null)] - [InlineData(typeof(NullableClass1), "Field8", null, null)] - [InlineData(typeof(NullableClass1), "Field9", null, null)] - [InlineData(typeof(NullableClass1), "Field10", null, null)] - [InlineData(typeof(NullableClass2), "Field1", null, null)] - [InlineData(typeof(NullableClass2), "Field2", null, null)] - [InlineData(typeof(NullableClass2), "Field3", null, null)] - [InlineData(typeof(NullableClass2), "Field4", null, null)] - [InlineData(typeof(NullableClass2), "Field5", null, null)] - [InlineData(typeof(NullableClass2), "Field6", null, null)] - [InlineData(typeof(NullableClass2), "Field7", null, null)] - [InlineData(typeof(NullableClass2), "Field8", null, null)] - [InlineData(typeof(NullableClass2), "Field9", 1, null)] - [InlineData(typeof(NullableClass2), "Field10", 1, null)] - [InlineData(typeof(NullableClass2), "Field11", null, null)] - [InlineData(typeof(NullableClass5), "Test", 1, null)] - [InlineData(typeof(NullableClass6), "Field1", 1, null)] - [InlineData(typeof(NullableClass6), "Field2", 2, null)] - [InlineData(typeof(NullableClass7), "Field1", null, null)] - [InlineData(typeof(NullableClass7), "Field2", null, null)] - [InlineData(typeof(NullableClass7), "Field3", 2, null)] - [InlineData(typeof(NullableClass8), "Field1", null, null)] - [InlineData(typeof(NullableClass8), "Field2", null, null)] - [InlineData(typeof(NullableClass8), "Field3", 1, null)] - [InlineData(typeof(NullableClass8), "Field4", null, null)] - [InlineData(typeof(NullableClass9), "Field1", 2, "1")] - [InlineData(typeof(NullableClass10), "Field1", 1, "2")] - [InlineData(typeof(NullableClass11), "Field1", null, null)] - [InlineData(typeof(NullableClass11), "Field2", null, "2")] - [InlineData(typeof(NullableClass12), "Field1", 1, null)] - [InlineData(typeof(NullableClass12), "Field2", null, "12")] - [InlineData(typeof(NullableClass12), "Field3", null, "21")] - [InlineData(typeof(NullableClass12), "Field4", 2, null)] - [InlineData(typeof(NullableClass13), "Field1", null, null)] - [InlineData(typeof(NullableClass13), "Field2", null, null)] - [InlineData(typeof(NullableClass14), "Field1", null, null)] - [InlineData(typeof(NullableClass14), "Field2", null, null)] - [InlineData(typeof(NullableClass15), "Field1", 1, null)] - [InlineData(typeof(NullableClass15), "Field2", null, "12")] - [InlineData(typeof(NullableClass15), "Field3", null, "21")] - [InlineData(typeof(NullableClass15), "Field4", 2, null)] - [InlineData(typeof(NullableClass16), "Field1", null, null)] - [InlineData(typeof(NullableClass16), "Field2", null, null)] - [InlineData(typeof(NullableClass16), "Field3", 2, null)] - [InlineData(typeof(NullableClass16.NestedClass1), "Field1", null, null)] - [InlineData(typeof(NullableClass16.NestedClass1), "Field2", null, null)] - [InlineData(typeof(NullableClass16.NestedClass1), "Field3", 2, null)] - [InlineData(typeof(NullableClass17), "Field1", null, null)] - [InlineData(typeof(NullableClass17), "Field2", null, null)] - [InlineData(typeof(NullableClass17), "Field3", null, "12")] - [InlineData(typeof(NullableClass18<>), "Field1", null, "112")] - [InlineData(typeof(NullableClass18<>), "Field2", null, "11221")] - [InlineData(typeof(NullableClass18<>), "Field3", null, "12")] - [InlineData(typeof(NullableClass18<>), "Field4", null, "12")] - [InlineData(typeof(NullableClass18<>), "Field5", null, "1112")] - [InlineData(typeof(NullableClass18<>), "Field6", null, "112")] - [InlineData(typeof(NullableClass18<>), "Field7", null, "1012")] - [InlineData(typeof(NullableClass18<>), "Field8", null, "1012")] - [InlineData(typeof(NullableClass18<>), "Field9", null, "102")] - [InlineData(typeof(NullableClass18<>), "Field10", null, "112")] - public void VerifyTestMethod(Type type, string methodName, int? nullableContext, string nullableValues) - { - var method = type.GetMethod(methodName); - var methodNullableAttribute = method.CustomAttributes.FirstOrDefault(x => x.AttributeType.Name == "NullableAttribute"); - methodNullableAttribute.ShouldBeNull(); //should not be possible to apply the attribute here - - var methodNullableContextAttribute = method.CustomAttributes.FirstOrDefault(x => x.AttributeType.Name == "NullableContextAttribute"); - if (nullableContext.HasValue) { - methodNullableContextAttribute.ShouldNotBeNull(); - methodNullableContextAttribute.ConstructorArguments[0].ArgumentType.ShouldBe(typeof(byte)); - methodNullableContextAttribute.ConstructorArguments[0].Value.ShouldBeOfType().ShouldBe((byte)nullableContext.Value); - } else { - methodNullableContextAttribute.ShouldBeNull(); - } - - var parameterNullableAttribute = method.ReturnParameter.CustomAttributes.FirstOrDefault(x => x.AttributeType.Name == "NullableAttribute"); - if (nullableValues != null) { - parameterNullableAttribute.ShouldNotBeNull(); - var expectedValues = nullableValues.Select(x => (byte)int.Parse(x.ToString())).ToArray(); - if (expectedValues.Length == 1) { - parameterNullableAttribute.ConstructorArguments[0].ArgumentType.ShouldBe(typeof(byte)); - var actualValue = parameterNullableAttribute.ConstructorArguments[0].Value.ShouldBeOfType().ToString(); - actualValue.ShouldBe(nullableValues); - } else { - parameterNullableAttribute.ConstructorArguments[0].ArgumentType.ShouldBe(typeof(byte[])); - var actualValues = string.Join("", parameterNullableAttribute.ConstructorArguments[0].Value.ShouldBeOfType>().Select(x => x.Value.ToString())); - actualValues.ShouldBe(nullableValues); - } - } else { - parameterNullableAttribute.ShouldBeNull(); - } - } - - [Theory] - [InlineData(typeof(NullableClass9), "Field1", "arg1", null)] - [InlineData(typeof(NullableClass9), "Field1", "arg2", null)] - [InlineData(typeof(NullableClass10), "Field1", "arg1", null)] - [InlineData(typeof(NullableClass10), "Field1", "arg2", null)] - [InlineData(typeof(NullableClass11), "Field2", "arg1", null)] - [InlineData(typeof(NullableClass11), "Field2", "arg2", null)] - [InlineData(typeof(NullableClass13), "Field2", "arg1", null)] - [InlineData(typeof(NullableClass13), "Field2", "arg2", "2")] - [InlineData(typeof(NullableClass13), "Field2", "arg3", null)] - [InlineData(typeof(NullableClass13), "Field2", "arg4", null)] - [InlineData(typeof(NullableClass13), "Field2", "arg5", null)] - [InlineData(typeof(NullableClass13), "Field2", "arg6", null)] - [InlineData(typeof(NullableClass13), "Field2", "arg7", "12")] - [InlineData(typeof(NullableClass13), "Field2", "arg8", "21")] - [InlineData(typeof(NullableClass13), "Field2", "arg9", "2")] - [InlineData(typeof(NullableClass14), "Field2", "arg1", null)] - [InlineData(typeof(NullableClass14), "Field2", "arg2", "1")] - [InlineData(typeof(NullableClass14), "Field2", "arg3", null)] - [InlineData(typeof(NullableClass14), "Field2", "arg4", null)] - [InlineData(typeof(NullableClass14), "Field2", "arg5", null)] - [InlineData(typeof(NullableClass14), "Field2", "arg6", "1")] - [InlineData(typeof(NullableClass14), "Field2", "arg7", "12")] - [InlineData(typeof(NullableClass14), "Field2", "arg8", "21")] - [InlineData(typeof(NullableClass14), "Field2", "arg9", null)] - public void VerifyTestArgument(Type type, string methodName, string argumentName, string nullableValues) - { - var method = type.GetMethod(methodName); - var argument = method.GetParameters().Single(x => x.Name == argumentName); - var parameterNullableAttribute = argument.CustomAttributes.FirstOrDefault(x => x.AttributeType.Name == "NullableAttribute"); - if (nullableValues != null) { - parameterNullableAttribute.ShouldNotBeNull(); - var expectedValues = nullableValues.Select(x => (byte)int.Parse(x.ToString())).ToArray(); - if (expectedValues.Length == 1) { - parameterNullableAttribute.ConstructorArguments[0].ArgumentType.ShouldBe(typeof(byte)); - var actualValue = parameterNullableAttribute.ConstructorArguments[0].Value.ShouldBeOfType().ToString(); - actualValue.ShouldBe(nullableValues); - } else { - parameterNullableAttribute.ConstructorArguments[0].ArgumentType.ShouldBe(typeof(byte[])); - var actualValues = string.Join("", parameterNullableAttribute.ConstructorArguments[0].Value.ShouldBeOfType>().Select(x => x.Value.ToString())); - actualValues.ShouldBe(nullableValues); - } - } else { - parameterNullableAttribute.ShouldBeNull(); - } - } - - [Theory] - [InlineData(typeof(NullableClass1), "Field1", typeof(string), Nullability.Nullable)] - [InlineData(typeof(NullableClass1), "Field2", typeof(string), Nullability.Nullable)] - [InlineData(typeof(NullableClass1), "Field3", typeof(int), Nullability.NonNullable)] - [InlineData(typeof(NullableClass1), "Field4", typeof(int), Nullability.NonNullable)] - [InlineData(typeof(NullableClass1), "Field5", typeof(int), Nullability.Nullable)] - [InlineData(typeof(NullableClass1), "Field6", typeof(int), Nullability.Nullable)] - [InlineData(typeof(NullableClass1), "Field7", typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass1), "Field8", typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass1), "Field9", typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass1), "Field10", typeof(List), Nullability.NonNullable, typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass2), "Field1", typeof(string), Nullability.Nullable)] - [InlineData(typeof(NullableClass2), "Field2", typeof(string), Nullability.Nullable)] - [InlineData(typeof(NullableClass2), "Field3", typeof(int), Nullability.NonNullable)] - [InlineData(typeof(NullableClass2), "Field4", typeof(int), Nullability.NonNullable)] - [InlineData(typeof(NullableClass2), "Field5", typeof(int), Nullability.Nullable)] - [InlineData(typeof(NullableClass2), "Field6", typeof(int), Nullability.Nullable)] - [InlineData(typeof(NullableClass2), "Field7", typeof(string), Nullability.Nullable)] - [InlineData(typeof(NullableClass2), "Field8", typeof(string), Nullability.Nullable)] - [InlineData(typeof(NullableClass2), "Field9", typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass2), "Field10", typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass2), "Field11", typeof(List), Nullability.Nullable, typeof(string), Nullability.Nullable)] - [InlineData(typeof(NullableClass5), "Test", typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass6), "Field1", typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass6), "Field2", typeof(string), Nullability.Nullable)] - [InlineData(typeof(NullableClass7), "Field1", typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass7), "Field2", typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass7), "Field3", typeof(string), Nullability.Nullable)] - [InlineData(typeof(NullableClass8), "Field1", typeof(string), Nullability.Nullable)] - [InlineData(typeof(NullableClass8), "Field2", typeof(string), Nullability.Nullable)] - [InlineData(typeof(NullableClass8), "Field3", typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass8), "Field4", typeof(int), Nullability.NonNullable)] - [InlineData(typeof(NullableClass9), "Field1", typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass10), "Field1", typeof(string), Nullability.Nullable)] - [InlineData(typeof(NullableClass11), "Field1", typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass11), "Field2", typeof(string), Nullability.Nullable)] - [InlineData(typeof(NullableClass12), "Field1", typeof(IDataLoaderResult), Nullability.NonNullable, typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass12), "Field2", typeof(IDataLoaderResult), Nullability.NonNullable, typeof(string), Nullability.Nullable)] - [InlineData(typeof(NullableClass12), "Field3", typeof(IDataLoaderResult), Nullability.Nullable, typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass12), "Field4", typeof(IDataLoaderResult), Nullability.Nullable, typeof(string), Nullability.Nullable)] - [InlineData(typeof(NullableClass13), "Field1", typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass13), "Field2", typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass14), "Field1", typeof(string), Nullability.Nullable)] - [InlineData(typeof(NullableClass14), "Field2", typeof(string), Nullability.Nullable)] - [InlineData(typeof(NullableClass15), "Field1", typeof(Task), Nullability.NonNullable, typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass15), "Field2", typeof(Task), Nullability.NonNullable, typeof(string), Nullability.Nullable)] - [InlineData(typeof(NullableClass15), "Field3", typeof(Task), Nullability.Nullable, typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass15), "Field4", typeof(Task), Nullability.Nullable, typeof(string), Nullability.Nullable)] - [InlineData(typeof(NullableClass16), "Field1", typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass16), "Field2", typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass16), "Field3", typeof(string), Nullability.Nullable)] - [InlineData(typeof(NullableClass16.NestedClass1), "Field1", typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass16.NestedClass1), "Field2", typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass16.NestedClass1), "Field3", typeof(string), Nullability.Nullable)] - [InlineData(typeof(NullableClass17), "Field1", typeof(Task), Nullability.NonNullable, typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass17), "Field2", typeof(Task), Nullability.NonNullable, typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass17), "Field3", typeof(Task), Nullability.NonNullable, typeof(string), Nullability.Nullable)] - public void Method_GetNullability(Type type, string methodName, Type expectedType, Nullability expectedNullability, Type expectedType2 = null, Nullability? expectedNullability2 = null) - { - var method = type.GetMethod(methodName); - var actual = TestGraphType.Instance.GetNullability(method.ReturnParameter).ToList(); - actual.Count.ShouldBe(expectedType2 == null ? 1 : 2); - actual[0].Item1.ShouldBe(expectedType); - actual[0].Item2.ShouldBe(expectedNullability); - if (expectedType2 != null) { - actual[1].Item1.ShouldBe(expectedType2); - actual[1].Item2.ShouldBe(expectedNullability2.Value); - } - } - - public static IEnumerable Class18_GetNullability_TestCases() - { - return new object[][] { - new object[] { "Field1", new List<(Type, Nullability)>() { - (typeof(Tuple), Nullability.NonNullable), - (typeof(string), Nullability.NonNullable), - (typeof(string), Nullability.Nullable), - } }, - new object[] { "Field2", new List<(Type, Nullability)>() { - (typeof(Tuple, string>), Nullability.NonNullable), - (typeof(Tuple), Nullability.NonNullable), - (typeof(string), Nullability.Nullable), - (typeof(string), Nullability.Nullable), - (typeof(string), Nullability.NonNullable), - } }, - new object[] { "Field3", new List<(Type, Nullability)>() { - (typeof(Tuple), Nullability.NonNullable), - (typeof(int), Nullability.Nullable), - (typeof(string), Nullability.Nullable), - } }, - new object[] { "Field4", new List<(Type, Nullability)>() { - (typeof(Tuple), Nullability.NonNullable), - (typeof(Guid), Nullability.NonNullable), - (typeof(string), Nullability.Nullable), - } }, - new object[] { "Field5", new List<(Type, Nullability)>() { - (typeof(Tuple), Nullability.NonNullable), - (typeof(string[]), Nullability.NonNullable), - (typeof(string), Nullability.NonNullable), - (typeof(string), Nullability.Nullable), - } }, - new object[] { "Field6", new List<(Type, Nullability)>() { - (typeof(Tuple), Nullability.NonNullable), - (typeof(int[]), Nullability.NonNullable), - (typeof(int), Nullability.NonNullable), - (typeof(string), Nullability.Nullable), - } }, - new object[] { "Field7", new List<(Type, Nullability)>() { - (typeof(Tuple<(int, string), string>), Nullability.NonNullable), - (typeof((int, string)), Nullability.NonNullable), - (typeof(int), Nullability.NonNullable), - (typeof(string), Nullability.NonNullable), - (typeof(string), Nullability.Nullable), - } }, - new object[] { "Field8", new List<(Type, Nullability)>() { - (typeof(Tuple<(int, string), string>), Nullability.NonNullable), - (typeof((int, string)), Nullability.NonNullable), - (typeof(int), Nullability.NonNullable), - (typeof(string), Nullability.NonNullable), - (typeof(string), Nullability.Nullable), - } }, - new object[] { "Field9", new List<(Type, Nullability)>() { - (typeof(Tuple, string>), Nullability.NonNullable), - (typeof(TestStruct), Nullability.NonNullable), - (typeof(Guid), Nullability.NonNullable), - (typeof(string), Nullability.Nullable), - } }, - new object[] { "Field10", new List<(Type, Nullability)>() { - (typeof(Tuple<,>).MakeGenericType(typeof(NullableClass18<>).GetGenericArguments()[0], typeof(string)), Nullability.NonNullable), - (typeof(NullableClass18<>).GetGenericArguments()[0], Nullability.NonNullable), - (typeof(string), Nullability.Nullable), - } }, - }; - } - - [Theory] - [MemberData(nameof(Class18_GetNullability_TestCases))] - public void Method_GetNullability_Class18(string methodName, List<(Type, Nullability)> expected) - { - var method = typeof(NullableClass18<>).GetMethod(methodName).ShouldNotBeNull(); - var actual = TestGraphType.Instance.GetNullability(method.ReturnParameter).ToList(); - expected.ShouldBe(actual); - } - - [Theory] - [InlineData(typeof(NullableClass1), "Field1", typeof(string), true)] - [InlineData(typeof(NullableClass1), "Field2", typeof(string), true, false, false, false)] - [InlineData(typeof(NullableClass1), "Field3", typeof(int), false)] - [InlineData(typeof(NullableClass1), "Field4", typeof(int), false, false, false, true)] - [InlineData(typeof(NullableClass1), "Field5", typeof(int), true)] - [InlineData(typeof(NullableClass1), "Field6", typeof(int), true, false, false, false)] - [InlineData(typeof(NullableClass1), "Field7", typeof(string), false)] - [InlineData(typeof(NullableClass1), "Field8", typeof(string), false, false, false, true)] - [InlineData(typeof(NullableClass1), "Field9", typeof(string), false, false, false, false, false, typeof(IdGraphType))] - [InlineData(typeof(NullableClass1), "Field10", typeof(string), false, true, false, false, true)] - [InlineData(typeof(NullableClass2), "Field1", typeof(string), true)] - [InlineData(typeof(NullableClass2), "Field2", typeof(string), true, false, false, false)] - [InlineData(typeof(NullableClass2), "Field3", typeof(int), false)] - [InlineData(typeof(NullableClass2), "Field4", typeof(int), false, false, false, true)] - [InlineData(typeof(NullableClass2), "Field5", typeof(int), true)] - [InlineData(typeof(NullableClass2), "Field6", typeof(int), true, false, false, false)] - [InlineData(typeof(NullableClass2), "Field7", typeof(string), true)] - [InlineData(typeof(NullableClass2), "Field8", typeof(string), true)] - [InlineData(typeof(NullableClass2), "Field9", typeof(string), false)] - [InlineData(typeof(NullableClass2), "Field10", typeof(string), false, false, false, true)] - [InlineData(typeof(NullableClass2), "Field11", typeof(string), true, true, true, true, false)] - [InlineData(typeof(NullableClass5), "Test", typeof(string), false)] - [InlineData(typeof(NullableClass6), "Field1", typeof(string), false)] - [InlineData(typeof(NullableClass6), "Field2", typeof(string), true)] - [InlineData(typeof(NullableClass7), "Field1", typeof(string), false)] - [InlineData(typeof(NullableClass7), "Field2", typeof(string), false)] - [InlineData(typeof(NullableClass7), "Field3", typeof(string), true)] - [InlineData(typeof(NullableClass8), "Field1", typeof(string), true)] - [InlineData(typeof(NullableClass8), "Field2", typeof(string), true)] - [InlineData(typeof(NullableClass8), "Field3", typeof(string), false)] - [InlineData(typeof(NullableClass8), "Field4", typeof(int), false)] - [InlineData(typeof(NullableClass9), "Field1", typeof(string), false)] - [InlineData(typeof(NullableClass10), "Field1", typeof(string), true)] - [InlineData(typeof(NullableClass11), "Field1", typeof(string), false)] - [InlineData(typeof(NullableClass11), "Field2", typeof(string), true)] - [InlineData(typeof(NullableClass12), "Field1", typeof(string), false)] - [InlineData(typeof(NullableClass12), "Field2", typeof(string), true)] - [InlineData(typeof(NullableClass12), "Field3", typeof(string), false)] - [InlineData(typeof(NullableClass12), "Field4", typeof(string), true)] - [InlineData(typeof(NullableClass13), "Field1", typeof(string), false)] - [InlineData(typeof(NullableClass13), "Field2", typeof(string), false)] - [InlineData(typeof(NullableClass14), "Field1", typeof(string), true)] - [InlineData(typeof(NullableClass14), "Field2", typeof(string), true)] - [InlineData(typeof(NullableClass15), "Field1", typeof(string), false)] - [InlineData(typeof(NullableClass15), "Field2", typeof(string), true)] - [InlineData(typeof(NullableClass15), "Field3", typeof(string), false)] - [InlineData(typeof(NullableClass15), "Field4", typeof(string), true)] - [InlineData(typeof(NullableClass16), "Field1", typeof(string), false)] - [InlineData(typeof(NullableClass16), "Field2", typeof(string), false)] - [InlineData(typeof(NullableClass16), "Field3", typeof(string), true)] - [InlineData(typeof(NullableClass16.NestedClass1), "Field1", typeof(string), false)] - [InlineData(typeof(NullableClass16.NestedClass1), "Field2", typeof(string), false)] - [InlineData(typeof(NullableClass16.NestedClass1), "Field3", typeof(string), true)] - [InlineData(typeof(NullableClass17), "Field1", typeof(string), false)] - [InlineData(typeof(NullableClass17), "Field2", typeof(string), false)] - [InlineData(typeof(NullableClass17), "Field3", typeof(string), true)] - [InlineData(typeof(NullableClass19), "Field1", typeof(string), false)] - [InlineData(typeof(NullableClass19), "Field2", typeof(string), false)] - [InlineData(typeof(NullableClass19), "Field3", typeof(string), false, true, false)] - [InlineData(typeof(NullableClass19), "Field4", typeof(string), false, true, false)] - [InlineData(typeof(NullableClass19), "Field5", typeof(string), false, true, false)] - [InlineData(typeof(NullableClass19), "Field6", typeof(string), false, true, false)] - [InlineData(typeof(NullableClass19), "Field7", typeof(string), false, true, false)] - [InlineData(typeof(NullableClass19), "Field8", typeof(string), false, true, false)] - [InlineData(typeof(NullableClass19), "Field9", typeof(string), false, true, false)] - [InlineData(typeof(NullableClass19), "Field10", typeof(string), false, true, false)] - [InlineData(typeof(NullableClass19), "Field11", typeof(string), false, true, false)] - [InlineData(typeof(NullableClass19), "Field12", typeof(object), true, true, false)] - [InlineData(typeof(NullableClass19), "Field13", typeof(object), true, true, false)] - [InlineData(typeof(NullableClass19), "Field14", typeof(string), false)] - [InlineData(typeof(NullableClass19), "Field15", typeof(object), true)] - [InlineData(typeof(NullableClass19), "Field16", typeof(string), false, true, false)] - [InlineData(typeof(NullableClass19), "Field17", typeof(object), false)] - public void Method_GetTypeInformation(Type type, string methodName, Type expectedType, bool isNullable = false, bool isList = false, bool isNullableList = false, bool? isNullableAfterAttributes = null, bool? isNullableListAfterAttributes = null, Type expectedGraphType = null) - { - var method = type.GetMethod(methodName); - var actual = TestGraphType.Instance.GetTypeInformation(method.ReturnParameter, false); - actual.ParameterInfo.ShouldBe(method.ReturnParameter); - actual.IsInputType.ShouldBeFalse(); - actual.GraphType.ShouldBeNull(); - actual.Type.ShouldBe(expectedType); - actual.IsNullable.ShouldBe(isNullable); - actual.IsList.ShouldBe(isList); - actual.ListIsNullable.ShouldBe(isNullableList); - actual = TestGraphType.Instance.ApplyAttributes(actual); - actual.IsNullable.ShouldBe(isNullableAfterAttributes ?? isNullable); - actual.ListIsNullable.ShouldBe(isNullableListAfterAttributes ?? isNullableList); - actual.GraphType.ShouldBe(expectedGraphType); - } - - [Theory] - [InlineData(typeof(NullableClass9), "Field1", "arg1", typeof(string), Nullability.Nullable)] - [InlineData(typeof(NullableClass9), "Field1", "arg2", typeof(string), Nullability.Nullable)] - [InlineData(typeof(NullableClass10), "Field1", "arg1", typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass10), "Field1", "arg2", typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass11), "Field2", "arg1", typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass11), "Field2", "arg2", typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass13), "Field2", "arg1", typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass13), "Field2", "arg2", typeof(string), Nullability.Nullable)] - [InlineData(typeof(NullableClass13), "Field2", "arg3", typeof(int), Nullability.NonNullable)] - [InlineData(typeof(NullableClass13), "Field2", "arg4", typeof(int), Nullability.Nullable)] - [InlineData(typeof(NullableClass13), "Field2", "arg5", typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass13), "Field2", "arg6", typeof(IEnumerable), Nullability.NonNullable, typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass13), "Field2", "arg7", typeof(IEnumerable), Nullability.NonNullable, typeof(string), Nullability.Nullable)] - [InlineData(typeof(NullableClass13), "Field2", "arg8", typeof(IEnumerable), Nullability.Nullable, typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass13), "Field2", "arg9", typeof(IEnumerable), Nullability.Nullable, typeof(string), Nullability.Nullable)] - [InlineData(typeof(NullableClass14), "Field2", "arg1", typeof(string), Nullability.Nullable)] - [InlineData(typeof(NullableClass14), "Field2", "arg2", typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass14), "Field2", "arg3", typeof(int), Nullability.NonNullable)] - [InlineData(typeof(NullableClass14), "Field2", "arg4", typeof(int), Nullability.Nullable)] - [InlineData(typeof(NullableClass14), "Field2", "arg5", typeof(string), Nullability.Nullable)] - [InlineData(typeof(NullableClass14), "Field2", "arg6", typeof(IEnumerable), Nullability.NonNullable, typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass14), "Field2", "arg7", typeof(IEnumerable), Nullability.NonNullable, typeof(string), Nullability.Nullable)] - [InlineData(typeof(NullableClass14), "Field2", "arg8", typeof(IEnumerable), Nullability.Nullable, typeof(string), Nullability.NonNullable)] - [InlineData(typeof(NullableClass14), "Field2", "arg9", typeof(IEnumerable), Nullability.Nullable, typeof(string), Nullability.Nullable)] - public void Argument(Type type, string methodName, string argumentName, Type expectedType, Nullability expectedNullability, Type expectedType2 = null, Nullability? expectedNullability2 = null) - { - var method = type.GetMethod(methodName); - var argument = method.GetParameters().Single(x => x.Name == argumentName); - var actual = TestGraphType.Instance.GetNullability(argument).ToList(); - actual.Count.ShouldBe(expectedType2 == null ? 1 : 2); - actual[0].Item1.ShouldBe(expectedType); - actual[0].Item2.ShouldBe(expectedNullability); - if (expectedType2 != null) { - actual[1].Item1.ShouldBe(expectedType2); - actual[1].Item2.ShouldBe(expectedNullability2.Value); - } - } - - [Theory] - [InlineData(typeof(NullableClass9), "Field1", "arg1", typeof(string), true)] - [InlineData(typeof(NullableClass9), "Field1", "arg2", typeof(string), true)] - [InlineData(typeof(NullableClass10), "Field1", "arg1", typeof(string), false)] - [InlineData(typeof(NullableClass10), "Field1", "arg2", typeof(string), false)] - [InlineData(typeof(NullableClass11), "Field2", "arg1", typeof(string), false)] - [InlineData(typeof(NullableClass11), "Field2", "arg2", typeof(string), false)] - [InlineData(typeof(NullableClass13), "Field2", "arg1", typeof(string), false)] - [InlineData(typeof(NullableClass13), "Field2", "arg2", typeof(string), true)] - [InlineData(typeof(NullableClass13), "Field2", "arg3", typeof(int), false)] - [InlineData(typeof(NullableClass13), "Field2", "arg4", typeof(int), true)] - [InlineData(typeof(NullableClass13), "Field2", "arg5", typeof(string), false, false, false, true)] - [InlineData(typeof(NullableClass13), "Field2", "arg6", typeof(string), false, true, false)] - [InlineData(typeof(NullableClass13), "Field2", "arg7", typeof(string), true, true, false)] - [InlineData(typeof(NullableClass13), "Field2", "arg8", typeof(string), false, true, true)] - [InlineData(typeof(NullableClass13), "Field2", "arg9", typeof(string), true, true, true)] - [InlineData(typeof(NullableClass14), "Field2", "arg1", typeof(string), true)] - [InlineData(typeof(NullableClass14), "Field2", "arg2", typeof(string), false)] - [InlineData(typeof(NullableClass14), "Field2", "arg3", typeof(int), false)] - [InlineData(typeof(NullableClass14), "Field2", "arg4", typeof(int), true)] - [InlineData(typeof(NullableClass14), "Field2", "arg5", typeof(string), true, false, false, false)] - [InlineData(typeof(NullableClass14), "Field2", "arg6", typeof(string), false, true, false)] - [InlineData(typeof(NullableClass14), "Field2", "arg7", typeof(string), true, true, false)] - [InlineData(typeof(NullableClass14), "Field2", "arg8", typeof(string), false, true, true)] - [InlineData(typeof(NullableClass14), "Field2", "arg9", typeof(string), true, true, true)] - [InlineData(typeof(NullableClass19), "Field18", "arg1", typeof(string), true)] - [InlineData(typeof(NullableClass19), "Field18", "arg2", typeof(string), false, true, true)] - [InlineData(typeof(NullableClass19), "Field18", "arg3", typeof(object), true)] - [InlineData(typeof(NullableClass19), "Field18", "arg4", typeof(object), false, true, true)] - public void Argument_GetTypeInformation(Type type, string methodName, string argumentName, Type expectedType, bool isNullable = false, bool isList = false, bool isNullableList = false, bool? isNullableAfterAttributes = null, bool? isNullableListAfterAttributes = null) - { - var method = type.GetMethod(methodName); - var argument = method.GetParameters().Single(x => x.Name == argumentName); - var actual = TestGraphType.Instance.GetTypeInformation(argument, true); - actual.ParameterInfo.ShouldBe(argument); - actual.IsInputType.ShouldBeTrue(); - actual.GraphType.ShouldBeNull(); - actual.Type.ShouldBe(expectedType); - actual.IsNullable.ShouldBe(isNullable); - actual.IsList.ShouldBe(isList); - actual.ListIsNullable.ShouldBe(isNullableList); - actual = TestGraphType.Instance.ApplyAttributes(actual); - actual.IsNullable.ShouldBe(isNullableAfterAttributes ?? isNullable); - actual.ListIsNullable.ShouldBe(isNullableListAfterAttributes ?? isNullableList); - } - private class TestGraphType : DIObjectGraphType - { - public static TestGraphType Instance = new(); - public new IEnumerable<(Type, Nullability)> GetNullability(ParameterInfo parameterInfo) => base.GetNullability(parameterInfo); - public new TypeInformation GetTypeInformation(ParameterInfo parameterInfo, bool isInputArgument) => base.GetTypeInformation(parameterInfo, isInputArgument); - public new TypeInformation ApplyAttributes(TypeInformation typeInformation) => base.ApplyAttributes(typeInformation); - } - - private class TestClass : DIObjectGraphBase { } - } -} diff --git a/src/Tests/Execution/DISchemaTypes.cs b/src/Tests/Execution/DISchemaTypes.cs new file mode 100644 index 0000000..c830ee5 --- /dev/null +++ b/src/Tests/Execution/DISchemaTypes.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using GraphQL.DI; +using GraphQL.Types; +using Moq; +using Shouldly; +using Xunit; + +namespace Execution +{ + public class DISchemaTypesTests + { + [Theory] + [InlineData(typeof(int), false, typeof(IntGraphType))] + [InlineData(typeof(int), true, typeof(IntGraphType))] + [InlineData(typeof(Class1), false, null)] + [InlineData(typeof(Class1), true, typeof(AutoInputObjectGraphType))] + public void GetGraphTypeFromClrType(Type clrType, bool isInputType, Type graphType) + { + var mySchemaTypes = new MySchemaTypes(); + var mappedTypes = new List<(Type, Type)>(); + mySchemaTypes.GetGraphTypeFromClrType(clrType, isInputType, mappedTypes).ShouldBe(graphType); + } + + private class MySchemaTypes : DISchemaTypes + { + public MySchemaTypes() : base(new Schema(), Mock.Of()) { } + + public new Type GetGraphTypeFromClrType(Type clrType, bool isInputType, List<(Type ClrType, Type GraphType)> typeMappings) + => base.GetGraphTypeFromClrType(clrType, isInputType, typeMappings); + } + + private class Class1 { } + } +} diff --git a/src/Tests/Execution/GraphQLBuilderTests.cs b/src/Tests/Execution/GraphQLBuilderTests.cs new file mode 100644 index 0000000..3511a65 --- /dev/null +++ b/src/Tests/Execution/GraphQLBuilderTests.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using GraphQL.DI; +using GraphQL.Types; +using Moq; +using Shouldly; +using Xunit; + +namespace Execution +{ + public class GraphQLBuilderTests + { + private readonly Mock _mockGraphQLBuilder = new Mock(MockBehavior.Strict); + private IGraphQLBuilder _graphQLBuilder => _mockGraphQLBuilder.Object; + + [Fact] + public void AddDIGraphTypes() + { + _mockGraphQLBuilder.Setup(x => x.TryRegister(typeof(AutoInputObjectGraphType<>), typeof(AutoInputObjectGraphType<>), ServiceLifetime.Transient)).Returns(_graphQLBuilder).Verifiable(); + _mockGraphQLBuilder.Setup(x => x.TryRegister(typeof(DIObjectGraphType<>), typeof(DIObjectGraphType<>), ServiceLifetime.Transient)).Returns(_graphQLBuilder).Verifiable(); + _mockGraphQLBuilder.Setup(x => x.TryRegister(typeof(DIObjectGraphType<,>), typeof(DIObjectGraphType<,>), ServiceLifetime.Transient)).Returns(_graphQLBuilder).Verifiable(); + _graphQLBuilder.AddDIGraphTypes().ShouldBe(_graphQLBuilder); + _mockGraphQLBuilder.Verify(); + } + + [Fact] + public void AddDIClrTypeMappings() + { + var actual = new List<(Type clrType, Type graphType)>(); + var existingMappings = new (Type clrType, Type graphType)[] { + (typeof(Class7), typeof(Graph7)), + (typeof(Class8), typeof(Graph8)) + }; + var mockSchema = new Mock(MockBehavior.Strict); + mockSchema.Setup(x => x.TypeMappings).Returns(existingMappings).Verifiable(); + mockSchema.Setup(x => x.RegisterTypeMapping(It.IsAny(), It.IsAny())) + .Callback((clrType, graphType) => actual.Add((clrType, graphType))); + _mockGraphQLBuilder.Setup(x => x.Register(typeof(IConfigureSchema), It.IsAny(), false)) + .Returns((_, configure, _) => { + configure.Configure(mockSchema.Object, null); + return _mockGraphQLBuilder.Object; + }) + .Verifiable(); + _graphQLBuilder.AddDIClrTypeMappings(); + mockSchema.Verify(); + _mockGraphQLBuilder.Verify(); + var expected = new List<(Type clrType, Type graphType)> { + (typeof(Class2), typeof(DIObjectGraphType)), + (typeof(Class4), typeof(DIObjectGraphType)), + (typeof(Class8), typeof(DIObjectGraphType)), + }; + actual.ShouldBe(actual); + } + + + private class Class1 { } + private class Class2 { } + private class Class3 { } + private class Class4 { } + private class Class7 { } + private class Class8 { } + private class Base1 : DIObjectGraphBase { } //don't register because graph1 + private class Base2 : DIObjectGraphBase { } //register because graph2 is input + private class Base3 : DIObjectGraphBase { } //don't register because graph3 + private class Base4 : DIObjectGraphBase { } //register because no conflict + private class Base5 : DIObjectGraphBase { } //don't register because object type + private class Base6 : DIObjectGraphBase { } //don't register because object type + private class Base7 : DIObjectGraphBase { } //don't register because graph7 was manually registered + private class Base8 : DIObjectGraphBase { } //register because graph8 is input + private class Graph1 : ObjectGraphType { } + private class Graph2 : InputObjectGraphType { } + private class Graph3 : DIObjectGraphType { } + private class Graph5 : DIObjectGraphType { } + private class Graph7 : ObjectGraphType { } + private class Graph8 : InputObjectGraphType { } + } +} diff --git a/src/Tests/NullableTests/ArgumentTests.cs b/src/Tests/NullableTests/ArgumentTests.cs new file mode 100644 index 0000000..13fe950 --- /dev/null +++ b/src/Tests/NullableTests/ArgumentTests.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using GraphQL.DI; +using Shouldly; +using Xunit; + +namespace NullableTests +{ + public class ArgumentTests + { + [Theory] + [InlineData(typeof(NullableClass9), "Field1", "arg1", typeof(string), Nullability.Nullable)] + [InlineData(typeof(NullableClass9), "Field1", "arg2", typeof(string), Nullability.Nullable)] + [InlineData(typeof(NullableClass10), "Field1", "arg1", typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass10), "Field1", "arg2", typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass11), "Field2", "arg1", typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass11), "Field2", "arg2", typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass13), "Field2", "arg1", typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass13), "Field2", "arg2", typeof(string), Nullability.Nullable)] + [InlineData(typeof(NullableClass13), "Field2", "arg3", typeof(int), Nullability.NonNullable)] + [InlineData(typeof(NullableClass13), "Field2", "arg4", typeof(int), Nullability.Nullable)] + [InlineData(typeof(NullableClass13), "Field2", "arg5", typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass13), "Field2", "arg6", typeof(IEnumerable), Nullability.NonNullable, typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass13), "Field2", "arg7", typeof(IEnumerable), Nullability.NonNullable, typeof(string), Nullability.Nullable)] + [InlineData(typeof(NullableClass13), "Field2", "arg8", typeof(IEnumerable), Nullability.Nullable, typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass13), "Field2", "arg9", typeof(IEnumerable), Nullability.Nullable, typeof(string), Nullability.Nullable)] + [InlineData(typeof(NullableClass14), "Field2", "arg1", typeof(string), Nullability.Nullable)] + [InlineData(typeof(NullableClass14), "Field2", "arg2", typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass14), "Field2", "arg3", typeof(int), Nullability.NonNullable)] + [InlineData(typeof(NullableClass14), "Field2", "arg4", typeof(int), Nullability.Nullable)] + [InlineData(typeof(NullableClass14), "Field2", "arg5", typeof(string), Nullability.Nullable)] + [InlineData(typeof(NullableClass14), "Field2", "arg6", typeof(IEnumerable), Nullability.NonNullable, typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass14), "Field2", "arg7", typeof(IEnumerable), Nullability.NonNullable, typeof(string), Nullability.Nullable)] + [InlineData(typeof(NullableClass14), "Field2", "arg8", typeof(IEnumerable), Nullability.Nullable, typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass14), "Field2", "arg9", typeof(IEnumerable), Nullability.Nullable, typeof(string), Nullability.Nullable)] + public void Argument(Type type, string methodName, string argumentName, Type expectedType, Nullability expectedNullability, Type expectedType2 = null, Nullability? expectedNullability2 = null) + { + var method = type.GetMethod(methodName); + var argument = method.GetParameters().Single(x => x.Name == argumentName); + var actual = argument.GetNullabilityInformation().ToList(); + actual.Count.ShouldBe(expectedType2 == null ? 1 : 2); + actual[0].Type.ShouldBe(expectedType); + actual[0].Nullable.ShouldBe(expectedNullability); + if (expectedType2 != null) { + actual[1].Type.ShouldBe(expectedType2); + actual[1].Nullable.ShouldBe(expectedNullability2.Value); + } + } + + [Theory] + [InlineData(typeof(NullableClass9), "Field1", "arg1", typeof(string), true)] + [InlineData(typeof(NullableClass9), "Field1", "arg2", typeof(string), true)] + [InlineData(typeof(NullableClass10), "Field1", "arg1", typeof(string), false)] + [InlineData(typeof(NullableClass10), "Field1", "arg2", typeof(string), false)] + [InlineData(typeof(NullableClass11), "Field2", "arg1", typeof(string), false)] + [InlineData(typeof(NullableClass11), "Field2", "arg2", typeof(string), false)] + [InlineData(typeof(NullableClass13), "Field2", "arg1", typeof(string), false)] + [InlineData(typeof(NullableClass13), "Field2", "arg2", typeof(string), true)] + [InlineData(typeof(NullableClass13), "Field2", "arg3", typeof(int), false)] + [InlineData(typeof(NullableClass13), "Field2", "arg4", typeof(int), true)] + [InlineData(typeof(NullableClass13), "Field2", "arg5", typeof(string), false, false, false, true)] + [InlineData(typeof(NullableClass13), "Field2", "arg6", typeof(string), false, true, false)] + [InlineData(typeof(NullableClass13), "Field2", "arg7", typeof(string), true, true, false)] + [InlineData(typeof(NullableClass13), "Field2", "arg8", typeof(string), false, true, true)] + [InlineData(typeof(NullableClass13), "Field2", "arg9", typeof(string), true, true, true)] + [InlineData(typeof(NullableClass14), "Field2", "arg1", typeof(string), true)] + [InlineData(typeof(NullableClass14), "Field2", "arg2", typeof(string), false)] + [InlineData(typeof(NullableClass14), "Field2", "arg3", typeof(int), false)] + [InlineData(typeof(NullableClass14), "Field2", "arg4", typeof(int), true)] + [InlineData(typeof(NullableClass14), "Field2", "arg5", typeof(string), true, false, false, false)] + [InlineData(typeof(NullableClass14), "Field2", "arg6", typeof(string), false, true, false)] + [InlineData(typeof(NullableClass14), "Field2", "arg7", typeof(string), true, true, false)] + [InlineData(typeof(NullableClass14), "Field2", "arg8", typeof(string), false, true, true)] + [InlineData(typeof(NullableClass14), "Field2", "arg9", typeof(string), true, true, true)] + [InlineData(typeof(NullableClass19), "Field18", "arg1", typeof(string), true)] + [InlineData(typeof(NullableClass19), "Field18", "arg2", typeof(string), false, true, true)] + [InlineData(typeof(NullableClass19), "Field18", "arg3", typeof(object), true)] + [InlineData(typeof(NullableClass19), "Field18", "arg4", typeof(object), false, true, true)] + public void Argument_GetTypeInformation(Type type, string methodName, string argumentName, Type expectedType, bool isNullable = false, bool isList = false, bool isNullableList = false, bool? isNullableAfterAttributes = null, bool? isNullableListAfterAttributes = null) + { + var method = type.GetMethod(methodName); + var argument = method.GetParameters().Single(x => x.Name == argumentName); + var actual = TestGraphType.Instance.GetTypeInformation(argument, true); + actual.ParameterInfo.ShouldBe(argument); + actual.MemberInfo.ShouldBe(argument.Member); + actual.IsInputType.ShouldBeTrue(); + actual.GraphType.ShouldBeNull(); + actual.Type.ShouldBe(expectedType); + actual.IsNullable.ShouldBe(isNullable); + actual.IsList.ShouldBe(isList); + actual.ListIsNullable.ShouldBe(isNullableList); + actual = TestGraphType.Instance.ApplyAttributes(actual); + actual.IsNullable.ShouldBe(isNullableAfterAttributes ?? isNullable); + actual.ListIsNullable.ShouldBe(isNullableListAfterAttributes ?? isNullableList); + } + } +} diff --git a/src/Tests/NullableTests/MethodTests.cs b/src/Tests/NullableTests/MethodTests.cs new file mode 100644 index 0000000..ba9612f --- /dev/null +++ b/src/Tests/NullableTests/MethodTests.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using GraphQL.DataLoader; +using GraphQL.DI; +using GraphQL.Types; +using Shouldly; +using Xunit; + +namespace NullableTests +{ + public class MethodTests + { + [Theory] + [InlineData(typeof(NullableClass1), "Field1", typeof(string), Nullability.Nullable)] + [InlineData(typeof(NullableClass1), "Field2", typeof(string), Nullability.Nullable)] + [InlineData(typeof(NullableClass1), "Field3", typeof(int), Nullability.NonNullable)] + [InlineData(typeof(NullableClass1), "Field4", typeof(int), Nullability.NonNullable)] + [InlineData(typeof(NullableClass1), "Field5", typeof(int), Nullability.Nullable)] + [InlineData(typeof(NullableClass1), "Field6", typeof(int), Nullability.Nullable)] + [InlineData(typeof(NullableClass1), "Field7", typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass1), "Field8", typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass1), "Field9", typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass1), "Field10", typeof(List), Nullability.NonNullable, typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass2), "Field1", typeof(string), Nullability.Nullable)] + [InlineData(typeof(NullableClass2), "Field2", typeof(string), Nullability.Nullable)] + [InlineData(typeof(NullableClass2), "Field3", typeof(int), Nullability.NonNullable)] + [InlineData(typeof(NullableClass2), "Field4", typeof(int), Nullability.NonNullable)] + [InlineData(typeof(NullableClass2), "Field5", typeof(int), Nullability.Nullable)] + [InlineData(typeof(NullableClass2), "Field6", typeof(int), Nullability.Nullable)] + [InlineData(typeof(NullableClass2), "Field7", typeof(string), Nullability.Nullable)] + [InlineData(typeof(NullableClass2), "Field8", typeof(string), Nullability.Nullable)] + [InlineData(typeof(NullableClass2), "Field9", typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass2), "Field10", typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass2), "Field11", typeof(List), Nullability.Nullable, typeof(string), Nullability.Nullable)] + [InlineData(typeof(NullableClass5), "Test", typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass6), "Field1", typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass6), "Field2", typeof(string), Nullability.Nullable)] + [InlineData(typeof(NullableClass7), "Field1", typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass7), "Field2", typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass7), "Field3", typeof(string), Nullability.Nullable)] + [InlineData(typeof(NullableClass8), "Field1", typeof(string), Nullability.Nullable)] + [InlineData(typeof(NullableClass8), "Field2", typeof(string), Nullability.Nullable)] + [InlineData(typeof(NullableClass8), "Field3", typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass8), "Field4", typeof(int), Nullability.NonNullable)] + [InlineData(typeof(NullableClass9), "Field1", typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass10), "Field1", typeof(string), Nullability.Nullable)] + [InlineData(typeof(NullableClass11), "Field1", typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass11), "Field2", typeof(string), Nullability.Nullable)] + [InlineData(typeof(NullableClass12), "Field1", typeof(IDataLoaderResult), Nullability.NonNullable, typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass12), "Field2", typeof(IDataLoaderResult), Nullability.NonNullable, typeof(string), Nullability.Nullable)] + [InlineData(typeof(NullableClass12), "Field3", typeof(IDataLoaderResult), Nullability.Nullable, typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass12), "Field4", typeof(IDataLoaderResult), Nullability.Nullable, typeof(string), Nullability.Nullable)] + [InlineData(typeof(NullableClass13), "Field1", typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass13), "Field2", typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass14), "Field1", typeof(string), Nullability.Nullable)] + [InlineData(typeof(NullableClass14), "Field2", typeof(string), Nullability.Nullable)] + [InlineData(typeof(NullableClass15), "Field1", typeof(Task), Nullability.NonNullable, typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass15), "Field2", typeof(Task), Nullability.NonNullable, typeof(string), Nullability.Nullable)] + [InlineData(typeof(NullableClass15), "Field3", typeof(Task), Nullability.Nullable, typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass15), "Field4", typeof(Task), Nullability.Nullable, typeof(string), Nullability.Nullable)] + [InlineData(typeof(NullableClass16), "Field1", typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass16), "Field2", typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass16), "Field3", typeof(string), Nullability.Nullable)] + [InlineData(typeof(NullableClass16.NestedClass1), "Field1", typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass16.NestedClass1), "Field2", typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass16.NestedClass1), "Field3", typeof(string), Nullability.Nullable)] + [InlineData(typeof(NullableClass17), "Field1", typeof(Task), Nullability.NonNullable, typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass17), "Field2", typeof(Task), Nullability.NonNullable, typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass17), "Field3", typeof(Task), Nullability.NonNullable, typeof(string), Nullability.Nullable)] + public void Method_GetNullability(Type type, string methodName, Type expectedType, Nullability expectedNullability, Type expectedType2 = null, Nullability? expectedNullability2 = null) + { + var method = type.GetMethod(methodName); + var actual = method.ReturnParameter.GetNullabilityInformation().ToList(); + actual.Count.ShouldBe(expectedType2 == null ? 1 : 2); + actual[0].Type.ShouldBe(expectedType); + actual[0].Nullable.ShouldBe(expectedNullability); + if (expectedType2 != null) { + actual[1].Type.ShouldBe(expectedType2); + actual[1].Nullable.ShouldBe(expectedNullability2.Value); + } + } + + public static IEnumerable Class18_GetNullability_TestCases() + { + return new object[][] { + new object[] { "Field1", new List<(Type, Nullability)>() { + (typeof(Tuple), Nullability.NonNullable), + (typeof(string), Nullability.NonNullable), + (typeof(string), Nullability.Nullable), + } }, + new object[] { "Field2", new List<(Type, Nullability)>() { + (typeof(Tuple, string>), Nullability.NonNullable), + (typeof(Tuple), Nullability.NonNullable), + (typeof(string), Nullability.Nullable), + (typeof(string), Nullability.Nullable), + (typeof(string), Nullability.NonNullable), + } }, + new object[] { "Field3", new List<(Type, Nullability)>() { + (typeof(Tuple), Nullability.NonNullable), + (typeof(int), Nullability.Nullable), + (typeof(string), Nullability.Nullable), + } }, + new object[] { "Field4", new List<(Type, Nullability)>() { + (typeof(Tuple), Nullability.NonNullable), + (typeof(Guid), Nullability.NonNullable), + (typeof(string), Nullability.Nullable), + } }, + new object[] { "Field5", new List<(Type, Nullability)>() { + (typeof(Tuple), Nullability.NonNullable), + (typeof(string[]), Nullability.NonNullable), + (typeof(string), Nullability.NonNullable), + (typeof(string), Nullability.Nullable), + } }, + new object[] { "Field6", new List<(Type, Nullability)>() { + (typeof(Tuple), Nullability.NonNullable), + (typeof(int[]), Nullability.NonNullable), + (typeof(int), Nullability.NonNullable), + (typeof(string), Nullability.Nullable), + } }, + new object[] { "Field7", new List<(Type, Nullability)>() { + (typeof(Tuple<(int, string), string>), Nullability.NonNullable), + (typeof((int, string)), Nullability.NonNullable), + (typeof(int), Nullability.NonNullable), + (typeof(string), Nullability.NonNullable), + (typeof(string), Nullability.Nullable), + } }, + new object[] { "Field8", new List<(Type, Nullability)>() { + (typeof(Tuple<(int, string), string>), Nullability.NonNullable), + (typeof((int, string)), Nullability.NonNullable), + (typeof(int), Nullability.NonNullable), + (typeof(string), Nullability.NonNullable), + (typeof(string), Nullability.Nullable), + } }, + new object[] { "Field9", new List<(Type, Nullability)>() { + (typeof(Tuple, string>), Nullability.NonNullable), + (typeof(TestStruct), Nullability.NonNullable), + (typeof(Guid), Nullability.NonNullable), + (typeof(string), Nullability.Nullable), + } }, + new object[] { "Field10", new List<(Type, Nullability)>() { + (typeof(Tuple<,>).MakeGenericType(typeof(NullableClass18<>).GetGenericArguments()[0], typeof(string)), Nullability.NonNullable), + (typeof(NullableClass18<>).GetGenericArguments()[0], Nullability.NonNullable), + (typeof(string), Nullability.Nullable), + } }, + }; + } + + [Theory] + [MemberData(nameof(Class18_GetNullability_TestCases))] + public void Method_GetNullability_Class18(string methodName, List<(Type, Nullability)> expected) + { + var method = typeof(NullableClass18<>).GetMethod(methodName).ShouldNotBeNull(); + var actual = method.ReturnParameter.GetNullabilityInformation().ToList(); + expected.ShouldBe(actual); + } + + [Theory] + [InlineData(typeof(NullableClass1), "Field1", typeof(string), true)] + [InlineData(typeof(NullableClass1), "Field2", typeof(string), true, false, false, false)] + [InlineData(typeof(NullableClass1), "Field3", typeof(int), false)] + [InlineData(typeof(NullableClass1), "Field4", typeof(int), false, false, false, true)] + [InlineData(typeof(NullableClass1), "Field5", typeof(int), true)] + [InlineData(typeof(NullableClass1), "Field6", typeof(int), true, false, false, false)] + [InlineData(typeof(NullableClass1), "Field7", typeof(string), false)] + [InlineData(typeof(NullableClass1), "Field8", typeof(string), false, false, false, true)] + [InlineData(typeof(NullableClass1), "Field9", typeof(string), false, false, false, false, false, typeof(IdGraphType))] + [InlineData(typeof(NullableClass1), "Field10", typeof(string), false, true, false, false, true)] + [InlineData(typeof(NullableClass2), "Field1", typeof(string), true)] + [InlineData(typeof(NullableClass2), "Field2", typeof(string), true, false, false, false)] + [InlineData(typeof(NullableClass2), "Field3", typeof(int), false)] + [InlineData(typeof(NullableClass2), "Field4", typeof(int), false, false, false, true)] + [InlineData(typeof(NullableClass2), "Field5", typeof(int), true)] + [InlineData(typeof(NullableClass2), "Field6", typeof(int), true, false, false, false)] + [InlineData(typeof(NullableClass2), "Field7", typeof(string), true)] + [InlineData(typeof(NullableClass2), "Field8", typeof(string), true)] + [InlineData(typeof(NullableClass2), "Field9", typeof(string), false)] + [InlineData(typeof(NullableClass2), "Field10", typeof(string), false, false, false, true)] + [InlineData(typeof(NullableClass2), "Field11", typeof(string), true, true, true, true, false)] + [InlineData(typeof(NullableClass5), "Test", typeof(string), false)] + [InlineData(typeof(NullableClass6), "Field1", typeof(string), false)] + [InlineData(typeof(NullableClass6), "Field2", typeof(string), true)] + [InlineData(typeof(NullableClass7), "Field1", typeof(string), false)] + [InlineData(typeof(NullableClass7), "Field2", typeof(string), false)] + [InlineData(typeof(NullableClass7), "Field3", typeof(string), true)] + [InlineData(typeof(NullableClass8), "Field1", typeof(string), true)] + [InlineData(typeof(NullableClass8), "Field2", typeof(string), true)] + [InlineData(typeof(NullableClass8), "Field3", typeof(string), false)] + [InlineData(typeof(NullableClass8), "Field4", typeof(int), false)] + [InlineData(typeof(NullableClass9), "Field1", typeof(string), false)] + [InlineData(typeof(NullableClass10), "Field1", typeof(string), true)] + [InlineData(typeof(NullableClass11), "Field1", typeof(string), false)] + [InlineData(typeof(NullableClass11), "Field2", typeof(string), true)] + [InlineData(typeof(NullableClass12), "Field1", typeof(string), false)] + [InlineData(typeof(NullableClass12), "Field2", typeof(string), true)] + [InlineData(typeof(NullableClass12), "Field3", typeof(string), false)] + [InlineData(typeof(NullableClass12), "Field4", typeof(string), true)] + [InlineData(typeof(NullableClass13), "Field1", typeof(string), false)] + [InlineData(typeof(NullableClass13), "Field2", typeof(string), false)] + [InlineData(typeof(NullableClass14), "Field1", typeof(string), true)] + [InlineData(typeof(NullableClass14), "Field2", typeof(string), true)] + [InlineData(typeof(NullableClass15), "Field1", typeof(string), false)] + [InlineData(typeof(NullableClass15), "Field2", typeof(string), true)] + [InlineData(typeof(NullableClass15), "Field3", typeof(string), false)] + [InlineData(typeof(NullableClass15), "Field4", typeof(string), true)] + [InlineData(typeof(NullableClass16), "Field1", typeof(string), false)] + [InlineData(typeof(NullableClass16), "Field2", typeof(string), false)] + [InlineData(typeof(NullableClass16), "Field3", typeof(string), true)] + [InlineData(typeof(NullableClass16.NestedClass1), "Field1", typeof(string), false)] + [InlineData(typeof(NullableClass16.NestedClass1), "Field2", typeof(string), false)] + [InlineData(typeof(NullableClass16.NestedClass1), "Field3", typeof(string), true)] + [InlineData(typeof(NullableClass17), "Field1", typeof(string), false)] + [InlineData(typeof(NullableClass17), "Field2", typeof(string), false)] + [InlineData(typeof(NullableClass17), "Field3", typeof(string), true)] + [InlineData(typeof(NullableClass19), "Field1", typeof(string), false)] + [InlineData(typeof(NullableClass19), "Field2", typeof(string), false)] + [InlineData(typeof(NullableClass19), "Field3", typeof(string), false, true, false)] + [InlineData(typeof(NullableClass19), "Field4", typeof(string), false, true, false)] + [InlineData(typeof(NullableClass19), "Field5", typeof(string), false, true, false)] + [InlineData(typeof(NullableClass19), "Field6", typeof(string), false, true, false)] + [InlineData(typeof(NullableClass19), "Field7", typeof(string), false, true, false)] + [InlineData(typeof(NullableClass19), "Field8", typeof(string), false, true, false)] + [InlineData(typeof(NullableClass19), "Field9", typeof(string), false, true, false)] + [InlineData(typeof(NullableClass19), "Field10", typeof(string), false, true, false)] + [InlineData(typeof(NullableClass19), "Field11", typeof(string), false, true, false)] + [InlineData(typeof(NullableClass19), "Field12", typeof(object), true, true, false)] + [InlineData(typeof(NullableClass19), "Field13", typeof(object), true, true, false)] + [InlineData(typeof(NullableClass19), "Field14", typeof(string), false)] + [InlineData(typeof(NullableClass19), "Field15", typeof(object), true)] + [InlineData(typeof(NullableClass19), "Field16", typeof(string), false, true, false)] + [InlineData(typeof(NullableClass19), "Field17", typeof(object), false)] + public void Method_GetTypeInformation(Type type, string methodName, Type expectedType, bool isNullable = false, bool isList = false, bool isNullableList = false, bool? isNullableAfterAttributes = null, bool? isNullableListAfterAttributes = null, Type expectedGraphType = null) + { + var method = type.GetMethod(methodName); + var actual = TestGraphType.Instance.GetTypeInformation(method.ReturnParameter, false); + actual.ParameterInfo.ShouldBe(method.ReturnParameter); + actual.MemberInfo.ShouldBe(method); + actual.IsInputType.ShouldBeFalse(); + actual.GraphType.ShouldBeNull(); + actual.Type.ShouldBe(expectedType); + actual.IsNullable.ShouldBe(isNullable); + actual.IsList.ShouldBe(isList); + actual.ListIsNullable.ShouldBe(isNullableList); + actual = TestGraphType.Instance.ApplyAttributes(actual); + actual.IsNullable.ShouldBe(isNullableAfterAttributes ?? isNullable); + actual.ListIsNullable.ShouldBe(isNullableListAfterAttributes ?? isNullableList); + actual.GraphType.ShouldBe(expectedGraphType); + } + } +} diff --git a/src/Tests/NullabilityTestClasses.cs b/src/Tests/NullableTests/NullabilityTestClasses.cs similarity index 95% rename from src/Tests/NullabilityTestClasses.cs rename to src/Tests/NullableTests/NullabilityTestClasses.cs index f54b11d..d870b2d 100644 --- a/src/Tests/NullabilityTestClasses.cs +++ b/src/Tests/NullableTests/NullabilityTestClasses.cs @@ -6,7 +6,7 @@ using GraphQL.DataLoader; using GraphQL.DI; -namespace Tests.NullabilityTestClasses +namespace NullableTests { public class NullableClass1 { @@ -187,4 +187,14 @@ public class NullableClass19 public static object Field17() => null!; public static string Field18(string arg1 = "test", List arg2 = null!, object arg3 = null!, object[] arg4 = null!) => null!; } + + public class NullableClass20 + { + public static int Field1 => 0; + public static string Field2 => null!; + public static string? Field3 => null; + public static List Field4 => null!; + public static int? Field5 => null; + public static string Field6 => null!; + } } diff --git a/src/Tests/NullableTests/PropertyTests.cs b/src/Tests/NullableTests/PropertyTests.cs new file mode 100644 index 0000000..6a09a48 --- /dev/null +++ b/src/Tests/NullableTests/PropertyTests.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using GraphQL.DataLoader; +using GraphQL.DI; +using GraphQL.Types; +using Shouldly; +using Xunit; + +namespace NullableTests +{ + public class PropertyTests + { + [Theory] + [InlineData(typeof(NullableClass20), "Field1", typeof(int), Nullability.NonNullable)] + [InlineData(typeof(NullableClass20), "Field2", typeof(string), Nullability.NonNullable)] + [InlineData(typeof(NullableClass20), "Field3", typeof(string), Nullability.Nullable)] + [InlineData(typeof(NullableClass20), "Field4", typeof(List), Nullability.NonNullable, typeof(string), Nullability.Nullable)] + [InlineData(typeof(NullableClass20), "Field5", typeof(int), Nullability.Nullable)] + [InlineData(typeof(NullableClass20), "Field6", typeof(string), Nullability.NonNullable)] + public void GetNullability(Type type, string propertyName, Type expectedType, Nullability expectedNullability, Type expectedType2 = null, Nullability? expectedNullability2 = null) + { + var property = type.GetProperty(propertyName); + var actual = property.GetNullabilityInformation().ToList(); + actual.Count.ShouldBe(expectedType2 == null ? 1 : 2); + actual[0].Type.ShouldBe(expectedType); + actual[0].Nullable.ShouldBe(expectedNullability); + if (expectedType2 != null) { + actual[1].Type.ShouldBe(expectedType2); + actual[1].Nullable.ShouldBe(expectedNullability2.Value); + } + } + + [Theory] + [InlineData(typeof(NullableClass20), "Field1", typeof(int), false)] + [InlineData(typeof(NullableClass20), "Field2", typeof(string), false)] + [InlineData(typeof(NullableClass20), "Field3", typeof(string), true)] + [InlineData(typeof(NullableClass20), "Field4", typeof(string), true, true, false)] + [InlineData(typeof(NullableClass20), "Field5", typeof(int), true)] + [InlineData(typeof(NullableClass20), "Field6", typeof(string), false)] + public void GetTypeInformation(Type type, string propertyName, Type expectedType, bool isNullable = false, bool isList = false, bool isNullableList = false, bool? isNullableAfterAttributes = null, bool? isNullableListAfterAttributes = null, Type expectedGraphType = null) + { + var property = type.GetProperty(propertyName); + var actual = TestInputGraphType.Instance.GetTypeInformation(property); + actual.ParameterInfo.ShouldBeNull(); + actual.MemberInfo.ShouldBe(property); + actual.IsInputType.ShouldBeTrue(); + actual.GraphType.ShouldBeNull(); + actual.Type.ShouldBe(expectedType); + actual.IsNullable.ShouldBe(isNullable); + actual.IsList.ShouldBe(isList); + actual.ListIsNullable.ShouldBe(isNullableList); + actual = TestInputGraphType.Instance.ApplyAttributes(actual); + actual.IsNullable.ShouldBe(isNullableAfterAttributes ?? isNullable); + actual.ListIsNullable.ShouldBe(isNullableListAfterAttributes ?? isNullableList); + actual.GraphType.ShouldBe(expectedGraphType); + } + } +} diff --git a/src/Tests/NullableTests/TestGraphType.cs b/src/Tests/NullableTests/TestGraphType.cs new file mode 100644 index 0000000..eb1416c --- /dev/null +++ b/src/Tests/NullableTests/TestGraphType.cs @@ -0,0 +1,21 @@ +using System.Reflection; +using GraphQL.DI; + +namespace NullableTests +{ + public class TestGraphType : DIObjectGraphType + { + public static TestGraphType Instance = new(); + public new TypeInformation GetTypeInformation(ParameterInfo parameterInfo, bool isInputArgument) => base.GetTypeInformation(parameterInfo, isInputArgument); + public new TypeInformation ApplyAttributes(TypeInformation typeInformation) => base.ApplyAttributes(typeInformation); + } + + public class TestInputGraphType : AutoInputObjectGraphType + { + public static TestInputGraphType Instance = new(); + public new TypeInformation GetTypeInformation(PropertyInfo propertyInfo) => base.GetTypeInformation(propertyInfo); + public new TypeInformation ApplyAttributes(TypeInformation typeInformation) => base.ApplyAttributes(typeInformation); + } + + public class TestClass : DIObjectGraphBase { } +} diff --git a/src/Tests/NullableTests/VerifyTestClasses.cs b/src/Tests/NullableTests/VerifyTestClasses.cs new file mode 100644 index 0000000..79f7449 --- /dev/null +++ b/src/Tests/NullableTests/VerifyTestClasses.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reflection; +using Shouldly; +using Xunit; + +namespace NullableTests +{ + public class VerifyTestClasses + { + //Verify... methods verify that .NET is building the classes + // with the expected attributes on them. Failure is not an + // error, but simply indicates that the Method and Argument + // tests may not be testing the anticipated scenarios + //Method and Argument should always pass + [Theory] + [InlineData(typeof(NullableClass1), 1, 0)] //default not nullable + [InlineData(typeof(NullableClass2), 2, 0)] //default nullable + [InlineData(typeof(NullableClass5), null, null)] + [InlineData(typeof(NullableClass6), null, null)] + [InlineData(typeof(NullableClass7), 1, 0)] //default not nullable + [InlineData(typeof(NullableClass8), 2, 0)] //default nullable + [InlineData(typeof(NullableClass9), null, null)] + [InlineData(typeof(NullableClass10), null, null)] + [InlineData(typeof(NullableClass11), 1, 0)] //default not nullable + [InlineData(typeof(NullableClass12), null, null)] + [InlineData(typeof(NullableClass13), 1, 0)] //default not nullable + [InlineData(typeof(NullableClass14), 2, 0)] //default nullable + [InlineData(typeof(NullableClass15), null, null)] + [InlineData(typeof(NullableClass16), 1, 0)] + [InlineData(typeof(NullableClass16.NestedClass1), null, 0)] + [InlineData(typeof(NullableClass17), 1, 0)] + [InlineData(typeof(NullableClass18<>), null, null)] + [InlineData(typeof(NullableClass19), 1, 0)] + [InlineData(typeof(NullableClass20), 1, 0)] + public void VerifyTestClass(Type type, int? nullableContext, int? nullable) + { + var actualHasNullableContext = type.CustomAttributes.FirstOrDefault( + x => x.AttributeType.Name == "NullableContextAttribute"); + if (nullableContext == null) { + actualHasNullableContext.ShouldBeNull(); + } else { + actualHasNullableContext.ShouldNotBeNull(); + actualHasNullableContext.ConstructorArguments[0].Value.ShouldBe(nullableContext); + } + + var actualHasNullable = type.CustomAttributes.FirstOrDefault( + x => x.AttributeType.Name == "NullableAttribute"); + if (nullable == null) { + actualHasNullable.ShouldBeNull(); + } else { + actualHasNullable.ShouldNotBeNull(); + actualHasNullable.ConstructorArguments[0].ArgumentType.ShouldBe(typeof(byte)); + actualHasNullable.ConstructorArguments[0].Value.ShouldBe(nullable); + } + } + + [Theory] + [InlineData(typeof(NullableClass1), "Field1", 2, null)] + [InlineData(typeof(NullableClass1), "Field2", 2, null)] + [InlineData(typeof(NullableClass1), "Field3", null, null)] + [InlineData(typeof(NullableClass1), "Field4", null, null)] + [InlineData(typeof(NullableClass1), "Field5", null, null)] + [InlineData(typeof(NullableClass1), "Field6", null, null)] + [InlineData(typeof(NullableClass1), "Field7", null, null)] + [InlineData(typeof(NullableClass1), "Field8", null, null)] + [InlineData(typeof(NullableClass1), "Field9", null, null)] + [InlineData(typeof(NullableClass1), "Field10", null, null)] + [InlineData(typeof(NullableClass2), "Field1", null, null)] + [InlineData(typeof(NullableClass2), "Field2", null, null)] + [InlineData(typeof(NullableClass2), "Field3", null, null)] + [InlineData(typeof(NullableClass2), "Field4", null, null)] + [InlineData(typeof(NullableClass2), "Field5", null, null)] + [InlineData(typeof(NullableClass2), "Field6", null, null)] + [InlineData(typeof(NullableClass2), "Field7", null, null)] + [InlineData(typeof(NullableClass2), "Field8", null, null)] + [InlineData(typeof(NullableClass2), "Field9", 1, null)] + [InlineData(typeof(NullableClass2), "Field10", 1, null)] + [InlineData(typeof(NullableClass2), "Field11", null, null)] + [InlineData(typeof(NullableClass5), "Test", 1, null)] + [InlineData(typeof(NullableClass6), "Field1", 1, null)] + [InlineData(typeof(NullableClass6), "Field2", 2, null)] + [InlineData(typeof(NullableClass7), "Field1", null, null)] + [InlineData(typeof(NullableClass7), "Field2", null, null)] + [InlineData(typeof(NullableClass7), "Field3", 2, null)] + [InlineData(typeof(NullableClass8), "Field1", null, null)] + [InlineData(typeof(NullableClass8), "Field2", null, null)] + [InlineData(typeof(NullableClass8), "Field3", 1, null)] + [InlineData(typeof(NullableClass8), "Field4", null, null)] + [InlineData(typeof(NullableClass9), "Field1", 2, "1")] + [InlineData(typeof(NullableClass10), "Field1", 1, "2")] + [InlineData(typeof(NullableClass11), "Field1", null, null)] + [InlineData(typeof(NullableClass11), "Field2", null, "2")] + [InlineData(typeof(NullableClass12), "Field1", 1, null)] + [InlineData(typeof(NullableClass12), "Field2", null, "12")] + [InlineData(typeof(NullableClass12), "Field3", null, "21")] + [InlineData(typeof(NullableClass12), "Field4", 2, null)] + [InlineData(typeof(NullableClass13), "Field1", null, null)] + [InlineData(typeof(NullableClass13), "Field2", null, null)] + [InlineData(typeof(NullableClass14), "Field1", null, null)] + [InlineData(typeof(NullableClass14), "Field2", null, null)] + [InlineData(typeof(NullableClass15), "Field1", 1, null)] + [InlineData(typeof(NullableClass15), "Field2", null, "12")] + [InlineData(typeof(NullableClass15), "Field3", null, "21")] + [InlineData(typeof(NullableClass15), "Field4", 2, null)] + [InlineData(typeof(NullableClass16), "Field1", null, null)] + [InlineData(typeof(NullableClass16), "Field2", null, null)] + [InlineData(typeof(NullableClass16), "Field3", 2, null)] + [InlineData(typeof(NullableClass16.NestedClass1), "Field1", null, null)] + [InlineData(typeof(NullableClass16.NestedClass1), "Field2", null, null)] + [InlineData(typeof(NullableClass16.NestedClass1), "Field3", 2, null)] + [InlineData(typeof(NullableClass17), "Field1", null, null)] + [InlineData(typeof(NullableClass17), "Field2", null, null)] + [InlineData(typeof(NullableClass17), "Field3", null, "12")] + [InlineData(typeof(NullableClass18<>), "Field1", null, "112")] + [InlineData(typeof(NullableClass18<>), "Field2", null, "11221")] + [InlineData(typeof(NullableClass18<>), "Field3", null, "12")] + [InlineData(typeof(NullableClass18<>), "Field4", null, "12")] + [InlineData(typeof(NullableClass18<>), "Field5", null, "1112")] + [InlineData(typeof(NullableClass18<>), "Field6", null, "112")] + [InlineData(typeof(NullableClass18<>), "Field7", null, "1012")] + [InlineData(typeof(NullableClass18<>), "Field8", null, "1012")] + [InlineData(typeof(NullableClass18<>), "Field9", null, "102")] + [InlineData(typeof(NullableClass18<>), "Field10", null, "112")] + public void VerifyTestMethod(Type type, string methodName, int? nullableContext, string nullableValues) + { + var method = type.GetMethod(methodName); + var methodNullableAttribute = method.CustomAttributes.FirstOrDefault(x => x.AttributeType.Name == "NullableAttribute"); + methodNullableAttribute.ShouldBeNull(); //should not be possible to apply the attribute here + + var methodNullableContextAttribute = method.CustomAttributes.FirstOrDefault(x => x.AttributeType.Name == "NullableContextAttribute"); + if (nullableContext.HasValue) { + methodNullableContextAttribute.ShouldNotBeNull(); + methodNullableContextAttribute.ConstructorArguments[0].ArgumentType.ShouldBe(typeof(byte)); + methodNullableContextAttribute.ConstructorArguments[0].Value.ShouldBeOfType().ShouldBe((byte)nullableContext.Value); + } else { + methodNullableContextAttribute.ShouldBeNull(); + } + + var parameterNullableAttribute = method.ReturnParameter.CustomAttributes.FirstOrDefault(x => x.AttributeType.Name == "NullableAttribute"); + if (nullableValues != null) { + parameterNullableAttribute.ShouldNotBeNull(); + var expectedValues = nullableValues.Select(x => (byte)int.Parse(x.ToString())).ToArray(); + if (expectedValues.Length == 1) { + parameterNullableAttribute.ConstructorArguments[0].ArgumentType.ShouldBe(typeof(byte)); + var actualValue = parameterNullableAttribute.ConstructorArguments[0].Value.ShouldBeOfType().ToString(); + actualValue.ShouldBe(nullableValues); + } else { + parameterNullableAttribute.ConstructorArguments[0].ArgumentType.ShouldBe(typeof(byte[])); + var actualValues = string.Join("", parameterNullableAttribute.ConstructorArguments[0].Value.ShouldBeOfType>().Select(x => x.Value.ToString())); + actualValues.ShouldBe(nullableValues); + } + } else { + parameterNullableAttribute.ShouldBeNull(); + } + } + + [Theory] + [InlineData(typeof(NullableClass20), "Field1", null)] + [InlineData(typeof(NullableClass20), "Field2", null)] + [InlineData(typeof(NullableClass20), "Field3", "2")] + [InlineData(typeof(NullableClass20), "Field4", "12")] + [InlineData(typeof(NullableClass20), "Field5", null)] + [InlineData(typeof(NullableClass20), "Field6", null)] + public void VerifyTestProperty(Type type, string propertyName, string nullableValues) + { + var property = type.GetProperty(propertyName); + var parameterNullableAttribute = property.CustomAttributes.FirstOrDefault(x => x.AttributeType.Name == "NullableAttribute"); + if (nullableValues != null) { + parameterNullableAttribute.ShouldNotBeNull(); + var expectedValues = nullableValues.Select(x => (byte)int.Parse(x.ToString())).ToArray(); + if (expectedValues.Length == 1) { + parameterNullableAttribute.ConstructorArguments[0].ArgumentType.ShouldBe(typeof(byte)); + var actualValue = parameterNullableAttribute.ConstructorArguments[0].Value.ShouldBeOfType().ToString(); + actualValue.ShouldBe(nullableValues); + } else { + parameterNullableAttribute.ConstructorArguments[0].ArgumentType.ShouldBe(typeof(byte[])); + var actualValues = string.Join("", parameterNullableAttribute.ConstructorArguments[0].Value.ShouldBeOfType>().Select(x => x.Value.ToString())); + actualValues.ShouldBe(nullableValues); + } + } else { + parameterNullableAttribute.ShouldBeNull(); + } + } + + [Theory] + [InlineData(typeof(NullableClass9), "Field1", "arg1", null)] + [InlineData(typeof(NullableClass9), "Field1", "arg2", null)] + [InlineData(typeof(NullableClass10), "Field1", "arg1", null)] + [InlineData(typeof(NullableClass10), "Field1", "arg2", null)] + [InlineData(typeof(NullableClass11), "Field2", "arg1", null)] + [InlineData(typeof(NullableClass11), "Field2", "arg2", null)] + [InlineData(typeof(NullableClass13), "Field2", "arg1", null)] + [InlineData(typeof(NullableClass13), "Field2", "arg2", "2")] + [InlineData(typeof(NullableClass13), "Field2", "arg3", null)] + [InlineData(typeof(NullableClass13), "Field2", "arg4", null)] + [InlineData(typeof(NullableClass13), "Field2", "arg5", null)] + [InlineData(typeof(NullableClass13), "Field2", "arg6", null)] + [InlineData(typeof(NullableClass13), "Field2", "arg7", "12")] + [InlineData(typeof(NullableClass13), "Field2", "arg8", "21")] + [InlineData(typeof(NullableClass13), "Field2", "arg9", "2")] + [InlineData(typeof(NullableClass14), "Field2", "arg1", null)] + [InlineData(typeof(NullableClass14), "Field2", "arg2", "1")] + [InlineData(typeof(NullableClass14), "Field2", "arg3", null)] + [InlineData(typeof(NullableClass14), "Field2", "arg4", null)] + [InlineData(typeof(NullableClass14), "Field2", "arg5", null)] + [InlineData(typeof(NullableClass14), "Field2", "arg6", "1")] + [InlineData(typeof(NullableClass14), "Field2", "arg7", "12")] + [InlineData(typeof(NullableClass14), "Field2", "arg8", "21")] + [InlineData(typeof(NullableClass14), "Field2", "arg9", null)] + public void VerifyTestArgument(Type type, string methodName, string argumentName, string nullableValues) + { + var method = type.GetMethod(methodName); + var argument = method.GetParameters().Single(x => x.Name == argumentName); + var parameterNullableAttribute = argument.CustomAttributes.FirstOrDefault(x => x.AttributeType.Name == "NullableAttribute"); + if (nullableValues != null) { + parameterNullableAttribute.ShouldNotBeNull(); + var expectedValues = nullableValues.Select(x => (byte)int.Parse(x.ToString())).ToArray(); + if (expectedValues.Length == 1) { + parameterNullableAttribute.ConstructorArguments[0].ArgumentType.ShouldBe(typeof(byte)); + var actualValue = parameterNullableAttribute.ConstructorArguments[0].Value.ShouldBeOfType().ToString(); + actualValue.ShouldBe(nullableValues); + } else { + parameterNullableAttribute.ConstructorArguments[0].ArgumentType.ShouldBe(typeof(byte[])); + var actualValues = string.Join("", parameterNullableAttribute.ConstructorArguments[0].Value.ShouldBeOfType>().Select(x => x.Value.ToString())); + actualValues.ShouldBe(nullableValues); + } + } else { + parameterNullableAttribute.ShouldBeNull(); + } + } + } +}