> ## Documentation Index
> Fetch the complete documentation index at: https://repost.sh/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# C# Quickstart

> Install the CLI, add the Repost.Client package, generate a typed client from your schema, and publish your first event from .NET.

export const RepostHighlight = () => {
  useEffect(() => {
    const colors = {
      comment: ['#116329', '#6A9955'],
      constant: ['#0550AE', '#9CDCFE'],
      keyword: ['#CF222E', '#569CD6'],
      name: ['#953800', '#4EC9B0'],
      string: ['#0A3069', '#CE9178'],
      type: ['#0550AE', '#4EC9B0'],
      variable: ['#1F2328', '#9CDCFE']
    };
    const tokenPattern = /"[^"\n]*"|@[A-Za-z_]\w*|\b(?:String|Int|Float|Boolean|DateTime|Json)\b|\b(?:true|false)\b|\b\d+(?:\.\d+)?\b|\b(?:now|uuid|cuid)(?=\s*\()|\b[A-Z]\w*\b|\b[A-Za-z_]\w*\b/g;
    const append = (line, text, color) => {
      if (!text) return;
      if (!color) {
        line.append(text);
        return;
      }
      const span = document.createElement('span');
      span.textContent = text;
      span.style.color = color[0];
      span.style.setProperty('--shiki-dark', color[1]);
      line.append(span);
    };
    const commentStart = text => {
      let quoted = false;
      for (let index = 0; index < text.length - 1; index += 1) {
        if (text[index] === '"' && text[index - 1] !== '\\') quoted = !quoted;
        if (!quoted && text[index] === '/' && text[index + 1] === '/') return index;
      }
      return -1;
    };
    const highlightCode = code => {
      if (code.dataset.repostHighlighted !== undefined || code.querySelector('span[style*="--shiki-dark"]')) {
        return;
      }
      code.dataset.repostHighlighted = '';
      const source = code.textContent ?? '';
      const fragment = document.createDocumentFragment();
      for (const text of source.split('\n')) {
        const line = document.createElement('span');
        line.className = 'line';
        const declaration = text.match(/^(\s*)(generator|model|enum|type|event)(\s+)([A-Za-z_]\w*)(.*)$/);
        if (declaration) {
          append(line, declaration[1]);
          append(line, declaration[2], colors.keyword);
          append(line, declaration[3]);
          append(line, declaration[4], colors.name);
          append(line, declaration[5]);
        } else {
          const start = commentStart(text);
          const codeText = start === -1 ? text : text.slice(0, start);
          const leading = codeText.match(/^(\s*)([A-Za-z_]\w*)(?=\s+(?:@|[A-Z]))/);
          const bare = codeText.match(/^(\s*)([A-Za-z_]\w*)(\s*)$/);
          let offset = 0;
          for (const match of codeText.matchAll(tokenPattern)) {
            append(line, codeText.slice(offset, match.index));
            const token = match[0];
            let color = colors.constant;
            if (token.startsWith('"')) color = colors.string; else if (token.startsWith('@') || (/^(now|uuid|cuid)$/).test(token)) color = colors.name; else if ((/^(String|Int|Float|Boolean|DateTime|Json)$/).test(token)) color = colors.type; else if (leading && match.index === leading[1].length) color = colors.variable; else if (bare && match.index === bare[1].length) color = colors.constant; else if ((/^[A-Z]/).test(token)) color = colors.name; else if ((/^[A-Za-z_]/).test(token)) color = colors.variable;
            append(line, token, color);
            offset = match.index + token.length;
          }
          append(line, codeText.slice(offset));
          if (start !== -1) append(line, text.slice(start), colors.comment);
        }
        fragment.append(line, '\n');
      }
      code.replaceChildren(fragment);
    };
    const highlightRepostBlocks = () => {
      for (const block of document.querySelectorAll('.code-block')) {
        const code = block.querySelector('pre code');
        const filename = block.querySelector('[data-component-part="code-block-header-filename"] [title$=".repost"]');
        const looksLikeRepost = (/^\s*(generator|model|enum|type|event)\s+[A-Za-z_]\w*\s*\{/m).test(code?.textContent ?? '');
        if (code && (filename || looksLikeRepost)) highlightCode(code);
      }
    };
    let frame;
    const schedule = () => {
      if (frame !== undefined) return;
      frame = requestAnimationFrame(() => {
        frame = undefined;
        highlightRepostBlocks();
      });
    };
    const observer = new MutationObserver(schedule);
    observer.observe(document.documentElement, {
      childList: true,
      subtree: true
    });
    schedule();
    return () => {
      observer.disconnect();
      if (frame !== undefined) cancelAnimationFrame(frame);
    };
  }, []);
  return null;
};

<RepostHighlight />

The C# client is generated from your `.repost` schema into a folder in your project: immutable model classes, a `Webhooks.<Catalog>.<Member>Async()` method tree, and a shared runtime that owns serialization, retries, idempotency, and delivery outcomes. The same schema and wire format back the [TypeScript](/docs/send/typescript/quickstart), Go, Python, and [Java](/docs/send/java/quickstart) SDKs, pinned by a cross-language conformance suite.

