Implement directory-capable InMemoryFileProvider
This commit is contained in:
parent
c439787ab5
commit
0ed4a4eba5
16
Blazor.sln
16
Blazor.sln
|
|
@ -44,7 +44,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StandaloneApp", "samples\St
|
|||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MonoSanity", "MonoSanity", "{2A076721-6081-4517-8329-B9E5110D6DAC}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Blazor.Server.Test", "Microsoft.Blazor.Server.Test\Microsoft.Blazor.Server.Test.csproj", "{71AF445F-0903-4743-B047-44B3B2C19DC9}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Blazor.Server.Test", "Microsoft.Blazor.Server.Test\Microsoft.Blazor.Server.Test.csproj", "{71AF445F-0903-4743-B047-44B3B2C19DC9}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Blazor.Common", "src\Microsoft.Blazor.Common\Microsoft.Blazor.Common.csproj", "{21EF76A4-63CC-455D-907C-F86C9E442CEC}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Blazor.Common.Test", "test\Microsoft.Blazor.Common.Test\Microsoft.Blazor.Common.Test.csproj", "{7F0BF3EA-6985-49F6-8070-0BBA41448BB0}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
|
|
@ -108,6 +112,14 @@ Global
|
|||
{71AF445F-0903-4743-B047-44B3B2C19DC9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{71AF445F-0903-4743-B047-44B3B2C19DC9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{71AF445F-0903-4743-B047-44B3B2C19DC9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{21EF76A4-63CC-455D-907C-F86C9E442CEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{21EF76A4-63CC-455D-907C-F86C9E442CEC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{21EF76A4-63CC-455D-907C-F86C9E442CEC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{21EF76A4-63CC-455D-907C-F86C9E442CEC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7F0BF3EA-6985-49F6-8070-0BBA41448BB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7F0BF3EA-6985-49F6-8070-0BBA41448BB0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7F0BF3EA-6985-49F6-8070-0BBA41448BB0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7F0BF3EA-6985-49F6-8070-0BBA41448BB0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -130,6 +142,8 @@ Global
|
|||
{B241434A-1642-44CC-AE9A-2012B5C5BD02} = {F5FDD4E5-6A52-4A86-BE5E-5E42CB1DC8DA}
|
||||
{2A076721-6081-4517-8329-B9E5110D6DAC} = {F5FDD4E5-6A52-4A86-BE5E-5E42CB1DC8DA}
|
||||
{71AF445F-0903-4743-B047-44B3B2C19DC9} = {ADA3AE29-F6DE-49F6-8C7C-B321508CAE8E}
|
||||
{21EF76A4-63CC-455D-907C-F86C9E442CEC} = {B867E038-B3CE-43E3-9292-61568C46CDEB}
|
||||
{7F0BF3EA-6985-49F6-8070-0BBA41448BB0} = {ADA3AE29-F6DE-49F6-8C7C-B321508CAE8E}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {504DA352-6788-4DC0-8705-82167E72A4D3}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
// 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.Linq;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections;
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Blazor.Internal.Common.FileProviders
|
||||
{
|
||||
internal class InMemoryDirectoryContents : IDirectoryContents
|
||||
{
|
||||
private readonly Dictionary<string, object> _names; // Just to ensure there are no duplicate names
|
||||
private readonly List<IFileInfo> _contents;
|
||||
|
||||
public InMemoryDirectoryContents(IEnumerable<IFileInfo> contents)
|
||||
{
|
||||
if (contents != null)
|
||||
{
|
||||
_contents = contents.ToList();
|
||||
_names = _contents.ToDictionary(item => item.Name, item => (object)null);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Exists => _contents != null;
|
||||
|
||||
public IEnumerator<IFileInfo> GetEnumerator() => GetEnumeratorIfExists();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumeratorIfExists();
|
||||
|
||||
internal void AddItem(IFileInfo item)
|
||||
{
|
||||
_names.Add(item.Name, null); // Asserts uniqueness
|
||||
_contents.Add(item);
|
||||
}
|
||||
internal bool ContainsName(string name)
|
||||
=> _names.ContainsKey(name);
|
||||
|
||||
private IEnumerator<IFileInfo> GetEnumeratorIfExists() => Exists
|
||||
? _contents.GetEnumerator()
|
||||
: throw new InvalidOperationException("The directory does not exist");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
// 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 Microsoft.Extensions.FileProviders;
|
||||
using System.IO;
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Blazor.Internal.Common.FileProviders
|
||||
{
|
||||
internal class InMemoryFileInfo : IFileInfo
|
||||
{
|
||||
private readonly bool _exists;
|
||||
private readonly bool _isDirectory;
|
||||
private readonly byte[] _fileData;
|
||||
private readonly string _name;
|
||||
private readonly string _physicalPath;
|
||||
private readonly DateTimeOffset _lastModified;
|
||||
|
||||
public static IFileInfo ForExistingDirectory(string path)
|
||||
=> new InMemoryFileInfo(path, isDir: true, exists: true);
|
||||
|
||||
public static IFileInfo ForExistingFile(string path, Stream dataStream, DateTimeOffset lastModified)
|
||||
=> new InMemoryFileInfo(path, isDir: false, exists: true, dataStream: dataStream, lastModified: lastModified);
|
||||
|
||||
public static IFileInfo ForNonExistingFile(string path)
|
||||
=> new InMemoryFileInfo(path, isDir: false, exists: false);
|
||||
|
||||
private InMemoryFileInfo(string physicalPath, bool isDir, bool exists, Stream dataStream = null, DateTimeOffset lastModified = default(DateTimeOffset))
|
||||
{
|
||||
_exists = exists;
|
||||
_isDirectory = isDir;
|
||||
_name = Path.GetFileName(physicalPath);
|
||||
_physicalPath = physicalPath ?? throw new ArgumentNullException(nameof(physicalPath));
|
||||
_lastModified = lastModified;
|
||||
|
||||
if (_exists)
|
||||
{
|
||||
if (!_physicalPath.StartsWith(InMemoryFileProvider.DirectorySeparatorChar))
|
||||
{
|
||||
throw new ArgumentException($"Must start with '{InMemoryFileProvider.DirectorySeparatorChar}'",
|
||||
nameof(physicalPath));
|
||||
}
|
||||
|
||||
if (_physicalPath.EndsWith(InMemoryFileProvider.DirectorySeparatorChar))
|
||||
{
|
||||
throw new ArgumentException($"Must not end with '{InMemoryFileProvider.DirectorySeparatorChar}'",
|
||||
nameof(physicalPath));
|
||||
}
|
||||
}
|
||||
|
||||
if (dataStream != null)
|
||||
{
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
dataStream.CopyTo(ms);
|
||||
_fileData = ms.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool Exists => _exists;
|
||||
|
||||
public long Length => Exists && !IsDirectory
|
||||
? _fileData.Length
|
||||
: throw new InvalidOperationException(IsDirectory
|
||||
? "The item is a directory."
|
||||
: "The item does not exist.");
|
||||
|
||||
public string PhysicalPath => _physicalPath;
|
||||
|
||||
public string Name => _name;
|
||||
|
||||
public DateTimeOffset LastModified => _lastModified;
|
||||
|
||||
public bool IsDirectory => _isDirectory;
|
||||
|
||||
public Stream CreateReadStream() => Exists && !IsDirectory
|
||||
? new MemoryStream(_fileData)
|
||||
: throw new InvalidOperationException(IsDirectory
|
||||
? "The item is a directory."
|
||||
: "The item does not exist.");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
// 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 Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.Blazor.Internal.Common.FileProviders
|
||||
{
|
||||
public class InMemoryFileProvider : IFileProvider
|
||||
{
|
||||
// Since the IFileProvider APIs don't include any way of asking for parent or
|
||||
// child directories, it's efficient to store everything by full path
|
||||
private readonly IDictionary<string, IFileInfo> _filesByFullPath;
|
||||
private readonly IDictionary<string, InMemoryDirectoryContents> _directoriesByFullPath;
|
||||
|
||||
// It's convenient to use forward slash, because it matches URL conventions
|
||||
public const char DirectorySeparatorChar = '/';
|
||||
|
||||
public InMemoryFileProvider(IEnumerable<(string, Stream)> contents)
|
||||
{
|
||||
_filesByFullPath = contents
|
||||
.Select(path => InMemoryFileInfo.ForExistingFile(path.Item1, path.Item2, DateTime.Now))
|
||||
.ToDictionary(
|
||||
fileInfo => fileInfo.PhysicalPath,
|
||||
fileInfo => (IFileInfo)fileInfo);
|
||||
|
||||
_directoriesByFullPath = _filesByFullPath.Values
|
||||
.GroupBy(file => GetDirectoryName(file.PhysicalPath))
|
||||
.ToDictionary(
|
||||
group => group.Key,
|
||||
group => new InMemoryDirectoryContents(group));
|
||||
|
||||
foreach (var dirToInsert in _directoriesByFullPath.Keys.ToList())
|
||||
{
|
||||
AddSubdirectoryEntry(dirToInsert);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddSubdirectoryEntry(string dirPath)
|
||||
{
|
||||
// If this is the root directory, there's no parent
|
||||
if (dirPath.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure parent directory exists
|
||||
var parentDirPath = GetDirectoryName(dirPath);
|
||||
if (!_directoriesByFullPath.ContainsKey(parentDirPath))
|
||||
{
|
||||
_directoriesByFullPath.Add(
|
||||
parentDirPath,
|
||||
new InMemoryDirectoryContents(Enumerable.Empty<IFileInfo>()));
|
||||
}
|
||||
|
||||
var parentDir = _directoriesByFullPath[parentDirPath];
|
||||
if (!parentDir.ContainsName(Path.GetFileName(dirPath)))
|
||||
{
|
||||
parentDir.AddItem(
|
||||
InMemoryFileInfo.ForExistingDirectory(dirPath));
|
||||
}
|
||||
|
||||
// Doing this recursively creates all ancestor directories
|
||||
AddSubdirectoryEntry(parentDirPath);
|
||||
}
|
||||
|
||||
private static string GetDirectoryName(string fullPath)
|
||||
=> fullPath.Substring(0, fullPath.LastIndexOf(DirectorySeparatorChar));
|
||||
|
||||
public IDirectoryContents GetDirectoryContents(string subpath)
|
||||
=> _directoriesByFullPath.TryGetValue(StripTrailingSeparator(subpath), out var result)
|
||||
? result
|
||||
: new InMemoryDirectoryContents(null);
|
||||
|
||||
public IFileInfo GetFileInfo(string subpath)
|
||||
=> _filesByFullPath.TryGetValue(subpath, out var fileInfo)
|
||||
? fileInfo
|
||||
: InMemoryFileInfo.ForNonExistingFile(subpath);
|
||||
|
||||
public IChangeToken Watch(string filter)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
private string StripTrailingSeparator(string path) => path.EndsWith(DirectorySeparatorChar)
|
||||
? path.Substring(0, path.Length - 1)
|
||||
: path;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Abstractions" Version="2.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
// 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 Microsoft.Blazor.Internal.Common.FileProviders;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Blazor.Common.Test
|
||||
{
|
||||
public class InMemoryFileProviderTest
|
||||
{
|
||||
private (string, Stream) TestItem(string name) => TestItem(name, Array.Empty<byte>());
|
||||
private (string, Stream) TestItem(string name, byte[] data) => (name, new MemoryStream(data));
|
||||
|
||||
[Fact]
|
||||
public void RequiresPathsToStartWithSlash()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
{
|
||||
new InMemoryFileProvider(new[] { TestItem("item") });
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequiresPathsNotToEndWithSlash()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
{
|
||||
new InMemoryFileProvider(new[] { TestItem("/item/") });
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReturnsFileInfosForExistingFiles()
|
||||
{
|
||||
// Arrange
|
||||
var instance = new InMemoryFileProvider(new[]
|
||||
{
|
||||
TestItem("/dirA/item", Encoding.UTF8.GetBytes("Contents of /dirA/item")),
|
||||
TestItem("/dirB/item", Encoding.UTF8.GetBytes("Contents of /dirB/item"))
|
||||
});
|
||||
|
||||
// Act
|
||||
var dirAItem = instance.GetFileInfo("/dirA/item");
|
||||
var dirBItem = instance.GetFileInfo("/dirB/item");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(
|
||||
"Contents of /dirA/item",
|
||||
new StreamReader(dirAItem.CreateReadStream()).ReadToEnd());
|
||||
Assert.Equal(
|
||||
"Contents of /dirB/item",
|
||||
new StreamReader(dirBItem.CreateReadStream()).ReadToEnd());
|
||||
Assert.True(dirAItem.Exists);
|
||||
Assert.False(dirAItem.IsDirectory);
|
||||
Assert.True((DateTime.Now - dirAItem.LastModified).TotalDays < 1); // Exact behaviour doesn't need to be defined (at least not yet) but it should be a valid date
|
||||
Assert.Equal(22, dirAItem.Length);
|
||||
Assert.Equal("item", dirAItem.Name);
|
||||
Assert.Equal("/dirA/item", dirAItem.PhysicalPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReturnsFileInfosForNonExistingFiles()
|
||||
{
|
||||
// Arrange
|
||||
var instance = new InMemoryFileProvider(new[] { TestItem("/dirA/item") });
|
||||
|
||||
// Act
|
||||
var mismatchedCaseItem = instance.GetFileInfo("/dira/item");
|
||||
var dirBItem = instance.GetFileInfo("/dirB/item");
|
||||
|
||||
// Assert
|
||||
Assert.False(mismatchedCaseItem.Exists);
|
||||
Assert.False(dirBItem.Exists);
|
||||
Assert.False(dirBItem.IsDirectory);
|
||||
Assert.Equal("item", dirBItem.Name);
|
||||
Assert.Equal("/dirB/item", dirBItem.PhysicalPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReturnsDirectoryContentsForExistingDirectory()
|
||||
{
|
||||
// Arrange
|
||||
var instance = new InMemoryFileProvider(new[]
|
||||
{
|
||||
TestItem("/dir/subdir/item1"),
|
||||
TestItem("/dir/subdir/item2"),
|
||||
TestItem("/dir/otherdir/item3")
|
||||
});
|
||||
|
||||
// Act
|
||||
var contents = instance.GetDirectoryContents("/dir/subdir");
|
||||
|
||||
// Assert
|
||||
Assert.True(contents.Exists);
|
||||
Assert.Collection(contents,
|
||||
item => Assert.Equal("/dir/subdir/item1", item.PhysicalPath),
|
||||
item => Assert.Equal("/dir/subdir/item2", item.PhysicalPath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyStringAndSlashAreBothInterpretedAsRootDir()
|
||||
{
|
||||
// Technically this test duplicates checking the behavior asserted
|
||||
// previously (i.e., trailing slashes are ignored), but it's worth
|
||||
// checking that nothing bad happens when the path is an empty string
|
||||
|
||||
// Arrange
|
||||
var instance = new InMemoryFileProvider(new[] { TestItem("/item") });
|
||||
|
||||
// Act/Assert
|
||||
Assert.Collection(instance.GetDirectoryContents(string.Empty),
|
||||
item => Assert.Equal("/item", item.PhysicalPath));
|
||||
Assert.Collection(instance.GetDirectoryContents("/"),
|
||||
item => Assert.Equal("/item", item.PhysicalPath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReturnsDirectoryContentsIfGivenPathEndsWithSlash()
|
||||
{
|
||||
// Arrange
|
||||
var instance = new InMemoryFileProvider(new[] { TestItem("/dir/subdir/item1") });
|
||||
|
||||
// Act
|
||||
var contents = instance.GetDirectoryContents("/dir/subdir/");
|
||||
|
||||
// Assert
|
||||
Assert.True(contents.Exists);
|
||||
Assert.Collection(contents,
|
||||
item => Assert.Equal("/dir/subdir/item1", item.PhysicalPath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReturnsDirectoryContentsForNonExistingDirectory()
|
||||
{
|
||||
// Arrange
|
||||
var instance = new InMemoryFileProvider(new[] { TestItem("/dir/subdir/item1") });
|
||||
|
||||
// Act
|
||||
var contents = instance.GetDirectoryContents("/dir/otherdir");
|
||||
|
||||
// Assert
|
||||
Assert.False(contents.Exists);
|
||||
Assert.Throws<InvalidOperationException>(() => contents.GetEnumerator());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IncludesSubdirectoriesInDirectoryContents()
|
||||
{
|
||||
// Arrange
|
||||
var instance = new InMemoryFileProvider(new[] {
|
||||
TestItem("/dir/sub/item1"),
|
||||
TestItem("/dir/sub/item2"),
|
||||
TestItem("/dir/sub2/item"),
|
||||
TestItem("/unrelated/item")
|
||||
});
|
||||
|
||||
// Act
|
||||
var contents = instance.GetDirectoryContents("/dir");
|
||||
|
||||
// Assert
|
||||
Assert.True(contents.Exists);
|
||||
Assert.Collection(contents,
|
||||
item =>
|
||||
{
|
||||
// For this example, verify all properties. Don't need to do this for all examples.
|
||||
Assert.True(item.Exists);
|
||||
Assert.True(item.IsDirectory);
|
||||
Assert.Equal(default(DateTimeOffset), item.LastModified);
|
||||
Assert.Throws<InvalidOperationException>(() => item.Length);
|
||||
Assert.Throws<InvalidOperationException>(() => item.CreateReadStream());
|
||||
Assert.Equal("sub", item.Name);
|
||||
Assert.Equal("/dir/sub", item.PhysicalPath);
|
||||
},
|
||||
item =>
|
||||
{
|
||||
Assert.Equal("/dir/sub2", item.PhysicalPath);
|
||||
Assert.True(item.IsDirectory);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasAllAncestorDirectoriesForDirectory()
|
||||
{
|
||||
// Arrange
|
||||
var instance = new InMemoryFileProvider(new[] { TestItem("/a/b/c") });
|
||||
|
||||
// Act/Assert
|
||||
Assert.Collection(instance.GetDirectoryContents("/"),
|
||||
item =>
|
||||
{
|
||||
Assert.Equal("/a", item.PhysicalPath);
|
||||
Assert.True(item.IsDirectory);
|
||||
});
|
||||
Assert.Collection(instance.GetDirectoryContents("/a"),
|
||||
item =>
|
||||
{
|
||||
Assert.Equal("/a/b", item.PhysicalPath);
|
||||
Assert.True(item.IsDirectory);
|
||||
});
|
||||
Assert.Collection(instance.GetDirectoryContents("/a/b"),
|
||||
item =>
|
||||
{
|
||||
Assert.Equal("/a/b/c", item.PhysicalPath);
|
||||
Assert.False(item.IsDirectory);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
|
||||
<PackageReference Include="xunit" Version="2.3.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Blazor.Common\Microsoft.Blazor.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Loading…
Reference in New Issue