Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature / Rich Transients #187

Merged
merged 30 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
2817ba4
init `feature/rich-transients`
dncsvr Oct 23, 2024
2a9b6ab
implement rich transient coding style - !!! TESTS FAILING !!!
dncsvr Oct 23, 2024
8e8e51a
fix failing tests
dncsvr Oct 23, 2024
4dc31cc
add rich transient with no data sample
dncsvr Oct 24, 2024
3f6bb60
merge from main
dncsvr Oct 24, 2024
427226e
use `Locatable` attribute in rich transient conventions
dncsvr Oct 24, 2024
bf48f63
move initialize using query parameters convention to rich transient f…
dncsvr Oct 24, 2024
d29e94b
merge from main
dncsvr Oct 24, 2024
ccd58ce
refactor rich transient coding style
dncsvr Oct 28, 2024
1b8ee83
add using rich transients as parameters test case
dncsvr Oct 28, 2024
2435d70
fix unused parameter issue
dncsvr Oct 28, 2024
8ead5c1
minor refactoring
dncsvr Oct 28, 2024
11c8edf
review changes
dncsvr Oct 29, 2024
405247d
edit rich transient api convention orders closer to `0` when available
dncsvr Oct 29, 2024
b159249
fix typo
dncsvr Oct 30, 2024
a666ef7
rename rich transient lookup conventions
dncsvr Oct 30, 2024
7792685
refactor rich transient feature and conventions
dncsvr Oct 30, 2024
48326ae
add initialize command with rich transient case
dncsvr Oct 30, 2024
8ecbbc3
add command with entity case - !!! RUN AND TESTS FAILING !!!
dncsvr Oct 30, 2024
1f1a7ae
fix failing run and tests
dncsvr Oct 30, 2024
0b7aedd
fix format
dncsvr Oct 30, 2024
bc8ad6e
merge from `main`
dncsvr Oct 30, 2024
a15ece7
fix build error
dncsvr Oct 30, 2024
f000547
refine compiler diagnostic error messages - !!! RUN AND TESTS FAILING…
dncsvr Oct 30, 2024
db81855
fix failing run and tests
dncsvr Oct 31, 2024
a362022
add rich transient nullable support
dncsvr Oct 31, 2024
6c0b5fe
add `IsTarget` helper for parameter model
dncsvr Oct 31, 2024
71e7e92
update coding styles documentation
dncsvr Oct 31, 2024
c0556bf
use `IsTarget` helper
dncsvr Oct 31, 2024
6aeeec8
merge from main
dncsvr Oct 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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