Часть 22.1 – Провайдер конфигурации Модели. Источники данных

В прошлой части были рассмотрены основные принципы создания провайдеров метаданных и правил проверки данных в ASP.NET MVC 3. Давайте теперь доработаем создаваемое демонстрационное веб-приложение и создадим класс для конфигурации любой Модели.

Цели разработки и некоторые её принципы

В созданном в прошлых частях провайдере метаданных Модели, можно отметить два недостатка:

  • он получает информацию только из одного типа источника (XML-файлов);
  • при наличии нескольких конфигураций, будут наблюдаться периодическая загрузка их файлов, т.к. в памяти может быть расположена только одна из них.

Учтем это при разработке нового провайдера. Кроме того, и метаданные и правила проверки Модели удобно хранить совместно. Поэтому создадим класс, который назовем провайдером конфигурации. Поставим для него следующие задачи:

  • Не зависеть от типа источника информации для Модели. Это могут быть файлы, базами данные или даже жестко заданные в коде значения.
  • Предоставлять как метаданные, так и правила проверки данные.

Проектируем источники данных

Чтобы обеспечить независимость создаваемой реализации от места хранения информации о Модели, создадим интерфейс IModelConfigurationSource. В его обязанности будет входить предоставление экземпляра данных для объекта заданного типа. При невозможности её предоставить будем возвращать пустую ссылку (null).

Источник данных не обязан кэшировать информацию. Это означает, что чтение XML файла или обращение к базе данных будет происходить при каждом её запросе. Однако, предоставим возможность реализациям IModelConfigurationSource уведомлять провайдера конфигурации о том, можно ли кэшировать переданные данные. Это может пригодиться в двух случаях. Во-первых, если со временем сами данные могут измениться. Во-вторых, если по каким-то причинам источник все же решает создать свой кэш.

Проектируем класс конфигурации Модели

Перейдем к разработке самого конфигуратора Модели. При его проектировании необходимо отметить и решить несколько задач:

  1. При проектированные данного решения, как и любого другого, желательно не раскрывать лишние подробности реализации класса.
  2. В основе провайдеров как метаданных так и правил лежат абстрактные базовые классы. А поскольку множественное наследование в C# не поддерживается, то невозможно создать реализацию, представляющую из себя сразу оба типа провайдеров.
  3. С другой стороны, нет необходимости смешивать в одном классе логику работы с источниками информации и реализации разных провайдеров.

Таким образом получаются противоречивые требования. С одной стороны, необходимо создать один класс, который будет предоставлять только нужные данные и методы. С другой – его не только не стоит делать (из-за пункта 3), но и не возможно (из-за пункта 2).

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

Поэтому реализуем класс провайдера конфигурации, который:

  • реализует логику работы с источниками данных и поиску информации по запросу провайдеров;
  • предоставляет доступ к провайдерам метаданных и правил проверки как к своим свойствам;
  • содержит внутренние классы, реализующие логику провайдеров и вспомогательные алгоритмы.

В результате, извне будут доступны только методы управления экземпляром класса. Сами провайдеры будут представлены свойствами тип IModelMetadataProvider и ModelValidatorProvider. При этом их реализации будут в различных классах. Кроме того, используя ключевое слово partial, это позволит разнести исходный код по трем файлом (по файлу на каждый класс).

При таком подходе, реализации провайдеров смогут обращаться к закрытым методам класса провайдера конфигурации. Используем это для реализации обмена данными.

Для внутреннего представления информации о Модели по прежнему будем использовать класс XElement. Важно то, что он позволяет не только загрузить данные из XML-файла, но и создать их самостоятельно. Кроме того, не будем усложнять разработку созданием собственного класса для этой цели.

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

Детали реализации конкретных классов уточним во время их создания. Приступим к разработке.

Подготовка к разработке

Не будем отказываться от возможности использовать несколько провайдером метаданных Модели.

Кроме того, придется сохранить поддержку интерфейса IModelMetadataProvider. Может показаться, что его использование не нужно. Ведь при невозможности создания метаданных для Модели можно просто вернуть null как признак этого.

Однако, в этом случае придется также отказаться от использования AssociatedMetadataProvider в качестве базового класса для реализации провайдера метаданных. Дело в том, что, как уже упоминалось, данный класс использует IMetadataAware для оповещения атрибутов проверки данных о завершении процесса создания метаданных. Соответственно, им в данной ситуации будет передано значение null. На что некоторые атрибуты, например AllowHtmlAttribute, отреагируют выбросом исключения.

Разработанный ранее провайдер метаданных больше не потребуется. Поэтому удалим из проекта файл XmlModelMetadataProvider.cs. Таким образом в папке MetadataProviders останется два файла с исходным кодом: IModelMetadataProvider.cs и ModelMetadataProvidersManager.cs.

В папке Models создадим папку Configuration в которой будут располагаться все создаваемые классы. Соответственно, основное пространство имен для них будет MVCDemo.Models.Configuration.

Разрабатываем источники данных

Для источников данных внутри папки Configuration создадим папку Sources.

Основа – интерфейс IModelConfigurationSource

Разработку начнем с создания интерфейса взаимодействия с источниками данных. Он будет достаточно простой (файл Models\Configuration\Sources\IModelConfigurationSource.cs):

/// Copyright (c) 2011 Andrey Veselov. All rights reserved.
/// WebSite: http://andrey.moveax.ru 
/// Email: andrey@moveax.ru
/// This source is subject to the Microsoft Public License (Ms-PL).

namespace MVCDemo.Models.Configuration.Sources
{
    using System;
    using System.Xml.Linq;

    public interface IModelConfigurationSource
    {
        XElement GetModelConfiguration(Type modelType, out bool useCache);
    }
}

Единственный метод GetModelConfiguration() возвращает метаданные для Модели указанного типа modelType или null, при невозможности их создать. Параметр useCache принимает значение false, если данные являются динамическими и их нельзя кэшировать, или true в противном случае.

Реализация источника данных на основе XML-файлов

Единственная реализация этого интерфейса в данном проекте будет использовать XML файлы в качестве хранилища. Разумеется, по этому же принципу могут быть созданы варианты с жестко заданными параметрами или с получением их из базы данных.

Создадим в папке Models\Configuration\Sources файл XmlModelConfigurationSource.cs:

/// Copyright (c) 2011 Andrey Veselov. All rights reserved.
/// WebSite: http://andrey.moveax.ru 
/// Email: andrey@moveax.ru
/// This source is subject to the Microsoft Public License (Ms-PL).

namespace MVCDemo.Models.Configuration.Sources
{
    using System;
    using System.IO;
    using System.Xml.Linq;

    public class XmlModelConfigurationSource : IModelConfigurationSource
    {
        private readonly string _xmlStoragePath;

        public XmlModelConfigurationSource(string xmlStoragePath)
        {
            if (!Directory.Exists(xmlStoragePath)) {
                throw new ArgumentException(
                    string.Format("Invalid directory {0}", xmlStoragePath));
            }

            this._xmlStoragePath =
                xmlStoragePath.EndsWith("\\") ?
                xmlStoragePath : string.Format("{0}\\", xmlStoragePath);
        }

        #region IModelConfigurationProvider Members

        public XElement GetModelConfiguration(Type modelType, out bool useCache)
        {
            if (modelType == null) {
                useCache = false;
                return null;
            }

            string metadataFile = string.Format(
                "{0}{1}.config", this._xmlStoragePath, modelType.FullName);

            if (!File.Exists(metadataFile)) {
                useCache = false;
                return null;
            }

            useCache = true;
            return XElement.Load(metadataFile);
        }

        #endregion
    }
}

Рассмотрим приведенный выше код подробнее. Конструктор запрашивает путь до места хранения XML файлов и проверяет его существование. При необходимости он будет дополнен в конце символом "\". Метод GetModelConfiguration() пытается найти XML файл c именем, состоящим из полного имени типа и расширения ".config". В случае успеха он считывает оттуда данные и возвращает их. Подобные конфигурации, как правило, изменяются редко. Поэтому установим значение useCache равное true.

На этом завершим реализацию классов источников данных и перейдем конфигуратору Модели.

Конфигуратор Модели

Как уже было отмечено выше, данный класс будет разбит на три части. Каждая из них расположена в своем файле и является относительно завершенной по своему смыслу и назначению.

В папке Models\Configuration создадим класс конфигуратора Модели ModelConfigurationProvider. Вначале разработаем код для работы с источниками. В дальнейшем добавим классы провайдеров метаданных и правил проверки и взаимодействие с ними.

///Copyright (c) 2011 Andrey Veselov. All rights reserved.
/// WebSite: http://andrey.moveax.ru 
/// Email: andrey@moveax.ru
/// This source is subject to the Microsoft Public License (Ms-PL).

namespace MVCDemo.Models.Configuration
{
    using System;
    using System.Collections.Generic;
    using System.Web.Mvc;
    using System.Xml.Linq;
    using MVCDemo.Models.Configuration.Rules;
    using MVCDemo.Models.Configuration.Sources;
    using MVCDemo.Models.MetadataProviders;

    public partial class ModelConfigurationProvider
    {
        #region Private fields

        private readonly Dictionary<Type, ModelConfiguraion> _cache;

        #endregion

        public ModelConfigurationProvider()
        {
            this.Sources = new List<IModelConfigurationSource>();
            this._cache = new Dictionary<Type, ModelConfiguraion>();
        }

        #region Properties

        public List<IModelConfigurationSource> Sources { get; private set; }

        #endregion

        private ModelConfiguraion GetModelConfiguraion(Type modelType)
        {
            foreach (var modelConfiguraion in this._cache) {
                if (modelConfiguraion.Key.Equals(modelType)) {
                    return modelConfiguraion.Value;
                }
            }

            foreach (var source in this.Sources) {
                bool useCache = false;
                XElement config = source.GetModelConfiguration(modelType, out useCache);
                if (config != null) {
                    var data = new ModelConfiguraion(config);

                    if (useCache) {
                        this._cache.Add(modelType, data);
                    }

                    return data;
                }
            }

            return null;
        }

        private class ModelConfiguraion
        {
            public ModelConfiguraion(XElement source)
            {
                this.Source = source;

                var resourceAttr = this.Source.Attribute("resource");
                if (resourceAttr != null) {
                    this.DefaultResourceType = Type.GetType(resourceAttr.Value, throwOnError: true);
                }
            }

            public XElement Source { get; private set; }

            public Type DefaultResourceType { get; private set; }
        }
    }
}

Также разберем подробно что здесь реализовано.

В первую очередь стоит отметить вспомогательный внутренний класс ModelConfiguraion. Во многих случаях будет использоваться ресурс по умолчанию, заданный в элементе <model>. Поэтому, сохраним тип данного ресурса в свойство DefaultResourceType. Информацию от источника данных поместим в Source.

Продолжим рассмотрение класса и поглядим на его поля. Там разместился кэш конфигураций Моделей, который представлен экземпляром словаря Dictionary<Type, ModelConfiguraion>. Именно в него будем заносить полученные от источников ссылки на результат их работы.

Код конструктора не должен вызывать особых вопросов. В нем создаются экземпляры объектов, используемых в данном классе.

Остается отметить последнее свойство – Source. Это список всех зарегистрированных в данном классе источников данных.

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

В следующей части рассмотрим новую версию провайдера метаданных Модели. После чего перейдем к провайдеру правил для неё. >>

Комментарии (2) -

"Подробно это дополнение в рассмотрим при реализации". Лишняя "в" или там должно быть "мы"

@ ch1seL: Спасибо, поправил.

Pingbacks and trackbacks (3)+

Добавить комментарий