using AutoMapper; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.SelfService.Portal.Core.API.Context; using Microsoft.SelfService.Portal.Core.API.Dto.ConfigurationDefinition.Add; using Microsoft.SelfService.Portal.Core.API.Dto.ConfigurationDefinition.Edit; using Microsoft.SelfService.Portal.Core.API.Dto.ConfigurationDefinition.Get; using Microsoft.SelfService.Portal.Core.API.Dto.ConfigurationValue.Get; using Microsoft.SelfService.Portal.Core.API.JsonDocuments; using Microsoft.SelfService.Portal.Core.API.Models; namespace Microsoft.SelfService.Portal.Core.API.Controllers { [Route("api/configuration-definitions")] [ApiController] public class ConfigurationDefinitionController : Controller { private readonly DataContext _context; private readonly IMapper _mapper; public ConfigurationDefinitionController(DataContext context, IMapper mapper) { _context = context; _mapper = mapper; } [HttpGet] [ProducesResponseType(200, Type = typeof(IEnumerable))] public IActionResult GetConfigurationDefinitions([FromQuery] Guid? templateRevisionId, [FromQuery] string? kind, [FromQuery] string? name) { var query = _context.ConfigurationDefinitions .AsNoTracking() .AsQueryable(); if (templateRevisionId.HasValue) query = query.Where(definition => definition.TemplateRevisionId == templateRevisionId.Value); if (!string.IsNullOrWhiteSpace(kind)) query = query.Where(definition => definition.Kind == kind); if (!string.IsNullOrWhiteSpace(name)) query = query.Where(definition => definition.Name == name); var definitions = query .OrderBy(definition => definition.TemplateRevisionId) .ThenBy(definition => definition.Kind) .ThenBy(definition => definition.Name) .ToList(); return Ok(_mapper.Map>(definitions)); } [HttpGet("{id}")] [ProducesResponseType(200, Type = typeof(GetConfigurationDefinitionDto))] [ProducesResponseType(404)] public IActionResult GetConfigurationDefinitionById(Guid id) { var definition = _context.ConfigurationDefinitions .AsNoTracking() .FirstOrDefault(existing => existing.Id == id); if (definition == null) return NotFound(); return Ok(_mapper.Map(definition)); } [HttpGet("{id}/values")] [ProducesResponseType(200, Type = typeof(IEnumerable))] [ProducesResponseType(404)] public IActionResult GetConfigurationDefinitionValues(Guid id) { if (!_context.ConfigurationDefinitions.Any(definition => definition.Id == id)) return NotFound(); var values = _context.ConfigurationValues .AsNoTracking() .Where(value => value.ConfigurationDefinitionId == id) .OrderBy(value => value.ScopeType) .ThenBy(value => value.SortOrder) .ToList(); return Ok(_mapper.Map>(values)); } [HttpPost] [ProducesResponseType(200, Type = typeof(Guid))] [ProducesResponseType(400)] [ProducesResponseType(422)] public IActionResult AddConfigurationDefinition([FromBody] AddConfigurationDefinitionDto configurationDefinition) { if (configurationDefinition == null) return BadRequest(ModelState); if (!TryPrepareConfigurationDefinition( configurationDefinition.TemplateRevisionId, configurationDefinition.Kind, configurationDefinition.Name, configurationDefinition.PropertiesJson, null, out var propertiesJson, out var errorField, out var errorMessage)) { ModelState.AddModelError(errorField, errorMessage); return BadRequest(ModelState); } if (ConfigurationDefinitionExists(configurationDefinition.TemplateRevisionId, configurationDefinition.Kind, configurationDefinition.Name, null)) { ModelState.AddModelError(nameof(configurationDefinition.Name), "Configuration definition already exists for this scope, kind and name."); return StatusCode(422, ModelState); } var model = _mapper.Map(configurationDefinition); model.Id = Guid.NewGuid(); model.PropertiesJson = propertiesJson; model.DefinitionHash = TemplateVersionModel.ComputeSha256(model.PropertiesJson); model.CreatedBy = User?.Identity?.Name ?? "System"; model.ModifiedBy = model.CreatedBy; _context.ConfigurationDefinitions.Add(model); _context.SaveChanges(); return Ok(model.Id); } [HttpPut("{id}")] [ProducesResponseType(204)] [ProducesResponseType(400)] [ProducesResponseType(404)] [ProducesResponseType(422)] public IActionResult UpdateConfigurationDefinition(Guid id, [FromBody] EditConfigurationDefinitionDto configurationDefinition) { if (configurationDefinition == null) return BadRequest(ModelState); var existing = _context.ConfigurationDefinitions.FirstOrDefault(definition => definition.Id == id); if (existing == null) return NotFound(); if (!TryPrepareConfigurationDefinition( configurationDefinition.TemplateRevisionId, configurationDefinition.Kind, configurationDefinition.Name, configurationDefinition.PropertiesJson, id, out var propertiesJson, out var errorField, out var errorMessage)) { ModelState.AddModelError(errorField, errorMessage); return BadRequest(ModelState); } if (ConfigurationDefinitionExists(configurationDefinition.TemplateRevisionId, configurationDefinition.Kind, configurationDefinition.Name, id)) { ModelState.AddModelError(nameof(configurationDefinition.Name), "Configuration definition already exists for this scope, kind and name."); return StatusCode(422, ModelState); } existing.TemplateRevisionId = configurationDefinition.TemplateRevisionId; existing.Kind = configurationDefinition.Kind; existing.Name = configurationDefinition.Name; existing.PropertiesJson = propertiesJson; existing.DefinitionHash = TemplateVersionModel.ComputeSha256(propertiesJson); existing.Modified = DateTime.UtcNow; existing.ModifiedBy = User?.Identity?.Name ?? "System"; _context.SaveChanges(); return NoContent(); } [HttpDelete("{id}")] [ProducesResponseType(204)] [ProducesResponseType(404)] [ProducesResponseType(409)] public IActionResult DeleteConfigurationDefinition(Guid id) { var definition = _context.ConfigurationDefinitions .Include(existing => existing.Values) .FirstOrDefault(existing => existing.Id == id); if (definition == null) return NotFound(); if (definition.Values.Any()) { ModelState.AddModelError(nameof(id), "Configuration definition cannot be deleted while configuration values reference it."); return StatusCode(409, ModelState); } _context.ConfigurationDefinitions.Remove(definition); _context.SaveChanges(); return NoContent(); } private bool TryPrepareConfigurationDefinition( Guid? templateRevisionId, string kind, string name, string propertiesJson, Guid? existingId, out string normalizedPropertiesJson, out string errorField, out string errorMessage) { normalizedPropertiesJson = "{}"; errorField = string.Empty; errorMessage = string.Empty; if (templateRevisionId.HasValue && !_context.TemplateRevisions.Any(revision => revision.Id == templateRevisionId.Value)) { errorField = nameof(templateRevisionId); errorMessage = "Template revision was not found."; return false; } if (!string.Equals(kind, ConfigurationDefinitionKinds.Parameter, StringComparison.OrdinalIgnoreCase) && !string.Equals(kind, ConfigurationDefinitionKinds.Variable, StringComparison.OrdinalIgnoreCase)) { errorField = nameof(kind); errorMessage = $"Configuration definition kind must be [{ConfigurationDefinitionKinds.Parameter}] or [{ConfigurationDefinitionKinds.Variable}]."; return false; } if (string.IsNullOrWhiteSpace(name)) { errorField = nameof(name); errorMessage = "Configuration definition name is required."; return false; } try { normalizedPropertiesJson = ConfigurationDocumentValidator.NormalizeAndValidate( propertiesJson, ConfigurationDocumentKind.Metadata).JsonData; } catch (ConfigurationDocumentValidationException ex) { errorField = nameof(propertiesJson); errorMessage = ex.Message; return false; } return true; } private bool ConfigurationDefinitionExists(Guid? templateRevisionId, string kind, string name, Guid? excludeId) { var query = _context.ConfigurationDefinitions.AsQueryable(); if (excludeId.HasValue) query = query.Where(definition => definition.Id != excludeId.Value); query = templateRevisionId.HasValue ? query.Where(definition => definition.TemplateRevisionId == templateRevisionId.Value) : query.Where(definition => definition.TemplateRevisionId == null); return query.Any(definition => definition.Kind == kind && definition.Name == name); } } }