using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using EntityFrameworkCore.Projectables;
using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Helpers;
using Iceshrimp.Shared.Helpers;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Iceshrimp.Backend.Core.Database.Tables;
[Table("user")]
[Index(nameof(Host))]
[Index(nameof(UsernameLower), nameof(Host), IsUnique = true)]
[Index(nameof(UpdatedAt))]
[Index(nameof(UsernameLower))]
[Index(nameof(Uri))]
[Index(nameof(LastActiveDate))]
[Index(nameof(IsExplorable))]
[Index(nameof(IsAdmin))]
[Index(nameof(IsModerator))]
[Index(nameof(CreatedAt))]
[Index(nameof(Tags))]
[Index(nameof(AvatarId), IsUnique = true)]
[Index(nameof(BannerId), IsUnique = true)]
[Index(nameof(IsSuspended))]
public class User : IIdentifiable
{
///
/// The created date of the User.
///
[Column("createdAt")]
public DateTime CreatedAt { get; set; }
///
/// The updated date of the User.
///
[Column("updatedAt")]
public DateTime? UpdatedAt { get; set; }
[Column("lastFetchedAt")] public DateTime? LastFetchedAt { get; set; }
[Column("outboxFetchedAt")] public DateTime? OutboxFetchedAt { get; set; }
[Column("lastNoteAt")] public DateTime? LastNoteAt { get; set; }
[NotMapped]
[Projectable]
public bool NeedsUpdate =>
Host != null && (LastFetchedAt == null || LastFetchedAt < DateTime.Now - TimeSpan.FromHours(24));
///
/// The username of the User.
///
[Column("username")]
[StringLength(128)]
public string Username { get; set; } = null!;
///
/// The username (lowercased) of the User.
///
[Column("usernameLower")]
[StringLength(128)]
public string UsernameLower { get; set; } = null!;
///
/// The name of the User.
///
[Column("name")]
[StringLength(128)]
public string? DisplayName { get; set; }
///
/// The count of followers.
///
[Column("followersCount")]
public int FollowersCount { get; set; }
///
/// The count of following.
///
[Column("followingCount")]
public int FollowingCount { get; set; }
///
/// The count of notes.
///
[Column("notesCount")]
public int NotesCount { get; set; }
///
/// The ID of avatar DriveFile.
///
[Column("avatarId")]
[StringLength(32)]
public string? AvatarId { get; set; }
///
/// The ID of banner DriveFile.
///
[Column("bannerId")]
[StringLength(32)]
public string? BannerId { get; set; }
[Column("tags", TypeName = "character varying(128)[]")]
public List Tags { get; set; } = [];
///
/// Whether the User is suspended.
///
[Column("isSuspended")]
public bool IsSuspended { get; set; }
///
/// Whether the User is silenced.
///
[Column("isSilenced")]
public bool IsSilenced { get; set; }
///
/// Whether the User is locked.
///
[Column("isLocked")]
public bool IsLocked { get; set; }
///
/// Whether the User is a bot.
///
[Column("isBot")]
public bool IsBot { get; set; }
[Column("isSystem")] public bool IsSystemUser { get; set; }
[Column("isRelayActor")] public bool IsRelayActor { get; set; }
///
/// Whether the User is a cat.
///
[Column("isCat")]
public bool IsCat { get; set; }
///
/// Whether the User is the admin.
///
[Column("isAdmin")]
public bool IsAdmin { get; set; }
///
/// Whether the User is a moderator.
///
[Column("isModerator")]
public bool IsModerator { get; set; }
[Column("emojis", TypeName = "character varying(128)[]")]
public List Emojis { get; set; } = [];
///
/// The host of the User. It will be null if the origin of the user is local.
///
[Column("host")]
[StringLength(512)]
public string? Host { get; set; }
[NotMapped] [Projectable] public string Acct => Username + (Host != null ? "@" + Host : "");
[NotMapped] [Projectable] public string AcctWithPrefix => "acct:" + Acct;
///
/// The inbox URL of the User. It will be null if the origin of the user is local.
///
[Column("inbox")]
[StringLength(512)]
public string? Inbox { get; set; }
///
/// The outbox URL of the User. It will be null if the origin of the user is local, or if no outbox is present.
///
[Column("outbox")]
[StringLength(512)]
public string? Outbox { get; set; }
///
/// The sharedInbox URL of the User. It will be null if the origin of the user is local.
///
[Column("sharedInbox")]
[StringLength(512)]
public string? SharedInbox { get; set; }
///
/// The featured URL of the User. It will be null if the origin of the user is local.
///
[Column("featured")]
[StringLength(512)]
public string? Featured { get; set; }
///
/// The URI of the User. It will be null if the origin of the user is local.
///
[Column("uri")]
[StringLength(512)]
public string? Uri { get; set; }
///
/// Whether the User is explorable.
///
[Column("isExplorable")]
public bool IsExplorable { get; set; }
///
/// The URI of the user Follower Collection. It will be null if the origin of the user is local.
///
[Column("followersUri")]
[StringLength(512)]
public string? FollowersUri { get; set; }
[Column("lastActiveDate")] public DateTime? LastActiveDate { get; set; }
///
/// Whether the User is deleted.
///
[Column("isDeleted")]
public bool IsDeleted { get; set; }
///
/// Overrides user drive capacity limit
///
[Column("driveCapacityOverrideMb")]
public int? DriveCapacityOverrideMb { get; set; }
///
/// The URI of the new account of the User
///
[Column("movedToUri")]
[StringLength(512)]
public string? MovedToUri { get; set; }
///
/// URIs the user is known as too
///
[Column("alsoKnownAs")]
public List? AlsoKnownAs { get; set; }
///
/// Whether to speak as a cat if isCat.
///
[Column("speakAsCat")]
public bool SpeakAsCat { get; set; }
///
/// The blurhash of the avatar DriveFile
///
[Column("avatarBlurhash")]
[StringLength(128)]
public string? AvatarBlurhash { get; set; }
///
/// The blurhash of the banner DriveFile
///
[Column("bannerBlurhash")]
[StringLength(128)]
public string? BannerBlurhash { get; set; }
[Column("splitDomainResolved")] public bool SplitDomainResolved { get; set; }
[InverseProperty(nameof(Report.Assignee))]
public virtual ICollection AbuseUserReportAssignees { get; set; } = new List();
[InverseProperty(nameof(Report.Reporter))]
public virtual ICollection AbuseUserReportReporters { get; set; } = new List();
[InverseProperty(nameof(Report.TargetUser))]
public virtual ICollection AbuseUserReportTargetUsers { get; set; } = new List();
[InverseProperty(nameof(AnnouncementRead.User))]
public virtual ICollection AnnouncementReads { get; set; } = new List();
[InverseProperty(nameof(Antenna.User))]
public virtual ICollection Antennas { get; set; } = new List();
[InverseProperty(nameof(AttestationChallenge.User))]
public virtual ICollection AttestationChallenges { get; set; } =
new List();
[ForeignKey(nameof(AvatarId))]
[InverseProperty(nameof(DriveFile.UserAvatar))]
public virtual DriveFile? Avatar { get; set; }
[ForeignKey(nameof(BannerId))]
[InverseProperty(nameof(DriveFile.UserBanner))]
public virtual DriveFile? Banner { get; set; }
[InverseProperty(nameof(Tables.Blocking.Blockee))]
public virtual ICollection IncomingBlocks { get; set; } = new List();
[InverseProperty(nameof(Tables.Blocking.Blocker))]
public virtual ICollection OutgoingBlocks { get; set; } = new List();
[NotMapped] [Projectable] public virtual IEnumerable BlockedBy => IncomingBlocks.Select(p => p.Blocker);
[NotMapped] [Projectable] public virtual IEnumerable Blocking => OutgoingBlocks.Select(p => p.Blockee);
[InverseProperty(nameof(ChannelFollowing.Follower))]
public virtual ICollection ChannelFollowings { get; set; } = new List();
[InverseProperty(nameof(Channel.User))]
public virtual ICollection Channels { get; set; } = new List();
[InverseProperty(nameof(Clip.User))] public virtual ICollection Clips { get; set; } = new List();
[InverseProperty(nameof(DriveFile.User))]
public virtual ICollection DriveFiles { get; set; } = new List();
[InverseProperty(nameof(DriveFolder.User))]
public virtual ICollection DriveFolders { get; set; } = new List();
[InverseProperty(nameof(FollowRequest.Followee))]
public virtual ICollection IncomingFollowRequests { get; set; } = new List();
[InverseProperty(nameof(FollowRequest.Follower))]
public virtual ICollection OutgoingFollowRequests { get; set; } = new List();
[NotMapped]
[Projectable]
public virtual IEnumerable ReceivedFollowRequests => IncomingFollowRequests.Select(p => p.Follower);
[NotMapped]
[Projectable]
public virtual IEnumerable SentFollowRequests => OutgoingFollowRequests.Select(p => p.Followee);
[InverseProperty(nameof(Tables.Following.Followee))]
public virtual ICollection IncomingFollowRelationships { get; set; } = new List();
[InverseProperty(nameof(Tables.Following.Follower))]
public virtual ICollection OutgoingFollowRelationships { get; set; } = new List();
[NotMapped]
[Projectable]
public virtual IEnumerable Followers => IncomingFollowRelationships.Select(p => p.Follower);
[NotMapped]
[Projectable]
public virtual IEnumerable Following => OutgoingFollowRelationships.Select(p => p.Followee);
[InverseProperty(nameof(GalleryLike.User))]
public virtual ICollection GalleryLikes { get; set; } = new List();
[InverseProperty(nameof(GalleryPost.User))]
public virtual ICollection GalleryPosts { get; set; } = new List();
[InverseProperty(nameof(Marker.User))]
public virtual ICollection Markers { get; set; } = new List();
[InverseProperty(nameof(MessagingMessage.Recipient))]
public virtual ICollection MessagingMessageRecipients { get; set; } =
new List();
[InverseProperty(nameof(MessagingMessage.User))]
public virtual ICollection MessagingMessageUsers { get; set; } = new List();
[InverseProperty(nameof(ModerationLog.User))]
public virtual ICollection ModerationLogs { get; set; } = new List();
[InverseProperty(nameof(Tables.Muting.Mutee))]
public virtual ICollection IncomingMutes { get; set; } = new List();
[InverseProperty(nameof(Tables.Muting.Muter))]
public virtual ICollection OutgoingMutes { get; set; } = new List();
[NotMapped] [Projectable] public virtual IEnumerable MutedBy => IncomingMutes.Select(p => p.Muter);
[NotMapped] [Projectable] public virtual IEnumerable Muting => OutgoingMutes.Select(p => p.Mutee);
[InverseProperty(nameof(NoteBookmark.User))]
public virtual ICollection NoteBookmarks { get; set; } = new List();
[NotMapped] [Projectable] public virtual IEnumerable BookmarkedNotes => NoteBookmarks.Select(p => p.Note);
[InverseProperty(nameof(NoteReaction.User))]
public virtual ICollection NoteLikes { get; set; } = new List();
[NotMapped] [Projectable] public virtual IEnumerable LikedNotes => NoteLikes.Select(p => p.Note);
[InverseProperty(nameof(NoteReaction.User))]
public virtual ICollection NoteReactions { get; set; } = new List();
[NotMapped]
[Projectable]
public virtual IEnumerable ReactedNotes => NoteReactions.Select(p => p.Note).Distinct();
[InverseProperty(nameof(NoteThreadMuting.User))]
public virtual ICollection NoteThreadMutings { get; set; } = new List();
[InverseProperty(nameof(NoteUnread.User))]
public virtual ICollection NoteUnreads { get; set; } = new List();
[InverseProperty(nameof(NoteWatching.User))]
public virtual ICollection NoteWatchings { get; set; } = new List();
[InverseProperty(nameof(Note.User))] public virtual ICollection Notes { get; set; } = new List();
[InverseProperty(nameof(Notification.Notifiee))]
public virtual ICollection NotificationNotifiees { get; set; } = new List();
[InverseProperty(nameof(Notification.Notifier))]
public virtual ICollection NotificationNotifiers { get; set; } = new List();
[InverseProperty(nameof(OauthToken.User))]
public virtual ICollection OauthTokens { get; set; } = new List();
[InverseProperty(nameof(PageLike.User))]
public virtual ICollection PageLikes { get; set; } = new List();
[InverseProperty(nameof(Page.User))] public virtual ICollection Pages { get; set; } = new List();
[InverseProperty(nameof(PasswordResetRequest.User))]
public virtual ICollection PasswordResetRequests { get; set; } =
new List();
[InverseProperty(nameof(PollVote.User))]
public virtual ICollection PollVotes { get; set; } = new List();
[InverseProperty(nameof(PromoRead.User))]
public virtual ICollection PromoReads { get; set; } = new List();
[InverseProperty(nameof(RegistryItem.User))]
public virtual ICollection RegistryItems { get; set; } = new List();
[InverseProperty(nameof(RenoteMuting.Mutee))]
public virtual ICollection RenoteMutingMutees { get; set; } = new List();
[InverseProperty(nameof(RenoteMuting.Muter))]
public virtual ICollection RenoteMutingMuters { get; set; } = new List();
[InverseProperty(nameof(Session.User))]
public virtual ICollection Sessions { get; set; } = new List();
[InverseProperty(nameof(SwSubscription.User))]
public virtual ICollection SwSubscriptions { get; set; } = new List();
[InverseProperty(nameof(PushSubscription.User))]
public virtual ICollection PushSubscriptions { get; set; } = new List();
[InverseProperty(nameof(UserGroupInvitation.User))]
public virtual ICollection UserGroupInvitations { get; set; } =
new List();
[InverseProperty(nameof(UserGroupMember.User))]
public virtual ICollection UserGroupMemberships { get; set; } = new List();
[InverseProperty(nameof(UserGroup.User))]
public virtual ICollection UserGroups { get; set; } = new List();
[InverseProperty(nameof(Tables.UserKeypair.User))]
public virtual UserKeypair? UserKeypair { get; set; }
[InverseProperty(nameof(UserListMember.User))]
public virtual ICollection UserListMembers { get; set; } = new List();
[InverseProperty(nameof(UserList.User))]
public virtual ICollection UserLists { get; set; } = new List();
[InverseProperty(nameof(UserNotePin.User))]
public virtual ICollection UserNotePins { get; set; } = new List();
[NotMapped] [Projectable] public virtual IEnumerable PinnedNotes => UserNotePins.Select(p => p.Note);
[InverseProperty(nameof(Tables.UserProfile.User))]
public virtual UserProfile? UserProfile { get; set; }
[InverseProperty(nameof(Tables.UserPublickey.User))]
public virtual UserPublickey? UserPublickey { get; set; }
[InverseProperty(nameof(Tables.UserSettings.User))]
public virtual UserSettings? UserSettings { get; set; }
[InverseProperty(nameof(UserSecurityKey.User))]
public virtual ICollection UserSecurityKeys { get; set; } = new List();
[InverseProperty(nameof(Webhook.User))]
public virtual ICollection Webhooks { get; set; } = new List();
[InverseProperty(nameof(Filter.User))]
public virtual ICollection Filters { get; set; } = new List();
[NotMapped] public bool? PrecomputedIsBlocking { get; set; }
[NotMapped] public bool? PrecomputedIsBlockedBy { get; set; }
[NotMapped] public bool? PrecomputedIsMuting { get; set; }
[NotMapped] public bool? PrecomputedIsMutedBy { get; set; }
[NotMapped] public bool? PrecomputedIsFollowing { get; set; }
[NotMapped] public bool? PrecomputedIsFollowedBy { get; set; }
[NotMapped] public bool? PrecomputedIsRequested { get; set; }
[NotMapped] public bool? PrecomputedIsRequestedBy { get; set; }
[Projectable] public bool IsLocalUser => Host == null;
[Projectable] public bool IsRemoteUser => Host != null;
[Projectable] public string IdenticonUrlPath => $"/identicon/{Id}";
[Key]
[Column("id")]
[StringLength(32)]
public string Id { get; set; } = null!;
[Projectable]
public string GetFqnLower(string accountDomain) => UsernameLower + "@" + (Host ?? accountDomain);
[Projectable]
public string GetFqn(string accountDomain) => Username + "@" + (Host ?? accountDomain);
[Projectable]
public bool DisplayNameContainsCaseInsensitive(string str) =>
DisplayName != null && EF.Functions.ILike(DisplayName, "%" + EfHelpers.EscapeLikeQuery(str) + "%", @"\");
[Projectable]
public bool UsernameContainsCaseInsensitive(string str) => UsernameLower.Contains(str.ToLowerInvariant());
[Projectable]
public bool FqnContainsCaseInsensitive(string str, string accountDomain) =>
GetFqnLower(accountDomain).Contains(str.ToLowerInvariant());
[Projectable]
public bool UsernameOrFqnContainsCaseInsensitive(string str, string accountDomain) =>
str.Contains('@') ? FqnContainsCaseInsensitive(str, accountDomain) : UsernameContainsCaseInsensitive(str);
[Projectable]
public bool DisplayNameOrUsernameOrFqnContainsCaseInsensitive(string str, string accountDomain) =>
str.Contains('@') && !str.Contains(' ')
? FqnContainsCaseInsensitive(str, accountDomain)
: UsernameContainsCaseInsensitive(str) || DisplayNameContainsCaseInsensitive(str);
[Projectable]
public bool IsBlockedBy(User user) => BlockedBy.Contains(user);
[Projectable]
public bool IsBlocking(User user) => Blocking.Contains(user);
[Projectable]
public bool IsFollowedBy(User user) => Followers.Contains(user);
[Projectable]
public bool IsFollowing(User user) => Following.Contains(user);
[Projectable]
public bool IsRequestedBy(User user) => ReceivedFollowRequests.Contains(user);
[Projectable]
public bool IsRequested(User user) => SentFollowRequests.Contains(user);
[Projectable]
public bool IsMutedBy(User user) => MutedBy.Contains(user);
[Projectable]
public bool IsMuting(User user) => Muting.Contains(user);
[Projectable]
public bool HasPinned(Note note) => PinnedNotes.Contains(note);
[Projectable]
public bool HasBookmarked(Note note) => BookmarkedNotes.Contains(note);
[Projectable]
public bool HasLiked(Note note) => LikedNotes.Contains(note);
[Projectable]
public bool HasReacted(Note note) => ReactedNotes.Contains(note);
[Projectable]
public bool HasRenoted(Note note) => Notes.Any(p => p.Renote == note && p.User == this);
[Projectable]
public bool HasReplied(Note note) => Notes.Any(p => p.Reply == note && p.User == this);
[Projectable]
public bool HasVoted(Note note) => PollVotes.Any(p => p.Note == note && p.User == this);
[Projectable]
public bool HasInteractedWith(Note note) =>
HasLiked(note)
|| HasReacted(note)
|| HasBookmarked(note)
|| HasReplied(note)
|| HasRenoted(note)
|| HasVoted(note);
[Projectable]
public bool ProhibitInteractionWith(User user) => IsBlocking(user) || IsBlockedBy(user);
public User WithPrecomputedBlockStatus(bool blocking, bool blockedBy)
{
PrecomputedIsBlocking = blocking;
PrecomputedIsBlockedBy = blockedBy;
return this;
}
public User WithPrecomputedMuteStatus(bool muting, bool mutedBy)
{
PrecomputedIsMuting = muting;
PrecomputedIsMutedBy = mutedBy;
return this;
}
public User WithPrecomputedFollowStatus(bool following, bool followedBy, bool requested, bool requestedBy)
{
PrecomputedIsFollowing = following;
PrecomputedIsFollowedBy = followedBy;
PrecomputedIsRequested = requested;
PrecomputedIsRequestedBy = requestedBy;
return this;
}
public string GetPublicUrl(Config.InstanceSection config) => GetPublicUrl(config.WebDomain);
public string GetPublicUri(Config.InstanceSection config) => GetPublicUri(config.WebDomain);
public string GetUriOrPublicUri(Config.InstanceSection config) => GetUriOrPublicUri(config.WebDomain);
public string GetIdenticonUrl(Config.InstanceSection config) => GetIdenticonUrl(config.WebDomain);
public string GetPublicUri(string webDomain) => Host == null
? $"https://{webDomain}/users/{Id}"
: throw new Exception("Cannot access PublicUri for remote user");
public string GetUriOrPublicUri(string webDomain) => Uri ?? GetPublicUri(webDomain);
public string GetPublicUrl(string webDomain) => Host == null
? $"https://{webDomain}{PublicUrlPath}"
: throw new Exception("Cannot access PublicUrl for remote user");
[Projectable] public string PublicUrlPath => $"/@{Username}";
public string GetIdenticonUrl(string webDomain) => $"https://{webDomain}{IdenticonUrlPath}";
public string GetAvatarUrl(Config.InstanceSection config)
=> $"https://{config.WebDomain}/avatars/{Id}/{AvatarId ?? "identicon"}";
public string? GetBannerUrl(Config.InstanceSection config)
=> BannerId != null ? $"https://{config.WebDomain}/banners/{Id}/{BannerId}" : null;
private class EntityTypeConfiguration : IEntityTypeConfiguration
{
public void Configure(EntityTypeBuilder entity)
{
entity.Property(e => e.AlsoKnownAs).HasComment("URIs the user is known as too");
entity.Property(e => e.AvatarBlurhash).HasComment("The blurhash of the avatar DriveFile");
entity.Property(e => e.AvatarId).HasComment("The ID of avatar DriveFile.");
entity.Property(e => e.BannerBlurhash).HasComment("The blurhash of the banner DriveFile");
entity.Property(e => e.BannerId).HasComment("The ID of banner DriveFile.");
entity.Property(e => e.CreatedAt).HasComment("The created date of the User.");
entity.Property(e => e.DriveCapacityOverrideMb).HasComment("Overrides user drive capacity limit");
entity.Property(e => e.Emojis).HasDefaultValueSql("'{}'::character varying[]");
entity.Property(e => e.Featured)
.HasComment("The featured URL of the User. It will be null if the origin of the user is local.");
entity.Property(e => e.FollowersCount)
.HasDefaultValue(0)
.HasComment("The count of followers.");
entity.Property(e => e.FollowersUri)
.HasComment("The URI of the user Follower Collection. It will be null if the origin of the user is local.");
entity.Property(e => e.FollowingCount)
.HasDefaultValue(0)
.HasComment("The count of following.");
entity.Property(e => e.Host)
.HasComment("The host of the User. It will be null if the origin of the user is local.");
entity.Property(e => e.Inbox)
.HasComment("The inbox URL of the User. It will be null if the origin of the user is local.");
entity.Property(e => e.IsAdmin)
.HasDefaultValue(false)
.HasComment("Whether the User is the admin.");
entity.Property(e => e.IsBot)
.HasDefaultValue(false)
.HasComment("Whether the User is a bot.");
entity.Property(e => e.IsCat)
.HasDefaultValue(false)
.HasComment("Whether the User is a cat.");
entity.Property(e => e.IsDeleted)
.HasDefaultValue(false)
.HasComment("Whether the User is deleted.");
entity.Property(e => e.IsExplorable)
.HasDefaultValue(true)
.HasComment("Whether the User is explorable.");
entity.Property(e => e.IsLocked)
.HasDefaultValue(false)
.HasComment("Whether the User is locked.");
entity.Property(e => e.IsModerator)
.HasDefaultValue(false)
.HasComment("Whether the User is a moderator.");
entity.Property(e => e.IsSilenced)
.HasDefaultValue(false)
.HasComment("Whether the User is silenced.");
entity.Property(e => e.IsSuspended)
.HasDefaultValue(false)
.HasComment("Whether the User is suspended.");
entity.Property(e => e.MovedToUri).HasComment("The URI of the new account of the User");
entity.Property(e => e.DisplayName).HasComment("The name of the User.");
entity.Property(e => e.NotesCount)
.HasDefaultValue(0)
.HasComment("The count of notes.");
entity.Property(e => e.SharedInbox)
.HasComment("The sharedInbox URL of the User. It will be null if the origin of the user is local.");
entity.Property(e => e.SpeakAsCat)
.HasDefaultValue(true)
.HasComment("Whether to speak as a cat if isCat.");
entity.Property(e => e.Tags).HasDefaultValueSql("'{}'::character varying[]");
entity.Property(e => e.UpdatedAt).HasComment("The updated date of the User.");
entity.Property(e => e.Uri)
.HasComment("The URI of the User. It will be null if the origin of the user is local.");
entity.Property(e => e.Username).HasComment("The username of the User.");
entity.Property(e => e.UsernameLower).HasComment("The username (lowercased) of the User.");
entity.Property(e => e.SplitDomainResolved).HasDefaultValue(false);
entity.HasOne(d => d.Avatar)
.WithOne(p => p.UserAvatar)
.OnDelete(DeleteBehavior.SetNull);
entity.HasOne(d => d.Banner)
.WithOne(p => p.UserBanner)
.OnDelete(DeleteBehavior.SetNull);
}
}
}