Почему 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
Как это работает:
- Убираем конечный слэш у базового URL.
- Для каждого сегмента убираем начальный слэш и добавляем через
/. - Используем
UriBuilderдля корректной сборки (экранирование, порты и т.д.).
Этот метод