Table of Contents

Tutorials

Skim Introduction, Configuration, and Services first. This page collects copy-pastable patterns for your own code: ILogger, DevKit redaction attributes on commands and HTTP endpoints, and combinations that keep secrets out of structured logs.

Manual logging with ILogger

Use Microsoft.Extensions.Logging.ILogger or ILogger<TCategory> anywhere DI can supply it:

using Microsoft.Extensions.Logging;

namespace MyApp.Infrastructure;

public sealed class MyService
{
    private readonly ILogger<MyService> _logger;

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

    public void Run()
    {
        _logger.LogInformation("MyService is doing work.");
    }
}

If you inherit BaseCommandHandler or BaseQueryHandler, a _logger field is already available—call LogDebug, LogInformation, and so on from Handle the same way DevKit’s base types do (for example when publishing domain events).

[DoNotLog] — redact commands, queries, DTOs, and events

  • Namespace: CodeBlock.DevKit.Core.Attributes

When the whole type has [DoNotLog], MediatRDispatcher skips logging the serialized command/query/event payload. When only properties are marked, Serializer.RedactSensitiveData replaces those values with ***REDACTED*** in logs. For serializer helpers, see Helpers and extensions.

// Copyright (c) CodeBlock.Dev. All rights reserved.
// For more information visit https://codeblock.dev

namespace CodeBlock.DevKit.Core.Attributes;

/// <summary>
/// Marks properties or classes to be excluded from logging operations.
/// When applied, sensitive data will be redacted and replaced with "***REDACTED***" in logs.
/// </summary>

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class, AllowMultiple = false)]
public class DoNotLogAttribute : Attribute { }

Entire command:

using CodeBlock.DevKit.Application.Commands;
using CodeBlock.DevKit.Core.Attributes;

[DoNotLog]
internal sealed class ChangePasswordRequest : BaseCommand
{
    public string CurrentPassword { get; }
    public string NewPassword { get; }
}

Single property:

using CodeBlock.DevKit.Core.Attributes;

public sealed class UserProfileDto
{
    public string DisplayName { get; set; }

    [DoNotLog]
    public string ApiKey { get; set; }
}

[DoNotLogQueryResult] — hide query result payloads

  • Namespace: CodeBlock.DevKit.Application.Queries

The dispatcher still logs that the query finished and how long it took, but it does not append the serialized Result<T> (useful for large or sensitive results). You can combine [DoNotLog] on the query type with [DoNotLogQueryResult] to hide both input and output.

// Copyright (c) CodeBlock.Dev. All rights reserved.
// For more information visit https://codeblock.dev

namespace CodeBlock.DevKit.Application.Queries;

/// <summary>
/// Attribute to mark query classes that should not have their results logged.
/// Applied to query classes to prevent sensitive or large result data from being logged.
/// </summary>

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class DoNotLogQueryResultAttribute : Attribute { }
using CodeBlock.DevKit.Application.Queries;

[DoNotLogQueryResult]
internal sealed class GetCustomerSecretsQuery : BaseQuery<SecretsDto>
{
    public string CustomerId { get; init; }
}

[DoNotLogRequestBody] — redact HTTP request body and form

  • Namespace: CodeBlock.DevKit.Web.Filters

HttpRequestLoggingMiddleware replaces logged body and form with Sensitive data redacted when this metadata is present on the endpoint.

// Copyright (c) CodeBlock.DevKit. All rights reserved.
// For more information visit https://codeblock.dev

namespace CodeBlock.DevKit.Web.Filters;

/// <summary>
/// Marker attribute that indicates request body content should not be logged.
/// Apply this attribute to controllers or actions to prevent logging of sensitive request data
/// such as passwords, credit card numbers, or other confidential information.
/// </summary>

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
public class DoNotLogRequestBodyAttribute : Attribute
{
    // This is a marker attribute to indicate sensitive body data should not be logged
}

Single action:

using CodeBlock.DevKit.Web.Filters;
using Microsoft.AspNetCore.Mvc;

[HttpPost]
[DoNotLogRequestBody]
public async Task<Result<CommandResult>> Login([FromBody] LoginDto input)
{
    // body and form are not written to HTTP request logs
}

Entire API controller:

using CodeBlock.DevKit.Web.Filters;

