Старый добрый web.config, похоже, отправился на пенсию вместе с классическим ASP.NET. За годы работы с различными проектами я убедился, что хорошо организованная конфигурация – это половина успеха при разработке крупных приложений. Помню свой первый проект на Core: тогда я часа три не мог понять, почему настройки из JSON-файла никак не хотели подхватыватся. Причина была банальной – забыл добавить провайдер в билдер конфигурации. Сейчас вспоминаю об этом с улыбкой.
В отличие от старого ASP.NET, где мы были привязаны к XML-конфигурации, новая система предлагает нам работу с ключами-значениями из практически любых источников: JSON, XML, переменные окружения, командная строка и даже наши собственные провайдеры. Добавьте сюда строгую типизацию через Options pattern – и получите мощный инструмент для управления любыми настройками от простых строк подключения до сложных иерархических структур.
Основы системы конфигурации
Система конфигурации в ASP.NET Core представляет собой настоящий квантовый скачок по сравнению с прежними подходами. Если раньше мы были привязаны к монолитному web.config, то теперь перед нами гибкая, расширяемая система, построенная на принципах современной архитектуры. Попробуем разобратся в её фундаменте.
Архитектура провайдеров и многоуровневость
В центре системы конфигурации ASP.NET Core лежит понятие провайдера. Провайдер – это компонент, отвечающий за получение конфигурационных данных из определённого источника. Ключевая особенность – возможность использования нескольких провайдеров одновременно, образуя многоуровневую структуру.
C# | 1
2
3
4
5
6
7
| var builder = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables()
.AddCommandLine(args);
Configuration = builder.Build(); |
|
В этом примере мы добавляем сразу четыре провайдера: два для JSON-файлов (базовый и специфичный для окружения), провайдер переменных окружения и провайдер командной строки. Конфигурация выстраивается слоями – последующие провайдеры могут перезаписывать значения, полученные от предыдущих. Я часто сталкивался с ситуацией, когда разработчики забывают об этом свойстве, а потом долго не понимают, почему настройка из JSON-файла не применяется. Оказывается, она перезаписана переменной окружения! Порядок добавления провайдеров имеет критическое значение.
Встроенные источники данных и их приоритет
ASP.NET Core поставляется с набором встроеных провайдеров:- JSON – самый распространённый формат для хранения настроек,
- XML – для обратной совместимости или сложных структурированных данных,
- INI – простой текстовый формат для базовых настроек,
- Переменные окружения – идеальны для конфигурации на уровне хоста,
- Командная строка – удобно для временного переопределения настроек,
- In-memory – для тестирования или динамически генерируемых настроек,
- User Secrets – для хранения чувствительных данных в процессе разработки.
Классическим подходом является использование JSON-файлов как базового уровня, переменных окружения для настроек зависящих от инфраструктуры, и командной строки для особых случаев.
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 Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder();
// Базовый уровень - значения по умолчанию
builder.AddJsonFile("appsettings.json");
// Специфичные для окружения настройки
if (env.IsDevelopment())
{
builder.AddUserSecrets<Startup>();
}
// Настройки инфраструктуры
builder.AddEnvironmentVariables();
// Временные переопределения
if (args != null)
{
builder.AddCommandLine(args);
}
Configuration = builder.Build();
} |
|
Такой подход обеспечивает максимальную гибкость при деплое приложения в различные среды – от локальной разработки до продакшена.
Анатомия конфигурационного ключа
Значения в конфигурации идентифицируются с помощью строковых ключей. Иерархия в плоских источниках (вроде переменных окружения) обеспечивается через разделитель : . Например, в JSON-файле мы имеем:
JSON | 1
2
3
4
5
6
7
8
| {
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning"
}
}
} |
|
Доступ к значению "Warning" осуществляется через ключ Logging:LogLevel:Microsoft .
Интересно, что в переменных окружения двоеточие неудобно, поэтому ASP.NET Core позволяет использовать двойное подчеркивание __ как альтернативу. Настройка ConnectionStrings:DefaultConnection может быть переопределена через переменную окружения ConnectionStrings__DefaultConnection .
Роль IConfiguration в приложении
Центральным компонентом системы является интерфейс IConfiguration . Он представляет собой иммутабельную коллекцию ключей-значений, образующих иерархическую структуру. Основные методы для доступа к настройкам:
C# | 1
2
3
4
5
6
7
8
| // Получение строкового значения по ключу
string value = Configuration["SomeKey"];
// Получение вложеного значения
string nestedValue = Configuration["SomeSection:SomeKey"];
// Получение целой секции
IConfigurationSection section = Configuration.GetSection("SomeSection"); |
|
После сборки конфигурации с помощью builder.Build() , экземпляр IConfiguration обычно регистрируется в контейнере зависимостей:
C# | 1
| services.AddSingleton<IConfiguration>(Configuration); |
|
Это позволяет инжектировать конфигурацию в любой компонент приложения:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| public class SomeService
{
private readonly IConfiguration _configuration;
public SomeService(IConfiguration configuration)
{
_configuration = configuration;
}
public void DoSomething()
{
var apiKey = _configuration["ApiKey"];
// Используем apiKey...
}
} |
|
Однако простое чтение строковых значений из IConfiguration имеет ряд недостатков:
1. Отсутствие типизации – вы получаете строки и должны сами преобразовывать их в нужные типы.
2. Нет проверки наличия ключей – вы можете запросить несуществующий ключ и получить null.
3. Строковые литералы ключей по всему коду – это потенциальные ошибки при рефакторинге.
Именно поэтому в ASP.NET Core появился паттерн Options, но о нём я расскажу в следующем разделе.
Внедрение зависимостей и жизненный цикл
ASP.NET Core изначально спроектирован с учётом принципов Dependency Injection. Система конфигурации тесно интегрирована с встроенным DI-контейнером. По умолчанию, экземпляр IConfiguration регистрируется как синглтон – один экземпляр на всё время жизни приложения. Это означает, что после инициализации приложения сама конфигурация обычно не меняется.
Однако стоит отметить важную деталь: хотя сам объект IConfiguration регистрируется как синглтон, значения внутри него могут меняться во время работы приложения. Некоторые провайдеры поддерживают динамическое обновление настроек. Например, если изменить файл JSON-конфигурации и провайдер настроен отслеживать изменения, то новые значения станут доступны через тот же интерфейс IConfiguration .
C# | 1
| .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) |
|
Параметр reloadOnChange: true указывает провайдеру отслеживать изменения в файле и обновлять значения в конфигурации. Это может быть полезно при разработке, но нужно аккуратно применять в продакшн-среде, особенно в высоконагруженых системах.
Однако тут кроется подводный камень. Представьте, что вы инжектировали строковое значение из конфигурации в свой сервис:
C# | 1
2
3
4
5
6
7
8
9
| public class ApiClient
{
private readonly string _apiKey;
public ApiClient(IConfiguration config)
{
_apiKey = config["Services:ExternalApi:ApiKey"];
}
} |
|
В этом случае, даже если конфигурация обновится, значение _apiKey останется прежним! Сервис прочитал значение один раз при создании, и больше к конфигурации не обращается. Для решения этой проблемы либо нужно каждый раз обращаться к конфигурации напрямую, либо использовать паттерн Options с IOptionsSnapshot или IOptionsMonitor , о которых речь пойдет в следующих разделах.
Работа с иерархическими структурами
Настройки приложения редко бывают плоскими – чаще они представляют собой иерархические структуры. В ASP.NET Core есть удобные способы работы с такими структурами:
C# | 1
2
3
4
5
6
7
8
9
| // Получение всей секции целиком
var loggingSection = Configuration.GetSection("Logging");
// Навигация по иерархии
var microsoftLogLevel = loggingSection.GetSection("LogLevel").GetValue<string>("Microsoft");
// Привязка секции к объекту
var loggingOptions = new LoggingOptions();
loggingSection.Bind(loggingOptions); |
|
Метод GetSection возвращает объект типа IConfigurationSection , который сам является реализацией IConfiguration . Это позволяет строить цепочки вызовов для навигации по вложеным секциям.
Пример работы с In-Memory провайдером
Для тестирования или динамического создания конфигурации можно использовать In-Memory провайдер:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
| var memoryConfig = new Dictionary<string, string>
{
{"Profile:FirstName", "Иван"},
{"Profile:LastName", "Петров"},
{"Profile:Designation", "Разработчик"}
};
var builder = new ConfigurationBuilder();
builder.AddInMemoryCollection(memoryConfig);
var configuration = builder.Build();
// Теперь можно работать с конфигурацией как обычно
var firstName = configuration["Profile:FirstName"]; // "Иван" |
|
Этот подход особенно полезен в юнит-тестах, когда нужно изолировать тестируемый код от внешних файлов конфигурации. Я часто использую его, чтобы быстро подготовить нужные настройки для тестирования различных компонентов.
Конфигурирование в ASP.NET Core 6+
С выходом .NET 6 подход к конфигурированию немного изменился. Теперь вместо класса Startup используется минимальный API в файле Program.cs :
C# | 1
2
3
4
5
6
7
8
9
10
| var builder = WebApplication.CreateBuilder(args);
// Конфигурация уже настроена с базовыми провайдерами
// Можно добавить дополнительные источники
builder.Configuration.AddJsonFile("customSettings.json", optional: true);
// Доступ к конфигурации
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
var app = builder.Build(); |
|
В таком подходе базовая конфигурация уже настроена с типичными провайдерами (JSON, переменные окружения и т.д.), и нам остается только расширить её при необходимости. Это упрощает стандартные сценарии, но принципы работы остаются теми же.
Разница между ASP.NET Core 2, ASP.NET Core MVC, ASP.NET MVC 5 и ASP.NET WEBAPI 2 Здравствуйте. Я в бекенд разработке полный ноль. В чем разница между вышеперечисленными... ASP.NET Core. Старт - что нужно знать, чтобы стать ASP.NET Core разработчиком? Попалось хор краткое обзорное видео 2016 года с таким названием - Что нужно знать, чтобы стать... Какая разница между ASP .Net Core и ASP .Net Core MVC? Какая разница между ASP .Net Core и ASP .Net Core MVC? Или я может что-то не так понял? И... ASP.NET MVC 4,ASP.NET MVC 4.5 и ASP.NET MVC 5 большая ли разница между ними? Начал во всю осваивать технологию,теперь хочу с книжкой посидеть и вдумчиво перебрать всё то что...
Практические техники работы
За пять лет использования ASP.NET Core в промышленной разработке я выработал несколько подходов, которые значительно упрощают работу с конфигурацией. Поделюсь самыми полезными из них.
Биндинг настроек к классам с сильной типизацией
Пожалуй, главное преимущество новой системы конфигурации – возможность привязать целые секции настроек к классам с сильной типизацией. Этот подход, известный как паттерн Options, избавляет от хрупких строковых ключей и ручных преобразований типов. Для начала определим класс, соответствующий структуре настроек:
C# | 1
2
3
4
5
6
7
8
| public class SmtpSettings
{
public string Server { get; set; }
public int Port { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
public bool EnableSsl { get; set; }
} |
|
Затем в JSON-файле создадим соответствующую секцию:
JSON | 1
2
3
4
5
6
7
8
9
| {
"SmtpSettings": {
"Server": "smtp.example.com",
"Port": 587,
"UserName": "user@example.com",
"Password": "P@ssw0rd",
"EnableSsl": true
}
} |
|
Теперь регистрируем эти настройки в DI-контейнере:
C# | 1
| services.Configure<SmtpSettings>(Configuration.GetSection("SmtpSettings")); |
|
И используем через инъекцию зависимостей:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| public class EmailService
{
private readonly SmtpSettings _smtpSettings;
public EmailService(IOptions<SmtpSettings> smtpSettingsOptions)
{
_smtpSettings = smtpSettingsOptions.Value;
}
public void SendEmail(string to, string subject, string body)
{
// Используем _smtpSettings.Server, _smtpSettings.Port и т.д.
}
} |
|
Что тут происходит? Метод Configure<T> связывает секцию конфигурации с типом T и регистрирует соответствующие сервисы в контейнере. Когда мы запрашиваем IOptions<SmtpSettings> , контейнер предоставляет нам обёртку над настроенным экземпляром SmtpSettings . На практике я столкнулся с интересным случаем: у нас было приложение с 20+ разными email-шаблонами, каждый со своими настройками. Мы создали базовый класс EmailTemplateSettings и серию наследников, зарегистрировав их в соответствующих секциях. Контейнер без проблем различал все эти типы, избавив нас от сотен строк бойлерплейта.
Валидация конфигурационных параметров
Ошибки в конфигурации могут привести к странным падениям в самый неподходящий момент. ASP.NET Core позволяет валидировать настройки сразу при запуске:
C# | 1
2
3
4
5
6
7
8
| services.AddOptions<SmtpSettings>()
.Bind(Configuration.GetSection("SmtpSettings"))
.Validate(settings =>
{
return !string.IsNullOrEmpty(settings.Server)
&& settings.Port > 0
&& settings.Port < 65536;
}, "SMTP server and port must be specified correctly."); |
|
Если валидация не пройдена, приложение выбросит исключение при первом обращении к настройкам. Это гарантирует, что проблемы будут выявлены на раннем этапе.
Но что если мы хотим более сложную валидацию? Для этого можно использовать DataAnnotations:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| public class SmtpSettings
{
[Required]
public string Server { get; set; }
[Range(1, 65535)]
public int Port { get; set; }
[EmailAddress]
public string UserName { get; set; }
[MinLength(8)]
public string Password { get; set; }
public bool EnableSsl { get; set; } = true;
}
// Регистрация с валидацией через DataAnnotations
services.AddOptions<SmtpSettings>()
.Bind(Configuration.GetSection("SmtpSettings"))
.ValidateDataAnnotations(); |
|
Динамическое обновление настроек
Представьте ситуацию: вам нужно изменить настройки приложения без его перезапуска. Например, увеличить уровень логирования для диагностики проблемы в продакшене. Тут на помощь приходят интерфейсы IOptionsSnapshot и IOptionsMonitor .
IOptionsSnapshot - это скоупированая версия IOptions, создающая новый экземпляр настроек для каждого запроса:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| public class LoggingController : Controller
{
private readonly IOptionsSnapshot<LoggingSettings> _loggingSettings;
public LoggingController(IOptionsSnapshot<LoggingSettings> loggingSettings)
{
_loggingSettings = loggingSettings;
}
public IActionResult Index()
{
// Всегда получаем актуальные настройки
var currentLogLevel = _loggingSettings.Value.DefaultLogLevel;
return View(new LoggingViewModel { CurrentLogLevel = currentLogLevel });
}
} |
|
Каждый новый HTTP-запрос получит свежую версию настроек, если они изменились на диске.
А IOptionsMonitor работает как синглтон, но позволяет подписаться на уведомления об изменениях:
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
| public class ConfigAwareService
{
private LoggingSettings _currentSettings;
private readonly IDisposable _changeListener;
public ConfigAwareService(IOptionsMonitor<LoggingSettings> settingsMonitor)
{
_currentSettings = settingsMonitor.CurrentValue;
// Подписка на изменения
_changeListener = settingsMonitor.OnChange(newSettings =>
{
Console.WriteLine($"Настройки изменились! Новый уровень логирования: {newSettings.DefaultLogLevel}");
_currentSettings = newSettings;
ApplyNewSettings(newSettings);
});
}
private void ApplyNewSettings(LoggingSettings settings)
{
// Применяем новые настройки
}
public void Dispose()
{
_changeListener?.Dispose();
}
} |
|
На одном из проектов мы использовали этот механизм для горячего изменения лимитов на количество запросов к API. Администратор менял значение в конфигурационном файле, и оно тут же применялось без перезапуска сервиса – удобство, о котором можно только мечтать в классическом ASP.NET.
Работа с конфигурацией в Middleware
Иногда требуется доступ к конфигурации прямо в pipeline обработки запроса. Middleware-компоненты также могут использовать преимущества DI для получения настроек:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| public class CustomHeaderMiddleware
{
private readonly RequestDelegate _next;
private readonly CustomHeaderSettings _settings;
public CustomHeaderMiddleware(RequestDelegate next, IOptions<CustomHeaderSettings> settings)
{
_next = next;
_settings = settings.Value;
}
public async Task InvokeAsync(HttpContext context)
{
// Добавляем заголовок из конфигурации
context.Response.Headers.Add(_settings.HeaderName, _settings.HeaderValue);
// Передаем управление следующему компоненту
await _next(context);
}
}
// Регистрация в pipeline
app.UseMiddleware<CustomHeaderMiddleware>(); |
|
Я однажды создал middleware, который добавлял специальные заголовки безопасности на основе конфигурации для разных окружений. В разработке – минимальный набор, а в продакшене – все необходимые заголовки с подходящими значениями. Настройки легко меняли безопасностники, не трогая код.
Фильтры действий и конфигурация
Аналогичным образом можно использовать конфигурацию в фильтрах действий MVC:
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
| public class ApiKeyAuthFilter : IAuthorizationFilter
{
private readonly string _validApiKey;
public ApiKeyAuthFilter(IOptions<SecuritySettings> securitySettings)
{
_validApiKey = securitySettings.Value.ApiKey;
}
public void OnAuthorization(AuthorizationFilterContext context)
{
if (!context.HttpContext.Request.Headers.TryGetValue("X-API-KEY", out var apiKeyValues))
{
context.Result = new UnauthorizedResult();
return;
}
var providedApiKey = apiKeyValues.FirstOrDefault();
if (providedApiKey != _validApiKey)
{
context.Result = new UnauthorizedResult();
}
}
}
// Регистрация фильтра глобально
services.AddMvc(options =>
{
options.Filters.Add<ApiKeyAuthFilter>();
}); |
|
Такой подход хорошо работает для простых сценариев авторизации по API-ключу, когда не требуется полноценная аутентификация через Identity.
Конфигурирование логирования
Настройка логирования – классический сценарий использования конфигурации. ASP.NET Core делает это особенно просто:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureLogging((hostContext, logging) =>
{
logging.ClearProviders();
logging.AddConfiguration(hostContext.Configuration.GetSection("Logging"));
logging.AddConsole();
logging.AddDebug();
logging.AddEventSourceLogger();
// Условное добавление провайдеров
if (hostContext.HostingEnvironment.IsProduction())
{
logging.AddEventLog();
}
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
}); |
|
Соответствующая конфигурация в JSON:
JSON | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| {
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
},
"Console": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting": "Warning"
}
}
}
} |
|
В этом примере у нас разные уровни логгирования для разных категорий и провайдеров. Например, всё, что связано с Microsoft, логируется на уровне Warning и выше, кроме Microsoft.Hosting.Lifetime , для которого установлен уровень Information. Интересный нюанс: для разных провайдеров логирования можно указывать разные настройки в одном файле конфигурации. Это значительно упрощает тонкую настройку логгирования в разных средах.
Мониторинг изменений конфигурации
Помимо IOptionsMonitor существует еще один механизм для отслеживания изменений конфигурации - ChangeToken. Это более низкоуровневый API, который позволяет реагировать на изменения в определенном разделе конфигурации.
C# | 1
2
3
4
5
6
| IChangeToken token = Configuration.GetReloadToken();
token.RegisterChangeCallback(state =>
{
Console.WriteLine("Конфигурация изменилась!");
// Перечитать конфигурацию или выполнить нужные действия
}, null); |
|
Я однажды создавал систему динамических правил маршрутизации, которые хранились в конфигурации. При изменении файла правил нужно было пересобрать всю таблицу маршрутов без перезапуска сервера. ChangeToken отлично справился с этой задачей, позволив реактивно обновлять правила. Важный нюанс: колбэк срабатывает только один раз. Если вам нужна постояная подписка, придется внутри колбэка регистрировать новый:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
| void RegisterChangeCallback()
{
ChangeToken.OnChange(
() => Configuration.GetReloadToken(),
() =>
{
Console.WriteLine($"Конфигурация обновилась в {DateTime.Now}");
// Применить изменения
// Не нужно явно регистрироваться снова - OnChange делает это автоматически
});
} |
|
Типизированный доступ к настройкам из контроллеров
Хотя я уже упоминал использование IOptions в контроллерах, есть несколько дополнительных паттернов, которые стоит рассмотреть:
1. Паттерн с использованием IOptionsSnapshot для обновления настроек:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| public class FeatureFlagsController : Controller
{
private readonly IOptionsSnapshot<FeatureFlags> _featureFlags;
public FeatureFlagsController(IOptionsSnapshot<FeatureFlags> featureFlags)
{
_featureFlags = featureFlags;
}
public IActionResult Index()
{
// Текущее состояние фичефлагов на момент запроса
return View(_featureFlags.Value);
}
} |
|
2. Паттерн с именоваными опциями:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // Регистрация
services.Configure<ConnectionString>(
"ReadOnly",
Configuration.GetSection("ConnectionStrings:ReadOnlyDb"));
services.Configure<ConnectionString>(
"ReadWrite",
Configuration.GetSection("ConnectionStrings:ReadWriteDb"));
// Использование в контроллере
public class DbController : Controller
{
private readonly ConnectionString _readOnlyConnection;
private readonly ConnectionString _readWriteConnection;
public DbController(IOptionsSnapshot<ConnectionString> connectionFactory)
{
_readOnlyConnection = connectionFactory.Get("ReadOnly");
_readWriteConnection = connectionFactory.Get("ReadWrite");
}
} |
|
Это очень удобно когда нужно инжектировать несколько экземпляров настроек одного типа, но с разными значениями.
Конфигурирование через фабрику опций
Иногда требуется не просто считать настройки из источника, но и как-то трансформировать их перед использованием. Например, расшифровать пароли или выполнить постобработку данных. Для этого существует механизм фабрики опций:
C# | 1
2
3
4
5
6
7
8
9
10
11
| services.AddOptions<ApiClientSettings>()
.Bind(Configuration.GetSection("ApiClient"))
.PostConfigure(settings =>
{
// Расшифровываем зашифрованный API-ключ
if (!string.IsNullOrEmpty(settings.EncryptedApiKey))
{
settings.ApiKey = _decryptionService.Decrypt(settings.EncryptedApiKey);
settings.EncryptedApiKey = null; // Очищаем зашифрованное значение из памяти
}
}); |
|
Метод PostConfigure позволяет модифицировать опции после их биндинга из конфигурации. Это мощный механизм для предварительной подготовки настроек перед использованием. На практике я применял этот подход для автоматического добавления URL-префиксов к относительным путям в настройках. Конфигурация хранила только относительные пути /api/v1/users , а PostConfigure добавлял базовый URL в зависимости от окружения.
Конфигурирование по-разному для разных окружений
ASP.NET Core позволяет настраивать поведение приложения для разных сред (Development, Staging, Production):
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
| public void ConfigureServices(IServiceCollection services)
{
// Базовая конфигурация для всех окружений
services.Configure<EmailSenderOptions>(Configuration.GetSection("Email"));
// Переопределения для разработки
if (_environment.IsDevelopment())
{
services.PostConfigure<EmailSenderOptions>(options =>
{
// В разработке все письма отправляем на тестовый адрес
options.OverrideRecipient = "dev-test@example.com";
options.DisableActualSending = true;
});
}
// Настройки для продакшена
if (_environment.IsProduction())
{
services.PostConfigure<EmailSenderOptions>(options =>
{
// В продакшене обязательно шифруем
options.EnableEncryption = true;
});
}
} |
|
Это избавляет от необходимости дублировать всю конфигурацию для разных окружений - нужно описать только отличия.
Комплексный пример: система уведомлений
Давайте соберем всё вместе на примере сервиса уведомлений, который может отправлять сообщения через email, SMS или push-уведомления в зависимости от настроек:
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
| // Модели настроек
public class NotificationSettings
{
public EmailSettings Email { get; set; }
public SmsSettings Sms { get; set; }
public PushSettings Push { get; set; }
public bool EnableEmails { get; set; } = true;
public bool EnableSms { get; set; } = false;
public bool EnablePush { get; set; } = false;
}
public class EmailSettings
{
[Required]
public string SmtpServer { get; set; }
[Range(1, 65535)]
public int Port { get; set; } = 25;
public string Username { get; set; }
public string Password { get; set; }
public bool UseSsl { get; set; } = true;
}
// Регистрация в DI
services.Configure<NotificationSettings>(
Configuration.GetSection("Notifications"));
// Сервис уведомлений
public class NotificationService
{
private readonly IOptionsMonitor<NotificationSettings> _settings;
private readonly ILogger<NotificationService> _logger;
public NotificationService(
IOptionsMonitor<NotificationSettings> settings,
ILogger<NotificationService> logger)
{
_settings = settings;
_logger = logger;
// Подписываемся на изменения настроек
_settings.OnChange(newSettings => {
_logger.LogInformation("Настройки уведомлений обновлены");
// Можно перенастроить клиенты, сбросить кеши и т.д.
});
}
public async Task SendNotificationAsync(string userId, string message)
{
// Получаем текущие настройки
var settings = _settings.CurrentValue;
if (settings.EnableEmails)
{
await SendEmailAsync(userId, message);
}
if (settings.EnableSms)
{
await SendSmsAsync(userId, message);
}
if (settings.EnablePush)
{
await SendPushAsync(userId, message);
}
}
private Task SendEmailAsync(string userId, string message)
{
var emailSettings = _settings.CurrentValue.Email;
// Логика отправки email
return Task.CompletedTask;
}
// Аналогичные методы для SMS и Push
} |
|
В такой реализации наш сервис уведомлений:
1. Автоматически реагирует на изменения в конфигурации.
2. Использует строгую типизацию для всех настроек.
3. Имеет четкую структуру настроек, соответствующую бизнес-доменам.
4. Логирует важные изменения для диагностики.
На одном из проектов у нас была похожая система, и маркетологи очень ценили возможность включать и отключать каналы уведомлений без перезапуска системы. Например, перед крупной рассылкой они временно отключали SMS-канал из-за его дороговизны и использовали только email.
Работа с конфигурацией в сервисах приложения
Сервисный слой часто нуждается в доступе к настройкам, но постоянные инъекции IOptions<T> в каждый сервис могут привести к беспорядку в коде. В некоторых случаях лучше создать специальный сервис-обёртку, который будет инкапсулировать доступ к настройкам:
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 interface ISettingsProvider
{
T GetSettings<T>() where T : class, new();
string GetConnectionString(string name);
string GetValue(string key);
}
public class SettingsProvider : ISettingsProvider
{
private readonly IConfiguration _configuration;
public SettingsProvider(IConfiguration configuration)
{
_configuration = configuration;
}
public T GetSettings<T>() where T : class, new()
{
var result = new T();
var sectionName = typeof(T).Name.Replace("Settings", "");
_configuration.GetSection(sectionName).Bind(result);
return result;
}
public string GetConnectionString(string name)
{
return _configuration.GetConnectionString(name);
}
public string GetValue(string key)
{
return _configuration[key];
}
} |
|
Такой подход не только упрощает работу с конфигурацией, но и позволяет легко заменить реализацию для тестирования. В моей практике этот паттерн значительно упростил юнит-тесты бизнес-логики, зависящей от настроек.
Feature Flags через конфигурацию
Флаги функциональности (feature flags) - мощный подход к развертыванию новых возможностей. 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
29
30
31
32
33
34
35
36
37
38
39
| public class FeatureFlags
{
public bool EnableNewCheckout { get; set; }
public bool UseNewSearchAlgorithm { get; set; }
public Dictionary<string, bool> BetaFeatures { get; set; } = new();
}
// В JSON
{
"Features": {
"EnableNewCheckout": false,
"UseNewSearchAlgorithm": true,
"BetaFeatures": {
"RedesignedProfile": true,
"AIRecommendations": false
}
}
}
// В контроллере
public class CheckoutController : Controller
{
private readonly IOptionsSnapshot<FeatureFlags> _featureFlags;
public CheckoutController(IOptionsSnapshot<FeatureFlags> featureFlags)
{
_featureFlags = featureFlags;
}
public IActionResult Index()
{
if (_featureFlags.Value.EnableNewCheckout)
{
return View("NewCheckout");
}
return View("Checkout");
}
} |
|
Комбинируя это с динамическим обновлением настроек, можно реализовать "тумблеры" для функций, которые можно включать и выключать "на лету" без перезапуска приложения.
Работа с массивами и коллекциями
Конфигурация в ASP.NET Core отлично справляется с массивами и коллекциями:
JSON | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| {
"AllowedOrigins": [
"https://example.com",
"https://api.example.com",
"https://admin.example.com"
],
"RateLimiting": {
"Rules": [
{
"Endpoint": "/api/public/*",
"Limit": 100,
"Period": "1m"
},
{
"Endpoint": "/api/admin/*",
"Limit": 20,
"Period": "1m"
}
]
}
} |
|
Такую структуру можно привязать к классам:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| public class CorsSettings
{
public string[] AllowedOrigins { get; set; }
}
public class RateLimitSettings
{
public List<RateLimitRule> Rules { get; set; }
}
public class RateLimitRule
{
public string Endpoint { get; set; }
public int Limit { get; set; }
public string Period { get; set; }
} |
|
В реальных проектах мне приходилось хранить в конфигурации целые таблицы маршрутизации с десятками правил. Система справлялась с этим без проблем, хотя для очень больших наборов данных стоит задуматься об альтернативных источниках, например, базе данных.
Конфигурирование кросс-культурных настроек
При разработке многоязычных приложений конфигурация может помочь в управлении культурными настройками:
JSON | 1
2
3
4
5
6
7
8
| {
"Localization": {
"SupportedCultures": ["en-US", "ru-RU", "es-ES", "de-DE"],
"DefaultCulture": "en-US",
"ResourcesPath": "Resources",
"FallbackToParentCultures": true
}
} |
|
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| public void ConfigureServices(IServiceCollection services)
{
var locSettings = new LocalizationSettings();
Configuration.GetSection("Localization").Bind(locSettings);
services.AddLocalization(options => options.ResourcesPath = locSettings.ResourcesPath);
services.Configure<RequestLocalizationOptions>(options =>
{
var cultures = locSettings.SupportedCultures
.Select(c => new CultureInfo(c))
.ToList();
options.DefaultRequestCulture = new RequestCulture(locSettings.DefaultCulture);
options.SupportedCultures = cultures;
options.SupportedUICultures = cultures;
options.FallBackToParentCultures = locSettings.FallbackToParentCultures;
});
} |
|
На практике мы создавали приложения с поддержкой до 20 языков, причем для некоторых регионов требовалась своя специфичная бизнес-логика. Вся эта конфигурация хранилась в JSON, что позволяло легко добавлять новые языки без изменения кода.
Защита чувствительных данных
Конфигурация часто содержит чувствительную информацию: пароли, ключи API, секреты. 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
| public void ConfigureServices(IServiceCollection services)
{
// Добавляем провайдер для зашифрованных секций
services.AddDataProtection();
var connectionStrings = Configuration
.GetSection("ConnectionStrings")
.Get<Dictionary<string, string>>();
// Расшифровываем защищенные строки
foreach (var key in connectionStrings.Keys.ToList())
{
if (connectionStrings[key].StartsWith("enc:"))
{
var protector = _dataProtectionProvider
.CreateProtector("ConnectionStrings");
var encryptedValue = connectionStrings[key].Substring(4);
connectionStrings[key] = protector.Unprotect(encryptedValue);
}
}
// Регистрируем расшифрованные значения
services.AddSingleton(connectionStrings);
} |
|
Продвинутые сценарии использования
Стандартные методы конфигурации хороши для большинства приложений, но когда мы говорим о крупных распределенных системах или особо чувствительных данных, потребуются более продвинутые техники. За годы работы с ASP.NET Core я нашел несколько нетривиальных подходов, которые выручали в сложных ситуациях.
Интеграция с Azure Key Vault
Если ваше приложение работает в Azure, Key Vault становится незаменимым инструментом для хранения секретов. Интеграция с 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
| public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, config) =>
{
var builtConfig = config.Build();
var keyVaultEndpoint = builtConfig["AzureKeyVault:Endpoint"];
if (!string.IsNullOrEmpty(keyVaultEndpoint))
{
var azureServiceTokenProvider = new AzureServiceTokenProvider();
var keyVaultClient = new KeyVaultClient(
new KeyVaultClient.AuthenticationCallback(
azureServiceTokenProvider.KeyVaultTokenCallback));
config.AddAzureKeyVault(
keyVaultEndpoint,
keyVaultClient,
new DefaultKeyVaultSecretManager());
}
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
}); |
|
Главная фишка в том, что Key Vault становится еще одним провайдером конфигурации, причем с самым высоким приоритетом. Это означает, что любые секреты из Key Vault переопределят значения из других источников – идеально для паролей и ключей API. На своем последнем проекте я оценил еще одно преимущество: возможность настроить разграниченый доступ к секретам. Разработчики могли видеть и изменять секреты для тестовых сред, а для продакшена такой доступ был только у DevOps-инженеров и архитекторов.
Создание кастомного провайдера конфигурации
Иногда стандартные провайдеры не подходят под специфические требования. В таких случаях можно создать свой провайдер конфигурации. Я однажды столкнулся с системой, где часть настроек хранилась в 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
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
| public class RedisConfigurationProvider : ConfigurationProvider, IDisposable
{
private readonly string _redisConnectionString;
private readonly string _keyPrefix;
private ConnectionMultiplexer _redis;
private ISubscriber _subscriber;
public RedisConfigurationProvider(RedisConfigurationSource source)
{
_redisConnectionString = source.ConnectionString;
_keyPrefix = source.KeyPrefix;
}
public override void Load()
{
_redis = ConnectionMultiplexer.Connect(_redisConnectionString);
var db = _redis.GetDatabase();
// Загрузка всех ключей с префиксом
var server = _redis.GetServer(_redis.GetEndPoints().First());
var keys = server.Keys(pattern: $"{_keyPrefix}*");
var data = new Dictionary<string, string>();
foreach (var key in keys)
{
var configKey = key.ToString().Substring(_keyPrefix.Length);
var value = db.StringGet(key);
data[configKey] = value;
}
Data = data;
// Подписка на изменения
_subscriber = _redis.GetSubscriber();
_subscriber.Subscribe($"{_keyPrefix}-changes", (channel, message) =>
{
// При получении уведомления перезагружаем конфигурацию
Load();
OnReload();
});
}
public void Dispose()
{
_redis?.Dispose();
}
}
public class RedisConfigurationSource : IConfigurationSource
{
public string ConnectionString { get; set; }
public string KeyPrefix { get; set; } = "app:config:";
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new RedisConfigurationProvider(this);
}
}
// Расширение для удобства использования
public static class RedisConfigurationExtensions
{
public static IConfigurationBuilder AddRedis(
this IConfigurationBuilder builder,
string connectionString,
string keyPrefix = "app:config:")
{
return builder.Add(new RedisConfigurationSource
{
ConnectionString = connectionString,
KeyPrefix = keyPrefix
});
}
} |
|
Теперь мы можем использовать его вместе с другими провайдерами:
C# | 1
2
3
4
| var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddRedis(redisConnectionString)
.Build(); |
|
Преимущество такого подхода в том, что администраторы могут менять настройки в Redis, и они будут моментально применятся во всех экземплярах приложения. Особенно полезно в микросервисной архитектуре, где у вас может быть несколько экземпляров одного сервиса.
Работа с конфигурацией через Consul
Consul – популярная система обнаружения сервисов, которая также может служить хранилищем ключей-значений. Интеграция с ASP.NET Core требует создания кастомного провайдера, аналогичного 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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
| public class ConsulConfigurationProvider : ConfigurationProvider, IDisposable
{
private readonly ConsulClient _client;
private readonly string _keyPrefix;
private readonly int _pollingInterval;
private Timer _pollingTimer;
public ConsulConfigurationProvider(string consulAddress, string keyPrefix, int pollingIntervalMs)
{
_client = new ConsulClient(config =>
{
config.Address = new Uri(consulAddress);
});
_keyPrefix = keyPrefix;
_pollingInterval = pollingIntervalMs;
}
public override void Load()
{
var result = _client.KV.List(_keyPrefix).Result;
if (result.StatusCode == HttpStatusCode.OK)
{
var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var pair in result.Response)
{
var key = pair.Key.Substring(_keyPrefix.Length);
var value = Encoding.UTF8.GetString(pair.Value);
data[key] = value;
}
Data = data;
}
// Запускаем периодическое обновление
if (_pollingTimer == null)
{
_pollingTimer = new Timer(_ =>
{
Load();
OnReload();
}, null, _pollingInterval, _pollingInterval);
}
}
public void Dispose()
{
_pollingTimer?.Dispose();
_client?.Dispose();
}
}
// Использование при инициализации
config.AddConsulConfiguration(
"http://localhost:8500",
"myapp/settings/",
pollingIntervalMs: 30000); |
|
На одном из моих проектов мы использовали Consul не только для хранения настроек, но и для service discovery. Это оказалось особено удобно в контейнерной среде: мы точно знали, где находится каждый экземпляр сервиса, и могли динамически настраивать их через единый интерфейс Consul.
Секреты приложения и переменные окружения
Для локальной разработки ASP.NET Core предлагает механизм User Secrets. Это позволяет хранить чувствительные данные вне репозитория:
PowerShell | 1
2
3
| # В терминале в корневой папке проекта
dotnet user-secrets init
dotnet user-secrets set "PaymentGateway:ApiKey" "sk_test_abcdef123456" |
|
Внутренне это просто JSON-файл, который хранится в специальной папке пользователя:
Windows: %APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json ,
macOS/Linux: ~/.microsoft/usersecrets/<user_secrets_id>/secrets.json .
А вот в продакшене лучше использовать переменные окружения, особено в контейнерах:
Windows Batch file | 1
2
3
4
5
| FROM mcr.microsoft.com/dotnet/aspnet:6.0
WORKDIR /app
COPY --from=build /app/out .
ENV ConnectionStrings__DefaultConnection="Server=prod-db;Database=MyApp;User=app;Password=secret123"
ENTRYPOINT ["dotnet", "MyApp.dll"] |
|
Интересный нюанс: в переменных окружения двоеточие (разделитель иерархии в ключах конфигурации) заменяется на двойное подчёркивание. Это связано с ограничениями синтаксиса переменных окружения в разных операционных системах.
Шифрование конфиденциальных данных в файлах конфигурации
Если вам всё же нужно хранить секреты в конфигурационных файлах, 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
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
| public void ConfigureServices(IServiceCollection services)
{
// Настраиваем защиту данных
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(@"C:\keys"))
.SetDefaultKeyLifetime(TimeSpan.FromDays(14));
// Получаем протектор с определенным назначением
var provider = services.BuildServiceProvider();
var dataProtectionProvider = provider.GetRequiredService<IDataProtectionProvider>();
var protector = dataProtectionProvider.CreateProtector("Configuration.ConnectionStrings");
// Зашифровываем строку подключения
var rawConnectionString = Configuration.GetConnectionString("DefaultConnection");
var encryptedConnectionString = protector.Protect(rawConnectionString);
// Регистрируем зашифрованную версию
var inMemoryConfig = new Dictionary<string, string>
{
{"ConnectionStrings:DefaultConnection", encryptedConnectionString}
};
var memoryConfigSource = new MemoryConfigurationSource { InitialData = inMemoryConfig };
Configuration.Add(memoryConfigSource);
// А вот и сервис, который умеет расшифровывать
services.AddTransient<IConnectionStringProvider, ProtectedConnectionStringProvider>();
}
public class ProtectedConnectionStringProvider : IConnectionStringProvider
{
private readonly IDataProtector _protector;
private readonly IConfiguration _configuration;
public ProtectedConnectionStringProvider(
IDataProtectionProvider dataProtectionProvider,
IConfiguration configuration)
{
_protector = dataProtectionProvider.CreateProtector("Configuration.ConnectionStrings");
_configuration = configuration;
}
public string GetConnectionString(string name)
{
var encryptedConnectionString = _configuration.GetConnectionString(name);
if (encryptedConnectionString.StartsWith("ENC:"))
{
// Удаляем префикс и расшифровываем
return _protector.Unprotect(encryptedConnectionString.Substring(4));
}
return encryptedConnectionString;
}
} |
|
Помню забавный случай: на одном проекте мы шифровали все секреты в конфигурации, но забыли добавить соответствующий секретный ключ на продакшн-сервер. В итоге приложение не могло расшифровать настройки и падало при старте. Урок: всегда тщательно тестируйте механизмы шифрования в среде, максимально близкой к продакшену!
Конфигурация через базы данных и Entity Framework
Для динамичных настроек, которые часто меняются или имеют сложную структуру, база данных может быть более подходящим хранилищем, чем файлы:
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
| public class DbConfigurationProvider : ConfigurationProvider, IDisposable
{
private readonly string _connectionString;
private readonly TimeSpan _refreshInterval;
private Timer _timer;
public DbConfigurationProvider(string connectionString, TimeSpan refreshInterval)
{
_connectionString = connectionString;
_refreshInterval = refreshInterval;
}
public override void Load()
{
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
using (var command = new SqlCommand("SELECT [Key], [Value] FROM [AppSettings]", connection))
using (var reader = command.ExecuteReader())
{
var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
while (reader.Read())
{
data[reader.GetString(0)] = reader.GetString(1);
}
Data = data;
}
}
if (_timer == null)
{
_timer = new Timer(_ =>
{
Load();
OnReload();
}, null, _refreshInterval, _refreshInterval);
}
}
public void Dispose()
{
_timer?.Dispose();
}
}
// Расширение для конфигурации
public static IConfigurationBuilder AddDbConfiguration(
this IConfigurationBuilder builder,
string connectionString,
TimeSpan refreshInterval)
{
return builder.Add(new DbConfigurationSource(connectionString, refreshInterval));
}
// Регистрация провайдера
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddDbConfiguration(
connectionString,
TimeSpan.FromMinutes(5))
.Build(); |
|
Этот провайдер каждые 5 минут опрашивает базу данных на предмет изменений и обновляет конфигурацию. Конечно, в реальном приложении стоит добавить обработку ошибок и, возможно, более умную стратегию обновления. Я работал с похожим подходом в системе, где администраторы хотели менять настройки через веб-интерфейс. Мы расширили этот паттерн, добавив REST API для управления настройками, и уведомления об изменениях через SignalR, чтобы не ждать планового опроса.
Распределённая конфигурация для микросервисов
В мире микросервисов часто возникает проблема: как синхронизировать общие настройки между множеством сервисов? Решение – централизованное хранилище конфигурации с механизмом уведомлений:
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
| public interface IConfigurationUpdateNotifier
{
Task NotifyUpdatedAsync(string key, string value);
}
public class RabbitMqConfigNotifier : IConfigurationUpdateNotifier
{
private readonly IConnection _connection;
private readonly IModel _channel;
private const string ExchangeName = "config.updates";
public RabbitMqConfigNotifier(string connectionString)
{
var factory = new ConnectionFactory { Uri = new Uri(connectionString) };
_connection = factory.CreateConnection();
_channel = _connection.CreateModel();
_channel.ExchangeDeclare(ExchangeName, ExchangeType.Topic);
}
public Task NotifyUpdatedAsync(string key, string value)
{
var message = JsonSerializer.Serialize(new { Key = key, Value = value });
var body = Encoding.UTF8.GetBytes(message);
_channel.BasicPublish(
exchange: ExchangeName,
routingKey: key,
body: body);
return Task.CompletedTask;
}
} |
|
Соответствующий приёмник настраивается в каждом микросервисе:
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
| public class ConfigurationUpdater : IHostedService
{
private readonly IConfigurationRoot _configuration;
private readonly IConnection _connection;
private readonly IModel _channel;
private readonly List<string> _subscribedKeys;
public ConfigurationUpdater(
IConfiguration configuration,
IConfiguration rabbitConfig,
string[] keysToMonitor)
{
_configuration = configuration as IConfigurationRoot;
_subscribedKeys = new List<string>(keysToMonitor);
var factory = new ConnectionFactory { Uri = new Uri(rabbitConfig["RabbitMQ:ConnectionString"]) };
_connection = factory.CreateConnection();
_channel = _connection.CreateModel();
_channel.ExchangeDeclare("config.updates", ExchangeType.Topic);
}
public Task StartAsync(CancellationToken cancellationToken)
{
var queueName = _channel.QueueDeclare().QueueName;
foreach (var key in _subscribedKeys)
{
_channel.QueueBind(queueName, "config.updates", key);
}
var consumer = new EventingBasicConsumer(_channel);
consumer.Received += (model, ea) =>
{
var body = ea.Body.ToArray();
var message = JsonSerializer.Deserialize<ConfigUpdateMessage>(body);
// Обновляем конфигурацию
var memoryConfig = new Dictionary<string, string>
{
{ message.Key, message.Value }
};
// Здесь мы программно обновляем IConfigurationRoot
_configuration.GetReloadToken().RegisterChangeCallback(_ =>
{
// Что-то делаем после обновления
}, null);
};
_channel.BasicConsume(queueName, true, consumer);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_channel?.Close();
_connection?.Close();
return Task.CompletedTask;
}
} |
|
Такая архитектура позволяет централизованно управлять настройками в распределенной системе. Изменение настройки в центральном хранилище автоматически распространяется на все экземпляры всех микросервисов.
Интеграция конфигурации с облачными провайдерами
Современные приложения часто запускаются в облаке, и каждый облачный провайдер предлагает свои сервисы конфигурации. Для AWS это Parameter Store, для GCP – Cloud Storage, а для Azure – App Configuration. Вот пример интеграции с Azure App Configuration:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, config) =>
{
var settings = config.Build();
config.AddAzureAppConfiguration(options =>
{
options.Connect(settings["ConnectionStrings:AppConfig"])
.Select(KeyFilter.Any)
.ConfigureRefresh(refresh =>
{
refresh.Register("Sentinel", refreshAll: true)
.SetCacheExpiration(TimeSpan.FromMinutes(5));
})
.UseFeatureFlags();
});
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
}); |
|
А в сервис-контейнер добавляем:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| public void ConfigureServices(IServiceCollection services)
{
services.AddAzureAppConfiguration();
// Остальные сервисы
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// Middleware для динамического обновления конфигурации
app.UseAzureAppConfiguration();
// Остальные компоненты pipeline
} |
|
Главное достоинство - интеграция с инфраструктурой управления доступом облачного провайдера. Например, в Azure вы можете использовать Managed Identity для доступа к App Configuration без хранения каких-либо секретов в самом приложении. Из моего опыта, облачные решения для конфигурации начинают окупаться при наличии нескольких команд, работающих над разными микросервисами. Каждая команда может управлять своими настройками, не мешая другим, а централизованое хранилище обеспечивает единый интерфейс и аудит изменений.
Вот как-то так выглядят продвинутые сценарии работы с конфигурацией в ASP.NET Core. Конечно, это лишь малая часть того, что можно сделать с этой системой. Её гибкость и расширяемость позволяют адаптировать конфигурацию под самые специфические требования вашего проекта.
Оптимизация производительности и кеширование конфигурации
Работа с конфигурацией может оказывать заметное влияние на производительность, особенно если ваш сервис обрабатывает тысячи запросов в секунду. Один из неочевидных подводных камней – частое обращение к IOptionsSnapshot в запросах с высокой частотой.
C# | 1
2
3
4
5
6
7
8
9
10
| // Так делать не стоит в высоконагруженных компонентах
public class HighLoadController : Controller
{
private readonly IOptionsSnapshot<SomeFrequentlyUsedSettings> _settings;
public HighLoadController(IOptionsSnapshot<SomeFrequentlyUsedSettings> settings)
{
_settings = settings; // Создается на каждый запрос!
}
} |
|
В одном проекте мы столкнулись с тем, что простой контроллер отнимал непропорционально много ресурсов именно из-за подобной конструкции. При каждом запросе создавался новый экземпляр настроек, что включало десериализацию JSON и выполнение валидации – ненужная работа, если настройки не менялись. Решение – правильный выбор между IOptions , IOptionsSnapshot и IOptionsMonitor :
IOptions<T> - синглтон, значение фиксируется при запуске,
IOptionsSnapshot<T> - создается на каждый запрос, подходит для редко вызываемых эндпоинтов,
IOptionsMonitor<T> - синглтон с поддержкой обновления, идеален для фоновых служб.
На практике я придерживаюсь правила: для контроллеров с высокой нагрузкой использую IOptionsMonitor , кешируя значение на время обработки запроса:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| public class OptimizedController : Controller
{
private readonly IOptionsMonitor<FrequentSettings> _settingsMonitor;
public OptimizedController(IOptionsMonitor<FrequentSettings> settings)
{
_settingsMonitor = settings;
}
public IActionResult Index()
{
// Получаем значение один раз на запрос
var currentSettings = _settingsMonitor.CurrentValue;
// Используем currentSettings...
}
} |
|
Секреты разработчика и производственные настройки
Ещё один важный аспект – как организовать конфигурацию для разных стадий жизненного цикла приложения. Если в development-режиме можно спокойно хранить все настройки в appsettings.json, то в production такой подход небезопасен. Я обычно применяю следующую стратегию:
1. Разработка: локальные файлы + User Secrets.
2. Тестирование: файлы конфигурации + переменные окружения.
3. Продакшн: переменные окружения + Key Vault/Consul/другое защищенное хранилище.
В ASP.NET Core 6+ такой подход выглядит так:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| var builder = WebApplication.CreateBuilder(args);
if (builder.Environment.IsDevelopment())
{
builder.Configuration.AddUserSecrets<Program>();
}
else if (builder.Environment.IsProduction())
{
// В продакшене добавляем Azure Key Vault
var builtConfig = builder.Configuration.Build();
var kvEndpoint = builtConfig["KeyVault:Endpoint"];
builder.Configuration.AddAzureKeyVault(
new Uri(kvEndpoint),
new DefaultAzureCredential());
} |
|
Раньше для разграничения настроек по окружениям я написал простенькое расширение:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| public static class ConfigurationExtensions
{
public static T GetEnvironmentSpecific<T>(
this IConfiguration config,
string key,
string environment)
{
// Сначала ищем специфичную для окружения настройку
var envSpecificKey = $"{environment}:{key}";
if (config.GetSection(envSpecificKey).Exists())
{
return config.GetValue<T>(envSpecificKey);
}
// Иначе возвращаем общую
return config.GetValue<T>(key);
}
} |
|
Такой подход позволял иметь в конфигурации секции с общими настройками и переопределениями для конкретных сред:
JSON | 1
2
3
4
5
6
7
8
9
| {
"ApiUrl": "https://api.example.com",
"Development": {
"ApiUrl": "https://dev-api.example.com"
},
"Staging": {
"ApiUrl": "https://staging-api.example.com"
}
} |
|
Entity Framework и конфигурация
Если вы храните настройки в базе данных, интеграция с Entity Framework напрашивается сама собой. Я написал небольшую утилиту, которая синхронизирует настройки между EF и стандартной системой конфигурации:
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 class EntityFrameworkConfigSync<TContext> where TContext : DbContext
{
private readonly TContext _dbContext;
private readonly IConfigurationRoot _configuration;
public EntityFrameworkConfigSync(TContext context, IConfigurationRoot config)
{
_dbContext = context;
_configuration = config;
}
public async Task SyncFromDbToConfigAsync(string settingsTable)
{
// Получаем настройки из БД
var sql = $"SELECT [Key], [Value] FROM {settingsTable}";
var settings = await _dbContext.Database
.SqlQueryRaw<KeyValuePair<string, string>>(sql)
.ToListAsync();
// Обновляем конфигурацию
var memoryConfig = new Dictionary<string, string>();
foreach (var setting in settings)
{
memoryConfig[setting.Key] = setting.Value;
}
_configuration.Add(new MemoryConfigurationSource { InitialData = memoryConfig });
}
} |
|
На практике мы расширили этот класс методами для обратной синхронизации и реализовали триггеры в базе данных, которые уведомляли приложение об изменениях через специальный API-эндпоинт.
Типизированные фабрики настроек
Когда в приложении много различных модулей, каждый со своими настройками, код становится захламленным множеством вызовов Configure<T> . Я придумал подход с типизированной фабрикой настроек:
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
| public class ConfigurationFactory
{
private readonly IConfiguration _configuration;
private readonly Dictionary<Type, string> _sectionMappings = new();
public ConfigurationFactory(IConfiguration configuration)
{
_configuration = configuration;
}
public void RegisterSection<T>(string sectionName) where T : class, new()
{
_sectionMappings[typeof(T)] = sectionName;
}
public void AddAllOptionsToServices(IServiceCollection services)
{
foreach (var mapping in _sectionMappings)
{
var type = mapping.Key;
var sectionName = mapping.Value;
// Используем рефлексию для вызова Configure<T>
var configureMethod = typeof(OptionsConfigurationServiceCollectionExtensions)
.GetMethod(nameof(OptionsConfigurationServiceCollectionExtensions.Configure),
new[] { typeof(IServiceCollection), typeof(IConfiguration) })
?.MakeGenericMethod(type);
configureMethod?.Invoke(null, new object[]
{
services,
_configuration.GetSection(sectionName)
});
}
}
} |
|
Использование:
C# | 1
2
3
4
5
6
7
8
| var factory = new ConfigurationFactory(Configuration);
factory.RegisterSection<SmtpSettings>("Email:Smtp");
factory.RegisterSection<SmsSettings>("Messaging:Sms");
factory.RegisterSection<AuthSettings>("Security:Auth");
// и так далее для всех настроек
// Одна строка вместо десятков вызовов Configure<T>
factory.AddAllOptionsToServices(services); |
|
Реальная выгода от такого подхода наступает, когда у вас десятки различных настроек – код становится намного чище и логичнее. Естественно, этот паттерн можно расширить для поддержки валидации, постконфигурации и других функций.
Интеграция с мониторингом и метриками
В промышленных приложениях важно отслеживать не только сам факт изменения конфигурации, но и то, как эти изменения влияют на поведение системы. Я разработал небольшой аспект для интеграции с системой метрик:
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 class ConfigurationChangeMonitor<T> where T : class, new()
{
private readonly ILogger _logger;
private readonly IMetrics _metrics;
private readonly string _configName;
public ConfigurationChangeMonitor(
ILogger<ConfigurationChangeMonitor<T>> logger,
IMetrics metrics)
{
_logger = logger;
_metrics = metrics;
_configName = typeof(T).Name;
}
public void Initialize(IOptionsMonitor<T> monitor)
{
monitor.OnChange((newOptions, name) =>
{
_logger.LogInformation("Configuration {ConfigName} changed", _configName);
_metrics.Measure.Counter.Increment("configuration_changes_total",
new MetricTags("config_name", _configName));
// Можно добавить глубокое сравнение старых и новых настроек
// и логировать конкретные изменения
});
}
} |
|
Архитектурные решения для масштабируемых enterprise-систем
Работа с конфигурацией в масштабных enterprise-системах требует особого внимания к архитектуре. Когда у вас десятки микросервисов, сотни настроек и множество окружений, случайный подход приводит к хаосу. За время работы с крупными распределенными системами на ASP.NET Core я выработал несколько архитектурных принципов, которые помогают держать конфигурацию под контролем.
Паттерны конфигурирования в Clean Architecture
В контексте Clean Architecture конфигурация обычно рассматривается как внешняя зависимость, которая должна быть инкапсулирована на уровне инфраструктуры. Следуя этой философии, я выработал подход с четырьмя слоями абстракции:
1. Модели настроек - POCO-классы, определяющие структуру настроек.
2. Интерфейсы провайдеров - абстракции для доступа к настройкам.
3. Конкретные реализации - имплементации на основе ASP.NET Core Configuration.
4. Композиция - регистрация в DI-контейнере.
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
| // 1. Модель настроек (Domain Layer)
public class AuthenticationSettings
{
public int TokenExpirationMinutes { get; set; }
public bool RequireTwoFactor { get; set; }
}
// 2. Интерфейс провайдера (Application Layer)
public interface ISettingsProvider<T> where T : class, new()
{
T GetSettings();
Task UpdateSettingsAsync(T settings);
}
// 3. Конкретная реализация (Infrastructure Layer)
public class ConfigurationSettingsProvider<T> : ISettingsProvider<T>
where T : class, new()
{
private readonly IOptionsMonitor<T> _options;
private readonly IConfigurationRoot _configRoot;
private readonly string _sectionName;
public ConfigurationSettingsProvider(
IOptionsMonitor<T> options,
IConfiguration configuration,
string sectionName = null)
{
_options = options;
_configRoot = configuration as IConfigurationRoot;
_sectionName = sectionName ?? typeof(T).Name;
}
public T GetSettings() => _options.CurrentValue;
public Task UpdateSettingsAsync(T settings)
{
// Логика обновления настроек
return Task.CompletedTask;
}
}
// 4. Композиция (Composition Root)
services.AddScoped<ISettingsProvider<AuthenticationSettings>,
ConfigurationSettingsProvider<AuthenticationSettings>>(); |
|
Этот подход обеспечивает четкое разделение ответственности: доменные объекты и бизнес-логика работают с абстракциями, не зная ничего о реальном источнике настроек.
Версионирование конфигурации и миграции
Одна из самых неприятных вещей, с которой я сталкивался - это необходимость обновления формата конфигурации при развертывании новой версии приложения. Представьте: вы добавляете новое поле в настройки, но на продакшене еще стоит старая конфигурация без этого поля. Вот проверенное решение - схема версионирования конфигурации:
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
| public class VersionedSettings<T> where T : class, new()
{
public int Version { get; set; }
public T Settings { get; set; }
}
public class ConfigurationMigrator
{
private readonly IConfigurationRoot _configRoot;
private readonly Dictionary<int, Func<JObject, JObject>> _migrations;
public ConfigurationMigrator(IConfiguration configuration)
{
_configRoot = configuration as IConfigurationRoot;
_migrations = new Dictionary<int, Func<JObject, JObject>>();
}
public void RegisterMigration(int fromVersion, Func<JObject, JObject> migrationFunc)
{
_migrations[fromVersion] = migrationFunc;
}
public async Task MigrateAsync<T>(string sectionName, int targetVersion)
where T : class, new()
{
var section = _configRoot.GetSection(sectionName);
var versionedConfig = section.Get<VersionedSettings<T>>();
if (versionedConfig == null || versionedConfig.Version >= targetVersion)
return;
var settingsJson = JObject.FromObject(versionedConfig.Settings);
for (int v = versionedConfig.Version; v < targetVersion; v++)
{
if (_migrations.TryGetValue(v, out var migration))
{
settingsJson = migration(settingsJson);
}
}
// Обновляем конфигурацию с новой версией
versionedConfig.Version = targetVersion;
versionedConfig.Settings = settingsJson.ToObject<T>();
// Сохраняем изменения
// Логика сохранения зависит от провайдера
}
} |
|
При таком подходе каждое изменение схемы конфигурации сопровождается соответствующей миграцией. Когда приложение запускается, оно проверяет версию конфигурации и автоматически применяет необходимые миграции.
Этот паттерн спас нам нервы при обновлении крупной системы, где конфигурационные файлы размещались на десятках серверов. Вместо ручного обновления каждого файла, система автоматически мигрировала их при первом запуске новой версии.
Демо-приложение
Я собрал небольшое, но полноценное демо-приложение, которое показывает практически все рассмотренные подходы в действии. Это простой сервис управления задачами с аутентификацией, уведомлениями и публичным API. Архитектура основана на принципах Clean Architecture с четким разделением на слои.
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
| public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostContext, config) =>
{
// Базовая конфигурация
config.AddJsonFile("appsettings.json", optional: false)
.AddJsonFile($"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
// Секреты разработки (только в Development)
if (hostContext.HostingEnvironment.IsDevelopment())
{
config.AddUserSecrets<Program>();
}
// Динамическая конфигурация из Redis (только в Production)
if (hostContext.HostingEnvironment.IsProduction())
{
var builtConfig = config.Build();
config.AddRedisConfiguration(
builtConfig["Redis:ConnectionString"],
"app:config:");
}
// Command-line аргументы имеют наивысший приоритет
config.AddCommandLine(args);
})
.ConfigureServices((hostContext, services) =>
{
// Регистрация Options с валидацией
services.AddOptions<DatabaseSettings>()
.Bind(hostContext.Configuration.GetSection("Database"))
.ValidateDataAnnotations()
.Validate(settings =>
{
return !string.IsNullOrEmpty(settings.ConnectionString)
&& settings.MaxPoolSize > 0;
}, "Неверные настройки базы данных");
// Настройки с мониторингом изменений
services.Configure<NotificationSettings>(
hostContext.Configuration.GetSection("Notifications"));
// Службы, использующие конфигурацию
services.AddScoped<INotificationService, EmailNotificationService>();
services.AddScoped<ITaskRepository, TaskRepository>();
// Сервис шифрования для защиты данных
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo("keys"));
// Регистрация фабрики опций для постобработки
services.AddTransient<IConfigureOptions<ApiSettings>, ApiSettingsSetup>();
});
} |
|
Соответствующие файлы конфигурации:
JSON | 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
| // appsettings.json - базовые настройки
{
"Database": {
"ConnectionString": "Server=localhost;Database=TaskManager;Integrated Security=true",
"MaxPoolSize": 100,
"CommandTimeout": 30
},
"Notifications": {
"Email": {
"SmtpServer": "localhost",
"Port": 25,
"SenderEmail": "noreply@example.com"
},
"EnableEmailNotifications": true,
"TaskDueSoonThresholdHours": 24
},
"Api": {
"BaseUrl": "https://api.example.com",
"DefaultPageSize": 20,
"MaxPageSize": 100,
"EnableRateLimiting": true
}
}
// appsettings.Development.json - переопределения для разработки
{
"Notifications": {
"Email": {
"SmtpServer": "localhost",
"Port": 1025
},
"OverrideRecipientEmail": "dev@example.com"
},
"Api": {
"BaseUrl": "https://localhost:5001",
"EnableRateLimiting": false
}
} |
|
Классы настроек с валидацией:
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
| public class DatabaseSettings
{
[Required]
public string ConnectionString { get; set; }
[Range(1, 1000)]
public int MaxPoolSize { get; set; } = 10;
[Range(1, 300)]
public int CommandTimeout { get; set; } = 30;
}
public class NotificationSettings
{
public EmailSettings Email { get; set; }
public bool EnableEmailNotifications { get; set; } = true;
public int TaskDueSoonThresholdHours { get; set; } = 24;
public string OverrideRecipientEmail { get; set; }
}
public class EmailSettings
{
public string SmtpServer { get; set; }
public int Port { get; set; }
public string SenderEmail { get; set; }
public string Username { get; set; }
public string Password { get; set; }
} |
|
Сервис уведомлений, реагирующий на изменения конфигурации:
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 EmailNotificationService : INotificationService, IDisposable
{
private readonly IOptionsMonitor<NotificationSettings> _settings;
private readonly ILogger<EmailNotificationService> _logger;
private IDisposable _settingsChangeToken;
private SmtpClient _smtpClient;
public EmailNotificationService(
IOptionsMonitor<NotificationSettings> settings,
ILogger<EmailNotificationService> logger)
{
_settings = settings;
_logger = logger;
// Инициализируем SMTP-клиент
InitializeSmtpClient();
// Подписываемся на изменения настроек
_settingsChangeToken = _settings.OnChange(_ => {
_logger.LogInformation("Настройки уведомлений изменились, переинициализация SMTP-клиента");
InitializeSmtpClient();
});
}
private void InitializeSmtpClient()
{
var settings = _settings.CurrentValue;
_smtpClient?.Dispose();
_smtpClient = new SmtpClient(settings.Email.SmtpServer, settings.Email.Port);
// Настройка аутентификации, если указаны учетные данные
if (!string.IsNullOrEmpty(settings.Email.Username))
{
_smtpClient.Credentials = new NetworkCredential(
settings.Email.Username,
settings.Email.Password);
}
}
public void Dispose()
{
_settingsChangeToken?.Dispose();
_smtpClient?.Dispose();
}
} |
|
ASP.NET Core: разный формат даты контроллера ASP.NET и AngularJS Собственно, проблему пока еще не разруливал, но уже погуглил. Разный формат даты который использует... ASP.NET MVC или ASP.NET Core Добрый вечер, подскажите что лучшие изучать ASP.NET MVC или ASP.NET Core ? Как я понимаю ASP.NET... Что выбрать ASP.NET или ASP.NET Core ? Добрый день форумчане, хотелось бы услышать ваше мнение, какой из перечисленных фреймворков лучше... ASP.NET Core или ASP.NET MVC Здравствуйте
После изучение основ c# я решил выбрать направление веб разработки. Подскажите какие... Стоит ли учить asp.net, если скоро станет asp.net core? Всем привет
Если я правильно понимаю, лучше учить Core ? ASP.NET или ASP.NET Core Добрый вечер, подскажите новичку в чем разница между asp.net и asp.net core, нужно ли знать оба... Почему скрипт из ASP.NET MVC 5 не работает в ASP.NET Core? В представлении в версии ASP.NET MVC 5 был скрипт:
@model RallyeAnmeldung.Cars
... Asp.net core rc 2 и Entity Framework core Добрый день, кто-нибудь уже перешел на новую версию фреймверка?
Хотелось бы получить пример.
... ASP.NET Core + EF Core: ошибка при обновлении БД после создания миграции Всем привет!
Начал осваивать ASP.NET Core: создал проект "Веб-приложение" без Identity.
Сразу же... Пагинация. Как установить колличество позиций на странице? Razor Pages с EF Core в ASP.NET Core Изучаю учебник - Razor Pages с Entity Framework Core в ASP.NET Core // docs.microsoft.com/ru-ru/
... ASP.NET Core 3.0 с Entity Framework Core + SQL Привет,
прохожу стажировку в одной компании. Дали вот такое задание, дедлайн отсутствует,... Как развернуть бд на другом ПК? ASP.NET Core + MS SQL(EF Core) Суть вот в чем. В ЧТ сдавать тестовое задание на анимации и простейший CRUD с базой данных. Окей....
|