Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Import vocabulary annotations declared in external referenced models #1987

Merged
merged 11 commits into from
Feb 23, 2021

Conversation

habbes
Copy link
Contributor

@habbes habbes commented Jan 25, 2021

Issues

Fix #1936

Description

Includes annotations referenced in the edmx:IncludeAnnotations tag of the edmx:Reference to be included when loading external referenced models.

This makes the vocabulary annotations imported from referenced models available through:

IEnumerable<IEdmVocabularyAnnotation> annotations = model.FindVocabularyAnnotations(element);

or

IEnumerable<IEdmVocabularyAnnotation> annotations = element.VocabularyAnnotations(model);

Note, these external vocabulary annotations will not appear in model.FindDeclaredVocabularyAnnotations(element) or model.VocabularyAnnotations since they only return the annotations declared directly in the current model.

This PR attempts to restrict the imported annotations based on the TermNamespace, TargetNamespace and Qualifier
as specified in the specification: http://docs.oasis-open.org/odata/odata-csdl-xml/v4.01/odata-csdl-xml-v4.01.html#sec_IncludedAnnotations

Assuming the main model has the following reference:

<edmx:Reference  Uri=""SimplePermissions.xml"">
    <edmx:IncludeAnnotations  TermNamespace=""Org.OData.Capabilities.V1"" Qualifier=""Insert""/>
    <edmx:IncludeAnnotations  TermNamespace=""OtherIncludedNS"" TargetNamespace=""Example.Types""/>
</edmx:Reference>

then this feature will import vocabulary annotations from the SimplePermissions.xml csdl which meet either of the following criteria:

  • The annotation's term is in the namespace Org.OData.Capabilities.V1 namespace and it has the qualifier Insert
  • The annotation's term is in the namespace OtherIncludedNS and it's target element is in the namespace Example.Types

Other vocabulary annotations in the referenced csdl will be skipped.

Note: This restriction only applies to "out of line" annotations, i.e. annotations with are declared inside of a top-level <Annotations> element rather than being declared directly inside ("in-line") the target element.

