using Microsoft.EntityFrameworkCore; using System.Security.Claims; using Socialize.Api.Data; using Socialize.Api.Infrastructure.Security; using Socialize.Api.Modules.Identity.Contracts; using Socialize.Api.Modules.Identity.Data; using Socialize.Api.Modules.Assets.Data; using Socialize.Api.Modules.Approvals.Data; using Socialize.Api.Modules.Channels.Data; using Socialize.Api.Modules.Comments.Data; using Socialize.Api.Modules.ContentItems.Data; using Socialize.Api.Modules.Clients.Data; using Socialize.Api.Modules.Notifications.Data; using Socialize.Api.Modules.Campaigns.Data; using Socialize.Api.Modules.Organizations.Data; using Socialize.Api.Modules.Organizations.Services; using Socialize.Api.Modules.Workspaces.Data; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; namespace Socialize.Api.Infrastructure.Development; public static class DevelopmentSeedExtensions { private static readonly Guid OrganizationId = Guid.Parse("99999999-9999-9999-9999-999999999999"); private static readonly Guid WorkspaceId = Guid.Parse("11111111-1111-1111-1111-111111111111"); private static readonly Guid AtlasWorkspaceId = Guid.Parse("11111111-1111-1111-1111-222222222222"); private static readonly Guid ScopedClientId = Guid.Parse("22222222-2222-2222-2222-222222222222"); private static readonly Guid HiddenClientId = Guid.Parse("22222222-2222-2222-2222-333333333333"); private static readonly Guid ScopedCampaignId = Guid.Parse("33333333-3333-3333-3333-333333333333"); private static readonly Guid HiddenCampaignId = Guid.Parse("33333333-3333-3333-3333-444444444444"); private static readonly Guid LumaInstagramChannelId = Guid.Parse("33333333-3333-3333-3333-000000000001"); private static readonly Guid LumaTikTokChannelId = Guid.Parse("33333333-3333-3333-3333-000000000002"); private static readonly Guid AtlasInstagramChannelId = Guid.Parse("33333333-3333-3333-3333-000000000003"); private static readonly Guid ScopedContentItemId = Guid.Parse("44444444-4444-4444-4444-444444444444"); private static readonly Guid HiddenContentItemId = Guid.Parse("44444444-4444-4444-4444-555555555555"); private static readonly Guid ScopedAssetId = Guid.Parse("55555555-5555-5555-5555-555555555555"); private static readonly Guid ScopedApprovalRequestId = Guid.Parse("66666666-6666-6666-6666-666666666666"); private static readonly Guid ClientCommentId = Guid.Parse("77777777-7777-7777-7777-777777777777"); private static readonly Guid NotificationId = Guid.Parse("88888888-8888-8888-8888-888888888888"); public static async Task UseDevelopmentSeedAsync( this IApplicationBuilder app, CancellationToken cancellationToken = default) { IHostEnvironment environment = app.ApplicationServices.GetRequiredService(); if (!environment.IsDevelopment()) { return app; } using IServiceScope scope = app.ApplicationServices.CreateScope(); IOptions options = scope.ServiceProvider.GetRequiredService>(); if (!options.Value.Enabled) { return app; } UserManager userManager = scope.ServiceProvider.GetRequiredService(); AppDbContext dbContext = scope.ServiceProvider.GetRequiredService(); User manager = await EnsureUserAsync( userManager, id: Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"), username: "manager", email: "manager@socialize.local", password: "manager", alias: "Northstar Manager", firstname: "Morgan", lastname: "Reid", portraitUrl: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=200&q=80", roles: [KnownRoles.Administrator, KnownRoles.Manager, KnownRoles.WorkspaceMember], claims: [ new Claim(KnownClaims.WorkspaceScope, WorkspaceId.ToString()), ]); User clientUser = await EnsureUserAsync( userManager, id: Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"), username: "client", email: "client@socialize.local", password: "client", alias: "Sofia Martin", firstname: "Sofia", lastname: "Martin", portraitUrl: "https://images.unsplash.com/photo-1544723795-3fb6469f5b39?auto=format&fit=crop&w=200&q=80", roles: [KnownRoles.Client, KnownRoles.WorkspaceMember], claims: [ new Claim(KnownClaims.WorkspaceScope, WorkspaceId.ToString()), new Claim(KnownClaims.ClientScope, ScopedClientId.ToString()), ]); User provider = await EnsureUserAsync( userManager, id: Guid.Parse("cccccccc-cccc-cccc-cccc-cccccccccccc"), username: "provider", email: "provider@socialize.local", password: "provider", alias: "Alex Studio", firstname: "Alex", lastname: "Studio", portraitUrl: "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=200&q=80", roles: [KnownRoles.Provider, KnownRoles.WorkspaceMember], claims: [ new Claim(KnownClaims.WorkspaceScope, WorkspaceId.ToString()), new Claim(KnownClaims.ClientScope, ScopedClientId.ToString()), new Claim(KnownClaims.CampaignScope, ScopedCampaignId.ToString()), ]); User dev = await EnsureUserAsync( userManager, id: Guid.Parse("dddddddd-dddd-dddd-dddd-dddddddddddd"), username: "dev", email: "dev@socialize.local", password: "dev", alias: "Socialize Dev", firstname: "Jo", lastname: "Bumble", portraitUrl: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=200&q=80", roles: [KnownRoles.Developer, KnownRoles.Administrator, KnownRoles.Manager, KnownRoles.WorkspaceMember], claims: [ ]); await EnsureOrganizationDataAsync( manager.Id, dev.Id, dbContext, cancellationToken); await EnsureWorkspaceDataAsync( manager.Id, clientUser.Id, provider.Id, dbContext, cancellationToken); return app; } private static async Task EnsureUserAsync( UserManager userManager, Guid id, string username, string email, string password, string alias, string firstname, string lastname, string? portraitUrl, IReadOnlyCollection roles, IReadOnlyCollection claims) { User? user = await userManager.FindByNameAsync(username) ?? await userManager.FindByEmailAsync(email); if (user is null) { user = new User { Id = id, UserName = username, Email = email, Alias = alias, Firstname = firstname, Lastname = lastname, PortraitUrl = portraitUrl, EmailConfirmed = true, }; IdentityResult createResult = await userManager.CreateAsync(user, password); if (!createResult.Succeeded) { throw new InvalidOperationException( $"Failed to seed development user '{username}': {string.Join(", ", createResult.Errors.Select(error => error.Description))}"); } } user.UserName = username; user.Email = email; user.Alias = alias; user.Firstname = firstname; user.Lastname = lastname; user.PortraitUrl = portraitUrl; user.EmailConfirmed = true; await userManager.UpdateAsync(user); if (!await userManager.CheckPasswordAsync(user, password)) { string resetToken = await userManager.GeneratePasswordResetTokenAsync(user); IdentityResult passwordResetResult = await userManager.ResetPasswordAsync(user, resetToken, password); if (!passwordResetResult.Succeeded) { throw new InvalidOperationException( $"Failed to set development password for '{username}': {string.Join(", ", passwordResetResult.Errors.Select(error => error.Description))}"); } } IList existingRoles = await userManager.GetRolesAsync(user); foreach (string role in roles.Except(existingRoles, StringComparer.Ordinal)) { await userManager.AddToRoleAsync(user, role); } foreach (string role in existingRoles .Where(role => role is KnownRoles.Manager or KnownRoles.Client or KnownRoles.Provider or KnownRoles.Administrator or KnownRoles.WorkspaceMember) .Except(roles, StringComparer.Ordinal)) { await userManager.RemoveFromRoleAsync(user, role); } IList existingClaims = await userManager.GetClaimsAsync(user); List managedClaims = existingClaims .Where(claim => claim.Type is KnownClaims.WorkspaceScope or KnownClaims.ClientScope or KnownClaims.CampaignScope or KnownClaims.Persona) .ToList(); foreach (Claim claim in managedClaims) { await userManager.RemoveClaimAsync(user, claim); } string persona = roles.Contains(KnownRoles.Manager, StringComparer.Ordinal) ? KnownRoles.Manager : roles.Contains(KnownRoles.Client, StringComparer.Ordinal) ? KnownRoles.Client : roles.Contains(KnownRoles.Provider, StringComparer.Ordinal) ? KnownRoles.Provider : KnownRoles.WorkspaceMember; foreach (Claim claim in claims.Concat([new Claim(KnownClaims.Persona, persona)])) { await userManager.AddClaimAsync(user, claim); } return user; } private static async Task EnsureOrganizationDataAsync( Guid managerUserId, Guid developerUserId, AppDbContext dbContext, CancellationToken cancellationToken) { Organization? organization = await dbContext.Organizations .SingleOrDefaultAsync(candidate => candidate.Id == OrganizationId, cancellationToken); if (organization is null) { organization = new Organization { Id = OrganizationId, Name = string.Empty, CreatedAt = DateTimeOffset.UtcNow, }; dbContext.Organizations.Add(organization); } organization.Name = "Northstar Agency"; organization.OwnerUserId = managerUserId; await UpsertOrganizationMembershipAsync( dbContext, Guid.Parse("99999999-9999-9999-9999-000000000001"), OrganizationId, managerUserId, OrganizationRoles.Owner, cancellationToken); await UpsertOrganizationMembershipAsync( dbContext, Guid.Parse("99999999-9999-9999-9999-000000000002"), OrganizationId, developerUserId, OrganizationRoles.Admin, cancellationToken); await dbContext.SaveChangesAsync(cancellationToken); } private static async Task UpsertOrganizationMembershipAsync( AppDbContext dbContext, Guid membershipId, Guid organizationId, Guid userId, string role, CancellationToken cancellationToken) { OrganizationMembership? membership = await dbContext.OrganizationMemberships .SingleOrDefaultAsync( candidate => candidate.OrganizationId == organizationId && candidate.UserId == userId, cancellationToken); if (membership is null) { membership = new OrganizationMembership { Id = membershipId, OrganizationId = organizationId, UserId = userId, Role = role, CreatedAt = DateTimeOffset.UtcNow, }; dbContext.OrganizationMemberships.Add(membership); } membership.Role = role; } private static async Task EnsureWorkspaceDataAsync( Guid managerUserId, Guid clientUserId, Guid providerUserId, AppDbContext dbContext, CancellationToken cancellationToken) { await UpsertWorkspaceAsync( dbContext, WorkspaceId, OrganizationId, managerUserId, "Luma Coffee", "America/Montreal", "/images/seed/luma-coffee-logo.svg", cancellationToken); await UpsertWorkspaceAsync( dbContext, AtlasWorkspaceId, OrganizationId, managerUserId, "Atlas Bakery", "America/Montreal", "/images/seed/atlas-bakery-logo.svg", cancellationToken); await UpsertClientAsync( dbContext, ScopedClientId, "Luma Coffee", "Active", "/images/seed/luma-coffee-logo.svg", "Sofia Martin", "client@socialize.local", WorkspaceId, cancellationToken); await UpsertClientAsync( dbContext, HiddenClientId, "Atlas Bakery", "Active", "/images/seed/atlas-bakery-logo.svg", "Nina Cole", "nina@atlasbakery.test", AtlasWorkspaceId, cancellationToken); await UpsertCampaignAsync( dbContext, ScopedCampaignId, WorkspaceId, ScopedClientId, "Spring Launch", "In progress", DateTimeOffset.UtcNow.AddDays(1), DateTimeOffset.UtcNow.AddDays(7), "Cross-channel launch campaign for the spring offer.", "Coordinate creative approvals before the final week.", cancellationToken); await UpsertCampaignAsync( dbContext, HiddenCampaignId, AtlasWorkspaceId, HiddenClientId, "Summer Retention", "Planned", DateTimeOffset.UtcNow.AddDays(10), DateTimeOffset.UtcNow.AddDays(16), "Retention campaign aimed at existing subscribers.", "Sequence email and paid social updates together.", cancellationToken); await UpsertChannelAsync( dbContext, LumaInstagramChannelId, WorkspaceId, "Luma Coffee Instagram", "Instagram", "@lumacoffee", null, cancellationToken); await UpsertChannelAsync( dbContext, LumaTikTokChannelId, WorkspaceId, "Luma Coffee TikTok", "TikTok", "@lumacoffee", null, cancellationToken); await UpsertChannelAsync( dbContext, AtlasInstagramChannelId, AtlasWorkspaceId, "Atlas Bakery Instagram", "Instagram", "@atlasbakery", null, cancellationToken); await UpsertContentItemAsync( dbContext, ScopedContentItemId, WorkspaceId, ScopedClientId, ScopedCampaignId, "Spring launch hero video", "Fresh seasonal menu launch across Instagram and TikTok.", "Luma Coffee Instagram, Luma Coffee TikTok", "In approval", DateTimeOffset.UtcNow.AddDays(3), "v3", 3, cancellationToken); await UpsertContentItemAsync( dbContext, HiddenContentItemId, AtlasWorkspaceId, HiddenClientId, HiddenCampaignId, "Bakery loyalty carousel", "Reward regular customers with a four-card retention carousel.", "Atlas Bakery Instagram", "Draft", DateTimeOffset.UtcNow.AddDays(10), "v1", 1, cancellationToken); await EnsureRevisionAsync(dbContext, Guid.Parse("44444444-4444-4444-4444-000000000001"), ScopedContentItemId, 1, "v1", "Spring launch hero video", "Initial draft for the seasonal menu launch.", "Luma Coffee Instagram, Luma Coffee TikTok", "Initial concept draft.", providerUserId, DateTimeOffset.UtcNow.AddDays(-5), cancellationToken); await EnsureRevisionAsync(dbContext, Guid.Parse("44444444-4444-4444-4444-000000000002"), ScopedContentItemId, 2, "v2", "Spring launch hero video", "Updated hook and transitions after internal review.", "Luma Coffee Instagram, Luma Coffee TikTok", "Addressed internal pacing feedback.", providerUserId, DateTimeOffset.UtcNow.AddDays(-3), cancellationToken); await EnsureRevisionAsync(dbContext, Guid.Parse("44444444-4444-4444-4444-000000000003"), ScopedContentItemId, 3, "v3", "Spring launch hero video", "Fresh seasonal menu launch across Instagram and TikTok.", "Luma Coffee Instagram, Luma Coffee TikTok", "Client-facing draft after copy cleanup.", providerUserId, DateTimeOffset.UtcNow.AddDays(-1), cancellationToken); await EnsureRevisionAsync(dbContext, Guid.Parse("44444444-4444-4444-4444-000000000004"), HiddenContentItemId, 1, "v1", "Bakery loyalty carousel", "Reward regular customers with a four-card retention carousel.", "Atlas Bakery Instagram", "First draft.", managerUserId, DateTimeOffset.UtcNow.AddDays(-2), cancellationToken); Asset? asset = await dbContext.Assets.SingleOrDefaultAsync(candidate => candidate.Id == ScopedAssetId, cancellationToken); if (asset is null) { asset = new Asset { Id = ScopedAssetId, AssetType = string.Empty, SourceType = string.Empty, DisplayName = string.Empty, CreatedAt = DateTimeOffset.UtcNow.AddDays(-4), }; dbContext.Assets.Add(asset); } asset.WorkspaceId = WorkspaceId; asset.ContentItemId = ScopedContentItemId; asset.AssetType = "Video"; asset.SourceType = "GoogleDrive"; asset.DisplayName = "Spring launch cut"; asset.GoogleDriveFileId = "dev-socialize-demo"; asset.GoogleDriveLink = "https://drive.google.com/file/d/dev-socialize-demo/view"; asset.PreviewUrl = "https://drive.google.com/thumbnail?id=dev-socialize-demo"; asset.CurrentRevisionNumber = 2; await dbContext.SaveChangesAsync(cancellationToken); await EnsureAssetRevisionAsync(dbContext, Guid.Parse("55555555-5555-5555-5555-000000000001"), ScopedAssetId, 1, "https://drive.google.com/file/d/dev-socialize-demo-v1/view", "https://drive.google.com/thumbnail?id=dev-socialize-demo-v1", "First uploaded cut from the editor.", providerUserId, DateTimeOffset.UtcNow.AddDays(-4), cancellationToken); await EnsureAssetRevisionAsync(dbContext, Guid.Parse("55555555-5555-5555-5555-000000000002"), ScopedAssetId, 2, "https://drive.google.com/file/d/dev-socialize-demo-v2/view", "https://drive.google.com/thumbnail?id=dev-socialize-demo-v2", "Re-export with pacing changes and updated title card.", providerUserId, DateTimeOffset.UtcNow.AddDays(-2), cancellationToken); Comment? comment = await dbContext.Comments.SingleOrDefaultAsync(candidate => candidate.Id == ClientCommentId, cancellationToken); if (comment is null) { comment = new Comment { Id = ClientCommentId, AuthorDisplayName = string.Empty, AuthorEmail = string.Empty, Body = string.Empty, CreatedAt = DateTimeOffset.UtcNow.AddHours(-20), }; dbContext.Comments.Add(comment); } comment.WorkspaceId = WorkspaceId; comment.ContentItemId = ScopedContentItemId; comment.AuthorUserId = clientUserId; comment.AuthorDisplayName = "Sofia Martin"; comment.AuthorEmail = "client@socialize.local"; comment.Body = "Please tighten the opening three seconds and make the launch CTA more explicit."; await dbContext.SaveChangesAsync(cancellationToken); ApprovalRequest? approvalRequest = await dbContext.ApprovalRequests.SingleOrDefaultAsync(candidate => candidate.Id == ScopedApprovalRequestId, cancellationToken); if (approvalRequest is null) { approvalRequest = new ApprovalRequest { Id = ScopedApprovalRequestId, Stage = string.Empty, ReviewerName = string.Empty, ReviewerEmail = string.Empty, State = string.Empty, AccessToken = string.Empty, SentAt = DateTimeOffset.UtcNow.AddHours(-12), }; dbContext.ApprovalRequests.Add(approvalRequest); } approvalRequest.WorkspaceId = WorkspaceId; approvalRequest.ContentItemId = ScopedContentItemId; approvalRequest.Stage = "Client"; approvalRequest.ReviewerName = "Sofia Martin"; approvalRequest.ReviewerEmail = "client@socialize.local"; approvalRequest.RequestedByUserId = managerUserId; approvalRequest.DueAt = DateTimeOffset.UtcNow.AddDays(1); approvalRequest.State = "Pending"; approvalRequest.AccessToken = "seed-client-review-token"; approvalRequest.CompletedAt = null; await dbContext.SaveChangesAsync(cancellationToken); NotificationEvent? approvalNotification = await dbContext.NotificationEvents.SingleOrDefaultAsync(candidate => candidate.Id == NotificationId, cancellationToken); if (approvalNotification is null) { approvalNotification = new NotificationEvent { Id = NotificationId, EventType = string.Empty, EntityType = string.Empty, Message = string.Empty, CreatedAt = DateTimeOffset.UtcNow.AddHours(-12), }; dbContext.NotificationEvents.Add(approvalNotification); } approvalNotification.WorkspaceId = WorkspaceId; approvalNotification.ContentItemId = ScopedContentItemId; approvalNotification.EventType = "approval.requested"; approvalNotification.EntityType = "ApprovalRequest"; approvalNotification.EntityId = ScopedApprovalRequestId; approvalNotification.Message = "Approval requested from Sofia Martin for Spring launch hero video."; approvalNotification.RecipientEmail = "client@socialize.local"; approvalNotification.MetadataJson = """{"stage":"Client"}"""; approvalNotification.ReadAt = null; Guid commentNotificationId = Guid.Parse("88888888-8888-8888-8888-000000000002"); NotificationEvent? commentNotification = await dbContext.NotificationEvents.SingleOrDefaultAsync(candidate => candidate.Id == commentNotificationId, cancellationToken); if (commentNotification is null) { commentNotification = new NotificationEvent { Id = commentNotificationId, EventType = string.Empty, EntityType = string.Empty, Message = string.Empty, CreatedAt = DateTimeOffset.UtcNow.AddHours(-20), }; dbContext.NotificationEvents.Add(commentNotification); } commentNotification.WorkspaceId = WorkspaceId; commentNotification.ContentItemId = ScopedContentItemId; commentNotification.EventType = "comment.created"; commentNotification.EntityType = "Comment"; commentNotification.EntityId = ClientCommentId; commentNotification.Message = "Sofia Martin commented on Spring launch hero video."; commentNotification.RecipientUserId = managerUserId; commentNotification.RecipientEmail = "manager@socialize.local"; commentNotification.MetadataJson = null; commentNotification.ReadAt = null; await dbContext.SaveChangesAsync(cancellationToken); } private static async Task UpsertWorkspaceAsync( AppDbContext dbContext, Guid id, Guid organizationId, Guid ownerUserId, string name, string timeZone, string logoUrl, CancellationToken cancellationToken) { Workspace? workspace = await dbContext.Workspaces .SingleOrDefaultAsync(candidate => candidate.Id == id, cancellationToken); if (workspace is null) { workspace = new Workspace { Id = id, Name = string.Empty, TimeZone = string.Empty, CreatedAt = DateTimeOffset.UtcNow, }; dbContext.Workspaces.Add(workspace); } workspace.Name = name; workspace.OrganizationId = organizationId; workspace.OwnerUserId = ownerUserId; workspace.TimeZone = timeZone; workspace.LogoUrl = logoUrl; await dbContext.SaveChangesAsync(cancellationToken); } private static async Task UpsertClientAsync( AppDbContext dbContext, Guid id, string name, string status, string portraitUrl, string primaryContactName, string primaryContactEmail, Guid workspaceId, CancellationToken cancellationToken) { Client? client = await dbContext.Clients.SingleOrDefaultAsync(candidate => candidate.Id == id, cancellationToken); if (client is null) { client = new Client { Id = id, Name = string.Empty, Status = string.Empty, CreatedAt = DateTimeOffset.UtcNow, }; dbContext.Clients.Add(client); } client.WorkspaceId = workspaceId; client.Name = name; client.Status = status; client.PortraitUrl = portraitUrl; client.PrimaryContactName = primaryContactName; client.PrimaryContactEmail = primaryContactEmail; client.PrimaryContactPortraitUrl = null; await dbContext.SaveChangesAsync(cancellationToken); } private static async Task UpsertCampaignAsync( AppDbContext dbContext, Guid id, Guid workspaceId, Guid clientId, string name, string status, DateTimeOffset startDate, DateTimeOffset endDate, string? description, string? notes, CancellationToken cancellationToken) { Campaign? campaign = await dbContext.Campaigns.SingleOrDefaultAsync(candidate => candidate.Id == id, cancellationToken); if (campaign is null) { campaign = new Campaign { Id = id, Name = string.Empty, Status = string.Empty, CreatedAt = DateTimeOffset.UtcNow, }; dbContext.Campaigns.Add(campaign); } campaign.WorkspaceId = workspaceId; campaign.ClientId = clientId; campaign.Name = name; campaign.Description = description; campaign.Notes = notes; campaign.Status = status; campaign.StartDate = startDate; campaign.EndDate = endDate; await dbContext.SaveChangesAsync(cancellationToken); } private static async Task UpsertChannelAsync( AppDbContext dbContext, Guid id, Guid workspaceId, string name, string network, string? handle, string? externalUrl, CancellationToken cancellationToken) { Channel? channel = await dbContext.Channels.SingleOrDefaultAsync(candidate => candidate.Id == id, cancellationToken); if (channel is null) { channel = new Channel { Id = id, Name = string.Empty, Network = string.Empty, CreatedAt = DateTimeOffset.UtcNow, }; dbContext.Channels.Add(channel); } channel.WorkspaceId = workspaceId; channel.Name = name; channel.Network = network; channel.Handle = handle; channel.ExternalUrl = externalUrl; await dbContext.SaveChangesAsync(cancellationToken); } private static async Task UpsertContentItemAsync( AppDbContext dbContext, Guid id, Guid workspaceId, Guid clientId, Guid campaignId, string title, string publicationMessage, string publicationTargets, string status, DateTimeOffset? dueDate, string currentRevisionLabel, int currentRevisionNumber, CancellationToken cancellationToken) { ContentItem? item = await dbContext.ContentItems.SingleOrDefaultAsync(candidate => candidate.Id == id, cancellationToken); if (item is null) { item = new ContentItem { Id = id, Title = string.Empty, PublicationMessage = string.Empty, PublicationTargets = string.Empty, Status = string.Empty, CurrentRevisionLabel = string.Empty, CreatedAt = DateTimeOffset.UtcNow, }; dbContext.ContentItems.Add(item); } item.WorkspaceId = workspaceId; item.ClientId = clientId; item.CampaignId = campaignId; item.Title = title; item.PublicationMessage = publicationMessage; item.PublicationTargets = publicationTargets; item.Status = status; item.DueDate = dueDate; item.CurrentRevisionLabel = currentRevisionLabel; item.CurrentRevisionNumber = currentRevisionNumber; await dbContext.SaveChangesAsync(cancellationToken); } private static async Task EnsureRevisionAsync( AppDbContext dbContext, Guid id, Guid contentItemId, int revisionNumber, string revisionLabel, string title, string publicationMessage, string publicationTargets, string changeSummary, Guid createdByUserId, DateTimeOffset createdAt, CancellationToken cancellationToken) { ContentItemRevision? revision = await dbContext.ContentItemRevisions.SingleOrDefaultAsync(candidate => candidate.Id == id, cancellationToken); if (revision is null) { revision = new ContentItemRevision { Id = id, RevisionLabel = string.Empty, Title = string.Empty, PublicationMessage = string.Empty, PublicationTargets = string.Empty, CreatedAt = createdAt, }; dbContext.ContentItemRevisions.Add(revision); } revision.ContentItemId = contentItemId; revision.RevisionNumber = revisionNumber; revision.RevisionLabel = revisionLabel; revision.Title = title; revision.PublicationMessage = publicationMessage; revision.PublicationTargets = publicationTargets; revision.ChangeSummary = changeSummary; revision.CreatedByUserId = createdByUserId; await dbContext.SaveChangesAsync(cancellationToken); } private static async Task EnsureAssetRevisionAsync( AppDbContext dbContext, Guid id, Guid assetId, int revisionNumber, string sourceReference, string? previewUrl, string? notes, Guid createdByUserId, DateTimeOffset createdAt, CancellationToken cancellationToken) { AssetRevision? revision = await dbContext.AssetRevisions.SingleOrDefaultAsync(candidate => candidate.Id == id, cancellationToken); if (revision is null) { revision = new AssetRevision { Id = id, SourceReference = string.Empty, CreatedAt = createdAt, }; dbContext.AssetRevisions.Add(revision); } revision.AssetId = assetId; revision.RevisionNumber = revisionNumber; revision.SourceReference = sourceReference; revision.PreviewUrl = previewUrl; revision.Notes = notes; revision.CreatedByUserId = createdByUserId; await dbContext.SaveChangesAsync(cancellationToken); } }