Skip to content

Dependency Injection RU

Алексей . edited this page Jul 26, 2026 · 1 revision

Внедрение зависимостей

English Русский

Страница описывает LogsParserServiceCollectionExtensions — две перегрузки AddLogsParser, регистрируемые ими сервисы, их времена жизни и объект настроек LogsParserRegistrationOptions.

LogsParserServiceCollectionExtensions

namespace LogsParser.DependencyInjection;

public static class LogsParserServiceCollectionExtensions

Библиотека напрямую зависит от Microsoft.Extensions.DependencyInjection, поэтому дополнительных пакетов для этих расширений не требуется.

AddLogsParser(IServiceCollection)

public static IServiceCollection AddLogsParser(this IServiceCollection services)

Регистрирует весь конвейер с настройками по умолчанию и без учётных данных.

Параметр Тип По умолчанию Описание
services IServiceCollection — Коллекция, в которую добавляются сервисы. Возвращается без изменений для цепочки вызовов.

Возвращает тот же экземпляр IServiceCollection.

Исключения:

Исключение Когда
ArgumentNullException services равен null
using LogsParser;
using LogsParser.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();
services.AddLogsParser();

using var provider = services.BuildServiceProvider();

var client = provider.GetRequiredService<LogsParserClient>();

AddLogsParser(IServiceCollection, Action<LogsParserRegistrationOptions>)

public static IServiceCollection AddLogsParser(
    this IServiceCollection services,
    Action<LogsParserRegistrationOptions> configure)

Создаёт новый LogsParserRegistrationOptions, передаёт его в configure, после чего регистрирует конвейер в соответствии со значениями, которые оставил обратный вызов.

Параметр Тип По умолчанию Описание
services IServiceCollection — Коллекция, в которую добавляются сервисы. Возвращается без изменений для цепочки вызовов.
configure Action<LogsParserRegistrationOptions> — Обратный вызов, выполняемый один раз, немедленно, с новым экземпляром настроек.

Возвращает тот же экземпляр IServiceCollection.

Исключения:

Исключение Когда
ArgumentNullException services равен null
ArgumentNullException configure равен null
using LogsParser.DependencyInjection;
using LogsParser.Models;
using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();
services.AddLogsParser(options =>
{
    options.Credentials = new LogsParserCredentials("my_login", "my_password", "BASE32SECRET");
});

Что регистрируется

Сервис Реализация Время жизни
ICookieStorage MemoryCookieStorage Singleton
LogsParserHttpOptions Экземпляр из LogsParserRegistrationOptions.HttpOptions; перегрузка без параметров регистрирует new LogsParserHttpOptions() Singleton
ILogsParserDataSource LogsParserHttpDataSource, создаваемый внутренней фабрикой Transient
LogsParserClient LogsParserClient Transient
LogsParserCredentials Экземпляр из LogsParserRegistrationOptions.Credentials — регистрируется только если он не null Singleton

Перегрузка без параметров не регистрирует LogsParserCredentials никогда; перегрузка с настройками — только если options.Credentials отличается от null. Источник данных получает учётные данные через GetService<LogsParserCredentials>(), поэтому их отсутствие допустимо и просто даёт неаутентифицированный транспорт — этого достаточно для страниц, которые сайт отдаёт анонимно, и для задач, ограниченных разбором HTML.

Конкретный тип LogsParserHttpDataSource отдельно не регистрируется — регистрируется только ILogsParserDataSource. Чтобы прочитать свойства лимита запросов, объявленные на конкретном классе, приведите разрешённый экземпляр к типу:

using LogsParser.Abstractions;
using LogsParser.Net;
using Microsoft.Extensions.DependencyInjection;

var dataSource = provider.GetRequiredService<ILogsParserDataSource>();

if (dataSource is LogsParserHttpDataSource http)
{
    Console.WriteLine($"{http.RateLimitRemaining}/{http.RateLimitMax}, reset at {http.RateLimitReset:u}");
}

