Azure-sdk-for-net: [FEATURE REQ] Make ConnectionString class public

Created on 25 Apr 2020  路  12Comments  路  Source: Azure/azure-sdk-for-net

Azure.Core

Make ConnectionString class public so developers can leverage it broadly, as "Key=Value;" format is widely adopted in Azure.

For example, Storage SDK used to have CloudStorageAccount.Parse. It doesn't exist in v12, and it's Storage-specific.
Now, in order to use Azure.Storage.StorageSharedKeyCredential with a connection string (together with BlobSasBuilder, for example) , they have to extract AccountKey value from the connection string themselves.

Client Service Attention Storage customer-reported feature-request needs-team-attention

Most helpful comment

We have the same issue. We want to return an URL, that allows a web browser to download the file directly from Azure Blob Storage. However we only store the ConnectionString in our configuration.

My current workaround looks like this

    public string DownloadUrl(BlobContainerClient client, string blobFileName, string downloadFileName, string azureStorageConnectionString)
    {
        // HACK: Resolve internal StorageConnectionString to parse storage connection string
        var storageConnectionStringType = Type.GetType("Azure.Storage.StorageConnectionString, Azure.Storage.Common");

        var storageConnectionString = storageConnectionStringType
            .GetMethod("Parse", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static)
            .Invoke(null, new object[] { azureStorageConnectionString });

        var storageCredentials = (StorageSharedKeyCredential)storageConnectionStringType
            .GetProperty("Credentials", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public)
            .GetValue(storageConnectionString);

        // Resolve the details about our blob
        var blobClient = client.GetBlobClient(blobFileName);
        BlobSasBuilder blobSasBuilder = new BlobSasBuilder()
        {
            BlobContainerName = blobClient.BlobContainerName,
            BlobName = blobClient.Name,
            Protocol = storageCredentials.AccountName == "devstoreaccount1" ? SasProtocol.HttpsAndHttp : SasProtocol.Https,
            CacheControl = "no-store",
            ExpiresOn = DateTimeOffset.UtcNow.AddMinutes(10)
        };
        blobSasBuilder.SetPermissions(BlobSasPermissions.Read); // Only allow downloading the file

        // Ensure browser downloads the file instead of loads it in the current page, and if required change filename
        var contentDispositionHeader = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment");
        if (downloadFileName != null)
        {
            contentDispositionHeader.FileName = downloadFileName;
        }
        blobSasBuilder.ContentDisposition = contentDispositionHeader.ToString();

        // Return the full url including sasToken
        string sasToken = blobSasBuilder.ToSasQueryParameters(storageCredentials).ToString();
        return blobClient.Uri.AbsoluteUri + "?" + sasToken;
    }

All 12 comments

I'm not sure I understand the use case for storage.

For SAS Url there is a ctor that doesn't take a credential https://docs.microsoft.com/en-us/dotnet/api/azure.storage.blobs.blobclient.-ctor?view=azure-dotnet#Azure_Storage_Blobs_BlobClient__ctor_System_Uri_Azure_Storage_Blobs_BlobClientOptions_ but I think I'm missing something because SAS is not a connection string.

In general our goal was to free customers from having to parse connection strings and expose ways to consume entire connection and parse it internally.

Can you please provide more details on when you have a connection string but have to manually parse it to create a valid client?

BlobClient itself is not as issue. The problem I faced is when I want to create an SAS-token myself, StorageSharedKeyCredential requires AccountName and AccountKey separately.
2 more examples on top of my head are:

  • Configuring Blob Storage on IoT Edge (they want those values separately again)
  • Creating a custom SAS for Service Bus
    In both cases I need to deal with keys, so if initially my service was configured with a connection string, I have to parse it myself.

We have the same issue. We want to return an URL, that allows a web browser to download the file directly from Azure Blob Storage. However we only store the ConnectionString in our configuration.

My current workaround looks like this

    public string DownloadUrl(BlobContainerClient client, string blobFileName, string downloadFileName, string azureStorageConnectionString)
    {
        // HACK: Resolve internal StorageConnectionString to parse storage connection string
        var storageConnectionStringType = Type.GetType("Azure.Storage.StorageConnectionString, Azure.Storage.Common");

        var storageConnectionString = storageConnectionStringType
            .GetMethod("Parse", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static)
            .Invoke(null, new object[] { azureStorageConnectionString });

        var storageCredentials = (StorageSharedKeyCredential)storageConnectionStringType
            .GetProperty("Credentials", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public)
            .GetValue(storageConnectionString);

        // Resolve the details about our blob
        var blobClient = client.GetBlobClient(blobFileName);
        BlobSasBuilder blobSasBuilder = new BlobSasBuilder()
        {
            BlobContainerName = blobClient.BlobContainerName,
            BlobName = blobClient.Name,
            Protocol = storageCredentials.AccountName == "devstoreaccount1" ? SasProtocol.HttpsAndHttp : SasProtocol.Https,
            CacheControl = "no-store",
            ExpiresOn = DateTimeOffset.UtcNow.AddMinutes(10)
        };
        blobSasBuilder.SetPermissions(BlobSasPermissions.Read); // Only allow downloading the file

        // Ensure browser downloads the file instead of loads it in the current page, and if required change filename
        var contentDispositionHeader = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment");
        if (downloadFileName != null)
        {
            contentDispositionHeader.FileName = downloadFileName;
        }
        blobSasBuilder.ContentDisposition = contentDispositionHeader.ToString();

        // Return the full url including sasToken
        string sasToken = blobSasBuilder.ToSasQueryParameters(storageCredentials).ToString();
        return blobClient.Uri.AbsoluteUri + "?" + sasToken;
    }

