Skip to content
This repository was archived by the owner on Dec 23, 2024. It is now read-only.

Commit 5a987d5

Browse files
authored
Merge pull request #3 from jltrem/dev
minimal sample: C# .NET Core Web App
2 parents c325291 + 85150f8 commit 5a987d5

39 files changed

+1913
-24
lines changed

Fescq.sln

Lines changed: 59 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3-
# Visual Studio 15
4-
VisualStudioVersion = 15.0.26124.0
3+
# Visual Studio Version 16
4+
VisualStudioVersion = 16.0.29519.181
55
MinimumVisualStudioVersion = 15.0.26124.0
66
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C397A34C-84F1-49E7-AEBC-2F9F2B196216}"
77
EndProject
8-
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Fescq", "src\Fescq\Fescq.fsproj", "{5D30E174-2538-47AC-8443-318C8C5DC2C9}"
8+
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Fescq", "src\Fescq\Fescq.fsproj", "{5D30E174-2538-47AC-8443-318C8C5DC2C9}"
99
EndProject
1010
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{ACBEE43C-7A88-4FB1-9B06-DB064D22B29F}"
1111
EndProject
12-
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Fescq.Tests", "tests\Fescq.Tests\Fescq.Tests.fsproj", "{1CA2E092-2320-451D-A4F0-9ED7C7C528CA}"
12+
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Fescq.Tests", "tests\Fescq.Tests\Fescq.Tests.fsproj", "{1CA2E092-2320-451D-A4F0-9ED7C7C528CA}"
1313
EndProject
14-
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "docsTool", "docsTool\docsTool.fsproj", "{8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}"
14+
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "docsTool", "docsTool\docsTool.fsproj", "{8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}"
15+
EndProject
16+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{89CC89F5-C04F-42D8-9C4E-7015377A240F}"
17+
EndProject
18+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetCoreWebApp", "samples\NetCoreWebApp\NetCoreWebApp.csproj", "{980549EA-C153-47E5-9B66-DB843B7A9D7E}"
19+
EndProject
20+
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "CrmDomain", "samples\CrmDomain\CrmDomain.fsproj", "{0C8BACE4-7E79-4513-BB03-BBFAFFB393DF}"
1521
EndProject
1622
Global
1723
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -22,34 +28,31 @@ Global
2228
Release|x64 = Release|x64
2329
Release|x86 = Release|x86
2430
EndGlobalSection
25-
GlobalSection(SolutionProperties) = preSolution
26-
HideSolutionNode = FALSE
27-
EndGlobalSection
2831
GlobalSection(ProjectConfigurationPlatforms) = postSolution
2932
{5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
3033
{5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
31-
{5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|x64.ActiveCfg = Debug|x64
32-
{5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|x64.Build.0 = Debug|x64
33-
{5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|x86.ActiveCfg = Debug|x86
34-
{5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|x86.Build.0 = Debug|x86
34+
{5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|x64.ActiveCfg = Debug|Any CPU
35+
{5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|x64.Build.0 = Debug|Any CPU
36+
{5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|x86.ActiveCfg = Debug|Any CPU
37+
{5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|x86.Build.0 = Debug|Any CPU
3538
{5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
3639
{5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|Any CPU.Build.0 = Release|Any CPU
37-
{5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|x64.ActiveCfg = Release|x64
38-
{5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|x64.Build.0 = Release|x64
39-
{5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|x86.ActiveCfg = Release|x86
40-
{5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|x86.Build.0 = Release|x86
40+
{5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|x64.ActiveCfg = Release|Any CPU
41+
{5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|x64.Build.0 = Release|Any CPU
42+
{5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|x86.ActiveCfg = Release|Any CPU
43+
{5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|x86.Build.0 = Release|Any CPU
4144
{1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
4245
{1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
43-
{1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|x64.ActiveCfg = Debug|x64
44-
{1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|x64.Build.0 = Debug|x64
45-
{1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|x86.ActiveCfg = Debug|x86
46-
{1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|x86.Build.0 = Debug|x86
46+
{1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|x64.ActiveCfg = Debug|Any CPU
47+
{1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|x64.Build.0 = Debug|Any CPU
48+
{1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|x86.ActiveCfg = Debug|Any CPU
49+
{1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|x86.Build.0 = Debug|Any CPU
4750
{1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
4851
{1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|Any CPU.Build.0 = Release|Any CPU
49-
{1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|x64.ActiveCfg = Release|x64
50-
{1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|x64.Build.0 = Release|x64
51-
{1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|x86.ActiveCfg = Release|x86
52-
{1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|x86.Build.0 = Release|x86
52+
{1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|x64.ActiveCfg = Release|Any CPU
53+
{1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|x64.Build.0 = Release|Any CPU
54+
{1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|x86.ActiveCfg = Release|Any CPU
55+
{1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|x86.Build.0 = Release|Any CPU
5356
{8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
5457
{8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}.Debug|Any CPU.Build.0 = Debug|Any CPU
5558
{8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -62,9 +65,41 @@ Global
6265
{8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}.Release|x64.Build.0 = Release|Any CPU
6366
{8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}.Release|x86.ActiveCfg = Release|Any CPU
6467
{8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}.Release|x86.Build.0 = Release|Any CPU
68+
{980549EA-C153-47E5-9B66-DB843B7A9D7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
69+
{980549EA-C153-47E5-9B66-DB843B7A9D7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
70+
{980549EA-C153-47E5-9B66-DB843B7A9D7E}.Debug|x64.ActiveCfg = Debug|Any CPU
71+
{980549EA-C153-47E5-9B66-DB843B7A9D7E}.Debug|x64.Build.0 = Debug|Any CPU
72+
{980549EA-C153-47E5-9B66-DB843B7A9D7E}.Debug|x86.ActiveCfg = Debug|Any CPU
73+
{980549EA-C153-47E5-9B66-DB843B7A9D7E}.Debug|x86.Build.0 = Debug|Any CPU
74+
{980549EA-C153-47E5-9B66-DB843B7A9D7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
75+
{980549EA-C153-47E5-9B66-DB843B7A9D7E}.Release|Any CPU.Build.0 = Release|Any CPU
76+
{980549EA-C153-47E5-9B66-DB843B7A9D7E}.Release|x64.ActiveCfg = Release|Any CPU
77+
{980549EA-C153-47E5-9B66-DB843B7A9D7E}.Release|x64.Build.0 = Release|Any CPU
78+
{980549EA-C153-47E5-9B66-DB843B7A9D7E}.Release|x86.ActiveCfg = Release|Any CPU
79+
{980549EA-C153-47E5-9B66-DB843B7A9D7E}.Release|x86.Build.0 = Release|Any CPU
80+
{0C8BACE4-7E79-4513-BB03-BBFAFFB393DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
81+
{0C8BACE4-7E79-4513-BB03-BBFAFFB393DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
82+
{0C8BACE4-7E79-4513-BB03-BBFAFFB393DF}.Debug|x64.ActiveCfg = Debug|Any CPU
83+
{0C8BACE4-7E79-4513-BB03-BBFAFFB393DF}.Debug|x64.Build.0 = Debug|Any CPU
84+
{0C8BACE4-7E79-4513-BB03-BBFAFFB393DF}.Debug|x86.ActiveCfg = Debug|Any CPU
85+
{0C8BACE4-7E79-4513-BB03-BBFAFFB393DF}.Debug|x86.Build.0 = Debug|Any CPU
86+
{0C8BACE4-7E79-4513-BB03-BBFAFFB393DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
87+
{0C8BACE4-7E79-4513-BB03-BBFAFFB393DF}.Release|Any CPU.Build.0 = Release|Any CPU
88+
{0C8BACE4-7E79-4513-BB03-BBFAFFB393DF}.Release|x64.ActiveCfg = Release|Any CPU
89+
{0C8BACE4-7E79-4513-BB03-BBFAFFB393DF}.Release|x64.Build.0 = Release|Any CPU
90+
{0C8BACE4-7E79-4513-BB03-BBFAFFB393DF}.Release|x86.ActiveCfg = Release|Any CPU
91+
{0C8BACE4-7E79-4513-BB03-BBFAFFB393DF}.Release|x86.Build.0 = Release|Any CPU
92+
EndGlobalSection
93+
GlobalSection(SolutionProperties) = preSolution
94+
HideSolutionNode = FALSE
6595
EndGlobalSection
6696
GlobalSection(NestedProjects) = preSolution
6797
{5D30E174-2538-47AC-8443-318C8C5DC2C9} = {C397A34C-84F1-49E7-AEBC-2F9F2B196216}
6898
{1CA2E092-2320-451D-A4F0-9ED7C7C528CA} = {ACBEE43C-7A88-4FB1-9B06-DB064D22B29F}
99+
{980549EA-C153-47E5-9B66-DB843B7A9D7E} = {89CC89F5-C04F-42D8-9C4E-7015377A240F}
100+
{0C8BACE4-7E79-4513-BB03-BBFAFFB393DF} = {89CC89F5-C04F-42D8-9C4E-7015377A240F}
101+
EndGlobalSection
102+
GlobalSection(ExtensibilityGlobals) = postSolution
103+
SolutionGuid = {79914A29-F087-4930-94F4-DBEF8217BF2A}
69104
EndGlobalSection
70105
EndGlobal
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"iisSettings": {
3+
"windowsAuthentication": false,
4+
"anonymousAuthentication": true,
5+
"iisExpress": {
6+
"applicationUrl": "http://localhost:61011/",
7+
"sslPort": 44371
8+
}
9+
},
10+
"profiles": {
11+
"IIS Express": {
12+
"commandName": "IISExpress",
13+
"launchBrowser": true,
14+
"environmentVariables": {
15+
"ASPNETCORE_ENVIRONMENT": "Development"
16+
}
17+
},
18+
"docsTool": {
19+
"commandName": "Project",
20+
"launchBrowser": true,
21+
"environmentVariables": {
22+
"ASPNETCORE_ENVIRONMENT": "Development"
23+
},
24+
"applicationUrl": "https://localhost:5001;http://localhost:5000"
25+
}
26+
}
27+
}
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
module CrmDomain.Aggregate.Contact
2+
3+
open System
4+
open Fescq
5+
open CrmDomain
6+
7+
type Contact = {
8+
Name: PersonalName
9+
Phones: Map<Guid, PhoneNumber>
10+
}
11+
12+
13+
type CreateContact (name:PersonalName) =
14+
inherit Fescq.Command.CreateAggregateCommand()
15+
member val Name = name
16+
17+
type RenameContact (aggregateId:Guid, originalVersion:int, name:PersonalName) =
18+
inherit Fescq.Command.UpdateCommand(aggregateId, originalVersion)
19+
member val Name = name
20+
21+
type AddContactPhone (aggregateId:Guid, originalVersion:int, phone: PhoneNumber) =
22+
inherit Fescq.Command.DetailCommand(aggregateId, originalVersion)
23+
member val Phone = phone
24+
25+
type UpdateContactPhone (aggregateId:Guid, originalVersion:int, phoneId:Guid, phone: PhoneNumber) =
26+
inherit Fescq.Command.DetailCommand(aggregateId, phoneId, originalVersion)
27+
member val Phone = phone
28+
29+
type ContactCommand =
30+
private
31+
| Create of CreateContact
32+
| Rename of RenameContact
33+
| AddPhone of AddContactPhone
34+
| UpdatePhone of UpdateContactPhone
35+
36+
// only public constructor for ContactCommand
37+
let validateCommand (command:Fescq.Command.ICommand) =
38+
match command with
39+
40+
| :? CreateContact ->
41+
let cmd = command :?> CreateContact
42+
cmd.Name
43+
|> Validate.personalName
44+
|> Option.map (fun _ -> cmd |> ContactCommand.Create)
45+
46+
| :? RenameContact ->
47+
let cmd = command :?> RenameContact
48+
cmd.Name
49+
|> Validate.personalName
50+
|> Option.map (fun _ -> cmd |> ContactCommand.Rename)
51+
52+
| :? AddContactPhone ->
53+
let cmd = command :?> AddContactPhone
54+
cmd.Phone
55+
|> Validate.phoneNumber
56+
|> Option.map (fun _ -> cmd |> ContactCommand.AddPhone)
57+
58+
| :? UpdateContactPhone ->
59+
let cmd = command :?> UpdateContactPhone
60+
cmd.Phone
61+
|> Validate.phoneNumber
62+
|> Option.map (fun _ -> cmd |> ContactCommand.UpdatePhone)
63+
64+
| _ -> failwith "unexpected command"
65+
66+
67+
[<EventData("contact-created", 1)>]
68+
type ContactCreated (name:PersonalName) =
69+
interface IEventData
70+
member val Name = name
71+
72+
[<EventData("contact-renamed", 1)>]
73+
type ContactRenamed (name:PersonalName) =
74+
interface IEventData
75+
member val Name = name
76+
77+
[<EventData("contact-phone-added", 1)>]
78+
type ContactPhoneAdded (phoneId:Guid, phone:PhoneNumber) =
79+
interface IEventData
80+
member val PhoneId = phoneId
81+
member val Phone = phone
82+
83+
[<EventData("contact-phone-updated", 1)>]
84+
type ContactPhoneUpdated (phoneId:Guid, phone:PhoneNumber) =
85+
interface IEventData
86+
member val PhoneId = phoneId
87+
member val Phone = phone
88+
89+
type ContactEvent =
90+
| Created of ContactCreated
91+
| Renamed of ContactRenamed
92+
| PhoneAdded of ContactPhoneAdded
93+
| PhoneUpdated of ContactPhoneUpdated
94+
95+
96+
let validateEventData (eventData:IEventData) =
97+
match eventData with
98+
| :? ContactCreated -> eventData :?> ContactCreated |> ContactEvent.Created
99+
| :? ContactRenamed -> eventData :?> ContactRenamed |> ContactEvent.Renamed
100+
| :? ContactPhoneAdded -> eventData :?> ContactPhoneAdded |> ContactEvent.PhoneAdded
101+
| :? ContactPhoneUpdated -> eventData :?> ContactPhoneUpdated |> ContactEvent.PhoneUpdated
102+
| _ -> failwith "unsupported event type"
103+
104+
105+
let apply (state:(int*Contact) option) (e:Event) =
106+
107+
let entity () =
108+
match state with
109+
| Some (_, value) -> value
110+
| None -> failwith "state was None for update"
111+
112+
let version =
113+
match state with
114+
| Some (value, _) -> value
115+
| None -> 1
116+
117+
let update =
118+
match validateEventData e.EventData with
119+
120+
| ContactEvent.Created data ->
121+
match state with
122+
| None -> { Name = data.Name; Phones = []|> Map }
123+
| Some _ -> failwith "state was Some for create"
124+
125+
| ContactEvent.Renamed data ->
126+
{ entity() with Name = data.Name }
127+
128+
| ContactEvent.PhoneAdded data ->
129+
let prev = entity()
130+
if prev.Phones.ContainsKey(data.PhoneId) then failwith "ContactPhoneAdded: id already exists"
131+
{ prev with Phones = prev.Phones.Add(data.PhoneId, data.Phone) }
132+
133+
| ContactEvent.PhoneUpdated data ->
134+
let prev = entity()
135+
if not(prev.Phones.ContainsKey(data.PhoneId)) then failwith "ContactPhoneUpdated: id does not exist"
136+
{ prev with Phones = prev.Phones.Add(data.PhoneId, data.Phone) }
137+
138+
Some (version, update)
139+
140+
module Handle =
141+
142+
let aggId (command:Command.ICommand) =
143+
command.AggregateId
144+
145+
let create utcNow metaData (command:CreateContact) =
146+
147+
let key =
148+
{ Name = "contact"
149+
Id = aggId command
150+
Version = 1 }
151+
152+
{
153+
AggregateKey = key
154+
Timestamp = utcNow
155+
MetaData = metaData
156+
EventData = ContactCreated(command.Name)
157+
}
158+
|> Aggregate.createWithFirstEvent apply
159+
160+
161+
let update utcNow metaData (command:Fescq.Command.ICommand) (aggregate:Agg<Contact>) =
162+
163+
try
164+
command
165+
|> validateCommand
166+
|> function
167+
| Some cmd ->
168+
match cmd with
169+
| Create cmd -> ContactCreated(cmd.Name) :> IEventData, cmd |> aggId
170+
| Rename cmd -> ContactRenamed(cmd.Name) :> IEventData, cmd |> aggId
171+
| AddPhone cmd -> ContactPhoneAdded(cmd.DetailId, cmd.Phone) :> IEventData, cmd |> aggId
172+
| UpdatePhone cmd -> ContactPhoneUpdated(cmd.DetailId, cmd.Phone) :> IEventData, cmd |> aggId
173+
| None -> failwith "data validation failed"
174+
175+
|> fun (eventData, aggId) ->
176+
if aggId = aggregate.Key.Id then
177+
178+
{ AggregateKey = { aggregate.Key with Version = aggregate.Key.Version + 1 }
179+
Timestamp = utcNow
180+
MetaData = metaData
181+
EventData = eventData }
182+
|> Aggregate.createWithNextEvent apply aggregate.History
183+
|> Ok
184+
else
185+
Error "aggregate and command refer to different ids"
186+
with
187+
ex -> Error ex.Message
188+
189+
module CSharp =
190+
191+
let Update utcNow metaData command aggregate =
192+
update utcNow metaData command aggregate
193+
|> function
194+
| Ok agg -> struct (Some struct (fst agg, snd agg), None)
195+
| Error msg -> struct (None, Some msg)
196+
197+
module Storage =
198+
199+
let private factory history =
200+
try
201+
let (agg, _) = Aggregate.createFromHistory<Contact> apply history
202+
Ok agg
203+
with
204+
ex -> Error ex.Message
205+
206+
let load (store:IEventStore) (aggId:Guid) =
207+
Repository<Contact> store
208+
:> IRepository<Contact>
209+
|> fun x -> x.Load(aggId, factory)
210+
211+
let loadExpectedVersion (store:IEventStore) (aggId:Guid) (expectedVersion:int) =
212+
Repository<Contact> store
213+
:> IRepository<Contact>
214+
|> fun x -> x.LoadExpectedVersion(aggId, factory, expectedVersion)
215+
216+
217+
let save (store:IEventStore) (update:Agg<Contact> * Event list) =
218+
219+
Repository<Contact> store
220+
:> IRepository<Contact>
221+
|> fun x -> x.Save(fst update, snd update)
222+
223+
module CSharp =
224+
225+
let Load (store:IEventStore, aggId:Guid) =
226+
load store aggId
227+
|> function
228+
| Ok agg -> struct (Some agg, None)
229+
| Error msg -> struct (None, Some msg)
230+
231+
232+
let LoadExpectedVersion (store:IEventStore, aggId:Guid, expectedVersion:int) =
233+
loadExpectedVersion store aggId expectedVersion
234+
|> function
235+
| Ok agg -> struct (Some agg, None)
236+
| Error msg -> struct (None, Some msg)
237+

0 commit comments

Comments
 (0)