.NET Разработчик
6.6K subscribers
446 photos
4 videos
14 files
2.15K links
Дневник сертифицированного .NET разработчика. Заметки, советы, новости из мира .NET и C#.

Для связи: @SBenzenko

Поддержать канал:
- https://boosty.to/netdeveloperdiary
- https://patreon.com/user?u=52551826
- https://pay.cloudtips.ru/p/70df3b3b
Download Telegram
День 2477. #ЗаметкиНаПолях
Тестирование с Использованием FakeLogger
Вместо того, чтобы заниматься сложными настройками моков или проверять вызовы вручную, FakeLogger собирает сообщения журнала, области действия и структурированные данные в памяти, делая утверждения простыми и выразительными. Сегодня рассмотрим, как тестировать области логирования с помощью FakeLogger, зачем нужны области и как использовать их в коде.

Зачем использовать области действия?
Область логирования в .NET позволяет прикреплять контекстную информацию ко всем записям журнала в блоке кода. Представьте себе облегчённый набор пар «ключ-значение», который дополняет сообщения журнала, не заставляя вас предоставлять одни и те же поля в каждую запись журнала.

Например, вы можете добавить идентификатор запроса, идентификатор корреляции или идентификатор сущности в область действия, тем самым предоставляя в каждую запись журнала в этом блоке одинаковые контекстные метаданные. Это значительно упрощает фильтрацию и анализ журналов в распределённых системах или при отладке конкретных запросов.

Использование FakeLogger с областями действия
Начнём с простого метода, использующего область действия журналирования:
public partial class 
DemoService(ILogger<DemoService> lgr)
{
public void LogMe(Guid id)
{
using var _ =
lgr.BeginScope(
new Dictionary<string, object> { { "id", id } });

lgr.LogInformation("Пишем в лог.");
}
}

Здесь вызов BeginScope создаёт контекст логирования с единственным ключом (id). При вызове lgr.LogInformation область действия автоматически включает эти контекстные данные. Но как это проверить в тесте?

FakeLogger отслеживает каждую запись журнала, включая шаблоны сообщений, уровни ведения журнала, исключения и области действия. Проверим, правильно ли DemoService пишет в лог, включая id:
public class DemoServiceTests
{
[Fact]
public void ScopeUsage()
{
var fakeLogger = new FakeLogger<DemoService>();
var sut = new DemoService(fakeLogger);
var id = Guid.NewGuid();

sut.LogMe(id);

fakeLogger
.LatestRecord
.Message
.Should()
.Be("Пишем в лог.");

var scope = fakeLogger
.LatestRecord
.Scopes[0] as Dictionary<string, object>;
scope.Should().NotBeNull();
scope.Keys.Should().Contain("id");
scope.Values.Should().Contain(id);
}
}

В этом тесте мы:
- Создаём экземпляр тестируемой системы (SUT), используя FakeLogger<DemoService>.
- Вызываем метод LogMe(), генерирующий одну запись журнала.
- Проверяем содержимое сообщения и проверяем, записана ли коллекция из области действия.

Каждая область действия регистрируется как объект, и в данном случае это Dictionary<string, object>, содержащий id. Этот подход понятен, выразителен и поддерживается фреймворком, устраняя необходимость в пользовательских оболочках для логирования или ненадёжных настройках верификации моков.

Недостаток в том, что вам, вероятно, потребуется знать, как использовалась область действия в коде, чтобы тесты проверяли то, что вам нужно, однако в данном контексте это приемлемо.

Источник: https://adamstorr.co.uk/blog/testing-with-fakelogger-scopes/
👍7
День 2478. #ЗаметкиНаПолях
Ограничение Доступа к Методу Действия в
ASP.NET Core MVC. Начало
Рассмотрим несколько вариантов ограничения доступа к определённому методу действия (или ко всем методам) в контроллере ASP.NET Core MVC.

Мы всегда должны добавлять поддержку аутентификации (кто пользователь) и авторизации (что он может делать) в конвейер ASP.NET Core. Авторизация требует аутентификации, но аутентификация может существовать и сама по себе в определённых схемах аутентификации:
builder.Services.AddAuthentication()
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme);
builder.Services.AddAuthorization();


Использование фильтров
Фильтры ASP.NET Core хорошо известны и популярны, и, пожалуй, это самый простой способ ограничить доступ к конечной точке. Фильтры можно применять:
- через атрибут, к методу действия или контроллеру,
- глобально для всех контроллеров и действий.

