Skip to content

juner/AspireDbAndAspNetCore_Error_20260409

Repository files navigation

AspireDbAndAspNetCore_Error_20260409

How to run

aspire run

Then run the data migration from the Aspire dashboard.


Endpoints

/swagger/index.html

Swagger UI


/product_1

Top-level IAsyncEnumerable<T>

  • The serializer streams the sequence directly.
  • Enumeration completes normally.
  • DisposeAsync() is invoked immediately when enumeration ends.
  • Always works.

/product_2

Object containing IAsyncEnumerable<T> properties

  • The serializer enumerates each EF Core sequence and produces JSON output.
  • However, even after enumeration completes, DisposeAsync() is not invoked.
  • The underlying EF Core database connection therefore remains open.
  • When the next enumeration begins, it collides with the still-open connection and fails.

Only this endpoint fails when maxRetryCount = 0

When maxRetryCount >= 1, EF Core’s retry logic closes the connection once,
which accidentally allows the next enumeration to succeed.


/product_3

Object containing async-iterator–wrapped IAsyncEnumerable<T>

  • Each sequence is wrapped in an async iterator using explicit await foreach.
  • await foreach guarantees that the inner EF Core enumerator is fully consumed and its DisposeAsync() is invoked immediately when enumeration completes.
  • The wrapper itself is enumerable by the serializer, but the important part is that the underlying database enumerator has already been disposed.
  • Always works.

/product_4

Object containing buffered arrays (ToArrayAsync)

  • Fully buffered before returning.
  • No streaming and no async enumeration.
  • Always works.

Problem Summary

Only /product_2 fails when maxRetryCount = 0.

The root cause is:

The serializer does not invoke DisposeAsync() after finishing enumeration
for property-based IAsyncEnumerable<T>.
As a result, the EF Core database connection remains open,
and the next enumeration attempt fails.

For EF Core:

  • IAsyncEnumerable<T> keeps the database connection open during enumeration.
  • The connection is closed only when DisposeAsync() is invoked.
  • In /product_2, DisposeAsync() is never called, leaving the connection open.
  • The next MoveNextAsync() collides with the still-open connection and fails.

When maxRetryCount >= 1, EF Core’s retry logic closes the connection once,
which accidentally avoids the failure.


Behavior Matrix

maxRetryCount 0 1+
/product_1 ok ok
/product_2 ng ok
/product_3 ok ok
/product_4 ok ok

Key Insight

The failure is not caused by concurrency or parallel JSON writing.

The real issue is:

For property-based IAsyncEnumerable<T>, the serializer delays DisposeAsync()
until after JSON serialization completes.
EF Core requires DisposeAsync() to close the database connection,
so delaying it causes the next enumeration to fail.


Additional Investigation: /dispose_1, /dispose_2, /dispose_3

These diagnostic endpoints show the exact timing of DisposeAsync().

/dispose_1 — Top-level IAsyncEnumerable<T>

  • The serializer streams the sequence.
  • DisposeAsync() is invoked immediately when enumeration ends.

Observed log:

[MOVE] ...
[DISPOSE]

/dispose_2 — Object with two IAsyncEnumerable<T> properties

  • The serializer enumerates both sequences.
  • However, DisposeAsync() is not invoked when enumeration ends.
  • Both enumerators are disposed only after JSON serialization finishes.

Observed log:

Num1: [MOVE] ...
Num2: [MOVE] ...
Num1: [DISPOSE]
Num2: [DISPOSE]

This confirms:
property-based IAsyncEnumerable<T> → delayed DisposeAsync.


/dispose_3 — Async-iterator–wrapped IAsyncEnumerable<T>

  • The inner enumerator is consumed via await foreach.
  • DisposeAsync() is invoked immediately when enumeration ends.
  • Matches the behavior of /product_3.

Observed log:

Num1: [MOVE] ...
[DISPOSE]   ← immediately after enumeration
Num2: [MOVE] ...
[DISPOSE]

Conclusion

/product_2 fails because:

  • enumeration completes but DisposeAsync() is not invoked
  • the EF Core database connection remains open
  • the next enumeration attempt fails due to the still-open connection

/dispose_1, /dispose_2, and /dispose_3 clearly show that:

  • Top-level sequences → immediate DisposeAsync
  • Property-based sequences → delayed DisposeAsync
  • Async iterator wrapper → immediate DisposeAsync

This fully explains all observed behavior.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages