Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Workaround for Blazor WASM to connect to Azure Cosmos with .NET 5 #26942

Closed
dotnetspark opened this issue Oct 15, 2020 · 8 comments
Closed

Workaround for Blazor WASM to connect to Azure Cosmos with .NET 5 #26942

dotnetspark opened this issue Oct 15, 2020 · 8 comments
Assignees
Labels
area-blazor Includes: Blazor, Razor Components ✔️ Resolution: Answered Resolved because the question asked by the original author has been answered. Status: Resolved
Milestone

Comments

@dotnetspark
Copy link

Hi @danroth27. I have a vanilla Blazor WASM (standalone) app which basically as I type autocompletes results from Azure CosmosDB. The Autocomplete razor file is a modified version of the one here. After upgrading to .NET 5 PNS exception started to be thrown (see #26450).

Autocomplete.razor

@using SampleCosmosDB.Components
@inherits InputText

<input @bind="BoundValue" @bind:event="oninput" @onblur="Hide" autocomplete="off" @attributes="@AdditionalAttributes" class="@CssClass" />

<div role="listbox" class="autocomplete-suggestions @(visible ? "visible" : "")">
    @foreach (var choice in currentChoices.Take(3))
    {
        <div role="option" @onclick="() => ChooseAsync(choice)">@choice.Name</div>
    }
</div>

@code {
    bool visible;
    AutocompleteProductItem[] currentChoices = Array.Empty<AutocompleteProductItem>();

    [Parameter] public Func<string, Task<IEnumerable<AutocompleteProductItem>>> Choices { get; set; }
    [Parameter] public EventCallback<string> OnItemChosen { get; set; }

    string BoundValue
    {
        get => CurrentValueAsString;
        set
        {
            CurrentValueAsString = value;
            _ = UpdateAutocompleteOptionsAsync();
        }
    }

    async Task UpdateAutocompleteOptionsAsync()
    {
        if (EditContext.GetValidationMessages(FieldIdentifier).Any())
        {
            // If the input is invalid, don't show any autocomplete options
            currentChoices = Array.Empty<AutocompleteProductItem>();
        }
        else
        {
            var valueSnapshot = CurrentValueAsString;
            var suppliedChoices = (await Choices(valueSnapshot)).ToArray();

            // By the time we get here, the user might have typed other characters
            // Only use the result if this still represents the latest entry
            if (CurrentValueAsString == valueSnapshot)
            {
                currentChoices = suppliedChoices;
                visible = currentChoices.Any();
                StateHasChanged();
            }
        }
    }

    void Hide() => visible = false;

    Task ChooseAsync(AutocompleteProductItem choice)
    {
        CurrentValueAsString = choice.Name;
        Hide();
        return OnItemChosen.InvokeAsync(choice.Id);
    }
}

AutocompleteProductItem

namespace SampleCosmosDB.Components
{
    public class AutocompleteProductItem
    {
        public string Id { get; set; }
        public string Name { get; set; }
    }
}

Index.razor

@page "/"
@using Microsoft.Extensions.Localization
@using SampleCosmosDB.Components
@using Microsoft.Azure.Cosmos
@using Microsoft.Azure.Cosmos.Linq
@using System.Linq
@inject NavigationManager Navigation
@inject IStringLocalizer<App> Localize

<main class="container">
    <EditForm class="home-options" Model="this" OnValidSubmit="FindProduct">
        <DataAnnotationsValidator />

        <p>@Localize["EnterProductName"]</p>
        <Autocomplete @bind-Value="@ProductName" 
                       Choices="@GetProductNameAutocompleteItems"
                       OnItemChosen="EditProduct" 
                       class="find-by-product-name" 
                       placeholder="@Localize["ProductNamePlaceHolder"]" />
        <ValidationMessage For="() => ProductName" />
    </EditForm>
</main>

@code {
    public string ProductName { get; set; }

    async Task<IEnumerable<AutocompleteProductItem>> GetProductNameAutocompleteItems(string prefix)
    {
        var autocompleteProductItems = new List<AutocompleteProductItem>();
        try
        {
            var iterator = CosmosClient.GetContainer("InventoryDataStore", "Products")
                .GetItemQueryIterator<dynamic>(queryText: "SELECT * FROM c");
            while (iterator.HasMoreResults)
            {
                var results = await iterator.ReadNextAsync();
                foreach (var product in results)
                {
                    autocompleteProductItems.AddRange(
                        from document in await iterator.ReadNextAsync()
                        select new AutocompleteProductItem {Id = document["id"], Name = document["name"]});
                }
            }
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
        }

        return autocompleteProductItems;
    }

    void EditProduct(string id)
    {
        if (!string.IsNullOrEmpty(id))
        {
            Navigation.NavigateTo($"product/{id}");
        }
    }

    void FindProduct()
    {
        //TODO
    }
}

ViewProduct.razor

@page "/product/view/{id}"

<h3>View Product @Id</h3>

@code {
    [Parameter] public string Id { get; set; }
}
@Pilchie Pilchie added the area-blazor Includes: Blazor, Razor Components label Oct 15, 2020
@danroth27
Copy link
Member

@ylr-research Could you please share the configuration of the CosmosClient?

@dotnetspark
Copy link
Author

        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("app");

            builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
            builder.Services.AddHttpClient();
            builder.Services.AddSingleton<CosmosClient>(serviceProvider =>
            {
                IHttpClientFactory httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();

                CosmosClientOptions cosmosClientOptions = new CosmosClientOptions
                {
                    HttpClientFactory = httpClientFactory.CreateClient,
                    ConnectionMode = ConnectionMode.Gateway
                };

                return new CosmosClient("<connectionstring>;", cosmosClientOptions);
            });
            builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");

            await builder.Build().RunAsync();
        }

