1717using Serilog . Events ;
1818using Serilog . Extensions . Hosting ;
1919using Serilog . Parsing ;
20+ using System . Buffers ;
2021using System . Diagnostics ;
22+ using System . Text ;
2123
2224namespace Serilog . AspNetCore ;
2325
@@ -32,13 +34,15 @@ class RequestLoggingMiddleware
3234 readonly Func < HttpContext , string , double , int , IEnumerable < LogEventProperty > > _getMessageTemplateProperties ;
3335 readonly ILogger ? _logger ;
3436 readonly bool _includeQueryInRequestPath ;
37+ readonly RequestLoggingOptions _options ;
3538 static readonly LogEventProperty [ ] NoProperties = [ ] ;
3639
3740 public RequestLoggingMiddleware ( RequestDelegate next , DiagnosticContext diagnosticContext , RequestLoggingOptions options )
3841 {
3942 if ( options == null ) throw new ArgumentNullException ( nameof ( options ) ) ;
4043 _next = next ?? throw new ArgumentNullException ( nameof ( next ) ) ;
4144 _diagnosticContext = diagnosticContext ?? throw new ArgumentNullException ( nameof ( diagnosticContext ) ) ;
45+ _options = options ;
4246
4347 _getLevel = options . GetLevel ;
4448 _enrichDiagnosticContext = options . EnrichDiagnosticContext ;
@@ -58,6 +62,7 @@ public async Task Invoke(HttpContext httpContext)
5862 var collector = _diagnosticContext . BeginCollection ( ) ;
5963 try
6064 {
65+ await CollectRequestBody ( httpContext , collector , _options ) ;
6166 await _next ( httpContext ) ;
6267 var elapsedMs = GetElapsedMilliseconds ( start , Stopwatch . GetTimestamp ( ) ) ;
6368 var statusCode = httpContext . Response . StatusCode ;
@@ -130,4 +135,104 @@ static string GetPath(HttpContext httpContext, bool includeQueryInRequestPath)
130135
131136 return requestPath ! ;
132137 }
138+
139+ private async static Task CollectRequestBody ( HttpContext httpContext , DiagnosticContextCollector collector , RequestLoggingOptions options )
140+ {
141+ // Check if we should include the request body
142+ if ( ! options . IncludeRequestBody )
143+ return ;
144+
145+ HttpRequest request = httpContext . Request ;
146+
147+ // Check if the Content-Type matches the specified types
148+ if ( ! IsContentTypeMatch ( request . ContentType , options . RequestBodyContentTypes ) )
149+ return ;
150+
151+ // Check if the Content-Length exceeds the maximum allowed length
152+ if ( options . RequestBodyContentMaxLength . HasValue &&
153+ request . ContentLength . HasValue &&
154+ request . ContentLength . Value > options . RequestBodyContentMaxLength . Value )
155+ {
156+ return ;
157+ }
158+
159+ string bodyAsText ;
160+
161+ #if NET5_0_OR_GREATER
162+ // Enable buffering to allow multiple reads of the request body
163+ request . EnableBuffering ( ) ;
164+
165+ // read the body as text
166+ var body = await request . BodyReader . ReadAsync ( ) ;
167+ bodyAsText = Encoding . UTF8 . GetString ( body . Buffer . ToArray ( ) ) ;
168+ #else
169+ // backward compatibility for .NET Standard 2.0 and .NET Framework
170+ bodyAsText = await ReadBodyAsString ( request ) ;
171+ #endif
172+
173+ // Reset the request body stream position for further processing
174+ request . Body . Position = 0 ;
175+
176+ var property = new LogEventProperty ( "RequestBody" , new ScalarValue ( bodyAsText ) ) ;
177+ collector . AddOrUpdate ( property ) ;
178+ }
179+
180+ private static bool IsContentTypeMatch ( string ? currentContentType , List < string > contentTypesToMatch )
181+ {
182+ // Extract the base MIME type from the current ContentType (ignore parameters like charset, boundary, etc.)
183+ var currentMimeType = ExtractBaseMimeType ( currentContentType ! ) ;
184+ if ( string . IsNullOrWhiteSpace ( currentMimeType ) || contentTypesToMatch == null || contentTypesToMatch . Count == 0 )
185+ return false ;
186+
187+ // Check if the base MIME type matches any in the list
188+ foreach ( var contentTypeToMatch in contentTypesToMatch )
189+ {
190+ var matchMimeType = ExtractBaseMimeType ( contentTypeToMatch ) ;
191+ if ( string . Equals ( currentMimeType , matchMimeType , StringComparison . OrdinalIgnoreCase ) )
192+ return true ;
193+ }
194+
195+ return false ;
196+ }
197+
198+ private static string ? ExtractBaseMimeType ( string ? contentType )
199+ {
200+ if ( contentType == null || string . IsNullOrWhiteSpace ( contentType ) )
201+ return contentType ;
202+
203+ // Split on semicolon to remove parameters (e.g., "text/html; charset=utf-8" -> "text/html")
204+ int semicolonIndex = contentType . IndexOf ( ';' ) ;
205+ string baseMimeType = semicolonIndex >= 0
206+ ? contentType . Substring ( 0 , semicolonIndex )
207+ : contentType ;
208+
209+ return baseMimeType . Trim ( ) ;
210+ }
211+
212+ #if ! NET5_0_OR_GREATER
213+ private static async Task < string > ReadBodyAsString ( HttpRequest request )
214+ {
215+ if ( request == null )
216+ throw new ArgumentNullException ( nameof ( request ) ) ;
217+
218+ // Read the body as bytes first
219+ byte [ ] bodyBytes ;
220+ using ( var memoryStream = new MemoryStream ( ) )
221+ {
222+ await request . Body . CopyToAsync ( memoryStream ) ;
223+ bodyBytes = memoryStream . ToArray ( ) ;
224+ }
225+
226+ // Convert bytes to string
227+ string bodyAsText = Encoding . UTF8 . GetString ( bodyBytes ) ;
228+
229+ // Only replace the request body stream if it doesn't support seeking
230+ if ( ! request . Body . CanSeek )
231+ request . Body = new MemoryStream ( bodyBytes ) ;
232+
233+ request . Body . Position = 0 ;
234+
235+ return bodyAsText ;
236+ }
237+ #endif
133238}
0 commit comments