OData / AspNetCoreOData

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

Dynamic OData Model w/ EnableQuery #793

Open pluc77 opened 1 year ago

pluc77 commented 1 year ago

Discussed in https://github.com/OData/AspNetCoreOData/discussions/789

Expansion for dynamic models when using the EnableQuery attribute generates a runtime exceptions like so: The query specified in the URI is not valid. Could not find a property named 'Items' on type 'Microsoft.AspNetCore.OData.Formatter.Value.IEdmEntityObject'. The exception only occurs when the EnableQuery attribute is present. With it removed all requests process without issue, but expansion beyond "level 2" for self-referencing navigation properties is problematic. I've stepped through the OData Lib and observed behavior within Microsoft.OData.UriParser's SelectExpandBinder class. The GenerateExpandItem method contained therein is unable to resolve properties for the dynamically generated models (IEdmEntityObject) returned from my data source. A working solution is attached. To reproduce the issue, follow these steps: 1. Remove the EnableQuery attribute from the lone controller method and invoke the following URL: http://localhost:4527/odata/ns/Items?$expand=Detail,Items($expand=Detail,Items($expand=Detail,Items($expand=Detail,Items))) 2. Make note of the levels contained with the previous response before invoking the following URL: http://localhost:4527/odata/ns/Items?$expand=Items($levels=10;$expand=Detail),Detail 3. Note that the previous response expanded 2 levels only. 4. Add the EnableQuery attribute back to the controller method and re-invoke the prior URLs, at which time the aforementioned exception will occur.

ODataDynamicModel.zip

xuzhg commented 1 year ago

@pluc77 Historically, Typeless (NO CLR Types) scenarios can't work with Query. If you want to make it working, you can customize the IFilterBinder, ISelectExpandBinder.

pluc77 commented 1 year ago

Sam, thank you so much for the comment! Pardon my ignorance, but is there a way to simply bypass and/or remove the default IFilterBinder and ISelectExpandBinder so that model in its current form can simply pass thru? I've yet to succeed in getting a custom binder properly injected with the dynamic model approach. I'm assuming the proper place for doing so is within the IApplicationModelProvider implementation, but no matter what I try the default ISelectExpandBinder implementation is still firing. I've tried something along the following lines:

public ApplicationModelProvider(IOptions<ODataOptions> options)
        {
            options.Value.AddRouteComponents(
                "odata/{namespace}",
                EdmCoreModel.Instance,
                s =>
                {
                    s.RemoveAll<ISelectExpandBinder>();
                    s.RemoveAll<IFilterBinder>();
                    s.TryAddSingleton<ISelectExpandBinder, OData.SelectExpandBinder>();
                    s.TryAddSingleton<IFilterBinder, OData.FilterBinder>();
                });
        }
xuzhg commented 1 year ago

For your reference: https://devblogs.microsoft.com/odata/customizing-filter-for-spatial-data-in-asp-net-core-odata-8/#inject-customfilterbinder-into-di-container

simply pass through?

Can you share your link for the repo?

pluc77 commented 1 year ago

Good morning, sir!

I actually just attached a working solution to my original post per the below screenshot. [image: image.png]

I've tried injecting a custom SelectExpandBinder both from Startup and ApplicationModelProvider's constructor. In both cases the custom binder is ignored and the default implementation executes instead.

I've tried this from Startup:

public void ConfigureServices(IServiceCollection services) { services.AddControllers().AddOData( o => o.EnableQueryFeatures().AddRouteComponents( "odata/{namespace}", EdmCoreModel.Instance, s => { //s.RemoveAll(); //s.RemoveAll(); s.TryAddSingleton<ISelectExpandBinder, OData.SelectExpandBinder>(); s.TryAddSingleton<IFilterBinder, OData.FilterBinder>(); }));

        services.TryAddTransient<IModelProvider, ModelProvider>();

        services.TryAddSingleton<IDataSourceStore,

EntityDataSourceStore>();

        services.TryAddEnumerable(
            ServiceDescriptor.Transient<IApplicationModelProvider,

ApplicationModelProvider>());

        services.TryAddEnumerable(
            ServiceDescriptor.Singleton<MatcherPolicy,

EntityRoutingMatcherPolicy>()); }

I've tried this from ApplicationModelProvider's constructor:

public ApplicationModelProvider(IOptions options) { options.Value.AddRouteComponents( "odata/{namespace}", EdmCoreModel.Instance, s => { s.RemoveAll(); s.RemoveAll(); s.TryAddSingleton<ISelectExpandBinder, OData.SelectExpandBinder>(); s.TryAddSingleton<IFilterBinder, OData.FilterBinder>(); }); }

Any thoughts on what I'm missing?

Many thanks, Paul

On Wed, Jan 11, 2023 at 4:53 PM Sam Xu @.***> wrote:

For your reference: https://devblogs.microsoft.com/odata/customizing-filter-for-spatial-data-in-asp-net-core-odata-8/#inject-customfilterbinder-into-di-container simply pass through?

Can you share your link for the repo?

— Reply to this email directly, view it on GitHub https://github.com/OData/AspNetCoreOData/issues/793#issuecomment-1379533548, or unsubscribe https://github.com/notifications/unsubscribe-auth/A44VQ4BLYRNRQENJXTWGMCDWR4TU5ANCNFSM6AAAAAATQC5GNQ . You are receiving this because you were mentioned.Message ID: @.***>

xuzhg commented 1 year ago

Good morning, sir! I actually just attached a working solution to my original post per the below screenshot. [image: image.png] I've tried injecting a custom SelectExpandBinder both from Startup and ApplicationModelProvider's constructor. In both cases the custom binder is ignored and the default implementation executes instead. I've tried this from Startup: public void ConfigureServices(IServiceCollection services) { services.AddControllers().AddOData( o => o.EnableQueryFeatures().AddRouteComponents( "odata/{namespace}", EdmCoreModel.Instance, s => { //s.RemoveAll(); //s.RemoveAll(); s.TryAddSingleton<ISelectExpandBinder, OData.SelectExpandBinder>(); s.TryAddSingleton<IFilterBinder, OData.FilterBinder>(); })); services.TryAddTransient<IModelProvider, ModelProvider>(); services.TryAddSingleton<IDataSourceStore, EntityDataSourceStore>(); services.TryAddEnumerable( ServiceDescriptor.Transient<IApplicationModelProvider, ApplicationModelProvider>()); services.TryAddEnumerable( ServiceDescriptor.Singleton<MatcherPolicy, EntityRoutingMatcherPolicy>()); } I've tried this from ApplicationModelProvider's constructor: public ApplicationModelProvider(IOptions options) { options.Value.AddRouteComponents( "odata/{namespace}", EdmCoreModel.Instance, s => { s.RemoveAll(); s.RemoveAll(); s.TryAddSingleton<ISelectExpandBinder, OData.SelectExpandBinder>(); s.TryAddSingleton<IFilterBinder, OData.FilterBinder>(); }); } Any thoughts on what I'm missing? Many thanks, Paul On Wed, Jan 11, 2023 at 4:53 PM Sam Xu @.> wrote: For your reference: https://devblogs.microsoft.com/odata/customizing-filter-for-spatial-data-in-asp-net-core-odata-8/#inject-customfilterbinder-into-di-container simply pass through? Can you share your link for the repo? — Reply to this email directly, view it on GitHub <#793 (comment)>, or unsubscribe https://github.com/notifications/unsubscribe-auth/A44VQ4BLYRNRQENJXTWGMCDWR4TU5ANCNFSM6AAAAAATQC5GNQ . You are receiving this because you were mentioned.Message ID: @.>

Do yu forget to attach the image?

pluc77 commented 1 year ago

It doesn't appear that the screen capture came thru with my response via email. Let's try this again. Capture

pluc77 commented 1 year ago

Hello, just checking in to see if you were able to access the attached solution?

Also, I realized I didn't completely answer your last response. By "simply pass through" I mean I'm fine with removing the binders completely from the pipeline. My production scenario is not using Entity Framework, but rather converting the parsed OData expression tree into TSQL. Therefore, my data access layer is applying selects, filters, expansion, ordering, pagination, etc. at the database level, so I don't need the binders to process the model in any way, shape, or form. I simply want the instantiated EdmEntityObject(s) to return completely within the response payload.

pluc77 commented 1 year ago

Just bumping this thread to see if anyone can tell me how to remove or override the default binders from the DI container when dealing with a dynamic model. My attempts seem to be ignored by the DI container as described in previous comments.

paweldyminski commented 1 year ago

Another bump. I'm also dealing with (almost) exactly the same problem, I've created and bound a dynamic EDM model into a OData Web API pipeline exactly as described in your example ODataDynamicModel

But most of the query filters like $filter $top etc. do not work in this scenario. I understand that it is not supported out of the box, but whenever I want to customize the binder classes and inject them exactly like @pluc77 did, the whole OData pipeline is ignoring them and is using a default ones.

Is there anything more I could do to solve this, any ideas? Any help will be appreciated.

pluc77 commented 1 year ago

I've tried numerous things and have been unsuccessful in getting custom binders injected into the pipeline. Wish I had better news for you. I've been pre-occupied elsewhere and haven't had a chance to revisit in a number of weeks. I do plan on researching to see if there's a way to configure "MaxDepth" without application of the EnableQuery attribute at the controller method level with the hope being it allows me to workaround the issue, but it might be a few more weeks before I get back to this.