You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

咨询:WCF+XPO+DevExpress项目中客户端操作服务端统一日志方案

解决方案:基于XPO的WCF服务统一日志系统搭建

针对你遇到的XPO非契约方法(如Session.Save)无法通过WCF的IOperationInvoker捕获、客户端操作日志无法同步到服务端的问题,可以从XPO扩展拦截和WCF日志联动两个方向入手,以下是具体实现方案:

一、服务端:捕获WCF方法调用与XPO数据库操作日志

1. WCF方法调用日志(含耗时)

通过实现IDispatchMessageInspector拦截WCF请求,记录方法名称、调用时间、耗时及上下文信息:

public class ServiceCallInspector : IDispatchMessageInspector
{
    public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
    {
        // 获取当前调用的方法名
        var operationName = OperationContext.Current.IncomingMessageHeaders.Action.Split('/').Last();
        var startTime = DateTime.UtcNow;
        // 将上下文信息存入OperationContext,用于后续关联XPO日志
        OperationContext.Current.Items["CorrelationId"] = Guid.NewGuid().ToString();
        OperationContext.Current.Items["OperationName"] = operationName;
        OperationContext.Current.Items["StartTime"] = startTime;

        return null;
    }

    public void BeforeSendReply(ref Message reply, object correlationState)
    {
        var startTime = (DateTime)OperationContext.Current.Items["StartTime"];
        var elapsedMs = (DateTime.UtcNow - startTime).TotalMilliseconds;
        var operationName = (string)OperationContext.Current.Items["OperationName"];
        var correlationId = (string)OperationContext.Current.Items["CorrelationId"];

        // 记录WCF方法调用日志
        var logEntry = new LogEntry
        {
            CorrelationId = correlationId,
            OperationType = "WcfMethodCall",
            Details = operationName,
            ElapsedMilliseconds = (long)elapsedMs,
            Timestamp = DateTime.UtcNow,
            Source = "Server"
        };
        LogManager.PersistLog(logEntry);
    }
}

将该拦截器注册到WCF服务行为中,即可自动捕获所有契约方法的调用日志。

2. XPO数据库操作日志(含Session.Save等内部操作)

XPO的核心操作无法通过WCF拦截,需从XPO本身扩展:

方案A:自定义Session子类,重写关键方法

通过继承Session并重写Save、CommitTransaction等方法,嵌入日志逻辑:

public class LoggingSession : Session
{
    public LoggingSession(IDataStore dataStore) : base(dataStore)
    {
        Committed += OnSessionCommitted;
    }

    private string GetCorrelationId()
    {
        return OperationContext.Current?.Items["CorrelationId"] as string ?? Guid.NewGuid().ToString();
    }

    public override void Save(object theObject)
    {
        var stopwatch = Stopwatch.StartNew();
        base.Save(theObject);
        stopwatch.Stop();

        var classInfo = GetClassInfo(theObject.GetType());
        var logEntry = new LogEntry
        {
            CorrelationId = GetCorrelationId(),
            OperationType = "XpoSave",
            ObjectType = classInfo.FullName,
            ObjectId = classInfo.KeyProperty.GetValue(theObject)?.ToString(),
            ElapsedMilliseconds = stopwatch.ElapsedMilliseconds,
            Timestamp = DateTime.UtcNow,
            Source = "Server"
        };
        LogManager.PersistLog(logEntry);
    }

    private void OnSessionCommitted(object sender, EventArgs e)
    {
        var logEntry = new LogEntry
        {
            CorrelationId = GetCorrelationId(),
            OperationType = "XpoTransactionCommit",
            Timestamp = DateTime.UtcNow,
            Source = "Server"
        };
        LogManager.PersistLog(logEntry);
    }
}

服务端所有WCF服务实例统一使用LoggingSession作为工作单元,即可捕获所有XPO操作。

方案B:自定义DataStore拦截SQL语句

若需记录底层SQL执行细节,可实现IDataStore包装原生DataStore,拦截所有SQL操作:

public class LoggingDataStore : IDataStore
{
    private readonly IDataStore _innerStore;

    public LoggingDataStore(IDataStore innerStore) => _innerStore = innerStore;

    public ModificationResult ModifyData(params ModificationStatement[] dmlStatements)
    {
        var stopwatch = Stopwatch.StartNew();
        var result = _innerStore.ModifyData(dmlStatements);
        stopwatch.Stop();

        var correlationId = OperationContext.Current?.Items["CorrelationId"] as string ?? Guid.NewGuid().ToString();
        foreach (var stmt in dmlStatements)
        {
            var logEntry = new LogEntry
            {
                CorrelationId = correlationId,
                OperationType = "SqlDml",
                Details = stmt.ToString(),
                ElapsedMilliseconds = stopwatch.ElapsedMilliseconds,
                Timestamp = DateTime.UtcNow,
                Source = "Server"
            };
            LogManager.PersistLog(logEntry);
        }

        return result;
    }

