Use file data with Attachment and Note records

Attachment (ActivityMimeAttachment) and Note (Annotation) tables contain special string columns that store file data. These tables existed before file or image columns, so they work differently from those tables.

  • The binary file data is stored as Base64-encoded string values in string columns. Attachments are stored in the columnActivityMimeAttachment.Body and notes are stored in the column Annotation.DocumentBody.
  • File name data is stored in the FileName column.
  • MIME type data is stored in the MimeType column.

Because FileName, MimeType, ActivityMimeAttachment.Body, and Annotation.DocumentBody are part of the data for the attachment or note record, you should update these three columns together with any other values.

You can directly get and set the values of the activitymimeattachment.body and annotation.documentbody columns as Base64-encoded strings. Setting these values should be fine as long as the files aren't too large, for example under 4 MB. By default the maximum size is 5 MB. You can configure these columns to accept files as large as 128 MB. When you have increased the maximum file size and are working with larger files, you should use messages provided to break the files into smaller chunks when uploading or downloading files. For information about retrieving or changing the file size limits, see File size limits.

Attachment files

An attachment is a file that is associated with an email activity, either directly or through an Email Template (Template). Multiple attachments can be associated with the activity or template. You can reuse attachment files by setting the activitymimeattachment.attachmentid value to refer to another existing attachment rather than by setting the body, filename, and mimetype properties.

Other Dataverse tables named attachment

Attachment (ActivityMimeAttachment) shouldn't be confused with activityfileattachment, which supports files associated with the Post table.

Within the Dataverse schema, there's also a public table with the name Attachment, which is exposed in the Web API as attachment EntityType. This table can be queried and it reflects data in the ActivityMimeAttachment table. But it doesn't support create, retrieve, update, or delete operations. This table doesn't appear within the Power Apps designer.

Upload attachment files

Use the InitializeAttachmentBlocksUpload, UploadBlock, and CommitAttachmentBlocksUpload messages to upload large files for attachments.

Important

You can only use these messages to create a new attachment. It you try to use them to update an existing attachment you'll get an error that the record already exists.

The following static UploadAttachment method shows how to create an attachment with a file using the InitializeAttachmentBlocksUploadRequest, UploadBlockRequest, and CommitAttachmentBlocksUploadRequest classes to return a CommitAttachmentBlocksUploadResponse with ActivityMimeAttachmentId and FileSizeInBytes properties.

static CommitAttachmentBlocksUploadResponse UploadAttachment(
   IOrganizationService service,
   Entity attachment,
   FileInfo fileInfo,
   string fileMimeType = null)
{
   if (attachment.LogicalName != "activitymimeattachment")
   {
         throw new ArgumentException(
            "The attachment parameter must be an activitymimeattachment entity.",
            nameof(attachment));
   }

   // body value in activitymimeattachment not needed. Remove if found.
   if (attachment.Contains("body"))
   {
         attachment.Attributes.Remove("body");
   }

   // Try to get the mimetype if not provided.
   if (string.IsNullOrEmpty(fileMimeType))
   {
         var provider = new FileExtensionContentTypeProvider();

         if (!provider.TryGetContentType(fileInfo.Name, out fileMimeType))
         {
            fileMimeType = "application/octet-stream";
         }
   }
   // Don't overwrite mimetype value if it exists
   if (!attachment.Contains("mimetype"))
   {
         attachment["mimetype"] = fileMimeType;
   }

   // Initialize the upload
   InitializeAttachmentBlocksUploadRequest initializeRequest = new()
   {
         Target = attachment
   };

   var initializeResponse =
         (InitializeAttachmentBlocksUploadResponse)service.Execute(initializeRequest);

   string fileContinuationToken = initializeResponse.FileContinuationToken;

   // Capture blockids while uploading
   List<string> blockIds = new();

   using Stream uploadFileStream = fileInfo.OpenRead();

   int blockSize = 4 * 1024 * 1024; // 4 MB

   byte[] buffer = new byte[blockSize];
   int bytesRead = 0;

   long fileSize = fileInfo.Length;

   // The number of iterations that will be required:
   // int blocksCount = (int)Math.Ceiling(fileSize / (float)blockSize);
   int blockNumber = 0;

   // While there is unread data from the file
   while ((bytesRead = uploadFileStream.Read(buffer, 0, buffer.Length)) > 0)
   {
         // The file or final block may be smaller than 4MB
         if (bytesRead < buffer.Length)
         {
            Array.Resize(ref buffer, bytesRead);
         }

         blockNumber++;

         string blockId = Convert.ToBase64String(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString()));

         blockIds.Add(blockId);

         // Prepare the request
         UploadBlockRequest uploadBlockRequest = new()
         {
            BlockData = buffer,
            BlockId = blockId,
            FileContinuationToken = fileContinuationToken,
         };

         // Send the request
         service.Execute(uploadBlockRequest);
   }

   // Commit the upload
   CommitAttachmentBlocksUploadRequest commitRequest = new()
   {
         BlockList = blockIds.ToArray(),
         FileContinuationToken = fileContinuationToken,
         Target = attachment
   };
   
      return  (CommitAttachmentBlocksUploadResponse)service.Execute(commitRequest);

}