Семантика регистрации

Здесь работают два разных механизма, и разница между ними определяет, кто побеждает при двойной регистрации сервиса.

  • Значения по умолчанию добавляются через TryAdd*. ICookieStorage, LogsParserHttpOptions, ILogsParserDataSource и LogsParserClient регистрируются вызовами TryAddSingleton / TryAddTransient. Если вызывающий код зарегистрировал этот тип сервиса до AddLogsParser, его регистрация сохраняется, а значение библиотеки по умолчанию пропускается.
  • Фабрики из настроек применяются через Replace. Если CookieStorageFactory или DataSourceFactory не равны null, соответствующий дескриптор регистрируется через services.Replace(...), который удаляет первый существующий дескриптор для этого типа сервиса и добавляет новый. Поэтому фабрика, переданная через настройки, перекрывает любую более раннюю регистрацию этого сервиса.
var services = new ServiceCollection();

// Зарегистрировано первым — сохраняется, потому что значение по умолчанию добавляется через TryAdd.
services.AddSingleton<ICookieStorage>(new MemoryCookieStorage());

services.AddLogsParser(options =>
{
    // Не задано, поэтому побеждает строка выше.
    // Если бы фабрика была задана, Replace удалил бы строку выше.
    options.HttpOptions = new LogsParserHttpOptions { MaxRetryAttempts = 3 };
});

Двойной вызов AddLogsParser идемпотентен для значений по умолчанию, но CookieStorageFactory или DataSourceFactory из второго вызова заменят то, что зарегистрировал первый.

LogsParserRegistrationOptions

namespace LogsParser.Models;

public sealed class LogsParserRegistrationOptions

Изменяемый объект настроек, используемый только на этапе регистрации. Сам он в контейнер не помещается.

Свойство Тип По умолчанию Описание
Credentials LogsParserCredentials? null Логин, пароль и TOTP-секрет в Base32. Регистрируется как singleton, если не null; при null транспорт работает без аутентификации.
HttpOptions LogsParserHttpOptions new() Базовый URI, User-Agent, Accept, MaxRetryAttempts, WaitForRateLimitReset. Всегда регистрируется как singleton-экземпляр (через TryAddSingleton).
CookieStorageFactory Func<IServiceProvider, ICookieStorage>? null Если задана, регистрируется как singleton ICookieStorage через Replace, вместо MemoryCookieStorage.
DataSourceFactory Func<IServiceProvider, ILogsParserDataSource>? null Если задана, регистрируется как transient ILogsParserDataSource через Replace, вместо встроенной фабрики LogsParserHttpDataSource.

LogsParserCredentials и LogsParserHttpOptions полностью описаны на странице Модели; поведение HTTP-настроек во время выполнения — на странице Транспорт и аутентификация.

Логирование

Встроенная фабрика источника данных получает ILoggerFactory из контейнера и откатывается к NullLoggerFactory.Instance, если ни одна не зарегистрирована. Выбранная фабрика передаётся в LogsParserHttpDataSource и одновременно устанавливается глобально для процесса через LogsParserLogging.UseLoggerFactory.

Отсюда следуют два вывода:

  • Вызывайте services.AddLogging(...) до первого разрешения сервисов, если нужны логи библиотеки. Поиск фабрики происходит в момент первого создания ILogsParserDataSource; провайдер логирования, добавленный в коллекцию позже, уже ни на что не влияет, поскольку коллекция к тому времени превращена в провайдер.
  • Если вы задаёте собственную DataSourceFactory, встроенная фабрика не выполняется и LogsParserLogging никем не перенастраивается. Вызовите LogsParserLogging.UseLoggerFactory(...) самостоятельно либо передайте ILoggerFactory в конструктор LogsParserHttpDataSource внутри своей фабрики.
using LogsParser.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

var services = new ServiceCollection();

services.AddLogging(builder => builder
    .AddSimpleConsole()
    .SetMinimumLevel(LogLevel.Debug));

services.AddLogsParser();

