Zibma.Campus.Drive.Api.Sdk 2.0.0

Zibma.Campus.Drive.Api.Sdk

Typed .NET client for the Zibma Campus Drive API, with one async method for each of its 30 endpoints. It is built on IHttpClientFactory, streams its responses, and uses source-generated System.Text.Json (no runtime reflection).

Getting started

1. Install the package

The package is published on the Zibma feed. If your nuget.config doesn't list that feed yet, add it:

<!-- nuget.config -->
<configuration>
  <packageSources>
    <add key="nuget" value="https://api.nuget.org/v3/index.json" />
    <add key="ZibmaBaGet" value="https://baget.zibma.com/v3/index.json" allowInsecureConnections="True" />
  </packageSources>
</configuration>
dotnet add package Zibma.Campus.Drive.Api.Sdk --version 2.0.0

The package targets .NET 10. Its only dependency is Zibma.Campus.Common.

2. Configure the Drive API's address

Add a DriveApi section to appsettings.json:

{
  "DriveApi": {
    "BaseAddress": "http://driveapi.campuserp-dev.svc.cluster.local:8080/",
    "Timeout": "00:01:40"
  }
}

You can set the same values with environment variables, e.g. in a Kubernetes deployment or a .env file:

DriveApi__BaseAddress=http://driveapi.campuserp-dev.svc.cluster.local:8080/
DriveApi__Timeout=00:01:40
Setting Required Meaning
BaseAddress yes The Drive API's address. If you call it through a path base (e.g. https://host/api/drive/), include the path base. A missing trailing / is added for you.
Timeout no The longest one call may take, including reading the response. The default is 100 seconds; raise it for large uploads.

The settings are validated at startup: a missing or relative BaseAddress stops the app with a clear message.

3. Register the client

// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDriveApiClient(); // reads the "DriveApi" section

var app = builder.Build();

AddDriveApiClient() lives in the Microsoft.Extensions.DependencyInjection namespace, so it needs no extra using. Two variants are available:

builder.Services.AddDriveApiClient("MyDriveSection");  // a different configuration section
builder.Services.AddDriveApiClient(options =>          // configured in code
{
    options.BaseAddress = new Uri("http://localhost:5006/");
});

AddDriveApiClient returns the IHttpClientBuilder, so you can add handlers to it. For example, to add retries from the Microsoft.Extensions.Http.Resilience package:

builder.Services.AddDriveApiClient().AddStandardResilienceHandler();

Retries also resend POSTs such as uploads, backups and copies. Only add them if a duplicate is acceptable, or restrict them to GET.

4. Make your first call

Inject IDriveApiClient wherever you need it. RefreshAsync takes no institute and no credentials, which makes it a good connectivity check:

using Zibma.Campus.Drive.Api.Sdk;

app.MapGet("/drive-status", async (IDriveApiClient drive, CancellationToken ct) =>
{
    var response = await drive.RefreshAsync(ct);
    return response.ResponseMessage; // "Refresh Successfully."
});

IDriveApiClient is a typed client. Inject it into scoped or transient services (endpoints, controllers, scoped services), not into a singleton.

Calling the API

The institute

The Drive API serves every institute, so each call (except RefreshAsync) names its institute in the first parameter, instituteUniqueId. The SDK sends it as the X-Client-Id header.

The three kinds of authorization

Each method's XML docs (shown in IntelliSense) say which kind it uses.

Kind What you pass Methods
Internal (service to service, cron jobs) apiKey: the institute's internal API key. The SDK sends it as the X-Api-Key header. *InternalAsync, OldCDNDataSaveAsync and the four cron jobs
User (on behalf of a signed-in user) accessToken: the user's JWT, issued for the same institute. Both a bare token and a forwarded "Bearer ..." header value work. CopyFileAsync, DeleteFileAsync, DeleteUnusedFileAsync, UploadPendingFileAsync, BackupConfigurationGetAsync / SetAsync
User, optional token accessToken or null. Without a token, put the year (DBYear) in the request. UploadFileAsync, GetUploadUrlAsync, GetFileAsync, GetFileSingleAsync

