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 / Reporting Beta #185

Merged
merged 10 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
44 changes: 44 additions & 0 deletions docs/features/reporting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Reporting

Implementations of this feature registers a singleton `IReportContext` with
which you can read raw data from database. Add this feature using
`AddReporting()` extension;

```csharp
app.Features.AddReporting(...);
```

## Fake

Adds a fake report context that allows you to return data directly from `.json`
resources.

```csharp
c => c.Fake(basePath: "Fake")
```

## Mock

Adds a mock instance of report context to be used during spec tests.

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

## Native SQL

Adds a report context instance that uses a `IStatelessSession` instance to
execute native SQL queries read from `.sql` resources in your project.

```csharp
c => c.NativeSql(basePath: "Queries/MySql")
```

> [!TIP]
>
> You may group your RDBMS specific queries in different folders, and use
> setting to specify which folder to use depending on environment.
>
> ```csharp
> c => c.NativeSql(basePath: Settings.Required("Reporting:NativeSql:BasePath"))
> ```
2 changes: 1 addition & 1 deletion docs/recipes/service.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Bake.New
| | With Method | |
| Communication | :white_check_mark: HTTP | :white_check_mark: Mock |
| Core | :white_check_mark: Dotnet | :white_check_mark: Mock |
| Cors(s) | :white_check_mark: Disabled | :no_entry: |
| Cors | :white_check_mark: Disabled | :no_entry: |
| Database | :white_check_mark: Sqlite | :white_check_mark: In Memory |
| Exception Handling | :white_check_mark: Default | :white_check_mark: |
| Greeting | :white_check_mark: Swagger | :no_entry: |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,19 @@ public static class AssertionExtensions
public static void ShouldFail(this Spec _, string message = "") =>
throw new AssertionException(message);

[DoesNotReturn]
public static Task ShouldFailAsync(this Spec _, string message = "") =>
throw new AssertionException(message);

[DoesNotReturn]
public static void ShouldPass(this Spec _, string message = "") =>
Assert.Pass(message);

[DoesNotReturn]
public static Task ShouldPassAsync(this Spec _, string message = "")
{
Assert.Pass(message);

return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,6 @@ protected override PhaseContext GetContext(AddServices phase)

services.AddSingleton(sp => _fluentConfiguration.BuildConfiguration());
services.AddSingleton(sp => sp.GetRequiredService<NHConfiguration>().BuildSessionFactory());
services.AddScoped(sp => sp.GetRequiredService<ISessionFactory>().OpenSession());
services.AddSingleton<Func<ISession>>(sp => () => sp.UsingCurrentScope().GetRequiredService<ISession>());
})
.Build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace Baked.Database.InMemory;

public class InMemoryDatabaseFeature : IFeature<DatabaseConfigurator>
{
ISession? _globalSession;
IStatelessSession? _globalSession;

public void Configure(LayerConfigurator configurator)
{
Expand All @@ -28,7 +28,7 @@ public void Configure(LayerConfigurator configurator)
initializations.AddInitializer(sf =>
{
// In memory db is disposed when last connection is closed, this connection is to keep the db open
_globalSession = sf.OpenSession();
_globalSession = sf.OpenStatelessSession();

sp.GetRequiredService<Configuration>().ExportSchema(false, true, false, _globalSession.Connection);
});
Expand All @@ -48,6 +48,5 @@ public void Configure(LayerConfigurator configurator)
spec.GiveMe.TheSession().Clear();
});
});

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ public void Configure(LayerConfigurator configurator)
configurator.ConfigureServiceCollection(services =>
{
services.AddFromAssembly(configurator.Context.GetGeneratedAssembly(nameof(AutoMapOrmFeature)));
services.AddScoped(sp => sp.GetRequiredService<ISessionFactory>().OpenSession());
services.AddSingleton<Func<ISession>>(sp => () => sp.UsingCurrentScope().GetRequiredService<ISession>());
services.AddScoped(typeof(IEntityContext<>), typeof(EntityContext<>));
services.AddSingleton(typeof(IQueryContext<>), typeof(QueryContext<>));
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Text.RegularExpressions;

namespace Baked.Reporting.Fake;

public record FakeData(
Dictionary<string, string?>? Parameters,
List<Dictionary<string, object?>> Result
)
{
public bool Matches(Dictionary<string, object> parameters)
{
if (Parameters is null) { return true; }

foreach (var (key, pattern) in Parameters)
{
if (!parameters.ContainsKey(key))
{
return false;
}

if (pattern is null) { continue; }
if (Regex.IsMatch($"{parameters[key]}", pattern)) { continue; }

return false;
}

return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Baked.Reporting;
using Baked.Reporting.Fake;
using Baked.Testing;
using Microsoft.Extensions.FileProviders;

namespace Baked;

public static class FakeReportingExtensions
{
public static FakeReportingFeature Fake(this ReportingConfigurator _) =>
new();

public static IReportContext AFakeReportContext(this Stubber giveMe,
string basePath = "Fake"
) => new ReportContext(giveMe.The<IFileProvider>(), new(basePath));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Baked.Architecture;
using Microsoft.Extensions.DependencyInjection;

namespace Baked.Reporting.Fake;

public class FakeReportingFeature : IFeature<ReportingConfigurator>
{
public void Configure(LayerConfigurator configurator)
{
configurator.ConfigureServiceCollection(services =>
{
services.AddSingleton<IReportContext, ReportContext>();
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Microsoft.Extensions.FileProviders;
using Newtonsoft.Json;

namespace Baked.Reporting.Fake;

public class ReportContext(IFileProvider _fileProvider, ReportOptions _options)
: IReportContext
{
public async Task<object?[][]> Execute(string queryName, Dictionary<string, object> parameters)
{
var dataPath = $"/{Path.Join(_options.BasePath, $"{queryName}.json")}";
if (!_fileProvider.Exists(dataPath)) { throw new QueryNotFoundException(queryName); }

var dataString = await _fileProvider.ReadAsStringAsync(dataPath) ?? string.Empty;

var fakes = JsonConvert.DeserializeObject<List<FakeData>>(dataString) ?? new();
var match = fakes.FirstOrDefault(fake => fake.Matches(parameters));
if (match is null) { return []; }

return match.Result.Select(row => row.Values.ToArray()).ToArray();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using Baked.Runtime;

namespace Baked.Reporting.Fake;

public record ReportOptions(Setting<string> BasePath);
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Baked.Reporting;
using Baked.Reporting.Mock;

namespace Baked;

public static class MockReportingExtensions
{
public static MockReportingFeature Mock(this ReportingConfigurator _) =>
new();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Baked.Architecture;

namespace Baked.Reporting.Mock;

public class MockReportingFeature : IFeature<ReportingConfigurator>
{
public void Configure(LayerConfigurator configurator)
{
configurator.ConfigureTestConfiguration(test =>
{
test.Mocks.Add<IReportContext>(singleton: true);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Baked.Reporting;
using Baked.Reporting.NativeSql;
using Baked.Runtime;

namespace Baked;

public static class NativeSqlReportingExtensions
{
public static NativeSqlReportingFeature NativeSql(this ReportingConfigurator _,
Setting<string>? basePath = default
) => new(basePath ?? string.Empty);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Baked.Architecture;
using Baked.Runtime;
using Microsoft.Extensions.DependencyInjection;
using NHibernate;

namespace Baked.Reporting.NativeSql;

public class NativeSqlReportingFeature(Setting<string> _basePath)
: IFeature<ReportingConfigurator>
{
public void Configure(LayerConfigurator configurator)
{
configurator.ConfigureConfigurationBuilder(configuration =>
{
configuration.AddJsonAsDefault($$"""
{
"Logging": {
"LogLevel": {
"NHibernate": "None",
"NHibernate.Sql": "{{(configurator.IsDevelopment() ? "Debug" : "None")}}"
}
}
}
""");
});

configurator.ConfigureServiceCollection(services =>
{
services.AddSingleton(new ReportOptions(_basePath));
services.AddSingleton<IReportContext, ReportContext>();
services.AddScoped(sp => sp.GetRequiredService<ISessionFactory>().OpenStatelessSession());
services.AddSingleton<Func<IStatelessSession>>(sp => () => sp.UsingCurrentScope().GetRequiredService<IStatelessSession>());
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Microsoft.Extensions.FileProviders;

namespace Baked.Reporting.NativeSql;

public class ReportContext(IFileProvider _fileProvider, Func<NHibernate.IStatelessSession> _getStatelessSession, ReportOptions _options)
: IReportContext
{
public async Task<object?[][]> Execute(string queryName, Dictionary<string, object> parameters)
{
var queryPath = $"/{Path.Join(_options.BasePath, $"{queryName}.sql")}";
if (!_fileProvider.Exists(queryPath))
{
throw new QueryNotFoundException(queryName);
}

var queryString = await _fileProvider.ReadAsStringAsync(queryPath);
var query = _getStatelessSession().CreateSQLQuery(queryString);
foreach (var (name, value) in parameters)
{
query.SetParameter(name, value);
}

var result = await query.ListAsync();

return result.Cast<object[]>().ToArray();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using Baked.Runtime;

namespace Baked.Reporting.NativeSql;

public record ReportOptions(Setting<string> BasePath);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Baked.Reporting;

public class ReportingConfigurator { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using Baked.Architecture;
using Baked.Reporting;
using Baked.Testing;
using Moq;

namespace Baked;

public static class ReportingExtensions
{
public static void AddReporting(this List<IFeature> features, Func<ReportingConfigurator, IFeature<ReportingConfigurator>> configure) =>
features.Add(configure(new()));

public static IReportContext TheReportContext(this Mocker mockMe,
object?[][]? data = default
)
{
data ??= [];

var result = Mock.Get(mockMe.Spec.GiveMe.The<IReportContext>());

if (data is not null)
{
result
.Setup(df => df.Execute(It.IsAny<string>(), It.IsAny<Dictionary<string, object>>()))
.ReturnsAsync(data);
}

return result.Object;
}

public static void VerifyExecute(this IReportContext dataFetcher,
string? queryName = default,
(string key, object value)? parameter = default,
List<(string key, object value)>? parameters = default
)
{
parameters ??= parameter is not null ? [parameter.Value] : [];

Mock.Get(dataFetcher).Verify(
df => df.Execute(
It.Is<string>(q => queryName == null || q == queryName),
It.Is<Dictionary<string, object>>(p =>
parameters.All((kvp) => p.ContainsKey(kvp.key) && Equals(p[kvp.key], kvp.value))
)
)
);
}
}
6 changes: 6 additions & 0 deletions src/recipe/Baked.Recipe.Service/Reporting/IReportContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Baked.Reporting;

public interface IReportContext
{
Task<object?[][]> Execute(string queryName, Dictionary<string, object> parameters);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
namespace Baked.Reporting;

public class QueryNotFoundException(string queryName)
: Exception($"No query file with '{queryName}' was found");
Loading