-
Notifications
You must be signed in to change notification settings - Fork 351
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
Cache container elements in ODataUriResolver model elements cache #2623
Conversation
|
||
/// <summary> | ||
/// Builds a case-insensitive cache of schema elements from | ||
/// the specified <paramref name="model"/>. | ||
/// </summary> | ||
/// <param name="model">The model whose schema elements to cache. This model should be immutable. See <see cref="ExtensionMethods.MarkAsImmutable(IEdmModel)"/>.</param> | ||
public NormalizedSchemaElementsCache(IEdmModel model) | ||
public NormalizedModelElementsCache(IEdmModel model) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why can't we do the cache in the 'EdmModel'?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you mean by cache in the EdmModel
? Does the EdmModel
have a cache?
Also, in this case we're dealing with the IEdmModel
interface, we can't guarantee whether it's an EdmModel
, a CsdlSemanticsModel
or some other implementation.
I've checked around other users in the codebase and in WebAPI, and any time some metadata is added to the model, it's added as an "annotation". In this case this cache will also be an annotation that's bound to the model. It's not a static cache.
Have I answered your question?
/// <returns>A list of matching navigation sources, or null if no navigation source matches the name.</returns> | ||
public List<IEdmNavigationSource> FindNavigationSources(string name) | ||
{ | ||
if (navigationSourcesCache.TryGetValue(name, out List<IEdmNavigationSource> results)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the name the simple identifier of the navigation source? not the qualified name?
How about searching the navigation source in the referenced model? with the same name but a different namespace?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or, is it only on Top-level model entity container?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it's the simple identifier of the navigation source. And yes, it's only searching the top-level model entity container.
The reason I implemented it this way is because I wanted to retain the same behaviour as the existing implementation which performs the search as follows:
IEdmEntityContainer container = model.EntityContainer;
if (container == null)
{
return null;
}
var result = container.Elements.OfType<IEdmNavigationSource>()
.Where(source => string.Equals(identifier, source.Name, StringComparison.OrdinalIgnoreCase));
As you can see the existing implementation only searches the top-level model's container and it only compares the source.Name
, not the fully qualified name (including the container name).
If I change the behaviour of the cache to include referenced models or fully qualified identifier, then there would be an inconsistency between the cache and non-cache code paths.
On the other hand, if the expected behaviour is that the URI resolver should search referenced models and/or fully qualified name, then the current implementation is a bug. And I think that should be a separate discussion since that change of behaviour could be observable to the customer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The doc comment of the ResolveOperationImports
describes the identifier
parameter as follows:
The qualified name of the operation import which may or may not include the container name.
However, the existing implementation does not consider the qualified name in its search:
IEdmEntityContainer container = model.EntityContainer;
if (container == null)
{
return Enumerable.Empty<IEdmOperationImport>();
}
return container.Elements.OfType<IEdmOperationImport>()
.Where(source => string.Equals(identifier, source.Name, StringComparison.OrdinalIgnoreCase));
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we are specifying the navigation source identifier and not searching in referenced models, is there a scenario where whatever that gets returned is a list and not just a single navigation source?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like can we have several navigation sources with the same identifier in the same model?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we're searching a single container, I don't think that's possible. Maybe if we were searching in multiple containers that could be possible, but that would be arguably a new feature, extending the existing behaviour, which is beyond the scope of this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
However, because we are dealing with a case insensitive scenario, we can have multiple navigation sources in the same container whose names only differ by case. That's why we return a list.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we have navigation sources in the same container whose names only differ by case? or we can access a navigation source using different cases?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The edm model is case-sensitive by default, so it is possible to have navigation sources which only differ by case because they are different names as far as the IEdmModel
is concerned. If the IEdmModel
were case-insensitive, then this cache would not be necessary.
Since this cache is case-insensitive, for the same key we might have multiple matching entries, that's why were store the values in lists.
src/Microsoft.OData.Core/UriParser/Resolver/NormalizedModelElementsCache.cs
Show resolved
Hide resolved
src/Microsoft.OData.Core/UriParser/Resolver/NormalizedModelElementsCache.cs
Show resolved
Hide resolved
|
||
private void PopulateContainerElements(IEdmModel model) | ||
{ | ||
if (model.EntityContainer is null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can the model be null here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's the responsibility of the caller to ensure that the model is not null here. Since this is an internal class, I placed a Debug.Assert(model != null)
in the constructor.
|
||
return null; | ||
} | ||
|
||
private void PopulateSchemaElements(IEdmModel model) | ||
{ | ||
foreach (IEdmSchemaElement element in model.SchemaElements) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can the model be null here or the check is done by the caller?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is the responsibility of the caller. In this class I only placed a Debug.Assert(model != null)
in the constructor.
That said, I checked the public methods in the public ODataUriResolver
class and they're not performing any null-checks on the args.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have added null argument checks to the public methods of ODataUriResolver.
|
||
if (cachedResults.Count > 1) | ||
{ | ||
throw new ODataException(Strings.UriParserMetadata_MultipleMatchingNavigationSourcesFound(identifier)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why throw an exception if cachedResults are more than one and you said this is a possibility? or what does this mean?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This cache-based code path replicates the existing behaviour of the ResolveNavigationSource
method, which is to throw an exception if multiple matches are found.
Notice that this method returns a single IEdmNavigationSource
, not a collection. If there are multiple matches, then it means the URI resolver doesn't know which one to use and throws an exception.
|
||
if (cachedResults != null) | ||
{ | ||
return cachedResults; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why don't you do the checks for multiple operations here like you doing with navigation sources?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because this cache-based code path aims to replicate the existing behaviour of the ResolveOperationImports
method, which is to return all the matches that were found.
Unlike the FindNavigationSource
method, this method returns a collection. Keep in mind that the IEdmModel
can have multiple operation overloads with the same name but different parameters. So this is handled differently from navigation sources.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how do you check for the params? or when two operations with the same name are returned but with different params how do you know which is which?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know exactly. That is outside the scope of the cache, and possibly resolved elsewhere. The cache was only concerned in replicating the already existing behaviour in a more efficient manner.
Here's a snippet of the existing implementation (this code path will still be used when the model is not immutable):
return container.Elements.OfType<IEdmOperationImport>()
.Where(source => string.Equals(identifier, source.Name, StringComparison.OrdinalIgnoreCase));
It would be a bug if the cache returned something different from the non-cached version.
@@ -545,11 +579,11 @@ internal static ODataUriResolver GetUriResolver(IServiceProvider container) | |||
} | |||
} | |||
|
|||
private static NormalizedSchemaElementsCache GetNormalizedSchemaElementsCache(IEdmModel model) | |||
private static NormalizedModelElementsCache GetNormalizedModelElementsCache(IEdmModel model) | |||
{ | |||
Debug.Assert(model != null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why are you doing these checks here if you already did them in the constructor?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could remove it. I think someone else suggested having it here just to be sure. But I don't think it makes a major difference to have it here or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed it. Added argument null checks to public methods in this class instead.
...onalTests/Microsoft.OData.Core.Tests/UriParser/Metadata/NormalizedModelElementsCacheTests.cs
Show resolved
Hide resolved
|
||
var matches = cache.FindNavigationSources(name); | ||
|
||
Assert.Equal(2, matches.Count); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do I think this should be 1 match? EntitySet for person is Persons
and not people..but you are using people to find the navigation sources? Or there is no need of having that persons in the test
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's to matches because the test is searching for "People" (and also "people"), and there are two navigation sources that match these keywords, the "People" entity set and the "peoPle" singleton.
The reason for having "Persons" in the test is to verify that navigation sources that do not match the key are not included in the result.
...onalTests/Microsoft.OData.Core.Tests/UriParser/Metadata/NormalizedModelElementsCacheTests.cs
Outdated
Show resolved
Hide resolved
var container = model.AddEntityContainer("NS", "Container"); | ||
var entitySet1 = container.AddEntitySet("People", person); | ||
var entitySet2 = container.AddSingleton("peoPle", person); | ||
container.AddEntitySet("Persons", person); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this line,,i don't think it is necessary here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added this line to ensure that the method returns only the items that match the keyword.
To explain the rationale, let's say I had not added "Persons" to the container and only had "People" and "peoPle". Now let's say that in the implementation of cache.FindNavigationSources
I had a bug that caused every navigation source in the container to be returned, as opposed to only returning those that match. Such a bug would not be caught by this test, the test would pass because everything in the container happens to match the keyword. So, to be able to catch such bugs, I added something that does not match to the container to verify that such items are not included in the result.
...onalTests/Microsoft.OData.Core.Tests/UriParser/Metadata/NormalizedModelElementsCacheTests.cs
Outdated
Show resolved
Hide resolved
This PR has Quantification details
Why proper sizing of changes matters
Optimal pull request sizes drive a better predictable PR flow as they strike a
What can I do to optimize my changes
How to interpret the change counts in git diff output
Was this comment helpful? 👍 :ok_hand: :thumbsdown: (Email) |
NormalizedModelElementsCache cache = GetNormalizedModelElementsCache(model); | ||
IList<IEdmNavigationSource> cachedResults = cache.FindNavigationSources(identifier); | ||
|
||
if (cachedResults != null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the reason why it's preferrable to return an empty collection rather than null from a function that has a collection return type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I agree that it's generally preferrable, return an empty collection for the cache would introduce more complexity to the code than this single null-check that's not exposed to the user. Most of the null checks against the result occur in code paths where we already have an if statement checking whether the collection count is 0, so they didn't result in that much uglier code in my opinion.
To return empty collections efficiently, I would have to create a static empty list for each element type that the cache supports:
static readonly List<IEdmSchemaType> emptySchemaTypesList = new List<IEdmSchemaType>();
static readonly List<IEdmOperation> emptyOperationsList = new List<...>;
static readonly List<IEdmTerm> emptyTermsList = ...;
static readonly List<IEdmNavigationSource> emptyNavigationSourcesList = ...;
static readonly List<IEdmOperationImport> emptyOperationImportsList = ...;
// then in the find methods:
if (navigationSources.TryGetValue(...))
{
return results;
}
return emptyNavigationSourcesList;
The runtime overhead is not that bad since these collections are allocated only once, but it didn't feel worthwhile since the cache is an internal implementation detail used in a very limited scope.
That said, I don't hold this opinion strongly. If you still believe that I should return an empty collection despite the explanation above, I can go ahead and make the change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Issues
This pull request fixes #2534 .
Description
This is a follow up to PR #2610, it adds container elements (operation imports and navigation sources) to the case-insensitive cache used during case-insensitive URI resolution.
Checklist (Uncheck if it is not completed)
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.