More work and tests

This commit is contained in:
Pavel Krymets 2016-03-02 15:31:13 -08:00
parent 9bd9ca1512
commit dcaea8c7ca
13 changed files with 389 additions and 69 deletions

View file

@ -115,8 +115,8 @@ namespace Microsoft.Extensions.DependencyModel
export.Library.Identity.Name, export.Library.Identity.Name,
export.Library.Identity.Version.ToString(), export.Library.Identity.Version.ToString(),
export.Library.Hash, export.Library.Hash,
assemblies, assemblies.Select(RuntimeAssembly.Create).ToArray(),
new RuntimeTarget[0], new RuntimeTarget[0],
libraryDependencies.ToArray(), libraryDependencies.ToArray(),
serviceable serviceable
); );

View file

@ -1,6 +1,8 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved. // Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information. // Licensed under the MIT license. See LICENSE file in the project root for full license information.
using Microsoft.Extensions.Internal;
namespace Microsoft.Extensions.DependencyModel namespace Microsoft.Extensions.DependencyModel
{ {
public struct Dependency public struct Dependency
@ -13,5 +15,24 @@ namespace Microsoft.Extensions.DependencyModel
public string Name { get; } public string Name { get; }
public string Version { get; } public string Version { get; }
public bool Equals(Dependency other)
{
return string.Equals(Name, other.Name) && string.Equals(Version, other.Version);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
return obj is Dependency && Equals((Dependency) obj);
}
public override int GetHashCode()
{
var combiner = HashCodeCombiner.Start();
combiner.Add(Name);
combiner.Add(Version);
return combiner.CombinedHash;
}
} }
} }

View file

@ -46,8 +46,8 @@ namespace Microsoft.Extensions.DependencyModel
packageName: identity.Item2, packageName: identity.Item2,
version: identity.Item3, version: identity.Item3,
hash: identity.Item4, hash: identity.Item4,
assemblies: packageGroup.Select(l => l.AssetPath).ToArray(), assemblies: packageGroup.Select(l => RuntimeAssembly.Create(l.AssetPath)).ToArray(),
subTargets: new RuntimeTarget[0], subTargets: new RuntimeTarget[0],
dependencies: new Dependency[] { }, dependencies: new Dependency[] { },
serviceable: false serviceable: false
)); ));

View file

