/// <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> /// Assigns the results of resolving the field to the items on the execution context. /// </summary> /// <param name="executionContext">The execution context.</param> /// <param name="resolutionContext">The resolution context.</param> private void AssignResults(GraphFieldExecutionContext executionContext, FieldResolutionContext resolutionContext) { // transfer the result to the execution context // then deteremine what (if any) data items can be updated from its value executionContext.Result = resolutionContext.Result; if (executionContext.Field.Mode == FieldResolutionMode.PerSourceItem) { if (executionContext.Request.DataSource.Items.Count == 1) { var item = executionContext.Request.DataSource.Items[0]; executionContext.ResolvedSourceItems.Add(item); item.AssignResult(resolutionContext.Result); return; } throw new GraphExecutionException( $"When attempting to resolve the field '{executionContext.Field.Route.Path}' an unexpected error occured and the request was teriminated.", executionContext.Request.Origin, new InvalidOperationException( $"The field '{executionContext.Field.Route.Parent}' has a resolution mode of '{nameof(FieldResolutionMode.PerSourceItem)}' " + $"but the execution context contains {executionContext.Request.DataSource.Items.Count} source items. The runtime is unable to determine which " + "item to assign the resultant value to.")); } else if (executionContext.Field.Mode == FieldResolutionMode.Batch) { var batchProcessor = new BatchResultProcessor( executionContext.Field, executionContext.Request.DataSource.Items, executionContext.Request.Origin); var itemsWithAssignedData = batchProcessor.Resolve(executionContext.Result); executionContext.ResolvedSourceItems.AddRange(itemsWithAssignedData); executionContext.Messages.AddRange(batchProcessor.Messages); return; } throw new ArgumentOutOfRangeException( nameof(executionContext.Field.Mode), $"The execution mode for field '{executionContext.Field.Route.Path}' cannot be resolved " + $"by {nameof(InvokeFieldResolverMiddleware<TSchema>)}. (Mode: {executionContext.Field.Mode.ToString()})"); }
public void IsBatchDictionary(Type typeToTest, Type typeOfKey, Type typeOfResult, bool expectedResult) { var isValid = BatchResultProcessor.IsBatchDictionaryType(typeToTest, typeOfKey, typeOfResult); Assert.AreEqual(expectedResult, isValid); }