diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index cd967fc..0000000 --- a/.dockerignore +++ /dev/null @@ -1,25 +0,0 @@ -**/.dockerignore -**/.env -**/.git -**/.gitignore -**/.project -**/.settings -**/.toolstarget -**/.vs -**/.vscode -**/.idea -**/*.*proj.user -**/*.dbmdl -**/*.jfm -**/azds.yaml -**/bin -**/charts -**/docker-compose* -**/Dockerfile* -**/node_modules -**/npm-debug.log -**/obj -**/secrets.dev.yaml -**/values.dev.yaml -LICENSE -README.md \ No newline at end of file diff --git a/StudentEnrollmentConsoleApp.sln b/StudentEnrollmentConsoleApp.sln index a339137..09d4272 100644 --- a/StudentEnrollmentConsoleApp.sln +++ b/StudentEnrollmentConsoleApp.sln @@ -6,7 +6,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ProjectSection(SolutionItems) = preProject docker-compose.yml = docker-compose.yml .gitignore = .gitignore - .dockerignore = .dockerignore global.json = global.json README.md = README.md EndProjectSection diff --git a/StudentEnrollmentConsoleApp/Dockerfile b/StudentEnrollmentConsoleApp/Dockerfile deleted file mode 100644 index cc5e694..0000000 --- a/StudentEnrollmentConsoleApp/Dockerfile +++ /dev/null @@ -1,21 +0,0 @@ -FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base -USER $APP_UID -WORKDIR /app - -FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build -ARG BUILD_CONFIGURATION=Release -WORKDIR /src -COPY ["StudentEnrollmentConsoleApp/StudentEnrollmentConsoleApp.csproj", "StudentEnrollmentConsoleApp/"] -RUN dotnet restore "StudentEnrollmentConsoleApp/StudentEnrollmentConsoleApp.csproj" -COPY . . -WORKDIR "/src/StudentEnrollmentConsoleApp" -RUN dotnet build "StudentEnrollmentConsoleApp.csproj" -c $BUILD_CONFIGURATION -o /app/build - -FROM build AS publish -ARG BUILD_CONFIGURATION=Release -RUN dotnet publish "StudentEnrollmentConsoleApp.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false - -FROM base AS final -WORKDIR /app -COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "StudentEnrollmentConsoleApp.dll"] diff --git a/StudentEnrollmentConsoleApp/Events/Event.cs b/StudentEnrollmentConsoleApp/Events/Event.cs index 93326e5..c46e770 100644 --- a/StudentEnrollmentConsoleApp/Events/Event.cs +++ b/StudentEnrollmentConsoleApp/Events/Event.cs @@ -1,8 +1,6 @@ namespace StudentEnrollmentConsoleApp.Events; -public abstract class Event +public abstract record Event { - public abstract string StreamId { get; } - - public DateTime CreatedAtUtc { get; set; } + public string Id { get; init; } = default!; } \ No newline at end of file diff --git a/StudentEnrollmentConsoleApp/Events/EventTypeMapper.cs b/StudentEnrollmentConsoleApp/Events/EventTypeMapper.cs new file mode 100644 index 0000000..2eaecbd --- /dev/null +++ b/StudentEnrollmentConsoleApp/Events/EventTypeMapper.cs @@ -0,0 +1,35 @@ +using System.Collections.Concurrent; + +namespace StudentEnrollmentConsoleApp.Events; + +public class EventTypeMapper +{ + public static readonly EventTypeMapper Instance = new(); + + private readonly ConcurrentDictionary _typeMap = new(); + private readonly ConcurrentDictionary _typeNameMap = new(); + + public string ToName() => ToName(typeof(TEventType)); + + public string ToName(Type eventType) => _typeNameMap.GetOrAdd(eventType, _ => + { + var eventTypeName = eventType.FullName!; + _typeMap.TryAdd(eventTypeName, eventType); + return eventTypeName; + }); + + public Type? ToType(string eventTypeName) => _typeMap.GetOrAdd(eventTypeName, _ => + { + var type = AppDomain.CurrentDomain + .GetAssemblies() + .SelectMany(a => a.GetTypes().Where(x => x.FullName == eventTypeName || x.Name == eventTypeName)) + .FirstOrDefault(); + + if (type == null) + return null; + + _typeNameMap.TryAdd(type, eventTypeName); + + return type; + }); +} \ No newline at end of file diff --git a/StudentEnrollmentConsoleApp/Events/StudentCreated.cs b/StudentEnrollmentConsoleApp/Events/StudentCreated.cs index cd410da..f196749 100644 --- a/StudentEnrollmentConsoleApp/Events/StudentCreated.cs +++ b/StudentEnrollmentConsoleApp/Events/StudentCreated.cs @@ -1,11 +1,9 @@ namespace StudentEnrollmentConsoleApp.Events; -public class StudentCreated : Event +public record StudentCreated : Event { - public required string StudentId { get; init; } public required string FullName { get; init; } public required string Email { get; init; } public required DateTime DateOfBirth { get; init; } - - public override string StreamId => StudentId; + public DateTime CreatedAtUtc { get; init; } } \ No newline at end of file diff --git a/StudentEnrollmentConsoleApp/Events/StudentEmailChanged.cs b/StudentEnrollmentConsoleApp/Events/StudentEmailChanged.cs new file mode 100644 index 0000000..686878d --- /dev/null +++ b/StudentEnrollmentConsoleApp/Events/StudentEmailChanged.cs @@ -0,0 +1,7 @@ +namespace StudentEnrollmentConsoleApp.Events; + +public record StudentEmailChanged : Event +{ + public required string Email { get; init; } + public DateTime ChangedAtUtc { get; init; } +} \ No newline at end of file diff --git a/StudentEnrollmentConsoleApp/Events/StudentEnrolled.cs b/StudentEnrollmentConsoleApp/Events/StudentEnrolled.cs index faee8d3..aecbf81 100644 --- a/StudentEnrollmentConsoleApp/Events/StudentEnrolled.cs +++ b/StudentEnrollmentConsoleApp/Events/StudentEnrolled.cs @@ -1,9 +1,7 @@ namespace StudentEnrollmentConsoleApp.Events; -public class StudentEnrolled : Event +public record StudentEnrolled : Event { - public required string StudentId { get; init; } public required string CourseName { get; init; } - - public override string StreamId => StudentId; + public DateTime EnrolledAtUtc { get; init; } } \ No newline at end of file diff --git a/StudentEnrollmentConsoleApp/Events/StudentUnEnrolled.cs b/StudentEnrollmentConsoleApp/Events/StudentUnEnrolled.cs deleted file mode 100644 index 07bb6e8..0000000 --- a/StudentEnrollmentConsoleApp/Events/StudentUnEnrolled.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace StudentEnrollmentConsoleApp.Events; - -public class StudentUnEnrolled : Event -{ - public required string StudentId { get; init; } - public required string CourseName { get; init; } - - public override string StreamId => StudentId; -} \ No newline at end of file diff --git a/StudentEnrollmentConsoleApp/Events/StudentUpdated.cs b/StudentEnrollmentConsoleApp/Events/StudentUpdated.cs deleted file mode 100644 index a57ea8c..0000000 --- a/StudentEnrollmentConsoleApp/Events/StudentUpdated.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace StudentEnrollmentConsoleApp.Events; - -public class StudentUpdated : Event -{ - public required string StudentId { get; init; } - public required string FullName { get; init; } - public required string Email { get; init; } - - public override string StreamId => StudentId; -} \ No newline at end of file diff --git a/StudentEnrollmentConsoleApp/Events/StudentWithdrawn.cs b/StudentEnrollmentConsoleApp/Events/StudentWithdrawn.cs new file mode 100644 index 0000000..446b80c --- /dev/null +++ b/StudentEnrollmentConsoleApp/Events/StudentWithdrawn.cs @@ -0,0 +1,7 @@ +namespace StudentEnrollmentConsoleApp.Events; + +public record StudentWithdrawn : Event +{ + public required string CourseName { get; init; } + public DateTime WithdrawnAtUtc { get; init; } +} \ No newline at end of file diff --git a/StudentEnrollmentConsoleApp/Program.cs b/StudentEnrollmentConsoleApp/Program.cs index 34cf624..0644d32 100644 --- a/StudentEnrollmentConsoleApp/Program.cs +++ b/StudentEnrollmentConsoleApp/Program.cs @@ -1,58 +1,107 @@ using System.Text; using System.Text.Json; using EventStore.Client; -using StudentEnrollmentConsoleApp; using StudentEnrollmentConsoleApp.Events; +// Register events to a singleton for ease-of-reference +EventTypeMapper.Instance.ToName(typeof(StudentCreated)); +EventTypeMapper.Instance.ToName(typeof(StudentEnrolled)); +EventTypeMapper.Instance.ToName(typeof(StudentWithdrawn)); +EventTypeMapper.Instance.ToName(typeof(StudentEmailChanged)); + var id = Guid.Parse("a662d446-4920-415e-8c2a-0dd4a6c58908"); -var studentId = $"student-{id}"; -var now = DateTime.Now; +var streamId = $"student-{id}"; -var studentCreated = new StudentCreated -{ - StudentId = studentId, - FullName = "Erik Shafer", - Email = "erik.shafer@eventstore.com", - DateOfBirth = new DateTime(1987, 1, 1) -}; -const string eventType = nameof(studentCreated); //"StudentCreated"; -const string streamName = "some-stream"; +var created = new EventData( + Uuid.NewUuid(), + "StudentCreated", + JsonSerializer.SerializeToUtf8Bytes(new StudentCreated + { + Id = streamId, + FullName = "Erik Shafer", + Email = "erik.shafer@eventstore.com", + DateOfBirth = new DateTime(1987, 1, 1), + CreatedAtUtc = DateTime.UtcNow + }) +); -var inMemoryDb = new StudentDatabase(); -inMemoryDb.Append(studentCreated); +var enrolled = new EventData( + Uuid.NewUuid(), + "StudentEnrolled", + JsonSerializer.SerializeToUtf8Bytes(new StudentEnrolled + { + Id = streamId, + CourseName = "From Zero to Hero: REST APis in .NET", + EnrolledAtUtc = DateTime.UtcNow + }) +); + +var emailChanged = new EventData( + Uuid.NewUuid(), + "StudentEmailChanged", + JsonSerializer.SerializeToUtf8Bytes(new StudentEmailChanged + { + Id = streamId, + Email = "erik.shafer.changed.his.email@eventstore.com", + ChangedAtUtc = DateTime.UtcNow + }) +); +// Our EventStoreDB (ESDB) const string connectionString = "esdb://admin:changeit@localhost:2113?tls=false&tlsVerifyCert=false"; var settings = EventStoreClientSettings.Create(connectionString); var client = new EventStoreClient(settings); -var eventData = new EventData( - Uuid.NewUuid(), - eventType, - JsonSerializer.SerializeToUtf8Bytes(studentCreated) -); - +// Append to ESDB await client.AppendToStreamAsync( - streamName, + streamId, StreamState.Any, - new[] { eventData }, + new[] { created, enrolled, emailChanged }, cancellationToken: default ); -var result = client.ReadStreamAsync( +// Read from ESDB +var readStreamResult = client.ReadStreamAsync( Direction.Forwards, - streamName, + streamId, StreamPosition.Start, cancellationToken: default ); +var eventStream = await readStreamResult.ToListAsync(); -var events = await result.ToListAsync(); +// Write out the events from the stream +Console.WriteLine("Events from selected stream: "); +foreach (var resolved in eventStream) +{ + Console.WriteLine($"\tEventId: {resolved.Event.EventId}"); + Console.WriteLine($"\tEventStreamId: {resolved.Event.EventStreamId}"); + Console.WriteLine($"\tEventType: {resolved.Event.EventType}"); + Console.WriteLine($"\tData: {Encoding.UTF8.GetString(resolved.Event.Data.ToArray())}"); + Console.WriteLine(""); +} -foreach (var @event in events) +// Write out all the courses the student enrolled in +var enrolledCourses = eventStream + .Where(re => re.Event.EventType == "StudentEnrolled") + .Select(re => JsonSerializer.Deserialize(re.Event.Data.ToArray())) + .Select(se => se!.CourseName) + .ToList(); +Console.WriteLine("Courses enrolled in: "); +enrolledCourses.ForEach(ec => Console.WriteLine($"\t- {ec}")); +Console.WriteLine(""); + +// Write out using the mapper +Console.WriteLine("Deserialized events:"); +foreach (var resolved in eventStream) { - Console.WriteLine($"EventType: {@event.Event.EventId}"); - Console.WriteLine($"EventStreamId: {@event.Event.EventStreamId}"); - Console.WriteLine($"EventType: {@event.Event.EventType}"); - var data = Encoding.UTF8.GetString(@event.Event.Data.ToArray()); - Console.WriteLine($"Data: {data}"); - Console.WriteLine("-----"); + var eventType = EventTypeMapper.Instance.ToType(resolved.Event.EventType); + + if (eventType == null) + break; + + var deserializedEvent = JsonSerializer.Deserialize(Encoding.UTF8.GetString(resolved.Event.Data.Span), eventType); + + Console.WriteLine($"\t{deserializedEvent}"); } + +Console.WriteLine(""); \ No newline at end of file diff --git a/StudentEnrollmentConsoleApp/Student.cs b/StudentEnrollmentConsoleApp/Student.cs new file mode 100644 index 0000000..817cfb6 --- /dev/null +++ b/StudentEnrollmentConsoleApp/Student.cs @@ -0,0 +1,57 @@ +using StudentEnrollmentConsoleApp.Events; + +namespace StudentEnrollmentConsoleApp; + +public class Student +{ + public string Id { get; set; } = default!; + public string FullName { get; set; } = default!; + public string Email { get; set; } = default!; + public DateTime DateOfBirth { get; set; } + public DateTime CreatedAtUtc { get; set; } + public List EnrolledCourses { get; set; } = []; + + public void Apply(Event @event) + { + switch (@event) + { + case StudentCreated created: + Apply(created); + break; + case StudentEmailChanged emailChanged: + Apply(emailChanged); + break; + case StudentEnrolled enrolled: + Apply(enrolled); + break; + case StudentWithdrawn withdrawn: + Apply(withdrawn); + break; + } + } + + private void Apply(StudentCreated @event) + { + Id = @event.Id; + FullName = @event.FullName; + Email = @event.Email; + CreatedAtUtc = @event.CreatedAtUtc; + } + + private void Apply(StudentEmailChanged @event) + { + Email = @event.Email; + } + + private void Apply(StudentEnrolled @event) + { + if (EnrolledCourses.Contains(@event.CourseName) is false) + EnrolledCourses.Add(@event.CourseName); + } + + private void Apply(StudentWithdrawn @event) + { + if (EnrolledCourses.Contains(@event.CourseName)) + EnrolledCourses.Add(@event.CourseName); + } +} \ No newline at end of file diff --git a/StudentEnrollmentConsoleApp/StudentDatabase.cs b/StudentEnrollmentConsoleApp/StudentDatabase.cs deleted file mode 100644 index 353b10a..0000000 --- a/StudentEnrollmentConsoleApp/StudentDatabase.cs +++ /dev/null @@ -1,20 +0,0 @@ -using StudentEnrollmentConsoleApp.Events; - -namespace StudentEnrollmentConsoleApp; - -public sealed class StudentDatabase -{ - private readonly Dictionary> _studentEvents = new(); - - public void Append(Event @event) - { - var stream = _studentEvents!.GetValueOrDefault(@event.StreamId, null); - if (stream is null) - { - _studentEvents[@event.StreamId] = new SortedList(); - } - - @event.CreatedAtUtc = DateTime.UtcNow; - _studentEvents[@event.StreamId].Add(@event.CreatedAtUtc, @event); - } -} \ No newline at end of file diff --git a/StudentEnrollmentConsoleApp/StudentEnrollmentConsoleApp.csproj b/StudentEnrollmentConsoleApp/StudentEnrollmentConsoleApp.csproj index 5f69141..89eaebc 100644 --- a/StudentEnrollmentConsoleApp/StudentEnrollmentConsoleApp.csproj +++ b/StudentEnrollmentConsoleApp/StudentEnrollmentConsoleApp.csproj @@ -8,12 +8,6 @@ Linux - - - .dockerignore - - - diff --git a/docker-compose.yml b/docker-compose.yml index 2380678..3ab6959 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,14 +12,3 @@ services: - EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP=true ports: - "2113:2113" - volumes: - - type: volume - source: eventstore-volume-data - target: /var/lib/eventstore - - type: volume - source: eventstore-volume-logs - target: /var/log/eventstore - -volumes: - eventstore-volume-data: - eventstore-volume-logs: \ No newline at end of file