diff --git a/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs b/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs index 46de9de45e10..51f483f4cd4f 100644 --- a/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs +++ b/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; @@ -49,7 +50,7 @@ public DefaultRepositoryCachePolicy( { } - protected string EntityTypeCacheKey { get; } = $"uRepo_{typeof(TEntity).Name}_"; + protected string EntityTypeCacheKey { get; } = RepositoryCacheKeys.GetKey(); /// public override void Create(TEntity entity, Action persistNew) diff --git a/src/Umbraco.Infrastructure/Cache/FullDataSetRepositoryCachePolicy.cs b/src/Umbraco.Infrastructure/Cache/FullDataSetRepositoryCachePolicy.cs index e894d0916a26..00e844e7818e 100644 --- a/src/Umbraco.Infrastructure/Cache/FullDataSetRepositoryCachePolicy.cs +++ b/src/Umbraco.Infrastructure/Cache/FullDataSetRepositoryCachePolicy.cs @@ -3,6 +3,7 @@ using Umbraco.Cms.Core.Collections; using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; @@ -53,7 +54,7 @@ public override void Create(TEntity entity, Action persistNew) } } - protected string GetEntityTypeCacheKey() => $"uRepo_{typeof(TEntity).Name}_"; + protected string GetEntityTypeCacheKey() => RepositoryCacheKeys.GetKey(); protected void InsertEntities(TEntity[]? entities) { diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs index 405152917d41..c7f3c6594d2a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs @@ -635,15 +635,32 @@ private sealed class DocumentVariation protected override Guid NodeObjectTypeId => Constants.ObjectTypes.Document; + /// + public override void Save(IContent entity) + { + base.Save(entity); + + // Also populate the GUID cache so subsequent lookups by GUID don't hit the database. + _contentByGuidReadRepository.PopulateCacheByKey(entity); + } + protected override IContent? PerformGet(int id) { Sql sql = GetBaseQuery(QueryType.Single) .Where(x => x.NodeId == id); DocumentDto? dto = Database.FirstOrDefault(sql); - return dto == null - ? null - : MapDtoToContent(dto); + if (dto is null) + { + return null; + } + + IContent content = MapDtoToContent(dto); + + // Also populate the GUID cache so subsequent lookups by GUID don't hit the database. + _contentByGuidReadRepository.PopulateCacheByKey(content); + + return content; } protected override IEnumerable PerformGetAll(params int[]? ids) @@ -655,7 +672,13 @@ protected override IEnumerable PerformGetAll(params int[]? ids) sql.WhereIn(x => x.NodeId, ids); } - return MapDtosToContent(Database.Fetch(sql)); + // MapDtosToContent returns a materialized array, so this is safe to enumerate multiple times. + IEnumerable contents = MapDtosToContent(Database.Fetch(sql)); + + // Also populate the GUID cache so subsequent lookups by GUID don't hit the database. + _contentByGuidReadRepository.PopulateCacheByKey(contents); + + return contents; } protected override IEnumerable PerformGetByQuery(IQuery query) @@ -1601,6 +1624,33 @@ IEnumerable IReadRepository.GetMany(params Guid[]? ids public bool Exists(Guid id) => _contentByGuidReadRepository.Exists(id); + /// + /// Populates the int-keyed cache with the given entity. + /// This allows entities retrieved by GUID to also be cached for int ID lookups. + /// + private void PopulateCacheById(IContent entity) + { + if (entity.HasIdentity) + { + var cacheKey = GetCacheKey(entity.Id); + IsolatedCache.Insert(cacheKey, () => entity, TimeSpan.FromMinutes(5), true); + } + } + + /// + /// Populates the int-keyed cache with the given entities. + /// This allows entities retrieved by GUID to also be cached for int ID lookups. + /// + private void PopulateCacheById(IEnumerable entities) + { + foreach (IContent entity in entities) + { + PopulateCacheById(entity); + } + } + + private static string GetCacheKey(int id) => RepositoryCacheKeys.GetKey() + id; + // reading repository purely for looking up by GUID // TODO: ugly and to fix we need to decouple the IRepositoryQueryable -> IRepository -> IReadRepository which should all be separate things! // This sub-repository pattern is super old and totally unecessary anymore, caching can be handled in much nicer ways without this @@ -1637,6 +1687,9 @@ public ContentByGuidReadRepository( IContent content = _outerRepo.MapDtoToContent(dto); + // Also populate the int-keyed cache so subsequent lookups by int ID don't hit the database + _outerRepo.PopulateCacheById(content); + return content; } @@ -1648,7 +1701,13 @@ protected override IEnumerable PerformGetAll(params Guid[]? ids) sql.WhereIn(x => x.UniqueId, ids); } - return _outerRepo.MapDtosToContent(Database.Fetch(sql)); + // MapDtosToContent returns a materialized array, so this is safe to enumerate multiple times + IEnumerable contents = _outerRepo.MapDtosToContent(Database.Fetch(sql)); + + // Also populate the int-keyed cache so subsequent lookups by int ID don't hit the database + _outerRepo.PopulateCacheById(contents); + + return contents; } protected override IEnumerable PerformGetByQuery(IQuery query) => @@ -1668,6 +1727,33 @@ protected override Sql GetBaseQuery(bool isCount) => protected override string GetBaseWhereClause() => throw new InvalidOperationException("This method won't be implemented."); + + /// + /// Populates the GUID-keyed cache with the given entity. + /// This allows entities retrieved by int ID to also be cached for GUID lookups. + /// + public void PopulateCacheByKey(IContent entity) + { + if (entity.HasIdentity) + { + var cacheKey = GetCacheKey(entity.Key); + IsolatedCache.Insert(cacheKey, () => entity, TimeSpan.FromMinutes(5), true); + } + } + + /// + /// Populates the GUID-keyed cache with the given entities. + /// This allows entities retrieved by int ID to also be cached for GUID lookups. + /// + public void PopulateCacheByKey(IEnumerable entities) + { + foreach (IContent entity in entities) + { + PopulateCacheByKey(entity); + } + } + + private static string GetCacheKey(Guid key) => RepositoryCacheKeys.GetKey() + key; } #endregion diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs index a5fc5cb67ce2..3667aab61d89 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs @@ -14,7 +14,6 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Persistence.Querying; @@ -226,15 +225,32 @@ private IMedia MapDtoToContent(ContentDto dto) protected override Guid NodeObjectTypeId => Constants.ObjectTypes.Media; + /// + public override void Save(IMedia entity) + { + base.Save(entity); + + // Also populate the GUID cache so subsequent lookups by GUID don't hit the database + _mediaByGuidReadRepository.PopulateCacheByKey(entity); + } + protected override IMedia? PerformGet(int id) { Sql sql = GetBaseQuery(QueryType.Single) .Where(x => x.NodeId == id); ContentDto? dto = Database.FirstOrDefault(sql); - return dto == null - ? null - : MapDtoToContent(dto); + if (dto == null) + { + return null; + } + + IMedia media = MapDtoToContent(dto); + + // Also populate the GUID cache so subsequent lookups by GUID don't hit the database + _mediaByGuidReadRepository.PopulateCacheByKey(media); + + return media; } protected override IEnumerable PerformGetAll(params int[]? ids) @@ -246,7 +262,13 @@ protected override IEnumerable PerformGetAll(params int[]? ids) sql.WhereIn(x => x.NodeId, ids); } - return MapDtosToContent(Database.Fetch(sql)); + // MapDtosToContent returns a materialized array, so this is safe to enumerate multiple times + IEnumerable media = MapDtosToContent(Database.Fetch(sql)); + + // Also populate the GUID cache so subsequent lookups by GUID don't hit the database + _mediaByGuidReadRepository.PopulateCacheByKey(media); + + return media; } protected override IEnumerable PerformGetByQuery(IQuery query) @@ -581,6 +603,33 @@ IEnumerable IReadRepository.GetMany(params Guid[]? ids) => public bool Exists(Guid id) => _mediaByGuidReadRepository.Exists(id); + /// + /// Populates the int-keyed cache with the given entity. + /// This allows entities retrieved by GUID to also be cached for int ID lookups. + /// + private void PopulateCacheById(IMedia entity) + { + if (entity.HasIdentity) + { + var cacheKey = GetCacheKey(entity.Id); + IsolatedCache.Insert(cacheKey, () => entity, TimeSpan.FromMinutes(5), true); + } + } + + /// + /// Populates the int-keyed cache with the given entities. + /// This allows entities retrieved by GUID to also be cached for int ID lookups. + /// + private void PopulateCacheById(IEnumerable entities) + { + foreach (IMedia entity in entities) + { + PopulateCacheById(entity); + } + } + + private static string GetCacheKey(int id) => RepositoryCacheKeys.GetKey() + id; + // A reading repository purely for looking up by GUID // TODO: This is ugly and to fix we need to decouple the IRepositoryQueryable -> IRepository -> IReadRepository which should all be separate things! // This sub-repository pattern is super old and totally unecessary anymore, caching can be handled in much nicer ways without this @@ -615,9 +664,12 @@ public MediaByGuidReadRepository( return null; } - IMedia content = _outerRepo.MapDtoToContent(dto); + IMedia media = _outerRepo.MapDtoToContent(dto); - return content; + // Also populate the int-keyed cache so subsequent lookups by int ID don't hit the database + _outerRepo.PopulateCacheById(media); + + return media; } protected override IEnumerable PerformGetAll(params Guid[]? ids) @@ -628,7 +680,13 @@ protected override IEnumerable PerformGetAll(params Guid[]? ids) sql.WhereIn(x => x.UniqueId, ids); } - return _outerRepo.MapDtosToContent(Database.Fetch(sql)); + // MapDtosToContent returns a materialized array, so this is safe to enumerate multiple times + IEnumerable media = _outerRepo.MapDtosToContent(Database.Fetch(sql)); + + // Also populate the int-keyed cache so subsequent lookups by int ID don't hit the database + _outerRepo.PopulateCacheById(media); + + return media; } protected override IEnumerable PerformGetByQuery(IQuery query) => @@ -648,6 +706,33 @@ protected override Sql GetBaseQuery(bool isCount) => protected override string GetBaseWhereClause() => throw new InvalidOperationException("This method won't be implemented."); + + /// + /// Populates the GUID-keyed cache with the given entity. + /// This allows entities retrieved by int ID to also be cached for GUID lookups. + /// + public void PopulateCacheByKey(IMedia entity) + { + if (entity.HasIdentity) + { + var cacheKey = GetCacheKey(entity.Key); + IsolatedCache.Insert(cacheKey, () => entity, TimeSpan.FromMinutes(5), true); + } + } + + /// + /// Populates the GUID-keyed cache with the given entities. + /// This allows entities retrieved by int ID to also be cached for GUID lookups. + /// + public void PopulateCacheByKey(IEnumerable entities) + { + foreach (IMedia entity in entities) + { + PopulateCacheByKey(entity); + } + } + + private static string GetCacheKey(Guid key) => RepositoryCacheKeys.GetKey() + key; } #endregion diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs index 80c6a7d9309f..a02b812a7acf 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs @@ -156,12 +156,14 @@ private DocumentRepository CreateRepository(IScopeAccessor scopeAccessor, out Co dataValueReferences, DataTypeService, ConfigurationEditorJsonSerializer, - Mock.Of()); + Mock.Of(), + Mock.Of(), + Mock.Of()); return repository; } [Test] - public void CacheActiveForIntsAndGuids() + public void Retrievals_By_Id_And_Key_After_Save_Are_Cached() { var realCache = new AppCaches( new ObjectCacheAppCache(), @@ -171,46 +173,124 @@ public void CacheActiveForIntsAndGuids() var provider = ScopeProvider; var scopeAccessor = ScopeAccessor; - using (var scope = provider.CreateScope()) - { - var repository = CreateRepository((IScopeAccessor)provider, out var contentTypeRepository, realCache); + using var scope = provider.CreateScope(); + var repository = CreateRepository((IScopeAccessor)provider, out var contentTypeRepository, realCache); - var udb = scopeAccessor.AmbientScope.Database; + var database = scopeAccessor.AmbientScope.Database; - udb.EnableSqlCount = false; + database.EnableSqlCount = false; - var template = TemplateBuilder.CreateTextPageTemplate(); - FileService.SaveTemplate(template); - var contentType = - ContentTypeBuilder.CreateSimpleContentType("umbTextpage1", "Textpage", defaultTemplateId: template.Id); + var content = CreateContent(repository, contentTypeRepository); - contentTypeRepository.Save(contentType); - var content = ContentBuilder.CreateSimpleContent(contentType); - repository.Save(content); + database.EnableSqlCount = true; + + // Initial and subsequent requests should use the cache, since the cache by Id and Key was populated on save. + repository.Get(content.Id); + Assert.AreEqual(0, database.SqlCount); + + repository.Get(content.Id); + Assert.AreEqual(0, database.SqlCount); + + repository.Get(content.Key); + Assert.AreEqual(0, database.SqlCount); + + repository.Get(content.Key); + Assert.AreEqual(0, database.SqlCount); + } - udb.EnableSqlCount = true; + [Test] + public void Retrieval_By_Key_After_Retrieval_By_Id_Is_Cached() + { + var realCache = new AppCaches( + new ObjectCacheAppCache(), + new DictionaryAppCache(), + new IsolatedCaches(t => new ObjectCacheAppCache())); + + var provider = ScopeProvider; + var scopeAccessor = ScopeAccessor; - // go get it, this should already be cached since the default repository key is the INT - repository.Get(content.Id); - Assert.AreEqual(0, udb.SqlCount); + using var scope = provider.CreateScope(); + var repository = CreateRepository((IScopeAccessor)provider, out var contentTypeRepository, realCache); - // retrieve again, this should use cache - repository.Get(content.Id); - Assert.AreEqual(0, udb.SqlCount); + var database = scopeAccessor.AmbientScope.Database; - // reset counter - udb.EnableSqlCount = false; - udb.EnableSqlCount = true; + database.EnableSqlCount = false; - // now get by GUID, this won't be cached yet because the default repo key is not a GUID - repository.Get(content.Key); - var sqlCount = udb.SqlCount; - Assert.Greater(sqlCount, 0); + var content = CreateContent(repository, contentTypeRepository); - // retrieve again, this should use cache now - repository.Get(content.Key); - Assert.AreEqual(sqlCount, udb.SqlCount); - } + database.EnableSqlCount = true; + + // Clear the isolated cache for IContent so the next retrieval hits the database + realCache.IsolatedCaches.ClearCache(); + + // Initial request by ID should hit the database. + repository.Get(content.Id); + Assert.Greater(database.SqlCount, 0); + + // Reset counter. + database.EnableSqlCount = false; + database.EnableSqlCount = true; + + // Subsequent requests should use the cache, since the cache by Id and Key was populated on retrieval. + repository.Get(content.Id); + Assert.AreEqual(0, database.SqlCount); + + repository.Get(content.Key); + Assert.AreEqual(0, database.SqlCount); + } + + [Test] + public void Retrieval_By_Id_After_Retrieval_By_Key_Is_Cached() + { + var realCache = new AppCaches( + new ObjectCacheAppCache(), + new DictionaryAppCache(), + new IsolatedCaches(t => new ObjectCacheAppCache())); + + var provider = ScopeProvider; + var scopeAccessor = ScopeAccessor; + + using var scope = provider.CreateScope(); + var repository = CreateRepository((IScopeAccessor)provider, out var contentTypeRepository, realCache); + + var database = scopeAccessor.AmbientScope.Database; + + database.EnableSqlCount = false; + + var content = CreateContent(repository, contentTypeRepository); + + database.EnableSqlCount = true; + + // Clear the isolated cache for IContent so the next retrieval hits the database + realCache.IsolatedCaches.ClearCache(); + + // Initial request by key should hit the database. + repository.Get(content.Key); + Assert.Greater(database.SqlCount, 0); + + // Reset counter. + database.EnableSqlCount = false; + database.EnableSqlCount = true; + + // Subsequent requests should use the cache, since the cache by Id and Key was populated on retrieval. + repository.Get(content.Key); + Assert.AreEqual(0, database.SqlCount); + + repository.Get(content.Id); + Assert.AreEqual(0, database.SqlCount); + } + + private Content CreateContent(DocumentRepository repository, ContentTypeRepository contentTypeRepository) + { + var template = TemplateBuilder.CreateTextPageTemplate(); + FileService.SaveTemplate(template); + var contentType = + ContentTypeBuilder.CreateSimpleContentType("umbTextpage1", "Textpage", defaultTemplateId: template.Id); + + contentTypeRepository.Save(contentType); + var content = ContentBuilder.CreateSimpleContent(contentType); + repository.Save(content); + return content; } [Test] diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaRepositoryTest.cs index f0cf94851caa..8db6ba3d6948 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaRepositoryTest.cs @@ -87,7 +87,7 @@ private MediaRepository CreateRepository(IScopeProvider provider, out MediaTypeR } [Test] - public void CacheActiveForIntsAndGuids() + public void Retrievals_By_Id_And_Key_After_Save_Are_Cached() { var realCache = new AppCaches( new ObjectCacheAppCache(), @@ -95,43 +95,123 @@ public void CacheActiveForIntsAndGuids() new IsolatedCaches(t => new ObjectCacheAppCache())); var provider = ScopeProvider; - using (var scope = provider.CreateScope()) - { - var repository = CreateRepository(provider, out var mediaTypeRepository, realCache); + var scopeAccessor = ScopeAccessor; - var udb = ScopeAccessor.AmbientScope.Database; + using var scope = provider.CreateScope(); + var repository = CreateRepository(provider, out var mediaTypeRepository, realCache); - udb.EnableSqlCount = false; + var database = scopeAccessor.AmbientScope.Database; - var mediaType = MediaTypeBuilder.CreateSimpleMediaType("umbTextpage1", "Textpage"); - mediaTypeRepository.Save(mediaType); + database.EnableSqlCount = false; - var media = MediaBuilder.CreateSimpleMedia(mediaType, "hello", -1); - repository.Save(media); + var media = CreateMedia(repository, mediaTypeRepository); - udb.EnableSqlCount = true; + database.EnableSqlCount = true; - // go get it, this should already be cached since the default repository key is the INT - var found = repository.Get(media.Id); - Assert.AreEqual(0, udb.SqlCount); + // Initial and subsequent requests should use the cache, since the cache by Id and Key was populated on save. + repository.Get(media.Id); + Assert.AreEqual(0, database.SqlCount); - // retrieve again, this should use cache - found = repository.Get(media.Id); - Assert.AreEqual(0, udb.SqlCount); + repository.Get(media.Id); + Assert.AreEqual(0, database.SqlCount); - // reset counter - udb.EnableSqlCount = false; - udb.EnableSqlCount = true; + repository.Get(media.Key); + Assert.AreEqual(0, database.SqlCount); - // now get by GUID, this won't be cached yet because the default repo key is not a GUID - found = repository.Get(media.Key); - var sqlCount = udb.SqlCount; - Assert.Greater(sqlCount, 0); + repository.Get(media.Key); + Assert.AreEqual(0, database.SqlCount); + } - // retrieve again, this should use cache now - found = repository.Get(media.Key); - Assert.AreEqual(sqlCount, udb.SqlCount); - } + [Test] + public void Retrieval_By_Key_After_Retrieval_By_Id_Is_Cached() + { + var realCache = new AppCaches( + new ObjectCacheAppCache(), + new DictionaryAppCache(), + new IsolatedCaches(t => new ObjectCacheAppCache())); + + var provider = ScopeProvider; + var scopeAccessor = ScopeAccessor; + + using var scope = provider.CreateScope(); + var repository = CreateRepository(provider, out var mediaTypeRepository, realCache); + + var database = scopeAccessor.AmbientScope.Database; + + database.EnableSqlCount = false; + + var media = CreateMedia(repository, mediaTypeRepository); + + database.EnableSqlCount = true; + + // Clear the isolated cache for IMedia so the next retrieval hits the database + realCache.IsolatedCaches.ClearCache(); + + // Initial request by ID should hit the database. + repository.Get(media.Id); + Assert.Greater(database.SqlCount, 0); + + // Reset counter. + database.EnableSqlCount = false; + database.EnableSqlCount = true; + + // Subsequent requests should use the cache, since the cache by Id and Key was populated on retrieval. + repository.Get(media.Id); + Assert.AreEqual(0, database.SqlCount); + + repository.Get(media.Key); + Assert.AreEqual(0, database.SqlCount); + } + + [Test] + public void Retrieval_By_Id_After_Retrieval_By_Key_Is_Cached() + { + var realCache = new AppCaches( + new ObjectCacheAppCache(), + new DictionaryAppCache(), + new IsolatedCaches(t => new ObjectCacheAppCache())); + + var provider = ScopeProvider; + var scopeAccessor = ScopeAccessor; + + using var scope = provider.CreateScope(); + var repository = CreateRepository(provider, out var mediaTypeRepository, realCache); + + var database = scopeAccessor.AmbientScope.Database; + + database.EnableSqlCount = false; + + var media = CreateMedia(repository, mediaTypeRepository); + + database.EnableSqlCount = true; + + // Clear the isolated cache for IMedia so the next retrieval hits the database + realCache.IsolatedCaches.ClearCache(); + + // Initial request by key should hit the database. + repository.Get(media.Key); + Assert.Greater(database.SqlCount, 0); + + // Reset counter. + database.EnableSqlCount = false; + database.EnableSqlCount = true; + + // Subsequent requests should use the cache, since the cache by Id and Key was populated on retrieval. + repository.Get(media.Key); + Assert.AreEqual(0, database.SqlCount); + + repository.Get(media.Id); + Assert.AreEqual(0, database.SqlCount); + } + + private Media CreateMedia(MediaRepository repository, MediaTypeRepository mediaTypeRepository) + { + var mediaType = MediaTypeBuilder.CreateSimpleMediaType("umbTextpage1", "Textpage"); + mediaTypeRepository.Save(mediaType); + + var media = MediaBuilder.CreateSimpleMedia(mediaType, "hello", -1); + repository.Save(media); + return media; } [Test]