Skip to content

Commit ec25dbb

Browse files
iammukeshmjarvis
andauthored
refactor: reduce cyclomatic complexity across multiple modules (#1188)
* refactor: reduce cyclomatic complexity in Identity module - Extract helper methods in UserService.Lifecycle.cs - Simplify SearchUsersQueryHandler with extracted methods - Fixes CA1827 warnings (Count vs Any) * refactor: reduce complexity in UserService.Roles and Registration - AssignRolesAsync: 19 → extracted ValidateAdminRoleChangeAsync, ProcessRoleAssignmentsAsync - GetOrCreateFromPrincipalAsync: 18 → extracted claim handling, user creation helpers - RegisterAsync: 13 → extracted password validation, email sending, shared helpers Average complexity: 4.7 → 3.5 High complexity methods: 3 → 0 * refactor: reduce cyclomatic complexity across multiple modules ## Changes ### TenantThemeState.cs & ThemeStateFactory.cs - MapFromDto: 52 → 6 (extracted MapLightPalette, MapDarkPalette, etc.) ### TokenRefreshService.cs - TryRefreshTokenAsync: 39 → 8 (extracted cache checks, token validation, claims building) ### AuditHttpMiddleware.cs - InvokeAsync: 30 → 6 (extracted request/response capture, audit writing) ### EntityDiffBuilder.cs - Build: 25 → 5 (extracted property change detection, operation determination) ### SmtpMailService.cs - SendAsync: 19 → 4 (extracted sender/recipient config, attachments) ### IdentityService.cs - ValidateCredentialsAsync: 14 → 5 - ValidateRefreshTokenAsync: 14 → 5 - Eliminated duplicated claims building logic ## Impact - Eliminated 6 'Very Complex' and 'Untestable' rated methods - All refactored methods now ≤10 complexity - Better testability and maintainability --------- Co-authored-by: jarvis <jarvis@codewithmukesh.com>
1 parent de81c6f commit ec25dbb

9 files changed

Lines changed: 1050 additions & 765 deletions

File tree

src/BuildingBlocks/Mailing/Services/SmtpMailService.cs

Lines changed: 83 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using MailKit.Security;
1+
using MailKit.Security;
22
using Microsoft.Extensions.Logging;
33
using Microsoft.Extensions.Options;
44
using MimeKit;
@@ -14,68 +14,124 @@ public class SmtpMailService(IOptions<MailOptions> settings, ILogger<SmtpMailSer
1414
public async Task SendAsync(MailRequest request, CancellationToken ct)
1515
{
1616
ArgumentNullException.ThrowIfNull(request);
17+
ValidateSmtpConfiguration();
1718

19+
using var email = BuildMimeMessage(request);
20+
await AddAttachmentsAsync(email, request, ct);
21+
await SendEmailAsync(email, ct);
22+
}
23+
24+
private void ValidateSmtpConfiguration()
25+
{
1826
if (_settings.Smtp?.Host is null)
27+
{
1928
throw new InvalidOperationException("SMTP Host is not configured.");
29+
}
30+
}
2031

21-
using var email = new MimeMessage();
32+
private MimeMessage BuildMimeMessage(MailRequest request)
33+
{
34+
var email = new MimeMessage();
35+
36+
ConfigureSender(email, request);
37+
ConfigureRecipients(email, request);
38+
ConfigureContent(email, request);
2239

23-
// From
40+
return email;
41+
}
42+
43+
private void ConfigureSender(MimeMessage email, MailRequest request)
44+
{
2445
email.From.Add(new MailboxAddress(_settings.DisplayName, request.From ?? _settings.From));
46+
email.Sender = new MailboxAddress(request.DisplayName ?? _settings.DisplayName, request.From ?? _settings.From);
47+
}
2548

26-
// To
49+
private void ConfigureRecipients(MimeMessage email, MailRequest request)
50+
{
2751
foreach (string address in request.To)
52+
{
2853
email.To.Add(MailboxAddress.Parse(address));
54+
}
2955

30-
// Reply To
3156
if (!string.IsNullOrEmpty(request.ReplyTo))
57+
{
3258
email.ReplyTo.Add(new MailboxAddress(request.ReplyToName, request.ReplyTo));
59+
}
60+
61+
AddBccRecipients(email, request);
62+
AddCcRecipients(email, request);
63+
AddHeaders(email, request);
64+
}
3365

34-
// Bcc
35-
if (request.Bcc != null && request.Bcc.Count > 0)
66+
private static void AddBccRecipients(MimeMessage email, MailRequest request)
67+
{
68+
if (request.Bcc is null || request.Bcc.Count == 0)
3669
{
37-
foreach (string address in request.Bcc.Where(bccValue => !string.IsNullOrWhiteSpace(bccValue)))
38-
email.Bcc.Add(MailboxAddress.Parse(address.Trim()));
70+
return;
3971
}
4072

41-
// Cc
42-
if (request.Cc != null && request.Cc.Count > 0)
73+
foreach (string address in request.Bcc.Where(bcc => !string.IsNullOrWhiteSpace(bcc)))
4374
{
44-
foreach (string? address in request.Cc.Where(ccValue => !string.IsNullOrWhiteSpace(ccValue)))
45-
email.Cc.Add(MailboxAddress.Parse(address.Trim()));
75+
email.Bcc.Add(MailboxAddress.Parse(address.Trim()));
4676
}
77+
}
4778

48-
// Headers
49-
if (request.Headers != null)
79+
private static void AddCcRecipients(MimeMessage email, MailRequest request)
80+
{
81+
if (request.Cc is null || request.Cc.Count == 0)
5082
{
51-
foreach (var header in request.Headers)
52-
email.Headers.Add(header.Key, header.Value);
83+
return;
5384
}
5485

55-
// Content
56-
var builder = new BodyBuilder();
57-
email.Sender = new MailboxAddress(request.DisplayName ?? _settings.DisplayName, request.From ?? _settings.From);
86+
foreach (string? address in request.Cc.Where(cc => !string.IsNullOrWhiteSpace(cc)))
87+
{
88+
email.Cc.Add(MailboxAddress.Parse(address.Trim()));
89+
}
90+
}
91+
92+
private static void AddHeaders(MimeMessage email, MailRequest request)
93+
{
94+
if (request.Headers is null)
95+
{
96+
return;
97+
}
98+
99+
foreach (var header in request.Headers)
100+
{
101+
email.Headers.Add(header.Key, header.Value);
102+
}
103+
}
104+
105+
private static void ConfigureContent(MimeMessage email, MailRequest request)
106+
{
58107
email.Subject = request.Subject;
59-
builder.HtmlBody = request.Body;
108+
}
60109

61-
// Create the file attachments for this e-mail message
62-
if (request.AttachmentData != null)
110+
private static async Task AddAttachmentsAsync(MimeMessage email, MailRequest request, CancellationToken ct)
111+
{
112+
var builder = new BodyBuilder { HtmlBody = request.Body };
113+
114+
if (request.AttachmentData is not null)
63115
{
64-
foreach (var attachmentInfo in request.AttachmentData)
116+
foreach (var attachment in request.AttachmentData)
65117
{
66118
using var stream = new MemoryStream();
67-
await stream.WriteAsync(attachmentInfo.Value, ct);
119+
await stream.WriteAsync(attachment.Value, ct);
68120
stream.Position = 0;
69-
await builder.Attachments.AddAsync(attachmentInfo.Key, stream, ct);
121+
await builder.Attachments.AddAsync(attachment.Key, stream, ct);
70122
}
71123
}
72124

73125
email.Body = builder.ToMessageBody();
126+
}
74127

128+
private async Task SendEmailAsync(MimeMessage email, CancellationToken ct)
129+
{
75130
using var client = new SmtpClient();
131+
76132
try
77133
{
78-
await client.ConnectAsync(_settings.Smtp.Host, _settings.Smtp.Port, SecureSocketOptions.StartTls, ct);
134+
await client.ConnectAsync(_settings.Smtp!.Host, _settings.Smtp.Port, SecureSocketOptions.StartTls, ct);
79135
await client.AuthenticateAsync(_settings.Smtp.UserName, _settings.Smtp.Password, ct);
80136
await client.SendAsync(email, ct);
81137
}
Lines changed: 113 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// Modules.Auditing/AuditHttpMiddleware.cs
21
using FSH.Modules.Auditing.Contracts;
32
using Microsoft.AspNetCore.Http;
43
using Microsoft.Extensions.DependencyInjection;
@@ -25,94 +24,147 @@ public async Task InvokeAsync(HttpContext ctx)
2524
return;
2625
}
2726

28-
var masker = ctx.RequestServices.GetService<IAuditMaskingService>();
27+
var requestContext = await CaptureRequestAsync(ctx);
2928
var sw = Stopwatch.StartNew();
3029

30+
var originalBody = ctx.Response.Body;
31+
await using var responseBuffer = new MemoryStream();
32+
ctx.Response.Body = responseBuffer;
33+
34+
try
35+
{
36+
await _next(ctx);
37+
sw.Stop();
38+
39+
await WriteSuccessAuditAsync(ctx, requestContext, responseBuffer, originalBody, sw);
40+
}
41+
catch (Exception ex)
42+
{
43+
sw.Stop();
44+
await WriteExceptionAuditAsync(ctx, ex);
45+
ctx.Response.Body = originalBody;
46+
throw;
47+
}
48+
}
49+
50+
private async Task<RequestCaptureContext> CaptureRequestAsync(HttpContext ctx)
51+
{
3152
object? reqPreview = null;
3253
int reqSize = 0;
33-
if (_opts.CaptureBodies &&
34-
ContentTypeHelper.IsJsonLike(ctx.Request.ContentType, _opts.AllowedContentTypes))
54+
55+
if (ShouldCaptureBody(ctx.Request.ContentType))
3556
{
57+
var masker = ctx.RequestServices.GetService<IAuditMaskingService>();
3658
(reqPreview, reqSize) = await HttpBodyReader.ReadRequestAsync(ctx, _opts.MaxRequestBytes, ctx.RequestAborted);
59+
3760
if (reqPreview is not null && masker is not null)
3861
{
3962
reqPreview = masker.ApplyMasking(reqPreview);
4063
}
4164
}
4265

43-
var originalBody = ctx.Response.Body;
44-
await using var tee = new MemoryStream();
45-
await using var respBuffer = new MemoryStream();
46-
ctx.Response.Body = tee;
66+
return new RequestCaptureContext(reqPreview, reqSize);
67+
}
4768

48-
try
69+
private async Task WriteSuccessAuditAsync(
70+
HttpContext ctx,
71+
RequestCaptureContext requestContext,
72+
MemoryStream responseBuffer,
73+
Stream originalBody,
74+
Stopwatch sw)
75+
{
76+
var (respPreview, respSize) = await CaptureResponseAsync(ctx, responseBuffer);
77+
78+
await RestoreResponseBodyAsync(responseBuffer, originalBody, ctx);
79+
80+
await WriteActivityAuditAsync(ctx, requestContext, respPreview, respSize, sw);
81+
}
82+
83+
private async Task<(object? Preview, int Size)> CaptureResponseAsync(HttpContext ctx, MemoryStream responseBuffer)
84+
{
85+
if (!ShouldCaptureBody(ctx.Response.ContentType))
4986
{
50-
await _next(ctx);
51-
sw.Stop();
87+
return (null, 0);
88+
}
5289

53-
object? respPreview = null;
54-
int respSize = 0;
90+
var masker = ctx.RequestServices.GetService<IAuditMaskingService>();
91+
responseBuffer.Position = 0;
5592

56-
if (_opts.CaptureBodies &&
57-
ContentTypeHelper.IsJsonLike(ctx.Response.ContentType, _opts.AllowedContentTypes))
58-
{
59-
tee.Position = 0;
60-
await tee.CopyToAsync(respBuffer, ctx.RequestAborted);
61-
(respPreview, respSize) = await HttpBodyReader.ReadResponseAsync(
62-
respBuffer, _opts.MaxResponseBytes, ctx.RequestAborted);
63-
if (respPreview is not null && masker is not null)
64-
{
65-
respPreview = masker.ApplyMasking(respPreview);
66-
}
67-
}
93+
await using var respBuffer = new MemoryStream();
94+
await responseBuffer.CopyToAsync(respBuffer, ctx.RequestAborted);
6895

69-
respBuffer.Position = 0;
70-
ctx.Response.Body = originalBody;
71-
if (respBuffer.Length > 0)
72-
await respBuffer.CopyToAsync(originalBody, ctx.RequestAborted);
73-
74-
await Audit.ForActivity(Contracts.ActivityKind.Http, ctx.Request.Path)
75-
.WithActivityResult(
76-
statusCode: ctx.Response.StatusCode,
77-
durationMs: (int)sw.Elapsed.TotalMilliseconds,
78-
captured: _opts.CaptureBodies ? BodyCapture.Both : BodyCapture.None,
79-
requestSize: reqSize,
80-
responseSize: respSize,
81-
requestPreview: reqPreview,
82-
responsePreview: respPreview)
83-
.WithSource("api")
84-
.WithTenant((_publisher.CurrentScope?.TenantId) ?? null)
85-
.WithUser(_publisher.CurrentScope?.UserId, _publisher.CurrentScope?.UserName)
86-
.WithCorrelation(_publisher.CurrentScope?.CorrelationId ?? ctx.TraceIdentifier)
87-
.WithRequestId(_publisher.CurrentScope?.RequestId ?? ctx.TraceIdentifier)
88-
.WriteAsync(ctx.RequestAborted);
96+
var (respPreview, respSize) = await HttpBodyReader.ReadResponseAsync(
97+
respBuffer, _opts.MaxResponseBytes, ctx.RequestAborted);
98+
99+
if (respPreview is not null && masker is not null)
100+
{
101+
respPreview = masker.ApplyMasking(respPreview);
89102
}
90-
catch (Exception ex)
103+
104+
return (respPreview, respSize);
105+
}
106+
107+
private static async Task RestoreResponseBodyAsync(MemoryStream responseBuffer, Stream originalBody, HttpContext ctx)
108+
{
109+
responseBuffer.Position = 0;
110+
ctx.Response.Body = originalBody;
111+
112+
if (responseBuffer.Length > 0)
91113
{
92-
sw.Stop();
114+
await responseBuffer.CopyToAsync(originalBody, ctx.RequestAborted);
115+
}
116+
}
93117

94-
var sev = ExceptionSeverityClassifier.Classify(ex);
95-
if (sev >= _opts.MinExceptionSeverity)
96-
{
97-
await Audit.ForException(ex, ExceptionArea.Api,
98-
routeOrLocation: ctx.Request.Path, severity: sev)
99-
.WithSource("api")
100-
.WithTenant((_publisher.CurrentScope?.TenantId) ?? null)
101-
.WithUser(_publisher.CurrentScope?.UserId, _publisher.CurrentScope?.UserName)
102-
.WithCorrelation(_publisher.CurrentScope?.CorrelationId ?? ctx.TraceIdentifier)
103-
.WithRequestId(_publisher.CurrentScope?.RequestId ?? ctx.TraceIdentifier)
104-
.WriteAsync(ctx.RequestAborted);
105-
}
118+
private async Task WriteActivityAuditAsync(
119+
HttpContext ctx,
120+
RequestCaptureContext requestContext,
121+
object? respPreview,
122+
int respSize,
123+
Stopwatch sw)
124+
{
125+
await Audit.ForActivity(Contracts.ActivityKind.Http, ctx.Request.Path)
126+
.WithActivityResult(
127+
statusCode: ctx.Response.StatusCode,
128+
durationMs: (int)sw.Elapsed.TotalMilliseconds,
129+
captured: _opts.CaptureBodies ? BodyCapture.Both : BodyCapture.None,
130+
requestSize: requestContext.Size,
131+
responseSize: respSize,
132+
requestPreview: requestContext.Preview,
133+
responsePreview: respPreview)
134+
.WithSource("api")
135+
.WithTenant(_publisher.CurrentScope?.TenantId)
136+
.WithUser(_publisher.CurrentScope?.UserId, _publisher.CurrentScope?.UserName)
137+
.WithCorrelation(_publisher.CurrentScope?.CorrelationId ?? ctx.TraceIdentifier)
138+
.WithRequestId(_publisher.CurrentScope?.RequestId ?? ctx.TraceIdentifier)
139+
.WriteAsync(ctx.RequestAborted);
140+
}
106141

107-
ctx.Response.Body = originalBody;
108-
throw;
142+
private async Task WriteExceptionAuditAsync(HttpContext ctx, Exception ex)
143+
{
144+
var sev = ExceptionSeverityClassifier.Classify(ex);
145+
if (sev < _opts.MinExceptionSeverity)
146+
{
147+
return;
109148
}
149+
150+
await Audit.ForException(ex, ExceptionArea.Api, routeOrLocation: ctx.Request.Path, severity: sev)
151+
.WithSource("api")
152+
.WithTenant(_publisher.CurrentScope?.TenantId)
153+
.WithUser(_publisher.CurrentScope?.UserId, _publisher.CurrentScope?.UserName)
154+
.WithCorrelation(_publisher.CurrentScope?.CorrelationId ?? ctx.TraceIdentifier)
155+
.WithRequestId(_publisher.CurrentScope?.RequestId ?? ctx.TraceIdentifier)
156+
.WriteAsync(ctx.RequestAborted);
110157
}
111158

159+
private bool ShouldCaptureBody(string? contentType) =>
160+
_opts.CaptureBodies && ContentTypeHelper.IsJsonLike(contentType, _opts.AllowedContentTypes);
161+
112162
private bool ShouldSkip(HttpContext ctx)
113163
{
114164
var path = ctx.Request.Path.Value ?? string.Empty;
115165
return _opts.ExcludePathStartsWith.Any(prefix =>
116166
path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase));
117167
}
168+
169+
private readonly record struct RequestCaptureContext(object? Preview, int Size);
118170
}

0 commit comments

Comments
 (0)