Last reviewed: October 30, 2026 (Asia/Shanghai).

Direct answer

Connect ASP.NET Core to CometAPI by registering a typed client with AddHttpClient<CometChatClient>(). Configure its base address once, inject it into a server-side handler, and build an authenticated JSON request inside the client. Keep the model and credential in server configuration. Return assistant text only after validating the HTTP response and the completion envelope.

The CometAPI chat reference documents POST /v1/chat/completions, bearer authentication, model, messages, and the non-streaming completion shape. Microsoft’s ASP.NET Core HTTP-client reference documents typed-client registration and handler pooling. Together they let the application expose a small method such as CompleteAsync(prompt, cancellationToken) while keeping transport configuration out of route logic.

The important .NET detail is lifetime ownership: the factory manages pooled handlers, while each call owns and disposes its request and response. Registering a typed client does not validate the provider’s JSON or decide which model outcomes your application accepts.

Who this is for

This tutorial is for C# developers adding a text-only CometAPI integration to an ASP.NET Core application. The example targets .NET 10 and uses the standard HTTP and JSON libraries. It provides a small loopback demo with one application endpoint and one outbound adapter.

Use an installed .NET 10 SDK and start a new example project with dotnet new web -n CometChatDemo. Replace Program.cs and add CometChatClient.cs using the code below. Supply COMETAPI_KEY through your local secret mechanism and set COMETAPI_MODEL to an exact identifier verified for the chat endpoint and your account. The example sends no model-specific options; check those separately before adding them.

The listener remains on loopback. Before exposing an application endpoint to users, place it behind your authenticated ingress and request limits. A server-side credential protects the key from browser code; application authorization controls who can spend through it.

Key takeaways

  • Register the typed client through AddHttpClient; retain that factory registration when integrating it into a larger app.
  • Use a base address ending in /v1/ and the relative path chat/completions.
  • Put bearer authentication on the outbound request and keep model selection server-owned.
  • Pass cancellation through the send and JSON-read operations.
  • Choose response completion mode deliberately when enforcing buffer and timeout limits.
  • Accept only the completion outcomes your application understands.

Register the client and application route

Save this as Program.cs:

using System.Net;
using System.Net.Http.Headers;

var builder = WebApplication.CreateBuilder(args);

var key = builder.Configuration["COMETAPI_KEY"];
var model = builder.Configuration["COMETAPI_MODEL"];
if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(model))
    throw new InvalidOperationException("missing_cometapi_configuration");

builder.Services.AddSingleton(new CometSettings(key, model));
builder.WebHost.ConfigureKestrel(options =>
{
    options.Listen(IPAddress.Loopback, 5080);
    options.Limits.MaxRequestBodySize = 8 * 1024;
});

builder.Services.AddHttpClient<CometChatClient>(client =>
{
    client.BaseAddress = new UriBuilder(
        Uri.UriSchemeHttps, "api.cometapi.com")
    {
        Path = "/v1/"
    }.Uri;
    client.Timeout = TimeSpan.FromSeconds(30);
    client.MaxResponseContentBufferSize = 1024 * 1024;
    client.DefaultRequestHeaders.Accept.Add(
        new MediaTypeWithQualityHeaderValue("application/json"));
})
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
    AllowAutoRedirect = false,
    UseCookies = false
});

var app = builder.Build();

app.MapPost("/api/chat", async Task<IResult> (
    ChatInput input, CometChatClient comet, CancellationToken aborted) =>
{
    if (string.IsNullOrWhiteSpace(input.Prompt) || input.Prompt.Length > 2000)
        return Results.BadRequest(new { code = "invalid_prompt" });

    try
    {
        var reply = await comet.CompleteAsync(input.Prompt, aborted);
        return Results.Ok(new { reply });
    }
    catch (OperationCanceledException) when (aborted.IsCancellationRequested)
    {
        throw;
    }
    catch (OperationCanceledException)
    {
        return Results.Json(new { code = "upstream_timeout" }, statusCode: 504);
    }
    catch (CometResponseException)
    {
        return Results.Json(
            new { code = "unaccepted_upstream_response" }, statusCode: 502);
    }
    catch (HttpRequestException)
    {
        return Results.Json(
            new { code = "upstream_http_failure" }, statusCode: 502);
    }
});

app.Run();

public sealed record ChatInput(string? Prompt);
public sealed record CometSettings(string Key, string Model);

Microsoft documents typed clients as transient services created through the factory. Inject CometChatClient where it is needed. Avoid adding another ordinary registration for the same class afterward, or capturing this transient client in a singleton that retains it indefinitely. Those changes can bypass the intended configuration or lifetime behavior.

