Skip to content

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

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

Open
qwertie opened this issue Mar 9, 2022 · 8 comments
Open

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

qwertie opened this issue Mar 9, 2022 · 8 comments
Assignees

Comments

@qwertie
Copy link

qwertie commented Mar 9, 2022

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
Copy link
Member

xuzhg commented Mar 10, 2022

@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
Copy link
Author

qwertie commented Mar 14, 2022

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
Copy link
Member

xuzhg commented Mar 14, 2022

@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
Copy link
Author

qwertie commented Mar 14, 2022

How does one create/customize the resource serializer?

@xuzhg
Copy link
Member

xuzhg commented Mar 14, 2022

@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
Copy link

sNakiex commented Mar 15, 2022

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
Copy link
Contributor

Nthemba commented Mar 15, 2022

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

@Mhajkhalil
Copy link

@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.

I have implement this serailizer and i can by it , add new static paramteres to output which is really great....
but my main problem is that I dont want them be static , but they should be filled from my database shadow properties of the current table which i have in Edm model. but shadow props are dynamikc in my app. they are reading by one file called model.json and each table may have this shadow props or not.... i want my seralizer at this point daynamically find the shoadow props and add them to output result.... how can i do it ?

because when i inject databse context to this seralizer , it wont work and i get 401 error....

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants