Перейти к основному содержимому

Примеры решений в C#

1. Работа с коллекциями и LINQ

1.1. Безопасное преобразование IEnumerableList с проверкой на null

public static List<T> ToSafeList<T>(this IEnumerable<T>? source)
{
return source?.ToList() ?? new List<T>();
}

⚠️ Используется при получении данных из внешних API или ORM, где null — допустимое значение.

1.2. Группировка с агрегацией и фильтрацией (типичный кейс отчётов)

var report = orders
.Where(o => o.Status == OrderStatus.Completed && o.Date >= startDate)
.GroupBy(o => o.CustomerId)
.Select(g => new
{
CustomerId = g.Key,
TotalOrders = g.Count(),
TotalAmount = g.Sum(o => o.Amount),
AvgAmount = g.Average(o => o.Amount),
LastOrderDate = g.Max(o => o.Date)
})
.ToList();

1.3. Разбиение коллекции на чанки (для пакетной обработки)

public static IEnumerable<List<T>> Chunk<T>(this IEnumerable<T> source, int chunkSize)
{
if (chunkSize <= 0) throw new ArgumentException("Chunk size must be positive.", nameof(chunkSize));

return source
.Select((item, index) => new { item, index })
.GroupBy(x => x.index / chunkSize)
.Select(g => g.Select(x => x.item).ToList());
}

// Использование:
foreach (var batch in largeList.Chunk(100))
{
await ProcessBatchAsync(batch);
}

🔹 C# 11+ предоставляет встроенный Enumerable.Chunk(), но данный вариант совместим с .NET Standard 2.0.


2. Обработка строк и текста

2.1. Безопасный парсинг числа с локализацией

public static bool TryParseDecimal(string? input, out decimal result, IFormatProvider? provider = null)
{
result = default;
if (string.IsNullOrWhiteSpace(input)) return false;

// Убираем неразрывные пробелы и заменяем запятую на точку при необходимости
var normalized = input.Replace('\u00A0', ' ').Trim();
return decimal.TryParse(normalized, NumberStyles.Number | NumberStyles.AllowDecimalPoint, provider ?? CultureInfo.InvariantCulture, out result);
}

2.2. Поиск подстроки с учётом диакритики (нечувствительно к «ё/е», «й/i» и т.п.)

public static bool ContainsInvariant(this string? source, string? value, StringComparison comparison = StringComparison.OrdinalIgnoreCase)
{
if (source == null || value == null) return false;
return source.Normalize(NormalizationForm.FormD)
.IndexOf(value.Normalize(NormalizationForm.FormD), comparison) >= 0;
}

⚠️ Для высоконагруженных сценариев — предварительно нормализовать и кэшировать.

2.3. Построение пути с валидацией и нормализацией

public static string SafeCombinePath(string basePath, params string[] parts)
{
if (string.IsNullOrEmpty(basePath))
throw new ArgumentException("Base path cannot be null or empty.", nameof(basePath));

var path = Path.GetFullPath(basePath);
foreach (var part in parts)
{
if (string.IsNullOrWhiteSpace(part)) continue;
// Защита от path traversal
if (part.Contains("..") || part.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
throw new ArgumentException($"Invalid path segment: '{part}'");
path = Path.Combine(path, part);
}
return Path.GetFullPath(path);
}

3. Асинхронность и конкурентность

3.1. SemaphoreSlim для ограничения параллельных вызовов API

private static readonly SemaphoreSlim _apiThrottle = new(5, 5); // макс. 5 одновременных запросов

public async Task<ApiResponse> CallThrottledApiAsync(string endpoint)
{
await _apiThrottle.WaitAsync().ConfigureAwait(false);
try
{
using var httpClient = _httpClientFactory.CreateClient("ExternalApi");
var response = await httpClient.GetAsync(endpoint).ConfigureAwait(false);
return await response.Content.ReadFromJsonAsync<ApiResponse>().ConfigureAwait(false);
}
finally
{
_apiThrottle.Release();
}
}

3.2. Отмена длительных операций с таймаутом

public async Task<T> ExecuteWithTimeoutAsync<T>(
Func<CancellationToken, Task<T>> operation,
TimeSpan timeout,
CancellationToken externalToken = default)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(externalToken);
cts.CancelAfter(timeout);

try
{
return await operation(cts.Token).ConfigureAwait(false);
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
throw new TimeoutException($"Operation timed out after {timeout}.");
}
}

3.3. Асинхронный итератор с поддержкой отмены

public static async IAsyncEnumerable<T> ReadLinesAsync(
string filePath,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await using var reader = new StreamReader(filePath);
string? line;
while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null)
{
cancellationToken.ThrowIfCancellationRequested();
yield return line;
}
}

4. Работа с датами и временем

4.1. Преобразование в UTC с учётом летнего времени и часовых поясов

public static DateTime ToUniversalTimeSafe(this DateTime localTime, string timeZoneId)
{
if (localTime.Kind == DateTimeKind.Utc) return localTime;

var tz = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
return TimeZoneInfo.ConvertTimeToUtc(localTime, tz);
}

// Использование:
var moscowTime = new DateTime(2025, 6, 15, 10, 0, 0); // лето, UTC+3
var utc = moscowTime.ToUniversalTimeSafe("Russian Standard Time"); // → 07:00 UTC

4.2. Генерация временных интервалов (для графиков, агрегаций)

public static IEnumerable<DateTime> GenerateIntervals(
DateTime start,
DateTime end,
TimeSpan interval,
bool includeEnd = false)
{
if (interval <= TimeSpan.Zero)
throw new ArgumentException("Interval must be positive.", nameof(interval));

for (var current = start; current < end || (includeEnd && current == end); current += interval)
{
yield return current;
}
}

// Пример: часовые интервалы за сутки
var hours = GenerateIntervals(
DateTime.Today,
DateTime.Today.AddDays(1),
TimeSpan.FromHours(1));

5. Обработка исключений и логирование

5.1. Оборачивание внешнего кода с унифицированным логированием

public static async Task<T> TryExecuteAsync<T>(
Func<Task<T>> action,
string operationName,
ILogger logger,
Func<Exception, bool>? isTransient = null)
{
try
{
return await action().ConfigureAwait(false);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
var severity = isTransient?.Invoke(ex) == true ? LogLevel.Warning : LogLevel.Error;
logger.Log(severity, ex, "Failed to execute {Operation}", operationName);
throw;
}
}

5.2. Safe-вызов делегата с fallback-значением

public static T SafeInvoke<T>(
Func<T> func,
T fallback = default!,
Action<Exception>? onError = null)
{
try
{
return func();
}
catch (Exception ex)
{
onError?.Invoke(ex);
return fallback;
}
}

// Пример:
var version = SafeInvoke(() => Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "unknown");

6. Работа с JSON (System.Text.Json)

6.1. Полиморфная десериализация по полю-дискриминатору

Часто требуется десериализовать JSON, где тип объекта определяется по значению поля (например, eventType). Встроенная поддержка JsonPolymorphic появилась в .NET 7, но ниже — совместимый с .NET 6+ кастомный конвертер:

[JsonConverter(typeof(PolymorphicEventConverter))]
public abstract record EventBase(string EventType);

public record UserCreatedEvent(string UserId, string Email) : EventBase("UserCreated");
public record OrderPlacedEvent(Guid OrderId, decimal Amount) : EventBase("OrderPlaced");

public class PolymorphicEventConverter : JsonConverter<EventBase>
{
public override EventBase? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using var doc = JsonDocument.ParseValue(ref reader);
var root = doc.RootElement;

if (!root.TryGetProperty("EventType", out var eventTypeElem) || eventTypeElem.ValueKind != JsonValueKind.String)
throw new JsonException("Missing or invalid 'EventType' field.");

return eventTypeElem.GetString() switch
{
"UserCreated" => JsonSerializer.Deserialize<UserCreatedEvent>(root, options),
"OrderPlaced" => JsonSerializer.Deserialize<OrderPlacedEvent>(root, options),
_ => throw new JsonException($"Unknown event type: {eventTypeElem.GetString()}")
};
}

public override void Write(Utf8JsonWriter writer, EventBase value, JsonSerializerOptions options)
=> JsonSerializer.Serialize(writer, value, value.GetType(), options);
}

✅ Преимущества:
— Чёткое разделение типов;
— Валидация на этапе десериализации;
— Поддержка record и неизменяемых сущностей.

6.2. Обработка null-значений при десериализации (без исключений)

public class SafeNumberConverter : JsonConverter<decimal>
{
public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.TokenType switch
{
JsonTokenType.Number => reader.GetDecimal(),
JsonTokenType.String when decimal.TryParse(reader.GetString(), out var result) => result,
JsonTokenType.Null => 0m, // или throw, в зависимости от политики
_ => throw new JsonException($"Unexpected token {reader.TokenType} for decimal.")
};
}

public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
=> writer.WriteNumberValue(value);
}

// Использование:
var opts = new JsonSerializerOptions { Converters = { new SafeNumberConverter() } };
var obj = JsonSerializer.Deserialize<MyDto>(json, opts);

6.3. Сериализация в JSON с сокрытием чувствительных полей (например, в логах)

public class SensitiveDataAttribute : JsonIgnoreConditionAttribute
{
public override bool ShouldSerialize(object? value, JsonSerializerOptions options)
=> !Debugger.IsAttached; // или Environment.IsDevelopment()
}

public record UserDto
{
public string Name { get; init; }

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
[JsonInclude]
public string? Email { get; init; }

[JsonIgnore]
[SensitiveData]
public string? PasswordHash { get; init; }
}

🔹 Альтернатива — IJsonOnSerializing, но атрибутный подход компактнее для массового применения.


7. Работа с базами данных и ORM

7.1. Пакетная вставка через Microsoft.Data.SqlClient (без ORM)

public async Task BulkInsertUsersAsync(IEnumerable<User> users, string connectionString)
{
const string sql = @"
INSERT INTO Users (Id, Name, Email, CreatedAt)
VALUES (@Id, @Name, @Email, @CreatedAt)";

using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();

using var transaction = connection.BeginTransaction();
try
{
using var command = new SqlCommand(sql, connection, transaction);
command.Parameters.Add("@Id", SqlDbType.UniqueIdentifier);
command.Parameters.Add("@Name", SqlDbType.NVarChar, 100);
command.Parameters.Add("@Email", SqlDbType.NVarChar, 255);
command.Parameters.Add("@CreatedAt", SqlDbType.DateTime2);

foreach (var user in users)
{
command.Parameters["@Id"].Value = user.Id;
command.Parameters["@Name"].Value = user.Name ?? (object)DBNull.Value;
command.Parameters["@Email"].Value = user.Email ?? (object)DBNull.Value;
command.Parameters["@CreatedAt"].Value = user.CreatedAt;

await command.ExecuteNonQueryAsync();
}

transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}

⚠️ Для больших объёмов (>10k записей) — использовать SqlBulkCopy.

7.2. SqlBulkCopy с DataTable (устойчивый к изменениям схемы)

public async Task BulkCopyAsync<T>(
IEnumerable<T> items,
string tableName,
string connectionString,
Func<T, object[]> rowMapper)
{
var table = new DataTable();
// Определяем схему по первой записи (можно кэшировать)
var sample = items.FirstOrDefault();
if (sample == null) return;

var props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (var prop in props)
{
// Используем SqlDbTypeMapper или простую логику
table.Columns.Add(prop.Name, PropertyTypeToClrType(prop.PropertyType));
}

foreach (var item in items)
{
table.Rows.Add(rowMapper(item));
}

using var bulkCopy = new SqlBulkCopy(connectionString)
{
DestinationTableName = tableName,
BatchSize = 1000,
BulkCopyTimeout = 300
};

await bulkCopy.WriteToServerAsync(table);
}

private static Type PropertyTypeToClrType(Type type)
{
return Nullable.GetUnderlyingType(type) ?? type;
}

7.3. Безопасный вызов хранимой процедуры с выходными параметрами

public async Task<(int ResultCode, string? Message)> ExecuteStoredProcedureAsync(
string procedureName,
SqlParameter[] inputParams,
string connectionString)
{
using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();

using var command = new SqlCommand(procedureName, connection)
{
CommandType = CommandType.StoredProcedure
};

command.Parameters.AddRange(inputParams);

var outputCode = new SqlParameter("@ResultCode", SqlDbType.Int) { Direction = ParameterDirection.Output };
var outputMsg = new SqlParameter("@Message", SqlDbType.NVarChar, 500) { Direction = ParameterDirection.Output };

command.Parameters.Add(outputCode);
command.Parameters.Add(outputMsg);

await command.ExecuteNonQueryAsync();

return ((int)outputCode.Value, outputMsg.Value as string);
}

8. Рефлексия и метапрограммирование

8.1. Быстрое получение значения свойства без PropertyInfo.GetValue

public static class FastPropertyAccessor
{
private static readonly ConcurrentDictionary<(Type, string), Func<object, object?>> _cache = new();

public static Func<object, object?> GetGetter(Type type, string propertyName)
{
return _cache.GetOrAdd((type, propertyName), key =>
{
var prop = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance)
?? throw new ArgumentException($"Property '{propertyName}' not found on {type}.");

var param = Expression.Parameter(typeof(object));
var cast = Expression.Convert(param, type);
var access = Expression.Property(cast, prop);
var convert = Expression.Convert(access, typeof(object));
return Expression.Lambda<Func<object, object?>>(convert, param).Compile();
});
}
}

// Использование:
var getter = FastPropertyAccessor.GetGetter(typeof(User), "Email");
var email = getter(userInstance) as string;

📈 Производительность: ~100x быстрее, чем PropertyInfo.GetValue() при многократном вызове.

8.2. Создание экземпляра по имени типа (с DI-инъекцией, если возможно)

public static class ActivatorUtilitiesEx
{
public static T CreateInstance<T>(
IServiceProvider provider,
string typeName,
params object[] args)
{
var type = Type.GetType(typeName, throwOnError: true);
if (type == null) throw new InvalidOperationException($"Type '{typeName}' not found.");

var ctor = type.GetConstructors()
.Where(c => c.GetParameters().Length == args.Length)
.OrderByDescending(c => c.GetParameters().Count(p => p.HasDefaultValue))
.FirstOrDefault();

if (ctor == null)
throw new InvalidOperationException($"No suitable constructor found for {typeName} with {args.Length} args.");

var parameters = ctor.GetParameters();
var resolvedArgs = new object[parameters.Length];

for (int i = 0; i < parameters.Length; i++)
{
if (i < args.Length && args[i] != null && args[i].GetType().IsAssignableTo(parameters[i].ParameterType))
resolvedArgs[i] = args[i];
else
resolvedArgs[i] = provider.GetService(parameters[i].ParameterType)
?? (parameters[i].HasDefaultValue ? parameters[i].DefaultValue : null);
}

return (T)Activator.CreateInstance(type, resolvedArgs)!;
}
}

✅ Подходит для динамических плагинов, BPM-правил, настраиваемых обработчиков.


9. Тестирование и отладка

9.1. Параметризованный xUnit-тест с теорией и кастомным MemberData

public class MathTests
{
public static TheoryData<int, int, int> AdditionTestData =>
new()
{
{ 1, 2, 3 },
{ -1, 1, 0 },
{ int.MaxValue, 1, int.MinValue }, // overflow test
};

[Theory]
[MemberData(nameof(AdditionTestData))]
public void Add_ShouldReturnCorrectSum(int a, int b, int expected)
{
var actual = Calculator.Add(a, b);
Assert.Equal(expected, actual);
}
}

9.2. Мок зависимостей через Moq с валидацией вызовов

[Fact]
public async Task ProcessOrder_ShouldPublishEvent_WhenValid()
{
// Arrange
var repositoryMock = new Mock<IOrderRepository>();
var publisherMock = new Mock<IEventPublisher>();

repositoryMock.Setup(r => r.GetByIdAsync(It.IsAny<Guid>()))
.ReturnsAsync(new Order { Id = Guid.NewGuid(), Status = OrderStatus.New });

var service = new OrderService(repositoryMock.Object, publisherMock.Object);

// Act
await service.ProcessAsync(Guid.NewGuid());

// Assert
publisherMock.Verify(p => p.PublishAsync(It.Is<OrderProcessedEvent>(e =>
e.Status == OrderStatus.Processed)), Times.Once);
}