More information:

Note

This example method includes some logic to try to get the MIME type of the file using the FileExtensionContentTypeProvider.TryGetContentType(String, String) method if it isn't provided. If the type isn't found, it's set to application/octet-stream.

Download attachment files

You can download an attachment file in a single operation using the Web API or in chunks using the SDK or Web API.

Download attachment files in a single operation using the Web API

Using the Web API, you can download an attachment file in a single operation.

Unlike retrieving file columns, this method doesn't provide information about file size, file name, or MIME type.

Request:

GET [Organization Uri]/api/data/v9.2/activitymimeattachments(<activitymimeattachmentid>)/body/$value HTTP/1.1
OData-MaxVersion: 4.0
OData-Version: 4.0
If-None-Match: null
Accept: application/json

Response:

HTTP/1.1 200 OK
OData-Version: 4.0
Content-Type: text/plain

<Base64 string content removed for brevity>

More information:

Download attachment files in chunks

To retrieve the file in chunks, use the following messages with either the SDK or Web API:

Message Description
InitializeAttachmentBlocksDownload Specifies the Note record that you want to download a file from. It returns the file size in bytes and a file continuation token that you can use to download the file in blocks using the DownloadBlock message.
DownloadBlock Requests the size of the block, the offset value, and the file continuation token.

After you download all the blocks, join them to create the entire downloaded file.

The following static DownloadAttachment method shows how to download an attachment using the SDK with the InitializeAttachmentBlocksDownloadRequest and DownloadBlockRequest classes. This function returns the byte[] data and the name of the file.

static (byte[] bytes, string fileName) DownloadAttachment(
   IOrganizationService service,
   EntityReference target)
{
   if (target.LogicalName != "activitymimeattachment")
   {
         throw new ArgumentException(
            "The target parameter must refer to an activitymimeattachment record.",
            nameof(target));
   }

   InitializeAttachmentBlocksDownloadRequest initializeRequest = new()
   {
         Target = target
   };

   var response =
         (InitializeAttachmentBlocksDownloadResponse)service.Execute(initializeRequest);

   string fileContinuationToken = response.FileContinuationToken;
   int fileSizeInBytes = response.FileSizeInBytes;
   string fileName = response.FileName;

   List<byte> fileBytes = new(fileSizeInBytes);

   long offset = 0;
   long blockSizeDownload = 4 * 1024 * 1024; // 4 MB

   // File size may be smaller than defined block size
   if (fileSizeInBytes < blockSizeDownload)
   {
         blockSizeDownload = fileSizeInBytes;
   }

   while (fileSizeInBytes > 0)
   {
         // Prepare the request
         DownloadBlockRequest downLoadBlockRequest = new()
         {
            BlockLength = blockSizeDownload,
            FileContinuationToken = fileContinuationToken,
            Offset = offset
         };

         // Send the request
         var downloadBlockResponse =
                  (DownloadBlockResponse)service.Execute(downLoadBlockRequest);

         // Add the block returned to the list
         fileBytes.AddRange(downloadBlockResponse.Data);

         // Subtract the amount downloaded,
         // which may make fileSizeInBytes < 0 and indicate
         // no further blocks to download
         fileSizeInBytes -= (int)blockSizeDownload;
         // Increment the offset to start at the beginning of the next block.
         offset += blockSizeDownload;
   }

   return (fileBytes.ToArray(), fileName);
}

More information:

Annotation files

A note is a record associated with a table row that contains text and may have a single file attached. Only tables with EntityMetadata.HasNotes set to true may have notes associated with them.

Upload annotation files

Use the InitializeAnnotationBlocksUpload, UploadBlock, and CommitAnnotationBlocksUpload messages to upload files for notes.

The annotation you pass as the Target parameter for these messages must have an annotationid value. This is how you can update existing annotation records.

Normally, it's best to let Dataverse generate the unique identifier values when creating new records, but that isn't possible with these messages. To create a new annotation with these messages, you must generate a new Guid value to set as the annotationid value rather than let Dataverse generate the value.