@tg-msft, tell me if I'm wrong but aren't you addressing the later issue with the new SasBuilder design and allowing to create it from the client?

The SAS changes won't help with this problem - you'll still need to pull a credential for signing from somewhere.

I wouldn't expose the entirety of the existing StorageConnectionString class because it's got a larger API surface area than we need here. We should look into something like a StorageConnectionBuilder if it's possible to create a utility analogous to BlobUriBuilder in Azure.Storage.Common. The trickier part will be serializing out a TokenCredential which may force us to only support parsing instead of creation.

This shouldn't be assigned to Azure.Core so I'm changing everything to Storage.

Thanks for the feedback! We are routing this to the appropriate team for follow-up. cc @xgithubtriage.

Having StorageConnectionString functionality hidden forces workarounds that every developer working with a connection string will end up implementing. I have a need to parse a connection string to extract information and rather not use reflection or duplicated the code.

Here's an example of how having this functionality public would save unnecessary issues and save me time:

We frequently use these things as well and would be happy to have it public

We appreciated the connection string _validation_ we got with TryParse. But we also have some scenarios were we have abstracted some of the work and need to generally build and/or validate URLs, etc. using information gleaned from the connection string. For example, ensuring that a blob URL is part of the same account/container as another component. At the moment, we are keeping a reference to the old library just for CloudStorageAccount.[Try]Parse, keeping around the account object to use properties off it (since reflection would get ugly there). The CloudStorageAccount also gives you access to the SecondaryUri's allowing you to pass them via BlobClientOptions (I know of no other way to retrieve this value?)

We were trying to use the BlobUriBuilder to construct a dynamic blob URI, only to realize we still needed to pass in some credentials and the only way to get those was to parse them out of the connection string

I also used CloudStorageAccount.Parse(_connectionString) method for connection string validation and extracting parts and had to copy-paste entire internal StorageConnectionString class for this.

It looks like I also need the StorageConnectionString class to extract the 'StorageAccountName' and 'StorageAccountKey' from the connection string to create a SAS when I follow the documentation.

Off-topic but related because this is about generating a SAS:
Unfortunately, the following code doesn't work. The 'GetBlobClient()' on the container doesn't invoke the correct constructor overload on BlobClient. In the current implementation '_storageSharedKeyCredential' remains empty.

var connectionString = "***";            
var containerName = "test";

BlobContainerClient container = new BlobContainerClient(connectionString, containerName);
foreach (var blobItem in container.GetBlobs())
{
    BlobClient blobClient = container.GetBlobClient(blobItem.Name);

    if (blobClient.CanGenerateSasUri)
    {
        var expiresOn = DateTimeOffset.Now.Add(TimeSpan.FromHours(5));
        var sas = blobClient.GenerateSasUri(BlobSasPermissions.Read, expiresOn);

        Console.WriteLine($"{blobItem.Name}: {sas}");
    }
}

But if you create the BlobClient with the correct constructor overload (with the connection string) there is no need to parse the connection string with the 'StorageConnectionString' class to create a SAS:

var connectionString = "***";            
var containerName = "test";

BlobContainerClient container = new BlobContainerClient(connectionString, containerName);
foreach (var blobItem in container.GetBlobs())
{
    BlobClient blobClient = new BlobClient(connectionString, containerName, blobItem.Name);

    if (blobClient.CanGenerateSasUri)
    {
        var expiresOn = DateTimeOffset.Now.Add(TimeSpan.FromHours(5));
        var sas = blobClient.GenerateSasUri(BlobSasPermissions.Read, expiresOn);

        Console.WriteLine($"{blobItem.Name}: {sas}");
    }
}

It would be better if the 'GetBlobClient()' method on the container would be able to return a BlobClient were the property of 'CanGenerateSasUri' is true after it's initialized with a connection string. With this solution, you're forced to pass the connection string throughout the application. I prefer to get it from the config/KeyVault in the startup class and only use the BlobContainerClient.

Hi @StannieV . I already created an issue for the off topic GetBlobClient issue you are referring to and am looking to resolve it with a PR I put out yesterday. Please look forward to the next release when the fix comes out. Thanks!

If you have anything to add to the relevant to this thread, feel free to, otherwise please add your concerns to the bug you are referring to the issue I linked, so we can stay organized with the issue at hand.

Was this page helpful?
0 / 5 - 0 ratings