Skip to content

Commit bb8a5ed

Browse files
committed
* fix all violations of ReSharper inspections
* enable `GenerateDocumentationFile` to fix `SA0001: XML comment analysis is disabled due to project configuration` at build time @ Directory.Build.props * disable Roslyn analyzer rule `CS1591` and ReSharper inspection `SeparateLocalFunctionsWithJumpStatement` @ .editorconfig @ c#
1 parent 56e93b4 commit bb8a5ed

14 files changed

+117
-107
lines changed

c#/.editorconfig

+7-6
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ root = true
55
###############################
66
# All files
77
[*]
8+
# Standard properties
89
indent_style = space
10+
end_of_line = lf
11+
charset = utf-8
12+
indent_size = 4
13+
insert_final_newline = true
914

1015
# ReSharper properties
1116
resharper_allow_comment_after_lbrace = true
@@ -62,12 +67,6 @@ resharper_wrap_chained_binary_expressions = chop_if_long
6267
resharper_wrap_chained_binary_patterns = chop_if_long
6368
resharper_wrap_chained_method_calls = wrap_if_long
6469

65-
# Standard properties
66-
end_of_line = lf
67-
insert_final_newline = true
68-
indent_size = 4
69-
charset = utf-8
70-
7170
# Microsoft .NET properties
7271
csharp_new_line_before_members_in_object_initializers = false
7372
csharp_preferred_modifier_order = public, private, protected, internal, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async:suggestion
@@ -113,6 +112,7 @@ dotnet_style_qualification_for_field = false:suggestion
113112
dotnet_style_qualification_for_method = false:suggestion
114113
dotnet_style_qualification_for_property = false:suggestion
115114
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
115+
dotnet_diagnostic.CS1591.severity = none
116116

117117
# ReSharper inspection severities
118118
# https://youtrack.jetbrains.com/issue/RSRP-463998
@@ -142,6 +142,7 @@ resharper_suggest_var_or_type_elsewhere_highlighting = hint
142142
resharper_suggest_var_or_type_simple_types_highlighting = hint
143143
resharper_entity_framework_model_validation_unlimited_string_length_highlighting = none
144144
resharper_move_local_function_after_jump_statement_highlighting = none
145+
resharper_separate_local_functions_with_jump_statement_highlighting = none
145146

146147
###############################
147148
# .NET Coding Conventions #

c#/Directory.Build.props

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
88
<PathMap>$(MSBuildProjectDirectory)=/</PathMap>
99
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
10+
<GenerateDocumentationFile>true</GenerateDocumentationFile>
1011
</PropertyGroup>
1112
<ItemGroup>
1213
<AdditionalFiles Include="$(MSBuildThisFileDirectory)\stylecop.json" />

