Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
155646 stories
·
33 followers

Inside JetPride: How JetBrains Employees Built an LGBTQIA+ Community

1 Share

What makes a workplace feel welcoming?

Sometimes it’s finding colleagues who share your interests. Sometimes it’s discovering people with similar experiences. Sometimes it’s knowing you can talk openly about your life, ask questions, share perspectives, and be yourself without giving it a second thought.

JetPride, our LGBTQIA+ employee resource group (ERG), is one place where people find that connection and support.

 This post is a look at the people behind the community, the ideas that shape it, and what it means to those who are part of it.

What is JetPride?

One of the things that makes JetBrains unique is how many initiatives begin with employees themselves. JetPride is one example.

JetPride was created by employees who wanted a dedicated space for LGBTQIA+ employees and allies at JetBrains. The community brings together people from different teams, disciplines, and locations.

One of the principles behind JetPride is that nobody needs to arrive with all the answers.

Some members are deeply involved in LGBTQIA+ communities. Others are encountering certain topics for the first time. Some join because they are looking for community. Others want to better understand the experiences of colleagues and friends.

The aim is simply to help people connect, better understand one another, and build a stronger sense of belonging.

A personal perspective

For Content Marketing Manager Conrad Schwellnus, belonging often shows up in small moments:

“Small moments of expression and shared language can help create a joyful, light-hearted, and welcoming environment, whether in person or on Slack.

Even playful cultural references or LGBTQIA+ terminology can help people feel seen and included when used respectfully and authentically.

At the same time, it’s important to leave room for questions, different comfort levels, and open dialogue. Curiosity, kindness, and mutual respect are what ultimately help create a space where everyone feels safe together.”

His perspective reflects one of the ideas at the heart of JetPride: understanding often starts with curiosity, conversation, and a willingness to learn from one another.

Growing with Pride

Pride Month is a time to celebrate LGBTQIA+ lives, history, creativity, resilience, and joy.

This year, JetPride is marking Pride Month with the theme Growing with Pride. The theme and visual identity were developed by JetPride Community Lead Gregorio Sanchez, who wanted to capture both the diversity of LGBTQIA+ communities and the idea that understanding grows over time.

Growing with Pride came from the idea that understanding rarely happens all at once. It grows through conversations, curiosity, shared experiences, and a willingness to listen to one another.

We also wanted the visual identity to reflect the diversity within LGBTQIA+ communities. Each flower draws inspiration from the colors of a different Pride flag, representing lesbian, gay, bisexual, trans, non-binary, intersex, asexual, aromantic, pansexual, queer, and other communities across the LGBTQIA+ spectrum.

Pride is often talked about as a single community, but it’s made up of many different identities, experiences, and stories. The flowers were designed to celebrate that diversity while reflecting the idea of growth at the heart of this year’s theme.”

Pride Month in action

While JetPride is active year-round, Pride Month is one of the community’s largest and most visible projects, bringing together employees across offices, teams, and locations.

This year’s activities include:

  • Employee stories exploring what helps people be themselves at work.
  • A Pride Month quiz covering LGBTQIA+ history, culture, and community.
  • Resources and guides on inclusive language and LGBTQIA+ terminology.
  • Country-specific resources highlighting local support organizations and information.
  • Talks, discussions, and learning sessions across offices.
  • Film screenings and community conversations.
  • Informal gatherings and local Pride celebrations.
  • Pride-themed pins and T-shirts created for community members and participants.
  • A social media campaign highlighting LGBTQIA+ visibility beyond JetBrains.


What makes Pride Month at JetBrains special is that activities are shaped locally. Rather than following a single format, offices organize events that reflect their own communities, interests, and ideas.

This year, colleagues in Amsterdam are hosting talks and a fireside chat. In Berlin, activities include DJ sets, a makeup station, and interactive games. Employees will also come together for a screening of the award-winning short film The Flowers, while colleagues in Cyprus, Munich, and other locations are planning to attend local Pride events together.

Other offices are organizing their own gatherings, conversations, treats, and community activities throughout June and beyond.

Together, these activities offer opportunities to learn something new, start conversations, celebrate Pride, and spend time with colleagues from across the company.

Looking ahead

Like many employee-led communities at JetBrains, JetPride exists because people saw an opportunity to create something meaningful and took the initiative to make it happen.

