Skip to content

Commit 343c433

Browse files
committed
update cqrs with read and write db
1 parent ec7c6a7 commit 343c433

File tree

46 files changed

+1072
-277
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1072
-277
lines changed

docker-compose.yml

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,29 @@ services:
2929
- "5004:8080"
3030
- "5005:8081"
3131

32-
evently.db:
32+
evently.write.db:
3333
image: postgres:17.5
34-
container_name: evently.database
34+
container_name: evently.write.database
3535
environment:
3636
- POSTGRES_USER=postgres
3737
- POSTGRES_PASSWORD=postgres
3838
- POSTGRES_DB=evently
3939
volumes:
40-
- ./.containers/database:/var/lib/postgresql/data
40+
- ./.containers/database/write:/var/lib/postgresql/data
4141
ports:
4242
- "5432:5432"
4343

44+
evently.read.db:
45+
image: mongo:8.2
46+
container_name: evently.read.database
47+
environment:
48+
- MONGO_INITDB_ROOT_USERNAME=admin
49+
- MONGO_INITDB_ROOT_PASSWORD=admin
50+
volumes:
51+
- ./.containers/database/read:/data/db
52+
ports:
53+
- "27017:27017"
54+
4455
evently.idp:
4556
image: quay.io/keycloak/keycloak:26.2.4
4657
container_name: evently.identity

launchSettings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
"commandName": "DockerCompose",
55
"commandVersion": "1.0",
66
"serviceActions": {
7+
"evently.gateway": "StartDebugging",
78
"evently.api": "StartDebugging",
8-
"evently.gateway": "StartDebugging"
9+
"evently.ticketing.api": "StartDebugging"
910
}
1011
}
1112
}

src/API/Evently.Api/Program.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Evently.Modules.Users.Infrastructure;
1010
using HealthChecks.UI.Client;
1111
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
12+
using MongoDB.Driver;
1213
using RabbitMQ.Client;
1314
using Scalar.AspNetCore;
1415
using Serilog;
@@ -35,7 +36,8 @@
3536

3637
builder.Services
3738
.AddHealthChecks()
38-
.AddNpgSql(builder.Configuration.GetConnectionStringOrThrow("Database"))
39+
.AddNpgSql(builder.Configuration.GetConnectionStringOrThrow("WriteDatabase"))
40+
.AddMongoDb(_ => new MongoClient(builder.Configuration.GetConnectionStringOrThrow("ReadDatabase")))
3941
.AddRedis(builder.Configuration.GetConnectionStringOrThrow("Cache"))
4042
.AddRabbitMQ(_ => new ConnectionFactory
4143
{

src/API/Evently.Api/appsettings.Development.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"ConnectionStrings": {
3-
"Database": "Host=evently.db;Port=5432;Database=evently;Username=postgres;Password=postgres;Include Error Detail=true;",
3+
"WriteDatabase": "Host=evently.write.db;Port=5432;Database=evently;Username=postgres;Password=postgres;Include Error Detail=true;",
4+
"ReadDatabase": "mongodb://admin:admin@evently.read.database:27017",
45
"Cache": "evently.cache:6379",
56
"Queue": "amqp://evently-queue:5672"
67
},

src/API/Evently.Ticketing.Api/Program.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Evently.Ticketing.Api.OpenTelemetry;
88
using HealthChecks.UI.Client;
99
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
10+
using MongoDB.Driver;
1011
using RabbitMQ.Client;
1112
using Scalar.AspNetCore;
1213
using Serilog;
@@ -31,7 +32,8 @@
3132

3233
builder.Services
3334
.AddHealthChecks()
34-
.AddNpgSql(builder.Configuration.GetConnectionStringOrThrow("Database"))
35+
.AddNpgSql(builder.Configuration.GetConnectionStringOrThrow("WriteDatabase"))
36+
.AddMongoDb(_ => new MongoClient(builder.Configuration.GetConnectionStringOrThrow("ReadDatabase")))
3537
.AddRedis(builder.Configuration.GetConnectionStringOrThrow("Cache"))
3638
.AddRabbitMQ(_ => new ConnectionFactory
3739
{

src/API/Evently.Ticketing.Api/appsettings.Development.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"ConnectionStrings": {
3-
"Database": "Host=evently.db;Port=5432;Database=evently_ticketing;Username=postgres;Password=postgres;Include Error Detail=true;",
3+
"WriteDatabase": "Host=evently.write.db;Port=5432;Database=evently_ticketing;Username=postgres;Password=postgres;Include Error Detail=true;",
4+
"ReadDatabase": "mongodb://admin:admin@evently.read.database:27017",
45
"Cache": "evently.cache:6379",
56
"Queue": "amqp://evently-queue:5672"
67
},
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
using System.Linq.Expressions;
2+
using System.Reflection;
3+
using MongoDB.Bson.Serialization;
4+
using MongoDB.Driver;
5+
6+
namespace Evently.Common.Infrastructure.Data;
7+
8+
public class DocumentDataBuilder<T>
9+
{
10+
public Action<BsonClassMap<T>> MappingAction { get; private set; } = _ => { };
11+
12+
public string CollectionName { get; private set; }
13+
public List<CreateIndexModel<T>> Indexes { get; } = [];
14+
15+
#pragma warning disable S4487
16+
#pragma warning disable CS0414
17+
private bool _useAutoMap = true;
18+
#pragma warning restore CS0414
19+
#pragma warning restore S4487
20+
21+
public DocumentDataBuilder()
22+
{
23+
ResetToAutoMap();
24+
}
25+
26+
/// <summary>
27+
/// Set the name of the collection where the documents of type <typeparamref name="T"/> will be stored.
28+
/// </summary>
29+
/// <param name="name">The name of the collection.</param>
30+
public DocumentDataBuilder<T> ToCollection(string name)
31+
{
32+
if (string.IsNullOrWhiteSpace(name))
33+
{
34+
throw new ArgumentNullException(nameof(name));
35+
}
36+
37+
CollectionName = name;
38+
return this;
39+
}
40+
41+
/// <summary>
42+
/// Disable the automatic mapping of the class properties to MongoDB fields.
43+
/// This will prevent the automatic mapping of properties to MongoDB fields.
44+
/// Use this when you want to manually configure the mapping.
45+
/// </summary>
46+
/// <returns>The <see cref="DocumentDataBuilder{T}"/> instance.</returns>
47+
public DocumentDataBuilder<T> DisableAutoMap()
48+
{
49+
_useAutoMap = false;
50+
MappingAction = _ => { };
51+
return this;
52+
}
53+
54+
/// <summary>
55+
/// Configure the mapping of the class properties to MongoDB fields.
56+
/// Use this when you want to manually configure the mapping.
57+
/// </summary>
58+
/// <param name="configure">An action that takes a <see cref="BsonClassMap{T}"/> instance and configures the mapping.</param>
59+
/// <returns>The <see cref="DocumentDataBuilder{T}"/> instance.</returns>
60+
public DocumentDataBuilder<T> ConfigureMapping(Action<BsonClassMap<T>> configure)
61+
{
62+
ArgumentNullException.ThrowIfNull(configure);
63+
MappingAction += configure;
64+
return this;
65+
}
66+
67+
/// <summary>
68+
/// Configure the mapping of the <paramref name="idExpr"/> property to the MongoDB "_id" field.
69+
/// Use this when you want to manually configure the mapping of the Id property.
70+
/// </summary>
71+
/// <param name="idExpr">An expression that identifies the Id property.</param>
72+
/// <returns>The <see cref="DocumentDataBuilder{T}"/> instance.</returns>
73+
public DocumentDataBuilder<T> MapId(Expression<Func<T, object>> idExpr)
74+
{
75+
return ConfigureMapping(cm =>
76+
{
77+
MemberInfo member = ReflectionHelper.GetMemberInfo(idExpr);
78+
cm.MapIdMember(member);
79+
});
80+
}
81+
82+
/// <summary>
83+
/// Configure the mapping of the <paramref name="propExpr"/> property.
84+
/// Use this when you want to manually configure the mapping of a property.
85+
/// </summary>
86+
/// <param name="propExpr">An expression that identifies the property.</param>
87+
/// <param name="configure">An action that takes a <see cref="BsonMemberMap"/> instance and configures the mapping of the property.</param>
88+
/// <returns>The <see cref="DocumentDataBuilder{T}"/> instance.</returns>
89+
public DocumentDataBuilder<T> MapProperty(Expression<Func<T, object>> propExpr, Action<BsonMemberMap>? configure = null)
90+
{
91+
return ConfigureMapping(cm =>
92+
{
93+
MemberInfo member = ReflectionHelper.GetMemberInfo(propExpr);
94+
BsonMemberMap? mm = cm.MapMember(member);
95+
configure?.Invoke(mm);
96+
});
97+
}
98+
99+
/// <summary>
100+
/// Configure the mapping to ignore the specified property.
101+
/// Use this when you want to explicitly ignore a property.
102+
/// </summary>
103+
/// <param name="propExpr">An expression that identifies the property.</param>
104+
/// <returns>The <see cref="DocumentDataBuilder{T}"/> instance.</returns>
105+
/// <exception cref="InvalidOperationException">Thrown when the property is "Id" or "_id".</exception>
106+
public DocumentDataBuilder<T> Ignore(Expression<Func<T, object>> propExpr)
107+
{
108+
return ConfigureMapping(cm =>
109+
{
110+
MemberInfo member = ReflectionHelper.GetMemberInfo(propExpr);
111+
if (member.Name is "Id" or "_id")
112+
{
113+
throw new InvalidOperationException("Cannot ignore Id member.");
114+
}
115+
cm.UnmapMember(member);
116+
});
117+
}
118+
119+
120+
/// <summary>
121+
/// Create an ascending index on the specified property.
122+
/// Use this when you want to create an index on a property.
123+
/// </summary>
124+
/// <param name="keyExpr">An expression that identifies the property.</param>
125+
/// <param name="options">Optional index options.</param>
126+
/// <returns>The <see cref="DocumentDataBuilder{T}"/> instance.</returns>
127+
public DocumentDataBuilder<T> IndexAscending(Expression<Func<T, object>> keyExpr, CreateIndexOptions? options = null)
128+
{
129+
IndexKeysDefinition<T>? key = Builders<T>.IndexKeys.Ascending(keyExpr);
130+
Indexes.Add(new CreateIndexModel<T>(key, options));
131+
return this;
132+
}
133+
134+
/// <summary>
135+
/// Create a descending index on the specified property.
136+
/// Use this when you want to create an index on a property.
137+
/// </summary>
138+
/// <param name="keyExpr">An expression that identifies the property.</param>
139+
/// <param name="options">Optional index options.</param>
140+
/// <returns>The <see cref="DocumentDataBuilder{T}"/> instance.</returns>
141+
public DocumentDataBuilder<T> IndexDescending(Expression<Func<T, object>> keyExpr, CreateIndexOptions? options = null)
142+
{
143+
IndexKeysDefinition<T>? key = Builders<T>.IndexKeys.Descending(keyExpr);
144+
Indexes.Add(new CreateIndexModel<T>(key, options));
145+
return this;
146+
}
147+
148+
/// <summary>
149+
/// Create a compound index on the specified keys.
150+
/// Use this when you want to create a compound index on multiple properties.
151+
/// </summary>
152+
/// <param name="keys">An object that contains the indexed properties.</param>
153+
/// <param name="options">Optional index options.</param>
154+
/// <returns>The <see cref="DocumentDataBuilder{T}"/> instance.</returns>
155+
public DocumentDataBuilder<T> IndexCompound(IndexKeysDefinition<T> keys, CreateIndexOptions? options = null)
156+
{
157+
Indexes.Add(new CreateIndexModel<T>(keys, options));
158+
return this;
159+
}
160+
161+
162+
private void ResetToAutoMap()
163+
{
164+
_useAutoMap = true;
165+
MappingAction = cm => cm.AutoMap();
166+
}
167+
168+
private static class ReflectionHelper
169+
{
170+
public static MemberInfo GetMemberInfo<TObj>(Expression<Func<TObj, object>> expr)
171+
{
172+
Expression body = expr.Body;
173+
174+
if (body is UnaryExpression { NodeType: ExpressionType.Convert } unary)
175+
{
176+
body = unary.Operand;
177+
}
178+
179+
if (body is MemberExpression memberExpr)
180+
{
181+
switch (memberExpr.Member.MemberType)
182+
{
183+
case MemberTypes.Property:
184+
return memberExpr.Member;
185+
case MemberTypes.Field:
186+
return memberExpr.Member;
187+
}
188+
}
189+
190+
throw new ArgumentException($"Expression '{expr}' must resolve to a Property or Field access.", nameof(expr));
191+
}
192+
}
193+
}

0 commit comments

Comments
 (0)