From 741a70b18208f6a2fc759a4a89909f4ce2c536fd Mon Sep 17 00:00:00 2001 From: Ben McCallum Date: Thu, 21 Dec 2023 03:45:28 +1000 Subject: [PATCH] More work on mutation conventions support --- README.md | 21 +- src/Directory.Build.props | 2 +- src/FairyBread/DefaultFairyBreadOptions.cs | 4 +- src/FairyBread/DefaultValidationError.cs | 20 ++ .../DefaultValidationErrorsHandler.cs | 7 +- src/FairyBread/DefaultValidationException.cs | 47 ---- .../GlobalValidationErrorsAttribute.cs | 56 ++++ src/FairyBread/IFairyBreadOptions.cs | 2 +- src/FairyBread/ValidationErrorConfigurer.cs | 42 +++ src/FairyBread/ValidationMiddlewareHelpers.cs | 221 +++++++++++++++ .../ValidationMiddlewareInjector.cs | 251 +----------------- src/FairyBread/WellKnownContextData.cs | 7 +- 12 files changed, 372 insertions(+), 308 deletions(-) create mode 100644 src/FairyBread/DefaultValidationError.cs delete mode 100644 src/FairyBread/DefaultValidationException.cs create mode 100644 src/FairyBread/GlobalValidationErrorsAttribute.cs create mode 100644 src/FairyBread/ValidationErrorConfigurer.cs create mode 100644 src/FairyBread/ValidationMiddlewareHelpers.cs diff --git a/README.md b/README.md index 12835b4..a3a6b19 100644 --- a/README.md +++ b/README.md @@ -48,8 +48,23 @@ public Task CreateUser(CreateUserInput userInput) { ... } ### How validation errors will be handled -By default, errors will be written out into the GraphQL execution result in the `Errors` property with one error being reported per failure on a field. -You can change this behaviour by implementing your own `IValidationErrorsHandler`. +Validation errors can be reported by FairyBread in one of two fashions, depending on the field in question: +1. Globally, under the GraphQL response's `errors` array. +2. Inline, under the mutation field's `errors` array as per [mutation conventions](). + +Currently, standard fields (i.e. not a mutation field), will always report errors globally. +This may be changed in the future as best practices and upstream support comes through. + +Mutation fields on the other hand, when using a field is using Hot Chocolate's mutation convention +(and FairyBread's UseMutationConvention setting is on, as is the default), +the validation errors will be written out under the convention-based `errors` array field +under the mutation field. + +Note: To assist in migration from the old behavior to the new one, you can use `[GlobalValidationErrors]` or +`.GlobalValidationErrors()` (fluent-style) on mutation fields to opt back into global errors field-by-field. + +In both cases, one error will be reported per failure on a field. +You can change this behavior by implementing your own `IValidationErrorsHandler`. ### Implicit vs explicit configuration @@ -117,6 +132,8 @@ services.AddFairyBread(options => { options.ShouldValidateArgument = (objTypeDef, fieldTypeDef, argTypeDef) => ...; options.ThrowIfNoValidatorsFound = true/false; + options.UseMutationConventions = true/false; + options.ValidationErrorType = typeof(...); }); ``` diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 940f250..27ee3c7 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -21,7 +21,7 @@ 10.0.0 10.0.0 - 13.6.0-preview.31 + 13.6.0 net7.0; net6.0; netstandard2.0 net7.0; net6.0 diff --git a/src/FairyBread/DefaultFairyBreadOptions.cs b/src/FairyBread/DefaultFairyBreadOptions.cs index 2a52d35..c308a5f 100644 --- a/src/FairyBread/DefaultFairyBreadOptions.cs +++ b/src/FairyBread/DefaultFairyBreadOptions.cs @@ -15,6 +15,6 @@ public class DefaultFairyBreadOptions : IFairyBreadOptions = true; /// - public virtual Type ValidationExceptionType { get; set; } - = typeof(DefaultValidationException); + public virtual Type ValidationErrorType { get; set; } + = typeof(DefaultValidationError); } diff --git a/src/FairyBread/DefaultValidationError.cs b/src/FairyBread/DefaultValidationError.cs new file mode 100644 index 0000000..5880543 --- /dev/null +++ b/src/FairyBread/DefaultValidationError.cs @@ -0,0 +1,20 @@ +namespace FairyBread; + +[GraphQLName("ValidationError")] +public class DefaultValidationError +{ + private DefaultValidationError( + string message) + { + Message = "A validation error occurred"; + // TODO: Complete a nice rich implementation. + } + + public static DefaultValidationError CreateErrorFrom( + ArgumentValidationResult argValRes) + { + return new(""); + } + + public string Message { get; } +} diff --git a/src/FairyBread/DefaultValidationErrorsHandler.cs b/src/FairyBread/DefaultValidationErrorsHandler.cs index 8ed4aae..a7f63ef 100644 --- a/src/FairyBread/DefaultValidationErrorsHandler.cs +++ b/src/FairyBread/DefaultValidationErrorsHandler.cs @@ -6,10 +6,9 @@ public virtual void Handle( IMiddlewareContext context, IEnumerable invalidResults) { - if (context.Selection.Field.ContextData.TryGetValue(WellKnownContextData.UsesMutationConvention, out var usesMutationConventionObj) && - usesMutationConventionObj is bool usesMutationConvention && - usesMutationConvention == true) - // TODO: And options are configured to write this into conventions now + if (context.Selection.Field.ContextData.TryGetValue(WellKnownContextData.UsesInlineErrors, out var usesInlineErrorsObj) && + usesInlineErrorsObj is bool usesInlineErrors && + usesInlineErrors == true) { context.Result = new MutationError( invalidResults diff --git a/src/FairyBread/DefaultValidationException.cs b/src/FairyBread/DefaultValidationException.cs deleted file mode 100644 index 5266659..0000000 --- a/src/FairyBread/DefaultValidationException.cs +++ /dev/null @@ -1,47 +0,0 @@ -namespace FairyBread; - -[GraphQLName("ValidationError")] -public class DefaultValidationError -{ - private DefaultValidationError( - string message) - { - Message = "A validation error occurred"; - // TODO: Complete a nice rich implementation. - } - - private DefaultValidationError( - DefaultValidationException exception) - { - Message = "A validation error occurred"; - // TODO: Complete a nice rich implementation. - } - - public static DefaultValidationError CreateErrorFrom( - DefaultValidationException ex) - { - return new(ex); - } - - public static DefaultValidationError CreateErrorFrom( - ArgumentValidationResult argValRes) - { - return new(""); - } - - public string Message { get; } -} - -public class DefaultValidationException : Exception -{ - public DefaultValidationException( - IMiddlewareContext context, - IEnumerable invalidResults) - { - Context = context; - InvalidResults = invalidResults; - } - - public IMiddlewareContext Context { get; } - public IEnumerable InvalidResults { get; } -} diff --git a/src/FairyBread/GlobalValidationErrorsAttribute.cs b/src/FairyBread/GlobalValidationErrorsAttribute.cs new file mode 100644 index 0000000..e2e65dc --- /dev/null +++ b/src/FairyBread/GlobalValidationErrorsAttribute.cs @@ -0,0 +1,56 @@ + +namespace FairyBread; + +/// +/// +/// Instructs FairyBread, when using HotChocolate's mutation conventions +/// and with FairyBread configured with UseMutationConventions as true, +/// to report validation errors in the global errors array of the GraphQL +/// response (rather than inline in this mutation fields' local errors array). +/// +/// +/// This attribute is intended to allow a progressive transition from FairyBread's +/// former behavior with mutation conventions (global errors) to the +/// new behavior (local errors). Fields that you aren't ready to modernize can +/// be annotated in the meantime. +/// +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +public class GlobalValidationErrorsAttribute : ObjectFieldDescriptorAttribute +{ + protected override void OnConfigure( + IDescriptorContext context, + IObjectFieldDescriptor descriptor, + MemberInfo memberInfo) + { + descriptor.GlobalValidationErrors(); + } +} + +public static class GlobalValidationErrorsObjectFieldDescriptorExtensions +{ + /// + /// + /// Instructs FairyBread, when using HotChocolate's mutation conventions + /// and with FairyBread configured with UseMutationConventions as true, + /// to report validation errors in the global errors array of the GraphQL + /// response (rather than inline in this mutation fields' local errors array). + /// + /// + /// This is intended to allow a progressive transition from FairyBread's + /// former behavior with mutation conventions (global errors) to the + /// new behavior (local errors). Fields that you aren't ready to modernize can + /// use this in the meantime. + /// + /// + public static IObjectFieldDescriptor GlobalValidationErrors( + this IObjectFieldDescriptor descriptor) + { + descriptor.Extend().OnBeforeNaming((completionContext, objFieldDef) => + { + objFieldDef.ContextData[WellKnownContextData.UsesGlobalErrors] = true; + }); + + return descriptor; + } +} diff --git a/src/FairyBread/IFairyBreadOptions.cs b/src/FairyBread/IFairyBreadOptions.cs index 0d333fd..2fe7ec3 100644 --- a/src/FairyBread/IFairyBreadOptions.cs +++ b/src/FairyBread/IFairyBreadOptions.cs @@ -18,5 +18,5 @@ public interface IFairyBreadOptions bool UseMutationConventions { get; set; } - Type ValidationExceptionType { get; set; } + Type ValidationErrorType { get; set; } } diff --git a/src/FairyBread/ValidationErrorConfigurer.cs b/src/FairyBread/ValidationErrorConfigurer.cs new file mode 100644 index 0000000..184e4a9 --- /dev/null +++ b/src/FairyBread/ValidationErrorConfigurer.cs @@ -0,0 +1,42 @@ +using HotChocolate.Types; + +namespace FairyBread; + +internal class ValidationErrorConfigurer : MutationErrorConfiguration +{ + public override void OnConfigure( + IDescriptorContext context, + ObjectFieldDefinition fieldDef) + { + var options = context.Services + .GetRequiredService(); + if (!options.UseMutationConventions) + { + return; + } + + var validatorRegistry = context.Services + .GetRequiredService(); + + // TODO: Can I get this? I think I need to PR this as Michael said he'd pass this through but hasn't. + var objTypeDef = new ObjectTypeDefinition(); + + if (ValidationMiddlewareHelpers.NeedsMiddleware(options, validatorRegistry, objTypeDef, fieldDef)) + { + ValidationMiddlewareHelpers.AddMiddleware(fieldDef); + + if (!fieldDef.ContextData.TryGetValue(WellKnownContextData.UsesGlobalErrors, out var usesGlobalErrorsObj) || + usesGlobalErrorsObj is not bool usesGlobalErrors || + usesGlobalErrors == false) + { + fieldDef.AddErrorType(context, options.ValidationErrorType); + + // Set a flag for the errors handler + fieldDef.ContextData[WellKnownContextData.UsesInlineErrors] = true; + + // Cleanup + //fieldDef.ContextData.Remove(WellKnownContextData.UsesGlobalErrors); + } + } + } +} diff --git a/src/FairyBread/ValidationMiddlewareHelpers.cs b/src/FairyBread/ValidationMiddlewareHelpers.cs new file mode 100644 index 0000000..f545a06 --- /dev/null +++ b/src/FairyBread/ValidationMiddlewareHelpers.cs @@ -0,0 +1,221 @@ +namespace FairyBread; + +internal static class ValidationMiddlewareHelpers +{ + private const string _middlewareKey = "FairyBread.ValidationMiddleware"; + internal static Lazy _validationFieldMiddlewareDef = new(() => + new FieldMiddlewareDefinition( + FieldClassMiddlewareFactory.Create(), + key: _middlewareKey)); + + internal static bool NeedsMiddleware( + IFairyBreadOptions options, + IValidatorRegistry validatorRegistry, + ObjectTypeDefinition objTypeDef, + ObjectFieldDefinition fieldDef) + { + // Don't add validation middleware unless: + // 1. we have args + var needsValidationMiddleware = false; + + foreach (var argDef in fieldDef.Arguments) + { + // 2. the argument should be validated according to options func + if (!options.ShouldValidateArgument(objTypeDef, fieldDef, argDef)) + { + continue; + } + + // 3. there's validators for it + List validatorDescs; + try + { + validatorDescs = DetermineValidatorsForArg(validatorRegistry, argDef); + if (validatorDescs.Count < 1) + { + continue; + } + } + catch (Exception ex) + { + throw new Exception( + $"Problem getting runtime type for argument '{argDef.Name}' " + + $"in field '{fieldDef.Name}' on object type '{objTypeDef.Name}'.", + ex); + } + + // Cleanup context now we're done with these + foreach (var key in argDef.ContextData.Keys) + { + if (key.StartsWith(WellKnownContextData.Prefix)) + { + argDef.ContextData.Remove(key); + } + } + + validatorDescs.TrimExcess(); + needsValidationMiddleware = true; + argDef.ContextData[WellKnownContextData.ValidatorDescriptors] = validatorDescs.AsReadOnly(); + } + + return needsValidationMiddleware; + } + + internal static void AddMiddleware(ObjectFieldDefinition fieldDef) + { + // TODO: Insert if not exists + fieldDef.MiddlewareDefinitions.Insert(0, _validationFieldMiddlewareDef.Value); + } + + private static List DetermineValidatorsForArg( + IValidatorRegistry validatorRegistry, + ArgumentDefinition argDef) + { + // If validation is explicitly disabled, return none so validation middleware won't be added + if (argDef.ContextData.ContainsKey(WellKnownContextData.DontValidate)) + { + return new List(0); + } + + var validators = new List(); + + // Include implicit validator/s first (if allowed) + if (!argDef.ContextData.ContainsKey(WellKnownContextData.DontValidateImplicitly)) + { + // And if we can figure out the arg's runtime type + var argRuntimeType = TryGetArgRuntimeType(argDef); + if (argRuntimeType is not null) + { + if (validatorRegistry.Cache.TryGetValue(argRuntimeType, out var implicitValidators) && + implicitValidators is not null) + { + validators.AddRange(implicitValidators); + } + } + } + + // Include explicit validator/s (that aren't already added implicitly) + if (argDef.ContextData.TryGetValue(WellKnownContextData.ExplicitValidatorTypes, out var explicitValidatorTypesRaw) && + explicitValidatorTypesRaw is IEnumerable explicitValidatorTypes) + { + // TODO: Potentially check and throw if there's a validator being explicitly applied for the wrong runtime type + + foreach (var validatorType in explicitValidatorTypes) + { + if (validators.Any(v => v.ValidatorType == validatorType)) + { + continue; + } + + validators.Add(new ValidatorDescriptor(validatorType)); + } + } + + return validators; + } + + private static Type? TryGetArgRuntimeType( + ArgumentDefinition argDef) + { + if (argDef.Parameter?.ParameterType is { } argRuntimeType) + { + return argRuntimeType; + } + + if (argDef.Type is ExtendedTypeReference extTypeRef) + { + return TryGetRuntimeType(extTypeRef.Type); + } + + return null; + } + + private static Type? TryGetRuntimeType(IExtendedType extType) + { + // It's already a runtime type, .Type(typeof(int)) + if (extType.Kind == ExtendedTypeKind.Runtime) + { + return extType.Source; + } + + // Array (though not sure what produces this scenario as seems to always be list) + if (extType.IsArray) + { + if (extType.ElementType is null) + { + return null; + } + + var elementRuntimeType = TryGetRuntimeType(extType.ElementType); + if (elementRuntimeType is null) + { + return null; + } + + return Array.CreateInstance(elementRuntimeType, 0).GetType(); + } + + // List + if (extType.IsList) + { + if (extType.ElementType is null) + { + return null; + } + + var elementRuntimeType = TryGetRuntimeType(extType.ElementType); + if (elementRuntimeType is null) + { + return null; + } + + return typeof(List<>).MakeGenericType(elementRuntimeType); + } + + // Input object + if (typeof(InputObjectType).IsAssignableFrom(extType)) + { + var currBaseType = extType.Type.BaseType; + while (currBaseType is not null && + (!currBaseType.IsGenericType || + currBaseType.GetGenericTypeDefinition() != typeof(InputObjectType<>))) + { + currBaseType = currBaseType.BaseType; + } + + if (currBaseType is null) + { + return null; + } + + return currBaseType!.GenericTypeArguments[0]; + } + + // Singular scalar + if (typeof(ScalarType).IsAssignableFrom(extType)) + { + var currBaseType = extType.Type.BaseType; + while (currBaseType is not null && + (!currBaseType.IsGenericType || + currBaseType.GetGenericTypeDefinition() != typeof(ScalarType<>))) + { + currBaseType = currBaseType.BaseType; + } + + if (currBaseType is null) + { + return null; + } + + var argRuntimeType = currBaseType.GenericTypeArguments[0]; + if (argRuntimeType.IsValueType && extType.IsNullable) + { + return typeof(Nullable<>).MakeGenericType(argRuntimeType); + } + + return argRuntimeType; + } + + return null; + } +} diff --git a/src/FairyBread/ValidationMiddlewareInjector.cs b/src/FairyBread/ValidationMiddlewareInjector.cs index 80f2907..24eaa92 100644 --- a/src/FairyBread/ValidationMiddlewareInjector.cs +++ b/src/FairyBread/ValidationMiddlewareInjector.cs @@ -1,33 +1,5 @@ namespace FairyBread; -internal class ValidationErrorConfigurer : MutationErrorConfiguration -{ - public override void OnConfigure(IDescriptorContext context, ObjectFieldDefinition fieldDef) - { - var options = context.Services - .GetRequiredService(); - if (!options.UseMutationConventions) - { - return; - } - - var validatorRegistry = context.Services - .GetRequiredService(); - - var objTypeDef = new ObjectTypeDefinition(); // TODO: Can I get this? - - if (Helpers.NeedsMiddleware(options, validatorRegistry, objTypeDef, fieldDef)) - { - fieldDef.AddErrorType(context, typeof(DefaultValidationError)); - - Helpers.AddMiddleware(fieldDef); - - // Set a flag that indicates to the error handler that this field uses mutation conventions - fieldDef.ContextData[WellKnownContextData.UsesMutationConvention] = true; - } - } -} - internal class ValidationMiddlewareInjector : TypeInterceptor { public override void OnBeforeRegisterDependencies( @@ -46,229 +18,10 @@ public override void OnBeforeRegisterDependencies( foreach (var fieldDef in objTypeDef.Fields) { - if (Helpers.NeedsMiddleware(options, validatorRegistry, objTypeDef, fieldDef)) - { - Helpers.AddMiddleware(fieldDef); - } - } - } -} - -internal static class Helpers -{ - private const string _middlewareKey = "FairyBread.ValidationMiddleware"; - internal static Lazy _validationFieldMiddlewareDef = new(() => - new FieldMiddlewareDefinition( - FieldClassMiddlewareFactory.Create(), - key: _middlewareKey)); - - internal static bool NeedsMiddleware( - IFairyBreadOptions options, - IValidatorRegistry validatorRegistry, - ObjectTypeDefinition objTypeDef, - ObjectFieldDefinition fieldDef) - { - // Don't add validation middleware unless: - // 1. we have args - var needsValidationMiddleware = false; - - foreach (var argDef in fieldDef.Arguments) - { - // 2. the argument should be validated according to options func - if (!options.ShouldValidateArgument(objTypeDef, fieldDef, argDef)) + if (ValidationMiddlewareHelpers.NeedsMiddleware(options, validatorRegistry, objTypeDef, fieldDef)) { - continue; + ValidationMiddlewareHelpers.AddMiddleware(fieldDef); } - - // 3. there's validators for it - List validatorDescs; - try - { - validatorDescs = DetermineValidatorsForArg(validatorRegistry, argDef); - if (validatorDescs.Count < 1) - { - continue; - } - } - catch (Exception ex) - { - throw new Exception( - $"Problem getting runtime type for argument '{argDef.Name}' " + - $"in field '{fieldDef.Name}' on object type '{objTypeDef.Name}'.", - ex); - } - - // Cleanup context now we're done with these - foreach (var key in argDef.ContextData.Keys) - { - if (key.StartsWith(WellKnownContextData.Prefix)) - { - argDef.ContextData.Remove(key); - } - } - - validatorDescs.TrimExcess(); - needsValidationMiddleware = true; - argDef.ContextData[WellKnownContextData.ValidatorDescriptors] = validatorDescs.AsReadOnly(); } - - return needsValidationMiddleware; - } - - internal static void AddMiddleware(ObjectFieldDefinition fieldDef) - { - fieldDef.MiddlewareDefinitions.Insert(0, _validationFieldMiddlewareDef.Value); - } - - private static List DetermineValidatorsForArg( - IValidatorRegistry validatorRegistry, - ArgumentDefinition argDef) - { - // If validation is explicitly disabled, return none so validation middleware won't be added - if (argDef.ContextData.ContainsKey(WellKnownContextData.DontValidate)) - { - return new List(0); - } - - var validators = new List(); - - // Include implicit validator/s first (if allowed) - if (!argDef.ContextData.ContainsKey(WellKnownContextData.DontValidateImplicitly)) - { - // And if we can figure out the arg's runtime type - var argRuntimeType = TryGetArgRuntimeType(argDef); - if (argRuntimeType is not null) - { - if (validatorRegistry.Cache.TryGetValue(argRuntimeType, out var implicitValidators) && - implicitValidators is not null) - { - validators.AddRange(implicitValidators); - } - } - } - - // Include explicit validator/s (that aren't already added implicitly) - if (argDef.ContextData.TryGetValue(WellKnownContextData.ExplicitValidatorTypes, out var explicitValidatorTypesRaw) && - explicitValidatorTypesRaw is IEnumerable explicitValidatorTypes) - { - // TODO: Potentially check and throw if there's a validator being explicitly applied for the wrong runtime type - - foreach (var validatorType in explicitValidatorTypes) - { - if (validators.Any(v => v.ValidatorType == validatorType)) - { - continue; - } - - validators.Add(new ValidatorDescriptor(validatorType)); - } - } - - return validators; - } - - private static Type? TryGetArgRuntimeType( - ArgumentDefinition argDef) - { - if (argDef.Parameter?.ParameterType is { } argRuntimeType) - { - return argRuntimeType; - } - - if (argDef.Type is ExtendedTypeReference extTypeRef) - { - return TryGetRuntimeType(extTypeRef.Type); - } - - return null; - } - - private static Type? TryGetRuntimeType(IExtendedType extType) - { - // It's already a runtime type, .Type(typeof(int)) - if (extType.Kind == ExtendedTypeKind.Runtime) - { - return extType.Source; - } - - // Array (though not sure what produces this scenario as seems to always be list) - if (extType.IsArray) - { - if (extType.ElementType is null) - { - return null; - } - - var elementRuntimeType = TryGetRuntimeType(extType.ElementType); - if (elementRuntimeType is null) - { - return null; - } - - return Array.CreateInstance(elementRuntimeType, 0).GetType(); - } - - // List - if (extType.IsList) - { - if (extType.ElementType is null) - { - return null; - } - - var elementRuntimeType = TryGetRuntimeType(extType.ElementType); - if (elementRuntimeType is null) - { - return null; - } - - return typeof(List<>).MakeGenericType(elementRuntimeType); - } - - // Input object - if (typeof(InputObjectType).IsAssignableFrom(extType)) - { - var currBaseType = extType.Type.BaseType; - while (currBaseType is not null && - (!currBaseType.IsGenericType || - currBaseType.GetGenericTypeDefinition() != typeof(InputObjectType<>))) - { - currBaseType = currBaseType.BaseType; - } - - if (currBaseType is null) - { - return null; - } - - return currBaseType!.GenericTypeArguments[0]; - } - - // Singular scalar - if (typeof(ScalarType).IsAssignableFrom(extType)) - { - var currBaseType = extType.Type.BaseType; - while (currBaseType is not null && - (!currBaseType.IsGenericType || - currBaseType.GetGenericTypeDefinition() != typeof(ScalarType<>))) - { - currBaseType = currBaseType.BaseType; - } - - if (currBaseType is null) - { - return null; - } - - var argRuntimeType = currBaseType.GenericTypeArguments[0]; - if (argRuntimeType.IsValueType && extType.IsNullable) - { - return typeof(Nullable<>).MakeGenericType(argRuntimeType); - } - - return argRuntimeType; - } - - return null; } } diff --git a/src/FairyBread/WellKnownContextData.cs b/src/FairyBread/WellKnownContextData.cs index 959190a..95f8c16 100644 --- a/src/FairyBread/WellKnownContextData.cs +++ b/src/FairyBread/WellKnownContextData.cs @@ -16,6 +16,9 @@ internal static class WellKnownContextData public const string ValidatorDescriptors = Prefix + "Validators"; - public const string UsesMutationConvention = - Prefix + "UsesMutationConvention"; + public const string UsesInlineErrors = + Prefix + "UsesInlineErrors"; + + public const string UsesGlobalErrors = + Prefix + "UsesGlobalErrors"; }