feat(templateMatcher): Добавлена проверка принадлежности связанного ЭК к КИИ, который имеет больший приоритет.
This commit is contained in:
@@ -6,7 +6,6 @@ using PARR.Core.Repositories.Interfaces.Job;
|
|||||||
using PARR.Core.Repositories.Interfaces.Unit;
|
using PARR.Core.Repositories.Interfaces.Unit;
|
||||||
using PARR.Core.Services.MatchingStatusService;
|
using PARR.Core.Services.MatchingStatusService;
|
||||||
using PARR.Core.Services.UnitFilterService;
|
using PARR.Core.Services.UnitFilterService;
|
||||||
using PARR.Core.Services.UnitFilterService.Models;
|
|
||||||
using PARR.Domain.Cache.Models;
|
using PARR.Domain.Cache.Models;
|
||||||
using PARR.Domain.Common.Rabbit.Messages;
|
using PARR.Domain.Common.Rabbit.Messages;
|
||||||
using PARR.Domain.Entities;
|
using PARR.Domain.Entities;
|
||||||
@@ -38,7 +37,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
private readonly ITemplateNameNormalizer templateNameNormalizer;
|
private readonly ITemplateNameNormalizer templateNameNormalizer;
|
||||||
private readonly ITemplateUpdaterMqSender templateUpdaterMqSender;
|
private readonly ITemplateUpdaterMqSender templateUpdaterMqSender;
|
||||||
private readonly IMatchingStatusService matchingStatusService;
|
private readonly IMatchingStatusService matchingStatusService;
|
||||||
|
private readonly IUnitInTemplateConflictMapper unitInTemplateConflictMapper;
|
||||||
|
|
||||||
public GroupedTemplateSynchronizer(
|
public GroupedTemplateSynchronizer(
|
||||||
ILogger<GroupedTemplateSynchronizer> logger,
|
ILogger<GroupedTemplateSynchronizer> logger,
|
||||||
@@ -55,7 +54,8 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
ITemplateDeactivator templateDeactivator,
|
ITemplateDeactivator templateDeactivator,
|
||||||
ITemplateNameNormalizer templateNameNormalizer,
|
ITemplateNameNormalizer templateNameNormalizer,
|
||||||
ITemplateUpdaterMqSender templateUpdaterMqSender,
|
ITemplateUpdaterMqSender templateUpdaterMqSender,
|
||||||
IMatchingStatusService matchingStatusService
|
IMatchingStatusService matchingStatusService,
|
||||||
|
IUnitInTemplateConflictMapper unitInTemplateConflictMapper
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
@@ -73,6 +73,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
this.templateNameNormalizer = templateNameNormalizer;
|
this.templateNameNormalizer = templateNameNormalizer;
|
||||||
this.templateUpdaterMqSender = templateUpdaterMqSender;
|
this.templateUpdaterMqSender = templateUpdaterMqSender;
|
||||||
this.matchingStatusService = matchingStatusService;
|
this.matchingStatusService = matchingStatusService;
|
||||||
|
this.unitInTemplateConflictMapper = unitInTemplateConflictMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
|
public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
|
||||||
@@ -236,16 +237,7 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.LogDebug("Построение обратного отображения: связанные юниты -> юниты, связанные с ними (после фильтрации).");
|
logger.LogDebug("Построение обратного отображения: связанные юниты -> юниты, связанные с ними (после фильтрации).");
|
||||||
var reverseMapping = await BuildReverseMappingAsync(finalFilteredUnitFilterResults, maxJob);
|
var reverseMapping = await unitInTemplateConflictMapper.BuildMappingAsync(finalFilteredUnitFilterResults, maxJob);
|
||||||
|
|
||||||
logger.LogDebug("Построено {Count} записей в обратном отображении.", reverseMapping.Count);
|
|
||||||
|
|
||||||
if (!reverseMapping.Any())
|
|
||||||
{
|
|
||||||
logger.LogInformation("После построения обратного отображения в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId);
|
|
||||||
await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после построения обратного отображения");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.LogDebug("Построено {Count} записей в обратном отображении.", reverseMapping.Count);
|
logger.LogDebug("Построено {Count} записей в обратном отображении.", reverseMapping.Count);
|
||||||
|
|
||||||
@@ -620,148 +612,5 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
return field.Id;
|
return field.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Строит обратное отображение: UnitId шаблона -> [UnitsInTemplate]
|
|
||||||
/// Решает конфликты, когда юнит из UnitsInTemplate может быть связан с несколькими UnitId шаблона.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="unitFilterResults">Результаты фильтрации, содержащие связи.</param>
|
|
||||||
/// <param name="maxJob">Job, используемый для определения направления связей (IsParentRelationships).</param>
|
|
||||||
/// <returns>Словарь, где ключ - это UnitId шаблона, а значение - список юнитов, входящих в него (UnitsInTemplate).</returns>
|
|
||||||
private async Task<Dictionary<Guid, List<Guid>>> BuildReverseMappingAsync(IEnumerable<UnitFilterResultDto> unitFilterResults, Job maxJob)
|
|
||||||
{
|
|
||||||
logger.LogDebug("Разрешение конфликта: определение, для какого UnitId выбрать каждый юнит из UnitsInTemplate.");
|
|
||||||
|
|
||||||
// 1. Собираем все потенциальные пары (relatedUnitId, unitInTemplateId)
|
|
||||||
var potentialAssignments = new Dictionary<Guid, List<Guid>>(); // relatedUnitId -> [unitInTemplateId, ...]
|
|
||||||
var allPotentialRelatedUnitIds = new HashSet<Guid>();
|
|
||||||
var allUnitInTemplateIds = new HashSet<Guid>();
|
|
||||||
|
|
||||||
foreach (var dto in unitFilterResults)
|
|
||||||
{
|
|
||||||
List<Guid> relatedUnitIds;
|
|
||||||
if (maxJob.IsParentRelationships == true)
|
|
||||||
{
|
|
||||||
relatedUnitIds = dto.Children.Select(c => c.UnitId).ToList(); // relatedUnitIds - это ChildUnitIds, которые станут UnitId шаблона
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
relatedUnitIds = dto.Parents.Select(p => p.UnitId).ToList(); // relatedUnitIds - это ParentUnitIds, которые станут UnitId шаблона
|
|
||||||
}
|
|
||||||
|
|
||||||
var unitInTemplateId = dto.Id; // dto.Id - это юнит, который прошел фильтры, он будет в UnitsInTemplate
|
|
||||||
|
|
||||||
allUnitInTemplateIds.Add(unitInTemplateId);
|
|
||||||
allPotentialRelatedUnitIds.UnionWith(relatedUnitIds); // Собираем все unique relatedUnitId
|
|
||||||
|
|
||||||
foreach (var relatedUnitId in relatedUnitIds)
|
|
||||||
{
|
|
||||||
if (!potentialAssignments.ContainsKey(relatedUnitId))
|
|
||||||
{
|
|
||||||
potentialAssignments[relatedUnitId] = new List<Guid>();
|
|
||||||
}
|
|
||||||
potentialAssignments[relatedUnitId].Add(unitInTemplateId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Загрузим имена всех potentialRelatedUnitIds для сортировки по алфавиту при равенстве связей
|
|
||||||
var relatedUnitNames = await unitService.Get()
|
|
||||||
.AsNoTracking()
|
|
||||||
.Where(u => allPotentialRelatedUnitIds.Contains(u.Id))
|
|
||||||
.ToDictionaryAsync(u => u.Id, u => u.Name ?? u.Id.ToString());
|
|
||||||
|
|
||||||
// 3. Для каждого unitInTemplateId, найти лучший relatedUnitId
|
|
||||||
var unitInTemplateToBestRelatedUnit = new Dictionary<Guid, Guid>(); // unitInTemplateId -> bestRelatedUnitId
|
|
||||||
|
|
||||||
foreach (var unitInTemplateId in allUnitInTemplateIds)
|
|
||||||
{
|
|
||||||
var candidates = potentialAssignments
|
|
||||||
.Where(kvp => kvp.Value.Contains(unitInTemplateId))
|
|
||||||
.Select(kvp => kvp.Key)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (candidates.Count == 1)
|
|
||||||
{
|
|
||||||
// Только один кандидат, просто назначаем
|
|
||||||
unitInTemplateToBestRelatedUnit[unitInTemplateId] = candidates[0];
|
|
||||||
}
|
|
||||||
else if (candidates.Count > 1)
|
|
||||||
{
|
|
||||||
// Несколько кандидатов, применяем правила: 1. Больше связей -> лучше. 2. По алфавиту.
|
|
||||||
Guid bestCandidate = candidates[0]; // Инициализируем первым кандидатом
|
|
||||||
|
|
||||||
// Загрузим количество связей для каждого кандидата
|
|
||||||
// Количество связей - это общее число юнитов (в Parents или Children) в UnitFilterResultDto, связанном с *этим* relatedUnitId
|
|
||||||
var candidateRelationshipCounts = new Dictionary<Guid, int>();
|
|
||||||
foreach (var candidateId in candidates)
|
|
||||||
{
|
|
||||||
// Найдем все dto, которые привели к этому candidateId
|
|
||||||
// Это dto.Id, у которых candidateId был в Parents (если IsParentRelationships) или в Children (если !IsParentRelationships)
|
|
||||||
var relevantDtos = unitFilterResults.Where(dto =>
|
|
||||||
{
|
|
||||||
if (maxJob.IsParentRelationships == true)
|
|
||||||
{
|
|
||||||
return dto.Children.Any(c => c.UnitId == candidateId);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return dto.Parents.Any(p => p.UnitId == candidateId);
|
|
||||||
}
|
|
||||||
}).ToList();
|
|
||||||
|
|
||||||
// Общее количество связей для этого candidateId - это сумма связей (Parents.Count или Children.Count) из *всех* relevantDtos
|
|
||||||
int totalRelationships = 0;
|
|
||||||
foreach (var relevantDto in relevantDtos)
|
|
||||||
{
|
|
||||||
if (maxJob.IsParentRelationships == true)
|
|
||||||
{
|
|
||||||
totalRelationships += relevantDto.Children.Count;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
totalRelationships += relevantDto.Parents.Count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
candidateRelationshipCounts[candidateId] = totalRelationships;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Применяем правило 1: больше связей -> лучше
|
|
||||||
int bestCount = candidateRelationshipCounts[bestCandidate];
|
|
||||||
foreach (var candidateId in candidates.Skip(1))
|
|
||||||
{
|
|
||||||
int candidateCount = candidateRelationshipCounts[candidateId];
|
|
||||||
if (candidateCount > bestCount ||
|
|
||||||
(candidateCount == bestCount && string.Compare(relatedUnitNames.GetValueOrDefault(candidateId, candidateId.ToString()), relatedUnitNames.GetValueOrDefault(bestCandidate, bestCandidate.ToString()), StringComparison.OrdinalIgnoreCase) < 0))
|
|
||||||
{
|
|
||||||
bestCandidate = candidateId;
|
|
||||||
bestCount = candidateCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unitInTemplateToBestRelatedUnit[unitInTemplateId] = bestCandidate;
|
|
||||||
}
|
|
||||||
// else: если candidates.Count == 0 (что маловероятно, если dto.Id гарантированно связан), то unitInTemplateId не будет в unitInTemplateToBestRelatedUnit
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.LogDebug("Разрешение конфликта завершено. Найдено {Count} однозначных назначений.", unitInTemplateToBestRelatedUnit.Count);
|
|
||||||
|
|
||||||
// 4. Построение reverseMapping на основе решённых конфликтов
|
|
||||||
var reverseMapping = new Dictionary<Guid, List<Guid>>();
|
|
||||||
foreach (var assignmentKvp in unitInTemplateToBestRelatedUnit)
|
|
||||||
{
|
|
||||||
var unitInTemplateId = assignmentKvp.Key;
|
|
||||||
var bestRelatedUnitId = assignmentKvp.Value;
|
|
||||||
|
|
||||||
if (!reverseMapping.ContainsKey(bestRelatedUnitId))
|
|
||||||
{
|
|
||||||
reverseMapping[bestRelatedUnitId] = new List<Guid>();
|
|
||||||
}
|
|
||||||
reverseMapping[bestRelatedUnitId].Add(unitInTemplateId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return reverseMapping;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using PARR.Core.Repositories.Interfaces.Unit;
|
||||||
|
using PARR.Core.Services.UnitFilterService.Models;
|
||||||
|
using PARR.Domain.Entities.Job;
|
||||||
|
using PARR.TemplateMatcher.Services.Interfaces;
|
||||||
|
|
||||||
|
internal class UnitInTemplateConflictMapper : IUnitInTemplateConflictMapper
|
||||||
|
{
|
||||||
|
private readonly ILogger<UnitInTemplateConflictMapper> logger;
|
||||||
|
private readonly IUnitRepository unitRepository;
|
||||||
|
private readonly IUnitKiiUnitRepository unitKiiUnitRepository;
|
||||||
|
|
||||||
|
public UnitInTemplateConflictMapper(
|
||||||
|
ILogger<UnitInTemplateConflictMapper> logger,
|
||||||
|
IUnitRepository unitRepository,
|
||||||
|
IUnitKiiUnitRepository unitKiiUnitRepository)
|
||||||
|
{
|
||||||
|
this.logger = logger;
|
||||||
|
this.unitRepository = unitRepository;
|
||||||
|
this.unitKiiUnitRepository = unitKiiUnitRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Dictionary<Guid, List<Guid>>> BuildMappingAsync(
|
||||||
|
IEnumerable<UnitFilterResultDto> unitFilterResults,
|
||||||
|
Job maxJob,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
logger.LogDebug("Начало разрешения конфликтов и построения маппинга для юнитов в шаблонах.");
|
||||||
|
|
||||||
|
var unitFilterResultsList = unitFilterResults.ToList();
|
||||||
|
if (!unitFilterResultsList.Any())
|
||||||
|
return new Dictionary<Guid, List<Guid>>();
|
||||||
|
|
||||||
|
var potentialAssignments = new Dictionary<Guid, List<Guid>>();
|
||||||
|
var allPotentialRelatedUnitIds = new HashSet<Guid>();
|
||||||
|
var allUnitInTemplateIds = new HashSet<Guid>();
|
||||||
|
|
||||||
|
foreach (var dto in unitFilterResultsList)
|
||||||
|
{
|
||||||
|
var relatedUnitIds = maxJob.IsParentRelationships == true
|
||||||
|
? dto.Children.Select(c => c.UnitId).ToList()
|
||||||
|
: dto.Parents.Select(p => p.UnitId).ToList();
|
||||||
|
|
||||||
|
var unitInTemplateId = dto.Id;
|
||||||
|
|
||||||
|
allUnitInTemplateIds.Add(unitInTemplateId);
|
||||||
|
allPotentialRelatedUnitIds.UnionWith(relatedUnitIds);
|
||||||
|
|
||||||
|
foreach (var relatedId in relatedUnitIds)
|
||||||
|
{
|
||||||
|
if (!potentialAssignments.TryGetValue(relatedId, out var list))
|
||||||
|
{
|
||||||
|
list = new List<Guid>();
|
||||||
|
potentialAssignments[relatedId] = list;
|
||||||
|
}
|
||||||
|
list.Add(unitInTemplateId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allPotentialRelatedUnitIds.Any())
|
||||||
|
return new Dictionary<Guid, List<Guid>>();
|
||||||
|
|
||||||
|
var relatedUnitNames = await unitRepository.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(u => allPotentialRelatedUnitIds.Contains(u.Id))
|
||||||
|
.ToDictionaryAsync(u => u.Id, u => u.Name ?? u.Id.ToString(), cancellationToken);
|
||||||
|
|
||||||
|
// Загружаем идентификаторы КИИ через ваш реальный репозиторий
|
||||||
|
var kiiUnitIds = new HashSet<Guid>(await unitKiiUnitRepository.Get()
|
||||||
|
.Select(k => k.UnitId)
|
||||||
|
.ToListAsync(cancellationToken));
|
||||||
|
|
||||||
|
logger.LogDebug("Загружено {KiiCount} идентификаторов КИИ юнитов для приоритезации.", kiiUnitIds.Count);
|
||||||
|
|
||||||
|
var unitInTemplateToBestRelatedUnit = new Dictionary<Guid, Guid>();
|
||||||
|
|
||||||
|
foreach (var unitInTemplateId in allUnitInTemplateIds)
|
||||||
|
{
|
||||||
|
var candidates = potentialAssignments
|
||||||
|
.Where(kvp => kvp.Value.Contains(unitInTemplateId))
|
||||||
|
.Select(kvp => kvp.Key)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (candidates.Count == 0) continue;
|
||||||
|
if (candidates.Count == 1)
|
||||||
|
{
|
||||||
|
unitInTemplateToBestRelatedUnit[unitInTemplateId] = candidates[0];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var kiiCandidates = candidates.Where(id => kiiUnitIds.Contains(id)).ToList();
|
||||||
|
var candidatesToConsider = kiiCandidates.Any() ? kiiCandidates : candidates;
|
||||||
|
|
||||||
|
var relationshipCounts = new Dictionary<Guid, int>();
|
||||||
|
foreach (var candidateId in candidatesToConsider)
|
||||||
|
{
|
||||||
|
int totalRelationships = 0;
|
||||||
|
foreach (var dto in unitFilterResultsList)
|
||||||
|
{
|
||||||
|
if (maxJob.IsParentRelationships == true && dto.Children.Any(c => c.UnitId == candidateId))
|
||||||
|
totalRelationships += dto.Children.Count;
|
||||||
|
else if (maxJob.IsParentRelationships == false && dto.Parents.Any(p => p.UnitId == candidateId))
|
||||||
|
totalRelationships += dto.Parents.Count;
|
||||||
|
}
|
||||||
|
relationshipCounts[candidateId] = totalRelationships;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bestCandidate = candidatesToConsider[0];
|
||||||
|
int bestCount = relationshipCounts[bestCandidate];
|
||||||
|
|
||||||
|
foreach (var candidateId in candidatesToConsider.Skip(1))
|
||||||
|
{
|
||||||
|
int currentCount = relationshipCounts[candidateId];
|
||||||
|
if (currentCount > bestCount ||
|
||||||
|
(currentCount == bestCount && string.Compare(
|
||||||
|
relatedUnitNames.GetValueOrDefault(candidateId, candidateId.ToString()),
|
||||||
|
relatedUnitNames.GetValueOrDefault(bestCandidate, bestCandidate.ToString()),
|
||||||
|
StringComparison.OrdinalIgnoreCase) < 0))
|
||||||
|
{
|
||||||
|
bestCandidate = candidateId;
|
||||||
|
bestCount = currentCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unitInTemplateToBestRelatedUnit[unitInTemplateId] = bestCandidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogDebug("Разрешение конфликтов завершено. Найдено {Count} однозначных назначений.", unitInTemplateToBestRelatedUnit.Count);
|
||||||
|
|
||||||
|
var reverseMapping = new Dictionary<Guid, List<Guid>>();
|
||||||
|
foreach (var kvp in unitInTemplateToBestRelatedUnit)
|
||||||
|
{
|
||||||
|
if (!reverseMapping.TryGetValue(kvp.Value, out var list))
|
||||||
|
{
|
||||||
|
list = new List<Guid>();
|
||||||
|
reverseMapping[kvp.Value] = list;
|
||||||
|
}
|
||||||
|
list.Add(kvp.Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return reverseMapping;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using PARR.Core.Services.UnitFilterService.Models;
|
||||||
|
using PARR.Domain.Entities.Job;
|
||||||
|
|
||||||
|
namespace PARR.TemplateMatcher.Services.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Разрешает конфликты при сопоставлении юнитов к шаблонам и строит итоговую карту связей.
|
||||||
|
/// </summary>
|
||||||
|
public interface IUnitInTemplateConflictMapper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Строит маппинг: UnitId шаблона -> [Список юнитов в UnitsInTemplate].
|
||||||
|
/// При наличии нескольких кандидатов для одного юнита выбирается лучший по приоритету:
|
||||||
|
/// 1. Наличие в таблице UnitKiiUnit
|
||||||
|
/// 2. Наибольшее количество связей
|
||||||
|
/// 3. Алфавитный порядок имени юнита
|
||||||
|
/// </summary>
|
||||||
|
Task<Dictionary<Guid, List<Guid>>> BuildMappingAsync(
|
||||||
|
IEnumerable<UnitFilterResultDto> unitFilterResults,
|
||||||
|
Job maxJob,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,6 +34,7 @@ namespace PARR.TemplateMatcher
|
|||||||
services.AddTransient<ITemplateNameNormalizer, TemplateNameNormalizer>();
|
services.AddTransient<ITemplateNameNormalizer, TemplateNameNormalizer>();
|
||||||
services.AddTransient<ITemplateUpdaterMqSender, TemplateUpdaterMqSender>();
|
services.AddTransient<ITemplateUpdaterMqSender, TemplateUpdaterMqSender>();
|
||||||
services.AddTransient<ITemplateDeactivator, TemplateDeactivator>();
|
services.AddTransient<ITemplateDeactivator, TemplateDeactivator>();
|
||||||
|
services.AddTransient<IUnitInTemplateConflictMapper, UnitInTemplateConflictMapper>();
|
||||||
services.AddTransient<ITemplateSynchronizer, SimpleTemplateSynchronizer>();
|
services.AddTransient<ITemplateSynchronizer, SimpleTemplateSynchronizer>();
|
||||||
services.AddTransient<ITemplateSynchronizer, GroupedTemplateSynchronizer>();
|
services.AddTransient<ITemplateSynchronizer, GroupedTemplateSynchronizer>();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user