@ -50,15 +50,19 @@ namespace Microsoft.Extensions.DependencyModel
{ {
throw new FormatException($"Target with name {runtimeTargetInfo.Name} not found"); throw new FormatException($"Target with name {runtimeTargetInfo.Name} not found");
} }
runtime = runtimeTargetInfo.Name.Substring(target.Length + 1);
var seperatorIndex = runtimeTargetInfo.Name.IndexOf(DependencyContextStrings.VersionSeperator);
if (seperatorIndex > -1 && seperatorIndex < runtimeTargetInfo.Name.Length)
{
runtime = runtimeTargetInfo.Name.Substring(seperatorIndex + 1);
}
} }
else else
{ {
runtimeTarget = compileTarget; runtimeTarget = compileTarget;
} }
} }
return new DependencyContext( return new DependencyContext(
target, target,
runtime, runtime,
@ -153,21 +157,65 @@ namespace Microsoft.Extensions.DependencyModel
var libraryObject = (JObject) property.Value; var libraryObject = (JObject) property.Value;
var dependencies = ReadDependencies(libraryObject); var dependencies = ReadDependencies(libraryObject);
var assemblies = ReadAssemblies(libraryObject, runtime);
if (runtime) if (runtime)
{ {
return new RuntimeLibrary(stub.Type, name, version, stub.Hash, assemblies, new RuntimeTarget[0], dependencies, stub.Serviceable); var runtimeTargets = new List<RuntimeTarget>();
var runtimeTargetsObject = (JObject)libraryObject[DependencyContextStrings.RuntimeTargetsPropertyName];
var entries = ReadRuntimeTargetEntries(runtimeTargetsObject).ToArray();
foreach (var ridGroup in entries.GroupBy(e => e.Rid))
{
var runtimeAssets = entries.Where(e => e.Type == DependencyContextStrings.RuntimeAssetType)
.Select(e => RuntimeAssembly.Create(e.Path))
.ToArray();
var nativeAssets = entries.Where(e => e.Type == DependencyContextStrings.NativeAssetType)
.Select(e => e.Path)
.ToArray();
runtimeTargets.Add(new RuntimeTarget(
ridGroup.Key,
runtimeAssets,
nativeAssets
));
}
var assemblies = ReadAssemblies(libraryObject, DependencyContextStrings.RuntimeAssembliesKey)
.Select(RuntimeAssembly.Create)
.ToArray();
return new RuntimeLibrary(stub.Type, name, version, stub.Hash, assemblies, runtimeTargets.ToArray(), dependencies, stub.Serviceable);
} }
else else
{ {
var assemblies = ReadAssemblies(libraryObject, DependencyContextStrings.CompileTimeAssembliesKey);
return new CompilationLibrary(stub.Type, name, version, stub.Hash, assemblies, dependencies, stub.Serviceable); return new CompilationLibrary(stub.Type, name, version, stub.Hash, assemblies, dependencies, stub.Serviceable);
} }
} }
private static string[] ReadAssemblies(JObject libraryObject, bool runtime) private static IEnumerable<RuntimeTargetEntryStub> ReadRuntimeTargetEntries(JObject runtimeTargetObject)
{ {
var assembliesObject = (JObject) libraryObject[runtime ? DependencyContextStrings.RuntimeAssembliesKey : DependencyContextStrings.CompileTimeAssembliesKey]; if (runtimeTargetObject == null)
{
yield break;
}
foreach (var libraryProperty in runtimeTargetObject)
{
var libraryObject = (JObject)libraryProperty.Value;
yield return new RuntimeTargetEntryStub()
{
Path = libraryProperty.Key,
Rid = libraryObject[DependencyContextStrings.RidPropertyName].Value<string>(),
Type = libraryObject[DependencyContextStrings.AssetTypePropertyName].Value<string>()
};
}
}
private static string[] ReadAssemblies(JObject libraryObject, string name)
{
var assembliesObject = (JObject) libraryObject[name];
if (assembliesObject == null) if (assembliesObject == null)
{ {
@ -211,6 +259,13 @@ namespace Microsoft.Extensions.DependencyModel
return libraries; return libraries;
} }
private struct RuntimeTargetEntryStub
{
public string Type;
public string Path;
public string Rid;
}
private struct RuntimeTargetInfo private struct RuntimeTargetInfo
{ {
public string Name; public string Name;

View file

@ -56,5 +56,15 @@ namespace Microsoft.Extensions.DependencyModel
internal const string RuntimeTargetNamePropertyName = "name"; internal const string RuntimeTargetNamePropertyName = "name";
internal const string RuntimesPropertyName = "runtimes"; internal const string RuntimesPropertyName = "runtimes";
internal const string RuntimeTargetsPropertyName = "runtimeTargets";
internal const string RidPropertyName = "rid";
internal const string AssetTypePropertyName = "assetType";
internal const string RuntimeAssetType = "runtime";
internal const string NativeAssetType = "native";
} }
} }

View file

