Assume the container is running and reachable at http://presidio-analyzer:3000. The request is a JSON body with the text, a language, and optionally the entities you care about.
public sealed record PiiFinding(
string EntityType,
int Start,
int End,
double Score);
public sealed class PresidioAnalyzer(HttpClient http)
{
private static readonly JsonSerializerOptions Json = new()
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
};
public async Task<IReadOnlyList<PiiFinding>> AnalyzeAsync(
string text,
string language = "en",
CancellationToken ct = default)
{
var request = new
{
text,
language,
score_threshold = 0.5
};
using var response = await http.PostAsJsonAsync("/analyze", request, ct);
response.EnsureSuccessStatusCode();
return await response.Content
.ReadFromJsonAsync<List<PiiFinding>>(Json, ct) ?? [];
}
}Register it as a typed client so that timeouts and retries are policy rather than an afterthought:
builder.Services.AddHttpClient<PresidioAnalyzer>(c =>
{
c.BaseAddress = new Uri(builder.Configuration["Presidio:AnalyzerUrl"]!);
c.Timeout = TimeSpan.FromSeconds(2);
});Two decisions are already embedded in that snippet and both deserve to be conscious.
The score threshold is your precision and recall dial, and 0.5 is a default rather than an answer. Chapter 7 is about choosing it on your own data.
The two-second timeout sets up the question Chapter 13 answers properly: what happens when this call fails? A catch that logs a warning and proceeds is a system that silently stops protecting anything the moment the container restarts. That is failing open, and failing open is the leak.
By default the analyzer looks for everything it knows. That is slower and noisier than you want. Name the entities your policy actually covers:
var request = new
{
text,
language = "en",
entities = new[]
{
"PERSON", "EMAIL_ADDRESS", "PHONE_NUMBER",
"US_SSN", "US_BANK_NUMBER", "CREDIT_CARD",
"LOCATION", "US_DRIVER_LICENSE"
},
score_threshold = 0.5
};Write that list down somewhere a human reads, because it is the exact boundary of what you claim to detect. Everything outside it is undetected by design, and "by design" is a much better answer in an audit than discovering the gap during one.
Azure AI Language PII detection is the serious .NET-native alternative, and it is a good product. Three feature types: Text PII for synchronous string payloads, Conversation PII for turn-based transcripts, and Document PII for native .pdf, .docx and .txt files, which returns redacted files with the document structure preserved plus machine-readable metadata. Official client libraries for C#, Java, JavaScript and Python. Configurable redactionPolicies from the 2025-11-15-preview API version, with more than one policy per request. Entity categories come back with confidence scores, and the model is not customised on your data.
For a .NET team this is the path of least resistance by a distance. Add a package, add a key, call a method.
And now the tension, which no vendor page will state for you: a cloud PII-detection API requires you to transmit the sensitive data in order to locate it.
Read that twice, because it is easy to nod past. To find out whether a payload contains personal data, you send the payload. The detection call is itself a disclosure to a third-party processor. You have not removed a data flow, you have added one.
That is sometimes completely fine. Azure offers regional processing with contractual commitments, you likely already have a data processing agreement with Microsoft, and adding one more Microsoft processor to a Microsoft-hosted application changes your risk profile very little. If your model calls are going to Azure OpenAI anyway, routing detection through Azure AI Language adds no new party at all.
It is sometimes exactly wrong. If you self-host your model to keep text inside your perimeter, and then call a cloud API to detect the personal data in that text, you have defeated the entire design with the control that was supposed to enforce it. Teams do this. It happens because detection feels like a security function rather than a data flow, and security functions do not feel like they need the same scrutiny as features.
The test is simple. Draw your trust boundary. Ask whether the detection call crosses it. If it does, that is a decision, and it needs to be a deliberate one rather than a default.
Azure's own data, privacy and security documentation answers the question this book flagged as open in earlier drafts, and the answer is favourable. Language does not store or process customer data outside the region where you deploy the instance, and encrypts all content at rest. Request data may be held for up to 48 hours for debugging by on-call engineers after a catastrophic failure, controlled by the LoggingOptOut query parameter, and that parameter defaults to true on the PII and health endpoints specifically — so the temporary storage that applies to sentiment or key-phrase calls does not apply to the calls you would be making. Verified against Microsoft's documentation on 14 September 2026.
That does not dissolve the tension above. Regional processing and a 48-hour opt-out default are strong terms, and they are still terms: the text crosses your perimeter and you are relying on a contract rather than on a mechanism. Chapter 16 makes that distinction properly.
The self-hosted sidecar, for one reason that outranks the others: it is the only option where the text never crosses your perimeter.
The supporting reasons are the ones Chapter 4's benchmark author gave when making the same call. Presidio is a complete framework rather than a model, so the NER backend is replaceable. If a better detector appears, or you fine-tune one on your own data, you swap the recogniser and keep the pipeline. It runs at roughly 15 ms against 118 to 198 ms for the transformer alternatives, which is 8 to 11 times faster and the difference between a control you can run on everything and one you ration. Its anonymise and de-anonymise pipeline is the round trip in Chapter 9, already built. And it is actively maintained.
Notice what is not in that list. Accuracy. Presidio averaged 0.481 in the benchmark, third of four, and the difference between it and the leader was not statistically significant. You are choosing an architecture that can be improved, not a decimal place you cannot reproduce.
The sidecar also happens to be the shape Part IV argues for anyway: a self-hosted service behind a gateway, driven by versioned policy. Choosing it here costs nothing later.
The container is running and returning findings. What those findings are worth on your data is still entirely unknown, and that is a measurable thing rather than a matter of faith.
microsoft.github.io/presidio address redirects twice to this. Checked 11 September 2026.Presidio.SDK v0.0.2, June 2025, ~5.6K downloads, 14 commits, .NET 6.0 / .NET Standard 2.1, analyzer coverage only — https://www.nuget.org/packages/Presidio.SDK · https://github.com/StefH/Presidio.SDKredactionPolicies from API version 2025-11-15-preview, confidence-scored categories, no model customisation on customer data — https://learn.microsoft.com/en-us/azure/ai-services/language-service/personally-identifiable-information/overview · Page dated 30 June 2026, updated 3 August 2026.LoggingOptOut, which defaults to true on the PII and health endpoints — Microsoft, Data, privacy, and security for Azure Language: https://learn.microsoft.com/en-us/azure/ai-foundry/responsible-ai/language-service/data-privacy · Page dated 1 April 2026, updated 17 August 2026. Verified 14 September 2026.The argument that a cloud detection API constitutes a disclosure in order to prevent one is the author's, and it describes a data flow rather than a defect in the service. The book's choice of the self-hosted sidecar is a judgement made on the architectural grounds listed, not on measured accuracy.
Download the full PDF for free?
Free download — no account required