The admin calls (BackupTakeNowAsync, BackupShrinkDatabaseAsync, ZquerySelectAsync and ZqueryExecuteAsync) carry the admin or ZQuery password in their request model. ConfirmUploadAsync, BackupGetLastDetailAsync and VersionUpdateNowAsync need only the institute.

Sample: internal calls

Where the institute id and its API key come from is up to your service; this sample reads them from its own settings:

{
  "DriveCaller": {
    "InstituteUniqueId": 79,
    "ApiKey": "<the institute's internal API key>"
  }
}
using Microsoft.Extensions.Options;
using Zibma.Campus.Common.Enums;
using Zibma.Campus.Drive.Api.Sdk;
using Zibma.Campus.Drive.Api.Sdk.Features.DriveAPIs.CopyFile;
using Zibma.Campus.Drive.Api.Sdk.Features.DriveAPIs.CopyFileInternal;
using Zibma.Campus.Drive.Api.Sdk.Features.DriveAPIs.DeleteFileInternal;
using Zibma.Campus.Drive.Api.Sdk.Features.DriveAPIs.GetFileInternal;

public sealed class DriveCallerSettings
{
    public int InstituteUniqueId { get; set; }
    public string ApiKey { get; set; } = string.Empty;
}

public sealed class StudentPhotoService(IDriveApiClient drive, IOptions<DriveCallerSettings> settings)
{
    private readonly DriveCallerSettings _settings = settings.Value;

    public async Task<string?> GetThumbnailUrlAsync(Guid photo, string dbYear, CancellationToken ct)
    {
        var response = await drive.GetFileInternalAsync(_settings.InstituteUniqueId, _settings.ApiKey, new GetFileInternalRequestModel
        {
            Guid = photo,
            PhotoSize = ePhotoSize.Thumb,
            EFileModule = eFileModule.PersonPhotos,
            DBYear = dbYear
        }, ct);

        return response.FileURL;
    }

    public async Task<Guid> CopyToNextYearAsync(Guid photo, string fromYear, string toYear, CancellationToken ct)
    {
        var response = await drive.CopyFileInternalAsync(_settings.InstituteUniqueId, _settings.ApiKey, new CopyFileInternalRequestModel
        {
            LstFiles = [new FilesDto { FromGuid = photo, FromFileModule = eFileModule.PersonPhotos, ToFileModule = eFileModule.PersonPhotos }],
            FromDBYear = fromYear,
            ToDBYear = toYear
        }, ct);

        return response.LstFiles.Single().NewGuid;
    }

    public Task DeleteAsync(IEnumerable<Guid> photos, string dbYear, CancellationToken ct) =>
        drive.DeleteFileInternalAsync(_settings.InstituteUniqueId, _settings.ApiKey, new DeleteFileInternalRequestModel
        {
            LstGuid = photos.ToList(),
            EFileModule = eFileModule.PersonPhotos,
            DBYear = dbYear
        }, ct);
}
// Program.cs
builder.Services.Configure<DriveCallerSettings>(builder.Configuration.GetSection("DriveCaller"));
builder.Services.AddScoped<StudentPhotoService>();

The cron jobs need only the institute and its key:

await drive.DriveDatabaseBackupInternalAsync(instituteUniqueId, apiKey, ct);

Sample: calling on behalf of the signed-in user

Forward the incoming request's Authorization header as the accessToken:

using Zibma.Campus.Common.Enums;
using Zibma.Campus.Drive.Api.Sdk;
using Zibma.Campus.Drive.Api.Sdk.Features.DriveAPIs.DeleteFile;
using Zibma.Campus.Drive.Api.Sdk.Features.DriveAPIs.GetFile;

app.MapDelete("/documents/{id:guid}", async (Guid id, HttpRequest http, IDriveApiClient drive, CancellationToken ct) =>
{
    int instituteUniqueId = int.Parse(http.Headers["X-Client-Id"]!);  // or wherever your service keeps it
    string accessToken = http.Headers.Authorization.ToString();        // "Bearer eyJ..."

    var response = await drive.DeleteFileAsync(instituteUniqueId, accessToken, new DeleteFileRequestModel
    {
        Guid = id,
        EFileModule = eFileModule.StudentDocuments
    }, ct);

    return response.IsDeleted == true ? Results.NoContent() : Results.NotFound();
});

