• C# 用Attribute实现AOP事务 [C# | AOP | Attribute | ContextAttribute | IContributeObjectSink | IMessageSink ]


    前言

         使用Attribute来实现方法级别事务一直是我的梦想,浅谈Attribute [C# | Attribute | DefaultValueAttribute]有体现我的无奈,Attribute确实是真真切切的非侵入式的东西(其实我是想侵入的: ) ),前有DUDU的Attribute在.net编程中的应用系列文章,但是总是离想象和需求有那么点出入,通过三天的努力,Google的陪伴,下面和大家一起分享我这三天的成果 Attribute实现AOP事务 吧!

    致谢文章

         1.     Aspect-Oriented Programming Enables Better Code Encapsulation and Reuse 关键性的CallContext是在这里发现的。

         2.     C# Attribute在.net编程中的应用 (转) 这篇文章原文地址找不到了,DUDU的Attribute在.net编程中的应用系列文章就是这篇文章的分解,他写到了五,后面的大家可以从这篇文章里面提前看到了。

    阅前注意

         1.     整篇文章的核心和突破点在于上下文Context的使用,务必注意CallContext在整个程序中起到的作用

         2.     本文中看到的SqlHelper使用的是微软SqlHelper.cs。

         3.     本文重点在于如何实现,并且已经测试通过,只贴关键性代码,所以请认真阅读,部分代码直接拷贝下来运行是会出错的!

    正文

         首先我们来看一段未加事务的代码:

         SqlDAL.cs     

    public abstract class SqlDAL
        {

            
    #region ConnectionString

            
    private SqlConnectionStringBuilder _ConnectionString = null;
            
    /// <summary>
            
    /// 字符串连接
            
    /// </summary>
            public virtual SqlConnectionStringBuilder ConnectionString
            {
                
    get
                {
                    
    if (_ConnectionString == null || string.IsNullOrEmpty(_ConnectionString.ConnectionString))
                    {
                        _ConnectionString 
    = new SqlConnectionStringBuilder(Configurations.SQLSERVER_CONNECTION_STRING);
                    }
                    
    return _ConnectionString;
                }
                
    set { _ConnectionString = value; }
            }

            
    #endregion

            
    #region ExecuteNonQuery

            
    public int ExecuteNonQuery(string cmdText)
            {
                
    return SqlHelper.ExecuteNonQuery(ConnectionString.ConnectionString, CommandType.Text, cmdText);
            }

            
    public int ExecuteNonQuery(string cmdText, CommandType type)
            {
                
    return SqlHelper.ExecuteNonQuery(ConnectionString.ConnectionString, type, cmdText);
            }

            
    public int ExecuteNonQuery(string cmdText, CommandType type, params SqlParameter[] cmdParameters)
            {
                
    return SqlHelper.ExecuteNonQuery(ConnectionString.ConnectionString, type, cmdText, cmdParameters);
            }

            
    #endregion

         代码说明:

              1.     本类对SqlHelper.cs 进一步封装。

              2.     Configurations.SQLSERVER_CONNECTION_STRING 替换成自己的连接字符串就行了。

         UserInfoAction.cs

        public class UserInfoAction : SqlDAL
        {
            
    /// <summary>
            
    /// 添加用户
            
    /// </summary>
            public void Add(UserInfo user)
            {
                StringBuilder sb 
    = new StringBuilder();
                sb.Append(
    "UPDATE [UserInfo] SET Password='");
                sb.Append(user.Password);
                sb.Append(
    "' WHERE UID=");
                sb.Append(user.UID);
                ExecuteNonQuery(sql);
            }
        }

         如果我们要加入事务,通常的办法就是在方法内try、catch然后Commit、Rollback,缺点就不说了,下面我会边贴代码边讲解,力图大家也能掌握这种方法: )

         先贴前面两个被我修改的类

         SqlDAL.cs

    public abstract class SqlDAL : ContextBoundObject
        {

            
    private SqlTransaction _SqlTrans;
            
    /// <summary>
            
    /// 仅支持有事务时操作
            
    /// </summary>
            public SqlTransaction SqlTrans
            {
                
    get
                {
                    
    if (_SqlTrans == null)
                    {
                        
    //从上下文中试图取得事务
                        object obj = CallContext.GetData(TransactionAop.ContextName);
                        
    if (obj != null && obj is SqlTransaction)
                            _SqlTrans 
    = obj as SqlTransaction;
                    }
                    
    return _SqlTrans;
                }
                
    set { _SqlTrans = value; }
            }

            
    #region ConnectionString

            
    private SqlConnectionStringBuilder _ConnectionString = null;
            
    /// <summary>
            
    /// 字符串连接
            
    /// </summary>
            public virtual SqlConnectionStringBuilder ConnectionString
            {
                
    get
                {
                    
    if (_ConnectionString == null || string.IsNullOrEmpty(_ConnectionString.ConnectionString))
                    {
                        _ConnectionString 
    = new SqlConnectionStringBuilder(Configurations.SQLSERVER_CONNECTION_STRING);
                    }
                    
    return _ConnectionString;
                }
                
    set { _ConnectionString = value; }
            }

            
    #endregion

            
    #region ExecuteNonQuery

            
    public int ExecuteNonQuery(string cmdText)
            {
                
    if (SqlTrans == null)
                    
    return SqlHelper.ExecuteNonQuery(ConnectionString.ConnectionString, CommandType.Text, cmdText);
                
    else
                    
    return SqlHelper.ExecuteNonQuery(SqlTrans, CommandType.Text, cmdText);
            }

            
    public int ExecuteNonQuery(string cmdText, CommandType type)
            {
                
    if (SqlTrans == null)
                    
    return SqlHelper.ExecuteNonQuery(ConnectionString.ConnectionString, type, cmdText);
                
    else
                    
    return SqlHelper.ExecuteNonQuery(SqlTrans, type, cmdText);
            }

            
    public int ExecuteNonQuery(string cmdText, CommandType type, params SqlParameter[] cmdParameters)
            {
                
    if (SqlTrans == null)
                    
    return SqlHelper.ExecuteNonQuery(ConnectionString.ConnectionString, type, cmdText, cmdParameters);
                
    else
                    
    return SqlHelper.ExecuteNonQuery(SqlTrans, type, cmdText, cmdParameters);
            }

            
    #endregion

        }

         代码说明:

              1.     加了一个属性(Property)SqlTrans,并且每个ExecuteNonQuery执行前都加了判断是否以事务方式执行。这样做是为后面从上下文中取事务做准备。

              2.     类继承了ContextBoundObject,注意,是必须的,MSDN是这样描述的:定义所有上下文绑定类的基类。

              3.     TransactionAop将在后面给出。

         UserInfoAction.cs

        [Transaction]
        
    public class UserInfoAction : SqlDAL
        {
            [TransactionMethod]
            
    public void Add(UserInfo user)
            {
                StringBuilder sb 
    = new StringBuilder();
                sb.Append(
    "UPDATE [UserInfo] SET Password='");
                sb.Append(user.Password);
                sb.Append(
    "' WHERE UID=");
                sb.Append(user.UID);
                ExecuteNonQuery(sql);
            }
        }

         代码说明:

              1.     很简洁、非侵入式、很少改动、非常方便(想要事务就加2个标记,不想要就去掉)。

              2.     两个Attribute后面将给出。

        /// <summary>
        
    /// 标注类某方法内所有数据库操作加入事务控制
        
    /// </summary>
        [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
        
    public sealed class TransactionAttribute : ContextAttribute, IContributeObjectSink
        {

            
    /// <summary>
            
    /// 标注类某方法内所有数据库操作加入事务控制,请使用TransactionMethodAttribute同时标注
            
    /// </summary>
            public TransactionAttribute()
                : 
    base("Transaction")
            { }

            
    public IMessageSink GetObjectSink(MarshalByRefObject obj, IMessageSink next)
            {
                
    return new TransactionAop(next);
            }
        }

        
    /// <summary>
        
    /// 标示方法内所有数据库操作加入事务控制
        
    /// </summary>
        [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
        
    public sealed class TransactionMethodAttribute : Attribute
        {
            
    /// <summary>
            
    /// 标示方法内所有数据库操作加入事务控制
            
    /// </summary>
            public TransactionMethodAttribute()
            {

            }
        }

         代码说明:

              1.     在上面两篇文章中都是把IContextProperty, IContributeObjectSink单独继承并实现的,其实我们发现ContextAttribute已经继承了IContextProperty,所有这里我仅仅只需要再继承一下IContributeObjectSink就行了。关于这两个接口的说明,上面文章中都有详细的说明。

              2.     TransactionAop将在后面给出。

              3.     需要注意的是两个Attribute需要一起用,并且我发现Attribute如果标记在类上他会被显示的实例化,但是放在方法上就不会,打断点可以跟踪到这一过程,要不然我也不会费力气弄两个来标注了。

         TransactionAop.cs

    public sealed class TransactionAop : IMessageSink
        {
            
    private IMessageSink nextSink; //保存下一个接收器

            
    /// <summary>
            
    /// 构造函数
            
    /// </summary>
            
    /// <param name="next">接收器</param>
            public TransactionAop(IMessageSink nextSink)
            {
                
    this.nextSink = nextSink;
            }

            
    /// <summary>
            
    ///  IMessageSink接口方法,用于异步处理,我们不实现异步处理,所以简单返回null,
            
    ///  不管是同步还是异步,这个方法都需要定义
            
    /// </summary>
            
    /// <param name="msg"></param>
            
    /// <param name="replySink"></param>
            
    /// <returns></returns>
            public IMessageCtrl AsyncProcessMessage(IMessage msg, IMessageSink replySink)
            {
                
    return null;
            }

            
    /// <summary>
            
    /// 下一个接收器
            
    /// </summary>
            public IMessageSink NextSink
            {
                
    get { return nextSink; }
            }

            
    /// <summary>
            
    /// 
            
    /// </summary>
            
    /// <param name="msg"></param>
            
    /// <returns></returns>
            public IMessage SyncProcessMessage(IMessage msg)
            {
                IMessage retMsg 
    = null;

                IMethodCallMessage call 
    = msg as IMethodCallMessage;
                
                
    if (call == null ||  (Attribute.GetCustomAttribute(call.MethodBase, typeof(TransactionMethodAttribute))) == null)
                    retMsg 
    = nextSink.SyncProcessMessage(msg);
                
    else
                {
                    
    //此处换成自己的数据库连接
                    using (SqlConnection Connect = new SqlConnection(Configurations.SQLSERVER_CONNECTION_STRING))
                    {
                        Connect.Open();
                        SqlTransaction SqlTrans 
    = Connect.BeginTransaction();

                        
    //讲存储存储在上下文
                        CallContext.SetData(TransactionAop.ContextName, SqlTrans);

                        
    //传递消息给下一个接收器 - > 就是指执行你自己的方法
                        retMsg = nextSink.SyncProcessMessage(msg);

                        
    if (SqlTrans != null)
                        {
                            IMethodReturnMessage methodReturn 
    = retMsg as IMethodReturnMessage;
                            Exception except 
    = methodReturn.Exception;

                            
    if (except != null)
                            {
                                SqlTrans.Rollback();
                                
    //可以做日志及其他处理
                            }
                            
    else
                            {
                                SqlTrans.Commit();
                            }
                            SqlTrans.Dispose();
                            SqlTrans 
    = null;
                        }
                    }
                }

                
    return retMsg;
            }

            
    /// <summary>
            
    /// 用于提取、存储SqlTransaction
            
    /// </summary>
            public static string ContextName
            {
                
    get { return "TransactionAop"; }
            }
        }

         代码说明:

              1.     IMessageSink     MSDN:定义消息接收器的接口。

              2.     主要关注SyncProcessMessage方法内的代码,在这里创建事务,并存储在上下文中间,还记得上面SqlDAL的SqlTrans属性么,里面就是从上下文中取得的。

              3.     请注意了,这里能捕捉到错误,但是没有办法处理错误,所以错误会继续往外抛,但是事务的完整性我们实现了。你可以在Global.asax可以做全局处理,也可以手动的try一下,但是我们不需要管理事务了,仅仅当普通的错误来处理了。

    结束

         大家可以看到,在被标注的方法里面所有的数据库操作都会被事务管理起来,也算是了了我心愿,貌似我的Attribute做权限又看到了一丝希望了,欢迎大家多提意见:)

    补充(2009-1-8)

         关于在评论中提到的性能的问题,如果要使用AOP的方式来实现事务肯定比直接try catch 然后Commit 和 Rollback效率要低的,但是很明显可维护性、使用方便性要高得多的,所以看个人需求了。这里补充的是关于SqlDAL继承ContextBoundObject的问题,以下是想到的解决办法:

              1.     最简单、修改UserInfoAction最少的办法:把SqlDAL复制一份改下类名,继承一下ContextBoundObject,然后把继承类改一下。很不推荐:  (

              2.     从一开始就不使用继承方法来访问数据层的方法,而是将SqlDAL改成一个普通类,通过声明一个SqlDAL方式来访问数据层:

            private SqlDAL _sqlDao;

            
    public SqlDAL SqlDao
            {
                
    get
                {
                    
    if (_sqlDao == null)
                    {
                        _sqlDao 
    = new SqlDAL();
                        
    object obj = CallContext.GetData(TransactionAop.ContextName);
                        
    if (obj != null && obj is SqlTransaction)
                            _sqlDao.SqlTrans 
    = obj as SqlTransaction;
                    }

                    
    return _sqlDao;
                }
            }

              这样相对于没有加事务类仅仅多一个取值过程和判断过程,效率应该还是比继承SqlDAL直接继承ContextBoundObject好很多。

              个人感觉还是不是很好,继续探索,已经想到了减少一个Attribute的办法了,感谢欢迎大家提建议 :)

  • 相关阅读:
    编程思想之正则表达式
    SQL查询顺序
    hibernate inverse属性的作用
    介绍一下Hibernate的二级缓存
    JSON数据
    你没玩过的全新版本!Win10这些骚操作你知多少
    VSCode 小鸡汤 第01期
    Editor REST Client
    k8s常用命令
    【项目3-2】多肉植物网站
  • 原文地址:https://www.cnblogs.com/over140/p/1371307.html
Copyright © 2020-2023  润新知