♻️ Refactored the authorized device (now client)

This commit is contained in:
2025-08-13 15:27:31 +08:00
parent 76fdf14e79
commit f8d8e485f1
12 changed files with 2095 additions and 91 deletions

View File

@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using DysonNetwork.Pass.Auth;
using DysonNetwork.Pass.Permission; using DysonNetwork.Pass.Permission;
using DysonNetwork.Pass.Wallet; using DysonNetwork.Pass.Wallet;
using DysonNetwork.Shared.Data; using DysonNetwork.Shared.Data;
@@ -9,7 +10,6 @@ using Microsoft.EntityFrameworkCore;
using NodaTime; using NodaTime;
using AuthService = DysonNetwork.Pass.Auth.AuthService; using AuthService = DysonNetwork.Pass.Auth.AuthService;
using AuthSession = DysonNetwork.Pass.Auth.AuthSession; using AuthSession = DysonNetwork.Pass.Auth.AuthSession;
using ChallengePlatform = DysonNetwork.Pass.Auth.ChallengePlatform;
namespace DysonNetwork.Pass.Account; namespace DysonNetwork.Pass.Account;
@@ -437,25 +437,16 @@ public class AccountCurrentController(
} }
} }
public class AuthorizedDevice
{
public string? Label { get; set; }
public string UserAgent { get; set; } = null!;
public string DeviceId { get; set; } = null!;
public ChallengePlatform Platform { get; set; }
public List<AuthSession> Sessions { get; set; } = [];
}
[HttpGet("devices")] [HttpGet("devices")]
[Authorize] [Authorize]
public async Task<ActionResult<List<AuthorizedDevice>>> GetDevices() public async Task<ActionResult<List<AuthClient>>> GetDevices()
{ {
if (HttpContext.Items["CurrentUser"] is not Account currentUser || if (HttpContext.Items["CurrentUser"] is not Account currentUser ||
HttpContext.Items["CurrentSession"] is not AuthSession currentSession) return Unauthorized(); HttpContext.Items["CurrentSession"] is not AuthSession currentSession) return Unauthorized();
Response.Headers.Append("X-Auth-Session", currentSession.Id.ToString()); Response.Headers.Append("X-Auth-Session", currentSession.Id.ToString());
var devices = await db.AuthDevices var devices = await db.AuthClients
.Where(device => device.AccountId == currentUser.Id) .Where(device => device.AccountId == currentUser.Id)
.ToListAsync(); .ToListAsync();
@@ -525,14 +516,14 @@ public class AccountCurrentController(
} }
} }
[HttpPatch("sessions/{id:guid}/label")] [HttpPatch("devices/{id}/label")]
public async Task<ActionResult<AuthSession>> UpdateSessionLabel(Guid id, [FromBody] string label) public async Task<ActionResult<AuthSession>> UpdateDeviceLabel(string id, [FromBody] string label)
{ {
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
try try
{ {
await accounts.UpdateSessionLabel(currentUser, id, label); await accounts.UpdateDeviceName(currentUser, id, label);
return NoContent(); return NoContent();
} }
catch (Exception ex) catch (Exception ex)
@@ -541,15 +532,18 @@ public class AccountCurrentController(
} }
} }
[HttpPatch("sessions/current/label")] [HttpPatch("devices/current/label")]
public async Task<ActionResult<AuthSession>> UpdateCurrentSessionLabel([FromBody] string label) public async Task<ActionResult<AuthSession>> UpdateCurrentDeviceLabel([FromBody] string label)
{ {
if (HttpContext.Items["CurrentUser"] is not Account currentUser || if (HttpContext.Items["CurrentUser"] is not Account currentUser ||
HttpContext.Items["CurrentSession"] is not AuthSession currentSession) return Unauthorized(); HttpContext.Items["CurrentSession"] is not AuthSession currentSession) return Unauthorized();
var device = await db.AuthClients.FirstOrDefaultAsync(d => d.Id == currentSession.Challenge.ClientId);
if (device is null) return NotFound();
try try
{ {
await accounts.UpdateSessionLabel(currentUser, currentSession.Id, label); await accounts.UpdateDeviceName(currentUser, device.DeviceId, label);
return NoContent(); return NoContent();
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -11,7 +11,6 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using NodaTime; using NodaTime;
using OtpNet; using OtpNet;
using AuthSession = DysonNetwork.Pass.Auth.AuthSession;
namespace DysonNetwork.Pass.Account; namespace DysonNetwork.Pass.Account;
@@ -458,37 +457,28 @@ public class AccountService(
public async Task<bool> IsDeviceActive(Guid id) public async Task<bool> IsDeviceActive(Guid id)
{ {
return await db.AuthChallenges.AnyAsync(d => d.DeviceId == id); return await db.AuthSessions
.Include(s => s.Challenge)
.AnyAsync(s => s.Challenge.ClientId == id);
} }
public async Task<AuthSession> UpdateSessionLabel(Account account, Guid sessionId, string label) public async Task<AuthClient> UpdateDeviceName(Account account, string deviceId, string label)
{ {
var session = await db.AuthSessions var device = await db.AuthClients.FirstOrDefaultAsync(d => d.DeviceId == deviceId && d.AccountId == account.Id);
.Include(s => s.Challenge) if (device is null) throw new InvalidOperationException("Device was not found.");
.Where(s => s.Id == sessionId && s.AccountId == account.Id)
.FirstOrDefaultAsync();
if (session is null) throw new InvalidOperationException("Session was not found.");
await db.AuthSessions device.DeviceLabel = label;
.Include(s => s.Challenge) db.Update(device);
.Where(s => s.Challenge.DeviceId == session.Challenge.DeviceId) await db.SaveChangesAsync();
.ExecuteUpdateAsync(p => p.SetProperty(s => s.Label, label));
var sessions = await db.AuthSessions return device;
.Include(s => s.Challenge)
.Where(s => s.AccountId == session.Id && s.Challenge.DeviceId == session.Challenge.DeviceId)
.ToListAsync();
foreach (var item in sessions)
await cache.RemoveAsync($"{DysonTokenAuthHandler.AuthCachePrefix}{item.Id}");
return session;
} }
public async Task DeleteSession(Account account, Guid sessionId) public async Task DeleteSession(Account account, Guid sessionId)
{ {
var session = await db.AuthSessions var session = await db.AuthSessions
.Include(s => s.Challenge) .Include(s => s.Challenge)
.ThenInclude(s => s.Device) .ThenInclude(s => s.Client)
.Where(s => s.Id == sessionId && s.AccountId == account.Id) .Where(s => s.Id == sessionId && s.AccountId == account.Id)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (session is null) throw new InvalidOperationException("Session was not found."); if (session is null) throw new InvalidOperationException("Session was not found.");
@@ -498,10 +488,13 @@ public class AccountService(
.Where(s => s.AccountId == session.Id && s.Challenge.DeviceId == session.Challenge.DeviceId) .Where(s => s.AccountId == session.Id && s.Challenge.DeviceId == session.Challenge.DeviceId)
.ToListAsync(); .ToListAsync();
if (!await IsDeviceActive(session.Challenge.DeviceId)) if (session.Challenge.ClientId.HasValue)
await pusher.UnsubscribePushNotificationsAsync(new UnsubscribePushNotificationsRequest() {
{ DeviceId = session.Challenge.Device.DeviceId } if (!await IsDeviceActive(session.Challenge.ClientId.Value))
); await pusher.UnsubscribePushNotificationsAsync(new UnsubscribePushNotificationsRequest()
{ DeviceId = session.Challenge.Client!.DeviceId }
);
}
// The current session should be included in the sessions' list // The current session should be included in the sessions' list
await db.AuthSessions await db.AuthSessions

View File

@@ -37,7 +37,7 @@ public class AppDatabase(
public DbSet<AuthSession> AuthSessions { get; set; } public DbSet<AuthSession> AuthSessions { get; set; }
public DbSet<AuthChallenge> AuthChallenges { get; set; } public DbSet<AuthChallenge> AuthChallenges { get; set; }
public DbSet<AuthDevice> AuthDevices { get; set; } public DbSet<AuthClient> AuthClients { get; set; }
public DbSet<Wallet.Wallet> Wallets { get; set; } public DbSet<Wallet.Wallet> Wallets { get; set; }
public DbSet<WalletPocket> WalletPockets { get; set; } public DbSet<WalletPocket> WalletPockets { get; set; }
@@ -89,7 +89,7 @@ public class AppDatabase(
} }
}); });
optionsBuilder.UseSeeding((context, _) => {}); optionsBuilder.UseSeeding((context, _) => { });
base.OnConfiguring(optionsBuilder); base.OnConfiguring(optionsBuilder);
} }

View File

@@ -68,7 +68,7 @@ public class AuthController(
IpAddress = ipAddress, IpAddress = ipAddress,
UserAgent = userAgent, UserAgent = userAgent,
Location = geo.GetPointFromIp(ipAddress), Location = geo.GetPointFromIp(ipAddress),
DeviceId = device.Id, ClientId = device.Id,
AccountId = account.Id AccountId = account.Id
}.Normalize(); }.Normalize();

View File

@@ -1,17 +0,0 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using DysonNetwork.Shared.Data;
using Microsoft.EntityFrameworkCore;
namespace DysonNetwork.Pass.Auth;
[Index(nameof(DeviceId), IsUnique = true)]
public class AuthDevice : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(1024)] public string DeviceName { get; set; } = string.Empty;
[MaxLength(1024)] public string DeviceId { get; set; } = string.Empty;
public Guid AccountId { get; set; }
[JsonIgnore] public Account.Account Account { get; set; } = null!;
}

