Blazor WebAssembly with Cookie Authentication

In the Backend I’ve started by adding Cookie Authentication in the startup and override the OnRedirectToLogin event handlers, so they are going to return a HTTP Status Code 401 to the consumer. This is handled in the Exception Handling Middleware and not shown here.

// Cookie Authentication
builder.Services
    .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.Cookie.HttpOnly = true;
        options.Cookie.SameSite = SameSiteMode.Lax; // We don't want to deal with CSRF Tokens

        options.Events.OnRedirectToAccessDenied = context => throw new AuthenticationFailedException();
        options.Events.OnRedirectToLogin = context => throw new AuthenticationFailedException();
    });

The user is signed in using HttpContext#SignInAsync with something along the lines of a AuthenticationController:

// Licensed under the MIT license. See LICENSE file in the project root for full license information.

// ...

namespace RebacExperiments.Server.Api.Controllers
{
    public class AuthenticationController : ODataController
    {
        // ...

        [HttpPost("odata/SignInUser")]
        public async Task SignInUser([FromServices] IUserService userService, [FromBody] ODataActionParameters parameters, CancellationToken cancellationToken)
        {
            // ...

            // Create the ClaimsPrincipal
            var claimsIdentity = new ClaimsIdentity(userClaims, CookieAuthenticationDefaults.AuthenticationScheme);
            var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);

            // It's a valid ClaimsPrincipal, sign in
            await HttpContext.SignInAsync(claimsPrincipal, new AuthenticationProperties { IsPersistent = rememberMe });
            // ...
        }
    }
}        

You can then open your Browsers Developer Tools and see, that an (encrypted) Cookie has been created.

Once we have successfully logged in and got our Cookie, we need to send the Authorization Cookie on every request to the API. So we start by adding a CookieDelegatingHandler, that does just that:

// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.AspNetCore.Components.WebAssembly.Http;
using RebacExperiments.Blazor.Shared.Logging;

namespace RebacExperiments.Blazor.Infrastructure
{
    public class CookieDelegatingHandler : DelegatingHandler
    {
        private readonly ILogger _logger;

        public CookieDelegatingHandler(ILogger logger)
        {
            _logger = logger;
        }

        protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            _logger.TraceMethodEntry();

            request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);

            return await base.SendAsync(request, cancellationToken);
        }
    }
}

The CookieDelegatingHandler needs to be registered for the HttpClient, so we use the IHttpClientBuilder#AddHttpMessageHandler extension method like this:

builder.Services
    .AddHttpClient(client => client.BaseAddress = new Uri("https://localhost:5000"))
    .AddHttpMessageHandler();

The Blazor Authorization Infrastructure uses an AuthenticationStateProvider to pass the user information into the components. We want to persist the user information across page refreshes, so the local storage of a Browser seems to be a good place to persist it.

We don’t need to take additional dependencies, just write a small LocalStorageService.

// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.JSInterop;
using System.Text.Json;

namespace RebacExperiments.Blazor.Infrastructure
{
    public class LocalStorageService
    {
        private IJSRuntime _jsRuntime;

        public LocalStorageService(IJSRuntime jsRuntime)
        {
            _jsRuntime = jsRuntime;
        }

        public async Task GetItemAsync(string key)
        {
            var json = await _jsRuntime.InvokeAsync("localStorage.getItem", key);

            if (json == null)
            {
                return default;
            }

            return JsonSerializer.Deserialize(json);
        }

        public async Task SetItem(string key, T value)
        {
            await _jsRuntime.InvokeVoidAsync("localStorage.setItem", key, JsonSerializer.Serialize(value));
        }

        public async Task RemoveItemAsync(string key)
        {
            await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", key);
        }
    }
}

And register it in the Program.cs.

builder.Services.AddSingleton();

We can then implement an AuthenticationStateProvider, that allows us to set a User (think of User Profile) and notify subscribers about the new AuthenticationState. The User is persisted using our LocalStorageService.

// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.AspNetCore.Components.Authorization;
using RebacExperiments.Shared.ApiSdk.Models;
using System.Security.Claims;

namespace RebacExperiments.Blazor.Infrastructure
{
    public class CustomAuthenticationStateProvider : AuthenticationStateProvider
    {
        private const string LocalStorageKey = "currentUser";

        private readonly LocalStorageService _localStorageService;

        public CustomAuthenticationStateProvider(LocalStorageService localStorageService)
        {
            _localStorageService = localStorageService;
        }

        public override async Task GetAuthenticationStateAsync()
        {
            var currentUser = await GetCurrentUserAsync();

            if(currentUser == null)
            {
                return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
            }

            Claim[] claims = [
                new Claim(ClaimTypes.NameIdentifier, currentUser.Id!.ToString()!),
                new Claim(ClaimTypes.Name, currentUser.LogonName!.ToString()!),
                new Claim(ClaimTypes.Email, currentUser.LogonName!.ToString()!)
            ];

            var authenticationState = new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(claims, authenticationType: nameof(CustomAuthenticationStateProvider))));

            return authenticationState;
        }

        public async Task SetCurrentUserAsync(User? currentUser)
        { 
            await _localStorageService.SetItem(LocalStorageKey, currentUser);

            NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
        }

        public Task GetCurrentUserAsync() => _localStorageService.GetItemAsync(LocalStorageKey);
    }
}

Don’t forget to register all authentication related services.

builder.Services.AddAuthorizationCore();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddSingleton();
builder.Services.AddSingleton(s => s.GetRequiredService());

In the App.razor add the CascadingAuthenticationState and AuthorizeRouteView components, so the AuthenticationState flows down to the components automagically.

@using Microsoft.AspNetCore.Components.Authorization



    
        
            
        
        
            Not found
            
                

Sorry, there's nothing at this address.

In the MainLayout, you can then use the <AuthorizeView> component, that allows to check, if a given user is authorized or not. If the User is not authorized, we are redirecting to the Login page using a <RedirectToLogin> component.

@using Microsoft.AspNetCore.Components
@using System.Runtime.InteropServices
@using RebacExperiments.Blazor.Components
@using RebacExperiments.Blazor.Components.RedirectToLogin
@namespace RebacExperiments.Blazor.Shared

Relationship-based Experiments with ASP.NET Core OData

    
        
    
    
        
    

The <RedirectToLogin> component simply uses the NavigationManager to navigate to the Login Page.

@inject NavigationManager Navigation

@code {
    protected override void OnInitialized()
    {
        var baseRelativePath = Navigation.ToBaseRelativePath(Navigation.Uri);

        if(string.IsNullOrWhiteSpace(baseRelativePath))
        {
            Navigation.NavigateTo($"Login", true);
        } else {
            Navigation.NavigateTo($"Login?returnUrl={Uri.EscapeDataString(baseRelativePath)}", true);
        }
    }
}

Now what happens, if the Web service returns a HTTP Status Code 401 (Unauthorized) and we still have the User in the Local Storage? Yes, it will be out of sync. So we need to update the AuthenticationState and clear the User information, if the service returns a HTTP Status Code 401.

This can be done by using a DelegatingHandler, that takes a dependency on our CustomAuthenticationStateProvider, and sets the current User to null. This should inform all subscribers, that we are now unauthorized to perform actions.

// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using RebacExperiments.Blazor.Shared.Logging;

namespace RebacExperiments.Blazor.Infrastructure
{
    public class UnauthorizedDelegatingHandler : DelegatingHandler
    {
        private readonly ILogger _logger;

        private readonly CustomAuthenticationStateProvider _customAuthenticationStateProvider;

        public UnauthorizedDelegatingHandler(ILogger logger, CustomAuthenticationStateProvider customAuthenticationStateProvider)
        {
            _logger = logger;
            _customAuthenticationStateProvider = customAuthenticationStateProvider;
        }

        protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            _logger.TraceMethodEntry();

            var response = await base.SendAsync(request, cancellationToken);

            if(response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
            {
                var currentUser = await _customAuthenticationStateProvider.GetCurrentUserAsync();

                if(currentUser != null)
                {
                    await _customAuthenticationStateProvider.SetCurrentUserAsync(null);
                }
            }

            return response;
        }
    }
}