That’s one of the things that makes JetBrains special. People are trusted not only to contribute through their roles, but also to shape the experiences that become part of life here.

JetPride is one example of what can happen when people decide to build something together.

If you’re looking for a place where you can do meaningful work, keep growing, and help shape what comes next, explore our open roles and learn more about life at JetBrains on our careers page.

Read the whole story
alvinashcraft
32 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Beyond the Chatbot: AI-Enabled .NET MAUI Apps with Codrina Merigo

1 Share

Strategic Technology Consultation Services

This episode of The Modern .NET Show is supported, in part, by RJJ Software's Strategic Technology Consultation Services. If you're an SME (Small to Medium Enterprise) leader wondering why your technology investments aren't delivering, or you're facing critical decisions about AI, modernization, or team productivity, let's talk.

Show Notes

"."— Codrina Merigo

Hey everyone, and welcome back to The Modern .NET Show; the premier .NET podcast, focusing entirely on the knowledge, tools, and frameworks that all .NET developers should have in their toolbox. I'm your host Jamie Taylor, bringing you conversations with the brightest minds in the .NET ecosystem.

Today, we are joined by Codina Merigo to talk about the cross section of .NET MAUI and AI, her new book "AI-Enabled Apps with .NET MAUI", and some of the scenarios where you might want to include AI in your applications.

"To get privacy by design nowadays is really impossible. So if you need to really have something disconnected from the internet maybe you'll need a tiny offline model that just, I don't know, does speech-to-text."— Codrina Merigo

Along the way, we discuss the differences between local (on device) AI models and the frontier (online) models, we talk about where models really help with the accessibility of your applications, and we talk about where you can go to get started learning about this new world..

Before we jump in, a quick reminder: if The Modern .NET Show has become part of your learning journey, please consider supporting us through Patreon or Buy Me A Coffee. Every contribution helps us continue bringing you these in-depth conversations with industry experts. You'll find all the links in the show notes.

So let's sit back, open up a terminal, type in `dotnet new podcast` and we'll dive into the core of Modern .NET.

Full Show Notes

The full show notes, including links to some of the things we discussed and a full transcription of this episode, can be found at: https://dotnetcore.show/season-8/beyond-the-chatbot-ai-enabled-net-maui-apps-with-codrina-merigo/

Useful Links:

Supporting the show:

Getting in Touch:

Remember to rate and review the show on Apple Podcasts, Podchaser, or wherever you find your podcasts, this will help the show's audience grow. Or you can just share the show with a friend.

And don't forget to reach out via our Contact page. We're very interested in your opinion of the show, so please get in touch.

You can support the show by making a monthly donation on the show's Patreon page at: https://www.patreon.com/TheDotNetCorePodcast.

Music created by Mono Memory Music, licensed to RJJ Software for use in The Modern .NET Show.

Editing and post-production services for this episode were provided by MB Podcast Services.





Download audio: https://traffic.libsyn.com/clean/secure/thedotnetcorepodcast/821-CodrinaMerigo-AI-MAUI.mp3?dest-id=767916
Read the whole story
alvinashcraft
32 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Why Fable 5 is the Most Controversial AI Release Ever

1 Share
From: AIDailyBrief
Duration: 26:27
Views: 1,997

Anthropic's Fable-5 launch sparked outrage over opaque safety filters, silent model degradation for AI research, and a 30-day enterprise data-retention rule. A rapid walkback restored visibility for safeguards but left deep trust and enterprise-adoption concerns. Broader themes included AI equity proposals for a sovereign wealth fund, massive data-center buildouts and local pushback, and tensions between concentrated lab power and open research.

The AI Daily Brief helps you understand the most important news and discussions in AI.
Subscribe to the podcast version of The AI Daily Brief wherever you listen: https://pod.link/1680633614
Get it ad free at http://patreon.com/aidailybrief
Learn more about the show https://aidailybrief.ai/

Read the whole story
alvinashcraft
32 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Episode 576: Observability's Next Phase

1 Share

Brandon talks with OpenObserve's Prabhat Sharma and Shani Shoham: why observability is still broken, how they fixed it, and where AI takes it next.

Watch the YouTube Live Recording of Episode 576

Show Links

Contact Prabhat Sharma

Contact Shani Shoham