@ -37,9 +37,12 @@ namespace Microsoft.Extensions.DependencyModel
private JObject WriteRuntimeTargetInfo(DependencyContext context) private JObject WriteRuntimeTargetInfo(DependencyContext context)
{ {
var target = context.IsPortable?
context.Target :
context.Target + DependencyContextStrings.VersionSeperator + context.Runtime;
return new JObject( return new JObject(
new JProperty(DependencyContextStrings.RuntimeTargetNamePropertyName, new JProperty(DependencyContextStrings.RuntimeTargetNamePropertyName, target),
context.Target + DependencyContextStrings.VersionSeperator + context.Runtime),
new JProperty(DependencyContextStrings.PortablePropertyName, context.IsPortable) new JProperty(DependencyContextStrings.PortablePropertyName, context.IsPortable)
); );
} }
@ -107,11 +110,22 @@ namespace Microsoft.Extensions.DependencyModel
private JObject WriteTargets(DependencyContext context) private JObject WriteTargets(DependencyContext context)
{ {
return new JObject( if (context.IsPortable)
new JProperty(context.Target, WriteTarget(context.CompileLibraries)), {
new JProperty(context.Target + DependencyContextStrings.VersionSeperator + context.Runtime, return new JObject(
WriteTarget(context.RuntimeLibraries)) new JProperty(context.Target, WriteTarget(context.CompileLibraries)),
); new JProperty(context.Target + DependencyContextStrings.VersionSeperator + context.Runtime,
WriteTarget(context.RuntimeLibraries))
);
}
else
{
return new JObject(
new JProperty(context.Target, WriteTarget(context.CompileLibraries)),
new JProperty(context.Target + DependencyContextStrings.VersionSeperator + context.Runtime,
WriteTarget(context.RuntimeLibraries))
);
}
} }
private JObject WriteTarget(IReadOnlyList<Library> libraries) private JObject WriteTarget(IReadOnlyList<Library> libraries)
@ -147,6 +161,38 @@ namespace Microsoft.Extensions.DependencyModel
} }
return new JObject(
new JProperty(DependencyContextStrings.DependenciesPropertyName, WriteDependencies(library.Dependencies)),
new JProperty(propertyName,
WriteAssemblies(assemblies))
);
}
private JObject WritePortableTargetLibrary(RuntimeLibrary compilationLibrary, CompilationLibrary runtimeLibrary)
{
var libraryObject = new JObject();
string propertyName;
string[] assemblies;
if (runtimeLibrary != null)
{
propertyName = DependencyContextStrings.RuntimeAssembliesKey;
assemblies = runtimeLibrary.Assemblies.Select(assembly => assembly.Path).ToArray();
}
RuntimeAssembly[] compilationAssemblies;
if (compilationLibrary != null)
{
propertyName = DependencyContextStrings.CompileTimeAssembliesKey;
compilationAssemblies = compilationLibrary.Assemblies.ToArray();
}
else
{
throw new NotSupportedException();
}
}
return new JObject( return new JObject(
new JProperty(DependencyContextStrings.DependenciesPropertyName, WriteDependencies(library.Dependencies)), new JProperty(DependencyContextStrings.DependenciesPropertyName, WriteDependencies(library.Dependencies)),
new JProperty(propertyName, new JProperty(propertyName,

View file

@ -10,11 +10,6 @@ namespace Microsoft.Extensions.DependencyModel
{ {
private readonly string _assemblyName; private readonly string _assemblyName;
public RuntimeAssembly(string path)
: this(System.IO.Path.GetFileNameWithoutExtension(path), path)
{
}
public RuntimeAssembly(string assemblyName, string path) public RuntimeAssembly(string assemblyName, string path)
{ {
_assemblyName = assemblyName; _assemblyName = assemblyName;
@ -24,5 +19,10 @@ namespace Microsoft.Extensions.DependencyModel
public AssemblyName Name => new AssemblyName(_assemblyName); public AssemblyName Name => new AssemblyName(_assemblyName);
public string Path { get; } public string Path { get; }
public static RuntimeAssembly Create(string path)
{
return new RuntimeAssembly(System.IO.Path.GetFileNameWithoutExtension(path), path);
}
} }
} }

View file

@ -13,13 +13,13 @@ namespace Microsoft.Extensions.DependencyModel
string packageName, string packageName,
string version, string version,
string hash, string hash,
string[] assemblies, RuntimeAssembly[] assemblies,
RuntimeTarget[] subTargets, RuntimeTarget[] subTargets,
Dependency[] dependencies, Dependency[] dependencies,
bool serviceable) bool serviceable)
: base(libraryType, packageName, version, hash, dependencies, serviceable) : base(libraryType, packageName, version, hash, dependencies, serviceable)
{ {
Assemblies = assemblies.Select(path => new RuntimeAssembly(path)).ToArray(); Assemblies = assemblies;
SubTargets = subTargets; SubTargets = subTargets;
} }
@ -27,20 +27,4 @@ namespace Microsoft.Extensions.DependencyModel
public IReadOnlyList<RuntimeTarget> SubTargets { get; } public IReadOnlyList<RuntimeTarget> SubTargets { get; }
} }
public class RuntimeTarget
{
public RuntimeTarget(string runtime, IReadOnlyList<RuntimeAssembly> assemblies, IReadOnlyList<string> nativeLibraries)
{
Runtime = runtime;
Assemblies = assemblies;
NativeLibraries = nativeLibraries;
}
public string Runtime { get; }
public IReadOnlyList<RuntimeAssembly> Assemblies { get; }
public IReadOnlyList<string> NativeLibraries { get; }
}
} }

