Skip to content

Dependency Injection

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

Dependency Injection

English Русский

This page documents LogsParserServiceCollectionExtensions — the two AddLogsParser overloads, the services they register, their lifetimes and the LogsParserRegistrationOptions object that configures them.

LogsParserServiceCollectionExtensions

namespace LogsParser.DependencyInjection;

public static class LogsParserServiceCollectionExtensions

The library depends on Microsoft.Extensions.DependencyInjection directly, so no extra package is needed to use these extensions.

AddLogsParser(IServiceCollection)

public static IServiceCollection AddLogsParser(this IServiceCollection services)

Registers the whole pipeline with default settings and no credentials.

Parameter Type Default Description
services IServiceCollection — The collection the services are added to. Returned unchanged for chaining.

Returns the same IServiceCollection instance.

Throws:

Exception When
ArgumentNullException services is 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)

Creates a fresh LogsParserRegistrationOptions, hands it to configure, then registers the pipeline according to the values the callback left behind.

Parameter Type Default Description
services IServiceCollection — The collection the services are added to. Returned unchanged for chaining.
configure Action<LogsParserRegistrationOptions> — Callback invoked once, immediately, with a new options instance.

Returns the same IServiceCollection instance.

Throws:

Exception When
ArgumentNullException services is null
ArgumentNullException configure is 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");
});

What gets registered

Service Implementation Lifetime
ICookieStorage MemoryCookieStorage Singleton
LogsParserHttpOptions The instance held by LogsParserRegistrationOptions.HttpOptions; the parameterless overload registers new LogsParserHttpOptions() Singleton
ILogsParserDataSource LogsParserHttpDataSource, built by an internal factory Transient
LogsParserClient LogsParserClient Transient
LogsParserCredentials The instance held by LogsParserRegistrationOptions.Credentials — registered only when it is not null Singleton

LogsParserCredentials is never registered by the parameterless overload and is never registered when options.Credentials stays null. The data source resolves it with GetService<LogsParserCredentials>(), so its absence is legal and simply produces an unauthenticated transport — enough for pages the site serves anonymously, and enough for parsing-only work.

The concrete LogsParserHttpDataSource type is not registered on its own; only ILogsParserDataSource is. To read the rate-limit properties, which live on the concrete class, cast the resolved instance:

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}");
}

Registration semantics

Two different mechanisms are in play, and the difference decides who wins when a service is registered twice.

  • Defaults use TryAdd*. ICookieStorage, LogsParserHttpOptions, ILogsParserDataSource and LogsParserClient are added with TryAddSingleton / TryAddTransient. If the caller already registered that service type before calling AddLogsParser, the earlier registration stays and the library's default is skipped.
  • Factories from the options use Replace. When CookieStorageFactory or DataSourceFactory is not null, the corresponding descriptor is applied with services.Replace(...), which drops the first existing descriptor for that service type and appends the new one. A factory supplied through the options therefore overrides any earlier registration of that service.
var services = new ServiceCollection();

// Registered first — survives, because the default is a TryAdd.
services.AddSingleton<ICookieStorage>(new MemoryCookieStorage());

services.AddLogsParser(options =>
{
    // Not supplied, so the line above wins.
    // If it were supplied, Replace would remove the line above.
    options.HttpOptions = new LogsParserHttpOptions { MaxRetryAttempts = 3 };
});

Calling AddLogsParser twice is therefore idempotent for the defaults, but the second call's CookieStorageFactory or DataSourceFactory would replace what the first call registered.

LogsParserRegistrationOptions

namespace LogsParser.Models;

public sealed class LogsParserRegistrationOptions

A mutable settings object, used only during registration. It is not itself registered in the container.

Property Type Default Description
Credentials LogsParserCredentials? null Login, password and Base32 TOTP secret. Registered as a singleton when not null; when null, the transport runs unauthenticated.
HttpOptions LogsParserHttpOptions new() Base URI, User-Agent, Accept, MaxRetryAttempts, WaitForRateLimitReset. Always registered as a singleton instance (with TryAddSingleton).
CookieStorageFactory Func<IServiceProvider, ICookieStorage>? null When set, registered as the ICookieStorage singleton via Replace, instead of MemoryCookieStorage.
DataSourceFactory Func<IServiceProvider, ILogsParserDataSource>? null When set, registered as the ILogsParserDataSource transient via Replace, instead of the built-in LogsParserHttpDataSource factory.

LogsParserCredentials and LogsParserHttpOptions are documented in full on Models; the runtime meaning of the HTTP options is on Transport and Authentication.

Logging

The built-in data source factory resolves ILoggerFactory from the container and falls back to NullLoggerFactory.Instance when none is registered. The factory it settles on is passed to LogsParserHttpDataSource and also installed process-wide through LogsParserLogging.UseLoggerFactory.

Two consequences follow:

  • Call services.AddLogging(...) before the first service resolution if you want library logs. The lookup happens when ILogsParserDataSource is first constructed; a logging provider added to the collection afterwards is irrelevant, because the collection has already been built into a provider.
  • When you supply your own DataSourceFactory, the built-in factory never runs, so nothing rewires LogsParserLogging. Call LogsParserLogging.UseLoggerFactory(...) yourself, or pass an ILoggerFactory into the LogsParserHttpDataSource constructor inside your factory.
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();

See Logging for categories and the level taxonomy.

Examples

Minimal registration, no credentials

Enough for anonymous pages and for code that only needs the client to reach a parser.

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");

Full registration with credentials and HTTP options

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));

Persistent cookie storage through CookieStorageFactory

The default MemoryCookieStorage loses the session when the process exits, which forces a fresh login plus TOTP confirmation on every start. A storage that survives restarts is registered through 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");
});

The factory receives the IServiceProvider, so the storage may take its own dependencies:

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

Both the session cookies and the anti-DDoS token share this one flat store — see Cookie Storage.

Replacing the transport through DataSourceFactory

DataSourceFactory swaps out the whole HTTP layer: useful for tests, for a proxy-aware HttpClient, or for a cached/offline source.

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,      // mandatory: the 302s are the auth protocol
            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>());
    };
});

An injected HttpClient whose handler follows redirects breaks authentication silently: the library never sees the 302 and returns login-page HTML as content. An injected client also keeps its own BaseAddress and User-Agent in preference to the LogsParserHttpOptions values — set them yourself if they matter.

A stub source is the same shape, and is exactly how the test suite overrides the transport:

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>"));

Resolving the client from a scope

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);
}
// The transient LogsParserHttpDataSource created inside the scope is disposed here.

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 configures logging before Build(), so the ILoggerFactory lookup inside the data source factory finds the host's factory and library logs flow into the usual providers. The client is transient and resolved per request, which means the request scope owns and disposes the data source created for it.

Lifetime caveats

LogsParserHttpDataSource implements IDisposable and is registered as transient. Microsoft's container tracks disposable transients and disposes them together with the scope that created them.

  • Every resolution of LogsParserClient builds a new data source, and the built-in factory passes httpClient: null, so every data source builds its own HttpClient and HttpClientHandler and disposes them on its own disposal. Resolving the client in a tight loop churns connections; hold one client for the duration of a unit of work.
  • The cookie storage is a singleton, so the authenticated session survives across data source instances even though each instance is short-lived. That is what keeps the login/TOTP round trip from repeating.
  • Resolving from the root provider leaks by design. A transient disposable resolved straight from the root IServiceProvider is tracked by the root and disposed only when the whole provider is disposed — i.e. at application shutdown. Prefer provider.CreateScope(), an ASP.NET Core request scope, or an IServiceScopeFactory.
  • When you need tighter control, register your own factory: DataSourceFactory may hand back a long-lived instance you own and dispose yourself, or one built over a shared HttpClient from IHttpClientFactory.

See also

Clone this wiki locally