LiteOrm writes runtime logs through Microsoft.Extensions.Logging. If the host application already has logging providers configured, such as Console, Debug, or Serilog, LiteOrm service-call logs, exception logs, and slow-query logs flow through the same pipeline.
Most built-in logging is centered around the service interceptor:
The built-in generic service interfaces already opt in to service logging:
[ServiceLog(LogLevel = ServiceLogLevel.Debug)]
public interface IEntityServiceAsync<T> : IEntityServiceAsync
{
}
So when your application reuses EntityService or EntityViewService, you usually already have baseline service-call logging.
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Host.RegisterLiteOrm();
var host = Host.CreateDefaultBuilder(args)
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddConsole();
logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Information);
})
.RegisterLiteOrm()
.Build();
LoggerFactory inside RegisterLiteOrm(options => ...)builder.Host.RegisterLiteOrm(options =>
{
options.LoggerFactory = LoggerFactory.Create(logging =>
{
logging.AddConsole();
logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Information);
});
});
options.LoggerFactory is mainly used for framework registration and assembly-scan logging. Regular service-call logs still use the host DI container’s ILoggerFactory.
ServiceLog[ServiceLog] controls service method invocation logging and can be applied to:
Its two main settings are:
LogLevel: ServiceLogLevel.Trace / Debug / Information / Warning / Error / Critical / NoneLogFormat: LogFormat.None / Args / ReturnValue / Full[ServiceLog]
public interface IUserService
{
Task<User?> GetByIdAsync(int id);
}
This is equivalent to:
[ServiceLog(LogLevel = ServiceLogLevel.Information, LogFormat = LogFormat.Full)]
[ServiceLog(LogLevel = ServiceLogLevel.Debug, LogFormat = LogFormat.Args)]
public interface IOrderService
{
Task<Order?> GetAsync(int id);
Task<IReadOnlyList<Order>> SearchAsync(string keyword);
}
This works well during development when you want to see what gets called and with which inputs, but do not want full return bodies.
public interface IAccountService
{
[ServiceLog(LogLevel = ServiceLogLevel.Warning, LogFormat = LogFormat.Full)]
Task<bool> TransferAsync(long fromId, long toId, decimal amount);
[ServiceLog(LogLevel = ServiceLogLevel.None)]
Task<string> GetHealthAsync();
}
TransferAsync is elevated for audit-worthy operations.GetHealthAsync disables service logs entirely to avoid noisy high-frequency entries.LogFormat guidance| Setting | Best for |
|---|---|
None |
Disable invoke/return logs |
Args |
Input-focused troubleshooting |
ReturnValue |
Result-focused diagnostics |
Full |
End-to-end debugging with both inputs and outputs |
Log[Log] and [Log(false)] control which data is allowed into logs.
The service interceptor reads LogAttribute from method parameters. If a parameter is marked as [Log(false)], LiteOrm replaces its value with * in the invocation log.
public interface IAuthService
{
Task<LoginResult> LoginAsync(string userName, [Log(false)] string password);
}
That keeps plaintext passwords out of logs.
CancellationTokenis excluded by default even without[Log(false)].
ObjectBase implements ILogable. When LiteOrm logs an entity object, it prefers ToLog(), and ObjectBase.ToLog() respects LogAttribute on properties.
[Table("Users")]
public class User : ObjectBase
{
[Column("Id", IsPrimaryKey = true, IsIdentity = true)]
public int Id { get; set; }
[Column("UserName")]
public string UserName { get; set; } = string.Empty;
[Column("PasswordHash")]
[Log(false)]
public string PasswordHash { get; set; } = string.Empty;
[Column("PasswordSalt")]
[Log(false)]
public string PasswordSalt { get; set; } = string.Empty;
}
With that setup, PasswordHash and PasswordSalt stay out of the generated log text.
If the default ObjectBase.ToLog() output is not enough, a type can implement ILogable directly:
public class PaymentRequest : ILogable
{
public string CardNo { get; set; } = string.Empty;
public decimal Amount { get; set; }
public string ToLog()
{
var tail = CardNo.Length >= 4 ? CardNo[^4..] : CardNo;
return $"CardNo:****{tail}, Amount:{Amount}";
}
}
When such an object is logged by the interceptor, LiteOrm uses ToLog().
ExceptionHook: add method-level exception handling[ExceptionHook] can be placed on a service method, class, or interface to add logic that runs when the service interceptor catches an exception. Typical uses include:
The hook type must implement IServiceExceptionHook:
public class OrderExceptionHook : IServiceExceptionHook
{
public void OnException(ServiceExceptionContext context)
{
// Access exception, method name, arguments, SQL stack, and more
}
}
Then declare it on the service:
[ExceptionHook(typeof(OrderExceptionHook), Mode = ServiceExceptionHookMode.Notify)]
public interface IOrderService
{
Task SubmitAsync(long id);
}
When a service method throws, ServiceInvokeInterceptor handles it in this order:
ServiceExceptionContextExceptionHookServiceInvokeInterceptor.ExceptionHandling eventThat makes ExceptionHook a good fit for local, method-specific exception behavior, while ExceptionHandling is better for global fallback policies.
Notify vs HandleExceptionHookAttribute.Mode has two modes:
| Mode | Meaning |
|---|---|
Notify |
Observe only; the hook must not mark the exception as handled |
Handle |
The hook may call context.Handle(...) and convert the exception into a normal result |
Notify: observe and rethrow[ExceptionHook(typeof(NotifyOnlyHook), Mode = ServiceExceptionHookMode.Notify)]
public void ThrowWithNotifyHook()
{
throw new InvalidOperationException("notify");
}
This mode is for alerting, metrics, and extra logging. The exception still propagates.
If a
Notifyhook callscontext.Handle(...), LiteOrm throws anInvalidOperationExceptionto prevent “configured as observe-only, but actually swallowed the exception” behavior.
Handle: convert the exception into a result[ExceptionHook(typeof(HandleHook), Mode = ServiceExceptionHookMode.Handle)]
public int GetStatus()
{
throw new InvalidOperationException("handle");
}
public class HandleHook : IServiceExceptionHook
{
public void OnException(ServiceExceptionContext context)
{
context.Handle(123);
}
}
After context.Handle(123), the interceptor treats the call as handled and returns 123.
This works for async methods too. For Task<T>, LiteOrm builds the handled result using T.
ServiceExceptionContext containsIn OnException(ServiceExceptionContext context), the most useful members are usually:
context.Exception: the original exceptioncontext.ServiceName / context.MethodName: current service and methodcontext.Arguments / context.LogArguments: raw and log-safe argumentscontext.SessionID: current session IDcontext.SqlStack: current SQL stackSo the hook can be used both for richer diagnostics and for exception-to-result decisions.
ServiceInvokeInterceptor resolves hook types through DI, so hook implementations should usually be registered in the container. The common pattern in this repository is:
[AutoRegister(Lifetime.Scoped, typeof(IServiceExceptionHook))]
public class OrderExceptionHook : IServiceExceptionHook
{
public void OnException(ServiceExceptionContext context)
{
}
}
If the hook is not registered, LiteOrm may still try to create it by type, but explicit DI registration is the safer choice once the hook depends on other services.
ServiceInvokeInterceptor exposes two useful static knobs:
ServiceInvokeInterceptor.SlowQueryThreshold = TimeSpan.FromSeconds(1);
ServiceInvokeInterceptor.MaxExpandedLogLength = 20;
SlowQueryThreshold: emits extra <Slow> and <SlowSQL> entries when exceededMaxExpandedLogLength: limits collection expansion to keep logs from exploding in sizeServiceLog on service interfaces so logging stays aligned with service boundaries.[Log(false)] or a custom ILogable.Debug + Full during local troubleshooting, then narrow to Information or Warning in production.ExceptionHook for method-level alerts, compensation, or exception-to-result conversion; use the global ExceptionHandling event for cross-service policy.ServiceLogLevel.None.