Neue Bildungspläne wachsen je Fach klassenstufenweise hoch, sodass Kurse abhängig vom Einschulungsjahr entweder dem alten oder dem neuen Bildungsplan folgen. CompetencyCatalogVersion (Fach + Name + IsDefault) macht das abbildbar: CompetencyDomain bekommt eine VersionId, Kurse wählen über LearningGroup.CompetencyCatalogVersionId (null = Standardversion des Fachs) welche Version gilt. Migration legt für bestehende Kataloge automatisch eine Standardversion an, damit alte Datenbanken unverändert weiterlaufen. MCP: neue Tools für Versions-CRUD (get/create/update/delete_competency_plan_version, update_group_competency_version), get_competency_catalog/create_competency_domain um optionalen versionId-Parameter erweitert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
336 lines
21 KiB
C#
336 lines
21 KiB
C#
using System.ComponentModel;
|
|
using System.Text;
|
|
using LehrerApp.Core.Interfaces;
|
|
using LehrerApp.Core.Models;
|
|
|
|
namespace LehrerApp.Desktop.Services.Mcp.Tools;
|
|
|
|
/// <summary>MCP-Tools für die Fach- und Kompetenzkatalog-Verwaltung (Nutzer-Nachtrag zu 4.5.28ff.,
|
|
/// später um Bildungsplan-Versionierung erweitert): <see cref="Subject"/>,
|
|
/// <see cref="CompetencyCatalogVersion"/> und <see cref="CompetencyDomain"/>/<see cref="CompetencyItem"/>
|
|
/// vollständig anlegen/ändern/löschen. Die Zuordnung von Kompetenzcodes zu einzelnen Stunden lebt
|
|
/// bewusst in <see cref="LessonPlanTools"/> (AddLessonCompetency/RemoveLessonCompetency), nicht hier -
|
|
/// sie gehört fachlich zur Stundenverwaltung, nicht zum Katalog selbst.
|
|
///
|
|
/// "delete_subject", "delete_competency_domain" und "delete_competency_plan_version" sind wie
|
|
/// "delete_lesson" (LessonPlanTools) endgültige Löschungen ohne Papierkorb (die
|
|
/// Repository-Implementierungen löschen direkt in der LiteDB-Collection statt über MoveToTrash) -
|
|
/// deshalb als Destructive registriert (siehe McpServerHostedService) und mit entsprechend
|
|
/// deutlicher Bestätigungsnachricht.</summary>
|
|
public class CompetencyTools(
|
|
ISubjectRepository subjects, ICompetencyDomainRepository domains,
|
|
ICompetencyCatalogVersionRepository versions, IMcpConfirmationService confirmation)
|
|
{
|
|
// ── Lesen ────────────────────────────────────────────────────────────────────────────────
|
|
|
|
[Description("Listet alle Fächer.")]
|
|
public List<SubjectDto> GetSubjects() =>
|
|
subjects.GetAll().Select(s => new SubjectDto(s.Id, s.Name, s.ShortName)).ToList();
|
|
|
|
[Description("Listet die Bildungsplan-Versionen eines Fachs (z.B. \"Bildungsplan 2016\" vs. \"Bildungsplan 2026\", während ein neuer Plan klassenstufenweise hochwächst). Genau eine Version je Fach ist die Standardversion.")]
|
|
public List<CompetencyPlanVersionDto> GetCompetencyPlanVersions(
|
|
[Description("Fach-ID, aus get_subjects.")] Guid subjectId) =>
|
|
versions.GetBySubject(subjectId)
|
|
.Select(v => new CompetencyPlanVersionDto(v.Id, v.SubjectId, v.Name, v.IsDefault, v.SortOrder))
|
|
.ToList();
|
|
|
|
[Description("Listet den Kompetenzkatalog (Bereiche mit Einzelkompetenzen) eines Fachs für eine Bildungsplan-Version, optional gefiltert auf eine Klassenstufe.")]
|
|
public List<CompetencyDomainDto> GetCompetencyCatalog(
|
|
[Description("Fach-ID, aus get_subjects.")] Guid subjectId,
|
|
[Description("Optionale Klassenstufe. Weglassen: alle Klassenstufen dieser Version.")] int? gradeLevel = null,
|
|
[Description("Bildungsplan-Version, aus get_competency_plan_versions. Weglassen: Standardversion des Fachs.")] Guid? versionId = null)
|
|
{
|
|
var effectiveVersionId = versionId ?? versions.GetOrCreateDefault(subjectId).Id;
|
|
var result = gradeLevel is not null
|
|
? domains.GetBySubjectAndGrade(subjectId, gradeLevel.Value, effectiveVersionId)
|
|
: domains.GetBySubjectAndVersion(subjectId, effectiveVersionId);
|
|
return result.Select(ToDto).ToList();
|
|
}
|
|
|
|
// ── Bildungsplan-Versionen (CompetencyCatalogVersion) ────────────────────────────────────
|
|
|
|
[Description("Legt eine neue Bildungsplan-Version für ein Fach an (z.B. \"Bildungsplan 2026\"), um einen neuen, parallel gültigen Kompetenzkatalog zu pflegen. Muss der Nutzer erst bestätigen.")]
|
|
public async Task<WriteResultDto> CreateCompetencyPlanVersion(
|
|
[Description("Fach-ID, aus get_subjects.")] Guid subjectId,
|
|
[Description("Name der Version, z.B. \"Bildungsplan 2026\".")] string name,
|
|
CancellationToken ct = default)
|
|
{
|
|
var subject = subjects.GetById(subjectId);
|
|
if (subject is null) return new WriteResultDto(false, null, "Unbekannte Fach-ID.");
|
|
if (string.IsNullOrWhiteSpace(name)) return new WriteResultDto(false, null, "Name der Version darf nicht leer sein.");
|
|
|
|
var message = $"Neue Bildungsplan-Version „{name.Trim()}“ für {subject.Name} anlegen?";
|
|
if (!await confirmation.ConfirmAsync("Version anlegen?", message, ct))
|
|
return new WriteResultDto(false, null, "Vom Nutzer abgelehnt oder nicht bestätigt.");
|
|
|
|
var version = new CompetencyCatalogVersion
|
|
{
|
|
SubjectId = subjectId, Name = name, SortOrder = versions.GetBySubject(subjectId).Count,
|
|
};
|
|
try { versions.Save(version); }
|
|
catch (ArgumentException ex) { return new WriteResultDto(false, null, ex.Message); }
|
|
return new WriteResultDto(true, version.Id, "Version gespeichert.");
|
|
}
|
|
|
|
[Description("Ändert Name/Standardstatus einer bestehenden Bildungsplan-Version. Nur angegebene Felder werden geändert.")]
|
|
public async Task<WriteResultDto> UpdateCompetencyPlanVersion(
|
|
[Description("ID der Version, aus get_competency_plan_versions.")] Guid versionId,
|
|
[Description("Neuer Name. Unverändert lassen: weglassen.")] string? name = null,
|
|
[Description("Auf true setzen, um diese Version zur Standardversion ihres Fachs zu machen (bisherige Standardversion wird automatisch zurückgenommen).")] bool? setAsDefault = null,
|
|
CancellationToken ct = default)
|
|
{
|
|
var version = versions.GetById(versionId);
|
|
if (version is null) return new WriteResultDto(false, null, "Unbekannte Versions-ID.");
|
|
|
|
var changes = new StringBuilder();
|
|
if (name is not null && name != version.Name) { changes.AppendLine($"Name: „{version.Name}“ → „{name}“"); version.Name = name; }
|
|
if (setAsDefault is true && !version.IsDefault) { changes.AppendLine("wird Standardversion des Fachs"); version.IsDefault = true; }
|
|
|
|
if (changes.Length == 0)
|
|
return new WriteResultDto(true, version.Id, "Keine Änderung nötig.");
|
|
|
|
if (!await confirmation.ConfirmAsync("Version ändern?", $"„{version.Name}“\n{changes}".TrimEnd(), ct))
|
|
return new WriteResultDto(false, null, "Vom Nutzer abgelehnt oder nicht bestätigt.");
|
|
|
|
try { versions.Save(version); }
|
|
catch (ArgumentException ex) { return new WriteResultDto(false, null, ex.Message); }
|
|
return new WriteResultDto(true, version.Id, "Version gespeichert.");
|
|
}
|
|
|
|
[Description("Löscht eine Bildungsplan-Version endgültig. Schlägt fehl, wenn sie noch Kompetenzbereiche oder Kurse referenziert, oder wenn sie die Standardversion ihres Fachs ist. Kein Papierkorb - nicht rückgängig zu machen. Muss der Nutzer erst bestätigen.")]
|
|
public async Task<WriteResultDto> DeleteCompetencyPlanVersion(
|
|
[Description("ID der Version.")] Guid versionId,
|
|
CancellationToken ct = default)
|
|
{
|
|
var version = versions.GetById(versionId);
|
|
if (version is null) return new WriteResultDto(false, null, "Unbekannte Versions-ID.");
|
|
|
|
var message = $"Die Bildungsplan-Version „{version.Name}“ wird endgültig gelöscht. Das kann NICHT rückgängig gemacht werden (kein Papierkorb für Versionen). Schlägt fehl, falls noch Kompetenzbereiche oder Kurse diese Version nutzen.";
|
|
if (!await confirmation.ConfirmAsync("Version endgültig löschen?", message, ct))
|
|
return new WriteResultDto(false, null, "Vom Nutzer abgelehnt oder nicht bestätigt.");
|
|
|
|
try { versions.Delete(versionId); }
|
|
catch (InvalidOperationException ex) { return new WriteResultDto(false, null, ex.Message); }
|
|
return new WriteResultDto(true, versionId, "Version gelöscht.");
|
|
}
|
|
|
|
// ── Fächer (Subject) ─────────────────────────────────────────────────────────────────────
|
|
|
|
[Description("Legt ein neues Fach an. Muss der Nutzer erst bestätigen.")]
|
|
public async Task<WriteResultDto> CreateSubject(
|
|
[Description("Name des Fachs, z.B. \"Mathematik\".")] string name,
|
|
[Description("Kurzform, z.B. \"Ma\".")] string shortName = "",
|
|
CancellationToken ct = default)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(name)) return new WriteResultDto(false, null, "Fachname darf nicht leer sein.");
|
|
|
|
var message = $"Neues Fach „{name.Trim()}“" +
|
|
(string.IsNullOrWhiteSpace(shortName) ? "" : $" ({shortName.Trim()})") + " anlegen?";
|
|
if (!await confirmation.ConfirmAsync("Fach anlegen?", message, ct))
|
|
return new WriteResultDto(false, null, "Vom Nutzer abgelehnt oder nicht bestätigt.");
|
|
|
|
var subject = new Subject { Name = name, ShortName = shortName };
|
|
try { subjects.Save(subject); }
|
|
catch (Exception ex) when (ex is ArgumentException or InvalidOperationException)
|
|
{
|
|
return new WriteResultDto(false, null, ex.Message);
|
|
}
|
|
return new WriteResultDto(true, subject.Id, "Fach gespeichert.");
|
|
}
|
|
|
|
[Description("Ändert Name/Kurzform eines bestehenden Fachs. Nur angegebene Felder werden geändert.")]
|
|
public async Task<WriteResultDto> UpdateSubject(
|
|
[Description("Fach-ID.")] Guid subjectId,
|
|
[Description("Neuer Name. Unverändert lassen: weglassen.")] string? name = null,
|
|
[Description("Neue Kurzform. Unverändert lassen: weglassen.")] string? shortName = null,
|
|
CancellationToken ct = default)
|
|
{
|
|
var subject = subjects.GetById(subjectId);
|
|
if (subject is null) return new WriteResultDto(false, null, "Unbekannte Fach-ID.");
|
|
|
|
var changes = new StringBuilder();
|
|
if (name is not null && name != subject.Name) { changes.AppendLine($"Name: „{subject.Name}“ → „{name}“"); subject.Name = name; }
|
|
if (shortName is not null && shortName != subject.ShortName) { changes.AppendLine($"Kurzform: „{subject.ShortName}“ → „{shortName}“"); subject.ShortName = shortName; }
|
|
|
|
if (changes.Length == 0)
|
|
return new WriteResultDto(true, subject.Id, "Keine Änderung nötig.");
|
|
|
|
if (!await confirmation.ConfirmAsync("Fach ändern?", $"„{subject.Name}“\n{changes}".TrimEnd(), ct))
|
|
return new WriteResultDto(false, null, "Vom Nutzer abgelehnt oder nicht bestätigt.");
|
|
|
|
try { subjects.Save(subject); }
|
|
catch (Exception ex) when (ex is ArgumentException or InvalidOperationException)
|
|
{
|
|
return new WriteResultDto(false, null, ex.Message);
|
|
}
|
|
return new WriteResultDto(true, subject.Id, "Fach gespeichert.");
|
|
}
|
|
|
|
[Description("Löscht ein Fach endgültig. Schlägt fehl, wenn noch eine Lerngruppe oder ein Kompetenzkatalog dieses Fach referenziert. Kein Papierkorb - nicht rückgängig zu machen. Muss der Nutzer erst bestätigen.")]
|
|
public async Task<WriteResultDto> DeleteSubject(
|
|
[Description("Fach-ID.")] Guid subjectId,
|
|
CancellationToken ct = default)
|
|
{
|
|
var subject = subjects.GetById(subjectId);
|
|
if (subject is null) return new WriteResultDto(false, null, "Unbekannte Fach-ID.");
|
|
|
|
var message = $"Das Fach „{subject.Name}“ wird endgültig gelöscht. Das kann NICHT rückgängig gemacht werden (kein Papierkorb für Fächer).";
|
|
if (!await confirmation.ConfirmAsync("Fach endgültig löschen?", message, ct))
|
|
return new WriteResultDto(false, null, "Vom Nutzer abgelehnt oder nicht bestätigt.");
|
|
|
|
try { subjects.Delete(subjectId); }
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
return new WriteResultDto(false, null, ex.Message);
|
|
}
|
|
return new WriteResultDto(true, subjectId, "Fach gelöscht.");
|
|
}
|
|
|
|
// ── Kompetenzbereiche (CompetencyDomain) ────────────────────────────────────────────────
|
|
|
|
[Description("Legt einen neuen Kompetenzbereich (z.B. \"Zahlen und Operationen\") für ein Fach, eine Klassenstufe und eine Bildungsplan-Version an, zunächst ohne Einzelkompetenzen. Muss der Nutzer erst bestätigen.")]
|
|
public async Task<WriteResultDto> CreateCompetencyDomain(
|
|
[Description("Fach-ID, aus get_subjects.")] Guid subjectId,
|
|
[Description("Klassenstufe.")] int gradeLevel,
|
|
[Description("Name des Bereichs.")] string name,
|
|
[Description("Optionales Kürzel, z.B. \"ZO\".")] string code = "",
|
|
[Description("Bildungsplan-Version, aus get_competency_plan_versions. Weglassen: Standardversion des Fachs.")] Guid? versionId = null,
|
|
CancellationToken ct = default)
|
|
{
|
|
var subject = subjects.GetById(subjectId);
|
|
if (subject is null) return new WriteResultDto(false, null, "Unbekannte Fach-ID.");
|
|
if (string.IsNullOrWhiteSpace(name)) return new WriteResultDto(false, null, "Bereichsname darf nicht leer sein.");
|
|
|
|
var effectiveVersionId = versionId ?? versions.GetOrCreateDefault(subjectId).Id;
|
|
var versionName = versions.GetById(effectiveVersionId)?.Name ?? "";
|
|
var sortOrder = domains.GetBySubjectAndGrade(subjectId, gradeLevel, effectiveVersionId).Count;
|
|
var message = $"Neuen Kompetenzbereich „{name.Trim()}“ für {subject.Name}, Klasse {gradeLevel} ({versionName}), anlegen?";
|
|
if (!await confirmation.ConfirmAsync("Kompetenzbereich anlegen?", message, ct))
|
|
return new WriteResultDto(false, null, "Vom Nutzer abgelehnt oder nicht bestätigt.");
|
|
|
|
var domain = new CompetencyDomain
|
|
{
|
|
SubjectId = subjectId, GradeLevel = gradeLevel, VersionId = effectiveVersionId,
|
|
Name = name, Code = code, SortOrder = sortOrder,
|
|
};
|
|
domains.Save(domain);
|
|
return new WriteResultDto(true, domain.Id, "Kompetenzbereich gespeichert.");
|
|
}
|
|
|
|
[Description("Ändert Name/Kürzel eines bestehenden Kompetenzbereichs. Nur angegebene Felder werden geändert.")]
|
|
public async Task<WriteResultDto> UpdateCompetencyDomain(
|
|
[Description("ID des Kompetenzbereichs, aus get_competency_catalog.")] Guid domainId,
|
|
[Description("Neuer Name. Unverändert lassen: weglassen.")] string? name = null,
|
|
[Description("Neues Kürzel. Unverändert lassen: weglassen.")] string? code = null,
|
|
CancellationToken ct = default)
|
|
{
|
|
var domain = domains.GetById(domainId);
|
|
if (domain is null) return new WriteResultDto(false, null, "Unbekannte Bereichs-ID.");
|
|
|
|
var changes = new StringBuilder();
|
|
if (name is not null && name != domain.Name) { changes.AppendLine($"Name: „{domain.Name}“ → „{name}“"); domain.Name = name; }
|
|
if (code is not null && code != domain.Code) { changes.AppendLine($"Kürzel: „{domain.Code}“ → „{code}“"); domain.Code = code; }
|
|
|
|
if (changes.Length == 0)
|
|
return new WriteResultDto(true, domain.Id, "Keine Änderung nötig.");
|
|
|
|
if (!await confirmation.ConfirmAsync("Kompetenzbereich ändern?", $"„{domain.Name}“\n{changes}".TrimEnd(), ct))
|
|
return new WriteResultDto(false, null, "Vom Nutzer abgelehnt oder nicht bestätigt.");
|
|
|
|
domains.Save(domain);
|
|
return new WriteResultDto(true, domain.Id, "Kompetenzbereich gespeichert.");
|
|
}
|
|
|
|
[Description("Löscht einen Kompetenzbereich endgültig, inklusive aller darin enthaltenen Einzelkompetenzen. Bereits vergebene Kompetenzcodes an Einheiten/Stunden/Klausuraufgaben bleiben als Freitext bestehen, gelten aber danach als \"nicht im Katalog\". Kein Papierkorb - nicht rückgängig zu machen. Muss der Nutzer erst bestätigen.")]
|
|
public async Task<WriteResultDto> DeleteCompetencyDomain(
|
|
[Description("ID des Kompetenzbereichs.")] Guid domainId,
|
|
CancellationToken ct = default)
|
|
{
|
|
var domain = domains.GetById(domainId);
|
|
if (domain is null) return new WriteResultDto(false, null, "Unbekannte Bereichs-ID.");
|
|
|
|
var message = $"Der Kompetenzbereich „{domain.Name}“ mit {domain.Items.Count} Einzelkompetenz(en) wird endgültig gelöscht. " +
|
|
"Das kann NICHT rückgängig gemacht werden (kein Papierkorb für Kompetenzbereiche).";
|
|
if (!await confirmation.ConfirmAsync("Kompetenzbereich endgültig löschen?", message, ct))
|
|
return new WriteResultDto(false, null, "Vom Nutzer abgelehnt oder nicht bestätigt.");
|
|
|
|
domains.Delete(domainId);
|
|
return new WriteResultDto(true, domainId, "Kompetenzbereich gelöscht.");
|
|
}
|
|
|
|
// ── Einzelkompetenzen (CompetencyItem) ──────────────────────────────────────────────────
|
|
|
|
[Description("Fügt einem Kompetenzbereich eine neue Einzelkompetenz hinzu (ans Ende). Muss der Nutzer erst bestätigen.")]
|
|
public async Task<WriteResultDto> AddCompetencyItem(
|
|
[Description("ID des Kompetenzbereichs, aus get_competency_catalog.")] Guid domainId,
|
|
[Description("Kompetenzcode, z.B. \"M.5.1\".")] string code,
|
|
[Description("Beschreibung der Kompetenz.")] string description,
|
|
CancellationToken ct = default)
|
|
{
|
|
var domain = domains.GetById(domainId);
|
|
if (domain is null) return new WriteResultDto(false, null, "Unbekannte Bereichs-ID.");
|
|
if (string.IsNullOrWhiteSpace(description)) return new WriteResultDto(false, null, "Beschreibung darf nicht leer sein.");
|
|
|
|
var message = $"Neue Kompetenz „{code}“ zu Bereich „{domain.Name}“ hinzufügen?\n{description}";
|
|
if (!await confirmation.ConfirmAsync("Kompetenz hinzufügen?", message, ct))
|
|
return new WriteResultDto(false, null, "Vom Nutzer abgelehnt oder nicht bestätigt.");
|
|
|
|
var item = new CompetencyItem { Code = code.Trim(), Description = description.Trim(), SortOrder = domain.Items.Count };
|
|
domain.Items.Add(item);
|
|
domains.Save(domain);
|
|
return new WriteResultDto(true, item.Id, "Kompetenz hinzugefügt.");
|
|
}
|
|
|
|
[Description("Ändert Code/Beschreibung einer bestehenden Einzelkompetenz. Nur angegebene Felder werden geändert.")]
|
|
public async Task<WriteResultDto> UpdateCompetencyItem(
|
|
[Description("ID des Kompetenzbereichs.")] Guid domainId,
|
|
[Description("ID der Einzelkompetenz, aus get_competency_catalog.")] Guid itemId,
|
|
[Description("Neuer Kompetenzcode. Unverändert lassen: weglassen.")] string? code = null,
|
|
[Description("Neue Beschreibung. Unverändert lassen: weglassen.")] string? description = null,
|
|
CancellationToken ct = default)
|
|
{
|
|
var domain = domains.GetById(domainId);
|
|
if (domain is null) return new WriteResultDto(false, null, "Unbekannte Bereichs-ID.");
|
|
var item = domain.Items.FirstOrDefault(i => i.Id == itemId);
|
|
if (item is null) return new WriteResultDto(false, null, "Unbekannte Kompetenz-ID in diesem Bereich.");
|
|
|
|
var changes = new StringBuilder();
|
|
if (code is not null && code != item.Code) { changes.AppendLine($"Code: „{item.Code}“ → „{code}“"); item.Code = code; }
|
|
if (description is not null && description != item.Description) { changes.AppendLine($"Beschreibung: „{item.Description}“ → „{description}“"); item.Description = description; }
|
|
|
|
if (changes.Length == 0)
|
|
return new WriteResultDto(true, item.Id, "Keine Änderung nötig.");
|
|
|
|
if (!await confirmation.ConfirmAsync("Kompetenz ändern?", $"Kompetenz „{item.Code}“ in Bereich „{domain.Name}“\n{changes}".TrimEnd(), ct))
|
|
return new WriteResultDto(false, null, "Vom Nutzer abgelehnt oder nicht bestätigt.");
|
|
|
|
domains.Save(domain);
|
|
return new WriteResultDto(true, item.Id, "Kompetenz gespeichert.");
|
|
}
|
|
|
|
[Description("Entfernt eine Einzelkompetenz aus einem Kompetenzbereich. Muss der Nutzer erst bestätigen.")]
|
|
public async Task<WriteResultDto> RemoveCompetencyItem(
|
|
[Description("ID des Kompetenzbereichs.")] Guid domainId,
|
|
[Description("ID der Einzelkompetenz, aus get_competency_catalog.")] Guid itemId,
|
|
CancellationToken ct = default)
|
|
{
|
|
var domain = domains.GetById(domainId);
|
|
if (domain is null) return new WriteResultDto(false, null, "Unbekannte Bereichs-ID.");
|
|
var item = domain.Items.FirstOrDefault(i => i.Id == itemId);
|
|
if (item is null) return new WriteResultDto(false, null, "Unbekannte Kompetenz-ID in diesem Bereich.");
|
|
|
|
var message = $"Kompetenz „{item.Code}“ ({item.Description}) aus Bereich „{domain.Name}“ entfernen?";
|
|
if (!await confirmation.ConfirmAsync("Kompetenz entfernen?", message, ct))
|
|
return new WriteResultDto(false, null, "Vom Nutzer abgelehnt oder nicht bestätigt.");
|
|
|
|
domain.Items.Remove(item);
|
|
domains.Save(domain);
|
|
return new WriteResultDto(true, item.Id, "Kompetenz entfernt.");
|
|
}
|
|
|
|
private static CompetencyDomainDto ToDto(CompetencyDomain d) => new(
|
|
d.Id, d.SubjectId, d.GradeLevel, d.VersionId, d.Name, d.Code, d.SortOrder,
|
|
d.Items.OrderBy(i => i.SortOrder)
|
|
.Select(i => new CompetencyItemDto(i.Id, i.Code, i.Description, i.SortOrder)).ToList());
|
|
}
|