Skip to content

Commit

Permalink
Merge branch 'main' into release/v0-12-0
Browse files Browse the repository at this point in the history
  • Loading branch information
cihandeniz committed Oct 31, 2024
2 parents e8a9eb9 + 16f0bd2 commit 0014115
Show file tree
Hide file tree
Showing 26 changed files with 186 additions and 22 deletions.
4 changes: 2 additions & 2 deletions .vimspector.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"adapter": "netcoredbg",
"configuration": {
"request": "launch",
"program": "${workspaceRoot}/test/recipe/Baked.Test.Recipe.Service.Application/bin/Debug/net6.0/Baked.Test.Recipe.Service.Application.dll",
"cwd": "${workspaceRoot}/test/recipe/Baked.Test.Recipe.Service.Application/bin/Debug/net6.0",
"program": "${workspaceRoot}/test/recipe/Baked.Test.Recipe.Service.Application/bin/Debug/net8.0/Baked.Test.Recipe.Service.Application.dll",
"cwd": "${workspaceRoot}/test/recipe/Baked.Test.Recipe.Service.Application/bin/Debug/net8.0",
"args": [],
"stopAtEntry": false,
"env": {
Expand Down
13 changes: 10 additions & 3 deletions docs/features/coding-style.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ uses the first unique property to discriminate entity records.
```csharp
c => c.EntitySubclassViaComposition()
```
## Namespace as Route

Reflects namespace of a domain class as base route for its endpoints.

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

## Object as JSON

Expand Down Expand Up @@ -94,12 +101,12 @@ 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
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.
their initializers.

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

Expand Down
1 change: 1 addition & 0 deletions docs/recipes/data-source.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Bake.New
| Coding Style(s) | :white_check_mark: | :white_check_mark: |
| | Add/Remove Child | |
| | Command Pattern | |
| | Namespace as Route | |
| | Records are DTOs | |
| | Remaining Services are Singleton | |
| | Scoped by Suffix | |
Expand Down
1 change: 1 addition & 0 deletions docs/recipes/service.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Bake.New
| | Command Pattern | |
| | Entity Extension via Composition | |
| | Entity Subclass via Composition | |
| | Namespace as Route | |
| | Object as JSON | |
| | Records are DTOs | |
| | Remaining Services are Singleton | |
Expand Down
4 changes: 3 additions & 1 deletion docs/release-notes/v0-12.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
for production, `Mock` and `Fake` for development
- `DataSource` recipe is available which includes minimal features for a web
application that only reads data from given database
- `RichTransient` coding style feature is now added
- `RichTransient` coding style feature is now added
- `NamespaceAsRoute` coding style is introduced where namespaces are directly
reflected to the endpoints routes

### Improvements

Expand Down
2 changes: 2 additions & 0 deletions src/recipe/Baked.Recipe.Service.Application/BakeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public static Application Service(this Bake bake,
c => c.CommandPattern(),
c => c.EntityExtensionViaComposition(),
c => c.EntitySubclassViaComposition(),
c => c.NamespaceAsRoute(),
c => c.ObjectAsJson(),
c => c.RecordsAreDtos(),
c => c.RemainingServicesAreSingleton(),
Expand Down Expand Up @@ -130,6 +131,7 @@ public static Application DataSource(this Bake bake,
app.Features.AddCodingStyles([
c => c.AddRemoveChild(),
c => c.CommandPattern(),
c => c.NamespaceAsRoute(),
c => c.RecordsAreDtos(),
c => c.RemainingServicesAreSingleton(),
c => c.ScopedBySuffix(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
namespace Baked.Business;

public class BusinessConfigurator { }
public class BusinessConfigurator;
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Baked.Architecture;
using Baked.Architecture;
using Baked.Business;
using Baked.Domain.Model;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
Expand Down Expand Up @@ -73,6 +74,29 @@ public static bool TryGetMappedMethod(this ApiDescription apiDescription, [NotNu
return result is not null;
}

public static bool TryGetNamespace(this TypeModel type, [NotNullWhen(true)] out string? @namespace)
{
if (!type.TryGetNamespaceAttribute(out var namespaceAttribute))
{
@namespace = null;

return false;
}

@namespace = namespaceAttribute.Value;

return true;
}

public static bool TryGetNamespaceAttribute(this TypeModel type, [NotNullWhen(true)] out NamespaceAttribute? namespaceAttribute)
{
namespaceAttribute = default;

return
type.TryGetMetadata(out var metadata) &&
metadata.TryGetSingle(out namespaceAttribute);
}

public static void SetJsonExample(this IDictionary<string, OpenApiMediaType> mediaTypes, XmlNode? documentation, string @for)
{
var example = documentation.GetExampleCode(@for);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Baked.Business;

[AttributeUsage(AttributeTargets.Class)]
public class CasterAttribute : Attribute { }
public class CasterAttribute : Attribute;
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,18 @@ public static class DomainAssembliesBusinessExtensions
public static DomainAssembliesBusinessFeature DomainAssemblies(this BusinessConfigurator configurator, List<Assembly> assemblies,
Func<IEnumerable<MethodOverloadModel>, MethodOverloadModel>? defaultOverloadSelector = default,
bool addEmbeddedFileProviders = true,
string? baseNamespace = default
string? baseNamespace = default,
Func<TypeModel, bool>? setNamespaceWhen = default
) => configurator.DomainAssemblies(assemblies.Select(a => (a, baseNamespace)),
defaultOverloadSelector: defaultOverloadSelector,
addEmbeddedFileProviders: addEmbeddedFileProviders
addEmbeddedFileProviders: addEmbeddedFileProviders,
setNamespaceWhen: setNamespaceWhen
);

public static DomainAssembliesBusinessFeature DomainAssemblies(this BusinessConfigurator _, IEnumerable<(Assembly, string?)> assemblyDescriptors,
Func<IEnumerable<MethodOverloadModel>, MethodOverloadModel>? defaultOverloadSelector = default,
bool addEmbeddedFileProviders = true
bool addEmbeddedFileProviders = true,
Func<TypeModel, bool>? setNamespaceWhen = default
) => new(
assemblyDescriptors,
defaultOverloadSelector ?? (overloads =>
Expand All @@ -30,7 +33,8 @@ public static DomainAssembliesBusinessFeature DomainAssemblies(this BusinessConf
overloads.FirstWithMostParametersOrDefault() ??
throw new($"Method without an overload should not exist")
),
addEmbeddedFileProviders
addEmbeddedFileProviders,
setNamespaceWhen ?? (t => true)
);

public static void AddAction(this ControllerModel controller, TypeModel type, MethodModel method) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Baked.Architecture;
using Baked.Architecture;
using Baked.Domain;
using Baked.Domain.Configuration;
using Baked.Domain.Model;
Expand All @@ -15,9 +15,12 @@ namespace Baked.Business.DomainAssemblies;
public class DomainAssembliesBusinessFeature(
IEnumerable<(Assembly assembly, string? baseNamespace)> _assemblyDescriptors,
Func<IEnumerable<MethodOverloadModel>, MethodOverloadModel> _defaultOverloadSelector,
bool _addEmbeddedFileProviders
bool _addEmbeddedFileProviders,
Func<TypeModel, bool> setNamespaceWhen
) : IFeature<BusinessConfigurator>
{
Dictionary<Assembly, string> BaseNamespaces { get; } = _assemblyDescriptors.ToDictionary(kvp => kvp.assembly, kvp => kvp.baseNamespace ?? kvp.assembly.GetName().Name ?? string.Empty);

public void Configure(LayerConfigurator configurator)
{
configurator.ConfigureDomainTypeCollection(types =>
Expand Down Expand Up @@ -64,6 +67,24 @@ public void Configure(LayerConfigurator configurator)
builder.Index.Type.Add<ServiceAttribute>();
builder.Index.Type.Add<CasterAttribute>();

builder.Conventions.AddTypeMetadata(
apply: (context, add) =>
{
var @namespace = context.Type.Namespace ?? string.Empty;
context.Type.Apply(t =>
{
if (!BaseNamespaces.TryGetValue(t.Assembly, out var baseNamespace)) { return; }

@namespace =
@namespace == baseNamespace ? string.Empty :
@namespace.StartsWith(baseNamespace) ? @namespace[(baseNamespace.Length + 1)..] :
@namespace;
});

add(context.Type, new NamespaceAttribute(@namespace));
},
when: c => setNamespaceWhen(c.Type)
);
builder.Conventions.AddTypeMetadata(new ServiceAttribute(),
when: c =>
c.Type.IsPublic &&
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Baked.Business;

[AttributeUsage(AttributeTargets.Method)]
public class ExternalAttribute : Attribute { }
public class ExternalAttribute : Attribute;
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Baked.Business;

[AttributeUsage(AttributeTargets.Class)]
public class LocatableAttribute : Attribute { }
public class LocatableAttribute : Attribute;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Baked.Business;

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class NamespaceAttribute(string value)
: Attribute
{
public string Value { get; } = value;
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@ public void Configure(LayerConfigurator configurator)
parameterTypeMetadata.Has<EntityAttribute>(),
order: 10
);
builder.Conventions.RemoveTypeMetadata<NamespaceAttribute>(c => c.Type.Has<EntityExtensionAttribute>(), order: 10);
builder.Conventions.AddTypeMetadata(
apply: (c, add) =>
{
var domain = configurator.Context.GetDomainModel();
var entityType = c.Type.GetSingle<EntityExtensionAttribute>().EntityType;
var entityTypeModel = domain.Types[entityType];
if (!entityTypeModel.TryGetNamespaceAttribute(out var namespaceAttribute)) { return; }

add(c.Type, namespaceAttribute);
},
when: c => c.Type.Has<EntityExtensionAttribute>(),
order: 10
);
builder.Conventions.AddTypeMetadata(
apply: (c, add) =>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Baked.CodingStyle;
using Baked.CodingStyle.NamespaceAsRoute;

namespace Baked;

public static class NamespaceAsRouteCodingStyleExtensions
{
public static NamespaceAsRouteCodingStyleFeature NamespaceAsRoute(this CodingStyleConfigurator _) =>
new();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Baked.Architecture;

namespace Baked.CodingStyle.NamespaceAsRoute;

public class NamespaceAsRouteCodingStyleFeature : IFeature<CodingStyleConfigurator>
{
public void Configure(LayerConfigurator configurator)
{
configurator.ConfigureApiModelConventions(conventions =>
{
conventions.Add(new UseNamespaceForBaseRouteConvention(), order: int.MaxValue - 10);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Baked.RestApi.Configuration;

namespace Baked.CodingStyle.NamespaceAsRoute;

public class UseNamespaceForBaseRouteConvention
: IApiModelConvention<ActionModelContext>
{
public void Apply(ActionModelContext context)
{
if (!context.Controller.MappedType.TryGetNamespace(out var @namespace)) { return; }

var baseRoute = @namespace.Split(".");

context.Action.RouteParts.InsertRange(0, baseRoute);
foreach (var routeParameter in context.Action.Parameters.Where(pm => pm.FromRoute))
{
routeParameter.RoutePosition += baseRoute.Length;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,15 @@ public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)

if (!unifiedActions.TryGetValue(action.GroupName, out var unifiedAction))
{
unifiedActions.Add(action.GroupName, unifiedAction = new($"/{route.Template.Split('/').First()}"));
var basePathParts = new List<string>();
foreach (var part in route.Template.Split('/'))
{
if (part.Contains("{")) { break; }

basePathParts.Add(part);
}

unifiedActions.Add(action.GroupName, unifiedAction = new($"/{basePathParts.Join('/')}"));
}

unifiedAction.PathsToRemove.Add($"/{route.Template.Replace(":guid", string.Empty)}");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ protected static void Init(
app.Features.AddCodingStyles([
c => c.AddRemoveChild(),
c => c.CommandPattern(),
c => c.NamespaceAsRoute(),
c => c.RecordsAreDtos(),
c => c.RemainingServicesAreSingleton(),
c => c.ScopedBySuffix(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@ public DomainModel Build(IDomainTypeCollection types)
}
while (!_buildQueue.IsEmpty);

var result = new DomainModel(new(_references.Select(t => t.Model)));
return new(new(_references.Select(t => t.Model)));

}

public void PostBuild(DomainModel result)
{
ApplyConventions(result);
BuildIndices(result);

return result;
}

TypeModel.Factory GetFactory(Type t)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ protected override void Initialize(IDomainTypeCollection domainTypes)
var model = builder.Build(domainTypes);

Context.Add(model);
builder.PostBuild(model);
}
}
}
1 change: 1 addition & 0 deletions src/recipe/Baked.Recipe.Service.Application/ServiceSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ protected static void Init(
c => c.CommandPattern(),
c => c.EntityExtensionViaComposition(),
c => c.EntitySubclassViaComposition(),
c => c.NamespaceAsRoute(),
c => c.ObjectAsJson(),
c => c.RecordsAreDtos(),
c => c.RemainingServicesAreSingleton(),
Expand Down
5 changes: 4 additions & 1 deletion test/recipe/Baked.Test.Recipe.Service.Application/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

Bake.New
.Service(
business: c => c.DomainAssemblies([typeof(Entity).Assembly], baseNamespace: "Baked.Test"),
business: c => c.DomainAssemblies([typeof(Entity).Assembly],
baseNamespace: "Baked.Test",
setNamespaceWhen: t => t.Namespace is not null && t.Namespace.StartsWith("Baked.Test.CodingStyle.NamespaceAsRoute")
),
authentications: [
c => c.FixedBearerToken(
tokens =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Net;

namespace Baked.Test.CodingStyle;

public class RoutingByNamespace : TestServiceNfr
{
[TestCase("Post", "coding-style/namespace-as-route/route-sample/method")]
public async Task Namespace_is_used_as_base_route_for_the_methods_under_certain_namespace(string method, string action)
{
var response = await Client.SendAsync(new(HttpMethod.Parse(method), $"/{action}"));

response.StatusCode.ShouldNotBe(HttpStatusCode.NotFound);
}
}
Loading

0 comments on commit 0014115

Please sign in to comment.