Skip to content

[dotnet] [bidi] Earlier preview feedback gathering  #14530

Closed
@nvborisenko

Description

@nvborisenko

Feature and motivation

Here we are going to gather everything related to BiDi implementation in .NET and improve it as soon as possible despite on any potential breaking changes.

1. Discriminated unions ✅ v4.26

We have a lot of classes what are inherited from a base. The basic example:

public abstract record ClipRectangle;

public record BoxClipRectangle(double X, double Y, double Width, double Height) : ClipRectangle;

public record ElementClipRectangle(Script.SharedReference Element) : ClipRectangle;

ClipRecatange is used like:

var screenshot = await context.CaptureScreenshotAsync(....); // it takes ClipRectangle as argument

And it is not clear what exactly I can put as arguments. I see base class as an argument, but I don't see what available options I can provide. We can add factory:

var screenshot = await context.CaptureScreenshotAsync(ClipRectangle.Box(5, 5, 10, 10));

But it requires to write so many boilerplate code from selenium team. Don't forget about optional parameters (which requires new class definition). So many code.

Solution

Use nested classes for all discriminated classes.

public abstract record ClipRectangle {
  public record Box(double X, double Y, double Width, double Height) : ClipRectangle;

  public record Element(Script.SharedReference Element) : ClipRectangle;
}

So user will be able to:

var screenshot = await context.CaptureScreenshotAsync(new ClipRectangle.Box(5, 5, 10, 10));

For user it seems there is no big diff, but for selenium team it is HUGE diff. And when https://github.com/dotnet/csharplang/blob/main/proposals/TypeUnions.md will be in place, it will everybody make happy: selenium team to write even less code, and user probably will write: var screenshot = await context.CaptureScreenshotAsync(new Box(5, 5, 10, 10));

2. Don't mimic `BiDi` instance as `BrowsingContext` ✅ v4.26 We have helper methods in `BiDi` class which actually forward to `BrowsingContext` module. Example: ```csharp await bidi.CreateContextAsync(...); ```

Stop doing it and just expose modules. So it would be better:

await bidi.BrowsingContext.CreateAsync(...);
3. Result object as Enumerable ✅ v4.26

Some commands return result, which seems to be a list of items.

var result = await context.Storage.GetCookiesAsync();

Where result is:

storage.GetCookiesResult = {
  cookies: [*network.Cookie],
  partitionKey: storage.PartitionKey,
}

So result is GetCookiesResult class, and it would be great if it behaves as enumerable.

Solution

Implement IReadOnlyList<T>. So user is able to:

var cookies = await context.Storage.GetCookiesAsync();

Console.WriteLine(cookies[0].Name);
Console.WriteLine(cookies.PartitionKey);

And it will be also good to rename result class to CookiesList (or CookiesReadOnlyList or CookiesCollection?)

4. `driver.AsBiDiAsync()` or `driver.AsBiDiContextAsync()` - only one should alive ✅ v4.29

We have 2 entry points into BiDi world, the both are useful. The problem is that driver.AsBidiContextAsync() holds underlying bidi connection, which cannot be disposed. And moreover, should not be disposed. So seems, only one driver.AsBiDiAsync() should be as single entry point.

Then the question is: if user has bidi instance, then how he can get an access to current BrowsingContext instance?

Solution

Keep only one entry point: driver.AsBiDiAsync(). And provide a way how to instantiate an instance of BrowsingContext.

await using var bidi = await driver.AsBiDiAsync();

var contexts = await bidi.BrowsingContext.GetTreeAsync(...); // already available

var context = new BrowsingContext(bidi, driver.Manage().CurrentWindowHandle); // is it good?
// or
var context = bidi.BrowsingContext.Create(driver.Manage().CurrentWindowHandle); // is it good? - No, it is bad.

Given that this is low-level API, I would like to not introduce any kind of "helper" methods.

One more elegant way is:

await using var context = await driver.AsBiDiAsync(driver.Manage().CurrentWindowHandle);

But it hides DisposeAsync() for underlying bidi connection. If bidi connection will be a part of WebDriver itself, then it is OK - user is not required to manage lifecycle of the connection. Even if user disposes underlying bidi connection, we can throw AlreadyDisposed exception for further commands. Not ideal.

Preferable

My current understanding, which is safe:

await using var bidi = await driver.AsBiDiAsync(); // always return new instance

var context = new BrowsingContext(bidi, driver.Manage().CurrentWindowHandle);
// or
var context = bidi.AsBrowsingContext(driver.Manage().CurrentWindowHandle);

UPD: Both preferable solutions are good. In v4.29 we removed bad AsBiDiContextAsync() method. User is still able to use:

var contexts = await bidi.BrowsingContext.GetTreeAsync(...);
5. `System.Uri` vs `string` ❌

We are trying to be strongly-typed. Seems System.Uri is a good candidate to deserialize/serialize. Like here url can be interpretated as System.Uri type.

It will allow us to be closer to .net ecosystem. Performance?.. - no, not in this case.

UPD: Using Uri is absolutely safe, so let's do it. Or not, given that this is low-level API. Ah, seems not :(

UPD: System.Uri is not safe in .net framework.

var uri = new Uri("blah");
System.UriFormatException: Invalid URI: The format of the URI could not be determined.

So string is correct choice.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions