Skip to content

Commit 794a651

Browse files
committed
feat: Added SOFTURE.Common.StronglyTypedIdentifiers
1 parent ecd0d19 commit 794a651

10 files changed

Lines changed: 478 additions & 0 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: SOFTURE STRONGLY TYPED IDENTIFIERS - RELEASE NEW VERSION TO NUGET.ORG
2+
3+
on:
4+
release:
5+
types: [released]
6+
7+
jobs:
8+
publishing:
9+
runs-on: ubuntu-latest
10+
11+
steps:
12+
- uses: actions/checkout@v2
13+
14+
- name: ⚙️ Install dotnet
15+
uses: actions/setup-dotnet@v1
16+
with:
17+
dotnet-version: 9.0.100
18+
19+
- name: 🔗 Restore dependencies
20+
run: dotnet restore ./API
21+
22+
- name: 📂 Create new nuget package
23+
run: dotnet pack --no-restore -c Release -o ./artifacts /p:PackageVersion=${{ github.ref_name }} /p:Version=${{ github.ref_name }} ./API/SOFTURE.Common.StronglyTypedIdentifiers/SOFTURE.Common.StronglyTypedIdentifiers.csproj
24+
continue-on-error: false
25+
26+
- name: 🚀 Push new nuget package
27+
run: dotnet nuget push ./artifacts/SOFTURE.Common.StronglyTypedIdentifiers.${{ github.ref_name }}.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json

API/API.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SOFTURE.Common.Resilience",
1616
EndProject
1717
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SOFTURE.Common.CQRS", "SOFTURE.Common.CQRS\SOFTURE.Common.CQRS.csproj", "{1AC78C07-8CFE-4F97-9111-427F8F074797}"
1818
EndProject
19+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SOFTURE.Common.StronglyTypedIdentifiers", "SOFTURE.Common.StronglyTypedIdentifiers\SOFTURE.Common.StronglyTypedIdentifiers.csproj", "{99307AA9-5A10-4A8E-8DC3-13ACCA197B31}"
20+
EndProject
1921
Global
2022
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2123
Debug|Any CPU = Debug|Any CPU
@@ -54,5 +56,9 @@ Global
5456
{1AC78C07-8CFE-4F97-9111-427F8F074797}.Debug|Any CPU.Build.0 = Debug|Any CPU
5557
{1AC78C07-8CFE-4F97-9111-427F8F074797}.Release|Any CPU.ActiveCfg = Release|Any CPU
5658
{1AC78C07-8CFE-4F97-9111-427F8F074797}.Release|Any CPU.Build.0 = Release|Any CPU
59+
{99307AA9-5A10-4A8E-8DC3-13ACCA197B31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
60+
{99307AA9-5A10-4A8E-8DC3-13ACCA197B31}.Debug|Any CPU.Build.0 = Debug|Any CPU
61+
{99307AA9-5A10-4A8E-8DC3-13ACCA197B31}.Release|Any CPU.ActiveCfg = Release|Any CPU
62+
{99307AA9-5A10-4A8E-8DC3-13ACCA197B31}.Release|Any CPU.Build.0 = Release|Any CPU
5763
EndGlobalSection
5864
EndGlobal
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
using SOFTURE.Language.Common;
4+
5+
namespace SOFTURE.Common.StronglyTypedIdentifiers.API.Converters;
6+
7+
public class StronglyTypedIdJsonConverter<TId, TValue> : JsonConverter<TId>
8+
where TId : IIdentifier
9+
where TValue : notnull
10+
{
11+
private readonly Type _identifierBaseGenericType = typeof(IdentifierBase<>);
12+
13+
public override TId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
14+
{
15+
var value = JsonSerializer.Deserialize<TValue>(ref reader, options);
16+
if (value is null)
17+
throw new JsonException($"Cannot deserialize null value to {typeof(TId).Name}");
18+
19+
return (TId)Activator.CreateInstance(typeToConvert, value)!;
20+
}
21+
22+
public override void Write(Utf8JsonWriter writer, TId value, JsonSerializerOptions options)
23+
{
24+
var baseType = typeof(TId).BaseType;
25+
if (baseType?.IsGenericType != true || baseType.GetGenericTypeDefinition() != _identifierBaseGenericType)
26+
throw new InvalidOperationException($"Type '{typeof(TId).Name}' does not inherit from IdentifierBase<T>");
27+
28+
var valueType = baseType.GetGenericArguments()[0];
29+
var propertyInfo = typeof(TId).GetProperties().FirstOrDefault(p => p.PropertyType == valueType && p.CanRead)
30+
?? throw new InvalidOperationException($"Type '{typeof(TId).Name}' does not have a readable property of type '{valueType.Name}'");
31+
32+
var underlyingValue = propertyInfo.GetValue(value);
33+
34+
JsonSerializer.Serialize(writer, underlyingValue, options);
35+
}
36+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using System.Text.Json;
2+
using SOFTURE.Common.StronglyTypedIdentifiers.API.Converters;
3+
using SOFTURE.Language.Common;
4+
5+
namespace SOFTURE.Common.StronglyTypedIdentifiers.API.Extensions;
6+
7+
internal static class JsonExtensions
8+
{
9+
internal static void RegisterStronglyTypedIdConverters<TLanguageAssemblyMarker>(this JsonSerializerOptions options)
10+
where TLanguageAssemblyMarker : IAssemblyMarker
11+
{
12+
var identifiersAssembly = typeof(TLanguageAssemblyMarker).Assembly;
13+
14+
var identifierTypes = identifiersAssembly.GetTypes()
15+
.Where(t => t.IsClass && typeof(IIdentifier).IsAssignableFrom(t) && !t.IsAbstract)
16+
.ToList();
17+
18+
foreach (var identifierType in identifierTypes)
19+
{
20+
var valueType = identifierType.BaseType?.GetGenericArguments().FirstOrDefault();
21+
if (valueType == typeof(Guid))
22+
{
23+
RegisterConverter(options, identifierType, typeof(Guid));
24+
}
25+
else if (valueType == typeof(int))
26+
{
27+
RegisterConverter(options, identifierType, typeof(int));
28+
}
29+
else if (valueType == typeof(long))
30+
{
31+
RegisterConverter(options, identifierType, typeof(long));
32+
}
33+
}
34+
}
35+
36+
private static void RegisterConverter(JsonSerializerOptions options, Type identifierType, Type valueType)
37+
{
38+
var converterType = typeof(StronglyTypedIdJsonConverter<,>).MakeGenericType(identifierType, valueType);
39+
var converter = Activator.CreateInstance(converterType);
40+
41+
if (converter != null)
42+
{
43+
options.Converters.Add((System.Text.Json.Serialization.JsonConverter)converter);
44+
}
45+
}
46+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using FastEndpoints;
2+
using SOFTURE.Common.StronglyTypedIdentifiers.API.Parsers;
3+
using SOFTURE.Language.Common;
4+
5+
namespace SOFTURE.Common.StronglyTypedIdentifiers.API.Extensions;
6+
7+
internal static class ParserExtensions
8+
{
9+
private const string ValueParserForMethodName = "ValueParserFor";
10+
11+
internal static void RegisterIdentifierParsers<TLanguageAssemblyMarker>(this Config config)
12+
where TLanguageAssemblyMarker : IAssemblyMarker
13+
{
14+
var identifiersAssembly = typeof(TLanguageAssemblyMarker).Assembly;
15+
16+
var identifierTypes = identifiersAssembly.GetTypes()
17+
.Where(t => t.IsClass && typeof(IIdentifier).IsAssignableFrom(t) && !t.IsAbstract)
18+
.ToList();
19+
20+
foreach (var identifierType in identifierTypes)
21+
{
22+
var valueType = identifierType.BaseType?.GetGenericArguments().FirstOrDefault();
23+
if (valueType == typeof(Guid))
24+
{
25+
config.RegisterParser(identifierType, nameof(IdentifierParsers.GuidParser));
26+
}
27+
else if (valueType == typeof(int) || valueType == typeof(long))
28+
{
29+
config.RegisterParser(identifierType, nameof(IdentifierParsers.NumberParser));
30+
}
31+
}
32+
}
33+
34+
private static void RegisterParser(this Config config, Type identifierType, string parserMethodName)
35+
{
36+
var method = typeof(IdentifierParsers).GetMethod(parserMethodName)?.MakeGenericMethod(identifierType);
37+
var delegateType = typeof(Func<,>).MakeGenericType(typeof(object), typeof(ParseResult));
38+
var parserDelegate = Delegate.CreateDelegate(delegateType, method!);
39+
40+
var bindingMethod = config.Binding.GetType().GetMethod(ValueParserForMethodName, [delegateType])
41+
?.MakeGenericMethod(identifierType);
42+
43+
bindingMethod?.Invoke(config.Binding, [parserDelegate]);
44+
}
45+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using FastEndpoints;
2+
using SOFTURE.Language.Common;
3+
4+
namespace SOFTURE.Common.StronglyTypedIdentifiers.API.Parsers;
5+
6+
public static class IdentifierParsers
7+
{
8+
public static ParseResult GuidParser<TIdentifier>(object? input)
9+
where TIdentifier : IIdentifier
10+
{
11+
var success = Guid.TryParse(input?.ToString(), out var result);
12+
13+
var identifier = (TIdentifier)Activator.CreateInstance(typeof(TIdentifier), result)!;
14+
15+
return new ParseResult(success, identifier);
16+
}
17+
18+
public static ParseResult NumberParser<TIdentifier>(object? input)
19+
where TIdentifier : IIdentifier
20+
{
21+
var success = int.TryParse(input?.ToString(), out var result);
22+
23+
var identifier = (TIdentifier)Activator.CreateInstance(typeof(TIdentifier), result)!;
24+
25+
return new ParseResult(success, identifier);
26+
}
27+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
using NJsonSchema;
2+
using NJsonSchema.Generation;
3+
using SOFTURE.Language.Common;
4+
5+
namespace SOFTURE.Common.StronglyTypedIdentifiers.API.SchemaProcessors;
6+
7+
public sealed class StronglyTypedIdSchemaProcessor : ISchemaProcessor
8+
{
9+
private static readonly Type IdentifierBaseGenericType = typeof(IdentifierBase<>);
10+
11+
private const string UuidFormat = "uuid";
12+
private const string Int32Format = "int32";
13+
private const string Int64Format = "int64";
14+
15+
public void Process(SchemaProcessorContext context)
16+
{
17+
var type = context.ContextualType.Type;
18+
19+
if (!IsStronglyTypedId(type))
20+
return;
21+
22+
var underlyingType = GetUnderlyingType(type);
23+
if (underlyingType == null)
24+
return;
25+
26+
var schema = CreateSchemaForUnderlyingType(underlyingType);
27+
context.Schema.Type = schema.Type;
28+
context.Schema.Format = schema.Format;
29+
context.Schema.Properties.Clear();
30+
context.Schema.AllOf.Clear();
31+
context.Schema.AnyOf.Clear();
32+
context.Schema.OneOf.Clear();
33+
}
34+
35+
private static bool IsStronglyTypedId(Type type)
36+
{
37+
if (type.IsInterface || type.IsAbstract)
38+
return false;
39+
40+
var baseType = type.BaseType;
41+
while (baseType != null)
42+
{
43+
if (baseType.IsGenericType)
44+
{
45+
var genericTypeDef = baseType.GetGenericTypeDefinition();
46+
if (genericTypeDef == IdentifierBaseGenericType)
47+
{
48+
return true;
49+
}
50+
}
51+
baseType = baseType.BaseType;
52+
}
53+
54+
return typeof(IIdentifier).IsAssignableFrom(type);
55+
}
56+
57+
private static Type? GetUnderlyingType(Type stronglyTypedIdType)
58+
{
59+
var baseType = stronglyTypedIdType.BaseType;
60+
while (baseType != null)
61+
{
62+
if (baseType.IsGenericType)
63+
{
64+
var args = baseType.GetGenericArguments();
65+
if (args.Length > 0)
66+
return args[0];
67+
}
68+
baseType = baseType.BaseType;
69+
}
70+
return null;
71+
}
72+
73+
private static JsonSchema CreateSchemaForUnderlyingType(Type underlyingType)
74+
{
75+
if (underlyingType == typeof(Guid))
76+
{
77+
return new JsonSchema
78+
{
79+
Type = JsonObjectType.String,
80+
Format = UuidFormat
81+
};
82+
}
83+
84+
if (underlyingType == typeof(int))
85+
{
86+
return new JsonSchema
87+
{
88+
Type = JsonObjectType.Integer,
89+
Format = Int32Format
90+
};
91+
}
92+
93+
if (underlyingType == typeof(long))
94+
{
95+
return new JsonSchema
96+
{
97+
Type = JsonObjectType.Integer,
98+
Format = Int64Format
99+
};
100+
}
101+
102+
return new JsonSchema
103+
{
104+
Type = JsonObjectType.String
105+
};
106+
}
107+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
namespace SOFTURE.Common.StronglyTypedIdentifiers.Persistance.Exceptions;
2+
3+
internal sealed class ConfigurationException(string message) : Exception(message);

0 commit comments

Comments
 (0)