CarpaNet 1.0.1

dotnet add package CarpaNet --version 1.0.1
                    
NuGet\Install-Package CarpaNet -Version 1.0.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="CarpaNet" Version="1.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CarpaNet" Version="1.0.1" />
                    
Directory.Packages.props
<PackageReference Include="CarpaNet" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add CarpaNet --version 1.0.1
                    
#r "nuget: CarpaNet, 1.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package CarpaNet@1.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=CarpaNet&version=1.0.1
                    
Install as a Cake Addin
#tool nuget:?package=CarpaNet&version=1.0.1
                    
Install as a Cake Tool

CarpaNet

NuGet Version License

CarpaNet Logo

CarpaNet is the core .NET runtime library for interacting with ATProtocol. It provides ATProtocol primitives, HTTP clients, OAuth support (Via CarpaNet.OAuth), identity resolution, CBOR serialization, event streams, Jetstream support (Via CarpaNet.Jetstream) and repo reading.

CarpaNet is intended as a "thin" implementation of accessing ATProtocol XRPC endpoints and services. Instead of binding itself to Bluesky services directly, you can use CarpaNet.SourceGen to create source generated bindings based on whatever version of the lexicons you wish to use. This should give much more flexability and maintainability for keeping up to date with ATProtocol changes, since now you don't need to depend on the library itself to stay updated.

1444070256569233

This library is experimental and not stable. Expect issues and bugs! Docs are not complete yet as the API is evolving and not stable.

Installation

dotnet add package CarpaNet

By itself, CarpaNet does not provide bindings to any ATProtocol lexicons. You can either write them yourself or use the source generator to create bound objects.

IATProtoClient

All clients implement IATProtoClient, which provides GetAsync, PostAsync, and SubscribeAsync methods for XRPC calls. The source generator produces typed extension methods on IATProtoClient (e.g., client.AppBskyFeedGetTimelineAsync()), so any client works with the generated API surface. You can either write your own implementation, or use the ones provided by CarpaNet or CarpaNet.OAuth.

ATProtoClient

The provided implementation client for CarpaNet. Supports public (unauthenticated) access, session-based authentication (App Passwords), and custom token providers. Includes auto-retry on auth failure and rate limiting.

// Public (unauthenticated) access
var client = ATProtoClient.Create(new ATProtoClientOptions
{
    JsonOptions = myJsonOptions,
    CborContext = myCborContext
});

var session = await client.LoginAsync("myhandle.bsky.social", "my-app-password");

// Or authenticate via password session directly using static factory.
var client = await ATProtoClient.CreateWithSessionAsync(
    "myhandle.bsky.social", "my-app-password",
    options: new ATProtoClientOptions
    {
        JsonOptions = myJsonOptions,
        CborContext = myCborContext
    });

Configuration

ATProtoClientOptions controls client behavior:

Property Description Default
BaseUrl Base URL for API requests
JsonOptions JsonSerializerOptions with source-gen resolver (required for AOT)
CborContext CborSerializerContext for event stream deserialization (required for AOT)
TokenProvider ITokenProvider for auth; null = public mode null
IdentityResolver Handle/DID resolver Auto-created
AutoRetryOnAuthFailure Retry on 401 with token refresh true
EnableRateLimitHandler Rate limit handling true
AutoRetryOnRateLimit Auto-retry on rate limit true
RateLimitMaxRetries Max rate limit retries 3
UserAgent User-Agent header null
Timeout Request timeout 100s
LabelerDids Labeler DIDs for atproto-accept-labelers header null

ATProtocol Types

  • ATDid — Strongly-typed DID (e.g., did:plc:...)
  • ATHandle — Strongly-typed handle (e.g., myname.bsky.social)
  • ATUri — AT URI with authority, collection, and record key parsing
  • ATCid — Content identifier (CID)
  • ATIdentifier — Accepts either a DID or handle
  • BlobRef — Blob reference with CID link

Identity Resolution

IdentityResolver resolves handles to DIDs and DIDs to DID documents. Supports both did:plc (via PLC directory) and did:web. Handle resolution uses DNS TXT records with HTTPS fallback.

