├── .gitignore
├── GameStore.App
├── Controllers
│ ├── AdminController.cs
│ ├── BaseController.cs
│ ├── HomeController.cs
│ ├── OrdersController.cs
│ └── UsersController.cs
├── Data
│ ├── GameStoreDbContext.cs
│ └── Models
│ │ ├── Game.cs
│ │ ├── Order.cs
│ │ └── User.cs
├── GameStore.App.csproj
├── Infrastructure
│ ├── DependencyControllerRouter.cs
│ ├── HtmlHelpers.cs
│ ├── HttpSessionExtensions.cs
│ ├── Mapping
│ │ ├── AutoMapperConfiguration.cs
│ │ ├── IHaveCustomMapping.cs
│ │ └── IMapFrom.cs
│ ├── StringExtensions.cs
│ └── Validation
│ │ ├── Games
│ │ ├── DescriptionAttribute.cs
│ │ ├── ThumbnailUrlAttribute.cs
│ │ ├── TitleAttribute.cs
│ │ └── VideoIdAttribute.cs
│ │ ├── RequiredAttribute.cs
│ │ └── Users
│ │ ├── EmailAttribute.cs
│ │ └── PasswordAttribute.cs
├── Launcher.cs
├── Migrations
│ ├── 20171024105903_UsersTable.Designer.cs
│ ├── 20171024105903_UsersTable.cs
│ ├── 20171024121234_UsersIsAdminColumn.Designer.cs
│ ├── 20171024121234_UsersIsAdminColumn.cs
│ ├── 20171024124624_GamesTable.Designer.cs
│ ├── 20171024124624_GamesTable.cs
│ └── GameStoreDbContextModelSnapshot.cs
├── Models
│ ├── Games
│ │ ├── GameAdminModel.cs
│ │ └── GameListingAdminModel.cs
│ ├── Home
│ │ └── GameListingHomeModel.cs
│ ├── Orders
│ │ ├── GameListingOrdersModel.cs
│ │ └── ShoppingCart.cs
│ └── Users
│ │ ├── LoginModel.cs
│ │ └── RegisterModel.cs
├── Resources
│ ├── css
│ │ └── bootstrap.min.css
│ └── js
│ │ ├── bootstrap.min.js
│ │ └── jquery-3.1.1.min.js
├── Services
│ ├── Contracts
│ │ ├── IGameService.cs
│ │ ├── IOrderService.cs
│ │ └── IUserService.cs
│ ├── GameService.cs
│ ├── OrderService.cs
│ └── UserService.cs
└── Views
│ ├── Admin
│ ├── Add.html
│ ├── All.html
│ ├── Delete.html
│ └── Edit.html
│ ├── Home
│ └── Index.html
│ ├── Layout.html
│ ├── Orders
│ └── Cart.html
│ └── Users
│ ├── Login.html
│ └── Register.html
├── LICENSE
├── README.md
├── SimpleMvc.Framework
├── ActionResults
│ ├── NotFoundResult.cs
│ ├── RedirectResult.cs
│ └── ViewResult.cs
├── Attributes
│ ├── Methods
│ │ ├── HttpGetAttribute.cs
│ │ ├── HttpMethodAttribute.cs
│ │ └── HttpPostAttribute.cs
│ └── Validation
│ │ ├── NumberRangeAttribute.cs
│ │ ├── PropertyValidationAttribute.cs
│ │ └── RegexAttribute.cs
├── Contracts
│ ├── IActionResult.cs
│ ├── IRedirectable.cs
│ ├── IRenderable.cs
│ └── IViewable.cs
├── Controllers
│ └── Controller.cs
├── Errors
│ └── Error.html
├── Helpers
│ ├── ControllerHelpers.cs
│ └── StringExtensions.cs
├── Models
│ └── ViewModel.cs
├── MvcContext.cs
├── MvcEngine.cs
├── Routers
│ ├── ControllerRouter.cs
│ └── ResourceRouter.cs
├── Security
│ └── Authentication.cs
├── SimpleMvc.Framework.csproj
└── ViewEngine
│ └── View.cs
├── SimpleMvc.sln
└── WebServer
├── Common
├── CoreValidator.cs
├── InternalServerErrorView.cs
└── NotFoundView.cs
├── ConnectionHandler.cs
├── Contracts
├── IHandleable.cs
├── IRunnable.cs
└── IView.cs
├── Enums
├── HttpRequestMethod.cs
└── HttpStatusCode.cs
├── Exceptions
├── BadRequestException.cs
└── InvalidResponseException.cs
├── Http
├── Contracts
│ ├── IHttpCookieCollection.cs
│ ├── IHttpHeaderCollection.cs
│ ├── IHttpRequest.cs
│ ├── IHttpResponse.cs
│ └── IHttpSession.cs
├── HttpCookie.cs
├── HttpCookieCollection.cs
├── HttpHeader.cs
├── HttpHeaderCollection.cs
├── HttpRequest.cs
├── HttpSession.cs
├── Response
│ ├── BadRequestResponse.cs
│ ├── ContentResponse.cs
│ ├── FileResponse.cs
│ ├── HttpResponse.cs
│ ├── InternalServerErrorResponse.cs
│ ├── NotFoundResponse.cs
│ └── RedirectResponse.cs
└── SessionStore.cs
├── WebServer.cs
└── WebServer.csproj
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.suo
8 | *.user
9 | *.userosscache
10 | *.sln.docstates
11 |
12 | # User-specific files (MonoDevelop/Xamarin Studio)
13 | *.userprefs
14 |
15 | # Build results
16 | [Dd]ebug/
17 | [Dd]ebugPublic/
18 | [Rr]elease/
19 | [Rr]eleases/
20 | x64/
21 | x86/
22 | bld/
23 | [Bb]in/
24 | [Oo]bj/
25 | [Ll]og/
26 |
27 | # Visual Studio 2015 cache/options directory
28 | .vs/
29 | # Uncomment if you have tasks that create the project's static files in wwwroot
30 | #wwwroot/
31 |
32 | # MSTest test Results
33 | [Tt]est[Rr]esult*/
34 | [Bb]uild[Ll]og.*
35 |
36 | # NUNIT
37 | *.VisualState.xml
38 | TestResult.xml
39 |
40 | # Build Results of an ATL Project
41 | [Dd]ebugPS/
42 | [Rr]eleasePS/
43 | dlldata.c
44 |
45 | # .NET Core
46 | project.lock.json
47 | project.fragment.lock.json
48 | artifacts/
49 | **/Properties/launchSettings.json
50 |
51 | *_i.c
52 | *_p.c
53 | *_i.h
54 | *.ilk
55 | *.meta
56 | *.obj
57 | *.pch
58 | *.pdb
59 | *.pgc
60 | *.pgd
61 | *.rsp
62 | *.sbr
63 | *.tlb
64 | *.tli
65 | *.tlh
66 | *.tmp
67 | *.tmp_proj
68 | *.log
69 | *.vspscc
70 | *.vssscc
71 | .builds
72 | *.pidb
73 | *.svclog
74 | *.scc
75 |
76 | # Chutzpah Test files
77 | _Chutzpah*
78 |
79 | # Visual C++ cache files
80 | ipch/
81 | *.aps
82 | *.ncb
83 | *.opendb
84 | *.opensdf
85 | *.sdf
86 | *.cachefile
87 | *.VC.db
88 | *.VC.VC.opendb
89 |
90 | # Visual Studio profiler
91 | *.psess
92 | *.vsp
93 | *.vspx
94 | *.sap
95 |
96 | # TFS 2012 Local Workspace
97 | $tf/
98 |
99 | # Guidance Automation Toolkit
100 | *.gpState
101 |
102 | # ReSharper is a .NET coding add-in
103 | _ReSharper*/
104 | *.[Rr]e[Ss]harper
105 | *.DotSettings.user
106 |
107 | # JustCode is a .NET coding add-in
108 | .JustCode
109 |
110 | # TeamCity is a build add-in
111 | _TeamCity*
112 |
113 | # DotCover is a Code Coverage Tool
114 | *.dotCover
115 |
116 | # Visual Studio code coverage results
117 | *.coverage
118 | *.coveragexml
119 |
120 | # NCrunch
121 | _NCrunch_*
122 | .*crunch*.local.xml
123 | nCrunchTemp_*
124 |
125 | # MightyMoose
126 | *.mm.*
127 | AutoTest.Net/
128 |
129 | # Web workbench (sass)
130 | .sass-cache/
131 |
132 | # Installshield output folder
133 | [Ee]xpress/
134 |
135 | # DocProject is a documentation generator add-in
136 | DocProject/buildhelp/
137 | DocProject/Help/*.HxT
138 | DocProject/Help/*.HxC
139 | DocProject/Help/*.hhc
140 | DocProject/Help/*.hhk
141 | DocProject/Help/*.hhp
142 | DocProject/Help/Html2
143 | DocProject/Help/html
144 |
145 | # Click-Once directory
146 | publish/
147 |
148 | # Publish Web Output
149 | *.[Pp]ublish.xml
150 | *.azurePubxml
151 | # TODO: Comment the next line if you want to checkin your web deploy settings
152 | # but database connection strings (with potential passwords) will be unencrypted
153 | *.pubxml
154 | *.publishproj
155 |
156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
157 | # checkin your Azure Web App publish settings, but sensitive information contained
158 | # in these scripts will be unencrypted
159 | PublishScripts/
160 |
161 | # NuGet Packages
162 | *.nupkg
163 | # The packages folder can be ignored because of Package Restore
164 | **/packages/*
165 | # except build/, which is used as an MSBuild target.
166 | !**/packages/build/
167 | # Uncomment if necessary however generally it will be regenerated when needed
168 | #!**/packages/repositories.config
169 | # NuGet v3's project.json files produces more ignorable files
170 | *.nuget.props
171 | *.nuget.targets
172 |
173 | # Microsoft Azure Build Output
174 | csx/
175 | *.build.csdef
176 |
177 | # Microsoft Azure Emulator
178 | ecf/
179 | rcf/
180 |
181 | # Windows Store app package directories and files
182 | AppPackages/
183 | BundleArtifacts/
184 | Package.StoreAssociation.xml
185 | _pkginfo.txt
186 |
187 | # Visual Studio cache files
188 | # files ending in .cache can be ignored
189 | *.[Cc]ache
190 | # but keep track of directories ending in .cache
191 | !*.[Cc]ache/
192 |
193 | # Others
194 | ClientBin/
195 | ~$*
196 | *~
197 | *.dbmdl
198 | *.dbproj.schemaview
199 | *.jfm
200 | *.pfx
201 | *.publishsettings
202 | orleans.codegen.cs
203 |
204 | # Since there are multiple workflows, uncomment next line to ignore bower_components
205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
206 | #bower_components/
207 |
208 | # RIA/Silverlight projects
209 | Generated_Code/
210 |
211 | # Backup & report files from converting an old project file
212 | # to a newer Visual Studio version. Backup files are not needed,
213 | # because we have git ;-)
214 | _UpgradeReport_Files/
215 | Backup*/
216 | UpgradeLog*.XML
217 | UpgradeLog*.htm
218 |
219 | # SQL Server files
220 | *.mdf
221 | *.ldf
222 | *.ndf
223 |
224 | # Business Intelligence projects
225 | *.rdl.data
226 | *.bim.layout
227 | *.bim_*.settings
228 |
229 | # Microsoft Fakes
230 | FakesAssemblies/
231 |
232 | # GhostDoc plugin setting file
233 | *.GhostDoc.xml
234 |
235 | # Node.js Tools for Visual Studio
236 | .ntvs_analysis.dat
237 | node_modules/
238 |
239 | # Typescript v1 declaration files
240 | typings/
241 |
242 | # Visual Studio 6 build log
243 | *.plg
244 |
245 | # Visual Studio 6 workspace options file
246 | *.opt
247 |
248 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
249 | *.vbw
250 |
251 | # Visual Studio LightSwitch build output
252 | **/*.HTMLClient/GeneratedArtifacts
253 | **/*.DesktopClient/GeneratedArtifacts
254 | **/*.DesktopClient/ModelManifest.xml
255 | **/*.Server/GeneratedArtifacts
256 | **/*.Server/ModelManifest.xml
257 | _Pvt_Extensions
258 |
259 | # Paket dependency manager
260 | .paket/paket.exe
261 | paket-files/
262 |
263 | # FAKE - F# Make
264 | .fake/
265 |
266 | # JetBrains Rider
267 | .idea/
268 | *.sln.iml
269 |
270 | # CodeRush
271 | .cr/
272 |
273 | # Python Tools for Visual Studio (PTVS)
274 | __pycache__/
275 | *.pyc
276 |
277 | # Cake - Uncomment if you are using it
278 | # tools/**
279 | # !tools/packages.config
280 |
281 | # Telerik's JustMock configuration file
282 | *.jmconfig
283 |
284 | # BizTalk build output
285 | *.btp.cs
286 | *.btm.cs
287 | *.odx.cs
288 | *.xsd.cs
289 |
--------------------------------------------------------------------------------
/GameStore.App/Controllers/AdminController.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Controllers
2 | {
3 | using Data.Models;
4 | using Infrastructure;
5 | using Models.Games;
6 | using Services.Contracts;
7 | using SimpleMvc.Framework.Attributes.Methods;
8 | using SimpleMvc.Framework.Contracts;
9 | using System.Linq;
10 |
11 | public class AdminController : BaseController
12 | {
13 | public const string GameError = "
Check your form for errors.
Title has to begin with uppercase letter and has length between 3 and 100 symbols (inclusive).
Price must be a positive number with precision up to 2 digits after floating point.
Size must be a positive number with precision up to 1 digit after floating point.
Videos should only be from YouTube.
Thumbnail URL should be a plain text starting with http://, https://.
Description must be at least 20 symbols.
";
14 |
15 | private readonly IGameService games;
16 |
17 | public AdminController(IGameService games)
18 | {
19 | this.games = games;
20 | }
21 |
22 | public IActionResult Add()
23 | {
24 | if (!this.IsAdmin)
25 | {
26 | return this.RedirectToHome();
27 | }
28 |
29 | return this.View();
30 | }
31 |
32 | [HttpPost]
33 | public IActionResult Add(GameAdminModel model)
34 | {
35 | if (!this.IsAdmin)
36 | {
37 | return this.RedirectToHome();
38 | }
39 |
40 | if (!this.IsValidModel(model))
41 | {
42 | this.ShowError(GameError);
43 | return this.View();
44 | }
45 |
46 | this.games.Create(
47 | model.Title,
48 | model.Description,
49 | model.ThumbnailUrl,
50 | model.Price,
51 | model.Size,
52 | model.VideoId,
53 | model.ReleaseDate);
54 |
55 | return this.RedirectToAllGames();
56 | }
57 |
58 | public IActionResult Edit(int id)
59 | {
60 | if (!this.IsAdmin)
61 | {
62 | return this.RedirectToHome();
63 | }
64 |
65 | var game = this.games.GetById(id);
66 |
67 | if (game == null)
68 | {
69 | return this.NotFound();
70 | }
71 |
72 | this.SetGameViewData(game);
73 |
74 | return this.View();
75 | }
76 |
77 | [HttpPost]
78 | public IActionResult Edit(int id, GameAdminModel model)
79 | {
80 | if (!this.IsAdmin)
81 | {
82 | return this.RedirectToHome();
83 | }
84 |
85 | if (!this.IsValidModel(model))
86 | {
87 | this.ShowError(GameError);
88 | return this.View();
89 | }
90 |
91 | this.games.Update(
92 | id,
93 | model.Title,
94 | model.Description,
95 | model.ThumbnailUrl,
96 | model.Price,
97 | model.Size,
98 | model.VideoId,
99 | model.ReleaseDate);
100 |
101 | return this.RedirectToAllGames();
102 | }
103 |
104 | public IActionResult Delete(int id)
105 | {
106 | if (!this.IsAdmin)
107 | {
108 | return this.RedirectToHome();
109 | }
110 |
111 | var game = this.games.GetById(id);
112 |
113 | if (game == null)
114 | {
115 | return this.NotFound();
116 | }
117 |
118 | this.ViewModel["id"] = id.ToString();
119 | this.SetGameViewData(game);
120 |
121 | return this.View();
122 | }
123 |
124 | [HttpPost]
125 | public IActionResult Destroy(int id)
126 | {
127 | if (!this.IsAdmin)
128 | {
129 | return this.RedirectToHome();
130 | }
131 |
132 | var game = this.games.GetById(id);
133 |
134 | if (game == null)
135 | {
136 | return this.NotFound();
137 | }
138 |
139 | this.games.Delete(id);
140 |
141 | return this.RedirectToAllGames();
142 | }
143 |
144 | public IActionResult All()
145 | {
146 | if (!this.IsAdmin)
147 | {
148 | return this.RedirectToHome();
149 | }
150 |
151 | var allGames = this.games
152 | .All()
153 | .Select(g => g.ToHtml());
154 |
155 | this.ViewModel["games"] = string.Join(string.Empty, allGames);
156 |
157 | return this.View();
158 | }
159 |
160 | private void SetGameViewData(Game game)
161 | {
162 | this.ViewModel["title"] = game.Title;
163 | this.ViewModel["description"] = game.Description;
164 | this.ViewModel["thumbnail"] = game.ThumbnailUrl;
165 | this.ViewModel["price"] = game.Price.ToString("F2");
166 | this.ViewModel["size"] = game.Size.ToString("F1");
167 | this.ViewModel["video-id"] = game.VideoId;
168 | this.ViewModel["release-date"] = game.ReleaseDate.ToString("yyyy-MM-dd");
169 | }
170 |
171 | private IActionResult RedirectToAllGames()
172 | => this.Redirect("/admin/all");
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/GameStore.App/Controllers/BaseController.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Controllers
2 | {
3 | using Data;
4 | using Data.Models;
5 | using SimpleMvc.Framework.Contracts;
6 | using SimpleMvc.Framework.Controllers;
7 | using System.Linq;
8 |
9 | public abstract class BaseController : Controller
10 | {
11 | protected BaseController()
12 | {
13 | this.ViewModel["anonymousDisplay"] = "flex";
14 | this.ViewModel["userDisplay"] = "none";
15 | this.ViewModel["adminDisplay"] = "none";
16 | this.ViewModel["show-error"] = "none";
17 | this.ViewModel["show-success"] = "none";
18 | }
19 |
20 | protected User Profile { get; private set; }
21 |
22 | protected void ShowError(string error)
23 | {
24 | this.ViewModel["show-error"] = "block";
25 | this.ViewModel["error"] = error;
26 | }
27 |
28 | protected void ShowSuccess(string successMessage)
29 | {
30 | this.ViewModel["show-success"] = "block";
31 | this.ViewModel["success"] = successMessage;
32 | }
33 |
34 | protected IActionResult RedirectToHome()
35 | => this.Redirect("/");
36 |
37 | protected IActionResult RedirectToLogin()
38 | => this.Redirect("/users/login");
39 |
40 | protected override void InitializeController()
41 | {
42 | base.InitializeController();
43 |
44 | if (this.User.IsAuthenticated)
45 | {
46 | this.ViewModel["anonymousDisplay"] = "none";
47 | this.ViewModel["userDisplay"] = "flex";
48 |
49 | using (var db = new GameStoreDbContext())
50 | {
51 | this.Profile = db
52 | .Users
53 | .First(u => u.Email == this.User.Name);
54 |
55 | if (this.Profile.IsAdmin)
56 | {
57 | this.ViewModel["userDisplay"] = "none";
58 | this.ViewModel["adminDisplay"] = "flex";
59 | }
60 | }
61 | }
62 | }
63 |
64 | protected bool IsAdmin => this.User.IsAuthenticated && this.Profile.IsAdmin;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/GameStore.App/Controllers/HomeController.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Controllers
2 | {
3 | using Infrastructure;
4 | using Models.Home;
5 | using Services.Contracts;
6 | using SimpleMvc.Framework.Contracts;
7 | using System.Linq;
8 | using System.Text;
9 |
10 | public class HomeController : BaseController
11 | {
12 | private readonly IGameService games;
13 |
14 | public HomeController(IGameService games)
15 | {
16 | this.games = games;
17 | }
18 |
19 | public IActionResult Index()
20 | {
21 | var ownedGames = this.User.IsAuthenticated
22 | && this.Request.UrlParameters.ContainsKey("filter")
23 | && this.Request.UrlParameters["filter"] == "Owned";
24 |
25 | var gameCards = this.games
26 | .All(ownedGames ? (int?)this.Profile.Id : null)
27 | .Select(g => g.ToHtml(this.IsAdmin))
28 | .ToList();
29 |
30 | var result = new StringBuilder();
31 |
32 | for (int i = 0; i < gameCards.Count; i++)
33 | {
34 | if (i % 3 == 0)
35 | {
36 | result.Append(@"");
37 | }
38 |
39 | result.Append(gameCards[i]);
40 |
41 | if (i % 3 == 2 || i == gameCards.Count - 1)
42 | {
43 | result.Append("
");
44 | }
45 | }
46 |
47 | this.ViewModel["games"] = result.ToString();
48 |
49 | return this.View();
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/GameStore.App/Controllers/OrdersController.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Controllers
2 | {
3 | using Infrastructure;
4 | using Models.Orders;
5 | using Services.Contracts;
6 | using SimpleMvc.Framework.Attributes.Methods;
7 | using SimpleMvc.Framework.Contracts;
8 | using System.Linq;
9 |
10 | public class OrdersController : BaseController
11 | {
12 | private readonly IGameService games;
13 | private readonly IOrderService orders;
14 |
15 | public OrdersController(
16 | IGameService games,
17 | IOrderService orders)
18 | {
19 | this.games = games;
20 | this.orders = orders;
21 | }
22 |
23 | public IActionResult Buy(int id)
24 | {
25 | if (!this.games.Exists(id))
26 | {
27 | return this.NotFound();
28 | }
29 |
30 | this.Request
31 | .Session
32 | .GetShoppingCart()
33 | .AddGame(id);
34 |
35 | return this.Redirect("/orders/cart");
36 | }
37 |
38 | public IActionResult Remove(int id)
39 | {
40 | this.Request
41 | .Session
42 | .GetShoppingCart()
43 | .RemoveGame(id);
44 |
45 | return this.Redirect("/orders/cart");
46 | }
47 |
48 | public IActionResult Cart()
49 | {
50 | var shoppingCart = this.Request.Session.GetShoppingCart();
51 |
52 | var gameIds = shoppingCart.AllGames();
53 |
54 | var gamesToBuy = this.games.ByIds(gameIds);
55 |
56 | var allGames = gamesToBuy.Select(g => g.ToHtml());
57 | var totalPrice = 0m;
58 |
59 | if (gamesToBuy.Any())
60 | {
61 | totalPrice = gamesToBuy.Sum(g => g.Price);
62 | }
63 |
64 | this.ViewModel["games"] = string.Join(string.Empty, allGames);
65 | this.ViewModel["total-price"] = totalPrice.ToString();
66 |
67 | return this.View();
68 | }
69 |
70 | [HttpPost]
71 | public IActionResult Finish()
72 | {
73 | if (!this.User.IsAuthenticated)
74 | {
75 | return this.RedirectToLogin();
76 | }
77 |
78 | var shoppingCart = this.Request.Session.GetShoppingCart();
79 |
80 | var gameIds = shoppingCart.AllGames();
81 |
82 | this.orders.Purchase(this.Profile.Id, gameIds);
83 |
84 | shoppingCart.Clear();
85 |
86 | return this.RedirectToHome();
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/GameStore.App/Controllers/UsersController.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Controllers
2 | {
3 | using Models.Users;
4 | using Services.Contracts;
5 | using SimpleMvc.Framework.Attributes.Methods;
6 | using SimpleMvc.Framework.Contracts;
7 |
8 | public class UsersController : BaseController
9 | {
10 | private const string RegisterError = "Check your form for errors.
E-mails must have at least one '@' and one '.' symbols.
Passwords must be at least 6 symbols and must contain at least 1 uppercase, 1 lowercase letter and 1 digit.
Confirm password must match the provided password.
";
11 | private const string EmailExistsError = "E-mail is already taken.";
12 | private const string LoginError = "Invalid credentials.
";
13 |
14 | private IUserService users;
15 |
16 | public UsersController(IUserService users)
17 | {
18 | this.users = users;
19 | }
20 |
21 | public IActionResult Register() => this.View();
22 |
23 | [HttpPost]
24 | public IActionResult Register(RegisterModel model)
25 | {
26 | if (model.Password != model.ConfirmPassword
27 | || !this.IsValidModel(model))
28 | {
29 | this.ShowError(RegisterError);
30 | return this.View();
31 | }
32 |
33 | var result = this.users.Create(
34 | model.Email,
35 | model.Password,
36 | model.Name);
37 |
38 | if (result)
39 | {
40 | return this.RedirectToLogin();
41 | }
42 | else
43 | {
44 | this.ShowError(EmailExistsError);
45 | return this.View();
46 | }
47 | }
48 |
49 | public IActionResult Login() => this.View();
50 |
51 | [HttpPost]
52 | public IActionResult Login(LoginModel model)
53 | {
54 | if (!this.IsValidModel(model))
55 | {
56 | this.ShowError(LoginError);
57 | return this.View();
58 | }
59 |
60 | if (this.users.UserExists(model.Email, model.Password))
61 | {
62 | this.SignIn(model.Email);
63 | return this.RedirectToHome();
64 | }
65 | else
66 | {
67 | this.ShowError(LoginError);
68 | return this.View();
69 | }
70 | }
71 |
72 | public IActionResult Logout()
73 | {
74 | this.SignOut();
75 | return this.RedirectToHome();
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/GameStore.App/Data/GameStoreDbContext.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Data
2 | {
3 | using Microsoft.EntityFrameworkCore;
4 | using Models;
5 |
6 | public class GameStoreDbContext : DbContext
7 | {
8 | public DbSet Users { get; set; }
9 |
10 | public DbSet Games { get; set; }
11 |
12 | public DbSet Orders { get; set; }
13 |
14 | protected override void OnConfiguring(DbContextOptionsBuilder builder)
15 | {
16 | builder
17 | .UseSqlServer("Server=.;Database=GameStoreExamDb;Integrated Security=True;");
18 | }
19 |
20 | protected override void OnModelCreating(ModelBuilder builder)
21 | {
22 | builder
23 | .Entity()
24 | .HasIndex(u => u.Email)
25 | .IsUnique();
26 |
27 | builder
28 | .Entity()
29 | .HasKey(o => new { o.UserId, o.GameId });
30 |
31 | builder
32 | .Entity()
33 | .HasOne(o => o.User)
34 | .WithMany(u => u.Orders)
35 | .HasForeignKey(o => o.UserId);
36 |
37 | builder
38 | .Entity()
39 | .HasOne(o => o.Game)
40 | .WithMany(g => g.Orders)
41 | .HasForeignKey(o => o.GameId);
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/GameStore.App/Data/Models/Game.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Data.Models
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 | using System.ComponentModel.DataAnnotations;
6 |
7 | public class Game
8 | {
9 | public int Id { get; set; }
10 |
11 | [Required]
12 | [MinLength(3)]
13 | [MaxLength(100)]
14 | public string Title { get; set; }
15 |
16 | [Range(0, double.MaxValue)]
17 | public decimal Price { get; set; }
18 |
19 | // In GB
20 | [Range(0, double.MaxValue)]
21 | public double Size { get; set; }
22 |
23 | [Required]
24 | [MinLength(11)]
25 | [MaxLength(11)]
26 | public string VideoId { get; set; }
27 |
28 | public string ThumbnailUrl { get; set; }
29 |
30 | [Required]
31 | [MinLength(20)]
32 | public string Description { get; set; }
33 |
34 | public DateTime ReleaseDate { get; set; }
35 |
36 | public List Orders { get; set; } = new List();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/GameStore.App/Data/Models/Order.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Data.Models
2 | {
3 | public class Order
4 | {
5 | public int UserId { get; set; }
6 |
7 | public User User { get; set; }
8 |
9 | public int GameId { get; set; }
10 |
11 | public Game Game { get; set; }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/GameStore.App/Data/Models/User.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Data.Models
2 | {
3 | using System.Collections.Generic;
4 | using System.ComponentModel.DataAnnotations;
5 |
6 | public class User
7 | {
8 | public int Id { get; set; }
9 |
10 | [Required]
11 | [MaxLength(50)]
12 | public string Email { get; set; }
13 |
14 | [Required]
15 | [MinLength(6)]
16 | [MaxLength(50)]
17 | public string Password { get; set; }
18 |
19 | [MaxLength(100)]
20 | public string Name { get; set; }
21 |
22 | public bool IsAdmin { get; set; }
23 |
24 | public List Orders { get; set; } = new List();
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/GameStore.App/GameStore.App.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | netcoreapp2.0
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/GameStore.App/Infrastructure/DependencyControllerRouter.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Infrastructure
2 | {
3 | using Data;
4 | using Services;
5 | using Services.Contracts;
6 | using SimpleMvc.Framework.Controllers;
7 | using SimpleMvc.Framework.Routers;
8 | using SimpleInjector;
9 | using SimpleInjector.Lifestyles;
10 | using System;
11 |
12 | public class DependencyControllerRouter : ControllerRouter
13 | {
14 | private readonly Container container;
15 |
16 | public DependencyControllerRouter()
17 | {
18 | this.container = new Container();
19 | this.container.Options.DefaultScopedLifestyle
20 | = new AsyncScopedLifestyle();
21 | }
22 |
23 | public Container Container => this.container;
24 |
25 | public static DependencyControllerRouter Get()
26 | {
27 | var router = new DependencyControllerRouter();
28 |
29 | var container = router.Container;
30 |
31 | container.Register();
32 | container.Register();
33 | container.Register();
34 | container.Register(Lifestyle.Scoped);
35 |
36 | container.Verify();
37 |
38 | return router;
39 | }
40 |
41 | protected override Controller CreateController(Type controllerType)
42 | {
43 | AsyncScopedLifestyle.BeginScope(this.Container);
44 | return (Controller)this.Container.GetInstance(controllerType);
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/GameStore.App/Infrastructure/HtmlHelpers.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Infrastructure
2 | {
3 | using Models.Games;
4 | using Models.Home;
5 | using Models.Orders;
6 |
7 | public static class HtmlHelpers
8 | {
9 | public static string ToHtml(this GameListingHomeModel game, bool isAdmin)
10 | => $@"
11 |
12 |
![]()
15 |
16 |
{game.Title}
17 |
Price - {game.Price}€
18 |
Size - {game.Size} GB
19 |
{game.Description.Shortify(300)}
20 |
21 |
22 | {(!isAdmin ? string.Empty : $@"
23 |
Edit
24 |
Delete
25 | ")}
26 |
27 |
Info
28 |
Buy
29 |
30 |
";
31 |
32 | public static string ToHtml(this GameListingAdminModel game)
33 | => $@"
34 |
35 | {game.Id} |
36 | {game.Name} |
37 | {game.Size} GB |
38 | {game.Price} € |
39 |
40 | Edit
41 | Delete
42 | |
43 |
";
44 |
45 | public static string ToHtml(this GameListingOrdersModel game)
46 | => $@"
47 |
48 |
49 |
X
50 |
![]()
52 |
60 |
61 |
{game.Price}€
62 |
63 |
64 |
";
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/GameStore.App/Infrastructure/HttpSessionExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Infrastructure
2 | {
3 | using Models.Orders;
4 | using WebServer.Http.Contracts;
5 |
6 | public static class HttpSessionExtensions
7 | {
8 | private const string ShoppingCartSessionKey = "%$Shopping_Cart$%";
9 |
10 | public static ShoppingCart GetShoppingCart(this IHttpSession session)
11 | {
12 | var shoppingCart = session.Get(ShoppingCartSessionKey);
13 | if (shoppingCart == null)
14 | {
15 | shoppingCart = new ShoppingCart();
16 | session.Add(ShoppingCartSessionKey, shoppingCart);
17 | }
18 |
19 | return shoppingCart;
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/GameStore.App/Infrastructure/Mapping/AutoMapperConfiguration.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Infrastructure.Mapping
2 | {
3 | using AutoMapper;
4 | using System;
5 | using System.Linq;
6 | using System.Reflection;
7 |
8 | public static class AutoMapperConfiguration
9 | {
10 | public static void Initialize()
11 | {
12 | Mapper.Initialize(config =>
13 | {
14 | var allTypes = Assembly
15 | .GetEntryAssembly()
16 | .GetTypes();
17 |
18 | var mappedTypes = allTypes
19 | .Where(t => t
20 | .GetInterfaces()
21 | .Where(i => i.IsGenericType)
22 | .Any(i => i.GetGenericTypeDefinition() == typeof(IMapFrom<>)))
23 | .Select(t => new
24 | {
25 | Destination = t,
26 | Source = t.GetInterfaces()
27 | .Where(i => i.IsGenericType
28 | && i.GetGenericTypeDefinition() == typeof(IMapFrom<>))
29 | .SelectMany(i => i.GetGenericArguments())
30 | .First()
31 | })
32 | .ToList();
33 |
34 | foreach (var type in mappedTypes)
35 | {
36 | config.CreateMap(type.Source, type.Destination);
37 | }
38 |
39 | var customMappedTypes = allTypes
40 | .Where(t => t.IsClass
41 | && typeof(IHaveCustomMapping).IsAssignableFrom(t))
42 | .Select(t => (IHaveCustomMapping)Activator.CreateInstance(t))
43 | .ToList();
44 |
45 | foreach (var type in customMappedTypes)
46 | {
47 | type.Configure(config);
48 | }
49 | });
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/GameStore.App/Infrastructure/Mapping/IHaveCustomMapping.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Infrastructure.Mapping
2 | {
3 | using AutoMapper;
4 |
5 | public interface IHaveCustomMapping
6 | {
7 | void Configure(IMapperConfigurationExpression config);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/GameStore.App/Infrastructure/Mapping/IMapFrom.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Infrastructure.Mapping
2 | {
3 | public interface IMapFrom
4 | {
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/GameStore.App/Infrastructure/StringExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Infrastructure
2 | {
3 | public static class StringExtensions
4 | {
5 | public static string Shortify(this string input, int length)
6 | {
7 | if (input == null)
8 | {
9 | return input;
10 | }
11 |
12 | return input.Length > length ? $"{input.Substring(length)}..." : input;
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/GameStore.App/Infrastructure/Validation/Games/DescriptionAttribute.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Infrastructure.Validation.Games
2 | {
3 | using SimpleMvc.Framework.Attributes.Validation;
4 |
5 | public class DescriptionAttribute : PropertyValidationAttribute
6 | {
7 | public override bool IsValid(object value)
8 | {
9 | var description = value as string;
10 | if (description == null)
11 | {
12 | return true;
13 | }
14 |
15 | return description.Length >= 20;
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/GameStore.App/Infrastructure/Validation/Games/ThumbnailUrlAttribute.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Infrastructure.Validation.Games
2 | {
3 | using SimpleMvc.Framework.Attributes.Validation;
4 |
5 | public class ThumbnailUrlAttribute : PropertyValidationAttribute
6 | {
7 | public override bool IsValid(object value)
8 | {
9 | var thumbnailUrl = value as string;
10 | if (thumbnailUrl == null)
11 | {
12 | return true;
13 | }
14 |
15 | return thumbnailUrl.StartsWith("http://")
16 | || thumbnailUrl.StartsWith("https://");
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/GameStore.App/Infrastructure/Validation/Games/TitleAttribute.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Infrastructure.Validation.Games
2 | {
3 | using SimpleMvc.Framework.Attributes.Validation;
4 |
5 | public class TitleAttribute : PropertyValidationAttribute
6 | {
7 | public override bool IsValid(object value)
8 | {
9 | var title = value as string;
10 | if (title == null)
11 | {
12 | return true;
13 | }
14 |
15 | return title.Length > 0
16 | && char.IsUpper(title[0])
17 | && title.Length >= 3
18 | && title.Length <= 100;
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/GameStore.App/Infrastructure/Validation/Games/VideoIdAttribute.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Infrastructure.Validation.Games
2 | {
3 | using SimpleMvc.Framework.Attributes.Validation;
4 |
5 | public class VideoIdAttribute : PropertyValidationAttribute
6 | {
7 | public override bool IsValid(object value)
8 | {
9 | var videoId = value as string;
10 | if (videoId == null)
11 | {
12 | return true;
13 | }
14 |
15 | return videoId.Length == 11;
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/GameStore.App/Infrastructure/Validation/RequiredAttribute.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Infrastructure.Validation
2 | {
3 | using SimpleMvc.Framework.Attributes.Validation;
4 |
5 | public class RequiredAttribute : PropertyValidationAttribute
6 | {
7 | public override bool IsValid(object value)
8 | => new System
9 | .ComponentModel
10 | .DataAnnotations
11 | .RequiredAttribute()
12 | .IsValid(value);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/GameStore.App/Infrastructure/Validation/Users/EmailAttribute.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Infrastructure.Validation.Users
2 | {
3 | using SimpleMvc.Framework.Attributes.Validation;
4 |
5 | public class EmailAttribute : PropertyValidationAttribute
6 | {
7 | public override bool IsValid(object value)
8 | {
9 | var email = value as string;
10 | if (email == null)
11 | {
12 | return true;
13 | }
14 |
15 | return email.Contains(".") && email.Contains("@");
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/GameStore.App/Infrastructure/Validation/Users/PasswordAttribute.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Infrastructure.Validation.Users
2 | {
3 | using SimpleMvc.Framework.Attributes.Validation;
4 | using System.Linq;
5 |
6 | public class PasswordAttribute : PropertyValidationAttribute
7 | {
8 | public override bool IsValid(object value)
9 | {
10 | var password = value as string;
11 | if (password == null)
12 | {
13 | return true;
14 | }
15 |
16 | return password.Any(s => char.IsDigit(s))
17 | && password.Any(s => char.IsUpper(s))
18 | && password.Any(s => char.IsLower(s))
19 | && password.Length >= 6;
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/GameStore.App/Launcher.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App
2 | {
3 | using Data;
4 | using Infrastructure;
5 | using Infrastructure.Mapping;
6 | using Microsoft.EntityFrameworkCore;
7 | using SimpleMvc.Framework;
8 | using SimpleMvc.Framework.Routers;
9 | using WebServer;
10 |
11 | public class Launcher
12 | {
13 | static Launcher()
14 | {
15 | using (var db = new GameStoreDbContext())
16 | {
17 | db.Database.Migrate();
18 | }
19 |
20 | AutoMapperConfiguration.Initialize();
21 | }
22 |
23 | public static void Main()
24 | => MvcEngine.Run(
25 | new WebServer(1337,
26 | DependencyControllerRouter.Get(),
27 | new ResourceRouter()));
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/GameStore.App/Migrations/20171024105903_UsersTable.Designer.cs:
--------------------------------------------------------------------------------
1 | //
2 | using GameStore.App.Data;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Infrastructure;
5 | using Microsoft.EntityFrameworkCore.Metadata;
6 | using Microsoft.EntityFrameworkCore.Migrations;
7 | using Microsoft.EntityFrameworkCore.Storage;
8 | using Microsoft.EntityFrameworkCore.Storage.Internal;
9 | using System;
10 |
11 | namespace GameStore.App.Migrations
12 | {
13 | [DbContext(typeof(GameStoreDbContext))]
14 | [Migration("20171024105903_UsersTable")]
15 | partial class UsersTable
16 | {
17 | protected override void BuildTargetModel(ModelBuilder modelBuilder)
18 | {
19 | #pragma warning disable 612, 618
20 | modelBuilder
21 | .HasAnnotation("ProductVersion", "2.0.0-rtm-26452")
22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
23 |
24 | modelBuilder.Entity("GameStore.App.Data.Models.User", b =>
25 | {
26 | b.Property("Id")
27 | .ValueGeneratedOnAdd();
28 |
29 | b.Property("Email")
30 | .IsRequired()
31 | .HasMaxLength(50);
32 |
33 | b.Property("Name")
34 | .HasMaxLength(100);
35 |
36 | b.Property("Password")
37 | .IsRequired()
38 | .HasMaxLength(50);
39 |
40 | b.HasKey("Id");
41 |
42 | b.HasIndex("Email")
43 | .IsUnique();
44 |
45 | b.ToTable("Users");
46 | });
47 | #pragma warning restore 612, 618
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/GameStore.App/Migrations/20171024105903_UsersTable.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Migrations
2 | {
3 | using Microsoft.EntityFrameworkCore.Metadata;
4 | using Microsoft.EntityFrameworkCore.Migrations;
5 |
6 | public partial class UsersTable : Migration
7 | {
8 | protected override void Up(MigrationBuilder migrationBuilder)
9 | {
10 | migrationBuilder.CreateTable(
11 | name: "Users",
12 | columns: table => new
13 | {
14 | Id = table.Column(type: "int", nullable: false)
15 | .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
16 | Email = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false),
17 | Name = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true),
18 | Password = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false)
19 | },
20 | constraints: table =>
21 | {
22 | table.PrimaryKey("PK_Users", x => x.Id);
23 | });
24 |
25 | migrationBuilder.CreateIndex(
26 | name: "IX_Users_Email",
27 | table: "Users",
28 | column: "Email",
29 | unique: true);
30 | }
31 |
32 | protected override void Down(MigrationBuilder migrationBuilder)
33 | {
34 | migrationBuilder.DropTable(
35 | name: "Users");
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/GameStore.App/Migrations/20171024121234_UsersIsAdminColumn.Designer.cs:
--------------------------------------------------------------------------------
1 | //
2 | using GameStore.App.Data;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Infrastructure;
5 | using Microsoft.EntityFrameworkCore.Metadata;
6 | using Microsoft.EntityFrameworkCore.Migrations;
7 | using Microsoft.EntityFrameworkCore.Storage;
8 | using Microsoft.EntityFrameworkCore.Storage.Internal;
9 | using System;
10 |
11 | namespace GameStore.App.Migrations
12 | {
13 | [DbContext(typeof(GameStoreDbContext))]
14 | [Migration("20171024121234_UsersIsAdminColumn")]
15 | partial class UsersIsAdminColumn
16 | {
17 | protected override void BuildTargetModel(ModelBuilder modelBuilder)
18 | {
19 | #pragma warning disable 612, 618
20 | modelBuilder
21 | .HasAnnotation("ProductVersion", "2.0.0-rtm-26452")
22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
23 |
24 | modelBuilder.Entity("GameStore.App.Data.Models.User", b =>
25 | {
26 | b.Property("Id")
27 | .ValueGeneratedOnAdd();
28 |
29 | b.Property("Email")
30 | .IsRequired()
31 | .HasMaxLength(50);
32 |
33 | b.Property("IsAdmin");
34 |
35 | b.Property("Name")
36 | .HasMaxLength(100);
37 |
38 | b.Property("Password")
39 | .IsRequired()
40 | .HasMaxLength(50);
41 |
42 | b.HasKey("Id");
43 |
44 | b.HasIndex("Email")
45 | .IsUnique();
46 |
47 | b.ToTable("Users");
48 | });
49 | #pragma warning restore 612, 618
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/GameStore.App/Migrations/20171024121234_UsersIsAdminColumn.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Migrations
2 | {
3 | using Microsoft.EntityFrameworkCore.Migrations;
4 |
5 | public partial class UsersIsAdminColumn : Migration
6 | {
7 | protected override void Up(MigrationBuilder migrationBuilder)
8 | {
9 | migrationBuilder.AddColumn(
10 | name: "IsAdmin",
11 | table: "Users",
12 | type: "bit",
13 | nullable: false,
14 | defaultValue: false);
15 | }
16 |
17 | protected override void Down(MigrationBuilder migrationBuilder)
18 | {
19 | migrationBuilder.DropColumn(
20 | name: "IsAdmin",
21 | table: "Users");
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/GameStore.App/Migrations/20171024124624_GamesTable.Designer.cs:
--------------------------------------------------------------------------------
1 | //
2 | using GameStore.App.Data;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Infrastructure;
5 | using Microsoft.EntityFrameworkCore.Metadata;
6 | using Microsoft.EntityFrameworkCore.Migrations;
7 | using Microsoft.EntityFrameworkCore.Storage;
8 | using Microsoft.EntityFrameworkCore.Storage.Internal;
9 | using System;
10 |
11 | namespace GameStore.App.Migrations
12 | {
13 | [DbContext(typeof(GameStoreDbContext))]
14 | [Migration("20171024124624_GamesTable")]
15 | partial class GamesTable
16 | {
17 | protected override void BuildTargetModel(ModelBuilder modelBuilder)
18 | {
19 | #pragma warning disable 612, 618
20 | modelBuilder
21 | .HasAnnotation("ProductVersion", "2.0.0-rtm-26452")
22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
23 |
24 | modelBuilder.Entity("GameStore.App.Data.Models.Game", b =>
25 | {
26 | b.Property("Id")
27 | .ValueGeneratedOnAdd();
28 |
29 | b.Property("Description")
30 | .IsRequired();
31 |
32 | b.Property("Price");
33 |
34 | b.Property("ReleaseDate");
35 |
36 | b.Property("Size");
37 |
38 | b.Property("ThumbnailUrl");
39 |
40 | b.Property("Title")
41 | .IsRequired()
42 | .HasMaxLength(100);
43 |
44 | b.Property("VideoId")
45 | .IsRequired()
46 | .HasMaxLength(11);
47 |
48 | b.HasKey("Id");
49 |
50 | b.ToTable("Games");
51 | });
52 |
53 | modelBuilder.Entity("GameStore.App.Data.Models.Order", b =>
54 | {
55 | b.Property("UserId");
56 |
57 | b.Property("GameId");
58 |
59 | b.HasKey("UserId", "GameId");
60 |
61 | b.HasIndex("GameId");
62 |
63 | b.ToTable("Orders");
64 | });
65 |
66 | modelBuilder.Entity("GameStore.App.Data.Models.User", b =>
67 | {
68 | b.Property("Id")
69 | .ValueGeneratedOnAdd();
70 |
71 | b.Property("Email")
72 | .IsRequired()
73 | .HasMaxLength(50);
74 |
75 | b.Property("IsAdmin");
76 |
77 | b.Property("Name")
78 | .HasMaxLength(100);
79 |
80 | b.Property("Password")
81 | .IsRequired()
82 | .HasMaxLength(50);
83 |
84 | b.HasKey("Id");
85 |
86 | b.HasIndex("Email")
87 | .IsUnique();
88 |
89 | b.ToTable("Users");
90 | });
91 |
92 | modelBuilder.Entity("GameStore.App.Data.Models.Order", b =>
93 | {
94 | b.HasOne("GameStore.App.Data.Models.Game", "Game")
95 | .WithMany("Orders")
96 | .HasForeignKey("GameId")
97 | .OnDelete(DeleteBehavior.Cascade);
98 |
99 | b.HasOne("GameStore.App.Data.Models.User", "User")
100 | .WithMany("Orders")
101 | .HasForeignKey("UserId")
102 | .OnDelete(DeleteBehavior.Cascade);
103 | });
104 | #pragma warning restore 612, 618
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/GameStore.App/Migrations/20171024124624_GamesTable.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Migrations
2 | {
3 | using Microsoft.EntityFrameworkCore.Metadata;
4 | using Microsoft.EntityFrameworkCore.Migrations;
5 | using System;
6 |
7 | public partial class GamesTable : Migration
8 | {
9 | protected override void Up(MigrationBuilder migrationBuilder)
10 | {
11 | migrationBuilder.CreateTable(
12 | name: "Games",
13 | columns: table => new
14 | {
15 | Id = table.Column(type: "int", nullable: false)
16 | .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
17 | Description = table.Column(type: "nvarchar(max)", nullable: false),
18 | Price = table.Column(type: "decimal(18, 2)", nullable: false),
19 | ReleaseDate = table.Column(type: "datetime2", nullable: false),
20 | Size = table.Column(type: "float", nullable: false),
21 | ThumbnailUrl = table.Column(type: "nvarchar(max)", nullable: true),
22 | Title = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false),
23 | VideoId = table.Column(type: "nvarchar(11)", maxLength: 11, nullable: false)
24 | },
25 | constraints: table =>
26 | {
27 | table.PrimaryKey("PK_Games", x => x.Id);
28 | });
29 |
30 | migrationBuilder.CreateTable(
31 | name: "Orders",
32 | columns: table => new
33 | {
34 | UserId = table.Column(type: "int", nullable: false),
35 | GameId = table.Column(type: "int", nullable: false)
36 | },
37 | constraints: table =>
38 | {
39 | table.PrimaryKey("PK_Orders", x => new { x.UserId, x.GameId });
40 | table.ForeignKey(
41 | name: "FK_Orders_Games_GameId",
42 | column: x => x.GameId,
43 | principalTable: "Games",
44 | principalColumn: "Id",
45 | onDelete: ReferentialAction.Cascade);
46 | table.ForeignKey(
47 | name: "FK_Orders_Users_UserId",
48 | column: x => x.UserId,
49 | principalTable: "Users",
50 | principalColumn: "Id",
51 | onDelete: ReferentialAction.Cascade);
52 | });
53 |
54 | migrationBuilder.CreateIndex(
55 | name: "IX_Orders_GameId",
56 | table: "Orders",
57 | column: "GameId");
58 | }
59 |
60 | protected override void Down(MigrationBuilder migrationBuilder)
61 | {
62 | migrationBuilder.DropTable(
63 | name: "Orders");
64 |
65 | migrationBuilder.DropTable(
66 | name: "Games");
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/GameStore.App/Migrations/GameStoreDbContextModelSnapshot.cs:
--------------------------------------------------------------------------------
1 | //
2 | namespace GameStore.App.Migrations
3 | {
4 | using GameStore.App.Data;
5 | using Microsoft.EntityFrameworkCore;
6 | using Microsoft.EntityFrameworkCore.Infrastructure;
7 | using Microsoft.EntityFrameworkCore.Metadata;
8 | using System;
9 |
10 | [DbContext(typeof(GameStoreDbContext))]
11 | partial class GameStoreDbContextModelSnapshot : ModelSnapshot
12 | {
13 | protected override void BuildModel(ModelBuilder modelBuilder)
14 | {
15 | #pragma warning disable 612, 618
16 | modelBuilder
17 | .HasAnnotation("ProductVersion", "2.0.0-rtm-26452")
18 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
19 |
20 | modelBuilder.Entity("GameStore.App.Data.Models.Game", b =>
21 | {
22 | b.Property("Id")
23 | .ValueGeneratedOnAdd();
24 |
25 | b.Property("Description")
26 | .IsRequired();
27 |
28 | b.Property("Price");
29 |
30 | b.Property("ReleaseDate");
31 |
32 | b.Property("Size");
33 |
34 | b.Property("ThumbnailUrl");
35 |
36 | b.Property("Title")
37 | .IsRequired()
38 | .HasMaxLength(100);
39 |
40 | b.Property("VideoId")
41 | .IsRequired()
42 | .HasMaxLength(11);
43 |
44 | b.HasKey("Id");
45 |
46 | b.ToTable("Games");
47 | });
48 |
49 | modelBuilder.Entity("GameStore.App.Data.Models.Order", b =>
50 | {
51 | b.Property("UserId");
52 |
53 | b.Property("GameId");
54 |
55 | b.HasKey("UserId", "GameId");
56 |
57 | b.HasIndex("GameId");
58 |
59 | b.ToTable("Orders");
60 | });
61 |
62 | modelBuilder.Entity("GameStore.App.Data.Models.User", b =>
63 | {
64 | b.Property("Id")
65 | .ValueGeneratedOnAdd();
66 |
67 | b.Property("Email")
68 | .IsRequired()
69 | .HasMaxLength(50);
70 |
71 | b.Property("IsAdmin");
72 |
73 | b.Property("Name")
74 | .HasMaxLength(100);
75 |
76 | b.Property("Password")
77 | .IsRequired()
78 | .HasMaxLength(50);
79 |
80 | b.HasKey("Id");
81 |
82 | b.HasIndex("Email")
83 | .IsUnique();
84 |
85 | b.ToTable("Users");
86 | });
87 |
88 | modelBuilder.Entity("GameStore.App.Data.Models.Order", b =>
89 | {
90 | b.HasOne("GameStore.App.Data.Models.Game", "Game")
91 | .WithMany("Orders")
92 | .HasForeignKey("GameId")
93 | .OnDelete(DeleteBehavior.Cascade);
94 |
95 | b.HasOne("GameStore.App.Data.Models.User", "User")
96 | .WithMany("Orders")
97 | .HasForeignKey("UserId")
98 | .OnDelete(DeleteBehavior.Cascade);
99 | });
100 | #pragma warning restore 612, 618
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/GameStore.App/Models/Games/GameAdminModel.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Models.Games
2 | {
3 | using Infrastructure.Validation;
4 | using Infrastructure.Validation.Games;
5 | using SimpleMvc.Framework.Attributes.Validation;
6 | using System;
7 |
8 | public class GameAdminModel
9 | {
10 | [Required]
11 | [Title]
12 | public string Title { get; set; }
13 |
14 | [Required]
15 | [Description]
16 | public string Description { get; set; }
17 |
18 | [ThumbnailUrl]
19 | public string ThumbnailUrl { get; set; }
20 |
21 | [NumberRange(0, double.MaxValue)]
22 | public decimal Price { get; set; }
23 |
24 | [NumberRange(0, double.MaxValue)]
25 | public double Size { get; set; }
26 |
27 | [Required]
28 | [VideoId]
29 | public string VideoId { get; set; }
30 |
31 | public DateTime ReleaseDate { get; set; }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/GameStore.App/Models/Games/GameListingAdminModel.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Models.Games
2 | {
3 | using AutoMapper;
4 | using Data.Models;
5 | using Infrastructure.Mapping;
6 |
7 | public class GameListingAdminModel : IMapFrom, IHaveCustomMapping
8 | {
9 | public int Id { get; set; }
10 |
11 | public string Name { get; set; }
12 |
13 | public double Size { get; set; }
14 |
15 | public decimal Price { get; set; }
16 |
17 | public void Configure(IMapperConfigurationExpression config)
18 | {
19 | config
20 | .CreateMap()
21 | .ForMember(gla => gla.Name, cfg => cfg.MapFrom(g => g.Title));
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/GameStore.App/Models/Home/GameListingHomeModel.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Models.Home
2 | {
3 | using Data.Models;
4 | using Infrastructure.Mapping;
5 |
6 | public class GameListingHomeModel : IMapFrom
7 | {
8 | public int Id { get; set; }
9 |
10 | public string Title { get; set; }
11 |
12 | public decimal Price { get; set; }
13 |
14 | public double Size { get; set; }
15 |
16 | public string ThumbnailUrl { get; set; }
17 |
18 | public string VideoId { get; set; }
19 |
20 | public string Description { get; set; }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/GameStore.App/Models/Orders/GameListingOrdersModel.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Models.Orders
2 | {
3 | using Data.Models;
4 | using Infrastructure.Mapping;
5 |
6 | public class GameListingOrdersModel : IMapFrom
7 | {
8 | public int Id { get; set; }
9 |
10 | public string Title { get; set; }
11 |
12 | public decimal Price { get; set; }
13 |
14 | public string VideoId { get; set; }
15 |
16 | public string ThumbnailUrl { get; set; }
17 |
18 | public string Description { get; set; }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/GameStore.App/Models/Orders/ShoppingCart.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Models.Orders
2 | {
3 | using System.Collections.Generic;
4 |
5 | public class ShoppingCart
6 | {
7 | private readonly ICollection gameIds;
8 |
9 | public ShoppingCart()
10 | {
11 | this.gameIds = new List();
12 | }
13 |
14 | public void AddGame(int gameId)
15 | {
16 | if (!this.gameIds.Contains(gameId))
17 | {
18 | this.gameIds.Add(gameId);
19 | }
20 | }
21 |
22 | public void RemoveGame(int gameId) => this.gameIds.Remove(gameId);
23 |
24 | public IEnumerable AllGames() => new List(this.gameIds);
25 |
26 | public void Clear() => this.gameIds.Clear();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/GameStore.App/Models/Users/LoginModel.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Models.Users
2 | {
3 | using Infrastructure.Validation;
4 |
5 | public class LoginModel
6 | {
7 | [Required]
8 | public string Email { get; set; }
9 |
10 | [Required]
11 | public string Password { get; set; }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/GameStore.App/Models/Users/RegisterModel.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Models.Users
2 | {
3 | using Infrastructure.Validation;
4 | using Infrastructure.Validation.Users;
5 |
6 | public class RegisterModel
7 | {
8 | [Required]
9 | [Email]
10 | public string Email { get; set; }
11 |
12 | public string Name { get; set; }
13 |
14 | [Required]
15 | [Password]
16 | public string Password { get; set; }
17 |
18 | [Required]
19 | public string ConfirmPassword { get; set; }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/GameStore.App/Resources/js/bootstrap.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap v4.0.0-beta (https://getbootstrap.com)
3 | * Copyright 2011-2017 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
5 | */
6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery. jQuery must be included before Bootstrap's JavaScript.");!function(t){var e=jQuery.fn.jquery.split(" ")[0].split(".");if(e[0]<2&&e[1]<9||1==e[0]&&9==e[1]&&e[2]<1||e[0]>=4)throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}(),function(){function t(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}function e(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}var i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},o=function(){function t(t,e){for(var n=0;n0?n:null}catch(t){return null}},reflow:function(t){return t.offsetHeight},triggerTransitionEnd:function(e){t(e).trigger(s.end)},supportsTransitionEnd:function(){return Boolean(s)},typeCheckConfig:function(t,i,o){for(var r in o)if(o.hasOwnProperty(r)){var s=o[r],a=i[r],l=a&&n(a)?"element":e(a);if(!new RegExp(s).test(l))throw new Error(t.toUpperCase()+': Option "'+r+'" provided type "'+l+'" but expected type "'+s+'".')}}};return s=o(),t.fn.emulateTransitionEnd=r,l.supportsTransitionEnd()&&(t.event.special[l.TRANSITION_END]=i()),l}(jQuery),s=(function(t){var e="alert",i=t.fn[e],s={DISMISS:'[data-dismiss="alert"]'},a={CLOSE:"close.bs.alert",CLOSED:"closed.bs.alert",CLICK_DATA_API:"click.bs.alert.data-api"},l={ALERT:"alert",FADE:"fade",SHOW:"show"},h=function(){function e(t){n(this,e),this._element=t}return e.prototype.close=function(t){t=t||this._element;var e=this._getRootElement(t);this._triggerCloseEvent(e).isDefaultPrevented()||this._removeElement(e)},e.prototype.dispose=function(){t.removeData(this._element,"bs.alert"),this._element=null},e.prototype._getRootElement=function(e){var n=r.getSelectorFromElement(e),i=!1;return n&&(i=t(n)[0]),i||(i=t(e).closest("."+l.ALERT)[0]),i},e.prototype._triggerCloseEvent=function(e){var n=t.Event(a.CLOSE);return t(e).trigger(n),n},e.prototype._removeElement=function(e){var n=this;t(e).removeClass(l.SHOW),r.supportsTransitionEnd()&&t(e).hasClass(l.FADE)?t(e).one(r.TRANSITION_END,function(t){return n._destroyElement(e,t)}).emulateTransitionEnd(150):this._destroyElement(e)},e.prototype._destroyElement=function(e){t(e).detach().trigger(a.CLOSED).remove()},e._jQueryInterface=function(n){return this.each(function(){var i=t(this),o=i.data("bs.alert");o||(o=new e(this),i.data("bs.alert",o)),"close"===n&&o[n](this)})},e._handleDismiss=function(t){return function(e){e&&e.preventDefault(),t.close(this)}},o(e,null,[{key:"VERSION",get:function(){return"4.0.0-beta"}}]),e}();t(document).on(a.CLICK_DATA_API,s.DISMISS,h._handleDismiss(new h)),t.fn[e]=h._jQueryInterface,t.fn[e].Constructor=h,t.fn[e].noConflict=function(){return t.fn[e]=i,h._jQueryInterface}}(jQuery),function(t){var e="button",i=t.fn[e],r={ACTIVE:"active",BUTTON:"btn",FOCUS:"focus"},s={DATA_TOGGLE_CARROT:'[data-toggle^="button"]',DATA_TOGGLE:'[data-toggle="buttons"]',INPUT:"input",ACTIVE:".active",BUTTON:".btn"},a={CLICK_DATA_API:"click.bs.button.data-api",FOCUS_BLUR_DATA_API:"focus.bs.button.data-api blur.bs.button.data-api"},l=function(){function e(t){n(this,e),this._element=t}return e.prototype.toggle=function(){var e=!0,n=!0,i=t(this._element).closest(s.DATA_TOGGLE)[0];if(i){var o=t(this._element).find(s.INPUT)[0];if(o){if("radio"===o.type)if(o.checked&&t(this._element).hasClass(r.ACTIVE))e=!1;else{var a=t(i).find(s.ACTIVE)[0];a&&t(a).removeClass(r.ACTIVE)}if(e){if(o.hasAttribute("disabled")||i.hasAttribute("disabled")||o.classList.contains("disabled")||i.classList.contains("disabled"))return;o.checked=!t(this._element).hasClass(r.ACTIVE),t(o).trigger("change")}o.focus(),n=!1}}n&&this._element.setAttribute("aria-pressed",!t(this._element).hasClass(r.ACTIVE)),e&&t(this._element).toggleClass(r.ACTIVE)},e.prototype.dispose=function(){t.removeData(this._element,"bs.button"),this._element=null},e._jQueryInterface=function(n){return this.each(function(){var i=t(this).data("bs.button");i||(i=new e(this),t(this).data("bs.button",i)),"toggle"===n&&i[n]()})},o(e,null,[{key:"VERSION",get:function(){return"4.0.0-beta"}}]),e}();t(document).on(a.CLICK_DATA_API,s.DATA_TOGGLE_CARROT,function(e){e.preventDefault();var n=e.target;t(n).hasClass(r.BUTTON)||(n=t(n).closest(s.BUTTON)),l._jQueryInterface.call(t(n),"toggle")}).on(a.FOCUS_BLUR_DATA_API,s.DATA_TOGGLE_CARROT,function(e){var n=t(e.target).closest(s.BUTTON)[0];t(n).toggleClass(r.FOCUS,/^focus(in)?$/.test(e.type))}),t.fn[e]=l._jQueryInterface,t.fn[e].Constructor=l,t.fn[e].noConflict=function(){return t.fn[e]=i,l._jQueryInterface}}(jQuery),function(t){var e="carousel",s="bs.carousel",a="."+s,l=t.fn[e],h={interval:5e3,keyboard:!0,slide:!1,pause:"hover",wrap:!0},c={interval:"(number|boolean)",keyboard:"boolean",slide:"(boolean|string)",pause:"(string|boolean)",wrap:"boolean"},u={NEXT:"next",PREV:"prev",LEFT:"left",RIGHT:"right"},d={SLIDE:"slide"+a,SLID:"slid"+a,KEYDOWN:"keydown"+a,MOUSEENTER:"mouseenter"+a,MOUSELEAVE:"mouseleave"+a,TOUCHEND:"touchend"+a,LOAD_DATA_API:"load.bs.carousel.data-api",CLICK_DATA_API:"click.bs.carousel.data-api"},f={CAROUSEL:"carousel",ACTIVE:"active",SLIDE:"slide",RIGHT:"carousel-item-right",LEFT:"carousel-item-left",NEXT:"carousel-item-next",PREV:"carousel-item-prev",ITEM:"carousel-item"},p={ACTIVE:".active",ACTIVE_ITEM:".active.carousel-item",ITEM:".carousel-item",NEXT_PREV:".carousel-item-next, .carousel-item-prev",INDICATORS:".carousel-indicators",DATA_SLIDE:"[data-slide], [data-slide-to]",DATA_RIDE:'[data-ride="carousel"]'},_=function(){function l(e,i){n(this,l),this._items=null,this._interval=null,this._activeElement=null,this._isPaused=!1,this._isSliding=!1,this.touchTimeout=null,this._config=this._getConfig(i),this._element=t(e)[0],this._indicatorsElement=t(this._element).find(p.INDICATORS)[0],this._addEventListeners()}return l.prototype.next=function(){this._isSliding||this._slide(u.NEXT)},l.prototype.nextWhenVisible=function(){document.hidden||this.next()},l.prototype.prev=function(){this._isSliding||this._slide(u.PREV)},l.prototype.pause=function(e){e||(this._isPaused=!0),t(this._element).find(p.NEXT_PREV)[0]&&r.supportsTransitionEnd()&&(r.triggerTransitionEnd(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null},l.prototype.cycle=function(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config.interval&&!this._isPaused&&(this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))},l.prototype.to=function(e){var n=this;this._activeElement=t(this._element).find(p.ACTIVE_ITEM)[0];var i=this._getItemIndex(this._activeElement);if(!(e>this._items.length-1||e<0))if(this._isSliding)t(this._element).one(d.SLID,function(){return n.to(e)});else{if(i===e)return this.pause(),void this.cycle();var o=e>i?u.NEXT:u.PREV;this._slide(o,this._items[e])}},l.prototype.dispose=function(){t(this._element).off(a),t.removeData(this._element,s),this._items=null,this._config=null,this._element=null,this._interval=null,this._isPaused=null,this._isSliding=null,this._activeElement=null,this._indicatorsElement=null},l.prototype._getConfig=function(n){return n=t.extend({},h,n),r.typeCheckConfig(e,n,c),n},l.prototype._addEventListeners=function(){var e=this;this._config.keyboard&&t(this._element).on(d.KEYDOWN,function(t){return e._keydown(t)}),"hover"===this._config.pause&&(t(this._element).on(d.MOUSEENTER,function(t){return e.pause(t)}).on(d.MOUSELEAVE,function(t){return e.cycle(t)}),"ontouchstart"in document.documentElement&&t(this._element).on(d.TOUCHEND,function(){e.pause(),e.touchTimeout&&clearTimeout(e.touchTimeout),e.touchTimeout=setTimeout(function(t){return e.cycle(t)},500+e._config.interval)}))},l.prototype._keydown=function(t){if(!/input|textarea/i.test(t.target.tagName))switch(t.which){case 37:t.preventDefault(),this.prev();break;case 39:t.preventDefault(),this.next();break;default:return}},l.prototype._getItemIndex=function(e){return this._items=t.makeArray(t(e).parent().find(p.ITEM)),this._items.indexOf(e)},l.prototype._getItemByDirection=function(t,e){var n=t===u.NEXT,i=t===u.PREV,o=this._getItemIndex(e),r=this._items.length-1;if((i&&0===o||n&&o===r)&&!this._config.wrap)return e;var s=(o+(t===u.PREV?-1:1))%this._items.length;return-1===s?this._items[this._items.length-1]:this._items[s]},l.prototype._triggerSlideEvent=function(e,n){var i=this._getItemIndex(e),o=this._getItemIndex(t(this._element).find(p.ACTIVE_ITEM)[0]),r=t.Event(d.SLIDE,{relatedTarget:e,direction:n,from:o,to:i});return t(this._element).trigger(r),r},l.prototype._setActiveIndicatorElement=function(e){if(this._indicatorsElement){t(this._indicatorsElement).find(p.ACTIVE).removeClass(f.ACTIVE);var n=this._indicatorsElement.children[this._getItemIndex(e)];n&&t(n).addClass(f.ACTIVE)}},l.prototype._slide=function(e,n){var i=this,o=t(this._element).find(p.ACTIVE_ITEM)[0],s=this._getItemIndex(o),a=n||o&&this._getItemByDirection(e,o),l=this._getItemIndex(a),h=Boolean(this._interval),c=void 0,_=void 0,g=void 0;if(e===u.NEXT?(c=f.LEFT,_=f.NEXT,g=u.LEFT):(c=f.RIGHT,_=f.PREV,g=u.RIGHT),a&&t(a).hasClass(f.ACTIVE))this._isSliding=!1;else if(!this._triggerSlideEvent(a,g).isDefaultPrevented()&&o&&a){this._isSliding=!0,h&&this.pause(),this._setActiveIndicatorElement(a);var m=t.Event(d.SLID,{relatedTarget:a,direction:g,from:s,to:l});r.supportsTransitionEnd()&&t(this._element).hasClass(f.SLIDE)?(t(a).addClass(_),r.reflow(a),t(o).addClass(c),t(a).addClass(c),t(o).one(r.TRANSITION_END,function(){t(a).removeClass(c+" "+_).addClass(f.ACTIVE),t(o).removeClass(f.ACTIVE+" "+_+" "+c),i._isSliding=!1,setTimeout(function(){return t(i._element).trigger(m)},0)}).emulateTransitionEnd(600)):(t(o).removeClass(f.ACTIVE),t(a).addClass(f.ACTIVE),this._isSliding=!1,t(this._element).trigger(m)),h&&this.cycle()}},l._jQueryInterface=function(e){return this.each(function(){var n=t(this).data(s),o=t.extend({},h,t(this).data());"object"===(void 0===e?"undefined":i(e))&&t.extend(o,e);var r="string"==typeof e?e:o.slide;if(n||(n=new l(this,o),t(this).data(s,n)),"number"==typeof e)n.to(e);else if("string"==typeof r){if(void 0===n[r])throw new Error('No method named "'+r+'"');n[r]()}else o.interval&&(n.pause(),n.cycle())})},l._dataApiClickHandler=function(e){var n=r.getSelectorFromElement(this);if(n){var i=t(n)[0];if(i&&t(i).hasClass(f.CAROUSEL)){var o=t.extend({},t(i).data(),t(this).data()),a=this.getAttribute("data-slide-to");a&&(o.interval=!1),l._jQueryInterface.call(t(i),o),a&&t(i).data(s).to(a),e.preventDefault()}}},o(l,null,[{key:"VERSION",get:function(){return"4.0.0-beta"}},{key:"Default",get:function(){return h}}]),l}();t(document).on(d.CLICK_DATA_API,p.DATA_SLIDE,_._dataApiClickHandler),t(window).on(d.LOAD_DATA_API,function(){t(p.DATA_RIDE).each(function(){var e=t(this);_._jQueryInterface.call(e,e.data())})}),t.fn[e]=_._jQueryInterface,t.fn[e].Constructor=_,t.fn[e].noConflict=function(){return t.fn[e]=l,_._jQueryInterface}}(jQuery),function(t){var e="collapse",s="bs.collapse",a=t.fn[e],l={toggle:!0,parent:""},h={toggle:"boolean",parent:"string"},c={SHOW:"show.bs.collapse",SHOWN:"shown.bs.collapse",HIDE:"hide.bs.collapse",HIDDEN:"hidden.bs.collapse",CLICK_DATA_API:"click.bs.collapse.data-api"},u={SHOW:"show",COLLAPSE:"collapse",COLLAPSING:"collapsing",COLLAPSED:"collapsed"},d={WIDTH:"width",HEIGHT:"height"},f={ACTIVES:".show, .collapsing",DATA_TOGGLE:'[data-toggle="collapse"]'},p=function(){function a(e,i){n(this,a),this._isTransitioning=!1,this._element=e,this._config=this._getConfig(i),this._triggerArray=t.makeArray(t('[data-toggle="collapse"][href="#'+e.id+'"],[data-toggle="collapse"][data-target="#'+e.id+'"]'));for(var o=t(f.DATA_TOGGLE),s=0;s0&&this._triggerArray.push(l)}this._parent=this._config.parent?this._getParent():null,this._config.parent||this._addAriaAndCollapsedClass(this._element,this._triggerArray),this._config.toggle&&this.toggle()}return a.prototype.toggle=function(){t(this._element).hasClass(u.SHOW)?this.hide():this.show()},a.prototype.show=function(){var e=this;if(!this._isTransitioning&&!t(this._element).hasClass(u.SHOW)){var n=void 0,i=void 0;if(this._parent&&((n=t.makeArray(t(this._parent).children().children(f.ACTIVES))).length||(n=null)),!(n&&(i=t(n).data(s))&&i._isTransitioning)){var o=t.Event(c.SHOW);if(t(this._element).trigger(o),!o.isDefaultPrevented()){n&&(a._jQueryInterface.call(t(n),"hide"),i||t(n).data(s,null));var l=this._getDimension();t(this._element).removeClass(u.COLLAPSE).addClass(u.COLLAPSING),this._element.style[l]=0,this._triggerArray.length&&t(this._triggerArray).removeClass(u.COLLAPSED).attr("aria-expanded",!0),this.setTransitioning(!0);var h=function(){t(e._element).removeClass(u.COLLAPSING).addClass(u.COLLAPSE).addClass(u.SHOW),e._element.style[l]="",e.setTransitioning(!1),t(e._element).trigger(c.SHOWN)};if(r.supportsTransitionEnd()){var d="scroll"+(l[0].toUpperCase()+l.slice(1));t(this._element).one(r.TRANSITION_END,h).emulateTransitionEnd(600),this._element.style[l]=this._element[d]+"px"}else h()}}}},a.prototype.hide=function(){var e=this;if(!this._isTransitioning&&t(this._element).hasClass(u.SHOW)){var n=t.Event(c.HIDE);if(t(this._element).trigger(n),!n.isDefaultPrevented()){var i=this._getDimension();if(this._element.style[i]=this._element.getBoundingClientRect()[i]+"px",r.reflow(this._element),t(this._element).addClass(u.COLLAPSING).removeClass(u.COLLAPSE).removeClass(u.SHOW),this._triggerArray.length)for(var o=0;o0},l.prototype._getPopperConfig=function(){var t={placement:this._getPlacement(),modifiers:{offset:{offset:this._config.offset},flip:{enabled:this._config.flip}}};return this._inNavbar&&(t.modifiers.applyStyle={enabled:!this._inNavbar}),t},l._jQueryInterface=function(e){return this.each(function(){var n=t(this).data(s),o="object"===(void 0===e?"undefined":i(e))?e:null;if(n||(n=new l(this,o),t(this).data(s,n)),"string"==typeof e){if(void 0===n[e])throw new Error('No method named "'+e+'"');n[e]()}})},l._clearMenus=function(e){if(!e||3!==e.which&&("keyup"!==e.type||9===e.which))for(var n=t.makeArray(t(d.DATA_TOGGLE)),i=0;i0&&r--,40===e.which&&rdocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},a.prototype._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},a.prototype._checkScrollbar=function(){this._isBodyOverflowing=document.body.clientWidth=n){var i=this._targets[this._targets.length-1];this._activeTarget!==i&&this._activate(i)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(var o=this._offsets.length;o--;)this._activeTarget!==this._targets[o]&&t>=this._offsets[o]&&(void 0===this._offsets[o+1]||t .dropdown-menu .active"},l=function(){function e(t){n(this,e),this._element=t}return e.prototype.show=function(){var e=this;if(!(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&t(this._element).hasClass(s.ACTIVE)||t(this._element).hasClass(s.DISABLED))){var n=void 0,o=void 0,l=t(this._element).closest(a.NAV_LIST_GROUP)[0],h=r.getSelectorFromElement(this._element);l&&(o=t.makeArray(t(l).find(a.ACTIVE)),o=o[o.length-1]);var c=t.Event(i.HIDE,{relatedTarget:this._element}),u=t.Event(i.SHOW,{relatedTarget:o});if(o&&t(o).trigger(c),t(this._element).trigger(u),!u.isDefaultPrevented()&&!c.isDefaultPrevented()){h&&(n=t(h)[0]),this._activate(this._element,l);var d=function(){var n=t.Event(i.HIDDEN,{relatedTarget:e._element}),r=t.Event(i.SHOWN,{relatedTarget:o});t(o).trigger(n),t(e._element).trigger(r)};n?this._activate(n,n.parentNode,d):d()}}},e.prototype.dispose=function(){t.removeData(this._element,"bs.tab"),this._element=null},e.prototype._activate=function(e,n,i){var o=this,l=t(n).find(a.ACTIVE)[0],h=i&&r.supportsTransitionEnd()&&l&&t(l).hasClass(s.FADE),c=function(){return o._transitionComplete(e,l,h,i)};l&&h?t(l).one(r.TRANSITION_END,c).emulateTransitionEnd(150):c(),l&&t(l).removeClass(s.SHOW)},e.prototype._transitionComplete=function(e,n,i,o){if(n){t(n).removeClass(s.ACTIVE);var l=t(n.parentNode).find(a.DROPDOWN_ACTIVE_CHILD)[0];l&&t(l).removeClass(s.ACTIVE),n.setAttribute("aria-expanded",!1)}if(t(e).addClass(s.ACTIVE),e.setAttribute("aria-expanded",!0),i?(r.reflow(e),t(e).addClass(s.SHOW)):t(e).removeClass(s.FADE),e.parentNode&&t(e.parentNode).hasClass(s.DROPDOWN_MENU)){var h=t(e).closest(a.DROPDOWN)[0];h&&t(h).find(a.DROPDOWN_TOGGLE).addClass(s.ACTIVE),e.setAttribute("aria-expanded",!0)}o&&o()},e._jQueryInterface=function(n){return this.each(function(){var i=t(this),o=i.data("bs.tab");if(o||(o=new e(this),i.data("bs.tab",o)),"string"==typeof n){if(void 0===o[n])throw new Error('No method named "'+n+'"');o[n]()}})},o(e,null,[{key:"VERSION",get:function(){return"4.0.0-beta"}}]),e}();t(document).on(i.CLICK_DATA_API,a.DATA_TOGGLE,function(e){e.preventDefault(),l._jQueryInterface.call(t(this),"show")}),t.fn.tab=l._jQueryInterface,t.fn.tab.Constructor=l,t.fn.tab.noConflict=function(){return t.fn.tab=e,l._jQueryInterface}}(jQuery),function(t){if("undefined"==typeof Popper)throw new Error("Bootstrap tooltips require Popper.js (https://popper.js.org)");var e="tooltip",s=".bs.tooltip",a=t.fn[e],l=new RegExp("(^|\\s)bs-tooltip\\S+","g"),h={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(number|string)",container:"(string|element|boolean)",fallbackPlacement:"(string|array)"},c={AUTO:"auto",TOP:"top",RIGHT:"right",BOTTOM:"bottom",LEFT:"left"},u={animation:!0,template:'',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip"},d={SHOW:"show",OUT:"out"},f={HIDE:"hide"+s,HIDDEN:"hidden"+s,SHOW:"show"+s,SHOWN:"shown"+s,INSERTED:"inserted"+s,CLICK:"click"+s,FOCUSIN:"focusin"+s,FOCUSOUT:"focusout"+s,MOUSEENTER:"mouseenter"+s,MOUSELEAVE:"mouseleave"+s},p={FADE:"fade",SHOW:"show"},_={TOOLTIP:".tooltip",TOOLTIP_INNER:".tooltip-inner",ARROW:".arrow"},g={HOVER:"hover",FOCUS:"focus",CLICK:"click",MANUAL:"manual"},m=function(){function a(t,e){n(this,a),this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}return a.prototype.enable=function(){this._isEnabled=!0},a.prototype.disable=function(){this._isEnabled=!1},a.prototype.toggleEnabled=function(){this._isEnabled=!this._isEnabled},a.prototype.toggle=function(e){if(e){var n=this.constructor.DATA_KEY,i=t(e.currentTarget).data(n);i||(i=new this.constructor(e.currentTarget,this._getDelegateConfig()),t(e.currentTarget).data(n,i)),i._activeTrigger.click=!i._activeTrigger.click,i._isWithActiveTrigger()?i._enter(null,i):i._leave(null,i)}else{if(t(this.getTipElement()).hasClass(p.SHOW))return void this._leave(null,this);this._enter(null,this)}},a.prototype.dispose=function(){clearTimeout(this._timeout),t.removeData(this.element,this.constructor.DATA_KEY),t(this.element).off(this.constructor.EVENT_KEY),t(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&t(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,null!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},a.prototype.show=function(){var e=this;if("none"===t(this.element).css("display"))throw new Error("Please use show on visible elements");var n=t.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){t(this.element).trigger(n);var i=t.contains(this.element.ownerDocument.documentElement,this.element);if(n.isDefaultPrevented()||!i)return;var o=this.getTipElement(),s=r.getUID(this.constructor.NAME);o.setAttribute("id",s),this.element.setAttribute("aria-describedby",s),this.setContent(),this.config.animation&&t(o).addClass(p.FADE);var l="function"==typeof this.config.placement?this.config.placement.call(this,o,this.element):this.config.placement,h=this._getAttachment(l);this.addAttachmentClass(h);var c=!1===this.config.container?document.body:t(this.config.container);t(o).data(this.constructor.DATA_KEY,this),t.contains(this.element.ownerDocument.documentElement,this.tip)||t(o).appendTo(c),t(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new Popper(this.element,o,{placement:h,modifiers:{offset:{offset:this.config.offset},flip:{behavior:this.config.fallbackPlacement},arrow:{element:_.ARROW}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){e._handlePopperPlacementChange(t)}}),t(o).addClass(p.SHOW),"ontouchstart"in document.documentElement&&t("body").children().on("mouseover",null,t.noop);var u=function(){e.config.animation&&e._fixTransition();var n=e._hoverState;e._hoverState=null,t(e.element).trigger(e.constructor.Event.SHOWN),n===d.OUT&&e._leave(null,e)};r.supportsTransitionEnd()&&t(this.tip).hasClass(p.FADE)?t(this.tip).one(r.TRANSITION_END,u).emulateTransitionEnd(a._TRANSITION_DURATION):u()}},a.prototype.hide=function(e){var n=this,i=this.getTipElement(),o=t.Event(this.constructor.Event.HIDE),s=function(){n._hoverState!==d.SHOW&&i.parentNode&&i.parentNode.removeChild(i),n._cleanTipClass(),n.element.removeAttribute("aria-describedby"),t(n.element).trigger(n.constructor.Event.HIDDEN),null!==n._popper&&n._popper.destroy(),e&&e()};t(this.element).trigger(o),o.isDefaultPrevented()||(t(i).removeClass(p.SHOW),"ontouchstart"in document.documentElement&&t("body").children().off("mouseover",null,t.noop),this._activeTrigger[g.CLICK]=!1,this._activeTrigger[g.FOCUS]=!1,this._activeTrigger[g.HOVER]=!1,r.supportsTransitionEnd()&&t(this.tip).hasClass(p.FADE)?t(i).one(r.TRANSITION_END,s).emulateTransitionEnd(150):s(),this._hoverState="")},a.prototype.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},a.prototype.isWithContent=function(){return Boolean(this.getTitle())},a.prototype.addAttachmentClass=function(e){t(this.getTipElement()).addClass("bs-tooltip-"+e)},a.prototype.getTipElement=function(){return this.tip=this.tip||t(this.config.template)[0]},a.prototype.setContent=function(){var e=t(this.getTipElement());this.setElementContent(e.find(_.TOOLTIP_INNER),this.getTitle()),e.removeClass(p.FADE+" "+p.SHOW)},a.prototype.setElementContent=function(e,n){var o=this.config.html;"object"===(void 0===n?"undefined":i(n))&&(n.nodeType||n.jquery)?o?t(n).parent().is(e)||e.empty().append(n):e.text(t(n).text()):e[o?"html":"text"](n)},a.prototype.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},a.prototype._getAttachment=function(t){return c[t.toUpperCase()]},a.prototype._setListeners=function(){var e=this;this.config.trigger.split(" ").forEach(function(n){if("click"===n)t(e.element).on(e.constructor.Event.CLICK,e.config.selector,function(t){return e.toggle(t)});else if(n!==g.MANUAL){var i=n===g.HOVER?e.constructor.Event.MOUSEENTER:e.constructor.Event.FOCUSIN,o=n===g.HOVER?e.constructor.Event.MOUSELEAVE:e.constructor.Event.FOCUSOUT;t(e.element).on(i,e.config.selector,function(t){return e._enter(t)}).on(o,e.config.selector,function(t){return e._leave(t)})}t(e.element).closest(".modal").on("hide.bs.modal",function(){return e.hide()})}),this.config.selector?this.config=t.extend({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},a.prototype._fixTitle=function(){var t=i(this.element.getAttribute("data-original-title"));(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},a.prototype._enter=function(e,n){var i=this.constructor.DATA_KEY;(n=n||t(e.currentTarget).data(i))||(n=new this.constructor(e.currentTarget,this._getDelegateConfig()),t(e.currentTarget).data(i,n)),e&&(n._activeTrigger["focusin"===e.type?g.FOCUS:g.HOVER]=!0),t(n.getTipElement()).hasClass(p.SHOW)||n._hoverState===d.SHOW?n._hoverState=d.SHOW:(clearTimeout(n._timeout),n._hoverState=d.SHOW,n.config.delay&&n.config.delay.show?n._timeout=setTimeout(function(){n._hoverState===d.SHOW&&n.show()},n.config.delay.show):n.show())},a.prototype._leave=function(e,n){var i=this.constructor.DATA_KEY;(n=n||t(e.currentTarget).data(i))||(n=new this.constructor(e.currentTarget,this._getDelegateConfig()),t(e.currentTarget).data(i,n)),e&&(n._activeTrigger["focusout"===e.type?g.FOCUS:g.HOVER]=!1),n._isWithActiveTrigger()||(clearTimeout(n._timeout),n._hoverState=d.OUT,n.config.delay&&n.config.delay.hide?n._timeout=setTimeout(function(){n._hoverState===d.OUT&&n.hide()},n.config.delay.hide):n.hide())},a.prototype._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},a.prototype._getConfig=function(n){return(n=t.extend({},this.constructor.Default,t(this.element).data(),n)).delay&&"number"==typeof n.delay&&(n.delay={show:n.delay,hide:n.delay}),n.title&&"number"==typeof n.title&&(n.title=n.title.toString()),n.content&&"number"==typeof n.content&&(n.content=n.content.toString()),r.typeCheckConfig(e,n,this.constructor.DefaultType),n},a.prototype._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},a.prototype._cleanTipClass=function(){var e=t(this.getTipElement()),n=e.attr("class").match(l);null!==n&&n.length>0&&e.removeClass(n.join(""))},a.prototype._handlePopperPlacementChange=function(t){this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},a.prototype._fixTransition=function(){var e=this.getTipElement(),n=this.config.animation;null===e.getAttribute("x-placement")&&(t(e).removeClass(p.FADE),this.config.animation=!1,this.hide(),this.show(),this.config.animation=n)},a._jQueryInterface=function(e){return this.each(function(){var n=t(this).data("bs.tooltip"),o="object"===(void 0===e?"undefined":i(e))&&e;if((n||!/dispose|hide/.test(e))&&(n||(n=new a(this,o),t(this).data("bs.tooltip",n)),"string"==typeof e)){if(void 0===n[e])throw new Error('No method named "'+e+'"');n[e]()}})},o(a,null,[{key:"VERSION",get:function(){return"4.0.0-beta"}},{key:"Default",get:function(){return u}},{key:"NAME",get:function(){return e}},{key:"DATA_KEY",get:function(){return"bs.tooltip"}},{key:"Event",get:function(){return f}},{key:"EVENT_KEY",get:function(){return s}},{key:"DefaultType",get:function(){return h}}]),a}();return t.fn[e]=m._jQueryInterface,t.fn[e].Constructor=m,t.fn[e].noConflict=function(){return t.fn[e]=a,m._jQueryInterface},m}(jQuery));!function(r){var a="popover",l=".bs.popover",h=r.fn[a],c=new RegExp("(^|\\s)bs-popover\\S+","g"),u=r.extend({},s.Default,{placement:"right",trigger:"click",content:"",template:''}),d=r.extend({},s.DefaultType,{content:"(string|element|function)"}),f={FADE:"fade",SHOW:"show"},p={TITLE:".popover-header",CONTENT:".popover-body"},_={HIDE:"hide"+l,HIDDEN:"hidden"+l,SHOW:"show"+l,SHOWN:"shown"+l,INSERTED:"inserted"+l,CLICK:"click"+l,FOCUSIN:"focusin"+l,FOCUSOUT:"focusout"+l,MOUSEENTER:"mouseenter"+l,MOUSELEAVE:"mouseleave"+l},g=function(s){function h(){return n(this,h),t(this,s.apply(this,arguments))}return e(h,s),h.prototype.isWithContent=function(){return this.getTitle()||this._getContent()},h.prototype.addAttachmentClass=function(t){r(this.getTipElement()).addClass("bs-popover-"+t)},h.prototype.getTipElement=function(){return this.tip=this.tip||r(this.config.template)[0]},h.prototype.setContent=function(){var t=r(this.getTipElement());this.setElementContent(t.find(p.TITLE),this.getTitle()),this.setElementContent(t.find(p.CONTENT),this._getContent()),t.removeClass(f.FADE+" "+f.SHOW)},h.prototype._getContent=function(){return this.element.getAttribute("data-content")||("function"==typeof this.config.content?this.config.content.call(this.element):this.config.content)},h.prototype._cleanTipClass=function(){var t=r(this.getTipElement()),e=t.attr("class").match(c);null!==e&&e.length>0&&t.removeClass(e.join(""))},h._jQueryInterface=function(t){return this.each(function(){var e=r(this).data("bs.popover"),n="object"===(void 0===t?"undefined":i(t))?t:null;if((e||!/destroy|hide/.test(t))&&(e||(e=new h(this,n),r(this).data("bs.popover",e)),"string"==typeof t)){if(void 0===e[t])throw new Error('No method named "'+t+'"');e[t]()}})},o(h,null,[{key:"VERSION",get:function(){return"4.0.0-beta"}},{key:"Default",get:function(){return u}},{key:"NAME",get:function(){return a}},{key:"DATA_KEY",get:function(){return"bs.popover"}},{key:"Event",get:function(){return _}},{key:"EVENT_KEY",get:function(){return l}},{key:"DefaultType",get:function(){return d}}]),h}(s);r.fn[a]=g._jQueryInterface,r.fn[a].Constructor=g,r.fn[a].noConflict=function(){return r.fn[a]=h,g._jQueryInterface}}(jQuery)}();
--------------------------------------------------------------------------------
/GameStore.App/Services/Contracts/IGameService.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Services.Contracts
2 | {
3 | using Data.Models;
4 | using System;
5 | using System.Collections.Generic;
6 |
7 | public interface IGameService
8 | {
9 | void Create(
10 | string title,
11 | string description,
12 | string thumbnailUrl,
13 | decimal price,
14 | double size,
15 | string videoId,
16 | DateTime releaseDate);
17 |
18 | void Update(
19 | int id,
20 | string title,
21 | string description,
22 | string thumbnailUrl,
23 | decimal price,
24 | double size,
25 | string videoId,
26 | DateTime releaseDate);
27 |
28 | void Delete(int id);
29 |
30 | Game GetById(int id);
31 |
32 | bool Exists(int id);
33 |
34 | IEnumerable ByIds(IEnumerable ids);
35 |
36 | IEnumerable All(int? userId = null);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/GameStore.App/Services/Contracts/IOrderService.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Services.Contracts
2 | {
3 | using System.Collections.Generic;
4 |
5 | public interface IOrderService
6 | {
7 | void Purchase(int userId, IEnumerable gameIds);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/GameStore.App/Services/Contracts/IUserService.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Services.Contracts
2 | {
3 | public interface IUserService
4 | {
5 | bool Create(string email, string password, string name);
6 |
7 | bool UserExists(string email, string password);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/GameStore.App/Services/GameService.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Services
2 | {
3 | using AutoMapper.QueryableExtensions;
4 | using Contracts;
5 | using Data;
6 | using Data.Models;
7 | using System;
8 | using System.Collections.Generic;
9 | using System.Linq;
10 |
11 | public class GameService : IGameService
12 | {
13 | private readonly GameStoreDbContext db;
14 |
15 | public GameService(GameStoreDbContext db)
16 | {
17 | this.db = db;
18 | }
19 |
20 | public void Create(
21 | string title,
22 | string description,
23 | string thumbnailUrl,
24 | decimal price,
25 | double size,
26 | string videoId,
27 | DateTime releaseDate)
28 | {
29 | var game = new Game
30 | {
31 | Title = title,
32 | Description = description,
33 | Price = price,
34 | Size = size,
35 | ThumbnailUrl = thumbnailUrl,
36 | VideoId = videoId,
37 | ReleaseDate = releaseDate
38 | };
39 |
40 | this.db.Games.Add(game);
41 | this.db.SaveChanges();
42 | }
43 |
44 | public void Update(
45 | int id,
46 | string title,
47 | string description,
48 | string thumbnailUrl,
49 | decimal price,
50 | double size,
51 | string videoId,
52 | DateTime releaseDate)
53 | {
54 | var game = this.db.Games.Find(id);
55 |
56 | game.Title = title;
57 | game.Description = description;
58 | game.ThumbnailUrl = thumbnailUrl;
59 | game.Price = price;
60 | game.Size = size;
61 | game.VideoId = videoId;
62 | game.ReleaseDate = releaseDate;
63 |
64 | this.db.SaveChanges();
65 | }
66 |
67 | public void Delete(int id)
68 | {
69 | var game = this.db.Games.Find(id);
70 | this.db.Games.Remove(game);
71 |
72 | this.db.SaveChanges();
73 | }
74 |
75 | public Game GetById(int id)
76 | => this.db.Games.Find(id);
77 |
78 | public bool Exists(int id)
79 | => this.db.Games.Any(g => g.Id == id);
80 |
81 | public IEnumerable ByIds(IEnumerable ids)
82 | => this.db
83 | .Games
84 | .Where(g => ids.Contains(g.Id))
85 | .ProjectTo()
86 | .ToList();
87 |
88 | public IEnumerable All(int? userId = null)
89 | {
90 | var query = this.db.Games.AsQueryable();
91 |
92 | if (userId != null)
93 | {
94 | query = query.Where(g => g.Orders.Any(o => o.UserId == userId));
95 | }
96 |
97 | return query
98 | .ProjectTo()
99 | .ToList();
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/GameStore.App/Services/OrderService.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Services
2 | {
3 | using Contracts;
4 | using Data;
5 | using Data.Models;
6 | using System.Collections.Generic;
7 | using System.Linq;
8 |
9 | public class OrderService : IOrderService
10 | {
11 | private readonly GameStoreDbContext db;
12 |
13 | public OrderService(GameStoreDbContext db)
14 | {
15 | this.db = db;
16 | }
17 |
18 | public void Purchase(int userId, IEnumerable gameIds)
19 | {
20 | var alreadyOwnedIds = this.db
21 | .Orders
22 | .Where(o => o.UserId == userId
23 | && gameIds.Contains(o.GameId))
24 | .Select(o => o.GameId)
25 | .ToList();
26 |
27 | var newGamesIds = new List(gameIds);
28 |
29 | foreach (var gameId in alreadyOwnedIds)
30 | {
31 | newGamesIds.Remove(gameId);
32 | }
33 |
34 | foreach (var newGameId in newGamesIds)
35 | {
36 | var order = new Order
37 | {
38 | GameId = newGameId,
39 | UserId = userId
40 | };
41 |
42 | db.Orders.Add(order);
43 | }
44 |
45 | db.SaveChanges();
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/GameStore.App/Services/UserService.cs:
--------------------------------------------------------------------------------
1 | namespace GameStore.App.Services
2 | {
3 | using Contracts;
4 | using Data;
5 | using Data.Models;
6 | using System.Linq;
7 |
8 | public class UserService : IUserService
9 | {
10 | private readonly GameStoreDbContext db;
11 |
12 | public UserService(GameStoreDbContext db)
13 | {
14 | this.db = db;
15 | }
16 |
17 | public bool Create(string email, string password, string name)
18 | {
19 | if (this.db.Users.Any(u => u.Email == email))
20 | {
21 | return false;
22 | }
23 |
24 | var isAdmin = !db.Users.Any();
25 |
26 | var user = new User
27 | {
28 | Email = email,
29 | Name = name,
30 | Password = password,
31 | IsAdmin = isAdmin
32 | };
33 |
34 | db.Add(user);
35 | db.SaveChanges();
36 |
37 | return true;
38 | }
39 |
40 | public bool UserExists(string email, string password)
41 | => this.db
42 | .Users
43 | .Any(u => u.Email == email && u.Password == password);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/GameStore.App/Views/Admin/Add.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
Add Game
9 |
10 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/GameStore.App/Views/Admin/All.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
All Games –
5 | + Add
6 | Game
7 |
8 |
9 |
10 |
11 | # |
12 | Name |
13 | Size |
14 | Price |
15 | Actions |
16 |
17 |
18 |
19 | {{{games}}}
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/GameStore.App/Views/Admin/Delete.html:
--------------------------------------------------------------------------------
1 |
68 |
--------------------------------------------------------------------------------
/GameStore.App/Views/Admin/Edit.html:
--------------------------------------------------------------------------------
1 |
68 |
--------------------------------------------------------------------------------
/GameStore.App/Views/Home/Index.html:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/GameStore.App/Views/Layout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Game Store
5 |
6 |
7 |
8 |
9 |
10 |
11 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | {{{error}}}
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | {{{success}}}
75 |
76 |
77 |
78 |
79 | {{{content}}}
80 |
81 |
86 |
87 |
88 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/GameStore.App/Views/Orders/Cart.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
Your Cart
7 |
8 |
9 |
10 |
11 |
12 | {{{games}}}
13 |
14 |
15 |
16 |
Total Price - {{{total-price}}} €
17 |
18 |
19 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/GameStore.App/Views/Users/Login.html:
--------------------------------------------------------------------------------
1 |
2 |
24 |
25 |
--------------------------------------------------------------------------------
/GameStore.App/Views/Users/Register.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Ivaylo Kenov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GameStoreSimpleMvc
2 | Simple MVC application built from scratch as an exercise for https://softuni.bg/trainings/1736/c-sharp-web-development-basics-september-2017. Enjoy! :)
3 |
--------------------------------------------------------------------------------
/SimpleMvc.Framework/ActionResults/NotFoundResult.cs:
--------------------------------------------------------------------------------
1 | namespace SimpleMvc.Framework.ActionResults
2 | {
3 | using Contracts;
4 | using WebServer.Http.Contracts;
5 | using WebServer.Http.Response;
6 |
7 | public class NotFoundResult : IActionResult
8 | {
9 | public IHttpResponse Invoke()
10 | => new NotFoundResponse();
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/SimpleMvc.Framework/ActionResults/RedirectResult.cs:
--------------------------------------------------------------------------------
1 | namespace SimpleMvc.Framework.ActionResults
2 | {
3 | using Contracts;
4 | using WebServer.Http.Contracts;
5 | using WebServer.Http.Response;
6 |
7 | public class RedirectResult : IRedirectable
8 | {
9 | public RedirectResult(string redirectUrl)
10 | {
11 | this.RedirectUrl = redirectUrl;
12 | }
13 |
14 | public string RedirectUrl { get; set; }
15 |
16 | public IHttpResponse Invoke() => new RedirectResponse(this.RedirectUrl);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/SimpleMvc.Framework/ActionResults/ViewResult.cs:
--------------------------------------------------------------------------------
1 | namespace SimpleMvc.Framework.ActionResults
2 | {
3 | using Contracts;
4 | using WebServer.Enums;
5 | using WebServer.Http.Contracts;
6 | using WebServer.Http.Response;
7 |
8 | public class ViewResult : IViewable
9 | {
10 | public ViewResult(IRenderable view)
11 | {
12 | this.View = view;
13 | }
14 |
15 | public IRenderable View { get; set; }
16 |
17 | public IHttpResponse Invoke()
18 | => new ContentResponse(
19 | HttpStatusCode.Ok,
20 | this.View.Render());
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/SimpleMvc.Framework/Attributes/Methods/HttpGetAttribute.cs:
--------------------------------------------------------------------------------
1 | namespace SimpleMvc.Framework.Attributes.Methods
2 | {
3 | public class HttpGetAttribute : HttpMethodAttribute
4 | {
5 | public override bool IsValid(string requestMethod)
6 | => requestMethod.ToUpper() == "GET";
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/SimpleMvc.Framework/Attributes/Methods/HttpMethodAttribute.cs:
--------------------------------------------------------------------------------
1 | namespace SimpleMvc.Framework.Attributes.Methods
2 | {
3 | using System;
4 |
5 | public abstract class HttpMethodAttribute : Attribute
6 | {
7 | public abstract bool IsValid(string requestMethod);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/SimpleMvc.Framework/Attributes/Methods/HttpPostAttribute.cs:
--------------------------------------------------------------------------------
1 | namespace SimpleMvc.Framework.Attributes.Methods
2 | {
3 | public class HttpPostAttribute : HttpMethodAttribute
4 | {
5 | public override bool IsValid(string requestMethod)
6 | => requestMethod.ToUpper() == "POST";
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/SimpleMvc.Framework/Attributes/Validation/NumberRangeAttribute.cs:
--------------------------------------------------------------------------------
1 | namespace SimpleMvc.Framework.Attributes.Validation
2 | {
3 | public class NumberRangeAttribute : PropertyValidationAttribute
4 | {
5 | private readonly double minimum;
6 | private readonly double maximum;
7 |
8 | public NumberRangeAttribute(double minimum, double maximum)
9 | {
10 | this.minimum = minimum;
11 | this.maximum = maximum;
12 | }
13 |
14 | public override bool IsValid(object value)
15 | {
16 | var valueAsDouble = value as double?;
17 | if (valueAsDouble == null)
18 | {
19 | return true;
20 | }
21 |
22 | return minimum <= valueAsDouble && valueAsDouble <= maximum;
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/SimpleMvc.Framework/Attributes/Validation/PropertyValidationAttribute.cs:
--------------------------------------------------------------------------------
1 | namespace SimpleMvc.Framework.Attributes.Validation
2 | {
3 | using System;
4 |
5 | [AttributeUsage(AttributeTargets.Property, Inherited = true)]
6 | public abstract class PropertyValidationAttribute : Attribute
7 | {
8 | public abstract bool IsValid(object value);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/SimpleMvc.Framework/Attributes/Validation/RegexAttribute.cs:
--------------------------------------------------------------------------------
1 | namespace SimpleMvc.Framework.Attributes.Validation
2 | {
3 | using System.Text.RegularExpressions;
4 |
5 | public class RegexAttribute : PropertyValidationAttribute
6 | {
7 | private readonly string pattern;
8 |
9 | public RegexAttribute(string pattern)
10 | {
11 | this.pattern = $"^{pattern}$";
12 | }
13 |
14 | public override bool IsValid(object value)
15 | {
16 | var valueAsString = value as string;
17 | if (valueAsString == null)
18 | {
19 | return true;
20 | }
21 |
22 | return Regex.IsMatch(valueAsString, this.pattern);
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/SimpleMvc.Framework/Contracts/IActionResult.cs:
--------------------------------------------------------------------------------
1 | namespace SimpleMvc.Framework.Contracts
2 | {
3 | using WebServer.Http.Contracts;
4 |
5 | public interface IActionResult
6 | {
7 | IHttpResponse Invoke();
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/SimpleMvc.Framework/Contracts/IRedirectable.cs:
--------------------------------------------------------------------------------
1 | namespace SimpleMvc.Framework.Contracts
2 | {
3 | public interface IRedirectable : IActionResult
4 | {
5 | string RedirectUrl { get; set; }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/SimpleMvc.Framework/Contracts/IRenderable.cs:
--------------------------------------------------------------------------------
1 | namespace SimpleMvc.Framework.Contracts
2 | {
3 | public interface IRenderable
4 | {
5 | string Render();
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/SimpleMvc.Framework/Contracts/IViewable.cs:
--------------------------------------------------------------------------------
1 | namespace SimpleMvc.Framework.Contracts
2 | {
3 | public interface IViewable : IActionResult
4 | {
5 | IRenderable View { get; set; }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/SimpleMvc.Framework/Controllers/Controller.cs:
--------------------------------------------------------------------------------
1 | namespace SimpleMvc.Framework.Controllers
2 | {
3 | using ActionResults;
4 | using Attributes.Validation;
5 | using Contracts;
6 | using Helpers;
7 | using Models;
8 | using Security;
9 | using System.Linq;
10 | using System.Reflection;
11 | using System.Runtime.CompilerServices;
12 | using ViewEngine;
13 | using WebServer.Http;
14 | using WebServer.Http.Contracts;
15 |
16 | public abstract class Controller
17 | {
18 | public Controller()
19 | {
20 | this.ViewModel = new ViewModel();
21 | this.User = new Authentication();
22 | }
23 |
24 | protected ViewModel ViewModel { get; private set; }
25 |
26 | protected internal IHttpRequest Request { get; internal set; }
27 |
28 | protected internal Authentication User { get; private set; }
29 |
30 | protected IViewable View([CallerMemberName]string caller = "")
31 | {
32 | var controllerName = ControllerHelpers.GetControllerName(this);
33 |
34 | var viewFullQualifiedName = ControllerHelpers
35 | .GetViewFullQualifiedName(controllerName, caller);
36 |
37 | var view = new View(viewFullQualifiedName, this.ViewModel.Data);
38 |
39 | return new ViewResult(view);
40 | }
41 |
42 | protected IRedirectable Redirect(string redirectUrl)
43 | => new RedirectResult(redirectUrl);
44 |
45 | protected IActionResult NotFound()
46 | => new NotFoundResult();
47 |
48 | protected bool IsValidModel(object model)
49 | {
50 | var properties = model.GetType().GetProperties();
51 |
52 | foreach (var property in properties)
53 | {
54 | var attributes = property
55 | .GetCustomAttributes()
56 | .Where(a => a is PropertyValidationAttribute)
57 | .Cast();
58 |
59 | foreach (var attribute in attributes)
60 | {
61 | var propertyValue = property.GetValue(model);
62 |
63 | if (!attribute.IsValid(propertyValue))
64 | {
65 | return false;
66 | }
67 | }
68 | }
69 |
70 | return true;
71 | }
72 |
73 | protected void SignIn(string name)
74 | {
75 | this.Request.Session.Add(SessionStore.CurrentUserKey, name);
76 | }
77 |
78 | protected void SignOut()
79 | {
80 | this.Request.Session.Remove(SessionStore.CurrentUserKey);
81 | }
82 |
83 | protected internal virtual void InitializeController()
84 | {
85 | var user = this.Request
86 | .Session
87 | .Get(SessionStore.CurrentUserKey);
88 |
89 | if (user != null)
90 | {
91 | this.User = new Authentication(user);
92 | }
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/SimpleMvc.Framework/Errors/Error.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Simple MVC Framework
7 |
8 |
9 | Ooops! There is a problem with your request!
10 |
11 | {{{error}}}
12 |
13 |
--------------------------------------------------------------------------------
/SimpleMvc.Framework/Helpers/ControllerHelpers.cs:
--------------------------------------------------------------------------------
1 | namespace SimpleMvc.Framework.Helpers
2 | {
3 | public static class ControllerHelpers
4 | {
5 | public static string GetControllerName(object controller)
6 | => controller.GetType()
7 | .Name
8 | .Replace(MvcContext.Get.ControllerSuffix, string.Empty);
9 |
10 | public static string GetViewFullQualifiedName(
11 | string controller,
12 | string action)
13 | => string.Format(
14 | "{0}\\{1}\\{2}",
15 | MvcContext.Get.ViewsFolder,
16 | controller,
17 | action);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/SimpleMvc.Framework/Helpers/StringExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace SimpleMvc.Framework.Helpers
2 | {
3 | using System.Linq;
4 |
5 | public static class StringExtensions
6 | {
7 | public static string Capitalize(this string input)
8 | {
9 | if (input == null || input.Length == 0)
10 | {
11 | return input;
12 | }
13 |
14 | var firstLetter = char.ToUpper(input.First());
15 | var rest = input.Substring(1);
16 |
17 | return $"{firstLetter}{rest}";
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/SimpleMvc.Framework/Models/ViewModel.cs:
--------------------------------------------------------------------------------
1 | namespace SimpleMvc.Framework.Models
2 | {
3 | using System.Collections.Generic;
4 |
5 | public class ViewModel
6 | {
7 | public ViewModel()
8 | {
9 | this.Data = new Dictionary();
10 | }
11 |
12 | public IDictionary Data { get; private set; }
13 |
14 | public string this[string key]
15 | {
16 | get => this.Data[key];
17 | set => this.Data[key] = value;
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/SimpleMvc.Framework/MvcContext.cs:
--------------------------------------------------------------------------------
1 | namespace SimpleMvc.Framework
2 | {
3 | public class MvcContext
4 | {
5 | private static MvcContext instance;
6 |
7 | private MvcContext() { }
8 |
9 | public static MvcContext Get
10 | => instance == null ? (instance = new MvcContext()) : instance;
11 |
12 | public string AssemblyName { get; set; }
13 |
14 | public string ControllersFolder { get; set; } = "Controllers";
15 |
16 | public string ControllerSuffix { get; set; } = "Controller";
17 |
18 | public string ViewsFolder { get; set; } = "Views";
19 |
20 | public string ModelsFolder { get; set; } = "Models";
21 |
22 | public string ResourcesFolder { get; set; } = "Resources";
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/SimpleMvc.Framework/MvcEngine.cs:
--------------------------------------------------------------------------------
1 | namespace SimpleMvc.Framework
2 | {
3 | using System;
4 | using System.Reflection;
5 | using WebServer;
6 |
7 | public static class MvcEngine
8 | {
9 | public static void Run(WebServer server)
10 | {
11 | RegisterAssemblyName();
12 |
13 | try
14 | {
15 | server.Run();
16 | }
17 | catch (Exception ex)
18 | {
19 | Console.WriteLine(ex.Message);
20 | }
21 | }
22 |
23 | private static void RegisterAssemblyName()
24 | {
25 | MvcContext.Get.AssemblyName = Assembly
26 | .GetEntryAssembly()
27 | .GetName()
28 | .Name;
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/SimpleMvc.Framework/Routers/ControllerRouter.cs:
--------------------------------------------------------------------------------
1 | namespace SimpleMvc.Framework.Routers
2 | {
3 | using Framework.Attributes.Methods;
4 | using Framework.Helpers;
5 | using SimpleMvc.Framework.Contracts;
6 | using SimpleMvc.Framework.Controllers;
7 | using System;
8 | using System.Collections.Generic;
9 | using System.Linq;
10 | using System.Reflection;
11 | using WebServer.Contracts;
12 | using WebServer.Exceptions;
13 | using WebServer.Http.Contracts;
14 | using WebServer.Http.Response;
15 |
16 | public class ControllerRouter : IHandleable
17 | {
18 | private IDictionary getParameters;
19 | private IDictionary postParameters;
20 | private string requestMethod;
21 | private Controller controllerInstance;
22 | private string controllerName;
23 | private string actionName;
24 | private object[] methodParameters;
25 |
26 | public IHttpResponse Handle(IHttpRequest request)
27 | {
28 | this.controllerInstance = null;
29 | this.actionName = null;
30 | this.methodParameters = null;
31 |
32 | this.getParameters = new Dictionary(request.UrlParameters);
33 | this.postParameters = new Dictionary(request.FormData);
34 | this.requestMethod = request.Method.ToString().ToUpper();
35 |
36 | this.PrepareControllerAndActionNames(request);
37 |
38 | var methodInfo = this.GetActionForExecution();
39 |
40 | if (methodInfo == null)
41 | {
42 | return new NotFoundResponse();
43 | }
44 |
45 | this.PrepareMethodParameters(methodInfo);
46 |
47 | try
48 | {
49 | if (this.controllerInstance != null)
50 | {
51 | this.controllerInstance.Request = request;
52 | this.controllerInstance.InitializeController();
53 | }
54 |
55 | return this.GetResponse(methodInfo, this.controllerInstance);
56 | }
57 | catch (Exception ex)
58 | {
59 | return new InternalServerErrorResponse(ex);
60 | }
61 | }
62 |
63 | protected virtual Controller CreateController(Type controllerType)
64 | => Activator.CreateInstance(controllerType) as Controller;
65 |
66 | private void PrepareControllerAndActionNames(IHttpRequest request)
67 | {
68 | var pathParts = request.Path.Split(
69 | new[] { '/', '?' },
70 | StringSplitOptions.RemoveEmptyEntries);
71 |
72 | if (pathParts.Length < 2)
73 | {
74 | if (request.Path == "/")
75 | {
76 | this.controllerName = "HomeController";
77 | this.actionName = "Index";
78 |
79 | return;
80 | }
81 | else
82 | {
83 | BadRequestException.ThrowFromInvalidRequest();
84 | }
85 | }
86 |
87 | this.controllerName = $"{pathParts[0].Capitalize()}{MvcContext.Get.ControllerSuffix}";
88 | this.actionName = pathParts[1].Capitalize();
89 | }
90 |
91 | private MethodInfo GetActionForExecution()
92 | {
93 | foreach (var method in this.GetSuitableMethods())
94 | {
95 | var httpMethodAttributes = method
96 | .GetCustomAttributes()
97 | .Where(a => a is HttpMethodAttribute)
98 | .Cast();
99 |
100 | if (!httpMethodAttributes.Any() && this.requestMethod == "GET")
101 | {
102 | return method;
103 | }
104 |
105 | foreach (var httpMethodAttribute in httpMethodAttributes)
106 | {
107 | if (httpMethodAttribute.IsValid(this.requestMethod))
108 | {
109 | return method;
110 | }
111 | }
112 | }
113 |
114 | return null;
115 | }
116 |
117 | private IEnumerable GetSuitableMethods()
118 | {
119 | var controller = this.GetControllerInstance();
120 |
121 | if (controller == null)
122 | {
123 | return new MethodInfo[0];
124 | }
125 |
126 | return controller
127 | .GetType()
128 | .GetMethods()
129 | .Where(m => m.Name.ToLower() == actionName.ToLower());
130 | }
131 |
132 | private object GetControllerInstance()
133 | {
134 | if (this.controllerInstance != null)
135 | {
136 | return controllerInstance;
137 | }
138 |
139 | var controllerFullQualifiedName = string.Format(
140 | "{0}.{1}.{2}, {0}",
141 | MvcContext.Get.AssemblyName,
142 | MvcContext.Get.ControllersFolder,
143 | this.controllerName);
144 |
145 | var controllerType = Type.GetType(controllerFullQualifiedName);
146 |
147 | if (controllerType == null)
148 | {
149 | return null;
150 | }
151 |
152 | this.controllerInstance = this.CreateController(controllerType);
153 | return this.controllerInstance;
154 | }
155 |
156 | private void PrepareMethodParameters(MethodInfo methodInfo)
157 | {
158 | var parameters = methodInfo.GetParameters();
159 |
160 | this.methodParameters = new object[parameters.Length];
161 |
162 | for (var i = 0; i < parameters.Length; i++)
163 | {
164 | var parameter = parameters[i];
165 |
166 | if (parameter.ParameterType.IsPrimitive
167 | || parameter.ParameterType == typeof(string))
168 | {
169 | this.ProcessPrimitiveParameter(parameter, i);
170 | }
171 | else
172 | {
173 | this.ProcessModelParameter(parameter, i);
174 | }
175 | }
176 | }
177 |
178 | private void ProcessPrimitiveParameter(ParameterInfo parameter, int index)
179 | {
180 | var getParameterValue = this.getParameters[parameter.Name];
181 |
182 | var value = Convert.ChangeType(
183 | getParameterValue,
184 | parameter.ParameterType);
185 |
186 | this.methodParameters[index] = value;
187 | }
188 |
189 | private void ProcessModelParameter(ParameterInfo parameter, int index)
190 | {
191 | var modelType = parameter.ParameterType;
192 | var modelInstance = Activator.CreateInstance(modelType);
193 |
194 | var modelProperties = modelType.GetProperties();
195 |
196 | foreach (var modelProperty in modelProperties)
197 | {
198 | var postParameterValue = this.postParameters[modelProperty.Name];
199 |
200 | var value = Convert.ChangeType(
201 | postParameterValue,
202 | modelProperty.PropertyType);
203 |
204 | modelProperty.SetValue(
205 | modelInstance,
206 | value);
207 | }
208 |
209 | this.methodParameters[index] = Convert.ChangeType(
210 | modelInstance,
211 | modelType);
212 | }
213 |
214 | private IHttpResponse GetResponse(MethodInfo method, object controller)
215 | {
216 | var actionResult = method.Invoke(controller, this.methodParameters)
217 | as IActionResult;
218 |
219 | if (actionResult == null)
220 | {
221 | var methodResultAsHttpResponse = actionResult as IHttpResponse;
222 |
223 | if (methodResultAsHttpResponse != null)
224 | {
225 | return methodResultAsHttpResponse;
226 | }
227 | else
228 | {
229 | throw new InvalidOperationException("Controller actions should return either IActionResult or IHttpResponse.");
230 | }
231 | }
232 |
233 | return actionResult.Invoke();
234 | }
235 | }
236 | }
237 |
--------------------------------------------------------------------------------
/SimpleMvc.Framework/Routers/ResourceRouter.cs:
--------------------------------------------------------------------------------
1 | namespace SimpleMvc.Framework.Routers
2 | {
3 | using System.IO;
4 | using System.Linq;
5 | using WebServer.Contracts;
6 | using WebServer.Enums;
7 | using WebServer.Http.Contracts;
8 | using WebServer.Http.Response;
9 |
10 | public class ResourceRouter : IHandleable
11 | {
12 | public IHttpResponse Handle(IHttpRequest request)
13 | {
14 | var fileName = request.Path.Split('/').Last();
15 | var fileExtension = fileName.Split('.').Last();
16 |
17 | try
18 | {
19 | var fileContents = this.ReadFile(fileName, fileExtension);
20 |
21 | return new FileResponse(HttpStatusCode.Found, fileContents);
22 | }
23 | catch
24 | {
25 | return new NotFoundResponse();
26 | }
27 | }
28 |
29 | private byte[] ReadFile(string fileName, string fileExtension)
30 | => File.ReadAllBytes(string.Format("{0}\\{1}\\{2}",
31 | MvcContext.Get.ResourcesFolder,
32 | fileExtension,
33 | fileName));
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/SimpleMvc.Framework/Security/Authentication.cs:
--------------------------------------------------------------------------------
1 | namespace SimpleMvc.Framework.Security
2 | {
3 | public class Authentication
4 | {
5 | internal Authentication()
6 | {
7 | this.IsAuthenticated = false;
8 | }
9 |
10 | internal Authentication(string name)
11 | {
12 | this.IsAuthenticated = true;
13 | this.Name = name;
14 | }
15 |
16 | public bool IsAuthenticated { get; private set; }
17 |
18 | public string Name { get; private set; }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/SimpleMvc.Framework/SimpleMvc.Framework.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/SimpleMvc.Framework/ViewEngine/View.cs:
--------------------------------------------------------------------------------
1 | namespace SimpleMvc.Framework.ViewEngine
2 | {
3 | using Contracts;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.IO;
7 |
8 | public class View : IRenderable
9 | {
10 | public const string BaseLayoutFileName = "Layout";
11 |
12 | public const string ContentPlaceholder = "{{{content}}}";
13 |
14 | public const string FileExtension = ".html";
15 |
16 | public const string LocalErrorPath = "\\SimpleMvc.Framework\\Errors\\Error.html";
17 |
18 | private readonly string templateFullQualifiedName;
19 |
20 | private readonly IDictionary viewData;
21 |
22 | public View(string templateFullQualifiedName, IDictionary viewData)
23 | {
24 | this.templateFullQualifiedName = templateFullQualifiedName;
25 | this.viewData = viewData;
26 | }
27 |
28 | public string Render()
29 | {
30 | var fileHtml = this.ReadFile();
31 |
32 | if (this.viewData.Any())
33 | {
34 | foreach (var data in this.viewData)
35 | {
36 | fileHtml = fileHtml.Replace($"{{{{{{{data.Key}}}}}}}", data.Value);
37 | }
38 | }
39 |
40 | return fileHtml;
41 | }
42 |
43 | private string ReadFile()
44 | {
45 | var layoutHtml = this.ReadLayoutFile();
46 |
47 | var templateFullFilePath = $"{this.templateFullQualifiedName}{FileExtension}";
48 |
49 | if (!File.Exists(templateFullFilePath))
50 | {
51 | this.viewData["error"] = $"The requested view ({templateFullFilePath}) could not be found!";
52 | return this.GetErrorHtml();
53 | }
54 |
55 | var templateHtml = File.ReadAllText(templateFullFilePath);
56 |
57 | return layoutHtml.Replace(ContentPlaceholder, templateHtml);
58 | }
59 |
60 | private string ReadLayoutFile()
61 | {
62 | var layoutHtmlFile = string.Format(
63 | "{0}\\{1}{2}",
64 | MvcContext.Get.ViewsFolder,
65 | BaseLayoutFileName,
66 | FileExtension);
67 |
68 | if (!File.Exists(layoutHtmlFile))
69 | {
70 | this.viewData["error"] = $"Layout view ({layoutHtmlFile}) could not be found!";
71 | return this.GetErrorHtml();
72 | }
73 |
74 | return File.ReadAllText(layoutHtmlFile);
75 | }
76 |
77 | private string GetErrorPath()
78 | {
79 | var currentDirectory = Directory.GetCurrentDirectory();
80 | var parentDirectory = Directory.GetParent(currentDirectory);
81 | var parentDirectoryPath = parentDirectory.FullName;
82 |
83 | return $"{parentDirectoryPath}{LocalErrorPath}";
84 | }
85 |
86 | private string GetErrorHtml()
87 | {
88 | var errorPath = this.GetErrorPath();
89 | var errorHtml = File.ReadAllText(errorPath);
90 | return errorHtml;
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/SimpleMvc.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.26730.16
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleMvc.Framework", "SimpleMvc.Framework\SimpleMvc.Framework.csproj", "{CC2E0567-0B1E-46D6-B06D-BE662260D137}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebServer", "WebServer\WebServer.csproj", "{537B17DC-C5A8-4F13-9D20-A2B15F069B14}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameStore.App", "GameStore.App\GameStore.App.csproj", "{C3819F45-2517-4AF9-877C-31C3683E3DF2}"
11 | EndProject
12 | Global
13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
14 | Debug|Any CPU = Debug|Any CPU
15 | Release|Any CPU = Release|Any CPU
16 | EndGlobalSection
17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
18 | {CC2E0567-0B1E-46D6-B06D-BE662260D137}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
19 | {CC2E0567-0B1E-46D6-B06D-BE662260D137}.Debug|Any CPU.Build.0 = Debug|Any CPU
20 | {CC2E0567-0B1E-46D6-B06D-BE662260D137}.Release|Any CPU.ActiveCfg = Release|Any CPU
21 | {CC2E0567-0B1E-46D6-B06D-BE662260D137}.Release|Any CPU.Build.0 = Release|Any CPU
22 | {537B17DC-C5A8-4F13-9D20-A2B15F069B14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
23 | {537B17DC-C5A8-4F13-9D20-A2B15F069B14}.Debug|Any CPU.Build.0 = Debug|Any CPU
24 | {537B17DC-C5A8-4F13-9D20-A2B15F069B14}.Release|Any CPU.ActiveCfg = Release|Any CPU
25 | {537B17DC-C5A8-4F13-9D20-A2B15F069B14}.Release|Any CPU.Build.0 = Release|Any CPU
26 | {C3819F45-2517-4AF9-877C-31C3683E3DF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27 | {C3819F45-2517-4AF9-877C-31C3683E3DF2}.Debug|Any CPU.Build.0 = Debug|Any CPU
28 | {C3819F45-2517-4AF9-877C-31C3683E3DF2}.Release|Any CPU.ActiveCfg = Release|Any CPU
29 | {C3819F45-2517-4AF9-877C-31C3683E3DF2}.Release|Any CPU.Build.0 = Release|Any CPU
30 | EndGlobalSection
31 | GlobalSection(SolutionProperties) = preSolution
32 | HideSolutionNode = FALSE
33 | EndGlobalSection
34 | GlobalSection(ExtensibilityGlobals) = postSolution
35 | SolutionGuid = {3ABA898C-743A-4BCB-94FE-871C34896B4B}
36 | EndGlobalSection
37 | EndGlobal
38 |
--------------------------------------------------------------------------------
/WebServer/Common/CoreValidator.cs:
--------------------------------------------------------------------------------
1 | namespace WebServer.Common
2 | {
3 | using System;
4 |
5 | public static class CoreValidator
6 | {
7 | public static void ThrowIfNull(object obj, string name)
8 | {
9 | if (obj == null)
10 | {
11 | throw new ArgumentNullException(name);
12 | }
13 | }
14 |
15 | public static void ThrowIfNullOrEmpty(string text, string name)
16 | {
17 | if (string.IsNullOrEmpty(text))
18 | {
19 | throw new ArgumentException($"{name} cannot be null or empty.", name);
20 | }
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/WebServer/Common/InternalServerErrorView.cs:
--------------------------------------------------------------------------------
1 | namespace WebServer.Common
2 | {
3 | using System;
4 | using Contracts;
5 |
6 | public class InternalServerErrorView : IView
7 | {
8 | private readonly Exception exception;
9 |
10 | public InternalServerErrorView(Exception exception)
11 | {
12 | this.exception = exception;
13 | }
14 |
15 | public string View()
16 | {
17 | return $"{this.exception.Message}
{this.exception.StackTrace}
";
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/WebServer/Common/NotFoundView.cs:
--------------------------------------------------------------------------------
1 | namespace WebServer.Common
2 | {
3 | using Contracts;
4 |
5 | public class NotFoundView : IView
6 | {
7 | public string View()
8 | {
9 | return "404 This page or resource you are trying to access does not exist :/
";
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/WebServer/ConnectionHandler.cs:
--------------------------------------------------------------------------------
1 | namespace WebServer
2 | {
3 | using Common;
4 | using Http;
5 | using Http.Contracts;
6 | using System;
7 | using System.Net.Sockets;
8 | using System.Text;
9 | using System.Threading.Tasks;
10 | using Contracts;
11 | using System.Linq;
12 | using global::WebServer.Http.Response;
13 |
14 | public class ConnectionHandler
15 | {
16 | private readonly Socket client;
17 | private readonly IHandleable mvcRequestHandler;
18 | private readonly IHandleable resourceHandler;
19 |
20 | public ConnectionHandler(
21 | Socket client,
22 | IHandleable mvcRequestHandler,
23 | IHandleable resourceHandler)
24 | {
25 | CoreValidator.ThrowIfNull(client, nameof(client));
26 | CoreValidator.ThrowIfNull(mvcRequestHandler, nameof(mvcRequestHandler));
27 | CoreValidator.ThrowIfNull(resourceHandler, nameof(resourceHandler));
28 |
29 | this.client = client;
30 | this.mvcRequestHandler = mvcRequestHandler;
31 | this.resourceHandler = resourceHandler;
32 | }
33 |
34 | public async Task ProcessRequestAsync()
35 | {
36 | var httpRequest = this.ReadRequest();
37 |
38 | if (httpRequest != null)
39 | {
40 | var httpResponse = this.HandleRequest(httpRequest);
41 |
42 | var responseBytes = this.GetResponseBytes(httpResponse);
43 |
44 | var byteSegments = new ArraySegment(responseBytes);
45 |
46 | await this.client.SendAsync(byteSegments, SocketFlags.None);
47 |
48 | Console.WriteLine($"-----REQUEST-----");
49 | Console.WriteLine(httpRequest);
50 | Console.WriteLine($"-----RESPONSE-----");
51 | Console.WriteLine(httpResponse);
52 | Console.WriteLine();
53 | }
54 |
55 | this.client.Shutdown(SocketShutdown.Both);
56 | }
57 |
58 | private IHttpRequest ReadRequest()
59 | {
60 | var result = new StringBuilder();
61 |
62 | var data = new ArraySegment(new byte[1024]);
63 |
64 | while (true)
65 | {
66 | int numberOfBytesRead = this.client.Receive(data.Array, SocketFlags.None);
67 |
68 | if (numberOfBytesRead == 0)
69 | {
70 | break;
71 | }
72 |
73 | var bytesAsString = Encoding.UTF8.GetString(data.Array, 0, numberOfBytesRead);
74 |
75 | result.Append(bytesAsString);
76 |
77 | if (numberOfBytesRead < 1023)
78 | {
79 | break;
80 | }
81 | }
82 |
83 | if (result.Length == 0)
84 | {
85 | return null;
86 | }
87 |
88 | return new HttpRequest(result.ToString());
89 | }
90 |
91 | private IHttpResponse HandleRequest(IHttpRequest httpRequest)
92 | {
93 | if (httpRequest.Path.Contains("."))
94 | {
95 | return this.resourceHandler.Handle(httpRequest);
96 | }
97 | else
98 | {
99 | string sessionIdToSend = this.SetRequestSession(httpRequest);
100 | var response = this.mvcRequestHandler.Handle(httpRequest);
101 | this.SetResponseSession(response, sessionIdToSend);
102 | return response;
103 | }
104 | }
105 |
106 | private string SetRequestSession(IHttpRequest request)
107 | {
108 | if (!request.Cookies.ContainsKey(SessionStore.SessionCookieKey))
109 | {
110 | var sessionId = Guid.NewGuid().ToString();
111 |
112 | request.Session = SessionStore.Get(sessionId);
113 |
114 | return sessionId;
115 | }
116 |
117 | return null;
118 | }
119 |
120 | private void SetResponseSession(IHttpResponse response, string sessionIdToSend)
121 | {
122 | if (sessionIdToSend != null)
123 | {
124 | response.Headers.Add(
125 | HttpHeader.SetCookie,
126 | $"{SessionStore.SessionCookieKey}={sessionIdToSend}; HttpOnly; path=/");
127 | }
128 | }
129 |
130 |
131 | private byte[] GetResponseBytes(IHttpResponse httpResponse)
132 | {
133 | var responseBytes = Encoding.UTF8
134 | .GetBytes(httpResponse.ToString())
135 | .ToList();
136 |
137 | var fileResponse = httpResponse as FileResponse;
138 | if (fileResponse != null)
139 | {
140 | responseBytes.AddRange(fileResponse.FileData);
141 | }
142 |
143 | return responseBytes.ToArray();
144 | }
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/WebServer/Contracts/IHandleable.cs:
--------------------------------------------------------------------------------
1 | namespace WebServer.Contracts
2 | {
3 | using Http.Contracts;
4 |
5 | public interface IHandleable
6 | {
7 | IHttpResponse Handle(IHttpRequest request);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/WebServer/Contracts/IRunnable.cs:
--------------------------------------------------------------------------------
1 | namespace WebServer.Contracts
2 | {
3 | public interface IRunnable
4 | {
5 | void Run();
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/WebServer/Contracts/IView.cs:
--------------------------------------------------------------------------------
1 | namespace WebServer.Contracts
2 | {
3 | public interface IView
4 | {
5 | string View();
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/WebServer/Enums/HttpRequestMethod.cs:
--------------------------------------------------------------------------------
1 | namespace WebServer.Enums
2 | {
3 | public enum HttpRequestMethod
4 | {
5 | Get,
6 | Post
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/WebServer/Enums/HttpStatusCode.cs:
--------------------------------------------------------------------------------
1 | namespace WebServer.Enums
2 | {
3 | public enum HttpStatusCode
4 | {
5 | Ok = 200,
6 | MovedPermanently = 301,
7 | Found = 302,
8 | MovedTemporary = 303,
9 | BadRequest = 400,
10 | NotAuthorized = 401,
11 | NotFound = 404,
12 | InternalServerError = 500
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/WebServer/Exceptions/BadRequestException.cs:
--------------------------------------------------------------------------------
1 | namespace WebServer.Exceptions
2 | {
3 | using System;
4 |
5 | public class BadRequestException : Exception
6 | {
7 | private const string InvalidRequestMessage = "Request is not valid.";
8 |
9 | public static object ThrowFromInvalidRequest()
10 | => throw new BadRequestException(InvalidRequestMessage);
11 |
12 | public BadRequestException(string message)
13 | : base(message)
14 | {
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/WebServer/Exceptions/InvalidResponseException.cs:
--------------------------------------------------------------------------------
1 | namespace WebServer.Exceptions
2 | {
3 | using System;
4 |
5 | public class InvalidResponseException : Exception
6 | {
7 | public InvalidResponseException(string message)
8 | : base(message)
9 | {
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/WebServer/Http/Contracts/IHttpCookieCollection.cs:
--------------------------------------------------------------------------------
1 | namespace WebServer.Http.Contracts
2 | {
3 | using System.Collections.Generic;
4 |
5 | public interface IHttpCookieCollection : IEnumerable
6 | {
7 | void Add(HttpCookie cookie);
8 |
9 | void Add(string key, string value);
10 |
11 | bool ContainsKey(string key);
12 |
13 | HttpCookie Get(string key);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/WebServer/Http/Contracts/IHttpHeaderCollection.cs:
--------------------------------------------------------------------------------
1 | namespace WebServer.Http.Contracts
2 | {
3 | using System.Collections.Generic;
4 |
5 | public interface IHttpHeaderCollection : IEnumerable>
6 | {
7 | void Add(HttpHeader header);
8 |
9 | void Add(string key, string value);
10 |
11 | bool ContainsKey(string key);
12 |
13 | ICollection Get(string key);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/WebServer/Http/Contracts/IHttpRequest.cs:
--------------------------------------------------------------------------------
1 | namespace WebServer.Http.Contracts
2 | {
3 | using Enums;
4 | using System.Collections.Generic;
5 |
6 | public interface IHttpRequest
7 | {
8 | IDictionary FormData { get; }
9 |
10 | IHttpHeaderCollection Headers { get; }
11 |
12 | IHttpCookieCollection Cookies { get; }
13 |
14 | string Path { get; }
15 |
16 | HttpRequestMethod Method { get; }
17 |
18 | string Url { get; }
19 |
20 | IDictionary UrlParameters { get; }
21 |
22 | IHttpSession Session { get; set; }
23 |
24 | void AddUrlParameter(string key, string value);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/WebServer/Http/Contracts/IHttpResponse.cs:
--------------------------------------------------------------------------------
1 | namespace WebServer.Http.Contracts
2 | {
3 | using Enums;
4 |
5 | public interface IHttpResponse
6 | {
7 | HttpStatusCode StatusCode { get; }
8 |
9 | IHttpHeaderCollection Headers { get; }
10 |
11 | IHttpCookieCollection Cookies { get; }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/WebServer/Http/Contracts/IHttpSession.cs:
--------------------------------------------------------------------------------
1 | namespace WebServer.Http.Contracts
2 | {
3 | public interface IHttpSession
4 | {
5 | string Id { get; }
6 |
7 | object Get(string key);
8 |
9 | T Get(string key);
10 |
11 | bool Contains(string key);
12 |
13 | void Add(string key, object value);
14 |
15 | void Remove(string key);
16 |
17 | void Clear();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/WebServer/Http/HttpCookie.cs:
--------------------------------------------------------------------------------
1 | namespace WebServer.Http
2 | {
3 | using Common;
4 | using System;
5 |
6 | public class HttpCookie
7 | {
8 | // expires is in days
9 | public HttpCookie(string key, string value, int expires = 3)
10 | {
11 | CoreValidator.ThrowIfNullOrEmpty(key, nameof(key));
12 | CoreValidator.ThrowIfNullOrEmpty(value, nameof(value));
13 |
14 | this.Key = key;
15 | this.Value = value;
16 |
17 | this.Expires = DateTime.UtcNow.AddDays(expires);
18 | }
19 |
20 | public HttpCookie(string key, string value, bool isNew, int expires = 3)
21 | : this(key, value, expires)
22 | {
23 | this.IsNew = isNew;
24 | }
25 |
26 | public string Key { get; private set; }
27 |
28 | public string Value { get; private set; }
29 |
30 | public DateTime Expires { get; private set; }
31 |
32 | public bool IsNew { get; private set; } = true;
33 |
34 | public override string ToString()
35 | => $"{this.Key}={this.Value}; Expires={this.Expires.ToLongTimeString()}";
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/WebServer/Http/HttpCookieCollection.cs:
--------------------------------------------------------------------------------
1 | namespace WebServer.Http
2 | {
3 | using Common;
4 | using Contracts;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Collections;
8 |
9 | public class HttpCookieCollection : IHttpCookieCollection
10 | {
11 | private readonly IDictionary cookies;
12 |
13 | public HttpCookieCollection()
14 | {
15 | this.cookies = new Dictionary();
16 | }
17 |
18 | public void Add(HttpCookie cookie)
19 | {
20 | CoreValidator.ThrowIfNull(cookie, nameof(cookie));
21 |
22 | this.cookies[cookie.Key] = cookie;
23 | }
24 |
25 | public void Add(string key, string value)
26 | {
27 | CoreValidator.ThrowIfNullOrEmpty(key, nameof(key));
28 | CoreValidator.ThrowIfNullOrEmpty(value, nameof(value));
29 |
30 | this.Add(new HttpCookie(key, value));
31 | }
32 |
33 | public bool ContainsKey(string key)
34 | {
35 | CoreValidator.ThrowIfNull(key, nameof(key));
36 |
37 | return this.cookies.ContainsKey(key);
38 | }
39 |
40 | public IEnumerator GetEnumerator()
41 | => this.cookies.Values.GetEnumerator();
42 |
43 | IEnumerator IEnumerable.GetEnumerator()
44 | => this.cookies.Values.GetEnumerator();
45 |
46 | public HttpCookie Get(string key)
47 | {
48 | CoreValidator.ThrowIfNull(key, nameof(key));
49 |
50 | if (!this.cookies.ContainsKey(key))
51 | {
52 | throw new InvalidOperationException($"The given key {key} is not present in the cookies collection.");
53 | }
54 |
55 | return this.cookies[key];
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/WebServer/Http/HttpHeader.cs:
--------------------------------------------------------------------------------
1 | namespace WebServer.Http
2 | {
3 | using Common;
4 |
5 | public class HttpHeader
6 | {
7 | public const string ContentType = "Content-Type";
8 | public const string Host = "Host";
9 | public const string Location = "Location";
10 | public const string Cookie = "Cookie";
11 | public const string SetCookie = "Set-Cookie";
12 | public const string ContentLength = "Content-Length";
13 | public const string ContentDisposition = "Content-Disposition";
14 |
15 | public HttpHeader(string key, string value)
16 | {
17 | CoreValidator.ThrowIfNullOrEmpty(key, nameof(key));
18 | CoreValidator.ThrowIfNullOrEmpty(value, nameof(value));
19 |
20 | this.Key = key;
21 | this.Value = value;
22 | }
23 |
24 | public string Key { get; private set; }
25 |
26 | public string Value { get; private set; }
27 |
28 | public override string ToString() => $"{this.Key}: {this.Value}";
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/WebServer/Http/HttpHeaderCollection.cs:
--------------------------------------------------------------------------------
1 | namespace WebServer.Http
2 | {
3 | using Common;
4 | using Contracts;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Text;
8 | using System.Collections;
9 |
10 | public class HttpHeaderCollection : IHttpHeaderCollection
11 | {
12 | private readonly IDictionary> headers;
13 |
14 | public HttpHeaderCollection()
15 | {
16 | this.headers = new Dictionary>();
17 | }
18 |
19 | public void Add(HttpHeader header)
20 | {
21 | CoreValidator.ThrowIfNull(header, nameof(header));
22 |
23 | var headerKey = header.Key;
24 |
25 | if (!this.headers.ContainsKey(headerKey))
26 | {
27 | this.headers[headerKey] = new List();
28 | }
29 |
30 | this.headers[headerKey].Add(header);
31 | }
32 |
33 | public void Add(string key, string value)
34 | {
35 | CoreValidator.ThrowIfNullOrEmpty(key, nameof(key));
36 | CoreValidator.ThrowIfNullOrEmpty(value, nameof(value));
37 |
38 | this.Add(new HttpHeader(key, value));
39 | }
40 |
41 | public bool ContainsKey(string key)
42 | {
43 | CoreValidator.ThrowIfNull(key, nameof(key));
44 |
45 | return this.headers.ContainsKey(key);
46 | }
47 |
48 | public ICollection Get(string key)
49 | {
50 | CoreValidator.ThrowIfNull(key, nameof(key));
51 |
52 | if (!this.headers.ContainsKey(key))
53 | {
54 | throw new InvalidOperationException($"The given key {key} is not present in the headers collection.");
55 | }
56 |
57 | return this.headers[key];
58 | }
59 |
60 | public IEnumerator> GetEnumerator()
61 | => this.headers.Values.GetEnumerator();
62 |
63 | IEnumerator IEnumerable.GetEnumerator()
64 | => this.headers.Values.GetEnumerator();
65 |
66 | public override string ToString()
67 | {
68 | var result = new StringBuilder();
69 |
70 | foreach (var header in this.headers)
71 | {
72 | var headerKey = header.Key;
73 |
74 | foreach (var headerValue in header.Value)
75 | {
76 | result.AppendLine($"{headerKey}: {headerValue.Value}");
77 | }
78 | }
79 |
80 | return result.ToString();
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/WebServer/Http/HttpRequest.cs:
--------------------------------------------------------------------------------
1 | namespace WebServer.Http
2 | {
3 | using Common;
4 | using Contracts;
5 | using Enums;
6 | using Exceptions;
7 | using System;
8 | using System.Collections.Generic;
9 | using System.Linq;
10 | using System.Net;
11 |
12 | public class HttpRequest : IHttpRequest
13 | {
14 | private readonly string requestText;
15 |
16 | public HttpRequest(string requestText)
17 | {
18 | CoreValidator.ThrowIfNullOrEmpty(requestText, nameof(requestText));
19 |
20 | this.requestText = requestText;
21 |
22 | this.FormData = new Dictionary();
23 | this.UrlParameters = new Dictionary();
24 | this.Headers = new HttpHeaderCollection();
25 | this.Cookies = new HttpCookieCollection();
26 |
27 | this.ParseRequest(requestText);
28 | }
29 |
30 | public IDictionary FormData { get; private set; }
31 |
32 | public IHttpHeaderCollection Headers { get; private set; }
33 |
34 | public IHttpCookieCollection Cookies { get; private set; }
35 |
36 | public string Path { get; private set; }
37 |
38 | public HttpRequestMethod Method { get; private set; }
39 |
40 | public string Url { get; private set; }
41 |
42 | public IDictionary UrlParameters { get; private set; }
43 |
44 | public IHttpSession Session { get; set; }
45 |
46 | public void AddUrlParameter(string key, string value)
47 | {
48 | CoreValidator.ThrowIfNullOrEmpty(key, nameof(key));
49 | CoreValidator.ThrowIfNullOrEmpty(value, nameof(value));
50 |
51 | this.UrlParameters[key] = value;
52 | }
53 |
54 | private void ParseRequest(string requestText)
55 | {
56 | var requestLines = requestText.Split(
57 | new[] { Environment.NewLine },
58 | StringSplitOptions.None);
59 |
60 | if (!requestLines.Any())
61 | {
62 | BadRequestException.ThrowFromInvalidRequest();
63 | }
64 |
65 | var requestLine = requestLines.First().Split(
66 | new[] { ' ' },
67 | StringSplitOptions.RemoveEmptyEntries);
68 |
69 | if (requestLine.Length != 3 || requestLine[2].ToLower() != "http/1.1")
70 | {
71 | BadRequestException.ThrowFromInvalidRequest();
72 | }
73 |
74 | this.Method = this.ParseMethod(requestLine.First());
75 | this.Url = requestLine[1];
76 | this.Path = this.ParsePath(this.Url);
77 |
78 | this.ParseHeaders(requestLines);
79 | this.ParseCookies();
80 | this.ParseParameters();
81 | this.ParseFormData(requestLines.Last());
82 |
83 | this.SetSession();
84 | }
85 |
86 | private HttpRequestMethod ParseMethod(string method)
87 | {
88 | HttpRequestMethod parsedMethod;
89 |
90 | if (!Enum.TryParse(method, true, out parsedMethod))
91 | {
92 | BadRequestException.ThrowFromInvalidRequest();
93 | }
94 |
95 | return parsedMethod;
96 | }
97 |
98 | private string ParsePath(string url)
99 | => url.Split(new[] { '?', '#' }, StringSplitOptions.RemoveEmptyEntries)[0];
100 |
101 | private void ParseHeaders(string[] requestLines)
102 | {
103 | var emptyLineAfterHeadersIndex = Array.IndexOf(requestLines, string.Empty);
104 |
105 | for (int i = 1; i < emptyLineAfterHeadersIndex; i++)
106 | {
107 | var currentLine = requestLines[i];
108 | var headerParts = currentLine.Split(new[] { ": " }, StringSplitOptions.RemoveEmptyEntries);
109 |
110 | if (headerParts.Length != 2)
111 | {
112 | BadRequestException.ThrowFromInvalidRequest();
113 | }
114 |
115 | var headerKey = headerParts[0];
116 | var headerValue = headerParts[1].Trim();
117 |
118 | var header = new HttpHeader(headerKey, headerValue);
119 |
120 | this.Headers.Add(header);
121 | }
122 |
123 | if (!this.Headers.ContainsKey(HttpHeader.Host))
124 | {
125 | BadRequestException.ThrowFromInvalidRequest();
126 | }
127 | }
128 |
129 | private void ParseCookies()
130 | {
131 | if (this.Headers.ContainsKey(HttpHeader.Cookie))
132 | {
133 | var allCookies = this.Headers.Get(HttpHeader.Cookie);
134 |
135 | foreach (var cookie in allCookies)
136 | {
137 | if (!cookie.Value.Contains('='))
138 | {
139 | return;
140 | }
141 |
142 | var cookieParts = cookie
143 | .Value
144 | .Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
145 | .ToList();
146 |
147 | if (!cookieParts.Any())
148 | {
149 | continue;
150 | }
151 |
152 | foreach (var cookiePart in cookieParts)
153 | {
154 | var cookieKeyValuePair = cookiePart
155 | .Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries);
156 |
157 | if (cookieKeyValuePair.Length == 2)
158 | {
159 | var key = cookieKeyValuePair[0].Trim();
160 | var value = cookieKeyValuePair[1].Trim();
161 |
162 | this.Cookies.Add(new HttpCookie(key, value, false));
163 | }
164 | }
165 | }
166 | }
167 | }
168 |
169 | private void ParseParameters()
170 | {
171 | if (!this.Url.Contains('?'))
172 | {
173 | return;
174 | }
175 |
176 | var query = this.Url
177 | .Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries)
178 | .Last();
179 |
180 | this.ParseQuery(query, this.UrlParameters);
181 | }
182 |
183 | private void ParseFormData(string formDataLine)
184 | {
185 | if (this.Method == HttpRequestMethod.Get)
186 | {
187 | return;
188 | }
189 |
190 | this.ParseQuery(formDataLine, this.FormData);
191 | }
192 |
193 | private void ParseQuery(string query, IDictionary dict)
194 | {
195 | if (!query.Contains('='))
196 | {
197 | return;
198 | }
199 |
200 | var queryPairs = query.Split(new[] { '&' });
201 |
202 | foreach (var queryPair in queryPairs)
203 | {
204 | var queryKvp = queryPair.Split(new[] { '=' });
205 |
206 | if (queryKvp.Length != 2)
207 | {
208 | return;
209 | }
210 |
211 | var queryKey = WebUtility.UrlDecode(queryKvp[0]);
212 | var queryValue = WebUtility.UrlDecode(queryKvp[1]);
213 |
214 | dict.Add(queryKey, queryValue);
215 | }
216 | }
217 |
218 | private void SetSession()
219 | {
220 | if (this.Cookies.ContainsKey(SessionStore.SessionCookieKey))
221 | {
222 | var cookie = this.Cookies.Get(SessionStore.SessionCookieKey);
223 | var sessionId = cookie.Value;
224 |
225 | this.Session = SessionStore.Get(sessionId);
226 | }
227 | }
228 |
229 | public override string ToString() => this.requestText;
230 | }
231 | }
232 |
--------------------------------------------------------------------------------
/WebServer/Http/HttpSession.cs:
--------------------------------------------------------------------------------
1 | namespace WebServer.Http
2 | {
3 | using Common;
4 | using Contracts;
5 | using System.Collections.Generic;
6 |
7 | public class HttpSession : IHttpSession
8 | {
9 | private readonly IDictionary values;
10 |
11 | public HttpSession(string id)
12 | {
13 | CoreValidator.ThrowIfNullOrEmpty(id, nameof(id));
14 |
15 | this.Id = id;
16 | this.values = new Dictionary();
17 | }
18 |
19 | public string Id { get; private set; }
20 |
21 | public void Add(string key, object value)
22 | {
23 | CoreValidator.ThrowIfNullOrEmpty(key, nameof(key));
24 | CoreValidator.ThrowIfNull(value, nameof(value));
25 |
26 | this.values[key] = value;
27 | }
28 |
29 | public void Remove(string key)
30 | {
31 | CoreValidator.ThrowIfNull(key, nameof(key));
32 |
33 | if (this.values.ContainsKey(key))
34 | {
35 | this.values.Remove(key);
36 | }
37 | }
38 |
39 | public void Clear() => this.values.Clear();
40 |
41 | public object Get(string key)
42 | {
43 | CoreValidator.ThrowIfNull(key, nameof(key));
44 |
45 | if (!this.values.ContainsKey(key))
46 | {
47 | return null;
48 | }
49 |
50 | return this.values[key];
51 | }
52 |
53 | public T Get(string key)
54 | => (T)this.Get(key);
55 |
56 | public bool Contains(string key) => this.values.ContainsKey(key);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/WebServer/Http/Response/BadRequestResponse.cs:
--------------------------------------------------------------------------------
1 | namespace WebServer.Http.Response
2 | {
3 | using Enums;
4 |
5 | public class BadRequestResponse : HttpResponse
6 | {
7 | public BadRequestResponse()
8 | {
9 | this.StatusCode = HttpStatusCode.BadRequest;
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/WebServer/Http/Response/ContentResponse.cs:
--------------------------------------------------------------------------------
1 | namespace WebServer.Http.Response
2 | {
3 | using Enums;
4 | using Exceptions;
5 | using global::WebServer.Contracts;
6 |
7 | public class ContentResponse : HttpResponse
8 | {
9 | private readonly string content;
10 |
11 | public ContentResponse(HttpStatusCode statusCode, string content)
12 | {
13 | this.ValidateStatusCode(statusCode);
14 |
15 | this.content = content;
16 | this.StatusCode = statusCode;
17 |
18 | this.Headers.Add(HttpHeader.ContentType, "text/html");
19 | }
20 |
21 | private void ValidateStatusCode(HttpStatusCode statusCode)
22 | {
23 | var statusCodeNumber = (int)statusCode;
24 |
25 | if (299 < statusCodeNumber && statusCodeNumber < 400)
26 | {
27 | throw new InvalidResponseException("View responses need a status code below 300 and above 400 (inclusive).");
28 | }
29 | }
30 |
31 | public override string ToString()
32 | {
33 | return $"{base.ToString()}{this.content}";
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/WebServer/Http/Response/FileResponse.cs:
--------------------------------------------------------------------------------
1 | namespace WebServer.Http.Response
2 | {
3 | using Common;
4 | using Enums;
5 | using System;
6 |
7 | public class FileResponse : HttpResponse
8 | {
9 | public FileResponse(HttpStatusCode statusCode, byte[] fileContents)
10 | {
11 | CoreValidator.ThrowIfNull(fileContents, nameof(fileContents));
12 |
13 | this.ValidateStatusCode(statusCode);
14 |
15 | this.StatusCode = statusCode;
16 | this.FileData = fileContents;
17 |
18 | this.Headers.Add(HttpHeader.ContentLength, fileContents.Length.ToString());
19 | this.Headers.Add(HttpHeader.ContentDisposition, "attachment");
20 | }
21 |
22 | private void ValidateStatusCode(HttpStatusCode statusCode)
23 | {
24 | var statusCodeNumber = (int)statusCode;
25 |
26 | if (statusCodeNumber <= 299 || statusCodeNumber >= 400)
27 | {
28 | throw new InvalidOperationException("File responses need to have status code between 300 and 399.");
29 | }
30 | }
31 |
32 | public byte[] FileData { get; private set; }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/WebServer/Http/Response/HttpResponse.cs:
--------------------------------------------------------------------------------
1 | namespace WebServer.Http.Response
2 | {
3 | using Contracts;
4 | using Enums;
5 | using System.Text;
6 |
7 | public abstract class HttpResponse : IHttpResponse
8 | {
9 | private string statusCodeMessage => this.StatusCode.ToString();
10 |
11 | protected HttpResponse()
12 | {
13 | this.Headers = new HttpHeaderCollection();
14 | this.Cookies = new HttpCookieCollection();
15 | }
16 |
17 | public IHttpHeaderCollection Headers { get; }
18 |
19 | public IHttpCookieCollection Cookies { get; }
20 |
21 | public HttpStatusCode StatusCode { get; protected set; }
22 |
23 | public override string ToString()
24 | {
25 | var response = new StringBuilder();
26 |
27 | var statusCodeNumber = (int)this.StatusCode;
28 | response.AppendLine($"HTTP/1.1 {statusCodeNumber} {this.statusCodeMessage}");
29 |
30 | response.AppendLine(this.Headers.ToString());
31 |
32 | return response.ToString();
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/WebServer/Http/Response/InternalServerErrorResponse.cs:
--------------------------------------------------------------------------------
1 | namespace WebServer.Http.Response
2 | {
3 | using System;
4 | using Enums;
5 | using Common;
6 |
7 | public class InternalServerErrorResponse : ContentResponse
8 | {
9 | public InternalServerErrorResponse(Exception ex)
10 | : base(HttpStatusCode.InternalServerError, new InternalServerErrorView(ex).View())
11 | {
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/WebServer/Http/Response/NotFoundResponse.cs:
--------------------------------------------------------------------------------
1 | namespace WebServer.Http.Response
2 | {
3 | using Common;
4 | using Enums;
5 |
6 | public class NotFoundResponse : ContentResponse
7 | {
8 | public NotFoundResponse()
9 | : base(HttpStatusCode.NotFound, new NotFoundView().View())
10 | {
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/WebServer/Http/Response/RedirectResponse.cs:
--------------------------------------------------------------------------------
1 | namespace WebServer.Http.Response
2 | {
3 | using Common;
4 | using Enums;
5 |
6 | public class RedirectResponse : HttpResponse
7 | {
8 | public RedirectResponse(string redirectUrl)
9 | {
10 | CoreValidator.ThrowIfNullOrEmpty(redirectUrl, nameof(redirectUrl));
11 |
12 | this.StatusCode = HttpStatusCode.Found;
13 |
14 | this.Headers.Add(HttpHeader.Location, redirectUrl);
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/WebServer/Http/SessionStore.cs:
--------------------------------------------------------------------------------
1 | namespace WebServer.Http
2 | {
3 | using System.Collections.Concurrent;
4 |
5 | public static class SessionStore
6 | {
7 | public const string SessionCookieKey = "MY_SID";
8 | public const string CurrentUserKey = "^%Current_User_Session_Key%^";
9 |
10 | private static readonly ConcurrentDictionary sessions =
11 | new ConcurrentDictionary();
12 |
13 | public static HttpSession Get(string id)
14 | => sessions.GetOrAdd(id, _ => new HttpSession(id));
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/WebServer/WebServer.cs:
--------------------------------------------------------------------------------
1 | namespace WebServer
2 | {
3 | using Contracts;
4 | using System;
5 | using System.Net;
6 | using System.Net.Sockets;
7 | using System.Threading.Tasks;
8 |
9 | public class WebServer : IRunnable
10 | {
11 | private const string localHostIpAddress = "127.0.0.1";
12 |
13 | private readonly int port;
14 | private readonly IHandleable mvcRequestHandler;
15 | private readonly IHandleable resourceHandler;
16 | private readonly TcpListener listener;
17 | private bool isRunning;
18 |
19 | public WebServer(int port, IHandleable mvcRequestHandler, IHandleable resourceHandler)
20 | {
21 | this.port = port;
22 | this.listener = new TcpListener(IPAddress.Parse(localHostIpAddress), port);
23 |
24 | this.mvcRequestHandler = mvcRequestHandler;
25 | this.resourceHandler = resourceHandler;
26 | }
27 |
28 | public void Run()
29 | {
30 | this.listener.Start();
31 | this.isRunning = true;
32 |
33 | Console.WriteLine($"Server running on {localHostIpAddress}:{this.port}");
34 |
35 | Task.Run(this.ListenLoop).Wait();
36 | }
37 |
38 | private async Task ListenLoop()
39 | {
40 | while (this.isRunning)
41 | {
42 | var client = await this.listener.AcceptSocketAsync();
43 | var connectionHandler = new ConnectionHandler(client, this.mvcRequestHandler, this.resourceHandler);
44 | await connectionHandler.ProcessRequestAsync();
45 | }
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/WebServer/WebServer.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------