Skip to content

Commit 2c9368a

Browse files
postgresql lib
1 parent 6fa1390 commit 2c9368a

10 files changed

Lines changed: 323 additions & 0 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"version": 1,
3+
"isRoot": true,
4+
"tools": {
5+
"dotnet-ef": {
6+
"version": "8.0.0-preview.5.23280.1",
7+
"commands": [
8+
"dotnet-ef"
9+
]
10+
}
11+
}
12+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<PackageId>CNinnovation.Codebreaker.PostgreSQL</PackageId>
5+
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<LangVersion>latest</LangVersion>
8+
<Nullable>enable</Nullable>
9+
<PackageTags>
10+
Codebreaker;CNinnovation;PostgreSQL
11+
</PackageTags>
12+
<Description>
13+
This library contains PostgreSQL data access for the Codebreaker backend services.
14+
See https://github.com/codebreakerapp for more information on the complete solution.
15+
</Description>
16+
<PackageReadmeFile>readme.md</PackageReadmeFile>
17+
<PackageIcon>codebreaker.jpeg</PackageIcon>
18+
<Version>3.8.0</Version>
19+
</PropertyGroup>
20+
21+
<ItemGroup>
22+
<PackageReference Include="CNinnovation.Codebreaker.BackendModels" Version="3.8.0" />
23+
</ItemGroup>
24+
25+
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
26+
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.11" />
27+
</ItemGroup>
28+
29+
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
30+
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
31+
</ItemGroup>
32+
33+
<ItemGroup>
34+
<None Include="docs/readme.md" Pack="true" PackagePath="\" />
35+
<None Include="Images/codebreaker.jpeg" Pack="true" PackagePath="\" />
36+
</ItemGroup>
37+
38+
</Project>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using Microsoft.EntityFrameworkCore.ChangeTracking;
2+
3+
namespace Codebreaker.Data.Postgres;
4+
5+
internal class GameConfiguration : IEntityTypeConfiguration<Game>
6+
{
7+
public void Configure(EntityTypeBuilder<Game> builder)
8+
{
9+
builder.HasKey(g => g.Id);
10+
11+
builder.HasMany(g => g.Moves)
12+
.WithOne()
13+
.HasForeignKey("GameId");
14+
15+
builder.Property(g => g.GameType).HasMaxLength(20);
16+
builder.Property(g => g.PlayerName).HasMaxLength(60);
17+
18+
builder.Property(g => g.Codes).HasMaxLength(120);
19+
20+
builder.Property(g => g.StartTime)
21+
.HasConversion(
22+
v => v.ToUniversalTime(),
23+
v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
24+
25+
builder.Property(g => g.EndTime)
26+
.HasConversion(
27+
v => v.HasValue ? v.Value.ToUniversalTime() : v,
28+
v => v.HasValue ? DateTime.SpecifyKind(v.Value, DateTimeKind.Utc) : v);
29+
30+
31+
builder.Property(g => g.FieldValues)
32+
.HasColumnName("Fields")
33+
.HasConversion(convertToProviderExpression: fields => fields.ToFieldsString(),
34+
convertFromProviderExpression: fields => fields.FromFieldsString(),
35+
valueComparer: new ValueComparer<IDictionary<string, IEnumerable<string>>>(
36+
equalsExpression: (a, b) => a!.SequenceEqual(b!),
37+
hashCodeExpression: a => a.Aggregate(0, (result, next) => HashCode.Combine(result, next.GetHashCode())),
38+
snapshotExpression: a => a.ToDictionary(kv => kv.Key, kv => kv.Value)));
39+
}
40+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace Codebreaker.Data.Postgres;
2+
3+
internal class MoveConfiguration : IEntityTypeConfiguration<Move>
4+
{
5+
public void Configure(EntityTypeBuilder<Move> builder)
6+
{
7+
// shadow property for the foreign key
8+
builder.Property<Guid>("GameId");
9+
10+
builder.Property(g => g.GuessPegs).HasMaxLength(120);
11+
builder.Property(g => g.KeyPegs).HasMaxLength(60);
12+
}
13+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<Project>
2+
<PropertyGroup>
3+
<!-- Enable central package management, https://learn.microsoft.com/en-us/nuget/consume-packages/Central-Package-Management -->
4+
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
5+
</PropertyGroup>
6+
<ItemGroup>
7+
</ItemGroup>
8+
</Project>
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
using Codebreaker.GameAPIs.Data;
2+
3+
namespace Codebreaker.Data.Postgres;
4+
5+
public class GamesPostgresContext(DbContextOptions<GamesPostgresContext> options) : DbContext(options), IGamesRepository
6+
{
7+
protected override void OnModelCreating(ModelBuilder modelBuilder)
8+
{
9+
modelBuilder.HasDefaultSchema("codebreaker");
10+
modelBuilder.ApplyConfiguration(new GameConfiguration());
11+
modelBuilder.ApplyConfiguration(new MoveConfiguration());
12+
}
13+
14+
public DbSet<Game> Games => Set<Game>();
15+
public DbSet<Move> Moves => Set<Move>();
16+
17+
public async Task AddGameAsync(Game game, CancellationToken cancellationToken = default)
18+
{
19+
Games.Add(game);
20+
await SaveChangesAsync(cancellationToken);
21+
}
22+
23+
public async Task AddMoveAsync(Game game, Move move, CancellationToken cancellationToken = default)
24+
{
25+
Moves.Add(move);
26+
Games.Update(game);
27+
28+
await SaveChangesAsync(cancellationToken);
29+
}
30+
31+
public async Task<bool> DeleteGameAsync(Guid id, CancellationToken cancellationToken = default)
32+
{
33+
var affected = await Games
34+
.Where(g => g.Id == id)
35+
.ExecuteDeleteAsync(cancellationToken);
36+
return affected == 1;
37+
}
38+
39+
public async Task<Game?> GetGameAsync(Guid id, CancellationToken cancellationToken = default)
40+
{
41+
var game = await Games
42+
.Include("Moves")
43+
.TagWith(nameof(GetGameAsync))
44+
.SingleOrDefaultAsync(g => g.Id == id, cancellationToken);
45+
return game;
46+
}
47+
48+
public async Task<IEnumerable<Game>> GetGamesByDateAsync(string gameType, DateOnly date, CancellationToken cancellationToken = default)
49+
{
50+
var d = date.ToDateTime(TimeOnly.MinValue);
51+
var games = await Games
52+
.Where(g => g.GameType == gameType && g.StartTime.Date == d)
53+
.TagWith(nameof(GetGamesByDateAsync))
54+
.ToListAsync(cancellationToken);
55+
return games;
56+
}
57+
58+
public async Task<IEnumerable<Game>> GetGamesByPlayerAsync(string playerName, CancellationToken cancellationToken = default)
59+
{
60+
var games = await Games
61+
.Where(g => g.PlayerName == playerName)
62+
.TagWith(nameof(GetGamesByPlayerAsync))
63+
.ToListAsync(cancellationToken);
64+
return games;
65+
}
66+
67+
public async Task<IEnumerable<Game>> GetRunningGamesByPlayerAsync(string playerName, CancellationToken cancellationToken = default)
68+
{
69+
var games = await Games
70+
.Where(g => g.PlayerName == playerName && g.EndTime == null)
71+
.ToListAsync(cancellationToken);
72+
return games;
73+
}
74+
75+
private const int MaxGamesReturned = 500;
76+
77+
public async Task<IEnumerable<Game>> GetGamesAsync(GamesQuery gamesQuery, CancellationToken cancellationToken = default)
78+
{
79+
IQueryable<Game> query = Games
80+
.TagWith(nameof(GetGamesAsync))
81+
.Include(g => g.Moves);
82+
83+
// Apply Game filters if provided.
84+
if (gamesQuery.Date.HasValue)
85+
{
86+
DateTime begin = gamesQuery.Date.Value.ToDateTime(TimeOnly.MinValue);
87+
DateTime end = begin.AddDays(1);
88+
query = query.Where(g => g.StartTime < end && g.StartTime > begin);
89+
}
90+
if (gamesQuery.PlayerName != null)
91+
{
92+
query = query.Where(g => g.PlayerName == gamesQuery.PlayerName);
93+
}
94+
if (gamesQuery.GameType != null)
95+
{
96+
query = query.Where(g => g.GameType == gamesQuery.GameType);
97+
}
98+
if (gamesQuery.RunningOnly)
99+
{
100+
query = query.Where(g => g.EndTime == null);
101+
}
102+
if (gamesQuery.Ended)
103+
{
104+
query = query.Where(g => g.EndTime != null)
105+
.OrderBy(g => g.Duration);
106+
}
107+
else
108+
{
109+
query = query.OrderByDescending(g => g.StartTime);
110+
}
111+
112+
query = query.Take(MaxGamesReturned);
113+
114+
return await query.ToListAsync(cancellationToken);
115+
}
116+
117+
public async Task<Game> UpdateGameAsync(Game game, CancellationToken cancellationToken = default)
118+
{
119+
Games.Update(game);
120+
await SaveChangesAsync(cancellationToken);
121+
return game;
122+
}
123+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
global using Codebreaker.GameAPIs.Models;
2+
global using Microsoft.EntityFrameworkCore;
3+
global using Microsoft.EntityFrameworkCore.Metadata.Builders;
11.4 KB
Loading
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
namespace Codebreaker.Data.Postgres;
2+
3+
public static class MappingExtensions
4+
{
5+
public static ICollection<T> ToFieldCollection<T>(this string fields)
6+
where T : IParsable<T>
7+
{
8+
return fields.Split('#')
9+
.Select(field => T.Parse(field, default))
10+
.ToList();
11+
}
12+
13+
public static string ToFieldString<T>(this IEnumerable<T> fields)
14+
where T : notnull
15+
{
16+
return string.Join('#', fields.Select(field => field.ToString()));
17+
}
18+
19+
public static string ToFieldString<T>(this T[] fields)
20+
where T : notnull
21+
{
22+
return string.Join('#', fields.Select(field => field.ToString()));
23+
}
24+
25+
public static T[] ToFieldArray<T>(this string fields)
26+
where T : IParsable<T>
27+
{
28+
return fields.Split('#')
29+
.Select(field => T.Parse(field, default))
30+
.ToArray();
31+
}
32+
33+
public static string ToFieldsString(this IDictionary<string, IEnumerable<string>> fields)
34+
{
35+
return string.Join(
36+
'#', fields.SelectMany(
37+
key => key.Value
38+
.Select(value => $"{key.Key}:{value}")));
39+
}
40+
41+
public static IDictionary<string, IEnumerable<string>> FromFieldsString(this string fieldsString)
42+
{
43+
Dictionary<string, List<string>> fields = [];
44+
45+
foreach (string pair in fieldsString.Split('#'))
46+
{
47+
int index = pair.IndexOf(':');
48+
49+
if (index < 0)
50+
{
51+
throw new ArgumentException($"Field {pair} does not contain ':' delimiter.");
52+
}
53+
54+
string key = pair[..index];
55+
string value = pair[(index + 1)..];
56+
57+
if (!fields.TryGetValue(key, out List<string>? list))
58+
{
59+
list = [];
60+
fields[key] = list;
61+
}
62+
63+
list.Add(value);
64+
}
65+
66+
return fields.ToDictionary(
67+
pair => pair.Key,
68+
pair => (IEnumerable<string>)pair.Value);
69+
}
70+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# CNinnovation.Codebreaker.PostgreSQL
2+
3+
This library contains the data backend for Codebreaker for PostgreSQL using EF Core.
4+
5+
See https://github.com/codebreakerapp for more information on the complete solution.
6+
7+
See [Codebreakerlight](https://github.com/codebreakerapp/codebreakerlight) for a simple version of the Codebreaker solution with a Wiki to create your own Codebreaker service.
8+
9+
## Types available in this package
10+
11+
12+
| Type | Description |
13+
| --- | --- |
14+
| `GamesPostgresContext` | This class implements `IGamesRepository` |
15+
16+
Configure this class to be injected for `IGamesRepository` in your DI container when Codebreaker games data should be stored in PosgreSQL.

0 commit comments

Comments
 (0)