Skip to content

Requests and URI Builder

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

Requests and URI Builder

English Русский

This page documents the three query records that describe a request (LogsQuery, AdminActivityQuery, TopOperationsQuery) and the static LogsRequestUriBuilder that turns them into relative URI strings, together with every validation and normalisation rule the builder applies.

Query models

All three records live in the LogsParser.Models namespace and are sealed immutable records.

LogsQuery

public sealed record LogsQuery(
    int ServerId,
    IReadOnlyCollection<string>? Filters = null,
    DateTime? PeriodFrom = null,
    DateTime? PeriodTo = null,
    string? Player = null,
    string? Target = null,
    IPAddress? IpAddress = null,
    IReadOnlyDictionary<string, string>? AdditionalParameters = null,
    int Page = 1,
    int Limit = 1000,
    string Sort = "desc");
Parameter Type Default Description
ServerId int — (required) Game server number. Emitted as server_number. Must be greater than zero.
Filters IReadOnlyCollection<string>? null Log type codes. Each non-blank value is emitted as a separate type%5B%5D= entry.
PeriodFrom DateTime? null Start of the period. Emitted as min_period; omitted when null.
PeriodTo DateTime? null End of the period. Emitted as max_period; omitted when null.
Player string? null Nickname of the acting player. Emitted as player; omitted when null, empty or whitespace.
Target string? null Nickname of the target player. Emitted as target; omitted when null, empty or whitespace.
IpAddress IPAddress? null IP filter. Emitted as ip using IPAddress.ToString(); omitted when null.
AdditionalParameters IReadOnlyDictionary<string, string>? null Filter-specific dynamic[n] parameters. Keys must match ^dynamic\[\d+\]$.
Page int 1 Page number. Emitted as page. Must be greater than zero.
Limit int 1000 Rows per page. Emitted as limit after being snapped to 100, 500 or 1000.
Sort string "desc" Ordering. Only "desc" and "asc" are accepted, case-insensitively.

Filters and AdditionalParameters are the only collection-typed members, and both are optional — null simply means "no such parameter in the query string".

using System.Net;
using LogsParser.Models;

var query = new LogsQuery(
    ServerId: 18,
    Filters: ["connect", "kill"],
    PeriodFrom: new DateTime(2026, 5, 1, 3, 0, 0),
    PeriodTo: new DateTime(2026, 6, 1, 3, 0, 0),
    Player: "Ivan",
    IpAddress: IPAddress.Parse("127.0.0.1"),
    Page: 2,
    Limit: 500);

AdminActivityQuery

public sealed record AdminActivityQuery(DateTime PeriodFrom, DateTime PeriodTo);
Parameter Type Default Description
PeriodFrom DateTime — (required) Start of the reporting period. Emitted as min_period.
PeriodTo DateTime — (required) End of the reporting period. Emitted as max_period.

Both members are non-nullable, so both query parameters are always present. The builder does not check that PeriodFrom precedes PeriodTo.

using LogsParser.Models;

var query = new AdminActivityQuery(
    new DateTime(2026, 5, 1, 3, 0, 0),
    new DateTime(2026, 6, 1, 3, 0, 0));

TopOperationsQuery

public sealed record TopOperationsQuery(string? Filter = null, DateTime? Date = null);
Parameter Type Default Description
Filter string? null Operation type code. Emitted as type; omitted when null, empty or whitespace.
Date DateTime? null Report date. Emitted as date; omitted when null.

Both members are optional. With both left at null the builder produces the bare string top?.

using LogsParser.Models;

var query = new TopOperationsQuery("transfer", new DateTime(2026, 6, 1));

LogsRequestUriBuilder

public static partial class LogsRequestUriBuilder

Namespace LogsParser (the root namespace, not LogsParser.Models). The class is partial because its dynamic[n] key validation uses a [GeneratedRegex] source-generated method.

The three methods return relative URI strings intended for ParserRequest.RelativeUri, which is resolved against the data source's base URI (https://arizonarp.logsparser.info/ by default). The three shapes differ:

Method Returned shape Example
BuildLogsUri starts with ? ?server_number=18&sort=desc&limit=1000&page=1
BuildAdminActivityUri starts with /admins? /admins?min_period=…&max_period=…
BuildTopOperationsUri starts with top? (no leading slash) top?type=transfer&date=…

BuildLogsUri

public static string BuildLogsUri(LogsQuery query)

Builds the logs query string. Parameters are emitted in a fixed order: server_number, sort, limit, page, then every type%5B%5D entry, then min_period, max_period, player, target, ip, then the AdditionalParameters entries in the dictionary's own enumeration order.

Validation runs in that same order, so an invalid Sort is reported before an invalid Limit, and an invalid Limit before an invalid Page.

