Amends to key value service/repository to better match existing repository patterns.

This commit is contained in:
Andy Butland
2020-02-17 22:12:42 +01:00
parent f92b43260a
commit 0bba475533
14 changed files with 272 additions and 150 deletions

View File

@@ -28,12 +28,5 @@ namespace Umbraco.Core.Models
get => _value;
set => SetPropertyValueAndDetectChanges(value, ref _value, nameof(Value));
}
/// <inheritdoc />
public DateTime UpdateDate
{
get => _updateDate;
set => SetPropertyValueAndDetectChanges(value, ref _updateDate, nameof(UpdateDate));
}
}
}

View File

@@ -1,13 +1,8 @@
namespace Umbraco.Infrastructure.Persistence.Repositories
using Umbraco.Core.Models;
namespace Umbraco.Core.Persistence.Repositories
{
public interface IKeyValueRepository
public interface IKeyValueRepository : IReadRepository<string, IKeyValue>, IWriteRepository<IKeyValue>
{
void Initialize();
string GetValue(string key);
void SetValue(string key, string value);
bool TrySetValue(string key, string originalValue, string newValue);
}
}

View File

@@ -32,12 +32,13 @@ namespace Umbraco.Core.Services
private readonly Lazy<IExternalLoginService> _externalLoginService;
private readonly Lazy<IRedirectUrlService> _redirectUrlService;
private readonly Lazy<IConsentService> _consentService;
private readonly Lazy<IKeyValueService> _keyValueService;
private readonly Lazy<IContentTypeBaseServiceProvider> _contentTypeBaseServiceProvider;
/// <summary>
/// Initializes a new instance of the <see cref="ServiceContext"/> class with lazy services.
/// </summary>
public ServiceContext(Lazy<IPublicAccessService> publicAccessService, Lazy<IDomainService> domainService, Lazy<IAuditService> auditService, Lazy<ILocalizedTextService> localizedTextService, Lazy<ITagService> tagService, Lazy<IContentService> contentService, Lazy<IUserService> userService, Lazy<IMemberService> memberService, Lazy<IMediaService> mediaService, Lazy<IContentTypeService> contentTypeService, Lazy<IMediaTypeService> mediaTypeService, Lazy<IDataTypeService> dataTypeService, Lazy<IFileService> fileService, Lazy<ILocalizationService> localizationService, Lazy<IPackagingService> packagingService, Lazy<IServerRegistrationService> serverRegistrationService, Lazy<IEntityService> entityService, Lazy<IRelationService> relationService, Lazy<IMacroService> macroService, Lazy<IMemberTypeService> memberTypeService, Lazy<IMemberGroupService> memberGroupService, Lazy<INotificationService> notificationService, Lazy<IExternalLoginService> externalLoginService, Lazy<IRedirectUrlService> redirectUrlService, Lazy<IConsentService> consentService, Lazy<IContentTypeBaseServiceProvider> contentTypeBaseServiceProvider)
public ServiceContext(Lazy<IPublicAccessService> publicAccessService, Lazy<IDomainService> domainService, Lazy<IAuditService> auditService, Lazy<ILocalizedTextService> localizedTextService, Lazy<ITagService> tagService, Lazy<IContentService> contentService, Lazy<IUserService> userService, Lazy<IMemberService> memberService, Lazy<IMediaService> mediaService, Lazy<IContentTypeService> contentTypeService, Lazy<IMediaTypeService> mediaTypeService, Lazy<IDataTypeService> dataTypeService, Lazy<IFileService> fileService, Lazy<ILocalizationService> localizationService, Lazy<IPackagingService> packagingService, Lazy<IServerRegistrationService> serverRegistrationService, Lazy<IEntityService> entityService, Lazy<IRelationService> relationService, Lazy<IMacroService> macroService, Lazy<IMemberTypeService> memberTypeService, Lazy<IMemberGroupService> memberGroupService, Lazy<INotificationService> notificationService, Lazy<IExternalLoginService> externalLoginService, Lazy<IRedirectUrlService> redirectUrlService, Lazy<IConsentService> consentService, Lazy<IKeyValueService> keyValueService, Lazy<IContentTypeBaseServiceProvider> contentTypeBaseServiceProvider)
{
_publicAccessService = publicAccessService;
_domainService = domainService;
@@ -64,6 +65,7 @@ namespace Umbraco.Core.Services
_externalLoginService = externalLoginService;
_redirectUrlService = redirectUrlService;
_consentService = consentService;
_keyValueService = keyValueService;
_contentTypeBaseServiceProvider = contentTypeBaseServiceProvider;
}
@@ -99,6 +101,7 @@ namespace Umbraco.Core.Services
IServerRegistrationService serverRegistrationService = null,
IRedirectUrlService redirectUrlService = null,
IConsentService consentService = null,
IKeyValueService keyValueService = null,
IContentTypeBaseServiceProvider contentTypeBaseServiceProvider = null)
{
Lazy<T> Lazy<T>(T service) => service == null ? null : new Lazy<T>(() => service);
@@ -129,6 +132,7 @@ namespace Umbraco.Core.Services
Lazy(externalLoginService),
Lazy(redirectUrlService),
Lazy(consentService),
Lazy(keyValueService),
Lazy(contentTypeBaseServiceProvider)
);
}
@@ -258,6 +262,11 @@ namespace Umbraco.Core.Services
/// </summary>
public IConsentService ConsentService => _consentService.Value;
/// <summary>
/// Gets the KeyValueService.
/// </summary>
public IKeyValueService KeyValueService => _keyValueService.Value;
/// <summary>
/// Gets the ContentTypeServiceBaseFactory.
/// </summary>

View File

@@ -47,6 +47,7 @@ namespace Umbraco.Core.Composing.CompositionExtensions
composition.RegisterUnique<IScriptRepository, ScriptRepository>();
composition.RegisterUnique<IStylesheetRepository, StylesheetRepository>();
composition.RegisterUnique<IContentTypeCommonRepository, ContentTypeCommonRepository>();
composition.RegisterUnique<IKeyValueRepository, KeyValueRepository>();
return composition;
}

View File

@@ -0,0 +1,9 @@
using Umbraco.Core.Persistence;
namespace Umbraco.Infrastructure.Migrations.Custom
{
public interface IKeyValueServiceInitialization
{
void PerformInitialMigration(IUmbracoDatabase database);
}
}

View File

@@ -0,0 +1,80 @@
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Migrations;
using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.Dtos;
namespace Umbraco.Infrastructure.Migrations.Custom
{
public class KeyValueServiceInitialization : IKeyValueServiceInitialization
{
private readonly ILogger _logger;
public KeyValueServiceInitialization(ILogger logger)
{
_logger = logger;
}
public void PerformInitialMigration(IUmbracoDatabase database)
{
var context = new MigrationContext(database, _logger);
var initMigration = new InitialMigration(context);
initMigration.Migrate();
}
/// <summary>
/// A custom migration that executes standalone during the Initialize phase of the KeyValueService.
/// </summary>
internal class InitialMigration : MigrationBase
{
public InitialMigration(IMigrationContext context)
: base(context)
{ }
public override void Migrate()
{
// as long as we are still running 7 this migration will be invoked,
// but due to multiple restarts during upgrades, maybe the table
// exists already
if (TableExists(Constants.DatabaseSchema.Tables.KeyValue))
return;
Logger.Info<KeyValueServiceInitialization>("Creating KeyValue structure.");
// the locks table was initially created with an identity (auto-increment) primary key,
// but we don't want this, especially as we are about to insert a new row into the table,
// so here we drop that identity
DropLockTableIdentity();
// insert the lock object for key/value
Insert.IntoTable(Constants.DatabaseSchema.Tables.Lock).Row(new { id = Constants.Locks.KeyValues, name = "KeyValues", value = 1 }).Do();
// create the key-value table
Create.Table<KeyValueDto>().Do();
}
private void DropLockTableIdentity()
{
// one cannot simply drop an identity, that requires a bit of work
// create a temp. id column and copy values
Alter.Table(Constants.DatabaseSchema.Tables.Lock).AddColumn("nid").AsInt32().Nullable().Do();
Execute.Sql("update umbracoLock set nid = id").Do();
// drop the id column entirely (cannot just drop identity)
Delete.PrimaryKey("PK_umbracoLock").FromTable(Constants.DatabaseSchema.Tables.Lock).Do();
Delete.Column("id").FromTable(Constants.DatabaseSchema.Tables.Lock).Do();
// recreate the id column without identity and copy values
Alter.Table(Constants.DatabaseSchema.Tables.Lock).AddColumn("id").AsInt32().Nullable().Do();
Execute.Sql("update umbracoLock set id = nid").Do();
// drop the temp. id column
Delete.Column("nid").FromTable(Constants.DatabaseSchema.Tables.Lock).Do();
// complete the primary key
Alter.Table(Constants.DatabaseSchema.Tables.Lock).AlterColumn("id").AsInt32().NotNullable().PrimaryKey("PK_umbracoLock").Do();
}
}
}
}

View File

@@ -4,12 +4,11 @@ using System.Linq;
using NPoco;
using Umbraco.Core.Cache;
using Umbraco.Core.Logging;
using Umbraco.Core.Migrations;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.Dtos;
using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Scoping;
using Umbraco.Infrastructure.Persistence.Repositories;
namespace Umbraco.Core.Persistence.Repositories.Implement
{
@@ -19,56 +18,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
: base(scopeAccessor, AppCaches.NoCache, logger)
{ }
public void Initialize()
{
var context = new MigrationContext(Database, Logger);
var initMigration = new InitialMigration(context);
initMigration.Migrate();
}
public string GetValue(string key)
{
return GetDtoByKey(key)?.Value;
}
public void SetValue(string key, string value)
{
var dto = GetDtoByKey(key);
if (dto == null)
{
dto = new KeyValueDto
{
Key = key,
Value = value,
UpdateDate = DateTime.Now
};
Database.Insert(dto);
}
else
{
UpdateDtoValue(dto, value);
}
}
public bool TrySetValue(string key, string originalValue, string newValue)
{
var dto = GetDtoByKey(key);
if (dto == null || dto.Value != originalValue)
return false;
UpdateDtoValue(dto, newValue);
return true;
}
private void UpdateDtoValue(KeyValueDto dto, string value)
{
dto.Value = value;
dto.UpdateDate = DateTime.Now;
Database.Update(dto);
}
/// <summary>
/// Gets a value directly from the database, no scope, nothing.
/// </summary>
@@ -84,6 +33,18 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
return database.FirstOrDefault<KeyValueDto>(sql)?.Value;
}
#region Overrides of IReadWriteQueryRepository<string, IKeyValue>
public override void Save(IKeyValue entity)
{
if (Get(entity.Identifier) == null)
PersistNewItem(entity);
else
PersistUpdatedItem(entity);
}
#endregion
#region Overrides of NPocoRepositoryBase<string, IKeyValue>
protected override Guid NodeObjectTypeId => throw new NotImplementedException();
@@ -114,16 +75,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
protected override IKeyValue PerformGet(string id)
{
var dto = GetDtoByKey(id);
var sql = GetBaseQuery(false).Where<KeyValueDto>(x => x.Key == id);
var dto = Database.Fetch<KeyValueDto>(sql).FirstOrDefault();
return dto == null ? null : Map(dto);
}
private KeyValueDto GetDtoByKey(string key)
{
var sql = GetBaseQuery(false).Where<KeyValueDto>(x => x.Key == key);
return Database.Fetch<KeyValueDto>(sql).FirstOrDefault();
}
protected override IEnumerable<IKeyValue> PerformGetAll(params string[] ids)
{
var sql = GetBaseQuery(false).WhereIn<KeyValueDto>(x => x.Key, ids);
@@ -170,64 +126,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
Value = dto.Value,
UpdateDate = dto.UpdateDate,
};
}
}
#endregion
/// <summary>
/// A custom migration that executes standalone during the Initialize phase of the KeyValueService.
/// </summary>
internal class InitialMigration : MigrationBase
{
public InitialMigration(IMigrationContext context)
: base(context)
{ }
public override void Migrate()
{
// as long as we are still running 7 this migration will be invoked,
// but due to multiple restarts during upgrades, maybe the table
// exists already
if (TableExists(Constants.DatabaseSchema.Tables.KeyValue))
return;
Logger.Info<KeyValueRepository>("Creating KeyValue structure.");
// the locks table was initially created with an identity (auto-increment) primary key,
// but we don't want this, especially as we are about to insert a new row into the table,
// so here we drop that identity
DropLockTableIdentity();
// insert the lock object for key/value
Insert.IntoTable(Constants.DatabaseSchema.Tables.Lock).Row(new { id = Constants.Locks.KeyValues, name = "KeyValues", value = 1 }).Do();
// create the key-value table
Create.Table<KeyValueDto>().Do();
}
private void DropLockTableIdentity()
{
// one cannot simply drop an identity, that requires a bit of work
// create a temp. id column and copy values
Alter.Table(Constants.DatabaseSchema.Tables.Lock).AddColumn("nid").AsInt32().Nullable().Do();
Execute.Sql("update umbracoLock set nid = id").Do();
// drop the id column entirely (cannot just drop identity)
Delete.PrimaryKey("PK_umbracoLock").FromTable(Constants.DatabaseSchema.Tables.Lock).Do();
Delete.Column("id").FromTable(Constants.DatabaseSchema.Tables.Lock).Do();
// recreate the id column without identity and copy values
Alter.Table(Constants.DatabaseSchema.Tables.Lock).AddColumn("id").AsInt32().Nullable().Do();
Execute.Sql("update umbracoLock set id = nid").Do();
// drop the temp. id column
Delete.Column("nid").FromTable(Constants.DatabaseSchema.Tables.Lock).Do();
// complete the primary key
Alter.Table(Constants.DatabaseSchema.Tables.Lock).AlterColumn("id").AsInt32().NotNullable().PrimaryKey("PK_umbracoLock").Do();
}
}
#endregion
}
}

