Merge pull request #122 from sixeyed/master
Add .NET Core variants running in Windows containers
This commit is contained in:
commit
bba801925e
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,4 +1,5 @@
|
|||||||
*.pyc
|
*.pyc
|
||||||
project.lock.json
|
project.lock.json
|
||||||
bin/
|
bin/
|
||||||
obj/
|
obj/
|
||||||
|
.vs/
|
37
ExampleVotingApp.sln
Normal file
37
ExampleVotingApp.sln
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio 15
|
||||||
|
VisualStudioVersion = 15.0.28010.2036
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Vote", "vote\dotnet\Vote\Vote.csproj", "{9687EAF5-BFF3-4F8D-9C78-1B8EE12CE091}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Worker", "worker\dotnet\Worker\Worker.csproj", "{083764E8-4C34-43FB-A468-F80CE0ADE10A}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Result", "result\dotnet\Result\Result.csproj", "{9AD16D72-E3F5-4A76-814C-40EBD1EE7892}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{9687EAF5-BFF3-4F8D-9C78-1B8EE12CE091}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{9687EAF5-BFF3-4F8D-9C78-1B8EE12CE091}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{9687EAF5-BFF3-4F8D-9C78-1B8EE12CE091}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{9687EAF5-BFF3-4F8D-9C78-1B8EE12CE091}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{083764E8-4C34-43FB-A468-F80CE0ADE10A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{083764E8-4C34-43FB-A468-F80CE0ADE10A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{083764E8-4C34-43FB-A468-F80CE0ADE10A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{083764E8-4C34-43FB-A468-F80CE0ADE10A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{9AD16D72-E3F5-4A76-814C-40EBD1EE7892}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{9AD16D72-E3F5-4A76-814C-40EBD1EE7892}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{9AD16D72-E3F5-4A76-814C-40EBD1EE7892}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{9AD16D72-E3F5-4A76-814C-40EBD1EE7892}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {9DEFC158-1225-4393-8A38-22256211D43D}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
42
README.md
42
README.md
@ -1,10 +1,19 @@
|
|||||||
Example Voting App
|
Example Voting App
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
A simple distributed application running across multiple Docker containers.
|
||||||
|
|
||||||
Getting started
|
Getting started
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
Download [Docker](https://www.docker.com/products/overview). If you are on Mac or Windows, [Docker Compose](https://docs.docker.com/compose) will be automatically installed. On Linux, make sure you have the latest version of [Compose](https://docs.docker.com/compose/install/). If you're using [Docker for Windows](https://docs.docker.com/docker-for-windows/) on Windows 10 pro or later, you must also [switch to Linux containers](https://docs.docker.com/docker-for-windows/#switch-between-windows-and-linux-containers).
|
Download [Docker Desktop](https://www.docker.com/products/docker-desktop) for Mac or Windows. [Docker Compose](https://docs.docker.com/compose) will be automatically installed. On Linux, make sure you have the latest version of [Compose](https://docs.docker.com/compose/install/).
|
||||||
|
|
||||||
|
|
||||||
|
## Linux Containers
|
||||||
|
|
||||||
|
The Linux stack uses Python, Node.js, .NET Core (or optionally Java), with Redis for messaging and Postgres for storage.
|
||||||
|
|
||||||
|
> If you're using [Docker Desktop on Windows](https://store.docker.com/editions/community/docker-ce-desktop-windows), you can run the Linux version by [switching to Linux containers](https://docs.docker.com/docker-for-windows/#switch-between-windows-and-linux-containers), or run the Windows containers version.
|
||||||
|
|
||||||
Run in this directory:
|
Run in this directory:
|
||||||
```
|
```
|
||||||
@ -21,6 +30,27 @@ Once you have your swarm, in this directory run:
|
|||||||
docker stack deploy --compose-file docker-stack.yml vote
|
docker stack deploy --compose-file docker-stack.yml vote
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Windows Containers
|
||||||
|
|
||||||
|
An alternative version of the app uses Windows containers based on Nano Server. This stack runs on .NET Core, using [NATS](https://nats.io) for messaging and [TiDB](https://github.com/pingcap/tidb) for storage.
|
||||||
|
|
||||||
|
You can build from source using:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose -f docker-compose-windows.yml build
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run the app using:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose -f docker-compose-windows.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
> Or in a Windows swarm, run `docker stack deploy -c docker-stack-windows.yml vote`
|
||||||
|
|
||||||
|
The app will be running at [http://localhost:5000](http://localhost:5000), and the results will be at [http://localhost:5001](http://localhost:5001).
|
||||||
|
|
||||||
|
|
||||||
Run the app in Kubernetes
|
Run the app in Kubernetes
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
@ -47,11 +77,11 @@ Architecture
|
|||||||
|
|
||||||
![Architecture diagram](architecture.png)
|
![Architecture diagram](architecture.png)
|
||||||
|
|
||||||
* A Python webapp which lets you vote between two options
|
* A front-end web app in [Python](/vote) or [ASP.NET Core](/vote/dotnet) which lets you vote between two options
|
||||||
* A Redis queue which collects new votes
|
* A [Redis](https://hub.docker.com/_/redis/) or [NATS](https://hub.docker.com/_/nats/) queue which collects new votes
|
||||||
* A .NET worker which consumes votes and stores them in…
|
* A [.NET Core](/worker/src/Worker), [Java](/worker/src/main) or [.NET Core 2.1](/worker/dotnet) worker which consumes votes and stores them in…
|
||||||
* A Postgres database backed by a Docker volume
|
* A [Postgres](https://hub.docker.com/_/postgres/) or [TiDB](https://hub.docker.com/r/dockersamples/tidb/tags/) database backed by a Docker volume
|
||||||
* A Node.js webapp which shows the results of the voting in real time
|
* A [Node.js](/result) or [ASP.NET Core SignalR](/result/dotnet) webapp which shows the results of the voting in real time
|
||||||
|
|
||||||
|
|
||||||
Note
|
Note
|
||||||
|
45
docker-compose-windows.yml
Normal file
45
docker-compose-windows.yml
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
version: "3.2"
|
||||||
|
|
||||||
|
services:
|
||||||
|
vote:
|
||||||
|
image: dockersamples/examplevotingapp_vote:dotnet-nanoserver-sac2016
|
||||||
|
build:
|
||||||
|
context: ./vote/dotnet
|
||||||
|
ports:
|
||||||
|
- "5000:80"
|
||||||
|
depends_on:
|
||||||
|
- message-queue
|
||||||
|
|
||||||
|
result:
|
||||||
|
image: dockersamples/examplevotingapp_result:dotnet-nanoserver-sac2016
|
||||||
|
build:
|
||||||
|
context: ./result/dotnet
|
||||||
|
ports:
|
||||||
|
- "5001:80"
|
||||||
|
environment:
|
||||||
|
- "ConnectionStrings:ResultData=Server=db;Port=4000;Database=votes;User=root;SslMode=None"
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
|
||||||
|
worker:
|
||||||
|
image: dockersamples/examplevotingapp_worker:dotnet-nanoserver-sac2016
|
||||||
|
build:
|
||||||
|
context: ./worker/dotnet
|
||||||
|
environment:
|
||||||
|
- "ConnectionStrings:VoteData=Server=db;Port=4000;Database=votes;User=root;SslMode=None"
|
||||||
|
depends_on:
|
||||||
|
- message-queue
|
||||||
|
- db
|
||||||
|
|
||||||
|
message-queue:
|
||||||
|
image: nats:nanoserver
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: dockersamples/tidb:nanoserver
|
||||||
|
ports:
|
||||||
|
- "3306:4000"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
external:
|
||||||
|
name: nat
|
61
docker-stack-windows.yml
Normal file
61
docker-stack-windows.yml
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
version: "3.2"
|
||||||
|
|
||||||
|
services:
|
||||||
|
vote:
|
||||||
|
image: dockersamples/examplevotingapp_vote:dotnet-nanoserver-sac2016
|
||||||
|
ports:
|
||||||
|
- mode: host
|
||||||
|
target: 80
|
||||||
|
published: 5000
|
||||||
|
deploy:
|
||||||
|
endpoint_mode: dnsrr
|
||||||
|
networks:
|
||||||
|
- frontend
|
||||||
|
- backend
|
||||||
|
|
||||||
|
result:
|
||||||
|
image: dockersamples/examplevotingapp_result:dotnet-nanoserver-sac2016
|
||||||
|
environment:
|
||||||
|
- "ConnectionStrings:ResultData=Server=db;Port=4000;Database=votes;User=root;SslMode=None"
|
||||||
|
ports:
|
||||||
|
- mode: host
|
||||||
|
target: 80
|
||||||
|
published: 5001
|
||||||
|
deploy:
|
||||||
|
endpoint_mode: dnsrr
|
||||||
|
networks:
|
||||||
|
- frontend
|
||||||
|
- backend
|
||||||
|
|
||||||
|
worker:
|
||||||
|
image: dockersamples/examplevotingapp_worker:dotnet-nanoserver-sac2016
|
||||||
|
environment:
|
||||||
|
- "ConnectionStrings:VoteData=Server=db;Port=4000;Database=votes;User=root;SslMode=None"
|
||||||
|
deploy:
|
||||||
|
endpoint_mode: dnsrr
|
||||||
|
mode: replicated
|
||||||
|
replicas: 3
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
|
||||||
|
message-queue:
|
||||||
|
image: nats:nanoserver
|
||||||
|
deploy:
|
||||||
|
endpoint_mode: dnsrr
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: dockersamples/tidb:nanoserver
|
||||||
|
ports:
|
||||||
|
- mode: host
|
||||||
|
target: 4000
|
||||||
|
published: 3306
|
||||||
|
deploy:
|
||||||
|
endpoint_mode: dnsrr
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
|
||||||
|
networks:
|
||||||
|
frontend:
|
||||||
|
backend:
|
16
result/dotnet/Dockerfile
Normal file
16
result/dotnet/Dockerfile
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
FROM microsoft/dotnet:2.1-sdk-nanoserver-sac2016 as builder
|
||||||
|
|
||||||
|
WORKDIR /Result
|
||||||
|
COPY Result/Result.csproj .
|
||||||
|
RUN dotnet restore
|
||||||
|
|
||||||
|
COPY /Result .
|
||||||
|
RUN dotnet publish -c Release -o /out Result.csproj
|
||||||
|
|
||||||
|
# app image
|
||||||
|
FROM microsoft/dotnet:2.1-aspnetcore-runtime-nanoserver-sac2016
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
ENTRYPOINT ["dotnet", "Result.dll"]
|
||||||
|
|
||||||
|
COPY --from=builder /out .
|
9
result/dotnet/Result/Data/IResultData.cs
Normal file
9
result/dotnet/Result/Data/IResultData.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using Result.Models;
|
||||||
|
|
||||||
|
namespace Result.Data
|
||||||
|
{
|
||||||
|
public interface IResultData
|
||||||
|
{
|
||||||
|
ResultsModel GetResults();
|
||||||
|
}
|
||||||
|
}
|
40
result/dotnet/Result/Data/MySqlResultData.cs
Normal file
40
result/dotnet/Result/Data/MySqlResultData.cs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using Dapper;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using MySql.Data.MySqlClient;
|
||||||
|
using Result.Models;
|
||||||
|
|
||||||
|
namespace Result.Data
|
||||||
|
{
|
||||||
|
public class MySqlResultData : IResultData
|
||||||
|
{
|
||||||
|
private readonly string _connectionString;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
public MySqlResultData(IConfiguration config, ILogger<MySqlResultData> logger)
|
||||||
|
{
|
||||||
|
_connectionString = config.GetConnectionString("ResultData");
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResultsModel GetResults()
|
||||||
|
{
|
||||||
|
var model = new ResultsModel();
|
||||||
|
using (var connection = new MySqlConnection(_connectionString))
|
||||||
|
{
|
||||||
|
var results = connection.Query("SELECT vote, COUNT(id) AS count FROM votes GROUP BY vote ORDER BY vote");
|
||||||
|
if (results.Any(x => x.vote == "a"))
|
||||||
|
{
|
||||||
|
model.OptionA = (int) results.First(x => x.vote == "a").count;
|
||||||
|
}
|
||||||
|
if (results.Any(x => x.vote == "b"))
|
||||||
|
{
|
||||||
|
model.OptionB = (int) results.First(x => x.vote == "b").count;
|
||||||
|
}
|
||||||
|
model.VoteCount = model.OptionA + model.OptionB;
|
||||||
|
}
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
result/dotnet/Result/Hubs/ResultsHub.cs
Normal file
9
result/dotnet/Result/Hubs/ResultsHub.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
|
namespace Result.Hubs
|
||||||
|
{
|
||||||
|
public class ResultsHub : Hub
|
||||||
|
{
|
||||||
|
//no public methods, only used for push from PublishRTesultsTimer
|
||||||
|
}
|
||||||
|
}
|
11
result/dotnet/Result/Models/ResultsModel.cs
Normal file
11
result/dotnet/Result/Models/ResultsModel.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
namespace Result.Models
|
||||||
|
{
|
||||||
|
public class ResultsModel
|
||||||
|
{
|
||||||
|
public int OptionA { get; set; }
|
||||||
|
|
||||||
|
public int OptionB { get; set; }
|
||||||
|
|
||||||
|
public int VoteCount { get; set; }
|
||||||
|
}
|
||||||
|
}
|
45
result/dotnet/Result/Pages/Index.cshtml
Normal file
45
result/dotnet/Result/Pages/Index.cshtml
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
@page
|
||||||
|
@model Result.Pages.IndexModel
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>@Model.OptionA vs @Model.OptionB -- Result</title>
|
||||||
|
<base href="/index.html">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale = 1.0">
|
||||||
|
<meta name="keywords" content="docker-compose, docker, stack">
|
||||||
|
<meta name="author" content="Docker">
|
||||||
|
<link rel='stylesheet' href='~/css/site.css' />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="background-stats">
|
||||||
|
<div id="background-stats-1">
|
||||||
|
</div>
|
||||||
|
<!--
|
||||||
|
-->
|
||||||
|
<div id="background-stats-2">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="content-container">
|
||||||
|
<div id="content-container-center">
|
||||||
|
<div id="choice">
|
||||||
|
<div class="choice resulta">
|
||||||
|
<div class="label">@Model.OptionA</div>
|
||||||
|
<div class="stat" id="optionA">50%</div>
|
||||||
|
</div>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="choice resultb">
|
||||||
|
<div class="label">@Model.OptionB</div>
|
||||||
|
<div class="stat" id="optionB">50%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="result">
|
||||||
|
<span id="totalVotes">No votes yet</span>
|
||||||
|
</div>
|
||||||
|
<script src="~/lib/signalr/dist/browser/signalr.min.js"></script>
|
||||||
|
<script src="~/js/results.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
33
result/dotnet/Result/Pages/Index.cshtml.cs
Normal file
33
result/dotnet/Result/Pages/Index.cshtml.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
namespace Result.Pages
|
||||||
|
{
|
||||||
|
public class IndexModel : PageModel
|
||||||
|
{
|
||||||
|
private string _optionA;
|
||||||
|
private string _optionB;
|
||||||
|
protected readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
public string OptionA { get; private set; }
|
||||||
|
public string OptionB { get; private set; }
|
||||||
|
|
||||||
|
public IndexModel(IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_configuration = configuration;
|
||||||
|
_optionA = _configuration.GetValue<string>("Voting:OptionA");
|
||||||
|
_optionB = _configuration.GetValue<string>("Voting:OptionB");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnGet()
|
||||||
|
{
|
||||||
|
OptionA = _optionA;
|
||||||
|
OptionB = _optionB;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
result/dotnet/Result/Program.cs
Normal file
24
result/dotnet/Result/Program.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Result
|
||||||
|
{
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
CreateWebHostBuilder(args).Build().Run();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
|
||||||
|
WebHost.CreateDefaultBuilder(args)
|
||||||
|
.UseStartup<Startup>();
|
||||||
|
}
|
||||||
|
}
|
27
result/dotnet/Result/Properties/launchSettings.json
Normal file
27
result/dotnet/Result/Properties/launchSettings.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:56785",
|
||||||
|
"sslPort": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Result": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
},
|
||||||
|
"applicationUrl": "https://localhost:5001;http://localhost:5000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
result/dotnet/Result/Result.csproj
Normal file
13
result/dotnet/Result/Result.csproj
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Dapper" Version="1.50.5" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
<PackageReference Include="MySql.Data" Version="8.0.12" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
52
result/dotnet/Result/Startup.cs
Normal file
52
result/dotnet/Result/Startup.cs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Result.Data;
|
||||||
|
using Result.Hubs;
|
||||||
|
using Result.Timers;
|
||||||
|
|
||||||
|
namespace Result
|
||||||
|
{
|
||||||
|
public class Startup
|
||||||
|
{
|
||||||
|
public Startup(IConfiguration configuration)
|
||||||
|
{
|
||||||
|
Configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IConfiguration Configuration { get; }
|
||||||
|
|
||||||
|
public void ConfigureServices(IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
|
||||||
|
services.AddSignalR();
|
||||||
|
|
||||||
|
services.AddTransient<IResultData, MySqlResultData>()
|
||||||
|
.AddSingleton<PublishResultsTimer>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
|
||||||
|
{
|
||||||
|
if (env.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseDeveloperExceptionPage();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
app.UseExceptionHandler("/Error");
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseStaticFiles();
|
||||||
|
app.UseSignalR(routes =>
|
||||||
|
{
|
||||||
|
routes.MapHub<ResultsHub>("/resultsHub");
|
||||||
|
});
|
||||||
|
app.UseMvc();
|
||||||
|
|
||||||
|
var timer = app.ApplicationServices.GetService<PublishResultsTimer>();
|
||||||
|
timer.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
result/dotnet/Result/Timers/PublishResultsTimer.cs
Normal file
41
result/dotnet/Result/Timers/PublishResultsTimer.cs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
using System.Timers;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Result.Data;
|
||||||
|
using Result.Hubs;
|
||||||
|
|
||||||
|
namespace Result.Timers
|
||||||
|
{
|
||||||
|
public class PublishResultsTimer
|
||||||
|
{
|
||||||
|
private readonly IHubContext<ResultsHub> _hubContext;
|
||||||
|
private readonly IResultData _resultData;
|
||||||
|
private readonly Timer _timer;
|
||||||
|
|
||||||
|
public PublishResultsTimer(IHubContext<ResultsHub> hubContext, IResultData resultData, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_hubContext = hubContext;
|
||||||
|
_resultData = resultData;
|
||||||
|
var publishMilliseconds = configuration.GetValue<int>("ResultsTimer:PublishMilliseconds");
|
||||||
|
_timer = new Timer(publishMilliseconds)
|
||||||
|
{
|
||||||
|
Enabled = false
|
||||||
|
};
|
||||||
|
_timer.Elapsed += PublishResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
if (!_timer.Enabled)
|
||||||
|
{
|
||||||
|
_timer.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PublishResults(object sender, ElapsedEventArgs e)
|
||||||
|
{
|
||||||
|
var model = _resultData.GetResults();
|
||||||
|
_hubContext.Clients.All.SendAsync("UpdateResults", model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
result/dotnet/Result/appsettings.Development.json
Normal file
9
result/dotnet/Result/appsettings.Development.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Debug",
|
||||||
|
"System": "Information",
|
||||||
|
"Microsoft": "Information"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
result/dotnet/Result/appsettings.json
Normal file
18
result/dotnet/Result/appsettings.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"Voting": {
|
||||||
|
"OptionA": "Cats",
|
||||||
|
"OptionB": "Dogs"
|
||||||
|
},
|
||||||
|
"ResultsTimer": {
|
||||||
|
"PublishMilliseconds": 2500
|
||||||
|
},
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"ResultData": "Server=mysql;Port=4000;Database=votes;User=root;SslMode=None"
|
||||||
|
},
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
14
result/dotnet/Result/libman.json
Normal file
14
result/dotnet/Result/libman.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0",
|
||||||
|
"defaultProvider": "unpkg",
|
||||||
|
"libraries": [
|
||||||
|
{
|
||||||
|
"library": "@aspnet/signalr@1.0.3",
|
||||||
|
"destination": "wwwroot/lib/signalr/",
|
||||||
|
"files": [
|
||||||
|
"dist/browser/signalr.js",
|
||||||
|
"dist/browser/signalr.min.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
121
result/dotnet/Result/wwwroot/css/site.css
Normal file
121
result/dotnet/Result/wwwroot/css/site.css
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
@import url(//fonts.googleapis.com/css?family=Open+Sans:400,700,600);
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 1s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
height: 150px;
|
||||||
|
width: 2px;
|
||||||
|
background-color: #C0C9CE;
|
||||||
|
position: relative;
|
||||||
|
top: 50%;
|
||||||
|
float: left;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#background-stats-1 {
|
||||||
|
background-color: #2196f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
#background-stats-2 {
|
||||||
|
background-color: #00cbca;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content-container {
|
||||||
|
z-index: 2;
|
||||||
|
position: relative;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: table;
|
||||||
|
padding: 10px;
|
||||||
|
max-width: 940px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content-container-center {
|
||||||
|
display: table-cell;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#result {
|
||||||
|
z-index: 3;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 40px;
|
||||||
|
right: 20px;
|
||||||
|
color: #fff;
|
||||||
|
opacity: 0.5;
|
||||||
|
font-size: 45px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
#choice {
|
||||||
|
transition: all 300ms linear;
|
||||||
|
line-height: 1.3em;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 10px 0 0 #fff, -10px 0 0 #fff;
|
||||||
|
vertical-align: middle;
|
||||||
|
font-size: 40px;
|
||||||
|
font-weight: 600;
|
||||||
|
width: 450px;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#choice a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#choice a:hover, #choice a:focus {
|
||||||
|
outline: 0;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
#choice .choice {
|
||||||
|
width: 49%;
|
||||||
|
position: relative;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#choice .choice .label {
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
#choice .choice.resultb {
|
||||||
|
color: #00cbca;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#choice .choice.resulta {
|
||||||
|
color: #2196f3;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#background-stats {
|
||||||
|
z-index: 1;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
#background-stats div {
|
||||||
|
transition: width 400ms ease-in-out;
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: -4px;
|
||||||
|
width: 50%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
1
result/dotnet/Result/wwwroot/css/site.min.css
vendored
Normal file
1
result/dotnet/Result/wwwroot/css/site.min.css
vendored
Normal file
@ -0,0 +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}}
|
41
result/dotnet/Result/wwwroot/js/results.js
Normal file
41
result/dotnet/Result/wwwroot/js/results.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
var connection = new signalR.HubConnectionBuilder().withUrl("/resultsHub").build();
|
||||||
|
|
||||||
|
connection.on("UpdateResults", function (results) {
|
||||||
|
document.body.style.opacity=1;
|
||||||
|
|
||||||
|
var a = parseInt(results.optionA || 0);
|
||||||
|
var b = parseInt(results.optionB || 0);
|
||||||
|
var percentages = getPercentages(a, b);
|
||||||
|
|
||||||
|
document.getElementById("optionA").innerText = percentages.a + "%";
|
||||||
|
document.getElementById("optionB").innerText = percentages.b + "%";
|
||||||
|
var totalVotes = 'No votes yet';
|
||||||
|
if (results.voteCount > 0) {
|
||||||
|
totalVotes = results.voteCount + (results.voteCount > 1 ? " votes" : " vote");
|
||||||
|
}
|
||||||
|
document.getElementById("totalVotes").innerText = totalVotes;
|
||||||
|
|
||||||
|
var bg1 = document.getElementById('background-stats-1');
|
||||||
|
var bg2 = document.getElementById('background-stats-2');
|
||||||
|
bg1.style.width = (percentages.a-0.2) + "%";
|
||||||
|
bg2.style.width = (percentages.b-0.2) + "%";
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.start().catch(function (err) {
|
||||||
|
return console.error(err.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
function getPercentages(a, b) {
|
||||||
|
var result = {};
|
||||||
|
|
||||||
|
if (a + b > 0) {
|
||||||
|
result.a = Math.round(a / (a + b) * 100);
|
||||||
|
result.b = 100 - result.a;
|
||||||
|
} else {
|
||||||
|
result.a = result.b = 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
3441
result/dotnet/Result/wwwroot/lib/signalr/dist/browser/signalr.js
vendored
Normal file
3441
result/dotnet/Result/wwwroot/lib/signalr/dist/browser/signalr.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
17
result/dotnet/Result/wwwroot/lib/signalr/dist/browser/signalr.min.js
vendored
Normal file
17
result/dotnet/Result/wwwroot/lib/signalr/dist/browser/signalr.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
16
vote/dotnet/Dockerfile
Normal file
16
vote/dotnet/Dockerfile
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
FROM microsoft/dotnet:2.1-sdk-nanoserver-sac2016 as builder
|
||||||
|
|
||||||
|
WORKDIR /Vote
|
||||||
|
COPY Vote/Vote.csproj .
|
||||||
|
RUN dotnet restore
|
||||||
|
|
||||||
|
COPY /Vote .
|
||||||
|
RUN dotnet publish -c Release -o /out Vote.csproj
|
||||||
|
|
||||||
|
# app image
|
||||||
|
FROM microsoft/dotnet:2.1-aspnetcore-runtime-nanoserver-sac2016
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
ENTRYPOINT ["dotnet", "Vote.dll"]
|
||||||
|
|
||||||
|
COPY --from=builder /out .
|
12
vote/dotnet/Vote/Messaging/IMessageQueue.cs
Normal file
12
vote/dotnet/Vote/Messaging/IMessageQueue.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using NATS.Client;
|
||||||
|
using Vote.Messaging.Messages;
|
||||||
|
|
||||||
|
namespace Vote.Messaging
|
||||||
|
{
|
||||||
|
public interface IMessageQueue
|
||||||
|
{
|
||||||
|
IConnection CreateConnection();
|
||||||
|
|
||||||
|
void Publish<TMessage>(TMessage message) where TMessage : Message;
|
||||||
|
}
|
||||||
|
}
|
23
vote/dotnet/Vote/Messaging/MessageHelper.cs
Normal file
23
vote/dotnet/Vote/Messaging/MessageHelper.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using Vote.Messaging.Messages;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Vote.Messaging
|
||||||
|
{
|
||||||
|
public class MessageHelper
|
||||||
|
{
|
||||||
|
public static byte[] ToData<TMessage>(TMessage message)
|
||||||
|
where TMessage : Message
|
||||||
|
{
|
||||||
|
var json = JsonConvert.SerializeObject(message);
|
||||||
|
return Encoding.Unicode.GetBytes(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TMessage FromData<TMessage>(byte[] data)
|
||||||
|
where TMessage : Message
|
||||||
|
{
|
||||||
|
var json = Encoding.Unicode.GetString(data);
|
||||||
|
return (TMessage)JsonConvert.DeserializeObject<TMessage>(json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
35
vote/dotnet/Vote/Messaging/MessageQueue.cs
Normal file
35
vote/dotnet/Vote/Messaging/MessageQueue.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using NATS.Client;
|
||||||
|
using Vote.Messaging.Messages;
|
||||||
|
|
||||||
|
namespace Vote.Messaging
|
||||||
|
{
|
||||||
|
public class MessageQueue : IMessageQueue
|
||||||
|
{
|
||||||
|
protected readonly IConfiguration _configuration;
|
||||||
|
protected readonly ILogger _logger;
|
||||||
|
|
||||||
|
public MessageQueue(IConfiguration configuration, ILogger<MessageQueue> logger)
|
||||||
|
{
|
||||||
|
_configuration = configuration;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Publish<TMessage>(TMessage message)
|
||||||
|
where TMessage : Message
|
||||||
|
{
|
||||||
|
using (var connection = CreateConnection())
|
||||||
|
{
|
||||||
|
var data = MessageHelper.ToData(message);
|
||||||
|
connection.Publish(message.Subject, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IConnection CreateConnection()
|
||||||
|
{
|
||||||
|
var url = _configuration.GetValue<string>("MessageQueue:Url");
|
||||||
|
return new ConnectionFactory().CreateConnection(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
vote/dotnet/Vote/Messaging/Messages/Events/VoteCastEvent.cs
Normal file
15
vote/dotnet/Vote/Messaging/Messages/Events/VoteCastEvent.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Vote.Messaging.Messages
|
||||||
|
{
|
||||||
|
public class VoteCastEvent : Message
|
||||||
|
{
|
||||||
|
public override string Subject { get { return MessageSubject; } }
|
||||||
|
|
||||||
|
public string VoterId {get; set;}
|
||||||
|
|
||||||
|
public string Vote {get; set; }
|
||||||
|
|
||||||
|
public static string MessageSubject = "events.vote.votecast";
|
||||||
|
}
|
||||||
|
}
|
16
vote/dotnet/Vote/Messaging/Messages/Message.cs
Normal file
16
vote/dotnet/Vote/Messaging/Messages/Message.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Vote.Messaging.Messages
|
||||||
|
{
|
||||||
|
public abstract class Message
|
||||||
|
{
|
||||||
|
public string CorrelationId { get; set; }
|
||||||
|
|
||||||
|
public abstract string Subject { get; }
|
||||||
|
|
||||||
|
public Message()
|
||||||
|
{
|
||||||
|
CorrelationId = Guid.NewGuid().ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
50
vote/dotnet/Vote/Pages/Index.cshtml
Normal file
50
vote/dotnet/Vote/Pages/Index.cshtml
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
@page
|
||||||
|
@model Vote.Pages.IndexModel
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>@Model.OptionA vs @Model.OptionB!</title>
|
||||||
|
<base href="/index.html">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale = 1.0">
|
||||||
|
<meta name="keywords" content="docker-compose, docker, stack">
|
||||||
|
<meta name="author" content="Docker DevRel team">
|
||||||
|
<link rel="stylesheet" href="~/css/site.css" />
|
||||||
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="content-container">
|
||||||
|
<div id="content-container-center">
|
||||||
|
<h3>@Model.OptionA vs @Model.OptionB!</h3>
|
||||||
|
<form method="POST" id="choice" name='form'>
|
||||||
|
<button id="a" type="submit" name="vote" class="a" value="a">@Model.OptionA</button>
|
||||||
|
<button id="b" type="submit" name="vote" class="b" value="b">@Model.OptionB</button>
|
||||||
|
</form>
|
||||||
|
<div id="tip">
|
||||||
|
(Tip: you can change your vote)
|
||||||
|
</div>
|
||||||
|
<div id="hostname">
|
||||||
|
Processed by container ID @System.Environment.MachineName
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="~/js/jquery-1.11.1-min.js" type="text/javascript"></script>
|
||||||
|
@*<script src="//cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.js"></script>*@
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var vote = "@Model.Vote";
|
||||||
|
|
||||||
|
if(vote == "a"){
|
||||||
|
$(".a").prop('disabled', true);
|
||||||
|
$(".a").html('@Model.OptionA <i class="fa fa-check-circle"></i>');
|
||||||
|
$(".b").css('opacity','0.5');
|
||||||
|
}
|
||||||
|
if(vote == "b"){
|
||||||
|
$(".b").prop('disabled', true);
|
||||||
|
$(".b").html('@Model.OptionB <i class="fa fa-check-circle"></i>');
|
||||||
|
$(".a").css('opacity','0.5');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
75
vote/dotnet/Vote/Pages/Index.cshtml.cs
Normal file
75
vote/dotnet/Vote/Pages/Index.cshtml.cs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Vote.Messaging;
|
||||||
|
using Vote.Messaging.Messages;
|
||||||
|
|
||||||
|
namespace Vote.Pages
|
||||||
|
{
|
||||||
|
public class IndexModel : PageModel
|
||||||
|
{
|
||||||
|
private string _optionA;
|
||||||
|
private string _optionB;
|
||||||
|
|
||||||
|
protected readonly IMessageQueue _messageQueue;
|
||||||
|
protected readonly IConfiguration _configuration;
|
||||||
|
protected readonly ILogger _logger;
|
||||||
|
|
||||||
|
public IndexModel(IMessageQueue messageQueue, IConfiguration configuration, ILogger<IndexModel> logger)
|
||||||
|
{
|
||||||
|
_messageQueue = messageQueue;
|
||||||
|
_configuration = configuration;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
_optionA = _configuration.GetValue<string>("Voting:OptionA");
|
||||||
|
_optionB = _configuration.GetValue<string>("Voting:OptionB");
|
||||||
|
}
|
||||||
|
|
||||||
|
public string OptionA { get; private set; }
|
||||||
|
|
||||||
|
public string OptionB { get; private set; }
|
||||||
|
|
||||||
|
[BindProperty]
|
||||||
|
public string Vote { get; private set; }
|
||||||
|
|
||||||
|
private string _voterId
|
||||||
|
{
|
||||||
|
get { return TempData.Peek("VoterId") as string; }
|
||||||
|
set { TempData["VoterId"] = value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnGet()
|
||||||
|
{
|
||||||
|
OptionA = _optionA;
|
||||||
|
OptionB = _optionB;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IActionResult OnPost(string vote)
|
||||||
|
{
|
||||||
|
Vote = vote;
|
||||||
|
OptionA = _optionA;
|
||||||
|
OptionB = _optionB;
|
||||||
|
if (_configuration.GetValue<bool>("MessageQueue:Enabled"))
|
||||||
|
{
|
||||||
|
PublishVote(vote);
|
||||||
|
}
|
||||||
|
return Page();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PublishVote(string vote)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_voterId))
|
||||||
|
{
|
||||||
|
_voterId = Guid.NewGuid().ToString();
|
||||||
|
}
|
||||||
|
var message = new VoteCastEvent
|
||||||
|
{
|
||||||
|
VoterId = _voterId,
|
||||||
|
Vote = vote
|
||||||
|
};
|
||||||
|
_messageQueue.Publish(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
vote/dotnet/Vote/Program.cs
Normal file
24
vote/dotnet/Vote/Program.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Vote
|
||||||
|
{
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
CreateWebHostBuilder(args).Build().Run();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
|
||||||
|
WebHost.CreateDefaultBuilder(args)
|
||||||
|
.UseStartup<Startup>();
|
||||||
|
}
|
||||||
|
}
|
27
vote/dotnet/Vote/Properties/launchSettings.json
Normal file
27
vote/dotnet/Vote/Properties/launchSettings.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:6116",
|
||||||
|
"sslPort": 44316
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Vote": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"applicationUrl": "https://localhost:5001;http://localhost:5000",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
vote/dotnet/Vote/Startup.cs
Normal file
45
vote/dotnet/Vote/Startup.cs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Vote.Messaging;
|
||||||
|
|
||||||
|
namespace Vote
|
||||||
|
{
|
||||||
|
public class Startup
|
||||||
|
{
|
||||||
|
public Startup(IConfiguration configuration)
|
||||||
|
{
|
||||||
|
Configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IConfiguration Configuration { get; }
|
||||||
|
|
||||||
|
public void ConfigureServices(IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddMvc()
|
||||||
|
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
|
||||||
|
.AddRazorPagesOptions(options =>
|
||||||
|
{
|
||||||
|
options.Conventions.ConfigureFilter(new IgnoreAntiforgeryTokenAttribute());
|
||||||
|
});
|
||||||
|
services.AddTransient<IMessageQueue, MessageQueue>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
|
||||||
|
{
|
||||||
|
if (env.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseDeveloperExceptionPage();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
app.UseExceptionHandler("/Error");
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseStaticFiles();
|
||||||
|
app.UseMvc();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
vote/dotnet/Vote/Vote.csproj
Normal file
12
vote/dotnet/Vote/Vote.csproj
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
<PackageReference Include="NATS.Client" Version="0.8.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
25
vote/dotnet/Vote/Vote.sln
Normal file
25
vote/dotnet/Vote/Vote.sln
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio 15
|
||||||
|
VisualStudioVersion = 15.0.27703.2042
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Vote", "Vote.csproj", "{DA159FEC-BE4D-4704-ACB2-E03FFA6F2D3B}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{DA159FEC-BE4D-4704-ACB2-E03FFA6F2D3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{DA159FEC-BE4D-4704-ACB2-E03FFA6F2D3B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{DA159FEC-BE4D-4704-ACB2-E03FFA6F2D3B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{DA159FEC-BE4D-4704-ACB2-E03FFA6F2D3B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {BEABFEFC-9957-41E3-96A1-7F501F69411D}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
12
vote/dotnet/Vote/appsettings.Development.json
Normal file
12
vote/dotnet/Vote/appsettings.Development.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"MessageQueue": {
|
||||||
|
"Enabled": false
|
||||||
|
},
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Debug",
|
||||||
|
"System": "Information",
|
||||||
|
"Microsoft": "Information"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
vote/dotnet/Vote/appsettings.json
Normal file
16
vote/dotnet/Vote/appsettings.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"Voting": {
|
||||||
|
"OptionA": "Cats",
|
||||||
|
"OptionB": "Dogs"
|
||||||
|
},
|
||||||
|
"MessageQueue": {
|
||||||
|
"Enabled": true,
|
||||||
|
"Url": "nats://message-queue:4222"
|
||||||
|
},
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
135
vote/dotnet/Vote/wwwroot/css/site.css
Normal file
135
vote/dotnet/Vote/wwwroot/css/site.css
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
@import url(//fonts.googleapis.com/css?family=Open+Sans:400,700,600);
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #F7F8F9;
|
||||||
|
height: 100vh;
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
button[type="submit"] {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-webkit-border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button i {
|
||||||
|
float: right;
|
||||||
|
padding-right: 30px;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.a {
|
||||||
|
background-color: #1aaaf8;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.b {
|
||||||
|
background-color: #00cbca;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tip {
|
||||||
|
text-align: left;
|
||||||
|
color: #c0c9ce;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hostname {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100px;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
color: #8f9ea8;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content-container {
|
||||||
|
z-index: 2;
|
||||||
|
position: relative;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: table;
|
||||||
|
padding: 10px;
|
||||||
|
max-width: 940px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content-container-center {
|
||||||
|
display: table-cell;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content-container-center h3 {
|
||||||
|
color: #254356;
|
||||||
|
}
|
||||||
|
|
||||||
|
#choice {
|
||||||
|
transition: all 300ms linear;
|
||||||
|
line-height: 1.3em;
|
||||||
|
display: inline;
|
||||||
|
vertical-align: middle;
|
||||||
|
font-size: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#choice a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#choice a:hover, #choice a:focus {
|
||||||
|
outline: 0;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
#choice button {
|
||||||
|
display: block;
|
||||||
|
height: 80px;
|
||||||
|
width: 330px;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#choice button.a:hover {
|
||||||
|
background-color: #1488c6;
|
||||||
|
}
|
||||||
|
|
||||||
|
#choice button.b:hover {
|
||||||
|
background-color: #00a2a1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#choice button.a:focus {
|
||||||
|
background-color: #1488c6;
|
||||||
|
}
|
||||||
|
|
||||||
|
#choice button.b:focus {
|
||||||
|
background-color: #00a2a1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#background-stats {
|
||||||
|
z-index: 1;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
#background-stats div {
|
||||||
|
transition: width 400ms ease-in-out;
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: -4px;
|
||||||
|
width: 50%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
1
vote/dotnet/Vote/wwwroot/css/site.min.css
vendored
Normal file
1
vote/dotnet/Vote/wwwroot/css/site.min.css
vendored
Normal file
@ -0,0 +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}}
|
13
vote/dotnet/Vote/wwwroot/js/jquery-1.11.1-min.js
vendored
Normal file
13
vote/dotnet/Vote/wwwroot/js/jquery-1.11.1-min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
16
worker/dotnet/Dockerfile
Normal file
16
worker/dotnet/Dockerfile
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
FROM microsoft/dotnet:2.1-sdk-nanoserver-sac2016 as builder
|
||||||
|
|
||||||
|
WORKDIR /Worker
|
||||||
|
COPY Worker/Worker.csproj .
|
||||||
|
RUN dotnet restore
|
||||||
|
|
||||||
|
COPY /Worker .
|
||||||
|
RUN dotnet publish -c Release -o /out Worker.csproj
|
||||||
|
|
||||||
|
# app image
|
||||||
|
FROM microsoft/dotnet:2.1-runtime-nanoserver-sac2016
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
ENTRYPOINT ["dotnet", "Worker.dll"]
|
||||||
|
|
||||||
|
COPY --from=builder /out .
|
7
worker/dotnet/Worker/Data/IVoteData.cs
Normal file
7
worker/dotnet/Worker/Data/IVoteData.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace Worker.Data
|
||||||
|
{
|
||||||
|
public interface IVoteData
|
||||||
|
{
|
||||||
|
void Set(string voterId, string vote);
|
||||||
|
}
|
||||||
|
}
|
35
worker/dotnet/Worker/Data/MySqlVoteData.cs
Normal file
35
worker/dotnet/Worker/Data/MySqlVoteData.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Worker.Entities;
|
||||||
|
|
||||||
|
namespace Worker.Data
|
||||||
|
{
|
||||||
|
public class MySqlVoteData : IVoteData
|
||||||
|
{
|
||||||
|
private readonly VoteContext _context;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
public MySqlVoteData(VoteContext context, ILogger<MySqlVoteData> logger)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Set(string voterId, string vote)
|
||||||
|
{
|
||||||
|
var currentVote = _context.Votes.Find(voterId);
|
||||||
|
if (currentVote == null)
|
||||||
|
{
|
||||||
|
_context.Votes.Add(new Vote
|
||||||
|
{
|
||||||
|
VoterId = voterId,
|
||||||
|
VoteOption = vote
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (currentVote.VoteOption != vote)
|
||||||
|
{
|
||||||
|
currentVote.VoteOption = vote;
|
||||||
|
}
|
||||||
|
_context.SaveChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
worker/dotnet/Worker/Entities/Vote.cs
Normal file
16
worker/dotnet/Worker/Entities/Vote.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace Worker.Entities
|
||||||
|
{
|
||||||
|
[Table("votes")]
|
||||||
|
public class Vote
|
||||||
|
{
|
||||||
|
[Column("id")]
|
||||||
|
[Key]
|
||||||
|
public string VoterId { get; set; }
|
||||||
|
|
||||||
|
[Column("vote")]
|
||||||
|
public string VoteOption { get; set; }
|
||||||
|
}
|
||||||
|
}
|
19
worker/dotnet/Worker/Entities/VoteContext.cs
Normal file
19
worker/dotnet/Worker/Entities/VoteContext.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Worker.Entities
|
||||||
|
{
|
||||||
|
public class VoteContext : DbContext
|
||||||
|
{
|
||||||
|
private static bool _EnsureCreated;
|
||||||
|
public VoteContext(DbContextOptions options) : base(options)
|
||||||
|
{
|
||||||
|
if (!_EnsureCreated)
|
||||||
|
{
|
||||||
|
Database.EnsureCreated();
|
||||||
|
_EnsureCreated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DbSet<Vote> Votes { get; set; }
|
||||||
|
}
|
||||||
|
}
|
12
worker/dotnet/Worker/Messaging/IMessageQueue.cs
Normal file
12
worker/dotnet/Worker/Messaging/IMessageQueue.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using NATS.Client;
|
||||||
|
using Worker.Messaging.Messages;
|
||||||
|
|
||||||
|
namespace Worker.Messaging
|
||||||
|
{
|
||||||
|
public interface IMessageQueue
|
||||||
|
{
|
||||||
|
IConnection CreateConnection();
|
||||||
|
|
||||||
|
void Publish<TMessage>(TMessage message) where TMessage : Message;
|
||||||
|
}
|
||||||
|
}
|
23
worker/dotnet/Worker/Messaging/MessageHelper.cs
Normal file
23
worker/dotnet/Worker/Messaging/MessageHelper.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using Worker.Messaging.Messages;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Worker.Messaging
|
||||||
|
{
|
||||||
|
public class MessageHelper
|
||||||
|
{
|
||||||
|
public static byte[] ToData<TMessage>(TMessage message)
|
||||||
|
where TMessage : Message
|
||||||
|
{
|
||||||
|
var json = JsonConvert.SerializeObject(message);
|
||||||
|
return Encoding.Unicode.GetBytes(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TMessage FromData<TMessage>(byte[] data)
|
||||||
|
where TMessage : Message
|
||||||
|
{
|
||||||
|
var json = Encoding.Unicode.GetString(data);
|
||||||
|
return (TMessage)JsonConvert.DeserializeObject<TMessage>(json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
35
worker/dotnet/Worker/Messaging/MessageQueue.cs
Normal file
35
worker/dotnet/Worker/Messaging/MessageQueue.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using NATS.Client;
|
||||||
|
using Worker.Messaging.Messages;
|
||||||
|
|
||||||
|
namespace Worker.Messaging
|
||||||
|
{
|
||||||
|
public class MessageQueue : IMessageQueue
|
||||||
|
{
|
||||||
|
protected readonly IConfiguration _configuration;
|
||||||
|
protected readonly ILogger _logger;
|
||||||
|
|
||||||
|
public MessageQueue(IConfiguration configuration, ILogger<MessageQueue> logger)
|
||||||
|
{
|
||||||
|
_configuration = configuration;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Publish<TMessage>(TMessage message)
|
||||||
|
where TMessage : Message
|
||||||
|
{
|
||||||
|
using (var connection = CreateConnection())
|
||||||
|
{
|
||||||
|
var data = MessageHelper.ToData(message);
|
||||||
|
connection.Publish(message.Subject, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IConnection CreateConnection()
|
||||||
|
{
|
||||||
|
var url = _configuration.GetValue<string>("MessageQueue:Url");
|
||||||
|
return new ConnectionFactory().CreateConnection(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Worker.Messaging.Messages
|
||||||
|
{
|
||||||
|
public class VoteCastEvent : Message
|
||||||
|
{
|
||||||
|
public override string Subject { get { return MessageSubject; } }
|
||||||
|
|
||||||
|
public string VoterId {get; set;}
|
||||||
|
|
||||||
|
public string Vote {get; set; }
|
||||||
|
|
||||||
|
public static string MessageSubject = "events.vote.votecast";
|
||||||
|
}
|
||||||
|
}
|
16
worker/dotnet/Worker/Messaging/Messages/Message.cs
Normal file
16
worker/dotnet/Worker/Messaging/Messages/Message.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Worker.Messaging.Messages
|
||||||
|
{
|
||||||
|
public abstract class Message
|
||||||
|
{
|
||||||
|
public string CorrelationId { get; set; }
|
||||||
|
|
||||||
|
public abstract string Subject { get; }
|
||||||
|
|
||||||
|
public Message()
|
||||||
|
{
|
||||||
|
CorrelationId = Guid.NewGuid().ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
39
worker/dotnet/Worker/Program.cs
Normal file
39
worker/dotnet/Worker/Program.cs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using Worker.Data;
|
||||||
|
using Worker.Entities;
|
||||||
|
using Worker.Messaging;
|
||||||
|
using Worker.Workers;
|
||||||
|
|
||||||
|
namespace Worker
|
||||||
|
{
|
||||||
|
class Program
|
||||||
|
{
|
||||||
|
static void Main(string[] args)
|
||||||
|
{
|
||||||
|
var config = new ConfigurationBuilder()
|
||||||
|
.AddJsonFile("appsettings.json")
|
||||||
|
.AddEnvironmentVariables()
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var loggerFactory = new LoggerFactory()
|
||||||
|
.AddConsole();
|
||||||
|
|
||||||
|
var services = new ServiceCollection()
|
||||||
|
.AddSingleton(loggerFactory)
|
||||||
|
.AddLogging()
|
||||||
|
.AddSingleton<IConfiguration>(config)
|
||||||
|
.AddTransient<IVoteData, MySqlVoteData>()
|
||||||
|
.AddTransient<IMessageQueue, MessageQueue>()
|
||||||
|
.AddSingleton<QueueWorker>()
|
||||||
|
.AddDbContext<VoteContext>(builder => builder.UseMySQL(config.GetConnectionString("VoteData")));
|
||||||
|
|
||||||
|
var provider = services.BuildServiceProvider();
|
||||||
|
var worker = provider.GetService<QueueWorker>();
|
||||||
|
worker.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
worker/dotnet/Worker/Worker.csproj
Normal file
30
worker/dotnet/Worker/Worker.csproj
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove="appsettings.json" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="appsettings.json">
|
||||||
|
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.0.3" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.1.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.1.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.1.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.1.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.1.1" />
|
||||||
|
<PackageReference Include="MySql.Data.EntityFrameworkCore" Version="8.0.12" />
|
||||||
|
<PackageReference Include="NATS.Client" Version="0.8.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
62
worker/dotnet/Worker/Workers/QueueWorker.cs
Normal file
62
worker/dotnet/Worker/Workers/QueueWorker.cs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using NATS.Client;
|
||||||
|
using Worker.Data;
|
||||||
|
using Worker.Messaging;
|
||||||
|
using Worker.Messaging.Messages;
|
||||||
|
|
||||||
|
namespace Worker.Workers
|
||||||
|
{
|
||||||
|
public class QueueWorker
|
||||||
|
{
|
||||||
|
private static ManualResetEvent _ResetEvent = new ManualResetEvent(false);
|
||||||
|
private const string QUEUE_GROUP = "save-handler";
|
||||||
|
|
||||||
|
private readonly IMessageQueue _messageQueue;
|
||||||
|
private readonly IConfiguration _config;
|
||||||
|
private readonly IVoteData _data;
|
||||||
|
protected readonly ILogger _logger;
|
||||||
|
|
||||||
|
public QueueWorker(IMessageQueue messageQueue, IVoteData data, IConfiguration config, ILogger<QueueWorker> logger)
|
||||||
|
{
|
||||||
|
_messageQueue = messageQueue;
|
||||||
|
_data = data;
|
||||||
|
_config = config;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
_logger.LogInformation($"Connecting to message queue url: {_config.GetValue<string>("MessageQueue:Url")}");
|
||||||
|
using (var connection = _messageQueue.CreateConnection())
|
||||||
|
{
|
||||||
|
var subscription = connection.SubscribeAsync(VoteCastEvent.MessageSubject, QUEUE_GROUP);
|
||||||
|
subscription.MessageHandler += SaveVote;
|
||||||
|
subscription.Start();
|
||||||
|
_logger.LogInformation($"Listening on subject: {VoteCastEvent.MessageSubject}, queue: {QUEUE_GROUP}");
|
||||||
|
|
||||||
|
_ResetEvent.WaitOne();
|
||||||
|
connection.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveVote(object sender, MsgHandlerEventArgs e)
|
||||||
|
{
|
||||||
|
_logger.LogDebug($"Received message, subject: {e.Message.Subject}");
|
||||||
|
var voteMessage = MessageHelper.FromData<VoteCastEvent>(e.Message.Data);
|
||||||
|
_logger.LogInformation($"Processing vote for '{voteMessage.Vote}' by '{voteMessage.VoterId}'");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_data.Set(voteMessage.VoterId, voteMessage.Vote);
|
||||||
|
_logger.LogDebug($"Succesffuly processed vote by '{voteMessage.VoterId}'");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError($"Vote processing FAILED for '{voteMessage.VoterId}', exception: {ex}");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
worker/dotnet/Worker/appsettings.json
Normal file
14
worker/dotnet/Worker/appsettings.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"MessageQueue": {
|
||||||
|
"Url": "nats://message-queue:4222"
|
||||||
|
},
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"VoteData": "Server=mysql;Port=4000;Database=votes;User=root;SslMode=None"
|
||||||
|
},
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user