Repository navigation
Expand file tree
/
Copy pathProgram.cs
More file actions
207 lines (184 loc) · 7.54 KB
/
Copy pathProgram.cs
File metadata and controls
207 lines (184 loc) · 7.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
class Program
{
/// <summary>Where Sample.WebServer listens, per its launchSettings.json.</summary>
const string serverAddress = "http://localhost:5000";
/// <param name="args">
/// <c>--live</c> watches the orders instead of printing the tables and exiting.
/// <c>--reprice <id></c> sends the <c>RepriceOrder</c> command and prints its outcome — the
/// write to make in one terminal while another runs <c>--live</c>.
/// <c>--server <url></c> points at a server other than Sample.WebServer — one of the
/// backplane samples.
/// </param>
static async Task<int> Main(string[] args)
{
var server = Value(args, "--server") ?? serverAddress;
// A console app needs no container and no host. An HttpClient, the endpoint MapScry was given,
// and the generated entry point are the whole of it.
// begin-snippet: consoleClientSetup
using var http = new HttpClient
{
BaseAddress = new(server)
};
var query = new ScryQuery(ScryClient.ForHttp(http, "/api/query"));
// end-snippet
try
{
if (args.Contains("--live"))
{
await WatchOrders(query);
return 0;
}
if (Value(args, "--reprice") is { } id)
{
return await Reprice(query, int.Parse(id, CultureInfo.InvariantCulture));
}
await ShowActiveEmployees(query);
await ShowRegions(query);
await ShowActiveCount(query);
return 0;
}
catch (HttpRequestException exception)
{
await Console.Error.WriteLineAsync($"Cannot reach {server}: {exception.Message}");
await Console.Error.WriteLineAsync("Start the server with: dotnet run --project samples/Sample.WebServer");
return 1;
}
}
static string? Value(string[] args, string name)
{
var index = Array.IndexOf(args, name);
if (index < 0 ||
index + 1 >= args.Length)
{
return null;
}
return args[index + 1];
}
record OrderRow(int Id, string Region, decimal Amount);
// A live query is an IAsyncEnumerable, so it is read the way any other is. Each answer is the
// whole current result, and the loop runs until Ctrl+C cancels it — which is also what tells the
// server the subscription is over.
// begin-snippet: consoleLive
static async Task WatchOrders(ScryQuery query)
{
using var leaving = new CancelSource();
Console.CancelKeyPress += (_, pressed) =>
{
pressed.Cancel = true;
leaving.Cancel();
};
Console.WriteLine("Watching orders. Ctrl+C to stop.");
Console.WriteLine();
var orders = query
.Order
.OrderBy(_ => _.Id)
.Select(_ => new OrderRow(_.Id, _.Region, _.Amount))
.Live();
try
{
await foreach (var answer in orders.WithCancellation(leaving.Token))
{
Console.WriteLine($"{DateTime.Now:T}");
WriteTable(
["Id", "Region", "Amount"],
[.. answer.Select(_ => new[] {_.Id.ToString(), _.Region, _.Amount.ToString("0.00")})]);
}
}
catch (OperationCanceledException)
{
// Ctrl+C.
}
}
// end-snippet
// A command, and its outcome. Most finish within the server's sync window and print at once; one
// still running when the client stops waiting is Pending, and the same outcome's Completion is
// what finishes it. A console has nowhere else to show pending work, so it simply waits.
// begin-snippet: consoleCommand
static async Task<int> Reprice(ScryQuery query, int id)
{
var outcome = await query.Commands.RepriceOrder(new() {Id = id});
if (outcome.Status == ScryCommandStatus.Pending)
{
Console.WriteLine($"Order {id} is being repriced…");
outcome = await outcome.Completion;
}
switch (outcome.Status)
{
case ScryCommandStatus.Completed:
Console.WriteLine($"Order {id} repriced.");
return 0;
case ScryCommandStatus.Failed:
await Console.Error.WriteLineAsync($"Order {id} was not repriced: {outcome.Error}");
return 1;
default:
await Console.Error.WriteLineAsync($"Whether order {id} was repriced is unknown: {outcome.Error}");
return 1;
}
}
// end-snippet
// The shapes this app wants, declared here rather than anywhere the server knows about. The
// response comes back keyed by these names.
record EmployeeRow(string Name, Status Status, string? Manager, string Department);
record RegionSummary(string Region, decimal Total, int Count);
// A filter, an ordering, and a projection that reaches through two navigations. The LINQ is
// captured rather than executed: it is translated to the wire AST, validated against the
// allow-list on the server, rebound to the real Employee, and run through EF Core.
// begin-snippet: consoleQuery
static async Task ShowActiveEmployees(ScryQuery query)
{
var rows = await query
.Employee
.Where(_ => _.Active)
.OrderBy(_ => _.Name)
.Select(_ => new EmployeeRow(_.Name, _.Status, _.Manager!.Name, _.Department!.Name))
.ToListAsync();
WriteTable(
["Name", "Status", "Manager", "Department"],
[.. rows.Select(_ => new[] {_.Name, _.Status.ToString(), _.Manager ?? "", _.Department})]);
}
// end-snippet
// Grouping and aggregation happen on the server, so what crosses the wire is one row per region
// rather than every order.
// begin-snippet: consoleGroupBy
static async Task ShowRegions(ScryQuery query)
{
var rows = await query
.Order
.GroupBy(_ => _.Region)
.Select(_ => new RegionSummary(_.Key, _.Sum(order => order.Amount), _.Count()))
.ToListAsync();
WriteTable(
["Region", "Total", "Orders"],
[.. rows.Select(_ => new[] {_.Region, _.Total.ToString("0.00"), _.Count.ToString()})]);
}
// end-snippet
// A terminal that takes a predicate of its own translates it the same way.
// begin-snippet: consoleCount
static async Task ShowActiveCount(ScryQuery query)
{
var active = await query.Employee.CountAsync(_ => _.Active);
Console.WriteLine($"{active} active employees");
Console.WriteLine();
}
// end-snippet
// Nothing to do with Scry: enough column alignment to make the rows readable in a terminal.
static void WriteTable(string[] headers, IReadOnlyList<string[]> rows)
{
var widths = new int[headers.Length];
for (var column = 0; column < headers.Length; column++)
{
widths[column] = headers[column].Length;
foreach (var row in rows)
{
widths[column] = Math.Max(widths[column], row[column].Length);
}
}
Console.WriteLine(string.Join(" ", headers.Select((_, column) => _.PadRight(widths[column]))));
Console.WriteLine(string.Join(" ", widths.Select(_ => new string('-', _))));
foreach (var row in rows)
{
Console.WriteLine(string.Join(" ", row.Select((_, column) => _.PadRight(widths[column]))));
}
Console.WriteLine();
}
}