diff --git a/c#/crawler/src/Db/CrawlerDbContext.cs b/c#/crawler/src/Db/CrawlerDbContext.cs index a4bafab2..275a4875 100644 --- a/c#/crawler/src/Db/CrawlerDbContext.cs +++ b/c#/crawler/src/Db/CrawlerDbContext.cs @@ -30,13 +30,13 @@ public CrawlerDbContext() : this(fid: 0) { } public void TimestampingEntities() => // https://www.entityframeworktutorial.net/faq/set-created-and-modified-date-in-efcore.aspx - ChangeTracker.Entries().ForEach(e => + ChangeTracker.Entries().ForEach(e => { Helper.GetNowTimestamp(out var now); var originalEntityState = e.State; // copy e.State since it might change after any prop value updated var createdAtProp = e.Property(ie => ie.CreatedAt); var updatedAtProp = e.Property(ie => ie.UpdatedAt); - var lastSeenAtProp = e.Entity is IPost ? e.Property(ie => ((IPost)ie).LastSeenAt) : null; + var lastSeenAtProp = e.Entity is BasePost ? e.Property(ie => ((BasePost)ie).LastSeenAt) : null; // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault switch (originalEntityState) diff --git a/c#/crawler/src/Db/Post/IPost.cs b/c#/crawler/src/Db/Post/BasePost.cs similarity index 67% rename from c#/crawler/src/Db/Post/IPost.cs rename to c#/crawler/src/Db/Post/BasePost.cs index 4304d173..91d7a3f0 100644 --- a/c#/crawler/src/Db/Post/IPost.cs +++ b/c#/crawler/src/Db/Post/BasePost.cs @@ -1,9 +1,10 @@ // ReSharper disable UnusedMemberInSuper.Global namespace tbm.Crawler.Db.Post; -public interface IPost : ITimestampedEntity, ICloneable +public abstract class BasePost : TimestampedEntity, ICloneable { public ulong Tid { get; set; } public long AuthorUid { get; set; } public uint? LastSeenAt { get; set; } + public abstract object Clone(); } diff --git a/c#/crawler/src/Db/Post/IPostContent.cs b/c#/crawler/src/Db/Post/PostContent.cs similarity index 60% rename from c#/crawler/src/Db/Post/IPostContent.cs rename to c#/crawler/src/Db/Post/PostContent.cs index 45453793..4d56e7e6 100644 --- a/c#/crawler/src/Db/Post/IPostContent.cs +++ b/c#/crawler/src/Db/Post/PostContent.cs @@ -1,6 +1,6 @@ namespace tbm.Crawler.Db.Post; -public interface IPostContent +public abstract class PostContent : RowVersionedEntity { public byte[]? ProtoBufBytes { get; set; } } diff --git a/c#/crawler/src/Db/Post/IPostWithAuthorExpGrade.cs b/c#/crawler/src/Db/Post/PostWithAuthorExpGrade.cs similarity index 71% rename from c#/crawler/src/Db/Post/IPostWithAuthorExpGrade.cs rename to c#/crawler/src/Db/Post/PostWithAuthorExpGrade.cs index b266c3c1..9a4522db 100644 --- a/c#/crawler/src/Db/Post/IPostWithAuthorExpGrade.cs +++ b/c#/crawler/src/Db/Post/PostWithAuthorExpGrade.cs @@ -1,7 +1,7 @@ // ReSharper disable UnusedMemberInSuper.Global namespace tbm.Crawler.Db.Post; -public interface IPostWithAuthorExpGrade : IPost +public abstract class PostWithAuthorExpGrade : BasePost { [NotMapped] public byte AuthorExpGrade { get; set; } } diff --git a/c#/crawler/src/Db/Post/ReplyContent.cs b/c#/crawler/src/Db/Post/ReplyContent.cs index 5f5ba136..e0f65fe3 100644 --- a/c#/crawler/src/Db/Post/ReplyContent.cs +++ b/c#/crawler/src/Db/Post/ReplyContent.cs @@ -1,8 +1,7 @@ // ReSharper disable PropertyCanBeMadeInitOnly.Global namespace tbm.Crawler.Db.Post; -public class ReplyContent : RowVersionedEntity, IPostContent +public class ReplyContent : PostContent { [Key] public ulong Pid { get; set; } - public byte[]? ProtoBufBytes { get; set; } } diff --git a/c#/crawler/src/Db/Post/ReplyPost.cs b/c#/crawler/src/Db/Post/ReplyPost.cs index 873a372d..c2a40460 100644 --- a/c#/crawler/src/Db/Post/ReplyPost.cs +++ b/c#/crawler/src/Db/Post/ReplyPost.cs @@ -1,9 +1,8 @@ // ReSharper disable PropertyCanBeMadeInitOnly.Global namespace tbm.Crawler.Db.Post; -public class ReplyPost : RowVersionedEntity, IPostWithAuthorExpGrade +public class ReplyPost : PostWithAuthorExpGrade { - public ulong Tid { get; set; } [Key] public ulong Pid { get; set; } public uint Floor { get; set; } [NotMapped] public byte[]? Content { get; set; } @@ -11,8 +10,6 @@ public class ReplyPost : RowVersionedEntity, IPostWithAuthorExpGrade [JsonConverter(typeof(ProtoBufRepeatedFieldJsonConverter))] [NotMapped] public required RepeatedField OriginalContents { get; set; } - public long AuthorUid { get; set; } - [NotMapped] public byte AuthorExpGrade { get; set; } public uint? SubReplyCount { get; set; } public uint PostedAt { get; set; } public byte? IsFold { get; set; } @@ -21,9 +18,6 @@ public class ReplyPost : RowVersionedEntity, IPostWithAuthorExpGrade public byte[]? Geolocation { get; set; } public uint? SignatureId { get; set; } [NotMapped] public byte[]? Signature { get; set; } - public uint CreatedAt { get; set; } - public uint? UpdatedAt { get; set; } - public uint? LastSeenAt { get; set; } - public object Clone() => MemberwiseClone(); + public override object Clone() => MemberwiseClone(); } diff --git a/c#/crawler/src/Db/Post/SubReplyContent.cs b/c#/crawler/src/Db/Post/SubReplyContent.cs index fec2c12a..3ebae1ff 100644 --- a/c#/crawler/src/Db/Post/SubReplyContent.cs +++ b/c#/crawler/src/Db/Post/SubReplyContent.cs @@ -1,8 +1,7 @@ // ReSharper disable PropertyCanBeMadeInitOnly.Global namespace tbm.Crawler.Db.Post; -public class SubReplyContent : RowVersionedEntity, IPostContent +public class SubReplyContent : PostContent { [Key] public ulong Spid { get; set; } - public byte[]? ProtoBufBytes { get; set; } } diff --git a/c#/crawler/src/Db/Post/SubReplyPost.cs b/c#/crawler/src/Db/Post/SubReplyPost.cs index 65cc028c..5ee1d47d 100644 --- a/c#/crawler/src/Db/Post/SubReplyPost.cs +++ b/c#/crawler/src/Db/Post/SubReplyPost.cs @@ -1,9 +1,8 @@ // ReSharper disable PropertyCanBeMadeInitOnly.Global namespace tbm.Crawler.Db.Post; -public class SubReplyPost : RowVersionedEntity, IPostWithAuthorExpGrade +public class SubReplyPost : PostWithAuthorExpGrade { - public ulong Tid { get; set; } public ulong Pid { get; set; } [Key] public ulong Spid { get; set; } [NotMapped] public byte[]? Content { get; set; } @@ -11,14 +10,9 @@ public class SubReplyPost : RowVersionedEntity, IPostWithAuthorExpGrade [JsonConverter(typeof(ProtoBufRepeatedFieldJsonConverter))] [NotMapped] public required RepeatedField OriginalContents { get; set; } - public long AuthorUid { get; set; } - [NotMapped] public byte AuthorExpGrade { get; set; } public uint PostedAt { get; set; } public int? AgreeCount { get; set; } public int? DisagreeCount { get; set; } - public uint CreatedAt { get; set; } - public uint? UpdatedAt { get; set; } - public uint? LastSeenAt { get; set; } - public object Clone() => MemberwiseClone(); + public override object Clone() => MemberwiseClone(); } diff --git a/c#/crawler/src/Db/Post/ThreadPost.cs b/c#/crawler/src/Db/Post/ThreadPost.cs index 8d2919d5..6b66e86f 100644 --- a/c#/crawler/src/Db/Post/ThreadPost.cs +++ b/c#/crawler/src/Db/Post/ThreadPost.cs @@ -1,9 +1,9 @@ // ReSharper disable PropertyCanBeMadeInitOnly.Global namespace tbm.Crawler.Db.Post; -public class ThreadPost : RowVersionedEntity, IPost +public class ThreadPost : BasePost { - [Key] public ulong Tid { get; set; } + [Key] public new ulong Tid { get; set; } [NotMapped] public ulong? FirstReplyPid { get; set; } [JsonConverter(typeof(ProtoBufRepeatedFieldJsonConverter))] @@ -14,7 +14,6 @@ public class ThreadPost : RowVersionedEntity, IPost public string? TopicType { get; set; } public byte? IsGood { get; set; } public required string Title { get; set; } - public long AuthorUid { get; set; } public string? AuthorPhoneType { get; set; } public uint PostedAt { get; set; } public uint LatestReplyPostedAt { get; set; } @@ -26,9 +25,6 @@ public class ThreadPost : RowVersionedEntity, IPost public int? DisagreeCount { get; set; } public byte[]? Zan { get; set; } public byte[]? Geolocation { get; set; } - public uint CreatedAt { get; set; } - public uint? UpdatedAt { get; set; } - public uint? LastSeenAt { get; set; } - public object Clone() => MemberwiseClone(); + public override object Clone() => MemberwiseClone(); } diff --git a/c#/crawler/src/Db/Revision/AuthorRevision.cs b/c#/crawler/src/Db/Revision/AuthorRevision.cs index afe957f3..2376a8ed 100644 --- a/c#/crawler/src/Db/Revision/AuthorRevision.cs +++ b/c#/crawler/src/Db/Revision/AuthorRevision.cs @@ -2,10 +2,8 @@ // ReSharper disable UnusedAutoPropertyAccessor.Global namespace tbm.Crawler.Db.Revision; -public abstract class AuthorRevision : RowVersionedEntity +public abstract class AuthorRevision : ForumScopedRevision { - public uint DiscoveredAt { get; set; } - public uint Fid { get; set; } public long Uid { get; set; } public required PostType TriggeredBy { get; set; } } diff --git a/c#/crawler/src/Db/Revision/ForumModeratorRevision.cs b/c#/crawler/src/Db/Revision/ForumModeratorRevision.cs index 8f1c667f..fd0cfd83 100644 --- a/c#/crawler/src/Db/Revision/ForumModeratorRevision.cs +++ b/c#/crawler/src/Db/Revision/ForumModeratorRevision.cs @@ -1,10 +1,8 @@ // ReSharper disable PropertyCanBeMadeInitOnly.Global namespace tbm.Crawler.Db.Revision; -public class ForumModeratorRevision : RowVersionedEntity +public class ForumModeratorRevision : ForumScopedRevision { - public uint DiscoveredAt { get; set; } - public uint Fid { get; set; } public required string Portrait { get; set; } public required string ModeratorTypes { get; set; } } diff --git a/c#/crawler/src/Db/Revision/ForumScopedRevision.cs b/c#/crawler/src/Db/Revision/ForumScopedRevision.cs new file mode 100644 index 00000000..29a13bb1 --- /dev/null +++ b/c#/crawler/src/Db/Revision/ForumScopedRevision.cs @@ -0,0 +1,7 @@ +namespace tbm.Crawler.Db.Revision; + +public abstract class ForumScopedRevision : RowVersionedEntity +{ + public uint DiscoveredAt { get; set; } + public uint Fid { get; set; } +} diff --git a/c#/crawler/src/Db/Revision/Splitting/IRevision.cs b/c#/crawler/src/Db/Revision/Splitting/BaseRevisionWithSplitting.cs similarity index 59% rename from c#/crawler/src/Db/Revision/Splitting/IRevision.cs rename to c#/crawler/src/Db/Revision/Splitting/BaseRevisionWithSplitting.cs index 53788ecd..3f0eb920 100644 --- a/c#/crawler/src/Db/Revision/Splitting/IRevision.cs +++ b/c#/crawler/src/Db/Revision/Splitting/BaseRevisionWithSplitting.cs @@ -1,9 +1,9 @@ // ReSharper disable UnusedMemberInSuper.Global namespace tbm.Crawler.Db.Revision.Splitting; -public interface IRevision +public abstract class BaseRevisionWithSplitting : RowVersionedEntity { public uint TakenAt { get; set; } public ushort? NullFieldsBitMask { get; set; } - public bool IsAllFieldsIsNullExceptSplit(); + public abstract bool IsAllFieldsIsNullExceptSplit(); } diff --git a/c#/crawler/src/Db/Revision/Splitting/RevisionWithSplitting.cs b/c#/crawler/src/Db/Revision/Splitting/RevisionWithSplitting.cs index aad3345d..cd6c088c 100644 --- a/c#/crawler/src/Db/Revision/Splitting/RevisionWithSplitting.cs +++ b/c#/crawler/src/Db/Revision/Splitting/RevisionWithSplitting.cs @@ -1,15 +1,11 @@ namespace tbm.Crawler.Db.Revision.Splitting; -public abstract class RevisionWithSplitting : RowVersionedEntity, IRevision - where TBaseRevision : class, IRevision +public abstract class RevisionWithSplitting : BaseRevisionWithSplitting + where TBaseRevision : BaseRevisionWithSplitting { private readonly Dictionary _splitEntities = []; - - public uint TakenAt { get; set; } - public ushort? NullFieldsBitMask { get; set; } public IReadOnlyDictionary SplitEntities => _splitEntities; - - public virtual bool IsAllFieldsIsNullExceptSplit() => throw new NotSupportedException(); + public override bool IsAllFieldsIsNullExceptSplit() => throw new NotSupportedException(); protected TValue? GetSplitEntityValue (Func valueSelector) diff --git a/c#/crawler/src/Db/ITimestampedEntity.cs b/c#/crawler/src/Db/TimestampedEntity.cs similarity index 64% rename from c#/crawler/src/Db/ITimestampedEntity.cs rename to c#/crawler/src/Db/TimestampedEntity.cs index 6a7e010a..da4cadb1 100644 --- a/c#/crawler/src/Db/ITimestampedEntity.cs +++ b/c#/crawler/src/Db/TimestampedEntity.cs @@ -1,6 +1,6 @@ namespace tbm.Crawler.Db; -public interface ITimestampedEntity +public abstract class TimestampedEntity : RowVersionedEntity { public uint CreatedAt { get; set; } public uint? UpdatedAt { get; set; } diff --git a/c#/crawler/src/Db/User.cs b/c#/crawler/src/Db/User.cs index 23db5db1..5c9866f2 100644 --- a/c#/crawler/src/Db/User.cs +++ b/c#/crawler/src/Db/User.cs @@ -1,6 +1,6 @@ namespace tbm.Crawler.Db; -public class User : RowVersionedEntity, ITimestampedEntity +public class User : TimestampedEntity { [Key] public long Uid { get; set; } public string? Name { get; set; } @@ -11,8 +11,6 @@ public class User : RowVersionedEntity, ITimestampedEntity public string? FansNickname { get; set; } public byte[]? Icon { get; set; } public string? IpGeolocation { get; set; } - public uint CreatedAt { get; set; } - public uint? UpdatedAt { get; set; } public static User CreateLatestReplier(long uid, string? name, string? displayName) => new() {Uid = uid, Name = name, DisplayName = displayName, Portrait = ""}; diff --git a/c#/crawler/src/Tieba/Crawl/Facade/BaseCrawlFacade.cs b/c#/crawler/src/Tieba/Crawl/Facade/BaseCrawlFacade.cs index d50c3fb1..0edce0c5 100644 --- a/c#/crawler/src/Tieba/Crawl/Facade/BaseCrawlFacade.cs +++ b/c#/crawler/src/Tieba/Crawl/Facade/BaseCrawlFacade.cs @@ -12,8 +12,8 @@ public abstract class BaseCrawlFacade, UserParser> userParserFactory, Func, UserSaver> userSaverFactory) : IDisposable - where TPost : class, IPost - where TBaseRevision : class, IRevision + where TPost : BasePost + where TBaseRevision : BaseRevisionWithSplitting where TResponse : class, IMessage where TPostProtoBuf : class, IMessage { diff --git a/c#/crawler/src/Tieba/Crawl/Parser/Post/BasePostParser.cs b/c#/crawler/src/Tieba/Crawl/Parser/Post/BasePostParser.cs index f5621743..a69a125f 100644 --- a/c#/crawler/src/Tieba/Crawl/Parser/Post/BasePostParser.cs +++ b/c#/crawler/src/Tieba/Crawl/Parser/Post/BasePostParser.cs @@ -1,7 +1,7 @@ namespace tbm.Crawler.Tieba.Crawl.Parser.Post; public abstract class BasePostParser - where TPost : class, IPost + where TPost : BasePost where TPostProtoBuf : class, IMessage { public void Parse( @@ -18,7 +18,7 @@ public void Parse( outPosts = ParseInternal(inPosts, nullableUsers).ToDictionary(PostIdSelector, post => post); if (outPosts.Values.Any(p => p.AuthorUid == 0)) throw new TiebaException(shouldRetry: true, - "Value of IPost.AuthorUid is the protoBuf default value 0."); + "Value of BasePost.AuthorUid is the protoBuf default value 0."); var users = new List(30); users.AddRange(nullableUsers.OfType() diff --git a/c#/crawler/src/Tieba/Crawl/Saver/AuthorRevisionSaver.cs b/c#/crawler/src/Tieba/Crawl/Saver/AuthorRevisionSaver.cs index 1a706215..df202930 100644 --- a/c#/crawler/src/Tieba/Crawl/Saver/AuthorRevisionSaver.cs +++ b/c#/crawler/src/Tieba/Crawl/Saver/AuthorRevisionSaver.cs @@ -13,7 +13,7 @@ public class AuthorRevisionSaver(PostType triggeredByPostType) public Action SaveAuthorExpGradeRevisions (CrawlerDbContext db, IReadOnlyCollection posts) - where TPostWithAuthorExpGrade : class, IPost, IPostWithAuthorExpGrade + where TPostWithAuthorExpGrade : PostWithAuthorExpGrade { SaveAuthorRevisions(db, posts, AuthorExpGradeLocks, db.AuthorExpGradeRevisions, @@ -46,7 +46,7 @@ private void SaveAuthorRevisions( Func isValueChangedPredicate, Expression>> latestRevisionProjectionFactory, Func<(Uid Uid, TValue? Value, Time DiscoveredAt), TRevision> revisionFactory) - where TPost : class, IPost + where TPost : BasePost where TRevision : AuthorRevision { Helper.GetNowTimestamp(out var now); diff --git a/c#/crawler/src/Tieba/Crawl/Saver/BaseSaver.cs b/c#/crawler/src/Tieba/Crawl/Saver/BaseSaver.cs index 04fc4888..e81954f5 100644 --- a/c#/crawler/src/Tieba/Crawl/Saver/BaseSaver.cs +++ b/c#/crawler/src/Tieba/Crawl/Saver/BaseSaver.cs @@ -6,7 +6,7 @@ public abstract class BaseSaver(ILogger> #pragma warning disable S1939 // Inheritance list should not be redundant : SaverWithRevision, IFieldChangeIgnorance #pragma warning restore S1939 // Inheritance list should not be redundant - where TBaseRevision : class, IRevision + where TBaseRevision : BaseRevisionWithSplitting { protected void SavePostsOrUsers( CrawlerDbContext db, @@ -15,7 +15,7 @@ protected void SavePostsOrUsers( ILookup existingOrNewLookup, Func existingSelector) where TPostOrUser : class - where TRevision : class, IRevision + where TRevision : BaseRevisionWithSplitting { db.Set().AddRange(existingOrNewLookup[false]); // newly added var newRevisions = existingOrNewLookup[true].Select(newPostOrUser => @@ -26,11 +26,11 @@ protected void SavePostsOrUsers( // this will mutate postOrUserInTracking which is referenced by entry entry.CurrentValues.SetValues(newPostOrUser); - bool IsTimestampingFieldName(string name) => name is nameof(IPost.LastSeenAt) - or nameof(ITimestampedEntity.CreatedAt) or nameof(ITimestampedEntity.UpdatedAt); + bool IsTimestampingFieldName(string name) => name is nameof(BasePost.LastSeenAt) + or nameof(TimestampedEntity.CreatedAt) or nameof(TimestampedEntity.UpdatedAt); // rollback changes that overwrite original values with the default value 0 or null - // for all fields of ITimestampedEntity and IPost.LastSeenAt + // for all fields of TimestampedEntity and BasePost.LastSeenAt // this will also affect the entity instance which postOrUserInTracking references to it entry.Properties .Where(prop => prop.IsModified && IsTimestampingFieldName(prop.Metadata.Name)) diff --git a/c#/crawler/src/Tieba/Crawl/Saver/IFieldChangeIgnorance.cs b/c#/crawler/src/Tieba/Crawl/Saver/IFieldChangeIgnorance.cs index d7e73216..d7fbfedc 100644 --- a/c#/crawler/src/Tieba/Crawl/Saver/IFieldChangeIgnorance.cs +++ b/c#/crawler/src/Tieba/Crawl/Saver/IFieldChangeIgnorance.cs @@ -71,7 +71,7 @@ when newValue is "" && newValue is null && oldValue is not null) return true; // possible rarely respond with the protoBuf default value 0 - return propName == nameof(IPost.AuthorUid) + return propName == nameof(BasePost.AuthorUid) && newValue is 0L && oldValue is not null; }, Revision: (whichPostType, propName, oldValue, _) => diff --git a/c#/crawler/src/Tieba/Crawl/Saver/Post/BasePostSaver.cs b/c#/crawler/src/Tieba/Crawl/Saver/Post/BasePostSaver.cs index 3af394b9..40537786 100644 --- a/c#/crawler/src/Tieba/Crawl/Saver/Post/BasePostSaver.cs +++ b/c#/crawler/src/Tieba/Crawl/Saver/Post/BasePostSaver.cs @@ -8,8 +8,8 @@ public abstract class BasePostSaver( AuthorRevisionSaver.New authorRevisionSaverFactory, PostType currentPostType) : BaseSaver(logger) - where TPost : class, IPost - where TBaseRevision : class, IRevision + where TPost : BasePost + where TBaseRevision : BaseRevisionWithSplitting { protected delegate void PostSaveEventHandler(); [SuppressMessage("Design", "MA0046:Use EventHandler to declare events")] @@ -30,7 +30,7 @@ protected SaverChangeSet Save( Func postIdSelector, Func revisionFactory, ExpressionStarter existingPostPredicate) - where TRevision : class, IRevision + where TRevision : BaseRevisionWithSplitting { var existingPostsKeyById = db.Set() .Where(existingPostPredicate).ToDictionary(postIdSelector); diff --git a/c#/crawler/src/Tieba/Crawl/Saver/SaverChangeSet.cs b/c#/crawler/src/Tieba/Crawl/Saver/SaverChangeSet.cs index fd826909..59fb8def 100644 --- a/c#/crawler/src/Tieba/Crawl/Saver/SaverChangeSet.cs +++ b/c#/crawler/src/Tieba/Crawl/Saver/SaverChangeSet.cs @@ -4,7 +4,7 @@ public class SaverChangeSet( IReadOnlyCollection existingBefore, ICollection existingAfterAndNewlyAdded, Func postIdSelector) - where TPost : class, IPost + where TPost : BasePost { public IReadOnlyCollection<(TPost Before, TPost After)> Existing { get; } = existingBefore .OrderBy(postIdSelector) diff --git a/c#/crawler/src/Tieba/Crawl/Saver/SaverWithRevision.cs b/c#/crawler/src/Tieba/Crawl/Saver/SaverWithRevision.cs index c1bf5f67..78336ea5 100644 --- a/c#/crawler/src/Tieba/Crawl/Saver/SaverWithRevision.cs +++ b/c#/crawler/src/Tieba/Crawl/Saver/SaverWithRevision.cs @@ -1,7 +1,7 @@ namespace tbm.Crawler.Tieba.Crawl.Saver; public abstract class SaverWithRevision : IRevisionProperties - where TBaseRevision : class, IRevision + where TBaseRevision : BaseRevisionWithSplitting { protected delegate void RevisionUpsertDelegate(CrawlerDbContext db, IEnumerable revision); diff --git a/c#/imagePipeline/src/Consumer/MetadataConsumer.cs b/c#/imagePipeline/src/Consumer/MetadataConsumer.cs index 24dbada4..f500c4d7 100644 --- a/c#/imagePipeline/src/Consumer/MetadataConsumer.cs +++ b/c#/imagePipeline/src/Consumer/MetadataConsumer.cs @@ -94,7 +94,7 @@ private Func GetImageMetaData TImageSharpProfile? profile, Func rawBytesSelector) where TImageSharpProfile : class - where TEmbeddedMetadata : class, ImageMetadata.IEmbedded, new() + where TEmbeddedMetadata : ImageMetadata.Embedded, new() { if (profile == null) return null; var rawBytes = rawBytesSelector(profile); // will be null when param profile is null diff --git a/c#/imagePipeline/src/Db/ImageFailed.cs b/c#/imagePipeline/src/Db/ImageFailed.cs index b0ca9020..423238af 100644 --- a/c#/imagePipeline/src/Db/ImageFailed.cs +++ b/c#/imagePipeline/src/Db/ImageFailed.cs @@ -3,10 +3,9 @@ namespace tbm.ImagePipeline.Db; -public class ImageFailed : RowVersionedEntity +public class ImageFailed : EntityWithImageId { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public uint Id { get; set; } - public uint ImageId { get; set; } public required string Exception { get; set; } } diff --git a/c#/imagePipeline/src/Db/ImageHash.cs b/c#/imagePipeline/src/Db/ImageHash.cs index d4eb672d..882222b7 100644 --- a/c#/imagePipeline/src/Db/ImageHash.cs +++ b/c#/imagePipeline/src/Db/ImageHash.cs @@ -1,10 +1,8 @@ // ReSharper disable PropertyCanBeMadeInitOnly.Global namespace tbm.ImagePipeline.Db; -public class ImageHash : RowVersionedEntity +public class ImageHash : ImageWithFrameIndex { - public uint ImageId { get; set; } - public uint FrameIndex { get; set; } public required byte[] PHash { get; set; } public required byte[] AverageHash { get; set; } public required byte[] BlockMeanHash { get; set; } diff --git a/c#/imagePipeline/src/Db/ImageMetadata.cs b/c#/imagePipeline/src/Db/ImageMetadata.cs index 815aeedb..2ce13f30 100644 --- a/c#/imagePipeline/src/Db/ImageMetadata.cs +++ b/c#/imagePipeline/src/Db/ImageMetadata.cs @@ -5,23 +5,12 @@ using System.ComponentModel; using SixLabors.ImageSharp.PixelFormats; using Point = NetTopologySuite.Geometries.Point; +using EntityWithImageIdAsKey = tbm.Shared.Db.EntityWithImageId.AsKey; namespace tbm.ImagePipeline.Db; -public class ImageMetadata : RowVersionedEntity, ImageMetadata.IImageMetadata +public class ImageMetadata : EntityWithImageIdAsKey { - public interface IImageMetadata - { - [Key] public uint ImageId { get; set; } - } - - public interface IEmbedded : IImageMetadata - { - public byte[] XxHash3 { get; set; } - public byte[]? RawBytes { get; set; } - } - - [Key] public uint ImageId { get; set; } public string? Format { get; set; } public ushort Width { get; set; } public ushort Height { get; set; } @@ -40,14 +29,19 @@ public interface IEmbedded : IImageMetadata public Gif? GifMetadata { get; set; } public Bmp? BmpMetadata { get; set; } - public class ByteSize : RowVersionedEntity, IImageMetadata + public class ByteSize : EntityWithImageIdAsKey { - [Key] public uint ImageId { get; set; } [SuppressMessage("Naming", "AV1710:Member name includes the name of its containing type")] public uint DownloadedByteSize { get; set; } } - public class Exif : RowVersionedEntity, IEmbedded + public abstract class Embedded : EntityWithImageIdAsKey + { + public byte[] XxHash3 { get; set; } = null!; + public byte[]? RawBytes { get; set; } + } + + public class Exif : Embedded { [SuppressMessage("ApiDesign", "SS039:An enum should specify a default value")] public enum ExifOrientation @@ -62,7 +56,6 @@ public enum ExifOrientation Rotate270Cw = 8 } - [Key] public uint ImageId { get; set; } public string? Orientation { get; set; } public string? ImageDescription { get; set; } public string? UserComment { get; set; } @@ -88,44 +81,23 @@ public enum ExifOrientation public Point? GpsCoordinate { get; set; } public float? GpsImgDirection { get; set; } public string? GpsImgDirectionRef { get; set; } - public byte[] XxHash3 { get; set; } = null!; - public byte[]? RawBytes { get; set; } // workaround to work with MetadataConsumer.CreateEmbeddedFromProfile() // https://stackoverflow.com/questions/75266722/type-cannot-satisfy-the-new-constraint-on-parameter-tparam-because-type public IEnumerable TagNames { get; set; } = []; - public class TagName : RowVersionedEntity, IImageMetadata + public class TagName : EntityWithImageId { - public uint ImageId { get; set; } public required string Name { get; set; } } } - public class Icc : RowVersionedEntity, IEmbedded - { - [Key] public uint ImageId { get; set; } - public byte[] XxHash3 { get; set; } = null!; - public byte[]? RawBytes { get; set; } - } - - public class Iptc : RowVersionedEntity, IEmbedded - { - [Key] public uint ImageId { get; set; } - public byte[] XxHash3 { get; set; } = null!; - public byte[]? RawBytes { get; set; } - } - - public class Xmp : RowVersionedEntity, IEmbedded - { - [Key] public uint ImageId { get; set; } - public byte[] XxHash3 { get; set; } = null!; - public byte[]? RawBytes { get; set; } - } + public class Icc : Embedded; + public class Iptc : Embedded; + public class Xmp : Embedded; - public class Jpg : RowVersionedEntity, IImageMetadata + public class Jpg : EntityWithImageIdAsKey { - [Key] public uint ImageId { get; set; } public int Quality { get; set; } public string? ColorType { get; set; } public bool? Interleaved { get; set; } @@ -145,9 +117,8 @@ public class Jpg : RowVersionedEntity, IImageMetadata } } - public class Png : RowVersionedEntity, IImageMetadata + public class Png : EntityWithImageIdAsKey { - [Key] public uint ImageId { get; set; } public string? BitDepth { get; set; } public string? ColorType { get; set; } public string? InterlaceMethod { get; set; } @@ -178,9 +149,8 @@ public class Png : RowVersionedEntity, IImageMetadata } } - public class Gif : RowVersionedEntity, IImageMetadata + public class Gif : EntityWithImageIdAsKey { - [Key] public uint ImageId { get; set; } public ushort RepeatCount { get; set; } public required string ColorTableMode { get; set; } public int GlobalColorTableLength { get; set; } @@ -203,9 +173,8 @@ public class Gif : RowVersionedEntity, IImageMetadata } } - public class Bmp : RowVersionedEntity, IImageMetadata + public class Bmp : EntityWithImageIdAsKey { - [Key] public uint ImageId { get; set; } public required string InfoHeaderType { get; set; } public required string BitsPerPixel { get; set; } diff --git a/c#/imagePipeline/src/Db/ImageOcrBox.cs b/c#/imagePipeline/src/Db/ImageOcrBox.cs index da8a0e03..cd89d240 100644 --- a/c#/imagePipeline/src/Db/ImageOcrBox.cs +++ b/c#/imagePipeline/src/Db/ImageOcrBox.cs @@ -1,10 +1,8 @@ // ReSharper disable PropertyCanBeMadeInitOnly.Global namespace tbm.ImagePipeline.Db; -public class ImageOcrBox : RowVersionedEntity +public class ImageOcrBox : ImageWithFrameIndex { - public uint ImageId { get; set; } - public uint FrameIndex { get; set; } public ushort CenterPointX { get; set; } public ushort CenterPointY { get; set; } public ushort Width { get; set; } diff --git a/c#/imagePipeline/src/Db/ImageOcrLine.cs b/c#/imagePipeline/src/Db/ImageOcrLine.cs index 5af886eb..5cb6d9b6 100644 --- a/c#/imagePipeline/src/Db/ImageOcrLine.cs +++ b/c#/imagePipeline/src/Db/ImageOcrLine.cs @@ -1,9 +1,7 @@ // ReSharper disable PropertyCanBeMadeInitOnly.Global namespace tbm.ImagePipeline.Db; -public class ImageOcrLine : RowVersionedEntity +public class ImageOcrLine : ImageWithFrameIndex { - public uint ImageId { get; set; } - public uint FrameIndex { get; set; } public required string TextLines { get; set; } } diff --git a/c#/imagePipeline/src/Db/ImagePipelineDbContext.cs b/c#/imagePipeline/src/Db/ImagePipelineDbContext.cs index 2f9697dc..c5cded19 100644 --- a/c#/imagePipeline/src/Db/ImagePipelineDbContext.cs +++ b/c#/imagePipeline/src/Db/ImagePipelineDbContext.cs @@ -41,7 +41,7 @@ protected override void OnModelCreating(ModelBuilder b) void SplitImageMetadata (Expression> keySelector, string tableNameSuffix) - where TRelatedEntity : class, IImageMetadata + where TRelatedEntity : EntityWithImageId { b.Entity().HasOne(keySelector).WithOne().HasForeignKey(e => e.ImageId); b.Entity().ToTable($"tbmi_metadata_{tableNameSuffix}"); diff --git a/c#/imagePipeline/src/Db/ImageQrCode.cs b/c#/imagePipeline/src/Db/ImageQrCode.cs index 38a7a96c..1dae698f 100644 --- a/c#/imagePipeline/src/Db/ImageQrCode.cs +++ b/c#/imagePipeline/src/Db/ImageQrCode.cs @@ -1,10 +1,8 @@ // ReSharper disable PropertyCanBeMadeInitOnly.Global namespace tbm.ImagePipeline.Db; -public class ImageQrCode : RowVersionedEntity +public class ImageQrCode : ImageWithFrameIndex { - public uint ImageId { get; set; } - public uint FrameIndex { get; set; } public short Point1X { get; set; } public short Point1Y { get; set; } public short Point2X { get; set; } diff --git a/c#/imagePipeline/src/Db/ImageWithFrameIndex.cs b/c#/imagePipeline/src/Db/ImageWithFrameIndex.cs new file mode 100644 index 00000000..61ebf29b --- /dev/null +++ b/c#/imagePipeline/src/Db/ImageWithFrameIndex.cs @@ -0,0 +1,6 @@ +namespace tbm.ImagePipeline.Db; + +public abstract class ImageWithFrameIndex : EntityWithImageId +{ + public uint FrameIndex { get; set; } +} diff --git a/c#/shared/src/Db/EntitiesWithImageId.cs b/c#/shared/src/Db/EntitiesWithImageId.cs new file mode 100644 index 00000000..b3f0c3cb --- /dev/null +++ b/c#/shared/src/Db/EntitiesWithImageId.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace tbm.Shared.Db; + +public abstract class EntityWithImageId : RowVersionedEntity +{ + public uint ImageId { get; set; } + + public abstract class AsKey : EntityWithImageId + { + [Key] public new uint ImageId { get; set; } + } +} diff --git a/c#/shared/src/Db/ImageInReply.cs b/c#/shared/src/Db/ImageInReply.cs index 44074577..d2bb7934 100644 --- a/c#/shared/src/Db/ImageInReply.cs +++ b/c#/shared/src/Db/ImageInReply.cs @@ -1,11 +1,8 @@ // ReSharper disable PropertyCanBeMadeInitOnly.Global -using System.ComponentModel.DataAnnotations; - namespace tbm.Shared.Db; -public class ImageInReply : RowVersionedEntity +public class ImageInReply : EntityWithImageId.AsKey { - [Key] public uint ImageId { get; set; } public required string UrlFilename { get; set; } public uint ExpectedByteSize { get; set; } public bool MetadataConsumed { get; set; } diff --git a/c#/shared/src/Db/ReplyContentImage.cs b/c#/shared/src/Db/ReplyContentImage.cs index a84e0ac4..6892fde8 100644 --- a/c#/shared/src/Db/ReplyContentImage.cs +++ b/c#/shared/src/Db/ReplyContentImage.cs @@ -1,9 +1,8 @@ // ReSharper disable PropertyCanBeMadeInitOnly.Global namespace tbm.Shared.Db; -public class ReplyContentImage +public class ReplyContentImage : EntityWithImageId { public ulong Pid { get; set; } - public uint ImageId { get; set; } public required ImageInReply ImageInReply { get; set; } }