Skip to content
Closed
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
31 changes: 31 additions & 0 deletions .github/workflows/dotnet_tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: .NET Tests

on:
push:
branches: ["main", "master"]
pull_request:
branches: ["main", "master"]
pull_request_target:
branches: ["main", "master"]

jobs:
test:
runs-on: windows-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.x'

- name: Restore dependencies
run: dotnet restore Bikes.sln

- name: Build
run: dotnet build Bikes.sln --no-restore --configuration Release

- name: Run tests
run: dotnet test Bikes.Tests/Bikes.Tests.csproj --no-build --configuration Release --verbosity normal
22 changes: 22 additions & 0 deletions Bikes.Api.Host/Bikes.Api.Host.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.1.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Bikes.Application\Bikes.Application.csproj" />
<ProjectReference Include="..\Bikes.Application.Contracts\Bikes.Application.Contracts.csproj" />
<ProjectReference Include="..\Bikes.Infrastructure.EfCore\Bikes.Infrastructure.EfCore.csproj" />
<ProjectReference Include="..\Bikes.ServiceDefaults\Bikes.ServiceDefaults.csproj" />
</ItemGroup>
</Project>
6 changes: 6 additions & 0 deletions Bikes.Api.Host/Bikes.Api.Host.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@Bikes.Api.Host_HostAddress = http://localhost:5145

GET {{Bikes.Api.Host_HostAddress}}/weatherforecast/
Accept: application/json

###
127 changes: 127 additions & 0 deletions Bikes.Api.Host/Controllers/AnalyticsController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using Bikes.Application.Contracts.Analytics;
using Bikes.Application.Contracts.Bikes;
using Microsoft.AspNetCore.Mvc;

namespace Bikes.Api.Host.Controllers;

/// <summary>
/// Controller for rental analytics
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class AnalyticsController(IAnalyticsService analyticsService) : ControllerBase
{
/// <summary>
/// Get all sport bikes
/// </summary>
[HttpGet("sport-bikes")]
[ProducesResponseType(typeof(List<BikeDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public ActionResult<List<BikeDto>> GetSportBikes()
{
try
{
var sportBikes = analyticsService.GetSportBikes();
return Ok(sportBikes);
}
catch (Exception ex)
{
return StatusCode(500, $"Internal server error: {ex.Message}");
}
}

/// <summary>
/// Get top 5 models by profit
/// </summary>
[HttpGet("top-models-by-profit")]
[ProducesResponseType(typeof(List<BikeModelAnalyticsDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public ActionResult<List<BikeModelAnalyticsDto>> GetTopModelsByProfit()
{
try
{
var topModels = analyticsService.GetTop5ModelsByProfit();
return Ok(topModels);
}
catch (Exception ex)
{
return StatusCode(500, $"Internal server error: {ex.Message}");
}
}

/// <summary>
/// Get top 5 models by rental duration
/// </summary>
[HttpGet("top-models-by-duration")]
[ProducesResponseType(typeof(List<BikeModelAnalyticsDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public ActionResult<List<BikeModelAnalyticsDto>> GetTopModelsByDuration()
{
try
{
var topModels = analyticsService.GetTop5ModelsByRentalDuration();
return Ok(topModels);
}
catch (Exception ex)
{
return StatusCode(500, $"Internal server error: {ex.Message}");
}
}

/// <summary>
/// Get rental statistics
/// </summary>
[HttpGet("rental-statistics")]
[ProducesResponseType(typeof(RentalStatistics), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public ActionResult<RentalStatistics> GetRentalStatistics()
{
try
{
var statistics = analyticsService.GetRentalStatistics();
return Ok(statistics);
}
catch (Exception ex)
{
return StatusCode(500, $"Internal server error: {ex.Message}");
}
}

/// <summary>
/// Get total rental time by bike type
/// </summary>
[HttpGet("rental-time-by-type")]
[ProducesResponseType(typeof(Dictionary<string, int>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public ActionResult<Dictionary<string, int>> GetRentalTimeByType()
{
try
{
var rentalTime = analyticsService.GetTotalRentalTimeByBikeType();
return Ok(rentalTime);
}
catch (Exception ex)
{
return StatusCode(500, $"Internal server error: {ex.Message}");
}
}

/// <summary>
/// Get top renters by rental count
/// </summary>
[HttpGet("top-renters")]
[ProducesResponseType(typeof(List<RenterAnalyticsDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public ActionResult<List<RenterAnalyticsDto>> GetTopRenters()
{
try
{
var topRenters = analyticsService.GetTopRentersByRentalCount();
return Ok(topRenters);
}
catch (Exception ex)
{
return StatusCode(500, $"Internal server error: {ex.Message}");
}
}
}
16 changes: 16 additions & 0 deletions Bikes.Api.Host/Controllers/BikeModelsController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Bikes.Application.Contracts;
using Bikes.Application.Contracts.Models;
using Microsoft.AspNetCore.Mvc;

namespace Bikes.Api.Host.Controllers;

/// <summary>
/// Controller for bike model management
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class BikeModelsController(IBikeModelService bikeModelService)
: CrudControllerBase<BikeModelDto, BikeModelCreateUpdateDto>
{
protected override IApplicationService<BikeModelDto, BikeModelCreateUpdateDto> Service => bikeModelService;
}
16 changes: 16 additions & 0 deletions Bikes.Api.Host/Controllers/BikesController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Bikes.Application.Contracts;
using Bikes.Application.Contracts.Bikes;
using Microsoft.AspNetCore.Mvc;

namespace Bikes.Api.Host.Controllers;

/// <summary>
/// Controller for bike management
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class BikesController(IBikeService bikeService)
: CrudControllerBase<BikeDto, BikeCreateUpdateDto>
{
protected override IApplicationService<BikeDto, BikeCreateUpdateDto> Service => bikeService;
}
145 changes: 145 additions & 0 deletions Bikes.Api.Host/Controllers/CrudControllerBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
using Bikes.Application.Contracts;
using Microsoft.AspNetCore.Mvc;

namespace Bikes.Api.Host.Controllers;

/// <summary>
/// Base controller for CRUD operations
/// </summary>
/// <typeparam name="TDto">DTO type</typeparam>
/// <typeparam name="TCreateUpdateDto">Create/Update DTO type</typeparam>
[ApiController]
[Route("api/[controller]")]
public abstract class CrudControllerBase<TDto, TCreateUpdateDto> : ControllerBase
where TDto : class
where TCreateUpdateDto : class
{
protected abstract IApplicationService<TDto, TCreateUpdateDto> Service { get; }

/// <summary>
/// Get all entities
/// </summary>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public virtual ActionResult<List<TDto>> GetAll()
{
try
{
var entities = Service.GetAll();
return Ok(entities);
}
catch (Exception ex)
{
return StatusCode(500, $"Internal server error: {ex.Message}");
}
}

/// <summary>
/// Get entity by id
/// </summary>
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public virtual ActionResult<TDto> GetById(int id)
{
try
{
var entity = Service.GetById(id);
return entity == null ? NotFound() : Ok(entity);
}
catch (Exception ex)
{
return StatusCode(500, $"Internal server error: {ex.Message}");
}
}

/// <summary>
/// Create new entity
/// </summary>
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public virtual ActionResult<TDto> Create([FromBody] TCreateUpdateDto request)
{
try
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

var entity = Service.Create(request);
return CreatedAtAction(nameof(GetById), new { id = GetEntityId(entity) }, entity);
}
catch (InvalidOperationException ex)
{
return BadRequest(ex.Message);
}
catch (Exception ex)
{
return StatusCode(500, $"Internal server error: {ex.Message}");
}
}

/// <summary>
/// Update entity
/// </summary>
[HttpPut("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public virtual ActionResult<TDto> Update(int id, [FromBody] TCreateUpdateDto request)
{
try
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

var entity = Service.Update(id, request);
return entity == null ? NotFound() : Ok(entity);
}
catch (InvalidOperationException ex)
{
return BadRequest(ex.Message);
}
catch (Exception ex)
{
return StatusCode(500, $"Internal server error: {ex.Message}");
}
}

/// <summary>
/// Delete entity
/// </summary>
[HttpDelete("{id}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public virtual ActionResult Delete(int id)
{
try
{
var result = Service.Delete(id);
return result ? NoContent() : NotFound();
}
catch (Exception ex)
{
return StatusCode(500, $"Internal server error: {ex.Message}");
}
}

/// <summary>
/// Extract entity ID for CreatedAtAction
/// </summary>
private static int GetEntityId(TDto entity)
{
var idProperty = typeof(TDto).GetProperty("Id");
return idProperty != null ? (int)idProperty.GetValue(entity)! : 0;
}
}
16 changes: 16 additions & 0 deletions Bikes.Api.Host/Controllers/RentersController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Bikes.Application.Contracts;
using Bikes.Application.Contracts.Renters;
using Microsoft.AspNetCore.Mvc;

namespace Bikes.Api.Host.Controllers;

/// <summary>
/// Controller for renter management
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class RentersController(IRenterService renterService)
: CrudControllerBase<RenterDto, RenterCreateUpdateDto>
{
protected override IApplicationService<RenterDto, RenterCreateUpdateDto> Service => renterService;
}
Loading