Client library

From Sense/Net Wiki
Jump to: navigation, search
  •  
  •  
  •  
  •  
  • 100%
  • 6.5.3
  • Enterprise
  • Community
  • Planned

Overview

Sense/Net ECMS is an enterprise application with an exceptionally rich OData REST API. For developers this means they can access the Content Repository from the client side using HTTP requests. This is even possible from a desktop application or a command line tool, although it is not very convenient to work with HTTP requests there. This article is about a client library written in C# that makes it super-easy to make the same operations without having to send HTTP requests and parse the results.

Goal

The OData REST API is built around simple principles, but when it comes to writing a robust and fast client code (e.g. a custom importer tool) there are many details that we have to take care of and many things that can be made a lot easier.

  • C# API instead of HTTP requests: from loading content items and collections to performing actions, posting parameters - it is a lot more convenient to achieve these things using a C# API instead of constructing HTTP requests all the time.
    • loading and querying content
    • accessing and modifying fields
    • uploading files
    • editing permissions and group memberships
    • etc...
  • portal URL and authentication: use a one-time initialization technique for the environment-related things and let developers concentrate on the business use case.
  • exception handling: extracting as many information from the server's response as possible.

We are constantly adding new features to the client library, please refer to the source code and the Intellisense in Visual Studio for all the possibilities.

Releases

sn-client-dotnet.png SenseNet.Client.png

Initialization

To use the client API you have to reference the SenseNet.Client.dll library. It is best if you simply install the NuGet package, or (in case you want to make some modifications) download the source code from GitHub, see the Releases section above.

Before you can perform content operations, you will have to initialize the component with the following server context information:

  • server URLs (mandatory): one or more URLs that the client will use to access the portal. Defining multiple URLs here makes sense if you want to send requests to multiple different web servers - or even different repositories (e.g. in case of a migration).
  • username and password (optional): if you provide these with the server context, the component will try to access the portal using basic authentication. If no credentials were provided, Windows authentication is used.
ClientContext.Initialize(new[] { new ServerContext { 
	Url = SiteUrl, 
	Username = Username, 
	Password = Password 
}});

All operations from now on will use this context information. For example you will not have to provide the server URL every time you want to load or save a content: we will use the first context you provided. Of course you can provide a specific (different) context on every operation too if it is necessary.

Content API

All the operations described here use the server context defined during initialization.

Loading content

In most cases you will start working with a Content:

var content = await Content.LoadAsync(id);

Note that the SenseNet.Client library exposes an async API. This makes sure that you can write an efficient client application that does not consume too much resources.

The result object has the client type Content. This is similar to our server side main Content class, but it is more lightweight and is designed around data operations.

When loading one or more content, please filter the data downloaded from the server when possible to make requests faster and lessen the network load. The methods in this client API let you define select and expand parameters that will be transformed to the corresponding OData query options.

Creating content

Creating a Content object is a local in-memory operation, so it is not async, only the Save operation is executed asynchronously.

var content = Content.CreateNew(parentPath, contentType, name);
await content.SaveAsync();

After saving a new content, the new Path and Id properties are populated from the response. Any other field info returned by the response is also accessible.

Modifying content

You can set any field of a content using the indexer technique. As the client side has no knowledge about the dynamic content types in Sense/Net, even non-existing fields can be set here without throwing an exception.

content["Description"] = "new description";
await content.SaveAsync();

The code above sends a POST or PATCH request with all the fields you set previously on that content. The verb depends on whether the content was an existing one or not.

If you have an id or path of an existing content, you do not have to load it before modifying it. You can simply create an in-memory object to represent that content:

var content = Content.Create(id);
content["BirthDate"] = DateTime.UtcNow;
await content.SaveAsync();

Accessing fields

Accessing a field looks like this:

var date = Convert.ToDateTime(content["BirthDate"]);

The conversion is needed because the Content class is actually a wrapper around a JSON object returned by the server. All special type conversions need to be done by the caller.

You can work with a content object as a dynamic object too. This makes working with our dynamic content types and fields a lot easier and results in cleaner code:

dynamic content = await Content.LoadAsync(id);
DateTime date = content.BirthDate;
int newIndex = content.Index + 1;

Collections

You can load multiple content with one statement.

// load child content in a folder or library
var list = await Content.LoadCollectionAsync(path);
 
// query content from anywhere
var results = await Content.QueryAsync(queryText);

For more advanced scenarios please visit the Examples section below.

Reference fields

// load referenced content
var list = await Content.LoadReferencesAsync(id, fieldName);
foreach(var content in list)
{
	//...
}

For more advanced scenarios please visit the Examples section below.

Upload

One of the most common operations is to upload a file to the Content Repository. With the client API this is a one-line operation:

await Content.UploadAsync(parentId, fileName, stream);

If you are familiar with the Blob provider technique, you may write binaries directly into the blob storage.

Download

If you want to download a file through the portal, you can either create an http request manually from the url that you can find in the content json's binary field, or you can use the built-in GetStreamRequest method as shown below.

var req = RESTCaller.GetStreamRequest(fileId);
 
try
{
    using (var response = await req.GetResponseAsync())
    {
        using (var stream = response.GetResponseStream())
        {
            // process downloaded stream...
        }
    }
}
catch (WebException webex)
{
    throw await RESTCaller.GetClientExceptionAsync(webex, req.RequestUri.ToString());
}

If you are familiar with the Blob provider technique, you may read binary streams directly from the blob storage.

Content operations

When you have a content object in your hand, there are a couple of built-in operations you can perform on it. These are the most widely used methods we implemented, and we are constantly adding new features to this list. You can also call any server action dynamically as seen below.

await content.CheckOutAsync();
await content.CheckInAsync();
await content.CopyToAsync(targetPath);
await content.MoveToAsync(targetPath);
await content.DeleteAsync();
await content.HasPermissionAsync(permissionArray);

Dynamic operations

As the Content class is a dynamic class that also supports dynamic method calls, you can call SenseNet OData actions without having to define the methods locally.

// note the 'dynamic' keyword here
dynamic folder = await Content.LoadAsync(folderPath);
 
// This 'method' does not exist locally. It will be resolved to an OData request 
// and will return a task of type dynamic that will contain the result.
Task<dynamic> task = folder.GetPermissionInfo(new {identity = AdminPath});
var result = await task;

Low-level API

In case the Content class and the high-level API is does not suit your needs, we offer an API that offers more customization options while still hiding HTTP request handling. For example you can call any OData operation available in Sense/Net using this API:

var result = await RESTCaller.GetResponseStringAsync(contentId, actionName, HttpMethod.Post, body);
var result2 = await RESTCaller.GetResponseJsonAsync(requestData, server);

This requires you to construct and serialize the post body for example, but you still do not have to bother with URLs and authentication.

Error handling

All operations described above may throw a ClientException. This is a custom exception type that parses and exposes the error data returned by the server.

  • ErrorData: error code and type and other server error details
  • StatusCode: the HTTP status code of the error
  • Response: original response text


Examples

Initializing and querying

static void Main(string[] args)
{
	ClientContext.Initialize(new[] { new ServerContext 
	{ 
		Url = "http://localhost", 
		Username = "admin", 
		Password = "admin" 
	}});
	ClientContext.Current.ChunkSizeInBytes = 419430;
 
	var tasks = await Content.QueryAsync("+TypeIs:Task",
		select: new[] {"Id", "Name", "Path", "DisplayName", "Description"},
		settings: new QuerySettings
		{
			EnableAutofilters = FilterStatus.Disabled,
			Top = 20
		});
}

Reference field

Loading a content for a reference field (with dynamic objects):

dynamic adminGroup = await Content.LoadAsync(new ODataRequest
{
    Path = administratorsGroupPath,
    Expand = new[] { "Members" },
    Select = new[] { "Id", "Name", "Members/Id", "Members/Path", "Members/Type", "Members/CreationDate" },
    SiteUrl = ServerContext.GetUrl(null)
});
 
IEnumerable<dynamic> members = adminGroup.Members;
 
// Use items as dynamic, without converting them to Content
// if you do not need the Content class' functionality.
dynamic admin = members.FirstOrDefault();
 
int id = admin.Id;
string path = admin.Path;
DateTime cd = admin.CreationDate;

Loading a content for a reference field (with Content items):

dynamic adminGroup = await Content.LoadAsync(new ODataRequest
{
    Path = administratorsGroupPath,
    Expand = new[] { "Members" },
    Select = new[] { "Id", "Name", "Members/Id", "Members/Name", "Members/Path", "Members/Type", "Members/CreationDate", "Members/Index" },
    SiteUrl = ServerContext.GetUrl(null)
});
 
// note the conversion method at the end
var members = ((IEnumerable<dynamic>)adminGroup.Members).ToContentEnumerable();
 
foreach (dynamic member in members)
{
    int newIndex = member.Index + 1;
    member.Index = newIndex;
 
    // use the client Content API, this was the purpose of the ToContentEnumerable extension method
    await member.SaveAsync();
}

Setting a reference field:

var visitorUser = await Content.LoadAsync("/Root/IMS/BuiltIn/Portal/Visitor");
var adminUser = await Content.LoadAsync("/Root/IMS/BuiltIn/Portal/Admin");
 
visitorUser["Manager"] = new[] { adminUser.Id };
 
await visitorUser.SaveAsync();

Strongly typed field

Accessing a strongly typed field (datetime):

dynamic content = await Content.LoadAsync(id);
DateTime date3 = content.CreationDate;

Related links

References

No external references for this article.