# C# quickstart

Location: [gRPC API](https://developer.m-files.com/APIs/gRPC-API/index.md) > [Guides](https://developer.m-files.com/APIs/gRPC-API/index.md#guides)

HTML version: https://developer.m-files.com/APIs/gRPC-API/guides/quickstart-csharp/

In about 15 minutes you build a .NET console app that connects to an M-Files vault over native
gRPC, logs in, lists the 10 most recently modified documents and logs out. The app uses the
official `Grpc.Net.Client` and `Google.Protobuf` packages, client code generated from the M-Files
protos, and a small connection helper from the samples that you can copy into your own project.

## Prerequisites

* The [.NET SDK](https://dotnet.microsoft.com/download) 10 or later.
* A vault you can reach over gRPC:
  * **M-Files Cloud:** the vault's host name; the endpoint is `<vault host>:443` with TLS.
  * **On-premises:** the server's host name and gRPC port, `7766` by default; plain HTTP/2
    unless the administrator has enabled TLS.
* The vault's GUID, for example `{7BBEA0BC-40D8-4BCB-A67D-8A3ACFBDB575}`. Don't know the GUID?
  [Find the vault by its name](https://developer.m-files.com/APIs/gRPC-API/guides/connect/index.md#find-the-vault-by-its-name) lists the vaults you can log
  in to, or ask your M-Files administrator.
* An M-Files user name and password. For single sign-on, see
  [Configure](#configure).

[Cloud and on-premises](https://developer.m-files.com/APIs/gRPC-API/runtime/deployments/index.md) and
[Connecting to the server](https://developer.m-files.com/APIs/gRPC-API/runtime/connecting/index.md) explain the endpoints in detail.

## Get the samples and the protos

Download the [gRPC API bundle](https://developer.m-files.com/APIs/gRPC-API/downloads/mfiles-grpc-api-bundle.zip) and unpack it. It contains:

* `proto/`: the API definition, one `.proto` file per service plus `mfiles/structs.proto`,
  `mfiles/enums.proto` and `mfiles/api/annotations.proto`, with the documentation as comments;
* `samples/csharp/`: the solution this guide uses.

An M-Files Server installation also has the API definition, in
`<installation folder>\Common\Web\GRPC\proto`, as one combined file without documentation. It
describes the same wire contract, but its generated C# namespace is `MFiles` rather than
`MFiles.Grpc`. Prefer the bundle.

## Set up the project

The samples are a solution with three projects:

| Project | What it is |
|---|---|
| `Connection` | Class library: the generated client code and the `MFilesConnection` helper |
| `Quickstart` | The console app of this guide |
| `Recipes` | One console program per task recipe |

`Connection` references three packages. `Grpc.Tools` is needed only at build time:

```xml
<ItemGroup>
  <PackageReference Include="Google.Protobuf" Version="3.36.2" />
  <PackageReference Include="Grpc.Net.Client" Version="2.85.0" />
  <PackageReference Include="Grpc.Tools" Version="2.85.0" PrivateAssets="All" />
</ItemGroup>
```

*Source: [csharp/Connection/Connection.csproj](https://developer.m-files.com/APIs/gRPC-API/downloads/samples/csharp/Connection/Connection.csproj)*

`Grpc.Tools` generates the client code from the protos on every build. `ProtoRoot` must be the
folder that contains `mfiles/`, because the files import each other by that path:

```xml
<!-- Generate client classes for every service of the bundle. ProtoRoot must be the folder that
     contains mfiles/, because the protos import each other as "mfiles/structs.proto". -->
<ItemGroup>
  <Protobuf Include="$(MFilesProtoDir)/mfiles/**/*.proto" ProtoRoot="$(MFilesProtoDir)"
            GrpcServices="Client" Access="Public" />
</ItemGroup>
```

*Source: [csharp/Connection/Connection.csproj](https://developer.m-files.com/APIs/gRPC-API/downloads/samples/csharp/Connection/Connection.csproj)*

`MFilesProtoDir` is set in `Directory.Build.props`: the `MFILES_PROTO_DIR` environment variable
if it is set, otherwise `../../proto`, which is where the bundle keeps the protos. To use protos
elsewhere, set `MFILES_PROTO_DIR` to the folder that contains `mfiles/structs.proto`.

The generated classes are in the namespace `MFiles.Grpc`: a class per message, such as
`SearchObjectsRequest`, and a client per service, such as `IRPCSearch.IRPCSearchClient`.

To start a project of your own instead, run `dotnet new console`, add the three packages, and copy
the `Protobuf` item and the `MFiles*.cs` files of `Connection`.

## Configure

The samples read their settings from environment variables:

| Variable | Value |
|---|---|
| `MFILES_ADDRESS` | `host:port`, for example `contoso.cloudvault.m-files.com:443` or `mfserver:7766` |
| `MFILES_TLS` | `true` (default), or `false` for a server without TLS |
| `MFILES_CA_CERT` | Optional: a PEM file of the root certificate to trust, for a server whose certificate comes from a private CA |
| `MFILES_VAULT` | The vault GUID, with braces |
| `MFILES_USERNAME`, `MFILES_PASSWORD` | An M-Files user account |

M-Files Cloud:

```powershell
$env:MFILES_ADDRESS = 'contoso.cloudvault.m-files.com:443'
$env:MFILES_VAULT = '{7BBEA0BC-40D8-4BCB-A67D-8A3ACFBDB575}'
$env:MFILES_USERNAME = 'alice'
$env:MFILES_PASSWORD = Read-Host -Prompt 'Password'
```

On-premises without TLS:

```powershell
$env:MFILES_ADDRESS = 'mfserver:7766'
$env:MFILES_TLS = 'false'
$env:MFILES_VAULT = '{7BBEA0BC-40D8-4BCB-A67D-8A3ACFBDB575}'
$env:MFILES_USERNAME = 'alice'
$env:MFILES_PASSWORD = Read-Host -Prompt 'Password'
```

**Single sign-on.** A vault that signs users in through M-Files Login Service, as is typical on
M-Files Cloud, refuses a password login with "Authentication failed.". Set `MFILES_TOKEN` to an
access token from the Login Service, and `MFILES_AUTH_PLUGIN` and `MFILES_AUTH_SCOPE` to the plugin
and scope that the vault returns.
[M-Files Login Service](https://developer.m-files.com/APIs/gRPC-API/runtime/authentication/index.md#m-files-login-service) shows how to get the
token; the Python sample `recipes/login_service.py` does it end to end.

## Run

```powershell
cd samples\csharp
dotnet run --project Quickstart
```

The first build restores the packages and generates the client code, which takes a minute. The
output lists the documents, newest first. In the M-Files Sample Vault it starts like this:

```text
Project Plan (template) (ID 445, version 1, modified 2012-06-07 12:01:57 UTC)
Project Tasks - CRM Development (ID 444, version 1, modified 2012-06-07 11:54:29 UTC)
Corporate Presentation (ID 443, version 1, modified 2012-06-07 11:40:14 UTC)
...
```

## The code

```csharp
using MFiles.Grpc;
using MFiles.Grpc.Samples;
using static MFiles.Grpc.Samples.MFilesValues;

// Connect and log in with the MFILES_* settings. Disposing the connection logs out.
await using var connection = await MFilesConnection.ConnectAsync(
    MFilesSettings.FromEnvironment(), clientName: "M-Files gRPC quickstart");

var request = new SearchObjectsRequest
{
    // Documents (object type 0) that are not deleted.
    Conditions = { new SearchConditionArray { Value = { IsObjectType(0), IsNotDeleted() } } },
    // Newest first by property 21, "Last modified". The server sorts, then takes 10.
    Sorting = new SearchResultsSorting
    {
        Rules = new SortRuleValueArray
        {
            Values = { new SortRule { Key = 21, Options = new SortRuleFlags { SortDescending = true } } },
        },
    },
    Limit = 10,
};

// Service clients run on the connection's call invoker, which adds the session to each call.
var search = new IRPCSearch.IRPCSearchClient(connection.CallInvoker);
var response = await search.SearchObjectsAsync(request);

foreach (var item in response.Results)
{
    var version = item.Object.VersionInfo;
    Console.WriteLine($"{version.Title} (ID {item.Object.ObjectInfo.ObjId.ItemId.InternalId}, " +
        $"version {version.Version.InternalVersion}, " +
        $"modified {version.LastModifiedAtUtc.ToDateTime():yyyy-MM-dd HH:mm:ss} UTC)");
}
```

*Source: [csharp/Quickstart/Program.cs](https://developer.m-files.com/APIs/gRPC-API/downloads/samples/csharp/Quickstart/Program.cs)*

* `MFilesConnection.ConnectAsync` opens the channel and logs in. `await using` logs out and
  closes the channel at the end.
* Every service is a generated client class created on `connection.CallInvoker`.
* The search has one array of conditions; all conditions in an array must match. Without the
  "not deleted" condition, users who may see deleted objects get them too.
* `Limit = 10` with a sort order returns the first 10 of all matches in that order.
* A failure ends the program with an `MFilesException` that names the M-Files error code.

`IsObjectType` and `IsNotDeleted` are small builders from the helper. They show the message
shapes of the two conditions:

```csharp
/// <summary>A condition on a property value, e.g. property 100 (Class) equals a lookup.</summary>
public static SearchCondition PropertyCondition(
    int propertyDef, ConditionType type, TypedValue value) => new()
{
    Expression = new Expression
    {
        Type = ExpressionType.PropertyValue,
        Data = new ExpressionUnion
        {
            PropertyValue = new ExpressionUnion.Types.PropertyValueData
            {
                PropertyDef = propertyDef,
            },
        },
    },
    Type = type,
    Value = value,
};

/// <summary>A condition on a status value, e.g. the object type or the deleted flag.</summary>
public static SearchCondition StatusCondition(
    StatusType status, ConditionType type, TypedValue value) => new()
{
    Expression = new Expression
    {
        Type = ExpressionType.StatusValue,
        Data = new ExpressionUnion
        {
            StatusValue = new ExpressionUnion.Types.StatusValueData { Type = status },
        },
    },
    Type = type,
    Value = value,
};

/// <summary>Objects of one object type. Value list -11 lists the object types.</summary>
public static SearchCondition IsObjectType(int objectType) =>
    StatusCondition(StatusType.ObjectType, ConditionType.Equal, Lookup(-11, objectType));

/// <summary>Objects that are not deleted.</summary>
public static SearchCondition IsNotDeleted() =>
    StatusCondition(StatusType.IsDeleted, ConditionType.Equal, Boolean(false));
```

*Source: [csharp/Connection/MFilesValues.cs](https://developer.m-files.com/APIs/gRPC-API/downloads/samples/csharp/Connection/MFilesValues.cs)*

## What the helper does

gRPC alone is not enough to talk to M-Files: part of the contract travels in call metadata, and
errors arrive in trailers. `MFilesConnection` takes care of that.

**The channel.** TLS or plain HTTP/2, an optional private root certificate, and the message size
limits that M-Files' own client uses: responses of any size, requests up to 48 MiB. With an `http://` address, .NET makes an unencrypted HTTP/2 connection; no extra switch
is needed on .NET 10. `SocketsHttpHandler` keeps cookies, so the affinity cookie that
M-Files Cloud sets (`mfilesmsm`) goes back on later calls, as
[Call metadata](https://developer.m-files.com/APIs/gRPC-API/runtime/call-metadata/index.md#cookies-and-stickiness) requires:

```csharp
var handler = new SocketsHttpHandler
{
    // M-Files Cloud answers with a server affinity cookie (mfilesmsm); the handler's
    // cookie container sends it back on later calls.
    UseCookies = true,
    // gRPC multiplexes calls on one HTTP/2 connection; allow more when it is busy.
    EnableMultipleHttp2Connections = true,
    KeepAlivePingDelay = TimeSpan.FromSeconds(60),
    KeepAlivePingTimeout = TimeSpan.FromSeconds(30),
};
if (settings.CaCertificatePath is { } caPath)
{
    // Trust this root (e.g. an on-premises server's own CA) instead of the system's roots.
    var roots = new X509Certificate2Collection();
    roots.ImportFromPemFile(caPath);
    handler.SslOptions.CertificateChainPolicy = new X509ChainPolicy
    {
        TrustMode = X509ChainTrustMode.CustomRootTrust,
        // A private CA usually publishes no revocation list.
        RevocationMode = X509RevocationMode.NoCheck,
    };
    handler.SslOptions.CertificateChainPolicy.CustomTrustStore.AddRange(roots);
}

// "http://" makes an unencrypted HTTP/2 connection (lab servers only).
string scheme = settings.UseTls ? "https" : "http";
var channel = GrpcChannel.ForAddress($"{scheme}://{settings.Address}", new GrpcChannelOptions
{
    HttpHandler = handler,
    // No limit (null) on received messages: responses can be much larger than gRPC's
    // default of 4 MiB.
    MaxReceiveMessageSize = null,
    MaxSendMessageSize = MaxSendMessageSize,
});
```

*Source: [csharp/Connection/MFilesConnection.cs](https://developer.m-files.com/APIs/gRPC-API/downloads/samples/csharp/Connection/MFilesConnection.cs)*

**Call metadata.** A call invoker adds the M-Files metadata to every call, and after login the
session ID:

```csharp
/// <summary>
/// The metadata M-Files expects on every call. gRPC base64-encodes "-bin" values itself.
/// </summary>
private CallOptions WithMFilesMetadata(CallOptions options)
{
    var headers = new Metadata();
    foreach (var entry in options.Headers ?? [])
        headers.Add(entry);

    // The caller is not one of the server's own local processes.
    headers.Add("mfiles-is-remote-call", "true");

    // A new 16-byte ID per call; it identifies the call in the server's logs.
    headers.Add("mfiles-activity-id-bin", Guid.NewGuid().ToByteArray());

    // After LogIn: the session ID, unchanged.
    if (connection.SessionId is { } sessionId)
        headers.Add("mfiles-session-id-bin", sessionId.ToByteArray());

    return options.WithHeaders(headers);
}
```

*Source: [csharp/Connection/MFilesCallInvoker.cs](https://developer.m-files.com/APIs/gRPC-API/downloads/samples/csharp/Connection/MFilesCallInvoker.cs)*

**Login.** `LogIn` with M-Files credentials returns a session; its `session_id` goes into
`mfiles-session-id-bin` from then on:

```csharp
var authentication = new AuthDataClient
{
    Type = AuthDataType.Credentials,
    Data = new AuthDataClientUnion
    {
        Credentials = new Credentials
        {
            Username = username,
            Password = password,
            Type = CredentialsType.Mfiles,  // M-Files authentication
        },
    },
};
```

*Source: [csharp/Connection/MFilesConnection.cs](https://developer.m-files.com/APIs/gRPC-API/downloads/samples/csharp/Connection/MFilesConnection.cs)*

```csharp
var request = new LogInRequest
{
    EnvironmentData = new EnvironmentData { ClientName = clientName, HostPlatform = ".NET" },
    ClientData = new ClientData
    {
        Type = ClientType.ServerApi,
        Language = "en",
        // Needed for the 32-byte attempt ID of a token login.
        Capabilities = new ClientCapabilities { LongAuthenticationAttemptIdentifier = true },
    },
    LoginData = new LoginData { ServerHostname = _host, VaultGuid = vault },
    AuthenticationData = authentication,
};
var login = new IRPCLogin.IRPCLoginClient(CallInvoker);
var response = await login.LogInAsync(request, cancellationToken: cancellationToken);

if (response.SessionData is not { SessionId.IsEmpty: false } session)
    throw new InvalidOperationException(
        "LogIn returned no session; this helper supports one-step logins only.");

// Every later call sends this session ID in mfiles-session-id-bin (see MFilesCallInvoker).
Session = session;
```

*Source: [csharp/Connection/MFilesConnection.cs](https://developer.m-files.com/APIs/gRPC-API/downloads/samples/csharp/Connection/MFilesConnection.cs)*

**Errors.** A failed call throws `MFilesException`, decoded from the
`mfiles-error-serialized-bin` and `mfiles-error-text-bin` trailers: the gRPC status
(`StatusCode`), the message, the M-Files error code of the top-level error (`ErrorCode`, an
HRESULT such as `0x8004000B` "Not found."; the last entry of the error stack that is not the
generic `0x80040001`), every code of the stack (`ErrorCodes`) and `Has(code)`:

```csharp
/// <summary>The generic code of an error that is only passed up the stack.</summary>
private const int PassThrough = unchecked((int)0x80040001);

/// <summary>
/// Decodes the M-Files error from the trailers of a failed call.
/// </summary>
public static MFilesException FromRpcException(RpcException e)
{
    // The error stack: a serialized MFiles.RPCErrorInfoArray, the original error first.
    var errors = e.Trailers.GetValueBytes("mfiles-error-serialized-bin") is { } stack
        ? RPCErrorInfoArray.Parser.ParseFrom(stack).Value.ToList()
        : [];

    // The long error message: UTF-8 text in a binary header.
    string? details = e.Trailers.GetValueBytes("mfiles-error-text-bin") is { } text
        ? System.Text.Encoding.UTF8.GetString(text)
        : null;

    // The call failed with the top-level error, the last entry; the status detail is its
    // message. Skip entries that only pass the error on.
    int? errorCode = errors.Count > 0 ? errors[^1].ErrorCode : null;
    if (errors.LastOrDefault(error => error.ErrorCode != PassThrough) is { } topLevel)
        errorCode = topLevel.ErrorCode;
    return new MFilesException(e, errorCode, details, errors);
}
```

*Source: [csharp/Connection/MFilesException.cs](https://developer.m-files.com/APIs/gRPC-API/downloads/samples/csharp/Connection/MFilesException.cs)*

Catch it like this:

```csharp
try
{
    await objects.GetObjectInfoAndPropertiesAsync(request);
}
catch (MFilesException e) when (e.Has(0x8004000B))  // not found
{
    Console.WriteLine($"{e.ErrorCodeHex}: {e.Message}");
}
```

[Errors](https://developer.m-files.com/APIs/gRPC-API/runtime/errors/index.md) lists the common codes and what to do about each.

## Troubleshooting

`dotnet run --project Recipes -- connect` calls `QueryVersion` before it logs in, so it tells
connection problems from login problems.

| Symptom | Cause |
|---|---|
| Build error "M-Files protos not found" | Set `MFILES_PROTO_DIR` to the folder that contains `mfiles/structs.proto`. |
| Status `Unavailable`, "Error connecting to subchannel" | Wrong host or port, or a firewall in between. |
| Status `Internal`, "The SSL connection could not be established" ... `UntrustedRoot` | The server's certificate comes from a CA your machine does not trust. Set `MFILES_CA_CERT` to its root certificate (PEM). |
| Status `Internal`, "The SSL connection could not be established" ... "corrupted frame" | TLS against a server without TLS: set `MFILES_TLS=false`, only for a lab server on a trusted network. |
| Status `Unimplemented`, "HTTP status code: 404", against M-Files Cloud | `MFILES_TLS=false`; M-Files Cloud needs TLS. |
| M-Files error `0x8004001A`, "Authentication failed." | Wrong user name or password, or the vault uses single sign-on (see [Configure](#configure)). |

## Next steps

* Run the task recipes: `dotnet run --project Recipes -- <recipe>`, with `connect`,
  `vault-structure`, `search`, `read-object`, `download-file`, `create-document`,
  `update-properties`, `workflow-state`, `paging` or `errors`. The samples' `README.md` lists
  their arguments.
* Learn the model: [Vault and metadata structure](https://developer.m-files.com/APIs/gRPC-API/concepts/vault-and-metadata-structure/index.md),
  [Objects and versions](https://developer.m-files.com/APIs/gRPC-API/concepts/objects-and-versions/index.md),
  [Properties and values](https://developer.m-files.com/APIs/gRPC-API/concepts/properties-and-values/index.md) and
  [Search](https://developer.m-files.com/APIs/gRPC-API/concepts/search/index.md).
* Keep a session healthy in a long-running program: [Authentication and
  sessions](https://developer.m-files.com/APIs/gRPC-API/runtime/authentication/index.md).
* Find any method in the reference, starting with
  [SearchObjects](https://developer.m-files.com/APIs/gRPC-API/Reference/MFiles.IRPCSearch/SearchObjects/index.md) and
  [GetObjectInfoAndProperties](https://developer.m-files.com/APIs/gRPC-API/Reference/MFiles.IRPCObjectOperations/GetObjectInfoAndProperties/index.md).
