Exercise Files

This commit is contained in:
Jess Chadwick
2018-06-06 23:57:58 -04:00
commit 20458e435e
4436 changed files with 1359080 additions and 0 deletions

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
</configSections>
<entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
<parameters>
<parameter value="mssqllocaldb" />
</parameters>
</defaultConnectionFactory>
<providers>
<provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
</providers>
</entityFramework>
</configuration>

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{7709A0D8-F311-4A8E-9E5B-05ED694449AD}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>HPlusSports</RootNamespace>
<AssemblyName>HPlusSports.Common</AssemblyName>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL">
<HintPath>..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.dll</HintPath>
</Reference>
<Reference Include="EntityFramework.SqlServer, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL">
<HintPath>..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll</HintPath>
</Reference>
<Reference Include="MediatR, Version=4.1.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\MediatR.4.1.0\lib\net45\MediatR.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.ComponentModel.DataAnnotations" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Models\Category.cs" />
<Compile Include="Models\EntityNotFoundException.cs" />
<Compile Include="Models\HPlusSportsDbContext.cs" />
<Compile Include="Models\HPlusSportsDbContext.SeedData.cs" />
<Compile Include="Models\Image.cs" />
<Compile Include="Models\Product.cs" />
<Compile Include="Models\ProductRating.cs" />
<Compile Include="Models\Review.cs" />
<Compile Include="Models\ShoppingCart.cs" />
<Compile Include="Models\ShoppingCartItem.cs" />
<Compile Include="Requests\UpdateProductRequest.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Services\ProductUpdateService.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="TestData.xml" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@ -0,0 +1,19 @@
using System.ComponentModel.DataAnnotations;
namespace HPlusSports.Models
{
public class Category
{
public long Id { get; set; }
[Required]
public string Key { get; set; }
[Required]
public string Name { get; set; }
public long? ImageId { get; set; }
public virtual Image Image { get; set; }
}
}

View File

@ -0,0 +1,25 @@
using System;
namespace HPlusSports
{
public class EntityNotFoundException : ApplicationException
{
public object Id { get; }
public Type Type { get; }
public EntityNotFoundException(Type type, object id)
: base($"Entity ${id?.ToString()} of type ${type?.Name} not found.")
{
Type = type;
Id = id;
}
}
public class EntityNotFoundException<T> : EntityNotFoundException
{
public EntityNotFoundException(object id) : base(typeof(T), id)
{
}
}
}

View File

@ -0,0 +1,138 @@
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.IO;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using HPlusSports.Models;
using System.Text.RegularExpressions;
namespace HPlusSports
{
public class HPlusSportsDbContextInitializer
: DropCreateDatabaseIfModelChanges<HPlusSportsDbContext>
{
protected override void Seed(HPlusSportsDbContext context)
{
new SeedData().Populate(context);
}
}
internal class SeedData
{
static readonly Random Random = new Random(1);
public const string TestUserId = "demo@hplussports.com";
static readonly IList<string> UserIds =
Enumerable.Range(0, 10)
.Select(x => $"user{x}")
.Concat(new[] { TestUserId })
.ToArray();
public void Populate(HPlusSportsDbContext context)
{
var doc = ReadTestData();
Populate(context, doc);
}
internal void Populate(HPlusSportsDbContext context, XDocument doc)
{
var categories = doc.Descendants("Categories").Descendants("Category")
.Select(x => new Category
{
Key = ToKey(x.Element("Name").Value),
Name = x.Element("Name").Value,
Image = new Image { Url = x.Element("ImageUrl").Value },
})
.ToArray();
context.Images.AddRange(categories.Select(x => x.Image));
context.SaveChanges();
context.Categories.AddRange(categories);
context.SaveChanges();
var products = doc.Descendants("Product")
.Select(x =>
{
var product = new Product
{
CategoryId = categories.First(cat => cat.Name == x.Element("Category").Value).Id,
Name = x.Element("Name").Value,
Description = StripHtml(x.Element("Description").Value),
MSRP = double.Parse(x.Element("MSRP").Value),
Price = double.Parse(x.Element("Price").Value),
SKU = x.Element("SKU").Value,
Summary = x.Element("Summary").Value,
LastUpdated = DateTime.UtcNow,
LastUpdatedUserId = "admin@hplussports.com",
ThumbnailImage = new Image { Url = x.Element("ThumbnailImageUrl").Value },
};
product.Images.Add(new Image { Url = x.Element("ImageUrl").Value });
return product;
})
.ToArray();
context.Products.AddRange(products);
context.SaveChanges();
var reviews = context.Products.SelectMany(GenerateReviews);
context.Reviews.AddRange(reviews);
context.SaveChanges();
}
IEnumerable<Review> GenerateReviews(Product product)
{
return Enumerable.Range(0, Random.Next(10))
.Select(i =>
new Review
{
SKU = product.SKU,
UserId = UserIds[Random.Next(0, UserIds.Count)],
Rating = Random.Next(3, 5),
});
}
private static string ToKey(string val)
{
return val
.Replace(" ", "-")
.Replace("--", "-")
.Replace("--", "-")
.ToLowerInvariant();
}
private static XDocument ReadTestData()
{
var set = new XmlReaderSettings
{
ConformanceLevel = ConformanceLevel.Fragment
};
using (var stream = typeof(SeedData).Assembly.GetManifestResourceStream("HPlusSports.TestData.xml"))
using (var reader = new StreamReader(stream))
{
return XDocument.Parse(reader.ReadToEnd());
}
}
private static string StripHtml(string source)
{
return
Regex.Replace(
Regex.Replace(
source,
"<[^>]*>",
""
),
"&#[^;]*;",
""
)
.Trim();
}
}
}

View File

@ -0,0 +1,56 @@
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using HPlusSports.Models;
namespace HPlusSports
{
public class HPlusSportsDbContext : DbContext
{
public DbSet<Category> Categories { get; set; }
public DbSet<Image> Images { get; set; }
public DbSet<Product> Products { get; set; }
public DbSet<Review> Reviews { get; set; }
public DbSet<ShoppingCart> ShoppingCarts { get; set; }
public DbSet<ShoppingCartItem> ShoppingCartItems { get; set; }
public HPlusSportsDbContext()
: this("HPlusSports")
{
}
public HPlusSportsDbContext(string connectionName)
: base(connectionName)
{
}
public Product FindProductBySku(string sku)
=> Products.FirstOrDefault(x => x.SKU == sku);
public ProductRating GetProductRating(string sku)
{
var reviews = Reviews.Where(x => x.SKU == sku);
return new ProductRating
{
SKU = sku,
Rating = reviews.Average(x => (double?)x.Rating),
ReviewCount = reviews.Count(),
};
}
public IQueryable<ProductRating> GetProductRatings(IEnumerable<string> skus)
{
return
Reviews
.Where(x => skus.Distinct().Contains(x.SKU))
.GroupBy(x => x.SKU)
.Select(reviews => new ProductRating
{
SKU = reviews.Key,
Rating = reviews.Average(x => x.Rating),
ReviewCount = reviews.Count(),
});
}
}
}

View File

@ -0,0 +1,13 @@
namespace HPlusSports.Models
{
public class Image
{
public long Id { get; set; }
public string Url { get; set; }
public byte[] Content { get; set; }
public string ContentType { get; set; }
}
}

View File

@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace HPlusSports.Models
{
public class Product
{
public long Id { get; set; }
[Required]
public long CategoryId { get; set; }
[Required]
public string SKU { get; set; }
[Required]
public string Name { get; set; }
[Required]
[DataType(DataType.Text)]
public string Summary { get; set; }
[Required]
[DataType(DataType.MultilineText)]
public string Description { get; set; }
[Range(minimum: 0, maximum: double.MaxValue)]
[DataType(DataType.Currency)]
public double MSRP { get; set; }
[Range(minimum: 0, maximum: double.MaxValue)]
[DataType(DataType.Currency)]
public double Price { get; set; }
[Required]
[Display(Name = "Last Updated By")]
public DateTime LastUpdated { get; set; }
[Required]
[Display(Name = "Last Update Timestamp")]
public string LastUpdatedUserId { get; set; }
[NotMapped]
public virtual Category Category { get; set; }
public long? ThumbnailImageId { get; set; }
public virtual Image ThumbnailImage { get; set; }
public virtual ICollection<Image> Images { get; private set; }
public Product()
{
Images = new List<Image>();
}
}
}

View File

@ -0,0 +1,9 @@
namespace HPlusSports.Models
{
public class ProductRating
{
public string SKU { get; set; }
public double? Rating { get; set; }
public int ReviewCount { get; set; }
}
}

View File

@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;
namespace HPlusSports.Models
{
public class Review
{
public long Id { get; set; }
public string UserId { get; set; }
public string SKU { get; set; }
[Range(minimum: 1, maximum: 5)]
public int Rating { get; set; }
[DataType(DataType.MultilineText)]
public string Comments { get; set; }
}
}

View File

@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
namespace HPlusSports.Models
{
public class ShoppingCart
{
public long Id { get; set; }
public string UserId { get; set; }
[DataType(DataType.Currency)]
public double Tax { get; set; }
[DataType(DataType.Currency)]
public double Shipping { get; set; }
[DataType(DataType.Currency)]
public double Total
{
get { return Subtotal + Tax + Shipping; }
}
public string Coupon { get; set; }
public virtual ICollection<ShoppingCartItem> Items { get; private set; }
[DataType(DataType.Currency)]
public double Subtotal
{
get { return Items.Sum(x => x.Total); }
}
public ShoppingCart()
{
Items = new List<ShoppingCartItem>();
}
public void Recalculate()
{
Shipping = Subtotal * 0.1;
Tax = Subtotal * 0.07;
}
}
}

View File

@ -0,0 +1,37 @@
using System.ComponentModel.DataAnnotations;
namespace HPlusSports.Models
{
public class ShoppingCartItem
{
public long Id { get; set; }
[Required]
public string SKU { get; set; }
[Required]
public string Name { get; set; }
[Required]
[DataType(DataType.Currency)]
[Range(minimum: 0, maximum: double.MaxValue)]
public double MSRP { get; set; }
[DataType(DataType.Currency)]
[Range(minimum: 0, maximum: double.MaxValue)]
public double Price { get; set; }
[Range(minimum: 0, maximum: int.MaxValue)]
public int Quantity { get; set; }
[DataType(DataType.Currency)]
public double Total
{
get { return Price * Quantity; }
}
public ShoppingCartItem()
{
}
}
}

View File

@ -0,0 +1,37 @@
using System.ComponentModel.DataAnnotations;
namespace HPlusSports.Models
{
public class UpdateProductRequest
{
[Required]
public long Id { get; set; }
[Required]
public long CategoryId { get; set; }
[Required]
public string SKU { get; set; }
[Required]
public string Name { get; set; }
[Required]
[DataType(DataType.Text)]
public string Summary { get; set; }
[Required]
[DataType(DataType.MultilineText)]
public string Description { get; set; }
[Range(minimum: 0, maximum: double.MaxValue)]
[DataType(DataType.Currency)]
public double MSRP { get; set; }
[Range(minimum: 0, maximum: double.MaxValue)]
[DataType(DataType.Currency)]
public double Price { get; set; }
public string LastUpdatedUserId { get; set; }
}
}

View File

@ -0,0 +1,38 @@
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("Common")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Common")]
[assembly: AssemblyCopyright("Copyright © 2018")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("7709a0d8-f311-4a8e-9e5b-05ed694449ad")]
// 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")]
[assembly: InternalsVisibleTo("HPlusSports.Tests")]

View File

@ -0,0 +1,43 @@
using System.ComponentModel.DataAnnotations;
namespace HPlusSports.Requests
{
public class UpdateProductResponse
{
public bool Success { get; set; }
public string Message { get; set; }
}
public class UpdateProductRequest : MediatR.IRequest<UpdateProductResponse>
{
[Required]
public long Id { get; set; }
[Required]
public long CategoryId { get; set; }
[Required]
public string SKU { get; set; }
[Required]
public string Name { get; set; }
[Required]
[DataType(DataType.Text)]
public string Summary { get; set; }
[Required]
[DataType(DataType.MultilineText)]
public string Description { get; set; }
[Range(minimum: 0, maximum: double.MaxValue)]
[DataType(DataType.Currency)]
public double MSRP { get; set; }
[Range(minimum: 0, maximum: double.MaxValue)]
[DataType(DataType.Currency)]
public double Price { get; set; }
public string LastUpdatedUserId { get; set; }
}
}

View File

@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using HPlusSports.Requests;
namespace HPlusSports.Services
{
public class ProductUpdateService
: MediatR.IRequestHandler<UpdateProductRequest, UpdateProductResponse>
{
private readonly HPlusSportsDbContext _context;
public ProductUpdateService(HPlusSportsDbContext context)
{
_context = context;
}
public Task<UpdateProductResponse> Handle(UpdateProductRequest request, CancellationToken cancellationToken)
{
return Task.FromResult(Update(request));
}
public UpdateProductResponse Update(UpdateProductRequest request)
{
var existing = _context.Products.Find(request.Id);
if (existing == null)
{
return new UpdateProductResponse
{
Success = false,
Message = $"Couldn't update product #\"{request.Id}\": product not found!",
};
}
var hasPriceChanged = existing.Price != request.Price;
existing.CategoryId = request.CategoryId;
existing.Description = request.Description;
existing.MSRP = request.MSRP;
existing.Name = request.Name;
existing.Price = request.Price;
existing.SKU = request.SKU;
existing.Summary = request.Summary;
existing.LastUpdated = DateTime.UtcNow;
existing.LastUpdatedUserId = request.LastUpdatedUserId;
_context.SaveChanges();
if (hasPriceChanged)
{
var cartsToUpdate =
_context.ShoppingCarts
.Include("Items")
.Where(cart => cart.Items.Any(x => x.SKU == request.SKU));
foreach (var cart in cartsToUpdate)
{
foreach (var cartItem in cart.Items.Where(x => x.SKU == request.SKU))
{
cartItem.Price = request.Price;
}
cart.Recalculate();
}
}
_context.SaveChanges();
return new UpdateProductResponse
{
Success = true,
Message = $"Successfully updated \"{request.Name}\"",
};
}
}
}

Binary file not shown.

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="EntityFramework" version="6.2.0" targetFramework="net461" />
<package id="MediatR" version="4.1.0" targetFramework="net461" />
</packages>