View file

@ -0,0 +1,20 @@
using System.Collections.Generic;
namespace Microsoft.Extensions.DependencyModel
{
public class RuntimeTarget
{
public RuntimeTarget(string runtime, IReadOnlyList<RuntimeAssembly> assemblies, IReadOnlyList<string> nativeLibraries)
{
Runtime = runtime;
Assemblies = assemblies;
NativeLibraries = nativeLibraries;
}
public string Runtime { get; }
public IReadOnlyList<RuntimeAssembly> Assemblies { get; }
public IReadOnlyList<string> NativeLibraries { get; }
}
}

View file

@ -10,6 +10,10 @@
"keyFile": "../../tools/Key.snk" "keyFile": "../../tools/Key.snk"
}, },
"dependencies": { "dependencies": {
"Microsoft.Extensions.HashCodeCombiner.Sources": {
"type": "build",
"version": "1.0.0-rc2-16054"
},
"Microsoft.DotNet.InternalAbstractions": { "Microsoft.DotNet.InternalAbstractions": {
"target": "project", "target": "project",
"version": "1.0.0-*" "version": "1.0.0-*"

View file

@ -109,9 +109,7 @@ namespace Microsoft.Extensions.DependencyModel.Tests
}, },
""libraries"":{ ""libraries"":{
""MyApp/1.0.1"": { ""MyApp/1.0.1"": {
""type"": ""project"", ""type"": ""project""
""serviceable"": true,
""sha512"": ""HASH-MyApp""
}, },
""System.Banana/1.0.0"": { ""System.Banana/1.0.0"": {
""type"": ""package"", ""type"": ""package"",
@ -124,11 +122,7 @@ namespace Microsoft.Extensions.DependencyModel.Tests
var project = context.CompileLibraries.Should().Contain(l => l.PackageName == "MyApp").Subject; var project = context.CompileLibraries.Should().Contain(l => l.PackageName == "MyApp").Subject;
project.Version.Should().Be("1.0.1"); project.Version.Should().Be("1.0.1");
project.Assemblies.Should().BeEquivalentTo("MyApp.dll"); project.Assemblies.Should().BeEquivalentTo("MyApp.dll");
project.Hash.Should().Be("HASH-MyApp");
project.LibraryType.Should().Be("project"); project.LibraryType.Should().Be("project");
project.Serviceable.Should().Be(true);
project.Hash.Should().BeEquivalentTo("HASH-MyApp");
var package = context.CompileLibraries.Should().Contain(l => l.PackageName == "System.Banana").Subject; var package = context.CompileLibraries.Should().Contain(l => l.PackageName == "System.Banana").Subject;
package.Version.Should().Be("1.0.0"); package.Version.Should().Be("1.0.0");
@ -137,5 +131,71 @@ namespace Microsoft.Extensions.DependencyModel.Tests
package.LibraryType.Should().Be("package"); package.LibraryType.Should().Be("package");
package.Serviceable.Should().Be(false); package.Serviceable.Should().Be(false);
} }
[Fact]
public void ReadsRuntimeLibrariesWithSubtargetsFromMainTargetForPortable()
{
var context = Read(
@"{
""runtimeTarget"": {
""portable"": true,
""name"": "".NETStandardApp,Version=v1.5"",
},
""targets"": {
"".NETStandardApp,Version=v1.5"": {
""MyApp/1.0.1"": {
""dependencies"": {
""AspNet.Mvc"": ""1.0.0""
},
""runtime"": {
""MyApp.dll"": { }
}
},
""System.Banana/1.0.0"": {
""dependencies"": {
""System.Foo"": ""1.0.0""
},
""runtime"": {
""lib/dotnet5.4/System.Banana.dll"": { }
},
""runtimeTargets"": {
""lib/win7/System.Banana.dll"": { ""assetType"": ""runtime"", ""rid"": ""win7-x64""},
""lib/win7/Banana.dll"": { ""assetType"": ""native"", ""rid"": ""win7-x64""}
}
}
}
},
""libraries"":{
""MyApp/1.0.1"": {
""type"": ""project"",
},
""System.Banana/1.0.0"": {
""type"": ""package"",
""serviceable"": false,
""sha512"": ""HASH-System.Banana""
},
}
}");
context.CompileLibraries.Should().HaveCount(2);
var project = context.RuntimeLibraries.Should().Contain(l => l.PackageName == "MyApp").Subject;
project.Version.Should().Be("1.0.1");
project.Assemblies.Should().Contain(a => a.Path == "MyApp.dll");
project.LibraryType.Should().Be("project");
var package = context.RuntimeLibraries.Should().Contain(l => l.PackageName == "System.Banana").Subject;
package.Version.Should().Be("1.0.0");
package.Hash.Should().Be("HASH-System.Banana");
package.LibraryType.Should().Be("package");
package.Serviceable.Should().Be(false);
package.Assemblies.Should().Contain(a => a.Path == "lib/dotnet5.4/System.Banana.dll");
var target = package.SubTargets.Should().Contain(t => t.Runtime == "win7-x64").Subject;
target.Assemblies.Should().Contain(a => a.Path == "lib/win7/System.Banana.dll");
target.NativeLibraries.Should().Contain("lib/win7/Banana.dll");
}
} }
} }

