diff --git a/README.markdown b/README.markdown deleted file mode 100644 index 6e306a9..0000000 --- a/README.markdown +++ /dev/null @@ -1,58 +0,0 @@ -# ArmChair.Core -======= - -**CouchDB + .NET + Unit-Of-Work = ArmChair.** - -ArmChair has been built from the ground up with the following goals: - -* Unit-Of-Work pattern support - supporting the all or nothing commit. -* POCO's - no base class or interface required. (Just add your own Id and Rev). -* Conventions - where possible we implement small conventions to make the framework work for you. -* Customisable - Add support for your IDs, Tracking, Serialization etc. -* Use of an Indexing Service - Search by IDs, where the initial search can be executed on ElasticSearch / Solr / Lucene - -## Other Features - -* Mongo Query Support -* Linq Support (Partial) -* (configurable) Logs requests and responses, to assist development - -## Documentation - -its being worked on but you can find some here: https://bitbucket.org/dboneslabs/arm-chair/wiki/Home - -##Nuget - -* **Url** https://www.nuget.org/packages/ArmChair.Core/ -* **Current version:** 0.11.x -* **Released:** July 2018 - -``` -PM> Install-Package ArmChair.Core -``` - -## Compatibility - -* .NET 4.5 + (Mono latest) -* .NET Standard 1.6 + (.NET core 1.1 +) -* CouchDB 2.0 + - -## Licence - -Apache 2.0 licensed. - -## Building from source - -To build this project you will need dotnet core - -``` -dotnet restore -dotnet msbuild /target:Build /p:Configuration=Release /p:BuildNumber=37 -dotnet test -``` - -the buildNumber is optional, if not included it will set the patch number to 0. - -#### Note - -*Please ensure that you test the usage of this library, before using this in your production system.* \ No newline at end of file diff --git a/nuspec/changes.txt b/nuspec/changes.txt index 7b6b602..648757d 100644 --- a/nuspec/changes.txt +++ b/nuspec/changes.txt @@ -1,5 +1,10 @@ + + + 0.11.x ====== +.NET 4.5.2 (for versions 0.11.2 +) +fix for Newtonsoft returning an empty string for null's fix for List added simple logger (only to be used for development) diff --git a/samples/todo/Todo.Service/Adapters/Controllers/TasksController.cs b/samples/todo/Todo.Service/Adapters/Controllers/TasksController.cs index 05dcbb3..242dfc3 100644 --- a/samples/todo/Todo.Service/Adapters/Controllers/TasksController.cs +++ b/samples/todo/Todo.Service/Adapters/Controllers/TasksController.cs @@ -26,7 +26,7 @@ public virtual async Task> Get([FromQuery] AllQ { if (queryString == null) throw new ArgumentNullException(nameof(queryString)); - var query = _mapper.Map(queryString); + var query = _mapper.Map(queryString); var tasks = await _mediator.Send(query); var result = _mapper.Map>(tasks); diff --git a/samples/todo/Todo.Service/Infrastructure/Behaviours/UnitOfWorkBehavior.cs b/samples/todo/Todo.Service/Infrastructure/Behaviours/UnitOfWorkBehavior.cs index e901a1e..39bca82 100644 --- a/samples/todo/Todo.Service/Infrastructure/Behaviours/UnitOfWorkBehavior.cs +++ b/samples/todo/Todo.Service/Infrastructure/Behaviours/UnitOfWorkBehavior.cs @@ -1,10 +1,11 @@ namespace Todo.Service.Infrastructure.Behaviours { + using System.Threading; using ArmChair; using MediatR; using System.Threading.Tasks; - public class UnitOfWorkBehavior : IPipelineBehavior + public class UnitOfWorkBehavior : IPipelineBehavior { private readonly ISession _session; @@ -13,7 +14,7 @@ public UnitOfWorkBehavior(ISession session) _session = session; } - public async Task Handle(TRequest request, RequestHandlerDelegate next) + public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) { var response = await next(); @@ -21,5 +22,6 @@ public async Task Handle(TRequest request, RequestHandlerDelegate + public class TaskClassMap : ClassMap { public TaskClassMap() { diff --git a/samples/todo/Todo.Service/Infrastructure/Mapping/QueriesMapping.cs b/samples/todo/Todo.Service/Infrastructure/Mapping/QueriesMapping.cs index 2f5161e..731b0cc 100644 --- a/samples/todo/Todo.Service/Infrastructure/Mapping/QueriesMapping.cs +++ b/samples/todo/Todo.Service/Infrastructure/Mapping/QueriesMapping.cs @@ -7,7 +7,7 @@ public class QueriesMapping : AutoMapper.Profile { public QueriesMapping() { - CreateMap(); + CreateMap(); CreateMap(); CreateMap(); } diff --git a/samples/todo/Todo.Service/Infrastructure/Mapping/ResourcesMapping.cs b/samples/todo/Todo.Service/Infrastructure/Mapping/ResourcesMapping.cs index 729b491..d03b553 100644 --- a/samples/todo/Todo.Service/Infrastructure/Mapping/ResourcesMapping.cs +++ b/samples/todo/Todo.Service/Infrastructure/Mapping/ResourcesMapping.cs @@ -19,18 +19,18 @@ public ResourcesMapping(IConfigurationRoot config) var port = webServerConfig.Port == 80 ? "" : $":{webServerConfig.Port}"; baseUrl = $"{http}{domain}{port}/api/v1/tasks"; - CreateMap() + CreateMap() .ForMember(dest => dest.Links, opt => opt.MapFrom(src => GetLinksFromTask(src))) .ForMember(dest => dest.Actions, opt => opt.MapFrom(src => GetActionsFromTask(src))); - CreateMap, CollectionResource>() + CreateMap, CollectionResource>() .ForMember(dest => dest.Data, opt => opt.MapFrom(src => src)) .ForMember(dest => dest.Links, opt => opt.MapFrom(src => GetCollectionLinksFromTask(src))) .ForMember(dest => dest.Actions, opt => opt.MapFrom(src => GetCollectionActionsFromTask(src))); } - protected IDictionary GetLinksFromTask(Task task) + protected IDictionary GetLinksFromTask(TodoItem todoItem) { var result = new Dictionary(); result.Add("collection", $"{baseUrl}/"); @@ -38,17 +38,17 @@ protected IDictionary GetLinksFromTask(Task task) } - protected IDictionary GetActionsFromTask(Task task) + protected IDictionary GetActionsFromTask(TodoItem todoItem) { var result = new Dictionary(); - if (!task.IsComplete) result.Add("update", $"{baseUrl}/{task.Id}"); - if (!task.IsComplete) result.Add("complete", $"{baseUrl}/{task.Id}/complete"); - result.Add("remove", $"{baseUrl}/{task.Id}"); + if (!todoItem.IsComplete) result.Add("update", $"{baseUrl}/{todoItem.Id}"); + if (!todoItem.IsComplete) result.Add("complete", $"{baseUrl}/{todoItem.Id}/complete"); + result.Add("remove", $"{baseUrl}/{todoItem.Id}"); return result; } - protected IDictionary GetCollectionLinksFromTask(IEnumerable task) + protected IDictionary GetCollectionLinksFromTask(IEnumerable task) { var result = new Dictionary(); result.Add("self", $"{baseUrl}/"); @@ -58,7 +58,7 @@ protected IDictionary GetCollectionLinksFromTask(IEnumerable GetCollectionActionsFromTask(IEnumerable task) + protected IDictionary GetCollectionActionsFromTask(IEnumerable task) { var result = new Dictionary(); result.Add("createTodoTask", $"{baseUrl}/"); diff --git a/samples/todo/Todo.Service/Infrastructure/Modules/DataAccessModule.cs b/samples/todo/Todo.Service/Infrastructure/Modules/DataAccessModule.cs index f872d77..bdcdcc2 100644 --- a/samples/todo/Todo.Service/Infrastructure/Modules/DataAccessModule.cs +++ b/samples/todo/Todo.Service/Infrastructure/Modules/DataAccessModule.cs @@ -89,7 +89,7 @@ protected void EnsureDatabase(Connection conn, DatabaseConfig dbConfig) /// the database instance protected void SetupDatabase(Database database) { - database.Settings.QueryPipeline.SetItemIterator(new ListsItemIterator()); + //database.Settings.QueryPipeline.SetItemIterator(new ListsItemIterator()); //register maps database.Register(new[] { new TaskClassMap() }); diff --git a/samples/todo/Todo.Service/Models/Task.cs b/samples/todo/Todo.Service/Models/TodoItem.cs similarity index 86% rename from samples/todo/Todo.Service/Models/Task.cs rename to samples/todo/Todo.Service/Models/TodoItem.cs index bbbf80a..8818505 100644 --- a/samples/todo/Todo.Service/Models/Task.cs +++ b/samples/todo/Todo.Service/Models/TodoItem.cs @@ -2,17 +2,17 @@ { using System; - public class Task : EntityRoot + public class TodoItem : EntityRoot { private string _description; private bool _isComplete; - protected Task() + protected TodoItem() { } - public Task(string description, PriorityLevel priorityLevel) + public TodoItem(string description, PriorityLevel priorityLevel) { Created = DateTime.UtcNow; _description = description; diff --git a/samples/todo/Todo.Service/Ports/Commands/CreateUpdateTask.cs b/samples/todo/Todo.Service/Ports/Commands/CreateUpdateTodoItem.cs similarity index 58% rename from samples/todo/Todo.Service/Ports/Commands/CreateUpdateTask.cs rename to samples/todo/Todo.Service/Ports/Commands/CreateUpdateTodoItem.cs index aa869ea..865b365 100644 --- a/samples/todo/Todo.Service/Ports/Commands/CreateUpdateTask.cs +++ b/samples/todo/Todo.Service/Ports/Commands/CreateUpdateTodoItem.cs @@ -1,18 +1,20 @@ namespace Todo.Service.Ports.Commands { using System.ComponentModel.DataAnnotations; + using System.Threading; + using System.Threading.Tasks; using ArmChair; using MediatR; using Models; - public class CreateUpdateTask : IRequest + public class CreateUpdateTask : IRequest { [Required] public string Description { get; set; } public PriorityLevel PriorityLevel { get; set; } = PriorityLevel.Medium; } - public class CreateUpdateTaskHandler : IRequestHandler + public class CreateUpdateTaskHandler : IRequestHandler { private readonly ISession _session; @@ -21,12 +23,14 @@ public CreateUpdateTaskHandler(ISession session) _session = session; } - public Task Handle(CreateUpdateTask message) + + + public Task Handle(CreateUpdateTask request, CancellationToken cancellationToken) { - var task = new Task(message.Description, message.PriorityLevel); - _session.Add(task); + var todoItem = new TodoItem(request.Description, request.PriorityLevel); + _session.Add(todoItem); - return task; + return Task.FromResult(todoItem); } } } \ No newline at end of file diff --git a/samples/todo/Todo.Service/Ports/Commands/MarkTaskAsComplete.cs b/samples/todo/Todo.Service/Ports/Commands/MarkTaskAsComplete.cs deleted file mode 100644 index 2b83d20..0000000 --- a/samples/todo/Todo.Service/Ports/Commands/MarkTaskAsComplete.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace Todo.Service.Ports.Commands -{ - using System; - using System.ComponentModel.DataAnnotations; - using ArmChair; - using Infrastructure.Exceptions; - using MediatR; - using Models; - - public class MarkTaskAsComplete : IRequest - { - [Required] - public string Id { get; set; } - } - - public class MarkTaskAsCompleteHandler : IRequestHandler - { - private readonly ISession _session; - - public MarkTaskAsCompleteHandler(ISession session) - { - _session = session; - } - - public Task Handle(MarkTaskAsComplete message) - { - if (message == null) throw new ArgumentNullException(nameof(message)); - var task = _session.GetById(message.Id); - - if (task == null) - { - throw new NotFoundException("Task", message.Id); - } - - task.IsComplete = true; - return task; - } - } -} \ No newline at end of file diff --git a/samples/todo/Todo.Service/Ports/Commands/MarkTodoItemAsComplete.cs b/samples/todo/Todo.Service/Ports/Commands/MarkTodoItemAsComplete.cs new file mode 100644 index 0000000..a6d616f --- /dev/null +++ b/samples/todo/Todo.Service/Ports/Commands/MarkTodoItemAsComplete.cs @@ -0,0 +1,42 @@ +namespace Todo.Service.Ports.Commands +{ + using System; + using System.ComponentModel.DataAnnotations; + using System.Threading; + using System.Threading.Tasks; + using ArmChair; + using Infrastructure.Exceptions; + using MediatR; + using Models; + + public class MarkTaskAsComplete : IRequest + { + [Required] + public string Id { get; set; } + } + + public class MarkTaskAsCompleteHandler : IRequestHandler + { + private readonly ISession _session; + + public MarkTaskAsCompleteHandler(ISession session) + { + _session = session; + } + + public Task Handle(MarkTaskAsComplete request, CancellationToken cancellationToken) + { + if (request == null) throw new ArgumentNullException(nameof(request)); + var todoItem = _session.GetById(request.Id); + + if (todoItem == null) + { + throw new NotFoundException("Task", request.Id); + } + + todoItem.IsComplete = true; + return new Task(() => todoItem); + } + + } +} \ No newline at end of file diff --git a/samples/todo/Todo.Service/Ports/Commands/PruneStaleTasks.cs b/samples/todo/Todo.Service/Ports/Commands/PruneStaleTodoItems.cs similarity index 54% rename from samples/todo/Todo.Service/Ports/Commands/PruneStaleTasks.cs rename to samples/todo/Todo.Service/Ports/Commands/PruneStaleTodoItems.cs index 2803c9c..ff10db8 100644 --- a/samples/todo/Todo.Service/Ports/Commands/PruneStaleTasks.cs +++ b/samples/todo/Todo.Service/Ports/Commands/PruneStaleTodoItems.cs @@ -1,6 +1,8 @@ namespace Todo.Service.Ports.Commands { using System; + using System.Threading; + using System.Threading.Tasks; using ArmChair; using MediatR; @@ -19,16 +21,20 @@ public PruneStaleTasksHandler(IMediator mediator, ISession session) _session = session; } - public void Handle(PuneStaleTasks message) + public Task Handle(PuneStaleTasks request, CancellationToken cancellationToken) { - if (message == null) throw new ArgumentNullException(nameof(message)); + if (request == null) throw new ArgumentNullException(nameof(request)); - var tasks = _mediator.Send(new Queries.TasksByStale()).Result; + var todoItems = _mediator.Send(new Queries.TasksByStale()).Result; - if (tasks != null) + if (todoItems != null) { - _session.RemoveRange(tasks); + _session.RemoveRange(todoItems); } + + return Task.FromResult(new Unit()); } + + } } \ No newline at end of file diff --git a/samples/todo/Todo.Service/Ports/Commands/RemoveTask.cs b/samples/todo/Todo.Service/Ports/Commands/RemoveTodoItem.cs similarity index 57% rename from samples/todo/Todo.Service/Ports/Commands/RemoveTask.cs rename to samples/todo/Todo.Service/Ports/Commands/RemoveTodoItem.cs index 409b397..b156aea 100644 --- a/samples/todo/Todo.Service/Ports/Commands/RemoveTask.cs +++ b/samples/todo/Todo.Service/Ports/Commands/RemoveTodoItem.cs @@ -2,6 +2,8 @@ { using System; using System.ComponentModel.DataAnnotations; + using System.Threading; + using System.Threading.Tasks; using ArmChair; using MediatR; using Models; @@ -21,16 +23,18 @@ public RemoveTaskHandler(ISession session) _session = session; } - public void Handle(RemoveTask message) + public Task Handle(RemoveTask request, CancellationToken cancellationToken) { - if (message == null) throw new ArgumentNullException(nameof(message)); + if (request == null) throw new ArgumentNullException(nameof(request)); - var task = _session.GetById(message.Id); + var todoItem = _session.GetById(request.Id); - if (task != null) + if (todoItem != null) { - _session.Remove(task); + _session.Remove(todoItem); } + + return Task.FromResult(new Unit()); } } } \ No newline at end of file diff --git a/samples/todo/Todo.Service/Ports/Queries/AllTasks.cs b/samples/todo/Todo.Service/Ports/Queries/AllTodoItems.cs similarity index 58% rename from samples/todo/Todo.Service/Ports/Queries/AllTasks.cs rename to samples/todo/Todo.Service/Ports/Queries/AllTodoItems.cs index fbfb738..122fe62 100644 --- a/samples/todo/Todo.Service/Ports/Queries/AllTasks.cs +++ b/samples/todo/Todo.Service/Ports/Queries/AllTodoItems.cs @@ -3,18 +3,20 @@ using System; using System.Collections.Generic; using System.Linq; + using System.Threading; + using System.Threading.Tasks; using ArmChair; using MediatR; using Models; - public class AllTasks : IRequest> + public class AllTodoItems : IRequest> { public int Skip { get; set; } = 0; public int Take { get; set; } = 50; public string OrderBy { get; set; } } - public class AllTasksHandler : IRequestHandler> + public class AllTasksHandler : IRequestHandler> { private readonly ISession _session; @@ -23,12 +25,12 @@ public AllTasksHandler(ISession session) _session = session; } - public IEnumerable Handle(AllTasks message) + public Task> Handle(AllTodoItems request, CancellationToken cancellationToken) { - var query = _session.Query(); + var query = _session.Query(); - if (message.OrderBy == null) message.OrderBy = "date"; - switch (message.OrderBy.ToLower()) + if (request.OrderBy == null) request.OrderBy = "date"; + switch (request.OrderBy.ToLower()) { case "priority": query = query.OrderByDescending(x => x.Priority); @@ -40,10 +42,13 @@ public IEnumerable Handle(AllTasks message) throw new NotSupportedException(); } - return query - .Skip(message.Skip) - .Take(message.Take) + IEnumerable result = query + .Skip(request.Skip) + .Take(request.Take) .ToList(); + return Task.FromResult(result); } + + } } \ No newline at end of file diff --git a/samples/todo/Todo.Service/Ports/Queries/TasksByActive.cs b/samples/todo/Todo.Service/Ports/Queries/TodoItemByActive.cs similarity index 63% rename from samples/todo/Todo.Service/Ports/Queries/TasksByActive.cs rename to samples/todo/Todo.Service/Ports/Queries/TodoItemByActive.cs index 1696f1a..bac3bbd 100644 --- a/samples/todo/Todo.Service/Ports/Queries/TasksByActive.cs +++ b/samples/todo/Todo.Service/Ports/Queries/TodoItemByActive.cs @@ -3,18 +3,20 @@ using System; using System.Collections.Generic; using System.Linq; + using System.Threading; + using System.Threading.Tasks; using ArmChair; using MediatR; using Models; - public class TasksByActive : IRequest> + public class TasksByActive : IRequest> { public int Skip { get; set; } = 0; public int Take { get; set; } = 50; public string OrderBy { get; set; } } - public class TasksByActiveHandler : IRequestHandler> + public class TasksByActiveHandler : IRequestHandler> { private readonly ISession _session; @@ -23,13 +25,13 @@ public TasksByActiveHandler(ISession session) _session = session; } - public IEnumerable Handle(TasksByActive message) + public Task> Handle(TasksByActive request, CancellationToken cancellationToken) { - var query = _session.Query() + var query = _session.Query() .Where(x=> x.IsComplete == false); - if (message.OrderBy == null) message.OrderBy = "date"; - switch (message.OrderBy.ToLower()) + if (request.OrderBy == null) request.OrderBy = "date"; + switch (request.OrderBy.ToLower()) { case "priority": query = query.OrderByDescending(x => x.Priority); @@ -41,10 +43,14 @@ public IEnumerable Handle(TasksByActive message) throw new NotSupportedException(); } - return query - .Skip(message.Skip) - .Take(message.Take) + IEnumerable result = query + .Skip(request.Skip) + .Take(request.Take) .ToList(); + + return Task.FromResult(result); } + + } } \ No newline at end of file diff --git a/samples/todo/Todo.Service/Ports/Queries/TasksByPriority.cs b/samples/todo/Todo.Service/Ports/Queries/TodoItemByPriority.cs similarity index 53% rename from samples/todo/Todo.Service/Ports/Queries/TasksByPriority.cs rename to samples/todo/Todo.Service/Ports/Queries/TodoItemByPriority.cs index fc09421..8c97d83 100644 --- a/samples/todo/Todo.Service/Ports/Queries/TasksByPriority.cs +++ b/samples/todo/Todo.Service/Ports/Queries/TodoItemByPriority.cs @@ -2,18 +2,20 @@ { using System.Collections.Generic; using System.Linq; + using System.Threading; + using System.Threading.Tasks; using ArmChair; using MediatR; using Models; - public class TasksByPriority : IRequest> + public class TasksByPriority : IRequest> { public int Skip { get; set; } = 0; public int Take { get; set; } = 50; public PriorityLevel Priority { get; set; } } - public class TasksByPriorityHandler : IRequestHandler> + public class TasksByPriorityHandler : IRequestHandler> { private readonly ISession _session; @@ -22,15 +24,17 @@ public TasksByPriorityHandler(ISession session) _session = session; } - public IEnumerable Handle(TasksByPriority message) + public Task> Handle(TasksByPriority request, CancellationToken cancellationToken) { - var results = _session.Query() - .Where(x=> x.Priority == message.Priority) - .Skip(message.Skip) - .Take(message.Take) + IEnumerable results = _session.Query() + .Where(x=> x.Priority == request.Priority) + .Skip(request.Skip) + .Take(request.Take) .ToList(); - return results; + return Task.FromResult(results); } + + } } \ No newline at end of file diff --git a/samples/todo/Todo.Service/Ports/Queries/TasksByStale.cs b/samples/todo/Todo.Service/Ports/Queries/TodoItemByStale.cs similarity index 55% rename from samples/todo/Todo.Service/Ports/Queries/TasksByStale.cs rename to samples/todo/Todo.Service/Ports/Queries/TodoItemByStale.cs index f5ee1e6..d2ae095 100644 --- a/samples/todo/Todo.Service/Ports/Queries/TasksByStale.cs +++ b/samples/todo/Todo.Service/Ports/Queries/TodoItemByStale.cs @@ -3,17 +3,19 @@ using System; using System.Collections.Generic; using System.Linq; + using System.Threading; + using System.Threading.Tasks; using ArmChair; using MediatR; using Models; - public class TasksByStale : IRequest> + public class TasksByStale : IRequest> { public int Skip { get; set; } = 0; public int Take { get; set; } = 50; } - public class TasksByStaleHandler : IRequestHandler> + public class TasksByStaleHandler : IRequestHandler> { private readonly ISession _session; @@ -22,16 +24,18 @@ public TasksByStaleHandler(ISession session) _session = session; } - public IEnumerable Handle(TasksByStale message) + public Task> Handle(TasksByStale request, CancellationToken cancellationToken) { - var query = _session.Query() + IEnumerable results = _session.Query() .Where(x => x.LastUpdated < DateTime.UtcNow.AddDays(-1)) - .Where(x => x.IsComplete); - - return query - .Skip(message.Skip) - .Take(message.Take) + .Where(x => x.IsComplete) + .Skip(request.Skip) + .Take(request.Take) .ToList(); + + return Task.FromResult(results); } + + } } \ No newline at end of file diff --git a/samples/todo/Todo.Service/Todo.Service.csproj b/samples/todo/Todo.Service/Todo.Service.csproj index 2d6169a..c08b3c8 100644 --- a/samples/todo/Todo.Service/Todo.Service.csproj +++ b/samples/todo/Todo.Service/Todo.Service.csproj @@ -1,6 +1,6 @@  - netcoreapp1.1 + netcoreapp2.1 @@ -8,17 +8,17 @@ - - - - - - - - - - - - + + + + + + + + + + + + \ No newline at end of file diff --git a/src/ArmChair.Core/ArmChair.Core.csproj b/src/ArmChair.Core/ArmChair.Core.csproj index e60760d..ef0665c 100644 --- a/src/ArmChair.Core/ArmChair.Core.csproj +++ b/src/ArmChair.Core/ArmChair.Core.csproj @@ -1,7 +1,7 @@  - net45;netstandard1.6 + netstandard1.6 ArmChair True False @@ -13,7 +13,7 @@ - + @@ -45,7 +45,7 @@ false - + 1701;1702;1705 diff --git a/src/ArmChair.Core/Commands/BulkDocResponse.cs b/src/ArmChair.Core/Commands/BulkDocResponse.cs index f029471..8aeecde 100644 --- a/src/ArmChair.Core/Commands/BulkDocResponse.cs +++ b/src/ArmChair.Core/Commands/BulkDocResponse.cs @@ -17,6 +17,10 @@ public class BulkDocResponse { public string Id { get; set; } public string Rev { get; set; } - public bool Ok { get; set; } + public bool? Ok { get; set; } + + //[{"id":"1pxXq49AuS0q-7RexxZYOxg","error":"conflict","reason":"Document update conflict."}] + public string Error { get; set; } + public string Reason { get; set; } } } \ No newline at end of file diff --git a/src/ArmChair.Core/Commands/CouchDb.cs b/src/ArmChair.Core/Commands/CouchDb.cs index 85c920e..6072eaa 100644 --- a/src/ArmChair.Core/Commands/CouchDb.cs +++ b/src/ArmChair.Core/Commands/CouchDb.cs @@ -15,8 +15,10 @@ namespace ArmChair.Commands { using System; using System.Collections.Generic; + using System.Linq; using System.Net; using System.Net.Http; + using Exceptions; using Http; using Serialization; using Utils.Logging; @@ -124,7 +126,9 @@ public virtual IEnumerable BulkApplyChanges(BulkDocsRequest upd { var content = response.GetBody(); _logger.Log(() => $"applyed changes:{Environment.NewLine}{content}"); - return _serializer.Deserialize>(content); + var results = _serializer.Deserialize>(content); + results = CheckForExceptions(results); + return results; } } @@ -172,5 +176,31 @@ public virtual CreateIndexResponse CreateIndex(CreateIndexRequest index) return _serializer.Deserialize(content); } } + + private IEnumerable CheckForExceptions(IEnumerable results) + { + List exceptions = new List(); + foreach (var result in results) + { + if (result.Ok == true) + { + yield return result; + continue; + } + + switch (result.Error) + { + case "conflict": + exceptions.Add(new ConflictException(result.Id, result.Rev, result.Error, result.Reason)); + break; + default: + exceptions.Add(new CouchDbException(result.Id, result.Rev, result.Error, result.Reason)); + break; + } + } + + if(exceptions.Any()) + throw new BulkException(exceptions); + } } } \ No newline at end of file diff --git a/src/ArmChair.Core/Exceptions/CouchDbException.cs b/src/ArmChair.Core/Exceptions/CouchDbException.cs new file mode 100644 index 0000000..0dd4d93 --- /dev/null +++ b/src/ArmChair.Core/Exceptions/CouchDbException.cs @@ -0,0 +1,55 @@ +// Copyright 2014 - dbones.co.uk (David Rundle) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace ArmChair.Exceptions +{ + using System; + using System.Collections.Generic; + + public class BulkException : Exception + { + public IEnumerable Exceptions { get; } + + public BulkException(IEnumerable exceptions) + { + Exceptions = exceptions; + } + } + + + public class CouchDbException : Exception + { + public string Id { get; } + public string Rev { get; } + public string Error { get; } + public string Reason { get; } + + + + public CouchDbException(string id, string rev, string error, string reason) + { + Id = id; + Rev = rev; + Error = error; + Reason = reason; + } + } + + public class ConflictException : CouchDbException + { + public ConflictException(string id, string rev, string error, string reason) : base(id, rev, error, reason) + { + } + } +} \ No newline at end of file diff --git a/src/ArmChair.Core/Indexing.cs b/src/ArmChair.Core/Indexing.cs index 839c139..2bc25aa 100644 --- a/src/ArmChair.Core/Indexing.cs +++ b/src/ArmChair.Core/Indexing.cs @@ -60,6 +60,7 @@ protected void Compile(IndexEntry indexEntry) { indexEntry.DesignDocument = indexEntry.Type.GetTypeInfo().FullName; } + if (string.IsNullOrEmpty(indexEntry.Name)) { var entries = indexEntry.Index.Fields.Select(x => diff --git a/src/ArmChair.Core/Linq/ToolKit/ReflectionExtensions.cs b/src/ArmChair.Core/Linq/ToolKit/ReflectionExtensions.cs index fc83441..2346715 100644 --- a/src/ArmChair.Core/Linq/ToolKit/ReflectionExtensions.cs +++ b/src/ArmChair.Core/Linq/ToolKit/ReflectionExtensions.cs @@ -30,7 +30,7 @@ public static object GetValue(this MemberInfo member, object instance) throw new Exception($"sorry this member is not supported: {member.Name}"); #endif -#if NETSTANDARD1_6 || NET45 +#if NETSTANDARD1_6 || NET452 switch (member.MemberType) { case MemberTypes.Property: diff --git a/src/ArmChair.Core/Linq/ToolKit/TypeHelper.cs b/src/ArmChair.Core/Linq/ToolKit/TypeHelper.cs index 9c8b768..b5ea2c1 100644 --- a/src/ArmChair.Core/Linq/ToolKit/TypeHelper.cs +++ b/src/ArmChair.Core/Linq/ToolKit/TypeHelper.cs @@ -134,7 +134,7 @@ public static bool IsReadOnly(MemberInfo member) return true; #endif -#if NETSTANDARD1_6 || NET45 +#if NETSTANDARD1_6 || NET452 switch (member.MemberType) { case MemberTypes.Field: @@ -170,7 +170,7 @@ public static bool IsInteger(Type type) return ints.Contains(nnType); #endif -#if NETSTANDARD1_6 || NET45 +#if NETSTANDARD1_6 || NET452 var nnType = GetNonNullableType(type); switch (Type.GetTypeCode(type)) { diff --git a/src/ArmChair.Core/Middleware/ActionWrapper.cs b/src/ArmChair.Core/Middleware/ActionWrapper.cs new file mode 100644 index 0000000..c5089d4 --- /dev/null +++ b/src/ArmChair.Core/Middleware/ActionWrapper.cs @@ -0,0 +1,20 @@ +namespace ArmChair.Middleware +{ + using System; + using System.Threading.Tasks; + + public class ActionWrapper : IAction + { + private readonly Func, Task> _action; + + public ActionWrapper(Func, Task> action) + { + _action = action; + } + + public async Task Execute(T context, Next next) + { + await _action(context, next); + } + } +} \ No newline at end of file diff --git a/src/ArmChair.Core/Middleware/Commit/CommitPipeline.cs b/src/ArmChair.Core/Middleware/Commit/CommitPipeline.cs new file mode 100644 index 0000000..f2bddb8 --- /dev/null +++ b/src/ArmChair.Core/Middleware/Commit/CommitPipeline.cs @@ -0,0 +1,68 @@ +namespace ArmChair.Middleware.Commit +{ + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Commands; + using EntityManagement; + using IdManagement; + using InSession; + using Processes.Commit; + using Tracking; + + /// + /// pipeline which will commit changes to the database + /// + public class CommitPipeline + { + private readonly CouchDb _couchDb; + private readonly IIdManager _idManager; + private readonly IRevisionAccessor _revisionAccessor; + + + public CommitPipeline( + CouchDb couchDb, + IIdManager idManager, + IRevisionAccessor revisionAccessor) + { + _couchDb = couchDb; + _idManager = idManager; + _revisionAccessor = revisionAccessor; + } + + +// /// +// /// register any task to be executed after commiting an item. +// /// +// /// func which will be used to create the task +// public void RegisterCommitAction(Func> createAction) +// { +// //_postProcessTasks.Add(createTask); +// } + + public virtual async Task Process(ISessionCache sessionCache, ITrackingProvider tracking) + { + //var taskCtx = new CreateTaskContext(_couchDb, _idManager, _revisionAccessor, sessionCache); + + //setup the pipeline + var pipe = new Middleware>(); + pipe.Use(new TrackingAction(tracking)); + pipe.Use(new SessionAction(sessionCache)); + pipe.Use(new CommitToDatabaseAction(_couchDb, _revisionAccessor)); + + //setup the bulk update context + var bulkContexts = sessionCache.Entries.Select(entry => + { + var bulkCtx = new CommitContext() + { + ActionType = entry.Action, + Entity = entry.Instance, + Key = entry.Key + }; + return bulkCtx; + }); + + await pipe.Execute(bulkContexts); + } + } +} \ No newline at end of file diff --git a/src/ArmChair.Core/Middleware/Commit/CommitToDatabaseAction.cs b/src/ArmChair.Core/Middleware/Commit/CommitToDatabaseAction.cs new file mode 100644 index 0000000..26fac90 --- /dev/null +++ b/src/ArmChair.Core/Middleware/Commit/CommitToDatabaseAction.cs @@ -0,0 +1,78 @@ +namespace ArmChair.Middleware.Commit +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Commands; + using EntityManagement; + using InSession; + using Processes.Commit; + + /// + /// this will send the correct command which will update the database + /// + public class CommitToDatabaseAction : IAction> + { + private readonly CouchDb _couchDb; + private readonly IRevisionAccessor _revisionAccessor; + + public CommitToDatabaseAction(CouchDb couchDb, IRevisionAccessor revisionAccessor) + { + _couchDb = couchDb; + _revisionAccessor = revisionAccessor; + } + + public Task Execute(IEnumerable items, Next> next) + { + items = items.ToList(); //ensure 1 iteration over list. (tasks to run once) + + //do not call the db if we have no items to transact on. + if (!items.Any()) return Task.CompletedTask; + + //setup the items for the database commit. + var entityUpdates = new Dictionary(); + var docRequests = new List(); + + foreach (var bulkContext in items) + { + var entry = new BulkDocRequest + { + Content = bulkContext.Entity, + Id = bulkContext.Key.CouchDbId + }; + + switch (bulkContext.ActionType) + { + case ActionType.Add: + break; + case ActionType.Update: + break; + case ActionType.Delete: + entry.Rev = (string) _revisionAccessor.GetRevision(bulkContext.Entity); + entry.Delete = true; + break; + default: + throw new ArgumentOutOfRangeException(); + } + + docRequests.Add(entry); + entityUpdates.Add(entry.Id, bulkContext.Entity); + } + + //apply the commit + var request = new BulkDocsRequest {Docs = docRequests}; + var updates = _couchDb.BulkApplyChanges(request); + + //update any revisions! + foreach (var update in updates) + { + var entity = entityUpdates[update.Id]; + _revisionAccessor.SetRevision(entity, update.Rev); + } + + return Task.CompletedTask; + + } + } +} \ No newline at end of file diff --git a/src/ArmChair.Core/Middleware/Commit/SessionAction.cs b/src/ArmChair.Core/Middleware/Commit/SessionAction.cs new file mode 100644 index 0000000..580197b --- /dev/null +++ b/src/ArmChair.Core/Middleware/Commit/SessionAction.cs @@ -0,0 +1,44 @@ +namespace ArmChair.Middleware.Commit +{ + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using InSession; + using Processes.Commit; + + /// + /// updates the cache accordingly on a db commit. + /// + public class SessionAction : IAction> + { + private readonly ISessionCache _sessionCache; + + public SessionAction(ISessionCache sessionCache) + { + _sessionCache = sessionCache; + } + + public async Task Execute(IEnumerable context, Next> next) + { + var items = context.ToList(); + + await next(items); + + foreach (var item in items) + { + switch (item.ActionType) + { + case ActionType.Update: + continue; + case ActionType.Add: + var entry = _sessionCache[item.Key]; + entry.Action = ActionType.Update; + break; + default: + _sessionCache.Remove(item.Key); + break; + } + } + } + } +} \ No newline at end of file diff --git a/src/ArmChair.Core/Middleware/Commit/TrackingAction.cs b/src/ArmChair.Core/Middleware/Commit/TrackingAction.cs new file mode 100644 index 0000000..68441f9 --- /dev/null +++ b/src/ArmChair.Core/Middleware/Commit/TrackingAction.cs @@ -0,0 +1,73 @@ +// Copyright 2014 - dbones.co.uk (David Rundle) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace ArmChair.Middleware.Commit +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using InSession; + using Processes.Commit; + using Tracking; + using ArmChair.Processes; + + /// + /// filters out entities which do not require updates to the db. + /// + public class TrackingAction : IAction> + { + private readonly ITrackingProvider _tracking; + + public TrackingAction(ITrackingProvider tracking) + { + _tracking = tracking; + } + + + public async Task Execute(IEnumerable context, Next> next) + { + bool AddOrRemove(CommitContext commitContext) => commitContext.ActionType != ActionType.Update; + + bool UpdateHasChanges(CommitContext commitContext) => commitContext.ActionType == ActionType.Update + && _tracking.HasChanges(commitContext.Entity); + + var toCommit = context + .Where(x => AddOrRemove(x) || UpdateHasChanges(x)).ToList(); + + + await next(toCommit); + + + foreach (var item in toCommit) + { + switch (item.ActionType) + { + case ActionType.Add: + _tracking.TrackInstance(item.Entity); + break; + case ActionType.Update: + _tracking.Reset(item.Entity); + break; + case ActionType.Delete: + _tracking.CeaseTracking(item.Entity); + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } + } + } +} \ No newline at end of file diff --git a/src/ArmChair.Core/Middleware/GetNextFactory.cs b/src/ArmChair.Core/Middleware/GetNextFactory.cs new file mode 100644 index 0000000..7842640 --- /dev/null +++ b/src/ArmChair.Core/Middleware/GetNextFactory.cs @@ -0,0 +1,53 @@ +namespace ArmChair.Middleware +{ + using System.Collections.Generic; + using System.Threading.Tasks; + + internal class GetNextFactory + { + private readonly IEnumerator _enumerator; + + public GetNextFactory(IEnumerator enumerator) + { + _enumerator = enumerator; + } + + public Next GetNext() + { + if (!_enumerator.MoveNext()) + { + return ctx => Task.FromResult(default(TOut)); + } + + var pipedType = _enumerator.Current; + + Task Next(TIn ctx) + { + var middleware = (IAction) pipedType.GivenInstance; + return middleware.Execute(ctx, GetNext()); + } + + return Next; + + } + + public Next GetNext() + { + if (!_enumerator.MoveNext()) + { + return ctx => Task.CompletedTask; + } + + var pipedType = _enumerator.Current; + + Task Next(TContext ctx) + { + var middleware = (IAction) pipedType.GivenInstance; + return middleware.Execute(ctx, GetNext()); + } + + return Next; + + } + } +} \ No newline at end of file diff --git a/src/ArmChair.Core/Middleware/IAction.cs b/src/ArmChair.Core/Middleware/IAction.cs new file mode 100644 index 0000000..5d6483f --- /dev/null +++ b/src/ArmChair.Core/Middleware/IAction.cs @@ -0,0 +1,14 @@ +namespace ArmChair.Middleware +{ + using System.Threading.Tasks; + + public interface IAction + { + Task Execute(T context, Next next); + } + + public interface IAction + { + Task Execute(TIn context, Next next); + } +} \ No newline at end of file diff --git a/src/ArmChair.Core/Middleware/Load/LoadFromDatabaseAction.cs b/src/ArmChair.Core/Middleware/Load/LoadFromDatabaseAction.cs new file mode 100644 index 0000000..a8ea41b --- /dev/null +++ b/src/ArmChair.Core/Middleware/Load/LoadFromDatabaseAction.cs @@ -0,0 +1,46 @@ +// Copyright 2014 - dbones.co.uk (David Rundle) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace ArmChair.Middleware.Load +{ + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Commands; + using Processes.Load; + + /// + /// load a single item from the database + /// + public class LoadFromDatabaseAction : IAction> + { + private readonly CouchDb _couchDb; + + public LoadFromDatabaseAction(CouchDb couchDb) + { + _couchDb = couchDb; + } + + public Task Execute(IEnumerable context, Next> next) + { + var item = context.FirstOrDefault(); + + if (item == null || item.LoadedFromCache) return Task.CompletedTask; + var entity = _couchDb.LoadEntity(item.Key.ToString(), item.Type); + item.Entity = entity; + + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/src/ArmChair.Core/Middleware/Load/LoadManyFromDatabaseAction.cs b/src/ArmChair.Core/Middleware/Load/LoadManyFromDatabaseAction.cs new file mode 100644 index 0000000..ac86c75 --- /dev/null +++ b/src/ArmChair.Core/Middleware/Load/LoadManyFromDatabaseAction.cs @@ -0,0 +1,46 @@ +namespace ArmChair.Middleware.Load +{ + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Commands; + using EntityManagement; + using IdManagement; + using Processes.Load; + + /// + /// load a number of items from the database using the ids. + /// + public class LoadManyFromDatabaseAction : IAction> + { + private readonly CouchDb _couchDb; + private readonly IIdManager _idManager; + private readonly IIdAccessor _idAccessor; + + public LoadManyFromDatabaseAction(CouchDb couchDb, IIdManager idManager, IIdAccessor idAccessor) + { + _couchDb = couchDb; + _idManager = idManager; + _idAccessor = idAccessor; + } + + public Task Execute(IEnumerable context, Next> next) + { + var items = context.ToList(); //ensure 1 iteration over list. (tasks to run once) + + //we do not want to load items which have been loaded from the cache + var toLoad = items.Where(x => !x.LoadedFromCache).ToDictionary(loadContext => loadContext.Key.CouchDbId); + var allDocs = _couchDb.LoadAllEntities(new AllDocsRequest() {Keys = toLoad.Keys}); + + //using the loaded docs, update the context entries. + foreach (var entity in allDocs.Rows.Select(x => x.Doc)) + { + var id = _idAccessor.GetId(entity); + var key = _idManager.GetFromId(entity.GetType(), id); + toLoad[key.CouchDbId].Entity = entity; + } + + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/src/ArmChair.Core/Middleware/Load/LoadPipeline.cs b/src/ArmChair.Core/Middleware/Load/LoadPipeline.cs new file mode 100644 index 0000000..90d4287 --- /dev/null +++ b/src/ArmChair.Core/Middleware/Load/LoadPipeline.cs @@ -0,0 +1,84 @@ +namespace ArmChair.Middleware.Load +{ + using System.Collections; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Commands; + using EntityManagement; + using IdManagement; + using InSession; + using Processes.Load; + using Tracking; + + public class LoadPipeline + { + private readonly CouchDb _couchDb; + private readonly IIdManager _idManager; + private readonly IIdAccessor _idAccessor; + private readonly IRevisionAccessor _revisionAccessor; + + public LoadPipeline(CouchDb couchDb, + IIdManager idManager, + IIdAccessor idAccessor, + IRevisionAccessor revisionAccessor) + { + _couchDb = couchDb; + _idManager = idManager; + _idAccessor = idAccessor; + _revisionAccessor = revisionAccessor; + } + + +// /// +// /// add a task to be execute after items are loaded from the database +// /// +// /// note this is a function that create your task in a context +// public void RegisterPostLoadTask(Func> createTask) +// { +// _postLoadTasks.Add(createTask); +// } + + public async Task LoadOne(object id, ISessionCache sessionCache, ITrackingProvider tracking) + where T : class + { + var result = await Load(new[] {id}, sessionCache, tracking, new LoadFromDatabaseAction(_couchDb)); + return result.FirstOrDefault(); + } + + public Task> LoadMany(IEnumerable ids, ISessionCache sessionCache, ITrackingProvider tracking) + where T : class + { + return Load(ids, sessionCache, tracking, + new LoadManyFromDatabaseAction(_couchDb, _idManager, _idAccessor)); + } + + protected virtual async Task> Load(IEnumerable ids, ISessionCache sessionCache, + ITrackingProvider tracking, IAction> loadTask) where T : class + { + //var taskCtx = new CreateTaskContext(_couchDb, _idManager, _revisionAccessor, sessionCache); + + //setup the pipeline + var pipe = new Middleware>(); + pipe.Use(new TrackingAction(tracking)); + pipe.Use(new SessionAction(sessionCache)); + pipe.Use(loadTask); + + //setup the load context + var type = typeof(T); + var loadContexts = ids.Cast().Select(id => + { + var idKey = _idManager.GetFromId(type, id); + return new LoadContext() + { + Key = idKey, + Type = type + }; + }).ToList(); + + + await pipe.Execute(loadContexts); + return loadContexts.Select(x => x.Entity).Where(x => x != null).Cast(); + } + } +} \ No newline at end of file diff --git a/src/ArmChair.Core/Middleware/Load/SessionAction.cs b/src/ArmChair.Core/Middleware/Load/SessionAction.cs new file mode 100644 index 0000000..30c8b19 --- /dev/null +++ b/src/ArmChair.Core/Middleware/Load/SessionAction.cs @@ -0,0 +1,57 @@ +namespace ArmChair.Middleware.Load +{ + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using InSession; + using Processes.Load; + + public class SessionAction : IAction> where T : LoadContext + { + private readonly ISessionCache _sessionCache; + + public SessionAction(ISessionCache sessionCache) + { + _sessionCache = sessionCache; + } + + public async Task Execute(IEnumerable context, Next> next) + { + var items = context + .Where(x => x.Entity != null) + .Select(x => + { + var entry = _sessionCache[x.Key]; + + //not in cache + if (entry == null) return x; + + //the item has been removed inside the session. + if (entry.Action == ActionType.Delete) + { + return null; + } + + x.Entity = entry.Instance; + x.LoadedFromCache = true; + return x; + }) + .Where(x => x != null) //.ToList(); + .Where(x => !x.LoadedFromCache).ToList(); + + await next(items); + + foreach (var item in items.Where(x => x.Entity != null)) + { + var sessionEntry = new SessionEntry() + { + Action = ActionType.Update, + Instance = item.Entity, + Key = item.Key + }; + + _sessionCache.Attach(sessionEntry); + } + } + } +} \ No newline at end of file diff --git a/src/ArmChair.Core/Middleware/Load/TrackingAction.cs b/src/ArmChair.Core/Middleware/Load/TrackingAction.cs new file mode 100644 index 0000000..5321479 --- /dev/null +++ b/src/ArmChair.Core/Middleware/Load/TrackingAction.cs @@ -0,0 +1,33 @@ +namespace ArmChair.Middleware.Load +{ + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Processes.Load; + using Tracking; + + public class TrackingAction : IAction> where T : LoadContext + { + private readonly ITrackingProvider _tracking; + + public TrackingAction(ITrackingProvider tracking) + { + _tracking = tracking; + } + + public async Task Execute(IEnumerable context, Next> next) + { + var items = context.ToList(); + + await next(items); + + items.Where(x => !x.LoadedFromCache) + .Where(x => x.Entity != null); + + foreach (var item in items) + { + _tracking.TrackInstance(item.Entity); + } + } + } +} \ No newline at end of file diff --git a/src/ArmChair.Core/Middleware/Middleware.cs b/src/ArmChair.Core/Middleware/Middleware.cs new file mode 100644 index 0000000..cfa5b81 --- /dev/null +++ b/src/ArmChair.Core/Middleware/Middleware.cs @@ -0,0 +1,43 @@ +namespace ArmChair.Middleware +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + + public class Middleware + { + private readonly List _pipedTypes = new List(); + + public void Use(IAction action) + { + _pipedTypes.Add(new PipeItem(action)); + } + + + public async Task Execute(TContext context) + { + var enumerator = _pipedTypes.GetEnumerator(); + var factory = new GetNextFactory(enumerator); + await factory.GetNext()(context); + } + } + + + public class Middleware + { + private readonly List _pipedTypes = new List(); + + public void Use(IAction action) + { + _pipedTypes.Add(new PipeItem(action)); + } + + + public async Task Execute(TIn context) + { + var enumerator = _pipedTypes.GetEnumerator(); + var factory = new GetNextFactory(enumerator); + return await factory.GetNext()(context); + } + } +} \ No newline at end of file diff --git a/src/ArmChair.Core/Middleware/Next.cs b/src/ArmChair.Core/Middleware/Next.cs new file mode 100644 index 0000000..1dd35f0 --- /dev/null +++ b/src/ArmChair.Core/Middleware/Next.cs @@ -0,0 +1,8 @@ +namespace ArmChair.Middleware +{ + using System.Threading.Tasks; + + public delegate Task Next(T context); + + public delegate Task Next(TIn context); +} \ No newline at end of file diff --git a/src/ArmChair.Core/Middleware/PipeItem.cs b/src/ArmChair.Core/Middleware/PipeItem.cs new file mode 100644 index 0000000..aa5c2e7 --- /dev/null +++ b/src/ArmChair.Core/Middleware/PipeItem.cs @@ -0,0 +1,26 @@ +namespace ArmChair.Middleware +{ + using System; + + internal class PipeItem + { + public PipeItem(Type type) + { + Type = type; + PipeItemType = PipeItemType.Type; + } + + public PipeItem(object givenInstance) + { + GivenInstance = givenInstance; + PipeItemType = PipeItemType.Instance; + } + + + public Type Type { get; } + + public object GivenInstance { get; } + + public PipeItemType PipeItemType { get; } + } +} \ No newline at end of file diff --git a/src/ArmChair.Core/Middleware/PipeItemType.cs b/src/ArmChair.Core/Middleware/PipeItemType.cs new file mode 100644 index 0000000..984d010 --- /dev/null +++ b/src/ArmChair.Core/Middleware/PipeItemType.cs @@ -0,0 +1,8 @@ +namespace ArmChair.Middleware +{ + public enum PipeItemType + { + Type, + Instance + } +} \ No newline at end of file diff --git a/src/ArmChair.Core/Middleware/Query/MongoQueryFromDatabaseAction.cs b/src/ArmChair.Core/Middleware/Query/MongoQueryFromDatabaseAction.cs new file mode 100644 index 0000000..6943f38 --- /dev/null +++ b/src/ArmChair.Core/Middleware/Query/MongoQueryFromDatabaseAction.cs @@ -0,0 +1,74 @@ +// Copyright 2014 - dbones.co.uk (David Rundle) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace ArmChair.Middleware.Query +{ + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Commands; + using EntityManagement; + using IdManagement; + using Load; + using Processes.Load; + using Processes.Query; + using Tasks; + + /// + /// query the db using the mongo query object + /// + public class MongoQueryFromDatabaseAction : IAction> + { + private readonly CouchDb _couchDb; + private readonly IIdManager _idManager; + private readonly IIdAccessor _idAccessor; + + public MongoQueryFromDatabaseAction(CouchDb couchDb, IIdManager idManager, IIdAccessor idAccessor) + { + _couchDb = couchDb; + _idManager = idManager; + _idAccessor = idAccessor; + } + + public Task> Execute(QueryContext context, + Next> next) + { + var ctx = context; + var q = ctx.Query; + var query = new MongoQueryRequest + { + Selector = q.Selector, + Limit = q.Limit, + Skip = q.Skip, + Sort = q.Sort + }; + + var allDocs = _couchDb.MongoQuery(query); + + //using the loaded docs, update the context entries. + var results = + (from entity in allDocs.Docs + let id = _idAccessor.GetId(entity) + let key = _idManager.GetFromId(entity.GetType(), id) + select new LoadContext() + { + Entity = entity, + Key = key, + Type = entity.GetType() + }).ToList(); + + return Task.FromResult((IEnumerable) results); + } + } +} \ No newline at end of file diff --git a/src/ArmChair.Core/Middleware/Query/OverrideUsingCacheAction.cs b/src/ArmChair.Core/Middleware/Query/OverrideUsingCacheAction.cs new file mode 100644 index 0000000..26e4a80 --- /dev/null +++ b/src/ArmChair.Core/Middleware/Query/OverrideUsingCacheAction.cs @@ -0,0 +1,43 @@ +namespace ArmChair.Middleware.Query +{ + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using InSession; + using Processes.Load; + using Processes.Query; + + /// + /// when running a query, we will load the object from the database + /// this task looks at the cached version and uses that, as we assume they has been + /// a change to the object which we cannot lose. + /// + /// type this task is operating on + public class OverrideUsingCacheAction : IAction> where T : LoadContext + { + private readonly ISessionCache _sessionCache; + + public OverrideUsingCacheAction(ISessionCache sessionCache) + { + _sessionCache = sessionCache; + } + + + public async Task> Execute(QueryContext context, Next> next) + { + var results = (await next(context)).ToList(); + + foreach (var result in results) + { + //we favour the cached version over the db in this context. + var entry = _sessionCache[result.Key]; + if (entry == null) continue; + + result.Entity = entry.Instance; + result.LoadedFromCache = true; + } + + return results; + } + } +} \ No newline at end of file diff --git a/src/ArmChair.Core/Middleware/Query/QueryPipeline.cs b/src/ArmChair.Core/Middleware/Query/QueryPipeline.cs new file mode 100644 index 0000000..09e7088 --- /dev/null +++ b/src/ArmChair.Core/Middleware/Query/QueryPipeline.cs @@ -0,0 +1,59 @@ +namespace ArmChair.Middleware.Query +{ + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Commands; + using EntityManagement; + using IdManagement; + using InSession; + using Processes.Load; + using Processes.Query; + using Tracking; + + /// + /// pipeline for querying against couchdb + /// + public class QueryPipeline + { + private readonly CouchDb _couchDb; + private readonly IIdManager _idManager; + private readonly IIdAccessor _idAccessor; + private readonly IRevisionAccessor _revisionAccessor; + + + public QueryPipeline(CouchDb couchDb, + IIdManager idManager, + IIdAccessor idAccessor, + IRevisionAccessor revisionAccessor) + { + _couchDb = couchDb; + _idManager = idManager; + _idAccessor = idAccessor; + _revisionAccessor = revisionAccessor; + } + + public virtual async Task> Query(MongoQuery query, ISessionCache sessionCache, ITrackingProvider tracking) + where T : class + { + + var pipe = new Middleware>(); + pipe.Use(new TrackingAction(tracking)); + pipe.Use(new UpdateCacheAction(sessionCache)); + pipe.Use(new OverrideUsingCacheAction(sessionCache)); + pipe.Use(new MongoQueryFromDatabaseAction(_couchDb, _idManager, _idAccessor)); + + //setup the load context + var type = typeof(T); + + var ctx = new QueryContext() + { + Query = query, + Type = type + }; + + var results = await pipe.Execute(ctx); + return results.Select(x => x.Entity).Where(x => x != null).Cast(); + } + } +} \ No newline at end of file diff --git a/src/ArmChair.Core/Middleware/Query/TrackingAction.cs b/src/ArmChair.Core/Middleware/Query/TrackingAction.cs new file mode 100644 index 0000000..292f7b5 --- /dev/null +++ b/src/ArmChair.Core/Middleware/Query/TrackingAction.cs @@ -0,0 +1,34 @@ +namespace ArmChair.Middleware.Query +{ + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Processes.Load; + using Processes.Query; + using Tracking; + + public class TrackingAction : IAction> + { + private readonly ITrackingProvider _tracking; + + public TrackingAction(ITrackingProvider tracking) + { + _tracking = tracking; + } + + public async Task> Execute(QueryContext context, + Next> next) + { + var items = (await next(context)).ToList(); + + foreach (var item in items) + { + if (item.LoadedFromCache) continue; + + _tracking.TrackInstance(item.Entity); + } + + return items; + } + } +} \ No newline at end of file diff --git a/src/ArmChair.Core/Middleware/Query/UpdateCacheAction.cs b/src/ArmChair.Core/Middleware/Query/UpdateCacheAction.cs new file mode 100644 index 0000000..221b2d6 --- /dev/null +++ b/src/ArmChair.Core/Middleware/Query/UpdateCacheAction.cs @@ -0,0 +1,42 @@ +namespace ArmChair.Middleware.Query +{ + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using InSession; + using Processes.Load; + using Processes.Query; + + public class UpdateCacheAction : IAction> + { + private readonly ISessionCache _sessionCache; + + public UpdateCacheAction(ISessionCache sessionCache) + { + _sessionCache = sessionCache; + } + + public async Task> Execute(QueryContext context, + Next> next) + { + var items = (await next(context)).ToList(); + + foreach (var item in items) + { + if (item.LoadedFromCache) continue; + if (item.Entity == null) continue; + + var sessionEntry = new SessionEntry() + { + Action = ActionType.Update, + Instance = item.Entity, + Key = item.Key + }; + + _sessionCache.Attach(sessionEntry); + } + + return items; + } + } +} \ No newline at end of file diff --git a/src/ArmChair.Core/Processes/Commit/CommitPayLoad.cs b/src/ArmChair.Core/Processes/Commit/CommitPayLoad.cs deleted file mode 100644 index 1b84443..0000000 --- a/src/ArmChair.Core/Processes/Commit/CommitPayLoad.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2014 - dbones.co.uk (David Rundle) -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -namespace ArmChair.Processes.Commit -{ - using System; - using System.Collections.Generic; - - [Obsolete("to be removed", true)] - public class CommitPayLoad - { - public IEnumerable Deletes { get; set; } - public IEnumerable Updates { get; set; } - public IEnumerable Creates { get; set; } - } -} \ No newline at end of file diff --git a/src/ArmChair.Core/Serialization/Newton/AllDocsResponseConverter.cs b/src/ArmChair.Core/Serialization/Newton/AllDocsResponseConverter.cs index 8399797..72b8c80 100644 --- a/src/ArmChair.Core/Serialization/Newton/AllDocsResponseConverter.cs +++ b/src/ArmChair.Core/Serialization/Newton/AllDocsResponseConverter.cs @@ -56,10 +56,17 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist Doc = x, }); + //newton json represents null as an empty string. + var offSet = jsonContent["offset"]; + if (offSet?.ToString() == "") + { + offSet = 0; + } + return new AllDocsResponse() { Rows = rows, - Offset = (int)(jsonContent["offset"] ?? 0), + Offset = (int)(offSet ?? 0), TotalRows = (int)jsonContent["total_rows"] }; } diff --git a/src/ArmChair.Core/Serialization/Newton/BulkDocsRequestConverter.cs b/src/ArmChair.Core/Serialization/Newton/BulkDocsRequestConverter.cs index 26b79a7..9808a1e 100644 --- a/src/ArmChair.Core/Serialization/Newton/BulkDocsRequestConverter.cs +++ b/src/ArmChair.Core/Serialization/Newton/BulkDocsRequestConverter.cs @@ -37,6 +37,8 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s var request = (BulkDocsRequest)value; writer.WriteStartObject(); + //writer.WritePropertyName("all_or_nothing"); + //writer.WriteValue(true); writer.WritePropertyName("docs"); writer.WriteStartArray(); foreach (var doc in request.Docs) diff --git a/src/ArmChair.Core/Serialization/Newton/BulkDocsResponseConverter.cs b/src/ArmChair.Core/Serialization/Newton/BulkDocsResponseConverter.cs index d5fa844..c8f7028 100644 --- a/src/ArmChair.Core/Serialization/Newton/BulkDocsResponseConverter.cs +++ b/src/ArmChair.Core/Serialization/Newton/BulkDocsResponseConverter.cs @@ -39,7 +39,9 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist { Id = (string)x["id"], Rev = (string)x["rev"], - Ok = (bool)x["ok"] + Ok = (bool?)x["ok"], + Error = (string)x["error"], + Reason = (string)x["reason"] }).ToList(); } diff --git a/src/ArmChair.Core/Utils/Copying/CopyMeta.cs b/src/ArmChair.Core/Utils/Copying/CopyMeta.cs index 2cc5072..3b11563 100644 --- a/src/ArmChair.Core/Utils/Copying/CopyMeta.cs +++ b/src/ArmChair.Core/Utils/Copying/CopyMeta.cs @@ -111,7 +111,7 @@ public virtual void Compile() /// private void AddTargetToCopy(ICopyToTarget copyToTarget) { - copyToTarget.Congfigure(_copier); + copyToTarget.Configure(_copier); _copyActions.Add(copyToTarget); } diff --git a/src/ArmChair.Core/Utils/Copying/CopyToTarget.cs b/src/ArmChair.Core/Utils/Copying/CopyToTarget.cs index 0282099..c74175b 100644 --- a/src/ArmChair.Core/Utils/Copying/CopyToTarget.cs +++ b/src/ArmChair.Core/Utils/Copying/CopyToTarget.cs @@ -18,7 +18,7 @@ public abstract class CopyToTarget : ICopyToTarget protected ShadowCopier Copier; public abstract void Copy(object source, object destination); - public virtual void Congfigure(ShadowCopier copier) + public virtual void Configure(ShadowCopier copier) { Copier = copier; } diff --git a/src/ArmChair.Core/Utils/Copying/ICopyToTarget.cs b/src/ArmChair.Core/Utils/Copying/ICopyToTarget.cs index ab58d53..bd57a48 100644 --- a/src/ArmChair.Core/Utils/Copying/ICopyToTarget.cs +++ b/src/ArmChair.Core/Utils/Copying/ICopyToTarget.cs @@ -26,6 +26,6 @@ public interface ICopyToTarget void Copy(object source, object destination); - void Congfigure(ShadowCopier copier); + void Configure(ShadowCopier copier); } } \ No newline at end of file diff --git a/src/ArmChair.Core/Utils/Hashing/Sha1Hash.cs b/src/ArmChair.Core/Utils/Hashing/Sha1Hash.cs index 7b10a30..b9d1ca5 100644 --- a/src/ArmChair.Core/Utils/Hashing/Sha1Hash.cs +++ b/src/ArmChair.Core/Utils/Hashing/Sha1Hash.cs @@ -1,15 +1,12 @@ namespace ArmChair.Utils.Hashing { - - /* - using System; using System.Security.Cryptography; using System.Text; /// - /// simple SHA1 hashing algoithm. + /// simple SHA1 hashing algorithm. /// public class Sha1Hash : IHash { @@ -23,10 +20,4 @@ public string ComputeHash(string content) } } } - - */ - - - - } \ No newline at end of file diff --git a/src/ArmChair.Core/Utils/Logging/ILogger.cs b/src/ArmChair.Core/Utils/Logging/ILogger.cs index 5aef838..811429d 100644 --- a/src/ArmChair.Core/Utils/Logging/ILogger.cs +++ b/src/ArmChair.Core/Utils/Logging/ILogger.cs @@ -2,8 +2,6 @@ namespace ArmChair.Utils.Logging { - using System.Diagnostics; - public interface ILogger { void Log(Func message); diff --git a/src/ArmChair.Core/Utils/TypeExtensions.cs b/src/ArmChair.Core/Utils/TypeExtensions.cs index 3edbc56..bd23c06 100644 --- a/src/ArmChair.Core/Utils/TypeExtensions.cs +++ b/src/ArmChair.Core/Utils/TypeExtensions.cs @@ -87,7 +87,7 @@ public static Type GetUnderlyingType(this MemberInfo member) ); #endif -#if NETSTANDARD1_6 || NET45 +#if NETSTANDARD1_6 || NET452 switch (member.MemberType) { case MemberTypes.Event: diff --git a/src/ArmChair.Core/Utils/TypeMeta.cs b/src/ArmChair.Core/Utils/TypeMeta.cs index e571e49..6a888cf 100644 --- a/src/ArmChair.Core/Utils/TypeMeta.cs +++ b/src/ArmChair.Core/Utils/TypeMeta.cs @@ -52,7 +52,7 @@ public TypeMeta(Type type) .SelectMany(t => t.GetTypeInfo().DeclaredFields) .Where(x => !x.IsStatic); #endif -#if NETSTANDARD1_6 || NET45 +#if NETSTANDARD1_6 || NET452 .SelectMany(t => t.GetTypeInfo().GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)); #endif _isAbstract = _typeInfo.IsAbstract; @@ -63,7 +63,7 @@ public TypeMeta(Type type) #if NETSTANDARD1_1 _typeInfo.DeclaredConstructors.Where(x => !x.IsStatic) #endif -#if NETSTANDARD1_6 || NET45 +#if NETSTANDARD1_6 || NET452 _typeInfo.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) #endif .FirstOrDefault(x => !x.GetParameters().Any()); diff --git a/src/ArmChair.Tests/Core/SavingTests.cs b/src/ArmChair.Tests/Core/SavingTests.cs index 2a75160..9017a6d 100644 --- a/src/ArmChair.Tests/Core/SavingTests.cs +++ b/src/ArmChair.Tests/Core/SavingTests.cs @@ -16,6 +16,7 @@ namespace ArmChair.Tests.Core using System.Collections.Generic; using System.Linq; using Domain.Sample1; + using Exceptions; using NUnit.Framework; public class SavingTests : TestCase @@ -80,6 +81,46 @@ public void Save_nothing() Assert.Pass(); } + + + [Test] + public void Concurrancy_Load_and_save() + { + string id; + string rev; + using (var session = Database.CreateSession()) + { + var author = new Person("dave"); + session.Add(author); + + id = author.Id; + + session.Commit(); + + rev = author.Rev; + } + + using (var session1 = Database.CreateSession()) + using (var session2 = Database.CreateSession()) + { + var p1 = session1.GetById(id); + var p2 = session2.GetById(id); + var p3 = new Person("chan"); + session2.Add(p3); + + p1.Name = "bob"; + p2.Name = "bobby"; + + session1.Commit(); + + var result = Assert.Throws(() => session2.Commit(), "should have a conflict"); + Assert.IsTrue(result.Exceptions.Any()); + var ex = result.Exceptions.First(); + Assert.IsTrue(ex is ConflictException); + Assert.AreEqual(ex.Id, id); + } + + } } } \ No newline at end of file diff --git a/src/ArmChair.Tests/Linq/BasicTests.cs b/src/ArmChair.Tests/Linq/BasicTests.cs index b69e2b5..2522532 100644 --- a/src/ArmChair.Tests/Linq/BasicTests.cs +++ b/src/ArmChair.Tests/Linq/BasicTests.cs @@ -58,6 +58,37 @@ public void Single_where_name_equals() Assert.IsTrue(people.Any(x => x.Id == "4")); } + + [Test] + public void load_item_twice_in_separate_sessions_it_be_the_same() + { + + using (var session = Database.CreateSession()) + { + var person = new Person("chan"); + session.Add(person); + session.Commit(); + } + Person p1; + Person p2; + using (var session = Database.CreateSession()) + { + p1 = session.Query().FirstOrDefault(x => x.Name == "chan"); + session.Commit(); + } + + using (var session = Database.CreateSession()) + { + p2 = session.Query().FirstOrDefault(x => x.Name == "chan"); + session.Commit(); + } + + Assert.IsFalse(p1 == p2, "these are the same object but difference instances"); + Assert.AreEqual(p1.Name, p2.Name); + Assert.AreEqual(p1.Id, p2.Id); + Assert.AreEqual(p1.Rev, p2.Rev); + } + } } \ No newline at end of file