Кэширование данных в архитектуре (VB)

Скотт Митчелл

Загрузить PDF-файл

В предыдущем руководстве мы узнали, как применить кэширование на уровне презентации. В этом руководстве мы узнаем, как использовать преимущества многоуровневой архитектуры для кэширования данных на уровне бизнес-логики. Для этого мы расширяем архитектуру, чтобы включить слой кэширования.

Введение

Как мы видели в предыдущем руководстве, кэширование данных ObjectDataSource так же просто, как задание нескольких свойств. К сожалению, ObjectDataSource применяет кэширование на уровне презентации, что тесно сцепляет политики кэширования со страницей ASP.NET. Одной из причин создания многоуровневой архитектуры является разорвание таких связей. Например, уровень бизнес-логики отделяет бизнес-логику от страниц ASP.NET, а уровень доступа к данным — сведения о доступе к данным. Это разделение бизнес-логики и сведений о доступе к данным предпочтительнее, отчасти потому, что это делает систему более читаемой, более поддерживаемой и более гибкой для изменений. Это также позволяет получить знания о предметной области и разделение труда, чтобы разработчик, работающий на уровне представления, не должен быть знаком с данными базы данных, чтобы выполнить свою работу. Отсоединение политики кэширования от уровня представления обеспечивает аналогичные преимущества.

В этом руководстве мы дополним нашу архитектуру, чтобы включить слой кэширования (или кратко cl), который использует нашу политику кэширования. Уровень кэширования будет включать класс, предоставляющий ProductsCL доступ к сведениям о продукте с помощью таких методов, как GetProducts(), GetProductsByCategoryID(categoryID)и т. д., который при вызове сначала попытается получить данные из кэша. Если кэш пуст, эти методы вызывают соответствующий ProductsBLL метод в BLL, который, в свою очередь, получает данные из DAL. Методы ProductsCL кэшируют данные, полученные из BLL, прежде чем возвращать их.

Как показано на рисунке 1, cl находится между уровнями презентации и бизнес-логики.

Уровень кэширования (CL) — это еще один слой в нашей архитектуре

Рис. 1. Слой кэширования (CL) — это еще один слой в нашей архитектуре

Шаг 1. Создание классов слоев кэширования

В этом руководстве мы создадим очень простую cl с одним классом ProductsCL , который содержит только несколько методов. Создание полного уровня кэширования для всего приложения потребует создания CategoriesCLклассов , EmployeesCLи SuppliersCL и предоставления метода в этих классах уровня кэширования для каждого метода доступа к данным или изменения в BLL. Как и в случае с BLL и DAL, слой кэширования в идеале должен быть реализован как отдельный проект библиотеки классов; однако мы реализуем его как класс в папке App_Code .

Чтобы более четко отделить классы CL от классов DAL и BLL, создадим в папке новую вложенную папку App_Code . Щелкните правой кнопкой мыши папку App_Code в Обозреватель решений, выберите Создать папку и назовите новую папку CL. После создания этой папки добавьте в нее новый класс с именем ProductsCL.vb.

Добавление новой папки с именем CL и класса с именем ProductsCL.vb

Рис. 2. Добавление новой папки с именем CL и класса с именем ProductsCL.vb

Класс ProductsCL должен содержать тот же набор методов доступа к данным и их изменения, что и в соответствующем классе уровня бизнес-логики (ProductsBLL). Вместо того, чтобы создавать все эти методы, давайте просто создадим пару здесь, чтобы получить представление о шаблонах, используемых CL. В частности, мы добавим методы и GetProductsByCategoryID(categoryID) на GetProducts() шаге 3 и перегрузку UpdateProduct на шаге 4. Вы можете добавить оставшиеся ProductsCL методы и CategoriesCLклассы , EmployeesCLи SuppliersCL в свободное время.

Шаг 2. Чтение и запись в кэш данных

Функция кэширования ObjectDataSource, рассмотренная в предыдущем руководстве, внутренне использует кэш данных ASP.NET для хранения данных, полученных из BLL. Доступ к кэшу данных также можно получить программным способом из ASP.NET классов кода программной части страниц или из классов в архитектуре веб-приложения. Для чтения и записи в кэш данных из класса кода программной части страницы ASP.NET используйте следующий шаблон:

' Read from the cache
Dim value as Object = Cache("key")
' Add a new item to the cache
Cache("key") = value
Cache.Insert(key, value)
Cache.Insert(key, value, CacheDependency)
Cache.Insert(key, value, CacheDependency, DateTime, TimeSpan)

Метод Cache класса s Insert имеет ряд перегрузок. Cache("key") = value и Cache.Insert(key, value) являются синонимами и оба добавляют элемент в кэш с помощью указанного ключа без определенного срока действия. Как правило, мы хотим указать срок действия при добавлении элемента в кэш как зависимость, срок действия на основе времени или и то, и другое. Используйте одну из перегрузок другого Insert метода, чтобы предоставить сведения об истечении срока действия на основе зависимостей или времени.

Методы уровня кэширования должны сначала проверка, есть ли запрошенные данные в кэше, и, если да, вернуть их оттуда. Если запрошенные данные отсутствуют в кэше, необходимо вызвать соответствующий метод BLL. Возвращаемое значение должно быть кэшировано, а затем возвращено, как показано на следующей схеме последовательностей.

Методы уровня кэширования возвращают данные из кэша, если они доступны

Рис. 3. Методы уровня кэширования возвращают данные из кэша, если они доступны

Последовательность, показанная на рис. 3, выполняется в классах CL по следующему шаблону:

Dim instance As Type = TryCast(Cache("key"), Type)
If instance Is Nothing Then
    instance = BllMethodToGetInstance()
    Cache.Insert(key, instance, ...)
End If
Return instance

Здесь Тип — это тип данных, хранящихся в кэше Northwind.ProductsDataTable, например , а key — это ключ, который уникальным образом идентифицирует элемент кэша. Если элемент с указанным ключом отсутствует в кэше, экземпляр будет Nothing иметь значение , а данные будут извлечены из соответствующего метода BLL и добавлены в кэш. К моменту Return instance достижения экземпляр содержит ссылку на данные из кэша или из BLL.

Не забудьте использовать приведенный выше шаблон при доступе к данным из кэша. Следующий шаблон, который, на первый взгляд, выглядит эквивалентно, содержит незначительное различие, которое вводит состояние гонки. Условия гонки трудно отлаживать, так как они проявляются спорадически и трудно воспроизвести.

If Cache("key") Is Nothing Then
    Cache.Insert(key, BllMethodToGetInstance(), ...)
End If
Return Cache("key")

Разница в этом втором, неправильном фрагменте кода заключается в том, что вместо хранения ссылки на кэшированный элемент в локальной переменной доступ к кэшу данных осуществляется непосредственно в условной инструкции и в Return. Представьте, что при достижении Cache("key") этого кода значение не Nothingравно , но до Return достижения инструкции система вытеснять ключ из кэша. В этом редком случае код возвращает Nothing вместо объекта ожидаемого типа.

Примечание

Кэш данных является потокобезопасным, поэтому вам не нужно синхронизировать доступ к потокам для простых операций чтения или записи. Однако если необходимо выполнить несколько операций с данными в кэше, которые должны быть атомарными, вы отвечаете за реализацию блокировки или другого механизма для обеспечения потокобезопасности. Дополнительные сведения см. в статье Синхронизация доступа к кэшу ASP.NET .

Элемент можно программно вытеснили из кэша данных с помощью следующегоRemove метода:

Cache.Remove(key)

Шаг 3. Возврат сведений о продуктеProductsCLиз класса

В этом руководстве мы реализуем два метода для возврата сведений о продукте ProductsCL из класса: GetProducts() и GetProductsByCategoryID(categoryID). Как и в случае с классом ProductsBL уровня бизнес-логики, GetProducts() метод в CL возвращает сведения обо всех продуктах в виде Northwind.ProductsDataTable объекта, а GetProductsByCategoryID(categoryID) все продукты из указанной категории.

В следующем коде показана часть методов в ProductsCL классе :

<System.ComponentModel.DataObject()> _
Public Class ProductsCL
    Private _productsAPI As ProductsBLL = Nothing
    Protected ReadOnly Property API() As ProductsBLL
        Get
            If _productsAPI Is Nothing Then
                _productsAPI = New ProductsBLL()
            End If
            Return _productsAPI
        End Get
    End Property
    <System.ComponentModel.DataObjectMethodAttribute _
    (DataObjectMethodType.Select, True)> _
    Public Function GetProducts() As Northwind.ProductsDataTable
        Const rawKey As String = "Products"
        ' See if the item is in the cache
        Dim products As Northwind.ProductsDataTable = _
            TryCast(GetCacheItem(rawKey), Northwind.ProductsDataTable)
        If products Is Nothing Then
            ' Item not found in cache - retrieve it and insert it into the cache
            products = API.GetProducts()
            AddCacheItem(rawKey, products)
        End If
        Return products
    End Function
    <System.ComponentModel.DataObjectMethodAttribute _
        (DataObjectMethodType.Select, False)> _
    Public Function GetProductsByCategoryID(ByVal categoryID As Integer) _
        As Northwind.ProductsDataTable
        If (categoryID < 0) Then
            Return GetProducts()
        Else
            Dim rawKey As String = String.Concat("ProductsByCategory-", categoryID)
            ' See if the item is in the cache
            Dim products As Northwind.ProductsDataTable = _
                TryCast(GetCacheItem(rawKey), Northwind.ProductsDataTable)
            If products Is Nothing Then
                ' Item not found in cache - retrieve it and insert it into the cache
                products = API.GetProductsByCategoryID(categoryID)
                AddCacheItem(rawKey, products)
            End If
            Return products
        End If
    End Function
End Class

Сначала обратите внимание на DataObject атрибуты и DataObjectMethodAttribute , применяемые к классу и методам. Эти атрибуты предоставляют мастеру ObjectDataSource сведения, указывающие, какие классы и методы должны отображаться в шагах мастера. Так как доступ к классам и методам CL будет осуществляться из ObjectDataSource на уровне представления, я добавил эти атрибуты для улучшения работы во время разработки. Более подробное описание этих атрибутов и их эффектов см. в руководстве По созданию уровня бизнес-логики .

В методах GetProducts() и GetProductsByCategoryID(categoryID) данные, возвращаемые методом GetCacheItem(key) , назначаются локальной переменной. Метод GetCacheItem(key) , который мы рассмотрим в ближайшее время, возвращает определенный элемент из кэша на основе указанного ключа. Если такие данные не найдены в кэше, они извлекаются из соответствующего ProductsBLL метода класса, а затем добавляются в кэш с помощью AddCacheItem(key, value) метода .

Методы GetCacheItem(key) и AddCacheItem(key, value) интерфейсируются с кэшем данных, считывая и записывая значения соответственно. Метод GetCacheItem(key) является более простым из двух. Он просто возвращает значение из класса Cache с помощью переданного ключа:

Private Function GetCacheItem(ByVal rawKey As String) As Object
    Return HttpRuntime.Cache(GetCacheKey(rawKey))
End Function
Private ReadOnly MasterCacheKeyArray() As String = {"ProductsCache"}
Private Function GetCacheKey(ByVal cacheKey As String) As String
    Return String.Concat(MasterCacheKeyArray(0), "-", cacheKey)
End Function

GetCacheItem(key) не использует значение ключа , как указано, но вместо этого вызывает GetCacheKey(key) метод , который возвращает ключ , добавленный к ProductsCache-. Объект MasterCacheKeyArray, содержащий строку ProductsCache, также используется методом AddCacheItem(key, value) , как мы увидим на мгновение.

Из класса кода программной части ASP.NET страницы доступ к кэшу данных можно получить с помощью Page свойства класса s Cacheи обеспечивает синтаксис, например Cache("key") = value, как описано в шаге 2. Из класса в архитектуре доступ к кэшу данных можно получить с помощью HttpRuntime.Cache или HttpContext.Current.Cache. Запись в блоге Питера ДжонсонаHttpRuntime.Cache и HttpContext.Current.Cache отмечает небольшое преимущество производительности при использовании HttpRuntime вместо HttpContext.Current. Следовательно, ProductsCL использует HttpRuntime.

Примечание

Если архитектура реализована с помощью проектов библиотеки классов, необходимо добавить ссылку на сборку System.Web , чтобы использовать HttpRuntime классы и HttpContext .

Если элемент не найден в кэше ProductsCL , методы класса получают данные из BLL и добавляют их в кэш с помощью AddCacheItem(key, value) метода . Чтобы добавить значение в кэш, можно использовать следующий код, который использует 60-секундный срок действия:

Const CacheDuration As Double = 60.0
Private Sub AddCacheItem(ByVal rawKey As String, ByVal value As Object)
    DataCache.Insert(GetCacheKey(rawKey), value, Nothing, _
        DateTime.Now.AddSeconds(CacheDuration), _
        System.Web.Caching.Cache.NoSlidingExpiration)
End Sub

DateTime.Now.AddSeconds(CacheDuration) указывает срок действия на основе времени в 60 секунд в будущем, а System.Web.Caching.Cache.NoSlidingExpiration указывает, что скользящий срок действия отсутствует. Хотя эта Insert перегрузка метода имеет входные параметры как для абсолютного, так и для скользящего истечения срока действия, можно указать только один из двух. При попытке указать как абсолютное время, так и диапазон времени, Insert метод вызовет ArgumentException исключение.

Примечание

В настоящее время эта реализация AddCacheItem(key, value) метода имеет некоторые недостатки. Мы уделим эти проблемы на шаге 4.

Шаг 4. Недопустимость кэша при изменении данных с помощью архитектуры

Наряду с методами извлечения данных уровень кэширования должен предоставлять те же методы, что и BLL для вставки, обновления и удаления данных. Методы изменения данных CL не изменяют кэшированные данные, а вызывают соответствующий метод изменения данных BLL, а затем делают кэш недействительным. Как мы видели в предыдущем руководстве, это то же поведение, что и ObjectDataSource, когда включены функции кэширования и вызываются методы Insert, Updateили Delete .

Следующая UpdateProduct перегрузка показывает, как реализовать методы изменения данных в cl:

<DataObjectMethodAttribute(DataObjectMethodType.Update, False)> _
Public Function UpdateProduct(productName As String, _
    unitPrice As Nullable(Of Decimal), productID As Integer) _
    As Boolean
    Dim result As Boolean = API.UpdateProduct(productName, unitPrice, productID)
    ' TODO: Invalidate the cache
    Return result
End Function

Вызывается соответствующий метод уровня бизнес-логики для изменения данных, но перед возвратом ответа необходимо сделать кэш недействительным. К сожалению, сделать кэш недействительным не так просто, так как ProductsCL классы GetProducts() и GetProductsByCategoryID(categoryID) методы добавляют элементы в кэш с разными ключами, а GetProductsByCategoryID(categoryID) метод добавляет отдельный элемент кэша для каждого уникального идентификатора categoryID.

Если кэш недействителен, необходимо удалить все элементы, которые могли быть добавлены классом ProductsCL . Это можно сделать, связав зависимость кэша с каждым элементом, добавленным в кэш в методе AddCacheItem(key, value) . Как правило, зависимость кэша может быть другим элементом кэша, файлом в файловой системе или данными из базы данных Microsoft SQL Server. При изменении зависимости или удалении из кэша элементы кэша, с которыми она связана, автоматически удаляются из кэша. В этом руководстве мы хотим создать в кэше дополнительный элемент, который служит зависимостью кэша для всех элементов, добавленных с помощью ProductsCL класса . Таким образом, все эти элементы можно удалить из кэша, просто удалив зависимость кэша.

Давайте обновим AddCacheItem(key, value) метод таким образом, чтобы каждый элемент, добавленный в кэш с помощью этого метода, был связан с одной зависимостью кэша:

Private Sub AddCacheItem(ByVal rawKey As String, ByVal value As Object)
    Dim DataCache As System.Web.Caching.Cache = HttpRuntime.Cache
    ' Make sure MasterCacheKeyArray[0] is in the cache - if not, add it
    If DataCache(MasterCacheKeyArray(0)) Is Nothing Then
        DataCache(MasterCacheKeyArray(0)) = DateTime.Now
    End If
    ' Add a CacheDependency
    Dim dependency As New Caching.CacheDependency(Nothing, MasterCacheKeyArray) _
        DataCache.Insert(GetCacheKey(rawKey), value, dependency, _
        DateTime.Now.AddSeconds(CacheDuration), _
        System.Web.Caching.Cache.NoSlidingExpiration)
End Sub

MasterCacheKeyArray — это строковый массив, содержащий одно значение ProductsCache. Сначала элемент кэша добавляется в кэш и назначается текущая дата и время. Если элемент кэша уже существует, он обновляется. Далее создается зависимость кэша. Конструктор CacheDependency класса имеет ряд перегрузок, но используемый здесь ожидает два String входных данных массива. Первый указывает набор файлов, используемых в качестве зависимостей. Так как мы не хотим использовать зависимости на основе файлов, для первого входного Nothing параметра используется значение . Второй входной параметр указывает набор ключей кэша для использования в качестве зависимостей. Здесь мы указываем нашу единственную зависимость , MasterCacheKeyArray. CacheDependency Затем передается в Insert метод .

Если изменить значение AddCacheItem(key, value), сделать кэш недействительным будет так же просто, как удалить зависимость.

<DataObjectMethodAttribute(DataObjectMethodType.Update, False)> _
Public Function UpdateProduct(ByVal productName As String, _
    ByVal unitPrice As Nullable(Of Decimal), ByVal productID As Integer) _
    As Boolean
    Dim result As Boolean = API.UpdateProduct(productName, unitPrice, productID)
    ' Invalidate the cache
    InvalidateCache()
    Return result
End Function
Public Sub InvalidateCache()
    ' Remove the cache dependency
    HttpRuntime.Cache.Remove(MasterCacheKeyArray(0))
End Sub

Шаг 5. Вызов уровня кэширования из уровня представления

Классы и методы уровня кэширования можно использовать для работы с данными с помощью методов, рассмотренных в этих руководствах. Чтобы проиллюстрировать работу с кэшируемыми данными, сохраните изменения в ProductsCL классе , а затем откройте FromTheArchitecture.aspx страницу в папке Caching и добавьте GridView. Из смарт-тега GridView создайте объект ObjectDataSource. На первом шаге мастера класс должен отображаться ProductsCL как один из вариантов из раскрывающегося списка.

Класс ProductsCL включен в список Drop-Down бизнес-объектов

Рис. 4. Класс ProductsCL включен в список бизнес-объектов Drop-Down (щелкните для просмотра полноразмерного изображения)

После выбора ProductsCLнажмите кнопку Далее. В раскрывающемся списке на вкладке SELECT есть два элемента: GetProducts() и GetProductsByCategoryID(categoryID) на вкладке UPDATE есть единственная UpdateProduct перегрузка. Выберите метод на GetProducts() вкладке SELECT и UpdateProducts метод на вкладке UPDATE и нажмите кнопку Готово.

Методы класса ProductsCL перечислены в Drop-Down Списки

Рис. 5. ProductsCL Методы класса перечислены в Drop-Down Списки (Щелкните для просмотра полноразмерного изображения)

После завершения работы мастера Visual Studio установит свойству original_{0} ObjectDataSource OldValuesParameterFormatString значение и добавит соответствующие поля в GridView. Измените OldValuesParameterFormatString свойство на его значение {0}по умолчанию и настройте GridView для поддержки разбиения по страницам, сортировки и редактирования. Так как перегрузка UploadProducts , используемая cl, принимает только имя и цену измененного продукта, ограничьте GridView, чтобы только эти поля были редактируемыми.

В предыдущем руководстве мы определили GridView, чтобы включить поля для ProductNameполей , CategoryNameи UnitPrice . Вы можете реплицировать это форматирование и структуру. В этом случае декларативная разметка GridView и ObjectDataSource должна выглядеть примерно так:

<asp:GridView ID="Products" runat="server" AutoGenerateColumns="False" 
    DataKeyNames="ProductID" DataSourceID="ProductsDataSource" 
    AllowPaging="True" AllowSorting="True">
    <Columns>
        <asp:CommandField ShowEditButton="True" />
        <asp:TemplateField HeaderText="Product" SortExpression="ProductName">
            <EditItemTemplate>
                <asp:TextBox ID="ProductName" runat="server" 
                    Text='<%# Bind("ProductName") %>' />
                <asp:RequiredFieldValidator ID="RequiredFieldValidator1"
                    ControlToValidate="ProductName" Display="Dynamic" 
                    ErrorMessage="You must provide a name for the product." 
                    SetFocusOnError="True"
                    runat="server">*</asp:RequiredFieldValidator>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label2" runat="server" 
                    Text='<%# Bind("ProductName") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:BoundField DataField="CategoryName" HeaderText="Category" 
            ReadOnly="True" SortExpression="CategoryName" />
        <asp:TemplateField HeaderText="Price" SortExpression="UnitPrice">
            <EditItemTemplate>
                $<asp:TextBox ID="UnitPrice" runat="server" Columns="8" 
                    Text='<%# Bind("UnitPrice", "{0:N2}") %>'></asp:TextBox>
                <asp:CompareValidator ID="CompareValidator1" runat="server" 
                    ControlToValidate="UnitPrice" Display="Dynamic" 
                    ErrorMessage="You must enter a valid currency value with 
                        no currency symbols. Also, the value must be greater than 
                        or equal to zero."
                    Operator="GreaterThanEqual" SetFocusOnError="True" 
                    Type="Currency" ValueToCompare="0">*</asp:CompareValidator>
            </EditItemTemplate>
            <ItemStyle HorizontalAlign="Right" />
            <ItemTemplate>
                <asp:Label ID="Label1" runat="server" 
                    Text='<%# Bind("UnitPrice", "{0:c}") %>' />
            </ItemTemplate>
        </asp:TemplateField>
    </Columns>
</asp:GridView>
<asp:ObjectDataSource ID="ProductsDataSource" runat="server" 
    OldValuesParameterFormatString="{0}" SelectMethod="GetProducts" 
    TypeName="ProductsCL" UpdateMethod="UpdateProduct">
    <UpdateParameters>
        <asp:Parameter Name="productName" Type="String" />
        <asp:Parameter Name="unitPrice" Type="Decimal" />
        <asp:Parameter Name="productID" Type="Int32" />
    </UpdateParameters>
</asp:ObjectDataSource>

На этом этапе у нас есть страница, использующая слой кэширования. Чтобы увидеть кэш в действии, задайте точки останова ProductsCL в классах GetProducts() и UpdateProduct методах . Перейдите на страницу в браузере и выполните пошаговое выполнение кода при сортировке и разбиении по страницам, чтобы просмотреть данные, полученные из кэша. Затем обновите запись и обратите внимание, что кэш становится недействительным и, следовательно, извлекается из BLL при отскоке данных в GridView.

Примечание

Уровень кэширования, предоставленный в сопроводительном файле этой статьи, не является полным. Он содержит только один класс , ProductsCLкоторый имеет только несколько методов. Кроме того, только на одной странице ASP.NET используется cl (~/Caching/FromTheArchitecture.aspx), все остальные по-прежнему ссылались на BLL напрямую. Если вы планируете использовать cl в приложении, все вызовы из уровня презентации должны переходить в cl, что потребует, чтобы классы и методы CL охватывали эти классы и методы в BLL, которые в настоящее время используются уровнем представления.

Сводка

Хотя кэширование можно применять на уровне представления с помощью элементов управления SqlDataSource и ObjectDataSource ASP.NET 2.0, в идеале обязанности по кэшированию будут делегированы отдельному уровню в архитектуре. В этом руководстве мы создали слой кэширования, расположенный между уровнем представления и уровнем бизнес-логики. Уровень кэширования должен предоставить тот же набор классов и методов, которые существуют в BLL и вызываются из уровня представления.

В примерах уровня кэширования, которые мы рассмотрели в этом и предыдущих руководствах, показана реактивная загрузка. При реактивной загрузке данные загружаются в кэш только в том случае, если выполняется запрос данных и эти данные отсутствуют в кэше. Данные также можно заранее загрузить в кэш, что позволяет загружать данные в кэш до того, как они действительно понадобятся. В следующем руководстве мы рассмотрим пример упреждающей загрузки, когда рассмотрим, как сохранить статические значения в кэше при запуске приложения.

Счастливого программирования!

Об авторе

Скотт Митчелл( Scott Mitchell), автор семи книг ASP/ASP.NET и основатель 4GuysFromRolla.com, работает с веб-технологиями Майкрософт с 1998 года. Скотт работает независимым консультантом, тренером и писателем. Его последняя книга Sams Teach Yourself ASP.NET 2.0 в 24 часах. Он может быть доступен в mitchell@4GuysFromRolla.com. или через его блог, который можно найти по адресу http://ScottOnWriting.NET.

Особая благодарность

Эта серия учебников была рассмотрена многими полезными рецензентами. Ведущим рецензентом этого руководства была Тетера Мерфи. Хотите просмотреть предстоящие статьи MSDN? Если да, опустите мне строку на mitchell@4GuysFromRolla.com.