People ask me which of my posts to read if they want to get better at product management. I never had a good answer, because I did not write them as a curriculum. I wrote them because something was on fire.
This is the inventory.
The job, as I have been saying since 2015:
Builders create the product. The PM’s job is to generate clarity and commitment so builders can create magic, and to protect one coherent customer experience across the rest of the company.
Everything below is a tool for doing that job. If a post does not help you create clarity, make a choice, or install a mechanism, it is not in the toolbox.
1. What the job is
Start here if you think PM means owning the backlog, insulating engineers from customers, or being the smartest person in the room.
Program: You Keep Using That Word is the lexicon: Program vs Product vs Project vs PM/PgM/PjM. If you cannot say which one you are doing, you are already misaligned.
Experience = Stuff / Time is the definition of an end-to-end experience: devices, people, brands, channels, services, and content, improving over time.
If it is not written in complete sentences, it is not thought through. If it is not debated early, execution will suck later because folks will be fundamentally misaligned.
Good intentions never survive contact with a calendar. If the behavior has to happen when you are not in the room, it needs an owner, a tool, adoption, and inspection.
If you cannot explain the PM job without saying “backlog,” start at 1. If you are about to build something, start at 2 and write a PRFAQ. If the room is using the same words and meaning different things, start at 3. If everything is urgent, start at 4. If it only works when you personally hero it, start at 5.
I am putting these under a Product Management category on tig.log so the set is findable as a set. If I missed a tool, tell me.
Max Corbridge, an ethical hacker and red teamer who is co-founder and CEO of Secure Agentics, speaks with SE Radio host Amey Ambade about how AI agents get attacked and what engineers can actually do to defend them. Drawing on years of offensive security work, Corbridge frames agents as a new and largely undefended attack surface: the industry has handed AI systems autonomy and the ability to act in the real world while carrying forward prompt injection, a flaw the frontier labs themselves describe as effectively unsolvable. He likens the moment to the early, lawless days of the web, when SQL injection was everywhere and adoption ran far ahead of security.
The conversation builds from first principles as Corbridge explains what separates an agent from ordinary software and why three properties make them hard to secure: they are non-deterministic, their language-model core can be coerced, and they are increasingly interconnected through MCP servers, other agents, databases, and email. Turning to the attack surface, Corbridge lays out his "lethal trifecta" (a vulnerable core, dense interconnection, and security tooling that has not caught up) and contrasts the decades of layered defenses protecting an ordinary email inbox with the thin protection around agents that take autonomous actions on critical systems.
The heart of the episode is defense. Corbridge orders practices by leverage: least-privilege access and privilege separation, sandboxing where feasible, imperfect-but-useful guardrails as one layer of defense in depth, and human-in-the-loop for irreversible actions (which he notes is contentious and does not scale). The discussion closes on detecting a compromised or drifting agent, the value of watching an agent's chain-of-thought reasoning alongside its actions, the open-source tooling landscape (including Corbridge's own project, Adrian), and his central advice: build security in proactively, define what good agent behavior looks like up front, and avoid bolting it on after agents have already spread across the business.
Grok 4.6 is fast, capable, dramatically cheaper than the leading models—and another sign that AI users have more genuinely strong options than ever. NLW explores how competition from xAI, Chinese labs, and open-weight models is giving individuals and businesses more freedom to choose the right combination of intelligence, speed, and price. In the headlines: massive funding rounds, booming infrastructure demand, and changes to the White House model-testing framework.
Every release post covers the headline. Thirteen major versions of those leaves a lot of API that shipped
in a bullet list and never got explained — which is a shame, because some of it is the stuff I actually
reach for most.
This post is the sweep. Three features get real depth because they change how you’d write the code around
them, and the rest are quick hits. Nothing here is new in v13; all of it is in the box today.
1. The store already knows what changed
You have a document in the database and an object in memory that’s been edited by a form, a merge, or an
LLM. The question every app asks next is what actually changed? — for an audit line, for a
confirmation screen, for an “are you sure, this touches 4 fields” dialog.
The usual answer is to write a comparer. You don’t have to:
GetDiff reads the stored document, compares it against the candidate, and hands back a
JsonPatchDocument<T> — a real RFC 6902 patch, not a string. Note the shape of it: this is a read. You
haven’t written anything yet, so it’s the thing to call before the save, which is exactly when the
question gets asked.
Writing only what changed
The write side has the same idea from the other direction. A full Update replaces the stored body; an
Upsert deep-merges it, RFC 7396:
// only the non-null properties are touched; everything else stays as stored
await store.Upsert(newOrder { Id = id, Status ="shipped" });
That’s the default behaviour of Upsert on every provider. Its inverse — replace-on-update rather than
merge — is Upsert(patch, patchIfUpdate: false), which the relational providers implement and the rest
refuse rather than fake. There’s a matching Update(document, patch: true) when the document must already
exist.
For a genuine partial update through a typed object, remember the type has to be able to express
“unset”: JsonIgnoreCondition.WhenWritingNull on the properties, or use the
JSON collection lane where the body is a JsonObject and absent means
absent.
And when it’s one field, skip the object entirely:
Both return bool — false when no such document — and both are a single statement against the JSON
column. No read, no round trip, no lost update from the write you didn’t know was concurrent with yours.
Then turn on temporal and it gets silly
Map a type as temporal and every one of those writes leaves a version behind:
await temporal.History<Order>(id); // every version
await temporal.AsOf<Order>(id, lastTuesday); // the document as it was
await temporal.ChangesByActor<Order>("user:42"); // everything one actor touched
await temporal.GetDiffBetween<Order>(id, 3, 7); // a patch between two versions
await temporal.Restore<Order>(id, version: 3); // put it back
That’s an audit trail, a point-in-time read, a per-user change log and an undo button, from one line of
configuration. It works on every provider — relational, Cosmos, MongoDB, LiteDB, IndexedDB — through
ITemporalDocumentStore, which you probe for rather than assume (store is ITemporalDocumentStore).
The reason I keep pointing at this one: almost everybody hand-rolls a ChangeLog table, and it’s almost
always worse than this, because a hand-rolled one records the fields somebody remembered to record.
2. Stop deserializing JSON just to serialize it again
Here’s the shape of an enormous number of API endpoints:
varorder=await store.Get<Order>(id); // JSON -> Order
return Results.Ok(order); // Order -> JSON
});
The database handed you a perfectly good JSON document. You parsed it into an object graph, allocated
every string and list in it, and then serialized it straight back into bytes that are — modulo whitespace
— what you started with. Order did no work. It was overhead with a type name.
return raw isnull? Results.NotFound() : Results.Content(raw, "application/json");
});
And for a list, don’t even materialize the list — stream it into the response as it comes off the reader:
ctx.Response.ContentType ="application/json";
await store.Query<Order>()
.Where(o=> o.Status =="open")
.OrderByDescending(o=> o.CreatedAt)
.WriteJsonArrayTo(ctx.Response.Body, ct);
The point that makes this usable rather than a curiosity: you still build the query with the typed
surface.Where, OrderBy, Paginate, global query filters, soft delete, tenancy — all of it applies,
because only the terminal changed. You get the compiler checking your predicate and the database
handing back bytes.
There’s a node lane too, when you want to touch the JSON before it leaves: ToJsonList,
ToJsonAsyncEnumerable, FirstJson / FirstOrDefaultJson, SingleJson / SingleOrDefaultJson,
ToJsonCursorPage — all returning JsonObject, all built on the same RawJsonRows primitive the raw
lane uses.
The fidelity rules, because they matter
On the relational providers and Cosmos DB, these are the persisted bytes, untouched. Zero parses.
Everywhere else the provider has to materialize T to finish the query, so the body is re-serialized
through the type’s JsonTypeInfo. Same JSON, same API — but the round trip is real, and you should
expect no win.
Materialized computed properties live outside the body, so they don’t appear. A DocumentBlob shows up
as its metadata envelope, not its payload.
A type with encrypted properties throws. The stored body is ciphertext, and only the typed
terminals decrypt.
That last group is why there’s a SupportsRawJson flag on the query. Test it rather than catching the
throw when the JSON lane is an optimization and the typed path is still correct — which is exactly how the
built-in OData and AI surfaces pick a lane.
Trim the fields on the way out
Pair it with the string projection when the caller only wants some of the document:
// a REST ?fields= sparse fieldset, resolved at runtime
varrows=await store.Query<Order>()
.Project("id, number, total, customer.name as customer, lower(status) as status")
.ToJsonList();
Dotted paths reach into nested objects and become first-class output keys. Scalar functions from the
string grammar (lower, length, substring, year, soundex, …) can be projected too, and require an
alias. Relational providers do this in SQL; the document providers do it client-side.
3. Everything soft delete taught me about extensibility
Deletes set the flag instead of deleting, and every read hides flagged documents. The part worth writing
about is that nothing in any store knows it exists. Here is, essentially, the whole implementation:
options.AddInterceptor(interceptor); // cancel the delete, set the flag instead
options.AddBulkInterceptor(interceptor); // same for ExecuteDelete / Clear
Three public calls. No provider changes, no if (softDelete) anywhere in the query pipeline, and it
works identically on all twenty-odd backends because it never went near one.
The two primitives it’s made of are worth knowing on their own.
ctx.Cancel() — replace a write, don’t just watch it
An interceptor’s BeforeWrite can substitute itself for the write:
Cancel() means the store does nothing, no AfterWrite runs, and no change notification is published —
the caller gets the outcome you name (Cancel(succeeded: false) reports failure). It’s only legal inside
BeforeWrite; calling it later throws rather than silently doing nothing. ctx.Session is scoped to the
write’s own transaction, so the archive row commits with the operation that caused it, or not at all.
That’s an append-only archive, in about twenty lines, that no provider needed to hear about.
Named query filters — and lifting them one query at a time
A global filter usually gets registered anonymously and then becomes a problem the first time an admin
screen needs to see past it. Give it a name:
store.Query<Order>().IgnoreQueryFilters("archived"); // just this one
store.Query<Order>().IgnoreQueryFilters(); // all of them
Which is exactly what soft delete’s own IncludeDeleted() does — it’s a one-line extension over
IgnoreQueryFilters(SoftDelete.FilterName). Features here are meant to be built this way: extension
methods over public hooks, so an optional feature never becomes a member on an options class that every
provider has to carry.
Quick hits
ToQueryString() — see what your LINQ actually became, without running it.
Console.WriteLine(q.Sql); // the provider's SQL (or MongoDB's rendered BSON)
Console.WriteLine(q.Parameters); // the bound values
Relational providers and Cosmos return SQL; MongoDB returns its filter as JSON; the in-memory evaluators
(LiteDB, IndexedDB) throw, because there’s nothing to show.
Cursor pagination — Skip/Take gets slower the deeper you go and shifts under concurrent writes.
Keyset paging doesn’t:
CursorPage<Order> page=await store.Query<Order>()
.Where(o=> o.Status =="open")
.OrderByDescending(o=> o.CreatedAt)
.ToCursorPage(cursor, take: 50);
page.Items; // this page
page.NextCursor; // opaque token; null means that was the last page
page.HasMore;
O(log n) per page with an index on the sort key, an Id tiebreaker appended for you, and a shape hash so
a cursor can’t be replayed against a differently-filtered query. ToJsonCursorPage is the same thing in
the JSON lane. There’s no total count — that’s what Paginate is for.
DocumentFunctions.Soundex — fuzzy name matching that pushes down to the engine:
indexed: true asks for a materialized, indexable computed column where the backend has one; without it,
it’s an alias expanded into the query.
IDocumentMaintenance — ClearAll() wipes every type including temporal, spatial and vector sidecars
(tests and dev resets, not tenant-scoped), and SweepOrphanedBlobs<T>() collects blob rows whose owning
document went away out of band. Probe for it: store is IDocumentMaintenance.
And the tools nobody’s seen
The two most under-advertised things in the project aren’t API at all. ShinyDocDbMyAdmin has had a
terminal front end since v12.5 — the same tool as the web UI, as a dotnet tool, over SSH — and the web
one has shipped as a Docker Desktop extension since 13.0.1, which
hands it every database container already running on your machine, connected.
Both deserve their own post with screenshots, and they’re getting one. In the meantime:
the admin docs.
In the .NET development world the two most significant frameworks for AI are Microsoft Agent Framework and Microsoft Foundry. Together, they create a powerful ecosystem for building, deploying, and managing AI agents that can automate tasks, respond to user queries, and integrate seamlessly with various services. This post will explore how these two technologies relate to each other, their key features, and their real-world applications.
What is Microsoft Agent Framework?
To quickly review, Microsoft Agent Framework is a development framework designed specifically for creating AI agents. These agents are capable of interacting with various services and data sources, making them versatile tools for developers. The framework provides a rich set of tools and libraries that enable developers to build intelligent applications that can automate tasks, respond to user queries, and integrate with other systems.
Key Features of Microsoft Agent Framework
Development Tools: The framework includes a variety of libraries and APIs that simplify the process of building AI agents. Developers can leverage these tools to create agents that can understand natural language, process data, and perform complex tasks.
Integration Capabilities: The framework is designed to work with various data sources and services, allowing developers to create agents that can pull information from multiple platforms and provide comprehensive responses to user queries.
Flexibility: Developers can use the Microsoft Agent Framework alongside other frameworks, such as the OpenAI Agents SDK, to create a wide range of applications, from simple chatbots to complex AI-driven solutions.
For much more on Microsoft Agent Framework and agentics in general see the blog posts beginning here.
What is Microsoft Foundry?
Microsoft Foundry is a managed platform that provides a comprehensive environment for building, deploying, and scaling AI agents. It offers a suite of tools and services that enable developers to utilize various AI models and frameworks, making it easier to create sophisticated applications.
Key Features of Microsoft Foundry
Managed Environment: Foundry provides a fully managed environment, which means developers can focus on building their applications without worrying about the underlying infrastructure. This allows for faster development cycles and easier scaling.
AI Model Integration: Foundry supports a wide range of AI models and tools, enabling developers to leverage the latest advancements in AI technology. This integration allows for the creation of more intelligent and capable agents.
Governance and Observability: Foundry includes features that help organizations maintain compliance with regulations, making it particularly beneficial for industries that require strict governance, such as finance and healthcare.
The Relationship Between Microsoft Agent Framework and Microsoft Foundry
The relationship between the Microsoft Agent Framework and Microsoft Foundry is one of synergy and integration. Together, they provide a robust environment for developing AI agents that can handle complex tasks and workflows. Here are some key aspects of their relationship:
1. Integration of Services
The Foundry Agent Service acts as a bridge between the Microsoft Agent Framework and various data sources and other agents. This integration enables seamless communication and data exchange, which is crucial for developing multi-agent workflows that can manage complex business processes.
Additionally, the Responses API serves as a single entry point for accessing Foundry models and tools. This allows developers to build agents using the Agent Framework while leveraging the capabilities of Foundry, creating a more cohesive development experience.
2. Multi-Agent Workflows
Both Foundry and Microsoft Agent Framework support the creation of multi-agent workflows. This feature allows developers to orchestrate complex, multi-step processes, enhancing the capabilities of AI applications. By enabling multiple agents to work together, organizations can automate intricate workflows that would be challenging to manage with a single agent.
3. Identity and Security
Security is a paramount concern in the development of AI applications. The Microsoft Agent Framework utilizes Microsoft Entra ID for managing agent identities, ensuring secure authentication and authorization. With Foundry you get this and many other infrastructure features out of the box.
4. Development Flexibility
The combination of the Microsoft Agent Framework and Microsoft Foundry offers developers significant flexibility in how they create agents. They can choose to build agents using the Microsoft Agent Framework and have them hosted in Foundry, or they can create AI applications directly with Foundry. This flexibility is essential for meeting the diverse needs of businesses and organizations.
For more on this, be sure to watch my video interview of Bruno Capuano and Jon Galloway, both of Microsoft, where, among other things, Bruno demonstrates how easy it is to have Foundry host a Microsoft Agent Framework application.
This is a preview build of WinGet for those interested in trying out upcoming features and fixes. While it has had some use and should be free of major issues, it may have bugs or usability problems. If you find any, please help us out by filing an issue.
New in v1.30
--ignore-unavailable flag for install
Added a new --ignore-unavailable flag to the install command. When installing multiple packages, this flag allows the operation to continue with the remaining packages instead of failing entirely when one or more packages are not found in the configured sources. This brings the same behavior previously available with import --ignore-unavailable to direct multi-package installs.
Bug Fixes
Updated NUnit to v4
Fixed a crash (0x8000ffff) when using --disable-interactivity with the Resume experimental feature enabled during install operations.