Skip to main content

12 posts tagged with "C#"

Practical C# — async, immutability, pattern matching and the traps that show up in real code.

View All Tags

When Does Calling .Result Deadlock, and When Does It Not?

· 14 min read
Nguyễn Huỳnh Minh Tiến
Middle Fullstack Developer @ Utop.vn
Summary

.Result and .Wait() block the current thread. When await suspends, it captures the current SynchronizationContext so that the code after it runs back on that same context. If the context only allows one thread at a time — a WPF UI thread, an ASP.NET Framework request context — and that thread is the one .Result is blocking, the continuation can never get in: deadlock. Console apps and ASP.NET Core have no SynchronizationContext, so the continuation runs on the thread pool and nothing gets stuck. The real fix is async all the way.

The code below runs perfectly in a console app, and freezes solid the moment you paste it verbatim into an ASP.NET Framework controller or a button handler in WPF:

public string GetCustomerName(int id)
{
return GetCustomerAsync(id).Result; // hangs here
}

private async Task<string> GetCustomerAsync(int id)
{
var response = await _http.GetStringAsync($"/customers/{id}");
return Parse(response).Name;
}

No exception, no stack trace, no timeout. The thread simply stops forever. This is not HttpClient's fault, and async is not "broken" — it is the direct consequence of a design decision in how await works.

Why Does EF Core Fire 201 Queries for One List Screen? Fixing N+1

· 13 min read
Nguyễn Huỳnh Minh Tiến
Middle Fullstack Developer @ Utop.vn
Summary

A 200-row list whose log shows 201 SQL statements is textbook N+1: one query fetches the list, then each row triggers another query to load its related data. The root cause is navigation properties being loaded one at a time instead of together. The default fix is a projection with Select into a DTO. Include is only the right tool when you need real entities, and once you Include two or more collections you need AsSplitQuery to avoid a cartesian explosion.

You open the customer list page, the API responds after four seconds, yet the database CPU barely moves and every individual SQL statement runs in a few milliseconds. Turn on logging and it turns out the problem is not one slow query, but 201 fast ones queued up nose to tail.

That is an N+1 query. It does not make the code wrong, it throws no exception, no unit test catches it, and on your local machine with twenty seeded rows it may even be faster than the correct fix. It only shows itself once the tables hold real data and the network latency to the database is non-zero.

Change the id in the URL and Get Someone Else's Data? Stop IDOR in a Single Layer

· 14 min read
Nguyễn Huỳnh Minh Tiến
Middle Fullstack Developer @ Utop.vn
Summary

IDOR happens when an endpoint takes an id from the request and loads the record directly, checking only that the caller is logged in rather than that the caller has rights over that particular record. Switching ids to GUIDs or encrypting them does not fix the bug, because the id still leaks somewhere else. The fix is to force every query through a layer that binds it to ownership on the server, instead of scattering checks through individual controllers.

You have a perfectly ordinary endpoint: GET /api/orders/1043 returns order details. It sits behind [Authorize], only a valid token gets through, and in the UI a user can only click into their own orders.

Then somebody logs in with their real account, changes 1043 to 1044, and hits send. If the server returns someone else's order, that is an Insecure Direct Object Reference — IDOR for short. No payload, no technique, just one number changed.

Singleton, Scoped or Transient? Get It Wrong and Your DbContext Lives Forever

· 14 min read
Nguyễn Huỳnh Minh Tiến
Middle Fullstack Developer @ Utop.vn
Summary

Choosing a lifetime means choosing when the container creates an instance and when it disposes it. Transient creates a new one on every resolve, Scoped gives one instance per HTTP request, Singleton one instance for the whole process. The single most important rule: a long-lived service must not take a short-lived one directly. Inject a Scoped repository into a Singleton and you have just kept that DbContext alive until the app shuts down. The fix is to inject IServiceScopeFactory and open a scope yourself when you need one.

The registration line that causes this looks so harmless that nobody pauses on it in review:

builder.Services.AddDbContext<CrmDbContext>(o => o.UseSqlServer(conn)); // Scoped
builder.Services.AddScoped<ICustomerRepository, EfCustomerRepository>();
builder.Services.AddSingleton<CustomerCacheRefresher>(); // ← the explosive

The app passes CI and reaches staging. Once it is under real load, the logs start showing A second operation was started on this context instance before a previous operation completed, or Cannot access a disposed context instance — or, worse, no exception at all and merely stale data that never changes. This post explains the mechanism behind it, with the code for both the broken and the fixed version.

Why Does .NET Eat 96% of Container RAM While Idle? Server GC and How to Cut Memory by 65%

· 12 min read
Nguyễn Huỳnh Minh Tiến
Middle Fullstack Developer @ Utop.vn
Summary

An ASP.NET Core 9 container sat at 987 MiB against a 1 GiB ceiling (96%) after two hours without receiving a single request. This was not a memory leak: the number was absolutely flat across repeated measurements, which is the signature of retained heap rather than a leak. The cause was Server GC — the ASP.NET Core default, which creates one heap per CPU core and releases very little memory back to the operating system. Setting DOTNET_gcServer=0 and DOTNET_GCConserveMemory=5 brought anonymous memory down from 822 MB to 280 MB (−65%), measured as stable after 30 minutes. The trade-off: Workstation GC gives lower throughput under heavy load on many-core machines.

This post records a real diagnostic session on a 4-core / 8 GB VPS running nine WordPress sites alongside a .NET API. Every number below is measured, not illustrative.