Пользовательские фильтры
IAuthorizationFilter (IAsyncAuthorizationFilter) — интерфейс, определяющий правила авторизации для данной конечной точки. Его можно реализовать с помощью атрибута, который затем применить к методу действия, классу контроллера или глобально ко всем запросам:
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public class DayOfWeekFilterAttribute(
params DayOfWeek[] daysOfWeek)
: Attribute, IAsyncAuthorizationFilter
{
public Task OnAuthorizationAsync(
AuthorizationFilterContext context)
{
if (!(daysOfWeek ?? [])
.Contains(DateTime.Today.DayOfWeek))
context.Result = new ForbidResult();

return Task.CompletedTask;
}
}


Использование:
[DaysOfWeek(DayOfWeek.Saturday, DayOfWeek.Sunday)]
public IActionResult Index() { … }


Возврат результата осуществляется через свойство Result объекта AuthorizationFilterContext:
- ForbidResult: HTTP 403 Forbidden;
- RedirectResult, RedirectToActionResult, RedirectToPageResult, RedirectToRouteResult, LocalRedirectResult: различные виды перенаправлений (HTTP 3xx);
- EmptyResult: ничего с кодом 200 OK;
- StatusCodeResult: пользовательский код статуса.

Продолжение следует…

Источник:
https://developmentwithadot.blogspot.com/2025/10/restricting-access-to-action-method-in.html
👍15
День 2479. #ЗаметкиНаПолях
Ограничение Доступа к Методу Действия в
ASP.NET Core MVC. Продолжение
Начало

Атрибут Authorize
Существует также встроенный атрибут [Authorize], который может быть применён к классам контроллеров или методам действий, но не реализует ни один из этих интерфейсов. Он допускает несколько ограничений:
- Политика: ограничение по именованной политике, которая должна быть определена.
- Роли: одна или несколько ролей, разделённых запятыми. Текущий пользователь должен иметь одну из указанных ролей. Роли берутся из утверждений запроса.

Возможно наличие нескольких атрибутов [Authorize]:
- если в одном [Authorize] несколько ролей, пользователю необходимо иметь одну из них;
- если в нескольких [Authorize] заданы роли, пользователь должен иметь их все.
То же относится и к политикам.

Атрибут [AllowAnonymous], если присутствует, обходит любой атрибут [Authorize].

[Authorize(Roles = "Admin")] 
//все методы действия контроллера требуют роли "Admin"
public class AdminController
: Controller
{
public IActionResult Index()
{ … } //требует роли "Admin"

[AllowAnonymous]
public IActionResult SignOut()
{ … } //может быть вызван кем угодно
}


Если мы хотим использовать именованные политики, мы должны определить их:
builder.Services.AddAuthorizationBuilder()
.AddPolicy("AdminPolicy", p =>
{
p.RequireRole("Admin");
p.RequireAuthenticatedUser();
});


Теперь можно использовать политику "AdminPolicy":
[Authorize(Policy = "AdminPolicy")]
public IActionResult Restricted()
{ … } //требует удовлетворять политике "AdminPolicy"


Роли и политики
Это разные способы управления доступом. Роли напрямую сопоставляются с требованиями аутентификации или группами пользователей, в зависимости от используемого типа аутентификации. Политики же позволяют настраивать требования, которые могут включать роли, но не ограничиваются этим. В определении политики мы можем требовать:
- аутентификации: RequireAuthenticatedUser();
- утверждений (claim): RequireClaim();
- одной из ролей: RequireRole();
- определённого имени: RequireUserName();
- определённого условия: RequireAssertion().
А также:
- Объединять несколько политик: Combine();
- Добавлять требования: AddRequirements().

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

Также возможно иметь более структурированный и переиспользуемый контроль доступа, об этом далее.

Окончание следует…

Источник:
https://developmentwithadot.blogspot.com/2025/10/restricting-access-to-action-method-in.html
👍5
День 2480. #ЗаметкиНаПолях
Ограничение Доступа к Методу Действия в
ASP.NET Core MVC. Окончание
Начало
Продолжение

Использование обработчиков авторизации
Обработчик авторизации — это реализация IAuthorizationHandler, в виде абстрактного класса AuthorizationHandler<TRequirement>, который принимает требование в качестве параметра. Внутри его метода HandleRequirementAsync мы можем реализовать любую логику, передавая требование в качестве параметра:
public record DayOfWeekRequirement(DayOfWeek Day)
: IAuthorizationRequirement
{
}

Маркерный интерфейс IAuthorizationRequirement не определяет методов. Мы можем добавить свойства в нашу реализацию требования, если хотим передавать данные обработчику.

Подключаем требование к обработчику:
public class DayOfWeekAuthHandler 
: AuthorizationHandler<DayOfWeekRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext ctx,
DayOfWeekRequirement req)
{
if (DateTime.Today.DayOfWeek == req.Day)
ctx.Succeed(req);
else
ctx.Fail(new AuthorizationFailureReason(this, "Неправильный день недели"));

return Task.CompletedTask;
}
}


Простой пример выше, принимает в качестве параметра только день недели, но вариантов ограничений бесконечное множество:
- по IP,
- по наличию/отсутствию cookie,
- по заголовкам запроса…

Внутри HandleRequirementAsync запрос либо завершается неудачей (Fail), либо успехом (Succeed). Если зарегистрировано много обработчиков, все они должны успешно завершиться для получения разрешения на доступ к конечной точке. Для всех обработчиков вызывается один и тот же экземпляр параметра AuthorizationHandlerContext, и оттуда можно проверить:
- текущего аутентифицированного пользователя (User),
- контекст (Resource),
- текущий статус запроса (HasFailed, HasSucceeded),
- обработанные требования (Requirements)
- ожидающие обработки требования (PendingRequirements),
- причины отказа (FailureReasons).

Обработчики необходимо зарегистрировать в DI. ASP.NET Core находит соответствующий обработчик, проверяя переданное требование:
builder.Services
.AddSingleton<IAuthorizationHandler, DayOfWeekAuthHandler>();


Мы должны добавить требования в именованную политику:
builder.Services.AddAuthorizationBuilder()
.AddPolicy("DayOfWeekPolicy", p =>
{
p.Requirements.Add(new DayOfWeekRequirement(DayOfWeek.Saturday));
p.Requirements.Add(new DayOfWeekRequirement(DayOfWeek.Sunday));
});


Теперь мы можем добавить атрибут [Authorize], который ссылается на новую политику:
[Authorize(Policy = "DayOfWeekPolicy")]
public IActionResult Index() { … }


Сравнение альтернатив
С [Authorize] мы можем использовать только политики или роли. Определение политики обеспечивает гораздо большую гибкость, поскольку мы можем указать именно то, что нужно, и это можно изменить в любое время. Тем не менее, обработчики авторизации — более отказоустойчивое и универсальное решение, позволяющее инкапсулировать несколько условий с параметрами, а также использовать DI.

Источник: https://developmentwithadot.blogspot.com/2025/10/restricting-access-to-action-method-in.html
👍10
День 2481. #ВопросыНаСобеседовании
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.

8. Language Integrated Query (LINQ)
«Расскажите, что такое LINQ и как его можно использовать в приложениях .NET? Приведите примеры различных типов поставщиков LINQ и объясните сценарий, в котором использование LINQ повышает читаемость и эффективность кода».

Хороший ответ
«LINQ, или Language Integrated Query, — это набор технологий, основанных на интеграции возможностей запросов непосредственно в язык C#. LINQ представляет шаблоны для извлечения и обновления данных из различных источников с использованием общего синтаксиса. Он абстрагирует базовый источник данных, позволяя разработчикам использовать один и тот же подход к запросам к БД, XML-документам и коллекциям в памяти.

В .NET существует несколько поставщиков LINQ, каждый из которых предназначен для доступа к различным типам данных:
- LINQ to Objects — используется для запросов к коллекциям в памяти, таким как списки или массивы, реализующие интерфейс IEnumerable.
- LINQ to SQL — предназначен для запросов к базам данных SQL непосредственно из C#.
- LINQ to XML (ранее известный как XLINQ) — предоставляет простые в использовании возможности для запросов и работы с XML-документами.
- LINQ to Entities — часть Entity Framework Core, которая позволяет выполнять запросы к реляционным БД и облачным хранилищам данных через ORM EF Core.

Пример использования LINQ для повышения читабельности кода и его эффективность можно оценить в задачах преобразования данных. Например, представим сценарий, в котором нужно отфильтровать и упорядочить список сотрудников по их зарплате. Без LINQ пришлось бы написать несколько строк императивного кода, включая циклы и условные операторы. С LINQ это можно сделать краткого и ясно в одну строчку:
```csharp
var highSalaryEmployees = employees
.Where(e => e.Salary > 150000)
.OrderBy(e => e.Salary);
```
Это не только сокращает объём кода, но и улучшает его читаемость, чётко выражая его назначение.»


Часто встречающийся неправильный ответ
«LINQ — это способ написания SQL-запросов на C#. Он используется, когда нужно выполнять запросы к базам данных, и работает практически так же, как SQL.»

Этот ответ демонстрирует ограниченное понимание LINQ и упускает из виду его более широкие области применения и преимущества:

- Непонимание области применения LINQ
LINQ предоставляет унифицированную модель для запросов к данным в памяти (например, массивам или спискам), XML и даже к наборам данных из API или других внешних сервисов данных, выходящую далеко за рамки простого взаимодействия с БД.

- Упрощение возможностей
Сравнение с SQL упускает из виду интегрированную природу LINQ в C# и его способность беспрепятственно работать с объектно-ориентированным программированием, предлагая такие функции, как отложенное выполнение и строгая типизация, которые выходят за рамки традиционных возможностей SQL.

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

Источник: https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md
👍15👎2
День 2482. #SystemDesign101
Современные Сетевые Сервисы
Каждый раз, когда вы открываете браузер, отправляете электронное письмо или подключаетесь к VPN, эти сетевые сервисы незаметно обеспечивают вашу работу по сети.

DNS: Преобразует доменные имена в IP-адреса, чтобы пользователи могли заходить на веб-сайты без необходимости запоминать цифры.

DHCP: Автоматически назначает IP-адреса и сетевые настройки устройствам, подключающимся к сети.

NTP: Синхронизирует часы между системами, обеспечивая единообразие журналов и аутентификации.

SSH: Обеспечивает безопасный удалённый вход в систему и зашифрованную передачу файлов через порт 22.

RDP: Обеспечивает удалённый доступ к рабочему столу систем Windows через порт 3389.

Почта (SMTP-отправка): Обеспечивает безопасную отправку электронных писем от клиентов на почтовые серверы.

HTTPS/HTTP3 (QUIC): Обеспечивает безопасность веб-сайтов, API и обмена данными между приложениями по зашифрованным каналам.

LDAP (по TLS): Выступает в качестве центрального каталога для корпоративных учётных записей и контроля доступа.

OAuth2.0/OpenID Connect: Обеспечивает современные процессы аутентификации, такие как «Войти с помощью Google».

MySQL/PostgreSQL/Oracle: Управляет хранением и извлечением данных на бэкенде для веб- и мобильных приложений.

WireGuard/IPsec: Создаёт зашифрованные туннели для приватного и удалённого доступа к сети.

Источник: https://blog.bytebytego.com
👍6
День 2483. #ЗаметкиНаПолях
Сжатие Запросов HttpClient с Помощью GZIP
Если вам нужно отправлять большие файлы во внешний API, вы можете решить, что необходимо сжимать их перед отправкой. Однако дело в том, что очевидное решение может привести к проблемам с памятью…

Рассмотрим такой код:
internal async ValueTask<HttpRequestMessage>
CreateRequest(Uri uri, Stream source)
{
var outStream = new MemoryStream();
await using (var zip =
new GZipStream(outStream,
CompressionMode.Compress,
leaveOpen: true))
{
await source.CopyToAsync(zip);
zip.Close();
}
outStream.Position = 0;

var request = new HttpRequestMessage(
HttpMethod.Post, uri)
{
Content = new StreamContent(outStream)
};
request.Content.Headers.ContentEncoding.Add("gzip");
return request;
}


Использовать его можно примерно так:
var uri = new Uri("https://my.api/endpoint");
var file = File.OpenRead("largefile.txt");
var request = await CreateRequest(uri, file);
await httpClient.SendAsync(request);

Обычно этот код просто работает. Однако каждый раз при отправке запроса сжатый gzip-контент создаётся в памяти (благодаря MemoryStream) перед отправкой. И если требуется отправлять много таких запросов, приложение начнёт испытывать нехватку памяти.

Решение в gzip-сжатии «на лету»:
public class GzipContent : HttpContent
{
private readonly Stream _source;

public GzipContent(Stream source)
{
_source = source;
Headers.ContentEncoding.Add("gzip");
}

protected override async Task
SerializeToStreamAsync(
Stream stream,
TransportContext? context)
{
await using var zip =
new GZipStream(stream,
CompressionMode.Compress,
leaveOpen: true);
await _source.CopyToAsync(zip);
}

protected override bool
TryComputeLength(out long length)
{
length = -1;
return false;
}
}

Также переделаем CreateRequest:
internal ValueTask<HttpRequestMessage>
CreateRequest(Uri uri, Stream source)
{
return ValueTask.FromResult(
new HttpRequestMessage(HttpMethod.Post, uri)
{
Content = new GzipContent(source)
});
}

Использование будет точно таким же.

Производительность
Вот мои тесты в .NET10. По времени особой разницы нет, зато версия GzipContent выделяет гораздо меньше памяти. Чем больше файл, тем заметнее разница.
| File | Method| Mean   | Ratio| Allocated| Alc Rat |
|-----:|-------|-------:|-----:|---------:|--------:|
| 7KB| Memory| 633.3us| 1.00| 6.35KB| 1.00|
| 7KB| Gzip | 603.5us| 0.96| 4.77KB| 0.75|
| 200KB| Memory| 8.025ms| 1.00| 508.52KB| 1.000|
| 200KB| Gzip | 7.260ms| 0.90| 4.74KB| 0.009|
| 3MB| Memory| 94.97ms| 1.00| 8189.99KB| 1.000|
| 3MB| Gzip | 86.02ms| 0.91| 4.82KB| 0.001|


PS: .NET API может автоматически распаковывать запросы. Достаточно добавить соответствующее промежуточное ПО:
builder.Services.AddRequestDecompression();

// …

app.UseRequestDecompression();


Источник: https://josef.codes/compress-httpclient-requests-with-gzip-dotnet-core/
👍14
День 2484. #ЗаметкиНаПолях
Разбираем Однофайловые Приложения в .NET 10. Начало
C# всегда был немного перегружен церемониями. Даже простейший «Hello World» традиционно требовал файла решения, файла проекта и достаточного количества шаблонного кода, чтобы заставить вас задуматься, а не лучше ли использовать скриптовый язык. В .NET 10 представлены однофайловые приложения.

Что это?
Идея проста: пишете код C# в одном CS-файле и запускаете его напрямую. Не нужно настраивать структуру целого проекта для быстрого служебного скрипта.

Все прелести C# сохраняются: типобезопасность, производительность, богатая стандартная библиотека. Но теперь вы можете использовать его для тех одноразовых скриптов, для которых создание полноценного проекта было бы излишним. По-прежнему можно ссылаться на NuGet-пакеты, на другие проекты C#, использовать конкретные SDK и настраивать свойства проекта — всё это из одного файла с помощью специальных директив.

Функционал основан на операторах верхнего уровня и доводит их до логического завершения. Если уж мы позволяем пользователям пропускать церемонию создания класса и метода Main, почему бы не позволить им также пропускать церемонию создания проекта?

Начало работы
Предположим, нужно быстро проверить, на какой день недели приходится определённая дата. Создадим файл с date-checker.cs:
var targetDate = new DateTime(2025, 12, 31);
Console.WriteLine(
$"Новый год в 2025 - это {targetDate.DayOfWeek}");
Console.WriteLine(
$"Через {(targetDate - DateTime.Today).Days} дней");

Выполним это с помощью:
dotnet run date-checker.cs

При первом запуске CLI творит чудеса: создаёт виртуальный проект, компилирует код и кэширует всё. Последующие запуски выполняются практически мгновенно, поскольку CLI достаточно умён, чтобы определять, когда ничего не изменилось.

Пример из реальной жизни
Допустим, нужно быстро обработать JSON и сформировать отчёт. Используем System.Text.Json и CsvHelper:
#:package [email protected]
using System.Text.Json;
using CsvHelper;
using System.Globalization;
using System.Text.Json.Serialization.Metadata;

var json = await
File.ReadAllTextAsync("sales_data.json");
var sales =
JsonSerializer.Deserialize<List<SaleRecord>>(
json,
new JsonSerializerOptions() {
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
});

var topProducts = sales
.GroupBy(s => s.Product)
.Select(g => new {
Product = g.Key,
TotalRevenue = g.Sum(s => s.Amount),
UnitsSold = g.Count()
})
.OrderByDescending(p => p.TotalRevenue)
.Take(10);

using var writer =
new StreamWriter("top_products.csv");
using var csv =
new CsvWriter(writer, CultureInfo.InvariantCulture);
csv.WriteRecords(topProducts);

Console.WriteLine(
"Отчёт сохранён в top_products.csv");

record SaleRecord(string Product, decimal Amount, DateTime Date);

Заметьте, как мы используем и ссылки на NuGet-пакеты, и асинхронные операции, и LINQ, и записи — всё в одном файле, который читается как связный скрипт. Обычно для таких задач используется Python, но теперь можно остаться в C#.

Окончание следует…

Источник:
https://www.milanjovanovic.tech/blog/exploring-csharp-file-based-apps-in-dotnet-10
👍32
День 2485. #ЗаметкиНаПолях
Разбираем Однофайловые Приложения в .NET 10. Окончание

Начало

Создаём что-то более амбициозное с Aspire
Даже AppHost для Aspire можно создать в одном файле:
#:sdk [email protected]
#:package [email protected]

var builder =
DistributedApplication.CreateBuilder(args);

var cache = builder.AddRedis("cache")
.WithDataVolume();

var postgres = builder.AddPostgres("postgres")
.WithDataVolume()
.AddDatabase("tododb");

var todoApi =
builder.AddProject<Projects.TodoApi>("api")
.WithReference(cache)
.WithReference(postgres);

builder.AddNpmApp("frontend", "../TodoApp")
.WithReference(todoApi)
.WithReference("api")
.WithHttpEndpoint(env: "PORT")
.WithExternalHttpEndpoints();

builder.Build().Run();


С помощью директивы #:sdk [email protected] ваш отдельный файл становится полноценным оркестратором для распределённого приложения. Вы определяете инфраструктуру, подключаете зависимости и настраиваете полноценную среду разработки — и всё это без создания файла проекта. Это особенно полезно при создании прототипов архитектур или для быстрого развёртывания тестовой среды.

Переход на полноценный проект
Со временем некоторые скрипты перерастают рамки одного файла. Возможно, требуется разделить код на несколько файлов или, может быть, нужна полноценная поддержка IDE для отладки. Переход проходит без проблем:
dotnet project convert MyUtility.cs

Это создаст правильную структуру проекта, сохраняя все ссылки на пакеты и выбранные SDK. Ваш код переместится в Program.cs, и вы получите файл .csproj, отражающий все использованные вами директивы #:.

Текущие ограничения
В настоящее время проект работает строго в одном файле. Если вам нужно несколько файлов, вам придётся дождаться .NET 11 или преобразовать его в полноценный проект. Конечно, вы по-прежнему можете ссылаться на другие проекты или пакеты, но основной скрипт должен находиться в одном файле.

Механизм кэширования может иногда давать сбои при быстром переборе версий пакетов. И хотя поддержка в IDE улучшается, она пока не достигает уровня полноценных проектов, особенно в случае IntelliSense с динамическими ссылками на пакеты.

Но с той задачей, для которой он предназначен (обеспечение доступности C# для сценариев написания скриптов), он работает на удивление хорошо. Его можно использовать для скриптов сборки, одноразовой миграции данных, быстрых тестов API или даже для обучения C#, не перегружая новичков структурой проекта.

Итого
Однофайловые приложения ощущаются как C#, наконец-то признавший, что не каждый фрагмент кода должен быть корпоративным приложением. Иногда просто нужно проанализировать файл журнала, иногда нужно быстро протестировать алгоритм, а иногда вы обучаете кого-то программированию и не хотите с первого дня объяснять, что такое файл решения.

Эта функция не производит революцию в разработке на C#, но заполняет пробел, который годами раздражал разработчиков. Если вы хранили папку со скриптами для быстрых задач, потому что C# казался слишком тяжёлым, возможно, пришло время дать любимому языку ещё один шанс. В конце концов, лучший язык для быстрого скрипта — это тот, который вы уже знаете, и теперь C# делает этот аргумент гораздо более убедительным.

Источник: https://www.milanjovanovic.tech/blog/exploring-csharp-file-based-apps-in-dotnet-10
👍9
День 2486. #ЗаметкиНаПолях
Удаляем Старые Версии .NET
Со временем на вашем компьютере и серверах, где вы работаете, может накопиться несколько версий среды выполнения и SDK .NET. Хотя наличие нескольких версий часто необходимо для обеспечения совместимости, старые версии, которые больше не нужны, могут занимать ценное дисковое пространство и загромождать систему. Рассмотрим процесс безопасного определения и удаления старых версий .NET.

Зачем?
- Место на диске: каждая версия SDK занимает несколько сотен мегабайт.
- Прозрачность: меньшее количество версий упрощает управление средой разработки.
- Безопасность: старые версии могут содержать известные уязвимости.
- Обслуживание: сохранение только необходимых версий упрощает обновления и устранение неполадок.

Проверка установленных версий
Откройте терминал или командную строку и выполните:
dotnet --list-sdks

Это выдаст все установленные версии SDK. Что-то вроде:
5.0.101 [C:\Program Files\dotnet\sdk]
8.0.100-preview.5.23303.2 [C:\Program Files\dotnet\sdk]
9.0.100-preview.6.24328.19 [C:\Program Files\dotnet\sdk]
10.0.100 [C:\Program Files\dotnet\sdk]

Чтобы проверить версии среды выполнения:
dotnet --list-runtimes


Важно перед удалением
Не удаляйте всё! Перед удалением версий учтите:
- Активные проекты: проверьте, на какие версии SDK ориентированы текущие проекты.
- Требования команды: убедитесь, что не удаляете версии, необходимые для командных проектов.
- Конвейеры CI/CD: убедитесь, что конвейеры сборки не зависят от определённых версий.
- Статус поддержки: сохраните хотя бы одну версию, которая в настоящее время поддерживается.
Вы можете проверить жизненный цикл поддержки .NET на официальной странице Microsoft.

Использование инструмента удаления .NET
Microsoft предоставляет официальный инструмент удаления, который делает процесс безопасным и простым. Скачайте msi-файл с официальной страницы релизов GitHub и запустите установку. После завершения установки инструмент dotnet-core-uninstall станет доступен и в командной строке.

Сначала попробуем команду list, чтобы увидеть, какие версии она может найти и удалить:
dotnet-core-uninstall list

Можно использовать команду пробного запуска с фильтром, чтобы увидеть, на что повлияет выполнение удаления:
dotnet-core-uninstall dry-run --all-below 8.0.0 --hosting-bundle

Если вы готовы, удалите версии:
dotnet-core-uninstall remove --all-below 8.0.0 --hosting-bundle

То же можно сделать для сред выполнения и SDK:
dotnet-core-uninstall remove --all-below 8.0.0 --aspnet-runtime
dotnet-core-uninstall remove --all-below 8.0.0 --runtime
dotnet-core-uninstall remove --all-below 8.0.0 --sdk


Полезные опции
--all-below <version>: Удалить все версии ниже указанной;
--all-but-latest: Оставить только последнюю версию;
--all-previews: Удалить все превью-версии;
--sdk: Точная версия SDK;
--runtime: Точная версия среды выполнения;
--force: Пропустить проверку зависимостей (используйте осторожно!).

Рекомендации
Дополнительные советы по управлению версиями .NET Core:
- Использование global.json: прикрепите определённые проекты к определённым версиям SDK.
- Регулярная очистка: запланируйте ежеквартальный просмотр установленных версий.
- Сохраните одну версию LTS: всегда поддерживайте как минимум одну версию с долгосрочной поддержкой.
- Документируйте зависимости: отслеживайте, какие версии нужны проектам.
- Использование Docker: для старых проектов рассмотрите возможность использования контейнеров Docker вместо локальной установки старых SDK.

Итого
Очистка старых версий .NET — это простой процесс, который может освободить значительное количество места на диске и упростить разработку. Официальная утилита .NET Uninstall Tool предоставляет самый безопасный метод с функциями предварительного просмотра, которые позволяют увидеть, какие именно версии будут удалены, прежде чем вносить изменения.

Источник: https://bartwullems.blogspot.com/2025/11/how-to-uninstall-older-net-core-versions.html
1👍24
День 2487. #ВопросыНаСобеседовании #Архитектура
Оптимизируем Генерацию Отчёта

«Пользователь нажимает кнопку в интерфейсе, чтобы создать отчёт в Excel или PDF. Создание отчёта занимает около пяти минут (время может быть произвольным). Пользователю приходится ждать завершения. Как бы вы оптимизировали этот процесс?»

Первая мысль - производительность: как ускорить создание отчётов? Возможно, оптимизировать SQL, сократить количество преобразований данных, кэшировать часть результатов. Если удастся сократить процесс с пяти минут до одной, это будет большой победой.

Но даже это заставит пользователя ждать. Если браузер зависнет, пропадёт соединение, закроется вкладка, - начинай сначала. Проблема не в производительности, а в дизайне.

Мы часто застреваем на мысли «сделать код быстрее». Это не что-то плохое, оптимизация производительности — ценный навык. Но мы не видим более серьёзной проблемы. Приложение выполняет всю работу синхронно, держа пользователя в заложниках до завершения.

Правильнее было бы спросить не «Как это ускорить?», а «Почему пользователь вообще ждёт?»
Если что-то выполняется минуты (или часы, дни), это не должно блокировать пользователя. Это должно происходить в фоновом режиме, вне основного потока запросов, пока пользователь занимается своими делами.

Однако, не стоит забывать оптимизировать сам код. Запросы к БД, обработка данных и генерация файлов — всё это имеет значение. Возможно, где-то отсутствует индекс, неэффективный цикл или есть более удобная библиотека для создания файлов Excel. Но эти оптимизации — лишь часть решения, а не полная картина.

Как решить проблему долгого ожидания?
Оставим ту же кнопку «Сгенерировать отчёт», но при этом бэкенд примет запрос, сохранит его где-то (например, как запись о задании в БД) и сразу же вернёт управление. В этом суть создания асинхронных API. Затем задание принимается фоновым обработчиком.

В роли обработчика может выступать фоновый сервис, задание Quartz или даже функция AWS Lambda, активируемая сообщением из очереди. Он берёт на себя всю основную работу: извлечение данных, создание файла и его загрузку в хранилище, например, S3 или Azure Blob.

После готовности отчёта обработчик обновит статус задания на «завершено» и уведомит пользователя. Это может быть email со ссылкой для скачивания или сообщение SignalR в режиме реального времени, которое отображается в приложении, со ссылкой на безопасное скачивание сохранённого отчёта.

Теперь пользователю не приходится ждать выполнения длительного HTTP-запроса. Сервер не удерживает открытые соединения в течение нескольких минут. В случае сбоя попытка может быть автоматически повторена. Вы также можете отслеживать ход выполнения или отменить задание при необходимости. А если сто пользователей одновременно запросят отчёты, система может масштабироваться без зависаний.

Работа кажется быстрее, даже если фактическое время генерации отчётов не изменилось. Ведь, в конечном счёте, пользователей интересуют не показатели производительности, а скорость отклика.

Это интересный вопрос, т.к. он показывает, как люди думают.

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

Другие на мгновение останавливаются и начинают думать о пользовательском опыте, фоновой обработке и отказоустойчивости. Вот тут-то и начинается настоящий разговор: очереди, повторные попытки, уведомления, безопасный обмен файлами и т.д. Существует множество способов превратить этот сценарий в более широкое обсуждение проектирования системы.

Единственно правильного ответа не существует. Но есть большая разница между тем, кто сосредоточен только на коде, и тем, кто может спроектировать масштабируемую систему.

Источник: https://www.milanjovanovic.tech/blog/the-interview-question-that-changed-how-i-think-about-system-design
👍21
День 2488. #SystemDesign101
Как Разработать Хороший API
Хорошо спроектированный API кажется интуитивно понятным, он просто работает. Но за этой простотой кроется набор последовательных принципов проектирования, которые делают API предсказуемым, безопасным и масштабируемым.

Вот что отличает хорошие API от плохих.

1. Идемпотентность
GET, HEAD, PUT и DELETE должны быть идемпотентными. Отправка одного и того же запроса несколько раз даёт один и тот же результат и не содержит непреднамеренных побочных эффектов.

POST и PATCH не являются идемпотентными. Каждый вызов создаёт новый ресурс или каким-либо образом изменяет состояние. Используйте ключи идемпотентности, хранящиеся в Redis или в базе данных. Клиент отправляет один и тот же ключ с повторными попытками, сервер распознает его и возвращает исходный ответ, не обрабатывая его повторно.

2. Версионирование
При изменении контракта API меняйте его версию. Указывать версию можно в URL, строке запроса или в заголовках запроса.

3. Имена ресурсов, основанные на существительных
Ресурсы должны быть существительными, а не глаголами. "/api/products", а не "/api/getProducts".

4. Безопасность
Обеспечьте безопасность каждой конечной точки с помощью надлежащей аутентификации. Bearer-токены (например, JWT) включают заголовок, полезную нагрузку и подпись для проверки запросов. Всегда используйте HTTPS и проверяйте токены при каждом вызове.

5. Пагинация
При возврате больших наборов данных используйте параметры пагинации, например "?limit=10&offset=20", чтобы обеспечить эффективность и согласованность ответов.

См. также «REST vs RESTful. В чём Разница?»

Источник: https://blog.bytebytego.com
👍7