I am developing an application that works with files and folder on a network share (similar to the windows explorer). The application is used by multiple users and provides some commands for modifying the file system. ProblemThe parallel access to the file system may result in conflicts.Example: User A renames a folder (FolderA) while user B is executing a long-running process that moves / creates / deletes some of the sub folders of folder B. Therefore the rename breaks the operation of user B.RequirementsAn application should be able to lock folders if required.Other applications should be notified as soon as possible about locks, so that these applications may disable corresponding commands (e.g. Rename will be disabled for a folder where any child folder is locked).All kinds of network shares should be supported (windwos, samba, ...)The application should be able to show the locking user / and timestamp.ApproachTo avoid conflicts as described above, a centralized locking mechanism should be introduced.The idea is, to create a file on the network share. That file contains all current locks (one per line). Format: RelativePath|User|DateTime.Each application checks in certain intervals (e.g. 100 ms) if the file changed and updates it's internal state accordingly. Each application may add / remove entries to / from the file.ImplementationThe API to the storage uses the following simple interface:ILockStorage.cspublic interface ILockStorage : IDisposable{    event EventHandler<LockEntriesChangedEventArgs> LockEntriesChanged;    void AddEntry(LockEntry entryToAdd);    void RemoveEntry(LockEntry entryToRemove);}LockEntriesChangedEventArgs.cspublic class LockEntriesChangedEventArgs : EventArgs{    private readonly LockEntry[] myEntriesAdded;    private readonly LockEntry[] myEntriesRemoved;    public LockEntriesChangedEventArgs(LockEntry[] entriesAdded, LockEntry[] EntriesRemoved)    {        myEntriesAdded = entriesAdded;        myEntriesRemoved = EntriesRemoved;    }    public LockEntry[] EntriesAdded { get { return myEntriesAdded; } }    public LockEntry[] EntriesRemoved { get { return myEntriesRemoved; } }}LockEntry.cspublic class LockEntry : IEquatable<LockEntry>{    private readonly string myPath;    private readonly string myUser;    private readonly DateTime myLockTime;    public LockEntry(string path, string user, DateTime lockTime)    {        path.Ensure(path).IsNotNull();        user.Ensure(user).IsNotNull();        myPath = path.ToLower().Trim('\\');        myUser = user;        myLockTime = lockTime;    }    public string User { get { return myUser; } }    public string Path { get { return myPath; } }    public DateTime LockTime { get { return myLockTime; } }    public static bool TryParse(string input, out LockEntry lockEntry)    {        lockEntry = null;        if (string.IsNullOrWhiteSpace(input))            return false;        var entryParts = input.Split('|');        if (entryParts.Length != 3)            return false;        var path = entryParts[0];        var user = entryParts[1];        var lockTimeString = entryParts[2];        DateTime lockTime;        if (string.IsNullOrWhiteSpace(path) ||            string.IsNullOrWhiteSpace(user) ||            !DateTime.TryParse(lockTimeString, CultureInfo.InvariantCulture, DateTimeStyles.None, out lockTime))            return false;        lockEntry = new LockEntry(path, user, lockTime);        return true;    }    public override string ToString()    {        return string.Format({0}|{1}|{2}, Path, User, LockTime.ToString(CultureInfo.InvariantCulture));    }    public override bool Equals(object obj)    {        var other = obj as LockEntry;        if (other == null)            return false;        return ((IEquatable<LockEntry>)this).Equals(other);    }    public override int GetHashCode()    {        return myLockTime.GetHashCode() ^               myUser.GetHashCode() ^               myPath.GetHashCode();    }    bool IEquatable<LockEntry>.Equals(LockEntry other)    {        return myLockTime == other.LockTime &&               myUser == other.User &&               myPath.Equals(other.Path);    }}The implementation of the interface:LockStorage.csinternal class LockStorage : ILockStorage{    private const int WRITE_RETRY_COUNT = 20;    private const int WRITE_RETRY_INTERVAL_MS = 50;    private readonly Timer myTimer;    private readonly FileInfo myFileInfo;    private LockEntry[] myLockEntries = new LockEntry[0];    private DateTime myLastSyncTimestamp = DateTime.MinValue;    private bool myIsDisposed;    public event EventHandler<LockEntriesChangedEventArgs> LockEntriesChanged;    /* Nested Types     * ********************************************************************/    private abstract class LockEntryWriter    {        private readonly LockStorage myLockStorage;        protected LockEntryWriter(LockStorage lockStorage)        {            myLockStorage = lockStorage;        }        public void Write()        {            myLockStorage.myTimer.Stop();            try            {                IOException lastException = null;                for (int i = 0; i < WRITE_RETRY_COUNT; i++)                {                    try                    {                        WriteInternal();                        return;                    }                    catch (IOException ex)                    {                        // may occur if the file stream is open for writing / readings                        lastException = ex;                        Thread.Sleep(WRITE_RETRY_INTERVAL_MS);                    }                }                // write failed                throw new IOException(Unable to write to lock file. (retried  + WRITE_RETRY_COUNT +  times), lastException);            }            finally            {                myLockStorage.myTimer.Start();            }        }        private void WriteInternal()        {            LockEntry[] entries;            using (var writeStream = OpenWriteStream(out entries))            {                if (!IsWriteRequired(entries))                    return;                using (var writer = new StreamWriter(writeStream))                {                    Write(writer, entries);                    // truncate file                    writeStream.SetLength(writeStream.Position);                }            }        }        private Stream OpenWriteStream(out LockEntry[] entries)        {            var fileInfo = myLockStorage.myFileInfo;            var lastLyncTimestamp = myLockStorage.myLastSyncTimestamp;            var readWriteStream = fileInfo.Open(FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);            fileInfo.Refresh();            entries = myLockStorage.LockEntries;            if (lastLyncTimestamp < fileInfo.LastWriteTime)            {                var lines = ReadLockFile(readWriteStream);                entries = ToLockEntries(lines.ToArray());            }            readWriteStream.Position = 0;            return readWriteStream;        }        protected abstract bool IsWriteRequired(LockEntry[] currentEntries);        protected abstract void Write(StreamWriter writer, LockEntry[] currentEntries);    }    private class AddLockEntryWriter : LockEntryWriter    {        private readonly LockEntry myEntryToAdd;        public AddLockEntryWriter(LockStorage lockStorage, LockEntry entryToAdd)            : base(lockStorage)        {            myEntryToAdd = entryToAdd;        }        protected override bool IsWriteRequired(LockEntry[] currentEntries)        {            return !currentEntries.Contains(myEntryToAdd);        }        protected override void Write(StreamWriter writer, LockEntry[] currentEntries)        {            foreach (var entry in currentEntries)                writer.WriteLine(entry.ToString());            writer.WriteLine(myEntryToAdd.ToString());        }    }    private class RemoveLockEntryWriter : LockEntryWriter    {        private readonly LockEntry myEntryToRemove;        public RemoveLockEntryWriter(LockStorage lockStorage, LockEntry entryToRemove)            : base(lockStorage)        {            myEntryToRemove = entryToRemove;        }        protected override bool IsWriteRequired(LockEntry[] currentEntries)        {            return currentEntries.Contains(myEntryToRemove);        }        protected override void Write(StreamWriter writer, LockEntry[] currentEntries)        {            foreach (var entry in currentEntries.Except(new[] { myEntryToRemove }))                writer.WriteLine(entry.ToString());        }    }    /* .ctor     * ********************************************************************/    internal LockStorage(string lockfilePath, TimeSpan intervall)    {        myFileInfo = new FileInfo(lockfilePath);        UpdateInternalState();        myTimer = new Timer(intervall.Milliseconds);        myTimer.AutoReset = false;        myTimer.Elapsed += TimerElapsed;        myTimer.Start();    }    /* public API     * ********************************************************************/    public LockEntry[] LockEntries { get { return myLockEntries.ToArray(); } }    public void AddEntry(LockEntry entryToAdd)    {        new AddLockEntryWriter(this, entryToAdd).Write();    }    public void RemoveEntry(LockEntry entryToRemove)    {        new RemoveLockEntryWriter(this, entryToRemove).Write();    }    /* privates     * ********************************************************************/    private void TimerElapsed(object sender, ElapsedEventArgs e)    {        try        {            myFileInfo.Refresh();            if (myFileInfo.LastWriteTime == myLastSyncTimestamp)                return;            UpdateInternalState();        }        catch (Exception ex)        {            // LogError(Error while reloading lock file, ex);        }        finally        {            myTimer.Start();        }    }    private void UpdateInternalState()    {        try        {            string[] lines;            using (var stream = myFileInfo.Open(FileMode.OpenOrCreate, FileAccess.Read, FileShare.Read))                lines = ReadLockFile(stream);            if (lines == null)                return;            var lockEntries = ToLockEntries(lines);            myLastSyncTimestamp = myFileInfo.LastWriteTime;            UpdateLockEntries(lockEntries);        }        catch (IOException)        {            /* Ignore: May be occur if the file is being written at the moment */        }    }    private void UpdateLockEntries(LockEntry[] lockEntries)    {        var entriesToRemove = new List<LockEntry>(myLockEntries);        var addedEntries = new List<LockEntry>();        foreach (var lockEntry in lockEntries)        {            entriesToRemove.Remove(lockEntry);            if (myLockEntries.Contains(lockEntry))                continue;            addedEntries.Add(lockEntry);        }        if (addedEntries.Any() || entriesToRemove.Any())        {            myLockEntries = myLockEntries.Except(entriesToRemove)                                         .Concat(addedEntries)                                         .ToArray();            OnLockEntriesChanged(addedEntries.ToArray(), entriesToRemove.ToArray());        }    }    private static LockEntry[] ToLockEntries(string[] lines)    {        var lockEntries = new List<LockEntry>();        foreach (var line in lines)        {            LockEntry lockEntry;            if (!LockEntry.TryParse(line, out lockEntry))            {                //LogWarning(Unable to parse lock file entry' + line + '. Entry skipped.);                continue;            }            lockEntries.Add(lockEntry);        }        return lockEntries.ToArray();    }    private static string[] ReadLockFile(Stream stream)    {        var lines = new List<string>();        using (var reader = new StreamReader(stream, Encoding.UTF8, true, 1024, true))            while (!reader.EndOfStream)                lines.Add(reader.ReadLine());        return lines.ToArray();    }    private void OnLockEntriesChanged(LockEntry[] entriesAdded, LockEntry[] entriesRemoved)    {        var handler = LockEntriesChanged;        if (handler != null)            handler(this, new LockEntriesChangedEventArgs(entriesAdded, entriesRemoved));    }    /* IDisposable implementation     * ********************************************************************/    public void Dispose()    {        Dispose(true);        GC.SuppressFinalize(this);    }    private void Dispose(bool disposing)    {        if (myIsDisposed)            return;        if (disposing)        {            myTimer.Stop();            myTimer.Dispose();        }        myIsDisposed = true;    }}I am primary interested in comments related to stability and robustness.Are there any use cases that would break the storage?Any alternative solutions for the problem of concurrent file access (read/write)?However, all kinds of comments are welcome of course.
Implemenation for concurrent file access (read/write)
c#;file system;concurrency;stream
There is no need to cast ((IEquatable<LockEntry>)this).Equals(other);, you can just call Equals(other)It's weird to use both Equals and == for the same type in the same bool expression:myUser == other.User &&myPath.Equals(other.Path);Just stick to one or the other, so reader does not have to look for deeper meaning to all this. :)SHOUT_CASE is unusual in C#. You should use PascalCase for constants instead: WriteRetryCount.I think you should extract constants to separate class, so you can easily serialize it later on and tweak those values without re-compilation. You can call it LockStorageSettings or something. You can move filePath and timer interval there as well.You shouldn't use nested classes to build class hierarchy. I'd say, that if nested class is larger than 50 lines - it probably deserves to be moved to dedicated .cs file.Default encoding may vary depending on your users' environments. Since you are using a shared file, you have to specify encoding explicitly when writing and reading, to make sure that all the users are using the same encoding.OpenWriteStream does too much: it opens the stream AND it parses the entries. Split it into two methods and replace out with proper return value. Nobody likes using out. :)I think Write method is the wrong method to override. I mean writing logic does not change, its fixed: you take a bunch of entries, and you write them one by one. What changes is which entries you have to write. So instead of abstract Write you should have abstract IEnumerable<LockEntry> GetUpdatedEntries(IEnumerable<LockEntry> oldEntries).Your LockStorage class is tightly coupled with LockEntryWriter class which is a sign of bad design. LockEntryWriter should have a single responsibility: writing entries to file. It should not manage timer or update cache of parent component. IMHO, you should remove LockStorage reference from LockEntryWriter constructor.