    // 实现SelectData、UpdateSchema等方法,转发给_innerStore并按需记录日志
    public SelectedData SelectData(params SelectStatement[] selectStatements)
    {
        var stopwatch = Stopwatch.StartNew();
        var result = _innerStore.SelectData(selectStatements);
        stopwatch.Stop();

        var correlationId = OperationContext.Current?.Items["CorrelationId"] as string ?? Guid.NewGuid().ToString();
        foreach (var stmt in selectStatements)
        {
            var logEntry = new LogEntry
            {
                CorrelationId = correlationId,
                OperationType = "SqlSelect",
                Details = stmt.ToString(),
                ElapsedMilliseconds = stopwatch.ElapsedMilliseconds,
                Timestamp = DateTime.UtcNow,
                Source = "Server"
            };
            LogManager.PersistLog(logEntry);
        }

        return result;
    }

    public void UpdateSchema(bool dontCreateIfFirstTableNotExist, params DBTable[] tables)
    {
        _innerStore.UpdateSchema(dontCreateIfFirstTableNotExist, tables);
    }

    public SchemaCorrectionResult[] UpdateSchema(bool dontCreateIfFirstTableNotExist, params SchemaCorrectionInfo[] schemaCorrectionInfos)
    {
        return _innerStore.UpdateSchema(dontCreateIfFirstTableNotExist, schemaCorrectionInfos);
    }
}

初始化XPO时使用该包装类:

var connectionString = "...";
var nativeStore = XpoDefault.GetConnectionProvider(connectionString, AutoCreateOption.DatabaseAndSchema);
var loggingStore = new LoggingDataStore(nativeStore);
var session = new LoggingSession(loggingStore);

二、客户端:同步XPO操作日志到服务端

客户端WinForm应用中的XPO操作(如Session.Save),可复用服务端的LoggingSession逻辑,将日志通过WCF发送到服务端:

1. 定义WCF日志提交契约

新增一个专门用于接收客户端日志的WCF服务:

[ServiceContract]
public interface ILogService
{
    [OperationContract]
    void SubmitClientLogs(List<LogEntry> logEntries);
}

public class LogService : ILogService
{
    public void SubmitClientLogs(List<LogEntry> logEntries)
    {
        foreach (var entry in logEntries)
        {
            entry.Source = "Client";
            LogManager.PersistLog(entry);
        }
    }
}

2. 客户端自定义LoggingSession修改

客户端的LoggingSession不直接持久化日志,而是缓存日志条目,批量提交到服务端:

public class ClientLoggingSession : Session
{
    private static readonly List<LogEntry> _logCache = new List<LogEntry>();
    private static readonly object _cacheLock = new object();

    public ClientLoggingSession(IDataStore dataStore) : base(dataStore)
    {
        Committed += OnSessionCommitted;
    }

    public override void Save(object theObject)
    {
        var stopwatch = Stopwatch.StartNew();
        base.Save(theObject);
        stopwatch.Stop();

        var classInfo = GetClassInfo(theObject.GetType());
        var logEntry = new LogEntry
        {
            CorrelationId = Guid.NewGuid().ToString(),
            OperationType = "XpoSave",
            ObjectType = classInfo.FullName,
            ObjectId = classInfo.KeyProperty.GetValue(theObject)?.ToString(),
            ElapsedMilliseconds = stopwatch.ElapsedMilliseconds,
            Timestamp = DateTime.UtcNow
        };

        lock (_cacheLock)
        {
            _logCache.Add(logEntry);
            // 每10条日志批量提交,或在UI空闲时提交
            if (_logCache.Count >= 10)
            {
                SubmitLogsToServer();
            }
        }
    }

    private void OnSessionCommitted(object sender, EventArgs e)
    {
        var logEntry = new LogEntry
        {
            CorrelationId = Guid.NewGuid().ToString(),
            OperationType = "XpoTransactionCommit",
            Timestamp = DateTime.UtcNow
        };

        lock (_cacheLock)
        {
            _logCache.Add(logEntry);
        }
    }

    private void SubmitLogsToServer()
    {
        List<LogEntry> logsToSubmit;
        lock (_cacheLock)
        {
            logsToSubmit = new List<LogEntry>(_logCache);
            _logCache.Clear();
        }

        // 异步调用WCF日志服务,避免阻塞UI
        Task.Run(() =>
        {
            using (var client = new LogServiceClient())
            {
                client.SubmitClientLogs(logsToSubmit);
            }
        });
    }
}

三、统一日志管理与关联

定义通用的LogEntry实体类,包含所有日志所需字段:

public class LogEntry
{
    public string CorrelationId { get; set; }
    public string OperationType { get; set; }
    public string ObjectType { get; set; }
    public string ObjectId { get; set; }
    public string Details { get; set; }
    public long ElapsedMilliseconds { get; set; }
    public DateTime Timestamp { get; set; }
    public string Source { get; set; } // Server/Client
}

实现LogManager统一处理日志持久化(如写入数据库、文件等),确保服务端和客户端的日志存储到同一位置。

内容的提问来源于stack exchange,提问作者phpain

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 04:03:10