SDT News & Hype

Special Guests: Prabhat Sharma and Shani Shoham.

Sponsored By:





Download audio: https://aphid.fireside.fm/d/1437767933/9b74150b-3553-49dc-8332-f89bbbba9f92/53a76a68-2419-442c-a033-1c69e5f9c886.mp3
Read the whole story
alvinashcraft
32 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Creating a multi-agent application. Part 2

1 Share

In my previous post, I showed the output of a multi-agent application I wrote to create blog posts (not to worry, it is for demonstration purposes only). In this post, I will begin the process of working through the code, line by line.

This application is written in Python, in a Colab notebook, using (among other things) LangChain and LangGraph. To follow along you will need to obtain an API key from OpenAI and a key from Tavily.

If you are a C# programmer with little or no Python experience, don’t panic! Python is pretty readable, and I’ll explain any part that is potentially obscure or confusing.

This will be a multi-agent application. The agents we’ll create will be:

  • Blogger which will orchestrate the others
  • Researcher, which will search the web for relevant information
  • Author, which will write drafts of the blog post
  • Reviewer, which will evaluate the drafts and suggest improvements

As a general rule, I try to limit the number of agents to 3-5. Any more than that can get terribly complicated with diminishing returns. Your mileage may vary.

Set Up

The program begins by importing the necessary libraries.

!pip install -q openai==1.66.3 \
                langchain==0.3.20 \
                langchain-openai==0.3.9 \
                langchain_experimental==0.3.4 \
                langchain-tavily==0.2.4 \
                tavily-python==0.5.0 \
                langgraph==0.3.21 \
                langgraph-supervisor==0.0.18

You then load your OpenAI API key and Tavily API key from either a configuration file (which is what I do here) or from the environment.

import json
import os
from pprint import pprint

file_name = 'config.json'
with open(file_name, 'r') as file:
    config = json.load(file)
    os.environ['OPENAI_API_KEY'] = config.get("API_KEY")
    os.environ["OPENAI_BASE_URL"] = config.get("OPENAI_API_BASE")
    os.environ["TAVILY_API_KEY"] = config.get("TAVILY_API_KEY")

Next, we set up the model.

from langchain_openai import ChatOpenAI

model_name = 'gpt-4o-mini'

llm = ChatOpenAI(
    model = model_name,
    temperature=0,
    max_tokens=4096
)

I’ve opted for the gpt-4o-mini model, as it’s the most cost-effective option. I’ve set the temperature to 0 to get the most consistent results.

Tavily is a tool for searching the web, so let’s set that up as well

from langchain_tavily import TavilySearch

tavily_tool = TavilySearch(
    max_results=5,
    topic="general",
    include_answer=False,
    include_raw_content=False,
    search_depth="basic"
)

You can find all the options for this on the Tavily website.

I’m going to want a state object so that I can pass it among the agents.

from typing import TypedDict, Annotated, List
from langgraph.graph import StateGraph, END
import operator

class ResearchState(TypedDict):
    """State for the research workflow."""
    main_task: str
    research_findings: Annotated[List[str], operator.add]
    draft: str
    review_notes: str
    revision_number: int
    next_step: str
    current_sub_task: str

Blogger

To get started, we’ll create the (non-trivial) blogger agent. This is actually the most powerful and thus the most complex of the agents.

We begin with the prompt template (this is, essentially, the system prompt)

blogger_prompt_template = """You are a blogger managing a blog post creation workflow.

Current Task: {main_task}

Current State:
- Research Findings: {research_findings}
- Blog Draft: {draft}
- Reviewer Feedback: {review_notes}
- Revision Number: {revision_number}

Your goal is to ensure a clear, engaging, and valuable blog post targeted at software developers.

Decide the next step and respond only with a JSON object (no extra text):
{
  "next_step": "researcher" or "author" or "END",
  "task_description": "Brief description of what needs to be done next"
}

Decision Rules:
- If no research exists, choose "researcher"
- If research exists but no draft, choose "author"
- If draft exists and critic says "APPROVED", choose "END"
- If draft needs revision, choose "author"
- If revision_number >= 4, choose "END"
"""

Take a moment to read this over; it sets the parameters and goals of the program. The decision rules are critical, they control the flow, and they set a limit on revisions (in this case 4).