9.3. Тестирование исключений с точной проверкой типа и сообщения

[Fact]
public void DivideByZero_ShouldThrowDivideByZeroException()
{
var ex = Assert.Throws<DivideByZeroException>(() => Calculator.Divide(10, 0));
Assert.Contains("division by zero", ex.Message, StringComparison.OrdinalIgnoreCase);
}

10. Внедрение зависимостей (DI) и жизненные циклы

10.1. Keyed Services (начиная с .NET 8) — выбор зависимости по ключу

// Регистрация
builder.Services.AddKeyedSingleton<INotificationService, EmailService>("email");
builder.Services.AddKeyedSingleton<INotificationService, SmsService>("sms");

// Использование в сервисе
public class OrderService
{
private readonly IKeyedServiceProvider _keyedProvider;

public OrderService(IKeyedServiceProvider keyedProvider)
=> _keyedProvider = keyedProvider;

public async Task NotifyAsync(Guid userId, string channel)
{
var service = _keyedProvider.GetRequiredKeyedService<INotificationService>(channel);
await service.SendAsync(userId, "Your order is confirmed.");
}
}

⚠️ Для .NET 6/7 — эмуляция через фабрику:

services.AddSingleton<Func<string, INotificationService>>(sp => key =>
key switch
{
"email" => sp.GetRequiredService<EmailService>(),
"sms" => sp.GetRequiredService<SmsService>(),
_ => throw new ArgumentException($"Unknown channel: {key}")
});

10.2. Scoped-зависимости в фоновых задачах (без утечек)

public class BackgroundWorker : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;

public BackgroundWorker(IServiceScopeFactory scopeFactory)
=> _scopeFactory = scopeFactory;

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);

// Создаём *новую* область видимости на каждую итерацию
using var scope = _scopeFactory.CreateScope();
var processor = scope.ServiceProvider.GetRequiredService<IOrderProcessor>();

try
{
await processor.ProcessPendingOrdersAsync(stoppingToken);
}
catch (Exception ex)
{
// Логируем, но не завершаем сервис
_logger.LogError(ex, "Order processing failed.");
}
}
}
}

✅ Гарантирует корректное освобождение DbContext, HttpClient, MemoryCache и т.п.

10.3. Фабрика с параметризацией конструктора (без рефлексии в hot path)

public interface IReportGeneratorFactory
{
IReportGenerator Create(string format, ReportContext context);
}

public class ReportGeneratorFactory : IReportGeneratorFactory
{
private readonly IServiceProvider _provider;

public ReportGeneratorFactory(IServiceProvider provider)
=> _provider = provider;

public IReportGenerator Create(string format, ReportContext context)
{
return format.ToLowerInvariant() switch
{
"pdf" => new PdfReportGenerator(context, _provider.GetRequiredService<IPdfRenderer>()),
"xlsx" => new ExcelReportGenerator(context, _provider.GetRequiredService<IExcelWriter>()),
"json" => ActivatorUtilities.CreateInstance<JsonReportGenerator>(_provider, context),
_ => throw new NotSupportedException($"Format '{format}' is not supported.")
};
}
}

11. Безопасность

11.1. Хеширование паролей с Microsoft.AspNetCore.Identity

public static class PasswordHasher
{
private static readonly PasswordHasher<object> _hasher = new();

public static string Hash(string password)
=> _hasher.HashPassword(new object(), password);

public static bool Verify(string hash, string password)
=> _hasher.VerifyHashedPassword(new object(), hash, password) != PasswordVerificationResult.Failed;
}

🔐 Использует PBKDF2 с 10 000 итераций по умолчанию (настраивается через PasswordHasherOptions).

11.2. Шифрование данных с AesGcm (AEAD, .NET Core 3.0+)

public static class AesGcmHelper
{
private const int NonceSize = 12; // 96 бит — рекомендовано NIST
private const int TagSize = 16;

public static byte[] Encrypt(byte[] plaintext, byte[] key)
{
var nonce = RandomNumberGenerator.GetBytes(NonceSize);
var ciphertext = new byte[plaintext.Length];
var tag = new byte[TagSize];

using var aes = new AesGcm(key);
aes.Encrypt(nonce, plaintext, ciphertext, tag);

return nonce.Concat(ciphertext).Concat(tag).ToArray(); // nonce || ciphertext || tag
}

public static byte[] Decrypt(byte[] encrypted, byte[] key)
{
if (encrypted.Length < NonceSize + TagSize)
throw new ArgumentException("Invalid encrypted data length.");

var nonce = encrypted[..NonceSize];
var tag = encrypted[^TagSize..];
var ciphertext = encrypted[NonceSize..^TagSize];

var plaintext = new byte[ciphertext.Length];
using var aes = new AesGcm(key);
aes.Decrypt(nonce, ciphertext, tag, plaintext);

return plaintext;
}
}

✅ Использовать для шифрования токенов, временных секретов, PII в кэше.

11.3. Валидация JWT с кастомными claims и отменой токенов

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "https://api.example.com",
ValidateAudience = true,
ValidAudience = "my-app",
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(5),
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("very-secret-key-256-bit"))
};