You need to add the UnauthorizedDelegatingHandler to the HttpClient.

builder.Services
    .AddHttpClient(client => client.BaseAddress = new Uri("https://localhost:5000"))
    .AddHttpMessageHandler()
    .AddHttpMessageHandler();

Now let’s connect everything!

I want the Login Page to have its own layout and don’t want to use the MainLayout. So I am adding an <EmptyLayout> component.

@inherits LayoutComponentBase

@Body

This EmptyLayout is then used as the Layout for the Login Page, so I can style it to my needs. The example uses a <SimpleValidator> for validation, that has been developed in a previous article. You could easily replace it with a <DataAnnotationsValidator>, to use Blazors built-in validations.

@page "/Login"
@layout EmptyLayout

@using RebacExperiments.Shared.ApiSdk

@inject ApiClient ApiClient
@inject IStringLocalizer Loc
@inject NavigationManager NavigationManager
@inject CustomAuthenticationStateProvider AuthStateProvider

Login

Login @if(!string.IsNullOrWhiteSpace(ErrorMessage)) { }

Let’s take a look at the Login.razor.cs Code-Behind.

The Login#SignInUserAsync methods starts by logging the User in. The Server will return the HttpOnly Cookie, that’s going to be sent with every request to the API. To get the User information for populating the AuthenticationState the /Me endpoint is called. The User is the set in the AuthStateProvider and we navigate to our application.

// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.AspNetCore.Components;
using RebacExperiments.Shared.ApiSdk.Odata.SignInUser;
using System.ComponentModel.DataAnnotations;
using RebacExperiments.Blazor.Infrastructure;
using Microsoft.Extensions.Localization;

namespace RebacExperiments.Blazor.Pages
{
    public partial class Login
    {
        /// 
        /// Data Model for binding to the Form.
        /// 
        private sealed class InputModel
        {
            /// 
            /// Gets or sets the Email.
            /// 
            [Required]
            [EmailAddress]
            public required string Email { get; set; }

            /// 
            /// Gets or sets the Password.
            /// 
            [Required]
            [DataType(DataType.Password)]
            public required string Password { get; set; }

            /// 
            /// Gets or sets the RememberMe Flag.
            /// 
            [Required]
            public bool RememberMe { get; set; } = false;
        }

        // Default Values.
        private static class Defaults
        {
            public static class Philipp
            {
                public const string Email = "[email protected]";
                public const string Password = "5!F25GbKwU3P";
                public const bool RememberMe = true;
            }

            public static class MaxMustermann
            {
                public const string Email = "[email protected]";
                public const string Password = "5!F25GbKwU3P";
                public const bool RememberMe = true;
            }
        }


        /// 
        /// If a Return URL is given, we will navigate there after login.
        /// 
        [SupplyParameterFromQuery(Name = "returnUrl")]
        private string? ReturnUrl { get; set; }

        /// 
        /// The Model the Form is going to bind to.
        /// 
        [SupplyParameterFromForm]
        private InputModel Input { get; set; } = new()
        {
            Email = Defaults.Philipp.Email,
            Password = Defaults.Philipp.Password,
            RememberMe = Defaults.Philipp.RememberMe
        };

        /// 
        /// Error Message.
        /// 
        private string? ErrorMessage;

        /// 
        /// Signs in the User to the Service using Cookie Authentication.
        /// 
        /// 
        public async Task SignInUserAsync()
        {
            ErrorMessage = null;

            try
            {
                await ApiClient.Odata.SignInUser.PostAsync(new SignInUserPostRequestBody
                {
                    Username = Input.Email,
                    Password = Input.Password,
                    RememberMe = true
                });

                // Now refresh the Authentication State:
                var me = await ApiClient.Odata.Me.GetAsync();

                await AuthStateProvider.SetCurrentUserAsync(me);

                var navigationUrl = GetNavigationUrl();

                NavigationManager.NavigateTo(navigationUrl);
            }
            catch
            {
                ErrorMessage = Loc["Login_Failed"];
            }
        }

        private string GetNavigationUrl()
        {
            if(string.IsNullOrWhiteSpace(ReturnUrl))
            {
                return "/";
            }

            return ReturnUrl;
        }

