diff --git a/TechTest/AddCustomers.sql b/TechTest/AddCustomers.sql new file mode 100644 index 0000000..8aae7ae --- /dev/null +++ b/TechTest/AddCustomers.sql @@ -0,0 +1,17 @@ +USE [Customers] +GO + +-- The initial customer table +IF (NOT EXISTS (SELECT * + FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_SCHEMA = 'dbo' + AND TABLE_NAME = 'Customer')) +BEGIN + CREATE TABLE [dbo].[Customer] + ( + [CustomerId] INT NOT NULL PRIMARY KEY IDENTITY(1,1), + [Name] NVARCHAR(100) NOT NULL, + [Country] VARCHAR(2) NOT NULL, + [DateOfBirth] DATETIME NOT NULL + ) +END \ No newline at end of file diff --git a/TechTest/AddOrders.sql b/TechTest/AddOrders.sql new file mode 100644 index 0000000..3c75b18 --- /dev/null +++ b/TechTest/AddOrders.sql @@ -0,0 +1,25 @@ +USE [Orders] +GO + +-- base table +IF (NOT EXISTS (SELECT * + FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_SCHEMA = 'dbo' + AND TABLE_NAME = 'Orders')) +BEGIN + CREATE TABLE [dbo].[Orders] + ( + [OrderId] INT NOT NULL PRIMARY KEY, + [Amount] DECIMAL(10, 2) NOT NULL, + [VAT] DECIMAL(10, 2) NOT NULL + ) +END + +-- first migration +IF NOT EXISTS(SELECT 1 FROM sys.columns + WHERE Name = N'CustomerId' + AND Object_ID = Object_ID(N'dbo.Orders')) +BEGIN + ALTER TABLE dbo.Orders + ADD CustomerId INT NOT NULL DEFAULT 0; +END \ No newline at end of file diff --git a/TechTest/AnyCompany.Data.Contract/AnyCompany.Data.Contract.csproj b/TechTest/AnyCompany.Data.Contract/AnyCompany.Data.Contract.csproj new file mode 100644 index 0000000..88f0629 --- /dev/null +++ b/TechTest/AnyCompany.Data.Contract/AnyCompany.Data.Contract.csproj @@ -0,0 +1,55 @@ + + + + + Debug + AnyCPU + {7F4D6121-26EF-48A2-A2F4-BBD5D114D8E7} + Library + Properties + AnyCompany.Data.Contract + AnyCompany.Data.Contract + v4.6.1 + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + {695D4D06-2336-4B91-BA68-4A4924CF4729} + AnyCompany.Models + + + + \ No newline at end of file diff --git a/TechTest/AnyCompany.Data.Contract/Properties/AssemblyInfo.cs b/TechTest/AnyCompany.Data.Contract/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..326c08d --- /dev/null +++ b/TechTest/AnyCompany.Data.Contract/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("AnyCompany.Data.Contract")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("AnyCompany.Data.Contract")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("7f4d6121-26ef-48a2-a2f4-bbd5d114d8e7")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/TechTest/AnyCompany.Data.Contract/Repositories/ICustomerRepository.cs b/TechTest/AnyCompany.Data.Contract/Repositories/ICustomerRepository.cs new file mode 100644 index 0000000..433c46f --- /dev/null +++ b/TechTest/AnyCompany.Data.Contract/Repositories/ICustomerRepository.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using AnyCompany.Models; + +namespace AnyCompany.Data.Contract.Repositories +{ + public interface ICustomerRepository + { + Customer Load(int customerId); + IEnumerable GetList(); + } +} diff --git a/TechTest/AnyCompany.Data.Contract/Repositories/IOrderRepository.cs b/TechTest/AnyCompany.Data.Contract/Repositories/IOrderRepository.cs new file mode 100644 index 0000000..6bcb6a5 --- /dev/null +++ b/TechTest/AnyCompany.Data.Contract/Repositories/IOrderRepository.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using AnyCompany.Models; + +namespace AnyCompany.Data.Contract.Repositories +{ + public interface IOrderRepository + { + void Add(Order order); + IEnumerable GetList(); + } +} diff --git a/TechTest/AnyCompany.Data.Dapper/AnyCompany.Data.Dapper.csproj b/TechTest/AnyCompany.Data.Dapper/AnyCompany.Data.Dapper.csproj new file mode 100644 index 0000000..1a615fe --- /dev/null +++ b/TechTest/AnyCompany.Data.Dapper/AnyCompany.Data.Dapper.csproj @@ -0,0 +1,81 @@ + + + + + Debug + AnyCPU + {CEF90CEA-CEDF-44B4-9203-9B9ADF9A15CE} + Library + Properties + AnyCompany.Data.Dapper + AnyCompany.Data.Dapper + v4.6.1 + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Dapper.1.60.6\lib\net451\Dapper.dll + + + + + + + + + + + + + + + + + + + + + True + True + SqlStatements.resx + + + + + {7F4D6121-26EF-48A2-A2F4-BBD5D114D8E7} + AnyCompany.Data.Contract + + + {695D4D06-2336-4B91-BA68-4A4924CF4729} + AnyCompany.Models + + + + + + + + ResXFileCodeGenerator + SqlStatements.Designer.cs + + + + \ No newline at end of file diff --git a/TechTest/AnyCompany.Data.Dapper/Enums/ConnectionType.cs b/TechTest/AnyCompany.Data.Dapper/Enums/ConnectionType.cs new file mode 100644 index 0000000..339f0fb --- /dev/null +++ b/TechTest/AnyCompany.Data.Dapper/Enums/ConnectionType.cs @@ -0,0 +1,8 @@ +namespace AnyCompany.Data.Dapper.Enums +{ + public enum ConnectionType + { + CustomerDb = 0, + OrderDb = 1 + } +} diff --git a/TechTest/AnyCompany.Data.Dapper/Factories/ConnectionFactory.cs b/TechTest/AnyCompany.Data.Dapper/Factories/ConnectionFactory.cs new file mode 100644 index 0000000..b03d61d --- /dev/null +++ b/TechTest/AnyCompany.Data.Dapper/Factories/ConnectionFactory.cs @@ -0,0 +1,27 @@ +using System.Data; +using System.Data.SqlClient; +using AnyCompany.Data.Dapper.Enums; + +namespace AnyCompany.Data.Dapper.Factories +{ + public class ConnectionFactory : IConnectionFactory + { + private readonly string _customerConnectionString; + private readonly string _orderDbConnectionString; + + public ConnectionFactory(string customerConnectionString, string orderDbConnectionString) + { + _customerConnectionString = customerConnectionString; + _orderDbConnectionString = orderDbConnectionString; + } + + public IDbConnection Create(ConnectionType connectionType) + { + var connectionString = connectionType == ConnectionType.CustomerDb + ? _customerConnectionString + : _orderDbConnectionString; + + return new SqlConnection(connectionString); + } + } +} diff --git a/TechTest/AnyCompany.Data.Dapper/Factories/IConnectionFactory.cs b/TechTest/AnyCompany.Data.Dapper/Factories/IConnectionFactory.cs new file mode 100644 index 0000000..ee5e551 --- /dev/null +++ b/TechTest/AnyCompany.Data.Dapper/Factories/IConnectionFactory.cs @@ -0,0 +1,10 @@ +using System.Data; +using AnyCompany.Data.Dapper.Enums; + +namespace AnyCompany.Data.Dapper.Factories +{ + public interface IConnectionFactory + { + IDbConnection Create(ConnectionType connectionType); + } +} diff --git a/TechTest/AnyCompany.Data.Dapper/Properties/AssemblyInfo.cs b/TechTest/AnyCompany.Data.Dapper/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..032cea6 --- /dev/null +++ b/TechTest/AnyCompany.Data.Dapper/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("AnyCompany.Data.Dapper")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("AnyCompany.Data.Dapper")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("cef90cea-cedf-44b4-9203-9b9adf9a15ce")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/TechTest/AnyCompany.Data.Dapper/Repositories/CustomerRepository.cs b/TechTest/AnyCompany.Data.Dapper/Repositories/CustomerRepository.cs new file mode 100644 index 0000000..3fb998d --- /dev/null +++ b/TechTest/AnyCompany.Data.Dapper/Repositories/CustomerRepository.cs @@ -0,0 +1,23 @@ +using System.Configuration; +using System.Data.SqlClient; +using System.Linq; +using AnyCompany.Data.Dapper.Sql; +using AnyCompany.Models; +using Dapper; + +namespace AnyCompany.Data.Dapper.Repositories +{ + public static class CustomerRepository + { + public static Customer Load(int customerId) + { + var connectionString = ConfigurationManager.ConnectionStrings["CustomerConnectionString"].ConnectionString; + using (var connection = new SqlConnection(connectionString)) + { + connection.Open(); + return connection.Query(SqlStatements.LoadCustomerById, + new {CustomerId = customerId}).FirstOrDefault(); + } + } + } +} diff --git a/TechTest/AnyCompany.Data.Dapper/Repositories/CustomerRepositoryWrapper.cs b/TechTest/AnyCompany.Data.Dapper/Repositories/CustomerRepositoryWrapper.cs new file mode 100644 index 0000000..e42be70 --- /dev/null +++ b/TechTest/AnyCompany.Data.Dapper/Repositories/CustomerRepositoryWrapper.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Linq; +using AnyCompany.Data.Contract.Repositories; +using AnyCompany.Data.Dapper.Enums; +using AnyCompany.Data.Dapper.Factories; +using AnyCompany.Data.Dapper.Sql; +using AnyCompany.Models; +using Dapper; + +namespace AnyCompany.Data.Dapper.Repositories +{ + public class CustomerRepositoryWrapper : ICustomerRepository + { + private readonly IConnectionFactory _connectionFactory; + + public CustomerRepositoryWrapper(IConnectionFactory connectionFactory) + { + _connectionFactory = connectionFactory; + } + + // using this to act as a proxy to the static CustomerRepository - I assumed this was legacy that + // we're trying to abstract and eventually replace + public Customer Load(int customerId) + { + return CustomerRepository.Load(customerId); + } + + public IEnumerable GetList() + { + using (var connection = _connectionFactory.Create(ConnectionType.CustomerDb)) + { + connection.Open(); + return connection.Query(SqlStatements.GetAllCustomers).ToList(); + } + } + } +} diff --git a/TechTest/AnyCompany.Data.Dapper/Repositories/OrderRepository.cs b/TechTest/AnyCompany.Data.Dapper/Repositories/OrderRepository.cs new file mode 100644 index 0000000..3320c23 --- /dev/null +++ b/TechTest/AnyCompany.Data.Dapper/Repositories/OrderRepository.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Data; +using System.Linq; +using AnyCompany.Data.Contract.Repositories; +using AnyCompany.Data.Dapper.Enums; +using AnyCompany.Data.Dapper.Factories; +using AnyCompany.Data.Dapper.Sql; +using AnyCompany.Models; +using Dapper; + +namespace AnyCompany.Data.Dapper.Repositories +{ + public class OrderRepository : IOrderRepository + { + private readonly IConnectionFactory _connectionFactory; + + public OrderRepository(IConnectionFactory connectionFactory) + { + _connectionFactory = connectionFactory; + } + + public void Add(Order order) + { + using (var connection = CreateConnection()) + { + connection.Open(); + connection.Execute(SqlStatements.InsertOrder, + new + { + OrderId = order.OrderId, + Amount = order.Amount, + VAT = order.VAT, + CustomerId = order.CustomerId + }); + } + } + + public IEnumerable GetList() + { + using (var connection = CreateConnection()) + { + connection.Open(); + return connection.Query(SqlStatements.GetAllOrders).ToList(); + } + } + + private IDbConnection CreateConnection() + { + return _connectionFactory.Create(ConnectionType.OrderDb); + } + } +} diff --git a/TechTest/AnyCompany.Data.Dapper/Sql/SqlStatements.Designer.cs b/TechTest/AnyCompany.Data.Dapper/Sql/SqlStatements.Designer.cs new file mode 100644 index 0000000..a085ca9 --- /dev/null +++ b/TechTest/AnyCompany.Data.Dapper/Sql/SqlStatements.Designer.cs @@ -0,0 +1,99 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace AnyCompany.Data.Dapper.Sql { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class SqlStatements { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal SqlStatements() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AnyCompany.Data.Dapper.Sql.SqlStatements", typeof(SqlStatements).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to SELECT CustomerId, Country, DateOfBirth, Name from dbo.Customer. + /// + internal static string GetAllCustomers { + get { + return ResourceManager.GetString("GetAllCustomers", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SELECT OrderId, Amount, VAT, CustomerId from dbo.Orders. + /// + internal static string GetAllOrders { + get { + return ResourceManager.GetString("GetAllOrders", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to INSERT INTO Orders VALUES (@OrderId, @Amount, @VAT, @CustomerId). + /// + internal static string InsertOrder { + get { + return ResourceManager.GetString("InsertOrder", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SELECT * FROM Customer WHERE CustomerId = @CustomerId. + /// + internal static string LoadCustomerById { + get { + return ResourceManager.GetString("LoadCustomerById", resourceCulture); + } + } + } +} diff --git a/TechTest/AnyCompany.Data.Dapper/Sql/SqlStatements.resx b/TechTest/AnyCompany.Data.Dapper/Sql/SqlStatements.resx new file mode 100644 index 0000000..8f6fc44 --- /dev/null +++ b/TechTest/AnyCompany.Data.Dapper/Sql/SqlStatements.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + SELECT CustomerId, Country, DateOfBirth, Name from dbo.Customer + + + SELECT OrderId, Amount, VAT, CustomerId from dbo.Orders + + + INSERT INTO Orders VALUES (@OrderId, @Amount, @VAT, @CustomerId) + + + SELECT * FROM Customer WHERE CustomerId = @CustomerId + + \ No newline at end of file diff --git a/TechTest/AnyCompany.Data.Dapper/packages.config b/TechTest/AnyCompany.Data.Dapper/packages.config new file mode 100644 index 0000000..b6f2e13 --- /dev/null +++ b/TechTest/AnyCompany.Data.Dapper/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/TechTest/AnyCompany.IntegrationTests/AnyCompany.IntegrationTests.csproj b/TechTest/AnyCompany.IntegrationTests/AnyCompany.IntegrationTests.csproj new file mode 100644 index 0000000..5670f60 --- /dev/null +++ b/TechTest/AnyCompany.IntegrationTests/AnyCompany.IntegrationTests.csproj @@ -0,0 +1,109 @@ + + + + + + Debug + AnyCPU + {DF80D016-3AFE-43B3-BC8F-3CEC313B4C91} + Library + Properties + AnyCompany.IntegrationTests + AnyCompany.IntegrationTests + v4.6.1 + 512 + true + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Castle.Core.4.3.1\lib\net45\Castle.Core.dll + + + ..\packages\Castle.Windsor.5.0.0\lib\net45\Castle.Windsor.dll + + + ..\packages\Dapper.1.60.6\lib\net451\Dapper.dll + + + ..\packages\Dapper.Extension.1.0.0.1\lib\net45\Dapper.Extension.dll + + + ..\packages\DapperExtensions.1.6.3\lib\net45\DapperExtensions.dll + + + ..\packages\NUnit.3.12.0\lib\net45\nunit.framework.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + {7F4D6121-26EF-48A2-A2F4-BBD5D114D8E7} + AnyCompany.Data.Contract + + + {CEF90CEA-CEDF-44B4-9203-9B9ADF9A15CE} + AnyCompany.Data.Dapper + + + {BE9D1186-D67F-4BA0-9A94-CE529CC1FEFA} + AnyCompany.Ioc + + + {695D4D06-2336-4B91-BA68-4A4924CF4729} + AnyCompany.Models + + + {c7e15594-7d8f-4c18-9dd7-14f3fbb1572d} + AnyCompany.Services + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/TechTest/AnyCompany.IntegrationTests/App.config b/TechTest/AnyCompany.IntegrationTests/App.config new file mode 100644 index 0000000..a9be8d4 --- /dev/null +++ b/TechTest/AnyCompany.IntegrationTests/App.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/TechTest/AnyCompany.IntegrationTests/CustomerServiceIntegrationTests.cs b/TechTest/AnyCompany.IntegrationTests/CustomerServiceIntegrationTests.cs new file mode 100644 index 0000000..8206a2c --- /dev/null +++ b/TechTest/AnyCompany.IntegrationTests/CustomerServiceIntegrationTests.cs @@ -0,0 +1,71 @@ + +using System; +using System.Linq; +using AnyCompany.IntegrationTests.DataHelpers; +using AnyCompany.Ioc; +using AnyCompany.Models; +using AnyCompany.Services.Services; +using Castle.Windsor; +using NUnit.Framework; + +namespace AnyCompany.IntegrationTests +{ + [TestFixture] + public class CustomerServiceIntegrationTests + { + private IWindsorContainer _container; + + [SetUp] + public void SetupFixture() + { + _container = Bootstrapper.GetContainer(); + } + + [Test] + public void GetAllCustomersWithOrders_ShouldReturnCustomersAndOrders() + { + // Arrange. + var customerOne = CustomerDataHelper.Add(GetCustomer("UK")); + var orderOne = OrderDataHelper.AddOrder(GetOrder(customerOne.CustomerId)); + var orderTwo = OrderDataHelper.AddOrder(GetOrder(customerOne.CustomerId)); + + var customerTwo = CustomerDataHelper.Add(GetCustomer("FR")); + var orderThree = OrderDataHelper.AddOrder(GetOrder(customerTwo.CustomerId)); + + var customerOrderService = _container.Resolve(); + + // Act. + var results = customerOrderService.GetAllCustomerWithOrders(); + var customerOneResult = results.First(c => c.Customer.CustomerId == customerOne.CustomerId); + var customerTwoResult = results.First(c => c.Customer.CustomerId == customerTwo.CustomerId); + + // Assert. + Assert.AreEqual(2, customerOneResult.Orders.Count()); + Assert.AreEqual(1, customerTwoResult.Orders.Count()); + Assert.IsTrue(customerOneResult.Orders.Any(o => o.OrderId == orderOne.OrderId)); + Assert.IsTrue(customerOneResult.Orders.Any(o => o.OrderId == orderTwo.OrderId)); + Assert.IsTrue(customerTwoResult.Orders.Any(o => o.OrderId == orderThree.OrderId)); + } + + private Order GetOrder(int customerId) + { + return new Order + { + OrderId = new Random().Next(1, 200000000), + Amount = 100, + CustomerId = customerId, + VAT = 0 + }; + } + + private Customer GetCustomer(string country) + { + return new Customer + { + Country = country, + DateOfBirth = DateTime.UtcNow.AddDays(-20), + Name = "John Smith" + }; + } + } +} diff --git a/TechTest/AnyCompany.IntegrationTests/DataHelpers/CustomerDataHelper.cs b/TechTest/AnyCompany.IntegrationTests/DataHelpers/CustomerDataHelper.cs new file mode 100644 index 0000000..1be92a3 --- /dev/null +++ b/TechTest/AnyCompany.IntegrationTests/DataHelpers/CustomerDataHelper.cs @@ -0,0 +1,27 @@ +using System.Configuration; +using System.Data.SqlClient; +using AnyCompany.Models; +using DapperExtensions; + +namespace AnyCompany.IntegrationTests.DataHelpers +{ + public static class CustomerDataHelper + { + public static Customer Add(Customer customer) + { + using (var connection = new SqlConnection(GetConnectionString())) + { + connection.Open(); + int id = connection.Insert(customer); + + customer.CustomerId = id; + return customer; + } + } + + private static string GetConnectionString() + { + return ConfigurationManager.ConnectionStrings["CustomerConnectionString"].ConnectionString; + } + } +} \ No newline at end of file diff --git a/TechTest/AnyCompany.IntegrationTests/DataHelpers/OrderDataHelper.cs b/TechTest/AnyCompany.IntegrationTests/DataHelpers/OrderDataHelper.cs new file mode 100644 index 0000000..a1bd397 --- /dev/null +++ b/TechTest/AnyCompany.IntegrationTests/DataHelpers/OrderDataHelper.cs @@ -0,0 +1,46 @@ +using System.Configuration; +using System.Data.Odbc; +using System.Data.SqlClient; +using System.Linq; +using AnyCompany.Models; +using Dapper; + +namespace AnyCompany.IntegrationTests.DataHelpers +{ + public static class OrderDataHelper + { + public static Order GetOrder(int id) + { + using (var connection = new SqlConnection(GetConnectionString())) + { + connection.Open(); + return connection.Query( + "SELECT * FROM dbo.Orders WHERE OrderId = @OrderId", + new { OrderId = id }).First(); + } + } + + public static Order AddOrder(Order order) + { + using (var connection = new SqlConnection(GetConnectionString())) + { + connection.Open(); + connection.Query(@"INSERT INTO Orders VALUES (@OrderId, @Amount, @VAT, @CustomerId)", + new + { + OrderId = order.OrderId, + Amount = order.Amount, + VAT = order.VAT, + CustomerId = order.CustomerId + }); + + return order; + } + } + + private static string GetConnectionString() + { + return ConfigurationManager.ConnectionStrings["OrderConnectionString"].ConnectionString; + } + } +} diff --git a/TechTest/AnyCompany.IntegrationTests/OrderServiceIntegrationTests.cs b/TechTest/AnyCompany.IntegrationTests/OrderServiceIntegrationTests.cs new file mode 100644 index 0000000..307db03 --- /dev/null +++ b/TechTest/AnyCompany.IntegrationTests/OrderServiceIntegrationTests.cs @@ -0,0 +1,65 @@ +using System; +using AnyCompany.IntegrationTests.DataHelpers; +using AnyCompany.Ioc; +using AnyCompany.Models; +using AnyCompany.Services.Constants; +using AnyCompany.Services.Dtos; +using AnyCompany.Services.Services; +using Castle.Windsor; +using NUnit.Framework; + +namespace AnyCompany.IntegrationTests +{ + [TestFixture] + public class OrderServiceIntegrationTests + { + private IWindsorContainer _container; + + [SetUp] + public void SetupFixture() + { + _container = Bootstrapper.GetContainer(); + } + + [TestCase("UK", VatConstants.UkVat)] + [TestCase("FR", VatConstants.RowVat)] + public void PlaceOrder_ShouldInsertAnOrder_AndReturnTrue(string country, double expectedVat) + { + // Arrange. + var customer = CustomerDataHelper.Add(GetCustomer(country)); + var order = GetOrderDto(); + var orderService = _container.Resolve(); + + // Act. + var result = orderService.PlaceOrder(order, customer.CustomerId); + var insertedOrder = OrderDataHelper.GetOrder(order.OrderId); + + // Assert. + Assert.IsTrue(result); + Assert.IsNotNull(insertedOrder); + Assert.AreEqual(customer.CustomerId, insertedOrder.CustomerId); + Assert.AreEqual(order.OrderId, insertedOrder.OrderId); + Assert.AreEqual(order.Amount, insertedOrder.Amount); + Assert.AreEqual(expectedVat, insertedOrder.VAT); + } + + private OrderDto GetOrderDto() + { + return new OrderDto + { + OrderId = new Random().Next(1, 200000000), // generate a random Id since the OrderId column is not an IDENTITY column + Amount = 200.99d + }; + } + + private Customer GetCustomer(string country) + { + return new Customer + { + Country = country, + DateOfBirth = DateTime.UtcNow.AddYears(-41), + Name = "John Smith" + }; + } + } +} diff --git a/TechTest/AnyCompany.IntegrationTests/Properties/AssemblyInfo.cs b/TechTest/AnyCompany.IntegrationTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..98f6cd7 --- /dev/null +++ b/TechTest/AnyCompany.IntegrationTests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("AnyCompany.IntegrationTests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("AnyCompany.IntegrationTests")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("df80d016-3afe-43b3-bc8f-3cec313b4c91")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/TechTest/AnyCompany.IntegrationTests/packages.config b/TechTest/AnyCompany.IntegrationTests/packages.config new file mode 100644 index 0000000..9a9ebea --- /dev/null +++ b/TechTest/AnyCompany.IntegrationTests/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/TechTest/AnyCompany.Ioc/AnyCompany.Ioc.csproj b/TechTest/AnyCompany.Ioc/AnyCompany.Ioc.csproj new file mode 100644 index 0000000..3808873 --- /dev/null +++ b/TechTest/AnyCompany.Ioc/AnyCompany.Ioc.csproj @@ -0,0 +1,75 @@ + + + + + Debug + AnyCPU + {BE9D1186-D67F-4BA0-9A94-CE529CC1FEFA} + Library + Properties + AnyCompany.Ioc + AnyCompany.Ioc + v4.6.1 + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Castle.Core.4.3.1\lib\net45\Castle.Core.dll + + + ..\packages\Castle.Windsor.5.0.0\lib\net45\Castle.Windsor.dll + + + + + + + + + + + + + + + + + + + + + + + + {7F4D6121-26EF-48A2-A2F4-BBD5D114D8E7} + AnyCompany.Data.Contract + + + {CEF90CEA-CEDF-44B4-9203-9B9ADF9A15CE} + AnyCompany.Data.Dapper + + + {C7E15594-7D8F-4C18-9DD7-14F3FBB1572D} + AnyCompany.Services + + + + \ No newline at end of file diff --git a/TechTest/AnyCompany.Ioc/Bootstrapper.cs b/TechTest/AnyCompany.Ioc/Bootstrapper.cs new file mode 100644 index 0000000..4e1f915 --- /dev/null +++ b/TechTest/AnyCompany.Ioc/Bootstrapper.cs @@ -0,0 +1,29 @@ +using Castle.Windsor; + +namespace AnyCompany.Ioc +{ + public static class Bootstrapper + { + private static object _objLock = new object(); + + private static WindsorContainer _container; + + public static WindsorContainer GetContainer() + { + if (_container != null) + return _container; + + lock (_objLock) + { + if (_container != null) + return _container; + + var container = new WindsorContainer(); + container.Install(new DependencyInstaller()); + + _container = container; + return _container; + } + } + } +} diff --git a/TechTest/AnyCompany.Ioc/Container.cs b/TechTest/AnyCompany.Ioc/Container.cs new file mode 100644 index 0000000..251ebc7 --- /dev/null +++ b/TechTest/AnyCompany.Ioc/Container.cs @@ -0,0 +1,29 @@ +using Castle.Windsor; + +namespace AnyCompany.Ioc +{ + public static class Container + { + private static object _objLock = new object(); + + private static WindsorContainer _container; + + public static WindsorContainer GetContainer() + { + if (_container != null) + return _container; + + lock (_objLock) + { + if (_container != null) + return _container; + + var container = new WindsorContainer(); + container.Install(new DependencyInstaller()); + + _container = container; + return _container; + } + } + } +} diff --git a/TechTest/AnyCompany.Ioc/DependencyInstaller.cs b/TechTest/AnyCompany.Ioc/DependencyInstaller.cs new file mode 100644 index 0000000..f474f33 --- /dev/null +++ b/TechTest/AnyCompany.Ioc/DependencyInstaller.cs @@ -0,0 +1,40 @@ +using System.Configuration; +using AnyCompany.Data.Contract.Repositories; +using AnyCompany.Data.Dapper.Factories; +using AnyCompany.Data.Dapper.Repositories; +using AnyCompany.Services.Services; +using Castle.MicroKernel; +using Castle.MicroKernel.Registration; +using Castle.MicroKernel.SubSystems.Configuration; +using Castle.Windsor; + +namespace AnyCompany.Ioc +{ + public class DependencyInstaller : IWindsorInstaller + { + public void Install(IWindsorContainer container, IConfigurationStore store) + { + container.Register( + Component.For()); + + container.Register( + Component.For()); + + container.Register( + Component.For()); + + container.Register( + Component.For()); + + container.Register( + Component.For().UsingFactoryMethod(CreateConnectionFactory)); + } + + private IConnectionFactory CreateConnectionFactory(IKernel kernel) + { + return new ConnectionFactory( + ConfigurationManager.ConnectionStrings["CustomerConnectionString"].ConnectionString, + ConfigurationManager.ConnectionStrings["OrderConnectionString"].ConnectionString); + } + } +} diff --git a/TechTest/AnyCompany.Ioc/Properties/AssemblyInfo.cs b/TechTest/AnyCompany.Ioc/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..133d763 --- /dev/null +++ b/TechTest/AnyCompany.Ioc/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("AnyCompany.Ioc")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("AnyCompany.Ioc")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("be9d1186-d67f-4ba0-9a94-ce529cc1fefa")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/TechTest/AnyCompany.Ioc/packages.config b/TechTest/AnyCompany.Ioc/packages.config new file mode 100644 index 0000000..927252c --- /dev/null +++ b/TechTest/AnyCompany.Ioc/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/TechTest/AnyCompany/AnyCompany.csproj b/TechTest/AnyCompany.Models/AnyCompany.Models.csproj similarity index 87% rename from TechTest/AnyCompany/AnyCompany.csproj rename to TechTest/AnyCompany.Models/AnyCompany.Models.csproj index 5b0498d..49e3a75 100644 --- a/TechTest/AnyCompany/AnyCompany.csproj +++ b/TechTest/AnyCompany.Models/AnyCompany.Models.csproj @@ -4,13 +4,14 @@ Debug AnyCPU - {C7E15594-7D8F-4C18-9DD7-14F3FBB1572D} + {695D4D06-2336-4B91-BA68-4A4924CF4729} Library Properties - AnyCompany - AnyCompany + AnyCompany.Models + AnyCompany.Models v4.6.1 512 + true true @@ -41,10 +42,7 @@ - - - diff --git a/TechTest/AnyCompany/Customer.cs b/TechTest/AnyCompany.Models/Customer.cs similarity index 72% rename from TechTest/AnyCompany/Customer.cs rename to TechTest/AnyCompany.Models/Customer.cs index aa994b6..e0fbd3f 100644 --- a/TechTest/AnyCompany/Customer.cs +++ b/TechTest/AnyCompany.Models/Customer.cs @@ -1,9 +1,11 @@ using System; -namespace AnyCompany +namespace AnyCompany.Models { public class Customer { + public int CustomerId { get; set; } + public string Country { get; set; } public DateTime DateOfBirth { get; set; } diff --git a/TechTest/AnyCompany/Order.cs b/TechTest/AnyCompany.Models/Order.cs similarity index 68% rename from TechTest/AnyCompany/Order.cs rename to TechTest/AnyCompany.Models/Order.cs index fec8e7b..14420a2 100644 --- a/TechTest/AnyCompany/Order.cs +++ b/TechTest/AnyCompany.Models/Order.cs @@ -1,9 +1,10 @@ -namespace AnyCompany +namespace AnyCompany.Models { public class Order { public int OrderId { get; set; } public double Amount { get; set; } public double VAT { get; set; } + public int CustomerId { get; set; } } } diff --git a/TechTest/AnyCompany.Models/Properties/AssemblyInfo.cs b/TechTest/AnyCompany.Models/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..71be66d --- /dev/null +++ b/TechTest/AnyCompany.Models/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("AnyCompany.Models")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("AnyCompany.Models")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("695d4d06-2336-4b91-ba68-4a4924cf4729")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/TechTest/AnyCompany.Services/AnyCompany.Services.csproj b/TechTest/AnyCompany.Services/AnyCompany.Services.csproj new file mode 100644 index 0000000..8186b56 --- /dev/null +++ b/TechTest/AnyCompany.Services/AnyCompany.Services.csproj @@ -0,0 +1,68 @@ + + + + + Debug + AnyCPU + {C7E15594-7D8F-4C18-9DD7-14F3FBB1572D} + Library + Properties + AnyCompany.Services + AnyCompany.Services + v4.6.1 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {7F4D6121-26EF-48A2-A2F4-BBD5D114D8E7} + AnyCompany.Data.Contract + + + {695D4D06-2336-4B91-BA68-4A4924CF4729} + AnyCompany.Models + + + + \ No newline at end of file diff --git a/TechTest/AnyCompany.Services/Constants/VatConstants.cs b/TechTest/AnyCompany.Services/Constants/VatConstants.cs new file mode 100644 index 0000000..ecca404 --- /dev/null +++ b/TechTest/AnyCompany.Services/Constants/VatConstants.cs @@ -0,0 +1,8 @@ +namespace AnyCompany.Services.Constants +{ + public static class VatConstants + { + public const double UkVat = 0.2d; + public const double RowVat = 0; + } +} diff --git a/TechTest/AnyCompany.Services/Dtos/CustomerDto.cs b/TechTest/AnyCompany.Services/Dtos/CustomerDto.cs new file mode 100644 index 0000000..b75328b --- /dev/null +++ b/TechTest/AnyCompany.Services/Dtos/CustomerDto.cs @@ -0,0 +1,15 @@ +using System; + +namespace AnyCompany.Services.Dtos +{ + public class CustomerDto + { + public int CustomerId { get; set; } + + public string Country { get; set; } + + public DateTime DateOfBirth { get; set; } + + public string Name { get; set; } + } +} diff --git a/TechTest/AnyCompany.Services/Dtos/CustomerOrdersDto.cs b/TechTest/AnyCompany.Services/Dtos/CustomerOrdersDto.cs new file mode 100644 index 0000000..5ff7144 --- /dev/null +++ b/TechTest/AnyCompany.Services/Dtos/CustomerOrdersDto.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using AnyCompany.Models; + +namespace AnyCompany.Services.Dtos +{ + public class CustomerOrdersDto + { + public CustomerDto Customer { get; set; } + + public IEnumerable Orders { get; set; } + } +} diff --git a/TechTest/AnyCompany.Services/Dtos/OrderDto.cs b/TechTest/AnyCompany.Services/Dtos/OrderDto.cs new file mode 100644 index 0000000..01d89c1 --- /dev/null +++ b/TechTest/AnyCompany.Services/Dtos/OrderDto.cs @@ -0,0 +1,9 @@ +namespace AnyCompany.Services.Dtos +{ + public class OrderDto + { + public int OrderId { get; set; } + public double Amount { get; set; } + public double VAT { get; set; } + } +} \ No newline at end of file diff --git a/TechTest/AnyCompany.Services/Exceptions/CustomerNotFoundException.cs b/TechTest/AnyCompany.Services/Exceptions/CustomerNotFoundException.cs new file mode 100644 index 0000000..6125841 --- /dev/null +++ b/TechTest/AnyCompany.Services/Exceptions/CustomerNotFoundException.cs @@ -0,0 +1,15 @@ +using System; + +namespace AnyCompany.Services.Exceptions +{ + public class CustomerNotFoundException : Exception + { + public CustomerNotFoundException() + { + } + + public CustomerNotFoundException(string message) : base(message) + { + } + } +} diff --git a/TechTest/AnyCompany.Services/Helpers/VatCalculator.cs b/TechTest/AnyCompany.Services/Helpers/VatCalculator.cs new file mode 100644 index 0000000..ad73735 --- /dev/null +++ b/TechTest/AnyCompany.Services/Helpers/VatCalculator.cs @@ -0,0 +1,13 @@ +using AnyCompany.Services.Constants; + +namespace AnyCompany.Services.Helpers +{ + internal static class VatCalculator + { + // small bit of logic so kept in a static helper method - could move behind an interface in the future + internal static double GetVat(string country) + { + return country == "UK" ? VatConstants.UkVat : VatConstants.RowVat; + } + } +} diff --git a/TechTest/AnyCompany.Services/Mappers/CustomerMapper.cs b/TechTest/AnyCompany.Services/Mappers/CustomerMapper.cs new file mode 100644 index 0000000..969c669 --- /dev/null +++ b/TechTest/AnyCompany.Services/Mappers/CustomerMapper.cs @@ -0,0 +1,19 @@ +using AnyCompany.Models; +using AnyCompany.Services.Dtos; + +namespace AnyCompany.Services.Mappers +{ + public static class CustomerMapper + { + public static CustomerDto Map(Customer customer) + { + return new CustomerDto + { + CustomerId = customer.CustomerId, + Country = customer.Country, + DateOfBirth = customer.DateOfBirth, + Name = customer.Name + }; + } + } +} \ No newline at end of file diff --git a/TechTest/AnyCompany.Services/Mappers/OrderMapper.cs b/TechTest/AnyCompany.Services/Mappers/OrderMapper.cs new file mode 100644 index 0000000..5812f1f --- /dev/null +++ b/TechTest/AnyCompany.Services/Mappers/OrderMapper.cs @@ -0,0 +1,28 @@ +using AnyCompany.Models; +using AnyCompany.Services.Dtos; + +namespace AnyCompany.Services.Mappers +{ + public static class OrderMapper + { + public static Order Map(OrderDto orderDto) + { + return new Order + { + OrderId = orderDto.OrderId, + Amount = orderDto.Amount, + VAT = orderDto.VAT + }; + } + + public static OrderDto Map(Order order) + { + return new OrderDto + { + OrderId = order.OrderId, + Amount = order.Amount, + VAT = order.VAT + }; + } + } +} diff --git a/TechTest/AnyCompany/Properties/AssemblyInfo.cs b/TechTest/AnyCompany.Services/Properties/AssemblyInfo.cs similarity index 100% rename from TechTest/AnyCompany/Properties/AssemblyInfo.cs rename to TechTest/AnyCompany.Services/Properties/AssemblyInfo.cs diff --git a/TechTest/AnyCompany.Services/Services/CustomerOrderService.cs b/TechTest/AnyCompany.Services/Services/CustomerOrderService.cs new file mode 100644 index 0000000..1e1e31d --- /dev/null +++ b/TechTest/AnyCompany.Services/Services/CustomerOrderService.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Linq; +using AnyCompany.Data.Contract.Repositories; +using AnyCompany.Services.Dtos; +using AnyCompany.Services.Mappers; + +namespace AnyCompany.Services.Services +{ + public class CustomerOrderService : ICustomerOrderService + { + private readonly ICustomerRepository _customerRepository; + private readonly IOrderRepository _orderRepository; + + public CustomerOrderService(ICustomerRepository customerRepository, IOrderRepository orderRepository) + { + _customerRepository = customerRepository; + _orderRepository = orderRepository; + } + + public IEnumerable GetAllCustomerWithOrders() + { + var customerOrderDtos = new List(); + + // load data + var customers = _customerRepository.GetList(); + var orders = _orderRepository.GetList(); + + // map orders to the appropriate customer and return a dto + foreach (var customer in customers) + { + var customerOrderDto = new CustomerOrdersDto + { + Customer = CustomerMapper.Map(customer), + Orders = orders.Where(o => o.CustomerId == customer.CustomerId).Select(OrderMapper.Map) + }; + + customerOrderDtos.Add(customerOrderDto); + } + + return customerOrderDtos; + } + } +} diff --git a/TechTest/AnyCompany.Services/Services/ICustomerOrderService.cs b/TechTest/AnyCompany.Services/Services/ICustomerOrderService.cs new file mode 100644 index 0000000..8fa937d --- /dev/null +++ b/TechTest/AnyCompany.Services/Services/ICustomerOrderService.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using AnyCompany.Services.Dtos; + +namespace AnyCompany.Services.Services +{ + public interface ICustomerOrderService + { + IEnumerable GetAllCustomerWithOrders(); + } +} diff --git a/TechTest/AnyCompany.Services/Services/IOrderService.cs b/TechTest/AnyCompany.Services/Services/IOrderService.cs new file mode 100644 index 0000000..227c155 --- /dev/null +++ b/TechTest/AnyCompany.Services/Services/IOrderService.cs @@ -0,0 +1,9 @@ +using AnyCompany.Services.Dtos; + +namespace AnyCompany.Services.Services +{ + public interface IOrderService + { + bool PlaceOrder(OrderDto order, int customerId); + } +} diff --git a/TechTest/AnyCompany.Services/Services/OrderService.cs b/TechTest/AnyCompany.Services/Services/OrderService.cs new file mode 100644 index 0000000..aef7449 --- /dev/null +++ b/TechTest/AnyCompany.Services/Services/OrderService.cs @@ -0,0 +1,44 @@ +using AnyCompany.Data.Contract.Repositories; +using AnyCompany.Models; +using AnyCompany.Services.Dtos; +using AnyCompany.Services.Exceptions; +using AnyCompany.Services.Helpers; +using AnyCompany.Services.Mappers; + +namespace AnyCompany.Services.Services +{ + public class OrderService : IOrderService + { + private readonly IOrderRepository _orderRepository; + private readonly ICustomerRepository _customerRepository; + + public OrderService(IOrderRepository orderRepository, ICustomerRepository customerRepository) + { + _orderRepository = orderRepository; + _customerRepository = customerRepository; + } + + public bool PlaceOrder(OrderDto orderDto, int customerId) + { + // load the customer + Customer customer = _customerRepository.Load(customerId); + if (customer == null) + throw new CustomerNotFoundException(); + + // do not proceed with order if amount is 0 + if (orderDto.Amount == 0) + return false; + + // get the appropriate vat for the customers country + orderDto.VAT = VatCalculator.GetVat(customer.Country); + + // build and save the order + var order = OrderMapper.Map(orderDto); + order.CustomerId = customerId; + + _orderRepository.Add(order); + + return true; + } + } +} diff --git a/TechTest/AnyCompany.Tests/AnyCompany.Tests.csproj b/TechTest/AnyCompany.Tests/AnyCompany.Tests.csproj index b537fc2..e9b4334 100644 --- a/TechTest/AnyCompany.Tests/AnyCompany.Tests.csproj +++ b/TechTest/AnyCompany.Tests/AnyCompany.Tests.csproj @@ -1,16 +1,19 @@ - + + Debug AnyCPU - cd5d577e-bdc9-4dfc-ac6a-b1da474995f3 + {CD5D577E-BDC9-4DFC-AC6A-B1DA474995F3} Library Properties AnyCompany.Tests AnyCompany.Tests v4.6.1 512 + + true @@ -30,24 +33,58 @@ 4 - - - - - - - - - - - - - - + + ..\packages\Castle.Core.4.4.0\lib\net45\Castle.Core.dll + + + ..\packages\Moq.4.12.0\lib\net45\Moq.dll + + + ..\packages\NUnit.3.12.0\lib\net45\nunit.framework.dll + + + + + + ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.0\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll + + + ..\packages\System.Threading.Tasks.Extensions.4.5.1\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll + + + + + + + - + + + + + + + + {7F4D6121-26EF-48A2-A2F4-BBD5D114D8E7} + AnyCompany.Data.Contract + + + {695D4D06-2336-4B91-BA68-4A4924CF4729} + AnyCompany.Models + + + {C7E15594-7D8F-4C18-9DD7-14F3FBB1572D} + AnyCompany.Services + + - + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/TechTest/AnyCompany.Tests/Class1.cs b/TechTest/AnyCompany.Tests/Class1.cs deleted file mode 100644 index 5957505..0000000 --- a/TechTest/AnyCompany.Tests/Class1.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace AnyCompany.Tests -{ - public class Class1 - { - } -} diff --git a/TechTest/AnyCompany.Tests/CustomerOrderServiceTests.cs b/TechTest/AnyCompany.Tests/CustomerOrderServiceTests.cs new file mode 100644 index 0000000..661eccd --- /dev/null +++ b/TechTest/AnyCompany.Tests/CustomerOrderServiceTests.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; +using System.Linq; +using AnyCompany.Data.Contract.Repositories; +using AnyCompany.Models; +using AnyCompany.Services.Services; +using Moq; +using NUnit.Framework; + +namespace AnyCompany.Tests +{ + [TestFixture] + public class CustomerOrderServiceTests + { + private Mock _customerRepositoryMock; + private Mock _orderRepositoryMock; + + [SetUp] + public void Setup() + { + _orderRepositoryMock = new Mock(); + _customerRepositoryMock = new Mock(); + } + + [Test] + public void GetCustomersWithOrders_ShouldReturnAllOrders() + { + // Arrange. + const int CustomerOneId = 3; + const int CustomerTwoId = 4; + + var customers = GetCustomers(CustomerOneId, CustomerTwoId); + _customerRepositoryMock.Setup(r => r.GetList()).Returns(customers); + + var orders = GetOrders(CustomerOneId, CustomerTwoId); + _orderRepositoryMock.Setup(r => r.GetList()).Returns(orders); + + var customerOrderService = GetService(); + + // Act. + var result = customerOrderService.GetAllCustomerWithOrders(); + + // Assert. + Assert.AreEqual(customers.Count(), result.Count()); + Assert.AreEqual(customers.First().CustomerId, result.First().Customer.CustomerId); + Assert.AreEqual(customers.First().Country, result.First().Customer.Country); + Assert.AreEqual(customers.First().DateOfBirth, result.First().Customer.DateOfBirth); + Assert.AreEqual(customers.First().Name, result.First().Customer.Name); + Assert.AreEqual(orders.Count(o => o.CustomerId == customers.First().CustomerId), result.First().Orders.Count()); + Assert.AreEqual(orders.First(o => o.CustomerId == customers.First().CustomerId).Amount, result.First().Orders.First().Amount); + Assert.AreEqual(orders.First(o => o.CustomerId == customers.First().CustomerId).VAT, result.First().Orders.First().VAT); + Assert.AreEqual(orders.First(o => o.CustomerId == customers.First().CustomerId).OrderId, result.First().Orders.First().OrderId); + } + + private CustomerOrderService GetService() + { + return new CustomerOrderService( + _customerRepositoryMock.Object, + _orderRepositoryMock.Object); + } + + private IEnumerable GetCustomers(int idOne, int idTwo) + { + return new List + { + GetCustomer(idOne), + GetCustomer(idTwo) + }; + } + + private IEnumerable GetOrders(int idOne, int idTwo) + { + return new List + { + GetOrder(33, idOne), + GetOrder(43, idTwo), + GetOrder(43, idOne), + }; + } + + private Customer GetCustomer(int id) + { + return new Customer + { + CustomerId = id, + Name = "John Smith", + Country = "UK" + }; + } + + private Order GetOrder(int id, int customerId) + { + return new Order + { + OrderId = id, + Amount = 20.99, + CustomerId = customerId, + VAT = 0.2d + }; + } + } +} diff --git a/TechTest/AnyCompany.Tests/OrderServiceTests.cs b/TechTest/AnyCompany.Tests/OrderServiceTests.cs new file mode 100644 index 0000000..6fc61e5 --- /dev/null +++ b/TechTest/AnyCompany.Tests/OrderServiceTests.cs @@ -0,0 +1,116 @@ +using System; +using AnyCompany.Data.Contract.Repositories; +using AnyCompany.Models; +using AnyCompany.Services; +using AnyCompany.Services.Dtos; +using AnyCompany.Services.Exceptions; +using AnyCompany.Services.Services; +using Moq; +using NUnit.Framework; + +namespace AnyCompany.Tests +{ + [TestFixture] + public class OrderServiceTests + { + private Mock _orderRepositoryMock; + private Mock _customerRepositoryMock; + + [SetUp] + public void Setup() + { + _orderRepositoryMock = new Mock(); + _customerRepositoryMock = new Mock(); + } + + [TestCase("UK", 0.2)] + [TestCase("FR", 0)] + public void PlaceOrder_ShouldLoadACustomerAndPlaceAnOrder_UK(string country, double vat) + { + // Arrange. + const int CustomerId = 23; + var orderDto = GetOrderDto(); + + var customer = GetCustomer(country); + _customerRepositoryMock.Setup(r => r.Load(CustomerId)) + .Returns(customer); + + Order inOrder = null; + _orderRepositoryMock.Setup(o => o.Add(It.IsAny())) + .Callback(o => inOrder = o); + + var orderService = GetOrderService(); + + // Act. + var result = orderService.PlaceOrder(orderDto, CustomerId); + + // Assert. + Assert.IsTrue(result); + Assert.AreEqual(orderDto.Amount, inOrder.Amount); + Assert.AreEqual(vat, inOrder.VAT); + Assert.AreEqual(CustomerId, inOrder.CustomerId); + } + + [Test] + public void PlaceOrder_CustomerNotFound_ShouldThrowException() + { + // Arrange. + const int CustomerId = 44; + + _customerRepositoryMock.Setup(r => r.Load(CustomerId)) + .Returns((Customer)null); + + var orderService = GetOrderService(); + + // Act/Assert. + Assert.Throws(() => orderService.PlaceOrder(GetOrderDto(), CustomerId)); + } + + [Test] + public void PlaceOrder_AmountIs0_ShouldReturnFalse() + { + // Arrange. + const int CustomerId = 23; + var orderDto = GetOrderDto(); + + var customer = GetCustomer("UK"); + _customerRepositoryMock.Setup(r => r.Load(CustomerId)) + .Returns(customer); + + orderDto.Amount = 0; + + var orderService = GetOrderService(); + + // Act. + var result = orderService.PlaceOrder(orderDto, CustomerId); + + // Assert. + Assert.IsFalse(result); + } + + private Customer GetCustomer(string country) + { + return new Customer + { + Name = "John Smith", + DateOfBirth = DateTime.UtcNow.AddYears(-31), + Country = country + }; + } + + private OrderService GetOrderService() + { + return new OrderService( + _orderRepositoryMock.Object, + _customerRepositoryMock.Object); + } + + private OrderDto GetOrderDto() + { + return new OrderDto + { + Amount = 20, + }; + } + } +} diff --git a/TechTest/AnyCompany.Tests/packages.config b/TechTest/AnyCompany.Tests/packages.config new file mode 100644 index 0000000..bf0c6ae --- /dev/null +++ b/TechTest/AnyCompany.Tests/packages.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/TechTest/AnyCompany/CustomerRepository.cs b/TechTest/AnyCompany/CustomerRepository.cs deleted file mode 100644 index e3de9b7..0000000 --- a/TechTest/AnyCompany/CustomerRepository.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Data.SqlClient; - -namespace AnyCompany -{ - public static class CustomerRepository - { - private static string ConnectionString = @"Data Source=(local);Database=Customers;User Id=admin;Password=password;"; - - public static Customer Load(int customerId) - { - Customer customer = new Customer(); - - SqlConnection connection = new SqlConnection(ConnectionString); - connection.Open(); - - SqlCommand command = new SqlCommand("SELECT * FROM Customer WHERE CustomerId = " + customerId, - connection); - var reader = command.ExecuteReader(); - - while (reader.Read()) - { - customer.Name = reader["Name"].ToString(); - customer.DateOfBirth = DateTime.Parse(reader["DateOfBirth"].ToString()); - customer.Country = reader["Country"].ToString(); - } - - connection.Close(); - - return customer; - } - } -} diff --git a/TechTest/AnyCompany/OrderRepository.cs b/TechTest/AnyCompany/OrderRepository.cs deleted file mode 100644 index 3229885..0000000 --- a/TechTest/AnyCompany/OrderRepository.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Data.SqlClient; - -namespace AnyCompany -{ - internal class OrderRepository - { - private static string ConnectionString = @"Data Source=(local);Database=Orders;User Id=admin;Password=password;"; - - public void Save(Order order) - { - SqlConnection connection = new SqlConnection(ConnectionString); - connection.Open(); - - SqlCommand command = new SqlCommand("INSERT INTO Orders VALUES (@OrderId, @Amount, @VAT)", connection); - - command.Parameters.AddWithValue("@OrderId", order.OrderId); - command.Parameters.AddWithValue("@Amount", order.Amount); - command.Parameters.AddWithValue("@VAT", order.VAT); - - command.ExecuteNonQuery(); - - connection.Close(); - } - } -} diff --git a/TechTest/AnyCompany/OrderService.cs b/TechTest/AnyCompany/OrderService.cs deleted file mode 100644 index ebfb103..0000000 --- a/TechTest/AnyCompany/OrderService.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace AnyCompany -{ - public class OrderService - { - private readonly OrderRepository orderRepository = new OrderRepository(); - - public bool PlaceOrder(Order order, int customerId) - { - Customer customer = CustomerRepository.Load(customerId); - - if (order.Amount == 0) - return false; - - if (customer.Country == "UK") - order.VAT = 0.2d; - else - order.VAT = 0; - - orderRepository.Save(order); - - return true; - } - } -} diff --git a/TechTest/TechTest.sln b/TechTest/TechTest.sln index 1c1c57a..ba64203 100644 --- a/TechTest/TechTest.sln +++ b/TechTest/TechTest.sln @@ -1,10 +1,8 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27004.2005 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28917.181 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnyCompany", "AnyCompany\AnyCompany.csproj", "{C7E15594-7D8F-4C18-9DD7-14F3FBB1572D}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnyCompany.Tests", "AnyCompany.Tests\AnyCompany.Tests.csproj", "{CD5D577E-BDC9-4DFC-AC6A-B1DA474995F3}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B6D3C1BB-2A37-4E17-9EE3-DEF28286E782}" @@ -12,24 +10,79 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Instructions.txt = Instructions.txt EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Database Scaffolding Scripts", "Database Scaffolding Scripts", "{0372EDAB-809E-4632-A929-146E339E9EB0}" + ProjectSection(SolutionItems) = preProject + AddCustomers.sql = AddCustomers.sql + AddOrders.sql = AddOrders.sql + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "1. Business Layer", "1. Business Layer", "{25F15798-EF39-43DD-B9A8-11DA2D8EB5EB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "4. Tests", "4. Tests", "{118DE10D-B767-428F-B9D3-3987DF71146A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnyCompany.IntegrationTests", "AnyCompany.IntegrationTests\AnyCompany.IntegrationTests.csproj", "{DF80D016-3AFE-43B3-BC8F-3CEC313B4C91}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "2. Data Access Layer", "2. Data Access Layer", "{A38327A3-2391-4469-AB71-3AB3714531BB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "3. Framework", "3. Framework", "{DB29E18C-EE03-4193-857E-FD98D35D783A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnyCompany.Models", "AnyCompany.Models\AnyCompany.Models.csproj", "{695D4D06-2336-4B91-BA68-4A4924CF4729}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnyCompany.Services", "AnyCompany.Services\AnyCompany.Services.csproj", "{C7E15594-7D8F-4C18-9DD7-14F3FBB1572D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnyCompany.Data.Contract", "AnyCompany.Data.Contract\AnyCompany.Data.Contract.csproj", "{7F4D6121-26EF-48A2-A2F4-BBD5D114D8E7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnyCompany.Data.Dapper", "AnyCompany.Data.Dapper\AnyCompany.Data.Dapper.csproj", "{CEF90CEA-CEDF-44B4-9203-9B9ADF9A15CE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnyCompany.Ioc", "AnyCompany.Ioc\AnyCompany.Ioc.csproj", "{BE9D1186-D67F-4BA0-9A94-CE529CC1FEFA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {C7E15594-7D8F-4C18-9DD7-14F3FBB1572D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C7E15594-7D8F-4C18-9DD7-14F3FBB1572D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C7E15594-7D8F-4C18-9DD7-14F3FBB1572D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C7E15594-7D8F-4C18-9DD7-14F3FBB1572D}.Release|Any CPU.Build.0 = Release|Any CPU {CD5D577E-BDC9-4DFC-AC6A-B1DA474995F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CD5D577E-BDC9-4DFC-AC6A-B1DA474995F3}.Debug|Any CPU.Build.0 = Debug|Any CPU {CD5D577E-BDC9-4DFC-AC6A-B1DA474995F3}.Release|Any CPU.ActiveCfg = Release|Any CPU {CD5D577E-BDC9-4DFC-AC6A-B1DA474995F3}.Release|Any CPU.Build.0 = Release|Any CPU + {DF80D016-3AFE-43B3-BC8F-3CEC313B4C91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DF80D016-3AFE-43B3-BC8F-3CEC313B4C91}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF80D016-3AFE-43B3-BC8F-3CEC313B4C91}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DF80D016-3AFE-43B3-BC8F-3CEC313B4C91}.Release|Any CPU.Build.0 = Release|Any CPU + {695D4D06-2336-4B91-BA68-4A4924CF4729}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {695D4D06-2336-4B91-BA68-4A4924CF4729}.Debug|Any CPU.Build.0 = Debug|Any CPU + {695D4D06-2336-4B91-BA68-4A4924CF4729}.Release|Any CPU.ActiveCfg = Release|Any CPU + {695D4D06-2336-4B91-BA68-4A4924CF4729}.Release|Any CPU.Build.0 = Release|Any CPU + {C7E15594-7D8F-4C18-9DD7-14F3FBB1572D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C7E15594-7D8F-4C18-9DD7-14F3FBB1572D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C7E15594-7D8F-4C18-9DD7-14F3FBB1572D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C7E15594-7D8F-4C18-9DD7-14F3FBB1572D}.Release|Any CPU.Build.0 = Release|Any CPU + {7F4D6121-26EF-48A2-A2F4-BBD5D114D8E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F4D6121-26EF-48A2-A2F4-BBD5D114D8E7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F4D6121-26EF-48A2-A2F4-BBD5D114D8E7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F4D6121-26EF-48A2-A2F4-BBD5D114D8E7}.Release|Any CPU.Build.0 = Release|Any CPU + {CEF90CEA-CEDF-44B4-9203-9B9ADF9A15CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CEF90CEA-CEDF-44B4-9203-9B9ADF9A15CE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CEF90CEA-CEDF-44B4-9203-9B9ADF9A15CE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CEF90CEA-CEDF-44B4-9203-9B9ADF9A15CE}.Release|Any CPU.Build.0 = Release|Any CPU + {BE9D1186-D67F-4BA0-9A94-CE529CC1FEFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BE9D1186-D67F-4BA0-9A94-CE529CC1FEFA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BE9D1186-D67F-4BA0-9A94-CE529CC1FEFA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BE9D1186-D67F-4BA0-9A94-CE529CC1FEFA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {CD5D577E-BDC9-4DFC-AC6A-B1DA474995F3} = {118DE10D-B767-428F-B9D3-3987DF71146A} + {DF80D016-3AFE-43B3-BC8F-3CEC313B4C91} = {118DE10D-B767-428F-B9D3-3987DF71146A} + {695D4D06-2336-4B91-BA68-4A4924CF4729} = {DB29E18C-EE03-4193-857E-FD98D35D783A} + {C7E15594-7D8F-4C18-9DD7-14F3FBB1572D} = {25F15798-EF39-43DD-B9A8-11DA2D8EB5EB} + {7F4D6121-26EF-48A2-A2F4-BBD5D114D8E7} = {A38327A3-2391-4469-AB71-3AB3714531BB} + {CEF90CEA-CEDF-44B4-9203-9B9ADF9A15CE} = {A38327A3-2391-4469-AB71-3AB3714531BB} + {BE9D1186-D67F-4BA0-9A94-CE529CC1FEFA} = {DB29E18C-EE03-4193-857E-FD98D35D783A} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4D629674-23F9-49DA-971E-77A5D7A7C39D} EndGlobalSection