I need to get opportunity files from Salesforce and copy them to some folder. I am using .NET library for connecting to Salesforcre. I can get any data I want, except the [VersionData]
field in [ContentVersion]
table, which contains base64
data of the files I want. I can get the data with Workbench tool, but the only thing I get via .NET library is a link to file. I could create HttpClient with appropriate headers and invoke that URL, but I don't like to go this ways. Can I get the file via .NET library?
Here is my solution (model class, endpoint method, authentication method):
public class ContentVersion
{
[JsonIgnoreSerialization]
[JsonProperty("Id", NullValueHandling = NullValueHandling.Ignore)]
public string Id { get; set; }
[JsonProperty("ContentDocumentId")]
public string ContentDocumentId { get; set; }
[JsonProperty("FileExtension")]
public string FileExtension { get; set; }
[JsonProperty("Title")]
public string Title { get; set; }
[JsonProperty("VersionNumber")]
public int VersionNumber { get; set; }
[JsonProperty("IsLatest")]
public bool IsLatest { get; set; }
[JsonProperty("VersionData")]
public string VersionDataURL { get; set; }
public Stream VersionDataStream { get; set; }
}
public async Threading.Task<ContentVersion> GetContentNewestVersion(string EntityId)
{
// Authenticate if not already
if (client == null) await Authenticate();
// Create query string
string query = @"SELECT
Id,
ContentDocumentId,
FileExtension,
Title,
VersionNumber,
IsLatest,
VersionData
FROM ContentVersion
WHERE ContentDocumentId = '" + EntityId + "'";
List<ContentVersion> results = new List<ContentVersion>();
QueryResult<ContentVersion> queryResult = await client.QueryAsync<ContentVersion>(query);
results.AddRange(queryResult.Records);
while (!queryResult.Done)
{
queryResult = await client.QueryContinuationAsync<ContentVersion>(queryResult.NextRecordsUrl);
results.AddRange(queryResult.Records);
}
// get only the newest Content version
ContentVersion latestContentVersion = results.Where(r => r.IsLatest).OrderByDescending(r => r.VersionNumber).FirstOrDefault();
// Get file stream via returned URL
using (HttpClient httpClient = new HttpClient())
{
// Add access token to request
httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", AccessToken);
// call server
var response = await httpClient.GetAsync(InstanceUrl + latestContentVersion.VersionDataURL);
// read stream and append it to object
latestContentVersion.VersionDataStream = await response.Content.ReadAsStreamAsync();
}
return latestContentVersion;
}
protected async Threading.Task Authenticate()
{
// Check if not already connected
if (client == null)
{
// Security settings
ServicePointManager.Expect100Continue = true;
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
// Create Auth client
var auth = new AuthenticationClient();
// Authorize user
await auth.UsernamePasswordAsync(LoginDetails.ClientId, LoginDetails.ClientSecret, LoginDetails.Username, LoginDetails.Password, LoginDetails.TokenRequestEndpoint);
_instanceURL = auth.InstanceUrl;
AccessToken = auth.AccessToken;
// Create and return client with session variables
client = new ForceClient(auth.InstanceUrl, auth.AccessToken, auth.ApiVersion);
}
}
And this is how I write recieved stream to file.
// deisred folder
string PathToFolder = @"C:\destination\";
// get stream from Salesforce
ContentVersion documentContent = await forceAPI.GetContentNewestVersion(contentDocumentlink.ContentDocumentId);
// write file from stream
using (FileStream file = new FileStream(PathToFolder + documentContent.Title + "." + documentContent.FileExtension, FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
documentContent.VersionDataStream.CopyTo(file);
}
In REST API it has to be pulled through that url you got. It'll be a raw binary stream of data which would be tricky to represent together within JSON of normal query results. REST API is focused on mobile apps, minimizing network usage and base64 decode is some processing power I guess.
It shouldn't be tricky though? Just a GET to the URL you got with header Authorization: Bearer <session id here>
If you want base64 you need to make it a SOAP API request (which is what Workbench really uses, note that "REST explorer" is a separate menu option).
POST request to https://<redacted>.my.salesforce.com/services/Soap/u/48.0
with payload like
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:urn="urn:partner.soap.sforce.com">
<soapenv:Header>
<urn:SessionHeader>
<urn:sessionId>nice try ;) you can reuse same session id</urn:sessionId>
</urn:SessionHeader>
</soapenv:Header>
<soapenv:Body>
<urn:query>
<urn:queryString>SELECT VersionData FROM ContentVersion WHERE Id = '068...'</urn:queryString>
</urn:query>
</soapenv:Body>
</soapenv:Envelope>
Will give you something like this back
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns="urn:partner.soap.sforce.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:sf="urn:sobject.partner.soap.sforce.com">
<soapenv:Header>
<LimitInfoHeader>
<limitInfo>
<current>12</current>
<limit>5000000</limit>
<type>API REQUESTS</type>
</limitInfo>
</LimitInfoHeader>
</soapenv:Header>
<soapenv:Body>
<queryResponse>
<result xsi:type="QueryResult">
<done>true</done>
<queryLocator xsi:nil="true"/>
<records xsi:type="sf:sObject">
<sf:type>ContentVersion</sf:type>
<sf:Id xsi:nil="true"/>
<sf:VersionData>/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAA8AAD/
(bla bla bla)
/X/lf0eG9Kl61//Z</sf:VersionData>
</records>
<size>1</size>
</result>
</queryResponse>
</soapenv:Body>
</soapenv:Envelope>
I was struggling with the same issue using the Force.com Toolkit for .NET and didn't want to switch to a workaround (for example doing a separate SOAP call or implement my own HttpClient). So I found out it is actually natively supported using the Toolkit. I didn't found it anywhere in the documentation (which is not extensive anyway), so I post it here, maybe it will help some other developers.
Console.WriteLine($"ContentDocumentLink ID: {contentDocumentLink.Id}");
Console.WriteLine($"ContentDocument ID: {contentDocumentLink.ContentDocumentId}");
// Get ContentDocument record
var contentDocument = await client.QueryByIdAsync<ContentDocument>("ContentDocument", contentDocumentLink.ContentDocumentId);
Console.WriteLine($"ContentVersion ID: {contentDocument.LatestPublishedVersionId}");
// Get ContentVersion record
var contentVersion = await client.QueryByIdAsync<ContentVersion>("ContentVersion", contentDocument.LatestPublishedVersionId);
// convert Bytes to KiloBytes presentation
string fullFileName = $"{contentVersion.Title}.{contentVersion.FileExtension}";
int FileSizeInBytes = contentVersion.ContentSize;
int FileSizeInKiloBytes = (int)ByteSize.FromBytes(FileSizeInBytes).LargestWholeNumberBinaryValue;
Console.WriteLine($"Filename: {fullFileName} ({FileSizeInKiloBytes} KB)");
// Get VersionData as a stream
var versionData = await client.GetBlobAsync("ContentVersion", contentDocument.LatestPublishedVersionId, "VersionData");
var fileStream = File.Create($"C:\\Temp\\{contentVersion.Title}.{contentVersion.FileExtension}");
versionData.CopyTo(fileStream);
fileStream.Close();
So the trick is using the GetBlobAsync method on base64 fields in SalesForce. This will initiate a GET request on the desired endpoint: '/services/data/v49.0/sobjects/ContentVersion/<ContentVersion_ID>/VersionData'. And the response will be a 'Content-Type: application/octetstream' one captured into a Stream.
Here is my solution (model class, endpoint method, authentication method):
public class ContentVersion
{
[JsonIgnoreSerialization]
[JsonProperty("Id", NullValueHandling = NullValueHandling.Ignore)]
public string Id { get; set; }
[JsonProperty("ContentDocumentId")]
public string ContentDocumentId { get; set; }
[JsonProperty("FileExtension")]
public string FileExtension { get; set; }
[JsonProperty("Title")]
public string Title { get; set; }
[JsonProperty("VersionNumber")]
public int VersionNumber { get; set; }
[JsonProperty("IsLatest")]
public bool IsLatest { get; set; }
[JsonProperty("VersionData")]
public string VersionDataURL { get; set; }
public Stream VersionDataStream { get; set; }
}
public async Threading.Task<ContentVersion> GetContentNewestVersion(string EntityId)
{
// Authenticate if not already
if (client == null) await Authenticate();
// Create query string
string query = @"SELECT
Id,
ContentDocumentId,
FileExtension,
Title,
VersionNumber,
IsLatest,
VersionData
FROM ContentVersion
WHERE ContentDocumentId = '" + EntityId + "'";
List<ContentVersion> results = new List<ContentVersion>();
QueryResult<ContentVersion> queryResult = await client.QueryAsync<ContentVersion>(query);
results.AddRange(queryResult.Records);
while (!queryResult.Done)
{
queryResult = await client.QueryContinuationAsync<ContentVersion>(queryResult.NextRecordsUrl);
results.AddRange(queryResult.Records);
}
// get only the newest Content version
ContentVersion latestContentVersion = results.Where(r => r.IsLatest).OrderByDescending(r => r.VersionNumber).FirstOrDefault();
// Get file stream via returned URL
using (HttpClient httpClient = new HttpClient())
{
// Add access token to request
httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", AccessToken);
// call server
var response = await httpClient.GetAsync(InstanceUrl + latestContentVersion.VersionDataURL);
// read stream and append it to object
latestContentVersion.VersionDataStream = await response.Content.ReadAsStreamAsync();
}
return latestContentVersion;
}
protected async Threading.Task Authenticate()
{
// Check if not already connected
if (client == null)
{
// Security settings
ServicePointManager.Expect100Continue = true;
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
// Create Auth client
var auth = new AuthenticationClient();
// Authorize user
await auth.UsernamePasswordAsync(LoginDetails.ClientId, LoginDetails.ClientSecret, LoginDetails.Username, LoginDetails.Password, LoginDetails.TokenRequestEndpoint);
_instanceURL = auth.InstanceUrl;
AccessToken = auth.AccessToken;
// Create and return client with session variables
client = new ForceClient(auth.InstanceUrl, auth.AccessToken, auth.ApiVersion);
}
}
And this is how I write recieved stream to file.
// deisred folder
string PathToFolder = @"C:\destination\";
// get stream from Salesforce
ContentVersion documentContent = await forceAPI.GetContentNewestVersion(contentDocumentlink.ContentDocumentId);
// write file from stream
using (FileStream file = new FileStream(PathToFolder + documentContent.Title + "." + documentContent.FileExtension, FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
documentContent.VersionDataStream.CopyTo(file);
}
© 2022 - 2024 — McMap. All rights reserved.