Fix wrong links generated for derived type with navigation property when odata.metadata=full - #2592
Conversation
| if (!uri.ToString().EndsWith(string.Concat('/', this.ResourceMetadataContext.ActualResourceTypeName), StringComparison.Ordinal)) | ||
| { | ||
| uri = this.UriBuilder.AppendTypeSegment(uri, this.ResourceMetadataContext.ActualResourceTypeName); | ||
| } |
There was a problem hiding this comment.
I considered using the following logic as an alternative but was concerned about increase in allocations and the appearance of overkill:
var odataUriParser = new OData.UriParser.ODataUriParser(this.MetadataContext.Model, this.MetadataContext.ServiceBaseUri, uri);
var odataPath = odataUriParser.ParsePath();
if (!(odataPath.LastSegment is OData.UriParser.TypeSegment typeSegment && typeSegment.TargetEdmType == this.ResourceMetadataContext.ActualResourceType))
{
uri = this.UriBuilder.AppendTypeSegment(uri, this.ResourceMetadataContext.ActualResourceTypeName);
}There was a problem hiding this comment.
Eventually adopted this approach since it made it easy to cater for the scenario raised here
| if (this.ResourceMetadataContext.ActualResourceTypeName != this.ResourceMetadataContext.TypeContext.NavigationSourceEntityTypeName) | ||
| { | ||
| uri = this.UriBuilder.AppendTypeSegment(uri, this.ResourceMetadataContext.ActualResourceTypeName); | ||
| // In some scenarios, the resource Id will already contain the cast segment |
There was a problem hiding this comment.
what scenarios? a little bit details?
| { | ||
| uri = this.UriBuilder.AppendTypeSegment(uri, this.ResourceMetadataContext.ActualResourceTypeName); | ||
| // In some scenarios, the resource Id will already contain the cast segment | ||
| if (!uri.ToString().EndsWith(string.Concat('/', this.ResourceMetadataContext.ActualResourceTypeName), StringComparison.Ordinal)) |
There was a problem hiding this comment.
string.Concat does not have an overload that accepts a char as the first argument, so this method call will probably invoke an overload that takes object as an argument, which means the / char will be boxed and cause an allocation.
I suspect it would be more efficient to call string.Concat("/", ...) instead.
Here's an illustration of the IL generated for both variants: https://sharplab.io/#v2:C4LglgNgNAJiDUAfAAgJgIwFgBQyDMABGgQMIEDeOB1Rhy6ADKQPYB2AxgIbAkAWnAJwAU9JgGcAlBSo1ZyAOwEAysAFhWAcwB0JNl2BCA5AHpDUApIDcM6gF8bBB/iKMWHbirWaRrydOyycoqiOnrcQgBExhHmVg722LZAA
There was a problem hiding this comment.
Also, it may be possible to match the string ending without performing a concatenation, but maybe the code complexity this would introduce is not worth it for this edge case (and it's not guaranteed that it would perform better). e.g.,: if (foo.EndsWith(bar) && foo[foo.length - bar.length - 1] == '/' ...) (there's probably an off-by-one error in there, but you get the idea).
There was a problem hiding this comment.
This logic seems to hinge on the fact ActualResourceTypeName is a fully-qualified name, and in this context only a type segment would be a fully qualified name. Is that correct?
4576f25 to
b603cac
Compare
| if (actualResourceTypeName != this.ResourceMetadataContext.TypeContext.NavigationSourceEntityTypeName) | ||
| { | ||
| uri = this.UriBuilder.AppendTypeSegment(uri, this.ResourceMetadataContext.ActualResourceTypeName); | ||
| // In some scenarios, the resource id will already contain the cast segment. For example, |
There was a problem hiding this comment.
Have you considered a scenario where the id comes after the typesegment?
e.g ~/Customers/My.Customer(1)
This is supported in the library
There was a problem hiding this comment.
@KenitoInc I made use of the ODataUriParser and ODataPath classes to take care of this scenario. Please check the updated logic
|
“No type-cast segment is added to the canonical URL, even if the entity is an instance of a type derived from the declared entity type of its entity set.” Thanks for looking into it. |
| // when full metadata is requested and a derived entity containing one or more navigation properties | ||
| // is being serialized, and the association and navigation links need to be serialized as well, the cast | ||
| // segment will already be contained in the resource id. | ||
| string uriToString = uri.ToString(); |
…hen odata.metadata=full
6762f75
b603cac to
6762f75
Compare
|
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) |
| private readonly ODataConventionalUriBuilder uriBuilder = new ODataConventionalUriBuilder(DefaultBaseUri, | ||
| ODataUrlKeyDelimiter.Parentheses); | ||
| private readonly TestMetadataContext metadataContext = new TestMetadataContext { GetMetadataDocumentUriFunc = () => MetadataDocumentUri, GetModelFunc = () => TestModel.Model, OperationsBoundToStructuredTypeMustBeContainerQualifiedFunc = type => false }; | ||
| private readonly TestMetadataContext metadataContext = new TestMetadataContext |
There was a problem hiding this comment.
Note: The explanation for the refactor of the code in this test class in provided in the PR description
| public IEdmEntityType ProductType { get; set; } | ||
| public IEdmEntityType DerivedProductType { get; set; } | ||
| public IEdmEntityType MultipleKeyType { get; set; } | ||
| public IEdmEntityType DerivedMleMultiKeyType { get; set; } |
There was a problem hiding this comment.
Note: The explanation for the refactor of the code in this test class in provided in the PR description
|
|
||
| public ODataNavigationLinkTests() | ||
| { | ||
| var model = new EdmModel(); |
There was a problem hiding this comment.
Note: The explanation for the refactor of the code in this test class in provided in the PR description
|
Replaced by OData/AspNetCoreOData#831 |
Issues
This pull request fixes OData/AspNetCoreOData#797.
Description
Fix wrong links generated for derived type with navigation property when
odata.metadata=full.There are scenarios where the resource id already contains the cast segment but
ComputeEditLinkmethod defined inODataConventionalEntityMetadataBuilderdoes not detected that so it ends up adding an extra cast segment.When that happens, you end up with the response payload looking as follows:
{ "@odata.context": "http://localhost:5219/odata/$metadata#Customers/ODataAlternateKeySample.Models.GoldCustomer/$entity", "@odata.type": "#ODataAlternateKeySample.Models.GoldCustomer", "@odata.id": "http://localhost:5219/odata/Customers(1)/ODataAlternateKeySample.Models.GoldCustomer", "@odata.editLink": "Customers(1)/ODataAlternateKeySample.Models.GoldCustomer/ODataAlternateKeySample.Models.GoldCustomer", "Id": 1, "Name": "Tom", "CountryOrRegion": null, "Passport": null, "SSN": "SSN-1-101", "Titles@odata.type": "#Collection(String)", "Titles": [ "abc", null, "efg" ], "Contact@odata.associationLink": "http://localhost:5219/odata/Customers(1)/ODataAlternateKeySample.Models.GoldCustomer/ODataAlternateKeySample.Models.GoldCustomer/Contact/$ref", "Contact@odata.navigationLink": "http://localhost:5219/odata/Customers(1)/ODataAlternateKeySample.Models.GoldCustomer/ODataAlternateKeySample.Models.GoldCustomer/Contact" }This pull request fixes the generation of
EditLink,AssociationLinkandNagivationLinksuch that the response payload looks as follows:{ "@odata.context": "http://localhost:5219/odata/$metadata#Customers/ODataAlternateKeySample.Models.GoldCustomer/$entity", "@odata.type": "#ODataAlternateKeySample.Models.GoldCustomer", "@odata.id": "http://localhost:5219/odata/Customers(1)/ODataAlternateKeySample.Models.GoldCustomer", "@odata.editLink": "Customers(1)/ODataAlternateKeySample.Models.GoldCustomer", "Id": 1, "Name": "Tom", "CountryOrRegion": null, "Passport": null, "SSN": "SSN-1-101", "Titles@odata.type": "#Collection(String)", "Titles": [ "abc", null, "efg" ], "Contact@odata.associationLink": "http://localhost:5219/odata/Customers(1)/ODataAlternateKeySample.Models.GoldCustomer/Contact/$ref", "Contact@odata.navigationLink": "http://localhost:5219/odata/Customers(1)/ODataAlternateKeySample.Models.GoldCustomer/Contact" }Explanation for the refactor of code in test classes
The code in the following test classes has been significantly refactored:
ODataConventionalEntityMetadataBuilderTestsODataConventionalUriBuilderTestsODataNavigationLinkTestsTestModelSubsequent to the introduction of
ODataUriParserandODataPathclasses to determine whether the resource id already contains the cast segment, tests in some classes started failing. An investigation revealed that in most scenarios, the test logic was impractical.In the
ODataConventionalEntityMetadataBuilderTeststest class for example, the tests for multi-property key were impractical since the entity type that the tests were based upon wasn't defined with a multi-property key. A refactor was therefore necessary since theODataUriParservalidates the supplied keys against the Edm model. The test class setup code had a lot of logical inconsistencies.In the
ODataNavigationLinkTeststest class, the refactor involved setting the delegates that are invoked to determine the model and the base Uri.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.