Note: If the reference only has edmx:IncludeAnnotations declarations, but not edmx:Include declaration (http://docs.oasis-open.org/odata/odata-csdl-xml/v4.01/odata-csdl-xml-v4.01.html#sec_IncludedSchema), then this feature will only vocabulary annotations from the referenced models and exclude other schema elements like entity types, operations, etc.

Checklist (Uncheck if it is not completed)

  • Test cases added
  • Build and test with one-click build and test script passed

Additional work necessary

If documentation update is needed, please add "Docs Needed" label to the issue and provide details about the required document change in the issue.

@habbes habbes force-pushed the fix/1936-get-external-referenced-vocab branch from f998660 to 26a681e Compare January 26, 2021 06:43
@@ -114,13 +114,14 @@ private CsdlSemanticsModel(CsdlModel referencedCsdlModel, IEdmDirectValueAnnotat
foreach (var schema in referencedCsdlModel.Schemata)
{
string schemaNamespace = schema.Namespace;
IEdmInclude edmInclude = referencedCsdlModel.ParentModelReferences.SelectMany(s => s.Includes).FirstOrDefault(s => s.Namespace == schemaNamespace);
if (edmInclude != null)
bool shouldAddTypes = referencedCsdlModel.ParentModelReferences.SelectMany(s => s.Includes).Any(s => s.Namespace == schemaNamespace);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
bool shouldAddTypes = referencedCsdlModel.ParentModelReferences.SelectMany(s => s.Includes).Any(s => s.Namespace == schemaNamespace);
bool addSchemaElements = referencedCsdlModel.ParentModelReferences.SelectMany(s => s.Includes).Any(s => s.Namespace == schemaNamespace);

Copy link
Member

@xuzhg xuzhg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

{
this.AddSchema(schema, true);
this.AddSchema(schema, addSchemaElements, true, null);
Copy link
Contributor

@gathogojr gathogojr Jan 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this.AddSchema(schema, addSchemaElements, true, null);
this.AddSchema(schema, addSchemaElements, true /*addAnnotations*/, null /*includeAnnotations*/);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I could use named parameters instead, like:

this.AddSchema(schema, addSchemaElements, addAnnotations: true, includeAnnotations: null);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@habbes Use John's suggestion. Named parameters aren't supported in some c# versions.

IEdmInclude edmInclude = referencedCsdlModel.ParentModelReferences.SelectMany(s => s.Includes).FirstOrDefault(s => s.Namespace == schemaNamespace);
if (edmInclude != null)
bool shouldAddTypes = referencedCsdlModel.ParentModelReferences.SelectMany(s => s.Includes).Any(s => s.Namespace == schemaNamespace);
IEnumerable<IEdmIncludeAnnotations> includeAnnotations = referencedCsdlModel.ParentModelReferences.SelectMany(s => s.IncludeAnnotations);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

includeAnnotations [](start = 52, length = 18)

I'm concerned about the performance of executing this (somewhat expensive) SelectMany LINQ expression multiple times -- we execute it once on line 119, again on line 625, and then for each term on line 641. Probably be better, especially since we are iterating multiple times, to make this a list. removing the linq expression all together and simply iterating through building the collection would be even better, but probably not a significant savings here.

// we check if the namespace matches, and (if applicable) also the target namespace and the qualifier
// see: http://docs.oasis-open.org/odata/odata-csdl-xml/v4.01/odata-csdl-xml-v4.01.html#sec_IncludedAnnotations

return includeAnnotations.Any(include =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any [](start = 38, length = 3)

LINQ is expensive -- AGS has gone through a LINQ hunt and removed all use of LINQ from their code. We should avoid it where we can.

Instead, it would be more efficient to iterate through the collection and evaluate the options.

<edmx:Edmx Version=""4.0"" xmlns:edmx=""http://docs.oasis-open.org/odata/ns/edmx"">
<edmx:Reference Uri=""SimpleModel.xml"">
<edmx:Include Namespace=""Default""/>
<edmx:Include Namespace=""Example.Types"" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a test where the annotation schema defines an alias for the referenced namespace that is used locally in specifying the target, and make sure that annotations restricted to that target are still (exclusively) included.

i.e., define an alias for the "Example.Types" namespace in permissions.Csdl (say "examples") and use it in the Annotations Target, and then in SimpleModel.xml specify a TargetNamespace of "Example.Types" in your include annotations and make sure it still returns the right set of annotations (I think your code handles that, just want to make sure it's in the tests...)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added the test


var filteredAnnotations = csdlAnnotations.Annotations.Where(annotation =>
ShouldIncludeAnnotation(annotation, target, includeAnnotations));
return new CsdlAnnotations(filteredAnnotations, csdlAnnotations.Target, csdlAnnotations.Qualifier);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

csdlAnnotations.Target [](start = 60, length = 22)

should this use csdlAnnotations.Target, or "target" (which has the alias already resolved)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The aim here is to return a copy of the CsdlAnnotations instance but with the actual annotations filtered. So every other property should be identical to the original CsdlAnnotations instance, hence csdlAnnotations.Target. The target with the alias already resolved is used when comparing against the include.TargetNamespace.

/// figure out whether the schema's types (entiy types, enums, containers, etc.) and annotations should
/// be imported
/// </summary>
/// <param name="schema"></param>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document this param and the following 2


/// <summary>
/// Process the <see cref="IEdmReference"/>s of the parent model to
/// figure out whether the schema's types (entiy types, enums, containers, etc.) and annotations should
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// figure out whether the schema's types (entiy types, enums, containers, etc.) and annotations should
/// figure out whether the schema's types (entity types, enums, containers, etc.) and annotations should

shouldAddSchemaElements = false;
includeAnnotationsIndex = new Dictionary<string, List<IEdmIncludeAnnotations>>();


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

string schemaNamespace = schema.Namespace;

shouldAddSchemaElements = false;
includeAnnotationsIndex = new Dictionary<string, List<IEdmIncludeAnnotations>>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe apply the constructor that takes an IEqualityComparer<T> argument if TermNamespace="Org.OData.Capabilities.V1" is considered to be equivalent to TermNamespace="org.OData.Capabilities.V1" (i.e. case-insensitive)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe namespaces are case-sensitive. At least in many cases, the spec mentions that identifiers are case-sensitive. I assume it applies to namespaces as well.

string target,
Dictionary<string, List<IEdmIncludeAnnotations>> includeAnnotationsIndex)
{
if (includeAnnotationsIndex == null)
Copy link
Contributor

@gathogojr gathogojr Feb 5, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this ever likely to evaluate to true given the initialization of includeAnnotationsIndex here? Maybe check for null and empty (or just empty if null is unlikely)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's actually set to null when called from the original AddSchema(schema, addAnnotations) method here.

In that method, when the addAnnotations was set true, all the annotations in the schema were added without any filtering. In that case I call the AddSchemaOutOfLineAnnotations with includeAnnotationsIndex set to null to indicate that all annotations should be added without any filtering. If, on the other hand, the includeAnnotationsIndex is empty, then all annotations will be filtered out and not get added, since the ShouldIncludeAnnotation method will return false.

I think I should mention in the doc comments what the intended behaviour is when includeAnnotationsIndex is null.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I should make an optimization to avoid filtering the annotations if the includeAnnotationsIndex is empty, since in that case none of the csdl annotations will be imported.


string qualifiedTerm = this.ReplaceAlias(annotation.Term);
string targetNamespace;
EdmUtil.TryGetNamespaceNameFromQualifiedName(target, out targetNamespace, out string baseTarget);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
EdmUtil.TryGetNamespaceNameFromQualifiedName(target, out targetNamespace, out string baseTarget);
EdmUtil.TryGetNamespaceNameFromQualifiedName(target, out targetNamespace, out _);

string qualifiedTerm = this.ReplaceAlias(annotation.Term);
string targetNamespace;
EdmUtil.TryGetNamespaceNameFromQualifiedName(target, out targetNamespace, out string baseTarget);
if (EdmUtil.TryGetNamespaceNameFromQualifiedName(qualifiedTerm, out string termNamespace, out string baseTerm))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (EdmUtil.TryGetNamespaceNameFromQualifiedName(qualifiedTerm, out string termNamespace, out string baseTerm))
if (EdmUtil.TryGetNamespaceNameFromQualifiedName(qualifiedTerm, out string termNamespace, out _))


annotations.Add(new CsdlSemanticsAnnotations(schemaWrapper, schemaOutOfLineAnnotations));
return includeAnnotations.Any(include =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to confirm annotation.Qualifier == include.Qualifier and targetNamespace == include.TargetNamespace will be a case-sensitive comparison...

Maybe

(string.IsNullOrEmpty(include.Qualifier) || include.Qualifier.Equals(annotation.Qualifier, StringComparison.OrdinalIgnoreCase))
&& (string.IsNullOrEmpty(include.TargetNamespace) || include.TargetNamespace.Equals(targetNamespace, StringComparison.OrdinalIgnoreCase)));

?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe they are case sensitive

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed: Names are case-sensitive, but service authors SHOULD NOT choose names that differ only in case.

@habbes habbes added the Ready for review Use this label if a pull request is ready to be reviewed label Feb 5, 2021
/// If this dictionary is non-empty, then only annotations that match
/// the references will be added. If it's empty, no annotations will be added. If it's null, all annotations
/// will be added unconditionally.</param>
private void AddOutOfLineAnnotationsFromSchema(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@habbes RE: If it's null, all annotations will be added unconditionally
Does this happen from the calling method or this (AddOutOfLineAnnotationsFromSchema) method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That actually takes place in the FilterIncludedAnnotations method, which is called by this method (AddOutOfLineAnnotationsFromSchema). The FilterIncludedAnnotations method has a similar note in its doc comments.

Copy link
Contributor

@gathogojr gathogojr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link
Contributor

@KenitoInc KenitoInc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@KenitoInc
Copy link
Contributor

I see the build is failing.
You have defined AddSchemaElements accepting 1 parameter, but calling it with 2 parameters.

@mikepizzo
Copy link
Member

😀


In reply to: 772912424 [](ancestors = 772912424)

CsdlSemanticsSchema schemaWrapper,
Dictionary<string, List<IEdmIncludeAnnotations>> includeAnnotationsIndex)
{
if (includeAnnotationsIndex?.Count == 0)
Copy link
Member

@marabooy marabooy Feb 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (includeAnnotationsIndex?.Count == 0)
if (!includeAnnotationsIndex?.Count > 0)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or handle the null in a better way as Any can cause some issues with performance here due. I believe null == 0 wil give you false but I could be wrong.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't understand this comment very well. But I made the null check more explicit includeAnnotationsIndex != null && includeAnnotationsIndex.Count == 0

@@ -1230,5 +1231,323 @@ public void ValidateEdmxVersions(string odataVersion)
Assert.Empty(edmErrors);
Assert.Equal(edmModel.GetEdmVersion(), odataVersion == "4.0" ? EdmConstants.EdmVersion4 : EdmConstants.EdmVersion401);
}

[Fact]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fact [](start = 9, length = 4)

Do you have any tests that validate referenced types (not annotations) from referenced namespace.

For example, in your tests, you could test loading permissionsCsdl and validating that it imported types from "Default" and "Exmaple.Types" from main.csdl, but didn't import types from main.csdl defined in 3rd namespace.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added the test.

Copy link
Member

@mikepizzo mikepizzo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

@mikepizzo
Copy link
Member

nice.

@habbes habbes merged commit 8444960 into OData:master Feb 23, 2021
@habbes habbes deleted the fix/1936-get-external-referenced-vocab branch February 23, 2021 18:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Ready for review Use this label if a pull request is ready to be reviewed
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Referenced external permissions annotations don't reflect in main EDM model
6 participants