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

Commit 85150f8

Browse files
committed
samples/NetCoreWebApp: minimal example
1 parent ce392a0 commit 85150f8

File tree

11 files changed

+639
-15
lines changed

11 files changed

+639
-15
lines changed

Fescq.sln

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{89CC
1717
EndProject
1818
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetCoreWebApp", "samples\NetCoreWebApp\NetCoreWebApp.csproj", "{980549EA-C153-47E5-9B66-DB843B7A9D7E}"
1919
EndProject
20+
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "CrmDomain", "samples\CrmDomain\CrmDomain.fsproj", "{0C8BACE4-7E79-4513-BB03-BBFAFFB393DF}"
21+
EndProject
2022
Global
2123
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2224
Debug|Any CPU = Debug|Any CPU
@@ -75,6 +77,18 @@ Global
7577
{980549EA-C153-47E5-9B66-DB843B7A9D7E}.Release|x64.Build.0 = Release|Any CPU
7678
{980549EA-C153-47E5-9B66-DB843B7A9D7E}.Release|x86.ActiveCfg = Release|Any CPU
7779
{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
7892
EndGlobalSection
7993
GlobalSection(SolutionProperties) = preSolution
8094
HideSolutionNode = FALSE
@@ -83,6 +97,7 @@ Global
8397
{5D30E174-2538-47AC-8443-318C8C5DC2C9} = {C397A34C-84F1-49E7-AEBC-2F9F2B196216}
8498
{1CA2E092-2320-451D-A4F0-9ED7C7C528CA} = {ACBEE43C-7A88-4FB1-9B06-DB064D22B29F}
8599
{980549EA-C153-47E5-9B66-DB843B7A9D7E} = {89CC89F5-C04F-42D8-9C4E-7015377A240F}
100+
{0C8BACE4-7E79-4513-BB03-BBFAFFB393DF} = {89CC89F5-C04F-42D8-9C4E-7015377A240F}
86101
EndGlobalSection
87102
GlobalSection(ExtensibilityGlobals) = postSolution
88103
SolutionGuid = {79914A29-F087-4930-94F4-DBEF8217BF2A}
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+

samples/CrmDomain/CrmDomain.fsproj

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.1</TargetFramework>
5+
</PropertyGroup>
6+
7+
<ItemGroup>
8+
<Compile Include="Library.fs" />
9+
<Compile Include="Aggregate.Contact.fs" />
10+
</ItemGroup>
11+
12+
<ItemGroup>
13+
<ProjectReference Include="..\..\src\Fescq\Fescq.fsproj" />
14+
</ItemGroup>
15+
16+
</Project>

samples/CrmDomain/Library.fs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
namespace CrmDomain
2+
3+
open System
4+
5+
type CompanyName = string
6+
7+
type PersonalName = {
8+
Given: string
9+
Middle: string
10+
Family: string
11+
}
12+
13+
type PhoneType =
14+
| Unknown = 0
15+
| Mobile = 1
16+
| Work = 2
17+
| Home = 3
18+
19+
type PhoneNumber = {
20+
PhoneType: PhoneType
21+
Number: string
22+
Ext: string
23+
}
24+
25+
type AddressType =
26+
| Unknown = 0
27+
| Primary = 1
28+
| Alternate = 2
29+
| Shipping = 3
30+
| Billing = 4
31+
32+
type Address = {
33+
AddressType: AddressType
34+
Line1: string
35+
Line2: string
36+
City: string
37+
State: string
38+
Country: string
39+
Zip: string
40+
}
41+
42+
module Validate =
43+
44+
let private trim (value:string) =
45+
if String.IsNullOrEmpty(value) then "" else value.Trim()
46+
47+
let private lengthWithin min max value =
48+
trim value
49+
|> fun x ->
50+
if x.Length >= min && x.Length <= max then
51+
Some value
52+
else
53+
None
54+
55+
let companyName (value:CompanyName) =
56+
lengthWithin 1 80 value
57+
58+
let personalName (value:PersonalName) =
59+
let g = trim value.Given
60+
let m = trim value.Middle
61+
let f = trim value.Family
62+
63+
String.Concat (g, m, f)
64+
|> lengthWithin 1 256
65+
|> Option.map (fun _ ->
66+
{
67+
Given = g
68+
Middle = m
69+
Family = f
70+
})
71+
72+
let phoneNumber (value:PhoneNumber) =
73+
lengthWithin 1 80 value.Number
74+
|> Option.map (fun n ->
75+
{
76+
PhoneType = value.PhoneType
77+
Number = n
78+
Ext = trim value.Ext
79+
})
80+
81+
let address (value:Address) =
82+
[lengthWithin 1 80 value.Line1; lengthWithin 1 80 value.City]
83+
|> function
84+
| [Some line1; Some city] ->
85+
{
86+
AddressType = value.AddressType
87+
Line1 = line1
88+
Line2 = trim value.Line2
89+
City = city
90+
State = trim value.State
91+
Country = trim value.Country
92+
Zip = trim value.Zip
93+
} |> Some
94+
| _ -> None
95+
96+

0 commit comments

Comments
 (0)