Статьи

Централизация событий SqlDataSource

Вступление:

Я работал со старым унаследованным веб-приложением. Эта веб-форма содержит большое количество физических страниц веб-формы, и каждая веб-форма активно использует элемент управления SqlDataSource. Мне нужно было кое-что сделать со всеми элементами управления SqlDataSource на всех страницах веб-форм. Делать это с каждым элементом управления SqlDataSource на всех страницах веб-форм — непростая задача. Мне удалось централизовать события SqlDataSource для выполнения моих задач со всеми элементами управления SqlDataSource только в одном месте. В этой статье я покажу вам, как регистрировать время, затраченное SqlDataSource, в операции / INSERT / UPDATE / DELETE / SELECT. Я также покажу вам, как глобально изменить уровень изоляции базы данных с помощью элемента управления SqlDataSource. 

Описание:

В моем предыдущем приложении у меня есть базовый класс страниц. Если у вас нет базовой страницы, сначала создайте ее. Допустим, у нас есть WebForm1.aspx, 

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="WebForm1.aspx.cs" Inherits="WebFormApp.WebForm1" %>
    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head runat="server">
        <title></title>
    </head>
    <body>
        <form id="form1" runat="server">
        <div>
        
            <asp:GridView ID="GridView1" AutoGenerateDeleteButton="True" AutoGenerateEditButton="True" AutoGenerateSelectButton="True" runat="server" AutoGenerateColumns="False" DataKeyNames="Id" DataSourceID="SqlDataSource1" EmptyDataText="There are no data records to display.">
                <Columns>
                    <asp:BoundField DataField="Id" HeaderText="Id" ReadOnly="True" SortExpression="Id" />
                    <asp:BoundField DataField="Name" HeaderText="Name" SortExpression="Name" />
                </Columns>
            </asp:GridView>
            <asp:SqlDataSource ID="SqlDataSource1" runat="server" ConnectionString="<%$ ConnectionStrings:SampleConnectionString1 %>" DeleteCommand="DELETE FROM [Product] WHERE [Id] = @Id" InsertCommand="INSERT INTO [Product] ([Id], [Name]) VALUES (@Id, @Name)" ProviderName="<%$ ConnectionStrings:SampleConnectionString1.ProviderName %>" SelectCommand="SELECT [Id], [Name] FROM [Product]" UpdateCommand="UPDATE [Product] SET [Name] = @Name WHERE [Id] = @Id">
                <DeleteParameters>
                    <asp:Parameter Name="Id" Type="Int32" />
                </DeleteParameters>
                <InsertParameters>
                    <asp:Parameter Name="Id" Type="Int32" />
                    <asp:Parameter Name="Name" Type="String" />
                </InsertParameters>
                <UpdateParameters>
                    <asp:Parameter Name="Name" Type="String" />
                    <asp:Parameter Name="Id" Type="Int32" />
                </UpdateParameters>
            </asp:SqlDataSource>
        
        </div>
        </form>
    </body>
    </html>

У меня есть таблица продуктов с идентификатором и именем. Эта простая веб-страница позволяет пользователю просматривать, обновлять и удалять продукт. Вот код позади WebForm1,

public partial class WebForm1 : BasePage
    {
        protected void Page_Load(object sender, EventArgs e)
        {
        }
    }

Обратите внимание, что WebForm1 наследуется от BasePage вместо System.Web.UI.Page. Все ваши страницы веб-форм должны быть унаследованы от BasePage, чтобы централизовать события SqlDataSource. Для облегчения нашей работы мы создадим методы расширения для класса ControllCollection, 

public static class ControllCollectionExtensions
    {
        public static IEnumerable<Control> FindAll(this ControlCollection collection)
        {
            foreach (Control item in collection)
            {
                yield return item;
                if (item.HasControls())
                {
                    foreach (var subItem in item.Controls.FindAll())
                    {
                        yield return subItem;
                    }
                }
            }
        }
        public static IEnumerable<T> FindAll<T>(this ControlCollection collection) where T : Control
        {
            return collection.FindAll().OfType<T>();
        }
    }

