├── .gitignore
├── After
├── OnlineTheater.sln
└── src
│ ├── Api
│ ├── Api.csproj
│ ├── Customers
│ │ ├── CreateCustomerDto.cs
│ │ ├── CustomerDto.cs
│ │ ├── CustomerInListDto.cs
│ │ ├── CustomersController.cs
│ │ ├── MovieDto.cs
│ │ ├── PurchasedMovieDto.cs
│ │ └── UpdateCustomerDto.cs
│ ├── Properties
│ │ └── launchSettings.json
│ ├── Utils
│ │ ├── BaseController.cs
│ │ ├── Envelope.cs
│ │ ├── ExceptionHandler.cs
│ │ ├── Program.cs
│ │ └── Startup.cs
│ ├── appsettings.Development.json
│ └── appsettings.json
│ ├── Database.sql
│ └── Logic
│ ├── Common
│ ├── Entity.cs
│ └── Repository.cs
│ ├── Customers
│ ├── Customer.cs
│ ├── CustomerMap.cs
│ ├── CustomerName.cs
│ ├── CustomerRepository.cs
│ ├── CustomerStatus.cs
│ ├── Dollars.cs
│ ├── Email.cs
│ ├── ExpirationDate.cs
│ ├── LicensingModel.cs
│ ├── PurchasedMovie.cs
│ └── PurchasedMovieMap.cs
│ ├── Logic.csproj
│ ├── Movies
│ ├── Movie.cs
│ ├── MovieMap.cs
│ └── MovieRepository.cs
│ ├── Properties
│ └── AssemblyInfo.cs
│ ├── Utils
│ ├── SessionFactory.cs
│ └── UnitOfWork.cs
│ ├── app.config
│ └── packages.config
├── Before
├── OnlineTheaterBefore.sln
└── src
│ ├── Api
│ ├── Api.csproj
│ ├── Controllers
│ │ └── CustomersController.cs
│ ├── Program.cs
│ ├── Properties
│ │ └── launchSettings.json
│ ├── Startup.cs
│ ├── appsettings.Development.json
│ └── appsettings.json
│ └── Logic
│ ├── Entities
│ ├── Customer.cs
│ ├── CustomerStatus.cs
│ ├── Entity.cs
│ ├── LicensingModel.cs
│ ├── Movie.cs
│ └── PurchasedMovie.cs
│ ├── Logic.csproj
│ ├── Mappings
│ ├── CustomerMap.cs
│ ├── MovieMap.cs
│ └── PurchasedMovieMap.cs
│ ├── Properties
│ └── AssemblyInfo.cs
│ ├── Repositories
│ ├── CustomerRepository.cs
│ ├── MovieRepository.cs
│ └── Repository.cs
│ ├── Services
│ ├── CustomerService.cs
│ └── MovieService.cs
│ ├── Utils
│ ├── SessionFactory.cs
│ └── UnitOfWork.cs
│ ├── app.config
│ └── packages.config
├── LICENSE
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | out
2 | .svn
3 | obj
4 | bin
5 | .idea
6 | _ReSharper*
7 | *.sln.GhostDoc.xml
8 | *.dotCover
9 | *.suo
10 | *.user
11 | *.Cache
12 | *.cache
13 | *.ncrunchsolution
14 | *.ncrunchproject
15 | */packages/*/*
16 | /Tools/*/*
17 | project.lock.json
18 | project.fragment.lock.json
19 | .vs
--------------------------------------------------------------------------------
/After/OnlineTheater.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}") = "Api", "src\Api\Api.csproj", "{36484BFF-E628-49BD-BF49-01BAD88D69E3}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logic", "src\Logic\Logic.csproj", "{0B9EC24F-E84F-4375-A7B0-0F8C3A4B015A}"
9 | EndProject
10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Database", "Database", "{85997060-C092-4F36-BA3D-94253CD7C9C5}"
11 | ProjectSection(SolutionItems) = preProject
12 | src\Database.sql = src\Database.sql
13 | EndProjectSection
14 | EndProject
15 | Global
16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
17 | Debug|Any CPU = Debug|Any CPU
18 | Release|Any CPU = Release|Any CPU
19 | EndGlobalSection
20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
21 | {36484BFF-E628-49BD-BF49-01BAD88D69E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
22 | {36484BFF-E628-49BD-BF49-01BAD88D69E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
23 | {36484BFF-E628-49BD-BF49-01BAD88D69E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
24 | {36484BFF-E628-49BD-BF49-01BAD88D69E3}.Release|Any CPU.Build.0 = Release|Any CPU
25 | {0B9EC24F-E84F-4375-A7B0-0F8C3A4B015A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
26 | {0B9EC24F-E84F-4375-A7B0-0F8C3A4B015A}.Debug|Any CPU.Build.0 = Debug|Any CPU
27 | {0B9EC24F-E84F-4375-A7B0-0F8C3A4B015A}.Release|Any CPU.ActiveCfg = Release|Any CPU
28 | {0B9EC24F-E84F-4375-A7B0-0F8C3A4B015A}.Release|Any CPU.Build.0 = Release|Any CPU
29 | EndGlobalSection
30 | GlobalSection(SolutionProperties) = preSolution
31 | HideSolutionNode = FALSE
32 | EndGlobalSection
33 | GlobalSection(ExtensibilityGlobals) = postSolution
34 | SolutionGuid = {2AF79079-DA8B-4E50-8B02-9F1EC4FE3476}
35 | EndGlobalSection
36 | EndGlobal
37 |
--------------------------------------------------------------------------------
/After/src/Api/Api.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net462
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/After/src/Api/Customers/CreateCustomerDto.cs:
--------------------------------------------------------------------------------
1 | namespace Api.Customers
2 | {
3 | public class CreateCustomerDto
4 | {
5 | public string Name { get; set; }
6 | public string Email { get; set; }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/After/src/Api/Customers/CustomerDto.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace Api.Customers
5 | {
6 | public class CustomerDto
7 | {
8 | public long Id { get; set; }
9 | public string Name { get; set; }
10 | public string Email { get; set; }
11 | public string Status { get; set; }
12 | public DateTime? StatusExpirationDate { get; set; }
13 | public decimal MoneySpent { get; set; }
14 | public List PurchasedMovies { get; set; }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/After/src/Api/Customers/CustomerInListDto.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Api.Customers
4 | {
5 | public class CustomerInListDto
6 | {
7 | public long Id { get; set; }
8 | public string Name { get; set; }
9 | public string Email { get; set; }
10 | public string Status { get; set; }
11 | public DateTime? StatusExpirationDate { get; set; }
12 | public decimal MoneySpent { get; set; }
13 | }
14 | }
--------------------------------------------------------------------------------
/After/src/Api/Customers/CustomersController.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using Api.Utils;
4 | using CSharpFunctionalExtensions;
5 | using Logic.Customers;
6 | using Logic.Movies;
7 | using Logic.Utils;
8 | using Microsoft.AspNetCore.Mvc;
9 |
10 | namespace Api.Customers
11 | {
12 | [Route("api/[controller]")]
13 | public class CustomersController : BaseController
14 | {
15 | private readonly MovieRepository _movieRepository;
16 | private readonly CustomerRepository _customerRepository;
17 |
18 | public CustomersController(UnitOfWork unitOfWork, MovieRepository movieRepository, CustomerRepository customerRepository)
19 | : base(unitOfWork)
20 | {
21 | _customerRepository = customerRepository;
22 | _movieRepository = movieRepository;
23 | }
24 |
25 | [HttpGet]
26 | [Route("{id}")]
27 | public IActionResult Get(long id)
28 | {
29 | Customer customer = _customerRepository.GetById(id);
30 | if (customer == null)
31 | return NotFound();
32 |
33 | var dto = new CustomerDto
34 | {
35 | Id = customer.Id,
36 | Name = customer.Name.Value,
37 | Email = customer.Email.Value,
38 | MoneySpent = customer.MoneySpent,
39 | Status = customer.Status.Type.ToString(),
40 | StatusExpirationDate = customer.Status.ExpirationDate,
41 | PurchasedMovies = customer.PurchasedMovies.Select(x => new PurchasedMovieDto
42 | {
43 | Price = x.Price,
44 | ExpirationDate = x.ExpirationDate,
45 | PurchaseDate = x.PurchaseDate,
46 | Movie = new MovieDto
47 | {
48 | Id = x.Movie.Id,
49 | Name = x.Movie.Name
50 | }
51 | }).ToList()
52 | };
53 |
54 | return Ok(dto);
55 | }
56 |
57 | [HttpGet]
58 | public IActionResult GetList()
59 | {
60 | IReadOnlyList customers = _customerRepository.GetList();
61 |
62 | List dtos = customers.Select(x => new CustomerInListDto
63 | {
64 | Id = x.Id,
65 | Name = x.Name.Value,
66 | Email = x.Email.Value,
67 | MoneySpent = x.MoneySpent,
68 | Status = x.Status.Type.ToString(),
69 | StatusExpirationDate = x.Status.ExpirationDate
70 | }).ToList();
71 |
72 | return Ok(dtos);
73 | }
74 |
75 | [HttpPost]
76 | public IActionResult Create([FromBody] CreateCustomerDto item)
77 | {
78 | Result customerNameOrError = CustomerName.Create(item.Name);
79 | Result emailOrError = Email.Create(item.Email);
80 |
81 | Result result = Result.Combine(customerNameOrError, emailOrError);
82 | if (result.IsFailure)
83 | return Error(result.Error);
84 |
85 | if (_customerRepository.GetByEmail(emailOrError.Value) != null)
86 | return Error("Email is already in use: " + item.Email);
87 |
88 | var customer = new Customer(customerNameOrError.Value, emailOrError.Value);
89 | _customerRepository.Add(customer);
90 |
91 | return Ok();
92 | }
93 |
94 | [HttpPut]
95 | [Route("{id}")]
96 | public IActionResult Update(long id, [FromBody] UpdateCustomerDto item)
97 | {
98 | Result customerNameOrError = CustomerName.Create(item.Name);
99 | if (customerNameOrError.IsFailure)
100 | return Error(customerNameOrError.Error);
101 |
102 | Customer customer = _customerRepository.GetById(id);
103 | if (customer == null)
104 | return Error("Invalid customer id: " + id);
105 |
106 | customer.Name = customerNameOrError.Value;
107 |
108 | return Ok();
109 | }
110 |
111 | [HttpPost]
112 | [Route("{id}/movies")]
113 | public IActionResult PurchaseMovie(long id, [FromBody] long movieId)
114 | {
115 | Movie movie = _movieRepository.GetById(movieId);
116 | if (movie == null)
117 | return Error("Invalid movie id: " + movieId);
118 |
119 | Customer customer = _customerRepository.GetById(id);
120 | if (customer == null)
121 | return Error("Invalid customer id: " + id);
122 |
123 | if (customer.HasPurchasedMovie(movie))
124 | return Error("The movie is already purchased: " + movie.Name);
125 |
126 | customer.PurchaseMovie(movie);
127 |
128 | return Ok();
129 | }
130 |
131 | [HttpPost]
132 | [Route("{id}/promotion")]
133 | public IActionResult PromoteCustomer(long id)
134 | {
135 | Customer customer = _customerRepository.GetById(id);
136 | if (customer == null)
137 | return Error("Invalid customer id: " + id);
138 |
139 | Result promotionCheck = customer.CanPromote();
140 | if (promotionCheck.IsFailure)
141 | return Error(promotionCheck.Error);
142 |
143 | customer.Promote();
144 |
145 | return Ok();
146 | }
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/After/src/Api/Customers/MovieDto.cs:
--------------------------------------------------------------------------------
1 | namespace Api.Customers
2 | {
3 | public class MovieDto
4 | {
5 | public long Id { get; set; }
6 | public string Name { get; set; }
7 | }
8 | }
--------------------------------------------------------------------------------
/After/src/Api/Customers/PurchasedMovieDto.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Api.Customers
4 | {
5 | public class PurchasedMovieDto
6 | {
7 | public MovieDto Movie { get; set; }
8 | public decimal Price { get; set; }
9 | public DateTime PurchaseDate { get; set; }
10 | public DateTime? ExpirationDate { get; set; }
11 | }
12 | }
--------------------------------------------------------------------------------
/After/src/Api/Customers/UpdateCustomerDto.cs:
--------------------------------------------------------------------------------
1 | namespace Api.Customers
2 | {
3 | public class UpdateCustomerDto
4 | {
5 | public string Name { get; set; }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/After/src/Api/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "iisSettings": {
3 | "windowsAuthentication": false,
4 | "anonymousAuthentication": true,
5 | "iisExpress": {
6 | "applicationUrl": "http://localhost:60367/",
7 | "sslPort": 0
8 | }
9 | },
10 | "profiles": {
11 | "IIS Express": {
12 | "commandName": "IISExpress",
13 | "launchBrowser": true,
14 | "launchUrl": "api/customers",
15 | "environmentVariables": {
16 | "ASPNETCORE_ENVIRONMENT": "Development"
17 | }
18 | },
19 | "Api": {
20 | "commandName": "Project",
21 | "launchBrowser": true,
22 | "launchUrl": "api/customers",
23 | "environmentVariables": {
24 | "ASPNETCORE_ENVIRONMENT": "Development"
25 | },
26 | "applicationUrl": "http://localhost:60368/"
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/After/src/Api/Utils/BaseController.cs:
--------------------------------------------------------------------------------
1 | using Logic.Utils;
2 | using Microsoft.AspNetCore.Mvc;
3 |
4 | namespace Api.Utils
5 | {
6 | public class BaseController : Controller
7 | {
8 | private readonly UnitOfWork _unitOfWork;
9 |
10 | public BaseController(UnitOfWork unitOfWork)
11 | {
12 | _unitOfWork = unitOfWork;
13 | }
14 |
15 | protected new IActionResult Ok()
16 | {
17 | _unitOfWork.Commit();
18 | return base.Ok(Envelope.Ok());
19 | }
20 |
21 | protected IActionResult Ok(T result)
22 | {
23 | _unitOfWork.Commit();
24 | return base.Ok(Envelope.Ok(result));
25 | }
26 |
27 | protected IActionResult Error(string errorMessage)
28 | {
29 | return BadRequest(Envelope.Error(errorMessage));
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/After/src/Api/Utils/Envelope.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Api.Utils
4 | {
5 | public class Envelope
6 | {
7 | public T Result { get; }
8 | public string ErrorMessage { get; }
9 | public DateTime TimeGenerated { get; }
10 |
11 | protected internal Envelope(T result, string errorMessage)
12 | {
13 | Result = result;
14 | ErrorMessage = errorMessage;
15 | TimeGenerated = DateTime.UtcNow;
16 | }
17 | }
18 |
19 | public class Envelope : Envelope
20 | {
21 | protected Envelope(string errorMessage)
22 | : base(null, errorMessage)
23 | {
24 | }
25 |
26 | public static Envelope Ok(T result)
27 | {
28 | return new Envelope(result, null);
29 | }
30 |
31 | public static Envelope Ok()
32 | {
33 | return new Envelope(null);
34 | }
35 |
36 | public static Envelope Error(string errorMessage)
37 | {
38 | return new Envelope(errorMessage);
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/After/src/Api/Utils/ExceptionHandler.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net;
3 | using System.Threading.Tasks;
4 | using Microsoft.AspNetCore.Http;
5 | using Newtonsoft.Json;
6 |
7 | namespace Api.Utils
8 | {
9 | public class ExceptionHandler
10 | {
11 | private readonly RequestDelegate _next;
12 |
13 | public ExceptionHandler(RequestDelegate next)
14 | {
15 | _next = next;
16 | }
17 |
18 | public async Task Invoke(HttpContext context)
19 | {
20 | try
21 | {
22 | await _next(context);
23 | }
24 | catch (Exception ex)
25 | {
26 | await HandleExceptionAsync(context, ex);
27 | }
28 | }
29 |
30 | private Task HandleExceptionAsync(HttpContext context, Exception exception)
31 | {
32 | // Log exception here
33 | string result = JsonConvert.SerializeObject(Envelope.Error(exception.Message));
34 | context.Response.ContentType = "application/json";
35 | context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
36 | return context.Response.WriteAsync(result);
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/After/src/Api/Utils/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore;
2 | using Microsoft.AspNetCore.Hosting;
3 |
4 | namespace Api.Utils
5 | {
6 | public class Program
7 | {
8 | public static void Main(string[] args)
9 | {
10 | BuildWebHost(args).Run();
11 | }
12 |
13 | public static IWebHost BuildWebHost(string[] args)
14 | {
15 | return WebHost.CreateDefaultBuilder(args)
16 | .UseStartup()
17 | .Build();
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/After/src/Api/Utils/Startup.cs:
--------------------------------------------------------------------------------
1 | using Logic.Customers;
2 | using Logic.Movies;
3 | using Logic.Utils;
4 | using Microsoft.AspNetCore.Builder;
5 | using Microsoft.Extensions.Configuration;
6 | using Microsoft.Extensions.DependencyInjection;
7 |
8 | namespace Api.Utils
9 | {
10 | public class Startup
11 | {
12 | public Startup(IConfiguration configuration)
13 | {
14 | Configuration = configuration;
15 | }
16 |
17 | public IConfiguration Configuration { get; }
18 |
19 | public void ConfigureServices(IServiceCollection services)
20 | {
21 | services.AddMvc();
22 |
23 | services.AddSingleton(new SessionFactory(Configuration["ConnectionString"]));
24 | services.AddScoped();
25 | services.AddTransient();
26 | services.AddTransient();
27 | }
28 |
29 | public void Configure(IApplicationBuilder app)
30 | {
31 | app.UseMiddleware();
32 | app.UseMvc();
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/After/src/Api/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "IncludeScopes": false,
4 | "LogLevel": {
5 | "Default": "Debug",
6 | "System": "Information",
7 | "Microsoft": "Information"
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/After/src/Api/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "IncludeScopes": false,
4 | "Debug": {
5 | "LogLevel": {
6 | "Default": "Warning"
7 | }
8 | },
9 | "Console": {
10 | "LogLevel": {
11 | "Default": "Warning"
12 | }
13 | }
14 | },
15 | "ConnectionString": "Server=.\\Sql;Database=OnlineTheater;Trusted_Connection=true;"
16 | }
17 |
--------------------------------------------------------------------------------
/After/src/Database.sql:
--------------------------------------------------------------------------------
1 | CREATE DATABASE [OnlineTheater]
2 | GO
3 | USE [OnlineTheater]
4 | GO
5 | /****** Object: Table [dbo].[Customer] Script Date: 11/15/2017 4:11:10 PM ******/
6 | SET ANSI_NULLS ON
7 | GO
8 | SET QUOTED_IDENTIFIER ON
9 | GO
10 | CREATE TABLE [dbo].[Customer](
11 | [CustomerID] [bigint] IDENTITY(1,1) NOT NULL,
12 | [Name] [nvarchar](50) NOT NULL,
13 | [Email] [nvarchar](150) NOT NULL,
14 | [Status] [int] NOT NULL,
15 | [StatusExpirationDate] [datetime] NULL,
16 | [MoneySpent] [decimal](18, 2) NOT NULL,
17 | CONSTRAINT [PK_Customer] PRIMARY KEY CLUSTERED
18 | (
19 | [CustomerID] ASC
20 | )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
21 | ) ON [PRIMARY]
22 |
23 | GO
24 | /****** Object: Table [dbo].[Movie] Script Date: 11/15/2017 4:11:10 PM ******/
25 | SET ANSI_NULLS ON
26 | GO
27 | SET QUOTED_IDENTIFIER ON
28 | GO
29 | CREATE TABLE [dbo].[Movie](
30 | [MovieID] [bigint] IDENTITY(1,1) NOT NULL,
31 | [Name] [nvarchar](50) NOT NULL,
32 | [LicensingModel] [int] NOT NULL,
33 | CONSTRAINT [PK_Movie] PRIMARY KEY CLUSTERED
34 | (
35 | [MovieID] ASC
36 | )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
37 | ) ON [PRIMARY]
38 |
39 | GO
40 | /****** Object: Table [dbo].[PurchasedMovie] Script Date: 11/15/2017 4:11:10 PM ******/
41 | SET ANSI_NULLS ON
42 | GO
43 | SET QUOTED_IDENTIFIER ON
44 | GO
45 | CREATE TABLE [dbo].[PurchasedMovie](
46 | [PurchasedMovieID] [bigint] IDENTITY(1,1) NOT NULL,
47 | [MovieID] [bigint] NOT NULL,
48 | [CustomerID] [bigint] NOT NULL,
49 | [Price] [decimal](18, 2) NOT NULL,
50 | [PurchaseDate] [datetime] NOT NULL,
51 | [ExpirationDate] [datetime] NULL,
52 | CONSTRAINT [PK_PurchasedMovie] PRIMARY KEY CLUSTERED
53 | (
54 | [PurchasedMovieID] ASC
55 | )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
56 | ) ON [PRIMARY]
57 |
58 | GO
59 | SET IDENTITY_INSERT [dbo].[Customer] ON
60 |
61 | GO
62 | INSERT [dbo].[Customer] ([CustomerID], [Name], [Email], [Status], [StatusExpirationDate], [MoneySpent]) VALUES (1, N'James Peterson', N'james.peterson@gmail.com', 1, NULL, CAST(16.00 AS Decimal(18, 2)))
63 | GO
64 | INSERT [dbo].[Customer] ([CustomerID], [Name], [Email], [Status], [StatusExpirationDate], [MoneySpent]) VALUES (2, N'Michal Samson', N'm.samson@yahoo.com', 2, CAST(N'2018-10-14 01:37:27.000' AS DateTime), CAST(9.00 AS Decimal(18, 2)))
65 | GO
66 | INSERT [dbo].[Customer] ([CustomerID], [Name], [Email], [Status], [StatusExpirationDate], [MoneySpent]) VALUES (4, N'Alan Turing 2', N'the.alan@gmail.com', 1, NULL, CAST(0.00 AS Decimal(18, 2)))
67 | GO
68 | INSERT [dbo].[Customer] ([CustomerID], [Name], [Email], [Status], [StatusExpirationDate], [MoneySpent]) VALUES (5, N'Alan Turing', N'the.alan2@gmail.com', 1, NULL, CAST(1004.00 AS Decimal(18, 2)))
69 | GO
70 | INSERT [dbo].[Customer] ([CustomerID], [Name], [Email], [Status], [StatusExpirationDate], [MoneySpent]) VALUES (6, N'Alan Turing', N'the.alan3@gmail.com', 1, NULL, CAST(0.00 AS Decimal(18, 2)))
71 | GO
72 | SET IDENTITY_INSERT [dbo].[Customer] OFF
73 | GO
74 | SET IDENTITY_INSERT [dbo].[Movie] ON
75 |
76 | GO
77 | INSERT [dbo].[Movie] ([MovieID], [Name], [LicensingModel]) VALUES (1, N'The Great Gatsby', 1)
78 | GO
79 | INSERT [dbo].[Movie] ([MovieID], [Name], [LicensingModel]) VALUES (2, N'The Secret Life of Pets', 2)
80 | GO
81 | SET IDENTITY_INSERT [dbo].[Movie] OFF
82 | GO
83 | SET IDENTITY_INSERT [dbo].[PurchasedMovie] ON
84 |
85 | GO
86 | INSERT [dbo].[PurchasedMovie] ([PurchasedMovieID], [MovieID], [CustomerID], [Price], [PurchaseDate], [ExpirationDate]) VALUES (1, 1, 2, CAST(5.00 AS Decimal(18, 2)), CAST(N'2017-09-16 16:30:05.773' AS DateTime), CAST(N'2017-09-18 00:00:00.000' AS DateTime))
87 | GO
88 | INSERT [dbo].[PurchasedMovie] ([PurchasedMovieID], [MovieID], [CustomerID], [Price], [PurchaseDate], [ExpirationDate]) VALUES (2, 2, 2, CAST(4.00 AS Decimal(18, 2)), CAST(N'2017-09-15 15:30:05.773' AS DateTime), NULL)
89 | GO
90 | INSERT [dbo].[PurchasedMovie] ([PurchasedMovieID], [MovieID], [CustomerID], [Price], [PurchaseDate], [ExpirationDate]) VALUES (3, 1, 5, CAST(4.00 AS Decimal(18, 2)), CAST(N'2017-10-07 23:54:22.000' AS DateTime), CAST(N'2017-10-09 23:54:22.000' AS DateTime))
91 | GO
92 | INSERT [dbo].[PurchasedMovie] ([PurchasedMovieID], [MovieID], [CustomerID], [Price], [PurchaseDate], [ExpirationDate]) VALUES (6, 1, 1, CAST(4.00 AS Decimal(18, 2)), CAST(N'2017-10-15 13:26:19.000' AS DateTime), CAST(N'2017-10-17 13:26:19.000' AS DateTime))
93 | GO
94 | INSERT [dbo].[PurchasedMovie] ([PurchasedMovieID], [MovieID], [CustomerID], [Price], [PurchaseDate], [ExpirationDate]) VALUES (7, 1, 1, CAST(4.00 AS Decimal(18, 2)), CAST(N'2017-10-22 16:06:51.000' AS DateTime), CAST(N'2017-10-24 16:06:51.000' AS DateTime))
95 | GO
96 | SET IDENTITY_INSERT [dbo].[PurchasedMovie] OFF
97 | GO
98 | ALTER TABLE [dbo].[PurchasedMovie] WITH CHECK ADD CONSTRAINT [FK_PurchasedMovie_Customer] FOREIGN KEY([CustomerID])
99 | REFERENCES [dbo].[Customer] ([CustomerID])
100 | GO
101 | ALTER TABLE [dbo].[PurchasedMovie] CHECK CONSTRAINT [FK_PurchasedMovie_Customer]
102 | GO
103 | ALTER TABLE [dbo].[PurchasedMovie] WITH CHECK ADD CONSTRAINT [FK_PurchasedMovie_Movie] FOREIGN KEY([MovieID])
104 | REFERENCES [dbo].[Movie] ([MovieID])
105 | GO
106 | ALTER TABLE [dbo].[PurchasedMovie] CHECK CONSTRAINT [FK_PurchasedMovie_Movie]
107 | GO
108 |
--------------------------------------------------------------------------------
/After/src/Logic/Common/Entity.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using NHibernate.Proxy;
3 |
4 | namespace Logic.Common
5 | {
6 | public abstract class Entity
7 | {
8 | public virtual long Id { get; protected set; }
9 |
10 | public override bool Equals(object obj)
11 | {
12 | var other = obj as Entity;
13 |
14 | if (ReferenceEquals(other, null))
15 | return false;
16 |
17 | if (ReferenceEquals(this, other))
18 | return true;
19 |
20 | if (GetRealType() != other.GetRealType())
21 | return false;
22 |
23 | if (Id == 0 || other.Id == 0)
24 | return false;
25 |
26 | return Id == other.Id;
27 | }
28 |
29 | public static bool operator ==(Entity a, Entity b)
30 | {
31 | if (ReferenceEquals(a, null) && ReferenceEquals(b, null))
32 | return true;
33 |
34 | if (ReferenceEquals(a, null) || ReferenceEquals(b, null))
35 | return false;
36 |
37 | return a.Equals(b);
38 | }
39 |
40 | public static bool operator !=(Entity a, Entity b)
41 | {
42 | return !(a == b);
43 | }
44 |
45 | public override int GetHashCode()
46 | {
47 | return (GetRealType().ToString() + Id).GetHashCode();
48 | }
49 |
50 | private Type GetRealType()
51 | {
52 | return NHibernateProxyHelper.GetClassWithoutInitializingProxy(this);
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/After/src/Logic/Common/Repository.cs:
--------------------------------------------------------------------------------
1 | using Logic.Utils;
2 |
3 | namespace Logic.Common
4 | {
5 | public abstract class Repository
6 | where T : Entity
7 | {
8 | protected readonly UnitOfWork _unitOfWork;
9 |
10 | protected Repository(UnitOfWork unitOfWork)
11 | {
12 | _unitOfWork = unitOfWork;
13 | }
14 |
15 | public T GetById(long id)
16 | {
17 | return _unitOfWork.Get(id);
18 | }
19 |
20 | public void Add(T entity)
21 | {
22 | _unitOfWork.SaveOrUpdate(entity);
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/After/src/Logic/Customers/Customer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using CSharpFunctionalExtensions;
5 | using Logic.Common;
6 | using Logic.Movies;
7 |
8 | namespace Logic.Customers
9 | {
10 | public class Customer : Entity
11 | {
12 | private string _name;
13 | public virtual CustomerName Name
14 | {
15 | get => (CustomerName)_name;
16 | set => _name = value;
17 | }
18 |
19 | private readonly string _email;
20 | public virtual Email Email => (Email)_email;
21 |
22 | public virtual CustomerStatus Status { get; protected set; }
23 |
24 | private decimal _moneySpent;
25 | public virtual Dollars MoneySpent
26 | {
27 | get => Dollars.Of(_moneySpent);
28 | protected set => _moneySpent = value;
29 | }
30 |
31 | private readonly IList _purchasedMovies;
32 | public virtual IReadOnlyList PurchasedMovies => _purchasedMovies.ToList();
33 |
34 | protected Customer()
35 | {
36 | _purchasedMovies = new List();
37 | }
38 |
39 | public Customer(CustomerName name, Email email) : this()
40 | {
41 | _name = name ?? throw new ArgumentNullException(nameof(name));
42 | _email = email ?? throw new ArgumentNullException(nameof(email));
43 |
44 | MoneySpent = Dollars.Of(0);
45 | Status = CustomerStatus.Regular;
46 | }
47 |
48 | public virtual bool HasPurchasedMovie(Movie movie)
49 | {
50 | return PurchasedMovies.Any(x => x.Movie == movie && !x.ExpirationDate.IsExpired);
51 | }
52 |
53 | public virtual void PurchaseMovie(Movie movie)
54 | {
55 | if (HasPurchasedMovie(movie))
56 | throw new Exception();
57 |
58 | ExpirationDate expirationDate = movie.GetExpirationDate();
59 | Dollars price = movie.CalculatePrice(Status);
60 |
61 | var purchasedMovie = new PurchasedMovie(movie, this, price, expirationDate);
62 | _purchasedMovies.Add(purchasedMovie);
63 |
64 | MoneySpent += price;
65 | }
66 |
67 | public virtual Result CanPromote()
68 | {
69 | if (Status.IsAdvanced)
70 | return Result.Fail("The customer already has the Advanced status");
71 |
72 | if (PurchasedMovies.Count(x =>
73 | x.ExpirationDate == ExpirationDate.Infinite || x.ExpirationDate.Date >= DateTime.UtcNow.AddDays(-30)) < 2)
74 | return Result.Fail("The customer has to have at least 2 active movies during the last 30 days");
75 |
76 | if (PurchasedMovies.Where(x => x.PurchaseDate > DateTime.UtcNow.AddYears(-1)).Sum(x => x.Price) < 100m)
77 | return Result.Fail("The customer has to have at least 100 dollars spent during the last year");
78 |
79 | return Result.Ok();
80 | }
81 |
82 | public virtual void Promote()
83 | {
84 | if (CanPromote().IsFailure)
85 | throw new Exception();
86 |
87 | Status = Status.Promote();
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/After/src/Logic/Customers/CustomerMap.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using FluentNHibernate.Mapping;
3 |
4 | namespace Logic.Customers
5 | {
6 | public class CustomerMap : ClassMap
7 | {
8 | public CustomerMap()
9 | {
10 | Id(x => x.Id);
11 |
12 | Map(x => x.Name).CustomType().Access.CamelCaseField(Prefix.Underscore);
13 | Map(x => x.Email).CustomType().Access.CamelCaseField(Prefix.Underscore);
14 | Map(x => x.MoneySpent).CustomType().Access.CamelCaseField(Prefix.Underscore);
15 |
16 | Component(x => x.Status, y =>
17 | {
18 | y.Map(x => x.Type, "Status").CustomType();
19 | y.Map(x => x.ExpirationDate, "StatusExpirationDate").CustomType()
20 | .Access.CamelCaseField(Prefix.Underscore)
21 | .Nullable();
22 | });
23 |
24 | HasMany(x => x.PurchasedMovies).Access.CamelCaseField(Prefix.Underscore); ;
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/After/src/Logic/Customers/CustomerName.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using CSharpFunctionalExtensions;
3 |
4 | namespace Logic.Customers
5 | {
6 | public class CustomerName : ValueObject
7 | {
8 | public string Value { get; }
9 |
10 | private CustomerName(string value)
11 | {
12 | Value = value;
13 | }
14 |
15 | public static Result Create(string customerName)
16 | {
17 | customerName = (customerName ?? string.Empty).Trim();
18 |
19 | if (customerName.Length == 0)
20 | return Result.Fail("Customer name should not be empty");
21 |
22 | if (customerName.Length > 50)
23 | return Result.Fail("Customer name is too long");
24 |
25 | return Result.Ok(new CustomerName(customerName));
26 | }
27 |
28 | protected override bool EqualsCore(CustomerName other)
29 | {
30 | return Value.Equals(other.Value, StringComparison.InvariantCultureIgnoreCase);
31 | }
32 |
33 | protected override int GetHashCodeCore()
34 | {
35 | return Value.GetHashCode();
36 | }
37 |
38 | public static implicit operator string(CustomerName customerName)
39 | {
40 | return customerName.Value;
41 | }
42 |
43 | public static explicit operator CustomerName(string customerName)
44 | {
45 | return Create(customerName).Value;
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/After/src/Logic/Customers/CustomerRepository.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using Logic.Common;
4 | using Logic.Utils;
5 |
6 | namespace Logic.Customers
7 | {
8 | public class CustomerRepository : Repository
9 | {
10 | public CustomerRepository(UnitOfWork unitOfWork)
11 | : base(unitOfWork)
12 | {
13 | }
14 |
15 | public IReadOnlyList GetList()
16 | {
17 | return _unitOfWork
18 | .Query()
19 | .ToList();
20 | }
21 |
22 | public Customer GetByEmail(Email email)
23 | {
24 | return _unitOfWork
25 | .Query()
26 | .SingleOrDefault(x => x.Email == email.Value);
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/After/src/Logic/Customers/CustomerStatus.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using CSharpFunctionalExtensions;
3 |
4 | namespace Logic.Customers
5 | {
6 | public class CustomerStatus : ValueObject
7 | {
8 | public static readonly CustomerStatus Regular = new CustomerStatus(CustomerStatusType.Regular, ExpirationDate.Infinite);
9 |
10 | public CustomerStatusType Type { get; }
11 |
12 | private readonly DateTime? _expirationDate;
13 | public ExpirationDate ExpirationDate => (ExpirationDate)_expirationDate;
14 |
15 | public bool IsAdvanced => Type == CustomerStatusType.Advanced && !ExpirationDate.IsExpired;
16 |
17 | private CustomerStatus()
18 | {
19 | }
20 |
21 | private CustomerStatus(CustomerStatusType type, ExpirationDate expirationDate)
22 | : this()
23 | {
24 | Type = type;
25 | _expirationDate = expirationDate ?? throw new ArgumentNullException(nameof(expirationDate));
26 | }
27 |
28 | public decimal GetDiscount() => IsAdvanced ? 0.25m : 0m;
29 |
30 | public CustomerStatus Promote()
31 | {
32 | return new CustomerStatus(CustomerStatusType.Advanced, (ExpirationDate)DateTime.UtcNow.AddYears(1));
33 | }
34 |
35 | protected override bool EqualsCore(CustomerStatus other)
36 | {
37 | return Type == other.Type && ExpirationDate == other.ExpirationDate;
38 | }
39 |
40 | protected override int GetHashCodeCore()
41 | {
42 | return Type.GetHashCode() ^ ExpirationDate.GetHashCode();
43 | }
44 | }
45 |
46 |
47 | public enum CustomerStatusType
48 | {
49 | Regular = 1,
50 | Advanced = 2
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/After/src/Logic/Customers/Dollars.cs:
--------------------------------------------------------------------------------
1 | using CSharpFunctionalExtensions;
2 |
3 | namespace Logic.Customers
4 | {
5 | public class Dollars : ValueObject
6 | {
7 | private const decimal MaxDollarAmount = 1_000_000;
8 |
9 | public decimal Value { get; }
10 |
11 | public bool IsZero => Value == 0;
12 |
13 | private Dollars(decimal value)
14 | {
15 | Value = value;
16 | }
17 |
18 | public static Result Create(decimal dollarAmount)
19 | {
20 | if (dollarAmount < 0)
21 | return Result.Fail("Dollar amount cannot be negative");
22 |
23 | if (dollarAmount > MaxDollarAmount)
24 | return Result.Fail("Dollar amount cannot be greater than " + MaxDollarAmount);
25 |
26 | if (dollarAmount % 0.01m > 0)
27 | return Result.Fail("Dollar amount cannot contain part of a penny");
28 |
29 | return Result.Ok(new Dollars(dollarAmount));
30 | }
31 |
32 | public static Dollars Of(decimal dollarAmount)
33 | {
34 | return Create(dollarAmount).Value;
35 | }
36 |
37 | public static Dollars operator *(Dollars dollars, decimal multiplier)
38 | {
39 | return new Dollars(dollars.Value * multiplier);
40 | }
41 |
42 | public static Dollars operator +(Dollars dollars1, Dollars dollars2)
43 | {
44 | return new Dollars(dollars1.Value + dollars2.Value);
45 | }
46 |
47 | protected override bool EqualsCore(Dollars other)
48 | {
49 | return Value == other.Value;
50 | }
51 |
52 | protected override int GetHashCodeCore()
53 | {
54 | return Value.GetHashCode();
55 | }
56 |
57 | public static implicit operator decimal(Dollars dollars)
58 | {
59 | return dollars.Value;
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/After/src/Logic/Customers/Email.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text.RegularExpressions;
3 | using CSharpFunctionalExtensions;
4 |
5 | namespace Logic.Customers
6 | {
7 | public class Email : ValueObject
8 | {
9 | public string Value { get; }
10 |
11 | private Email(string value)
12 | {
13 | Value = value;
14 | }
15 |
16 | public static Result Create(string email)
17 | {
18 | email = (email ?? string.Empty).Trim();
19 |
20 | if (email.Length == 0)
21 | return Result.Fail("Email should not be empty");
22 |
23 | if(email.Length > 150)
24 | return Result.Fail("Email is too long");
25 |
26 | if (!Regex.IsMatch(email, @"^(.+)@(.+)$"))
27 | return Result.Fail("Email is invalid");
28 |
29 | return Result.Ok(new Email(email));
30 | }
31 |
32 | protected override bool EqualsCore(Email other)
33 | {
34 | return Value.Equals(other.Value, StringComparison.InvariantCultureIgnoreCase);
35 | }
36 |
37 | protected override int GetHashCodeCore()
38 | {
39 | return Value.GetHashCode();
40 | }
41 |
42 | public static explicit operator Email(string email)
43 | {
44 | return Create(email).Value;
45 | }
46 |
47 | public static implicit operator string(Email email)
48 | {
49 | return email.Value;
50 | }
51 | }
52 | }
--------------------------------------------------------------------------------
/After/src/Logic/Customers/ExpirationDate.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using CSharpFunctionalExtensions;
3 |
4 | namespace Logic.Customers
5 | {
6 | public class ExpirationDate : ValueObject
7 | {
8 | public static readonly ExpirationDate Infinite = new ExpirationDate(null);
9 |
10 | public DateTime? Date { get; }
11 |
12 | public bool IsExpired => this != Infinite && Date < DateTime.UtcNow;
13 |
14 | private ExpirationDate(DateTime? date)
15 | {
16 | Date = date;
17 | }
18 |
19 | public static Result Create(DateTime date)
20 | {
21 | return Result.Ok(new ExpirationDate(date));
22 | }
23 |
24 | protected override bool EqualsCore(ExpirationDate other)
25 | {
26 | return Date == other.Date;
27 | }
28 |
29 | protected override int GetHashCodeCore()
30 | {
31 | return Date.GetHashCode();
32 | }
33 |
34 | public static explicit operator ExpirationDate(DateTime? date)
35 | {
36 | if (date.HasValue)
37 | return Create(date.Value).Value;
38 |
39 | return Infinite;
40 | }
41 |
42 | public static implicit operator DateTime? (ExpirationDate date)
43 | {
44 | return date.Date;
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/After/src/Logic/Customers/LicensingModel.cs:
--------------------------------------------------------------------------------
1 | namespace Logic.Customers
2 | {
3 | public enum LicensingModel
4 | {
5 | TwoDays = 1,
6 | LifeLong = 2
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/After/src/Logic/Customers/PurchasedMovie.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Logic.Common;
3 | using Logic.Movies;
4 |
5 | namespace Logic.Customers
6 | {
7 | public class PurchasedMovie : Entity
8 | {
9 | public virtual Movie Movie { get; protected set; }
10 | public virtual Customer Customer { get; protected set; }
11 |
12 | private decimal _price;
13 | public virtual Dollars Price
14 | {
15 | get => Dollars.Of(_price);
16 | protected set => _price = value;
17 | }
18 |
19 | public virtual DateTime PurchaseDate { get; protected set; }
20 |
21 | private DateTime? _expirationDate;
22 | public virtual ExpirationDate ExpirationDate
23 | {
24 | get => (ExpirationDate)_expirationDate;
25 | protected set => _expirationDate = value;
26 | }
27 |
28 | protected PurchasedMovie()
29 | {
30 | }
31 |
32 | internal PurchasedMovie(Movie movie, Customer customer, Dollars price, ExpirationDate expirationDate)
33 | {
34 | if (price == null || price.IsZero)
35 | throw new ArgumentException(nameof(price));
36 | if (expirationDate == null || expirationDate.IsExpired)
37 | throw new ArgumentException(nameof(expirationDate));
38 |
39 | Movie = movie ?? throw new ArgumentNullException(nameof(movie));
40 | Customer = customer ?? throw new ArgumentNullException(nameof(customer));
41 | Price = price;
42 | ExpirationDate = expirationDate;
43 | PurchaseDate = DateTime.UtcNow;
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/After/src/Logic/Customers/PurchasedMovieMap.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using FluentNHibernate.Mapping;
3 |
4 | namespace Logic.Customers
5 | {
6 | public class PurchasedMovieMap : ClassMap
7 | {
8 | public PurchasedMovieMap()
9 | {
10 | Id(x => x.Id);
11 |
12 | Map(x => x.Price).CustomType().Access.CamelCaseField(Prefix.Underscore);
13 | Map(x => x.PurchaseDate);
14 | Map(x => x.ExpirationDate).CustomType().Access.CamelCaseField(Prefix.Underscore).Nullable();
15 |
16 | References(x => x.Movie);
17 | References(x => x.Customer);
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/After/src/Logic/Logic.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {0B9EC24F-E84F-4375-A7B0-0F8C3A4B015A}
8 | Library
9 | Properties
10 | Logic
11 | Logic
12 | v4.6.2
13 | 512
14 |
15 |
16 | true
17 | full
18 | false
19 | bin\Debug\
20 | DEBUG;TRACE
21 | prompt
22 | 4
23 |
24 |
25 | pdbonly
26 | true
27 | bin\Release\
28 | TRACE
29 | prompt
30 | 4
31 |
32 |
33 |
34 | ..\..\packages\CSharpFunctionalExtensions.1.7.1\lib\netstandard1.0\CSharpFunctionalExtensions.dll
35 |
36 |
37 | ..\..\packages\FluentNHibernate.2.0.3.0\lib\net40\FluentNHibernate.dll
38 |
39 |
40 | ..\..\packages\Iesi.Collections.4.0.0.4000\lib\net40\Iesi.Collections.dll
41 |
42 |
43 | ..\..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll
44 |
45 |
46 | ..\..\packages\NHibernate.4.1.1.4000\lib\net40\NHibernate.dll
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/After/src/Logic/Movies/Movie.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Logic.Common;
3 | using Logic.Customers;
4 |
5 | namespace Logic.Movies
6 | {
7 | public abstract class Movie : Entity
8 | {
9 | public virtual string Name { get; protected set; }
10 | protected virtual LicensingModel LicensingModel { get; set; }
11 |
12 | public abstract ExpirationDate GetExpirationDate();
13 |
14 | public virtual Dollars CalculatePrice(CustomerStatus status)
15 | {
16 | decimal modifier = 1 - status.GetDiscount();
17 | return GetBasePrice() * modifier;
18 | }
19 |
20 | protected abstract Dollars GetBasePrice();
21 | }
22 |
23 | public class TwoDaysMovie : Movie
24 | {
25 | public override ExpirationDate GetExpirationDate()
26 | {
27 | return (ExpirationDate)DateTime.UtcNow.AddDays(2);
28 | }
29 |
30 | protected override Dollars GetBasePrice()
31 | {
32 | return Dollars.Of(4);
33 | }
34 | }
35 |
36 | public class LifeLongMovie : Movie
37 | {
38 | public override ExpirationDate GetExpirationDate()
39 | {
40 | return ExpirationDate.Infinite;
41 | }
42 |
43 | protected override Dollars GetBasePrice()
44 | {
45 | return Dollars.Of(8);
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/After/src/Logic/Movies/MovieMap.cs:
--------------------------------------------------------------------------------
1 | using FluentNHibernate;
2 | using FluentNHibernate.Mapping;
3 |
4 | namespace Logic.Movies
5 | {
6 | public class MovieMap : ClassMap
7 | {
8 | public MovieMap()
9 | {
10 | Id(x => x.Id);
11 |
12 | DiscriminateSubClassesOnColumn("LicensingModel");
13 |
14 | Map(x => x.Name);
15 | Map(Reveal.Member("LicensingModel")).CustomType();
16 | }
17 | }
18 |
19 | public class TwoDaysMovieMap : SubclassMap
20 | {
21 | public TwoDaysMovieMap()
22 | {
23 | DiscriminatorValue(1);
24 | }
25 | }
26 |
27 | public class LifeLongMovieMap : SubclassMap
28 | {
29 | public LifeLongMovieMap()
30 | {
31 | DiscriminatorValue(2);
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/After/src/Logic/Movies/MovieRepository.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using Logic.Common;
4 | using Logic.Utils;
5 |
6 | namespace Logic.Movies
7 | {
8 | public class MovieRepository : Repository
9 | {
10 | public MovieRepository(UnitOfWork unitOfWork)
11 | : base(unitOfWork)
12 | {
13 | }
14 |
15 | public IReadOnlyList GetList()
16 | {
17 | return _unitOfWork.Query().ToList();
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/After/src/Logic/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("Logic")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("Logic")]
13 | [assembly: AssemblyCopyright("Copyright © 2017")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // Setting ComVisible to false makes the types in this assembly not visible
18 | // to COM components. If you need to access a type in this assembly from
19 | // COM, set the ComVisible attribute to true on that type.
20 | [assembly: ComVisible(false)]
21 |
22 | // The following GUID is for the ID of the typelib if this project is exposed to COM
23 | [assembly: Guid("0b9ec24f-e84f-4375-a7b0-0f8c3a4b015a")]
24 |
25 | // Version information for an assembly consists of the following four values:
26 | //
27 | // Major Version
28 | // Minor Version
29 | // Build Number
30 | // Revision
31 | //
32 | // You can specify all the values or you can default the Build and Revision Numbers
33 | // by using the '*' as shown below:
34 | // [assembly: AssemblyVersion("1.0.*")]
35 | [assembly: AssemblyVersion("1.0.0.0")]
36 | [assembly: AssemblyFileVersion("1.0.0.0")]
37 |
--------------------------------------------------------------------------------
/After/src/Logic/Utils/SessionFactory.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using FluentNHibernate.Cfg;
3 | using FluentNHibernate.Cfg.Db;
4 | using FluentNHibernate.Conventions;
5 | using FluentNHibernate.Conventions.AcceptanceCriteria;
6 | using FluentNHibernate.Conventions.Helpers;
7 | using FluentNHibernate.Conventions.Instances;
8 | using FluentNHibernate.Mapping;
9 | using NHibernate;
10 |
11 | namespace Logic.Utils
12 | {
13 | public class SessionFactory
14 | {
15 | private readonly ISessionFactory _factory;
16 |
17 | public SessionFactory(string connectionString)
18 | {
19 | _factory = BuildSessionFactory(connectionString);
20 | }
21 |
22 | internal ISession OpenSession()
23 | {
24 | return _factory.OpenSession();
25 | }
26 |
27 | private static ISessionFactory BuildSessionFactory(string connectionString)
28 | {
29 | FluentConfiguration configuration = Fluently.Configure()
30 | .Database(MsSqlConfiguration.MsSql2012.ConnectionString(connectionString))
31 | .Mappings(m => m.FluentMappings
32 | .AddFromAssembly(Assembly.GetExecutingAssembly())
33 | .Conventions.Add(
34 | ForeignKey.EndsWith("ID"),
35 | ConventionBuilder.Property.When(criteria => criteria.Expect(x => x.Nullable, Is.Not.Set), x => x.Not.Nullable()))
36 | .Conventions.Add()
37 | .Conventions.Add()
38 | .Conventions.Add()
39 | );
40 |
41 | return configuration.BuildSessionFactory();
42 | }
43 |
44 |
45 | private class OtherConversions : IHasManyConvention, IReferenceConvention
46 | {
47 | public void Apply(IOneToManyCollectionInstance instance)
48 | {
49 | instance.LazyLoad();
50 | instance.AsBag();
51 | instance.Cascade.SaveUpdate();
52 | instance.Inverse();
53 | }
54 |
55 | public void Apply(IManyToOneInstance instance)
56 | {
57 | instance.LazyLoad(Laziness.Proxy);
58 | instance.Cascade.None();
59 | instance.Not.Nullable();
60 | }
61 | }
62 |
63 |
64 | public class TableNameConvention : IClassConvention
65 | {
66 | public void Apply(IClassInstance instance)
67 | {
68 | instance.Table(instance.EntityType.Name);
69 | }
70 | }
71 |
72 |
73 | public class HiLoConvention : IIdConvention
74 | {
75 | public void Apply(IIdentityInstance instance)
76 | {
77 | instance.Column(instance.EntityType.Name + "ID");
78 | instance.GeneratedBy.Native();
79 | }
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/After/src/Logic/Utils/UnitOfWork.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Data;
3 | using System.Linq;
4 |
5 | using NHibernate;
6 | using NHibernate.Linq;
7 |
8 | namespace Logic.Utils
9 | {
10 | public class UnitOfWork
11 | {
12 | private readonly ISession _session;
13 | private readonly ITransaction _transaction;
14 | private bool _isAlive = true;
15 |
16 | public UnitOfWork(SessionFactory sessionFactory)
17 | {
18 | _session = sessionFactory.OpenSession();
19 | _transaction = _session.BeginTransaction(IsolationLevel.ReadCommitted);
20 | }
21 |
22 | public void Commit()
23 | {
24 | if (!_isAlive)
25 | return;
26 |
27 | try
28 | {
29 | _transaction.Commit();
30 | }
31 | finally
32 | {
33 | _isAlive = false;
34 | _transaction.Dispose();
35 | _session.Dispose();
36 | }
37 | }
38 |
39 | internal T Get(long id)
40 | where T : class
41 | {
42 | return _session.Get(id);
43 | }
44 |
45 | internal void SaveOrUpdate(T entity)
46 | {
47 | _session.SaveOrUpdate(entity);
48 | }
49 |
50 | internal void Delete(T entity)
51 | {
52 | _session.Delete(entity);
53 | }
54 |
55 | public IQueryable Query()
56 | {
57 | return _session.Query();
58 | }
59 |
60 | public ISQLQuery CreateSQLQuery(string q)
61 | {
62 | return _session.CreateSQLQuery(q);
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/After/src/Logic/app.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/After/src/Logic/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Before/OnlineTheaterBefore.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.26730.12
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api", "src\Api\Api.csproj", "{36484BFF-E628-49BD-BF49-01BAD88D69E3}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logic", "src\Logic\Logic.csproj", "{0B9EC24F-E84F-4375-A7B0-0F8C3A4B015A}"
9 | EndProject
10 | Global
11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
12 | Debug|Any CPU = Debug|Any CPU
13 | Release|Any CPU = Release|Any CPU
14 | EndGlobalSection
15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
16 | {36484BFF-E628-49BD-BF49-01BAD88D69E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17 | {36484BFF-E628-49BD-BF49-01BAD88D69E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
18 | {36484BFF-E628-49BD-BF49-01BAD88D69E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
19 | {36484BFF-E628-49BD-BF49-01BAD88D69E3}.Release|Any CPU.Build.0 = Release|Any CPU
20 | {0B9EC24F-E84F-4375-A7B0-0F8C3A4B015A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {0B9EC24F-E84F-4375-A7B0-0F8C3A4B015A}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {0B9EC24F-E84F-4375-A7B0-0F8C3A4B015A}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {0B9EC24F-E84F-4375-A7B0-0F8C3A4B015A}.Release|Any CPU.Build.0 = Release|Any CPU
24 | EndGlobalSection
25 | GlobalSection(SolutionProperties) = preSolution
26 | HideSolutionNode = FALSE
27 | EndGlobalSection
28 | GlobalSection(ExtensibilityGlobals) = postSolution
29 | SolutionGuid = {2AF79079-DA8B-4E50-8B02-9F1EC4FE3476}
30 | EndGlobalSection
31 | EndGlobal
32 |
--------------------------------------------------------------------------------
/Before/src/Api/Api.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net462
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/Before/src/Api/Controllers/CustomersController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using Logic.Entities;
5 | using Logic.Repositories;
6 | using Logic.Services;
7 | using Microsoft.AspNetCore.Mvc;
8 |
9 | namespace Api.Controllers
10 | {
11 | [Route("api/[controller]")]
12 | public class CustomersController : Controller
13 | {
14 | private readonly MovieRepository _movieRepository;
15 | private readonly CustomerRepository _customerRepository;
16 | private readonly CustomerService _customerService;
17 |
18 | public CustomersController(MovieRepository movieRepository, CustomerRepository customerRepository, CustomerService customerService)
19 | {
20 | _customerRepository = customerRepository;
21 | _movieRepository = movieRepository;
22 | _customerService = customerService;
23 | }
24 |
25 | [HttpGet]
26 | [Route("{id}")]
27 | public IActionResult Get(long id)
28 | {
29 | Customer customer = _customerRepository.GetById(id);
30 | if (customer == null)
31 | {
32 | return NotFound();
33 | }
34 |
35 | return Json(customer);
36 | }
37 |
38 | [HttpGet]
39 | public JsonResult GetList()
40 | {
41 | IReadOnlyList customers = _customerRepository.GetList();
42 | return Json(customers);
43 | }
44 |
45 | [HttpPost]
46 | public IActionResult Create([FromBody] Customer item)
47 | {
48 | try
49 | {
50 | if (!ModelState.IsValid)
51 | {
52 | return BadRequest(ModelState);
53 | }
54 |
55 | if (_customerRepository.GetByEmail(item.Email) != null)
56 | {
57 | return BadRequest("Email is already in use: " + item.Email);
58 | }
59 |
60 | item.Id = 0;
61 | item.Status = CustomerStatus.Regular;
62 | _customerRepository.Add(item);
63 | _customerRepository.SaveChanges();
64 |
65 | return Ok();
66 | }
67 | catch (Exception e)
68 | {
69 | return StatusCode(500, new { error = e.Message });
70 | }
71 | }
72 |
73 | [HttpPut]
74 | [Route("{id}")]
75 | public IActionResult Update(long id, [FromBody] Customer item)
76 | {
77 | try
78 | {
79 | if (!ModelState.IsValid)
80 | {
81 | return BadRequest(ModelState);
82 | }
83 |
84 | Customer customer = _customerRepository.GetById(id);
85 | if (customer == null)
86 | {
87 | return BadRequest("Invalid customer id: " + id);
88 | }
89 |
90 | customer.Name = item.Name;
91 | _customerRepository.SaveChanges();
92 |
93 | return Ok();
94 | }
95 | catch (Exception e)
96 | {
97 | return StatusCode(500, new { error = e.Message });
98 | }
99 | }
100 |
101 | [HttpPost]
102 | [Route("{id}/movies")]
103 | public IActionResult PurchaseMovie(long id, [FromBody] long movieId)
104 | {
105 | try
106 | {
107 | Movie movie = _movieRepository.GetById(movieId);
108 | if (movie == null)
109 | {
110 | return BadRequest("Invalid movie id: " + movieId);
111 | }
112 |
113 | Customer customer = _customerRepository.GetById(id);
114 | if (customer == null)
115 | {
116 | return BadRequest("Invalid customer id: " + id);
117 | }
118 |
119 | if (customer.PurchasedMovies.Any(x => x.MovieId == movie.Id && (x.ExpirationDate == null || x.ExpirationDate.Value >= DateTime.UtcNow)))
120 | {
121 | return BadRequest("The movie is already purchased: " + movie.Name);
122 | }
123 |
124 | _customerService.PurchaseMovie(customer, movie);
125 |
126 | _customerRepository.SaveChanges();
127 |
128 | return Ok();
129 | }
130 | catch (Exception e)
131 | {
132 | return StatusCode(500, new { error = e.Message });
133 | }
134 | }
135 |
136 | [HttpPost]
137 | [Route("{id}/promotion")]
138 | public IActionResult PromoteCustomer(long id)
139 | {
140 | try
141 | {
142 | Customer customer = _customerRepository.GetById(id);
143 | if (customer == null)
144 | {
145 | return BadRequest("Invalid customer id: " + id);
146 | }
147 |
148 | if (customer.Status == CustomerStatus.Advanced && (customer.StatusExpirationDate == null || customer.StatusExpirationDate.Value < DateTime.UtcNow))
149 | {
150 | return BadRequest("The customer already has the Advanced status");
151 | }
152 |
153 | bool success = _customerService.PromoteCustomer(customer);
154 | if (!success)
155 | {
156 | return BadRequest("Cannot promote the customer");
157 | }
158 |
159 | _customerRepository.SaveChanges();
160 |
161 | return Ok();
162 | }
163 | catch (Exception e)
164 | {
165 | return StatusCode(500, new { error = e.Message });
166 | }
167 | }
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/Before/src/Api/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore;
2 | using Microsoft.AspNetCore.Hosting;
3 |
4 | namespace Api
5 | {
6 | public class Program
7 | {
8 | public static void Main(string[] args)
9 | {
10 | BuildWebHost(args).Run();
11 | }
12 |
13 | public static IWebHost BuildWebHost(string[] args)
14 | {
15 | return WebHost.CreateDefaultBuilder(args)
16 | .UseStartup()
17 | .Build();
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Before/src/Api/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "iisSettings": {
3 | "windowsAuthentication": false,
4 | "anonymousAuthentication": true,
5 | "iisExpress": {
6 | "applicationUrl": "http://localhost:60367/",
7 | "sslPort": 0
8 | }
9 | },
10 | "profiles": {
11 | "IIS Express": {
12 | "commandName": "IISExpress",
13 | "launchBrowser": true,
14 | "launchUrl": "api/customers",
15 | "environmentVariables": {
16 | "ASPNETCORE_ENVIRONMENT": "Development"
17 | }
18 | },
19 | "Api": {
20 | "commandName": "Project",
21 | "launchBrowser": true,
22 | "launchUrl": "api/customers",
23 | "environmentVariables": {
24 | "ASPNETCORE_ENVIRONMENT": "Development"
25 | },
26 | "applicationUrl": "http://localhost:60368/"
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Before/src/Api/Startup.cs:
--------------------------------------------------------------------------------
1 | using Logic.Repositories;
2 | using Logic.Services;
3 | using Logic.Utils;
4 | using Microsoft.AspNetCore.Builder;
5 | using Microsoft.AspNetCore.Hosting;
6 | using Microsoft.Extensions.Configuration;
7 | using Microsoft.Extensions.DependencyInjection;
8 |
9 | namespace Api
10 | {
11 | public class Startup
12 | {
13 | public Startup(IConfiguration configuration)
14 | {
15 | Configuration = configuration;
16 | }
17 |
18 | public IConfiguration Configuration { get; }
19 |
20 | public void ConfigureServices(IServiceCollection services)
21 | {
22 | services.AddMvc();
23 |
24 | services.AddSingleton(new SessionFactory(Configuration["ConnectionString"]));
25 | services.AddScoped();
26 | services.AddTransient();
27 | services.AddTransient();
28 | services.AddTransient();
29 | services.AddTransient();
30 | }
31 |
32 | public void Configure(IApplicationBuilder app, IHostingEnvironment env)
33 | {
34 | if (env.IsDevelopment())
35 | {
36 | app.UseDeveloperExceptionPage();
37 | }
38 |
39 | app.UseMvc();
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Before/src/Api/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "IncludeScopes": false,
4 | "LogLevel": {
5 | "Default": "Debug",
6 | "System": "Information",
7 | "Microsoft": "Information"
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Before/src/Api/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "IncludeScopes": false,
4 | "Debug": {
5 | "LogLevel": {
6 | "Default": "Warning"
7 | }
8 | },
9 | "Console": {
10 | "LogLevel": {
11 | "Default": "Warning"
12 | }
13 | }
14 | },
15 | "ConnectionString": "Server=.\\Sql;Database=OnlineTheater;Trusted_Connection=true;"
16 | }
17 |
--------------------------------------------------------------------------------
/Before/src/Logic/Entities/Customer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.ComponentModel.DataAnnotations;
4 | using Newtonsoft.Json;
5 | using Newtonsoft.Json.Converters;
6 |
7 | namespace Logic.Entities
8 | {
9 | public class Customer : Entity
10 | {
11 | [Required]
12 | [MaxLength(100, ErrorMessage = "Name is too long")]
13 | public virtual string Name { get; set; }
14 |
15 | [Required]
16 | [RegularExpression(@"^(.+)@(.+)$", ErrorMessage = "Email is invalid")]
17 | public virtual string Email { get; set; }
18 |
19 | [JsonConverter(typeof(StringEnumConverter))]
20 | public virtual CustomerStatus Status { get; set; }
21 |
22 | public virtual DateTime? StatusExpirationDate { get; set; }
23 |
24 | public virtual decimal MoneySpent { get; set; }
25 |
26 | public virtual IList PurchasedMovies { get; set; }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Before/src/Logic/Entities/CustomerStatus.cs:
--------------------------------------------------------------------------------
1 | namespace Logic.Entities
2 | {
3 | public enum CustomerStatus
4 | {
5 | Regular = 1,
6 | Advanced = 2
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/Before/src/Logic/Entities/Entity.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using NHibernate.Proxy;
3 |
4 | namespace Logic.Entities
5 | {
6 | public abstract class Entity
7 | {
8 | public virtual long Id { get; set; }
9 |
10 | public override bool Equals(object obj)
11 | {
12 | var other = obj as Entity;
13 |
14 | if (ReferenceEquals(other, null))
15 | return false;
16 |
17 | if (ReferenceEquals(this, other))
18 | return true;
19 |
20 | if (GetRealType() != other.GetRealType())
21 | return false;
22 |
23 | if (Id == 0 || other.Id == 0)
24 | return false;
25 |
26 | return Id == other.Id;
27 | }
28 |
29 | public static bool operator ==(Entity a, Entity b)
30 | {
31 | if (ReferenceEquals(a, null) && ReferenceEquals(b, null))
32 | return true;
33 |
34 | if (ReferenceEquals(a, null) || ReferenceEquals(b, null))
35 | return false;
36 |
37 | return a.Equals(b);
38 | }
39 |
40 | public static bool operator !=(Entity a, Entity b)
41 | {
42 | return !(a == b);
43 | }
44 |
45 | public override int GetHashCode()
46 | {
47 | return (GetRealType().ToString() + Id).GetHashCode();
48 | }
49 |
50 | private Type GetRealType()
51 | {
52 | return NHibernateProxyHelper.GetClassWithoutInitializingProxy(this);
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Before/src/Logic/Entities/LicensingModel.cs:
--------------------------------------------------------------------------------
1 | namespace Logic.Entities
2 | {
3 | public enum LicensingModel
4 | {
5 | TwoDays = 1,
6 | LifeLong = 2
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/Before/src/Logic/Entities/Movie.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 |
3 | namespace Logic.Entities
4 | {
5 | public class Movie : Entity
6 | {
7 | public virtual string Name { get; set; }
8 |
9 | [JsonIgnore]
10 | public virtual LicensingModel LicensingModel { get; set; }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Before/src/Logic/Entities/PurchasedMovie.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Newtonsoft.Json;
3 |
4 | namespace Logic.Entities
5 | {
6 | public class PurchasedMovie : Entity
7 | {
8 | [JsonIgnore]
9 | public virtual long MovieId { get; set; }
10 |
11 | public virtual Movie Movie { get; set; }
12 |
13 | [JsonIgnore]
14 | public virtual long CustomerId { get; set; }
15 |
16 | public virtual decimal Price { get; set; }
17 |
18 | public virtual DateTime PurchaseDate { get; set; }
19 |
20 | public virtual DateTime? ExpirationDate { get; set; }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Before/src/Logic/Logic.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {0B9EC24F-E84F-4375-A7B0-0F8C3A4B015A}
8 | Library
9 | Properties
10 | Logic
11 | Logic
12 | v4.6.2
13 | 512
14 |
15 |
16 | true
17 | full
18 | false
19 | bin\Debug\
20 | DEBUG;TRACE
21 | prompt
22 | 4
23 |
24 |
25 | pdbonly
26 | true
27 | bin\Release\
28 | TRACE
29 | prompt
30 | 4
31 |
32 |
33 |
34 | ..\..\packages\FluentNHibernate.2.0.3.0\lib\net40\FluentNHibernate.dll
35 |
36 |
37 | ..\..\packages\Iesi.Collections.4.0.0.4000\lib\net40\Iesi.Collections.dll
38 |
39 |
40 | ..\..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll
41 |
42 |
43 | ..\..\packages\NHibernate.4.1.1.4000\lib\net40\NHibernate.dll
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/Before/src/Logic/Mappings/CustomerMap.cs:
--------------------------------------------------------------------------------
1 | using FluentNHibernate.Mapping;
2 | using Logic.Entities;
3 |
4 | namespace Logic.Mappings
5 | {
6 | public class CustomerMap : ClassMap
7 | {
8 | public CustomerMap()
9 | {
10 | Id(x => x.Id);
11 |
12 | Map(x => x.Name);
13 | Map(x => x.Email);
14 | Map(x => x.Status).CustomType();
15 | Map(x => x.StatusExpirationDate).Nullable();
16 | Map(x => x.MoneySpent);
17 |
18 | HasMany(x => x.PurchasedMovies);
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Before/src/Logic/Mappings/MovieMap.cs:
--------------------------------------------------------------------------------
1 | using FluentNHibernate.Mapping;
2 | using Logic.Entities;
3 |
4 | namespace Logic.Mappings
5 | {
6 | public class MovieMap : ClassMap
7 | {
8 | public MovieMap()
9 | {
10 | Id(x => x.Id);
11 |
12 | Map(x => x.Name);
13 | Map(x => x.LicensingModel).CustomType();
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Before/src/Logic/Mappings/PurchasedMovieMap.cs:
--------------------------------------------------------------------------------
1 | using FluentNHibernate.Mapping;
2 | using Logic.Entities;
3 |
4 | namespace Logic.Mappings
5 | {
6 | public class PurchasedMovieMap : ClassMap
7 | {
8 | public PurchasedMovieMap()
9 | {
10 | Id(x => x.Id);
11 |
12 | Map(x => x.Price);
13 | Map(x => x.PurchaseDate);
14 | Map(x => x.ExpirationDate).Nullable();
15 | Map(x => x.MovieId);
16 | Map(x => x.CustomerId);
17 |
18 | References(x => x.Movie).LazyLoad(Laziness.False).ReadOnly();
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Before/src/Logic/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("Logic")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("Logic")]
13 | [assembly: AssemblyCopyright("Copyright © 2017")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // Setting ComVisible to false makes the types in this assembly not visible
18 | // to COM components. If you need to access a type in this assembly from
19 | // COM, set the ComVisible attribute to true on that type.
20 | [assembly: ComVisible(false)]
21 |
22 | // The following GUID is for the ID of the typelib if this project is exposed to COM
23 | [assembly: Guid("0b9ec24f-e84f-4375-a7b0-0f8c3a4b015a")]
24 |
25 | // Version information for an assembly consists of the following four values:
26 | //
27 | // Major Version
28 | // Minor Version
29 | // Build Number
30 | // Revision
31 | //
32 | // You can specify all the values or you can default the Build and Revision Numbers
33 | // by using the '*' as shown below:
34 | // [assembly: AssemblyVersion("1.0.*")]
35 | [assembly: AssemblyVersion("1.0.0.0")]
36 | [assembly: AssemblyFileVersion("1.0.0.0")]
37 |
--------------------------------------------------------------------------------
/Before/src/Logic/Repositories/CustomerRepository.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using Logic.Entities;
4 | using Logic.Utils;
5 |
6 | namespace Logic.Repositories
7 | {
8 | public class CustomerRepository : Repository
9 | {
10 | public CustomerRepository(UnitOfWork unitOfWork)
11 | : base(unitOfWork)
12 | {
13 | }
14 |
15 | public IReadOnlyList GetList()
16 | {
17 | return _unitOfWork
18 | .Query()
19 | .ToList()
20 | .Select(x =>
21 | {
22 | x.PurchasedMovies = null;
23 | return x;
24 | })
25 | .ToList();
26 | }
27 |
28 | public Customer GetByEmail(string email)
29 | {
30 | return _unitOfWork
31 | .Query()
32 | .SingleOrDefault(x => x.Email == email);
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Before/src/Logic/Repositories/MovieRepository.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using Logic.Entities;
4 | using Logic.Utils;
5 |
6 | namespace Logic.Repositories
7 | {
8 | public class MovieRepository : Repository
9 | {
10 | public MovieRepository(UnitOfWork unitOfWork)
11 | : base(unitOfWork)
12 | {
13 | }
14 |
15 | public IReadOnlyList GetList()
16 | {
17 | return _unitOfWork.Query().ToList();
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Before/src/Logic/Repositories/Repository.cs:
--------------------------------------------------------------------------------
1 | using Logic.Entities;
2 | using Logic.Utils;
3 |
4 | namespace Logic.Repositories
5 | {
6 | public abstract class Repository
7 | where T : Entity
8 | {
9 | protected readonly UnitOfWork _unitOfWork;
10 |
11 | protected Repository(UnitOfWork unitOfWork)
12 | {
13 | _unitOfWork = unitOfWork;
14 | }
15 |
16 | public T GetById(long id)
17 | {
18 | return _unitOfWork.Get(id);
19 | }
20 |
21 | public void Add(T entity)
22 | {
23 | _unitOfWork.SaveOrUpdate(entity);
24 | }
25 |
26 | public void SaveChanges()
27 | {
28 | _unitOfWork.Commit();
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Before/src/Logic/Services/CustomerService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using Logic.Entities;
4 |
5 | namespace Logic.Services
6 | {
7 | public class CustomerService
8 | {
9 | private readonly MovieService _movieService;
10 |
11 | public CustomerService(MovieService movieService)
12 | {
13 | _movieService = movieService;
14 | }
15 |
16 | private decimal CalculatePrice(CustomerStatus status, DateTime? statusExpirationDate, LicensingModel licensingModel)
17 | {
18 | decimal price;
19 | switch (licensingModel)
20 | {
21 | case LicensingModel.TwoDays:
22 | price = 4;
23 | break;
24 |
25 | case LicensingModel.LifeLong:
26 | price = 8;
27 | break;
28 |
29 | default:
30 | throw new ArgumentOutOfRangeException();
31 | }
32 |
33 | if (status == CustomerStatus.Advanced && (statusExpirationDate == null || statusExpirationDate.Value >= DateTime.UtcNow))
34 | {
35 | price = price * 0.75m;
36 | }
37 |
38 | return price;
39 | }
40 |
41 | public void PurchaseMovie(Customer customer, Movie movie)
42 | {
43 | DateTime? expirationDate = _movieService.GetExpirationDate(movie.LicensingModel);
44 | decimal price = CalculatePrice(customer.Status, customer.StatusExpirationDate, movie.LicensingModel);
45 |
46 | var purchasedMovie = new PurchasedMovie
47 | {
48 | MovieId = movie.Id,
49 | CustomerId = customer.Id,
50 | ExpirationDate = expirationDate,
51 | Price = price
52 | };
53 |
54 | customer.PurchasedMovies.Add(purchasedMovie);
55 | customer.MoneySpent += price;
56 | }
57 |
58 | public bool PromoteCustomer(Customer customer)
59 | {
60 | // at least 2 active movies during the last 30 days
61 | if (customer.PurchasedMovies.Count(x => x.ExpirationDate == null || x.ExpirationDate.Value >= DateTime.UtcNow.AddDays(-30)) < 2)
62 | return false;
63 |
64 | // at least 100 dollars spent during the last year
65 | if (customer.PurchasedMovies.Where(x => x.PurchaseDate > DateTime.UtcNow.AddYears(-1)).Sum(x => x.Price) < 100m)
66 | return false;
67 |
68 | customer.Status = CustomerStatus.Advanced;
69 | customer.StatusExpirationDate = DateTime.UtcNow.AddYears(1);
70 |
71 | return true;
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/Before/src/Logic/Services/MovieService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Logic.Entities;
3 |
4 | namespace Logic.Services
5 | {
6 | public class MovieService
7 | {
8 | public DateTime? GetExpirationDate(LicensingModel licensingModel)
9 | {
10 | DateTime? result;
11 |
12 | switch (licensingModel)
13 | {
14 | case LicensingModel.TwoDays:
15 | result = DateTime.UtcNow.AddDays(2);
16 | break;
17 |
18 | case LicensingModel.LifeLong:
19 | result = null;
20 | break;
21 |
22 | default:
23 | throw new ArgumentOutOfRangeException();
24 | }
25 |
26 | return result;
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Before/src/Logic/Utils/SessionFactory.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using FluentNHibernate.Cfg;
3 | using FluentNHibernate.Cfg.Db;
4 | using FluentNHibernate.Conventions;
5 | using FluentNHibernate.Conventions.AcceptanceCriteria;
6 | using FluentNHibernate.Conventions.Helpers;
7 | using FluentNHibernate.Conventions.Instances;
8 | using FluentNHibernate.Mapping;
9 | using NHibernate;
10 |
11 | namespace Logic.Utils
12 | {
13 | public class SessionFactory
14 | {
15 | private readonly ISessionFactory _factory;
16 |
17 | public SessionFactory(string connectionString)
18 | {
19 | _factory = BuildSessionFactory(connectionString);
20 | }
21 |
22 | internal ISession OpenSession()
23 | {
24 | return _factory.OpenSession();
25 | }
26 |
27 | private static ISessionFactory BuildSessionFactory(string connectionString)
28 | {
29 | FluentConfiguration configuration = Fluently.Configure()
30 | .Database(MsSqlConfiguration.MsSql2012.ConnectionString(connectionString))
31 | .Mappings(m => m.FluentMappings
32 | .AddFromAssembly(Assembly.GetExecutingAssembly())
33 | .Conventions.Add(
34 | ForeignKey.EndsWith("ID"),
35 | ConventionBuilder.Property.When(criteria => criteria.Expect(x => x.Nullable, Is.Not.Set), x => x.Not.Nullable()))
36 | .Conventions.Add()
37 | .Conventions.Add()
38 | .Conventions.Add()
39 | );
40 |
41 | return configuration.BuildSessionFactory();
42 | }
43 |
44 |
45 | private class OtherConversions : IHasManyConvention, IReferenceConvention
46 | {
47 | public void Apply(IOneToManyCollectionInstance instance)
48 | {
49 | instance.LazyLoad();
50 | instance.AsBag();
51 | instance.Cascade.SaveUpdate();
52 | instance.Inverse();
53 | }
54 |
55 | public void Apply(IManyToOneInstance instance)
56 | {
57 | instance.LazyLoad(Laziness.Proxy);
58 | instance.Cascade.None();
59 | instance.Not.Nullable();
60 | }
61 | }
62 |
63 |
64 | public class TableNameConvention : IClassConvention
65 | {
66 | public void Apply(IClassInstance instance)
67 | {
68 | instance.Table(instance.EntityType.Name);
69 | }
70 | }
71 |
72 |
73 | public class HiLoConvention : IIdConvention
74 | {
75 | public void Apply(IIdentityInstance instance)
76 | {
77 | instance.Column(instance.EntityType.Name + "ID");
78 | instance.GeneratedBy.Native();
79 | }
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/Before/src/Logic/Utils/UnitOfWork.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Data;
3 | using System.Linq;
4 | using NHibernate;
5 | using NHibernate.Linq;
6 |
7 | namespace Logic.Utils
8 | {
9 | public class UnitOfWork : IDisposable
10 | {
11 | private readonly ISession _session;
12 | private readonly ITransaction _transaction;
13 | private bool _isAlive = true;
14 | private bool _isCommitted;
15 |
16 | public UnitOfWork(SessionFactory sessionFactory)
17 | {
18 | _session = sessionFactory.OpenSession();
19 | _transaction = _session.BeginTransaction(IsolationLevel.ReadCommitted);
20 | }
21 |
22 | public void Dispose()
23 | {
24 | if (!_isAlive)
25 | return;
26 |
27 | _isAlive = false;
28 |
29 | try
30 | {
31 | if (_isCommitted)
32 | {
33 | _transaction.Commit();
34 | }
35 | }
36 | finally
37 | {
38 | _transaction.Dispose();
39 | _session.Dispose();
40 | }
41 | }
42 |
43 | public void Commit()
44 | {
45 | if (!_isAlive)
46 | return;
47 |
48 | _isCommitted = true;
49 | }
50 |
51 | internal T Get(long id)
52 | where T : class
53 | {
54 | return _session.Get(id);
55 | }
56 |
57 | internal void SaveOrUpdate(T entity)
58 | {
59 | _session.SaveOrUpdate(entity);
60 | }
61 |
62 | internal void Delete(T entity)
63 | {
64 | _session.Delete(entity);
65 | }
66 |
67 | public IQueryable Query()
68 | {
69 | return _session.Query();
70 | }
71 |
72 | public ISQLQuery CreateSQLQuery(string q)
73 | {
74 | return _session.CreateSQLQuery(q);
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/Before/src/Logic/app.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Before/src/Logic/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Vladimir Khorikov
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 | Welcome to Refactoring from Anemic Domain Model Towards a Rich One
2 | =====================
3 |
4 | This is the source code for my Pluralsight course [Refactoring from Anemic Domain Model Towards a Rich One][L5].
5 |
6 | How to Get Started
7 | --------------
8 |
9 | There are 2 versions of the source code: Before and After. You can go ahead and look at the After version but I would recommend that you follow the course and do all refactoring steps along with me.
10 |
11 | In order to run the application, you need to create a database and change the connection string in the composition root.
12 |
13 | .NET 6
14 | --------------
15 | The version of the "Before" solution for .NET 6 can be found in a separate branch: [dotnet6][L6]
16 |
17 | [L2]: DBCreationScript.txt
18 | [L3]: DddInPractice.UI/App.xaml.cs
19 | [L1]: http://www.apache.org/licenses/LICENSE-2.0
20 | [L4]: https://www.pluralsight.com/courses/domain-driven-design-in-practice
21 | [L5]: http://www.pluralsight.com/courses/refactoring-anemic-domain-model
22 | [L6]: https://github.com/vkhorikov/AnemicDomainModel/tree/dotnet6
23 |
--------------------------------------------------------------------------------