using Microsoft.EntityFrameworkCore; using Microsoft.SelfService.Portal.Core.API.Context; using Microsoft.SelfService.Portal.Core.API.Interfaces; using Microsoft.SelfService.Portal.Core.API.JsonDocuments; using Microsoft.SelfService.Portal.Core.API.Models; using System.Text.Json; namespace Microsoft.SelfService.Portal.Core.API.Repository { public class TemplateRepository : ITemplateInterface { private readonly DataContext _context; public TemplateRepository(DataContext context) { _context = context; } public ICollection GetTemplates() { var templates = _context.Templates .AsNoTracking() .Include(template => template.TemplateVersions) .ToList(); foreach (var template in templates) { ApplyPublishedVersion(template); } return templates; } public TemplateModel GetTemplateById(Guid Id) { var template = _context.Templates .AsNoTracking() .Include(t => t.TemplateVersions) .Where(t => t.Id == Id) .FirstOrDefault(); if (template != null) { ApplyPublishedVersion(template); } return template; } public bool AddTemplateById(TemplateModel template) { if (template.Id == Guid.Empty) { template.Id = Guid.NewGuid(); } var document = ConfigurationDocumentValidator.NormalizeAndValidate(template.JSONData, ConfigurationDocumentKind.Template); template.JSONData = document.JsonData; var version = new TemplateVersionModel { Id = Guid.NewGuid(), TemplateId = template.Id, Version = string.IsNullOrWhiteSpace(template.Version) ? "1.0.0" : template.Version, SchemaVersion = document.SchemaVersion, IsPublished = true, PublishedAt = DateTime.UtcNow, PublishedBy = template.CreatedBy }; version.SetJsonData(document.JsonData); version.TemplateRevisions.Add(CreateTemplateRevision(version.Id, document.JsonData, document.SchemaVersion, true, template.CreatedBy)); template.TemplateVersions.Add(version); _context.Add(template); return SaveChanges(); } public bool EditTemplateById(TemplateModel template) { var existingTemplate = _context.Templates .Include(existing => existing.TemplateVersions) .FirstOrDefault(existing => existing.Id == template.Id); if (existingTemplate == null) { return false; } var document = ConfigurationDocumentValidator.NormalizeAndValidate(template.JSONData, ConfigurationDocumentKind.Template); var jsonData = document.JsonData; var publishedVersion = existingTemplate.TemplateVersions.FirstOrDefault(version => version.IsPublished); var currentRevision = publishedVersion == null ? null : GetCurrentTemplateRevision(publishedVersion.Id); var jsonChanged = currentRevision == null || !string.Equals(currentRevision.JsonData, jsonData, StringComparison.Ordinal); existingTemplate.Name = template.Name; existingTemplate.Version = template.Version; existingTemplate.Description = template.Description; existingTemplate.JSONData = jsonData; existingTemplate.TemplateCategoryId = template.TemplateCategoryId; existingTemplate.DeploymentRuleId = template.DeploymentRuleId; existingTemplate.Modified = DateTime.UtcNow; existingTemplate.ModifiedBy = template.ModifiedBy; if (jsonChanged) { var version = publishedVersion; if (version == null || !string.Equals(version.Version, template.Version, StringComparison.OrdinalIgnoreCase)) { var versionName = GetUniqueVersionName( existingTemplate.Id, string.IsNullOrWhiteSpace(template.Version) ? DateTime.UtcNow.ToString("yyyyMMddHHmmss") : template.Version); version = new TemplateVersionModel { Id = Guid.NewGuid(), TemplateId = existingTemplate.Id, Version = versionName, SchemaVersion = document.SchemaVersion, IsPublished = true, PublishedAt = DateTime.UtcNow, PublishedBy = template.ModifiedBy }; existingTemplate.TemplateVersions.Add(version); } version.SetJsonData(jsonData); version.SchemaVersion = document.SchemaVersion; version.IsPublished = true; version.PublishedAt = DateTime.UtcNow; version.PublishedBy = template.ModifiedBy; foreach (var existingVersion in existingTemplate.TemplateVersions.Where(existing => existing.Id != version.Id)) { existingVersion.IsPublished = false; } var revision = CreateTemplateRevision(version.Id, jsonData, document.SchemaVersion, true, template.ModifiedBy); _context.TemplateRevisions.Add(revision); } return SaveChanges(); } public bool DeleteTemplateById(TemplateModel template) { _context.Remove(template); return SaveChanges(); } public bool CheckTemplateById(Guid Id) { return _context.Templates .Any(t => t.Id == Id); } public TemplateModel GetTemplateByName(string Name) { return _context.Templates .AsNoTracking() .Where(t => t.Name == Name) .FirstOrDefault(); } public bool CheckTemplateByName(string Name) { return _context.Templates .Any(t => t.Name == Name); } public ICollection GetTemplateVersions(Guid templateId) { return _context.TemplateVersions .AsNoTracking() .Include(version => version.TemplateRevisions) .Where(version => version.TemplateId == templateId) .OrderByDescending(version => version.Created) .ToList(); } public TemplateVersionModel? GetTemplateVersionById(Guid templateId, Guid versionId) { return _context.TemplateVersions .AsNoTracking() .Include(version => version.TemplateRevisions) .FirstOrDefault(version => version.TemplateId == templateId && version.Id == versionId); } public TemplateVersionModel? GetPublishedTemplateVersion(Guid templateId) { return _context.TemplateVersions .AsNoTracking() .Include(version => version.TemplateRevisions) .FirstOrDefault(version => version.TemplateId == templateId && version.IsPublished); } public ICollection GetTemplateRevisions(Guid templateId, Guid versionId) { if (!_context.TemplateVersions.Any(version => version.TemplateId == templateId && version.Id == versionId)) { return new List(); } return _context.TemplateRevisions .AsNoTracking() .Include(revision => revision.ConfigurationDefinitions) .Where(revision => revision.TemplateVersionId == versionId) .OrderByDescending(revision => revision.RevisionNumber) .ToList(); } public TemplateRevisionModel? GetTemplateRevisionById(Guid templateId, Guid versionId, Guid revisionId) { if (!_context.TemplateVersions.Any(version => version.TemplateId == templateId && version.Id == versionId)) { return null; } return _context.TemplateRevisions .AsNoTracking() .Include(revision => revision.ConfigurationDefinitions) .FirstOrDefault(revision => revision.TemplateVersionId == versionId && revision.Id == revisionId); } public TemplateRevisionModel? GetCurrentTemplateRevision(Guid versionId) { return _context.TemplateRevisions .AsNoTracking() .FirstOrDefault(revision => revision.TemplateVersionId == versionId && revision.IsCurrent); } public bool AddTemplateVersion(Guid templateId, TemplateVersionModel version, bool publish) { if (!_context.Templates.Any(template => template.Id == templateId)) { return false; } version.Id = version.Id == Guid.Empty ? Guid.NewGuid() : version.Id; version.TemplateId = templateId; version.Version = string.IsNullOrWhiteSpace(version.Version) ? "1.0.0" : version.Version; var document = ConfigurationDocumentValidator.NormalizeAndValidate( version.JsonData, ConfigurationDocumentKind.Template, version.SchemaVersion); version.SchemaVersion = document.SchemaVersion; version.SetJsonData(document.JsonData); version.TemplateRevisions.Add(CreateTemplateRevision(version.Id, document.JsonData, document.SchemaVersion, publish, version.PublishedBy)); if (publish) { foreach (var existingVersion in _context.TemplateVersions.Where(existing => existing.TemplateId == templateId && existing.IsPublished)) { existingVersion.IsPublished = false; } version.IsPublished = true; version.PublishedAt = DateTime.UtcNow; } _context.TemplateVersions.Add(version); return SaveChanges(); } public bool AddTemplateRevision(Guid templateId, Guid versionId, TemplateRevisionModel revision, bool publish) { var version = _context.TemplateVersions .Include(existing => existing.TemplateRevisions) .FirstOrDefault(existing => existing.TemplateId == templateId && existing.Id == versionId); if (version == null) { return false; } var document = ConfigurationDocumentValidator.NormalizeAndValidate( revision.JsonData, ConfigurationDocumentKind.Template, revision.SchemaVersion); revision = CreateTemplateRevision(versionId, document.JsonData, document.SchemaVersion, publish, revision.PublishedBy); if (publish) { foreach (var existingRevision in _context.TemplateRevisions.Where(existing => existing.TemplateVersionId == versionId && existing.IsCurrent)) { existingRevision.IsCurrent = false; existingRevision.IsPublished = false; } version.SetJsonData(document.JsonData); version.SchemaVersion = document.SchemaVersion; version.PublishedAt = DateTime.UtcNow; version.PublishedBy = revision.PublishedBy; } _context.TemplateRevisions.Add(revision); return SaveChanges(); } public bool PublishTemplateVersion(Guid templateId, Guid versionId, string? publishedBy) { var version = _context.TemplateVersions .FirstOrDefault(existing => existing.TemplateId == templateId && existing.Id == versionId); if (version == null) { return false; } foreach (var existingVersion in _context.TemplateVersions.Where(existing => existing.TemplateId == templateId && existing.IsPublished)) { existingVersion.IsPublished = false; } version.IsPublished = true; version.PublishedAt = DateTime.UtcNow; version.PublishedBy = string.IsNullOrWhiteSpace(publishedBy) ? "System" : publishedBy; var currentRevision = _context.TemplateRevisions.FirstOrDefault(revision => revision.TemplateVersionId == version.Id && revision.IsCurrent); if (currentRevision != null) { currentRevision.IsPublished = true; currentRevision.PublishedAt = DateTime.UtcNow; currentRevision.PublishedBy = version.PublishedBy; } var template = _context.Templates.FirstOrDefault(existing => existing.Id == templateId); if (template != null) { template.Version = version.Version; template.JSONData = version.JsonData; template.Modified = DateTime.UtcNow; template.ModifiedBy = version.PublishedBy; } return SaveChanges(); } public bool CheckTemplateVersionById(Guid templateId, Guid versionId) { return _context.TemplateVersions .Any(version => version.TemplateId == templateId && version.Id == versionId); } public bool CheckTemplateVersionByVersion(Guid templateId, string version) { return _context.TemplateVersions .Any(existing => existing.TemplateId == templateId && existing.Version == version); } public bool SaveChanges() { var saved = _context.SaveChanges(); return saved > 0 ? true : false; } private static void ApplyPublishedVersion(TemplateModel template) { var publishedVersion = template.TemplateVersions?.FirstOrDefault(version => version.IsPublished); if (publishedVersion == null) { return; } template.Version = publishedVersion.Version; var currentRevision = publishedVersion.TemplateRevisions?.FirstOrDefault(revision => revision.IsCurrent); template.JSONData = currentRevision?.JsonData ?? publishedVersion.JsonData; } private TemplateRevisionModel CreateTemplateRevision(Guid templateVersionId, string jsonData, string schemaVersion, bool publish, string? publishedBy) { var revision = new TemplateRevisionModel { Id = Guid.NewGuid(), TemplateVersionId = templateVersionId, RevisionNumber = GetNextRevisionNumber(templateVersionId), SchemaVersion = schemaVersion, IsCurrent = true, IsPublished = publish, PublishedAt = publish ? DateTime.UtcNow : null, PublishedBy = publishedBy ?? "System" }; revision.SetJsonData(jsonData); foreach (var existingRevision in _context.TemplateRevisions.Where(existing => existing.TemplateVersionId == templateVersionId && existing.IsCurrent)) { existingRevision.IsCurrent = false; } foreach (var definition in ExtractConfigurationDefinitions(revision.Id, jsonData)) { revision.ConfigurationDefinitions.Add(definition); } return revision; } private int GetNextRevisionNumber(Guid templateVersionId) { return (_context.TemplateRevisions .Where(revision => revision.TemplateVersionId == templateVersionId) .Select(revision => (int?)revision.RevisionNumber) .Max() ?? 0) + 1; } private static IEnumerable ExtractConfigurationDefinitions(Guid revisionId, string jsonData) { using var document = JsonDocument.Parse(jsonData); var root = document.RootElement; foreach (var definition in ExtractConfigurationDefinitions(revisionId, root, "parameters", ConfigurationDefinitionKinds.Parameter)) { yield return definition; } foreach (var definition in ExtractConfigurationDefinitions(revisionId, root, "variables", ConfigurationDefinitionKinds.Variable)) { yield return definition; } } private static IEnumerable ExtractConfigurationDefinitions(Guid revisionId, JsonElement root, string propertyName, string kind) { if (!root.TryGetProperty(propertyName, out var definitions) || definitions.ValueKind != JsonValueKind.Object) { yield break; } foreach (var property in definitions.EnumerateObject()) { var propertiesJson = property.Value.GetRawText(); yield return new ConfigurationDefinitionModel { Id = Guid.NewGuid(), TemplateRevisionId = revisionId, Kind = kind, Name = property.Name, PropertiesJson = propertiesJson, DefinitionHash = TemplateVersionModel.ComputeSha256(propertiesJson) }; } } private string GetUniqueVersionName(Guid templateId, string version) { if (!CheckTemplateVersionByVersion(templateId, version)) { return version; } var timestamp = DateTime.UtcNow.ToString("yyyyMMddHHmmss"); var candidate = $"{version}-{timestamp}"; var counter = 1; while (CheckTemplateVersionByVersion(templateId, candidate)) { counter++; candidate = $"{version}-{timestamp}-{counter}"; } return candidate; } } }