Blog

Tagged by 'generic hander'

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