c#/crawler/src/Tieba/Crawl/Crawler/BaseCrawler.cs

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ namespace tbm.Crawler.Tieba.Crawl.Crawler;
33
public abstract partial class BaseCrawler<TResponse, TPostProtoBuf>
44
{
55
public abstract Exception FillExceptionData(Exception e);
6+
7+
// ReSharper disable once UnusedParameter.Global
68
public abstract IList<TPostProtoBuf> GetValidPosts(TResponse response, CrawlRequestFlag flag);
79
public abstract TbClient.Page? GetResponsePage(TResponse response);
810
protected abstract RepeatedField<TPostProtoBuf> GetResponsePostList(TResponse response);

c#/crawler/src/Worker/ArchiveCrawlWorker.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,12 @@ private async Task<SaverChangeSet<ThreadPost>?> CrawlThreads
120120
var crawler = facadeFactory.Value(fid, forumName);
121121
var savedThreads = (await crawler.CrawlPageRange(
122122
page, page, stoppingToken)).SaveCrawled(stoppingToken);
123+
124+
// ReSharper disable once InvertIf
123125
if (savedThreads != null)
124126
{
125-
var failureCountsKeyByTid = savedThreads.NewlyAdded.ToDictionary(th => th.Tid, _ => (FailureCount)0);
127+
var failureCountsKeyByTid = savedThreads.NewlyAdded
128+
.ToDictionary(th => th.Tid, _ => (FailureCount)0);
126129
await using var threadLate = threadLateCrawlerAndSaverFactory();
127130
await threadLate.Value(fid).CrawlThenSave(failureCountsKeyByTid, stoppingToken);
128131
}

c#/crawler/src/Worker/PushAllPostContentsIntoSonicWorker.cs

+19-19
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace tbm.Crawler.Worker;
44

5+
// ReSharper disable once UnusedType.Global
56
public class PushAllPostContentsIntoSonicWorker(
67
ILogger<PushAllPostContentsIntoSonicWorker> logger,
78
IConfiguration config,
@@ -82,27 +83,26 @@ private int PushPostContentsWithTiming<T>(
8283
var pushedCount = acc.Count + 1;
8384
var totalPushedCount = previousPushedPostCount + pushedCount;
8485
var ca = ArchiveCrawlWorker.GetCumulativeAverage(elapsedMs, acc.DurationCa, pushedCount);
85-
if (pushedCount % 1000 == 0)
86-
{
87-
static double GetPercentage(float current, float total, int digits = 2) =>
88-
Math.Round(current / total * 100, digits);
86+
if (pushedCount % 1000 != 0) return (pushedCount, ca);
87+
88+
static double GetPercentage(float current, float total, int digits = 2) =>
89+
Math.Round(current / total * 100, digits);
8990
#pragma warning disable IDE0042 // Deconstruct variable declaration
90-
var currentForumEta = ArchiveCrawlWorker.GetEta(postApproxCount, pushedCount, ca);
91-
var totalForumEta = ArchiveCrawlWorker.GetEta(forumsPostTotalApproxCount, totalPushedCount, ca);
91+
var currentForumEta = ArchiveCrawlWorker.GetEta(postApproxCount, pushedCount, ca);
92+
var totalForumEta = ArchiveCrawlWorker.GetEta(forumsPostTotalApproxCount, totalPushedCount, ca);
9293
#pragma warning restore IDE0042 // Deconstruct variable declaration
93-
logger.LogInformation("Pushing progress for {} in fid {}: {}/~{} ({}%) cumulativeAvg={:F3}ms"
94-
+ " ETA: {} @ {}, Total forums progress: {}/{} posts: {}/~{} ({}%) ETA {} @ {}",
95-
postTypeInLog, fid,
96-
pushedCount, postApproxCount, GetPercentage(pushedCount, postApproxCount),
97-
ca, currentForumEta.Relative, currentForumEta.At, currentForumIndex, forumCount,
98-
totalPushedCount, forumsPostTotalApproxCount, GetPercentage(totalPushedCount, forumsPostTotalApproxCount),
99-
totalForumEta.Relative, totalForumEta.At);
100-
Console.Title = $"Pushing progress for {postTypeInLog} in fid {fid}"
101-
+ $": {pushedCount}/~{postApproxCount} ({GetPercentage(pushedCount, postApproxCount)}%)"
102-
+ $", Total forums progress: {currentForumIndex}/{forumCount} posts:"
103-
+ $" {totalPushedCount}/~{forumsPostTotalApproxCount} ({GetPercentage(totalPushedCount, forumsPostTotalApproxCount)}%)"
104-
+ $" ETA {totalForumEta.Relative} @ {totalForumEta.At}";
105-
}
94+
logger.LogInformation("Pushing progress for {} in fid {}: {}/~{} ({}%) cumulativeAvg={:F3}ms"
95+
+ " ETA: {} @ {}, Total forums progress: {}/{} posts: {}/~{} ({}%) ETA {} @ {}",
96+
postTypeInLog, fid,
97+
pushedCount, postApproxCount, GetPercentage(pushedCount, postApproxCount),
98+
ca, currentForumEta.Relative, currentForumEta.At, currentForumIndex, forumCount,
99+
totalPushedCount, forumsPostTotalApproxCount, GetPercentage(totalPushedCount, forumsPostTotalApproxCount),
100+
totalForumEta.Relative, totalForumEta.At);
101+
Console.Title = $"Pushing progress for {postTypeInLog} in fid {fid}"
102+
+ $": {pushedCount}/~{postApproxCount} ({GetPercentage(pushedCount, postApproxCount)}%)"
103+
+ $", Total forums progress: {currentForumIndex}/{forumCount} posts:"
104+
+ $" {totalPushedCount}/~{forumsPostTotalApproxCount} ({GetPercentage(totalPushedCount, forumsPostTotalApproxCount)}%)"
105+
+ $" ETA {totalForumEta.Relative} @ {totalForumEta.At}";
106106
return (pushedCount, ca);
107107
});
108108
logger.LogInformation("Pushing {} historical {}' content into sonic for fid {} finished after {} (total={}ms, cumulativeAvg={:F3}ms)",

c#/crawler/src/Worker/ResumeSuspendPostContentsPushingWorker.cs

+2
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ protected override Task DoWork(CancellationToken stoppingToken)
3737
logger.LogWarning("Malformed fid {} when resume suspend post contents push into sonic, line={}", fidStr, line);
3838
return null;
3939
}
40+
41+
// ReSharper disable once InvertIf
4042
if (!PostId.TryParse(postIdStr, out var postId))
4143
{
4244
logger.LogWarning("Malformed post id {} when resume suspend post contents push into sonic, line={}", postIdStr, line);

c#/crawler/src/Worker/RetryCrawlWorker.cs

+13-13
Original file line numberDiff line numberDiff line change
@@ -37,20 +37,20 @@ protected override async Task DoWork(CancellationToken stoppingToken)
3737
var pages = failureCountsKeyByPage.Keys.ToList();
3838
FailureCount FailureCountSelector(Page p) => failureCountsKeyByPage[p];
3939

40-
if (lockType == "thread")
40+
switch (lockType)
4141
{
42-
await RetryThread(fid, pages,
43-
failureCountsKeyByPage.Count, FailureCountSelector, stoppingToken);
44-
}
45-
else if (lockType == "reply" && tid != null)
46-
{
47-
await RetryReply(fid, tid.Value, pages,
48-
failureCountsKeyByPage.Count, FailureCountSelector, stoppingToken);
49-
}
50-
else if (lockType == "subReply" && tid != null && pid != null)
51-
{
52-
await RetrySubReply(fid, tid.Value, pid.Value, pages,
53-
failureCountsKeyByPage.Count, FailureCountSelector, stoppingToken);
42+
case "thread":
43+
await RetryThread(fid, pages,
44+
failureCountsKeyByPage.Count, FailureCountSelector, stoppingToken);
45+
break;
46+
case "reply" when tid != null:
47+
await RetryReply(fid, tid.Value, pages,
48+
failureCountsKeyByPage.Count, FailureCountSelector, stoppingToken);
49+
break;
50+
case "subReply" when tid != null && pid != null:
51+
await RetrySubReply(fid, tid.Value, pid.Value, pages,
52+
failureCountsKeyByPage.Count, FailureCountSelector, stoppingToken);
53+
break;
5454
}
5555
};
5656

c#/imagePipeline/src/Consumer/HashConsumer.cs

+7-7
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,13 @@ private static Func<ImageKeyWithMatrix, KeyValuePair<ImageKeyWithMatrix, byte[]>
7070
{
7171
stoppingToken.ThrowIfCancellationRequested();
7272
var mat = imageKeyWithMatrix.Matrix;
73-
if (mat.Width > 100 || mat.Height > 100)
74-
{ // not preserve the original aspect ratio
75-
// https://stackoverflow.com/questions/44650888/resize-an-image-without-distortion-opencv
76-
using var thumbnail = mat.Resize(new(100, 100), interpolation: InterpolationFlags.Area);
77-
return new(imageKeyWithMatrix, GetThumbHashForMatrix(thumbnail));
78-
}
79-
return new(imageKeyWithMatrix, GetThumbHashForMatrix(mat));
73+
if (mat is {Width: <= 100, Height: <= 100})
74+
return new(imageKeyWithMatrix, GetThumbHashForMatrix(mat));
75+
76+
// not preserve the original aspect ratio
77+
// https://stackoverflow.com/questions/44650888/resize-an-image-without-distortion-opencv
78+
using var thumbnail = mat.Resize(new(100, 100), interpolation: InterpolationFlags.Area);
79+
return new(imageKeyWithMatrix, GetThumbHashForMatrix(thumbnail));
8080

8181
static byte[] GetThumbHashForMatrix(Mat mat)
8282
{

c#/imagePipeline/src/Consumer/MetadataConsumer.cs

+54-54
Original file line numberDiff line numberDiff line change
@@ -119,58 +119,58 @@ private Func<ImageWithBytes, ImageMetadata> GetImageMetaData
119119

120120
var ret = CreateEmbeddedFromProfile<ExifProfile, ImageMetadata.Exif>
121121
(_commonEmbeddedMetadataXxHash3ToIgnore.Exif, exif, i => i.ToByteArray());
122-
if (ret != null && exif != null)
123-
{ // https://exiftool.org/TagNames/EXIF.html, https://exiv2.org/tags.html
124-
ret.Orientation = exif.TryGetValue(ExifTag.Orientation, out var orientation)
125-
? Enum.GetName((ImageMetadata.Exif.ExifOrientation)orientation.Value)
126-
: null;
127-
ret.ImageDescription = GetExifTagValueOrNull(ExifTag.ImageDescription).NullIfEmpty();
128-
ret.UserComment = GetExifTagValueOrNull2(ExifTag.UserComment).ToString().NullIfEmpty();
129-
ret.Artist = GetExifTagValueOrNull(ExifTag.Artist).NullIfEmpty();
130-
ret.XpAuthor = GetExifTagValueOrNull(ExifTag.XPAuthor).NullIfEmpty();
131-
ret.Copyright = GetExifTagValueOrNull(ExifTag.Copyright).NullIfEmpty();
132-
ret.ImageUniqueId = GetExifTagValueOrNull(ExifTag.ImageUniqueID).NullIfEmpty();
133-
ret.BodySerialNumber = GetExifTagValueOrNull(ExifTag.SerialNumber).NullIfEmpty();
134-
ret.Make = GetExifTagValueOrNull(ExifTag.Make).NullIfEmpty();
135-
ret.Model = GetExifTagValueOrNull(ExifTag.Model).NullIfEmpty();
136-
ret.Software = GetExifTagValueOrNull(ExifTag.Software).NullIfEmpty();
137-
ret.CustomRendered = GetExifTagValueOrNull2(ExifTag.CustomRendered);
138-
139-
var parsedDateTime = ExifDateTimeTagValuesParser.ParseExifDateTimeOrNull(
140-
GetExifTagValueOrNull(ExifTag.DateTime), GetExifTagValueOrNull(ExifTag.SubsecTime));
141-
ret.DateTime = parsedDateTime?.DateTime;
142-
ret.DateTimeOffset = parsedDateTime?.Offset;
143-
144-
var parsedDateTimeDigitized = ExifDateTimeTagValuesParser.ParseExifDateTimeOrNull(
145-
GetExifTagValueOrNull(ExifTag.DateTimeDigitized), GetExifTagValueOrNull(ExifTag.SubsecTimeDigitized));
146-
ret.DateTimeDigitized = parsedDateTimeDigitized?.DateTime;
147-
ret.DateTimeDigitizedOffset = parsedDateTimeDigitized?.Offset;
148-
149-
var parsedDateTimeOriginal = ExifDateTimeTagValuesParser.ParseExifDateTimeOrNull(
150-
GetExifTagValueOrNull(ExifTag.DateTimeOriginal), GetExifTagValueOrNull(ExifTag.SubsecTimeOriginal));
151-
ret.DateTimeOriginal = parsedDateTimeOriginal?.DateTime;
152-
ret.DateTimeOriginalOffset = parsedDateTimeOriginal?.Offset;
153-
154-
ret.OffsetTime = GetExifTagValueOrNull(ExifTag.OffsetTime).NullIfEmpty();
155-
ret.OffsetTimeDigitized = GetExifTagValueOrNull(ExifTag.OffsetTimeDigitized).NullIfEmpty();
156-
ret.OffsetTimeOriginal = GetExifTagValueOrNull(ExifTag.OffsetTimeOriginal).NullIfEmpty();
157-
158-
ret.GpsDateTime = ExifGpsTagValuesParser.ParseGpsDateTimeOrNull(
159-
GetExifTagValueOrNull(ExifTag.GPSTimestamp),
160-
GetExifTagValueOrNull(ExifTag.GPSDateStamp));
161-
ret.GpsCoordinate = ExifGpsTagValuesParser.ParseGpsCoordinateOrNull(exif.Values,
162-
GetExifTagValueOrNull(ExifTag.GPSLatitude),
163-
GetExifTagValueOrNull(ExifTag.GPSLatitudeRef),
164-
GetExifTagValueOrNull(ExifTag.GPSLongitude),
165-
GetExifTagValueOrNull(ExifTag.GPSLongitudeRef));
166-
ret.GpsImgDirection = GetExifTagValueOrNull2(ExifTag.GPSImgDirection)?.ToSingle().NanToNull();
167-
ret.GpsImgDirectionRef = GetExifTagValueOrNull(ExifTag.GPSImgDirectionRef).NullIfEmpty();
168-
169-
ret.TagNames = exif.Values.Select(i => new ImageMetadata.Exif.TagName {Name = i.Tag.ToString()})
170-
171-
// tags might be duplicated in EXIF with same or different values
172-
.DistinctBy(tagName => tagName.Name).ToList();
173-
}
122+
if (ret == null || exif == null) return ret;
123+
124+
// https://exiftool.org/TagNames/EXIF.html, https://exiv2.org/tags.html
125+
ret.Orientation = exif.TryGetValue(ExifTag.Orientation, out var orientation)
126+
? Enum.GetName((ImageMetadata.Exif.ExifOrientation)orientation.Value)
127+
: null;
128+
ret.ImageDescription = GetExifTagValueOrNull(ExifTag.ImageDescription).NullIfEmpty();
129+
ret.UserComment = GetExifTagValueOrNull2(ExifTag.UserComment).ToString().NullIfEmpty();
130+
ret.Artist = GetExifTagValueOrNull(ExifTag.Artist).NullIfEmpty();
131+
ret.XpAuthor = GetExifTagValueOrNull(ExifTag.XPAuthor).NullIfEmpty();
132+
ret.Copyright = GetExifTagValueOrNull(ExifTag.Copyright).NullIfEmpty();
133+
ret.ImageUniqueId = GetExifTagValueOrNull(ExifTag.ImageUniqueID).NullIfEmpty();
134+
ret.BodySerialNumber = GetExifTagValueOrNull(ExifTag.SerialNumber).NullIfEmpty();
135+
ret.Make = GetExifTagValueOrNull(ExifTag.Make).NullIfEmpty();
136+
ret.Model = GetExifTagValueOrNull(ExifTag.Model).NullIfEmpty();
137+
ret.Software = GetExifTagValueOrNull(ExifTag.Software).NullIfEmpty();
138+
ret.CustomRendered = GetExifTagValueOrNull2(ExifTag.CustomRendered);
139+
140+
var parsedDateTime = ExifDateTimeTagValuesParser.ParseExifDateTimeOrNull(
141+
GetExifTagValueOrNull(ExifTag.DateTime), GetExifTagValueOrNull(ExifTag.SubsecTime));
142+
ret.DateTime = parsedDateTime?.DateTime;
143+
ret.DateTimeOffset = parsedDateTime?.Offset;
144+
145+
var parsedDateTimeDigitized = ExifDateTimeTagValuesParser.ParseExifDateTimeOrNull(
146+
GetExifTagValueOrNull(ExifTag.DateTimeDigitized), GetExifTagValueOrNull(ExifTag.SubsecTimeDigitized));
147+
ret.DateTimeDigitized = parsedDateTimeDigitized?.DateTime;
148+
ret.DateTimeDigitizedOffset = parsedDateTimeDigitized?.Offset;
149+
150+
var parsedDateTimeOriginal = ExifDateTimeTagValuesParser.ParseExifDateTimeOrNull(
151+
GetExifTagValueOrNull(ExifTag.DateTimeOriginal), GetExifTagValueOrNull(ExifTag.SubsecTimeOriginal));
152+
ret.DateTimeOriginal = parsedDateTimeOriginal?.DateTime;
153+
ret.DateTimeOriginalOffset = parsedDateTimeOriginal?.Offset;
154+
155+
ret.OffsetTime = GetExifTagValueOrNull(ExifTag.OffsetTime).NullIfEmpty();
156+
ret.OffsetTimeDigitized = GetExifTagValueOrNull(ExifTag.OffsetTimeDigitized).NullIfEmpty();
157+
ret.OffsetTimeOriginal = GetExifTagValueOrNull(ExifTag.OffsetTimeOriginal).NullIfEmpty();
158+
159+
ret.GpsDateTime = ExifGpsTagValuesParser.ParseGpsDateTimeOrNull(
160+
GetExifTagValueOrNull(ExifTag.GPSTimestamp),
161+
GetExifTagValueOrNull(ExifTag.GPSDateStamp));
162+
ret.GpsCoordinate = ExifGpsTagValuesParser.ParseGpsCoordinateOrNull(exif.Values,
163+
GetExifTagValueOrNull(ExifTag.GPSLatitude),
164+
GetExifTagValueOrNull(ExifTag.GPSLatitudeRef),
165+
GetExifTagValueOrNull(ExifTag.GPSLongitude),
166+
GetExifTagValueOrNull(ExifTag.GPSLongitudeRef));
167+
ret.GpsImgDirection = GetExifTagValueOrNull2(ExifTag.GPSImgDirection)?.ToSingle().NanToNull();
168+
ret.GpsImgDirectionRef = GetExifTagValueOrNull(ExifTag.GPSImgDirectionRef).NullIfEmpty();
169+
170+
ret.TagNames = exif.Values.Select(i => new ImageMetadata.Exif.TagName {Name = i.Tag.ToString()})
171+
172+
// tags might be duplicated in EXIF with same or different values
173+
.DistinctBy(tagName => tagName.Name).ToList();
174174
return ret;
175175
}
176176

@@ -199,7 +199,7 @@ private static class ExifGpsTagValuesParser
199199
}
200200

201201
public static Point? ParseGpsCoordinateOrNull(
202-
IReadOnlyList<IExifValue> allTagValues,
202+
IEnumerable<IExifValue> allTagValues,
203203
IEnumerable<Rational>? latitude,
204204
string? latitudeRef,
205205
IEnumerable<Rational>? longitude,
@@ -251,7 +251,7 @@ private static double ConvertDmsToDd(IReadOnlyList<double> dms)
251251
}
252252
}
253253

