Skip to content

Cookies on Jobverse

We use cookies that are needed to sign you in and keep the site working. With your permission we’d also use an analytics cookie to count job views, and Google advertising cookies to show ads. The site works the same whatever you choose. Read the Cookie Policy.

Jobverse
Sign in

.NET Backend Developer Top 100 Interview Questions

100 .NET backend developer interview questions with structured answers, comparison tables, flow diagrams, worked calculations and runnable C# code. Covers the language and runtime, async and concurrency, the web pipeline, data access, caching, security, testing, diagnostics, performance and system design, from the basics to production incidents.

100 questions

Questions 41-60 of 100

  1. Q41.How do background services run in a .NET host, and how do they shut down?

    A hosted background service is a long-running task started by the application host alongside the web server. The base class provides an ExecuteAsync method that receives a cancellation token, which the host cancels when the application starts shutting down.

    Typical uses: consuming a queue, processing a channel of work from requests, scheduled cleanup and cache refreshing.

    Writing one correctly:

    1. Loop until cancelled: while (!stoppingToken.IsCancellationRequested), passing the token to every await.
    2. Create a scope per unit of work to resolve scoped services, such as a database context, since the background service itself is a singleton.
    3. Catch and log exceptions per iteration, so one bad item does not stop the service; decide whether a fatal error should stop the whole host.
    4. Do not block startup: return quickly from the start phase, and do slow initialization inside the loop.
    5. Finish in-flight work on shutdown: the host waits for a configurable time for services to stop; drain or checkpoint work within it.

    Multiple instances: every instance of the application runs its own copy of the background service, so jobs that must run once across the fleet need a distributed lock or a dedicated worker deployment.

  2. Q42.How do you implement retries with exponential backoff and jitter in C#?

    Retry only transient failures, wait an exponentially growing, randomized delay between attempts, respect cancellation and stop after a fixed number of attempts. An exception filter makes it easy to retry some exceptions while letting the last attempt's failure propagate.

    Design points:

    1. Transient only: network errors, timeouts and responses such as 429 and 503; never validation errors.
    2. Exponential delay with jitter: for example, half fixed and half random, so many clients do not retry in lockstep.
    3. Cancellation-aware delays: Task.Delay(delay, ct), so retries stop when the caller gives up.
    4. Respect server hints, such as a Retry-After header.
    5. Idempotency: retry only operations that are safe to repeat, or use idempotency keys.
    6. One layer: retry in one place in the call chain, to avoid multiplying attempts.

    Resilience libraries offer ready-made pipelines with retries, circuit breakers and timeouts, but the core logic is small enough to understand and test directly.

    Example

    The first operation succeeds on the third call; the second always fails and surfaces its last exception.

    int calls = 0;
    string result = await RetryAsync(async ct =>
    {
        calls++;
        await Task.Yield();
        if (calls < 3) throw new HttpRequestException("temporary");
        return "ok";
    }, attempts: 5, baseDelay: TimeSpan.FromMilliseconds(10));
    Console.WriteLine($"{result} after {calls} calls"); // ok after 3 calls
    
    try
    {
        await RetryAsync<string>(_ => throw new HttpRequestException("down"), 3, TimeSpan.FromMilliseconds(5));
    }
    catch (HttpRequestException e)
    {
        Console.WriteLine(e.Message); // down
    }
    
    static async Task<T> RetryAsync<T>(Func<CancellationToken, Task<T>> op, int attempts, TimeSpan baseDelay, CancellationToken ct = default)
    {
        for (int attempt = 1; ; attempt++)
        {
            try
            {
                return await op(ct);
            }
            catch (HttpRequestException) when (attempt < attempts)
            {
                double ms = baseDelay.TotalMilliseconds * Math.Pow(2, attempt - 1);
                await Task.Delay(TimeSpan.FromMilliseconds(ms / 2 + Random.Shared.NextDouble() * ms / 2), ct);
            }
        }
    }
  3. Q43.How does rate limiting work in a .NET web service?

    The .NET web stack includes rate limiting middleware with several algorithms. Endpoints are assigned policies, and requests over the limit are rejected, usually with status 429, or queued briefly.

    Fixed windowSliding windowToken bucketConcurrency limiter
    Allows N requests per time windowSplits the window into segments for smoother limitsRefills tokens at a steady rate and allows short burstsLimits requests in progress at once
    Simple, but allows bursts at window edgesSmoother, with a little more stateGood balance of bursts and average rateProtects expensive endpoints regardless of rate

    Designing limits:

    1. Partition by client: API key, user or tenant, rather than one global limit, so one client cannot starve others.
    2. Different policies per endpoint: stricter for login and expensive searches.
    3. Queue carefully: a small queue smooths bursts, but long queues only add latency.
    4. Tell clients: return a Retry-After header when possible.

    Distributed limits: the built-in limiters keep state in each process. With several instances, either divide limits per instance or enforce them at a shared gateway or store.

  4. Q44.How do you avoid deadlocks in asynchronous C# code?

    Most async deadlocks come from blocking on async code, from holding locks across awaits or from two operations waiting on each other's resources. Keep code asynchronous end to end, and keep critical sections small and consistent.

    Common causes and fixes:

    1. Sync over async: .Result or .Wait() on a task whose continuation needs the blocked thread; use await all the way.
    2. Lock across await: lock cannot contain await, and working around it with other primitives can hold a lock while waiting on slow input and output. Use SemaphoreSlim.WaitAsync and keep the protected section short.
    3. Lock ordering: two code paths that acquire the same locks in different orders can deadlock; define one order.
    4. Semaphores not released: a missing Release in an error path permanently reduces capacity until everything waits; always release in finally.
    5. Waiting on yourself: a bounded channel whose consumer is the same code that is blocked writing to it.

    Diagnosis: a process dump with async stack information shows which tasks are waiting on which; growing waits on one semaphore or lock usually point to the cause.

    Interview tip: stating that you never call .Result in request code, and explaining why, is one of the quickest ways to show real async experience.

  5. Q45.Which timer should a .NET service use for periodic work?

    For periodic work inside an async service, use PeriodicTimer: it is awaited in a loop, never runs ticks concurrently and stops cleanly with a cancellation token. Other timers fit specific needs.

    PeriodicTimerSystem.Threading.TimerTask.Delay in a loop
    Await WaitForNextTickAsync in a loopInvokes a callback on the thread pool at intervalsAwaits a delay after each iteration
    Ticks never overlap; missed ticks are combinedCallbacks can overlap if work runs longInterval drifts by the work's duration
    Stops with a token or DisposeMust be disposed; callbacks must handle their own exceptionsSimple, but drifts over time
    Best for background loops in servicesBest for low-level, fire-and-forget callbacksBest for simple retry waits

    Good practices:

    1. Catch exceptions inside each iteration, so one failure does not stop the loop.
    2. Pass the stopping token to the work, not just the timer.
    3. For wall-clock schedules, such as "every day at 02:00", compute the next run time instead of relying on an interval.
    4. Remember that every instance of the service runs the timer; coordinate if the job must run once.
  6. Q46.How do you measure elapsed time and make time-dependent code testable in .NET?

    Use Stopwatch or Stopwatch.GetTimestamp for measuring durations, since they use a monotonic, high-resolution clock that is unaffected by wall-clock changes. For code that depends on the current time, such as token expiry, inject a TimeProvider, so tests can control time.

    TimeProvider:

    • TimeProvider.System is the real clock in production.
    • Code calls time.GetUtcNow() instead of DateTimeOffset.UtcNow.
    • It also creates timers and measures elapsed time, so delays and timeouts can be tested.
    • Tests pass a provider whose time they move forward manually.

    Pitfalls avoided:

    1. DateTime.Now in business logic, which mixes local time zones and makes tests depend on the clock.
    2. Measuring durations with wall-clock subtraction, which breaks when the clock is adjusted.
    3. Tests that sleep for real seconds to check expiry, which are slow and flaky.

    Example

    A hand-written time provider moves time forward, so expiry is tested instantly.

    var clock = new ManualTimeProvider(new DateTimeOffset(2026, 3, 1, 9, 0, 0, TimeSpan.Zero));
    var issuer = new TokenIssuer(clock, TimeSpan.FromMinutes(15));
    
    Token token = issuer.Issue("u-7");
    Console.WriteLine(issuer.IsValid(token)); // True
    
    clock.Advance(TimeSpan.FromMinutes(16));
    Console.WriteLine(issuer.IsValid(token)); // False
    
    class ManualTimeProvider(DateTimeOffset start) : TimeProvider
    {
        private DateTimeOffset now = start;
        public override DateTimeOffset GetUtcNow() => now;
        public void Advance(TimeSpan by) => now += by;
    }
    
    record Token(string UserId, DateTimeOffset Expires);
    
    class TokenIssuer(TimeProvider time, TimeSpan lifetime)
    {
        public Token Issue(string userId) => new(userId, time.GetUtcNow() + lifetime);
        public bool IsValid(Token token) => time.GetUtcNow() < token.Expires;
    }
  7. Q47.Which tools do you use to diagnose a running .NET service?

    The .NET diagnostic command-line tools attach to a running process without restarting it, which makes them practical in production and in containers. Each answers a different question.

    dotnet-countersdotnet-tracedotnet-dumpdotnet-gcdump
    Live metrics: processor, memory, garbage collection, thread pool, requestsRecords events and processor samples over a periodCaptures a full process memory dumpCaptures only the managed heap's object graph
    First look during an incidentFinding where time goes, such as hot methods or slow waitsAnalyzing hangs, deadlocks and crashes, including all stacksFinding which objects hold memory

    A typical workflow:

    1. Start with counters to see whether the problem is processor, memory, garbage collection or thread pool related.
    2. Trace for processor problems and slow code paths, then open the trace in a profiler view.
    3. Dump for hangs and deadlocks, and inspect thread stacks and async state.
    4. Heap snapshots compared over time for memory growth.

    In containers: include the tools in a diagnostic sidecar or debug image, share the diagnostic socket, and make sure dumps can be copied out safely, since they contain sensitive data.

  8. Q48.How do you find a memory leak in a .NET service?

    Confirm that managed memory grows over time without being reclaimed, then compare heap snapshots to find which object types grow and what keeps them alive. In managed code, "leaks" are almost always references that are accidentally kept.

    1. Confirm the trend: generation 2 heap size and total memory rising steadily at constant traffic, across garbage collections.
    2. Take two heap snapshots some time apart.
    3. Compare object counts by type: the types that keep growing point to the leak.
    4. Follow the retention path: look at what references the growing objects, from the roots down.

    Common causes:

    • Static or singleton collections that only grow, such as caches without eviction.
    • Event subscriptions from short-lived objects to long-lived publishers that are never removed.
    • Captive dependencies: singletons holding scoped services.
    • Timers that are never disposed, keeping their callbacks and captured objects alive.
    • Large object heap fragmentation from frequent large buffers, which looks like a leak.
    • Native memory from unmanaged libraries, which does not show in managed heap snapshots.

    After fixing, watch memory level off under the same load, and add an alert on generation 2 size growth.

  9. Q49.How do object pooling and `ArrayPool` reduce allocations?

    Pooling reuses objects instead of allocating new ones for every operation. ArrayPool<T>.Shared lends arrays, typically buffers for reading, writing and serialization, and takes them back when you return them. Fewer large allocations mean fewer garbage collections and less pressure on the large object heap.

    Using ArrayPool:

    1. byte[] buffer = ArrayPool<byte>.Shared.Rent(minimumLength); returns an array at least that long, possibly longer.
    2. Use only the part you need, tracking the length yourself.
    3. ArrayPool<byte>.Shared.Return(buffer) in a finally block.

    Rules:

    • Never use a buffer after returning it, since another caller may already be using it.
    • Clear sensitive data before returning, with the clearArray option.
    • Do not return the same array twice.

    Object pools for other types work the same way: get, use, reset and return. They help for expensive-to-create objects used at high rates, such as large string builders.

    When pooling is not worth it: small, short-lived objects are cheap to allocate in generation 0; pooling them adds complexity without measurable gain. Measure allocations first.

  10. Q50.How do you benchmark C# code reliably?

    Measuring performance correctly is harder than it looks. The just-in-time compiler, tiered compilation, garbage collection and processor frequency changes all distort naive timings, so use a disciplined process or a dedicated benchmarking harness.

    Principles:

    1. Release builds only: debug builds disable most optimizations.
    2. Warm up: run the code many times before measuring, so tiered compilation reaches optimized code.
    3. Many iterations and repeated runs: report the mean, the spread and percentiles, not a single number.
    4. Measure allocations along with time, since allocation reduction often matters more in services.
    5. Prevent dead-code elimination: use the result, or the compiler may remove the work.
    6. Stable environment: a quiet machine, a fixed power plan and the same runtime version for comparisons.
    7. Realistic data: sizes and shapes like production, since performance often depends on input.

    Beyond micro-benchmarks: a function that is twice as fast may not matter if it is 1% of a request. Profile the whole service under load first, then benchmark the hot spots that profiling identified.

    Open-source harnesses automate warm-up, statistics and allocation reporting, and are the standard way to benchmark .NET code.

  11. Q51.What happens when an HTTP request reaches a .NET web application?

    The built-in web server accepts the connection, parses the request and passes it through the application's middleware pipeline. Each middleware component can act on the request, call the next component and act on the response on the way back. At the end of the pipeline, routing selects an endpoint, model binding builds its parameters and the endpoint's result is written as the response.

    flowchart LR
      A["Web server parses request"] --> B["Exception handling middleware"]
      B --> C["Logging and request ID"]
      C --> D["Authentication"]
      D --> E["Authorization"]
      E --> F["Routing to endpoint"]
      F --> G["Model binding and validation"]
      G --> H["Endpoint runs, result serialized"]

    What developers should know:

    1. Requests run concurrently on thread pool threads, so shared services must be thread-safe.
    2. A dependency injection scope is created per request; scoped services, such as a database context, live for that request.
    3. The request has a cancellation token that fires when the client disconnects; pass it to database and outgoing calls.
    4. Middleware order defines behavior, so exception handling must come first and authentication before authorization.
    5. Headers are sent when the body starts, so set headers and status codes before writing.
  12. Q52.How does a middleware pipeline work, and why does its order matter?

    Middleware components are chained delegates. Each receives the request context and a reference to the next component, does its work, optionally calls the next one and then continues after it returns. The first component registered wraps all the others, so it sees the request first and the response last.

    Consequences of order:

    • Exception handling first, so it can catch failures from everything after it.
    • Authentication before authorization, since authorization needs to know who the user is.
    • Short-circuiting: a component that does not call the next one ends the pipeline, as an authorization failure or a cached response does.
    • Response-modifying middleware, such as compression, must wrap the components that write the body.

    Writing middleware well:

    1. Keep each component focused on one concern.
    2. Do not write to the response after calling the next component if the body may already have started.
    3. Pass the request's cancellation token to asynchronous work.
    4. Prefer endpoint filters for logic that applies only to specific endpoints.

    Example

    A small hand-built pipeline shows the same mechanics: logging wraps everything, and the admin check short-circuits.

    var builder = new PipelineBuilder();
    builder.Use(async (ctx, next) =>
    {
        ctx.Log.Add("logging: start");
        await next(ctx);
        ctx.Log.Add($"logging: end {ctx.Status}");
    });
    builder.Use(async (ctx, next) =>
    {
        if (ctx.Path.StartsWith("/admin") && !ctx.IsAdmin)
        {
            ctx.Status = 403;
            return;
        }
        await next(ctx);
    });
    builder.Run(ctx =>
    {
        ctx.Status = 200;
        ctx.Log.Add($"handler: {ctx.Path}");
        return Task.CompletedTask;
    });
    
    RequestDelegate app = builder.Build();
    foreach (var (path, admin) in new[] { ("/orders", false), ("/admin/users", false) })
    {
        var ctx = new Context(path, admin);
        await app(ctx);
        Console.WriteLine(string.Join(" | ", ctx.Log));
    }
    // Output:
    // logging: start | handler: /orders | logging: end 200
    // logging: start | logging: end 403
    
    delegate Task RequestDelegate(Context ctx);
    
    class Context(string path, bool isAdmin)
    {
        public string Path { get; } = path;
        public bool IsAdmin { get; } = isAdmin;
        public int Status { get; set; }
        public List<string> Log { get; } = [];
    }
    
    class PipelineBuilder
    {
        private readonly List<Func<RequestDelegate, RequestDelegate>> components = [];
        private RequestDelegate terminal = _ => Task.CompletedTask;
    
        public void Use(Func<Context, RequestDelegate, Task> middleware) =>
            components.Add(next => ctx => middleware(ctx, next));
    
        public void Run(RequestDelegate handler) => terminal = handler;
    
        public RequestDelegate Build()
        {
            RequestDelegate app = terminal;
            for (int i = components.Count - 1; i >= 0; i--) app = components[i](app);
            return app;
        }
    }
  13. Q53.How do minimal APIs and controllers differ in .NET?

    Both build HTTP endpoints on the same routing, binding and middleware infrastructure. Minimal APIs map routes directly to functions with little ceremony; controllers group actions in classes with conventions and filters inherited from a long history of the framework.

    Minimal APIsControllers
    Endpoints are lambdas or methods mapped with MapGet, MapPost and similarEndpoints are methods on controller classes, selected by attributes or conventions
    Little boilerplate, fast startup and compatible with ahead-of-time compilationMore structure and conventions, with a large existing ecosystem
    Organized with route groups and endpoint filtersOrganized with controllers, action filters and areas
    Best for new services, microservices and focused APIsBest for large existing codebases and teams used to the pattern

    Shared concepts: parameter binding from routes, query strings, headers and bodies, dependency injection into handlers, typed results such as Results.NotFound() and the same authentication and authorization.

    Organizing minimal APIs as they grow:

    1. Put related endpoints in a static class with an extension method that maps them.
    2. Use route groups for shared prefixes, filters and authorization.
    3. Keep handlers thin, delegating business logic to services.
  14. Q54.How do model binding and validation work in a .NET web service?

    Model binding creates handler parameters from the request: route values, query strings, headers, form data and the JSON body. Validation then checks the bound objects against rules, and invalid requests should be rejected with a clear 400 response before business logic runs.

    Binding sources:

    • Simple types come from the route or query string by default.
    • Complex types come from the JSON body.
    • Attributes such as [FromHeader] or [FromQuery] make the source explicit.
    • Special types, such as the cancellation token and the current user, are bound automatically.

    Validation approaches:

    1. Data annotations: attributes such as [Required], [Range] and [EmailAddress] on request types, which the framework or Validator checks.
    2. Custom validation: implement IValidatableObject for rules involving several properties.
    3. Validation in the domain: constructors and value objects that cannot represent invalid states.

    Good practices: validate at the boundary, return all errors at once in a standard format, never bind request bodies directly to database entities and treat validation failures as client errors, not exceptions.

    Example

    Data annotations report every invalid property in one pass.

    using System.ComponentModel.DataAnnotations;
    
    var request = new CreateOrder { CustomerId = "", Quantity = 0, Email = "not-an-email" };
    var results = new List<ValidationResult>();
    bool valid = Validator.TryValidateObject(request, new ValidationContext(request), results, validateAllProperties: true);
    
    Console.WriteLine(valid);
    foreach (var r in results)
        Console.WriteLine($"{string.Join(",", r.MemberNames)}: {r.ErrorMessage}");
    // Output:
    // False
    // CustomerId: The CustomerId field is required.
    // Quantity: The field Quantity must be between 1 and 100.
    // Email: The Email field is not a valid e-mail address.
    
    class CreateOrder
    {
        [Required] public string CustomerId { get; set; } = "";
        [Range(1, 100)] public int Quantity { get; set; }
        [EmailAddress] public string? Email { get; set; }
    }
  15. Q55.How do you return consistent error responses from a .NET API?

    Use one error format for every failure, so clients can handle errors generically. The standard problem details format, defined in an open specification for HTTP APIs, is a JSON object with a type, a title, a status, a detail message, the request path and optional extra fields, such as validation errors.

    How to apply it:

    1. A central exception handler turns unhandled exceptions into a 500 problem response, logs the exception and never exposes stack traces.
    2. Known domain failures map to specific statuses: not found to 404, conflicts to 409 and validation failures to 400 with an errors field listing each invalid field.
    3. Include a trace or request ID, so support teams can find the matching logs.
    4. Use stable type values, such as /errors/out-of-stock, that clients can rely on, rather than parsing messages.

    What to avoid:

    • Returning 200 with an error flag in the body.
    • Different error shapes from different endpoints.
    • Leaking internal details, such as SQL errors or file paths.

    The framework includes built-in support for producing problem details from exceptions and status codes.

    Example

    A validation failure serialized in the problem details shape.

    using System.Text.Json;
    
    var problem = new Problem(
        Type: "/errors/validation",
        Title: "One or more fields are invalid.",
        Status: 400,
        Instance: "/orders",
        TraceId: "00-4bf9-01",
        Errors: new() { ["quantity"] = ["Must be between 1 and 100."] });
    
    var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
    Console.WriteLine(JsonSerializer.Serialize(problem, options));
    // Output:
    // {"type":"/errors/validation","title":"One or more fields are invalid.","status":400,"instance":"/orders","traceId":"00-4bf9-01","errors":{"quantity":["Must be between 1 and 100."]}}
    
    record Problem(string Type, string Title, int Status, string Instance, string TraceId, Dictionary<string, string[]> Errors);
  16. Q56.How do you configure JSON serialization with `System.Text.Json`?

    System.Text.Json is the built-in, high-performance JSON library. Its behavior is controlled through JsonSerializerOptions, attributes on types and converters for special cases. Web services usually use the web defaults: camel-case property names and case-insensitive reading.

    Common settings:

    • Naming: PropertyNamingPolicy such as camel case or snake case.
    • Nulls: DefaultIgnoreCondition = WhenWritingNull omits null properties.
    • Enums as strings: JsonStringEnumConverter, so values stay readable and stable when enum members are reordered.
    • Unknown properties: UnmappedMemberHandling = Disallow rejects unexpected fields, catching client mistakes.
    • Attributes: [JsonPropertyName], [JsonIgnore] and [JsonRequired] on individual members.

    Good practices:

    1. Create options once and reuse them; creating new options per call discards internal caches and is slow.
    2. Use source-generated contexts for performance and ahead-of-time compatibility.
    3. Be careful with numbers: decimals keep their scale, while very large integers may need to travel as strings for some clients.
    4. Version your contracts instead of serializing internal domain types directly.

    Example

    Web defaults, string enums and ignored nulls on output; case-insensitive matching on input.

    using System.Text.Json;
    using System.Text.Json.Serialization;
    
    var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
    {
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
        Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) },
    };
    
    var order = new Order(42, OrderStatus.PartiallyShipped, null, 1250.50m);
    Console.WriteLine(JsonSerializer.Serialize(order, options));
    
    Order? back = JsonSerializer.Deserialize<Order>("""{"ID":7,"status":"paid","total":99}""", options);
    Console.WriteLine(back);
    // Output:
    // {"id":42,"status":"partiallyShipped","total":1250.50}
    // Order { Id = 7, Status = Paid, Note = , Total = 99 }
    
    record Order(int Id, OrderStatus Status, string? Note, decimal Total);
    
    enum OrderStatus { Paid, PartiallyShipped }
  17. Q57.How do you use `HttpClient` correctly in a .NET service?

    Reuse clients and their connection pools, set timeouts, pass cancellation tokens and handle non-success responses explicitly. Creating and disposing an HttpClient per request wastes connections and can exhaust sockets; holding one forever without refreshing connections can miss changes in name resolution.

    Recommended approaches:

    1. Client factory registration: register named or typed clients with the dependency injection container; the factory pools message handlers and recycles them periodically, solving both problems.
    2. Or a long-lived client with a SocketsHttpHandler whose PooledConnectionLifetime is set, so connections are refreshed regularly.

    Per call:

    • Pass the request's CancellationToken.
    • Check response.IsSuccessStatusCode, or call EnsureSuccessStatusCode, and read error bodies for logging.
    • Use GetFromJsonAsync and PostAsJsonAsync helpers for JSON.
    • Stream large responses with HttpCompletionOption.ResponseHeadersRead.

    Resilience: add timeouts, retries with backoff for transient failures and circuit breakers around the client, ideally configured once per client registration.

    Security: set base addresses from configuration, validate any URL built from user input to prevent server-side request forgery and never log authorization headers.

  18. Q58.How do you query a database safely with the base data access classes in .NET?

    Open connections from a pooled provider, run commands with parameters, read results with a data reader and dispose everything promptly. The base classes, DbConnection, DbCommand and DbDataReader, are shared by all relational providers, and object-relational mappers are built on top of them.

    Rules for safe queries:

    1. Always use parameters: command.Parameters.Add(...) with typed values, never string concatenation, which prevents SQL injection and lets the database reuse query plans.
    2. Open late, close early: await using var connection and open it just before use; disposing returns it to the pool.
    3. Use async methods with the request's cancellation token: OpenAsync, ExecuteReaderAsync, ReadAsync.
    4. Set command timeouts suited to the query.
    5. Read only what you need: select specific columns and page large results.

    Connection pooling: providers keep pools per connection string. Pool exhaustion usually comes from connections not being disposed, long transactions or too many concurrent requests; monitor pool usage and keep queries short.

    When to use a mapper instead: for productivity with complex object graphs. Lightweight mapping libraries sit in between, keeping hand-written SQL with automatic mapping to objects.

  19. Q59.How do object-relational mappers track changes, and what are the common pitfalls?

    An object-relational mapper loads rows into objects, tracks changes to those objects and generates SQL to save them. A unit of work, usually a context object scoped to one request, remembers which entities were loaded and which properties changed, then writes all changes in one transaction when you save.

    Common pitfalls:

    1. N+1 queries: loading a list, then lazily loading related data for each item, runs one query per item; load related data explicitly in the same query, or project to what you need.
    2. Tracking overhead: tracking thousands of entities for read-only screens wastes memory and time; use no-tracking queries for reads.
    3. Loading whole entities when only a few columns are needed; project with Select into small result types.
    4. Client-side evaluation: filters that cannot be translated to SQL run in memory after loading too much data.
    5. Long-lived contexts: reusing one context across requests grows its tracked set and causes stale data and thread-safety bugs.
    6. Hidden queries in loops that look like property access.

    Good practices: log generated SQL in development, review query counts per request, add indexes for common filters and keep the mapper's model close to the database schema rather than hiding it completely.

  20. Q60.How do you run database transactions in .NET?

    Group operations that must succeed or fail together into one transaction, commit only when all succeed and roll back on any error. .NET offers explicit transactions on a connection and ambient transactions that flow through the code.

    Explicit transactionAmbient transaction scope
    Begin a transaction on a connection and attach commands to itWrap code in a scope; connections opened inside enlist automatically
    Clear and predictable, tied to one connectionConvenient across several methods
    Works the same everywhere, with full async supportNeeds care with async flow and can escalate to distributed transactions
    Recommended default for most servicesUse only when its behavior is well understood

    Good practices:

    1. Keep transactions short: no calls to other services, no user interaction and no long computations inside them.
    2. Choose isolation deliberately: the default is often enough; stricter levels prevent anomalies but increase blocking and deadlocks.
    3. Retry whole transactions on deadlocks and serialization failures, with idempotent logic.
    4. One unit of work per request when using a mapper: save changes once, which runs in a transaction.
    5. For work spanning services, use an outbox table or sagas instead of distributed transactions.