OData / odata.net

ODataLib: Open Data Protocol - .NET Libraries and Frameworks
https://docs.microsoft.com/odata
Other
686 stars 349 forks source link

Named elements length is not validated according to OData specification rule #2465

Open elize-vdr opened 2 years ago

elize-vdr commented 2 years ago

The IEdmModel.Validate() does not validate the length of an Entity Set's name according to the OData specification rules.

Assemblies affected

Microsoft.Odata.Edm 7.12.1 & Microsoft.OData.Core 7.12.1

Reproduce steps

Validate an OData contract where the EntitySet name is longer than 128 characters, it will pass the validation.

Expected result

If the name of an EntitySet is longer than 128 validation should fail for this name

Actual result

An EntitySet name of length longer than 128 characters passed the IEdmModel.Validate()

Additional detail

According to the OData 4.01 specification the name of an Enity Set is defined as:

An entity set is identified by its name, a [simple identifier](http://docs.oasis-open.org/odata/odata-csdl-xml/v4.01/odata-csdl-xml-v4.01.html#sec_SimpleIdentifier) that MUST be unique within its entity container.

Where a Simple Identier is defined as:

A simple identifier is a Unicode character sequence with the following restrictions:

It consists of at least one and at most 128 Unicode characters (code points).
The first character MUST be the underscore character (U+005F) or any character in the Unicode category “Letter (L)” or “Letter number (Nl)”.
The remaining characters MUST be the underscore character (U+005F) or any character in the Unicode category “Letter (L)”, “Letter number (Nl)”, “Decimal number (Nd)”, “Non-spacing mark (Mn)”, “Combining spacing mark (Mc)”, “Connector punctuation (Pc)”, and “Other, format (Cf)”.
Non-normatively speaking it starts with a letter or underscore, followed by at most 127 letters, underscores or digits.

Therefore the maximum length of an Entity Set's name should be 128 characters, if an Entity Set's name is logner than 128 characters it should fail validation, currently the IEdmModel.Validate() allows 480 characters for an EntitySet's name.

For names in OData they are mostly specified as a Simple Identifier, these include for example: Nominal Type name: Simple Identifier Namespace alias: Simple Identifier Entity Type Name: Simple Identifier Structural Property name: Simple Identifier Navigation Property name: Simple Identifier Complex Type name: Simple Identifier Enumeration Type name: Simple Identifier TypeDefinition name: Simple Identifier Action name: Simple Identifier Function name: Simple Identifier Parameter name: Simple Identifier Entity Container name: Simple Identifier Entity Set name: Simple Identifier Singleton name: Simple Identifier Term name: Simple Identifier

But in the .Net OData implementation the validation rule NamedElementNameIsTooLong in the class ValidationRules tests for a length of 480. What is the motivation for this? The OData specification states the maximum length for a Simple Identifier to be 128 and names are all indicated as of type Simple Identifier.

ElizabethOkerio commented 2 years ago

@mikepizzo can they create custom validation rules here?

elize-vdr commented 2 years ago

Hi @ElizabethOkerio, Thank you for looking into this. I saw your comment and in case this may be of value to you on this issue: I built a work-around for now by creating an extenstion of the IEdmModel and creating a new function called ValidationExtended to replace the original Validation, and I also added a function called GetEdmModelExtendedRuleSet in which I create the base ruleset as it is in the original code (base + the additional rules based on the version) then I add a custom ValidationRule that I created that checks for character length 128 (I paste code below). I am not sure if this is the correct way but for now it works. The only problem with this is that the original validation rule for 480 characters is still present, so if it encoutners a name longer than 480 characters it will spit out 2 validation errors, one stating the name is longer than 480 characters as well as the one stating the name is longer than 128 characters, because I cannot remove the 480 character check. So it would be useful if the ruleset was available to be customized, it can solve some problems. The code to make all this happen:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.OData.Edm;
using Microsoft.OData.Edm.Validation;

namespace ODataParser.ParserODataV4
{
    public static class EdmValidatorExtensions
    {
         /// <summary>
         /// Validate the <see cref="T:Microsoft.OData.Edm.IEdmModel" /> and all of its properties given certain version.
         /// This extension was required to validate the name length of named elements correctly because the .net implementation incorrectly takes the max length of
         /// names as 480 where is should be 128 according to the OData specification.
         /// </summary>
         /// <param name="root">The root of the model to be validated.</param>
         /// Indicates whether named elements must be validated with our custom rule that validates
         /// for a max length of 128 according to OData specification, this is to allow for the shortcoming in the .net implementation where the
         /// rule ValidationRules.NamedElementNameIsTooLong checks for named elements of name length 480 which is incorrect according to OData.
         /// If this is false then the normal validation of the .net implementation is done, else we do our own validation against a max length of 128.
         /// </param>
         /// <param name="errors">Errors encountered while validating the model.</param>
         /// <returns>True if model is valid, otherwise false.</returns>
         /// <footer><a href="https://docs.microsoft.com/en-us/dotnet/api/Microsoft.OData.Edm.Validation.EdmValidator.Validate?view=netstandard-2.0">`EdmValidator.Validate` on docs.microsoft.com</a></footer>
         public static bool ValidationExtended(
             this IEdmModel root,
             out IEnumerable<EdmError> errors)
         {
             Version version = root.GetEdmVersion();
             if ((object) version == null)
                 version = EdmConstants.EdmVersionDefault;
             return root.Validate(GetEdmModelExtendedRuleSet(version), out errors);
         }

         /// <summary>
         /// Gets the default validation ruleset for the given version with the extended rule added.
         /// </summary>
         /// <param name="version">The EDM version being validated.</param>
         /// <returns>The set of rules to validate that the model conforms to the given version.</returns>
         /// <footer><a href="https://docs.microsoft.com/en-us/dotnet/api/Microsoft.OData.Edm.Validation.ValidationRuleSet.GetEdmModelRuleSet?view=netstandard-2.0">`ValidationRuleSet.GetEdmModelRuleSet` on docs.microsoft.com</a></footer>
         private static ValidationRuleSet GetEdmModelExtendedRuleSet(Version version)
         {
             var baseValidationRuleSet = ValidationRuleSet.GetEdmModelRuleSet(version);

             ValidationRuleSet customRuleSet = new ValidationRuleSet((IEnumerable<ValidationRule>) baseValidationRuleSet, (IEnumerable<ValidationRule>) new ValidationRule[1]
             {
                 (ValidationRule)ValidationRulesExtension.ExtendNamedElementNameIsTooLong
             });

            return customRuleSet;
         }

    }

    public static class ValidationRulesExtension
    {
        /// <summary>
        /// Validates that an element name is not too long according to the CSDL spec which is 128 characters.
        /// </summary>
        public static readonly ValidationRule<IEdmNamedElement> ExtendNamedElementNameIsTooLong = new ValidationRule<IEdmNamedElement>((Action<ValidationContext, IEdmNamedElement>) ((context, item) =>
        {
            if (IsNullOrWhiteSpaceInternal(item.Name) || item.Name.Length <= 128)
                return;
            context.AddError(item.Location(), EdmErrorCode.NameTooLong, $"The specified name must not be longer than 128 characters: '{item.Name}' - [{item.GetType()}]");
        }));

        private static bool IsNullOrWhiteSpaceInternal(string value) => value == null || ((IEnumerable<char>) value.ToCharArray()).All<char>(new Func<char, bool>(char.IsWhiteSpace));
    }

}