254-
private sealed partial class ExifDateTimeTagValuesParser
254+
private static partial class ExifDateTimeTagValuesParser
255255
{
256256
public static DateTimeAndOffset? ParseExifDateTimeOrNull(string? exifDateTime, string? exifFractionalSeconds)
257257
{ // https://gist.github.com/thanatos/eee17100476a336a711e

c#/imagePipeline/src/Db/ImageMetadata.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// ReSharper disable UnusedAutoPropertyAccessor.Global
22
// ReSharper disable PropertyCanBeMadeInitOnly.Global
33
// ReSharper disable UnusedMember.Global
4+
// ReSharper disable UnusedMemberInSuper.Global
45
using System.ComponentModel;
56
using SixLabors.ImageSharp.PixelFormats;
67
using Point = NetTopologySuite.Geometries.Point;
@@ -56,7 +57,7 @@ public enum ExifOrientation
5657
MirrorHorizontalRotate270Cw = 5,
5758
Rotate90Cw = 6,
5859
MirrorHorizontalRotate90Cw = 7,
59-
Rotate270Cw = 8,
60+
Rotate270Cw = 8
6061
}
6162

6263
[Key] public uint ImageId { get; set; }
@@ -90,7 +91,7 @@ public enum ExifOrientation
9091

9192
// workaround to work with MetadataConsumer.CreateEmbeddedFromProfile()
9293
// https://stackoverflow.com/questions/75266722/type-cannot-satisfy-the-new-constraint-on-parameter-tparam-because-type
93-
public ICollection<TagName> TagNames { get; set; } = new List<TagName>();
94+
public ICollection<TagName> TagNames { get; set; } = [];
9495

9596
public class TagName : IImageMetadata
9697
{

0 commit comments

Comments
 (0)