diff --git a/.nuget/NuGet.Config b/.nuget/NuGet.Config
new file mode 100644
index 0000000..6a318ad
--- /dev/null
+++ b/.nuget/NuGet.Config
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.nuget/NuGet.exe b/.nuget/NuGet.exe
new file mode 100644
index 0000000..319b05f
Binary files /dev/null and b/.nuget/NuGet.exe differ
diff --git a/.nuget/NuGet.targets b/.nuget/NuGet.targets
new file mode 100644
index 0000000..b44341f
--- /dev/null
+++ b/.nuget/NuGet.targets
@@ -0,0 +1,150 @@
+
+
+
+ $(MSBuildProjectDirectory)\..\
+
+
+ false
+
+
+ false
+
+
+ true
+
+
+ false
+
+
+
+
+
+
+
+
+
+ $([System.IO.Path]::Combine($(SolutionDir), ".nuget"))
+ $([System.IO.Path]::Combine($(ProjectDir), "packages.config"))
+
+
+
+
+ $(SolutionDir).nuget
+ packages.config
+
+
+
+
+ $(NuGetToolsPath)\nuget.exe
+ @(PackageSource)
+
+ "$(NuGetExePath)"
+ mono --runtime=v4.0.30319 $(NuGetExePath)
+
+ $(TargetDir.Trim('\\'))
+
+ -RequireConsent
+
+ $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(RequireConsentSwitch) -solutionDir "$(SolutionDir) "
+ $(NuGetCommand) pack "$(ProjectPath)" -p Configuration=$(Configuration) -o "$(PackageOutputDir)" -symbols
+
+
+
+ RestorePackages;
+ $(ResolveReferencesDependsOn);
+
+
+
+
+ $(BuildDependsOn);
+ BuildPackage;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Ebuy.Common/App.config b/Ebuy.Common/App.config
new file mode 100644
index 0000000..da2b57b
--- /dev/null
+++ b/Ebuy.Common/App.config
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Ebuy.Common/Clock.cs b/Ebuy.Common/Clock.cs
new file mode 100644
index 0000000..c23a561
--- /dev/null
+++ b/Ebuy.Common/Clock.cs
@@ -0,0 +1,42 @@
+using System;
+
+namespace Ebuy.Util
+{
+ public static class Clock
+ {
+ private static Func _nowThunk = () => DateTime.UtcNow;
+
+
+ public static DateTime Now
+ {
+ get { return _nowThunk(); }
+ }
+
+ internal static ModifiedTimeLock ModifiedTime(DateTime time)
+ {
+ return new ModifiedTimeLock(() => time);
+ }
+
+ internal static ModifiedTimeLock ModifiedTime(Func time)
+ {
+ return new ModifiedTimeLock(time);
+ }
+
+ internal class ModifiedTimeLock : IDisposable
+ {
+ private readonly Func _previousThunk;
+
+ public ModifiedTimeLock(Func time)
+ {
+ _previousThunk = _nowThunk;
+ _nowThunk = time;
+ }
+
+ public void Dispose()
+ {
+ _nowThunk = _previousThunk;
+ }
+ }
+ }
+
+}
diff --git a/Ebuy.Common/DataAccess/EbuyDataContext.cs b/Ebuy.Common/DataAccess/EbuyDataContext.cs
new file mode 100644
index 0000000..e6e7b4f
--- /dev/null
+++ b/Ebuy.Common/DataAccess/EbuyDataContext.cs
@@ -0,0 +1,30 @@
+using System.Data.Entity;
+
+namespace Ebuy.DataAccess
+{
+ public class EbuyDataContext : DbContext
+ {
+ public DbSet Auctions { get; set; }
+ public DbSet Bids { get; set; }
+ public DbSet Categories { get; set; }
+ public DbSet Users { get; set; }
+
+ public EbuyDataContext()
+ {
+ Configuration.ProxyCreationEnabled = false;
+#if(DEBUG)
+ Database.SetInitializer(new EbuyInitializer());
+#endif
+ }
+
+ protected override void OnModelCreating(DbModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity()
+ .HasRequired(x => x.Auction)
+ .WithMany()
+ .WillCascadeOnDelete(false);
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/Ebuy.Common/DataAccess/EbuyInitializer.cs b/Ebuy.Common/DataAccess/EbuyInitializer.cs
new file mode 100644
index 0000000..d8ce85e
--- /dev/null
+++ b/Ebuy.Common/DataAccess/EbuyInitializer.cs
@@ -0,0 +1,149 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Data.Entity;
+using System.Linq;
+
+namespace Ebuy.DataAccess
+{
+ public class EbuyInitializer
+ : DropCreateDatabaseIfModelChanges
+ {
+ readonly Random _rand = new Random();
+
+ protected override void Seed(EbuyDataContext context)
+ {
+ context.Users.Add(new User
+ {
+ Username = "Frank Sinatra",
+ EmailAddress = "frank@theratpack.com",
+ });
+
+ context.Users.Add(new User
+ {
+ Username = "Freddie Mercury",
+ EmailAddress = "freddie@queenband.com",
+ });
+
+ context.Users.Add(new User
+ {
+ Username = "John Lennon",
+ EmailAddress = "lenny@thebeatles.com",
+ });
+
+ context.Categories.Add(new Category("Collectibles"));
+
+ context.Categories.Add(
+ new Category("Electronics")
+ {
+ SubCategories = new[] {
+ new Category("Cameras & Photography"),
+ new Category("Computers & Networking"),
+ new Category("TV, Audio, and Video"),
+ new Category("Video Games & Systems") {
+ SubCategories = new [] {
+ new Category("Video Game Systems")
+ }
+ },
+ }
+ });
+
+ context.Categories.Add(
+ new Category("Home & Outdoors")
+ {
+ SubCategories = new[] {
+ new Category("Home & Garden"),
+ new Category("Sporting Goods"),
+ new Category("Toys & Hobbies"),
+ }
+ });
+
+ var videoGameSystems = context.Categories.Local.Single(x => x.Name == "Video Game Systems");
+
+ context.Auctions.Add(new Auction
+ {
+ Categories = new[] { videoGameSystems },
+ Title = "Xbox 360 Elite",
+ Description = "The Xbox 360 Elite gaming system is the ultimate in gaming",
+ Images = new WebsiteImage[] { "~/Content/images/products/xbox360elite.jpg" },
+ });
+
+ context.Auctions.Add(new Auction
+ {
+ Categories = new[] { videoGameSystems },
+ Title = "Sony PSP Go",
+ Description = "The smallest and mightiest PSP system yet.",
+ Images = new WebsiteImage[] { "~/Content/images/products/psp.jpg" },
+ });
+
+ context.Auctions.Add(new Auction
+ {
+ Categories = new[] { videoGameSystems },
+ Title = "Xbox 360 Kinect Sensor with Game Bundle",
+ Description = "You are the controller with Kinect for Xbox 360!",
+ Images = new WebsiteImage[] { "~/Content/images/products/kinect.jpg" },
+ });
+
+ context.Auctions.Add(new Auction
+ {
+ Categories = new[] { videoGameSystems },
+ Title = "Sony Playstation 3 120GB Slim Console",
+ Description = "The fourth generation of hardware released for the PlayStation 3 entertainment platform, the PlayStation 3 120GB system is the next stage in the evolution of Sony's console gaming powerhouse.",
+ Images = new WebsiteImage[] { "~/Content/images/products/ps3.jpg" },
+ });
+
+ context.Auctions.Add(new Auction
+ {
+ Categories = new[] { videoGameSystems },
+ Title = "Nintendo Wii Console Black",
+ Description = "Wii Sports Resort takes the inclusive, fun and intuitive controls of the original Wii Sports to the next level, introducing a whole new set of entertaining and physically immersive activities.",
+ Images = new WebsiteImage[] { "~/Content/images/products/wii.jpg" },
+ });
+
+
+
+ var sports = context.Categories.Local.Single(x => x.Name == "Sporting Goods");
+
+ context.Auctions.Add(new Auction
+ {
+ Categories = new Collection { sports },
+ Title = "Burton Mayhem snow board",
+ Description = "Burton Mayhem snow board: 159cm wide",
+ Images = new WebsiteImage[] { "~/Content/images/products/burtonMayhem.jpg" },
+ });
+
+
+
+ var collectibles = context.Categories.Local.Single(x => x.Name == "Collectibles");
+
+ context.Auctions.Add(new Auction
+ {
+ Categories = new Collection { collectibles },
+ Title = "Lock of John Lennon's hair",
+ Description = "Lock of John Lennon's hair",
+ Images = new WebsiteImage[] { "~/Content/images/products/lockOfHair.jpg" },
+ });
+
+
+
+ int featured = 0;
+ var users = context.Users.Local.ToArray();
+
+ foreach (var auction in context.Auctions.Local)
+ {
+ auction.StartTime = DateTime.UtcNow
+ .AddDays(_rand.Next(-10, -1))
+ .AddHours(_rand.Next(1, 24))
+ .AddHours(_rand.Next(1, 60));
+
+ auction.EndTime = auction.StartTime.AddDays(_rand.Next(3, 14));
+ auction.Owner = users[_rand.Next(users.Length)];
+ auction.StartPrice = "$" + _rand.Next(1, 10);
+ auction.CurrentPrice = "$" + _rand.Next(10, 100);
+
+ if (featured++ < 3)
+ auction.FeatureAuction();
+ }
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ebuy.Common/DataAccess/IRepository.cs b/Ebuy.Common/DataAccess/IRepository.cs
new file mode 100644
index 0000000..dc97886
--- /dev/null
+++ b/Ebuy.Common/DataAccess/IRepository.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+
+namespace Ebuy.DataAccess
+{
+ public interface IRepository : IDisposable
+ {
+ void Add(TModel instance) where TModel : class, IEntity;
+ void Add(IEnumerable instances) where TModel : class, IEntity;
+
+ IQueryable All(params string[] includePaths) where TModel : class, IEntity;
+
+ void Delete(object key) where TModel : class, IEntity;
+ void Delete(TModel instance) where TModel : class, IEntity;
+ void Delete(Expression> predicate) where TModel : class, IEntity;
+
+ TModel Single(object key) where TModel : class, IEntity;
+ TModel Single(Expression> predicate, params string[] includePaths) where TModel : class, IEntity;
+
+ IQueryable Query(Expression> predicate, params string[] includePaths) where TModel : class, IEntity;
+ }
+}
\ No newline at end of file
diff --git a/Ebuy.Common/DataAccess/Repository.cs b/Ebuy.Common/DataAccess/Repository.cs
new file mode 100644
index 0000000..8fde81c
--- /dev/null
+++ b/Ebuy.Common/DataAccess/Repository.cs
@@ -0,0 +1,139 @@
+using System;
+using System.Collections.Generic;
+using System.Data.Entity;
+using System.Data.Entity.Infrastructure;
+using System.Diagnostics.Contracts;
+using System.Linq;
+using System.Linq.Expressions;
+
+namespace Ebuy.DataAccess
+{
+ public class Repository : IRepository
+ {
+ private readonly DbContext _context;
+ private readonly bool _isSharedContext;
+
+ public Repository(DbContext context, bool isSharedContext = true)
+ {
+ Contract.Requires(context != null);
+
+ _context = context;
+ _isSharedContext = isSharedContext;
+ }
+
+
+ public void Add(TModel instance)
+ where TModel : class, IEntity
+ {
+ Contract.Requires(instance != null);
+
+ _context.Set().Add(instance);
+
+ if (_isSharedContext == false)
+ _context.SaveChanges();
+ }
+
+ public void Add(IEnumerable instances)
+ where TModel : class, IEntity
+ {
+ Contract.Requires(instances != null);
+
+ foreach (var instance in instances)
+ {
+ Add(instance);
+ }
+ }
+
+
+ public IQueryable All(params string[] includePaths)
+ where TModel : class, IEntity
+ {
+ return Query(x => true, includePaths);
+ }
+
+
+ public void Dispose()
+ {
+ // If this is a shared (or null) context then
+ // we're not responsible for disposing it
+ if (_isSharedContext || _context == null)
+ return;
+
+ _context.Dispose();
+ }
+
+
+ public void Delete(object id)
+ where TModel : class, IEntity
+ {
+ Contract.Requires(id != null);
+
+ var instance = Single(id);
+ Delete(instance);
+ }
+
+ public void Delete(TModel instance)
+ where TModel : class, IEntity
+ {
+ Contract.Requires(instance != null);
+
+ if (instance != null)
+ _context.Set().Remove(instance);
+ }
+
+ public void Delete(Expression> predicate)
+ where TModel : class, IEntity
+ {
+ Contract.Requires(predicate != null);
+
+ TModel entity = Single(predicate);
+ Delete(entity);
+ }
+
+
+ public TModel Single(object id)
+ where TModel : class, IEntity
+ {
+ Contract.Requires(id != null);
+
+ var instance = _context.Set().Find(id);
+ return instance;
+ }
+
+ public TModel Single(Expression> predicate, params string[] includePaths)
+ where TModel : class, IEntity
+ {
+ Contract.Requires(predicate != null);
+
+ var instance = GetSetWithIncludedPaths(includePaths).SingleOrDefault(predicate);
+ return instance;
+ }
+
+
+ public IQueryable Query(Expression> predicate, params string[] includePaths)
+ where TModel : class, IEntity
+ {
+ Contract.Requires(predicate != null);
+
+ var items = GetSetWithIncludedPaths(includePaths);
+
+ if (predicate != null)
+ return items.Where(predicate);
+
+ return items;
+ }
+
+
+ private DbQuery GetSetWithIncludedPaths(IEnumerable includedPaths) where TModel : class, IEntity
+ {
+ DbQuery items = _context.Set();
+
+ foreach (var path in includedPaths ?? Enumerable.Empty())
+ {
+ items = items.Include(path);
+ }
+
+ return items;
+ }
+ }
+}
diff --git a/Ebuy.Common/Ebuy.Common.csproj b/Ebuy.Common/Ebuy.Common.csproj
new file mode 100644
index 0000000..18b3e75
--- /dev/null
+++ b/Ebuy.Common/Ebuy.Common.csproj
@@ -0,0 +1,81 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {F2B03BD9-D3AE-4F1C-BC2F-2CBEA3DE29DF}
+ Library
+ Properties
+ Ebuy
+ Ebuy.Common
+ v4.5
+ 512
+
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+ false
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+ false
+
+
+
+ False
+ ..\packages\EntityFramework.5.0.0\lib\net45\EntityFramework.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Ebuy.Common/Ebuy.Common.csproj.DotSettings b/Ebuy.Common/Ebuy.Common.csproj.DotSettings
new file mode 100644
index 0000000..21f1e82
--- /dev/null
+++ b/Ebuy.Common/Ebuy.Common.csproj.DotSettings
@@ -0,0 +1,2 @@
+
+ True
\ No newline at end of file
diff --git a/Ebuy.Common/Entities/Annotations/UniqueAttribute.cs b/Ebuy.Common/Entities/Annotations/UniqueAttribute.cs
new file mode 100644
index 0000000..15a70b3
--- /dev/null
+++ b/Ebuy.Common/Entities/Annotations/UniqueAttribute.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Data.Entity;
+using System.Linq;
+using System.Reflection;
+
+namespace Ebuy.DataAnnotations
+{
+ public class UniqueConstraintApplier
+ {
+ private const string UniqueConstraintQuery = "ALTER TABLE [{0}] ADD CONSTRAINT [{0}_{1}_unique] UNIQUE ([{1}])";
+
+ public void ApplyUniqueConstraints(DbContext context)
+ {
+ var modelTypes =
+ from dbContextProperties in context.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public)
+ let propertyTypeGenericArguments = dbContextProperties.PropertyType.GetGenericArguments()
+ where propertyTypeGenericArguments.Count() == 1
+ select propertyTypeGenericArguments.Single();
+
+ var modelsWithUniqueProperties =
+ from modelType in modelTypes
+ from property in modelType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
+ from uniqueAttribute in property.GetCustomAttributes(true).OfType()
+ let propertyName = property.Name
+
+ group propertyName by modelType into uniquePropertiesByModel
+
+ select new {
+ Model = uniquePropertiesByModel.Key,
+ Properties = (IEnumerable) uniquePropertiesByModel
+ };
+
+ foreach (var model in modelsWithUniqueProperties)
+ {
+ foreach (var property in model.Properties)
+ {
+ string tableName = GetTableName(model.Model);
+ string query = string.Format(UniqueConstraintQuery, tableName, property);
+ context.Database.ExecuteSqlCommand(query);
+ }
+ }
+ }
+
+ private string GetTableName(Type model)
+ {
+ var modelName = model.Name;
+
+ if (modelName.EndsWith("y"))
+ modelName = modelName.Substring(0, modelName.Length - 1) + "ie";
+
+ return modelName + "s";
+ }
+ }
+
+ [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
+ public class UniqueAttribute : RequiredAttribute
+ {
+ }
+}
diff --git a/Ebuy.Common/Entities/Auction.cs b/Ebuy.Common/Entities/Auction.cs
new file mode 100644
index 0000000..57fea82
--- /dev/null
+++ b/Ebuy.Common/Entities/Auction.cs
@@ -0,0 +1,125 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Diagnostics.Contracts;
+using System.Linq;
+
+namespace Ebuy
+{
+ public class Auction : Entity
+ {
+ [Required, StringLength(500)]
+ public virtual string Title { get; set; }
+
+ [Required]
+ public virtual string Description { get; set; }
+
+ [Required]
+ public virtual DateTime StartTime { get; set; }
+
+ [Required]
+ public virtual DateTime EndTime { get; set; }
+
+ public virtual Currency StartPrice { get; set; }
+
+ public virtual Currency CurrentPrice { get; set; }
+
+ [ForeignKey("WinningBid")]
+ public Guid? WinningBidId { get; set; }
+ public virtual Bid WinningBid { get; private set; }
+
+ public bool IsCompleted
+ {
+ get { return EndTime <= DateTime.Now; }
+ }
+
+ public virtual bool IsFeaturedAuction { get; private set; }
+
+ public virtual ICollection Bids { get; set; }
+
+ public virtual ICollection Categories { get; set; }
+
+ public virtual ICollection Images { get; set; }
+
+ public long OwnerId { get; set; }
+ public virtual User Owner { get; set; }
+
+ public virtual CurrencyCode CurrencyCode
+ {
+ get
+ {
+ return (CurrentPrice != null) ? CurrentPrice.Code : null;
+ }
+ }
+
+ public Auction()
+ {
+ Bids = new Collection();
+ Categories = new Collection();
+ Images = new Collection();
+ StartTime = DateTime.UtcNow;
+ }
+
+ public void FeatureAuction()
+ {
+ IsFeaturedAuction = true;
+ }
+
+ public Bid PostBid(User user, double bidAmount)
+ {
+ return PostBid(user, new Currency(CurrencyCode, bidAmount));
+ }
+
+ public Bid PostBid(User user, Currency bidAmount)
+ {
+ Contract.Requires(user != null);
+
+ if (bidAmount.Code != CurrencyCode)
+ throw new InvalidBidException(bidAmount, WinningBid);
+
+ if (bidAmount.Value <= CurrentPrice.Value)
+ throw new InvalidBidException(bidAmount, WinningBid);
+
+ var bid = new Bid(user, this, bidAmount);
+
+ CurrentPrice = bidAmount;
+ WinningBidId = bid.Id;
+
+ Bids.Add(bid);
+
+ return bid;
+ }
+
+ }
+
+ public class InvalidBidException : Exception
+ {
+ public Currency BidAmount { get; set; }
+ public Bid WinningBid { get; set; }
+
+ public InvalidBidException(Currency bidAmount, Bid winningBid = null)
+ {
+ BidAmount = bidAmount;
+ WinningBid = winningBid;
+ }
+ }
+
+
+ public static class AuctionExtensions
+ {
+
+ public static IEnumerable Active(this IEnumerable auctions)
+ {
+ return auctions.Where(x => x.IsCompleted == false);
+ }
+
+ public static IEnumerable Featured(this IEnumerable auctions)
+ {
+ return auctions.Where(x => x.IsFeaturedAuction);
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/Ebuy.Common/Entities/Bid.cs b/Ebuy.Common/Entities/Bid.cs
new file mode 100644
index 0000000..5234f39
--- /dev/null
+++ b/Ebuy.Common/Entities/Bid.cs
@@ -0,0 +1,71 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.Contracts;
+
+namespace Ebuy
+{
+ public class Bid : Entity, IEquatable
+ {
+ public Currency Amount { get; private set; }
+
+ public virtual Auction Auction { get; private set; }
+
+ public DateTime Timestamp { get; private set; }
+
+ public virtual User User { get; private set; }
+
+ public bool IsWinningBid
+ {
+ get
+ {
+ return Auction != null
+ && Id == Auction.WinningBidId;
+ }
+ }
+
+
+ public Bid(User user, Auction auction, Currency price)
+ {
+ Contract.Requires(user != null);
+ Contract.Requires(auction != null);
+ Contract.Requires(price != null);
+
+ User = user;
+ Auction = auction;
+ Amount = price;
+ Timestamp = DateTime.Now;
+ }
+
+ public Bid()
+ {
+ }
+
+ public bool Equals(Bid other)
+ {
+ if (ReferenceEquals(null, other)) return false;
+ if (ReferenceEquals(this, other)) return true;
+ return other.Id.Equals(Id);
+ }
+
+ public override int GetHashCode()
+ {
+ return Id.GetHashCode();
+ }
+
+
+ public class Metadata
+ {
+ [Required]
+ public object Auction;
+
+ [Required]
+ public object Amount;
+
+ [Required]
+ public object Timestamp;
+
+ [Required]
+ public object User;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ebuy.Common/Entities/Category.cs b/Ebuy.Common/Entities/Category.cs
new file mode 100644
index 0000000..3345590
--- /dev/null
+++ b/Ebuy.Common/Entities/Category.cs
@@ -0,0 +1,57 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Ebuy
+{
+ [MetadataType(typeof(Category.Metadata))]
+ public class Category : Entity
+ {
+ public virtual ICollection Auctions { get; set; }
+
+ public bool IsTopLevelCategory
+ {
+ get { return ParentId == null; }
+ }
+
+ public string Name { get; set; }
+
+ public long? ParentId { get; set; }
+
+ public virtual Category Parent { get; set; }
+
+ public virtual ICollection SubCategories { get; set; }
+
+
+ public Category()
+ {
+ Auctions = new Collection();
+ SubCategories = new Collection();
+ }
+
+ public Category(string name)
+ {
+ Name = name;
+ }
+
+
+ protected override string GenerateKey()
+ {
+ if (string.IsNullOrWhiteSpace(Name))
+ // TODO: Localize
+ throw new EntityKeyGenerationException(GetType(), "Name is empty");
+
+ return KeyGenerator.Generate(Name);
+ }
+
+ public class Metadata
+ {
+ [Required, StringLength(100)]
+ public object Name;
+
+ [ForeignKey("Parent")]
+ public object ParentId;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ebuy.Common/Entities/Currency.cs b/Ebuy.Common/Entities/Currency.cs
new file mode 100644
index 0000000..fb9b69d
--- /dev/null
+++ b/Ebuy.Common/Entities/Currency.cs
@@ -0,0 +1,129 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Diagnostics.Contracts;
+using System.Linq;
+
+namespace Ebuy
+{
+ [ComplexType]
+ public class CurrencyCode
+ {
+ private readonly string _value;
+
+ public CurrencyCode(string value) : this()
+ {
+ _value = value;
+ }
+
+ public CurrencyCode()
+ {
+ }
+
+ public static implicit operator CurrencyCode(string code)
+ {
+ return new CurrencyCode(code);
+ }
+
+ public static implicit operator string(CurrencyCode code)
+ {
+ return code == null ? null : code._value;
+ }
+ }
+
+ [ComplexType]
+ public class Currency : IEquatable
+ {
+ public static IDictionary CurrencyCodesBySymbol = new Dictionary() {
+ { '€', "EUR" },
+ { '£', "GBP" },
+ { '¥', "JPY" },
+ { '$', "USD" },
+ };
+
+ public string Code { get; private set; }
+ public double Value { get; private set; }
+
+
+ public Currency(CurrencyCode code, double value)
+ {
+ Code = code;
+ Value = value;
+ }
+
+ public Currency(string currency)
+ {
+ Contract.Requires(!string.IsNullOrWhiteSpace(currency));
+ Contract.Requires(currency.Length > 1);
+
+ Code = CurrencyCodesBySymbol[currency[0]];
+ Value = double.Parse(currency.Substring(1));
+ }
+
+ public Currency()
+ {
+ }
+
+
+ public bool Equals(Currency other)
+ {
+ if (ReferenceEquals(null, other)) return false;
+ if (ReferenceEquals(this, other)) return true;
+ return Equals(other.Code, Code) && other.Value == Value;
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(null, obj)) return false;
+ if (ReferenceEquals(this, obj)) return true;
+ if (obj.GetType() != typeof (Currency)) return false;
+ return Equals((Currency) obj);
+ }
+
+ public override string ToString()
+ {
+ var symbol = CurrencyCodesBySymbol.Single(x => x.Value == Code).Key;
+ return string.Format("{0}{1:N2}", symbol, Value);
+ }
+
+ public static Currency operator +(Currency x, double amount)
+ {
+ Contract.Requires(x != null);
+ return new Currency(x.Code, x.Value + amount);
+ }
+
+ public static Currency operator -(Currency x, double amount)
+ {
+ Contract.Requires(x != null);
+ return new Currency(x.Code, x.Value - amount);
+ }
+
+ public static bool operator ==(Currency left, Currency right)
+ {
+ return Equals(left, right);
+ }
+
+ public static bool operator !=(Currency left, Currency right)
+ {
+ return !Equals(left, right);
+ }
+
+ public static implicit operator Currency(string currency)
+ {
+ return new Currency(currency);
+ }
+
+ public static implicit operator string(Currency currency)
+ {
+ return currency.ToString();
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return ((Code != null ? Code.GetHashCode() : 0)*397) ^ Value.GetHashCode();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ebuy.Common/Entities/Entity.cs b/Ebuy.Common/Entities/Entity.cs
new file mode 100644
index 0000000..56863b2
--- /dev/null
+++ b/Ebuy.Common/Entities/Entity.cs
@@ -0,0 +1,112 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.Contracts;
+using System.Web;
+using Ebuy.DataAnnotations;
+
+namespace Ebuy
+{
+ public interface IEntity
+ {
+ ///
+ /// The entity's unique (and URL-safe) public identifier
+ ///
+ ///
+ /// This is the identifier that should be exposed via the web, etc.
+ ///
+ string Key { get; }
+ }
+
+ public abstract class Entity : IEntity, IEquatable>
+ where TId : struct
+ {
+ [Key]
+ public virtual TId Id
+ {
+ get
+ {
+ if (_id == null && typeof(TId) == typeof(Guid))
+ _id = Guid.NewGuid();
+
+ return _id == null ? default(TId) : (TId)_id;
+ }
+ protected set { _id = value; }
+ }
+ private object _id;
+
+ [Unique, StringLength(50)]
+ public virtual string Key
+ {
+ get { return _key = _key ?? GenerateKey(); }
+ protected set { _key = value; }
+ }
+ private string _key;
+
+
+ protected virtual string GenerateKey()
+ {
+ return KeyGenerator.Generate();
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(null, obj)) return false;
+ if (ReferenceEquals(this, obj)) return true;
+ if (obj.GetType() != typeof(Entity)) return false;
+ return Equals((Entity)obj);
+ }
+
+ public bool Equals(Entity other)
+ {
+ if (ReferenceEquals(null, other)) return false;
+ if (ReferenceEquals(this, other)) return true;
+ if (other.GetType() != GetType()) return false;
+
+ if (default(TId).Equals(Id) || default(TId).Equals(other.Id))
+ return Equals(other._key, _key);
+
+ return other.Id.Equals(Id);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ if (default(TId).Equals(Id))
+ return Key.GetHashCode() * 397;
+
+ return Id.GetHashCode();
+ }
+ }
+
+ public override string ToString()
+ {
+ return Key;
+ }
+
+ public static bool operator ==(Entity left, Entity right)
+ {
+ return Equals(left, right);
+ }
+
+ public static bool operator !=(Entity left, Entity right)
+ {
+ return !Equals(left, right);
+ }
+
+
+ public static class KeyGenerator
+ {
+ public static string Generate()
+ {
+ return Generate(Guid.NewGuid().ToString("D").Substring(24));
+ }
+
+ public static string Generate(string input)
+ {
+ Contract.Requires(!string.IsNullOrWhiteSpace(input));
+ return HttpUtility.UrlEncode(input.Replace(" ", "_").Replace("-", "_").Replace("&", "and"));
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ebuy.Common/Entities/EntityKeyGenerationException.cs b/Ebuy.Common/Entities/EntityKeyGenerationException.cs
new file mode 100644
index 0000000..c94543d
--- /dev/null
+++ b/Ebuy.Common/Entities/EntityKeyGenerationException.cs
@@ -0,0 +1,15 @@
+using System;
+
+namespace Ebuy
+{
+ public class EntityKeyGenerationException : Exception
+ {
+ public Type EntityType { get; set; }
+
+ public EntityKeyGenerationException(Type entityType, string message)
+ : base(message)
+ {
+ EntityType = entityType;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ebuy.Common/Entities/Payment.cs b/Ebuy.Common/Entities/Payment.cs
new file mode 100644
index 0000000..1ac4c16
--- /dev/null
+++ b/Ebuy.Common/Entities/Payment.cs
@@ -0,0 +1,28 @@
+using System;
+
+namespace Ebuy
+{
+ public class Payment : Entity
+ {
+ public Currency Amount { get; private set; }
+
+ public Auction Auction { get; private set; }
+
+ public DateTime Timestamp { get; private set; }
+
+ public User User { get; set; }
+
+
+ public Payment(User user, Auction auction, Currency amount)
+ {
+ User = user;
+ Auction = auction;
+ Amount = amount;
+ Timestamp = DateTime.Now;
+ }
+
+ private Payment()
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ebuy.Common/Entities/User.cs b/Ebuy.Common/Entities/User.cs
new file mode 100644
index 0000000..94686fe
--- /dev/null
+++ b/Ebuy.Common/Entities/User.cs
@@ -0,0 +1,70 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.Contracts;
+using Ebuy.DataAnnotations;
+
+namespace Ebuy
+{
+ [MetadataType(typeof(User.Metadata))]
+ public class User : Entity
+ {
+ public virtual ICollection Selling { get; private set; }
+
+ public virtual string DisplayName
+ {
+ get { return _displayName ?? Username; }
+ set { _displayName = value; }
+ }
+ private string _displayName;
+
+ [Unique]
+ public string EmailAddress { get; set; }
+
+ public virtual ICollection Payments { get; private set; }
+
+ [Unique]
+ public string Username { get; set; }
+
+ public virtual ICollection WatchedAuctions { get; private set; }
+
+
+ public User()
+ {
+ Payments = new Collection();
+ Selling = new Collection();
+ WatchedAuctions = new Collection();
+ }
+
+
+ protected override string GenerateKey()
+ {
+ if(string.IsNullOrWhiteSpace(Username))
+ throw new EntityKeyGenerationException(GetType(), "Username is empty");
+
+ return KeyGenerator.Generate(Username);
+ }
+
+
+ public void Bid(Auction auction, Currency bidAmount)
+ {
+ Contract.Requires(auction != null);
+ Contract.Requires(bidAmount != null);
+
+ auction.PostBid(this, bidAmount);
+ }
+
+
+ public class Metadata
+ {
+ [Required, StringLength(50)]
+ public object DisplayName;
+
+ [Required, StringLength(100, MinimumLength = 5)]
+ public object EmailAddress;
+
+ [Required, StringLength(100, MinimumLength = 3)]
+ public object Username;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ebuy.Common/Entities/WebsiteImage.cs b/Ebuy.Common/Entities/WebsiteImage.cs
new file mode 100644
index 0000000..c93a3bf
--- /dev/null
+++ b/Ebuy.Common/Entities/WebsiteImage.cs
@@ -0,0 +1,43 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+
+namespace Ebuy
+{
+ [MetadataType(typeof(WebsiteImage.Metadata))]
+ public class WebsiteImage : Entity
+ {
+ public string ImageUrl { get; set; }
+
+ public string Title { get; set; }
+
+ public string ThumbnailUrl { get; set; }
+
+
+ public WebsiteImage()
+ {
+ }
+
+ public WebsiteImage(string imageUrl)
+ {
+ ImageUrl = imageUrl;
+ }
+
+
+ public static implicit operator WebsiteImage(string imageUrl)
+ {
+ return new WebsiteImage(imageUrl);
+ }
+
+ public class Metadata
+ {
+ [StringLength(2000)]
+ public object ImageUrl;
+
+ [StringLength(2000)]
+ public object ThumbnailUrl;
+
+ [StringLength(2000)]
+ public object Title;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ebuy.Common/Properties/AssemblyInfo.cs b/Ebuy.Common/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..ac336c2
--- /dev/null
+++ b/Ebuy.Common/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("Ebuy.Common")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("Ebuy.Common")]
+[assembly: AssemblyCopyright("Copyright © 2012")]
+[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("043c52b8-8c03-4480-a745-abc235fa6e2e")]
+
+// 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/Ebuy.Common/packages.config b/Ebuy.Common/packages.config
new file mode 100644
index 0000000..a90387c
--- /dev/null
+++ b/Ebuy.Common/packages.config
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/Ebuy.Mvc/Ebuy.Mvc.csproj b/Ebuy.Mvc/Ebuy.Mvc.csproj
index fedc201..c1f62bc 100644
--- a/Ebuy.Mvc/Ebuy.Mvc.csproj
+++ b/Ebuy.Mvc/Ebuy.Mvc.csproj
@@ -43,6 +43,7 @@
+
diff --git a/Ebuy.Mvc/Extensions/CollectionExtensions.cs b/Ebuy.Mvc/Extensions/CollectionExtensions.cs
new file mode 100644
index 0000000..a5c0edc
--- /dev/null
+++ b/Ebuy.Mvc/Extensions/CollectionExtensions.cs
@@ -0,0 +1,39 @@
+using System.Collections.Generic;
+using System.Diagnostics.Contracts;
+using System.Linq;
+
+namespace Ebuy
+{
+ public static class CollectionExtensions
+ {
+ public static IEnumerable Page(this IEnumerable source, int pageIndex, int pageSize)
+ {
+ Contract.Requires(pageIndex >= 0, "Page index cannot be negative");
+ Contract.Requires(pageSize >= 0, "Page size cannot be negative");
+
+ int skip = pageIndex * pageSize;
+
+ if (skip > 0)
+ source = source.Skip(skip);
+
+ source = source.Take(pageSize);
+
+ return source;
+ }
+
+ public static IQueryable Page(this IQueryable source, int pageIndex, int pageSize)
+ {
+ Contract.Requires(pageIndex >= 0, "Page index cannot be negative");
+ Contract.Requires(pageSize >= 0, "Page size cannot be negative");
+
+ int skip = pageIndex * pageSize;
+
+ if (skip > 0)
+ source = source.Skip(skip);
+
+ source = source.Take(pageSize);
+
+ return source;
+ }
+ }
+}
diff --git a/Ebuy.Website.Tests/Ebuy.Website.Tests.csproj b/Ebuy.Website.Tests/Ebuy.Website.Tests.csproj
index 1555964..22980b4 100644
--- a/Ebuy.Website.Tests/Ebuy.Website.Tests.csproj
+++ b/Ebuy.Website.Tests/Ebuy.Website.Tests.csproj
@@ -14,6 +14,8 @@
v4.5
512
{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
+ ..\
+ true
true
@@ -109,6 +111,7 @@
+