View File

@@ -101,16 +101,16 @@ public class AuthService(
return session; return session;
} }
public async Task<AuthDevice> GetOrCreateDeviceAsync(Guid accountId, string deviceId) public async Task<AuthClient> GetOrCreateDeviceAsync(Guid accountId, string deviceId)
{ {
var device = await db.AuthDevices.FirstOrDefaultAsync(d => d.DeviceId == deviceId && d.AccountId == accountId); var device = await db.AuthClients.FirstOrDefaultAsync(d => d.DeviceId == deviceId && d.AccountId == accountId);
if (device is not null) return device; if (device is not null) return device;
device = new AuthDevice device = new AuthClient
{ {
DeviceId = deviceId, DeviceId = deviceId,
AccountId = accountId AccountId = accountId
}; };
db.AuthDevices.Add(device); db.AuthClients.Add(device);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
return device; return device;

View File

@@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using DysonNetwork.Shared.Data; using DysonNetwork.Shared.Data;
using Microsoft.EntityFrameworkCore;
using NodaTime; using NodaTime;
using NodaTime.Serialization.Protobuf; using NodaTime.Serialization.Protobuf;
using Point = NetTopologySuite.Geometries.Point; using Point = NetTopologySuite.Geometries.Point;
@@ -68,12 +69,13 @@ public class AuthChallenge : ModelBase
[MaxLength(128)] public string? IpAddress { get; set; } [MaxLength(128)] public string? IpAddress { get; set; }
[MaxLength(512)] public string? UserAgent { get; set; } [MaxLength(512)] public string? UserAgent { get; set; }
[MaxLength(1024)] public string? Nonce { get; set; } [MaxLength(1024)] public string? Nonce { get; set; }
[MaxLength(1024)] public string? DeviceId { get; set; } = string.Empty;
public Point? Location { get; set; } public Point? Location { get; set; }
public Guid AccountId { get; set; } public Guid AccountId { get; set; }
[JsonIgnore] public Account.Account Account { get; set; } = null!; [JsonIgnore] public Account.Account Account { get; set; } = null!;
public Guid DeviceId { get; set; } public Guid? ClientId { get; set; }
public AuthDevice Device { get; set; } = null!; public AuthClient? Client { get; set; } = null!;
public AuthChallenge Normalize() public AuthChallenge Normalize()
{ {
@@ -95,8 +97,20 @@ public class AuthChallenge : ModelBase
Scopes = { Scopes }, Scopes = { Scopes },
IpAddress = IpAddress, IpAddress = IpAddress,
UserAgent = UserAgent, UserAgent = UserAgent,
DeviceId = DeviceId.ToString(), DeviceId = Client.DeviceId.ToString(),
Nonce = Nonce, Nonce = Nonce,
AccountId = AccountId.ToString() AccountId = AccountId.ToString()
}; };
} }
[Index(nameof(DeviceId), IsUnique = true)]
public class AuthClient : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(1024)] public string DeviceName { get; set; } = string.Empty;
[MaxLength(1024)] public string? DeviceLabel { get; set; }
[MaxLength(1024)] public string DeviceId { get; set; } = string.Empty;
public Guid AccountId { get; set; }
[JsonIgnore] public Account.Account Account { get; set; } = null!;
}

