Skip to content

Commit 075eea2

Browse files
committed
Enhance querying and performance in GymMgmt API
Introduced advanced filtering, sorting, and pagination for members and subscriptions via new endpoints in `MembersController` and `SubscriptionsController`. Added `DashboardController` for member statistics. Implemented query handlers using Dapper for efficient database operations. Introduced DTOs, enums, and `PaginatedList<T>` for consistent data handling. Added `ISqlConnectionFactory` for centralized SQL connection management and enhanced logging. Optimized database performance with indexes and explicit enum values. Updated `Program.cs` and DI configuration to register new services. Removed unused query classes and improved dynamic SQL handling for secure queries. Updated project files to include new dependencies (`Dapper`, `Microsoft.Data.SqlClient`) and enhanced maintainability with better logging and resource management.
1 parent 51016db commit 075eea2

24 files changed

+525
-85
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using GymMgmt.Api.Middlewares.Responses;
2+
using GymMgmt.Application.Features.Members.Queries.GetMemberStatistics;
3+
using GymMgmt.Application.Features.Members.Queries;
4+
using MediatR;
5+
using Microsoft.AspNetCore.Authorization;
6+
using Microsoft.AspNetCore.Http;
7+
using Microsoft.AspNetCore.Mvc;
8+
using System.Net;
9+
10+
namespace GymMgmt.Api.Controllers.Dashboard
11+
{
12+
[Route("api/[controller]")]
13+
[ApiController]
14+
public class DashboardController : ControllerBase
15+
{
16+
private readonly IMediator _mediator;
17+
18+
public DashboardController(IMediator mediator)
19+
{
20+
_mediator = mediator;
21+
}
22+
23+
[AllowAnonymous]
24+
[HttpGet("Stats")]
25+
[ProducesResponseType(typeof(ApiResponse<MemberStatisticsDto>), (int)HttpStatusCode.OK)]
26+
[ProducesResponseType(typeof(ApiResponse<object>), (int)HttpStatusCode.BadRequest)]
27+
public async Task<ActionResult> GetStats()
28+
{
29+
var result = await _mediator.Send(new GetMemberStatisticsQuery());
30+
return Ok(ApiResponse<MemberStatisticsDto>.Success(result));
31+
}
32+
}
33+
}

GymMgmt.Api/Controllers/MemberShipsPlans/MemberShipsPlansController.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using GymMgmt.Api.Middlewares.Responses;
2-
using GymMgmt.Application.Common.Exceptions;
32
using GymMgmt.Application.Features.Memberships.CreateMembership;
43
using GymMgmt.Application.Features.Memberships.GetAllMemberShipsPlans;
54
using GymMgmt.Application.Features.Memberships.GetMemberShipPlanById;
@@ -8,7 +7,6 @@
87
using Microsoft.AspNetCore.Authorization;
98
using Microsoft.AspNetCore.Mvc;
109
using System.Net;
11-
using static Microsoft.EntityFrameworkCore.DbLoggerCategory.Database;
1210

