Salesforce .NET API: Get File Attachment
Reading and writing files from an external application to Saleforce has always resulted in giving me quite the headache... Writing to Salesforce probably exacerbates things more than reading. I will aim to detail in a separate post on how you can write a file to Salesforce.
In this post I will demonstrate how to read a file found in the "Notes & Attachments" area of Salesforce as well as getting back all information about that file.
The first thing we need is our attachment object, to get back all information about our file. I created one called "AttachmentInfo":
public class AttachmentInfo
{
public string Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string BodyLength { get; set; }
public string ContentType { get; set; }
public byte[] FileBytes { get; set; }
}
I created two methods in a class named "AttachmentInfoProvider". Both methods are pretty straight-forward and retrieve data from Salesforce using a custom GetRows() method that is part of another class object I created: ObjectDetailInfoProvider. You can get the code for this from the following blog post - Salesforce .NET API: Select/Insert/Update Methods.
GetAttachmentsDataByParentId() Method
/// <summary>
/// Gets all attachments that belong to an object. For example a contact.
/// </summary>
/// <param name="parentId"></param>
/// <param name="fileNameMatch"></param>
/// <param name="orderBy"></param>
/// <returns></returns>
public static async Task<List<AttachmentInfo>> GetAttachmentsDataByParentId(string parentId, string fileNameMatch, string orderBy)
{
string cacheKey = $"GetAttachmentsByParentId|{parentId}|{fileNameMatch}";
List<AttachmentInfo> attachments = CacheEngine.Get<List<AttachmentInfo>>(cacheKey);
if (attachments == null)
{
string whereCondition = string.Empty;
if (!string.IsNullOrEmpty(fileNameMatch))
whereCondition = $"Name LIKE '%{fileNameMatch}%'";
List<dynamic> attachmentObjects = await ObjectDetailInfoProvider.GetRows("Attachment", new List<string> {"Id", "Name", "Description", "Body", "BodyLength", "ContentType"}, whereCondition, orderBy);
if (attachmentObjects.Any())
{
attachments = attachmentObjects.Select(attObj => new AttachmentInfo
{
Id = attObj.Id,
Name = attObj.Name,
Description = attObj.Description,
BodyLength = attObj.BodyLength,
ContentType = attObj.ContentType
}).ToList();
// Add collection of pick list items to cache.
CacheEngine.Add(attachments, cacheKey, 15);
}
}
return attachments;
}
The GetAttachmentsDataByParentId() method takes in three parameters:
- parentId: The ID that links an attachment to another object. For example, a contact.
- fileNameMatch: The name of the file you wish to search for. For most flexibility, a wildcard search is performed.
- orderBy: Order the returned dataset.
If you're thinking this method alone will return the file itself, you'd be disappointed - this is where our next method GetFile() comes into play.
GetFile() Method
/// <summary>
/// Gets attachment in its raw form ready for transformation to a physical file, in addition to its file attributes.
/// </summary>
/// <param name="attachmentId"></param>
/// <returns></returns>
public static async Task<AttachmentInfo> GetFile(string attachmentId)
{
List<dynamic> attachmentObjects = await ObjectDetailInfoProvider.GetRows("Attachment", new List<string> {"Id", "Name", "Description", "BodyLength", "ContentType"}, $"Id = '{attachmentId}'", string.Empty);
if (attachmentObjects.Any())
{
AttachmentInfo attachInfo = new AttachmentInfo();
#region Get Core File Information
attachInfo.Id = attachmentObjects[0].Id;
attachInfo.Name = attachmentObjects[0].Name;
attachInfo.BodyLength = attachmentObjects[0].BodyLength;
attachInfo.ContentType = attachmentObjects[0].ContentType;
#endregion
#region Get Attachment As Byte Array
Authentication salesforceAuth = await AuthenticationResponse.Rest();
HttpClient queryClient = new HttpClient();
string apiUrl = $"{SalesforceConfig.PlatformUrl}/services/data/v37.0/sobjects/Attachment/{attachmentId}/Body";
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, apiUrl);
request.Headers.Add("Authorization", $"OAuth {salesforceAuth.AccessToken}");
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
HttpResponseMessage response = await queryClient.SendAsync(request);
if (response.StatusCode == HttpStatusCode.OK)
attachInfo.FileBytes = await response.Content.ReadAsByteArrayAsync();
#endregion
return attachInfo;
}
else
{
return null;
}
}
An attachment ID is all we need to get back a file in its raw form. You will probably notice there is some similar functionality happening in this method where I am populating all fields of the AttachmentInfo object, just like the GetAttachmentsDataByParentId() method I detailed above. The only difference being is the fact this time round only a single file is returned.
The reason behind this approach comes from a performance standpoint. I could have modified the GetAttachmentsDataByParentId() method to also return the file in its byte form. However, this didn't seem a good approach, since we could be outputting multiple files large in size. So making a separate call to focus on getting the physical file seemed like a wise approach.
To take things one step further, you can render the attachment from Salesforce within your ASP.NET application using a Generic Handler (.ashx file):
<%@ WebHandler Language="C#" Class="SalesforceFileHandler" %>
using System;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using Site.Salesforce;
using Site.Salesforce.Models.Attachment;
public class SalesforceFileHandler : HttpTaskAsyncHandler
{
public override async Task ProcessRequestAsync(HttpContext context)
{
string fileId = context.Request.QueryString["FileId"];
// Check if there is a File ID in the query string.
if (!string.IsNullOrEmpty(fileId))
{
AttachmentInfo attachment = await AttachmentInfoProvider.GetFile(fileId);
// If attachment is returned, render to the browser window.
if (attachment != null)
{
context.Response.Buffer = true;
context.Response.AppendHeader("Content-Disposition", $"attachment; filename=\"{attachment.Name}\"");
context.Response.BinaryWrite(attachment.FileBytes);
context.Response.OutputStream.Write(attachment.FileBytes, 0, attachment.FileBytes.Length);
context.Response.ContentType = attachment.ContentType;
}
else
{
context.Response.ContentType = "text/plain";
context.Response.Write("Invalid File");
}
}
else
{
context.Response.ContentType = "text/plain";
context.Response.Write("Invalid Request");
}
context.Response.Flush();
context.Response.End();
}
}