Skip to content
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@

A full-stack web application with .NET Web API backend and Angular frontend, featuring comprehensive observability with Grafana, Prometheus, Tempo, and Loki, plus fun Databricks integration for banana analytics! 🍌

Built using enterprise architecture patterns including Clean Architecture, CQRS, and Repository Pattern for maintainability and scalability.

> **🚀 [Quick Start Guide](QUICKSTART.md)** - Get up and running in minutes!
>
> **🏛️ [Enterprise Architecture Patterns](docs/ENTERPRISE_ARCHITECTURE_PATTERNS.md)** - Clean Architecture, CQRS, Repository Pattern
>
> **📊 [Architecture Visual Guide](docs/ARCHITECTURE_VISUAL_GUIDE.md)** - Diagrams and visual documentation
>
> **🍌 [Databricks Integration](docs/DATABRICKS_INTEGRATION.md)** - Banana analytics powered by Databricks
>
> **📊 [Observability Guide](observability/README.md)** - Complete monitoring and tracing documentation
Expand Down
28 changes: 28 additions & 0 deletions backend/GrafanaBanana.Api/Application/DependencyInjection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using GrafanaBanana.Api.Domain.Repositories;
using GrafanaBanana.Api.Infrastructure.Repositories;
using System.Reflection;

namespace GrafanaBanana.Api.Application;

/// <summary>
/// Extension methods for registering application layer services
/// Implements Dependency Injection pattern for loose coupling
/// </summary>
public static class DependencyInjection
{
/// <summary>
/// Registers all application layer services including MediatR, repositories, and handlers
/// Following the Dependency Inversion Principle - depending on abstractions, not concretions
/// </summary>
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
{
// Register MediatR for CQRS pattern
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()));

// Register repositories (Repository Pattern)
services.AddScoped<IWeatherForecastRepository, WeatherForecastRepository>();
services.AddScoped<IBananaAnalyticsRepository, BananaAnalyticsRepository>();

return services;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using GrafanaBanana.Api.Application.Queries;
using GrafanaBanana.Api.Databricks;
using GrafanaBanana.Api.Domain.Repositories;
using MediatR;

namespace GrafanaBanana.Api.Application.Handlers;