View file

@ -30,17 +30,33 @@ namespace Microsoft.Extensions.DependencyModel.Tests
} }
} }
public DependencyContext Create(
string target = null,
string runtime = null,
bool? isPortable = null,
CompilationOptions compilationOptions = null,
CompilationLibrary[] compileLibraries = null,
RuntimeLibrary[] runtimeLibraries = null,
IReadOnlyList<KeyValuePair<string, string[]>> runtimeGraph = null)
{
return new DependencyContext(
target,
runtime,
isPortable ?? false,
compilationOptions ?? CompilationOptions.Default,
compileLibraries ?? new CompilationLibrary[0],
runtimeLibraries ?? new RuntimeLibrary[0],
runtimeGraph ?? new KeyValuePair<string, string[]>[0]
);
}
[Fact] [Fact]
public void SavesRuntimeGraph() public void SavesRuntimeGraph()
{ {
var result = Save(new DependencyContext( var result = Save(Create(
"Target", "Target",
"Target/runtime", "Target/runtime",
false, runtimeGraph: new[]
CompilationOptions.Default,
new CompilationLibrary[0],
new RuntimeLibrary[0],
new[]
{ {
new KeyValuePair<string, string[]>("win7-x64", new [] { "win6", "win5"}), new KeyValuePair<string, string[]>("win7-x64", new [] { "win6", "win5"}),
new KeyValuePair<string, string[]>("win8-x64", new [] { "win7-x64"}), new KeyValuePair<string, string[]>("win8-x64", new [] { "win7-x64"}),
@ -64,14 +80,10 @@ namespace Microsoft.Extensions.DependencyModel.Tests
[Fact] [Fact]
public void WritesRuntimeTargetPropertyIfNotPortable() public void WritesRuntimeTargetPropertyIfNotPortable()
{ {
var result = Save(new DependencyContext( var result = Save(Create(
"Target", "Target",
"runtime", "runtime",
false, false)
CompilationOptions.Default,
new CompilationLibrary[0],
new RuntimeLibrary[0],
new KeyValuePair<string, string[]>[0])
); );
var runtimeTarget = result.Should().HaveProperty("runtimeTarget") var runtimeTarget = result.Should().HaveProperty("runtimeTarget")
@ -83,20 +95,118 @@ namespace Microsoft.Extensions.DependencyModel.Tests
runtimeTarget.Should().HaveProperty("portable") runtimeTarget.Should().HaveProperty("portable")
.Subject.Value<bool>().Should().Be(false); .Subject.Value<bool>().Should().Be(false);
} }
[Fact] [Fact]
public void DoesNotWritesRuntimeTargetPropertyIfPortable() public void WritesMainTargetNameToRuntimeTargetIfPortable()
{ {
var result = Save(new DependencyContext( var result = Save(Create(
"Target", "Target",
"runtime", "runtime",
false, true)
CompilationOptions.Default,
new CompilationLibrary[0],
new RuntimeLibrary[0],
new KeyValuePair<string, string[]>[0])
); );
var runtimeTarget = result.Should().HaveProperty("runtimeTarget")
.Subject.Should().BeOfType<JObject>().Subject;
result.Should().NotHaveProperty("runtimeTarget"); runtimeTarget.Should().HaveProperty("name")
.Subject.Value<string>().Should().Be("Target");
runtimeTarget.Should().HaveProperty("portable")
.Subject.Value<bool>().Should().Be(true);
}
[Fact]
public void WritesCompilationLibraries()
{
var result = Save(Create(
"Target",
"runtime",
true,
compileLibraries: new[]
{
new CompilationLibrary(
"package",
"PackageName",
"1.2.3",
"HASH",
new [] {"Banana.dll"},
new [] {
new Dependency("Fruits.Abstract.dll","2.0.0")
},
true
)
}));
// targets
var targets = result.Should().HavePropertyAsObject("targets").Subject;
var target = targets.Should().HavePropertyAsObject("Target").Subject;
var library = target.Should().HavePropertyAsObject("PackageName/1.2.3").Subject;
var dependencies = library.Should().HavePropertyAsObject("dependencies").Subject;
dependencies.Should().HavePropertyValue("Fruits.Abstract.dll", "2.0.0");
library.Should().HavePropertyAsObject("compile")
.Subject.Should().HaveProperty("Banana.dll");
//libraries
var libraries = result.Should().HavePropertyAsObject("libraries").Subject;
library = libraries.Should().HavePropertyAsObject("PackageName/1.2.3").Subject;
library.Should().HavePropertyValue("sha512", "HASH");
library.Should().HavePropertyValue("type", "package");
library.Should().HavePropertyValue("serviceable", true);
}
[Fact]
public void WritesRuntimeLibrariesToRuntimeTarget()
{
var result = Save(Create(
"Target",
"runtime",
true,
runtimeLibraries: new[]
{
new RuntimeLibrary(
"package",
"PackageName",
"1.2.3",
"HASH",
new [] { RuntimeAssembly.Create("Banana.dll")},
new []
{
new RuntimeTarget("win7-x64",
new [] { RuntimeAssembly.Create("Banana.Win7-x64.dll") },
new [] { "Banana.Win7-x64.so" }
)
},
new [] {
new Dependency("Fruits.Abstract.dll","2.0.0")
},
true
),
}));
// targets
var targets = result.Should().HavePropertyAsObject("targets").Subject;
var target = targets.Should().HavePropertyAsObject("Target").Subject;
var library = target.Should().HavePropertyAsObject("PackageName/1.2.3").Subject;
var dependencies = library.Should().HavePropertyAsObject("dependencies").Subject;
dependencies.Should().HavePropertyValue("Fruits.Abstract.dll", "2.0.0");
library.Should().HavePropertyAsObject("runtime")
.Subject.Should().HaveProperty("Banana.dll");
var runtimeTargets = library.Should().HavePropertyAsObject("target").Subject;
var runtimeAssembly = runtimeTargets.Should().HavePropertyAsObject("Banana.Win7-x64.dll").Subject;
runtimeAssembly.Should().HavePropertyValue("rid", "win7-x64");
runtimeAssembly.Should().HavePropertyValue("assetType", "runtime");
var nativeLibrary = runtimeTargets.Should().HavePropertyAsObject("Banana.Win7-x64.so").Subject;
nativeLibrary.Should().HavePropertyValue("rid", "win7-x64");
nativeLibrary.Should().HavePropertyValue("assetType", "native");
//libraries
var libraries = result.Should().HavePropertyAsObject("libraries").Subject;
library = libraries.Should().HavePropertyAsObject("PackageName/1.2.3").Subject;
library.Should().HavePropertyValue("sha512", "HASH");
library.Should().HavePropertyValue("type", "package");
library.Should().HavePropertyValue("serviceable", true);
} }
} }
} }

View file

@ -45,5 +45,15 @@ namespace Microsoft.Extensions.DependencyModel.Tests
return new AndConstraint<JsonAssetions>(this); return new AndConstraint<JsonAssetions>(this);
} }
public AndWhichConstraint<JsonAssetions, JObject> HavePropertyAsObject(string expected)
{
return HaveProperty(expected).Subject.Should().BeOfType<JObject>();
}
public AndConstraint<ObjectAssertions> HavePropertyValue<T>(string expected, T value)
{
return HaveProperty(expected).Subject.Value<T>().Should().Be(value);
}
} }
} }