Склеиваем URL в C# правильно: туториал по Path.Combine для веба

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему Path.Combine ломает URL и как это исправить?

Представьте: вы пишете на C# приложение, которое обращается к API. Вам нужно собрать URL вида https://api.example.com/v1/users/123. Первое, что приходит в голову — использовать знакомый Path.Combine. И это ошибка, которую совершают тысячи разработчиков. Вопрос «Path.Combine for URLs?» на Stack Overflow собрал сотни тысяч просмотров — и неспроста.

В этой статье мы разберём, почему Path.Combine смертельно опасен для URL, и покажем 4 правильных способа склеивать адреса в C#. Вы узнаете, как работает UriBuilder, почему стоит обратить внимание на библиотеку Flurl и как написать свой безопасный метод объединения URL. Поехали!

Проблема: что не так с Path.Combine?

Метод Path.Combine создан для файловых систем Windows. Он использует обратную косую черту (\) и учитывает особенности дисков (например, C:\). URL же работает с прямой косой чертой (/) и не имеет понятия «диск». Посмотрим на пример:

string baseUrl = "https://api.example.com/api";
string endpoint = "/users/123";
string result = Path.Combine(baseUrl, endpoint);
Console.WriteLine(result);
// Вывод: https:\\api.example.com\api\users\123 (на Windows)
// Или ещё хуже: https://api.example.com/api/users/123 (если повезёт)

Результат, мягко говоря, невалидный. Path.Combine заменяет косые черты на обратные (в Windows), и ваш HTTP-запрос упадёт с ошибкой. Даже если на Linux всё выглядит нормально, код становится непереносимым. А если в базовом URL уже есть слэш в конце? Или в endpoint — двойной слэш? Начинается ад.

Реальный пример из Stack Overflow

Один из самых популярных ответов на вопрос «Path.Combine for URLs?» (автор — Brian, 600+ апвоутов) начинается с фразы: «Пожалуйста, не делайте этого. Path.Combine — для файлов, не для URL». И это золотое правило. Давайте разберёмся, какие инструменты предлагает C# для безопасной работы с URL.

Способ 1: Uri.TryCreate — встроенный метод .NET

Первый и самый очевидный способ — использовать класс Uri. Он умеет правильно объединять базовый URL и относительный путь:

string baseUrl = "https://api.example.com/api";
string relativePath = "/users/123";

if (Uri.TryCreate(baseUrl, relativePath, out Uri result))
{
    Console.WriteLine(result.ToString());
    // Вывод: https://api.example.com/users/123
}
else
{
    Console.WriteLine("Некорректный URL");
}

Важно: второй аргумент TryCreate должен быть относительным URI (начинаться с /). Если передать просто "users/123", он будет интерпретирован как абсолютный — и результат будет неверным. Это не самый гибкий метод, но для простых случаев — отличное встроенное решение.

Способ 2: UriBuilder — мощный и гибкий

Класс UriBuilder даёт полный контроль над каждой частью URL: схемой, хостом, портом, путём и query-параметрами. Это идеальный выбор, когда нужно собрать сложный адрес:

var builder = new UriBuilder("https://api.example.com");
builder.Port = 8080;
builder.Path = "/api/v1/users";
builder.Query = "page=1&limit=10";

Uri uri = builder.Uri;
Console.WriteLine(uri.ToString());
// Вывод: https://api.example.com:8080/api/v1/users?page=1&limit=10

Почему это круто?

  • Автоматическое экранирование — специальные символы в query-параметрах будут закодированы.
  • Правильные слэши — UriBuilder сам добавляет или убирает лишние слэши.
  • Порты и схемы — легко менять http на https или добавлять порт.

Но есть нюанс: Path нужно задавать целиком. Если вы хотите добавить сегмент к существующему пути, придётся делать это вручную:

builder.Path = builder.Path.TrimEnd('/') + "/newsegment";

Это неудобно, но решается обёрткой (см. способ 4).

Способ 3: Flurl — библиотека для ленивых (и умных)

Если вы пишете современное приложение на C# и не хотите изобретать велосипед, обратите внимание на Flurl — легковесную библиотеку для работы с URL и HTTP. Она доступна как NuGet-пакет (Install-Package Flurl) и предлагает элегантный fluent-интерфейс:

using Flurl;

string url = "https://api.example.com"
    .AppendPathSegment("api")
    .AppendPathSegment("v1")
    .AppendPathSegment("users")
    .SetQueryParams(new { page = 1, limit = 10 });

Console.WriteLine(url);
// Вывод: https://api.example.com/api/v1/users?page=1&limit=10

Преимущества Flurl:

  • Интуитивно понятный API — метод AppendPathSegment сам заботится о слэшах.
  • Работа с query-параметрами — SetQueryParams принимает анонимный объект или словарь.
  • Полная интеграция с HTTP-клиентом — Flurl.Http позволяет сразу выполнять запросы.

Единственный минус — это внешняя зависимость. Но для серьёзных проектов она себя оправдывает.

Способ 4: Свой метод CombineUrl — контролируем всё

Хотите максимальный контроль и никаких зависимостей? Напишите свой метод. Вот простой и надёжный вариант, основанный на советах со Stack Overflow:

public static string CombineUrl(string baseUrl, params string[] segments)
{
    if (string.IsNullOrWhiteSpace(baseUrl))
        throw new ArgumentException("Base URL is required", nameof(baseUrl));

    var uriBuilder = new UriBuilder(baseUrl.TrimEnd('/'));
    
    foreach (var segment in segments)
    {
        if (!string.IsNullOrWhiteSpace(segment))
        {
            uriBuilder.Path = uriBuilder.Path.TrimEnd('/') + "/" + segment.TrimStart('/');
        }
    }
    
    return uriBuilder.Uri.ToString();
}

// Использование:
string url = CombineUrl("https://api.example.com/api/", "/v1", "users", "123");
Console.WriteLine(url);
// Вывод: https://api.example.com/api/v1/users/123

Как это работает:

  1. Убираем конечный слэш у базового URL.
  2. Для каждого сегмента убираем начальный слэш и добавляем через /.
  3. Используем UriBuilder для корректной сборки (экранирование, порты и т.д.).

Этот метод

Блоги

Рекомендаций по книгам