- Introduction
- Installation
- Highlighting the Key Differences
- Diving into Authentication
- Configuration
Welcome to the Box DotNet SDK
, the pinnacle of Box's SDK evolution tailored for developers eager to integrate with the Box API using C# with .NET. This next-generation toolkit is meticulously crafted with contemporary development practices, ensuring an unparalleled, seamless experience.
While the Box Windows V2 SDK
served its purpose well, the Box DotNet SDK
elevates the experience to new heights. One of its standout features is its auto-generation directly from the Open API Specification. This guarantees that developers are always equipped with the latest Box API features, eliminating any lag or discrepancies.
This guide is your compass, offering insights and directions for a smooth migration from the legacy Box Windows V2 SDK
to the state-of-the-art Box DotNet SDK
. As we journey through, we'll spotlight the key differences, enhanced functionalities, and the myriad benefits that await.
For those who wish to delve deeper into the intricacies and advantages of the new SDK, the official README is a treasure trove of information.
Embarking on your journey with the Box DotNet SDK
is straightforward. Here's how you can set it up:
Install-Package Box.Sdk.Gen
Alternatively, you can find this package and it's latest version on nuget and manually add it to the .csproj
file as a reference:
<ItemGroup>
<PackageReference Include="Box.Sdk.Gen" Version="X.Y.Z" />
</ItemGroup>
For those who are hesitant to make the full leap, fear not. The Box DotNet SDK
can coexist peacefully alongside the legacy Box Windows V2 SDK
within the same project. This coexistence offers a gentle migration path, allowing developers to transition at their own pace. However, for an optimal experience, a complete transition to the new SDK is recommended in due course.
The SDK now supports nullable reference types whenever possible. This allows to perform null state analyis and detect many problems even before the code is run.
To enable support for nullable references, enable it in your project by adding the following line to your .csproj
<Nullable>enable</Nullable>
When this option is enabled, a null state analysis will be performed and more information about the object will be present. Let's examine the following scenario:
var user = await client.Users.GetUserMeAsync();
// a warning is displayed: "Enterprise" may be null here. You should add a check for the null value to prevent NRE
Console.WriteLine(user.Enterprise.Id);
Thanks to the null state analysis, it is known that Enterprise
part of the user
is not always present in the object and can sometimes be set to null
.
In this case, we need to perform an additional check to avoid the Null Reference Exception.
var user = await client.Users.GetUserMeAsync();
// no warning now
Console.WriteLine(user.Enterprise?.Id);
Without nullable reference types, it would not be possible to determine which reference types could be set to null, and we would have to do it with each of them. Otherwise we would risk encountering Null Reference Exception.
In the Legacy SDK Box Windows V2 SDK
all objects exposed setters and an empty constructor. It was possible to create an object without setting some of the fields required by the API. This would result in the API error (usually Bad Request) saying that the request was missing some requeired fields. Some of the functions validated if the request fields are set properly, but even then, the lack of required fields could not be detected when call to such function was executed.
The modern Box DotNet SDK
is designed to minimize problems with object creation and "force" the creation of objects in the correct state by utilizng constructors capabilities. Let's look at the following example
// It is not possible to create user without a name so it's a required argument of a constructor
var requestBody = new CreateUserRequestBodyArg(name: "my-test-account");
var response = await client.Users.CreateUserAsync(requestBody);
If a field is required by the API, it must be passed to the constructor. In other words, it's not possible to create a CreateUserRequestBodyArg
without a name supplied as an argument to the constructor. Optional parameters can be passed using object initializer syntax.
// IsPlatformAccessOnly is not required by the API so it's passed in object initializer
var requestBody = new CreateUserRequestBodyArg(name: "my-test-account") { IsPlatformAccessOnly = true };
var response = await client.Users.CreateUserAsync(requestBody);
This allows for smooth, one-line and immutable object creation and it's easy to see which fields must be filled in and which are optional. Combined with the previous point about nullable reference types, it allows you to take full advantage of C#'s typing arsenal to prevent errors in your code even before the code is executed.
The new SDK also introduces native support for CancellationToken for every call made to the Box API. This means that every API call can be easily cancelled and the task can be shutdown gracefully. Every API method accepts cancellation token usually as the last, optional parameter. You can use CancellationToken
in response to your system events or as a timeout mechanism by utilizing the CancelAfter
method.
var requestBody = new CreateUserRequestBodyArg(name: "my-test-account");
var cancellationTokenSource = new CancellationTokenSource();
var cancellationToken = cancellationTokenSource.Token;
// cancel request after 10s
cancellationTokenSource.CancelAfter(10000);
try
{
var response = await client.Users.CreateUserAsync(requestBody, cancellationToken: cancellationToken);
}
catch (TaskCanceledException ex)
{
//handle exception
}
When task is cancelled, native .NET TaskCanceledException
is thrown.
Legacy Box Windows V2 SDK
was distributed as a two separate packages namely Box.V2
and Box.V2.Core
. Adding the fact that some supported frameworks overlapped in both packages caused additional confusion for the customers, who had to decide which one was best for their projects. Additionally, the maintenance of both packages introduced developer overhead, extending the time of development for some features.
In our new generation .NET SDK Box DotNet SDK
, we have decided to distribute one package that is designed for multiple platforms, namely Box.Sdk.Gen
. The determination of the best possible package match to the project in which this package is used will be delegated to the .NET platform itself. Each target platform with significiant differences will have a separate code base so that one target does not limit another. Most of the SDK is automatically generated now, so this will not cause a significant development overhead. For now, only .NET (formerly .NET Core) is supported, but .NET Framework support is planned for the future.
Legacy Box Windows V2 SDK
contained multiple namespaces which greatly reduced the discoverability of new features. The namespaces were also grouped based on their physical location rather than purpose they serve. Developers had to navigate through numerous namespaces to find the classes and methods they needed, which could be overwhelming and confusing, especially for newcomers.
To simplify it, our the new generation .NET SDK Box DotNet SDK
, provides only 4 main namespaces:
- Box.Sdk.Gen - mostly contains base classes used by the SDK (e.g.
OneOf
,StringEnum
). - Box.Sdk.Gen.Managers - contains managers that expose endpoints as functions. It also contains classes used to create requests, such as typed request body or query parameters.
- Box.Sdk.Gen.Schemas - contains data models described by the API spec.
- Box.Sdk.Gen.Internal - utility functions and classes used by the SDK. Should be used only if needed too.
Legacy Box Windows V2 SDK
used C# enumeration types when API exposed model with limited number of possible value for String
.
public enum BoxRetentionType
{
modifiable,
non_modifiable
}
//response fails during deserialization if BoxRetention type returned from the API is not defined on enum (e.g. partially_modifiable)
var retentionType = await (client.RetentionPoliciesManager.GetRetentionPolicyAsync('id')).Type;
Sometimes this led to a situation where a newly added enum variant was present in the specification, but not in the SDK, and an exception could be thrown when deserializing the response.
To overcome these issues the new generation .NET SDK Box DotNet SDK
wraps enum types in safe, custom StringEnum
structure.
// Type can be null, but it no longer fails,
var retentionType = (await client.RetentionPolicies.GetRetentionPolicyByIdAsync(retentionPolicyId: retentionPolicy.Id)).Type;
if (retentionType.Value.HasValue)
{
//it's a nullable enum so we need to access the underlying value
RetentionPolicyRetentionTypeField enumVal = retentionType.Value.Value;
}
else
{
//if the enum value was null it means that this enum is unknown to the SDK at the moment, but we still can access the original value that came from the api through .StringValue prop
string enumVaueAsString = retentionType.StringValue;
}
This allows safe access to the underlying enumeration type. If the enumeration value is not yet known to the SDK, you can still access the original value from the API, by accessing .StringValue
field.
Thanks to implicit conversion operators converting enum types to StringEnum
is simple and usually there's no need to do it explicitly.
var requestBody = new CreateRetentionPolicyRequestBody("myPolicy", CreateRetentionPolicyRequestBodyPolicyTypeField.Finite, CreateRetentionPolicyRequestBodyDispositionActionField.PermanentlyDelete);
RetentionPolicy retentionPolicy = await client.RetentionPolicies.CreateRetentionPolicyAsync(requestBody);
Usually there's no need to wrap enum type in the StringEnum
, but if such need arises, you can use existing constructor of the StringEnum
.
RetentionPolicyRetentionTypeField retentionPolicyType = RetentionPolicyRetentionTypeField.Modifiable;
StringEnum<RetentionPolicyRetentionTypeField> retentionPolicyTypeWrapped = new StringEnum<RetentionPolicyRetentionTypeField>(retentionPolicyType);
Box APIs can return and accept objects with different schemas at certain endpoints. This means that in C#, they must be represented by different classes to take full advantage of the type system.
Legacy Box Windows V2 SDK
used approach with common ancestor, by returning base object (in this case BoxEntity
) that needed to be explicitly casted to the desired type.
var enterpriseEvents = await boxClient.EventsManager.EnterpriseEventsAsync();
foreach (BoxEnterpriseEvent entry in enterpriseEvents.Entries)
{
// we need to check if Source is of type BoxFileEventSource and cast it
if (entry.Source is BoxFileEventSource fileEvent)
{
Console.WriteLine(fileEvent.Parent.Id);
}
}
This solution is error-prone and relies on the knowledge of the internals of the SDK.
New generation .NET SDK Box DotNet SDK
provides a custom structure to represent union types (or sum types) called OneOf<T1, T2, ...>
. The different variant are represented as different fields on this structure. This allows new SDK users to use it like any other model. In addition, the SDK does not directly expose the OneOf
structure in the models, but inherits from it, so you do not need to know how it works in detail when working with models.
var enterpriseEvents = await client.Events.GetEventsAsync(queryParams: new GetEventsQueryParams() { StreamType = GetEventsQueryParamsStreamTypeField.AdminLogs });
foreach (var entry in enterpriseEvents.Entries)
{
//if .File is not null it means it was returned from the API
if (entry.Source.File != null)
{
Console.WriteLine(entry.Source.File.Parent.Id);
}
}
Authentication is a crucial aspect of any SDK. Let's delve into the authentication methods supported by both SDKs and understand the enhancements in the new version:
The Developer Token remains a straightforward method for authentication:
Legacy (Box Windows V2 SDK
):
using Box.Sdk.V2.Auth;
using Box.Sdk.V2.Config;
using Box.Sdk.V2;
var config = new BoxConfigBuilder("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET").Build();
var session = new OAuthSession("YOUR_DEVELOPER_TOKEN", "N/A", 3600, "bearer");
var client = new BoxClient(config, session);
Modern (Box DotNet SDK
):
The new SDK offers a more streamlined approach:
using Box.Sdk.Gen;
var auth = new BoxDeveloperTokenAuth(token: "DEVELOPER_TOKEN_GOES_HERE");
var client = new BoxClient(auth: auth);
JSON Web Tokens (JWT) offer a secure method of authentication. Here's how the process has evolved:
Legacy (Box Windows V2 SDK
):
using Box.Sdk.V2;
using Box.Sdk.V2.Config;
using Box.Sdk.V2.JWTAuth;
using var file = File.Open("/path/to/config.json", FileMode.Open, FileAccess.Read, FileShare.None);
var config = BoxConfigBuilder.CreateFromJsonFile(file).Build();
var boxJWT = new BoxJWTAuth(config);
var adminToken = await boxJWT.AdminTokenAsync();
BoxClient client = boxJWT.AdminClient(adminToken);
Modern (Box DotNet SDK
):
The new SDK provides a more organized approach:
using Box.Sdk.Gen;
using var file = File.Open("/path/to/config.json", FileMode.Open, FileAccess.Read, FileShare.None);
var jwtConfig = JwtConfig.FromConfigFile(file);
var auth = new BoxJwtAuth(config: jwtConfig);
var client = new BoxClient(auth: auth);
For those who prefer manual configurations, both SDKs offer flexibility:
Legacy (Box Windows V2 SDK
):
using Box.Sdk.V2;
using Box.Sdk.V2.Config;
using Box.Sdk.V2.JWTAuth;
var boxConfig = new BoxConfigBuilder("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET", "YOUR_ENTERPRISE_ID", "YOUR_PRIVATE_KEY", "YOUR_PRIVATE_KEY_PASSPHRASE", "YOUR_KEY_ID").Build();
var boxJWT = new BoxJWTAuth(boxConfig);
var adminToken = await boxJWT.AdminTokenAsync();
BoxClient adminClient = boxJWT.AdminClient(adminToken);
Modern (Box DotNet SDK
):
The new SDK introduces a more structured approach:
using Box.Sdk.Gen;
var jwtConfig = new JwtConfig(clientId: "YOUR_CLIENT_ID", clientSecret: "YOUR_CLIENT_SECRET", enterpriseId: "YOUR_ENTERPRISE_ID",
privateKey: "YOUR_PRIVATE_KEY", privateKeyPassphrase: "YOUR_PRIVATE_KEY_PASSPHRASE", publicKeyId: "YOUR_KEY_ID");
var auth = new BoxJwtAuth(config: jwtConfig);
var client = new BoxClient(auth: auth);
Authenticating as a user has been made even more straightforward:
Legacy (Box Windows V2 SDK
):
using Box.Sdk.V2;
using Box.Sdk.V2.Config;
using Box.Sdk.V2.JWTAuth;
var userToken = await boxJWT.UserTokenAsync("USER_ID");
var userClient = boxJWT.UserClient(userToken, "YOUR_USER_ID");
Modern (Box DotNet SDK
):
The new SDK makes this method more organised:
using Box.Sdk.Gen;
await auth.AsUserAsync("USER_ID");
var userClient = new BoxClient(auth: auth);
The Client Credentials Grant method is a popular choice for many developers. Let's see how it's been enhanced:
Legacy (Box Windows V2 SDK
):
using Box.Sdk.V2.CCGAuth;
using Box.Sdk.V2.Config;
var boxConfig = new BoxConfigBuilder(clientId: "YOUR_CLIENT_ID", clientSecret: "YOUR_CLIENT_SECRET")
.SetEnterpriseId("YOUR_ENTERPRISE_ID")
.Build();
var boxCCG = new BoxCCGAuth(boxConfig);
var adminToken = await boxCCG.AdminTokenAsync();
var adminClient = boxCCG.AdminClient(adminToken: adminToken);
Modern (Box DotNet SDK
):
The new SDK offers a more organized structure:
using Box.Sdk.Gen;
var ccgConfig = new CcgConfig(clientId: "YOUR_CLIENT_ID", clientSecret: "YOUR_CLIENT_SECRET", enterpriseId: "YOUR_ENTERPRISE_ID");
var auth = new BoxCcgAuth(config: ccgConfig);
var client = new BoxClient(auth: auth);
Legacy (Box Windows V2 SDK
):
using Box.Sdk.V2.CCGAuth;
using Box.Sdk.V2.Config;
var boxConfig = new BoxConfigBuilder(clientId: "YOUR_CLIENT_ID", clientSecret: "YOUR_CLIENT_SECRET")
.Build();
var boxCCG = new BoxCCGAuth(boxConfig);
var userToken = await boxCCG.UserTokenAsync("YOUR_USER_ID");
var userClient = boxCCG.UserClient(userToken: userToken, userId: "YOUR_USER_ID");
Modern (Box DotNet SDK
):
The new SDK streamlines the process:
using Box.Sdk.Gen;
var ccgConfig = new CcgConfig(clientId: "YOUR_CLIENT_ID", clientSecret: "YOUR_CLIENT_SECRET", userId: "YOUR_USER_ID");
var auth = new BoxCcgAuth(config: ccgConfig);
var userClient = new BoxClient(auth: auth);
Transitioning between account types is now more intuitive:
Modern (Box DotNet SDK
):
using Box.Sdk.Gen;
await auth.AsEnterpriseAsync("ENTERPRISE_ID");
OAuth 2.0 remains a robust authentication method. Let's explore the improvements:
Legacy (Box Windows V2 SDK
):
using Box.Sdk.V2;
using Box.Sdk.V2.Config;
var config = new BoxConfigBuilder("CLIENT_ID", "CLIENT_SECRET", new System.Uri("YOUR_REDIRECT_URL")).Build();
var client = new BoxClient(config);
// the URL to redirect the user to
var authorizeUrl = "https://account.box.com/api/oauth2/authorize?client_id=[CLIENT_ID]&response_type=code";
Modern (Box DotNet SDK
):
The new SDK provides more flexibility:
using Box.Sdk.Gen;
var config = new OAuthConfig(clientId: "YOUR_CLIENT_ID", clientSecret: "YOUR_CLIENT_SECRET");
var auth = new BoxOAuth(config: config);
// the URL to redirect the user to
var authorizeUrl = auth.GetAuthorizeUrl(new GetAuthorizeUrlOptions() { RedirectUri = "YOUR_REDIRECT_URL" });
Legacy (Box Windows V2 SDK
):
using Box.Sdk.V2;
var session = await client.Auth.AuthenticateAsync("[CODE]");
var client = new BoxClient(config, session);
Modern (Box DotNet SDK
):
The new SDK simplifies the process:
using Box.Sdk.Gen;
await auth.GetTokensAuthorizationCodeGrantAsync("code");
Token management is crucial for maintaining secure sessions. The new SDK offers enhanced flexibility:
Legacy (Box Windows V2 SDK
):
// there are no good ways to implement token storage in the legacy sdk
// best that can be done is to respond to the authorization events
public class CustomTokenStorage
{
// retrieves token
string RetrieveToken();
// stores token
void SaveToken(string accessToken);
}
var client = new BoxClient(config, session);
client.Auth.SessionAuthenticated += delegate (object o, SessionAuthenticatedEventArgs e)
{
string newAccessToken = e.Session.AccessToken;
tokenStorage.SaveToken(newAccessToken);
};
Modern (Box DotNet SDK
):
The new SDK allows developers to define custom classes for token storage:
using Box.Sdk.Gen;
using Box.Sdk.Gen.Schemas;
using Task = System.Threading.Tasks.Task;
class CustomTokenStorage : ITokenStorage
{
// Clear token
public Task ClearAsync()
{
throw new NotImplementedException();
}
// Retrieve token
public Task<AccessToken?> GetAsync()
{
throw new NotImplementedException();
}
// Store token
public Task StoreAsync(AccessToken token)
{
throw new NotImplementedException();
}
}
var tokenStorage = new CustomTokenStorage();
var config = new OAuthConfig(clientId: "YOUR_CLIENT_ID", clientSecret: "YOUR_CLIENT_SECRET", tokenStorage: tokenStorage);
The As-User header is used by enterprise admins to make API calls on behalf of their enterprise's users.
This requires the API request to pass an As-User: USER-ID
header. The following examples assume that the client has
been instantiated with an access token with appropriate privileges to make As-User calls.
In old SDK you could call client constructor with asUser
parameter to create a new client to impersonate the provided user.
Legacy (Box Windows V2 SDK
):
using Box.V2.Config;
var config = new BoxConfigBuilder("clientId", "clientSecret").Build();
var client = new BoxClient(config, asUser: "userId");
Modern (Box DotNet SDK
):
In the new SDK the method was renamed to WithAsUserHeader(string userId)
and returns a new instance of BoxClient
class with the As-User header appended to all API calls made by the client.
The method accepts only user id as a parameter.
using Box.Sdk.Gen;
var client = new BoxClient(new BoxCcgAuth(new CcgConfig("clientId", "clientSecret")));
var asUserClient = client.WithAsUserHeader("userId");
Additionally BoxClient
offers a WithExtraHeaders(Dictionary<string, string>? extraHeaders = default)
method, which allows you to specify the custom set of headers, which will be included in every API call made by client.
Calling the client.WithExtraHeaders(extraHeaders)
method creates a new client, leaving the original client unmodified.
using Box.Sdk.Gen;
var client = new BoxClient(new BoxCcgAuth(new CcgConfig("clientId", "clientSecret")));
var clientWithHeaders = client.WithExtraHeaders(new Dictionary<string, string>() { { "customHeaders", "customHeaderValue" } });
Legacy (Box Windows V2 SDK
):
In old SDK you could specify the custom base URLs, which will be used for API calls made by setting
the new values by calling function of BoxConfigBuilder
.
using Box.V2.Config;
var config = new BoxConfigBuilder("clientId", "clientSecret")
.SetBoxApiHostUri(new Uri("https://new-base-url.com"))
.SetBoxUploadApiUri(new Uri("https://my-company-upload-url.com"))
.Build();
Modern (Box DotNet SDK
):
In the new SDK this functionality has been implemented as part of the BoxClient
class.
By calling the client.WithCustomBaseUrls()
method, you can specify the custom base URLs that will be used for API
calls made by client. Following the immutability pattern, this call creates a new client, leaving the original client unmodified.
using Box.Sdk.Gen;
var client = new BoxClient(new BoxCcgAuth(new CcgConfig("clientId", "clientSecret")));
var clientWithCustomUrls = client.WithCustomBaseUrls(new BaseUrls("https://new-base-url.com",
"https://my-company-upload-url.com",
"https://my-company.com/oauth2"));