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 + + + + + + + +
+ TNTP Solution +
+ + + + + + + + + + + + + + + + + +
+ Username: + + +
+ +
+ {{error}} + + {{commentLength}} + +
+ + + + +
+ @{{c.Username}} +
+
{{c.Content}}
+ {{c.CreationTimestamp}} +
+ + \ 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