/// <summary>
/// Handler for GetBananaAnalyticsQuery
/// Implements the business logic for retrieving banana analytics
/// Following CQRS and Mediator patterns
/// </summary>
public class GetBananaAnalyticsQueryHandler : IRequestHandler<GetBananaAnalyticsQuery, BananaAnalytics>
{
private readonly IBananaAnalyticsRepository _repository;
private readonly ILogger<GetBananaAnalyticsQueryHandler> _logger;

public GetBananaAnalyticsQueryHandler(
IBananaAnalyticsRepository repository,
ILogger<GetBananaAnalyticsQueryHandler> logger)
{
_repository = repository;
_logger = logger;
}

public async Task<BananaAnalytics> Handle(GetBananaAnalyticsQuery request, CancellationToken cancellationToken)
{
_logger.LogInformation("Handling GetBananaAnalyticsQuery");

var analytics = await _repository.GetBananaAnalyticsAsync(cancellationToken);

_logger.LogInformation(
"Retrieved banana analytics: {ProductionTons} tons produced, {Revenue} revenue, {Countries} countries served",
analytics.Summary.TotalProductionTons,
analytics.Summary.TotalRevenue,
analytics.Summary.CountriesServed);

return analytics;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using GrafanaBanana.Api.Application.Queries;
using GrafanaBanana.Api.Databricks;
using GrafanaBanana.Api.Domain.Repositories;
using MediatR;

namespace GrafanaBanana.Api.Application.Handlers;

/// <summary>
/// Handler for GetBananaProductionQuery
/// Implements the business logic for retrieving banana production data
/// Following CQRS and Mediator patterns
/// </summary>
public class GetBananaProductionQueryHandler : IRequestHandler<GetBananaProductionQuery, List<BananaProduction>>
{
private readonly IBananaAnalyticsRepository _repository;
private readonly ILogger<GetBananaProductionQueryHandler> _logger;

public GetBananaProductionQueryHandler(
IBananaAnalyticsRepository repository,
ILogger<GetBananaProductionQueryHandler> logger)
{
_repository = repository;
_logger = logger;
}

public async Task<List<BananaProduction>> Handle(GetBananaProductionQuery request, CancellationToken cancellationToken)
{
_logger.LogInformation("Handling GetBananaProductionQuery for year {Year}", request.Year);

var production = await _repository.GetProductionDataAsync(request.Year, cancellationToken);

_logger.LogInformation("Retrieved {Count} production records for year {Year}", production.Count, request.Year);

return production;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using GrafanaBanana.Api.Application.Queries;
using GrafanaBanana.Api.Databricks;
using GrafanaBanana.Api.Domain.Repositories;
using MediatR;

namespace GrafanaBanana.Api.Application.Handlers;

/// <summary>
/// Handler for GetBananaSalesQuery
/// Implements the business logic for retrieving banana sales data
/// Following CQRS and Mediator patterns
/// </summary>
public class GetBananaSalesQueryHandler : IRequestHandler<GetBananaSalesQuery, List<BananaSalesData>>
{
private readonly IBananaAnalyticsRepository _repository;
private readonly ILogger<GetBananaSalesQueryHandler> _logger;

public GetBananaSalesQueryHandler(
IBananaAnalyticsRepository repository,
ILogger<GetBananaSalesQueryHandler> logger)
{
_repository = repository;
_logger = logger;
}

public async Task<List<BananaSalesData>> Handle(GetBananaSalesQuery request, CancellationToken cancellationToken)
{
// Sanitize region parameter for logging to prevent log forging
var sanitizedRegion = request.Region.Replace("\n", "").Replace("\r", "");
_logger.LogInformation("Handling GetBananaSalesQuery for region {Region}", sanitizedRegion);

var sales = await _repository.GetSalesDataAsync(request.Region, cancellationToken);

_logger.LogInformation("Retrieved {Count} sales records", sales.Count);

return sales;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using GrafanaBanana.Api.Application.Queries;
using GrafanaBanana.Api.Domain.Entities;
using GrafanaBanana.Api.Domain.Repositories;
using MediatR;

namespace GrafanaBanana.Api.Application.Handlers;

/// <summary>
/// Handler for GetWeatherForecastQuery
/// Implements the business logic for retrieving weather forecasts
/// Following CQRS and Mediator patterns
/// </summary>
public class GetWeatherForecastQueryHandler : IRequestHandler<GetWeatherForecastQuery, IEnumerable<WeatherForecast>>
{
private readonly IWeatherForecastRepository _repository;
private readonly ILogger<GetWeatherForecastQueryHandler> _logger;

public GetWeatherForecastQueryHandler(
IWeatherForecastRepository repository,
ILogger<GetWeatherForecastQueryHandler> logger)
{
_repository = repository;
_logger = logger;
}

public async Task<IEnumerable<WeatherForecast>> Handle(GetWeatherForecastQuery request, CancellationToken cancellationToken)
{
_logger.LogInformation("Handling GetWeatherForecastQuery for {Days} days", request.Days);

var forecasts = await _repository.GetForecastsAsync(request.Days, cancellationToken);

_logger.LogInformation("Successfully retrieved {Count} weather forecast entries", forecasts.Count());

return forecasts;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using GrafanaBanana.Api.Databricks;
using MediatR;

namespace GrafanaBanana.Api.Application.Queries;

/// <summary>
/// Query to get banana analytics data
/// Following CQRS pattern - read operation
/// </summary>
public record GetBananaAnalyticsQuery : IRequest<BananaAnalytics>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using GrafanaBanana.Api.Databricks;
using MediatR;

namespace GrafanaBanana.Api.Application.Queries;

/// <summary>
/// Query to get banana production data by year
/// Following CQRS pattern - read operation
/// </summary>
public record GetBananaProductionQuery(int Year) : IRequest<List<BananaProduction>>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using GrafanaBanana.Api.Databricks;
using MediatR;

namespace GrafanaBanana.Api.Application.Queries;

/// <summary>
/// Query to get banana sales data by region
/// Following CQRS pattern - read operation
/// </summary>
public record GetBananaSalesQuery(string Region) : IRequest<List<BananaSalesData>>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using GrafanaBanana.Api.Domain.Entities;
using MediatR;

namespace GrafanaBanana.Api.Application.Queries;

/// <summary>
/// Query to get weather forecasts
/// Following CQRS pattern - read operation
/// </summary>
public record GetWeatherForecastQuery(int Days = 5) : IRequest<IEnumerable<WeatherForecast>>;
31 changes: 31 additions & 0 deletions backend/GrafanaBanana.Api/Domain/Entities/WeatherForecast.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace GrafanaBanana.Api.Domain.Entities;

/// <summary>
/// Domain entity representing a weather forecast
/// </summary>
public class WeatherForecast
{
public DateOnly Date { get; private set; }
public int TemperatureC { get; private set; }
public string? Summary { get; private set; }

// Domain logic
public int TemperatureF => 32 + (int)(TemperatureC * 9.0 / 5.0);

public WeatherForecast(DateOnly date, int temperatureC, string? summary)
{
Date = date;
TemperatureC = temperatureC;
Summary = summary;
}

// Factory method for creating forecasts
public static WeatherForecast Create(DateOnly date, int temperatureC, string? summary)
{
// Domain validation
if (temperatureC < -273) // Absolute zero check
throw new ArgumentException("Temperature cannot be below absolute zero", nameof(temperatureC));

return new WeatherForecast(date, temperatureC, summary);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using GrafanaBanana.Api.Databricks;

namespace GrafanaBanana.Api.Domain.Repositories;

/// <summary>
/// Repository interface for banana analytics data access
/// Abstracts the Databricks service implementation
/// </summary>
public interface IBananaAnalyticsRepository
{
Task<BananaAnalytics> GetBananaAnalyticsAsync(CancellationToken cancellationToken = default);
Task<List<BananaProduction>> GetProductionDataAsync(int year, CancellationToken cancellationToken = default);
Task<List<BananaSalesData>> GetSalesDataAsync(string region, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using GrafanaBanana.Api.Domain.Entities;

namespace GrafanaBanana.Api.Domain.Repositories;

/// <summary>
/// Repository interface for weather forecast data access
/// Following Repository Pattern for abstraction of data access logic
/// </summary>
public interface IWeatherForecastRepository
{
Task<IEnumerable<WeatherForecast>> GetForecastsAsync(int days, CancellationToken cancellationToken = default);
}
1 change: 1 addition & 0 deletions backend/GrafanaBanana.Api/GrafanaBanana.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" />

<!-- OpenTelemetry packages for comprehensive observability -->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using GrafanaBanana.Api.Databricks;
using GrafanaBanana.Api.Domain.Repositories;

namespace GrafanaBanana.Api.Infrastructure.Repositories;

/// <summary>
/// Repository implementation for banana analytics data
/// Delegates to the existing DatabricksService while providing a repository abstraction
/// This follows the Adapter pattern to integrate legacy code with new architecture
/// </summary>
public class BananaAnalyticsRepository : IBananaAnalyticsRepository
{
private readonly IDatabricksService _databricksService;
private readonly ILogger<BananaAnalyticsRepository> _logger;

public BananaAnalyticsRepository(
IDatabricksService databricksService,
ILogger<BananaAnalyticsRepository> logger)
{
_databricksService = databricksService;
_logger = logger;
}

public async Task<BananaAnalytics> GetBananaAnalyticsAsync(CancellationToken cancellationToken = default)
{
_logger.LogDebug("Fetching banana analytics from Databricks service");
return await _databricksService.GetBananaAnalyticsAsync(cancellationToken);
}

public async Task<List<BananaProduction>> GetProductionDataAsync(int year, CancellationToken cancellationToken = default)
{
_logger.LogDebug("Fetching banana production data for year {Year}", year);
return await _databricksService.GetProductionDataAsync(year, cancellationToken);
}

public async Task<List<BananaSalesData>> GetSalesDataAsync(string region, CancellationToken cancellationToken = default)
{
_logger.LogDebug("Fetching banana sales data for region {Region}", region);
return await _databricksService.GetSalesDataAsync(region, cancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using GrafanaBanana.Api.Domain.Entities;
using GrafanaBanana.Api.Domain.Repositories;

namespace GrafanaBanana.Api.Infrastructure.Repositories;

/// <summary>
/// Repository implementation for weather forecast data
/// Implements in-memory data generation for demonstration
/// In a real application, this would connect to a database or external API
/// </summary>
public class WeatherForecastRepository : IWeatherForecastRepository
{
private readonly ILogger<WeatherForecastRepository> _logger;
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

public WeatherForecastRepository(ILogger<WeatherForecastRepository> logger)
{
_logger = logger;
}

public Task<IEnumerable<WeatherForecast>> GetForecastsAsync(int days, CancellationToken cancellationToken = default)
{
_logger.LogDebug("Generating {Days} weather forecasts", days);

var forecasts = Enumerable.Range(1, days).Select(index =>
WeatherForecast.Create(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
Summaries[Random.Shared.Next(Summaries.Length)]
))
.ToList();

return Task.FromResult<IEnumerable<WeatherForecast>>(forecasts);
}
}
Loading
Loading