示例#1
0
        /// <summary>
        /// All types covered by RESTable are selected and validated here
        ///
        /// Resources
        ///   entity
        ///     regular
        ///     wrapper
        ///   terminal
        ///   binary
        /// Views
        ///
        /// </summary>
        /// <returns></returns>
        private void ValidateAndBuildTypeLists(out List <Type> regularTypes, out List <Type> wrapperTypes, out List <Type> terminalTypes,
                                               out List <Type> binaryTypes, out List <Type> eventTypes)
        {
            var allTypes      = typeof(object).GetSubclasses().ToList();
            var resourceTypes = allTypes.Where(t => t.HasAttribute <RESTableAttribute>(out var a) && a is not RESTableProceduralAttribute).ToArray();
            var viewTypes     = allTypes.Where(t => t.HasAttribute <RESTableViewAttribute>()).ToArray();

            if (resourceTypes.Union(viewTypes).ContainsDuplicates(t => t.GetRESTableTypeName()?.ToLower() ?? "unknown", out var dupe))
            {
                throw new InvalidResourceDeclarationException("Types used by RESTable must have unique case insensitive names. Found " +
                                                              $"multiple types with case insensitive name '{dupe}'.");
            }

            void ValidateViewTypes(ICollection <Type> _viewTypes)
            {
                foreach (var viewType in _viewTypes)
                {
                    var resource = viewType.DeclaringType;
                    if (!viewType.IsClass || !viewType.IsNestedPublic || resource == null)
                    {
                        throw new InvalidResourceViewDeclarationException(viewType,
                                                                          "Resource view types must be declared as public classes nested within the the " +
                                                                          "resource type they are views for");
                    }
                    if (viewType.IsSubclassOf(resource))
                    {
                        throw new InvalidResourceViewDeclarationException(viewType, "Views cannot inherit from their resource types");
                    }
                    if (typeof(IResourceWrapper).IsAssignableFrom(resource))
                    {
                        var wrapped = resource.GetWrappedType();
                        if (!viewType.ImplementsGenericInterface(typeof(ISelector <>), out var param) || param[0] != wrapped)
                        {
                            throw new InvalidResourceViewDeclarationException(viewType,
                                                                              $"Expected view type to implement ISelector<{wrapped.GetRESTableTypeName()}>");
                        }
                    }
                    else if (!viewType.ImplementsGenericInterface(typeof(ISelector <>), out var param) || param[0] != resource)
                    {
                        throw new InvalidResourceViewDeclarationException(viewType,
                                                                          $"Expected view type to implement ISelector<{resource.GetRESTableTypeName()}>");
                    }
                    var resourceProperties = TypeCache.GetDeclaredProperties(resource);
                    foreach (var property in TypeCache.FindAndParseDeclaredProperties(viewType).Where(prop => resourceProperties.ContainsKey(prop.Name)))
                    {
                        throw new InvalidResourceViewDeclarationException(viewType,
                                                                          $"Invalid property '{property.Name}'. Resource view types must not contain any public instance " +
                                                                          "properties with the same name (case insensitive) as a property of the corresponding resource. " +
                                                                          "All properties in the resource are automatically inherited for use in conditions with the view.");
                    }
                }
            }

            (regularTypes, wrapperTypes, terminalTypes, binaryTypes, eventTypes) = ResourceValidator.Validate(resourceTypes);
            ValidateViewTypes(viewTypes);
        }
示例#2
0
        public (List <Type> regular, List <Type> wrappers, List <Type> terminals, List <Type> binaries, List <Type> events) Validate(params Type[] types)
        {
            var entityTypes = types
                              .Where(t => !typeof(Terminal).IsAssignableFrom(t) &&
                                     !typeof(IEvent).IsAssignableFrom(t) &&
                                     !t.ImplementsGenericInterface(typeof(IBinary <>)))
                              .ToList();
            var regularTypes = entityTypes
                               .Where(t => !typeof(IResourceWrapper).IsAssignableFrom(t))
                               .ToList();
            var wrapperTypes = entityTypes
                               .Where(t => typeof(IResourceWrapper).IsAssignableFrom(t))
                               .ToList();
            var terminalTypes = types
                                .Where(t => typeof(Terminal).IsAssignableFrom(t))
                                .ToList();
            var binaryTypes = types
                              .Where(t => t.ImplementsGenericInterface(typeof(IBinary <>)))
                              .ToList();
            var eventTypes = types
                             .Where(t => !t.IsAbstract && typeof(IEvent).IsAssignableFrom(t))
                             .ToList();

            void ValidateCommon(Type type)
            {
                #region Check general stuff

                if (type.FullName == null)
                {
                    throw new InvalidResourceDeclarationException(
                              "Encountered an unknown type. No further information is available.");
                }

                if (type.IsGenericTypeDefinition)
                {
                    throw new InvalidResourceDeclarationException(
                              $"Found a generic resource type '{type.GetRESTableTypeName()}'. RESTable resource types must be non-generic");
                }

                if (type.FullName.Count(c => c == '+') >= 2)
                {
                    throw new InvalidResourceDeclarationException($"Invalid resource '{type.GetRESTableTypeName()}'. " +
                                                                  "Inner resources cannot have their own inner resources");
                }

                if (type.HasAttribute <RESTableViewAttribute>())
                {
                    throw new InvalidResourceDeclarationException(
                              $"Invalid resource type '{type.GetRESTableTypeName()}'. Resource types cannot be " +
                              "decorated with the 'RESTableViewAttribute'");
                }

                if (type.Namespace == null)
                {
                    throw new InvalidResourceDeclarationException($"Invalid type '{type.GetRESTableTypeName()}'. Unknown namespace");
                }

                if (Configuration.ReservedNamespaces.Contains(type.Namespace.ToLower()) &&
                    type.Assembly != typeof(RESTableConfigurator).Assembly)
                {
                    throw new InvalidResourceDeclarationException(
                              $"Invalid namespace for resource type '{type.GetRESTableTypeName()}'. Namespace '{type.Namespace}' is reserved by RESTable");
                }

                if ((!type.IsClass || !type.IsPublic && !type.IsNestedPublic) && type.Assembly != typeof(Resource).Assembly)
                {
                    throw new InvalidResourceDeclarationException(
                              $"Invalid type '{type.GetRESTableTypeName()}'. Resource types must be public classes");
                }

                if (type.GetRESTableInterfaceType() is Type interfaceType)
                {
                    if (!interfaceType.IsInterface)
                    {
                        throw new InvalidResourceDeclarationException(
                                  $"Invalid Interface of type '{interfaceType.GetRESTableTypeName()}' assigned to resource '{type.GetRESTableTypeName()}'. " +
                                  "Type is not an interface");
                    }

                    if (interfaceType.GetProperties()
                        .Select(p => p.Name)
                        .ContainsDuplicates(StringComparer.OrdinalIgnoreCase, out var interfacePropDupe))
                    {
                        throw new InvalidResourceMemberException(
                                  $"Invalid Interface of type '{interfaceType.GetRESTableTypeName()}' assigned to resource '{type.GetRESTableTypeName()}'. " +
                                  $"Interface contained properties with duplicate names matching '{interfacePropDupe}' (case insensitive).");
                    }

                    var interfaceName = interfaceType.GetRESTableTypeName();
                    foreach (var method in type.GetInterfaceMap(interfaceType).TargetMethods)
                    {
                        if (!method.IsSpecialName)
                        {
                            continue;
                        }
                        var interfaceProperty = interfaceType
                                                .GetProperties()
                                                .First(p => p.GetGetMethod()?.Name is string getname && method.Name.EndsWith(getname) ||
                                                       p.GetSetMethod()?.Name is string setname && method.Name.EndsWith(setname));

                        Type propertyType = null;
                        if (method.IsPrivate && method.Name.StartsWith($"{interfaceName}.get_") || method.Name.StartsWith("get_"))
                        {
                            propertyType = method.ReturnType;
                        }
                        else if (method.IsPrivate && method.Name.StartsWith($"{interfaceName}.set_") || method.Name.StartsWith("set_"))
                        {
                            propertyType = method.GetParameters()[0].ParameterType;
                        }

                        if (propertyType == null)
                        {
                            throw new InvalidResourceDeclarationException(
                                      $"Invalid implementation of interface '{interfaceType.GetRESTableTypeName()}' assigned to resource '{type.GetRESTableTypeName()}'. " +
                                      $"Unable to determine the type for interface property '{interfaceProperty.Name}'");
                        }

                        PropertyInfo projectedProperty;
                        if (method.Name.StartsWith($"{interfaceName}.get_"))
                        {
                            projectedProperty = method.GetInstructions()
                                                .Select(i => i.OpCode == OpCodes.Call && i.Operand is MethodInfo calledMethod && method.IsSpecialName
                                    ? type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
                                                        .FirstOrDefault(p => p.GetGetMethod() == calledMethod)
                                    : null)
                                                .LastOrDefault(p => p != null);
                        }
                        else if (method.Name.StartsWith($"{interfaceName}.set_"))
                        {
                            projectedProperty = method.GetInstructions()
                                                .Select(i => i.OpCode == OpCodes.Call && i.Operand is MethodInfo calledMethod && method.IsSpecialName
                                    ? type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
                                                        .FirstOrDefault(p => p.GetSetMethod() == calledMethod)
                                    : null)
                                                .LastOrDefault(p => p != null);
                        }
                        else
                        {
                            continue;
                        }

                        if (projectedProperty == null)
                        {
                            throw new InvalidResourceDeclarationException(
                                      $"Invalid implementation of interface '{interfaceType.GetRESTableTypeName()}' assigned to resource '{type.GetRESTableTypeName()}'. " +
                                      $"RESTable was unable to determine which property of '{type.GetRESTableTypeName()}' that is exposed by interface " +
                                      $"property '{interfaceProperty.Name}'. For getters, RESTable will look for the last IL instruction " +
                                      "in the method body that fetches a property value from the resource type. For setters, RESTable will look " +
                                      "for the last IL instruction in the method body that sets a property value in the resource type.");
                        }

                        if (projectedProperty.PropertyType != propertyType)
                        {
                            throw new InvalidResourceDeclarationException(
                                      $"Invalid implementation of interface '{interfaceType.GetRESTableTypeName()}' assigned to resource '{type.GetRESTableTypeName()}'. " +
                                      $"RESTable matched interface property '{interfaceProperty.Name}' with resource property '{projectedProperty.Name}' " +
                                      "using the interface property matching rules, but these properties have a type mismatch. Expected " +
                                      $"'{projectedProperty.PropertyType.GetRESTableTypeName()}' but found '{propertyType.GetRESTableTypeName()}' in interface");
                        }
                    }
                }

                #endregion

                #region Check for invalid IDictionary implementation

                var validTypes = new[] { typeof(string), typeof(object) };
                if (type.ImplementsGenericInterface(typeof(IDictionary <,>), out var typeParams) &&
                    !type.IsSubclassOf(typeof(JObject)) &&
                    !typeParams.SequenceEqual(validTypes))
                {
                    throw new InvalidResourceDeclarationException(
                              $"Invalid resource declaration for type '{type.GetRESTableTypeName()}'. All resource types implementing " +
                              "the generic 'System.Collections.Generic.IDictionary`2' interface must either be subclasses of " +
                              "Newtonsoft.Json.Linq.JObject or have System.String as first type parameter and System.Object as " +
                              $"second type parameter. Found {typeParams[0].GetRESTableTypeName()} and {typeParams[1].GetRESTableTypeName()}");
                }

                #endregion

                #region Check for invalid IEnumerable implementation

                if ((type.ImplementsGenericInterface(typeof(IEnumerable <>)) || typeof(IEnumerable).IsAssignableFrom(type)) &&
                    !type.ImplementsGenericInterface(typeof(IDictionary <,>)))
                {
                    throw new InvalidResourceDeclarationException(
                              $"Invalid resource declaration for type '{type.GetRESTableTypeName()}'. The type has an invalid imple" +
                              $"mentation of an IEnumerable interface. The resource '{type.GetRESTableTypeName()}' (or any of its base types) " +
                              "cannot implement the \'System.Collections.Generic.IEnumerable`1\' or \'System.Collections.IEnume" +
                              "rable\' interfaces without also implementing the \'System.Collections.Generic.IDictionary`2\' interface."
                              );
                }

                #endregion

                #region Check for public instance fields

                var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance);
                if (fields.Any())
                {
                    throw new InvalidResourceMemberException(
                              $"A RESTable resource cannot have public instance fields, only properties. Resource: '{type.GetRESTableTypeName()}' had " +
                              $"fields: {string.Join(", ", fields.Select(f => $"'{f.Name}'"))} in resource '{type.GetRESTableTypeName()}'"
                              );
                }

                #endregion

                #region Check for properties with duplicate case insensitive names

                if (TypeCache.FindAndParseDeclaredProperties(type).ContainsDuplicates(DeclaredProperty.NameComparer, out var duplicate))
                {
                    throw new InvalidResourceMemberException(
                              $"Invalid properties for resource '{type.GetRESTableTypeName()}'. Names of public instance properties must " +
                              $"be unique (case insensitive). Two or more property names were equivalent to '{duplicate.Name}'."
                              );
                }

                #endregion
            }

            void ValidateEntityDeclarations(List <Type> regularResources)
            {
                foreach (var type in regularResources)
                {
                    ValidateCommon(type);
                }
            }

            void ValidateWrapperDeclaration(List <Type> wrappers)
            {
                if (wrappers.Select(type => (type, wrapped: type.GetWrappedType())).ContainsDuplicates(pair => pair.wrapped, out var dupe))
                {
                    throw new InvalidResourceWrapperException(dupe, "must wrap unique types. Found multiple wrapper declarations for " +
                                                              $"wrapped type '{dupe.wrapped.GetRESTableTypeName()}'.");
                }

                foreach (var wrapper in wrappers)
                {
                    var wrapped = wrapper.GetWrappedType();
                    var _types  = (wrapper, wrapped);
                    var members = wrapper.GetMembers(BindingFlags.Public | BindingFlags.Instance);
                    if (members.OfType <PropertyInfo>().Any() || members.OfType <FieldInfo>().Any())
                    {
                        throw new InvalidResourceWrapperException(_types, "cannot contain public instance properties or fields");
                    }
                    ValidateCommon(wrapper);
                    if (wrapper.GetInterfaces()
                        .Where(i => typeof(IOperationsInterface).IsAssignableFrom(i))
                        .Any(i => i.IsGenericType && i.GenericTypeArguments[0] != wrapped))
                    {
                        throw new InvalidResourceWrapperException(_types, "cannot implement operations interfaces for types other than " +
                                                                  $"'{wrapped.GetRESTableTypeName()}'.");
                    }
                    if (wrapped.FullName?.Contains("+") == true)
                    {
                        throw new InvalidResourceWrapperException(_types, "cannot wrap types that are declared within the scope of some other class.");
                    }
                    if (wrapped.HasAttribute <RESTableAttribute>())
                    {
                        throw new InvalidResourceWrapperException(_types, "cannot wrap types already decorated with the 'RESTableAttribute' attribute");
                    }
                    if (wrapper.Assembly == typeof(RESTableConfigurator).Assembly)
                    {
                        throw new InvalidResourceWrapperException(_types, "cannot wrap RESTable types");
                    }
                }
            }

            void ValidateTerminalDeclarations(List <Type> terminals)
            {
                foreach (var terminal in terminals)
                {
                    ValidateCommon(terminal);
                    var constructors = terminal.GetConstructors();
                    if (constructors.Length != 1)
                    {
                        throw new InvalidTerminalDeclarationException(terminal, "must have exactly one public constructor. Found " + constructors.Length);
                    }
                    var constructorParameterNames = new HashSet <string>(StringComparer.OrdinalIgnoreCase);
                    var properties = TypeCache.GetDeclaredProperties(terminal);
                    foreach (var parameter in constructors[0].GetParameters())
                    {
                        if (!constructorParameterNames.Add(parameter.Name))
                        {
                            throw new InvalidTerminalDeclarationException(terminal, "must not define multiple constructor parameters with the same case " +
                                                                          $"insensitive parameter name. Found duplicate of '{parameter.Name.ToLowerInvariant()}'");
                        }
                        if (!properties.ContainsKey(parameter.Name))
                        {
                            throw new InvalidTerminalDeclarationException(terminal, "must not define a constructor parameter with a name that does not equal the name of a " +
                                                                          "public instance property on the same type (case insensitive). Found parameter " +
                                                                          $"'{parameter.Name.ToLowerInvariant()}' with no matching public instance property.");
                        }
                    }
                    if (terminal.ImplementsGenericInterface(typeof(IEnumerable <>)))
                    {
                        throw new InvalidTerminalDeclarationException(terminal, "must not be collections");
                    }
                    if (terminal.HasResourceProviderAttribute())
                    {
                        throw new InvalidTerminalDeclarationException(terminal, "must not be decorated with a resource provider attribute");
                    }
                    if (typeof(IOperationsInterface).IsAssignableFrom(terminal))
                    {
                        throw new InvalidTerminalDeclarationException(terminal, "must not implement any other RESTable operations interfaces");
                    }
                }
            }

            void ValidateBinaryDeclarations(List <Type> binaries)
            {
                foreach (var binary in binaries)
                {
                    ValidateCommon(binary);
                    if (binary.ImplementsGenericInterface(typeof(IEnumerable <>)))
                    {
                        throw new InvalidBinaryDeclarationException(binary, "must not be collections");
                    }
                    if (binary.HasResourceProviderAttribute())
                    {
                        throw new InvalidBinaryDeclarationException(binary, "must not be decorated with a resource provider attribute");
                    }
                    if (typeof(IOperationsInterface).IsAssignableFrom(binary))
                    {
                        throw new InvalidBinaryDeclarationException(binary, "must not implement any other RESTable operations interfaces");
                    }
                }
            }

            void ValidateEventDeclarations(List <Type> events)
            {
                foreach (var @event in events)
                {
                    ValidateCommon(@event);
                    if (!typeof(IEvent).IsAssignableFrom(@event))
                    {
                        throw new InvalidEventDeclarationException(@event, "must inherit from 'RESTable.Resources.Event<T>'");
                    }
                    if (@event.ImplementsGenericInterface(typeof(IEnumerable <>)))
                    {
                        throw new InvalidEventDeclarationException(@event, "must not be collections");
                    }
                    if (@event.HasResourceProviderAttribute())
                    {
                        throw new InvalidEventDeclarationException(@event, "must not be decorated with a resource provider attribute");
                    }
                    if (typeof(IOperationsInterface).IsAssignableFrom(@event))
                    {
                        throw new InvalidEventDeclarationException(@event, "must not implement any RESTable operations interfaces");
                    }
                }
            }

            ValidateEntityDeclarations(entityTypes);
            ValidateWrapperDeclaration(wrapperTypes);
            ValidateTerminalDeclarations(terminalTypes);
            ValidateBinaryDeclarations(binaryTypes);
            ValidateEventDeclarations(eventTypes);

            return(regularTypes, wrapperTypes, terminalTypes, binaryTypes, eventTypes);
        }