[ApiController]
[DoNotLogRequestBody]
public class PaymentsController : ControllerBase
{
    // all actions skip body/form in HTTP request logs
}

[DoNotLogResponseBody] — redact API response JSON

  • Namespace: CodeBlock.DevKit.Web.Filters

The API HttpResponseLogging pipeline checks for this metadata and skips dumping response JSON when it is present.

// Copyright (c) CodeBlock.DevKit. All rights reserved.
// For more information visit https://codeblock.dev

namespace CodeBlock.DevKit.Web.Filters;

/// <summary>
/// Marker attribute that indicates response body content should not be logged.
/// Apply this attribute to controllers or actions to prevent logging of sensitive response data.
/// Currently not implemented in the logging middleware but available for future use.
/// </summary>

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
public class DoNotLogResponseBodyAttribute : Attribute { }
using CodeBlock.DevKit.Web.Filters;

[HttpGet("{id}")]
[DoNotLogResponseBody]
public async Task<Result<SecretsDto>> GetSecrets(string id)
{
    // response JSON is not written to response-body logs
}

HttpResponseLogging on your own API base type

If you replace BaseApiController but still want response-body logging, apply the same filter DevKit uses:

// Copyright (c) CodeBlock.Dev. All rights reserved.
// For more information visit https://codeblock.dev

using System.Text;
using CodeBlock.DevKit.Web.Filters;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;

namespace CodeBlock.DevKit.Web.Api.Filters;

/// <summary>
/// Attribute that logs HTTP response bodies for debugging and monitoring purposes.
/// Can be applied to controllers to automatically log all response content.
/// Respects the DoNotLogResponseBody attribute to skip logging when needed.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class HttpResponseLoggingAttribute : Attribute, IAsyncResultFilter
{
    /// <summary>
    /// Intercepts the response execution to capture and log the response body.
    /// Temporarily replaces the response stream to capture content without affecting the client.
    /// </summary>
    /// <param name="context">The result execution context</param>
    /// <param name="next">The next filter in the pipeline</param>
    /// <returns>A task representing the asynchronous operation</returns>

    public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
    {
        var loggerFactory = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>();
        var logger = loggerFactory.CreateLogger<HttpResponseLoggingAttribute>();

        // Check if the DoNotLogResponseBody attribute is present on the controller or action
        var endpoint = context.HttpContext.GetEndpoint();
        var hasDoNotLogAttr = endpoint?.Metadata.GetMetadata<DoNotLogResponseBodyAttribute>() != null;

        // Swap out the response body stream with a temporary memory stream
        var originalBodyStream = context.HttpContext.Response.Body;
        using var temporaryStream = new MemoryStream();
        context.HttpContext.Response.Body = temporaryStream;

        // Execute the action and response
        var resultContext = await next();

        var responseBody = await GetResponseBody(context.HttpContext.Response);

        // Skip logging if the attribute is present
        if (!hasDoNotLogAttr)
        {
            logger.LogInformation("Response Body: {ResponseBody}", responseBody);
        }
        else
        {
            logger.LogInformation("Response Body: response body logging is disabled for this endpoint");
        }

        // Reset the stream position again to copy it back to the original stream
        await temporaryStream.CopyToAsync(originalBodyStream);
        context.HttpContext.Response.Body = originalBodyStream;
    }

    /// <summary>
    /// Reads and formats the response body for logging purposes.
    /// Handles stream positioning and JSON formatting for better readability.
    /// </summary>
    /// <param name="response">The HTTP response to read the body from</param>
    /// <returns>A formatted string representation of the response body</returns>
    private async Task<string> GetResponseBody(HttpResponse response)
    {
        if (!response.Body.CanRead)
            return "Response body is not readable.";

        response.Body.Seek(0, SeekOrigin.Begin);
        using var reader = new StreamReader(response.Body, Encoding.UTF8, leaveOpen: true);
        var body = await reader.ReadToEndAsync();
        response.Body.Seek(0, SeekOrigin.Begin);

        if (string.IsNullOrWhiteSpace(body))
            return "No Body Data!";

        var jsonBody = JsonConvert.DeserializeObject(body);
        return JsonConvert.SerializeObject(jsonBody, Formatting.Indented);
    }
}

See also