app.MapPost("/documents/urls", async (List<Guid> ids, HttpRequest http, IDriveApiClient drive, CancellationToken ct) =>
{
    int instituteUniqueId = int.Parse(http.Headers["X-Client-Id"]!);

    var response = await drive.GetFileAsync(instituteUniqueId, http.Headers.Authorization.ToString(), new GetFileRequestModel
    {
        LstGuid = ids,
        PhotoSize = ePhotoSize.Original,
        EFileModule = eFileModule.StudentDocuments
        // DBYear omitted: the year in the user's token is used
    }, ct);

    return response.LstFile;
});

Uploading files

Through the Drive API

Pass the file as a stream. The SDK streams it without buffering, and it doesn't dispose your stream:

using Zibma.Campus.Common.Enums;
using Zibma.Campus.Drive.Api.Sdk;
using Zibma.Campus.Drive.Api.Sdk.Features.DriveAPIs.UploadFile;

app.MapPost("/documents", async (IFormFile file, HttpRequest http, IDriveApiClient drive, CancellationToken ct) =>
{
    int instituteUniqueId = int.Parse(http.Headers["X-Client-Id"]!);

    await using var stream = file.OpenReadStream();
    var uploaded = await drive.UploadFileAsync(instituteUniqueId, http.Headers.Authorization.ToString(), new UploadFileRequestModel
    {
        Content = stream,
        FileName = file.FileName,          // the extension decides the stored file type
        ContentType = file.ContentType,
        EFileModule = eFileModule.StudentDocuments
    }, ct);

    return uploaded.Guid;
}).DisableAntiforgery();

Without a user, use UploadFileInternalAsync(instituteUniqueId, apiKey, ...). It takes the same file fields plus a required DBYear. For an image held as base64, use UploadBase64FileInternalAsync.

Direct to storage (presigned URL)

A direct upload skips the Drive API for the file's bytes. It takes three steps:

using Zibma.Campus.Common.Enums;
using Zibma.Campus.Drive.Api.Sdk;
using Zibma.Campus.Drive.Api.Sdk.Features.DriveAPIs.ConfirmUpload;
using Zibma.Campus.Drive.Api.Sdk.Features.DriveAPIs.GetUploadUrlInternal;

public sealed class DirectUploader(IDriveApiClient drive, IHttpClientFactory httpClientFactory)
{
    public async Task<Guid> UploadAsync(int instituteUniqueId, string apiKey, Stream file, string fileName, string dbYear, CancellationToken ct)
    {
        // 1. Reserve the file and get a presigned PUT URL (valid for 30 minutes)
        var slot = await drive.GetUploadUrlInternalAsync(instituteUniqueId, apiKey, new GetUploadUrlInternalRequestModel
        {
            FileName = fileName,
            FileModule = eFileModule.StudentDocuments,
            DBYear = dbYear
        }, ct);

        // 2. PUT the bytes straight to storage. This is a plain HTTP call, not a Drive API call.
        using var put = await httpClientFactory.CreateClient().PutAsync(slot.PresignedUrl, new StreamContent(file), ct);
        put.EnsureSuccessStatusCode();

        // 3. Tell the Drive API the file is there, so it is processed
        await drive.ConfirmUploadAsync(instituteUniqueId, new ConfirmUploadRequestModel
        {
            Guid = slot.Guid,
            FileModule = eFileModule.StudentDocuments,
            DBYear = dbYear
        }, ct);

        return slot.Guid;
    }
}

A signed-in user follows the same flow with GetUploadUrlAsync(instituteUniqueId, accessToken, ...).

Errors

A method returns the response model when the API accepts the request. When the API rejects it, the method throws DriveApiException:

using System.Net;
using Zibma.Campus.Drive.Api.Sdk;

