Техническое собеседование на позицию .NET Core разработчика часто заставляет понервничать даже опытных программистов. Многие специалисты, проведя не одно собеседование в таких компаниях как Microsoft, Deloitte и других крупных игроках из списка Forbes 500, отмечают, что рекрутеры уделяют особое внимание знанию структур данных, алгоритмов и, естественно, специфике .NET Core и ASP.NET.
Что ожидают увидеть работодатели у разработчиков разного уровня? От джуниор-разработчиков обычно требуют базовых знаний C# и понимания отличий .NET Core от .NET Framework. Для мидл-разработчиков планка повышается – здесь уже нужно разбираться в принципах SOLID, знать особенности работы с middleware в ASP.NET Core и иметь опыт с Entity Framework Core. Сеньоры же должны демонстрировать глубокое понимание архитектурных решений, знать нюансы производительности и масштабирования, разбираться в микросервисах и контейнеризации. Не будут лишними и знания по CI/CD.
Основы .NET Core
На собеседовании практически всегда спрашивают про отличия .NET Core от .NET Framework. Это логично и ожидаемо — ведь важно понимать преимущества технологии, с которой вам предстоит работать.
Отличия от .NET Framework
Три ключевых преимущества .NET Core, о которых стоит упомянуть на собеседовании:
1. Кроссплатформенность. В отличие от .NET Framework, который работает только на Windows, .NET Core запускается на Windows, Linux и macOS. Это делает его идеальным выбором для облачных приложений и микросервисной архитектуры.
2. Производительность. .NET Core разрабатывался с нуля и получился гораздо быстрее и легче своего предшественника. Оптимизация производительности была одной из главных целей при его создании.
3. Открытый исходный код. .NET Core — полностью открытый проект, где каждый может внести свои улучшения в репозиторий GitHub. Это привело к быстрой эволюции платформы и широкой поддержке сообщества.
Стоит помнить, что с появлением .NET 5 и выше, Microsoft объединила .NET Framework и .NET Core под одним названием просто ".NET", но архитектурные преимущества Core сохранились.
Архитектура .NET Core
Архитектура .NET Core существенно отличается от .NET Framework. Если на собеседовании спросят про архитектуру, выделите эти отличительные черты:
Модульность — одна из ключевых особенностей .NET Core. Вместо монолитной сборки, как в .NET Framework, компоненты .NET Core распространяются как отдельные NuGet-пакеты. Вы подключаете только те модули, которые нужны вашему приложению, что уменьшает занимаемое место и повышает производительность.
CoreCLR — это среда выполнения .NET Core, включающая сборщик мусора, JIT-компилятор и другие низкоуровневые компоненты. В отличие от CLR из .NET Framework, CoreCLR спроектирован для работы на разных платформах и имеет более современную архитектуру.
CoreFX — это набор библиотек и API, которые составляют базовый функционал .NET Core. Фактически, это аналог .NET Framework Base Class Library, только переписанный с учетом современных подходов и кроссплатформенности.
NuGet и управление пакетами
Если на собеседовании зайдет речь о NuGet, помните, что в .NET Core эта система пакетов играет гораздо более важную роль, чем в .NET Framework. В .NET Core практически всё, включая базовые компоненты платформы, распространяется через NuGet-пакеты.
Как выглядит файл проекта с зависимостями в .NET Core:
| XML | 1
2
3
4
5
6
7
8
9
| <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.0" />
</ItemGroup>
</Project> |
|
Здесь каждый PackageReference — это отдельный NuGet-пакет, который будет загружен при сборке проекта. Такой подход позволяет создавать легковесные приложения, содержащие только необходимые компоненты.
Kestrel: веб-сервер для .NET Core
Если вы проходите собеседование на позицию веб-разработчика, вам наверняка зададут вопрос про Kestrel. Это кроссплатформенный веб-сервер, входящий в состав ASP.NET Core. В отличие от IIS, который привязан к Windows, Kestrel работает на всех платформах, поддерживаемых .NET Core. Это ключевой компонент, делающий ASP.NET Core по-настоящему кроссплатформенным.
Важно пояснить, что в реальных сценариях Kestrel часто используется вместе с обратным прокси-сервером вроде IIS (на Windows) или Nginx (на Linux). Обратный прокси обрабатывает HTTP-запросы и передает их в Kestrel, который уже запускает ваше ASP.NET Core приложение.
| C# | 1
2
3
4
5
6
7
8
9
10
11
| public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseKestrel(options =>
{
// Здесь можно настроить Kestrel
options.Limits.MaxRequestBodySize = 10 * 1024 * 1024; // 10 MB
});
webBuilder.UseStartup<Startup>();
}); |
|
Этот пример показывает, как можно настроить Kestrel при запуске ASP.NET Core приложения.
Конфигурация в .NET Core
Система конфигурации в .NET Core полностью переработана и стала гораздо гибче по сравнению с .NET Framework. Она поддерживает множество источников конфигурации, которые могут комбинироваться:- JSON-файлы (appsettings.json),
- Переменные среды,
- Аргументы командной строки,
- Секреты пользователя (user secrets),
- Хранилища ключей (Azure Key Vault и т.п.),
- Пользовательские провайдеры.
Важной особенностью является то, что конфигурация может иметь разные файлы для разных окружений. Например, appsettings.Development.json будет использован в режиме разработки, а appsettings.Production.json — в продакшене.
Пример кода для доступа к конфигурации:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| public class DatabaseOptions
{
public string ConnectionString { get; set; }
public int Timeout { get; set; }
}
// В методе ConfigureServices
services.Configure<DatabaseOptions>(Configuration.GetSection("Database"));
// В сервисе, где нужен доступ к настройкам
public class DataService
{
private readonly DatabaseOptions _options;
public DataService(IOptions<DatabaseOptions> options)
{
_options = options.Value;
}
} |
|
Такой подход с типизированными настройками делает код более надежным и упрощает тестирование.
Вопросы по MS SQL на собеседовании .net junior Доброго времени суток, друзья. В ходе подготовки к собеседованию в одну продуктовую компанию, возник вопрос, какой ориентировочный список базовых... Какие вопросы надо задать на собеседовании? Собственно наверно странный вопрос, но....
Какие вопросы надо задать на собеседовании? :wall:%-)
Надо со беседовать человека на... Какие вопросы могут задать на собеседовании по C# для начинающего программиста Подскажите пожалуиста, какие вопросы могут задать на собеседовании по C# для начинающего программиста? ПС: Буду рад услышать полезые советы так же ASP.NET Core. Старт - что нужно знать, чтобы стать ASP.NET Core разработчиком? Попалось хор краткое обзорное видео 2016 года с таким названием - Что нужно знать, чтобы стать ASP.NET MVC разработчиком?
Сейчас уже 2018 год....
Принципы SOLID в .NET Core
На собеседованиях часто спрашивают о принципах SOLID, особенно если позиция предполагает проектирование архитектуры приложений. SOLID — это набор принципов объектно-ориентированного программирования, которые делают код более понятным, гибким и поддерживаемым.
Разберем, что означает каждый из этих принципов:
S — Single Responsibility (Принцип единственной ответственности). Класс должен иметь только одну причину для изменения, то есть выполнять только одну задачу. Забудьте о классах на тысячи строк кода, которые делают всё и сразу.
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // Плохой пример
public class UserService
{
public void RegisterUser(User user) { /* ... */ }
public void SendEmail(string to, string subject) { /* ... */ }
public void GeneratePdf(User user) { /* ... */ }
}
// Хороший пример
public class UserService
{
private readonly EmailService _emailService;
private readonly PdfGenerator _pdfGenerator;
public UserService(EmailService emailService, PdfGenerator pdfGenerator)
{
_emailService = emailService;
_pdfGenerator = pdfGenerator;
}
public void RegisterUser(User user) { /* ... */ }
} |
|
O — Open/Closed (Принцип открытости/закрытости). Классы должны быть открыты для расширения, но закрыты для модификации. Проще говоря, вы должны иметь возможность добавлять новое поведение без изменения существующего кода.
L — Liskov Substitution (Принцип подстановки Лисков). Объекты базового класса могут быть заменены объектами их подтипов без нарушения работы программы. Грубо говоря, используйте интерфейсы вместо конкретных классов, когда это возможно.
I — Interface Segregation (Принцип разделения интерфейсов). Лучше иметь множество специализированных интерфейсов, чем один универсальный. Никто не должен реализовывать методы, которые не использует.
D — Dependency Inversion (Принцип инверсии зависимостей). Зависимости должны строиться на абстракциях, а не на конкретных реализациях. Высокоуровневые модули не должны зависеть от низкоуровневых; оба должны зависеть от абстракций.
DRY и другие принципы
Еще один важный принцип — DRY (Don't Repeat Yourself, не повторяйся). Суть в том, чтобы не писать одинаковый код дважды. Если вам нужно использовать один и тот же фрагмент кода в нескольких местах, выделите его в отдельный метод или класс.
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| // Вместо этого:
public decimal CalculateDiscount(Order order)
{
decimal totalDiscount = 0;
foreach (var item in order.Items)
{
if (item.Product.IsPromotional)
{
totalDiscount += item.Price * 0.1m;
}
}
return totalDiscount;
}
public decimal CalculateTax(Order order)
{
decimal totalTax = 0;
// Тот же цикл, что и выше, но с другой логикой
foreach (var item in order.Items)
{
totalTax += item.Price * 0.2m;
}
return totalTax;
}
// Лучше сделать так:
public decimal ProcessOrderItems(Order order, Func<OrderItem, decimal> processor)
{
return order.Items.Sum(processor);
}
// И использовать:
var discount = ProcessOrderItems(order, item => item.Product.IsPromotional ? item.Price * 0.1m : 0);
var tax = ProcessOrderItems(order, item => item.Price * 0.2m); |
|
Middleware в ASP.NET Core
Middleware — это компоненты, которые обрабатывают HTTP-запросы и ответы в ASP.NET Core. Они формируют конвейер обработки запросов, через который проходит каждый запрос. Вот как создается простой middleware:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| public class SimpleMiddleware
{
private readonly RequestDelegate _next;
public SimpleMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
// Код, выполняемый до вызова следующего middleware
Console.WriteLine($"Request: {context.Request.Path}");
// Вызов следующего middleware в конвейере
await _next(context);
// Код, выполняемый после вызова следующего middleware
Console.WriteLine($"Response status: {context.Response.StatusCode}");
}
} |
|
В методе Configure класса Startup middleware регистрируются в определенном порядке, который и определяет последовательность их выполнения:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// Middleware для обработки исключений
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}
// Middleware для статических файлов
app.UseStaticFiles();
// Middleware для маршрутизации
app.UseRouting();
// Middleware для аутентификации и авторизации
app.UseAuthentication();
app.UseAuthorization();
// Middleware для конечных точек
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapRazorPages();
});
} |
|
На собеседовании вас могут спросить о различиях между методами app.Use и app.Run. Метод Use получает делегат, представляющий следующий middleware в конвейере, тогда как Run — это терминальный middleware, который не передает управление дальше.
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
| // Use позволяет продолжить выполнение конвейера
app.Use(async (context, next) =>
{
Console.WriteLine("Перед следующим middleware");
await next(); // Вызов следующего middleware
Console.WriteLine("После следующего middleware");
});
// Run завершает конвейер
app.Run(async context =>
{
await context.Response.WriteAsync("Конец конвейера");
}); |
|
Фильтры в ASP.NET Core
Важно понимать разницу между middleware и фильтрами. Middleware работают на уровне обработки HTTP-запроса до и после его обработки, в то время как фильтры выполняются в процессе обработки запроса в конвейере MVC. Middleware могут обрабатывать любые запросы, тогда как фильтры применяются только к конкретным контроллерам или действиям. Кроме того, middleware оперирует только с HttpContext, в то время как фильтры имеют доступ к объектам MVC или Razor Pages, таким как ModelState. Типичный пример фильтра — это [Authorize], который проверяет, аутентифицирован ли пользователь перед выполнением действия контроллера:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| [Authorize]
public class AdminController : Controller
{
public IActionResult Index()
{
return View();
}
[AllowAnonymous] // Исключение из правила авторизации
public IActionResult Login()
{
return View();
}
} |
|
Понимание разницы между middleware и фильтрами поможет вам правильно структурировать ваше приложение и выбирать подходящие инструменты для решения конкретных задач.
Продвинутые концепции
Внедрение зависимостей в .NET Core
Dependency Injection (DI) – одна из самых мощных возможностей, которую .NET Core предоставляет из коробки. На собеседовании часто спрашивают о том, зачем нужно внедрение зависимостей, и что оно даёт разработчикам. DI позволяет отделить создание объекта от его использования. Это значит, что вы можете внедрять разные экземпляры в зависимости от ситуации. Например, у вас есть класс с внедрённым подключением к базе данных. При написании unit-тестов для этого класса вы вряд ли захотите реально обращаться к базе. Вместо этого можно внедрить мок-объект, имитирующий работу с базой. Кроме того, DI позволяет избежать дублирования кода, ответственного за инстанцирование объектов. Ваши классы становятся чище и понятнее.
В .NET Core сервисы регистрируются в контейнере DI в методе ConfigureServices класса Startup:
| C# | 1
2
3
4
5
6
7
| public void ConfigureServices(IServiceCollection services)
{
// Регистрация сервисов
services.AddTransient<IEmailSender, SmtpEmailSender>();
services.AddScoped<IUserRepository, UserRepository>();
services.AddSingleton<ICacheService, MemoryCacheService>();
} |
|
В ASP.NET Core существует три типа служб с разным жизненным циклом:
1. Singleton – создаётся один экземпляр на всё время жизни приложения.
2. Scoped – создаётся один экземпляр на каждый запрос.
3. Transient – создаётся новый экземпляр каждый раз, когда служба запрашивается.
Различие между scoped и transient службами часто вызывает вопросы на собеседованиях. Scoped-сервис гарантирует, что в рамках одного запроса вы получите один и тот же экземпляр. Например, если сервис внедряется в другой сервис и в контроллер в рамках одного HTTP-запроса, это будет один и тот же объект. Transient-сервис означает, что каждый раз при запросе сервиса создаётся новый экземпляр. Это полезно для легковесных сервисов, не хранящих состояние.
Асинхронное программирование в .NET Core
Асинхронное программирование – еще одна тема, которая часто всплывает на собеседованиях. В .NET Core асинхронное программирование реализуется с помощью ключевых слов async и await и паттерна Task-based Asynchronous Pattern (TAP).
Интересный вопрос, который могут задать: "Когда лучше использовать Parallel.For вместо async/await?" Ответ: класс Parallel предназначен для параллелизма – для задач с интенсивными вычислениями, которые можно выполнять параллельно. Например, обработка изображений часто может быть распараллелена. А вот async/await предназначены для задач, которые должны ждать завершения предыдущих задач. Например, вы хотите записать исходный код веб-сайта в текстовый файл. Обе операции блокируют текущий поток до завершения.
Пример асинхронного метода в контроллере ASP.NET Core:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| [HttpGet]
public async Task<IActionResult> GetUserDataAsync(int userId)
{
// Асинхронный запрос к базе данных
var user = await _userRepository.GetByIdAsync(userId);
if (user == null)
return NotFound();
// Асинхронный запрос к другому сервису
var orders = await _orderService.GetOrdersForUserAsync(userId);
var viewModel = new UserViewModel
{
User = user,
Orders = orders
};
return View(viewModel);
} |
|
Такой подход не блокирует поток во время ожидания ответа от базы данных или другого сервиса, что позволяет серверу обрабатывать другие запросы параллельно.
Работа с Entity Framework Core
Entity Framework Core (EF Core) – это современная ORM для .NET Core. На собеседованиях часто спрашивают о различиях между EF Core и другими способами доступа к данным, такими как Dapper. EF Core предлагает высокоуровневую абстракцию и множество функций, таких как отслеживание изменений, lazy loading, миграции и т.д. Dapper, с другой стороны, является микро-ORM, которая фокусируется на производительности и простоте.
Пример работы с EF Core:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
public DbSet<User> Users { get; set; }
public DbSet<Order> Orders { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Конфигурация модели с использованием Fluent API
modelBuilder.Entity<User>()
.HasMany(u => u.Orders)
.WithOne(o => o.User)
.HasForeignKey(o => o.UserId);
}
}
// В методе ConfigureServices
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); |
|
А вот пример с Dapper:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| public async Task<IEnumerable<User>> GetUsersAsync()
{
using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
return await connection.QueryAsync<User>("SELECT * FROM Users");
}
public async Task<User> GetUserWithOrdersAsync(int userId)
{
using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
var sql = @"
SELECT u.*, o.*
FROM Users u
LEFT JOIN Orders o ON u.Id = o.UserId
WHERE u.Id = @UserId";
User user = null;
var orders = new List<Order>();
await connection.QueryAsync<User, Order, User>(
sql,
(u, o) =>
{
if (user == null)
user = u;
if (o != null)
orders.Add(o);
return user;
},
new { UserId = userId },
splitOn: "Id");
if (user != null)
user.Orders = orders;
return user;
} |
|
Выбор между EF Core и Dapper зависит от конкретных требований проекта. EF Core хорош для быстрой разработки и сложных отношений между сущностями, в то время как Dapper обеспечивает лучшую производительность и больший контроль над SQL-запросами.
Обработка HTTP-запросов в ASP.NET Core
В ASP.NET Core есть несколько способов обработки HTTP-запросов, о которых могут спросить на собеседовании:
1. Контроллеры MVC – традиционный подход, где каждый контроллер содержит методы (действия), обрабатывающие определённые HTTP-запросы.
2. Razor Pages – подход, основанный на страницах, где каждая страница содержит обработчики для разных HTTP-методов.
3. Minimal APIs – новый подход в .NET 6 и выше, позволяющий определять API с минимальным количеством кода.
Пример Minimal API из .NET 6:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Определение маршрута с обработчиком
app.MapGet("/hello", () => "Hello, World!");
// Определение маршрута с параметром
app.MapGet("/users/{id}", (int id) => $"User ID: {id}");
// Обработка POST-запроса
app.MapPost("/users", async (User user, IUserRepository repository) =>
{
await repository.AddAsync(user);
return Results.Created($"/users/{user.Id}", user);
});
app.Run(); |
|
На собеседовании также могут спросить о различиях между методами HTTP, например, GET и POST. GET-метод менее безопасен, так как параметры передаются прямо в URL. Кроме того, GET-запросы можно кешировать и добавлять в закладки, а POST-запросы – нет. GET может передавать только ASCII-символы, тогда как POST не имеет таких ограничений и может отправлять двоичные данные, строки и специальные символы. Веб-браузеры обычно ограничивают URL 255 символами, а тело POST-запроса ограничено только настройками сервера.
Razor Pages vs MVC
Ещё одна тема собеседований — различия между Razor Pages и MVC. Хотя оба подхода используются для создания веб-приложений в ASP.NET Core, между ними есть существенные отличия. Razor Pages использует паттерн MVVM (Model-View-ViewModel), тогда как MVC — классический паттерн Model-View-Controller. В Razor Pages используется маршрутизация на основе страниц, а в MVC — контроллеры для управления запросами.
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // Пример Razor Page модели
public class IndexModel : PageModel
{
private readonly IProductService _productService;
public IndexModel(IProductService productService)
{
_productService = productService;
}
public IList<Product> Products { get; set; }
public async Task OnGetAsync()
{
Products = await _productService.GetProductsAsync();
}
} |
|
Многие разработчики считают Razor Pages более подходящим для простых сценариев, а MVC — для сложных веб-приложений с богатой бизнес-логикой.
ViewModels и передача данных в представления
Часто спрашивают, почему рекомендуется передавать данные во View через ViewModel, а не напрямую через модели. Во-первых, мы обычно не хотим раскрывать все поля модели. Во-вторых, представление может требовать данные из нескольких моделей. Вместо передачи избыточных данных, мы создаём объект, содержащий только необходимую информацию.
| C# | 1
2
3
4
5
6
7
8
9
10
| public class ProductDetailsViewModel
{
public string Name { get; set; }
public decimal Price { get; set; }
public string Description { get; set; }
public string CategoryName { get; set; }
public List<string> Tags { get; set; }
// Заметьте, что здесь нет полей модели Product вроде CreatedDate,
// InternalCode и других, которые не нужны представлению
} |
|
Управление состоянием в ASP.NET Core
HTTP по своей природе — протокол без сохранения состояния. Он не хранит данные пользователя между запросами. ASP.NET Core должен предоставить способ сохранять данные между запросами.
Для хранения состояния в ASP.NET Core можно использовать:
1. Куки — хранят небольшие фрагменты данных на клиентской стороне.
2. TempData — хранит данные между двумя последовательными запросами.
3. Кэш — хранит данные на стороне сервера с возможностью установки срока действия.
4. Сессии — хранят данные на сервере, связанные с конкретным пользовательским сеансом.
5. Скрытые поля — хранят данные в HTML-форме, невидимые для пользователя.
6. Строки запроса — хранят данные в URL.
7. HttpContext.Items — хранят данные на протяжении одного запроса.
Сессии работают путём генерации идентификатора сессии, когда пользователь впервые посещает сайт. Этот ID сохраняется в куки пользователя. Когда пользователь возвращается, ASP.NET извлекает этот куки и получает значения, хранящиеся в сессии.
Загрузка файлов в ASP.NET Core
Для обработки загрузки файлов в ASP.NET Core используется интерфейс IFormFile:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| public IActionResult UploadProfilePhoto(IFormFile file)
{
if (file == null || file.Length == 0)
return BadRequest("Файл не загружен");
var uploadsFolder = Path.Combine(_environment.WebRootPath, "uploads");
var uniqueFileName = Guid.NewGuid().ToString() + "_" + file.FileName;
var filePath = Path.Combine(uploadsFolder, uniqueFileName);
using (var stream = new FileStream(filePath, FileMode.Create))
{
file.CopyTo(stream);
}
// Сохраняем путь к файлу в базе данных
_userService.UpdateProfilePhoto(User.Identity.Name, uniqueFileName);
return RedirectToAction("Profile");
} |
|
Проверка работоспособности в ASP.NET Core
Health Check — это решение, которое позволяет пользователям приложения проверить, что сервис работает. Это особенно полезно в микросервисной архитектуре или при использовании балансировщиков нагрузки.
Базовая проверка работоспособности настраивается довольно просто:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| // В ConfigureServices
services.AddHealthChecks()
.AddDbContextCheck<ApplicationDbContext>()
.AddCheck<CustomHealthCheck>("Custom");
// В Configure
app.UseEndpoints(endpoints =>
{
endpoints.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = WriteResponse
});
// другие маршруты...
});
// Метод для форматирования ответа
private static Task WriteResponse(HttpContext context, HealthReport result)
{
context.Response.ContentType = "application/json";
var response = new
{
status = result.Status.ToString(),
checks = result.Entries.Select(x => new
{
component = x.Key,
status = x.Value.Status.ToString(),
description = x.Value.Description
})
};
return context.Response.WriteAsync(JsonSerializer.Serialize(response));
} |
|
Если сервис недоступен, будет возвращен HTTP-ответ со статусом 503 Service Unavailable.
Настройка логирования в ASP.NET Core
Логирование — важная часть любого производственного приложения. В ASP.NET Core предлагается встроенная инфраструктура логирования через пакет Microsoft.Extensions.Logging.
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| public class UserService
{
private readonly ILogger<UserService> _logger;
public UserService(ILogger<UserService> logger)
{
_logger = logger;
}
public async Task<User> GetUserByIdAsync(int id)
{
try
{
_logger.LogInformation("Получение пользователя с ID: {UserId}", id);
var user = await _userRepository.GetByIdAsync(id);
if (user == null)
{
_logger.LogWarning("Пользователь с ID {UserId} не найден", id);
return null;
}
return user;
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка при получении пользователя с ID {UserId}", id);
throw;
}
}
} |
|
Для конфигурации логирования используется интерфейс ILoggerFactory:
| C# | 1
2
3
4
5
6
7
8
| public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
{
// Настройка логирования
loggerFactory.AddConsole();
loggerFactory.AddDebug();
// Также можно использовать популярные библиотеки, такие как NLog или Serilog
} |
|
Логирование позволяет отслеживать поведение приложения в различных средах и быстрее диагностировать проблемы при их возникновении.
Практические аспекты
Микросервисная архитектура на .NET Core
В современной разработке микросервисная архитектура становится всё более популярной, и .NET Core идеально подходит для её реализации. На собеседованиях часто просят рассказать о преимуществах микросервисной архитектуры и опыте её использования.
Ключевые преимущества микросервисов:
1. Технологическая гибкость. Каждый микросервис может быть написан на любом языке программирования. Разные команды могут работать над разными частями проекта, выбирая удобный для себя язык разработки.
2. Масштабируемость. Микросервисы гораздо легче масштабировать по сравнению с монолитными приложениями. Вместо масштабирования всего приложения можно увеличивать ресурсы лишь для тех сервисов, которые испытывают повышенную нагрузку. Для оркестрации контейнеров часто используется Kubernetes.
3. Изоляция ошибок. Если в микросервисе возникает ошибка, она изолирована от других сервисов. Например, представьте, что у вас интернет-магазин, и сервис оформления заказа дал сбой. В микросервисной архитектуре сервис авторизации продолжит работать, так как не зависит от сервиса оформления заказа.
Пример простого микросервиса на ASP.NET Core:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| [ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
private readonly IProductRepository _repository;
public ProductsController(IProductRepository repository)
{
_repository = repository;
}
[HttpGet]
public async Task<ActionResult<IEnumerable<ProductDto>>> GetAll()
{
var products = await _repository.GetAllAsync();
return Ok(products.Select(p => new ProductDto(p)));
}
[HttpGet("{id}")]
public async Task<ActionResult<ProductDto>> GetById(int id)
{
var product = await _repository.GetByIdAsync(id);
if (product == null)
return NotFound();
return Ok(new ProductDto(product));
}
} |
|
Контейнеризация с Docker
При обсуждении микросервисов часто всплывает тема использования Docker-контейнеров. Контейнеры предоставляют изолированную среду для запуска приложений, что упрощает развёртывание и масштабирование. Docker-контейнеры — это безопасный способ создавать и доставлять изолированные модули. Они позволяют разработчикам работать с одними и теми же библиотеками и конфигурациями в разных средах. Используя контейнер, вы не беспокоитесь об операционной системе или конфигурации машины.
Пример файла Dockerfile для .NET Core приложения:
| Bash | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| # Стадия сборки
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["MyApp.csproj", "./"]
RUN dotnet restore "MyApp.csproj"
COPY . .
RUN dotnet build "MyApp.csproj" -c Release -o /app/build
# Стадия публикации
FROM build AS publish
RUN dotnet publish "MyApp.csproj" -c Release -o /app/publish
# Финальный образ
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "MyApp.dll"] |
|
Фоновые задачи в ASP.NET Core
На собеседованиях часто спрашивают о способах запуска фоновых задач в ASP.NET Core. Существует несколько подходов:
1. Использование библиотеки Hangfire. Это популярное решение для выполнения фоновых задач в .NET:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // В Startup.ConfigureServices
services.AddHangfire(config =>
{
config.UseSqlServerStorage(Configuration.GetConnectionString("HangfireConnection"));
});
services.AddHangfireServer();
// В Startup.Configure
app.UseHangfireDashboard();
// Планирование задач
BackgroundJob.Enqueue(() => Console.WriteLine("Fire-and-forget!"));
BackgroundJob.Schedule(() => Console.WriteLine("Delayed!"), TimeSpan.FromDays(1));
RecurringJob.AddOrUpdate("myrecurringjob", () => Console.WriteLine("Recurring!"), Cron.Daily); |
|
2. Hosted Services. Для этого нужно реализовать интерфейс IHostedService, определяющий два метода: StartAsync и StopAsync:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| public class TimedHostedService : IHostedService, IDisposable
{
private Timer _timer;
private readonly ILogger<TimedHostedService> _logger;
public TimedHostedService(ILogger<TimedHostedService> logger)
{
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Timed Hosted Service запущен.");
_timer = new Timer(DoWork, null, TimeSpan.Zero,
TimeSpan.FromSeconds(5));
return Task.CompletedTask;
}
private void DoWork(object state)
{
_logger.LogInformation($"Выполняется в фоне: {DateTime.Now}");
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Timed Hosted Service остановлен.");
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
}
// Регистрация в Startup.ConfigureServices
services.AddHostedService<TimedHostedService>(); |
|
3. Task.Run. Самый простой способ для кратковременных операций:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| public async Task<IActionResult> ProcessData()
{
// Запуск длительной операции в фоне
_ = Task.Run(async () =>
{
try
{
await ProcessLongRunningTask();
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка в фоновой задаче");
}
});
return Ok("Обработка начата");
} |
|
Производительность и кеширование
На собеседованиях также важно показать знание того, как оптимизировать производительность приложений .NET Core. Один из ключевых моментов — грамотное использование кеширования.
ASP.NET Core предлагает несколько вариантов кеширования:
1. In-Memory кеширование:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| public class ProductService
{
private readonly IMemoryCache _cache;
private readonly IProductRepository _repository;
public ProductService(IMemoryCache cache, IProductRepository repository)
{
_cache = cache;
_repository = repository;
}
public async Task<Product> GetProductByIdAsync(int id)
{
string cacheKey = $"product_{id}";
if (!_cache.TryGetValue(cacheKey, out Product product))
{
product = await _repository.GetByIdAsync(id);
if (product != null)
{
var cacheOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromMinutes(10))
.SetSlidingExpiration(TimeSpan.FromMinutes(2));
_cache.Set(cacheKey, product, cacheOptions);
}
}
return product;
}
} |
|
2. Распределённое кеширование (например, с Redis):
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| // В Startup.ConfigureServices
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = Configuration.GetConnectionString("Redis");
options.InstanceName = "SampleInstance_";
});
// Использование
public class ProductService
{
private readonly IDistributedCache _cache;
private readonly IProductRepository _repository;
// ... конструктор ...
public async Task<Product> GetProductByIdAsync(int id)
{
string cacheKey = $"product_{id}";
Product product = null;
var cachedData = await _cache.GetStringAsync(cacheKey);
if (cachedData != null)
{
product = JsonSerializer.Deserialize<Product>(cachedData);
}
else
{
product = await _repository.GetByIdAsync(id);
if (product != null)
{
var options = new DistributedCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromMinutes(10));
await _cache.SetStringAsync(
cacheKey,
JsonSerializer.Serialize(product),
options);
}
}
return product;
}
} |
|
Правильное использование кеширования может значительно улучшить производительность приложения, особенно при работе с данными, которые редко меняются или требуют сложных вычислений для получения.
Безопасность приложений
Вопросы безопасности — ещё одна важная тема на собеседованиях. Для защиты от атак типа CSRF (Cross-Site Request Forgery) в ASP.NET Core используются анти-подделочные токены.
| C# | 1
2
3
4
5
6
7
8
9
10
| // В форме
@Html.AntiForgeryToken()
// В контроллере
[ValidateAntiForgeryToken]
public async Task<IActionResult> Update(ProductViewModel model)
{
// Обновление продукта...
return RedirectToAction("Index");
} |
|
Примеры вопросов и типичные ошибки
Проходя собеседования на позиции, связанные с .NET Core, я заметил определённые паттерны в вопросах и частые ошибки кандидатов.
Типичные вопросы с примерами ответов
OWIN: что это и зачем?
OWIN (Open Web Interface for .NET) — довольно частый вопрос на собеседованиях. Это спецификация, которая описывает, как веб-приложения и веб-серверы должны быть отделены друг от друга для облегчения переноса в другие среды. Ключевая идея OWIN — разорвать тесную связь между веб-приложением и сервером, которая исторически существовала в ASP.NET. Это позволяет запускать веб-приложения в различных средах без сильной привязки к конкретному серверу. Когда отвечаете на этот вопрос, подчеркните, что .NET Core был разработан с учётом принципов OWIN, что позволило создать гораздо более модульную и гибкую архитектуру.
Методы Dispose и Finalize: в чём разница?
Ещё один хитрый вопрос, на который многие отвечают неправильно. Метод Finalize вызывается сборщиком мусора, когда тот считает нужным, и вы не имеете контроля над временем вызова. А вот метод Dispose должен вызываться из вашего кода.
Dispose предназначен для закрытия и очистки неуправляемых ресурсов, таких как подключения к базе данных, файловые дескрипторы и HTTP-соединения:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| public class DatabaseService : IDisposable
{
private SqlConnection _connection;
private bool _disposed = false;
public DatabaseService(string connectionString)
{
_connection = new SqlConnection(connectionString);
_connection.Open();
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Освобождаем управляемые ресурсы
_connection?.Close();
}
// Освобождаем неуправляемые ресурсы
_connection = null;
_disposed = true;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
~DatabaseService()
{
Dispose(false);
}
} |
|
Типы результатов в ASP.NET Core контроллерах
Многие кандидаты знают лишь пару типов результатов, но на собеседовании лучше продемонстрировать более полное понимание:- ViewResult — возвращает представление (Razor-шаблон), используемый для генерации HTML.
- ContentResult — возвращает строку контента, обычно для простого текста или HTML.
- JsonResult — возвращает объект в формате JSON, часто используется для клиентских скриптов.
- FileResult — возвращает файл, такой как PDF, текстовый файл или изображение.
- RedirectResult — перенаправляет клиента на другой URL.
- RedirectToActionResult — перенаправляет клиента на другой метод действия в том же контроллере.
- RedirectToRouteResult — перенаправляет клиента на другой маршрут.
- StatusCodeResult — возвращает конкретный HTTP-код состояния, например 404 (не найдено) или 500 (внутренняя ошибка сервера).
- ObjectResult — возвращает объект.
- EmptyResult — возвращает пустой результат.
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
| // Пример контроллера, демонстрирующего различные типы результатов
public class DemoController : Controller
{
public IActionResult Index()
{
return View(); // ViewResult
}
public IActionResult GetText()
{
return Content("Привет, мир!"); // ContentResult
}
public IActionResult GetJson()
{
var data = new { Name = "Иван", Age = 30 };
return Json(data); // JsonResult
}
public IActionResult GetFile()
{
byte[] fileBytes = System.IO.File.ReadAllBytes("sample.pdf");
return File(fileBytes, "application/pdf", "sample.pdf"); // FileResult
}
public IActionResult RedirectToGoogle()
{
return Redirect("https://www.google.com"); // RedirectResult
}
public IActionResult RedirectToIndex()
{
return RedirectToAction("Index"); // RedirectToActionResult
}
public IActionResult NotFound()
{
return StatusCode(404); // StatusCodeResult
}
public IActionResult GetObject()
{
var user = new User { Id = 1, Name = "Иван" };
return new ObjectResult(user); // ObjectResult
}
public IActionResult DoNothing()
{
return new EmptyResult(); // EmptyResult
}
} |
|
Отличия JSON от Content
Вопрос, который тоже часто задают: чем метод Json отличается от Content? Ведь, в конце концов, JSON — тоже контент, не так ли?
Ключевое отличие: метод Json автоматически устанавливает правильный MIME-тип для JSON (application/json), а Content этого не делает. Кроме того, Json выполняет сериализацию объекта в JSON-строку.
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // Использование Json
public IActionResult GetUserJson()
{
var user = new User { Id = 1, Name = "Иван" };
return Json(user); // Автоматически сериализует и устанавливает Content-Type: application/json
}
// Эквивалентно, но более многословно:
public IActionResult GetUserContent()
{
var user = new User { Id = 1, Name = "Иван" };
var json = JsonSerializer.Serialize(user);
return Content(json, "application/json");
} |
|
HTTP-запросы
Хороший кандидат должен знать основные HTTP-запросы (глаголы) и их назначение:- GET — получение ресурса или данных.
- POST — создание нового ресурса.
- PUT — полное обновление существующего ресурса.
- PATCH — частичное обновление ресурса.
- DELETE — удаление ресурса.
- HEAD — получение метаданных о ресурсе без самого ресурса.
- OPTIONS — получение информации о методах, доступных для ресурса.
Особенно важно понимать разницу между PUT и PATCH. PUT используется, когда клиент отправляет весь объект целиком, а PATCH — когда клиент отправляет только частичные данные.
OWASP: что это и почему важно?
OWASP (Open Web Application Security Project) — это список распространённых уязвимостей веб-приложений. Многие компании ожидают, что разработчики знакомы с OWASP TOP 10 — списком самых частых угроз безопасности.
Вот некоторые из них:
1. Инъекции (SQL, NoSQL, OS-команд).
2. Нарушения аутентификации.
3. Утечка конфиденциальных данных.
4. XML External Entities (XXE).
5. Нарушения контроля доступа.
6. Неправильная настройка безопасности.
7. Cross-Site Scripting (XSS).
8. Небезопасная десериализация.
9. Использование компонентов с известными уязвимостями.
10. Недостаточное логирование и мониторинг.
Знание этих уязвимостей особенно важно для веб-разработчиков, так как они помогают создавать более безопасные приложения.
Типичные ошибки кандидатов
Неумение объяснить SOLID
Многие кандидаты слышали о принципах SOLID, но не могут внятно объяснить, что они означают, или привести конкретные примеры их применения. Особые проблемы вызывают принципы Лисков (L) и Инверсии зависимостей (D). Подготовьте конкретные примеры из вашего опыта для каждого принципа:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| // Пример нарушения принципа подстановки Лисков
public class Rectangle
{
public virtual int Width { get; set; }
public virtual int Height { get; set; }
public int GetArea()
{
return Width * Height;
}
}
public class Square : Rectangle
{
private int _size;
public override int Width
{
get { return _size; }
set { _size = value; }
}
public override int Height
{
get { return _size; }
set { _size = value; }
}
}
// Это нарушает принцип подстановки Лисков:
Rectangle rect = new Square();
rect.Width = 5;
rect.Height = 10;
// Ожидаем 50, но получаем 100, так как Square изменил оба свойства
int area = rect.GetArea(); |
|
Путаница с циклом жизни сервисов
Многие не могут чётко объяснить разницу между Singleton, Scoped и Transient службами, или когда какой тип использовать. Часто встречаю ответы вроде "Singleton быстрее, потому что создаётся один раз" — это не совсем правильный акцент.
Правильное понимание:- Singleton используется для сервисов, которые не имеют состояния или их состояние должно быть общим для всего приложения (например, кэши).
- Scoped идеален для сервисов, состояние которых должно быть одинаковым в рамках запроса (например, подключение к базе данных).
- Transient лучше для небольших сервисов без состояния, особенно если они могут использоваться в разных контекстах.
Неправильные подходы к многопоточности
Часто кандидаты не могут объяснить, когда использовать async/await, а когда Parallel.ForEach или Task.Run. Или, ещё хуже, начинают создавать потоки вручную там, где это не нужно.
Вот простой ориентир:- Используйте
async/await для операций ввода-вывода (I/O-bound операций).
- Используйте
Parallel для операций, интенсивно использующих процессор (CPU-bound операций).
- Используйте
Task.Run для переноса блокирующих операций в фоновый поток, особенно в UI-приложениях.
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // Пример правильного использования async/await для I/O-операций
public async Task<string> DownloadContentAsync(string url)
{
using var httpClient = new HttpClient();
return await httpClient.GetStringAsync(url);
}
// Пример правильного использования Parallel для CPU-операций
public void ProcessImages(List<Image> images)
{
Parallel.ForEach(images, image =>
{
// Интенсивная обработка картинки
ApplyFilters(image);
ResizeImage(image);
});
} |
|
Недостаточное знание безопасности
Многие кандидаты игнорируют аспекты безопасности, такие как защита от CSRF, XSS, SQL-инъекций и т.д. Однако на собеседованиях почти всегда спрашивают о подходах к обеспечению безопасности. Например, кандидат должен знать, как правильно обрабатывать пароли (используя хэширование с солью), как защищаться от атак типа CSRF (используя антиподделочные токены) и как предотвращать SQL-инъекции (используя параметризованные запросы или ORM).
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // Плохой пример - уязвим для SQL-инъекций
public User GetUserByUsername(string username)
{
using var connection = new SqlConnection(_connectionString);
connection.Open();
var command = new SqlCommand($"SELECT * FROM Users WHERE Username = '{username}'", connection);
var reader = command.ExecuteReader();
// Чтение данных...
}
// Хороший пример - защищён от SQL-инъекций
public User GetUserByUsername(string username)
{
using var connection = new SqlConnection(_connectionString);
connection.Open();
var command = new SqlCommand("SELECT * FROM Users WHERE Username = @username", connection);
command.Parameters.AddWithValue("@username", username);
var reader = command.ExecuteReader();
// Чтение данных...
} |
|
Непонимание производительности
Часто спрашивают о том, как оптимизировать производительность .NET Core приложений. Многие кандидаты дают поверхностные ответы, не демонстрируя глубокого понимания. Будьте готовы обсудить:- Использование асинхронных операций для улучшения масштабируемости.
- Различные стратегии кеширования (in-memory, distributed).
- Пулы соединений и их настройка.
- Статические анализаторы кода для выявления проблем с производительностью.
- Профилирование приложений для поиска узких мест.
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
| // Пример неэффективного кода
public IEnumerable<Product> GetExpensiveProducts(List<Product> products)
{
var filtered = products.Where(p => p.Price > 1000).ToList(); // Создаёт временный список
return filtered.OrderBy(p => p.Name); // Сортирует временный список
}
// Более эффективная версия
public IEnumerable<Product> GetExpensiveProducts(List<Product> products)
{
return products.Where(p => p.Price > 1000).OrderBy(p => p.Name); // Чейнинг без промежуточных списков
} |
|
Пример реальной задачи с собеседования
Пример задачи, которую я сам получал на собеседовании в крупной компании:
Задача: Реализовать простой API для работы с заметками, который должен:
1. Создавать новую заметку.
2. Получать список всех заметок.
3. Получать заметку по ID.
4. Обновлять заметку.
5. Удалять заметку.
Заметка должна содержать:
Уникальный ID
Заголовок
Содержание
Дату создания
Дату последнего обновления
Решение:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
| public class Note
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}
public interface INoteRepository
{
Task<IEnumerable<Note>> GetAllAsync();
Task<Note> GetByIdAsync(int id);
Task<Note> CreateAsync(Note note);
Task<Note> UpdateAsync(Note note);
Task DeleteAsync(int id);
}
public class InMemoryNoteRepository : INoteRepository
{
private readonly List<Note> _notes = new List<Note>();
private int _nextId = 1;
public Task<IEnumerable<Note>> GetAllAsync()
{
return Task.FromResult(_notes.AsEnumerable());
}
public Task<Note> GetByIdAsync(int id)
{
var note = _notes.FirstOrDefault(n => n.Id == id);
return Task.FromResult(note);
}
public Task<Note> CreateAsync(Note note)
{
note.Id = _nextId++;
note.CreatedAt = DateTime.UtcNow;
note.UpdatedAt = note.CreatedAt;
_notes.Add(note);
return Task.FromResult(note);
}
public Task<Note> UpdateAsync(Note note)
{
var existingNote = _notes.FirstOrDefault(n => n.Id == note.Id);
if (existingNote == null)
return Task.FromResult<Note>(null);
existingNote.Title = note.Title;
existingNote.Content = note.Content;
existingNote.UpdatedAt = DateTime.UtcNow;
return Task.FromResult(existingNote);
}
public Task DeleteAsync(int id)
{
var note = _notes.FirstOrDefault(n => n.Id == id);
if (note != null)
_notes.Remove(note);
return Task.CompletedTask;
}
}
[ApiController]
[Route("api/[controller]")]
public class NotesController : ControllerBase
{
private readonly INoteRepository _repository;
public NotesController(INoteRepository repository)
{
_repository = repository;
}
[HttpGet]
public async Task<ActionResult<IEnumerable<Note>>> GetAll()
{
var notes = await _repository.GetAllAsync();
return Ok(notes);
}
[HttpGet("{id}")]
public async Task<ActionResult<Note>> GetById(int id)
{
var note = await _repository.GetByIdAsync(id);
if (note == null)
return NotFound();
return Ok(note);
}
[HttpPost]
public async Task<ActionResult<Note>> Create(Note note)
{
var createdNote = await _repository.CreateAsync(note);
return CreatedAtAction(nameof(GetById), new { id = createdNote.Id }, createdNote);
}
[HttpPut("{id}")]
public async Task<IActionResult> Update(int id, Note note)
{
if (id != note.Id)
return BadRequest();
var updatedNote = await _repository.UpdateAsync(note);
if (updatedNote == null)
return NotFound();
return NoContent();
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id)
{
await _repository.DeleteAsync(id);
return NoContent();
}
}
// В Startup.ConfigureServices
services.AddSingleton<INoteRepository, InMemoryNoteRepository>(); |
|
Такое решение демонстрирует:- Использование принципов SOLID (интерфейс для репозитория, разделение ответственностей).
- Асинхронное программирование (все методы асинхронные).
- Знание REST API и HTTP-глаголов.
- Внедрение зависимостей.
- Корректные возвращаемые статус-коды HTTP.
Код-ревью и рефакторинг
Некоторые компании включают в процесс собеседования задачи на рефакторинг или код-ревью. Вам могут показать фрагмент кода и попросить улучшить его или найти проблемы.
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| // Проблемный код
public class UserManager
{
public List<User> GetAllUsers()
{
var users = new List<User>();
var connection = new SqlConnection("Server=...;Database=...;User=...;Password=...");
connection.Open();
var command = new SqlCommand("SELECT * FROM Users", connection);
var reader = command.ExecuteReader();
while (reader.Read())
{
var user = new User
{
Id = (int)reader["Id"],
Name = (string)reader["Name"],
Email = (string)reader["Email"]
};
users.Add(user);
}
reader.Close();
connection.Close();
return users;
}
public User GetUserById(int id)
{
var connection = new SqlConnection("Server=...;Database=...;User=...;Password=...");
connection.Open();
var command = new SqlCommand($"SELECT * FROM Users WHERE Id = {id}", connection);
var reader = command.ExecuteReader();
User user = null;
if (reader.Read())
{
user = new User
{
Id = (int)reader["Id"],
Name = (string)reader["Name"],
Email = (string)reader["Email"]
};
}
reader.Close();
connection.Close();
return user;
}
} |
|
Проблемы в этом коде:
1. Дублирование кода (подключение к базе, чтение данных).
2. Жёстко заданная строка подключения.
3. Отсутствие обработки ошибок.
4. Ресурсы не освобождаются правильно (нет using или try-finally).
5. SQL-инъекция в методе GetUserById.
6. Класс делает слишком много (нарушение SRP).
Рефакторинг этого кода:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
| public interface IUserRepository
{
Task<IEnumerable<User>> GetAllAsync();
Task<User> GetByIdAsync(int id);
}
public class UserRepository : IUserRepository
{
private readonly string _connectionString;
public UserRepository(IConfiguration configuration)
{
_connectionString = configuration.GetConnectionString("DefaultConnection");
}
public async Task<IEnumerable<User>> GetAllAsync()
{
var users = new List<User>();
using (var connection = new SqlConnection(_connectionString))
{
await connection.OpenAsync();
using var command = new SqlCommand("SELECT Id, Name, Email FROM Users", connection);
using var reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
users.Add(MapUser(reader));
}
}
return users;
}
public async Task<User> GetByIdAsync(int id)
{
using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
using var command = new SqlCommand("SELECT Id, Name, Email FROM Users WHERE Id = @Id", connection);
command.Parameters.AddWithValue("@Id", id);
using var reader = await command.ExecuteReaderAsync();
return await reader.ReadAsync() ? MapUser(reader) : null;
}
private static User MapUser(SqlDataReader reader)
{
return new User
{
Id = reader.GetInt32(reader.GetOrdinal("Id")),
Name = reader.GetString(reader.GetOrdinal("Name")),
Email = reader.GetString(reader.GetOrdinal("Email"))
};
}
} |
|
Преимущества рефакторинга:
1. Следование SRP (класс делает только одно - работает с репозиторием пользователей).
2. Использование интерфейса для возможности тестирования и замены реализации.
3. Получение строки подключения из конфигурации.
4. Правильная работа с ресурсами через using.
5. Асинхронные методы для лучшей масштабируемости.
6. Защита от SQL-инъекций через параметризованный запрос.
7. Вынесение маппинга в отдельный метод для устранения дублирования.
8. Использование более безопасных методов чтения (GetInt32, GetString вместо приведения типов).
Глубокий анализ кода на собеседовании
Иногда на техническом собеседовании вам могут показать фрагмент кода и попросить прокомментировать его или объяснить, что происходит:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| public async Task<IActionResult> GetItems()
{
var items = await _itemsService.GetItemsAsync();
if (items == null)
return NotFound();
var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromMinutes(5))
.SetAbsoluteExpiration(TimeSpan.FromHours(1));
_cache.Set("items", items, cacheEntryOptions);
return Ok(items.Select(i => new ItemDto(i)));
} |
|
Хороший анализ должен включать:
1. Метод асинхронный, что правильно для операций ввода-вывода.
2. Код кэширует результат, что хорошо для производительности.
3. Используются как скользящее, так и абсолютное истечение срока кэша, что является хорошей практикой.
4. Результаты преобразуются в DTO перед возвратом, что обеспечивает разделение между внутренними моделями и API.
5. Есть проверка на null и возврат NotFound, что корректно.
Однако можно отметить и проблемы:
1. Кэш устанавливается каждый раз при вызове, даже если данные уже есть в кэше.
2. Метод не использует контрольный кэш ETag или If-Modified-Since.
3. Ключ кэша "items" слишком общий и может конфликтовать с другими кэшами.
Улучшенная версия:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| public async Task<IActionResult> GetItems()
{
const string cacheKey = "items_list_v1";
if (_cache.TryGetValue(cacheKey, out List<ItemDto> cachedItems))
{
return Ok(cachedItems);
}
var items = await _itemsService.GetItemsAsync();
if (items == null)
return NotFound();
var itemDtos = items.Select(i => new ItemDto(i)).ToList();
var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromMinutes(5))
.SetAbsoluteExpiration(TimeSpan.FromHours(1));
_cache.Set(cacheKey, itemDtos, cacheEntryOptions);
return Ok(itemDtos);
} |
|
Заключение по типичным ошибкам
Подводя итоги, на чём стоит сфокусироваться при подготовке к собеседованию по .NET Core:
1. Углубите знания в основных концепциях — SOLID, DI, асинхронное программирование и т.д. Не просто знайте названия, но умейте объяснить и привести примеры.
2. Практикуйтесь в написании чистого кода — код, который вы пишете на собеседовании, должен быть ясным, поддерживаемым и безопасным.
3. Изучите нюансы .NET Core — понимайте разницу между разными версиями платформы и особенности её архитектуры.
4. Осваивайте смежные технологии — Docker, Kubernetes, CI/CD, облачные платформы — всё это часто спрашивают на собеседованиях.
5. Готовьтесь к задачам на рефакторинг — умение улучшить существующий код высоко ценится в индустрии.
На собеседовании оценивается не только ваше знание фактов, но и способность применять их на практике, а также мыслить критически и находить оптимальные решения проблем. Демонстрация этих качеств отличит вас от других кандидатов и повысит ваши шансы на успешное прохождение собеседования.
Какая разница между ASP .Net Core и ASP .Net Core MVC? Какая разница между ASP .Net Core и ASP .Net Core MVC? Или я может что-то не так понял? И подскажите пожалуйста еще какие-то книги для изучения... Оптимизация производительности C#.NET (Алгоритм, Многопоточность, Debug, Release, .Net Core, Net Native) Решил поделится своим небольшим опытом по оптимизации вычислений на C#.NET.
НЕ профи, палками не кидать, конструктив приветствуется!
Тестом будет... Объясните на пальцах совместимость библиотек в .Net Core, .Net Framework, .Net Standart Изучаю .Net. Хочу написать некое серверное приложение (думаю что учеба лучше на реальном примере, нежели писать примитивные hello world). Выбор... Разница между ASP.NET Core 2, ASP.NET Core MVC, ASP.NET MVC 5 и ASP.NET WEBAPI 2 Здравствуйте. Я в бекенд разработке полный ноль. В чем разница между вышеперечисленными технологиями? Есть ли в них что-то общее - могу ли выучив... Как развернуть бд на другом ПК? ASP.NET Core + MS SQL(EF Core) Суть вот в чем. В ЧТ сдавать тестовое задание на анимации и простейший CRUD с базой данных. Окей. Саму БД я накатил(на metanit хорошие примеры) и... ASP.NET Core + EF Core: ошибка при обновлении БД после создания миграции Всем привет!
Начал осваивать ASP.NET Core: создал проект "Веб-приложение" без Identity.
Сразу же решил подружить его с Entity Framework Core ... Segmentation fault (core dumped) при запуске консольного приложения .NET Core Добрый день!
Собственно, задача: сделать скриншот экрана удаленной машины, работающей под xubuntu.
Написал говнокод (приведен ниже), в... Ошибка при добавлении пакета nuGet Entity Framework Core SQLserver net Core 3.1 Всем привет! Столкнулся с проблемой добавления пакета nuGet Entity Framework Core SQLserver в свой проект WPF (.net Core 3.1). Пытаюсь добавить... ASP.NET Core 3.0 с Entity Framework Core + SQL Привет,
прохожу стажировку в одной компании. Дали вот такое задание, дедлайн отсутствует, поэтому позволил себе обратиться сюда.
Нужно... .net framework и .net core входят в состав .net? Какая там структура(в простом виде)? Пагинация. Как установить колличество позиций на странице? Razor Pages с EF Core в ASP.NET Core Изучаю учебник - Razor Pages с Entity Framework Core в ASP.NET Core // docs.microsoft.com/ru-ru/
Результатом раздела "Добавление на... Asp.net core rc 2 и Entity Framework core Добрый день, кто-нибудь уже перешел на новую версию фреймверка?
Хотелось бы получить пример.
...
|