Xml repository implementation

Update 2020: There is already nice nuget packages that handle this nicelly, such as SharpRepository.XmlRepository, which is part of a suite of many repository implementations.


Well, Petter solution is nice.

Just to share my implementation I will answer my question again, I hope that can be usefull to someone. Please, rate and comment.

public interface IRepository<T>
{
    IEnumerable<T> GetAll();
    IEnumerable<T> GetAll(object parentId);
    T GetByKey(object keyValue);

    void Insert(T entidade, bool autoPersist = true);
    void Update(T entidade, bool autoPersist = true);
    void Delete(T entidade, bool autoPersist = true);

    void Save();
}

And the base class for XML Repositories

public abstract class XmlRepositoryBase<T> : IRepository<T>
{

    public virtual XElement ParentElement { get; protected set; }

    protected XName ElementName { get; private set; }

    protected abstract Func<XElement, T> Selector { get; }

    #endregion

    protected XmlRepositoryBase(XName elementName)
    {
        ElementName = elementName;

        // clears the "cached" ParentElement to allow hot file changes
        XDocumentProvider.Default.CurrentDocumentChanged += (sender, eventArgs) => ParentElement = null;
    }

    #region

    protected abstract void SetXElementValue(T model, XElement element);

    protected abstract XElement CreateXElement(T model);

    protected abstract object GetEntityId(T entidade);

    #region IRepository<T>

    public T GetByKey(object keyValue)
    {
        // I intend to remove this magic string "Id"
        return XDocumentProvider.Default.GetDocument().Descendants(ElementName)
            .Where(e => e.Attribute("Id").Value == keyValue.ToString())
            .Select(Selector)
            .FirstOrDefault();
    }

    public IEnumerable<T> GetAll()
    {
        return ParentElement.Elements(ElementName).Select(Selector);
    }

    public virtual IEnumerable<T> GetAll(object parentId)
    {
        throw new InvalidOperationException("This entity doesn't contains a parent.");
    }

    public virtual void Insert(T entity, bool autoPersist = true)
    {
        ParentElement.Add(CreateXElement(entity));

        if (autoPersist)
            Save();
    }

    public virtual void Update(T entity, bool autoPersist= true)
    {
        // I intend to remove this magic string "Id"
        SetXElementValue(
            entity,
            ParentElement.Elements().FirstOrDefault(a => a.Attribute("Id").Value == GetEntityId(entity).ToString()
        ));

        if (persistir)
            Save();
    }

    public virtual void Delete(T entity, bool autoPersist = true)
    {
        ParentElement.Elements().FirstOrDefault(a => a.Attribute("Id").Value == GetEntityId(entity).ToString()).Remove();

        if (autoPersist)
            Save();
    }


    public virtual void Save()
    {
        XDocumentProvider.Default.Save();
    }
    #endregion

    #endregion
}

And 2 more abstract classes, one to Independent entities and other to child entities. To avoid reading the Xml file every time, I've made a kind of cache control

public abstract class EntityXmlRepository<T> : XmlRepositoryBase<T>
{
    #region cache control

    private XElement _parentElement;
    private XName xName;

    protected EntityXmlRepository(XName entityName)
        : base(entityName)
    {
    }

    public override XElement ParentElement
    {
        get
        {
            // returns in memory element or get it from file
            return _parentElement ?? ( _parentElement = GetParentElement() );
        }
        protected set
        {
            _parentElement = value;
        }
    }

    /// <summary>
    /// Gets the parent element for this node type
    /// </summary>
    protected abstract XElement GetParentElement();
    #endregion
}

Now the implementation for child types

public abstract class ChildEntityXmlRepository<T> : XmlRepositoryBase<T>
{
    private object _currentParentId;
    private object _lastParentId;

    private XElement _parentElement;

    public override XElement ParentElement
    {
        get 
        {
            if (_parentElement == null)
            {
                _parentElement = GetParentElement(_currentParentId);
                _lastParentId = _currentParentId;
            }
            return _parentElement;
        }
        protected set 
        {
            _parentElement = value; 
        }
    }

    /// <summary>
    /// Defines wich parent entity is active
    /// when this property changes, the parent element field is nuled, forcing the parent element to be updated
    /// </summary>
    protected object CurrentParentId
    {
        get
        {
            return _currentParentId;
        }
        set
        {
            _currentParentId = value;
            if (value != _lastParentId)
            {
                _parentElement = null;
            }
        }
    }       



    protected ChildEntityXmlRepository(XName entityName) : base(entityName){}

    protected abstract XElement GetParentElement(object parentId);

    protected abstract object GetParentId(T entity);


    public override IEnumerable<T> GetAll(object parentId)
    {
        CurrentParentId = parentId;
        return ParentElement.Elements(ElementName).Select(Selector);
    }

    public override void Insert(T entity, bool persistir = true)
    {
        CurrentParentId = GetParentId(entity);
        base.Insert(entity, persistir);
    }

    public override void Update(T entity, bool persistir = true)
    {
        CurrentParentId = GetParentId(entity);
        base.Update(entity, persistir);
    }

    public override void Delete(T entity, bool persistir = true)
    {
        CurrentParentId = GetParentId(entity);
        base.Delete(entity, persistir);
    }
}

Now, a real world implementation

public class RepositorioAgendamento : EntityXmlRepository<Agendamento>, IRepositorioAgendamento
{
    protected override Func<XElement, Agendamento> Selector
    {
        get
        {
            return x => new Agendamento() {
                Id = x.Attribute("Id").GetGuid(),
                   Descricao = x.Attribute("Descricao").Value,
                   TipoAgendamento = x.Attribute("TipoAgendamento").GetByte(),
                   Dias = x.Attribute("Dias").GetByte(),
                   Data = x.Attribute("Data").GetDateTime(),
                   Ativo = x.Attribute("Ativo").GetBoolean(),
            };
        }
    }

    protected override XElement CreateXElement(Agendamento agendamento)
    {
        agendamento.Id = Guid.NewGuid();

        return new XElement(ElementName,
                new XAttribute("Id", agendamento.Id),
                new XAttribute("Descricao", agendamento.Descricao),
                new XAttribute("TipoAgendamento", agendamento.TipoAgendamento),
                new XAttribute("Dias", agendamento.Dias),
                new XAttribute("Data", agendamento.Data),
                new XAttribute("Ativo", agendamento.Ativo),
                new XElement(XmlNames.GruposBackup),
                new XElement(XmlNames.Credenciais)
                );
    }

    protected override void SetXElementValue(Agendamento modelo, XElement elemento)
    {
        elemento.Attribute("Descricao").SetValue(modelo.Descricao);
        elemento.Attribute("TipoAgendamento").SetValue(modelo.TipoAgendamento);
        elemento.Attribute("Dias").SetValue(modelo.Dias);
        elemento.Attribute("Data").SetValue(modelo.Data);
        elemento.Attribute("Ativo").SetValue(modelo.Ativo);
    }


    public RepositorioAgendamento() : base(XmlNames.Agendamento)
    {

    }

    protected override XElement GetParentElement()
    {
        return XDocumentProvider.Default.GetDocument().Elements(XmlNames.Agendamentos).First();
    }

    protected override object GetEntityId(Agendamento entidade)
    {
        return entidade.Id;
    }

    public IEnumerable<Agendamento> ObterAtivos()
    {
        return ParentElement.Elements()
            .Where(e => e.Attribute("Ativo").GetBoolean())
            .Select(Selector);
    }
}

And now, the XDocumentProvider. Its function is to abstract the access to the xml file and unify to all repositories what XDocument is the data context. This can be named UnitOfWork?

public abstract class XDocumentProvider
{
    // not thread safe yet
    private static bool pendingChanges;

    private bool _documentLoadedFromFile;

    FileSystemWatcher fileWatcher;

    public static XDocumentProvider Default;

    public event EventHandler CurrentDocumentChanged;

    private XDocument _loadedDocument;

    public string FileName { get; set; }


    protected XDocumentProvider()
    {
        fileWatcher = new FileSystemWatcher();
        fileWatcher.NotifyFilter = NotifyFilters.LastWrite;
        fileWatcher.Changed += fileWatcher_Changed;
    }

    void fileWatcher_Changed(object sender, FileSystemEventArgs e)
    {
        if (_documentLoadedFromFile && !pendingChanges)
        {
            GetDocument(true);
        }
    }


    /// <summary>
    /// Returns an open XDocument or create a new document
    /// </summary>
    /// <returns></returns>
    public XDocument GetDocument(bool refresh = false)
    {
        if (refresh || _loadedDocument == null)
        {
            // we need to refactor it, but just to demonstrate how should work I will send this way ;P
            if (File.Exists(FileName))
            {
                _loadedDocument = XDocument.Load(FileName);
                _documentLoadedFromFile = true;

                if (fileWatcher.Path != Environment.CurrentDirectory)
                {
                    fileWatcher.Path = Environment.CurrentDirectory;
                    fileWatcher.Filter = FileName;
                    fileWatcher.EnableRaisingEvents = true;
                }
            }
            else
            {
                _loadedDocument = CreateNewDocument();
                fileWatcher.EnableRaisingEvents = false;
                _documentLoadedFromFile = false;
            }

            if(CurrentDocumentChanged != null) CurrentDocumentChanged(this, EventArgs.Empty);
        }

        return _loadedDocument;
    }

    /// <summary>
    /// Creates a new XDocument with a determined schemma.
    /// </summary>
    public abstract XDocument CreateNewDocument();

    public void Save()
    {
        if (_loadedDocument == null)
            throw new InvalidOperationException();

        try
        {
            // tells the file watcher that he cannot raise the changed event, because his function is to capture external changes.
            pendingChanges = true;
            _loadedDocument.Save(FileName);
        }
        finally
        {
            pendingChanges = false;
        }
    }
}

Then I can have many repositories for diferent entities adding pendent persistence actions in a single data context.

I have made tests for my application that uses this repository using mocks and worked well.

On my IoC configuration I have to set the Default for XDocumentProvider. If necessary, we can pass the XDocumentProvider throught constructor instead of this static "Default" property

What do you think about my implementation?

Thanks


A colleague and I implemented exactly such an XML repository, and it's called XmlRepository :-).

It's built internally with linq to XML and the external access is similar to how you use linq for nhibernate. It's done with linq to objects, the usage in client code is very simple, easy and quickly understandable because of the simple XML-commented interface.

The current release (assembly) has no support built in for subclasses or 1:n relations, but the current development source code, which you can find also on the site above, has both of them built in.

It is not completely ready to release-- it may have some bit minor bugs, but try it out, take the source code and improve it, if you like. It's open source.

Any comments, wishes, constructive criticism, and patches for the open source (read: only source) project will make my colleague (Golo Roden) and I happy and bring the project to a better state.

An example application is available here (text is in German).