Skip to content

Commit

Permalink
Merge pull request #187 from dncsvr/feature/rich-transients
Browse files Browse the repository at this point in the history
  • Loading branch information
dncsvr authored Oct 31, 2024
2 parents 0d88ec4 + 6aeeec8 commit 2102b5f
Show file tree
Hide file tree
Showing 40 changed files with 615 additions and 82 deletions.
20 changes: 16 additions & 4 deletions docs/features/coding-style.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,7 @@ c => c.AddRemoveChild()

## Command Pattern

Configures public methods of transient services as api methods. This coding
style allows you to have a public initializer (`With`) with parameters which
will render as query parameters. It also removes `Execute` and `Process` names
from route.
Uses class names as route and removes `Execute` and `Process` names from route.

```csharp
c => c.CommandPattern()
Expand Down Expand Up @@ -95,6 +92,21 @@ Configures routes and swagger docs to use entity methods as resource actions.
c => c.RichEntity()
```

## Rich Transient

Configures transient services as api services. This coding style allows you to
have a public initializer (`With`) with parameters which will render as query
parameters or single `id` parameter wich will render from route.

Rich transients with `id` types can be method parameters and located using
their initializers.

Configures routes and swagger docs to use entity methods as resource actions.

```csharp
c => c.RichTransient()
```

## Scoped by Suffix

Adds `ScopedAttribute` to the services that has name with any of the given
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public static Application Service(this Bake bake,
c => c.RecordsAreDtos(),
c => c.RemainingServicesAreSingleton(),
c => c.RichEntity(),
c => c.RichTransient(),
c => c.ScopedBySuffix(),
c => c.SingleByUnique(),
c => c.UriReturnIsRedirect(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ .. method.DefaultOverload.Parameters.Select(p =>
}
);

public static bool IsTarget(this RestApi.Model.ParameterModel parameter) =>
parameter.Id == "target";

public static MethodOverloadModel? FirstPublicInstanceWithMostParametersOrDefault(this IEnumerable<MethodOverloadModel> overloads) =>
overloads
.Where(o => o.IsPublic && !o.IsStatic)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using Baked.Architecture;
using Baked.CodeGeneration;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Reflection;

namespace Baked;
Expand Down Expand Up @@ -60,4 +62,20 @@ public static GeneratedAssemblyDescriptor AddReferences(this GeneratedAssemblyDe

return descriptor;
}

internal static string? FindClosestScopedCode(this Diagnostic diagnostic)
{
var tree = diagnostic.Location.SourceTree;
if (tree is null) { return null; }

return GetScopeNode(tree.GetRoot().FindNode(diagnostic.Location.SourceSpan))?.ToString() ?? diagnostic.Location.SourceTree?.ToString();
}

static SyntaxNode? GetScopeNode(SyntaxNode? node)
{
if (node is null) { return null; }
if (node is MethodDeclarationSyntax or ClassDeclarationSyntax) { return node; }

return GetScopeNode(node?.Parent);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,14 @@ public Assembly Compile()
var errors = new StringBuilder();
foreach (var diagnostic in failures)
{
errors.AppendLine();
errors.AppendLine(diagnostic.GetMessage());
errors.AppendLine();
errors.AppendLine(diagnostic.FindClosestScopedCode());
errors.AppendLine();
}

throw new Exception($"""
{errors}

{string.Join(Environment.NewLine, _descriptor.Codes)}
""");
throw new Exception($"{errors}");
}

ms.Seek(0, SeekOrigin.Begin);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ public void Configure(LayerConfigurator configurator)

configurator.ConfigureApiModelConventions(conventions =>
{
conventions.Add(new InitializeUsingQueryParametersConvention(), order: -10);
conventions.Add(new IncludeClassDocsForActionNamesConvention(_methodNames), order: -10);
conventions.Add(new UseClassNameInsteadOfActionNamesConvention(_methodNames), order: -10);
conventions.Add(new RemoveFromRouteConvention(_methodNames));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ public void Configure(LayerConfigurator configurator)
{
var domain = configurator.Context.GetDomainModel();
conventions.Add(new TargetEntityExtensionFromRouteConvention(domain));
conventions.Add(new TargetEntityExtensionFromRouteByUniquePropertiesConvention(domain));
conventions.Add(new EntityExtensionsUnderEntitiesConvention(domain));
conventions.Add(new LookupEntityExtensionByIdConvention(domain));
conventions.Add(new LookupEntityExtensionsByIdsConvention(domain));
conventions.Add(new TargetEntityExtensionFromRouteConvention(domain), order: 20);
conventions.Add(new TargetEntityExtensionFromRouteByUniquePropertiesConvention(domain), order: 20);
});
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Baked.Business;
using Baked.Domain.Model;
using Baked.Domain.Model;
using Baked.RestApi.Configuration;
using System.Diagnostics.CodeAnalysis;

Expand All @@ -10,9 +9,7 @@ public class LookupEntityExtensionByIdConvention(DomainModel _domain)
{
public void Apply(ParameterModelContext context)
{
if (context.Action.MappedMethod is null) { return; }
if (context.Action.MappedMethod.Has<InitializerAttribute>()) { return; }
if (!context.Parameter.IsInvokeMethodParameter) { return; }
if (context.Parameter.IsTarget()) { return; }

var entityExtensionType = context.Parameter.TypeModel;
if (!entityExtensionType.TryGetEntityTypeFromExtension(_domain, out var entityType)) { return; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public void Apply(ParameterModelContext context)
{
if (context.Action.MappedMethod is null) { return; }
if (context.Action.MappedMethod.Has<InitializerAttribute>()) { return; }
if (context.Parameter.IsInvokeMethodParameter) { return; }
if (!context.Parameter.IsTarget()) { return; }

var entityExtensionType = context.Parameter.TypeModel;
if (!entityExtensionType.TryGetEntityTypeFromExtension(_domain, out var entityType)) { return; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@ public void Apply(ParameterModelContext context)
if (context.Action.MappedMethod is null) { return; }
if (!context.Action.MappedMethod.Has<InitializerAttribute>()) { return; }

context.Parameter.Name = "newTarget";
context.Parameter.Type = $"Func<{context.Parameter.Type}>";

context.Action.FindTargetStatement = "newTarget()";
context.Action.RouteParts = [entityType.Name.Pluralize(), subclassName];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ public void Configure(LayerConfigurator configurator)
{
var domain = configurator.Context.GetDomainModel();
conventions.Add(new TargetEntitySubclassFromRouteConvention(domain));
conventions.Add(new EntitySubclassUnderEntitiesConvention(domain));
conventions.Add(new EntitySubclassInitializerIsPostResourceConvention(domain));
conventions.Add(new TargetEntitySubclassFromRouteConvention(domain), order: 20);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public void Apply(ParameterModelContext context)
{
if (context.Action.MappedMethod is null) { return; }
if (context.Action.MappedMethod.Has<InitializerAttribute>()) { return; }
if (context.Parameter.IsInvokeMethodParameter) { return; }
if (!context.Parameter.IsTarget()) { return; }

var entitySubclassType = context.Parameter.TypeModel;
if (!entitySubclassType.TryGetSubclassName(out var subclassName)) { return; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,17 @@

namespace Baked.CodingStyle.RichEntity;

public class EntityInitializerIsPostResourceConvention : IApiModelConvention<ParameterModelContext>
public class EntityInitializerIsPostResourceConvention : IApiModelConvention<ActionModelContext>
{
public void Apply(ParameterModelContext context)
public void Apply(ActionModelContext context)
{
if (context.Parameter.IsInvokeMethodParameter) { return; }
if (!context.Parameter.TypeModel.TryGetMetadata(out var metadata)) { return; }
if (context.Controller.MappedType is null) { return; }
if (!context.Controller.MappedType.TryGetMetadata(out var metadata)) { return; }
if (!metadata.Has<EntityAttribute>()) { return; }
if (context.Action.MappedMethod is null) { return; }
if (!context.Action.MappedMethod.Has<InitializerAttribute>()) { return; }

context.Parameter.Name = "newTarget";
context.Parameter.Type = $"Func<{context.Parameter.Type}>";

context.Action.FindTargetStatement = "newTarget()";
context.Action.RouteParts = [context.Parameter.TypeModel.Name.Pluralize()];
context.Action.Method = HttpMethod.Post;
context.Action.RouteParts = [context.Controller.MappedType.Name.Pluralize()];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Baked.Business;
using Baked.Domain.Model;
using Baked.Orm;
using Baked.RestApi.Configuration;
using Baked.RestApi.Model;
using Humanizer;

namespace Baked.CodingStyle.RichEntity;

public class FindTargetUsingQueryContextConvention(DomainModel _domain)
: IApiModelConvention<ActionModelContext>
{
public void Apply(ActionModelContext context)
{
if (context.Action.MappedMethod is null) { return; }
if (context.Action.MappedMethod.Has<InitializerAttribute>()) { return; }
if (context.Controller.MappedType is null) { return; }
if (!context.Controller.MappedType.TryGetMetadata(out var metadata)) { return; }
if (!metadata.Has<EntityAttribute>()) { return; }

var entityType = context.Controller.MappedType;
if (!entityType.TryGetQueryContextType(_domain, out var queryContextType)) { return; }

var idProperty = entityType.GetMembers().Properties["Id"];

var target = context.Action.Parameters.Single(p => p.IsTarget());
target.Name = "id";
target.From = ParameterModelFrom.Route;
target.RoutePosition = 1;
target.AdditionalAttributes.Add($"SwaggerSchema(\"Unique value to find {context.Controller.MappedType.Name.Humanize().ToLowerInvariant()} resource\")");
target.Type = idProperty.PropertyType.CSharpFriendlyFullName;

var queryContextParameter = context.Action.AddQueryContextAsService(queryContextType);
context.Action.RouteParts = [entityType.Name.Pluralize(), context.Action.Name];
context.Action.FindTargetStatement = queryContextParameter.BuildSingleBy("id", fromRoute: true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public void Configure(LayerConfigurator configurator)
conventions.Add(new EntityUnderEntitiesConvention());
conventions.Add(new EntityInitializerIsPostResourceConvention());
conventions.Add(new TargetEntityFromRouteConvention(domainModel));
conventions.Add(new FindTargetUsingQueryContextConvention(domainModel), order: 20);
});
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Baked.Business;
using Baked.RestApi.Configuration;
using Baked.RestApi.Model;
using Humanizer;

namespace Baked.CodingStyle.RichTransient;

public class AddIdParameterToRouteConvention : IApiModelConvention<ActionModelContext>
{
public void Apply(ActionModelContext context)
{
if (!context.Controller.MappedType.TryGetMembers(out var members)) { return; }
if (!members.Methods.Having<InitializerAttribute>().Any()) { return; }
if (!members.Has<LocatableAttribute>()) { return; }
if (context.Action.MappedMethod is null) { return; }
if (context.Action.MappedMethod.Has<InitializerAttribute>()) { return; }

var initializer = members.Methods.Having<InitializerAttribute>().Single();
if (!initializer.DefaultOverload.Parameters.TryGetValue("id", out var parameter)) { return; }

context.Action.Parameter["id"] =
new(parameter.ParameterType, ParameterModelFrom.Route, parameter.Name, MappedParameter: parameter)
{
IsOptional = parameter.IsOptional,
DefaultValue = parameter.DefaultValue,
IsInvokeMethodParameter = false,
RoutePosition = 1,
AdditionalAttributes = [$"SwaggerSchema(\"Unique value to find {context.Controller.MappedType.Name.Humanize().ToLowerInvariant()} resource\")"]
};
context.Action.RouteParts = [context.Controller.MappedType.Name.Pluralize(), context.Action.Name];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
using Baked.RestApi.Configuration;
using Baked.RestApi.Model;

namespace Baked.CodingStyle.CommandPattern;
namespace Baked.CodingStyle.RichTransient;

public class InitializeUsingQueryParametersConvention : IApiModelConvention<ActionModelContext>
public class AddInitializerParametersToQueryConvention : IApiModelConvention<ActionModelContext>
{
public void Apply(ActionModelContext context)
{
if (!context.Controller.MappedType.TryGetMembers(out var members)) { return; }
if (!members.Has<PubliclyInitializableAttribute>()) { return; }
if (!members.Methods.Having<InitializerAttribute>().Any()) { return; }
if (members.Has<LocatableAttribute>()) { return; }

var initializer = members.Methods.Having<InitializerAttribute>().Single();
foreach (var parameter in initializer.DefaultOverload.Parameters)
Expand All @@ -22,11 +23,5 @@ public void Apply(ActionModelContext context)
IsInvokeMethodParameter = false
};
}

var targetParameter = context.Action.Parameter["target"];
targetParameter.Name = "newTarget";
targetParameter.Type = $"Func<{targetParameter.Type}>";

context.Action.FindTargetStatement = $"newTarget().{initializer.Name}({initializer.DefaultOverload.Parameters.Select(p => $"@{p.Name}").Join(", ")})";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Baked.Business;
using Baked.RestApi.Configuration;

namespace Baked.CodingStyle.RichTransient;

public class FindTargetUsingInitializerConvention : IApiModelConvention<ActionModelContext>
{
public void Apply(ActionModelContext context)
{
if (!context.Controller.MappedType.TryGetMembers(out var members)) { return; }
if (!members.Methods.Having<InitializerAttribute>().Any()) { return; }
if (context.Action.MappedMethod is null) { return; }
if (context.Action.MappedMethod.Has<InitializerAttribute>()) { return; }

var initializer = members.Methods.Having<InitializerAttribute>().Single();
var initilzerParameters = context.Action.Parameters.Where(p => !p.FromServices && !p.IsInvokeMethodParameter && (p.FromQuery || p.FromRoute));
context.Action.FindTargetStatement = $"target.{initializer.Name}({initilzerParameters.Select(p => $"{p.InternalName}: {p.RenderLookup($"@{p.Name}")}").Join(", ")})";
}
}
Loading

0 comments on commit 2102b5f

Please sign in to comment.