        /// 
        /// Validates an <see cref="InputModel"/>.
        /// 
        /// InputModel to validate
        /// The list of validation errors for the EditContext model fields
        private IEnumerable ValidateInputModel(InputModel model)
        {
            if(string.IsNullOrWhiteSpace(model.Email))
            {
                yield return new ValidationError
                {
                    PropertyName = nameof(model.Email),
                    ErrorMessage = Loc.GetString("Validation_IsRequired", nameof(model.Email))
                };
            }

            if(string.IsNullOrWhiteSpace(model.Password))
            {
                yield return new ValidationError
                {
                    PropertyName = nameof(model.Password),
                    ErrorMessage = Loc.GetString("Validation_IsRequired", nameof(model.Password))
                };
            }
        }
    }
}

In the Login.razor.css we add a bit of styling.

@keyframes fade {
    from {
        opacity: 0;
    }

    to {
        opacity: 1;
    }
}

.container {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    animation: fade 0.2s ease-in-out forwards;
}

h1 {
    font-size: 35px;
    font-weight: 100;
    text-align: center;
}

Conclusion

And that’s it! You will now be able to use Cookie Authentication in your Blazor Application.

References
https://www.bytefish.de/blog/blazor_wasm_cookie_authentication.html

Custom NavLink to Support Complex URL Matching in ASP.NET Blazor

To make the “Home” NavLink selected when navigating to http://localhost:3002/ or http://localhost:3002/Monitoring/, you can adjust the Match attribute of the NavLink to use a custom match condition. Blazor does not support complex URL matching directly out of the box, but you can achieve this by creating a custom CustomNavLink component.

CustomNavLink.razor

@inject NavigationManager Navigation
@implements IDisposable

@if (IsActive)
{
    <NavLink class="nav-link active" href="" Match="NavLinkMatch.All">
        @ChildContent
    </NavLink>
}
else
{
    <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
        @ChildContent
    </NavLink>
}

@code {
    [Parameter] public RenderFragment ChildContent { get; set; } = default!;

    private bool IsActive { get; set; }

    protected override void OnInitialized()
    {
        Navigation.LocationChanged += OnLocationChanged;
    }
    
    private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
    {
        // Handle the URL change here
        IsActive = MatchUrl();
        StateHasChanged(); // Update the UI
    }
    
    private bool MatchUrl()
    {
        var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri).PathAndQuery;

        if (uri.Equals("/"))
        {
            return true;
        }

        if (uri.StartsWith("/Monitoring", StringComparison.OrdinalIgnoreCase))
        {
            return true;
        }
        
        return false;
    }

    [Inject] private NavigationManager NavigationManager { get; set; } = default!;
    public void Dispose()
    {
        Navigation.LocationChanged -= OnLocationChanged;
    }

}
<CustomNavLink>
    <span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</CustomNavLink>

 

Handle an Unknown Number of Route Parameters in ASP.NET Blazor

To handle an unknown number of route parameters in ASP.NET Blazor, you can define a single route with a wildcard parameter to capture the entire path.

Here’s how you can achieve this in Blazor:

  1. Define the Route: Use a route parameter in the @page directive. The wildcard parameter is specified using an asterisk (*). This allows you to capture the entire path as a single string.
  2. Process the Parameters: Once you have the full path, you can split it into individual parameters and process them as needed.
@page "/Monitoring/{**Parameters}"
@inject NavigationManager Navigation


<h3>Monitoring</h3>

<ul>
    @foreach (var param in RouteParameters)
    {
    <li>@param</li>
    }
</ul>
[Parameter]
public string Parameters { get; set; }

private List<string> RouteParameters { get; set; } = new();

protected override void OnParametersSet()
{
    var uri = new Uri(Navigation.Uri);
    var path = uri.AbsolutePath;

    // Remove the initial part of the path
    var trimmedPath = path.Substring("/Monitoring/".Length);

    // Split the remaining part of the path by '/'
    RouteParameters = trimmedPath.Split('/', StringSplitOptions.RemoveEmptyEntries).ToList();
}

 