View File

@@ -227,7 +227,7 @@ public abstract class OidcService(
Audiences = [ProviderName], Audiences = [ProviderName],
Scopes = ["*"], Scopes = ["*"],
AccountId = account.Id, AccountId = account.Id,
DeviceId = device.Id, ClientId = device.Id,
IpAddress = request.Connection.RemoteIpAddress?.ToString() ?? null, IpAddress = request.Connection.RemoteIpAddress?.ToString() ?? null,
UserAgent = request.Request.Headers.UserAgent, UserAgent = request.Request.Headers.UserAgent,
}; };

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,110 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using NodaTime;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class AddAuthorizeDevice : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "device_id",
table: "auth_challenges",
type: "character varying(1024)",
maxLength: 1024,
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(256)",
oldMaxLength: 256,
oldNullable: true);
migrationBuilder.AddColumn<Guid>(
name: "client_id",
table: "auth_challenges",
type: "uuid",
nullable: true);
migrationBuilder.CreateTable(
name: "auth_clients",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
device_name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
device_label = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
device_id = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_auth_clients", x => x.id);
table.ForeignKey(
name: "fk_auth_clients_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "ix_auth_challenges_client_id",
table: "auth_challenges",
column: "client_id");
migrationBuilder.CreateIndex(
name: "ix_auth_clients_account_id",
table: "auth_clients",
column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_auth_clients_device_id",
table: "auth_clients",
column: "device_id",
unique: true);
migrationBuilder.AddForeignKey(
name: "fk_auth_challenges_auth_clients_client_id",
table: "auth_challenges",
column: "client_id",
principalTable: "auth_clients",
principalColumn: "id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "fk_auth_challenges_auth_clients_client_id",
table: "auth_challenges");
migrationBuilder.DropTable(
name: "auth_clients");
migrationBuilder.DropIndex(
name: "ix_auth_challenges_client_id",
table: "auth_challenges");
migrationBuilder.DropColumn(
name: "client_id",
table: "auth_challenges");
migrationBuilder.AlterColumn<string>(
name: "device_id",
table: "auth_challenges",
type: "character varying(256)",
maxLength: 256,
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(1024)",
oldMaxLength: 1024,
oldNullable: true);
}
}
}

View File

@@ -435,7 +435,7 @@ namespace DysonNetwork.Pass.Migrations
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("last_seen_at"); .HasColumnName("last_seen_at");
b.Property<Dictionary<string, string>>("Links") b.Property<List<ProfileLink>>("Links")
.HasColumnType("jsonb") .HasColumnType("jsonb")
.HasColumnName("links"); .HasColumnName("links");
@@ -817,6 +817,10 @@ namespace DysonNetwork.Pass.Migrations
.HasColumnType("jsonb") .HasColumnType("jsonb")
.HasColumnName("blacklist_factors"); .HasColumnName("blacklist_factors");
b.Property<Guid?>("ClientId")
.HasColumnType("uuid")
.HasColumnName("client_id");
b.Property<Instant>("CreatedAt") b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("created_at"); .HasColumnName("created_at");
@@ -826,8 +830,8 @@ namespace DysonNetwork.Pass.Migrations
.HasColumnName("deleted_at"); .HasColumnName("deleted_at");
b.Property<string>("DeviceId") b.Property<string>("DeviceId")
.HasMaxLength(256) .HasMaxLength(1024)
.HasColumnType("character varying(256)") .HasColumnType("character varying(1024)")
.HasColumnName("device_id"); .HasColumnName("device_id");
b.Property<Instant?>("ExpiredAt") b.Property<Instant?>("ExpiredAt")
@@ -888,9 +892,65 @@ namespace DysonNetwork.Pass.Migrations
b.HasIndex("AccountId") b.HasIndex("AccountId")
.HasDatabaseName("ix_auth_challenges_account_id"); .HasDatabaseName("ix_auth_challenges_account_id");
b.HasIndex("ClientId")
.HasDatabaseName("ix_auth_challenges_client_id");
b.ToTable("auth_challenges", (string)null); b.ToTable("auth_challenges", (string)null);
}); });
modelBuilder.Entity("DysonNetwork.Pass.Auth.AuthClient", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Guid>("AccountId")
.HasColumnType("uuid")
.HasColumnName("account_id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<string>("DeviceId")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("device_id");
b.Property<string>("DeviceLabel")
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("device_label");
b.Property<string>("DeviceName")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("device_name");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_auth_clients");
b.HasIndex("AccountId")
.HasDatabaseName("ix_auth_clients_account_id");
b.HasIndex("DeviceId")
.IsUnique()
.HasDatabaseName("ix_auth_clients_device_id");
b.ToTable("auth_clients", (string)null);
});
modelBuilder.Entity("DysonNetwork.Pass.Auth.AuthSession", b => modelBuilder.Entity("DysonNetwork.Pass.Auth.AuthSession", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@@ -1586,6 +1646,25 @@ namespace DysonNetwork.Pass.Migrations
.IsRequired() .IsRequired()
.HasConstraintName("fk_auth_challenges_accounts_account_id"); .HasConstraintName("fk_auth_challenges_accounts_account_id");
b.HasOne("DysonNetwork.Pass.Auth.AuthClient", "Client")
.WithMany()
.HasForeignKey("ClientId")
.HasConstraintName("fk_auth_challenges_auth_clients_client_id");
b.Navigation("Account");
b.Navigation("Client");
});
modelBuilder.Entity("DysonNetwork.Pass.Auth.AuthClient", b =>
{
b.HasOne("DysonNetwork.Pass.Account.Account", "Account")
.WithMany()
.HasForeignKey("AccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_auth_clients_accounts_account_id");
b.Navigation("Account"); b.Navigation("Account");
}); });

