It's vital that if an entity has a required navigation property, that it be
set when the entity is created. For example, the Group property of Item:
public class Item
{
[Key]
public int Id { get; set; }
[Required]
public string Name { get; set; }
[Required]
public virtual Group Group { get; set; }
}
public class Group
{
[Key]
public int Id { get; set; }
[Required]
public string Name { get; set; }
}
If this example, there is currently no way to POST such an entity to an
ODataController method and reference the required group. The @odata.bind
annotation is meant to provide this functionality, but
it is not currently implemented. As such, there is no support for this kind of
modelling and the current workaround of creating the link after the entity is
created cannot work as the item cannot be created with an assigned group.
Original CodePlex Issue: Issue 2163
Status: Proposed
Reason Closed: Unassigned
Assigned to: Unassigned
Reported on: Oct 21, 2014 at 10:30 AM
Reported by: Barguast
Updated on: Tue at 7:24 AM
Updated by: cysu
On _2014-11-03 17:50:10 UTC_, ShahzorKhan commented:
Currently we are overriding the OData entity Serializer to override ApplyNavigationProperty.
public override void ApplyNavigationProperty(object entityResource, ODataNavigationLinkWithItems navigationLinkWrapper,
IEdmEntityTypeReference entityType, ODataDeserializerContext readContext)
{
if ((entityResource is EdmEntityObject) && (navigationLinkWrapper.NestedItems.OfType
{
string navigationPropertyName = navigationLinkWrapper.NavigationLink.Name;
string url = navigationLinkWrapper.NestedItems.OfType
// Store url on the entity, and handle the response accordingly.
}
else
{
base.ApplyNavigationProperty(entityResource, navigationLinkWrapper, entityType, readContext);
}
}
But having a correct fix from WebAPI team is appreaciated.
On _2015-01-06 15:24:43 UTC_, cysu commented:
For v5.6.
Another possible implementation - this time the relationship Uri is automatically stored in a property called after the navigation property, but with a "Ref" suffix. Such property should be typically marked as [NotMapped].
public class ExtendedODataEntityDeserializer : ODataEntityDeserializer
{
public ExtendedODataEntityDeserializer(
ODataDeserializerProvider deserializerProvider)
: base(deserializerProvider)
{
}
public override void ApplyNavigationProperty(
object entityResource,
ODataNavigationLinkWithItems navigationLinkWrapper,
IEdmEntityTypeReference entityType,
ODataDeserializerContext readContext)
{
base.ApplyNavigationProperty(
entityResource,
navigationLinkWrapper,
entityType,
readContext);
foreach (var childItem in navigationLinkWrapper.NestedItems)
{
var entityReferenceLink = childItem as ODataEntityReferenceLinkBase;
if (entityReferenceLink != null)
{
var navigationPropertyName = navigationLinkWrapper.NavigationLink.Name;
Uri referencedEntityUrl = entityReferenceLink.EntityReferenceLink.Url;
if (!referencedEntityUrl.IsAbsoluteUri)
{
referencedEntityUrl = new Uri(readContext.Request.RequestUri, referencedEntityUrl);
}
// Looks for Uri property named "{NavigationPropertyName}Ref"
var refProperty = entityResource.GetType().GetProperties()
.FirstOrDefault(p => p.Name.Equals(navigationPropertyName + "Ref", StringComparison.InvariantCultureIgnoreCase)
&& p.PropertyType.Equals(typeof(Uri)));
if (refProperty != null)
{
refProperty.SetValue(entityResource, referencedEntityUrl);
}
}
}
}
}
I wonder why there is so little discussion on this issue. The Microsoft.OData.Client generates @odata.bind statements when trying to link to an existing entity and skips the ~/ref request. But web api OData does not support @odata.bind. Therefore Microsoft.OData.Client is quite useless right now...
Because I have to use Microsoft.OData.Client, I'm trying to use vojtechvit's ExtendedODataEntityDeserializer, but I don't know an elegant way to get the key of the referenced entity from the stored Uri. Any hints?
Edit: I'm now using the Helper class from http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/odata-v4/entity-relations-in-odata-v4 to get a key from an Uri.
I would love to see this implemented especially since it's generated by the Microsoft.OData.Client. As it stands right now I have to Add the parent item in one call and add the navigation references in a separate call.
Currently running into this issue of this not being supported. The client I am using uses @odata.bind and now an annoying workaround is needed. Please look into this issue.
I also support for this feature...
+1 to support for this feature
+1 How this has been neglected from Oct 21, 2014 is beyond me.
While this is being implemented, I post here our current solution which is an improved version of what I posted above (it stores entity reference URLs in properties called "XxxRef" or "XxxRefs" for collections).
/// <summary>
/// Extended <see cref="ODataEntityDeserializer"/> that supports additional deserialization scenarios
/// like support for <c>@odata.bind</c>.
/// </summary>
public class ExtendedODataEntityDeserializer : ODataEntityDeserializer
{
/// <summary>
/// Initializes a new instance of the <see cref="ExtendedODataEntityDeserializer"/> class.
/// </summary>
/// <param name="deserializerProvider">The deserializer provider.</param>
public ExtendedODataEntityDeserializer(
ODataDeserializerProvider deserializerProvider)
: base(deserializerProvider)
{
}
/// <inheritdoc />
public override void ApplyNavigationProperty(
object entityResource,
ODataNavigationLinkWithItems navigationLinkWrapper,
IEdmEntityTypeReference entityType,
ODataDeserializerContext readContext)
{
if (entityResource == null)
{
throw new ArgumentNullException(nameof(entityResource));
}
if (navigationLinkWrapper == null)
{
throw new ArgumentNullException(nameof(navigationLinkWrapper));
}
if (readContext == null)
{
throw new ArgumentNullException(nameof(readContext));
}
// The base behaviour is good for "deep inserts",
// but intentionally skips references.
base.ApplyNavigationProperty(
entityResource,
navigationLinkWrapper,
entityType,
readContext);
foreach (var childItem in navigationLinkWrapper.NestedItems)
{
var entityReferenceLink = childItem as ODataEntityReferenceLinkBase;
if (entityReferenceLink != null)
{
var navigationPropertyName = navigationLinkWrapper.NavigationLink.Name;
Uri referencedEntityUrl = entityReferenceLink.EntityReferenceLink.Url;
if (!referencedEntityUrl.IsAbsoluteUri)
{
referencedEntityUrl = new Uri(readContext.Request.RequestUri, referencedEntityUrl);
}
if (navigationLinkWrapper.NavigationLink.IsCollection.HasValue && navigationLinkWrapper.NavigationLink.IsCollection.Value)
{
// Looks for Uri property named "{NavigationPropertyName}Refs"
var refsProperty = entityResource.GetType().GetProperties()
.FirstOrDefault(p => p.Name.Equals(navigationPropertyName + "Refs", StringComparison.OrdinalIgnoreCase)
&& p.PropertyType.Equals(typeof(IList<Uri>)));
if (refsProperty != null)
{
var refs = (IList<Uri>)refsProperty.GetValue(entityResource);
if (refs == null)
{
refs = new List<Uri>();
refsProperty.SetValue(entityResource, refs);
}
refs.Add(referencedEntityUrl);
}
}
else
{
// Looks for Uri property named "{NavigationPropertyName}Ref"
var refProperty = entityResource.GetType().GetProperties()
.FirstOrDefault(p => p.Name.Equals(navigationPropertyName + "Ref", StringComparison.OrdinalIgnoreCase)
&& p.PropertyType.Equals(typeof(Uri)));
if (refProperty != null)
{
refProperty.SetValue(entityResource, referencedEntityUrl);
}
}
}
}
}
}
And below some helper extensions to parse those @odata.bind URLs:
/// <summary>
/// Defines OData-related extension methods for <see cref="HttpRequestMessage"/>.
/// </summary>
public static class HttpRequestMessageExtensions
{
/// <summary>
/// Helper method to get the OData path for an arbitrary OData URI.
/// </summary>
/// <param name="request">The request instance in current context.</param>
/// <param name="uri">The OData URI.</param>
/// <returns>The parsed OData path.</returns>
/// <remarks>
/// Useful to parse and work with entity references, e.g. when implementing $link endpoints or when using <c>@odata.bind</c>.
/// </remarks>
/// <example>
/// <code>
/// [EnableQuery]
/// public IHttpActionResult Post([FromBody] Invitation invitation)
/// {
/// if (!ModelState.IsValid)
/// {
/// return this.ODataBadRequest(ModelState);
/// }
///
/// if (invitation.Inviter != null)
/// {
/// return this.ForbiddenOperation(new ODataErrorDetail
/// {
/// Message = "Deep insert of inviter is not allowed on this endpoint. Please use @odata.bind to reference a User from the 'users' entity set instead.",
/// Target = "invitation.inviter"
/// });
/// }
///
/// int? inviterId = null;
///
/// if (invitation.InviterRef != null)
/// {
/// var inviterPath = Request.CreateODataPath(invitation.InviterRef);
///
/// if (!inviterPath.NavigationSource.Name.Equals("users", StringComparison.InvariantCultureIgnoreCase))
/// {
/// return this.ForbiddenOperation(new ODataErrorDetail
/// {
/// Message = "Inviter must be a User entity from the 'users' entity set.",
/// Target = "invitation.inviter"
/// });
/// }
///
/// inviterId = Request.GetKeyValue<int>(invitation.InviterRef);
/// }
///
/// invitation.Token = _invitationService.Create(
/// invitation.EmailAddress,
/// invitation.Topic,
/// inviterId);
///
/// return Created(invitation);
/// }
/// </code>
/// </example>
public static ODataPath CreateODataPath(this HttpRequestMessage request, Uri uri)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}
if (uri == null)
{
throw new ArgumentNullException(nameof(uri));
}
uri = ODataUriHelper.UriEncodeStringLiterals(uri);
using (var newRequest = new HttpRequestMessage(HttpMethod.Get, uri))
{
var route = request.GetRouteData().Route;
var newRoute = new HttpRoute(
route.RouteTemplate,
new HttpRouteValueDictionary(route.Defaults),
new HttpRouteValueDictionary(route.Constraints),
new HttpRouteValueDictionary(route.DataTokens),
route.Handler);
var routeData = newRoute.GetRouteData(
request.GetConfiguration().VirtualPathRoot,
newRequest);
if (routeData == null)
{
throw new InvalidOperationException(
string.Format(
CultureInfo.CurrentCulture,
"The provided URI '{0}' is not a valid OData link.",
uri.OriginalString));
}
var path = newRequest.ODataProperties().Path;
return new ODataPath(path.Segments.Select(UriDecodeStringLiterals).ToList());
}
}
/// <summary>
/// Helper method to get the key value from a URI.
/// </summary>
/// <typeparam name="TKey">The type of the key.</typeparam>
/// <param name="request">The request instance in current context.</param>
/// <param name="uri">OData uri that contains the key value.</param>
/// <returns>The key value.</returns>
/// <remarks>
/// Usually used by $link action or actions working with <c>@odata.bind</c>
/// to extract the key value from the URL in body.
/// </remarks>
/// <example>
/// <code>
/// [EnableQuery]
/// public IHttpActionResult Post([FromBody] Invitation invitation)
/// {
/// if (!ModelState.IsValid)
/// {
/// return this.ODataBadRequest(ModelState);
/// }
///
/// if (invitation.Inviter != null)
/// {
/// return this.ForbiddenOperation(new ODataErrorDetail
/// {
/// Message = "Deep insert of inviter is not allowed on this endpoint. Please use @odata.bind to reference a User from the 'users' entity set instead.",
/// Target = "invitation.inviter"
/// });
/// }
///
/// int? inviterId = null;
///
/// if (invitation.InviterRef != null)
/// {
/// var inviterPath = Request.CreateODataPath(invitation.InviterRef);
///
/// if (!inviterPath.NavigationSource.Name.Equals("users", StringComparison.InvariantCultureIgnoreCase))
/// {
/// return this.ForbiddenOperation(new ODataErrorDetail
/// {
/// Message = "Inviter must be a User entity from the 'users' entity set.",
/// Target = "invitation.inviter"
/// });
/// }
///
/// inviterId = Request.GetKeyValue<int>(invitation.InviterRef);
/// }
///
/// invitation.Token = _invitationService.Create(
/// invitation.EmailAddress,
/// invitation.Topic,
/// inviterId);
///
/// return Created(invitation);
/// }
/// </code>
/// </example>
public static TKey GetKeyValue<TKey>(this HttpRequestMessage request, Uri uri)
{
if (uri == null)
{
throw new ArgumentNullException(nameof(uri));
}
//get the odata path Ex: ~/entityset/key/$links/navigation
var odataPath = request.CreateODataPath(uri);
var keySegment = odataPath.Segments.OfType<KeyValuePathSegment>().LastOrDefault();
if (keySegment == null)
{
throw new InvalidOperationException(
string.Format(
CultureInfo.CurrentCulture,
"The provided URI '{0}' does not contain a key.",
uri.OriginalString));
}
var value = ODataUriUtils.ConvertFromUriLiteral(keySegment.Value, ODataVersion.V4);
return (TKey)value;
}
}
When I tried to use the @vojtechvit deserializer in my v6.0.0 OData Web API project I discovered that there were quite a few breaking changes introduced. For anyone else looking for a v6 implementation of this workaround, here is my updated version.
using Microsoft.OData.Edm;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.OData.Formatter.Deserialization;
public class EntityReferenceODataResourceDeserializer : ODataResourceDeserializer
{
public EntityReferenceODataResourceDeserializer(ODataDeserializerProvider deserializerProvider) : base(deserializerProvider)
{
}
public override void ApplyNestedProperty(
object entityResource,
ODataNestedResourceInfoWrapper resourceInfoWrapper,
IEdmStructuredTypeReference structuredType,
ODataDeserializerContext readContext)
{
base.ApplyNestedProperty(entityResource, resourceInfoWrapper, structuredType, readContext);
foreach (var childItem in resourceInfoWrapper.NestedItems)
{
var entityReferenceLink = childItem as ODataEntityReferenceLinkBase;
if (entityReferenceLink != null)
{
var navigationPropertyName = resourceInfoWrapper.NestedResourceInfo.Name;
Uri referencedEntityUrl = entityReferenceLink.EntityReferenceLink.Url;
if (!referencedEntityUrl.IsAbsoluteUri)
{
referencedEntityUrl = new Uri(readContext.Request.RequestUri, referencedEntityUrl);
}
if (resourceInfoWrapper.NestedResourceInfo.IsCollection.HasValue && resourceInfoWrapper.NestedResourceInfo.IsCollection.Value)
{
// Looks for Uri property named "{NavigationPropertyName}Refs"
var refsProperty = entityResource.GetType().GetProperties()
.FirstOrDefault(p => p.Name.Equals(navigationPropertyName + "Refs", StringComparison.OrdinalIgnoreCase)
&& p.PropertyType.Equals(typeof(IList<Uri>)));
if (refsProperty != null)
{
var refs = (IList<Uri>)refsProperty.GetValue(entityResource);
if (refs == null)
{
refs = new List<Uri>();
refsProperty.SetValue(entityResource, refs);
}
refs.Add(referencedEntityUrl);
}
}
else
{
// Looks for Uri property named "{NavigationPropertyName}Ref"
var refProperty = entityResource.GetType().GetProperties()
.FirstOrDefault(p => p.Name.Equals(navigationPropertyName + "Ref", StringComparison.OrdinalIgnoreCase)
&& p.PropertyType.Equals(typeof(Uri)));
if (refProperty != null)
{
refProperty.SetValue(entityResource, referencedEntityUrl);
}
}
}
}
}
}
public class EntityReferenceODataDeserializerProvider : DefaultODataDeserializerProvider
{
private EntityReferenceODataResourceDeserializer _entityReferenceODataResourceDeserializer;
public EntityReferenceODataDeserializerProvider(IServiceProvider rootContainer) : base(rootContainer)
{
_entityReferenceODataResourceDeserializer = new EntityReferenceODataResourceDeserializer(this);
}
public override ODataEdmTypeDeserializer GetEdmTypeDeserializer(IEdmTypeReference edmType)
{
if (edmType.IsEntity())
{
return _entityReferenceODataResourceDeserializer;
}
return base.GetEdmTypeDeserializer(edmType);
}
}
and in WebApiConfig.cs, you'll need to register the deserializer as follows:
var containerBuilder = new DefaultContainerBuilder();
config.UseCustomContainerBuilder(() => containerBuilder);
config.EnableDependencyInjection();
var serviceProvider = containerBuilder.BuildContainer();
var oDataSerializerProvider = new DefaultODataSerializerProvider(serviceProvider);
var oDataDeserializerProvider = new EntityReferenceODataDeserializerProvider(serviceProvider);
var odataFormatters = ODataMediaTypeFormatters.Create(oDataSerializerProvider, oDataDeserializerProvider);
config.Formatters.InsertRange(0, odataFormatters);
The workaround from @peter172sp above broke again as ODataMediaTypeFormatters.Create() no longer takes Serializer arguments as of commit 46fb5319374438b6e3a55f82f97e30549ba7cd42 by @robward-ms. I have not yet found a solution that works with the current version (7.0).
@dbenzhuser - In that commit, look at ODataFormatterTests.cs for how inject a custom deserializer/deserializer provider. You can still use a custom DeserializerProvider but it's injected through DI instead of injecting it through ODataMediaTypeFormatters.
Is anyone working on that? JOtherwise i may pick it up becasue like so many things, we have data models with integrity (which somehow the odata team does not seem to have) and that means certain relationships MUST be set at insert time, period. And like so many issues, it seems that for every line of OData relevant code I write, I find basic functionality not working.
@robward-ms Can you get me started with where I would properly put that in? I would assume this is not trivial - after all, getting the entity is harder and I will sort of have to either make a http request (using same credentials) or somehow bypass the tcp level and issue that request into the asp.net processing stack to get the entity out.
Hello,
What are the other possibilities to create an entity which links to an existing related mandatory entity ? (in my case, offer must link to an already existing category).
Did anyone manage to make this work with version 7.0 ? (see here : https://stackoverflow.com/questions/51825765/odata-webapi-how-to-inject-a-odataresourcedeserializer-in-7-0-core)
With this fix, do we use the @odata.bind POST syntax ?
For me, Odata WebAPI is not suitable for production without this kind of basic features.
Any process for @odata.bind ? It is really the basic feature for production, why pending this so many years?
This issue was opened in 2015 ... Will there be a fix very soon ? Thanks
I have it working actually. I had to write my own serializer that extracts the bind into a uri object on the backend and then i have to write my own code to get an instance of the entity (which DOES Make sense because how would a controller know whether to access that object). It is a couple of pages of code and a lot of additional properties that are [NotMapped] and follow a naming pattern (EntityProperty -> EntityPropertyRef) so the deserializer knows where to put things. My solution actually is using a separate internal HTTP request for getting a copy of the object - so the controller of related objects executes. Which is needed because there I can do things like check permissions. Tricky as ** and I would consider this not so much a bug as a missing feature at this point.
Not sure I would expect the OData tem to ever fix this - they just with core broke the whole serialization hierarchy so that now you get serialization problems for complex types that a property can not be another complex type. Which tells you the property can only be the foollowing types, which includes complex types. No idea whether the concept of "unit tests" and "fix bugs first" ever arrived there, but this stuff is seriously more critical because right now you can not even implement a quarter of the odata spec that once worked ;(
Thanks. Is an example of your solution available somewhere ? does it work with Odata v7?
Not really. I was thinking of putting that up on github in a library to add - will see next week. Just in the process of trying to get it work on dotnetcore 3.1. Once that is done, i may do it.
Ok, upate - have it working, isolating a lot of stuff into separate apps but not able to get it working in patches. Will likely open a separate bug here to ask for help. So far it works in patch because i rewind the input stream an manually pull the bindings out there - patch checks that an item is in the list of non navigation properties, otherwise throws the data away. And I have had no luck replacing patch yet.
Most helpful comment
+1 How this has been neglected from Oct 21, 2014 is beyond me.