Having told the Blogger what we are trying to accomplish and the general tone of the output we’re ready to create the decision tree that constitutes the workflow for the Blogger. This is pretty long, but most of it is self-explanatory.

def create_blogger_chain():
    """Creates the bloger decision chain."""

    def blogger_invoke(state):
        research = state.get("research_findings", [])
        research_text = "\n".join(research) if research else "No research yet."
        revision = state.get("revision_number", 0)
        has_research = len(research) > 0
        has_draft = bool(state.get("draft", "").strip())
        review = state.get("review_notes", "")

        if "APPROVED" in review.upper() and has_draft:
            print("Blogger: Draft approved, ending workflow")
            return {
                "next_step": "END",
                "task_description": "Report approved and complete"
            }

        if not has_research:
            print("Blogger: No research yet, directing to researcher")
            return {
                "next_step": "researcher",
                "task_description": f"Research the topic: {state.get('main_task', '')}"
            }


        if has_research and not has_draft:
            print("Blogger: Have research, creating first draft")
            return {
                "next_step": "author",
                "task_description": "Write the first draft based on research findings"
            }

        if has_draft and not review:
            print("Blogger: Have draft, sending to reviewer")
            return {
                "next_step": "author",
                "task_description": "Prepare draft for review"
            }

        if review and "APPROVED" not in review.upper() and revision <= 4:
            print(f"Supervisor: Revision {revision}, sending back to author")
            return {
                "next_step": "author",
                "task_description": "Revise the draft based on review feedback"
            }

        if revision >= 4:
            print("Blogger: Max revisions reached! Ending")
            return {
                "next_step": "END",
                "task_description": "Maximum revisions reached! Finalizing report"
            }

        # LLM as fallback
        prompt = blogger_prompt_template.format(
            main_task=state.get("main_task", ""),
            research_findings=research_text,
            draft=state.get("draft", "No draft yet."),
            review_notes=review if review else "No review yet.",
            revision_number=revision
        )

        try:
            response = llm.invoke(prompt)
            content = response.content if hasattr(response, 'content') else str(response)

            text = content.strip()
            if text.startswith("```"):
                lines = text.split("\n")
                text = "\n".join([l for l in lines if not l.strip().startswith("```")])
            text = text.strip()

            decision = json.loads(text)

            if "next_step" in decision:
                return decision

        except Exception as e:
            print(f"LLM parsing error: {e}")

        # Final fallback 
        print("Blogger: Using final fallback - continuing with author")
        return {
            "next_step": "author",
            "task_description": "Continue with draft creation"
        }

    return blogger_invoke

# Creating a callable object
blogger_chain = create_blogger_chain()

We begin with a nested function.

def create_blogger_chain():
"""Creates the blogger decision chain."""

def blogger_invoke(state):
...

When called, Python executes the code inside it, that is, the inner function. In this case, the inner function does the real work.

We define a second function inside create_blogger_chain. We do this to create a closure, that is the inner function can access variables from the outer function. This is a common construct when working with langchain. The inner function has access to the llm without it being passed every time. So, in short, create_blogger_chain is actually a factory function that constructs and configures a callable function and then returns it.

We next set up our variables based on starting values in the state object. With that done, we’re ready to progress through a series of possible conditions. These are pretty self-explanatory and follow the rules established in the template.

All that’s left for the blogger is to create a node where the decision will be implemented. We’ll use this node, and the others we’ll create, when we implement the workflow (after we define all the agents).

def blogger_node(state: ResearchState) -> dict:
    """Blogger decides the next step."""
    print("\n>>>Blogger")

    decision = blogger_chain(state)

    next_step = decision.get("next_step", "researcher")
    task_desc = decision.get("task_description", "Continue work")

    print(f"Decision: {next_step}")
    print(f"Task: {task_desc}")

    return {
        "next_step": next_step,
        "current_sub_task": task_desc,
    }

Note that the variable decision is assigned as a result of calling the code we just reviewed. With that in hand, we call get on the decision, asking for the next step. If none is returned, we use researcher.

That’s it for blogger. We’ll review the other agents in the next posting.

Read the whole story
alvinashcraft
33 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Introducing Corvus.Text.Json V5: Mutable Documents

1 Share

