Introduction
This article is the .NET part of a series whose main article is How to build a language binding for a web API which you should read before this one to get general background information about implementing a language binding.
If you are interested in the Java implementation you can refer to the dedicated article: How to build a Java binding for a web API.
This article will give you more concrete information, mainly source code, if you need to implement a web API binding in .NET using the C# language.
When applicable each section of this article references a section of the main article to give you more context before diving into the .NET/C# implementation details.
The different sections are not directly related so you can read them in a random order, and particularly if you’re an experienced .NET developer some sections, e.g. about object-oriented programming, may bore you, so feel free to skip them completely.
The open source code of the project is available on CodePlex at CometDocs.Net.
Design
For an overview of the design of the Cometdocs binding see the “Design” section of the main article.
The Client class
For an overview of the design of the client class see “The client class” section of the main article.
First here is the C# interface that reifies the functional interface of a Cometdocs client:
public interface IClient
{
AuthenticationToken Authenticate(string username, string password, string key, int? validity = null);
FileInfo ConvertFile(AuthenticationToken token, FileInfo file,
ConversionType conversionType, TimeSpan? timeout = null);
void CreateAccount(string name, string email, string password);
FolderInfo CreateFolder(AuthenticationToken token, FolderInfo parent, String name);
void DeleteFile(AuthenticationToken token, FileInfo file, bool? deleteRevisions = null);
void DeleteFolder(AuthenticationToken token, FolderInfo folder);
File DownloadFile(AuthenticationToken token, FileInfo file);
Category[] GetCategories();
Conversion[] GetConversions(AuthenticationToken token, FileInfo file = null);
ConversionStatus GetConversionStatus(AuthenticationToken token, FileInfo file, ConversionType conversion);
ConversionType[] GetConversionTypes();
Folder GetFolder(AuthenticationToken token, FolderInfo folder = null, bool? recursive = null);
string[] GetMethods();
Notification[] GetNotifications(AuthenticationToken token);
FileInfo[] GetPublicFiles(AuthenticationToken token, Category category = null);
FileInfo[] GetSharedFiles(AuthenticationToken token);
void InvalidateToken(AuthenticationToken token);
void RefreshToken(AuthenticationToken token, int? validity = null);
void SendFile(AuthenticationToken token, FileInfo file,
string[] recipients, string sender = null, string message = null);
FileInfo UploadFile(AuthenticationToken token, Stream file, string name, long? folderId = null);
FileInfo UploadFile(AuthenticationToken token, Stream file, string name, FolderInfo folder);
FileInfo UploadFile(AuthenticationToken token, Stream file, FolderInfo folder);
FileInfo UploadFile(AuthenticationToken token, string file, long? folderId = null);
FileInfo UploadFile(AuthenticationToken token, File file, long? folderId = null);
FileInfo UploadFile(AuthenticationToken token, File file, FolderInfo folder);
FileInfo UploadFileFromUrl(AuthenticationToken token,
string url, string name = null, int? folderId = null);
FileInfo UploadFileFromUrl(AuthenticationToken token, string url, FolderInfo folder);
}
Well, nothing special to say, except that some of the methods benefits from C# 4.0 default parameters feature to map the optional parameters of the Cometdocs API.
This saves us from writing dumb overloads that would simply forward the call to another overload: e.g. there is one SendFile
method instead of three.
The client factory is as simple as it can be:
public class ClientFactory
{
public static IClient GetClient()
{
return new Client();
}
}
The source code of the Client
class as been split into multiple files, one for each entry point, possibly containing more than one overload, thanks to the C# partial class mechanism.
Moreover, in the csproj file, these files are marked with the MSBuild
DependentUpon
modifier which indicates association between files and which is nicely rendered inside Visual Studio as a tree:
Visual Studio Client view
The authentication token class
Here is the wrapper used to represent the authentication token:
public class AuthenticationToken
{
public string Value { get; private set; }
public AuthenticationToken(string value)
{
Value = value;
}
}
This is a minimal illustration of object-oriented abstraction.
The files and folders types
And finally here are the different classes that carry the basic info about a
file, FileInfo
:
public class FileInfo
{
public long ID { get; set; }
public string Name { get; set; }
public string Extension { get; set; }
public long Size { get; set; }
public bool HasConversions { get; set; }
public FileInfo(string nameWithExtension = null)
{
if (nameWithExtension != null)
{
string[] tokens = nameWithExtension.Split('.');
if (tokens.Length >= 2)
{
Name = tokens[0];
for (int i = 1; i < tokens.Length - 1; ++i)
{
Name += "." + tokens[i];
}
Extension = tokens.Last();
}
else
{
Name = tokens.Single();
}
}
}
public override int GetHashCode()
{
return ID.GetHashCode();
}
public override bool Equals(object obj)
{
FileInfo other = obj as FileInfo;
return other != null && other.ID == this.ID;
}
}
and about a folder, “FolderInfo“:
public class FolderInfo
{
public long ID { get; set; }
public string Name { get; set; }
}
For FileInfo
I’ve implemented
GetHashCode
and
Equals
to be able to compare two instances based on their internal Cometdocs ID.
File
is the class that represents a file with some content:
public class File : FileInfo
{
private byte[] content;
public byte[] Content
{
get
{
return content;
}
set
{
if (value == null)
throw new ArgumentNullException("value",
"Content cannot be null; for empty files provide " +
"an empty array or do not provide anything!");
this.content = value;
}
}
public File(string nameWithExtension = null)
: base(nameWithExtension)
{
content = new byte[0];
}
}
And Folder
a folder with some files and sub-folders:
public class Folder : FolderInfo
{
public Folder[] Folders { get; set; }
public FileInfo[] Files { get; set; }
}
The web/HTTP client
For general information about what is an HTTP client and how it fits into the building of a web API binding have a look at the dedicated section in the main article.
The .Net WebClient
Contrary to Java .NET has a built-in HTTP client, the
WebClient
class, which has a simple API, with just enough abstraction.
Technically the WebClient
is a wrapper around the low-level HttpWebRequest class.
So unless you have really specific customization needs you should prefer the WebClient over the
HttpWebRequest
, and personally I’ve always been able to work only with it.
As said in the main article the only type of request used by the Cometdocs API is HTTP/POST.
To emit POST request with the WebClient you use its UploadValues method providing it with a set of pairs, one pair for each parameter you want to transmit to the API.
As an example here is how you could invoke the
convertFile
entry-point:
NameValueCollection parameters = new NameValueCollection(3);
parameters.Add("token", "12345678-1234-1234-abcd-abcd12345678");
parameters.Add("id", 123);
parameters.Add("conversionType", "PDF2XLS");
byte[] result = webClient.UploadValues("https://www.cometdocs.com/api/v1/convertFile", parameters);
What the UploadValues
returns is the raw binary representation of the server response so you have to interpret it depending on the documentation.
In the Cometdocs API case each entry-point returns a JSON document, except
downloadFile
which when successful returns the content of the requested file.
Here is a full example, the authentication method, Authenticate
:
public AuthenticationToken Authenticate(string username, string password, string key, int? validity = null)
{
NameValueCollection @params = new NameValueCollection(4);
@params.Add("username", username);
@params.Add("password", password);
@params.Add("clientKey", key);
if (validity != null)
{
@params.Add("validity", validity.ToString());
}
byte[] result = webClient.UploadValues(APIRoot + "authenticate", @params);
string json = encoding.GetString(result);
AuthenticateResponse response = JsonConvert.DeserializeObject<AuthenticateResponse>(json);
CheckAndThrow(response);
return new AuthenticationToken(response.Token);
}
File upload
While most of the time the WebClient has a simple method for each of the scenario: executing a HTTP/GET request, posting data with a HTTP/POST request… it lacks support for the upload of files: you can’t simply give it a set of parameters and a bunch of bytes to upload a file.
You need to go down a layer and directly attack the HTTP layer, forging raw multipart HTTP requests, which is a not so trivial task.
Here is the C# implementation of the UploadFile
method:
public FileInfo UploadFile(AuthenticationToken token, Stream file, string name, long? folderId = null)
{
string boundary = Guid.NewGuid().ToString();
byte[] boundaryBytes = encoding.GetBytes(boundary);
byte[] dashDashBytes = encoding.GetBytes("--");
byte[] CRLFBytes = encoding.GetBytes("\r\n");
string tokenPart = "Content-Disposition: form-data; name=\"token\"\r\n" +
"\r\n" +
token.Value + "\r\n";
byte[] tokenPartBytes = encoding.GetBytes(tokenPart);
string folderPart = null;
byte[] folderPartBytes = null;
if (folderId != null)
{
folderPart = "Content-Disposition: form-data; name=\"folderId\"\r\n" +
"\r\n" +
folderId.ToString() + "\r\n";
folderPartBytes = encoding.GetBytes(folderPart);
}
string fileHeaderPart = "Content-Disposition: form-data; " +
"name=\"file\"; filename=\"" + name + "\"\r\n" +
"\r\n";
byte[] fileHeaderPartBytes = encoding.GetBytes(fileHeaderPart);
HttpWebRequest request = HttpWebRequest.Create(APIRoot + "uploadFile") as HttpWebRequest;
request.Method = "POST";
request.ContentType = "multipart/form-data; boundary=" + boundary;
using (Stream requestStream = request.GetRequestStream())
{
requestStream.Write(dashDashBytes, 0, dashDashBytes.Length);
requestStream.Write(boundaryBytes, 0, boundaryBytes.Length);
requestStream.Write(CRLFBytes, 0, CRLFBytes.Length);
requestStream.Write(tokenPartBytes, 0, tokenPartBytes.Length);
requestStream.Write(dashDashBytes, 0, dashDashBytes.Length);
requestStream.Write(boundaryBytes, 0, boundaryBytes.Length);
requestStream.Write(CRLFBytes, 0, CRLFBytes.Length);
if (folderId != null)
{
requestStream.Write(folderPartBytes, 0, folderPartBytes.Length);
requestStream.Write(dashDashBytes, 0, dashDashBytes.Length);
requestStream.Write(boundaryBytes, 0, boundaryBytes.Length);
requestStream.Write(CRLFBytes, 0, CRLFBytes.Length);
}
requestStream.Write(fileHeaderPartBytes, 0, fileHeaderPartBytes.Length);
file.CopyTo(requestStream);
requestStream.Write(CRLFBytes, 0, CRLFBytes.Length);
requestStream.Write(dashDashBytes, 0, dashDashBytes.Length);
requestStream.Write(boundaryBytes, 0, boundaryBytes.Length);
requestStream.Write(dashDashBytes, 0, dashDashBytes.Length);
}
HttpWebResponse httpResponse = request.GetResponse() as HttpWebResponse;
string json = null;
using (Stream responseStream = httpResponse.GetResponseStream())
{
using (StreamReader reader = new StreamReader(responseStream, encoding))
{
json = reader.ReadToEnd();
}
}
UploadFileResponse response = JsonConvert.DeserializeObject<UploadFileResponse>(json);
CheckAndThrow(response);
return response.File;
}
Note that the parts of the requests are separated using a randomly generated boundary based on Guids.
So yes raw HTTP requests forging is not that cool and fun.
JSON data binding
For a quick introduction to data binding and how it is applied to the Cometdocs binding see the “Data binding” section of the main article.
Json.NET
When it comes to JSON data binding in .Net, i.e. mapping back and forth between .Net objects and JSON strings, your choice is really simple: there is one simple, well designed, fast and with good extensibility points, library: Json.NET.
This is the kind of library you’d like to see more often: it just works; as an example out-of-the-box Json.NET manages integers representing boolean values (0 and 1).
Here is an illustration of its usage in our Cometdocs binding: when we authenticate to the Cometdocs API we get back a JSON response that looks like:
{"token":"b687500d-3cff-44b7-ad14-a241a7edabf1","status":0,"message":"OK"}
Json.NET maps this JSON representation to a .Net object:
AuthenticateResponse instance
whose class is AuthenticateResponse
:
internal class Response
{
public Status Status { get; set; }
public string Message { get; set; }
public string Error { get; set; }
}
class AuthenticateResponse : Response
{
public string Token { get; set; }
}
99% of the time Json.NET is a plug-and-play library, it manages all the standard stuff, like boolean values represented with integers, but there is no magic and when you have some specific format you have to customize the JSON data binding process by using one of the extensibility points provided by Json.NET.
Mapping conversion types
The Cometdocs API represents the files conversions in a compacted text format: e.g. to represent a conversion from PDF to XLS instead of using a composite object like:
{from: "PDF", to: "XLS"}
it uses a simple string:
"PDF2XLS"
Json.NET supports “converters” which are objects that fill the gap between two representations of an entity.
A converter class must extend the JsonConverter class and override the conversions methods: CanConvert to check if the converter can convert the given type,
ReadJson
to parse an object from JSON and
WriteJson
to serialize an object to JSON.
Here is the ConversionTypeJsonConverter
class that converts ConversionType
instances:
internal class ConversionTypeJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(ConversionType);
}
public override object ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer)
{
string s = reader.Value as string;
return ConversionType.FromString(s);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteComment(value.ToString());
}
}
The ToString
and FromString
methods being members
of the ConversionType
class itself:
public class ConversionType
{
...
public override string ToString()
{
return From + "2" + To;
}
public static ConversionType FromString(string s)
{
string[] tokens = s.Split('2');
return new ConversionType(tokens[0], tokens[1]);
}
}
We make Json.NET aware of this converter by passing an instance of it to the
DeserializeObject
method:
private static readonly JsonConverter ConversionTypeJsonConverter = new ConversionTypeJsonConverter();
...
GetConversionTypesResponse response =
JsonConvert.DeserializeObject<GetConversionTypesResponse>(json, ConversionTypeJsonConverter);
Security
For an overview of the security issues associated with the implementation of a web API binding have a look at the “Security” section in the main article.
Communication with the Cometdocs API is secured using SSL/TLS which implies to jump through some additional hoops to get things work, at least this is what I expected.
Consequently I was really surprised and troubled when I realized that without doing anything, naively trying to communicate with a bare WebClient, all was working fine.
Why being surprised when all is working? Because I implemented a binding for the Java platform and concerning SSL/TLS setup things were really not obvious for a security rookie like me and you can consult the “Security” section of the article dedicated to the Java implementation to get an idea of how complicated things can be.
Indeed for Java to accept communication with the Cometdocs API you need to export the Cometdocs certificate and import it into the certificates store of the Java runtime you’ll use to run your binding, and when you finally understand how SSL/TLS is designed it totally makes sense.
But for .Net things seemed to work in a different way and I’ve even doubted that they effectively worked and that the WebClient was correctly using a secured channel to interact with the API.
Hopefully this is not the case and .NET correctly uses SSL/TLS to secure the communication but it does this using some “shortcuts”: the .Net runtime does not have its own certificates store but instead it uses the store of the host system.
So if your system already trusts the Cometdocs certificate, which should be the case as they are emitted by a trusted certification authority, the .Net framework will automatically allow trusted communication with the API.
To consult the list of trusted authorities you can use the CertMgr tool.
You can run it by typing “certmgr.msc” in the “Run” dialog box (keyboard shortcut is Windows-R
):
Run certmgr.msc
And here is the
CertMgr main window:
CertMgr window
And there is more: it’s almost impossible to break this trust relationship because Windows tries its best to maintain some certification authorities, so deleting them has no effect unless you disable the Windows policy that makes it fight to maintain them.
It was an interesting experience because instead of fighting to make things work as with Java, I’ve fought to make them break!
Errors management
For general information on the way errors are managed see “Errors management” in the main article.
Here is the C# class that is the representation of a Cometdocs API error:
public class CometDocsException : Exception
{
public new string Message { get; private set; }
public Status Status { get; private set; }
public string Error { get; private set; }
public CometDocsException(string message, Status status, string error)
: base(string.Format("{0} ({1}) [{2}]", message, error, status))
{
Message = message;
Status = status;
Error = error;
}
}
And here is a specialization (currently the sole) for the “invalid token” error:
public class InvalidTokenException : CometDocsException
{
public InvalidTokenException(string message, Status status, string error)
: base(message, status, error)
{
}
}
Well this is basic OOP.
And finally the CheckAndThrow
method that is called right after receiving a response from the Cometdocs API:
private void CheckAndThrow(Response response)
{
if (response.Status != Status.OK)
{
if (response.Status == Status.InvalidToken)
{
throw new InvalidTokenException(response.Message, response.Status, response.Error);
}
throw new CometDocsException(response.Message, response.Status, response.Error);
}
}
Testing
For more general information about the testing of the bindings have a look at the “Testing” section in the main article.
Tools
For testing I’ve used the NUnit framework, the .Net binding of the universal JUnit series, which is the more standard in .NET.
To run tests easily from Visual Studio I’ve used TestDriven.Net a really cool and simple tool perfectly integrated in Visual Studio.
Note that TestDriven.Net does not work with Visual Studio Express editions due to obscure commercial reasons, I’ve found this article about the issue: Microsoft threatens its Most Valuable Professional.
For those not familiar with NUnit, here is how you can create a set of unit-tests:
- create a class that will hold all the unit-tests, in our case
ClientTests
,
and annotate it with the
TestFixture
attribute - create a method for each unit-test and annotate it with the “Test” attribute
As an example here is the unit-test for checking that we can retrieve all the Cometdocs categories:
[TestFixture]
public class ClientTests
{
...
[Test]
public void CanGetCategories()
{
Category[] categories = client.GetCategories();
Assert.GreaterOrEqual(categories.Length, 10);
Assert.That(categories.Any(cat => cat.Name == "Art"));
Assert.That(categories.Any(cat => cat.Name == "Business"));
Assert.That(!categories.Any(cat => cat.Name == "Unicorns"));
}
...
}
And here is what we get when all the tests are OK:
NUnit tests OK
(No Photoshopping I promise
)
Test fixture setup
In NUnit you define a test fixture setup by marking a method with the TestFixtureSetup attribute.
Before NUnit 2.5 the method had to be an instance method, from 2.5 you can also use static methods.
Here is the C# source code of the fixture setup:
[TestFixtureSetUp]
public void TestFixtureSetUp()
{
ReadCredentials();
client = ClientFactory.GetClient();
CanAuthenticate();
Folder root = client.GetFolder(authToken);
TestFolderInfo = root.Folders.SingleOrDefault(f => f.Name == TestFolderName);
if (TestFolderInfo == null)
{
throw new Exception(string.Format(
"Unable to find tests folder '{0}'!\nPlease create it first.", TestFolderName));
}
}
Reflection
In .Net the entry point to the reflection API is the Type class, which represents a type, be it a reference type (class) or a value type (struct).
Type exposes all the methods necessary to explore the properties of a type like its methods using the GetMethods method which returns an array of MethodInfo, a type whose instances represent a single method; the MethodInfo type itself has a Name property with the name of the method it represents.
There is one subtlety: we don’t want to get all the methods of the Client
type because we’re only interested in the public instance methods, not the implementation details which are private like the
CheckAndThrow
method; moreover we only need the methods defined by the
Client
class itself, not those inherited from the Object
base class like ToString
.
Hence we apply the “BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly
” filter.
Here is the complete code of the unit-test:
[Test]
public void CanGetMethods()
{
string[] methods = client.GetMethods();
string[] methodsName = methods.Select(m => m.Split('(')[0]).OrderBy(m => m).ToArray();
string[] clientMethods = typeof(Client).GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
.Select(m => m.Name)
.Distinct()
.OrderBy(m => m)
.ToArray();
Assert.That(clientMethods.SequenceEqual(methodsName, StringComparer.InvariantCultureIgnoreCase));
}
The list of implemented methods is obtained using some LINQ magic (Distinct
is there for avoiding duplicates for each overload of a method, typically
UploadFile
) and compared to the official list of methods with the
SequenceEqual
method.
Result
To illustrate how the language binding can be used to simply store and retrieve a file here is the NUnit
unit-test used to test the UploadFile
and DownloadFile
methods:
[Test]
public void CanUploadAndDownloadAFile()
{
AssertClean();
string content = @"The peanut, or groundnut (Arachis hypogaea), is a species " +
"in the legume or ""bean"" family (Fabaceae). The peanut " +
"was probably first domesticated and cultivated in the valleys of Paraguay.[1] " +
"It is an annual herbaceous plant growing 30 to 50 cm (1.0 to 1.6 ft) tall. " +
"The leaves are opposite, pinnate with four leaflets (two opposite pairs; no " +
"terminal leaflet), each leaflet is 1 to 7 cm (⅜ to 2¾ in) long and 1 " +
"to 3 cm (⅜ to 1 inch) broad." +
"The flowers are a typical peaflower in shape, 2 to 4 cm (0.8 to 1.6 in) (¾ to 1½ in) " +
"across, yellow with reddish veining. Hypogaea means ""under the " +
"earth""; after pollination, the flower stalk elongates causing it " +
"to bend until the ovary touches the ground. Continued stalk growth then pushes " +
"the ovary underground where the mature fruit develops into a legume pod, " +
"the peanut – a classical example of geocarpy. Pods are 3 to 7 cm (1.2 to 2.8 in) " +
"long, containing 1 to 4 seeds.[2]" +
" Peanuts are known by many other local names such as earthnuts, ground nuts, " +
"goober peas, monkey nuts, pygmy nuts and pig nuts.[3] Despite its name " +
"and appearance, the peanut is not a nut, but rather a legume.";
File inputFile = new File("Peanuts.txt")
{
Content = encoding.GetBytes(content)
};
FileInfo info = client.UploadFile(authToken, inputFile, TestFolderInfo);
Assert.AreEqual(inputFile.Content.Length, info.Size);
File outputFile = client.DownloadFile(authToken, info);
Assert.That(inputFile.Content.SequenceEqual(outputFile.Content));
string outputContent = encoding.GetString(outputFile.Content);
Assert.AreEqual(content, outputContent);
}
If you drop all the assertions you can see that this non trivial task is made relatively straightforward.
I let you judge whether the result is worth the trouble…
Conclusion
As you see combining the WebClient, Json.NET, NUnit, reflection and some background on web development is enough to develop a .Net binding for virtually any web API.
If you want more precision on one of the sections of this article or on a part of the C# source code not detailed in this article do not hesitate to ask by letting a comment, I’ll update the article accordingly.
If you have already developed a .NET binding or expect to build one please share your experience and suggestions, I’d like to hear from you.