Handle Clipboard Paste in WPF

public YourWindow()
{
    InitializeComponent();

    // "yourTextBox" is your TextBox
    DataObject.AddPastingHandler(yourTextBox, OnPaste);
}

private void OnPaste(object sender, DataObjectPastingEventArgs e)
{
    var isText = e.SourceDataObject.GetDataPresent(DataFormats.UnicodeText, true);
    if (!isText) return;

    var text = e.SourceDataObject.GetData(DataFormats.UnicodeText) as string;
    // Manipulate the text here
    text = text.Replace("oldValue", "newValue"); // Example manipulation

    // Set the new data
    e.DataObject = new DataObject(DataFormats.UnicodeText, text);
}

Note that this event is triggered after the user initiates the paste command but before the content is actually pasted into the TextBox, allowing you to modify or cancel the paste operation.

References
https://stackoverflow.com/questions/3061475/paste-event-in-a-wpf-textbox

ASP.NET Authentication with Identity in a Web API with Bearer Tokens & Cookies in .NET 8

We’ll use the in-memory database for this example.

dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.InMemory
dotnet add package Swashbuckle.AspNetCore.Filters
class MyUser : IdentityUser {}
public class DataContext : IdentityDbContext<MyUser>
{
    public DataContext(DbContextOptions<DataContext> options) : base(options)
    {

    }
}

Program.cs

using Microsoft.EntityFrameworkCore;
using Swashbuckle.AspNetCore.Filters;
using WebApplication1;
using WebApplication1.Data;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
    options.AddSecurityDefinition("oauth2", new Microsoft.OpenApi.Models.OpenApiSecurityScheme
    {
        In = Microsoft.OpenApi.Models.ParameterLocation.Header,
        Name = "Authorization",
        Type = Microsoft.OpenApi.Models.SecuritySchemeType.ApiKey
    });

    options.OperationFilter<SecurityRequirementsOperationFilter>();
});


builder.Services.AddDbContext<DataContext>(options => options.UseInMemoryDatabase("AppDb"));
builder.Services.AddAuthentication();
builder.Services.AddIdentityApiEndpoints<MyUser>()
    .AddEntityFrameworkStores<DataContext>();


var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.MapIdentityApi<MyUser>();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

In Swagger

References
https://www.youtube.com/watch?v=8J3nuUegtL4
https://devblogs.microsoft.com/dotnet/whats-new-with-identity-in-dotnet-8/

Install .NET 8 on Ubuntu 22.04 using Microsoft package feed

Remove the existing .NET packages from your distribution. You want to start over and ensure that you don’t install them from the wrong repository.

sudo apt remove 'dotnet*' 'aspnet*' 'netstandard*'

Configure your package manager to ignore the .NET packages from the distribution’s repository. It’s possible that you’ve installed .NET from both repositories, so you want to choose one or the other.

touch /etc/apt/preferences
nano /etc/apt/preferences
Package: dotnet* aspnet* netstandard*
Pin: origin "<your-package-source>"
Pin-Priority: -10

Make sure to replace <your-package-source> with your distribution’s package source, for example, on Ubuntu you may use archive.ubuntu.com in the US.

Use the apt-cache policy command to find the source:

apt-cache policy '~ndotnet.*' | grep -v microsoft | grep '/ubuntu' | cut -d"/" -f3 | sort -u
# Get Ubuntu version
declare repo_version=$(if command -v lsb_release &> /dev/null; then lsb_release -r -s; else grep -oP '(?<=^VERSION_ID=).+' /etc/os-release | tr -d '"'; fi)

# Download Microsoft signing key and repository
wget https://packages.microsoft.com/config/ubuntu/$repo_version/packages-microsoft-prod.deb -O packages-microsoft-prod.deb

# Install Microsoft signing key and repository
sudo dpkg -i packages-microsoft-prod.deb

# Clean up
rm packages-microsoft-prod.deb

# Update packages
sudo apt update
sudo apt-get update && \
  sudo apt-get install -y dotnet-sdk-8.0

