OData / OData/AspNetCoreOData

Instance Annotations for properties with $select query option

Open
#1,198 1 comment 0 reactions 1 assignee View on GitHub

@xuzhg is already working on this.

Since Mar 26, 2024.

follow-up investigated
Dominant language
C#
Stars
505
Forks
186
PR merge metrics
No merged PRs in 30d

Description

Describe the bug
I would like to add instance annotation to a property (OwnerId) by including an additional property (OwnerIdName) as its value.
This setup works as expected without using the $select query option. However, when $select is applied, the resourceContext.ResourceInstance does not include the OwnerIdName property, leading to a null reference error because the OwnerIdName is not fetched and thus cannot be added as an instance annotation.

I am seeking advice on how to ensure OwnerIdName is included as an instance annotation for OwnerId when $select is applied, or how to work around this issue

Code
I've created:

  • ODataSerializerProvider and ODataResourceSerializer to provide custom serialization and add annotation.

  • IEdmModel, where I add OwnerIdName as ignored property,

  • Simple Controller to consume client request.

Below i've attached my codes, result without $select and error with $select param.
Additionally, i've also added codes to my repo.

public class CustomODataSerializerProvider : ODataSerializerProvider
    {
        private IServiceProvider _rootProvider;

        public CustomODataSerializerProvider(IServiceProvider rootContainer) : base(rootContainer)
        {
            _rootProvider = rootContainer;
        }

        public override IODataEdmTypeSerializer GetEdmTypeSerializer(IEdmTypeReference edmType)
        {
            if (edmType.Definition.TypeKind == EdmTypeKind.Entity)
            {
                return new CustomODataResourceSerializer(this);
            }

            return base.GetEdmTypeSerializer(edmType);
        }
    }
    public class CustomODataResourceSerializer(IODataSerializerProvider provider) : ODataResourceSerializer(provider)
    {
        public override ODataResource CreateResource(SelectExpandNode selectExpandNode, ResourceContext resourceContext)
        {
            var resource = base.CreateResource(selectExpandNode, resourceContext);
            if (resource != null)
            {
                var resourceContextPropDictionary = GetPropertiesDictionary(resourceContext.ResourceInstance);

                foreach (var prop in resource.Properties)
                {
                    var propNameToLower = prop.Name.ToLower();

                    if (resourceContextPropDictionary.TryGetValue($"{propNameToLower}name", out object lookupNamePropValue))
                    {
                        prop.InstanceAnnotations.Add(new ODataInstanceAnnotation("lookup.name", new ODataPrimitiveValue(lookupNamePropValue)));
                    }
                }
            }
            return resource;
        }

        private Dictionary<string, object> GetPropertiesDictionary(object obj)
        {
            Dictionary<string, object> propertiesDictionary = new Dictionary<string, object>();
            PropertyInfo[] properties = obj.GetType().GetProperties();

            foreach (var property in properties)
            {
                object value = property.GetValue(obj);
                propertiesDictionary.Add(property.Name.ToLower(), value);
            }

            return propertiesDictionary;
        }
    }
    public static IEdmModel CreateEdmModel()
    {
        var builder = new ODataConventionModelBuilder();

        var accountConfiguration = builder.EntitySet<Account>("Accounts").EntityType;
        accountConfiguration.Ignore(x => x.OwnerIdName);

        return builder.GetEdmModel();
    }
builder.Services.AddControllers()
    .AddOData(opt => opt.AddRouteComponents("odata", EdmModelBuilder.CreateEdmModel(), 
            builder => builder.AddSingleton<IODataSerializerProvider, CustomODataSerializerProvider>()).EnableQueryFeatures());
    public class Account
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
        public Guid OwnerId { get; set; }
        public string OwnerIdName { get; set; }
    }

    public class AccountsController() : ODataController
    {
        private static readonly IList<Account> accounts =
        [
            new() { Id = Guid.NewGuid(), Name = "Interstellar Mining Corp", OwnerId = Guid.NewGuid(), OwnerIdName = "User 1" },
            new() { Id = Guid.NewGuid(), Name = "Quantum Communications", OwnerId = Guid.NewGuid(), OwnerIdName = "User 2" },
            new() { Id = Guid.NewGuid(), Name = "Galaxy Graphics", OwnerId = Guid.NewGuid(), OwnerIdName = "User 3" }
        ];

        [HttpGet]
        [EnableQuery]
        public IEnumerable<Account> Get()
        {
            return accounts;
        }
    }

Result without $select param:

{
    "@odata.context": "https://localhost:7243/odata/$metadata#Accounts",
    "value": [
        {
            "Id": "fb752e65-4649-447d-859e-bc3d5349edee",
            "Name": "Interstellar Mining Corp",
            "OwnerId@lookup.name": "User 1",
            "OwnerId": "f04b6814-f409-467e-84bf-f993bff69d7b"
        },
        {
            "Id": "1bbfc7fe-04ae-4b4a-adfc-b77e8251cd67",
            "Name": "Quantum Communications",
            "OwnerId@lookup.name": "User 2",
            "OwnerId": "80d96bd6-74cd-4970-b071-dc10264b3aeb"
        },
        {
            "Id": "7c50381d-0007-48b1-9e24-b04c249f7004",
            "Name": "Galaxy Graphics",
            "OwnerId@lookup.name": "User 3",
            "OwnerId": "a71672ac-9e39-446a-bf1d-99775d2178a8"
        }
    ]
}

Error with $select param (line 23 in file CustomODataResourceSerializer.cs):

System.ArgumentNullException: 'Cannot create an ODataPrimitiveValue from null; use ODataNullValue instead.'

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.