Форум программистов, компьютерный форум, киберфорум
Reangularity
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск Заказать работу  

CRUD REST API на Angular и ASP.NET Core

Запись от Reangularity размещена 05.09.2025 в 20:20
Показов 7084 Комментарии 0

Нажмите на изображение для увеличения
Название: CRUD REST API на Angular и ASP.NET Core.jpg
Просмотров: 431
Размер:	225.2 Кб
ID:	11106
В моей карьере я сталкивался с множеством технологических стеков, но комбинация Angular и ASP.NET Core для создания CRUD-приложений остаётся одной из самых мощных и гибких. Однажды, когда мне поручили разработать высоконагруженную систему для финтех-стартапа, я решил использовать именно этот тандем. Не буду приукрашивать - путь был тернистым, с множеством граблей, на которые я наступил так громко, что до сих пор слышу эхо в своих ночных кошмарах.

Архитектура и подготовка проекта



Перед началом любого проекта я всегда тратю время на обдумывание архитектуры - это как планирование фундамента дома. Плохое решение на этом этапе может привести к катастрофическим последствиям позже. Для CRUD-приложения, которое взаимодействует с REST API, ключевым становится правильное разделение ответственности между клиентом и сервером.

Нажмите на изображение для увеличения
Название: CRUD REST API на Angular и ASP.NET Core 2.jpg
Просмотров: 115
Размер:	175.0 Кб
ID:	11107

Настройка ASP.NET Core Web API



Когда я приступаю к созданию бэкенда, первым делом создаю проект ASP.NET Core Web API. Visual Studio делает это почти тривиальной задачей.

C#
1
2
dotnet new webapi -n CrudApi
cd CrudApi
Эта простая команда создаёт скелет API-проекта. Однако, чтобы получить по-настоящему надёжное и масштабируемое решение, я предпочитаю организовывать код по принципу "луковичной архитектуры" (Onion Architecture). Помню случай из своей практики: на одном проекте мы не уделили должного внимания этому аспекту, и когда бизнес-требования начали меняться, нам пришлось переписать половину системы. Это было... познавательно, скажем так.
Структура проекта, которую я рекомендую:

C#
1
2
3
4
5
6
CrudApi/
├── CrudApi.API              // Проект с контроллерами API
├── CrudApi.Core             // Доменные модели и интерфейсы
├── CrudApi.Application      // Бизнес-логика и сервисы
├── CrudApi.Infrastructure   // Реализация доступа к данным
└── CrudApi.Tests            // Тесты
Такое разделение может показаться излишним для простого CRUD-приложения, но оно окупается, когда проект начинает расти. Кроме того, эта структура отлично подходит для применения принципов SOLID и DDD.

В файле Startup.cs (или Program.cs для .NET 6+) я конфигурирую сервисы и middleware:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers()
            .AddJsonOptions(options => 
            {
                options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
            });
    
    services.AddDbContext<AppDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
    
    // Регистрация репозиториев и сервисов
    services.AddScoped<IEmployeeRepository, EmployeeRepository>();
    services.AddScoped<IEmployeeService, EmployeeService>();
    
    // Swagger для документации API
    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new OpenApiInfo { Title = "CRUD API", Version = "v1" });
    });
}
Кстати, о Swagger - это незаменимый инструмент, когда работаешь над API. Он не только документирует эндпоинты, но и позволяет тестировать их прямо из браузера.

Структура Angular приложения



Теперь перейдём к фронтенд-части. Angular - фреймворк с строгой структурой, что делает его идеальным выбором для крупных проектов. Начнём с создания базового приложения:

Bash
1
2
ng new crud-client --routing=true --style=scss
cd crud-client
Для организации кода в Angular я предпочитаю модульный подход с разделением по функциональному назначению:

Code
1
2
3
4
5
6
7
8
9
src/
├── app/
│   ├── core/             // Синглтон-сервисы, guards, interceptors
│   ├── shared/           // Переиспользуемые компоненты и директивы
│   ├── features/         // Функциональные модули приложения
│   │   └── employees/    // Модуль для работы с сотрудниками
│   ├── app.module.ts
│   └── app-routing.module.ts
└── assets/
Такая структура позволяет легко масштабировать приложение и добавлять новые функциональные блоки. В моём опыте работы с Angular (а я делал проекты от маленьких SPA до огромных энтерпрайз-систем) эта структура доказала свою эффективность. Помню забавный случай - однажды я унаследовал проект, где вся логика была свалена в одну папку, без модулей, с плоской структурой. Да, он работал, но каждое изменение превращалось в квест "найди, где используется этот компонент".

Для организации модулей я часто использую feature modules с routing modules для каждой функциональности:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// employees.module.ts
@NgModule({
  declarations: [
    EmployeeListComponent,
    EmployeeDetailsComponent,
    EmployeeFormComponent
  ],
  imports: [
    CommonModule,
    EmployeesRoutingModule,
    ReactiveFormsModule,
    SharedModule
  ]
})
export class EmployeesModule { }

Выбор архитектурных паттернов



Для организации потока данных в приложении существует множество подходов. На бэкенде я предпочитаю Repository Pattern в сочетании с Service Layer. Это позволяет изолировать бизнес-логику от доступа к данным:

C#
1
2
3
4
5
6
7
8
public interface IEmployeeRepository
{
    Task<IEnumerable<Employee>> GetAllAsync();
    Task<Employee> GetByIdAsync(int id);
    Task<Employee> CreateAsync(Employee employee);
    Task UpdateAsync(Employee employee);
    Task DeleteAsync(int id);
}
На фронтенде для Angular приложений я часто использую Smart/Dumb components (или Container/Presentational components). Это разделение делает компоненты более тестируемыми и переиспользуемыми:

TypeScript
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
// Smart Component (container)
@Component({
  selector: 'app-employee-list-container',
  template: `<app-employee-list 
                [employees]="employees$ | async" 
                (deleteEmployee)="onDelete($event)">
              </app-employee-list>`
})
export class EmployeeListContainerComponent {
  employees$ = this.employeeService.getAll();
  
  constructor(private employeeService: EmployeeService) {}
  
  onDelete(id: number) {
    this.employeeService.delete(id).subscribe(() => {
      // Обновить список после удаления
    });
  }
}
 
// Dumb Component (presentational)
@Component({
  selector: 'app-employee-list',
  templateUrl: './employee-list.component.html'
})
export class EmployeeListComponent {
  @Input() employees: Employee[] = [];
  @Output() deleteEmployee = new EventEmitter<number>();
  
  onDelete(id: number) {
    this.deleteEmployee.emit(id);
  }
}
В зависимости от сложности проекта можно также рассмотреть использование state management библиотек, таких как NgRx или NGXS, но для простых CRUD-приложений это часто избыточно.

Есть и другие архитектурные паттерны, которые я часто применяю в подобных проектах. Например, CQRS (Command Query Responsibility Segregation) может быть чрезвычайно полезен, когда модели чтения и записи существенно отличаются. Однажды я работал над проектом для ритейл-компании, где модели товаров для записи содержали десятки полей, а для витрины нужны были лишь базовые атрибуты. Внедрение CQRS спасло нас от постоянной перепаковки данных.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
// Query
public class GetEmployeeQuery : IRequest<EmployeeDto>
{
    public int Id { get; set; }
}
 
// Command
public class CreateEmployeeCommand : IRequest<int>
{
    public string Name { get; set; }
    public string Position { get; set; }
    public decimal Salary { get; set; }
}
Для малых и средних проектов можно обойтись более простыми решениями. Я иногда ловлю себя на мысли, что слишком увлекаюсь "архитектурной астрономией" - то есть строю звёздные системы там, где достаточно небольшого астероида. Помните об этом, когда дизайните свои системы.

Конфигурация CORS и middleware



О, CORS - мой старый "друг". Не сосчитать, сколько часов я провел, разбираясь с ошибками вида "Access-Control-Allow-Origin". Эта защита браузера критически важна для безопасности, но часто становится источником головной боли.
В ASP.NET Core настройка CORS проста, если знаешь, что делаешь:

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 void ConfigureServices(IServiceCollection services)
{
    // Предыдущие настройки...
    
    services.AddCors(options =>
    {
        options.AddPolicy("AllowSpecificOrigin",
            builder => builder.WithOrigins("http://localhost:4200")  // Angular dev server
                              .AllowAnyMethod()
                              .AllowAnyHeader()
                              .AllowCredentials());
    });
}
 
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // Другие middleware...
    
    app.UseCors("AllowSpecificOrigin");
    
    app.UseRouting();
    app.UseAuthentication();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}
Обратите внимание на порядок middleware - он критически важен! Помню, как один раз я потратил целый день на отладку, пока не понял, что расположил app.UseCors() после app.UseRouting(). Порядок действительно имеет значение. Для продакшена я рекомендую более строгую политику CORS, где вы точно указываете разрешённые домены, методы и заголовки. Никаких AllowAnyOrigin() в боевой среде! Это всё равно что оставить ключи от квартиры под ковриком.

Кроме CORS, важно настроить и другие middleware, такие как:

1. Аутентификация и авторизация - я часто использую JWT-токены для защиты API:
C#
1
2
   services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
           .AddJwtBearer(options => { /*...*/});