Категории и уровни описаны на странице Логирование.

Примеры

Минимальная регистрация без учётных данных

Достаточно для анонимных страниц и для кода, которому клиент нужен лишь как способ добраться до парсера.

using LogsParser;
using LogsParser.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();
services.AddLogsParser();

using var provider = services.BuildServiceProvider();

var client = provider.GetRequiredService<LogsParserClient>();
var catalog = await client.GetLogsFilterCatalogAsync();

Console.WriteLine($"{catalog.Filters.Count} filters");

Полная регистрация с учётными данными и HTTP-настройками

using LogsParser;
using LogsParser.DependencyInjection;
using LogsParser.Models;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

var services = new ServiceCollection();

services.AddLogging(builder => builder.AddSimpleConsole().SetMinimumLevel(LogLevel.Information));

services.AddLogsParser(options =>
{
    options.Credentials = new LogsParserCredentials(
        Login: "my_login",
        Password: "my_password",
        TotpSecret: "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ");

    options.HttpOptions = new LogsParserHttpOptions
    {
        MaxRetryAttempts = 3,
        WaitForRateLimitReset = true
    };
});

using var provider = services.BuildServiceProvider();

var client = provider.GetRequiredService<LogsParserClient>();
var page = await client.GetLogsAsync(new LogsQuery(ServerId: 201, Limit: 500));

Постоянное хранилище cookie через CookieStorageFactory

MemoryCookieStorage по умолчанию теряет сессию при завершении процесса, из-за чего каждый запуск начинается с повторного входа и подтверждения TOTP. Хранилище, переживающее перезапуски, регистрируется через CookieStorageFactory.

using System.Text.Json;
using LogsParser;
using LogsParser.Abstractions;
using LogsParser.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;

public sealed class FileCookieStorage : ICookieStorage
{
    private readonly string _path;
    private readonly object _sync = new();

    public FileCookieStorage(string path) => _path = path;

    public IReadOnlyCollection<ParserCookie> GetCookies()
    {
        lock (_sync)
        {
            return File.Exists(_path)
                ? JsonSerializer.Deserialize<ParserCookie[]>(File.ReadAllText(_path)) ?? []
                : [];
        }
    }

    public void SetCookies(IReadOnlyCollection<ParserCookie> cookies)
    {
        lock (_sync)
        {
            File.WriteAllText(_path, JsonSerializer.Serialize(cookies));
        }
    }
}

var services = new ServiceCollection();
services.AddLogsParser(options =>
{
    options.CookieStorageFactory = _ => new FileCookieStorage("cookies.json");
});

Фабрика получает IServiceProvider, поэтому хранилище может иметь собственные зависимости:

options.CookieStorageFactory = sp => new FileCookieStorage(
    sp.GetRequiredService<IHostEnvironment>().ContentRootPath + "/cookies.json");

Cookie сессии и токен защиты от DDoS хранятся в одном плоском хранилище — см. Хранилище cookie.

Полная замена транспорта через DataSourceFactory

DataSourceFactory заменяет весь HTTP-слой: это удобно для тестов, для HttpClient с прокси или для кеширующего/офлайнового источника.

using System.Net;
using LogsParser.Abstractions;
using LogsParser.DependencyInjection;
using LogsParser.Models;
using LogsParser.Net;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

var services = new ServiceCollection();
services.AddLogsParser(options =>
{
    options.Credentials = new LogsParserCredentials("my_login", "my_password", "BASE32SECRET");

    options.DataSourceFactory = sp =>
    {
        var handler = new HttpClientHandler
        {
            AllowAutoRedirect = false,      // обязательно: ответы 302 и есть протокол аутентификации
            Proxy = new WebProxy("socks5://127.0.0.1:1488"),
            UseProxy = true
        };

        return new LogsParserHttpDataSource(
            credentials: sp.GetService<LogsParserCredentials>(),
            cookieStorage: sp.GetRequiredService<ICookieStorage>(),
            options: sp.GetRequiredService<LogsParserHttpOptions>(),
            httpClient: new HttpClient(handler, disposeHandler: true),
            loggerFactory: sp.GetService<ILoggerFactory>());
    };
});

