Salesforce .NET API: Get File Attachment

Published on
-
4 min read

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

Before you go...

If you've found this post helpful, you can buy me a coffee. It's certainly not necessary but much appreciated!

Buy Me A Coffee

Leave A Comment

If you have any questions or suggestions, feel free to leave a comment. I do get inundated with messages regarding my posts via LinkedIn and leaving a comment below is a better place to have an open discussion. Your comment will not only help others, but also myself.