2. Обработка исключений - глобальный обработчик исключений упрощает код и делает его более чистым:
C#
1
2
3
4
5
6
7
   app.UseExceptionHandler(builder =>
   {
       builder.Run(async context =>
       {
           // Логика обработки ошибок
       });
   });
3. Compression - сжатие ответов может значительно уменьшить объем передаваемых данных:
C#
1
2
3
4
5
   services.AddResponseCompression(options =>
   {
       options.Providers.Add<BrotliCompressionProvider>();
       options.Providers.Add<GzipCompressionProvider>();
   });
Правильная настройка middleware в ASP.NET Core - это как искуство. Для каждого проекта набор и порядок компонентов может отличаться. Бывает, что избыточный middleware создаёт ненужные накладные расходы. В одном из моих проектов мы отказались от некоторых "стандартных" компонентов и получили прирост производительности почти на 20%.

Разница между 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? Или я может что-то не так понял? И...

Polygon, Gemetry приложение CRUD ASP.NET Core
Добрый день господа программисты и системные администраторы. Имеется проблема и необходима ваша...


Реализация серверной части



Нажмите на изображение для увеличения
Название: CRUD REST API на Angular и ASP.NET Core 3.jpg
Просмотров: 83
Размер:	258.0 Кб
ID:	11108

Серверная часть - это фундамент любого API-приложения. Здесь я сосредоточусь на бэкенде нашего CRUD-приложения, который будет принимать запросы от Angular-клиента, обрабатывать их и возвращать результат.

Entity Framework Core и миграции



Entity Framework Core - мой верный спутник во всех .NET-проектах, связанных с базами данных. Для начала мне нужно определить доменные модели, которые будут отражать структуру данных. Создадим модель для сотрудников:

C#
1
2
3
4
5
6
7
8
9
10
public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public DateTime DateOfBirth { get; set; }
    public string Address { get; set; }
    public string Gender { get; set; }
    public string PinCode { get; set; }
}
Затем я создаю контекст данных, который будет мостом между кодом и базой:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
    
    public DbSet<Employee> Employees { get; set; }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Конфигурация моделей с помощью Fluent API
        modelBuilder.Entity<Employee>()
            .HasIndex(e => e.Email)
            .IsUnique();
            
        // Дополнительные настройки...
    }
}
Когда я только начинал работать с EF Core, я делал смешную ошибку - использовал annotations прямо в доменных моделях. Потом я обжегся, когда потребовалось использовать те же модели в другом контексте. С тех пор я предпочитаю Fluent API, которое позволяет отделить конфигурацию от самих моделей.

Миграции в EF Core - это как система контроля версий для вашей базы данных. Вместо ручного создания SQL-скриптов (боже, сколько времени я когда-то потратил на это!), я использую команды:

C#
1
2
dotnet ef migrations add InitialCreate
dotnet ef database update
Хотя в реальных проектах я часто использую подход "сначала код, потом миграции", бывают случаи, когда удобнее обратный подход - "сначала база, потом код". Для этого есть отличный инструмент:

C#
1
dotnet ef dbcontext scaffold "Connection String" Microsoft.EntityFrameworkCore.SqlServer -o Models
Эта команда на основе существующей базы сгенерирует все необходимые классы моделей и контекст. Спасительная штука, когда работаешь с унаследованными системами!

Реализация пагинации и фильтрации записей



В одном крупном проекте я столкнулся с проблемой - клиент грузил из базы все 50 000 записей за раз, что приводило к зависанию приложения. Решение? Пагинация и фильтрация на уровне API. Сначала создадим модель запроса:

C#
1
2
3
4
5
6
7
8
public class EmployeeFilterModel
{
    public string SearchTerm { get; set; }
    public int Page { get; set; } = 1;
    public int PageSize { get; set; } = 10;
    public string SortBy { get; set; } = "Name";
    public bool SortAscending { get; set; } = true;
}
А теперь реализуем расширение для IQueryable, которое будет применять фильтры:

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 static class QueryableExtensions
{
    public static IQueryable<Employee> ApplyFilters(
        this IQueryable<Employee> query, 
        EmployeeFilterModel filter)
    {
        if (!string.IsNullOrEmpty(filter.SearchTerm))
        {
            query = query.Where(e => 
                e.Name.Contains(filter.SearchTerm) || 
                e.Email.Contains(filter.SearchTerm));
        }
        
        // Сортировка
        query = filter.SortAscending 
            ? query.OrderBy(GetSortProperty(filter.SortBy))
            : query.OrderByDescending(GetSortProperty(filter.SortBy));
            
        // Пагинация
        return query
            .Skip((filter.Page - 1) * filter.PageSize)
            .Take(filter.PageSize);
    }
    
    private static Expression<Func<Employee, object>> GetSortProperty(string propertyName)
    {
        // Логика выбора свойства для сортировки
        // ...
    }
}
Замечу, что работая с большими наборами данных, важно применять фильтрацию и сортировку именно на уровне запроса к БД, а не после загрузки данных в память. Иначе вы получите все те же проблемы с производительностью.

Контроллеры и маршрутизация



ASP.NET Core API строится вокруг контроллеров. Вот как выглядит наш контроллер для сотрудников:

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
[Route("api/[controller]")]
[ApiController]
public class EmployeesController : ControllerBase
{
    private readonly IEmployeeService _employeeService;
    
    public EmployeesController(IEmployeeService employeeService)
    {
        _employeeService = employeeService;
    }
    
    [HttpGet]
    public async Task<ActionResult<PagedResult<EmployeeDto>>> GetAll([FromQuery] EmployeeFilterModel filter)
    {
        var result = await _employeeService.GetPagedAsync(filter);
        return Ok(result);
    }
    
    [HttpGet("{id}")]
    public async Task<ActionResult<EmployeeDto>> GetById(int id)
    {
        var employee = await _employeeService.GetByIdAsync(id);
        if (employee == null)
            return NotFound();
            
        return Ok(employee);
    }
    
    [HttpPost]
    public async Task<ActionResult<EmployeeDto>> Create([FromBody] CreateEmployeeDto dto)
    {
        if (!ModelState.IsValid)
            return BadRequest(ModelState);
            
        var created = await _employeeService.CreateAsync(dto);
        return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
    }
    
    [HttpPut("{id}")]
    public async Task<IActionResult> Update(int id, [FromBody] UpdateEmployeeDto dto)
    {
        if (id != dto.Id)
            return BadRequest("ID в пути запроса не соответствует ID в теле");
            
        await _employeeService.UpdateAsync(dto);
        return NoContent();
    }
    
    [HttpDelete("{id}")]
    public async Task<IActionResult> Delete(int id)
    {
        await _employeeService.DeleteAsync(id);
        return NoContent();
    }
}
Обратите внимание на возвращаемые статус-коды - это важная часть REST API. Например, при создании записи мы возвращаем 201 (Created) вместе с URL для доступа к новому ресурсу. Это следование принципам REST, которое сделает ваш API более предсказуемым и дружелюбным. Недавно я работал с проектом, где контроллеры были завалены бизнес-логикой. Это классическая ошибка! Контроллер должен быть тонким - он принимает запросы, делегирует работу сервисам и возвращает результаты. Всё остальное - не его забота.

Валидация и обработка ошибок



Никогда не доверяйте входным данным - это мой девиз с тех пор, как один "креативный" пользователь умудрился сломать целую систему, отправив JSON с вложенностью в 100 уровней. Для валидации в ASP.NET Core я использую атрибуты валидации на DTO:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CreateEmployeeDto
{
    [Required(ErrorMessage = "Имя обязательно")]
    [StringLength(100, MinimumLength = 2)]
    public string Name { get; set; }
    
    [Required]
    [EmailAddress]
    public string Email { get; set; }
    
    [Required]
    [DataType(DataType.Date)]
    public DateTime DateOfBirth { get; set; }
    
    // Другие поля...
}
Но атрибутов не всегда достаточно. Для сложной валидации я применяю FluentValidation:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class CreateEmployeeValidator : AbstractValidator<CreateEmployeeDto>
{
    public CreateEmployeeValidator(AppDbContext dbContext)
    {
        RuleFor(x => x.Email)
            .MustAsync(async (email, cancellation) => 
                !await dbContext.Employees.AnyAsync(e => e.Email == email))
            .WithMessage("Этот email уже зарегистрирован");
            
        RuleFor(x => x.DateOfBirth)
            .Must(dob => dob < DateTime.Now.AddYears(-18))
            .WithMessage("Сотрудник должен быть старше 18 лет");
    }
}
Для глобальной обработки исключений я создаю 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
30
31
32
33
34
35
public class ErrorHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;
    
    // ...
    
    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Необработанное исключение");
            await HandleExceptionAsync(context, ex);
        }
    }
    
    private static Task HandleExceptionAsync(HttpContext context, Exception ex)
    {
        var code = HttpStatusCode.InternalServerError; // 500
        var result = JsonConvert.SerializeObject(new { error = "Произошла ошибка" });
        
        if (ex is NotFoundException) code = HttpStatusCode.NotFound;
        else if (ex is ValidationException) code = HttpStatusCode.BadRequest;
        // Другие типы исключений...
        
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)code;
        
        return context.Response.WriteAsync(result);
    }
}
Такой middleware спасает от необходимости обернуть каждый метод контроллера в try/catch, что делает код намного чище. В моей практике был случай, когда мы внедрили подобный обработчик в большой проект и удалили почти 30% кода - все эти бесконечные try/catch блоки!

Автоматическое документирование API с Swagger/OpenAPI



Ох, сколько раз я слышал от фронтенд-разработчиков: "А как вызывать этот эндпоинт? Что он принимает? Что возвращает?" Swagger решает эту проблему, автоматически генерируя документацию API на основе вашего кода.
Добавить Swagger в 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
public void ConfigureServices(IServiceCollection services)
{
    // Предыдущие настройки...
    
    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new OpenApiInfo 
        { 
            Title = "Employee CRUD API", 
            Version = "v1",
            Description = "API для управления сотрудниками",
            Contact = new OpenApiContact
            {
                Name = "Ваше имя",
                Email = "your.email@example.com"
            }
        });
        
        // Включаем XML-комментарии
        var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
        var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
        c.IncludeXmlComments(xmlPath);
    });
}
 
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // Предыдущие настройки...
    
    app.UseSwagger();
    app.UseSwaggerUI(c =>
    {
        c.SwaggerEndpoint("/swagger/v1/swagger.json", "Employee API v1");
        c.RoutePrefix = string.Empty; // Swagger UI будет доступен по корневому URL
    });
    
    // Последующие настройки...
}
Теперь вы можете документировать методы с помощью XML-комментариев:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/// <summary>
/// Получает сотрудника по идентификатору
/// </summary>
/// <param name="id">Идентификатор сотрудника</param>
/// <returns>Информация о сотруднике</returns>
/// <response code="200">Возвращает сотрудника</response>
/// <response code="404">Если сотрудник не найден</response>
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<EmployeeDto>> GetById(int id)
{
    // Реализация...
}
Помню, как в одном проекте я потратил целую неделю на написание детальной API-документации в Confluence. А потом пришлось ещё неделю её обновлять, потому что API изменился. Со Swagger такой проблемы нет - документация всегда синхронизирована с кодом.

Логирование и мониторинг запросов



"Без логов, как без рук" - мой личный девиз после того, как я провел 48 часов непрерывной отладки критической проблемы в продакшене без достаточного логирования. Никогда больше! В ASP.NET Core есть встроенный механизм логирования. Я настраиваю его так:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void ConfigureServices(IServiceCollection services)
{
    // Предыдущие настройки...
    
    services.AddLogging(builder =>
    {
        builder.AddConsole();
        builder.AddDebug();
        builder.AddApplicationInsights(configureTelemetry: (config) =>
        {
            config.ConnectionString = Configuration["ApplicationInsights:ConnectionString"];
        });
        
        // Можно добавить другие провайдеры, например Serilog
        // builder.AddSerilog();
    });
}
Для мониторинга производительности API я создаю 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
30
31
32
33
public class RequestPerformanceMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestPerformanceMiddleware> _logger;
    private readonly Stopwatch _timer;
    
    public RequestPerformanceMiddleware(
        RequestDelegate next,
        ILogger<RequestPerformanceMiddleware> logger)
    {
        _next = next;
        _logger = logger;
        _timer = new Stopwatch();
    }
    
    public async Task InvokeAsync(HttpContext context)
    {
        _timer.Start();
        
        await _next(context);
        
        _timer.Stop();
        
        if (_timer.ElapsedMilliseconds > 500) // Порог для "медленных" запросов
        {
            _logger.LogWarning(
                "Медленный запрос: {Method} {Path} выполнялся {ElapsedMilliseconds}ms",
                context.Request.Method,
                context.Request.Path,
                _timer.ElapsedMilliseconds);
        }
    }
}
Это помогает выявить проблемные эндпоинты до того, как они станут причиной жалоб от пользователей.

Применение AutoMapper для преобразования между DTO и доменными моделями



Постоянное ручное маппирование объектов из одного типа в другой - утомительная задача. AutoMapper решает эту проблему:

C#
1
2
3
4
5
6
public void ConfigureServices(IServiceCollection services)
{
    // Предыдущие настройки...
    
    services.AddAutoMapper(typeof(Startup));
}
Затем создаем профили маппинга:

C#
1
2
3
4
5
6
7
8
9
10
11
12
public class MappingProfile : Profile
{
    public MappingProfile()
    {
        CreateMap<Employee, EmployeeDto>();
        CreateMap<CreateEmployeeDto, Employee>();
        
        CreateMap<UpdateEmployeeDto, Employee>()
            .ForAllMembers(opts => opts.Condition(
                (src, dest, srcMember) => srcMember != null));
    }
}
И используем в сервисах:

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 class EmployeeService : IEmployeeService
{
    private readonly IEmployeeRepository _repository;
    private readonly IMapper _mapper;
    
    public EmployeeService(IEmployeeRepository repository, IMapper mapper)
    {
        _repository = repository;
        _mapper = mapper;
    }
    
    public async Task<EmployeeDto> GetByIdAsync(int id)
    {
        var entity = await _repository.GetByIdAsync(id);
        return _mapper.Map<EmployeeDto>(entity);
    }
    
    public async Task<EmployeeDto> CreateAsync(CreateEmployeeDto dto)
    {
        var entity = _mapper.Map<Employee>(dto);
        var created = await _repository.CreateAsync(entity);
        return _mapper.Map<EmployeeDto>(created);
    }
    
    // Остальные методы...
}
Однажды в крупном проекте мы отказались от AutoMapper в пользу ручного маппинга из-за проблем с производительностью. Но это исключительный случай - для большинства проектов AutoMapper прекрасно работает и экономит массу времени.

Кеширование на серверной стороне



Кеширование - мощный инструмент оптимизации производительности. В ASP.NET Core для этого есть встроенные средства:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
public void ConfigureServices(IServiceCollection services)
{
    // Предыдущие настройки...
    
    services.AddMemoryCache(); // In-memory кеширование
    
    // Или Redis для распределенного кеширования
    services.AddStackExchangeRedisCache(options =>
    {
        options.Configuration = Configuration.GetConnectionString("Redis");
        options.InstanceName = "EmployeeCache_";
    });
}
Для удобства я создаю сервис-обертку над кешем:

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 CacheService : ICacheService
{
    private readonly IMemoryCache _memoryCache;
    
    public CacheService(IMemoryCache memoryCache)
    {
        _memoryCache = memoryCache;
    }
    
    public T Get<T>(string key)
    {
        _memoryCache.TryGetValue(key, out T value);
        return value;
    }
    
    public void Set<T>(string key, T value, TimeSpan expiration)
    {
        var options = new MemoryCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = expiration
        };
        
        _memoryCache.Set(key, value, options);
    }
    
    public void Remove(string key)
    {
        _memoryCache.Remove(key);
    }
}
И применяю его в сервисах:

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 async Task<PagedResult<EmployeeDto>> GetPagedAsync(EmployeeFilterModel filter)
{
    var cacheKey = $"Employees_{JsonConvert.SerializeObject(filter)}";
    
    var cached = _cacheService.Get<PagedResult<EmployeeDto>>(cacheKey);
    if (cached != null)
        return cached;
    
    // Если нет в кеше, получаем из базы
    var entities = await _repository.GetPagedAsync(filter);
    var result = new PagedResult<EmployeeDto>
    {
        Items = _mapper.Map<List<EmployeeDto>>(entities.Items),
        TotalCount = entities.TotalCount,
        Page = filter.Page,
        PageSize = filter.PageSize
    };
    
    // Сохраняем в кеш на 5 минут
    _cacheService.Set(cacheKey, result, TimeSpan.FromMinutes(5));
    
    return result;
}
В одном проекте мы внедрили кеширование для часто запрашиваемых данных и сократили нагрузку на БД на 80%! Но помните о необходимости инвалидации кеша при изменении данных - иначе пользователи будут видеть устаревшую информацию.

Клиентская разработка на Angular



Нажмите на изображение для увеличения
Название: CRUD REST API на Angular и ASP.NET Core 4.jpg
Просмотров: 63
Размер:	168.2 Кб
ID:	11109

Теперь, когда у нас готов бэкенд, самое время перейти к фронтенду. Angular - это мощный фреймворк, который превосходно подходит для создания CRUD-приложений. Его компонентный подход, мощная система инъекции зависимостей и реактивное программирование делают разработку предсказуемой и масштабируемой.

Настройка HttpClient и создание базового сервиса для API взаимодействия



Первым делом, нам нужно импортировать HttpClientModule в корневой модуль приложения:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { HttpClientModule } from '@angular/common/http';
 
@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    // Другие модули...
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
Затем создадим базовый сервис, который будет отвечать за взаимодействие с нашим API:

TypeScript
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
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';
 
@Injectable({
  providedIn: 'root'
})
export class ApiService {
  private baseUrl = environment.apiUrl;
 
  constructor(private http: HttpClient) { }
 
  get<T>(url: string, params: any = {}): Observable<T> {
    let httpParams = new HttpParams();
    for (const key in params) {
      if (params.hasOwnProperty(key)) {
        httpParams = httpParams.set(key, params[key]);
      }
    }
    return this.http.get<T>(`${this.baseUrl}/${url}`, { params: httpParams });
  }
 
  post<T>(url: string, body: any): Observable<T> {
    return this.http.post<T>(`${this.baseUrl}/${url}`, body);
  }
 
  put<T>(url: string, body: any): Observable<T> {
    return this.http.put<T>(`${this.baseUrl}/${url}`, body);
  }
 
  delete<T>(url: string): Observable<T> {
    return this.http.delete<T>(`${this.baseUrl}/${url}`);
  }
}
Этот базовый сервис инкапсулирует HTTP-вызовы к нашему API и упрощает их использование в конкретных сервисах. Помню, как в одном из проектов я не создал такой базовый класс, и мы в результате имели дублирование кода в каждом сервисе. Когда понадобилось добавить аутентификацию, пришлось менять десятки файлов вместо одного!

Создание специализированного сервиса для сотрудников



Теперь на основе базового сервиса создадим специализированный сервис для работы с сотрудниками:

TypeScript
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
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { ApiService } from './api.service';
import { Employee } from '../models/employee';
import { PagedResult } from '../models/paged-result';
 
@Injectable({
  providedIn: 'root'
})
export class EmployeeService {
  private endpoint = 'employees';
 
  constructor(private apiService: ApiService) { }
 
  getAll(filters: any = {}): Observable<PagedResult<Employee>> {
    return this.apiService.get<PagedResult<Employee>>(this.endpoint, filters);
  }
 
  getById(id: number): Observable<Employee> {
    return this.apiService.get<Employee>(`${this.endpoint}/${id}`);
  }
 
  create(employee: Partial<Employee>): Observable<Employee> {
    return this.apiService.post<Employee>(this.endpoint, employee);
  }
 
  update(employee: Employee): Observable<any> {
    return this.apiService.put<any>(`${this.endpoint}/${employee.id}`, employee);
  }
 
  delete(id: number): Observable<any> {
    return this.apiService.delete<any>(`${this.endpoint}/${id}`);
  }
}

Управление состоянием с помощью RxJS или специальных библиотек



В простых Angular-приложениях я часто использую сервисы с Subject/BehaviorSubject для управления состоянием:

TypeScript
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
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { Employee } from '../models/employee';
import { EmployeeService } from './employee.service';
import { tap, catchError } from 'rxjs/operators';
 
@Injectable({
  providedIn: 'root'
})
export class EmployeeStoreService {
  private employees$ = new BehaviorSubject<Employee[]>([]);
  private loading$ = new BehaviorSubject<boolean>(false);
  private error$ = new BehaviorSubject<any>(null);
 
  constructor(private employeeService: EmployeeService) { }
 
  getEmployees(): Observable<Employee[]> {
    return this.employees$.asObservable();
  }
 
  getLoading(): Observable<boolean> {
    return this.loading$.asObservable();
  }
 
  getError(): Observable<any> {
    return this.error$.asObservable();
  }
 
  loadEmployees(filters: any = {}): void {
    this.loading$.next(true);
    this.error$.next(null);
    
    this.employeeService.getAll(filters).pipe(
      tap(response => {
        this.employees$.next(response.items);
        this.loading$.next(false);
      }),
      catchError(error => {
        this.error$.next(error);
        this.loading$.next(false);
        throw error;
      })
    ).subscribe();
  }
 
  addEmployee(employee: Partial<Employee>): void {
    this.loading$.next(true);
    this.error$.next(null);
    
    this.employeeService.create(employee).pipe(
      tap(newEmployee => {
        const currentEmployees = this.employees$.getValue();
        this.employees$.next([...currentEmployees, newEmployee]);
        this.loading$.next(false);
      }),
      catchError(error => {
        this.error$.next(error);
        this.loading$.next(false);
        throw error;
      })
    ).subscribe();
  }
 
  // Остальные методы (update, delete...)
}
Для более сложных приложений я рекомендую использовать специализированные библиотеки управления состоянием, такие как NgRx или Akita. Они предоставляют более строгий и предсказуемый способ управления данными. Однажды я работал над проектом, где мы начали без библиотеки состояний, а потом, когда приложение выросло, пришлось все переписывать на NgRx. Это было... не весело. Лучше принимать решение об архитектуре состояний заранее.

Создание переиспользуемых UI компонентов



Создание переиспользуемых компонентов - одна из сильных сторон Angular. Например, можно создать компонент для отображения списка с возможностью пагинации и сортировки:

TypeScript
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
import { Component, Input, Output, EventEmitter } from '@angular/core';
 
@Component({
  selector: 'app-data-table',
  templateUrl: './data-table.component.html'
})
export class DataTableComponent<T> {
  @Input() items: T[] = [];
  @Input() columns: { key: string, label: string }[] = [];
  @Input() totalItems = 0;
  @Input() pageSize = 10;
  @Input() currentPage = 1;
  @Input() loading = false;
  
  @Output() pageChange = new EventEmitter<number>();
  @Output() sortChange = new EventEmitter<{ key: string, direction: 'asc' | 'desc' }>();
  
  // Методы для пагинации и сортировки...
}
[/CSHARP]
 
А вот соответствующий шаблон:
 
[/CSHARP]html
<div class="data-table">
  <table>
    <thead>
      <tr>
        <th *ngFor="let column of columns" 
            (click)="onSort(column.key)">
          {{ column.label }}
          <!-- Индикатор сортировки -->
        </th>
      </tr>
    </thead>
    <tbody>
      <tr *ngIf="loading">
        <td [attr.colspan]="columns.length" class="loading">Загрузка...</td>
      </tr>
      <tr *ngIf="!loading && items.length === 0">
        <td [attr.colspan]="columns.length" class="no-data">Нет данных</td>
      </tr>
      <tr *ngFor="let item of items">
        <td *ngFor="let column of columns">
          {{ item[column.key] }}
        </td>
      </tr>
    </tbody>
  </table>
  
  <!-- Пагинация -->
  <div class="pagination" *ngIf="totalItems > pageSize">
    <button (click)="onPageChange(currentPage - 1)" 
            [disabled]="currentPage === 1">Предыдущая</button>
    <!-- Номера страниц -->
    <button (click)="onPageChange(currentPage + 1)" 
            [disabled]="currentPage * pageSize >= totalItems">Следующая</button>
  </div>
</div>
В моей практике бывали случаи, когда переиспользуемые компоненты превращались в монстров с десятками параметров. Это не лучшая практика. Лучше создавать компоненты с чётко определенной и ограниченной ответственностью.

Компоненты для CRUD операций



Для нашего CRUD-приложения нам понадобятся как минимум три компонента:
1. Компонент списка сотрудников.
2. Компонент просмотра деталей сотрудника.
3. Компонент создания/редактирования сотрудника.
Вот пример компонента для создания/редактирования:

TypeScript
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
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { EmployeeService } from '../../services/employee.service';
import { Employee } from '../../models/employee';
 
@Component({
  selector: 'app-employee-form',
  templateUrl: './employee-form.component.html'
})
export class EmployeeFormComponent implements OnInit {
  form: FormGroup;
  employeeId: number | null = null;
  isEditMode = false;
  loading = false;
  submitted = false;
  
  constructor(
    private formBuilder: FormBuilder,
    private employeeService: EmployeeService,
    private route: ActivatedRoute,
    private router: Router
  ) {
    this.form = this.createForm();
  }
  
  ngOnInit(): void {
    this.employeeId = this.route.snapshot.params['id'] ? 
      +this.route.snapshot.params['id'] : null;
    this.isEditMode = !!this.employeeId;
    
    if (this.isEditMode) {
      this.loadEmployee();
    }
  }
  
  createForm(): FormGroup {
    return this.formBuilder.group({
      name: ['', [Validators.required]],
      email: ['', [Validators.required, Validators.email]],
      dateOfBirth: ['', [Validators.required]],
      address: ['', [Validators.required]],
      gender: ['', [Validators.required]],
      pinCode: ['', [Validators.required]]
    });
  }
  
  loadEmployee(): void {
    if (!this.employeeId) return;
    
    this.loading = true;
    this.employeeService.getById(this.employeeId).subscribe(
      employee => {
        this.form.patchValue({
          name: employee.name,
          email: employee.email,
          dateOfBirth: new Date(employee.dateOfBirth).toISOString().split('T')[0],
          address: employee.address,
          gender: employee.gender,
          pinCode: employee.pinCode
        });
        this.loading = false;
      },
      error => {
        console.error('Failed to load employee', error);
        this.loading = false;
      }
    );
  }
  
  onSubmit(): void {
    this.submitted = true;
    
    if (this.form.invalid) {
      return;
    }
    
    const employeeData = { ...this.form.value };
    
    this.loading = true;
    
    if (this.isEditMode) {
      this.employeeService.update({ id: this.employeeId, ...employeeData })
        .subscribe(
          () => {
            this.loading = false;
            this.router.navigate(['/employees']);
          },
          error => {
            console.error('Failed to update employee', error);
            this.loading = false;
          }
        );
    } else {
      this.employeeService.create(employeeData)
        .subscribe(
          () => {
            this.loading = false;
            this.router.navigate(['/employees']);
          },
          error => {
            console.error('Failed to create employee', error);
            this.loading = false;
          }
        );
    }
  }
}
А вот часть шаблона для этого компонента:

HTML5
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
<form [formGroup]="form" (ngSubmit)="onSubmit()">
  <div class="form-group">
    <label for="name">Имя</label>
    <input type="text" formControlName="name" id="name" class="form-control" 
           [ngClass]="{ 'is-invalid': submitted && form.controls.name.errors }" />
    <div *ngIf="submitted && form.controls.name.errors" class="invalid-feedback">
      <div *ngIf="form.controls.name.errors.required">Имя обязательно</div>
    </div>
  </div>
  
  <div class="form-group">
    <label for="email">Email</label>
    <input type="email" formControlName="email" id="email" class="form-control"
           [ngClass]="{ 'is-invalid': submitted && form.controls.email.errors }" />
    <div *ngIf="submitted && form.controls.email.errors" class="invalid-feedback">
      <div *ngIf="form.controls.email.errors.required">Email обязателен</div>
      <div *ngIf="form.controls.email.errors.email">Введите корректный email</div>
    </div>
  </div>
  
  <!-- Остальные поля формы... -->
  
  <div class="form-group">
    <button type="submit" class="btn btn-primary" [disabled]="loading">
      <span *ngIf="loading" class="spinner-border spinner-border-sm mr-1"></span>
      {{ isEditMode ? 'Сохранить' : 'Создать' }}
    </button>
    <a routerLink="/employees" class="btn btn-link">Отмена</a>
  </div>
</form>

Реактивные формы и их валидация



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

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
 
export function ageValidator(minAge: number): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    if (!control.value) {
      return null;
    }
    
    const birthDate = new Date(control.value);
    const today = new Date();
    let age = today.getFullYear() - birthDate.getFullYear();
    const monthDiff = today.getMonth() - birthDate.getMonth();
    
    if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
      age--;
    }
    
    return age < minAge ? { 'minAge': { required: minAge, actual: age } } : null;
  };
}
И применить его в форме:

TypeScript
1
2
3
4
this.form = this.formBuilder.group({
  // Другие поля...
  dateOfBirth: ['', [Validators.required, ageValidator(18)]]
});
Иногда бывает нужна асинхронная валидация, например, для проверки уникальности email:

TypeScript
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
import { Injectable } from '@angular/core';
import { AbstractControl, AsyncValidatorFn } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { map, catchError, debounceTime, take, switchMap } from 'rxjs/operators';
import { EmployeeService } from './employee.service';
 
@Injectable({
  providedIn: 'root'
})
export class EmployeeValidators {
  constructor(private employeeService: EmployeeService) { }
  
  uniqueEmail(employeeId?: number): AsyncValidatorFn {
    return (control: AbstractControl): Observable<{ [key: string]: any } | null> => {
      if (!control.value) {
        return of(null);
      }
      
      return of(control.value).pipe(
        debounceTime(300),
        take(1),
        switchMap(email => 
          this.employeeService.checkEmailExists(email, employeeId).pipe(
            map(exists => exists ? { 'emailExists': true } : null),
            catchError(() => of(null))
          )
        )
      );
    };
  }
}

Interceptors для обработки HTTP запросов и ошибок



Interceptors - это мощный механизм в Angular для глобальной обработки HTTP-запросов. Я использую их для добавления заголовков аутентификации, обработки ошибок и мониторинга:

TypeScript
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
import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { NotificationService } from '../services/notification.service';
 
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
  constructor(private notificationService: NotificationService) {}
 
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(req).pipe(
      catchError((error: HttpErrorResponse) => {
        let errorMsg = '';
        
        if (error.error instanceof ErrorEvent) {
          // Клиентская ошибка
          errorMsg = `Ошибка: ${error.error.message}`;
        } else {
          // Серверная ошибка
          errorMsg = `Код ошибки: ${error.status}, Сообщение: ${error.message}`;
          
          // Специальная обработка в зависимости от кода ошибки
          switch (error.status) {
            case 401:
              this.notificationService.showError('Необходима авторизация');
              // Здесь можно добавить редирект на страницу логина
              break;
            case 403:
              this.notificationService.showError('Доступ запрещен');
              break;
            case 404:
              this.notificationService.showError('Ресурс не найден');
              break;
            case 500:
              this.notificationService.showError('Внутренняя ошибка сервера');
              break;
            default:
              this.notificationService.showError('Произошла ошибка');
          }
        }
        
        console.error(errorMsg);
        return throwError(error);
      })
    );
  }
}
А вот еще один полезный интерцептор, который я часто добавляю в свои проекты - для отображения индикатора загрузки:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Injectable()
export class LoadingInterceptor implements HttpInterceptor {
  private requests: HttpRequest<any>[] = [];
  
  constructor(private loadingService: LoadingService) {}
  
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    this.requests.push(req);
    this.loadingService.setLoading(true);
    
    return next.handle(req).pipe(
      finalize(() => {
        const index = this.requests.indexOf(req);
        if (index !== -1) {
          this.requests.splice(index, 1);
        }
        this.loadingService.setLoading(this.requests.length > 0);
      })
    );
  }
}
Не забудьте зарегистрировать интерцепторы в модуле:

TypeScript
1
2
3
4
5
6
7
8
@NgModule({
  // ...
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true },
    { provide: HTTP_INTERCEPTORS, useClass: LoadingInterceptor, multi: true },
  ]
})
export class CoreModule { }

Использование RxJS операторов для управления потоками данных



RxJS - настоящая волшебная палочка для работы с асинхронными данными в Angular. Я не устаю восхищаться гибкостью этой библиотеки. Однажды мне пришлось реализовать функцию живого поиска с задержкой, отменой предыдущих запросов и обработкой ошибок. Без RxJS это была бы настоящая боль:

TypeScript
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
@Component({
  // ...
})
export class EmployeeSearchComponent implements OnInit, OnDestroy {
  searchControl = new FormControl('');
  employees$: Observable<Employee[]>;
  loading = false;
  private destroy$ = new Subject<void>();
  
  constructor(private employeeService: EmployeeService) {}
  
  ngOnInit(): void {
    this.employees$ = this.searchControl.valueChanges.pipe(
      // Не отправляем запрос при каждом нажатии клавиши
      debounceTime(300), 
      
      // Игнорируем если значение не изменилось
      distinctUntilChanged(),
      
      // Показываем индикатор загрузки
      tap(() => this.loading = true),
      
      // Преобразуем строку поиска в запрос к API
      switchMap(term => 
        term ? 
          this.employeeService.search(term).pipe(
            // Обрабатываем ошибку, но не завершаем поток
            catchError(() => of([]))
          ) : 
          of([])
      ),
      
      // Скрываем индикатор загрузки
      tap(() => this.loading = false),
      
      // Отписываемся при уничтожении компонента
      takeUntil(this.destroy$)
    );
  }
  
  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
}
Обратите внимание на оператор switchMap - он не только преобразует данные, но и отменяет предыдущие запросы, если пришел новый. Это очень важно для таких сценариев, как живой поиск, где последовательные быстрые запросы могут возвращаться в непредсказуемом порядке.
А вот еще один прекрасный пример - комбинирование нескольких потоков данных:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
combineLatest([
  this.route.params.pipe(map(params => params['id'])),
  this.authService.currentUser$
]).pipe(
  switchMap(([employeeId, currentUser]) => 
    this.employeeService.getById(employeeId).pipe(
      map(employee => ({
        employee,
        canEdit: currentUser && (
          currentUser.role === 'admin' || 
          currentUser.id === employee.managerId
        )
      }))
    )
  )
).subscribe(({ employee, canEdit }) => {
  this.employee = employee;
  this.canEdit = canEdit;
});
Этот код одновременно отслеживает изменения параметра маршрута и текущего пользователя, затем загружает данные сотрудника и определяет права доступа. Вся эта сложная логика выражена компактно и декларативно благодаря RxJS.

Оптимизация производительности Angular компонентов



С ростом приложения производительность становится все более важной. Вот несколько техник, которые я постоянно применяю для оптимизации Angular-приложений:

1. OnPush стратегия обнаружения изменений

TypeScript
1
2
3
4
5
6
7
8
@Component({
  selector: 'app-employee-card',
  templateUrl: './employee-card.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class EmployeeCardComponent {
  @Input() employee: Employee;
}
Это существенно снижает количество проверок изменений, особенно в длинных списках. Но помните, что при использовании OnPush нужно быть аккуратным с мутабельными объектами. Я однажды потратил полдня на поиск бага, когда компонент не обновлялся после изменений - оказалось, я мутировал объект вместо создания нового.

2. Трекинг элементов в ngFor с trackBy

HTML5
1
2
3
<div *ngFor="let employee of employees; trackBy: trackById">
  {{ employee.name }}
</div>
TypeScript
1
2
3
trackById(index: number, employee: Employee): number {
  return employee.id;
}
Эта простая функция значительно повышает производительность при обновлении длинных списков, поскольку Angular может идентифицировать, какие элементы добавлены/удалены/изменены, вместо того чтобы перерисовывать весь список.

3. Виртуальная прокрутка для длинных списков

HTML5
1
2
3
4
5
<cdk-virtual-scroll-viewport itemSize="50" class="viewport">
  <div *cdkVirtualFor="let employee of employees" class="employee-item">
    {{ employee.name }}
  </div>
</cdk-virtual-scroll-viewport>
Это решение от Angular CDK рендерит только видимые элементы списка, а не все сразу. Для списков в несколько тысяч элементов это может быть разницей между плавным UI и замершим браузером.

4. Ленивая загрузка модулей

TypeScript
1
2
3
4
5
6
const routes: Routes = [
  {
    path: 'employees',
    loadChildren: () => import('./employees/employees.module').then(m => m.EmployeesModule)
  }
];
Это стандартная практика, но удивительно, как часто о ней забывают. Ленивая загрузка модулей значительно ускоряет начальную загрузку приложения.

5. Мемоизация тяжелых вычислений с pure pipes

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Pipe({
  name: 'filterEmployees',
  pure: true
})
export class FilterEmployeesPipe implements PipeTransform {
  transform(employees: Employee[], searchTerm: string): Employee[] {
    if (!employees || !searchTerm) {
      return employees;
    }
    
    return employees.filter(employee => 
      employee.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
      employee.email.toLowerCase().includes(searchTerm.toLowerCase())
    );
  }
}
Pure pipes выполняют трансформацию только при изменении входных параметров, что делает их идеальным выбором для мемоизации результатов тяжелых вычислений.

Глобальная обработка ошибок и уведомлений пользователя



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

TypeScript
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
import { Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Subject } from 'rxjs';
 
export interface Notification {
  message: string;
  type: 'success' | 'error' | 'warning' | 'info';
  duration?: number;
}
 
@Injectable({
  providedIn: 'root'
})
export class NotificationService {
  private notificationSubject = new Subject<Notification>();
  notifications$ = this.notificationSubject.asObservable();
  
  constructor(private snackBar: MatSnackBar) {
    this.notifications$.subscribe(notification => {
      this.showSnackbar(notification);
    });
  }
  
  showSuccess(message: string, duration = 3000): void {
    this.notificationSubject.next({ message, type: 'success', duration });
  }
  
  showError(message: string, duration = 5000): void {
    this.notificationSubject.next({ message, type: 'error', duration });
  }
  
  showWarning(message: string, duration = 4000): void {
    this.notificationSubject.next({ message, type: 'warning', duration });
  }
  
  showInfo(message: string, duration = 3000): void {
    this.notificationSubject.next({ message, type: 'info', duration });
  }
  
  private showSnackbar(notification: Notification): void {
    this.snackBar.open(notification.message, 'Закрыть', {
      duration: notification.duration,
      panelClass: [INLINE]notification-${notification.type}[/INLINE]
    });
  }
}
Для глобальной обработки ошибок я создаю специальный обработчик:

TypeScript
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
import { ErrorHandler, Injectable, Injector } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { NotificationService } from './notification.service';
import { LoggingService } from './logging.service';
 
@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
  constructor(private injector: Injector) {}
  
  handleError(error: any): void {
    const notificationService = this.injector.get(NotificationService);
    const loggingService = this.injector.get(LoggingService);
    
    let message = 'Произошла неизвестная ошибка';
    let stackTrace = '';
    
    if (error instanceof HttpErrorResponse) {
      // Серверная ошибка
      message = error.error?.message || `${error.status}: ${error.statusText}`;
    } else if (error instanceof Error) {
      // Клиентская ошибка
      message = error.message;
      stackTrace = error.stack || '';
    }
    
    // Уведомляем пользователя
    notificationService.showError(message);
    
    // Логируем ошибку
    loggingService.logError(message, stackTrace);
    
    // Не забываем вывести в консоль для разработчика
    console.error(error);
  }
}
И регистрирую его в модуле:

TypeScript
1
2
3
4
5
6
7
@NgModule({
  // ...
  providers: [
    { provide: ErrorHandler, useClass: GlobalErrorHandler }
  ]
})
export class AppModule { }
Эти инструменты значительно упрощают обработку ошибок и информирование пользователя о состоянии приложения. Никогда не забывайте о том, что пользователи не должны видеть технические сообщения об ошибках - они должны получать понятную информацию о том, что произошло и что им делать дальше.

Интеграция и тестирование



Нажмите на изображение для увеличения
Название: CRUD REST API на Angular и ASP.NET Core 5.jpg
Просмотров: 70
Размер:	234.9 Кб
ID:	11110

После создания серверной и клиентской частей приложения наступает, пожалуй, самый интересный и одновременно раздражающий этап - интеграция и тестирование. Помню, как в одном из моих проектов мы потратили две недели на разработку компонентов и целый месяц на то, чтобы заставить их корректно взаимодействовать. "Оно работает на моей машине!" - классика жанра, которую я слышал от разработчиков слишком много раз.

Мокирование API для разработки и тестирования



Разработка фронтенда часто идёт параллельно с бэкендом, и для фронтендеров критически важно иметь стабильное API. Мой любимый подход - использование сервисов-моков в Angular:

TypeScript
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
import { Injectable } from '@angular/core';
import { Observable, of, throwError } from 'rxjs';
import { delay } from 'rxjs/operators';
import { Employee } from '../models/employee';
 
@Injectable()
export class MockEmployeeService {
  private employees: Employee[] = [
    { id: 1, name: 'Иван Иванов', email: 'ivan@example.com', dateOfBirth: new Date(1990, 1, 1), gender: '0', address: 'Москва', pinCode: '123456' },
    { id: 2, name: 'Анна Петрова', email: 'anna@example.com', dateOfBirth: new Date(1992, 5, 15), gender: '1', address: 'Санкт-Петербург', pinCode: '654321' },
    // Другие тестовые данные...
  ];
 
  getAll(): Observable<any> {
    return of({
      items: this.employees,
      totalCount: this.employees.length,
      page: 1,
      pageSize: 10
    }).pipe(delay(500)); // Имитируем задержку сети
  }
 
  getById(id: number): Observable<Employee> {
    const employee = this.employees.find(e => e.id === id);
    if (!employee) {
      return throwError({ status: 404, message: 'Employee not found' });
    }
    return of(employee).pipe(delay(300));
  }
 
  create(employee: Partial<Employee>): Observable<Employee> {
    const newId = Math.max(...this.employees.map(e => e.id)) + 1;
    const newEmployee = { id: newId, ...employee } as Employee;
    this.employees.push(newEmployee);
    return of(newEmployee).pipe(delay(700));
  }
 
  update(employee: Employee): Observable<any> {
    const index = this.employees.findIndex(e => e.id === employee.id);
    if (index === -1) {
      return throwError({ status: 404, message: 'Employee not found' });
    }
    this.employees[index] = { ...employee };
    return of({}).pipe(delay(600));
  }
 
  delete(id: number): Observable<any> {
    const index = this.employees.findIndex(e => e.id === id);
    if (index === -1) {
      return throwError({ status: 404, message: 'Employee not found' });
    }
    this.employees.splice(index, 1);
    return of({}).pipe(delay(400));
  }
}
Этот мок-сервис позволяет фронтенд-разработчикам продолжать работу, даже если API ещё не готов или недоступен. Я часто настраиваю систему так, чтобы в режиме разработки использовались моки, а в продакшене - реальные сервисы:

TypeScript
1
2
3
4
5
6
7
8
9
10
@NgModule({
  // ...
  providers: [
    {
      provide: EmployeeService,
      useClass: environment.useMocks ? MockEmployeeService : EmployeeService
    }
  ]
})
export class AppModule { }
Другой подход, который я люблю - использование инструментов вроде json-server, который создаёт полноценный REST API на основе JSON-файла. Это особенно удобно, когда нужно быстро прототипировать интерфейс:

Bash
1
2
3
4
5
6
7
8
9
10
11
12
13
# Установка json-server
npm install -g json-server
 
# Создаем файл db.json
{
  "employees": [
    { "id": 1, "name": "Иван Иванов", "email": "ivan@example.com" },
    { "id": 2, "name": "Анна Петрова", "email": "anna@example.com" }
  ]
}
 
# Запускаем сервер
json-server --watch db.json --port 3000
Теперь у нас есть рабочий REST API с эндпоинтами для всех CRUD-операций!

Соединение фронтенда с API



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

TypeScript
1
2
3
4
5
6
7
8
9
10
11
// environment.ts (development)
export const environment = {
  production: false,
  apiUrl: 'http://localhost:5000/api'
};
 
// environment.prod.ts
export const environment = {
  production: true,
  apiUrl: 'https://your-production-api.com/api'
};
В процессе интеграции я всегда уделяю особое внимание обработке граничных случаев. Нет ничего хуже, чем приложение, которое просто перестаёт работать, если сервер не отвечает. Я создаю специальные компоненты для отображения разных состояний:

TypeScript
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
@Component({
  selector: 'app-loading',
  template: '<div class="spinner">Загрузка данных...</div>'
})
export class LoadingComponent { }
 
@Component({
  selector: 'app-error',
  template: `
    <div class="error-container">
      <h2>Произошла ошибка</h2>
      <p>{{ message }}</p>
      <button (click)="retry.emit()">Повторить</button>
    </div>
  `
})
export class ErrorComponent {
  @Input() message: string = 'Не удалось загрузить данные';
  @Output() retry = new EventEmitter<void>();
}
 
@Component({
  selector: 'app-empty-state',
  template: `
    <div class="empty-container">
      <p>{{ message }}</p>
      <button *ngIf="showAddButton" (click)="add.emit()">Добавить</button>
    </div>
  `
})
export class EmptyStateComponent {
  @Input() message: string = 'Нет данных для отображения';
  @Input() showAddButton = false;
  @Output() add = new EventEmitter<void>();
}
Затем я использую эти компоненты в своих шаблонах:

HTML5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<ng-container *ngIf="employeeState$ | async as state">
  <app-loading *ngIf="state.loading"></app-loading>
  
  <app-error 
    *ngIf="state.error" 
    [message]="state.error" 
    (retry)="loadEmployees()">
  </app-error>
  
  <ng-container *ngIf="!state.loading && !state.error">
    <app-empty-state 
      *ngIf="state.data.length === 0"
      message="У вас пока нет сотрудников" 
      [showAddButton]="true"
      (add)="addEmployee()">
    </app-empty-state>
    
    <app-employee-list 
      *ngIf="state.data.length > 0"
      [employees]="state.data"
      (delete)="deleteEmployee($event)">
    </app-employee-list>
  </ng-container>
</ng-container>
Такой подход значительно улучшает UX приложения, делая его более отзывчивым и информативным.

Настройка Docker контейнеров для развертывания



Docker кардинально изменил подход к развертыванию приложений. "Работает на моей машине" больше не является оправданием, когда вы используете контейнеры. Я создаю Dockerfile для ASP.NET Core API:

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
 
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["CrudApi/CrudApi.csproj", "CrudApi/"]
RUN dotnet restore "CrudApi/CrudApi.csproj"
COPY . .
WORKDIR "/src/CrudApi"
RUN dotnet build "CrudApi.csproj" -c Release -o /app/build
 
FROM build AS publish
RUN dotnet publish "CrudApi.csproj" -c Release -o /app/publish
 
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "CrudApi.dll"]
И Dockerfile для Angular приложения:

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Stage 1: Build the Angular application
FROM node:16 as build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
RUN npm run build -- --configuration production
 
# Stage 2: Serve the application with nginx
FROM nginx:alpine
COPY --from=build /app/dist/crud-client /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Затем создаю docker-compose.yml для запуска всего стека:

YAML
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
version: '3.8'
 
services:
  api:
    build:
      context: ./backend
      dockerfile: Dockerfile
    ports:
      - "5000:80"
    depends_on:
      - db
    environment:
      - ConnectionStrings__DefaultConnection=Server=db;Database=EmployeeDB;User=sa;Password=YourStrong@Passw0rd;
      - ASPNETCORE_ENVIRONMENT=Production
 
  client:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    ports:
      - "4200:80"
    depends_on:
      - api
 
  db:
    image: mcr.microsoft.com/mssql/server:2019-latest
    ports:
      - "1433:1433"
    environment:
      - ACCEPT_EULA=Y
      - SA_PASSWORD=YourStrong@Passw0rd
    volumes:
      - dbdata:/var/opt/mssql
 
volumes:
  dbdata:
Это позволяет в одну команду развернуть всё приложение:

Bash
1
docker-compose up -d
Однажды я работал в компании, где каждый разработчик настраивал окружение по-своему. Когда мы перешли на Docker, количество багов с пометкой "не воспроизводится" уменьшилось на 70%! Это было как глоток свежего воздуха.

Обработка асинхронных операций



В приложениях с REST API почти все операции асинхронны, и их правильная обработка критически важна. Я придерживаюсь нескольких паттернов:

1. Состояние загрузки и ошибок

TypeScript
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
interface State<T> {
  loading: boolean;
  error: string | null;
  data: T;
}
 
function createInitialState<T>(initialData: T): State<T> {
  return {
    loading: false,
    error: null,
    data: initialData
  };
}
 
@Component({
  // ...
})
export class EmployeeListComponent implements OnInit {
  state$: Observable<State<Employee[]>>;
 
  ngOnInit(): void {
    this.state$ = this.employeeService.getAll().pipe(
      map(data => ({ loading: false, error: null, data })),
      startWith({ loading: true, error: null, data: [] }),
      catchError(error => of({ loading: false, error: error.message, data: [] }))
    );
  }
}
2. Параллельные запросы

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ngOnInit(): void {
  forkJoin({
    employees: this.employeeService.getAll(),
    departments: this.departmentService.getAll(),
    positions: this.positionService.getAll()
  }).pipe(
    catchError(error => {
      this.notificationService.showError('Ошибка загрузки данных');
      return throwError(error);
    })
  ).subscribe(({ employees, departments, positions }) => {
    this.employees = employees;
    this.departments = departments;
    this.positions = positions;
    this.isLoading = false;
  });
}
3. Последовательные зависимые запросы

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ngOnInit(): void {
  this.activatedRoute.params.pipe(
    map(params => params['id']),
    switchMap(id => this.employeeService.getById(id)),
    switchMap(employee => {
      this.employee = employee;
      return this.departmentService.getById(employee.departmentId);
    }),
    catchError(error => {
      this.notificationService.showError('Ошибка загрузки данных');
      return throwError(error);
    })
  ).subscribe(department => {
    this.department = department;
    this.isLoading = false;
  });
}
Правильная обработка асинхронных операций требует глубокого понимания RxJS и его операторов. Однажды я потратил целый день, пытаясь понять, почему приложение делает множество дублирующих запросов. Оказалось, я использовал mergeMap вместо switchMap, и каждое изменение параметров маршрута добавляло новую подписку, не отменяя предыдущую.

Мониторинг ошибок с Application Insights или Sentry



Продакшн-приложение без мониторинга - это как самолёт без радара. Я предпочитаю использовать либо Azure Application Insights для .NET-проектов, либо Sentry для кроссплатформенного мониторинга. Интеграция Sentry с Angular до безобразия проста:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import * as Sentry from '@sentry/angular';
 
Sentry.init({
  dsn: 'https://your-dsn-key@sentry.io/project-id',
  integrations: [
    new Sentry.BrowserTracing({
      routingInstrumentation: Sentry.routingInstrumentation,
    }),
  ],
  tracesSampleRate: 1.0, // В продакшене уменьшить до 0.2-0.5
});
 
@NgModule({
  // ...
  providers: [
    { provide: ErrorHandler, useValue: Sentry.createErrorHandler() }
  ]
})
export class AppModule {}
Однажды благодаря Sentry я обнаружил странную ошибку, которая происходила только у пользователей с Safari на iOS. Без подобного мониторинга я бы никогда не догадался, что проблема в специфической особенности движка браузера, который по-особому обрабатывал некоторые выражения в JavaScript.

Кеширование и оптимизация сетевых запросов



Для оптимизации производительности клиента я использую несколько уровней кеширования:

1. HTTP-кеширование с помощью директив заголовков:

C#
1
2
3
4
5
6
// На сервере
[ResponseCache(Duration = 60, Location = ResponseCacheLocation.Any)]
public async Task<ActionResult<IEnumerable<EmployeeDto>>> GetStaticData()
{
    // ...
}
2. Кеш в Angular-сервисе с инвалидацией:

TypeScript
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
export class CachingService {
  private cache = new Map<string, CacheEntry<any>>();
 
  get<T>(key: string): T | null {
    const entry = this.cache.get(key);
    if (!entry) return null;
    
    if (Date.now() > entry.expiresAt) {
      this.cache.delete(key);
      return null;
    }
    
    return entry.value;
  }
 
  set<T>(key: string, value: T, ttlSeconds = 60): void {
    const expiresAt = Date.now() + ttlSeconds * 1000;
    this.cache.set(key, { value, expiresAt });
  }
  
  // Метод для инвалидации кеша при изменениях
  invalidateStartingWith(prefix: string): void {
    for (const key of this.cache.keys()) {
      if (key.startsWith(prefix)) {
        this.cache.delete(key);
      }
    }
  }
}
В одном из проектов я реализовал многоуровневую стратегию кеширования, где данные, редко меняющиеся (справочники, настройки), кешировались агрессивно, а часто меняющиеся (контент, генерируемый пользователями) - с коротким TTL. Это дало прирост производительности около 40% и снизило нагрузку на сервер.

Настройка HTTPS и SSL сертификатов



Безопасность должна быть приоритетом. Для работы с HTTPS в ASP.NET Core я использую следующую конфигурацию:

C#
1
2
3
4
5
6
7
8
9
10
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // Принудительное перенаправление HTTP на HTTPS
    app.UseHttpsRedirection();
    
    // Добавление заголовков безопасности
    app.UseHsts();
    
    // Остальной middleware...
}
Для локальной разработки можно использовать самоподписанные сертификаты:

Bash
1
dotnet dev-certs https --trust
А для продакшена я предпочитаю Let's Encrypt - бесплатные и автоматически обновляемые сертификаты. Настроил их однажды на продакшен-сервере и забыл про проблему истекающих сертификатов на ближайшие годы!

Безопасность и защита от XSS атак



Angular имеет встроенную защиту от XSS-атак, автоматически экранируя потенциально опасный контент. Но дополнительно я всегда реализую:

1. Content Security Policy (CSP) - настраиваю заголовки на сервере:

C#
1
2
3
4
5
6
7
8
app.Use(async (context, next) =>
{
    context.Response.Headers.Add(
        "Content-Security-Policy",
        "default-src 'self'; script-src 'self'; object-src 'none'; img-src 'self' data:; style-src 'self' 'unsafe-inline'");
    
    await next();
});
2. Защиту от CSRF-атак - в Angular это тоже встроено, но нужно настроить на сервере:

C#
1
2
3
4
services.AddAntiforgery(options =>
{
    options.HeaderName = "X-XSRF-TOKEN";
});
В Angular этот токен будет автоматически добавляться к запросам через HttpClient.

Unit и интеграционное тестирование



Для тестирования Angular-приложений я использую Jasmine и Karma:

TypeScript
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
describe('EmployeeService', () => {
  let service: EmployeeService;
  let httpMock: HttpTestingController;
 
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [EmployeeService]
    });
 
    service = TestBed.inject(EmployeeService);
    httpMock = TestBed.inject(HttpTestingController);
  });
 
  it('should retrieve employees', () => {
    const mockEmployees = [{ id: 1, name: 'Test Employee' }];
 
    service.getAll().subscribe(employees => {
      expect(employees.items).toEqual(mockEmployees);
    });
 
    const req = httpMock.expectOne(`${environment.apiUrl}/employees`);
    expect(req.request.method).toBe('GET');
    req.flush({ items: mockEmployees, totalCount: 1, page: 1, pageSize: 10 });
  });
  
  afterEach(() => {
    httpMock.verify();
  });
});
Для компонентов я предпочитаю модульные тесты с неглубоким рендерингом:

TypeScript
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
describe('EmployeeListComponent', () => {
  let component: EmployeeListComponent;
  let fixture: ComponentFixture<EmployeeListComponent>;
  let mockEmployeeService: jasmine.SpyObj<EmployeeService>;
 
  beforeEach(() => {
    mockEmployeeService = jasmine.createSpyObj('EmployeeService', ['getAll', 'delete']);
    mockEmployeeService.getAll.and.returnValue(of({
      items: [{ id: 1, name: 'Test' }],
      totalCount: 1,
      page: 1,
      pageSize: 10
    }));
 
    TestBed.configureTestingModule({
      declarations: [EmployeeListComponent],
      providers: [
        { provide: EmployeeService, useValue: mockEmployeeService }
      ]
    }).compileComponents();
 
    fixture = TestBed.createComponent(EmployeeListComponent);
    component = fixture.componentInstance;
  });
 
  it('should load employees on init', () => {
    fixture.detectChanges();
    expect(mockEmployeeService.getAll).toHaveBeenCalled();
    expect(component.employees.length).toBe(1);
  });
});
Тестирование помогло мне не раз избежать регрессий при рефакторинге. Помню случай, когда небольшое изменение в компоненте нарушило отображение данных в таблице, но тесты сразу поймали эту проблему.

Профилирование и советы по отладке



Для анализа производительности Angular-приложений я использую:
1. Angular DevTools - расширение для Chrome, которое позволяет исследовать дерево компонентов, профилировать производительность и отлаживать состояние приложения.
2. Lighthouse - для комплексного анализа производительности, доступности и SEO.
3. Профилировщик Chrome - для анализа использования памяти и CPU.

Мой топ-3 совета по отладке Angular:
  • Используйте точки останова в Chrome DevTools не только для JS, но и для DOM-изменений и XHR-запросов.
  • Логируйте жизненные циклы компонентов при отладке проблем с рендерингом.
  • Отслеживайте утечки памяти с помощью вкладки Memory в DevTools, особенно для компонентов с подписками на Observable.

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

Заключение: Анализ производительности и масштабируемости решения



Нажмите на изображение для увеличения
Название: CRUD REST API на Angular и ASP.NET Core 6.jpg
Просмотров: 62
Размер:	296.7 Кб
ID:	11111

Пройдя весь путь создания CRUD-приложения от проектирования до тестирования, я хочу поделиться некоторыми наблюдениями о производительности и масштабируемости разработанного решения. Начну с того, что архитектура, которую мы выбрали - разделение на фронтенд на Angular и бэкенд на ASP.NET Core с RESTful API - уже сама по себе обеспечивает хорошую масштабируемость. Каждый компонент может масштабироваться независимо в зависимости от нагрузки. Однажды я работал над проектом финансовой компании, где мы столкнулись с неожиданным ростом пользовательской базы. Благодаря разделению на микросервисы мы смогли быстро увеличить количество инстансов API без необходимости масштабировать всю систему целиком. Это сэкономило нам десятки тысяч долларов на инфраструктуре.

Бутылочные горлышки производительности



В любой системе всегда есть узкие места. В нашем решении такими потенциальными точками могут стать:

1. База данных - при росте трафика операции чтения/записи могут стать узким местом. Решения: использование кэширования (как мы реализовали с Redis), оптимизация запросов, индексация, партиционирование данных.
2. Обработка запросов API - высокая нагрузка на сервер может привести к задержкам. Здесь помогает горизонтальное масштабирование через балансировщик нагрузки и оптимизация работы самих контроллеров.
3. Фронтенд - тяжелый интерфейс может плохо работать на слабых устройствах. Ленивая загрузка модулей, оптимизация бандлов, использование стратегии OnPush для обнаружения изменений значительно улучшают производительность.

Масштабирование в облаке



Наше решение прекрасно подходит для развертывания в современных облачных средах. Я рекомендую:
  1. Использовать контейнеризацию через Docker для унифицированного развертывания,
  2. Настроить автомасштабирование в Kubernetes или Azure App Service,
  3. Применять CDN для статических ресурсов Angular-приложения,
  4. Использовать репликацию базы данных для распределения нагрузки чтения,

ASP.NET MVC 4,ASP.NET MVC 4.5 и ASP.NET MVC 5 большая ли разница между ними?
Начал во всю осваивать технологию,теперь хочу с книжкой посидеть и вдумчиво перебрать всё то что...

Angular + ASP.Net Core не запускается проект
перестал запускаться ангуляр проект. Причем созданый с нуля тоже не работает. Ошибки: 1....

ASP.Net Core + Angular. Шаблон из VS
Собственно вот, по шаблону студии создал проект. Все настроил где надо - заработало. Теперь о...

Можно ли использовать ASP.Net Core Identity вместе с Angular?
Собственно вопрос в названии темы

Использование Identity Server и ASP .Net Core 3.00 с Angular
Приложение Angular ASP Net Core создано на основе шаблона VS2019 .NetCore 3.0 с аутентификацией и...

ASP.NET Core 2.2 Angular Windows аутентификация для IIS сервера
Не очень много знаю о реализации аутентификации для asp.net core, но у меня проект в котором...

Пагинация в ASP .NET Core- Angular 8
Есть проект созданный в связке Angular 8 - Core т.е. фронтенд-сервет, нужно сделать...

Социальная сеть на asp.NET Core + Angular 8
Вчера закончил свой первый тестовый проект Получил новую тестовую задачу - написать социальную...

Интересен ваше мнение asp.NET Core + Angular 8
Интересно ваше мнение и плиз аргументируйте его, в общем есть 2 проекта, сервер и клиент, приведу...

asp.net core +Angular 8 Siglanr
пытаюсь реализовать чат при помощи Signalr, для примера нашел исходник проекта установил все...

Как передать EnvironmentName при публикации приложения ASP.NET Core с Angular 2022
Я создал проект на основе руководства и столкнулся с проблемой. ...

ASP.NET Core: разный формат даты контроллера ASP.NET и AngularJS
Собственно, проблему пока еще не разруливал, но уже погуглил. Разный формат даты который использует...

Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
20. Мат мед. Абсентеизм как отдельный тип простоя
anaschu 29.05.2026
Апдейт модели: исправленные баги, абсентеизм и новые механизмы Продолжаю развивать ранее описанную модель рабочего коллектива на AnyLogic. За последние несколько дней был проведён серьёзный. . .
19. здоровье, усталость и психотип работника влияют на производительность предприятия, и наоборот, производительность на здоровье, усталось и психотип
anaschu 28.05.2026
Дискретно-событийная модель рабочего коллектива на AnyLogic: здоровье, выгорание, психотипы и микростимуляция Привет, коллеги. Хочу поделиться итогами нескольких недель работы над симуляционной. . .
"Прокси" для последовательного порта
Eddy_Em 28.05.2026
Эту штуку написал я достаточно давно. Но сейчас вот понадобилось настроить датчик грозы, но при этом не отключать его от "метеодемона". Соответственно, надо запустить этот "прокси": метеодемон будет. . .
Рефакторинг программы уравнивания.
Massaraksh7 26.05.2026
Пример по предыдущей записи в блоге. Но, надо заметить, что, во-первых, там оптимизация не только математики, но и работы с базой данных, и с графами, а во-вторых, это ещё не всё.
Использование TThread в Lazarus для математических вычислений.
Massaraksh7 25.05.2026
Производя рефакторинг своих программ на предмет ускорения их работы, обратил внимание на такой аспект, как сокращение времени матвычислений. Дело в том, что приходится работать с большими матрицами. . .
Модель здравосохранения 18. Чем здоровее работник, тем быстрее выгорает
anaschu 24.05.2026
Имитационная модель корпоративного здравоохранения: что показывает математика Сегодня в модели рабочего коллектива на AnyLogic появились три новые механики — выгорание через накопленную усталость,. . .
Модель здравосохранения 17. Планы на выгорание
anaschu 23.05.2026
Вот конкретная схема реализации: В классе Работник добавить: накопленнаяУсталость — растёт каждый час работы, снижается в перерывы и болезни коэффициентПрезентеизма — снижает продуктивность. . .
Изменение цветов в палитре gif файла aka фавикона
russiannick 23.05.2026
Изменение цветов в палитре gif файла, юзаемого как фавиконка в составе html-файла, помещенная в base64, средствами нативного Java Script, навеянное сном в майский день. Для работы необходим браузер,. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru