├── .gitignore ├── CommanderDemo.Console ├── App.config ├── CommanderDemo.Console.csproj ├── Program.cs ├── Properties │ └── AssemblyInfo.cs └── packages.config ├── CommanderDemo.Domain ├── Authentication │ ├── GetUserToken.cs │ ├── LoginUser.cs │ └── LogoutUser.cs ├── CommanderDemo.Domain.csproj ├── CommanderDemo.Domain.csproj.DotSettings ├── Commands │ ├── Alert.cs │ ├── AsyncPing.cs │ ├── Ping.cs │ └── SendEmail.cs ├── Contacts │ ├── DeleteContact.cs │ ├── GetContact.cs │ ├── QueryContacts.cs │ └── SaveContact.cs ├── Models │ ├── Contact.cs │ ├── ContactDb.cs │ ├── MigrationsConfig.cs │ └── User.cs ├── Properties │ └── AssemblyInfo.cs ├── Users │ ├── DeleteUser.cs │ ├── GetUser.cs │ ├── QueryUsers.cs │ └── SaveUser.cs └── packages.config ├── CommanderDemo.Test ├── CommanderDemo.Test.csproj ├── CommanderDemo.Test.csproj.DotSettings ├── FakeDbSet.cs ├── Properties │ └── AssemblyInfo.cs ├── TestQueryContacts.cs ├── TestQueryUsers.cs ├── TestSaveContact.cs └── packages.config ├── CommanderDemo.Web ├── CommanderDemo.Web.csproj ├── CommanderDemo.Web.csproj.DotSettings ├── Global.asax ├── Global.asax.cs ├── Properties │ └── AssemblyInfo.cs ├── Scripts │ ├── jquery-1.6.4-vsdoc.js │ ├── jquery-1.6.4.js │ ├── jquery-1.6.4.min.js │ ├── jquery.signalR-2.2.0.js │ └── jquery.signalR-2.2.0.min.js ├── Services │ ├── AuditHandler.cs │ ├── AuditService.cs │ ├── HomeController.cs │ ├── LoggingHandler.cs │ ├── NotificationHub.cs │ ├── SignalrHandler.cs │ ├── Startup.cs │ ├── TaskRegistry.cs │ ├── TokenService.cs │ └── TransactionHandler.cs ├── Views │ ├── Home │ │ ├── EditUser.cshtml │ │ ├── Index.cshtml │ │ ├── Login.cshtml │ │ └── Users.cshtml │ └── Shared │ │ ├── _Layout.cshtml │ │ └── _Messages.cshtml ├── Web.config ├── config.js ├── gulpfile.js ├── package.json ├── packages.config ├── src │ ├── api.js │ ├── api.tt │ ├── app.html │ ├── app.js │ ├── contact-detail.html │ ├── contact-detail.js │ ├── contact-list.html │ ├── contact-list.js │ ├── json-rpc.js │ ├── loading-indicator.js │ ├── messages.js │ ├── no-selection.html │ ├── no-selection.js │ ├── notifications.js │ ├── utility.js │ └── web-api.js └── styles │ └── styles.css ├── CommanderDemo.sln ├── LICENSE ├── Presentation ├── CalcIncomeStmt.cs ├── CommanderDemo.hjt ├── IncomeStmtExcel.cs ├── IncomeStmtRpt.cs ├── Mediator.linq ├── Nancy_Akka_Mediator.linq ├── Owin_Akka_Mediator.linq ├── Presentation.html └── SaveContact.ps1 └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studo 2015 cache/options directory 26 | .vs/ 27 | 28 | # MSTest test Results 29 | [Tt]est[Rr]esult*/ 30 | [Bb]uild[Ll]og.* 31 | 32 | # NUNIT 33 | *.VisualState.xml 34 | TestResult.xml 35 | 36 | # Build Results of an ATL Project 37 | [Dd]ebugPS/ 38 | [Rr]eleasePS/ 39 | dlldata.c 40 | 41 | *_i.c 42 | *_p.c 43 | *_i.h 44 | *.ilk 45 | *.meta 46 | *.obj 47 | *.pch 48 | *.pdb 49 | *.pgc 50 | *.pgd 51 | *.rsp 52 | *.sbr 53 | *.tlb 54 | *.tli 55 | *.tlh 56 | *.tmp 57 | *.tmp_proj 58 | *.log 59 | *.vspscc 60 | *.vssscc 61 | .builds 62 | *.pidb 63 | *.svclog 64 | *.scc 65 | 66 | # Chutzpah Test files 67 | _Chutzpah* 68 | 69 | # Visual C++ cache files 70 | ipch/ 71 | *.aps 72 | *.ncb 73 | *.opensdf 74 | *.sdf 75 | *.cachefile 76 | 77 | # Visual Studio profiler 78 | *.psess 79 | *.vsp 80 | *.vspx 81 | 82 | # TFS 2012 Local Workspace 83 | $tf/ 84 | 85 | # Guidance Automation Toolkit 86 | *.gpState 87 | 88 | # ReSharper is a .NET coding add-in 89 | _ReSharper*/ 90 | *.[Rr]e[Ss]harper 91 | *.DotSettings.user 92 | 93 | # JustCode is a .NET coding addin-in 94 | .JustCode 95 | 96 | # TeamCity is a build add-in 97 | _TeamCity* 98 | 99 | # DotCover is a Code Coverage Tool 100 | *.dotCover 101 | 102 | # NCrunch 103 | _NCrunch_* 104 | .*crunch*.local.xml 105 | 106 | # MightyMoose 107 | *.mm.* 108 | AutoTest.Net/ 109 | 110 | # Web workbench (sass) 111 | .sass-cache/ 112 | 113 | # Installshield output folder 114 | [Ee]xpress/ 115 | 116 | # DocProject is a documentation generator add-in 117 | DocProject/buildhelp/ 118 | DocProject/Help/*.HxT 119 | DocProject/Help/*.HxC 120 | DocProject/Help/*.hhc 121 | DocProject/Help/*.hhk 122 | DocProject/Help/*.hhp 123 | DocProject/Help/Html2 124 | DocProject/Help/html 125 | 126 | # Click-Once directory 127 | publish/ 128 | 129 | # Publish Web Output 130 | *.[Pp]ublish.xml 131 | *.azurePubxml 132 | # TODO: Comment the next line if you want to checkin your web deploy settings 133 | # but database connection strings (with potential passwords) will be unencrypted 134 | *.pubxml 135 | *.publishproj 136 | 137 | # NuGet Packages 138 | *.nupkg 139 | # The packages folder can be ignored because of Package Restore 140 | **/packages/* 141 | # except build/, which is used as an MSBuild target. 142 | !**/packages/build/ 143 | # Uncomment if necessary however generally it will be regenerated when needed 144 | #!**/packages/repositories.config 145 | 146 | # Windows Azure Build Output 147 | csx/ 148 | *.build.csdef 149 | 150 | # Windows Store app package directory 151 | AppPackages/ 152 | 153 | # Others 154 | *.[Cc]ache 155 | ClientBin/ 156 | [Ss]tyle[Cc]op.* 157 | ~$* 158 | *~ 159 | *.dbmdl 160 | *.dbproj.schemaview 161 | *.pfx 162 | *.publishsettings 163 | node_modules/ 164 | bower_components/ 165 | 166 | # RIA/Silverlight projects 167 | Generated_Code/ 168 | 169 | # Backup & report files from converting an old project file 170 | # to a newer Visual Studio version. Backup files are not needed, 171 | # because we have git ;-) 172 | _UpgradeReport_Files/ 173 | Backup*/ 174 | UpgradeLog*.XML 175 | UpgradeLog*.htm 176 | 177 | # SQL Server files 178 | *.mdf 179 | *.ldf 180 | 181 | # Business Intelligence projects 182 | *.rdl.data 183 | *.bim.layout 184 | *.bim_*.settings 185 | 186 | # Microsoft Fakes 187 | FakesAssemblies/ 188 | 189 | # Node.js Tools for Visual Studio 190 | .ntvs_analysis.dat 191 | 192 | # Visual Studio 6 build log 193 | *.plg 194 | 195 | # Visual Studio 6 workspace options file 196 | *.opt 197 | CommanderDemo.Web/jspm_packages/ 198 | CommanderDemo.Web/dist/ 199 | -------------------------------------------------------------------------------- /CommanderDemo.Console/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /CommanderDemo.Console/CommanderDemo.Console.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {FA37B34D-EE79-431A-8DF3-0564E7C9DD9A} 8 | Exe 9 | Properties 10 | CommanderDemo.Console 11 | CommanderDemo.Console 12 | v4.5.2 13 | 512 14 | 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | ..\packages\Command-R.0.2.1-beta\lib\net45\CommandR.dll 38 | True 39 | 40 | 41 | ..\packages\MediatR.2.0.0-beta-003\lib\portable-net45+win+wpa81+wp80+MonoAndroid10+Xamarin.iOS10+MonoTouch10\MediatR.dll 42 | True 43 | 44 | 45 | ..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll 46 | True 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | {2b6ad508-fbff-46a0-b1fa-2d8967a370fd} 71 | CommanderDemo.Domain 72 | 73 | 74 | 75 | 82 | -------------------------------------------------------------------------------- /CommanderDemo.Console/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using CommanderDemo.Domain; 3 | using CommandR; 4 | 5 | //Project Reference: CommanderDemo.Domain 6 | //Solution Nuget: Command-R 7 | namespace CommanderDemo.Console 8 | { 9 | internal class Program 10 | { 11 | private static void Main() 12 | { 13 | var client = new JsonRpcClient("http://localhost:64862/jsonrpc"); 14 | client.Authorization = client.Send(new LoginUser 15 | { 16 | Username = "Admin", 17 | Password = "password", 18 | }); 19 | 20 | var id = client.Send(new SaveContact 21 | { 22 | FirstName = "Test", 23 | LastName = Guid.NewGuid().ToString("N").Substring(10), 24 | Email = "test@example.com", 25 | PhoneNumber = "555-1212", 26 | }); 27 | 28 | System.Console.WriteLine(id); 29 | System.Console.ReadKey(); 30 | } 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /CommanderDemo.Console/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("CommanderDemo.Console")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("CommanderDemo.Console")] 13 | [assembly: AssemblyCopyright("Copyright © 2015")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("340ed3dd-b871-4ded-b176-b2e3f9a5fb78")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /CommanderDemo.Console/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CommanderDemo.Domain/Authentication/GetUserToken.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using CommandR.Authentication; 4 | using MediatR; 5 | 6 | namespace CommanderDemo.Domain 7 | { 8 | /// 9 | /// HACK: Internal command to create a token from the already-validated Asp.net Identity since 10 | /// we are mixing MVC and API tokens 11 | /// 12 | [AllowAnonymous] 13 | internal class GetUserToken : IRequest 14 | { 15 | public string Username { get; set; } 16 | 17 | internal class Handler : IRequestHandler 18 | { 19 | private readonly ContactDb _db; 20 | private readonly ITokenService _tokenService; 21 | 22 | public Handler(ContactDb db, ITokenService tokenService) 23 | { 24 | _db = db; 25 | _tokenService = tokenService; 26 | } 27 | 28 | public string Handle(GetUserToken cmd) 29 | { 30 | var user = _db.Users 31 | .SingleOrDefault(x => x.Username == cmd.Username); 32 | 33 | if (user == null) 34 | return null; 35 | 36 | var tokenId = _tokenService.CreateToken(new Dictionary 37 | { 38 | {"Username", user.Username}, 39 | {"Roles", new string[0]}, 40 | }); 41 | 42 | return tokenId; 43 | } 44 | }; 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /CommanderDemo.Domain/Authentication/LoginUser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using CommandR; 5 | using CommandR.Authentication; 6 | using MediatR; 7 | 8 | namespace CommanderDemo.Domain 9 | { 10 | /// 11 | /// Creates a TokenId which can be used to retrieve the authenticated user's info. 12 | /// Tokens are normally used by javascript frameworks to authorize AJAX calls back 13 | /// to the server so the login only happens once. 14 | /// 15 | [AllowAnonymous] 16 | public class LoginUser : ICommand, IRequest 17 | { 18 | public string Username { get; set; } 19 | public string Password { get; set; } 20 | 21 | internal class Handler : IRequestHandler 22 | { 23 | private readonly ContactDb _db; 24 | private readonly ITokenService _tokenService; 25 | 26 | public Handler(ContactDb db, ITokenService tokenService) 27 | { 28 | _db = db; 29 | _tokenService = tokenService; 30 | } 31 | 32 | public string Handle(LoginUser cmd) 33 | { 34 | var user = _db.Users 35 | .SingleOrDefault(x => x.Username == cmd.Username); 36 | 37 | if (user == null || user.Password != cmd.Password || !user.IsActive) 38 | throw new ApplicationException("Invalid login"); 39 | 40 | var tokenId = _tokenService.CreateToken(new Dictionary 41 | { 42 | {"Username", user.Username}, 43 | {"Roles", new string[0]}, 44 | }); 45 | 46 | return tokenId; 47 | } 48 | }; 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /CommanderDemo.Domain/Authentication/LogoutUser.cs: -------------------------------------------------------------------------------- 1 | using CommandR; 2 | using CommandR.Authentication; 3 | using MediatR; 4 | 5 | namespace CommanderDemo.Domain 6 | { 7 | /// 8 | /// CommandR provides [AllowAnonymous] to indicate authentication isn't 9 | /// required for this command. The ApiAuthorizationFilter will throw 10 | /// an exception if a command doesn't have Authorize or AllowAnonymous attributes. 11 | /// 12 | [AllowAnonymous] 13 | public class LogoutUser : ICommand, IRequest 14 | { 15 | public string TokenId { get; set; } 16 | 17 | internal class Handler : RequestHandler 18 | { 19 | private readonly ITokenService _tokenService; 20 | 21 | public Handler(ITokenService tokenService) 22 | { 23 | _tokenService = tokenService; 24 | } 25 | 26 | protected override void HandleCore(LogoutUser cmd) 27 | { 28 | _tokenService.DeleteToken(cmd.TokenId); 29 | } 30 | }; 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /CommanderDemo.Domain/CommanderDemo.Domain.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {2B6AD508-FBFF-46A0-B1FA-2D8967A370FD} 8 | Library 9 | Properties 10 | CommanderDemo.Domain 11 | CommanderDemo.Domain 12 | v4.5.2 13 | 512 14 | 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | pdbonly 27 | true 28 | bin\Release\ 29 | TRACE 30 | prompt 31 | 4 32 | 33 | 34 | 35 | ..\packages\Command-R.0.2.1-beta\lib\net45\CommandR.dll 36 | True 37 | 38 | 39 | ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll 40 | True 41 | 42 | 43 | ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll 44 | True 45 | 46 | 47 | ..\packages\MediatR.2.0.0-beta-003\lib\portable-net45+win+wpa81+wp80+MonoAndroid10+Xamarin.iOS10+MonoTouch10\MediatR.dll 48 | True 49 | 50 | 51 | ..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll 52 | True 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 101 | -------------------------------------------------------------------------------- /CommanderDemo.Domain/CommanderDemo.Domain.csproj.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True 3 | True 4 | True 5 | True 6 | True -------------------------------------------------------------------------------- /CommanderDemo.Domain/Commands/Alert.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace CommanderDemo.Domain 4 | { 5 | /// 6 | /// The Alert notification allows us to send messages to the client 7 | /// via SignalR. Just a silly example of potential ways to use 8 | /// MediatR INotification. 9 | /// 10 | public class Alert : INotification 11 | { 12 | public string Message { get; set; } 13 | 14 | internal class Handler : INotificationHandler 15 | { 16 | public void Handle(Alert notification) 17 | { 18 | //HACK: nothing to do here, the SignalrHandler does the work. 19 | } 20 | }; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /CommanderDemo.Domain/Commands/AsyncPing.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using CommandR.Authentication; 3 | using MediatR; 4 | 5 | namespace CommanderDemo.Domain 6 | { 7 | /// 8 | /// Simple example that can be used to test that CommandR /jsonrpc endpoint works 9 | /// from something like postman and can execute IAsyncRequests. 10 | /// ex: POST http://localhost:64862/jsonrpc {"method":"AsyncPing","params":{"Name":"Postman"}} 11 | /// 12 | [AllowAnonymous] 13 | public class AsyncPing : IAsyncRequest 14 | { 15 | public string Name { get; set; } 16 | 17 | public class Pong 18 | { 19 | public string Message { get; set; } 20 | }; 21 | 22 | internal class Handler : IAsyncRequestHandler 23 | { 24 | public Task Handle(AsyncPing request) 25 | { 26 | var response = new Pong 27 | { 28 | Message = "Hello (async): " + request.Name, 29 | }; 30 | return Task.FromResult(response); 31 | } 32 | } 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /CommanderDemo.Domain/Commands/Ping.cs: -------------------------------------------------------------------------------- 1 | using CommandR.Authentication; 2 | using MediatR; 3 | 4 | namespace CommanderDemo.Domain 5 | { 6 | /// 7 | /// Simple example that can be used to test that CommandR /jsonrpc endpoint works 8 | /// from something like postman and can execute IRequests. 9 | /// ex: POST http://localhost:64862/jsonrpc {"method":"Ping","params":{"Name":"Postman"}} 10 | /// 11 | [AllowAnonymous] 12 | public class Ping : IRequest 13 | { 14 | public string Name { get; set; } 15 | 16 | public class Pong 17 | { 18 | public string Message { get; set; } 19 | }; 20 | 21 | internal class Handler : IRequestHandler 22 | { 23 | public Pong Handle(Ping request) 24 | { 25 | return new Pong 26 | { 27 | Message = "Hello (not async): " + request.Name, 28 | }; 29 | } 30 | } 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /CommanderDemo.Domain/Commands/SendEmail.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Mail; 2 | using System.Threading.Tasks; 3 | using CommandR; 4 | using CommandR.Authentication; 5 | using MediatR; 6 | 7 | namespace CommanderDemo.Domain 8 | { 9 | /// 10 | /// This command can be added to and IQueueService and be executed in the background 11 | /// by FluentScheduler. 12 | /// 13 | [Authorize] 14 | public class SendEmail : ITask, IAsyncRequest 15 | { 16 | public string To { get; set; } 17 | public string Subject { get; set; } 18 | public string Body { get; set; } 19 | 20 | internal class Handler : IAsyncRequestHandler 21 | { 22 | public async Task Handle(SendEmail sendEmail) 23 | { 24 | var email = new MailMessage(); 25 | email.To.Add(sendEmail.To); 26 | email.Subject = sendEmail.Subject; 27 | email.Body = sendEmail.Body; 28 | 29 | var smtp = new SmtpClient(); 30 | await smtp.SendMailAsync(email); 31 | 32 | return Unit.Value; 33 | } 34 | }; 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /CommanderDemo.Domain/Contacts/DeleteContact.cs: -------------------------------------------------------------------------------- 1 | using CommandR; 2 | using CommandR.Authentication; 3 | using MediatR; 4 | 5 | namespace CommanderDemo.Domain 6 | { 7 | /// 8 | /// Deletes a Contact. 9 | /// CommandR provides ICommand (cqrs-like) marker interface (only used by api.tt) 10 | /// 11 | [Authorize] 12 | public class DeleteContact : ICommand, IRequest 13 | { 14 | public int Id { get; set; } 15 | 16 | internal class Handler : IRequestHandler 17 | { 18 | private readonly ContactDb _db; 19 | 20 | public Handler(ContactDb db) 21 | { 22 | _db = db; 23 | } 24 | 25 | public bool Handle(DeleteContact cmd) 26 | { 27 | var contact = _db.Contacts.Find(cmd.Id); 28 | _db.Contacts.Remove(contact); 29 | _db.SaveChanges(); 30 | return true; 31 | } 32 | }; 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /CommanderDemo.Domain/Contacts/GetContact.cs: -------------------------------------------------------------------------------- 1 | using CommandR; 2 | using CommandR.Authentication; 3 | using MediatR; 4 | 5 | namespace CommanderDemo.Domain 6 | { 7 | /// 8 | /// Retrieves a Contact by Id or returns a new unsaved object for binding. 9 | /// CommandR provides ICommand (cqrs-like) marker interface (only used by api.tt) 10 | /// 11 | [Authorize] 12 | public class GetContact : IQuery, IRequest 13 | { 14 | public int Id { get; set; } 15 | 16 | internal class Handler : IRequestHandler 17 | { 18 | private readonly ContactDb _db; 19 | 20 | public Handler(ContactDb db) 21 | { 22 | _db = db; 23 | } 24 | 25 | public Contact Handle(GetContact cmd) 26 | { 27 | return _db.Contacts.Find(cmd.Id) ?? new Contact(); 28 | } 29 | }; 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /CommanderDemo.Domain/Contacts/QueryContacts.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using CommandR; 3 | using CommandR.Authentication; 4 | using CommandR.Extensions; 5 | using MediatR; 6 | 7 | namespace CommanderDemo.Domain 8 | { 9 | /// 10 | /// Returns a list of Contacts that match the Search criteria. 11 | /// CommandR provides IQuery (cqrs-like) marker interface. 12 | /// CommandR provides IPageable and PagedList classes. 13 | /// 14 | [Authorize] 15 | public class QueryContacts : IQuery, IPageable, IRequest> 16 | { 17 | public string Search { get; set; } 18 | public int? PageNumber { get; set; } 19 | public int? PageSize { get; set; } 20 | 21 | public class ContactInfo 22 | { 23 | public int Id { get; set; } 24 | public string FirstName { get; set; } 25 | public string LastName { get; set; } 26 | public string Email { get; set; } 27 | }; 28 | 29 | internal class Handler : IRequestHandler> 30 | { 31 | private readonly ContactDb _db; 32 | 33 | public Handler(ContactDb db) 34 | { 35 | _db = db; 36 | } 37 | 38 | public PagedList Handle(QueryContacts cmd) 39 | { 40 | var query = _db.Contacts.AsQueryable(); 41 | 42 | if (!string.IsNullOrEmpty(cmd.Search)) 43 | { 44 | int id; 45 | int.TryParse(cmd.Search, out id); 46 | query = query.Where(x => x.Id == id 47 | || x.FirstName.Contains(cmd.Search) 48 | || x.LastName.Contains(cmd.Search) 49 | || x.Email.Contains(cmd.Search) 50 | || x.PhoneNumber == cmd.Search); 51 | } 52 | 53 | return query 54 | .Select(x => new ContactInfo 55 | { 56 | Id = x.Id, 57 | FirstName = x.FirstName, 58 | LastName = x.LastName, 59 | Email = x.Email, 60 | }) 61 | .OrderBy(x => x.Id) 62 | .ToPagedList(cmd, 25, 100); 63 | } 64 | }; 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /CommanderDemo.Domain/Contacts/SaveContact.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using CommandR; 4 | using CommandR.Authentication; 5 | using CommandR.Extensions; 6 | using CommandR.Services; 7 | using MediatR; 8 | 9 | namespace CommanderDemo.Domain 10 | { 11 | /// 12 | /// Retrieve a User by id, or create a new unsaved one for binding. 13 | /// CommandR provides CopyTo method. Similar to AutoMapper, but supports 14 | /// CommandR's extension to JsonRpc, IPatchable which automatically maps 15 | /// which properties were actually included by the caller (eg only Username). 16 | /// 17 | [Authorize] 18 | public class SaveContact : ICommand, IPatchable, IRequest 19 | { 20 | public int Id { get; set; } 21 | public string FirstName { get; set; } 22 | public string LastName { get; set; } 23 | public string Email { get; set; } 24 | public string PhoneNumber { get; set; } 25 | public string[] PatchFields { get; set; } 26 | 27 | internal class Handler : IRequestHandler 28 | { 29 | private readonly ContactDb _db; 30 | private readonly AppContext _appContext; 31 | private readonly IQueueService _queue; 32 | private readonly IMediator _mediator; 33 | 34 | public Handler(ContactDb db, AppContext appContext, IQueueService queue, IMediator mediator) 35 | { 36 | _db = db; 37 | _appContext = appContext; 38 | _queue = queue; 39 | _mediator = mediator; 40 | } 41 | 42 | public int Handle(SaveContact cmd) 43 | { 44 | if (string.IsNullOrWhiteSpace(cmd.Email)) 45 | throw new ApplicationException("Invalid email: " + cmd.Email); 46 | 47 | if (string.IsNullOrWhiteSpace(cmd.PhoneNumber)) 48 | throw new ApplicationException("Invalid phone: " + cmd.PhoneNumber); 49 | 50 | var contact = _db.Contacts.Find(cmd.Id) 51 | ?? new Contact(); 52 | 53 | cmd.CopyTo(contact, cmd.PatchFields); 54 | if (contact.Id == 0) _db.Contacts.Add(contact); 55 | _db.SaveChanges(); 56 | 57 | var contactInfo = contact.GetType().GetProperties().Select(x => x.Name + ": " + x.GetValue(contact)); 58 | _queue.Enqueue(new SendEmail 59 | { 60 | To = "paul@paulwheeler.com", 61 | Subject = "Contact Saved", 62 | Body = string.Join(Environment.NewLine, contactInfo), 63 | }, _appContext); 64 | 65 | _mediator.Publish(new Alert { Message = "Contact Saved: " + contact.Id }); 66 | 67 | return contact.Id; 68 | } 69 | }; 70 | }; 71 | } 72 | -------------------------------------------------------------------------------- /CommanderDemo.Domain/Models/Contact.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | 4 | namespace CommanderDemo.Domain 5 | { 6 | /// 7 | /// EntityFramework code first POCO model. 8 | /// The ContactDb.Contacts collection tells EntityFramework which 9 | /// DbContext schema this model belongs to. 10 | /// 11 | [Table("Contact")] 12 | internal class Contact 13 | { 14 | [Key] 15 | public int Id { get; set; } 16 | 17 | [StringLength(25)] 18 | public string FirstName { get; set; } 19 | 20 | [StringLength(25)] 21 | public string LastName { get; set; } 22 | 23 | [StringLength(100)] 24 | public string Email { get; set; } 25 | 26 | [StringLength(25)] 27 | public string PhoneNumber { get; set; } 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /CommanderDemo.Domain/Models/ContactDb.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Entity; 2 | using System.Diagnostics; 3 | 4 | namespace CommanderDemo.Domain 5 | { 6 | /// 7 | /// We keep our models and database as internal because *everything* is only ever 8 | /// exposed via the command view models. We're also going to tell EntityFramework to 9 | /// use our MigrationsConfig which configures AutomaticMigrations to keep our db schema 10 | /// in sync with our model changes automatically on app startup. 11 | /// 12 | internal class ContactDb : DbContext 13 | { 14 | public ContactDb() 15 | { 16 | Database.SetInitializer(new MigrateDatabaseToLatestVersion()); 17 | Database.Log = sql => Debug.WriteLine(sql); 18 | } 19 | 20 | public virtual IDbSet Contacts { get; set; } 21 | public virtual IDbSet Users { get; set; } 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /CommanderDemo.Domain/Models/MigrationsConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Entity.Migrations; 2 | 3 | namespace CommanderDemo.Domain 4 | { 5 | /// 6 | /// AutomaticMigrations allows EntityFramework to keep our database schema 7 | /// in sync with our code models automatically. It will create a local database 8 | /// called CommanderDemo.Domain.ContactDb automatically since we aren't providing 9 | /// a connectionstring to ContactDb. 10 | /// 11 | internal class MigrationsConfig : DbMigrationsConfiguration 12 | { 13 | public MigrationsConfig() 14 | { 15 | AutomaticMigrationsEnabled = true; 16 | AutomaticMigrationDataLossAllowed = true; 17 | } 18 | 19 | protected override void Seed(ContactDb db) 20 | { 21 | AddUsers(db); 22 | base.Seed(db); 23 | } 24 | 25 | private static void AddUsers(ContactDb db) 26 | { 27 | //Let's add a default user to the database so we can log in 28 | db.Users.AddOrUpdate(x => x.Username, 29 | new User 30 | { 31 | Username = "Admin", 32 | Password = "password", 33 | IsActive = true, 34 | } 35 | ); 36 | } 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /CommanderDemo.Domain/Models/User.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | 4 | namespace CommanderDemo.Domain 5 | { 6 | /// 7 | /// EntityFramework code first POCO model. 8 | /// The ContactDb.Users collection tells EntityFramework which 9 | /// DbContext schema this model belongs to. 10 | /// 11 | [Table("User")] 12 | internal class User 13 | { 14 | [Key] 15 | public int Id { get; set; } 16 | 17 | [StringLength(100)] 18 | public string Username { get; set; } 19 | 20 | [StringLength(100)] 21 | public string Password { get; set; } 22 | 23 | public bool IsActive { get; set; } 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /CommanderDemo.Domain/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("CommanderDemo.Domain")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("CommanderDemo.Domain")] 13 | [assembly: AssemblyCopyright("Copyright © 2015")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("3c9004b4-09e0-4a8d-82f6-c99fae4f31be")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | 38 | [assembly: InternalsVisibleTo("CommanderDemo.Web")] 39 | [assembly: InternalsVisibleTo("CommanderDemo.Test")] 40 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] 41 | -------------------------------------------------------------------------------- /CommanderDemo.Domain/Users/DeleteUser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using CommandR; 3 | using CommandR.Authentication; 4 | using MediatR; 5 | 6 | namespace CommanderDemo.Domain 7 | { 8 | /// 9 | /// Deletes a Contact. 10 | /// CommandR provides ICommand (cqrs-like) marker interface (only used by api.tt) 11 | /// 12 | [Authorize(Users = "Admin")] 13 | public class DeleteUser : ICommand, IRequest 14 | { 15 | public int Id { get; set; } 16 | 17 | internal class Handler : IRequestHandler 18 | { 19 | private readonly ContactDb _db; 20 | 21 | public Handler(ContactDb db) 22 | { 23 | _db = db; 24 | } 25 | 26 | public bool Handle(DeleteUser cmd) 27 | { 28 | try 29 | { 30 | var user = _db.Users.Find(cmd.Id); 31 | _db.Users.Remove(user); 32 | _db.SaveChanges(); 33 | return true; 34 | } 35 | catch (Exception ex) 36 | { 37 | throw new ApplicationException("DeleteUser ERROR for Id: " + cmd.Id, ex); 38 | } 39 | } 40 | }; 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /CommanderDemo.Domain/Users/GetUser.cs: -------------------------------------------------------------------------------- 1 | using CommandR; 2 | using CommandR.Authentication; 3 | using CommandR.Extensions; 4 | using MediatR; 5 | 6 | namespace CommanderDemo.Domain 7 | { 8 | /// 9 | /// Retrieve a User by id, or create a new unsaved one for binding. 10 | /// CommandR provides CopyTo method (similar to AutoMapper). 11 | /// 12 | [Authorize] 13 | public class GetUser : IQuery, IPatchable, IRequest 14 | { 15 | public int Id { get; set; } 16 | public string[] PatchFields { get; set; } 17 | 18 | public class UserInfo 19 | { 20 | public int Id { get; set; } 21 | public string Username { get; set; } 22 | public string Password { get; set; } 23 | public bool IsActive { get; set; } 24 | }; 25 | 26 | internal class Handler : IRequestHandler 27 | { 28 | private readonly ContactDb _db; 29 | 30 | public Handler(ContactDb db) 31 | { 32 | _db = db; 33 | } 34 | 35 | public UserInfo Handle(GetUser cmd) 36 | { 37 | var user = _db.Users.Find(cmd.Id) 38 | ?? new User(); 39 | 40 | return user.CopyTo(new UserInfo()); 41 | } 42 | }; 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /CommanderDemo.Domain/Users/QueryUsers.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using CommandR; 3 | using CommandR.Authentication; 4 | using CommandR.Extensions; 5 | using MediatR; 6 | 7 | namespace CommanderDemo.Domain 8 | { 9 | /// 10 | /// Returns a list of Users. 11 | /// This shows an example of a command returning the original Query along with 12 | /// the Result. This can make our MVC Controllers simpler since the razor view 13 | /// can bind to the Query.Inactive property. 14 | /// CommandR provides the [Authorize] to secure the command 15 | /// CommandR provides IPageable and PagedList classes 16 | /// CommandR provides the IQuery (cqrs-ish) marker interface. Is only used by the 17 | /// api.tt example to auto-generate calls to commands for javascript. 18 | /// 19 | [Authorize] 20 | public class QueryUsers : IQuery, IPageable, IRequest 21 | { 22 | public bool Inactive { get; set; } 23 | public int? PageNumber { get; set; } 24 | public int? PageSize { get; set; } 25 | 26 | public class Response 27 | { 28 | public QueryUsers Query { get; set; } 29 | public PagedList Result { get; set; } 30 | }; 31 | 32 | public class UserInfo 33 | { 34 | public int Id { get; set; } 35 | public string Username { get; set; } 36 | public bool IsActive { get; set; } 37 | }; 38 | 39 | internal class Handler : IRequestHandler 40 | { 41 | private readonly ContactDb _db; 42 | 43 | public Handler(ContactDb db) 44 | { 45 | _db = db; 46 | } 47 | 48 | public Response Handle(QueryUsers cmd) 49 | { 50 | var query = _db.Users.AsQueryable(); 51 | 52 | if (!cmd.Inactive) 53 | { 54 | query = query.Where(x => x.IsActive); 55 | } 56 | 57 | var result = query 58 | .Select(x => new UserInfo 59 | { 60 | Id = x.Id, 61 | Username = x.Username, 62 | IsActive = x.IsActive, 63 | }) 64 | .OrderBy(x => x.Id) 65 | .ToPagedList(cmd, 25, 100); 66 | 67 | return new Response 68 | { 69 | Query = cmd, 70 | Result = result, 71 | }; 72 | } 73 | }; 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /CommanderDemo.Domain/Users/SaveUser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using CommandR; 3 | using CommandR.Authentication; 4 | using CommandR.Extensions; 5 | using MediatR; 6 | 7 | namespace CommanderDemo.Domain 8 | { 9 | /// 10 | /// Retrieve a User by id, or create a new unsaved one for binding. 11 | /// CommandR provides ICommand (cqrs-like) marker interface (only used by api.tt) 12 | /// CommandR provides CopyTo method. Similar to AutoMapper, but supports 13 | /// CommandR's extension to JsonRpc, IPatchable which automatically maps 14 | /// which properties were actually included by the caller (eg only Username). 15 | /// 16 | [Authorize] 17 | public class SaveUser : ICommand, IPatchable, IRequest 18 | { 19 | public int Id { get; set; } 20 | public string Username { get; set; } 21 | public string Password { get; set; } 22 | public bool IsActive { get; set; } 23 | public string[] PatchFields { get; set; } 24 | 25 | internal class Handler : IRequestHandler 26 | { 27 | private readonly ContactDb _db; 28 | 29 | public Handler(ContactDb db) 30 | { 31 | _db = db; 32 | } 33 | 34 | public int Handle(SaveUser cmd) 35 | { 36 | if (string.IsNullOrEmpty(cmd.Username)) 37 | throw new ApplicationException("Username is required"); 38 | 39 | if (string.IsNullOrEmpty(cmd.Password)) 40 | throw new ApplicationException("Password is required"); 41 | 42 | var user = _db.Users.Find(cmd.Id) 43 | ?? new User(); 44 | 45 | cmd.CopyTo(user, cmd.PatchFields); 46 | if (user.Id == 0) _db.Users.Add(user); 47 | _db.SaveChanges(); 48 | 49 | return user.Id; 50 | } 51 | }; 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /CommanderDemo.Domain/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /CommanderDemo.Test/CommanderDemo.Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {D9F9ECD9-D53C-4E1B-80C8-B2949B1CF6C6} 8 | Library 9 | Properties 10 | CommanderDemo.Test 11 | CommanderDemo.Test 12 | v4.5.2 13 | 512 14 | 15 | 16 | true 17 | full 18 | false 19 | bin\Debug\ 20 | DEBUG;TRACE 21 | prompt 22 | 4 23 | 24 | 25 | pdbonly 26 | true 27 | bin\Release\ 28 | TRACE 29 | prompt 30 | 4 31 | 32 | 33 | 34 | ..\packages\Command-R.0.2.1-beta\lib\net45\CommandR.dll 35 | True 36 | 37 | 38 | ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll 39 | True 40 | 41 | 42 | ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll 43 | True 44 | 45 | 46 | ..\packages\FakeItEasy.1.25.2\lib\net40\FakeItEasy.dll 47 | True 48 | 49 | 50 | ..\packages\MediatR.2.0.0-beta-003\lib\portable-net45+win+wpa81+wp80+MonoAndroid10+Xamarin.iOS10+MonoTouch10\MediatR.dll 51 | True 52 | 53 | 54 | ..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll 55 | True 56 | 57 | 58 | ..\packages\Shouldly.2.5.0\lib\net40\Shouldly.dll 59 | True 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | ..\packages\xunit.1.9.2\lib\net20\xunit.dll 75 | True 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | {2b6ad508-fbff-46a0-b1fa-2d8967a370fd} 91 | CommanderDemo.Domain 92 | 93 | 94 | 95 | 102 | -------------------------------------------------------------------------------- /CommanderDemo.Test/CommanderDemo.Test.csproj.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | CSharp50 -------------------------------------------------------------------------------- /CommanderDemo.Test/FakeDbSet.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Collections.ObjectModel; 5 | using System.ComponentModel.DataAnnotations; 6 | using System.Data.Entity; 7 | using System.Linq; 8 | using System.Linq.Expressions; 9 | using System.Reflection; 10 | 11 | namespace CommanderDemo.Test 12 | { 13 | /// 14 | /// FakeDbSet lets us test EntityFramework by mocking the virtual IDbSet collections defined 15 | /// for each table (thanks Brent!, see url below :) 16 | /// 17 | public static class FakeDbSetExtensions 18 | { 19 | public static IDbSet ToDbSet(this IEnumerable items) where T : class 20 | { 21 | return new FakeDbSet(items); 22 | } 23 | }; 24 | 25 | //REF: http://blog.brentmckendrick.com/generic-repository-fake-idbset-implementation-update-find-method-identity-key/ 26 | public class FakeDbSet : IDbSet where T : class 27 | { 28 | private readonly HashSet _data; 29 | private readonly IQueryable _query; 30 | private int _identity; 31 | private List _keyProperties; 32 | 33 | private void GetKeyProperties() 34 | { 35 | _keyProperties = new List(); 36 | var properties = typeof(T).GetProperties(); 37 | foreach (var property in properties) 38 | { 39 | foreach (Attribute attribute in property.GetCustomAttributes(true)) 40 | { 41 | if (attribute is KeyAttribute) 42 | { 43 | _keyProperties.Add(property); 44 | } 45 | } 46 | } 47 | } 48 | 49 | private void GenerateId(T entity) 50 | { 51 | // If non-composite integer key 52 | if (_keyProperties.Count == 1 && _keyProperties[0].PropertyType == typeof(int)) 53 | _keyProperties[0].SetValue(entity, _identity++, null); 54 | } 55 | 56 | public FakeDbSet(IEnumerable startData = null) 57 | { 58 | GetKeyProperties(); 59 | _data = (startData != null ? new HashSet(startData) : new HashSet()); 60 | _query = _data.AsQueryable(); 61 | _identity = GetMaxId() + 1; 62 | } 63 | 64 | public virtual T Find(params object[] keyValues) 65 | { 66 | if (keyValues.Length != _keyProperties.Count) 67 | throw new ArgumentException("Incorrect number of keys passed to find method"); 68 | 69 | var keyQuery = this.AsQueryable(); 70 | for (var i = 0; i < keyValues.Length; i++) 71 | { 72 | var x = i; // nested linq 73 | keyQuery = keyQuery.Where(entity => _keyProperties[x].GetValue(entity, null).Equals(keyValues[x])); 74 | } 75 | 76 | return keyQuery.SingleOrDefault(); 77 | } 78 | 79 | private int GetMaxId() 80 | { 81 | if (_keyProperties.Count != 1 || _keyProperties[0].PropertyType != typeof(int) || _data.Count == 0) 82 | return 0; 83 | 84 | var entity = _data.Last(); 85 | return (int)_keyProperties[0].GetValue(entity); 86 | } 87 | 88 | public virtual T Add(T item) 89 | { 90 | GenerateId(item); 91 | _data.Add(item); 92 | return item; 93 | } 94 | 95 | public virtual T Remove(T item) 96 | { 97 | _data.Remove(item); 98 | return item; 99 | } 100 | 101 | public virtual T Attach(T item) 102 | { 103 | _data.Add(item); 104 | return item; 105 | } 106 | 107 | public virtual void Detach(T item) 108 | { 109 | _data.Remove(item); 110 | } 111 | 112 | Type IQueryable.ElementType 113 | { 114 | get { return _query.ElementType; } 115 | } 116 | 117 | Expression IQueryable.Expression 118 | { 119 | get { return _query.Expression; } 120 | } 121 | 122 | IQueryProvider IQueryable.Provider 123 | { 124 | get { return _query.Provider; } 125 | } 126 | 127 | IEnumerator IEnumerable.GetEnumerator() 128 | { 129 | return _data.GetEnumerator(); 130 | } 131 | 132 | IEnumerator IEnumerable.GetEnumerator() 133 | { 134 | return _data.GetEnumerator(); 135 | } 136 | 137 | public virtual T Create() 138 | { 139 | return Activator.CreateInstance(); 140 | } 141 | 142 | public virtual ObservableCollection Local 143 | { 144 | get { return new ObservableCollection(_data); } 145 | } 146 | 147 | public virtual TDerivedEntity Create() where TDerivedEntity : class, T 148 | { 149 | return Activator.CreateInstance(); 150 | } 151 | }; 152 | } 153 | -------------------------------------------------------------------------------- /CommanderDemo.Test/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("CommanderDemo.Test")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("CommanderDemo.Test")] 13 | [assembly: AssemblyCopyright("Copyright © 2015")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("b3b7a42c-8932-4e84-a00d-6e92955a852c")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /CommanderDemo.Test/TestQueryContacts.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using CommanderDemo.Domain; 3 | using FakeItEasy; 4 | using Shouldly; 5 | using Xunit; 6 | 7 | namespace CommanderDemo.Test 8 | { 9 | /// 10 | /// Test the QueryContacts command. 11 | /// We can use a single _handler instance since our command has only 12 | /// one method, we're just changing the inputs and testing the outputs. 13 | /// 14 | public class TestQueryContacts 15 | { 16 | private readonly QueryContacts.Handler _handler; 17 | 18 | public TestQueryContacts() 19 | { 20 | //Our Fake Contact data for the DbContext 21 | var contacts = new[] 22 | { 23 | new Contact {Id = 1, FirstName = "A", LastName = "B", Email = "C", PhoneNumber = "6"}, 24 | new Contact {Id = 2, FirstName = "D", LastName = "E", Email = "F", PhoneNumber = "7"}, 25 | new Contact {Id = 3, FirstName = "G", LastName = "H", Email = "I", PhoneNumber = "8"}, 26 | new Contact {Id = 4, FirstName = "J", LastName = "K", Email = "L", PhoneNumber = "9"}, 27 | new Contact {Id = 5, FirstName = "M", LastName = "N", Email = "O", PhoneNumber = "0"} 28 | }.ToDbSet(); 29 | 30 | var db = A.Fake(); 31 | A.CallTo(() => db.Contacts).Returns(contacts); 32 | 33 | _handler = new QueryContacts.Handler(db); 34 | } 35 | 36 | [Fact] 37 | public void Test_QueryContacts_Returns_All_By_Default() 38 | { 39 | var request = new QueryContacts(); 40 | var response = _handler.Handle(request); 41 | response.Items.Count().ShouldBe(5); 42 | } 43 | 44 | [Fact] 45 | public void Test_QueryContacts_Search_Id() 46 | { 47 | var request = new QueryContacts { Search = "1" }; 48 | var response = _handler.Handle(request); 49 | response.Items.Single().Id.ShouldBe(1); 50 | } 51 | 52 | [Fact] 53 | public void Test_QueryContacts_Search_FirstName() 54 | { 55 | var request = new QueryContacts { Search = "D" }; 56 | var response = _handler.Handle(request); 57 | response.Items.Single().Id.ShouldBe(2); 58 | } 59 | 60 | [Fact] 61 | public void Test_QueryContacts_Search_LastName() 62 | { 63 | var request = new QueryContacts { Search = "H" }; 64 | var response = _handler.Handle(request); 65 | response.Items.Single().Id.ShouldBe(3); 66 | } 67 | 68 | [Fact] 69 | public void Test_QueryContacts_Search_Email() 70 | { 71 | var request = new QueryContacts { Search = "L" }; 72 | var response = _handler.Handle(request); 73 | response.Items.Single().Id.ShouldBe(4); 74 | } 75 | 76 | [Fact] 77 | public void Test_QueryContacts_Search_Phone() 78 | { 79 | var request = new QueryContacts { Search = "0" }; 80 | var response = _handler.Handle(request); 81 | response.Items.Single().Id.ShouldBe(5); 82 | } 83 | 84 | [Fact] 85 | public void Test_QueryContacts_Paging() 86 | { 87 | var request = new QueryContacts { PageNumber = 3, PageSize = 1 }; 88 | var response = _handler.Handle(request); 89 | response.Items.Single().Id.ShouldBe(3); 90 | } 91 | }; 92 | } 93 | -------------------------------------------------------------------------------- /CommanderDemo.Test/TestQueryUsers.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using CommanderDemo.Domain; 3 | using FakeItEasy; 4 | using Shouldly; 5 | using Xunit; 6 | 7 | namespace CommanderDemo.Test 8 | { 9 | /// 10 | /// Test the QueryContacts command. 11 | /// We can use a single _handler instance since our command has only 12 | /// one method, we're just changing the inputs and testing the outputs. 13 | /// 14 | public class TestQueryUsers 15 | { 16 | private readonly QueryUsers.Handler _handler; 17 | 18 | public TestQueryUsers() 19 | { 20 | var users = new[] 21 | { 22 | new User {Id = 1, IsActive = true}, 23 | new User {Id = 2, IsActive = false}, 24 | new User {Id = 3, IsActive = true}, 25 | new User {Id = 4, IsActive = false}, 26 | }.ToDbSet(); 27 | 28 | var db = A.Fake(); 29 | A.CallTo(() => db.Users).Returns(users); 30 | 31 | _handler = new QueryUsers.Handler(db); 32 | } 33 | 34 | [Fact] 35 | public void Test_QueryUsers_Excludes_Inactive_By_Default() 36 | { 37 | var queryUsers = new QueryUsers(); 38 | var response = _handler.Handle(queryUsers); 39 | response.Result.Items.Count().ShouldBe(2); 40 | } 41 | 42 | [Fact] 43 | public void Test_QueryUsers_Includes_Inactive_If_True() 44 | { 45 | var queryUsers = new QueryUsers { Inactive = true }; 46 | var response = _handler.Handle(queryUsers); 47 | response.Result.Items.Count().ShouldBe(4); 48 | } 49 | 50 | [Fact] 51 | public void Test_QueryUsers_Paging() 52 | { 53 | var queryUsers = new QueryUsers { Inactive = true, PageNumber = 3, PageSize = 1 }; 54 | var response = _handler.Handle(queryUsers); 55 | response.Result.Items.Single().Id.ShouldBe(3); 56 | } 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /CommanderDemo.Test/TestSaveContact.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using CommanderDemo.Domain; 3 | using CommandR.Authentication; 4 | using CommandR.Services; 5 | using FakeItEasy; 6 | using MediatR; 7 | using Shouldly; 8 | using Xunit; 9 | 10 | namespace CommanderDemo.Test 11 | { 12 | /// 13 | /// Test the SaveContact command. 14 | /// We have a shared _handler, but some tests create their own 15 | /// instance to mock the dependencies differently. 16 | /// 17 | public class TestSaveContact 18 | { 19 | private readonly ContactDb _db; 20 | private readonly AppContext _appContext; 21 | private readonly IQueueService _queue; 22 | private readonly IMediator _mediator; 23 | private readonly SaveContact.Handler _handler; 24 | 25 | public TestSaveContact() 26 | { 27 | var contacts = new[] 28 | { 29 | new Contact 30 | { 31 | Id = 1, 32 | FirstName = "First", 33 | LastName = "Last", 34 | Email = "test@example.com", 35 | PhoneNumber = "555-1212" 36 | }, 37 | }.ToDbSet(); 38 | 39 | _db = A.Fake(); 40 | A.CallTo(() => _db.Contacts).Returns(contacts); 41 | 42 | _appContext = A.Fake(); 43 | _queue = A.Fake(); 44 | _mediator = A.Fake(); 45 | 46 | _handler = new SaveContact.Handler(_db, _appContext, _queue, _mediator); 47 | } 48 | 49 | [Fact] 50 | public void Test_SaveContact_Requires_Email() 51 | { 52 | var request = new SaveContact 53 | { 54 | FirstName = "First", 55 | LastName = "Last", 56 | Email = null, 57 | PhoneNumber = "555-1212", 58 | }; 59 | Should.Throw(() => _handler.Handle(request)); 60 | } 61 | 62 | [Fact] 63 | public void Test_SaveContact_Requires_Phone() 64 | { 65 | var request = new SaveContact 66 | { 67 | FirstName = "First", 68 | LastName = "Last", 69 | Email = "test@example.com", 70 | PhoneNumber = null, 71 | }; 72 | Should.Throw(() => _handler.Handle(request)); 73 | } 74 | 75 | [Fact] 76 | public void Test_SaveContact_Creates_New_Contact() 77 | { 78 | var request = new SaveContact 79 | { 80 | FirstName = "First", 81 | LastName = "Last", 82 | Email = "test@example.com", 83 | PhoneNumber = "555-1212", 84 | }; 85 | var response = _handler.Handle(request); 86 | response.ShouldBe(2); 87 | } 88 | 89 | [Fact] 90 | public void Test_SaveContact_Enqueues_Email() 91 | { 92 | var request = new SaveContact 93 | { 94 | Id = 1, 95 | FirstName = "First", 96 | LastName = "Last", 97 | Email = "test@example.com", 98 | PhoneNumber = "555-1212", 99 | }; 100 | 101 | var queue = A.Fake(); 102 | var handler = new SaveContact.Handler(_db, _appContext, queue, _mediator); 103 | handler.Handle(request); 104 | 105 | A.CallTo(() => queue.Enqueue(A._, _appContext)).MustHaveHappened(Repeated.Exactly.Once); 106 | } 107 | 108 | [Fact] 109 | public void Test_SaveContact_Publishes_Alert() 110 | { 111 | var request = new SaveContact 112 | { 113 | Id = 1, 114 | FirstName = "First", 115 | LastName = "Last", 116 | Email = "test@example.com", 117 | PhoneNumber = "555-1212", 118 | }; 119 | 120 | var mediator = A.Fake(); 121 | var handler = new SaveContact.Handler(_db, _appContext, _queue, mediator); 122 | handler.Handle(request); 123 | 124 | A.CallTo(() => mediator.Publish(A._)).MustHaveHappened(Repeated.Exactly.Once); 125 | } 126 | }; 127 | } 128 | -------------------------------------------------------------------------------- /CommanderDemo.Test/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /CommanderDemo.Web/CommanderDemo.Web.csproj.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True -------------------------------------------------------------------------------- /CommanderDemo.Web/Global.asax: -------------------------------------------------------------------------------- 1 | <%@ Application Codebehind="Global.asax.cs" Inherits="CommanderDemo.Web.Global" Language="C#" %> 2 | -------------------------------------------------------------------------------- /CommanderDemo.Web/Global.asax.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data.Entity; 3 | using System.Reflection; 4 | using System.Web; 5 | using System.Web.Http; 6 | using System.Web.Mvc; 7 | using System.Web.Routing; 8 | using CfgDotNet; 9 | using CommanderDemo.Domain; 10 | using CommandR; 11 | using CommandR.Authentication; 12 | using CommandR.MongoQueue; 13 | using CommandR.Services; 14 | using CommandR.WebApi; 15 | using FluentScheduler; 16 | using MediatR; 17 | using SimpleInjector; 18 | using SimpleInjector.Extensions; 19 | using SimpleInjector.Extensions.LifetimeScoping; 20 | using SimpleInjector.Integration.Web; 21 | using SimpleInjector.Integration.Web.Mvc; 22 | using SimpleInjector.Integration.WebApi; 23 | 24 | namespace CommanderDemo.Web 25 | { 26 | public class Global : HttpApplication 27 | { 28 | private static Container _container; 29 | private static Assembly[] _assemblies; 30 | 31 | protected void Application_Start(object sender, EventArgs e) 32 | { 33 | _container = new Container(); 34 | 35 | // Since we are using Mvc, WebApi, and FluentScheduler the LifetimeScope is needed 36 | // when an HttpContext.Current is not available. 37 | var lifestyle = Lifestyle.CreateHybrid(() => HttpContext.Current == null, 38 | new LifetimeScopeLifestyle(true), 39 | new WebRequestLifestyle(true)); 40 | 41 | // These are all the assemblies that need to be scanned by the container. 42 | _assemblies = new[] 43 | { 44 | typeof(Global).Assembly, 45 | typeof(LoginUser).Assembly, 46 | typeof(Commander).Assembly, 47 | typeof(JsonRpcController).Assembly, 48 | typeof(MongoQueueService).Assembly, 49 | }; 50 | 51 | ConfigureServices(lifestyle); 52 | ConfigureSettings(); 53 | ConfigureMediator(); 54 | ConfigureCommander(GlobalConfiguration.Configuration, lifestyle); 55 | ConfigureRoutes(GlobalConfiguration.Configuration, RouteTable.Routes); 56 | ConfigureMvc(); 57 | ConfigureWebApi(GlobalConfiguration.Configuration); 58 | ConfigureFluentScheduler(); 59 | 60 | _container.Verify(); 61 | } 62 | 63 | //HACK: to simplify MVC + API reconsituting the TokenId from Forms Auth cookie-persisted Username 64 | protected void Application_AcquireRequestState(object sender, EventArgs e) 65 | { 66 | if (Context.Session == null) 67 | return; 68 | 69 | var tokenId = (string)Session["TokenId"]; 70 | if (tokenId == null) 71 | { 72 | tokenId = _container.GetInstance().Send(new GetUserToken 73 | { 74 | Username = User.Identity.Name 75 | }); 76 | Session["TokenId"] = tokenId; 77 | } 78 | 79 | var tokenService = _container.GetInstance(); 80 | var dict = tokenService.GetTokenData(tokenId); 81 | _container.GetInstance().AppContext = new AppContext(dict) 82 | { 83 | RequestIsLocal = Request.IsLocal, 84 | }; 85 | } 86 | 87 | private static void ConfigureServices(Lifestyle lifestyle) 88 | { 89 | _container.Register(lifestyle); 90 | _container.Register(() => _container.GetInstance()); //Used by TransactionHandler 91 | _container.Register(); 92 | _container.RegisterSingle(); 93 | _container.Register(lifestyle); 94 | } 95 | 96 | private static void ConfigureSettings() 97 | { 98 | // Scan all our assemblies for ISettings, load their values from the supplied Providers, 99 | // and Validate so we can fail fast if anything is not set up correctly, then register 100 | // the settings in the container. Changes to the Settings classes will *not* be persisted, but 101 | // will be reloaded when the app is restarted. 102 | new SettingsManager() 103 | .AddProvider(new ConnectionStringsSettingsProvider()) 104 | .AddProvider(new AppSettingsSettingsProvider()) 105 | .AddSettings(_assemblies) 106 | .Validate() 107 | .ForEach(x => _container.RegisterSingle(x.GetType(), x)); 108 | } 109 | 110 | private static void ConfigureMediator() 111 | { 112 | _container.RegisterSingle(() => new Mediator(_container.GetInstance, _container.GetAllInstances)); 113 | 114 | _container.RegisterManyForOpenGeneric(typeof(IRequestHandler<,>), _assemblies); 115 | _container.RegisterManyForOpenGeneric(typeof(IAsyncRequestHandler<,>), _assemblies); 116 | _container.RegisterManyForOpenGeneric(typeof(INotificationHandler<>), _container.RegisterAll, _assemblies); 117 | _container.RegisterManyForOpenGeneric(typeof(IAsyncNotificationHandler<>), _container.RegisterAll, _assemblies); 118 | 119 | _container.RegisterDecorator(typeof(IRequestHandler<,>), typeof(TransactionHandler<,>)); 120 | _container.RegisterDecorator(typeof(IRequestHandler<,>), typeof(LoggingHandler<,>)); 121 | _container.RegisterDecorator(typeof(IRequestHandler<,>), typeof(AuditHandler<,>)); 122 | _container.RegisterDecorator(typeof(INotificationHandler<>), typeof(SignalrHandler<>)); 123 | } 124 | 125 | private static void ConfigureCommander(HttpConfiguration config, Lifestyle lifestyle) 126 | { 127 | _container.Register(() => _container.GetInstance().AppContext ?? new AppContext(), lifestyle); 128 | _container.Register(lifestyle); 129 | 130 | _container.RegisterDecorator(typeof(IRequestHandler<,>), typeof(AuthorizationHandler<,>)); 131 | _container.RegisterDecorator(typeof(IAsyncRequestHandler<,>), typeof(AsyncAuthorizationHandler<,>)); 132 | 133 | config.Filters.Add(new ApiAuthorizationFilter()); 134 | Commander.Initialize(_assemblies); //Register all the commands 135 | } 136 | 137 | private static void ConfigureRoutes(HttpConfiguration config, RouteCollection routes) 138 | { 139 | config.MapHttpAttributeRoutes(); 140 | 141 | routes.MapRoute("MvcControllers", 142 | "{controller}/{action}/{id}", 143 | new { controller = "Home", action = "Index", id = UrlParameter.Optional }); 144 | } 145 | 146 | private static void ConfigureMvc() 147 | { 148 | _container.RegisterMvcControllers(_assemblies); 149 | _container.RegisterMvcIntegratedFilterProvider(); 150 | DependencyResolver.SetResolver(new SimpleInjectorDependencyResolver(_container)); 151 | } 152 | 153 | private static void ConfigureWebApi(HttpConfiguration config) 154 | { 155 | _container.RegisterWebApiControllers(config, _assemblies); 156 | config.DependencyResolver = new SimpleInjectorWebApiDependencyResolver(_container); 157 | config.EnsureInitialized(); 158 | } 159 | 160 | private static void ConfigureFluentScheduler() 161 | { 162 | TaskManager.Initialize(new TaskRegistry(_container)); 163 | } 164 | }; 165 | } 166 | -------------------------------------------------------------------------------- /CommanderDemo.Web/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("CommanderDemo.Web")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("CommanderDemo.Web")] 13 | [assembly: AssemblyCopyright("Copyright © 2015")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("a14cebc2-5782-4b04-9c28-3def77d47bdb")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Revision and Build Numbers 33 | // by using the '*' as shown below: 34 | [assembly: AssemblyVersion("1.0.0.0")] 35 | [assembly: AssemblyFileVersion("1.0.0.0")] 36 | -------------------------------------------------------------------------------- /CommanderDemo.Web/Services/AuditHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using CommandR.Authentication; 5 | using MediatR; 6 | 7 | //_container.RegisterDecorator(typeof(IRequestHandler<,>), typeof(AuditHandler<,>)); 8 | namespace CommanderDemo.Web 9 | { 10 | /// 11 | /// The AuditHandler uses the AuditService to persist all commands and responses to Mongo. 12 | /// This functionality will eventually be cleaned up and added as a separate Nuget package. 13 | /// 14 | internal class AuditHandler : IRequestHandler 15 | where TRequest : IRequest 16 | { 17 | private readonly IRequestHandler _inner; 18 | private readonly AuditService _auditService; 19 | private readonly ExecutionEnvironment _executionEnvironment; 20 | private readonly AuditService.Settings _settings; 21 | private readonly List _inclusionList; 22 | private readonly List _exclusionList; 23 | 24 | public AuditHandler(IRequestHandler inner, AuditService auditService, ExecutionEnvironment executionEnvironment, AuditService.Settings settings) 25 | { 26 | _inner = inner; 27 | _auditService = auditService; 28 | _executionEnvironment = executionEnvironment; 29 | _settings = settings; 30 | 31 | _inclusionList = (settings.IncludeCommands ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); 32 | _exclusionList = (settings.ExcludeCommands ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); 33 | } 34 | 35 | public TResponse Handle(TRequest request) 36 | { 37 | if (_settings.IsDisabled) 38 | return _inner.Handle(request); 39 | 40 | var requestName = request.GetType().Name; 41 | var context = _executionEnvironment.AppContext; 42 | 43 | if ((_inclusionList.Count > 0 && !_inclusionList.Contains(requestName)) || _exclusionList.Contains(requestName)) 44 | return _inner.Handle(request); 45 | 46 | try 47 | { 48 | _auditService.AddChild(new AuditDocument("Request", requestName, request), context); 49 | var response = _inner.Handle(request); 50 | _auditService.AddChild(new AuditDocument("Response", typeof(TResponse).FullName, response), context); 51 | return response; 52 | } 53 | catch (Exception ex) 54 | { 55 | var exceptionInfo = new ExceptionInfo(ex); 56 | _auditService.AddChild(new AuditDocument("ExceptionInfo", exceptionInfo.GetType().FullName, exceptionInfo), context); 57 | throw; 58 | } 59 | } 60 | }; 61 | 62 | [Serializable] 63 | public class ExceptionInfo 64 | { 65 | public ExceptionInfo(Exception exception) 66 | { 67 | Source = exception.Source; 68 | Message = exception.Message; 69 | StackTrace = exception.StackTrace; 70 | } 71 | 72 | public string Source { get; set; } 73 | public string Message { get; set; } 74 | public string StackTrace { get; set; } 75 | }; 76 | } 77 | -------------------------------------------------------------------------------- /CommanderDemo.Web/Services/AuditService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Reflection; 6 | using CfgDotNet; 7 | using CommandR.Authentication; 8 | using MongoDB.Driver; 9 | 10 | //_container.Register(lifestyle); 11 | namespace CommanderDemo.Web 12 | { 13 | /// 14 | /// The AuditService stores a hierarchy of commands and sql executed to Mongo. 15 | /// This functionality will eventually be cleaned up and added as a separate Nuget package. 16 | /// 17 | public class AuditService : IDisposable 18 | { 19 | private readonly Settings _settings; 20 | private readonly MongoCollection _collection; 21 | private readonly string _process; 22 | private AuditDocument _auditDocument; 23 | private bool _disposed; // false 24 | 25 | public AuditService(Settings settings) 26 | { 27 | _settings = settings; 28 | if (_settings.IsDisabled) 29 | return; 30 | 31 | var assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly(); 32 | _process = assembly.GetName().Name; 33 | 34 | var mongoUrl = MongoUrl.Create(settings.ConnectionString); 35 | var client = new MongoClient(settings.ConnectionString); 36 | var server = client.GetServer(); 37 | var database = server.GetDatabase(mongoUrl.DatabaseName); 38 | 39 | _collection = database.GetCollection(settings.CollectionName); 40 | } 41 | 42 | public void AddChild(AuditDocument auditDocument, AppContext context) 43 | { 44 | if (_settings.IsDisabled) 45 | return; 46 | 47 | if (_auditDocument == null) 48 | _auditDocument = new AuditDocument("Parent"); 49 | 50 | if (_auditDocument.Body == null) 51 | _auditDocument.Body = new List(); 52 | 53 | var children = (List)_auditDocument.Body; 54 | auditDocument.Context = context; 55 | auditDocument.Process = _process; 56 | 57 | if (auditDocument.DocumentType != "SQL") 58 | { 59 | children.Add(auditDocument); 60 | } 61 | else 62 | { 63 | var firstSqlAuditDocument = children.FirstOrDefault(x => x.DocumentType == "SQL"); 64 | if (firstSqlAuditDocument != null) 65 | { 66 | firstSqlAuditDocument.Body = (firstSqlAuditDocument.Body ?? string.Empty) + auditDocument.Body.ToString(); 67 | } 68 | else 69 | { 70 | children.Add(auditDocument); 71 | } 72 | } 73 | } 74 | 75 | public void Dispose() 76 | { 77 | Dispose(true); 78 | GC.SuppressFinalize(this); 79 | } 80 | 81 | protected virtual void Dispose(bool disposing) 82 | { 83 | if (_disposed) 84 | return; 85 | 86 | if (disposing && _auditDocument != null && _collection != null) 87 | { 88 | _collection.Insert(_auditDocument); 89 | } 90 | 91 | _disposed = true; 92 | } 93 | 94 | public class Settings : BaseSettings 95 | { 96 | // ex. mongodb://[Username:password@]host[:port]/[database][?options] 97 | public string ConnectionString { get; set; } 98 | public string CollectionName { get; set; } 99 | public string IncludeCommands { get; set; } 100 | public string ExcludeCommands { get; set; } 101 | 102 | public override void Validate() 103 | { 104 | if (IsDisabled) 105 | return; 106 | 107 | if (string.IsNullOrWhiteSpace(CollectionName)) 108 | CollectionName = "Audit"; 109 | 110 | if (string.IsNullOrWhiteSpace(ConnectionString)) 111 | ConnectionString = "mongodb://127.0.0.1/test"; 112 | 113 | CollectionName = CollectionName.Replace("_MACHINE", "_" + Environment.MachineName); 114 | 115 | TestMongoConnection(ConnectionString); 116 | } 117 | 118 | private static void TestMongoConnection(string connectionString) 119 | { 120 | var settings = MongoClientSettings.FromUrl(new MongoUrl(connectionString)); 121 | settings.ConnectTimeout = TimeSpan.FromSeconds(5); 122 | var client = new MongoClient(settings); 123 | var server = client.GetServer(); 124 | server.Ping(); 125 | } 126 | }; 127 | }; 128 | 129 | public class AuditDocument 130 | { 131 | private object _body; 132 | 133 | public AuditDocument(string documentType, string name = "", object body = null) 134 | { 135 | DocumentType = documentType; 136 | Name = name; 137 | Body = body; 138 | Created = DateTime.UtcNow; 139 | HostName = Dns.GetHostEntry(string.Empty).HostName; 140 | } 141 | 142 | public DateTime Created { get; set; } 143 | public Dictionary Context { get; set; } 144 | public string HostName { get; set; } 145 | public string Process { get; set; } 146 | public string DocumentType { get; set; } 147 | public string Name { get; set; } 148 | public string BodyType { get; set; } 149 | 150 | public object Body 151 | { 152 | get { return _body; } 153 | set 154 | { 155 | // mongodb apache code 156 | // if we want shorter full names - TBD 157 | // TypeNameDiscriminator.GetDiscriminator(value.GetType()); 158 | BodyType = value == null ? "null" : value.GetType().FullName; 159 | _body = value; 160 | } 161 | } 162 | }; 163 | } -------------------------------------------------------------------------------- /CommanderDemo.Web/Services/HomeController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Web.Mvc; 3 | using CommanderDemo.Domain; 4 | using MediatR; 5 | 6 | namespace CommanderDemo.Web 7 | { 8 | /// 9 | /// Note how the HomeController only has a single dependency on IMediator. 10 | /// 11 | [Authorize] 12 | public class HomeController : Controller 13 | { 14 | private readonly IMediator _mediator; 15 | 16 | public HomeController(IMediator mediator) 17 | { 18 | _mediator = mediator; 19 | } 20 | 21 | [HttpGet] 22 | public ActionResult Index() 23 | { 24 | return View(); 25 | } 26 | 27 | [HttpGet, AllowAnonymous] 28 | public ActionResult Login() 29 | { 30 | return View(new LoginUser()); 31 | } 32 | 33 | [HttpPost, AllowAnonymous] 34 | public ActionResult Login(LoginUser loginUser) 35 | { 36 | return Send(loginUser, x => RedirectToAction("Index"), View); 37 | } 38 | 39 | [AcceptVerbs(HttpVerbs.Get | HttpVerbs.Post), AllowAnonymous] 40 | public ActionResult Logout(LogoutUser logoutUser) 41 | { 42 | return Send(logoutUser, x => RedirectToAction("Index")); 43 | } 44 | 45 | [HttpGet] 46 | public ActionResult Users(QueryUsers queryUsers) 47 | { 48 | return Send(queryUsers, View); 49 | } 50 | 51 | [HttpGet] 52 | public ActionResult EditUser(GetUser getUser) 53 | { 54 | return Send(getUser, x => View("EditUser", x)); 55 | } 56 | 57 | [HttpPost] 58 | public ActionResult EditUser(SaveUser saveUser) 59 | { 60 | return Send(saveUser, x => RedirectToAction("Users"), () => EditUser(new GetUser { Id = saveUser.Id })); 61 | } 62 | 63 | //Note: Only Admin can delete users due to the Authorize(Users="Admin") attribute, create another user and try it. 64 | [HttpGet] 65 | public ActionResult DeleteUser(DeleteUser deleteUser) 66 | { 67 | return Send(deleteUser, x => RedirectToAction("Users"), () => EditUser(new GetUser { Id = deleteUser.Id })); 68 | } 69 | 70 | /// 71 | /// Send is a wrapper for the Mediator call that provides a common way to put exceptions in the ViewBag 72 | /// which can be displayed by Shared\_Messages. 73 | /// 74 | private ActionResult Send(IRequest cmd, Func success, Func failure = null) 75 | { 76 | try 77 | { 78 | var response = _mediator.Send(cmd); 79 | return success(response); 80 | } 81 | catch (Exception ex) 82 | { 83 | ViewBag._Error = ex.Message; 84 | return failure == null ? success(default(T)) : failure(); 85 | } 86 | } 87 | }; 88 | } 89 | -------------------------------------------------------------------------------- /CommanderDemo.Web/Services/LoggingHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using MediatR; 3 | using Newtonsoft.Json; 4 | 5 | namespace CommanderDemo.Web 6 | { 7 | /// 8 | /// Simple handler that just prints the command and response to the Output window. 9 | /// 10 | public class LoggingHandler : IRequestHandler where TReq : IRequest 11 | { 12 | private readonly IRequestHandler _inner; 13 | 14 | public LoggingHandler(IRequestHandler inner) 15 | { 16 | _inner = inner; 17 | } 18 | 19 | public TResp Handle(TReq request) 20 | { 21 | Debug.WriteLine("{0} (Request) ===================================\r\n{1}", 22 | request.GetType().Name, 23 | JsonConvert.SerializeObject(request, Formatting.Indented)); 24 | 25 | var response = _inner.Handle(request); 26 | 27 | Debug.WriteLine("{0} (Response) ===================================\r\n{1}", 28 | request.GetType().Name, 29 | JsonConvert.SerializeObject(response, Formatting.Indented)); 30 | 31 | return response; 32 | } 33 | }; 34 | } -------------------------------------------------------------------------------- /CommanderDemo.Web/Services/NotificationHub.cs: -------------------------------------------------------------------------------- 1 | using CfgDotNet; 2 | using Microsoft.AspNet.SignalR; 3 | 4 | namespace CommanderDemo.Web 5 | { 6 | /// 7 | /// This Hub is used the SignalrHandler to send INotifications to the clients. 8 | /// It might not be necessary but I'm new to SignalR and was just matching examples 9 | /// I saw online. 10 | /// 11 | public class NotificationHub : Hub 12 | { 13 | internal class Settings : BaseSettings 14 | { 15 | //inherits IsDisabled 16 | }; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /CommanderDemo.Web/Services/SignalrHandler.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Microsoft.AspNet.SignalR; 3 | 4 | namespace CommanderDemo.Web 5 | { 6 | /// 7 | /// Sends all MediatR INotifications to SignalR clients. Just a silly example 8 | /// of how you could use MediatR INotifications. 9 | /// 10 | public class SignalrHandler : INotificationHandler where T : INotification 11 | { 12 | private readonly INotificationHandler _inner; 13 | 14 | public SignalrHandler(INotificationHandler inner) 15 | { 16 | _inner = inner; 17 | } 18 | 19 | public void Handle(T notification) 20 | { 21 | _inner.Handle(notification); 22 | 23 | //Send to clients 24 | var hub = GlobalHost.ConnectionManager.GetHubContext(); 25 | hub.Clients.All.publish(notification); 26 | } 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /CommanderDemo.Web/Services/Startup.cs: -------------------------------------------------------------------------------- 1 | using System.Web.Mvc; 2 | using Owin; 3 | 4 | namespace CommanderDemo.Web 5 | { 6 | /// 7 | /// Bootstrap SignalR 8 | /// 9 | internal class Startup 10 | { 11 | public void Configuration(IAppBuilder app) 12 | { 13 | var hubSettings = DependencyResolver.Current.GetService(); 14 | if (hubSettings.IsDisabled) 15 | return; 16 | 17 | app.MapSignalR(); 18 | } 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /CommanderDemo.Web/Services/TaskRegistry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using CfgDotNet; 4 | using CommanderDemo.Domain; 5 | using CommandR; 6 | using CommandR.Authentication; 7 | using CommandR.Services; 8 | using FluentScheduler; 9 | using FluentScheduler.Model; 10 | using SimpleInjector; 11 | 12 | namespace CommanderDemo.Web 13 | { 14 | /// 15 | /// Configure the schedule for the background tasks we want FluentScheduler to run 16 | /// for us in the background. 17 | /// 18 | public class TaskRegistry : Registry 19 | { 20 | private readonly Container _container; 21 | private readonly Commander _commander; 22 | private readonly AppContext _system; 23 | 24 | public TaskRegistry(Container container) 25 | { 26 | _container = container; 27 | _commander = _container.GetInstance(); 28 | 29 | var settings = _container.GetInstance(); 30 | if (settings.IsDisabled) 31 | { 32 | TaskManager.Stop(); 33 | return; 34 | } 35 | 36 | TaskManager.UnobservedTaskException += TaskManager_UnobservedTaskException; 37 | 38 | //We'll run our system tasks as Admin 39 | _system = new AppContext { Username = "Admin" }; 40 | 41 | Schedule(StartQueueInfiniteLoop).ToRunOnceIn(5).Seconds(); 42 | Schedule(PingMe).ToRunNow().AndEvery(5).Seconds(); 43 | } 44 | 45 | /// 46 | /// The MongoQueueService uses tailable cursors which block until a new item 47 | /// is added. Since all tasks run in the background, no problem. 48 | /// 49 | private void StartQueueInfiniteLoop() 50 | { 51 | var cancellation = new System.Threading.CancellationTokenSource(); 52 | var queueService = _container.GetInstance(); 53 | queueService.StartProcessing(cancellation.Token, Send); 54 | } 55 | 56 | /// 57 | /// Just an example, serves no point, but you should see it in the Output window due 58 | /// to the LoggingHandler. 59 | /// 60 | private void PingMe() 61 | { 62 | Send(new Ping {Name = "Schedule"}, _system); 63 | } 64 | 65 | /// 66 | /// Since each task execution runs in an background thread, set up container lifetime and provide context. 67 | /// 68 | private void Send(object command, AppContext context) 69 | { 70 | using (_container.BeginLifetimeScope()) 71 | { 72 | _container.GetInstance().AppContext = context; 73 | _commander.Send(command).Wait(); 74 | } 75 | } 76 | 77 | /// 78 | /// Any exceptions that occur in the background task threads are caught here. We should probably log. 79 | /// 80 | private static void TaskManager_UnobservedTaskException(TaskExceptionInformation sender, UnhandledExceptionEventArgs e) 81 | { 82 | Debug.WriteLine("TASK ERROR: " + e.ExceptionObject); 83 | } 84 | 85 | internal class Settings : BaseSettings 86 | { 87 | //inherits IsDisabled 88 | }; 89 | }; 90 | } 91 | -------------------------------------------------------------------------------- /CommanderDemo.Web/Services/TokenService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Security.Cryptography; 5 | using System.Web; 6 | using System.Web.Security; 7 | using CfgDotNet; 8 | using CommandR.Authentication; 9 | using JWT; 10 | 11 | namespace CommanderDemo.Web 12 | { 13 | /// 14 | /// NOT FOR PROD: This class is only for the demo which integrates, MVC, WebAPI, and FluentScheduler 15 | /// Manages Tokens for provided context data like Username. Store as little as possibly in the token, 16 | /// it can get big and is sent on each request. 17 | /// 18 | internal class TokenService : ITokenService 19 | { 20 | private readonly Settings _settings; 21 | 22 | public TokenService(Settings settings) 23 | { 24 | _settings = settings; 25 | } 26 | 27 | public string CreateToken(IDictionary data) 28 | { 29 | //MVC 30 | var username = (string)data["Username"]; 31 | FormsAuthentication.SetAuthCookie(username, true); 32 | 33 | //JWT 34 | var tokenId = JsonWebToken.Encode(data, _settings.Key, _settings.Algorithm); 35 | 36 | //Session 37 | if (HttpContext.Current.Session != null) 38 | HttpContext.Current.Session["TokenId"] = tokenId; 39 | 40 | return tokenId; 41 | } 42 | 43 | public IDictionary GetTokenData(string tokenId) 44 | { 45 | if (string.IsNullOrWhiteSpace(tokenId)) 46 | return new Dictionary(); 47 | 48 | try 49 | { 50 | var data = (IDictionary)JsonWebToken.DecodeToObject(tokenId, _settings.Key, verify: true); 51 | return new Dictionary 52 | { 53 | {"Username", TryGet(data, "Username")}, 54 | {"Roles", ParseRoles(TryGet(data, "Roles"))}, 55 | 56 | }; 57 | } 58 | catch 59 | { 60 | return new Dictionary(); 61 | } 62 | } 63 | 64 | public void DeleteToken(string tokenId) 65 | { 66 | FormsAuthentication.SignOut(); 67 | if (HttpContext.Current.Session != null) 68 | { 69 | HttpContext.Current.Session.Abandon(); 70 | } 71 | } 72 | 73 | private static object TryGet(IDictionary dict, string key) 74 | { 75 | return dict.ContainsKey(key) ? dict[key] : null; 76 | } 77 | 78 | //For some reason JWT stores a string array as "ArrayList" so must be parsed 79 | private static string[] ParseRoles(Object roles) 80 | { 81 | var arrayList = roles as ArrayList; 82 | if (arrayList == null) 83 | return new string[0]; 84 | 85 | return (string[])arrayList.ToArray(typeof (string)); 86 | } 87 | 88 | //CfgDotNet strongly-typed settingss 89 | internal class Settings : BaseSettings 90 | { 91 | public string Key { get; set; } 92 | public JwtHashAlgorithm Algorithm { get; set; } 93 | 94 | public override void Validate() 95 | { 96 | //if (string.IsNullOrWhiteSpace(Key)) 97 | // Key = GenerateKey(); 98 | 99 | if (string.IsNullOrWhiteSpace(Key)) 100 | throw new ApplicationException("Invalid TokenService+Settings.Key"); 101 | } 102 | 103 | //REF: http://stackoverflow.com/questions/16574655/generate-a-128-bit-string-in-c-sharp 104 | private static string GenerateKey() 105 | { 106 | var bytes = new byte[100]; 107 | var rng = new RNGCryptoServiceProvider(); 108 | rng.GetBytes(bytes); 109 | var key = Convert.ToBase64String(bytes); 110 | return key.Substring(0, 64); 111 | } 112 | }; 113 | }; 114 | } 115 | -------------------------------------------------------------------------------- /CommanderDemo.Web/Services/TransactionHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data.Entity; 3 | using System.Data.Entity.Validation; 4 | using System.Linq; 5 | using MediatR; 6 | 7 | namespace CommanderDemo.Web 8 | { 9 | /// 10 | /// Wraps inner handlers in an EntityFramework DbContextTransaction. This is not really used 11 | /// by our comands since most of them call SaveChanges directly, but is an example and technically 12 | /// makes their calls unnecessary (except the Saves that return new ids). 13 | /// 14 | internal class TransactionHandler : IRequestHandler 15 | where TRequest : IRequest 16 | { 17 | private readonly IRequestHandler _inner; 18 | private readonly DbContext _db; 19 | 20 | public TransactionHandler(IRequestHandler inner, DbContext db) 21 | { 22 | _inner = inner; 23 | _db = db; 24 | } 25 | 26 | public TResponse Handle(TRequest request) 27 | { 28 | if (_db.Database.CurrentTransaction != null) 29 | return _inner.Handle(request); 30 | 31 | using (var scope = _db.Database.BeginTransaction()) 32 | { 33 | try 34 | { 35 | var response = _inner.Handle(request); 36 | _db.SaveChanges(); 37 | scope.Commit(); 38 | return response; 39 | } 40 | catch (DbEntityValidationException ex) 41 | { 42 | scope.Rollback(); 43 | var errors = ex.EntityValidationErrors 44 | .SelectMany(x => x.ValidationErrors.Select(y => x.Entry.Entity.GetType().Name + ": " + y.ErrorMessage)); 45 | throw new ApplicationException(string.Join(Environment.NewLine, errors)); 46 | } 47 | } 48 | } 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /CommanderDemo.Web/Views/Home/EditUser.cshtml: -------------------------------------------------------------------------------- 1 | @model GetUser.UserInfo 2 | @{ 3 | Layout = "~/Views/Shared/_Layout.cshtml"; 4 | } 5 |
6 |