options.Events = new JwtBearerEvents
{
OnTokenValidated = async context =>
{
var tokenId = context.Principal.FindFirstValue("jti");
if (tokenId == null) return;

var cache = context.HttpContext.RequestServices.GetRequiredService<IDistributedCache>();
var revoked = await cache.GetStringAsync($"revoked:{tokenId}");
if (revoked != null)
context.Fail("Token has been revoked.");
}
};
});

📌 Отзыв токена: await cache.SetStringAsync($"revoked:{jti}", "1", new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1) });


12. Работа с файлами и потоками

12.1. Построчное чтение больших файлов без загрузки в память

public static async IAsyncEnumerable<string> ReadLinesChunkedAsync(
string path,
int bufferSize = 65536,
[EnumeratorCancellation] CancellationToken ct = default)
{
await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize, FileOptions.SequentialScan);
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize);

string? line;
while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null)
{
ct.ThrowIfCancellationRequested();
yield return line;
}
}

12.2. Запись в ZIP-архив с потоковой обработкой (без временных файлов)

public async Task CreateZipFromStreamsAsync(
Dictionary<string, Stream> entries,
Stream outputZipStream,
CancellationToken ct = default)
{
using var archive = new ZipArchive(outputZipStream, ZipArchiveMode.Create, leaveOpen: true);

foreach (var (filename, sourceStream) in entries)
{
ct.ThrowIfCancellationRequested();

var entry = archive.CreateEntry(filename, CompressionLevel.Optimal);
await using var entryStream = entry.Open();
await sourceStream.CopyToAsync(entryStream, ct);
}
}

12.3. Memory-mapped файл для быстрого доступа к большим данным

public class MappedFileReader : IDisposable
{
private readonly MemoryMappedFile _mmf;
private readonly MemoryMappedViewAccessor _accessor;

public MappedFileReader(string path)
{
var fileInfo = new FileInfo(path);
if (!fileInfo.Exists) throw new FileNotFoundException(path);

_mmf = MemoryMappedFile.CreateFromFile(path, FileMode.Open, null, 0, MemoryMappedFileAccess.Read);
_accessor = _mmf.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read);
}

public Span<byte> GetSpan(long offset, int count)
{
// Внимание: Span недолговечен; копируйте при необходимости
return _accessor.AsSpan<byte>(offset, count);
}

public void Dispose()
{
_accessor?.Dispose();
_mmf?.Dispose();
}
}

⚠️ Используйте только в высоконагруженных сценариях (индексы, логи, бинарные форматы). Избегайте Span в async-методах.


13. Метрики и мониторинг (OpenTelemetry)

13.1. Ручное создание метрик (counter, histogram)

public class OrderMetrics
{
private readonly Counter<long> _ordersCounter;
private readonly Histogram<double> _processingTimeHistogram;

public OrderMetrics(Meter meter)
{
_ordersCounter = meter.CreateCounter<long>("orders_processed", description: "Total orders processed");
_processingTimeHistogram = meter.CreateHistogram<double>("order_processing_time", unit: "ms", description: "Order processing latency");
}

public void RecordOrderProcessed(string status, double durationMs)
{
_ordersCounter.Add(1, new("status", status));
_processingTimeHistogram.Record(durationMs, new("status", status));
}
}

// Регистрация:
var meter = new Meter("MyApp.Metrics", "1.0");
services.AddSingleton(new OrderMetrics(meter));
services.AddOpenTelemetry()
.WithMetrics(builder => builder
.AddMeter("MyApp.Metrics")
.AddPrometheusExporter()); // или .AddOtlpExporter()

13.2. Трассировка с контекстом (распространение traceparent)

public async Task ProcessOrderWithTracingAsync(Guid orderId)
{
using var activity = MyActivitySource.StartActivity("ProcessOrder");
activity?.SetTag("order.id", orderId);

try
{
await ValidateOrderAsync(orderId);
await ChargePaymentAsync(orderId);
await ShipOrderAsync(orderId);

activity?.SetStatus(ActivityStatusCode.Ok);
}
catch (Exception ex)
{
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
activity?.AddEvent(new("error", tags: new ActivityTagsCollection { { "exception.message", ex.Message } }));
throw;
}
}

private static readonly ActivitySource MyActivitySource = new("MyApp");

✅ Интеграция с Jaeger, Zipkin, Application Insights происходит на уровне экспортера.


14. Производительность и оптимизация

14.1. Пул объектов (ArrayPool, MemoryPool)

public static class BufferHelper
{
public static async Task<byte[]> ReadStreamToByteArrayAsync(Stream stream, CancellationToken ct = default)
{
const int DefaultBufferSize = 8192;
var buffer = ArrayPool<byte>.Shared.Rent(DefaultBufferSize);
try
{
using var ms = new MemoryStream();
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, DefaultBufferSize), ct)) > 0)
{
await ms.WriteAsync(buffer.AsMemory(0, bytesRead), ct);
}
return ms.ToArray();
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
}

15.2. Избегание аллокаций в hot path: Span, ref struct

public ref struct Utf8StringParser
{
private readonly ReadOnlySpan<byte> _data;

public Utf8StringParser(ReadOnlySpan<byte> data) => _data = data;

public bool TryParseInt32InRange(int min, int max, out int value)
{
value = 0;
foreach (var b in _data)
{
if (b is < (byte)'0' or > (byte)'9') return false;
var digit = b - (byte)'0';
if (value > (int.MaxValue - digit) / 10) return false;
value = value * 10 + digit;
}
return value >= min && value <= max;
}
}