View File

@@ -37,6 +37,7 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEnforcerExtension_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fbb4a120e56464fc6abd8c30969ef70864ba00_003Fb5_003F180850e0_003FEnforcerExtension_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEnforcerExtension_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fbb4a120e56464fc6abd8c30969ef70864ba00_003Fb5_003F180850e0_003FEnforcerExtension_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEnforcer_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fbb4a120e56464fc6abd8c30969ef70864ba00_003F47_003F3a6b6c4b_003FEnforcer_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEnforcer_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fbb4a120e56464fc6abd8c30969ef70864ba00_003F47_003F3a6b6c4b_003FEnforcer_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEntityFrameworkQueryableExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fe096e6f12c5d6b49356bc34ff1ea08738f910c0929c9d717c9cba7f44288_003FEntityFrameworkQueryableExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEntityFrameworkQueryableExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fe096e6f12c5d6b49356bc34ff1ea08738f910c0929c9d717c9cba7f44288_003FEntityFrameworkQueryableExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEntityFrameworkQueryableExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F363c1765261146f1a68840a2d3ce7e39291438_003F2a_003F960244de_003FEntityFrameworkQueryableExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEntityFrameworkQueryableExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fcb0587797ea44bd6915ede69888c6766291038_003F55_003F277f2d4c_003FEntityFrameworkQueryableExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEntityFrameworkQueryableExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fcb0587797ea44bd6915ede69888c6766291038_003F55_003F277f2d4c_003FEntityFrameworkQueryableExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEntityFrameworkServiceCollectionExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F4a28847852ee9ba45fd3107526c0a749a733bd4f4ebf33aa3c9a59737a3f758_003FEntityFrameworkServiceCollectionExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEntityFrameworkServiceCollectionExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F4a28847852ee9ba45fd3107526c0a749a733bd4f4ebf33aa3c9a59737a3f758_003FEntityFrameworkServiceCollectionExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEnumerable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F832399abc13b45b6bdbabfa022e4a28487e00_003F7f_003F7aece4dd_003FEnumerable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEnumerable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F832399abc13b45b6bdbabfa022e4a28487e00_003F7f_003F7aece4dd_003FEnumerable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>