/// <summary> /// Determines whether the the provided type to check is IDictionary{ExpectedKey, AnyValue} ensuring that it can be /// used as an input to a batch processor that supplies data to multiple waiting items. /// </summary> /// <param name="typeToCheck">The type being checked.</param> /// <param name="expectedKeyType">The expected key type.</param> /// <param name="batchedItemType">Type of the batched item. if the type to check returns an IEnumerable per /// key this type must represent the individual item of the set. (i.e. the 'T' of IEnumerable{T}).</param> /// <returns><c>true</c> if the type represents a dictionary keyed on the expected key type; otherwise, <c>false</c>.</returns> public static bool IsBatchDictionaryType(Type typeToCheck, Type expectedKeyType, Type batchedItemType) { if (typeToCheck == null || expectedKeyType == null || batchedItemType == null) { return(false); } var enumeratedType = typeToCheck.GetEnumerableUnderlyingType(); if (enumeratedType == null || !enumeratedType.IsGenericType || typeof(KeyValuePair <,>) != enumeratedType.GetGenericTypeDefinition()) { return(false); } var paramSet = enumeratedType.GetGenericArguments(); if (paramSet.Length != 2) { return(false); } if (paramSet[0] != expectedKeyType) { return(false); } var unwrapped = GraphValidation.EliminateWrappersFromCoreType(paramSet[1]); if (unwrapped != batchedItemType) { return(false); } return(true); }
/// <summary> /// Adds the given type to the schema as a graph type or a registered controller depending /// on the type. /// </summary> /// <param name="type">The concrete type to add.</param> /// <param name="kind">The kind of graph type to create from the supplied concrete type. If not supplied the concrete type will /// attempt to auto assign a type of scalar, enum or object as necessary.</param> public void EnsureGraphType(Type type, TypeKind?kind = null) { if (Validation.IsCastable <GraphController>(type)) { if (GraphQLProviders.TemplateProvider.ParseType(type) is IGraphControllerTemplate controllerDefinition) { this.AddController(controllerDefinition); } return; } type = GraphValidation.EliminateWrappersFromCoreType(type); // if the type is already registered, early exit no point in running it through again var actualKind = GraphValidation.ResolveTypeKindOrThrow(type, kind); if (this.Schema.KnownTypes.Contains(type, actualKind)) { return; } var maker = GraphQLProviders.GraphTypeMakerProvider.CreateTypeMaker(this.Schema, actualKind); if (maker != null) { var result = maker.CreateGraphType(type); if (result != null) { this.Schema.KnownTypes.EnsureGraphType(result.GraphType, result.ConcreteType); this.EnsureDependents(result); } } }
/// <summary> /// Ensures the provided <see cref="IGraphType" /> exists in this collection (adding it if it is missing) /// and that the given type reference is assigned to it. An exception will be thrown if the type reference is already assigned /// to a different <see cref="IGraphType" />. No dependents or additional types will be added. /// </summary> /// <param name="graphType">Type of the graph.</param> /// <param name="associatedType">The concrete type to associate to the graph type.</param> /// <returns><c>true</c> if type had to be added, <c>false</c> if it already existed in the collection.</returns> public bool EnsureGraphType(IGraphType graphType, Type associatedType = null) { Validation.ThrowIfNull(graphType, nameof(graphType)); associatedType = GraphValidation.EliminateWrappersFromCoreType(associatedType); GraphValidation.EnsureValidGraphTypeOrThrow(associatedType); // attempt to create a relationship between the graph type and the associated type // the concrete type collection will throw an exception if that relationship fails or isnt // updated to the new data correctly. var justAdded = _graphTypesByName.TryAdd(graphType.Name, graphType); _concreteTypes.EnsureRelationship(graphType, associatedType); _extendableGraphTypeTracker.MonitorGraphType(graphType); // dequeue and add any extension fields if present if (associatedType != null) { var unregisteredFields = _typeQueue.DequeueFields(associatedType); if (graphType is IExtendableGraphType objType) { foreach (var field in unregisteredFields) { _extendableGraphTypeTracker.AddFieldExtension(objType, field); } } } return(justAdded); }
/// <summary> /// Builds out this field template parsing out the required type declarations unions, nameing scheme etc. This method should be /// overridden in any child classes for additional decalration requirements. /// </summary> protected override void ParseTemplateDefinition() { // ensure type extension right out of the gate // the field can't be built otherwise _typeAttrib = this.SingleAttributeOfTypeOrDefault <TypeExtensionAttribute>(); if (_typeAttrib == null) { return; } _sourceType = _typeAttrib.TypeToExtend; base.ParseTemplateDefinition(); var returnType = GraphValidation.EliminateWrappersFromCoreType(this.DeclaredReturnType); if (returnType != typeof(IGraphActionResult)) { // inspect the return type, if its a valid dictionary extract the return type from the value // and set the type modifiers and method type based on the value of each dictionary entry if (_typeAttrib.ExecutionMode == FieldResolutionMode.Batch) { returnType = returnType.GetValueTypeOfDictionary(); this.ObjectType = GraphValidation.EliminateWrappersFromCoreType(returnType); this.TypeExpression = GraphValidation.GenerateTypeExpression(returnType, this); this.PossibleTypes.Insert(0, this.ObjectType); } } }
/// <summary> /// Type extensions used as batch methods required a speceial input and output signature for the runtime /// to properly supply and retrieve data from the batch. This method ensures the signature coorisponds to those requirements or /// throws an exception indicating the problem if one is found. /// </summary> private void ValidateBatchMethodSignatureOrThrow() { if (this.Mode != FieldResolutionMode.Batch) { return; } // the method MUST accept a parameter of type IEnumerable<TypeToExtend> in its signature somewhere // when declared in batch mode var requiredEnumerable = typeof(IEnumerable <>).MakeGenericType(this.SourceObjectType); if (this.Arguments.All(arg => arg.DeclaredArgumentType != requiredEnumerable)) { throw new GraphTypeDeclarationException( $"Invalid batch method signature. The field '{this.InternalFullName}' declares itself as batch method but does not accept a batch " + $"of data as an input parameter. This method must accept a parameter of type '{requiredEnumerable.FriendlyName()}' somewhere in its method signature to " + $"be used as a batch extension for the type '{this.SourceObjectType.FriendlyName()}'."); } var declaredType = GraphValidation.EliminateWrappersFromCoreType(this.DeclaredReturnType); if (declaredType == typeof(IGraphActionResult)) { return; } // when a batch method doesn't return an action result, indicating the developer // opts to specify his return types explicitly; ensure that their chosen return type is a dictionary // keyed on the type being extended allowing the runtime to seperate the batch // for proper segmentation in the object graph. // -- // when the return type is a graph action this check is deferred after results of the batch are produced if (!BatchResultProcessor.IsBatchDictionaryType(declaredType, this.SourceObjectType, this.ObjectType)) { throw new GraphTypeDeclarationException( $"Invalid batch method signature. The field '{this.InternalFullName}' declares a return type of '{declaredType.FriendlyName()}', however; " + $"batch methods must return either an '{typeof(IGraphActionResult).FriendlyName()}' or a dictionary keyed " + "on the provided source data (e.g. 'IDictionary<SourceType, ResultsPerSourceItem>')."); } // ensure any possible type declared via attribution matches the value type of the resultant dictionary // e.g.. if they supply a union for the field but declare a dictionary of IDictionary<T,K> // each member of the union must be castable to 'K' in order for the runtime to properly seperate // and process the batch results var dictionaryValue = GraphValidation.EliminateWrappersFromCoreType(declaredType.GetValueTypeOfDictionary()); foreach (var type in this.PossibleTypes) { if (!Validation.IsCastable(type, dictionaryValue)) { throw new GraphTypeDeclarationException( $"The field '{this.InternalFullName}' returns '{this.ObjectType.FriendlyName()}' and declares a possible type of '{type.FriendlyName()}' " + $"but that type is not castable to '{this.ObjectType.FriendlyName()}' and therefore not returnable by this field. Due to the strongly-typed nature of C# any possible type on a field " + "must be castable to the type of the field in order to ensure its not inadvertantly nulled out during processing. If this field returns a union " + $"of multiple, disperate types consider returning '{typeof(object).Name}' from the field to ensure each possible return type can be successfully processed."); } } }
/// <summary> /// Parses the primary metadata about the method. /// </summary> public void Parse() { DirectiveLifeCycle lifeCycle; switch (this.Method.Name) { case Constants.ReservedNames.DIRECTIVE_BEFORE_RESOLUTION_METHOD_NAME: lifeCycle = DirectiveLifeCycle.BeforeResolution; break; case Constants.ReservedNames.DIRECTIVE_AFTER_RESOLUTION_METHOD_NAME: lifeCycle = DirectiveLifeCycle.AfterResolution; break; default: return; } this.IsValidDirectiveMethod = (this.Method.ReturnType == typeof(IGraphActionResult) || this.Method.ReturnType == typeof(Task <IGraphActionResult>)) && !this.Method.IsGenericMethod; this.Description = this.Method.SingleAttributeOrDefault <DescriptionAttribute>()?.Description; this.DeclaredType = this.Method.ReturnType; this.IsAsyncField = Validation.IsCastable <Task>(this.Method.ReturnType); // is the method asyncronous? if so ensure that a Task<T> is returned // and not an empty task if (this.IsAsyncField && this.Method.ReturnType.IsGenericType) { // for any ssync field attempt to pull out the T in Task<T> var genericArgs = this.Method.ReturnType.GetGenericArguments(); if (genericArgs.Any()) { this.DeclaredType = genericArgs[0]; } } this.ObjectType = GraphValidation.EliminateWrappersFromCoreType(this.DeclaredType); this.LifeCycle = lifeCycle; this.Route = this.GenerateRoute(); this.TypeExpression = new GraphTypeExpression(this.ObjectType.FriendlyName()); // parse all input parameters into the method foreach (var parameter in this.Method.GetParameters()) { var argTemplate = new GraphFieldArgumentTemplate(this, parameter); argTemplate.Parse(); _arguments.Add(argTemplate); } this.MethodSignature = this.GenerateMethodSignatureString(); this.ExpectedReturnType = GraphValidation.EliminateWrappersFromCoreType( this.DeclaredType, false, true, false); }
/// <summary> /// Updates this instance with a piece of data recieved from the completion of a user resolver function. /// This method performs no validation or resolution of the data, it simply assigns it as a value that was recieved /// from a resolver function and attaches it to this instance. /// </summary> /// <param name="data">The data.</param> public virtual void AssignResult(object data) { _childListItems = null; _childFields = null; this.ResultData = data; this.SetStatus(FieldItemResolutionStatus.ResultAssigned); // initialize the children of this instance from the assigned result if (this.IsListField) { if (this.ResultData == null) { return; } if (!GraphValidation.IsValidListType(this.ResultData.GetType())) { return; } if (!(this.ResultData is IEnumerable arraySource)) { return; } _childListItems = new List <GraphDataItem>(); // this instances's type expression is a list (or else we wouldnt be rendering out children) // strip out that outer most list component // to represent what each element of said list should be so that the rule // processor can validate the item as its own entity var childTypeExpression = this.TypeExpression.UnWrapExpression(MetaGraphTypes.IsList); var path = this.Path.Clone(); int index = 0; foreach (var item in arraySource) { var indexedPath = path.Clone(); indexedPath.AddArrayIndex(index++); var childItem = new GraphDataItem(this.FieldContext, item, indexedPath) { TypeExpression = childTypeExpression.Clone(), }; this.AddListItem(childItem); // child items are immediately resolved // using the enumerated source data from the resolver that supplied data // to this parent item. // that is to say that no child resolver is needed to be processed to // retrieve data for each child individually. childItem.AssignResult(item); childItem.SetStatus(this.Status); } } }
/// <summary> /// Updates the field resolver used by this graph field. /// </summary> /// <param name="newResolver">The new resolver this field should use.</param> /// <param name="mode">The new resolution mode used by the runtime to invoke the resolver.</param> public void UpdateResolver(IGraphFieldResolver newResolver, FieldResolutionMode mode) { this.Resolver = newResolver; this.Mode = mode; var unrwrappedType = GraphValidation.EliminateWrappersFromCoreType(this.Resolver?.ObjectType); this.IsLeaf = this.Resolver?.ObjectType != null && GraphQLProviders.ScalarProvider.IsLeaf(unrwrappedType); }
/// <summary> /// When overridden in a child class this method builds out the template according to its own individual requirements. /// </summary> protected override void ParseTemplateDefinition() { base.ParseTemplateDefinition(); this.ExpectedReturnType = GraphValidation.EliminateWrappersFromCoreType( this.DeclaredReturnType, false, true, false); }
/// <summary> /// When overridden in a child class, this metyhod builds the route that will be assigned to this method /// using the implementation rules of the concrete type. /// </summary> /// <returns>GraphRoutePath.</returns> protected override GraphFieldPath GenerateFieldPath() { // an object method cannot contain any route pathing or nesting like controller methods can // before creating hte route, ensure that the declared name, by itself, is valid for graphql var graphName = this.Method.SingleAttributeOrDefault <GraphFieldAttribute>()?.Template?.Trim() ?? Constants.Routing.ACTION_METHOD_META_NAME; graphName = graphName.Replace(Constants.Routing.ACTION_METHOD_META_NAME, this.Method.Name).Trim(); GraphValidation.EnsureGraphNameOrThrow(this.InternalFullName, graphName); return(new GraphFieldPath(GraphFieldPath.Join(this.Parent.Route.Path, graphName))); }
/// <summary> /// Invokes this middleware component allowing it to perform its work against the supplied context. /// </summary> /// <param name="context">The context containing the request passed through the pipeline.</param> /// <param name="next">The delegate pointing to the next piece of middleware to be invoked.</param> /// <param name="cancelToken">The cancel token.</param> /// <returns>Task.</returns> public Task InvokeAsync(GraphFieldExecutionContext context, GraphMiddlewareInvocationDelegate <GraphFieldExecutionContext> next, CancellationToken cancelToken) { // ensure that the data items on teh request match the field they are being executed against var field = context.Field; var expectedFieldGraphType = _schema.KnownTypes.FindGraphType(field); var dataSource = context.Request.DataSource; // ensure the source being supplied // matches the expected source of the field being resolved if (context.InvocationContext.ExpectedSourceType != null) { var expectedSourceType = context.InvocationContext.ExpectedSourceType; var actualSourceType = GraphValidation.EliminateWrappersFromCoreType(dataSource.Value.GetType()); if (expectedSourceType != actualSourceType) { var expectedSourceGraphType = _schema.KnownTypes.FindGraphType(expectedSourceType, TypeKind.OBJECT); var analysis = _schema.KnownTypes.AnalyzeRuntimeConcreteType(expectedSourceGraphType, actualSourceType); if (!analysis.ExactMatchFound) { throw new GraphExecutionException( $"Operation failed. The field execution context for '{field.Route.Path}' was passed " + $"a source item of type '{dataSource.Value.GetType().FriendlyName()}' which could not be coerced " + $"to '{context.InvocationContext.ExpectedSourceType}' as requested by the target graph type '{expectedFieldGraphType.Name}'."); } if (context.Field.Mode == FieldResolutionMode.Batch && !(dataSource.GetType() is IEnumerable)) { throw new GraphExecutionException( $"Operation failed. The field execution context for '{field.Route.Path}' was executed in batch mode " + $"but was not passed an {nameof(IEnumerable)} for its source data."); } } } if (dataSource.Items.Count == 0) { var expected = context.InvocationContext.Field.Mode == FieldResolutionMode.PerSourceItem ? "1" : "at least 1"; throw new GraphExecutionException( $"Operation failed. The field execution context for '{field.Route.Path}' was passed " + $"0 items but expected {expected}. (Field Mode: {field.Mode.ToString()})"); } if (context.InvocationContext.Field.Mode == FieldResolutionMode.PerSourceItem && dataSource.Items.Count != 1) { throw new GraphExecutionException( $"Operation failed. The field execution context for '{field.Route.Path}' was passed " + $"{dataSource.Items.Count} items to resolve but expected 1. (Field Mode: {field.Mode.ToString()})"); } return(next(context, cancelToken)); }
/// <summary> /// Parses the template contents according to the rules of the template. /// </summary> public virtual void Parse() { this.DeclaredArgumentType = this.Parameter.ParameterType; this.ObjectType = GraphValidation.EliminateWrappersFromCoreType(this.Parameter.ParameterType); // set the name _fieldDeclaration = this.Parameter.SingleAttributeOrDefault <FromGraphQLAttribute>(); string name = null; if (_fieldDeclaration != null) { name = _fieldDeclaration?.ArgumentName?.Trim(); } if (string.IsNullOrWhiteSpace(name)) { name = Constants.Routing.PARAMETER_META_NAME; } name = name.Replace(Constants.Routing.PARAMETER_META_NAME, this.Parameter.Name); this.Route = new GraphArgumentFieldPath(this.Parent.Route, name); this.Description = this.Parameter.SingleAttributeOrDefault <DescriptionAttribute>()?.Description?.Trim(); if (this.Parameter.HasDefaultValue && this.Parameter.DefaultValue != null) { // enums will present their default value as a raw int if (this.ObjectType.IsEnum) { this.DefaultValue = Enum.ToObject(this.ObjectType, this.Parameter.DefaultValue); } else { this.DefaultValue = this.Parameter.DefaultValue; } } // set appropriate meta data about this parameter for inclusion in the type system this.TypeExpression = GraphValidation.GenerateTypeExpression(this.DeclaredArgumentType, this); this.TypeExpression = this.TypeExpression.CloneTo(GraphTypeNames.ParseName(this.ObjectType, TypeKind.INPUT_OBJECT)); // when this argument accepts the same data type as the data returned by its owners target source type // i.e. if the source data supplied to the field for resolution is the same as this argument // then assume this argument is to contain the source data // since the source data will be an OBJECT type (not INPUT_OBJECT) there is no way the user could have supplied it if (this.IsSourceDataArgument()) { this.ArgumentModifiers = this.ArgumentModifiers | GraphArgumentModifiers.ParentFieldResult | GraphArgumentModifiers.Internal; } }
public void IsValidGraphType(Type inputType, bool isValidType) { var result = GraphValidation.IsValidGraphType(inputType); Assert.AreEqual(isValidType, result); if (!result) { Assert.Throws <GraphTypeDeclarationException>(() => { GraphValidation.IsValidGraphType(inputType, true); }); } }
/// <summary> /// Determines if the given concrete type is associated to a graph type of the supplied kind. This /// method will perform a <see cref="TypeKind"/> coersion if possible. /// </summary> /// <param name="type">The concrete type to search for.</param> /// <param name="kind">The graph type kind to look for.</param> /// <returns><c>true</c> if a graph type exists, <c>false</c> otherwise.</returns> public bool Contains(Type type, TypeKind?kind = null) { type = GraphQLProviders.ScalarProvider.EnsureBuiltInTypeReference(type); if (_graphTypesByConcreteType.TryGetValue(type, out var typeSet)) { var resolvedKind = kind ?? GraphValidation.ResolveTypeKind(type); return(typeSet.ContainsKey(resolvedKind)); } else { return(false); } }
/// <summary> /// Retrieves the concrete types that this instance may return or make use of in a graph query. /// </summary> /// <returns>IEnumerable<Type>.</returns> public IEnumerable <DependentType> RetrieveRequiredTypes() { if (this.ArgumentModifiers.IsSourceParameter()) { // source parameters should not be injected into the object graph // so they have no dependents return(Enumerable.Empty <DependentType>()); } else { var expectedTypeKind = GraphValidation.ResolveTypeKind(this.ObjectType, TypeKind.INPUT_OBJECT); return(new DependentType(this.ObjectType, expectedTypeKind).AsEnumerable()); } }
public void GenerateTypeExpression( Type type, string expectedExpression, bool definesDefaultValue, GTW[] wrappers) { var mock = new Mock <IGraphTypeExpressionDeclaration>(); mock.Setup(x => x.HasDefaultValue).Returns(definesDefaultValue); mock.Setup(x => x.TypeWrappers).Returns(wrappers); var typeExpression = GraphValidation.GenerateTypeExpression(type, mock.Object); Assert.AreEqual(expectedExpression, typeExpression.ToString()); }
/// <summary> /// Determines whether the given container could be used as a graph field either because it is /// explictly declared as such or that it conformed to the required parameters of being /// a field. /// </summary> /// <param name="memberInfo">The member information to check.</param> /// <returns> /// <c>true</c> if the info represents a possible graph field; otherwise, <c>false</c>.</returns> protected virtual bool CouldBeGraphField(MemberInfo memberInfo) { // always skip those marked as such regardless of anything else if (memberInfo.HasAttribute <GraphSkipAttribute>()) { return(false); } // when the member declares any known attribute in the library include it // and allow it to generate validation failures if its not properly constructed if (memberInfo.SingleAttributeOfTypeOrDefault <GraphFieldAttribute>() != null) { return(true); } switch (memberInfo) { case MethodInfo mi: if (!GraphValidation.IsValidGraphType(mi.ReturnType, false)) { return(false); } if (mi.GetParameters().Any(x => !GraphValidation.IsValidGraphType(x.ParameterType, false))) { return(false); } break; case PropertyInfo pi: if (pi.GetGetMethod() == null) { return(false); } if (pi.GetIndexParameters().Length > 0) { return(false); } if (!GraphValidation.IsValidGraphType(pi.PropertyType, false)) { return(false); } break; } return(true); }
/// <summary> /// Attempts to find the graph type of a given kind for the supplied concrete type. Returns null if no type is found. This /// method will perform a <see cref="TypeKind"/> coersion if possible. /// </summary> /// <param name="type">The concrete type to search with.</param> /// <param name="kind">The kind of graph type to search for. If not supplied the schema will attempt to automatically /// resolve the correct kind from the given <see cref="Type"/>.</param> /// <returns>IGraphType.</returns> public IGraphType FindGraphType(Type type, TypeKind?kind = null) { Validation.ThrowIfNull(type, nameof(type)); type = GraphQLProviders.ScalarProvider.EnsureBuiltInTypeReference(type); var resolvedKind = GraphValidation.ResolveTypeKind(type, kind); if (_graphTypesByConcreteType.TryGetValue(type, out var typeSet)) { if (typeSet.TryGetValue(resolvedKind, out var graphType)) { return(graphType); } } return(null); }
/// <summary> /// When overridden in a child class this method builds out the template according to its own individual requirements. /// </summary> protected override void ParseTemplateDefinition() { base.ParseTemplateDefinition(); // parse all input parameters from the method signature foreach (var parameter in this.Method.GetParameters()) { var argTemplate = this.CreateGraphFieldArgument(parameter); argTemplate.Parse(); _arguments.Add(argTemplate); } this.ExpectedReturnType = GraphValidation.EliminateWrappersFromCoreType( this.DeclaredReturnType, false, true, false); }
/// <summary> /// Parses the provided type, extracting the metadata to used in type generation for the object graph. /// </summary> /// <param name="objectType">The type of the object to parse.</param> /// <param name="kind">The kind of graph type to parse for.</param> /// <returns>IGraphTypeTemplate.</returns> public IGraphTypeTemplate ParseType(Type objectType, TypeKind?kind = null) { Validation.ThrowIfNull(objectType, nameof(objectType)); var typeKind = GraphValidation.ResolveTypeKind(objectType, kind); var typeKey = Tuple.Create(typeKind, objectType); if (_knownObjects.TryGetValue(typeKey, out var template) && this.CacheTemplates) { return(template); } if (GraphQLProviders.ScalarProvider.IsScalar(objectType)) { throw new GraphTypeDeclarationException( $"The type '{objectType.FriendlyName()}' is a known scalar type. Scalars must be explicitly defined and cannot be templated.", objectType); } if (Validation.IsCastable <IGraphUnionProxy>(objectType)) { throw new GraphTypeDeclarationException( $"The union proxy '{objectType.FriendlyName()}' cannot be directly parsed as a graph type. Double check " + "your field attribute declarations'.", objectType); } GraphValidation.IsValidGraphType(objectType, true); template = this.MakeTemplate(objectType, typeKind); template.Parse(); template.ValidateOrThrow(); if (this.CacheTemplates) { _knownObjects.TryAdd(typeKey, template); } return(template); }
/// <summary> /// Validates the completed field context to ensure it is "correct" against the specification before finalizing its reslts. /// </summary> /// <param name="context">The context containing the resolved field.</param> /// <returns><c>true</c> if the node is valid, <c>false</c> otherwise.</returns> public override bool Execute(FieldValidationContext context) { var dataObject = context.ResultData; // did they forget to await a task and accidentally returned Task<T> from their resolver? if (dataObject is Task) { this.ValidationError( context, $"A field resolver for '{context.FieldPath}' yielded an invalid data object. See exception " + "for details. ", new GraphExecutionException( $"The field '{context.FieldPath}' yielded a {nameof(Task)} as its result but expected a value. " + "Did you forget to await an async method?")); context.DataItem.InvalidateResult(); } else { // is the concrete type of the data item not known to the schema? var strippedSourceType = GraphValidation.EliminateWrappersFromCoreType(dataObject.GetType()); var graphType = context.Schema.KnownTypes.FindGraphType(strippedSourceType); if (graphType == null) { this.ValidationError( context, $"A field resolver for '{context.Field.Route.Path}' generated a result " + "object type not known to the target schema. See exception for " + "details", new GraphExecutionException( $"The class '{strippedSourceType.FriendlyName()}' is not not mapped to a graph type " + "on the target schema.")); context.DataItem.InvalidateResult(); } } return(true); }
/// <summary> /// Validates the completed field context to ensure it is "correct" against the specification before finalizing its reslts. /// </summary> /// <param name="context">The context containing the resolved field.</param> /// <returns><c>true</c> if the node is valid, <c>false</c> otherwise.</returns> public override bool Execute(FieldValidationContext context) { var dataObject = context.ResultData; var dataItemTypeExpression = context.DataItem.TypeExpression; // did they forget to await a task and accidentally returned Task<T> from their resolver? // put in a special error message for better feed back if (dataObject is Task) { this.ValidationError( context, $"A field resolver for '{context.FieldPath}' yielded an invalid data object. See exception " + "for details. ", new GraphExecutionException( $"The field '{context.FieldPath}' yielded a {nameof(Task)} as its result but expected a value. " + "Did you forget to await an async method?")); context.DataItem.InvalidateResult(); return(true); } var expectedGraphType = context.Schema?.KnownTypes.FindGraphType(context.Field); if (expectedGraphType == null) { this.ValidationError( context, $"The graph type for field '{context.FieldPath}' ({dataItemTypeExpression?.TypeName}) does not exist on the target schema. The field" + "cannot be properly evaluated."); context.DataItem.InvalidateResult(); return(true); } // virtual graph types aren't real, they can be safely skipped if (expectedGraphType.IsVirtual) { return(true); } var rootSourceType = GraphValidation.EliminateWrappersFromCoreType(dataObject.GetType()); // Check that the actual .NET type of the result data IS (or can be cast to) the expected .NET // type for the graphtype of the field being checked var analysisResult = context.Schema.KnownTypes.AnalyzeRuntimeConcreteType(expectedGraphType, rootSourceType); if (!analysisResult.ExactMatchFound) { string foundTypeNames; if (!analysisResult.FoundTypes.Any()) { foundTypeNames = "~none~"; } else { foundTypeNames = string.Join(", ", analysisResult.FoundTypes.Select(x => x.FriendlyName())); } this.ValidationError( context, $"A field resolver for '{context.Field.Route.Path}' generated a result " + "object type not known to the target schema. See exception for " + "details", new GraphExecutionException( $"For target field of '{context.FieldPath}' (Graph Type: {expectedGraphType.Name}, Kind: {expectedGraphType.Kind}), a supplied object " + $"of class '{rootSourceType.FriendlyName()}' attempted to fill the request but graphql was not able to determine which " + $"of the matched concrete types to use and cannot resolve the field. Matched Types: [{foundTypeNames}]")); context.DataItem.InvalidateResult(); } else { // once the type is confirmed, confirm the actual value // for scenarios such as where a number may be masqurading as an enum but the enum doesn't define // a label for said number. var isValidValue = expectedGraphType.ValidateObject(dataObject); if (!isValidValue) { string actual = string.Empty; if (expectedGraphType is ObjectGraphType) { actual = dataObject.GetType().Name; } else { actual = dataObject.ToString(); } this.ValidationError( context, $"A resolved value for field '{context.FieldPath}' does not match the required graph type. " + $"Expected '{expectedGraphType.Name}' but got '{actual}'."); context.DataItem.InvalidateResult(); } } return(true); }
public override void OnShapesInserted(List <Shape> affectedShapes) { base.OnShapesInserted(affectedShapes); if (affectedShapes.Count == 1 && affectedShapes.First() is Polyline) { var p = affectedShapes.First() as Polyline; var cps = p.GetControlPointIds(ControlPointCapabilities.All); var p1 = p.GetControlPointPosition(cps.First()); var p2 = p.GetControlPointPosition(cps.Last()); Box b1 = null; Box b2 = null; foreach (var s in p.Diagram.Shapes.Except(p)) { if (b1 == null && s.ContainsPoint(p1.X, p1.Y)) { b1 = s as Box; } else if (b2 == null && s.ContainsPoint(p2.X, p2.Y)) { b2 = s as Box; } } if (b1 != null && b2 != null && b1 != b2) { var n1 = DiagramWrapper.GetNodeForShape(b1); var n2 = DiagramWrapper.GetNodeForShape(b2); if (!n2.Quest.IsLinksToEditable()) { DeleteShapeOnDeselect(p); var msg = $"Couldn't create link to {n2.Quest.Name} quest - links to this quest are editable only through code."; MessageBox.Show(msg); return; } var link = new Link(n1, n2); if (Context.Flow.Graph.ExistsLink(link)) { DeleteShapeOnDeselect(p); var msg = $"Couldn't create link from {n1.Quest.Name} to {n2.Quest.Name} quest - link already exists."; MessageBox.Show(msg); return; } (var loopForms, var loop) = GraphValidation.LoopForms(Context.Flow.Graph, link); if (loopForms) { DeleteShapeOnDeselect(p); var loopStringRepresentation = string.Join(" - ", loop.Select(q => q.Quest.Name)); var msg = $"Couldn't create link from {n1.Quest.Name} to {n2.Quest.Name} quest - loop forms: {loopStringRepresentation}."; MessageBox.Show(msg); return; } DiagramWrapper.RegisterShapeForLink(p, link); if (!n1.Quest.IsActive() || !n2.Quest.IsActive()) { var command = new CompositeCommand(); void InitQuestActivationCommand(Quest quest) => command.AddCommand( CommandsCreation.ActivateQuest( quest, Context.Flow.GetSectorForQuest(quest), Context, DiagramWrapper ) ); if (!n1.Quest.IsActive()) { InitQuestActivationCommand(n1.Quest); } if (!n2.Quest.IsActive()) { InitQuestActivationCommand(n2.Quest); } command.AddCommand(CommandsCreation.AddLink(link, Context, DiagramWrapper)); Context.History.Do(command); } else { Context.History.Do(CommandsCreation.AddLink(link, Context, DiagramWrapper)); } } else { DeleteShapeOnDeselect(p); } } else { DeleteShapesOnDeselect(affectedShapes); } }
/// <summary> /// When overridden in a child class, allows the template to perform some final validation checks /// on the integrity of itself. An exception should be thrown to stop the template from being /// persisted if the object is unusable or otherwise invalid in the manner its been built. /// </summary> public void ValidateOrThrow() { GraphValidation.EnsureGraphNameOrThrow($"{_parentEnum.Name}", this.Name); }
/// <summary> /// Creates a <see cref="IGraphType" /> representing the union this field should masquerade as in the object graph. /// Returns null if no union is declared for this field. /// </summary> /// <returns>IGraphType.</returns> public virtual IEnumerable <DependentType> RetrieveRequiredTypes() { // a base field knows, at most, about the return object type it is dependent on return(this.PossibleTypes?.Select(x => new DependentType(x, GraphValidation.ResolveTypeKind(x, this.Kind))) ?? Enumerable.Empty <DependentType>()); }
/// <summary> /// When overridden in a child class, allows the template to perform some final validation checks /// on the integrity of itself. An exception should be thrown to stop the template from being /// persisted if the object is unusable or otherwise invalid in the manner its been built. /// </summary> public override void ValidateOrThrow() { base.ValidateOrThrow(); if (this.DeclaredReturnType == typeof(void)) { throw new GraphTypeDeclarationException($"The graph field '{this.InternalFullName}' has a void return. All graph fields must return something."); } if (this.IsAsyncField) { // account for a mistake by the developer in using a potential return type of just "Task" instead of Task<T> var genericArgs = this.DeclaredReturnType.GetGenericArguments(); if (genericArgs.Length != 1) { throw new GraphTypeDeclarationException( $"The field '{this.InternalFullName}' defines a return type of'{typeof(Task).Name}' but " + "defines no contained return type for the resultant model object yielding a void return after " + "completion of the task. All graph methods must return a single model object. Consider using " + $"'{typeof(Task<>).Name}' instead for asyncronous methods"); } } if (this.UnionProxy != null) { GraphValidation.EnsureGraphNameOrThrow($"{this.InternalFullName}[{nameof(GraphFieldAttribute)}][{nameof(IGraphUnionProxy)}]", this.UnionProxy.Name); if (this.UnionProxy.Types.Count < 1) { throw new GraphTypeDeclarationException( $"The field '{this.InternalFullName}' declares union type of '{this.UnionProxy.Name}' " + $"but that type includes 0 possible types in the union. Unions require 1 or more possible types. Add additional types" + "or remove the union."); } } // ensure the object type returned by the graph field is set correctly bool returnsActionResult = Validation.IsCastable <IGraphActionResult>(this.ObjectType); var enforceUnionRules = this.UnionProxy != null; if (this.PossibleTypes.Count == 0) { throw new GraphTypeDeclarationException( $"The field '{this.InternalFullName}' declared no possible return types either on its attribute declarations or as the " + "declared return type for the field. GraphQL requires the type information be known " + $"to setup the schema and client tooling properly. If this field returns a '{nameof(IGraphActionResult)}' you must " + "provide a graph field declaration attribute and add at least one type; be that a concrete type, an interface or a union."); } // validate each type in the list for "correctness" // Possible Types must conform to the rules of those required by sub type declarations of unions and interfaces // interfaces: https://graphql.github.io/graphql-spec/June2018/#sec-Interfaces // unions: https://graphql.github.io/graphql-spec/June2018/#sec-Unions foreach (var type in this.PossibleTypes) { if (enforceUnionRules) { if (GraphQLProviders.ScalarProvider.IsScalar(type)) { throw new GraphTypeDeclarationException( $"The field '{this.InternalFullName}' declares union with a possible type of '{type.FriendlyName()}' " + $"but that type is a scalar. Scalars cannot be included in a field's possible type collection, only object types can."); } if (type.IsInterface) { throw new GraphTypeDeclarationException( $"The field '{this.InternalFullName}' declares union with a possible type of '{type.FriendlyName()}' " + $"but that type is an interface. Interfaces cannot be included in a field's possible type collection, only object types can."); } if (type.IsEnum) { throw new GraphTypeDeclarationException( $"The field '{this.InternalFullName}' declares a union with a possible type of '{type.FriendlyName()}' " + "but that type is an enum. Only concrete, non-abstract classes may be used. Value types, such as structs or enumerations, are not allowed."); } if (!type.IsClass) { throw new GraphTypeDeclarationException( $"The field '{this.InternalFullName}' returns an interface named '{this.ObjectType.FriendlyName()}' and declares a possible type of '{type.FriendlyName()}' " + "but that type is not a valid class. Only concrete, non-abstract classes may be used. Value types, such as structs or enumerations, are also not allowed."); } } foreach (var invalidFieldType in Constants.InvalidFieldTemplateTypes) { if (Validation.IsCastable(type, invalidFieldType)) { throw new GraphTypeDeclarationException( $"The field '{this.InternalFullName}' declares a possible return type of '{type.FriendlyName()}' " + $"but that type inherits from '{invalidFieldType.FriendlyName()}' which is a reserved type declared by the graphql-aspnet library. This type cannot cannot be returned by a graphql field."); } } // to ensure an object isn't arbitrarly returned as null and lost // ensure that the any possible type returned from this field is returnable AS the type this field declares // as its return type. In doing this we know that, potentially, an object returned by this // field "could" cast to the return type and allow field execution to continue. // // This is a helpful developer safety check, not a complete guarantee as concrete types for interface // declarations are not required at this stage // // batch processed fields are not subject to this restriction if (!returnsActionResult && this.Mode == FieldResolutionMode.PerSourceItem && !Validation.IsCastable(type, this.ObjectType)) { throw new GraphTypeDeclarationException( $"The field '{this.InternalFullName}' returns '{this.ObjectType.FriendlyName()}' and declares a possible type of '{type.FriendlyName()}' " + $"but that type is not castable to '{this.ObjectType.FriendlyName()}' and therefore not returnable by this field. Due to the strongly-typed nature of C# any possible type on a field " + "must be castable to the type of the field in order to ensure its not inadvertantly nulled out during processing. If this field returns a union " + $"of multiple, disperate types consider returning '{typeof(object).Name}' from the field to ensure each possible return type can be successfully processed."); } } foreach (var argument in this.Arguments) { argument.ValidateOrThrow(); } if (this.Complexity.HasValue && this.Complexity < 0) { throw new GraphTypeDeclarationException( $"The field '{this.InternalFullName}' declares a complexity value of " + $"`{this.Complexity.Value}`. The complexity factor must be greater than or equal to 0."); } this.ValidateBatchMethodSignatureOrThrow(); }
/// <summary> /// Adds a new scalar type to the global collection of recognized scalar values. These graph types are used as /// singleton instances across all schema's and all querys. /// </summary> /// <param name="graphType">The scalar type to register.</param> public void RegisterCustomScalar(IScalarGraphType graphType) { Validation.ThrowIfNull(graphType, nameof(graphType)); if (string.IsNullOrWhiteSpace(graphType.Name)) { throw new GraphTypeDeclarationException( "The scalar must supply a name that is not null or whitespace."); } if (!GraphValidation.IsValidGraphName(graphType.Name)) { throw new GraphTypeDeclarationException( $"The scalar must supply a name that that conforms to the standard rules for GraphQL. (Regex: {Constants.RegExPatterns.NameRegex})"); } if (graphType.Kind != TypeKind.SCALAR) { throw new GraphTypeDeclarationException( $"The scalar's type kind must be set to '{nameof(TypeKind.SCALAR)}'."); } if (graphType.ObjectType == null) { throw new GraphTypeDeclarationException( $"The scalar must supply a value for '{nameof(graphType.ObjectType)}', is cannot be null."); } if (graphType.SourceResolver == null) { throw new GraphTypeDeclarationException( $"The scalar must supply a value for '{nameof(graphType.SourceResolver)}' that can convert data from a " + $"query into the primary object type of '{graphType.ObjectType.FriendlyName()}'."); } if (graphType.ValueType == ScalarValueType.Unknown) { throw new GraphTypeDeclarationException( $"The scalar must supply a value for '{nameof(graphType.ValueType)}'. This lets the validation engine " + "know what data types submitted on a user query could be parsed into a value for this scale."); } if (graphType.OtherKnownTypes == null) { throw new GraphTypeDeclarationException( $"Custom scalars must supply a value for '{nameof(graphType.OtherKnownTypes)}', it cannot be null. " + "Use an empty list if there are no other known types."); } var isAScalarAlready = this.IsScalar(graphType.Name); if (isAScalarAlready) { throw new GraphTypeDeclarationException( $"A scalar named '{graphType.Name}' already exists in this graphql instance."); } isAScalarAlready = this.IsScalar(graphType.ObjectType); if (isAScalarAlready) { var scalar = this.RetrieveScalar(graphType.ObjectType); throw new GraphTypeDeclarationException( $"The scalar's primary object type of '{graphType.ObjectType.FriendlyName()}' is " + $"already reserved by the scalar '{scalar.Name}'. Scalar object types must be unique."); } foreach (var type in graphType.OtherKnownTypes) { isAScalarAlready = this.IsScalar(type); if (isAScalarAlready) { var scalar = this.RetrieveScalar(type); throw new GraphTypeDeclarationException( $"The scalar's other known type of '{type.FriendlyName()}' is " + $"already reserved by the scalar '{scalar.Name}'. Scalar object types must be unique."); } } this.AddScalar(graphType); }
/// <summary> /// When overridden in a child class this method builds out the template according to its own individual requirements. /// </summary> protected override void ParseTemplateDefinition() { _fieldDeclaration = this.SingleAttributeOfTypeOrDefault <GraphFieldAttribute>(); // ------------------------------------ // Common Metadata // ------------------------------------ this.Route = this.GenerateFieldPath(); this.Mode = _fieldDeclaration?.ExecutionMode ?? FieldResolutionMode.PerSourceItem; this.Complexity = _fieldDeclaration?.Complexity; this.Description = this.SingleAttributeOfTypeOrDefault <DescriptionAttribute>()?.Description; var depreciated = this.SingleAttributeOfTypeOrDefault <DeprecatedAttribute>(); if (depreciated != null) { this.IsDeprecated = true; this.DeprecationReason = depreciated.Reason?.Trim(); } var objectType = GraphValidation.EliminateWrappersFromCoreType(this.DeclaredReturnType); var typeExpression = GraphValidation.GenerateTypeExpression(this.DeclaredReturnType, this); typeExpression = typeExpression.CloneTo(GraphTypeNames.ParseName(objectType, this.Parent.Kind)); // ------------------------------------ // Gather Possible Types and/or union definition // ------------------------------------ this.BuildUnionProxyInstance(); if (this.UnionProxy != null) { this.PossibleTypes = new List <Type>(this.UnionProxy.Types); } else { this.PossibleTypes = new List <Type>(); // the possible types attribte is optional but expects taht the concrete types are added // to the schema else where lest a runtime exception occurs of a missing graph type. var typesAttrib = this.SingleAttributeOfTypeOrDefault <PossibleTypesAttribute>(); if (typesAttrib != null) { foreach (var type in typesAttrib.PossibleTypes) { this.PossibleTypes.Add(type); } } // add any types decalred on the primary field declaration if (_fieldDeclaration != null) { foreach (var type in _fieldDeclaration.Types) { var strippedType = GraphValidation.EliminateWrappersFromCoreType(type); if (strippedType != null) { this.PossibleTypes.Add(strippedType); } } } if (objectType != null && !Validation.IsCastable <IGraphActionResult>(objectType) && GraphValidation.IsValidGraphType(objectType)) { this.PossibleTypes.Insert(0, objectType); } } this.PossibleTypes = this.PossibleTypes.Distinct().ToList(); // ------------------------------------ // Adjust the action result to the actual return type this field returns // ------------------------------------ if (Validation.IsCastable <IGraphActionResult>(objectType)) { if (this.UnionProxy != null) { // if a union was decalred preserve whatever modifer elements // were decalred but alter the return type to object (a known common element among all members of the union) objectType = typeof(object); } else if (_fieldDeclaration != null && _fieldDeclaration.Types.Count > 0) { objectType = _fieldDeclaration.Types[0]; typeExpression = GraphValidation.GenerateTypeExpression(objectType, this) .CloneTo(GraphTypeNames.ParseName(objectType, this.Parent.Kind)); objectType = GraphValidation.EliminateWrappersFromCoreType(objectType); } else if (this.PossibleTypes.Count > 0) { objectType = this.PossibleTypes[0]; typeExpression = GraphValidation.GenerateTypeExpression(objectType, this) .CloneTo(GraphTypeNames.ParseName(objectType, this.Parent.Kind)); objectType = GraphValidation.EliminateWrappersFromCoreType(objectType); } else { objectType = typeof(object); } } this.ObjectType = objectType; if (this.UnionProxy != null) { this.TypeExpression = typeExpression.CloneTo(this.UnionProxy.Name); } else { this.TypeExpression = typeExpression; } // ------------------------------------ // Async Requirements // ------------------------------------ this.IsAsyncField = Validation.IsCastable <Task>(this.DeclaredReturnType); // ------------------------------------ // Security Policies // ------------------------------------ _securityPolicies = FieldSecurityGroup.FromAttributeCollection(this.AttributeProvider); }
/// <summary> /// Validates the completed field context to ensure it is "correct" against the specification before finalizing its reslts. /// </summary> /// <param name="context">The context containing the resolved field.</param> /// <returns><c>true</c> if the node is valid, <c>false</c> otherwise.</returns> public override bool Execute(FieldValidationContext context) { var typeExpression = context.TypeExpression; if (typeExpression.IsRequired && context.ResultData == null) { // 6.4.3 section 1c this.ValidationError( context, $"Field '{context.FieldPath}' expected a non-null result but received {{null}}."); context.DataItem.InvalidateResult(); } // 6.4.3 section 2 if (context.ResultData == null) { return(true); } // 6.4.3 section 3, ensure an IEnumerable for a type expression that is a list if (typeExpression.IsListOfItems && !GraphValidation.IsValidListType(context.ResultData.GetType())) { this.ValidationError( context, $"Field '{context.FieldPath}' was expected to return a list of items but instead returned a single item."); context.DataItem.InvalidateResult(); return(true); } var graphType = context.Schema?.KnownTypes.FindGraphType(typeExpression?.TypeName); if (graphType == null) { this.ValidationError( context, $"The graph type for field '{context.FieldPath}' ({typeExpression?.TypeName}) does not exist on the target schema. The field" + $"cannot be properly evaluated."); context.DataItem.InvalidateResult(); } else if (!typeExpression.Matches(context.ResultData, graphType.ValidateObject)) { // generate a valid, properly cased type expression reference for the data that was provided var actualExpression = GraphValidation.GenerateTypeExpression(context.ResultData.GetType()); var coreType = GraphValidation.EliminateWrappersFromCoreType(context.ResultData.GetType()); var actualType = context.Schema.KnownTypes.FindGraphType(coreType); if (actualType != null) { actualExpression = actualExpression.CloneTo(actualType.Name); } // 6.4.3 section 4 & 5 this.ValidationError( context, $"The resolved value for field '{context.FieldPath}' does not match the required type expression. " + $"Expected {typeExpression} but got {actualExpression}."); context.DataItem.InvalidateResult(); } return(true); }
public void IsValidListType(Type inputType, bool isValidListType) { var result = GraphValidation.IsValidListType(inputType); Assert.AreEqual(isValidListType, result); }