Some random rambelings bumping my head with Azure Blob Storage with access to Private Files (App Internals) And Public Web Assets (Image the app displays)
I needed two different Azure Blob Storage setups for the same app: one for private chatbot uploads and one for public web assets that the browser could read directly. I wanted the upload flow to stay simple, but I also wanted the storage behaviour to be explicit.
This is the setup I ended up with, and the same shape is what I used to wire up the public background image upload for the chatbot UI.
1. Create the storage accounts with Terraform
I split this into two Terraform files so the private and public storage behaviours were obvious. The names need to be unique for the region, I replaced 00000001 and 000000002 with the end of a random GUID, example from 14db1e4e-5c64-4aa6-8de5-0f948e30ddee I would have used 0f948e30ddee.
The public assets account is the one I used for the chatbot background image. It needs public blob access enabled, because the browser reads the image directly from the blob URL.
1 | # blob-storage-public-assets.tf |
The private storage account is for the existing chatbot file uploads. That one should stay private so the app can control access and keep those files out of the public path.
1 | # blob-storage.tf |
The important difference is that the public account allows anonymous blob reads, while the private account does not.
Docs: Azure Blob Storage overview • Authorize access to blobs
2. Find the connection string in Azure Portal
Once the storage accounts existed, I needed the connection string for each one. The easiest place to find it is in the Azure portal.
- Open the storage account in the Azure portal.
- Go to Security + networking.
- Open Access keys.
- Copy the Connection string value from one of the keys.
It will look like this:
1 | DefaultEndpointsProtocol=https;AccountName=<storage-account-name>;AccountKey=<storage-key>;EndpointSuffix=core.windows.net |
That is the exact value I used in app settings for the blob storage configuration. The key detail is that it must match the storage account that owns the container you want to use.
So for this app:
PrivateFiles.ConnectionStringpoints at the storage account that ownschatbot-filesPublicFiles.ConnectionStringpoints at the storage account that ownschatbot-web-assets
Docs: View account access keys • Configure Azure Storage connection strings
3. Put the values into app settings
In the app, I split the blob config into two sections so the code could stay explicit about which storage path it was using.
1 | { |
That setup lets the app use one storage path for private files and another for public assets without mixing the two concerns together.
4. Wire the configuration into the C# app
The app uses an options class for the blob settings, and then registers a single storage service that knows about both containers.
1 | // AzureBlobStorageOptions.cs |
1 | // Program.cs |
The storage implementation then creates two BlobContainerClient instances: one for the private container and one for the public container.
1 | public sealed class AzureBlobClientFileStorage : IClientFileStorage |
That is the core of the split: private uploads stay on the private client, while public assets use the public client.
Docs: ASP.NET Core configuration • Azure.Storage.Blobs client library
5. Upload the background image from onboarding
The onboarding flow now accepts an uploaded image, validates it, and sends it to the public blob container.
1 | private async Task UploadBackgroundImageAsync(Guid clientUid, IFormFile backgroundImage, CancellationToken cancellationToken) |
The validation step is intentionally strict so the image dimensions match the expected hero image size.
1 | public static void ValidateUploadImageDimensions(Stream imageStream, string fileName) |
That means the app expects a very specific image size before it will accept the upload.
6. Serve the uploaded image in chat
Once the image is uploaded, the app stores the relative blob path and builds a URL for the chat page. The public base URL comes from the ChatUi section.
1 | private static string BuildBackgroundImageUrl(ChatUiOptions chatUiSettings, string? backgroundImagePath) |
That is what lets the chatbot render a per-bot background image when one exists, and fall back to a local default image when it does not.
7. What I learned
The key thing for me was to keep the two storage paths separate from the start. If I had used a single blob account for both private files and public assets, the configuration would have become ambiguous very quickly.
Using two storage accounts and two config sections made the app easier to reason about:
- private uploads stay private
- public assets can be served directly by URL
- the connection string is easy to find and easy to swap if the environment changes
That is the shape I would use again if I needed the same pattern in another app.