1311
namespace GymMgmt.Api.Controllers.MemberShipsPlans
1412
{

GymMgmt.Api/Controllers/Members/MembersController.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
using GymMgmt.Api.Middlewares.Responses;
2+
using GymMgmt.Application.Common.Models;
3+
using GymMgmt.Application.Features.Members;
24
using GymMgmt.Application.Features.Members.CreateMember;
35
using GymMgmt.Application.Features.Members.GetAllMembers;
46
using GymMgmt.Application.Features.Members.GetMemberById;
57
using GymMgmt.Application.Features.Members.PayInsurrance;
8+
using GymMgmt.Application.Features.Members.Queries.GetmembersByStatus;
9+
using GymMgmt.Application.Features.Members.Queries.GetMemberStatistics;
10+
using GymMgmt.Application.Features.Members.Queries;
611
using GymMgmt.Application.Features.Members.UpdateMember;
712
using GymMgmt.Application.Features.Memberships.UpdateMembershipPlan;
13+
using GymMgmt.Application.Features.Subscriptions.GetAllSubscriptions;
814
using MediatR;
915
using Microsoft.AspNetCore.Authorization;
1016
using Microsoft.AspNetCore.Mvc;
@@ -58,6 +64,21 @@ public async Task<ActionResult> GetAllMembers()
5864
var result = await _mediator.Send(new GetAllMembersQuery());
5965
return Ok(ApiResponse<IEnumerable<ReadMemberDto>>.Success(result));
6066
}
67+
68+
69+
[AllowAnonymous]
70+
[HttpGet("Memberslist/{filter}")]
71+
[ProducesResponseType(typeof(ApiResponse<PaginatedList<MemberListDto>>), (int)HttpStatusCode.OK)]
72+
[ProducesResponseType(typeof(ApiResponse<object>), (int)HttpStatusCode.BadRequest)]
73+
public async Task<ActionResult> GetList(
74+
MemberStatusFilter filter,
75+
[FromQuery] int pageNumber = 1,
76+
[FromQuery] int pageSize = 10)
77+
{
78+
var result = await _mediator.Send(new GetMembersByStatusQuery(filter, pageNumber, pageSize));
79+
80+
return Ok(ApiResponse<PaginatedList<MemberListDto>>.Success(result));
81+
}
6182

6283
[AllowAnonymous]
6384
[HttpPut("{id:Guid}")]
@@ -85,5 +106,6 @@ public async Task<ActionResult> PayInsurance([FromBody] CreateInsurancePaymentCo
85106
return Ok(ApiResponse<bool>.Success(result));
86107
}
87108

109+
88110
}
89111
}

GymMgmt.Api/Controllers/Subscriptions/SubscriptionsController.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using GymMgmt.Api.Middlewares.Responses;
2+
using GymMgmt.Application.Common.Models;
23
using GymMgmt.Application.Features.Subscriptions.CancelSubscription;
34
using GymMgmt.Application.Features.Subscriptions.ExtendSubscription;
5+
using GymMgmt.Application.Features.Subscriptions.GetAllSubscriptions;
46
using GymMgmt.Application.Features.Subscriptions.StartSubscription;
57
using MediatR;
68
using Microsoft.AspNetCore.Authorization;
@@ -49,5 +51,20 @@ public async Task<ActionResult> CancelSubscription([FromBody] CancelSubscription
4951
var result = await _mediator.Send(command);
5052
return Ok(ApiResponse<bool>.Success(result, "Subscription cancelled successfully"));
5153
}
54+
/// <summary>
55+
/// Gets a paginated list of subscriptions with filtering and sorting.
56+
/// </summary>
57+
///
58+
[AllowAnonymous]
59+
[HttpGet("GetSubscriptions")]
60+
[ProducesResponseType(typeof(ApiResponse<PaginatedList<SubscriptionDto>>), StatusCodes.Status200OK)]
61+
public async Task<ActionResult<ApiResponse<PaginatedList<SubscriptionDto>>>> GetSubscriptions(
62+
[FromQuery] GetSubscriptionsQuery query,
63+
CancellationToken cancellationToken)
64+
{
65+
// MediatR sends the query to your GetSubscriptionsHandler
66+
var result = await _mediator.Send(query, cancellationToken);
67+
return Ok(ApiResponse<PaginatedList<SubscriptionDto>>.Success(result, "Subscription Retrieved successfully"));
68+
}
5269
}
5370
}

GymMgmt.Api/Program.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Microsoft.AspNetCore.Authorization;
99
using Microsoft.OpenApi.Models;
1010
using GymMgmt.Api.Middlewares;
11+
using System.Text.Json.Serialization;
1112

1213
namespace GymMgmt.Api
1314
{
@@ -99,6 +100,12 @@ public static async Task Main(string[] args)
99100
}
100101
});
101102
});
103+
builder.Services.AddControllers()
104+
.AddJsonOptions(options =>
105+
{
106+
// This allows passing "Active" instead of 1 in JSON bodies and often helps query strings too
107+
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
108+
});
102109