View File

@@ -132,7 +132,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
/// </summary>
/// <remarks>This method is backed by an <see cref="IAppPolicyCache"/> cache</remarks>
/// <param name="entity"></param>
public void Save(TEntity entity)
public virtual void Save(TEntity entity)
{
if (entity.HasIdentity == false)
CachePolicy.Create(entity, PersistNewItem);

View File

@@ -21,6 +21,7 @@ using Umbraco.Core.Services;
using Umbraco.Core.Strings;
using Umbraco.Core.Sync;
using IntegerValidator = Umbraco.Core.PropertyEditors.Validators.IntegerValidator;
using Umbraco.Infrastructure.Migrations.Custom;
namespace Umbraco.Core.Runtime
{
@@ -128,6 +129,7 @@ namespace Umbraco.Core.Runtime
.Append<DefaultUrlSegmentProvider>();
composition.RegisterUnique<IMigrationBuilder>(factory => new MigrationBuilder(factory));
composition.RegisterUnique<IKeyValueServiceInitialization, KeyValueServiceInitialization>();
// by default, register a noop factory
composition.RegisterUnique<IPublishedModelFactory, NoopPublishedModelFactory>();

View File

@@ -1,8 +1,10 @@
using System;
using Umbraco.Core.Configuration;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence.Repositories;
using Umbraco.Core.Scoping;
using Umbraco.Infrastructure.Persistence.Repositories;
using Umbraco.Infrastructure.Migrations.Custom;
namespace Umbraco.Core.Services.Implement
{
@@ -11,14 +13,16 @@ namespace Umbraco.Core.Services.Implement
private readonly object _initialock = new object();
private readonly IScopeProvider _scopeProvider;
private readonly IKeyValueRepository _repository;
private readonly IKeyValueServiceInitialization _initialization;
private readonly ILogger _logger;
private readonly IUmbracoVersion _umbracoVersion;
private bool _initialized;
public KeyValueService(IScopeProvider scopeProvider, IKeyValueRepository repository, ILogger logger, IUmbracoVersion umbracoVersion)
public KeyValueService(IScopeProvider scopeProvider, IKeyValueRepository repository, IKeyValueServiceInitialization initialization, ILogger logger, IUmbracoVersion umbracoVersion)
{
_scopeProvider = scopeProvider;
_repository = repository;
_initialization = initialization;
_logger = logger;
_umbracoVersion = umbracoVersion;
}
@@ -51,7 +55,7 @@ namespace Umbraco.Core.Services.Implement
using (var scope = _scopeProvider.CreateScope())
{
_repository.Initialize();
_initialization.PerformInitialMigration(scope.Database);
scope.Complete();
}
@@ -67,7 +71,7 @@ namespace Umbraco.Core.Services.Implement
using (var scope = _scopeProvider.CreateScope())
{
return _repository.GetValue(key);
return _repository.Get(key)?.Value;
}
}
@@ -80,7 +84,23 @@ namespace Umbraco.Core.Services.Implement
{
scope.WriteLock(Constants.Locks.KeyValues);
_repository.SetValue(key, value);
var keyValue = _repository.Get(key);
if (keyValue == null)
{
keyValue = new KeyValue
{
Identifier = key,
Value = value,
UpdateDate = DateTime.Now,
};
}
else
{
keyValue.Value = value;
keyValue.UpdateDate = DateTime.Now;
}
_repository.Save(keyValue);
scope.Complete();
}
@@ -102,11 +122,16 @@ namespace Umbraco.Core.Services.Implement
{
scope.WriteLock(Constants.Locks.KeyValues);
if (!_repository.TrySetValue(key, originalValue, newValue))
var keyValue = _repository.Get(key);
if (keyValue == null || keyValue.Value != originalValue)
{
return false;
}
keyValue.Value = newValue;
keyValue.UpdateDate = DateTime.Now;
_repository.Save(keyValue);
scope.Complete();
}

View File

@@ -1,7 +1,9 @@
using NUnit.Framework;
using System;
using NUnit.Framework;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence.Repositories;
using Umbraco.Core.Persistence.Repositories.Implement;
using Umbraco.Core.Scoping;
using Umbraco.Infrastructure.Persistence.Repositories;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.Testing;
@@ -19,8 +21,14 @@ namespace Umbraco.Tests.Persistence.Repositories
// Insert new key/value
using (var scope = provider.CreateScope())
{
var keyValue = new KeyValue
{
Identifier = "foo",
Value = "bar",
UpdateDate = DateTime.Now,
};
var repo = CreateRepository(provider);
repo.SetValue("foo", "bar");
repo.Save(keyValue);
scope.Complete();
}
@@ -28,17 +36,20 @@ namespace Umbraco.Tests.Persistence.Repositories
using (var scope = provider.CreateScope())
{
var repo = CreateRepository(provider);
var value = repo.GetValue("foo");
var keyValue = repo.Get("foo");
scope.Complete();
Assert.AreEqual("bar", value);
Assert.AreEqual("bar", keyValue.Value);
}
// Update new key/value
// Update value
using (var scope = provider.CreateScope())
{
var repo = CreateRepository(provider);
repo.SetValue("foo", "buzz");
var keyValue = repo.Get("foo");
keyValue.Value = "buzz";
keyValue.UpdateDate = DateTime.Now;
repo.Save(keyValue);
scope.Complete();
}
@@ -46,10 +57,10 @@ namespace Umbraco.Tests.Persistence.Repositories
using (var scope = provider.CreateScope())
{
var repo = CreateRepository(provider);
var value = repo.GetValue("foo");
var keyValue = repo.Get("foo");
scope.Complete();
Assert.AreEqual("buzz", value);
Assert.AreEqual("buzz", keyValue.Value);
}
}

View File

@@ -0,0 +1,93 @@
using System.Threading;
using NUnit.Framework;
using NUnit.Framework.Internal;
using Umbraco.Core.Services;
using Umbraco.Tests.Testing;
namespace Umbraco.Tests.Services
{
/// <summary>
/// Tests covering methods in the KeyValueService class.
/// This is more of an integration test as it involves multiple layers
/// as well as configuration.
/// </summary>
[TestFixture]
[Apartment(ApartmentState.STA)]
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
public class KeyValueServiceTests : TestWithSomeContentBase
{
[Test]
public void GetValue_ForMissingKey_ReturnsNull()
{
// Arrange
var keyValueService = ServiceContext.KeyValueService;
// Act
var value = keyValueService.GetValue("foo");
// Assert
Assert.IsNull(value);
}
[Test]
public void GetValue_ForExistingKey_ReturnsValue()
{
// Arrange
var keyValueService = ServiceContext.KeyValueService;
keyValueService.SetValue("foo", "bar");
// Act
var value = keyValueService.GetValue("foo");
// Assert
Assert.AreEqual("bar", value);
}
[Test]
public void SetValue_ForExistingKey_SavesValue()
{
// Arrange
var keyValueService = ServiceContext.KeyValueService;
keyValueService.SetValue("foo", "bar");
// Act
keyValueService.SetValue("foo", "buzz");
var value = keyValueService.GetValue("foo");
// Assert
Assert.AreEqual("buzz", value);
}
[Test]
public void TrySetValue_ForExistingKeyWithProvidedValue_ReturnsTrueAndSetsValue()
{
// Arrange
var keyValueService = ServiceContext.KeyValueService;
keyValueService.SetValue("foo", "bar");
// Act
var result = keyValueService.TrySetValue("foo", "bar", "buzz");
var value = keyValueService.GetValue("foo");
// Assert
Assert.IsTrue(result);
Assert.AreEqual("buzz", value);
}
[Test]
public void TrySetValue_ForExistingKeyWithoutProvidedValue_ReturnsFalseAndDoesNotSetValue()
{
// Arrange
var keyValueService = ServiceContext.KeyValueService;
keyValueService.SetValue("foo", "bar");
// Act
var result = keyValueService.TrySetValue("foo", "bang", "buzz");
var value = keyValueService.GetValue("foo");
// Assert
Assert.IsFalse(result);
Assert.AreEqual("bar", value);
}
}
}

View File

@@ -21,6 +21,7 @@ using Umbraco.Core.Scoping;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Implement;
using Umbraco.Core.Strings;
using Umbraco.Infrastructure.Migrations.Custom;
using Umbraco.Tests.TestHelpers.Stubs;
using Current = Umbraco.Web.Composing.Current;
@@ -193,6 +194,7 @@ namespace Umbraco.Tests.TestHelpers
var tagService = GetLazyService<ITagService>(factory, c => new TagService(scopeProvider, logger, eventMessagesFactory, GetRepo<ITagRepository>(c)));
var redirectUrlService = GetLazyService<IRedirectUrlService>(factory, c => new RedirectUrlService(scopeProvider, logger, eventMessagesFactory, GetRepo<IRedirectUrlRepository>(c)));
var consentService = GetLazyService<IConsentService>(factory, c => new ConsentService(scopeProvider, logger, eventMessagesFactory, GetRepo<IConsentRepository>(c)));
var keyValueService = GetLazyService<IKeyValueService>(factory, c => new KeyValueService(scopeProvider, GetRepo<IKeyValueRepository>(c), Mock.Of<IKeyValueServiceInitialization>(), logger, umbracoVersion));
var contentTypeServiceBaseFactory = GetLazyService<IContentTypeBaseServiceProvider>(factory, c => new ContentTypeBaseServiceProvider(factory.GetInstance<IContentTypeService>(),factory.GetInstance<IMediaTypeService>(),factory.GetInstance<IMemberTypeService>()));
return new ServiceContext(
@@ -221,6 +223,7 @@ namespace Umbraco.Tests.TestHelpers
externalLoginService,
redirectUrlService,
consentService,
keyValueService,
contentTypeServiceBaseFactory);
}

View File

@@ -149,6 +149,7 @@
<Compile Include="Persistence\Repositories\DocumentRepositoryTest.cs" />
<Compile Include="Persistence\Repositories\EntityRepositoryTest.cs" />
<Compile Include="Persistence\Repositories\KeyValueRepositoryTests.cs" />
<Compile Include="Services\KeyValueServiceTests.cs" />
<Compile Include="UmbracoExamine\ExamineExtensions.cs" />
<Compile Include="PublishedContent\NuCacheChildrenTests.cs" />
<Compile Include="PublishedContent\PublishedContentLanguageVariantTests.cs" />