├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── Database ├── Dockerfile ├── SqlCmdScript.sql ├── SqlCmdStartup.sh └── entrypoint.sh ├── Foundation ├── Events │ ├── ApplicantAppliedEvent.cs │ ├── Events.csproj │ └── IntegrationEvent.cs └── Http │ ├── Http.csproj │ ├── IHttpClient.cs │ └── StandardHttpClient.cs ├── README.md ├── Services ├── Applicants.Api │ ├── Controllers │ │ └── ApplicantsController.cs │ ├── Dockerfile.debug │ ├── Messaging │ │ └── Consumers │ │ │ └── ApplicantAppliedEventConsumer.cs │ ├── Models │ │ ├── Applicant.cs │ │ └── ApplicantSubmission.cs │ ├── Program.cs │ ├── Services │ │ └── ApplicantRepository.cs │ ├── Startup.cs │ ├── applicants.api.csproj │ ├── appsettings.Development.json │ └── appsettings.json ├── Identity.Api │ ├── Controllers │ │ └── UsersController.cs │ ├── Dockerfile.debug │ ├── Identity.Api.csproj │ ├── Messaging │ │ └── Consumers │ │ │ └── ApplicantAppliedEventConsumer.cs │ ├── Models │ │ └── User.cs │ ├── Program.cs │ ├── Services │ │ ├── IIdentityRepository.cs │ │ └── IdentityRepository.cs │ ├── Startup.cs │ ├── appsettings.Development.json │ └── appsettings.json └── Jobs.Api │ ├── Controllers │ └── JobsController.cs │ ├── Dockerfile.debug │ ├── Models │ ├── Job.cs │ └── JobApplicant.cs │ ├── Program.cs │ ├── Services │ ├── IJobRepository.cs │ └── JobRepository.cs │ ├── Startup.cs │ ├── appsettings.Development.json │ ├── appsettings.json │ └── jobs.api.csproj ├── Web ├── .bowerrc ├── Config │ └── ApiConfig.cs ├── Controllers │ ├── HomeController.cs │ └── JobsController.cs ├── Dockerfile.debug ├── Models │ └── ErrorViewModel.cs ├── Program.cs ├── Services │ ├── IIdentityService.cs │ ├── IJobService.cs │ ├── IdentityService.cs │ └── JobService.cs ├── Startup.cs ├── ViewModels │ ├── HomeViewModels │ │ └── IndexViewModel.cs │ ├── Job.cs │ ├── JobApplicant.cs │ ├── JobsViewModels │ │ ├── ApplySuccessViewModel.cs │ │ ├── ApplyViewModel.cs │ │ └── JobApplicationViewModel.cs │ └── User.cs ├── Views │ ├── Home │ │ ├── About.cshtml │ │ ├── Contact.cshtml │ │ └── Index.cshtml │ ├── Jobs │ │ ├── Apply.cshtml │ │ └── ApplySuccess.cshtml │ ├── Shared │ │ ├── Error.cshtml │ │ ├── _Layout.cshtml │ │ └── _ValidationScriptsPartial.cshtml │ ├── _ViewImports.cshtml │ └── _ViewStart.cshtml ├── Web.csproj ├── appsettings.Development.json ├── appsettings.json ├── bower.json ├── bundleconfig.json └── wwwroot │ ├── css │ ├── site.css │ └── site.min.css │ ├── favicon.ico │ ├── images │ ├── banner1.svg │ ├── banner2.svg │ ├── banner3.svg │ └── banner4.svg │ ├── js │ ├── site.js │ └── site.min.js │ └── lib │ ├── bootstrap │ ├── .bower.json │ ├── LICENSE │ └── dist │ │ ├── css │ │ ├── bootstrap-theme.css │ │ ├── bootstrap-theme.css.map │ │ ├── bootstrap-theme.min.css │ │ ├── bootstrap-theme.min.css.map │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ ├── bootstrap.min.css │ │ └── bootstrap.min.css.map │ │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ │ └── js │ │ ├── bootstrap.js │ │ ├── bootstrap.min.js │ │ └── npm.js │ ├── jquery-validation-unobtrusive │ ├── .bower.json │ ├── jquery.validate.unobtrusive.js │ └── jquery.validate.unobtrusive.min.js │ ├── jquery-validation │ ├── .bower.json │ ├── LICENSE.md │ └── dist │ │ ├── additional-methods.js │ │ ├── additional-methods.min.js │ │ ├── jquery.validate.js │ │ └── jquery.validate.min.js │ └── jquery │ ├── .bower.json │ ├── LICENSE.txt │ └── dist │ ├── jquery.js │ ├── jquery.min.js │ └── jquery.min.map ├── docker-compose.yml └── dotnetgigs.sln /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | project.assets.json 57 | 58 | # StyleCop 59 | StyleCopReport.xml 60 | 61 | # Files built by Visual Studio 62 | *_i.c 63 | *_p.c 64 | *_i.h 65 | *.ilk 66 | *.meta 67 | *.obj 68 | *.pch 69 | *.pdb 70 | *.pgc 71 | *.pgd 72 | *.rsp 73 | *.sbr 74 | *.tlb 75 | *.tli 76 | *.tlh 77 | *.tmp 78 | *.tmp_proj 79 | *.log 80 | *.vspscc 81 | *.vssscc 82 | .builds 83 | *.pidb 84 | *.svclog 85 | *.scc 86 | 87 | # Chutzpah Test files 88 | _Chutzpah* 89 | 90 | # Visual C++ cache files 91 | ipch/ 92 | *.aps 93 | *.ncb 94 | *.opendb 95 | *.opensdf 96 | *.sdf 97 | *.cachefile 98 | *.VC.db 99 | *.VC.VC.opendb 100 | 101 | # Visual Studio profiler 102 | *.psess 103 | *.vsp 104 | *.vspx 105 | *.sap 106 | 107 | # Visual Studio Trace Files 108 | *.e2e 109 | 110 | # TFS 2012 Local Workspace 111 | $tf/ 112 | 113 | # Guidance Automation Toolkit 114 | *.gpState 115 | 116 | # ReSharper is a .NET coding add-in 117 | _ReSharper*/ 118 | *.[Rr]e[Ss]harper 119 | *.DotSettings.user 120 | 121 | # JustCode is a .NET coding add-in 122 | .JustCode 123 | 124 | # TeamCity is a build add-in 125 | _TeamCity* 126 | 127 | # DotCover is a Code Coverage Tool 128 | *.dotCover 129 | 130 | # AxoCover is a Code Coverage Tool 131 | .axoCover/* 132 | !.axoCover/settings.json 133 | 134 | # Visual Studio code coverage results 135 | *.coverage 136 | *.coveragexml 137 | 138 | # NCrunch 139 | _NCrunch_* 140 | .*crunch*.local.xml 141 | nCrunchTemp_* 142 | 143 | # MightyMoose 144 | *.mm.* 145 | AutoTest.Net/ 146 | 147 | # Web workbench (sass) 148 | .sass-cache/ 149 | 150 | # Installshield output folder 151 | [Ee]xpress/ 152 | 153 | # DocProject is a documentation generator add-in 154 | DocProject/buildhelp/ 155 | DocProject/Help/*.HxT 156 | DocProject/Help/*.HxC 157 | DocProject/Help/*.hhc 158 | DocProject/Help/*.hhk 159 | DocProject/Help/*.hhp 160 | DocProject/Help/Html2 161 | DocProject/Help/html 162 | 163 | # Click-Once directory 164 | publish/ 165 | 166 | # Publish Web Output 167 | *.[Pp]ublish.xml 168 | *.azurePubxml 169 | # Note: Comment the next line if you want to checkin your web deploy settings, 170 | # but database connection strings (with potential passwords) will be unencrypted 171 | *.pubxml 172 | *.publishproj 173 | 174 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 175 | # checkin your Azure Web App publish settings, but sensitive information contained 176 | # in these scripts will be unencrypted 177 | PublishScripts/ 178 | 179 | # NuGet Packages 180 | *.nupkg 181 | # The packages folder can be ignored because of Package Restore 182 | **/[Pp]ackages/* 183 | # except build/, which is used as an MSBuild target. 184 | !**/[Pp]ackages/build/ 185 | # Uncomment if necessary however generally it will be regenerated when needed 186 | #!**/[Pp]ackages/repositories.config 187 | # NuGet v3's project.json files produces more ignorable files 188 | *.nuget.props 189 | *.nuget.targets 190 | *.nuget.g.targets 191 | *.nuget.cache 192 | *.nuget.g.props 193 | 194 | # Microsoft Azure Build Output 195 | csx/ 196 | *.build.csdef 197 | 198 | # Microsoft Azure Emulator 199 | ecf/ 200 | rcf/ 201 | 202 | # Windows Store app package directories and files 203 | AppPackages/ 204 | BundleArtifacts/ 205 | Package.StoreAssociation.xml 206 | _pkginfo.txt 207 | *.appx 208 | 209 | # Visual Studio cache files 210 | # files ending in .cache can be ignored 211 | *.[Cc]ache 212 | # but keep track of directories ending in .cache 213 | !*.[Cc]ache/ 214 | 215 | # Others 216 | ClientBin/ 217 | ~$* 218 | *~ 219 | *.dbmdl 220 | *.dbproj.schemaview 221 | *.jfm 222 | *.pfx 223 | *.publishsettings 224 | orleans.codegen.cs 225 | 226 | # Since there are multiple workflows, uncomment next line to ignore bower_components 227 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 228 | #bower_components/ 229 | 230 | # RIA/Silverlight projects 231 | Generated_Code/ 232 | 233 | # Backup & report files from converting an old project file 234 | # to a newer Visual Studio version. Backup files are not needed, 235 | # because we have git ;-) 236 | _UpgradeReport_Files/ 237 | Backup*/ 238 | UpgradeLog*.XML 239 | UpgradeLog*.htm 240 | 241 | # SQL Server files 242 | *.mdf 243 | *.ldf 244 | *.ndf 245 | 246 | # Business Intelligence projects 247 | *.rdl.data 248 | *.bim.layout 249 | *.bim_*.settings 250 | 251 | # Microsoft Fakes 252 | FakesAssemblies/ 253 | 254 | # GhostDoc plugin setting file 255 | *.GhostDoc.xml 256 | 257 | # Node.js Tools for Visual Studio 258 | .ntvs_analysis.dat 259 | node_modules/ 260 | 261 | # TypeScript v1 declaration files 262 | typings/ 263 | 264 | # Visual Studio 6 build log 265 | *.plg 266 | 267 | # Visual Studio 6 workspace options file 268 | *.opt 269 | 270 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 271 | *.vbw 272 | 273 | # Visual Studio LightSwitch build output 274 | **/*.HTMLClient/GeneratedArtifacts 275 | **/*.DesktopClient/GeneratedArtifacts 276 | **/*.DesktopClient/ModelManifest.xml 277 | **/*.Server/GeneratedArtifacts 278 | **/*.Server/ModelManifest.xml 279 | _Pvt_Extensions 280 | 281 | # Paket dependency manager 282 | .paket/paket.exe 283 | paket-files/ 284 | 285 | # FAKE - F# Make 286 | .fake/ 287 | 288 | # JetBrains Rider 289 | .idea/ 290 | *.sln.iml 291 | 292 | # CodeRush 293 | .cr/ 294 | 295 | # Python Tools for Visual Studio (PTVS) 296 | __pycache__/ 297 | *.pyc 298 | 299 | # Cake - Uncomment if you are using it 300 | # tools/** 301 | # !tools/packages.config 302 | 303 | # Tabs Studio 304 | *.tss 305 | 306 | # Telerik's JustMock configuration file 307 | *.jmconfig 308 | 309 | # BizTalk build output 310 | *.btp.cs 311 | *.btm.cs 312 | *.odx.cs 313 | *.xsd.cs 314 | 315 | # OpenCover UI analysis results 316 | OpenCover/ 317 | 318 | # Azure Stream Analytics local run output 319 | ASALocalRun/ -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "web", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "publish", 12 | "program": "/app/web.dll", 13 | "sourceFileMap": { 14 | "/app": "${workspaceRoot}/web" 15 | }, 16 | "pipeTransport": { 17 | "pipeProgram": "docker", 18 | "pipeCwd": "${workspaceRoot}", 19 | "pipeArgs": [ 20 | "exec -i web" 21 | ], 22 | "debuggerPath": "/vsdbg/vsdbg", 23 | "quoteArgs": false 24 | } 25 | }, 26 | { 27 | "name": "applicants-api", 28 | "type": "coreclr", 29 | "request": "launch", 30 | "preLaunchTask": "publish", 31 | "program": "/app/applicants.api.dll", 32 | "sourceFileMap": { 33 | "/app": "${workspaceRoot}/applicants.api" 34 | }, 35 | "pipeTransport": { 36 | "pipeProgram": "docker", 37 | "pipeCwd": "${workspaceRoot}", 38 | "pipeArgs": [ 39 | "exec -i applicants.api" 40 | ], 41 | "debuggerPath": "/vsdbg/vsdbg", 42 | "quoteArgs": false 43 | } 44 | }, 45 | { 46 | "name": "jobs-api", 47 | "type": "coreclr", 48 | "request": "launch", 49 | "preLaunchTask": "publish", 50 | "program": "/app/jobs.api.dll", 51 | "sourceFileMap": { 52 | "/app": "${workspaceRoot}/jobs.api" 53 | }, 54 | "pipeTransport": { 55 | "pipeProgram": "docker", 56 | "pipeCwd": "${workspaceRoot}", 57 | "pipeArgs": [ 58 | "exec -i jobs.api" 59 | ], 60 | "debuggerPath": "/vsdbg/vsdbg", 61 | "quoteArgs": false 62 | } 63 | }, 64 | { 65 | "name": "identity-api", 66 | "type": "coreclr", 67 | "request": "launch", 68 | "preLaunchTask": "publish", 69 | "program": "/app/identity.api.dll", 70 | "sourceFileMap": { 71 | "/app": "${workspaceRoot}/identity.api" 72 | }, 73 | "pipeTransport": { 74 | "pipeProgram": "docker", 75 | "pipeCwd": "${workspaceRoot}", 76 | "pipeArgs": [ 77 | "exec -i identity.api" 78 | ], 79 | "debuggerPath": "/vsdbg/vsdbg", 80 | "quoteArgs": false 81 | } 82 | } 83 | ], 84 | "compounds": [ 85 | { 86 | "name": "All Projects", 87 | "configurations": [ 88 | "web", 89 | "applicants-api", 90 | "jobs-api", 91 | "identity-api" 92 | ] 93 | } 94 | ] 95 | } 96 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "command": "dotnet", 4 | "isShellCommand": true, 5 | "args": [], 6 | "tasks": [ 7 | { 8 | "taskName": "publish", 9 | "args": [ 10 | "${workspaceRoot}/dotnetgigs.sln", "-c", "Debug", "-o", "bin/pub" 11 | ], 12 | "isBuildCommand": true, 13 | "problemMatcher": "$msCompile" 14 | } 15 | /*{ 16 | "taskName": "build", 17 | "args": [ 18 | "${workspaceRoot}/customer.api/customer.api.csproj" 19 | ], 20 | "isBuildCommand": true, 21 | "problemMatcher": "$msCompile" 22 | }*/ 23 | ] 24 | } -------------------------------------------------------------------------------- /Database/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM microsoft/mssql-server-linux:2017-latest 2 | ENV SA_PASSWORD=Pass@word 3 | ENV ACCEPT_EULA=Y 4 | COPY entrypoint.sh entrypoint.sh 5 | COPY SqlCmdStartup.sh SqlCmdStartup.sh 6 | COPY SqlCmdScript.sql SqlCmdScript.sql 7 | RUN chmod +x ./SqlCmdStartup.sh 8 | CMD /bin/bash ./entrypoint.sh -------------------------------------------------------------------------------- /Database/SqlCmdScript.sql: -------------------------------------------------------------------------------- 1 | use master; 2 | create database [dotnetgigs.applicants]; 3 | GO 4 | use [dotnetgigs.applicants]; 5 | CREATE TABLE [dbo].[Applicants]( 6 | [ApplicantId] [int] IDENTITY(1,1) NOT NULL, 7 | [Name] [nvarchar](max) NULL, 8 | [Email] [nvarchar](max) NULL, 9 | [Address] [nvarchar](max) NULL, 10 | [PhoneNo] [nvarchar](max) NULL, 11 | CONSTRAINT [PK_Applicants] PRIMARY KEY CLUSTERED 12 | ( 13 | [ApplicantId] ASC 14 | )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] 15 | ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] 16 | GO 17 | /****** Object: Table [dbo].[ApplicantSubmissions] Script Date: 2017-12-15 12:18:39 AM ******/ 18 | SET ANSI_NULLS ON 19 | GO 20 | SET QUOTED_IDENTIFIER ON 21 | GO 22 | CREATE TABLE [dbo].[ApplicantSubmissions]( 23 | [Id] [int] IDENTITY(1,1) NOT NULL, 24 | [JobId] [int] NOT NULL, 25 | [ApplicantId] [int] NOT NULL, 26 | [Title] [nvarchar](max) NOT NULL, 27 | [SubmissionDate] [datetime] NOT NULL, 28 | CONSTRAINT [PK_ApplicantSubmissions] PRIMARY KEY CLUSTERED 29 | ( 30 | [Id] ASC 31 | )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] 32 | ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] 33 | GO 34 | ALTER TABLE [dbo].[ApplicantSubmissions] WITH CHECK ADD CONSTRAINT [FK_ApplicantSubmissions_Applicants] FOREIGN KEY([ApplicantId]) 35 | REFERENCES [dbo].[Applicants] ([ApplicantId]) 36 | GO 37 | ALTER TABLE [dbo].[ApplicantSubmissions] CHECK CONSTRAINT [FK_ApplicantSubmissions_Applicants] 38 | GO 39 | 40 | --data 41 | INSERT INTO Applicants([Name],[Email],[Address],[PhoneNo]) VALUES('Josh Dillinger','josh903902@gmail.com','P.O. Box 184, 8629 A, Ave','1-509-324-5745'),('Vance','erat.nonummy@Nullaaliquet.co.uk','P.O. Box 281, 3168 Nulla Ave','1-451-284-3449'),('Lee','aliquet.lobortis@Nullamut.com','Ap #259-5713 Erat, Avenue','1-526-114-4025'),('Tanisha','Suspendisse.sagittis@mitempor.ca','Ap #220-3725 Donec St.','1-372-601-6991'),('Prescott','Suspendisse.aliquet@incursus.co.uk','P.O. Box 248, 9885 In Rd.','1-443-179-4456'),('Bruce','lectus@arcu.net','Ap #712-620 Cursus. Av.','1-616-100-8361'),('Justin','mollis.dui.in@pede.co.uk','272-6182 Curabitur Rd.','1-441-243-3710'),('Penelope','inceptos.hymenaeos.Mauris@sedtortor.net','892-2321 Sodales. Rd.','1-553-633-7203'),('Amena','bibendum@sapien.co.uk','Ap #291-7263 Feugiat Ave','1-689-762-4168'),('Zenaida','non@risusodio.ca','Ap #922-9109 Neque Ave','1-735-619-7338'); 42 | INSERT INTO Applicants([Name],[Email],[Address],[PhoneNo]) VALUES('Micah','penatibus@at.edu','P.O. Box 703, 2230 Fringilla Street','1-441-833-0075'),('Vivian','odio.Aliquam@facilisis.com','937-3205 Elit Rd.','1-269-130-6315'),('Hilel','ultricies.sem@egetipsumSuspendisse.net','P.O. Box 730, 377 Ut, Rd.','1-265-435-5162'),('Joelle','adipiscing.lacus@Nullatempor.net','P.O. Box 200, 3623 Ac Ave','1-774-496-4023'),('Doris','malesuada.fringilla@magnaPhasellus.ca','897-3445 Etiam Ave','1-805-501-1838'),('Madeson','amet@Uttinciduntvehicula.com','7307 Ac Rd.','1-334-730-8395'),('Dalton','ullamcorper.viverra.Maecenas@tempus.edu','Ap #250-6679 Semper Ave','1-932-757-7168'),('Melissa','scelerisque.scelerisque.dui@odioEtiamligula.co.uk','389-1352 Tellus. Street','1-772-921-7051'),('Briar','vitae.purus.gravida@Aliquamerat.edu','958-2858 Quisque Ave','1-907-581-9797'),('Brock','et.ultrices.posuere@velvulputate.co.uk','Ap #351-3919 In, St.','1-812-582-0934'); 43 | 44 | create database [dotnetgigs.jobs]; 45 | GO 46 | use [dotnetgigs.jobs]; 47 | 48 | CREATE TABLE [dbo].[JobApplicants]( 49 | [Id] [int] IDENTITY(1,1) NOT NULL, 50 | [JobId] [int] NOT NULL, 51 | [ApplicantId] [int] NOT NULL, 52 | [Name] [nvarchar](max) NOT NULL, 53 | [Email] [nvarchar](max) NOT NULL, 54 | [CreatedDate] [datetime] NOT NULL, 55 | [Active] [bit] NOT NULL, 56 | CONSTRAINT [PK_JobApplicants] PRIMARY KEY CLUSTERED 57 | ( 58 | [Id] ASC 59 | )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] 60 | ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] 61 | GO 62 | 63 | CREATE TABLE [dbo].[Jobs]( 64 | [JobId] [int] IDENTITY(1,1) NOT NULL, 65 | [Title] [nvarchar](max) NULL, 66 | [Description] [nvarchar](max) NULL, 67 | [Company] [nvarchar](max) NULL, 68 | [PostedDate] [datetime] NULL, 69 | [Location] [nvarchar](max) NULL, 70 | CONSTRAINT [PK_Jobs] PRIMARY KEY CLUSTERED 71 | ( 72 | [JobId] ASC 73 | )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] 74 | ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] 75 | GO 76 | ALTER TABLE [dbo].[JobApplicants] WITH CHECK ADD CONSTRAINT [FK_JobApplicants_Jobs] FOREIGN KEY([JobId]) 77 | REFERENCES [dbo].[Jobs] ([JobId]) 78 | GO 79 | ALTER TABLE [dbo].[JobApplicants] CHECK CONSTRAINT [FK_JobApplicants_Jobs] 80 | GO 81 | 82 | --data 83 | INSERT INTO Jobs([Title],[Description],[Company],[PostedDate],[Location]) VALUES('Senior Software Engineer','We are seeking a Senior Software Engineer to implement ease-of-use functionality for our integrated IT Risk Management Platform built on .Net, SQL and Angular JS.','HyperSec',getutcdate(),'Toronto, ON') 84 | INSERT INTO Jobs([Title],[Description],[Company],[PostedDate],[Location]) VALUES('Developer (.Net)','Design, implement, debug web-based applications using the appropriate tools and adhering to our coding standards Review project requirements, assess and estimate the necessary time-to-completion Contribute to and lead architecture and design activities Create unit test plans and scenarios for development unit testing Interact with other development teams to carry out code reviews and to ensure a consistent approach to software development Deploy all integration artifacts to a testing and production environment Assist other developers in resolving software development issues Perform additional duties as needed','HyperSec',getutcdate(),'Toronto, ON'); 85 | INSERT INTO Jobs([Title],[Description],[Company],[PostedDate],[Location]) VALUES('C#/.NET Developer','Help schools and parents in a meaningful way by using your talent as a developer to create real world solutions for their needs. We are looking for a strong C# / .NET Developer to join our product development team responsible for designing then developing new products, as well as improving our current suite of desktop, web, and mobile applications. Some of the technologies we work with are ASP.NET Core / React / Redux, WPF using the MVVM pattern, SOAP / REST based services, and MS-SQL.','Progressive Soft',getutcdate(),'Toronto, ON'); 86 | INSERT INTO Jobs([Title],[Description],[Company],[PostedDate],[Location]) VALUES('Full-Stack Web Developer (ASP.NET Core / React.js / C#)','Help schools and parents in a meaningful way by using your talent as a developer to create real world solutions for their needs. We are looking for a strong Full-Stack Web Developer to join our product development team responsible for designing then developing new products, as well as improving our current suite of application using modern web technologies such as ASP.NET Core 2.0 / Node.js / React / Redux / Bootstrap using C# and TypeScript. Our intelligent software manages student fees, bills parents, collect payments, synchronizes and transforms data from many different sources, and includes reporting and visualization features. Contribute to project requirements, system architecture, and brainstorm product ideas. Then, build and execute on these as part of a Scrum team using a high-end computer and large screens!','Progressive Soft',getutcdate(),'Vancouver, BC'); 87 | INSERT INTO Jobs([Title],[Description],[Company],[PostedDate],[Location]) VALUES('Snr full stack C#.NET Developer','We''re currently looking for a talented Senior Back-End .Net Developer to join our team on a permanent full time position. The ideal candidate will have the opportunity to work for one of the highest grossing E-Tailers in North America. What you''ll do: Maintain existing CRM, and Web-based applications in VB.NET/ASP.NET. Create user-friendly and process-efficient interfaces and tools for internal staff to access data relevant to our business. Interact with staff to track down bugs, feature requests, and potential improvements to internal applications Assist in IT related activities.','CyMax',getutcdate(),'Vancouver, BC'); 88 | INSERT INTO Jobs([Title],[Description],[Company],[PostedDate],[Location]) VALUES('Software Developer/Consultant','Collaborate in small teams to design, build, and deploy quality software solutions for our enterprise customers Help shape our long-term technical roadmap as we scale our infrastructure and build new products Solve complex problems related to development and provide accurate estimates and scope for team deliverables Work independently and be able to effectively communicate verbally and in writing with both our customers and internal teams Perform other job-related duties as assigned, we are a small and growing organization where we are all continuously improving our daily activities to be more productive Needs to be available for occasional travel (BC, Alberta, Washington) Needs to be able to multitask, we are looking for a keen individual ready to learn and adapt to a high paced customer facing position','DMS Group',getutcdate(),'Vancouver, BC'); 89 | INSERT INTO Jobs([Title],[Description],[Company],[PostedDate],[Location]) VALUES('Lead Software Developer','Based in Vancouver, the Lead Software Developer will be responsible for bringing creative solutions to complex software requirements as part of a small team of software developers. This individual must understand modern/agile software development lifecycle processes, and enjoy working in a challenging environment to develop web applications, as well as building state of the art software with complex requirements. The successful candidate must have a very good knowledge of C# programming and be an independent, highly-motivated, results-driven individual who has a positive attitude and a willingness to learn.','ComWave',getutcdate(),'Calgary, AB'); 90 | INSERT INTO Jobs([Title],[Description],[Company],[PostedDate],[Location]) VALUES('Front End Web Developer','MarTec Investigations is seeking a front end web developer with 5+ years of JavaScript development who is proficient in ES6/ES2015 and newer language features. Extensive experience with the React/Redux framework is required. The candidate should also have considerable experience with a Microsoft .Net full stack (ASP.NET MVC/ C# / SQL Server). The successful candidate will work with the MarTec Data Services team developing geo-spatial visualization applications, reporting and operations management tools and interfaces for access from PC and mobile devices. This is an ideal position for a self-starter who is able to produce results with limited supervision.','MarTec Investigations',getutcdate(),'Toronto, ON'); 91 | INSERT INTO Jobs([Title],[Description],[Company],[PostedDate],[Location]) VALUES('Software Applications, Developer (with .Net Experience)','Analysis, Design, and Development of Software Applications using various software languages and tools like Dot Net and Developing Dot Net Web-based Applications. Analyze users’ needs by researching through the available documentation and then design, test, and develop the features of the product. Also perform necessary integration support to the basic product. Evaluate & recommend software upgrades for existing and prospective customers. Design each unit at a modular level and plan for unit wise integration of the pieces to ensure that final product works in one single piece.','Marvel Technologies',getutcdate(),'Toronto, ON'); 92 | INSERT INTO Jobs([Title],[Description],[Company],[PostedDate],[Location]) VALUES('Sr. Software Developer','Transform business and system requirements into functional code Contributes to creation of standard approaches and techniques Assist with code quality checks and peer reviews Provide technical software support, including investigating and qualifying bugs, interpreting procedure manuals, and maintaining accurate documentation Out of the box thinking, to identify and work around process design Understand, learn and develop on different robotics tools Involvement in full development life cycle; design, coding, test, build, QA, deployment and maintenance','SmartLink',getutcdate(),'Ottawa, ON'); 93 | -------------------------------------------------------------------------------- /Database/SqlCmdStartup.sh: -------------------------------------------------------------------------------- 1 | #wait for the SQL Server to start up 2 | sleep 25 3 | #run the setup script to create the DB and the schema in the DB 4 | /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P Pass@word -d master -i SqlCmdScript.sql -------------------------------------------------------------------------------- /Database/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #start the script to create the DB and data then start the sqlserver 2 | ./SqlCmdStartup.sh & /opt/mssql/bin/sqlservr -------------------------------------------------------------------------------- /Foundation/Events/ApplicantAppliedEvent.cs: -------------------------------------------------------------------------------- 1 |  2 | 3 | namespace Events 4 | { 5 | public class ApplicantAppliedEvent : IntegrationEvent 6 | { 7 | public int JobId { get; } 8 | public int ApplicantId { get; } 9 | public string Title { get; } 10 | 11 | public ApplicantAppliedEvent(int jobId,int applicantId,string title) 12 | { 13 | JobId = jobId; 14 | ApplicantId = applicantId; 15 | Title = title; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Foundation/Events/Events.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Foundation/Events/IntegrationEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Events 4 | { 5 | public class IntegrationEvent 6 | { 7 | public IntegrationEvent() 8 | { 9 | Id = Guid.NewGuid(); 10 | CreationDate = DateTime.UtcNow; 11 | } 12 | 13 | public Guid Id { get; } 14 | public DateTime CreationDate { get; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Foundation/Http/Http.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Foundation/Http/IHttpClient.cs: -------------------------------------------------------------------------------- 1 |  2 | 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | 6 | namespace Http 7 | { 8 | public interface IHttpClient 9 | { 10 | Task GetStringAsync(string uri); 11 | Task PostAsync(string uri, T item); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Foundation/Http/StandardHttpClient.cs: -------------------------------------------------------------------------------- 1 |  2 | 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | using Newtonsoft.Json; 7 | 8 | 9 | namespace Http 10 | { 11 | public class StandardHttpClient : IHttpClient 12 | { 13 | private static readonly HttpClient Client = new HttpClient(); 14 | public async Task GetStringAsync(string uri) 15 | { 16 | var requestMessage = new HttpRequestMessage(HttpMethod.Get, uri); 17 | var response = await Client.SendAsync(requestMessage); 18 | return await response.Content.ReadAsStringAsync(); 19 | } 20 | 21 | public async Task PostAsync(string uri, T item) 22 | { 23 | var requestMessage = new HttpRequestMessage(HttpMethod.Post, uri) 24 | { 25 | Content = new StringContent(JsonConvert.SerializeObject(item), System.Text.Encoding.UTF8,"application/json") 26 | }; 27 | 28 | var response = await Client.SendAsync(requestMessage); 29 | 30 | if (response.StatusCode == HttpStatusCode.InternalServerError) 31 | { 32 | throw new HttpRequestException(); 33 | } 34 | 35 | return response; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ASPNETCoreDockerMicroservices 2 | Sample project for getting off the ground with ASP.NET Core, Docker and Microservices based on the tutorial: https://fullstackmark.com/post/12/get-started-building-microservices-with-asp.net-core-and-docker-in-visual-studio-code 3 | 4 | This repo contains the demo project used as the subject for a blog post I did on getting started with ASP.NET Core-based microservices and Docker. If you just want to run the project please follow instructions below. If you'd like to learn more on how it was built please check out this [detailed guide on my blog](https://fullstackmark.com/post/12/get-started-building-microservices-with-asp.net-core-and-docker-in-visual-studio-code). 5 | 6 | 7 | # Environment 8 | It should be fairly cross-platform friendly to get up and running but was developed on my windows 10 machine along with the following: 9 | 10 | - Windows 10 and PowerShell 11 | - Visual Studio Code - v1.19.0 12 | - C# for Visual Studio Code extension 13 | - Docker extension 14 | - SQL Server Management Studio 17.4 15 | - .NET Core SDK v2.0.0 16 | - Docker Community Edition 17.09.1-ce-win42 using Linux containers 17 | 18 | # Setup 19 | 20 | 1. Download/clone repo. 21 | 22 | 2. From the root folder (where _docker-compose.yml_ resides) use the Docker CLI to build and start the containers for the solution: `PS> docker-compose up -d`. This step will take a few minutes or more as all the base images must be downloaded. When it completes you can check that all **7** containers for the solution have been built and started successfully by running `PS> docker ps`. 23 | ![Alt](https://fullstackmark.com/img/posts/12/aspnetcore-microservice-and-service-docker-containers.png "7 containers are deployed in this solution") Additionally, you can connect to the Sql Server on Linux instance in the container using SQL Server Management Studio to ensure the databases **dotnetgigs.applicants** and **dotnetgigs.jobs** were created. The server name is: **localhost,5433** with username **sa** and password **Pass@word**. 24 | 25 | 3. At this point, you can run and debug the solution from Visual Studio Code. Simply open the root folder in VSCode and start all projects in the solution simultaneously using the _All Projects_ configuration or start them individually. The order they're started does not matter. ![Alt](https://fullstackmark.com/img/posts/12/aspnetcore-services-running-in-visual-studio-code-debugger.png "containerized services running in visual studio code debugger") 26 | 27 | 4. With all services running in the debugger you can hit the web app in your browser at **localhost:8080** and set breakpoints in any of the projects to debug directly. 28 | 29 | # Known Issues 30 | 31 | When running on windows ensure the line ending type for the Database/SqlCmdStartup.sh remains as `LF`. When opening/saving this file in VSCode it can get switched to `CRLF` in which case the script won't run and the required databases never get created. If the databases aren't created check `PS> docker logs mssql-linux` and the presence of an error at the top such as _sleep: invalid time interval '25\r'_. 32 | 33 | I have also noticed, on first run of newly created containers that when starting services that use Rabbit a connection exception will be thrown when starting the project in the debugger for the first time. Start it again and things seem to work fine from then on. 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Services/Applicants.Api/Controllers/ApplicantsController.cs: -------------------------------------------------------------------------------- 1 |  2 | 3 | using System.Threading.Tasks; 4 | using System.Collections.Generic; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Applicants.Api.Services; 7 | using Applicants.Api.Models; 8 | 9 | 10 | namespace Applicants.Api.Controllers 11 | { 12 | [Route("api/[controller]")] 13 | public class ApplicantsController : Controller 14 | { 15 | private readonly IApplicantRepository _applicantRepository; 16 | 17 | public ApplicantsController(IApplicantRepository applicantRepository){ 18 | _applicantRepository = applicantRepository; 19 | } 20 | 21 | // GET api/values 22 | [HttpGet] 23 | public async Task> Get() 24 | { 25 | return await _applicantRepository.GetAll(); 26 | } 27 | 28 | // GET api/values/5 29 | [HttpGet("{id}")] 30 | public string Get(int id) 31 | { 32 | return "value"; 33 | } 34 | 35 | // POST api/values 36 | [HttpPost] 37 | public void Post([FromBody]string value) 38 | { 39 | } 40 | 41 | // PUT api/values/5 42 | [HttpPut("{id}")] 43 | public void Put(int id, [FromBody]string value) 44 | { 45 | } 46 | 47 | // DELETE api/values/5 48 | [HttpDelete("{id}")] 49 | public void Delete(int id) 50 | { 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Services/Applicants.Api/Dockerfile.debug: -------------------------------------------------------------------------------- 1 | FROM microsoft/aspnetcore:latest 2 | RUN mkdir app 3 | 4 | #Install debugger 5 | RUN apt-get update 6 | RUN apt-get install curl -y unzip 7 | RUN curl -sSL https://aka.ms/getvsdbgsh | bash /dev/stdin -v latest -l /vsdbg 8 | 9 | EXPOSE 80/tcp 10 | 11 | #Keep the debugger container on 12 | ENTRYPOINT ["tail", "-f", "/dev/null"] -------------------------------------------------------------------------------- /Services/Applicants.Api/Messaging/Consumers/ApplicantAppliedEventConsumer.cs: -------------------------------------------------------------------------------- 1 |  2 | using System.Threading.Tasks; 3 | using Applicants.Api.Models; 4 | using Applicants.Api.Services; 5 | using Events; 6 | using MassTransit; 7 | 8 | namespace Applicants.Api.Messaging.Consumers 9 | { 10 | public class ApplicantAppliedEventConsumer : IConsumer 11 | { 12 | private readonly IApplicantRepository _applicantRepository; 13 | 14 | public ApplicantAppliedEventConsumer(IApplicantRepository applicantRepository) 15 | { 16 | _applicantRepository = applicantRepository; 17 | } 18 | 19 | public async Task Consume(ConsumeContext context) 20 | { 21 | await _applicantRepository.AddApplicantSubmission(new ApplicantSubmission 22 | { 23 | JobId = context.Message.JobId, 24 | ApplicantId = context.Message.ApplicantId, 25 | Title = context.Message.Title, 26 | SubmissionDate = context.Message.CreationDate 27 | }); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Services/Applicants.Api/Models/Applicant.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Applicants.Api.Models 4 | { 5 | public class Applicant 6 | { 7 | [Key] 8 | public int ApplicantId { get; set; } 9 | public string Name { get; set; } 10 | public string Email { get; set; } 11 | public string Address { get; set; } 12 | public string PhoneNo { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /Services/Applicants.Api/Models/ApplicantSubmission.cs: -------------------------------------------------------------------------------- 1 |  2 | using System; 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | namespace Applicants.Api.Models 6 | { 7 | public class ApplicantSubmission 8 | { 9 | [Key] 10 | public int Id { get; set; } 11 | public int JobId { get; set; } 12 | public int ApplicantId { get; set; } 13 | public string Title { get; set; } 14 | public DateTime SubmissionDate { get; set; } 15 | } 16 | } 17 | 18 | 19 | -------------------------------------------------------------------------------- /Services/Applicants.Api/Program.cs: -------------------------------------------------------------------------------- 1 |  2 | using Microsoft.AspNetCore; 3 | using Microsoft.AspNetCore.Hosting; 4 | 5 | namespace Applicants.Api 6 | { 7 | public class Program 8 | { 9 | public static void Main(string[] args) 10 | { 11 | BuildWebHost(args).Run(); 12 | } 13 | 14 | public static IWebHost BuildWebHost(string[] args) => 15 | WebHost.CreateDefaultBuilder(args) 16 | .UseStartup() 17 | .Build(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Services/Applicants.Api/Services/ApplicantRepository.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Data.SqlClient; 5 | using System.Threading.Tasks; 6 | using Applicants.Api.Models; 7 | using Dapper; 8 | 9 | namespace Applicants.Api.Services 10 | { 11 | public interface IApplicantRepository 12 | { 13 | Task> GetAll(); 14 | Task AddApplicantSubmission(ApplicantSubmission applicantSubmission); 15 | } 16 | 17 | public class ApplicantRepository : IApplicantRepository 18 | { 19 | private readonly string _connectionString; 20 | 21 | public ApplicantRepository(string connectionString) 22 | { 23 | _connectionString = connectionString; 24 | } 25 | 26 | public IDbConnection Connection => new SqlConnection(_connectionString); 27 | 28 | public async Task> GetAll() 29 | { 30 | using (var dbConnection = Connection) 31 | { 32 | dbConnection.Open(); 33 | return await dbConnection.QueryAsync("SELECT * FROM Applicants"); 34 | } 35 | } 36 | 37 | public async Task AddApplicantSubmission(ApplicantSubmission applicantSubmission) 38 | { 39 | using (var dbConnection = Connection) 40 | { 41 | dbConnection.Open(); 42 | return await dbConnection.ExecuteAsync( 43 | "insert ApplicantSubmissions values(@jobId,@applicantId,@title,@submissionDate)", 44 | new 45 | { 46 | jobId = applicantSubmission.JobId, 47 | applicantId = applicantSubmission.ApplicantId, 48 | title = applicantSubmission.Title, 49 | submissionDate = applicantSubmission.SubmissionDate 50 | }); 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /Services/Applicants.Api/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Applicants.Api.Messaging.Consumers; 3 | using MassTransit.Util; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Applicants.Api.Services; 9 | using Autofac; 10 | using Autofac.Extensions.DependencyInjection; 11 | using MassTransit; 12 | 13 | 14 | namespace Applicants.Api 15 | { 16 | public class Startup 17 | { 18 | public Startup(IConfiguration configuration) 19 | { 20 | Configuration = configuration; 21 | } 22 | 23 | public IContainer ApplicationContainer { get; private set; } 24 | 25 | public IConfiguration Configuration { get; } 26 | 27 | // This method gets called by the runtime. Use this method to add services to the container. 28 | public IServiceProvider ConfigureServices(IServiceCollection services) 29 | { 30 | services.AddMvc(); 31 | services.AddScoped(c => new ApplicantRepository(Configuration["ConnectionString"])); 32 | 33 | var builder = new ContainerBuilder(); 34 | 35 | // register a specific consumer 36 | builder.RegisterType(); 37 | 38 | builder.Register(context => 39 | { 40 | var busControl = Bus.Factory.CreateUsingRabbitMq(cfg => 41 | { 42 | var host = cfg.Host(new Uri("rabbitmq://rabbitmq/"), h => 43 | { 44 | h.Username("guest"); 45 | h.Password("guest"); 46 | }); 47 | 48 | // https://stackoverflow.com/questions/39573721/disable-round-robin-pattern-and-use-fanout-on-masstransit 49 | cfg.ReceiveEndpoint(host, "dotnetgigs" + Guid.NewGuid().ToString(), e => 50 | { 51 | e.LoadFrom(context); 52 | //e.Consumer(); 53 | }); 54 | }); 55 | 56 | return busControl; 57 | }) 58 | .SingleInstance() 59 | .As() 60 | .As(); 61 | 62 | builder.Populate(services); 63 | ApplicationContainer = builder.Build(); 64 | 65 | return new AutofacServiceProvider(ApplicationContainer); 66 | } 67 | 68 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 69 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime lifetime) 70 | { 71 | if (env.IsDevelopment()) 72 | { 73 | app.UseDeveloperExceptionPage(); 74 | } 75 | 76 | app.UseMvc(); 77 | 78 | var bus = ApplicationContainer.Resolve(); 79 | var busHandle = TaskUtil.Await(() => bus.StartAsync()); 80 | lifetime.ApplicationStopping.Register(() => busHandle.Stop()); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Services/Applicants.Api/applicants.api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp2.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Services/Applicants.Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Services/Applicants.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "Debug": { 5 | "LogLevel": { 6 | "Default": "Warning" 7 | } 8 | }, 9 | "Console": { 10 | "LogLevel": { 11 | "Default": "Warning" 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Services/Identity.Api/Controllers/UsersController.cs: -------------------------------------------------------------------------------- 1 |  2 | 3 | using System.Threading.Tasks; 4 | using Identity.Api.Models; 5 | using Identity.Api.Services; 6 | using Microsoft.AspNetCore.Mvc; 7 | 8 | namespace Identity.Api.Controllers 9 | { 10 | [Route("api/[controller]")] 11 | public class UsersController : Controller 12 | { 13 | private readonly IIdentityRepository _identityRespository; 14 | 15 | public UsersController(IIdentityRepository identityRespository) 16 | { 17 | _identityRespository = identityRespository; 18 | } 19 | 20 | // GET api/users/5 21 | [HttpGet("{id}")] 22 | public async Task Get(string id) 23 | { 24 | var user = await _identityRespository.GetUserAsync(id); 25 | return Ok(user); 26 | } 27 | 28 | // GET api/users/applicationcount/5 29 | [HttpGet("applicationcount/{id}")] 30 | public async Task GetUserApplicantCount(string id) 31 | { 32 | var count = await _identityRespository.GetUserApplicationCountAsync(id); 33 | return Ok(count); 34 | } 35 | 36 | 37 | // POST api/users 38 | [HttpPost] 39 | public async Task Post([FromBody]User value) 40 | { 41 | var user = await _identityRespository.UpdateUserAsync(value); 42 | return Ok(user); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Services/Identity.Api/Dockerfile.debug: -------------------------------------------------------------------------------- 1 | FROM microsoft/aspnetcore:latest 2 | RUN mkdir app 3 | 4 | #Install debugger 5 | RUN apt-get update 6 | RUN apt-get install curl -y unzip 7 | RUN curl -sSL https://aka.ms/getvsdbgsh | bash /dev/stdin -v latest -l /vsdbg 8 | 9 | EXPOSE 80/tcp 10 | 11 | #Keep the debugger container on 12 | ENTRYPOINT ["tail", "-f", "/dev/null"] -------------------------------------------------------------------------------- /Services/Identity.Api/Identity.Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp2.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Services/Identity.Api/Messaging/Consumers/ApplicantAppliedEventConsumer.cs: -------------------------------------------------------------------------------- 1 |  2 | using System.Threading.Tasks; 3 | using Events; 4 | using Identity.Api.Services; 5 | using MassTransit; 6 | 7 | namespace Identity.Api.Messaging.Consumers 8 | { 9 | public class ApplicantAppliedEventConsumer : IConsumer 10 | { 11 | private readonly IIdentityRepository _identityRepository; 12 | 13 | public ApplicantAppliedEventConsumer(IIdentityRepository applicantRepository) 14 | { 15 | _identityRepository = applicantRepository; 16 | } 17 | 18 | public async Task Consume(ConsumeContext context) 19 | { 20 | // increment the user's application count in the cache 21 | await _identityRepository.UpdateUserApplicationCountAsync(context.Message.ApplicantId.ToString()); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Services/Identity.Api/Models/User.cs: -------------------------------------------------------------------------------- 1 |  2 | 3 | namespace Identity.Api.Models 4 | { 5 | public class User 6 | { 7 | public string Id { get; set; } 8 | public string Name { get; set; } 9 | public string Email { get; set; } 10 | public string Avatar { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Services/Identity.Api/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace Identity.Api 12 | { 13 | public class Program 14 | { 15 | public static void Main(string[] args) 16 | { 17 | BuildWebHost(args).Run(); 18 | } 19 | 20 | public static IWebHost BuildWebHost(string[] args) => 21 | WebHost.CreateDefaultBuilder(args) 22 | .UseStartup() 23 | .Build(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Services/Identity.Api/Services/IIdentityRepository.cs: -------------------------------------------------------------------------------- 1 |  2 | using Identity.Api.Models; 3 | using System.Threading.Tasks; 4 | 5 | namespace Identity.Api.Services 6 | { 7 | public interface IIdentityRepository 8 | { 9 | Task UpdateUserAsync(User user); 10 | Task GetUserAsync(string userId); 11 | Task UpdateUserApplicationCountAsync(string userId); 12 | Task GetUserApplicationCountAsync(string userId); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Services/Identity.Api/Services/IdentityRepository.cs: -------------------------------------------------------------------------------- 1 |  2 | using System; 3 | using System.Threading.Tasks; 4 | using Identity.Api.Models; 5 | using Newtonsoft.Json; 6 | using StackExchange.Redis; 7 | 8 | namespace Identity.Api.Services 9 | { 10 | public class IdentityRepository : IIdentityRepository 11 | { 12 | private readonly IDatabase _database; 13 | 14 | public IdentityRepository(ConnectionMultiplexer redis) 15 | { 16 | _database = redis.GetDatabase(); 17 | } 18 | 19 | public async Task UpdateUserAsync(User user) 20 | { 21 | var created = await _database.StringSetAsync(user.Id, JsonConvert.SerializeObject(user)); 22 | if (!created) return null; 23 | return await GetUserAsync(user.Id); 24 | } 25 | 26 | public async Task UpdateUserApplicationCountAsync(string userId) 27 | { 28 | return await _database.StringIncrementAsync($"{userId}-appcnt"); 29 | } 30 | 31 | public async Task GetUserAsync(string userId) 32 | { 33 | var data = await _database.StringGetAsync(userId); 34 | return data.IsNullOrEmpty ? null : JsonConvert.DeserializeObject(data); 35 | } 36 | 37 | public async Task GetUserApplicationCountAsync(string userId) 38 | { 39 | var data = await _database.StringGetAsync($"{userId}-appcnt"); 40 | return data.IsNullOrEmpty ? 0 : Convert.ToInt64(data); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Services/Identity.Api/Startup.cs: -------------------------------------------------------------------------------- 1 |  2 | using System; 3 | using Autofac; 4 | using Autofac.Extensions.DependencyInjection; 5 | using Identity.Api.Messaging.Consumers; 6 | using Identity.Api.Models; 7 | using Identity.Api.Services; 8 | using MassTransit; 9 | using MassTransit.Util; 10 | using Microsoft.AspNetCore.Builder; 11 | using Microsoft.AspNetCore.Hosting; 12 | using Microsoft.Extensions.Configuration; 13 | using Microsoft.Extensions.DependencyInjection; 14 | using StackExchange.Redis; 15 | 16 | namespace Identity.Api 17 | { 18 | public class Startup 19 | { 20 | public Startup(IConfiguration configuration) 21 | { 22 | Configuration = configuration; 23 | } 24 | 25 | public IContainer ApplicationContainer { get; private set; } 26 | 27 | public IConfiguration Configuration { get; } 28 | 29 | // This method gets called by the runtime. Use this method to add services to the container. 30 | public IServiceProvider ConfigureServices(IServiceCollection services) 31 | { 32 | services.AddMvc(); 33 | 34 | //By connecting here we are making sure that our service 35 | //cannot start until redis is ready. This might slow down startup, 36 | //but given that there is a delay on resolving the ip address 37 | //and then creating the connection it seems reasonable to move 38 | //that cost to startup instead of having the first request pay the 39 | //penalty. 40 | services.AddSingleton(sp => 41 | { 42 | var configuration = new ConfigurationOptions {ResolveDns = true}; 43 | configuration.EndPoints.Add(Configuration["RedisHost"]); 44 | return ConnectionMultiplexer.Connect(configuration); 45 | }); 46 | 47 | services.AddTransient(); 48 | var builder = new ContainerBuilder(); 49 | 50 | // register a specific consumer 51 | builder.RegisterType(); 52 | 53 | builder.Register(context => 54 | { 55 | var busControl = Bus.Factory.CreateUsingRabbitMq(cfg => 56 | { 57 | var host = cfg.Host(new Uri("rabbitmq://rabbitmq/"), h => 58 | { 59 | h.Username("guest"); 60 | h.Password("guest"); 61 | }); 62 | 63 | // https://stackoverflow.com/questions/39573721/disable-round-robin-pattern-and-use-fanout-on-masstransit 64 | cfg.ReceiveEndpoint(host, "dotnetgigs" + Guid.NewGuid().ToString(), e => 65 | { 66 | e.LoadFrom(context); 67 | //e.Consumer(); 68 | }); 69 | }); 70 | 71 | return busControl; 72 | }) 73 | .SingleInstance() 74 | .As() 75 | .As(); 76 | 77 | builder.Populate(services); 78 | ApplicationContainer = builder.Build(); 79 | return new AutofacServiceProvider(ApplicationContainer); 80 | } 81 | 82 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 83 | public async void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider serviceProvider, IApplicationLifetime lifetime) 84 | { 85 | if (env.IsDevelopment()) 86 | { 87 | app.UseDeveloperExceptionPage(); 88 | } 89 | 90 | app.UseMvc(); 91 | 92 | // stash an applicant's user data in redis for test purposes...this would simulate establishing auth/session in the real world 93 | var identityRepository=serviceProvider.GetService(); 94 | await identityRepository.UpdateUserAsync(new User {Id = "1", Email = "josh903902@gmail.com",Name = "Josh Dillinger"}); 95 | 96 | var bus = ApplicationContainer.Resolve(); 97 | var busHandle = TaskUtil.Await(() => bus.StartAsync()); 98 | lifetime.ApplicationStopping.Register(() => busHandle.Stop()); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Services/Identity.Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Services/Identity.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "Debug": { 5 | "LogLevel": { 6 | "Default": "Warning" 7 | } 8 | }, 9 | "Console": { 10 | "LogLevel": { 11 | "Default": "Warning" 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Services/Jobs.Api/Controllers/JobsController.cs: -------------------------------------------------------------------------------- 1 |  2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Events; 5 | using Jobs.Api.Models; 6 | using Jobs.Api.Services; 7 | using MassTransit; 8 | using Microsoft.AspNetCore.Mvc; 9 | 10 | namespace Jobs.api.Controllers 11 | { 12 | [Route("api/[controller]")] 13 | public class JobsController : Controller 14 | { 15 | private readonly IJobRepository _jobRepository; 16 | private readonly IBus _bus; 17 | 18 | public JobsController(IJobRepository jobRepository, IBus bus) 19 | { 20 | _jobRepository = jobRepository; 21 | _bus = bus; 22 | } 23 | 24 | // GET api/jobs 25 | [HttpGet] 26 | public async Task> Get() 27 | { 28 | return await _jobRepository.GetAll(); 29 | } 30 | 31 | // GET api/jobs/5 32 | [HttpGet("{id}")] 33 | public async Task Get(int id) 34 | { 35 | return await _jobRepository.Get(id); 36 | } 37 | 38 | // POST api/values 39 | [HttpPost("/api/jobs/applicants")] 40 | public async Task Post([FromBody]JobApplicant model) 41 | { 42 | // fetch the job data 43 | var job = await _jobRepository.Get(model.JobId); 44 | var id = await _jobRepository.AddApplicant(model); 45 | //var endpoint = await _bus.GetSendEndpoint(new Uri("rabbitmq://rabbitmq/dotnetgigs")); //?bind=true&queue=dotnetgigs 46 | //await endpoint.Send(new { model.JobId,model.ApplicantId,job.Title}); 47 | await _bus.Publish(new { model.JobId, model.ApplicantId, job.Title }); 48 | return Ok(id); 49 | } 50 | 51 | // PUT api/values/5 52 | [HttpPut("{id}")] 53 | public void Put(int id, [FromBody]string value) 54 | { 55 | } 56 | 57 | // DELETE api/values/5 58 | [HttpDelete("{id}")] 59 | public void Delete(int id) 60 | { 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Services/Jobs.Api/Dockerfile.debug: -------------------------------------------------------------------------------- 1 | FROM microsoft/aspnetcore:latest 2 | RUN mkdir app 3 | 4 | #Install debugger 5 | RUN apt-get update 6 | RUN apt-get install curl -y unzip 7 | RUN curl -sSL https://aka.ms/getvsdbgsh | bash /dev/stdin -v latest -l /vsdbg 8 | 9 | EXPOSE 80/tcp 10 | 11 | #Keep the debugger container on 12 | ENTRYPOINT ["tail", "-f", "/dev/null"] -------------------------------------------------------------------------------- /Services/Jobs.Api/Models/Job.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace Jobs.Api.Models 5 | { 6 | public class Job 7 | { 8 | [Key] 9 | public int JobId { get; set; } 10 | public string Title { get; set; } 11 | public string Description { get; set; } 12 | public string Company { get; set; } 13 | public DateTime PostedDate { get; set; } 14 | public string Location { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /Services/Jobs.Api/Models/JobApplicant.cs: -------------------------------------------------------------------------------- 1 |  2 | 3 | using System; 4 | 5 | namespace Jobs.Api.Models 6 | { 7 | public class JobApplicant 8 | { 9 | public int Id { get; set; } 10 | public int JobId { get; set; } 11 | public int ApplicantId { get; set; } 12 | public string Name { get; set; } 13 | public string Email { get; set; } 14 | public DateTime CreatedDate { get; set; } 15 | public bool? Active { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Services/Jobs.Api/Program.cs: -------------------------------------------------------------------------------- 1 |  2 | using Jobs.Api; 3 | using Microsoft.AspNetCore; 4 | using Microsoft.AspNetCore.Hosting; 5 | 6 | namespace jobs.api 7 | { 8 | public class Program 9 | { 10 | public static void Main(string[] args) 11 | { 12 | BuildWebHost(args).Run(); 13 | } 14 | 15 | public static IWebHost BuildWebHost(string[] args) => 16 | WebHost.CreateDefaultBuilder(args) 17 | .UseStartup() 18 | .Build(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Services/Jobs.Api/Services/IJobRepository.cs: -------------------------------------------------------------------------------- 1 |  2 | 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | using Jobs.Api.Models; 6 | 7 | namespace Jobs.Api.Services 8 | { 9 | public interface IJobRepository 10 | { 11 | Task> GetAll(); 12 | Task Get(int jobId); 13 | Task AddApplicant(JobApplicant jobApplicant); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Services/Jobs.Api/Services/JobRepository.cs: -------------------------------------------------------------------------------- 1 |  2 | 3 | using System.Collections.Generic; 4 | using System.Data; 5 | using System.Data.SqlClient; 6 | using System.Threading.Tasks; 7 | using Jobs.Api.Models; 8 | using Dapper; 9 | 10 | namespace Jobs.Api.Services 11 | { 12 | public class JobRepository : IJobRepository 13 | { 14 | private readonly string _connectionString; 15 | 16 | public JobRepository(string connectionString) 17 | { 18 | _connectionString = connectionString; 19 | } 20 | 21 | public IDbConnection Connection => new SqlConnection(_connectionString); 22 | 23 | public async Task> GetAll() 24 | { 25 | using (var dbConnection = Connection) 26 | { 27 | dbConnection.Open(); 28 | return await dbConnection.QueryAsync("SELECT * FROM Jobs"); 29 | } 30 | } 31 | 32 | public async Task Get(int jobId) 33 | { 34 | using (var dbConnection = Connection) 35 | { 36 | dbConnection.Open(); 37 | return await dbConnection.QueryFirstOrDefaultAsync("SELECT * FROM Jobs where JobId=@JobId", new{JobId=jobId}); 38 | } 39 | } 40 | 41 | public async Task AddApplicant(JobApplicant jobApplicant) 42 | { 43 | using (var dbConnection = Connection) 44 | { 45 | dbConnection.Open(); 46 | return await dbConnection.ExecuteAsync( 47 | "insert JobApplicants values(@jobId,@applicantId,@name,@email,getutcdate(),1)", 48 | new 49 | { 50 | jobId = jobApplicant.JobId, 51 | applicantId = jobApplicant.ApplicantId, 52 | name = jobApplicant.Name, 53 | email = jobApplicant.Email 54 | }); 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /Services/Jobs.Api/Startup.cs: -------------------------------------------------------------------------------- 1 |  2 | using System; 3 | using Autofac; 4 | using Autofac.Extensions.DependencyInjection; 5 | using Jobs.Api.Services; 6 | using MassTransit; 7 | using MassTransit.Util; 8 | using Microsoft.AspNetCore.Builder; 9 | using Microsoft.AspNetCore.Hosting; 10 | using Microsoft.Extensions.Configuration; 11 | using Microsoft.Extensions.DependencyInjection; 12 | using RabbitMQ.Client; 13 | 14 | namespace Jobs.Api 15 | { 16 | public class Startup 17 | { 18 | public Startup(IConfiguration configuration) 19 | { 20 | Configuration = configuration; 21 | } 22 | 23 | public IContainer ApplicationContainer { get; private set; } 24 | 25 | public IConfiguration Configuration { get; } 26 | 27 | // This method gets called by the runtime. Use this method to add services to the container. 28 | public IServiceProvider ConfigureServices(IServiceCollection services) 29 | { 30 | services.AddMvc(); 31 | services.AddScoped(c => new JobRepository(Configuration["ConnectionString"])); 32 | 33 | var builder = new ContainerBuilder(); 34 | builder.Register(c => 35 | { 36 | return Bus.Factory.CreateUsingRabbitMq(sbc => 37 | { 38 | sbc.Host("rabbitmq", "/", h => 39 | { 40 | h.Username("guest"); 41 | h.Password("guest"); 42 | }); 43 | 44 | sbc.ExchangeType = ExchangeType.Fanout; 45 | }); 46 | }) 47 | .As() 48 | .As() 49 | .As() 50 | .SingleInstance(); 51 | 52 | builder.Populate(services); 53 | ApplicationContainer = builder.Build(); 54 | 55 | return new AutofacServiceProvider(ApplicationContainer); 56 | } 57 | 58 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 59 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime lifetime) 60 | { 61 | if (env.IsDevelopment()) 62 | { 63 | app.UseDeveloperExceptionPage(); 64 | } 65 | 66 | app.UseMvc(); 67 | 68 | var bus = ApplicationContainer.Resolve(); 69 | var busHandle = TaskUtil.Await(() => bus.StartAsync()); 70 | lifetime.ApplicationStopping.Register(() => busHandle.Stop()); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Services/Jobs.Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Services/Jobs.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "Debug": { 5 | "LogLevel": { 6 | "Default": "Warning" 7 | } 8 | }, 9 | "Console": { 10 | "LogLevel": { 11 | "Default": "Warning" 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Services/Jobs.Api/jobs.api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp2.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Web/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "wwwroot/lib" 3 | } 4 | -------------------------------------------------------------------------------- /Web/Config/ApiConfig.cs: -------------------------------------------------------------------------------- 1 |  2 | 3 | namespace Web.Config 4 | { 5 | public class ApiConfig 6 | { 7 | public string IdentityApiUrl { get; set; } 8 | public string JobsApiUrl { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Web/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 |  2 | using System.Diagnostics; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Web.Models; 6 | using Web.Services; 7 | using Web.ViewModels.HomeViewModels; 8 | 9 | namespace Web.Controllers 10 | { 11 | public class HomeController : Controller 12 | { 13 | private readonly IJobService _jobService; 14 | 15 | 16 | public HomeController(IJobService jobService) 17 | { 18 | _jobService = jobService; 19 | 20 | } 21 | 22 | public async Task Index() 23 | { 24 | return View(new IndexViewModel { Jobs = await _jobService.GetJobs() }); 25 | } 26 | 27 | public IActionResult About() 28 | { 29 | ViewData["Message"] = "Your application description page."; 30 | 31 | return View(); 32 | } 33 | 34 | public IActionResult Contact() 35 | { 36 | ViewData["Message"] = "Your contact page."; 37 | 38 | return View(); 39 | } 40 | 41 | public IActionResult Error() 42 | { 43 | return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Web/Controllers/JobsController.cs: -------------------------------------------------------------------------------- 1 |  2 | 3 | using System; 4 | using System.Runtime.CompilerServices; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Web.Services; 9 | using Web.ViewModels; 10 | using Web.ViewModels.JobsViewModels; 11 | 12 | namespace Web.Controllers 13 | { 14 | public class JobsController : Controller 15 | { 16 | private readonly IJobService _jobService; 17 | private readonly IIdentityService _identityService; 18 | 19 | public JobsController(IJobService jobService, IIdentityService identityService) 20 | { 21 | _jobService = jobService; 22 | _identityService = identityService; 23 | } 24 | 25 | // GET jobs/apply/5 26 | [HttpGet("jobs/apply/{id}")] 27 | public async Task Apply(int id) 28 | { 29 | var job = await _jobService.GetJob(id); 30 | var applicant = await _identityService.GetUserAsync("1"); // hardcoded to user we stashed in identity service 31 | return View(new ApplyViewModel(job,applicant)); 32 | } 33 | 34 | // POST jobs/apply 35 | [HttpPost] 36 | [ValidateAntiForgeryToken] 37 | public async Task Apply(JobApplicationViewModel model) 38 | { 39 | await _jobService.AddApplicant(new JobApplicant 40 | { 41 | Email = model.Applicant.Email, 42 | Name = model.Applicant.Name, 43 | ApplicantId = Convert.ToInt32(model.Applicant.Id), 44 | JobId = model.Job.JobId 45 | }); 46 | 47 | return RedirectToAction("ApplySuccess",new{jobId=model.Job.JobId,applicantId= model.Applicant.Id }); 48 | } 49 | 50 | // GET jobs/apply/success/5 51 | [HttpGet("jobs/apply/success/{id}")] 52 | public async Task ApplySuccess(int jobId,string applicantId) 53 | { 54 | await Task.Delay(500); // give a sec for redis to eventually update 55 | var job = await _jobService.GetJob(jobId); 56 | var applicationCount = await _identityService.GetUserApplicationCountAsync(applicantId); 57 | return View(new ApplySuccessViewModel(job,applicationCount)); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Web/Dockerfile.debug: -------------------------------------------------------------------------------- 1 | FROM microsoft/aspnetcore:latest 2 | RUN mkdir app 3 | 4 | #Install debugger 5 | RUN apt-get update 6 | RUN apt-get install curl -y unzip 7 | RUN curl -sSL https://aka.ms/getvsdbgsh | bash /dev/stdin -v latest -l /vsdbg 8 | 9 | EXPOSE 80/tcp 10 | WORKDIR app 11 | 12 | #Keep the debugger container on 13 | ENTRYPOINT ["tail", "-f", "/dev/null"] -------------------------------------------------------------------------------- /Web/Models/ErrorViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Web.Models 4 | { 5 | public class ErrorViewModel 6 | { 7 | public string RequestId { get; set; } 8 | 9 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 10 | } 11 | } -------------------------------------------------------------------------------- /Web/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace Web 12 | { 13 | public class Program 14 | { 15 | public static void Main(string[] args) 16 | { 17 | BuildWebHost(args).Run(); 18 | } 19 | 20 | public static IWebHost BuildWebHost(string[] args) => 21 | WebHost.CreateDefaultBuilder(args) 22 | .UseStartup() 23 | .Build(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Web/Services/IIdentityService.cs: -------------------------------------------------------------------------------- 1 |  2 | using System.Threading.Tasks; 3 | using Web.ViewModels; 4 | 5 | namespace Web.Services 6 | { 7 | public interface IIdentityService 8 | { 9 | Task UpdateUserAsync(User user); 10 | Task GetUserAsync(string userId); 11 | Task GetUserApplicationCountAsync(string userId); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Web/Services/IJobService.cs: -------------------------------------------------------------------------------- 1 |  2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Web.ViewModels; 5 | 6 | namespace Web.Services 7 | { 8 | public interface IJobService 9 | { 10 | Task> GetJobs(); 11 | Task GetJob(int jobId); 12 | Task AddApplicant(JobApplicant jobApplicant); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Web/Services/IdentityService.cs: -------------------------------------------------------------------------------- 1 |  2 | using System; 3 | using System.Threading.Tasks; 4 | using Http; 5 | using Newtonsoft.Json; 6 | using Web.Config; 7 | using Web.ViewModels; 8 | 9 | namespace Web.Services 10 | { 11 | public class IdentityService : IIdentityService 12 | { 13 | private readonly IHttpClient _apiClient; 14 | private readonly ApiConfig _apiConfig; 15 | private const string BasePath= "/api/users"; 16 | 17 | public IdentityService(IHttpClient httpClient, ApiConfig apiConfig) 18 | { 19 | _apiClient = httpClient; 20 | _apiConfig = apiConfig; 21 | } 22 | 23 | public async Task UpdateUserAsync(User user) 24 | { 25 | var response = await _apiClient.PostAsync(_apiConfig.IdentityApiUrl+BasePath, user); 26 | response.EnsureSuccessStatusCode(); 27 | } 28 | 29 | public async Task GetUserAsync(string userId) 30 | { 31 | var dataString = await _apiClient.GetStringAsync(_apiConfig.IdentityApiUrl+ BasePath + "/" + userId); 32 | return JsonConvert.DeserializeObject(dataString); 33 | } 34 | 35 | public async Task GetUserApplicationCountAsync(string userId) 36 | { 37 | var dataString = await _apiClient.GetStringAsync(_apiConfig.IdentityApiUrl + BasePath + "/applicationcount/" + userId); 38 | return Convert.ToInt64(dataString); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Web/Services/JobService.cs: -------------------------------------------------------------------------------- 1 |  2 | 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | using Http; 6 | using Newtonsoft.Json; 7 | using Web.Config; 8 | using Web.ViewModels; 9 | 10 | namespace Web.Services 11 | { 12 | public class JobService : IJobService 13 | { 14 | private readonly IHttpClient _apiClient; 15 | private readonly ApiConfig _apiConfig; 16 | 17 | public JobService(IHttpClient httpClient, ApiConfig apiConfig) 18 | { 19 | _apiClient = httpClient; 20 | _apiConfig = apiConfig; 21 | } 22 | 23 | public async Task> GetJobs() 24 | { 25 | var dataString = await _apiClient.GetStringAsync(_apiConfig.JobsApiUrl+"/api/jobs"); 26 | return JsonConvert.DeserializeObject>(dataString); 27 | } 28 | 29 | public async Task GetJob(int jobId) 30 | { 31 | var dataString = await _apiClient.GetStringAsync(_apiConfig.JobsApiUrl + "/api/jobs/"+jobId); 32 | return JsonConvert.DeserializeObject(dataString); 33 | } 34 | 35 | public async Task AddApplicant(JobApplicant jobApplicant) 36 | { 37 | var response = await _apiClient.PostAsync(_apiConfig.JobsApiUrl + "/api/jobs/applicants",jobApplicant); 38 | response.EnsureSuccessStatusCode(); 39 | } 40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /Web/Startup.cs: -------------------------------------------------------------------------------- 1 |  2 | using System; 3 | using Autofac; 4 | using Autofac.Extensions.DependencyInjection; 5 | using Http; 6 | using Microsoft.AspNetCore.Builder; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Web.Config; 11 | using Web.Services; 12 | 13 | namespace Web 14 | { 15 | public class Startup 16 | { 17 | public IContainer ApplicationContainer { get; private set; } 18 | 19 | public Startup(IConfiguration configuration) 20 | { 21 | Configuration = configuration; 22 | } 23 | 24 | public IConfiguration Configuration { get; } 25 | 26 | // This method gets called by the runtime. Use this method to add services to the container. 27 | public IServiceProvider ConfigureServices(IServiceCollection services) 28 | { 29 | services.AddMvc(); 30 | 31 | var builder = new ContainerBuilder(); 32 | builder.RegisterInstance(new StandardHttpClient()); 33 | services.AddTransient(); 34 | services.AddTransient(); 35 | builder.RegisterInstance(Configuration.GetSection("ApiSettings").Get()); 36 | 37 | builder.Populate(services); 38 | ApplicationContainer = builder.Build(); 39 | 40 | return new AutofacServiceProvider(ApplicationContainer); 41 | } 42 | 43 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 44 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime appLifetime) 45 | { 46 | if (env.IsDevelopment()) 47 | { 48 | app.UseDeveloperExceptionPage(); 49 | } 50 | else 51 | { 52 | app.UseExceptionHandler("/Home/Error"); 53 | } 54 | 55 | app.UseStaticFiles(); 56 | 57 | app.UseMvc(routes => 58 | { 59 | routes.MapRoute( 60 | name: "default", 61 | template: "{controller=Home}/{action=Index}/{id?}"); 62 | }); 63 | 64 | // If you want to dispose of resources that have been resolved in the 65 | // application container, register for the "ApplicationStopped" event. 66 | // You can only do this if you have a direct reference to the container, 67 | // so it won't work with the above ConfigureContainer mechanism. 68 | appLifetime.ApplicationStopped.Register(() => ApplicationContainer.Dispose()); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Web/ViewModels/HomeViewModels/IndexViewModel.cs: -------------------------------------------------------------------------------- 1 |  2 | 3 | using System.Collections.Generic; 4 | 5 | namespace Web.ViewModels.HomeViewModels 6 | { 7 | public class IndexViewModel 8 | { 9 | public IEnumerable Jobs { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Web/ViewModels/Job.cs: -------------------------------------------------------------------------------- 1 |  2 | using System; 3 | 4 | namespace Web.ViewModels 5 | { 6 | public class Job 7 | { 8 | public int JobId { get; set; } 9 | public string Title { get; set; } 10 | public string Description { get; set; } 11 | public string Company { get; set; } 12 | public DateTime PostedDate { get; set; } 13 | public string Location { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Web/ViewModels/JobApplicant.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace Web.ViewModels 3 | { 4 | public class JobApplicant 5 | { 6 | public int Id { get; set; } 7 | public int JobId { get; set; } 8 | public int ApplicantId { get; set; } 9 | public string Name { get; set; } 10 | public string Email { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Web/ViewModels/JobsViewModels/ApplySuccessViewModel.cs: -------------------------------------------------------------------------------- 1 |  2 | 3 | namespace Web.ViewModels.JobsViewModels 4 | { 5 | public class ApplySuccessViewModel 6 | { 7 | public Job Job { get; } 8 | public long RecentApplicantCount { get; } 9 | 10 | public ApplySuccessViewModel(Job job, long recentApplicantCount) 11 | { 12 | Job = job; 13 | RecentApplicantCount = recentApplicantCount; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Web/ViewModels/JobsViewModels/ApplyViewModel.cs: -------------------------------------------------------------------------------- 1 |  2 | 3 | namespace Web.ViewModels.JobsViewModels 4 | { 5 | public class ApplyViewModel 6 | { 7 | public Job Job { get; } 8 | public User Applicant { get; } 9 | 10 | public ApplyViewModel(Job job,User applicant) 11 | { 12 | Job = job; 13 | Applicant = applicant; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Web/ViewModels/JobsViewModels/JobApplicationViewModel.cs: -------------------------------------------------------------------------------- 1 |  2 | 3 | namespace Web.ViewModels.JobsViewModels 4 | { 5 | public class JobApplicationViewModel 6 | { 7 | public User Applicant { get; set; } 8 | public Job Job { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Web/ViewModels/User.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace Web.ViewModels 3 | { 4 | public class User 5 | { 6 | public string Id { get; set; } 7 | public string Name { get; set; } 8 | public string Email { get; set; } 9 | public string Avatar { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Web/Views/Home/About.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "About"; 3 | } 4 |

@ViewData["Title"]

5 |

@ViewData["Message"]

6 | 7 |

Use this area to provide additional information.

8 | -------------------------------------------------------------------------------- /Web/Views/Home/Contact.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Contact"; 3 | } 4 |

@ViewData["Title"]

5 |

@ViewData["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 |
18 | -------------------------------------------------------------------------------- /Web/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Home Page"; 3 | @model Web.ViewModels.HomeViewModels.IndexViewModel 4 | } 5 | 6 |
7 |
8 |

New Jobs (@Model.Jobs.Count())

9 |
10 |
11 |
12 | 13 |
14 |
15 | @foreach (var job in Model.Jobs) 16 | { 17 |
18 |
19 |

@job.Title

20 | @job.Company | @job.Location | posted on @job.PostedDate.ToString("MM/dd/yyyy") 21 |
22 |

@job.Description

23 | Apply for this job 24 |
25 |
26 | } 27 |
28 |
29 | 30 | -------------------------------------------------------------------------------- /Web/Views/Jobs/Apply.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Apply for a gig"; 3 | @model Web.ViewModels.JobsViewModels.ApplyViewModel 4 | } 5 | 6 |
7 |
8 |

@Model.Job.Title

9 | 10 |

@Model.Job.Description

11 |
12 |
13 |

You're applying as:

14 | 15 | 16 |
17 | 18 | 19 |
20 |
21 | 22 | 23 |
24 | 25 |
26 | 27 |
28 |
-------------------------------------------------------------------------------- /Web/Views/Jobs/ApplySuccess.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "You've successfully applied to: " + Model.Job.Title; 3 | @model Web.ViewModels.JobsViewModels.ApplySuccessViewModel 4 | } 5 | 6 |
7 |
8 |

Way to go!

9 |
10 |

You've successfully applied to @Model.Job.Company for the position of @Model.Job.Title.

11 |

You've recently applied for @Model.RecentApplicantCount job(s).

12 | Cool, let's go home. 13 |
14 |
-------------------------------------------------------------------------------- /Web/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @model ErrorViewModel 2 | @{ 3 | ViewData["Title"] = "Error"; 4 | } 5 | 6 |

Error.

7 |

An error occurred while processing your request.

8 | 9 | @if (Model.ShowRequestId) 10 | { 11 |

12 | Request ID: @Model.RequestId 13 |

14 | } 15 | 16 |

Development Mode

17 |

18 | Swapping to Development environment will display more detailed information about the error that occurred. 19 |

20 |

21 | Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application. 22 |

23 | -------------------------------------------------------------------------------- /Web/Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | DotNetGigs - @ViewData["Title"] 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | 20 | 40 |
41 | @RenderBody() 42 |
43 |
44 |

© @DateTime.Today.Year - DotNetGigs

45 |
46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 60 | 66 | 67 | 68 | 69 | @RenderSection("Scripts", required: false) 70 | 71 | 72 | -------------------------------------------------------------------------------- /Web/Views/Shared/_ValidationScriptsPartial.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /Web/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using Web 2 | @using Web.Models 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | -------------------------------------------------------------------------------- /Web/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /Web/Web.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp2.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Web/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Web/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Warning" 6 | } 7 | }, 8 | "ApiSettings": { 9 | "IdentityApiUrl": "http://identity.api", 10 | "JobsApiUrl": "http://jobs.api" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Web/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asp.net", 3 | "private": true, 4 | "dependencies": { 5 | "bootstrap": "3.3.7", 6 | "jquery": "2.2.0", 7 | "jquery-validation": "1.14.0", 8 | "jquery-validation-unobtrusive": "3.2.6" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Web/bundleconfig.json: -------------------------------------------------------------------------------- 1 | // Configure bundling and minification for the project. 2 | // More info at https://go.microsoft.com/fwlink/?LinkId=808241 3 | [ 4 | { 5 | "outputFileName": "wwwroot/css/site.min.css", 6 | // An array of relative input file paths. Globbing patterns supported 7 | "inputFiles": [ 8 | "wwwroot/css/site.css" 9 | ] 10 | }, 11 | { 12 | "outputFileName": "wwwroot/js/site.min.js", 13 | "inputFiles": [ 14 | "wwwroot/js/site.js" 15 | ], 16 | // Optionally specify minification options 17 | "minify": { 18 | "enabled": true, 19 | "renameLocals": true 20 | }, 21 | // Optionally generate .map file 22 | "sourceMap": false 23 | } 24 | ] 25 | -------------------------------------------------------------------------------- /Web/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | padding-bottom: 20px; 4 | } 5 | 6 | /* Wrapping element */ 7 | /* Set some basic padding to keep content from hitting the edges */ 8 | .body-content { 9 | padding-left: 15px; 10 | padding-right: 15px; 11 | } 12 | 13 | /* Carousel */ 14 | .carousel-caption p { 15 | font-size: 20px; 16 | line-height: 1.4; 17 | } 18 | 19 | /* Make .svg files in the carousel display properly in older browsers */ 20 | .carousel-inner .item img[src$=".svg"] { 21 | width: 100%; 22 | } 23 | 24 | /* QR code generator */ 25 | #qrCode { 26 | margin: 15px; 27 | } 28 | 29 | /* Hide/rearrange for smaller screens */ 30 | @media screen and (max-width: 767px) { 31 | /* Hide captions */ 32 | .carousel-caption { 33 | display: none; 34 | } 35 | } 36 | 37 | .posting { padding: .1em 0 1.05em 0} 38 | .posting > div.title-container { border-bottom: solid 1px #f0f0f0;padding-bottom: .75em} 39 | .posting > div.title-container > h3 { margin-bottom: 0} 40 | .posting > div.title-container > span { color: #999 } 41 | .posting > p.description { padding:1.25em 0} 42 | 43 | .posting-apply > p.post-meta {color: #999;font-size:.85em} 44 | 45 | .application-success { float: none;margin: 0 auto;padding-top: 2.25em} 46 | .application-success > h1 { color: #3d9668;font-weight: bold;font-size:3.65em} 47 | .application-success > .summary-description { font-size: 1.5em;padding: 2em 0 1.25em 0} 48 | .application-success > .summary-recent-count {font-size: 1.5em;padding:.65em 0} 49 | .application-success .recent-application-count {color: #3d9668;font-size: 1.5em} 50 | 51 | .no-margin {margin:0} 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /Web/wwwroot/css/site.min.css: -------------------------------------------------------------------------------- 1 | body{padding-top:50px;padding-bottom:20px}.body-content{padding-left:15px;padding-right:15px}.carousel-caption p{font-size:20px;line-height:1.4}.carousel-inner .item img[src$=".svg"]{width:100%}#qrCode{margin:15px}@media screen and (max-width:767px){.carousel-caption{display:none}}.posting{padding:.1em 0 1.05em 0}.posting>div.title-container{border-bottom:solid 1px #f0f0f0;padding-bottom:.75em}.posting>div.title-container>h3{margin-bottom:0}.posting>div.title-container>span{color:#999}.posting>p.description{padding:1.25em 0}.posting-apply>p.post-meta{color:#999;font-size:.85em}.application-success{float:none;margin:0 auto;padding-top:2.25em}.application-success>h1{color:#3d9668;font-weight:bold;font-size:3.65em}.application-success>.summary-description{font-size:1.5em;padding:2em 0 1.25em 0}.application-success>.summary-recent-count{font-size:1.5em;padding:.65em 0}.application-success .recent-application-count{color:#3d9668;font-size:1.5em}.no-margin{margin:0} -------------------------------------------------------------------------------- /Web/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmacneil/ASPNETCoreDockerMicroservices/8181ee59a0dfd2d75819da9633b401b350587ae2/Web/wwwroot/favicon.ico -------------------------------------------------------------------------------- /Web/wwwroot/images/banner1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Web/wwwroot/images/banner2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Web/wwwroot/images/banner3.svg: -------------------------------------------------------------------------------- 1 | banner3b -------------------------------------------------------------------------------- /Web/wwwroot/images/banner4.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Web/wwwroot/js/site.js: -------------------------------------------------------------------------------- 1 | // Write your JavaScript code. 2 | -------------------------------------------------------------------------------- /Web/wwwroot/js/site.min.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmacneil/ASPNETCoreDockerMicroservices/8181ee59a0dfd2d75819da9633b401b350587ae2/Web/wwwroot/js/site.min.js -------------------------------------------------------------------------------- /Web/wwwroot/lib/bootstrap/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bootstrap", 3 | "description": "The most popular front-end framework for developing responsive, mobile first projects on the web.", 4 | "keywords": [ 5 | "css", 6 | "js", 7 | "less", 8 | "mobile-first", 9 | "responsive", 10 | "front-end", 11 | "framework", 12 | "web" 13 | ], 14 | "homepage": "http://getbootstrap.com", 15 | "license": "MIT", 16 | "moduleType": "globals", 17 | "main": [ 18 | "less/bootstrap.less", 19 | "dist/js/bootstrap.js" 20 | ], 21 | "ignore": [ 22 | "/.*", 23 | "_config.yml", 24 | "CNAME", 25 | "composer.json", 26 | "CONTRIBUTING.md", 27 | "docs", 28 | "js/tests", 29 | "test-infra" 30 | ], 31 | "dependencies": { 32 | "jquery": "1.9.1 - 3" 33 | }, 34 | "version": "3.3.7", 35 | "_release": "3.3.7", 36 | "_resolution": { 37 | "type": "version", 38 | "tag": "v3.3.7", 39 | "commit": "0b9c4a4007c44201dce9a6cc1a38407005c26c86" 40 | }, 41 | "_source": "https://github.com/twbs/bootstrap.git", 42 | "_target": "v3.3.7", 43 | "_originalSource": "bootstrap", 44 | "_direct": true 45 | } -------------------------------------------------------------------------------- /Web/wwwroot/lib/bootstrap/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2016 Twitter, Inc. 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Web/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmacneil/ASPNETCoreDockerMicroservices/8181ee59a0dfd2d75819da9633b401b350587ae2/Web/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /Web/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmacneil/ASPNETCoreDockerMicroservices/8181ee59a0dfd2d75819da9633b401b350587ae2/Web/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /Web/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmacneil/ASPNETCoreDockerMicroservices/8181ee59a0dfd2d75819da9633b401b350587ae2/Web/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /Web/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmacneil/ASPNETCoreDockerMicroservices/8181ee59a0dfd2d75819da9633b401b350587ae2/Web/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /Web/wwwroot/lib/bootstrap/dist/js/npm.js: -------------------------------------------------------------------------------- 1 | // This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment. 2 | require('../../js/transition.js') 3 | require('../../js/alert.js') 4 | require('../../js/button.js') 5 | require('../../js/carousel.js') 6 | require('../../js/collapse.js') 7 | require('../../js/dropdown.js') 8 | require('../../js/modal.js') 9 | require('../../js/tooltip.js') 10 | require('../../js/popover.js') 11 | require('../../js/scrollspy.js') 12 | require('../../js/tab.js') 13 | require('../../js/affix.js') -------------------------------------------------------------------------------- /Web/wwwroot/lib/jquery-validation-unobtrusive/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-validation-unobtrusive", 3 | "version": "3.2.6", 4 | "homepage": "https://github.com/aspnet/jquery-validation-unobtrusive", 5 | "description": "Add-on to jQuery Validation to enable unobtrusive validation options in data-* attributes.", 6 | "main": [ 7 | "jquery.validate.unobtrusive.js" 8 | ], 9 | "ignore": [ 10 | "**/.*", 11 | "*.json", 12 | "*.md", 13 | "*.txt", 14 | "gulpfile.js" 15 | ], 16 | "keywords": [ 17 | "jquery", 18 | "asp.net", 19 | "mvc", 20 | "validation", 21 | "unobtrusive" 22 | ], 23 | "authors": [ 24 | "Microsoft" 25 | ], 26 | "license": "http://www.microsoft.com/web/webpi/eula/net_library_eula_enu.htm", 27 | "repository": { 28 | "type": "git", 29 | "url": "git://github.com/aspnet/jquery-validation-unobtrusive.git" 30 | }, 31 | "dependencies": { 32 | "jquery-validation": ">=1.8", 33 | "jquery": ">=1.8" 34 | }, 35 | "_release": "3.2.6", 36 | "_resolution": { 37 | "type": "version", 38 | "tag": "v3.2.6", 39 | "commit": "13386cd1b5947d8a5d23a12b531ce3960be1eba7" 40 | }, 41 | "_source": "git://github.com/aspnet/jquery-validation-unobtrusive.git", 42 | "_target": "3.2.6", 43 | "_originalSource": "jquery-validation-unobtrusive" 44 | } -------------------------------------------------------------------------------- /Web/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js: -------------------------------------------------------------------------------- 1 | /*! 2 | ** Unobtrusive validation support library for jQuery and jQuery Validate 3 | ** Copyright (C) Microsoft Corporation. All rights reserved. 4 | */ 5 | 6 | /*jslint white: true, browser: true, onevar: true, undef: true, nomen: true, eqeqeq: true, plusplus: true, bitwise: true, regexp: true, newcap: true, immed: true, strict: false */ 7 | /*global document: false, jQuery: false */ 8 | 9 | (function ($) { 10 | var $jQval = $.validator, 11 | adapters, 12 | data_validation = "unobtrusiveValidation"; 13 | 14 | function setValidationValues(options, ruleName, value) { 15 | options.rules[ruleName] = value; 16 | if (options.message) { 17 | options.messages[ruleName] = options.message; 18 | } 19 | } 20 | 21 | function splitAndTrim(value) { 22 | return value.replace(/^\s+|\s+$/g, "").split(/\s*,\s*/g); 23 | } 24 | 25 | function escapeAttributeValue(value) { 26 | // As mentioned on http://api.jquery.com/category/selectors/ 27 | return value.replace(/([!"#$%&'()*+,./:;<=>?@\[\\\]^`{|}~])/g, "\\$1"); 28 | } 29 | 30 | function getModelPrefix(fieldName) { 31 | return fieldName.substr(0, fieldName.lastIndexOf(".") + 1); 32 | } 33 | 34 | function appendModelPrefix(value, prefix) { 35 | if (value.indexOf("*.") === 0) { 36 | value = value.replace("*.", prefix); 37 | } 38 | return value; 39 | } 40 | 41 | function onError(error, inputElement) { // 'this' is the form element 42 | var container = $(this).find("[data-valmsg-for='" + escapeAttributeValue(inputElement[0].name) + "']"), 43 | replaceAttrValue = container.attr("data-valmsg-replace"), 44 | replace = replaceAttrValue ? $.parseJSON(replaceAttrValue) !== false : null; 45 | 46 | container.removeClass("field-validation-valid").addClass("field-validation-error"); 47 | error.data("unobtrusiveContainer", container); 48 | 49 | if (replace) { 50 | container.empty(); 51 | error.removeClass("input-validation-error").appendTo(container); 52 | } 53 | else { 54 | error.hide(); 55 | } 56 | } 57 | 58 | function onErrors(event, validator) { // 'this' is the form element 59 | var container = $(this).find("[data-valmsg-summary=true]"), 60 | list = container.find("ul"); 61 | 62 | if (list && list.length && validator.errorList.length) { 63 | list.empty(); 64 | container.addClass("validation-summary-errors").removeClass("validation-summary-valid"); 65 | 66 | $.each(validator.errorList, function () { 67 | $("
  • ").html(this.message).appendTo(list); 68 | }); 69 | } 70 | } 71 | 72 | function onSuccess(error) { // 'this' is the form element 73 | var container = error.data("unobtrusiveContainer"); 74 | 75 | if (container) { 76 | var replaceAttrValue = container.attr("data-valmsg-replace"), 77 | replace = replaceAttrValue ? $.parseJSON(replaceAttrValue) : null; 78 | 79 | container.addClass("field-validation-valid").removeClass("field-validation-error"); 80 | error.removeData("unobtrusiveContainer"); 81 | 82 | if (replace) { 83 | container.empty(); 84 | } 85 | } 86 | } 87 | 88 | function onReset(event) { // 'this' is the form element 89 | var $form = $(this), 90 | key = '__jquery_unobtrusive_validation_form_reset'; 91 | if ($form.data(key)) { 92 | return; 93 | } 94 | // Set a flag that indicates we're currently resetting the form. 95 | $form.data(key, true); 96 | try { 97 | $form.data("validator").resetForm(); 98 | } finally { 99 | $form.removeData(key); 100 | } 101 | 102 | $form.find(".validation-summary-errors") 103 | .addClass("validation-summary-valid") 104 | .removeClass("validation-summary-errors"); 105 | $form.find(".field-validation-error") 106 | .addClass("field-validation-valid") 107 | .removeClass("field-validation-error") 108 | .removeData("unobtrusiveContainer") 109 | .find(">*") // If we were using valmsg-replace, get the underlying error 110 | .removeData("unobtrusiveContainer"); 111 | } 112 | 113 | function validationInfo(form) { 114 | var $form = $(form), 115 | result = $form.data(data_validation), 116 | onResetProxy = $.proxy(onReset, form), 117 | defaultOptions = $jQval.unobtrusive.options || {}, 118 | execInContext = function (name, args) { 119 | var func = defaultOptions[name]; 120 | func && $.isFunction(func) && func.apply(form, args); 121 | } 122 | 123 | if (!result) { 124 | result = { 125 | options: { // options structure passed to jQuery Validate's validate() method 126 | errorClass: defaultOptions.errorClass || "input-validation-error", 127 | errorElement: defaultOptions.errorElement || "span", 128 | errorPlacement: function () { 129 | onError.apply(form, arguments); 130 | execInContext("errorPlacement", arguments); 131 | }, 132 | invalidHandler: function () { 133 | onErrors.apply(form, arguments); 134 | execInContext("invalidHandler", arguments); 135 | }, 136 | messages: {}, 137 | rules: {}, 138 | success: function () { 139 | onSuccess.apply(form, arguments); 140 | execInContext("success", arguments); 141 | } 142 | }, 143 | attachValidation: function () { 144 | $form 145 | .off("reset." + data_validation, onResetProxy) 146 | .on("reset." + data_validation, onResetProxy) 147 | .validate(this.options); 148 | }, 149 | validate: function () { // a validation function that is called by unobtrusive Ajax 150 | $form.validate(); 151 | return $form.valid(); 152 | } 153 | }; 154 | $form.data(data_validation, result); 155 | } 156 | 157 | return result; 158 | } 159 | 160 | $jQval.unobtrusive = { 161 | adapters: [], 162 | 163 | parseElement: function (element, skipAttach) { 164 | /// 165 | /// Parses a single HTML element for unobtrusive validation attributes. 166 | /// 167 | /// The HTML element to be parsed. 168 | /// [Optional] true to skip attaching the 169 | /// validation to the form. If parsing just this single element, you should specify true. 170 | /// If parsing several elements, you should specify false, and manually attach the validation 171 | /// to the form when you are finished. The default is false. 172 | var $element = $(element), 173 | form = $element.parents("form")[0], 174 | valInfo, rules, messages; 175 | 176 | if (!form) { // Cannot do client-side validation without a form 177 | return; 178 | } 179 | 180 | valInfo = validationInfo(form); 181 | valInfo.options.rules[element.name] = rules = {}; 182 | valInfo.options.messages[element.name] = messages = {}; 183 | 184 | $.each(this.adapters, function () { 185 | var prefix = "data-val-" + this.name, 186 | message = $element.attr(prefix), 187 | paramValues = {}; 188 | 189 | if (message !== undefined) { // Compare against undefined, because an empty message is legal (and falsy) 190 | prefix += "-"; 191 | 192 | $.each(this.params, function () { 193 | paramValues[this] = $element.attr(prefix + this); 194 | }); 195 | 196 | this.adapt({ 197 | element: element, 198 | form: form, 199 | message: message, 200 | params: paramValues, 201 | rules: rules, 202 | messages: messages 203 | }); 204 | } 205 | }); 206 | 207 | $.extend(rules, { "__dummy__": true }); 208 | 209 | if (!skipAttach) { 210 | valInfo.attachValidation(); 211 | } 212 | }, 213 | 214 | parse: function (selector) { 215 | /// 216 | /// Parses all the HTML elements in the specified selector. It looks for input elements decorated 217 | /// with the [data-val=true] attribute value and enables validation according to the data-val-* 218 | /// attribute values. 219 | /// 220 | /// Any valid jQuery selector. 221 | 222 | // $forms includes all forms in selector's DOM hierarchy (parent, children and self) that have at least one 223 | // element with data-val=true 224 | var $selector = $(selector), 225 | $forms = $selector.parents() 226 | .addBack() 227 | .filter("form") 228 | .add($selector.find("form")) 229 | .has("[data-val=true]"); 230 | 231 | $selector.find("[data-val=true]").each(function () { 232 | $jQval.unobtrusive.parseElement(this, true); 233 | }); 234 | 235 | $forms.each(function () { 236 | var info = validationInfo(this); 237 | if (info) { 238 | info.attachValidation(); 239 | } 240 | }); 241 | } 242 | }; 243 | 244 | adapters = $jQval.unobtrusive.adapters; 245 | 246 | adapters.add = function (adapterName, params, fn) { 247 | /// Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation. 248 | /// The name of the adapter to be added. This matches the name used 249 | /// in the data-val-nnnn HTML attribute (where nnnn is the adapter name). 250 | /// [Optional] An array of parameter names (strings) that will 251 | /// be extracted from the data-val-nnnn-mmmm HTML attributes (where nnnn is the adapter name, and 252 | /// mmmm is the parameter name). 253 | /// The function to call, which adapts the values from the HTML 254 | /// attributes into jQuery Validate rules and/or messages. 255 | /// 256 | if (!fn) { // Called with no params, just a function 257 | fn = params; 258 | params = []; 259 | } 260 | this.push({ name: adapterName, params: params, adapt: fn }); 261 | return this; 262 | }; 263 | 264 | adapters.addBool = function (adapterName, ruleName) { 265 | /// Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where 266 | /// the jQuery Validate validation rule has no parameter values. 267 | /// The name of the adapter to be added. This matches the name used 268 | /// in the data-val-nnnn HTML attribute (where nnnn is the adapter name). 269 | /// [Optional] The name of the jQuery Validate rule. If not provided, the value 270 | /// of adapterName will be used instead. 271 | /// 272 | return this.add(adapterName, function (options) { 273 | setValidationValues(options, ruleName || adapterName, true); 274 | }); 275 | }; 276 | 277 | adapters.addMinMax = function (adapterName, minRuleName, maxRuleName, minMaxRuleName, minAttribute, maxAttribute) { 278 | /// Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where 279 | /// the jQuery Validate validation has three potential rules (one for min-only, one for max-only, and 280 | /// one for min-and-max). The HTML parameters are expected to be named -min and -max. 281 | /// The name of the adapter to be added. This matches the name used 282 | /// in the data-val-nnnn HTML attribute (where nnnn is the adapter name). 283 | /// The name of the jQuery Validate rule to be used when you only 284 | /// have a minimum value. 285 | /// The name of the jQuery Validate rule to be used when you only 286 | /// have a maximum value. 287 | /// The name of the jQuery Validate rule to be used when you 288 | /// have both a minimum and maximum value. 289 | /// [Optional] The name of the HTML attribute that 290 | /// contains the minimum value. The default is "min". 291 | /// [Optional] The name of the HTML attribute that 292 | /// contains the maximum value. The default is "max". 293 | /// 294 | return this.add(adapterName, [minAttribute || "min", maxAttribute || "max"], function (options) { 295 | var min = options.params.min, 296 | max = options.params.max; 297 | 298 | if (min && max) { 299 | setValidationValues(options, minMaxRuleName, [min, max]); 300 | } 301 | else if (min) { 302 | setValidationValues(options, minRuleName, min); 303 | } 304 | else if (max) { 305 | setValidationValues(options, maxRuleName, max); 306 | } 307 | }); 308 | }; 309 | 310 | adapters.addSingleVal = function (adapterName, attribute, ruleName) { 311 | /// Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where 312 | /// the jQuery Validate validation rule has a single value. 313 | /// The name of the adapter to be added. This matches the name used 314 | /// in the data-val-nnnn HTML attribute(where nnnn is the adapter name). 315 | /// [Optional] The name of the HTML attribute that contains the value. 316 | /// The default is "val". 317 | /// [Optional] The name of the jQuery Validate rule. If not provided, the value 318 | /// of adapterName will be used instead. 319 | /// 320 | return this.add(adapterName, [attribute || "val"], function (options) { 321 | setValidationValues(options, ruleName || adapterName, options.params[attribute]); 322 | }); 323 | }; 324 | 325 | $jQval.addMethod("__dummy__", function (value, element, params) { 326 | return true; 327 | }); 328 | 329 | $jQval.addMethod("regex", function (value, element, params) { 330 | var match; 331 | if (this.optional(element)) { 332 | return true; 333 | } 334 | 335 | match = new RegExp(params).exec(value); 336 | return (match && (match.index === 0) && (match[0].length === value.length)); 337 | }); 338 | 339 | $jQval.addMethod("nonalphamin", function (value, element, nonalphamin) { 340 | var match; 341 | if (nonalphamin) { 342 | match = value.match(/\W/g); 343 | match = match && match.length >= nonalphamin; 344 | } 345 | return match; 346 | }); 347 | 348 | if ($jQval.methods.extension) { 349 | adapters.addSingleVal("accept", "mimtype"); 350 | adapters.addSingleVal("extension", "extension"); 351 | } else { 352 | // for backward compatibility, when the 'extension' validation method does not exist, such as with versions 353 | // of JQuery Validation plugin prior to 1.10, we should use the 'accept' method for 354 | // validating the extension, and ignore mime-type validations as they are not supported. 355 | adapters.addSingleVal("extension", "extension", "accept"); 356 | } 357 | 358 | adapters.addSingleVal("regex", "pattern"); 359 | adapters.addBool("creditcard").addBool("date").addBool("digits").addBool("email").addBool("number").addBool("url"); 360 | adapters.addMinMax("length", "minlength", "maxlength", "rangelength").addMinMax("range", "min", "max", "range"); 361 | adapters.addMinMax("minlength", "minlength").addMinMax("maxlength", "minlength", "maxlength"); 362 | adapters.add("equalto", ["other"], function (options) { 363 | var prefix = getModelPrefix(options.element.name), 364 | other = options.params.other, 365 | fullOtherName = appendModelPrefix(other, prefix), 366 | element = $(options.form).find(":input").filter("[name='" + escapeAttributeValue(fullOtherName) + "']")[0]; 367 | 368 | setValidationValues(options, "equalTo", element); 369 | }); 370 | adapters.add("required", function (options) { 371 | // jQuery Validate equates "required" with "mandatory" for checkbox elements 372 | if (options.element.tagName.toUpperCase() !== "INPUT" || options.element.type.toUpperCase() !== "CHECKBOX") { 373 | setValidationValues(options, "required", true); 374 | } 375 | }); 376 | adapters.add("remote", ["url", "type", "additionalfields"], function (options) { 377 | var value = { 378 | url: options.params.url, 379 | type: options.params.type || "GET", 380 | data: {} 381 | }, 382 | prefix = getModelPrefix(options.element.name); 383 | 384 | $.each(splitAndTrim(options.params.additionalfields || options.element.name), function (i, fieldName) { 385 | var paramName = appendModelPrefix(fieldName, prefix); 386 | value.data[paramName] = function () { 387 | var field = $(options.form).find(":input").filter("[name='" + escapeAttributeValue(paramName) + "']"); 388 | // For checkboxes and radio buttons, only pick up values from checked fields. 389 | if (field.is(":checkbox")) { 390 | return field.filter(":checked").val() || field.filter(":hidden").val() || ''; 391 | } 392 | else if (field.is(":radio")) { 393 | return field.filter(":checked").val() || ''; 394 | } 395 | return field.val(); 396 | }; 397 | }); 398 | 399 | setValidationValues(options, "remote", value); 400 | }); 401 | adapters.add("password", ["min", "nonalphamin", "regex"], function (options) { 402 | if (options.params.min) { 403 | setValidationValues(options, "minlength", options.params.min); 404 | } 405 | if (options.params.nonalphamin) { 406 | setValidationValues(options, "nonalphamin", options.params.nonalphamin); 407 | } 408 | if (options.params.regex) { 409 | setValidationValues(options, "regex", options.params.regex); 410 | } 411 | }); 412 | 413 | $(function () { 414 | $jQval.unobtrusive.parse(document); 415 | }); 416 | }(jQuery)); -------------------------------------------------------------------------------- /Web/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** Unobtrusive validation support library for jQuery and jQuery Validate 3 | ** Copyright (C) Microsoft Corporation. All rights reserved. 4 | */ 5 | !function(a){function e(a,e,n){a.rules[e]=n,a.message&&(a.messages[e]=a.message)}function n(a){return a.replace(/^\s+|\s+$/g,"").split(/\s*,\s*/g)}function t(a){return a.replace(/([!"#$%&'()*+,./:;<=>?@\[\\\]^`{|}~])/g,"\\$1")}function r(a){return a.substr(0,a.lastIndexOf(".")+1)}function i(a,e){return 0===a.indexOf("*.")&&(a=a.replace("*.",e)),a}function o(e,n){var r=a(this).find("[data-valmsg-for='"+t(n[0].name)+"']"),i=r.attr("data-valmsg-replace"),o=i?a.parseJSON(i)!==!1:null;r.removeClass("field-validation-valid").addClass("field-validation-error"),e.data("unobtrusiveContainer",r),o?(r.empty(),e.removeClass("input-validation-error").appendTo(r)):e.hide()}function d(e,n){var t=a(this).find("[data-valmsg-summary=true]"),r=t.find("ul");r&&r.length&&n.errorList.length&&(r.empty(),t.addClass("validation-summary-errors").removeClass("validation-summary-valid"),a.each(n.errorList,function(){a("
  • ").html(this.message).appendTo(r)}))}function s(e){var n=e.data("unobtrusiveContainer");if(n){var t=n.attr("data-valmsg-replace"),r=t?a.parseJSON(t):null;n.addClass("field-validation-valid").removeClass("field-validation-error"),e.removeData("unobtrusiveContainer"),r&&n.empty()}}function l(e){var n=a(this),t="__jquery_unobtrusive_validation_form_reset";if(!n.data(t)){n.data(t,!0);try{n.data("validator").resetForm()}finally{n.removeData(t)}n.find(".validation-summary-errors").addClass("validation-summary-valid").removeClass("validation-summary-errors"),n.find(".field-validation-error").addClass("field-validation-valid").removeClass("field-validation-error").removeData("unobtrusiveContainer").find(">*").removeData("unobtrusiveContainer")}}function m(e){var n=a(e),t=n.data(v),r=a.proxy(l,e),i=p.unobtrusive.options||{},m=function(n,t){var r=i[n];r&&a.isFunction(r)&&r.apply(e,t)};return t||(t={options:{errorClass:i.errorClass||"input-validation-error",errorElement:i.errorElement||"span",errorPlacement:function(){o.apply(e,arguments),m("errorPlacement",arguments)},invalidHandler:function(){d.apply(e,arguments),m("invalidHandler",arguments)},messages:{},rules:{},success:function(){s.apply(e,arguments),m("success",arguments)}},attachValidation:function(){n.off("reset."+v,r).on("reset."+v,r).validate(this.options)},validate:function(){return n.validate(),n.valid()}},n.data(v,t)),t}var u,p=a.validator,v="unobtrusiveValidation";p.unobtrusive={adapters:[],parseElement:function(e,n){var t,r,i,o=a(e),d=o.parents("form")[0];d&&(t=m(d),t.options.rules[e.name]=r={},t.options.messages[e.name]=i={},a.each(this.adapters,function(){var n="data-val-"+this.name,t=o.attr(n),s={};void 0!==t&&(n+="-",a.each(this.params,function(){s[this]=o.attr(n+this)}),this.adapt({element:e,form:d,message:t,params:s,rules:r,messages:i}))}),a.extend(r,{__dummy__:!0}),n||t.attachValidation())},parse:function(e){var n=a(e),t=n.parents().addBack().filter("form").add(n.find("form")).has("[data-val=true]");n.find("[data-val=true]").each(function(){p.unobtrusive.parseElement(this,!0)}),t.each(function(){var a=m(this);a&&a.attachValidation()})}},u=p.unobtrusive.adapters,u.add=function(a,e,n){return n||(n=e,e=[]),this.push({name:a,params:e,adapt:n}),this},u.addBool=function(a,n){return this.add(a,function(t){e(t,n||a,!0)})},u.addMinMax=function(a,n,t,r,i,o){return this.add(a,[i||"min",o||"max"],function(a){var i=a.params.min,o=a.params.max;i&&o?e(a,r,[i,o]):i?e(a,n,i):o&&e(a,t,o)})},u.addSingleVal=function(a,n,t){return this.add(a,[n||"val"],function(r){e(r,t||a,r.params[n])})},p.addMethod("__dummy__",function(a,e,n){return!0}),p.addMethod("regex",function(a,e,n){var t;return this.optional(e)?!0:(t=new RegExp(n).exec(a),t&&0===t.index&&t[0].length===a.length)}),p.addMethod("nonalphamin",function(a,e,n){var t;return n&&(t=a.match(/\W/g),t=t&&t.length>=n),t}),p.methods.extension?(u.addSingleVal("accept","mimtype"),u.addSingleVal("extension","extension")):u.addSingleVal("extension","extension","accept"),u.addSingleVal("regex","pattern"),u.addBool("creditcard").addBool("date").addBool("digits").addBool("email").addBool("number").addBool("url"),u.addMinMax("length","minlength","maxlength","rangelength").addMinMax("range","min","max","range"),u.addMinMax("minlength","minlength").addMinMax("maxlength","minlength","maxlength"),u.add("equalto",["other"],function(n){var o=r(n.element.name),d=n.params.other,s=i(d,o),l=a(n.form).find(":input").filter("[name='"+t(s)+"']")[0];e(n,"equalTo",l)}),u.add("required",function(a){("INPUT"!==a.element.tagName.toUpperCase()||"CHECKBOX"!==a.element.type.toUpperCase())&&e(a,"required",!0)}),u.add("remote",["url","type","additionalfields"],function(o){var d={url:o.params.url,type:o.params.type||"GET",data:{}},s=r(o.element.name);a.each(n(o.params.additionalfields||o.element.name),function(e,n){var r=i(n,s);d.data[r]=function(){var e=a(o.form).find(":input").filter("[name='"+t(r)+"']");return e.is(":checkbox")?e.filter(":checked").val()||e.filter(":hidden").val()||"":e.is(":radio")?e.filter(":checked").val()||"":e.val()}}),e(o,"remote",d)}),u.add("password",["min","nonalphamin","regex"],function(a){a.params.min&&e(a,"minlength",a.params.min),a.params.nonalphamin&&e(a,"nonalphamin",a.params.nonalphamin),a.params.regex&&e(a,"regex",a.params.regex)}),a(function(){p.unobtrusive.parse(document)})}(jQuery); -------------------------------------------------------------------------------- /Web/wwwroot/lib/jquery-validation/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-validation", 3 | "homepage": "http://jqueryvalidation.org/", 4 | "repository": { 5 | "type": "git", 6 | "url": "git://github.com/jzaefferer/jquery-validation.git" 7 | }, 8 | "authors": [ 9 | "Jörn Zaefferer " 10 | ], 11 | "description": "Form validation made easy", 12 | "main": "dist/jquery.validate.js", 13 | "keywords": [ 14 | "forms", 15 | "validation", 16 | "validate" 17 | ], 18 | "license": "MIT", 19 | "ignore": [ 20 | "**/.*", 21 | "node_modules", 22 | "bower_components", 23 | "test", 24 | "demo", 25 | "lib" 26 | ], 27 | "dependencies": { 28 | "jquery": ">= 1.7.2" 29 | }, 30 | "version": "1.14.0", 31 | "_release": "1.14.0", 32 | "_resolution": { 33 | "type": "version", 34 | "tag": "1.14.0", 35 | "commit": "c1343fb9823392aa9acbe1c3ffd337b8c92fed48" 36 | }, 37 | "_source": "git://github.com/jzaefferer/jquery-validation.git", 38 | "_target": ">=1.8", 39 | "_originalSource": "jquery-validation" 40 | } -------------------------------------------------------------------------------- /Web/wwwroot/lib/jquery-validation/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright Jörn Zaefferer 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Web/wwwroot/lib/jquery-validation/dist/additional-methods.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery Validation Plugin - v1.14.0 - 6/30/2015 2 | * http://jqueryvalidation.org/ 3 | * Copyright (c) 2015 Jörn Zaefferer; Licensed MIT */ 4 | !function(a){"function"==typeof define&&define.amd?define(["jquery","./jquery.validate.min"],a):a(jQuery)}(function(a){!function(){function b(a){return a.replace(/<.[^<>]*?>/g," ").replace(/ | /gi," ").replace(/[.(),;:!?%#$'\"_+=\/\-“”’]*/g,"")}a.validator.addMethod("maxWords",function(a,c,d){return this.optional(c)||b(a).match(/\b\w+\b/g).length<=d},a.validator.format("Please enter {0} words or less.")),a.validator.addMethod("minWords",function(a,c,d){return this.optional(c)||b(a).match(/\b\w+\b/g).length>=d},a.validator.format("Please enter at least {0} words.")),a.validator.addMethod("rangeWords",function(a,c,d){var e=b(a),f=/\b\w+\b/g;return this.optional(c)||e.match(f).length>=d[0]&&e.match(f).length<=d[1]},a.validator.format("Please enter between {0} and {1} words."))}(),a.validator.addMethod("accept",function(b,c,d){var e,f,g="string"==typeof d?d.replace(/\s/g,"").replace(/,/g,"|"):"image/*",h=this.optional(c);if(h)return h;if("file"===a(c).attr("type")&&(g=g.replace(/\*/g,".*"),c.files&&c.files.length))for(e=0;ec;c++)d=h-c,e=f.substring(c,c+1),g+=d*e;return g%11===0},"Please specify a valid bank account number"),a.validator.addMethod("bankorgiroaccountNL",function(b,c){return this.optional(c)||a.validator.methods.bankaccountNL.call(this,b,c)||a.validator.methods.giroaccountNL.call(this,b,c)},"Please specify a valid bank or giro account number"),a.validator.addMethod("bic",function(a,b){return this.optional(b)||/^([A-Z]{6}[A-Z2-9][A-NP-Z1-2])(X{3}|[A-WY-Z0-9][A-Z0-9]{2})?$/.test(a)},"Please specify a valid BIC code"),a.validator.addMethod("cifES",function(a){"use strict";var b,c,d,e,f,g,h=[];if(a=a.toUpperCase(),!a.match("((^[A-Z]{1}[0-9]{7}[A-Z0-9]{1}$|^[T]{1}[A-Z0-9]{8}$)|^[0-9]{8}[A-Z]{1}$)"))return!1;for(d=0;9>d;d++)h[d]=parseInt(a.charAt(d),10);for(c=h[2]+h[4]+h[6],e=1;8>e;e+=2)f=(2*h[e]).toString(),g=f.charAt(1),c+=parseInt(f.charAt(0),10)+(""===g?0:parseInt(g,10));return/^[ABCDEFGHJNPQRSUVW]{1}/.test(a)?(c+="",b=10-parseInt(c.charAt(c.length-1),10),a+=b,h[8].toString()===String.fromCharCode(64+b)||h[8].toString()===a.charAt(a.length-1)):!1},"Please specify a valid CIF number."),a.validator.addMethod("cpfBR",function(a){if(a=a.replace(/([~!@#$%^&*()_+=`{}\[\]\-|\\:;'<>,.\/? ])+/g,""),11!==a.length)return!1;var b,c,d,e,f=0;if(b=parseInt(a.substring(9,10),10),c=parseInt(a.substring(10,11),10),d=function(a,b){var c=10*a%11;return(10===c||11===c)&&(c=0),c===b},""===a||"00000000000"===a||"11111111111"===a||"22222222222"===a||"33333333333"===a||"44444444444"===a||"55555555555"===a||"66666666666"===a||"77777777777"===a||"88888888888"===a||"99999999999"===a)return!1;for(e=1;9>=e;e++)f+=parseInt(a.substring(e-1,e),10)*(11-e);if(d(f,b)){for(f=0,e=1;10>=e;e++)f+=parseInt(a.substring(e-1,e),10)*(12-e);return d(f,c)}return!1},"Please specify a valid CPF number"),a.validator.addMethod("creditcardtypes",function(a,b,c){if(/[^0-9\-]+/.test(a))return!1;a=a.replace(/\D/g,"");var d=0;return c.mastercard&&(d|=1),c.visa&&(d|=2),c.amex&&(d|=4),c.dinersclub&&(d|=8),c.enroute&&(d|=16),c.discover&&(d|=32),c.jcb&&(d|=64),c.unknown&&(d|=128),c.all&&(d=255),1&d&&/^(5[12345])/.test(a)?16===a.length:2&d&&/^(4)/.test(a)?16===a.length:4&d&&/^(3[47])/.test(a)?15===a.length:8&d&&/^(3(0[012345]|[68]))/.test(a)?14===a.length:16&d&&/^(2(014|149))/.test(a)?15===a.length:32&d&&/^(6011)/.test(a)?16===a.length:64&d&&/^(3)/.test(a)?16===a.length:64&d&&/^(2131|1800)/.test(a)?15===a.length:128&d?!0:!1},"Please enter a valid credit card number."),a.validator.addMethod("currency",function(a,b,c){var d,e="string"==typeof c,f=e?c:c[0],g=e?!0:c[1];return f=f.replace(/,/g,""),f=g?f+"]":f+"]?",d="^["+f+"([1-9]{1}[0-9]{0,2}(\\,[0-9]{3})*(\\.[0-9]{0,2})?|[1-9]{1}[0-9]{0,}(\\.[0-9]{0,2})?|0(\\.[0-9]{0,2})?|(\\.[0-9]{1,2})?)$",d=new RegExp(d),this.optional(b)||d.test(a)},"Please specify a valid currency"),a.validator.addMethod("dateFA",function(a,b){return this.optional(b)||/^[1-4]\d{3}\/((0?[1-6]\/((3[0-1])|([1-2][0-9])|(0?[1-9])))|((1[0-2]|(0?[7-9]))\/(30|([1-2][0-9])|(0?[1-9]))))$/.test(a)},a.validator.messages.date),a.validator.addMethod("dateITA",function(a,b){var c,d,e,f,g,h=!1,i=/^\d{1,2}\/\d{1,2}\/\d{4}$/;return i.test(a)?(c=a.split("/"),d=parseInt(c[0],10),e=parseInt(c[1],10),f=parseInt(c[2],10),g=new Date(Date.UTC(f,e-1,d,12,0,0,0)),h=g.getUTCFullYear()===f&&g.getUTCMonth()===e-1&&g.getUTCDate()===d?!0:!1):h=!1,this.optional(b)||h},a.validator.messages.date),a.validator.addMethod("dateNL",function(a,b){return this.optional(b)||/^(0?[1-9]|[12]\d|3[01])[\.\/\-](0?[1-9]|1[012])[\.\/\-]([12]\d)?(\d\d)$/.test(a)},a.validator.messages.date),a.validator.addMethod("extension",function(a,b,c){return c="string"==typeof c?c.replace(/,/g,"|"):"png|jpe?g|gif",this.optional(b)||a.match(new RegExp("\\.("+c+")$","i"))},a.validator.format("Please enter a value with a valid extension.")),a.validator.addMethod("giroaccountNL",function(a,b){return this.optional(b)||/^[0-9]{1,7}$/.test(a)},"Please specify a valid giro account number"),a.validator.addMethod("iban",function(a,b){if(this.optional(b))return!0;var c,d,e,f,g,h,i,j,k,l=a.replace(/ /g,"").toUpperCase(),m="",n=!0,o="",p="";if(c=l.substring(0,2),h={AL:"\\d{8}[\\dA-Z]{16}",AD:"\\d{8}[\\dA-Z]{12}",AT:"\\d{16}",AZ:"[\\dA-Z]{4}\\d{20}",BE:"\\d{12}",BH:"[A-Z]{4}[\\dA-Z]{14}",BA:"\\d{16}",BR:"\\d{23}[A-Z][\\dA-Z]",BG:"[A-Z]{4}\\d{6}[\\dA-Z]{8}",CR:"\\d{17}",HR:"\\d{17}",CY:"\\d{8}[\\dA-Z]{16}",CZ:"\\d{20}",DK:"\\d{14}",DO:"[A-Z]{4}\\d{20}",EE:"\\d{16}",FO:"\\d{14}",FI:"\\d{14}",FR:"\\d{10}[\\dA-Z]{11}\\d{2}",GE:"[\\dA-Z]{2}\\d{16}",DE:"\\d{18}",GI:"[A-Z]{4}[\\dA-Z]{15}",GR:"\\d{7}[\\dA-Z]{16}",GL:"\\d{14}",GT:"[\\dA-Z]{4}[\\dA-Z]{20}",HU:"\\d{24}",IS:"\\d{22}",IE:"[\\dA-Z]{4}\\d{14}",IL:"\\d{19}",IT:"[A-Z]\\d{10}[\\dA-Z]{12}",KZ:"\\d{3}[\\dA-Z]{13}",KW:"[A-Z]{4}[\\dA-Z]{22}",LV:"[A-Z]{4}[\\dA-Z]{13}",LB:"\\d{4}[\\dA-Z]{20}",LI:"\\d{5}[\\dA-Z]{12}",LT:"\\d{16}",LU:"\\d{3}[\\dA-Z]{13}",MK:"\\d{3}[\\dA-Z]{10}\\d{2}",MT:"[A-Z]{4}\\d{5}[\\dA-Z]{18}",MR:"\\d{23}",MU:"[A-Z]{4}\\d{19}[A-Z]{3}",MC:"\\d{10}[\\dA-Z]{11}\\d{2}",MD:"[\\dA-Z]{2}\\d{18}",ME:"\\d{18}",NL:"[A-Z]{4}\\d{10}",NO:"\\d{11}",PK:"[\\dA-Z]{4}\\d{16}",PS:"[\\dA-Z]{4}\\d{21}",PL:"\\d{24}",PT:"\\d{21}",RO:"[A-Z]{4}[\\dA-Z]{16}",SM:"[A-Z]\\d{10}[\\dA-Z]{12}",SA:"\\d{2}[\\dA-Z]{18}",RS:"\\d{18}",SK:"\\d{20}",SI:"\\d{15}",ES:"\\d{20}",SE:"\\d{20}",CH:"\\d{5}[\\dA-Z]{12}",TN:"\\d{20}",TR:"\\d{5}[\\dA-Z]{17}",AE:"\\d{3}\\d{16}",GB:"[A-Z]{4}\\d{14}",VG:"[\\dA-Z]{4}\\d{16}"},g=h[c],"undefined"!=typeof g&&(i=new RegExp("^[A-Z]{2}\\d{2}"+g+"$",""),!i.test(l)))return!1;for(d=l.substring(4,l.length)+l.substring(0,4),j=0;j9&&a.match(/^(?:(?:(?:00\s?|\+)44\s?|0)7(?:[1345789]\d{2}|624)\s?\d{3}\s?\d{3})$/)},"Please specify a valid mobile number"),a.validator.addMethod("nieES",function(a){"use strict";return a=a.toUpperCase(),a.match("((^[A-Z]{1}[0-9]{7}[A-Z0-9]{1}$|^[T]{1}[A-Z0-9]{8}$)|^[0-9]{8}[A-Z]{1}$)")?/^[T]{1}/.test(a)?a[8]===/^[T]{1}[A-Z0-9]{8}$/.test(a):/^[XYZ]{1}/.test(a)?a[8]==="TRWAGMYFPDXBNJZSQVHLCKE".charAt(a.replace("X","0").replace("Y","1").replace("Z","2").substring(0,8)%23):!1:!1},"Please specify a valid NIE number."),a.validator.addMethod("nifES",function(a){"use strict";return a=a.toUpperCase(),a.match("((^[A-Z]{1}[0-9]{7}[A-Z0-9]{1}$|^[T]{1}[A-Z0-9]{8}$)|^[0-9]{8}[A-Z]{1}$)")?/^[0-9]{8}[A-Z]{1}$/.test(a)?"TRWAGMYFPDXBNJZSQVHLCKE".charAt(a.substring(8,0)%23)===a.charAt(8):/^[KLM]{1}/.test(a)?a[8]===String.fromCharCode(64):!1:!1},"Please specify a valid NIF number."),jQuery.validator.addMethod("notEqualTo",function(b,c,d){return this.optional(c)||!a.validator.methods.equalTo.call(this,b,c,d)},"Please enter a different value, values must not be the same."),a.validator.addMethod("nowhitespace",function(a,b){return this.optional(b)||/^\S+$/i.test(a)},"No white space please"),a.validator.addMethod("pattern",function(a,b,c){return this.optional(b)?!0:("string"==typeof c&&(c=new RegExp("^(?:"+c+")$")),c.test(a))},"Invalid format."),a.validator.addMethod("phoneNL",function(a,b){return this.optional(b)||/^((\+|00(\s|\s?\-\s?)?)31(\s|\s?\-\s?)?(\(0\)[\-\s]?)?|0)[1-9]((\s|\s?\-\s?)?[0-9]){8}$/.test(a)},"Please specify a valid phone number."),a.validator.addMethod("phoneUK",function(a,b){return a=a.replace(/\(|\)|\s+|-/g,""),this.optional(b)||a.length>9&&a.match(/^(?:(?:(?:00\s?|\+)44\s?)|(?:\(?0))(?:\d{2}\)?\s?\d{4}\s?\d{4}|\d{3}\)?\s?\d{3}\s?\d{3,4}|\d{4}\)?\s?(?:\d{5}|\d{3}\s?\d{3})|\d{5}\)?\s?\d{4,5})$/)},"Please specify a valid phone number"),a.validator.addMethod("phoneUS",function(a,b){return a=a.replace(/\s+/g,""),this.optional(b)||a.length>9&&a.match(/^(\+?1-?)?(\([2-9]([02-9]\d|1[02-9])\)|[2-9]([02-9]\d|1[02-9]))-?[2-9]([02-9]\d|1[02-9])-?\d{4}$/)},"Please specify a valid phone number"),a.validator.addMethod("phonesUK",function(a,b){return a=a.replace(/\(|\)|\s+|-/g,""),this.optional(b)||a.length>9&&a.match(/^(?:(?:(?:00\s?|\+)44\s?|0)(?:1\d{8,9}|[23]\d{9}|7(?:[1345789]\d{8}|624\d{6})))$/)},"Please specify a valid uk phone number"),a.validator.addMethod("postalCodeCA",function(a,b){return this.optional(b)||/^[ABCEGHJKLMNPRSTVXY]\d[A-Z] \d[A-Z]\d$/.test(a)},"Please specify a valid postal code"),a.validator.addMethod("postalcodeBR",function(a,b){return this.optional(b)||/^\d{2}.\d{3}-\d{3}?$|^\d{5}-?\d{3}?$/.test(a)},"Informe um CEP válido."),a.validator.addMethod("postalcodeIT",function(a,b){return this.optional(b)||/^\d{5}$/.test(a)},"Please specify a valid postal code"),a.validator.addMethod("postalcodeNL",function(a,b){return this.optional(b)||/^[1-9][0-9]{3}\s?[a-zA-Z]{2}$/.test(a)},"Please specify a valid postal code"),a.validator.addMethod("postcodeUK",function(a,b){return this.optional(b)||/^((([A-PR-UWYZ][0-9])|([A-PR-UWYZ][0-9][0-9])|([A-PR-UWYZ][A-HK-Y][0-9])|([A-PR-UWYZ][A-HK-Y][0-9][0-9])|([A-PR-UWYZ][0-9][A-HJKSTUW])|([A-PR-UWYZ][A-HK-Y][0-9][ABEHMNPRVWXY]))\s?([0-9][ABD-HJLNP-UW-Z]{2})|(GIR)\s?(0AA))$/i.test(a)},"Please specify a valid UK postcode"),a.validator.addMethod("require_from_group",function(b,c,d){var e=a(d[1],c.form),f=e.eq(0),g=f.data("valid_req_grp")?f.data("valid_req_grp"):a.extend({},this),h=e.filter(function(){return g.elementValue(this)}).length>=d[0];return f.data("valid_req_grp",g),a(c).data("being_validated")||(e.data("being_validated",!0),e.each(function(){g.element(this)}),e.data("being_validated",!1)),h},a.validator.format("Please fill at least {0} of these fields.")),a.validator.addMethod("skip_or_fill_minimum",function(b,c,d){var e=a(d[1],c.form),f=e.eq(0),g=f.data("valid_skip")?f.data("valid_skip"):a.extend({},this),h=e.filter(function(){return g.elementValue(this)}).length,i=0===h||h>=d[0];return f.data("valid_skip",g),a(c).data("being_validated")||(e.data("being_validated",!0),e.each(function(){g.element(this)}),e.data("being_validated",!1)),i},a.validator.format("Please either skip these fields or fill at least {0} of them.")),a.validator.addMethod("stateUS",function(a,b,c){var d,e="undefined"==typeof c,f=e||"undefined"==typeof c.caseSensitive?!1:c.caseSensitive,g=e||"undefined"==typeof c.includeTerritories?!1:c.includeTerritories,h=e||"undefined"==typeof c.includeMilitary?!1:c.includeMilitary;return d=g||h?g&&h?"^(A[AEKLPRSZ]|C[AOT]|D[CE]|FL|G[AU]|HI|I[ADLN]|K[SY]|LA|M[ADEINOPST]|N[CDEHJMVY]|O[HKR]|P[AR]|RI|S[CD]|T[NX]|UT|V[AIT]|W[AIVY])$":g?"^(A[KLRSZ]|C[AOT]|D[CE]|FL|G[AU]|HI|I[ADLN]|K[SY]|LA|M[ADEINOPST]|N[CDEHJMVY]|O[HKR]|P[AR]|RI|S[CD]|T[NX]|UT|V[AIT]|W[AIVY])$":"^(A[AEKLPRZ]|C[AOT]|D[CE]|FL|GA|HI|I[ADLN]|K[SY]|LA|M[ADEINOST]|N[CDEHJMVY]|O[HKR]|PA|RI|S[CD]|T[NX]|UT|V[AT]|W[AIVY])$":"^(A[KLRZ]|C[AOT]|D[CE]|FL|GA|HI|I[ADLN]|K[SY]|LA|M[ADEINOST]|N[CDEHJMVY]|O[HKR]|PA|RI|S[CD]|T[NX]|UT|V[AT]|W[AIVY])$",d=f?new RegExp(d):new RegExp(d,"i"),this.optional(b)||d.test(a)},"Please specify a valid state"),a.validator.addMethod("strippedminlength",function(b,c,d){return a(b).text().length>=d},a.validator.format("Please enter at least {0} characters")),a.validator.addMethod("time",function(a,b){return this.optional(b)||/^([01]\d|2[0-3]|[0-9])(:[0-5]\d){1,2}$/.test(a)},"Please enter a valid time, between 00:00 and 23:59"),a.validator.addMethod("time12h",function(a,b){return this.optional(b)||/^((0?[1-9]|1[012])(:[0-5]\d){1,2}(\ ?[AP]M))$/i.test(a)},"Please enter a valid time in 12-hour am/pm format"),a.validator.addMethod("url2",function(a,b){return this.optional(b)||/^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)*(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(a)},a.validator.messages.url),a.validator.addMethod("vinUS",function(a){if(17!==a.length)return!1;var b,c,d,e,f,g,h=["A","B","C","D","E","F","G","H","J","K","L","M","N","P","R","S","T","U","V","W","X","Y","Z"],i=[1,2,3,4,5,6,7,8,1,2,3,4,5,7,9,2,3,4,5,6,7,8,9],j=[8,7,6,5,4,3,2,10,0,9,8,7,6,5,4,3,2],k=0;for(b=0;17>b;b++){if(e=j[b],d=a.slice(b,b+1),8===b&&(g=d),isNaN(d)){for(c=0;c").attr("name",c.submitButton.name).val(a(c.submitButton).val()).appendTo(c.currentForm)),e=c.settings.submitHandler.call(c,c.currentForm,b),c.submitButton&&d.remove(),void 0!==e?e:!1):!0}return c.settings.debug&&b.preventDefault(),c.cancelSubmit?(c.cancelSubmit=!1,d()):c.form()?c.pendingRequest?(c.formSubmitted=!0,!1):d():(c.focusInvalid(),!1)})),c)},valid:function(){var b,c,d;return a(this[0]).is("form")?b=this.validate().form():(d=[],b=!0,c=a(this[0].form).validate(),this.each(function(){b=c.element(this)&&b,d=d.concat(c.errorList)}),c.errorList=d),b},rules:function(b,c){var d,e,f,g,h,i,j=this[0];if(b)switch(d=a.data(j.form,"validator").settings,e=d.rules,f=a.validator.staticRules(j),b){case"add":a.extend(f,a.validator.normalizeRule(c)),delete f.messages,e[j.name]=f,c.messages&&(d.messages[j.name]=a.extend(d.messages[j.name],c.messages));break;case"remove":return c?(i={},a.each(c.split(/\s/),function(b,c){i[c]=f[c],delete f[c],"required"===c&&a(j).removeAttr("aria-required")}),i):(delete e[j.name],f)}return g=a.validator.normalizeRules(a.extend({},a.validator.classRules(j),a.validator.attributeRules(j),a.validator.dataRules(j),a.validator.staticRules(j)),j),g.required&&(h=g.required,delete g.required,g=a.extend({required:h},g),a(j).attr("aria-required","true")),g.remote&&(h=g.remote,delete g.remote,g=a.extend(g,{remote:h})),g}}),a.extend(a.expr[":"],{blank:function(b){return!a.trim(""+a(b).val())},filled:function(b){return!!a.trim(""+a(b).val())},unchecked:function(b){return!a(b).prop("checked")}}),a.validator=function(b,c){this.settings=a.extend(!0,{},a.validator.defaults,b),this.currentForm=c,this.init()},a.validator.format=function(b,c){return 1===arguments.length?function(){var c=a.makeArray(arguments);return c.unshift(b),a.validator.format.apply(this,c)}:(arguments.length>2&&c.constructor!==Array&&(c=a.makeArray(arguments).slice(1)),c.constructor!==Array&&(c=[c]),a.each(c,function(a,c){b=b.replace(new RegExp("\\{"+a+"\\}","g"),function(){return c})}),b)},a.extend(a.validator,{defaults:{messages:{},groups:{},rules:{},errorClass:"error",validClass:"valid",errorElement:"label",focusCleanup:!1,focusInvalid:!0,errorContainer:a([]),errorLabelContainer:a([]),onsubmit:!0,ignore:":hidden",ignoreTitle:!1,onfocusin:function(a){this.lastActive=a,this.settings.focusCleanup&&(this.settings.unhighlight&&this.settings.unhighlight.call(this,a,this.settings.errorClass,this.settings.validClass),this.hideThese(this.errorsFor(a)))},onfocusout:function(a){this.checkable(a)||!(a.name in this.submitted)&&this.optional(a)||this.element(a)},onkeyup:function(b,c){var d=[16,17,18,20,35,36,37,38,39,40,45,144,225];9===c.which&&""===this.elementValue(b)||-1!==a.inArray(c.keyCode,d)||(b.name in this.submitted||b===this.lastElement)&&this.element(b)},onclick:function(a){a.name in this.submitted?this.element(a):a.parentNode.name in this.submitted&&this.element(a.parentNode)},highlight:function(b,c,d){"radio"===b.type?this.findByName(b.name).addClass(c).removeClass(d):a(b).addClass(c).removeClass(d)},unhighlight:function(b,c,d){"radio"===b.type?this.findByName(b.name).removeClass(c).addClass(d):a(b).removeClass(c).addClass(d)}},setDefaults:function(b){a.extend(a.validator.defaults,b)},messages:{required:"This field is required.",remote:"Please fix this field.",email:"Please enter a valid email address.",url:"Please enter a valid URL.",date:"Please enter a valid date.",dateISO:"Please enter a valid date ( ISO ).",number:"Please enter a valid number.",digits:"Please enter only digits.",creditcard:"Please enter a valid credit card number.",equalTo:"Please enter the same value again.",maxlength:a.validator.format("Please enter no more than {0} characters."),minlength:a.validator.format("Please enter at least {0} characters."),rangelength:a.validator.format("Please enter a value between {0} and {1} characters long."),range:a.validator.format("Please enter a value between {0} and {1}."),max:a.validator.format("Please enter a value less than or equal to {0}."),min:a.validator.format("Please enter a value greater than or equal to {0}.")},autoCreateRanges:!1,prototype:{init:function(){function b(b){var c=a.data(this.form,"validator"),d="on"+b.type.replace(/^validate/,""),e=c.settings;e[d]&&!a(this).is(e.ignore)&&e[d].call(c,this,b)}this.labelContainer=a(this.settings.errorLabelContainer),this.errorContext=this.labelContainer.length&&this.labelContainer||a(this.currentForm),this.containers=a(this.settings.errorContainer).add(this.settings.errorLabelContainer),this.submitted={},this.valueCache={},this.pendingRequest=0,this.pending={},this.invalid={},this.reset();var c,d=this.groups={};a.each(this.settings.groups,function(b,c){"string"==typeof c&&(c=c.split(/\s/)),a.each(c,function(a,c){d[c]=b})}),c=this.settings.rules,a.each(c,function(b,d){c[b]=a.validator.normalizeRule(d)}),a(this.currentForm).on("focusin.validate focusout.validate keyup.validate",":text, [type='password'], [type='file'], select, textarea, [type='number'], [type='search'], [type='tel'], [type='url'], [type='email'], [type='datetime'], [type='date'], [type='month'], [type='week'], [type='time'], [type='datetime-local'], [type='range'], [type='color'], [type='radio'], [type='checkbox']",b).on("click.validate","select, option, [type='radio'], [type='checkbox']",b),this.settings.invalidHandler&&a(this.currentForm).on("invalid-form.validate",this.settings.invalidHandler),a(this.currentForm).find("[required], [data-rule-required], .required").attr("aria-required","true")},form:function(){return this.checkForm(),a.extend(this.submitted,this.errorMap),this.invalid=a.extend({},this.errorMap),this.valid()||a(this.currentForm).triggerHandler("invalid-form",[this]),this.showErrors(),this.valid()},checkForm:function(){this.prepareForm();for(var a=0,b=this.currentElements=this.elements();b[a];a++)this.check(b[a]);return this.valid()},element:function(b){var c=this.clean(b),d=this.validationTargetFor(c),e=!0;return this.lastElement=d,void 0===d?delete this.invalid[c.name]:(this.prepareElement(d),this.currentElements=a(d),e=this.check(d)!==!1,e?delete this.invalid[d.name]:this.invalid[d.name]=!0),a(b).attr("aria-invalid",!e),this.numberOfInvalids()||(this.toHide=this.toHide.add(this.containers)),this.showErrors(),e},showErrors:function(b){if(b){a.extend(this.errorMap,b),this.errorList=[];for(var c in b)this.errorList.push({message:b[c],element:this.findByName(c)[0]});this.successList=a.grep(this.successList,function(a){return!(a.name in b)})}this.settings.showErrors?this.settings.showErrors.call(this,this.errorMap,this.errorList):this.defaultShowErrors()},resetForm:function(){a.fn.resetForm&&a(this.currentForm).resetForm(),this.submitted={},this.lastElement=null,this.prepareForm(),this.hideErrors();var b,c=this.elements().removeData("previousValue").removeAttr("aria-invalid");if(this.settings.unhighlight)for(b=0;c[b];b++)this.settings.unhighlight.call(this,c[b],this.settings.errorClass,"");else c.removeClass(this.settings.errorClass)},numberOfInvalids:function(){return this.objectLength(this.invalid)},objectLength:function(a){var b,c=0;for(b in a)c++;return c},hideErrors:function(){this.hideThese(this.toHide)},hideThese:function(a){a.not(this.containers).text(""),this.addWrapper(a).hide()},valid:function(){return 0===this.size()},size:function(){return this.errorList.length},focusInvalid:function(){if(this.settings.focusInvalid)try{a(this.findLastActive()||this.errorList.length&&this.errorList[0].element||[]).filter(":visible").focus().trigger("focusin")}catch(b){}},findLastActive:function(){var b=this.lastActive;return b&&1===a.grep(this.errorList,function(a){return a.element.name===b.name}).length&&b},elements:function(){var b=this,c={};return a(this.currentForm).find("input, select, textarea").not(":submit, :reset, :image, :disabled").not(this.settings.ignore).filter(function(){return!this.name&&b.settings.debug&&window.console&&console.error("%o has no name assigned",this),this.name in c||!b.objectLength(a(this).rules())?!1:(c[this.name]=!0,!0)})},clean:function(b){return a(b)[0]},errors:function(){var b=this.settings.errorClass.split(" ").join(".");return a(this.settings.errorElement+"."+b,this.errorContext)},reset:function(){this.successList=[],this.errorList=[],this.errorMap={},this.toShow=a([]),this.toHide=a([]),this.currentElements=a([])},prepareForm:function(){this.reset(),this.toHide=this.errors().add(this.containers)},prepareElement:function(a){this.reset(),this.toHide=this.errorsFor(a)},elementValue:function(b){var c,d=a(b),e=b.type;return"radio"===e||"checkbox"===e?this.findByName(b.name).filter(":checked").val():"number"===e&&"undefined"!=typeof b.validity?b.validity.badInput?!1:d.val():(c=d.val(),"string"==typeof c?c.replace(/\r/g,""):c)},check:function(b){b=this.validationTargetFor(this.clean(b));var c,d,e,f=a(b).rules(),g=a.map(f,function(a,b){return b}).length,h=!1,i=this.elementValue(b);for(d in f){e={method:d,parameters:f[d]};try{if(c=a.validator.methods[d].call(this,i,b,e.parameters),"dependency-mismatch"===c&&1===g){h=!0;continue}if(h=!1,"pending"===c)return void(this.toHide=this.toHide.not(this.errorsFor(b)));if(!c)return this.formatAndAdd(b,e),!1}catch(j){throw this.settings.debug&&window.console&&console.log("Exception occurred when checking element "+b.id+", check the '"+e.method+"' method.",j),j instanceof TypeError&&(j.message+=". Exception occurred when checking element "+b.id+", check the '"+e.method+"' method."),j}}if(!h)return this.objectLength(f)&&this.successList.push(b),!0},customDataMessage:function(b,c){return a(b).data("msg"+c.charAt(0).toUpperCase()+c.substring(1).toLowerCase())||a(b).data("msg")},customMessage:function(a,b){var c=this.settings.messages[a];return c&&(c.constructor===String?c:c[b])},findDefined:function(){for(var a=0;aWarning: No message defined for "+b.name+"")},formatAndAdd:function(b,c){var d=this.defaultMessage(b,c.method),e=/\$?\{(\d+)\}/g;"function"==typeof d?d=d.call(this,c.parameters,b):e.test(d)&&(d=a.validator.format(d.replace(e,"{$1}"),c.parameters)),this.errorList.push({message:d,element:b,method:c.method}),this.errorMap[b.name]=d,this.submitted[b.name]=d},addWrapper:function(a){return this.settings.wrapper&&(a=a.add(a.parent(this.settings.wrapper))),a},defaultShowErrors:function(){var a,b,c;for(a=0;this.errorList[a];a++)c=this.errorList[a],this.settings.highlight&&this.settings.highlight.call(this,c.element,this.settings.errorClass,this.settings.validClass),this.showLabel(c.element,c.message);if(this.errorList.length&&(this.toShow=this.toShow.add(this.containers)),this.settings.success)for(a=0;this.successList[a];a++)this.showLabel(this.successList[a]);if(this.settings.unhighlight)for(a=0,b=this.validElements();b[a];a++)this.settings.unhighlight.call(this,b[a],this.settings.errorClass,this.settings.validClass);this.toHide=this.toHide.not(this.toShow),this.hideErrors(),this.addWrapper(this.toShow).show()},validElements:function(){return this.currentElements.not(this.invalidElements())},invalidElements:function(){return a(this.errorList).map(function(){return this.element})},showLabel:function(b,c){var d,e,f,g=this.errorsFor(b),h=this.idOrName(b),i=a(b).attr("aria-describedby");g.length?(g.removeClass(this.settings.validClass).addClass(this.settings.errorClass),g.html(c)):(g=a("<"+this.settings.errorElement+">").attr("id",h+"-error").addClass(this.settings.errorClass).html(c||""),d=g,this.settings.wrapper&&(d=g.hide().show().wrap("<"+this.settings.wrapper+"/>").parent()),this.labelContainer.length?this.labelContainer.append(d):this.settings.errorPlacement?this.settings.errorPlacement(d,a(b)):d.insertAfter(b),g.is("label")?g.attr("for",h):0===g.parents("label[for='"+h+"']").length&&(f=g.attr("id").replace(/(:|\.|\[|\]|\$)/g,"\\$1"),i?i.match(new RegExp("\\b"+f+"\\b"))||(i+=" "+f):i=f,a(b).attr("aria-describedby",i),e=this.groups[b.name],e&&a.each(this.groups,function(b,c){c===e&&a("[name='"+b+"']",this.currentForm).attr("aria-describedby",g.attr("id"))}))),!c&&this.settings.success&&(g.text(""),"string"==typeof this.settings.success?g.addClass(this.settings.success):this.settings.success(g,b)),this.toShow=this.toShow.add(g)},errorsFor:function(b){var c=this.idOrName(b),d=a(b).attr("aria-describedby"),e="label[for='"+c+"'], label[for='"+c+"'] *";return d&&(e=e+", #"+d.replace(/\s+/g,", #")),this.errors().filter(e)},idOrName:function(a){return this.groups[a.name]||(this.checkable(a)?a.name:a.id||a.name)},validationTargetFor:function(b){return this.checkable(b)&&(b=this.findByName(b.name)),a(b).not(this.settings.ignore)[0]},checkable:function(a){return/radio|checkbox/i.test(a.type)},findByName:function(b){return a(this.currentForm).find("[name='"+b+"']")},getLength:function(b,c){switch(c.nodeName.toLowerCase()){case"select":return a("option:selected",c).length;case"input":if(this.checkable(c))return this.findByName(c.name).filter(":checked").length}return b.length},depend:function(a,b){return this.dependTypes[typeof a]?this.dependTypes[typeof a](a,b):!0},dependTypes:{"boolean":function(a){return a},string:function(b,c){return!!a(b,c.form).length},"function":function(a,b){return a(b)}},optional:function(b){var c=this.elementValue(b);return!a.validator.methods.required.call(this,c,b)&&"dependency-mismatch"},startRequest:function(a){this.pending[a.name]||(this.pendingRequest++,this.pending[a.name]=!0)},stopRequest:function(b,c){this.pendingRequest--,this.pendingRequest<0&&(this.pendingRequest=0),delete this.pending[b.name],c&&0===this.pendingRequest&&this.formSubmitted&&this.form()?(a(this.currentForm).submit(),this.formSubmitted=!1):!c&&0===this.pendingRequest&&this.formSubmitted&&(a(this.currentForm).triggerHandler("invalid-form",[this]),this.formSubmitted=!1)},previousValue:function(b){return a.data(b,"previousValue")||a.data(b,"previousValue",{old:null,valid:!0,message:this.defaultMessage(b,"remote")})},destroy:function(){this.resetForm(),a(this.currentForm).off(".validate").removeData("validator")}},classRuleSettings:{required:{required:!0},email:{email:!0},url:{url:!0},date:{date:!0},dateISO:{dateISO:!0},number:{number:!0},digits:{digits:!0},creditcard:{creditcard:!0}},addClassRules:function(b,c){b.constructor===String?this.classRuleSettings[b]=c:a.extend(this.classRuleSettings,b)},classRules:function(b){var c={},d=a(b).attr("class");return d&&a.each(d.split(" "),function(){this in a.validator.classRuleSettings&&a.extend(c,a.validator.classRuleSettings[this])}),c},normalizeAttributeRule:function(a,b,c,d){/min|max/.test(c)&&(null===b||/number|range|text/.test(b))&&(d=Number(d),isNaN(d)&&(d=void 0)),d||0===d?a[c]=d:b===c&&"range"!==b&&(a[c]=!0)},attributeRules:function(b){var c,d,e={},f=a(b),g=b.getAttribute("type");for(c in a.validator.methods)"required"===c?(d=b.getAttribute(c),""===d&&(d=!0),d=!!d):d=f.attr(c),this.normalizeAttributeRule(e,g,c,d);return e.maxlength&&/-1|2147483647|524288/.test(e.maxlength)&&delete e.maxlength,e},dataRules:function(b){var c,d,e={},f=a(b),g=b.getAttribute("type");for(c in a.validator.methods)d=f.data("rule"+c.charAt(0).toUpperCase()+c.substring(1).toLowerCase()),this.normalizeAttributeRule(e,g,c,d);return e},staticRules:function(b){var c={},d=a.data(b.form,"validator");return d.settings.rules&&(c=a.validator.normalizeRule(d.settings.rules[b.name])||{}),c},normalizeRules:function(b,c){return a.each(b,function(d,e){if(e===!1)return void delete b[d];if(e.param||e.depends){var f=!0;switch(typeof e.depends){case"string":f=!!a(e.depends,c.form).length;break;case"function":f=e.depends.call(c,c)}f?b[d]=void 0!==e.param?e.param:!0:delete b[d]}}),a.each(b,function(d,e){b[d]=a.isFunction(e)?e(c):e}),a.each(["minlength","maxlength"],function(){b[this]&&(b[this]=Number(b[this]))}),a.each(["rangelength","range"],function(){var c;b[this]&&(a.isArray(b[this])?b[this]=[Number(b[this][0]),Number(b[this][1])]:"string"==typeof b[this]&&(c=b[this].replace(/[\[\]]/g,"").split(/[\s,]+/),b[this]=[Number(c[0]),Number(c[1])]))}),a.validator.autoCreateRanges&&(null!=b.min&&null!=b.max&&(b.range=[b.min,b.max],delete b.min,delete b.max),null!=b.minlength&&null!=b.maxlength&&(b.rangelength=[b.minlength,b.maxlength],delete b.minlength,delete b.maxlength)),b},normalizeRule:function(b){if("string"==typeof b){var c={};a.each(b.split(/\s/),function(){c[this]=!0}),b=c}return b},addMethod:function(b,c,d){a.validator.methods[b]=c,a.validator.messages[b]=void 0!==d?d:a.validator.messages[b],c.length<3&&a.validator.addClassRules(b,a.validator.normalizeRule(b))},methods:{required:function(b,c,d){if(!this.depend(d,c))return"dependency-mismatch";if("select"===c.nodeName.toLowerCase()){var e=a(c).val();return e&&e.length>0}return this.checkable(c)?this.getLength(b,c)>0:b.length>0},email:function(a,b){return this.optional(b)||/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(a)},url:function(a,b){return this.optional(b)||/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(a)},date:function(a,b){return this.optional(b)||!/Invalid|NaN/.test(new Date(a).toString())},dateISO:function(a,b){return this.optional(b)||/^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/.test(a)},number:function(a,b){return this.optional(b)||/^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(a)},digits:function(a,b){return this.optional(b)||/^\d+$/.test(a)},creditcard:function(a,b){if(this.optional(b))return"dependency-mismatch";if(/[^0-9 \-]+/.test(a))return!1;var c,d,e=0,f=0,g=!1;if(a=a.replace(/\D/g,""),a.length<13||a.length>19)return!1;for(c=a.length-1;c>=0;c--)d=a.charAt(c),f=parseInt(d,10),g&&(f*=2)>9&&(f-=9),e+=f,g=!g;return e%10===0},minlength:function(b,c,d){var e=a.isArray(b)?b.length:this.getLength(b,c);return this.optional(c)||e>=d},maxlength:function(b,c,d){var e=a.isArray(b)?b.length:this.getLength(b,c);return this.optional(c)||d>=e},rangelength:function(b,c,d){var e=a.isArray(b)?b.length:this.getLength(b,c);return this.optional(c)||e>=d[0]&&e<=d[1]},min:function(a,b,c){return this.optional(b)||a>=c},max:function(a,b,c){return this.optional(b)||c>=a},range:function(a,b,c){return this.optional(b)||a>=c[0]&&a<=c[1]},equalTo:function(b,c,d){var e=a(d);return this.settings.onfocusout&&e.off(".validate-equalTo").on("blur.validate-equalTo",function(){a(c).valid()}),b===e.val()},remote:function(b,c,d){if(this.optional(c))return"dependency-mismatch";var e,f,g=this.previousValue(c);return this.settings.messages[c.name]||(this.settings.messages[c.name]={}),g.originalMessage=this.settings.messages[c.name].remote,this.settings.messages[c.name].remote=g.message,d="string"==typeof d&&{url:d}||d,g.old===b?g.valid:(g.old=b,e=this,this.startRequest(c),f={},f[c.name]=b,a.ajax(a.extend(!0,{mode:"abort",port:"validate"+c.name,dataType:"json",data:f,context:e.currentForm,success:function(d){var f,h,i,j=d===!0||"true"===d;e.settings.messages[c.name].remote=g.originalMessage,j?(i=e.formSubmitted,e.prepareElement(c),e.formSubmitted=i,e.successList.push(c),delete e.invalid[c.name],e.showErrors()):(f={},h=d||e.defaultMessage(c,"remote"),f[c.name]=g.message=a.isFunction(h)?h(b):h,e.invalid[c.name]=!0,e.showErrors(f)),g.valid=j,e.stopRequest(c,j)}},d)),"pending")}}});var b,c={};a.ajaxPrefilter?a.ajaxPrefilter(function(a,b,d){var e=a.port;"abort"===a.mode&&(c[e]&&c[e].abort(),c[e]=d)}):(b=a.ajax,a.ajax=function(d){var e=("mode"in d?d:a.ajaxSettings).mode,f=("port"in d?d:a.ajaxSettings).port;return"abort"===e?(c[f]&&c[f].abort(),c[f]=b.apply(this,arguments),c[f]):b.apply(this,arguments)})}); -------------------------------------------------------------------------------- /Web/wwwroot/lib/jquery/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery", 3 | "main": "dist/jquery.js", 4 | "license": "MIT", 5 | "ignore": [ 6 | "package.json" 7 | ], 8 | "keywords": [ 9 | "jquery", 10 | "javascript", 11 | "browser", 12 | "library" 13 | ], 14 | "homepage": "https://github.com/jquery/jquery-dist", 15 | "version": "2.2.0", 16 | "_release": "2.2.0", 17 | "_resolution": { 18 | "type": "version", 19 | "tag": "2.2.0", 20 | "commit": "6fc01e29bdad0964f62ef56d01297039cdcadbe5" 21 | }, 22 | "_source": "git://github.com/jquery/jquery-dist.git", 23 | "_target": "2.2.0", 24 | "_originalSource": "jquery" 25 | } -------------------------------------------------------------------------------- /Web/wwwroot/lib/jquery/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright jQuery Foundation and other contributors, https://jquery.org/ 2 | 3 | This software consists of voluntary contributions made by many 4 | individuals. For exact contribution history, see the revision history 5 | available at https://github.com/jquery/jquery 6 | 7 | The following license applies to all parts of this software except as 8 | documented below: 9 | 10 | ==== 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining 13 | a copy of this software and associated documentation files (the 14 | "Software"), to deal in the Software without restriction, including 15 | without limitation the rights to use, copy, modify, merge, publish, 16 | distribute, sublicense, and/or sell copies of the Software, and to 17 | permit persons to whom the Software is furnished to do so, subject to 18 | the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be 21 | included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | ==== 32 | 33 | All files located in the node_modules and external directories are 34 | externally maintained libraries used by this software which have their 35 | own licenses; we recommend you read them, as their terms may differ from 36 | the terms above. 37 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | 5 | webmvc: 6 | image: web 7 | build: 8 | context: ./web 9 | dockerfile: Dockerfile.debug 10 | ports: 11 | - "8080:80" 12 | volumes: 13 | - ./web/bin/pub/:/app 14 | container_name: web 15 | depends_on: 16 | - applicants.api 17 | - identity.api 18 | - jobs.api 19 | 20 | applicants.api: 21 | image: applicants.api 22 | environment: 23 | - ConnectionString=Server=sql.data;User=sa;Password=Pass@word;Database=dotnetgigs.applicants; 24 | build: 25 | context: ./services/applicants.api 26 | dockerfile: Dockerfile.debug 27 | ports: 28 | - "8081:80" 29 | volumes: 30 | - ./services/applicants.api/bin/pub/:/app 31 | container_name: applicants.api 32 | depends_on: 33 | - sql.data 34 | - rabbitmq 35 | 36 | jobs.api: 37 | image: jobs.api 38 | environment: 39 | - ConnectionString=Server=sql.data;User=sa;Password=Pass@word;Database=dotnetgigs.jobs; 40 | build: 41 | context: ./services/jobs.api 42 | dockerfile: Dockerfile.debug 43 | ports: 44 | - "8083:80" 45 | volumes: 46 | - ./services/jobs.api/bin/pub/:/app 47 | container_name: jobs.api 48 | depends_on: 49 | - sql.data 50 | - rabbitmq 51 | 52 | identity.api: 53 | image: identity.api 54 | environment: 55 | - RedisHost=user.data:6379 56 | build: 57 | context: ./services/identity.api 58 | dockerfile: Dockerfile.debug 59 | ports: 60 | - "8084:80" 61 | volumes: 62 | - ./services/identity.api/bin/pub/:/app 63 | container_name: identity.api 64 | depends_on: 65 | - user.data 66 | 67 | sql.data: 68 | image: mssql-linux 69 | build: 70 | context: ./Database 71 | dockerfile: Dockerfile 72 | ports: 73 | - "5433:1433" 74 | container_name: mssql-linux 75 | 76 | user.data: 77 | image: redis 78 | 79 | rabbitmq: 80 | image: rabbitmq:3-management 81 | ports: 82 | - "15672:15672" 83 | container_name: rabbitmq 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /dotnetgigs.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio 15 3 | VisualStudioVersion = 15.0.26906.1 4 | MinimumVisualStudioVersion = 15.0.26124.0 5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Services", "Services", "{8AD59A0B-BE21-4351-913E-2AD6FA00997A}" 6 | EndProject 7 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jobs.Api", "Services\Jobs.Api\Jobs.Api.csproj", "{3957D371-D8B9-4934-B5F2-D9396C334B53}" 8 | EndProject 9 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Applicants.Api", "Services\Applicants.Api\Applicants.Api.csproj", "{76840075-AE2C-4C75-973E-F15BAE2E9F37}" 10 | EndProject 11 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Web", "Web\Web.csproj", "{45783FD5-34AA-4580-82F3-734A559AC916}" 12 | EndProject 13 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Http", "Foundation\Http\Http.csproj", "{05604D70-24F8-40AD-934D-CA500F9377F2}" 14 | EndProject 15 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Foundation", "Foundation", "{0FB1B357-0E1A-4351-BF85-DDE1A1359AE2}" 16 | EndProject 17 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Web", "Web", "{A79C663C-57A5-48B7-BF5B-2F5DDDBF866C}" 18 | EndProject 19 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Identity.Api", "Services\Identity.Api\Identity.Api.csproj", "{91EBB73B-ABF1-41CB-8D39-A4E17AE32E2F}" 20 | EndProject 21 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Events", "Foundation\Events\Events.csproj", "{939ABF9A-2C7E-4A2D-8C44-5BB6DA926C68}" 22 | EndProject 23 | Global 24 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 25 | Debug|Any CPU = Debug|Any CPU 26 | Debug|x64 = Debug|x64 27 | Debug|x86 = Debug|x86 28 | Release|Any CPU = Release|Any CPU 29 | Release|x64 = Release|x64 30 | Release|x86 = Release|x86 31 | EndGlobalSection 32 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 33 | {3957D371-D8B9-4934-B5F2-D9396C334B53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {3957D371-D8B9-4934-B5F2-D9396C334B53}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {3957D371-D8B9-4934-B5F2-D9396C334B53}.Debug|x64.ActiveCfg = Debug|Any CPU 36 | {3957D371-D8B9-4934-B5F2-D9396C334B53}.Debug|x64.Build.0 = Debug|Any CPU 37 | {3957D371-D8B9-4934-B5F2-D9396C334B53}.Debug|x86.ActiveCfg = Debug|Any CPU 38 | {3957D371-D8B9-4934-B5F2-D9396C334B53}.Debug|x86.Build.0 = Debug|Any CPU 39 | {3957D371-D8B9-4934-B5F2-D9396C334B53}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {3957D371-D8B9-4934-B5F2-D9396C334B53}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {3957D371-D8B9-4934-B5F2-D9396C334B53}.Release|x64.ActiveCfg = Release|Any CPU 42 | {3957D371-D8B9-4934-B5F2-D9396C334B53}.Release|x64.Build.0 = Release|Any CPU 43 | {3957D371-D8B9-4934-B5F2-D9396C334B53}.Release|x86.ActiveCfg = Release|Any CPU 44 | {3957D371-D8B9-4934-B5F2-D9396C334B53}.Release|x86.Build.0 = Release|Any CPU 45 | {76840075-AE2C-4C75-973E-F15BAE2E9F37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {76840075-AE2C-4C75-973E-F15BAE2E9F37}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {76840075-AE2C-4C75-973E-F15BAE2E9F37}.Debug|x64.ActiveCfg = Debug|Any CPU 48 | {76840075-AE2C-4C75-973E-F15BAE2E9F37}.Debug|x64.Build.0 = Debug|Any CPU 49 | {76840075-AE2C-4C75-973E-F15BAE2E9F37}.Debug|x86.ActiveCfg = Debug|Any CPU 50 | {76840075-AE2C-4C75-973E-F15BAE2E9F37}.Debug|x86.Build.0 = Debug|Any CPU 51 | {76840075-AE2C-4C75-973E-F15BAE2E9F37}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {76840075-AE2C-4C75-973E-F15BAE2E9F37}.Release|Any CPU.Build.0 = Release|Any CPU 53 | {76840075-AE2C-4C75-973E-F15BAE2E9F37}.Release|x64.ActiveCfg = Release|Any CPU 54 | {76840075-AE2C-4C75-973E-F15BAE2E9F37}.Release|x64.Build.0 = Release|Any CPU 55 | {76840075-AE2C-4C75-973E-F15BAE2E9F37}.Release|x86.ActiveCfg = Release|Any CPU 56 | {76840075-AE2C-4C75-973E-F15BAE2E9F37}.Release|x86.Build.0 = Release|Any CPU 57 | {45783FD5-34AA-4580-82F3-734A559AC916}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 58 | {45783FD5-34AA-4580-82F3-734A559AC916}.Debug|Any CPU.Build.0 = Debug|Any CPU 59 | {45783FD5-34AA-4580-82F3-734A559AC916}.Debug|x64.ActiveCfg = Debug|Any CPU 60 | {45783FD5-34AA-4580-82F3-734A559AC916}.Debug|x64.Build.0 = Debug|Any CPU 61 | {45783FD5-34AA-4580-82F3-734A559AC916}.Debug|x86.ActiveCfg = Debug|Any CPU 62 | {45783FD5-34AA-4580-82F3-734A559AC916}.Debug|x86.Build.0 = Debug|Any CPU 63 | {45783FD5-34AA-4580-82F3-734A559AC916}.Release|Any CPU.ActiveCfg = Release|Any CPU 64 | {45783FD5-34AA-4580-82F3-734A559AC916}.Release|Any CPU.Build.0 = Release|Any CPU 65 | {45783FD5-34AA-4580-82F3-734A559AC916}.Release|x64.ActiveCfg = Release|Any CPU 66 | {45783FD5-34AA-4580-82F3-734A559AC916}.Release|x64.Build.0 = Release|Any CPU 67 | {45783FD5-34AA-4580-82F3-734A559AC916}.Release|x86.ActiveCfg = Release|Any CPU 68 | {45783FD5-34AA-4580-82F3-734A559AC916}.Release|x86.Build.0 = Release|Any CPU 69 | {05604D70-24F8-40AD-934D-CA500F9377F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 70 | {05604D70-24F8-40AD-934D-CA500F9377F2}.Debug|Any CPU.Build.0 = Debug|Any CPU 71 | {05604D70-24F8-40AD-934D-CA500F9377F2}.Debug|x64.ActiveCfg = Debug|Any CPU 72 | {05604D70-24F8-40AD-934D-CA500F9377F2}.Debug|x64.Build.0 = Debug|Any CPU 73 | {05604D70-24F8-40AD-934D-CA500F9377F2}.Debug|x86.ActiveCfg = Debug|Any CPU 74 | {05604D70-24F8-40AD-934D-CA500F9377F2}.Debug|x86.Build.0 = Debug|Any CPU 75 | {05604D70-24F8-40AD-934D-CA500F9377F2}.Release|Any CPU.ActiveCfg = Release|Any CPU 76 | {05604D70-24F8-40AD-934D-CA500F9377F2}.Release|Any CPU.Build.0 = Release|Any CPU 77 | {05604D70-24F8-40AD-934D-CA500F9377F2}.Release|x64.ActiveCfg = Release|Any CPU 78 | {05604D70-24F8-40AD-934D-CA500F9377F2}.Release|x64.Build.0 = Release|Any CPU 79 | {05604D70-24F8-40AD-934D-CA500F9377F2}.Release|x86.ActiveCfg = Release|Any CPU 80 | {05604D70-24F8-40AD-934D-CA500F9377F2}.Release|x86.Build.0 = Release|Any CPU 81 | {91EBB73B-ABF1-41CB-8D39-A4E17AE32E2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 82 | {91EBB73B-ABF1-41CB-8D39-A4E17AE32E2F}.Debug|Any CPU.Build.0 = Debug|Any CPU 83 | {91EBB73B-ABF1-41CB-8D39-A4E17AE32E2F}.Debug|x64.ActiveCfg = Debug|Any CPU 84 | {91EBB73B-ABF1-41CB-8D39-A4E17AE32E2F}.Debug|x64.Build.0 = Debug|Any CPU 85 | {91EBB73B-ABF1-41CB-8D39-A4E17AE32E2F}.Debug|x86.ActiveCfg = Debug|Any CPU 86 | {91EBB73B-ABF1-41CB-8D39-A4E17AE32E2F}.Debug|x86.Build.0 = Debug|Any CPU 87 | {91EBB73B-ABF1-41CB-8D39-A4E17AE32E2F}.Release|Any CPU.ActiveCfg = Release|Any CPU 88 | {91EBB73B-ABF1-41CB-8D39-A4E17AE32E2F}.Release|Any CPU.Build.0 = Release|Any CPU 89 | {91EBB73B-ABF1-41CB-8D39-A4E17AE32E2F}.Release|x64.ActiveCfg = Release|Any CPU 90 | {91EBB73B-ABF1-41CB-8D39-A4E17AE32E2F}.Release|x64.Build.0 = Release|Any CPU 91 | {91EBB73B-ABF1-41CB-8D39-A4E17AE32E2F}.Release|x86.ActiveCfg = Release|Any CPU 92 | {91EBB73B-ABF1-41CB-8D39-A4E17AE32E2F}.Release|x86.Build.0 = Release|Any CPU 93 | {939ABF9A-2C7E-4A2D-8C44-5BB6DA926C68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 94 | {939ABF9A-2C7E-4A2D-8C44-5BB6DA926C68}.Debug|Any CPU.Build.0 = Debug|Any CPU 95 | {939ABF9A-2C7E-4A2D-8C44-5BB6DA926C68}.Debug|x64.ActiveCfg = Debug|Any CPU 96 | {939ABF9A-2C7E-4A2D-8C44-5BB6DA926C68}.Debug|x64.Build.0 = Debug|Any CPU 97 | {939ABF9A-2C7E-4A2D-8C44-5BB6DA926C68}.Debug|x86.ActiveCfg = Debug|Any CPU 98 | {939ABF9A-2C7E-4A2D-8C44-5BB6DA926C68}.Debug|x86.Build.0 = Debug|Any CPU 99 | {939ABF9A-2C7E-4A2D-8C44-5BB6DA926C68}.Release|Any CPU.ActiveCfg = Release|Any CPU 100 | {939ABF9A-2C7E-4A2D-8C44-5BB6DA926C68}.Release|Any CPU.Build.0 = Release|Any CPU 101 | {939ABF9A-2C7E-4A2D-8C44-5BB6DA926C68}.Release|x64.ActiveCfg = Release|Any CPU 102 | {939ABF9A-2C7E-4A2D-8C44-5BB6DA926C68}.Release|x64.Build.0 = Release|Any CPU 103 | {939ABF9A-2C7E-4A2D-8C44-5BB6DA926C68}.Release|x86.ActiveCfg = Release|Any CPU 104 | {939ABF9A-2C7E-4A2D-8C44-5BB6DA926C68}.Release|x86.Build.0 = Release|Any CPU 105 | EndGlobalSection 106 | GlobalSection(SolutionProperties) = preSolution 107 | HideSolutionNode = FALSE 108 | EndGlobalSection 109 | GlobalSection(NestedProjects) = preSolution 110 | {3957D371-D8B9-4934-B5F2-D9396C334B53} = {8AD59A0B-BE21-4351-913E-2AD6FA00997A} 111 | {76840075-AE2C-4C75-973E-F15BAE2E9F37} = {8AD59A0B-BE21-4351-913E-2AD6FA00997A} 112 | {45783FD5-34AA-4580-82F3-734A559AC916} = {A79C663C-57A5-48B7-BF5B-2F5DDDBF866C} 113 | {05604D70-24F8-40AD-934D-CA500F9377F2} = {0FB1B357-0E1A-4351-BF85-DDE1A1359AE2} 114 | {91EBB73B-ABF1-41CB-8D39-A4E17AE32E2F} = {8AD59A0B-BE21-4351-913E-2AD6FA00997A} 115 | {939ABF9A-2C7E-4A2D-8C44-5BB6DA926C68} = {0FB1B357-0E1A-4351-BF85-DDE1A1359AE2} 116 | EndGlobalSection 117 | GlobalSection(ExtensibilityGlobals) = postSolution 118 | SolutionGuid = {48BF9F0A-345E-4B8E-B455-D5D049B14AD3} 119 | EndGlobalSection 120 | EndGlobal 121 | --------------------------------------------------------------------------------