OData / OData/AspNetCoreOData

$expand - MaxExpansionDepth not honored

Open
#272 6 comments 1 reaction 1 assignee View on GitHub

@xuzhg is already working on this.

Since Aug 10, 2021.

investigating
Dominant language
C#
Stars
505
Forks
186
PR merge metrics
No merged PRs in 30d

Description

We have got multiple API applications that act differently on $expands.

There are 2 cases that I were able to observe:

  1. All requested navigation properties are expanded as expected.
  2. Requested navigation properties are only expanded for first and second level expands in depth.

I have made sure that both applications use the same Environment:

  • .NET 5.0
  • Microsoft.AspNetCore.OData 8.0.1
  • Microsoft.EntityFrameworkCore 5.0.4

I also made sure that both applications use the same EF Core and OData configuration.
Because that did not help in eliminating the difference in behavior, i created a completly new API application to serve the minimal setup with OData.
Because both applications use a different DbContext, I referenced both and created an ODataController for the same Entity on both DBContexts.
Now in this new API application both endpoints do not return all requested navigation properties.
This brings up the conclusion that the minimal differences of the models are not the cause.

I observed the EF Core debug log which shows that the SQL includes the requested expands.
The debug log also shows that even for the deepest expanded entity there is a log entry for the entity to be tracked by the change tracker.

I have made sure to use the same JSON serialisazion options.

However the returned responses contain only first and second level expands in depth.

I tried different expands on different levels.
There are a lot of navigation properties on our models.
For this issue I picked a simple expansion path.
On the shown JSONs i removed a bunch of properties not required for this issue.

On the controllers I use [EnableQuery(MaxExpansionDepth = 0)] to disable the limit at all.
I also tried [EnableQuery(MaxExpansionDepth = 10)] to check if that changes anything, but that was not the case.

To sum it up:
  • Case 1 application expands all navigation properties as expected
  • Case 2 application does not expand all navigation properties
  • New application does not expand all navigation properties, for both case 1 and case 2 endpoints.
Conclusions
  • Since I made sure to provide equal environments, I am wondering if OData is using some kind of implicit or hidden configuration which is created automagically and not supposed to be seen or configured by devs?
  • The OData-Result-Object delivers the @odata.context property which contains the full expansion path for all cases, does that mean OData correctly recognizes all expansions?
Setup
public class Startup
{
	public IConfiguration Configuration { get; }

	public Startup(IConfiguration configuration)
	{
		Configuration = configuration;
	}

	// This method gets called by the runtime. Use this method to add services to the container.
	// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
	public void ConfigureServices(IServiceCollection services)
	{
		var connectionString = Configuration.GetConnectionString("MyConnectionString");

		services.AddDbContext<Case_1_DbContext>(options =>
		{
			options.UseLazyLoadingProxies();
			options.UseSqlServer(connectionString, sqlServerOptions => sqlServerOptions.CommandTimeout(300));
#if DEBUG
			options.LogTo(message => Debug.WriteLine(message), LogLevel.Trace);
#endif
		});

		services.AddDbContext<Case_2_DbContext>(options =>
		{
			options.UseLazyLoadingProxies();
			options.UseSqlServer(connectionString, sqlServerOptions => sqlServerOptions.CommandTimeout(300));
#if DEBUG
			options.LogTo(message => Debug.WriteLine(message), LogLevel.Trace);
#endif
		});

		services.AddControllers(options =>
		{
			options.RespectBrowserAcceptHeader = true;
			options.MaxIAsyncEnumerableBufferLimit = int.MaxValue;
		}).AddJsonOptions(options =>
		{
			//Stop using JsonNamingPolicy.CamelCase 
			//=> Prevent Serializer from producing properties with different casing than defined by model.
			options.JsonSerializerOptions.PropertyNamingPolicy = null;
		}).AddOData(options =>
		{
			var defaultBatchHandler = new DefaultODataBatchHandler();
			defaultBatchHandler.MessageQuotas.MaxNestingDepth = 2;
			defaultBatchHandler.MessageQuotas.MaxOperationsPerChangeset = 10;
			defaultBatchHandler.MessageQuotas.MaxReceivedMessageSize = 100;

			options.AddRouteComponents("odata", ModelProvider.GetEdmModel(), defaultBatchHandler);
			options.Select().Expand().Filter().OrderBy().SetMaxTop(null).Count();
		});
	}

	// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
	public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
	{
		if (env.IsDevelopment())
		{
			app.UseDeveloperExceptionPage();
		}

		app.UseODataBatching();
		app.UseRouting();

		app.UseEndpoints(endpoints =>
		{
			endpoints.MapControllers();
		});
	}
}
public class ModelProvider
{
	public static IEdmModel GetEdmModel()
	{
		ODataConventionModelBuilder builder = new ODataConventionModelBuilder();

		builder.EntitySet<Case_1_Model.Order>("Orders");
		builder.EntitySet<Case_2_Model.Order>("Orders2");

		return builder.GetEdmModel();
	}
}
public class OrdersController : ODataController
{
	private readonly Case_1_DbContext _context;

	public OrdersController(Case_1_DbContext context)
	{
		_context = context;
	}

	[EnableQuery(MaxExpansionDepth = 0)]
	public IActionResult Get()
	{
		return Ok(_context.Order);
	}
}
public class Orders2Controller : ODataController
{
	private readonly Case_2_DbContext _context;

	public OrdersController(Case_2_DbContext context)
	{
		_context = context;
	}

	[EnableQuery(MaxExpansionDepth = 0)]
	public IActionResult Get()
	{
		return Ok(_context.Order);
	}
}
OData-Query
  • {{baseUrl}}/odata/Orders?$top=10&$expand=OrderPosition($expand=OrderDetail($expand=Item($expand=ItemDetail)))
  • {{baseUrl}}/odata/Orders2?$top=10&$expand=OrderPosition($expand=OrderDetail($expand=Item($expand=ItemDetail)))
Received Response
{
    "@odata.context": "{{baseUrl}}/odata/$metadata#Orders(OrderPosition(OrderDetail(Item(ItemDetail()))))",
    "value": [
        {
            "Id": 127,
            "SupplierId": 3665,
            "OrderPosition": [
                {
                    "Id": 907,
                    "OrderId": 127,
                    "PositionNo": 34,
                    "ArticleIdId": 84905,
                    "OrderDetail": [
                        {
                            "Id": 3486,
                            "OrderPositionId": 907,
                            "ItemId": 177422,
                            "Quantity": 1.00,
                        }
                    ]
                }
            ]
        }
    ]
}
Expected Response
{
    "@odata.context": "{{baseUrl}}/odata/$metadata#Orders(OrderPosition(OrderDetail(Item(ItemDetail()))))",
    "value": [
        {
            "Id": 127,
            "SupplierId": 3665,
            "OrderPosition": [
                {
                    "Id": 907,
                    "OrderId": 127,
                    "PositionNo": 34,
                    "ArticleIdId": 84905,
                    "OrderDetail": [
                        {
                            "Id": 3486,
                            "OrderPositionId": 907,
                            "ItemId": 177422,
                            "Quantity": 1.00,
                            "Item": {
                                "Id": 177422,
                                "ArticleId": 84905,
                                "ItemDetail": [
                                    {
                                        "Id": 2234,
                                        "ItemId": 177422,
                                        "StoreId": 4
                                    }
                                ]
                            }
                        }
                    ]
                }
            ]
        }
    ]
}

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.