Throws When
ArgumentNullException query is null.
ArgumentOutOfRangeException ServerId is zero or negative; Limit is zero or negative; Page is zero or negative.
ArgumentException Sort is not "desc" or "asc"; an AdditionalParameters key collides with a reserved key or does not match ^dynamic\[\d+\]$.
using LogsParser;
using LogsParser.Models;

var uri = LogsRequestUriBuilder.BuildLogsUri(new LogsQuery(ServerId: 18));
// ?server_number=18&sort=desc&limit=1000&page=1

BuildAdminActivityUri

public static string BuildAdminActivityUri(AdminActivityQuery query)

Builds the admin activity URI. Both dates are always emitted, so the result always contains two parameters.

Throws When
ArgumentNullException query is null.
using LogsParser;
using LogsParser.Models;

var uri = LogsRequestUriBuilder.BuildAdminActivityUri(new AdminActivityQuery(
    new DateTime(2026, 5, 1, 3, 0, 0),
    new DateTime(2026, 6, 1, 3, 0, 0)));
// /admins?min_period=2026-05-01%2003%3A00%3A00&max_period=2026-06-01%2003%3A00%3A00

BuildTopOperationsUri

public static string BuildTopOperationsUri(TopOperationsQuery query)

Builds the top operations URI. Filter is emitted as type and Date as date; both are skipped when absent.

Throws When
ArgumentNullException query is null.
using LogsParser;
using LogsParser.Models;

var uri = LogsRequestUriBuilder.BuildTopOperationsUri(
    new TopOperationsQuery("transfer", new DateTime(2026, 6, 1)));
// top?type=transfer&date=2026-06-01%2000%3A00%3A00

Validation and normalisation rules

ServerId

Must be greater than zero. Zero or a negative value throws ArgumentOutOfRangeException with the message ServerId must be greater than zero. This is the first check after the null check.

// throws ArgumentOutOfRangeException
LogsRequestUriBuilder.BuildLogsUri(new LogsQuery(0));

Page

Must be greater than zero. Zero or a negative value throws ArgumentOutOfRangeException with the message page must be greater than zero. Any positive value is passed through unchanged.

// throws ArgumentOutOfRangeException
LogsRequestUriBuilder.BuildLogsUri(new LogsQuery(18, Page: 0));

Limit

The site supports exactly three page sizes — 100, 500 and 1000 — so any other positive value is snapped down to the nearest supported one. Zero or a negative value throws ArgumentOutOfRangeException with the message limit must be greater than zero.

Input Limit Emitted limit
<= 0 throws ArgumentOutOfRangeException
1 … 99 100
100 100
101 … 499 100
500 500
501 … 999 500
1000 1000
> 1000 1000

In short: anything below 500 becomes 100, anything below 1000 becomes 500, and 1000 or more becomes 1000; 100, 500 and 1000 pass through untouched.

LogsRequestUriBuilder.BuildLogsUri(new LogsQuery(18, Limit: 250)); // …&limit=100&…
LogsRequestUriBuilder.BuildLogsUri(new LogsQuery(18, Limit: 600)); // …&limit=500&…

Sort

Only "desc" (newest first) and "asc" (oldest first) are accepted. The comparison is case-insensitive, and the value is lowercased before being written into the query string, so "ASC" produces sort=asc. Anything else — including an empty string, a whitespace string and null — throws ArgumentException with the message Sort '<value>' is not supported. Use 'desc' (newest first) or 'asc' (oldest first).

LogsRequestUriBuilder.BuildLogsUri(new LogsQuery(18, Sort: "ASC"));        // …&sort=asc&…
LogsRequestUriBuilder.BuildLogsUri(new LogsQuery(18, Sort: "descending")); // throws ArgumentException

Filters

Each filter is emitted as its own repeated entry with a pre-encoded type[] key: type%5B%5D=<value>. Entries that are null, empty or whitespace are skipped silently; the value itself is URL-encoded.

var uri = LogsRequestUriBuilder.BuildLogsUri(new LogsQuery(18, Filters: ["connect", "kill"]));
// contains type%5B%5D=connect and type%5B%5D=kill

Dates

PeriodFrom, PeriodTo and Date are formatted with ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture) and the result is then URL-encoded, so the space becomes %20 and the colons become %3A. No time-zone conversion is performed — the DateTime is used exactly as given.

new DateTime(2026, 5, 1, 3, 0, 0)   // → min_period=2026-05-01%2003%3A00%3A00

AdditionalParameters

