From 98613e7070842fa7a7e268648b32f41d43b8c1d9 Mon Sep 17 00:00:00 2001 From: Lum Date: Sat, 11 Jul 2026 18:45:27 +0100 Subject: [PATCH 1/8] Blog: enforce Restrict FK on BlogPost.Author and Comment.Author MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tout billet a un auteur, tout commentaire a un auteur. On aligne la base sur ce contrat (Postgres) en droppant les orphelins existants puis en remplaçant les FK en cascade par des FK Restrict. - ApplicationDbContext: fluent pour BlogPost.Author et Comment.Author en DeleteBehavior.Restrict. - ApplicationUser: ajoute la nav inverse BlogComments (manquait, EF aurait sinon créé une shadow FK). - Migration 20260711173717_EnforceBlogAuthorFKs: Up purge les Comment/BlogSpot dont l'AuthorId n'existe plus, log le volume, puis drop+add des FK. Down laisse la cascade (état pré-migration). Le code applicatif (BlogSpotService.Details) s'appuiera sur cette contrainte dans un commit séparé. --- ...711173717_EnforceBlogAuthorFKs.Designer.cs | 4675 +++++++++++++++++ .../20260711173717_EnforceBlogAuthorFKs.cs | 89 + .../ApplicationDbContextModelSnapshot.cs | 9 +- .../Models/ApplicationDbContext.cs | 16 + src/Yavsc.Server/Models/ApplicationUser.cs | 7 + 5 files changed, 4793 insertions(+), 3 deletions(-) create mode 100644 src/Yavsc.Org/Migrations/20260711173717_EnforceBlogAuthorFKs.Designer.cs create mode 100644 src/Yavsc.Org/Migrations/20260711173717_EnforceBlogAuthorFKs.cs diff --git a/src/Yavsc.Org/Migrations/20260711173717_EnforceBlogAuthorFKs.Designer.cs b/src/Yavsc.Org/Migrations/20260711173717_EnforceBlogAuthorFKs.Designer.cs new file mode 100644 index 00000000..4f8c664e --- /dev/null +++ b/src/Yavsc.Org/Migrations/20260711173717_EnforceBlogAuthorFKs.Designer.cs @@ -0,0 +1,4675 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Yavsc.Models; + +#nullable disable + +namespace Yavsc.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260711173717_EnforceBlogAuthorFKs")] + partial class EnforceBlogAuthorFKs + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("AllowedAccessTokenSigningAlgorithms") + .HasColumnType("text"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("LastAccessed") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("NonEditable") + .HasColumnType("boolean"); + + b.Property("ShowInDiscoveryDocument") + .HasColumnType("boolean"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ApiResources"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiResourceId") + .HasColumnType("integer"); + + b.Property("ApiResourceId1") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.HasIndex("ApiResourceId1"); + + b.ToTable("ApiResourceClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiResourceId") + .HasColumnType("integer"); + + b.Property("ApiResourceId1") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.HasIndex("ApiResourceId1"); + + b.ToTable("ApiResourceProperties"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiResourceId") + .HasColumnType("integer"); + + b.Property("ApiResourceId1") + .HasColumnType("integer"); + + b.Property("Scope") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.HasIndex("ApiResourceId1"); + + b.ToTable("ApiResourceScopes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceSecret", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiResourceId") + .HasColumnType("integer"); + + b.Property("ApiResourceId1") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.HasIndex("ApiResourceId1"); + + b.ToTable("ApiResourceSecrets"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("Emphasize") + .HasColumnType("boolean"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Required") + .HasColumnType("boolean"); + + b.Property("ShowInDiscoveryDocument") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("ApiScopes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScopeClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ScopeId") + .HasColumnType("integer"); + + b.Property("ScopeId1") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScopeId"); + + b.HasIndex("ScopeId1"); + + b.ToTable("ApiScopeClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScopeProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("ScopeId") + .HasColumnType("integer"); + + b.Property("ScopeId1") + .HasColumnType("integer"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScopeId"); + + b.HasIndex("ScopeId1"); + + b.ToTable("ApiScopeProperties"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.Client", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("AbsoluteRefreshTokenLifetime") + .HasColumnType("integer"); + + b.Property("AccessTokenLifetime") + .HasColumnType("integer"); + + b.Property("AccessTokenType") + .HasColumnType("integer"); + + b.Property("AllowAccessTokensViaBrowser") + .HasColumnType("boolean"); + + b.Property("AllowOfflineAccess") + .HasColumnType("boolean"); + + b.Property("AllowPlainTextPkce") + .HasColumnType("boolean"); + + b.Property("AllowRememberConsent") + .HasColumnType("boolean"); + + b.Property("AllowedIdentityTokenSigningAlgorithms") + .HasColumnType("text"); + + b.Property("AlwaysIncludeUserClaimsInIdToken") + .HasColumnType("boolean"); + + b.Property("AlwaysSendClientClaims") + .HasColumnType("boolean"); + + b.Property("AuthorizationCodeLifetime") + .HasColumnType("integer"); + + b.Property("BackChannelLogoutSessionRequired") + .HasColumnType("boolean"); + + b.Property("BackChannelLogoutUri") + .HasColumnType("text"); + + b.Property("ClientClaimsPrefix") + .HasColumnType("text"); + + b.Property("ClientId") + .HasColumnType("text"); + + b.Property("ClientName") + .HasColumnType("text"); + + b.Property("ClientUri") + .HasColumnType("text"); + + b.Property("ConsentLifetime") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DeviceCodeLifetime") + .HasColumnType("integer"); + + b.Property("EnableLocalLogin") + .HasColumnType("boolean"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("FrontChannelLogoutSessionRequired") + .HasColumnType("boolean"); + + b.Property("FrontChannelLogoutUri") + .HasColumnType("text"); + + b.Property("IdentityTokenLifetime") + .HasColumnType("integer"); + + b.Property("IncludeJwtId") + .HasColumnType("boolean"); + + b.Property("LastAccessed") + .HasColumnType("timestamp with time zone"); + + b.Property("LogoUri") + .HasColumnType("text"); + + b.Property("NonEditable") + .HasColumnType("boolean"); + + b.Property("PairWiseSubjectSalt") + .HasColumnType("text"); + + b.Property("ProtocolType") + .HasColumnType("text"); + + b.Property("RefreshTokenExpiration") + .HasColumnType("integer"); + + b.Property("RefreshTokenUsage") + .HasColumnType("integer"); + + b.Property("RequireClientSecret") + .HasColumnType("boolean"); + + b.Property("RequireConsent") + .HasColumnType("boolean"); + + b.Property("RequirePkce") + .HasColumnType("boolean"); + + b.Property("RequireRequestObject") + .HasColumnType("boolean"); + + b.Property("SlidingRefreshTokenLifetime") + .HasColumnType("integer"); + + b.Property("UpdateAccessTokenClaimsOnRefresh") + .HasColumnType("boolean"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone"); + + b.Property("UserCodeType") + .HasColumnType("text"); + + b.Property("UserSsoLifetime") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Clients"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientCorsOrigin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("Origin") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientCorsOrigins"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientGrantType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("GrantType") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientGrantTypes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientIdPRestriction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("Provider") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientIdPRestrictions"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientPostLogoutRedirectUri", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("PostLogoutRedirectUri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientPostLogoutRedirectUris"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientProperties"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientRedirectUri", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("RedirectUri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientRedirectUris"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("Scope") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientScopes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientSecret", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientSecrets"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.DeviceFlowCodes", b => + { + b.Property("UserCode") + .HasColumnType("text"); + + b.Property("DeviceCode") + .HasColumnType("text"); + + b.Property("ClientId") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .HasColumnType("text"); + + b.Property("SubjectId") + .HasColumnType("text"); + + b.HasKey("UserCode", "DeviceCode"); + + b.ToTable("DeviceFlowCodes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("Emphasize") + .HasColumnType("boolean"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("NonEditable") + .HasColumnType("boolean"); + + b.Property("Required") + .HasColumnType("boolean"); + + b.Property("ShowInDiscoveryDocument") + .HasColumnType("boolean"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("IdentityResources"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResourceClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("IdentityResourceId") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("IdentityResourceId"); + + b.ToTable("IdentityResourceClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResourceProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("IdentityResourceId") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("IdentityResourceId"); + + b.ToTable("IdentityResourceProperties"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.PersistedGrant", b => + { + b.Property("Key") + .HasColumnType("text"); + + b.Property("ClientId") + .HasColumnType("text"); + + b.Property("ConsumedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .HasColumnType("text"); + + b.Property("SubjectId") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("text"); + + b.HasKey("Key"); + + b.ToTable("PersistedGrants"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Yavsc.Abstract.Identity.ClientProviderInfo", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("Avatar") + .HasColumnType("text"); + + b.Property("BillingAddressId") + .HasColumnType("bigint"); + + b.Property("EMail") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UserName") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("ClientProviderInfo"); + }); + + modelBuilder.Entity("Yavsc.Abstract.Models.Messaging.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Target") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("body") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("click_action") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("color") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("icon") + .ValueGeneratedOnAdd() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasDefaultValue("exclam"); + + b.Property("sound") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("tag") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("title") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.ToTable("Notification"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.Ban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("TargetId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TargetId"); + + b.ToTable("Ban"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.BlackListed", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("UserId"); + + b.ToTable("BlackListed"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.CircleAuthorizationToBlogPost", b => + { + b.Property("CircleId") + .HasColumnType("bigint"); + + b.Property("BlogPostId") + .HasColumnType("bigint"); + + b.Property("Comment") + .HasColumnType("boolean"); + + b.HasKey("CircleId", "BlogPostId"); + + b.HasIndex("BlogPostId"); + + b.ToTable("CircleAuthorizationToBlogPost"); + }); + + modelBuilder.Entity("Yavsc.Models.AccountBalance", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("ContactCredits") + .HasColumnType("bigint"); + + b.Property("Credits") + .HasColumnType("numeric"); + + b.HasKey("UserId"); + + b.ToTable("BankStatus"); + }); + + modelBuilder.Entity("Yavsc.Models.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("AllowMonthlyEmail") + .HasColumnType("boolean"); + + b.Property("Avatar") + .ValueGeneratedOnAdd() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasDefaultValue("/images/Users/icon_user.png"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("DedicatedGoogleCalendar") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("DiskQuota") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(524288000L); + + b.Property("DiskUsage") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FullName") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("MaxFileSize") + .HasColumnType("bigint"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("PostalAddressId") + .HasColumnType("bigint"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Email"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.HasIndex("PostalAddressId"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Yavsc.Models.BalanceImpact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BalanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExecDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Impact") + .HasColumnType("numeric"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BalanceId"); + + b.ToTable("BalanceImpact"); + }); + + modelBuilder.Entity("Yavsc.Models.Bank.BankIdentity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountNumber") + .HasColumnType("text"); + + b.Property("BIC") + .HasColumnType("text"); + + b.Property("BankCode") + .HasColumnType("text"); + + b.Property("BankedKey") + .HasColumnType("integer"); + + b.Property("IBAN") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("WicketCode") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("BankIdentity"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.CommandLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Count") + .HasColumnType("integer"); + + b.Property("Currency") + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("EstimateId") + .HasColumnType("bigint"); + + b.Property("EstimateTemplateId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UnitaryCost") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EstimateId"); + + b.HasIndex("EstimateTemplateId"); + + b.ToTable("CommandLine"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Estimate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AttachedFilesString") + .HasColumnType("text"); + + b.Property("AttachedGraphicsString") + .HasColumnType("text"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClientValidationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CommandId") + .HasColumnType("bigint"); + + b.Property("CommandType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("ProviderValidationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("CommandId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Estimates"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.EstimateTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EstimateTemplates"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.ExceptionSIREN", b => + { + b.Property("SIREN") + .HasColumnType("text"); + + b.HasKey("SIREN"); + + b.ToTable("ExceptionsSIREN"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Signature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CapturedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("CoordinateMax") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(10000); + + b.Property("EstimateId") + .HasColumnType("bigint"); + + b.Property("FilePath") + .IsRequired() + .HasColumnType("text"); + + b.Property("SignerId") + .IsRequired() + .HasColumnType("text"); + + b.PrimitiveCollection("Strokes") + .IsRequired() + .HasColumnType("integer[]"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SignerId"); + + b.HasIndex("EstimateId", "Type") + .IsUnique(); + + b.ToTable("Signatures"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogAttachedFile", b => + { + b.Property("FileId") + .HasColumnType("bigint"); + + b.Property("PostId") + .HasColumnType("bigint"); + + b.HasKey("FileId", "PostId"); + + b.HasIndex("PostId"); + + b.ToTable("BlogAttachedFiles"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogPost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Article") + .HasMaxLength(56224) + .HasColumnType("character varying(56224)"); + + b.Property("AuthorId") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Photo") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.ToTable("BlogSpot"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogTag", b => + { + b.Property("PostId") + .HasColumnType("bigint"); + + b.Property("TagId") + .HasColumnType("bigint"); + + b.HasKey("PostId", "TagId"); + + b.HasIndex("TagId"); + + b.ToTable("BlogTag"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Article") + .HasColumnType("text"); + + b.Property("AuthorId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ParentId") + .HasColumnType("bigint"); + + b.Property("ReceiverId") + .HasColumnType("bigint"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.Property("Visible") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("ParentId"); + + b.HasIndex("ReceiverId"); + + b.ToTable("Comment"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.UploadedFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContentType") + .HasColumnType("text"); + + b.Property("Length") + .HasColumnType("bigint"); + + b.Property("Path") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("UploadedFiles"); + }); + + modelBuilder.Entity("Yavsc.Models.BlogSpotPublication", b => + { + b.Property("BlogpostId") + .HasColumnType("bigint"); + + b.HasKey("BlogpostId"); + + b.ToTable("blogSpotPublications"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.Schedule", b => + { + b.Property("OwnerId") + .HasColumnType("text"); + + b.HasKey("OwnerId"); + + b.ToTable("Schedule"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.ScheduledEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PeriodEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("PeriodStart") + .HasColumnType("timestamp with time zone"); + + b.Property("Reccurence") + .HasColumnType("integer"); + + b.Property("ScheduleOwnerId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScheduleOwnerId"); + + b.HasIndex("PeriodStart", "PeriodEnd"); + + b.ToTable("ScheduledEvent"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatConnection", b => + { + b.Property("ConnectionId") + .HasColumnType("text"); + + b.Property("ApplicationUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Connected") + .HasColumnType("boolean"); + + b.Property("UserAgent") + .HasColumnType("text"); + + b.HasKey("ConnectionId"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("ChatConnection"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoom", b => + { + b.Property("Name") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("LatestJoinPart") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Topic") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Name"); + + b.HasIndex("OwnerId"); + + b.ToTable("ChatRoom"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoomAccess", b => + { + b.Property("ChannelName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Level") + .HasColumnType("integer"); + + b.HasKey("ChannelName", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ChatRoomAccess"); + }); + + modelBuilder.Entity("Yavsc.Models.Cratie.Option", b => + { + b.Property("Code") + .HasColumnType("text"); + + b.Property("CodeScrutin") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Code", "CodeScrutin"); + + b.ToTable("Option"); + }); + + modelBuilder.Entity("Yavsc.Models.Drawing.Color", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Blue") + .HasColumnType("smallint"); + + b.Property("Green") + .HasColumnType("smallint"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Red") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.ToTable("Color"); + }); + + modelBuilder.Entity("Yavsc.Models.Forms.Form", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Summary") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Form"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.BrusherProfile", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("ActionDistance") + .HasColumnType("integer"); + + b.Property("CarePrice") + .HasColumnType("numeric"); + + b.Property("FlatFeeDiscount") + .HasColumnType("numeric"); + + b.Property("HalfBalayagePrice") + .HasColumnType("numeric"); + + b.Property("HalfBrushingPrice") + .HasColumnType("numeric"); + + b.Property("HalfColorPrice") + .HasColumnType("numeric"); + + b.Property("HalfDefrisPrice") + .HasColumnType("numeric"); + + b.Property("HalfFoldingPrice") + .HasColumnType("numeric"); + + b.Property("HalfMechPrice") + .HasColumnType("numeric"); + + b.Property("HalfMultiColorPrice") + .HasColumnType("numeric"); + + b.Property("HalfPermanentPrice") + .HasColumnType("numeric"); + + b.Property("KidCutPrice") + .HasColumnType("numeric"); + + b.Property("LongBalayagePrice") + .HasColumnType("numeric"); + + b.Property("LongBrushingPrice") + .HasColumnType("numeric"); + + b.Property("LongColorPrice") + .HasColumnType("numeric"); + + b.Property("LongDefrisPrice") + .HasColumnType("numeric"); + + b.Property("LongFoldingPrice") + .HasColumnType("numeric"); + + b.Property("LongMechPrice") + .HasColumnType("numeric"); + + b.Property("LongMultiColorPrice") + .HasColumnType("numeric"); + + b.Property("LongPermanentPrice") + .HasColumnType("numeric"); + + b.Property("ManBrushPrice") + .HasColumnType("numeric"); + + b.Property("ManCutPrice") + .HasColumnType("numeric"); + + b.Property("ScheduleOwnerId") + .HasColumnType("text"); + + b.Property("ShampooPrice") + .HasColumnType("numeric"); + + b.Property("ShortBalayagePrice") + .HasColumnType("numeric"); + + b.Property("ShortBrushingPrice") + .HasColumnType("numeric"); + + b.Property("ShortColorPrice") + .HasColumnType("numeric"); + + b.Property("ShortDefrisPrice") + .HasColumnType("numeric"); + + b.Property("ShortFoldingPrice") + .HasColumnType("numeric"); + + b.Property("ShortMechPrice") + .HasColumnType("numeric"); + + b.Property("ShortMultiColorPrice") + .HasColumnType("numeric"); + + b.Property("ShortPermanentPrice") + .HasColumnType("numeric"); + + b.Property("WomenHalfCutPrice") + .HasColumnType("numeric"); + + b.Property("WomenLongCutPrice") + .HasColumnType("numeric"); + + b.Property("WomenShortCutPrice") + .HasColumnType("numeric"); + + b.HasKey("UserId"); + + b.HasIndex("ScheduleOwnerId"); + + b.ToTable("BrusherProfile"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairCutQuery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActivityCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("AdditionalInfo") + .HasColumnType("text"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Consent") + .HasColumnType("boolean"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("EventDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LocationId") + .HasColumnType("bigint"); + + b.Property("PaymentId") + .HasColumnType("text"); + + b.Property("PerformerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PrestationId") + .HasColumnType("bigint"); + + b.Property("Provisional") + .HasColumnType("numeric"); + + b.Property("SelectedProfileUserId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.Property("ValidationDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ActivityCode"); + + b.HasIndex("ClientId"); + + b.HasIndex("LocationId"); + + b.HasIndex("PaymentId"); + + b.HasIndex("PerformerId"); + + b.HasIndex("PrestationId"); + + b.HasIndex("SelectedProfileUserId"); + + b.ToTable("HairCutQueries"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairMultiCutQuery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActivityCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Consent") + .HasColumnType("boolean"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("EventDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LocationId") + .HasColumnType("bigint"); + + b.Property("PaymentId") + .HasColumnType("text"); + + b.Property("PerformerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provisional") + .HasColumnType("numeric"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.Property("ValidationDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ActivityCode"); + + b.HasIndex("ClientId"); + + b.HasIndex("LocationId"); + + b.HasIndex("PaymentId"); + + b.HasIndex("PerformerId"); + + b.ToTable("HairMultiCutQueries"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Cares") + .HasColumnType("boolean"); + + b.Property("Cut") + .HasColumnType("boolean"); + + b.Property("Dressing") + .HasColumnType("integer"); + + b.Property("Gender") + .HasColumnType("integer"); + + b.Property("Length") + .HasColumnType("integer"); + + b.Property("Shampoo") + .HasColumnType("boolean"); + + b.Property("Tech") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("HairPrestation"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestationCollectionItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PrestationId") + .HasColumnType("bigint"); + + b.Property("QueryId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("PrestationId"); + + b.HasIndex("QueryId"); + + b.ToTable("HairPrestationCollectionItem"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairTaint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Brand") + .HasColumnType("text"); + + b.Property("ColorId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ColorId"); + + b.ToTable("HairTaint"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairTaintInstance", b => + { + b.Property("TaintId") + .HasColumnType("bigint"); + + b.Property("PrestationId") + .HasColumnType("bigint"); + + b.HasKey("TaintId", "PrestationId"); + + b.HasIndex("PrestationId"); + + b.ToTable("HairTaintInstance"); + }); + + modelBuilder.Entity("Yavsc.Models.IT.Evolution.Feature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ShortName") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Feature"); + }); + + modelBuilder.Entity("Yavsc.Models.IT.Fixing.Bug", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasMaxLength(10240) + .HasColumnType("character varying(10240)"); + + b.Property("FeatureId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FeatureId"); + + b.ToTable("Bug"); + }); + + modelBuilder.Entity("Yavsc.Models.Identity.DeviceDeclaration", b => + { + b.Property("DeviceId") + .HasColumnType("text"); + + b.Property("DeclarationDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("LOCALTIMESTAMP"); + + b.Property("DeviceOwnerId") + .HasColumnType("text"); + + b.Property("LatestActivityUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("Model") + .HasColumnType("text"); + + b.Property("Platform") + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("text"); + + b.HasKey("DeviceId"); + + b.HasIndex("DeviceOwnerId"); + + b.ToTable("DeviceDeclaration"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.DeclarationFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DeclarationId") + .HasColumnType("bigint"); + + b.Property("MatchExcerpt") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PatternId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DeclarationId"); + + b.HasIndex("PatternId"); + + b.ToTable("DeclarationFlag"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.ModerationLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .HasColumnType("integer"); + + b.Property("DeclarationId") + .HasColumnType("bigint"); + + b.Property("ModeratorId") + .HasColumnType("text"); + + b.Property("ScoreDelta") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DeclarationId"); + + b.HasIndex("ModeratorId"); + + b.HasIndex("Timestamp"); + + b.ToTable("ModerationLogs", t => + { + t.HasCheckConstraint("CK_ModerationLog_Immutable", "1=1"); + }); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.RegexAlertPattern", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Pattern") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Severity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.ToTable("RegexAlertPatterns"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustDeclaration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Content") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("DeclarantTokenId") + .HasColumnType("uuid"); + + b.Property("ScoreDelta") + .HasColumnType("integer"); + + b.Property("Sentiment") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TrustTokenId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("SubmittedAt"); + + b.HasIndex("TrustTokenId"); + + b.ToTable("TrustDeclarations"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TokenSource") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("TrustScore") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TokenHash") + .IsUnique(); + + b.ToTable("TrustTokens"); + }); + + modelBuilder.Entity("Yavsc.Models.Market.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Depth") + .HasColumnType("numeric"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Height") + .HasColumnType("numeric"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("Public") + .HasColumnType("boolean"); + + b.Property("Weight") + .HasColumnType("numeric"); + + b.Property("Width") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("Yavsc.Models.Market.Service", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContextId") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Public") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("ContextId"); + + b.ToTable("Services"); + }); + + modelBuilder.Entity("Yavsc.Models.Messaging.Announce", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("For") + .HasColumnType("smallint"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Sender") + .HasColumnType("text"); + + b.Property("Topic") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("Announce"); + }); + + modelBuilder.Entity("Yavsc.Models.Messaging.DismissClicked", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("NotificationId") + .HasColumnType("bigint"); + + b.HasKey("UserId", "NotificationId"); + + b.HasIndex("NotificationId"); + + b.ToTable("DismissClicked"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Instrument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("Instrument"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.InstrumentRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("InstrumentId") + .HasColumnType("bigint"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("InstrumentId", "OwnerId"); + + b.HasIndex("OwnerId"); + + b.ToTable("InstrumentRating"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.MusicalPreference", b => + { + b.Property("OwnerProfileId") + .HasColumnType("text"); + + b.Property("DjSettingsUserId") + .HasColumnType("text"); + + b.Property("MusicLoverSettingsUserId") + .HasColumnType("text"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.Property("TendencyId") + .HasColumnType("bigint"); + + b.HasKey("OwnerProfileId"); + + b.HasIndex("DjSettingsUserId"); + + b.HasIndex("MusicLoverSettingsUserId"); + + b.HasIndex("TendencyId"); + + b.ToTable("MusicalPreference"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.MusicalTendency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("MusicalTendency"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.DjSettings", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("SoundCloudId") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("DjSettings"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.Instrumentation", b => + { + b.Property("InstrumentId") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("InstrumentId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("Instrumentation"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.MusicLoverSettings", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("MusicLoverSettings"); + }); + + modelBuilder.Entity("Yavsc.Models.Payment.PayPalPayment", b => + { + b.Property("CreationToken") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ExecutorId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderReference") + .HasColumnType("text"); + + b.Property("PaypalPayerId") + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("CreationToken"); + + b.HasIndex("ExecutorId"); + + b.ToTable("PayPalPayment"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Circle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApplicationUserId") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Public") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("Circle"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.CircleMember", b => + { + b.Property("MemberId") + .HasColumnType("text"); + + b.Property("CircleId") + .HasColumnType("bigint"); + + b.HasKey("MemberId", "CircleId"); + + b.HasIndex("CircleId"); + + b.ToTable("CircleMembers"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Contact", b => + { + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("AddressId") + .HasColumnType("bigint"); + + b.Property("ApplicationUserId") + .HasColumnType("text"); + + b.Property("EMail") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.HasKey("OwnerId", "UserId"); + + b.HasIndex("AddressId"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("Contact"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.HyperLink", b => + { + b.Property("HRef") + .HasColumnType("text"); + + b.Property("Method") + .HasColumnType("text"); + + b.Property("BrusherProfileUserId") + .HasColumnType("text"); + + b.Property("ContentType") + .HasColumnType("text"); + + b.Property("PayPalPaymentCreationToken") + .HasColumnType("text"); + + b.Property("Rel") + .HasColumnType("text"); + + b.HasKey("HRef", "Method"); + + b.HasIndex("BrusherProfileUserId"); + + b.HasIndex("PayPalPaymentCreationToken"); + + b.ToTable("HyperLink"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Location", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.HasKey("Id"); + + b.ToTable("Locations"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.PostalAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("text"); + + b.Property("Street1") + .HasColumnType("text"); + + b.Property("Street2") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PostalAddress"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("Yavsc.Models.Skill", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("SiteSkills"); + }); + + modelBuilder.Entity("Yavsc.Models.Streaming.LiveFlow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DifferedFileName") + .HasColumnType("text"); + + b.Property("MediaType") + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Pitch") + .HasColumnType("text"); + + b.Property("SequenceNumber") + .HasColumnType("integer"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("LiveFlow"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Activity", b => + { + b.Property("Code") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Hidden") + .HasColumnType("boolean"); + + b.Property("Moderated") + .HasColumnType("boolean"); + + b.Property("ModeratorGroupName") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentCode") + .HasColumnType("text"); + + b.Property("Photo") + .HasColumnType("text"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.Property("SettingsClassName") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Code"); + + b.HasIndex("ParentCode"); + + b.ToTable("Activities"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.CoWorking", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FormationSettingsUserId") + .HasColumnType("text"); + + b.Property("PerformerId") + .HasColumnType("text"); + + b.Property("WorkingForId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FormationSettingsUserId"); + + b.HasIndex("PerformerId"); + + b.HasIndex("WorkingForId"); + + b.ToTable("CoWorking"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.CommandForm", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionName") + .HasColumnType("text"); + + b.Property("ActivityCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ActivityCode"); + + b.ToTable("CommandForm"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerProfile", b => + { + b.Property("PerformerId") + .HasColumnType("text"); + + b.Property("AcceptNotifications") + .HasColumnType("boolean"); + + b.Property("AcceptPublicContact") + .HasColumnType("boolean"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("MaxDailyCost") + .HasColumnType("integer"); + + b.Property("MinDailyCost") + .HasColumnType("integer"); + + b.Property("OrganizationAddressId") + .HasColumnType("bigint"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.Property("SIREN") + .IsRequired() + .HasColumnType("text"); + + b.Property("UseGeoLocalizationToReduceDistanceWithClients") + .HasColumnType("boolean"); + + b.Property("WebSite") + .HasColumnType("text"); + + b.HasKey("PerformerId"); + + b.HasIndex("OrganizationAddressId"); + + b.ToTable("Performers"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Profiles.FormationSettings", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("FormationSettings"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.RdvQuery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActivityCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Consent") + .HasColumnType("boolean"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("EventDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LocationId") + .HasColumnType("bigint"); + + b.Property("LocationType") + .HasColumnType("integer"); + + b.Property("PaymentId") + .HasColumnType("text"); + + b.Property("PerformerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provisional") + .HasColumnType("numeric"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.Property("ValidationDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ActivityCode"); + + b.HasIndex("ClientId"); + + b.HasIndex("LocationId"); + + b.HasIndex("PaymentId"); + + b.HasIndex("PerformerId"); + + b.ToTable("RdvQueries"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.UserActivity", b => + { + b.Property("DoesCode") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("Weight") + .HasColumnType("integer"); + + b.HasKey("DoesCode", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("UserActivities"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.Calendar.Period", b => + { + b.Property("Start") + .HasColumnType("timestamp with time zone"); + + b.Property("End") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Start", "End"); + + b.ToTable("Period"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.EMailing.MailingTemplate", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .HasMaxLength(65536) + .HasColumnType("character varying(65536)"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ReplyToAddress") + .HasColumnType("text"); + + b.Property("ToSend") + .HasColumnType("integer"); + + b.Property("Topic") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("MailingTemplate"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActivityCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Consent") + .HasColumnType("boolean"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("GitId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("PaymentId") + .HasColumnType("text"); + + b.Property("PerformerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provisional") + .HasColumnType("numeric"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.Property("ValidationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ActivityCode"); + + b.HasIndex("ClientId"); + + b.HasIndex("GitId"); + + b.HasIndex("PaymentId"); + + b.HasIndex("PerformerId"); + + b.ToTable("Project"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.ProjectBuildConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("ProjectBuildConfiguration"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.SourceCode.GitRepositoryReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Branch") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("GitRepositoryReference"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceClaim", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", null) + .WithMany("UserClaims") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany() + .HasForeignKey("ApiResourceId1"); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceProperty", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", null) + .WithMany("Properties") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany() + .HasForeignKey("ApiResourceId1"); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceScope", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", null) + .WithMany("Scopes") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany() + .HasForeignKey("ApiResourceId1"); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceSecret", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", null) + .WithMany("Secrets") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany() + .HasForeignKey("ApiResourceId1"); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScopeClaim", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiScope", null) + .WithMany("UserClaims") + .HasForeignKey("ScopeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiScope", "Scope") + .WithMany() + .HasForeignKey("ScopeId1"); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScopeProperty", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiScope", null) + .WithMany("Properties") + .HasForeignKey("ScopeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiScope", "Scope") + .WithMany() + .HasForeignKey("ScopeId1"); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientClaim", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany("Claims") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientCorsOrigin", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("AllowedCorsOrigins") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientGrantType", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("AllowedGrantTypes") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientIdPRestriction", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("IdentityProviderRestrictions") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientPostLogoutRedirectUri", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("PostLogoutRedirectUris") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientProperty", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("Properties") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientRedirectUri", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("RedirectUris") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientScope", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("AllowedScopes") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientSecret", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("ClientSecrets") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResourceClaim", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.IdentityResource", "IdentityResource") + .WithMany("UserClaims") + .HasForeignKey("IdentityResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("IdentityResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResourceProperty", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.IdentityResource", "IdentityResource") + .WithMany("Properties") + .HasForeignKey("IdentityResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("IdentityResource"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Yavsc.Models.Access.Ban", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "TargetUser") + .WithMany() + .HasForeignKey("TargetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TargetUser"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.BlackListed", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany("BlackList") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.CircleAuthorizationToBlogPost", b => + { + b.HasOne("Yavsc.Models.Blog.BlogPost", "Target") + .WithMany("ACL") + .HasForeignKey("BlogPostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Relationship.Circle", "Allowed") + .WithMany() + .HasForeignKey("CircleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Allowed"); + + b.Navigation("Target"); + }); + + modelBuilder.Entity("Yavsc.Models.AccountBalance", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithOne("AccountBalance") + .HasForeignKey("Yavsc.Models.AccountBalance", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.ApplicationUser", b => + { + b.HasOne("Yavsc.Models.Relationship.Location", "PostalAddress") + .WithMany() + .HasForeignKey("PostalAddressId"); + + b.Navigation("PostalAddress"); + }); + + modelBuilder.Entity("Yavsc.Models.BalanceImpact", b => + { + b.HasOne("Yavsc.Models.AccountBalance", "Balance") + .WithMany() + .HasForeignKey("BalanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Balance"); + }); + + modelBuilder.Entity("Yavsc.Models.Bank.BankIdentity", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "User") + .WithMany("BankInfo") + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.CommandLine", b => + { + b.HasOne("Yavsc.Models.Billing.Estimate", null) + .WithMany("Bill") + .HasForeignKey("EstimateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Billing.EstimateTemplate", null) + .WithMany("Bill") + .HasForeignKey("EstimateTemplateId"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Estimate", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Workflow.RdvQuery", "Query") + .WithMany() + .HasForeignKey("CommandId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "Owner") + .WithMany() + .HasForeignKey("OwnerId"); + + b.Navigation("Client"); + + b.Navigation("Owner"); + + b.Navigation("Query"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Signature", b => + { + b.HasOne("Yavsc.Models.Billing.Estimate", "Estimate") + .WithMany("Signatures") + .HasForeignKey("EstimateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Signer") + .WithMany() + .HasForeignKey("SignerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Estimate"); + + b.Navigation("Signer"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogAttachedFile", b => + { + b.HasOne("Yavsc.Models.Blog.UploadedFile", "File") + .WithMany() + .HasForeignKey("FileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Blog.BlogPost", "Post") + .WithMany() + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("File"); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogPost", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Author") + .WithMany("Posts") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogTag", b => + { + b.HasOne("Yavsc.Models.Blog.BlogPost", "Post") + .WithMany("Tags") + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Relationship.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Post"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.Comment", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Author") + .WithMany("BlogComments") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Yavsc.Models.Blog.Comment", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId"); + + b.HasOne("Yavsc.Models.Blog.BlogPost", "Post") + .WithMany("Comments") + .HasForeignKey("ReceiverId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Parent"); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("Yavsc.Models.BlogSpotPublication", b => + { + b.HasOne("Yavsc.Models.Blog.BlogPost", "BlogPost") + .WithMany() + .HasForeignKey("BlogpostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BlogPost"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.Schedule", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.ScheduledEvent", b => + { + b.HasOne("Yavsc.Models.Calendar.Schedule", null) + .WithMany("Events") + .HasForeignKey("ScheduleOwnerId"); + + b.HasOne("Yavsc.Server.Models.Calendar.Period", "Period") + .WithMany() + .HasForeignKey("PeriodStart", "PeriodEnd"); + + b.Navigation("Period"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatConnection", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany("Connections") + .HasForeignKey("ApplicationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoom", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany("Rooms") + .HasForeignKey("OwnerId"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoomAccess", b => + { + b.HasOne("Yavsc.Models.Chat.ChatRoom", "Room") + .WithMany("Moderation") + .HasForeignKey("ChannelName") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "User") + .WithMany("RoomAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.BrusherProfile", b => + { + b.HasOne("Yavsc.Models.Calendar.Schedule", "Schedule") + .WithMany() + .HasForeignKey("ScheduleOwnerId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "BaseProfile") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseProfile"); + + b.Navigation("Schedule"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairCutQuery", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany() + .HasForeignKey("ActivityCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Relationship.Location", "Location") + .WithMany() + .HasForeignKey("LocationId"); + + b.HasOne("Yavsc.Models.Payment.PayPalPayment", "Regularization") + .WithMany() + .HasForeignKey("PaymentId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "PerformerProfile") + .WithMany() + .HasForeignKey("PerformerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Haircut.HairPrestation", "Prestation") + .WithMany() + .HasForeignKey("PrestationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Haircut.BrusherProfile", "SelectedProfile") + .WithMany() + .HasForeignKey("SelectedProfileUserId"); + + b.Navigation("Client"); + + b.Navigation("Context"); + + b.Navigation("Location"); + + b.Navigation("PerformerProfile"); + + b.Navigation("Prestation"); + + b.Navigation("Regularization"); + + b.Navigation("SelectedProfile"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairMultiCutQuery", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany() + .HasForeignKey("ActivityCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Relationship.Location", "Location") + .WithMany() + .HasForeignKey("LocationId"); + + b.HasOne("Yavsc.Models.Payment.PayPalPayment", "Regularization") + .WithMany() + .HasForeignKey("PaymentId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "PerformerProfile") + .WithMany() + .HasForeignKey("PerformerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Context"); + + b.Navigation("Location"); + + b.Navigation("PerformerProfile"); + + b.Navigation("Regularization"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestationCollectionItem", b => + { + b.HasOne("Yavsc.Models.Haircut.HairPrestation", "Prestation") + .WithMany() + .HasForeignKey("PrestationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Haircut.HairMultiCutQuery", "Query") + .WithMany("Prestations") + .HasForeignKey("QueryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Prestation"); + + b.Navigation("Query"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairTaint", b => + { + b.HasOne("Yavsc.Models.Drawing.Color", "Color") + .WithMany() + .HasForeignKey("ColorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Color"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairTaintInstance", b => + { + b.HasOne("Yavsc.Models.Haircut.HairPrestation", "Prestation") + .WithMany("Taints") + .HasForeignKey("PrestationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Haircut.HairTaint", "Taint") + .WithMany() + .HasForeignKey("TaintId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Prestation"); + + b.Navigation("Taint"); + }); + + modelBuilder.Entity("Yavsc.Models.IT.Fixing.Bug", b => + { + b.HasOne("Yavsc.Models.IT.Evolution.Feature", "False") + .WithMany() + .HasForeignKey("FeatureId"); + + b.Navigation("False"); + }); + + modelBuilder.Entity("Yavsc.Models.Identity.DeviceDeclaration", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "DeviceOwner") + .WithMany("DeviceDeclaration") + .HasForeignKey("DeviceOwnerId"); + + b.Navigation("DeviceOwner"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.DeclarationFlag", b => + { + b.HasOne("Yavsc.Models.Kyc.TrustDeclaration", "Declaration") + .WithMany("Flags") + .HasForeignKey("DeclarationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Kyc.RegexAlertPattern", "Pattern") + .WithMany() + .HasForeignKey("PatternId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Declaration"); + + b.Navigation("Pattern"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.ModerationLog", b => + { + b.HasOne("Yavsc.Models.Kyc.TrustDeclaration", "Declaration") + .WithMany() + .HasForeignKey("DeclarationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Declaration"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustDeclaration", b => + { + b.HasOne("Yavsc.Models.Kyc.TrustToken", "Subject") + .WithMany("Declarations") + .HasForeignKey("TrustTokenId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Subject"); + }); + + modelBuilder.Entity("Yavsc.Models.Market.Service", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany("Services") + .HasForeignKey("ContextId"); + + b.Navigation("Context"); + }); + + modelBuilder.Entity("Yavsc.Models.Messaging.Announce", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Messaging.DismissClicked", b => + { + b.HasOne("Yavsc.Abstract.Models.Messaging.Notification", "Notified") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notified"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.InstrumentRating", b => + { + b.HasOne("Yavsc.Models.Musical.Instrument", "Instrument") + .WithMany() + .HasForeignKey("InstrumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "Profile") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instrument"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.MusicalPreference", b => + { + b.HasOne("Yavsc.Models.Musical.Profiles.DjSettings", null) + .WithMany("SoundColor") + .HasForeignKey("DjSettingsUserId"); + + b.HasOne("Yavsc.Models.Musical.Profiles.MusicLoverSettings", null) + .WithMany("SoundColor") + .HasForeignKey("MusicLoverSettingsUserId"); + + b.HasOne("Yavsc.Models.Musical.MusicalTendency", "MusicalTendency") + .WithMany() + .HasForeignKey("TendencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MusicalTendency"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.Instrumentation", b => + { + b.HasOne("Yavsc.Models.Musical.Instrument", "Tool") + .WithMany() + .HasForeignKey("InstrumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tool"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Payment.PayPalPayment", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Executor") + .WithMany() + .HasForeignKey("ExecutorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Executor"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Circle", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany("Circles") + .HasForeignKey("ApplicationUserId"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.CircleMember", b => + { + b.HasOne("Yavsc.Models.Relationship.Circle", "Circle") + .WithMany("Members") + .HasForeignKey("CircleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Member") + .WithMany("Membership") + .HasForeignKey("MemberId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Circle"); + + b.Navigation("Member"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Contact", b => + { + b.HasOne("Yavsc.Models.Relationship.PostalAddress", "PostalAddress") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany("Book") + .HasForeignKey("ApplicationUserId"); + + b.Navigation("PostalAddress"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.HyperLink", b => + { + b.HasOne("Yavsc.Models.Haircut.BrusherProfile", null) + .WithMany("Links") + .HasForeignKey("BrusherProfileUserId"); + + b.HasOne("Yavsc.Models.Payment.PayPalPayment", null) + .WithMany("Links") + .HasForeignKey("PayPalPaymentCreationToken"); + }); + + modelBuilder.Entity("Yavsc.Models.Streaming.LiveFlow", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Activity", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Parent") + .WithMany("Children") + .HasForeignKey("ParentCode"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.CoWorking", b => + { + b.HasOne("Yavsc.Models.Workflow.Profiles.FormationSettings", null) + .WithMany("CoWorking") + .HasForeignKey("FormationSettingsUserId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "Performer") + .WithMany() + .HasForeignKey("PerformerId"); + + b.HasOne("Yavsc.Models.ApplicationUser", "WorkingFor") + .WithMany() + .HasForeignKey("WorkingForId"); + + b.Navigation("Performer"); + + b.Navigation("WorkingFor"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.CommandForm", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany("Forms") + .HasForeignKey("ActivityCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Context"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerProfile", b => + { + b.HasOne("Yavsc.Models.Relationship.Location", "OrganizationAddress") + .WithMany() + .HasForeignKey("OrganizationAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Performer") + .WithMany() + .HasForeignKey("PerformerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationAddress"); + + b.Navigation("Performer"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.RdvQuery", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany() + .HasForeignKey("ActivityCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Relationship.Location", "Location") + .WithMany() + .HasForeignKey("LocationId"); + + b.HasOne("Yavsc.Models.Payment.PayPalPayment", "Regularization") + .WithMany() + .HasForeignKey("PaymentId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "PerformerProfile") + .WithMany() + .HasForeignKey("PerformerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Context"); + + b.Navigation("Location"); + + b.Navigation("PerformerProfile"); + + b.Navigation("Regularization"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.UserActivity", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Does") + .WithMany() + .HasForeignKey("DoesCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "User") + .WithMany("Activity") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Does"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.Project", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany() + .HasForeignKey("ActivityCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Server.Models.IT.SourceCode.GitRepositoryReference", "Repository") + .WithMany() + .HasForeignKey("GitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Payment.PayPalPayment", "Regularization") + .WithMany() + .HasForeignKey("PaymentId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "PerformerProfile") + .WithMany() + .HasForeignKey("PerformerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Context"); + + b.Navigation("PerformerProfile"); + + b.Navigation("Regularization"); + + b.Navigation("Repository"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.ProjectBuildConfiguration", b => + { + b.HasOne("Yavsc.Server.Models.IT.Project", "TargetProject") + .WithMany("Configurations") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TargetProject"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.SourceCode.GitRepositoryReference", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResource", b => + { + b.Navigation("Properties"); + + b.Navigation("Scopes"); + + b.Navigation("Secrets"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScope", b => + { + b.Navigation("Properties"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.Client", b => + { + b.Navigation("AllowedCorsOrigins"); + + b.Navigation("AllowedGrantTypes"); + + b.Navigation("AllowedScopes"); + + b.Navigation("Claims"); + + b.Navigation("ClientSecrets"); + + b.Navigation("IdentityProviderRestrictions"); + + b.Navigation("PostLogoutRedirectUris"); + + b.Navigation("Properties"); + + b.Navigation("RedirectUris"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResource", b => + { + b.Navigation("Properties"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("Yavsc.Models.ApplicationUser", b => + { + b.Navigation("AccountBalance"); + + b.Navigation("BankInfo"); + + b.Navigation("BlackList"); + + b.Navigation("BlogComments"); + + b.Navigation("Book"); + + b.Navigation("Circles"); + + b.Navigation("Connections"); + + b.Navigation("DeviceDeclaration"); + + b.Navigation("Membership"); + + b.Navigation("Posts"); + + b.Navigation("RoomAccess"); + + b.Navigation("Rooms"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Estimate", b => + { + b.Navigation("Bill"); + + b.Navigation("Signatures"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.EstimateTemplate", b => + { + b.Navigation("Bill"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogPost", b => + { + b.Navigation("ACL"); + + b.Navigation("Comments"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.Comment", b => + { + b.Navigation("Children"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.Schedule", b => + { + b.Navigation("Events"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoom", b => + { + b.Navigation("Moderation"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.BrusherProfile", b => + { + b.Navigation("Links"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairMultiCutQuery", b => + { + b.Navigation("Prestations"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestation", b => + { + b.Navigation("Taints"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustDeclaration", b => + { + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustToken", b => + { + b.Navigation("Declarations"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.DjSettings", b => + { + b.Navigation("SoundColor"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.MusicLoverSettings", b => + { + b.Navigation("SoundColor"); + }); + + modelBuilder.Entity("Yavsc.Models.Payment.PayPalPayment", b => + { + b.Navigation("Links"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Circle", b => + { + b.Navigation("Members"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Activity", b => + { + b.Navigation("Children"); + + b.Navigation("Forms"); + + b.Navigation("Services"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerProfile", b => + { + b.Navigation("Activity"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Profiles.FormationSettings", b => + { + b.Navigation("CoWorking"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.Project", b => + { + b.Navigation("Configurations"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Yavsc.Org/Migrations/20260711173717_EnforceBlogAuthorFKs.cs b/src/Yavsc.Org/Migrations/20260711173717_EnforceBlogAuthorFKs.cs new file mode 100644 index 00000000..b13ef467 --- /dev/null +++ b/src/Yavsc.Org/Migrations/20260711173717_EnforceBlogAuthorFKs.cs @@ -0,0 +1,89 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Yavsc.Migrations +{ + /// + public partial class EnforceBlogAuthorFKs : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // Assainir les orphelins AVANT d'enforcer la FK Restrict. + // En prod (Postgres), la migration aurait sinon planté + // sur des billets/commentaires dont l'AuthorId pointe + // vers un user déjà supprimé. La logique métier refuse + // désormais l'orphelin (cf. BlogSpotService.Details) — on + // aligne l'état de la base avec ce contrat. + migrationBuilder.Sql(@" + DO $$ + DECLARE n_comments int; + n_posts int; + BEGIN + DELETE FROM ""Comment"" + WHERE ""AuthorId"" NOT IN (SELECT ""Id"" FROM ""AspNetUsers""); + GET DIAGNOSTICS n_comments = ROW_COUNT; + + DELETE FROM ""BlogSpot"" + WHERE ""AuthorId"" NOT IN (SELECT ""Id"" FROM ""AspNetUsers""); + GET DIAGNOSTICS n_posts = ROW_COUNT; + + RAISE NOTICE 'EnforceBlogAuthorFKs: % orphaned comments deleted, % orphaned blog posts deleted', + n_comments, n_posts; + END $$; + "); + + migrationBuilder.DropForeignKey( + name: "FK_BlogSpot_AspNetUsers_AuthorId", + table: "BlogSpot"); + + migrationBuilder.DropForeignKey( + name: "FK_Comment_AspNetUsers_AuthorId", + table: "Comment"); + + migrationBuilder.AddForeignKey( + name: "FK_BlogSpot_AspNetUsers_AuthorId", + table: "BlogSpot", + column: "AuthorId", + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_Comment_AspNetUsers_AuthorId", + table: "Comment", + column: "AuthorId", + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_BlogSpot_AspNetUsers_AuthorId", + table: "BlogSpot"); + + migrationBuilder.DropForeignKey( + name: "FK_Comment_AspNetUsers_AuthorId", + table: "Comment"); + + migrationBuilder.AddForeignKey( + name: "FK_BlogSpot_AspNetUsers_AuthorId", + table: "BlogSpot", + column: "AuthorId", + principalTable: "AspNetUsers", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Comment_AspNetUsers_AuthorId", + table: "Comment", + column: "AuthorId", + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/src/Yavsc.Org/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Yavsc.Org/Migrations/ApplicationDbContextModelSnapshot.cs index 398b5a6e..ef96638c 100644 --- a/src/Yavsc.Org/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Yavsc.Org/Migrations/ApplicationDbContextModelSnapshot.cs @@ -3803,7 +3803,8 @@ namespace Yavsc.Migrations { b.HasOne("Yavsc.Models.ApplicationUser", "Author") .WithMany("Posts") - .HasForeignKey("AuthorId"); + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Restrict); b.Navigation("Author"); }); @@ -3830,9 +3831,9 @@ namespace Yavsc.Migrations modelBuilder.Entity("Yavsc.Models.Blog.Comment", b => { b.HasOne("Yavsc.Models.ApplicationUser", "Author") - .WithMany() + .WithMany("BlogComments") .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.Cascade) + .OnDelete(DeleteBehavior.Restrict) .IsRequired(); b.HasOne("Yavsc.Models.Blog.Comment", "Parent") @@ -4542,6 +4543,8 @@ namespace Yavsc.Migrations b.Navigation("BlackList"); + b.Navigation("BlogComments"); + b.Navigation("Book"); b.Navigation("Circles"); diff --git a/src/Yavsc.Server/Models/ApplicationDbContext.cs b/src/Yavsc.Server/Models/ApplicationDbContext.cs index 8643f3d3..c3404aed 100644 --- a/src/Yavsc.Server/Models/ApplicationDbContext.cs +++ b/src/Yavsc.Server/Models/ApplicationDbContext.cs @@ -214,6 +214,22 @@ namespace Yavsc.Models // Log immuable — pas de update autorisé e.ToTable(tb => tb.HasCheckConstraint("CK_ModerationLog_Immutable", "1=1")); }); + + // ── Blog FK strictness ───────────────────────────────────────────── + // Tout billet a un auteur, tout commentaire a un auteur : pas de + // cascade en suppression d'un user, pas d'orphelin toléré. Le code + // applicatif (BlogSpotService.Details) s'appuie sur cette + // contrainte pour pouvoir assumer l'existence de l'auteur. + builder.Entity() + .HasOne(b => b.Author) + .WithMany(u => u.Posts) + .HasForeignKey(b => b.AuthorId) + .OnDelete(DeleteBehavior.Restrict); + builder.Entity() + .HasOne(c => c.Author) + .WithMany(u => u.BlogComments) + .HasForeignKey(c => c.AuthorId) + .OnDelete(DeleteBehavior.Restrict); } /// diff --git a/src/Yavsc.Server/Models/ApplicationUser.cs b/src/Yavsc.Server/Models/ApplicationUser.cs index fa577c8b..b64bad3e 100644 --- a/src/Yavsc.Server/Models/ApplicationUser.cs +++ b/src/Yavsc.Server/Models/ApplicationUser.cs @@ -114,6 +114,13 @@ namespace Yavsc.Models [InverseProperty("Member")] public virtual List? Membership { get; set; } + /// + /// User's blog comments + /// + [JsonIgnore] + [InverseProperty("Author")] + public virtual List? BlogComments { get; set; } + IAccountBalance? IApplicationUser.AccountBalance => AccountBalance; ILocation? IApplicationUser.PostalAddress { get => PostalAddress; } From bbdcc7f2adb1a1b6303918db3142dc95a0ee0f2a Mon Sep 17 00:00:00 2001 From: Lum Date: Sat, 11 Jul 2026 19:41:09 +0100 Subject: [PATCH 2/8] Blog: render user avatar through a null-safe helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GET /BlogSpot/Details/1 retournait 500 (avec un 500-sur-500 sur la page d'erreur elle-même) parce que DisplayTemplates /ApplicationUser.cshtml faisait `var avuri = "/Avatars/" + Model.UserName + ".s.png"` : avec enable, Razor émet un null-check implicite sur Model.UserName et lève NullReferenceException quand l'auteur n'a pas de UserName posé (donnée héritée, user partiellement initialisé). - UserDisplayHelpers.AvatarSrc : helper statique pur dans Yavsc.Abstract.Identity qui retourne YavscConstants.DefaultAvatar pour user null / UserName vide ou whitespace, et un path /avatars/.s.png sinon. - ApplicationUser.cshtml : utilise le helper. - Tests : 4 cas (null, vide, whitespace, valide) dans Yavsc.Org.Tests/NonRegression. Bonus : le path d'avatar passe de "/Avatars/" (S majuscule, ne résolvait pas dans le middleware de fichiers statiques) à YavscConstants.AvatarsPath ("/avatars" minuscule), pour fermer l'autre trou que centraliser le calcul permettait de fixer proprement. --- .../Identity/UserDisplayHelpers.cs | 36 +++++++++++ .../NonRegression/UserDisplayHelpersTests.cs | 63 +++++++++++++++++++ .../DisplayTemplates/ApplicationUser.cshtml | 6 +- 3 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 src/Yavsc.Abstract/Identity/UserDisplayHelpers.cs create mode 100644 src/Yavsc.Org.Tests/NonRegression/UserDisplayHelpersTests.cs diff --git a/src/Yavsc.Abstract/Identity/UserDisplayHelpers.cs b/src/Yavsc.Abstract/Identity/UserDisplayHelpers.cs new file mode 100644 index 00000000..c2700e93 --- /dev/null +++ b/src/Yavsc.Abstract/Identity/UserDisplayHelpers.cs @@ -0,0 +1,36 @@ +namespace Yavsc.Abstract.Identity +{ + /// + /// Helpers Razor-friendly pour rendre un user dans une vue + /// sans exposer le template à des accesseurs nullables qui + /// lèveraient . + /// + public static class UserDisplayHelpers + { + /// + /// Chemin d'avatar à utiliser pour + /// dans un display template. Défense contre null + /// (user pas chargé, FK orpheline) et contre un + /// UserName vide (donnée héritée, user partiellement + /// initialisé). Sans cette garde, Razor émet une + /// NullReferenceException dès qu'un accesseur .UserName + /// apparaît dans le template, ce qui remonte en 500 et + /// masque la page d'erreur elle-même. + /// + /// + /// Le path retourné est aligné sur + /// (minuscule). + /// Les anciens display templates utilisaient "/Avatars/" + /// avec un S majuscule, en désaccord avec le path statique + /// servi par le middleware de fichiers — les images ne + /// résolvaient pas. Centraliser le calcul ici ferme les + /// deux trous. + /// + public static string AvatarSrc(IApplicationUser? user) + { + if (string.IsNullOrWhiteSpace(user?.UserName)) + return YavscConstants.DefaultAvatar; + return $"{YavscConstants.AvatarsPath}/{user!.UserName}.s.png"; + } + } +} diff --git a/src/Yavsc.Org.Tests/NonRegression/UserDisplayHelpersTests.cs b/src/Yavsc.Org.Tests/NonRegression/UserDisplayHelpersTests.cs new file mode 100644 index 00000000..a0249fa4 --- /dev/null +++ b/src/Yavsc.Org.Tests/NonRegression/UserDisplayHelpersTests.cs @@ -0,0 +1,63 @@ +using Xunit; +using Yavsc.Abstract; +using Yavsc.Abstract.Identity; + +namespace Yavsc.Org.Tests.NonRegression; + +/// +/// Régression sur le 500 GET /BlogSpot/Details/{id} : un +/// billet dont l'auteur a un UserName null ou absent faisait +/// lever NullReferenceException dans le display template +/// ApplicationUser.cshtml à l'évaluation de +/// Model.UserName. ASP.NET transforme en 500, et la page +/// d'erreur elle-même crash (ErrorViewModel manquant), donc on +/// ne voit rien — juste un 500 muet. +/// +/// Le fix passe par qui +/// retourne pour toute +/// donnée partielle. Ces tests couvrent les trois formes de +/// "donnée absente" : user null, UserName vide, UserName whitespace. +/// +public class UserDisplayHelpersTests +{ + [Fact] + public void AvatarSrc_null_user_returns_default_avatar() + { + Assert.Equal(YavscConstants.DefaultAvatar, UserDisplayHelpers.AvatarSrc(null)); + } + + [Fact] + public void AvatarSrc_user_with_empty_UserName_returns_default_avatar() + { + var user = new FakeUser { UserName = "" }; + Assert.Equal(YavscConstants.DefaultAvatar, UserDisplayHelpers.AvatarSrc(user)); + } + + [Fact] + public void AvatarSrc_user_with_whitespace_UserName_returns_default_avatar() + { + var user = new FakeUser { UserName = " " }; + Assert.Equal(YavscConstants.DefaultAvatar, UserDisplayHelpers.AvatarSrc(user)); + } + + [Fact] + public void AvatarSrc_valid_user_returns_canonical_avatars_path() + { + var user = new FakeUser { UserName = "alice" }; + // Le path doit matcher YavscConstants.AvatarsPath (minuscule), + // pas un /Avatars/ avec S majuscule qui ne résout pas + // dans le middleware de fichiers statiques. + var expected = $"{YavscConstants.AvatarsPath}/alice.s.png"; + Assert.Equal(expected, UserDisplayHelpers.AvatarSrc(user)); + } + + private sealed class FakeUser : IApplicationUser + { + public string Id { get; set; } = ""; + public string? UserName { get; set; } + public string? Avatar { get; set; } + public IAccountBalance? AccountBalance => null; + public string? DedicatedGoogleCalendar => null; + public ILocation? PostalAddress => null; + } +} diff --git a/src/Yavsc.Org/Views/Shared/DisplayTemplates/ApplicationUser.cshtml b/src/Yavsc.Org/Views/Shared/DisplayTemplates/ApplicationUser.cshtml index b7bd532f..2e4ef1bb 100644 --- a/src/Yavsc.Org/Views/Shared/DisplayTemplates/ApplicationUser.cshtml +++ b/src/Yavsc.Org/Views/Shared/DisplayTemplates/ApplicationUser.cshtml @@ -1,7 +1,11 @@ @using Yavsc.Abstract.Identity @model ApplicationUser @{ - var avuri = "/Avatars/" + Model.UserName + ".s.png"; + // Le helper défend contre Model null et contre UserName vide + // ou whitespace. Sans cette garde, Razor lève + // NullReferenceException ici, ce qui propage un 500 et + // masque aussi la page d'erreur. + var avuri = UserDisplayHelpers.AvatarSrc(Model); }
From cb7526de9dcbdd5708f1a95d95bda1b5b9f7655d Mon Sep 17 00:00:00 2001 From: Lum Date: Sat, 11 Jul 2026 19:59:03 +0100 Subject: [PATCH 3/8] Blog: add test guarding the display-template fix + document test architecture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le commit 2 a fixé la NPE du /BlogSpot/Details/{id} en passant le display template par UserDisplayHelpers.AvatarSrc, qui défend contre un UserName null. Ce commit complète le filet de non-régression et pose la doc d'architecture des tests. - ApplicationUserDisplayTemplateTests : assert que le cshtml ne concatène plus directement Model.UserName (ancien code fautif) et qu'il utilise bien le helper. Si quelqu'un revert la ligne 4 du cshtml, les tests cassent. Les autres usages de Model.UserName (alt, title, asp-route-id) sont autorisés : ils ne sont pas la cause du 500, juste laids si null. - doc/testing.md : vue d'ensemble de la stratégie de test (conventions NonRegression/Mandatory/Smoke/Controllers, EF in-memory via InMemoryDatabaseRoot partagé, auth stubs, quand ne pas écrire de test). - src/Yavsc.Tests.Shared/README.md : détails du scaffold partagé (WebHostFixture + son cycle de vie et ses hooks, TestAuthPolicyProvider, TestTokenIssuer) et des deux spécialisations dans le repo (Yavsc.Org.Tests.WebServerFixture et Yavsc.Blogs.Tests.BlogsWebServerFixture). - doc/README.md : entrée vers testing.md dans l'index. --- doc/README.md | 1 + doc/testing.md | 88 +++++++++++ .../ApplicationUserDisplayTemplateTests.cs | 69 ++++++++ src/Yavsc.Tests.Shared/README.md | 149 ++++++++++++++++++ 4 files changed, 307 insertions(+) create mode 100644 doc/testing.md create mode 100644 src/Yavsc.Org.Tests/NonRegression/ApplicationUserDisplayTemplateTests.cs create mode 100644 src/Yavsc.Tests.Shared/README.md diff --git a/doc/README.md b/doc/README.md index b189bb27..912a2e56 100644 --- a/doc/README.md +++ b/doc/README.md @@ -17,6 +17,7 @@ La racine de l'architecture est [Architecture.md](Architecture.md). | [architecture/postit-oidc.md](architecture/postit-oidc.md) | Client desktop PostIt, custom URI scheme, silent refresh | | [architecture/postit.md](architecture/postit.md) | PostIt — topologie des projets, ViewLocator custo, navigation, DI, conventions de binding | | [architecture/decoupage-organisation.md](architecture/decoupage-organisation.md) | Découpage des projets .NET (Abstract, Server, Org, Api, Blogs, Web, Org.Tests) | +| [testing.md](testing.md) | Stratégie de test : conventions des dossiers, EF Core in-memory, auth stubs, scaffold partagé | ## Roadmap & design exploration diff --git a/doc/testing.md b/doc/testing.md new file mode 100644 index 00000000..ef80cda9 --- /dev/null +++ b/doc/testing.md @@ -0,0 +1,88 @@ +# Stratégie de test + +Yavsc utilise **xUnit** (`xunit.v3`) avec un mix d'unitaire pur +et d'intégration légère. Les projets de tests sont sous +`src/.Tests/` et consomment le scaffold partagé +`src/Yavsc.Tests.Shared/`. + +## Vue d'ensemble + +| Sujet | Document | +|---|---| +| Scaffold partagé (`WebHostFixture`, JWT de test, etc.) | [src/Yavsc.Tests.Shared/README.md](../src/Yavsc.Tests.Shared/README.md) | +| Convention des dossiers de tests | [Conventions](#conventions-des-dossiers-de-tests) | +| Driver EF Core en test | [EF Core en test](#ef-core-en-test) | +| Stubs d'authentification et de permissions | [Auth et permissions](#auth-et-permissions) | + +## Conventions des dossiers de tests + +Sous `src/.Tests/`, on trouve quatre dossiers de premier +niveau qui classifient les tests par intention : + +| Dossier | Usage | +|---|---| +| `NonRegression/` | Régressions : un bug constaté, un test qui le détecte si on le réintroduit | +| `Mandatory/` | Tests bloquants : ils doivent passer avant tout merge | +| `Smoke/` | Smoke tests HTTP rapides, montent un host léger | +| `Controllers/` | Tests unitaires des contrôleurs (mock du service, assertions sur le mapping HTTP) | + +Les `NonRegression` sont la cible par défaut quand on fixe un +bug : ils doivent être **rouges avant le fix, verts après**, et +continuer à **casser** si quelqu'un revert le fix. Pas de test +qui passe à vide. + +## EF Core en test + +Pour les tests qui ont besoin d'un `ApplicationDbContext`, on +utilise **`UseInMemoryDatabase`** avec un `InMemoryDatabaseRoot` +partagé au niveau de la fixture. Pas de SQLite, pas de Docker, +pas de mock du contexte : le service testé s'exécute contre +un vrai `DbContext` sur in-memory. + +```csharp +private static readonly InMemoryDatabaseRoot _dbRoot = new(); + +var opts = new DbContextOptionsBuilder() + .UseInMemoryDatabase("Yavsc.Org.Tests.MyFixture", _dbRoot) + .Options; +``` + +Le `InMemoryDatabaseRoot` partagé est important : sans lui, EF +crée un store indépendant par `DbContext` dans certaines +configurations, et un test qui seed + read sur deux contextes +voit un store vide. Le pattern est documenté dans +`BlogsWebServerFixture` ([src/Yavsc.Blogs.Tests/BlogsWebServerFixture.cs](../src/Yavsc.Blogs.Tests/BlogsWebServerFixture.cs)). + +> **Limite connue** : le provider in-memory **ignore** les +> `Migration` EF et ne respecte pas les FK **sur les raw +> SQL** (`ExecuteSqlRaw`). Pour tester des contraintes FK, on +> écrit la configuration dans `OnModelCreating` et on s'appuie +> sur le fait qu'EF la respecte à l'`Add`/`SaveChanges`. Pour +> tester des migrations, c'est l'environnement de staging. + +## Auth et permissions + +L'authorization policy provider de prod est swappé contre +`TestAuthPolicyProvider` (dans `Yavsc.Tests.Shared`) par les +fixtures spécialisées. Les tests qui ont besoin qu'un user soit +"Administrator" envoient un header `X-Test-Rôle` ; ceux qui +veulent un user anonyme omettent le header. + +Pour les tests unitaires qui n'ont pas besoin du pipeline +HTTP, on stub `IAuthorizationService` directement (cf. +`BlogspotController` dans `Yavsc.Org.Tests/NonRegression/`) +pour éviter de monter un host complet. + +## Quand ne PAS écrire de test + +Un test qui ne détecte rien n'est pas un test. Si l'invariant +qu'on cherche à protéger est déjà enforced par EF, par le +compilateur, ou par une couche applicative en amont, le test +est du bruit. Mieux vaut : +- Un test qui assert un **comportement observable** (code + retour HTTP, exception typée, valeur de retour) +- Ou pas de test, et une note dans le code + +La non-régression se prouve par un test qui casse si on +réintroduit le bug. Pas par un test qui passe aujourd'hui et +qui continuera à passer après un revert. diff --git a/src/Yavsc.Org.Tests/NonRegression/ApplicationUserDisplayTemplateTests.cs b/src/Yavsc.Org.Tests/NonRegression/ApplicationUserDisplayTemplateTests.cs new file mode 100644 index 00000000..9fed6a15 --- /dev/null +++ b/src/Yavsc.Org.Tests/NonRegression/ApplicationUserDisplayTemplateTests.cs @@ -0,0 +1,69 @@ +using System.IO; +using Xunit; + +namespace Yavsc.Org.Tests.NonRegression; + +/// +/// Régression du 500 sur GET /BlogSpot/Details/{id} (auteur +/// sans UserName) : le display template +/// ApplicationUser.cshtml ne doit plus accéder à +/// Model.UserName directement. Toute lecture passe par +/// UserDisplayHelpers.AvatarSrc, qui défend contre +/// null et contre les chaînes vides/whitespace. +/// +/// On ne compile pas la vue Razor ici (coût de mise en place +/// disproportionné pour un seul display template) ; on asserte +/// statiquement que le cshtml ne porte plus l'accès fautif. Si +/// quelqu'un revert la ligne, ce test casse. +/// +public class ApplicationUserDisplayTemplateTests +{ + [Fact] + public void ApplicationUser_cshtml_does_not_construct_avatar_path_from_Model_UserName() + { + // L'invariant qu'on protège : l'URL d'avatar ne doit plus + // être construite à partir de Model.UserName direct (le + // commit 2 du fix). Cette construction était la cause du + // 500 sur GET /BlogSpot/Details/{id} : avec + // enable, Razor émet un null-check + // implicite sur l'expression, et lève NPE si UserName est + // null. Le helper AvatarSrc défend contre ce cas. + // + // On n'interdit pas les autres usages de Model.UserName + // (alt, title, asp-route-id) : Razor les rend en chaîne + // vide si null, sans NPE. C'est laid, pas cassé. + var path = ResolveTemplatePath(); + var content = File.ReadAllText(path); + + // L'ancien code fautif concaténait directement + // "/Avatars/" + Model.UserName + ".s.png". + Assert.DoesNotContain("Model.UserName + ", content); + Assert.DoesNotContain("Model.UserName+", content); + } + + [Fact] + public void ApplicationUser_cshtml_uses_the_null_safe_helper_for_avatar() + { + var path = ResolveTemplatePath(); + var content = File.ReadAllText(path); + + Assert.Contains("UserDisplayHelpers.AvatarSrc", content); + } + + private static string ResolveTemplatePath() + { + // Le test s'exécute depuis src/Yavsc.Org.Tests/bin/..., + // on remonte pour trouver la vue source. + var dir = AppContext.BaseDirectory; + for (var i = 0; i < 8 && dir is not null; i++) + { + var candidate = Path.Combine(dir, + "src", "Yavsc.Org", "Views", "Shared", + "DisplayTemplates", "ApplicationUser.cshtml"); + if (File.Exists(candidate)) return candidate; + dir = Path.GetDirectoryName(dir); + } + throw new FileNotFoundException( + "Could not locate ApplicationUser.cshtml from " + AppContext.BaseDirectory); + } +} diff --git a/src/Yavsc.Tests.Shared/README.md b/src/Yavsc.Tests.Shared/README.md new file mode 100644 index 00000000..2bef8fc9 --- /dev/null +++ b/src/Yavsc.Tests.Shared/README.md @@ -0,0 +1,149 @@ +# Yavsc.Tests.Shared + +Scaffold partagé pour les tests d'intégration ASP.NET Core de +Yavsc. Ce projet **n'est pas lui-même un projet de tests** — il +n'a pas xUnit ni de test runner. Il expose des fixtures +réutilisables que les projets de tests consommateurs +(`Yavsc.Org.Tests`, `Yavsc.Blogs.Tests`, etc.) héritent ou +instancient. + +## Contenu + +| Fichier | Rôle | +|---|---| +| `WebHostFixture.cs` | Base abstraite : Kestrel HTTPS, certificat auto-signé, port dynamique, host partagé inter-fixtures | +| `TestAuthPolicyProvider.cs` | `IAuthorizationPolicyProvider` de test, lit `X-Test-Role` au lieu d'interroger la DB | +| `TestTokenIssuer.cs` | Émet des JWT HS256 signés avec une clé statique, pour les tests d'API qui montent un `AddJwtBearer` réel | + +## WebHostFixture + +`WebHostFixture` est la base de toute fixture d'intégration. +Une seule instance de `WebApplication` tourne par process ; les +fixtures qui héritent partagent le host. Kestrel est bindé sur +`127.0.0.1:0` (port dynamique) avec un certificat auto-signé +généré lazily. + +### Cycle de vie + +- **Premier ctor** d'une fixture concrète → `InitializeAsync()` + lance `BuildApp(builder)` puis `ConfigurePipelineAsync(app)` + puis `app.StartAsync()`. L'`IServerAddressesFeature` est lu + pour peupler `Addresses`. +- **Ctors suivants** (xUnit instancie une fixture par + `IClassFixture`) → reprise de l'état partagé via + `CopySpecialisedSharedState()` (vide par défaut, surchargeable). +- **Dernier `Dispose`** → `app.StopAsync()`, reset des slots + statiques. + +### Hooks à surcharger + +| Hook | Quand | Quoi y mettre | +|---|---|---| +| `BuildApp(builder)` | Toujours | Enregistrement des services, configuration in-memory, seeding éventuel | +| `ConfigurePipelineAsync(app)` | Optionnel | Pipeline middleware spécifique (sinon : pas de pipeline custom) | +| `CopySpecialisedSharedState()` | Optionnel | Recopie des slots statiques de la spécialisation sur les propriétés d'instance | + +### Exemple : fixture de portée minimale + +```csharp +public sealed class MyFixture : WebHostFixture +{ + protected override WebApplication BuildApp(WebApplicationBuilder builder) + { + // In-memory config, services, etc. + return builder.Build(); + } +} +``` + +`MyFixture` n'a pas de test runner propre ; c'est l'assembly +consommateur (par exemple `Yavsc.MyModule.Tests`) qui déclare +les `[Fact]` et utilise `IClassFixture`. + +## Spécifications : fixtures concrètes + +Deux fixtures héritent de `WebHostFixture` dans le repo : + +### Yavsc.Org.Tests.WebServerFixture + +Pour le host principal de Yavsc.Org. Caractéristiques : + +- Configure `InMemory` pour la `ConnectionStrings` Yavsc +- Remplace `IAuthorizationPolicyProvider` par + `TestAuthPolicyProvider` **avant** `ConfigureWebAppServices` + (qui freeze la collection de services) +- Stub `ISmtpClientFactory` par `RecordingSmtpClientFactory` + pour capturer les envois sans SMTP réel +- Seed IdentityServer8 : un `Client` + une `ApiScope` "test" + + un `ApplicationUser` "Tester" +- Configure le pipeline via `app.ConfigurePipeline(...)` avec + un manifeste de static assets explicite (le MSBuild target + `CopyYavscOrgStaticAssets` du csproj miroir les manifests + Yavsc.Org sous le nom Yavsc.Org.Tests.* dans le bin de test) + +Cf. `src/Yavsc.Org.Tests/WebServerFixture.cs`. + +### Yavsc.Blogs.Tests.BlogsWebServerFixture + +Pour le host API de Yavsc.Blogs. Caractéristiques : + +- `UseInMemoryDatabase("Yavsc.Blogs.Tests", _inMemoryRoot)` — + un `InMemoryDatabaseRoot` partagé pour que POST + GET voient + le même store +- `BlogSpotService` réel (pas de mock) +- `PermissionHandler` réel (le handler d'authorization qui + résout `IsOwner(user, blog)`) +- `AddJwtBearer` réel avec HS256, validation contre + `TestTokenIssuer.SigningKey` — pas d'OIDC discovery, pas + d'IdP +- Politique `BlogScope` verbatim (`RequireAuthenticatedUser` + + `RequireClaim("scope", "blogs")`) + +Cf. `src/Yavsc.Blogs.Tests/BlogsWebServerFixture.cs`. + +## TestAuthPolicyProvider + +`IAuthorizationPolicyProvider` de test qui lit le rôle dans +l'en-tête HTTP `X-Test-Role` au lieu d'interroger la +`UserManager`. Permet aux smoke tests d'exercer `[Authorize +("AdministratorOnly")]` sans seed de rôle réel. + +Activation : enregistré par les fixtures spécialisées **avant** +`ConfigureWebAppServices` (qui call `builder.Build()` et +fige la collection). La sémantique last-write-wins du +`AddSingleton` fait que le test provider prend le pas. + +## TestTokenIssuer + +Émet un JWT HS256 avec une `SigningKey` statique, exposé en +`TestTokenIssuer.SigningKey` (et `Issuer`). Les fixtures qui +montent un `AddJwtBearer` réutilisent cette clé pour valider +les tokens localement, sans OIDC discovery. + +Helpers : + +- `TestTokenIssuer.Issue(subject, scope, lifetime)` → + chaîne `"Bearer "` prête pour un header HTTP +- `TestTokenIssuer.SigningKey` — `SymmetricSecurityKey` à + passer au `TokenValidationParameters` du `AddJwtBearer` + +## Tests statiques sur du code compilé + +Pour tester un display template Razor sans monter un host +ASP.NET, on peut s'appuyer sur la lecture du fichier source +et asserter des invariants syntaxiques. Cf. +`Yavsc.Org.Tests/NonRegression/ApplicationUserDisplayTemplateTests` +pour un exemple : on asserte que le cshtml ne porte plus +`Model.UserName` directement, ce qui aurait rouvert la +non-régression du 500 sur `/BlogSpot/Details/{id}`. + +C'est pragmatique : la mise en place d'un `RazorProjectEngine` +pour compiler et rendre une vue hors host coûte plus cher que +ce qu'elle protège pour un seul template. + +## Pour aller plus loin + +- `doc/testing.md` à la racine : vue d'ensemble de la + stratégie de test +- `src/Yavsc.Org.Tests/NonRegression/` et + `src/Yavsc.Blogs.Tests/` : exemples d'utilisation From 7a066707b3ec1ab411e194af32998a9988dd3f9e Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sat, 11 Jul 2026 20:50:39 +0100 Subject: [PATCH 4/8] Tests: route Yavsc.Org test host through Testing environment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TestWebApplicationFactory used ASPNETCORE_ENVIRONMENT=Development, which caused Program.Main's AddConfiguration("org") to load the tracked appsettings-org.json (the reference file with the '*** via dotnet user-secrets ou variable d'environnement ***' placeholder connection string). Npgsql then failed to parse that placeholder during host startup, failing six integration tests (observed 2026-07-11: System.ArgumentException on NpgsqlConnectionStringBuilder.set_Item). Switching the test host to a dedicated Testing environment makes AddConfiguration("org") pick up the new optional appsettings-org.Testing.json file as the last source in the chain (JSON → env vars), which overrides YavscConnection with the InMemory marker and the Smtp section with the test stub values. The .gitignore exception whitelists this file explicitly: it is a configuration source for the test host, not a secrets file. The WebServerFixture path is unchanged — it owns its WebApplicationBuilder and adds the same in-memory override via its BuildApp hook. --- .gitignore | 9 +++++++++ src/Yavsc.Org.Tests/TestWebApplicationFactory.cs | 13 +++++++++---- src/Yavsc.Org/appsettings-org.Testing.json | 11 +++++++++++ 3 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 src/Yavsc.Org/appsettings-org.Testing.json diff --git a/.gitignore b/.gitignore index fb843f96..a94475e3 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,15 @@ data/ appsettings.*.json appsettings-*.*.json +# Exception: the Testing-environment override for Yavsc.Org is a tracked +# configuration source, not a secrets file. TestWebApplicationFactory +# (Yavsc.Org.Tests) flips ASPNETCORE_ENVIRONMENT to "Testing" so +# AddConfiguration("org") in Program.Main loads this file as the +# last in the chain (it is optional). It overrides the connection +# string and SMTP section for the in-memory test host and contains +# no production secrets. +!src/Yavsc.Org/appsettings-org.Testing.json + generated/ *.tmp DataDir/ diff --git a/src/Yavsc.Org.Tests/TestWebApplicationFactory.cs b/src/Yavsc.Org.Tests/TestWebApplicationFactory.cs index c51f3f8e..dfd6edea 100644 --- a/src/Yavsc.Org.Tests/TestWebApplicationFactory.cs +++ b/src/Yavsc.Org.Tests/TestWebApplicationFactory.cs @@ -26,10 +26,15 @@ public class TestWebApplicationFactory : WebApplicationFactory { protected override void ConfigureWebHost(IWebHostBuilder builder) { - // UseDevelopmentEnvironment triggers the dev signing credential - // path in the production startup, so we don't need a real cert - // to satisfy IdentityServer at boot. - builder.UseEnvironment("Development"); + // UseEnvironment("Testing") puts the host in a dedicated + // configuration environment so AddConfiguration("org") in + // Program.Main loads the optional appsettings-org.Testing.json + // file (which overrides the connection string and SMTP section + // for the test host). See that file for the values. + // We don't use "Development" because that environment is also + // used by the dev launcher and would change the signing + // credential path in IdentityServer; "Testing" is unambiguous. + builder.UseEnvironment("Testing"); builder.ConfigureTestServices(services => { diff --git a/src/Yavsc.Org/appsettings-org.Testing.json b/src/Yavsc.Org/appsettings-org.Testing.json new file mode 100644 index 00000000..c640ffcd --- /dev/null +++ b/src/Yavsc.Org/appsettings-org.Testing.json @@ -0,0 +1,11 @@ +{ + "ConnectionStrings": { + "YavscConnection": "InMemory" + }, + "Smtp": { + "Host": "smtp.test.local", + "Port": 465, + "UserName": "test-user", + "Password": "test-pass" + } +} From fa7794b7a01c41588e54fc4395777b118940deab Mon Sep 17 00:00:00 2001 From: Lum Date: Sat, 11 Jul 2026 21:56:25 +0100 Subject: [PATCH 5/8] repoduces the bug --- Directory.Packages.props | 1 + src/Yavsc.Blogs.Tests/BlogApiTests.cs | 9 +-- .../BlogsWebServerFixture.cs | 32 ++++++--- .../Yavsc.Blogs.Tests.csproj | 1 + .../TestWebApplicationFactory.cs | 69 +++++++++++++++++++ src/Yavsc.Org.Tests/WebServerFixture.cs | 15 +++- src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj | 1 + src/Yavsc.Org/Extensions/HostingExtensions.cs | 56 +++++++++++++-- src/Yavsc.Org/Yavsc.Org.csproj | 1 + src/Yavsc.Org/appsettings-org.Testing.json | 2 +- 10 files changed, 169 insertions(+), 18 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index e4b09159..84380e44 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -18,6 +18,7 @@ + diff --git a/src/Yavsc.Blogs.Tests/BlogApiTests.cs b/src/Yavsc.Blogs.Tests/BlogApiTests.cs index fd64555a..4ac14c50 100644 --- a/src/Yavsc.Blogs.Tests/BlogApiTests.cs +++ b/src/Yavsc.Blogs.Tests/BlogApiTests.cs @@ -30,10 +30,11 @@ public sealed class BlogApiTests : IClassFixture } /// Reset the in-memory database to a known empty state. - /// UseInMemoryDatabase shares its store across the - /// lifetime of the instance, - /// so without a per-test reset the test order would leak - /// state between tests. + /// The fixture now uses SQLite in-memory (see forgejo#3), which + /// shares its store across the lifetime of the + /// instance, so without a + /// per-test reset the test order would leak state between + /// tests.
private void ResetDatabase() { using var scope = _fixture.Services.CreateScope(); diff --git a/src/Yavsc.Blogs.Tests/BlogsWebServerFixture.cs b/src/Yavsc.Blogs.Tests/BlogsWebServerFixture.cs index 6e39fd00..a76a37a7 100644 --- a/src/Yavsc.Blogs.Tests/BlogsWebServerFixture.cs +++ b/src/Yavsc.Blogs.Tests/BlogsWebServerFixture.cs @@ -48,6 +48,19 @@ namespace Yavsc.Blogs.Tests; /// public sealed class BlogsWebServerFixture : WebHostFixture { + // SQLite in-memory database is created once and shared across all + // DbContext instances for the test lifetime. The connection must + // stay open: closing it destroys the in-memory database. The + // Microsoft.Data.Sqlite pool will then open additional connections + // to the same in-memory store, as long as the original connection + // is alive. This is the SQLite equivalent of the EF Core + // InMemoryDatabaseRoot we used to use. + private Microsoft.Data.Sqlite.SqliteConnection? _sharedSqliteConnection; + // Legacy field kept to make the migration diff readable. The + // InMemory provider path is no longer used by this fixture, but + // removing it is out of scope for the SQLite-in-memory migration + // (forgejo#3 follow-up). + [System.Obsolete("Replaced by SQLite in-memory (forgejo#3).")] private InMemoryDatabaseRoot? _inMemoryRoot; protected override WebApplication BuildApp(WebApplicationBuilder builder) @@ -58,15 +71,18 @@ public sealed class BlogsWebServerFixture : WebHostFixture // against an empty table returns an empty list, which is // exactly what the first test wants to assert. // - // Share a single InMemoryDatabaseRoot across the test - // lifetime so POST + GET on the same fixture see the same - // store. Without the root, EF Core's In-Memory provider - // creates independent stores per DbContext in some - // configurations, and the second request would see an - // empty list even after the first wrote a row. - _inMemoryRoot = new InMemoryDatabaseRoot(); + // We use SQLite in-memory (not the EF Core InMemory provider) + // because the InMemory provider cannot materialise navigation + // properties from IdentityServer8 entity types (see forgejo#3). + // SQLite in-memory is a transient, file-less store that + // executes real SQL, so navigation properties work as + // expected. The shared SqliteConnection keeps the database + // alive for the test lifetime, mirroring the + // InMemoryDatabaseRoot pattern we used previously. + _sharedSqliteConnection = new Microsoft.Data.Sqlite.SqliteConnection("Data Source=:memory:"); + _sharedSqliteConnection.Open(); builder.Services.AddDbContext(opt => - opt.UseInMemoryDatabase("Yavsc.Blogs.Tests", _inMemoryRoot)); + opt.UseSqlite(_sharedSqliteConnection)); // Trivial file-system auth: the GET index path never calls // into it, but the DI container needs an instance. diff --git a/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj b/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj index ec1f7f0a..831097b0 100644 --- a/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj +++ b/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj @@ -17,6 +17,7 @@ + diff --git a/src/Yavsc.Org.Tests/TestWebApplicationFactory.cs b/src/Yavsc.Org.Tests/TestWebApplicationFactory.cs index dfd6edea..af40bb78 100644 --- a/src/Yavsc.Org.Tests/TestWebApplicationFactory.cs +++ b/src/Yavsc.Org.Tests/TestWebApplicationFactory.cs @@ -2,7 +2,11 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Yavsc.Models; using Yavsc.Tests.Shared; namespace Yavsc.Org.Tests; @@ -24,6 +28,21 @@ namespace Yavsc.Org.Tests; /// public class TestWebApplicationFactory : WebApplicationFactory { + // SQLite in-memory: the connection must stay open for the lifetime + // of the host, otherwise the in-memory database is destroyed and + // every new DbContext sees an empty store. We hold the connection + // here so it is disposed only when the factory is disposed. The + // Microsoft.Data.Sqlite pool reuses the underlying in-memory store + // across additional connections opened against the same connection + // string, as long as the original connection is alive. This is the + // SQLite equivalent of the EF Core InMemoryDatabaseRoot pattern. + private readonly SqliteConnection _sharedSqliteConnection = new("Data Source=:memory:"); + + public TestWebApplicationFactory() + { + _sharedSqliteConnection.Open(); + } + protected override void ConfigureWebHost(IWebHostBuilder builder) { // UseEnvironment("Testing") puts the host in a dedicated @@ -38,6 +57,25 @@ public class TestWebApplicationFactory : WebApplicationFactory builder.ConfigureTestServices(services => { + // The production Program.Main calls AddConfiguration("org") + // and then AddIdentityDBAndStores which calls + // GetConnectionString("YavscConnection"). The result is + // "Data Source=:memory:" (from appsettings-org.Testing.json), + // and the production code path in HostingExtensions routes + // that to UseSqlite. However, the EF Core in-memory test + // pattern needs all DbContext instances to see the same + // store; with a raw "Data Source=:memory:" connection string, + // each connection opens its own private database. We + // therefore drop the production DbContext registration and + // re-register ApplicationDbContext with the shared + // SqliteConnection held by this factory. Tests that need + // the schema to exist call EnsureCreated on the resulting + // DbContext (e.g. ClientControllerCollectionTests seeds a + // Client row in its constructor). + services.RemoveAll>(); + services.AddDbContext(opt => + opt.UseSqlite(_sharedSqliteConnection)); + // Replace the production IAuthorizationPolicyProvider with // the test one. The default registered by AddAuthorization // becomes irrelevant: any GetPolicyAsync call is routed here. @@ -48,6 +86,37 @@ public class TestWebApplicationFactory : WebApplicationFactory // TestUserMiddleware runs after UseAuthentication/Authorization. services.AddTransient(); services.AddTransient(); + + // Run EnsureCreated once at host start. With SQLite in-memory + // and a shared connection, this creates the schema once + // and the schema persists for the host lifetime. The + // test code (e.g. ClientControllerCollectionTests seed) can + // then write rows without having to call EnsureCreated + // itself. EnsureCreated is idempotent: re-running it on + // an existing schema is a no-op. + services.AddHostedService(); }); } + + private sealed class SqliteEnsureCreatedHostedService : IHostedService + { + private readonly IServiceProvider _services; + public SqliteEnsureCreatedHostedService(IServiceProvider services) + { + _services = services; + } + public Task StartAsync(CancellationToken cancellationToken) + { + using var scope = _services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + return db.Database.EnsureCreatedAsync(cancellationToken); + } + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } + + protected override void Dispose(bool disposing) + { + if (disposing) _sharedSqliteConnection.Dispose(); + base.Dispose(disposing); + } } diff --git a/src/Yavsc.Org.Tests/WebServerFixture.cs b/src/Yavsc.Org.Tests/WebServerFixture.cs index ed0bd69d..ebb50fc7 100644 --- a/src/Yavsc.Org.Tests/WebServerFixture.cs +++ b/src/Yavsc.Org.Tests/WebServerFixture.cs @@ -75,7 +75,20 @@ public sealed class WebServerFixture : WebHostFixture // that plus the in-memory overrides below. builder.AddConfiguration(null).AddInMemoryCollection(new Dictionary { - [$"ConnectionStrings:{YavscConstants.YavscConnectionStringName}"] = "InMemory", + // The EF Core in-memory provider cannot materialise + // entity types from IdentityServer8 (see forgejo#3): + // it crashes with IndexOutOfRangeException on the + // multi-Include query in ClientController.LoadClientAsync + // and on per-collection LoadAsync. SQLite in-memory is + // a transient, file-less store that uses the same + // connection string semantics as the InMemory provider + // ("keep the connection open for the host lifetime") + // but actually executes SQL, so it handles + // navigation-property entities correctly. The + // HostingExtensions code path detects this connection + // string and routes to UseSqlite. See + // doc/testing.md for the test-driver policy. + [$"ConnectionStrings:{YavscConstants.YavscConnectionStringName}"] = "Data Source=:memory:", // SMTP test config: UserName non-null so MailSender // exercises the Authenticate branch — the // RecordingSmtpClient captures it. diff --git a/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj b/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj index 79a6bae1..5d049626 100644 --- a/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj +++ b/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj @@ -53,6 +53,7 @@ +