103110

104111
var app = builder.Build();
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System.Data;
2+
3+
4+
namespace GymMgmt.Application.Common.Interfaces
5+
{
6+
public interface ISqlConnectionFactory
7+
{
8+
IDbConnection GetOpenConnection();
9+
IDbConnection CreateNewConnection();
10+
string GetConnectionString();
11+
}
12+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
7+
namespace GymMgmt.Application.Features.Members
8+
{
9+
public enum MemberStatusFilter
10+
{
11+
Active = 1,
12+
GracePeriod = 2,
13+
Expired = 3,
14+
Cancelled = 4
15+
}
16+
}

GymMgmt.Application/Features/Members/PayInsurrance/CreateInsurancePaymentCommandHandler.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,7 @@
33
using GymMgmt.Domain.Entities.Members;
44
using GymMgmt.Domain.Entities.Payments;
55
using MediatR;
6-
using System;
7-
using System.Collections.Generic;
8-
using System.Linq;
9-
using System.Text;
10-
using System.Threading.Tasks;
6+
117

128
namespace GymMgmt.Application.Features.Members.PayInsurrance
139
{
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using Dapper;
2+
using GymMgmt.Application.Common.Interfaces;
3+
using MediatR;
4+
5+
6+
namespace GymMgmt.Application.Features.Members.Queries.GetMemberStatistics
7+
{
8+
public class GetMemberStatisticsHandler : IRequestHandler<GetMemberStatisticsQuery, MemberStatisticsDto>
9+
{
10+
private readonly ISqlConnectionFactory _connectionFactory;
11+
12+
public GetMemberStatisticsHandler(ISqlConnectionFactory connectionFactory)
13+
{
14+
_connectionFactory = connectionFactory;
15+
}
16+
17+
public async Task<MemberStatisticsDto> Handle(GetMemberStatisticsQuery request, CancellationToken cancellationToken)
18+
{
19+
using var connection = _connectionFactory.CreateNewConnection();
20+
21+
// We fetch the GracePeriod days from settings dynamically
22+
// Then we use CASE WHEN to categorize every subscription efficiently
23+
var sql = @"
24+
DECLARE @GraceDays INT = (SELECT TOP 1 SubscriptionGracePeriodInDays FROM ClubSettings);
25+
DECLARE @Now DATETIME2 = SYSDATETIME();
26+
27+
SELECT
28+
COUNT(CASE
29+
WHEN s.Status = 'Active' AND s.EndDate >= @Now
30+
THEN 1 END) as ActiveCount,
31+
32+
COUNT(CASE
33+
WHEN s.Status = 'Active' AND s.EndDate < @Now AND s.EndDate >= DATEADD(day, -@GraceDays, @Now)
34+
THEN 1 END) as GracePeriodCount,
35+
36+
COUNT(CASE
37+
WHEN (s.Status = 'Active' AND s.EndDate < DATEADD(day, -@GraceDays, @Now)) OR s.Status = 'Expired'
38+
THEN 1 END) as ExpiredCount,
39+
40+
COUNT(CASE
41+
WHEN s.Status = 'Cancelled'
42+
THEN 1 END) as CancelledCount
43+
44+
FROM Subscriptions s
45+
-- Ensure we only count the latest/relevant subscription per member if needed
46+
-- For now, this counts ALL subscriptions in the system
47+
WHERE s.WillNotRenew = 0 OR s.Status = 'Cancelled'";
48+
49+
return await connection.QuerySingleAsync<MemberStatisticsDto>(sql);
50+
}
51+
}
52+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using MediatR;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Text;
6+
using System.Threading.Tasks;
7+
8+
namespace GymMgmt.Application.Features.Members.Queries.GetMemberStatistics
9+
{
10+
public record GetMemberStatisticsQuery : IRequest<MemberStatisticsDto>;
11+
}

0 commit comments

Comments
 (0)