OData / AspNetCoreOData

ASP.NET Core OData: A server library built upon ODataLib and ASP.NET Core
Other
447 stars 158 forks source link

How do I define custom/dynamic OData columns/fields/properties? #517

Open qwertie opened 2 years ago

qwertie commented 2 years ago

I realize that there is a sample here for creating 100% dynamic models. However, in our case we have an existing model that we are happy with, but we want to add custom fields to it.

In other words, we want an OData model with entities that have some fixed columns/properties and some dynamically-generated columns/properties that come from the database, like this:

public class ODataEntity
{
    [Key]
    public int Id { get; set; }
    public string Name { get; set; } = "";

    // From the perspective of Power BI, this should produce a series of additional columns 
    // (the columns are the same on all instances, but the schema can change at any time)
    public Dictionary<string, object> CustomFields { get; set; }
}

To my tremendous surprise, the key-value pairs in CustomFields become properties in the JSON output (i.e. there is no CustomFields column; its contents are inserted into the parent object). However, the custom fields are not recognized by Power BI:

image

I assume that this is because there is no metadata for the custom fields in https://.../odata/$metadata. So my question is:

  1. How can I modify the following code so that the custom columns are included in the IEdmModel?

    static IEdmModel GetEdmModel(params CustomFieldDef[] customFields)
    {
        var builder = new ODataConventionModelBuilder() {
            Namespace = "Namespace",
            ContainerName = "Container", // no idea what this is for
        };
        builder.EntitySet<ODataEntity>("objects");
    
        return builder.GetEdmModel();
    }
    
    public class CustomFieldDef {
        public string FieldName;
        public Type Type;
    }
  2. How can I modify the following startup code so that the IEdmModel is regenerated every time https://.../odata/$metadata is accessed?

    IMvcBuilder mvc = builder.Services.AddControllers();
    
    mvc.AddOData(opt => opt.AddRouteComponents("odata", GetEdmModel())
       .Select().Filter().OrderBy().Count().Expand().SkipToken());

Edit: mirrored on SO

xuzhg commented 2 years ago

@qwertie

for the #1, you can do like (pseudocode codes).

....
EdmModel model = builder.GetEdmModel() as EdmModel;

var entityType = model.SchemaElements.OfType<IEdmEntityType>().First(e => e.Name == "ODataEntity");

IODataTypeMapper mapper = model.GetTypeMapper();
foreach (var fds in cusomFields)
{
     // get the Edm type from the CLR type, make sure the corresponding Edm type is defined already in the model.
     var fdEdmType = mapper.GetEdmTypeReference(model, fds.Type);

     entityType.AddStructuralProperty(fds.Name, fdEdmType);
}

return model;

For your second question, can you share more details about what you want to do?

qwertie commented 2 years ago

Thank you. Unfortunately, code like that doesn't work. The problem is that when the data endpoint is accessed, Microsoft.AspNetCore.OData.Formatter.ResourceContext.GetPropertyValue is called for every property in the EDM model, which calls TypedEdmStructuredObject.TryGetPropertyValue, which returns null because it's not a CLR property, and then ResourceContext.GetPropertyValue throws an exception like InvalidOperationException: 'The EDM instance of type '[ODataEntity Nullable=True]' is missing the property 'ExampleCustomField'.'

For your second question, can you share more details about what you want to do?

It's very simple. Sometimes a user will create (or remove) custom fields. When this happens, the metadata returned from https://.../odata/$metadata must also change.

xuzhg commented 2 years ago

@qwertie For the first issue, you can create/customize the resource serializer to provide the property value.

For the second, you can replace the built-in MetadataRoutingConvention to provide the change model.

qwertie commented 2 years ago

How does one create/customize the resource serializer?

xuzhg commented 2 years ago

@qwertie https://devblogs.microsoft.com/odata/build-formatter-extensions-in-asp-net-core-odata-8-and-hooks-in-odataconnectedservice/ is a post illustrating how to create the serializer. Let me know if it can't work and need further help.

sNakiex commented 2 years ago

The dynamic example gives you all the code you need to have dynamic metadata generation, the class you are looking for that ties it together is MyODataRoutingMatcherPolicy. As for the model generation I ended up using the TypeExtender nuget package and used that to add the additional fields to the base class.

Nthemba commented 2 years ago

When creating your OData connection to the feed are you selecting the checkbox to include open type properties MicrosoftTeams-image .