Skip to content
This repository was archived by the owner on Nov 6, 2018. It is now read-only.

Implement a CombinedFileProvider #142

Closed
wants to merge 10 commits into from
Closed
33 changes: 31 additions & 2 deletions FileSystem.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.22823.1
VisualStudioVersion = 14.0.23107.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A1477614-E825-4204-A684-385004B63AEB}"
EndProject
Expand All @@ -28,6 +27,10 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.FileProvid
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.FileProviders.Sources", "src\Microsoft.AspNet.FileProviders.Sources\Microsoft.AspNet.FileProviders.Sources.xproj", "{92C2C85C-D1A5-44BD-BE23-238E08471B4D}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.FileProviders.Combined", "src\Microsoft.AspNet.FileProviders.Combined\Microsoft.AspNet.FileProviders.Combined.xproj", "{CAAF52EF-F91B-474D-AC56-FE9D96FF8254}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.FileProviders.Combined.Tests", "test\Microsoft.AspNet.FileProviders.Combined.Tests\Microsoft.AspNet.FileProviders.Combined.Tests.xproj", "{C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -132,6 +135,30 @@ Global
{92C2C85C-D1A5-44BD-BE23-238E08471B4D}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{92C2C85C-D1A5-44BD-BE23-238E08471B4D}.Release|x86.ActiveCfg = Release|Any CPU
{92C2C85C-D1A5-44BD-BE23-238E08471B4D}.Release|x86.Build.0 = Release|Any CPU
{CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Debug|x86.ActiveCfg = Debug|Any CPU
{CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Debug|x86.Build.0 = Debug|Any CPU
{CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Release|Any CPU.Build.0 = Release|Any CPU
{CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Release|x86.ActiveCfg = Release|Any CPU
{CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Release|x86.Build.0 = Release|Any CPU
{C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Debug|x86.ActiveCfg = Debug|Any CPU
{C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Debug|x86.Build.0 = Debug|Any CPU
{C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Release|Any CPU.Build.0 = Release|Any CPU
{C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Release|x86.ActiveCfg = Release|Any CPU
{C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -145,5 +172,7 @@ Global
{6B6BA57A-B32D-430A-AF39-09CAA85308C2} = {E399495E-82B8-4C06-8779-C1D02BEF4495}
{66FE5FDF-BBF9-4573-A7B7-53551731C0F9} = {E399495E-82B8-4C06-8779-C1D02BEF4495}
{92C2C85C-D1A5-44BD-BE23-238E08471B4D} = {A1477614-E825-4204-A684-385004B63AEB}
{CAAF52EF-F91B-474D-AC56-FE9D96FF8254} = {A1477614-E825-4204-A684-385004B63AEB}
{C2EA9BD0-C986-4B60-9E45-5EA51E1EA494} = {E399495E-82B8-4C06-8779-C1D02BEF4495}
EndGlobalSection
EndGlobal
168 changes: 168 additions & 0 deletions src/Microsoft.AspNet.FileProviders.Combined/CombinedFileProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Primitives;

namespace Microsoft.AspNet.FileProviders
{
/// <summary>
/// Looks up files using embedded resources in the specified assembly.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary is incorrrect.

/// This file provider is case sensitive.
/// </summary>
public class CombinedFileProvider : IFileProvider
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be named CompositeFileProvider?

{
private readonly IEnumerable<IFileProvider> _fileProviders;

/// <summary>
/// Initializes a new instance of the <see cref="CombinedFileProvider" /> class using a list of file provider.
/// </summary>
/// <param name="fileProviders"></param>
public CombinedFileProvider(params IFileProvider[] fileProviders)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd avoid params. Just take an IEnumerable<IFileProvider>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A params overload definitely makes sense, because that's how people will use this combined thingy:

options.FileProvider = new CombinedFileSystemProvider(
    new EmbeddedFileProvider(...),
    new EmbeddedFileProvider(...));

Since the file provider is a hot path, using IEnumerable<IFileProvider> is not really appropriate. Not to mention that, semantically, it's not immediately clear that the order matters when using IEnumerable. IMHO, an array or a list is much more appropriate in this case. That said, nothing prevents you from adding an IEnumerable<> overload that calls ToArray() or ToList() to avoid allocating enumerators in GetFileInfo/GetDirectoryContents.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A params overload definitely makes sense, because that's how people will use this combined thingy:

Not really. Just make an array if you need that:

new CombinedFileSystemProvider(new[] {
    new EmbeddedFileProvider(...),
    new EmbeddedFileProvider(...) });

I'd keep the params array overload for a static factory method instead of baking it into the ctor.

Converting it to an array or list is more appropriate than enforcing an array be passed in.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @davidfowl, is it a general purpose to avoid params, or just in this situation ?

{
_fileProviders = fileProviders;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fileProviders needs a null check.

}

/// <summary>
/// Locates a file at the given path.
/// </summary>
/// <param name="subpath">The path that identifies the file. </param>
/// <returns>The file information. Caller must check Exists property. This will be the first existing <see cref="IFileInfo"/> returned by the provided <see cref="IFileProvider"/> or a not found <see cref="IFileInfo"/> if no existing files is found.</returns>
public IFileInfo GetFileInfo(string subpath)
{
foreach (var fileProvider in _fileProviders)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: you could change _fileProviders's type to IFileProvider[] to avoid allocating an enumerator here (you don't have to explicitly use a for loop, since the compiler is supposed to optimize that for you).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand your point: IFileProvider[] implements IEnumerable<IFileProvider> so when an enumerator will be allocated (which will not be in your case) ?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a variable is declared as an array, the compiler automatically replaces the foreach loop by a for loop and directly accesses the items via array[index] without having to instantiate a new enumerator. The compiler can't apply the same optimization when the type is IEnumerable<>.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I'm happy to learn something today!

{
var fileInfo = fileProvider.GetFileInfo(subpath);
if (fileInfo != null && fileInfo.Exists)
{
return fileInfo;
}
}
return new NotFoundFileInfo(subpath);
}

/// <summary>
/// Enumerate a directory at the given path, if any.
/// </summary>
/// <param name="subpath">The path that identifies the directory</param>
/// <returns>Contents of the directory. Caller must check Exists property. The content is a merge of the contents of the provided <see cref="IFileProvider"/>. When there is multiple <see cref="IFileInfo"/> with the same Name property, only the first one is included on the results.</returns>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this line seems a bit long 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's the problem when VS is configured to do "Word wrap". I will add some new lines 😄

public IDirectoryContents GetDirectoryContents(string subpath)
{
// gets the content of all directories and merged them
var existingDirectoryContentsForAllProviders = _fileProviders.Select(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: for maximal performance, you could avoid using LINQ here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I will change it for a old-school traditionnal loop 😢

fileProvider => fileProvider.GetDirectoryContents(subpath))
.Where(directoryContents => directoryContents != null && directoryContents.Exists)
.ToList();

// There is no existing directory contents
if (existingDirectoryContentsForAllProviders.Count == 0)
{
return new NotFoundDirectoryContents();
}
var combinedDirectoryContents = new CombinedDirectoryContents(existingDirectoryContentsForAllProviders);
return combinedDirectoryContents;
}

/// <summary>
/// Creates a <see cref="IChangeToken"/> for the specified <paramref name="filter"/>.
/// </summary>
/// <remarks></remarks>
/// <param name="pattern">Filter string used to determine what files or folders to monitor. Example: **/*.cs, *.*, subFolder/**/*.cshtml.</param>
/// <returns>An <see cref="IChangeToken"/> that is notified when a file matching <paramref name="filter"/> is added, modified or deleted. The change token will be notified when one of the change token returned by the provided <see cref="IFileProvider"/> will be notified.</returns>
public IChangeToken Watch(string pattern)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No documentation for this method? I think especially for this one it is interesting.

{
// Watch all file providers
var activeChangeTokens = _fileProviders
.Select(fileProvider => fileProvider.Watch(pattern))
.Where(changeToken => changeToken != null && changeToken.ActiveChangeCallbacks).ToList();

// There is no change token with active change callbacks
if (activeChangeTokens.Count == 0)
{
return NoopChangeToken.Singleton;
}
var combinedFileChangeToken = new CombinedFileChangeToken(activeChangeTokens);
return combinedFileChangeToken;
}

private class CombinedDirectoryContents : IDirectoryContents

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm perhaps alone with this opinion and I've seen this in the other FileSystem projects already, but can't we stop with these nested classes and just make it an internal class within an own file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK for me.
I could submit a new PR in order to do it after this one to un-nest these classes in all projects.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could start with this - just make these types public.

{
private readonly Dictionary<string, IFileInfo> _files = new Dictionary<string, IFileInfo>();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious: why using a dictionary here? AFAICT, the key doesn't seem to be used anywhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dictionary is used to filter duplicate IFileInfo (see line 102). I can rewrite it to use a Hash<string> but I will have the hashset and the list of corresponding IFileInfo. I think it was simpler to do it this way but I can change.


public CombinedDirectoryContents(IEnumerable<IEnumerable<IFileInfo>> listOfFiles)
{
foreach (var files in listOfFiles)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code path will immediately enumerate all the files exposed by the different providers, which may take some time. You should maybe consider deferring this operation to the last moment (in GetEnumerator) and using an iterator.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have made the assumption that the directory contents will be pre-calculated (based on the two other providers), but you're right. I can move it to a Lazy<IEnumerable<IFileInfo>> (to avoiding dealing with the thread-safe and evaluating only once issues).

{
Exists = true;
foreach (var file in files)
{
if (!_files.ContainsKey(file.Name))
{
_files.Add(file.Name, file);
}
}
}
}

public IEnumerator<IFileInfo> GetEnumerator()
{
return _files.Values.GetEnumerator();
}

IEnumerator IEnumerable.GetEnumerator()
{
return _files.Values.GetEnumerator();
}

public bool Exists { get; }
}

private class CombinedFileChangeToken : IChangeToken
{
private readonly IEnumerable<IChangeToken> _changeTokens;

public CombinedFileChangeToken(IEnumerable<IChangeToken> changeTokens)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same remark as above: since this parameter is always used with a list, you could replace IEnumerable with a list or an array.

{
_changeTokens = changeTokens ?? new List<IChangeToken>();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: looks like changeTokens cannot be null when used by Watch. I'd personally removed the null coalescing operator 😄

}

public IDisposable RegisterChangeCallback(Action<object> callback, object state)
{
return new Disposables(_changeTokens.Where(changeToken => changeToken.ActiveChangeCallbacks).Select(changeToken => changeToken.RegisterChangeCallback(callback, state)).ToList());
}

public bool HasChanged
{
get { return _changeTokens.Any(_ => _.HasChanged); }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use underscores for things you're using:
_changeTokens.Any(token => token.HasChanged). Same comment for ActiveChangeCallbacks

}

public bool ActiveChangeCallbacks
{
get { return _changeTokens.Any(_ => _.ActiveChangeCallbacks); }
}

private class Disposables : IDisposable
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CombinedDisposable to maintain the naming pattern.

{
private readonly IEnumerable<IDisposable> _disposables;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: same remark 😄


public Disposables(IEnumerable<IDisposable> disposables)
{
_disposables = disposables;
}
public void Dispose()
{
if (_disposables != null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this guaranteed to be non null?

{
foreach (var disposable in _disposables)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same remark 👍

{
disposable.Dispose();
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="__ToolsVersion__" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>caaf52ef-f91b-474d-ac56-fe9d96ff8254</ProjectGuid>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'" Label="Configuration">
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x86'" Label="Configuration">
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Reflection;
using System.Resources;

[assembly: AssemblyMetadata("Serviceable", "True")]
[assembly: NeutralResourcesLanguage("en-us")]
29 changes: 29 additions & 0 deletions src/Microsoft.AspNet.FileProviders.Combined/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"version": "1.0.0-*",
"compilationOptions": {
"warningsAsErrors": true,
"keyFile": "../../tools/Key.snk"
},
"description": "Implementation of ASP.NET 5 file provider abstractions to combine file providers.",
"repository": {
"type": "git",
"url": "git://github.com/aspnet/filesystem"
},
"dependencies": {
"Microsoft.AspNet.FileProviders.Sources": {
"version": "1.0.0-*",
"type": "build"
},
"Microsoft.AspNet.FileProviders.Abstractions": "1.0.0-*"
},
"frameworks": {
"net451": {},
"dotnet5.4": {
"dependencies": {
"System.Collections": "4.0.11-*",
"System.Reflection": "4.0.10-*",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might not need these.

"System.Runtime.Extensions": "4.0.11-*"
}
}
}
}
Loading