var resolver = IdentityResolver.CreateWithCache();
var didDoc = await resolver.ResolveAsync("myhandle.bsky.social");
var pdsEndpoint = didDoc.PdsEndpoint;

CBOR Serialization

The CarpaNet.Cbor namespace provides DAG-CBOR serialization using System.Formats.Cbor. The CborSerializerContext pattern mirrors System.Text.Json's JsonSerializerContext for AOT compatibility. Used for event stream message deserialization and repository operations.

Event Streams

EventStreamClient provides WebSocket-based subscription to ATProtocol event streams, with CBOR frame parsing. This is used internally by the clients' SubscribeAsync method.

Repository

The Repo namespace includes CarReader for reading CAR (Content Addressable aRchive) files, Repository for working with ATProtocol repositories, and MstNode/RepoCommit for Merkle Search Tree operations.

Authentication

ITokenProvider abstracts token management. Two implementations are provided:

  • SessionTokenProvider — For app password sessions (JWT access/refresh tokens)
  • DPoPTokenProvider — For OAuth sessions with DPoP proof-of-possession, you can find this in CarpaNet.OAuth.

Both support automatic token refresh and raise TokenRefreshed events for session persistence.

Storage Interfaces

OAuth flows require persistent storage. CarpaNet provides interfaces and in-memory defaults:

  • IOAuthSessionStore — Store/retrieve/delete OAuth session data by DID
  • IOAuthStateStore — Store/consume OAuth authorization state (replay-safe)
  • MemoryOAuthSessionStore / MemoryOAuthStateStore — In-memory implementations

CarpaNet.SourceGen

CarpaNet includes a Roslyn source generator for generating ATProtocol classes from lexicon files.

1. Import the MSBuild targets (If using from source)

<Import Project="path/to/CarpaNet.SourceGen.targets" />

When using the NuGet package, the targets are imported automatically.

2. Add Lexicon files

Point LexiconFiles to your ATProtocol Lexicon JSON files:

<ItemGroup>
  <LexiconFiles Include="path/to/lexicons/**/*.json" />
</ItemGroup>

This will generate the types based on the lexicon files. You can include multiple entries, the source gen will consolidate them down, although you should try and limit the amount you generate to what your program or library needs.

3. Resolve Lexicons from DNS

You can also resolve lexicon schemas directly from the network using ATProtocol's DNS-based lexicon resolution. Add LexiconResolve items with the NSIDs you want:

<ItemGroup>
  
  <LexiconFiles Include="lexicons/**/*.json" />

  
  <LexiconResolve Include="com.example.myapp.getProfile" />
  <LexiconResolve Include="com.example.myapp.createPost" />
</ItemGroup>

At build time, each NSID is resolved via DNS TXT lookup to find the publishing authority's DID, then the lexicon schema is fetched from their PDS. Resolved files are cached locally and automatically fed into LexiconFiles, so the source generator handles them identically to local files.

NOTE: Remote resolution has its pros and cons - For one, you need a network connection for it to work, and the lexicon has to be registered for it to work, which it may not be. It will fetch new versions once the TTL expires, and that can break your build if it changes. Of course, if the lexicon owner did change their spec, you may want it to break since they now changed their API.

How resolution works

Read the spec linked above, but the TL;DR is

  1. NSID com.example.myapp.getProfile → authority com.example.myapp → DNS lookup _lexicon.myapp.example.com
  2. DNS TXT record returns did=did:plc:xxx
  3. DID document is resolved to find the PDS endpoint
  4. Lexicon record is fetched: GET {pds}/xrpc/com.atproto.repo.getRecord?repo={did}&collection=com.atproto.lexicon.schema&rkey={nsid}

NSIDs sharing the same authority are grouped so the DNS + DID resolution only happens once per authority.

5. Resolve All Lexicons from an Authority

If you want to pull in every lexicon published by a given authority (or DID), use LexiconResolveAuthority instead of listing each NSID individually:

<ItemGroup>
  <LexiconResolveAuthority Include="blog.pckt" />
  <LexiconResolveAuthority Include="pub.leaflet" />
  <LexiconResolveAuthority Include="site.standard" />