The handler is configured without redirects or automatic cookies because this integration uses a fixed authenticated API destination. Microsoft’s factory reference explains that pooled handlers can share cookie containers. There is no application need for a browser cookie jar on this outbound path.

The 8 KiB request ceiling, 2,000-character prompt limit, 30-second HTTP timeout, and 1 MiB response ceiling are illustrative application settings. They are independent of CometAPI token or account limits. The Kestrel setting applies to the whole dedicated sample application; choose suitable placement when adding the route to an existing service.

Implement the typed adapter

Save this as CometChatClient.cs:

using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;

public sealed class CometChatClient(HttpClient http, CometSettings settings)
{
    public async Task<string> CompleteAsync(
        string prompt, CancellationToken cancellationToken)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(prompt);
        if (prompt.Length > 2000)
            throw new ArgumentException("invalid_prompt", nameof(prompt));

        using var request = new HttpRequestMessage(
            HttpMethod.Post, "chat/completions");
        request.Headers.Authorization = new AuthenticationHeaderValue(
            "Bearer", settings.Key);
        request.Content = JsonContent.Create(new
        {
            model = settings.Model,
            stream = false,
            messages = new[] { new { role = "user", content = prompt } }
        });

        using var response = await http.SendAsync(
            request, HttpCompletionOption.ResponseContentRead,
            cancellationToken);

        if (response.StatusCode != HttpStatusCode.OK)
            throw new HttpRequestException(
                "cometapi_http_failure", null, response.StatusCode);

        var mediaType = response.Content.Headers.ContentType?.MediaType;
        if (!string.Equals(mediaType, "application/json",
                StringComparison.OrdinalIgnoreCase))
            throw new CometResponseException();

        CompletionDto? data;
        try
        {
            data = await response.Content.ReadFromJsonAsync<CompletionDto>(
                cancellationToken: cancellationToken);
        }
        catch (JsonException)
        {
            throw new CometResponseException();
        }

        var choice = data?.Choices is { Length: 1 } choices
            ? choices[0] : null;
        var message = choice?.Message;
        if (data is null || data.Object != "chat.completion" ||
            choice is null || choice.FinishReason != "stop" ||
            message is null || message.Role != "assistant" ||
            message.Refusal is not null ||
            string.IsNullOrWhiteSpace(message.Content))
            throw new CometResponseException();

        return message.Content;
    }
}

public sealed record CompletionDto(
    [property: JsonPropertyName("object")] string? Object,
    [property: JsonPropertyName("choices")] ChoiceDto?[]? Choices);

public sealed record ChoiceDto(
    [property: JsonPropertyName("finish_reason")] string? FinishReason,
    [property: JsonPropertyName("message")] MessageDto? Message);

public sealed record MessageDto(
    [property: JsonPropertyName("role")] string? Role,
    [property: JsonPropertyName("content")] string? Content,
    [property: JsonPropertyName("refusal")] string? Refusal);

public sealed class CometResponseException : Exception
{
    public CometResponseException() : base("unaccepted_chat_completion") { }
}

The request serializes a newly constructed object. A caller cannot change its model, credential, endpoint, or message role through the prompt field. The application input record contains only Prompt; extra JSON properties are ignored by default, so this sample does not promise strict rejection of unknown fields. The Fastify input-validation tutorial explains why a rejection policy needs an explicit runtime validation contract.

Microsoft’s HTTP JSON extensions guide documents serialization and deserialization helpers and their web defaults. Here, JsonContent.Create supplies the JSON body and ReadFromJsonAsync reads the response into narrow DTOs. Explicit property attributes handle names such as finish_reason; they also keep the wire names visible during review.

This adapter accepts one completed assistant text choice. A refusal, length-limited completion, tool call, empty content, or incompatible envelope becomes an application error. Those outcomes need separate product branches if you later support them. Adding a DTO does not make arbitrary model output trustworthy for HTML rendering or tool execution.

Preserve the versioned path

Keep the trailing slash on the base address’s /v1/ path and omit a leading slash from chat/completions. The resulting request must target /v1/chat/completions. A leading slash on the relative path resets it to the host root; a base without its trailing slash can replace the final path segment. Assert the complete outbound URI in your local check.

Keep completion mode and limits together

The example deliberately uses ResponseContentRead. Microsoft’s HttpCompletionOption reference states that this mode completes after reading the response content and enforces MaxResponseContentBufferSize. Consequently, the bounded body is read before the code inspects status and media type, including for error responses. An oversized body can fail as an HTTP-client exception before a status-specific branch runs.

