Contents
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 by 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:
The authentication token class
Here is the wrapper used to represent the authentication token:
/// <summary> /// Wrapper for the CometDocs raw authentication token (a simple string) /// </summary> public class AuthenticationToken { /// <summary> /// The raw string value. /// </summary> public string Value { get; private set; } /// <summary> /// Creates a new AuthenticationToken from the CometDocs raw token. /// </summary> /// <param name="value">The CometDocs raw token</param> 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“:
/// <summary> /// A structure that wraps the identification information of a file. /// </summary> public class FileInfo { /// <summary> /// The file CometDocs ID. /// </summary> public long ID { get; set; } /// <summary> /// The file name without extension: e.g. for "Test.pdf" would be "Test". /// </summary> public string Name { get; set; } /// <summary> /// The file extension: e.g. for "Test.pdf" would be "pdf". /// </summary> public string Extension { get; set; } /// <summary> /// The file size in bytes. /// </summary> public long Size { get; set; } /// <summary> /// True if the file has some conversions, e.g. if "Test.pdf" has already been converted to "Test.xls". /// </summary> public bool HasConversions { get; set; } /// <summary> /// Create a new FileInfo instance. /// If the provided name is "Test.pdf" the resulting File instance "Name" will be "Test" and the "Extension" "pdf". /// </summary> /// <param name="nameWithExtension">The full file name: e.g. "Test.pdf".</param> 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(); } } } /// <summary> /// Get the object hashcode: only based on the ID. /// </summary> /// <returns></returns> public override int GetHashCode() { return ID.GetHashCode(); } /// <summary> /// Compare two FileObject: only based on the IDs. /// </summary> /// <param name="obj"></param> /// <returns></returns> public override bool Equals(object obj) { FileInfo other = obj as FileInfo; return other != null && other.ID == this.ID; } }
and about a folder, “FolderInfo“:
/// <summary> /// A structure that wraps the identification information of a folder. /// </summary> public class FolderInfo { /// <summary> /// Gets or sets the folder ID. /// </summary> public long ID { get; set; } /// <summary> /// Gets or sets the folder name. /// </summary> 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:
/// <summary> /// Represents a CometDocs file. /// Its original descriptions is available here: https://www.cometdocs.com/developer/apiDocumentation#types /// </summary> public class File : FileInfo { private byte[] content; /// <summary> /// The file content, useful for uploading from and downloading to local file-system. /// </summary> 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; } } /// <summary> /// Create a new FileInfo instance. /// If the provided name is "Test.pdf" the resulting File instance "Name" will be "Test" and the "Extension" "pdf". /// </summary> /// <param name="nameWithExtension">The full file name: e.g. "Test.pdf".</param> public File(string nameWithExtension = null) : base(nameWithExtension) { content = new byte[0]; } }
And “Folder” a folder with some files and sub-folders:
/// <summary> /// Represents a CometDocs folder. /// Its original descriptions is available here: https://www.cometdocs.com/developer/apiDocumentation#types /// </summary> public class Folder : FolderInfo { /// <summary> /// Gets or sets the list of sub-folders. /// </summary> public Folder[] Folders { get; set; } /// <summary> /// Gets or sets the list of files in this folder. /// </summary> 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“:
/// <summary> /// Authenticate to the Cometdocs API: https://www.cometdocs.com/developer/apiDocumentation#method-authenticate /// </summary> /// <param name="username">The CometDocs website username (typically an email address).</param> /// <param name="password">The CometDocs website password.</param> /// <param name="key">The CometDocs API key (you can create one here: https://www.cometdocs.com/developer/myApps. </param> /// <param name="validity">The desired validity of the emitted token.</param> /// <returns>An authentication token to use with subsequent API calls.</returns> 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:
/// <summary> /// Upload a file from the local file-system to the CometDocs store: https://www.cometdocs.com/developer/apiDocumentation#method-uploadFile /// </summary> /// <param name="token">A valid authentication token.</param> /// <param name="file">A file stream.</param> /// <param name="name">The name to give to the uploaded file.</param> /// <param name="folderId">The folder in which the file will be uploaded or the root folder if unspecified.</param> /// <returns>A FileInfo instance representing the uploaded file.</returns> 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:
whose class is “AuthenticateResponse“:
/// <summary> /// The base class for all the Cometdocs web API responses. /// </summary> internal class Response { /// <summary> /// The status of the response. /// </summary> public Status Status { get; set; } /// <summary> /// A short description of the state. /// </summary> public string Message { get; set; } /// <summary> /// A longer description of the state. /// </summary> 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:
/// <summary> /// Converter for the ConversionType class. /// Indeed the Cometdocs web API represents conversion types as strings: e.g. "PDF2XLS" is ConversionType{ From = "PDF", To = "XLS" }. /// </summary> 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 { ... /// <summary> /// Produce the Cometdocs compacted text representation of the current instance. /// </summary> /// <returns></returns> public override string ToString() { return From + "2" + To; } /// <summary> /// Parse a string representing a conversion type. /// </summary> /// <param name="s"></param> /// <returns></returns> 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
):
And here is the CertMgr main 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:
/// <summary> /// Represents an Exception raised by the Cometdocs API. /// </summary> public class CometDocsException : Exception { /// <summary> /// The corresponding Response message. /// </summary> public new string Message { get; private set; } /// <summary> /// The corresponding Response message. /// </summary> public Status Status { get; private set; } /// <summary> /// The corresponding Response error message. /// </summary> public string Error { get; private set; } /// <summary> /// Create a new instance of CometDocsException using the error data sent by the CometDocs web API. /// </summary> /// <param name="message"></param> /// <param name="status"></param> /// <param name="error"></param> 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:
/// <summary> /// Exception raised when an invalid (e.g. expired) token is provided to execute an operation that requires authentication. /// </summary> 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:
/// <summary> /// Check if the response represents a successful operation or a failure. /// It throws a CometDocsException in case of failure. /// </summary> /// <param name="response">The Response object to check</param> 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 { ... /// <summary> /// Ensure we can get the list of all the files categories. /// </summary> [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:
(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:
/// <summary> /// Ensure we can upload a file from memory to the store and get it back. /// </summary> [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.