diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a0ab064
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,13 @@
+# Tntp.Solution exclusions - .vs, packages, bin, and obj directories; .dbmdl, .jfm, and .user files
+Tntp.Solution/.vs/
+Tntp.Solution/packages/
+Tntp.Solution/Tntp.Database/bin/
+Tntp.Solution/Tntp.Database/obj/
+Tntp.Solution/Tntp.Database/Tntp.Database.dbmdl
+Tntp.Solution/Tntp.Database/Tntp.Database.jfm
+Tntp.Solution/Tntp.Database/Tntp.Database.sqlproj.user
+Tntp.Solution/Tntp.WebApplication/bin/
+Tntp.Solution/Tntp.WebApplication/obj/
+Tntp.Solution/Tntp.WebApplication/Tntp.WebApplication.csproj.user
+Tntp.Solution/Tntp.WebApplication.Tests/bin/
+Tntp.Solution/Tntp.WebApplication.Tests/obj/
\ No newline at end of file
diff --git a/Tntp.Solution/Tntp.Database/Comments.sql b/Tntp.Solution/Tntp.Database/Comments.sql
new file mode 100644
index 0000000..1702b69
--- /dev/null
+++ b/Tntp.Solution/Tntp.Database/Comments.sql
@@ -0,0 +1,11 @@
+CREATE TABLE [dbo].[Comments]
+(
+ [Id] INT NOT NULL PRIMARY KEY IDENTITY,
+ [Username] NVARCHAR(15) NOT NULL,
+ [Content] NVARCHAR(140) NOT NULL,
+ [CreationTimestamp] DATETIME2 NOT NULL DEFAULT sysdatetime()
+)
+
+GO
+
+CREATE INDEX [IX_Comments_CreationTimestamp] ON [dbo].[Comments] ([CreationTimestamp] DESC)
diff --git a/Tntp.Solution/Tntp.Database/Tntp.Database.sqlproj b/Tntp.Solution/Tntp.Database/Tntp.Database.sqlproj
new file mode 100644
index 0000000..253910e
--- /dev/null
+++ b/Tntp.Solution/Tntp.Database/Tntp.Database.sqlproj
@@ -0,0 +1,64 @@
+
+
+
+
+ Debug
+ AnyCPU
+ Tntp.Database
+ 2.0
+ 4.1
+ {ee3ce98d-6e0d-4b23-b524-763b5b65ba47}
+ Microsoft.Data.Tools.Schema.Sql.Sql130DatabaseSchemaProvider
+ Database
+
+
+ Tntp.Database
+ Tntp.Database
+ 1033, CI
+ BySchemaAndSchemaType
+ True
+ v4.6.1
+ CS
+ Properties
+ False
+ True
+ True
+
+
+ bin\Release\
+ $(MSBuildProjectName).sql
+ False
+ pdbonly
+ true
+ false
+ true
+ prompt
+ 4
+
+
+ bin\Debug\
+ $(MSBuildProjectName).sql
+ false
+ true
+ full
+ false
+ true
+ true
+ prompt
+ 4
+
+
+ 11.0
+
+ True
+ 11.0
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tntp.Solution/Tntp.Solution.sln b/Tntp.Solution/Tntp.Solution.sln
new file mode 100644
index 0000000..887ea11
--- /dev/null
+++ b/Tntp.Solution/Tntp.Solution.sln
@@ -0,0 +1,36 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 14
+VisualStudioVersion = 14.0.25420.1
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{00D1A9C2-B5F0-4AF3-8072-F6C62B433612}") = "Tntp.Database", "Tntp.Database\Tntp.Database.sqlproj", "{EE3CE98D-6E0D-4B23-B524-763B5B65BA47}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tntp.WebApplication", "Tntp.WebApplication\Tntp.WebApplication.csproj", "{A5487DA2-BDEB-4DF3-99BB-E983BCBB66CA}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tntp.WebApplication.Tests", "Tntp.WebApplication.Tests\Tntp.WebApplication.Tests.csproj", "{42453A53-3307-432C-8ACA-04940576906B}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {EE3CE98D-6E0D-4B23-B524-763B5B65BA47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EE3CE98D-6E0D-4B23-B524-763B5B65BA47}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EE3CE98D-6E0D-4B23-B524-763B5B65BA47}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
+ {EE3CE98D-6E0D-4B23-B524-763B5B65BA47}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EE3CE98D-6E0D-4B23-B524-763B5B65BA47}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EE3CE98D-6E0D-4B23-B524-763B5B65BA47}.Release|Any CPU.Deploy.0 = Release|Any CPU
+ {A5487DA2-BDEB-4DF3-99BB-E983BCBB66CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A5487DA2-BDEB-4DF3-99BB-E983BCBB66CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A5487DA2-BDEB-4DF3-99BB-E983BCBB66CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A5487DA2-BDEB-4DF3-99BB-E983BCBB66CA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {42453A53-3307-432C-8ACA-04940576906B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {42453A53-3307-432C-8ACA-04940576906B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {42453A53-3307-432C-8ACA-04940576906B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {42453A53-3307-432C-8ACA-04940576906B}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
diff --git a/Tntp.Solution/Tntp.WebApplication.Tests/CommentsControllerTests.cs b/Tntp.Solution/Tntp.WebApplication.Tests/CommentsControllerTests.cs
new file mode 100644
index 0000000..1c64c03
--- /dev/null
+++ b/Tntp.Solution/Tntp.WebApplication.Tests/CommentsControllerTests.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Web.Http;
+using System.Web.Http.Results;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Tntp.WebApplication.Controllers;
+using Tntp.WebApplication.Models;
+
+namespace Tntp.WebApplication.Tests
+{
+ [TestClass]
+ public class CommentsControllerTests
+ {
+ private readonly IRepository _repository = new MockRepository();
+ private readonly CommentsController _controller;
+
+ public CommentsControllerTests()
+ {
+ _repository.Comments.Add(new Comment { Username = "Eddard", Content = "Stark", CreationTimestamp = DateTime.Now.AddDays(-1) });
+ _repository.Comments.Add(new Comment { Username = "Jon", Content = "Snow", CreationTimestamp = DateTime.Now });
+
+ _controller = new CommentsController(_repository, new MockWebSocketHub());
+ _controller.Request = new HttpRequestMessage();
+ _controller.Configuration = new HttpConfiguration();
+ }
+
+ [TestMethod]
+ public void GetCommentsTest()
+ {
+ var results = _controller.GetComments();
+ Assert.AreEqual("Jon", results.ElementAt(0).Username);
+ Assert.AreEqual("Snow", results.ElementAt(0).Content);
+ Assert.AreEqual("Eddard", results.ElementAt(1).Username);
+ Assert.AreEqual("Stark", results.ElementAt(1).Content);
+ }
+
+ [TestMethod]
+ public void AddCommentTest_NullUsername()
+ {
+ var result = _controller.AddComment(new Comment { Content = "Seaworth" }) as BadRequestErrorMessageResult;
+ Assert.IsNotNull(result);
+ Assert.AreEqual("A username is required.", result.Message);
+ }
+
+ [TestMethod]
+ public void AddCommentTest_EmptyUsername()
+ {
+ var result = _controller.AddComment(new Comment { Username = "", Content = "Seaworth" }) as BadRequestErrorMessageResult;
+ Assert.IsNotNull(result);
+ Assert.AreEqual("A username is required.", result.Message);
+ }
+
+ [TestMethod]
+ public void AddCommentTest_TooLongUsername()
+ {
+ var result = _controller.AddComment(new Comment { Username = "Stannnnnnnnnnnis", Content = "Baratheon" }) as BadRequestErrorMessageResult;
+ Assert.IsNotNull(result);
+ Assert.AreEqual("A username must not exceed 15 characters.", result.Message);
+ }
+
+ [TestMethod]
+ public void AddCommentTest_NullContent()
+ {
+ var result = _controller.AddComment(new Comment { Username = "Davos" }) as BadRequestErrorMessageResult;
+ Assert.IsNotNull(result);
+ Assert.AreEqual("A comment is required.", result.Message);
+ }
+
+ [TestMethod]
+ public void AddCommentTest_EmptyContent()
+ {
+ var result = _controller.AddComment(new Comment { Username = "Davos", Content = "" }) as BadRequestErrorMessageResult;
+ Assert.IsNotNull(result);
+ Assert.AreEqual("A comment is required.", result.Message);
+ }
+
+ [TestMethod]
+ public void AddCommentTest_TooLongContent()
+ {
+ var result = _controller.AddComment(new Comment
+ {
+ Username = "Hodor",
+ Content = "Hodor hodor hodor hodor hodor hodor hodor hodor hodor hodor hodor hodor hodor hodor hodor hodor hodor hodor hodor hodor hodor hodor hodor hodor hodor"
+ }) as BadRequestErrorMessageResult;
+
+ Assert.IsNotNull(result);
+ Assert.AreEqual("A comment must not exceed 140 characters.", result.Message);
+ }
+
+ [TestMethod]
+ public void AddCommentTest_Successful()
+ {
+ var result = _controller.AddComment(new Comment { Username = "Davos", Content = "Seaworth" }) as StatusCodeResult;
+ Assert.IsNotNull(result);
+ Assert.AreEqual(HttpStatusCode.OK, result.StatusCode);
+ Assert.AreEqual(3, _repository.Comments.Count());
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tntp.Solution/Tntp.WebApplication.Tests/MockDbSet.cs b/Tntp.Solution/Tntp.WebApplication.Tests/MockDbSet.cs
new file mode 100644
index 0000000..10aa658
--- /dev/null
+++ b/Tntp.Solution/Tntp.WebApplication.Tests/MockDbSet.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Data.Entity;
+using System.Linq;
+using System.Linq.Expressions;
+
+namespace Tntp.WebApplication.Tests
+{
+ public class MockDbSet : IDbSet where T : class
+ {
+ private readonly List _entities = new List();
+
+ public Type ElementType
+ {
+ get { return typeof(T); }
+ }
+
+ public Expression Expression
+ {
+ get { return _entities.AsQueryable().Expression; }
+ }
+
+ public ObservableCollection Local
+ {
+ get { throw new NotImplementedException(); }
+ }
+
+ public IQueryProvider Provider
+ {
+ get { return _entities.AsQueryable().Provider; }
+ }
+
+ public T Add(T entity)
+ {
+ _entities.Add(entity);
+ return entity;
+ }
+
+ public T Attach(T entity)
+ {
+ throw new NotImplementedException();
+ }
+
+ public T Create()
+ {
+ throw new NotImplementedException();
+ }
+
+ public TDerivedEntity Create() where TDerivedEntity : class, T
+ {
+ throw new NotImplementedException();
+ }
+
+ public T Find(params object[] keyValues)
+ {
+ throw new NotImplementedException();
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return _entities.GetEnumerator();
+ }
+
+ public T Remove(T entity)
+ {
+ throw new NotImplementedException();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return _entities.GetEnumerator();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tntp.Solution/Tntp.WebApplication.Tests/MockRepository.cs b/Tntp.Solution/Tntp.WebApplication.Tests/MockRepository.cs
new file mode 100644
index 0000000..6156bc0
--- /dev/null
+++ b/Tntp.Solution/Tntp.WebApplication.Tests/MockRepository.cs
@@ -0,0 +1,19 @@
+using System.Data.Entity;
+using Tntp.WebApplication.Models;
+
+namespace Tntp.WebApplication.Tests
+{
+ public class MockRepository : IRepository
+ {
+ private readonly IDbSet _comments = new MockDbSet();
+
+ public IDbSet Comments
+ {
+ get { return _comments; }
+ }
+
+ public void SaveChanges() { }
+
+ public void Dispose() { }
+ }
+}
\ No newline at end of file
diff --git a/Tntp.Solution/Tntp.WebApplication.Tests/MockWebSocketHandler.cs b/Tntp.Solution/Tntp.WebApplication.Tests/MockWebSocketHandler.cs
new file mode 100644
index 0000000..182b38a
--- /dev/null
+++ b/Tntp.Solution/Tntp.WebApplication.Tests/MockWebSocketHandler.cs
@@ -0,0 +1,11 @@
+using Microsoft.Web.WebSockets;
+
+namespace Tntp.WebApplication.Tests
+{
+ public class MockWebSocketHandler : WebSocketHandler
+ {
+ public override void OnOpen() { }
+ public override void OnMessage(string message) { }
+ public override void OnClose() { }
+ }
+}
\ No newline at end of file
diff --git a/Tntp.Solution/Tntp.WebApplication.Tests/MockWebSocketHub.cs b/Tntp.Solution/Tntp.WebApplication.Tests/MockWebSocketHub.cs
new file mode 100644
index 0000000..ca3bbd1
--- /dev/null
+++ b/Tntp.Solution/Tntp.WebApplication.Tests/MockWebSocketHub.cs
@@ -0,0 +1,16 @@
+using Microsoft.Web.WebSockets;
+using Tntp.WebApplication.Controllers;
+
+namespace Tntp.WebApplication.Tests
+{
+ public class MockWebSocketHub : IWebSocketHub
+ {
+ public void AddHandler(WebSocketHandler handler) { }
+
+ public void Broadcast(string message) { }
+
+ public void CreateHandler() { }
+
+ public void RemoveHandler(WebSocketHandler handler) { }
+ }
+}
\ No newline at end of file
diff --git a/Tntp.Solution/Tntp.WebApplication.Tests/Properties/AssemblyInfo.cs b/Tntp.Solution/Tntp.WebApplication.Tests/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..b2e3700
--- /dev/null
+++ b/Tntp.Solution/Tntp.WebApplication.Tests/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("Tntp.WebApplication.Tests")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("Tntp.WebApplication.Tests")]
+[assembly: AssemblyCopyright("Copyright © 2018")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("42453a53-3307-432c-8aca-04940576906b")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/Tntp.Solution/Tntp.WebApplication.Tests/Tntp.WebApplication.Tests.csproj b/Tntp.Solution/Tntp.WebApplication.Tests/Tntp.WebApplication.Tests.csproj
new file mode 100644
index 0000000..c83b30b
--- /dev/null
+++ b/Tntp.Solution/Tntp.WebApplication.Tests/Tntp.WebApplication.Tests.csproj
@@ -0,0 +1,93 @@
+
+
+
+ Debug
+ AnyCPU
+ {42453A53-3307-432C-8ACA-04940576906B}
+ Library
+ Properties
+ Tntp.WebApplication.Tests
+ Tntp.WebApplication.Tests
+ v4.6.1
+ 512
+ {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
+ False
+ UnitTest
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+ ..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.dll
+ True
+
+
+ ..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll
+ True
+
+
+
+
+ ..\packages\Microsoft.WebSockets.0.2.3.1\lib\net45\Microsoft.WebSockets.dll
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+ ..\packages\Microsoft.AspNet.WebApi.Core.5.2.6\lib\net45\System.Web.Http.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {A5487DA2-BDEB-4DF3-99BB-E983BCBB66CA}
+ Tntp.WebApplication
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tntp.Solution/Tntp.WebApplication.Tests/app.config b/Tntp.Solution/Tntp.WebApplication.Tests/app.config
new file mode 100644
index 0000000..aec0c51
--- /dev/null
+++ b/Tntp.Solution/Tntp.WebApplication.Tests/app.config
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tntp.Solution/Tntp.WebApplication.Tests/packages.config b/Tntp.Solution/Tntp.WebApplication.Tests/packages.config
new file mode 100644
index 0000000..c462916
--- /dev/null
+++ b/Tntp.Solution/Tntp.WebApplication.Tests/packages.config
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/Tntp.Solution/Tntp.WebApplication/App_Start/WebApiConfig.cs b/Tntp.Solution/Tntp.WebApplication/App_Start/WebApiConfig.cs
new file mode 100644
index 0000000..c215bf6
--- /dev/null
+++ b/Tntp.Solution/Tntp.WebApplication/App_Start/WebApiConfig.cs
@@ -0,0 +1,33 @@
+using SimpleInjector;
+using SimpleInjector.Integration.WebApi;
+using SimpleInjector.Lifestyles;
+using System.Net.Http.Formatting;
+using System.Web.Http;
+using Tntp.WebApplication.Controllers;
+using Tntp.WebApplication.Models;
+
+namespace Tntp.WebApplication
+{
+ public static class WebApiConfig
+ {
+ public static void Register(HttpConfiguration configuration)
+ {
+ // Web API configuration and services
+ var container = new Container();
+ container.Register(new AsyncScopedLifestyle());
+ container.RegisterSingleton();
+ container.RegisterWebApiControllers(configuration);
+ container.Verify();
+
+ // We want JSON to be the only content/media type.
+ // In the future, we might not want to go through content negotiation at all - see https://www.strathweb.com/2013/06/supporting-only-json-in-asp-net-web-api-the-right-way/
+ // for implementation details.
+ configuration.Formatters.Clear();
+ configuration.Formatters.Add(new JsonMediaTypeFormatter());
+
+ // Web API routes
+ configuration.DependencyResolver = new SimpleInjectorWebApiDependencyResolver(container);
+ configuration.MapHttpAttributeRoutes();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tntp.Solution/Tntp.WebApplication/Client/application.js b/Tntp.Solution/Tntp.WebApplication/Client/application.js
new file mode 100644
index 0000000..f48d308
--- /dev/null
+++ b/Tntp.Solution/Tntp.WebApplication/Client/application.js
@@ -0,0 +1,37 @@
+angular.module("CommentsClient", []).controller("CommentsController", function($scope, $http) {
+ const BadRequestStatusCode = 400;
+ const CommentCharacterLimit = 140;
+
+ $http.get("http://localhost:61781/api/comments").then(function(response) {
+ $scope.commentList = response.data;
+
+ $scope.webSocket = new WebSocket("ws://localhost:61781/api/comments/feed");
+
+ $scope.webSocket.onmessage = function(event) {
+ $scope.commentList.unshift(JSON.parse(event.data));
+ }
+ });
+
+ $scope.commentLength = CommentCharacterLimit;
+
+ $scope.updateCommentLength = function() {
+ $scope.commentLength = CommentCharacterLimit - $scope.comment.length;
+
+ if($scope.commentLength < 0) {
+ $scope.commentLength = 0;
+ }
+ }
+
+ $scope.postComment = function() {
+ $http.post("http://localhost:61781/api/comments", { Username: $scope.username, Content: $scope.comment }).then(function(response) {
+ $scope.username = "";
+ $scope.comment = "";
+ $scope.error = "";
+ $scope.commentLength = CommentCharacterLimit;
+ }).catch(function(response) {
+ if(response.status == BadRequestStatusCode) {
+ $scope.error = response.data.Message;
+ }
+ });
+ }
+});
\ No newline at end of file
diff --git a/Tntp.Solution/Tntp.WebApplication/Client/index.html b/Tntp.Solution/Tntp.WebApplication/Client/index.html
new file mode 100644
index 0000000..e0afdea
--- /dev/null
+++ b/Tntp.Solution/Tntp.WebApplication/Client/index.html
@@ -0,0 +1,55 @@
+
+
+
+ TNTP Solution
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tntp.Solution/Tntp.WebApplication/Client/styles.css b/Tntp.Solution/Tntp.WebApplication/Client/styles.css
new file mode 100644
index 0000000..be91d9d
--- /dev/null
+++ b/Tntp.Solution/Tntp.WebApplication/Client/styles.css
@@ -0,0 +1,55 @@
+table {
+ width: 25%;
+ margin: auto;
+ font-size: large;
+}
+
+input, textarea, button {
+ margin-bottom: 0.5em;
+}
+
+input, textarea {
+ width: 100%;
+}
+
+button {
+ float: right;
+ margin-bottom: 1em;
+}
+
+.header-div {
+ margin: auto;
+ text-align: center;
+ font-size: 3em;
+ margin-bottom: 0.5em;
+}
+
+.content-div {
+ word-break: break-all;
+}
+
+.timestamp-div {
+ float: right;
+ margin-top: 0.5em;
+ font-style: italic;
+ font-size: small;
+}
+
+.error-span {
+ font-size: smaller;
+ color: red;
+}
+
+.comment-length-span {
+ font-style: italic;
+}
+
+.comments-table {
+ border-spacing: 0px 10px;
+ border-collapse: separate;
+}
+
+.comments-td {
+ border: 1px solid gray;
+ padding: 0.5em;
+}
\ No newline at end of file
diff --git a/Tntp.Solution/Tntp.WebApplication/Controllers/CommentsController.cs b/Tntp.Solution/Tntp.WebApplication/Controllers/CommentsController.cs
new file mode 100644
index 0000000..d4c31e4
--- /dev/null
+++ b/Tntp.Solution/Tntp.WebApplication/Controllers/CommentsController.cs
@@ -0,0 +1,69 @@
+using Newtonsoft.Json;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Web.Http;
+using Tntp.WebApplication.Models;
+
+namespace Tntp.WebApplication.Controllers
+{
+ ///
+ /// The CommentsController class serves as a web-API controller for posting and viewing comments.
+ ///
+ [RoutePrefix("api/comments")]
+ public class CommentsController : ApiController
+ {
+ private const int UsernameMaxLength = 15;
+ private const int ContentMaxLength = 140;
+
+ private readonly IRepository _repository;
+ private readonly IWebSocketHub _webSocketHub;
+
+ public CommentsController(IRepository repository, IWebSocketHub webSocketHub)
+ {
+ _repository = repository;
+ _webSocketHub = webSocketHub;
+ }
+
+ [Route, HttpGet]
+ public IEnumerable GetComments()
+ {
+ return _repository.Comments.OrderByDescending(c => c.CreationTimestamp);
+ }
+
+ [Route, HttpPost]
+ public IHttpActionResult AddComment(Comment comment)
+ {
+ if((comment.Username?.Length ?? 0) == 0)
+ {
+ return BadRequest("A username is required.");
+ }
+ else if(comment.Username.Length > UsernameMaxLength)
+ {
+ return BadRequest(string.Format("A username must not exceed {0} characters.", UsernameMaxLength));
+ }
+ else if((comment.Content?.Length ?? 0) == 0)
+ {
+ return BadRequest("A comment is required.");
+ }
+ else if(comment.Content.Length > ContentMaxLength)
+ {
+ return BadRequest(string.Format("A comment must not exceed {0} characters.", ContentMaxLength));
+ }
+
+ _repository.Comments.Add(comment);
+ _repository.SaveChanges();
+ _webSocketHub.Broadcast(JsonConvert.SerializeObject(comment));
+
+ return StatusCode(HttpStatusCode.OK);
+ }
+
+ [Route("feed"), HttpGet]
+ public HttpResponseMessage GetFeed()
+ {
+ _webSocketHub.CreateHandler();
+ return Request.CreateResponse(HttpStatusCode.SwitchingProtocols);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tntp.Solution/Tntp.WebApplication/Controllers/CommentsWebSocketHandler.cs b/Tntp.Solution/Tntp.WebApplication/Controllers/CommentsWebSocketHandler.cs
new file mode 100644
index 0000000..eb42a2b
--- /dev/null
+++ b/Tntp.Solution/Tntp.WebApplication/Controllers/CommentsWebSocketHandler.cs
@@ -0,0 +1,32 @@
+using Microsoft.Web.WebSockets;
+
+namespace Tntp.WebApplication.Controllers
+{
+ ///
+ /// The CommentsWebSocketHandler class is a subclass of WebSocketHandler used to broadcast new comments to connected users.
+ ///
+ public class CommentsWebSocketHandler : WebSocketHandler
+ {
+ private readonly IWebSocketHub _hub;
+
+ public CommentsWebSocketHandler(IWebSocketHub hub)
+ {
+ _hub = hub;
+ }
+
+ public override void OnOpen()
+ {
+ _hub.AddHandler(this);
+ }
+
+ public override void OnClose()
+ {
+ _hub.RemoveHandler(this);
+ }
+
+ public override void OnMessage(string message)
+ {
+ _hub.Broadcast(message);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tntp.Solution/Tntp.WebApplication/Controllers/CommentsWebSocketHub.cs b/Tntp.Solution/Tntp.WebApplication/Controllers/CommentsWebSocketHub.cs
new file mode 100644
index 0000000..f1a6616
--- /dev/null
+++ b/Tntp.Solution/Tntp.WebApplication/Controllers/CommentsWebSocketHub.cs
@@ -0,0 +1,33 @@
+using Microsoft.Web.WebSockets;
+using System.Web;
+
+namespace Tntp.WebApplication.Controllers
+{
+ ///
+ /// The CommentsWebSocketHub class is a simple implementation of IWebSocketHub that wraps a WebSocketCollection.
+ ///
+ public class CommentsWebSocketHub : IWebSocketHub
+ {
+ private readonly WebSocketCollection _webSockets = new WebSocketCollection();
+
+ public void AddHandler(WebSocketHandler handler)
+ {
+ _webSockets.Add(handler);
+ }
+
+ public void Broadcast(string message)
+ {
+ _webSockets.Broadcast(message);
+ }
+
+ public void CreateHandler()
+ {
+ HttpContext.Current.AcceptWebSocketRequest(new CommentsWebSocketHandler(this));
+ }
+
+ public void RemoveHandler(WebSocketHandler handler)
+ {
+ _webSockets.Remove(handler);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tntp.Solution/Tntp.WebApplication/Controllers/IWebSocketHub.cs b/Tntp.Solution/Tntp.WebApplication/Controllers/IWebSocketHub.cs
new file mode 100644
index 0000000..7f87554
--- /dev/null
+++ b/Tntp.Solution/Tntp.WebApplication/Controllers/IWebSocketHub.cs
@@ -0,0 +1,15 @@
+using Microsoft.Web.WebSockets;
+
+namespace Tntp.WebApplication.Controllers
+{
+ ///
+ /// The IWebSocketHub interface is a light abstraction for making it easier to use the Microsoft.WebSockets API with dependency injection.
+ ///
+ public interface IWebSocketHub
+ {
+ void CreateHandler();
+ void AddHandler(WebSocketHandler handler);
+ void RemoveHandler(WebSocketHandler handler);
+ void Broadcast(string message);
+ }
+}
\ No newline at end of file
diff --git a/Tntp.Solution/Tntp.WebApplication/Global.asax b/Tntp.Solution/Tntp.WebApplication/Global.asax
new file mode 100644
index 0000000..a68fcf8
--- /dev/null
+++ b/Tntp.Solution/Tntp.WebApplication/Global.asax
@@ -0,0 +1 @@
+<%@ Application Codebehind="Global.asax.cs" Inherits="Tntp.WebApplication.WebApiApplication" Language="C#" %>
diff --git a/Tntp.Solution/Tntp.WebApplication/Global.asax.cs b/Tntp.Solution/Tntp.WebApplication/Global.asax.cs
new file mode 100644
index 0000000..d55937f
--- /dev/null
+++ b/Tntp.Solution/Tntp.WebApplication/Global.asax.cs
@@ -0,0 +1,12 @@
+using System.Web.Http;
+
+namespace Tntp.WebApplication
+{
+ public class WebApiApplication : System.Web.HttpApplication
+ {
+ protected void Application_Start()
+ {
+ GlobalConfiguration.Configure(WebApiConfig.Register);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tntp.Solution/Tntp.WebApplication/Models/Comment.cs b/Tntp.Solution/Tntp.WebApplication/Models/Comment.cs
new file mode 100644
index 0000000..f9ffe99
--- /dev/null
+++ b/Tntp.Solution/Tntp.WebApplication/Models/Comment.cs
@@ -0,0 +1,20 @@
+using System;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Tntp.WebApplication.Models
+{
+ ///
+ /// The Comment class is an entity class representing rows in the Comments database table.
+ ///
+ public class Comment
+ {
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; set; }
+
+ public string Username { get; set; }
+ public string Content { get; set; }
+
+ [DatabaseGenerated(DatabaseGeneratedOption.Computed)]
+ public DateTime CreationTimestamp { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Tntp.Solution/Tntp.WebApplication/Models/DatabaseContext.cs b/Tntp.Solution/Tntp.WebApplication/Models/DatabaseContext.cs
new file mode 100644
index 0000000..16fc372
--- /dev/null
+++ b/Tntp.Solution/Tntp.WebApplication/Models/DatabaseContext.cs
@@ -0,0 +1,18 @@
+using System.Data.Entity;
+
+namespace Tntp.WebApplication.Models
+{
+ ///
+ /// The DatabaseContext class is a subclass of DbContext that's aware of the Comments table. It's used by the DatabaseRepository class.
+ ///
+ public class DatabaseContext : DbContext
+ {
+ public DbSet Comments { get; set; }
+
+ public DatabaseContext() : base("name=Tntp")
+ {
+ // Code First with Existing Database - Make sure to not automatically create a new database or update an existing one.
+ Database.SetInitializer(null);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tntp.Solution/Tntp.WebApplication/Models/DatabaseRepository.cs b/Tntp.Solution/Tntp.WebApplication/Models/DatabaseRepository.cs
new file mode 100644
index 0000000..725c905
--- /dev/null
+++ b/Tntp.Solution/Tntp.WebApplication/Models/DatabaseRepository.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Data.Entity;
+
+namespace Tntp.WebApplication.Models
+{
+ ///
+ /// The DatabaseRepository class is an implementation of IRepository that connects to a database.
+ ///
+ public class DatabaseRepository : IRepository
+ {
+ private readonly DatabaseContext _databaseContext = new DatabaseContext();
+
+ private bool _isDisposed;
+
+ public IDbSet Comments
+ {
+ get { return _databaseContext.Comments; }
+ }
+
+ public void SaveChanges()
+ {
+ _databaseContext.SaveChanges();
+ }
+
+ public void Dispose()
+ {
+ if(!_isDisposed)
+ {
+ _databaseContext.Dispose();
+ _isDisposed = true;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tntp.Solution/Tntp.WebApplication/Models/IRepository.cs b/Tntp.Solution/Tntp.WebApplication/Models/IRepository.cs
new file mode 100644
index 0000000..2c6840a
--- /dev/null
+++ b/Tntp.Solution/Tntp.WebApplication/Models/IRepository.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Data.Entity;
+
+namespace Tntp.WebApplication.Models
+{
+ ///
+ /// The IRepository interface is a light abstraction for data access.
+ ///
+ public interface IRepository : IDisposable
+ {
+ IDbSet Comments { get; }
+ void SaveChanges();
+ }
+}
\ No newline at end of file
diff --git a/Tntp.Solution/Tntp.WebApplication/Properties/AssemblyInfo.cs b/Tntp.Solution/Tntp.WebApplication/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..ac84d3e
--- /dev/null
+++ b/Tntp.Solution/Tntp.WebApplication/Properties/AssemblyInfo.cs
@@ -0,0 +1,35 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("Tntp.WebApplication")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("Tntp.WebApplication")]
+[assembly: AssemblyCopyright("Copyright © 2018")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("a5487da2-bdeb-4df3-99bb-e983bcbb66ca")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Revision and Build Numbers
+// by using the '*' as shown below:
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/Tntp.Solution/Tntp.WebApplication/Tntp.WebApplication.csproj b/Tntp.Solution/Tntp.WebApplication/Tntp.WebApplication.csproj
new file mode 100644
index 0000000..f63b6fd
--- /dev/null
+++ b/Tntp.Solution/Tntp.WebApplication/Tntp.WebApplication.csproj
@@ -0,0 +1,182 @@
+
+
+
+
+
+
+ Debug
+ AnyCPU
+
+
+ 2.0
+ {A5487DA2-BDEB-4DF3-99BB-E983BCBB66CA}
+ {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}
+ Library
+ Properties
+ Tntp.WebApplication
+ Tntp.WebApplication
+ v4.6.1
+ true
+
+
+
+
+
+
+
+
+
+ true
+ full
+ false
+ bin\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\
+ TRACE
+ prompt
+ 4
+
+
+
+ ..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.dll
+ True
+
+
+ ..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll
+ True
+
+
+ ..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.2.0.0\lib\net45\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll
+ True
+
+
+
+ ..\packages\Microsoft.WebSockets.0.2.3.1\lib\net45\Microsoft.WebSockets.dll
+ True
+
+
+ ..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll
+ True
+
+
+ ..\packages\SimpleInjector.4.3.0\lib\net45\SimpleInjector.dll
+ True
+
+
+ ..\packages\SimpleInjector.Extensions.ExecutionContextScoping.4.0.0\lib\net45\SimpleInjector.Extensions.ExecutionContextScoping.dll
+ True
+
+
+ ..\packages\SimpleInjector.Integration.WebApi.4.3.0\lib\net45\SimpleInjector.Integration.WebApi.dll
+ True
+
+
+
+ ..\packages\Microsoft.AspNet.WebApi.Client.5.2.6\lib\net45\System.Net.Http.Formatting.dll
+ True
+
+
+
+
+
+
+
+
+
+
+
+ ..\packages\Microsoft.AspNet.WebApi.Core.5.2.6\lib\net45\System.Web.Http.dll
+ True
+
+
+ ..\packages\Microsoft.AspNet.WebApi.WebHost.5.2.6\lib\net45\System.Web.Http.WebHost.dll
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Global.asax
+
+
+
+
+
+
+
+
+
+
+ Web.config
+
+
+ Web.config
+
+
+
+
+
+
+ 10.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+
+
+
+
+
+ True
+ True
+ 61781
+ /
+ http://localhost:61781/
+ False
+ False
+
+
+ False
+
+
+
+
+
+
+ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tntp.Solution/Tntp.WebApplication/Web.Debug.config b/Tntp.Solution/Tntp.WebApplication/Web.Debug.config
new file mode 100644
index 0000000..2e302f9
--- /dev/null
+++ b/Tntp.Solution/Tntp.WebApplication/Web.Debug.config
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tntp.Solution/Tntp.WebApplication/Web.Release.config b/Tntp.Solution/Tntp.WebApplication/Web.Release.config
new file mode 100644
index 0000000..c358444
--- /dev/null
+++ b/Tntp.Solution/Tntp.WebApplication/Web.Release.config
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tntp.Solution/Tntp.WebApplication/Web.config b/Tntp.Solution/Tntp.WebApplication/Web.config
new file mode 100644
index 0000000..d4ac1ce
--- /dev/null
+++ b/Tntp.Solution/Tntp.WebApplication/Web.config
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tntp.Solution/Tntp.WebApplication/packages.config b/Tntp.Solution/Tntp.WebApplication/packages.config
new file mode 100644
index 0000000..178aed3
--- /dev/null
+++ b/Tntp.Solution/Tntp.WebApplication/packages.config
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file