From 1b289c13879c3c2a1f04c6c6c36ff5701c256ad1 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Tue, 18 Aug 2026 00:20:01 +0100 Subject: [PATCH] refactor(model): rename Yavsc.Blogspot.BlogPost to BlogPostDto MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When commit 0e95e283 moved BlogPost from PostIt.Models to Yavsc.Blogspot, it created an unfortunate collision with the server-side EF entity Yavsc.Models.Blog.BlogPost. The two classes have nothing in common beyond the name; the DTO is the wire shape PostIt exchanges with the Blogs API, the EF entity is the persistence model. Server code that imports both namespaces (BlogSpotService.cs, etc.) ended up with 'BlogPost is an ambiguous reference between X and Y' errors. Renaming the client DTO to BlogPostDto (matching the naming convention of the other DTOs in Yavsc.Api.Client.Dtos — CircleDto, CircleAuthorizationDto, UserSearchResultDto) disambiguates without renaming the EF entity on the server. The namespace stays Yavsc.Blogspot; only the class name changes. All call sites (client code, tests, XAML DataTemplates, XML doc comments) are updated mechanically. --- src/PostIt.Tests/BearerScopeTests.cs | 2 +- src/PostIt.Tests/BlogApiTestFakes.cs | 14 ++++++------ src/PostIt.Tests/MainPageSaveTests.cs | 6 ++--- src/PostIt.Tests/PostItViewModelTests.cs | 14 ++++++------ .../PostIt/ViewModels/MainPageViewModel.cs | 22 +++++++++---------- .../ViewModels/PostAclDialogViewModel.cs | 4 ++-- src/PostIt/PostIt/Views/MainPage.axaml | 2 +- src/PostIt/PostIt/Views/MainPage.axaml.cs | 2 +- .../PostIt/Views/PostAclDialog.axaml.cs | 2 +- src/Yavsc.Abstract/Blogspot/BlogPost.cs | 2 +- src/Yavsc.Api.Client/BlogAclApiClient.cs | 2 +- src/Yavsc.Api.Client/BlogApiClient.cs | 14 ++++++------ 12 files changed, 43 insertions(+), 43 deletions(-) diff --git a/src/PostIt.Tests/BearerScopeTests.cs b/src/PostIt.Tests/BearerScopeTests.cs index 1f47a176..fbccb606 100644 --- a/src/PostIt.Tests/BearerScopeTests.cs +++ b/src/PostIt.Tests/BearerScopeTests.cs @@ -97,7 +97,7 @@ public class BearerScopeTests // CapturingHttpHandler is the assertion point. It // records the first request's Authorization header and // returns 200 with an empty array (BlogApiClient - // deserialises to List). + // deserialises to List). var captured = new CapturingHttpHandler(); var client = new YavscApiClient( settings, diff --git a/src/PostIt.Tests/BlogApiTestFakes.cs b/src/PostIt.Tests/BlogApiTestFakes.cs index 3f8b98b0..4b541e42 100644 --- a/src/PostIt.Tests/BlogApiTestFakes.cs +++ b/src/PostIt.Tests/BlogApiTestFakes.cs @@ -18,7 +18,7 @@ internal sealed class CallRecorder /// Test fake that records every CallAsync invocation /// and answers them with a canned sequence: the first call gets -/// a server-issued BlogPost (Id=42), the second call gets a +/// a server-issued BlogPostDto (Id=42), the second call gets a /// single-element list containing that post. Used by the ViewModel /// tests and the headless UI test to capture exactly what the /// Save button posts to the server. @@ -44,20 +44,20 @@ internal sealed class RecordingYavscApiClient : YavscApiClient public override Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) { _recorder.Calls.Add((method, path, body)); - // BlogPost? boxes to BlogPost at runtime, so we test the - // non-nullable type — typeof(BlogPost?) is a C# error + // BlogPostDto? boxes to BlogPostDto at runtime, so we test the + // non-nullable type — typeof(BlogPostDto?) is a C# error // (CS8639: "typeof cannot be used on a nullable reference // type"). - if (typeof(T) == typeof(BlogPost)) - return Task.FromResult((T)(object)new BlogPost + if (typeof(T) == typeof(BlogPostDto)) + return Task.FromResult((T)(object)new BlogPostDto { Id = 42, Title = "Mon premier billet", AuthorId = "tester", Article = "Contenu du billet de test.", }); - if (typeof(T) == typeof(List)) - return Task.FromResult((T)(object)new List + if (typeof(T) == typeof(List)) + return Task.FromResult((T)(object)new List { new() { Id = 42, Title = "Mon premier billet" } }); diff --git a/src/PostIt.Tests/MainPageSaveTests.cs b/src/PostIt.Tests/MainPageSaveTests.cs index 350a71f1..b6bf963a 100644 --- a/src/PostIt.Tests/MainPageSaveTests.cs +++ b/src/PostIt.Tests/MainPageSaveTests.cs @@ -25,7 +25,7 @@ namespace PostIt.Tests; /// in which a brand-new post can be created), the binding has /// no target and the user's keystrokes are silently dropped. /// Clicking "Save" then routes to the VM branch -/// if (SelectedPost is null) { new BlogPost { Title = string.Empty, ... } } +/// if (SelectedPost is null) { new BlogPostDto { Title = string.Empty, ... } } /// which the controller rejects with 400 "The Title field is /// required." This test fails on that branch today and will /// pass once the VM owns a dedicated Title/Article @@ -77,14 +77,14 @@ public class MainPageSaveTests // we inspect the recorder. await Task.Delay(200); - // Assert: the first POST to "blog" carried a BlogPost + // Assert: the first POST to "blog" carried a BlogPostDto // whose Title is exactly what the user typed. The bug // fails this assertion with Title == string.Empty. Assert.NotEmpty(recorder.Calls); var (method, path, body) = recorder.FirstCall; Assert.Equal(HttpMethod.Post, method); Assert.Equal("blog", path); - var sent = Assert.IsType(body); + var sent = Assert.IsType(body); Assert.Equal(typed, sent.Title); } } diff --git a/src/PostIt.Tests/PostItViewModelTests.cs b/src/PostIt.Tests/PostItViewModelTests.cs index b964a18e..2dee4604 100644 --- a/src/PostIt.Tests/PostItViewModelTests.cs +++ b/src/PostIt.Tests/PostItViewModelTests.cs @@ -18,9 +18,9 @@ public class PostItViewModelTests var blog = new BlogApiClient(fakeApi, "http://localhost/"); var viewModel = new MainPageViewModel(blog); - viewModel.Posts.Add(new BlogPost { Id = 1, Title = "First post", Article = "Hello world", AuthorId = "alice" }); - viewModel.Posts.Add(new BlogPost { Id = 2, Title = "Second post", Article = "Nothing here", AuthorId = "bob" }); - viewModel.Posts.Add(new BlogPost { Id = 3, Title = "Third post", Article = "Search me", AuthorId = "carol" }); + viewModel.Posts.Add(new BlogPostDto { Id = 1, Title = "First post", Article = "Hello world", AuthorId = "alice" }); + viewModel.Posts.Add(new BlogPostDto { Id = 2, Title = "Second post", Article = "Nothing here", AuthorId = "bob" }); + viewModel.Posts.Add(new BlogPostDto { Id = 3, Title = "Third post", Article = "Search me", AuthorId = "carol" }); viewModel.SearchText = "search"; viewModel.SearchCommand.Execute(null); @@ -41,7 +41,7 @@ public class PostItViewModelTests // The new BlogApiClient delegates transport to YavscApiClient. // We feed it a fake YavscApiClient that returns the expected // list straight from CallAsync. - var expected = new List + var expected = new List { new() { Id = 1, Title = "Hello" }, new() { Id = 2, Title = "World" } @@ -77,8 +77,8 @@ public class PostItViewModelTests /// Test fake that hands back a canned list of posts from any CallAsync. private sealed class StubYavscApiClient : YavscApiClient { - private readonly List _posts; - public StubYavscApiClient(List posts) + private readonly List _posts; + public StubYavscApiClient(List posts) : base( new Settings { @@ -98,7 +98,7 @@ public class PostItViewModelTests { // The canned fake only knows about a list of posts; the // BlogApiClient test asserts on that list directly. - if (typeof(T) == typeof(List)) + if (typeof(T) == typeof(List)) return Task.FromResult((T)(object)_posts); return Task.FromResult(default(T)!); } diff --git a/src/PostIt/PostIt/ViewModels/MainPageViewModel.cs b/src/PostIt/PostIt/ViewModels/MainPageViewModel.cs index ddf6a732..d907606f 100644 --- a/src/PostIt/PostIt/ViewModels/MainPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/MainPageViewModel.cs @@ -25,7 +25,7 @@ public partial class MainPageViewModel : ViewModelBase /// previous "{Binding SelectedPost.Title}" binding, the user's /// keystrokes were silently dropped whenever /// SelectedPost was null, which made the editor a trap - /// and caused Save to POST a BlogPost with an empty + /// and caused Save to POST a BlogPostDto with an empty /// title — hence the 400 "The Title field is required". [ObservableProperty] public partial string DraftTitle { get; set; } @@ -47,13 +47,13 @@ public partial class MainPageViewModel : ViewModelBase public partial string SearchText { get; set; } [ObservableProperty] - public partial ObservableCollection Posts { get; set; } + public partial ObservableCollection Posts { get; set; } [ObservableProperty] - public partial ObservableCollection FilteredPosts { get; set; } + public partial ObservableCollection FilteredPosts { get; set; } [ObservableProperty] - public partial BlogPost? SelectedPost { get; set; } + public partial BlogPostDto? SelectedPost { get; set; } [ObservableProperty] public partial bool IsBusy { get; set; } @@ -83,8 +83,8 @@ public partial class MainPageViewModel : ViewModelBase private void Init(Settings? settings) { SearchText = string.Empty; - Posts = new ObservableCollection(); - FilteredPosts = new ObservableCollection(); + Posts = new ObservableCollection(); + FilteredPosts = new ObservableCollection(); SelectedPost = null; IsBusy = false; StatusMessage = "Ready"; @@ -120,7 +120,7 @@ public partial class MainPageViewModel : ViewModelBase partial void OnSearchTextChanged(string value) => ApplyFilter(); - partial void OnSelectedPostChanged(BlogPost? value) + partial void OnSelectedPostChanged(BlogPostDto? value) { // Mirror the selection into the editor buffer so the // XAML-bound TextBox/TextEditor show the right content @@ -177,7 +177,7 @@ public partial class MainPageViewModel : ViewModelBase await ExecuteAsync(async () => { - // Build a fresh BlogPost from the editor buffer on + // Build a fresh BlogPostDto from the editor buffer on // every Save — we no longer mutate SelectedPost in // place. The previous behaviour copied the buffer // (which was a no-op when SelectedPost was null) @@ -189,7 +189,7 @@ public partial class MainPageViewModel : ViewModelBase // the update path. if (SelectedPost is null || SelectedPost.Id == 0) { - var draft = new BlogPost + var draft = new BlogPostDto { Title = DraftTitle, Article = DraftArticle ?? string.Empty, @@ -205,7 +205,7 @@ public partial class MainPageViewModel : ViewModelBase } else { - var update = new BlogPost + var update = new BlogPostDto { Id = SelectedPost.Id, AuthorId = SelectedPost.AuthorId, @@ -327,7 +327,7 @@ public partial class MainPageViewModel : ViewModelBase /// because the navigation surface (NavigationPage) lives /// in the View layer. /// - public event EventHandler? ManageAclRequested; + public event EventHandler? ManageAclRequested; [RelayCommand(CanExecute = nameof(CanManageAcl))] public void ManageAcl() diff --git a/src/PostIt/PostIt/ViewModels/PostAclDialogViewModel.cs b/src/PostIt/PostIt/ViewModels/PostAclDialogViewModel.cs index 476a6b9a..68b96b7c 100644 --- a/src/PostIt/PostIt/ViewModels/PostAclDialogViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/PostAclDialogViewModel.cs @@ -34,7 +34,7 @@ public partial class PostAclDialogViewModel : ViewModelBase /// The post whose ACL is being edited. Set by the /// caller (MainPage) when opening the dialog. - public BlogPost Post { get; } + public BlogPostDto Post { get; } [ObservableProperty] public partial ObservableCollection MyCircles { get; set; } = new(); @@ -52,7 +52,7 @@ public partial class PostAclDialogViewModel : ViewModelBase public partial string StatusMessage { get; set; } = string.Empty; public PostAclDialogViewModel( - BlogPost post, + BlogPostDto post, BlogAclApiClient aclClient, CircleApiClient circleClient) { diff --git a/src/PostIt/PostIt/Views/MainPage.axaml b/src/PostIt/PostIt/Views/MainPage.axaml index 1686feef..c6e7fb10 100644 --- a/src/PostIt/PostIt/Views/MainPage.axaml +++ b/src/PostIt/PostIt/Views/MainPage.axaml @@ -53,7 +53,7 @@ - + diff --git a/src/PostIt/PostIt/Views/MainPage.axaml.cs b/src/PostIt/PostIt/Views/MainPage.axaml.cs index c39bf2ff..bec3e86c 100644 --- a/src/PostIt/PostIt/Views/MainPage.axaml.cs +++ b/src/PostIt/PostIt/Views/MainPage.axaml.cs @@ -37,7 +37,7 @@ public partial class MainPage : ContentPage } } - void OnManageAclRequested(object? sender, BlogPost post) + void OnManageAclRequested(object? sender, BlogPostDto post) { var app = Application.Current as App; var services = app?.ServiceProvider; diff --git a/src/PostIt/PostIt/Views/PostAclDialog.axaml.cs b/src/PostIt/PostIt/Views/PostAclDialog.axaml.cs index c632378a..c52b6f63 100644 --- a/src/PostIt/PostIt/Views/PostAclDialog.axaml.cs +++ b/src/PostIt/PostIt/Views/PostAclDialog.axaml.cs @@ -22,7 +22,7 @@ public partial class PostAclDialog : ContentPage InitializeComponent(); } - public PostAclDialog(BlogPost post, BlogAclApiClient aclClient, CircleApiClient circleClient) + public PostAclDialog(BlogPostDto post, BlogAclApiClient aclClient, CircleApiClient circleClient) { InitializeComponent(); DataContext = new PostAclDialogViewModel(post, aclClient, circleClient); diff --git a/src/Yavsc.Abstract/Blogspot/BlogPost.cs b/src/Yavsc.Abstract/Blogspot/BlogPost.cs index 854aa29f..5e397245 100644 --- a/src/Yavsc.Abstract/Blogspot/BlogPost.cs +++ b/src/Yavsc.Abstract/Blogspot/BlogPost.cs @@ -4,7 +4,7 @@ using Yavsc.Abstract.Identity.Security; namespace Yavsc.Blogspot; -public class BlogPost : IBlogPost +public class BlogPostDto : IBlogPost { public string AuthorId { get; set; } diff --git a/src/Yavsc.Api.Client/BlogAclApiClient.cs b/src/Yavsc.Api.Client/BlogAclApiClient.cs index 71263ca4..4cb19f9a 100644 --- a/src/Yavsc.Api.Client/BlogAclApiClient.cs +++ b/src/Yavsc.Api.Client/BlogAclApiClient.cs @@ -11,7 +11,7 @@ namespace Yavsc.Api.Client; /// HTTP client for /api/blogacl on the Yavsc Blogs server. /// /// Each grants a single -/// Circle access to a single BlogPost. The server +/// Circle access to a single BlogPostDto. The server /// scopes every endpoint to the caller's uid: only the author of /// the underlying blog post can list, create, modify, or delete /// its ACL entries. diff --git a/src/Yavsc.Api.Client/BlogApiClient.cs b/src/Yavsc.Api.Client/BlogApiClient.cs index 537124a7..611c892f 100644 --- a/src/Yavsc.Api.Client/BlogApiClient.cs +++ b/src/Yavsc.Api.Client/BlogApiClient.cs @@ -53,19 +53,19 @@ public sealed class BlogApiClient _pathPrefix = pathPrefix?.TrimStart('/') ?? DefaultPathPrefix; } - public Task> GetPostsAsync(int start = 0, int take = 25, CancellationToken ct = default) - => _api.CallAsync>( + public Task> GetPostsAsync(int start = 0, int take = 25, CancellationToken ct = default) + => _api.CallAsync>( HttpMethod.Get, $"{_pathPrefix}?start={start}&take={take}", ct: ct); - public Task GetPostAsync(long id, CancellationToken ct = default) - => _api.CallAsync(HttpMethod.Get, $"{_pathPrefix}/{id}", ct: ct); + public Task GetPostAsync(long id, CancellationToken ct = default) + => _api.CallAsync(HttpMethod.Get, $"{_pathPrefix}/{id}", ct: ct); - public Task CreatePostAsync(BlogPost post, CancellationToken ct = default) - => _api.CallAsync(HttpMethod.Post, _pathPrefix, body: post, ct: ct); + public Task CreatePostAsync(BlogPostDto post, CancellationToken ct = default) + => _api.CallAsync(HttpMethod.Post, _pathPrefix, body: post, ct: ct); - public Task UpdatePostAsync(long id, BlogPost post, CancellationToken ct = default) + public Task UpdatePostAsync(long id, BlogPostDto post, CancellationToken ct = default) => _api.CallAsync(HttpMethod.Put, $"{_pathPrefix}/{id}", body: post, ct: ct); public Task DeletePostAsync(long id, CancellationToken ct = default)