✅ Применяется в парсерах, сетевых протоколах, обработке логов.

14.3. Параллельная агрегация без блокировок (ParallelEnumerable.Aggregate)

var total = orders.AsParallel()
.Aggregate(
seed: 0m,
func: (sum, order) => sum + order.Amount,
combine: (sum1, sum2) => sum1 + sum2,
resultSelector: sum => Math.Round(sum, 2));

🔹 Используйте AsParallel() только при большом объёме данных (>10k элементов) и CPU-bound операциях.


15. Работа с датами: календари, рабочие дни, праздники

15.1. Расчёт рабочих дней по ГОСТ Р 54710-2011 (с учётом переносов)

public class WorkingDayCalculator
{
private readonly HashSet<DateOnly> _holidays;
private readonly HashSet<DateOnly> _transferredWorkdays;

public WorkingDayCalculator(IEnumerable<DateOnly> holidays, IEnumerable<DateOnly> transferredWorkdays)
{
_holidays = holidays.ToHashSet();
_transferredWorkdays = transferredWorkdays.ToHashSet();
}

public bool IsWorkingDay(DateOnly date)
{
var dayOfWeek = (int)date.DayOfWeek; // 0 = воскресенье
var isWeekend = dayOfWeek == 0 || dayOfWeek == 6;

if (_transferredWorkdays.Contains(date)) return true;
if (_holidays.Contains(date)) return false;
return !isWeekend;
}

public DateOnly AddWorkingDays(DateOnly startDate, int workingDays)
{
var current = startDate;
var count = 0;

while (count < workingDays)
{
current = current.AddDays(1);
if (IsWorkingDay(current)) count++;
}
return current;
}
}

// Инициализация (пример для РФ 2025):
var holidays2025 = new[]
{
new DateOnly(2025, 1, 1), new DateOnly(2025, 1, 2), new DateOnly(2025, 1, 3),
new DateOnly(2025, 1, 4), new DateOnly(2025, 1, 5), new DateOnly(2025, 1, 6),
new DateOnly(2025, 1, 7), new DateOnly(2025, 1, 8), new DateOnly(2025, 2, 24),
new DateOnly(2025, 3, 8), new DateOnly(2025, 5, 1), new DateOnly(2025, 5, 9),
new DateOnly(2025, 6, 12), new DateOnly(2025, 11, 4)
};

var transferred = new[]
{
new DateOnly(2025, 1, 11), // сб → пн
new DateOnly(2025, 2, 22), // сб → пн
new DateOnly(2025, 3, 31), // пн → сб
// … актуальные переносы по Постановлению Правительства
};

var calc = new WorkingDayCalculator(holidays2025, transferred);
var deadline = calc.AddWorkingDays(DateOnly.FromDateTime(DateTime.Today), 10);

✅ Подходит для расчёта сроков исполнения поручений, договоров, регламентов (ГОСТ Р 6.30-2003 п. 4.2.5).

15.2. Форматирование даты по ГОСТ Р 6.30-2003: «01.01.2025 г.»

public static class GostDateFormatter
{
private static readonly string[] _monthsAccusative =
{ "января", "февраля", "марта", "апреля", "мая", "июня",
"июля", "августа", "сентября", "октября", "ноября", "декабря" };

// «01.01.2025 г.» — для реквизитов документов (подпись, дата составления)
public static string ToGostShort(DateTime date) => date.ToString("dd.MM.yyyy") + " г.";

// «1 января 2025 г.» — для текста (вводная часть приказа, протокола)
public static string ToGostLong(DateTime date)
{
var day = date.Day;
var month = _monthsAccusative[date.Month - 1];
var year = date.Year;
return $"{day} {month} {year} г.";
}
}

15.3. Валидация даты в формате ДД.ММ.ГГГГ (строгое соответствие ГОСТ)

public static bool TryParseGostDate(string? input, out DateTime result)
{
result = default;
if (string.IsNullOrWhiteSpace(input)) return false;

// Требуется ровно 10 символов: 01.01.2025
if (input.Length != 10 || input[2] != '.' || input[5] != '.') return false;

Span<char> span = stackalloc char[10];
input.AsSpan().CopyTo(span);
if (!char.IsDigit(span[0]) || !char.IsDigit(span[1]) ||
!char.IsDigit(span[3]) || !char.IsDigit(span[4]) ||
!char.IsDigit(span[6]) || !char.IsDigit(span[7]) ||
!char.IsDigit(span[8]) || !char.IsDigit(span[9]))
return false;

var day = int.Parse(span[0..2]);
var month = int.Parse(span[3..5]);
var year = int.Parse(span[6..10]);

return DateTime.TryParseExact($"{day:D2}.{month:D2}.{year:D4}", "dd.MM.yyyy",
CultureInfo.InvariantCulture, DateTimeStyles.None, out result);
}

16. Интеграции

16.1. SOAP-клиент без генерации прокси (ручной HttpContent)

Актуально при работе с устаревшими ГИС, где WSDL недоступен или некорректен.