The following static UploadNote method shows how to create or update a note with a file using the InitializeAnnotationBlocksUploadRequest, UploadBlockRequest, and CommitAnnotationBlocksUploadRequest classes. It returns a CommitAnnotationBlocksUploadResponse with AnnotationId and FileSizeInBytes properties.

static CommitAnnotationBlocksUploadResponse UploadNote(
   IOrganizationService service,
   Entity annotation,
   FileInfo fileInfo,
   string? fileMimeType = null)
{

   if (annotation.LogicalName != "annotation")
   {
         throw new ArgumentException(
            message: "The annotation parameter must be an annotation entity",
            paramName: nameof(annotation));
   }
   if (!annotation.Attributes.Contains("annotationid") || annotation.Id != Guid.Empty)
   {
         throw new ArgumentException(
            message: "The annotation parameter must include a valid annotationid value.",
            paramName: nameof(annotation));
   }

   // documentbody value in annotation not needed. Remove if found.
   if (annotation.Contains("documentbody"))
   {
         annotation.Attributes.Remove("documentbody");
   }

   // Try to get the mimetype if not provided.
   if (string.IsNullOrEmpty(fileMimeType))
   {
         var provider = new FileExtensionContentTypeProvider();

         if (!provider.TryGetContentType(fileInfo.Name, out fileMimeType))
         {
            fileMimeType = "application/octet-stream";
         }
   }
   // Don't override what might be included in the annotation.
   if (!annotation.Contains("mimetype")) {
         annotation["mimetype"] = fileMimeType;
   }
   
   // Initialize the upload
   InitializeAnnotationBlocksUploadRequest initializeRequest = new()
   {
         Target = annotation
   };

   var initializeResponse =
         (InitializeAnnotationBlocksUploadResponse)service.Execute(initializeRequest);

   string fileContinuationToken = initializeResponse.FileContinuationToken;

   // Capture blockids while uploading
   List<string> blockIds = new();

   using Stream uploadFileStream = fileInfo.OpenRead();

   int blockSize = 4 * 1024 * 1024; // 4 MB

   byte[] buffer = new byte[blockSize];
   int bytesRead = 0;

   long fileSize = fileInfo.Length;

   // The number of iterations that will be required:
   // int blocksCount = (int)Math.Ceiling(fileSize / (float)blockSize);
   int blockNumber = 0;

   // While there is unread data from the file
   while ((bytesRead = uploadFileStream.Read(buffer, 0, buffer.Length)) > 0)
   {
         // The file or final block may be smaller than 4MB
         if (bytesRead < buffer.Length)
         {
            Array.Resize(ref buffer, bytesRead);
         }

         blockNumber++;
         // Generates base64 string blockId values based on a Guid value so they are always the same length.
         string blockId = Convert.ToBase64String(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString()));

         blockIds.Add(blockId);

         // Prepare the request
         UploadBlockRequest uploadBlockRequest = new()
         {
            BlockData = buffer,
            BlockId = blockId,
            FileContinuationToken = fileContinuationToken,
         };

         // Send the request
         service.Execute(uploadBlockRequest);
   }

   // Commit the upload
   CommitAnnotationBlocksUploadRequest commitRequest = new()
   {
         BlockList = blockIds.ToArray(),
         FileContinuationToken = fileContinuationToken,
         Target = annotation
   };

      return  (CommitAnnotationBlocksUploadResponse)service.Execute(commitRequest);
}

More information:

Note

This example method includes some logic to try to get the MIME type of the file using the FileExtensionContentTypeProvider.TryGetContentType(String, String) method if it isn't provided. If the type isn't found, it's set to application/octet-stream.

Download annotation files

You can download a Note file in a single operation using the Web API, or in chunks using the SDK or Web API.

Download Annotation files in a single operation using the Web API

Using the Web API, you can download a Note file in a single operation:

Unlike retrieving file columns, this method doesn't provide information about file size, file name, or MIME type.

Request:

GET [Organization Uri]/api/data/v9.2/annotations(<annotationid>)/documentbody/$value HTTP/1.1
OData-MaxVersion: 4.0
OData-Version: 4.0
If-None-Match: null
Accept: application/json

Response:

HTTP/1.1 200 OK
OData-Version: 4.0
Content-Type: text/plain

<Base64 string content removed for brevity>

More information:

Download annotation files in chunks

To retrieve the file in chunks, use the following messages with either the SDK or Web API:

