├── .gitignore ├── Common ├── Common.csproj ├── Common.v2.ncrunchproject ├── Domain.Model │ ├── Command.cs │ ├── Entity.cs │ ├── EventSourcedAggregateRoot.cs │ ├── EventStore.cs │ ├── FakeBus.cs │ ├── ICommandHandler.cs │ ├── ICommandSender.cs │ ├── IEventHandler.cs │ ├── IEventPublisher.cs │ ├── IEventStore.cs │ ├── IIdentity.cs │ ├── IRepository.cs │ ├── IRouteMessages.cs │ ├── IUnitOfWork.cs │ ├── Identity.cs │ ├── InMemoryQueue.cs │ ├── Message.cs │ └── ValueObject.cs ├── Events │ ├── Event.cs │ ├── EventSourcedRepository.cs │ └── EventStore.cs ├── Properties │ └── AssemblyInfo.cs └── packages.config ├── FirstPopCoffee.sln ├── FirstPopCoffee.v2.ncrunchsolution ├── LICENSE ├── RoastPlanning.Tests ├── Bus │ ├── Publishing_events │ │ ├── TestEvent.cs │ │ ├── TestEventHandler.cs │ │ └── When_publishing_an_event_to_the_fakebus_with_a_single_event_handler_registered.cs │ ├── Sending_commands │ │ ├── TestCommand.cs │ │ ├── TestCommandHandler.cs │ │ └── When_sending_a_command_to_the_bus.cs │ └── TestAggregateRoot.cs ├── CommandSpecification.cs ├── EventSpecification.cs ├── Properties │ └── AssemblyInfo.cs ├── RoastPlanning.Tests.csproj ├── RoastPlanning.Tests.v2.ncrunchproject ├── Scenarios │ ├── Adding_a_new_roast_schedule │ │ └── When_starting_a_new_roast_schedule.cs │ └── Choosing_roast_days_for_roast_schedule │ │ ├── When_choosing_roast_days_for_roast_schedule.cs │ │ └── When_choosing_roast_days_for_roast_schedule_using_event_store.cs ├── SpecificationNoMocks.cs └── packages.config ├── RoastPlanning ├── Application │ └── RoastScheduleCommandHandlers.cs ├── Domain.Model │ ├── ChooseRoastDaysForRoastScheduleCommand.cs │ ├── CreateNewRoastScheduleCommand.cs │ ├── RoastDays.cs │ ├── RoastSchedule.cs │ ├── RoastScheduleCreatedEvent.cs │ ├── RoastScheduleId.cs │ ├── RoastScheduleRoastDaysChosenEvent.cs │ └── RoastScheduleStartOfWeekService.cs ├── Properties │ └── AssemblyInfo.cs ├── RoastPlanning.csproj └── RoastPlanning.v2.ncrunchproject ├── WebUI.Tests ├── IoCTests.cs ├── Properties │ └── AssemblyInfo.cs ├── WebUI.Tests.csproj ├── WebUI.Tests.v2.ncrunchproject ├── app.config └── packages.config └── WebUI ├── App_Start ├── BundleConfig.cs ├── FilterConfig.cs ├── IdentityConfig.cs ├── NinjectWebCommon.cs ├── RouteConfig.cs └── Startup.Auth.cs ├── Content ├── Site.css ├── bootstrap-flatly.min.css ├── bootstrap.css └── bootstrap.min.css ├── DependencyResolution └── RoastPlanningNinjectModule.cs ├── Features ├── Account │ ├── AccountController.cs │ ├── ConfirmEmail.cshtml │ ├── ExternalLoginConfirmation.cshtml │ ├── ExternalLoginFailure.cshtml │ ├── ForgotPassword.cshtml │ ├── ForgotPasswordConfirmation.cshtml │ ├── Login.cshtml │ ├── ManageController.cs │ ├── Register.cshtml │ ├── ResetPassword.cshtml │ ├── ResetPasswordConfirmation.cshtml │ ├── SendCode.cshtml │ ├── VerifyCode.cshtml │ └── _ExternalLoginsListPartial.cshtml ├── Home │ ├── About.cshtml │ ├── Contact.cshtml │ ├── HomeController.cs │ └── Index.cshtml ├── Manage │ ├── AddPhoneNumber.cshtml │ ├── ChangePassword.cshtml │ ├── Index.cshtml │ ├── ManageLogins.cshtml │ ├── SetPassword.cshtml │ └── VerifyPhoneNumber.cshtml ├── RoastPlanning │ ├── Create.cshtml │ ├── Index.cshtml │ ├── RoastPlanningController.cs │ └── RoastSchedule.cshtml ├── Shared │ ├── Error.cshtml │ ├── Lockout.cshtml │ ├── _Layout.cshtml │ └── _LoginPartial.cshtml ├── Web.config └── _ViewStart.cshtml ├── Global.asax ├── Global.asax.cs ├── Infrastructure ├── FakeDbSet.cs └── FeatureViewLocationRazorViewEngine.cs ├── Models ├── AccountViewModels.cs ├── IdentityModels.cs ├── ManageViewModels.cs └── RoastScheduleViewModel.cs ├── Project_Readme.html ├── Properties └── AssemblyInfo.cs ├── ReadModel ├── RoastPlanningContext.cs ├── RoastPlanningReadModel.cs └── RoastPlanningReadModelProjections.cs ├── Scripts ├── _references.js ├── bootstrap.js ├── bootstrap.min.js ├── jquery-1.10.2.intellisense.js ├── jquery-1.10.2.js ├── jquery-1.10.2.min.js ├── jquery-1.10.2.min.map ├── jquery.validate-vsdoc.js ├── jquery.validate.js ├── jquery.validate.min.js ├── jquery.validate.unobtrusive.js ├── jquery.validate.unobtrusive.min.js ├── modernizr-2.6.2.js ├── respond.js └── respond.min.js ├── Startup.cs ├── Web.Debug.config ├── Web.Release.config ├── Web.config ├── WebUI.csproj ├── WebUI.v2.ncrunchproject ├── favicon.ico ├── fonts ├── glyphicons-halflings-regular.eot ├── glyphicons-halflings-regular.svg ├── glyphicons-halflings-regular.ttf └── glyphicons-halflings-regular.woff └── packages.config /.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 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | *.VC.VC.opendb 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | # TODO: Comment the next line if you want to checkin your web deploy settings 144 | # but database connection strings (with potential passwords) will be unencrypted 145 | *.pubxml 146 | *.publishproj 147 | 148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 149 | # checkin your Azure Web App publish settings, but sensitive information contained 150 | # in these scripts will be unencrypted 151 | PublishScripts/ 152 | 153 | # NuGet Packages 154 | *.nupkg 155 | # The packages folder can be ignored because of Package Restore 156 | **/packages/* 157 | # except build/, which is used as an MSBuild target. 158 | !**/packages/build/ 159 | # Uncomment if necessary however generally it will be regenerated when needed 160 | #!**/packages/repositories.config 161 | # NuGet v3's project.json files produces more ignoreable files 162 | *.nuget.props 163 | *.nuget.targets 164 | 165 | # Microsoft Azure Build Output 166 | csx/ 167 | *.build.csdef 168 | 169 | # Microsoft Azure Emulator 170 | ecf/ 171 | rcf/ 172 | 173 | # Windows Store app package directories and files 174 | AppPackages/ 175 | BundleArtifacts/ 176 | Package.StoreAssociation.xml 177 | _pkginfo.txt 178 | 179 | # Visual Studio cache files 180 | # files ending in .cache can be ignored 181 | *.[Cc]ache 182 | # but keep track of directories ending in .cache 183 | !*.[Cc]ache/ 184 | 185 | # Others 186 | ClientBin/ 187 | ~$* 188 | *~ 189 | *.dbmdl 190 | *.dbproj.schemaview 191 | *.pfx 192 | *.publishsettings 193 | node_modules/ 194 | orleans.codegen.cs 195 | 196 | # Since there are multiple workflows, uncomment next line to ignore bower_components 197 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 198 | #bower_components/ 199 | 200 | # RIA/Silverlight projects 201 | Generated_Code/ 202 | 203 | # Backup & report files from converting an old project file 204 | # to a newer Visual Studio version. Backup files are not needed, 205 | # because we have git ;-) 206 | _UpgradeReport_Files/ 207 | Backup*/ 208 | UpgradeLog*.XML 209 | UpgradeLog*.htm 210 | 211 | # SQL Server files 212 | *.mdf 213 | *.ldf 214 | 215 | # Business Intelligence projects 216 | *.rdl.data 217 | *.bim.layout 218 | *.bim_*.settings 219 | 220 | # Microsoft Fakes 221 | FakesAssemblies/ 222 | 223 | # GhostDoc plugin setting file 224 | *.GhostDoc.xml 225 | 226 | # Node.js Tools for Visual Studio 227 | .ntvs_analysis.dat 228 | 229 | # Visual Studio 6 build log 230 | *.plg 231 | 232 | # Visual Studio 6 workspace options file 233 | *.opt 234 | 235 | # Visual Studio LightSwitch build output 236 | **/*.HTMLClient/GeneratedArtifacts 237 | **/*.DesktopClient/GeneratedArtifacts 238 | **/*.DesktopClient/ModelManifest.xml 239 | **/*.Server/GeneratedArtifacts 240 | **/*.Server/ModelManifest.xml 241 | _Pvt_Extensions 242 | 243 | # Paket dependency manager 244 | .paket/paket.exe 245 | paket-files/ 246 | 247 | # FAKE - F# Make 248 | .fake/ 249 | 250 | # JetBrains Rider 251 | .idea/ 252 | *.sln.iml 253 | -------------------------------------------------------------------------------- /Common/Common.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {3F9E80F7-9744-4B2E-8248-D80E2ED7E54F} 8 | Library 9 | Properties 10 | FirstPopCoffee.Common 11 | FirstPopCoffee.Common 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\ReflectionMagic.2.1.0\lib\net40\ReflectionMagic.dll 35 | True 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 | 80 | -------------------------------------------------------------------------------- /Common/Common.v2.ncrunchproject: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heynickc/firstpopcoffee/ca6174066ae81c461ccc66b1941d4a34aa373dd0/Common/Common.v2.ncrunchproject -------------------------------------------------------------------------------- /Common/Domain.Model/Command.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FirstPopCoffee.Common.Domain.Model { 4 | public class Command : Message { 5 | public Guid Id { get; } 6 | public Command() { 7 | Id = Guid.NewGuid(); 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Common/Domain.Model/Entity.cs: -------------------------------------------------------------------------------- 1 | namespace FirstPopCoffee.Common.Domain.Model { 2 | public abstract class Entity { } 3 | } 4 | -------------------------------------------------------------------------------- /Common/Domain.Model/EventSourcedAggregateRoot.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using FirstPopCoffee.Common.Events; 4 | using ReflectionMagic; 5 | 6 | namespace FirstPopCoffee.Common.Domain.Model { 7 | public abstract class EventSourcedAggregateRoot : Entity { 8 | 9 | private readonly List _changes = new List(); 10 | 11 | public Guid Id { get; protected set; } 12 | public int Version { get; internal set; } 13 | 14 | public IEnumerable GetUncommittedChanges() { 15 | return _changes; 16 | } 17 | 18 | public void MarkChangesAsCommitted() { 19 | _changes.Clear(); 20 | } 21 | 22 | public void LoadFromHistory(IEnumerable history) { 23 | foreach (var e in history) ApplyChange(e, false); 24 | } 25 | 26 | protected void ApplyChange(Event @event) { 27 | ApplyChange(@event, true); 28 | } 29 | 30 | // push atomic aggregate changes to local history for further processing (EventStore.SaveEvents) 31 | private void ApplyChange(Event @event, bool isNew) { 32 | this.AsDynamic().Apply(@event); 33 | if (isNew) _changes.Add(@event); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Common/Domain.Model/EventStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using FirstPopCoffee.Common.Events; 5 | 6 | namespace FirstPopCoffee.Common.Domain.Model { 7 | public class EventStore : IEventStore { 8 | private readonly IEventPublisher _publisher; 9 | 10 | private struct EventDescriptor { 11 | 12 | public readonly Event EventData; 13 | public readonly Guid Id; 14 | public readonly int Version; 15 | 16 | public EventDescriptor(Guid id, Event eventData, int version) { 17 | EventData = eventData; 18 | Version = version; 19 | Id = id; 20 | } 21 | } 22 | 23 | public EventStore(IEventPublisher publisher) { 24 | _publisher = publisher; 25 | } 26 | 27 | private readonly Dictionary> _current = new Dictionary>(); 28 | 29 | public void SaveEvents(Guid aggregateId, IEnumerable events, int expectedVersion) { 30 | List eventDescriptors; 31 | 32 | // try to get event descriptors list for given aggregate id 33 | // otherwise -> create empty dictionary 34 | if (!_current.TryGetValue(aggregateId, out eventDescriptors)) { 35 | eventDescriptors = new List(); 36 | _current.Add(aggregateId, eventDescriptors); 37 | } 38 | // check whether latest event version matches current aggregate version 39 | // otherwise -> throw exception 40 | else if (eventDescriptors[eventDescriptors.Count - 1].Version != expectedVersion && expectedVersion != -1) { 41 | throw new ConcurrencyException(); 42 | } 43 | var i = expectedVersion; 44 | 45 | // iterate through current aggregate events increasing version with each processed event 46 | foreach (var @event in events) { 47 | i++; 48 | @event.Version = i; 49 | 50 | // push event to the event descriptors list for current aggregate 51 | eventDescriptors.Add(new EventDescriptor(aggregateId, @event, i)); 52 | 53 | // publish current event to the bus for further processing by subscribers 54 | _publisher.Publish(@event); 55 | } 56 | } 57 | 58 | // collect all processed events for given aggregate and return them as a list 59 | // used to build up an aggregate from its history (Domain.LoadsFromHistory) 60 | public List GetEventsForAggregate(Guid aggregateId) { 61 | List eventDescriptors; 62 | 63 | if (!_current.TryGetValue(aggregateId, out eventDescriptors)) { 64 | throw new AggregateNotFoundException(); 65 | } 66 | 67 | return eventDescriptors.Select(desc => desc.EventData).ToList(); 68 | } 69 | } 70 | 71 | public class AggregateNotFoundException : Exception { 72 | } 73 | 74 | public class ConcurrencyException : Exception { 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Common/Domain.Model/FakeBus.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using FirstPopCoffee.Common.Events; 6 | 7 | namespace FirstPopCoffee.Common.Domain.Model { 8 | public class FakeBus : IEventPublisher, ICommandSender 9 | { 10 | private readonly Dictionary>> _routes = new Dictionary>>(); 11 | 12 | public void RegisterHandler(Action handler) where T : Message { 13 | List> handlers; 14 | 15 | if (!_routes.TryGetValue(typeof(T), out handlers)) { 16 | handlers = new List>(); 17 | _routes.Add(typeof(T), handlers); 18 | } 19 | 20 | handlers.Add((x => handler((T) x))); 21 | } 22 | 23 | public void Send(T command) where T : Command { 24 | List> handlers; 25 | 26 | if (_routes.TryGetValue(typeof(T), out handlers)) { 27 | if (handlers.Count != 1) throw new InvalidOperationException("cannot send to more than one handler"); 28 | handlers[0](command); 29 | } 30 | else { 31 | throw new InvalidOperationException("no handler registered"); 32 | } 33 | } 34 | 35 | public void Publish(T @event) where T : Event { 36 | List> handlers; 37 | 38 | if (!_routes.TryGetValue(@event.GetType(), out handlers)) return; 39 | 40 | foreach (var handler in handlers) { 41 | Task.Run(() => handler(@event)); 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Common/Domain.Model/ICommandHandler.cs: -------------------------------------------------------------------------------- 1 | namespace FirstPopCoffee.Common.Domain.Model { 2 | public interface ICommandHandler where TCommand : Message { 3 | void Handle(TCommand message); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Common/Domain.Model/ICommandSender.cs: -------------------------------------------------------------------------------- 1 | namespace FirstPopCoffee.Common.Domain.Model { 2 | public interface ICommandSender { 3 | void Send(T command) where T : Command; 4 | } 5 | } -------------------------------------------------------------------------------- /Common/Domain.Model/IEventHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace FirstPopCoffee.Common.Domain.Model { 8 | public interface IEventHandler where TEvent : Message { 9 | void Handle(TEvent message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Common/Domain.Model/IEventPublisher.cs: -------------------------------------------------------------------------------- 1 | using FirstPopCoffee.Common.Events; 2 | 3 | namespace FirstPopCoffee.Common.Domain.Model { 4 | public interface IEventPublisher { 5 | void Publish(T @event) where T : Event; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Common/Domain.Model/IEventStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using FirstPopCoffee.Common.Events; 4 | 5 | namespace FirstPopCoffee.Common.Domain.Model { 6 | public interface IEventStore { 7 | void SaveEvents(Guid aggregateId, IEnumerable events, int expectedVersion); 8 | List GetEventsForAggregate(Guid aggregateId); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Common/Domain.Model/IIdentity.cs: -------------------------------------------------------------------------------- 1 | namespace FirstPopCoffee.Common.Domain.Model { 2 | interface IIdentity { 3 | string Id { get; } 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Common/Domain.Model/IRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FirstPopCoffee.Common.Domain.Model { 4 | public interface IRepository where TAggregateRoot : EventSourcedAggregateRoot, new() { 5 | void Save(TAggregateRoot aggregate, int exptectedVersion); 6 | TAggregateRoot GetById(Guid id); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Common/Domain.Model/IRouteMessages.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace FirstPopCoffee.Common.Domain.Model { 8 | public interface IRouteMessages { 9 | void Route(object message); 10 | } 11 | 12 | public class MessageRouter : IRouteMessages { 13 | private readonly IDictionary>> _routes; 14 | 15 | public MessageRouter() { 16 | _routes = new Dictionary>>(); 17 | } 18 | 19 | public void Register(Action route) where TMessage : class { 20 | var routingKey = typeof(TMessage); 21 | ICollection> routes; 22 | 23 | if (!_routes.TryGetValue(routingKey, out routes)) 24 | _routes[routingKey] = routes = new LinkedList>(); 25 | 26 | routes.Add(message => route(message as TMessage)); 27 | } 28 | 29 | public void Route(object message) { 30 | ICollection> routes; 31 | 32 | if (!_routes.TryGetValue(message.GetType(), out routes)) 33 | throw new RouteNotRegisteredException(message.GetType()); 34 | 35 | foreach (var route in routes) 36 | route(message); 37 | } 38 | } 39 | 40 | public class RouteNotRegisteredException : Exception { 41 | public RouteNotRegisteredException(Type messageType) : base(string.Format("No route specified for message '{0}'", messageType.FullName)) { 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Common/Domain.Model/IUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace FirstPopCoffee.Common.Domain.Model { 8 | public interface IUnitOfWork { 9 | void Commit(); 10 | void Rollback(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Common/Domain.Model/Identity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FirstPopCoffee.Common.Domain.Model { 4 | public abstract class Identity : IEquatable, IIdentity { 5 | public Identity() { 6 | this.Id = Guid.NewGuid().ToString(); 7 | } 8 | 9 | public Identity(string id) { 10 | this.Id = id; 11 | } 12 | 13 | public string Id { get; private set; } 14 | 15 | public bool Equals(Identity id) { 16 | if (object.ReferenceEquals(this, id)) return true; 17 | if (object.ReferenceEquals(null, id)) return false; 18 | return this.Id.Equals(id.Id); 19 | } 20 | 21 | public override bool Equals(object anotherObject) { 22 | return Equals(anotherObject as Identity); 23 | } 24 | 25 | public override int GetHashCode() { 26 | return (this.GetType().GetHashCode()*907) + this.Id.GetHashCode(); 27 | } 28 | 29 | public override string ToString() { 30 | return this.GetType().Name + " [Id=" + Id + "]"; 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /Common/Domain.Model/InMemoryQueue.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace FirstPopCoffee.Common.Domain.Model { 8 | public interface IQueue { 9 | void Put(object item); 10 | void Pop(Action popAction); 11 | } 12 | 13 | public class InMemoryQueue : IQueue { 14 | private readonly Queue _itemQueue; 15 | private readonly Queue> _listenerQueue; 16 | 17 | public InMemoryQueue() { 18 | _itemQueue = new Queue(32); 19 | _listenerQueue = new Queue>(32); 20 | } 21 | 22 | public void Put(object item) { 23 | if (_listenerQueue.Count == 0) { 24 | _itemQueue.Enqueue(item); 25 | return; 26 | } 27 | 28 | var listener = _listenerQueue.Dequeue(); 29 | listener(item); 30 | } 31 | 32 | public void Pop(Action popAction) { 33 | if (_itemQueue.Count == 0) { 34 | _listenerQueue.Enqueue(popAction); 35 | return; 36 | } 37 | 38 | var item = _itemQueue.Dequeue(); 39 | popAction(item); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Common/Domain.Model/Message.cs: -------------------------------------------------------------------------------- 1 | namespace FirstPopCoffee.Common.Domain.Model { 2 | public interface Message { } 3 | } 4 | -------------------------------------------------------------------------------- /Common/Domain.Model/ValueObject.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace FirstPopCoffee.Common.Domain.Model { 7 | 8 | // https://github.com/tpierrain/Value 9 | /// 10 | /// Base class for any Value Type (i.e. the 'Value Object' oxymoron of DDD). 11 | /// All you have to do is to implement the abstract methods: 12 | /// 13 | /// Domain type to be 'turned' into a Value Type. 14 | public abstract class ValueObject : EquatableByValue where T : class { 15 | } 16 | 17 | /// 18 | /// Support a by-Value Equality and Unicity. 19 | /// 20 | /// This latest implementation has been inspired from Scott Millett's book (Patterns, Principles, and Practices of Domain-Driven Design). 21 | /// 22 | public abstract class EquatableByValue : IEquatable where T : class { 23 | private const int undefined = -1; 24 | 25 | private volatile int hashCode = undefined; 26 | 27 | public static bool operator ==(EquatableByValue x, EquatableByValue y) { 28 | if (ReferenceEquals(x, null) && ReferenceEquals(y, null)) { 29 | return true; 30 | } 31 | 32 | if (ReferenceEquals(x, null) || ReferenceEquals(y, null)) { 33 | return false; 34 | } 35 | 36 | return x.Equals(y); 37 | } 38 | 39 | public static bool operator !=(EquatableByValue x, EquatableByValue y) { 40 | return !(x == y); 41 | } 42 | 43 | public bool Equals(T other) { 44 | var otherEquatable = other as EquatableByValue; 45 | if (otherEquatable == null) { 46 | return false; 47 | } 48 | 49 | return this.GetAllAttributesToBeUsedForEquality().SequenceEqual(otherEquatable.GetAllAttributesToBeUsedForEquality()); 50 | } 51 | 52 | public override bool Equals(object obj) { 53 | if (obj == null) { 54 | return false; 55 | } 56 | 57 | var other = obj as T; 58 | 59 | if (ReferenceEquals(other, null)) { 60 | return false; 61 | } 62 | 63 | return this.Equals(other); 64 | } 65 | 66 | public override int GetHashCode() { 67 | return this.GetHashCodeImpl(); 68 | } 69 | 70 | protected abstract IEnumerable GetAllAttributesToBeUsedForEquality(); 71 | 72 | protected virtual void ResetHashCode() { 73 | this.hashCode = undefined; 74 | } 75 | 76 | private int GetHashCodeImpl() { 77 | if (this.hashCode == undefined) { 78 | var code = 0; 79 | 80 | foreach (var attribute in this.GetAllAttributesToBeUsedForEquality()) { 81 | code = (code * 397) ^ (attribute == null ? 0 : attribute.GetHashCode()); 82 | } 83 | 84 | this.hashCode = code; 85 | } 86 | 87 | return this.hashCode; 88 | } 89 | } 90 | 91 | /// 92 | /// A list with equality based on its content and not on the list's reference 93 | /// (i.e.: 2 different instances containing the same items in the same order will be equals). 94 | /// 95 | /// This type is not thread-safe (for hashcode updates). 96 | /// Type of the listed items. 97 | public class ListByValue : EquatableByValue>, IList { 98 | private readonly IList list; 99 | 100 | public ListByValue() : this(new List()) { 101 | } 102 | 103 | public ListByValue(IList list) { 104 | this.list = list; 105 | } 106 | 107 | public int Count => this.list.Count; 108 | 109 | public bool IsReadOnly => ((ICollection) this.list).IsReadOnly; 110 | 111 | public T this[int index] { 112 | get { return this.list[index]; } 113 | set { 114 | this.ResetHashCode(); 115 | this.list[index] = value; 116 | } 117 | } 118 | 119 | public void Add(T item) { 120 | this.ResetHashCode(); 121 | this.list.Add(item); 122 | } 123 | 124 | public void Clear() { 125 | this.ResetHashCode(); 126 | this.list.Clear(); 127 | } 128 | 129 | public bool Contains(T item) { 130 | return this.list.Contains(item); 131 | } 132 | 133 | public void CopyTo(T[] array, int arrayIndex) { 134 | this.list.CopyTo(array, arrayIndex); 135 | } 136 | 137 | public IEnumerator GetEnumerator() { 138 | return this.list.GetEnumerator(); 139 | } 140 | 141 | public int IndexOf(T item) { 142 | return this.list.IndexOf(item); 143 | } 144 | 145 | public void Insert(int index, T item) { 146 | this.ResetHashCode(); 147 | this.list.Insert(index, item); 148 | } 149 | 150 | public bool Remove(T item) { 151 | this.ResetHashCode(); 152 | return this.list.Remove(item); 153 | } 154 | 155 | public void RemoveAt(int index) { 156 | this.ResetHashCode(); 157 | this.list.RemoveAt(index); 158 | } 159 | 160 | IEnumerator IEnumerable.GetEnumerator() { 161 | return ((IEnumerable) this.list).GetEnumerator(); 162 | } 163 | 164 | protected override IEnumerable GetAllAttributesToBeUsedForEquality() { 165 | return (IEnumerable) this.list; 166 | } 167 | } 168 | 169 | /// 170 | /// A Set with equality based on its content and not on the Set's reference 171 | /// (i.e.: 2 different instances containing the same items will be equals whatever their storage order). 172 | /// 173 | /// This type is not thread-safe (for hashcode updates). 174 | /// Type of the listed items. 175 | public class SetByValue : ISet { 176 | private readonly ISet hashSet; 177 | 178 | private int? hashCode; 179 | 180 | public SetByValue(ISet hashSet) { 181 | this.hashSet = hashSet; 182 | } 183 | 184 | public SetByValue() : this(new HashSet()) { 185 | } 186 | 187 | public int Count => this.hashSet.Count; 188 | 189 | public bool IsReadOnly => this.hashSet.IsReadOnly; 190 | 191 | public void Add(T item) { 192 | this.ResetHashCode(); 193 | this.hashSet.Add(item); 194 | } 195 | 196 | public void Clear() { 197 | this.ResetHashCode(); 198 | this.hashSet.Clear(); 199 | } 200 | 201 | public bool Contains(T item) { 202 | return this.hashSet.Contains(item); 203 | } 204 | 205 | public void CopyTo(T[] array, int arrayIndex) { 206 | this.hashSet.CopyTo(array, arrayIndex); 207 | } 208 | 209 | public override bool Equals(object obj) { 210 | var other = obj as SetByValue; 211 | if (other == null) { 212 | return false; 213 | } 214 | 215 | return this.hashSet.SetEquals(other); 216 | } 217 | 218 | public void ExceptWith(IEnumerable other) { 219 | this.ResetHashCode(); 220 | this.hashSet.ExceptWith(other); 221 | } 222 | 223 | public IEnumerator GetEnumerator() { 224 | return this.hashSet.GetEnumerator(); 225 | } 226 | 227 | public override int GetHashCode() { 228 | if (this.hashCode == null) { 229 | var code = 0; 230 | 231 | // Two instances with same elements added in different order must return the same hashcode 232 | // Let's compute and sort hashcodes of all elements (always in the same order) 233 | var sortedHashCodes = new SortedSet(); 234 | foreach (var element in this.hashSet) { 235 | sortedHashCodes.Add(element.GetHashCode()); 236 | } 237 | 238 | foreach (var elementHashCode in sortedHashCodes) { 239 | code = (code * 397) ^ elementHashCode; 240 | } 241 | 242 | // Cache the result in a field 243 | this.hashCode = code; 244 | } 245 | 246 | return this.hashCode.Value; 247 | } 248 | 249 | public void IntersectWith(IEnumerable other) { 250 | this.ResetHashCode(); 251 | this.hashSet.IntersectWith(other); 252 | } 253 | 254 | public bool IsProperSubsetOf(IEnumerable other) { 255 | return this.hashSet.IsProperSubsetOf(other); 256 | } 257 | 258 | public bool IsProperSupersetOf(IEnumerable other) { 259 | return this.hashSet.IsProperSupersetOf(other); 260 | } 261 | 262 | public bool IsSubsetOf(IEnumerable other) { 263 | return this.hashSet.IsSubsetOf(other); 264 | } 265 | 266 | public bool IsSupersetOf(IEnumerable other) { 267 | return this.hashSet.IsSupersetOf(other); 268 | } 269 | 270 | public bool Overlaps(IEnumerable other) { 271 | return this.hashSet.Overlaps(other); 272 | } 273 | 274 | public bool Remove(T item) { 275 | this.ResetHashCode(); 276 | return this.hashSet.Remove(item); 277 | } 278 | 279 | public bool SetEquals(IEnumerable other) { 280 | return this.hashSet.SetEquals(other); 281 | } 282 | 283 | public void SymmetricExceptWith(IEnumerable other) { 284 | this.ResetHashCode(); 285 | this.hashSet.SymmetricExceptWith(other); 286 | } 287 | 288 | public void UnionWith(IEnumerable other) { 289 | this.ResetHashCode(); 290 | this.hashSet.UnionWith(other); 291 | } 292 | 293 | bool ISet.Add(T item) { 294 | this.ResetHashCode(); 295 | return this.hashSet.Add(item); 296 | } 297 | 298 | IEnumerator IEnumerable.GetEnumerator() { 299 | return ((IEnumerable) this.hashSet).GetEnumerator(); 300 | } 301 | 302 | protected virtual void ResetHashCode() { 303 | this.hashCode = null; 304 | } 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /Common/Events/Event.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FirstPopCoffee.Common.Domain.Model; 3 | 4 | namespace FirstPopCoffee.Common.Events { 5 | public class Event : Message { 6 | public Guid Id; 7 | public int Version; 8 | public Event() { 9 | Id = Guid.NewGuid(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Common/Events/EventSourcedRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FirstPopCoffee.Common.Domain.Model; 3 | 4 | namespace FirstPopCoffee.Common.Events { 5 | public class EventSourcedRepository : IRepository where T : EventSourcedAggregateRoot, new() { 6 | 7 | private readonly IEventStore _storage; 8 | public EventSourcedRepository(IEventStore storage) { 9 | _storage = storage; 10 | } 11 | 12 | public void Save(T aggregate, int expectedVersion) { 13 | _storage.SaveEvents(aggregate.Id, aggregate.GetUncommittedChanges(), expectedVersion); 14 | } 15 | 16 | public T GetById(Guid id) { 17 | var obj = new T();//lots of ways to do this 18 | var e = _storage.GetEventsForAggregate(id); 19 | obj.LoadFromHistory(e); 20 | return obj; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Common/Events/EventStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using FirstPopCoffee.Common.Domain.Model; 5 | 6 | namespace FirstPopCoffee.Common.Events { 7 | public class EventStore : IEventStore { 8 | private readonly IEventPublisher _publisher; 9 | 10 | private struct EventDescriptor { 11 | 12 | public readonly Event EventData; 13 | public readonly Guid Id; 14 | public readonly int Version; 15 | 16 | public EventDescriptor(Guid id, Event eventData, int version) { 17 | EventData = eventData; 18 | Version = version; 19 | Id = id; 20 | } 21 | } 22 | 23 | public EventStore(IEventPublisher publisher) { 24 | _publisher = publisher; 25 | } 26 | 27 | private readonly Dictionary> _current = new Dictionary>(); 28 | 29 | public void SaveEvents(Guid aggregateId, IEnumerable events, int expectedVersion) { 30 | List eventDescriptors; 31 | 32 | // try to get event descriptors list for given aggregate id 33 | // otherwise -> create empty dictionary 34 | if (!_current.TryGetValue(aggregateId, out eventDescriptors)) { 35 | eventDescriptors = new List(); 36 | _current.Add(aggregateId, eventDescriptors); 37 | } 38 | // check whether latest event version matches current aggregate version 39 | // otherwise -> throw exception 40 | else if (eventDescriptors[eventDescriptors.Count - 1].Version != expectedVersion && expectedVersion != -1) { 41 | throw new ConcurrencyException(); 42 | } 43 | var i = expectedVersion; 44 | 45 | // iterate through current aggregate events increasing version with each processed event 46 | foreach (var @event in events) { 47 | i++; 48 | @event.Version = i; 49 | 50 | // push event to the event descriptors list for current aggregate 51 | eventDescriptors.Add(new EventDescriptor(aggregateId, @event, i)); 52 | 53 | // publish current event to the bus for further processing by subscribers 54 | _publisher.Publish(@event); 55 | } 56 | } 57 | 58 | // collect all processed events for given aggregate and return them as a list 59 | // used to build up an aggregate from its history (Domain.LoadsFromHistory) 60 | public List GetEventsForAggregate(Guid aggregateId) { 61 | List eventDescriptors; 62 | 63 | if (!_current.TryGetValue(aggregateId, out eventDescriptors)) { 64 | throw new AggregateNotFoundException(); 65 | } 66 | 67 | return eventDescriptors.Select(desc => desc.EventData).ToList(); 68 | } 69 | } 70 | 71 | public class AggregateNotFoundException : Exception { 72 | } 73 | 74 | public class ConcurrencyException : Exception { 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Common/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("Common")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("Cambridge International, Inc")] 12 | [assembly: AssemblyProduct("Common")] 13 | [assembly: AssemblyCopyright("Copyright © Cambridge International, Inc 2016")] 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("3f9e80f7-9744-4b2e-8248-d80e2ed7e54f")] 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 | -------------------------------------------------------------------------------- /Common/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /FirstPopCoffee.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RoastPlanningContext", "RoastPlanningContext", "{34B37611-ED73-49D7-B254-F98556D5BEB5}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RoastPlanning", "RoastPlanning\RoastPlanning.csproj", "{907EB8AA-9306-4BE6-81CD-08216347FA20}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Common", "Common", "{C5DC54CF-FA9D-4EE7-8C12-99E1BE98A122}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "Common\Common.csproj", "{3F9E80F7-9744-4B2E-8248-D80E2ED7E54F}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RoastPlanning.Tests", "RoastPlanning.Tests\RoastPlanning.Tests.csproj", "{E6E81603-66F5-441C-BB1A-7316F025C6BF}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UI", "UI", "{6B649A0E-F3C9-4622-B8B6-211B2279A4E9}" 17 | EndProject 18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebUI", "WebUI\WebUI.csproj", "{B9DBC432-9B90-449D-A623-E0C6376D259B}" 19 | EndProject 20 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebUI.Tests", "WebUI.Tests\WebUI.Tests.csproj", "{D0E3D322-1ECC-485F-86D0-0A88A066A56F}" 21 | EndProject 22 | Global 23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 24 | Debug|Any CPU = Debug|Any CPU 25 | Release|Any CPU = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {907EB8AA-9306-4BE6-81CD-08216347FA20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {907EB8AA-9306-4BE6-81CD-08216347FA20}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {907EB8AA-9306-4BE6-81CD-08216347FA20}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {907EB8AA-9306-4BE6-81CD-08216347FA20}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {3F9E80F7-9744-4B2E-8248-D80E2ED7E54F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {3F9E80F7-9744-4B2E-8248-D80E2ED7E54F}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {3F9E80F7-9744-4B2E-8248-D80E2ED7E54F}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {3F9E80F7-9744-4B2E-8248-D80E2ED7E54F}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {E6E81603-66F5-441C-BB1A-7316F025C6BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {E6E81603-66F5-441C-BB1A-7316F025C6BF}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {E6E81603-66F5-441C-BB1A-7316F025C6BF}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {E6E81603-66F5-441C-BB1A-7316F025C6BF}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {B9DBC432-9B90-449D-A623-E0C6376D259B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {B9DBC432-9B90-449D-A623-E0C6376D259B}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {B9DBC432-9B90-449D-A623-E0C6376D259B}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {B9DBC432-9B90-449D-A623-E0C6376D259B}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {D0E3D322-1ECC-485F-86D0-0A88A066A56F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {D0E3D322-1ECC-485F-86D0-0A88A066A56F}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {D0E3D322-1ECC-485F-86D0-0A88A066A56F}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {D0E3D322-1ECC-485F-86D0-0A88A066A56F}.Release|Any CPU.Build.0 = Release|Any CPU 48 | EndGlobalSection 49 | GlobalSection(SolutionProperties) = preSolution 50 | HideSolutionNode = FALSE 51 | EndGlobalSection 52 | GlobalSection(NestedProjects) = preSolution 53 | {907EB8AA-9306-4BE6-81CD-08216347FA20} = {34B37611-ED73-49D7-B254-F98556D5BEB5} 54 | {3F9E80F7-9744-4B2E-8248-D80E2ED7E54F} = {C5DC54CF-FA9D-4EE7-8C12-99E1BE98A122} 55 | {E6E81603-66F5-441C-BB1A-7316F025C6BF} = {34B37611-ED73-49D7-B254-F98556D5BEB5} 56 | {B9DBC432-9B90-449D-A623-E0C6376D259B} = {6B649A0E-F3C9-4622-B8B6-211B2279A4E9} 57 | {D0E3D322-1ECC-485F-86D0-0A88A066A56F} = {6B649A0E-F3C9-4622-B8B6-211B2279A4E9} 58 | EndGlobalSection 59 | EndGlobal 60 | -------------------------------------------------------------------------------- /FirstPopCoffee.v2.ncrunchsolution: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heynickc/firstpopcoffee/ca6174066ae81c461ccc66b1941d4a34aa373dd0/FirstPopCoffee.v2.ncrunchsolution -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Nick Chamberlain 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 | -------------------------------------------------------------------------------- /RoastPlanning.Tests/Bus/Publishing_events/TestEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using FirstPopCoffee.Common.Domain.Model; 7 | using FirstPopCoffee.Common.Events; 8 | 9 | namespace FirstPopCoffee.RoastPlanning.Tests.Bus.Publishing_events { 10 | public class TestEvent : Event { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /RoastPlanning.Tests/Bus/Publishing_events/TestEventHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using FirstPopCoffee.Common.Domain.Model; 7 | 8 | namespace FirstPopCoffee.RoastPlanning.Tests.Bus.Publishing_events { 9 | public class TestEventHandler : IEventHandler { 10 | public List HandledEventIds { get; } 11 | 12 | public TestEventHandler() { 13 | HandledEventIds = new List(); 14 | } 15 | 16 | public void Handle(TestEvent @event) { 17 | HandledEventIds.Add(@event.Id); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /RoastPlanning.Tests/Bus/Publishing_events/When_publishing_an_event_to_the_fakebus_with_a_single_event_handler_registered.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using FirstPopCoffee.Common.Domain.Model; 7 | using FirstPopCoffee.RoastPlanning.Tests.Bus.Sending_commands; 8 | using FluentAssertions; 9 | 10 | namespace FirstPopCoffee.RoastPlanning.Tests.Bus.Publishing_events { 11 | 12 | public class When_publishing_an_event_to_the_fakebus_with_a_single_event_handler_registered : EventSpecification { 13 | 14 | private readonly FakeBus _bus; 15 | private TestEventHandler _handler; 16 | private TestEvent _event; 17 | public When_publishing_an_event_to_the_fakebus_with_a_single_event_handler_registered() { 18 | _bus = new FakeBus(); 19 | _bus.RegisterHandler(_handler.Handle); 20 | } 21 | 22 | protected override TestEvent When() { 23 | _event = new TestEvent(); 24 | return _event; 25 | } 26 | 27 | protected override IEventHandler EventHandler() { 28 | _handler = new TestEventHandler(); 29 | return _handler; 30 | } 31 | 32 | [Then] 33 | public void Then_handle_method_is_invoked_on_command_handler() { 34 | _handler.HandledEventIds.First().Should().Be(_event.Id); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /RoastPlanning.Tests/Bus/Sending_commands/TestCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FirstPopCoffee.Common.Domain.Model; 3 | 4 | namespace FirstPopCoffee.RoastPlanning.Tests.Bus.Sending_commands { 5 | 6 | public class TestCommand : Command { } 7 | 8 | } -------------------------------------------------------------------------------- /RoastPlanning.Tests/Bus/Sending_commands/TestCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using FirstPopCoffee.Common.Domain.Model; 4 | 5 | namespace FirstPopCoffee.RoastPlanning.Tests.Bus.Sending_commands { 6 | 7 | public class TestCommandHandler : ICommandHandler { 8 | public List HandledCommandIds { get; } 9 | public TestCommandHandler() { 10 | HandledCommandIds = new List(); 11 | } 12 | public void Handle(TestCommand command) { 13 | HandledCommandIds.Add(command.Id); 14 | } 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /RoastPlanning.Tests/Bus/Sending_commands/When_sending_a_command_to_the_bus.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using FirstPopCoffee.Common.Domain.Model; 6 | using FluentAssertions; 7 | 8 | namespace FirstPopCoffee.RoastPlanning.Tests.Bus.Sending_commands { 9 | 10 | public class When_sending_a_command_to_the_fakebus_with_a_single_command_handler : CommandSpecification { 11 | private readonly FakeBus _bus; 12 | private TestCommandHandler _handler; 13 | private TestCommand _command; 14 | public When_sending_a_command_to_the_fakebus_with_a_single_command_handler() { 15 | _bus = new FakeBus(); 16 | _bus.RegisterHandler(_handler.Handle); 17 | } 18 | 19 | protected override TestCommand When() { 20 | _command = new TestCommand(); 21 | return _command; 22 | } 23 | 24 | protected override ICommandHandler CommandHandler() { 25 | _handler = new TestCommandHandler(); 26 | return _handler; 27 | } 28 | 29 | [Then] 30 | public void Then_handle_method_is_invoked_on_command_handler() { 31 | _handler.HandledCommandIds.First().Should().Be(_command.Id); 32 | } 33 | } 34 | 35 | public class When_sending_a_command_to_the_fakebus_with_multiple_command_handlers : CommandSpecification { 36 | private readonly FakeBus _bus; 37 | private TestCommandHandler _handler; 38 | private TestCommand _command; 39 | public When_sending_a_command_to_the_fakebus_with_multiple_command_handlers() { 40 | _bus = new FakeBus(); 41 | _bus.RegisterHandler(_handler.Handle); 42 | } 43 | 44 | protected override TestCommand When() { 45 | _command = new TestCommand(); 46 | return _command; 47 | } 48 | 49 | protected override ICommandHandler CommandHandler() { 50 | _handler = new TestCommandHandler(); 51 | return _handler; 52 | } 53 | 54 | [Then] 55 | public void Then_handle_method_is_invoked_on_command_handler() { 56 | _handler.HandledCommandIds.First().Should().Be(_command.Id); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /RoastPlanning.Tests/Bus/TestAggregateRoot.cs: -------------------------------------------------------------------------------- 1 | using FirstPopCoffee.Common.Domain.Model; 2 | 3 | namespace FirstPopCoffee.RoastPlanning.Tests.Bus { 4 | 5 | public class TestAggregateRoot : EventSourcedAggregateRoot { 6 | 7 | } 8 | 9 | } -------------------------------------------------------------------------------- /RoastPlanning.Tests/CommandSpecification.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using FirstPopCoffee.Common.Domain.Model; 4 | using FirstPopCoffee.Common.Events; 5 | using Moq; 6 | using Xunit; 7 | 8 | namespace FirstPopCoffee.RoastPlanning.Tests { 9 | public abstract class CommandSpecification 10 | where TAggregateRoot : EventSourcedAggregateRoot, new() 11 | where TCommand : Command { 12 | 13 | protected virtual IEnumerable Given() { 14 | return new List(); 15 | } 16 | protected abstract TCommand When(); 17 | protected abstract ICommandHandler CommandHandler(); 18 | protected TAggregateRoot AggregateRoot; 19 | protected Mock> MockRepository; 20 | 21 | // collects published events for assertion 22 | protected IEnumerable PublishedEvents; 23 | protected Exception CaughtException; 24 | 25 | protected CommandSpecification() { 26 | 27 | AggregateRoot = new TAggregateRoot(); 28 | AggregateRoot.LoadFromHistory(Given()); 29 | 30 | MockRepository = new Mock>(); 31 | MockRepository.Setup(x => x.GetById(It.IsAny())).Returns(AggregateRoot); 32 | MockRepository.Setup(x => x.Save(It.IsAny(), It.IsAny())) 33 | .Callback((x, _) => AggregateRoot = x); 34 | 35 | try { 36 | CommandHandler().Handle(When()); 37 | PublishedEvents = new List(AggregateRoot.GetUncommittedChanges()); 38 | } 39 | catch (Exception exception) { 40 | CaughtException = exception; 41 | } 42 | 43 | } 44 | } 45 | 46 | public class ThenAttribute : FactAttribute { } 47 | 48 | public class PrepareEvent { 49 | 50 | public static EventVersionSetter Set(Event domainEvent) { 51 | return new EventVersionSetter(domainEvent); 52 | } 53 | } 54 | 55 | public class EventVersionSetter { 56 | 57 | private readonly Event _event; 58 | 59 | public EventVersionSetter(Event @event) { 60 | _event = @event; 61 | } 62 | 63 | public Event ToVersion(int version) { 64 | _event.Version = version; 65 | return _event; 66 | } 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /RoastPlanning.Tests/EventSpecification.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using FirstPopCoffee.Common.Domain.Model; 4 | using FirstPopCoffee.Common.Events; 5 | using Moq; 6 | 7 | namespace FirstPopCoffee.RoastPlanning.Tests { 8 | 9 | public abstract class EventSpecification 10 | where TAggregateRoot : EventSourcedAggregateRoot, new() 11 | where TEvent : Event { 12 | 13 | protected virtual IEnumerable Given() { 14 | return new List(); 15 | } 16 | protected abstract TEvent When(); 17 | protected abstract IEventHandler EventHandler(); 18 | protected TAggregateRoot AggregateRoot; 19 | protected Mock> MockRepository; 20 | 21 | // collects published events for assertion 22 | protected IEnumerable PublishedEvents; 23 | protected Exception CaughtException; 24 | 25 | protected EventSpecification() { 26 | 27 | AggregateRoot = new TAggregateRoot(); 28 | AggregateRoot.LoadFromHistory(Given()); 29 | 30 | MockRepository = new Mock>(); 31 | MockRepository.Setup(x => x.GetById(It.IsAny())).Returns(AggregateRoot); 32 | MockRepository.Setup(x => x.Save(It.IsAny(), It.IsAny())) 33 | .Callback((x, _) => AggregateRoot = x); 34 | 35 | try { 36 | EventHandler().Handle(When()); 37 | PublishedEvents = new List(AggregateRoot.GetUncommittedChanges()); 38 | } 39 | catch (Exception exception) { 40 | CaughtException = exception; 41 | } 42 | 43 | } 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /RoastPlanning.Tests/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("RoastPlanning.Tests")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("Cambridge International, Inc")] 12 | [assembly: AssemblyProduct("RoastPlanning.Tests")] 13 | [assembly: AssemblyCopyright("Copyright © Cambridge International, Inc 2016")] 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("e6e81603-66f5-441c-bb1a-7316f025c6bf")] 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 | -------------------------------------------------------------------------------- /RoastPlanning.Tests/RoastPlanning.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {E6E81603-66F5-441C-BB1A-7316F025C6BF} 8 | Library 9 | Properties 10 | FirstPopCoffee.RoastPlanning.Tests 11 | FirstPopCoffee.RoastPlanning.Tests 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\Castle.Core.3.3.3\lib\net45\Castle.Core.dll 35 | True 36 | 37 | 38 | ..\packages\FluentAssertions.4.16.0\lib\net45\FluentAssertions.dll 39 | True 40 | 41 | 42 | ..\packages\FluentAssertions.4.16.0\lib\net45\FluentAssertions.Core.dll 43 | True 44 | 45 | 46 | ..\packages\Moq.4.5.23\lib\net45\Moq.dll 47 | True 48 | 49 | 50 | ..\packages\ReflectionMagic.2.1.0\lib\net40\ReflectionMagic.dll 51 | True 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | ..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll 63 | True 64 | 65 | 66 | ..\packages\xunit.assert.2.1.0\lib\dotnet\xunit.assert.dll 67 | True 68 | 69 | 70 | ..\packages\xunit.extensibility.core.2.1.0\lib\dotnet\xunit.core.dll 71 | True 72 | 73 | 74 | ..\packages\xunit.extensibility.execution.2.1.0\lib\net45\xunit.execution.desktop.dll 75 | True 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | {3F9E80F7-9744-4B2E-8248-D80E2ED7E54F} 98 | Common 99 | 100 | 101 | {907EB8AA-9306-4BE6-81CD-08216347FA20} 102 | RoastPlanning 103 | 104 | 105 | 106 | 107 | 108 | 109 | 116 | -------------------------------------------------------------------------------- /RoastPlanning.Tests/RoastPlanning.Tests.v2.ncrunchproject: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heynickc/firstpopcoffee/ca6174066ae81c461ccc66b1941d4a34aa373dd0/RoastPlanning.Tests/RoastPlanning.Tests.v2.ncrunchproject -------------------------------------------------------------------------------- /RoastPlanning.Tests/Scenarios/Adding_a_new_roast_schedule/When_starting_a_new_roast_schedule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using FirstPopCoffee.Common.Domain.Model; 4 | using FirstPopCoffee.RoastPlanning.Application; 5 | using FirstPopCoffee.RoastPlanning.Domain.Model; 6 | using FluentAssertions; 7 | 8 | namespace FirstPopCoffee.RoastPlanning.Tests.Scenarios.Adding_a_new_roast_schedule { 9 | public class When_starting_to_create_a_new_roast_schedule : CommandSpecification { 10 | 11 | private readonly Guid _roastScheduleId = Guid.NewGuid(); 12 | 13 | protected override CreateNewRoastScheduleCommand When() { 14 | return new CreateNewRoastScheduleCommand(_roastScheduleId); 15 | } 16 | 17 | protected override ICommandHandler CommandHandler() { 18 | return new RoastScheduleCommandHandlers(MockRepository.Object); 19 | } 20 | 21 | [Then] 22 | public void Then_a_roast_schedule_is_created() { 23 | PublishedEvents.Should().NotBeNullOrEmpty(); 24 | PublishedEvents.Last().Should().BeOfType(); 25 | PublishedEvents.Last().As().RoastScheduleId.Should().Be(_roastScheduleId); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /RoastPlanning.Tests/Scenarios/Choosing_roast_days_for_roast_schedule/When_choosing_roast_days_for_roast_schedule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using FirstPopCoffee.Common.Domain.Model; 5 | using FirstPopCoffee.Common.Events; 6 | using FirstPopCoffee.RoastPlanning.Application; 7 | using FirstPopCoffee.RoastPlanning.Domain.Model; 8 | using FluentAssertions; 9 | 10 | namespace FirstPopCoffee.RoastPlanning.Tests.Scenarios.Choosing_roast_days_for_roast_schedule { 11 | public class When_choosing_roast_days_for_roast_schedule : CommandSpecification { 12 | 13 | private readonly Guid Id = Guid.NewGuid(); 14 | 15 | protected override IEnumerable Given() { 16 | yield return PrepareEvent.Set( 17 | new RoastScheduleCreatedEvent(Id, DateTime.Now.StartOfWeek(DayOfWeek.Sunday))).ToVersion(0); 18 | } 19 | 20 | protected override ChooseRoastDaysForRoastScheduleCommand When() { 21 | var roastDays = new[] { 22 | DayOfWeek.Monday, 23 | DayOfWeek.Tuesday, 24 | DayOfWeek.Thursday, 25 | DayOfWeek.Friday, 26 | DayOfWeek.Saturday 27 | }; 28 | return new ChooseRoastDaysForRoastScheduleCommand(Id, roastDays, 0); 29 | } 30 | 31 | protected override ICommandHandler CommandHandler() { 32 | return new RoastScheduleCommandHandlers(MockRepository.Object); 33 | } 34 | 35 | [Then] 36 | public void Then_days_for_roast_schedule_are_chosen() { 37 | PublishedEvents.Last().Should().BeOfType(); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /RoastPlanning.Tests/Scenarios/Choosing_roast_days_for_roast_schedule/When_choosing_roast_days_for_roast_schedule_using_event_store.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using FirstPopCoffee.Common.Domain.Model; 5 | using FirstPopCoffee.Common.Events; 6 | using FirstPopCoffee.RoastPlanning.Application; 7 | using FirstPopCoffee.RoastPlanning.Domain.Model; 8 | using FluentAssertions; 9 | 10 | namespace FirstPopCoffee.RoastPlanning.Tests.Scenarios.Choosing_roast_days_for_roast_schedule { 11 | 12 | //public class When_choosing_roast_days_for_roast_schedule_using_event_store : SpecificationNoMocks { 13 | 14 | // private readonly Guid Id = Guid.NewGuid(); 15 | 16 | // protected override IEnumerable Given() { 17 | // yield return PrepareEvent.Set( 18 | // new RoastScheduleCreatedEvent(Id, DateTime.Now.StartOfWeek(DayOfWeek.Sunday))).ToVersion(1); 19 | // } 20 | 21 | // protected override ChooseRoastDaysForRoastScheduleCommand When() { 22 | // var roastDays = new[] { 23 | // DayOfWeek.Monday, 24 | // DayOfWeek.Tuesday, 25 | // DayOfWeek.Thursday, 26 | // DayOfWeek.Friday, 27 | // DayOfWeek.Saturday 28 | // }; 29 | // return new ChooseRoastDaysForRoastScheduleCommand(Id, roastDays, 1); 30 | // } 31 | 32 | // protected override ICommandHandler CommandHandler() { 33 | // return new ChooseRoastDaysForRoastScheduleCommandHandler(Repository); 34 | // } 35 | 36 | // [Then(Skip = "Not working yet")] 37 | // public void Then_days_for_roast_schedule_are_chosen() { 38 | // PublishedEvents.Last().Should().BeOfType(); 39 | // } 40 | //} 41 | } -------------------------------------------------------------------------------- /RoastPlanning.Tests/SpecificationNoMocks.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using FirstPopCoffee.Common.Domain.Model; 4 | using FirstPopCoffee.Common.Events; 5 | using ReflectionMagic; 6 | 7 | namespace FirstPopCoffee.RoastPlanning.Tests { 8 | 9 | public abstract class SpecificationNoMocks 10 | where TAggregateRoot : EventSourcedAggregateRoot, new() 11 | where TCommand : Command { 12 | 13 | protected virtual IEnumerable Given() { 14 | return new List(); 15 | } 16 | protected abstract TCommand When(); 17 | protected abstract ICommandHandler CommandHandler(); 18 | protected TAggregateRoot AggregateRoot; 19 | protected IRepository Repository; 20 | 21 | // collects published events for assertion 22 | protected IEnumerable PublishedEvents; 23 | protected Exception CaughtException; 24 | 25 | protected SpecificationNoMocks() { 26 | 27 | Repository = new EventSourcedRepository( 28 | new EventStore( 29 | new FakeBus())); 30 | 31 | AggregateRoot = new TAggregateRoot(); 32 | foreach (var @event in Given()) { 33 | AggregateRoot.AsDynamic().Apply(@event); 34 | 35 | } 36 | 37 | Repository.Save(AggregateRoot, 1); 38 | 39 | try { 40 | CommandHandler().Handle(When()); 41 | PublishedEvents = new List(AggregateRoot.GetUncommittedChanges()); 42 | } 43 | catch (Exception exception) { 44 | CaughtException = exception; 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /RoastPlanning.Tests/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /RoastPlanning/Application/RoastScheduleCommandHandlers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using FirstPopCoffee.Common.Domain.Model; 7 | using FirstPopCoffee.RoastPlanning.Domain.Model; 8 | 9 | namespace FirstPopCoffee.RoastPlanning.Application 10 | { 11 | public class RoastScheduleCommandHandlers : ICommandHandler, ICommandHandler 12 | { 13 | private readonly IRepository _repository; 14 | 15 | public RoastScheduleCommandHandlers(IRepository repository) 16 | { 17 | _repository = repository; 18 | } 19 | 20 | public void Handle(CreateNewRoastScheduleCommand message) 21 | { 22 | var roastWeekStartsOn = DateTime.Now.StartOfWeek(DayOfWeek.Sunday); 23 | var roastSchedule = new RoastSchedule(message.RoastScheduleId, roastWeekStartsOn); 24 | _repository.Save(roastSchedule, -1); 25 | } 26 | 27 | public void Handle(ChooseRoastDaysForRoastScheduleCommand message) 28 | { 29 | var roastSchedule = _repository.GetById(message.Id); 30 | roastSchedule.SetRoastDays(new RoastDays(message.RoastDays)); 31 | _repository.Save(roastSchedule, message.OriginalVersion); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /RoastPlanning/Domain.Model/ChooseRoastDaysForRoastScheduleCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FirstPopCoffee.Common.Domain.Model; 3 | 4 | namespace FirstPopCoffee.RoastPlanning.Domain.Model { 5 | 6 | public class ChooseRoastDaysForRoastScheduleCommand : Command { 7 | public readonly DayOfWeek[] RoastDays; 8 | public readonly int OriginalVersion; 9 | public ChooseRoastDaysForRoastScheduleCommand(Guid id, DayOfWeek[] roastDays, int originalVersion) { 10 | RoastDays = roastDays; 11 | OriginalVersion = originalVersion; 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /RoastPlanning/Domain.Model/CreateNewRoastScheduleCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FirstPopCoffee.Common.Domain.Model; 3 | 4 | namespace FirstPopCoffee.RoastPlanning.Domain.Model { 5 | public class CreateNewRoastScheduleCommand : Command { 6 | public readonly Guid RoastScheduleId; 7 | public CreateNewRoastScheduleCommand(Guid roastScheduleId) { 8 | RoastScheduleId = roastScheduleId; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /RoastPlanning/Domain.Model/RoastDays.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using FirstPopCoffee.Common.Domain.Model; 4 | 5 | namespace FirstPopCoffee.RoastPlanning.Domain.Model { 6 | public class RoastDays : ValueObject { 7 | public HashSet Days { get; private set; } 8 | public RoastDays(HashSet days) { 9 | Days = days; 10 | } 11 | public RoastDays(DayOfWeek[] days) { 12 | Days = new HashSet(); 13 | foreach (var day in days) { 14 | Days.Add(new RoastDay(day)); 15 | } 16 | } 17 | protected override IEnumerable GetAllAttributesToBeUsedForEquality() { 18 | return new List() { new SetByValue(this.Days) }; 19 | } 20 | } 21 | 22 | public class RoastDay : ValueObject { 23 | public DayOfWeek Day { get; private set; } 24 | public RoastDay(DayOfWeek day) { 25 | Day = day; 26 | } 27 | protected override IEnumerable GetAllAttributesToBeUsedForEquality() { 28 | return new List() { this.Day }; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /RoastPlanning/Domain.Model/RoastSchedule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using FirstPopCoffee.Common.Domain.Model; 5 | 6 | namespace FirstPopCoffee.RoastPlanning.Domain.Model { 7 | public class RoastSchedule : EventSourcedAggregateRoot { 8 | 9 | public DateTime RoastWeekStartsOn { get; private set; } 10 | public RoastDays RoastDays { get; private set; } 11 | 12 | public void Apply(RoastScheduleCreatedEvent e) 13 | { 14 | RoastWeekStartsOn = e.RoastWeekStartsOn; 15 | Id = e.RoastScheduleId; 16 | } 17 | 18 | public void Apply(RoastScheduleRoastDaysChosenEvent e) { 19 | RoastDays = new RoastDays(e.Days); 20 | } 21 | 22 | public RoastSchedule(Guid id, DateTime roastWeekStartsOn) { 23 | RoastDays = new RoastDays(new HashSet()); 24 | ApplyChange(new RoastScheduleCreatedEvent(id, roastWeekStartsOn)); 25 | } 26 | 27 | public void SetRoastDays(RoastDays roastDays) { 28 | if (roastDays.Days.Count == 0) throw new ArgumentException("roastDays count must be greater than 0"); 29 | var newDays = roastDays.Days.Select(x => x.Day).ToArray(); 30 | ApplyChange(new RoastScheduleRoastDaysChosenEvent(Id, newDays)); 31 | } 32 | 33 | public RoastSchedule() { 34 | // used to create in repository ... many ways to avoid this, eg making private constructor 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /RoastPlanning/Domain.Model/RoastScheduleCreatedEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FirstPopCoffee.Common.Domain.Model; 3 | using FirstPopCoffee.Common.Events; 4 | 5 | namespace FirstPopCoffee.RoastPlanning.Domain.Model { 6 | public class RoastScheduleCreatedEvent : Event { 7 | public readonly Guid RoastScheduleId; 8 | public readonly DateTime RoastWeekStartsOn; 9 | public RoastScheduleCreatedEvent(Guid roastScheduleId,DateTime roastWeekStartsOn) { 10 | RoastScheduleId = roastScheduleId; 11 | RoastWeekStartsOn = roastWeekStartsOn; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /RoastPlanning/Domain.Model/RoastScheduleId.cs: -------------------------------------------------------------------------------- 1 | using FirstPopCoffee.Common.Domain.Model; 2 | 3 | namespace FirstPopCoffee.RoastPlanning.Domain.Model { 4 | public class RoastScheduleId : Identity { 5 | public RoastScheduleId() 6 | : base() { 7 | } 8 | 9 | public RoastScheduleId(string id) 10 | : base(id) { 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /RoastPlanning/Domain.Model/RoastScheduleRoastDaysChosenEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FirstPopCoffee.Common.Domain.Model; 3 | using FirstPopCoffee.Common.Events; 4 | 5 | namespace FirstPopCoffee.RoastPlanning.Domain.Model { 6 | public class RoastScheduleRoastDaysChosenEvent : Event { 7 | public Guid RoastScheduleId { get; private set; } 8 | public DayOfWeek[] Days { get; private set; } 9 | public RoastScheduleRoastDaysChosenEvent(Guid roastScheduleId, DayOfWeek[] days) { 10 | RoastScheduleId = roastScheduleId; 11 | Days = days; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /RoastPlanning/Domain.Model/RoastScheduleStartOfWeekService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace FirstPopCoffee.RoastPlanning.Domain.Model 8 | { 9 | public static class RoastScheduleStartOfWeekService 10 | { 11 | public static DateTime StartOfWeek(this DateTime dt, DayOfWeek startOfWeek) 12 | { 13 | int diff = dt.DayOfWeek - startOfWeek; 14 | if (diff < 0) 15 | { 16 | diff += 7; 17 | } 18 | return dt.AddDays(-1 * diff).Date; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /RoastPlanning/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("RoastPlanning")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("Cambridge International, Inc")] 12 | [assembly: AssemblyProduct("RoastPlanning")] 13 | [assembly: AssemblyCopyright("Copyright © Cambridge International, Inc 2016")] 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("907eb8aa-9306-4be6-81cd-08216347fa20")] 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 | -------------------------------------------------------------------------------- /RoastPlanning/RoastPlanning.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {907EB8AA-9306-4BE6-81CD-08216347FA20} 8 | Library 9 | Properties 10 | FirstPopCoffee.RoastPlanning 11 | FirstPopCoffee.RoastPlanning 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 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | {3F9E80F7-9744-4B2E-8248-D80E2ED7E54F} 57 | Common 58 | 59 | 60 | 61 | 62 | 63 | 64 | 71 | -------------------------------------------------------------------------------- /RoastPlanning/RoastPlanning.v2.ncrunchproject: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heynickc/firstpopcoffee/ca6174066ae81c461ccc66b1941d4a34aa373dd0/RoastPlanning/RoastPlanning.v2.ncrunchproject -------------------------------------------------------------------------------- /WebUI.Tests/IoCTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using FirstPopCoffee.Common; 7 | using FirstPopCoffee.Common.Domain.Model; 8 | using FirstPopCoffee.Common.Events; 9 | using FirstPopCoffee.RoastPlanning.Domain.Model; 10 | using Ninject; 11 | using Ninject.Extensions.Conventions; 12 | using WebUI.DependencyResolution; 13 | using WebUI.Infrastructure; 14 | using WebUI.Models; 15 | using WebUI.ReadModel; 16 | using Xunit; 17 | using Xunit.Abstractions; 18 | 19 | namespace WebUI.Tests 20 | { 21 | public class IoCTests 22 | { 23 | private readonly ITestOutputHelper _output; 24 | public IoCTests(ITestOutputHelper output) 25 | { 26 | _output = output; 27 | } 28 | 29 | [Fact] 30 | public void Bind_EventStore() 31 | { 32 | using (var kernel = new StandardKernel(new RoastPlanningNinjectModule())) 33 | { 34 | var eventStore = kernel.Get(); 35 | Assert.IsType(eventStore); 36 | } 37 | } 38 | 39 | [Fact] 40 | public void Bind_EventSourcedRepository() 41 | { 42 | using (var kernel = new StandardKernel(new RoastPlanningNinjectModule())) 43 | { 44 | var eventSourcedRepository = kernel.Get>(); 45 | Assert.IsType>(eventSourcedRepository); 46 | } 47 | } 48 | 49 | [Fact] 50 | public void Bind_RoastPlanningContext() 51 | { 52 | using (var kernel = new StandardKernel(new RoastPlanningNinjectModule())) 53 | { 54 | var roastPlanningContext = kernel.Get(); 55 | Assert.IsType(roastPlanningContext); 56 | } 57 | } 58 | 59 | [Fact] 60 | public void Bind_RoastScheduleView() 61 | { 62 | using (var kernel = new StandardKernel(new RoastPlanningNinjectModule())) 63 | { 64 | var roastScheduleView = kernel.Get(); 65 | Assert.IsType(roastScheduleView); 66 | } 67 | } 68 | 69 | [Fact] 70 | public void Bind_EventPublisherCommandSender() 71 | { 72 | using (var kernel = new StandardKernel(new RoastPlanningNinjectModule())) 73 | { 74 | var fakeBusAsPublisher = kernel.Get(); 75 | var fakeBusAsSender = kernel.Get(); 76 | Assert.IsType(fakeBusAsPublisher); 77 | Assert.IsType(fakeBusAsSender); 78 | } 79 | } 80 | 81 | //[Fact] 82 | //public void Bind_FakeBus() 83 | //{ 84 | // using (var kernel = new StandardKernel(new RoastPlanningNinjectModule())) 85 | // { 86 | // var bus = kernel.Get(); 87 | // Assert.IsType(bus); 88 | // } 89 | //} 90 | 91 | [Fact] 92 | public void Bind_RoastPlanningReadModel() 93 | { 94 | using (var kernel = new StandardKernel(new RoastPlanningNinjectModule())) 95 | { 96 | var readModel = kernel.Get(); 97 | Assert.IsType(readModel); 98 | } 99 | } 100 | 101 | [Fact] 102 | public void RoastModelView_same_DbSet_as_RoastPlanningContext() 103 | { 104 | using (var kernel = new StandardKernel(new RoastPlanningNinjectModule())) 105 | { 106 | var readModelDbContext = kernel.Get(); 107 | var readModel = kernel.Get(); 108 | readModelDbContext.RoastSchedules.Add(new RoastScheduleViewModel(Guid.NewGuid(), DateTime.Now)); 109 | 110 | Assert.Same(readModelDbContext.RoastSchedules, readModel.RoastSchedules); 111 | Assert.Equal( 112 | readModel.RoastSchedules.ToList().Count, 113 | readModelDbContext.RoastSchedules.ToList().Count); 114 | } 115 | } 116 | 117 | [Fact] 118 | public void FakeBus_is_same_as_IEventPublisher_Binding() 119 | { 120 | using (var kernel = new StandardKernel(new RoastPlanningNinjectModule())) 121 | { 122 | var fakeBusAsPublisher = kernel.Get(); 123 | var fakeBusAsSender = kernel.Get(); 124 | Assert.IsType(fakeBusAsPublisher); 125 | Assert.IsType(fakeBusAsSender); 126 | 127 | Assert.Same(fakeBusAsPublisher, fakeBusAsSender); 128 | } 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /WebUI.Tests/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("WebUI.Tests")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("Cambridge International, Inc")] 12 | [assembly: AssemblyProduct("WebUI.Tests")] 13 | [assembly: AssemblyCopyright("Copyright © Cambridge International, Inc 2017")] 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("d0e3d322-1ecc-485f-86d0-0a88a066a56f")] 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 | -------------------------------------------------------------------------------- /WebUI.Tests/WebUI.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {D0E3D322-1ECC-485F-86D0-0A88A066A56F} 8 | Library 9 | Properties 10 | WebUI.Tests 11 | WebUI.Tests 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\Castle.Core.3.2.0\lib\net45\Castle.Core.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\Ninject.3.2.0.0\lib\net45-full\Ninject.dll 47 | True 48 | 49 | 50 | ..\packages\Ninject.Extensions.Conventions.3.2.0.0\lib\net45-full\Ninject.Extensions.Conventions.dll 51 | True 52 | 53 | 54 | ..\packages\Ninject.Extensions.Factory.3.2.1.0\lib\net45-full\Ninject.Extensions.Factory.dll 55 | True 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | ..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll 68 | True 69 | 70 | 71 | ..\packages\xunit.assert.2.1.0\lib\dotnet\xunit.assert.dll 72 | True 73 | 74 | 75 | ..\packages\xunit.extensibility.core.2.1.0\lib\dotnet\xunit.core.dll 76 | True 77 | 78 | 79 | ..\packages\xunit.extensibility.execution.2.1.0\lib\net45\xunit.execution.desktop.dll 80 | True 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | {3F9E80F7-9744-4B2E-8248-D80E2ED7E54F} 94 | Common 95 | 96 | 97 | {907EB8AA-9306-4BE6-81CD-08216347FA20} 98 | RoastPlanning 99 | 100 | 101 | {b9dbc432-9b90-449d-a623-e0c6376d259b} 102 | WebUI 103 | 104 | 105 | 106 | 113 | -------------------------------------------------------------------------------- /WebUI.Tests/WebUI.Tests.v2.ncrunchproject: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heynickc/firstpopcoffee/ca6174066ae81c461ccc66b1941d4a34aa373dd0/WebUI.Tests/WebUI.Tests.v2.ncrunchproject -------------------------------------------------------------------------------- /WebUI.Tests/app.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 | -------------------------------------------------------------------------------- /WebUI.Tests/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /WebUI/App_Start/BundleConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Web; 2 | using System.Web.Optimization; 3 | 4 | namespace WebUI { 5 | public class BundleConfig { 6 | // For more information on bundling, visit http://go.microsoft.com/fwlink/?LinkId=301862 7 | public static void RegisterBundles(BundleCollection bundles) { 8 | bundles.Add(new ScriptBundle("~/bundles/jquery").Include( 9 | "~/Scripts/jquery-{version}.js")); 10 | 11 | bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include( 12 | "~/Scripts/jquery.validate*")); 13 | 14 | // Use the development version of Modernizr to develop with and learn from. Then, when you're 15 | // ready for production, use the build tool at http://modernizr.com to pick only the tests you need. 16 | bundles.Add(new ScriptBundle("~/bundles/modernizr").Include( 17 | "~/Scripts/modernizr-*")); 18 | 19 | bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include( 20 | "~/Scripts/bootstrap.js", 21 | "~/Scripts/respond.js")); 22 | 23 | bundles.Add(new StyleBundle("~/Content/css").Include( 24 | "~/Content/bootstrap-flatly.min.css", 25 | "~/Content/site.css")); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /WebUI/App_Start/FilterConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Web; 2 | using System.Web.Mvc; 3 | 4 | namespace WebUI { 5 | public class FilterConfig { 6 | public static void RegisterGlobalFilters(GlobalFilterCollection filters) { 7 | filters.Add(new HandleErrorAttribute()); 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /WebUI/App_Start/IdentityConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data.Entity; 4 | using System.Linq; 5 | using System.Security.Claims; 6 | using System.Threading.Tasks; 7 | using System.Web; 8 | using Microsoft.AspNet.Identity; 9 | using Microsoft.AspNet.Identity.EntityFramework; 10 | using Microsoft.AspNet.Identity.Owin; 11 | using Microsoft.Owin; 12 | using Microsoft.Owin.Security; 13 | using WebUI.Models; 14 | 15 | namespace WebUI 16 | { 17 | public class EmailService : IIdentityMessageService 18 | { 19 | public Task SendAsync(IdentityMessage message) 20 | { 21 | // Plug in your email service here to send an email. 22 | return Task.FromResult(0); 23 | } 24 | } 25 | 26 | public class SmsService : IIdentityMessageService 27 | { 28 | public Task SendAsync(IdentityMessage message) 29 | { 30 | // Plug in your SMS service here to send a text message. 31 | return Task.FromResult(0); 32 | } 33 | } 34 | 35 | // Configure the application user manager used in this application. UserManager is defined in ASP.NET Identity and is used by the application. 36 | public class ApplicationUserManager : UserManager 37 | { 38 | public ApplicationUserManager(IUserStore store) 39 | : base(store) 40 | { 41 | } 42 | 43 | public static ApplicationUserManager Create(IdentityFactoryOptions options, IOwinContext context) 44 | { 45 | var manager = new ApplicationUserManager(new UserStore(context.Get())); 46 | // Configure validation logic for usernames 47 | manager.UserValidator = new UserValidator(manager) 48 | { 49 | AllowOnlyAlphanumericUserNames = false, 50 | RequireUniqueEmail = true 51 | }; 52 | 53 | // Configure validation logic for passwords 54 | manager.PasswordValidator = new PasswordValidator 55 | { 56 | RequiredLength = 6, 57 | RequireNonLetterOrDigit = true, 58 | RequireDigit = true, 59 | RequireLowercase = true, 60 | RequireUppercase = true, 61 | }; 62 | 63 | // Configure user lockout defaults 64 | manager.UserLockoutEnabledByDefault = true; 65 | manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5); 66 | manager.MaxFailedAccessAttemptsBeforeLockout = 5; 67 | 68 | // Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user 69 | // You can write your own provider and plug it in here. 70 | manager.RegisterTwoFactorProvider("Phone Code", new PhoneNumberTokenProvider 71 | { 72 | MessageFormat = "Your security code is {0}" 73 | }); 74 | manager.RegisterTwoFactorProvider("Email Code", new EmailTokenProvider 75 | { 76 | Subject = "Security Code", 77 | BodyFormat = "Your security code is {0}" 78 | }); 79 | manager.EmailService = new EmailService(); 80 | manager.SmsService = new SmsService(); 81 | var dataProtectionProvider = options.DataProtectionProvider; 82 | if (dataProtectionProvider != null) 83 | { 84 | manager.UserTokenProvider = 85 | new DataProtectorTokenProvider(dataProtectionProvider.Create("ASP.NET Identity")); 86 | } 87 | return manager; 88 | } 89 | } 90 | 91 | // Configure the application sign-in manager which is used in this application. 92 | public class ApplicationSignInManager : SignInManager 93 | { 94 | public ApplicationSignInManager(ApplicationUserManager userManager, IAuthenticationManager authenticationManager) 95 | : base(userManager, authenticationManager) 96 | { 97 | } 98 | 99 | public override Task CreateUserIdentityAsync(ApplicationUser user) 100 | { 101 | return user.GenerateUserIdentityAsync((ApplicationUserManager)UserManager); 102 | } 103 | 104 | public static ApplicationSignInManager Create(IdentityFactoryOptions options, IOwinContext context) 105 | { 106 | return new ApplicationSignInManager(context.GetUserManager(), context.Authentication); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /WebUI/App_Start/NinjectWebCommon.cs: -------------------------------------------------------------------------------- 1 | using WebUI.DependencyResolution; 2 | using WebUI.Infrastructure; 3 | 4 | [assembly: WebActivatorEx.PreApplicationStartMethod(typeof(WebUI.App_Start.NinjectWebCommon), "Start")] 5 | [assembly: WebActivatorEx.ApplicationShutdownMethodAttribute(typeof(WebUI.App_Start.NinjectWebCommon), "Stop")] 6 | 7 | namespace WebUI.App_Start 8 | { 9 | using System; 10 | using System.Web; 11 | 12 | using Microsoft.Web.Infrastructure.DynamicModuleHelper; 13 | 14 | using Ninject; 15 | using Ninject.Web.Common; 16 | 17 | public static class NinjectWebCommon 18 | { 19 | private static readonly Bootstrapper bootstrapper = new Bootstrapper(); 20 | 21 | /// 22 | /// Starts the application 23 | /// 24 | public static void Start() 25 | { 26 | DynamicModuleUtility.RegisterModule(typeof(OnePerRequestHttpModule)); 27 | DynamicModuleUtility.RegisterModule(typeof(NinjectHttpModule)); 28 | bootstrapper.Initialize(CreateKernel); 29 | } 30 | 31 | /// 32 | /// Stops the application. 33 | /// 34 | public static void Stop() 35 | { 36 | bootstrapper.ShutDown(); 37 | } 38 | 39 | /// 40 | /// Creates the kernel that will manage your application. 41 | /// 42 | /// The created kernel. 43 | private static IKernel CreateKernel() 44 | { 45 | var kernel = new StandardKernel(); 46 | try 47 | { 48 | kernel.Bind>().ToMethod(ctx => () => new Bootstrapper().Kernel); 49 | kernel.Bind().To(); 50 | 51 | RegisterServices(kernel); 52 | return kernel; 53 | } 54 | catch 55 | { 56 | kernel.Dispose(); 57 | throw; 58 | } 59 | } 60 | 61 | /// 62 | /// Load your modules or register your services here! 63 | /// 64 | /// The kernel. 65 | private static void RegisterServices(IKernel kernel) 66 | { 67 | kernel.Load(); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /WebUI/App_Start/RouteConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Web; 5 | using System.Web.Mvc; 6 | using System.Web.Routing; 7 | 8 | namespace WebUI { 9 | public class RouteConfig { 10 | public static void RegisterRoutes(RouteCollection routes) { 11 | routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 12 | 13 | routes.MapRoute( 14 | name: "Default", 15 | url: "{controller}/{action}/{id}", 16 | defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } 17 | ); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /WebUI/App_Start/Startup.Auth.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNet.Identity; 3 | using Microsoft.AspNet.Identity.Owin; 4 | using Microsoft.Owin; 5 | using Microsoft.Owin.Security.Cookies; 6 | using Microsoft.Owin.Security.Google; 7 | using Owin; 8 | using WebUI.Models; 9 | 10 | namespace WebUI 11 | { 12 | public partial class Startup 13 | { 14 | // For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864 15 | public void ConfigureAuth(IAppBuilder app) 16 | { 17 | // Configure the db context, user manager and signin manager to use a single instance per request 18 | app.CreatePerOwinContext(ApplicationDbContext.Create); 19 | app.CreatePerOwinContext(ApplicationUserManager.Create); 20 | app.CreatePerOwinContext(ApplicationSignInManager.Create); 21 | 22 | // Enable the application to use a cookie to store information for the signed in user 23 | // and to use a cookie to temporarily store information about a user logging in with a third party login provider 24 | // Configure the sign in cookie 25 | app.UseCookieAuthentication(new CookieAuthenticationOptions 26 | { 27 | AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, 28 | LoginPath = new PathString("/Account/Login"), 29 | Provider = new CookieAuthenticationProvider 30 | { 31 | // Enables the application to validate the security stamp when the user logs in. 32 | // This is a security feature which is used when you change a password or add an external login to your account. 33 | OnValidateIdentity = SecurityStampValidator.OnValidateIdentity( 34 | validateInterval: TimeSpan.FromMinutes(30), 35 | regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager)) 36 | } 37 | }); 38 | app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); 39 | 40 | // Enables the application to temporarily store user information when they are verifying the second factor in the two-factor authentication process. 41 | app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5)); 42 | 43 | // Enables the application to remember the second login verification factor such as phone or email. 44 | // Once you check this option, your second step of verification during the login process will be remembered on the device where you logged in from. 45 | // This is similar to the RememberMe option when you log in. 46 | app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie); 47 | 48 | // Uncomment the following lines to enable logging in with third party login providers 49 | //app.UseMicrosoftAccountAuthentication( 50 | // clientId: "", 51 | // clientSecret: ""); 52 | 53 | //app.UseTwitterAuthentication( 54 | // consumerKey: "", 55 | // consumerSecret: ""); 56 | 57 | //app.UseFacebookAuthentication( 58 | // appId: "", 59 | // appSecret: ""); 60 | 61 | //app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions() 62 | //{ 63 | // ClientId = "", 64 | // ClientSecret = "" 65 | //}); 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /WebUI/Content/Site.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | padding-bottom: 20px; 4 | } 5 | 6 | /* Set padding to keep content from hitting the edges */ 7 | .body-content { 8 | padding-left: 15px; 9 | padding-right: 15px; 10 | } 11 | 12 | /* Override the default bootstrap behavior where horizontal description lists 13 | will truncate terms that are too long to fit in the left column 14 | */ 15 | .dl-horizontal dt { 16 | white-space: normal; 17 | } 18 | 19 | /* Set width on the form input elements since they're 100% wide by default */ 20 | input, 21 | select, 22 | textarea { 23 | max-width: 280px; 24 | } 25 | -------------------------------------------------------------------------------- /WebUI/DependencyResolution/RoastPlanningNinjectModule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using FirstPopCoffee.Common.Domain.Model; 4 | using FirstPopCoffee.Common.Events; 5 | using FirstPopCoffee.RoastPlanning.Application; 6 | using FirstPopCoffee.RoastPlanning.Domain.Model; 7 | using Ninject; 8 | using Ninject.Extensions.Factory; 9 | using Ninject.Modules; 10 | using WebUI.Infrastructure; 11 | using WebUI.Models; 12 | using WebUI.ReadModel; 13 | 14 | namespace WebUI.DependencyResolution 15 | { 16 | public class RoastPlanningNinjectModule : NinjectModule 17 | { 18 | public override void Load() 19 | { 20 | Bind().To(); 21 | Bind>().To>(); 22 | 23 | //need to send RoastScheduleContext.RoastSchedules DbSet as argument 24 | Bind>().ToSelf().InSingletonScope(); 25 | Bind().To(); 26 | Bind().To(); 27 | 28 | Bind().To() 29 | .InSingletonScope() 30 | .OnActivation(bus => 31 | { 32 | bus.RegisterHandler( 33 | new RoastScheduleCommandHandlers(Kernel.Get>()).Handle); 34 | bus.RegisterHandler( 35 | new RoastScheduleCommandHandlers(Kernel.Get>()).Handle); 36 | bus.RegisterHandler( 37 | new RoastScheduleView(Kernel.Get()).Handle); 38 | }); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /WebUI/Features/Account/ConfirmEmail.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = "Confirm Email"; 3 | } 4 | 5 |

