diff options
| author | unknown <mkhan@.arcresources.ca> | 2009-10-22 16:58:51 -0600 |
|---|---|---|
| committer | unknown <mkhan@.arcresources.ca> | 2009-10-22 16:58:51 -0600 |
| commit | 38421b7f4fd44975ddcc6bb3188cc9c4c7a27559 (patch) | |
| tree | 946ad17b8efd0a4b789cc16ce27b380d1eb6999d | |
| parent | 2f3dd3f706e3f5097195f7bd4100c91458c1bea3 (diff) | |
cleaning up.
61 files changed, 2294 insertions, 0 deletions
diff --git a/product/Presentation/Model/Menu/Help/DisplayInformationAboutTheApplication.cs b/product/Presentation/Model/Menu/Help/DisplayInformationAboutTheApplication.cs new file mode 100644 index 0000000..50f2112 --- /dev/null +++ b/product/Presentation/Model/Menu/Help/DisplayInformationAboutTheApplication.cs @@ -0,0 +1,23 @@ +using gorilla.commons.utility; +using momoney.presentation.presenters; +using MoMoney.Presentation.Presenters; + +namespace MoMoney.Presentation.model.menu.help +{ + public interface IDisplayInformationAboutTheApplication : Command {} + + public class DisplayInformationAboutTheApplication : IDisplayInformationAboutTheApplication + { + public DisplayInformationAboutTheApplication(IRunPresenterCommand run_presenter) + { + this.run_presenter = run_presenter; + } + + public void run() + { + run_presenter.run<IAboutApplicationPresenter>(); + } + + readonly IRunPresenterCommand run_presenter; + } +}
\ No newline at end of file diff --git a/product/Presentation/Model/eventing/ClosingProjectEvent.cs b/product/Presentation/Model/eventing/ClosingProjectEvent.cs new file mode 100644 index 0000000..64545e5 --- /dev/null +++ b/product/Presentation/Model/eventing/ClosingProjectEvent.cs @@ -0,0 +1,8 @@ +using MoMoney.Service.Infrastructure.Eventing; + +namespace momoney.presentation.model.events +{ + public class ClosingProjectEvent : IEvent + { + } +}
\ No newline at end of file diff --git a/product/Presentation/Model/eventing/ClosingTheApplication.cs b/product/Presentation/Model/eventing/ClosingTheApplication.cs new file mode 100644 index 0000000..e342dc7 --- /dev/null +++ b/product/Presentation/Model/eventing/ClosingTheApplication.cs @@ -0,0 +1,8 @@ +using MoMoney.Service.Infrastructure.Eventing; + +namespace momoney.presentation.model.events +{ + public class ClosingTheApplication : IEvent + { + } +}
\ No newline at end of file diff --git a/product/Presentation/Model/eventing/FinishedRunningCommand.cs b/product/Presentation/Model/eventing/FinishedRunningCommand.cs new file mode 100644 index 0000000..f891190 --- /dev/null +++ b/product/Presentation/Model/eventing/FinishedRunningCommand.cs @@ -0,0 +1,14 @@ +using MoMoney.Service.Infrastructure.Eventing; + +namespace momoney.presentation.model.events +{ + public class FinishedRunningCommand : IEvent + { + public FinishedRunningCommand(object running_command) + { + completed_action = running_command; + } + + public object completed_action { get; private set; } + } +}
\ No newline at end of file diff --git a/product/Presentation/Model/eventing/NewProjectOpened.cs b/product/Presentation/Model/eventing/NewProjectOpened.cs new file mode 100644 index 0000000..952cfa6 --- /dev/null +++ b/product/Presentation/Model/eventing/NewProjectOpened.cs @@ -0,0 +1,14 @@ +using MoMoney.Service.Infrastructure.Eventing; + +namespace momoney.presentation.model.events +{ + public class NewProjectOpened : IEvent + { + public NewProjectOpened(string path) + { + this.path = path; + } + + public string path { private set; get; } + } +}
\ No newline at end of file diff --git a/product/Presentation/Model/eventing/SavedChangesEvent.cs b/product/Presentation/Model/eventing/SavedChangesEvent.cs new file mode 100644 index 0000000..89c173c --- /dev/null +++ b/product/Presentation/Model/eventing/SavedChangesEvent.cs @@ -0,0 +1,8 @@ +using MoMoney.Service.Infrastructure.Eventing; + +namespace momoney.presentation.model.events +{ + public class SavedChangesEvent : IEvent + { + } +}
\ No newline at end of file diff --git a/product/Presentation/Model/eventing/StartedRunningCommand.cs b/product/Presentation/Model/eventing/StartedRunningCommand.cs new file mode 100644 index 0000000..86c7ac1 --- /dev/null +++ b/product/Presentation/Model/eventing/StartedRunningCommand.cs @@ -0,0 +1,14 @@ +using MoMoney.Service.Infrastructure.Eventing; + +namespace momoney.presentation.model.events +{ + public class StartedRunningCommand : IEvent + { + public StartedRunningCommand(object running_command) + { + running_action = running_command; + } + + public object running_action { get; private set; } + } +}
\ No newline at end of file diff --git a/product/Presentation/Model/eventing/UnhandledErrorOccurred.cs b/product/Presentation/Model/eventing/UnhandledErrorOccurred.cs new file mode 100644 index 0000000..2b573ae --- /dev/null +++ b/product/Presentation/Model/eventing/UnhandledErrorOccurred.cs @@ -0,0 +1,15 @@ +using System; +using MoMoney.Service.Infrastructure.Eventing; + +namespace momoney.presentation.model.events +{ + public class UnhandledErrorOccurred : IEvent + { + public UnhandledErrorOccurred(Exception error) + { + this.error = error; + } + + public Exception error { get; private set; } + } +}
\ No newline at end of file diff --git a/product/Presentation/Model/eventing/UnsavedChangesEvent.cs b/product/Presentation/Model/eventing/UnsavedChangesEvent.cs new file mode 100644 index 0000000..8883fbc --- /dev/null +++ b/product/Presentation/Model/eventing/UnsavedChangesEvent.cs @@ -0,0 +1,8 @@ +using MoMoney.Service.Infrastructure.Eventing; + +namespace momoney.presentation.model.events +{ + public class UnsavedChangesEvent : IEvent + { + } +}
\ No newline at end of file diff --git a/product/Presentation/Presenters/DisplayTheSplashScreen.cs b/product/Presentation/Presenters/DisplayTheSplashScreen.cs new file mode 100644 index 0000000..67aaf9a --- /dev/null +++ b/product/Presentation/Presenters/DisplayTheSplashScreen.cs @@ -0,0 +1,34 @@ +using System; +using MoMoney.Presentation.Presenters; +using momoney.presentation.views; +using MoMoney.Service.Infrastructure.Threading; + +namespace momoney.presentation.presenters +{ + public class DisplayTheSplashScreen : ISplashScreenState + { + readonly ITimer timer; + readonly ISplashScreenView view; + readonly ISplashScreenPresenter presenter; + + public DisplayTheSplashScreen(ITimer timer, ISplashScreenView view, ISplashScreenPresenter presenter) + { + this.timer = timer; + this.view = view; + this.presenter = presenter; + timer.start_notifying(presenter, new TimeSpan(50)); + } + + public void update() + { + if (view.current_opacity() < 1) + { + view.increment_the_opacity(); + } + else + { + timer.stop_notifying(presenter); + } + } + } +}
\ No newline at end of file diff --git a/product/database/IConnectionFactory.cs b/product/database/IConnectionFactory.cs new file mode 100644 index 0000000..25d37d0 --- /dev/null +++ b/product/database/IConnectionFactory.cs @@ -0,0 +1,10 @@ +using Gorilla.Commons.Infrastructure.FileSystem; +using momoney.database.transactions; + +namespace momoney.database +{ + public interface IConnectionFactory + { + IDatabaseConnection open_connection_to(File the_path_to_the_database_file); + } +}
\ No newline at end of file diff --git a/product/database/IDatabaseConfiguration.cs b/product/database/IDatabaseConfiguration.cs new file mode 100644 index 0000000..92c7393 --- /dev/null +++ b/product/database/IDatabaseConfiguration.cs @@ -0,0 +1,11 @@ +using Gorilla.Commons.Infrastructure.FileSystem; + +namespace momoney.database +{ + public interface IDatabaseConfiguration + { + void open(File file); + void copy_to(string path); + void close(string name); + } +}
\ No newline at end of file diff --git a/product/database/ObjectDatabase.cs b/product/database/ObjectDatabase.cs new file mode 100644 index 0000000..b96ad20 --- /dev/null +++ b/product/database/ObjectDatabase.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Gorilla.Commons.Infrastructure.FileSystem; +using gorilla.commons.utility; +using momoney.database.transactions; +using File = Gorilla.Commons.Infrastructure.FileSystem.File; + +namespace momoney.database +{ + public class ObjectDatabase : IDatabase, IDatabaseConfiguration + { + readonly IConnectionFactory factory; + File path; + + public ObjectDatabase(IConnectionFactory factory) + { + this.factory = factory; + path = new ApplicationFile(Path.GetTempFileName()); + } + + public IEnumerable<T> fetch_all<T>() where T : Identifiable<Guid> + { + using (var connection = factory.open_connection_to(path_to_database())) + { + return connection.query<T>().ToList(); + } + } + + public void apply(IStatement statement) + { + using (var connection = factory.open_connection_to(path_to_database())) + { + statement.prepare(connection); + connection.commit(); + } + } + + public void open(File file) + { + path = new ApplicationFile(Path.GetTempFileName()); + file.copy_to(path.path); + } + + public void copy_to(string new_path) + { + path.copy_to(new_path); + } + + public void close(string name) + { + path.delete(); + path = new ApplicationFile(Path.GetTempFileName()); + } + + File path_to_database() + { + return path; + } + } +}
\ No newline at end of file diff --git a/product/database/database.csproj b/product/database/database.csproj new file mode 100644 index 0000000..df48913 --- /dev/null +++ b/product/database/database.csproj @@ -0,0 +1,152 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="3.5" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> + <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> + <ProductVersion>9.0.30729</ProductVersion> + <SchemaVersion>2.0</SchemaVersion> + <ProjectGuid>{580E68A8-EDEE-4350-8BBE-A053645B0F83}</ProjectGuid> + <OutputType>Library</OutputType> + <AppDesignerFolder>Properties</AppDesignerFolder> + <RootNamespace>momoney.database</RootNamespace> + <AssemblyName>momoney.database</AssemblyName> + <TargetFrameworkVersion>v3.5</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="bdddoc, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> + <HintPath>..\..\build\lib\test\bdd.doc\bdddoc.dll</HintPath> + </Reference> + <Reference Include="Db4objects.Db4o, Version=7.5.57.11498, Culture=neutral, PublicKeyToken=6199cd4f203aa8eb, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> + <HintPath>..\..\build\lib\app\db40\Db4objects.Db4o.dll</HintPath> + </Reference> + <Reference Include="Db4objects.Db4o.Linq, Version=7.5.57.11498, Culture=neutral, PublicKeyToken=6199cd4f203aa8eb, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> + <HintPath>..\..\build\lib\app\db40\Db4objects.Db4o.Linq.dll</HintPath> + </Reference> + <Reference Include="developwithpassion.bdd, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> + <HintPath>..\..\build\lib\test\developwithpassion\developwithpassion.bdd.dll</HintPath> + </Reference> + <Reference Include="gorilla.commons.infrastructure, Version=2009.5.5.1633, Culture=neutral, PublicKeyToken=687787ccb6c36c9f, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> + <HintPath>..\..\build\lib\app\gorilla\gorilla.commons.infrastructure.dll</HintPath> + </Reference> + <Reference Include="gorilla.commons.utility, Version=2009.5.5.1633, Culture=neutral, PublicKeyToken=687787ccb6c36c9f, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> + <HintPath>..\..\build\lib\app\gorilla\gorilla.commons.utility.dll</HintPath> + </Reference> + <Reference Include="gorilla.testing, Version=2009.5.5.194, Culture=neutral, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> + <HintPath>..\..\build\lib\test\gorilla\gorilla.testing.dll</HintPath> + </Reference> + <Reference Include="MbUnit.Framework, Version=2.4.2.175, Culture=neutral, PublicKeyToken=5e72ecd30bc408d5"> + <SpecificVersion>False</SpecificVersion> + <HintPath>..\..\build\lib\test\mbunit\MbUnit.Framework.dll</HintPath> + </Reference> + <Reference Include="Rhino.Mocks, Version=3.5.0.1337, Culture=neutral, PublicKeyToken=0b3305902db7183f, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> + <HintPath>..\..\build\lib\test\rhino.mocks\Rhino.Mocks.dll</HintPath> + </Reference> + <Reference Include="System" /> + <Reference Include="System.Core"> + <RequiredTargetFramework>3.5</RequiredTargetFramework> + </Reference> + <Reference Include="System.Xml.Linq"> + <RequiredTargetFramework>3.5</RequiredTargetFramework> + </Reference> + <Reference Include="System.Data.DataSetExtensions"> + <RequiredTargetFramework>3.5</RequiredTargetFramework> + </Reference> + <Reference Include="System.Data" /> + <Reference Include="System.Xml" /> + </ItemGroup> + <ItemGroup> + <Compile Include="db4o\ConfigureObjectContainerStep.cs" /> + <Compile Include="db4o\Spiking\db40_spike_specs.cs" /> + <Compile Include="repositories\AccountHolderRepository.cs" /> + <Compile Include="repositories\BillRepository.cs" /> + <Compile Include="repositories\CompanyRepository.cs" /> + <Compile Include="repositories\IncomeRepository.cs" /> + <Compile Include="db4o\ConfigureDatabaseStep.cs" /> + <Compile Include="db4o\ConnectionFactory.cs" /> + <Compile Include="ObjectDatabase.cs" /> + <Compile Include="db4o\DatabaseConnection.cs" /> + <Compile Include="IConnectionFactory.cs" /> + <Compile Include="IDatabaseConfiguration.cs" /> + <Compile Include="transactions\ChangeTracker.cs" /> + <Compile Include="transactions\ChangeTrackerFactory.cs" /> + <Compile Include="transactions\ChangeTrackerFactorySpecs.cs" /> + <Compile Include="transactions\ChangeTrackerSpecs.cs" /> + <Compile Include="transactions\Context.cs" /> + <Compile Include="transactions\ContextFactory.cs" /> + <Compile Include="transactions\ContextFactorySpecs.cs" /> + <Compile Include="transactions\CurrentThread.cs" /> + <Compile Include="transactions\IChangeTracker.cs" /> + <Compile Include="transactions\IChangeTrackerFactory.cs" /> + <Compile Include="transactions\IContext.cs" /> + <Compile Include="transactions\IDatabase.cs" /> + <Compile Include="transactions\IDatabaseConnection.cs" /> + <Compile Include="transactions\IdentityMapProxy.cs" /> + <Compile Include="transactions\IdentityMapSpecs.cs" /> + <Compile Include="transactions\IIdentityMap.cs" /> + <Compile Include="transactions\IKey.cs" /> + <Compile Include="transactions\IScopedStorage.cs" /> + <Compile Include="transactions\IStatement.cs" /> + <Compile Include="transactions\IStatementRegistry.cs" /> + <Compile Include="transactions\IThread.cs" /> + <Compile Include="transactions\PerThread.cs" /> + <Compile Include="transactions\PerThreadScopedStorage.cs" /> + <Compile Include="transactions\PerThreadScopedStorageSpecs.cs" /> + <Compile Include="transactions\Session.cs" /> + <Compile Include="transactions\SessionFactory.cs" /> + <Compile Include="transactions\SessionFactorySpecs.cs" /> + <Compile Include="transactions\SessionNotStartedException.cs" /> + <Compile Include="transactions\SessionProvider.cs" /> + <Compile Include="transactions\SessionSpecs.cs" /> + <Compile Include="transactions\SingletonScopedStorage.cs" /> + <Compile Include="transactions\StatementRegistry.cs" /> + <Compile Include="transactions\TrackerEntry.cs" /> + <Compile Include="transactions\TrackerEntryMapper.cs" /> + <Compile Include="transactions\TrackerEntrySpecs.cs" /> + <Compile Include="transactions\Transaction.cs" /> + <Compile Include="transactions\TransactionSpecs.cs" /> + <Compile Include="transactions\TypedKey.cs" /> + </ItemGroup> + <ItemGroup> + <ProjectReference Include="..\Domain\domain.csproj"> + <Project>{BE790BCC-4412-473F-9D0A-5AA48FE7A74F}</Project> + <Name>domain</Name> + </ProjectReference> + </ItemGroup> + <ItemGroup> + <Folder Include="Properties\" /> + </ItemGroup> + <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> + <!-- To modify your build process, add your task inside one of the targets below and uncomment it. + Other similar extension points exist, see Microsoft.Common.targets. + <Target Name="BeforeBuild"> + </Target> + <Target Name="AfterBuild"> + </Target> + --> +</Project>
\ No newline at end of file diff --git a/product/database/db4o/ConfigureDatabaseStep.cs b/product/database/db4o/ConfigureDatabaseStep.cs new file mode 100644 index 0000000..b6e59d7 --- /dev/null +++ b/product/database/db4o/ConfigureDatabaseStep.cs @@ -0,0 +1,18 @@ +using Db4objects.Db4o.Config; +using gorilla.commons.utility; + +namespace momoney.database.db4o +{ + public interface IConfigureDatabaseStep : Configuration<IConfiguration> {} + + public class ConfigureDatabaseStep : IConfigureDatabaseStep + { + public void configure(IConfiguration item) + { + item.LockDatabaseFile(false); + //item.UpdateDepth(10); + //item.WeakReferences(true); + //item.AutomaticShutDown(true); + } + } +}
\ No newline at end of file diff --git a/product/database/db4o/ConfigureObjectContainerStep.cs b/product/database/db4o/ConfigureObjectContainerStep.cs new file mode 100644 index 0000000..4e5feca --- /dev/null +++ b/product/database/db4o/ConfigureObjectContainerStep.cs @@ -0,0 +1,20 @@ +using Db4objects.Db4o; +using Db4objects.Db4o.Events; +using Gorilla.Commons.Infrastructure.Logging; +using gorilla.commons.utility; + +namespace momoney.database.db4o +{ + public interface IConfigureObjectContainerStep : Configuration<IObjectContainer> {} + + public class ConfigureObjectContainerStep : IConfigureObjectContainerStep + { + public void configure(IObjectContainer item) + { + var registry = EventRegistryFactory.ForObjectContainer(item); + registry.ClassRegistered += (sender, args) => this.log().debug("class registered: {0}", args.ClassMetadata()); + registry.Instantiated += (sender, args) => this.log().debug("class instantiated: {0}", args.Object.GetType().Name); + registry.Committed += (sender, args) => this.log().debug("added: {0}, updated: {1}, deleted: {2}", args.Added, args.Updated, args.Deleted); + } + } +}
\ No newline at end of file diff --git a/product/database/db4o/ConnectionFactory.cs b/product/database/db4o/ConnectionFactory.cs new file mode 100644 index 0000000..e18895f --- /dev/null +++ b/product/database/db4o/ConnectionFactory.cs @@ -0,0 +1,34 @@ +using Db4objects.Db4o; +using Db4objects.Db4o.Config; +using Gorilla.Commons.Infrastructure.FileSystem; +using gorilla.commons.utility; +using momoney.database.transactions; + +namespace momoney.database.db4o +{ + public class ConnectionFactory : IConnectionFactory + { + readonly IConfigureDatabaseStep setup; + readonly IConfigureObjectContainerStep setup_container; + + public ConnectionFactory(IConfigureDatabaseStep setup, IConfigureObjectContainerStep setup_container) + { + this.setup = setup; + this.setup_container = setup_container; + } + + public IDatabaseConnection open_connection_to(File the_path_to_the_database_file) + { + var configuration = Db4oFactory.NewConfiguration(); + setup.configure(configuration); + return new DatabaseConnection(get_container(the_path_to_the_database_file, configuration)); + } + + IObjectContainer get_container(File the_path_to_the_database_file, IConfiguration configuration) + { + return Db4oFactory + .OpenFile(configuration, the_path_to_the_database_file.path) + .and_configure_with(setup_container); + } + } +}
\ No newline at end of file diff --git a/product/database/db4o/DatabaseConnection.cs b/product/database/db4o/DatabaseConnection.cs new file mode 100644 index 0000000..9e8e57a --- /dev/null +++ b/product/database/db4o/DatabaseConnection.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using Db4objects.Db4o; +using momoney.database.transactions; + +namespace momoney.database.db4o +{ + public class DatabaseConnection : IDatabaseConnection + { + readonly IObjectContainer container; + + public DatabaseConnection(IObjectContainer container) + { + this.container = container; + } + + public void Dispose() + { + container.Close(); + container.Dispose(); + } + + public IEnumerable<T> query<T>() + { + return container.Query<T>(); + } + + public IEnumerable<T> query<T>(Predicate<T> predicate) + { + return container.Query(predicate); + } + + public void delete<T>(T entity) + { + container.Delete(entity); + } + + public void commit() + { + container.Commit(); + } + + public void store<T>(T entity) + { + container.Store(entity); + } + } +}
\ No newline at end of file diff --git a/product/database/db4o/Spiking/db40_spike_specs.cs b/product/database/db4o/Spiking/db40_spike_specs.cs new file mode 100644 index 0000000..a5dbae3 --- /dev/null +++ b/product/database/db4o/Spiking/db40_spike_specs.cs @@ -0,0 +1,91 @@ +using System.Collections.Generic; +using System.IO; +using Db4objects.Db4o; +using developwithpassion.bdd.contexts; +using Gorilla.Commons.Testing; +using gorilla.commons.utility; + +namespace momoney.database.db4o.Spiking +{ + [Concern(typeof (Db4oFactory))] + public class when_opening_an_existing_database_ : concerns + { + before_each_observation be = () => {}; + + context c = () => + { + original = new TestObject(88, "mo"); + the_database_file = Path.GetTempFileName(); + var configuration = Db4oFactory.NewConfiguration(); + configuration.LockDatabaseFile(false); + database = Db4oFactory.OpenFile(configuration, the_database_file); + }; + + because b = () => + { + database.Store(original); + database.Close(); + database.Dispose(); + database = Db4oFactory.OpenFile(the_database_file); + results = database.Query<ITestObject>().databind(); + }; + + it should_be_able_to_load_the_original_contents = () => results.should_contain(original); + + it they_should_be_equal = () => new TestObject(99, "gretzky").Equals(new TestObject(99, "gretzky")); + + it should_only_contain_the_original_item = () => results.Count.should_be_equal_to(1); + + after_each_observation ae = () => + { + database.Close(); + database.Dispose(); + }; + + static ITestObject original; + static string the_database_file; + static IList<ITestObject> results; + static IObjectContainer database; + } + + public interface ITestObject + { + int Id { get; } + string Name { get; } + } + + public class TestObject : ITestObject + { + public TestObject(int id, string name) + { + Id = id; + Name = name; + } + + public int Id { get; private set; } + public string Name { get; private set; } + + public bool Equals(TestObject obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj.Id == Id && Equals(obj.Name, Name); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != typeof (TestObject)) return false; + return Equals((TestObject) obj); + } + + public override int GetHashCode() + { + unchecked + { + return (Id*397) ^ (Name != null ? Name.GetHashCode() : 0); + } + } + } +}
\ No newline at end of file diff --git a/product/database/repositories/AccountHolderRepository.cs b/product/database/repositories/AccountHolderRepository.cs new file mode 100644 index 0000000..7d3658e --- /dev/null +++ b/product/database/repositories/AccountHolderRepository.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using momoney.database.transactions; +using MoMoney.Domain.accounting; +using MoMoney.Domain.repositories; + +namespace momoney.database.repositories +{ + public class AccountHolderRepository : IAccountHolderRepository + { + readonly ISession session; + + public AccountHolderRepository(ISession session) + { + this.session = session; + } + + public IEnumerable<IAccountHolder> all() + { + return session.all<IAccountHolder>(); + } + + public void save(IAccountHolder account_holder) + { + session.save(account_holder); + } + } +}
\ No newline at end of file diff --git a/product/database/repositories/BillRepository.cs b/product/database/repositories/BillRepository.cs new file mode 100644 index 0000000..1ed1b8a --- /dev/null +++ b/product/database/repositories/BillRepository.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using momoney.database.transactions; +using MoMoney.Domain.Accounting; +using MoMoney.Domain.repositories; + +namespace momoney.database.repositories +{ + public class BillRepository : IBillRepository + { + readonly ISession session; + + public BillRepository(ISession session) + { + this.session = session; + } + + public IEnumerable<IBill> all() + { + return session.all<IBill>(); + } + } +}
\ No newline at end of file diff --git a/product/database/repositories/CompanyRepository.cs b/product/database/repositories/CompanyRepository.cs new file mode 100644 index 0000000..b009cc7 --- /dev/null +++ b/product/database/repositories/CompanyRepository.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using gorilla.commons.utility; +using momoney.database.transactions; +using MoMoney.Domain.Accounting; +using MoMoney.Domain.repositories; + +namespace momoney.database.repositories +{ + public class CompanyRepository : ICompanyRepository + { + readonly ISession session; + + public CompanyRepository(ISession session) + { + this.session = session; + } + + public IEnumerable<ICompany> all() + { + return session.all<ICompany>(); + } + + public ICompany find_company_named(string name) + { + return session + .all<ICompany>() + .SingleOrDefault(x => x.name.is_equal_to_ignoring_case(name)); + } + + public ICompany find_company_by(Guid id) + { + return session.all<ICompany>().SingleOrDefault(x => x.id.Equals(id)); + } + + public void save(ICompany company) + { + session.save(company); + } + } +}
\ No newline at end of file diff --git a/product/database/repositories/IncomeRepository.cs b/product/database/repositories/IncomeRepository.cs new file mode 100644 index 0000000..bf0c055 --- /dev/null +++ b/product/database/repositories/IncomeRepository.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using momoney.database.transactions; +using MoMoney.Domain.Accounting; +using MoMoney.Domain.repositories; + +namespace momoney.database.repositories +{ + public class IncomeRepository : IIncomeRepository + { + readonly ISession session; + + public IncomeRepository(ISession session) + { + this.session = session; + } + + public IEnumerable<IIncome> all() + { + return session.all<IIncome>(); + } + } +}
\ No newline at end of file diff --git a/product/database/transactions/ChangeTracker.cs b/product/database/transactions/ChangeTracker.cs new file mode 100644 index 0000000..4bfd41a --- /dev/null +++ b/product/database/transactions/ChangeTracker.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using gorilla.commons.utility; + +namespace momoney.database.transactions +{ + public class ChangeTracker<T> : IChangeTracker<T> where T : Identifiable<Guid> + { + readonly ITrackerEntryMapper<T> mapper; + readonly IStatementRegistry registry; + readonly IList<ITrackerEntry<T>> items; + readonly IList<T> to_be_deleted; + + public ChangeTracker(ITrackerEntryMapper<T> mapper, IStatementRegistry registry) + { + this.mapper = mapper; + this.registry = registry; + items = new List<ITrackerEntry<T>>(); + to_be_deleted = new List<T>(); + } + + public void register(T entity) + { + items.Add(mapper.map_from(entity)); + } + + public void delete(T entity) + { + to_be_deleted.Add(entity); + } + + public void commit_to(IDatabase database) + { + items.each(x => commit(x, database)); + to_be_deleted.each(x => database.apply(registry.prepare_command_for(x))); + } + + public bool is_dirty() + { + return items.Count(x => x.has_changes()) > 0 || to_be_deleted.Count > 0; + } + + public void Dispose() + { + items.Clear(); + } + + void commit(ITrackerEntry<T> entry, IDatabase database) + { + if (entry.has_changes()) database.apply(registry.prepare_command_for(entry.current)); + } + } +}
\ No newline at end of file diff --git a/product/database/transactions/ChangeTrackerFactory.cs b/product/database/transactions/ChangeTrackerFactory.cs new file mode 100644 index 0000000..a346cf8 --- /dev/null +++ b/product/database/transactions/ChangeTrackerFactory.cs @@ -0,0 +1,23 @@ +using System; +using Gorilla.Commons.Infrastructure.Container; +using gorilla.commons.utility; + +namespace momoney.database.transactions +{ + public class ChangeTrackerFactory : IChangeTrackerFactory + { + readonly IStatementRegistry statement_registry; + readonly DependencyRegistry registry; + + public ChangeTrackerFactory(IStatementRegistry statement_registry, DependencyRegistry registry) + { + this.statement_registry = statement_registry; + this.registry = registry; + } + + public IChangeTracker<T> create_for<T>() where T : Identifiable<Guid> + { + return new ChangeTracker<T>(registry.get_a<ITrackerEntryMapper<T>>(), statement_registry); + } + } +}
\ No newline at end of file diff --git a/product/database/transactions/ChangeTrackerFactorySpecs.cs b/product/database/transactions/ChangeTrackerFactorySpecs.cs new file mode 100644 index 0000000..45f511e --- /dev/null +++ b/product/database/transactions/ChangeTrackerFactorySpecs.cs @@ -0,0 +1,22 @@ +using System; +using developwithpassion.bdd.contexts; +using Gorilla.Commons.Testing; +using gorilla.commons.utility; + +namespace momoney.database.transactions +{ + public class ChangeTrackerFactorySpecs {} + + [Concern(typeof (ChangeTrackerFactory))] + public class when_creating_a_change_tracker_for_an_item : concerns_for<IChangeTrackerFactory, ChangeTrackerFactory> + { + it should_return_a_new_tracker = () => result.should_not_be_null(); + + because b = () => + { + result = sut.create_for<Identifiable<Guid>>(); + }; + + static IChangeTracker<Identifiable<Guid>> result; + } +}
\ No newline at end of file diff --git a/product/database/transactions/ChangeTrackerSpecs.cs b/product/database/transactions/ChangeTrackerSpecs.cs new file mode 100644 index 0000000..ef0cbb7 --- /dev/null +++ b/product/database/transactions/ChangeTrackerSpecs.cs @@ -0,0 +1,103 @@ +using System; +using developwithpassion.bdd.contexts; +using Gorilla.Commons.Testing; +using gorilla.commons.utility; + +namespace momoney.database.transactions +{ + public class ChangeTrackerSpecs + { + + [Concern(typeof (ChangeTracker<Identifiable<Guid>>))] + public abstract class behaves_like_change_tracker : + concerns_for<IChangeTracker<Identifiable<Guid>>, ChangeTracker<Identifiable<Guid>>> + { + context c = () => + { + mapper = the_dependency<ITrackerEntryMapper<Identifiable<Guid>>>(); + registry = the_dependency<IStatementRegistry>(); + }; + + static protected ITrackerEntryMapper<Identifiable<Guid>> mapper; + static protected IStatementRegistry registry; + } + + [Concern(typeof (ChangeTracker<Identifiable<Guid>>))] + public class when_commit_that_changes_made_to_an_item : behaves_like_change_tracker + { + it should_save_the_changes_to_the_database = () => database.was_told_to(x => x.apply(statement)); + + context c = () => + { + item = an<Identifiable<Guid>>(); + statement = an<IStatement>(); + database = an<IDatabase>(); + var entry = an<ITrackerEntry<Identifiable<Guid>>>(); + + when_the(mapper).is_told_to(x => x.map_from(item)).it_will_return(entry); + when_the(entry).is_told_to(x => x.has_changes()).it_will_return(true); + when_the(entry).is_told_to(x => x.current).it_will_return(item); + when_the(registry).is_told_to(x => x.prepare_command_for(item)).it_will_return(statement); + }; + + because b = () => + { + sut.register(item); + sut.commit_to(database); + }; + + static Identifiable<Guid> item; + static IDatabase database; + static IStatement statement; + } + + [Concern(typeof (ChangeTracker<Identifiable<Guid>>))] + public class when_checking_if_there_are_changes_and_there_are : behaves_like_change_tracker + { + it should_tell_the_truth = () => result.should_be_true(); + + context c = () => + { + item = an<Identifiable<Guid>>(); + var registration = an<ITrackerEntry<Identifiable<Guid>>>(); + + when_the(mapper).is_told_to(x => x.map_from(item)).it_will_return(registration); + when_the(registration).is_told_to(x => x.has_changes()).it_will_return(true); + when_the(registration).is_told_to(x => x.current).it_will_return(item); + }; + + because b = () => + { + sut.register(item); + result = sut.is_dirty(); + }; + + static bool result; + static Identifiable<Guid> item; + } + + [Concern(typeof (ChangeTracker<Identifiable<Guid>>))] + public class when_checking_if_there_are_changes_and_there_are_not : behaves_like_change_tracker + { + it should_tell_the_truth = () => result.should_be_false(); + + context c = () => + { + item = an<Identifiable<Guid>>(); + var entry = an<ITrackerEntry<Identifiable<Guid>>>(); + + when_the(mapper).is_told_to(x => x.map_from(item)).it_will_return(entry); + when_the(entry).is_told_to(x => x.has_changes()).it_will_return(false); + }; + + because b = () => + { + sut.register(item); + result = sut.is_dirty(); + }; + + static bool result; + static Identifiable<Guid> item; + } + } +}
\ No newline at end of file diff --git a/product/database/transactions/Context.cs b/product/database/transactions/Context.cs new file mode 100644 index 0000000..26aab7f --- /dev/null +++ b/product/database/transactions/Context.cs @@ -0,0 +1,34 @@ +using System.Collections; + +namespace momoney.database.transactions +{ + public class Context : IContext + { + readonly IDictionary items; + + public Context(IDictionary items) + { + this.items = items; + } + + public bool contains<T>(IKey<T> key) + { + return key.is_found_in(items); + } + + public void add<T>(IKey<T> key, T value) + { + key.add_value_to(items, value); + } + + public T value_for<T>(IKey<T> key) + { + return key.parse_from(items); + } + + public void remove<T>(IKey<T> key) + { + key.remove_from(items); + } + } +}
\ No newline at end of file diff --git a/product/database/transactions/ContextFactory.cs b/product/database/transactions/ContextFactory.cs new file mode 100644 index 0000000..a1c35a9 --- /dev/null +++ b/product/database/transactions/ContextFactory.cs @@ -0,0 +1,15 @@ +namespace momoney.database.transactions +{ + public interface IContextFactory + { + IContext create_for(IScopedStorage storage); + } + + public class ContextFactory : IContextFactory + { + public IContext create_for(IScopedStorage storage) + { + return new Context(storage.provide_storage()); + } + } +}
\ No newline at end of file diff --git a/product/database/transactions/ContextFactorySpecs.cs b/product/database/transactions/ContextFactorySpecs.cs new file mode 100644 index 0000000..2e807af --- /dev/null +++ b/product/database/transactions/ContextFactorySpecs.cs @@ -0,0 +1,31 @@ +using System.Collections; +using developwithpassion.bdd.contexts; +using Gorilla.Commons.Testing; + +namespace momoney.database.transactions +{ + public class ContextFactorySpecs + { + } + + [Concern(typeof (ContextFactory))] + public class when_creating_a_new_context : concerns_for<IContextFactory, ContextFactory> + { + context c = () => + { + scope = an<IScopedStorage>(); + storage = an<IDictionary>(); + + when_the(scope).is_told_to(x => x.provide_storage()).it_will_return(storage); + }; + + because b = () => { result = sut.create_for(scope); }; + + it should_return_a_context_that_represents_the_specified_scope = + () => result.should_be_an_instance_of<Context>(); + + static IDictionary storage; + static IScopedStorage scope; + static IContext result; + } +}
\ No newline at end of file diff --git a/product/database/transactions/CurrentThread.cs b/product/database/transactions/CurrentThread.cs new file mode 100644 index 0000000..00be2fa --- /dev/null +++ b/product/database/transactions/CurrentThread.cs @@ -0,0 +1,19 @@ +using System.Threading; + +namespace momoney.database.transactions +{ + public class CurrentThread : IThread + { + public T provide_slot_for<T>() where T : class, new() + { + var slot = Thread.GetNamedDataSlot(create_key_for<T>()); + if (null == Thread.GetData(slot)) Thread.SetData(slot, new T()); + return (T) Thread.GetData(slot); + } + + string create_key_for<T>() + { + return Thread.CurrentThread.ManagedThreadId + GetType().FullName + typeof (T).FullName; + } + } +}
\ No newline at end of file diff --git a/product/database/transactions/IChangeTracker.cs b/product/database/transactions/IChangeTracker.cs new file mode 100644 index 0000000..14f2e82 --- /dev/null +++ b/product/database/transactions/IChangeTracker.cs @@ -0,0 +1,17 @@ +using System; +using gorilla.commons.utility; + +namespace momoney.database.transactions +{ + public interface IChangeTracker : IDisposable + { + bool is_dirty(); + void commit_to(IDatabase database); + } + + public interface IChangeTracker<T> : IChangeTracker where T : Identifiable<Guid> + { + void register(T value); + void delete(T entity); + } +}
\ No newline at end of file diff --git a/product/database/transactions/IChangeTrackerFactory.cs b/product/database/transactions/IChangeTrackerFactory.cs new file mode 100644 index 0000000..4102d8d --- /dev/null +++ b/product/database/transactions/IChangeTrackerFactory.cs @@ -0,0 +1,10 @@ +using System; +using gorilla.commons.utility; + +namespace momoney.database.transactions +{ + public interface IChangeTrackerFactory + { + IChangeTracker<T> create_for<T>() where T : Identifiable<Guid>; + } +}
\ No newline at end of file diff --git a/product/database/transactions/IContext.cs b/product/database/transactions/IContext.cs new file mode 100644 index 0000000..593f704 --- /dev/null +++ b/product/database/transactions/IContext.cs @@ -0,0 +1,10 @@ +namespace momoney.database.transactions +{ + public interface IContext + { + bool contains<T>(IKey<T> key); + void add<T>(IKey<T> key, T value); + T value_for<T>(IKey<T> key); + void remove<T>(IKey<T> key); + } +}
\ No newline at end of file diff --git a/product/database/transactions/IDatabase.cs b/product/database/transactions/IDatabase.cs new file mode 100644 index 0000000..433f52a --- /dev/null +++ b/product/database/transactions/IDatabase.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using gorilla.commons.utility; + +namespace momoney.database.transactions +{ + public interface IDatabase + { + IEnumerable<T> fetch_all<T>() where T : Identifiable<Guid>; + void apply(IStatement statement); + } +}
\ No newline at end of file diff --git a/product/database/transactions/IDatabaseConnection.cs b/product/database/transactions/IDatabaseConnection.cs new file mode 100644 index 0000000..98f79f4 --- /dev/null +++ b/product/database/transactions/IDatabaseConnection.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; + +namespace momoney.database.transactions +{ + public interface IDatabaseConnection : IDisposable + { + IEnumerable<T> query<T>(); + IEnumerable<T> query<T>(Predicate<T> predicate); + void delete<T>(T entity); + void commit(); + void store<T>(T entity); + } +}
\ No newline at end of file diff --git a/product/database/transactions/IIdentityMap.cs b/product/database/transactions/IIdentityMap.cs new file mode 100644 index 0000000..e2f25a0 --- /dev/null +++ b/product/database/transactions/IIdentityMap.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; + +namespace momoney.database.transactions +{ + public interface IIdentityMap<TKey, TValue> + { + IEnumerable<TValue> all(); + void add(TKey key, TValue value); + void update_the_item_for(TKey key, TValue new_value); + bool contains_an_item_for(TKey key); + TValue item_that_belongs_to(TKey key); + void evict(TKey key); + } + + public class IdentityMap<TKey, TValue> : IIdentityMap<TKey, TValue> + { + readonly IDictionary<TKey, TValue> items_in_map; + + public IdentityMap() : this(new Dictionary<TKey, TValue>()) + { + } + + public IdentityMap(IDictionary<TKey, TValue> items_in_map) + { + this.items_in_map = items_in_map; + } + + public IEnumerable<TValue> all() + { + return items_in_map.Values; + } + + public void add(TKey key, TValue value) + { + items_in_map.Add(key, value); + } + + public void update_the_item_for(TKey key, TValue new_value) + { + if (contains_an_item_for(key)) items_in_map[key] = new_value; + else add(key, new_value); + } + + public bool contains_an_item_for(TKey key) + { + return items_in_map.ContainsKey(key); + } + + public TValue item_that_belongs_to(TKey key) + { + return contains_an_item_for(key) ? items_in_map[key] : default(TValue); + } + + public void evict(TKey key) + { + if (contains_an_item_for(key)) items_in_map.Remove(key); + } + } +}
\ No newline at end of file diff --git a/product/database/transactions/IKey.cs b/product/database/transactions/IKey.cs new file mode 100644 index 0000000..dce68cf --- /dev/null +++ b/product/database/transactions/IKey.cs @@ -0,0 +1,12 @@ +using System.Collections; + +namespace momoney.database.transactions +{ + public interface IKey<T> + { + bool is_found_in(IDictionary items); + T parse_from(IDictionary items); + void remove_from(IDictionary items); + void add_value_to(IDictionary items, T value); + } +}
\ No newline at end of file diff --git a/product/database/transactions/IScopedStorage.cs b/product/database/transactions/IScopedStorage.cs new file mode 100644 index 0000000..4f1340f --- /dev/null +++ b/product/database/transactions/IScopedStorage.cs @@ -0,0 +1,9 @@ +using System.Collections; + +namespace momoney.database.transactions +{ + public interface IScopedStorage + { + IDictionary provide_storage(); + } +}
\ No newline at end of file diff --git a/product/database/transactions/IStatement.cs b/product/database/transactions/IStatement.cs new file mode 100644 index 0000000..ea2092f --- /dev/null +++ b/product/database/transactions/IStatement.cs @@ -0,0 +1,7 @@ +namespace momoney.database.transactions +{ + public interface IStatement + { + void prepare(IDatabaseConnection connection); + } +}
\ No newline at end of file diff --git a/product/database/transactions/IStatementRegistry.cs b/product/database/transactions/IStatementRegistry.cs new file mode 100644 index 0000000..e2b3b16 --- /dev/null +++ b/product/database/transactions/IStatementRegistry.cs @@ -0,0 +1,11 @@ +using System; +using gorilla.commons.utility; + +namespace momoney.database.transactions +{ + public interface IStatementRegistry + { + IStatement prepare_delete_statement_for<T>(T entity) where T : Identifiable<Guid>; + IStatement prepare_command_for<T>(T entity) where T : Identifiable<Guid>; + } +}
\ No newline at end of file diff --git a/product/database/transactions/IThread.cs b/product/database/transactions/IThread.cs new file mode 100644 index 0000000..7ac0edf --- /dev/null +++ b/product/database/transactions/IThread.cs @@ -0,0 +1,7 @@ +namespace momoney.database.transactions +{ + public interface IThread + { + T provide_slot_for<T>() where T : class, new(); + } +}
\ No newline at end of file diff --git a/product/database/transactions/IdentityMapProxy.cs b/product/database/transactions/IdentityMapProxy.cs new file mode 100644 index 0000000..a251d10 --- /dev/null +++ b/product/database/transactions/IdentityMapProxy.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using gorilla.commons.utility; + +namespace momoney.database.transactions +{ + public class IdentityMapProxy<Key, Value> : IIdentityMap<Key, Value> where Value : Identifiable<Guid> + { + readonly IIdentityMap<Key, Value> real_map; + readonly IChangeTracker<Value> change_tracker; + + public IdentityMapProxy(IChangeTracker<Value> change_tracker, IIdentityMap<Key, Value> real_map) + { + this.change_tracker = change_tracker; + this.real_map = real_map; + } + + public IEnumerable<Value> all() + { + return real_map.all(); + } + + public void add(Key key, Value value) + { + change_tracker.register(value); + real_map.add(key, value); + } + + public void update_the_item_for(Key key, Value new_value) + { + real_map.update_the_item_for(key, new_value); + } + + public bool contains_an_item_for(Key key) + { + return real_map.contains_an_item_for(key); + } + + public Value item_that_belongs_to(Key key) + { + return real_map.item_that_belongs_to(key); + } + + public void evict(Key key) + { + change_tracker.delete(real_map.item_that_belongs_to(key)); + real_map.evict(key); + } + } +}
\ No newline at end of file diff --git a/product/database/transactions/IdentityMapSpecs.cs b/product/database/transactions/IdentityMapSpecs.cs new file mode 100644 index 0000000..e7afea8 --- /dev/null +++ b/product/database/transactions/IdentityMapSpecs.cs @@ -0,0 +1,95 @@ +using developwithpassion.bdd.contexts; +using Gorilla.Commons.Testing; + +namespace momoney.database.transactions +{ + [Concern(typeof (IdentityMap<,>))] + public class behaves_like_identity_map : concerns_for<IIdentityMap<int, string>, IdentityMap<int, string>> + { + public override IIdentityMap<int, string> create_sut() + { + return new IdentityMap<int, string>(); + } + } + + [Concern(typeof (IdentityMap<,>))] + public class when_getting_an_item_from_the_identity_map_for_an_item_that_has_been_added : behaves_like_identity_map + { + it should_return_the_item_that_was_added_for_the_given_key = () => result.should_be_equal_to("1"); + + because b = () => + { + sut.add(1, "1"); + result = sut.item_that_belongs_to(1); + }; + + static string result; + } + + [Concern(typeof (IdentityMap<,>))] + public class when_getting_an_item_from_the_identity_map_that_has_not_been_added : behaves_like_identity_map + { + it should_return_the_default_value_for_that_type = () => result.should_be_equal_to(null); + + because b = () => { result = sut.item_that_belongs_to(2); }; + + static string result; + } + + [Concern(typeof (IdentityMap<,>))] + public class when_checking_if_an_item_has_been_added_to_the_identity_map_that_has_been_added : + behaves_like_identity_map + { + it should_return_true = () => result.should_be_true(); + + because b = () => + { + sut.add(10, "10"); + result = sut.contains_an_item_for(10); + }; + + static bool result; + } + + [Concern(typeof (IdentityMap<,>))] + public class when_checking_if_an_item_has_been_added_to_the_identity_map_that_has_not_been_added : + behaves_like_identity_map + { + it should_return_false = () => result.should_be_false(); + + because b = () => { result = sut.contains_an_item_for(9); }; + + static bool result; + } + + [Concern(typeof (IdentityMap<,>))] + public class when_updating_the_value_for_a_key_that_has_already_been_added_to_the_identity_map : + behaves_like_identity_map + { + it should_replace_the_old_item_with_the_new_one = () => result.should_be_equal_to("7"); + + because b = () => + { + sut.add(6, "6"); + sut.update_the_item_for(6, "7"); + result = sut.item_that_belongs_to(6); + }; + + static string result; + } + + [Concern(typeof (IdentityMap<,>))] + public class when_updating_the_value_for_a_key_that_has_not_been_added_to_the_identity_map : + behaves_like_identity_map + { + it should_add_the_new_item = () => result.should_be_equal_to("3"); + + because b = () => + { + sut.update_the_item_for(3, "3"); + result = sut.item_that_belongs_to(3); + }; + + static string result; + } +}
\ No newline at end of file diff --git a/product/database/transactions/PerThread.cs b/product/database/transactions/PerThread.cs new file mode 100644 index 0000000..6e5c800 --- /dev/null +++ b/product/database/transactions/PerThread.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading; + +namespace momoney.database.transactions +{ + public class PerThread : IContext + { + readonly IDictionary<int, LocalDataStoreSlot> slots; + readonly object mutex = new object(); + + public PerThread() + { + slots = new Dictionary<int, LocalDataStoreSlot>(); + } + + public bool contains<T>(IKey<T> key) + { + return key.is_found_in(get_items()); + } + + public void add<T>(IKey<T> key, T value) + { + key.add_value_to(get_items(), value); + } + + public T value_for<T>(IKey<T> key) + { + return key.parse_from(get_items()); + } + + public void remove<T>(IKey<T> key) + { + key.remove_from(get_items()); + } + + IDictionary get_items() + { + var id = Thread.CurrentThread.ManagedThreadId; + within_lock(() => + { + if (!slots.ContainsKey(id)) + { + var slot = Thread.GetNamedDataSlot(GetType().FullName); + slots.Add(id, slot); + Thread.SetData(slot, new Hashtable()); + } + }); + return (IDictionary) Thread.GetData(slots[id]); + } + + void within_lock(Action action) + { + lock (mutex) action(); + } + } +}
\ No newline at end of file diff --git a/product/database/transactions/PerThreadScopedStorage.cs b/product/database/transactions/PerThreadScopedStorage.cs new file mode 100644 index 0000000..9d32462 --- /dev/null +++ b/product/database/transactions/PerThreadScopedStorage.cs @@ -0,0 +1,19 @@ +using System.Collections; + +namespace momoney.database.transactions +{ + public class PerThreadScopedStorage : IScopedStorage + { + readonly IThread current_thread; + + public PerThreadScopedStorage(IThread current_thread) + { + this.current_thread = current_thread; + } + + public IDictionary provide_storage() + { + return current_thread.provide_slot_for<Hashtable>(); + } + } +}
\ No newline at end of file diff --git a/product/database/transactions/PerThreadScopedStorageSpecs.cs b/product/database/transactions/PerThreadScopedStorageSpecs.cs new file mode 100644 index 0000000..2f072a3 --- /dev/null +++ b/product/database/transactions/PerThreadScopedStorageSpecs.cs @@ -0,0 +1,34 @@ +using System.Collections; +using developwithpassion.bdd.contexts; +using Gorilla.Commons.Testing; + +namespace momoney.database.transactions +{ + public class PerThreadScopedStorageSpecs + { + [Concern(typeof (PerThreadScopedStorage))] + public class when_retrieving_the_storage_for_a_specific_thread : + concerns_for<IScopedStorage, PerThreadScopedStorage> + { + context c = () => + { + thread = the_dependency<IThread>(); + storage = new Hashtable(); + when_the(thread) + .is_told_to(x => x.provide_slot_for<Hashtable>()) + .it_will_return(storage); + }; + + because b = () => + { + result = sut.provide_storage(); + }; + + it should_return_the_storage_the_corresponds_to_the_current_thread = () => result.should_be_equal_to(storage); + + static IDictionary result; + static IThread thread; + static Hashtable storage; + } + } +}
\ No newline at end of file diff --git a/product/database/transactions/Session.cs b/product/database/transactions/Session.cs new file mode 100644 index 0000000..1ccbd6f --- /dev/null +++ b/product/database/transactions/Session.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Gorilla.Commons.Infrastructure.Logging; +using gorilla.commons.utility; + +namespace momoney.database.transactions +{ + public interface ISession : IDisposable + { + T find<T>(Guid guid) where T : Identifiable<Guid>; + IEnumerable<T> all<T>() where T : Identifiable<Guid>; + void save<T>(T entity) where T : Identifiable<Guid>; + void delete<T>(T entity) where T : Identifiable<Guid>; + void flush(); + bool is_dirty(); + } + + public class Session : ISession + { + ITransaction transaction; + readonly IDatabase database; + readonly IDictionary<Type, object> identity_maps; + long id; + + public Session(ITransaction transaction, IDatabase database) + { + this.database = database; + this.transaction = transaction; + identity_maps = new Dictionary<Type, object>(); + id = DateTime.Now.Ticks; + } + + public T find<T>(Guid id) where T : Identifiable<Guid> + { + if (get_identity_map_for<T>().contains_an_item_for(id)) + { + return get_identity_map_for<T>().item_that_belongs_to(id); + } + + var entity = database.fetch_all<T>().Single(x => x.id.Equals(id)); + get_identity_map_for<T>().add(id, entity); + return entity; + } + + public IEnumerable<T> all<T>() where T : Identifiable<Guid> + { + database + .fetch_all<T>() + .where(x => !get_identity_map_for<T>().contains_an_item_for(x.id)) + .each(x => get_identity_map_for<T>().add(x.id, x)); + return get_identity_map_for<T>().all(); + } + + public void save<T>(T entity) where T : Identifiable<Guid> + { + this.log().debug("saving {0}: {1}", id, entity); + get_identity_map_for<T>().add(entity.id, entity); + } + + public void delete<T>(T entity) where T : Identifiable<Guid> + { + get_identity_map_for<T>().evict(entity.id); + } + + public void flush() + { + this.log().debug("flushing session {0}", id); + transaction.commit_changes(); + transaction = null; + } + + public bool is_dirty() + { + this.log().debug("is dirty? {0}", id); + return null != transaction && transaction.is_dirty(); + } + + public void Dispose() + { + if (null != transaction) transaction.rollback_changes(); + } + + IIdentityMap<Guid, T> get_identity_map_for<T>() where T : Identifiable<Guid> + { + return identity_maps.ContainsKey(typeof (T)) + ? identity_maps[typeof (T)].downcast_to<IIdentityMap<Guid, T>>() + : create_map_for<T>(); + } + + IIdentityMap<Guid, T> create_map_for<T>() where T : Identifiable<Guid> + { + var identity_map = transaction.create_for<T>(); + identity_maps.Add(typeof (T), identity_map); + return identity_map; + } + + public override string ToString() + { + return "session: {0}".formatted_using(id); + } + } +}
\ No newline at end of file diff --git a/product/database/transactions/SessionFactory.cs b/product/database/transactions/SessionFactory.cs new file mode 100644 index 0000000..4486b2b --- /dev/null +++ b/product/database/transactions/SessionFactory.cs @@ -0,0 +1,23 @@ +using gorilla.commons.utility; + +namespace momoney.database.transactions +{ + public interface ISessionFactory : Factory<ISession> {} + + public class SessionFactory : ISessionFactory + { + readonly IDatabase database; + readonly IChangeTrackerFactory factory; + + public SessionFactory(IDatabase database, IChangeTrackerFactory factory) + { + this.database = database; + this.factory = factory; + } + + public ISession create() + { + return new Session(new Transaction(database, factory), database); + } + } +}
\ No newline at end of file diff --git a/product/database/transactions/SessionFactorySpecs.cs b/product/database/transactions/SessionFactorySpecs.cs new file mode 100644 index 0000000..9ad65e2 --- /dev/null +++ b/product/database/transactions/SessionFactorySpecs.cs @@ -0,0 +1,21 @@ +using developwithpassion.bdd.contexts; +using Gorilla.Commons.Testing; + +namespace momoney.database.transactions +{ + public class SessionFactorySpecs + { + [Concern(typeof (SessionFactory))] + public class when_creating_a_new_session : concerns_for<ISessionFactory, SessionFactory> + { + it should_return_a_new_session = () => result.should_not_be_null(); + + because b = () => + { + result = sut.create(); + }; + + static ISession result; + } + } +}
\ No newline at end of file diff --git a/product/database/transactions/SessionNotStartedException.cs b/product/database/transactions/SessionNotStartedException.cs new file mode 100644 index 0000000..105cd26 --- /dev/null +++ b/product/database/transactions/SessionNotStartedException.cs @@ -0,0 +1,11 @@ +using System; + +namespace momoney.database.transactions +{ + public class SessionNotStartedException : Exception + { + public SessionNotStartedException() : base("A session could not be found. Did you forget to open a session?") + { + } + } +}
\ No newline at end of file diff --git a/product/database/transactions/SessionProvider.cs b/product/database/transactions/SessionProvider.cs new file mode 100644 index 0000000..724dc8d --- /dev/null +++ b/product/database/transactions/SessionProvider.cs @@ -0,0 +1,25 @@ +namespace momoney.database.transactions +{ + public interface ISessionProvider + { + ISession get_the_current_session(); + } + + public class SessionProvider : ISessionProvider + { + readonly IContext context; + readonly IKey<ISession> session_key; + + public SessionProvider(IContext context, IKey<ISession> session_key) + { + this.context = context; + this.session_key = session_key; + } + + public ISession get_the_current_session() + { + if (!context.contains(session_key)) throw new SessionNotStartedException(); + return context.value_for(session_key); + } + } +}
\ No newline at end of file diff --git a/product/database/transactions/SessionSpecs.cs b/product/database/transactions/SessionSpecs.cs new file mode 100644 index 0000000..4e3cf55 --- /dev/null +++ b/product/database/transactions/SessionSpecs.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using developwithpassion.bdd.contexts; +using Gorilla.Commons.Testing; +using gorilla.commons.utility; + +namespace momoney.database.transactions +{ + public class SessionSpecs + { + public class behaves_like_session : concerns_for<ISession, Session> + { + context c = () => + { + transaction = the_dependency<ITransaction>(); + database = the_dependency<IDatabase>(); + }; + + static protected ITransaction transaction; + static protected IDatabase database; + } + + [Concern(typeof (Session))] + public class when_saving_a_transient_item_to_a_session : behaves_like_session + { + it should_add_the_entity_to_the_identity_map = () => map.was_told_to(x => x.add(guid, entity)); + + context c = () => + { + guid = Guid.NewGuid(); + entity = an<ITestEntity>(); + map = an<IIdentityMap<Guid, ITestEntity>>(); + + when_the(entity).is_told_to(x => x.id).it_will_return(guid); + when_the(transaction).is_told_to(x => x.create_for<ITestEntity>()).it_will_return(map); + }; + + because b = () => sut.save(entity); + + static ITestEntity entity; + static IIdentityMap<Guid, ITestEntity> map; + static Id<Guid> guid; + } + + [Concern(typeof (Session))] + public class when_commiting_the_changes_made_in_a_session : behaves_like_session + { + it should_commit_all_the_changes_from_the_running_transaction = + () => transaction.was_told_to(x => x.commit_changes()); + + it should_not_rollback_any_changes_from_the_running_transaction = + () => transaction.was_not_told_to(x => x.rollback_changes()); + + because b = () => + { + sut.flush(); + sut.Dispose(); + }; + } + + [Concern(typeof (Session))] + public class when_closing_a_session_before_flushing_the_changes : behaves_like_session + { + it should_rollback_any_changes_made_in_the_current_transaction = + () => transaction.was_told_to(x => x.rollback_changes()); + + because b = () => sut.Dispose(); + } + + [Concern(typeof (Session))] + public class when_loading_all_instances_of_a_certain_type_and_some_have_already_been_loaded : behaves_like_session + { + it should_return_the_items_from_the_cache = () => results.should_contain(cached_item); + + it should_exclude_duplicates_from_the_database = () => results.should_not_contain(database_item); + + it should_add_items_from_the_database_to_the_identity_map = + () => identity_map.was_told_to(x => x.add(id_of_the_uncached_item, uncached_item)); + + context c = () => + { + id = Guid.NewGuid(); + id_of_the_uncached_item = Guid.NewGuid(); + identity_map = an<IIdentityMap<Guid, ITestEntity>>(); + cached_item = an<ITestEntity>(); + database_item = an<ITestEntity>(); + uncached_item = an<ITestEntity>(); + + when_the(cached_item).is_told_to(x => x.id).it_will_return(id); + when_the(database_item).is_told_to(x => x.id).it_will_return(id); + when_the(uncached_item).is_told_to(x => x.id).it_will_return(id_of_the_uncached_item); + when_the(transaction).is_told_to(x => x.create_for<ITestEntity>()).it_will_return(identity_map); + when_the(identity_map).is_told_to(x => x.contains_an_item_for(id)).it_will_return(true); + when_the(identity_map).is_told_to(x => x.all()).it_will_return(cached_item); + when_the(database).is_told_to(x => x.fetch_all<ITestEntity>()) + .it_will_return(database_item, uncached_item); + }; + + because b = () => + { + sut.find<ITestEntity>(id); + results = sut.all<ITestEntity>(); + }; + + static IEnumerable<ITestEntity> results; + static Id<Guid> id; + static Id<Guid> id_of_the_uncached_item; + static ITestEntity cached_item; + static ITestEntity database_item; + static IIdentityMap<Guid, ITestEntity> identity_map; + static ITestEntity uncached_item; + } + + [Concern(typeof (Session))] + public class when_looking_up_a_specific_entity_by_its_id_and_it_has_not_been_loaded_into_the_cache : + behaves_like_session + { + it should_return_that_item = () => + { + result.should_be_equal_to(correct_item); + }; + + it should_add_that_item_to_the_identity_map = () => map.was_told_to(x => x.add(id, correct_item)); + + context c = () => + { + id = Guid.NewGuid(); + wrong_item = an<ITestEntity>(); + correct_item = an<ITestEntity>(); + map = an<IIdentityMap<Guid, ITestEntity>>(); + when_the(wrong_item).is_told_to(x => x.id).it_will_return<Id<Guid>>(Guid.NewGuid()); + when_the(correct_item).is_told_to(x => x.id).it_will_return(id); + when_the(database) + .is_told_to(x => x.fetch_all<ITestEntity>()) + .it_will_return(wrong_item, correct_item); + when_the(transaction).is_told_to(x => x.create_for<ITestEntity>()) + .it_will_return(map); + }; + + because b = () => + { + result = sut.find<ITestEntity>(id); + }; + + static Id<Guid> id; + static Identifiable<Guid> result; + static ITestEntity correct_item; + static ITestEntity wrong_item; + static IIdentityMap<Guid, ITestEntity> map; + } + + [Concern(typeof (Session))] + public class when_deleting_an_item_from_the_database : behaves_like_session + { + it should_remove_that_item_from_the_cache = () => map.was_told_to(x => x.evict(id)); + + context c = () => + { + id = Guid.NewGuid(); + entity = an<ITestEntity>(); + map = an<IIdentityMap<Guid, ITestEntity>>(); + + when_the(entity).is_told_to(x => x.id).it_will_return(id); + when_the(transaction).is_told_to(x => x.create_for<ITestEntity>()).it_will_return(map); + when_the(database).is_told_to(x => x.fetch_all<ITestEntity>()).it_will_return(entity); + }; + + because b = () => + { + sut.find<ITestEntity>(id); + sut.delete(entity); + }; + + static Id<Guid> id; + static IIdentityMap<Guid, ITestEntity> map; + static ITestEntity entity; + } + + public interface ITestEntity : Identifiable<Guid> {} + } +}
\ No newline at end of file diff --git a/product/database/transactions/SingletonScopedStorage.cs b/product/database/transactions/SingletonScopedStorage.cs new file mode 100644 index 0000000..395b6a0 --- /dev/null +++ b/product/database/transactions/SingletonScopedStorage.cs @@ -0,0 +1,14 @@ +using System.Collections; + +namespace momoney.database.transactions +{ + public class SingletonScopedStorage : IScopedStorage + { + static readonly IDictionary storage = new Hashtable(); + + public IDictionary provide_storage() + { + return storage; + } + } +}
\ No newline at end of file diff --git a/product/database/transactions/StatementRegistry.cs b/product/database/transactions/StatementRegistry.cs new file mode 100644 index 0000000..66ca362 --- /dev/null +++ b/product/database/transactions/StatementRegistry.cs @@ -0,0 +1,48 @@ +using System; +using gorilla.commons.utility; + +namespace momoney.database.transactions +{ + public class StatementRegistry : IStatementRegistry + { + public IStatement prepare_delete_statement_for<T>(T entity) where T : Identifiable<Guid> + { + return new DeletionStatement<T>(entity); + } + + public IStatement prepare_command_for<T>(T entity) where T : Identifiable<Guid> + { + return new SaveOrUpdateStatement<T>(entity); + } + } + + public class SaveOrUpdateStatement<T> : IStatement where T : Identifiable<Guid> + { + readonly T entity; + + public SaveOrUpdateStatement(T entity) + { + this.entity = entity; + } + + public void prepare(IDatabaseConnection connection) + { + connection.store(entity); + } + } + + public class DeletionStatement<T> : IStatement + { + readonly T entity; + + public DeletionStatement(T entity) + { + this.entity = entity; + } + + public void prepare(IDatabaseConnection connection) + { + connection.delete(entity); + } + } +}
\ No newline at end of file diff --git a/product/database/transactions/TrackerEntry.cs b/product/database/transactions/TrackerEntry.cs new file mode 100644 index 0000000..df0e837 --- /dev/null +++ b/product/database/transactions/TrackerEntry.cs @@ -0,0 +1,47 @@ +using System.Reflection; +using Gorilla.Commons.Infrastructure.Logging; + +namespace momoney.database.transactions +{ + public interface ITrackerEntry<T> + { + T current { get; } + bool has_changes(); + } + + public class TrackerEntry<T> : ITrackerEntry<T> + { + readonly T original; + + public TrackerEntry(T original, T current) + { + this.original = original; + this.current = current; + } + + public T current { get; set; } + + public bool has_changes() + { + this.log().debug("checking for changes"); + var type = original.GetType(); + foreach (var field in type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance)) + { + var original_value = field.GetValue(original); + var current_value = field.GetValue(current); + if (original_value == null && current_value != null) + { + this.log().debug("{0} has changes: {1}", field, original); + return true; + } + if (original_value != null && !original_value.Equals(current_value)) + { + this.log().debug("{0} has changes: {1}", field, original); + return true; + } + } + this.log().debug("has no changes: {0}", original); + return false; + } + } +}
\ No newline at end of file diff --git a/product/database/transactions/TrackerEntryMapper.cs b/product/database/transactions/TrackerEntryMapper.cs new file mode 100644 index 0000000..7f81f80 --- /dev/null +++ b/product/database/transactions/TrackerEntryMapper.cs @@ -0,0 +1,27 @@ +using Gorilla.Commons.Infrastructure.Cloning; +using gorilla.commons.utility; + +namespace momoney.database.transactions +{ + public interface ITrackerEntryMapper<T> : Mapper<T, ITrackerEntry<T>> {} + + public class TrackerEntryMapper<T> : ITrackerEntryMapper<T> + { + readonly IPrototype prototype; + + public TrackerEntryMapper(IPrototype prototype) + { + this.prototype = prototype; + } + + public ITrackerEntry<T> map_from(T item) + { + return new TrackerEntry<T>(create_prototype(item), item); + } + + T create_prototype(T item) + { + return prototype.clone(item); + } + } +}
\ No newline at end of file diff --git a/product/database/transactions/TrackerEntrySpecs.cs b/product/database/transactions/TrackerEntrySpecs.cs new file mode 100644 index 0000000..bad8c65 --- /dev/null +++ b/product/database/transactions/TrackerEntrySpecs.cs @@ -0,0 +1,145 @@ +using System; +using developwithpassion.bdd.contexts; +using Gorilla.Commons.Testing; +using gorilla.commons.utility; + +namespace momoney.database.transactions +{ + public class TrackerEntrySpecs {} + + public abstract class behaves_like_tracker_entry : concerns_for<ITrackerEntry<Pillow>> {} + + [Concern(typeof (ITrackerEntry<>))] + public class when_comparing_the_current_instance_of_a_component_with_its_original_and_it_has_changes : + behaves_like_tracker_entry + { + it should_indicate_that_there_are_changes = () => result.should_be_true(); + + because b = () => + { + result = sut.has_changes(); + }; + + public override ITrackerEntry<Pillow> create_sut() + { + return new TrackerEntry<Pillow>(new Pillow("pink"), new Pillow("yellow")); + } + + static bool result; + } + + [Concern(typeof (ITrackerEntry<>))] + public class when_the_original_instance_has_a_null_field_that_is_now_not_null : + behaves_like_tracker_entry + { + it should_indicate_that_there_are_changes = () => result.should_be_true(); + + because b = () => + { + result = sut.has_changes(); + }; + + public override ITrackerEntry<Pillow> create_sut() + { + return new TrackerEntry<Pillow>(new Pillow(null), new Pillow("yellow")); + } + + static bool result; + } + + [Concern(typeof (ITrackerEntry<>))] + public class when_the_original_instance_had_a_non_null_field_and_the_current_instance_has_a_null_field : + behaves_like_tracker_entry + { + it should_indicate_that_there_are_changes = () => result.should_be_true(); + + because b = () => + { + result = sut.has_changes(); + }; + + context c = () => + { + var id = Guid.NewGuid(); + original = new Pillow("green", id); + current = new Pillow(null, id); + }; + + public override ITrackerEntry<Pillow> create_sut() + { + return new TrackerEntry<Pillow>(original, current); + } + + static bool result; + static Pillow original; + static Pillow current; + } + + [Concern(typeof (ITrackerEntry<>))] + public class when_the_original_instance_has_the_same_value_as_the_current_instance : + behaves_like_tracker_entry + { + it should_indicate_that_there_are_no_changes = () => result.should_be_false(); + + because b = () => + { + result = sut.has_changes(); + }; + + context c = () => + { + var id = Guid.NewGuid(); + original = new Pillow("green", id); + current = new Pillow("green", id); + }; + + public override ITrackerEntry<Pillow> create_sut() + { + return new TrackerEntry<Pillow>(original, current); + } + + static bool result; + static Pillow original; + static Pillow current; + } + + public class Pillow : Identifiable<Guid> + { + readonly string color; + + public Pillow(string color) : this(color, Guid.NewGuid()) {} + + public Pillow(string color, Guid id) + { + this.color = color; + this.id = id; + } + + public Id<Guid> id { get; set; } + + public bool Equals(Pillow other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return other.id.Equals(id); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != typeof (Pillow)) return false; + return Equals((Pillow) obj); + } + + public override int GetHashCode() + { + return id.GetHashCode(); + } + + public override string ToString() + { + return "{0} id: {1}".formatted_using(base.ToString(), id); + } + } +}
\ No newline at end of file diff --git a/product/database/transactions/Transaction.cs b/product/database/transactions/Transaction.cs new file mode 100644 index 0000000..cc350bf --- /dev/null +++ b/product/database/transactions/Transaction.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using gorilla.commons.utility; + +namespace momoney.database.transactions +{ + public interface ITransaction + { + IIdentityMap<Guid, T> create_for<T>() where T : Identifiable<Guid>; + void commit_changes(); + void rollback_changes(); + bool is_dirty(); + } + + public class Transaction : ITransaction + { + readonly IDatabase database; + readonly IChangeTrackerFactory factory; + readonly IDictionary<Type, IChangeTracker> change_trackers; + + public Transaction(IDatabase database, IChangeTrackerFactory factory) + { + this.factory = factory; + this.database = database; + change_trackers = new Dictionary<Type, IChangeTracker>(); + } + + public IIdentityMap<Guid, T> create_for<T>() where T : Identifiable<Guid> + { + return new IdentityMapProxy<Guid, T>(get_change_tracker_for<T>(), new IdentityMap<Guid, T>()); + } + + public void commit_changes() + { + change_trackers.Values.where(x => x.is_dirty()).each(x => x.commit_to(database)); + } + + public void rollback_changes() + { + change_trackers.each(x => x.Value.Dispose()); + change_trackers.Clear(); + } + + public bool is_dirty() + { + return change_trackers.Values.Count(x => x.is_dirty()) > 0; + } + + IChangeTracker<T> get_change_tracker_for<T>() where T : Identifiable<Guid> + { + if (!change_trackers.ContainsKey(typeof (T))) change_trackers.Add(typeof (T), factory.create_for<T>()); + return change_trackers[typeof (T)].downcast_to<IChangeTracker<T>>(); + } + } +}
\ No newline at end of file diff --git a/product/database/transactions/TransactionSpecs.cs b/product/database/transactions/TransactionSpecs.cs new file mode 100644 index 0000000..65d400f --- /dev/null +++ b/product/database/transactions/TransactionSpecs.cs @@ -0,0 +1,113 @@ +using System; +using developwithpassion.bdd.contexts; +using Gorilla.Commons.Testing; +using gorilla.commons.utility; + +namespace momoney.database.transactions +{ + public class TransactionSpecs + { + } + + [Concern(typeof (Transaction))] + public class behaves_like_transaction : concerns_for<ITransaction, Transaction> + { + context c = () => + { + database = the_dependency<IDatabase>(); + factory = the_dependency<IChangeTrackerFactory>(); + }; + + static protected IDatabase database; + static protected IChangeTrackerFactory factory; + } + + [Concern(typeof (Transaction))] + public class when_creating_an_identity_map_for_a_specific_entity : behaves_like_transaction + { + it should_return_a_new_identity_map = () => result.should_not_be_null(); + + because b = () => { result = sut.create_for<Identifiable<Guid>>(); }; + + static IIdentityMap<Guid, Identifiable<Guid>> result; + } + + [Concern(typeof (Transaction))] + public class when_committing_a_transaction_and_an_item_in_the_identity_map_has_changed : behaves_like_transaction + { + it should_commit_the_changes_to_that_item = + () => tracker.was_told_to<IChangeTracker<IMovie>>(x => x.commit_to(database)); + + context c = () => + { + movie = new Movie("Goldeneye"); + tracker = an<IChangeTracker<IMovie>>(); + + when_the(factory).is_told_to(x => x.create_for<IMovie>()).it_will_return(tracker); + when_the(tracker).is_told_to(x => x.is_dirty()).it_will_return(true); + }; + + + because b = () => + { + sut.create_for<IMovie>().add(movie.id, movie); + movie.change_name_to("Austin Powers"); + sut.commit_changes(); + }; + + static IMovie movie; + static IChangeTracker<IMovie> tracker; + } + + [Concern(typeof (Transaction))] + public class when_deleting_a_set_of_entities_from_the_database : behaves_like_transaction + { + it should_prepare_to_delete_that_item_form_the_database = () => tracker.was_told_to(x => x.delete(movie)); + + it should_delete_all_items_marked_for_deletion = () => tracker.was_told_to(x => x.commit_to(database)); + + context c = () => + { + movie = new Movie("Goldeneye"); + tracker = an<IChangeTracker<IMovie>>(); + + when_the(factory).is_told_to(x => x.create_for<IMovie>()).it_will_return(tracker); + when_the(tracker).is_told_to(x => x.is_dirty()).it_will_return(true); + }; + + because b = () => + { + var map = sut.create_for<IMovie>(); + map.add(movie.id, movie); + map.evict(movie.id); + sut.commit_changes(); + }; + + static IMovie movie; + static IChangeTracker<IMovie> tracker; + } + + public interface IMovie : Identifiable<Guid> + { + string name { get; } + void change_name_to(string name); + } + + internal class Movie : IMovie + { + public Movie(string name) + { + id = Guid.NewGuid(); + this.name = name; + } + + public string name { get; set; } + + public void change_name_to(string new_name) + { + name = new_name; + } + + public Id<Guid> id { get; set; } + } +}
\ No newline at end of file diff --git a/product/database/transactions/TypedKey.cs b/product/database/transactions/TypedKey.cs new file mode 100644 index 0000000..12887f1 --- /dev/null +++ b/product/database/transactions/TypedKey.cs @@ -0,0 +1,50 @@ +using System.Collections; + +namespace momoney.database.transactions +{ + public class TypedKey<T> : IKey<T> + { + public bool is_found_in(IDictionary items) + { + return items.Contains(create_key()); + } + + public T parse_from(IDictionary items) + { + return (T) items[create_key()]; + } + + public void remove_from(IDictionary items) + { + if (is_found_in(items)) items.Remove(create_key()); + } + + public void add_value_to(IDictionary items, T value) + { + items[create_key()] = value; + } + + public bool Equals(TypedKey<T> obj) + { + return !ReferenceEquals(null, obj); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != typeof (TypedKey<T>)) return false; + return Equals((TypedKey<T>) obj); + } + + public override int GetHashCode() + { + return GetType().GetHashCode(); + } + + string create_key() + { + return GetType().FullName; + } + } +}
\ No newline at end of file |