Message Description
InitializeAnnotationBlocksDownload Specifies the Note record that you want to download a file from. It returns the file size in bytes and a file continuation token that you can use to download the file in blocks using the DownloadBlock message.
DownloadBlock Requests the size of the block, the offset value, and the file continuation token.

After you download all the blocks, join them to create the entire downloaded file.

The following static DownloadNote method shows how to download a note using the SDK with the InitializeAnnotationBlocksDownloadRequest and DownloadBlockRequest classes. This function returns the byte[] data and the name of the file.

static (byte[] bytes, string fileName) DownloadNote(
    IOrganizationService service,
    EntityReference target)
{
if (target.LogicalName != "annotation")
{
      throw new ArgumentException("The target parameter must refer to an note record.", nameof(target));
}

InitializeAnnotationBlocksDownloadRequest initializeRequest = new()
{
      Target = target
};

var response =
      (InitializeAnnotationBlocksDownloadResponse)service.Execute(initializeRequest);

string fileContinuationToken = response.FileContinuationToken;
int fileSizeInBytes = response.FileSizeInBytes;
string fileName = response.FileName;

List<byte> fileBytes = new(fileSizeInBytes);

long offset = 0;
long blockSizeDownload = 4 * 1024 * 1024; // 4 MB

// File size may be smaller than defined block size
if (fileSizeInBytes < blockSizeDownload)
{
      blockSizeDownload = fileSizeInBytes;
}

while (fileSizeInBytes > 0)
{
      // Prepare the request
      DownloadBlockRequest downLoadBlockRequest = new()
      {
            BlockLength = blockSizeDownload,
            FileContinuationToken = fileContinuationToken,
            Offset = offset
      };

      // Send the request
      var downloadBlockResponse =
                  (DownloadBlockResponse)service.Execute(downLoadBlockRequest);

      // Add the block returned to the list
      fileBytes.AddRange(downloadBlockResponse.Data);

      // Subtract the amount downloaded,
      // which may make fileSizeInBytes < 0 and indicate
      // no further blocks to download
      fileSizeInBytes -= (int)blockSizeDownload;
      // Increment the offset to start at the beginning of the next block.
      offset += blockSizeDownload;
}

return (fileBytes.ToArray(), fileName);
}

More information:

File size limits

The Organization.MaxUploadFileSize column specifies the maximum allowed size of a file in bytes for an attachment and note, and other kinds of data, such as web resource files used for model-driven apps. The maximum upload file size limit applies to the size of the file in Base64 encoding. A Base64 encoding produces a string that is larger than the original byte[] file data.

The default size is 5 MB (5,242,880 bytes) and the maximum value is 128 MB (131072000 bytes) and can be set in the email settings for the environment. More information: Manage email settings

If you try to upload a file that's too large, you get the following error:

Name: unManagedidsattachmentinvalidfilesize
Code: 0x80044a02
Number: -2147202558
Message: Attachment file size is too big.

Retrieve max upload file size

You can retrieve the maximum upload file size in a couple of ways.

Use a static method like the following GetMaxUploadFileSize to get the value.

public static int GetMaxUploadFileSize(IOrganizationService service) {

   QueryExpression query = new("organization") { 
         ColumnSet = new ColumnSet("maxuploadfilesize")
   };

   EntityCollection organizations = service.RetrieveMultiple(query);

   // There is only one row in organization table
   return (int)organizations.Entities.FirstOrDefault()["maxuploadfilesize"];
}

More information:

Change max upload file size

You can set the organization.maxuploadfilesize value in a couple of ways.

Use a static method like the following SetMaxUploadFileSize to set the maximum upload file size.

public static void SetMaxUploadFileSize(
    IOrganizationService service, 
    int maxUploadFileSizeInBytes)
{
   if (maxUploadFileSizeInBytes > 131072000 || maxUploadFileSizeInBytes < 1) {
         throw new ArgumentOutOfRangeException(nameof(maxUploadFileSizeInBytes), 
         "The maxUploadFileSizeInBytes parameter must be less than 131072000 bytes and greater than 0 bytes.");
   }

   QueryExpression query = new("organization")
   {
         ColumnSet = new ColumnSet("organizationid")
   };

   EntityCollection organizations = service.RetrieveMultiple(query);

   // There is only one row in organization table
   Entity organization = organizations.Entities.FirstOrDefault();
   organization["maxuploadfilesize"] = maxUploadFileSizeInBytes;

   service.Update(organization);
}

More information:

See also

Files and images overview
Sample: File operations with Attachments and Notes using the Dataverse SDK for .NET
Sample: Attachment and Annotation file operations using Dataverse Web API