Entries whose key or value is null, empty or whitespace are skipped silently. Every remaining key is validated in two steps, both of which throw ArgumentException:

  1. Reserved-key collision. The key must not be one of the parameters the builder emits itself. The reserved set is compared with StringComparer.OrdinalIgnoreCase and contains exactly:

    server_number, sort, limit, page, type[], type, min_period, max_period, player, target, ip

    Message: Additional parameter '<key>' conflicts with a reserved logs query parameter.

  2. Shape. The key must match the regular expression ^dynamic\[\d+\]$ (matched case-insensitively). Message: Additional parameter '<key>' is invalid. Only dynamic[n] parameters from the logs page are supported.

Key and value are both URL-encoded, so dynamic[0] is written as dynamic%5B0%5D.

var uri = LogsRequestUriBuilder.BuildLogsUri(new LogsQuery(
    18,
    AdditionalParameters: new Dictionary<string, string> { ["dynamic[0]"] = "42" }));
// contains dynamic%5B0%5D=42

// throws ArgumentException — reserved key
LogsRequestUriBuilder.BuildLogsUri(new LogsQuery(
    18,
    AdditionalParameters: new Dictionary<string, string> { ["player"] = "x" }));

// throws ArgumentException — not a dynamic[n] key
LogsRequestUriBuilder.BuildLogsUri(new LogsQuery(
    18,
    AdditionalParameters: new Dictionary<string, string> { ["foo"] = "x" }));

Where valid dynamic[n] keys come from

The dynamic[n] keys are not arbitrary: each one belongs to a specific log filter and is published by the site itself in the logs page markup. They reach you through the filter catalogue as LogsFilterCatalog.Filters[].AdditionalParameters[].QueryKey:

public sealed record LogsFilterCatalog(
    IReadOnlyList<LogsFilterDefinition> Filters,
    LogsAccount? Account = null);
public sealed record LogsFilterDefinition(
    string Code,
    string Name,
    IReadOnlyList<LogsFilterAdditionalParameter> AdditionalParameters);
public sealed record LogsFilterAdditionalParameter(
    string QueryKey,
    string Label);

QueryKey is the dynamic[n] string to use as a dictionary key, and Label is the human-readable caption the site shows next to the input. Read the catalogue once, pick the filter you need, and feed its QueryKey straight into LogsQuery.AdditionalParameters:

using LogsParser;
using LogsParser.Models;

var catalog = await client.GetLogsFilterCatalogAsync();

var filter = catalog.Filters.First(definition => definition.Code == "transfer");
var extra = filter.AdditionalParameters.First(parameter => parameter.Label.Contains("сумма"));

var page = await client.GetLogsAsync(new LogsQuery(
    ServerId: 18,
    Filters: [filter.Code],
    AdditionalParameters: new Dictionary<string, string> { [extra.QueryKey] = "1000000" }));

Because the keys come from the live page, a filter with no extra inputs simply has an empty AdditionalParameters list — the collection is never null.

Worked example

using System.Net;
using LogsParser;
using LogsParser.Models;

var query = new LogsQuery(
    ServerId: 18,
    Filters: ["connect", "kill"],
    PeriodFrom: new DateTime(2026, 5, 1, 3, 0, 0),
    PeriodTo: new DateTime(2026, 6, 1, 3, 0, 0),
    Player: "Ivan",
    IpAddress: IPAddress.Parse("127.0.0.1"),
    Page: 2,
    Limit: 600,
    Sort: "ASC");

Console.WriteLine(LogsRequestUriBuilder.BuildLogsUri(query));

Output:

?server_number=18&sort=asc&limit=500&page=2&type%5B%5D=connect&type%5B%5D=kill&min_period=2026-05-01%2003%3A00%3A00&max_period=2026-06-01%2003%3A00%3A00&player=Ivan&ip=127.0.0.1

Three normalisations are visible in that one line: Sort: "ASC" was lowercased to sort=asc, Limit: 600 was snapped down to limit=500, and both dates were formatted with the invariant culture and URL-encoded. Target was left null, so no target parameter appears at all.

No HTTP involved

LogsRequestUriBuilder is a static class of pure functions: it takes a record, returns a string, and never opens a connection, touches cookies or reads configuration. That makes it usable on its own — for logging the exact URI a call will produce, for unit tests, or for building requests you intend to execute through your own HTTP stack instead of LogsParserHttpDataSource.

using LogsParser;
using LogsParser.Models;

var relativeUri = LogsRequestUriBuilder.BuildLogsUri(new LogsQuery(18, Limit: 100));
var absolute = new Uri(new Uri("https://arizonarp.logsparser.info/"), relativeUri);

Console.WriteLine(absolute);

The same string is what LogsParserClient passes to the data source as ParserRequest.RelativeUri:

public sealed record ParserRequest(string RelativeUri, ICookieStorage? CookieStorage = null);

See also

Clone this wiki locally