I am trying to build interactive login functionality into my .NET 6 Winforms Application, using Identity Server 6 as my IDP.

I have modelled my client-side code on an Identity Server reference example for WinForms. The reference example project is this one:

https://github.com/IdentityModel/IdentityModel.OidcClient.Samples/tree/main/WinFormsWebView2/WinFormsWebView2

The reference project code works fine; but my project produces this exception from the WebView2 UI component:

COMException: Cannot change thread mode after it is set on EnsureCoreWebView2Async(null) C#

Both projects use the same packages: IdentityModel.OidcClient v5.2.1 and Microsoft.Web.WebView2 v1.0.1823.32

A key difference between the two projects is that mine is .NET 6 whereas the reference project is .NET Framework 4.7.2

In the SO article mentioned above, the cited root cause is calling the EnsureCoreWebView2Async method of the WebView2 component from the 'wrong' thread, which is to say, a thread other than the UI thread. However in the context of the projects discussed here, that call is being made through a call stack which consists of code that isn't mine; so I am uncertain of the best way forwards. Is there an implementation flaw in the OidcClient code when used in .NET 6; or a problem in the reference sample code; or am I doing something wrong in my own code which I have failed to spot?

This is my Program.cs code:

   internal static class Program
{
    /// <summary>
    ///  The main entry point for the application.
    /// </summary>
    [STAThread]
    static async Task Main()
    {
        // To customize application configuration such as set high DPI settings or default font,
        // see https://aka.ms/applicationconfiguration.
        ApplicationConfiguration.Initialize();

        var configuration = new ConfigurationBuilder()
            .AddJsonFile($"appsettings.json");

        var config = configuration.Build();

        Application.Run(new LandingForm(config));
    }
}

And in my LandingForm form

    public partial class LandingForm : Form
{
//
        private readonly WinFormsWebView winFormsWebView;

    public LandingForm()
    {
        InitializeComponent();
    }

    public LandingForm(IConfigurationRoot config)
    {
        InitializeComponent();

        this.config = config;

        this.winFormsWebView = new WinFormsWebView();

    }


    private void LandingForm_Load(object sender, EventArgs e)
    {

    }

    private async void LoginBtn_Click(object sender, EventArgs e)
    {
        var oidcOptions = new OidcClientOptions
        {
            Authority = config["IdentityServerAddress"].ToString(),
            ClientId = "xxx-client",
            Scope = "xxx-scopes",
            RedirectUri = "http://localhost/winforms.client",
            Browser = this.winFormsWebView
        };
        
        OidcClient _oidcClient = new OidcClient(oidcOptions);
                    
        LoginResult loginResult = await _oidcClient.LoginAsync();

        if (loginResult.IsError)
        {
            MessageBox.Show(loginResult.Error, "Login", MessageBoxButtons.OK, MessageBoxIcon.Error);
            return;
        }


        vr2Button.Enabled = true;
    }
}

In my project, the WinFormsWebView.cs code is identical to the working example code (other than for residing in a different namespace) but I post it fully here for ease of reference, and I have added a comment where the Exception occurs:

    public class WinFormsWebView : IBrowser
{
    private readonly Func<Form> _formFactory;
    private BrowserOptions _options;

    public WinFormsWebView(Func<Form> formFactory)
    {
        _formFactory = formFactory;
    }

    public WinFormsWebView(string title = "Authenticating ...", int width = 1024, int height = 768)
        : this(() => new Form
        {
            Name = "WebAuthentication",
            Text = title,
            Width = width,
            Height = height
        })
    { 
    }

    public async Task<BrowserResult> InvokeAsync(BrowserOptions options, CancellationToken token = default)
    {
        _options = options;

        using (var form = _formFactory.Invoke())
        {
            using (var webView = new Microsoft.Web.WebView2.WinForms.WebView2()
            {
                Dock = DockStyle.Fill
            })
            {
                var signal = new SemaphoreSlim(0, 1);

                var browserResult = new BrowserResult
                {
                    ResultType = BrowserResultType.UserCancel
                };

                form.FormClosed += (o, e) =>
                {
                    signal.Release();
                };

                webView.NavigationStarting += (s, e) =>
                {
                    if (IsBrowserNavigatingToRedirectUri(new Uri(e.Uri)))
                    {
                        e.Cancel = true;

                        browserResult = new BrowserResult()
                        {
                            ResultType = BrowserResultType.Success,
                            Response = new Uri(e.Uri).AbsoluteUri
                        };

                        signal.Release();
                        form.Close();
                    }
                };

                try
                {
                    form.Controls.Add(webView);
                    webView.Show();

                    form.Show();

// EXCEPTION OCCURS ON THIS NEXT LINE 
                    // Initialization
                    await webView.EnsureCoreWebView2Async(null);

                    // Delete existing Cookies so previous logins won't remembered
                    webView.CoreWebView2.CookieManager.DeleteAllCookies();

                    // Navigate
                    webView.CoreWebView2.Navigate(_options.StartUrl);

                    await signal.WaitAsync();
                }
                finally
                {
                    form.Hide();
                    webView.Hide();
                }

                return browserResult;
            }
        }
    }

    private bool IsBrowserNavigatingToRedirectUri(Uri uri)
    {
        return uri.AbsoluteUri.StartsWith(_options?.EndUrl);
    }
}

I can envisage how the root cause described in the other SO post may apply to my project, because I'm ultimately calling EnsureCoreWebView2Async() from the async LoginBtn_Click() method and as this method is async I guess it doesn't run on the UI thread. However I am not completely certain about this, because my code is not materially different in this respect to the example reference code, which works fine (does not raise the exception).

1

There are 1 answers

0
user3561406 On

In Program.cs use this:

[STAThread]
        static void Main()
        {}

Instead of using the async equivalent:

[STAThread]
        static async Task Main()
        {}