diff --git a/.ci/appsettings.override.postgres.docker.json b/.ci/appsettings.override.postgres.docker.json
index 061b69368f..2705554bee 100644
--- a/.ci/appsettings.override.postgres.docker.json
+++ b/.ci/appsettings.override.postgres.docker.json
@@ -95,6 +95,76 @@
}
}
},
+ "Tags": {
+ "Application": {
+ "SupportedLanguages": [
+ "de",
+ "en"
+ ],
+ "TagsForAttributeValueTypes": {
+ "IdentityFileReference": {
+ "schulabschluss": {
+ "displayNames": {
+ "de": "Abschluss",
+ "en": "Degree"
+ },
+ "children": {
+ "realschule": {
+ "displayNames": {
+ "de": "Realschule",
+ "en": "Secondary School"
+ },
+ "children": {
+ "zeugnis": {
+ "displayNames": {
+ "de": "Zeugnis",
+ "en": "Diploma"
+ }
+ }
+ }
+ },
+ "gymnasium": {
+ "displayNames": {
+ "de": "Gymnasium",
+ "en": "High School"
+ },
+ "children": {
+ "zeugnis": {
+ "displayNames": {
+ "de": "Zeugnis",
+ "en": "Diploma"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "PhoneNumber": {
+ "notfall": {
+ "displayNames": {
+ "de": "Notfallkontakt",
+ "en": "Emergency Contact"
+ }
+ }
+ },
+ "StreetAddress": {
+ "lieferung": {
+ "displayNames": {
+ "de": "Lieferadresse",
+ "en": "Deliver Address"
+ }
+ },
+ "heimat": {
+ "displayNames": {
+ "de": "Heimatadresse",
+ "en": "Home Address"
+ }
+ }
+ }
+ }
+ }
+ },
"Tokens": {
"Infrastructure": {
"SqlDatabase": {
diff --git a/.ci/appsettings.override.postgres.local.json b/.ci/appsettings.override.postgres.local.json
index d8f49ffaf7..7321d4ae9c 100644
--- a/.ci/appsettings.override.postgres.local.json
+++ b/.ci/appsettings.override.postgres.local.json
@@ -95,6 +95,76 @@
}
}
},
+ "Tags": {
+ "Application": {
+ "SupportedLanguages": [
+ "de",
+ "en"
+ ],
+ "TagsForAttributeValueTypes": {
+ "IdentityFileReference": {
+ "schulabschluss": {
+ "displayNames": {
+ "de": "Abschluss",
+ "en": "Degree"
+ },
+ "children": {
+ "realschule": {
+ "displayNames": {
+ "de": "Realschule",
+ "en": "Secondary School"
+ },
+ "children": {
+ "zeugnis": {
+ "displayNames": {
+ "de": "Zeugnis",
+ "en": "Diploma"
+ }
+ }
+ }
+ },
+ "gymnasium": {
+ "displayNames": {
+ "de": "Gymnasium",
+ "en": "High School"
+ },
+ "children": {
+ "zeugnis": {
+ "displayNames": {
+ "de": "Zeugnis",
+ "en": "Diploma"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "PhoneNumber": {
+ "notfall": {
+ "displayNames": {
+ "de": "Notfallkontakt",
+ "en": "Emergency Contact"
+ }
+ }
+ },
+ "StreetAddress": {
+ "lieferung": {
+ "displayNames": {
+ "de": "Lieferadresse",
+ "en": "Deliver Address"
+ }
+ },
+ "heimat": {
+ "displayNames": {
+ "de": "Heimatadresse",
+ "en": "Home Address"
+ }
+ }
+ }
+ }
+ }
+ },
"Tokens": {
"Infrastructure": {
"SqlDatabase": {
diff --git a/.ci/appsettings.override.sqlserver.docker.json b/.ci/appsettings.override.sqlserver.docker.json
index 7b8f8880b5..a6102ed26e 100644
--- a/.ci/appsettings.override.sqlserver.docker.json
+++ b/.ci/appsettings.override.sqlserver.docker.json
@@ -95,6 +95,76 @@
}
}
},
+ "Tags": {
+ "Application": {
+ "SupportedLanguages": [
+ "de",
+ "en"
+ ],
+ "TagsForAttributeValueTypes": {
+ "IdentityFileReference": {
+ "schulabschluss": {
+ "displayNames": {
+ "de": "Abschluss",
+ "en": "Degree"
+ },
+ "children": {
+ "realschule": {
+ "displayNames": {
+ "de": "Realschule",
+ "en": "Secondary School"
+ },
+ "children": {
+ "zeugnis": {
+ "displayNames": {
+ "de": "Zeugnis",
+ "en": "Diploma"
+ }
+ }
+ }
+ },
+ "gymnasium": {
+ "displayNames": {
+ "de": "Gymnasium",
+ "en": "High School"
+ },
+ "children": {
+ "zeugnis": {
+ "displayNames": {
+ "de": "Zeugnis",
+ "en": "Diploma"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "PhoneNumber": {
+ "notfall": {
+ "displayNames": {
+ "de": "Notfallkontakt",
+ "en": "Emergency Contact"
+ }
+ }
+ },
+ "StreetAddress": {
+ "lieferung": {
+ "displayNames": {
+ "de": "Lieferadresse",
+ "en": "Deliver Address"
+ }
+ },
+ "heimat": {
+ "displayNames": {
+ "de": "Heimatadresse",
+ "en": "Home Address"
+ }
+ }
+ }
+ }
+ }
+ },
"Tokens": {
"Infrastructure": {
"SqlDatabase": {
diff --git a/.ci/appsettings.override.sqlserver.local.json b/.ci/appsettings.override.sqlserver.local.json
index 45bb2811be..74b61bb4e9 100644
--- a/.ci/appsettings.override.sqlserver.local.json
+++ b/.ci/appsettings.override.sqlserver.local.json
@@ -95,6 +95,76 @@
}
}
},
+ "Tags": {
+ "Application": {
+ "SupportedLanguages": [
+ "de",
+ "en"
+ ],
+ "TagsForAttributeValueTypes": {
+ "IdentityFileReference": {
+ "schulabschluss": {
+ "displayNames": {
+ "de": "Abschluss",
+ "en": "Degree"
+ },
+ "children": {
+ "realschule": {
+ "displayNames": {
+ "de": "Realschule",
+ "en": "Secondary School"
+ },
+ "children": {
+ "zeugnis": {
+ "displayNames": {
+ "de": "Zeugnis",
+ "en": "Diploma"
+ }
+ }
+ }
+ },
+ "gymnasium": {
+ "displayNames": {
+ "de": "Gymnasium",
+ "en": "High School"
+ },
+ "children": {
+ "zeugnis": {
+ "displayNames": {
+ "de": "Zeugnis",
+ "en": "Diploma"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "PhoneNumber": {
+ "notfall": {
+ "displayNames": {
+ "de": "Notfallkontakt",
+ "en": "Emergency Contact"
+ }
+ }
+ },
+ "StreetAddress": {
+ "lieferung": {
+ "displayNames": {
+ "de": "Lieferadresse",
+ "en": "Deliver Address"
+ }
+ },
+ "heimat": {
+ "displayNames": {
+ "de": "Heimatadresse",
+ "en": "Home Address"
+ }
+ }
+ }
+ }
+ }
+ },
"Tokens": {
"Infrastructure": {
"SqlDatabase": {
diff --git a/Applications/ConsumerApi/src/ConsumerApi.csproj b/Applications/ConsumerApi/src/ConsumerApi.csproj
index 3c0a17c437..c214c725d9 100644
--- a/Applications/ConsumerApi/src/ConsumerApi.csproj
+++ b/Applications/ConsumerApi/src/ConsumerApi.csproj
@@ -37,6 +37,7 @@
+
diff --git a/Applications/ConsumerApi/src/Program.cs b/Applications/ConsumerApi/src/Program.cs
index a6dc80cf4f..0f2df63a56 100644
--- a/Applications/ConsumerApi/src/Program.cs
+++ b/Applications/ConsumerApi/src/Program.cs
@@ -26,6 +26,7 @@
using Backbone.Modules.Relationships.Infrastructure.Persistence.Database;
using Backbone.Modules.Synchronization.ConsumerApi;
using Backbone.Modules.Synchronization.Infrastructure.Persistence.Database;
+using Backbone.Modules.Tags.ConsumerApi;
using Backbone.Modules.Tokens.ConsumerApi;
using Backbone.Modules.Tokens.Infrastructure.Persistence.Database;
using Backbone.Tooling.Extensions;
@@ -147,6 +148,7 @@ static void ConfigureServices(IServiceCollection services, IConfiguration config
.AddModule(configuration)
.AddModule(configuration)
.AddModule(configuration)
+ .AddModule(configuration)
.AddModule(configuration);
var quotasSqlDatabaseConfiguration = parsedConfiguration.Modules.Quotas.Infrastructure.SqlDatabase;
diff --git a/Applications/ConsumerApi/src/http/Tags/ListTags.bru b/Applications/ConsumerApi/src/http/Tags/ListTags.bru
new file mode 100644
index 0000000000..d1ac3fc138
--- /dev/null
+++ b/Applications/ConsumerApi/src/http/Tags/ListTags.bru
@@ -0,0 +1,11 @@
+meta {
+ name: /Tags
+ type: http
+ seq: 1
+}
+
+get {
+ url: {{baseUrl}}/Tags
+ body: none
+ auth: none
+}
diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/Tags/GET.feature b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/Tags/GET.feature
new file mode 100644
index 0000000000..79faf4f3c8
--- /dev/null
+++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/Tags/GET.feature
@@ -0,0 +1,10 @@
+@Integration
+Feature: GET /Tags
+
+User requests available Tags
+
+ Scenario: Requesting the available Tags
+ When A GET request to the /Tags endpoint gets sent
+ Then the response status code is 200 (OK)
+ And the response supports the English language
+ And the response attributes contain tags
diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/TagsStepDefinitions.cs b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/TagsStepDefinitions.cs
new file mode 100644
index 0000000000..1c2cfe05f5
--- /dev/null
+++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/TagsStepDefinitions.cs
@@ -0,0 +1,43 @@
+using Backbone.BuildingBlocks.SDK.Endpoints.Common.Types;
+using Backbone.ConsumerApi.Sdk.Endpoints.Tags.Types.Responses;
+using Backbone.ConsumerApi.Tests.Integration.Contexts;
+using Backbone.ConsumerApi.Tests.Integration.Helpers;
+
+namespace Backbone.ConsumerApi.Tests.Integration.StepDefinitions;
+
+[Binding]
+public class TagsStepDefinitions
+{
+ private readonly ResponseContext _responseContext;
+ private readonly ClientPool _clientPool;
+
+ private ApiResponse? _listTagsResponse;
+
+ public TagsStepDefinitions(ResponseContext responseContext, ClientPool clientPool)
+ {
+ _responseContext = responseContext;
+ _clientPool = clientPool;
+ }
+
+ [When("A GET request to the /Tags endpoint gets sent")]
+ public async Task WhenAGETRequestToTheTagsEndpointGetsSent()
+ {
+ var client = _clientPool.Anonymous;
+ _responseContext.WhenResponse = _listTagsResponse = await client.Tags.ListTags();
+ }
+
+ [Then("the response supports the English language")]
+ public void AndTheResponseSupportsTheEnglishLanguage()
+ {
+ _listTagsResponse!.Result!.SupportedLanguages.Should().Contain("en");
+ }
+
+ [Then("the response attributes contain tags")]
+ public void AndTheResponseAttributesContainTags()
+ {
+ foreach (var attr in _listTagsResponse!.Result!.TagsForAttributeValueTypes.Values)
+ {
+ attr.Should().NotBeEmpty();
+ }
+ }
+}
diff --git a/Backbone.sln b/Backbone.sln
index f4fb2411bb..502f7b2fc6 100644
--- a/Backbone.sln
+++ b/Backbone.sln
@@ -370,6 +370,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{1BFEE2F6-D
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DatabaseMigrator.Tests", "Applications\DatabaseMigrator\test\DatabaseMigrator.Tests\DatabaseMigrator.Tests.csproj", "{553F1C8B-E099-4856-9416-67C103D3D194}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tags", "Tags", "{2E8887F1-55BD-4751-A522-0377C80DC70B}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{2A9D40E4-AB8F-49B5-878B-A8C7763EE609}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tags.Application", "Modules\Tags\src\Tags.Application\Tags.Application.csproj", "{04A11F4A-6D3E-484E-89EC-041D223CED21}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tags.ConsumerApi", "Modules\Tags\src\Tags.ConsumerApi\Tags.ConsumerApi.csproj", "{2BE30D30-CCFF-453E-AEAC-9CB3EF677058}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tags.Domain", "Modules\Tags\src\Tags.Domain\Tags.Domain.csproj", "{3A107CCD-A7E9-448B-82DF-0FD87D1ABA9E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tags.Infrastructure", "Modules\Tags\src\Tags.Infrastructure\Tags.Infrastructure.csproj", "{98C16B16-7ECE-4E23-8D6C-2CA372EC310C}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -808,6 +820,22 @@ Global
{553F1C8B-E099-4856-9416-67C103D3D194}.Debug|Any CPU.Build.0 = Debug|Any CPU
{553F1C8B-E099-4856-9416-67C103D3D194}.Release|Any CPU.ActiveCfg = Release|Any CPU
{553F1C8B-E099-4856-9416-67C103D3D194}.Release|Any CPU.Build.0 = Release|Any CPU
+ {04A11F4A-6D3E-484E-89EC-041D223CED21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {04A11F4A-6D3E-484E-89EC-041D223CED21}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {04A11F4A-6D3E-484E-89EC-041D223CED21}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {04A11F4A-6D3E-484E-89EC-041D223CED21}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2BE30D30-CCFF-453E-AEAC-9CB3EF677058}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2BE30D30-CCFF-453E-AEAC-9CB3EF677058}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2BE30D30-CCFF-453E-AEAC-9CB3EF677058}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2BE30D30-CCFF-453E-AEAC-9CB3EF677058}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3A107CCD-A7E9-448B-82DF-0FD87D1ABA9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3A107CCD-A7E9-448B-82DF-0FD87D1ABA9E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3A107CCD-A7E9-448B-82DF-0FD87D1ABA9E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3A107CCD-A7E9-448B-82DF-0FD87D1ABA9E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {98C16B16-7ECE-4E23-8D6C-2CA372EC310C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {98C16B16-7ECE-4E23-8D6C-2CA372EC310C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {98C16B16-7ECE-4E23-8D6C-2CA372EC310C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {98C16B16-7ECE-4E23-8D6C-2CA372EC310C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -977,6 +1005,12 @@ Global
{D28F10DA-33BD-4F08-B736-1B7AB53026BD} = {CA03350A-2013-4D18-B68D-8C0F680DBA39}
{1BFEE2F6-D514-4458-994A-3DD106405990} = {A82D9F44-8B80-4803-BA97-7D737DEF6A98}
{553F1C8B-E099-4856-9416-67C103D3D194} = {1BFEE2F6-D514-4458-994A-3DD106405990}
+ {2E8887F1-55BD-4751-A522-0377C80DC70B} = {0EAF57B8-E97C-469E-A74B-596D78C978B2}
+ {2A9D40E4-AB8F-49B5-878B-A8C7763EE609} = {2E8887F1-55BD-4751-A522-0377C80DC70B}
+ {04A11F4A-6D3E-484E-89EC-041D223CED21} = {2A9D40E4-AB8F-49B5-878B-A8C7763EE609}
+ {2BE30D30-CCFF-453E-AEAC-9CB3EF677058} = {2A9D40E4-AB8F-49B5-878B-A8C7763EE609}
+ {3A107CCD-A7E9-448B-82DF-0FD87D1ABA9E} = {2A9D40E4-AB8F-49B5-878B-A8C7763EE609}
+ {98C16B16-7ECE-4E23-8D6C-2CA372EC310C} = {2A9D40E4-AB8F-49B5-878B-A8C7763EE609}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {1F3BD2C6-7CB3-450F-A21A-23EA520D5B7A}
diff --git a/Modules/Tags/src/Tags.Application/ApplicationOptions.cs b/Modules/Tags/src/Tags.Application/ApplicationOptions.cs
new file mode 100644
index 0000000000..e718810507
--- /dev/null
+++ b/Modules/Tags/src/Tags.Application/ApplicationOptions.cs
@@ -0,0 +1,75 @@
+using System.ComponentModel.DataAnnotations;
+using System.Globalization;
+using Backbone.Modules.Tags.Domain;
+
+namespace Backbone.Modules.Tags.Application;
+
+[ContainsValidTags]
+public class ApplicationOptions
+{
+ [Required]
+ public List SupportedLanguages { get; set; } = [];
+
+ public Dictionary> TagsForAttributeValueTypes { get; set; } = [];
+}
+
+[AttributeUsage(AttributeTargets.Class)]
+public class ContainsValidTagsAttribute : ValidationAttribute
+{
+ private static readonly CultureInfo[] CULTURES = CultureInfo.GetCultures(CultureTypes.AllCultures & ~CultureTypes.NeutralCultures);
+
+ private List _supportedLanguages = [];
+
+ protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
+ {
+ if (value is not ApplicationOptions applicationOptions) return new ValidationResult($"The attribute can only be used for {nameof(ApplicationOptions)}.");
+
+ _supportedLanguages = applicationOptions.SupportedLanguages;
+ var result = ValidateLanguages();
+ if (result != ValidationResult.Success) return result;
+
+ foreach (var (attributeName, tags) in applicationOptions.TagsForAttributeValueTypes)
+ {
+ foreach (var (tagName, tag) in tags)
+ {
+ result = ValidateTag([attributeName, tagName], tag);
+ if (result != ValidationResult.Success) return result;
+ }
+ }
+
+ return ValidationResult.Success;
+ }
+
+ private ValidationResult? ValidateLanguages()
+ {
+ if (!_supportedLanguages.Contains("en")) return new ValidationResult("The tags have to support the English language.", [nameof(ApplicationOptions.SupportedLanguages)]);
+
+ var invalidLanguageEntries = _supportedLanguages.Where(value => CULTURES.All(c => c.TwoLetterISOLanguageName != value)).ToList();
+ if (invalidLanguageEntries.Count != 0)
+ return new ValidationResult($"The language entries \"{Enumerate(invalidLanguageEntries)}\" are not valid language codes.", [nameof(ApplicationOptions.SupportedLanguages)]);
+
+ return ValidationResult.Success;
+ }
+
+ private ValidationResult? ValidateTag(IEnumerable nameParts, TagInfo tag)
+ {
+ var notSupportedLanguages = tag.DisplayNames.Keys.Except(_supportedLanguages).ToList();
+ var notImplementedLanguages = _supportedLanguages.Except(tag.DisplayNames.Keys).ToList();
+
+ if (notSupportedLanguages.Count != 0)
+ return new ValidationResult($"The languages \"{Enumerate(notSupportedLanguages)}\" are unsupported", [GetPathOfProperty(nameParts)]);
+ if (notImplementedLanguages.Count != 0)
+ return new ValidationResult($"A display name for the language(s) \"{Enumerate(notImplementedLanguages)}\" is required.", [GetPathOfProperty(nameParts)]);
+
+ foreach (var (childName, child) in tag.Children)
+ {
+ var result = ValidateTag(nameParts.Append(childName), child);
+ if (result != ValidationResult.Success) return result;
+ }
+
+ return ValidationResult.Success;
+ }
+
+ private static string GetPathOfProperty(IEnumerable nameParts) => string.Join('.', nameParts);
+ private static string Enumerate(IEnumerable names) => string.Join(',', names);
+}
diff --git a/Modules/Tags/src/Tags.Application/Extensions/IServiceCollectionExtensions.cs b/Modules/Tags/src/Tags.Application/Extensions/IServiceCollectionExtensions.cs
new file mode 100644
index 0000000000..467c502c4c
--- /dev/null
+++ b/Modules/Tags/src/Tags.Application/Extensions/IServiceCollectionExtensions.cs
@@ -0,0 +1,14 @@
+using Backbone.Modules.Tags.Application.Tags.Queries.ListTags;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Backbone.Modules.Tags.Application.Extensions;
+
+public static class IServiceCollectionExtensions
+{
+ public static void AddApplication(this IServiceCollection services)
+ {
+ services.AddMediatR(c => c
+ .RegisterServicesFromAssemblyContaining()
+ );
+ }
+}
diff --git a/Modules/Tags/src/Tags.Application/Infrastructure/Persistence/Repository/ITagsRepository.cs b/Modules/Tags/src/Tags.Application/Infrastructure/Persistence/Repository/ITagsRepository.cs
new file mode 100644
index 0000000000..9815619c40
--- /dev/null
+++ b/Modules/Tags/src/Tags.Application/Infrastructure/Persistence/Repository/ITagsRepository.cs
@@ -0,0 +1,9 @@
+using Backbone.Modules.Tags.Domain;
+
+namespace Backbone.Modules.Tags.Application.Infrastructure.Persistence.Repository;
+
+public interface ITagsRepository
+{
+ public IEnumerable GetSupportedLanguages();
+ public Dictionary> GetAttributes();
+}
diff --git a/Modules/Tags/src/Tags.Application/Tags.Application.csproj b/Modules/Tags/src/Tags.Application/Tags.Application.csproj
new file mode 100644
index 0000000000..688eca0f04
--- /dev/null
+++ b/Modules/Tags/src/Tags.Application/Tags.Application.csproj
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/Modules/Tags/src/Tags.Application/Tags/Queries/ListTags/Handler.cs b/Modules/Tags/src/Tags.Application/Tags/Queries/ListTags/Handler.cs
new file mode 100644
index 0000000000..49cc334725
--- /dev/null
+++ b/Modules/Tags/src/Tags.Application/Tags/Queries/ListTags/Handler.cs
@@ -0,0 +1,25 @@
+using Backbone.Modules.Tags.Application.Infrastructure.Persistence.Repository;
+using MediatR;
+
+namespace Backbone.Modules.Tags.Application.Tags.Queries.ListTags;
+
+public class Handler : IRequestHandler
+{
+ private readonly ITagsRepository _tagsRepository;
+
+ public Handler(ITagsRepository tagsRepository)
+ {
+ _tagsRepository = tagsRepository;
+ }
+
+ public Task Handle(ListTagsQuery request, CancellationToken cancellationToken)
+ {
+ var response = new ListTagsResponse
+ {
+ SupportedLanguages = _tagsRepository.GetSupportedLanguages(),
+ TagsForAttributeValueTypes = _tagsRepository.GetAttributes()
+ };
+
+ return Task.FromResult(response);
+ }
+}
diff --git a/Modules/Tags/src/Tags.Application/Tags/Queries/ListTags/ListTagsQuery.cs b/Modules/Tags/src/Tags.Application/Tags/Queries/ListTags/ListTagsQuery.cs
new file mode 100644
index 0000000000..d6004cb1bf
--- /dev/null
+++ b/Modules/Tags/src/Tags.Application/Tags/Queries/ListTags/ListTagsQuery.cs
@@ -0,0 +1,5 @@
+using MediatR;
+
+namespace Backbone.Modules.Tags.Application.Tags.Queries.ListTags;
+
+public class ListTagsQuery : IRequest;
diff --git a/Modules/Tags/src/Tags.Application/Tags/Queries/ListTags/ListTagsResponse.cs b/Modules/Tags/src/Tags.Application/Tags/Queries/ListTags/ListTagsResponse.cs
new file mode 100644
index 0000000000..e838121db1
--- /dev/null
+++ b/Modules/Tags/src/Tags.Application/Tags/Queries/ListTags/ListTagsResponse.cs
@@ -0,0 +1,12 @@
+using System.Text.Json.Serialization;
+using Backbone.Modules.Tags.Domain;
+
+namespace Backbone.Modules.Tags.Application.Tags.Queries.ListTags;
+
+public class ListTagsResponse
+{
+ public required IEnumerable SupportedLanguages { get; set; }
+
+ [JsonConverter(typeof(PascalCaseDictionaryConverter>))]
+ public required Dictionary> TagsForAttributeValueTypes { get; set; }
+}
diff --git a/Modules/Tags/src/Tags.ConsumerApi/Controllers/TagsController.cs b/Modules/Tags/src/Tags.ConsumerApi/Controllers/TagsController.cs
new file mode 100644
index 0000000000..ee28cbe550
--- /dev/null
+++ b/Modules/Tags/src/Tags.ConsumerApi/Controllers/TagsController.cs
@@ -0,0 +1,24 @@
+using Backbone.BuildingBlocks.API.Mvc;
+using Backbone.Modules.Tags.Application.Tags.Queries.ListTags;
+using MediatR;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Backbone.Modules.Tags.ConsumerApi.Controllers;
+
+[Route("api/v1/[controller]")]
+[Authorize("OpenIddict.Validation.AspNetCore")]
+public class TagsController : ApiControllerBase
+{
+ public TagsController(IMediator mediator) : base(mediator)
+ {
+ }
+
+ [HttpGet]
+ [AllowAnonymous]
+ public async Task ListTags(CancellationToken cancellationToken)
+ {
+ var response = await _mediator.Send(new ListTagsQuery(), cancellationToken);
+ return Ok(response);
+ }
+}
diff --git a/Modules/Tags/src/Tags.ConsumerApi/Tags.ConsumerApi.csproj b/Modules/Tags/src/Tags.ConsumerApi/Tags.ConsumerApi.csproj
new file mode 100644
index 0000000000..70e3ad5aab
--- /dev/null
+++ b/Modules/Tags/src/Tags.ConsumerApi/Tags.ConsumerApi.csproj
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Modules/Tags/src/Tags.ConsumerApi/TagsModule.cs b/Modules/Tags/src/Tags.ConsumerApi/TagsModule.cs
new file mode 100644
index 0000000000..9dacc76eb4
--- /dev/null
+++ b/Modules/Tags/src/Tags.ConsumerApi/TagsModule.cs
@@ -0,0 +1,27 @@
+using Backbone.BuildingBlocks.API;
+using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus;
+using Backbone.Modules.Tags.Application;
+using Backbone.Modules.Tags.Application.Extensions;
+using Backbone.Modules.Tags.Infrastructure.Persistence;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Backbone.Modules.Tags.ConsumerApi;
+
+public class TagsModule : AbstractModule
+{
+ public override string Name => "Tags";
+
+ public override void ConfigureServices(IServiceCollection services, IConfigurationSection configuration)
+ {
+ services.ConfigureAndValidate(options => configuration.GetSection("Application").Bind(options));
+
+ services.AddApplication();
+ services.AddPersistence();
+ }
+
+ public override void ConfigureEventBus(IEventBus eventBus)
+ {
+ // No Event bus needed here
+ }
+}
diff --git a/Modules/Tags/src/Tags.Domain/TagInfo.cs b/Modules/Tags/src/Tags.Domain/TagInfo.cs
new file mode 100644
index 0000000000..6b963d58a5
--- /dev/null
+++ b/Modules/Tags/src/Tags.Domain/TagInfo.cs
@@ -0,0 +1,35 @@
+using System.ComponentModel.DataAnnotations;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Backbone.Modules.Tags.Domain;
+
+public class TagInfo
+{
+ [Required]
+ public Dictionary DisplayNames { get; set; } = [];
+
+ [JsonConverter(typeof(PascalCaseDictionaryConverter))]
+ public Dictionary Children { get; set; } = [];
+}
+
+public class PascalCaseDictionaryConverter : JsonConverter>
+{
+ public override Dictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ throw new NotSupportedException("Deserialization is not supported by this converter.");
+ }
+
+ public override void Write(Utf8JsonWriter writer, Dictionary value, JsonSerializerOptions options)
+ {
+ writer.WriteStartObject();
+ foreach (var key in value.Keys)
+ {
+ var value2 = value[key];
+ writer.WritePropertyName(key);
+ JsonSerializer.Serialize(writer, value2, options);
+ }
+
+ writer.WriteEndObject();
+ }
+}
diff --git a/Modules/Tags/src/Tags.Domain/Tags.Domain.csproj b/Modules/Tags/src/Tags.Domain/Tags.Domain.csproj
new file mode 100644
index 0000000000..3a63532952
--- /dev/null
+++ b/Modules/Tags/src/Tags.Domain/Tags.Domain.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
diff --git a/Modules/Tags/src/Tags.Infrastructure/Persistence/IServiceCollectionExtensions.cs b/Modules/Tags/src/Tags.Infrastructure/Persistence/IServiceCollectionExtensions.cs
new file mode 100644
index 0000000000..eb5ab7da5f
--- /dev/null
+++ b/Modules/Tags/src/Tags.Infrastructure/Persistence/IServiceCollectionExtensions.cs
@@ -0,0 +1,12 @@
+using Backbone.Modules.Tags.Infrastructure.Persistence.Repository;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Backbone.Modules.Tags.Infrastructure.Persistence;
+
+public static class IServiceCollectionExtensions
+{
+ public static void AddPersistence(this IServiceCollection services)
+ {
+ services.AddRepositories();
+ }
+}
diff --git a/Modules/Tags/src/Tags.Infrastructure/Persistence/Repository/IServiceCollectionExtensions.cs b/Modules/Tags/src/Tags.Infrastructure/Persistence/Repository/IServiceCollectionExtensions.cs
new file mode 100644
index 0000000000..974a02f6f2
--- /dev/null
+++ b/Modules/Tags/src/Tags.Infrastructure/Persistence/Repository/IServiceCollectionExtensions.cs
@@ -0,0 +1,12 @@
+using Backbone.Modules.Tags.Application.Infrastructure.Persistence.Repository;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Backbone.Modules.Tags.Infrastructure.Persistence.Repository;
+
+public static class IServiceCollectionExtensions
+{
+ public static void AddRepositories(this IServiceCollection services)
+ {
+ services.AddTransient();
+ }
+}
diff --git a/Modules/Tags/src/Tags.Infrastructure/Persistence/Repository/TagsRepository.cs b/Modules/Tags/src/Tags.Infrastructure/Persistence/Repository/TagsRepository.cs
new file mode 100644
index 0000000000..77bf236827
--- /dev/null
+++ b/Modules/Tags/src/Tags.Infrastructure/Persistence/Repository/TagsRepository.cs
@@ -0,0 +1,21 @@
+using Backbone.Modules.Tags.Application;
+using Backbone.Modules.Tags.Application.Infrastructure.Persistence.Repository;
+using Backbone.Modules.Tags.Domain;
+using Microsoft.Extensions.Options;
+
+namespace Backbone.Modules.Tags.Infrastructure.Persistence.Repository;
+
+public class TagsRepository : ITagsRepository
+{
+ private readonly List _supportedLanguages;
+ private readonly Dictionary> _attributes;
+
+ public TagsRepository(IOptions options)
+ {
+ _supportedLanguages = options.Value.SupportedLanguages;
+ _attributes = options.Value.TagsForAttributeValueTypes;
+ }
+
+ public IEnumerable GetSupportedLanguages() => _supportedLanguages;
+ public Dictionary> GetAttributes() => _attributes;
+}
diff --git a/Modules/Tags/src/Tags.Infrastructure/Tags.Infrastructure.csproj b/Modules/Tags/src/Tags.Infrastructure/Tags.Infrastructure.csproj
new file mode 100644
index 0000000000..a027b534c3
--- /dev/null
+++ b/Modules/Tags/src/Tags.Infrastructure/Tags.Infrastructure.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
diff --git a/Sdks/ConsumerApi.Sdk/src/Client.cs b/Sdks/ConsumerApi.Sdk/src/Client.cs
index 36fceaf0a7..3a3b9de046 100644
--- a/Sdks/ConsumerApi.Sdk/src/Client.cs
+++ b/Sdks/ConsumerApi.Sdk/src/Client.cs
@@ -17,6 +17,7 @@
using Backbone.ConsumerApi.Sdk.Endpoints.RelationshipTemplates;
using Backbone.ConsumerApi.Sdk.Endpoints.SyncRuns;
using Backbone.ConsumerApi.Sdk.Endpoints.SyncRuns.Types.Requests;
+using Backbone.ConsumerApi.Sdk.Endpoints.Tags;
using Backbone.ConsumerApi.Sdk.Endpoints.Tokens;
using Backbone.Crypto;
using Backbone.Crypto.Implementations;
@@ -49,6 +50,7 @@ private Client(HttpClient httpClient, Configuration configuration, DeviceData? d
Relationships = new RelationshipsEndpoint(endpointClient);
RelationshipTemplates = new RelationshipTemplatesEndpoint(endpointClient);
SyncRuns = new SyncRunsEndpoint(endpointClient);
+ Tags = new TagsEndpoint(endpointClient);
Tokens = new TokensEndpoint(endpointClient);
}
@@ -67,6 +69,7 @@ private Client(HttpClient httpClient, Configuration configuration, DeviceData? d
public RelationshipsEndpoint Relationships { get; }
public RelationshipTemplatesEndpoint RelationshipTemplates { get; }
public SyncRunsEndpoint SyncRuns { get; }
+ public TagsEndpoint Tags { get; }
public TokensEndpoint Tokens { get; }
// ReSharper restore UnusedAutoPropertyAccessor.Global
diff --git a/Sdks/ConsumerApi.Sdk/src/Endpoints/Tags/TagsEndpoint.cs b/Sdks/ConsumerApi.Sdk/src/Endpoints/Tags/TagsEndpoint.cs
new file mode 100644
index 0000000000..917410cc7f
--- /dev/null
+++ b/Sdks/ConsumerApi.Sdk/src/Endpoints/Tags/TagsEndpoint.cs
@@ -0,0 +1,13 @@
+using Backbone.BuildingBlocks.SDK.Endpoints.Common;
+using Backbone.BuildingBlocks.SDK.Endpoints.Common.Types;
+using Backbone.ConsumerApi.Sdk.Endpoints.Tags.Types.Responses;
+
+namespace Backbone.ConsumerApi.Sdk.Endpoints.Tags;
+
+public class TagsEndpoint(EndpointClient client) : ConsumerApiEndpoint(client)
+{
+ public async Task> ListTags()
+ {
+ return await _client.GetUnauthenticated($"api/{API_VERSION}/Tags");
+ }
+}
diff --git a/Sdks/ConsumerApi.Sdk/src/Endpoints/Tags/Types/Responses/ListTagsResponse.cs b/Sdks/ConsumerApi.Sdk/src/Endpoints/Tags/Types/Responses/ListTagsResponse.cs
new file mode 100644
index 0000000000..a1129e43e3
--- /dev/null
+++ b/Sdks/ConsumerApi.Sdk/src/Endpoints/Tags/Types/Responses/ListTagsResponse.cs
@@ -0,0 +1,7 @@
+namespace Backbone.ConsumerApi.Sdk.Endpoints.Tags.Types.Responses;
+
+public class ListTagsResponse
+{
+ public required List SupportedLanguages { get; set; }
+ public required Dictionary TagsForAttributeValueTypes { get; set; }
+}
diff --git a/Sdks/ConsumerApi.Sdk/src/Endpoints/Tags/Types/Tag.cs b/Sdks/ConsumerApi.Sdk/src/Endpoints/Tags/Types/Tag.cs
new file mode 100644
index 0000000000..e12d9460ef
--- /dev/null
+++ b/Sdks/ConsumerApi.Sdk/src/Endpoints/Tags/Types/Tag.cs
@@ -0,0 +1,9 @@
+namespace Backbone.ConsumerApi.Sdk.Endpoints.Tags.Types;
+
+public class TagDefinition : Dictionary;
+
+public class Tag
+{
+ public required Dictionary DisplayNames { get; set; }
+ public TagDefinition Children { get; set; } = [];
+}
diff --git a/appsettings.override.json b/appsettings.override.json
index 8e82844419..69c0bcbf7a 100644
--- a/appsettings.override.json
+++ b/appsettings.override.json
@@ -122,6 +122,76 @@
}
}
},
+ "Tags": {
+ "Application": {
+ "SupportedLanguages": [
+ "de",
+ "en"
+ ],
+ "TagsForAttributeValueTypes": {
+ "IdentityFileReference": {
+ "schulabschluss": {
+ "displayNames": {
+ "de": "Abschluss",
+ "en": "Degree"
+ },
+ "children": {
+ "realschule": {
+ "displayNames": {
+ "de": "Realschule",
+ "en": "Secondary School"
+ },
+ "children": {
+ "zeugnis": {
+ "displayNames": {
+ "de": "Zeugnis",
+ "en": "Diploma"
+ }
+ }
+ }
+ },
+ "gymnasium": {
+ "displayNames": {
+ "de": "Gymnasium",
+ "en": "High School"
+ },
+ "children": {
+ "zeugnis": {
+ "displayNames": {
+ "de": "Zeugnis",
+ "en": "Diploma"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "PhoneNumber": {
+ "notfall": {
+ "displayNames": {
+ "de": "Notfallkontakt",
+ "en": "Emergency Contact"
+ }
+ }
+ },
+ "StreetAddress": {
+ "lieferung": {
+ "displayNames": {
+ "de": "Lieferadresse",
+ "en": "Deliver Address"
+ }
+ },
+ "heimat": {
+ "displayNames": {
+ "de": "Heimatadresse",
+ "en": "Home Address"
+ }
+ }
+ }
+ }
+ }
+ },
"Tokens": {
"Infrastructure": {
"SqlDatabase": {