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 @@ +