At endjin, we maintain Corvus.JsonSchema, and in the previous post we looked at pooled-memory parsing with ParsedJsonDocument<T>.

Now let's look at the other side of the coin: mutation.

The V4/V5 trade-off

This is the fundamental design decision in V5, so it's worth being explicit about it.

In V4, every document is immutable. If you want to change a property, you call a With*() method that returns a new instance with the modification applied. The old instance is unchanged. This is the functional approach. It is safe, thread-friendly, and easy to reason about.

V4 is smarter than a naive copy-on-write: it avoids copying unmodified parts of the document and defers serialization to write operations. But it still has to create new immutable data structures wherever objects or arrays are modified. In a pipeline where you parse, modify, and write JSON repeatedly, that adds up to a lot of short-lived allocations.

V5 takes the opposite approach. JsonDocumentBuilder lets you mutate documents in place, using pooled memory managed by a JsonWorkspace. You create a workspace, build or modify documents, write the output, and dispose everything. The workspace recycles the memory for the next operation.

This is the builder approach. It is fast and low-allocation, but you need to be mindful of lifetimes and ownership. V5 includes version tracking to catch stale references at runtime, which mitigates the most common class of bugs.

Neither approach is universally better. If you want safety guarantees, use V4. If you want maximum throughput, use V5.

The workspace

Every mutable operation starts with a JsonWorkspace:

using JsonWorkspace workspace = JsonWorkspace.Create();

The workspace manages pooled buffers and Utf8JsonWriter instances. When you dispose it, all resources go back to the pool. You can create multiple builders within a single workspace. They share the pooled resources.

Building from scratch

The most common pattern is building an object using a builder delegate:

using JsonWorkspace workspace = JsonWorkspace.Create();

using var doc = JsonElement.CreateBuilder(
    workspace,
    new(static (ref objectBuilder) =>
    {
        objectBuilder.AddProperty("name"u8, "Alice"u8);
        objectBuilder.AddProperty("age"u8, 30);
        objectBuilder.AddProperty("active"u8, true);
    }));

Console.WriteLine(doc.RootElement.ToString());
// {"name":"Alice","age":30,"active":true}

That new(...) is a target-typed new. The compiler knows it needs a JsonElement.Source from the CreateBuilder parameter type. The static modifier on the delegate prevents accidental closure allocations. Use UTF-8 string literals (u8) for property names to avoid transcoding overhead.

What is Source?

JsonElement.Source is a ref struct that acts as a discriminated union. It can hold any value that might appear in a JSON document. It has implicit conversions from over 30 .NET types, so you rarely need to think about it:

  • Primitives: bool, int, long, double, decimal, float, short, byte, and the unsigned variants, plus Half, Int128, UInt128
  • Strings: string, ReadOnlySpan<char>, ReadOnlySpan<byte> (UTF-8)
  • Dates: DateTime, DateTimeOffset, and NodaTime types (LocalDate, OffsetDateTime, Period, etc.)
  • Other: Guid, Uri, BigNumber, BigInteger, JsonElement, JsonElement.Mutable
  • Delegates: JsonElement.ObjectBuilder.Build for nested objects, JsonElement.ArrayBuilder.Build for nested arrays

This is what makes the builder API feel natural. JsonElement.ObjectBuilder.AddProperty has direct overloads for all these types, so objectBuilder.AddProperty("age"u8, 30) just works. The int matches directly. For CreateBuilder and SetProperty, your value implicitly converts to a Source. Either way, you just pass values. The type system handles the rest.

For objects and arrays, you pass a builder delegate:

// Object builder - delegate receives ref JsonElement.ObjectBuilder
objectBuilder.AddProperty("address"u8, static (ref addressBuilder) =>
{
    addressBuilder.AddProperty("city"u8, "London"u8);
});

// Array builder - delegate receives ref JsonElement.ArrayBuilder
objectBuilder.AddProperty("tags"u8, static (ref tagsBuilder) =>
{
    tagsBuilder.AddItem("admin"u8);
    tagsBuilder.AddItem("user"u8);
});

There's also a generic Source<TContext> variant for passing context to a delegate without allocating a closure - useful in hot paths where even a single delegate allocation matters.

Nested objects and arrays

Builder delegates compose naturally:

