diff --git a/src/LinkDotNet.Blog.Domain/BlogPost.cs b/src/LinkDotNet.Blog.Domain/BlogPost.cs index cb3866d2..bd675ed7 100644 --- a/src/LinkDotNet.Blog.Domain/BlogPost.cs +++ b/src/LinkDotNet.Blog.Domain/BlogPost.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; namespace LinkDotNet.Blog.Domain; @@ -24,7 +25,7 @@ private BlogPost() public DateTime? ScheduledPublishDate { get; private set; } - public ICollection Tags { get; private set; } + public IReadOnlyCollection Tags { get; private set; } public bool IsPublished { get; private set; } @@ -32,7 +33,7 @@ private BlogPost() public bool IsScheduled => ScheduledPublishDate is not null; - public string TagsAsString => Tags is null ? string.Empty : string.Join(", ", Tags.Select(t => t.Content)); + public string TagsAsString => Tags is null ? string.Empty : string.Join(", ", Tags); public static BlogPost Create( string title, @@ -62,7 +63,7 @@ public static BlogPost Create( PreviewImageUrl = previewImageUrl, PreviewImageUrlFallback = previewImageUrlFallback, IsPublished = isPublished, - Tags = tags?.Select(Tag.Create).ToList(), + Tags = tags?.Select(t => t.Trim()).ToImmutableArray(), }; return blogPost; @@ -89,22 +90,6 @@ public void Update(BlogPost from) PreviewImageUrl = from.PreviewImageUrl; PreviewImageUrlFallback = from.PreviewImageUrlFallback; IsPublished = from.IsPublished; - ReplaceTags(from.Tags); - } - - private void ReplaceTags(IEnumerable tags) - { - Tags?.Clear(); - if (Tags == null || tags == null) - { - Tags = tags?.ToList(); - } - else - { - foreach (var tag in tags) - { - Tags.Add(tag); - } - } + Tags = from.Tags; } } diff --git a/src/LinkDotNet.Blog.Domain/Tag.cs b/src/LinkDotNet.Blog.Domain/Tag.cs deleted file mode 100644 index c8865485..00000000 --- a/src/LinkDotNet.Blog.Domain/Tag.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; - -namespace LinkDotNet.Blog.Domain; - -public sealed class Tag -{ - private Tag() - { - } - - public string Id { get; private set; } - - public string Content { get; private set; } - - public static Tag Create(string content) - { - ArgumentException.ThrowIfNullOrWhiteSpace(content); - - return new Tag - { - Content = content.Trim(), - }; - } -} diff --git a/src/LinkDotNet.Blog.Infrastructure/Persistence/Sql/BlogDbContext.cs b/src/LinkDotNet.Blog.Infrastructure/Persistence/Sql/BlogDbContext.cs index a2458159..629eb9eb 100644 --- a/src/LinkDotNet.Blog.Infrastructure/Persistence/Sql/BlogDbContext.cs +++ b/src/LinkDotNet.Blog.Infrastructure/Persistence/Sql/BlogDbContext.cs @@ -14,8 +14,6 @@ public BlogDbContext(DbContextOptions options) public DbSet BlogPosts { get; set; } - public DbSet Tags { get; set; } - public DbSet ProfileInformationEntries { get; set; } public DbSet Skills { get; set; } @@ -27,7 +25,6 @@ public BlogDbContext(DbContextOptions options) protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfiguration(new BlogPostConfiguration()); - modelBuilder.ApplyConfiguration(new TagsConfiguration()); modelBuilder.ApplyConfiguration(new ProfileInformationEntryConfiguration()); modelBuilder.ApplyConfiguration(new SkillConfiguration()); modelBuilder.ApplyConfiguration(new UserRecordConfiguration()); diff --git a/src/LinkDotNet.Blog.Infrastructure/Persistence/Sql/Mapping/BlogPostConfiguration.cs b/src/LinkDotNet.Blog.Infrastructure/Persistence/Sql/Mapping/BlogPostConfiguration.cs index 7b9046a2..05b9e50b 100644 --- a/src/LinkDotNet.Blog.Infrastructure/Persistence/Sql/Mapping/BlogPostConfiguration.cs +++ b/src/LinkDotNet.Blog.Infrastructure/Persistence/Sql/Mapping/BlogPostConfiguration.cs @@ -10,11 +10,6 @@ public void Configure(EntityTypeBuilder builder) { builder.HasKey(c => c.Id); builder.Property(c => c.Id).ValueGeneratedOnAdd(); - - builder.HasMany(t => t.Tags) - .WithOne() - .OnDelete(DeleteBehavior.Cascade); - builder.Navigation(x => x.Tags).AutoInclude(); builder.Property(x => x.Title).HasMaxLength(256).IsRequired(); builder.Property(x => x.PreviewImageUrl).HasMaxLength(1024).IsRequired(); builder.Property(x => x.PreviewImageUrlFallback).HasMaxLength(1024); @@ -22,6 +17,7 @@ public void Configure(EntityTypeBuilder builder) builder.Property(x => x.ShortDescription).IsRequired(); builder.Property(x => x.Likes).IsRequired(); builder.Property(x => x.IsPublished).IsRequired(); + builder.Property(x => x.Tags).HasMaxLength(2096); builder.HasIndex(x => new { x.IsPublished, x.UpdatedDate }) .HasDatabaseName("IX_BlogPosts_IsPublished_UpdatedDate") diff --git a/src/LinkDotNet.Blog.Infrastructure/Persistence/Sql/Mapping/TagsConfiguration.cs b/src/LinkDotNet.Blog.Infrastructure/Persistence/Sql/Mapping/TagsConfiguration.cs deleted file mode 100644 index 09a76f7c..00000000 --- a/src/LinkDotNet.Blog.Infrastructure/Persistence/Sql/Mapping/TagsConfiguration.cs +++ /dev/null @@ -1,15 +0,0 @@ -using LinkDotNet.Blog.Domain; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace LinkDotNet.Blog.Infrastructure.Persistence.Sql.Mapping; - -public sealed class TagsConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.HasKey(c => c.Id); - builder.Property(c => c.Id).ValueGeneratedOnAdd(); - builder.Property(c => c.Content).HasMaxLength(64).IsRequired(); - } -} \ No newline at end of file diff --git a/src/LinkDotNet.Blog.Infrastructure/Persistence/Sql/Repository.cs b/src/LinkDotNet.Blog.Infrastructure/Persistence/Sql/Repository.cs index f8a1e4e4..7804bf5d 100644 --- a/src/LinkDotNet.Blog.Infrastructure/Persistence/Sql/Repository.cs +++ b/src/LinkDotNet.Blog.Infrastructure/Persistence/Sql/Repository.cs @@ -74,7 +74,7 @@ public async ValueTask StoreAsync(TEntity entity) await blogDbContext.SaveChangesAsync(); } - + public async ValueTask DeleteAsync(string id) { var entityToDelete = await GetByIdAsync(id); diff --git a/src/LinkDotNet.Blog.Web/Controller/RssFeedController.cs b/src/LinkDotNet.Blog.Web/Controller/RssFeedController.cs index 43532e3d..3a71e894 100644 --- a/src/LinkDotNet.Blog.Web/Controller/RssFeedController.cs +++ b/src/LinkDotNet.Blog.Web/Controller/RssFeedController.cs @@ -16,6 +16,7 @@ namespace LinkDotNet.Blog.Web.Controller; public sealed class RssFeedController : ControllerBase { + private static readonly XmlWriterSettings Settings = CreateXmlWriterSettings(); private readonly AppConfiguration appConfiguration; private readonly IRepository blogPostRepository; @@ -45,8 +46,7 @@ public async Task GetRssFeed() private static async Task WriteRssInfoToStreamAsync(Stream stream, SyndicationFeed feed) { - var settings = CreateXmlWriterSettings(); - await using var xmlWriter = XmlWriter.Create(stream, settings); + await using var xmlWriter = XmlWriter.Create(stream, Settings); var rssFormatter = new Rss20FeedFormatter(feed, false); rssFormatter.WriteTo(xmlWriter); await xmlWriter.FlushAsync(); @@ -86,9 +86,9 @@ private static SyndicationItem CreateSyndicationItemFromBlogPost(string url, Blo private static void AddCategories(ICollection categories, BlogPostRssInfo blogPost) { - foreach (var tag in blogPost.Tags ?? Array.Empty()) + foreach (var tag in blogPost.Tags ?? Array.Empty()) { - categories.Add(new SyndicationCategory(tag.Content)); + categories.Add(new SyndicationCategory(tag)); } } @@ -107,5 +107,5 @@ private sealed record BlogPostRssInfo( string ShortDescription, DateTime UpdatedDate, string PreviewImageUrl, - ICollection Tags); + IReadOnlyCollection Tags); } \ No newline at end of file diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/Sitemap/Services/SitemapService.cs b/src/LinkDotNet.Blog.Web/Features/Admin/Sitemap/Services/SitemapService.cs index 10be3635..79a0c1fb 100644 --- a/src/LinkDotNet.Blog.Web/Features/Admin/Sitemap/Services/SitemapService.cs +++ b/src/LinkDotNet.Blog.Web/Features/Admin/Sitemap/Services/SitemapService.cs @@ -57,7 +57,6 @@ private IEnumerable CreateUrlsForTags(IEnumerable blogPost { return blogPosts .SelectMany(b => b.Tags) - .Select(t => t.Content) .Distinct() .Select(t => new SitemapUrl { diff --git a/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor b/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor index 88bfcfa5..e9500fe8 100644 --- a/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor +++ b/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor @@ -28,7 +28,7 @@ {
    • - @foreach (var tag in BlogPost.Tags.Select(t => t.Content)) + @foreach (var tag in BlogPost.Tags) {
    • @tag
    • } diff --git a/src/LinkDotNet.Blog.Web/Features/Search/SearchPage.razor b/src/LinkDotNet.Blog.Web/Features/Search/SearchPage.razor index 87811368..a6ac8a75 100644 --- a/src/LinkDotNet.Blog.Web/Features/Search/SearchPage.razor +++ b/src/LinkDotNet.Blog.Web/Features/Search/SearchPage.razor @@ -24,7 +24,7 @@ { var term = Uri.UnescapeDataString(SearchTerm); blogPosts = await BlogPostRepository.GetAllAsync(t => t.IsPublished && (t.Title.Contains(term) - || t.Tags.Any(tag => tag.Content == term)), + || t.Tags.Any(tag => tag == term)), b => b.UpdatedDate); } diff --git a/src/LinkDotNet.Blog.Web/Features/SearchByTag/SearchByTagPage.razor b/src/LinkDotNet.Blog.Web/Features/SearchByTag/SearchByTagPage.razor index 9611b696..108f3039 100644 --- a/src/LinkDotNet.Blog.Web/Features/SearchByTag/SearchByTagPage.razor +++ b/src/LinkDotNet.Blog.Web/Features/SearchByTag/SearchByTagPage.razor @@ -23,7 +23,7 @@ { Tag = Uri.UnescapeDataString(Tag); blogPosts = await BlogPostRepository.GetAllAsync( - b => b.IsPublished && b.Tags.Any(t => t.Content == Tag), + b => b.IsPublished && b.Tags.Any(t => t == Tag), b => b.UpdatedDate); } diff --git a/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor b/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor index a0998a11..8e18dc1d 100644 --- a/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor +++ b/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor @@ -37,7 +37,7 @@ else @if (BlogPost.Tags != null && BlogPost.Tags.Any()) { - @foreach (var tag in BlogPost.Tags.Select(t => t.Content)) + @foreach (var tag in BlogPost.Tags) { @tag } diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/CachedRepositoryTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/CachedRepositoryTests.cs index f972b370..4c85d260 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/CachedRepositoryTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/CachedRepositoryTests.cs @@ -20,13 +20,13 @@ public async Task ShouldNotCacheWhenDifferentQueries() await Repository.StoreAsync(bp3); var searchTerm = "tag 1"; var sut = new CachedRepository(Repository, new MemoryCache(new MemoryCacheOptions())); - await sut.GetAllAsync(f => f.Tags.Any(t => t.Content == searchTerm)); + await sut.GetAllAsync(f => f.Tags.Any(t => t == searchTerm)); searchTerm = "tag 2"; - var allWithTag2 = await sut.GetAllAsync(f => f.Tags.Any(t => t.Content == searchTerm)); + var allWithTag2 = await sut.GetAllAsync(f => f.Tags.Any(t => t == searchTerm)); allWithTag2.Count.Should().Be(1); - allWithTag2.Single().Tags.Single().Content.Should().Be("tag 2"); + allWithTag2.Single().Tags.Single().Should().Be("tag 2"); } [Fact] diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/RavenDb/BlogPostRepositoryTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/RavenDb/BlogPostRepositoryTests.cs index 26584248..1acf01d5 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/RavenDb/BlogPostRepositoryTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/RavenDb/BlogPostRepositoryTests.cs @@ -37,7 +37,7 @@ public async Task ShouldLoadBlogPost() blogPostFromRepo.PreviewImageUrl.Should().Be("url"); blogPostFromRepo.IsPublished.Should().BeTrue(); blogPostFromRepo.Tags.Should().HaveCount(2); - var tagContent = blogPostFromRepo.Tags.Select(t => t.Content).ToList(); + var tagContent = blogPostFromRepo.Tags; tagContent.Should().Contain(new[] { "Tag 1", "Tag 2" }); } @@ -96,7 +96,7 @@ public async Task ShouldSaveBlogPost() blogPostFromContext.IsPublished.Should().BeTrue(); blogPostFromContext.PreviewImageUrl.Should().Be("url"); blogPostFromContext.Tags.Should().HaveCount(2); - var tagContent = blogPostFromContext.Tags.Select(t => t.Content).ToList(); + var tagContent = blogPostFromContext.Tags; tagContent.Should().Contain(new[] { "Tag 1", "Tag 2" }); } @@ -117,7 +117,7 @@ public async Task ShouldGetAllBlogPosts() blogPostFromRepo.PreviewImageUrl.Should().Be("url"); blogPostFromRepo.IsPublished.Should().BeTrue(); blogPostFromRepo.Tags.Should().HaveCount(2); - var tagContent = blogPostFromRepo.Tags.Select(t => t.Content).ToList(); + var tagContent = blogPostFromRepo.Tags; tagContent.Should().Contain(new[] { "Tag 1", "Tag 2" }); } diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/Sql/BlogPostRepositoryTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/Sql/BlogPostRepositoryTests.cs index 60a35ad3..3b18b6ae 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/Sql/BlogPostRepositoryTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/Sql/BlogPostRepositoryTests.cs @@ -1,8 +1,10 @@ using System.Linq; using System.Threading.Tasks; using LinkDotNet.Blog.Domain; +using LinkDotNet.Blog.Infrastructure.Persistence; using LinkDotNet.Blog.TestUtilities; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; namespace LinkDotNet.Blog.IntegrationTests.Infrastructure.Persistence.Sql; @@ -24,7 +26,7 @@ public async Task ShouldLoadBlogPost() blogPostFromRepo.PreviewImageUrl.Should().Be("url"); blogPostFromRepo.IsPublished.Should().BeTrue(); blogPostFromRepo.Tags.Should().HaveCount(2); - var tagContent = blogPostFromRepo.Tags.Select(t => t.Content).ToList(); + var tagContent = blogPostFromRepo.Tags; tagContent.Should().Contain(new[] { "Tag 1", "Tag 2" }); } @@ -35,7 +37,7 @@ public async Task ShouldSaveBlogPost() await Repository.StoreAsync(blogPost); - var blogPostFromContext = await DbContext.BlogPosts.Include(b => b.Tags).AsNoTracking().SingleOrDefaultAsync(s => s.Id == blogPost.Id); + var blogPostFromContext = await DbContext.BlogPosts.AsNoTracking().SingleOrDefaultAsync(s => s.Id == blogPost.Id); blogPostFromContext.Should().NotBeNull(); blogPostFromContext.Title.Should().Be("Title"); blogPostFromContext.ShortDescription.Should().Be("Subtitle"); @@ -43,7 +45,7 @@ public async Task ShouldSaveBlogPost() blogPostFromContext.IsPublished.Should().BeTrue(); blogPostFromContext.PreviewImageUrl.Should().Be("url"); blogPostFromContext.Tags.Should().HaveCount(2); - var tagContent = blogPostFromContext.Tags.Select(t => t.Content).ToList(); + var tagContent = blogPostFromContext.Tags; tagContent.Should().Contain(new[] { "Tag 1", "Tag 2" }); } @@ -65,7 +67,7 @@ public async Task ShouldGetAllBlogPosts() blogPostFromRepo.PreviewImageUrl.Should().Be("url"); blogPostFromRepo.IsPublished.Should().BeTrue(); blogPostFromRepo.Tags.Should().HaveCount(2); - var tagContent = blogPostFromRepo.Tags.Select(t => t.Content).ToList(); + var tagContent = blogPostFromRepo.Tags; tagContent.Should().Contain(new[] { "Tag 1", "Tag 2" }); } @@ -116,4 +118,20 @@ public async Task ShouldDelete() (await DbContext.BlogPosts.AsNoTracking().AnyAsync(b => b.Id == blogPost.Id)).Should().BeFalse(); } + + [Fact] + public async Task GivenBlogPostWithTags_WhenLoadingAndDeleting_ThenShouldBeUpdated() + { + var bp = new BlogPostBuilder().WithTags("tag 1").Build(); + var sut = new CachedRepository(Repository, new MemoryCache(new MemoryCacheOptions())); + await sut.StoreAsync(bp); + var updateBp = new BlogPostBuilder().WithTags("tag 2").Build(); + var bpFromCache = await sut.GetByIdAsync(bp.Id); + bpFromCache.Update(updateBp); + await sut.StoreAsync(bpFromCache); + + var bpFromDb = await sut.GetByIdAsync(bp.Id); + + bpFromDb.Tags.Single().Should().Be("tag 2"); + } } diff --git a/tests/LinkDotNet.Blog.UnitTests/Domain/BlogPostTests.cs b/tests/LinkDotNet.Blog.UnitTests/Domain/BlogPostTests.cs index 77a1b29e..d0422927 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Domain/BlogPostTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Domain/BlogPostTests.cs @@ -37,7 +37,7 @@ public void ShouldUpdateTagsWhenExisting() blogPostToUpdate.Update(blogPost); blogPostToUpdate.Tags.Should().HaveCount(1); - blogPostToUpdate.Tags.Single().Content.Should().Be("tag 2"); + blogPostToUpdate.Tags.Single().Should().Be("tag 2"); } [Fact] @@ -45,8 +45,8 @@ public void ShouldTrimWhitespacesFromTags() { var blogPost = BlogPost.Create("Title", "Sub", "Content", "Preview", false, tags: new[] { " Tag 1", " Tag 2 ", }); - blogPost.Tags.Select(t => t.Content).Should().Contain("Tag 1"); - blogPost.Tags.Select(t => t.Content).Should().Contain("Tag 2"); + blogPost.Tags.Should().Contain("Tag 1"); + blogPost.Tags.Should().Contain("Tag 2"); } [Fact] @@ -67,7 +67,7 @@ public void ShouldNotDeleteTagsWhenSameReference() bp.Update(bp); bp.Tags.Should().HaveCount(1); - bp.Tags.Single().Content.Should().Be("tag 1"); + bp.Tags.Single().Should().Be("tag 1"); } [Fact] diff --git a/tests/LinkDotNet.Blog.UnitTests/Domain/TagTests.cs b/tests/LinkDotNet.Blog.UnitTests/Domain/TagTests.cs deleted file mode 100644 index b960a14c..00000000 --- a/tests/LinkDotNet.Blog.UnitTests/Domain/TagTests.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using LinkDotNet.Blog.Domain; - -namespace LinkDotNet.Blog.UnitTests.Domain; - -public class TagTests -{ - [Fact] - public void ShouldCreateTag() - { - var tag = Tag.Create(" Test "); - - tag.Content.Should().Be("Test"); - } - - [Theory] - [InlineData("")] - [InlineData(null)] - [InlineData(" ")] - public void ShouldThrowExceptionIfInvalid(string content) - { - Action act = () => Tag.Create(content); - - act.Should().Throw(); - } -} diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPostTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPostTests.cs index 83038e92..496335b6 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPostTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPostTests.cs @@ -44,7 +44,7 @@ public void ShouldCreateNewBlogPostWhenValidDataGiven() blogPost.IsPublished.Should().BeFalse(); blogPost.UpdatedDate.Should().NotBe(default); blogPost.Tags.Should().HaveCount(3); - blogPost.Tags.Select(t => t.Content).Should().Contain(new[] { "Tag1", "Tag2", "Tag3" }); + blogPost.Tags.Should().Contain(new[] { "Tag1", "Tag2", "Tag3" }); } [Fact] @@ -69,8 +69,8 @@ public void ShouldFillGivenBlogPost() blogPostFromComponent.Title.Should().Be("Title"); blogPostFromComponent.ShortDescription.Should().Be("Desc"); blogPostFromComponent.Content.Should().Be("Content"); - blogPostFromComponent.Tags.Select(t => t.Content).Should().Contain("tag1"); - blogPostFromComponent.Tags.Select(t => t.Content).Should().Contain("tag2"); + blogPostFromComponent.Tags.Should().Contain("tag1"); + blogPostFromComponent.Tags.Should().Contain("tag2"); } [Fact] @@ -168,7 +168,7 @@ public void ShouldAcceptInputWithoutLosingFocusOrEnter() blogPost.PreviewImageUrl.Should().Be("My preview url"); blogPost.IsPublished.Should().BeFalse(); blogPost.Tags.Should().HaveCount(3); - blogPost.Tags.Select(t => t.Content).Should().Contain(new[] { "Tag1", "Tag2", "Tag3" }); + blogPost.Tags.Should().Contain(new[] { "Tag1", "Tag2", "Tag3" }); } [Fact]