Внедрённый HttpClient, обработчик которого следует за перенаправлениями, ломает аутентификацию незаметно: библиотека не видит 302 и возвращает HTML страницы входа как содержимое. Внедрённый клиент также сохраняет собственные BaseAddress и User-Agent вместо значений из LogsParserHttpOptions — задавайте их сами, если они важны.

Заглушка источника устроена так же — именно так транспорт подменяется в тестах:

using LogsParser;
using LogsParser.Abstractions;

public sealed class StubDataSource(string html) : ILogsParserDataSource
{
    public Task<string> GetContentAsync(ParserRequest request, CancellationToken cancellationToken = default)
        => Task.FromResult(html);
}

services.AddLogsParser(options => options.DataSourceFactory = _ => new StubDataSource("<html></html>"));

Получение клиента из области видимости

using LogsParser;
using LogsParser.DependencyInjection;
using LogsParser.Models;
using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();
services.AddLogsParser();

using var provider = services.BuildServiceProvider();

using (var scope = provider.CreateScope())
{
    var client = scope.ServiceProvider.GetRequiredService<LogsParserClient>();
    var report = await client.GetTopOperationsAsync(new TopOperationsQuery());

    Console.WriteLine(report.MetaInfo.TotalTransactions);
}
// Созданный внутри области transient-экземпляр LogsParserHttpDataSource освобождается здесь.

ASP.NET Core

using LogsParser;
using LogsParser.DependencyInjection;
using LogsParser.Models;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddLogsParser(options =>
{
    options.Credentials = new LogsParserCredentials(
        builder.Configuration["LogsParser:Login"]!,
        builder.Configuration["LogsParser:Password"]!,
        builder.Configuration["LogsParser:TotpSecret"]!);
});

var app = builder.Build();

app.MapGet("/logs/{serverId:int}", async (int serverId, [FromServices] LogsParserClient client) =>
{
    var page = await client.GetLogsAsync(new LogsQuery(ServerId: serverId, Limit: 100));
    return Results.Ok(page.Entries);
});

app.Run();

WebApplicationBuilder настраивает логирование до вызова Build(), поэтому поиск ILoggerFactory внутри фабрики источника данных находит фабрику хоста и логи библиотеки попадают в обычные провайдеры. Клиент — transient и разрешается на каждый запрос, то есть область запроса владеет созданным для неё источником данных и освобождает его.

Особенности времени жизни

LogsParserHttpDataSource реализует IDisposable и регистрируется как transient. Контейнер Microsoft отслеживает освобождаемые transient-объекты и освобождает их вместе с областью, в которой они были созданы.

  • Каждое разрешение LogsParserClient создаёт новый источник данных, а встроенная фабрика передаёт httpClient: null, поэтому каждый источник создаёт собственные HttpClient и HttpClientHandler и освобождает их при собственном освобождении. Разрешение клиента в тесном цикле расходует соединения — держите один клиент на всю единицу работы.
  • Хранилище cookie — singleton, поэтому аутентифицированная сессия переживает смену экземпляров источника данных, хотя каждый из них живёт недолго. Именно это избавляет от повторного входа и подтверждения TOTP.
  • Разрешение из корневого провайдера удерживает объекты по построению. Освобождаемый transient, полученный напрямую из корневого IServiceProvider, отслеживается корнем и освобождается только вместе со всем провайдером, то есть при завершении приложения. Предпочитайте provider.CreateScope(), область запроса ASP.NET Core или IServiceScopeFactory.
  • Если нужен более тонкий контроль, зарегистрируйте собственную фабрику: DataSourceFactory может возвращать долгоживущий экземпляр, которым вы владеете и освобождаете сами, либо экземпляр поверх общего HttpClient из IHttpClientFactory.

См. также

Clone this wiki locally