using var doc = JsonElement.CreateBuilder(
    workspace,
    new(static (ref objectBuilder) =>
    {
        objectBuilder.AddProperty("user"u8, static (ref userBuilder) =>
        {
            userBuilder.AddProperty("name"u8, "Alice"u8);
            userBuilder.AddProperty("roles"u8, static (ref rolesBuilder) =>
            {
                rolesBuilder.AddItem("admin"u8);
                rolesBuilder.AddItem("editor"u8);
            });
        });
    }));

// {"user":{"name":"Alice","roles":["admin","editor"]}}

Parse-and-mutate

In most real-world scenarios, you're receiving JSON, modifying it, and sending it on. Parse directly into a mutable builder for the best performance:

using JsonWorkspace workspace = JsonWorkspace.Create();

// Single pass - UTF-8 bytes become the builder's backing store
using var builder = JsonDocumentBuilder<JsonElement.Mutable>.Parse(
    workspace,
    """{"status":"pending","count":5}""");

JsonElement.Mutable root = builder.RootElement;
root.SetProperty("status", "completed"u8);
root.SetProperty("count", 10);

Console.WriteLine(root.ToString());
// {"status":"completed","count":10}

All the same Parse overloads are available - from strings, UTF-8 bytes, streams, or a Utf8JsonReader.

Retaining the original

If you need to keep an immutable copy alongside the mutable version - for auditing, comparison, or read-only queries - use the two-step approach:

using JsonWorkspace workspace = JsonWorkspace.Create();

using var sourceDoc = ParsedJsonDocument<JsonElement>.Parse(
    """{"name":"Original","value":100}""");

// Convert to mutable - sourceDoc remains unchanged
using var builder = sourceDoc.RootElement.CreateBuilder(workspace);

builder.RootElement.SetProperty("name", "Modified"u8);

Console.WriteLine(sourceDoc.RootElement.ToString());  // {"name":"Original",...}
Console.WriteLine(builder.RootElement.ToString());     // {"name":"Modified",...}

Version tracking

Here's how V5 catches the aliasing bugs that in-place mutation can introduce.

Every JsonDocumentBuilder tracks a ulong version number. When you obtain a mutable element reference, it captures the current version. If you modify the document through a different reference and then try to use the stale one, V5 throws InvalidOperationException:

JsonElement.Mutable root = builder.RootElement;
JsonElement.Mutable name = root.GetProperty("name");

// Mutate through root - this bumps the version
root.SetProperty("name", "Changed"u8);

// Try to use the stale reference
name.GetString();  // throws InvalidOperationException

This won't catch every possible misuse, but it catches the most common class of bugs: holding a reference across a mutation boundary.

Clone and freeze

When you're working with a mutable document, you sometimes need an immutable copy. It is a value that won't go stale when you mutate the builder again. Freeze() gives you exactly that.

Freeze() performs a fast blit of the metadata and value backing arrays into a new immutable document registered in the same workspace, without a serialization round-trip. The result is immutable, and you can keep mutating the original builder while the frozen element stays valid:

using JsonWorkspace workspace = JsonWorkspace.Create();
using var doc = ParsedJsonDocument<JsonElement>.Parse("""{"name": "Alice", "age": 30}""");
using var builder = doc.RootElement.CreateBuilder(workspace);

builder.RootElement.SetProperty("age"u8, 31);

// Freeze - cheap immutable copy, stays in the workspace
JsonElement frozen = builder.RootElement.Freeze();

// Keep mutating - the frozen element is unaffected
builder.RootElement.SetProperty("age"u8, 99);

Assert.Equal(31, frozen.GetProperty("age"u8).GetInt32()); // still 31

The frozen element is tied to the workspace's lifetime. It's backed by pooled memory and will be cleaned up with the workspace.

