using Mapster; using foodmarket.Application.Catalog; using foodmarket.Domain.Catalog; namespace foodmarket.Application.Mapping; /// Sprint 20 / TD-3: централизованная Mapster-конфигурация /// для проекций domain → DTO. Используется через /// queryable.ProjectToType<TDto>(MapsterConfig.Config) /// — Mapster кодогенерирует SQL-friendly `.Select(...)`-выражение, /// что эквивалентно ручному `Select(p => new TDto(...))` по /// производительности, но компактнее в контроллере. /// /// Главное правило записи: /// 1. Все computed-поля (joins, агрегаты) — через `.Map(...)`. /// 2. Все коллекции (Prices, Barcodes) — через `.Map(...)` с /// `.Adapt<TItemDto>()` на каждом элементе. /// 3. PreserveReference = false (default) — для EF-проекций /// циклы не нужны. /// /// Регистрация — в `Program.cs`: /// /// var cfg = MapsterConfig.Build(); /// services.AddSingleton(cfg); /// services.AddScoped<IMapper, ServiceMapper>(); /// /// public static class MapsterConfig { private static TypeAdapterConfig? _cached; /// Singleton TypeAdapterConfig. Lazy-initialized чтобы /// тесты тоже могли вызвать без DI. public static TypeAdapterConfig Config => _cached ??= Build(); public static TypeAdapterConfig Build() { var cfg = new TypeAdapterConfig(); cfg.NewConfig() .ConstructUsing(src => new ProductBarcodeDto( src.Id, src.Code, src.Type, src.IsPrimary)); cfg.NewConfig() .ConstructUsing(src => new ProductPriceDto( src.Id, src.PriceTypeId, src.PriceType!.Name, src.Amount, src.CurrencyId, src.Currency!.Code)); cfg.NewConfig() .ConstructUsing(src => new ProductDto( src.Id, src.Name, src.Article, src.Description, src.UnitOfMeasureId, src.UnitOfMeasure!.Name, src.Vat, src.VatEnabled, src.ProductGroupId, src.ProductGroup!.Name, src.DefaultSupplierId, src.DefaultSupplier != null ? src.DefaultSupplier.Name : null, src.CountryOfOriginId, src.CountryOfOrigin != null ? src.CountryOfOrigin.Name : null, src.IsService, src.Packaging, src.IsMarked, src.MinStock, src.MaxStock, src.ReferencePrice, src.ReferencePriceUpdatedAt, src.PurchaseCurrencyId, src.PurchaseCurrency != null ? src.PurchaseCurrency.Code : null, src.Cost, src.LastSupplyAt, src.ImageUrl, src.IsArchived, src.IsAvailableForSale, src.Prices.Select(pr => new ProductPriceDto( pr.Id, pr.PriceTypeId, pr.PriceType!.Name, pr.Amount, pr.CurrencyId, pr.Currency!.Code)).ToList(), src.Barcodes.Select(b => new ProductBarcodeDto( b.Id, b.Code, b.Type, b.IsPrimary)).ToList())); cfg.NewConfig() .ConstructUsing(src => new CounterpartyDto( src.Id, src.Name, src.LegalName, src.Type, src.Bin, src.Iin, src.TaxNumber, src.CountryId, src.Country != null ? src.Country.Name : null, src.Address, src.Phone, src.Email, src.BankName, src.BankAccount, src.Bik, src.ContactPerson, src.Notes)); return cfg; } }