{"items":[{"docs":[{"label":"Best practices for Azure RBAC","url":"https://learn.microsoft.com/azure/role-based-access-control/best-practices"},{"label":"Azure built-in roles","url":"https://learn.microsoft.com/azure/role-based-access-control/built-in-roles"}],"excerpt":"Contributor is the role people reach for when something needs to work now. It usually works, but it gives the identity far more power than an application normally needs. Most workloads do not need to manage Azure resources. They need to read a secret, write a blob, send a message, or query data.\nThe useful split is control plane vs. data plane. Control-plane permissions manage resources: create, update, delete, …","search":"assign least-privilege azure roles, not broad contributor rights give workloads and people the narrow azure role they need instead of defaulting to contributor. c# .net azure security azure rbac least privilege managed identity azure roles contributor security contributor is the role people reach for when something needs to work now. it usually works, but it gives the identity far more power than an application normally needs. most workloads do not need to manage azure resources. they need to read a secret, write a blob, send a message, or query data.\nthe useful split is control plane vs. data plane. control-plane permissions manage resources: create, update, delete, configure, list keys, or change infrastructure. data-plane permissions use the data inside those resources. for application identities, this distinction keeps the runtime from becoming a resource admin.\ngive the identity the role for the operation. a managed identity that reads secrets from key vault usually needs key vault secrets user. a worker that writes blobs usually needs storage blob data contributor on the storage account or container it uses. the running app and the deployment pipeline should not share the same permission shape.\nthis matters when something goes wrong. a bug, leaked token, compromised workload, or bad configuration can only do what the identity is allowed to do. if every identity is contributor, a small application failure can become an azure failure.\nscope the assignment tightly. prefer a specific resource or resource group when subscription scope is not needed. keep production identities separate from local development and test identities, even if the same application code uses all of them.\nin .net, this can still keep the application code simple. use defaultazurecredential or a managed identity credential in azure sdk clients, then let azure rbac decide what that identity can actually do.\nleast privilege is not ceremony. it keeps access reviews readable and makes mistakes smaller.\n","summary":"Give workloads and people the narrow Azure role they need instead of defaulting to Contributor.","tagSlugs":["c","net","azure","security"],"tags":["C#",".NET","Azure","Security"],"title":"Assign least-privilege Azure roles, not broad contributor rights","url":"/tips/assign-least-privilege-azure-roles-not-broad-contributor-rights/"},{"docs":[{"label":"Debug a memory leak in .NET","url":"https://learn.microsoft.com/dotnet/core/diagnostics/debug-memory-leak"},{"label":"dotnet-counters diagnostic tool","url":"https://learn.microsoft.com/dotnet/core/diagnostics/dotnet-counters"}],"excerpt":"The allocation that hurts is often not the large object you notice in review. It is the tiny allocation inside a loop that runs for every request, row, chunk, token, or message.\nWatch hot paths for Split(), Substring(), ToArray(), accidental LINQ chains, repeated formatting, closure captures, boxing, unnecessary materialization, and converting between strings, bytes, and streams more often than needed. Each call can …","search":"avoid hidden allocations in hot paths watch for small per-item allocations inside loops, parsers, serializers, and request paths. c# .net performance diagnostics allocations gc hot path span linq substring split performance memory the allocation that hurts is often not the large object you notice in review. it is the tiny allocation inside a loop that runs for every request, row, chunk, token, or message.\nwatch hot paths for split(), substring(), toarray(), accidental linq chains, repeated formatting, closure captures, boxing, unnecessary materialization, and converting between strings, bytes, and streams more often than needed. each call can look harmless in isolation. under load, it can become gc pressure.\nclosure captures are a good example because the allocation is not visible in the source:\npublic sealed record item(int id); public static bool hasitem(ienumerable\u0026lt;item\u0026gt; items, int targetid) { return items.any(item =\u0026gt; item.id == targetid); } public static bool hasitemhotpath(ireadonlylist\u0026lt;item\u0026gt; items, int targetid) { for (var i = 0; i \u0026lt; items.count; i++) { if (items[i].id == targetid) { return true; } } return false; } the first version is readable and fine in normal application code. in a hot path, the captured targetid means the compiler needs extra state for the predicate. the second version is dull, but it removes that hidden capture and makes the allocation story easier to inspect.\ndo not rewrite everything into allocation-free code. that makes normal application code harder to read and easier to break. start with measured hotspots, then remove copies that do not need to exist.\nfor parsers and data pipelines, span\u0026lt;t\u0026gt;, memory\u0026lt;t\u0026gt;, pooled buffers, streaming apis, and projection can help when the data can be processed without creating intermediate objects. for request handlers, the bigger win may be avoiding materializing huge payloads or database results. if dotnet-counters shows high allocation rate or constant gen 0 collections, inspect these small patterns before reaching for a bigger rewrite.\nthe practical rule: if the code runs once, keep it clear. if it runs millions of times, check what it allocates before assuming the gc cost is acceptable.\n","summary":"Watch for small per-item allocations inside loops, parsers, serializers, and request paths.","tagSlugs":["c","net","performance","diagnostics"],"tags":["C#",".NET","Performance","Diagnostics"],"title":"Avoid hidden allocations in hot paths","url":"/tips/avoid-hidden-allocations-in-hot-paths/"},{"docs":[{"label":"Background tasks with hosted services","url":"https://learn.microsoft.com/aspnet/core/fundamentals/host/hosted-services"},{"label":"Exception handling in the Task Parallel Library","url":"https://learn.microsoft.com/dotnet/standard/parallel-programming/exception-handling-task-parallel-library"}],"excerpt":"Fire-and-forget tasks are easy to write and hard to operate. The call site returns, the request finishes, and the actual work continues somewhere with unclear ownership.\nThe dangerous part is failure. If the task throws, who observes the exception? Who logs it with the right correlation id? Who retries it? Who cancels it during shutdown? Who tells the user that the work did not happen?\nFor real application work, …","search":"do not hide exceptions inside fire-and-forget tasks if work can fail, give it ownership, logging, cancellation, and a lifecycle. c# .net reliability async fire and forget task exceptions background work async hosted service reliability fire-and-forget tasks are easy to write and hard to operate. the call site returns, the request finishes, and the actual work continues somewhere with unclear ownership.\nthe dangerous part is failure. if the task throws, who observes the exception? who logs it with the right correlation id? who retries it? who cancels it during shutdown? who tells the user that the work did not happen?\nfor real application work, prefer an explicit background queue, hosted service, durable job, message bus, or workflow. that gives the work a lifecycle and a place for cancellation, retries, error handling, telemetry, and backpressure.\nsmall fire-and-forget work can still be acceptable for best-effort telemetry or non-critical cleanup, but make that decision visible. catch and log exceptions inside the task, pass cancellation where possible, and avoid using scoped services after the request scope has ended.\nthe scoping trap usually looks like this:\n// dangerous: dbcontext belongs to the http request scope. _ = task.run(async () =\u0026gt; { dbcontext.auditlogs.add(auditlog); await dbcontext.savechangesasync(); }); // acceptable only for non-critical best-effort work. _ = task.run(async () =\u0026gt; { try { using var scope = servicescopefactory.createscope(); var scopeddb = scope.serviceprovider.getrequiredservice\u0026lt;appdbcontext\u0026gt;(); scopeddb.auditlogs.add(auditlog); await scopeddb.savechangesasync(); } catch (exception ex) { logger.logerror(ex, \u0026#34;failed to write audit log in background.\u0026#34;); } }); the second version is not a replacement for a real queue. it only shows the minimum shape for best-effort work: create a fresh scope, resolve scoped services inside that scope, and observe exceptions inside the task. if the request scope\u0026rsquo;s dbcontext is captured, the request can finish first and dispose the context while the background work is still running.\nif the work matters to correctness, do not hide it behind _ = task.run(...). make it a first-class part of the system.\n","summary":"If work can fail, give it ownership, logging, cancellation, and a lifecycle.","tagSlugs":["c","net","reliability","async"],"tags":["C#",".NET","Reliability","Async"],"title":"Do not hide exceptions inside fire-and-forget tasks","url":"/tips/do-not-hide-exceptions-inside-fire-and-forget-tasks/"},{"docs":[{"label":"FastAPI first steps","url":"https://fastapi.tiangolo.com/tutorial/first-steps/"},{"label":"Aspire AppHost","url":"https://aspire.dev/get-started/app-host/"}],"excerpt":"If an AI capability fits the Python ecosystem better, do not force it into .NET just to keep the solution single-language. Put the Python code behind a small FastAPI service and treat it as a real service boundary.\nThat works well for model experiments, data science utilities, document processing, evaluation helpers, or libraries that are simply stronger in Python. The .NET application calls an HTTP API instead of …","search":"expose a python fastapi ai tool as an aspire service keep python ai code behind an http boundary and let aspire wire it into the local system. c# .net aspire python ai engineering aspire python fastapi ai tool service boundary http api polyglot orchestration if an ai capability fits the python ecosystem better, do not force it into .net just to keep the solution single-language. put the python code behind a small fastapi service and treat it as a real service boundary.\nthat works well for model experiments, data science utilities, document processing, evaluation helpers, or libraries that are simply stronger in python. the .net application calls an http api instead of importing python logic directly.\nuse aspire to make that polyglot setup runnable locally. older examples used communitytoolkit.aspire.hosting.python.extensions, but current aspire puts this in the aspire.hosting.python apphost package. model the fastapi app as a uvicorn resource, then reference it from the .net service that calls it.\n// apphost program.cs var aibackend = builder.adduvicornapp(\u0026#34;ai-backend\u0026#34;, \u0026#34;../ai_service\u0026#34;, \u0026#34;main:app\u0026#34;) .withuv(); // or .withpip() for requirements.txt builder.addproject\u0026lt;projects.webfrontend\u0026gt;(\u0026#34;frontend\u0026#34;) .withreference(aibackend); that keeps the python url in the application graph instead of scattered through local settings, launch profiles, and client constructors.\nkeep the boundary boring. define request and response models, use health checks, make timeouts explicit, and avoid turning the python service into a hidden bag of scripts. it should be inspectable, testable, and replaceable.\n","summary":"Keep Python AI code behind an HTTP boundary and let Aspire wire it into the local system.","tagSlugs":["c","net","aspire","python","ai-engineering"],"tags":["C#",".NET","Aspire","Python","AI Engineering"],"title":"Expose a Python FastAPI AI tool as an Aspire service","url":"/tips/expose-a-python-fastapi-ai-tool-as-an-aspire-service/"},{"docs":[{"label":"Azure OpenAI in Azure AI Foundry Models","url":"https://learn.microsoft.com/azure/ai-foundry/openai/overview"},{"label":"Use the IChatClient interface","url":"https://learn.microsoft.com/dotnet/ai/ichatclient"}],"excerpt":"Model and deployment choices change over time. You may move between providers, switch Azure OpenAI deployments, test a cheaper model, split routing by endpoint, or roll back after a regression.\nKeep those names in configuration instead of scattering them through controllers, agents, services, and background jobs. The application should ask for a configured chat model, embedding model, evaluation model, or deployment …","search":"keep model name and deployment name in config do not hard-code model routing decisions into application logic. c# .net azure ai engineering configuration model name deployment name azure openai configuration model routing ai deployment model and deployment choices change over time. you may move between providers, switch azure openai deployments, test a cheaper model, split routing by endpoint, or roll back after a regression.\nkeep those names in configuration instead of scattering them through controllers, agents, services, and background jobs. the application should ask for a configured chat model, embedding model, evaluation model, or deployment name through options or a small routing abstraction.\nthis is especially important with azure openai, where the deployment name is not always the same thing as the model name. hard-coding one string in five places makes a model rollout look like a code refactor.\nin .net, put that decision in the composition root. register the configured ichatclient once, then let application code depend on the abstraction:\nusing azure.ai.openai; using azure.identity; using microsoft.extensions.ai; using microsoft.extensions.dependencyinjection; string endpoint = builder.configuration[\u0026#34;azureopenai:endpoint\u0026#34;] ?? throw new invalidoperationexception(\u0026#34;missing azure openai endpoint.\u0026#34;); string deployment = builder.configuration[\u0026#34;azureopenai:chatdeployment\u0026#34;] ?? throw new invalidoperationexception(\u0026#34;missing azure openai deployment.\u0026#34;); builder.services.addchatclient( new azureopenaiclient(new uri(endpoint), new defaultazurecredential()) .getchatclient(deployment) .asichatclient()); public sealed class supportagent(ichatclient chatclient) { public async task\u0026lt;string\u0026gt; summarizeasync(string input) { var response = await chatclient.getresponseasync(input); return response.text; } } now supportagent does not know the deployment name, the model version, or the provider. if you need a fast summarization model and a heavier reasoning model, use keyed services or a small routing service at the di boundary. keep the keys stable and keep the actual deployment names in configuration.\nconfiguration does not remove the need for tests. treat model changes like behavior changes. run evals, watch latency and cost, and keep enough telemetry to know which model or deployment produced a bad result.\nthe practical rule: model identity is operational configuration, not domain logic.\n","summary":"Do not hard-code model routing decisions into application logic.","tagSlugs":["c","net","azure","ai-engineering","configuration"],"tags":["C#",".NET","Azure","AI Engineering","Configuration"],"title":"Keep model name and deployment name in config","url":"/tips/keep-model-name-and-deployment-name-in-config/"},{"docs":[{"label":"Add a filter to a vector query in Azure AI Search","url":"https://learn.microsoft.com/azure/search/vector-search-filters"},{"label":"Hybrid search in Azure AI Search","url":"https://learn.microsoft.com/azure/search/hybrid-search-overview"}],"excerpt":"In retrieval systems, filtering and ranking answer different questions. Filtering decides which documents are eligible. Ranking decides which eligible documents are most relevant.\nKeep those concerns separate. Tenant ID, permissions, language, product area, document type, retention state, and security labels are usually filters. Semantic similarity, keyword score, freshness boosts, and reranking are relevance …","search":"keep vector search filters separate from semantic ranking use filters for eligibility and ranking for relevance instead of blending both concerns. c# .net ai engineering rag azure ai search vector search filters semantic ranking hybrid search rag azure ai search retrieval in retrieval systems, filtering and ranking answer different questions. filtering decides which documents are eligible. ranking decides which eligible documents are most relevant.\nkeep those concerns separate. tenant id, permissions, language, product area, document type, retention state, and security labels are usually filters. semantic similarity, keyword score, freshness boosts, and reranking are relevance signals.\nthis matters because mixing them leads to fragile behavior. a document should not rank highly enough to bypass authorization. a low-similarity document should not be retrieved just because it matches a broad metadata tag. the retrieval pipeline needs both eligibility and relevance.\nfor rag systems, apply hard filters before or around vector search when the constraint is mandatory. then use semantic or hybrid ranking to order the allowed candidates. that keeps retrieval safer, easier to debug, and easier to tune.\nin azure ai search, this boundary is visible in the query shape. the odata filter defines eligibility. vector search and semantic ranking define relevance:\nstring tenant = currenttenantid.replace(\u0026#34;\u0026#39;\u0026#34;, \u0026#34;\u0026#39;\u0026#39;\u0026#34;); var options = new searchoptions { filter = $\u0026#34;tenantid eq \u0026#39;{tenant}\u0026#39; and status eq \u0026#39;published\u0026#39;\u0026#34;, querytype = searchquerytype.semantic, semanticsearch = new semanticsearchoptions { semanticconfigurationname = \u0026#34;default\u0026#34; }, vectorsearch = new vectorsearchoptions { queries = { new vectorizedquery(queryvector) { knearestneighborscount = 50, fields = { \u0026#34;contentvector\u0026#34; } } } } }; searchresults\u0026lt;searchdocument\u0026gt; results = await searchclient.searchasync\u0026lt;searchdocument\u0026gt;(searchtext, options); the tenant and publication state stay in the filter. the vector query and semantic ranker can only rank documents that are eligible to be returned.\nfor newer azure ai search vector indexes, prefiltering is the default and recommended mode. that means the filter is applied during vector search traversal, which favors recall for selective filters. post-filtering can be faster in some cases, but it can also miss matching documents when k is small or the filter is highly selective.\n","summary":"Use filters for eligibility and ranking for relevance instead of blending both concerns.","tagSlugs":["c","net","ai-engineering","rag","azure-ai-search"],"tags":["C#",".NET","AI Engineering","RAG","Azure AI Search"],"title":"Keep vector search filters separate from semantic ranking","url":"/tips/keep-vector-search-filters-separate-from-semantic-ranking/"},{"docs":[{"label":"dotnet-counters diagnostic tool","url":"https://learn.microsoft.com/dotnet/core/diagnostics/dotnet-counters"},{"label":"dotnet-trace diagnostic tool","url":"https://learn.microsoft.com/dotnet/core/diagnostics/dotnet-trace"}],"excerpt":"Performance work should start with evidence. Before changing code, decide what is actually slow: latency, throughput, allocations, CPU, database time, lock contention, cold start, serialization, network calls, model latency, or queue depth.\nDifferent problems need different tools. Use traces to understand request paths, metrics to see trends, counters to inspect runtime pressure, logs to explain rare cases, profilers …","search":"measure before you optimize use traces, counters, profiles, and focused benchmarks before changing code for performance. c# .net performance diagnostics performance measurement profiling dotnet-counters benchmarkdotnet optimization latency throughput performance work should start with evidence. before changing code, decide what is actually slow: latency, throughput, allocations, cpu, database time, lock contention, cold start, serialization, network calls, model latency, or queue depth.\ndifferent problems need different tools. use traces to understand request paths, metrics to see trends, counters to inspect runtime pressure, logs to explain rare cases, profilers for cpu and allocations, and focused benchmarks for small code paths. in .net, that usually means starting with dotnet-counters for system.runtime counters such as gc pressure, allocation rate, cpu, lock contention, and threadpool queue length. use dotnet-trace when you need a cpu trace instead of another guess.\nthis matters because many \u0026ldquo;obvious\u0026rdquo; optimizations move the cost instead of reducing it. caching can increase memory pressure. parallelism can saturate dependencies. async wrappers can hide cpu work. a faster query can still return too much data. a smaller prompt can make a model less reliable.\nmeasure the baseline, make one change, then measure again with the same scenario. if the change does not move the metric you care about, remove it.\nfor application code, prefer production-like measurements over tiny synthetic examples. for library or hot-path code, use benchmarkdotnet to isolate the operation and compare one change at a time:\nusing benchmarkdotnet.attributes; [memorydiagnoser] public class csvfirstfieldbenchmark { private const string data = \u0026#34;value1,value2,value3\u0026#34;; [benchmark(baseline = true)] public int usingsplit() =\u0026gt; data.split(\u0026#39;,\u0026#39;)[0].length; [benchmark] public int usingspan() { readonlyspan\u0026lt;char\u0026gt; span = data; return span[..span.indexof(\u0026#39;,\u0026#39;)].length; } } [memorydiagnoser] matters because the faster-looking version may still allocate too much. baseline = true makes the comparison explicit. the goal is not to make every decision heavy. the goal is to stop optimizing the part that only felt suspicious in review.\n","summary":"Use traces, counters, profiles, and focused benchmarks before changing code for performance.","tagSlugs":["c","net","performance","diagnostics"],"tags":["C#",".NET","Performance","Diagnostics"],"title":"Measure before you optimize","url":"/tips/measure-before-you-optimize/"},{"docs":[{"label":"ASP.NET Core middleware","url":"https://learn.microsoft.com/aspnet/core/fundamentals/middleware/#middleware-order"},{"label":"ASP.NET Core request and response operations","url":"https://learn.microsoft.com/aspnet/core/fundamentals/middleware/request-response"}],"excerpt":"ASP.NET Core middleware is not a bag of independent features. It is an ordered pipeline. Each component can run before the next one, after the next one, short-circuit the request, or change the request and response seen by everything later.\nThat means order affects behavior. Exception handling needs to wrap the parts of the pipeline whose failures it should catch. Routing needs to run before endpoint-aware …","search":"middleware order is important place asp.net core middleware in the order that matches routing, security, and endpoint behavior. c# .net asp.net core architecture middleware asp.net core pipeline routing authentication authorization cors exception handling asp.net core middleware is not a bag of independent features. it is an ordered pipeline. each component can run before the next one, after the next one, short-circuit the request, or change the request and response seen by everything later.\nthat means order affects behavior. exception handling needs to wrap the parts of the pipeline whose failures it should catch. routing needs to run before endpoint-aware middleware. authentication needs to establish the user before authorization checks policies. cors, static files, response compression, rate limiting, antiforgery, and custom middleware all have placement rules that change what they can see and enforce.\nthis becomes more important when you add cross-cutting behavior such as api key checks, tenant resolution, logging scopes, correlation ids, request body limits, or model/tool authorization. put those checks too late and sensitive work may already have started. put them too early and they may not have route or user context yet.\nthe boring mvc or api pipeline usually looks close to this:\napp.useexceptionhandler(\u0026#34;/error\u0026#34;); app.usehttpsredirection(); app.usestaticfiles(); app.userouting(); app.usecors(\u0026#34;frontend\u0026#34;); app.useauthentication(); app.usemiddleware\u0026lt;tenantresolutionmiddleware\u0026gt;(); app.useauthorization(); app.mapcontrollers(); the exact middleware set depends on the app, but the relative order matters. userouting() selects endpoint data. usecors() belongs after routing and before authorization so preflight and rejected requests still get the right cors headers. useauthentication() must run before useauthorization(). custom middleware that needs both route data and the authenticated user usually belongs between authentication and authorization.\nkeep the pipeline boring and intentional. group related middleware, avoid clever custom short-circuits, and add a comment when order is non-obvious. if changing the order would change security or endpoint behavior, make that visible in review.\n","summary":"Place ASP.NET Core middleware in the order that matches routing, security, and endpoint behavior.","tagSlugs":["c","net","aspnet-core","architecture"],"tags":["C#",".NET","ASP.NET Core","Architecture"],"title":"Middleware order is important","url":"/tips/middleware-order-is-important/"},{"docs":[{"label":"Nullable reference types","url":"https://learn.microsoft.com/dotnet/csharp/nullable-references"},{"label":"Resolve nullable warnings","url":"https://learn.microsoft.com/dotnet/csharp/language-reference/compiler-messages/nullable-warnings"}],"excerpt":"Nullable warnings are not just compiler noise. They are often feedback that a type, API, or initialization path is not explicit enough.\nDo not silence them reflexively with !. The null-forgiving operator can be useful at a boundary the compiler cannot understand, but it should not become a way to hide uncertain object state. If a property is required, make it required. If a value can be missing, model that in the …","search":"nullable warnings are design feedback, not noise treat nullable warnings as pressure to make object state and api contracts clearer. c# .net reliability code quality nullable reference types nullable warnings csharp nullability api contracts code quality nullable warnings are not just compiler noise. they are often feedback that a type, api, or initialization path is not explicit enough.\ndo not silence them reflexively with !. the null-forgiving operator can be useful at a boundary the compiler cannot understand, but it should not become a way to hide uncertain object state. if a property is required, make it required. if a value can be missing, model that in the type. if a method cannot accept null, let the signature say so.\nfor example, this hides an invalid construction path:\npublic sealed class createusercommand { public string email { get; set; } = null!; } if the caller must provide the value, make that part of the type:\npublic sealed class createusercommand { public required string email { get; init; } } required does not replace validation for untrusted input, but it does stop your own code from pretending the object can be valid without that state.\nthis matters most at application boundaries: configuration, deserialization, database projections, external api responses, command handlers, and background jobs. those are places where null values often enter even when the happy path looks clean.\nwhen a nullable warning appears, ask what the code is trying to say. maybe the object has an invalid construction path. maybe a method accepts more than it should. maybe the fallback case is missing. maybe the data contract is lying.\nthe goal is not zero warnings as a vanity metric. the goal is code where nullability describes reality closely enough that the next developer can trust the shape.\n","summary":"Treat nullable warnings as pressure to make object state and API contracts clearer.","tagSlugs":["c","net","reliability","code-quality"],"tags":["C#",".NET","Reliability","Code Quality"],"title":"Nullable warnings are design feedback, not noise","url":"/tips/nullable-warnings-are-design-feedback-not-noise/"},{"docs":[{"label":"CancellationToken struct","url":"https://learn.microsoft.com/dotnet/api/system.threading.cancellationtoken"},{"label":"Cancellation in managed threads","url":"https://learn.microsoft.com/dotnet/standard/threading/cancellation-in-managed-threads"}],"excerpt":"A CancellationToken only helps if the expensive work actually receives it. In an ASP.NET Core API, cancellation often starts at the request boundary: the client disconnects, a timeout expires, or the caller no longer needs the result.\nIf the controller, endpoint, or handler accepts the token but a service method drops it, the lower-level operation may keep running anyway. That can leave a database query, HTTP call, …","search":"pass cancellationtoken through real boundaries thread cancellation through every expensive operation instead of dropping it mid-chain. c# .net asp.net core reliability cancellationtoken cancellation request aborted api cancelled async database http ichatclient a cancellationtoken only helps if the expensive work actually receives it. in an asp.net core api, cancellation often starts at the request boundary: the client disconnects, a timeout expires, or the caller no longer needs the result.\nif the controller, endpoint, or handler accepts the token but a service method drops it, the lower-level operation may keep running anyway. that can leave a database query, http call, storage request, queue operation, or model call consuming resources after the original request has already been cancelled.\nin minimal apis, asp.net core can bind the request cancellation token for you. pass it into the expensive call instead of stopping at the endpoint signature:\napp.mappost(\u0026#34;/api/generate\u0026#34;, async ( chatrequest request, ichatclient chatclient, cancellationtoken ct) =\u0026gt; { var response = await chatclient.getresponseasync( request.messages, options: null, cancellationtoken: ct); return results.ok(response.text); }); for ai calls, this is not just about freeing a thread. if the client disconnects during generation and the token is dropped, the model can keep producing tokens nobody will read.\ntreat cancellation as part of the method contract for real i/o boundaries. if a method performs work that can take noticeable time, accept a cancellationtoken and pass it to the next async operation. do not replace it with cancellationtoken.none unless you intentionally want the operation to outlive the caller.\nfor your own service interfaces, make the token part of the contract and use default when the caller should not be forced to pass one:\npublic interface idocumentprocessor { task processasync(string documentid, cancellationtoken ct = default); } cancellation is cooperative. it is not a magic thread abort. the code doing the work has to observe the token, either by passing it into apis that support cancellation or by checking it at safe points in long-running logic.\n","summary":"Thread cancellation through every expensive operation instead of dropping it mid-chain.","tagSlugs":["c","net","aspnet-core","reliability"],"tags":["C#",".NET","ASP.NET Core","Reliability"],"title":"Pass CancellationToken through real boundaries","url":"/tips/pass-cancellationtoken-through-real-boundaries/"},{"docs":[{"label":"Health checks in ASP.NET Core","url":"https://learn.microsoft.com/aspnet/core/host-and-deploy/health-checks"},{"label":"Entity Framework Core health checks","url":"https://learn.microsoft.com/aspnet/core/host-and-deploy/health-checks#entity-framework-core-dbcontext-probe"}],"excerpt":"A health endpoint should not only prove that the web process can return 200 OK. It should answer the question your platform or operator is actually asking: can this instance safely receive traffic, or should it stay out of rotation?\nThat does not mean every health check should call every dependency on every probe. A database query, cache call, queue check, or downstream HTTP request can become load if it runs too …","search":"prefer health checks that test dependencies deliberately health checks should say something useful about readiness without turning every probe into production load. c# .net asp.net core reliability observability health checks readiness liveness dependencies asp.net core database health check probes resilience a health endpoint should not only prove that the web process can return 200 ok. it should answer the question your platform or operator is actually asking: can this instance safely receive traffic, or should it stay out of rotation?\nthat does not mean every health check should call every dependency on every probe. a database query, cache call, queue check, or downstream http request can become load if it runs too often or does too much work. test dependencies deliberately, with cheap checks, timeouts, and a clear reason for why that dependency affects readiness.\nseparate liveness from readiness. a liveness probe should usually answer whether the process is alive and should not trigger restarts because a database is temporarily slow. a readiness probe can be stricter because it controls whether traffic should be sent to the instance.\nin asp.net core, that usually means tagging checks and mapping separate endpoints:\nbuilder.services .addhealthchecks() .adddbcontextcheck\u0026lt;appdbcontext\u0026gt;( name: \u0026#34;database\u0026#34;, tags: new[] { \u0026#34;ready\u0026#34; }); app.maphealthchecks(\u0026#34;/health/live\u0026#34;, new healthcheckoptions { predicate = _ =\u0026gt; false }); app.maphealthchecks(\u0026#34;/health/ready\u0026#34;, new healthcheckoptions { predicate = check =\u0026gt; check.tags.contains(\u0026#34;ready\u0026#34;) }); the liveness endpoint proves the process can respond. the readiness endpoint includes dependency checks that decide whether the instance should receive traffic.\nfor dependency checks, prefer simple signals over realistic work. opening a connection may be enough. if you run a query, keep it tiny and predictable. avoid checks that create data, depend on external rate limits, call expensive ai models, or hide real production failures behind broad exception handling.\nhealth checks are part of the reliability contract. make the result meaningful, cheap, and aligned with what the orchestrator will do when it fails.\n","summary":"Health checks should say something useful about readiness without turning every probe into production load.","tagSlugs":["c","net","aspnet-core","reliability","observability"],"tags":["C#",".NET","ASP.NET Core","Reliability","Observability"],"title":"Prefer health checks that test dependencies deliberately","url":"/tips/prefer-health-checks-that-test-dependencies-deliberately/"},{"docs":[{"label":"Pagination in EF Core","url":"https://learn.microsoft.com/ef/core/querying/pagination"}],"excerpt":"Offset pagination with Skip and Take is simple, but it gets worse as users move deeper into a large list. The database still has to process the skipped rows, and concurrent inserts or deletes can make items appear twice or disappear between pages.\nUse keyset pagination when the user mostly moves forward or backward through an ordered result set. Instead of asking for page 500, ask for the next items after the last …","search":"prefer keyset pagination for deep lists use seek-based pagination when users move through large ordered result sets. c# .net entity framework performance apis keyset pagination seek pagination offset pagination skip take ef core pagination deep lists offset pagination with skip and take is simple, but it gets worse as users move deeper into a large list. the database still has to process the skipped rows, and concurrent inserts or deletes can make items appear twice or disappear between pages.\nuse keyset pagination when the user mostly moves forward or backward through an ordered result set. instead of asking for page 500, ask for the next items after the last seen key.\nin ef core, that usually means replacing a deep skip with a seek predicate:\n// avoid for deep lists: the database still has to walk skipped rows. var page500 = await db.auditlogs .orderbydescending(x =\u0026gt; x.createdat) .thenbydescending(x =\u0026gt; x.id) .skip(10_000) .take(20) .tolistasync(ct); // prefer: continue after the last row from the previous page. var nextpage = await db.auditlogs .where(x =\u0026gt; x.createdat \u0026lt; lastseendate || (x.createdat == lastseendate \u0026amp;\u0026amp; x.id \u0026lt; lastseenid)) .orderbydescending(x =\u0026gt; x.createdat) .thenbydescending(x =\u0026gt; x.id) .take(20) .tolistasync(ct); ef core can express the composite seek as normal linq predicates, but you still have to carry the cursor values yourself. sql row-value syntax such as (createdat, id) \u0026lt; (@date, @id) is not currently expressible in linq.\nthe important part is a stable, unique order. if you sort by date, include a tie-breaker such as an id. then the next query can continue from the last seen date and id instead of counting past thousands of rows.\nforward pagination is the simple case. previous-page navigation usually needs its own cursor, often from the first row in the current page, plus a reversed sort that you normalize before returning results.\noffset pagination is still fine when users need random page access. for feeds, logs, search results, audit trails, and deep admin lists, keyset pagination usually matches the user behavior and the database better.\n","summary":"Use seek-based pagination when users move through large ordered result sets.","tagSlugs":["c","net","entity-framework","performance","apis"],"tags":["C#",".NET","Entity Framework","Performance","APIs"],"title":"Prefer keyset pagination for deep lists","url":"/tips/prefer-keyset-pagination-for-deep-lists/"},{"docs":[{"label":"OWASP LLM01:2025 Prompt Injection","url":"https://genai.owasp.org/llmrisk/llm01-prompt-injection/"},{"label":"Policy-based authorization in ASP.NET Core","url":"https://learn.microsoft.com/aspnet/core/security/authorization/policies"}],"excerpt":"Prompts are not an authorization layer. A system prompt can describe rules, but it cannot prove who the user is, what tenant they belong to, or which data and actions they are allowed to access.\nKeep authorization in deterministic application code. Apply normal identity, role, tenant, ownership, and policy checks before data is retrieved, before a tool is called, and before a side effect is committed.\nThis matters …","search":"separate prompts from authorization do not let prompt instructions decide what data or actions a user is allowed to access. c# .net ai engineering security architecture prompt injection authorization least privilege ai security tools rag system prompt access control prompts are not an authorization layer. a system prompt can describe rules, but it cannot prove who the user is, what tenant they belong to, or which data and actions they are allowed to access.\nkeep authorization in deterministic application code. apply normal identity, role, tenant, ownership, and policy checks before data is retrieved, before a tool is called, and before a side effect is committed.\nthis matters most in rag and agentic systems. retrieval should only return documents the caller is allowed to see. tools should only receive operations the caller is allowed to perform. the model should never be the component that decides whether access is valid.\nfor retrieval, keep the authority on the server side. the model can provide the search query, but the application should derive tenant, ownership, and permission filters from the authenticated user:\npublic async task\u0026lt;ireadonlylist\u0026lt;documentchunk\u0026gt;\u0026gt; searchallowedcontentasync( string modelgeneratedquery, claimsprincipal user, cancellationtoken cancellationtoken) { var tenantid = user.requiretenantid(); var allowedprojectids = await permissions.getallowedprojectidsasync( user, cancellationtoken); return await retrieval.searchasync( modelgeneratedquery, new retrievalfilter(tenantid, allowedprojectids), cancellationtoken); } the model controls the query text. it does not control the tenant, caller identity, allowed projects, roles, or authorization decision.\nuse prompts to guide behavior and wording. use authorization code to enforce boundaries. if the model is tricked by direct or indirect prompt injection, the application boundary should still prevent unauthorized data access and unsafe actions.\n","summary":"Do not let prompt instructions decide what data or actions a user is allowed to access.","tagSlugs":["c","net","ai-engineering","security","architecture"],"tags":["C#",".NET","AI Engineering","Security","Architecture"],"title":"Separate prompts from authorization","url":"/tips/separate-prompts-from-authorization/"},{"docs":[{"label":"Asynchronous programming scenarios","url":"https://learn.microsoft.com/dotnet/csharp/asynchronous-programming/async-scenarios"},{"label":"Task.WhenAll method","url":"https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.whenall"}],"excerpt":"When two asynchronous operations do not depend on each other, start both before awaiting either one. That lets the I/O work overlap instead of turning the method into a sequence of unnecessary waits.\nThis shows up often in API handlers and application services. Maybe one call loads the current customer, another loads feature flags, and a third calls an external pricing service. If those operations are independent, …","search":"start independent tasks before awaiting them create unrelated asynchronous operations first, then await them together. c# .net async performance async await task task whenall concurrency independent tasks io bound when two asynchronous operations do not depend on each other, start both before awaiting either one. that lets the i/o work overlap instead of turning the method into a sequence of unnecessary waits.\nthis shows up often in api handlers and application services. maybe one call loads the current customer, another loads feature flags, and a third calls an external pricing service. if those operations are independent, awaiting them one by one makes total latency closer to the sum of all three calls.\nstart the tasks first, then await them with task.whenall. after that, read each task\u0026rsquo;s result. the code still has one clear synchronization point, but the independent i/o operations can run at the same time.\ntask\u0026lt;customer\u0026gt; customertask = customerclient.getcustomerasync(customerid, cancellationtoken); task\u0026lt;ireadonlylist\u0026lt;price\u0026gt;\u0026gt; pricingtask = pricingclient.getpricesasync(cancellationtoken); await task.whenall(customertask, pricingtask); customer customer = await customertask; ireadonlylist\u0026lt;price\u0026gt; pricing = await pricingtask; await the completed tasks instead of reading .result. it keeps the call path async and avoids changing exception behavior just because you are collecting results.\nbe careful with ef core. a single dbcontext instance does not support multiple parallel operations. if two database queries truly need to run concurrently, use separate dbcontext instances, for example through idbcontextfactory, and make sure the database can handle the extra concurrency.\ndo not use this pattern when the second operation needs data from the first, when the operations must happen in a specific order, or when parallel calls would overload a dependency. for http fan-out, also think about connection limits, downstream rate limits, timeouts, and cancellation. concurrency is useful when the boundary can handle it. it is not a free performance switch.\n","summary":"Create unrelated asynchronous operations first, then await them together.","tagSlugs":["c","net","async","performance"],"tags":["C#",".NET","Async","Performance"],"title":"Start independent tasks before awaiting them","url":"/tips/start-independent-tasks-before-awaiting/"},{"docs":[{"label":"Using function tools with human in the loop approvals","url":"https://learn.microsoft.com/agent-framework/agents/tools/tool-approval"},{"label":"Using function tools with an agent","url":"https://learn.microsoft.com/agent-framework/agents/tools/function-tools"}],"excerpt":"Do not put every tool call behind human approval just because an agent is involved. Approval should protect meaningful risk, not turn every lookup into a modal.\nStart with side effects. Sending an email, deploying, deleting data, issuing a refund, changing permissions, publishing content, placing an order, or calling an external system on behalf of a user may deserve approval. A read-only lookup usually does not, …","search":"use approval for side effects, not for every tool call reserve human approval for actions where a wrong call would actually matter. c# .net ai engineering agents approval tool approval human in the loop side effects agent tools approval fatigue risk do not put every tool call behind human approval just because an agent is involved. approval should protect meaningful risk, not turn every lookup into a modal.\nstart with side effects. sending an email, deploying, deleting data, issuing a refund, changing permissions, publishing content, placing an order, or calling an external system on behalf of a user may deserve approval. a read-only lookup usually does not, assuming authorization and validation are already correct.\noverusing approval creates fatigue. users stop reading the request, developers look for ways around the friction, and the approval screen becomes theater instead of a real control.\nclassify tools by risk. some tools can run automatically after validation. some need stricter authorization. some need human approval. some should not be exposed to the agent at all.\nin microsoft agent framework, that distinction can be made at the tool boundary:\naifunction getcustomer = aifunctionfactory.create(getcustomerasync); aifunction issuerefund = aifunctionfactory.create(issuerefundasync); aifunction approvalrequiredrefund = new approvalrequiredaifunction(issuerefund); aiagent supportagent = chatclient.asaiagent( name: \u0026#34;support-agent\u0026#34;, instructions: \u0026#34;help support staff inspect customers and prepare refunds.\u0026#34;, tools: [ getcustomer, approvalrequiredrefund ]); the read-only lookup can run automatically after normal validation and authorization. the refund tool is a side effect, so the agent can request it, but the application gets an approval request before execution continues.\napproval is strongest when it is rare, specific, and understandable. show the exact operation and arguments, then let the user decide whether that side effect should happen.\n","summary":"Reserve human approval for actions where a wrong call would actually matter.","tagSlugs":["c","net","ai-engineering","agents","approval"],"tags":["C#",".NET","AI Engineering","Agents","Approval"],"title":"Use approval for side effects, not for every tool call","url":"/tips/use-approval-for-side-effects-not-for-every-tool-call/"},{"docs":[{"label":"File-based apps","url":"https://learn.microsoft.com/dotnet/core/sdk/file-based-apps"},{"label":"Build file-based C# programs","url":"https://learn.microsoft.com/dotnet/csharp/fundamentals/tutorials/file-based-programs"}],"excerpt":"Small repo utilities often start as shell scripts, copied console apps, or commands hidden in a README. File-based C# apps are a good fit when the task benefits from C# and .NET libraries, but a full project would be more structure than the utility deserves.\nUse them for migration helpers, data cleanup commands, local diagnostics, one-off API calls, small code generators, repros, and repository maintenance tasks. A …","search":"use file-based c# apps for small repo utilities use a single c# file when a repo utility is useful but a full project would be ceremony. c# .net developer tools file-based apps csharp dotnet run repo utilities scripts .net 10 cli tools automation small repo utilities often start as shell scripts, copied console apps, or commands hidden in a readme. file-based c# apps are a good fit when the task benefits from c# and .net libraries, but a full project would be more structure than the utility deserves.\nuse them for migration helpers, data cleanup commands, local diagnostics, one-off api calls, small code generators, repros, and repository maintenance tasks. a single .cs file can live near the workflow it supports and still use packages, arguments, configuration, and normal .net apis.\nthe key is scope. file-based apps are best when the tool has one clear job. if it grows into shared infrastructure, needs tests, has multiple commands, or becomes part of the product build, convert it into a normal project.\nkeep these utilities visible and boring. put them in a predictable folder such as tools or scripts, document how to run them, and avoid mixing them into existing project directories where sdk and build configuration can become surprising.\n","summary":"Use a single C# file when a repo utility is useful but a full project would be ceremony.","tagSlugs":["c","net","developer-tools"],"tags":["C#",".NET","Developer Tools"],"title":"Use file-based C# apps for small repo utilities","url":"/tips/use-file-based-csharp-apps-for-small-repo-utilities/"},{"docs":[{"label":"HybridCache library in ASP.NET Core","url":"https://learn.microsoft.com/aspnet/core/performance/caching/hybrid"}],"excerpt":"Cache stampedes happen when many requests miss the same cache entry at the same time and all of them recompute or reload the value. That can turn a cache miss into a database spike, an API quota problem, or a slow dependency incident.\nUse HybridCache for shared expensive lookups where stampede protection matters. It gives you one cache abstraction for local memory and optional distributed storage. Even without Redis …","search":"use hybridcache when cache stampedes matter use hybridcache for expensive shared lookups where many callers can miss at once. c# .net asp.net core performance caching hybridcache cache stampede distributed cache memory cache getorcreateasync cache invalidation thundering herd cache stampedes happen when many requests miss the same cache entry at the same time and all of them recompute or reload the value. that can turn a cache miss into a database spike, an api quota problem, or a slow dependency incident.\nuse hybridcache for shared expensive lookups where stampede protection matters. it gives you one cache abstraction for local memory and optional distributed storage. even without redis or another idistributedcache, it still gives you in-process caching and coordinates concurrent callers for the same key.\nthat makes it a better default than raw imemorycache when many requests can miss the same expensive value at once:\napp.mapget(\u0026#34;/api/tenant/{id}/settings\u0026#34;, async ( string id, hybridcache cache, tenantdb db, cancellationtoken ct) =\u0026gt; { return await cache.getorcreateasync( $\u0026#34;tenant-settings-{id}\u0026#34;, async cancel =\u0026gt; await db.getsettingsasync(id, cancel), options: new hybridcacheentryoptions { expiration = timespan.fromminutes(5) }, tags: [\u0026#34;tenant-settings\u0026#34;], cancellationtoken: ct); }); good candidates are configuration snapshots, reference data, expensive read models, model metadata, tenant settings, and external api results that can tolerate short-lived staleness.\ndo not hide business correctness behind a cache. make the key explicit, set an intentional expiration, and keep invalidation or refresh behavior visible. if related entries need to be refreshed together, use tags and invalidate them with removebytagasync. treat that as part of the write path, not as a background cleanup detail.\na cache should reduce pressure, not make state changes impossible to reason about.\n","summary":"Use HybridCache for expensive shared lookups where many callers can miss at once.","tagSlugs":["c","net","aspnet-core","performance","caching"],"tags":["C#",".NET","ASP.NET Core","Performance","Caching"],"title":"Use HybridCache when cache stampedes matter","url":"/tips/use-hybridcache-when-cache-stampedes-matter/"},{"docs":[{"label":"Authenticate Azure-hosted .NET apps with managed identity","url":"https://learn.microsoft.com/dotnet/azure/sdk/authentication/user-assigned-managed-identity"},{"label":"Credential chains in the Azure Identity library","url":"https://learn.microsoft.com/dotnet/azure/sdk/authentication/credential-chains"}],"excerpt":"Connection strings with embedded secrets are easy to start with and painful to operate. They need rotation, storage, environment-specific handling, incident response, and careful access control across every place they appear.\nFor Azure-hosted applications, prefer managed identity when the target service supports Microsoft Entra authentication. The app gets an identity from Azure, and permissions are assigned to that …","search":"use managed identity before connection strings prefer azure-managed identities over long-lived secrets in deployed applications. c# .net azure security managed identity connection strings azure identity entra id secrets tokencredential azure sdk connection strings with embedded secrets are easy to start with and painful to operate. they need rotation, storage, environment-specific handling, incident response, and careful access control across every place they appear.\nfor azure-hosted applications, prefer managed identity when the target service supports microsoft entra authentication. the app gets an identity from azure, and permissions are assigned to that identity through roles instead of copied secrets.\nthis works especially well for services such as storage, key vault, service bus, azure sql, and many azure sdk clients. your code asks for a token through azure identity, and azure decides whether the running workload is allowed to access the resource.\nin .net, the usual starting point is defaultazurecredential in composition code:\nusing azure.core; using azure.identity; using azure.storage.blobs; tokencredential credential = new defaultazurecredential(); builder.services.addsingleton(_ =\u0026gt; new blobserviceclient( new uri(builder.configuration[\u0026#34;storage:blobendpoint\u0026#34;]!), credential)); that keeps local development practical. the same code can use developer credentials from the azure cli, visual studio, or other supported tools on your machine, then use the managed identity when the app runs in azure.\nchoose the identity deliberately. a system-assigned managed identity is tied to one azure resource and follows its lifecycle. a user-assigned managed identity is a separate azure resource that can be reused and managed independently, which is often nicer for infrastructure-as-code and stable rbac assignments.\nazure sql fits the same identity-first rule, but the shape is different. with microsoft.data.sqlclient, you normally configure microsoft entra authentication in the connection string, for example authentication=active directory default, instead of passing a tokencredential to an azure sdk client constructor.\nconnection strings are still sometimes necessary, but they should not be the default. start with identity-based access, assign the least role the app needs, and reserve secrets for cases where the dependency cannot support managed identity.\n","summary":"Prefer Azure-managed identities over long-lived secrets in deployed applications.","tagSlugs":["c","net","azure","security"],"tags":["C#",".NET","Azure","Security"],"title":"Use managed identity before connection strings","url":"/tips/use-managed-identity-before-connection-strings/"},{"docs":[{"label":".NET observability with OpenTelemetry","url":"https://learn.microsoft.com/dotnet/core/diagnostics/observability-with-otel"},{"label":"Adding distributed tracing instrumentation","url":"https://learn.microsoft.com/dotnet/core/diagnostics/distributed-tracing-instrumentation-walkthroughs"}],"excerpt":"Do not wait until production is already slow or failing before adding observability. At that point, you are trying to understand a live system with missing evidence, partial logs, and no reliable view of where time is spent.\nAdd OpenTelemetry while the system is still small enough to reason about. Start with the boring signals: request duration, dependency calls, database queries, queue operations, cache misses, …","search":"use opentelemetry before you need production debugging add traces, metrics, and logs while the system is still easy to reason about. c# .net observability reliability architecture opentelemetry observability distributed tracing metrics logs activitysource production debugging diagnostics do not wait until production is already slow or failing before adding observability. at that point, you are trying to understand a live system with missing evidence, partial logs, and no reliable view of where time is spent.\nadd opentelemetry while the system is still small enough to reason about. start with the boring signals: request duration, dependency calls, database queries, queue operations, cache misses, model calls, retries, and failures. those traces and metrics become the baseline you will need later.\nin .net, keep the opentelemetry packages at the hosting edge where you configure collection, sampling, and export. application and domain code should normally emit through the bcl apis: system.diagnostics.activitysource for traces, system.diagnostics.metrics.meter for metrics, and ilogger for logs. that keeps the core code independent from the telemetry backend.\nthe host setup can be small:\nbuilder.services.addopentelemetry() .withtracing(tracing =\u0026gt; tracing .addsource(builder.environment.applicationname) .addaspnetcoreinstrumentation() .addhttpclientinstrumentation() .addsqlclientinstrumentation()) .withmetrics(metrics =\u0026gt; metrics .addmeter(builder.environment.applicationname) .addruntimeinstrumentation()); those instrumentation calls give you a lot of the boring signals without hand-written spans: incoming asp.net core requests, outgoing httpclient calls, sql client activity, and runtime metrics. add your own activitysource only around boundaries the framework cannot see, such as a business workflow step, a tool invocation, or a queue handoff.\nfor ai calls built on microsoft.extensions.ai, prefer the built-in useopentelemetry(...) chat client middleware over custom timing wrappers. configure it with the source name you collect in the host, then add the same source and meter names through addsource(...) and addmeter(...). that is usually enough to see model calls and token-related metrics when the provider returns usage information.\nthe point is not to collect everything. the point is to make important boundaries visible. when a request crosses into a database, http dependency, background job, storage service, or ai model call, there should be enough telemetry to answer what happened, how long it took, and which part failed.\nkeep the signal safe and useful. do not put secrets, prompts with sensitive user data, access tokens, raw payloads, or personally identifiable information into tags and logs. if you enable sensitive ai telemetry such as raw inputs, outputs, tool arguments, or tool results, treat that as production data with explicit access control and retention rules.\nuse this early for services where production debugging would cross process, database, queue, cache, or model boundaries. do not use telemetry as a dumping ground for payloads you would not store in a normal audit log.\n","summary":"Add traces, metrics, and logs while the system is still easy to reason about.","tagSlugs":["c","net","observability","reliability","architecture"],"tags":["C#",".NET","Observability","Reliability","Architecture"],"title":"Use OpenTelemetry before you need production debugging","url":"/tips/use-opentelemetry-before-you-need-production-debugging/"},{"docs":[{"label":"Handle errors in ASP.NET Core APIs","url":"https://learn.microsoft.com/aspnet/core/fundamentals/error-handling-api"},{"label":"ProblemDetails class","url":"https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.mvc.problemdetails"}],"excerpt":"Use ProblemDetails as the public error contract for an API instead of returning ad hoc error objects from different endpoints. Clients should not have to handle one shape for validation errors, another shape for exceptions, and a third shape for business rule failures.\nThe value is bigger than the JSON format. Every error can carry the same core fields: status, title, detail, type, and instance. That gives clients a …","search":"use problemdetails as your api error contract return one predictable error shape instead of inventing a new json format for every failure. c# .net asp.net core apis problemdetails api errors error contract validation exceptions http api rfc 7807 use problemdetails as the public error contract for an api instead of returning ad hoc error objects from different endpoints. clients should not have to handle one shape for validation errors, another shape for exceptions, and a third shape for business rule failures.\nthe value is bigger than the json format. every error can carry the same core fields: status, title, detail, type, and instance. that gives clients a predictable contract while still leaving room for extensions such as trace ids, error codes, validation keys, or support links.\nin asp.net core, configure problem details once and let middleware or mvc produce consistent responses for common failure paths. then use explicit problem responses for domain-level errors where the api needs to explain what the caller can do next.\nbuilder.services.addproblemdetails(); if (!app.environment.isdevelopment()) { app.useexceptionhandler(); } app.mappost(\u0026#34;/api/transfer\u0026#34;, (transferrequest request) =\u0026gt; { if (request.amount \u0026gt; request.balance) { return results.problem( statuscode: 400, title: \u0026#34;insufficient balance\u0026#34;, detail: \u0026#34;the transfer amount exceeds the available balance.\u0026#34;, extensions: new dictionary\u0026lt;string, object?\u0026gt; { [\u0026#34;errorcode\u0026#34;] = \u0026#34;insufficient_funds\u0026#34; }); } return results.ok(); }); keep the details safe for production. a problem response should describe the api error, not leak stack traces, connection strings, model prompts, dependency payloads, or internal exception messages. log the internal detail, return the client-facing contract.\n","summary":"Return one predictable error shape instead of inventing a new JSON format for every failure.","tagSlugs":["c","net","aspnet-core","apis"],"tags":["C#",".NET","ASP.NET Core","APIs"],"title":"Use ProblemDetails as your API error contract","url":"/tips/use-problemdetails-as-your-api-error-contract/"}]}