</ItemGroup>

You can also pass a DID directly to skip the DNS lookup:

<ItemGroup>
  <LexiconResolveAuthority Include="did:plc:revjuqmkvrw6fnkxppqtszpv" />
</ItemGroup>

At build time, the authority is resolved to a DID (via DNS, unless a DID is provided directly), then com.atproto.repo.listRecords is used to enumerate all com.atproto.lexicon.schema records published by that identity. Each discovered lexicon is fetched, cached, and fed into the source generator.

This is useful for third-party or custom lexicon namespaces where you want everything the author publishes without maintaining an explicit list.

6. Resolve All Lexicons from an AT Protocol Handle

If you know someone's AT Protocol handle (e.g. their Bluesky handle), you can use LexiconResolveHandle to fetch all lexicons they've published:

<ItemGroup>
  <LexiconResolveHandle Include="atproto-lexicons.bsky.social" />
  <LexiconResolveHandle Include="bsky-lexicons.bsky.social" />
</ItemGroup>

At build time, the handle is resolved to a DID using standard AT Protocol handle resolution (DNS TXT _atproto.{handle} first, HTTPS https://{handle}/.well-known/atproto-did fallback), then the DID is resolved to a PDS endpoint, and com.atproto.repo.listRecords is used to enumerate all com.atproto.lexicon.schema records. Each discovered lexicon is fetched, cached, and fed into the source generator.

This differs from LexiconResolveAuthority in that it takes a user handle rather than an NSID authority or DID. Use this when you know an account's handle but not the authority namespace or DID they publish lexicons under.

LexiconResolve, LexiconResolveAuthority, LexiconResolveHandle, and LexiconFiles can all be combined freely:

<ItemGroup>
  
  <LexiconFiles Include="lexicons/**/*.json" />

  
  <LexiconResolve Include="com.atproto.repo.listRecords" />

  
  <LexiconResolveAuthority Include="blog.pckt" />
  <LexiconResolveAuthority Include="site.standard" />

  
  <LexiconResolveHandle Include="alice.bsky.social" />
</ItemGroup>

MSBuild Properties

Source Generator

Property Description Default
CarpaNet_SourceGen_RootNamespace Root namespace for generated code Project's root namespace
CarpaNet_JsonContextName Name for the generated JsonSerializerContext class ATProtoJsonContext
CarpaNet_CborContextName Name for the generated CborSerializerContext class ATProtoCborContext
CarpaNet_EmitValidationAttributes Emit validation attributes on generated properties false

Lexicon Resolution

These properties configure the DNS-based lexicon resolution used with <LexiconResolve> and <LexiconResolveAuthority> items:

Property Description Default
CarpaNet_LexiconCacheDir Directory for caching resolved lexicon files $(BaseIntermediateOutputPath)lexicon-cache/ (i.e. obj/lexicon-cache/)
CarpaNet_LexiconCacheTtlHours How long cached lexicons are valid (in hours). Set to 0 to force refresh. 24
CarpaNet_LexiconFailOnError Whether resolution failures cause a build error (true) or warning (false) true
CarpaNet_PlcDirectoryUrl PLC directory URL for did:plc resolution https://plc.directory
CarpaNet_DnsServers Semicolon-separated DNS server IPs for TXT lookups 1.1.1.1;8.8.8.8
CarpaNet_LexiconAutoResolve Automatically discover and resolve transitive lexicon dependencies at build time false
CarpaNet_LexiconAutoResolveMaxDepth Maximum number of iterations for transitive dependency resolution 10
Automatic Transitive Resolution

When CarpaNet_LexiconAutoResolve is set to true, the build will automatically scan all known lexicon files for external NSID references and resolve any missing dependencies. This is repeated iteratively until all transitive dependencies are satisfied (or MaxDepth is reached).

This means you only need to list your top-level lexicons — all transitive deps are discovered and resolved automatically:

<PropertyGroup>
    <CarpaNet_LexiconAutoResolve>true</CarpaNet_LexiconAutoResolve>
</PropertyGroup>
<ItemGroup>
    <LexiconFiles Include="lexicons\com\whtwnd\**\*.json" />
    <LexiconResolve Include="app.bsky.feed.defs" />
    
</ItemGroup>
Caching

Resolved lexicons are cached on disk under CarpaNet_LexiconCacheDir with a configurable TTL. This avoids repeated network requests on every build.

  • Force refresh: dotnet build -p:CarpaNet_LexiconCacheTtlHours=0
  • Clear cache: delete the obj/*/lexicon-cache/ directory

What Gets Generated

Data model classes

Each Lexicon record, object, and token definition becomes a C# class with System.Text.Json attributes. Classes are grouped by NSID namespace prefix into files like AppBsky_Feed.g.cs, ComAtproto_Repo.g.cs, etc.

Record types include a static RecordType string and are annotated with [ATRecord].

API extension methods (ATProtoExtensions.g.cs)

Lexicon query and procedure definitions become typed extension methods on IATProtoClient:

// Generated from app.bsky.actor.getProfile query
await client.AppBskyActorGetProfileAsync(parameters);

// Generated from com.atproto.repo.createRecord procedure
await client.ComAtprotoRepoCreateRecordAsync(input);

Query parameters are gathered into a *Parameters class with a ToDictionary() method. Procedure inputs use an *Input class. Subscriptions generate SubscribeAsync extensions returning IAsyncEnumerable<T>.

Union types (UnionImplementations.g.cs)

Lexicon union references become discriminated union classes with System.Text.Json polymorphic serialization via [JsonPolymorphic] and [JsonDerivedType] attributes.

JSON serialization context (ATProtoJsonContext.g.cs)

A source-generated JsonSerializerContext that registers all generated types. The context name is configurable via CarpaNet_JsonContextName.

CBOR serialization context (ATProtoCborContext.g.cs)

A CborSerializerContext that registers converters for all generated types, used for event stream deserialization and repository operations. The context name is configurable via CarpaNet_CborContextName.

Serialization helpers (SerializationMethods.g.cs)

ToJson() and FromJson() methods on generated types for convenient serialization using the generated JSON context.

Client factory (ATProtoClientFactory.g.cs)

A convenience factory class in the CarpaNet namespace that creates pre-configured ATProtoClient instances with the generated JSON and CBOR contexts already wired up.

using CarpaNet;

// Uses generated contexts automatically
var client = ATProtoClientFactory.CreateSessionClient();
await client.LoginAsync("myhandle.bsky.social", "my-app-password");

Inspecting Generated Code

Since this is a Roslyn generator, you can emit the compiled code to files so you can inspect them. It may also make it easier for LLMs to be able to debug and get the correct APIs. Set EmitCompilerGeneratedFiles in your project to write the generated files to disk:

<PropertyGroup>
  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>

Generated files appear under obj/Debug/<tfm>/generated/CarpaNet.SourceGen/CarpaNet.LexiconGenerator/.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (3)

Showing the top 3 NuGet packages that depend on CarpaNet:

Package Downloads
CarpaNet.OAuth

.NET ATProtocol OAuth Implementation Library for CarpaNet.

CarpaNet.Jetstream

.NET ATProtocol Jetstream Library, built for bindings with source generators.

CarpaNet.AspNetCore

ASP.NET Core XRPC endpoint support for CarpaNet ATProtocol library.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.1 177 3/30/2026
0.1.0-alpha.76 38 3/30/2026
0.1.0-alpha.71 47 3/24/2026
0.1.0-alpha.68 63 3/15/2026
0.1.0-alpha.58 46 3/8/2026
0.1.0-alpha.54 50 2/18/2026
0.1.0-alpha.52 44 2/18/2026
0.1.0-alpha.50 47 2/18/2026
0.1.0-alpha.47 54 2/17/2026
0.1.0-alpha.38 55 2/16/2026
0.1.0-alpha.37 56 2/16/2026
0.1.0-alpha.35 52 2/16/2026
0.1.0-alpha.34 54 2/15/2026
0.1.0-alpha.33 50 2/15/2026