Hi Philip.
I modified the code to work with v2.0.
One main difference between 1.43 and v2.0 is in how collections are handled.
In v2.0 we removed the ability to return child collections in the same response for performance reasons. In v2.0 fetching a collection will return an object with the count and the url from which to get the collection data.
In the older versions of WS API certain fetch lists create a lot recursive calls, and all the collections included in the fetch make the call quite expensive. In WS API v2.0 this will not happen, since a separate call will have to be made in order to get objects of the collections. Please see WS API documentation (
https://rally1.rallydev.com/slm/doc/webservice/rest_collections.jsp).
Before we get to the code that works with v2.0, I wanted to mention that I was able to run the original 1.43 example and download an image attachement successfully without making any changes to the code (other than using my workspace/project refs, and query string to look for a story in my workspace) while using the latest dll 2.0.1 from here (
https://github.com/RallyTools/RallyRestToolkitFor.NET).
I did not change this line in the code:
String wsapiVersion = "1.43";
If I change this line to:
String wsapiVersion = "v2.0";
then I get an error
Unhandled Exception: Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: The
best overloaded method match for 'Rally.RestApi.DynamicJsonObject.this[string]'
has some invalid arguments
at CallSite.Target(Closure , CallSite , Object , Int32 )
at System.Dynamic.UpdateDelegates.UpdateAndExecute2[T0,T1,TRet](CallSite site
, T0 arg0, T1 arg1)
at RestExample_DownloadAttachment.Program.Main(String[] args) in C:\Users\nick\Documents\Visual Studio 2010\Projects\DownloadAttachments\DownloadAttach
ments\Program.cs:line 72
If I set WS API version in this code example to v2.0 but the rest of the code is the same, the problem starts here:
var myAttachmentFromStory = storyAttachments[0];
Let's step back from the code and consider WS API json results that return Attachments in v2.0 and 1.43 respectively.
This is the same story US20 I query in the code.
When using v2.0 the Attachments object looks like this:
Attachments: {
_rallyAPIMajor: "2",
_rallyAPIMinor: "0",
_ref: "https://rally1.rallydev.com/slm/webservice/v2.0/HierarchicalRequirement/12525994836/Attachments",
_type: "Attachment",
Count: 3
},
When using 1.43 it looks like this:
Attachments: [
{
_rallyAPIMajor: 1
_rallyAPIMinor: 43
_ref: https://rally1.rallydev.com/slm/webservice/1.43/attachment/12570906304.js
_refObjectName: AttachmentFromREST.png
_type: Attachment
}
{
_rallyAPIMajor: 1
_rallyAPIMinor: 43
_ref: https://rally1.rallydev.com/slm/webservice/1.43/attachment/15875922498.js
_refObjectName: AttachmentFromREST.png
_type: Attachment
}
{
_rallyAPIMajor: 1
_rallyAPIMinor: 43
_ref: https://rally1.rallydev.com/slm/webservice/1.43/attachment/15875900890.js
_refObjectName: AttachmentFromREST.png
_type: Attachment
}
Now we can see why storyAttachments[0] in the code works with 1.43 and does not work with v2.0. In v2.0 after we get the Attachments collection we need to make a separate request to hydrate it to get to all the elements of the collection.
Here is the relevant part of the code example:
Request attachmentsRequest = new Request(story["Attachments"]);
QueryResult attachmentsResult = restApi.Query(attachmentsRequest);
var myAttachmentFromStory = attachmentsResult.Results.First();
The full code example is below. I have to mention that it is ouside of Rally support's scope to write and debug custom code and this is for illustration purposes only. We have Technical Services team (
https://rallydev.force.com/answers?id=kA014000000PK1j) that does custom coding.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using Rally.RestApi;
using Rally.RestApi.Response;
namespace DownloadAttachment
{
class Program
{
static void Main(string[] args)
{
RallyRestApi restApi;
String userName = "user@co.com";
String userPassword = "secret";
String rallyURL = "https://rally1.rallydev.com";
String wsapiVersion = "v2.0";
restApi = new RallyRestApi(
userName,
userPassword,
rallyURL,
wsapiVersion
);
String workspaceRef = "/workspace/12352608129";
String projectRef = "/project/12352608219";
bool projectScopingUp = false;
bool projectScopingDown = true;
Request storyRequest = new Request("hierarchicalrequirement");
storyRequest.Workspace = workspaceRef;
storyRequest.Project = projectRef;
storyRequest.ProjectScopeDown = projectScopingDown;
storyRequest.ProjectScopeUp = projectScopingUp;
storyRequest.Fetch = new List<string>()
{
"Name",
"FormattedID",
"Attachments"
};
storyRequest.Query = new Query("FormattedID", Query.Operator.Equals, "US20");
QueryResult queryResult = restApi.Query(storyRequest);
DynamicJsonObject story = queryResult.Results.First();
// Grab the Attachments collection
Request attachmentsRequest = new Request(story["Attachments"]);
QueryResult attachmentsResult = restApi.Query(attachmentsRequest);
//Download the first attachment
var myAttachmentFromStory = attachmentsResult.Results.First();
String myAttachmentRef = myAttachmentFromStory["_ref"];
Console.WriteLine("Found Attachment: " + myAttachmentRef);
// Fetch fields for the Attachment
string[] attachmentFetch = { "ObjectID", "Name", "Content", "ContentType", "Size" };
// Now query for the attachment
DynamicJsonObject attachmentObject = restApi.GetByReference(myAttachmentRef, "true");
// Grab the AttachmentContent
DynamicJsonObject attachmentContentFromAttachment = attachmentObject["Content"];
String attachmentContentRef = attachmentContentFromAttachment["_ref"];
// Lastly pull the content
// Fetch fields for the Attachment
string[] attachmentContentFetch = { "ObjectID", "Content" };
// Now query for the attachment
Console.WriteLine("Querying for Content...");
DynamicJsonObject attachmentContentObject = restApi.GetByReference(attachmentContentRef, "true");
Console.WriteLine("AttachmentContent: " + attachmentObject["_ref"]);
String base64EncodedContent = attachmentContentObject["Content"];
// File information
String attachmentSavePath = "C:\\Users\\nmusaelian\\NewFolder";
String attachmentFileName = attachmentObject["Name"];
String fullAttachmentFile = attachmentSavePath + attachmentFileName;
// Determine attachment Content mime-type
String attachmentContentType = attachmentObject["ContentType"];
// Specify Image format
System.Drawing.Imaging.ImageFormat attachmentImageFormat;
try
{
attachmentImageFormat = getImageFormat(attachmentContentType);
}
catch (System.ArgumentException e)
{
Console.WriteLine("Invalid attachment file format:" + e.StackTrace);
}
try
{
// Convert base64 content to Image
Console.WriteLine("Converting base64 AttachmentContent String to Image.");
// Convert Base64 string to bytes
byte[] bytes = Convert.FromBase64String(base64EncodedContent);
Image myAttachmentImage;
using (MemoryStream ms = new MemoryStream(bytes))
{
myAttachmentImage = Image.FromStream(ms);
// Save the image
Console.WriteLine("Saving Image: " + fullAttachmentFile);
myAttachmentImage.Save(fullAttachmentFile, System.Drawing.Imaging.ImageFormat.Jpeg);
Console.WriteLine("Finished Saving Attachment: " + fullAttachmentFile);
}
}
catch (Exception e)
{
Console.WriteLine("Unhandled exception occurred: " + e.StackTrace);
Console.WriteLine(e.Message);
}
Console.ReadKey();
}
// Returns an ImageFormat type based on Rally contentType / mime-type
public static System.Drawing.Imaging.ImageFormat getImageFormat(String contentType)
{
// Save Image format
System.Drawing.Imaging.ImageFormat attachmentImageFormat;
switch (contentType)
{
case "image/png":
attachmentImageFormat = System.Drawing.Imaging.ImageFormat.Png;
break;
case "image/jpeg":
attachmentImageFormat = System.Drawing.Imaging.ImageFormat.Jpeg;
break;
case "image/tiff":
attachmentImageFormat = System.Drawing.Imaging.ImageFormat.Tiff;
break;
default:
Console.WriteLine("Invalid image file format.");
throw new System.ArgumentException("Invalid attachment file format.");
};
return attachmentImageFormat;
}
}
}