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();
}
}