public async Task<XmlElement> CallSoapServiceAsync(string endpoint, string action, string soapBody, CancellationToken ct = default)
{
const string soapEnvelope = @"
<soap:Envelope xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'
xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
xmlns:xsd='http://www.w3.org/2001/XMLSchema'>
<soap:Header/>
<soap:Body>{0}</soap:Body>
</soap:Envelope>";

var body = string.Format(soapEnvelope, soapBody);
var content = new StringContent(body, Encoding.UTF8, "text/xml");
content.Headers.Add("SOAPAction", $"\"{action}\"");

using var request = new HttpRequestMessage(HttpMethod.Post, endpoint) { Content = content };
using var response = await _httpClient.SendAsync(request, ct);
response.EnsureSuccessStatusCode();

var xml = await response.Content.ReadAsStringAsync(ct);
var doc = new XmlDocument();
doc.LoadXml(xml);

var bodyNode = doc.SelectSingleNode("//*[local-name()='Body']/*") ??
throw new InvalidOperationException("SOAP response body is empty.");
return (XmlElement)bodyNode;
}

⚠️ Используется при интеграции с ЕГАИС, ЕИС, СМЭВ 2.0, где WCF иногда несовместим.

16.2. Клиент REST с Refit + автоматической обработкой ошибок

public interface IExternalApi
{
[Get("/orders/{id}")]
Task<Order> GetOrderAsync(Guid id);
}

public class ResilientExternalApiClient : IExternalApi
{
private readonly IExternalApi _inner;
private readonly ILogger _logger;

public ResilientExternalApiClient(HttpClient httpClient, ILogger logger)
{
_logger = logger;
_inner = RestService.For<IExternalApi>(httpClient);
}

public async Task<Order> GetOrderAsync(Guid id)
{
try
{
return await _inner.GetOrderAsync(id);
}
catch (ApiException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
throw new OrderNotFoundException(id);
}
catch (ApiException ex)
{
_logger.LogWarning(ex, "API call failed for order {OrderId}", id);
throw new ExternalApiException("Order service unavailable.", ex);
}
}
}

// Регистрация:
services.AddHttpClient<IExternalApi, ResilientExternalApiClient>(client =>
{
client.BaseAddress = new Uri("https://api.example.com/v1/");
client.Timeout = TimeSpan.FromSeconds(30);
})
.AddPolicyHandler(Policy.RetryAsync<HttpResponseMessage>(3, (ex, _) =>
{
_logger.LogWarning(ex, "Retry attempt for external API");
}))
.AddPolicyHandler(Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(10)));

16.3. gRPC-сервер с метаданными аутентификации

public override async Task<GetUserResponse> GetUser(
GetUserRequest request,
ServerCallContext context)
{
// Извлечение токена из metadata
if (!context.RequestHeaders.TryGetValue("Authorization", out var authHeader) ||
!authHeader.Value.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
throw new RpcException(new Status(StatusCode.Unauthenticated, "Missing or invalid token"));

var token = authHeader.Value["Bearer ".Length..];
var principal = await _jwtValidator.ValidateAsync(token);
if (principal == null)
throw new RpcException(new Status(StatusCode.Unauthenticated, "Invalid token"));

// Проброс claims в context
context.User.AddIdentity(new ClaimsIdentity(principal.Claims));

var user = await _userService.GetByIdAsync(request.Id);
return new GetUserResponse { Id = user.Id.ToString(), Name = user.Name };
}

✅ Соответствует требованиям ФСТЭК: аутентификация на уровне транспорта + токен.


17. Локализация и интернационализация (I18n)

17.1. Мультиязычные ресурсы с fallback на ru-RU (ГОСТ 34.201-89)

public class LocalizedStringProvider
{
private readonly IStringLocalizer _localizer;

public LocalizedStringProvider(IStringLocalizer localizer)
=> _localizer = localizer;

public string this[string name, params object[] args]
=> _localizer.GetString(name, args);
}

// В конфигурации:
services.AddLocalization(options => options.ResourcesPath = "Resources");
app.UseRequestLocalization(new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture("ru-RU"),
SupportedCultures = new[] { new CultureInfo("ru-RU"), new CultureInfo("en-US"), new CultureInfo("tt-RU") },
RequestCultureProviders = new[]
{
new HeaderRequestCultureProvider { HeaderName = "Accept-Language" },
new QueryStringRequestCultureProvider()
}
});

// Ресурс: Resources/DocumentResource.ru-RU.resx
// — Name: "OrderNumber", Value: "Номер заказа"
// — Name: "SignAndSeal", Value: "Подпись и печать"

17.2. Форматирование денежных сумм по ГОСТ Р 6.30-2003: 1 000 000 (один миллион) рублей 00 коп.

public static string FormatAmountGost(decimal amount)
{
var rub = Math.Floor(amount);
var kop = (int)Math.Round((amount - rub) * 100);

var rubText = RublesToText((long)rub);
return $"{rub:N0} ({rubText}) рублей {kop:00} коп.";
}

private static string RublesToText(long n)
{
// Упрощённая реализация; для продакшена — использовать библиотеку Humanizer или кастомный engine
var units = new[] { "", "один", "два", "три", "четыре", "пять", "шесть", "семь", "восемь", "девять" };
var teens = new[] { "десять", "одиннадцать", "двенадцать", "тринадцать", "четырнадцать", "пятнадцать", "шестнадцать", "семнадцать", "восемнадцать", "девятнадцать" };
var tens = new[] { "", "", "двадцать", "тридцать", "сорок", "пятьдесят", "шестьдесят", "семьдесят", "восемьдесят", "девяносто" };
var hundreds = new[] { "", "сто", "двести", "триста", "четыреста", "пятьсот", "шестьсот", "семьсот", "восемьсот", "девятьсот" };

if (n == 0) return "ноль";
if (n < 0) return "минус " + RublesToText(-n);

var parts = new List<string>();
if (n >= 100) { parts.Add(hundreds[n / 100]); n %= 100; }
if (n >= 20) { parts.Add(tens[n / 10]); n %= 10; }
else if (n >= 10) return teens[n - 10];
if (n > 0) parts.Add(units[n]);

return string.Join(" ", parts);
}