With ResponseHeadersRead, the send completes at the headers. The same reference warns that the client’s timeout then covers only that operation and that its buffer limit is not automatically enforced for later content reads. If you change modes for streaming or earlier status rejection, add explicit body-byte and body-time limits. Merely retaining the two client properties does not preserve this example’s behavior.

The route passes the request cancellation token to the adapter and preserves caller cancellation rather than converting it into an upstream-timeout response. Other cancellation from the client’s timeout maps to HTTP 504. Local cancellation still does not establish whether remote generation stopped or what was billed.

Contract details to verify

Use a custom HttpMessageHandler in a test project to capture outbound requests and return synthetic responses. Construct the typed client with an HttpClient wrapping that handler, the same base address and limits, and synthetic settings. Test registration separately by resolving the client from the application’s service provider; a manually constructed client cannot detect a broken DI registration.

CheckRequired observation
Blank or oversized promptApplication rejects it before any outbound dispatch.
Ordinary requestOne POST targets /v1/chat/completions, with the configured model, one user message, stream: false, and a bearer header.
Completed text fixtureHTTP 200 JSON with one assistant choice and finish_reason: stop returns its text.
Refusal, truncation, or tool callAdapter reports an unaccepted response.
Malformed JSON or wrong media typeApplication returns its fixed response-error code.
HTTP error or redirectApplication returns a fixed failure without copying the upstream body.
Response larger than 1 MiBBuffered send fails instead of passing an oversized document to deserialization.
Delayed body or canceled callerCancellation reaches the active read; no automatic second request starts.

Run dotnet build and your synthetic checks before a live smoke test. Then run dotnet run --no-launch-profile and send a local JSON POST to /api/chat containing a harmless prompt. Expect a small { "reply": "..." } response for an accepted completion. Repeat the invalid-input checks through the HTTP route and require zero outbound calls.

The preparation environment did not contain a .NET SDK, so these snippets were checked against the references but were not compiled or executed here. No authenticated CometAPI request or deployment was performed. Keep the fixture observations above as acceptance criteria, rather than treating them as recorded test results.

Failure modes

The response is HTML or the path is wrong. Inspect the complete outbound URI and status. Check both slashes before changing the JSON decoder. Redirects remain visible because automatic following is disabled.

The typed client loses its settings. Look for a later registration that replaced AddHttpClient<CometChatClient>(), or a singleton that captured the client. Verify resolution through the actual application service provider.

The route returns 502 for valid JSON. JSON syntax alone is insufficient. Inspect a controlled, redacted fixture for the object type, choice count, finish reason, assistant role, refusal, and text. Do not weaken the parser to accept every provider result as a reply.

An exception leaks private content. Keep fixed error codes in application responses. Do not serialize CometSettings, whose generated record representation includes its values, or log the authorization header, prompts, response bodies, or raw exception details. Review existing factory logging and custom middleware before enabling verbose diagnostics.

Repeated calls multiply after a timeout. The example adds no retry handler. A transport timeout can leave the remote outcome unknown. Introduce retries only with an explicit attempt limit, caller deadline, and replay policy.

The Ruby Net::HTTP client tutorial provides a useful comparison for request construction and response classification in another runtime. Keep the .NET factory and completion-mode checks when adopting those general boundaries.

Sources checked

These four primary references were fetched successfully on October 30, 2026 UTC. They support an evergreen integration task; the client design and local limits are recommendations, not a new-release announcement or provider defaults.

FAQ

Does this require a CometAPI-specific .NET package?

No. The example calls the documented HTTP contract using the ASP.NET Core web template and standard HTTP and JSON libraries. A provider SDK is an alternative when it supports the exact endpoint and behavior you need.

Can I use the same client for streaming?

Create a separate streaming method with an SSE parser, body limits, a deadline that remains active during consumption, and explicit response ownership. The non-streaming DTOs here do not represent chat chunks.

Should I dispose the injected HttpClient after each call?

Dispose the request and response inside the method. Let the application and factory manage the client and pooled-handler lifecycle. Do not dispose the injected client inside CompleteAsync, where another call could still need it.

Why reject a completion whose finish reason is length?

The chat reference defines that value as hitting the generation limit. This sample’s application contract accepts completed text. A product that displays truncation should return a distinct result with that state, rather than silently labeling it complete.

Reader next step

Compile the two files, verify factory resolution, and run the synthetic request and failure matrix first. Save the SDK version, fixture outcome, outbound path, response category, and redaction result. Then make one authorized staging request to validate the chosen model and live network path.

For account setup, Start with CometAPI , configure the credential server-side, and repeat the same narrow smoke test. Add streaming or tools only after defining their separate response contracts.