@danroth27
Copy link
Member

danroth27 commented Oct 15, 2020

@ylr-research Because Blazor WebAssembly apps are public clients that can't keep secrets you need to use resource tokens. @JeremyLikness wrote up a nice blog post on how to do this: https://blog.jeremylikness.com/blog/azure-ad-secured-serverless-cosmosdb-from-blazor-webassembly/. However, I don't think we've tested this setup with .NET 5 yet, but I believe it should work. Can you confirm whether you are using this approach and it if works for you?

@dotnetspark
Copy link
Author

@danroth27 I'm not using his approach. I'll give it a try now with RC2 and let you know how it goes.

@dotnetspark
Copy link
Author

@danroth27, I do have a question though. How was it working before, when the APIs were Mono based? I used to just add a AzureCosmos nuget package, register a client and everything worked. Blazor WASM can't keep a secret now neither before. Most importantly, it is my understanding this issue is to be resolved with .NET 6 wave. Would that means I won't need to follow the above approach and keep doing it as before?

@danroth27
Copy link
Member

How was it working before, when the APIs were Mono based?

For Blazor WebAssembly 3.2 we shipped the Mono implementations of the crypto APIs. So if you tried to use HMAC with 3.2 it would work. However, using HMAC means you need to have access to a secret key, which there is no way to protect in a public client app like a browser app. So it worked, but it would expose the key. I'm guessing the key is embedded in the connection string?

In .NET 5, as part of unifying on the .NET 5 framework libraries, we had to remove the crypto implementations because of issues with being able to service them. We generally prefer to rely on the platform crypto implementations, which get serviced with the platform. Browsers do surface crypto support, but in a form that doesn't match well with the .NET crypto APIs. That's what we're planning to address in .NET 6. With .NET 5 you need to access the browser's crypto support through JS interop.

But even with crypto support coming back in .NET 6, there still won't be a safe way to store a secret in the browser, so generating HMACs still won't be recommended. The approach using resource tokens will still be the preferred approach.

@dotnetspark
Copy link
Author

Fair enough. Thanks for your prompt response.

@mkArtakMSFT mkArtakMSFT added this to the Discussions milestone Oct 19, 2020
@danroth27 danroth27 added the ✔️ Resolution: Answered Resolved because the question asked by the original author has been answered. label Oct 19, 2020
@ghost ghost added the Status: Resolved label Oct 19, 2020
@ghost
Copy link

ghost commented Oct 21, 2020

This issue has been resolved and has not had any activity for 1 day. It will be closed for housekeeping purposes.

See our Issue Management Policies for more information.

This issue was closed.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-blazor Includes: Blazor, Razor Components ✔️ Resolution: Answered Resolved because the question asked by the original author has been answered. Status: Resolved
Projects
None yet
Development

No branches or pull requests

4 participants