References
https://learn.microsoft.com/en-us/dotnet/core/install/linux-ubuntu-2204
https://learn.microsoft.com/en-us/dotnet/core/install/linux-ubuntu
https://learn.microsoft.com/en-us/dotnet/core/install/linux-package-mixup?pivots=os-linux-redhat

Get and Set the value of an input element In Selenium using C#

Get the Value of an Input Element:

To retrieve the current value of an input element, you can use the GetAttribute method to fetch the “value” attribute. Here’s an example:

using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;

class Program
{
    static void Main(string[] args)
    {
        // Create a new instance of the Chrome driver
        IWebDriver driver = new ChromeDriver();

        // Navigate to your web page
        driver.Navigate().GoToUrl("your_website_url_here");

        // Find the input element by its ID (replace with your element's ID)
        IWebElement inputElement = driver.FindElement(By.Id("your_input_element_id"));

        // Get the current value of the input element
        string inputValue = inputElement.GetAttribute("value");

        // Print the value to the console
        Console.WriteLine("Current Value: " + inputValue);

        // Close the driver when done
        driver.Quit();
    }
}

Set the Value of an Input Element:

To set a new value to an input element, you can use the SendKeys method. Here’s an example:

using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;

class Program
{
    static void Main(string[] args)
    {
        // Create a new instance of the Chrome driver
        IWebDriver driver = new ChromeDriver();

        // Navigate to your web page
        driver.Navigate().GoToUrl("your_website_url_here");

        // Find the input element by its ID (replace with your element's ID)
        IWebElement inputElement = driver.FindElement(By.Id("your_input_element_id"));

        // Set a new value to the input element
        inputElement.SendKeys("New Value");

        // Close the driver when done
        driver.Quit();
    }
}

 

Send double-click event to a web element using Selenium in C#

using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Interactions;
using System;

class Program
{
    static void Main(string[] args)
    {
        // Create a new instance of the Chrome driver
        IWebDriver driver = new ChromeDriver();

        // Navigate to your web page
        driver.Navigate().GoToUrl("your_website_url_here");

        // Find the element you want to double-click (replace with your element locator)
        IWebElement elementToDoubleClick = driver.FindElement(By.Id("your_element_id_here"));

        // Create an Actions object
        Actions actions = new Actions(driver);

        // Double-click the element
        actions.DoubleClick(elementToDoubleClick).Build().Perform();

        // You can add additional actions or interactions here if needed

        // Close the driver when done
        driver.Quit();
    }
}

 

Find td Element inside a tr Element using Selenium in C#

using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;

class Program
{
    static void Main(string[] args)
    {
        // Create a new instance of the Chrome driver
        IWebDriver driver = new ChromeDriver();

        // Navigate to your web page
        driver.Navigate().GoToUrl("your_website_url_here");

        // Find the tr element by its class or other suitable attribute
        IWebElement trElement = driver.FindElement(By.ClassName("rgRow"));

        // Find the td element inside the tr element
        IWebElement tdElement = trElement.FindElement(By.TagName("td"));

        // You can now interact with the tdElement as needed
        // For example, to get its text:
        string tdText = tdElement.Text;

        // Close the driver when done
        driver.Quit();
    }
}

 

Clicking an element in Selenium WebDriver using C#

Clicking an element in Selenium WebDriver using C# is quite straightforward. Once you have located the element using any of the locator strategies (like ID, class name, XPath, etc.), you can use the Click method to perform a click action on that element.

Here’s a simple example to demonstrate how to find an HTML element by its XPath and then click it:

using System;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;

namespace MySeleniumApp
{
    class Program
    {
        static void Main(string[] args)
        {
            // Initialize the Chrome Driver
            using (IWebDriver driver = new ChromeDriver())
            {
                // Navigate to a website
                driver.Navigate().GoToUrl("https://www.example.com");

                // Find the element by its XPath
                IWebElement element = driver.FindElement(By.XPath("your_xpath_here"));

                // Click the element
                element.Click();

                // Optionally, wait for the action to complete or for the next element to be visible
                // Example: using WebDriverWait
                
                // Close the driver
                driver.Quit();
            }
        }
    }
}

You can also use other locator strategies like By.Id, By.ClassName, etc., to find the element you want to click.