Метод расширения ControlCollection.FindAll рекурсивно находит все элементы управления определенного типа. Вот простая версия BasePage.cs,

public class BasePage : System.Web.UI.Page
    {
        private Logger logger = LogManager.GetCurrentClassLogger();
        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);
            RegisterEvents();
        }

        protected virtual void ExecutingSqlDatasource(object sender, SqlDataSourceCommandEventArgs e)
        {
            var stopWatch = new Stopwatch();
            stopWatch.Start();
            Context.Items[e.Command.CommandText] = stopWatch;
            e.Command.Connection.Open();
            e.Command.Transaction = e.Command.Connection.BeginTransaction(IsolationLevel.ReadUncommitted);
        }

        protected virtual void ExecutedSqlDatasource(object sender, SqlDataSourceStatusEventArgs e)
        {
            var stopWatch = Context.Items[e.Command.CommandText] as Stopwatch;
            stopWatch.Stop();
            logger.Info(string.Format("{0} takes {1}", e.Command.CommandText, stopWatch.Elapsed.ToString()));
            if (e.Exception == null)
            {
                e.Command.Transaction.Commit();
            }
            else
            {
                e.Command.Transaction.Rollback();
            }
        }

        private void RegisterEvents()
        {
            var sqlDataSourceCollection = Controls.FindAll<SqlDataSource>();
            foreach (var sqlDataSource in sqlDataSourceCollection)
            {
                if (!string.IsNullOrEmpty(sqlDataSource.SelectCommand))
                {
                    sqlDataSource.Selecting += ExecutingSqlDatasource;
                    sqlDataSource.Selected += ExecutedSqlDatasource;
                }
                if (!string.IsNullOrEmpty(sqlDataSource.InsertCommand))
                {
                    sqlDataSource.Inserting += ExecutingSqlDatasource;
                    sqlDataSource.Inserted += ExecutedSqlDatasource;
                }
                if (!string.IsNullOrEmpty(sqlDataSource.UpdateCommand))
                {
                    sqlDataSource.Updating += ExecutingSqlDatasource;
                    sqlDataSource.Updated += ExecutedSqlDatasource;
                }
                if (!string.IsNullOrEmpty(sqlDataSource.DeleteCommand))
                {
                    sqlDataSource.Deleting += ExecutingSqlDatasource;
                    sqlDataSource.Deleted += ExecutedSqlDatasource;
                }
            }
        }
    }

BasePage просто вызывает метод RegisterEvents во время OnLoad. Метод RegisterEvents сначала захватывает все элементы управления SqlDataSource на текущей странице, используя вышеуказанный метод расширения FindAll. Затем для каждого элемента управления SqlDataSource зарегистрируйте события XXXing и XXXted, если существуют связанные с ними команды (например, проверьте SqlDataSource.SelectCommand существуют до регистрации событий SqlDataSource.Selecting и SqlDataSource.Selected). Затем в событии XXXing я создаю и запускаю секундомер, а затем сохраняю этот секундомер в Context.Items для дальнейшего использования. Я также устанавливаю уровень транзакции соединения sql как read-uncommitted / dirty-read (не используйте этот уровень транзакции, если у вас нет веской причины). Затем в событии XXXted я совершаю или откатываю транзакцию в зависимости от существующего исключения. Я также беру секундомер,остановка и регистрация общего прошедшего времени. Здесь я использую Я используюNLog,  чтобы войти в промежуток времени. Вы можете использовать любую другую структуру ведения журнала.

Резюме:

Централизация событий SqlDataSource является непростой задачей. Особенно, когда у вас огромное количество страниц веб-форм. В этой статье я показал вам, как легко можно централизовать события SqlDataSource. Я также показал вам, как изменить уровень транзакции, используемый в SqlDataSource, и как регистрировать временной интервал для каждого необработанного SQL (или SP), используемого SqlDataSource. Надеюсь, вам понравилась моя статья.