Introduction

When working with FTP (File Transfer Protocol) servers in .NET, the FtpWebRequest class is one of the most commonly used tools. FtpWebRequest is a class designed specifically for handling FTP operations in .NET. It inherits from the WebRequest class, which is a base class for making requests to various types of servers (HTTP, FTP, etc.).
In this article, we’ll explore how to use FtpWebRequest by walking through the implementation of my FTP Client project, which is available on GitHub and SourceForge. If you’d like to see the project in action, I’ve also created a video overview on YouTube.
This article will focus specifically on the FTP-related functionality of the application, using FtpWebRequest from the System.Net namespace. We won’t be diving into the Avalonia UI framework or the MVVM pattern, as the goal here is to concentrate on the core FTP operations and how they are implemented in the project.

Project Structure

The WhisperFTPApp project is structured to separate concerns. Here’s a breakdown of the key components:
IFtpService Interface

  • This interface defines the contract for all FTP operations. It abstracts the implementation details, making it easier to swap out the FTP service if needed.
public interface IFtpService
{
    Task<bool> ConnectAsync(FtpConfiguration configuration);
    Task DisconnectAsync();
    Task<IEnumerable<FileSystemItem>> ListDirectoryAsync(FtpConfiguration configuration, string path = "/");
    Task UploadFileAsync(FtpConfiguration configuration, string localPath, string remotePath, IProgress<double> progress);
    Task DownloadFileAsync(FtpConfiguration configuration, string remotePath, string localPath, IProgress<double> progress);
    Task DeleteFileAsync(FtpConfiguration configuration, string remotePath);
    Task DeleteDirectoryAsync(FtpConfiguration configuration, string path);
}

FtpConfiguration Class

  • This class stores the configuration settings required to connect to an FTP server.
public class FtpConfiguration
{
    public string FtpAddress { get; }
    public string Username { get; }
    public string Password { get; }
    public int Port { get; }
    public int Timeout { get; }

    public FtpConfiguration(string ftpAddress, string username, string password, int port, int timeout)
    {
        FtpAddress = ftpAddress;
        Username = username;
        Password = password;
        Port = port;
        Timeout = timeout;
    }
}

FtpService Class

  • This class implements the IFtpService interface and provides the actual implementation of the FTP operations using FtpWebRequest. In this article, I’ll be focusing on the ConnectAsync method, breaking down how it interacts with FtpWebRequest to establish a connection.
public class FtpService : IFtpService
{
    private FtpWebRequest _currentRequest;
    private const int MaxRetries = 3;
    private const int BaseDelay = 1000;

    public async Task<bool> ConnectAsync(FtpConfiguration configuration)
    {
        StaticFileLogger.LogInformation($"Attempting to connect to {configuration.FtpAddress}");
        for (int attempt = 1; attempt <= MaxRetries; attempt++)
        {
            try
            {
                StaticFileLogger.LogInformation($"Connection attempt {attempt} of {MaxRetries}");
                var connectTask = SendRequest(configuration);
                var timeoutTask = Task.Delay(configuration.Timeout);
                var completedTask = await Task.WhenAny(connectTask, timeoutTask);

                if (completedTask == connectTask)
                {
                    return await connectTask;
                }

                StaticFileLogger.LogError($"Connection attempt {attempt} timed out");
                if (attempt < MaxRetries)
                {
                    int delay = BaseDelay * (int)Math.Pow(2, attempt - 1);
                    StaticFileLogger.LogInformation($"Waiting {delay}ms before retry");
                    await Task.Delay(delay);
                }
            }
            catch (WebException ex)
            {
                StaticFileLogger.LogError($"Connection attempt {attempt} failed: {ex.Message}");
                if (attempt == MaxRetries)
                {
                    return IsAuthenticationError(ex);
                }

                int delay = BaseDelay * (int)Math.Pow(2, attempt - 1);
                await Task.Delay(delay);
            }
        }

        StaticFileLogger.LogError("All connection attempts failed");
        return false;
    }

    private async Task<bool> SendRequest(FtpConfiguration configuration)
    {
        var uriBuilder = new UriBuilder(configuration.FtpAddress)
        {
            Port = configuration.Port
        };

        _currentRequest = (FtpWebRequest)WebRequest.Create(uriBuilder.Uri);
        _currentRequest.Method = WebRequestMethods.Ftp.ListDirectory;
        _currentRequest.Credentials = new NetworkCredential(configuration.Username, configuration.Password);
        _currentRequest.Timeout = configuration.Timeout;
        _currentRequest.KeepAlive = false;

        using (FtpWebResponse response = (FtpWebResponse)await _currentRequest.GetResponseAsync())
        {
            return response.StatusCode == FtpStatusCode.OpeningData ||
                   response.StatusCode == FtpStatusCode.AccountNeeded;
        }
    }

// Other methods omitted for brevity...
}

Understanding the Connection Process

The ConnectAsync method is a critical part of the application, as it ensures that the client can reliably connect to the server before performing any operations. Let’s break down the key components and understand their purpose:

_currentRequest This field holds the current FtpWebRequest object. It’s used to manage the connection and send requests to the FTP server.
MaxRetries This defines how many times the application will try to connect to the server before throwing in the towel. Sometimes, connections fail due to temporary issues like network hiccups or server overload. By setting a maximum number of retries, we give the application a chance to recover from these hiccups without giving up too quickly.
BaseDelay This constant defines the initial wait time (in milliseconds) between retry attempts. But here’s the clever part: the delay increases exponentially with each retry. For example, if the first retry happens after 1 second, the next might wait 2 seconds, then 4 seconds, and so on. This "backoff" strategy prevents the application from bombarding the server with too many requests in a short time, which could make things worse.

Then we go to our method and see that it starts with logging. This is a very useful approach for debugging and monitoring the application’s behavior.
The method uses a for loop to attempt the connection up to MaxRetries times. This ensures that temporary issues (like network glitches) don’t immediately cause the connection to fail.

Connection Attempt and Handling the Result

...

StaticFileLogger.LogInformation($"Connection attempt {attempt} of {MaxRetries}");
var connectTask = SendRequest(configuration);
var timeoutTask = Task.Delay(configuration.Timeout);
var completedTask = await Task.WhenAny(connectTask, timeoutTask);

if (completedTask == connectTask)
  {
     return await connectTask;
  }

...

The SendRequest method is called to create and send the FtpWebRequest. It takes the configuration details (like the server address, port, username, and password) and uses them to build the request. This includes setting the request type, adding the credentials, and defining how long the request should wait before timing out. Once everything is set up, it sends the request and waits for the server’s response..
A Task.Delay is used to create a timeout task. If the connection takes too long (longer than the specified timeout), the timer will "ring," and we’ll know something’s wrong. The Task.WhenAny method waits for either the connection task or the timeout task to complete. If the timeout task completes first, the connection attempt is considered failed.
If the connection task completes first, the method returns the result of the connection attempt (either true or false).

Handling Timeouts and Handling Exceptions

...

StaticFileLogger.LogError($"Connection attempt {attempt} timed out");
  if (attempt < MaxRetries)
    {
      int delay = BaseDelay * (int)Math.Pow(2, attempt - 1);
      StaticFileLogger.LogInformation($"Waiting {delay}ms before retry");
      await Task.Delay(delay);
    }
  }
     catch (WebException ex)
        {
         StaticFileLogger.LogError($"Connection attempt {attempt} failed: {ex.Message}");
          if (attempt == MaxRetries)
            {
              eturn IsAuthenticationError(ex);
             }
             int delay = BaseDelay * (int)Math.Pow(2, attempt - 1);
            await Task.Delay(delay);
         }
   }

...

If the connection attempt times out, the method logs the error and calculates a delay before the next retry. The delay increases exponentially with each attempt to avoid overwhelming the server.

The SendRequest Method

The SendRequest method is responsible for creating and sending the FtpWebRequest. Here’s how it works:


private async Task<bool> SendRequest(FtpConfiguration configuration)
{
    var uriBuilder = new UriBuilder(configuration.FtpAddress)
    {
        Port = configuration.Port
    };

    _currentRequest = (FtpWebRequest)WebRequest.Create(uriBuilder.Uri);
    _currentRequest.Method = WebRequestMethods.Ftp.ListDirectory;
    _currentRequest.Credentials = new NetworkCredential(configuration.Username, configuration.Password);
    _currentRequest.Timeout = configuration.Timeout;
    _currentRequest.KeepAlive = false;

...
}

URI Construction: The method constructs the FTP URI using the UriBuilder class, which ensures that the address and port are correctly formatted.

Setting Properties: Credentials, timeout, and other configurations are applied. _currentRequest.Method: Set to ListDirectory to test the connection.
_currentRequest.Credentials: Set using the provided username and password. _currentRequest.Timeout: Set to the specified timeout value. _currentRequest.KeepAlive: Set to false to ensure that the connection is closed after the request is completed.

Handling the Response

...
     using (FtpWebResponse response = (FtpWebResponse)await _currentRequest.GetResponseAsync())

       {
            return response.StatusCode == FtpStatusCode.OpeningData ||
                   response.StatusCode == FtpStatusCode.AccountNeeded;
       }
     }
...

It uses a using statement to ensure that FtpWebResponse is properly disposed of, managing network resources efficiently. The method checks for status codes like OpeningData (150) and AccountNeeded (532), signaling a ready or responsive server. These codes are essential for identifying a successful connection, even if additional authentication is needed. Integrated into ConnectAsync, this method tests the server's readiness by attempting a directory listing. By differentiating successful connections from failures, the method enhances reliability. As a result, this strategy makes the FTP connection process more dependable.

Using IFtpService in the MainWindowViewModel

The MainWindowViewModel class in WhisperFTPApp manages the interaction between the main window and IFtpService, processing FTP requests. The ConnectToFtpAsync method takes the server address, credentials, and port, validates them, and attempts to establish a connection. Upon success, it updates the UI and loads the list of files and directories from the server.

Initial Setup and Input Validation

public class MainWindowViewModel : ViewModelBase
{
...
    //Other properties and fields omitted for brevity

    private async Task ConnectToFtpAsync()
        {
            IsTransferring = true;
            StaticFileLogger.LogInformation($"Attempting to connect to {FtpAddress}");
            try
                {
                    StatusMessage = "Connecting to FTP server...";
                    FtpItems?.Clear();
                    UpdateRemoteStats();

                    if (!int.TryParse(Port, out int portNumber))
                    {
                        StatusMessage = "Invalid port number";
                        return;
                    }
                    StatusMessage = "Invalid port number";
                     return;
                }

                if (!FtpAddress.StartsWith("ftp://", StringComparison.OrdinalIgnoreCase))
                {
                    FtpAddress = $"ftp://{FtpAddress}";
                }
            FtpAddress = $"ftp://{FtpAddress}";
        }
...

Before attempting to connect, the method sets IsTransferring = true, indicating that an operation is in progress. A log entry is created: "Attempting to connect to [FtpAddress]".
The UI updates to reflect the connection attempt, and the list of remote files is cleared. The method then validates the user’s input. If the port number is invalid, an error message is displayed, and the process stops. Similarly, if the FTP address is missing the ftp:// prefix, it is corrected before proceeding. Once these checks are complete, the connection attempt begins.

Creating FTP Configuration and Attempting Connection

...
        var uri = new Uri(FtpAddress);
        var baseAddress = $"{uri.Scheme}://{uri.Host}";

        var configuration = new FtpConfiguration(
            baseAddress,
            Username,
            Password,
            portNumber,
            timeout: 10000);

        bool isConnected = await _ftpService.ConnectAsync(configuration);
... 

With a properly formatted address, a Uri object is created to extract the base FTP address. A configuration object is then initialized, containing the server address, credentials, port number, and a timeout setting.
Using this configuration, the method calls _ftpService.ConnectAsync(), sending a request to the server. The success of the connection now depends on the server’s response.

Handling Connection Results

...
        if (isConnected)
        {
            StatusMessage = "Connected to FTP server";
            IsConnected = true;
            FtpItems = new ObservableCollection<FileSystemItem>(await _ftpService.ListDirectoryAsync(configuration));
            UpdateRemoteStats();
        }
        else
        {
            StatusMessage = "Failed to connect to FTP server";
            IsConnected = false;
        }
    }
    catch (Exception ex)
    {
        StatusMessage = $"Error: {ex.Message}";
        IsConnected = false;
    }
    finally
    {
        IsTransferring = false;
    }
}

If the connection succeeds, IsConnected = true, and the UI updates with a success message. A log entry confirms the connection, and the method retrieves the list of remote files, displaying them in the UI. The connection details are also saved for future use.
If the connection fails, an error message appears, and a log entry records the failure. Any unexpected errors—such as network issues—are caught, logged, and displayed. Finally, the method resets IsTransferring = false, indicating that the process has finished, regardless of the outcome.

Conclusion

In this article we explored the core aspects of working with FTP servers using FtpWebRequest in C#. We covered connection handling, retries, error management, and integration into an application. With intelligent retry mechanisms, this approach helps in building scalable and robust FTP-based applications. Whether you’re building a simple tool or a full-fledged FTP client, adopting these techniques will help you create a more resilient and user-friendly solution.

Author Of article : Bohdan Harabadzhyu Read full article