On the other hand, if you need data to escape the workspace entirely (e.g. to return it to a caller who shouldn't need to worry about workspaces or lifetimes) then that's what Clone() is for.

Clone() serializes the mutable element into a fresh immutable ParsedJsonDocument that owns its own memory on the GC heap. The clone is completely independent of both the builder and the workspace, so it remains valid after both are disposed:

JsonElement clone;

using (JsonWorkspace workspace = JsonWorkspace.Create())
using (var doc = ParsedJsonDocument<JsonElement>.Parse("[[[]]]"))
using (var builder = doc.RootElement.CreateBuilder(workspace))
{
    clone = builder.RootElement[0].Clone();
    // builder and workspace are disposed here
}

// clone is still valid - it owns its own memory
Assert.Equal("[[]]", clone.GetRawText());

Use Freeze() for cheap immutable copies within the workspace scope. Use Clone() when the result needs to escape entirely.

Snapshots for rollback

JsonDocumentBuilderSnapshot<T> captures the complete state of a builder so you can restore it later. This is useful for speculative mutations where you may want to roll back if something goes wrong. We mentioned this briefly in the context of JSON Patch, but it's a general-purpose mechanism.

using JsonWorkspace workspace = JsonWorkspace.Create();
using var doc = ParsedJsonDocument<JsonElement>.Parse("""{"status": "pending", "retries": 0}""");
using var builder = doc.RootElement.CreateBuilder(workspace);

// Take a snapshot before applying changes
using var snapshot = builder.CreateSnapshot();

builder.RootElement.SetProperty("status"u8, "processing");
builder.RootElement.SetProperty("retries"u8, 1);

// Something went wrong - roll back to the snapshot
builder.Restore(snapshot);

Assert.Equal("pending", builder.RootElement.GetProperty("status"u8).GetString());
Assert.Equal(0, builder.RootElement.GetProperty("retries"u8).GetInt32());

CreateSnapshot() creates a rented copy of the builder's internal state, and Restore() copies it back. The snapshot is IDisposable and must be disposed to return the rented buffers to the pool.

Dynamic construction with runtime data

Real-world JSON isn't all static strings. Here's how you mix structure with runtime data:

string[] tags = ["admin", "user", "active"];

using JsonWorkspace workspace = JsonWorkspace.Create();

using var doc = JsonElement.CreateBuilder(
    workspace,
    new((ref objectBuilder) =>
    {
        objectBuilder.AddProperty("id"u8, Guid.NewGuid());

        // Runtime collection becomes a JSON array
        objectBuilder.AddProperty("tags"u8, (ref tagsBuilder) =>
        {
            foreach (string tag in tags)
            {
                tagsBuilder.AddItem(tag);
            }
        });
    }));

The delegate in this example captures tags from the enclosing scope, so it can't be static. That's often fine, but if you need to avoid the closure allocation there is a Source<TContext> overload that lets you pass context explicitly:

string[] tags = ["admin", "user", "active"];

using JsonWorkspace workspace = JsonWorkspace.Create();

using var doc = JsonElement.CreateBuilder(
    workspace,
    tags,
    static (in string[] tags, ref JsonElement.ObjectBuilder objectBuilder) =>
    {
        objectBuilder.AddProperty("id"u8, Guid.NewGuid());

        objectBuilder.AddProperty("tags"u8, tags,
            static (in string[] tags, ref JsonElement.ArrayBuilder tagsBuilder) =>
            {
                foreach (string tag in tags)
                {
                    tagsBuilder.AddItem(tag);
                }
            });
    });

The context parameter is passed by in reference, so there is no copying or boxing. Every delegate is static, so there are no closure allocations.

Generated types and the builder

Everything in this post uses JsonElement and JsonElement.Mutable, but the same builder API works for all generated types. If you have a Person type generated from a JSON Schema, you can create a builder, mutate it, and freeze it in exactly the same way.

The difference is that the generated types only emit .NET members that are compatible with the constraints in their schema. A Person.Mutable will have SetProperty for the properties defined in the schema, and conversions to and from .NET types that match the schema's type constraints. For example, numeric conversions are only available if the schema allows the value to be a number. This means the compiler catches type errors at build time rather than at runtime.

At a glance

JsonNode V4 With*() V5 JsonDocumentBuilder
Mutation In-place, per-node Returns new immutable instance In-place, pooled
Memory Managed heap per node Managed heap per copy ArrayPool via workspace
Safety No aliasing protection Immutability prevents aliasing Version-tracked stale detection
Best for Long-lived trees Safety-critical pipelines High-throughput request/response

Next up

In the next post, we'll look at the standalone evaluator. It's a lightweight code generation mode that produces just a validator and annotation collector, without the full type hierarchy. It's ideal for schema-driven tooling like form generators and configuration editors.



Read the whole story
alvinashcraft
34 minutes ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories