diff --git a/.gitignore b/.gitignore index 61d23209..93728e20 100644 --- a/.gitignore +++ b/.gitignore @@ -363,4 +363,5 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +FodyWeavers.xsd +/PARR.AIHITRelationshipsSyncerWorker/MockData.json diff --git a/PARR.AIHITRelationshipsSyncer/AihitRelationshpsSyncerInstaller.cs b/PARR.AIHITRelationshipsSyncer/AihitRelationshpsSyncerInstaller.cs index 1b6939fb..60afd7ed 100644 --- a/PARR.AIHITRelationshipsSyncer/AihitRelationshpsSyncerInstaller.cs +++ b/PARR.AIHITRelationshipsSyncer/AihitRelationshpsSyncerInstaller.cs @@ -29,7 +29,7 @@ namespace PARR.AIHITRelationshipsSyncer services.AddTransient(); services.AddTransient(); - services.AddTransient(); + services.AddTransient(); } diff --git a/PARR.AIHITRelationshipsSyncer/Context/AIHITContext.cs b/PARR.AIHITRelationshipsSyncer/Context/AIHITContext.cs index 529660cd..6a9be518 100644 --- a/PARR.AIHITRelationshipsSyncer/Context/AIHITContext.cs +++ b/PARR.AIHITRelationshipsSyncer/Context/AIHITContext.cs @@ -9,5 +9,17 @@ namespace PARR.AIHITRelationshipsSyncer.Context public DbSet AihitDatas { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + //Делаем это чтобы EF не пытался трекать объкты полученые из хранимой процедуры + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(entity => + { + entity.HasNoKey(); + entity.ToView(null); + }); + } } } diff --git a/PARR.AIHITRelationshipsSyncer/Models/AihitData.cs b/PARR.AIHITRelationshipsSyncer/Models/AihitData.cs index f61e0961..590df0b2 100644 --- a/PARR.AIHITRelationshipsSyncer/Models/AihitData.cs +++ b/PARR.AIHITRelationshipsSyncer/Models/AihitData.cs @@ -3,11 +3,11 @@ using System.ComponentModel.DataAnnotations.Schema; namespace PARR.AIHITRelationshipsSyncer.Models { - [PrimaryKey(nameof(EKFindCode), nameof(ChildEk))] - public class AihitData + [PrimaryKey(nameof(ParentName), nameof(ChildName))] + public record AihitData { [Column("КОД_ПОИСКА_ЭК")] - public string? EKFindCode { get; set; } + public string? ParentName { get; set; } //[Column("СТАТУС")] //public string? Status { get; set; } @@ -21,6 +21,6 @@ namespace PARR.AIHITRelationshipsSyncer.Models //[Column("ТИП_ЭК")] //public string? EKType { get; set; } [Column("Дочерний ЭК")] - public string? ChildEk { get; set; } + public string? ChildName { get; set; } } } diff --git a/PARR.AIHITRelationshipsSyncer/RelationshipsSyncer.cs b/PARR.AIHITRelationshipsSyncer/RelationshipsSyncer.cs index 787e4546..f63915ff 100644 --- a/PARR.AIHITRelationshipsSyncer/RelationshipsSyncer.cs +++ b/PARR.AIHITRelationshipsSyncer/RelationshipsSyncer.cs @@ -1,7 +1,9 @@ using Microsoft.Extensions.Logging; +using PARR.AIHITRelationshipsSyncer.Models; using PARR.AIHITRelationshipsSyncer.Services.Interfaces; using PARR.AIHITRelationshipsSyncer.Settings; using PARR.BLL.Services.Interfaces; +using System.Text.Json; namespace PARR.AIHITRelationshipsSyncer { @@ -39,21 +41,62 @@ namespace PARR.AIHITRelationshipsSyncer { logger.LogInformation($"Начата загрузка иерархических связей между ЭК из АИХ ИТ"); - var aihitdata = aihitService.GetData(); + List? aihitDataList = null; - var aihitDataCount = aihitdata?.Count(); +#if DEBUG + const string mockFilePath = "MockData.json"; - if (aihitdata == null || aihitDataCount == 0) + //Пытаемся загрузить mock-данные + if (File.Exists(mockFilePath) && new FileInfo(mockFilePath).Length > 0) + { + logger.LogInformation("Загрузка данных из mock-файла: {MockFile}", mockFilePath); + try + { + var json = await File.ReadAllTextAsync(mockFilePath); + aihitDataList = JsonSerializer.Deserialize>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + logger.LogInformation("Mock-данные успешно загружены"); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Не уадлось десериализовать {MockFile}. Будет выполнен запрос в базу АИХ ИТ", mockFilePath); + aihitDataList = null; + } + } + + if (aihitDataList == null) + { + logger.LogInformation("Выполнение запроса в АИХ ИТ в режиме отладки"); + var rawData = aihitService.GetData(); + aihitDataList = rawData?.ToList(); + + if (aihitDataList != null && aihitDataList.Count > 0) + { + try + { + var json = JsonSerializer.Serialize(aihitDataList, new JsonSerializerOptions { WriteIndented = true }); + await File.WriteAllTextAsync(mockFilePath, json); + logger.LogInformation("Данные сохранены в {MockFile} для будущей отладки", mockFilePath); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Не удалось сохранить данные в {MockFile}", mockFilePath); + } + } + } +#else + //Release + var rawData = aihitService.GetData(); + aihitDataList = rawData?.ToList(); +#endif + + if (aihitDataList == null || aihitDataList.Count == 0) { logger.LogInformation("АИХ ИТ вернул пустые данные"); return; } - //File.WriteAllText("MockData.json", aihitdata.ToJson()); - //string text = File.OpenText("MockData.json").ReadToEnd(); - //var aihitdata = JsonSerializer.Deserialize>(text); - - await relationshipsSyncService.SyncAsync(aihitdata.ToList()); + logger.LogInformation("Получено {EkCount} записей для синхронизации", aihitDataList.Count); + await relationshipsSyncService.SyncAsync(aihitDataList); } catch (Exception ex) diff --git a/PARR.AIHITRelationshipsSyncer/Services/Implementations/RelationshipsSyncService.cs b/PARR.AIHITRelationshipsSyncer/Services/Implementations/RelationshipsSyncService.cs new file mode 100644 index 00000000..61785cbb --- /dev/null +++ b/PARR.AIHITRelationshipsSyncer/Services/Implementations/RelationshipsSyncService.cs @@ -0,0 +1,236 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using PARR.AIHITRelationshipsSyncer.Models; +using PARR.AIHITRelationshipsSyncer.Services.Interfaces; +using PARR.AIHITRelationshipsSyncer.Settings; +using PARR.DAL.Models.Unit; +using PARR.DAL.Services.Interfaces.Unit; + +namespace PARR.AIHITRelationshipsSyncer.Services.Implementations +{ + internal class RelationshipsSyncService : IRelationshipsSyncService + { + private readonly ILogger logger; + private readonly IUnitService unitService; + private readonly WorkerSettings workerSettings; + + public RelationshipsSyncService( + ILogger logger, + IUnitService unitService, + WorkerSettings workerSettings + ) + { + this.logger = logger; + this.unitService = unitService; + this.workerSettings = workerSettings; + } + + + public async Task SyncAsync(List aihitdata) + { + var validPairs = ValidateInput(aihitdata); + if (!validPairs.Any()) return; + + var sourceUnitNames = ExtractUnitNames(validPairs); + var existingUnits = await GetOrCreateUnitsAsync(sourceUnitNames); + + var (toRemove, toAdd) = await GetDeltaAsync(validPairs); + + await RemoveRelationshipsAsync(toRemove); + await AddRelationshipsAsync(toAdd, existingUnits); + + await FinalCommitAsync(toRemove.Count, toAdd.Count); + } + + private List<(string Parent, string Child)> ValidateInput(List data) + { + if (data == null || data.Count == 0) + { + logger.LogWarning("Получены пустые или null данные из АИХ ИТ. Пропускаю синхронизацию"); + return null!; + } + + var validPairs = data + .Where(t => !string.IsNullOrWhiteSpace(t.ParentName) && !string.IsNullOrWhiteSpace(t.ChildName)) + .Where(t => t.ParentName != t.ChildName) + .Select(t => ( + Parent: t.ParentName!.Trim().ToUpperInvariant(), + Child: t.ChildName!.Trim().ToUpperInvariant() + )) + .Distinct() + .ToList(); + if (validPairs.Count == 0) + { + logger.LogWarning("После нормализации не осталось валидных связей. Пропускаю синхронизацию"); + return new(); + } + + logger.LogInformation("Получено {Count} валидных связей", validPairs.Count); + return validPairs; + } + + + private static List ExtractUnitNames(List<(string Parent, string Child)> pairs) + { + return pairs + .SelectMany(x => new[] { x.Parent, x.Child }) + .Distinct() + .ToList(); + } + + + private async Task> GetOrCreateUnitsAsync(List names) + { + var existing = await unitService.Get() + .AsNoTracking() + .ToDictionaryAsync(u => u.Name, StringComparer.Ordinal); + + var missing = names.Except(existing.Keys).ToList(); + if (!missing.Any()) return existing; + + logger.LogInformation("Создание отсутствующих Unit:{Count}", missing.Count); + foreach (var name in missing) + { + var unit = new Unit + { + Id = Guid.NewGuid(), + Name = name, + DateCreated = DateTimeOffset.UtcNow + }; + + if (await unitService.CreateAsync(unit)) + { + logger.LogInformation("Создан Unit: {Name}", name); + existing[name] = unit; + } + else + logger.LogError("Не удалось создать Unit: {Name}", name); + } + + await unitService.CommitAsync(); + return existing; + } + + + private async Task<(List ToRemove, List<(string, string)> ToAdd)> GetDeltaAsync(List<(string Parent, string Child)> sourcePairs) + { + var allExistings = await unitService.Get() + .AsNoTracking() + .Include(u => u.ChildUnits).ThenInclude(r => r.ChildUnit) + .Where(u => u.ChildUnits.Any()) + .SelectMany(u => u.ChildUnits, (parent, relationship) => new ExistingRelationship + { + ParentName = parent.Name, + ChildName = relationship.ChildUnit!.Name, + Relationship = relationship + } + ) + .ToListAsync(); + + var sourceSet = new HashSet<(string, string)>(sourcePairs); + var existingSet = new HashSet<(string, string)>(allExistings.Select(x => (x.ParentName, x.ChildName))); + + var toRemove = allExistings.Where(r => !sourceSet.Contains((r.ParentName, r.ChildName))).ToList(); + var toAdd = sourcePairs.Where(p => !existingSet.Contains(p)).ToList(); + + logger.LogInformation("Связей для удаления: {ToRemove}, для добавления: {ToAdd}", toRemove.Count, toAdd.Count); + return (toRemove, toAdd); + } + + + private async Task RemoveRelationshipsAsync(List toRemove) + { + if (!toRemove.Any()) return; + + var parentNames = toRemove.Select(r => r.ParentName).Distinct().ToList(); + + var parents = await unitService.Get() + .Include(u => u.ChildUnits) + .Where(u => parentNames.Contains(u.Name)) + .ToDictionaryAsync(u => u.Name, StringComparer.Ordinal); + + foreach (var rel in toRemove) + { + if (!parents.TryGetValue(rel.ParentName, out var parent)) + { + logger.LogWarning("Родитель '{Parent}' не найден при попытке удаления связи - '{Child}'", rel.ParentName, rel.ChildName); + continue; + } + + var relationshipToRemove = parent.ChildUnits + .FirstOrDefault(r => r.ChildUnitId == rel.Relationship.ChildUnitId); + + if (relationshipToRemove != null) + { + logger.LogInformation("Удаление связи: '{Parent}' - '{Child}'", rel.ParentName, rel.ChildName); + parent.ChildUnits.Remove(relationshipToRemove); + } + else + { + logger.LogWarning("Связь '{Parent}'-'{Child}' не найдена в коллекции ChildUnits для удаления", rel.ParentName, rel.ChildName); + } + } + } + + private async Task AddRelationshipsAsync(List<(string Parent, string Child)> toAdd, Dictionary allUnits) + { + if (!toAdd.Any()) return; + + var parentNames = toAdd.Select(x => x.Parent).Distinct().ToList(); + var parents = await unitService.Get() + .Include(u => u.ChildUnits) + .Where(u => parentNames.Contains(u.Name)) + .ToDictionaryAsync(u => u.Name, StringComparer.Ordinal); + + foreach (var (Parent, Child) in toAdd) + { + if (!parents.TryGetValue(Parent, out var parent) || + !allUnits.TryGetValue(Child, out var child)) + { + logger.LogWarning("Пропущена связь '{Parent}' - '{Child}': Unit не найден", Parent, Child); + continue; + } + + if (parent!.ChildUnits.Any(r => r.ChildUnitId == child.Id)) + { + logger.LogWarning("Связь уже существует: '{Parent}' - '{Child}'", Parent, Child); + continue; + } + + parent.ChildUnits.Add(new UnitInUnit + { + ParentUnitId = parent.Id, + ChildUnitId = child.Id, + DateCreated = DateTimeOffset.UtcNow + }); + + logger.LogInformation("Добавлена связь: '{Parent}' - '{Child}'", Parent, Child); + } + } + + + private async Task FinalCommitAsync(int removedCount, int addedCount) + { + if (removedCount > 0 || addedCount > 0) + { + if (await unitService.CommitAsync()) + { + logger.LogInformation("Синхронизация завершена. Удалено: {Removed}, добавлено: {Added}", removedCount, addedCount); + } + else + logger.LogError("Не удалось сохранить изменения в БД"); + } + else + logger.LogInformation("Изменений не обнаружено"); + } + + private sealed class ExistingRelationship + { + public string ParentName { get; init; } + public string ChildName { get; init; } + public UnitInUnit Relationship { get; init; } + } + + } +} + diff --git a/PARR.AIHITRelationshipsSyncer/Services/Implementations/RelationshipsSyncSrevice.cs b/PARR.AIHITRelationshipsSyncer/Services/Implementations/RelationshipsSyncSrevice.cs deleted file mode 100644 index ddaf30f8..00000000 --- a/PARR.AIHITRelationshipsSyncer/Services/Implementations/RelationshipsSyncSrevice.cs +++ /dev/null @@ -1,219 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using PARR.AIHITRelationshipsSyncer.Models; -using PARR.AIHITRelationshipsSyncer.Services.Interfaces; -using PARR.AIHITRelationshipsSyncer.Settings; -using PARR.DAL.Models.Unit; -using PARR.DAL.Services.Interfaces.Unit; -using System.Reactive; -using Unit = PARR.DAL.Models.Unit.Unit; - -namespace PARR.AIHITRelationshipsSyncer.Services.Implementations -{ - internal class RelationshipsSyncSrevice : IRelationshipsSyncService - { - private readonly ILogger logger; - private readonly IUnitService unitService; - private readonly WorkerSettings workerSettings; - - public RelationshipsSyncSrevice(ILogger logger, - IUnitService unitService, - WorkerSettings workerSettings - ) - { - this.logger = logger; - this.unitService = unitService; - this.workerSettings = workerSettings; - } - - - public async Task SyncAsync(List aihitdata) - { - var groupedData = aihitdata.Where(t => t.EKFindCode != null && t.ChildEk != null).GroupBy(ad => ad.EKFindCode); - //var i = 0; - //var c = groupedData.Count(); - - var onlyUpdateTransactionCount = 0; - //Перебираем сгруппированные по родительскому ЭК данные - foreach (var parrent in groupedData) - { - //i = i+1; - - //Получаем имя родителя - var parentName = parrent.First().EKFindCode; - - //По имени получаем экземпляр Unit со всеми дочерними связями - var unit = await GetUnitByNameAsync(parentName!); - - if (unit == null) - { - logger.LogError($"Не удалось получиться родительский ЭК по имени{parentName}"); - return; - } - - //Из данных АИХ ИТ получаем имена всех дочерних ЭК - var childsName = parrent.Where(t => t.ChildEk != parentName && t.ChildEk != null).Select(t => t.ChildEk); - - childsName = childsName.Select(t => t?.Trim()).Distinct(); - - //Проходим циклом по полученному списку для актуализации связей в полученном нами экземпляре Unit - var isChanged = await SyncChildSUnitAsync(unit, childsName); - - if (!isChanged) onlyUpdateTransactionCount = ++onlyUpdateTransactionCount; - - //logger.LogInformation($"Обработка № {i.ToString()} из {c}. Выполнено только обновление поля DateSynced - {onlyUpdateTransactionCount}"); - - if (onlyUpdateTransactionCount == workerSettings.CommitAfter) - { - //применяем изменения в базе данных - if (!await unitService.CommitAsync()) - logger.LogError($"Не удалось изменить связи Unit после {workerSettings.CommitAfter} обновлений поля DateSynced"); - else - logger.LogDebug($"----- Изменены связи Unit после {workerSettings.CommitAfter} обновлений поля DateSynced -----"); - onlyUpdateTransactionCount = 0; - } - //применяем изменения в базе данных - if (isChanged && !await unitService.CommitAsync()) - logger.LogError($"Не удалось изменить связи Unit {unit.Name}"); - else - logger.LogDebug($"----- Изменены связи Unit: {unit.Name} -----"); - } - - //Удаляем детей у тех кого детей в выборке АИХ ИТ не оказалось - await RemoveWasteRelationships(groupedData); - - //применяем изменения в базе данных - if (!await unitService.CommitAsync()) - logger.LogError($"Не удалось удалить лишние связи"); - else - logger.LogInformation($"----- Иереархические связи актуализированы -----"); - } - - private async Task RemoveWasteRelationships(IEnumerable> groupedData) - { - var parentNames = groupedData.Select(t => t.Key?.ToLower().Trim()); - - var units = await unitService.Get() - .Include(t => t.ChildUnits).ToListAsync(); - - var parentsToDeleteChild = units.Where(t => parentNames.All(a => t.Name.ToLower() != a) && t.ChildUnits.Any()); - - if (parentsToDeleteChild.Any()) - { - foreach (var unit in parentsToDeleteChild) - { - DeleteChilds(unit, unit.ChildUnits); - } - } - } - - private async Task SyncChildSUnitAsync(Unit unit, IEnumerable childsName) - { - var isChanged = false; - - //удаляем лишние дочерние связи существующие только в БД - var childsToDelete = unit.ChildUnits.Where(t => !childsName.Any(a => t.ChildUnit?.Name.ToLower() == a!.ToLower())); - if (childsToDelete.Any()) - { - DeleteChilds(unit, childsToDelete); - if (!isChanged) - isChanged = true; - } - - //проверяем отсутсвующие связи - var absentChildrens = childsName.Where(c => !unit.ChildUnits.Any(x => x.ChildUnit?.Name.ToLower() == c?.ToLower())); - foreach (var absentChildren in absentChildrens) - { - var childUnit = await GetUnitByNameAsync(absentChildren!); - if (childUnit == null) - { - logger.LogError($"Ошибка получения дочернего ЭК. Из базы пришел NULL для ЭК{absentChildren}"); - return false; - } - - if (unit.ChildUnits.Any(x => x.ChildUnitId == childUnit.Id)) - logger.LogWarning($"Попытка создать существующую связь для {unit.Name} - {childUnit.Name}"); - else - { - unit.ChildUnits.Add( - new UnitInUnit - { - ParentUnitId = unit.Id, - ChildUnitId = childUnit.Id, - DateCreated = DateTimeOffset.UtcNow, - } - ); - logger.LogInformation($"----- Создана связь Unit {unit.Name} - {childUnit.Name}-----"); - } - - if (!isChanged) - isChanged = true; - } - - foreach (var child in unit.ChildUnits) - { - child.DateSynced = DateTimeOffset.UtcNow; - } - return isChanged; - } - - - /// - /// Удаление дочерних связей у ЭК - /// - /// - /// - private void DeleteChilds(Unit unit, IEnumerable unitToDeleteChilds) - { - foreach (var childUnit in unitToDeleteChilds) - { - logger.LogInformation($"----- Удаление связи Unit: {unit.Name} -----"); - unit.ChildUnits.Remove(childUnit); - } - } - - - /// - /// Создать новый ЭК по имени - /// - /// - /// - private async Task CreateUnit(string ekName) - { - var unit = new Unit - { - Name = ekName.Trim() - }; - - if (!await unitService.CreateAsync(unit)) - { - logger.LogError($"Не удалось создать Unit {unit.Name}"); - return null; - } - else - { - logger.LogInformation($"----- Создан Unit: {unit.Name} -----"); - return unit; - } - } - - - /// - /// Получить ЭК с дочерними связями по имени, если такого ещё нет создать - /// - /// - /// - private async Task GetUnitByNameAsync(string ekName) - { - if (!unitService.Get().AsNoTracking().Any(u => u.Name.ToLower() == ekName.ToLower().Trim())) - { - return await CreateUnit(ekName); - } - - return await unitService.Get() - .Include(u => u.ChildUnits) - .ThenInclude(t => t.ChildUnit) - .FirstAsync(u => u.Name.ToLower() == ekName.ToLower().Trim()); - } - } -} diff --git a/PARR.AIHITRelationshipsSyncerWorker/appsettings.Development.json b/PARR.AIHITRelationshipsSyncerWorker/appsettings.Development.json index b2dcdb67..a8276f5c 100644 --- a/PARR.AIHITRelationshipsSyncerWorker/appsettings.Development.json +++ b/PARR.AIHITRelationshipsSyncerWorker/appsettings.Development.json @@ -1,8 +1,11 @@ { - "Logging": { - "LogLevel": { + "Serilog": { + "MinimumLevel": { "Default": "Information", - "Microsoft.Hosting.Lifetime": "Information" + "Override": { + "Microsoft": "Debug", + "Microsoft.Hosting.Lifetime": "Information" + } } } } diff --git a/PARR.DAL/Context/DataContext.cs b/PARR.DAL/Context/DataContext.cs index 398fa87d..6a8476c8 100644 --- a/PARR.DAL/Context/DataContext.cs +++ b/PARR.DAL/Context/DataContext.cs @@ -108,6 +108,17 @@ namespace PARR.DAL.Context var dateCreated = new DateTimeOffset(2023, 05, 01, 0, 0, 0, new TimeSpan(0)); + #region Unit + //Добавляем ограничение по имени ЭК. нельзя писать прописными!!! + modelBuilder.Entity(f => + { + f.ToTable(t => t.HasCheckConstraint( + "CK_Units_Name_Upper", + "\"Name\" = UPPER(\"Name\") AND \"Name\" IS NOT NULL AND LENGTH(\"Name\")>0" + )); + }); + #endregion + #region ApplicationType modelBuilder.Entity(f => { diff --git a/PARR.DAL/Migrations/20251031040748_AddUnitNameUppercaseCheck.cs b/PARR.DAL/Migrations/20251031040748_AddUnitNameUppercaseCheck.cs new file mode 100644 index 00000000..6f8eca96 --- /dev/null +++ b/PARR.DAL/Migrations/20251031040748_AddUnitNameUppercaseCheck.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace PARR.DAL.Migrations +{ + /// + public partial class AddUnitNameUppercaseCheck : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddCheckConstraint( + name: "CK_Units_Name_Upper", + schema: "unit", + table: "Units", + sql: "\"Name\" = UPPER(\"Name\") AND \"Name\" IS NOT NULL AND LENGTH(\"Name\")>0"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropCheckConstraint( + name: "CK_Units_Name_Upper", + schema: "unit", + table: "Units"); + } + } +} diff --git a/PARR.DAL/Migrations/DataContextModelSnapshot.cs b/PARR.DAL/Migrations/DataContextModelSnapshot.cs index c3ba1b8f..9a2f521b 100644 --- a/PARR.DAL/Migrations/DataContextModelSnapshot.cs +++ b/PARR.DAL/Migrations/DataContextModelSnapshot.cs @@ -2768,6 +2768,8 @@ namespace PARR.DAL.Migrations b.ToTable("Units", "unit", t => { t.HasComment("Таблица с ЭК"); + + t.HasCheckConstraint("CK_Units_Name_Upper", "\"Name\" = UPPER(\"Name\") AND \"Name\" IS NOT NULL AND LENGTH(\"Name\")>0"); }); });