├── .editorconfig ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── Chapter10 ├── Chapter10.csproj ├── Program.cs ├── README.md ├── Structured │ ├── Database.cs │ ├── IDatabase.cs │ ├── IUserDetailsService.cs │ ├── IUsersService.cs │ ├── RegisterUser.cs │ ├── User.cs │ ├── UserDetails.cs │ ├── UserDetailsService.cs │ ├── UsersController.cs │ └── UsersService.cs ├── Unstructured │ └── UsersController.cs ├── appsettings.Development.json └── appsettings.json ├── Chapter11 ├── Chapter11.csproj ├── ConfidentialAttribute.cs ├── ConfidentialMetadataProvider.cs ├── JournalEntry.cs ├── JournalEntryMetadataProvider.cs ├── Patient.cs └── Program.cs ├── Chapter12 ├── AccountBalance.cs ├── AccountLifecycle.cs ├── Chapter12.csproj ├── EventSourcing │ ├── EventContext.cs │ ├── EventLog.cs │ ├── EventSequenceNumber.cs │ ├── EventSourceId.cs │ ├── IEvent.cs │ ├── IEventLog.cs │ ├── IObservers.cs │ ├── ObserverAttribute.cs │ ├── ObserverHandler.cs │ └── Observers.cs ├── Events.cs └── Program.cs ├── Chapter13 ├── AdminForNamespace.cs ├── AdminForNamespaceHandler.cs ├── Chapter13.csproj ├── Commands │ ├── CommandActionFilter.cs │ ├── CommandResult.cs │ ├── ModelErrorExtensions.cs │ └── ValidationResult.cs ├── CrossCuttingAuthorizationMiddlewareResultHandler.cs ├── CrossCuttingPoliciesProvider.cs ├── Employee.cs ├── EmployeesController.cs ├── HardCodedAuthenticationHandler.cs ├── HardCodedAuthenticationOptions.cs ├── HttpContextExtensions.cs ├── Program.cs ├── appsettings.Development.json └── appsettings.json ├── Chapter14 ├── Authenticator.cs ├── AuthorizationInterceptor.cs ├── Authorizer.cs ├── Chapter14.csproj ├── DefaultInstaller.cs ├── IAuthenticator.cs ├── IAuthorizer.cs ├── IUsersService.cs ├── IUsersServiceComposition.cs ├── InterceptorSelector.cs ├── LoggingInterceptor.cs ├── LoggingInterceptorLogMessages.cs ├── Program.cs ├── Todo │ ├── ITodoService.cs │ └── TodoService.cs └── UsersService.cs ├── Chapter15 ├── Chapter15.csproj ├── Program.cs └── Something.cs ├── Chapter16 ├── Chapter16.csproj ├── Employee.cs ├── EmployeesController.cs ├── EmployeesControllerMetrics.cs ├── GDPRReport.txt ├── Metrics.cs ├── Program.cs ├── ProgramMetrics.cs ├── appsettings.Development.json ├── appsettings.json └── build.sh ├── Chapter17 ├── Chapter17.csproj ├── GDPRReport.txt ├── MyException.cs └── Program.cs ├── Chapter2 ├── Calculator.cs ├── CalculatorTests.cs ├── Chapter2.csproj ├── Person.cs ├── Program.cs └── Serializer.cs ├── Chapter3 ├── Chapter3.csproj ├── Employee.cs ├── EmployeesController.cs ├── Program.cs ├── ValidationFilter.cs ├── appsettings.Development.json ├── appsettings.json └── employees ├── Chapter4 ├── AssemblyDiscovery │ ├── AssemblyDiscovery.csproj │ └── Program.cs └── BusinessApp │ ├── Concepts │ ├── Concepts.csproj │ └── Employees │ │ ├── FirstName.cs │ │ ├── LastName.cs │ │ └── SocialSecurityNumber.cs │ ├── Domain │ ├── Domain.csproj │ └── Employees │ │ ├── RegisterEmployee.cs │ │ └── SetSalaryLevelForEmployee.cs │ └── Main │ ├── Main.csproj │ └── Program.cs ├── Chapter5 ├── Chapter5.csproj ├── Employee.cs └── Program.cs ├── Chapter6 ├── Chapter6.csproj ├── Employee.cs ├── IgnoreChangesAttribute.cs ├── MyTypeGenerator.cs ├── NotifyChangesForAttribute.cs ├── NotifyingObjectWeaver.cs └── Program.cs ├── Chapter7 ├── Chapter7.csproj ├── Employee.cs └── Program.cs ├── Chapter8 ├── Chapter8.csproj ├── DictionaryStringObjectJsonConverter.cs ├── MyType.cs ├── Program.cs ├── QueryParser.cs ├── data.json ├── definition.json └── query.json ├── Chapter9 ├── Chapter9.csproj ├── InvalidTypeForProperty.cs ├── JsonSchemaType.cs ├── Program.cs └── person.json ├── Fundamentals ├── ArrayIndexer.cs ├── ArrayIndexers.cs ├── ArrayProperty.cs ├── ChildrenPropertyIsNotEnumerable.cs ├── ChildrenPropertyIsNotEnumerableForType.cs ├── Compliance │ ├── ComplianceDetailsAttribute.cs │ ├── ComplianceDetailsExtensions.cs │ ├── ComplianceMetadata.cs │ ├── ComplianceMetadataResolver.cs │ ├── ComplianceMetadataType.cs │ ├── GDPR │ │ ├── IHoldPersonalIdentifiableInformation.cs │ │ ├── PersonalIdentifiableInformationAttribute.cs │ │ ├── PersonalIdentifiableInformationConceptAs.cs │ │ └── PersonalIdentifiableInformationMetadataProvider.cs │ ├── ICanProvideComplianceMetadataForProperty.cs │ ├── ICanProvideComplianceMetadataForType.cs │ ├── IComplianceMetadataResolver.cs │ ├── NoComplianceMetadataForProperty.cs │ └── NoComplianceMetadataForType.cs ├── ConceptAs.cs ├── ConceptAsJsonConverter.cs ├── ConceptAsJsonConverterFactory.cs ├── ConceptAsTypeConverter.cs ├── ConceptExtensions.cs ├── ConceptFactory.cs ├── ConceptMap.cs ├── ContractToImplementorsMap.cs ├── ExpandoObjectExtensions.cs ├── ExpressionExtensions.cs ├── Fundamentals.csproj ├── Globals.cs ├── IArrayIndexers.cs ├── ICommand.cs ├── IContractToImplementorsMap.cs ├── IImplementationsOf.cs ├── IInstancesOf.cs ├── IPropertyPathSegment.cs ├── ITypeInfo.cs ├── ITypes.cs ├── ImplementationsOf.cs ├── InstancesOf.cs ├── LICENSE ├── MemberInfoExtensions.cs ├── Metrics │ ├── CounterAttribute.cs │ └── GlobalMetrics.cs ├── MissingArrayIndexerForPropertyPath.cs ├── MultipleTypesFound.cs ├── ObjectExtensions.cs ├── PIIConceptAs.cs ├── PropertyName.cs ├── PropertyPath.cs ├── README.md ├── SegmentValueIsNotCollection.cs ├── ServiceCollectionExtensions.cs ├── SingletonAttribute.cs ├── StringExtensions.cs ├── TypeConversion.cs ├── TypeExtensions.cs ├── TypeInfo.cs ├── TypeIsNotAConcept.cs ├── Types.cs ├── UnableToResolvePropertyPathOnType.cs └── UnableToResolveTypeByName.cs ├── LICENSE ├── README.md ├── Roslyn.Extensions.Tests ├── ExceptionShouldNotBeSuffixed │ ├── AnalyzerTests.cs │ └── CodeFixTests.cs └── Roslyn.Extensions.Tests.csproj └── Roslyn.Extensions ├── .globalconfig ├── AnalyzerReleases.Shipped.md ├── AnalyzerReleases.Unshipped.md ├── CodeAnalysis └── ExceptionShouldNotBeSuffixed │ ├── Analyzer.cs │ └── CodeFix.cs ├── GDPR ├── GDPRSourceGenerator.cs └── GDPRSyntaxReceiver.cs ├── Metrics ├── CounterTagTemplateData.cs ├── CounterTemplateData.cs ├── MetricsSourceGenerator.cs ├── MetricsSyntaxReceiver.cs └── MetricsTemplateData.cs ├── Roslyn.Extensions.csproj ├── Roslyn.Extensions.props ├── Templates ├── Metrics.hbs └── TemplateTypes.cs ├── logo.png └── stylecop.json /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | .buildsettings 7 | 8 | # User-specific files 9 | *.suo 10 | *.user 11 | *.userosscache 12 | *.sln.docstates 13 | 14 | # User-specific files (MonoDevelop/Xamarin Studio) 15 | *.userprefs 16 | 17 | # Build results 18 | [Dd]ebug/ 19 | [Dd]ebugPublic/ 20 | [Rr]elease/ 21 | [Rr]eleases/ 22 | x64/ 23 | x86/ 24 | bld/ 25 | [Bb]in/ 26 | [Oo]bj/ 27 | [Ll]og/ 28 | 29 | # Visual Studio 2015 cache/options directory 30 | .vs/ 31 | # Uncomment if you have tasks that create the project's static files in wwwroot 32 | wwwroot/ 33 | 34 | # MSTest test Results 35 | [Tt]est[Rr]esult*/ 36 | [Bb]uild[Ll]og.* 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # .NET Core 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | **/Properties/launchSettings.json 48 | 49 | *_i.c 50 | *_p.c 51 | *_i.h 52 | *.ilk 53 | *.meta 54 | *.obj 55 | *.pch 56 | *.pdb 57 | *.pgc 58 | *.pgd 59 | *.rsp 60 | *.sbr 61 | *.tlb 62 | *.tli 63 | *.tlh 64 | *.tmp 65 | *.tmp_proj 66 | *.log 67 | *.vspscc 68 | *.vssscc 69 | .builds 70 | *.pidb 71 | *.svclog 72 | *.scc 73 | 74 | # Chutzpah Test files 75 | _Chutzpah* 76 | 77 | # Visual C++ cache files 78 | ipch/ 79 | *.aps 80 | *.ncb 81 | *.opendb 82 | *.opensdf 83 | *.sdf 84 | *.cachefile 85 | *.VC.db 86 | *.VC.VC.opendb 87 | 88 | # Visual Studio profiler 89 | *.psess 90 | *.vsp 91 | *.vspx 92 | *.sap 93 | 94 | # TFS 2012 Local Workspace 95 | $tf/ 96 | 97 | # Guidance Automation Toolkit 98 | *.gpState 99 | 100 | # ReSharper is a .NET coding add-in 101 | _ReSharper*/ 102 | *.[Rr]e[Ss]harper 103 | *.DotSettings.user 104 | 105 | # JustCode is a .NET coding add-in 106 | .JustCode 107 | 108 | # TeamCity is a build add-in 109 | _TeamCity* 110 | 111 | # DotCover is a Code Coverage Tool 112 | *.dotCover 113 | 114 | # Visual Studio code coverage results 115 | *.coverage 116 | *.coveragexml 117 | 118 | # NCrunch 119 | _NCrunch_* 120 | .*crunch*.local.xml 121 | nCrunchTemp_* 122 | 123 | # MightyMoose 124 | *.mm.* 125 | AutoTest.Net/ 126 | 127 | # Web workbench (sass) 128 | .sass-cache/ 129 | 130 | # Installshield output folder 131 | [Ee]xpress/ 132 | 133 | # DocProject is a documentation generator add-in 134 | DocProject/buildhelp/ 135 | DocProject/Help/*.HxT 136 | DocProject/Help/*.HxC 137 | DocProject/Help/*.hhc 138 | DocProject/Help/*.hhk 139 | DocProject/Help/*.hhp 140 | DocProject/Help/Html2 141 | DocProject/Help/html 142 | 143 | # Click-Once directory 144 | publish/ 145 | 146 | # Publish Web Output 147 | *.[Pp]ublish.xml 148 | *.azurePubxml 149 | # TODO: Comment the next line if you want to checkin your web deploy settings 150 | # but database connection strings (with potential passwords) will be unencrypted 151 | *.pubxml 152 | *.publishproj 153 | 154 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 155 | # checkin your Azure Web App publish settings, but sensitive information contained 156 | # in these scripts will be unencrypted 157 | PublishScripts/ 158 | 159 | # NuGet Packages 160 | **/*.nupkg 161 | **/*.snupkg 162 | # The packages folder can be ignored because of Package Restore 163 | **/packages/* 164 | # except build/, which is used as an MSBuild target. 165 | !**/packages/build/ 166 | # Uncomment if necessary however generally it will be regenerated when needed 167 | #!**/packages/repositories.config 168 | # NuGet v3's project.json files produces more ignorable files 169 | *.nuget.props 170 | *.nuget.targets 171 | 172 | # Microsoft Azure Build Output 173 | csx/ 174 | *.build.csdef 175 | 176 | # Microsoft Azure Emulator 177 | ecf/ 178 | rcf/ 179 | 180 | # Windows Store app package directories and files 181 | AppPackages/ 182 | BundleArtifacts/ 183 | Package.StoreAssociation.xml 184 | _pkginfo.txt 185 | 186 | # Visual Studio cache files 187 | # files ending in .cache can be ignored 188 | *.[Cc]ache 189 | # but keep track of directories ending in .cache 190 | !*.[Cc]ache/ 191 | 192 | # Others 193 | ClientBin/ 194 | ~$* 195 | *~ 196 | *.dbmdl 197 | *.dbproj.schemaview 198 | *.jfm 199 | *.pfx 200 | *.publishsettings 201 | orleans.codegen.cs 202 | 203 | # Since there are multiple workflows, uncomment next line to ignore bower_components 204 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 205 | #bower_components/ 206 | 207 | # RIA/Silverlight projects 208 | Generated_Code/ 209 | 210 | # Backup & report files from converting an old project file 211 | # to a newer Visual Studio version. Backup files are not needed, 212 | # because we have git ;-) 213 | _UpgradeReport_Files/ 214 | Backup*/ 215 | UpgradeLog*.XML 216 | UpgradeLog*.htm 217 | 218 | # SQL Server files 219 | *.mdf 220 | *.ldf 221 | *.ndf 222 | 223 | # Business Intelligence projects 224 | *.rdl.data 225 | *.bim.layout 226 | *.bim_*.settings 227 | 228 | # Microsoft Fakes 229 | FakesAssemblies/ 230 | 231 | # GhostDoc plugin setting file 232 | *.GhostDoc.xml 233 | 234 | # Node.js Tools for Visual Studio 235 | .ntvs_analysis.dat 236 | node_modules/ 237 | 238 | 239 | # Typescript v1 declaration files 240 | typings/ 241 | *.d.ts 242 | !*.scss.d.ts 243 | yarn.lock 244 | 245 | # Visual Studio 6 build log 246 | *.plg 247 | 248 | # Visual Studio 6 workspace options file 249 | *.opt 250 | 251 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 252 | *.vbw 253 | 254 | # Visual Studio LightSwitch build output 255 | **/*.HTMLClient/GeneratedArtifacts 256 | **/*.DesktopClient/GeneratedArtifacts 257 | **/*.DesktopClient/ModelManifest.xml 258 | **/*.Server/GeneratedArtifacts 259 | **/*.Server/ModelManifest.xml 260 | _Pvt_Extensions 261 | 262 | # Paket dependency manager 263 | .paket/paket.exe 264 | paket-files/ 265 | 266 | # FAKE - F# Make 267 | .fake/ 268 | 269 | # JetBrains Rider 270 | .idea/ 271 | *.sln.iml 272 | 273 | # CodeRush 274 | .cr/ 275 | 276 | # Python Tools for Visual Studio (PTVS) 277 | __pycache__/ 278 | *.pyc 279 | 280 | # Cake - Uncomment if you are using it 281 | # tools/** 282 | # !tools/packages.config 283 | 284 | yarn.lock 285 | package-json.lock 286 | package-lock.json 287 | dist 288 | **/node_modules/ 289 | *.tsbuildinfo 290 | .eslintcache 291 | 292 | wwwroot 293 | out 294 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Attach", 9 | "type": "coreclr", 10 | "request": "attach" 11 | }, 12 | { 13 | "name": ".NET Core Launch", 14 | "type": "coreclr", 15 | "request": "launch", 16 | "preLaunchTask": "build", 17 | "program": "${workspaceFolder}/Chapter14/bin/Debug/net7.0/Chapter14.dll", 18 | "args": [], 19 | "cwd": "${workspaceFolder}/Chapter14", 20 | "stopAtEntry": false, 21 | "env": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Brtrue", 4 | "Callvirt", 5 | "Castclass", 6 | "Cratis", 7 | "Expando", 8 | "inheritdoc", 9 | "Ldarg", 10 | "Ldfld", 11 | "Ldloc", 12 | "Ldnull", 13 | "Ldstr", 14 | "Newobj", 15 | "Stfld", 16 | "Stloc", 17 | "typeparam", 18 | "Vtable", 19 | "Xunit" 20 | ] 21 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "--no-restore", 11 | "${workspaceFolder}/Chapter14/Chapter14.csproj", 12 | "/property:GenerateFullPaths=true", 13 | "/consoleloggerparameters:NoSummary" 14 | ], 15 | "problemMatcher": "$msCompile" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /Chapter10/Chapter10.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Chapter10/Program.cs: -------------------------------------------------------------------------------- 1 | using Fundamentals; 2 | 3 | var builder = WebApplication.CreateBuilder(args); 4 | builder.Services.AddControllers(); 5 | var types = new Types(); 6 | builder.Services.AddSingleton(types); 7 | builder.Services.AddBindingsByConvention(types); 8 | 9 | var app = builder.Build(); 10 | app.UseRouting(); 11 | app.UseEndpoints(_ => _.MapControllers()); 12 | app.Run(); 13 | -------------------------------------------------------------------------------- /Chapter10/README.md: -------------------------------------------------------------------------------- 1 | # 2 | 3 | ```shell 4 | docker run -d -p 27017:27017 mongo 5 | ``` -------------------------------------------------------------------------------- /Chapter10/Structured/Database.cs: -------------------------------------------------------------------------------- 1 | using Fundamentals; 2 | using Humanizer; 3 | using MongoDB.Driver; 4 | 5 | namespace Chapter10.Structured; 6 | 7 | [Singleton] 8 | public class Database : IDatabase 9 | { 10 | readonly IMongoDatabase _mongoDatabase; 11 | 12 | public Database() 13 | { 14 | var client = new MongoClient("mongodb://localhost:27017"); 15 | _mongoDatabase = client.GetDatabase("TheSystem"); 16 | } 17 | 18 | public IMongoCollection GetCollectionFor() => _mongoDatabase.GetCollection(typeof(T).Name.Pluralize()); 19 | } 20 | -------------------------------------------------------------------------------- /Chapter10/Structured/IDatabase.cs: -------------------------------------------------------------------------------- 1 | using MongoDB.Driver; 2 | 3 | namespace Chapter10.Structured; 4 | 5 | public interface IDatabase 6 | { 7 | IMongoCollection GetCollectionFor(); 8 | } 9 | -------------------------------------------------------------------------------- /Chapter10/Structured/IUserDetailsService.cs: -------------------------------------------------------------------------------- 1 | namespace Chapter10.Structured; 2 | 3 | public interface IUserDetailsService 4 | { 5 | Task Register(string firstName, string lastName, string socialSecurityNumber, Guid userId); 6 | } 7 | -------------------------------------------------------------------------------- /Chapter10/Structured/IUsersService.cs: -------------------------------------------------------------------------------- 1 | namespace Chapter10.Structured; 2 | 3 | public interface IUsersService 4 | { 5 | Task Register(string userName, string password); 6 | } 7 | -------------------------------------------------------------------------------- /Chapter10/Structured/RegisterUser.cs: -------------------------------------------------------------------------------- 1 | namespace Chapter10.Structured; 2 | 3 | public record RegisterUser(string FirstName, string LastName, string SocialSecurityNumber, string UserName, string Password); 4 | -------------------------------------------------------------------------------- /Chapter10/Structured/User.cs: -------------------------------------------------------------------------------- 1 | namespace Chapter10.Structured; 2 | 3 | public record User(Guid Id, string UserName, string Password); 4 | -------------------------------------------------------------------------------- /Chapter10/Structured/UserDetails.cs: -------------------------------------------------------------------------------- 1 | namespace Chapter10.Structured; 2 | 3 | public record UserDetails(Guid Id, Guid UserId, string FirstName, string LastName, string SocialSecurityNumber); 4 | -------------------------------------------------------------------------------- /Chapter10/Structured/UserDetailsService.cs: -------------------------------------------------------------------------------- 1 | using Fundamentals; 2 | 3 | namespace Chapter10.Structured; 4 | 5 | [Singleton] 6 | public class UserDetailsService : IUserDetailsService 7 | { 8 | readonly IDatabase _database; 9 | 10 | public UserDetailsService(IDatabase database) 11 | { 12 | _database = database; 13 | } 14 | 15 | public Task Register(string firstName, string lastName, string socialSecurityNumber, Guid userId) 16 | => _database.GetCollectionFor().InsertOneAsync(new(Guid.NewGuid(), userId, firstName, lastName, socialSecurityNumber)); 17 | } 18 | -------------------------------------------------------------------------------- /Chapter10/Structured/UsersController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace Chapter10.Structured; 4 | 5 | [Route("/api/users")] 6 | public class UsersController : Controller 7 | { 8 | readonly IUsersService _usersService; 9 | readonly IUserDetailsService _userDetailsService; 10 | 11 | public UsersController(IUsersService usersService, IUserDetailsService userDetailsService) 12 | { 13 | _usersService = usersService; 14 | _userDetailsService = userDetailsService; 15 | } 16 | 17 | [HttpPost("register")] 18 | public async Task Register([FromBody] RegisterUser userRegistration) 19 | { 20 | var userId = await _usersService.Register(userRegistration.UserName, userRegistration.Password); 21 | await _userDetailsService.Register( 22 | userRegistration.FirstName, 23 | userRegistration.LastName, 24 | userRegistration.SocialSecurityNumber, 25 | userId); 26 | } 27 | } -------------------------------------------------------------------------------- /Chapter10/Structured/UsersService.cs: -------------------------------------------------------------------------------- 1 | using Fundamentals; 2 | 3 | namespace Chapter10.Structured; 4 | 5 | [Singleton] 6 | public class UsersService : IUsersService 7 | { 8 | readonly IDatabase _database; 9 | 10 | public UsersService(IDatabase database) 11 | { 12 | _database = database; 13 | } 14 | 15 | public async Task Register(string userName, string password) 16 | { 17 | var user = new User(Guid.NewGuid(), userName, password); 18 | await _database.GetCollectionFor().InsertOneAsync(user); 19 | return user.Id; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Chapter10/Unstructured/UsersController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using MongoDB.Driver; 3 | 4 | namespace Chapter10.Unstructured; 5 | 6 | public record RegisterUser(string FirstName, string LastName, string SocialSecurityNumber, string UserName, string Password); 7 | 8 | public record User(Guid Id, string UserName, string Password); 9 | 10 | public record UserDetails(Guid Id, Guid UserId, string FirstName, string LastName, string SocialSecurityNumber); 11 | 12 | [Route("/api/unstructured/users")] 13 | public class UsersController : Controller 14 | { 15 | IMongoCollection _userCollection; 16 | IMongoCollection _userDetailsCollection; 17 | 18 | public UsersController() 19 | { 20 | var client = new MongoClient("mongodb://localhost:27017"); 21 | var database = client.GetDatabase("TheSystem"); 22 | _userCollection = database.GetCollection("users"); 23 | _userDetailsCollection = database.GetCollection("user-details"); 24 | } 25 | 26 | [HttpPost("register")] 27 | public async Task Register([FromBody] RegisterUser userRegistration) 28 | { 29 | var user = new User(Guid.NewGuid(), userRegistration.UserName, userRegistration.Password); 30 | var userDetails = new UserDetails(Guid.NewGuid(), user.Id, userRegistration.FirstName, userRegistration.LastName, userRegistration.SocialSecurityNumber); 31 | await _userCollection.InsertOneAsync(user); 32 | await _userDetailsCollection.InsertOneAsync(userDetails); 33 | } 34 | } -------------------------------------------------------------------------------- /Chapter10/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Chapter10/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /Chapter11/Chapter11.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Exe 13 | net7.0 14 | enable 15 | enable 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Chapter11/ConfidentialAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Chapter11; 2 | 3 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] 4 | public sealed class ConfidentialAttribute : Attribute 5 | { 6 | } 7 | -------------------------------------------------------------------------------- /Chapter11/ConfidentialMetadataProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Fundamentals.Compliance; 3 | 4 | namespace Chapter11; 5 | 6 | public class ConfidentialMetadataProvider : ICanProvideComplianceMetadataForType 7 | { 8 | public bool CanProvide(Type type) => type.GetCustomAttribute() != null; 9 | 10 | public ComplianceMetadata Provide(Type type) => new("8dd1709a-bbe1-4b98-84e1-9e7be2fd4912", "The data is confidential"); 11 | } -------------------------------------------------------------------------------- /Chapter11/JournalEntry.cs: -------------------------------------------------------------------------------- 1 | namespace Chapter11; 2 | 3 | public class JournalEntry 4 | { 5 | public string Title { get; set; } = string.Empty; 6 | 7 | public string Content { get; set; } = string.Empty; 8 | } 9 | -------------------------------------------------------------------------------- /Chapter11/JournalEntryMetadataProvider.cs: -------------------------------------------------------------------------------- 1 | using Fundamentals.Compliance; 2 | 3 | namespace Chapter11; 4 | 5 | public class JournalEntryMetadataProvider : ICanProvideComplianceMetadataForType 6 | { 7 | public bool CanProvide(Type type) => type == typeof(JournalEntry); 8 | 9 | public ComplianceMetadata Provide(Type type) => new("7242aed8-8d70-49df-8713-eea45e2764d4", "Journal entry"); 10 | } -------------------------------------------------------------------------------- /Chapter11/Patient.cs: -------------------------------------------------------------------------------- 1 | using Fundamentals.Compliance.GDPR; 2 | 3 | namespace Chapter11; 4 | 5 | [Confidential] 6 | public class Patient 7 | { 8 | [PersonalIdentifiableInformation("Employment records")] 9 | public string FirstName { get; set; } = string.Empty; 10 | 11 | [PersonalIdentifiableInformation("Employment records")] 12 | public string LastName { get; set; } = string.Empty; 13 | 14 | [PersonalIdentifiableInformation("Uniquely identifies the employee")] 15 | public string SocialSecurityNumber { get; set; } = string.Empty; 16 | 17 | public IEnumerable JournalEntries { get; set; } = Enumerable.Empty(); 18 | } 19 | -------------------------------------------------------------------------------- /Chapter11/Program.cs: -------------------------------------------------------------------------------- 1 | using Chapter11; 2 | using Fundamentals; 3 | using Fundamentals.Compliance; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | 7 | var host = Host.CreateDefaultBuilder() 8 | .ConfigureServices((context, services) => 9 | { 10 | var types = new Types(); 11 | services.AddSingleton(types); 12 | services.AddBindingsByConvention(types); 13 | services.AddSelfBinding(types); 14 | }) 15 | .Build(); 16 | 17 | var complianceMetadataResolver = host.Services.GetRequiredService(); 18 | 19 | var typeToCheck = typeof(Patient); 20 | Console.WriteLine($"Checking type for compliance rules: {typeToCheck.FullName}"); 21 | 22 | if (complianceMetadataResolver.HasMetadataFor(typeToCheck)) 23 | { 24 | var metadata = complianceMetadataResolver.GetMetadataFor(typeToCheck); 25 | foreach (var item in metadata) 26 | { 27 | Console.WriteLine($"Type level - {item.Details}"); 28 | } 29 | } 30 | 31 | Console.WriteLine(""); 32 | 33 | foreach (var property in typeToCheck.GetProperties()) 34 | { 35 | if (complianceMetadataResolver.HasMetadataFor(property)) 36 | { 37 | var metadata = complianceMetadataResolver.GetMetadataFor(property); 38 | foreach (var item in metadata) 39 | { 40 | Console.WriteLine($"Property: {property.Name} - {item.Details}"); 41 | } 42 | } 43 | else if (complianceMetadataResolver.HasMetadataFor(property.PropertyType)) 44 | { 45 | var metadata = complianceMetadataResolver.GetMetadataFor(property.PropertyType); 46 | foreach (var item in metadata) 47 | { 48 | Console.WriteLine($"Property: {property.Name} - {item.Details}"); 49 | } 50 | } 51 | else if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition().IsAssignableTo(typeof(IEnumerable<>))) 52 | { 53 | var type = property.PropertyType.GetGenericArguments().First(); 54 | if (complianceMetadataResolver.HasMetadataFor(type)) 55 | { 56 | Console.WriteLine($"\nProperty {property.Name} is a collection of type {type.FullName} with type level metadata"); 57 | 58 | var metadata = complianceMetadataResolver.GetMetadataFor(type); 59 | foreach (var item in metadata) 60 | { 61 | Console.WriteLine($"{property.Name} - {item.Details}"); 62 | } 63 | } 64 | } 65 | } 66 | 67 | -------------------------------------------------------------------------------- /Chapter12/AccountBalance.cs: -------------------------------------------------------------------------------- 1 | using EventSourcing; 2 | 3 | namespace Chapter12; 4 | 5 | [Observer] 6 | public class AccountBalance 7 | { 8 | public Task DepositPerformed(DepositPerformed @event, EventContext context) 9 | { 10 | Console.WriteLine($"Deposit of {@event.Amount} performed on {context.EventSourceId}"); 11 | return Task.CompletedTask; 12 | } 13 | 14 | public Task WithdrawalPerformed(WithdrawalPerformed @event, EventContext context) 15 | { 16 | Console.WriteLine($"Withdrawal of {@event.Amount} performed on {context.EventSourceId}"); 17 | return Task.CompletedTask; 18 | } 19 | } -------------------------------------------------------------------------------- /Chapter12/AccountLifecycle.cs: -------------------------------------------------------------------------------- 1 | using EventSourcing; 2 | 3 | namespace Chapter12; 4 | 5 | [Observer] 6 | public class AccountLifecycle 7 | { 8 | public Task Opened(BankAccountOpened @event) 9 | { 10 | Console.WriteLine($"Account opened for {@event.CustomerName}"); 11 | return Task.CompletedTask; 12 | } 13 | 14 | public Task Closed(BankAccountClosed @event, EventContext context) 15 | { 16 | Console.WriteLine($"Account with id {context.EventSourceId} closed"); 17 | return Task.CompletedTask; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Chapter12/Chapter12.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Exe 13 | net7.0 14 | enable 15 | enable 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Chapter12/EventSourcing/EventContext.cs: -------------------------------------------------------------------------------- 1 | namespace EventSourcing; 2 | 3 | public record EventContext( 4 | EventSourceId EventSourceId, 5 | EventSequenceNumber SequenceNumber, 6 | DateTimeOffset Occurred); 7 | -------------------------------------------------------------------------------- /Chapter12/EventSourcing/EventLog.cs: -------------------------------------------------------------------------------- 1 | namespace EventSourcing; 2 | 3 | public class EventLog : IEventLog 4 | { 5 | readonly IObservers _observers; 6 | EventSequenceNumber _sequenceNumber = 0; 7 | 8 | public EventLog(IObservers observers) 9 | { 10 | _observers = observers; 11 | } 12 | 13 | public async Task Append(EventSourceId eventSourceId, IEvent @event) 14 | { 15 | await _observers.OnNext( 16 | @event, 17 | new EventContext(eventSourceId, _sequenceNumber, DateTimeOffset.UtcNow)); 18 | _sequenceNumber++; 19 | } 20 | } -------------------------------------------------------------------------------- /Chapter12/EventSourcing/EventSequenceNumber.cs: -------------------------------------------------------------------------------- 1 | using Fundamentals; 2 | 3 | namespace EventSourcing; 4 | 5 | public record EventSequenceNumber(ulong Value) : ConceptAs(Value) 6 | { 7 | public static implicit operator EventSequenceNumber(ulong value) => new(value); 8 | } 9 | -------------------------------------------------------------------------------- /Chapter12/EventSourcing/EventSourceId.cs: -------------------------------------------------------------------------------- 1 | using Fundamentals; 2 | 3 | namespace EventSourcing; 4 | 5 | public record EventSourceId(string Value) : ConceptAs(Value) 6 | { 7 | public static EventSourceId New() => new(Guid.NewGuid().ToString()); 8 | } -------------------------------------------------------------------------------- /Chapter12/EventSourcing/IEvent.cs: -------------------------------------------------------------------------------- 1 | namespace EventSourcing; 2 | 3 | public interface IEvent { } 4 | -------------------------------------------------------------------------------- /Chapter12/EventSourcing/IEventLog.cs: -------------------------------------------------------------------------------- 1 | namespace EventSourcing; 2 | 3 | public interface IEventLog 4 | { 5 | Task Append(EventSourceId eventSourceId, IEvent @event); 6 | } 7 | -------------------------------------------------------------------------------- /Chapter12/EventSourcing/IObservers.cs: -------------------------------------------------------------------------------- 1 | namespace EventSourcing; 2 | 3 | public interface IObservers 4 | { 5 | Task OnNext(IEvent @event, EventContext context); 6 | } 7 | -------------------------------------------------------------------------------- /Chapter12/EventSourcing/ObserverAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace EventSourcing; 2 | 3 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] 4 | public sealed class ObserverAttribute : Attribute 5 | { 6 | } 7 | -------------------------------------------------------------------------------- /Chapter12/EventSourcing/ObserverHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace EventSourcing; 4 | 5 | public class ObserverHandler 6 | { 7 | readonly Dictionary> _methodsByEventType; 8 | readonly IServiceProvider _serviceProvider; 9 | readonly Type _targetType; 10 | 11 | public IEnumerable EventTypes => _methodsByEventType.Keys; 12 | 13 | public ObserverHandler(IServiceProvider serviceProvider, Type targetType) 14 | { 15 | _serviceProvider = serviceProvider; 16 | _targetType = targetType; 17 | 18 | _methodsByEventType = targetType.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) 19 | .Where(_ => IsObservingMethod(_)) 20 | .GroupBy(_ => _.GetParameters()[0].ParameterType) 21 | .ToDictionary(_ => _.Key, _ => _.ToArray().AsEnumerable()); 22 | } 23 | 24 | public async Task OnNext(IEvent @event, EventContext context) 25 | { 26 | var eventType = @event.GetType(); 27 | 28 | if (_methodsByEventType.ContainsKey(eventType)) 29 | { 30 | var actualObserver = _serviceProvider.GetService(_targetType); 31 | Task returnValue; 32 | foreach (var method in _methodsByEventType[eventType]) 33 | { 34 | var parameters = method.GetParameters(); 35 | 36 | if (parameters.Length == 2) 37 | { 38 | returnValue = (Task)method.Invoke(actualObserver, new object[] { @event, context })!; 39 | } 40 | else 41 | { 42 | returnValue = (Task)method.Invoke(actualObserver, new object[] { @event })!; 43 | } 44 | 45 | if (returnValue is not null) await returnValue; 46 | } 47 | } 48 | } 49 | 50 | bool IsObservingMethod(MethodInfo methodInfo) 51 | { 52 | var isObservingMethod = methodInfo.ReturnType.IsAssignableTo(typeof(Task)) || 53 | methodInfo.ReturnType == typeof(void); 54 | 55 | if (!isObservingMethod) return false; 56 | var parameters = methodInfo.GetParameters(); 57 | if (parameters.Length >= 1) 58 | { 59 | isObservingMethod = parameters[0].ParameterType.IsAssignableTo(typeof(IEvent)); 60 | if (parameters.Length == 2) 61 | { 62 | isObservingMethod &= parameters[1].ParameterType == typeof(EventContext); 63 | } 64 | else if (parameters.Length > 2) 65 | { 66 | isObservingMethod = false; 67 | } 68 | return isObservingMethod; 69 | } 70 | 71 | return false; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Chapter12/EventSourcing/Observers.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Fundamentals; 3 | 4 | namespace EventSourcing; 5 | 6 | [Singleton] 7 | public class Observers : IObservers 8 | { 9 | readonly IEnumerable _handlers; 10 | 11 | public Observers(ITypes types, IServiceProvider serviceProvider) 12 | { 13 | _handlers = types.All.Where(_ => _.HasAttribute()) 14 | .Select(_ => 15 | { 16 | var observer = _.GetCustomAttribute()!; 17 | return new ObserverHandler(serviceProvider, _); 18 | }); 19 | } 20 | 21 | public Task OnNext(IEvent @event, EventContext context) 22 | { 23 | var tasks = _handlers.Where(_ => _.EventTypes.Contains(@event.GetType())) 24 | .Select(_ => _.OnNext(@event, context)); 25 | return Task.WhenAll(tasks); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Chapter12/Events.cs: -------------------------------------------------------------------------------- 1 | using EventSourcing; 2 | 3 | namespace Chapter12; 4 | 5 | public record BankAccountOpened(string CustomerName) : IEvent; 6 | public record BankAccountClosed() : IEvent; 7 | public record DepositPerformed(decimal Amount) : IEvent; 8 | public record WithdrawalPerformed(decimal Amount) : IEvent; 9 | -------------------------------------------------------------------------------- /Chapter12/Program.cs: -------------------------------------------------------------------------------- 1 | using Chapter12; 2 | using EventSourcing; 3 | using Fundamentals; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | 7 | var host = Host.CreateDefaultBuilder() 8 | .ConfigureServices((context, services) => 9 | { 10 | var types = new Types(); 11 | services.AddSingleton(types); 12 | services.AddBindingsByConvention(types); 13 | services.AddSelfBinding(types); 14 | }) 15 | .Build(); 16 | 17 | var eventLog = host.Services.GetRequiredService(); 18 | 19 | var bankAccountId = EventSourceId.New(); 20 | eventLog.Append(bankAccountId, new BankAccountOpened("Jane Doe")); 21 | eventLog.Append(bankAccountId, new DepositPerformed(100)); 22 | eventLog.Append(bankAccountId, new WithdrawalPerformed(32)); 23 | eventLog.Append(bankAccountId, new BankAccountClosed()); 24 | -------------------------------------------------------------------------------- /Chapter13/AdminForNamespace.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | 3 | namespace Chapter13; 4 | 5 | public class AdminForNamespace : IAuthorizationRequirement 6 | { 7 | public AdminForNamespace(string @namespace) 8 | { 9 | Namespace = @namespace; 10 | } 11 | 12 | public string Namespace { get; } 13 | } -------------------------------------------------------------------------------- /Chapter13/AdminForNamespaceHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc.Controllers; 3 | 4 | namespace Chapter13; 5 | 6 | public class AdminForNamespaceHandler : AuthorizationHandler 7 | { 8 | protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AdminForNamespace requirement) 9 | { 10 | if (context.Resource is HttpContext httpContext) 11 | { 12 | var endpoint = httpContext.GetEndpoint(); 13 | if (endpoint is not null) 14 | { 15 | var controllerActionDescriptor = endpoint!.Metadata.GetMetadata(); 16 | if (controllerActionDescriptor? 17 | .MethodInfo 18 | .DeclaringType? 19 | .Namespace? 20 | .StartsWith(requirement.Namespace, StringComparison.InvariantCulture) == true && 21 | !httpContext.User.IsInRole("Admin")) 22 | { 23 | context.Fail(); 24 | } 25 | else 26 | { 27 | context.Succeed(requirement); 28 | } 29 | } 30 | } 31 | return Task.CompletedTask; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Chapter13/Chapter13.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | net7.0 9 | enable 10 | enable 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Chapter13/Commands/CommandActionFilter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.Filters; 3 | 4 | namespace Chapter13.Commands; 5 | 6 | public class CommandActionFilter : IAsyncActionFilter 7 | { 8 | public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) 9 | { 10 | if (context.HttpContext.Request.Method == HttpMethod.Post.Method) 11 | { 12 | var authorizationResult = context.HttpContext.GetAuthorizationResult(); 13 | var isAuthorized = authorizationResult?.Succeeded ?? true; 14 | 15 | var exceptionMessages = new List(); 16 | var exceptionStackTrace = string.Empty; 17 | ActionExecutedContext? result = null; 18 | object? response = null; 19 | if (context.ModelState.IsValid && isAuthorized) 20 | { 21 | result = await next(); 22 | 23 | if (result.Exception is not null) 24 | { 25 | var exception = result.Exception; 26 | exceptionStackTrace = exception.StackTrace; 27 | 28 | do 29 | { 30 | exceptionMessages.Add(exception.Message); 31 | exception = exception.InnerException; 32 | } 33 | while (exception is not null); 34 | 35 | result.Exception = null!; 36 | } 37 | 38 | if (result.Result is ObjectResult objectResult) 39 | { 40 | response = objectResult.Value; 41 | } 42 | } 43 | 44 | var commandResult = new CommandResult 45 | { 46 | CorrelationId = Guid.NewGuid(), 47 | IsAuthorized = isAuthorized, 48 | ValidationResults = context.ModelState.SelectMany(_ => _.Value!.Errors.Select(e => e.ToValidationResult(_.Key))), 49 | ExceptionMessages = exceptionMessages.ToArray(), 50 | ExceptionStackTrace = exceptionStackTrace ?? string.Empty, 51 | Response = response 52 | }; 53 | 54 | if (!commandResult.IsAuthorized) 55 | { 56 | context.HttpContext.Response.StatusCode = 401; // Forbidden: https://www.rfc-editor.org/rfc/rfc9110.html#name-401-unauthorized 57 | } 58 | else if (!commandResult.IsValid) 59 | { 60 | context.HttpContext.Response.StatusCode = 409; // Conflict: https://www.rfc-editor.org/rfc/rfc9110.html#name-409-conflict 61 | } 62 | else if (commandResult.HasExceptions) 63 | { 64 | context.HttpContext.Response.StatusCode = 500; // Internal Server error: https://www.rfc-editor.org/rfc/rfc9110.html#name-500-internal-server-error 65 | } 66 | 67 | var actualResult = new ObjectResult(commandResult); 68 | 69 | if (result is not null) 70 | { 71 | result.Result = actualResult; 72 | } 73 | else 74 | { 75 | context.Result = actualResult; 76 | } 77 | } 78 | else 79 | { 80 | await next(); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Chapter13/Commands/CommandResult.cs: -------------------------------------------------------------------------------- 1 | namespace Chapter13.Commands; 2 | 3 | public class CommandResult 4 | { 5 | public Guid CorrelationId { get; init; } 6 | public bool IsSuccess => IsAuthorized && IsValid && !HasExceptions; 7 | public bool IsAuthorized { get; init; } = true; 8 | public bool IsValid => !ValidationResults.Any(); 9 | public bool HasExceptions => ExceptionMessages.Any(); 10 | public IEnumerable ValidationResults { get; init; } = Enumerable.Empty(); 11 | public IEnumerable ExceptionMessages { get; init; } = Enumerable.Empty(); 12 | public string ExceptionStackTrace { get; init; } = string.Empty; 13 | public object? Response { get; init; } 14 | } 15 | -------------------------------------------------------------------------------- /Chapter13/Commands/ModelErrorExtensions.cs: -------------------------------------------------------------------------------- 1 | using Fundamentals; 2 | using Microsoft.AspNetCore.Mvc.ModelBinding; 3 | 4 | namespace Chapter13.Commands; 5 | 6 | public static class ModelErrorExtensions 7 | { 8 | public static ValidationResult ToValidationResult(this ModelError error, string member) 9 | { 10 | member = string.Join('.', member.Split('.').Select(_ => _.ToCamelCase())); 11 | return new ValidationResult(error.ErrorMessage, member); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Chapter13/Commands/ValidationResult.cs: -------------------------------------------------------------------------------- 1 | namespace Chapter13.Commands; 2 | 3 | public record ValidationResult(string Message, string Member); 4 | -------------------------------------------------------------------------------- /Chapter13/CrossCuttingAuthorizationMiddlewareResultHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Authorization.Policy; 3 | 4 | namespace Chapter13; 5 | 6 | public class CrossCuttingAuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler 7 | { 8 | readonly AuthorizationMiddlewareResultHandler _defaultHandler = new(); 9 | 10 | public async Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult) 11 | { 12 | context.SetAuthorizationResult(authorizeResult); 13 | await _defaultHandler.HandleAsync(next, context, policy, PolicyAuthorizationResult.Success()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Chapter13/CrossCuttingPoliciesProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | 3 | namespace Chapter13; 4 | 5 | public class CrossCuttingPoliciesProvider : IAuthorizationPolicyProvider 6 | { 7 | readonly AuthorizationPolicy _policy; 8 | 9 | public CrossCuttingPoliciesProvider() 10 | { 11 | _policy = new AuthorizationPolicyBuilder() 12 | .AddRequirements(new AdminForNamespace("Chapter13") 13 | ).Build(); 14 | } 15 | 16 | public Task GetDefaultPolicyAsync() => Task.FromResult(_policy); 17 | public Task GetFallbackPolicyAsync() => Task.FromResult(_policy); 18 | public Task GetPolicyAsync(string policyName) => Task.FromResult(_policy); 19 | } 20 | -------------------------------------------------------------------------------- /Chapter13/Employee.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Chapter13; 4 | 5 | public record Employee( 6 | [Required] 7 | string FirstName, 8 | 9 | [Required] 10 | string LastName); 11 | -------------------------------------------------------------------------------- /Chapter13/EmployeesController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace Chapter13; 4 | 5 | [Route("/api/employees")] 6 | public class EmployeesController : Controller 7 | { 8 | [HttpPost] 9 | public int Register([FromBody] Employee employee) 10 | { 11 | return 1; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Chapter13/HardCodedAuthenticationHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using System.Text.Encodings.Web; 3 | using Microsoft.AspNetCore.Authentication; 4 | using Microsoft.Extensions.Options; 5 | 6 | namespace Chapter13; 7 | 8 | public class HardCodedAuthenticationHandler : AuthenticationHandler 9 | { 10 | public const string SchemeName = "HardCodedAuthenticationHandler"; 11 | 12 | public HardCodedAuthenticationHandler( 13 | IOptionsMonitor options, 14 | ILoggerFactory logger, 15 | UrlEncoder encoder, 16 | ISystemClock clock) : base(options, logger, encoder, clock) 17 | { 18 | } 19 | 20 | protected override Task HandleAuthenticateAsync() => Task.FromResult( 21 | AuthenticateResult.Success( 22 | new AuthenticationTicket( 23 | new ClaimsPrincipal( 24 | new ClaimsIdentity( 25 | new[] 26 | { 27 | new Claim(ClaimTypes.Name, "Bob"), 28 | new Claim(ClaimTypes.Role, "User") 29 | }, 30 | SchemeName)), SchemeName))); 31 | } 32 | -------------------------------------------------------------------------------- /Chapter13/HardCodedAuthenticationOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication; 2 | 3 | namespace Chapter13; 4 | 5 | public class HardCodedAuthenticationOptions : AuthenticationSchemeOptions 6 | { 7 | } 8 | -------------------------------------------------------------------------------- /Chapter13/HttpContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization.Policy; 2 | 3 | namespace Chapter13; 4 | 5 | public static class HttpContextExtensions 6 | { 7 | const string AuthorizeResultKey = "_AuthorizeResult"; 8 | 9 | public static PolicyAuthorizationResult? GetAuthorizationResult(this HttpContext context) => (context.Items[AuthorizeResultKey] as PolicyAuthorizationResult)!; 10 | 11 | public static void SetAuthorizationResult(this HttpContext context, PolicyAuthorizationResult result) => context.Items[AuthorizeResultKey] = result; 12 | } -------------------------------------------------------------------------------- /Chapter13/Program.cs: -------------------------------------------------------------------------------- 1 | using Chapter13; 2 | using Chapter13.Commands; 3 | using Microsoft.AspNetCore.Authorization; 4 | 5 | var builder = WebApplication.CreateBuilder(args); 6 | builder.Services.AddControllers(mvcOptions => mvcOptions.Filters.Add()); 7 | 8 | builder.Services.AddAuthorization(options => options.AddPolicy("Chapter13Admins", policy => policy.Requirements.Add(new AdminForNamespace("Chapter13")))); 9 | builder.Services.AddSingleton(); 10 | builder.Services.AddSingleton(); 11 | builder.Services.AddSingleton(); 12 | builder.Services 13 | .AddAuthentication(options => options.DefaultScheme = HardCodedAuthenticationHandler.SchemeName) 14 | .AddScheme(HardCodedAuthenticationHandler.SchemeName, _ => {}); 15 | 16 | var app = builder.Build(); 17 | app.MapControllers(); 18 | 19 | app.UseAuthentication(); 20 | app.UseAuthorization(); 21 | 22 | app.Run(); 23 | -------------------------------------------------------------------------------- /Chapter13/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Chapter13/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /Chapter14/Authenticator.cs: -------------------------------------------------------------------------------- 1 | namespace Chapter14; 2 | 3 | public class Authenticator : IAuthenticator 4 | { 5 | public bool Authenticate(string username, string password) 6 | { 7 | return true; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Chapter14/AuthorizationInterceptor.cs: -------------------------------------------------------------------------------- 1 | using Castle.DynamicProxy; 2 | 3 | namespace Chapter14; 4 | 5 | public class AuthorizationInterceptor : IInterceptor 6 | { 7 | readonly IUsersServiceComposition _usersService; 8 | 9 | public AuthorizationInterceptor(IUsersServiceComposition usersService) 10 | { 11 | _usersService = usersService; 12 | } 13 | 14 | public void Intercept(IInvocation invocation) 15 | { 16 | if (_usersService.IsAuthorized("jane@doe.io", invocation.Method.Name)) 17 | { 18 | invocation.Proceed(); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Chapter14/Authorizer.cs: -------------------------------------------------------------------------------- 1 | namespace Chapter14; 2 | 3 | public class Authorizer : IAuthorizer 4 | { 5 | public bool IsAuthorized(string username, string action) 6 | { 7 | return false; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Chapter14/Chapter14.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net7.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Chapter14/DefaultInstaller.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Castle.DynamicProxy; 3 | using Castle.MicroKernel.Registration; 4 | using Castle.MicroKernel.SubSystems.Configuration; 5 | using Castle.Windsor; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace Chapter14; 9 | 10 | public class DefaultInstaller : IWindsorInstaller 11 | { 12 | public void Install(IWindsorContainer container, IConfigurationStore store) 13 | { 14 | container.Register(Component.For()); 15 | container.Register(Component.For()); 16 | 17 | container.Register( 18 | Component.For() 19 | .ImplementedBy() 20 | .LifestyleTransient()); 21 | 22 | container.Register( 23 | Component.For() 24 | .ImplementedBy() 25 | .LifestyleTransient()); 26 | 27 | container.Register( 28 | Component.For() 29 | .ImplementedBy() 30 | .Proxy.AdditionalInterfaces(typeof(IAuthorizer), typeof(IAuthenticator)) 31 | .Proxy.MixIns(_ => _ 32 | .Component() 33 | .Component()) 34 | .Interceptors() 35 | .Interceptors() 36 | .SelectInterceptorsWith(s => s.Service()) 37 | .LifestyleTransient()); 38 | 39 | container.Register( 40 | Component.For() 41 | .UsingFactoryMethod((kernel, context) => 42 | { 43 | var proxyGenerator = new ProxyGenerator(); 44 | var proxyGenerationOptions = new ProxyGenerationOptions(); 45 | proxyGenerationOptions.AddMixinInstance(container.Resolve()); 46 | proxyGenerationOptions.AddMixinInstance(container.Resolve()); 47 | var logger = container.Resolve>(); 48 | proxyGenerationOptions.AddMixinInstance(new UsersService(logger)); 49 | return (proxyGenerator.CreateClassProxyWithTarget( 50 | typeof(object), 51 | new[] { typeof(IUsersServiceComposition) }, 52 | new object(), 53 | proxyGenerationOptions) as IUsersServiceComposition)!; 54 | })); 55 | 56 | container.Register(Component.For()); 57 | container.Register(Classes.FromAssemblyInThisApplication(Assembly.GetEntryAssembly()) 58 | .Pick() 59 | .WithService.DefaultInterfaces() 60 | .Configure(_ => _ 61 | .Interceptors() 62 | .Interceptors() 63 | .SelectInterceptorsWith(s => s.Service())) 64 | .LifestyleTransient()); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Chapter14/IAuthenticator.cs: -------------------------------------------------------------------------------- 1 | namespace Chapter14; 2 | 3 | public interface IAuthenticator 4 | { 5 | bool Authenticate(string username, string password); 6 | } 7 | -------------------------------------------------------------------------------- /Chapter14/IAuthorizer.cs: -------------------------------------------------------------------------------- 1 | namespace Chapter14; 2 | 3 | public interface IAuthorizer 4 | { 5 | bool IsAuthorized(string username, string action); 6 | } 7 | -------------------------------------------------------------------------------- /Chapter14/IUsersService.cs: -------------------------------------------------------------------------------- 1 | namespace Chapter14; 2 | 3 | public interface IUsersService 4 | { 5 | Task Register(string userName, string password); 6 | } 7 | -------------------------------------------------------------------------------- /Chapter14/IUsersServiceComposition.cs: -------------------------------------------------------------------------------- 1 | namespace Chapter14; 2 | 3 | public interface IUsersServiceComposition : IUsersService, IAuthenticator, IAuthorizer 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /Chapter14/InterceptorSelector.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Castle.DynamicProxy; 3 | 4 | namespace Chapter14; 5 | 6 | public class InterceptorSelector : IInterceptorSelector 7 | { 8 | public IInterceptor[] SelectInterceptors(Type type, MethodInfo method, IInterceptor[] interceptors) 9 | { 10 | if (type.Namespace?.StartsWith("Chapter14.Todo", StringComparison.InvariantCulture) ?? false) 11 | { 12 | return interceptors; 13 | } 14 | 15 | return interceptors.Where(_ => _.GetType() != typeof(AuthorizationInterceptor)).ToArray(); 16 | } 17 | } -------------------------------------------------------------------------------- /Chapter14/LoggingInterceptor.cs: -------------------------------------------------------------------------------- 1 | using Castle.DynamicProxy; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace Chapter14; 5 | 6 | public class LoggingInterceptor : IInterceptor 7 | { 8 | readonly ILoggerFactory _loggerFactory; 9 | 10 | public LoggingInterceptor(ILoggerFactory loggerFactory) 11 | { 12 | _loggerFactory = loggerFactory; 13 | } 14 | 15 | public void Intercept(IInvocation invocation) 16 | { 17 | var logger = _loggerFactory.CreateLogger(invocation.TargetType)!; 18 | logger.BeforeInvocation(invocation.Method.Name); 19 | 20 | try 21 | { 22 | invocation.Proceed(); 23 | 24 | if (invocation.ReturnValue is Task task) 25 | { 26 | task.ContinueWith(t => 27 | { 28 | if (t.IsFaulted) 29 | { 30 | logger.InvocationError(invocation.Method.Name, t.Exception!); 31 | } 32 | else 33 | { 34 | logger.AfterInvocation(invocation.Method.Name); 35 | } 36 | }); 37 | } 38 | else 39 | { 40 | logger.AfterInvocation(invocation.Method.Name); 41 | } 42 | } 43 | catch (Exception ex) 44 | { 45 | logger.InvocationError(invocation.Method.Name, ex); 46 | throw; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Chapter14/LoggingInterceptorLogMessages.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | 3 | namespace Chapter14; 4 | 5 | internal static partial class LoggingInterceptorLogMessages 6 | { 7 | [LoggerMessage(1, LogLevel.Information, "Before invoking {Method}", EventName = "BeforeInvocation")] 8 | internal static partial void BeforeInvocation(this ILogger logger, string method); 9 | 10 | [LoggerMessage(2, LogLevel.Error, "Error invoking {Method}", EventName = "InvocationError")] 11 | internal static partial void InvocationError(this ILogger logger, string method, Exception exception); 12 | 13 | [LoggerMessage(3, LogLevel.Information, "Before invoking {Method}", EventName = "AfterInvocation")] 14 | internal static partial void AfterInvocation(this ILogger logger, string method); 15 | } -------------------------------------------------------------------------------- /Chapter14/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Castle.MicroKernel.Registration; 3 | using Castle.Windsor; 4 | using Castle.Windsor.Installer; 5 | using Chapter14; 6 | using Chapter14.Todo; 7 | using Microsoft.Extensions.Logging; 8 | 9 | 10 | var container = new WindsorContainer(); 11 | 12 | var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); 13 | container.Register(Component.For().Instance(loggerFactory)); 14 | 15 | var createLoggerMethod = typeof(LoggerFactoryExtensions) 16 | .GetMethods(BindingFlags.Public | BindingFlags.Static) 17 | .First(_ => _.Name == nameof(LoggerFactory.CreateLogger) && _.IsGenericMethod); 18 | 19 | container.Register(Component.For().UsingFactoryMethod((kernel, context) => 20 | { 21 | var loggerFactory = kernel.Resolve(); 22 | return loggerFactory.CreateLogger(context.Handler.ComponentModel.Implementation); 23 | }).LifestyleTransient()); 24 | container.Register(Component.For(typeof(ILogger<>)).UsingFactoryMethod((kernel, context) => 25 | { 26 | var loggerFactory = kernel.Resolve(); 27 | var logger = createLoggerMethod.MakeGenericMethod(context.RequestedType.GenericTypeArguments[0]).Invoke(null, new[] { loggerFactory }); 28 | return logger; 29 | })); 30 | 31 | container.Install(FromAssembly.InThisApplication(Assembly.GetEntryAssembly())); 32 | 33 | var usersService = container.Resolve(); 34 | var result = await usersService.Register("jane@doe.io", "Password1"); 35 | 36 | var authenticated = (usersService as IAuthenticator)!.Authenticate("jane@doe.io", "Password1"); 37 | var authorized = (usersService as IAuthorizer)!.IsAuthorized("jane@doe.io", "Some Action"); 38 | Console.WriteLine($"Authenticated: {authenticated}"); 39 | Console.WriteLine($"Authorized: {authorized}"); 40 | 41 | var composition = container.Resolve(); 42 | authenticated = composition.Authenticate("jane@doe.io", "Password1"); 43 | authorized = composition.IsAuthorized("jane@doe.io", "Some Action"); 44 | Console.WriteLine($"Authenticated: {authenticated}"); 45 | Console.WriteLine($"Authorized: {authorized}"); 46 | 47 | var todo = container.Resolve(); 48 | todo.Add("Buy milk"); 49 | 50 | Console.ReadLine(); -------------------------------------------------------------------------------- /Chapter14/Todo/ITodoService.cs: -------------------------------------------------------------------------------- 1 | namespace Chapter14.Todo; 2 | 3 | public interface ITodoService 4 | { 5 | void Add(string item); 6 | } 7 | 8 | -------------------------------------------------------------------------------- /Chapter14/Todo/TodoService.cs: -------------------------------------------------------------------------------- 1 | namespace Chapter14.Todo; 2 | 3 | public class TodoService : ITodoService 4 | { 5 | public void Add(string item) 6 | { 7 | Console.WriteLine($"Adding '{item}' to the todo list"); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Chapter14/UsersService.cs: -------------------------------------------------------------------------------- 1 | 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace Chapter14; 5 | 6 | public class UsersService : IUsersService 7 | { 8 | readonly ILogger _logger; 9 | 10 | public UsersService(ILogger logger) 11 | { 12 | _logger = logger; 13 | } 14 | 15 | public Task Register(string userName, string password) 16 | { 17 | _logger.LogInformation("Inside register method"); 18 | var id = Guid.NewGuid(); 19 | return Task.FromResult(id); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Chapter15/Chapter15.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Chapter15/Program.cs: -------------------------------------------------------------------------------- 1 | // See https://aka.ms/new-console-template for more information 2 | Console.WriteLine("Hello, World!"); 3 | -------------------------------------------------------------------------------- /Chapter15/Something.cs: -------------------------------------------------------------------------------- 1 | namespace Chapter15; 2 | 3 | /// 4 | /// asdad. 5 | /// 6 | public class Something 7 | { 8 | readonly string _blah; 9 | 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | public Something() 14 | { 15 | _blah = "blah"; 16 | 17 | Console.WriteLine(_blah); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Chapter16/Chapter16.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Exe 7 | net7.0 8 | enable 9 | enable 10 | true 11 | $(MSBuildThisFileDirectory)GDPRReport.txt 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Chapter16/Employee.cs: -------------------------------------------------------------------------------- 1 | using Fundamentals.Compliance.GDPR; 2 | 3 | namespace Chapter16; 4 | 5 | public record Employee( 6 | [PersonalIdentifiableInformation("Needed for registration")] 7 | string FirstName, 8 | 9 | [PersonalIdentifiableInformation("Needed for registration")] 10 | string LastName, 11 | 12 | [PersonalIdentifiableInformation("Needed for uniquely identifying an employee")] 13 | string SocialSecurityNumber); -------------------------------------------------------------------------------- /Chapter16/EmployeesController.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Diagnostics.Metrics; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace Chapter16; 6 | 7 | [Route("/api/employees")] 8 | public class EmployeesController : Controller 9 | { 10 | static readonly Counter _registeredEmployees = Metrics.Meter.CreateCounter("RegisteredEmployees", "# of registered employees"); 11 | 12 | [HttpGet("manual")] 13 | public IActionResult RegisterManual() 14 | { 15 | var now = DateTimeOffset.UtcNow; 16 | var tags = new TagList(new ReadOnlySpan>(new KeyValuePair[] 17 | { 18 | new("Year", now.Year), 19 | new("Month", now.Month), 20 | new("Day", now.Day), 21 | })); 22 | 23 | _registeredEmployees.Add(1, tags); 24 | 25 | return Ok(); 26 | } 27 | 28 | [HttpGet] 29 | public IActionResult Register() 30 | { 31 | EmployeesControllerMetrics.RegisteredEmployees(DateOnly.FromDateTime(DateTime.UtcNow)); 32 | 33 | return Ok(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Chapter16/EmployeesControllerMetrics.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.Metrics; 2 | using Fundamentals.Metrics; 3 | 4 | namespace Chapter16; 5 | 6 | public static partial class EmployeesControllerMetrics 7 | { 8 | [Counter("RegisteredEmployees", "# of registered employees")] 9 | public static partial void RegisteredEmployees(DateOnly date); 10 | } -------------------------------------------------------------------------------- /Chapter16/GDPRReport.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Metaprogramming-in-C-Sharp/ac312276b9e01f8c6caa800fd1dab10ace942cc6/Chapter16/GDPRReport.txt -------------------------------------------------------------------------------- /Chapter16/Metrics.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.Metrics; 2 | 3 | namespace Chapter16; 4 | 5 | public static class Metrics 6 | { 7 | public static readonly Meter Meter = new("Chapter16"); 8 | } -------------------------------------------------------------------------------- /Chapter16/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.Metrics; 2 | using Fundamentals.Metrics; 3 | 4 | GlobalMetrics.Meter = new Meter("Chapter16"); 5 | 6 | var builder = WebApplication.CreateBuilder(args); 7 | builder.Services.AddControllers(); 8 | 9 | var app = builder.Build(); 10 | app.UseRouting(); 11 | app.MapControllers(); 12 | app.Run(); 13 | -------------------------------------------------------------------------------- /Chapter16/ProgramMetrics.cs: -------------------------------------------------------------------------------- 1 | using Fundamentals.Metrics; 2 | 3 | namespace ConsoleApp; 4 | 5 | public static partial class ProgramMetrics 6 | { 7 | [Counter("Started", "How many times we've started the program")] 8 | public static partial void Started(int something, string blabbedi, string theThird); 9 | } 10 | -------------------------------------------------------------------------------- /Chapter16/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Chapter16/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /Chapter16/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | dotnet clean 3 | dotnet build-server shutdown 4 | dotnet build -------------------------------------------------------------------------------- /Chapter17/Chapter17.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Exe 10 | net7.0 11 | enable 12 | enable 13 | $(MSBuildThisFileDirectory)GDPRReport.txt 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Chapter17/GDPRReport.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Metaprogramming-in-C-Sharp/ac312276b9e01f8c6caa800fd1dab10ace942cc6/Chapter17/GDPRReport.txt -------------------------------------------------------------------------------- /Chapter17/MyException.cs: -------------------------------------------------------------------------------- 1 | namespace Chapter17; 2 | 3 | public class MyException : Exception 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /Chapter17/Program.cs: -------------------------------------------------------------------------------- 1 | using Chapter17; 2 | 3 | throw new MyException(); -------------------------------------------------------------------------------- /Chapter2/Calculator.cs: -------------------------------------------------------------------------------- 1 | namespace Chapter2; 2 | 3 | public static class Calculator 4 | { 5 | public static int Add(int left, int right) => left + right; 6 | } 7 | -------------------------------------------------------------------------------- /Chapter2/CalculatorTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Chapter2; 4 | 5 | public class CalculatorTests 6 | { 7 | [Fact] 8 | public void Add() 9 | { 10 | // Arrange 11 | var left = 5; 12 | var right = 3; 13 | var expectedResult = 8; 14 | 15 | // Act 16 | var actualResult = Calculator.Add(left, right); 17 | Assert.Equal(expectedResult, actualResult); 18 | } 19 | } -------------------------------------------------------------------------------- /Chapter2/Chapter2.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net7.0 6 | enable 7 | enable 8 | 9 | true 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Chapter2/Person.cs: -------------------------------------------------------------------------------- 1 | namespace Chapter2; 2 | 3 | public class Person 4 | { 5 | public string FirstName { get; set; } 6 | public string LastName { get; set; } 7 | public string SocialSecurityNumber { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /Chapter2/Program.cs: -------------------------------------------------------------------------------- 1 | // See https://aka.ms/new-console-template for more information 2 | using Chapter2; 3 | 4 | Console.WriteLine("Hello, World!"); 5 | 6 | 7 | var person = new Person 8 | { 9 | FirstName = "Jane", 10 | LastName = "Doe", 11 | SocialSecurityNumber = "12345abcd" 12 | }; 13 | Console.WriteLine(Serializer.SerializeToJson(person)); -------------------------------------------------------------------------------- /Chapter2/Serializer.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace Chapter2; 4 | 5 | public static class Serializer 6 | { 7 | public static string SerializeToJson(object instance) 8 | { 9 | var stringBuilder = new StringBuilder(); 10 | var type = instance.GetType(); 11 | var properties = type.GetProperties(); 12 | stringBuilder.Append("{\n"); 13 | var first = true; 14 | 15 | foreach (var property in properties) 16 | { 17 | if (!first) 18 | { 19 | stringBuilder.Append(",\n"); 20 | } 21 | first = false; 22 | stringBuilder.Append($" \"{property.Name}\": \"{property.GetValue(instance)}\""); 23 | } 24 | 25 | stringBuilder.Append("\n}"); 26 | 27 | return stringBuilder.ToString(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Chapter3/Chapter3.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Chapter3/Employee.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Chapter3; 4 | 5 | public record Employee( 6 | [Required] 7 | string FirstName, 8 | 9 | [Required] 10 | string LastName); 11 | -------------------------------------------------------------------------------- /Chapter3/EmployeesController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace Chapter3; 4 | 5 | [Route("/api/employees")] 6 | public class EmployeesController : Controller 7 | { 8 | [HttpPost] 9 | public IActionResult Register(Employee employee) 10 | { 11 | return Ok(); 12 | } 13 | 14 | [HttpGet] 15 | public IEnumerable AllEmployees() 16 | { 17 | return new Employee[] 18 | { 19 | new("Jane", "Doe"), 20 | new("John", "Doe") 21 | }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Chapter3/Program.cs: -------------------------------------------------------------------------------- 1 | using Chapter3; 2 | var builder = WebApplication.CreateBuilder(args); 3 | builder.Services.AddControllers(mvcOptions => mvcOptions.Filters.Add()); 4 | var app = builder.Build(); 5 | app.MapControllers(); 6 | app.Run(); 7 | -------------------------------------------------------------------------------- /Chapter3/ValidationFilter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.Filters; 3 | 4 | namespace Chapter3; 5 | 6 | public class ValidationFilter : IAsyncActionFilter 7 | { 8 | public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) 9 | { 10 | if (context.ModelState.IsValid) 11 | { 12 | await next(); 13 | } 14 | else 15 | { 16 | context.Result = new BadRequestObjectResult(new ValidationProblemDetails(context.ModelState)); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /Chapter3/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Chapter3/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /Chapter3/employees: -------------------------------------------------------------------------------- 1 | HEllo world -------------------------------------------------------------------------------- /Chapter4/AssemblyDiscovery/AssemblyDiscovery.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net7.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Chapter4/AssemblyDiscovery/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | var assembly = Assembly.GetEntryAssembly(); 4 | Console.WriteLine(assembly!.FullName); 5 | var assemblies = assembly!.GetReferencedAssemblies(); 6 | var assemblyNames = string.Join(", ", assemblies.Select(_ => _.Name)); 7 | Console.WriteLine(assemblyNames); 8 | 9 | -------------------------------------------------------------------------------- /Chapter4/BusinessApp/Concepts/Concepts.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net7.0 4 | enable 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Chapter4/BusinessApp/Concepts/Employees/FirstName.cs: -------------------------------------------------------------------------------- 1 | using Fundamentals; 2 | 3 | namespace Concepts.Employees; 4 | 5 | public record FirstName(string Value) : PIIConceptAs(Value); 6 | -------------------------------------------------------------------------------- /Chapter4/BusinessApp/Concepts/Employees/LastName.cs: -------------------------------------------------------------------------------- 1 | using Fundamentals; 2 | 3 | namespace Concepts.Employees; 4 | 5 | public record LastName(string Value) : PIIConceptAs(Value); 6 | -------------------------------------------------------------------------------- /Chapter4/BusinessApp/Concepts/Employees/SocialSecurityNumber.cs: -------------------------------------------------------------------------------- 1 | using Fundamentals; 2 | 3 | namespace Concepts.Employees; 4 | 5 | public record SocialSecurityNumber(string Value) : PIIConceptAs(Value); -------------------------------------------------------------------------------- /Chapter4/BusinessApp/Domain/Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net7.0 4 | enable 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Chapter4/BusinessApp/Domain/Employees/RegisterEmployee.cs: -------------------------------------------------------------------------------- 1 | using Concepts.Employees; 2 | using Fundamentals; 3 | 4 | namespace Domain.Employees; 5 | 6 | public class RegisterEmployee : ICommand 7 | { 8 | public FirstName FirstName { get; set; } = new(string.Empty); 9 | public LastName LastName { get; set; } = new(string.Empty); 10 | public SocialSecurityNumber SocialSecurityNumber { get; set; } = new(string.Empty); 11 | } 12 | -------------------------------------------------------------------------------- /Chapter4/BusinessApp/Domain/Employees/SetSalaryLevelForEmployee.cs: -------------------------------------------------------------------------------- 1 | using Fundamentals; 2 | 3 | namespace Domain.Employees; 4 | 5 | public class SetSalaryLevelForEmployee : ICommand 6 | { 7 | public string SocialSecurityNumber { get; set; } = string.Empty; 8 | public decimal SalaryLevel { get; set; } 9 | } -------------------------------------------------------------------------------- /Chapter4/BusinessApp/Main/Main.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Exe 10 | net7.0 11 | enable 12 | enable 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Chapter4/BusinessApp/Main/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Fundamentals; 3 | 4 | var types = new Types(); 5 | var commands = types.FindMultiple(); 6 | var typeNames = string.Join("\n", commands.Select(_ => _.Name)); 7 | Console.WriteLine(typeNames); 8 | 9 | Console.WriteLine("\n\nGDPR Report"); 10 | var typesWithConcepts = types.All 11 | .SelectMany(_ => 12 | _.GetProperties() 13 | .Where(p => p.PropertyType.IsPIIConcept())) 14 | .GroupBy(_ => _.DeclaringType); 15 | 16 | foreach (var typeWithConcepts in typesWithConcepts) 17 | { 18 | Console.WriteLine($"Type: {typeWithConcepts.Key!.FullName}"); 19 | foreach (var property in typeWithConcepts) 20 | { 21 | Console.WriteLine($" Property : {property.Name}"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Chapter5/Chapter5.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net7.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Chapter5/Employee.cs: -------------------------------------------------------------------------------- 1 | using Fundamentals.Compliance.GDPR; 2 | 3 | namespace Chapter5; 4 | 5 | public class Employee 6 | { 7 | [PersonalIdentifiableInformation("Employment records")] 8 | public string FirstName { get; set; } = string.Empty; 9 | 10 | [PersonalIdentifiableInformation("Employment records")] 11 | public string LastName { get; set; } = string.Empty; 12 | 13 | [PersonalIdentifiableInformation("Uniquely identifies the employee")] 14 | public string SocialSecurityNumber { get; set; } = string.Empty; 15 | } 16 | -------------------------------------------------------------------------------- /Chapter5/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Fundamentals; 3 | 4 | var types = new Types(); 5 | 6 | var piiTypes = types.All.Where(_ => _ 7 | .GetMembers() 8 | .Any(m => m.HasAttribute())); 9 | var typeNames = string.Join("\n", piiTypes.Select(_ => _.FullName)); 10 | Console.WriteLine(typeNames); 11 | 12 | Console.WriteLine("\n\nGDPR Report"); 13 | var typesWithPII = types.All 14 | .SelectMany(_ => 15 | _.GetProperties() 16 | .Where(p => p.HasAttribute())) 17 | .GroupBy(_ => _.DeclaringType); 18 | 19 | foreach (var typeWithPII in typesWithPII) 20 | { 21 | Console.WriteLine($"Type: {typeWithPII.Key!.FullName}"); 22 | foreach (var property in typeWithPII) 23 | { 24 | var pii = property.GetCustomAttribute(); 25 | Console.WriteLine($" Property : {property.Name}"); 26 | Console.WriteLine($" Reason : {pii!.ReasonForCollecting}"); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Chapter6/Chapter6.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net7.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Chapter6/Employee.cs: -------------------------------------------------------------------------------- 1 | namespace Chapter6; 2 | 3 | public class Employee 4 | { 5 | [NotifyChangesFor(nameof(FullName))] 6 | public virtual string FirstName { get; set; } = string.Empty; 7 | 8 | [NotifyChangesFor(nameof(FullName))] 9 | public virtual string LastName { get; set; } = string.Empty; 10 | 11 | public virtual string FullName => $"{FirstName} {LastName}"; 12 | } -------------------------------------------------------------------------------- /Chapter6/IgnoreChangesAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Chapter6; 2 | 3 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] 4 | public class IgnoreChangesAttribute : Attribute 5 | { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /Chapter6/MyTypeGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Reflection.Emit; 3 | 4 | namespace Chapter6; 5 | 6 | public class MyTypeGenerator 7 | { 8 | public static Type Generate() 9 | { 10 | var name = new AssemblyName("MyDynamicAssembly"); 11 | var assembly = AssemblyBuilder.DefineDynamicAssembly(name, AssemblyBuilderAccess.Run); 12 | var module = assembly.DefineDynamicModule("MyDynamicModule"); 13 | var typeBuilder = module.DefineType("MyType", TypeAttributes.Public | TypeAttributes.Class); 14 | var methodBuilder = typeBuilder.DefineMethod("SaySomething", MethodAttributes.Public); 15 | methodBuilder.SetParameters(typeof(string)); 16 | methodBuilder.DefineParameter(0, ParameterAttributes.None, "message"); 17 | var methodILGenerator = methodBuilder.GetILGenerator(); 18 | 19 | var consoleType = typeof(Console); 20 | var writeLineMethod = consoleType.GetMethod(nameof(Console.WriteLine), new[] { typeof(string) })!; 21 | 22 | methodILGenerator.Emit(OpCodes.Ldarg_1); 23 | methodILGenerator.EmitCall(OpCodes.Call, writeLineMethod, new[] { typeof(string) }); 24 | methodILGenerator.Emit(OpCodes.Ret); 25 | 26 | var toStringMethod = typeof(object).GetMethod(nameof(object.ToString))!; 27 | var newToStringMethod = typeBuilder.DefineMethod(nameof(object.ToString), toStringMethod.Attributes, typeof(string), Array.Empty()); 28 | var toStringGenerator = newToStringMethod.GetILGenerator(); 29 | toStringGenerator.Emit(OpCodes.Ldstr, "A message from ToString()"); 30 | toStringGenerator.Emit(OpCodes.Ret); 31 | typeBuilder.DefineMethodOverride(newToStringMethod, toStringMethod); 32 | 33 | return typeBuilder.CreateType()!; 34 | } 35 | } -------------------------------------------------------------------------------- /Chapter6/NotifyChangesForAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Chapter6; 2 | 3 | [AttributeUsage(AttributeTargets.Property)] 4 | public class NotifyChangesForAttribute : Attribute 5 | { 6 | public NotifyChangesForAttribute(params string[] propertyNames) 7 | { 8 | PropertyNames = propertyNames; 9 | } 10 | 11 | public string[] PropertyNames { get; } 12 | } 13 | -------------------------------------------------------------------------------- /Chapter6/Program.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using Chapter6; 3 | 4 | var myType = MyTypeGenerator.Generate(); 5 | var method = myType.GetMethod("SaySomething")!; 6 | var myTypeInstance = Activator.CreateInstance(myType); 7 | method.Invoke(myTypeInstance, new[] { "Hello world" }); 8 | 9 | Console.WriteLine(myTypeInstance); 10 | 11 | var type = NotifyingObjectWeaver.GetProxyType(); 12 | Console.WriteLine($"Type name : {type}"); 13 | 14 | var instance = (Activator.CreateInstance(type) as INotifyPropertyChanged)!; 15 | instance.PropertyChanged += (sender, e) => Console.WriteLine($"{e.PropertyName} changed"); 16 | 17 | var instanceAsViewModel = (instance as Employee)!; 18 | instanceAsViewModel.FirstName = "John"; 19 | -------------------------------------------------------------------------------- /Chapter7/Chapter7.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net7.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Chapter7/Employee.cs: -------------------------------------------------------------------------------- 1 | namespace Chapter7; 2 | 3 | public record Employee(string FirstName, string LastName); -------------------------------------------------------------------------------- /Chapter7/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using Chapter7; 3 | 4 | Expression e = (int i) => Console.WriteLine("Hello"); 5 | 6 | int[] items = { 1, 2, 3, 42, 84, 37, 23 }; 7 | 8 | // Imperative 9 | var filtered = new List(); 10 | foreach (var item in items) 11 | { 12 | if (item >= 37) 13 | { 14 | filtered.Add(item); 15 | } 16 | } 17 | Console.WriteLine(string.Join(',', filtered)); 18 | 19 | // LINQ 20 | var selectedItems = 21 | from i in items 22 | where i >= 37 23 | select i; 24 | 25 | Console.WriteLine(string.Join(',', selectedItems)); 26 | 27 | // LINQ fluent interface 28 | var selectedItemsExpressions = items 29 | .Where(i => i >= 37) 30 | .Select(i => i); 31 | 32 | Console.WriteLine(string.Join(',', selectedItemsExpressions)); 33 | 34 | // LINQ fluent interface with expression 35 | Expression> filter = (i) => i >= 37; 36 | 37 | var selectedItemsQueryable = items 38 | .AsQueryable() 39 | .Where(filter) 40 | .Select(i => i); 41 | 42 | Console.WriteLine(string.Join(',', selectedItemsQueryable)); 43 | 44 | Expression> addExpression = (i) => i + 42; 45 | 46 | Console.WriteLine(addExpression); 47 | 48 | Expression> employeeFilter = (employee) => employee.FirstName == "Jane"; 49 | 50 | Console.WriteLine(employeeFilter); 51 | -------------------------------------------------------------------------------- /Chapter8/Chapter8.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net7.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Chapter8/DictionaryStringObjectJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Chapter8; 5 | 6 | // From: https://josef.codes/custom-dictionary-string-object-jsonconverter-for-system-text-json/ 7 | public class DictionaryStringObjectJsonConverter : JsonConverter> 8 | { 9 | public override Dictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 10 | { 11 | if (reader.TokenType != JsonTokenType.StartObject) 12 | { 13 | throw new JsonException($"JsonTokenType was of type {reader.TokenType}, only objects are supported"); 14 | } 15 | 16 | var dictionary = new Dictionary(); 17 | while (reader.Read()) 18 | { 19 | if (reader.TokenType == JsonTokenType.EndObject) 20 | { 21 | return dictionary; 22 | } 23 | 24 | if (reader.TokenType != JsonTokenType.PropertyName) 25 | { 26 | throw new JsonException("JsonTokenType was not PropertyName"); 27 | } 28 | 29 | var propertyName = reader.GetString(); 30 | 31 | if (string.IsNullOrWhiteSpace(propertyName)) 32 | { 33 | throw new JsonException("Failed to get property name"); 34 | } 35 | 36 | reader.Read(); 37 | 38 | dictionary.Add(propertyName, ExtractValue(ref reader, options)); 39 | } 40 | 41 | return dictionary; 42 | } 43 | 44 | public override void Write(Utf8JsonWriter writer, Dictionary value, JsonSerializerOptions options) 45 | { 46 | JsonSerializer.Serialize(writer, value, options); 47 | } 48 | 49 | private object ExtractValue(ref Utf8JsonReader reader, JsonSerializerOptions options) 50 | { 51 | switch (reader.TokenType) 52 | { 53 | case JsonTokenType.String: 54 | if (reader.TryGetDateTime(out var date)) 55 | { 56 | return date; 57 | } 58 | return reader.GetString(); 59 | case JsonTokenType.False: 60 | return false; 61 | case JsonTokenType.True: 62 | return true; 63 | case JsonTokenType.Null: 64 | return null; 65 | case JsonTokenType.Number: 66 | if (reader.TryGetInt32(out var result)) 67 | { 68 | return result; 69 | } 70 | return reader.GetDecimal(); 71 | case JsonTokenType.StartObject: 72 | return Read(ref reader, null, options); 73 | case JsonTokenType.StartArray: 74 | var list = new List(); 75 | while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) 76 | { 77 | list.Add(ExtractValue(ref reader, options)); 78 | } 79 | return list; 80 | default: 81 | throw new JsonException($"'{reader.TokenType}' is not supported"); 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /Chapter8/MyType.cs: -------------------------------------------------------------------------------- 1 | namespace Chapter8; 2 | 3 | public class MyType 4 | { 5 | public string StringProperty { get; set; } = String.Empty; 6 | } -------------------------------------------------------------------------------- /Chapter8/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using System.Text.Json; 3 | using Chapter8; 4 | 5 | // Simple 6 | var parameter = Expression.Parameter(typeof(MyType)); 7 | var property = typeof(MyType).GetProperty(nameof(MyType.StringProperty))!; 8 | var propertyExpression = Expression.Property(parameter,property); 9 | var assignExpression = Expression.Assign( 10 | propertyExpression, 11 | Expression.Constant("Hello world")); 12 | 13 | var lambdaExpression = Expression.Lambda>(assignExpression, parameter); 14 | var expressionAction = lambdaExpression.Compile(); 15 | var instance = new MyType(); 16 | expressionAction(instance); 17 | Console.WriteLine(instance.StringProperty); 18 | 19 | // Advanced 20 | var query = File.ReadAllText("query.json"); 21 | var queryDocument = JsonDocument.Parse(query); 22 | var expression = QueryParser.Parse(queryDocument); 23 | 24 | var documentsRaw = File.ReadAllText("data.json"); 25 | var serializerOptions = new JsonSerializerOptions(); 26 | serializerOptions.Converters.Add(new DictionaryStringObjectJsonConverter()); 27 | var documents = JsonSerializer.Deserialize>>(documentsRaw, serializerOptions)!; 28 | 29 | var filtered = documents.AsQueryable().Where(expression); 30 | foreach (var document in filtered) 31 | { 32 | Console.WriteLine(JsonSerializer.Serialize(document)); 33 | } 34 | -------------------------------------------------------------------------------- /Chapter8/QueryParser.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using System.Text.Json; 3 | 4 | namespace Chapter8; 5 | 6 | public static class QueryParser 7 | { 8 | static readonly ParameterExpression _dictionaryParameter = Expression.Parameter(typeof(IDictionary), "input"); 9 | 10 | public static Expression, bool>> Parse(JsonDocument json) 11 | { 12 | var element = json.RootElement; 13 | var query = GetQueryExpression(element); 14 | return Expression.Lambda, bool>>(query, _dictionaryParameter); 15 | } 16 | 17 | static Expression GetQueryExpression(JsonElement element) 18 | { 19 | Expression? currentExpression = null; 20 | 21 | foreach (var property in element.EnumerateObject()) 22 | { 23 | Expression expression = property.Name switch 24 | { 25 | "$or" => GetOrExpression(currentExpression!, property), 26 | "$in" => Expression.Empty(), 27 | _ => GetFilterExpression(property) 28 | }; 29 | 30 | if (currentExpression is not null && expression is not BinaryExpression) 31 | { 32 | currentExpression = Expression.And(currentExpression, expression); 33 | } 34 | else 35 | { 36 | currentExpression = expression; 37 | } 38 | } 39 | 40 | return currentExpression ?? Expression.Empty(); 41 | } 42 | 43 | static Expression GetOrExpression(Expression expression, JsonProperty property) 44 | { 45 | foreach (var element in property.Value.EnumerateArray()) 46 | { 47 | var elementExpression = GetQueryExpression(element); 48 | expression = Expression.OrElse(expression, elementExpression); 49 | } 50 | 51 | return expression; 52 | } 53 | 54 | static Expression GetFilterExpression(JsonProperty property) 55 | { 56 | return property.Value.ValueKind switch 57 | { 58 | JsonValueKind.Object => GetNestedFilterExpression(property), 59 | _ => Expression.Equal(GetLeftValueExpression(property, property), GetRightValueExpression(property)) 60 | }; 61 | } 62 | 63 | static Expression GetNestedFilterExpression(JsonProperty property) 64 | { 65 | Expression? currentExpression = null; 66 | 67 | foreach (var expressionProperty in property.Value.EnumerateObject()) 68 | { 69 | var getValueExpression = GetLeftValueExpression(property, expressionProperty); 70 | var valueConstantExpression = GetRightValueExpression(expressionProperty); 71 | Expression comparisonExpression = expressionProperty.Name switch 72 | { 73 | "$lt" => Expression.LessThan(getValueExpression, valueConstantExpression), 74 | "$lte" => Expression.LessThanOrEqual(getValueExpression, valueConstantExpression), 75 | "$gt" => Expression.GreaterThan(getValueExpression, valueConstantExpression), 76 | "$gte" => Expression.GreaterThanOrEqual(getValueExpression, valueConstantExpression), 77 | _ => Expression.Empty() 78 | }; 79 | 80 | if (currentExpression is not null) 81 | { 82 | currentExpression = Expression.And(currentExpression, comparisonExpression); 83 | } 84 | else 85 | { 86 | currentExpression = comparisonExpression; 87 | } 88 | } 89 | 90 | return currentExpression ?? Expression.Empty(); 91 | } 92 | 93 | static Expression GetLeftValueExpression(JsonProperty parentProperty, JsonProperty property) 94 | { 95 | var keyParam = Expression.Constant(parentProperty.Name); 96 | var indexer = typeof(IDictionary).GetProperty("Item")!; 97 | var indexerExpr = Expression.Property(_dictionaryParameter, indexer, keyParam); 98 | 99 | return property.Value.ValueKind switch 100 | { 101 | JsonValueKind.Number => Expression.Unbox(indexerExpr, typeof(int)), 102 | JsonValueKind.String => Expression.TypeAs(indexerExpr, typeof(string)), 103 | JsonValueKind.True or JsonValueKind.False => Expression.TypeAs(indexerExpr, typeof(bool)), 104 | _ => indexerExpr 105 | }; 106 | } 107 | 108 | static Expression GetRightValueExpression(JsonProperty property) 109 | { 110 | return property.Value.ValueKind switch 111 | { 112 | JsonValueKind.Number => Expression.Constant(property.Value.GetInt32()), 113 | JsonValueKind.String => Expression.Constant((object)property.Value.GetString()!), 114 | JsonValueKind.True or JsonValueKind.False => Expression.Constant((object)property.Value.GetBoolean()), 115 | _ => Expression.Empty() 116 | }; 117 | } 118 | } -------------------------------------------------------------------------------- /Chapter8/data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "FirstName": "Jane", "LastName": "Doe", "Age": 57 }, 3 | { "FirstName": "John", "LastName": "Doe", "Age": 55 }, 4 | { "FirstName": "Michael", "LastName": "Corleone", "Age": 47 }, 5 | { "FirstName": "Anthony", "LastName": "Soprano", "Age": 51 }, 6 | { "FirstName": "Paulie", "LastName": "Gualtieri", "Age": 58 } 7 | ] -------------------------------------------------------------------------------- /Chapter8/definition.json: -------------------------------------------------------------------------------- 1 | { 2 | "property": "value", 3 | "$in": [], 4 | "complex_property_query": { 5 | "$lt": 30, 6 | "$gt": 30, 7 | "$lte": 30, 8 | "$gte": 30 9 | }, 10 | "$or": [ 11 | { 12 | "property": "value" 13 | }, 14 | { 15 | "otherProperty": { 16 | "$lt": 30 17 | } 18 | } 19 | ], 20 | "$all": [ 21 | "item1", 22 | "item2" 23 | ], 24 | "array.index": { 25 | "$gt": 25 26 | }, 27 | "array": { 28 | "$size": 3 29 | }, 30 | "item": null, 31 | "someProperty": { 32 | "$exists": false 33 | } 34 | } -------------------------------------------------------------------------------- /Chapter8/query.json: -------------------------------------------------------------------------------- 1 | { 2 | "Age": { 3 | "$gte": 52 4 | }, 5 | "$or": [ 6 | { 7 | "LastName": "Doe" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /Chapter9/Chapter9.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net7.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Chapter9/InvalidTypeForProperty.cs: -------------------------------------------------------------------------------- 1 | namespace Chapter9; 2 | 3 | public class InvalidTypeForProperty : Exception 4 | { 5 | public InvalidTypeForProperty(string type, string property) : base($"Property '{property}' on '{type}' is invalid.") 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Chapter9/JsonSchemaType.cs: -------------------------------------------------------------------------------- 1 | using System.Dynamic; 2 | using NJsonSchema; 3 | 4 | namespace Chapter9; 5 | 6 | public class JsonSchemaType : DynamicObject 7 | { 8 | readonly IDictionary _values = new Dictionary(); 9 | readonly JsonSchema _schema; 10 | 11 | public JsonSchemaType(JsonSchema schema) 12 | { 13 | _schema = schema; 14 | } 15 | 16 | public override bool TrySetMember(SetMemberBinder binder, object? value) 17 | { 18 | if (!_schema.ActualProperties.ContainsKey(binder.Name)) 19 | { 20 | return false; 21 | } 22 | ValidateType(binder.Name, value); 23 | _values[binder.Name] = value; 24 | return true; 25 | } 26 | 27 | public override bool TryGetMember(GetMemberBinder binder, out object? result) 28 | { 29 | if (!_schema.ActualProperties.ContainsKey(binder.Name)) 30 | { 31 | result = null!; 32 | return false; 33 | } 34 | 35 | result = _values.ContainsKey(binder.Name) ? result = _values[binder.Name] : result = null!; 36 | return true; 37 | } 38 | 39 | public override bool TryConvert(ConvertBinder binder, out object? result) 40 | { 41 | if (binder.Type.Equals(typeof(Dictionary))) 42 | { 43 | var returnValue = new Dictionary(_values); 44 | var missingProperties = _schema.ActualProperties.Where(_ => !_values.Any(kvp => _.Key == kvp.Key)); 45 | foreach (var property in missingProperties) 46 | { 47 | object defaultValue = property.Value.Type switch 48 | { 49 | JsonObjectType.Array => Enumerable.Empty(), 50 | JsonObjectType.Boolean => false, 51 | JsonObjectType.Integer => 0, 52 | JsonObjectType.Number => 0, 53 | _ => null! 54 | }; 55 | 56 | returnValue[property.Key] = defaultValue; 57 | } 58 | result = returnValue; 59 | return true; 60 | } 61 | return base.TryConvert(binder, out result); 62 | } 63 | 64 | public override IEnumerable GetDynamicMemberNames() => _schema.ActualProperties.Keys; 65 | 66 | 67 | 68 | void ValidateType(string property, object? value) 69 | { 70 | if (value is not null) 71 | { 72 | var schemaType = GetSchemaTypeFrom(value.GetType()); 73 | if (!_schema.ActualProperties[property].Type.HasFlag(schemaType)) 74 | { 75 | throw new InvalidTypeForProperty(_schema.Title, property); 76 | } 77 | } 78 | } 79 | 80 | JsonObjectType GetSchemaTypeFrom(Type type) 81 | { 82 | return type switch 83 | { 84 | Type _ when type == typeof(string) => JsonObjectType.String, 85 | Type _ when type == typeof(DateOnly) => JsonObjectType.String, 86 | Type _ when type == typeof(int) => JsonObjectType.Integer, 87 | Type _ when type == typeof(float) => JsonObjectType.Number, 88 | Type _ when type == typeof(double) => JsonObjectType.Number, 89 | _ => JsonObjectType.Object 90 | }; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Chapter9/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Dynamic; 2 | using System.Linq.Expressions; 3 | using System.Runtime.CompilerServices; 4 | using System.Text.Json; 5 | using Chapter9; 6 | using Microsoft.CSharp.RuntimeBinder; 7 | using NJsonSchema; 8 | 9 | dynamic person = new ExpandoObject(); 10 | person.FirstName = "Jane"; 11 | person.LastName = "Doe"; 12 | Console.WriteLine($"{person.FirstName} {person.LastName}"); 13 | 14 | var provider = (person as IDynamicMetaObjectProvider)!; 15 | var meta = provider.GetMetaObject(Expression.Constant(person)); 16 | var members = string.Join(',', meta.GetDynamicMemberNames()); 17 | Console.WriteLine(members); 18 | 19 | foreach (var member in meta.GetDynamicMemberNames()) 20 | { 21 | var binder = Binder.GetMember( 22 | CSharpBinderFlags.None, 23 | member, 24 | person.GetType(), 25 | new[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) }); 26 | 27 | var site = CallSite>.Create(binder); 28 | var propertyValue = site.Target(site, person); 29 | Console.WriteLine($"{member} = {propertyValue}"); 30 | } 31 | 32 | Func BuildDynamicGetter(Type type, string propertyName) 33 | { 34 | var binder = Binder.GetMember( 35 | CSharpBinderFlags.None, 36 | propertyName, 37 | type, 38 | new[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) }); 39 | var rootParameter = Expression.Parameter(typeof(object)); 40 | var binderExpression = Expression.Dynamic(binder, typeof(object), Expression.Convert(rootParameter, type)); 41 | var getterExpression = Expression.Lambda>(binderExpression, rootParameter); 42 | return getterExpression.Compile(); 43 | } 44 | 45 | var firstNameExpression = BuildDynamicGetter(person.GetType(), "FirstName"); 46 | var lastNameExpression = BuildDynamicGetter(person.GetType(), "LastName"); 47 | Console.WriteLine($"{firstNameExpression(person)} {lastNameExpression(person)}"); 48 | 49 | var schema = await JsonSchema.FromFileAsync("person.json"); 50 | dynamic personInstance = new JsonSchemaType(schema); 51 | var personMetaObject = personInstance.GetMetaObject(Expression.Constant(personInstance)); 52 | var personProperties = personMetaObject.GetDynamicMemberNames(); 53 | Console.WriteLine(string.Join(',', personProperties)); 54 | personInstance.FirstName = "Jane"; 55 | Console.WriteLine($"FirstName : '{personInstance.FirstName}'"); 56 | Console.WriteLine($"LastName : '{personInstance.LastName}'"); 57 | 58 | var dictionary = (Dictionary)personInstance; 59 | Console.WriteLine(JsonSerializer.Serialize(dictionary)); 60 | 61 | // personInstance.FullName = "Jane Doe"; 62 | personInstance.LastName = 42; 63 | -------------------------------------------------------------------------------- /Chapter9/person.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "title": "Person", 4 | "type": "object", 5 | "additionalProperties": false, 6 | "properties": { 7 | "FirstName": { 8 | "type": "string" 9 | }, 10 | "LastName": { 11 | "type": "string" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /Fundamentals/ArrayIndexer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Fundamentals; 5 | 6 | /// 7 | /// Represents information on how to index an array. 8 | /// 9 | /// Fully qualified identifier of the within an object structure. 10 | /// within the object that holds the identifying value. 11 | /// The identifying value. 12 | public record ArrayIndexer(PropertyPath ArrayProperty, PropertyPath IdentifierProperty, object Identifier); 13 | -------------------------------------------------------------------------------- /Fundamentals/ArrayIndexers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Fundamentals; 5 | 6 | /// 7 | /// Represents an implementation of . 8 | /// 9 | public class ArrayIndexers : IArrayIndexers 10 | { 11 | /// 12 | /// Represents no indexers - used when you don't have any indexers. 13 | /// 14 | public static readonly IArrayIndexers NoIndexers = new ArrayIndexers(Array.Empty()); 15 | 16 | readonly IDictionary _arrayIndexers; 17 | 18 | /// 19 | public IEnumerable All => _arrayIndexers.Values; 20 | 21 | /// 22 | /// Initializes a new instance of the class. 23 | /// 24 | /// A collection of array indexers. 25 | public ArrayIndexers(IEnumerable arrayIndexers) 26 | { 27 | _arrayIndexers = arrayIndexers.ToDictionary(_ => _.ArrayProperty, _ => _); 28 | } 29 | 30 | /// 31 | public ArrayIndexer GetFor(PropertyPath propertyPath) 32 | { 33 | ThrowIfMissingArrayIndexerForPropertyPath(propertyPath); 34 | return _arrayIndexers[propertyPath]; 35 | } 36 | 37 | /// 38 | public bool HasFor(PropertyPath propertyPath) => _arrayIndexers.ContainsKey(propertyPath); 39 | 40 | void ThrowIfMissingArrayIndexerForPropertyPath(PropertyPath propertyPath) 41 | { 42 | if (!_arrayIndexers.ContainsKey(propertyPath)) 43 | { 44 | throw new MissingArrayIndexerForPropertyPath(propertyPath); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Fundamentals/ArrayProperty.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Fundamentals; 5 | 6 | /// 7 | /// Represents a for an array. 8 | /// 9 | /// Identifier of the array indexer. 10 | public record ArrayProperty(string Value) : IPropertyPathSegment 11 | { 12 | /// 13 | public override string ToString() => $"[{Value}]"; 14 | } 15 | -------------------------------------------------------------------------------- /Fundamentals/ChildrenPropertyIsNotEnumerable.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Fundamentals; 5 | 6 | /// 7 | /// Exception that gets thrown when a children property is not enumerable. 8 | /// 9 | public class ChildrenPropertyIsNotEnumerable : Exception 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// that is wrong type. 15 | public ChildrenPropertyIsNotEnumerable(PropertyPath property) : base($"Property at '{property.Path}' is not of enumerable type.") 16 | { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Fundamentals/ChildrenPropertyIsNotEnumerableForType.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Fundamentals; 5 | 6 | /// 7 | /// Exception that gets thrown when a children property is not enumerable for a specific type. 8 | /// 9 | public class ChildrenPropertyIsNotEnumerableForType : Exception 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// Type that is the root of the . 15 | /// that is wrong type. 16 | public ChildrenPropertyIsNotEnumerableForType(Type type, PropertyPath property) : base($"Property at '{property.Path}' on '{type.FullName}' is not of enumerable type.") 17 | { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Fundamentals/Compliance/ComplianceDetailsAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Fundamentals.Compliance; 5 | 6 | /// 7 | /// Attribute to adorn for providing the details as to why or to what purpose/extent the type or property marked is classified as PII. 8 | /// 9 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Parameter)] 10 | public sealed class ComplianceDetailsAttribute : Attribute 11 | { 12 | /// 13 | /// Gets the details as to why or to what purpose/extent the type or property marked is classified as PII. 14 | /// 15 | public string Details { get; } 16 | 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | /// The details as to why or to what purpose/extent the type or property marked is classified as PII. 21 | public ComplianceDetailsAttribute(string details) 22 | { 23 | Details = details; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Fundamentals/Compliance/ComplianceDetailsExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Reflection; 5 | 6 | namespace Fundamentals.Compliance; 7 | 8 | /// 9 | /// Extension methods for providing compliance details. 10 | /// 11 | public static class ComplianceDetailsExtensions 12 | { 13 | /// 14 | /// Get the compliance metadata details from a . 15 | /// 16 | /// Type to get from. 17 | /// The details - empty string if none is set. 18 | public static string GetComplianceMetadataDetails(this Type type) => type.GetCustomAttribute()?.Details ?? string.Empty; 19 | 20 | /// 21 | /// Get the compliance metadata details from a . 22 | /// 23 | /// Property to get from. 24 | /// The details - empty string if none is set. 25 | public static string GetComplianceMetadataDetails(this PropertyInfo property) => 26 | property.GetCustomAttribute()?.Details ?? 27 | property.DeclaringType?.GetCustomAttribute()?.Details ?? 28 | property.PropertyType.GetComplianceMetadataDetails() ?? 29 | string.Empty; 30 | } 31 | -------------------------------------------------------------------------------- /Fundamentals/Compliance/ComplianceMetadata.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Fundamentals.Compliance; 5 | 6 | /// 7 | /// Represents the metadata related to compliance. 8 | /// 9 | /// The . 10 | /// Any additional details - can be empty. 11 | public record ComplianceMetadata(ComplianceMetadataType MetadataType, string Details); 12 | -------------------------------------------------------------------------------- /Fundamentals/Compliance/ComplianceMetadataResolver.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Reflection; 5 | 6 | namespace Fundamentals.Compliance; 7 | 8 | /// 9 | /// Represents an implementation of . 10 | /// 11 | public class ComplianceMetadataResolver : IComplianceMetadataResolver 12 | { 13 | readonly IEnumerable _typeProviders; 14 | readonly IEnumerable _propertyProviders; 15 | 16 | /// 17 | /// Initializes a new instance of the . 18 | /// 19 | /// Type providers. 20 | /// Property providers. 21 | public ComplianceMetadataResolver( 22 | IInstancesOf typeProviders, 23 | IInstancesOf propertyProviders) 24 | { 25 | _typeProviders = typeProviders.ToArray(); 26 | _propertyProviders = propertyProviders.ToArray(); 27 | } 28 | 29 | /// 30 | public bool HasMetadataFor(Type type) => _typeProviders.Any(_ => _.CanProvide(type)); 31 | 32 | /// 33 | public bool HasMetadataFor(PropertyInfo property) => _propertyProviders.Any(_ => _.CanProvide(property)); 34 | 35 | /// 36 | public IEnumerable GetMetadataFor(Type type) 37 | { 38 | ThrowIfNoComplianceMetadataForType(type); 39 | return _typeProviders 40 | .Where(_ => _.CanProvide(type)) 41 | .Select(_ => _.Provide(type)) 42 | .ToArray(); 43 | } 44 | 45 | /// 46 | public IEnumerable GetMetadataFor(PropertyInfo property) 47 | { 48 | ThrowIfNoComplianceMetadataForProperty(property); 49 | return _propertyProviders 50 | .Where(_ => _.CanProvide(property)) 51 | .Select(_ => _.Provide(property)) 52 | .ToArray(); 53 | } 54 | 55 | void ThrowIfNoComplianceMetadataForType(Type type) 56 | { 57 | if (!HasMetadataFor(type)) 58 | { 59 | throw new NoComplianceMetadataForType(type); 60 | } 61 | } 62 | 63 | void ThrowIfNoComplianceMetadataForProperty(PropertyInfo property) 64 | { 65 | if (!HasMetadataFor(property)) 66 | { 67 | throw new NoComplianceMetadataForProperty(property); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Fundamentals/Compliance/ComplianceMetadataType.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Fundamentals.Compliance; 5 | 6 | /// 7 | /// Represents a type of compliance metadata. 8 | /// 9 | /// Underlying value. 10 | public record ComplianceMetadataType(Guid Value) : ConceptAs(Value) 11 | { 12 | /// 13 | /// Personal Identifiable Information according to the definition in GDPR. 14 | /// 15 | public static readonly ComplianceMetadataType PII = new(Guid.Parse("cae5580e-83d6-44dc-9d7a-a72e8a2f17d7")); 16 | 17 | /// 18 | /// Convert from a representation of a to . 19 | /// 20 | /// representation. 21 | public static implicit operator ComplianceMetadataType(string value) => new(Guid.Parse(value)); 22 | 23 | /// 24 | /// Convert from to . 25 | /// 26 | /// to convert from. 27 | public static implicit operator ComplianceMetadataType(Guid value) => new(value); 28 | } 29 | -------------------------------------------------------------------------------- /Fundamentals/Compliance/GDPR/IHoldPersonalIdentifiableInformation.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Fundamentals.Compliance.GDPR; 5 | 6 | /// 7 | /// Defines a marker interface for types to indicate the type holds PII defined by GDPR. 8 | /// 9 | public interface IHoldPersonalIdentifiableInformation 10 | { 11 | } 12 | -------------------------------------------------------------------------------- /Fundamentals/Compliance/GDPR/PersonalIdentifiableInformationAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Fundamentals.Compliance.GDPR; 5 | 6 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] 7 | public class PersonalIdentifiableInformationAttribute : Attribute 8 | { 9 | public PersonalIdentifiableInformationAttribute(string reasonForCollecting = "") 10 | { 11 | ReasonForCollecting = reasonForCollecting; 12 | } 13 | 14 | public string ReasonForCollecting { get; } 15 | } 16 | -------------------------------------------------------------------------------- /Fundamentals/Compliance/GDPR/PersonalIdentifiableInformationConceptAs.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Fundamentals.Compliance.GDPR; 5 | 6 | /// 7 | /// Represents a that holds PII according to the definition of GDPR. 8 | /// 9 | /// Underlying value. 10 | /// Type of the underlying value. 11 | public record PersonalIdentifiableInformationConceptAs(T Value) : ConceptAs(Value), IHoldPersonalIdentifiableInformation 12 | { 13 | /// 14 | /// Gets the details for the PII. 15 | /// 16 | public virtual string Details => string.Empty; 17 | } 18 | -------------------------------------------------------------------------------- /Fundamentals/Compliance/GDPR/PersonalIdentifiableInformationMetadataProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Reflection; 5 | 6 | namespace Fundamentals.Compliance.GDPR; 7 | 8 | /// 9 | /// Represents a metadata provider for PII. 10 | /// 11 | public class PersonalIdentifiableInformationMetadataProvider : ICanProvideComplianceMetadataForType, ICanProvideComplianceMetadataForProperty 12 | { 13 | /// 14 | public bool CanProvide(Type type) => type.Implements(typeof(IHoldPersonalIdentifiableInformation)) || type.GetCustomAttribute() != default; 15 | 16 | /// 17 | public bool CanProvide(PropertyInfo property) => 18 | property.GetCustomAttribute() != default || 19 | property.DeclaringType?.GetCustomAttribute() != default || 20 | CanProvide(property.PropertyType); 21 | 22 | /// 23 | public ComplianceMetadata Provide(Type type) 24 | { 25 | if (!CanProvide(type)) 26 | { 27 | throw new NoComplianceMetadataForType(type); 28 | } 29 | 30 | return new ComplianceMetadata(ComplianceMetadataType.PII, type.GetComplianceMetadataDetails()); 31 | } 32 | 33 | /// 34 | public ComplianceMetadata Provide(PropertyInfo property) 35 | { 36 | if (!CanProvide(property)) 37 | { 38 | throw new NoComplianceMetadataForProperty(property); 39 | } 40 | 41 | var details = property.GetComplianceMetadataDetails(); 42 | if (string.IsNullOrEmpty(details)) 43 | { 44 | details = property.GetCustomAttribute()!.ReasonForCollecting; 45 | } 46 | return new ComplianceMetadata(ComplianceMetadataType.PII, details); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Fundamentals/Compliance/ICanProvideComplianceMetadataForProperty.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Reflection; 5 | 6 | namespace Fundamentals.Compliance; 7 | 8 | /// 9 | /// Defines a provider of for types. 10 | /// 11 | public interface ICanProvideComplianceMetadataForProperty 12 | { 13 | /// 14 | /// Checks whether or not it can provide for the type. 15 | /// 16 | /// to check for. 17 | /// True if it can provide, false if not. 18 | bool CanProvide(PropertyInfo property); 19 | 20 | /// 21 | /// Provide the metadata for the type. 22 | /// 23 | /// to provide for. 24 | /// Provided . 25 | ComplianceMetadata Provide(PropertyInfo property); 26 | } 27 | -------------------------------------------------------------------------------- /Fundamentals/Compliance/ICanProvideComplianceMetadataForType.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Fundamentals.Compliance; 5 | 6 | /// 7 | /// Defines a provider of for types. 8 | /// 9 | public interface ICanProvideComplianceMetadataForType 10 | { 11 | /// 12 | /// Checks whether or not it can provide for the type. 13 | /// 14 | /// to check for. 15 | /// True if it can provide, false if not. 16 | bool CanProvide(Type type); 17 | 18 | /// 19 | /// Provide the metadata for the type. 20 | /// 21 | /// to provide for. 22 | /// Provided . 23 | ComplianceMetadata Provide(Type type); 24 | } 25 | -------------------------------------------------------------------------------- /Fundamentals/Compliance/IComplianceMetadataResolver.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Reflection; 5 | 6 | namespace Fundamentals.Compliance; 7 | 8 | /// 9 | /// Defines a resolver of for types and properties. 10 | /// 11 | public interface IComplianceMetadataResolver 12 | { 13 | /// 14 | /// Check whether or not a specific has any associated with it. 15 | /// 16 | /// to check. 17 | /// True if it has, false if not. 18 | bool HasMetadataFor(Type type); 19 | 20 | /// 21 | /// Check whether or not a specific has any associated with it. 22 | /// 23 | /// to check. 24 | /// True if it has, false if not. 25 | bool HasMetadataFor(PropertyInfo property); 26 | 27 | /// 28 | /// Get the associated with a . 29 | /// 30 | /// to get for. 31 | /// Collection of associated with the type. 32 | IEnumerable GetMetadataFor(Type type); 33 | 34 | /// 35 | /// Get the associated with a . 36 | /// 37 | /// to get for. 38 | /// Collection of associated with the type. 39 | IEnumerable GetMetadataFor(PropertyInfo property); 40 | } 41 | -------------------------------------------------------------------------------- /Fundamentals/Compliance/NoComplianceMetadataForProperty.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Reflection; 5 | 6 | namespace Fundamentals.Compliance; 7 | 8 | /// 9 | /// Exception that gets thrown when where is no associated with a property. 10 | /// 11 | public class NoComplianceMetadataForProperty : Exception 12 | { 13 | /// 14 | /// Initializes a new instance of the class. 15 | /// 16 | /// that does not have compliance metadata. 17 | public NoComplianceMetadataForProperty(PropertyInfo property) 18 | : base($"Property '{property.Name}' on type '{property.DeclaringType?.FullName}' does not have any compliance metadata.") 19 | { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Fundamentals/Compliance/NoComplianceMetadataForType.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Fundamentals.Compliance; 5 | 6 | /// 7 | /// Exception that gets thrown when where is no associated with a property. 8 | /// 9 | public class NoComplianceMetadataForType : Exception 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// that does not have compliance metadata. 15 | public NoComplianceMetadataForType(Type type) 16 | : base($"Types '{type.FullName}' does not have any compliance metadata.") 17 | { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Fundamentals/ConceptAs.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Fundamentals; 5 | 6 | public record ConceptAs 7 | { 8 | public ConceptAs(T value) 9 | { 10 | ArgumentNullException.ThrowIfNull(value, nameof(value)); 11 | Value = value; 12 | } 13 | 14 | public T Value { get; init; } 15 | 16 | public static implicit operator T(ConceptAs value) => value.Value; 17 | 18 | public sealed override string ToString() 19 | { 20 | return Value!.ToString() ?? "[n/a]"; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Fundamentals/ConceptAsJsonConverter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Text.Json; 5 | using System.Text.Json.Serialization; 6 | 7 | namespace Fundamentals; 8 | 9 | public class ConceptAsJsonConverter : JsonConverter 10 | { 11 | public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 12 | { 13 | object? value = null; 14 | var conceptValueType = typeof(T).GetConceptValueType(); 15 | switch (reader.TokenType) 16 | { 17 | case JsonTokenType.True: 18 | case JsonTokenType.False: 19 | value = reader.GetBoolean(); 20 | break; 21 | 22 | case JsonTokenType.Number: 23 | if (conceptValueType == typeof(sbyte)) 24 | { 25 | value = reader.GetSByte(); 26 | } 27 | if (conceptValueType == typeof(short)) 28 | { 29 | value = reader.GetInt16(); 30 | } 31 | else if (conceptValueType == typeof(int)) 32 | { 33 | value = reader.GetInt32(); 34 | } 35 | else if (conceptValueType == typeof(long)) 36 | { 37 | value = reader.GetInt64(); 38 | } 39 | else if (conceptValueType == typeof(byte)) 40 | { 41 | value = reader.GetByte(); 42 | } 43 | else if (conceptValueType == typeof(ushort)) 44 | { 45 | value = reader.GetUInt16(); 46 | } 47 | else if (conceptValueType == typeof(uint)) 48 | { 49 | value = reader.GetUInt32(); 50 | } 51 | else if (conceptValueType == typeof(ulong)) 52 | { 53 | value = reader.GetUInt64(); 54 | } 55 | else if (conceptValueType == typeof(float)) 56 | { 57 | value = (float)reader.GetDouble(); 58 | } 59 | else if (conceptValueType == typeof(double)) 60 | { 61 | value = reader.GetDouble(); 62 | } 63 | else if (conceptValueType == typeof(decimal)) 64 | { 65 | value = reader.GetDecimal(); 66 | } 67 | break; 68 | 69 | default: 70 | value = reader.GetString(); 71 | break; 72 | } 73 | 74 | if (value is null) 75 | { 76 | return default!; 77 | } 78 | 79 | return (T)ConceptFactory.CreateConceptInstance(typeToConvert, value)!; 80 | } 81 | 82 | public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) 83 | { 84 | var actualValue = value?.GetConceptValue(); 85 | var conceptValueType = typeof(T).GetConceptValueType(); 86 | if (conceptValueType == typeof(DateOnly)) 87 | { 88 | writer.WriteStringValue(((DateOnly)actualValue!).ToString("O")); 89 | } 90 | else if (conceptValueType == typeof(TimeOnly)) 91 | { 92 | writer.WriteStringValue(((TimeOnly)actualValue!).ToString("O")); 93 | } 94 | else 95 | { 96 | var rawValue = JsonSerializer.Serialize(actualValue, conceptValueType, options); 97 | writer.WriteRawValue(rawValue); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Fundamentals/ConceptAsJsonConverterFactory.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Text.Json; 5 | using System.Text.Json.Serialization; 6 | 7 | namespace Fundamentals; 8 | 9 | public class ConceptAsJsonConverterFactory : JsonConverterFactory 10 | { 11 | public override bool CanConvert(Type typeToConvert) => typeToConvert.IsConcept(); 12 | 13 | public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) 14 | { 15 | var converterType = typeof(ConceptAsJsonConverter<>).MakeGenericType(typeToConvert); 16 | return (Activator.CreateInstance(converterType) as JsonConverter)!; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Fundamentals/ConceptAsTypeConverter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.ComponentModel; 5 | using System.Globalization; 6 | 7 | namespace Fundamentals; 8 | 9 | public class ConceptAsTypeConverter : TypeConverter 10 | { 11 | public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) => sourceType == typeof(TValue) || sourceType == typeof(string); 12 | 13 | public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) 14 | { 15 | return ConceptFactory.CreateConceptInstance(typeof(TConcept), value); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Fundamentals/ConceptExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Fundamentals; 5 | 6 | public static class ConceptExtensions 7 | { 8 | public static bool IsPIIConcept(this Type objectType) 9 | { 10 | return objectType.IsDerivedFromOpenGeneric(typeof(PIIConceptAs<>)); 11 | } 12 | 13 | public static bool IsConcept(this Type objectType) 14 | { 15 | return objectType.IsDerivedFromOpenGeneric(typeof(ConceptAs<>)); 16 | } 17 | 18 | public static bool IsConcept(this object instance) 19 | { 20 | return IsConcept(instance.GetType()); 21 | } 22 | 23 | public static Type GetConceptValueType(this Type type) 24 | { 25 | return ConceptMap.GetConceptValueType(type); 26 | } 27 | 28 | public static object GetConceptValue(this object conceptObject) 29 | { 30 | if (!IsConcept(conceptObject)) throw new TypeIsNotAConcept(conceptObject.GetType()); 31 | 32 | return ((dynamic)conceptObject).Value; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Fundamentals/ConceptFactory.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Reflection; 5 | 6 | #nullable disable 7 | 8 | namespace Fundamentals; 9 | 10 | public static class ConceptFactory 11 | { 12 | public static object CreateConceptInstance(Type type, object value) 13 | { 14 | ArgumentNullException.ThrowIfNull(value, nameof(value)); 15 | 16 | var genericArgumentType = GetPrimitiveTypeConceptIsBasedOn(type); 17 | value = TypeConversion.Convert(genericArgumentType, value); 18 | var instance = Activator.CreateInstance(type, value); 19 | var valueProperty = type.GetTypeInfo().GetProperty(nameof(ConceptAs.Value)); 20 | valueProperty.SetValue(instance, value, null); 21 | return instance; 22 | } 23 | 24 | static Type GetPrimitiveTypeConceptIsBasedOn(Type conceptType) 25 | { 26 | return ConceptMap.GetConceptValueType(conceptType); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Fundamentals/ConceptMap.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Collections.Concurrent; 5 | using System.Reflection; 6 | 7 | namespace Fundamentals; 8 | 9 | public static class ConceptMap 10 | { 11 | static readonly ConcurrentDictionary _cache = new(); 12 | 13 | public static Type GetConceptValueType(Type type) 14 | { 15 | return _cache.GetOrAdd(type, GetPrimitiveType); 16 | } 17 | 18 | static Type GetPrimitiveType(Type type) 19 | { 20 | var conceptType = type; 21 | for (; ; ) 22 | { 23 | if (conceptType == typeof(ConceptAs<>) || conceptType.BaseType == null) break; 24 | 25 | if (conceptType.BaseType.IsGenericType && conceptType.BaseType.GetGenericTypeDefinition() == typeof(ConceptAs<>)) 26 | { 27 | return conceptType.BaseType.GetGenericArguments()[0]; 28 | } 29 | 30 | if (conceptType == typeof(object)) break; 31 | 32 | conceptType = conceptType.GetTypeInfo().BaseType!; 33 | } 34 | 35 | return typeof(void); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Fundamentals/ContractToImplementorsMap.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Collections.Concurrent; 5 | 6 | namespace Fundamentals; 7 | 8 | /// 9 | /// Represents an implementation of . 10 | /// 11 | public class ContractToImplementorsMap : IContractToImplementorsMap 12 | { 13 | readonly ConcurrentDictionary> _contractsAndImplementors = new(); 14 | readonly ConcurrentDictionary _allTypes = new(); 15 | 16 | /// 17 | public IDictionary> ContractsAndImplementors => _contractsAndImplementors.ToDictionary(_ => _.Key, _ => _.Value.AsEnumerable()); 18 | 19 | /// 20 | public IEnumerable All => _allTypes.Keys; 21 | 22 | /// 23 | public void Feed(IEnumerable types) 24 | { 25 | MapTypes(types); 26 | AddTypesToAllTypes(types); 27 | } 28 | 29 | /// 30 | public IEnumerable GetImplementorsFor() 31 | { 32 | return GetImplementorsFor(typeof(T)); 33 | } 34 | 35 | /// 36 | public IEnumerable GetImplementorsFor(Type contract) 37 | { 38 | return GetImplementingTypesFor(contract); 39 | } 40 | 41 | void AddTypesToAllTypes(IEnumerable types) 42 | { 43 | foreach (var type in types) _allTypes[type] = type; 44 | } 45 | 46 | void MapTypes(IEnumerable types) 47 | { 48 | var implementors = types.Where(IsImplementation); 49 | Parallel.ForEach(implementors, implementor => 50 | { 51 | foreach (var contract in implementor.AllBaseAndImplementingTypes()) 52 | { 53 | var implementingTypes = GetImplementingTypesFor(contract); 54 | if (!implementingTypes.Contains(implementor)) implementingTypes.Add(implementor); 55 | } 56 | }); 57 | } 58 | 59 | bool IsImplementation(Type type) => !type.IsInterface && !type.IsAbstract; 60 | 61 | ConcurrentBag GetImplementingTypesFor(Type contract) 62 | { 63 | return _contractsAndImplementors.GetOrAdd(contract, _ => new ConcurrentBag()); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Fundamentals/Fundamentals.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Fundamentals/Globals.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Text.Json; 5 | using System.Text.Json.Serialization; 6 | 7 | namespace Fundamentals; 8 | 9 | /// 10 | /// Represents global configuration for JSON serialization. 11 | /// 12 | public static class Globals 13 | { 14 | static JsonSerializerOptions? _jsonSerializerOptions; 15 | 16 | /// 17 | /// Gets the global - it can be null if not initialized. 18 | /// 19 | public static JsonSerializerOptions JsonSerializerOptions 20 | { 21 | get 22 | { 23 | if (_jsonSerializerOptions is null) 24 | { 25 | Configure(); 26 | } 27 | 28 | return _jsonSerializerOptions!; 29 | } 30 | } 31 | 32 | /// 33 | /// Configure the globals. 34 | /// 35 | /// . 36 | public static void Configure() 37 | { 38 | if (_jsonSerializerOptions is not null) 39 | { 40 | return; 41 | } 42 | 43 | _jsonSerializerOptions = new() 44 | { 45 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 46 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, 47 | Converters = 48 | { 49 | new ConceptAsJsonConverterFactory() 50 | } 51 | }; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Fundamentals/IArrayIndexers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Fundamentals; 5 | 6 | /// 7 | /// Defines a system working with . 8 | /// 9 | public interface IArrayIndexers 10 | { 11 | /// 12 | /// Gets all . 13 | /// 14 | /// Collection of . 15 | IEnumerable All { get; } 16 | 17 | /// 18 | /// Get an for a . 19 | /// 20 | /// to get for. 21 | /// The . 22 | ArrayIndexer GetFor(PropertyPath propertyPath); 23 | 24 | /// 25 | /// Check if there is an for a . 26 | /// 27 | /// to check for. 28 | /// True if it has it, false if not. 29 | bool HasFor(PropertyPath propertyPath); 30 | } 31 | -------------------------------------------------------------------------------- /Fundamentals/ICommand.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Fundamentals; 5 | 6 | public interface ICommand 7 | { 8 | } -------------------------------------------------------------------------------- /Fundamentals/IContractToImplementorsMap.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Fundamentals; 5 | 6 | /// 7 | /// Defines a system that handles the relationship between contracts and their implementors. 8 | /// 9 | /// 10 | /// A contract is considered an abstract type or an interface. 11 | /// 12 | public interface IContractToImplementorsMap 13 | { 14 | /// 15 | /// Gets the dictionary holding the mapping between contracts and all their implementors. 16 | /// 17 | IDictionary> ContractsAndImplementors { get; } 18 | 19 | /// 20 | /// Gets all the types in the map. 21 | /// 22 | IEnumerable All { get; } 23 | 24 | /// 25 | /// Feed the map with types. 26 | /// 27 | /// Types to feed with. 28 | void Feed(IEnumerable types); 29 | 30 | /// 31 | /// Retrieve implementors of a specific contract. 32 | /// 33 | /// Type of contract to retrieve for. 34 | /// Types implementing the contract. 35 | IEnumerable GetImplementorsFor(); 36 | 37 | /// 38 | /// Retrieve implementors of a specific contract. 39 | /// 40 | /// of contract to retrieve for. 41 | /// Types implementing the contract. 42 | IEnumerable GetImplementorsFor(Type contract); 43 | } 44 | -------------------------------------------------------------------------------- /Fundamentals/IImplementationsOf.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Fundamentals; 5 | 6 | /// 7 | /// Defines something that can discover implementations of types enable enumeration of these. 8 | /// 9 | /// Base type to discover for - must be an abstract class or an interface. 10 | public interface IImplementationsOf : IEnumerable 11 | where T : class 12 | { 13 | } 14 | -------------------------------------------------------------------------------- /Fundamentals/IInstancesOf.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Fundamentals; 5 | 6 | /// 7 | /// Defines something that can discover implementations of types and give instance of these types 8 | /// when enumerated over. 9 | /// 10 | /// Base type to discover for - must be an abstract class or an interface. 11 | public interface IInstancesOf : IEnumerable 12 | where T : class 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /Fundamentals/IPropertyPathSegment.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Fundamentals; 5 | 6 | /// 7 | /// Defines a segment within a . 8 | /// 9 | public interface IPropertyPathSegment 10 | { 11 | /// 12 | /// Gets the value that represents the segment. 13 | /// 14 | string Value { get; } 15 | } 16 | -------------------------------------------------------------------------------- /Fundamentals/ITypeInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Fundamentals; 5 | 6 | /// 7 | /// Defines information for types. 8 | /// 9 | public interface ITypeInfo 10 | { 11 | /// 12 | /// Gets a value indicating whether or not the type has a default constructor that takes no arguments. 13 | /// 14 | bool HasDefaultConstructor { get; } 15 | } 16 | -------------------------------------------------------------------------------- /Fundamentals/ITypes.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Reflection; 5 | 6 | namespace Fundamentals; 7 | 8 | /// 9 | /// Defines a system for working with types. 10 | /// 11 | public interface ITypes 12 | { 13 | /// 14 | /// Gets returns all collected types. 15 | /// 16 | IEnumerable All { get; } 17 | 18 | /// 19 | /// Find a single implementation of a basetype. 20 | /// 21 | /// Basetype to find for. 22 | /// Type found. 23 | /// 24 | /// If the base type is an interface, it will look for any types implementing the interface. 25 | /// If it is a class, it will find anyone inheriting from that class. 26 | /// 27 | /// If there is more than one instance found. 28 | Type FindSingle(); 29 | 30 | /// 31 | /// Find multiple implementations of a basetype. 32 | /// 33 | /// Basetype to find for. 34 | /// All types implementing or inheriting from the given basetype. 35 | /// 36 | /// If the base type is an interface, it will look for any types implementing the interface. 37 | /// If it is a class, it will find anyone inheriting from that class. 38 | /// 39 | IEnumerable FindMultiple(); 40 | 41 | /// 42 | /// Find a single implementation of a basetype. 43 | /// 44 | /// Basetype to find for. 45 | /// Type found. 46 | /// 47 | /// If the base type is an interface, it will look for any types implementing the interface. 48 | /// If it is a class, it will find anyone inheriting from that class. 49 | /// 50 | /// If there is more than one instance found. 51 | Type FindSingle(Type type); 52 | 53 | /// 54 | /// Find multiple implementations of a basetype. 55 | /// 56 | /// Basetype to find for. 57 | /// All types implementing or inheriting from the given basetype. 58 | /// 59 | /// If the base type is an interface, it will look for any types implementing the interface. 60 | /// If it is a class, it will find anyone inheriting from that class. 61 | /// 62 | IEnumerable FindMultiple(Type type); 63 | 64 | /// 65 | /// Find a single type using the full name, without assembly. 66 | /// 67 | /// full name of the type to find. 68 | /// The type is found, null otherwise. 69 | Type FindTypeByFullName(string fullName); 70 | } 71 | -------------------------------------------------------------------------------- /Fundamentals/ImplementationsOf.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Collections; 5 | 6 | namespace Fundamentals; 7 | 8 | /// 9 | /// Represents an implementation of . 10 | /// 11 | /// Base type to discover for - must be an abstract class or an interface. 12 | public class ImplementationsOf : IImplementationsOf 13 | where T : class 14 | { 15 | readonly IEnumerable _types; 16 | 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | /// to use for finding types. 21 | public ImplementationsOf(ITypes types) 22 | { 23 | _types = types.FindMultiple(); 24 | } 25 | 26 | /// 27 | public IEnumerator GetEnumerator() 28 | { 29 | return _types.GetEnumerator(); 30 | } 31 | 32 | /// 33 | IEnumerator IEnumerable.GetEnumerator() 34 | { 35 | return _types.GetEnumerator(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Fundamentals/InstancesOf.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Collections; 5 | 6 | namespace Fundamentals; 7 | 8 | /// 9 | /// Represents an implementation of . 10 | /// 11 | /// Base type to discover for - must be an abstract class or an interface. 12 | public class InstancesOf : IInstancesOf 13 | where T : class 14 | { 15 | readonly IEnumerable _types; 16 | readonly IServiceProvider _serviceProvider; 17 | 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | /// used for finding types. 22 | /// used for managing instances of the types when needed. 23 | public InstancesOf(ITypes types, IServiceProvider serviceProvider) 24 | { 25 | _types = types.FindMultiple(); 26 | _serviceProvider = serviceProvider; 27 | } 28 | 29 | /// 30 | public IEnumerator GetEnumerator() 31 | { 32 | foreach (var type in _types) yield return (T)_serviceProvider.GetService(type)!; 33 | } 34 | 35 | /// 36 | IEnumerator IEnumerable.GetEnumerator() 37 | { 38 | foreach (var type in _types) yield return _serviceProvider.GetService(type); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Fundamentals/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Aksio Insurtech 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Fundamentals/MemberInfoExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Reflection; 5 | 6 | namespace Fundamentals; 7 | 8 | public static class MemberInfoExtensions 9 | { 10 | public static bool HasAttribute(this MemberInfo memberInfo) where TAttribute : Attribute 11 | => memberInfo.GetCustomAttributes().Any(); 12 | } 13 | -------------------------------------------------------------------------------- /Fundamentals/Metrics/CounterAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Fundamentals.Metrics; 2 | 3 | [AttributeUsage(AttributeTargets.Method)] 4 | public sealed class CounterAttribute : Attribute 5 | { 6 | public CounterAttribute(string name, string description) 7 | { 8 | Name = name; 9 | Description = description; 10 | } 11 | 12 | public string Name { get; } 13 | 14 | public string Description { get; } 15 | } 16 | -------------------------------------------------------------------------------- /Fundamentals/Metrics/GlobalMetrics.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.Metrics; 2 | 3 | namespace Fundamentals.Metrics; 4 | 5 | public static class GlobalMetrics 6 | { 7 | public static Meter Meter = new("Global"); 8 | } -------------------------------------------------------------------------------- /Fundamentals/MissingArrayIndexerForPropertyPath.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Fundamentals; 5 | 6 | /// 7 | /// Exception that gets thrown when a is missing for a . 8 | /// 9 | public class MissingArrayIndexerForPropertyPath : Exception 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// it is missing for. 15 | public MissingArrayIndexerForPropertyPath(PropertyPath propertyPath) : base($"Missing array indexer for '{propertyPath}'") 16 | { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Fundamentals/MultipleTypesFound.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Fundamentals; 5 | 6 | /// 7 | /// Exception that gets thrown when multiple types are found and not allowed. 8 | /// 9 | public class MultipleTypesFound : ArgumentException 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// Type that multiple of it. 15 | /// The types that was found. 16 | public MultipleTypesFound(Type type, IEnumerable typesFound) 17 | : base($"More than one type found for '{type.FullName}' - types found : [{string.Join(",", typesFound.Select(_ => _.FullName))}]") 18 | { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Fundamentals/PIIConceptAs.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Fundamentals; 5 | 6 | public record PIIConceptAs(T Value) : ConceptAs(Value) 7 | { 8 | } 9 | -------------------------------------------------------------------------------- /Fundamentals/PropertyName.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Fundamentals; 5 | 6 | /// 7 | /// Represents a for a property name. 8 | /// 9 | /// Name of the property. 10 | public record PropertyName(string Value) : IPropertyPathSegment 11 | { 12 | /// 13 | public override string ToString() => Value; 14 | } 15 | -------------------------------------------------------------------------------- /Fundamentals/README.md: -------------------------------------------------------------------------------- 1 | # Fundamentals 2 | 3 | All code in this folder is originally from an open source project the author (Einar Ingebrigtsen) is part of maintaining. 4 | You can find the original code [here](https://github.com/aksio-insurtech/Cratis/tree/main/Source/Fundamentals). 5 | 6 | If you see benefits in using this and don't want to maintain your own copy, you can leverage the fundamentals package 7 | directly from NuGet [here](https://www.nuget.org/packages/Aksio.Cratis.Fundamentals). 8 | 9 | The original license file has been included here in this folder for clarity and legal purposes. 10 | -------------------------------------------------------------------------------- /Fundamentals/SegmentValueIsNotCollection.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Fundamentals; 5 | 6 | /// 7 | /// Exception that gets thrown when a segment within a is not an ExpandoObject. 8 | /// 9 | public class SegmentValueIsNotCollection : Exception 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// . 15 | /// . 16 | public SegmentValueIsNotCollection(PropertyPath propertyPath, IPropertyPathSegment segment) : base($"Segment '{segment.Value}' in '{propertyPath}' is not a collection of ExpandoObject") 17 | { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Fundamentals/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace Fundamentals; 5 | 6 | public static class ServiceCollectionExtensions 7 | { 8 | static readonly string[] _namespacesToIgnoreForSelfBinding = new[] { "System", "Microsoft" }; 9 | 10 | public static IServiceCollection AddBindingsByConvention(this IServiceCollection services, ITypes types) 11 | { 12 | Func convention = (i, t) => i.Namespace == t.Namespace && i.Name == $"I{t.Name}"; 13 | var conventionBasedTypes = types!.All.Where(_ => 14 | { 15 | var interfaces = _.GetInterfaces(); 16 | if (interfaces.Length > 0) 17 | { 18 | var conventionInterface = interfaces.SingleOrDefault(i => convention(i, _)); 19 | if (conventionInterface != default) 20 | { 21 | return types!.All.Count(type => type.HasInterface(conventionInterface)) == 1; 22 | } 23 | } 24 | return false; 25 | }); 26 | 27 | foreach (var conventionBasedType in conventionBasedTypes) 28 | { 29 | var interfaceToBind = types.All.Single(_ => _.IsInterface && convention(_, conventionBasedType)); 30 | if (services.Any(_ => _.ServiceType == interfaceToBind)) 31 | { 32 | continue; 33 | } 34 | 35 | _ = conventionBasedType.HasAttribute() ? 36 | services.AddSingleton(interfaceToBind, conventionBasedType) : 37 | services.AddTransient(interfaceToBind, conventionBasedType); 38 | } 39 | 40 | return services; 41 | } 42 | 43 | public static IServiceCollection AddSelfBinding(this IServiceCollection services, ITypes types) 44 | { 45 | const TypeAttributes staticType = TypeAttributes.Abstract | TypeAttributes.Sealed; 46 | 47 | types.All.Where(_ => 48 | (_.Attributes & staticType) != staticType && 49 | !_.IsInterface && 50 | !_.IsAbstract && 51 | !ShouldIgnoreNamespace(_.Namespace ?? string.Empty) && 52 | !HasConstructorWithUnresolvableParameters(_) && 53 | !HasConstructorWithRecordTypes(_) && 54 | !_.IsAssignableTo(typeof(Exception)) && 55 | services.Any(s => s.ServiceType != _)).ToList().ForEach(_ => 56 | { 57 | var __ = _.HasAttribute() ? 58 | services.AddSingleton(_, _) : 59 | services.AddTransient(_, _); 60 | }); 61 | 62 | return services; 63 | } 64 | 65 | static bool ShouldIgnoreNamespace(string namespaceToCheck) => 66 | _namespacesToIgnoreForSelfBinding.Any(n => namespaceToCheck.StartsWith(n)); 67 | 68 | static bool HasConstructorWithUnresolvableParameters(Type type) => 69 | type.GetConstructors().Any(_ => _.GetParameters().Any(p => p.ParameterType.IsAPrimitiveType())); 70 | 71 | static bool HasConstructorWithRecordTypes(Type type) => 72 | type.GetConstructors().Any(_ => _.GetParameters().Any(p => p.ParameterType.IsRecord())); 73 | } 74 | -------------------------------------------------------------------------------- /Fundamentals/SingletonAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Fundamentals; 2 | 3 | [AttributeUsage(AttributeTargets.Class)] 4 | public sealed class SingletonAttribute : Attribute 5 | { 6 | } 7 | -------------------------------------------------------------------------------- /Fundamentals/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Fundamentals; 5 | 6 | /// 7 | /// Provides a set of extension methods to . 8 | /// 9 | public static class StringExtensions 10 | { 11 | /// 12 | /// Convert a string into a camel cased string. 13 | /// 14 | /// string to convert. 15 | /// Converted string. 16 | public static string ToCamelCase(this string stringToConvert) 17 | { 18 | if (!string.IsNullOrEmpty(stringToConvert)) 19 | { 20 | if (stringToConvert.Length == 1) 21 | return stringToConvert.ToLowerInvariant(); 22 | 23 | var firstLetter = stringToConvert[0].ToString().ToLowerInvariant(); 24 | return $"{firstLetter}{stringToConvert.Substring(1)}"; 25 | } 26 | 27 | return stringToConvert; 28 | } 29 | 30 | /// 31 | /// Convert a string into a pascal cased string. 32 | /// 33 | /// string to convert. 34 | /// Converted string. 35 | public static string ToPascalCase(this string stringToConvert) 36 | { 37 | if (!string.IsNullOrEmpty(stringToConvert)) 38 | { 39 | if (stringToConvert.Length == 1) 40 | return stringToConvert.ToUpperInvariant(); 41 | 42 | var firstLetter = stringToConvert[0].ToString().ToUpperInvariant(); 43 | return $"{firstLetter}{stringToConvert.Substring(1)}"; 44 | } 45 | 46 | return stringToConvert; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Fundamentals/TypeConversion.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Fundamentals; 5 | 6 | public static class TypeConversion 7 | { 8 | public static object Convert(Type type, object value) 9 | { 10 | var val = new object(); 11 | 12 | if (type.IsGuid()) 13 | { 14 | if (value.GetType().IsGuid()) 15 | { 16 | val = value; 17 | } 18 | else if (value.GetType().IsString()) 19 | { 20 | val = Guid.Parse(value.ToString()!); 21 | } 22 | else 23 | { 24 | val = value.GetType() == typeof(byte[]) ? new Guid((value as byte[])!) : (object)Guid.Empty; 25 | } 26 | } 27 | else if (type.IsString()) 28 | { 29 | val = value ?? string.Empty; 30 | } 31 | else if (type.IsDate()) 32 | { 33 | val = value ?? default(DateTime); 34 | } 35 | else if (type.IsDateOnly()) 36 | { 37 | if (value is DateOnly) 38 | { 39 | val = value; 40 | } 41 | else if (DateOnly.TryParse(value.ToString(), out var dateOnlyValue)) 42 | { 43 | val = dateOnlyValue; 44 | } 45 | else 46 | { 47 | val = default(DateOnly); 48 | } 49 | } 50 | else if (type.IsTimeOnly()) 51 | { 52 | if (value is TimeOnly) 53 | { 54 | val = value; 55 | } 56 | else if (TimeOnly.TryParse(value.ToString(), out var timeOnlyValue)) 57 | { 58 | val = timeOnlyValue; 59 | } 60 | else 61 | { 62 | val = default(TimeOnly); 63 | } 64 | } 65 | else if (type.IsDateTimeOffset()) 66 | { 67 | if (value is DateTimeOffset) 68 | { 69 | val = value; 70 | } 71 | else if (DateTimeOffset.TryParse(value.ToString(), out var dateTimeOffsetValue)) 72 | { 73 | val = dateTimeOffsetValue; 74 | } 75 | else 76 | { 77 | val = default(DateTimeOffset); 78 | } 79 | } 80 | else if (type.IsAPrimitiveType()) 81 | { 82 | val = value ?? Activator.CreateInstance(type); 83 | } 84 | 85 | if (val is not null && val.GetType() != type && !IsGuidFromString(type, val)) 86 | { 87 | val = System.Convert.ChangeType(val, type, null); 88 | } 89 | return val!; 90 | } 91 | 92 | static bool IsGuidFromString(Type genericArgumentType, object value) 93 | { 94 | return genericArgumentType == typeof(Guid) && value.GetType() == typeof(string); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Fundamentals/TypeInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Reflection; 5 | 6 | namespace Fundamentals; 7 | 8 | /// 9 | /// Represents an implementation of . 10 | /// 11 | /// Type it holds info for. 12 | public class TypeInfo : ITypeInfo 13 | { 14 | /// 15 | /// Gets a singleton instance of the TypeInfo. 16 | /// 17 | public static readonly TypeInfo Instance = new(); 18 | 19 | /// 20 | public bool HasDefaultConstructor { get; } 21 | 22 | TypeInfo() 23 | { 24 | var type = typeof(T); 25 | var typeInfo = type.GetTypeInfo(); 26 | 27 | var defaultConstructor = typeInfo.DeclaredConstructors.Any(_ => _.GetParameters().Length == 0); 28 | 29 | HasDefaultConstructor = 30 | typeInfo.IsValueType || 31 | defaultConstructor; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Fundamentals/TypeIsNotAConcept.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Fundamentals; 5 | 6 | public class TypeIsNotAConcept : Exception 7 | { 8 | public TypeIsNotAConcept(Type type) 9 | : base($"Type '{type.AssemblyQualifiedName}' is not a concept - implement ConceptAs<> for it to be one.") 10 | { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Fundamentals/Types.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Reflection; 5 | using Microsoft.Extensions.DependencyModel; 6 | 7 | namespace Fundamentals; 8 | 9 | public class Types : ITypes 10 | { 11 | readonly IContractToImplementorsMap _contractToImplementorsMap = new ContractToImplementorsMap(); 12 | 13 | public Types(params string[] assemblyPrefixesToInclude) 14 | { 15 | All = DiscoverAllTypes(assemblyPrefixesToInclude); 16 | _contractToImplementorsMap.Feed(All); 17 | } 18 | 19 | public IEnumerable All { get; } 20 | 21 | IEnumerable DiscoverAllTypes(IEnumerable assemblyPrefixesToInclude) 22 | { 23 | var entryAssembly = Assembly.GetEntryAssembly(); 24 | var dependencyModel = DependencyContext.Load(entryAssembly); 25 | var projectReferencedAssemblies = dependencyModel.RuntimeLibraries 26 | .Where(_ => _.Type.Equals("project")) 27 | .Select(_ => Assembly.Load(_.Name)) 28 | .ToArray(); 29 | 30 | var assemblies = dependencyModel.RuntimeLibraries 31 | .Where(_ => _.RuntimeAssemblyGroups.Count > 0 && 32 | assemblyPrefixesToInclude.Any(asm => _.Name.StartsWith(asm))) 33 | .Select(_ => 34 | { 35 | try 36 | { 37 | return Assembly.Load(_.Name); 38 | } 39 | catch 40 | { 41 | return null!; 42 | } 43 | }) 44 | .Where(_ => _ is not null) 45 | .Distinct() 46 | .ToList(); 47 | 48 | assemblies.AddRange(projectReferencedAssemblies); 49 | return assemblies.SelectMany(_ => _.GetTypes()).ToArray(); 50 | } 51 | 52 | public Type FindSingle() => FindSingle(typeof(T)); 53 | 54 | public IEnumerable FindMultiple() => FindMultiple(typeof(T)); 55 | 56 | public Type FindSingle(Type type) 57 | { 58 | var typesFound = _contractToImplementorsMap.GetImplementorsFor(type); 59 | ThrowIfMultipleTypesFound(type, typesFound); 60 | return typesFound.SingleOrDefault()!; 61 | } 62 | 63 | public IEnumerable FindMultiple(Type type) => _contractToImplementorsMap.GetImplementorsFor(type); 64 | 65 | public Type FindTypeByFullName(string fullName) 66 | { 67 | var typeFound = _contractToImplementorsMap.All.SingleOrDefault(t => t.FullName == fullName); 68 | ThrowIfTypeNotFound(fullName, typeFound!); 69 | return typeFound!; 70 | } 71 | 72 | void ThrowIfMultipleTypesFound(Type type, IEnumerable typesFound) 73 | { 74 | if (typesFound.Count() > 1) 75 | { 76 | throw new MultipleTypesFound(type, typesFound); 77 | } 78 | } 79 | 80 | void ThrowIfTypeNotFound(string fullName, Type typeFound) 81 | { 82 | if (typeFound == null) 83 | { 84 | throw new UnableToResolveTypeByName(fullName); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Fundamentals/UnableToResolvePropertyPathOnType.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Fundamentals; 5 | 6 | /// 7 | /// Exception that gets thrown when property path is not possible to be resolved on a type. 8 | /// 9 | public class UnableToResolvePropertyPathOnType : Exception 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// Type that does not hold the property path. 15 | /// The that is not possible to resolve. 16 | public UnableToResolvePropertyPathOnType(Type type, PropertyPath path) : base($"Unable to resolve property path '${path}' on type '${type.FullName}'") 17 | { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Fundamentals/UnableToResolveTypeByName.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aksio Insurtech. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Fundamentals; 5 | 6 | /// 7 | /// Exception that gets thrown when a type is not possible to be resolved by its name. 8 | /// 9 | public class UnableToResolveTypeByName : ArgumentException 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// Name of the type that was not possible to resolve. 15 | public UnableToResolveTypeByName(string typeName) 16 | : base($"Unable to resolve '{typeName}'.") 17 | { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Packt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Metaprogramming in C# 2 | 3 | Metaprogramming in C# 4 | 5 | This is the code repository for [Metaprogramming in C#](https://www.packtpub.com/product/metaprogramming-in-c/9781837635429?utm_source=github&utm_medium=repository&utm_campaign=), published by Packt. 6 | 7 | **Automate your .NET development and simplify overcomplicated code** 8 | 9 | ## What is this book about? 10 | Unlock the full potential of C# development with Metaprogramming in C#. Write better solutions that are easy to maintain, scale, and more secure. Discover advanced methods and approaches to automate tasks, generate code, and improve your productivity while taking your C# development skills to the next level. 11 | 12 | This book covers the following exciting features: 13 | * Explore how to leverage the .NET runtime 14 | * Improve code quality and increase productivity 15 | * Write adaptable code for changing requirements 16 | * Learn Roslyn for code generation and static analysis 17 | * Master metaprogramming and its practical implementations 18 | * Use Dynamic Language Runtime for flexible and expressive programming 19 | 20 | If you feel this book is for you, get your [copy](https://www.amazon.com/dp/1837635420) today! 21 | 22 | https://www.packtpub.com/ 24 | 25 | ## Instructions and Navigations 26 | All of the code is organized into folders. For example, Chapter02. 27 | 28 | The code will look like the following: 29 | ``` 30 | namespace Fundamentals.Compliance; 31 | public interface ICanProvideComplianceMetadataForType 32 | { 33 | bool CanProvide(Type type); 34 | ComplianceMetadata Provide(Type type); 35 | } 36 | ``` 37 | 38 | **Following is what you need for this book:** 39 | This book is for C# developers interested in learning about the .NET runtime and how to leverage it for writing maintainable, scalable, and secure code. Software architects who are responsible for designing and managing complex software solutions will also benefit from the book. 40 | 41 | With the following software and hardware list you can run all code files present in the book (Chapter 1-17). 42 | ### Software and Hardware List 43 | | Chapter | Software required | OS required | 44 | | -------- | ------------------------------------ | ----------------------------------- | 45 | | 1-17 | C# using .NET 7 | Windows, Mac OS X, and Linux (Any) | 46 | | 3-13 | Postman | Windows, Mac OS X, and Linux (Any) | 47 | | 8-10 | MongoDB | Windows, Mac OS X, and Linux (Any) | 48 | 49 | 50 | We also provide a PDF file that has color images of the screenshots/diagrams used in this book. [Click here to download it](https://static.packt-cdn.com/downloads/9781837635429_ColorImages.pdf). 51 | 52 | ### Related products 53 | * Real-World Implementation of C# Design Patterns [[Packt]](https://www.packtpub.com/product/real-world-implementation-of-c-design-patterns/9781803242736?utm_source=github&utm_medium=repository&utm_campaign=) [[Amazon]](https://www.amazon.com/dp/1803242736) 54 | 55 | * High-Performance Programming in C# and .NET [[Packt]](https://www.packtpub.com/product/high-performance-programming-in-c-and-net/9781800564718?utm_source=github&utm_medium=repository&utm_campaign=) [[Amazon]](https://www.amazon.com/dp/1800564716) 56 | 57 | * Template Metaprogramming with C++ [[Packt]](https://www.packtpub.com/product/template-metaprogramming-with-c/9781803243450?utm_source=github&utm_medium=repository&utm_campaign=) [[Amazon]](https://www.amazon.com/dp/1803243457) 58 | 59 | 60 | 61 | ## Original Source 62 | 63 | The following folders/projects contains code that is copied and modified from open source projects maintained by the author: 64 | 65 | | Folder/Project | Original source | 66 | | -------------- | --------------- | 67 | | Fundamentals | [Fundamentals](https://github.com/aksio-insurtech/Fundamentals) | 68 | | Roslyn.Extensions | [Aksio Defaults](https://github.com/aksio-insurtech/Defaults) | 69 | | Roslyn.Extensions.Tests | [Aksio Defaults](https://github.com/aksio-insurtech/Defaults) | 70 | 71 | If you want to leverage the original maintained code provided by the projects, you can visit these projects and use the packages they produce directly. 72 | 73 | ## Get to Know the Author 74 | **Einar Ingebrigtsen** 75 | works as chief architect at Aksio InsurTech, a company focusing on building insurance and pension solutions. His heart is in architecture and improving the lives of developers and he loves to create solutions that make other developers more productive and help in delivering great products to end users. Einar has been developing software professionally since 1994 and has done so in everything from games on different platforms to broadcast TV software, to telecom software, to line of business software within multiple different verticals. Of all of his experiences, he has fallen in love with a specific flavor of architecture, mindset, and approach – namely, event sourcing. Most of his time at work (and spare time) is devoted to building out a platform that has the goal of democratizing event sourcing called Cratis 76 | -------------------------------------------------------------------------------- /Roslyn.Extensions.Tests/ExceptionShouldNotBeSuffixed/AnalyzerTests.cs: -------------------------------------------------------------------------------- 1 | namespace Roslyn.Extensions.CodeAnalysis.ExceptionShouldNotBeSuffixed; 2 | 3 | using Xunit; 4 | using Verify = Microsoft.CodeAnalysis.CSharp.Testing.XUnit.AnalyzerVerifier; 5 | 6 | public class AnalyzerTests 7 | { 8 | [Fact] 9 | public async Task WithoutSuffix() 10 | { 11 | const string content = @" 12 | using System; 13 | 14 | namespace MyNamespace; 15 | public class SomethingWentWrong : Exception 16 | { 17 | } 18 | "; 19 | 20 | await Verify.VerifyAnalyzerAsync(content); 21 | } 22 | 23 | [Fact] 24 | public async Task WithSuffix() 25 | { 26 | const string content = @" 27 | using System; 28 | 29 | namespace MyNamespace; 30 | public class MyException : Exception 31 | { 32 | } 33 | "; 34 | 35 | var expected = Verify.Diagnostic().WithLocation(5, 30).WithArguments("MyException"); 36 | await Verify.VerifyAnalyzerAsync(content, expected); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Roslyn.Extensions.Tests/ExceptionShouldNotBeSuffixed/CodeFixTests.cs: -------------------------------------------------------------------------------- 1 | namespace Roslyn.Extensions.CodeAnalysis.ExceptionShouldNotBeSuffixed; 2 | 3 | using Xunit; 4 | using Verify = Microsoft.CodeAnalysis.CSharp.Testing.XUnit.CodeFixVerifier; 5 | 6 | public class CodeFixTests 7 | { 8 | [Fact] 9 | public async Task WithoutSuffix() 10 | { 11 | const string content = @" 12 | using System; 13 | 14 | namespace MyNamespace; 15 | public class SomethingWentWrong : Exception 16 | { 17 | } 18 | "; 19 | 20 | await Verify.VerifyCodeFixAsync(content, content); 21 | } 22 | 23 | [Fact] 24 | public async Task WithSuffix() 25 | { 26 | const string content = @" 27 | using System; 28 | 29 | namespace MyNamespace; 30 | public class MyException : Exception 31 | { 32 | } 33 | "; 34 | 35 | var expected = Verify.Diagnostic().WithLocation(5, 30).WithArguments("MyException"); 36 | await Verify.VerifyCodeFixAsync(content, expected, content.Replace("MyException", "My")); 37 | } 38 | } -------------------------------------------------------------------------------- /Roslyn.Extensions.Tests/Roslyn.Extensions.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | net7.0 9 | enable 10 | true 11 | enable 12 | $(NoWarn);IDE0065 13 | 14 | 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | runtime; build; native; contentfiles; analyzers; buildtransitive 27 | all 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Roslyn.Extensions/AnalyzerReleases.Shipped.md: -------------------------------------------------------------------------------- 1 | ## Release 1.0 2 | 3 | ### New Rules 4 | 5 | Rule ID | Category | Severity | Notes 6 | --------|----------|----------|-------------------- 7 | PP0001 | Naming | Error | 8 | -------------------------------------------------------------------------------- /Roslyn.Extensions/AnalyzerReleases.Unshipped.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Roslyn.Extensions/CodeAnalysis/ExceptionShouldNotBeSuffixed/Analyzer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.CodeAnalysis.CSharp; 4 | using Microsoft.CodeAnalysis.CSharp.Syntax; 5 | using Microsoft.CodeAnalysis.Diagnostics; 6 | 7 | namespace Roslyn.Extensions.CodeAnalysis.ExceptionShouldNotBeSuffixed; 8 | 9 | [DiagnosticAnalyzer(LanguageNames.CSharp)] 10 | public class Analyzer : DiagnosticAnalyzer 11 | { 12 | public const string DiagnosticId = "PP0001"; 13 | 14 | public static readonly DiagnosticDescriptor BrokenRule = new( 15 | id: DiagnosticId, 16 | title: "ExceptionShouldNotBeSuffixed", 17 | messageFormat: "The use of the word 'Exception' should not be added as a suffix - create a well understood and self explanatory name for the exception", 18 | category: "Naming", 19 | defaultSeverity: DiagnosticSeverity.Error, 20 | isEnabledByDefault: true, 21 | description: null, 22 | helpLinkUri: string.Empty, 23 | customTags: Array.Empty()); 24 | 25 | public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(BrokenRule); 26 | 27 | public override void Initialize(AnalysisContext context) 28 | { 29 | context.EnableConcurrentExecution(); 30 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); 31 | context.RegisterSyntaxNodeAction( 32 | HandleClassDeclaration, 33 | ImmutableArray.Create( 34 | SyntaxKind.ClassDeclaration)); 35 | } 36 | 37 | void HandleClassDeclaration(SyntaxNodeAnalysisContext context) 38 | { 39 | var classDeclaration = context.Node as ClassDeclarationSyntax; 40 | if (classDeclaration?.BaseList == null || classDeclaration?.BaseList?.Types == null) return; 41 | 42 | var classSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclaration); 43 | if (classSymbol?.BaseType is null) return; 44 | 45 | var exceptionType = context.Compilation.GetTypeByMetadataName("System.Exception"); 46 | if (SymbolEqualityComparer.Default.Equals(classSymbol?.BaseType, exceptionType) && 47 | classDeclaration.Identifier.Text.EndsWith("Exception", StringComparison.InvariantCulture)) 48 | { 49 | var diagnostic = Diagnostic.Create(BrokenRule, classDeclaration.Identifier.GetLocation()); 50 | context.ReportDiagnostic(diagnostic); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Roslyn.Extensions/CodeAnalysis/ExceptionShouldNotBeSuffixed/CodeFix.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Composition; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CodeActions; 5 | using Microsoft.CodeAnalysis.CodeFixes; 6 | using Microsoft.CodeAnalysis.CSharp; 7 | using Microsoft.CodeAnalysis.CSharp.Syntax; 8 | 9 | namespace Roslyn.Extensions.CodeAnalysis.ExceptionShouldNotBeSuffixed; 10 | 11 | [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CodeFix))] 12 | [Shared] 13 | public class CodeFix : CodeFixProvider 14 | { 15 | public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(Analyzer.DiagnosticId); 16 | 17 | public override Task RegisterCodeFixesAsync(CodeFixContext context) 18 | { 19 | var diagnostic = context.Diagnostics[0]; 20 | context.RegisterCodeFix( 21 | CodeAction.Create( 22 | title: "Remove Exception suffix", 23 | createChangedDocument: c => RemoveSuffix(context.Document, diagnostic, c)), 24 | diagnostic); 25 | 26 | return Task.CompletedTask; 27 | } 28 | 29 | public override FixAllProvider? GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; 30 | 31 | async Task RemoveSuffix(Document document, Diagnostic diagnostic, CancellationToken c) 32 | { 33 | var root = await document.GetSyntaxRootAsync(c); 34 | 35 | if (!(root!.FindNode(diagnostic.Location.SourceSpan) is ClassDeclarationSyntax node)) return document; 36 | var newName = node.Identifier.Text.Replace("Exception", string.Empty); 37 | var newRoot = root.ReplaceNode(node, node.WithIdentifier(SyntaxFactory.Identifier(newName))); 38 | return document.WithSyntaxRoot(newRoot); 39 | } 40 | } -------------------------------------------------------------------------------- /Roslyn.Extensions/GDPR/GDPRSourceGenerator.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp.Syntax; 3 | 4 | namespace Roslyn.Extensions.GDPR; 5 | 6 | [Generator] 7 | public class GDPRSourceGenerator : ISourceGenerator 8 | { 9 | public void Execute(GeneratorExecutionContext context) 10 | { 11 | if (context.SyntaxReceiver is not GDPRSyntaxReceiver receiver) return; 12 | 13 | if (!context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.GDPRReport", out var filename)) return; 14 | 15 | var writer = File.CreateText(filename); 16 | writer.AutoFlush = true; 17 | 18 | var piiAttribute = context.Compilation.GetTypeByMetadataName("Fundamentals.Compliance.GDPR.PersonalIdentifiableInformationAttribute"); 19 | foreach (var candidate in receiver.Candidates) 20 | { 21 | var memberNamesAndReasons = new List<(string MemberName, string Reason)>(); 22 | 23 | var semanticModel = context.Compilation.GetSemanticModel(candidate.SyntaxTree); 24 | 25 | var symbols = new List(); 26 | 27 | if (candidate is RecordDeclarationSyntax record) 28 | { 29 | foreach (var parameter in record.ParameterList!.Parameters) 30 | { 31 | var parameterSymbol = semanticModel.GetDeclaredSymbol(parameter); 32 | if (parameterSymbol is not null) 33 | { 34 | symbols.Add(parameterSymbol); 35 | } 36 | } 37 | } 38 | 39 | foreach (var member in candidate.Members) 40 | { 41 | if (member is not PropertyDeclarationSyntax property) continue; 42 | 43 | var propertySymbol = semanticModel.GetDeclaredSymbol(property); 44 | if (propertySymbol is not null) 45 | { 46 | symbols.Add(propertySymbol); 47 | } 48 | } 49 | 50 | foreach (var symbol in symbols) 51 | { 52 | var attributes = symbol.GetAttributes(); 53 | var attribute = attributes.FirstOrDefault(_ => SymbolEqualityComparer.Default.Equals(_.AttributeClass?.OriginalDefinition, piiAttribute)); 54 | if (attribute is not null) 55 | { 56 | memberNamesAndReasons.Add((symbol.Name, attribute.ConstructorArguments[0].Value!.ToString())); 57 | } 58 | } 59 | 60 | if (memberNamesAndReasons.Count > 0) 61 | { 62 | var @namespace = (candidate.Parent as BaseNamespaceDeclarationSyntax)!.Name.ToString(); 63 | writer.WriteLine($"Type: {@namespace}.{candidate.Identifier.ValueText}"); 64 | writer.WriteLine("Members:"); 65 | foreach (var (memberName, reason) in memberNamesAndReasons) 66 | { 67 | var reasonText = string.IsNullOrEmpty(reason) ? "No reason provided" : reason; 68 | writer.WriteLine($" {memberName}: {reasonText}"); 69 | } 70 | 71 | writer.WriteLine(string.Empty); 72 | } 73 | } 74 | } 75 | 76 | public void Initialize(GeneratorInitializationContext context) 77 | { 78 | context.RegisterForSyntaxNotifications(() => new GDPRSyntaxReceiver()); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Roslyn.Extensions/GDPR/GDPRSyntaxReceiver.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp.Syntax; 3 | 4 | namespace Roslyn.Extensions.GDPR; 5 | 6 | public class GDPRSyntaxReceiver : ISyntaxReceiver 7 | { 8 | readonly List _candidates = new(); 9 | 10 | internal IEnumerable Candidates => _candidates; 11 | 12 | public void OnVisitSyntaxNode(SyntaxNode syntaxNode) 13 | { 14 | if (syntaxNode is not TypeDeclarationSyntax typeSyntax) return; 15 | _candidates.Add(typeSyntax); 16 | } 17 | } -------------------------------------------------------------------------------- /Roslyn.Extensions/Metrics/CounterTagTemplateData.cs: -------------------------------------------------------------------------------- 1 | namespace Roslyn.Extensions.Metrics; 2 | 3 | public class CounterTagTemplateData 4 | { 5 | public string Type { get; set; } = string.Empty; 6 | 7 | public string Name { get; set; } = string.Empty; 8 | } -------------------------------------------------------------------------------- /Roslyn.Extensions/Metrics/CounterTemplateData.cs: -------------------------------------------------------------------------------- 1 | namespace Roslyn.Extensions.Metrics; 2 | 3 | public class CounterTemplateData 4 | { 5 | public string Type { get; set; } = string.Empty; 6 | 7 | public string MethodName { get; set; } = string.Empty; 8 | 9 | public string Name { get; set; } = string.Empty; 10 | 11 | public string Description { get; set; } = string.Empty; 12 | 13 | public IEnumerable Tags { get; set; } = Enumerable.Empty(); 14 | } 15 | -------------------------------------------------------------------------------- /Roslyn.Extensions/Metrics/MetricsSourceGenerator.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp.Syntax; 3 | using Roslyn.Extensions.Templates; 4 | 5 | namespace Roslyn.Extensions.Metrics; 6 | 7 | [Generator] 8 | public class MetricsSourceGenerator : ISourceGenerator 9 | { 10 | public void Execute(GeneratorExecutionContext context) 11 | { 12 | if (context.SyntaxReceiver is not MetricsSyntaxReceiver receiver) return; 13 | 14 | var counterAttribute = context.Compilation.GetTypeByMetadataName("Fundamentals.Metrics.CounterAttribute`1"); 15 | foreach (var candidate in receiver.Candidates) 16 | { 17 | var templateData = new MetricsTemplateData 18 | { 19 | Namespace = (candidate.Parent as BaseNamespaceDeclarationSyntax)!.Name.ToString(), 20 | ClassName = candidate.Identifier.ValueText 21 | }; 22 | 23 | var semanticModel = context.Compilation.GetSemanticModel(candidate.SyntaxTree); 24 | foreach (var member in candidate.Members) 25 | { 26 | if (member is not MethodDeclarationSyntax method) continue; 27 | 28 | var methodSymbol = semanticModel.GetDeclaredSymbol(method); 29 | if (methodSymbol is not null) 30 | { 31 | var attributes = methodSymbol.GetAttributes(); 32 | var attribute = attributes.FirstOrDefault(_ => SymbolEqualityComparer.Default.Equals(_.AttributeClass?.OriginalDefinition, counterAttribute)); 33 | if (attribute is not null) 34 | { 35 | var tags = method.ParameterList.Parameters.Select(parameter => new CounterTagTemplateData 36 | { 37 | Name = parameter.Identifier.ValueText, 38 | Type = parameter.Type!.ToString() 39 | }); 40 | 41 | var type = attribute.AttributeClass!.TypeArguments[0].ToString(); 42 | var name = attribute.ConstructorArguments[0].Value!.ToString(); 43 | var description = attribute.ConstructorArguments[1].Value!.ToString(); 44 | templateData.Counters.Add( 45 | new CounterTemplateData 46 | { 47 | Name = name, 48 | Description = description, 49 | Type = type, 50 | MethodName = method.Identifier.ValueText, 51 | Tags = tags 52 | }); 53 | } 54 | } 55 | } 56 | 57 | if (templateData.Counters.Count > 0) 58 | { 59 | var source = TemplateTypes.Metrics(templateData); 60 | context.AddSource($"{candidate.Identifier.ValueText}.g.cs", source); 61 | } 62 | } 63 | } 64 | 65 | public void Initialize(GeneratorInitializationContext context) 66 | { 67 | context.RegisterForSyntaxNotifications(() => new MetricsSyntaxReceiver()); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Roslyn.Extensions/Metrics/MetricsSyntaxReceiver.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.CodeAnalysis.CSharp.Syntax; 4 | 5 | namespace Roslyn.Extensions.Metrics; 6 | 7 | public class MetricsSyntaxReceiver : ISyntaxReceiver 8 | { 9 | readonly List _candidates = new(); 10 | 11 | internal IEnumerable Candidates => _candidates; 12 | 13 | public void OnVisitSyntaxNode(SyntaxNode syntaxNode) 14 | { 15 | if (syntaxNode is not ClassDeclarationSyntax classSyntax) return; 16 | 17 | if (classSyntax.Modifiers.Any(modifier => modifier.IsKind(SyntaxKind.PartialKeyword)) && 18 | classSyntax.Modifiers.Any(modifier => modifier.IsKind(SyntaxKind.StaticKeyword))) 19 | { 20 | if (classSyntax.Members.Any(member => 21 | member.IsKind(SyntaxKind.MethodDeclaration) && 22 | member.Modifiers.Any(modifier => modifier.IsKind(SyntaxKind.PartialKeyword)) && 23 | member.Modifiers.Any(modifier => modifier.IsKind(SyntaxKind.StaticKeyword)))) 24 | { 25 | _candidates.Add(classSyntax); 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /Roslyn.Extensions/Metrics/MetricsTemplateData.cs: -------------------------------------------------------------------------------- 1 | namespace Roslyn.Extensions.Metrics; 2 | 3 | public class MetricsTemplateData 4 | { 5 | public string Namespace { get; set; } = string.Empty; 6 | 7 | public string ClassName { get; set; } = string.Empty; 8 | 9 | public List Counters { get; set; } = new(); 10 | } -------------------------------------------------------------------------------- /Roslyn.Extensions/Roslyn.Extensions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0 4 | 11.0 5 | enable 6 | enable 7 | true 8 | 9 | Packt Publishing 10 | all contributors 11 | https://github.com/PacktPublishing/Metaprogramming-in-C-Sharp 12 | git 13 | true 14 | MIT 15 | https://github.com/PacktPublishing/Metaprogramming-in-C-Sharp 16 | logo.png 17 | README.md 18 | 19 | true 20 | true 21 | true 22 | true 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 | $(GetTargetPathDependsOn);GetDependencyTargetPaths 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /Roslyn.Extensions/Roslyn.Extensions.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1.0.0 4 | Copyright Aksio Insurtech 5 | all contributors 6 | portable 7 | logo.png 8 | MIT 9 | git 10 | true 11 | 12 | true 13 | true 14 | 15 | 11.0 16 | 17 | $(NoWarn);NU5105;NU5118;CS0012 18 | enable 19 | True 20 | true 21 | 22 | True 23 | true 24 | false 25 | 26 | 27 | true 28 | True 29 | snupkg 30 | True 31 | 32 | 33 | false 34 | true 35 | 36 | True 37 | True 38 | True 39 | True 40 | AllEnabledByDefault 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | @(PackageReference->'%(Identity)') 59 | @(PackageReference->'%(Alias)') 60 | 61 | 62 | 63 | 64 | $(AliasName) 65 | 66 | 67 | 68 | 69 | 70 | 71 | src/ 72 | true 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /Roslyn.Extensions/Templates/Metrics.hbs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Diagnostics.Metrics; 3 | using Fundamentals.Metrics; 4 | 5 | namespace {{Namespace}}; 6 | 7 | #nullable enable 8 | 9 | public static partial class {{ClassName}} 10 | { 11 | {{#Counters}} 12 | static readonly Counter<{{Type}}> {{MethodName}}Metric = GlobalMetrics.Meter.CreateCounter<{{Type}}>("{{Name}}", "{{Description}}"); 13 | {{/Counters}} 14 | 15 | {{#Counters}} 16 | public static partial void {{MethodName}}({{#Tags}}{{Type}} {{Name}}{{#unless @last}}, {{/unless}}{{/Tags}}) 17 | { 18 | var tags = new TagList(new ReadOnlySpan>(new KeyValuePair[] 19 | { 20 | {{#Tags}} 21 | new("{{Name}}", {{name}}){{#unless @last}},{{/unless}} 22 | {{/Tags}} 23 | })); 24 | 25 | {{MethodName}}Metric.Add(1, tags); 26 | } 27 | {{/Counters}} 28 | } -------------------------------------------------------------------------------- /Roslyn.Extensions/Templates/TemplateTypes.cs: -------------------------------------------------------------------------------- 1 | using HandlebarsDotNet; 2 | 3 | namespace Roslyn.Extensions.Templates; 4 | 5 | public static class TemplateTypes 6 | { 7 | public static readonly HandlebarsTemplate Metrics = Handlebars.Compile(GetTemplate("Metrics")); 8 | 9 | static string GetTemplate(string name) 10 | { 11 | var rootType = typeof(TemplateTypes); 12 | var stream = rootType.Assembly.GetManifestResourceStream($"{rootType.Namespace}.{name}.hbs"); 13 | if (stream != default) 14 | { 15 | using var reader = new StreamReader(stream); 16 | return reader.ReadToEnd(); 17 | } 18 | 19 | return string.Empty; 20 | } 21 | } -------------------------------------------------------------------------------- /Roslyn.Extensions/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Metaprogramming-in-C-Sharp/ac312276b9e01f8c6caa800fd1dab10ace942cc6/Roslyn.Extensions/logo.png -------------------------------------------------------------------------------- /Roslyn.Extensions/stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 3 | "settings": { 4 | "indentation": { 5 | "useTabs": false, 6 | "indentationSize": 4, 7 | "tabSize": 4 8 | }, 9 | "orderingRules": { 10 | "systemUsingDirectivesFirst": true, 11 | "usingDirectivesPlacement": "outsideNamespace", 12 | "blankLinesBetweenUsingGroups": "omit" 13 | }, 14 | "documentationRules": { 15 | "xmlHeader": false, 16 | "documentInterfaces": true, 17 | "documentExposedElements": true, 18 | "documentationCulture": "en-US", 19 | "documentInternalElements": false, 20 | "documentPrivateElements": false, 21 | "documentPrivateFields": false, 22 | "fileNamingConvention": "stylecop", 23 | "companyName": "Packt Publishing", 24 | "variables": { 25 | "licenseName": "MIT", 26 | "licenseFile": "LICENSE" 27 | } 28 | } 29 | } 30 | } --------------------------------------------------------------------------------