diff --git a/src/recipe/Baked.Recipe.Service.Application/CodingStyle/RichTransient/LookUpTransientByIdConvention.cs b/src/recipe/Baked.Recipe.Service.Application/CodingStyle/RichTransient/LookUpTransientByIdConvention.cs new file mode 100644 index 000000000..029e3e73e --- /dev/null +++ b/src/recipe/Baked.Recipe.Service.Application/CodingStyle/RichTransient/LookUpTransientByIdConvention.cs @@ -0,0 +1,28 @@ +using Baked.Business; +using Baked.Domain.Model; +using Baked.RestApi.Configuration; + +namespace Baked.CodingStyle.RichTransient; + +public class LookUpTransientByIdConvention(DomainModel _domain) + : IApiModelConvention +{ + public void Apply(ParameterModelContext context) + { + if (context.Action.MappedMethod is null) { return; } + if (context.Action.MappedMethod.Has()) { return; } + if (!context.Parameter.IsInvokeMethodParameter) { return; } + if (context.Parameter.MappedParameter is null) { return; } + if (!context.Parameter.MappedParameter.ParameterType.TryGetMembers(out var members)) { return; } + if (!members.Has()) { return; } + if (members.TryGetQueryType(_domain, out var _)) { return; } + + var initializer = members.Methods.Having().Single(); + if (!initializer.DefaultOverload.Parameters.TryGetValue("id", out var parameter)) { return; } + + var factoryParameter = context.Action.AddFactoryAsService(_domain, context.Parameter.MappedParameter.ParameterType); + context.Parameter.Name = $"{context.Parameter.Name}Id"; + context.Parameter.Type = "string"; + context.Parameter.LookupRenderer = p => factoryParameter.BuildInitializer(p); + } +} \ No newline at end of file diff --git a/src/recipe/Baked.Recipe.Service.Application/CodingStyle/RichTransient/LookUpTransientsByIdsConvention.cs b/src/recipe/Baked.Recipe.Service.Application/CodingStyle/RichTransient/LookUpTransientsByIdsConvention.cs new file mode 100644 index 000000000..d74b9fba8 --- /dev/null +++ b/src/recipe/Baked.Recipe.Service.Application/CodingStyle/RichTransient/LookUpTransientsByIdsConvention.cs @@ -0,0 +1,30 @@ +using Baked.Business; +using Baked.Domain.Model; +using Baked.RestApi.Configuration; +using Humanizer; + +namespace Baked.CodingStyle.RichTransient; + +public class LookUpTransientsByIdsConvention(DomainModel _domain) + : IApiModelConvention +{ + public void Apply(ParameterModelContext context) + { + if (context.Action.MappedMethod is null) { return; } + if (context.Action.MappedMethod.Has()) { return; } + if (!context.Parameter.IsInvokeMethodParameter) { return; } + if (context.Parameter.MappedParameter is null) { return; } + if (!context.Parameter.MappedParameter.ParameterType.TryGetElementType(out var elementType)) { return; } + if (elementType.TryGetQueryContextType(_domain, out var queryContextType)) { return; } + if (!elementType.GetMetadata().Has()) { return; } + + var initializer = elementType.GetMembers().Methods.Having().Single(); + if (!initializer.DefaultOverload.Parameters.TryGetValue("id", out var parameter)) { return; } + + var factoryParameter = context.Action.AddFactoryAsService(_domain, elementType); + + context.Parameter.Type = "IEnumerable"; + context.Parameter.Name = $"{context.Parameter.Name.Singularize()}Ids"; + context.Parameter.LookupRenderer = p => factoryParameter.BuildInitializerByIds(p, isArray: context.Parameter.TypeModel.IsArray); + } +} \ No newline at end of file diff --git a/src/recipe/Baked.Recipe.Service.Application/CodingStyle/RichTransient/RichTransientCodingStyleExtensions.cs b/src/recipe/Baked.Recipe.Service.Application/CodingStyle/RichTransient/RichTransientCodingStyleExtensions.cs index 022e1febf..71717ce42 100644 --- a/src/recipe/Baked.Recipe.Service.Application/CodingStyle/RichTransient/RichTransientCodingStyleExtensions.cs +++ b/src/recipe/Baked.Recipe.Service.Application/CodingStyle/RichTransient/RichTransientCodingStyleExtensions.cs @@ -1,5 +1,10 @@ -using Baked.CodingStyle; +using Baked.Business; +using Baked.CodingStyle; using Baked.CodingStyle.RichTransient; +using Baked.Domain.Model; +using Baked.RestApi.Model; +using Humanizer; +using ParameterModel = Baked.RestApi.Model.ParameterModel; namespace Baked; @@ -7,4 +12,41 @@ public static class RichTransientCodingStyleExtensions { public static RichTransientCodingStyleFeature RichTransient(this CodingStyleConfigurator _) => new(); + + public static ParameterModel AddFactoryAsService(this ActionModel action, DomainModel domain, TypeModel transientType) + { + var parameter = + new ParameterModel(transientType, ParameterModelFrom.Services, $"new{transientType.Name.Pascalize()}") + { + IsInvokeMethodParameter = false, + Type = $"Func<{transientType.CSharpFriendlyFullName}>" + }; + + action.Parameter[parameter.Name] = parameter; + + return parameter; + } + + public static string BuildInitializer(this ParameterModel factoryParameter, string valueExpression) + { + if (factoryParameter.TypeModel is null) { throw new("FactoryParameter shold have mapped parameter"); } + + var initializer = factoryParameter.TypeModel.GetMembers().Methods.Having().Single(); + + return $"new{factoryParameter.TypeModel.Name.Pascalize()}().{initializer.Name}({valueExpression})"; + } + + public static string BuildInitializerByIds(this ParameterModel factoryParameter, string valueExpression, + bool isArray = default + ) + { + if (factoryParameter.TypeModel is null) { throw new("FactoryParameter shold have mapped parameter"); } + + var initializer = factoryParameter.TypeModel.GetMembers().Methods.Having().Single(); + var byIds = $"{valueExpression}.Select(id => new{factoryParameter.TypeModel.Name.Pascalize()}().{initializer.Name}(id))"; + + return isArray + ? $"{byIds}.ToArray()" + : $"{byIds}.ToList()"; + } } \ No newline at end of file diff --git a/src/recipe/Baked.Recipe.Service.Application/CodingStyle/RichTransient/RichTransientCodingStyleFeature.cs b/src/recipe/Baked.Recipe.Service.Application/CodingStyle/RichTransient/RichTransientCodingStyleFeature.cs index eb9fe436a..054d5d618 100644 --- a/src/recipe/Baked.Recipe.Service.Application/CodingStyle/RichTransient/RichTransientCodingStyleFeature.cs +++ b/src/recipe/Baked.Recipe.Service.Application/CodingStyle/RichTransient/RichTransientCodingStyleFeature.cs @@ -19,8 +19,8 @@ public void Configure(LayerConfigurator configurator) when: c => c.Type.IsClass && !c.Type.IsAbstract && c.Type.TryGetMembers(out var members) && - members.Has() && members.Has() && + members.Has() && members.Methods.Any(m => m.Has() && m.DefaultOverload.IsPublic && @@ -30,7 +30,7 @@ public void Configure(LayerConfigurator configurator) (p.ParameterType.IsValueType || p.ParameterType.Is()) ) ), - order: 40 + order: 10 ); builder.Conventions.AddMethodMetadata(new ApiMethodAttribute(), when: c => @@ -54,6 +54,8 @@ public void Configure(LayerConfigurator configurator) conventions.Add(new InitializeUsingQueryParametersConvention(), order: -30); conventions.Add(new InitializeUsingIdParameterConvention(domainModel), order: -30); conventions.Add(new RichTransientInitializerIsGetResourceConvention(), order: -30); + conventions.Add(new LookUpTransientByIdConvention(domainModel), order: -30); + conventions.Add(new LookUpTransientsByIdsConvention(domainModel), order: -30); }); } } \ No newline at end of file diff --git a/test/recipe/Baked.Test.Recipe.Service.Test/CodingStyle/LookingUpTransientsById.cs b/test/recipe/Baked.Test.Recipe.Service.Test/CodingStyle/LookingUpTransientsById.cs new file mode 100644 index 000000000..3626eba17 --- /dev/null +++ b/test/recipe/Baked.Test.Recipe.Service.Test/CodingStyle/LookingUpTransientsById.cs @@ -0,0 +1,37 @@ +using Newtonsoft.Json; +using System.Net.Http.Json; + +namespace Baked.Test.CodingStyle; + +public class LookingUpTransientsById : TestServiceNfr +{ + [Test] + public async Task TransientParameters() + { + var response = await Client.PostAsync("/method-samples/transient-parameters", JsonContent.Create( + new + { + transientId = "1" + } + )); + dynamic? actual = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + + $"{actual?.id}".ShouldBe("1"); + } + + [Test] + public async Task TransientListParameters() + { + var response = await Client.PostAsync("/method-samples/transient-list-parameters", JsonContent.Create( + new + { + transientIds = new[] { "1", "2" }, + otherTransientIds = new[] { "3", "4" }, + } + )); + dynamic? actual = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + + ((int?)actual?.Count).ShouldBe(4); + $"{actual?[0].id}".ShouldBe("1"); + } +} \ No newline at end of file diff --git a/test/recipe/Baked.Test.Recipe.Service/Business/MethodSamples.cs b/test/recipe/Baked.Test.Recipe.Service/Business/MethodSamples.cs index 91c75d718..bf504014f 100644 --- a/test/recipe/Baked.Test.Recipe.Service/Business/MethodSamples.cs +++ b/test/recipe/Baked.Test.Recipe.Service/Business/MethodSamples.cs @@ -1,4 +1,5 @@ -using Baked.Test.Orm; +using Baked.Test.CodingStyle.RichTransient; +using Baked.Test.Orm; using Microsoft.Extensions.Logging; namespace Baked.Test.Business; @@ -93,6 +94,21 @@ public Entity EntityParameters(Entity entity) => public IEnumerable EntityListParameters(IEnumerable entities, Entity[] otherEntities) => [.. entities, .. otherEntities]; + /// + /// Transient description + /// + public RichTransientWithData TransientParameters(RichTransientWithData transient) => + transient; + + /// + /// Transients description + /// + /// + /// Other transients description + /// + public IEnumerable TransientListParameters(IEnumerable transients, RichTransientWithData[] otherTransients) => + [.. transients, .. otherTransients]; + internal Internal Internal() => new(); } \ No newline at end of file