Using your software is a great way to make it better - three personal experiences with the Pointless Waymarks Windows Desktop Feed Reader:
Bruce Percy creates stunning images and - unusual these days - actually writes frequently on www.brucepercy.co.uk. The photos and thoughtful words are exactly the content I wrote the Pointless Waymarks Feed Reader to follow - high-value content, not a stream of attention-economy clickbait.
But lately I haven't been able to keep up - great content or not Bruce Percy's posts have made my Feed Reader Items seem more like a list to manage than a source of daily inspiration.
As a .NET Developer I consider the Morning Dew by Alvin Ashcraft – Daily links for Windows and .NET developers essential reading. The daily links are an easy way to keep in touch with the constantly evolving world of .NET and occasionally link to some package or concept that are stunningly valuable - and that I might otherwise not have found.
The Morning Dew hasn't been in my Feed Reader list because with nearly every workday publication it makes more sense just to visit the website. There isn't any mystery about whether there is a new post, and I long ago gave up on keeping up with every single day.
Matt Payne - Fine Art Landscape Photography Prints of Colorado and Beyond - creates excellent images, produces and hosts the F-Stop Collaborate and Listen Photography Podcast and has made his website an important part of his online presence.
I recently tried to add his site to my feeds via his Atom feed, but it only resulted in an error...
I initially imagined that the content I would follow in the Pointless Waymarks Windows Desktop Feed Reader would be infrequent enough that the program didn't need any options around automatically managing feed items. That has largely proved to be true, but some important outliers like the ones above have crept in, and it is now obvious that borrowing a few features found in other readers would be helpful - the Feeds now have options for:
- Mark Read if More Than __ Items: Inspired by Bruce Percy this setting allows me to see some content but without the list ever growing too long if I don't catch up for a bit.
- Auto Mark Read After __ Days: Inspired by Alvin Ashcraft's Morning Dew I'm using this set to '1' so that I only see the current post in my reader items - a nice compromise so that only the latest is ever in my morning reading.
Coding these features reminded me that I've been using FluentMigrator: Fluent migrations framework for .NET for over 4 years. I found FluentMigrator after being puzzled about how to make EF Core Migrations work nicely for my scenarios. In recent versions, EF Core Migrations would probably be less of a puzzle - both packages have improved. Some FluentMigrator code related to the changes above are included below. This simple usage/example isn't a tour-de-force (or maybe even best practices) example of FluentMigrator, but it is a simple pattern that has been very stable/easy/reliable for me over several years:
using System.Data;
using FluentMigrator;
namespace PointlessWaymarks.FeedReaderData.Migrations;
[Migration(202412260000)]
public class AddAutoMarkReadMigration : Migration
{
public override void Down()
{
throw new DataException($"No Down Available for Migration {nameof(AddAutoMarkReadMigration)}");
}
public override void Up()
{
if (!Schema.Table("Feeds").Column("AutoMarkReadAfterDays").Exists())
Execute.Sql(@"ALTER TABLE Feeds
ADD COLUMN AutoMarkReadAfterDays INTEGER NULL");
if (!Schema.Table("HistoricFeeds").Column("AutoMarkReadAfterDays").Exists())
Execute.Sql(@"ALTER TABLE HistoricFeeds
ADD COLUMN AutoMarkReadAfterDays INTEGER NULL");
}
}
Line 71 from Matt Payne's (feed)[https://www.mattpaynephotography.com/rss.xml] on 12/28/2024:
<title>Kidney Transplant and Artistic Vision: Tracey Halladay’s Powerful Photography Journey</title>
That line triggers a System.Xml.XmlException 'Reference to undeclared entity 'rsquo''. In many situations it could be productive to work with the producer of the XML to explore and resolve the error. In this situation I want to make the most of whatever is offered, so I added code to catch this specific error, parse out the entity that is causing problems, modify the feed, and try again.
/// <summary>
/// Extracts the undeclared entity from and XML exception message - the intent is that
/// this can then be removed from the XML content and the parsing retried.
/// </summary>
/// <param name="exception"></param>
/// <returns></returns>
private static (string entity, int lineNumber, int linePosition) ExtractUndeclaredEntityInformation(
XmlException exception)
{
var startIndex = exception.Message.IndexOf("undeclared entity '", StringComparison.Ordinal) +
"undeclared entity '".Length;
var endIndex = exception.Message.IndexOf("'", startIndex, StringComparison.Ordinal);
if (startIndex > 0 && endIndex > startIndex)
return ($"&{exception.Message[startIndex..endIndex]};", exception.LineNumber, exception.LinePosition - 1);
return (string.Empty, exception.LineNumber, exception.LinePosition - 1);
}
/// <summary>
/// Replaces a string in the content at the given line and position - but if the value to replace is not found at
/// the line and position, it replaces it everywhere in the content. This is very specific to parsing the feed
/// content and the fallback is just-in-case there is some unexpected difference between the line and position
/// from the exception and the parsing done in this method.
/// </summary>
/// <param name="content"></param>
/// <param name="toReplace"></param>
/// <param name="lineNumber"></param>
/// <param name="linePosition"></param>
/// <param name="replacement"></param>
/// <returns></returns>
private static string ReplaceStringAtLineAndPositionOrAllContentsIfNotFound(string content, string toReplace,
int lineNumber, int linePosition, string replacement)
{
var lines = content.Split('\n');
if (lineNumber - 1 < lines.Length)
{
var line = lines[lineNumber - 1];
if (linePosition - 1 < line.Length && line.Substring(linePosition - 1, toReplace.Length) == toReplace)
{
var start = line.Substring(0, linePosition - 1);
var end = line.Substring(linePosition - 1 + toReplace.Length);
lines[lineNumber - 1] = start + replacement + end;
return string.Join('\n', lines);
}
}
// If the value to replace is not found at the given line and position, replace it everywhere in the content
return content.Replace(toReplace, replacement);
}
XDocument? feedDoc = null; // 2.) read document to get the used encoding
var tryProcess = true;
var tryProcessCount = 0;
//2024-12-27: 50 is arbitrary...
while (tryProcess && tryProcessCount < 50)
{
tryProcessCount++;
tryProcess = false;
try
{
feedDoc = XDocument.Parse(feedContent);
}
catch (XmlException ex) when (ex.Message.Contains("Reference to undeclared entity"))
{
// Extract the undeclared entity from the exception message
var entityExceptionInformation = ExtractUndeclaredEntityInformation(ex);
if (!string.IsNullOrEmpty(entityExceptionInformation.entity))
{
var undeclaredEntities = new Dictionary<string, string>
{
{ "’", "’" },
{ "‘", "‘" },
{ "”", "”" },
{ "“", "“" },
{ " ", " " }
};
// Remove the specific undeclared entity and retry parsing
var replacement = undeclaredEntities.GetValueOrDefault(entityExceptionInformation.entity, " ");
feedContent = ReplaceStringAtLineAndPositionOrAllContentsIfNotFound(feedContent,
entityExceptionInformation.entity, entityExceptionInformation.lineNumber,
entityExceptionInformation.linePosition, replacement);
tryProcess = true;
}
else
{
throw;
}
}
}
With only one feed sample to test for the Undeclared Entity issues I can't say whether this is best/right/most useful/most durable - but this code solves the current test case!
If you have any comments or suggestions contact me at [email protected] - Pointless Waymarks Windows Desktop Feed Reader use-at-your-own-risk Beta Version Installers available here.