views:

51

answers:

5

I have an XML document, several actually, that will be editable via a front-end UI. I've discovered a problem with this approach (other than the fact that it is using xml files instead of a database... but I cannot change that right now).

If one user makes a change while another user is in the process of making a change, then the second one's changes will overwrite the first.

I need to be able to request objects from the xml files, change them, and then submit the changes back to the xml file without re-writing the entire file. I've got my entire xml access class posted here (which was formed thanks to wonderful help from stackoverflow!)

using System;
using System.Linq;

using System.Collections;
using System.Collections.Generic;

namespace Repositories
{
    /// <summary>
    /// A file base repository represents a data backing that is stored in an .xml file.
    /// </summary>
    public partial class Repository<T> : IRepository
    {
        /// <summary>
        /// Default constructor for a file repository
        /// </summary>
        public Repository() { }

        /// <summary>
        /// Initialize a basic repository with a filename. This will have to be passed from a context to be mapped.
        /// </summary>
        /// <param name="filename"></param>
        public Repository(string filename)
        {
            FileName = filename;
        }

        /// <summary>
        /// Discovers a single item from this repository.
        /// </summary>
        /// <typeparam name="TItem">The type of item to recover.</typeparam>
        /// <typeparam name="TCollection">The collection the item belongs to.</typeparam>
        /// <param name="expression"></param>
        /// <returns></returns>
        public TItem Single<TItem, TCollection>(Predicate<TItem> expression)
            where TCollection : IDisposable, IEnumerable<TItem>
        {
            using (var list = List<TCollection>())
            {
                return list.Single(i => expression(i));
            }
        }

        /// <summary>
        /// Discovers a collection from the repository,
        /// </summary>
        /// <typeparam name="TCollection"></typeparam>
        /// <returns></returns>
        public TCollection List<TCollection>() 
            where TCollection : IDisposable
        {
            using (var list = System.Xml.Serializer.Deserialize<TCollection>(FileName))
            {
                return (TCollection)list;
            }
        }

        /// <summary>
        /// Discovers a single item from this repository.
        /// </summary>
        /// <typeparam name="TItem">The type of item to recover.</typeparam>
        /// <typeparam name="TCollection">The collection the item belongs to.</typeparam>
        /// <param name="expression"></param>
        /// <returns></returns>
        public List<TItem> Select<TItem, TCollection>(Predicate<TItem> expression)
            where TCollection : IDisposable, IEnumerable<TItem>
        {
            using (var list = List<TCollection>())
            {
                return list.Where( i => expression(i) ).ToList<TItem>();
            }
        }

        /// <summary>
        /// Attempts to save an entire collection.
        /// </summary>
        /// <typeparam name="TCollection"></typeparam>
        /// <param name="collection"></param>
        /// <returns></returns>
        public Boolean Save<TCollection>(TCollection collection)
        {
            try
            {
                // load the collection into an xml reader and try to serialize it.
                System.Xml.XmlDocument xDoc = new System.Xml.XmlDocument();
                xDoc.LoadXml(System.Xml.Serializer.Serialize<TCollection>(collection));

                // attempt to flush the file
                xDoc.Save(FileName);

                // assume success
                return true;
            }
            catch
            {
                return false;
            }
        }


        internal string FileName { get; private set; }
    }

    public interface IRepository
    {
        TItem Single<TItem, TCollection>(Predicate<TItem> expression) where TCollection : IDisposable, IEnumerable<TItem>;
        TCollection List<TCollection>() where TCollection : IDisposable;
        List<TItem> Select<TItem, TCollection>(Predicate<TItem> expression) where TCollection : IDisposable, IEnumerable<TItem>;

        Boolean Save<TCollection>(TCollection collection);
    }
}
+1  A: 

I think you're learning why to use a database.

You cannot have multiple threads both writing the same XML document on disk. The best you could do is have some setup where you submit changes to some central service which will maintain an in-memory copy of the disk document. It would update each change into memory, then flush it to disk. Since it would be the central service for this, it could serialize requests for updates, ensuring a consistent document.

I suppose it should also serve the resulting document to clients.

John Saunders
I have 19 hours to finish this and I don't know how to do it with a database. It isn't a matter of not know why, it's a matter of absolute physical impossibility.
Stacey
I've told them it's not doable in that amount of time, but managers seldom listen to reason.
Stacey
Thank you again for all of your suggestions. At least having input from actual experts let me convince my managers to extend the timeline on this part of the project.
Stacey
@Stacey: now, that's praise! That my input actually swayed managers!
John Saunders
+1  A: 

I don't think that there is a simple solution to your problem, because there are many problems: You cannot update an XML file without rewriting the whole file, but independent of that you have to care about resource locking if multiple users access your data at the same time. There is no easy solution for that problem!

The class you have posted looks also quite strange to me: Why do you serialize to an XmlDocument to write it to disc afterwards? Why not serialize to disc immediately? Catching exceptions and just ignoring them is very bad style!

Achim
Yeah, I know. As for why I pipe it to a document first, that's just the code I had in the older serializer - I need to modify it.
Stacey
+2  A: 

Xml is not a fixed-width record format. It is very hard (read: don't bother) to edit the internals without re-writing it. In most sensible use-cases of xml this is fine and expected. If this is a problem, consider an alternative storage mechanism (a database leaps to mind), and simply import/export xml when needed.

Marc Gravell
+1  A: 

You have 2 ways to go as far as i can see. One, implementing a locking mechanism to prevent more than one person from modifying any given file at a time. Since you are asking how to allow them both to make changes at the same time, I'll assume you can't do this.

The other way is ugly, real ugly. But it should work. Basically, turn your Repository into a singleton (only one static reference across all user requests,) and "edit" the file in memory. After every change set, save the changes to the disk. Since you are working on one copy of the info in memory, one user won't overwrite the other. (Unless they both change the component)

You will have to implement locking for this to work. (You would basically treat each Request as a Thread, and program in a Multi-Threaded Environment.

Good Luck...

TJMonk15
+1  A: 

If you have some flexibility with the XML document format, then there are some things you can do.

One possibility:

  • When you open the file, load a full copy, either into memory, or into a temp file.
  • While editing, keep track of changes, don't modify the files.
  • Frequently (every minute or so) load in any changes to the external file. If the file system supports it, monitor the file for changes and refresh immediately when the file is written to.
  • On save, lock the file for writing, check that there aren't any conflicting external changes to the sections that have been locally modified.
  • If you're clear, make the changes, and release the file.
  • If there's a conflict, you'll have to deal with that somehow anyway.

If you app is amenable to it, you can reduce conflicts by auto-saving and refreshing the local copy often. Just make sure you have exceptional undo capability if you are going to auto-save without user intervention.

Eclipse
I think this is what I will have to go with, for the time being. What I'm having trouble with is figuring out how to declare memory that everyone will access, since it's on a web application.
Stacey
I was thinking of this from a desktop app point of view where each client would have their own in-memory or temp file copy. You could extend this to sessions to that each session works on their own copy.
Eclipse