.NET 8, .NET Framework 4.7.2, or any `netstandard2.0` target is supported. The runtime is **server-side only** — it holds a publish credential and must not ship in a client app.

<Note>
  The runtime package `Repost.Client` is version `1.0.0` and the schema engine is `0.10.0`.
</Note>

<Steps>
  <Step title="Install the CLI and the runtime package">
    <CodeGroup>
      ```bash npm theme={"languages":{"custom":["/languages/repost.json"]}}
      npm install -g @repost/cli
      dotnet add package Repost.Client
      ```

      ```bash Install script theme={"languages":{"custom":["/languages/repost.json"]}}
      curl -fsSL https://repost.sh/install | sh
      dotnet add package Repost.Client
      ```
    </CodeGroup>
  </Step>

  <Step title="Create a schema workspace">
    ```bash theme={"languages":{"custom":["/languages/repost.json"]}}
    repost schema init --language csharp
    ```

    This scaffolds `repost/schema.repost` with a starter event and a `.env` file with an empty `REPOST_SEND_API_KEY`. A C# `generator` block sets three things the other languages don't need — `output`, `namespace`, and `clientName`:

    ```repost repost/schema.repost theme={"languages":{"custom":["/languages/repost.json"]}}
    generator sdk {
      language   = "csharp"
      output     = "../Generated"
      namespace  = "Contoso.Events"
      clientName = "RepostClient"
    }

    enum Currency {
      USD
      EUR
    }

    type Book {
      /// A book was created
      created
    }

    model Author {
      name  String
      email String?
    }

    model Book {
      title    String
      authors  Author[]
      price    Float
      currency Currency
      subtitle String?
    }

    event BookCreated {
      type      @type(Book.created)
      data      Book
      timestamp DateTime
    }
    ```

    `output` is resolved relative to the schema file, so `../Generated` writes the client to a `Generated/` folder next to your `.csproj`. [Code generation](/docs/send/csharp/generation) explains each field and the build-time MSBuild alternative.
  </Step>

  <Step title="Record the schema and generate">
    ```bash theme={"languages":{"custom":["/languages/repost.json"]}}
    repost schema migrate dev --name init
    ```

    This records your schema as a migration and generates the client into `Generated/` — `Client.cs`, `Descriptors.cs`, and `Models.cs`, plus a copy of the schema and a marker file. The `.cs` files compile through your project's default glob, so there is nothing to wire up.
  </Step>

  <Step title="Connect an environment">
    Create an environment in the [dashboard](https://app.repost.sh), copy its publish API key into `.env`, and deploy your schema:

    ```bash theme={"languages":{"custom":["/languages/repost.json"]}}
    repost auth login
    repost schema migrate deploy
    ```

    Load `.env` however your app already loads configuration; the runtime reads `REPOST_SEND_API_KEY` from the environment at send time.
  </Step>
</Steps>

## Your first send

The generated client is `IAsyncDisposable`. Open it with `await using`; with no `ApiKey` set the runtime resolves the credential and endpoint from the environment on the first send:

```csharp theme={"languages":{"custom":["/languages/repost.json"]}}
using Repost.Client;
using Contoso.Events;

await using var repost = new RepostClient(new RepostClientOptions());

SendResult result = await repost.Webhooks.Book.CreatedAsync(new BookCreatedInput
{
    CustomerId = "cus_123",
    Data = new Book
    {
        Title = "Dune",
        Authors = new[] { new Author { Name = "Frank Herbert" } },
        Price = 9.99,
        Currency = Currency.Usd,
        Subtitle = "A Desert Epic",
    },
    IdempotencyKey = "book_dune:created",
});

Console.WriteLine(result.Id); // msg_...
```

`new RepostClient(...)` validates its options and runs the descriptor-version handshake at construction; a missing credential surfaces on the first send. `CustomerId` names the customer receiving the event. `IdempotencyKey` is optional; reusing it with the same payload is safe, and [Reliability](/docs/send/csharp/reliability) explains when to pass your own. The compiler rejects unknown events, missing required fields, and wrong types before a send ever leaves your process.

## Continue

<Columns cols={3} className="gap-y-4">
  <Card title="Code generation" icon="cog" href="/docs/send/csharp/generation" cta="Generation" arrow="true">
    What lands in `Generated/`, build-time MSBuild generation, and the version handshake.
  </Card>

  <Card title="Configuration" icon="sliders-horizontal" href="/docs/send/csharp/configuration" cta="Configure" arrow="true">
    Client options, credential precedence, and transport tuning.
  </Card>

  <Card title="Reliability" icon="shield-check" href="/docs/send/csharp/reliability" cta="Outcomes" arrow="true">
    Retries, idempotency, the delivery states, and the exception taxonomy.
  </Card>
</Columns>
