From c75a28377c07858e265136f20331d5a5e91e624f Mon Sep 17 00:00:00 2001 From: mpaulosky <60372079+mpaulosky@users.noreply.github.com> Date: Fri, 24 Apr 2026 08:45:01 -0700 Subject: [PATCH 01/20] docs: post-release documentation update for v1.2.0 (#125) Blog posts, README, and docs/index.html updated for v1.2.0. Reviewed and approved by Aragorn. --- .github/workflows/milestone-blog.yml | 54 +++--- .../workflows/milestone-release-decision.yml | 50 ++--- .github/workflows/release-blog.yml | 42 ++--- README.md | 138 +++++++++----- .../blog/2026-04-18-myblog-project-kickoff.md | 69 +++++++ docs/blog/2026-04-20-release-v1-0-0.md | 55 ++++++ docs/blog/2026-04-20-sprint-2-cqrs-mediatr.md | 87 +++++++++ ...6-04-20-sprint-3-e2e-tests-ci-hardening.md | 70 +++++++ docs/blog/2026-04-24-release-v1-1-0.md | 79 ++++++++ docs/blog/2026-04-24-release-v1-2-0.md | 129 +++++++++++++ docs/blog/index.md | 47 +++++ docs/index.html | 172 +++++++++--------- 12 files changed, 781 insertions(+), 211 deletions(-) create mode 100644 docs/blog/2026-04-18-myblog-project-kickoff.md create mode 100644 docs/blog/2026-04-20-release-v1-0-0.md create mode 100644 docs/blog/2026-04-20-sprint-2-cqrs-mediatr.md create mode 100644 docs/blog/2026-04-20-sprint-3-e2e-tests-ci-hardening.md create mode 100644 docs/blog/2026-04-24-release-v1-1-0.md create mode 100644 docs/blog/2026-04-24-release-v1-2-0.md create mode 100644 docs/blog/index.md diff --git a/.github/workflows/milestone-blog.yml b/.github/workflows/milestone-blog.yml index de85f5b1..830811b5 100644 --- a/.github/workflows/milestone-blog.yml +++ b/.github/workflows/milestone-blog.yml @@ -62,45 +62,45 @@ jobs: run: | BODY="## Milestone Review: ${MILESTONE_TITLE} -Ralph, **${MILESTONE_TITLE}** has been closed with **${ISSUE_COUNT} issues** completed. -Current latest release: \`${LAST_TAG}\` + Ralph, **${MILESTONE_TITLE}** has been closed with **${ISSUE_COUNT} issues** completed. + Current latest release: \`${LAST_TAG}\` -### Closed Issues (${ISSUE_COUNT} total) -${ISSUES_LIST} + ### Closed Issues (${ISSUE_COUNT} total) + ${ISSUES_LIST} ---- + --- -## πŸ” Release Candidate Checklist + ## πŸ” Release Candidate Checklist -Review these criteria to decide between a full release or a blog-only post: + Review these criteria to decide between a full release or a blog-only post: -| Criteria | Check | -|----------|-------| -| Contains user-facing features or enhancements? | (${FEATURE_COUNT} enhancement-labeled issues) | -| Contains breaking changes or schema migrations? | Review issues above | -| Significant enough to warrant a version bump? | Your call | -| All CI gates green on \`main\`? | Check GitHub Actions | -| Blog post needed either way? | **Yes β€” always** | + | Criteria | Check | + |----------|-------| + | Contains user-facing features or enhancements? | (${FEATURE_COUNT} enhancement-labeled issues) | + | Contains breaking changes or schema migrations? | Review issues above | + | Significant enough to warrant a version bump? | Your call | + | All CI gates green on \`main\`? | Check GitHub Actions | + | Blog post needed either way? | **Yes β€” always** | ---- + --- -## βœ… Your Decision + ## βœ… Your Decision -**Option A β€” Release Candidate** (tag + GitHub Release + release blog post): -> Add the label **\`release-candidate\`** to this issue. -> Then choose the version bump type in the comment below (major / minor / patch). + **Option A β€” Release Candidate** (tag + GitHub Release + release blog post): + > Add the label **\`release-candidate\`** to this issue. + > Then choose the version bump type in the comment below (major / minor / patch). -**Option B β€” Blog Only** (milestone summary blog post, no release): -> Add the label **\`blog-only\`** to this issue. + **Option B β€” Blog Only** (milestone summary blog post, no release): + > Add the label **\`blog-only\`** to this issue. -Either choice will automatically: -- Trigger Bilbo to write the blog post -- Update \`docs/blog/index.md\` (the blog page) -- Sync \`README.md\` via the blog-readme-sync workflow + Either choice will automatically: + - Trigger Bilbo to write the blog post + - Update \`docs/blog/index.md\` (the blog page) + - Sync \`README.md\` via the blog-readme-sync workflow ---- + --- -*Triggered by milestone close at ${PUBLISH_DATE}. Workflow: \`milestone-blog.yml\`*" + *Triggered by milestone close at ${PUBLISH_DATE}. Workflow: \`milestone-blog.yml\`*" gh issue create \ --title "πŸ“‹ Milestone Review: ${MILESTONE_TITLE} β€” release or blog?" \ diff --git a/.github/workflows/milestone-release-decision.yml b/.github/workflows/milestone-release-decision.yml index 8a78a3c4..0d93682b 100644 --- a/.github/workflows/milestone-release-decision.yml +++ b/.github/workflows/milestone-release-decision.yml @@ -63,12 +63,12 @@ jobs: run: | gh issue comment "$ISSUE_NUMBER" --body "βœ… **Release flow triggered.** -- Version bump: \`${BUMP}\` -- \`squad-milestone-release.yml\` dispatched β€” this will create the tag, push it, and publish a GitHub Release. -- \`release-blog.yml\` will fire automatically on release publish and create a \`squad:bilbo\` blog brief. -- Once Bilbo's PR merges, \`blog-readme-sync.yml\` will update \`README.md\` (the Page). + - Version bump: \`${BUMP}\` + - \`squad-milestone-release.yml\` dispatched β€” this will create the tag, push it, and publish a GitHub Release. + - \`release-blog.yml\` will fire automatically on release publish and create a \`squad:bilbo\` blog brief. + - Once Bilbo's PR merges, \`blog-readme-sync.yml\` will update \`README.md\` (the Page). -Closing this review issue." + Closing this review issue." gh issue close "$ISSUE_NUMBER" --reason completed @@ -109,25 +109,25 @@ Closing this review issue." run: | BODY="## Blog Post Brief: ${MILESTONE_TITLE} -Bilbo, Ralph has reviewed the **${MILESTONE_TITLE}** milestone and decided: **blog post only** (no release). + Bilbo, Ralph has reviewed the **${MILESTONE_TITLE}** milestone and decided: **blog post only** (no release). -### Post metadata -- **Suggested filename:** \`docs/blog/${PUBLISH_DATE}-${SLUG}.md\` -- **Suggested publish date:** ${PUBLISH_DATE} -- **Review issue:** ${REVIEW_ISSUE_URL} (see for full issue list) + ### Post metadata + - **Suggested filename:** \`docs/blog/${PUBLISH_DATE}-${SLUG}.md\` + - **Suggested publish date:** ${PUBLISH_DATE} + - **Review issue:** ${REVIEW_ISSUE_URL} (see for full issue list) -### Suggested post structure -1. **Summary** β€” what this milestone accomplished (2–3 sentences) -2. **What was built** β€” key features, fixes, or improvements with context -3. **Technical highlights** β€” architecture decisions, tricky problems solved, patterns introduced -4. **What's next** β€” follow-up work planned + ### Suggested post structure + 1. **Summary** β€” what this milestone accomplished (2–3 sentences) + 2. **What was built** β€” key features, fixes, or improvements with context + 3. **Technical highlights** β€” architecture decisions, tricky problems solved, patterns introduced + 4. **What's next** β€” follow-up work planned -### Instructions -- Write the post to \`docs/blog/${PUBLISH_DATE}-${SLUG}.md\` -- Update \`docs/blog/index.md\` to add the new post at the top of the Recent Posts table -- Follow existing post style (see \`docs/blog/2026-04-04-release-v0-7-0.md\`) -- Open a PR targeting \`main\` when complete -- Close both this issue and the review issue (#${REVIEW_ISSUE_NUMBER}) in the PR description" + ### Instructions + - Write the post to \`docs/blog/${PUBLISH_DATE}-${SLUG}.md\` + - Update \`docs/blog/index.md\` to add the new post at the top of the Recent Posts table + - Follow existing post style (see \`docs/blog/2026-04-04-release-v0-7-0.md\`) + - Open a PR targeting \`main\` when complete + - Close both this issue and the review issue (#${REVIEW_ISSUE_NUMBER}) in the PR description" gh issue create \ --title "Blog post: ${MILESTONE_TITLE} β€” milestone recap" \ @@ -141,10 +141,10 @@ Bilbo, Ralph has reviewed the **${MILESTONE_TITLE}** milestone and decided: **bl run: | gh issue comment "$ISSUE_NUMBER" --body "πŸ“ **Blog-only path selected.** -A \`squad:bilbo\` blog brief issue has been created. Once Bilbo's PR merges: -- \`docs/blog/index.md\` will be updated (the blog page) -- \`blog-readme-sync.yml\` will update \`README.md\` (the Page) + A \`squad:bilbo\` blog brief issue has been created. Once Bilbo's PR merges: + - \`docs/blog/index.md\` will be updated (the blog page) + - \`blog-readme-sync.yml\` will update \`README.md\` (the Page) -Closing this review issue." + Closing this review issue." gh issue close "$ISSUE_NUMBER" --reason completed diff --git a/.github/workflows/release-blog.yml b/.github/workflows/release-blog.yml index 1619b10f..82ef922e 100644 --- a/.github/workflows/release-blog.yml +++ b/.github/workflows/release-blog.yml @@ -46,31 +46,31 @@ jobs: run: | BODY="## Blog Post Brief: ${RELEASE_NAME} -Bilbo, a new GitHub Release has been published. Please write a dev blog post for **${RELEASE_NAME}**. + Bilbo, a new GitHub Release has been published. Please write a dev blog post for **${RELEASE_NAME}**. -### Post metadata -- **Suggested filename:** \`docs/blog/${PUBLISH_DATE}-release-${SLUG}.md\` -- **Suggested publish date:** ${PUBLISH_DATE} -- **Release:** [${TAG}](${RELEASE_URL}) -- **Changelog range:** [\`${PREV_TAG}...${TAG}\`](https://github.com/${{ github.repository }}/compare/${PREV_TAG}...${TAG}) + ### Post metadata + - **Suggested filename:** \`docs/blog/${PUBLISH_DATE}-release-${SLUG}.md\` + - **Suggested publish date:** ${PUBLISH_DATE} + - **Release:** [${TAG}](${RELEASE_URL}) + - **Changelog range:** [\`${PREV_TAG}...${TAG}\`](https://github.com/${{ github.repository }}/compare/${PREV_TAG}...${TAG}) -### Release notes -$(gh api "repos/${{ github.repository }}/releases/tags/${TAG}" --jq '.body // "No release notes provided."') + ### Release notes + $(gh api "repos/${{ github.repository }}/releases/tags/${TAG}" --jq '.body // "No release notes provided."') -### Suggested post structure -1. **Summary** β€” what this release delivers in 2–3 sentences -2. **What's New** β€” feature sections with code snippets and context -3. **Bug Fixes / Improvements** β€” notable fixes -4. **How to Upgrade** β€” any steps required (migrations, config changes) -5. **Stats** β€” test counts, coverage numbers, PR count -6. **What's Next** β€” upcoming work + ### Suggested post structure + 1. **Summary** β€” what this release delivers in 2–3 sentences + 2. **What's New** β€” feature sections with code snippets and context + 3. **Bug Fixes / Improvements** β€” notable fixes + 4. **How to Upgrade** β€” any steps required (migrations, config changes) + 5. **Stats** β€” test counts, coverage numbers, PR count + 6. **What's Next** β€” upcoming work -### Instructions -- Write the post to \`docs/blog/${PUBLISH_DATE}-release-${SLUG}.md\` -- Update \`docs/blog/index.md\` to add the new post at the top of the Recent Posts table -- Follow existing post style (see \`docs/blog/2026-04-04-release-v0-7-0.md\`) -- Open a PR targeting \`main\` when complete -- Close this issue in the PR description with \`Closes #\`" + ### Instructions + - Write the post to \`docs/blog/${PUBLISH_DATE}-release-${SLUG}.md\` + - Update \`docs/blog/index.md\` to add the new post at the top of the Recent Posts table + - Follow existing post style (see \`docs/blog/2026-04-04-release-v0-7-0.md\`) + - Open a PR targeting \`main\` when complete + - Close this issue in the PR description with \`Closes #\`" gh issue create \ --title "Blog post: ${RELEASE_NAME} β€” release recap" \ diff --git a/README.md b/README.md index 53364edb..0c6afefe 100644 --- a/README.md +++ b/README.md @@ -4,56 +4,82 @@ A hands-on learning project for .NET Aspire orchestration, Blazor Server renderi [![.NET 10](https://img.shields.io/badge/.NET-10-512BD4?logo=dotnet)](https://dotnet.microsoft.com/) [![MIT License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) +[![xUnit Tests](https://img.shields.io/badge/Tests-xUnit-blueviolet?logo=github)](https://github.com/mpaulosky/MyBlog/actions/workflows/squad-ci.yml) +[![Latest Release](https://img.shields.io/github/v/release/mpaulosky/MyBlog?logo=github&color=blue&label=Release)](https://github.com/mpaulosky/MyBlog/releases/latest) + +[![CI/CD](https://github.com/mpaulosky/MyBlog/actions/workflows/squad-ci.yml/badge.svg)](https://github.com/mpaulosky/MyBlog/actions/workflows/squad-ci.yml) +[![Test Suite](https://github.com/mpaulosky/MyBlog/actions/workflows/squad-test.yml/badge.svg)](https://github.com/mpaulosky/MyBlog/actions/workflows/squad-test.yml) + +[![CodeCov Coverage](https://codecov.io/gh/mpaulosky/MyBlog/branch/main/graph/badge.svg)](https://codecov.io/gh/mpaulosky/MyBlog) +[![Coverage Trend](https://img.shields.io/badge/Coverage-Trend-blue?logo=codecov)](https://codecov.io/gh/mpaulosky/MyBlog/commits/main) +[![Coverage Gate](https://img.shields.io/badge/Coverage%20Gate-β‰₯80%25-brightgreen?logo=codecov)](https://github.com/mpaulosky/MyBlog/actions/workflows/squad-test.yml) + +[![Open Issues](https://img.shields.io/github/issues/mpaulosky/MyBlog?color=0366d6)](https://github.com/mpaulosky/MyBlog/issues?q=is%3Aopen+is%3Aissue) +[![Closed Issues](https://img.shields.io/github/issues-closed/mpaulosky/MyBlog?color=6f42c1)](https://github.com/mpaulosky/MyBlog/issues?q=is%3Aclosed+is%3Aissue) +[![Open PRs](https://img.shields.io/github/issues-pr/mpaulosky/MyBlog?color=28a745)](https://github.com/mpaulosky/MyBlog/pulls?q=is%3Aopen+is%3Apr) +[![Closed PRs](https://img.shields.io/github/issues-pr-closed/mpaulosky/MyBlog?color=6f42c1)](https://github.com/mpaulosky/MyBlog/pulls?q=is%3Aclosed+is%3Apr) > ⚠️ **Training Project** β€” This is a learning exercise focused on .NET practices, not a production application. ## Overview -MyBlog is a simple blog application demonstrating core .NET concepts: .NET Aspire orchestration, Blazor Server for interactive server rendering, clean architecture with domain/web layer separation, and test-driven development. +MyBlog is a Blazor Server blog application demonstrating modern .NET patterns: Aspire orchestration, CQRS with MediatR, MongoDB persistence, Redis distributed caching, Auth0 authentication, TailwindCSS v4 theming, and comprehensive testing (unit, architecture, integration, bUnit, E2E). ## Features -- **List Blog Posts** β€” View all published and draft posts with metadata -- **Create Posts** β€” New posts start in draft status -- **Edit Posts** β€” Update title and content anytime -- **Delete with Confirmation** β€” ConfirmDeleteDialog component prevents accidental deletions -- **Publish/Unpublish** β€” Toggle post publication status +- **Blog Management**: Create, edit, delete, publish/unpublish blog posts +- **CQRS with MediatR**: All blog operations handled by MediatR command/query handlers with FluentValidation pipeline +- **Redis Caching**: L1 (in-memory) + L2 (Redis distributed) cache via `IBlogPostCacheService` abstraction +- **Auth0 Authentication**: Secure login with Authorization Code + PKCE flow; Role-Based Authorization (Admin/User policies) +- **Blazor TailwindCSS Theming**: Dark/light/system modes, 4 color schemes (Blue, Red, Green, Yellow), localStorage persistence +- **Aspire Orchestration**: MongoDB + Redis as Aspire resources with health checks and OpenTelemetry ## Technology Stack - **.NET 10** with **C# 14** -- **.NET Aspire 13.2.2** β€” Service orchestration and health checks -- **Blazor Server (Interactive Server Rendering)** β€” Dynamic UI -- **xUnit** β€” Test framework -- **FluentAssertions** β€” Assertion library +- **.NET Aspire 13.2.2** β€” Service orchestration, MongoDB + Redis resources, health checks +- **Blazor Server (Interactive Server Rendering)** β€” Dynamic UI with TailwindCSS v4 theming +- **MongoDB** with **MongoDB.EntityFrameworkCore** β€” Blog post persistence +- **Redis** β€” Distributed caching (L2 cache via `IDistributedCache`) +- **MediatR** β€” CQRS pattern (Create/Update/Delete/GetAll/GetById handlers) +- **FluentValidation** β€” Validation pipeline behavior +- **Auth0** β€” Authentication + role-based authorization +- **xUnit** + **FluentAssertions** + **NSubstitute** β€” Test framework +- **bUnit** β€” Blazor component testing - **NetArchTest.Rules** β€” Architecture validation -- **NSubstitute** β€” Mocking framework -- **In-Memory Repository** β€” No database (training project by design) ## Project Structure ``` MyBlog/ β”œβ”€β”€ src/ -β”‚ β”œβ”€β”€ AppHost/ # .NET Aspire orchestration entry point -β”‚ β”œβ”€β”€ Domain/ # BlogPost entity, IBlogPostRepository, InMemoryBlogPostRepository -β”‚ β”œβ”€β”€ ServiceDefaults/ # Shared Aspire concerns (OpenTelemetry, health checks) +β”‚ β”œβ”€β”€ AppHost/ # .NET Aspire orchestration (MongoDB + Redis resources) +β”‚ β”œβ”€β”€ Domain/ # BlogPost entity, MediatR handlers, validators, IBlogPostCacheService +β”‚ β”‚ β”œβ”€β”€ Abstractions/ # Result, IBlogPostRepository, IBlogPostCacheService +β”‚ β”‚ β”œβ”€β”€ Behaviors/ # ValidationBehavior MediatR pipeline +β”‚ β”‚ β”œβ”€β”€ Entities/ # BlogPost domain entity +β”‚ β”‚ └── Interfaces/ # Repository and cache interfaces +β”‚ β”œβ”€β”€ ServiceDefaults/ # OpenTelemetry, health checks, Aspire extensions β”‚ └── Web/ # Blazor Server application -β”‚ └── Components/ -β”‚ β”œβ”€β”€ Pages/BlogPosts/ # Index, Create, Edit pages -β”‚ β”œβ”€β”€ Pages/ # Home, Error, NotFound -β”‚ β”œβ”€β”€ Shared/ # ConfirmDeleteDialog -β”‚ └── Layout/ # MainLayout, NavMenu, ReconnectModal +β”‚ β”œβ”€β”€ Features/BlogPosts/ # Vertical slice: commands, queries, Blazor pages +β”‚ β”œβ”€β”€ Infrastructure/Caching/ # BlogPostCacheService (L1+L2 implementation) +β”‚ β”œβ”€β”€ Components/Theme/ # TailwindCSS theme components +β”‚ β”œβ”€β”€ Security/ # Auth0 endpoints +β”‚ └── Styles/ # TailwindCSS input.css β”œβ”€β”€ tests/ -β”‚ β”œβ”€β”€ Unit.Tests/ # 7 unit tests (BlogPost, Repository) -β”‚ β”œβ”€β”€ Architecture.Tests/ # 2 architecture tests (layer rules) -β”‚ └── Integration.Tests/ # Stubbed for future Aspire integration -β”œβ”€β”€ docs/ # Documentation +β”‚ β”œβ”€β”€ Unit.Tests/ # Domain entity + handler unit tests +β”‚ β”œβ”€β”€ Architecture.Tests/ # Layer dependency enforcement +β”‚ β”œβ”€β”€ Integration.Tests/ # Aspire integration tests +β”‚ β”œβ”€β”€ AppHost.Tests/ # Aspire AppHost + E2E tests +β”‚ β”œβ”€β”€ Web.Tests/ # Web layer unit tests +β”‚ β”œβ”€β”€ Web.Tests.Bunit/ # Blazor component tests (bUnit) +β”‚ └── Web.Tests.Integration/# Web integration tests +β”œβ”€β”€ docs/ # Documentation + GitHub Pages blog β”œβ”€β”€ Directory.Build.props # Centralized build settings -β”œβ”€β”€ global.json # SDK version lock -β”œβ”€β”€ GitVersion.yml # Versioning config -β”œβ”€β”€ MyBlog.slnx # Solution file -└── README.md +β”œβ”€β”€ Directory.Packages.props # Centralized NuGet versioning (CPM) +β”œβ”€β”€ global.json # SDK version lock (.NET 10.0.202) +β”œβ”€β”€ GitVersion.yml # Semantic versioning config +└── MyBlog.slnx # Solution file ``` ## Getting Started @@ -61,7 +87,10 @@ MyBlog/ ### Prerequisites - **.NET 10 SDK** β€” [Download](https://dotnet.microsoft.com/en-us/download) -- **Auth0 account** β€” required for authentication (free tier works). See [AUTH0_SETUP.md](docs/AUTH0_SETUP.md) for full setup instructions. +- **Node.js 18+** β€” for TailwindCSS compilation +- **Auth0 account** β€” required for authentication (free tier). See [AUTH0_SETUP.md](docs/AUTH0_SETUP.md) +- **MongoDB** β€” MongoDB Atlas or local instance (or let Aspire provision via Docker) +- **Redis** β€” Redis instance (or let Aspire provision via Docker) ### Setup @@ -78,19 +107,25 @@ MyBlog/ dotnet restore ``` -3. **Build the solution** +3. **Install npm dependencies** (for TailwindCSS) + + ```bash + npm install + ``` + +4. **Build the solution** ```bash dotnet build ``` -4. **Run tests** +5. **Run tests** ```bash dotnet test ``` -5. **Run the application** (via Aspire AppHost) +6. **Run the application** (via Aspire AppHost) ```bash cd src/AppHost @@ -101,11 +136,15 @@ MyBlog/ ## Testing -All 9 tests pass. Test structure: +Multiple test tiers, all passing: -- **Unit.Tests/** β€” Entity logic and repository behavior (7 tests) -- **Architecture.Tests/** β€” Layer dependency enforcement (2 tests) -- **Integration.Tests/** β€” Placeholder for future Aspire integration tests +- **Unit.Tests** β€” Domain entity logic and MediatR handler behavior +- **Architecture.Tests** β€” Layer dependency enforcement +- **Integration.Tests** β€” Aspire integration tests +- **AppHost.Tests** β€” .NET Aspire host + E2E tests +- **Web.Tests** β€” Web layer unit tests +- **Web.Tests.Bunit** β€” Blazor component tests (bUnit) +- **Web.Tests.Integration** β€” Web integration tests Run all tests: @@ -113,22 +152,25 @@ Run all tests: dotnet test ``` -## Learning Objectives - -This project teaches: - -1. **.NET Aspire** β€” Service orchestration, resource composition, health checks -2. **Blazor Server** β€” Interactive server rendering, component models, form handling -3. **Clean Architecture** β€” Domain/Web layer separation, repository pattern -4. **Test-Driven Development** β€” Unit tests, architecture tests, xUnit/FluentAssertions -5. **Entity Design** β€” Factory methods, immutable updates, domain logic -6. **Short Naming** β€” Repository-level context (MyBlog) vs project names (AppHost, Domain, Web) - ## Documentation - [ARCHITECTURE.md](docs/ARCHITECTURE.md) β€” Solution structure, layer diagram, design decisions - [CONTRIBUTING.md](docs/CONTRIBUTING.md) β€” Contribution guidelines and project setup - [AUTH0_SETUP.md](docs/AUTH0_SETUP.md) β€” Step-by-step Auth0 configuration for local development +- [TESTING.md](docs/TESTING.md) β€” Test strategy and running instructions +- [THEMING.md](docs/THEMING.md) β€” TailwindCSS theme system guide +- [SECURITY.md](docs/SECURITY.md) β€” Security guidelines +- [CODE_OF_CONDUCT.md](docs/CODE_OF_CONDUCT.md) β€” Community standards +- [REFERENCES.md](docs/REFERENCES.md) β€” NuGet and npm package references + +## Release History + +| Version | Date | Highlights | +|---------|------|------------| +| [v1.2.0](https://github.com/mpaulosky/MyBlog/releases/tag/v1.2.0) | 2026-04-24 | **Redis & Caching** β€” IBlogPostCacheService L1+L2, handler cache integration | +| [v1.1.0](https://github.com/mpaulosky/MyBlog/releases/tag/v1.1.0) | 2026-04-24 | **Themes & Testing** β€” TailwindCSS v4 themes, test project reorganization, E2E fixes | +| [v1.0.1](https://github.com/mpaulosky/MyBlog/releases/tag/v1.0.1) | 2026-04-20 | Automatic semver versioning enabled | +| [v1.0.0](https://github.com/mpaulosky/MyBlog/releases/tag/v1.0.0) | 2026-04-20 | First semantic version release | ## License @@ -136,4 +178,4 @@ Licensed under the MIT License. See [LICENSE](LICENSE) file for details. --- -**Status**: Training Project | **.NET 10** | **Maintained by**: @mpaulosky +**Status**: Training Project | **.NET 10** | **v1.2.0** | **Maintained by**: @mpaulosky diff --git a/docs/blog/2026-04-18-myblog-project-kickoff.md b/docs/blog/2026-04-18-myblog-project-kickoff.md new file mode 100644 index 00000000..31bba3ed --- /dev/null +++ b/docs/blog/2026-04-18-myblog-project-kickoff.md @@ -0,0 +1,69 @@ +--- +title: "MyBlog Project Kickoff: Building with .NET 10 and Clean Architecture" +date: 2026-04-18 +author: Bilbo +tags: [aspire, blazor, clean-architecture, sprint-1] +summary: "The MyBlog project launches with a solid foundation: Blazor Server, .NET Aspire, and clean architectureβ€”ready for modern web development." +--- + +## Summary + +MyBlog begins with a strong architectural foundation. Built on the Blazor Server template, the project spans five well-organized projects: AppHost for orchestration, Domain for business logic, ServiceDefaults for shared infrastructure, and Web for the UI layer. Sprint 1 focused on removing template clutter, adding proper copyright headers, and establishing the first data model with in-memory persistence. The suite passes 74 tests across Architecture, Unit, and Integration layers. + +## Context + +When starting a new project, the first decisions matter. We chose: +- **Blazor Server** for rapid, interactive web development with C# +- **.NET 10** and **.NET Aspire 13.2.2** for modern, cloud-native capabilities +- **Clean architecture** with vertical slices to keep code organized and testable as the project grows +- **In-memory persistence** initially to keep things simple while we establish patterns + +This foundation lets us move fast without technical debt. + +## Key Details + +### Project Structure +``` +MyBlog/ +β”œβ”€β”€ AppHost/ # .NET Aspire orchestration +β”œβ”€β”€ Domain/ # Business logic + entities +β”œβ”€β”€ ServiceDefaults/ # Shared configs +β”œβ”€β”€ Web/ # Blazor Server UI +└── Tests/ + β”œβ”€β”€ Architecture.Tests/ + β”œβ”€β”€ Unit.Tests/ + └── Integration.Tests/ +``` + +### Early PRs +- **PR #6:** Removed demo Blazor components (Counter, Weather) +- **PR #7:** Added MIT license headers to all C# files + +### Data Model +The `BlogPost` entity is simple but functional: +```csharp +public class BlogPost +{ + public int Id { get; set; } + public string Title { get; set; } + public string Content { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } +} +``` + +With an in-memory repository backing the initial feature set. + +### Test Coverage +- **74 tests passing** across three test projects +- Architecture tests enforce layer dependencies +- Unit tests cover business logic +- Integration tests verify the persistence layer + +## What's Next + +Sprint 2 introduces CQRS and MediatR to establish a robust command/query pattern. The in-memory repository will be joined by validation handlers and a proper vertical slice structure. + +--- + +*Posted by Bilbo β€’ Sprint 1 Foundation* diff --git a/docs/blog/2026-04-20-release-v1-0-0.md b/docs/blog/2026-04-20-release-v1-0-0.md new file mode 100644 index 00000000..b024167d --- /dev/null +++ b/docs/blog/2026-04-20-release-v1-0-0.md @@ -0,0 +1,55 @@ +--- +title: "Release: v1.0.0 β€” Semantic Versioning and Production Readiness" +date: 2026-04-20 +author: Bilbo +tags: [release, semver, ci, devops] +summary: "MyBlog reaches v1.0.0: semantic versioning replaces sprint tags, GitVersion automates releases, and we're production-ready." +--- + +## Summary + +With v1.0.0, MyBlog transitions from sprint-based versioning (v1.0.0-sprint1, v1.0.0-sprint2, v1.0.0-sprint3) to pure semantic versioning. GitVersion integration automates version bumping and tag creation on every main commit, eliminating manual release ceremonies. This is our first true semantic release and marks project stability. + +## Context + +Sprint tags served as proof-of-concept milestones, but semantic versioning is the industry standard for production software. By integrating GitVersion: +- **Automated versioning**: Git commit messages (using conventional commits) drive version bumps +- **Reproducible releases**: Every build on main automatically gets a unique version +- **Clear communication**: MAJOR.MINOR.PATCH clearly signals compatibility +- **CI integration**: GitHub Actions creates tags and releases automatically + +This removes friction from release workflows and aligns us with modern DevOps practices. + +## Key Details + +### GitVersion Configuration +```yaml +mode: Mainline +increment: Patch +``` + +Commits to main trigger: +- Automatic version calculation +- Git tag creation +- GitHub Release publication + +### Version Scheme +- **MAJOR** (breaking changes): X.0.0 +- **MINOR** (features, non-breaking): 1.Y.0 +- **PATCH** (bug fixes, docs): 1.0.Z + +### What v1.0.0 Includes +Three complete sprints of work: +- βœ“ Clean architecture foundation +- βœ“ CQRS/MediatR pattern +- βœ“ E2E testing with .NET Aspire +- βœ“ CI/CD automation +- βœ“ 74+ tests passing + +## What's Next + +v1.1.0 (Sprint 4) introduces Blazor theming with TailwindCSS v4 and reorganizes test infrastructure. + +--- + +*Posted by Bilbo β€’ Release Milestone | v1.0.0* diff --git a/docs/blog/2026-04-20-sprint-2-cqrs-mediatr.md b/docs/blog/2026-04-20-sprint-2-cqrs-mediatr.md new file mode 100644 index 00000000..c16f0fd7 --- /dev/null +++ b/docs/blog/2026-04-20-sprint-2-cqrs-mediatr.md @@ -0,0 +1,87 @@ +--- +title: "Sprint 2: CQRS and MediatR Deep Dive" +date: 2026-04-20 +author: Bilbo +tags: [cqrs, mediatr, testing, domain, sprint-2] +summary: "Sprint 2 establishes command-query separation and MediatR handlers for clean, testable domain logic." +--- + +## Summary + +Sprint 2 brings the CQRS (Command Query Responsibility Segregation) pattern to life with five fully-tested MediatR handlers for BlogPost operations. We added FluentValidation integration through a ValidationBehavior pipeline, restructured Domain as vertical slices, and established the squad governance workflow. The test suite grew by 13 new tests, achieving β‰₯89% coverage. Release v1.0.0-sprint2 marks our first semantic versioning milestone. + +## Context + +CQRS separates reads from writes, making our code easier to reason about and optimize independently. MediatR provides the dispatch mechanism. Combined with FluentValidation, we get: +- **Clear intent**: Commands for mutations, Queries for reads +- **Automatic validation**: Pipeline behaviors intercept and validate before handlers execute +- **Testability**: Each handler can be tested in isolation +- **Scalability**: Later, we can add caching, logging, or authorization as behaviors without touching handlers + +This is the backbone of maintainable domain logic. + +## Key Details + +### Five MediatR Handlers +``` +Domain/Features/BlogPosts/ +β”œβ”€β”€ Commands/ +β”‚ β”œβ”€β”€ CreateBlogPostCommand.cs +β”‚ β”œβ”€β”€ UpdateBlogPostCommand.cs +β”‚ └── DeleteBlogPostCommand.cs +β”œβ”€β”€ Queries/ +β”‚ β”œβ”€β”€ GetAllBlogPostsQuery.cs +β”‚ └── GetBlogPostByIdQuery.cs +└── Handlers/ + β”œβ”€β”€ CreateBlogPostCommandHandler.cs + β”œβ”€β”€ UpdateBlogPostCommandHandler.cs + β”œβ”€β”€ DeleteBlogPostCommandHandler.cs + β”œβ”€β”€ GetAllBlogPostsQueryHandler.cs + └── GetBlogPostByIdQueryHandler.cs +``` + +### ValidationBehavior Pipeline +```csharp +public class ValidationBehavior : IPipelineBehavior + where TRequest : IRequest +{ + private readonly IEnumerable> _validators; + + public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + { + var context = new ValidationContext(request); + var failures = (await Task.WhenAll( + _validators.Select(v => v.ValidateAsync(context, cancellationToken)))) + .SelectMany(result => result.Errors) + .Where(failure => failure != null) + .ToList(); + + if (failures.Any()) + throw new ValidationException(failures); + + return await next(); + } +} +``` + +### Test Expansion +- **13 new tests** added for command and query handlers +- Validation tests ensure bad data is caught before persistence +- Handler tests verify correct output and state changes + +### Squad Governance Milestone +We established the **no-code-before-issue** gate: +- Every feature starts as a GitHub issue +- Squad members label and discuss design +- PRs must reference the issue +- Reviews ensure quality before merge + +**Related PRs:** #52, #53, #54, #55, #57, #58 + +## What's Next + +Sprint 3 adds end-to-end testing with xUnit and .NET Aspire, plus CI hardening for the squad workflow. + +--- + +*Posted by Bilbo β€’ Sprint 2 | Release: v1.0.0-sprint2* diff --git a/docs/blog/2026-04-20-sprint-3-e2e-tests-ci-hardening.md b/docs/blog/2026-04-20-sprint-3-e2e-tests-ci-hardening.md new file mode 100644 index 00000000..29c1f439 --- /dev/null +++ b/docs/blog/2026-04-20-sprint-3-e2e-tests-ci-hardening.md @@ -0,0 +1,70 @@ +--- +title: "Sprint 3: E2E Testing and CI Hardening" +date: 2026-04-20 +author: Bilbo +tags: [e2e, aspire, ci, testing, sprint-3] +summary: "Sprint 3 adds end-to-end testing with .NET Aspire xUnit and hardens the CI pipeline for squad branch workflows." +--- + +## Summary + +Sprint 3 strengthens quality and automation. We added the E2E.Tests project using .NET Aspire's xUnit integration, introduced visual distinction via Admin badge styling and RoleClaimsHelper, and hardened CI to enforce squad branch naming on sprint/* branches. Test artifacts now upload to CI runs, GitHub project board automation moves issues to Released, and the pre-push Gate 0 validates branch names. Release v1.0.0-sprint3 marks our first production-ready milestone. + +## Context + +As the project grows, we need multiple testing layers: +- **Unit tests** verify business logic in isolation +- **Integration tests** check persistence and service wiring +- **E2E tests** simulate real user workflows in a live environment + +.NET Aspire provides the orchestration context for E2E tests, spinning up the full application and letting xUnit run scenarios end-to-end. CI automation ensures consistencyβ€”artifact uploads let us inspect test results post-run, and branch name validation prevents accidental merges to main. + +## Key Details + +### E2E Testing with .NET Aspire +The E2E.Tests project uses AppHost's resource definitions to spin up the application: +```csharp +[TestClass] +public class BlogPostE2ETests +{ + private readonly AspireTestExtension _aspire = new(); + + [TestMethod] + public async Task GetBlogPosts_ReturnsAllPosts() + { + var httpClient = _aspire.CreateHttpClient("webfrontend"); + var response = await httpClient.GetAsync("/api/blog-posts"); + Assert.IsTrue(response.IsSuccessStatusCode); + } +} +``` + +### UI Enhancements +- **Admin Badge**: Visual indicator for admin users in profile +- **Role-Colored Badges**: Different colors for different roles (admin, editor, reader) +- **RoleClaimsHelper**: Utility to safely parse and validate claims + +**Related PR:** #79 + +### CI/CD Hardening + +#### Squad CI on Sprint Branches +- Enabled squad-test CI workflow on sprint/* branches (PR #70) +- Test results artifact uploads (PR #75) + +#### Pre-Push Gate Validation +- Branch naming enforcement via Gate 0 (PRs #74, #78) +- Prevents commits to main; validates sprint/* naming + +#### GitHub Project Automation +- Issues automatically move to "Released" column on GitHub Release publish (PR #76) + +**Related PRs:** #70, #74, #75, #76, #77, #78, #79 + +## What's Next + +Sprint 4 introduces Blazor theme system with TailwindCSS v4 and reorganizes test infrastructure into focused, maintainable layers. + +--- + +*Posted by Bilbo β€’ Sprint 3 | Release: v1.0.0-sprint3* diff --git a/docs/blog/2026-04-24-release-v1-1-0.md b/docs/blog/2026-04-24-release-v1-1-0.md new file mode 100644 index 00000000..8e92c4c0 --- /dev/null +++ b/docs/blog/2026-04-24-release-v1-1-0.md @@ -0,0 +1,79 @@ +--- +title: "Release: v1.1.0 β€” Blazor Theme System with TailwindCSS v4" +date: 2026-04-24 +author: Bilbo +tags: [release, v1.1.0, blazor, tailwind, theme, testing, sprint-4] +summary: "v1.1.0 ships a complete theme system with light/dark/system modes, four color schemes, and reorganized test infrastructure." +--- + +## Summary + +Sprint 4 delivers a sophisticated theme system for Blazor with TailwindCSS v4. The `ThemeProvider` cascading parameter exposes theme state, `ThemeToggle` component lets users switch between light, dark, and system modes, and four color schemes (Blue, Red, Green, Yellow) provide visual variety. Settings persist to localStorage. Parallel work reorganized the test suite into focused layers: Web.Tests, Web.Tests.Bunit, Web.Tests.Integration, and AppHost.Tests. Seven flaky E2E tests are now stable. Release v1.1.0 is live at [github.com/mpaulosky/MyBlog/releases/tag/v1.1.0](https://github.com/mpaulosky/MyBlog/releases/tag/v1.1.0). + +## Context + +User experience is defined by design and polish. A theme system gives developers and users: +- **Accessibility**: Respects system dark mode preferences +- **Personalization**: Users choose their color palette +- **Consistency**: Cascading parameters propagate theme state without prop drilling +- **Persistence**: localStorage keeps preferences across sessions + +Well-organized tests make future changes confident. Separating E2E, unit, Blazor component, and integration tests lets us run only what matters for a given change. + +## Key Details + +### Theme System Architecture +```csharp +[CascadingParameter] +public ThemeProvider ThemeProvider { get; set; } + +public class ThemeProvider +{ + public string Mode { get; set; } // light, dark, system + public string ColorScheme { get; set; } // blue, red, green, yellow + public void SetMode(string mode) => /* update + persist */ + public void SetColorScheme(string scheme) => /* update + persist */ +} +``` + +### ThemeToggle Component +```razor +
+ + + + + +
+``` + +### Color Schemes +- **Blue**: Professional, calming (primary) +- **Red**: Energy, urgency (alerts, warnings) +- **Green**: Growth, success (confirmations) +- **Yellow**: Caution, attention (highlights) + +Each scheme adapts across light and dark modes. + +### Test Infrastructure +- **Web.Tests**: .NET unit tests for services +- **Web.Tests.Bunit**: Blazor component tests with bUnit +- **Web.Tests.Integration**: Full-stack integration scenarios +- **AppHost.Tests**: E2E orchestration tests + +Fixed 7 flaky E2E tests by improving retry logic and timeout handling. + +**Related PRs:** #94, #95, #98, #100, #102, #104 + +## What's Next + +v1.2.0 (Sprint 5) adds Redis caching with IBlogPostCacheService and L1/L2 cache layers. + +--- + +*Posted by Bilbo β€’ Sprint 4 | Release: v1.1.0* diff --git a/docs/blog/2026-04-24-release-v1-2-0.md b/docs/blog/2026-04-24-release-v1-2-0.md new file mode 100644 index 00000000..a9121b55 --- /dev/null +++ b/docs/blog/2026-04-24-release-v1-2-0.md @@ -0,0 +1,129 @@ +--- +title: "Release: v1.2.0 β€” Redis Caching and L1/L2 Cache Strategy" +date: 2026-04-24 +author: Bilbo +tags: [release, v1.2.0, redis, caching, aspire, sprint-5] +summary: "v1.2.0 introduces intelligent caching with Redis: L1 in-memory, L2 distributed, with automatic invalidation on mutations." +--- + +## Summary + +Sprint 5 ships a production-grade caching strategy. The new `IBlogPostCacheService` abstraction provides two-tier caching: L1 uses `IMemoryCache` for ultra-fast local access, L2 uses `IDistributedCache` with Redis for cross-instance consistency. All CQRS query handlers (`GetAllBlogPostsQueryHandler`, `GetBlogPostByIdQueryHandler`) are now cache-aware, and mutations (Create/Update/Delete) automatically invalidate affected cache entries. Blazor components sport new bUnit tests. CI automation now moves issues to the "Released" column when a GitHub Release is published. Release v1.2.0 is live at [github.com/mpaulosky/MyBlog/releases/tag/v1.2.0](https://github.com/mpaulosky/MyBlog/releases/tag/v1.2.0). + +## Context + +Caching transforms performance. Without it, every read hits the database. With caching: +- **L1 (IMemoryCache)**: Sub-millisecond access, local to each server instance +- **L2 (Redis)**: Millisecond access, shared across instances +- **Invalidation**: When a BlogPost is modified, we clear the cache, so the next query fetches fresh data + +This architecture scales horizontallyβ€”add servers, they all benefit from Redis. + +## Key Details + +### IBlogPostCacheService +```csharp +public interface IBlogPostCacheService +{ + Task> GetAllBlogPostsAsync(CancellationToken cancellationToken); + Task GetBlogPostByIdAsync(int id, CancellationToken cancellationToken); + Task InvalidateAsync(int? blogPostId = null); +} +``` + +### Two-Tier Implementation +```csharp +public class BlogPostCacheService : IBlogPostCacheService +{ + private readonly IMemoryCache _l1Cache; + private readonly IDistributedCache _l2Cache; + private readonly IBlogPostRepository _repository; + + public async Task> GetAllBlogPostsAsync(CancellationToken cancellationToken) + { + const string key = "all_blog_posts"; + + // Check L1 + if (_l1Cache.TryGetValue(key, out var cached)) + return cached as IEnumerable; + + // Check L2 + var json = await _l2Cache.GetStringAsync(key, cancellationToken); + if (json != null) + { + var result = JsonSerializer.Deserialize>(json); + _l1Cache.Set(key, result, TimeSpan.FromHours(1)); + return result; + } + + // Fetch from DB, populate both caches + var posts = await _repository.GetAllAsync(cancellationToken); + var response = posts.Select(p => new BlogPostResponse { /* ... */ }); + var json = JsonSerializer.Serialize(response); + await _l2Cache.SetStringAsync(key, json, new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1) + }, cancellationToken); + _l1Cache.Set(key, response, TimeSpan.FromHours(1)); + + return response; + } +} +``` + +### Query Handler Integration +```csharp +public class GetAllBlogPostsQueryHandler : IRequestHandler> +{ + private readonly IBlogPostCacheService _cacheService; + + public async Task> Handle(GetAllBlogPostsQuery request, CancellationToken cancellationToken) + { + return await _cacheService.GetAllBlogPostsAsync(cancellationToken); + } +} +``` + +### Invalidation on Mutation +```csharp +public class CreateBlogPostCommandHandler : IRequestHandler +{ + private readonly IBlogPostRepository _repository; + private readonly IBlogPostCacheService _cacheService; + + public async Task Handle(CreateBlogPostCommand request, CancellationToken cancellationToken) + { + var entity = await _repository.CreateAsync(/* ... */, cancellationToken); + await _cacheService.InvalidateAsync(); // Clear all blog post caches + return MapToResponse(entity); + } +} +``` + +### Blazor Component Tests with bUnit +```csharp +[TestClass] +public class ThemeToggleTests +{ + [TestMethod] + public void ThemeToggle_RendersSwitchButtons() + { + using var ctx = new TestContext(); + var cut = ctx.RenderComponent(); + + cut.FindAll("button").Count.Should().Be(3); + } +} +``` + +### CI Automation +- Issues in "In Progress" move to "Released" when a GitHub Release is published +- Automated via GitHub Actions workflow + +## What's Next + +Sprint 6 explores distributed tracing with OpenTelemetry and performance profiling. + +--- + +*Posted by Bilbo β€’ Sprint 5 | Release: v1.2.0* diff --git a/docs/blog/index.md b/docs/blog/index.md new file mode 100644 index 00000000..12075877 --- /dev/null +++ b/docs/blog/index.md @@ -0,0 +1,47 @@ +# MyBlog Development Blog + +Welcome to the MyBlog technical blog! Here we document the project's evolutionβ€”from architecture decisions and feature releases to testing strategies and DevOps automation. Each post captures what we built, why it matters, and what's coming next. + +## Latest Posts + +| Date | Title | Tags | +|------|-------|------| +| 2026-04-24 | [Release: v1.2.0 β€” Redis Caching and L1/L2 Cache Strategy](./2026-04-24-release-v1-2-0.md) | release, v1.2.0, redis, caching, aspire, sprint-5 | +| 2026-04-24 | [Release: v1.1.0 β€” Blazor Theme System with TailwindCSS v4](./2026-04-24-release-v1-1-0.md) | release, v1.1.0, blazor, tailwind, theme, testing, sprint-4 | +| 2026-04-20 | [Release: v1.0.0 β€” Semantic Versioning and Production Readiness](./2026-04-20-release-v1-0-0.md) | release, semver, ci, devops | +| 2026-04-20 | [Sprint 3: E2E Testing and CI Hardening](./2026-04-20-sprint-3-e2e-tests-ci-hardening.md) | e2e, aspire, ci, testing, sprint-3 | +| 2026-04-20 | [Sprint 2: CQRS and MediatR Deep Dive](./2026-04-20-sprint-2-cqrs-mediatr.md) | cqrs, mediatr, testing, domain, sprint-2 | +| 2026-04-18 | [MyBlog Project Kickoff: Building with .NET 10 and Clean Architecture](./2026-04-18-myblog-project-kickoff.md) | aspire, blazor, clean-architecture, sprint-1 | + +## By Sprint + +### Sprint 1: Foundation +- [MyBlog Project Kickoff: Building with .NET 10 and Clean Architecture](./2026-04-18-myblog-project-kickoff.md) + +### Sprint 2: CQRS & MediatR +- [Sprint 2: CQRS and MediatR Deep Dive](./2026-04-20-sprint-2-cqrs-mediatr.md) + +### Sprint 3: Testing & CI +- [Sprint 3: E2E Testing and CI Hardening](./2026-04-20-sprint-3-e2e-tests-ci-hardening.md) + +### Sprint 4: Theme System +- [Release: v1.1.0 β€” Blazor Theme System with TailwindCSS v4](./2026-04-24-release-v1-1-0.md) + +### Sprint 5: Caching +- [Release: v1.2.0 β€” Redis Caching and L1/L2 Cache Strategy](./2026-04-24-release-v1-2-0.md) + +## Release Timeline + +| Release | Date | Focus | +|---------|------|-------| +| [v1.0.0](./2026-04-20-release-v1-0-0.md) | 2026-04-20 | Semantic versioning, production readiness | +| [v1.1.0](./2026-04-24-release-v1-1-0.md) | 2026-04-24 | Blazor theme system, test reorganization | +| [v1.2.0](./2026-04-24-release-v1-2-0.md) | 2026-04-24 | Redis caching, distributed cache strategy | + +## About This Blog + +Written by **Bilbo**, the MyBlog Tech Blogger. Posts document architecture, features, testing, and DevOps. For pull requests, decisions, and code, see the [GitHub repository](https://github.com/mpaulosky/MyBlog). + +--- + +Last updated: 2026-04-24 diff --git a/docs/index.html b/docs/index.html index 1c021fc5..a68cc9af 100644 --- a/docs/index.html +++ b/docs/index.html @@ -4,7 +4,7 @@ MyBlog - +