Search code examples
c#disposeusingsystem.io.compression

return disposable depending on parent disposable


Here is part of the code i am working on (modified for clarity):

public Stream getMyArchiveStream(string archivepath)
{
    using(var archive = ZipFile.OpenRead(_filepath))
    {
        var entry = archive.GetEntry(archivepath);
        return entry.Open();
    }
}

public void useMyArchiveStream()
{
    using(var myStream = getMyArchiveStream("test.path"))
    {
        //Do stuff
    }
}

Now this fails, because archive is being disposed at the exit of getMyArchiveStream which prevents usage of myStream.

Is there a way to dispose of archive when myStream is being disposed of?

The alternative is to leave archive open and make the containing class disposable, but that has it's own drawbacks in usability.

Background:

I created a simple packaging class (Simpler that System.IO.Packaging at least) that returns the files as byte arrays. Obviously that consumes a lot of memory and i want to use streams instead.


Solution

  • Simply put, you're taking shortcuts you just can't take. As your question title already literally says, you're trying to use an object that depends on an object you dispose.

    What you are trying to do is open a file, read some information about its internals from it (not the actual internals themselves), then close the file, and then try to use that information to actually read from that file. It's impossible; the file is already closed. Just like you can't return a disposable object from inside its own using block without ending up with a disposed and thus unusable object, you obviously also can't return something that depends on the disposable object.

    So, basically, the whole thought process behind your getMyArchiveStream function is flawed. You should not have that function at all. You just need to make the other function like this:

    public void UseMyArchiveStream()
    {
        using(var archive = ZipFile.OpenRead(_filepath))
        {
            var entry = archive.GetEntry("test.path");
            using(var myStream = entry.Open())
            {
                //Do stuff
            }
        }
    }
    

    One alternative is indeed to leave archive open... but as mjwills commented, there is another way to do what you want, and that's to give UseMyArchiveStream an Action<> or Func<> as argument. This literally means that the "Do stuff" comment in the code above is replaced by a call to whatever function you give as argument:

    public void UseMyArchiveStream(String zipPath, String entryName, Action<Stream, String> doStuff)
    {
        using (var archive = ZipFile.OpenRead(zipPath))
        {
            var entry = archive.GetEntry(entryName);
            using (var myStream = entry.Open())
            {
                doStuff(myStream, entry.FullName);
            }
        }
    }
    

    Demonstrated with a function void SaveStreamToFile(Stream file, String filename):

    UseMyArchiveStream(_filepath, "test.path", (str, nm) => SaveStreamToFile(str, nm));
    

    With Func<>, you can make an overload that gives a return value too. The last argument inside the <> is always the return type. But you can easily use generics to make that depend on the calling input:

    public T UseMyArchiveStream<T>(String zipPath, String entryName, Func<Stream, String, T> doStuff)
    {
        using (var archive = ZipFile.OpenRead(zipPath))
        {
            var entry = archive.GetEntry(entryName);
            using (var myStream = entry.Open())
            {
                return doStuff(myStream, entry.FullName);
            }
        }
    }
    

    You can call this the same way, only with a function that returns a value rather than void. Demonstrated with Boolean DostuffWithFile(Stream file, String entryName):

    Boolean ok = UseMyArchiveStream(_filepath, "test.path", (str, nm) => DostuffWithFile(str, nm));
    

    Note that the function you call does not have to match the exact signature of the argument. You can perfectly substitute missing arguments with local data in this way of calling.

    Demonstrated with Boolean DostuffWithFile(Stream file, String entryName, Boolean someOption, String outputFolder):

    Boolean ok = UseMyArchiveStream(_filepath, "test.path", (str, nm) => DostuffWithFile(str, nm, true, _savePath));
    

    This will work as long as the input that needs to be provided by UseMyArchiveStream is just the part before the =>. Of course, you can juggle the arguments however you want; you can even just give the function the whole ZipArchiveEntry object, and perhaps even the source ZipFile, so you can do whatever you want with it.

    The only disadvantage to this approach is that you can't actually name the components of the Action<> or Func<>, so, in this case, you can't know from just the function signature of UseMyArchiveStream whether that String argument given to Func<Stream, String, T> will receive the entry.Name or the entry.FullName. The same applies to giving multiple arguments of the same type; if you have five boolean options in that Func<> you may have a hard time remembering exactly which is which without having to look into the code each time. So be sure to accurately document that in the function comments, to avoid confusion later.