try
{
    await drive.CopyFileAsync(instituteUniqueId, accessToken, request, ct);
}
catch (DriveApiException e) when (e.StatusCode == HttpStatusCode.BadRequest)
{
    logger.LogWarning("Drive rejected the copy: {Message} {Errors}", e.ResponseMessage, e.Errors);
    // e.ResponseMessage: e.g. "ToDBYear Invalid." or "Api Key is Invalid."
    // e.Errors: validation failures, e.g. "'Lst Files' must not be empty."
}
  • The exception message holds the HTTP method, the path and the API's message. It never includes the query string.
  • A missing or wrong API key gives HTTP 400 with the message "Api Key is Invalid.".
  • On a user call, a missing or invalid token gives HTTP 401. A token issued for another institute, or without the needed permission, gives HTTP 403.
  • version-update-now reports a failed update with HTTP 200 and an error status in the body. That case also throws.
  • Network failures throw HttpRequestException, and timeouts throw TaskCanceledException.
  • Argument errors (a non-positive institute, a missing token on a user call, a missing API key on an internal call, a null request) throw immediately, before any request is sent.

Testing your code

Your code depends on the IDriveApiClient interface, so you can replace it with a fake in your own tests:

using Zibma.Campus.Drive.Api.Sdk;
using Zibma.Campus.Drive.Api.Sdk.Features.DriveAPIs.GetFileInternal;

// e.g. with Moq
var drive = new Mock<IDriveApiClient>();
drive.Setup(d => d.GetFileInternalAsync(79, "key", It.IsAny<GetFileInternalRequestModel>(), It.IsAny<CancellationToken>()))
     .ReturnsAsync(new GetFileInternalResponseModel { FileURL = "https://cdn.test/a.jpg" });

To test a rejection, throw new DriveApiException(HttpStatusCode.BadRequest, "Bad Request", responseMessage: "DBYear Invalid.").

Migrating from 1.x

  • IDriveApiSdkService is now IDriveApiClient. Replace RegisterDriveApiSdkServiceExtension(), ConfigureDriveApiSdkServiceExtension() and your AddHttpClient("DriveApiClient") with AddDriveApiClient().
  • Configuration is a single DriveApi:BaseAddress instead of one URL per endpoint (DriveApiConfiguration:*Endpoint).
  • Methods return the response model rather than (model, HttpResponseMessage), and they throw DriveApiException on failure. cancellationToken is now optional.
  • User calls take the accessToken parameter. 1.x never sent one, so those endpoints always failed with 401.
  • Internal calls take an apiKey parameter, sent as the X-Api-Key header. The ApiKey properties are gone from the request models, and the cron-job methods no longer take a request model. The API accepts the key only in the header now.
  • Model properties are PascalCase (lstFiles is now LstFiles, guid is now Guid). The JSON names sent to the API are unchanged.
  • Uploads take a Stream, FileName and ContentType instead of an IFormFile.
  • Renamed models: CopyFileInternalRequest(Response) and GetFileInternalRequest(Response) gained the Model suffix, and UploadBase64RequestModel / UploadBase64FileResponse are now UploadBase64FileRequestModel / UploadBase64FileResponseModel. eDBType moved to Zibma.Campus.Drive.Api.Sdk.Common.Enums.
  • New methods: UploadFileInternalAsync, UploadBase64FileInternalAsync, GetUploadUrlAsync, GetUploadUrlInternalAsync, ConfirmUploadAsync and the four cron-job calls.
  • Fixed methods: BackupTakeNowAsync, BackupShrinkDatabaseAsync (now POST with a body), DeleteFileInternalAsync (POST with its own model) and UploadFileAsync (values in the query string, where the API reads them).

No packages depend on Zibma.Campus.Drive.Api.Sdk.

2.0.0: all 30 Drive API endpoints; typed IDriveApiClient registered with AddDriveApiClient() and configured by DriveApi:BaseAddress; DriveApiException on failure; user calls take an accessToken; internal calls take an apiKey, sent as the X-Api-Key header. Breaking: see "Migrating from 1.x" in the README.

.NET 10.0

Version Downloads Last updated
2.0.0 126 10/26/2026
1.0.2 25 09/05/2025
1.0.1 13 08/04/2025
1.0.0 16 07/30/2025