Skip to content

Standardize the behavior of CreateFile/CreateFolder across different OS#20850

Open
Frederisk wants to merge 4 commits intoAvaloniaUI:masterfrom
Frederisk:io-create-fix
Open

Standardize the behavior of CreateFile/CreateFolder across different OS#20850
Frederisk wants to merge 4 commits intoAvaloniaUI:masterfrom
Frederisk:io-create-fix

Conversation

@Frederisk
Copy link
Contributor

@Frederisk Frederisk commented Mar 9, 2026

What does the pull request do?

Currently, Avalonia only provides a very simple wrapper for system I/O. This results in significant behavioral differences in the same I/O method across different operating systems, which may lead to unexpected operations.

This PR aims to standardize the differences in method behavior across different platforms, starting with creating files and folders. Specifically, the approach here targets .NET's FileInfo.Create() and DirectoryInfo.CreateSubdirectory() methods to achieve similar interface behavior. The consideration is that this approach may be closer to the user experience of most .NET developers. This is also a default implementation of Avalonia:

internal static FileInfo CreateFileCore(DirectoryInfo directoryInfo, string name)
{
var fileName = System.IO.Path.Combine(directoryInfo.FullName, name);
var newFile = new FileInfo(fileName);
using var stream = newFile.Create();
return newFile;
}
internal static DirectoryInfo CreateFolderCore(DirectoryInfo directoryInfo, string name) =>
directoryInfo.CreateSubdirectory(name);

What is the current behavior?

Currently, this behavior exhibits significant inconsistencies across different operating systems:

However, there are still a few issues that may need to be discussed to decide how to handle this. The first is whether the file should be truncated when attempting to call the CreateFileAsync method if it already exists. This question is somewhat similar to another recent PR.

Another issue is that I noticed the original code heavily used Task.FromResult and didn't put methods like CreateFile, which would cause I/O blocking, into Task.Run to avoid thread blocking. I'm not sure what the purpose of this is, or if it needs fixing. I'd be happy to improve this in another PR if needed (to avoid including too much irrelevant content in this PR).

What is the updated/expected behavior with this PR?

Now,

  • in the Browser, because of { keepExistingData: false }, the file will be truncated first when attempting to write;
  • in Android, these methods will first try to retrieve an existing object; if it exists, they will return it; otherwise, they will create a new one;
  • in iOS, when creating a folder, if the folder already exists, you won't get an error; instead, you'll try to return the existing one.

In addition, I have reserved code to truncate existing files, but it has been commented out and we can decide whether to keep it based on the discussion.

How was the solution implemented (if it's not obvious)?

Before:

OS Behavior if file already exists Behavior if folder already exists
Android Creates file (1) Creates folder (1)
iOS Truncates and returns file Throws an NSErrorException
Browser Returns file Returns folder
Others Truncates and returns file Returns folder

After:

OS Behavior if file already exists Behavior if folder already exists
Android Returns file Returns folder
iOS Truncates and returns file Returns folder
Browser Returns file Returns folder
Others Truncates and returns file Returns folder

Checklist

Breaking changes

Aside from desktop environments such as Windows and Linux, the behavior of Create will change significantly on other operating systems.

Obsoletions / Deprecations

Fixed issues

Fixes #20580

@Frederisk Frederisk closed this Mar 9, 2026
@Frederisk Frederisk reopened this Mar 9, 2026
@Frederisk Frederisk marked this pull request as draft March 9, 2026 12:24
@Frederisk
Copy link
Contributor Author

I've just tried writing some basic unit tests to test these features, but I seem to be finding that there aren't any platform-specific test projects in the current solution? Or am I missing something?

@drweb86
Copy link

drweb86 commented Mar 9, 2026

Thanks. File I/O is really neeeded to Avalonia to be cross-platform.

}

public Task<IStorageFile?> CreateFileAsync(string name)
public async Task<IStorageFile?> CreateFileAsync(string name)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For CreateFileAsync i would have it truncated content. And separate method AppendFIleAsync - that will behave like CreateFileAsync, but append on top of existing data.

That will help to have retries for fetching files. and Append - for logs.

Copy link
Contributor Author

@Frederisk Frederisk Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For write actions, after #20804, when you try to call OpenWriteAsync, it will already truncate the file correctly. The question here is what happens when the Create method is called on an existing file. Just post that file; or truncate the existing file first and post it.

This can cause differences in behavior if someone tries to write some weird code:

var parentFolder = await GetParentFolderAsync();
byte[] bytes = [1, 2, 3];

// Create a file and write to it.
var file = await parentFolder.CreateFileAsync('file.txt');
var stream = await file.OpenWriteAsync();
await stream.WriteAsync(bytes);
await stream.FlushAsync();
strean.Dispose();
// Contents of file file.txt: 123

// Get and write to it.
file = await parentFolder.GetFileAsync('file.txt');
stream = await file.OpenWriteAsync();
await stream.WriteAsync(bytes);
await stream.FlushAsync();
strean.Dispose();
// Contents of file file.txt: 123

// Try creating an existing file and appending content:
file = await parentFolder.CreateFileAsync('file.txt'); // The file may or may not be truncated here, depending on the operating system.
// Contents of file file.txt: empty or 123

However, you did remind me that we do need a new method to handle file appending. AppendFileAsync seems to be confused with GetFileAsync, so perhaps IStorageFile.OpenAppendAsync or another solution would be a better choice. (Of course, that's another topic.)

@avaloniaui-bot
Copy link

You can test this PR using the following package version. 12.0.999-cibuild0063149-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

Frederisk added a commit to Frederisk/avalonia-docs that referenced this pull request Mar 10, 2026
@Frederisk Frederisk marked this pull request as ready for review March 11, 2026 03:23
@avaloniaui-bot
Copy link

You can test this PR using the following package version. 12.0.999-cibuild0063217-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@avaloniaui-bot
Copy link

You can test this PR using the following package version. 12.0.999-cibuild0063219-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Android] Create file via CreateFileAsync produces undeterministic file names

3 participants