@ViewBag.Title.

6 |
7 |

8 | Thank you for confirming your email. Please @Html.ActionLink("Click here to Log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" }) 9 |

10 |
11 | -------------------------------------------------------------------------------- /WebUI/Features/Account/ExternalLoginConfirmation.cshtml: -------------------------------------------------------------------------------- 1 | @model WebUI.Models.ExternalLoginConfirmationViewModel 2 | @{ 3 | ViewBag.Title = "Register"; 4 | } 5 |

@ViewBag.Title.

6 |

Associate your @ViewBag.LoginProvider account.

7 | 8 | @using (Html.BeginForm("ExternalLoginConfirmation", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" })) 9 | { 10 | @Html.AntiForgeryToken() 11 | 12 |

Association Form

13 |
14 | @Html.ValidationSummary(true, "", new { @class = "text-danger" }) 15 |

16 | You've successfully authenticated with @ViewBag.LoginProvider. 17 | Please enter a user name for this site below and click the Register button to finish 18 | logging in. 19 |

20 |
21 | @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" }) 22 |
23 | @Html.TextBoxFor(m => m.Email, new { @class = "form-control" }) 24 | @Html.ValidationMessageFor(m => m.Email, "", new { @class = "text-danger" }) 25 |
26 |
27 |
28 |
29 | 30 |
31 |
32 | } 33 | 34 | @section Scripts { 35 | @Scripts.Render("~/bundles/jqueryval") 36 | } 37 | -------------------------------------------------------------------------------- /WebUI/Features/Account/ExternalLoginFailure.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = "Login Failure"; 3 | } 4 | 5 |
6 |

@ViewBag.Title.

7 |

Unsuccessful login with service.

8 |
9 | -------------------------------------------------------------------------------- /WebUI/Features/Account/ForgotPassword.cshtml: -------------------------------------------------------------------------------- 1 | @model WebUI.Models.ForgotPasswordViewModel 2 | @{ 3 | ViewBag.Title = "Forgot your password?"; 4 | } 5 | 6 |

@ViewBag.Title.

7 | 8 | @using (Html.BeginForm("ForgotPassword", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) 9 | { 10 | @Html.AntiForgeryToken() 11 |

Enter your email.

12 |
13 | @Html.ValidationSummary("", new { @class = "text-danger" }) 14 |
15 | @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" }) 16 |
17 | @Html.TextBoxFor(m => m.Email, new { @class = "form-control" }) 18 |
19 |
20 |
21 |
22 | 23 |
24 |
25 | } 26 | 27 | @section Scripts { 28 | @Scripts.Render("~/bundles/jqueryval") 29 | } 30 | -------------------------------------------------------------------------------- /WebUI/Features/Account/ForgotPasswordConfirmation.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = "Forgot Password Confirmation"; 3 | } 4 | 5 |
6 |

@ViewBag.Title.

7 |
8 |
9 |

10 | Please check your email to reset your password. 11 |

12 |
13 | 14 | -------------------------------------------------------------------------------- /WebUI/Features/Account/Login.cshtml: -------------------------------------------------------------------------------- 1 | @using WebUI.Models 2 | @model LoginViewModel 3 | @{ 4 | ViewBag.Title = "Log in"; 5 | } 6 | 7 |

@ViewBag.Title.

8 |
9 |
10 |
11 | @using (Html.BeginForm("Login", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" })) 12 | { 13 | @Html.AntiForgeryToken() 14 |

Use a local account to log in.

15 |
16 | @Html.ValidationSummary(true, "", new { @class = "text-danger" }) 17 |
18 | @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" }) 19 |
20 | @Html.TextBoxFor(m => m.Email, new { @class = "form-control" }) 21 | @Html.ValidationMessageFor(m => m.Email, "", new { @class = "text-danger" }) 22 |
23 |
24 |
25 | @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" }) 26 |
27 | @Html.PasswordFor(m => m.Password, new { @class = "form-control" }) 28 | @Html.ValidationMessageFor(m => m.Password, "", new { @class = "text-danger" }) 29 |
30 |
31 |
32 |
33 |
34 | @Html.CheckBoxFor(m => m.RememberMe) 35 | @Html.LabelFor(m => m.RememberMe) 36 |
37 |
38 |
39 |
40 |
41 | 42 |
43 |
44 |

45 | @Html.ActionLink("Register as a new user", "Register") 46 |

47 | @* Enable this once you have account confirmation enabled for password reset functionality 48 |

49 | @Html.ActionLink("Forgot your password?", "ForgotPassword") 50 |

*@ 51 | } 52 |
53 |
54 |
55 |
56 | @Html.Partial("_ExternalLoginsListPartial", new ExternalLoginListViewModel { ReturnUrl = ViewBag.ReturnUrl }) 57 |
58 |
59 |
60 | 61 | @section Scripts { 62 | @Scripts.Render("~/bundles/jqueryval") 63 | } -------------------------------------------------------------------------------- /WebUI/Features/Account/Register.cshtml: -------------------------------------------------------------------------------- 1 | @model WebUI.Models.RegisterViewModel 2 | @{ 3 | ViewBag.Title = "Register"; 4 | } 5 | 6 |

@ViewBag.Title.

7 | 8 | @using (Html.BeginForm("Register", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) 9 | { 10 | @Html.AntiForgeryToken() 11 |

Create a new account.

12 |
13 | @Html.ValidationSummary("", new { @class = "text-danger" }) 14 |
15 | @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" }) 16 |
17 | @Html.TextBoxFor(m => m.Email, new { @class = "form-control" }) 18 |
19 |
20 |
21 | @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" }) 22 |
23 | @Html.PasswordFor(m => m.Password, new { @class = "form-control" }) 24 |
25 |
26 |
27 | @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" }) 28 |
29 | @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" }) 30 |
31 |
32 |
33 |
34 | 35 |
36 |
37 | } 38 | 39 | @section Scripts { 40 | @Scripts.Render("~/bundles/jqueryval") 41 | } 42 | -------------------------------------------------------------------------------- /WebUI/Features/Account/ResetPassword.cshtml: -------------------------------------------------------------------------------- 1 | @model WebUI.Models.ResetPasswordViewModel 2 | @{ 3 | ViewBag.Title = "Reset password"; 4 | } 5 | 6 |

@ViewBag.Title.

7 | 8 | @using (Html.BeginForm("ResetPassword", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) 9 | { 10 | @Html.AntiForgeryToken() 11 |

Reset your password.

12 |
13 | @Html.ValidationSummary("", new { @class = "text-danger" }) 14 | @Html.HiddenFor(model => model.Code) 15 |
16 | @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" }) 17 |
18 | @Html.TextBoxFor(m => m.Email, new { @class = "form-control" }) 19 |
20 |
21 |
22 | @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" }) 23 |
24 | @Html.PasswordFor(m => m.Password, new { @class = "form-control" }) 25 |
26 |
27 |
28 | @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" }) 29 |
30 | @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" }) 31 |
32 |
33 |
34 |
35 | 36 |
37 |
38 | } 39 | 40 | @section Scripts { 41 | @Scripts.Render("~/bundles/jqueryval") 42 | } 43 | -------------------------------------------------------------------------------- /WebUI/Features/Account/ResetPasswordConfirmation.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = "Reset password confirmation"; 3 | } 4 | 5 |
6 |

@ViewBag.Title.

7 |
8 |
9 |

10 | Your password has been reset. Please @Html.ActionLink("click here to log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" }) 11 |

12 |
13 | -------------------------------------------------------------------------------- /WebUI/Features/Account/SendCode.cshtml: -------------------------------------------------------------------------------- 1 | @model WebUI.Models.SendCodeViewModel 2 | @{ 3 | ViewBag.Title = "Send"; 4 | } 5 | 6 |

@ViewBag.Title.

7 | 8 | @using (Html.BeginForm("SendCode", "Account", new { ReturnUrl = Model.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" })) { 9 | @Html.AntiForgeryToken() 10 | @Html.Hidden("rememberMe", @Model.RememberMe) 11 |

Send verification code

12 |
13 |
14 |
15 | Select Two-Factor Authentication Provider: 16 | @Html.DropDownListFor(model => model.SelectedProvider, Model.Providers) 17 | 18 |
19 |
20 | } 21 | 22 | @section Scripts { 23 | @Scripts.Render("~/bundles/jqueryval") 24 | } 25 | -------------------------------------------------------------------------------- /WebUI/Features/Account/VerifyCode.cshtml: -------------------------------------------------------------------------------- 1 | @model WebUI.Models.VerifyCodeViewModel 2 | @{ 3 | ViewBag.Title = "Verify"; 4 | } 5 | 6 |

@ViewBag.Title.

7 | 8 | @using (Html.BeginForm("VerifyCode", "Account", new { ReturnUrl = Model.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" })) { 9 | @Html.AntiForgeryToken() 10 | @Html.Hidden("provider", @Model.Provider) 11 | @Html.Hidden("rememberMe", @Model.RememberMe) 12 |

Enter verification code

13 |
14 | @Html.ValidationSummary("", new { @class = "text-danger" }) 15 |
16 | @Html.LabelFor(m => m.Code, new { @class = "col-md-2 control-label" }) 17 |
18 | @Html.TextBoxFor(m => m.Code, new { @class = "form-control" }) 19 |
20 |
21 |
22 |
23 |
24 | @Html.CheckBoxFor(m => m.RememberBrowser) 25 | @Html.LabelFor(m => m.RememberBrowser) 26 |
27 |
28 |
29 |
30 |
31 | 32 |
33 |
34 | } 35 | 36 | @section Scripts { 37 | @Scripts.Render("~/bundles/jqueryval") 38 | } 39 | -------------------------------------------------------------------------------- /WebUI/Features/Account/_ExternalLoginsListPartial.cshtml: -------------------------------------------------------------------------------- 1 | @model WebUI.Models.ExternalLoginListViewModel 2 | @using Microsoft.Owin.Security 3 | 4 |

Use another service to log in.

5 |
6 | @{ 7 | var loginProviders = Context.GetOwinContext().Authentication.GetExternalAuthenticationTypes(); 8 | if (loginProviders.Count() == 0) { 9 |
10 |

11 | There are no external authentication services configured. See this article 12 | for details on setting up this ASP.NET application to support logging in via external services. 13 |

14 |
15 | } 16 | else { 17 | using (Html.BeginForm("ExternalLogin", "Account", new { ReturnUrl = Model.ReturnUrl })) { 18 | @Html.AntiForgeryToken() 19 |
20 |

21 | @foreach (AuthenticationDescription p in loginProviders) { 22 | 23 | } 24 |

25 |
26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /WebUI/Features/Home/About.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = "About"; 3 | } 4 |

@ViewBag.Title.

5 |

@ViewBag.Message

6 | 7 |

Use this area to provide additional information.

8 | -------------------------------------------------------------------------------- /WebUI/Features/Home/Contact.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = "Contact"; 3 | } 4 |

@ViewBag.Title.

5 |

@ViewBag.Message

6 | 7 |
8 | One Microsoft Way
9 | Redmond, WA 98052-6399
10 | P: 11 | 425.555.0100 12 |
13 | 14 |
15 | Support: Support@example.com
16 | Marketing: Marketing@example.com 17 |
-------------------------------------------------------------------------------- /WebUI/Features/Home/HomeController.cs: -------------------------------------------------------------------------------- 1 | using System.Web.Mvc; 2 | 3 | namespace WebUI.Features.Home { 4 | public class HomeController : Controller { 5 | 6 | public ActionResult Index() { 7 | return View(); 8 | } 9 | 10 | public ActionResult About() { 11 | ViewBag.Message = "Your application description page."; 12 | 13 | return View(); 14 | } 15 | 16 | public ActionResult Contact() { 17 | ViewBag.Message = "Your contact page."; 18 | 19 | return View(); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /WebUI/Features/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = "Home Page"; 3 | } 4 | 5 |
6 |

ASP.NET

7 |

ASP.NET is a free web framework for building great Web sites and Web applications using HTML, CSS and JavaScript.

8 |

Learn more »

9 |
10 | 11 |
12 |
13 |

Getting started

14 |

15 | ASP.NET MVC gives you a powerful, patterns-based way to build dynamic websites that 16 | enables a clean separation of concerns and gives you full control over markup 17 | for enjoyable, agile development. 18 |

19 |

Learn more »

20 |
21 |
22 |

Get more libraries

23 |

NuGet is a free Visual Studio extension that makes it easy to add, remove, and update libraries and tools in Visual Studio projects.

24 |

Learn more »

25 |
26 |
27 |

Web Hosting

28 |

You can easily find a web hosting company that offers the right mix of features and price for your applications.

29 |

Learn more »

30 |
31 |
-------------------------------------------------------------------------------- /WebUI/Features/Manage/AddPhoneNumber.cshtml: -------------------------------------------------------------------------------- 1 | @model WebUI.Models.AddPhoneNumberViewModel 2 | @{ 3 | ViewBag.Title = "Phone Number"; 4 | } 5 | 6 |

@ViewBag.Title.

7 | 8 | @using (Html.BeginForm("AddPhoneNumber", "Manage", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) 9 | { 10 | @Html.AntiForgeryToken() 11 |

Add a phone number

12 |
13 | @Html.ValidationSummary("", new { @class = "text-danger" }) 14 |
15 | @Html.LabelFor(m => m.Number, new { @class = "col-md-2 control-label" }) 16 |
17 | @Html.TextBoxFor(m => m.Number, new { @class = "form-control" }) 18 |
19 |
20 |
21 |
22 | 23 |
24 |
25 | } 26 | 27 | @section Scripts { 28 | @Scripts.Render("~/bundles/jqueryval") 29 | } 30 | -------------------------------------------------------------------------------- /WebUI/Features/Manage/ChangePassword.cshtml: -------------------------------------------------------------------------------- 1 | @model WebUI.Models.ChangePasswordViewModel 2 | @{ 3 | ViewBag.Title = "Change Password"; 4 | } 5 | 6 |

@ViewBag.Title.

7 | 8 | @using (Html.BeginForm("ChangePassword", "Manage", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) 9 | { 10 | @Html.AntiForgeryToken() 11 |

Change Password Form

12 |
13 | @Html.ValidationSummary("", new { @class = "text-danger" }) 14 |
15 | @Html.LabelFor(m => m.OldPassword, new { @class = "col-md-2 control-label" }) 16 |
17 | @Html.PasswordFor(m => m.OldPassword, new { @class = "form-control" }) 18 |
19 |
20 |
21 | @Html.LabelFor(m => m.NewPassword, new { @class = "col-md-2 control-label" }) 22 |
23 | @Html.PasswordFor(m => m.NewPassword, new { @class = "form-control" }) 24 |
25 |
26 |
27 | @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" }) 28 |
29 | @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" }) 30 |
31 |
32 |
33 |
34 | 35 |
36 |
37 | } 38 | @section Scripts { 39 | @Scripts.Render("~/bundles/jqueryval") 40 | } -------------------------------------------------------------------------------- /WebUI/Features/Manage/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model WebUI.Models.IndexViewModel 2 | @{ 3 | ViewBag.Title = "Manage"; 4 | } 5 | 6 |

@ViewBag.Title.

7 | 8 |

@ViewBag.StatusMessage

9 |
10 |

Change your account settings

11 |
12 |
13 |
Password:
14 |
15 | [ 16 | @if (Model.HasPassword) 17 | { 18 | @Html.ActionLink("Change your password", "ChangePassword") 19 | } 20 | else 21 | { 22 | @Html.ActionLink("Create", "SetPassword") 23 | } 24 | ] 25 |
26 |
External Logins:
27 |
28 | @Model.Logins.Count [ 29 | @Html.ActionLink("Manage", "ManageLogins") ] 30 |
31 | @* 32 | Phone Numbers can used as a second factor of verification in a two-factor authentication system. 33 | 34 | See this article 35 | for details on setting up this ASP.NET application to support two-factor authentication using SMS. 36 | 37 | Uncomment the following block after you have set up two-factor authentication 38 | *@ 39 | @* 40 |
Phone Number:
41 |
42 | @(Model.PhoneNumber ?? "None") 43 | @if (Model.PhoneNumber != null) 44 | { 45 |
46 | [  @Html.ActionLink("Change", "AddPhoneNumber")  ] 47 | using (Html.BeginForm("RemovePhoneNumber", "Manage", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) 48 | { 49 | @Html.AntiForgeryToken() 50 | [] 51 | } 52 | } 53 | else 54 | { 55 | [  @Html.ActionLink("Add", "AddPhoneNumber") 56 | } 57 |
58 | *@ 59 |
Two-Factor Authentication:
60 |
61 |

62 | There are no two-factor authentication providers configured. See this article 63 | for details on setting up this ASP.NET application to support two-factor authentication. 64 |

65 | @*@if (Model.TwoFactor) 66 | { 67 | using (Html.BeginForm("DisableTwoFactorAuthentication", "Manage", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) 68 | { 69 | @Html.AntiForgeryToken() 70 | Enabled 71 | 72 | 73 | } 74 | } 75 | else 76 | { 77 | using (Html.BeginForm("EnableTwoFactorAuthentication", "Manage", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) 78 | { 79 | @Html.AntiForgeryToken() 80 | Disabled 81 | 82 | 83 | } 84 | }*@ 85 |
86 |
87 |
88 | -------------------------------------------------------------------------------- /WebUI/Features/Manage/ManageLogins.cshtml: -------------------------------------------------------------------------------- 1 | @model WebUI.Models.ManageLoginsViewModel 2 | @using Microsoft.Owin.Security 3 | @{ 4 | ViewBag.Title = "Manage your external logins"; 5 | } 6 | 7 |

@ViewBag.Title.

8 | 9 |

@ViewBag.StatusMessage

10 | @{ 11 | var loginProviders = Context.GetOwinContext().Authentication.GetExternalAuthenticationTypes(); 12 | if (loginProviders.Count() == 0) { 13 |
14 |

15 | There are no external authentication services configured. See this article 16 | for details on setting up this ASP.NET application to support logging in via external services. 17 |

18 |
19 | } 20 | else 21 | { 22 | if (Model.CurrentLogins.Count > 0) 23 | { 24 |

Registered Logins

25 | 26 | 27 | @foreach (var account in Model.CurrentLogins) 28 | { 29 | 30 | 31 | 49 | 50 | } 51 | 52 |
@account.LoginProvider 32 | @if (ViewBag.ShowRemoveButton) 33 | { 34 | using (Html.BeginForm("RemoveLogin", "Manage")) 35 | { 36 | @Html.AntiForgeryToken() 37 |
38 | @Html.Hidden("loginProvider", account.LoginProvider) 39 | @Html.Hidden("providerKey", account.ProviderKey) 40 | 41 |
42 | } 43 | } 44 | else 45 | { 46 | @:   47 | } 48 |
53 | } 54 | if (Model.OtherLogins.Count > 0) 55 | { 56 | using (Html.BeginForm("LinkLogin", "Manage")) 57 | { 58 | @Html.AntiForgeryToken() 59 |
60 |

61 | @foreach (AuthenticationDescription p in Model.OtherLogins) 62 | { 63 | 64 | } 65 |

66 |
67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /WebUI/Features/Manage/SetPassword.cshtml: -------------------------------------------------------------------------------- 1 | @model WebUI.Models.SetPasswordViewModel 2 | @{ 3 | ViewBag.Title = "Create Password"; 4 | } 5 | 6 |

@ViewBag.Title.

7 |

8 | You do not have a local username/password for this site. Add a local 9 | account so you can log in without an external login. 10 |

11 | 12 | @using (Html.BeginForm("SetPassword", "Manage", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) 13 | { 14 | @Html.AntiForgeryToken() 15 | 16 |

Create Local Login

17 |
18 | @Html.ValidationSummary("", new { @class = "text-danger" }) 19 |
20 | @Html.LabelFor(m => m.NewPassword, new { @class = "col-md-2 control-label" }) 21 |
22 | @Html.PasswordFor(m => m.NewPassword, new { @class = "form-control" }) 23 |
24 |
25 |
26 | @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" }) 27 |
28 | @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" }) 29 |
30 |
31 |
32 |
33 | 34 |
35 |
36 | } 37 | @section Scripts { 38 | @Scripts.Render("~/bundles/jqueryval") 39 | } -------------------------------------------------------------------------------- /WebUI/Features/Manage/VerifyPhoneNumber.cshtml: -------------------------------------------------------------------------------- 1 | @model WebUI.Models.VerifyPhoneNumberViewModel 2 | @{ 3 | ViewBag.Title = "Verify Phone Number"; 4 | } 5 | 6 |

@ViewBag.Title.

7 | 8 | @using (Html.BeginForm("VerifyPhoneNumber", "Manage", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) 9 | { 10 | @Html.AntiForgeryToken() 11 | @Html.Hidden("phoneNumber", @Model.PhoneNumber) 12 |

Enter verification code

13 |
@ViewBag.Status
14 |
15 | @Html.ValidationSummary("", new { @class = "text-danger" }) 16 |
17 | @Html.LabelFor(m => m.Code, new { @class = "col-md-2 control-label" }) 18 |
19 | @Html.TextBoxFor(m => m.Code, new { @class = "form-control" }) 20 |
21 |
22 |
23 |
24 | 25 |
26 |
27 | } 28 | 29 | @section Scripts { 30 | @Scripts.Render("~/bundles/jqueryval") 31 | } 32 | -------------------------------------------------------------------------------- /WebUI/Features/RoastPlanning/Create.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = "Create New Roast Schedule"; 3 | } 4 | 5 |

@ViewBag.Title.

6 |

@ViewBag.Message

7 | 8 | @using (Html.BeginForm()) 9 | { 10 |
11 |

Click Below to Create New Roast Schedule

12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 |
20 | } -------------------------------------------------------------------------------- /WebUI/Features/RoastPlanning/Index.cshtml: -------------------------------------------------------------------------------- 1 | @using WebUI.Models 2 | @model List 3 | @{ 4 | ViewBag.Title = "Roast Planning"; 5 | } 6 | 7 |

@ViewBag.Title.

8 |

@ViewBag.Message

9 | 10 | @if (@Model.Count > 0) 11 | { 12 |

Your Roast Schedules:

13 | foreach (var roastSchedule in @Model) 14 | { 15 |
    16 |
  • @($"RoastScheduleId: {@roastSchedule.RoastScheduleId}, RoastWeekStartsOn: {@roastSchedule.RoastWeekStartsOn.ToShortDateString()}")
  • 17 |
18 | } 19 | } 20 | -------------------------------------------------------------------------------- /WebUI/Features/RoastPlanning/RoastPlanningController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Web.Mvc; 4 | using FirstPopCoffee.Common.Domain.Model; 5 | using FirstPopCoffee.RoastPlanning.Domain.Model; 6 | using WebUI.ReadModel; 7 | 8 | namespace WebUI.Features.RoastPlanning 9 | { 10 | public class RoastPlanningController : Controller 11 | { 12 | private readonly ICommandSender _bus; 13 | private readonly IRoastPlanningReadModel _readModel; 14 | 15 | public RoastPlanningController(ICommandSender bus, IRoastPlanningReadModel readModel) 16 | { 17 | _bus = bus; 18 | _readModel = readModel; 19 | } 20 | 21 | public ActionResult Index() 22 | { 23 | var viewModel = _readModel.RoastSchedules.ToList(); 24 | return View(viewModel); 25 | } 26 | 27 | public ActionResult Create() 28 | { 29 | return View(); 30 | } 31 | 32 | [HttpPost] 33 | public ActionResult Create(string create) 34 | { 35 | _bus.Send(new CreateNewRoastScheduleCommand(Guid.NewGuid())); 36 | return this.RedirectToAction(nameof(Index)); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /WebUI/Features/RoastPlanning/RoastSchedule.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = "Roast Schedule"; 3 | } 4 | 5 |

@ViewBag.Title.

6 | 7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
DayIs Roast Day
MondayY
23 |
24 |
-------------------------------------------------------------------------------- /WebUI/Features/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @model System.Web.Mvc.HandleErrorInfo 2 | 3 | @{ 4 | ViewBag.Title = "Error"; 5 | } 6 | 7 |

Error.

8 |

An error occurred while processing your request.

9 | 10 | -------------------------------------------------------------------------------- /WebUI/Features/Shared/Lockout.cshtml: -------------------------------------------------------------------------------- 1 | @model System.Web.Mvc.HandleErrorInfo 2 | 3 | @{ 4 | ViewBag.Title = "Locked Out"; 5 | } 6 | 7 |
8 |

Locked out.

9 |

This account has been locked out, please try again later.

10 |
11 | -------------------------------------------------------------------------------- /WebUI/Features/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | @ViewBag.Title - My ASP.NET Application 7 | @Styles.Render("~/Content/css") 8 | @Scripts.Render("~/bundles/modernizr") 9 | 10 | 11 | 12 | 32 |
33 | @RenderBody() 34 |
35 |
36 |

© @DateTime.Now.Year - My ASP.NET Application

37 |
38 |
39 | 40 | @Scripts.Render("~/bundles/jquery") 41 | @Scripts.Render("~/bundles/bootstrap") 42 | @RenderSection("scripts", required: false) 43 | 44 | 45 | -------------------------------------------------------------------------------- /WebUI/Features/Shared/_LoginPartial.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNet.Identity 2 | @if (Request.IsAuthenticated) 3 | { 4 | using (Html.BeginForm("LogOff", "Account", FormMethod.Post, new { id = "logoutForm", @class = "navbar-right" })) 5 | { 6 | @Html.AntiForgeryToken() 7 | 8 | 14 | } 15 | } 16 | else 17 | { 18 | 22 | } 23 | -------------------------------------------------------------------------------- /WebUI/Features/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 | -------------------------------------------------------------------------------- /WebUI/Features/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "~/Features/Shared/_Layout.cshtml"; 3 | } 4 | -------------------------------------------------------------------------------- /WebUI/Global.asax: -------------------------------------------------------------------------------- 1 | <%@ Application Codebehind="Global.asax.cs" Inherits="WebUI.MvcApplication" Language="C#" %> 2 | -------------------------------------------------------------------------------- /WebUI/Global.asax.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Web; 5 | using System.Web.Mvc; 6 | using System.Web.Optimization; 7 | using System.Web.Routing; 8 | using FirstPopCoffee.Common.Domain.Model; 9 | using FirstPopCoffee.Common.Events; 10 | using FirstPopCoffee.RoastPlanning.Application; 11 | using FirstPopCoffee.RoastPlanning.Domain.Model; 12 | using Ninject; 13 | using WebUI.Infrastructure; 14 | using WebUI.ReadModel; 15 | 16 | namespace WebUI 17 | { 18 | public class MvcApplication : System.Web.HttpApplication 19 | { 20 | protected void Application_Start() 21 | { 22 | AreaRegistration.RegisterAllAreas(); 23 | FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); 24 | RouteConfig.RegisterRoutes(RouteTable.Routes); 25 | BundleConfig.RegisterBundles(BundleTable.Bundles); 26 | 27 | ViewEngines.Engines.Clear(); 28 | ViewEngines.Engines.Add(new FeatureViewLocationRazorViewEngine()); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /WebUI/Infrastructure/FakeDbSet.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Data.Entity; 5 | using System.Data.Entity.Infrastructure; 6 | using System.Linq; 7 | using System.Linq.Expressions; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using System.Web; 11 | 12 | namespace WebUI.Infrastructure 13 | { 14 | public class FakeDbSet : DbSet, IQueryable, IEnumerable, IDbAsyncEnumerable 15 | where TEntity : class 16 | { 17 | ObservableCollection _data; 18 | IQueryable _query; 19 | 20 | public FakeDbSet() 21 | { 22 | _data = new ObservableCollection(); 23 | _query = _data.AsQueryable(); 24 | } 25 | 26 | public override TEntity Add(TEntity item) 27 | { 28 | _data.Add(item); 29 | return item; 30 | } 31 | 32 | public override TEntity Remove(TEntity item) 33 | { 34 | _data.Remove(item); 35 | return item; 36 | } 37 | 38 | public override TEntity Attach(TEntity item) 39 | { 40 | _data.Add(item); 41 | return item; 42 | } 43 | 44 | public override TEntity Create() 45 | { 46 | return Activator.CreateInstance(); 47 | } 48 | 49 | public override TDerivedEntity Create() 50 | { 51 | return Activator.CreateInstance(); 52 | } 53 | 54 | public override ObservableCollection Local { 55 | get { return _data; } 56 | } 57 | 58 | Type IQueryable.ElementType { 59 | get { return _query.ElementType; } 60 | } 61 | 62 | Expression IQueryable.Expression { 63 | get { return _query.Expression; } 64 | } 65 | 66 | IQueryProvider IQueryable.Provider { 67 | get { return new FakeDbAsyncQueryProvider(_query.Provider); } 68 | } 69 | 70 | System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() 71 | { 72 | return _data.GetEnumerator(); 73 | } 74 | 75 | IEnumerator IEnumerable.GetEnumerator() 76 | { 77 | return _data.GetEnumerator(); 78 | } 79 | 80 | IDbAsyncEnumerator IDbAsyncEnumerable.GetAsyncEnumerator() 81 | { 82 | return new FakeTestDbAsyncEnumerator(_data.GetEnumerator()); 83 | } 84 | } 85 | 86 | internal class FakeDbAsyncQueryProvider : IDbAsyncQueryProvider 87 | { 88 | private readonly IQueryProvider _inner; 89 | 90 | internal FakeDbAsyncQueryProvider(IQueryProvider inner) 91 | { 92 | _inner = inner; 93 | } 94 | 95 | public IQueryable CreateQuery(Expression expression) 96 | { 97 | return new FakeDbAsyncEnumerable(expression); 98 | } 99 | 100 | public IQueryable CreateQuery(Expression expression) 101 | { 102 | return new FakeDbAsyncEnumerable(expression); 103 | } 104 | 105 | public object Execute(Expression expression) 106 | { 107 | return _inner.Execute(expression); 108 | } 109 | 110 | public TResult Execute(Expression expression) 111 | { 112 | return _inner.Execute(expression); 113 | } 114 | 115 | public Task ExecuteAsync(Expression expression, CancellationToken cancellationToken) 116 | { 117 | return Task.FromResult(Execute(expression)); 118 | } 119 | 120 | public Task ExecuteAsync(Expression expression, CancellationToken cancellationToken) 121 | { 122 | return Task.FromResult(Execute(expression)); 123 | } 124 | } 125 | 126 | internal class FakeDbAsyncEnumerable : EnumerableQuery, IDbAsyncEnumerable, IQueryable 127 | { 128 | public FakeDbAsyncEnumerable(IEnumerable enumerable) 129 | : base(enumerable) 130 | { } 131 | 132 | public FakeDbAsyncEnumerable(Expression expression) 133 | : base(expression) 134 | { } 135 | 136 | public IDbAsyncEnumerator GetAsyncEnumerator() 137 | { 138 | return new FakeTestDbAsyncEnumerator(this.AsEnumerable().GetEnumerator()); 139 | } 140 | 141 | IDbAsyncEnumerator IDbAsyncEnumerable.GetAsyncEnumerator() 142 | { 143 | return GetAsyncEnumerator(); 144 | } 145 | 146 | IQueryProvider IQueryable.Provider { 147 | get { return new FakeDbAsyncQueryProvider(this); } 148 | } 149 | } 150 | 151 | internal class FakeTestDbAsyncEnumerator : IDbAsyncEnumerator 152 | { 153 | private readonly IEnumerator _inner; 154 | 155 | public FakeTestDbAsyncEnumerator(IEnumerator inner) 156 | { 157 | _inner = inner; 158 | } 159 | 160 | public void Dispose() 161 | { 162 | _inner.Dispose(); 163 | } 164 | 165 | public Task MoveNextAsync(CancellationToken cancellationToken) 166 | { 167 | return Task.FromResult(_inner.MoveNext()); 168 | } 169 | 170 | public T Current { 171 | get { return _inner.Current; } 172 | } 173 | 174 | object IDbAsyncEnumerator.Current { 175 | get { return Current; } 176 | } 177 | } 178 | } -------------------------------------------------------------------------------- /WebUI/Infrastructure/FeatureViewLocationRazorViewEngine.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Web; 5 | using System.Web.Mvc; 6 | 7 | namespace WebUI.Infrastructure { 8 | public class FeatureViewLocationRazorViewEngine : RazorViewEngine { 9 | public FeatureViewLocationRazorViewEngine() { 10 | ViewLocationFormats = new[] 11 | { 12 | "~/Features/{1}/{0}.cshtml", 13 | "~/Features/{1}/{0}.vbhtml", 14 | "~/Features/Shared/{0}.cshtml", 15 | "~/Features/Shared/{0}.vbhtml", 16 | }; 17 | 18 | MasterLocationFormats = ViewLocationFormats; 19 | 20 | PartialViewLocationFormats = new[] 21 | { 22 | "~/Features/{1}/{0}.cshtml", 23 | "~/Features/{1}/{0}.vbhtml", 24 | "~/Features/Shared/{0}.cshtml", 25 | "~/Features/Shared/{0}.vbhtml", 26 | }; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /WebUI/Models/AccountViewModels.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace WebUI.Models 5 | { 6 | public class ExternalLoginConfirmationViewModel 7 | { 8 | [Required] 9 | [Display(Name = "Email")] 10 | public string Email { get; set; } 11 | } 12 | 13 | public class ExternalLoginListViewModel 14 | { 15 | public string ReturnUrl { get; set; } 16 | } 17 | 18 | public class SendCodeViewModel 19 | { 20 | public string SelectedProvider { get; set; } 21 | public ICollection Providers { get; set; } 22 | public string ReturnUrl { get; set; } 23 | public bool RememberMe { get; set; } 24 | } 25 | 26 | public class VerifyCodeViewModel 27 | { 28 | [Required] 29 | public string Provider { get; set; } 30 | 31 | [Required] 32 | [Display(Name = "Code")] 33 | public string Code { get; set; } 34 | public string ReturnUrl { get; set; } 35 | 36 | [Display(Name = "Remember this browser?")] 37 | public bool RememberBrowser { get; set; } 38 | 39 | public bool RememberMe { get; set; } 40 | } 41 | 42 | public class ForgotViewModel 43 | { 44 | [Required] 45 | [Display(Name = "Email")] 46 | public string Email { get; set; } 47 | } 48 | 49 | public class LoginViewModel 50 | { 51 | [Required] 52 | [Display(Name = "Email")] 53 | [EmailAddress] 54 | public string Email { get; set; } 55 | 56 | [Required] 57 | [DataType(DataType.Password)] 58 | [Display(Name = "Password")] 59 | public string Password { get; set; } 60 | 61 | [Display(Name = "Remember me?")] 62 | public bool RememberMe { get; set; } 63 | } 64 | 65 | public class RegisterViewModel 66 | { 67 | [Required] 68 | [EmailAddress] 69 | [Display(Name = "Email")] 70 | public string Email { get; set; } 71 | 72 | [Required] 73 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] 74 | [DataType(DataType.Password)] 75 | [Display(Name = "Password")] 76 | public string Password { get; set; } 77 | 78 | [DataType(DataType.Password)] 79 | [Display(Name = "Confirm password")] 80 | [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] 81 | public string ConfirmPassword { get; set; } 82 | } 83 | 84 | public class ResetPasswordViewModel 85 | { 86 | [Required] 87 | [EmailAddress] 88 | [Display(Name = "Email")] 89 | public string Email { get; set; } 90 | 91 | [Required] 92 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] 93 | [DataType(DataType.Password)] 94 | [Display(Name = "Password")] 95 | public string Password { get; set; } 96 | 97 | [DataType(DataType.Password)] 98 | [Display(Name = "Confirm password")] 99 | [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] 100 | public string ConfirmPassword { get; set; } 101 | 102 | public string Code { get; set; } 103 | } 104 | 105 | public class ForgotPasswordViewModel 106 | { 107 | [Required] 108 | [EmailAddress] 109 | [Display(Name = "Email")] 110 | public string Email { get; set; } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /WebUI/Models/IdentityModels.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Entity; 2 | using System.Security.Claims; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNet.Identity; 5 | using Microsoft.AspNet.Identity.EntityFramework; 6 | 7 | namespace WebUI.Models 8 | { 9 | // You can add profile data for the user by adding more properties to your ApplicationUser class, please visit http://go.microsoft.com/fwlink/?LinkID=317594 to learn more. 10 | public class ApplicationUser : IdentityUser 11 | { 12 | public async Task GenerateUserIdentityAsync(UserManager manager) 13 | { 14 | // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType 15 | var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie); 16 | // Add custom user claims here 17 | return userIdentity; 18 | } 19 | } 20 | 21 | public class ApplicationDbContext : IdentityDbContext 22 | { 23 | public ApplicationDbContext() 24 | : base("DefaultConnection", throwIfV1Schema: false) 25 | { 26 | } 27 | 28 | public static ApplicationDbContext Create() 29 | { 30 | return new ApplicationDbContext(); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /WebUI/Models/ManageViewModels.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel.DataAnnotations; 3 | using Microsoft.AspNet.Identity; 4 | using Microsoft.Owin.Security; 5 | 6 | namespace WebUI.Models 7 | { 8 | public class IndexViewModel 9 | { 10 | public bool HasPassword { get; set; } 11 | public IList Logins { get; set; } 12 | public string PhoneNumber { get; set; } 13 | public bool TwoFactor { get; set; } 14 | public bool BrowserRemembered { get; set; } 15 | } 16 | 17 | public class ManageLoginsViewModel 18 | { 19 | public IList CurrentLogins { get; set; } 20 | public IList OtherLogins { get; set; } 21 | } 22 | 23 | public class FactorViewModel 24 | { 25 | public string Purpose { get; set; } 26 | } 27 | 28 | public class SetPasswordViewModel 29 | { 30 | [Required] 31 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] 32 | [DataType(DataType.Password)] 33 | [Display(Name = "New password")] 34 | public string NewPassword { get; set; } 35 | 36 | [DataType(DataType.Password)] 37 | [Display(Name = "Confirm new password")] 38 | [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] 39 | public string ConfirmPassword { get; set; } 40 | } 41 | 42 | public class ChangePasswordViewModel 43 | { 44 | [Required] 45 | [DataType(DataType.Password)] 46 | [Display(Name = "Current password")] 47 | public string OldPassword { get; set; } 48 | 49 | [Required] 50 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] 51 | [DataType(DataType.Password)] 52 | [Display(Name = "New password")] 53 | public string NewPassword { get; set; } 54 | 55 | [DataType(DataType.Password)] 56 | [Display(Name = "Confirm new password")] 57 | [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] 58 | public string ConfirmPassword { get; set; } 59 | } 60 | 61 | public class AddPhoneNumberViewModel 62 | { 63 | [Required] 64 | [Phone] 65 | [Display(Name = "Phone Number")] 66 | public string Number { get; set; } 67 | } 68 | 69 | public class VerifyPhoneNumberViewModel 70 | { 71 | [Required] 72 | [Display(Name = "Code")] 73 | public string Code { get; set; } 74 | 75 | [Required] 76 | [Phone] 77 | [Display(Name = "Phone Number")] 78 | public string PhoneNumber { get; set; } 79 | } 80 | 81 | public class ConfigureTwoFactorViewModel 82 | { 83 | public string SelectedProvider { get; set; } 84 | public ICollection Providers { get; set; } 85 | } 86 | } -------------------------------------------------------------------------------- /WebUI/Models/RoastScheduleViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Web; 5 | 6 | namespace WebUI.Models 7 | { 8 | public class RoastScheduleViewModel 9 | { 10 | public Guid RoastScheduleId { get; private set; } 11 | public DateTime RoastWeekStartsOn { get; private set; } 12 | 13 | public RoastScheduleViewModel(Guid roastScheduleId, DateTime roastWeekStartsOn) 14 | { 15 | RoastScheduleId = roastScheduleId; 16 | RoastWeekStartsOn = roastWeekStartsOn; 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /WebUI/Project_Readme.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Your ASP.NET application 6 | 95 | 96 | 97 | 98 | 102 | 103 |
104 |
105 |

This application consists of:

106 |
    107 |
  • Sample pages showing basic nav between Home, About, and Contact
  • 108 |
  • Theming using Bootstrap
  • 109 |
  • Authentication, if selected, shows how to register and sign in
  • 110 |
  • ASP.NET features managed using NuGet
  • 111 |
112 |
113 | 114 | 131 | 132 |
133 |

Deploy

134 | 139 |
140 | 141 |
142 |

Get help

143 | 147 |
148 |
149 | 150 | 151 | -------------------------------------------------------------------------------- /WebUI/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("WebUI")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("Cambridge International, Inc")] 12 | [assembly: AssemblyProduct("WebUI")] 13 | [assembly: AssemblyCopyright("Copyright © Cambridge International, Inc 2016")] 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("343f8b81-f31d-4d08-ad36-b23f66bb76cf")] 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 | -------------------------------------------------------------------------------- /WebUI/ReadModel/RoastPlanningContext.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Entity; 2 | using FirstPopCoffee.RoastPlanning.Domain.Model; 3 | using WebUI.Infrastructure; 4 | using WebUI.Models; 5 | 6 | namespace WebUI.ReadModel 7 | { 8 | public interface IRoastPlanningContext 9 | { 10 | DbSet RoastSchedules { get; } 11 | int SaveChanges(); 12 | } 13 | 14 | public class RoastPlanningContext : IRoastPlanningContext 15 | { 16 | public DbSet RoastSchedules { get; set; } 17 | 18 | public RoastPlanningContext(FakeDbSet roastSchedules) 19 | { 20 | this.RoastSchedules = roastSchedules; 21 | } 22 | 23 | public int SaveChangesCount { get; private set; } 24 | public int SaveChanges() 25 | { 26 | this.SaveChangesCount++; 27 | return 1; 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /WebUI/ReadModel/RoastPlanningReadModel.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Entity; 2 | using System.Linq; 3 | using FirstPopCoffee.RoastPlanning.Domain.Model; 4 | using WebUI.Infrastructure; 5 | using WebUI.Models; 6 | 7 | namespace WebUI.ReadModel 8 | { 9 | public interface IRoastPlanningReadModel 10 | { 11 | IQueryable RoastSchedules { get; } 12 | } 13 | 14 | public class FakeRoastPlanningReadModel : DbContext, IRoastPlanningReadModel 15 | { 16 | private readonly FakeDbSet _roastSchedules; 17 | 18 | //public FakeRoastPlanningReadModel() : base("RoastScheduleDb") 19 | //{ 20 | // _roastSchedules = base.Set(); 21 | //} 22 | 23 | public FakeRoastPlanningReadModel(FakeDbSet roastSchedules) 24 | { 25 | _roastSchedules = roastSchedules; 26 | } 27 | public IQueryable RoastSchedules => this._roastSchedules; 28 | } 29 | } -------------------------------------------------------------------------------- /WebUI/ReadModel/RoastPlanningReadModelProjections.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Web; 5 | using FirstPopCoffee.Common.Domain.Model; 6 | using FirstPopCoffee.RoastPlanning.Domain.Model; 7 | using WebUI.Models; 8 | 9 | namespace WebUI.ReadModel 10 | { 11 | public class RoastScheduleView : IEventHandler 12 | { 13 | private readonly IRoastPlanningContext _roastPlanningContext; 14 | 15 | public RoastScheduleView(IRoastPlanningContext roastPlanningContext) 16 | { 17 | _roastPlanningContext = roastPlanningContext; 18 | } 19 | public void Handle(RoastScheduleCreatedEvent message) 20 | { 21 | var newRoastSchedule = new RoastScheduleViewModel(message.RoastScheduleId, message.RoastWeekStartsOn); 22 | _roastPlanningContext.RoastSchedules.Add(newRoastSchedule); 23 | _roastPlanningContext.SaveChanges(); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /WebUI/Scripts/_references.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heynickc/firstpopcoffee/ca6174066ae81c461ccc66b1941d4a34aa373dd0/WebUI/Scripts/_references.js -------------------------------------------------------------------------------- /WebUI/Scripts/jquery.validate.unobtrusive.min.js: -------------------------------------------------------------------------------- 1 | /* NUGET: BEGIN LICENSE TEXT 2 | * 3 | * Microsoft grants you the right to use these script files for the sole 4 | * purpose of either: (i) interacting through your browser with the Microsoft 5 | * website or online service, subject to the applicable licensing or use 6 | * terms; or (ii) using the files as included with a Microsoft product subject 7 | * to that product's license terms. Microsoft reserves all other rights to the 8 | * files not expressly granted by Microsoft, whether by implication, estoppel 9 | * or otherwise. Insofar as a script file is dual licensed under GPL, 10 | * Microsoft neither took the code under GPL nor distributes it thereunder but 11 | * under the terms set out in this paragraph. All notices and licenses 12 | * below are for informational purposes only. 13 | * 14 | * NUGET: END LICENSE TEXT */ 15 | /* 16 | ** Unobtrusive validation support library for jQuery and jQuery Validate 17 | ** Copyright (C) Microsoft Corporation. All rights reserved. 18 | */ 19 | (function(a){var d=a.validator,b,e="unobtrusiveValidation";function c(a,b,c){a.rules[b]=c;if(a.message)a.messages[b]=a.message}function j(a){return a.replace(/^\s+|\s+$/g,"").split(/\s*,\s*/g)}function f(a){return a.replace(/([!"#$%&'()*+,./:;<=>?@\[\\\]^`{|}~])/g,"\\$1")}function h(a){return a.substr(0,a.lastIndexOf(".")+1)}function g(a,b){if(a.indexOf("*.")===0)a=a.replace("*.",b);return a}function m(c,e){var b=a(this).find("[data-valmsg-for='"+f(e[0].name)+"']"),d=b.attr("data-valmsg-replace"),g=d?a.parseJSON(d)!==false:null;b.removeClass("field-validation-valid").addClass("field-validation-error");c.data("unobtrusiveContainer",b);if(g){b.empty();c.removeClass("input-validation-error").appendTo(b)}else c.hide()}function l(e,d){var c=a(this).find("[data-valmsg-summary=true]"),b=c.find("ul");if(b&&b.length&&d.errorList.length){b.empty();c.addClass("validation-summary-errors").removeClass("validation-summary-valid");a.each(d.errorList,function(){a("
  • ").html(this.message).appendTo(b)})}}function k(d){var b=d.data("unobtrusiveContainer"),c=b.attr("data-valmsg-replace"),e=c?a.parseJSON(c):null;if(b){b.addClass("field-validation-valid").removeClass("field-validation-error");d.removeData("unobtrusiveContainer");e&&b.empty()}}function n(){var b=a(this),c="__jquery_unobtrusive_validation_form_reset";if(b.data(c))return;b.data(c,true);try{b.data("validator").resetForm()}finally{b.removeData(c)}b.find(".validation-summary-errors").addClass("validation-summary-valid").removeClass("validation-summary-errors");b.find(".field-validation-error").addClass("field-validation-valid").removeClass("field-validation-error").removeData("unobtrusiveContainer").find(">*").removeData("unobtrusiveContainer")}function i(b){var c=a(b),f=c.data(e),i=a.proxy(n,b),g=d.unobtrusive.options||{},h=function(e,d){var c=g[e];c&&a.isFunction(c)&&c.apply(b,d)};if(!f){f={options:{errorClass:g.errorClass||"input-validation-error",errorElement:g.errorElement||"span",errorPlacement:function(){m.apply(b,arguments);h("errorPlacement",arguments)},invalidHandler:function(){l.apply(b,arguments);h("invalidHandler",arguments)},messages:{},rules:{},success:function(){k.apply(b,arguments);h("success",arguments)}},attachValidation:function(){c.off("reset."+e,i).on("reset."+e,i).validate(this.options)},validate:function(){c.validate();return c.valid()}};c.data(e,f)}return f}d.unobtrusive={adapters:[],parseElement:function(b,h){var d=a(b),f=d.parents("form")[0],c,e,g;if(!f)return;c=i(f);c.options.rules[b.name]=e={};c.options.messages[b.name]=g={};a.each(this.adapters,function(){var c="data-val-"+this.name,i=d.attr(c),h={};if(i!==undefined){c+="-";a.each(this.params,function(){h[this]=d.attr(c+this)});this.adapt({element:b,form:f,message:i,params:h,rules:e,messages:g})}});a.extend(e,{__dummy__:true});!h&&c.attachValidation()},parse:function(c){var b=a(c),e=b.parents().addBack().filter("form").add(b.find("form")).has("[data-val=true]");b.find("[data-val=true]").each(function(){d.unobtrusive.parseElement(this,true)});e.each(function(){var a=i(this);a&&a.attachValidation()})}};b=d.unobtrusive.adapters;b.add=function(c,a,b){if(!b){b=a;a=[]}this.push({name:c,params:a,adapt:b});return this};b.addBool=function(a,b){return this.add(a,function(d){c(d,b||a,true)})};b.addMinMax=function(e,g,f,a,d,b){return this.add(e,[d||"min",b||"max"],function(b){var e=b.params.min,d=b.params.max;if(e&&d)c(b,a,[e,d]);else if(e)c(b,g,e);else d&&c(b,f,d)})};b.addSingleVal=function(a,b,d){return this.add(a,[b||"val"],function(e){c(e,d||a,e.params[b])})};d.addMethod("__dummy__",function(){return true});d.addMethod("regex",function(b,c,d){var a;if(this.optional(c))return true;a=(new RegExp(d)).exec(b);return a&&a.index===0&&a[0].length===b.length});d.addMethod("nonalphamin",function(c,d,b){var a;if(b){a=c.match(/\W/g);a=a&&a.length>=b}return a});if(d.methods.extension){b.addSingleVal("accept","mimtype");b.addSingleVal("extension","extension")}else b.addSingleVal("extension","extension","accept");b.addSingleVal("regex","pattern");b.addBool("creditcard").addBool("date").addBool("digits").addBool("email").addBool("number").addBool("url");b.addMinMax("length","minlength","maxlength","rangelength").addMinMax("range","min","max","range");b.addMinMax("minlength","minlength").addMinMax("maxlength","minlength","maxlength");b.add("equalto",["other"],function(b){var i=h(b.element.name),j=b.params.other,d=g(j,i),e=a(b.form).find(":input").filter("[name='"+f(d)+"']")[0];c(b,"equalTo",e)});b.add("required",function(a){(a.element.tagName.toUpperCase()!=="INPUT"||a.element.type.toUpperCase()!=="CHECKBOX")&&c(a,"required",true)});b.add("remote",["url","type","additionalfields"],function(b){var d={url:b.params.url,type:b.params.type||"GET",data:{}},e=h(b.element.name);a.each(j(b.params.additionalfields||b.element.name),function(i,h){var c=g(h,e);d.data[c]=function(){var d=a(b.form).find(":input").filter("[name='"+f(c)+"']");return d.is(":checkbox")?d.filter(":checked").val()||d.filter(":hidden").val()||"":d.is(":radio")?d.filter(":checked").val()||"":d.val()}});c(b,"remote",d)});b.add("password",["min","nonalphamin","regex"],function(a){a.params.min&&c(a,"minlength",a.params.min);a.params.nonalphamin&&c(a,"nonalphamin",a.params.nonalphamin);a.params.regex&&c(a,"regex",a.params.regex)});a(function(){d.unobtrusive.parse(document)})})(jQuery); -------------------------------------------------------------------------------- /WebUI/Scripts/respond.js: -------------------------------------------------------------------------------- 1 | /* NUGET: BEGIN LICENSE TEXT 2 | * 3 | * Microsoft grants you the right to use these script files for the sole 4 | * purpose of either: (i) interacting through your browser with the Microsoft 5 | * website or online service, subject to the applicable licensing or use 6 | * terms; or (ii) using the files as included with a Microsoft product subject 7 | * to that product's license terms. Microsoft reserves all other rights to the 8 | * files not expressly granted by Microsoft, whether by implication, estoppel 9 | * or otherwise. Insofar as a script file is dual licensed under GPL, 10 | * Microsoft neither took the code under GPL nor distributes it thereunder but 11 | * under the terms set out in this paragraph. All notices and licenses 12 | * below are for informational purposes only. 13 | * 14 | * NUGET: END LICENSE TEXT */ 15 | /*! matchMedia() polyfill - Test a CSS media type/query in JS. Authors & copyright (c) 2012: Scott Jehl, Paul Irish, Nicholas Zakas. Dual MIT/BSD license */ 16 | /*! NOTE: If you're already including a window.matchMedia polyfill via Modernizr or otherwise, you don't need this part */ 17 | window.matchMedia = window.matchMedia || (function(doc, undefined){ 18 | 19 | var bool, 20 | docElem = doc.documentElement, 21 | refNode = docElem.firstElementChild || docElem.firstChild, 22 | // fakeBody required for 23 | fakeBody = doc.createElement('body'), 24 | div = doc.createElement('div'); 25 | 26 | div.id = 'mq-test-1'; 27 | div.style.cssText = "position:absolute;top:-100em"; 28 | fakeBody.style.background = "none"; 29 | fakeBody.appendChild(div); 30 | 31 | return function(q){ 32 | 33 | div.innerHTML = '­'; 34 | 35 | docElem.insertBefore(fakeBody, refNode); 36 | bool = div.offsetWidth == 42; 37 | docElem.removeChild(fakeBody); 38 | 39 | return { matches: bool, media: q }; 40 | }; 41 | 42 | })(document); 43 | 44 | 45 | 46 | 47 | /*! Respond.js v1.2.0: min/max-width media query polyfill. (c) Scott Jehl. MIT/GPLv2 Lic. j.mp/respondjs */ 48 | (function( win ){ 49 | //exposed namespace 50 | win.respond = {}; 51 | 52 | //define update even in native-mq-supporting browsers, to avoid errors 53 | respond.update = function(){}; 54 | 55 | //expose media query support flag for external use 56 | respond.mediaQueriesSupported = win.matchMedia && win.matchMedia( "only all" ).matches; 57 | 58 | //if media queries are supported, exit here 59 | if( respond.mediaQueriesSupported ){ return; } 60 | 61 | //define vars 62 | var doc = win.document, 63 | docElem = doc.documentElement, 64 | mediastyles = [], 65 | rules = [], 66 | appendedEls = [], 67 | parsedSheets = {}, 68 | resizeThrottle = 30, 69 | head = doc.getElementsByTagName( "head" )[0] || docElem, 70 | base = doc.getElementsByTagName( "base" )[0], 71 | links = head.getElementsByTagName( "link" ), 72 | requestQueue = [], 73 | 74 | //loop stylesheets, send text content to translate 75 | ripCSS = function(){ 76 | var sheets = links, 77 | sl = sheets.length, 78 | i = 0, 79 | //vars for loop: 80 | sheet, href, media, isCSS; 81 | 82 | for( ; i < sl; i++ ){ 83 | sheet = sheets[ i ], 84 | href = sheet.href, 85 | media = sheet.media, 86 | isCSS = sheet.rel && sheet.rel.toLowerCase() === "stylesheet"; 87 | 88 | //only links plz and prevent re-parsing 89 | if( !!href && isCSS && !parsedSheets[ href ] ){ 90 | // selectivizr exposes css through the rawCssText expando 91 | if (sheet.styleSheet && sheet.styleSheet.rawCssText) { 92 | translate( sheet.styleSheet.rawCssText, href, media ); 93 | parsedSheets[ href ] = true; 94 | } else { 95 | if( (!/^([a-zA-Z:]*\/\/)/.test( href ) && !base) 96 | || href.replace( RegExp.$1, "" ).split( "/" )[0] === win.location.host ){ 97 | requestQueue.push( { 98 | href: href, 99 | media: media 100 | } ); 101 | } 102 | } 103 | } 104 | } 105 | makeRequests(); 106 | }, 107 | 108 | //recurse through request queue, get css text 109 | makeRequests = function(){ 110 | if( requestQueue.length ){ 111 | var thisRequest = requestQueue.shift(); 112 | 113 | ajax( thisRequest.href, function( styles ){ 114 | translate( styles, thisRequest.href, thisRequest.media ); 115 | parsedSheets[ thisRequest.href ] = true; 116 | makeRequests(); 117 | } ); 118 | } 119 | }, 120 | 121 | //find media blocks in css text, convert to style blocks 122 | translate = function( styles, href, media ){ 123 | var qs = styles.match( /@media[^\{]+\{([^\{\}]*\{[^\}\{]*\})+/gi ), 124 | ql = qs && qs.length || 0, 125 | //try to get CSS path 126 | href = href.substring( 0, href.lastIndexOf( "/" )), 127 | repUrls = function( css ){ 128 | return css.replace( /(url\()['"]?([^\/\)'"][^:\)'"]+)['"]?(\))/g, "$1" + href + "$2$3" ); 129 | }, 130 | useMedia = !ql && media, 131 | //vars used in loop 132 | i = 0, 133 | j, fullq, thisq, eachq, eql; 134 | 135 | //if path exists, tack on trailing slash 136 | if( href.length ){ href += "/"; } 137 | 138 | //if no internal queries exist, but media attr does, use that 139 | //note: this currently lacks support for situations where a media attr is specified on a link AND 140 | //its associated stylesheet has internal CSS media queries. 141 | //In those cases, the media attribute will currently be ignored. 142 | if( useMedia ){ 143 | ql = 1; 144 | } 145 | 146 | 147 | for( ; i < ql; i++ ){ 148 | j = 0; 149 | 150 | //media attr 151 | if( useMedia ){ 152 | fullq = media; 153 | rules.push( repUrls( styles ) ); 154 | } 155 | //parse for styles 156 | else{ 157 | fullq = qs[ i ].match( /@media *([^\{]+)\{([\S\s]+?)$/ ) && RegExp.$1; 158 | rules.push( RegExp.$2 && repUrls( RegExp.$2 ) ); 159 | } 160 | 161 | eachq = fullq.split( "," ); 162 | eql = eachq.length; 163 | 164 | for( ; j < eql; j++ ){ 165 | thisq = eachq[ j ]; 166 | mediastyles.push( { 167 | media : thisq.split( "(" )[ 0 ].match( /(only\s+)?([a-zA-Z]+)\s?/ ) && RegExp.$2 || "all", 168 | rules : rules.length - 1, 169 | hasquery: thisq.indexOf("(") > -1, 170 | minw : thisq.match( /\(min\-width:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/ ) && parseFloat( RegExp.$1 ) + ( RegExp.$2 || "" ), 171 | maxw : thisq.match( /\(max\-width:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/ ) && parseFloat( RegExp.$1 ) + ( RegExp.$2 || "" ) 172 | } ); 173 | } 174 | } 175 | 176 | applyMedia(); 177 | }, 178 | 179 | lastCall, 180 | 181 | resizeDefer, 182 | 183 | // returns the value of 1em in pixels 184 | getEmValue = function() { 185 | var ret, 186 | div = doc.createElement('div'), 187 | body = doc.body, 188 | fakeUsed = false; 189 | 190 | div.style.cssText = "position:absolute;font-size:1em;width:1em"; 191 | 192 | if( !body ){ 193 | body = fakeUsed = doc.createElement( "body" ); 194 | body.style.background = "none"; 195 | } 196 | 197 | body.appendChild( div ); 198 | 199 | docElem.insertBefore( body, docElem.firstChild ); 200 | 201 | ret = div.offsetWidth; 202 | 203 | if( fakeUsed ){ 204 | docElem.removeChild( body ); 205 | } 206 | else { 207 | body.removeChild( div ); 208 | } 209 | 210 | //also update eminpx before returning 211 | ret = eminpx = parseFloat(ret); 212 | 213 | return ret; 214 | }, 215 | 216 | //cached container for 1em value, populated the first time it's needed 217 | eminpx, 218 | 219 | //enable/disable styles 220 | applyMedia = function( fromResize ){ 221 | var name = "clientWidth", 222 | docElemProp = docElem[ name ], 223 | currWidth = doc.compatMode === "CSS1Compat" && docElemProp || doc.body[ name ] || docElemProp, 224 | styleBlocks = {}, 225 | lastLink = links[ links.length-1 ], 226 | now = (new Date()).getTime(); 227 | 228 | //throttle resize calls 229 | if( fromResize && lastCall && now - lastCall < resizeThrottle ){ 230 | clearTimeout( resizeDefer ); 231 | resizeDefer = setTimeout( applyMedia, resizeThrottle ); 232 | return; 233 | } 234 | else { 235 | lastCall = now; 236 | } 237 | 238 | for( var i in mediastyles ){ 239 | var thisstyle = mediastyles[ i ], 240 | min = thisstyle.minw, 241 | max = thisstyle.maxw, 242 | minnull = min === null, 243 | maxnull = max === null, 244 | em = "em"; 245 | 246 | if( !!min ){ 247 | min = parseFloat( min ) * ( min.indexOf( em ) > -1 ? ( eminpx || getEmValue() ) : 1 ); 248 | } 249 | if( !!max ){ 250 | max = parseFloat( max ) * ( max.indexOf( em ) > -1 ? ( eminpx || getEmValue() ) : 1 ); 251 | } 252 | 253 | // if there's no media query at all (the () part), or min or max is not null, and if either is present, they're true 254 | if( !thisstyle.hasquery || ( !minnull || !maxnull ) && ( minnull || currWidth >= min ) && ( maxnull || currWidth <= max ) ){ 255 | if( !styleBlocks[ thisstyle.media ] ){ 256 | styleBlocks[ thisstyle.media ] = []; 257 | } 258 | styleBlocks[ thisstyle.media ].push( rules[ thisstyle.rules ] ); 259 | } 260 | } 261 | 262 | //remove any existing respond style element(s) 263 | for( var i in appendedEls ){ 264 | if( appendedEls[ i ] && appendedEls[ i ].parentNode === head ){ 265 | head.removeChild( appendedEls[ i ] ); 266 | } 267 | } 268 | 269 | //inject active styles, grouped by media type 270 | for( var i in styleBlocks ){ 271 | var ss = doc.createElement( "style" ), 272 | css = styleBlocks[ i ].join( "\n" ); 273 | 274 | ss.type = "text/css"; 275 | ss.media = i; 276 | 277 | //originally, ss was appended to a documentFragment and sheets were appended in bulk. 278 | //this caused crashes in IE in a number of circumstances, such as when the HTML element had a bg image set, so appending beforehand seems best. Thanks to @dvelyk for the initial research on this one! 279 | head.insertBefore( ss, lastLink.nextSibling ); 280 | 281 | if ( ss.styleSheet ){ 282 | ss.styleSheet.cssText = css; 283 | } 284 | else { 285 | ss.appendChild( doc.createTextNode( css ) ); 286 | } 287 | 288 | //push to appendedEls to track for later removal 289 | appendedEls.push( ss ); 290 | } 291 | }, 292 | //tweaked Ajax functions from Quirksmode 293 | ajax = function( url, callback ) { 294 | var req = xmlHttp(); 295 | if (!req){ 296 | return; 297 | } 298 | req.open( "GET", url, true ); 299 | req.onreadystatechange = function () { 300 | if ( req.readyState != 4 || req.status != 200 && req.status != 304 ){ 301 | return; 302 | } 303 | callback( req.responseText ); 304 | } 305 | if ( req.readyState == 4 ){ 306 | return; 307 | } 308 | req.send( null ); 309 | }, 310 | //define ajax obj 311 | xmlHttp = (function() { 312 | var xmlhttpmethod = false; 313 | try { 314 | xmlhttpmethod = new XMLHttpRequest(); 315 | } 316 | catch( e ){ 317 | xmlhttpmethod = new ActiveXObject( "Microsoft.XMLHTTP" ); 318 | } 319 | return function(){ 320 | return xmlhttpmethod; 321 | }; 322 | })(); 323 | 324 | //translate CSS 325 | ripCSS(); 326 | 327 | //expose update for re-running respond later on 328 | respond.update = ripCSS; 329 | 330 | //adjust on resize 331 | function callMedia(){ 332 | applyMedia( true ); 333 | } 334 | if( win.addEventListener ){ 335 | win.addEventListener( "resize", callMedia, false ); 336 | } 337 | else if( win.attachEvent ){ 338 | win.attachEvent( "onresize", callMedia ); 339 | } 340 | })(this); 341 | -------------------------------------------------------------------------------- /WebUI/Scripts/respond.min.js: -------------------------------------------------------------------------------- 1 | /* NUGET: BEGIN LICENSE TEXT 2 | * 3 | * Microsoft grants you the right to use these script files for the sole 4 | * purpose of either: (i) interacting through your browser with the Microsoft 5 | * website or online service, subject to the applicable licensing or use 6 | * terms; or (ii) using the files as included with a Microsoft product subject 7 | * to that product's license terms. Microsoft reserves all other rights to the 8 | * files not expressly granted by Microsoft, whether by implication, estoppel 9 | * or otherwise. Insofar as a script file is dual licensed under GPL, 10 | * Microsoft neither took the code under GPL nor distributes it thereunder but 11 | * under the terms set out in this paragraph. All notices and licenses 12 | * below are for informational purposes only. 13 | * 14 | * NUGET: END LICENSE TEXT */ 15 | /*! matchMedia() polyfill - Test a CSS media type/query in JS. Authors & copyright (c) 2012: Scott Jehl, Paul Irish, Nicholas Zakas. Dual MIT/BSD license */ 16 | /*! NOTE: If you're already including a window.matchMedia polyfill via Modernizr or otherwise, you don't need this part */ 17 | window.matchMedia=window.matchMedia||(function(e,f){var c,a=e.documentElement,b=a.firstElementChild||a.firstChild,d=e.createElement("body"),g=e.createElement("div");g.id="mq-test-1";g.style.cssText="position:absolute;top:-100em";d.style.background="none";d.appendChild(g);return function(h){g.innerHTML='­';a.insertBefore(d,b);c=g.offsetWidth==42;a.removeChild(d);return{matches:c,media:h}}})(document); 18 | 19 | /*! Respond.js v1.2.0: min/max-width media query polyfill. (c) Scott Jehl. MIT/GPLv2 Lic. j.mp/respondjs */ 20 | (function(e){e.respond={};respond.update=function(){};respond.mediaQueriesSupported=e.matchMedia&&e.matchMedia("only all").matches;if(respond.mediaQueriesSupported){return}var w=e.document,s=w.documentElement,i=[],k=[],q=[],o={},h=30,f=w.getElementsByTagName("head")[0]||s,g=w.getElementsByTagName("base")[0],b=f.getElementsByTagName("link"),d=[],a=function(){var D=b,y=D.length,B=0,A,z,C,x;for(;B-1,minw:F.match(/\(min\-width:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/)&&parseFloat(RegExp.$1)+(RegExp.$2||""),maxw:F.match(/\(max\-width:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/)&&parseFloat(RegExp.$1)+(RegExp.$2||"")})}}j()},l,r,v=function(){var z,A=w.createElement("div"),x=w.body,y=false;A.style.cssText="position:absolute;font-size:1em;width:1em";if(!x){x=y=w.createElement("body");x.style.background="none"}x.appendChild(A);s.insertBefore(x,s.firstChild);z=A.offsetWidth;if(y){s.removeChild(x)}else{x.removeChild(A)}z=p=parseFloat(z);return z},p,j=function(I){var x="clientWidth",B=s[x],H=w.compatMode==="CSS1Compat"&&B||w.body[x]||B,D={},G=b[b.length-1],z=(new Date()).getTime();if(I&&l&&z-l-1?(p||v()):1)}if(!!J){J=parseFloat(J)*(J.indexOf(y)>-1?(p||v()):1)}if(!K.hasquery||(!A||!L)&&(A||H>=C)&&(L||H<=J)){if(!D[K.media]){D[K.media]=[]}D[K.media].push(k[K.rules])}}for(var E in q){if(q[E]&&q[E].parentNode===f){f.removeChild(q[E])}}for(var E in D){var M=w.createElement("style"),F=D[E].join("\n");M.type="text/css";M.media=E;f.insertBefore(M,G.nextSibling);if(M.styleSheet){M.styleSheet.cssText=F}else{M.appendChild(w.createTextNode(F))}q.push(M)}},n=function(x,z){var y=c();if(!y){return}y.open("GET",x,true);y.onreadystatechange=function(){if(y.readyState!=4||y.status!=200&&y.status!=304){return}z(y.responseText)};if(y.readyState==4){return}y.send(null)},c=(function(){var x=false;try{x=new XMLHttpRequest()}catch(y){x=new ActiveXObject("Microsoft.XMLHTTP")}return function(){return x}})();a();respond.update=a;function t(){j(true)}if(e.addEventListener){e.addEventListener("resize",t,false)}else{if(e.attachEvent){e.attachEvent("onresize",t)}}})(this); -------------------------------------------------------------------------------- /WebUI/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Owin; 2 | using Owin; 3 | 4 | [assembly: OwinStartupAttribute(typeof(WebUI.Startup))] 5 | namespace WebUI 6 | { 7 | public partial class Startup 8 | { 9 | public void Configuration(IAppBuilder app) 10 | { 11 | ConfigureAuth(app); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /WebUI/Web.Debug.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 17 | 18 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /WebUI/Web.Release.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 17 | 18 | 19 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /WebUI/Web.config: -------------------------------------------------------------------------------- 1 |  2 | 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 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /WebUI/WebUI.v2.ncrunchproject: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heynickc/firstpopcoffee/ca6174066ae81c461ccc66b1941d4a34aa373dd0/WebUI/WebUI.v2.ncrunchproject -------------------------------------------------------------------------------- /WebUI/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heynickc/firstpopcoffee/ca6174066ae81c461ccc66b1941d4a34aa373dd0/WebUI/favicon.ico -------------------------------------------------------------------------------- /WebUI/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heynickc/firstpopcoffee/ca6174066ae81c461ccc66b1941d4a34aa373dd0/WebUI/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /WebUI/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heynickc/firstpopcoffee/ca6174066ae81c461ccc66b1941d4a34aa373dd0/WebUI/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /WebUI/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heynickc/firstpopcoffee/ca6174066ae81c461ccc66b1941d4a34aa373dd0/WebUI/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /WebUI/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 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | --------------------------------------------------------------------------------