✅ Обязательно для платёжных поручений, актов, смет (п. 4.2.7 ГОСТ Р 6.30).


18. Кастомные атрибуты и source generators

18.1. Атрибут для валидации по регулярному выражению (compile-time)

[AttributeUsage(AttributeTargets.Property)]
public sealed class RegexPatternAttribute : Attribute
{
public string Pattern { get; }
public bool IsCaseSensitive { get; }

public RegexPatternAttribute(string pattern, bool isCaseSensitive = false)
{
Pattern = pattern;
IsCaseSensitive = isCaseSensitive;
}
}

// Пример использования:
public partial record User
{
[RegexPattern(@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")]
public string Email { get; init; } = null!;
}

18.2. Source Generator: генерация Validate() метода

[Generator]
public class ValidationGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var provider = context.SyntaxProvider
.ForAttributeWithMetadataName(
"RegexPatternAttribute",
predicate: static (node, _) => node is PropertyDeclarationSyntax,
transform: static (ctx, _) => (Property: (PropertyDeclarationSyntax)ctx.TargetNode,
Attribute: (AttributeData)ctx.Attributes[0]));

context.RegisterSourceOutput(provider, (spc, source) =>
{
var prop = source.Property;
var attr = source.Attribute;
var pattern = attr.ConstructorArguments[0].Value?.ToString() ?? "";
var className = prop.Ancestors().OfType<TypeDeclarationSyntax>().First().Identifier.Text;

var sourceText = $@"
public partial class {className}
{{
public bool Validate{prop.Identifier}(out string? error)
{{
error = null;
var value = this.{prop.Identifier};
if (string.IsNullOrEmpty(value)) return true;
if (!System.Text.RegularExpressions.Regex.IsMatch(value, ""{pattern}""))
{{
error = ""Значение '{value}' не соответствует шаблону: {pattern}"";
return false;
}}
return true;
}}
}}";
spc.AddSource($"{className}.{prop.Identifier}.Validate.g.cs", SourceText.From(sourceText, Encoding.UTF8));
});
}
}

✅ Повышает производительность: валидация — без рефлексии, compile-time безопасность.


19. Паттерны проектирования без сторонних библиотек

19.1. Pipeline (обработка документа по этапам)

public interface IPipelineStep<TContext>
{
Task ExecuteAsync(TContext context, CancellationToken ct);
}

public class DocumentProcessingPipeline
{
private readonly IEnumerable<IPipelineStep<DocumentContext>> _steps;

public DocumentProcessingPipeline(IEnumerable<IPipelineStep<DocumentContext>> steps)
=> _steps = steps;

public async Task ExecuteAsync(DocumentContext context, CancellationToken ct = default)
{
foreach (var step in _steps)
{
ct.ThrowIfCancellationRequested();
await step.ExecuteAsync(context, ct);
if (context.IsCancelled) break;
}
}
}

// Пример шага:
public class ValidateRecipientStep : IPipelineStep<DocumentContext>
{
public Task ExecuteAsync(DocumentContext context, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(context.RecipientInn))
context.AddError("ИНН получателя не указан.");
return Task.CompletedTask;
}
}

19.2. CQRS Lite: разделение команд и запросов без MediatR

// Запрос
public record GetOrderQuery(Guid Id) : IRequest<OrderDto>;
public class GetOrderQueryHandler : IRequestHandler<GetOrderQuery, OrderDto>
{
public Task<OrderDto> Handle(GetOrderQuery request, CancellationToken ct)
=> _db.Orders.Where(o => o.Id == request.Id).ProjectTo<OrderDto>(_mapper.ConfigurationProvider).FirstAsync(ct);
}

// Команда
public record ApproveOrderCommand(Guid Id) : IRequest<Unit>;
public class ApproveOrderCommandHandler : IRequestHandler<ApproveOrderCommand, Unit>
{
public async Task<Unit> Handle(ApproveOrderCommand request, CancellationToken ct)
{
var order = await _db.Orders.FindAsync(request.Id, ct);
order.Approve();
await _db.SaveChangesAsync(ct);
_mediator.Publish(new OrderApprovedEvent(request.Id));
return Unit.Value;
}
}

// Диспетчер (без DI-сканирования сборок — явная регистрация)
services.AddSingleton<IRequestHandler<GetOrderQuery, OrderDto>, GetOrderQueryHandler>();
services.AddSingleton<IRequestHandler<ApproveOrderCommand, Unit>, ApproveOrderCommandHandler>();

public static class RequestDispatcher
{
public static async Task<TResponse> Send<TRequest, TResponse>(
this IServiceProvider sp, TRequest request, CancellationToken ct = default)
where TRequest : IRequest<TResponse>
{
var handlerType = typeof(IRequestHandler<,>).MakeGenericType(typeof(TRequest), typeof(TResponse));
var handler = sp.GetRequiredService(handlerType);
var method = handlerType.GetMethod("Handle")!;
return (TResponse)await (Task<TResponse>)method.Invoke(handler, new object[] { request, ct })!;
}
}

✅ Минимум зависимостей, полный контроль, легко тестируется.