Create a task provider - Part 2
In part 1, we have created a simple generator provider. The generated code will be static since it only call the ng new command to create an angular project skeleton. In this tutorial we will expand the code generation logic so that it creates angular component for each Models that is available in the CodeGeneratorProvider base class. This way, user can create/update the models, and the changes will be reflected on the code generated by our provider.
You can find the code in this tutorial in our GitHub repository: https://github.com/Polyrific-Inc/Polyrific.Catapult.TaskProviders.Angular/tree/tutorial-part-2

Create Helper classes

Our Program class would get too big if we put all of the code generation logic in there. So it'd be wise to separate some logic into different classes. Though you can actually put all of the logic inside the Program.cs, it'd not be too maintainable once the code generation logic get more complex.
First we'd need a helper Class to run Angular CLI commands. In the previous part, we directly call the ProcessStartInfo inside our code generator method. But we'll be calling several CLI commands, so it'd be wise to have a separate helper method that will run the angular cli commands.
Create a folder Helpers and create a class CommandHelper.cs inside of it. The CommandHelper static class will have a static method named ExecuteShellCommand that execute a shell command along with its arguments.
1
using System;
2
using System.Collections.Generic;
3
using System.Diagnostics;
4
using System.Runtime.InteropServices;
5
using System.Text;
6
using System.Threading.Tasks;
7
using Microsoft.Extensions.Logging;
8
9
namespace MyCodeGenerator.Helpers
10
{
11
public static class CommandHelper
12
{
13
public static async Task<string> ExecuteShellCommand(string command, string workingDirectory, ILogger logger = null)
14
{
15
var outputBuilder = new StringBuilder();
16
var errorBuilder = new StringBuilder();
17
var error = "";
18
19
string fileName;
20
21
// we cannot start the node module in ProcessStartInfo, so we will use powershell
22
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
23
{
24
fileName = "powershell";
25
}
26
else
27
{
28
fileName = "pwsh";
29
command = quot;-c \"{command}\"";
30
}
31
32
var info = new ProcessStartInfo(fileName)
33
{
34
UseShellExecute = false,
35
Arguments = command,
36
RedirectStandardInput = true,
37
RedirectStandardOutput = true,
38
RedirectStandardError = true,
39
CreateNoWindow = true,
40
WorkingDirectory = workingDirectory
41
};
42
43
using (var process = Process.Start(info))
44
{
45
if (process != null)
46
{
47
var reader = process.StandardOutput;
48
while (!reader.EndOfStream)
49
{
50
var line = await reader.ReadLineAsync();
51
52
logger?.LogDebug(line);
53
54
outputBuilder.AppendLine(line);
55
}
56
57
var errorReader = process.StandardError;
58
while (!errorReader.EndOfStream)
59
{
60
var line = await errorReader.ReadLineAsync();
61
62
if (line.StartsWith("npm WARN"))
63
{
64
logger?.LogWarning(line);
65
}
66
else if (!string.IsNullOrEmpty(line))
67
{
68
errorBuilder.AppendLine(line);
69
}
70
}
71
72
error = errorBuilder.ToString();
73
}
74
}
75
76
if (!string.IsNullOrEmpty(error))
77
throw new Exception(error);
78
79
return outputBuilder.ToString();
80
}
81
}
82
}
Copied!
Aside from running the command and returning the result, it would also log any output of the command into the logger class.
Now we can create the class that will do most of the heavy lifting, we shall create a class called CodeGenerator, that exposes 1 public method, Generate.
1
using System;
2
using System.Collections.Generic;
3
using System.IO;
4
using System.Reflection;
5
using System.Text;
6
using System.Threading.Tasks;
7
using Humanizer;
8
using Microsoft.Extensions.Logging;
9
using Polyrific.Catapult.TaskProviders.Angular.Helpers;
10
using Polyrific.Catapult.Shared.Dto.Constants;
11
using Polyrific.Catapult.Shared.Dto.ProjectDataModel;
12
13
namespace Polyrific.Catapult.TaskProviders.Angular
14
{
15
public class CodeGenerator
16
{
17
private readonly ILogger _logger;
18
19
private static string AssemblyDirectory
20
{
21
get
22
{
23
string codeBase = Assembly.GetExecutingAssembly().CodeBase;
24
var uri = new UriBuilder(codeBase);
25
string path = Uri.UnescapeDataString(uri.Path);
26
return Path.GetDirectoryName(path);
27
}
28
}
29
30
public CodeGenerator(ILogger logger)
31
{
32
_logger = logger;
33
}
34
35
public async Task<string> Generate(string projectName, string projectTitle, string outputLocation, List<ProjectDataModelDto> models)
36
{
37
try
38
{
39
// clean project name form space
40
projectName = projectName.Replace(" ", "").Kebaberize();
41
var projectFolder = Path.Combine(outputLocation, projectName);
42
43
// 1. Generate the project
44
await CreateAngularProject(projectName, outputLocation);
45
await InitializeProject(projectFolder);
46
47
// 2. Generate each model files
48
await CreateHomeComponent(projectFolder, projectTitle, models);
49
foreach (var model in models)
50
{
51
await CreateModelRelatedFile(projectFolder, model);
52
}
53
54
return "";
55
}
56
catch (Exception ex)
57
{
58
_logger.LogError(ex, ex.Message);
59
return ex.Message;
60
}
61
}
62
63
private async Task CreateAngularProject(string projectName, string outputLocation)
64
{
65
await CommandHelper.ExecuteShellCommand(quot;ng new {projectName} --routing=true --skipGit=true", outputLocation, _logger);
66
}
67
68
private async Task InitializeProject(string projectFolder)
69
{
70
// install angular material to project
71
await CommandHelper.ExecuteShellCommand(quot;ng add @angular/material", projectFolder, _logger);
72
}
73
74
private async Task CreateHomeComponent(string projectFolder, string projectTitle, List<ProjectDataModelDto> models)
75
{
76
var appFolder = Path.Combine(projectFolder, "src/app");
77
78
if (File.Exists(Path.Combine(appFolder, "app.component.ts")))
79
{
80
var content = await LoadFile(Path.Combine(AssemblyDirectory, "Template/app", "app.component.ts"));
81
content = content.Replace("$Titlequot;, projectTitle);
82
await File.WriteAllTextAsync(Path.Combine(appFolder, "app.component.ts"), content);
83
}
84
85
if (File.Exists(Path.Combine(appFolder, "app.component.css")))
86
{
87
var content = await LoadFile(Path.Combine(AssemblyDirectory, "Template/app", "app.component.css"));
88
await File.WriteAllTextAsync(Path.Combine(appFolder, "app.component.css"), content);
89
}
90
91
if (File.Exists(Path.Combine(appFolder, "app.component.html")))
92
{
93
var content = await LoadFile(Path.Combine(AssemblyDirectory, "Template/app", "app.component.html"));
94
95
var sb = new StringBuilder();
96
foreach (var model in models)
97
{
98
sb.AppendLine(quot;<a mat-list-item routerLink=\"/{model.Name.Kebaberize()}\">{model.Label}</a>");
99
}
100
content = content.Replace("$navlistquot;, sb.ToString());
101
102
await File.WriteAllTextAsync(Path.Combine(appFolder, "app.component.html"), content);
103
}
104
105
106
if (File.Exists(Path.Combine(appFolder, "app.module.ts")))
107
{
108
var content = await LoadFile(Path.Combine(AssemblyDirectory, "Template/app", "app.module.ts"));
109
await File.WriteAllTextAsync(Path.Combine(appFolder, "app.module.ts"), content);
110
}
111
112
if (File.Exists(Path.Combine(appFolder, "app-routing.module.ts")))
113
{
114
var content = await LoadFile(Path.Combine(AssemblyDirectory, "Template/app", "app-routing.module.ts"));
115
116
var sb = new StringBuilder();
117
foreach (var model in models)
118
{
119
var modelName = model.Name.Kebaberize();
120
sb.AppendLine(quot;import {{ {model.Name}Component }} from './{modelName}/{modelName}.component';");
121
}
122
123
content = content.Replace("$ImportComponentsquot;, sb.ToString());
124
125
sb = new StringBuilder();
126
foreach (var model in models)
127
{
128
var modelName = model.Name.Kebaberize();
129
sb.AppendLine(quot;{{path: '{modelName}', component: {model.Name}Component }},");
130
}
131
132
content = content.Replace("$RouteComponentsquot;, sb.ToString());
133
134
await File.WriteAllTextAsync(Path.Combine(appFolder, "app-routing.module.ts"), content);
135
}
136
137
138
await CommandHelper.ExecuteShellCommand(quot;ng generate component home", projectFolder, _logger);
139
140
if (File.Exists(Path.Combine(appFolder, "home", "home.component.html")))
141
{
142
var content = await LoadFile(Path.Combine(AssemblyDirectory, "Template/app/home", "home.component.html"));
143
await File.WriteAllTextAsync(Path.Combine(appFolder, "home", "home.component.html"), content);
144
}
145
}
146
147
private async Task CreateModelRelatedFile(string projectFolder, ProjectDataModelDto model)
148
{
149
await CreateModelComponent(projectFolder, model);
150
151
await CreateModelDataSource(projectFolder, model);
152
}
153
154
private async Task CreateModelComponent(string projectFolder, ProjectDataModelDto model)
155
{
156
var modelName = model.Name.Kebaberize();
157
await CommandHelper.ExecuteShellCommand(quot;ng generate component {modelName}", projectFolder, _logger);
158
159
string componentFolder = Path.Combine(projectFolder, "src/app", modelName);
160
if (File.Exists(Path.Combine(componentFolder, quot;{modelName}.component.ts")))
161
{
162
var content = await LoadFile(Path.Combine(AssemblyDirectory, "Template/app/model", "model.component.ts"));
163
164
content = content.Replace("$ModelNamequot;, modelName);
165
content = content.Replace("$ModelClassNamequot;, model.Name);
166
167
var sb = new StringBuilder();
168
foreach (var property in model.Properties)
169
{
170
var propertyName = property.Name.Camelize();
171
sb.Append(quot;'{propertyName}', ");
172
}
173
174
content = content.Replace("$PropertyListquot;, sb.ToString());
175
176
await File.WriteAllTextAsync(Path.Combine(componentFolder, quot;{modelName}.component.ts"), content);
177
}
178
179
if (File.Exists(Path.Combine(componentFolder, quot;{modelName}.component.css")))
180
{
181
var content = await LoadFile(Path.Combine(AssemblyDirectory, "Template/app/model", "model.component.css"));
182
await File.WriteAllTextAsync(Path.Combine(componentFolder, quot;{modelName}.component.css"), content);
183
}
184
185
if (File.Exists(Path.Combine(componentFolder, quot;{modelName}.component.html")))
186
{
187
var content = await LoadFile(Path.Combine(AssemblyDirectory, "Template/app/model", "model.component.html"));
188
189
var sb = new StringBuilder();
190
foreach (var property in model.Properties)
191
{
192
var propertyName = property.Name.Camelize();
193
sb.AppendLine(quot;<!-- {property.Name} column -->");
194
sb.AppendLine(quot;<ng-container matColumnDef=\"{propertyName}\">");
195
sb.AppendLine(quot;<th mat-header-cell *matHeaderCellDef mat-sort-header>{property.Label}</th>");
196
sb.AppendLine(quot;<td mat-cell *matCellDef=\"let row\">{{{{row.{propertyName}}}}}</td>");
197
sb.AppendLine("</ng-container>");
198
}
199
content = content.Replace("$ColumnDefinitionquot;, sb.ToString());
200
201
await File.WriteAllTextAsync(Path.Combine(componentFolder, quot;{modelName}.component.html"), content);
202
}
203
}
204
205
private async Task CreateModelDataSource(string projectFolder, ProjectDataModelDto model)
206
{
207
var modelName = model.Name.Kebaberize();
208
var componentFolder = Path.Combine(projectFolder, "src/app", modelName);
209
var content = await LoadFile(Path.Combine(AssemblyDirectory, "Template/app/model", "model-datasource.ts"));
210
211
var sb = new StringBuilder();
212
foreach (var property in model.Properties)
213
{
214
var propertyName = property.Name.Camelize();
215
216
string propertyType;
217
switch (property.DataType)
218
{
219
case PropertyDataType.String:
220
propertyType = "string";
221
break;
222
case PropertyDataType.Integer:
223
case PropertyDataType.Short:
224
case PropertyDataType.Float:
225
case PropertyDataType.Decimal:
226
case PropertyDataType.Double:
227
propertyType = "number";
228
break;
229
case PropertyDataType.Boolean:
230
propertyType = "boolean";
231
break;
232
default:
233
propertyType = "any";
234
break;
235
}
236
sb.AppendLine(quot;{propertyName}: {propertyType};");
237
}
238
239
content = content.Replace("$ModelDefinitionquot;, sb.ToString());
240
241
sb = new StringBuilder();
242
for (var i = 0; i < 10; i++)
243
{
244
sb.Append("{");
245
foreach (var property in model.Properties)
246
{
247
var propertyName = property.Name.Camelize();
248
sb.Append(quot;{propertyName}: {GetRandomData(property.DataType)}, ");
249
}
250
251
sb.Append("},");
252
sb.AppendLine();
253
}
254
255
content = content.Replace("$ModelDummyDataquot;, sb.ToString());
256
257
sb = new StringBuilder();
258
sb.AppendLine("switch (this.sort.active) {");
259
foreach (var property in model.Properties)
260
{
261
var propertyName = property.Name.Camelize();
262
switch (property.DataType)
263
{
264
case PropertyDataType.Boolean:
265
case PropertyDataType.String:
266
sb.AppendLine(quot;case '{propertyName}': return compare(a.{propertyName}, b.{propertyName}, isAsc);");
267
break;
268
case PropertyDataType.Integer:
269
case PropertyDataType.Short:
270
case PropertyDataType.Float:
271
case PropertyDataType.Decimal:
272
case PropertyDataType.Double:
273
sb.AppendLine(quot;case '{propertyName}': return compare(+a.{propertyName}, +b.{propertyName}, isAsc);");
274
break;
275
default:
276
break;
277
}
278
}
279
sb.AppendLine("default: return 0;");
280
sb.AppendLine("}");
281
content = content.Replace("$ModelSortquot;, sb.ToString());
282
283
content = content.Replace("$ModelNamequot;, model.Name);
284
285
await File.WriteAllTextAsync(Path.Combine(componentFolder, quot;{modelName}-datasource.ts"), content);
286
}
287
288
private async Task<string> LoadFile(string filePath)
289
{
290
var content = await File.ReadAllTextAsync(filePath);
291
292
content = content.Replace("// @ts-ignore", "");
293
294
return content;
295
}
296
297
private string GetRandomData(string propertyType)
298
{
299
var rand = new Random();
300
switch (propertyType)
301
{
302
case PropertyDataType.Integer:
303
case PropertyDataType.Short:
304
case PropertyDataType.Float:
305
case PropertyDataType.Decimal:
306
case PropertyDataType.Double:
307
return rand.Next(10).ToString();
308
case PropertyDataType.Boolean:
309
return (rand.NextDouble() >= 0.5) ? "true" : "false";
310
default:
311
return quot;\"dummy {rand.Next(10)}\"";
312
}
313
}
314
}
315
}
Copied!
The code above basically do four things:
    Create the angular project using ng new
    Add Material UI library using ng add
    Add angular components for each model using ng generate component
    Modify the generated components based on the model's property

Provide model template

To modify the generated components based on the model's property, we'd need to add some template files in our project. Please download the following zip file , and put it into your project. The folder structure should like this:
Project structure

Call the CodeGenerator in Program.cs

The last thing is to call the CodeGenerator in the Program.cs. The generate method will return an error message if exists. If there's an error message, we should return it at the third tupple item.
1
var error = await _codeGenerator.Generate(ProjectName, projectTitle, Config.OutputLocation, Models);
2
3
if (!string.IsNullOrEmpty(error))
4
return ("", null, error);
Copied!
Here's how the Program.cs should look now:
1
using System;
2
using System.Collections.Generic;
3
using System.IO;
4
using System.Runtime.CompilerServices;
5
using System.Threading.Tasks;
6
using Humanizer;
7
using Polyrific.Catapult.TaskProviders.Core;
8
9
namespace Polyrific.Catapult.TaskProviders.Angular
10
{
11
class Program : CodeGeneratorProvider
12
{
13
private readonly CodeGenerator _codeGenerator;
14
15
public Program(string[] args) : base(args)
16
{
17
_codeGenerator = new CodeGenerator(Logger);
18
}
19
20
public override string Name => "Polyrific.Catapult.TaskProviders.Angular";
21
22
static async Task Main(string[] args)
23
{
24
var app = new Program(args);
25
26
var result = await app.Execute();
27
app.ReturnOutput(result);
28
}
29
30
public override async Task<(string outputLocation, Dictionary<string, string> outputValues, string errorMessage)> Generate()
31
{
32
string projectTitle = ProjectName.Humanize(); // set the default title to project name
33
if (AdditionalConfigs != null && AdditionalConfigs.ContainsKey("Title") && !string.IsNullOrEmpty(AdditionalConfigs["Title"]))
34
projectTitle = AdditionalConfigs["Title"];
35
36
Config.OutputLocation = Config.OutputLocation ?? Config.WorkingLocation;
37
38
var error = await _codeGenerator.Generate(ProjectName, projectTitle, Config.OutputLocation, Models);
39
40
if (!string.IsNullOrEmpty(error))
41
return ("", null, error);
42
43
return (Config.OutputLocation, null, "");
44
}
45
}
46
}
Copied!

Summary

We have created a code generator provider that would generate code based on the models supplied in opencatapult. The generated code will change each time the model is changed. This make it easier in our development when we need to add a new feature and required to add new models. We only need to modify the model structure in opencatapult, run our code generation task, and the new components will be created for us.
Now, let see our generator in action in Part 3
Last modified 2yr ago