Edit User

7 | @Html.Partial("_Messages") 8 |
9 |
10 | @Model.Id 11 |
12 |
13 | @Html.TextBoxFor(x => x.Username, new { @class = "form-control", placeholder = "Username" }) 14 |
15 |
16 | @Html.TextBoxFor(x => x.Password, new { @class = "form-control", placeholder = "Password" }) 17 |
18 |
19 | 20 |
21 | 22 | @Html.ActionLink("Cancel", "Users", null, new { @class = "btn btn-default" }) 23 | @if (Model.Id > 0) 24 | { 25 | @Html.ActionLink("Delete", "DeleteUser", new { Model.Id }, new { @class = "btn btn-warning" }) 26 | } 27 |
28 |
29 | -------------------------------------------------------------------------------- /CommanderDemo.Web/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
Aurelia Contacts
14 | 15 |
16 | 17 | 18 | 25 | 28 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /CommanderDemo.Web/Views/Home/Login.cshtml: -------------------------------------------------------------------------------- 1 | @model LoginUser 2 | @{ 3 | Layout = "~/Views/Shared/_Layout.cshtml"; 4 | } 5 |
6 |

Login

7 | @Html.Partial("_Messages") 8 |
9 |
10 | @Html.TextBoxFor(x => x.Username, new { @class = "form-control", placeholder = "Username" }) 11 |
12 |
13 | @Html.PasswordFor(x => x.Password, new { @class = "form-control", placeholder = "Password" }) 14 |
15 | 16 |
17 |
18 | -------------------------------------------------------------------------------- /CommanderDemo.Web/Views/Home/Users.cshtml: -------------------------------------------------------------------------------- 1 | @model QueryUsers.Response 2 | @{ 3 | Layout = "~/Views/Shared/_Layout.cshtml"; 4 | } 5 |
6 | @Html.ActionLink("Add User", "EditUser", null, new { @class = "btn btn-primary" }) 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | @foreach (var user in Model.Result.Items) 21 | { 22 | 23 | 24 | 25 | 26 | 27 | 28 | } 29 | 30 |
 IdUsernameIsActive
@Html.ActionLink("Edit", "EditUser", new { user.Id }, new { @class = "btn btn-primary" })@user.Id@user.Username@user.IsActive
31 | -------------------------------------------------------------------------------- /CommanderDemo.Web/Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | MediatorDemo 6 | 7 | 8 | 9 | 10 | 11 | 33 |
34 |
35 | @RenderBody() 36 |
37 |
38 | @if (Request.IsAuthenticated) 39 | { 40 | @Html.ActionLink("Logout " + User.Identity.Name, "Logout", "Home") 41 | } 42 |
43 |
44 | 45 | 46 | -------------------------------------------------------------------------------- /CommanderDemo.Web/Views/Shared/_Messages.cshtml: -------------------------------------------------------------------------------- 1 | @model dynamic 2 | @{ 3 | var error = (string)ViewBag._Error; 4 | } 5 | 6 | @if (!string.IsNullOrWhiteSpace(error)) 7 | { 8 |
@error
9 | } 10 | -------------------------------------------------------------------------------- /CommanderDemo.Web/Web.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 |
6 |
7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /CommanderDemo.Web/config.js: -------------------------------------------------------------------------------- 1 | System.config({ 2 | "transpiler": "babel", 3 | "babelOptions": { 4 | "optional": [ 5 | "runtime", 6 | "es7.decorators" 7 | ] 8 | }, 9 | "paths": { 10 | "*": "*.js", 11 | "github:*": "jspm_packages/github/*.js", 12 | "npm:*": "jspm_packages/npm/*.js" 13 | } 14 | }); 15 | 16 | System.config({ 17 | "map": { 18 | "aurelia-bootstrapper": "github:aurelia/bootstrapper@0.14.0", 19 | "aurelia-event-aggregator": "github:aurelia/event-aggregator@0.6.0", 20 | "aurelia-http-client": "github:aurelia/http-client@0.10.0", 21 | "babel": "npm:babel-core@5.6.20", 22 | "babel-runtime": "npm:babel-runtime@5.6.20", 23 | "bootstrap": "github:twbs/bootstrap@3.3.4", 24 | "core-js": "npm:core-js@0.9.18", 25 | "css": "github:systemjs/plugin-css@0.1.10", 26 | "font-awesome": "npm:font-awesome@4.3.0", 27 | "nprogress": "github:rstacruz/nprogress@0.1.6", 28 | "github:aurelia/binding@0.8.0": { 29 | "aurelia-dependency-injection": "github:aurelia/dependency-injection@0.9.0", 30 | "aurelia-metadata": "github:aurelia/metadata@0.7.0", 31 | "aurelia-task-queue": "github:aurelia/task-queue@0.6.0", 32 | "core-js": "npm:core-js@0.9.18" 33 | }, 34 | "github:aurelia/bootstrapper@0.14.0": { 35 | "aurelia-event-aggregator": "github:aurelia/event-aggregator@0.6.0", 36 | "aurelia-framework": "github:aurelia/framework@0.13.0", 37 | "aurelia-history": "github:aurelia/history@0.6.0", 38 | "aurelia-history-browser": "github:aurelia/history-browser@0.6.0", 39 | "aurelia-loader-default": "github:aurelia/loader-default@0.9.0", 40 | "aurelia-logging-console": "github:aurelia/logging-console@0.6.0", 41 | "aurelia-router": "github:aurelia/router@0.10.0", 42 | "aurelia-templating": "github:aurelia/templating@0.13.0", 43 | "aurelia-templating-binding": "github:aurelia/templating-binding@0.13.0", 44 | "aurelia-templating-resources": "github:aurelia/templating-resources@0.13.0", 45 | "aurelia-templating-router": "github:aurelia/templating-router@0.14.0", 46 | "core-js": "npm:core-js@0.9.18" 47 | }, 48 | "github:aurelia/dependency-injection@0.9.0": { 49 | "aurelia-logging": "github:aurelia/logging@0.6.0", 50 | "aurelia-metadata": "github:aurelia/metadata@0.7.0", 51 | "core-js": "npm:core-js@0.9.18" 52 | }, 53 | "github:aurelia/event-aggregator@0.5.0": { 54 | "aurelia-logging": "github:aurelia/logging@0.5.0" 55 | }, 56 | "github:aurelia/event-aggregator@0.6.0": { 57 | "aurelia-logging": "github:aurelia/logging@0.6.0" 58 | }, 59 | "github:aurelia/framework@0.13.0": { 60 | "aurelia-binding": "github:aurelia/binding@0.8.0", 61 | "aurelia-dependency-injection": "github:aurelia/dependency-injection@0.9.0", 62 | "aurelia-loader": "github:aurelia/loader@0.8.0", 63 | "aurelia-logging": "github:aurelia/logging@0.6.0", 64 | "aurelia-metadata": "github:aurelia/metadata@0.7.0", 65 | "aurelia-path": "github:aurelia/path@0.8.0", 66 | "aurelia-task-queue": "github:aurelia/task-queue@0.6.0", 67 | "aurelia-templating": "github:aurelia/templating@0.13.0", 68 | "core-js": "npm:core-js@0.9.18" 69 | }, 70 | "github:aurelia/history-browser@0.6.0": { 71 | "aurelia-history": "github:aurelia/history@0.6.0", 72 | "core-js": "npm:core-js@0.9.18" 73 | }, 74 | "github:aurelia/http-client@0.10.0": { 75 | "aurelia-path": "github:aurelia/path@0.8.0", 76 | "core-js": "npm:core-js@0.9.18" 77 | }, 78 | "github:aurelia/loader-default@0.9.0": { 79 | "aurelia-loader": "github:aurelia/loader@0.8.0", 80 | "aurelia-metadata": "github:aurelia/metadata@0.7.0" 81 | }, 82 | "github:aurelia/loader@0.8.0": { 83 | "aurelia-html-template-element": "github:aurelia/html-template-element@0.2.0", 84 | "aurelia-path": "github:aurelia/path@0.8.0", 85 | "core-js": "npm:core-js@0.9.18", 86 | "webcomponentsjs": "github:webcomponents/webcomponentsjs@0.6.3" 87 | }, 88 | "github:aurelia/metadata@0.7.0": { 89 | "core-js": "npm:core-js@0.9.18" 90 | }, 91 | "github:aurelia/route-recognizer@0.6.0": { 92 | "core-js": "npm:core-js@0.9.18" 93 | }, 94 | "github:aurelia/router@0.10.0": { 95 | "aurelia-dependency-injection": "github:aurelia/dependency-injection@0.9.0", 96 | "aurelia-event-aggregator": "github:aurelia/event-aggregator@0.6.0", 97 | "aurelia-history": "github:aurelia/history@0.6.0", 98 | "aurelia-logging": "github:aurelia/logging@0.6.0", 99 | "aurelia-path": "github:aurelia/path@0.8.0", 100 | "aurelia-route-recognizer": "github:aurelia/route-recognizer@0.6.0", 101 | "core-js": "npm:core-js@0.9.18" 102 | }, 103 | "github:aurelia/templating-binding@0.13.0": { 104 | "aurelia-binding": "github:aurelia/binding@0.8.0", 105 | "aurelia-logging": "github:aurelia/logging@0.6.0", 106 | "aurelia-templating": "github:aurelia/templating@0.13.0" 107 | }, 108 | "github:aurelia/templating-resources@0.13.0": { 109 | "aurelia-binding": "github:aurelia/binding@0.8.0", 110 | "aurelia-dependency-injection": "github:aurelia/dependency-injection@0.9.0", 111 | "aurelia-logging": "github:aurelia/logging@0.6.0", 112 | "aurelia-task-queue": "github:aurelia/task-queue@0.6.0", 113 | "aurelia-templating": "github:aurelia/templating@0.13.0", 114 | "core-js": "npm:core-js@0.9.18" 115 | }, 116 | "github:aurelia/templating-router@0.14.0": { 117 | "aurelia-dependency-injection": "github:aurelia/dependency-injection@0.9.0", 118 | "aurelia-metadata": "github:aurelia/metadata@0.7.0", 119 | "aurelia-path": "github:aurelia/path@0.8.0", 120 | "aurelia-router": "github:aurelia/router@0.10.0", 121 | "aurelia-templating": "github:aurelia/templating@0.13.0" 122 | }, 123 | "github:aurelia/templating@0.13.0": { 124 | "aurelia-binding": "github:aurelia/binding@0.8.0", 125 | "aurelia-dependency-injection": "github:aurelia/dependency-injection@0.9.0", 126 | "aurelia-html-template-element": "github:aurelia/html-template-element@0.2.0", 127 | "aurelia-loader": "github:aurelia/loader@0.8.0", 128 | "aurelia-logging": "github:aurelia/logging@0.6.0", 129 | "aurelia-metadata": "github:aurelia/metadata@0.7.0", 130 | "aurelia-path": "github:aurelia/path@0.8.0", 131 | "aurelia-task-queue": "github:aurelia/task-queue@0.6.0", 132 | "core-js": "npm:core-js@0.9.18" 133 | }, 134 | "github:jspm/nodelibs-assert@0.1.0": { 135 | "assert": "npm:assert@1.3.0" 136 | }, 137 | "github:jspm/nodelibs-buffer@0.1.0": { 138 | "buffer": "npm:buffer@3.2.2" 139 | }, 140 | "github:jspm/nodelibs-events@0.1.0": { 141 | "events-browserify": "npm:events-browserify@0.0.1" 142 | }, 143 | "github:jspm/nodelibs-http@1.7.1": { 144 | "Base64": "npm:Base64@0.2.1", 145 | "events": "github:jspm/nodelibs-events@0.1.0", 146 | "inherits": "npm:inherits@2.0.1", 147 | "stream": "github:jspm/nodelibs-stream@0.1.0", 148 | "url": "github:jspm/nodelibs-url@0.1.0", 149 | "util": "github:jspm/nodelibs-util@0.1.0" 150 | }, 151 | "github:jspm/nodelibs-https@0.1.0": { 152 | "https-browserify": "npm:https-browserify@0.0.0" 153 | }, 154 | "github:jspm/nodelibs-os@0.1.0": { 155 | "os-browserify": "npm:os-browserify@0.1.2" 156 | }, 157 | "github:jspm/nodelibs-path@0.1.0": { 158 | "path-browserify": "npm:path-browserify@0.0.0" 159 | }, 160 | "github:jspm/nodelibs-process@0.1.1": { 161 | "process": "npm:process@0.10.1" 162 | }, 163 | "github:jspm/nodelibs-stream@0.1.0": { 164 | "stream-browserify": "npm:stream-browserify@1.0.0" 165 | }, 166 | "github:jspm/nodelibs-url@0.1.0": { 167 | "url": "npm:url@0.10.3" 168 | }, 169 | "github:jspm/nodelibs-util@0.1.0": { 170 | "util": "npm:util@0.10.3" 171 | }, 172 | "github:rstacruz/nprogress@0.1.6": { 173 | "css": "github:systemjs/plugin-css@0.1.10" 174 | }, 175 | "github:systemjs/plugin-css@0.1.10": { 176 | "clean-css": "npm:clean-css@3.1.9", 177 | "fs": "github:jspm/nodelibs-fs@0.1.2", 178 | "path": "github:jspm/nodelibs-path@0.1.0" 179 | }, 180 | "github:twbs/bootstrap@3.3.4": { 181 | "jquery": "github:components/jquery@2.1.4" 182 | }, 183 | "npm:amdefine@0.1.0": { 184 | "fs": "github:jspm/nodelibs-fs@0.1.2", 185 | "module": "github:jspm/nodelibs-module@0.1.0", 186 | "path": "github:jspm/nodelibs-path@0.1.0", 187 | "process": "github:jspm/nodelibs-process@0.1.1" 188 | }, 189 | "npm:assert@1.3.0": { 190 | "util": "npm:util@0.10.3" 191 | }, 192 | "npm:babel-runtime@5.6.20": { 193 | "process": "github:jspm/nodelibs-process@0.1.1" 194 | }, 195 | "npm:buffer@3.2.2": { 196 | "base64-js": "npm:base64-js@0.0.8", 197 | "ieee754": "npm:ieee754@1.1.5", 198 | "is-array": "npm:is-array@1.0.1" 199 | }, 200 | "npm:clean-css@3.1.9": { 201 | "buffer": "github:jspm/nodelibs-buffer@0.1.0", 202 | "commander": "npm:commander@2.6.0", 203 | "fs": "github:jspm/nodelibs-fs@0.1.2", 204 | "http": "github:jspm/nodelibs-http@1.7.1", 205 | "https": "github:jspm/nodelibs-https@0.1.0", 206 | "os": "github:jspm/nodelibs-os@0.1.0", 207 | "path": "github:jspm/nodelibs-path@0.1.0", 208 | "process": "github:jspm/nodelibs-process@0.1.1", 209 | "source-map": "npm:source-map@0.1.43", 210 | "url": "github:jspm/nodelibs-url@0.1.0", 211 | "util": "github:jspm/nodelibs-util@0.1.0" 212 | }, 213 | "npm:commander@2.6.0": { 214 | "child_process": "github:jspm/nodelibs-child_process@0.1.0", 215 | "events": "github:jspm/nodelibs-events@0.1.0", 216 | "path": "github:jspm/nodelibs-path@0.1.0", 217 | "process": "github:jspm/nodelibs-process@0.1.1" 218 | }, 219 | "npm:core-js@0.9.18": { 220 | "fs": "github:jspm/nodelibs-fs@0.1.2", 221 | "process": "github:jspm/nodelibs-process@0.1.1", 222 | "systemjs-json": "github:systemjs/plugin-json@0.1.0" 223 | }, 224 | "npm:core-util-is@1.0.1": { 225 | "buffer": "github:jspm/nodelibs-buffer@0.1.0" 226 | }, 227 | "npm:events-browserify@0.0.1": { 228 | "process": "github:jspm/nodelibs-process@0.1.1" 229 | }, 230 | "npm:font-awesome@4.3.0": { 231 | "css": "github:systemjs/plugin-css@0.1.10" 232 | }, 233 | "npm:https-browserify@0.0.0": { 234 | "http": "github:jspm/nodelibs-http@1.7.1" 235 | }, 236 | "npm:inherits@2.0.1": { 237 | "util": "github:jspm/nodelibs-util@0.1.0" 238 | }, 239 | "npm:os-browserify@0.1.2": { 240 | "os": "github:jspm/nodelibs-os@0.1.0" 241 | }, 242 | "npm:path-browserify@0.0.0": { 243 | "process": "github:jspm/nodelibs-process@0.1.1" 244 | }, 245 | "npm:punycode@1.3.2": { 246 | "process": "github:jspm/nodelibs-process@0.1.1" 247 | }, 248 | "npm:readable-stream@1.1.13": { 249 | "buffer": "github:jspm/nodelibs-buffer@0.1.0", 250 | "core-util-is": "npm:core-util-is@1.0.1", 251 | "events": "github:jspm/nodelibs-events@0.1.0", 252 | "inherits": "npm:inherits@2.0.1", 253 | "isarray": "npm:isarray@0.0.1", 254 | "process": "github:jspm/nodelibs-process@0.1.1", 255 | "stream": "github:jspm/nodelibs-stream@0.1.0", 256 | "stream-browserify": "npm:stream-browserify@1.0.0", 257 | "string_decoder": "npm:string_decoder@0.10.31", 258 | "util": "github:jspm/nodelibs-util@0.1.0" 259 | }, 260 | "npm:source-map@0.1.43": { 261 | "amdefine": "npm:amdefine@0.1.0", 262 | "fs": "github:jspm/nodelibs-fs@0.1.2", 263 | "path": "github:jspm/nodelibs-path@0.1.0", 264 | "process": "github:jspm/nodelibs-process@0.1.1" 265 | }, 266 | "npm:stream-browserify@1.0.0": { 267 | "events": "github:jspm/nodelibs-events@0.1.0", 268 | "inherits": "npm:inherits@2.0.1", 269 | "readable-stream": "npm:readable-stream@1.1.13" 270 | }, 271 | "npm:string_decoder@0.10.31": { 272 | "buffer": "github:jspm/nodelibs-buffer@0.1.0" 273 | }, 274 | "npm:url@0.10.3": { 275 | "assert": "github:jspm/nodelibs-assert@0.1.0", 276 | "punycode": "npm:punycode@1.3.2", 277 | "querystring": "npm:querystring@0.2.0", 278 | "util": "github:jspm/nodelibs-util@0.1.0" 279 | }, 280 | "npm:util@0.10.3": { 281 | "inherits": "npm:inherits@2.0.1", 282 | "process": "github:jspm/nodelibs-process@0.1.1" 283 | } 284 | } 285 | }); 286 | 287 | -------------------------------------------------------------------------------- /CommanderDemo.Web/gulpfile.js: -------------------------------------------------------------------------------- 1 | require('require-dir')('build/tasks'); -------------------------------------------------------------------------------- /CommanderDemo.Web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app-contacts", 3 | "version": "0.3.0", 4 | "description": "A sample app that lets you browse and edit contacts.", 5 | "keywords": [ 6 | "aurelia", 7 | "application" 8 | ], 9 | "homepage": "http://aurelia.io", 10 | "bugs": { 11 | "url": "https://github.com/aurelia/app-contacts/issues" 12 | }, 13 | "license": "MIT", 14 | "author": "Rob Eisenberg (http://robeisenberg.com/)", 15 | "main": "dist/commonjs/index.js", 16 | "repository": { 17 | "type": "git", 18 | "url": "http://github.com/aurelia/app-contacts" 19 | }, 20 | "devDependencies": { 21 | "aurelia-tools": "^0.1.3", 22 | "browser-sync": "^1.8.1", 23 | "conventional-changelog": "0.0.11", 24 | "del": "^1.1.0", 25 | "gulp": "^3.8.10", 26 | "gulp-babel": "^5.1.0", 27 | "gulp-bump": "^0.3.1", 28 | "gulp-changed": "^1.1.0", 29 | "gulp-jshint": "^1.9.0", 30 | "gulp-plumber": "^0.6.6", 31 | "gulp-protractor": "^0.0.12", 32 | "gulp-sourcemaps": "^1.3.0", 33 | "gulp-yuidoc": "^0.1.2", 34 | "jasmine-core": "^2.1.3", 35 | "jshint-stylish": "^1.0.0", 36 | "karma": "^0.12.28", 37 | "karma-babel-preprocessor": "^5.2.1", 38 | "karma-chrome-launcher": "^0.1.7", 39 | "karma-jasmine": "^0.3.2", 40 | "karma-jspm": "^1.1.4", 41 | "object.assign": "^1.0.3", 42 | "require-dir": "^0.1.0", 43 | "run-sequence": "^1.0.2", 44 | "vinyl-paths": "^1.0.0", 45 | "yargs": "^2.1.1" 46 | }, 47 | "jspm": { 48 | "dependencies": { 49 | "aurelia-bootstrapper": "github:aurelia/bootstrapper@^0.14.0", 50 | "aurelia-event-aggregator": "github:aurelia/event-aggregator@^0.6.0", 51 | "aurelia-http-client": "github:aurelia/http-client@^0.10.0", 52 | "bootstrap": "github:twbs/bootstrap@^3.3.4", 53 | "css": "github:systemjs/plugin-css@^0.1.9", 54 | "font-awesome": "npm:font-awesome@^4.3.0", 55 | "nprogress": "github:rstacruz/nprogress@^0.1.6" 56 | }, 57 | "devDependencies": { 58 | "babel": "npm:babel-core@^5.1.13", 59 | "babel-runtime": "npm:babel-runtime@^5.1.13", 60 | "core-js": "npm:core-js@^0.9.4" 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /CommanderDemo.Web/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /CommanderDemo.Web/src/api.js: -------------------------------------------------------------------------------- 1 | angular.module("app").service("api", [ 2 | 'jsonRpc', 3 | function (jsonRpc) { 4 | var self = {}; 5 | self.LoginUser = function() { 6 | return jsonRpc.send("LoginUser", Array.prototype.slice.call(arguments)); 7 | }; 8 | self.LogoutUser = function() { 9 | return jsonRpc.send("LogoutUser", Array.prototype.slice.call(arguments)); 10 | }; 11 | self.DeleteContact = function() { 12 | return jsonRpc.send("DeleteContact", Array.prototype.slice.call(arguments)); 13 | }; 14 | self.GetContact = function() { 15 | return jsonRpc.send("GetContact", Array.prototype.slice.call(arguments)); 16 | }; 17 | self.QueryContacts = function() { 18 | return jsonRpc.send("QueryContacts", Array.prototype.slice.call(arguments)); 19 | }; 20 | self.SaveContact = function() { 21 | return jsonRpc.send("SaveContact", Array.prototype.slice.call(arguments)); 22 | }; 23 | self.DeleteUser = function() { 24 | return jsonRpc.send("DeleteUser", Array.prototype.slice.call(arguments)); 25 | }; 26 | self.GetUser = function() { 27 | return jsonRpc.send("GetUser", Array.prototype.slice.call(arguments)); 28 | }; 29 | self.QueryUsers = function() { 30 | return jsonRpc.send("QueryUsers", Array.prototype.slice.call(arguments)); 31 | }; 32 | self.SaveUser = function() { 33 | return jsonRpc.send("SaveUser", Array.prototype.slice.call(arguments)); 34 | }; 35 | return self; 36 | } 37 | ]); 38 | -------------------------------------------------------------------------------- /CommanderDemo.Web/src/api.tt: -------------------------------------------------------------------------------- 1 | <#@ Template Language="C#" Debug="true" HostSpecific="true" #> 2 | <#@ Output Extension=".js" #> 3 | <#@ Assembly Name="System.Core" #> 4 | <#@ Assembly Name="EnvDTE" #> 5 | <#@ Import Namespace="EnvDTE" #> 6 | <#@ Import Namespace="System.Linq" #> 7 | <#@ Import Namespace="System.Collections.Generic" #> 8 | <# 9 | // Generate a jsonRpc call for every ICommand or IQuery in the system. 10 | const string fileInProject = "ContactDb.cs"; 11 | // TEMPLATE // 12 | #> 13 | angular.module("app").service("api", [ 14 | 'jsonRpc', 15 | function (jsonRpc) { 16 | var self = {}; 17 | <# 18 | foreach (var rpc in GetRpcs(fileInProject)) { 19 | #> 20 | self.<#= rpc.FormattedName #> = function() { 21 | return jsonRpc.send("<#= rpc.Name #>", Array.prototype.slice.call(arguments)); 22 | }; 23 | <# 24 | } 25 | #> 26 | return self; 27 | } 28 | ]); 29 | <#+ // SHARED CODE // 30 | public class Rpc { 31 | public string Name { get; set; } 32 | public string FormattedName { get; set; } 33 | }; 34 | 35 | public IEnumerable GetRpcs(string fileInProject) 36 | { 37 | var list = new List(); 38 | var requests = GetClasses(fileInProject).Where(cl => cl.ImplementedInterfaces.Cast().Any(i => i.Name.Contains("ICommand") || i.Name.Contains("IQuery"))); 39 | foreach (var req in requests) 40 | { 41 | list.Add(new Rpc { 42 | Name = req.Name, 43 | FormattedName = req.Name, 44 | }); 45 | } 46 | return list; 47 | } 48 | 49 | //REF: http://www.codeproject.com/Articles/39071/Declarative-Dependency-Property-Definition-with-T4.aspx 50 | public IEnumerable GetClasses(string fileInProject) { 51 | var hostServiceProvider = (IServiceProvider)Host; 52 | var dte = (EnvDTE.DTE)hostServiceProvider.GetService(typeof(EnvDTE.DTE)); 53 | var containingProjectItem = dte.Solution.FindProjectItem(fileInProject); 54 | var project = containingProjectItem.ContainingProject; 55 | var elements = CodeElementsInProjectItems(project.ProjectItems); 56 | return elements.Where(el => el.Kind == vsCMElement.vsCMElementClass).Cast(); 57 | } 58 | public IEnumerable GetMethods(CodeClass codeClass) { 59 | foreach (CodeElement codeElement in codeClass.Members) { 60 | if (codeElement.Kind == vsCMElement.vsCMElementFunction) { 61 | yield return (CodeFunction)codeElement; 62 | } 63 | } 64 | } 65 | public IEnumerable CodeElementsInProjectItems(ProjectItems projectItems) { 66 | foreach (ProjectItem projectItem in projectItems) { 67 | foreach (CodeElement el in CodeElementsInProjectItem(projectItem)) { 68 | yield return el; 69 | } 70 | } 71 | } 72 | public IEnumerable CodeElementsInProjectItem(ProjectItem projectItem) { 73 | FileCodeModel fileCodeModel = projectItem.FileCodeModel; 74 | if (fileCodeModel != null) { 75 | foreach (CodeElement codeElement in fileCodeModel.CodeElements) { 76 | foreach(CodeElement el in CodeElementDescendantsAndSelf(codeElement)) { 77 | yield return el; 78 | } 79 | } 80 | } 81 | if (projectItem.ProjectItems != null) { 82 | foreach (ProjectItem childItem in projectItem.ProjectItems) { 83 | foreach (CodeElement el in CodeElementsInProjectItem(childItem)) { 84 | yield return el; 85 | } 86 | } 87 | } 88 | } 89 | public IEnumerable CodeElementsDescendants(CodeElements codeElements) { 90 | foreach(CodeElement element in codeElements) { 91 | foreach (CodeElement descendant in CodeElementDescendantsAndSelf(element)) { 92 | yield return descendant; 93 | } 94 | } 95 | } 96 | public IEnumerable CodeElementDescendantsAndSelf(CodeElement codeElement) { 97 | yield return codeElement; 98 | CodeElements codeElements; 99 | switch(codeElement.Kind) { 100 | /* namespaces */ 101 | case vsCMElement.vsCMElementNamespace: { 102 | CodeNamespace codeNamespace = (CodeNamespace)codeElement; 103 | codeElements = codeNamespace.Members; 104 | foreach(CodeElement descendant in CodeElementsDescendants(codeElements)) { 105 | yield return descendant; 106 | } 107 | break; 108 | } 109 | /* Process classes */ 110 | case vsCMElement.vsCMElementClass: { 111 | CodeClass codeClass = (CodeClass)codeElement; 112 | codeElements = codeClass.Members; 113 | foreach(CodeElement descendant in CodeElementsDescendants(codeElements)) { 114 | yield return descendant; 115 | } 116 | break; 117 | } 118 | } 119 | } 120 | public IEnumerable Attributes(CodeClass codeClass) { 121 | foreach (CodeElement element in codeClass.Attributes) { 122 | yield return (CodeAttribute)element; 123 | } 124 | } 125 | public IEnumerable Attributes(CodeFunction codeFunction) { 126 | foreach (CodeElement element in codeFunction.Attributes) { 127 | yield return (CodeAttribute)element; 128 | } 129 | } 130 | #> -------------------------------------------------------------------------------- /CommanderDemo.Web/src/app.html: -------------------------------------------------------------------------------- 1 | 30 | -------------------------------------------------------------------------------- /CommanderDemo.Web/src/app.js: -------------------------------------------------------------------------------- 1 | import {WebAPI} from './web-api'; 2 | import {Notifications} from './notifications'; 3 | 4 | export class App { 5 | static inject = [WebAPI, Notifications]; 6 | constructor(api, notifications) { 7 | this.api = api; 8 | this.notifications = notifications; 9 | } 10 | 11 | configureRouter(config, router){ 12 | config.title = 'Contacts'; 13 | config.map([ 14 | { route: '', moduleId: 'no-selection', name:'home', title: 'Select'}, 15 | { route: 'contacts/:id', moduleId: 'contact-detail', name:'contacts' } 16 | ]); 17 | 18 | this.router = router; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /CommanderDemo.Web/src/contact-detail.html: -------------------------------------------------------------------------------- 1 | 40 | -------------------------------------------------------------------------------- /CommanderDemo.Web/src/contact-detail.js: -------------------------------------------------------------------------------- 1 | import {EventAggregator} from 'aurelia-event-aggregator'; 2 | import {WebAPI} from './web-api'; 3 | import {ContactUpdated,ContactViewed} from './messages'; 4 | import {areEqual} from './utility'; 5 | 6 | export class ContactDetail { 7 | static inject = [WebAPI, EventAggregator]; 8 | constructor(api, ea){ 9 | this.api = api; 10 | this.ea = ea; 11 | } 12 | 13 | activate(params, config){ 14 | return this.api.getContactDetails(params.id).then(contact => { 15 | this.contact = contact; 16 | config.navModel.setTitle(contact.firstName); 17 | this.originalContact = JSON.parse(JSON.stringify(contact)); 18 | this.ea.publish(new ContactViewed(contact)); 19 | }).catch(function(){}); 20 | } 21 | 22 | get canSave(){ 23 | return this.contact.firstName && this.contact.lastName && !this.api.isRequesting; 24 | } 25 | 26 | save(){ 27 | this.api.saveContact(this.contact).then(contact => { 28 | this.contact = contact; 29 | this.originalContact = JSON.parse(JSON.stringify(contact)); 30 | this.ea.publish(new ContactUpdated(contact)); 31 | }).catch(function(){}); 32 | } 33 | 34 | get canDelete(){ 35 | return !!this.contact.id; 36 | } 37 | 38 | delete(){ 39 | this.api.deleteContact(this.contact).then(contact => { 40 | this.ea.publish(new ContactUpdated(null)); 41 | }).catch(function(){}); 42 | } 43 | 44 | canDeactivate(){ 45 | if(!areEqual(this.originalContact, this.contact)){ 46 | let result = confirm('You have unsaved changes. Are you sure you wish to leave?'); 47 | 48 | if(!result){ 49 | this.ea.publish(new ContactViewed(this.contact)); 50 | } 51 | 52 | return result; 53 | } 54 | 55 | return true; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /CommanderDemo.Web/src/contact-list.html: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /CommanderDemo.Web/src/contact-list.js: -------------------------------------------------------------------------------- 1 | import {EventAggregator} from 'aurelia-event-aggregator'; 2 | import {WebAPI} from './web-api'; 3 | import {ContactUpdated, ContactViewed} from './messages'; 4 | import {Router} from 'aurelia-router'; 5 | 6 | export class ContactList { 7 | static inject = [WebAPI, EventAggregator, Router]; 8 | constructor(api, ea, router){ 9 | this.api = api; 10 | this.contacts = []; 11 | this.router = router; 12 | 13 | ea.subscribe(ContactViewed, msg => this.select(msg.contact)); 14 | ea.subscribe(ContactUpdated, msg => { 15 | this.api.getContactList().then(contacts => { 16 | this.contacts = contacts; 17 | if (!msg.contact) { 18 | this.router.navigateToRoute('home'); 19 | } else if (msg.contact.id !== this.selectedId) { 20 | this.router.navigateToRoute('contacts', {id:msg.contact.id}); 21 | } 22 | }).catch(function(){}); 23 | }); 24 | } 25 | 26 | created(){ 27 | this.api.getContactList().then(contacts => { 28 | this.contacts = contacts; 29 | }).catch(function(){}); 30 | } 31 | 32 | select(contact){ 33 | this.selectedId = contact.id; 34 | return true; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /CommanderDemo.Web/src/json-rpc.js: -------------------------------------------------------------------------------- 1 | import {HttpClient} from 'aurelia-http-client'; 2 | 3 | class Request { 4 | constructor(method, params) { 5 | this.jsonrpc = "2.0"; 6 | this.id = guid(); 7 | this.method = method; 8 | this.params = params || {}; 9 | } 10 | } 11 | 12 | class Response { 13 | constructor(response) { 14 | this.jsonrpc = response.jsonrpc; 15 | this.id = response.id; 16 | this.result = response.result; 17 | this.error = response.error; 18 | } 19 | } 20 | 21 | function guid() { 22 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 23 | var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); 24 | return v.toString(16); 25 | }); 26 | } 27 | 28 | //NOTE: this is an incomplete implementation of JsonRpc for Aurelia just for the demo. 29 | export class JsonRpc { 30 | static inject = [HttpClient]; 31 | constructor(http) { 32 | this.http = http; 33 | } 34 | 35 | send(method, params) { 36 | return new Promise((resolve, reject) => { 37 | return this.http 38 | .createRequest('/jsonrpc') 39 | .asPost() 40 | .withHeader('Authorization', window.JsonRpcToken) 41 | .withContent(new Request(method, params)) 42 | .send() 43 | .then(response => { 44 | var resp = new Response(response.content); 45 | if (resp.error) { 46 | reject(resp.error); 47 | }else { 48 | resolve(resp.result); 49 | } 50 | }).catch(error => { 51 | reject(error); 52 | }); 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /CommanderDemo.Web/src/loading-indicator.js: -------------------------------------------------------------------------------- 1 | import nprogress from 'nprogress'; 2 | import {bindable, noView} from 'aurelia-framework'; 3 | 4 | @noView 5 | export class LoadingIndicator { 6 | @bindable loading = false; 7 | 8 | loadingChanged(newValue){ 9 | if(newValue){ 10 | nprogress.start(); 11 | }else{ 12 | nprogress.done(); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /CommanderDemo.Web/src/messages.js: -------------------------------------------------------------------------------- 1 | export class ContactUpdated { 2 | constructor(contact){ 3 | this.contact = contact; 4 | } 5 | } 6 | 7 | export class ContactViewed { 8 | constructor(contact){ 9 | this.contact = contact; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /CommanderDemo.Web/src/no-selection.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /CommanderDemo.Web/src/no-selection.js: -------------------------------------------------------------------------------- 1 | export class NoSelection{ 2 | constructor(){ 3 | this.message = "Please Select a Contact."; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /CommanderDemo.Web/src/notifications.js: -------------------------------------------------------------------------------- 1 | import {EventAggregator} from 'aurelia-event-aggregator'; 2 | import {ContactUpdated} from './messages'; 3 | 4 | export class Notifications { 5 | static inject = [EventAggregator]; 6 | constructor(ea) { 7 | $(function () { 8 | var hub = $.connection.notificationHub; 9 | if (!hub) 10 | return; 11 | 12 | hub.client.publish = function(message) { 13 | setTimeout(function() { 14 | alert(message.Message); 15 | ea.publish(new ContactUpdated(null)); 16 | }, 1); 17 | } 18 | 19 | $.connection.hub.logging = true; 20 | $.connection.hub.url = "/signalr"; 21 | $.connection.hub.start().done(function(resp) { 22 | console.log("SignalR Connected"); 23 | console.log(resp); 24 | }).fail(function(err) { 25 | console.log("SignalR Connect ERROR"); 26 | console.log(err); 27 | }); 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /CommanderDemo.Web/src/utility.js: -------------------------------------------------------------------------------- 1 | //HACK: I introduced a bug, this makes it disappear (but you don't get the contact-detail canDeactivate message now) 2 | export function areEqual(obj1, obj2) { 3 | return true;// Object.keys(obj1).every((key) => obj2.hasOwnProperty(key) && (obj1[key] === obj2[key])); 4 | }; -------------------------------------------------------------------------------- /CommanderDemo.Web/src/web-api.js: -------------------------------------------------------------------------------- 1 | import {inject} from 'aurelia-framework'; 2 | import {JsonRpc} from 'json-rpc'; 3 | 4 | function map(contact) { 5 | return { 6 | id: contact.Id, 7 | firstName: contact.FirstName, 8 | lastName: contact.LastName, 9 | email: contact.Email, 10 | phoneNumber: contact.PhoneNumber 11 | } 12 | } 13 | 14 | @inject(JsonRpc) 15 | export class WebAPI { 16 | jsonRpc; 17 | 18 | constructor(jsonRpc) { 19 | this.jsonRpc = jsonRpc; 20 | } 21 | 22 | getContactList() { 23 | this.isRequesting = true; 24 | return new Promise((resolve, reject) => { 25 | this.jsonRpc.send('QueryContacts').then(resp => { 26 | this.isRequesting = false; 27 | resolve(resp.Items.map(map)); 28 | }).catch(error => { 29 | this.isRequesting = false; 30 | console.log("getContactList ERROR", error); 31 | alert(error.message); 32 | reject(error); 33 | }); 34 | }); 35 | } 36 | 37 | getContactDetails(id) { 38 | this.isRequesting = true; 39 | return new Promise((resolve, reject) => { 40 | this.jsonRpc.send('GetContact', {Id:id}).then(resp => { 41 | this.isRequesting = false; 42 | resolve(map(resp)); 43 | }).catch(error => { 44 | this.isRequesting = false; 45 | console.log("getContactDetails ERROR", error); 46 | alert(error.message); 47 | reject(error); 48 | }); 49 | }); 50 | } 51 | 52 | saveContact(contact) { 53 | this.isRequesting = true; 54 | return new Promise((resolve, reject) => { 55 | this.jsonRpc.send('SaveContact', contact).then(id => { 56 | this.jsonRpc.send('GetContact', {Id:id}).then(resp => { 57 | this.isRequesting = false; 58 | resolve(map(resp)); 59 | }); 60 | }).catch(error => { 61 | this.isRequesting = false; 62 | console.log("saveContact ERROR", error); 63 | alert(error.message); 64 | reject(error); 65 | }); 66 | }); 67 | } 68 | 69 | deleteContact(contact) { 70 | this.isRequesting = true; 71 | return new Promise((resolve, reject) => { 72 | this.jsonRpc.send('DeleteContact', {Id:contact.id}).then(resp => { 73 | this.isRequesting = false; 74 | resolve(resp); 75 | }).catch(error => { 76 | this.isRequesting = false; 77 | console.log("deleteContact ERROR", error); 78 | alert(error.message); 79 | reject(error); 80 | }); 81 | }); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /CommanderDemo.Web/styles/styles.css: -------------------------------------------------------------------------------- 1 | .splash { 2 | text-align: center; 3 | margin: 10% 0 0 0; 4 | } 5 | 6 | .splash .message { 7 | font-size: 5em; 8 | line-height: 1.5em; 9 | -webkit-text-shadow: rgba(0, 0, 0, 0.5) 0 0 15px; 10 | text-shadow: rgba(0, 0, 0, 0.5) 0 0 15px; 11 | text-transform: uppercase; 12 | } 13 | 14 | .splash .fa-spinner { 15 | text-align: center; 16 | display: inline-block; 17 | font-size: 5em; 18 | margin-top: 50px; 19 | } 20 | 21 | /*.page-host { 22 | position: absolute; 23 | left: 0; 24 | right: 0; 25 | top: 50px; 26 | bottom: 0; 27 | overflow-x: hidden; 28 | overflow-y: hidden; 29 | }*/ 30 | 31 | body { padding-top: 70px; } 32 | 33 | section { 34 | margin: 0 20px; 35 | } 36 | 37 | a:focus { 38 | outline: none; 39 | } 40 | 41 | .navbar-nav li.loader { 42 | margin: 12px 24px 0 6px; 43 | } 44 | 45 | .no-selection { 46 | margin: 20px; 47 | } 48 | 49 | .contact-list { 50 | overflow-y: auto; 51 | border: 1px solid #ddd; 52 | padding: 10px; 53 | } 54 | 55 | .panel { 56 | margin: 20px; 57 | } 58 | 59 | .button-bar { 60 | right: 0; 61 | left: 0; 62 | bottom: 0; 63 | border-top: 1px solid #ddd; 64 | background: white; 65 | } 66 | 67 | .button-bar > button { 68 | float: right; 69 | margin: 20px; 70 | } 71 | 72 | li.list-group-item { 73 | list-style: none; 74 | } 75 | 76 | li.list-group-item > a { 77 | text-decoration: none; 78 | } 79 | 80 | li.list-group-item.active > a { 81 | color: white; 82 | } 83 | -------------------------------------------------------------------------------- /CommanderDemo.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2013 4 | VisualStudioVersion = 12.0.31101.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommanderDemo.Domain", "CommanderDemo.Domain\CommanderDemo.Domain.csproj", "{2B6AD508-FBFF-46A0-B1FA-2D8967A370FD}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommanderDemo.Test", "CommanderDemo.Test\CommanderDemo.Test.csproj", "{D9F9ECD9-D53C-4E1B-80C8-B2949B1CF6C6}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommanderDemo.Web", "CommanderDemo.Web\CommanderDemo.Web.csproj", "{38444B05-1380-4595-92DA-28083024F3AC}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommanderDemo.Console", "CommanderDemo.Console\CommanderDemo.Console.csproj", "{FA37B34D-EE79-431A-8DF3-0564E7C9DD9A}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8F91F863-9803-449C-A7B9-59D50312C7E7}" 15 | ProjectSection(SolutionItems) = preProject 16 | README.md = README.md 17 | EndProjectSection 18 | EndProject 19 | Global 20 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 21 | Debug|Any CPU = Debug|Any CPU 22 | Release|Any CPU = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 25 | {2B6AD508-FBFF-46A0-B1FA-2D8967A370FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {2B6AD508-FBFF-46A0-B1FA-2D8967A370FD}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {2B6AD508-FBFF-46A0-B1FA-2D8967A370FD}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {2B6AD508-FBFF-46A0-B1FA-2D8967A370FD}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {D9F9ECD9-D53C-4E1B-80C8-B2949B1CF6C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {D9F9ECD9-D53C-4E1B-80C8-B2949B1CF6C6}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {D9F9ECD9-D53C-4E1B-80C8-B2949B1CF6C6}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {D9F9ECD9-D53C-4E1B-80C8-B2949B1CF6C6}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {38444B05-1380-4595-92DA-28083024F3AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {38444B05-1380-4595-92DA-28083024F3AC}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {38444B05-1380-4595-92DA-28083024F3AC}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {38444B05-1380-4595-92DA-28083024F3AC}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {FA37B34D-EE79-431A-8DF3-0564E7C9DD9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {FA37B34D-EE79-431A-8DF3-0564E7C9DD9A}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {FA37B34D-EE79-431A-8DF3-0564E7C9DD9A}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {FA37B34D-EE79-431A-8DF3-0564E7C9DD9A}.Release|Any CPU.Build.0 = Release|Any CPU 41 | EndGlobalSection 42 | GlobalSection(SolutionProperties) = preSolution 43 | HideSolutionNode = FALSE 44 | EndGlobalSection 45 | EndGlobal 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Command-R 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 | 23 | -------------------------------------------------------------------------------- /Presentation/CalcIncomeStmt.cs: -------------------------------------------------------------------------------- 1 | /* 2 | You could easily code generate commands from t-sql Table-Valued Functions. 3 | The Request contains the TVF inputs, the Response is a list of the rows returned. 4 | */ 5 | using System; 6 | using System.Collections.Generic; 7 | using Common; 8 | using MediatR; 9 | 10 | namespace Dashboard 11 | { 12 | public class CalcIncomeStmt : IRequest> 13 | { 14 | public int? UserId { get; set; } 15 | public int? ClientId { get; set; } 16 | public DateTime StartDate { get; set; } 17 | public DateTime EndDate { get; set; } 18 | public int? SheetDim1 { get; set; } 19 | public int? SheetDim2 { get; set; } 20 | public int? SheetDim3 { get; set; } 21 | public int? SheetDim4 { get; set; } 22 | public int? SheetDim5 { get; set; } 23 | public int? CategoryId { get; set; } 24 | public int? LineDim1 { get; set; } 25 | public int? LineDim2 { get; set; } 26 | public int? LineDim3 { get; set; } 27 | public string RowView { get; set; } 28 | public string ColView { get; set; } 29 | 30 | public class Item 31 | { 32 | public int Id { get; set; } 33 | public string WorksheetType { get; set; } 34 | public string AccountType { get; set; } 35 | public string Row { get; set; } 36 | public string Col { get; set; } 37 | public decimal Value { get; set; } 38 | }; 39 | 40 | internal class Handler : IRequestHandler> 41 | { 42 | private readonly IAppContext _appContext; 43 | private readonly DashboardDb _db; 44 | 45 | public Handler(IAppContext appContext, DashboardDb db) 46 | { 47 | _appContext = appContext; 48 | _db = db; 49 | } 50 | 51 | public List Handle(CalcIncomeStmt cmd) 52 | { 53 | if (cmd.UserId == null) 54 | cmd.UserId = _appContext.User.Id; 55 | 56 | if (cmd.ClientId == null) 57 | cmd.ClientId = _appContext.ClientId; 58 | 59 | var sql = @"SELECT * FROM CalcIncomeStmt({0})"; 60 | return _db.Execute(sql, cmd); 61 | } 62 | } 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /Presentation/CommanderDemo.hjt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Command-R/CommanderDemo/a6e72ea9870478dc0db9e3e94ac6d11c83e771e2/Presentation/CommanderDemo.hjt -------------------------------------------------------------------------------- /Presentation/IncomeStmtExcel.cs: -------------------------------------------------------------------------------- 1 | /* 2 | NOTE how the UI would execute IncomeStmtExcel which formats the report into Excel (as 3 | opposed to a PDF or HTML page), which first executes IncomeStmtRpt to build the report model, 4 | but only after calling the CalcIncomeStmt proc command to get the data from the database. Each also 5 | uses CopyTo to pass along all the matching arguments to the other commands. 6 | */ 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using Common; 11 | using MediatR; 12 | 13 | namespace Dashboard 14 | { 15 | public class IncomeStmtExcel : IRequest 16 | { 17 | public int? UserId { get; set; } 18 | public int? ClientId { get; set; } 19 | public DateTime StartDate { get; set; } 20 | public DateTime EndDate { get; set; } 21 | public bool GainLoss { get; set; } 22 | public int? SheetDim1 { get; set; } 23 | public int? SheetDim2 { get; set; } 24 | public int? SheetDim3 { get; set; } 25 | public int? SheetDim4 { get; set; } 26 | public int? SheetDim5 { get; set; } 27 | public int? CategoryId { get; set; } 28 | public int? LineDim1 { get; set; } 29 | public int? LineDim2 { get; set; } 30 | public int? LineDim3 { get; set; } 31 | public string RowView { get; set; } 32 | public string ColView { get; set; } 33 | 34 | internal class Handler : IRequestHandler 35 | { 36 | private readonly IMediator _mediator; 37 | private IncomeStmtRpt.Report _rpt; 38 | private Excel _excel; 39 | 40 | public Handler(IMediator mediator) 41 | { 42 | _mediator = mediator; 43 | } 44 | 45 | public StreamInfo Handle(IncomeStmtExcel cmd) 46 | { 47 | _rpt = _mediator.Send(cmd.CopyTo(new IncomeStmtRpt())); 48 | using (_excel = new Excel()) 49 | { 50 | BuildHeader(); 51 | if (cmd.GainLoss) 52 | { 53 | DisplaySection("GAIN/LOSS", _rpt.GainLosses, _rpt.NetGainLoss); 54 | } 55 | else 56 | { 57 | DisplaySection("INCOME", _rpt.Incomes, _rpt.TotalIncome); 58 | DisplaySection("EXPENSE", _rpt.Expenses, _rpt.TotalExpense); 59 | if (_rpt.OtherIncomes.Count > 0 || _rpt.OtherExpenses.Count > 0) 60 | { 61 | DisplayRow(_rpt.OperatingGainLoss, true); 62 | } 63 | DisplaySection("OTHER INCOME", _rpt.OtherIncomes, _rpt.TotalOtherIncome); 64 | DisplaySection("OTHER EXPENSE", _rpt.OtherExpenses, _rpt.TotalOtherExpense); 65 | DisplayRow(_rpt.NetGainLoss, true); 66 | } 67 | _excel.FormatAll(new Excel.Style { Format = NumberFormat }); 68 | return _excel.GetStreamInfo("IncomeSmtRpt.xlsx"); 69 | } 70 | } 71 | 72 | private void BuildHeader() 73 | { 74 | _excel.NextCol(" "); 75 | foreach (var col in _rpt.Columns) 76 | { 77 | _excel.NextCol(col + "\nActual"); 78 | } 79 | if (_rpt.ShowMonthlyBudget) 80 | { 81 | _excel.NextCol(_rpt.Columns.LastOrDefault() + "\nBudget"); 82 | _excel.NextCol("Monthly\nVariance"); 83 | } 84 | _excel.NextCol(" "); 85 | _excel.NextCol("Total\nActual"); 86 | _excel.NextCol("Total\nBudget"); 87 | _excel.NextCol("Total\nVariance"); 88 | _excel.FormatRow(new Excel.Style { Bold = true }); 89 | } 90 | 91 | private void DisplaySection(string title, List rows, IncomeStmtRpt.Row total) 92 | { 93 | if (rows.Count == 0) 94 | return; 95 | 96 | _excel.NextRow(); 97 | _excel.NextCol(title); 98 | _excel.FormatRow(new Excel.Style { Bold = true, Merge = true }, _rpt.Columns.Count + 7); 99 | 100 | foreach (var row in rows) 101 | { 102 | DisplayRow(row, false); 103 | } 104 | DisplayRow(total, true); 105 | } 106 | 107 | private void DisplayRow(IncomeStmtRpt.Row row, bool header) 108 | { 109 | _excel.NextRow(); 110 | _excel.NextCol(row.Text); 111 | foreach (var value in row.Values) 112 | { 113 | _excel.NextCol(value); 114 | } 115 | if (_rpt.ShowMonthlyBudget) 116 | { 117 | _excel.NextCol(row.MonthlyBudget); 118 | _excel.NextCol(row.MonthlyVariance); 119 | } 120 | _excel.NextCol(" "); 121 | _excel.NextCol(row.TotalActual); 122 | _excel.NextCol(row.TotalBudget); 123 | _excel.NextCol(row.TotalVariance); 124 | 125 | if (header) 126 | _excel.FormatRow(new Excel.Style { Bold = true }); 127 | } 128 | 129 | private const string NumberFormat = @"_(* #,##0_);_(* -#,##0_);_(* "" - ""_);_(@_)"; 130 | }; 131 | }; 132 | } 133 | -------------------------------------------------------------------------------- /Presentation/IncomeStmtRpt.cs: -------------------------------------------------------------------------------- 1 | /* 2 | NOTE how the IncomeStmtRpt uses MediatR to the CalcIncomeStmt proc 3 | command to retrieve the data. 4 | */ 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using Common; 9 | using MediatR; 10 | 11 | namespace Dashboard 12 | { 13 | public class IncomeStmtRpt : IRequest 14 | { 15 | public int? UserId { get; set; } 16 | public int? ClientId { get; set; } 17 | public DateTime StartDate { get; set; } 18 | public DateTime EndDate { get; set; } 19 | public bool GainLoss { get; set; } 20 | public int? SheetDim1 { get; set; } 21 | public int? SheetDim2 { get; set; } 22 | public int? SheetDim3 { get; set; } 23 | public int? SheetDim4 { get; set; } 24 | public int? SheetDim5 { get; set; } 25 | public int? CategoryId { get; set; } 26 | public int? LineDim1 { get; set; } 27 | public int? LineDim2 { get; set; } 28 | public int? LineDim3 { get; set; } 29 | public string RowView { get; set; } 30 | public string ColView { get; set; } 31 | public string RowLink { get; set; } 32 | public string ValueLink { get; set; } 33 | 34 | public class Report 35 | { 36 | public string RowView { get; set; } 37 | public string ColView { get; set; } 38 | public string RowLink { get; set; } 39 | public string ValueLink { get; set; } 40 | public List Columns { get; set; } 41 | public List Incomes { get; set; } 42 | public Row TotalIncome { get; set; } 43 | public List Expenses { get; set; } 44 | public Row TotalExpense { get; set; } 45 | public Row OperatingGainLoss { get; set; } 46 | public List OtherIncomes { get; set; } 47 | public Row TotalOtherIncome { get; set; } 48 | public List OtherExpenses { get; set; } 49 | public Row TotalOtherExpense { get; set; } 50 | public Row NetGainLoss { get; set; } 51 | public List GainLosses { get; set; } 52 | 53 | public bool ShowMonthlyBudget 54 | { 55 | get { return ColView == "Month"; } 56 | } 57 | 58 | public string Fmt(decimal? num) 59 | { 60 | return num == null || num == 0m ? null : num.ToString("#,##0"); 61 | } 62 | 63 | public string GetValueLink(Row row, Ext.ForEachItem value) 64 | { 65 | if (row == null || value.Item == 0m) 66 | return null; 67 | 68 | if (ValueLink == null) 69 | return Fmt(value.Item); 70 | 71 | var url = string.Format(ValueLink, row.Id, Columns[value.Index]); 72 | return string.Format("{1}", 73 | url, Fmt(value.Item)); 74 | } 75 | }; 76 | 77 | public class Row 78 | { 79 | public int Id { get; set; } 80 | public string Text { get; set; } 81 | public List Values { get; set; } 82 | public decimal MonthlyBudget { get; set; } 83 | public decimal MonthlyVariance { get; set; } 84 | public decimal TotalActual { get; set; } 85 | public decimal TotalBudget { get; set; } 86 | public decimal TotalVariance { get; set; } 87 | }; 88 | 89 | internal class Handler : IRequestHandler 90 | { 91 | private readonly IAppContext _appContext; 92 | private readonly IMediator _mediator; 93 | private List _rows; 94 | private List _columns; 95 | 96 | public Handler(IAppContext appContext, IMediator mediator) 97 | { 98 | _appContext = appContext; 99 | _mediator = mediator; 100 | } 101 | 102 | public Report Handle(IncomeStmtRpt request) 103 | { 104 | if (request.UserId == null) 105 | request.UserId = _appContext.User.Id; 106 | 107 | if (request.ClientId == null) 108 | request.ClientId = _appContext.ClientId; 109 | 110 | _rows = _mediator.Send(request.CopyTo(new CalcIncomeStmt())); 111 | _columns = _rows.Where(x => x.WorksheetType == "Columns").Select(x => x.Col).ToList(); 112 | 113 | var report = new Report 114 | { 115 | RowView = request.RowView, 116 | ColView = request.ColView, 117 | RowLink = request.RowLink, 118 | ValueLink = request.ValueLink, 119 | Columns = _columns, 120 | Incomes = new List(), 121 | TotalIncome = NewRow(0, "Total Income"), 122 | Expenses = new List(), 123 | TotalExpense = NewRow(0, "Total Expense"), 124 | OperatingGainLoss = NewRow(0, "Operating Gain/Loss"), 125 | OtherIncomes = new List(), 126 | TotalOtherIncome = NewRow(0, "Total Other Income"), 127 | OtherExpenses = new List(), 128 | TotalOtherExpense = NewRow(0, "Total Other Expense"), 129 | NetGainLoss = NewRow(0, "Net Gain/Loss"), 130 | GainLosses = new List(), 131 | }; 132 | 133 | if (request.GainLoss) 134 | { 135 | ComputeGainLossRows(report.GainLosses, report.NetGainLoss); 136 | } 137 | else 138 | { 139 | ComputeRows(Account.INCOME, report.Incomes, report.TotalIncome); 140 | ComputeRows(Account.EXPENSE, report.Expenses, report.TotalExpense); 141 | ComputeDifference(NewRow(0, ""), report.TotalIncome, report.TotalExpense, report.OperatingGainLoss); 142 | ComputeRows(Account.OTHER_INCOME, report.OtherIncomes, report.TotalOtherIncome); 143 | ComputeRows(Account.OTHER_EXPENSE, report.OtherExpenses, report.TotalOtherExpense); 144 | ComputeDifference(report.OperatingGainLoss, report.TotalOtherIncome, report.TotalOtherExpense, report.NetGainLoss); 145 | } 146 | 147 | return report; 148 | } 149 | 150 | private void ComputeRows(string type, List section, Row total) 151 | { 152 | var accounts = _rows.Where(x => x.WorksheetType == Worksheet.ACTUAL && x.AccountType == type) 153 | .Select(x => new {x.Id, x.Row}) 154 | .Distinct(); 155 | 156 | foreach (var account in accounts) 157 | { 158 | var row = NewRow(account.Id, account.Row); 159 | var budgets = _rows.Where(x => x.WorksheetType == Worksheet.BUDGET 160 | && x.AccountType == type && x.Id == row.Id) 161 | .ToList(); 162 | 163 | foreach (var col in _columns.ToFor()) 164 | { 165 | var actual = _rows.Where(x => x.WorksheetType == Worksheet.ACTUAL 166 | && x.AccountType == type && x.Id == row.Id && 167 | x.Col == col.Item) 168 | .Select(x => (decimal?)x.Value) 169 | .SingleOrDefault() ?? 0; 170 | 171 | if (col.Last) 172 | { 173 | var budget = budgets.Where(x => x.Col == col.Item) 174 | .Select(x => (decimal?)x.Value) 175 | .SingleOrDefault() ?? 0; 176 | 177 | row.MonthlyBudget = budget; 178 | row.MonthlyVariance = budget - actual; 179 | } 180 | 181 | row.Values[col.Index] = actual; 182 | total.Values[col.Index] += actual; 183 | } 184 | 185 | row.TotalActual = row.Values.Sum(); 186 | row.TotalBudget = budgets.Sum(x => x.Value); 187 | row.TotalVariance = row.TotalActual - row.TotalBudget; 188 | 189 | total.MonthlyBudget += row.MonthlyBudget; 190 | total.MonthlyVariance += row.MonthlyVariance; 191 | total.TotalActual += row.TotalActual; 192 | total.TotalBudget += row.TotalBudget; 193 | total.TotalVariance += row.TotalVariance; 194 | 195 | section.Add(row); 196 | } 197 | } 198 | 199 | private void ComputeDifference(Row init, Row income, Row expense, Row gainLoss) 200 | { 201 | foreach (var col in _columns.ToFor()) 202 | { 203 | gainLoss.Values[col.Index] = init.Values[col.Index] + income.Values[col.Index] - expense.Values[col.Index]; 204 | } 205 | 206 | gainLoss.MonthlyBudget = init.MonthlyBudget + income.MonthlyBudget - expense.MonthlyBudget; 207 | gainLoss.MonthlyVariance = init.MonthlyVariance + income.MonthlyVariance - expense.MonthlyVariance; 208 | gainLoss.TotalActual = init.TotalActual + income.TotalActual - expense.TotalActual; 209 | gainLoss.TotalBudget = init.TotalBudget + income.TotalBudget - expense.TotalBudget; 210 | gainLoss.TotalVariance = init.TotalVariance + income.TotalVariance - expense.TotalVariance; 211 | } 212 | 213 | private void ComputeGainLossRows(List section, Row total) 214 | { 215 | var accounts = _rows.Where(x => x.WorksheetType == Worksheet.ACTUAL) 216 | .Select(x => new { x.Id, x.Row }) 217 | .Distinct(); 218 | 219 | foreach (var account in accounts) 220 | { 221 | var row = NewRow(account.Id, account.Row); 222 | var incomeBudgets = _rows.Where(x => x.WorksheetType == Worksheet.BUDGET && x.Id == row.Id 223 | && (x.AccountType == Account.INCOME || x.AccountType == Account.OTHER_INCOME)) 224 | .ToList(); 225 | var expenseBudgets = _rows.Where(x => x.WorksheetType == Worksheet.BUDGET && x.Id == row.Id 226 | && (x.AccountType == Account.EXPENSE || x.AccountType == Account.OTHER_EXPENSE)) 227 | .ToList(); 228 | 229 | foreach (var col in _columns.ToFor()) 230 | { 231 | var incomeActual = _rows.Where(x => x.WorksheetType == Worksheet.ACTUAL && x.Id == row.Id 232 | && (x.AccountType == Account.INCOME || x.AccountType == Account.OTHER_INCOME) 233 | && x.Col == col.Item) 234 | .Select(x => (decimal?)x.Value) 235 | .SingleOrDefault() ?? 0; 236 | 237 | var expenseActual = _rows.Where(x => x.WorksheetType == Worksheet.ACTUAL && x.Id == row.Id 238 | && (x.AccountType == Account.EXPENSE || x.AccountType == Account.OTHER_EXPENSE) 239 | && x.Col == col.Item) 240 | .Select(x => (decimal?)x.Value) 241 | .SingleOrDefault() ?? 0; 242 | var actual = incomeActual - expenseActual; 243 | 244 | if (col.Last) 245 | { 246 | var incomeBudget = incomeBudgets.Where(x => x.Col == col.Item) 247 | .Sum(x => (decimal?)x.Value) ?? 0; 248 | var expenseBudget = expenseBudgets.Where(x => x.Col == col.Item) 249 | .Sum(x => (decimal?)x.Value) ?? 0; 250 | 251 | row.MonthlyBudget = incomeBudget - expenseBudget; 252 | row.MonthlyVariance = row.MonthlyBudget - actual; 253 | } 254 | 255 | row.Values[col.Index] = actual; 256 | total.Values[col.Index] += actual; 257 | } 258 | 259 | row.TotalActual = row.Values.Sum(); 260 | row.TotalBudget = incomeBudgets.Sum(x => x.Value) - expenseBudgets.Sum(x => x.Value); 261 | row.TotalVariance = row.TotalActual - row.TotalBudget; 262 | 263 | total.MonthlyBudget += row.MonthlyBudget; 264 | total.MonthlyVariance += row.MonthlyVariance; 265 | total.TotalActual += row.TotalActual; 266 | total.TotalBudget += row.TotalBudget; 267 | total.TotalVariance += row.TotalVariance; 268 | 269 | section.Add(row); 270 | } 271 | } 272 | 273 | private Row NewRow(int id, string text) 274 | { 275 | return new Row 276 | { 277 | Id = id, 278 | Text = text, 279 | Values = new List(new decimal[_columns.Count]), 280 | }; 281 | } 282 | }; 283 | }; 284 | } 285 | -------------------------------------------------------------------------------- /Presentation/Mediator.linq: -------------------------------------------------------------------------------- 1 | 2 | MediatR 3 | SimpleInjector 4 | MediatR 5 | SimpleInjector 6 | SimpleInjector.Extensions 7 | 8 | 9 | void Main() { 10 | var mediator = CreateMediator(); 11 | var response = mediator.Send(new Ping { Name = "Paul" }); 12 | Console.WriteLine(response.Message); 13 | mediator.Publish(new OrderCreated { Id=1 }); 14 | } 15 | 16 | public class Ping : IRequest 17 | { 18 | public string Name { get; set; } 19 | 20 | public class Pong 21 | { 22 | public string Message { get; set; } 23 | } 24 | 25 | internal class PingHandler : IRequestHandler { 26 | private readonly MessageService _messageService; 27 | 28 | public PingHandler(MessageService messageService) { 29 | _messageService = messageService; 30 | } 31 | 32 | public Pong Handle(Ping request) { 33 | return new Pong { 34 | Message = "Pong: " + _messageService.GetMessage() + request.Name 35 | }; 36 | } 37 | } 38 | } 39 | 40 | public class OrderCreated : INotification { 41 | public int Id { get; set; } 42 | } 43 | 44 | internal class OrderCreatedHandler1 : INotificationHandler { 45 | public void Handle(OrderCreated message) { 46 | Console.WriteLine("OrderCreatedHandler1: Handled Order #" + message.Id); 47 | } 48 | } 49 | 50 | internal class OrderCreatedHandler2 : INotificationHandler { 51 | public void Handle(OrderCreated message) { 52 | Console.WriteLine("OrderCreatedHandler2: Handled Order #" + message.Id); 53 | } 54 | } 55 | 56 | internal class MessageService { 57 | public string GetMessage() { 58 | return "MessageService: Hello "; 59 | } 60 | } 61 | 62 | internal class RequestLoggingHandler : IRequestHandler where TReq:IRequest { 63 | private readonly IRequestHandler _inner; 64 | public RequestLoggingHandler(IRequestHandler inner) { 65 | _inner = inner; 66 | } 67 | public TResp Handle(TReq request) { 68 | Console.WriteLine("RequestLoggingHandler: " + request.GetType().Name); 69 | return _inner.Handle(request); 70 | } 71 | } 72 | 73 | internal class NotificationLoggingHandler : INotificationHandler where T:INotification { 74 | private readonly INotificationHandler _inner; 75 | public NotificationLoggingHandler(INotificationHandler inner) { 76 | _inner = inner; 77 | } 78 | public void Handle(T msg) { 79 | Console.WriteLine("NotificationLoggingHandler: " + msg.GetType()); 80 | _inner.Handle(msg); 81 | } 82 | } 83 | 84 | static IMediator CreateMediator() { 85 | var assemblies = new[] { Assembly.GetExecutingAssembly() }; 86 | var container = new Container(); 87 | container.RegisterSingle(() => new Mediator(container.GetInstance, container.GetAllInstances)); 88 | 89 | container.RegisterManyForOpenGeneric(typeof(IRequestHandler<,>), assemblies); 90 | container.RegisterManyForOpenGeneric(typeof(IAsyncRequestHandler<,>), assemblies); 91 | container.RegisterManyForOpenGeneric(typeof(INotificationHandler<>), container.RegisterAll, assemblies); 92 | container.RegisterManyForOpenGeneric(typeof(IAsyncNotificationHandler<>), container.RegisterAll, assemblies); 93 | 94 | container.RegisterDecorator(typeof(IRequestHandler<,>), typeof(RequestLoggingHandler<,>)); 95 | container.RegisterDecorator(typeof(INotificationHandler<>), typeof(NotificationLoggingHandler<>)); 96 | return container.GetInstance(); 97 | } -------------------------------------------------------------------------------- /Presentation/Nancy_Akka_Mediator.linq: -------------------------------------------------------------------------------- 1 | 2 | Akka 3 | Akka.DI.Core 4 | Nancy 5 | Nancy.Hosting.Self 6 | Nancy.Serialization.JsonNet 7 | SimpleInjector 8 | Akka 9 | Akka.Actor 10 | Akka.DI.Core 11 | CommandR 12 | MediatR 13 | Nancy 14 | Nancy.Extensions 15 | Nancy.Hosting.Self 16 | Nancy.ModelBinding 17 | Nancy.Serialization.JsonNet 18 | Nancy.TinyIoc 19 | Newtonsoft.Json 20 | SimpleInjector 21 | SimpleInjector.Extensions 22 | System.Threading.Tasks 23 | 24 | 25 | //Server 26 | void Main() { 27 | using (var host = new NancyHost(new Uri("http://localhost:8080"), new LinqpadNancyBootstrapper())) { 28 | host.Start(); 29 | Console.ReadLine(); 30 | } 31 | } 32 | 33 | //Nancy 34 | public class HelloModule : NancyModule { 35 | private Mediator _mediator; 36 | public HelloModule() { 37 | var container = new Container(); 38 | var system = ActorSystem.Create("JsonRpcActors"); 39 | _mediator = new Mediator(container, system); 40 | container.RegisterSingle(_mediator); 41 | 42 | Get["/"] = _ => "POST to /jsonrpc"; 43 | Post["/jsonrpc"] = JsonRpc; 44 | } 45 | private dynamic JsonRpc(dynamic args) { 46 | var request = JsonConvert.DeserializeObject(Request.Body.AsString()); 47 | var response = _mediator.SendAsync(request).Result; 48 | return response; 49 | } 50 | }; 51 | 52 | //Command 53 | public class Ping : IAsyncRequest { 54 | public string Name { get; set; } 55 | public class Pong { 56 | public string Message { get; set; } 57 | }; 58 | internal class Handler : TypedActor, IAsyncRequestHandler { 59 | private TestService _service; 60 | public Handler(TestService service) { 61 | _service = service; 62 | } 63 | public Task Handle(Ping cmd) { 64 | var response = new Pong { 65 | Message = _service.Message + cmd.Name, 66 | }; 67 | Sender.Tell(response); 68 | return null; 69 | } 70 | }; 71 | }; 72 | 73 | //Service 74 | internal class TestService { 75 | public string Message { get { return "Test: "; } } 76 | }; 77 | 78 | //Akka Mediator 79 | public class Mediator { 80 | private SimpleInjectorDependencyResolver _resolver; 81 | private ActorSystem _system; 82 | public Mediator(Container container, ActorSystem system) { 83 | _resolver = new SimpleInjectorDependencyResolver(container, system); 84 | _system = system; 85 | } 86 | public async Task SendAsync(JsonRpcRequest request) { 87 | try { 88 | var cmdType = Type.GetType("UserQuery+" + request.method, true); 89 | var cmd = request.@params.ToObject(cmdType); 90 | var handler = Type.GetType(cmdType + "+Handler"); 91 | var actor = _system.ActorOf(_resolver.Create(handler)); 92 | var result = await actor.Ask(cmd); 93 | return new JsonRpcResponse { 94 | result = result 95 | }; 96 | } catch (Exception ex) { 97 | return new JsonRpcResponse { 98 | error = new JsonRpcError { 99 | message = ex.ToString() 100 | } 101 | }; 102 | } 103 | } 104 | }; 105 | 106 | //REF: http://getakka.net/docs/DI%20Core 107 | public class SimpleInjectorDependencyResolver : IDependencyResolver { 108 | private Container _container; 109 | private ActorSystem _system; 110 | public SimpleInjectorDependencyResolver(Container container, ActorSystem system) { 111 | _container = container; 112 | _system = system; 113 | _system.AddDependencyResolver(this); 114 | } 115 | public Type GetType(string actorName) { 116 | return null; 117 | } 118 | public Func CreateActorFactory(Type actorType) { 119 | return () => (ActorBase)_container.GetInstance(actorType); 120 | } 121 | public Props Create() where TActor : ActorBase { 122 | return Create(typeof(TActor)); 123 | } 124 | public Props Create(Type actorType) { 125 | return _system.GetExtension().Props(actorType); 126 | } 127 | public void Release(ActorBase actor) { 128 | //this.container.Release(actor); 129 | } 130 | }; 131 | 132 | public class LinqpadNancyBootstrapper : DefaultNancyBootstrapper { 133 | protected override void ConfigureApplicationContainer(TinyIoCContainer container) { 134 | } 135 | }; -------------------------------------------------------------------------------- /Presentation/Owin_Akka_Mediator.linq: -------------------------------------------------------------------------------- 1 | 2 | Akka 3 | Akka.DI.Core 4 | Command-R 5 | Microsoft.Owin.SelfHost 6 | Newtonsoft.Json 7 | SimpleInjector 8 | Akka 9 | Akka.Actor 10 | Akka.DI.Core 11 | CommandR 12 | MediatR 13 | Newtonsoft.Json 14 | Newtonsoft.Json.Linq 15 | Owin 16 | SimpleInjector 17 | SimpleInjector.Extensions 18 | System.Threading.Tasks 19 | 20 | 21 | //Server 22 | void Main() { 23 | //netsh http add urlacl "http://+:8080/" user=Everyone listen=yes 24 | using (Microsoft.Owin.Hosting.WebApp.Start("http://+:8080/")) { 25 | Console.WriteLine("Listening on port 8080 Press [enter] to quit..."); 26 | Console.ReadLine(); 27 | } 28 | } 29 | 30 | //Owin 31 | public class Startup { 32 | public void Configuration(IAppBuilder app) { 33 | var container = new Container(); 34 | var system = ActorSystem.Create("JsonRpcActors"); 35 | var mediator = new Mediator(container, system); 36 | container.RegisterSingle(mediator); 37 | 38 | app.Run(context => { 39 | if (context.Request.Method == "POST") { 40 | using (var streamReader = new StreamReader(context.Request.Body)) { 41 | var request = (JsonRpcRequest)new JsonSerializer().Deserialize(streamReader, typeof(JsonRpcRequest)); 42 | var response = mediator.SendAsync(request).Result; 43 | context.Response.ContentType = "application/json"; 44 | return context.Response.WriteAsync(JsonConvert.SerializeObject(response)); 45 | } 46 | } else { 47 | context.Response.ContentType = "text/plain"; 48 | return context.Response.WriteAsync("POST to /jsonrpc"); 49 | } 50 | }); 51 | } 52 | }; 53 | 54 | //Command 55 | public class Ping : IAsyncRequest { 56 | public string Name { get; set; } 57 | public class Pong { 58 | public string Message { get; set; } 59 | }; 60 | internal class Handler : TypedActor, IAsyncRequestHandler { 61 | private TestService _service; 62 | public Handler(TestService service) { 63 | _service = service; 64 | } 65 | public Task Handle(Ping cmd) { 66 | var response = new Pong { 67 | Message = _service.Message + cmd.Name, 68 | }; 69 | Sender.Tell(response); 70 | return null; 71 | } 72 | }; 73 | }; 74 | 75 | //Service 76 | internal class TestService { 77 | public string Message { get { return "Test: "; } } 78 | } 79 | 80 | //Akka Mediator 81 | public class Mediator { 82 | private SimpleInjectorDependencyResolver _resolver; 83 | private ActorSystem _system; 84 | public Mediator(Container container, ActorSystem system) { 85 | _resolver = new SimpleInjectorDependencyResolver(container, system); 86 | _system = system; 87 | } 88 | public async Task SendAsync(JsonRpcRequest request) { 89 | try { 90 | var cmdType = Type.GetType("UserQuery+" + request.method, true); 91 | var cmd = request.@params.ToObject(cmdType); 92 | var handler = Type.GetType(cmdType + "+Handler"); 93 | var actor = _system.ActorOf(_resolver.Create(handler)); 94 | var result = await actor.Ask(cmd); 95 | return new JsonRpcResponse { 96 | result = result 97 | }; 98 | } catch (Exception ex) { 99 | return new JsonRpcResponse { 100 | error = new JsonRpcError { 101 | message = ex.ToString() 102 | } 103 | }; 104 | } 105 | } 106 | }; 107 | 108 | //REF: http://getakka.net/docs/DI%20Core 109 | public class SimpleInjectorDependencyResolver : IDependencyResolver { 110 | private Container _container; 111 | private ActorSystem _system; 112 | public SimpleInjectorDependencyResolver(Container container, ActorSystem system) { 113 | _container = container; 114 | _system = system; 115 | _system.AddDependencyResolver(this); 116 | } 117 | public Type GetType(string actorName) { 118 | return null; 119 | } 120 | public Func CreateActorFactory(Type actorType) { 121 | return () => (ActorBase)_container.GetInstance(actorType); 122 | } 123 | public Props Create() where TActor : ActorBase { 124 | return Create(typeof(TActor)); 125 | } 126 | public Props Create(Type actorType) { 127 | return _system.GetExtension().Props(actorType); 128 | } 129 | public void Release(ActorBase actor) { 130 | //this.container.Release(actor); 131 | } 132 | }; -------------------------------------------------------------------------------- /Presentation/Presentation.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Command and Conquer 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |

Command and Conquer

19 |
20 | Paul Wheeler
21 | Solutions Architect @ Martus Solutions 22 |
23 |
24 | Paul@PaulWheeler.com
25 | @PaulWheeler 26 |
27 |
28 |
29 | 35 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Presentation/SaveContact.ps1: -------------------------------------------------------------------------------- 1 | cls 2 | $uri = "http://localhost:51460/jsonrpc" 3 | 4 | Function Main() { 5 | $login = New-Object PSObject -Property @{ 6 | Username = "Admin"; 7 | Password = "password"; 8 | } 9 | $tokenId = JsonRpc $uri null "LoginUser" $login 10 | 11 | $guid = [guid]::NewGuid().ToString("N").Substring(10) 12 | $saveContact = New-Object PSObject -Property @{ 13 | FirstName = "Test"; 14 | LastName = $guid; 15 | Email = "test@example.com"; 16 | PhoneNumber = "5551212"; 17 | } 18 | $id = JsonRpc $uri $tokenId "SaveContact" $saveContact 19 | Write-Host $id 20 | } 21 | 22 | Function JsonRpc([String] $uri, [String] $tokenId, [String] $method, [PSObject] $params) { 23 | $json = New-Object PSObject -Property @{method=$method; params=$params} | ConvertTo-Json 24 | $response = (Invoke-WebRequest -uri $uri -Headers @{"Authorization"=$tokenId} -Method POST -Body $json).Content | ConvertFrom-Json 25 | if ($response.result -ne $null) { return $response.result } 26 | else { throw $response.error.message } 27 | } 28 | 29 | Main 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Command and Conquer 2 | 3 | This is the demo application I showed off at the Charlotte Alt.net meeting on 7/15/2015. It explores how the command pattern and the Command-R projects can be used in an application. Some of my notes from the talk are below along with instructions on running the app. Feel free to open issues with any bugs or hit me up on twitter [@PaulWheeler](https://twitter.com/PaulWheeler). I'm thinking about creating another demo that does not contain the MVC part and is just an Aurelia.js SPA, and possibly an Angular.js version. Let me know if there is interest in either. 4 | 5 | ## Startup instructions 6 | 7 | * Visual Studio will need to restore all the nuget packages 8 | 9 | * You will need a .\SqlExpress Sql Server database, or configure a ConnectionString in the Web.config as below 10 | 11 | ``` 12 | 13 | 14 | 15 | ``` 16 | 17 | * Follow [these steps](https://github.com/aurelia/app-contacts) in the CommanderDemo.Web folder to get Aurelia working. This will create \jspm_packages\ and \node_modules\ folders. Don't forget to run gulp build after making any changes to the \src\*.js files. You can use the Visual Studio extension [Task Runner Explorer](https://visualstudiogallery.msdn.microsoft.com/8e1b4368-4afb-467a-bc13-9650572db708) to automatically run gulp after a VS build. 18 | 19 | * If you want to turn on the MongoQueueService or AuditService you'll need to [download MongoDb](http://www.mongodb.org/downloads) then [install it](http://docs.mongodb.org/manual/tutorial/install-mongodb-on-windows/). You can create a "start.bat" file like below in the \mongodb\bin\ folder to start it up. 20 | 21 | ``` 22 | SET Db_Folder=.\db 23 | IF NOT EXIST "%Db_Folder%" MKDIR "%Db_Folder%" 24 | mongod.exe --dbpath "%Db_Folder%" 25 | PAUSE 26 | ``` 27 | 28 | * You can enable the optional services by setting IsDisabled to "false" in the web.config (eg key="TaskRegistry+Settings.IsDisabled" value="false") 29 | 30 | ## Command-R Projects 31 | 32 | * **Command-R** - Base Nuget package that can deserialize a JsonRpcRequest string (provided via any mechanism), and pass the IRequest or IAsyncRequest along to MediatR to be executed, then serialize back the JsonRpcResponse or JsonRpcError. 33 | 34 | * **Command-R.WebApi** - Provides a JsonRpcController using WebAPI to provide a single /jsonrpc endpoint. 35 | 36 | * **Command-R.MongoQueue** - A queue implemented using MongoDb tailable cursors (thanks Matt) that can be used to process tasks in the background or by services running on other servers. 37 | 38 | * **CfgDotNet** - Allows you to store your application configuration data in cfg.json files, and supports multiple environments (eg local, dev, qa, uat, prod). Also contains SettingsManager which enables strongly-typed settings. 39 | 40 | ## What is the Command Pattern? 41 | 42 | > In object-oriented programming, the command pattern is a behavioral design pattern in which an object is used to *encapsulate all information needed to perform an action* or trigger an event at a later time. This information includes the method name, the object that owns the method and values for the method parameters. 43 | 44 | [https://en.wikipedia.org/?title=Command_pattern](https://en.wikipedia.org/?title=Command_pattern) 45 | 46 | The focus of the Command-R project is to make it easy to structure applications around the command pattern. A simple example command might look like this: 47 | 48 | ``` 49 | public class Ping : IRequest 50 | { 51 | public string Name { get; set; } 52 | 53 | public class Pong 54 | { 55 | public string Message { get; set; } 56 | } 57 | 58 | internal class Handler : IRequestHandler 59 | { 60 | public Pong Handle(Ping request) 61 | { 62 | return new Pong { 63 | Message = "Ping: " + request.Name 64 | }; 65 | } 66 | } 67 | } 68 | ``` 69 | 70 | Command-R allows us to execute these commands with [MediatR](https://github.com/jbogard/MediatR) via the [JSON-RPC 2.0 specification](http://www.jsonrpc.org/specification). Command-R has two minor extensions to the JSON-RPC spec. 1) You can upload files who paths on the server are mapped to matching property names on the command. 2) Added the IPatchable interface which sends along a list of which properties existed in the json params request dictionary that was sent. 71 | ## Mediator Pattern 72 | 73 | > In Software Engineering, the mediator pattern defines an object that encapsulates how a set of objects interact. This pattern is considered to be a behavioral pattern due to the way it can alter the program's running behavior. 74 | 75 | [https://en.wikipedia.org/wiki/Mediator_pattern](https://en.wikipedia.org/wiki/Mediator_pattern) 76 | 77 | Jimmy Bogard's MediatR project uses .Net generics along with your choice of IoC container to execute requests, which have a single handler and return a response, and notifications which can have many handlers but no response. 78 | 79 | ``` 80 | //Request with string response 81 | public class Ping : IRequest {} 82 | 83 | //Handler 84 | public class PingHandler : IRequestHandler { 85 | public string Handle(Ping request) { 86 | return "Pong"; 87 | } 88 | } 89 | ``` 90 | 91 | The one model in, one model out pattern greatly simplifies conceptualizing the system and realizing more powerful patterns. The single Handler interface method represents the ability to take an input model, perform work, and return an output model. 92 | 93 | Besides the handlers for specific requests, you can define generic handlers will will be run for all requests. These should be registered explicitly in the container using open generics (eg map RequestLoggingHandler<,> to IRequestHandler<,>) since the order they are added matters (last one runs first). 94 | 95 | ``` 96 | public class RequestLoggingHandler : IRequestHandler where TReq:IRequest 97 | { 98 | private readonly IRequestHandler _inner; 99 | 100 | public RequestLoggingHandler(IRequestHandler inner) { 101 | _inner = inner; 102 | } 103 | 104 | public TResp Handle(TReq request) { 105 | Console.WriteLine("RequestLoggingHandler (before): " + request.GetType().Name); 106 | var response = _inner.Handle(request); 107 | Console.WriteLine("RequestLoggingHandler (after): " + request.GetType().Name); 108 | return response; 109 | } 110 | } 111 | 112 | //container.RegisterDecorator(typeof(IRequestHandler<,>), typeof(RequestLoggingHandler<,>)); 113 | ``` 114 | 115 | Since the request handlers all come from the container, any dependencies will automatically be injected. This allows you to build sophisticated commands that need to access external resources (database, services, etc). 116 | 117 | ``` 118 | public class Ping : IRequest 119 | { 120 | public string Name { get; set; } 121 | 122 | public class Pong { 123 | public string Message { get; set; } 124 | } 125 | 126 | internal class Handler : IRequestHandler 127 | { 128 | private readonly MessageService _messageService; 129 | 130 | public Handler(MessageService messageService) { 131 | _messageService = messageService; 132 | } 133 | 134 | public Pong Handle(Ping request) { 135 | return new Pong { 136 | Message = _messageService.GetMessage() + "Ping: " + request.Name 137 | }; 138 | } 139 | } 140 | } 141 | 142 | internal class MessageService 143 | { 144 | public string GetMessage() { 145 | return "MessageService: "; 146 | } 147 | } 148 | 149 | ``` 150 | 151 | ## Strongly-typed Settings 152 | 153 | One of the new features of Asp.net 5 is that it allows you to define your own [strongly-typed AppSettings classes](https://weblog.west-wind.com/posts/2015/Jun/03/Strongly-typed-AppSettings-Configuration-in-ASPNET-5). These settings values can be loaded via various sources and then injected into the classes where needed via an IoC container. 154 | 155 | The CfgDotNet project allows a similar mechanism with its SettingsManager class. The SettingsManager will scan all requsted assemblies for any ISettings present, load their values from the configured providers, then inject them into the IoC container. 156 | 157 | It supports various sources for settings: 158 | * AppSettings 159 | * ConnectionStrings 160 | * cfg.json 161 | * Database 162 | * Any other ISettingsProvider you want to write 163 | 164 | In addition, the optional ISetting.Validate method allows each setting to verify it is configured correctly and any external resources (database, folders, etc) are available. Any exceptions thrown by a validation causes the application to fail fast on startup with clear diagnostic info. 165 | 166 | ``` 167 | const string environment = "prod"; 168 | const string cfgPath = "cfg.json"; 169 | var assemblies = new[] { GetType().Assembly }; 170 | 171 | new SettingsManager() 172 | .AddProvider(new ConnectionStringsSettingsProvider()) 173 | .AddProvider(new AppSettingsSettingsProvider()) 174 | .AddProvider(new CfgDotNetSettingsProvider(environment, cfgPath)) 175 | .AddSettings(assemblies) 176 | .AddProvider(x => new SqlDatabaseSettingsProvider(x.ConnectionString)) 177 | .LoadSettingsFromProviders() 178 | .Validate() 179 | .ForEach(x => Container.RegisterSingle(x.GetType(), x)); 180 | ``` 181 | 182 | ## Additional Reading 183 | 184 | * [MediatR Project](https://github.com/jbogard/MediatR) 185 | 186 | * [LINQPad](https://www.linqpad.net/) 187 | 188 | * [JSON-RPC 2.0 Specifiation](http://www.jsonrpc.org/specification) 189 | 190 | * [Tackling cross-cutting concerns with a mediator pipeline](https://lostechies.com/jimmybogard/2014/09/09/tackling-cross-cutting-concerns-with-a-mediator-pipeline/) 191 | 192 | * [Analyzing a DDD application](http://ayende.com/blog/153889/limit-your-abstractions-analyzing-a-ddd-application) 193 | 194 | * [The key is in the infrastructure](http://ayende.com/blog/154241/limit-your-abstractions-the-key-is-in-the-infrastructure) 195 | 196 | * [Put your controllers on a diet: POSTs and commands](https://lostechies.com/jimmybogard/2013/12/19/put-your-controllers-on-a-diet-posts-and-commands/) 197 | 198 | * [CQRS with MediatR and AutoMapper](https://lostechies.com/jimmybogard/2015/05/05/cqrs-with-mediatr-and-automapper/) 199 | 200 | * [Why the n-layer approach is bad for us all](http://tech.pro/blog/1498/why-the-n-layer-approach-is-bad-for-us-all) 201 | 202 | * [DDD – Special scenarios, part 2](https://lostechies.com/gabrielschenker/2015/05/11/ddd-special-scenarios-part-2/) 203 | 204 | * [DDD applied](https://lostechies.com/gabrielschenker/2015/04/28/ddd-applied/) 205 | 206 | * [Strongly typed AppSettings Configuration in ASP.NET 5](https://weblog.west-wind.com/posts/2015/Jun/03/Strongly-typed-AppSettings-Configuration-in-ASPNET-5) 207 | 208 | * [cmder Portable console emulator for Windows](http://gooseberrycreative.com/cmder/) 209 | 210 | * [Task Runner Explorer](https://visualstudiogallery.msdn.microsoft.com/8e1b4368-4afb-467a-bc13-9650572db708) 211 | 212 | * [mongoDB](https://www.mongodb.org/) 213 | 214 | * [SimpleInjector IoC](https://simpleinjector.org/index.html) 215 | 216 | * [Akka.net Actor framework](http://getakka.net/) 217 | 218 | * [FakeItEasy TDD mocks/fakes/stubs](https://github.com/FakeItEasy/FakeItEasy) 219 | 220 | * [Shouldly TDD assertion library](https://github.com/shouldly/shouldly) 221 | 222 | * [xUnit.net TDD](http://xunit.github.io/) 223 | 224 | * [FluentScheduler background task runner](https://github.com/jgeurts/FluentScheduler) 225 | 226 | * [JWT JSON Web Token](https://github.com/jwt-dotnet/jwt) 227 | --------------------------------------------------------------------------------