├── architecture.png ├── src ├── Graph.API │ ├── Graph │ │ ├── GraphQLUserContext.cs │ │ ├── GraphQLRequest.cs │ │ ├── InputValidationRule.cs │ │ ├── GraphQLSettings.cs │ │ └── GraphQLMiddleware.cs │ ├── Program.cs │ ├── Graph.API.csproj │ ├── Properties │ │ └── launchSettings.json │ └── Startup.cs ├── Graph.Application │ ├── Commands │ │ ├── CommandBase.cs │ │ ├── Task │ │ │ ├── RemoveTaskCommand.cs │ │ │ ├── UpdateDeadlineCommand.cs │ │ │ ├── ChangeAssigneeCommand.cs │ │ │ ├── UpdateTaskStatusCommand.cs │ │ │ ├── UpdateTaskInfoCommand.cs │ │ │ └── AddTaskCommand.cs │ │ ├── User │ │ │ ├── AddUserCommand.cs │ │ │ └── UpdateUserInfoCommand.cs │ │ └── Project │ │ │ ├── AddUserProjectCommand.cs │ │ │ ├── AddProjectCommand.cs │ │ │ ├── RemoveUserProjectCommand.cs │ │ │ └── UpdateProjectInfoCommand.cs │ ├── Graph │ │ ├── Common │ │ │ ├── Pagination.cs │ │ │ ├── TaskStatusEnumType.cs │ │ │ ├── FilterType.cs │ │ │ ├── MutationResultType.cs │ │ │ ├── PaginationType.cs │ │ │ ├── EnumFilterType.cs │ │ │ ├── DateFilterType.cs │ │ │ ├── UpdateDescriptionInput.cs │ │ │ └── Filter.cs │ │ ├── Task │ │ │ ├── Types │ │ │ │ ├── Query │ │ │ │ │ ├── TaskUserType.cs │ │ │ │ │ ├── TaskProjectType.cs │ │ │ │ │ └── TaskType.cs │ │ │ │ └── Input │ │ │ │ │ ├── UpdateDeadlineInput.cs │ │ │ │ │ ├── ChangeAssigneeInput.cs │ │ │ │ │ ├── UpdateTaskStatusInput.cs │ │ │ │ │ ├── TaskFilterType.cs │ │ │ │ │ └── AddTaskInput.cs │ │ │ ├── Query │ │ │ │ └── TaskQuery.cs │ │ │ └── Mutation │ │ │ │ └── TaskMutation.cs │ │ ├── Project │ │ │ ├── Types │ │ │ │ ├── Query │ │ │ │ │ ├── ProjectUserType.cs │ │ │ │ │ ├── ProjectTaskType.cs │ │ │ │ │ └── ProjectType.cs │ │ │ │ └── Input │ │ │ │ │ ├── ProjectFilterType.cs │ │ │ │ │ ├── UserProjectInput.cs │ │ │ │ │ └── AddProjectInput.cs │ │ │ ├── Query │ │ │ │ └── ProjectQuery.cs │ │ │ └── Mutation │ │ │ │ └── ProjectMutation.cs │ │ ├── User │ │ │ ├── Types │ │ │ │ ├── Input │ │ │ │ │ ├── UserFilterType.cs │ │ │ │ │ ├── AddUserInput.cs │ │ │ │ │ └── EditUserInput.cs │ │ │ │ └── Query │ │ │ │ │ ├── UserProjectType.cs │ │ │ │ │ └── UserType.cs │ │ │ ├── Mutation │ │ │ │ └── UserMutation.cs │ │ │ └── Query │ │ │ │ └── UserQuery.cs │ │ └── GraphSchema.cs │ ├── MessageHandler │ │ ├── TaskProjectMessage.cs │ │ ├── ProjectUserMessage.cs │ │ └── BusMessageHandler.cs │ └── Graph.Application.csproj ├── Graph.CrossCutting │ ├── Interfaces │ │ ├── IModel.cs │ │ ├── IQueryModel.cs │ │ └── IDomain.cs │ ├── TaskStatusEnum.cs │ ├── Exceptions │ │ ├── ElementNotFoundException.cs │ │ ├── ValidationException.cs │ │ └── QueryArgumentException.cs │ ├── Graph.CrossCutting.csproj │ └── Extensions │ │ ├── GraphQL │ │ ├── FilterTypeEnum.cs │ │ ├── GraphFilter.cs │ │ ├── SchemaExtensions.cs │ │ ├── FilterUtils.cs │ │ └── ArgumentUtils.cs │ │ ├── DomainExtensions.cs │ │ └── CodeBaseExtensions.cs ├── Graph.Infrastructure │ ├── Database │ │ ├── DatabaseProvider.cs │ │ ├── Command │ │ │ ├── Interfaces │ │ │ │ ├── IUserRepository.cs │ │ │ │ ├── IUnitOfWork.cs │ │ │ │ ├── ITaskRepository.cs │ │ │ │ ├── IProjectRepository.cs │ │ │ │ └── IRepository.cs │ │ │ ├── Model │ │ │ │ ├── Status.cs │ │ │ │ ├── UserProject.cs │ │ │ │ ├── User.cs │ │ │ │ ├── Project.cs │ │ │ │ └── Task.cs │ │ │ ├── Repository │ │ │ │ ├── TaskRepository.cs │ │ │ │ ├── UserRepository.cs │ │ │ │ ├── ProjectRepository.cs │ │ │ │ └── Repository.cs │ │ │ ├── UnitOfWork.cs │ │ │ ├── ConnectionFactory.cs │ │ │ └── GraphContext.cs │ │ ├── Query │ │ │ ├── Model │ │ │ │ ├── Task │ │ │ │ │ ├── TaskUser.cs │ │ │ │ │ ├── TaskProject.cs │ │ │ │ │ └── Task.cs │ │ │ │ ├── Project │ │ │ │ │ ├── ProjectUser.cs │ │ │ │ │ ├── ProjectTask.cs │ │ │ │ │ └── Project.cs │ │ │ │ └── User │ │ │ │ │ ├── UserProject.cs │ │ │ │ │ └── User.cs │ │ │ ├── Manager │ │ │ │ ├── TaskManager.cs │ │ │ │ ├── UserManager.cs │ │ │ │ ├── ProjectManager.cs │ │ │ │ ├── IEntityManager.cs │ │ │ │ └── EntityManager.cs │ │ │ ├── IManager.cs │ │ │ ├── ManagerFactory.cs │ │ │ ├── InMemoryManager.cs │ │ │ ├── MongoManager.cs │ │ │ └── ElasticSearchManager.cs │ │ └── DatabaseConfiguration.cs │ ├── ServiceBus │ │ ├── BusTransport.cs │ │ ├── ISubscribe.cs │ │ ├── IServiceBus.cs │ │ ├── BusConfiguration.cs │ │ ├── Message.cs │ │ ├── BusCrendentials.cs │ │ └── MassTransitSB.cs │ ├── Graph.Infrastructure.csproj │ └── Migrations │ │ └── 20191125185945_Initial.cs ├── Graph.Domain │ ├── Model │ │ ├── DomainState.cs │ │ ├── User.cs │ │ ├── Task.cs │ │ └── Project.cs │ ├── Graph.Domain.csproj │ └── Validator │ │ ├── ProjectValidator.cs │ │ ├── UserValidator.cs │ │ └── TaskValidator.cs ├── GraphAPI │ └── appSettings.json ├── Graph.Domain.Service │ ├── Graph.Domain.Service.csproj │ ├── Mappings │ │ ├── StatusProfile.cs │ │ ├── UserProfile.cs │ │ ├── TaskProfile.cs │ │ └── ProjectProfile.cs │ └── CommandHandler │ │ ├── UserCommandHandler.cs │ │ └── ProjectCommandHandler.cs └── Graph.CrossCutting.Ioc │ └── Graph.CrossCutting.Ioc.csproj ├── .dockerignore ├── docker-compose-stack.yml ├── appveyor.yml ├── azure-pipelines.yml ├── Dockerfile ├── test └── Graph.Tests │ ├── Graph.Tests.csproj │ ├── Comparers.cs │ ├── Startup.cs │ ├── Integration │ ├── Mutation │ │ ├── UserMutationTest.cs │ │ ├── ProjectMutationTest.cs │ │ └── TaskMutationTest.cs │ └── Query │ │ ├── UserQueryTest.cs │ │ ├── TaskQueryTest.cs │ │ └── ProjectQueryTest.cs │ └── CommandHandler │ └── UserCommandHandlerTest.cs ├── docker-compose.yml └── GraphAPI.sln /architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gbauso/GraphAPI/HEAD/architecture.png -------------------------------------------------------------------------------- /src/Graph.API/Graph/GraphQLUserContext.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | 3 | namespace Graph.API 4 | { 5 | public class GraphQLUserContext 6 | { 7 | public ClaimsPrincipal User { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Graph.Application/Commands/CommandBase.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace Graph.Application.Commands 7 | { 8 | public abstract class CommandBase : IRequest 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Graph.CrossCutting/Interfaces/IModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Graph.CrossCutting.Interfaces 6 | { 7 | public interface IModel 8 | { 9 | public Guid Id { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/DatabaseProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Graph.Infrastructure.Database 2 | { 3 | public enum DatabaseProvider 4 | { 5 | POSTGRES = 1, 6 | MSSQL, 7 | ELASTICSEARCH, 8 | MONGODB, 9 | MOCK 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Graph.CrossCutting/Interfaces/IQueryModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Graph.CrossCutting.Interfaces 6 | { 7 | public interface IQueryModel 8 | { 9 | string Id { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/ServiceBus/BusTransport.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Graph.Infrastructure.ServiceBus 6 | { 7 | public enum BusTransport 8 | { 9 | RABBITMQ = 1, 10 | AZURE 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Graph.API/Graph/GraphQLRequest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | 3 | namespace Graph.API 4 | { 5 | public class GraphQLRequest 6 | { 7 | public string OperationName { get; set; } 8 | public string Query { get; set; } 9 | public JObject Variables { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /src/Graph.CrossCutting/Interfaces/IDomain.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Graph.CrossCutting.Interfaces 6 | { 7 | public interface IDomain 8 | { 9 | Guid Id { get; } 10 | void Validate(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Graph.Application/Commands/Task/RemoveTaskCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Graph.Application.Commands.Task 6 | { 7 | public class RemoveTaskCommand : CommandBase 8 | { 9 | public Guid Id { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/Common/Pagination.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Graph.Application.Graph.Common 6 | { 7 | public class Pagination 8 | { 9 | public int Skip { get; set; } 10 | public int Take { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Graph.CrossCutting/TaskStatusEnum.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Graph.CrossCutting 6 | { 7 | public enum TaskStatusEnum 8 | { 9 | BACKLOG = 1, 10 | IN_PROGRESS, 11 | TESTING, 12 | DONE 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Graph.Domain/Model/DomainState.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Graph.Domain.Model 6 | { 7 | public enum DomainState 8 | { 9 | NEW = 1, 10 | FROM_DB, 11 | ADD_RELATION, 12 | REMOVE_RELATION 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/ServiceBus/ISubscribe.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | 6 | namespace Graph.Infrastructure.ServiceBus 7 | { 8 | public interface ISubscribe 9 | { 10 | Task HandleMessage(Message message); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Command/Interfaces/IUserRepository.cs: -------------------------------------------------------------------------------- 1 | using Graph.Infrastructure.Database.Repository.Interfaces; 2 | using Graph.Infrastructure.Database.Command.Model; 3 | 4 | namespace Graph.Infrastructure.Database.Command.Interfaces 5 | { 6 | public interface IUserRepository : IRepository 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Query/Model/Task/TaskUser.cs: -------------------------------------------------------------------------------- 1 | using MongoDB.Bson.Serialization.Attributes; 2 | 3 | namespace Graph.Infrastructure.Database.Query.TaskSchema 4 | { 5 | public class TaskUser 6 | { 7 | public string Id { get; set; } 8 | 9 | [BsonElement("name")] 10 | public string Name { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/ServiceBus/IServiceBus.cs: -------------------------------------------------------------------------------- 1 | using MassTransit; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Graph.Infrastructure.ServiceBus 8 | { 9 | public interface IServiceBus 10 | { 11 | Task SendMessage(Message message); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Graph.Application/Commands/User/AddUserCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Graph.Application.Commands.User 6 | { 7 | public class AddUserCommand : CommandBase 8 | { 9 | public string Name { get; set; } 10 | public string Email { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Graph.CrossCutting/Exceptions/ElementNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Graph.CrossCutting.Exceptions 6 | { 7 | public class ElementNotFoundException : Exception 8 | { 9 | public ElementNotFoundException() 10 | { 11 | 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Command/Interfaces/IUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Thread = System.Threading.Tasks; 5 | 6 | namespace Graph.Infrastructure.Database.Command.Interfaces 7 | { 8 | public interface IUnitOfWork 9 | { 10 | Thread.Task Commit(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Graph.API/Graph/InputValidationRule.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Validation; 2 | 3 | namespace Graph.API 4 | { 5 | public class InputValidationRule : IValidationRule 6 | { 7 | public INodeVisitor Validate(ValidationContext context) 8 | { 9 | return new EnterLeaveListener(_ => 10 | { 11 | }); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Graph.Application/Commands/Task/UpdateDeadlineCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Graph.Application.Commands.Task 6 | { 7 | public class UpdateDeadlineCommand : CommandBase 8 | { 9 | public Guid Id { get; set; } 10 | public DateTime Deadline { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Query/Model/Project/ProjectUser.cs: -------------------------------------------------------------------------------- 1 | using MongoDB.Bson.Serialization.Attributes; 2 | 3 | namespace Graph.Infrastructure.Database.Query.ProjectSchema 4 | { 5 | public class ProjectUser 6 | { 7 | public string Id { get; set; } 8 | 9 | [BsonElement("name")] 10 | public string Name { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Graph.Application/Commands/Task/ChangeAssigneeCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Graph.Application.Commands.Task 6 | { 7 | public class ChangeAssigneeCommand : CommandBase 8 | { 9 | public Guid Id { get; set; } 10 | public Guid NewAssigneeId { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Query/Model/Task/TaskProject.cs: -------------------------------------------------------------------------------- 1 | using MongoDB.Bson.Serialization.Attributes; 2 | 3 | namespace Graph.Infrastructure.Database.Query.TaskSchema 4 | { 5 | public class TaskProject 6 | { 7 | public string Id { get; set; } 8 | 9 | [BsonElement("description")] 10 | public string Description { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Graph.Application/MessageHandler/TaskProjectMessage.cs: -------------------------------------------------------------------------------- 1 | using Graph.Infrastructure.Database.Query.ProjectSchema; 2 | using Graph.Infrastructure.Database.Query.TaskSchema; 3 | 4 | namespace Graph.Application.MessageHandler 5 | { 6 | public class TaskProjectMessage 7 | { 8 | public Project Project { get; set; } 9 | public Task Task { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Graph.Application/Commands/Project/AddUserProjectCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Graph.Application.Commands.Project 6 | { 7 | public class AddUserProjectCommand : CommandBase 8 | { 9 | public Guid ProjectId { get; set; } 10 | public Guid UserId { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Graph.CrossCutting/Graph.CrossCutting.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Graph.Application/Commands/Project/AddProjectCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Graph.Application.Commands.Project 6 | { 7 | public class AddProjectCommand : CommandBase 8 | { 9 | public string Description { get; set; } 10 | public string LongDescription { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Graph.Application/Commands/Project/RemoveUserProjectCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Graph.Application.Commands.Project 6 | { 7 | public class RemoveUserProjectCommand : CommandBase 8 | { 9 | public Guid UserId { get; set; } 10 | public Guid ProjectId { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Graph.Application/MessageHandler/ProjectUserMessage.cs: -------------------------------------------------------------------------------- 1 | using Graph.Infrastructure.Database.Query.ProjectSchema; 2 | using Graph.Infrastructure.Database.Query.UserSchema; 3 | 4 | namespace Graph.Application.MessageHandler 5 | { 6 | public class ProjectUserMessage 7 | { 8 | public Project Project { get; set; } 9 | public User User { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Command/Model/Status.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Graph.Infrastructure.Database.Command.Model 6 | { 7 | public class Status 8 | { 9 | public int Id { get; set; } 10 | public string Description { get; set; } 11 | public string LongDescription { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Graph.Application/Commands/Task/UpdateTaskStatusCommand.cs: -------------------------------------------------------------------------------- 1 | using Graph.CrossCutting; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace Graph.Application.Commands.Task 7 | { 8 | public class UpdateTaskStatusCommand : CommandBase 9 | { 10 | public Guid Id { get; set; } 11 | public TaskStatusEnum Status { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/Common/TaskStatusEnumType.cs: -------------------------------------------------------------------------------- 1 | using Graph.CrossCutting; 2 | using GraphQL.Types; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | 7 | namespace Graph.Application.Graph.Common 8 | { 9 | public class TaskStatusEnumType : EnumerationGraphType 10 | { 11 | public TaskStatusEnumType() 12 | { 13 | 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Graph.Application/Commands/User/UpdateUserInfoCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Graph.Application.Commands.User 6 | { 7 | public class UpdateUserInfoCommand : CommandBase 8 | { 9 | public Guid Id { get; set; } 10 | public string Name { get; set; } 11 | public string Email { get; set; } 12 | 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/Common/FilterType.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace Graph.Application.Graph.Common 7 | { 8 | public class FilterType : InputObjectGraphType 9 | { 10 | public FilterType() 11 | { 12 | Field(f => f.Operation); 13 | Field(f => f.Value); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/Common/MutationResultType.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace Graph.Application.Graph.Common 7 | { 8 | public class MutationResultType : ObjectGraphType 9 | { 10 | public MutationResultType() 11 | { 12 | Field("result", (a) => true); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/Common/PaginationType.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace Graph.Application.Graph.Common 7 | { 8 | public class PaginationType : InputObjectGraphType 9 | { 10 | public PaginationType() 11 | { 12 | Field(i => i.Skip); 13 | Field(i => i.Take); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/Task/Types/Query/TaskUserType.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | using Model = Graph.Infrastructure.Database.Query.TaskSchema; 3 | 4 | namespace Graph.Application.Graph.Project.Types.Query 5 | { 6 | public class TaskUserType : ObjectGraphType 7 | { 8 | public TaskUserType() 9 | { 10 | Field(i => i.Id); 11 | Field(i => i.Name); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Graph.Application/Commands/Task/UpdateTaskInfoCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Graph.Application.Commands.Task 6 | { 7 | public class UpdateTaskInfoCommand : CommandBase 8 | { 9 | public Guid Id { get; set; } 10 | public string Description { get; set; } 11 | public string LongDescription { get; set; } 12 | 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Graph.Application/Commands/Project/UpdateProjectInfoCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Graph.Application.Commands.Project 6 | { 7 | public class UpdateProjectInfoCommand : CommandBase 8 | { 9 | public Guid Id { get; set; } 10 | public string Description { get; set; } 11 | public string LongDescription { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md -------------------------------------------------------------------------------- /src/Graph.Domain/Graph.Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Command/Interfaces/ITaskRepository.cs: -------------------------------------------------------------------------------- 1 | using Graph.Infrastructure.Database.Repository; 2 | using Graph.Infrastructure.Database.Repository.Interfaces; 3 | using Graph.Infrastructure.Database.Command; 4 | using Graph.Infrastructure.Database.Command.Model; 5 | 6 | namespace Graph.Infrastructure.Database.Command.Interfaces 7 | { 8 | public interface ITaskRepository : IRepository 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Query/Manager/TaskManager.cs: -------------------------------------------------------------------------------- 1 | using Graph.Infrastructure.Database.Query.TaskSchema; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace Graph.Infrastructure.Database.Query.Manager 7 | { 8 | public class TaskManager : EntityManager 9 | { 10 | public TaskManager(IManager manager) : base(manager) 11 | { 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Graph.API/Graph/GraphQLSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using GraphQL.Types; 3 | using Microsoft.AspNetCore.Http; 4 | 5 | namespace Graph.API 6 | { 7 | public class GraphQLSettings 8 | { 9 | public PathString Path { get; set; } = "/api/graphql"; 10 | public Func BuildUserContext { get; set; } 11 | public bool EnableMetrics { get; set; } 12 | 13 | public ISchema Schema { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/Project/Types/Query/ProjectUserType.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | using Model = Graph.Infrastructure.Database.Query.ProjectSchema; 3 | 4 | namespace Graph.Application.Graph.Project.Types.Query 5 | { 6 | public class ProjectUserType : ObjectGraphType 7 | { 8 | public ProjectUserType() 9 | { 10 | Field(i => i.Id); 11 | Field(i => i.Name); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/Task/Types/Query/TaskProjectType.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | using Model = Graph.Infrastructure.Database.Query.TaskSchema; 3 | 4 | namespace Graph.Application.Graph.Project.Types.Query 5 | { 6 | public class TaskProjectType : ObjectGraphType 7 | { 8 | public TaskProjectType() 9 | { 10 | Field(i => i.Id); 11 | Field(i => i.Description); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/Common/EnumFilterType.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace Graph.Application.Graph.Common 7 | { 8 | public class EnumFilterType: InputObjectGraphType 9 | { 10 | public EnumFilterType() 11 | { 12 | Field(f => f.Operation); 13 | Field().Name("value"); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/ServiceBus/BusConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Graph.Infrastructure.ServiceBus 6 | { 7 | public class BusConfiguration 8 | { 9 | public string Url { get; set; } 10 | public string Queue { get; set; } 11 | public BusTransport Transport { get; set; } 12 | public BusCrendentials Credentials { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Graph.CrossCutting/Exceptions/ValidationException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Graph.CrossCutting.Exceptions 6 | { 7 | public class ValidationException : Exception 8 | { 9 | public ValidationException() 10 | { 11 | 12 | } 13 | 14 | public ValidationException(string message) : base(message) 15 | { 16 | 17 | } 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Query/Manager/UserManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Graph.Infrastructure.Database.Query.UserSchema; 4 | using Microsoft.Extensions.Options; 5 | 6 | namespace Graph.Infrastructure.Database.Query.Manager 7 | { 8 | public class UserManager : EntityManager 9 | { 10 | public UserManager(IManager manager) : base(manager) 11 | { 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Graph.CrossCutting/Exceptions/QueryArgumentException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Graph.CrossCutting.Exceptions 6 | { 7 | public class QueryArgumentException : Exception 8 | { 9 | public QueryArgumentException() 10 | { 11 | 12 | } 13 | 14 | public QueryArgumentException(string message) : base(message) 15 | { 16 | 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/Common/DateFilterType.cs: -------------------------------------------------------------------------------- 1 | using Graph.CrossCutting.Extensions; 2 | using GraphQL.Types; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | 7 | namespace Graph.Application.Graph.Common 8 | { 9 | public class DateFilterType : InputObjectGraphType 10 | { 11 | public DateFilterType() 12 | { 13 | Field(f => f.Operation); 14 | Field(f => f.Value); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Query/Model/User/UserProject.cs: -------------------------------------------------------------------------------- 1 | using MongoDB.Bson.Serialization.Attributes; 2 | 3 | namespace Graph.Infrastructure.Database.Query.UserSchema 4 | { 5 | public class UserProject 6 | { 7 | public string Id { get; set; } 8 | 9 | [BsonElement("description")] 10 | public string Description { get; set; } 11 | 12 | [BsonElement("longDescription")] 13 | public string LongDescription { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Query/Manager/ProjectManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Graph.Infrastructure.Database.Query.ProjectSchema; 4 | using Microsoft.Extensions.Options; 5 | 6 | namespace Graph.Infrastructure.Database.Query.Manager 7 | { 8 | public class ProjectManager : EntityManager 9 | { 10 | public ProjectManager(IManager manager) : base(manager) 11 | { 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/Common/UpdateDescriptionInput.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | 3 | namespace Graph.Application.Graph.Common 4 | { 5 | public class UpdateDescriptionInput : InputObjectGraphType 6 | { 7 | public UpdateDescriptionInput() 8 | { 9 | Field().Name("id"); 10 | Field().Name("description"); 11 | Field().Name("longDescription"); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/User/Types/Input/UserFilterType.cs: -------------------------------------------------------------------------------- 1 | using Graph.Application.Graph.Common; 2 | using GraphQL.Types; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | 7 | namespace Graph.Application.Graph.User.Types.Input 8 | { 9 | public class UserFilterType : InputObjectGraphType 10 | { 11 | public UserFilterType() 12 | { 13 | Field("name"); 14 | Field("email"); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/DatabaseConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Graph.Infrastructure.Database 6 | { 7 | public class DatabaseConfiguration 8 | { 9 | public DatabaseProvider WriteDatabaseProvider { get; set; } 10 | public string WriteDatabase { get; set; } 11 | public DatabaseProvider ReadDatabaseProvider { get; set; } 12 | public string ReadDatabase { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/ServiceBus/Message.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Graph.Infrastructure.ServiceBus 6 | { 7 | public class Message 8 | { 9 | public string MessageType { get; set; } 10 | public string MessageData { get; private set; } 11 | 12 | public void SetData(object obj) 13 | { 14 | MessageData = Newtonsoft.Json.JsonConvert.SerializeObject(obj); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/Project/Types/Input/ProjectFilterType.cs: -------------------------------------------------------------------------------- 1 | using Graph.Application.Graph.Common; 2 | using GraphQL.Types; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | 7 | namespace Graph.Application.Graph.Project.Types.Input 8 | { 9 | public class ProjectFilterType : InputObjectGraphType 10 | { 11 | public ProjectFilterType() 12 | { 13 | Field("description"); 14 | Field("longDescription"); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/Task/Types/Input/UpdateDeadlineInput.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Graph.Application.Commands.Task; 5 | using GraphQL.Types; 6 | 7 | namespace Graph.Application.Graph.Types.Input 8 | { 9 | public class UpdateDeadlineInput : InputObjectGraphType 10 | { 11 | public UpdateDeadlineInput() 12 | { 13 | Field().Name("id"); 14 | Field(i => i.Deadline); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Command/Interfaces/IProjectRepository.cs: -------------------------------------------------------------------------------- 1 | using Thread = System.Threading.Tasks; 2 | using Graph.Infrastructure.Database.Repository.Interfaces; 3 | using System; 4 | using Graph.Infrastructure.Database.Command.Model; 5 | 6 | namespace Graph.Infrastructure.Database.Command.Interfaces 7 | { 8 | public interface IProjectRepository : IRepository 9 | { 10 | Thread.Task AddUser(UserProject userProject); 11 | Thread.Task RemoveUser(UserProject userProject); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/Project/Types/Query/ProjectTaskType.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | using Model = Graph.Infrastructure.Database.Query.ProjectSchema; 3 | 4 | namespace Graph.Application.Graph.Project.Types.Query 5 | { 6 | public class ProjectTaskType : ObjectGraphType 7 | { 8 | public ProjectTaskType() 9 | { 10 | Field(i => i.Id); 11 | Field(i => i.Description); 12 | Field(i => i.Status); 13 | Field(i => i.Responsible); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Command/Interfaces/IRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Graph.Infrastructure.Database.Repository.Interfaces 8 | { 9 | public interface IRepository : IDisposable 10 | { 11 | Task Add(T obj); 12 | Task GetById(Guid id); 13 | Task> GetAll(); 14 | Task Update(T obj); 15 | Task Remove(T obj); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Graph.CrossCutting/Extensions/GraphQL/FilterTypeEnum.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Graph.CrossCutting.Extensions.GraphQL 6 | { 7 | public enum FilterTypeEnum 8 | { 9 | GREATER = 'g', // > 10 | LESS = 'l', // < 11 | NOT = 'n', // ! 12 | EQUAL = 'e', // = 13 | CONTAIN = 'c', // Contains(@0) 14 | 15 | // GREATER and EQUAL - >= 16 | // LESS and EQUAL - <= 17 | // NOT and EQUAL - != 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Graph.Domain/Validator/ProjectValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace Graph.Domain.Validator 7 | { 8 | public class ProjectValidator : AbstractValidator 9 | { 10 | public ProjectValidator() 11 | { 12 | RuleFor(i => i.Id).NotNull().NotEqual(Guid.Empty).WithErrorCode("ID-01"); 13 | RuleFor(i => i.Description).NotNull().NotEmpty().WithErrorCode("DESC-01"); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/Common/Filter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Graph.Application.Graph.Common 6 | { 7 | public class Filter 8 | { 9 | public Filter() 10 | { 11 | 12 | } 13 | 14 | public Filter(string operation, string value) 15 | { 16 | Operation = operation; 17 | Value = value; 18 | } 19 | 20 | public string Operation { get; set; } 21 | public string Value { get; set; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Graph.Application/Commands/Task/AddTaskCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Graph.Application.Commands.Task 6 | { 7 | public class AddTaskCommand : CommandBase 8 | { 9 | public Guid ProjectId { get; set; } 10 | public string Description { get; set; } 11 | public string LongDescription { get; set; } 12 | public DateTime DeadLine { get; set; } 13 | public Guid AssigneeId { get; set; } 14 | public Guid ReporterId { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/Task/Types/Input/ChangeAssigneeInput.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Graph.Application.Commands.Task; 5 | using GraphQL.Types; 6 | 7 | namespace Graph.Application.Graph.Task.Types.Input 8 | { 9 | public class ChangeAssigneeInput : InputObjectGraphType 10 | { 11 | public ChangeAssigneeInput() 12 | { 13 | Field().Name("id"); 14 | Field().Name("newAssigneeId"); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Query/Model/Project/ProjectTask.cs: -------------------------------------------------------------------------------- 1 | using MongoDB.Bson.Serialization.Attributes; 2 | 3 | namespace Graph.Infrastructure.Database.Query.ProjectSchema 4 | { 5 | public class ProjectTask 6 | { 7 | public string Id { get; set; } 8 | 9 | [BsonElement("description")] 10 | public string Description { get; set; } 11 | 12 | [BsonElement("responsible")] 13 | public string Responsible { get; set; } 14 | 15 | [BsonElement("status")] 16 | public string Status { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/ServiceBus/BusCrendentials.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Graph.Infrastructure.ServiceBus 6 | { 7 | public class BusCrendentials 8 | { 9 | // RabbitMQ 10 | public string Username { get; set; } 11 | public string Password { get; set; } 12 | 13 | // Azure 14 | public string KeyName { get; set; } 15 | public string SharedAccessKey { get; set; } 16 | public int TokenTimeToLive { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/GraphAPI/appSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ServiceBus": { 3 | "Url": "rabbitmq://localhost", 4 | "Queue": "graphapi-queue", 5 | "Transport": "rabbitmq", 6 | "Credentials": { 7 | "Username": "admin", 8 | "Password": "Secr3t" 9 | } 10 | }, 11 | "ConnectionStrings": { 12 | //"WriteDatabaseProvider": "postgres", 13 | //"WriteDatabase": "Host=localhost;Database=graph;Username=user;Password=Secr3t", 14 | "ReadDatabaseProvider": "mongodb", 15 | "ReadDatabase": "mongodb://localhost:27017" 16 | }, 17 | "Settings": { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/User/Types/Input/AddUserInput.cs: -------------------------------------------------------------------------------- 1 | using Graph.Application.Commands; 2 | using Graph.Application.Commands.User; 3 | using GraphQL.Types; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | 8 | namespace Graph.Application.Graph.Types.Input 9 | { 10 | public class AddUserInput : InputObjectGraphType 11 | { 12 | public AddUserInput() 13 | { 14 | Name = "UserInput"; 15 | Field(f => f.Name); 16 | Field(f => f.Email); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Command/Model/UserProject.cs: -------------------------------------------------------------------------------- 1 | using Graph.CrossCutting.Interfaces; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace Graph.Infrastructure.Database.Command.Model 7 | { 8 | public class UserProject : IModel 9 | { 10 | public Guid Id { get; set; } 11 | 12 | public Guid Projectid { get; set; } 13 | public Guid UserId { get; set; } 14 | 15 | public virtual Project Project { get; set; } 16 | public virtual User User { get; set; } 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/Project/Types/Input/UserProjectInput.cs: -------------------------------------------------------------------------------- 1 | using Graph.Application.Commands; 2 | using Graph.Application.Commands.Project; 3 | using GraphQL.Types; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | 8 | namespace Graph.Application.Graph.Project.Types.Input 9 | { 10 | public class UserProjectInput : InputObjectGraphType 11 | { 12 | public UserProjectInput() 13 | { 14 | Field().Name("projectId"); 15 | Field().Name("userId"); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/User/Types/Query/UserProjectType.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using Model = Graph.Infrastructure.Database.Query.UserSchema; 6 | 7 | namespace Graph.Application.Graph.User.Types.Query 8 | { 9 | public class UserProjectType : ObjectGraphType 10 | { 11 | public UserProjectType() 12 | { 13 | Field(i => i.Id); 14 | Field(i => i.Description); 15 | Field(i => i.LongDescription); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docker-compose-stack.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | mongodb: 5 | image: mvertes/alpine-mongo 6 | ports: 7 | - 27017:27017 8 | postgres: 9 | image: postgres:12-alpine 10 | ports: 11 | - 5432:5432 12 | environment: 13 | - POSTGRES_USER=user 14 | - POSTGRES_PASSWORD=Secr3t 15 | - POSTGRES_DB=graph 16 | rabbitmq: 17 | image: rabbitmq:3.8-management-alpine 18 | restart: always 19 | ports: 20 | - 8080:15672 21 | - 5672:5672 22 | environment: 23 | - RABBITMQ_DEFAULT_USER=admin 24 | - RABBITMQ_DEFAULT_PASS=Secr3t 25 | -------------------------------------------------------------------------------- /src/Graph.CrossCutting/Extensions/GraphQL/GraphFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Graph.CrossCutting.Extensions.GraphQL 6 | { 7 | public class GraphFilter 8 | { 9 | public GraphFilter() 10 | { 11 | 12 | } 13 | public GraphFilter(string operation, object value) 14 | { 15 | Operation = operation; 16 | Value = value; 17 | } 18 | 19 | public string Operation { get; set; } 20 | public object Value { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/Task/Types/Input/UpdateTaskStatusInput.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Graph.Application.Commands.Task; 5 | using Graph.Application.Graph.Common; 6 | using GraphQL.Types; 7 | 8 | namespace Graph.Application.Graph.Types.Input 9 | { 10 | public class UpdateTaskStatusInput : InputObjectGraphType 11 | { 12 | public UpdateTaskStatusInput() 13 | { 14 | Field().Name("id"); 15 | Field().Name("status"); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/User/Types/Query/UserType.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | using Model = Graph.Infrastructure.Database.Query.UserSchema; 3 | 4 | namespace Graph.Application.Graph.User.Types.Query 5 | { 6 | public class UserType : ObjectGraphType 7 | { 8 | public UserType() 9 | { 10 | Field(i => i.Id).Name("id"); 11 | Field(i => i.Name).Name("name"); 12 | Field(i => i.Email).Name("email"); 13 | Field>().Name("projects").Resolve((ctx) => ctx.Source.Projects); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Graph.Domain.Service/Graph.Domain.Service.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Graph.Domain.Service/Mappings/StatusProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Graph.CrossCutting; 3 | using Graph.Infrastructure.Database.Command.Model; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | 8 | namespace Graph.Domain.Service.Mappings 9 | { 10 | public class StatusProfile : Profile 11 | { 12 | public StatusProfile() 13 | { 14 | CreateMap() 15 | .AfterMap((model, status) => 16 | { 17 | status = (TaskStatusEnum)model.Id; 18 | }); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Command/Repository/TaskRepository.cs: -------------------------------------------------------------------------------- 1 | using Graph.Infrastructure.Database.Repository; 2 | using Graph.Infrastructure.Database.Command.Interfaces; 3 | using System; 4 | using Graph.Infrastructure.Database.Command.Model; 5 | using Microsoft.EntityFrameworkCore; 6 | using Thread = System.Threading.Tasks; 7 | 8 | namespace Graph.Infrastructure.Database.Command.Repository 9 | { 10 | public class TaskRepository : Repository, ITaskRepository 11 | { 12 | public TaskRepository(GraphContext context) : base(context) 13 | { 14 | } 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Graph.Domain/Validator/UserValidator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using FluentValidation; 5 | 6 | namespace Graph.Domain.Validator 7 | { 8 | public class UserValidator : AbstractValidator 9 | { 10 | public UserValidator() 11 | { 12 | RuleFor(i => i.Id).NotNull().NotEmpty().NotEqual(Guid.Empty).WithErrorCode("NAME-01"); 13 | RuleFor(i => i.Email).NotNull().NotEmpty().EmailAddress().WithErrorCode("EMAIL-01"); 14 | RuleFor(i => i.Name).NotNull().NotEmpty().WithErrorCode("NAME-01"); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/Project/Types/Input/AddProjectInput.cs: -------------------------------------------------------------------------------- 1 | using Graph.Application.Commands; 2 | using Graph.Application.Commands.Project; 3 | using GraphQL.Types; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | 8 | namespace Graph.Application.Graph.Project.Types.Input 9 | { 10 | public class AddProjectInput : InputObjectGraphType 11 | { 12 | public AddProjectInput() 13 | { 14 | Field>().Name("description"); 15 | Field().Name("longDescription"); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/User/Types/Input/EditUserInput.cs: -------------------------------------------------------------------------------- 1 | using Graph.Application.Commands; 2 | using Graph.Application.Commands.User; 3 | using GraphQL.Types; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | 8 | namespace Graph.Application.Graph.Types.Input 9 | { 10 | public class EditUserInput : InputObjectGraphType 11 | { 12 | public EditUserInput() 13 | { 14 | Name = "UserInput"; 15 | Field().Name("Id"); 16 | Field(f => f.Name); 17 | Field(f => f.Email); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Command/Model/User.cs: -------------------------------------------------------------------------------- 1 | using Graph.CrossCutting.Interfaces; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace Graph.Infrastructure.Database.Command.Model 7 | { 8 | public class User : IModel 9 | { 10 | public User() 11 | { 12 | UserProjects = new HashSet(); 13 | } 14 | 15 | public Guid Id { get; set; } 16 | public string Name { get; set; } 17 | public string Email { get; set; } 18 | 19 | public virtual ICollection UserProjects { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Query/Model/User/User.cs: -------------------------------------------------------------------------------- 1 | using Graph.CrossCutting.Interfaces; 2 | using MongoDB.Bson; 3 | using MongoDB.Bson.Serialization.Attributes; 4 | using System.Collections.Generic; 5 | 6 | namespace Graph.Infrastructure.Database.Query.UserSchema 7 | { 8 | public class User : IQueryModel 9 | { 10 | public string Id { get; set; } 11 | 12 | [BsonElement("name")] 13 | public string Name { get; set; } 14 | 15 | [BsonElement("email")] 16 | public string Email { get; set; } 17 | 18 | [BsonElement("projects")] 19 | public IEnumerable Projects { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Query/IManager.cs: -------------------------------------------------------------------------------- 1 | using Graph.CrossCutting.Extensions.GraphQL; 2 | using Graph.CrossCutting.Interfaces; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Graph.Infrastructure.Database.Query 9 | { 10 | public interface IManager where T: class, IQueryModel 11 | { 12 | Task Index(T entry); 13 | Task Remove(Guid entryId); 14 | Task GetById(Guid id, string[] fields); 15 | Task> Get(string[] fields, IDictionary filters, string order, int skip, int take); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/Task/Types/Input/TaskFilterType.cs: -------------------------------------------------------------------------------- 1 | using Graph.Application.Graph.Common; 2 | using Graph.CrossCutting.Extensions; 3 | using GraphQL.Types; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | 8 | namespace Graph.Application.Graph.Project.Types.Input 9 | { 10 | public class TaskFilterType : InputObjectGraphType 11 | { 12 | public TaskFilterType() 13 | { 14 | Field("description"); 15 | Field("deadLine"); 16 | Field("assignee"); 17 | Field("reporter"); 18 | Field("status"); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Query/Manager/IEntityManager.cs: -------------------------------------------------------------------------------- 1 | using Graph.CrossCutting.Extensions.GraphQL; 2 | using Graph.CrossCutting.Interfaces; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Graph.Infrastructure.Database.Query.Manager 9 | { 10 | public interface IEntityManager where T : IQueryModel 11 | { 12 | Task Index(T entry); 13 | Task Remove(Guid entryId); 14 | Task GetById(Guid id, string[] fields); 15 | Task> Get(string[] fields, IDictionary filters, string order, int skip, int take); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Command/Repository/UserRepository.cs: -------------------------------------------------------------------------------- 1 | using Graph.Infrastructure.Database.Repository; 2 | using Graph.Infrastructure.Database.Repository.Interfaces; 3 | using Graph.Infrastructure.Database.Command.Interfaces; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | using Graph.Infrastructure.Database.Command.Model; 8 | using Microsoft.EntityFrameworkCore; 9 | using Thread = System.Threading.Tasks; 10 | 11 | namespace Graph.Infrastructure.Database.Command.Repository 12 | { 13 | public class UserRepository : Repository, IUserRepository 14 | { 15 | public UserRepository(GraphContext context) : base(context) 16 | { 17 | } 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/Task/Types/Input/AddTaskInput.cs: -------------------------------------------------------------------------------- 1 | using Graph.Application.Commands.Project; 2 | using Graph.Application.Commands.Task; 3 | using GraphQL.Types; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | 8 | namespace Graph.Application.Graph.Types.Input 9 | { 10 | public class AddTaskInput : InputObjectGraphType 11 | { 12 | public AddTaskInput() 13 | { 14 | Field(i => i.Description); 15 | Field(i => i.LongDescription); 16 | Field(i => i.DeadLine); 17 | Field("projectId"); 18 | Field("reporterId"); 19 | Field("assigneeId"); 20 | 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Command/Model/Project.cs: -------------------------------------------------------------------------------- 1 | using Graph.CrossCutting.Interfaces; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace Graph.Infrastructure.Database.Command.Model 7 | { 8 | public class Project : IModel 9 | { 10 | public Project() 11 | { 12 | UserProjects = new List(); 13 | Tasks = new List(); 14 | } 15 | 16 | public Guid Id { get; set; } 17 | public string Description { get; set; } 18 | public string LongDescription { get; set; } 19 | public virtual ICollection UserProjects { get; set; } 20 | public virtual ICollection Tasks { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Graph.API/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace Graph.API 11 | { 12 | public class Program 13 | { 14 | public static void Main(string[] args) 15 | { 16 | CreateHostBuilder(args).Build().Run(); 17 | } 18 | 19 | public static IHostBuilder CreateHostBuilder(string[] args) => 20 | Host.CreateDefaultBuilder(args) 21 | .ConfigureWebHostDefaults(webBuilder => 22 | { 23 | webBuilder.UseStartup(); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Graph.CrossCutting.Ioc/Graph.CrossCutting.Ioc.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/GraphSchema.cs: -------------------------------------------------------------------------------- 1 | using GraphQL; 2 | using GraphQL.Types; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | using Graph.CrossCutting.Extensions.GraphQL; 7 | using Graph.Application.Graph.User.Types.Query; 8 | using Graph.Application.Graph.Query; 9 | using Graph.Application.Graph.Mutation; 10 | 11 | namespace Graph.Application.Graph 12 | { 13 | public class GraphSchema : Schema 14 | { 15 | public GraphSchema(IDependencyResolver resolver) : base(resolver) 16 | { 17 | this.AddQuery(); 18 | this.AddQuery(); 19 | this.AddQuery(); 20 | 21 | this.AddMutation(); 22 | this.AddMutation(); 23 | this.AddMutation(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | image: Ubuntu 2 | 3 | before_build: 4 | - dotnet restore 5 | 6 | build_script: 7 | - sh: dotnet build 8 | 9 | after_build: 10 | - dotnet tool install --global dotnet-sonarscanner 11 | - dotnet sonarscanner begin /k:"gbauso_GraphAPI" /o:"gbauso" /d:sonar.login="c9637c6be7788b1eb7c14f14984a08971652bfed" /d:sonar.host.url="https://sonarcloud.io" 12 | - dotnet build 13 | - dotnet sonarscanner end /d:sonar.login="c9637c6be7788b1eb7c14f14984a08971652bfed" 14 | 15 | test_script: 16 | - cd test && cd Graph.Tests 17 | - dotnet test /p:CollectCoverage=true /p:CoverletOutput=TestResults/ /p:CoverletOutputFormat=lcov /p:Exclude=\"[Graph.API]*%2c[Graph.Infrastructure]*%2c[Graph.CrossCutting*]*%2c[Graph.Application.MessageHandler]*\" 18 | - cd TestResults 19 | - bash <(curl -s https://codecov.io/bash) -f coverage.info -t 7b7d9ee8-1384-4dc7-a6b8-771cde0fb11a || echo "Codecov did not collect coverage reports" 20 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph.Application.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/Project/Types/Query/ProjectType.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using Model = Graph.Infrastructure.Database.Query.ProjectSchema; 6 | 7 | namespace Graph.Application.Graph.Project.Types.Query 8 | { 9 | public class ProjectType : ObjectGraphType 10 | { 11 | public ProjectType() 12 | { 13 | Field(i => i.Id); 14 | Field(i => i.Description); 15 | Field().Name("longDescription").Resolve(ctx => ctx.Source.LongDescription ?? string.Empty); 16 | Field(i => i.FinishedCount); 17 | Field(i => i.UnfinishedCount); 18 | 19 | Field>().Name("participants").Resolve((ctx) => ctx.Source.Participants); 20 | Field>().Name("tasks").Resolve((ctx) => ctx.Source.Tasks); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Command/Model/Task.cs: -------------------------------------------------------------------------------- 1 | using Graph.CrossCutting.Interfaces; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace Graph.Infrastructure.Database.Command.Model 7 | { 8 | public class Task : IModel 9 | { 10 | public Guid Id { get; set; } 11 | public string Description { get; set; } 12 | public string LongDescription { get; set; } 13 | public DateTime CreatedDate { get; set; } 14 | public DateTime DeadLine { get; set; } 15 | public Guid ProjectId { get; set; } 16 | public Guid AssigneeId { get; set; } 17 | public Guid ReporterId { get; set; } 18 | public int StatusId { get; set; } 19 | 20 | public virtual Status Status { get; set; } 21 | public virtual User Assignee { get; set; } 22 | public virtual User Reporter { get; set; } 23 | public virtual Project Project { get; set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Graph.Domain.Service/Mappings/UserProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Command = Graph.Infrastructure.Database.Command.Model; 3 | using Query = Graph.Infrastructure.Database.Query.UserSchema; 4 | using System.Linq; 5 | 6 | namespace Graph.Domain.Service.Mappings 7 | { 8 | public class UserProfile : Profile 9 | { 10 | public UserProfile() 11 | { 12 | CreateMap(); 13 | 14 | CreateMap() 15 | .ForMember(i => i.Projects, j => j.MapFrom(m => m.UserProjects.Select(s => s.Project))); 16 | 17 | CreateMap() 18 | .ForMember(i => i.Projects, j => j.MapFrom(m => m.UserProjects.Select(s => s.Project))); 19 | 20 | CreateMap() 21 | .ForMember(i => i.Projects, j => j.MapFrom(m => m.Projects)); 22 | 23 | CreateMap(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Query/Model/Project/Project.cs: -------------------------------------------------------------------------------- 1 | using Graph.CrossCutting.Interfaces; 2 | using MongoDB.Bson; 3 | using MongoDB.Bson.Serialization.Attributes; 4 | using System.Collections.Generic; 5 | 6 | namespace Graph.Infrastructure.Database.Query.ProjectSchema 7 | { 8 | public class Project : IQueryModel 9 | { 10 | public string Id { get; set; } 11 | 12 | [BsonElement("description")] 13 | public string Description { get; set; } 14 | 15 | [BsonElement("longDescription")] 16 | public string LongDescription { get; set; } 17 | 18 | [BsonElement("tasks")] 19 | public ICollection Tasks { get; set; } 20 | 21 | [BsonElement("participants")] 22 | public ICollection Participants { get; set; } 23 | 24 | [BsonElement("finishedCount")] 25 | public int FinishedCount { get; set; } 26 | 27 | [BsonElement("unfinishedCount")] 28 | public int UnfinishedCount { get; set; } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/Graph.API/Graph.API.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.0 5 | 9c6dc2bc-8d85-42e0-8ee9-395a199dc74e 6 | Linux 7 | ..\.. 8 | ..\..\docker-compose.dcproj 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Query/Model/Task/Task.cs: -------------------------------------------------------------------------------- 1 | using Graph.CrossCutting.Interfaces; 2 | using MongoDB.Bson.Serialization.Attributes; 3 | 4 | namespace Graph.Infrastructure.Database.Query.TaskSchema 5 | { 6 | public class Task : IQueryModel 7 | { 8 | public string Id { get; set; } 9 | 10 | [BsonElement("description")] 11 | public string Description { get; set; } 12 | 13 | [BsonElement("longDescription")] 14 | public string LongDescription { get; set; } 15 | 16 | [BsonElement("createdDate")] 17 | public long CreatedDate { get; set; } 18 | 19 | [BsonElement("deadLine")] 20 | public long DeadLine { get; set; } 21 | 22 | [BsonElement("assignee")] 23 | public TaskUser Assignee { get; set; } 24 | 25 | [BsonElement("reporter")] 26 | public TaskUser Reporter { get; set; } 27 | 28 | [BsonElement("project")] 29 | public TaskProject Project { get; set; } 30 | 31 | [BsonElement("status")] 32 | public string Status { get; set; } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Query/ManagerFactory.cs: -------------------------------------------------------------------------------- 1 | using Graph.CrossCutting.Interfaces; 2 | using Microsoft.Extensions.Options; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | 7 | namespace Graph.Infrastructure.Database.Query 8 | { 9 | public class ManagerFactory 10 | { 11 | private readonly IOptions _Configuration; 12 | public ManagerFactory(IOptions options) 13 | { 14 | _Configuration = options; 15 | } 16 | 17 | public IManager GetManager() where T : class, IQueryModel 18 | { 19 | switch(_Configuration.Value.ReadDatabaseProvider) 20 | { 21 | case DatabaseProvider.MONGODB: 22 | return new MongoManager(_Configuration); 23 | 24 | case DatabaseProvider.ELASTICSEARCH: 25 | return new ElasticSearchManager(_Configuration); 26 | 27 | default: 28 | return new InMemoryManager(); 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # ASP.NET Core (.NET Framework) 2 | # Build and test ASP.NET Core projects targeting the full .NET Framework. 3 | # Add steps that publish symbols, save build artifacts, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/dotnet-core 5 | 6 | trigger: 7 | - master 8 | 9 | pool: 10 | vmImage: 'windows-latest' 11 | 12 | variables: 13 | solution: '**/*.sln' 14 | buildPlatform: 'Any CPU' 15 | buildConfiguration: 'Release' 16 | 17 | steps: 18 | - task: NuGetToolInstaller@1 19 | 20 | - task: NuGetCommand@2 21 | inputs: 22 | restoreSolution: '$(solution)' 23 | 24 | - task: VSBuild@1 25 | inputs: 26 | solution: '$(solution)' 27 | msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:DesktopBuildPackageLocation="$(build.artifactStagingDirectory)\WebApp.zip" /p:DeployIisAppPath="Default Web Site"' 28 | platform: '$(buildPlatform)' 29 | configuration: '$(buildConfiguration)' 30 | 31 | - task: VSTest@2 32 | inputs: 33 | platform: '$(buildPlatform)' 34 | configuration: '$(buildConfiguration)' 35 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Command/UnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using Graph.Infrastructure.Database.Command.Interfaces; 2 | using Microsoft.EntityFrameworkCore; 3 | using Thread = System.Threading.Tasks; 4 | 5 | namespace Graph.Infrastructure.Database.Command 6 | { 7 | public class UnitOfWork : IUnitOfWork 8 | { 9 | private readonly GraphContext _Context; 10 | 11 | public UnitOfWork(GraphContext context) 12 | { 13 | _Context = context; 14 | } 15 | public async Thread.Task Commit() 16 | { 17 | if (_Context.Database.IsInMemory()) return; 18 | 19 | using (var transaction = _Context.Database.BeginTransaction()) 20 | { 21 | try 22 | { 23 | await _Context.SaveChangesAsync(); 24 | await transaction.CommitAsync(); 25 | } 26 | catch 27 | { 28 | await transaction.RollbackAsync(); 29 | throw; 30 | } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Command/Repository/ProjectRepository.cs: -------------------------------------------------------------------------------- 1 | using Graph.Infrastructure.Database.Repository; 2 | using Graph.Infrastructure.Database.Command.Interfaces; 3 | using System.Linq; 4 | using Graph.Infrastructure.Database.Command.Model; 5 | using Thread = System.Threading.Tasks; 6 | using Microsoft.EntityFrameworkCore; 7 | using Graph.CrossCutting.Extensions; 8 | 9 | namespace Graph.Infrastructure.Database.Command.Repository 10 | { 11 | public class ProjectRepository : Repository, IProjectRepository 12 | { 13 | public ProjectRepository(GraphContext context) : base(context) 14 | { 15 | } 16 | 17 | public async Thread.Task AddUser(UserProject userProject) 18 | { 19 | await _Context.UserProjects.AddAsync(userProject); 20 | } 21 | 22 | public Thread.Task RemoveUser(UserProject userProject) 23 | { 24 | ClearChangeTrack(); 25 | 26 | _Context.UserProjects.Remove(userProject); 27 | 28 | return Thread.Task.CompletedTask; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/core/aspnet:3.0-alpine AS base 2 | WORKDIR /app 3 | EXPOSE 80 4 | EXPOSE 443 5 | 6 | FROM mcr.microsoft.com/dotnet/core/sdk:3.0-buster AS build 7 | WORKDIR /src 8 | COPY ["src/GraphAPI/Graph.API.csproj", "src/GraphAPI/"] 9 | COPY ["src/Graph.Application/Graph.Application.csproj", "src/Graph.Application/"] 10 | COPY ["src/Graph.Infrastructure/Graph.Infrastructure.csproj", "src/Graph.Infrastructure/"] 11 | COPY ["src/Graph.CrossCutting/Graph.CrossCutting.csproj", "src/Graph.CrossCutting/"] 12 | COPY ["src/Graph.CrossCutting.Ioc/Graph.CrossCutting.Ioc.csproj", "src/Graph.CrossCutting.Ioc/"] 13 | COPY ["src/Graph.Domain/Graph.Domain.csproj", "src/Graph.Domain/"] 14 | COPY ["src/Graph.Domain.Service/Graph.Domain.Service.csproj", "src/Graph.Domain.Service/"] 15 | RUN dotnet restore "src/GraphAPI/Graph.API.csproj" 16 | COPY . . 17 | WORKDIR "/src/src/GraphAPI" 18 | RUN dotnet build "Graph.API.csproj" -c Release -o /app/build 19 | 20 | FROM build AS publish 21 | RUN dotnet publish "Graph.API.csproj" -c Release -o /app/publish 22 | 23 | FROM base AS final 24 | WORKDIR /app 25 | COPY --from=publish /app/publish . 26 | ENTRYPOINT ["dotnet", "Graph.API.dll"] -------------------------------------------------------------------------------- /src/Graph.CrossCutting/Extensions/DomainExtensions.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Graph.CrossCutting.Exceptions; 3 | using Graph.CrossCutting.Interfaces; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Graph.CrossCutting.Extensions 10 | { 11 | public static class DomainExtensions 12 | { 13 | public static D ToDomain(this IModel fromRepository, IMapper mapper) where D: IDomain 14 | { 15 | if (fromRepository == null) throw new ElementNotFoundException(); 16 | 17 | var domain = mapper.Map(fromRepository); 18 | 19 | return domain; 20 | } 21 | 22 | public static T ToModel(this IDomain domain, IMapper mapper) where T : IModel 23 | { 24 | var commandModel = mapper.Map(domain); 25 | 26 | return commandModel; 27 | } 28 | 29 | public static R ToQueryModel(this IDomain domain, IMapper mapper) where R : IQueryModel 30 | { 31 | var queryModel = mapper.Map(domain); 32 | 33 | return queryModel; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Command/ConnectionFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.Extensions.Options; 3 | using System.Reflection; 4 | 5 | namespace Graph.Infrastructure.Database.Command 6 | { 7 | public static class ConnectionFactory 8 | { 9 | public static DbContextOptionsBuilder SetConnectionConfig(this DbContextOptionsBuilder options, IOptions configuration) 10 | { 11 | var config = configuration.Value; 12 | 13 | switch (config.WriteDatabaseProvider) 14 | { 15 | case DatabaseProvider.POSTGRES: 16 | return options 17 | .UseNpgsql(config.WriteDatabase, 18 | opt => opt.MigrationsAssembly("Graph.API")); 19 | case DatabaseProvider.MSSQL: 20 | return options 21 | .UseSqlServer(config.WriteDatabase, 22 | opt => opt.MigrationsAssembly("Graph.API")); 23 | default: 24 | return options 25 | .UseInMemoryDatabase("graphdb"); 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/Task/Types/Query/TaskType.cs: -------------------------------------------------------------------------------- 1 | using Graph.Application.Graph.Project.Types.Query; 2 | using Graph.CrossCutting.Extensions; 3 | using GraphQL.Types; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | using Model = Graph.Infrastructure.Database.Query.TaskSchema; 8 | 9 | namespace Graph.Application.Graph.Task.Types.Query 10 | { 11 | public class TaskType : ObjectGraphType 12 | { 13 | public TaskType() 14 | { 15 | Field(i => i.Id); 16 | Field(i => i.Description); 17 | Field().Name("longDescription").Resolve(ctx => ctx.Source.LongDescription ?? string.Empty); 18 | Field(i => i.Status); 19 | Field().Name("deadline").Resolve(ctx => ctx.Source.DeadLine.UnixToDateTime()); 20 | Field().Name("createdDate").Resolve(ctx => ctx.Source.CreatedDate.UnixToDateTime()); 21 | 22 | Field().Name("assignee").Resolve((ctx) => ctx.Source.Assignee); 23 | Field().Name("reporter").Resolve((ctx) => ctx.Source.Reporter); 24 | 25 | Field().Name("project").Resolve((ctx) => ctx.Source.Project); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Graph.Domain/Validator/TaskValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using Graph.CrossCutting; 3 | using System; 4 | using System.Linq; 5 | 6 | namespace Graph.Domain.Validator 7 | { 8 | public class TaskValidator : AbstractValidator 9 | { 10 | public TaskValidator() 11 | { 12 | RuleFor(i => i.Id).NotNull().NotEqual(Guid.Empty).WithErrorCode("ID-01"); 13 | RuleFor(i => i.Description).NotNull().NotEmpty().WithErrorCode("DESC-01"); 14 | RuleFor(i => i.CreatedDate).NotNull().NotEmpty().WithErrorCode("CREATE-01"); 15 | RuleFor(i => i.DeadLine).NotNull().NotEmpty().WithErrorCode("DEADLINE-01"); 16 | RuleFor(i => i.DeadLine).GreaterThan((i) => i.CreatedDate).WithErrorCode("DEADLINE-02"); 17 | RuleFor(i => i.Assignee).NotNull().WithErrorCode("ASSIGNEE-01"); 18 | //RuleFor(i => i.Assignee).NotEqual((task) => task.Project.Users.FirstOrDefault(i => i.Id == task.Assignee.Id)).WithErrorCode("ASSIGNEE-02"); 19 | RuleFor(i => i.Reporter).NotNull().WithErrorCode("REPORTER-01"); 20 | RuleFor(i => i.Status).NotNull().WithErrorCode("STATUS-01"); 21 | RuleFor(i => i.Status).IsInEnum().WithErrorCode("STATUS-02"); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Query/Manager/EntityManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Graph.CrossCutting.Extensions.GraphQL; 6 | using Graph.CrossCutting.Interfaces; 7 | 8 | namespace Graph.Infrastructure.Database.Query.Manager 9 | { 10 | public abstract class EntityManager : IEntityManager where T: class, IQueryModel 11 | { 12 | protected readonly IManager _Manager; 13 | 14 | public EntityManager(IManager manager) 15 | { 16 | _Manager = manager; 17 | } 18 | 19 | public Task> Get(string[] fields, IDictionary filters, string order, int skip, int take) 20 | { 21 | return _Manager.Get(fields, filters, order, skip, take); 22 | } 23 | 24 | public Task GetById(Guid id, string[] fields) 25 | { 26 | return _Manager.GetById(id, fields); 27 | } 28 | 29 | public Task Index(T entry) 30 | { 31 | return _Manager.Index(entry); 32 | } 33 | 34 | public Task Remove(Guid entryId) 35 | { 36 | return _Manager.Remove(entryId); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/Graph.Tests/Graph.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | all 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /test/Graph.Tests/Comparers.cs: -------------------------------------------------------------------------------- 1 | using Graph.Infrastructure.Database.Command.Model; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.Text; 6 | 7 | namespace Graph.Tests.Comparers 8 | { 9 | public class UserComparer : IEqualityComparer 10 | { 11 | public bool Equals([AllowNull] User x, [AllowNull] User y) 12 | { 13 | return x.Email.ToLower() == y.Email.ToLower(); 14 | } 15 | 16 | public int GetHashCode([DisallowNull] User obj) 17 | { 18 | return obj.Email.GetHashCode(); 19 | } 20 | } 21 | 22 | public class ProjectComparer : IEqualityComparer 23 | { 24 | public bool Equals([AllowNull] Project x, [AllowNull] Project y) 25 | { 26 | return x.Id == y.Id; 27 | } 28 | 29 | public int GetHashCode([DisallowNull] Project obj) 30 | { 31 | return obj.Id.GetHashCode(); 32 | } 33 | } 34 | 35 | public class TaskComparer : IEqualityComparer 36 | { 37 | public bool Equals([AllowNull] Task x, [AllowNull] Task y) 38 | { 39 | return x.Id == y.Id; 40 | } 41 | 42 | public int GetHashCode([DisallowNull] Task obj) 43 | { 44 | return obj.Id.GetHashCode(); 45 | } 46 | } 47 | 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/Graph.API/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:44341", 7 | "sslPort": 44307 8 | } 9 | }, 10 | "$schema": "http://json.schemastore.org/launchsettings.json", 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "ui/playground", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "GraphAPI": { 21 | "commandName": "Project", 22 | "launchUrl": "weatherforecast", 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | }, 26 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 27 | }, 28 | "Docker": { 29 | "commandName": "Docker", 30 | "launchBrowser": true, 31 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/weatherforecast", 32 | "environmentVariables": { 33 | "ASPNETCORE_URLS": "https://+:443;http://+:80", 34 | "ASPNETCORE_HTTPS_PORT": "44308" 35 | }, 36 | "httpPort": 50624, 37 | "useSSL": true, 38 | "sslPort": 44308 39 | }, 40 | "Azure Dev Spaces": { 41 | "commandName": "AzureDevSpaces", 42 | "launchBrowser": true 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/Graph.CrossCutting/Extensions/GraphQL/SchemaExtensions.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | 3 | namespace Graph.CrossCutting.Extensions.GraphQL 4 | { 5 | public static class SchemaExtensions 6 | { 7 | public static ISchema AddQuery(this Schema schema) where T : IComplexGraphType 8 | { 9 | if (schema.Query == null) 10 | { 11 | var type = new ObjectGraphType 12 | { 13 | Name = "query" 14 | }; 15 | 16 | schema.Query = type; 17 | } 18 | 19 | var fields = schema.DependencyResolver.Resolve().Fields; 20 | 21 | foreach (var field in fields) schema.Query.AddField(field); 22 | 23 | return schema; 24 | } 25 | 26 | public static ISchema AddMutation(this Schema schema) where T : IComplexGraphType 27 | { 28 | if (schema.Mutation == null) 29 | { 30 | var type = new ObjectGraphType 31 | { 32 | Name = "mutation" 33 | }; 34 | 35 | schema.Mutation = type; 36 | } 37 | 38 | var fields = schema.DependencyResolver.Resolve().Fields; 39 | 40 | foreach (var field in fields) schema.Mutation.AddField(field); 41 | 42 | return schema; 43 | } 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | mongodb: 5 | image: mvertes/alpine-mongo 6 | ports: 7 | - 27017:27017 8 | postgres: 9 | image: postgres:12-alpine 10 | ports: 11 | - 5432:5432 12 | environment: 13 | - POSTGRES_USER=user 14 | - POSTGRES_PASSWORD=Secr3t 15 | - POSTGRES_DB=graph 16 | rabbitmq: 17 | image: rabbitmq:3.8-management-alpine 18 | restart: always 19 | ports: 20 | - 8080:15672 21 | - 5672:5672 22 | environment: 23 | - RABBITMQ_DEFAULT_USER=admin 24 | - RABBITMQ_DEFAULT_PASS=Secr3t 25 | graph.api: 26 | image: ${DOCKER_REGISTRY-}graphapi 27 | ports: 28 | # - 5017:443 29 | - 5016:80 30 | build: 31 | context: . 32 | dockerfile: Dockerfile 33 | environment: 34 | - ServiceBus__Url=rabbitmq://rabbitmq 35 | - ServiceBus__Transport=rabbitmq 36 | - ServiceBus__Queue=graph-queue 37 | - ServiceBus__Credentials__Username=admin 38 | - ServiceBus__Credentials__Password=Secr3t 39 | - ConnectionStrings__WriteDatabaseProvider=postgres 40 | - ConnectionStrings__WriteDatabase=Host=postgres;Database=graph;Username=user;Password=Secr3t 41 | - ConnectionStrings__ReadDatabaseProvider=mongodb 42 | - ConnectionStrings__ReadDatabase=mongodb://mongodb:27017 43 | # - ASPNETCORE_URLS=http://*:5016 44 | depends_on: 45 | - postgres 46 | - rabbitmq 47 | - mongodb -------------------------------------------------------------------------------- /src/Graph.CrossCutting/Extensions/CodeBaseExtensions.cs: -------------------------------------------------------------------------------- 1 | using Graph.CrossCutting.Interfaces; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace Graph.CrossCutting.Extensions 8 | { 9 | public static class CodeBaseExtensions 10 | { 11 | public static bool IsNull(this object obj) 12 | { 13 | return obj == null; 14 | } 15 | 16 | public static void Update(this ICollection list, L toUpdate) where L : IDomain 17 | { 18 | var original = list.FirstOrDefault(i => i.Id == toUpdate.Id); 19 | 20 | if(!original.IsNull()) 21 | { 22 | list.Remove(original); 23 | list.Add(toUpdate); 24 | } 25 | } 26 | 27 | public static bool ListIsNullOrEmpty(this IEnumerable list) 28 | { 29 | return list.IsNull() || list.Count() == 0 || list.Any(i => i.IsNull()); 30 | } 31 | 32 | public static long ToUnixTime(this DateTime date) 33 | { 34 | var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); 35 | return Convert.ToInt64((date.ToUniversalTime() - epoch).TotalSeconds); 36 | } 37 | 38 | public static DateTime UnixToDateTime(this long unixTimeStamp) 39 | { 40 | var date = new DateTime(1970, 1, 1, 0, 0, 0, 0, System.DateTimeKind.Utc); 41 | return date.AddSeconds(unixTimeStamp).ToLocalTime(); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Graph.CrossCutting/Extensions/GraphQL/FilterUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Graph.CrossCutting.Extensions.GraphQL 6 | { 7 | public static class FilterUtils 8 | { 9 | public static IDictionary ParseArgumentFilter(this IDictionary filters) 10 | { 11 | var dictionary = new Dictionary(); 12 | if (filters == null) return dictionary; 13 | 14 | foreach (var filter in filters) 15 | { 16 | var innerDictionary = (IDictionary)filter.Value; 17 | 18 | var filterObject = new GraphFilter() 19 | { 20 | Value = GetValue(innerDictionary["value"]), 21 | Operation = innerDictionary["operation"].ToString() 22 | }; 23 | 24 | ValidateOperation(filterObject.Operation); 25 | 26 | dictionary.Add(filter.Key, filterObject); 27 | } 28 | 29 | return dictionary; 30 | 31 | } 32 | 33 | private static void ValidateOperation(string operation) 34 | { 35 | var allowedOperations = new[] { "le", "l", "c", "g", "ge", "e", "ne" }; 36 | 37 | if (!allowedOperations.Any(i => i == operation)) 38 | throw new ArgumentException("The type of provided filter is invalid"); 39 | } 40 | 41 | private static object GetValue(object obj) 42 | { 43 | DateTime date; 44 | if(DateTime.TryParse(obj.ToString(), out date)) 45 | { 46 | return date.ToUnixTime(); 47 | } 48 | 49 | return obj.ToString(); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/Graph.Tests/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Xunit; 3 | using Xunit.Abstractions; 4 | using Xunit.DependencyInjection; 5 | using Graph.CrossCutting.IoC; 6 | using Graph.Infrastructure.Database.Query.UserSchema; 7 | using Graph.Infrastructure.Database.Query.Manager; 8 | using Graph.Infrastructure.Database.Query.ProjectSchema; 9 | using Graph.Infrastructure.Database.Query.TaskSchema; 10 | 11 | [assembly: TestFramework("Graph.Tests.Startup", "Graph.Tests")] 12 | 13 | namespace Graph.Tests 14 | { 15 | public class Startup : DependencyInjectionTestFramework 16 | { 17 | public Startup(IMessageSink messageSink) : base(messageSink) { } 18 | 19 | protected override void ConfigureServices(IServiceCollection services) 20 | { 21 | services.ResolveGraphDependencies(true); 22 | services.ResolveRequestHandlers(); 23 | services.ResolveAuxiliaries(); 24 | 25 | services.AddTransient(sp => MockHelper.GetUserManager()); 26 | services.AddTransient, UserManager>(); 27 | 28 | services.AddTransient(sp => MockHelper.GetProjectManager()); 29 | services.AddTransient, ProjectManager>(); 30 | 31 | services.AddTransient(sp => MockHelper.GetTaskManager()); 32 | services.AddTransient, TaskManager>(); 33 | 34 | services.AddTransient(sp => MockHelper.GetServiceBus()); 35 | services.AddTransient(sp => MockHelper.GetUserRepository()); 36 | services.AddTransient(sp => MockHelper.GetProjectRepository()); 37 | services.AddTransient(sp => MockHelper.GetTaskRepository()); 38 | services.AddTransient(sp => MockHelper.GetUnitOfWork()); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Command/GraphContext.cs: -------------------------------------------------------------------------------- 1 | using Graph.CrossCutting; 2 | using Graph.Infrastructure.Database.Command; 3 | using Graph.Infrastructure.Database.Command.Model; 4 | using Microsoft.EntityFrameworkCore; 5 | using System; 6 | using System.Linq; 7 | 8 | namespace Graph.Infrastructure.Database.Command 9 | { 10 | public class GraphContext : DbContext 11 | { 12 | public GraphContext(DbContextOptions options) : base(options) 13 | { 14 | } 15 | 16 | public DbSet Users { get; set; } 17 | public DbSet Projects { get; set; } 18 | public DbSet Tasks { get; set; } 19 | public DbSet Statuses { get; set; } 20 | public DbSet UserProjects { get; set; } 21 | 22 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 23 | { 24 | optionsBuilder.UseLazyLoadingProxies(); 25 | } 26 | 27 | protected override void OnModelCreating(ModelBuilder modelBuilder) 28 | { 29 | var statuses = Enum.GetValues(typeof(TaskStatusEnum)).OfType().Select(i => new Status() { Id = (int)i, Description = i.ToString() }); 30 | 31 | modelBuilder.Entity().HasData(statuses); 32 | 33 | modelBuilder.Entity(e => 34 | { 35 | e.HasIndex(i => i.Email).IsUnique(); 36 | e.Property(i => i.Email).HasMaxLength(100).IsRequired(); 37 | e.Property(i => i.Name).HasMaxLength(100).IsRequired(); 38 | }); 39 | 40 | modelBuilder.Entity(e => 41 | { 42 | e.HasMany(i => i.Tasks).WithOne(i => i.Project); 43 | }); 44 | 45 | modelBuilder.Entity(e => 46 | { 47 | e.Ignore(i => i.Id); 48 | e.HasKey(i => new { i.Projectid, i.UserId }); 49 | }); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Graph.Infrastructure.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /test/Graph.Tests/Integration/Mutation/UserMutationTest.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Graph.Application.Graph.Mutation; 3 | using GraphQL; 4 | using GraphQL.Types; 5 | using Moq; 6 | using Xunit; 7 | 8 | namespace Graph.Tests 9 | { 10 | public class UserMutationTest 11 | { 12 | private readonly IDocumentExecuter _Executer; 13 | private readonly ISchema _Schema; 14 | 15 | public UserMutationTest(IDocumentExecuter executor, UserMutation mutation) 16 | { 17 | _Executer = executor; 18 | 19 | var schemaMock = new Mock().Object; 20 | schemaMock.Mutation = mutation; 21 | _Schema = schemaMock; 22 | } 23 | 24 | [Fact(DisplayName = "Add a valid User")] 25 | [Trait("Integration", "User")] 26 | public async void DocumentExecuter_UserMutation_AddUser_ValidUser() 27 | { 28 | var dataJson = "{ name: \"user\", email: \"user@email.com\" }"; 29 | 30 | var request = "mutation { addUser(data: " + dataJson.ToLower() + " ) { result } }"; 31 | 32 | var result = await _Executer.ExecuteAsync(_ => 33 | { 34 | _.Schema = _Schema; 35 | _.Query = request; 36 | }); 37 | 38 | result.Errors.Should().BeNullOrEmpty(); 39 | } 40 | 41 | [Fact(DisplayName = "Edit a valid User")] 42 | [Trait("Integration", "User")] 43 | public async void DocumentExecuter_UserMutation_EditUser_ValidUser() 44 | { 45 | var dataJson = "{ id: \"" + MockHelper.Guids[0] + "\", name: \"user\", email: \"user@email.com\" }"; 46 | 47 | var request = "mutation { editUserInfo(data: " + dataJson.ToLower() + " ) { result } }"; 48 | 49 | var result = await _Executer.ExecuteAsync(_ => 50 | { 51 | _.Schema = _Schema; 52 | _.Query = request; 53 | }); 54 | 55 | result.Errors.Should().BeNullOrEmpty(); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Command/Repository/Repository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using System.Linq; 6 | using Graph.Infrastructure.Database.Repository.Interfaces; 7 | using Microsoft.EntityFrameworkCore; 8 | using Graph.Infrastructure.Database.Command; 9 | using Graph.CrossCutting.Extensions; 10 | using Graph.CrossCutting.Interfaces; 11 | 12 | namespace Graph.Infrastructure.Database.Repository 13 | { 14 | public abstract class Repository : IRepository where T : class 15 | { 16 | protected GraphContext _Context; 17 | 18 | public Repository(GraphContext context) 19 | { 20 | _Context = context; 21 | } 22 | 23 | public virtual async Task Add(T obj) 24 | { 25 | await _Context.Set().AddAsync(obj); 26 | } 27 | 28 | public virtual Task> GetAll() 29 | { 30 | return Task.FromResult(_Context.Set().AsQueryable()); 31 | } 32 | 33 | public virtual async Task GetById(Guid id) 34 | { 35 | return await _Context.Set().FindAsync(id); 36 | } 37 | 38 | public virtual Task Remove(T obj) 39 | { 40 | ClearChangeTrack(); 41 | 42 | return Task.FromResult(_Context.Set().Remove(obj)); 43 | } 44 | 45 | public virtual Task Update(T obj) 46 | { 47 | ClearChangeTrack(); 48 | 49 | _Context.Set().Update(obj); 50 | 51 | return Task.CompletedTask; 52 | } 53 | 54 | public async void Dispose() 55 | { 56 | await _Context.DisposeAsync(); 57 | } 58 | 59 | protected void ClearChangeTrack() where E : class 60 | { 61 | var entry = _Context.ChangeTracker.Entries().FirstOrDefault(); 62 | if (!entry.IsNull()) entry.State = EntityState.Detached; 63 | } 64 | 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Graph.CrossCutting/Extensions/GraphQL/ArgumentUtils.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Language.AST; 2 | using System.Linq; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using System.Text.RegularExpressions; 6 | 7 | namespace Graph.CrossCutting.Extensions.GraphQL 8 | { 9 | public static class ArgumentUtils 10 | { 11 | public static string[] ParseSubFields(this IDictionary subFields) 12 | { 13 | var fields = new List(); 14 | 15 | foreach (var field in subFields) 16 | { 17 | var selections = field.Value.SelectionSet.Selections; 18 | 19 | if (selections.Any()) 20 | { 21 | var complexFieldType = field.Key.CapitalizeFirst(); 22 | 23 | var childFields = new List(); 24 | childFields.AddRange(selections.Select(sel => $"{complexFieldType}.{(sel as Field).Name}".ToLower())); 25 | 26 | fields.AddRange(childFields); 27 | } 28 | else 29 | { 30 | fields.Add(field.Key); 31 | } 32 | 33 | } 34 | 35 | return fields.ToArray(); 36 | } 37 | 38 | public static IEnumerable GetRelatedEntities(this IDictionary subFields) 39 | { 40 | return subFields.Where(i => i.Value.SelectionSet.Selections.Any()).Select(i => i.Key); 41 | } 42 | 43 | private static string CapitalizeFirst(this string field) 44 | { 45 | var complexFieldType = new StringBuilder(); 46 | complexFieldType.Append(field.Substring(0, 1).ToUpper()); 47 | complexFieldType.Append(field.Substring(1)); 48 | 49 | return complexFieldType.ToString(); 50 | } 51 | 52 | private static string[] SplitCamelCase(this string source) 53 | { 54 | return Regex.Split(source, @"(?(); 19 | } 20 | public User(Guid id, string name, string email, ICollection projects) 21 | { 22 | Id = id; 23 | Name = name; 24 | Email = email; 25 | Projects = projects; 26 | } 27 | 28 | public Guid Id { get; private set; } 29 | public string Name { get; private set; } 30 | public string Email { get; private set; } 31 | public ICollection Projects { get; private set; } 32 | public DomainState State { get; private set; } 33 | public void SetPersonalInfo(string name, string email) 34 | { 35 | this.Name = name; 36 | this.Email = email; 37 | 38 | this.Validate(); 39 | } 40 | 41 | public void SetStateForRelation(bool isInsert) 42 | { 43 | State = (isInsert) ? DomainState.ADD_RELATION : DomainState.REMOVE_RELATION; 44 | } 45 | 46 | public void AddProject(Project project) 47 | { 48 | Projects.Add(project); 49 | } 50 | 51 | public void RemoveProject(Project project) 52 | { 53 | Projects.Remove(Projects.FirstOrDefault(i => i.Id == project.Id)); 54 | } 55 | 56 | public void Validate() 57 | { 58 | var validator = new UserValidator(); 59 | 60 | var validationResult = validator.Validate(this); 61 | 62 | if (!validationResult.IsValid) throw new ValidationException(string.Join(";", validationResult.Errors.Select(i => i.ErrorCode))); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/User/Mutation/UserMutation.cs: -------------------------------------------------------------------------------- 1 | using Graph.Application.Commands; 2 | using Graph.Application.Commands.User; 3 | using Graph.Application.Graph.Common; 4 | using Graph.Application.Graph.Types.Input; 5 | using Graph.Application.Graph.User.Types; 6 | using GraphQL.Types; 7 | using MediatR; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Text; 12 | 13 | namespace Graph.Application.Graph.Mutation 14 | { 15 | public class UserMutation : ObjectGraphType 16 | { 17 | public UserMutation(IServiceProvider serviceProvider) 18 | { 19 | Name = "UserMutation"; 20 | Field( 21 | "addUser", 22 | arguments: new QueryArguments( 23 | new QueryArgument> { Name = "data" } 24 | ), 25 | resolve: context => 26 | { 27 | var command = context.GetArgument("data"); 28 | 29 | using(var scope = serviceProvider.CreateScope()) 30 | { 31 | var mediator = scope.ServiceProvider.GetRequiredService(); 32 | return mediator.Send(command).Result; 33 | } 34 | }); 35 | 36 | Field( 37 | "editUserInfo", 38 | arguments: new QueryArguments( 39 | new QueryArgument> { Name = "data" } 40 | ), 41 | resolve: context => 42 | { 43 | var command = context.GetArgument("data"); 44 | 45 | using (var scope = serviceProvider.CreateScope()) 46 | { 47 | var mediator = scope.ServiceProvider.GetRequiredService(); 48 | return mediator.Send(command).Result; 49 | } 50 | }); 51 | } 52 | 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/User/Query/UserQuery.cs: -------------------------------------------------------------------------------- 1 | using Graph.Application.Graph.User.Types.Query; 2 | using Graph.CrossCutting.Extensions.GraphQL; 3 | using Graph.Infrastructure.Database.Query.Manager; 4 | using Model = Graph.Infrastructure.Database.Query.UserSchema; 5 | using GraphQL.Types; 6 | using System; 7 | using System.Collections.Generic; 8 | using Graph.Application.Graph.Common; 9 | using Graph.Application.Graph.User.Types.Input; 10 | 11 | namespace Graph.Application.Graph.Query 12 | { 13 | public class UserQuery : ObjectGraphType 14 | { 15 | public UserQuery(IEntityManager query) 16 | { 17 | Func userById = (context, id) => query.GetById(Guid.Parse(id), context.SubFields.ParseSubFields()); 18 | 19 | Func users = context => query.Get 20 | ( 21 | context.SubFields.ParseSubFields(), 22 | context.GetArgument>("filter").ParseArgumentFilter(), 23 | context.GetArgument("order"), 24 | context.GetArgument("pagination").Skip, 25 | context.GetArgument("pagination").Take 26 | ) 27 | .Result; 28 | 29 | FieldDelegate( 30 | "user", 31 | arguments: new QueryArguments( 32 | new QueryArgument> { Name = "id", Description = "id of the user" } 33 | ), 34 | resolve: userById 35 | ); 36 | 37 | FieldDelegate>( 38 | "users", 39 | arguments: new QueryArguments( 40 | new QueryArgument> { Name = "pagination" }, 41 | new QueryArgument { Name = "order" }, 42 | new QueryArgument { Name = "filter" } 43 | ), 44 | resolve: users 45 | ); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/Project/Query/ProjectQuery.cs: -------------------------------------------------------------------------------- 1 | using Graph.Application.Graph.Common; 2 | using Graph.Application.Graph.Project.Types.Input; 3 | using Graph.Application.Graph.Project.Types.Query; 4 | using Graph.CrossCutting.Extensions.GraphQL; 5 | using Graph.Infrastructure.Database.Query.Manager; 6 | using GraphQL.Types; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Text; 10 | using Model = Graph.Infrastructure.Database.Query.ProjectSchema; 11 | 12 | namespace Graph.Application.Graph.Query 13 | { 14 | public class ProjectQuery : ObjectGraphType 15 | { 16 | public ProjectQuery(IEntityManager query) 17 | { 18 | Func projectById = (context, id) => query.GetById(Guid.Parse(id), context.SubFields.ParseSubFields()); 19 | 20 | Func projects = context => query.Get 21 | ( 22 | context.SubFields.ParseSubFields(), 23 | context.GetArgument>("filter").ParseArgumentFilter(), 24 | context.GetArgument("order"), 25 | context.GetArgument("pagination").Skip, 26 | context.GetArgument("pagination").Take 27 | ) 28 | .Result; 29 | 30 | FieldDelegate( 31 | "project", 32 | arguments: new QueryArguments( 33 | new QueryArgument> { Name = "id", Description = "id of the project" } 34 | ), 35 | resolve: projectById 36 | ); 37 | 38 | FieldDelegate>( 39 | "projects", 40 | arguments: new QueryArguments( 41 | new QueryArgument> { Name = "pagination" }, 42 | new QueryArgument { Name = "order" }, 43 | new QueryArgument { Name = "filter" } 44 | ), 45 | resolve: projects 46 | ); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/Task/Query/TaskQuery.cs: -------------------------------------------------------------------------------- 1 | using Graph.Application.Graph.Common; 2 | using Graph.Application.Graph.Project.Types.Input; 3 | using Graph.Application.Graph.Project.Types.Query; 4 | using Graph.Application.Graph.Task.Types.Query; 5 | using Graph.CrossCutting.Extensions.GraphQL; 6 | using Graph.Infrastructure.Database.Query.Manager; 7 | using GraphQL.Types; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Text; 11 | using Model = Graph.Infrastructure.Database.Query.TaskSchema; 12 | 13 | namespace Graph.Application.Graph.Query 14 | { 15 | public class TaskQuery : ObjectGraphType 16 | { 17 | public TaskQuery(IEntityManager query) 18 | { 19 | Func taskById = (context, id) => query.GetById(Guid.Parse(id), context.SubFields.ParseSubFields()); 20 | 21 | Func tasks = context => query.Get 22 | ( 23 | context.SubFields.ParseSubFields(), 24 | context.GetArgument>("filter").ParseArgumentFilter(), 25 | context.GetArgument("order"), 26 | context.GetArgument("pagination").Skip, 27 | context.GetArgument("pagination").Take 28 | ) 29 | .Result; 30 | 31 | FieldDelegate( 32 | "task", 33 | arguments: new QueryArguments( 34 | new QueryArgument> { Name = "id", Description = "id of the task" } 35 | ), 36 | resolve: taskById 37 | ); 38 | 39 | FieldDelegate>( 40 | "tasks", 41 | arguments: new QueryArguments( 42 | new QueryArgument> { Name = "pagination" }, 43 | new QueryArgument { Name = "order" }, 44 | new QueryArgument { Name = "filter" } 45 | ), 46 | resolve: tasks 47 | ); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Graph.Domain.Service/Mappings/TaskProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Command = Graph.Infrastructure.Database.Command.Model; 3 | using Query = Graph.Infrastructure.Database.Query.TaskSchema; 4 | using System.Linq; 5 | using System; 6 | using Graph.CrossCutting.Extensions; 7 | using Graph.Domain.Model; 8 | using Graph.CrossCutting; 9 | 10 | namespace Graph.Domain.Service.Mappings 11 | { 12 | public class TaskProfile : Profile 13 | { 14 | public TaskProfile() 15 | { 16 | CreateMap() 17 | .ForMember(i => i.ProjectId, j => j.MapFrom(m => m.Project.Id)) 18 | .ForMember(i => i.ReporterId, j => j.MapFrom(m => m.Reporter.Id)) 19 | .ForMember(i => i.AssigneeId, j => j.MapFrom(m => m.Assignee.Id)) 20 | .ForMember(i => i.StatusId, j => j.MapFrom(m => (int)m.Status)) 21 | .ForMember(i => i.Project, j => j.Ignore()) 22 | .ForMember(i => i.Reporter, j => j.Ignore()) 23 | .ForMember(i => i.Status, j => j.Ignore()) 24 | .ForMember(i => i.Assignee, j => j.Ignore()); 25 | 26 | CreateMap() 27 | .ForCtorParam("status", j => j.MapFrom(m => (TaskStatusEnum)m.StatusId)) 28 | .ForMember(i => i.State, j => j.MapFrom(m => DomainState.FROM_DB)) 29 | .ForMember(i => i.Project, j => j.MapFrom(m => m.Project)) 30 | .ForMember(i => i.Assignee, j => j.MapFrom(m => m.Assignee)) 31 | .ForMember(i => i.Status, j => j.Ignore()) 32 | .ForMember(i => i.Reporter, j => j.MapFrom(m => m.Reporter)); 33 | 34 | CreateMap() 35 | .ForMember(i => i.DeadLine, j => j.MapFrom(m => m.DeadLine.ToUnixTime())) 36 | .ForMember(i => i.CreatedDate, j => j.MapFrom(m => m.CreatedDate.ToUnixTime())) 37 | .ForMember(i => i.Status, j => j.MapFrom(m => m.Status)) 38 | .ForMember(i => i.Assignee, j => j.MapFrom(m => m.Assignee)) 39 | .ForMember(i => i.Reporter, j => j.MapFrom(m => m.Reporter)) 40 | .ForMember(i => i.Project, j => j.MapFrom(m => m.Project)); 41 | 42 | CreateMap() 43 | .ForMember(i => i.Id, j => j.MapFrom(m => m.Id)) 44 | .ForMember(i => i.Name, j => j.MapFrom(m => m.Name)); 45 | 46 | CreateMap() 47 | .ForMember(i => i.Id, j => j.MapFrom(m => m.Id)) 48 | .ForMember(i => i.Description, j => j.MapFrom(m => m.Description)); 49 | 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Graph.Domain.Service/CommandHandler/UserCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Graph.Application.Commands; 3 | using Command = Graph.Infrastructure.Database.Command.Model; 4 | using Query = Graph.Infrastructure.Database.Query.UserSchema; 5 | using Graph.Infrastructure.Database.Command.Interfaces; 6 | using MediatR; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using Graph.Infrastructure.ServiceBus; 10 | using Graph.CrossCutting.Exceptions; 11 | using Graph.CrossCutting.Extensions; 12 | using Graph.Application.Commands.User; 13 | 14 | namespace Graph.Domain.Service.CommandHandler 15 | { 16 | public class UserCommandHandler : IRequestHandler, 17 | IRequestHandler 18 | { 19 | private readonly IUnitOfWork _UnitOfWork; 20 | private readonly IServiceBus _Bus; 21 | private readonly IUserRepository _UserRepository; 22 | private readonly IMapper _Mapper; 23 | 24 | public UserCommandHandler(IUnitOfWork unitOfWork, IServiceBus bus, IUserRepository userRepository, IMapper mapper) 25 | { 26 | _UnitOfWork = unitOfWork; 27 | _Bus = bus; 28 | 29 | _UserRepository = userRepository; 30 | _Mapper = mapper; 31 | } 32 | 33 | public async Task Handle(AddUserCommand request, CancellationToken cancellationToken) 34 | { 35 | var userDomain = new User(request.Name, request.Email); 36 | userDomain.Validate(); 37 | 38 | await _UserRepository.Add(userDomain.ToModel(_Mapper)); 39 | await _UnitOfWork.Commit(); 40 | 41 | var publishMessage = new Message(); 42 | publishMessage.MessageType = "AddUser"; 43 | publishMessage.SetData(_Mapper.Map(userDomain)); 44 | 45 | await _Bus.SendMessage(publishMessage); 46 | 47 | return true; 48 | } 49 | 50 | public async Task Handle(UpdateUserInfoCommand request, CancellationToken cancellationToken) 51 | { 52 | var userDomain = _UserRepository.GetById(request.Id).Result.ToDomain(_Mapper); 53 | 54 | userDomain.SetPersonalInfo(request.Name, request.Email); 55 | 56 | await _UserRepository.Update(userDomain.ToModel(_Mapper)); 57 | await _UnitOfWork.Commit(); 58 | 59 | var publishMessage = new Message(); 60 | publishMessage.MessageType = "UpdateUser"; 61 | publishMessage.SetData(userDomain.ToQueryModel(_Mapper)); 62 | 63 | await _Bus.SendMessage(publishMessage); 64 | 65 | return true; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Graph.Domain.Service/Mappings/ProjectProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Command = Graph.Infrastructure.Database.Command.Model; 3 | using Query = Graph.Infrastructure.Database.Query.ProjectSchema; 4 | using System.Linq; 5 | using Graph.Domain.Model; 6 | using System; 7 | 8 | namespace Graph.Domain.Service.Mappings 9 | { 10 | public class ProjectProfile : Profile 11 | { 12 | public ProjectProfile() 13 | { 14 | CreateMap() 15 | .AfterMap((domain, entity) => 16 | { 17 | foreach (var user in domain.Users) 18 | { 19 | var userProject = new Command.UserProject() { Projectid = domain.Id, UserId = user.Id }; 20 | entity.UserProjects.Add(userProject); 21 | } 22 | }); 23 | 24 | CreateMap() 25 | .ForMember(i => i.Projectid, j => j.MapFrom(m => m.Id)) 26 | .ForMember(i => i.UserId, 27 | j => j.MapFrom(m => 28 | m.Users.FirstOrDefault(u => u.State == DomainState.REMOVE_RELATION 29 | || u.State == DomainState.ADD_RELATION 30 | ).Id 31 | ) 32 | ); 33 | 34 | CreateMap() 35 | .ForMember(i => i.Tasks, j => j.MapFrom(m => m.Tasks)) 36 | .ForMember(i => i.Users, j => j.MapFrom(m => m.UserProjects.Select(s => s.User))) 37 | .ForMember(i => i.State, j => j.MapFrom(m => DomainState.FROM_DB)); 38 | 39 | CreateMap() 40 | .ForMember(i => i.Tasks, j => j.MapFrom(m => m.Tasks)) 41 | .ForMember(i => i.Participants, j => j.MapFrom(m => m.Users.Where(u => u.State != DomainState.REMOVE_RELATION))) 42 | .ForMember(i => i.FinishedCount, j => j.MapFrom(m => m.Tasks.Where(i => i.Status == CrossCutting.TaskStatusEnum.DONE).Count())) 43 | .ForMember(i => i.UnfinishedCount, j => j.MapFrom(m => m.Tasks.Where(i => i.Status != CrossCutting.TaskStatusEnum.DONE).Count())); 44 | 45 | CreateMap() 46 | .ForMember(i => i.Tasks, j => j.MapFrom(m => m.Tasks)) 47 | .ForMember(i => i.Participants, j => j.MapFrom(m => m.UserProjects.Select(s => s.User))); 48 | 49 | CreateMap() 50 | .ForMember(i => i.Responsible, j => j.MapFrom(m => m.Assignee.Name)); 51 | 52 | CreateMap(); 53 | 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Graph.API/Startup.cs: -------------------------------------------------------------------------------- 1 | using Graph.Application.MessageHandler; 2 | using Graph.Infrastructure.Database.Command; 3 | using Graph.Infrastructure.ServiceBus; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Hosting; 9 | using Graph.CrossCutting.IoC; 10 | using Microsoft.Extensions.Options; 11 | using MediatR; 12 | using AutoMapper; 13 | using GraphQL.Server.Ui.Playground; 14 | using Graph.Domain.Service.Mappings; 15 | using Graph.Domain.Service.CommandHandler; 16 | using Graph.Infrastructure.Database; 17 | using Microsoft.EntityFrameworkCore; 18 | 19 | namespace Graph.API 20 | { 21 | public class Startup 22 | { 23 | public Startup(IConfiguration configuration) 24 | { 25 | Configuration = configuration; 26 | } 27 | 28 | public IConfiguration Configuration { get; } 29 | 30 | // This method gets called by the runtime. Use this method to add services to the container. 31 | public void ConfigureServices(IServiceCollection services) 32 | { 33 | services.Configure(Configuration.GetSection("ServiceBus")); 34 | services.Configure(Configuration.GetSection("ConnectionStrings")); 35 | 36 | services.AddControllers(); 37 | 38 | services.ResolveServiceBus(); 39 | services.ResolveCommandDatabase(); 40 | services.ResolveQueryDatabase(); 41 | services.ResolveGraphDependencies(); 42 | services.ResolveRequestHandlers(); 43 | services.ResolveAuxiliaries(); 44 | } 45 | 46 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 47 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 48 | { 49 | if (env.IsDevelopment()) 50 | { 51 | app.UseDeveloperExceptionPage(); 52 | } 53 | 54 | app.UseMiddleware(new GraphQLSettings 55 | { 56 | Path = "/api/graphql", 57 | BuildUserContext = ctx => new GraphQLUserContext 58 | { 59 | User = ctx.User 60 | }, 61 | EnableMetrics = true 62 | }); 63 | 64 | app.UseGraphQLPlayground(new GraphQLPlaygroundOptions 65 | { 66 | Path = "/ui/playground", 67 | GraphQLEndPoint = "/api/graphql", 68 | 69 | }); 70 | 71 | app.UseHttpsRedirection(); 72 | UpdateDatabase(app); 73 | } 74 | 75 | private static void UpdateDatabase(IApplicationBuilder app) 76 | { 77 | using (var serviceScope = app.ApplicationServices 78 | .GetRequiredService() 79 | .CreateScope()) 80 | { 81 | using (var context = serviceScope.ServiceProvider.GetRequiredService()) 82 | { 83 | context.Database.EnsureCreated(); 84 | if(!context.Database.IsInMemory()) context.Database.Migrate(); 85 | } 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /test/Graph.Tests/Integration/Mutation/ProjectMutationTest.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Graph.Application.Graph.Mutation; 3 | using GraphQL; 4 | using GraphQL.Types; 5 | using Moq; 6 | using Xunit; 7 | 8 | namespace Graph.Tests 9 | { 10 | public class ProjectMutationTest 11 | { 12 | private readonly IDocumentExecuter _Executer; 13 | private readonly ISchema _Schema; 14 | 15 | public ProjectMutationTest(IDocumentExecuter executor, ProjectMutation mutation) 16 | { 17 | _Executer = executor; 18 | 19 | var schemaMock = new Mock().Object; 20 | schemaMock.Mutation = mutation; 21 | _Schema = schemaMock; 22 | } 23 | 24 | [Fact(DisplayName = "Add a valid project")] 25 | [Trait("Integration", "Project")] 26 | public async void DocumentExecuter_ProjectMutation_AddProject_ValidProject() 27 | { 28 | var dataJson = "{ description: \"Project\", longDescription: \"Scrum Project\" }"; 29 | 30 | var request = "mutation { addProject(data: " + dataJson + " ) { result } }"; 31 | 32 | var result = await _Executer.ExecuteAsync(_ => 33 | { 34 | _.Schema = _Schema; 35 | _.Query = request; 36 | }); 37 | 38 | result.Errors.Should().BeNullOrEmpty(); 39 | } 40 | 41 | [Fact(DisplayName = "Edit a valid project")] 42 | [Trait("Integration", "Project")] 43 | public async void DocumentExecuter_ProjectMutation_EditProject_ValidProject() 44 | { 45 | var dataJson = "{ id: \"" + MockHelper.Guids[3] + "\", description: \"Project\", longDescription: \"Kanban Project\" }"; 46 | 47 | var request = "mutation { updateProjectInfo(data: " + dataJson + " ) { result } }"; 48 | 49 | var result = await _Executer.ExecuteAsync(_ => 50 | { 51 | _.Schema = _Schema; 52 | _.Query = request; 53 | }); 54 | 55 | result.Errors.Should().BeNullOrEmpty(); 56 | } 57 | 58 | [Fact(DisplayName = "Add user to a valid project")] 59 | [Trait("Integration", "Project")] 60 | public async void DocumentExecuter_ProjectMutation_AddUserProject_ValidProject() 61 | { 62 | var dataJson = "{ projectId: \"" + MockHelper.Guids[3] + "\", userId: \"" + MockHelper.Guids[2] + "\" }"; 63 | 64 | var request = "mutation { addUserProject(data: " + dataJson + " ) { result } }"; 65 | 66 | var result = await _Executer.ExecuteAsync(_ => 67 | { 68 | _.Schema = _Schema; 69 | _.Query = request; 70 | }); 71 | 72 | result.Errors.Should().BeNullOrEmpty(); 73 | } 74 | 75 | [Fact(DisplayName = "Remove user to a valid project")] 76 | [Trait("Integration", "Project")] 77 | public async void DocumentExecuter_ProjectMutation_RemoveUserProject_ValidProject() 78 | { 79 | var dataJson = "{ projectId: \"" + MockHelper.Guids[4] + "\", userId: \"" + MockHelper.Guids[1] + "\" }"; 80 | 81 | var request = "mutation { removeUserProject(data: " + dataJson + " ) { result } }"; 82 | 83 | var result = await _Executer.ExecuteAsync(_ => 84 | { 85 | _.Schema = _Schema; 86 | _.Query = request; 87 | }); 88 | 89 | result.Errors.Should().BeNullOrEmpty(); 90 | } 91 | 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Graph.Domain/Model/Task.cs: -------------------------------------------------------------------------------- 1 | using Graph.CrossCutting; 2 | using Graph.CrossCutting.Exceptions; 3 | using Graph.CrossCutting.Interfaces; 4 | using Graph.Domain.Model; 5 | using Graph.Domain.Validator; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | 10 | namespace Graph.Domain 11 | { 12 | public class Task : IDomain 13 | { 14 | public Task(string description, string longDescription, DateTime deadLine, User assignee, User reporter, Project project) 15 | { 16 | Id = Guid.NewGuid(); 17 | Description = description; 18 | LongDescription = longDescription; 19 | CreatedDate = DateTime.UtcNow; 20 | DeadLine = deadLine; 21 | Assignee = assignee; 22 | Reporter = reporter; 23 | Project = project; 24 | Status = TaskStatusEnum.BACKLOG; 25 | State = DomainState.NEW; 26 | } 27 | 28 | public Task(Guid id, string description, string longDescription, DateTime createdDate, DateTime deadLine, User assignee, User reporter, Project project, TaskStatusEnum status) 29 | { 30 | Id = id; 31 | Description = description; 32 | LongDescription = longDescription; 33 | CreatedDate = createdDate; 34 | DeadLine = deadLine; 35 | Assignee = assignee; 36 | Reporter = reporter; 37 | Status = status; 38 | Project = project; 39 | State = DomainState.FROM_DB; 40 | } 41 | 42 | public Guid Id { get; private set; } 43 | public string Description { get; private set; } 44 | public string LongDescription { get; private set; } 45 | public DateTime CreatedDate { get; private set; } 46 | public DateTime DeadLine { get; private set; } 47 | public User Assignee { get; private set; } 48 | public User Reporter { get; private set; } 49 | public Project Project { get; private set; } 50 | public TaskStatusEnum Status { get; private set; } 51 | public DomainState State { get; private set; } 52 | 53 | public void SetDescription(string description, string longDescription) 54 | { 55 | this.Description = description; 56 | this.LongDescription = longDescription; 57 | 58 | this.Validate(); 59 | 60 | this.Project.UpdateTask(this); 61 | } 62 | 63 | public void SetStatus(TaskStatusEnum status) 64 | { 65 | this.Status = status; 66 | 67 | this.Validate(); 68 | 69 | this.Project.UpdateTask(this); 70 | } 71 | 72 | public void SetDeadline(DateTime deadline) 73 | { 74 | this.DeadLine = deadline; 75 | 76 | this.Validate(); 77 | 78 | this.Project.UpdateTask(this); 79 | } 80 | 81 | public void ChangeAssignee(User assignee) 82 | { 83 | assignee.Validate(); 84 | 85 | this.Assignee = assignee; 86 | 87 | this.Validate(); 88 | 89 | this.Project.UpdateTask(this); 90 | } 91 | 92 | public void Validate() 93 | { 94 | var validator = new TaskValidator(); 95 | 96 | var validationResult = validator.Validate(this); 97 | 98 | if (!validationResult.IsValid) throw new ValidationException(string.Join(";", validationResult.Errors.Select(i => i.ErrorCode))); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/Project/Mutation/ProjectMutation.cs: -------------------------------------------------------------------------------- 1 | using Graph.Application.Commands; 2 | using Graph.Application.Commands.Project; 3 | using Graph.Application.Graph.Common; 4 | using Graph.Application.Graph.Project.Types.Input; 5 | using Graph.Application.Graph.User.Types; 6 | using GraphQL.Types; 7 | using MediatR; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Text; 12 | 13 | namespace Graph.Application.Graph.Mutation 14 | { 15 | public class ProjectMutation : ObjectGraphType 16 | { 17 | public ProjectMutation(IServiceProvider serviceProvider) 18 | { 19 | Name = "projectMutation"; 20 | Field( 21 | "addProject", 22 | arguments: new QueryArguments( 23 | new QueryArgument> { Name = "data" } 24 | ), 25 | resolve: context => 26 | { 27 | var command = context.GetArgument("data"); 28 | 29 | using (var scope = serviceProvider.CreateScope()) 30 | { 31 | var mediator = scope.ServiceProvider.GetRequiredService(); 32 | return mediator.Send(command).Result; 33 | } 34 | }); 35 | 36 | Field( 37 | "addUserProject", 38 | arguments: new QueryArguments( 39 | new QueryArgument> { Name = "data" } 40 | ), 41 | resolve: context => 42 | { 43 | var command = context.GetArgument("data"); 44 | 45 | using (var scope = serviceProvider.CreateScope()) 46 | { 47 | var mediator = scope.ServiceProvider.GetRequiredService(); 48 | return mediator.Send(command).Result; 49 | } 50 | }); 51 | 52 | Field( 53 | "removeUserProject", 54 | arguments: new QueryArguments( 55 | new QueryArgument> { Name = "data" } 56 | ), 57 | resolve: context => 58 | { 59 | var command = context.GetArgument("data"); 60 | 61 | using (var scope = serviceProvider.CreateScope()) 62 | { 63 | var mediator = scope.ServiceProvider.GetRequiredService(); 64 | return mediator.Send(command).Result; 65 | } 66 | }); 67 | 68 | Field( 69 | "updateProjectInfo", 70 | arguments: new QueryArguments( 71 | new QueryArgument> { Name = "data" } 72 | ), 73 | resolve: context => 74 | { 75 | var command = context.GetArgument("data"); 76 | 77 | using (var scope = serviceProvider.CreateScope()) 78 | { 79 | var mediator = scope.ServiceProvider.GetRequiredService(); 80 | return mediator.Send(command).Result; 81 | } 82 | }); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /test/Graph.Tests/Integration/Query/UserQueryTest.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Graph.Application.Graph.Query; 3 | using Graph.Infrastructure.Database.Query.UserSchema; 4 | using GraphQL; 5 | using GraphQL.Types; 6 | using Moq; 7 | using System.Collections.Generic; 8 | using Xunit; 9 | using AutoMapper; 10 | using System.Linq; 11 | 12 | namespace Graph.Tests 13 | { 14 | public class UserQueryTest 15 | { 16 | private readonly IDocumentExecuter _Executer; 17 | private readonly ISchema _Schema; 18 | 19 | public UserQueryTest(IDocumentExecuter executor, UserQuery query) 20 | { 21 | _Executer = executor; 22 | 23 | var schemaMock = new Mock().Object; 24 | schemaMock.Query = query; 25 | _Schema = schemaMock; 26 | } 27 | 28 | [Fact(DisplayName = "Invalid User Query ( without pagination )")] 29 | [Trait("Integration", "User")] 30 | public async void DocumentExecuter_UserQuery_InvalidQuery_MissingPagination() 31 | { 32 | var request = "query { users() { id, name } }"; 33 | 34 | var result = await _Executer.ExecuteAsync(_ => 35 | { 36 | _.Schema = _Schema; 37 | _.Query = request; 38 | }); 39 | 40 | result.Errors.Should().NotBeEmpty(); 41 | } 42 | 43 | [Fact(DisplayName = "Invalid User Query ( without projection )")] 44 | [Trait("Integration", "User")] 45 | public async void DocumentExecuter_UserQuery_InvalidQuery_MissingProjection() 46 | { 47 | var request = "query { users(pagination: { skip: 0, take: 10 }) { } }"; 48 | 49 | var result = await _Executer.ExecuteAsync(_ => 50 | { 51 | _.Schema = _Schema; 52 | _.Query = request; 53 | }); 54 | 55 | result.Errors.Should().NotBeEmpty(); 56 | } 57 | 58 | [Fact(DisplayName = "Get valid users")] 59 | [Trait("Integration", "User")] 60 | public async void DocumentExecuter_UserQuery_GetUsers() 61 | { 62 | var request = "query { users(pagination: { skip: 0, take: 10 }) { id, name } }"; 63 | 64 | var result = await _Executer.ExecuteAsync(_ => 65 | { 66 | _.Schema = _Schema; 67 | _.Query = request; 68 | }); 69 | 70 | result.Errors.Should().BeNullOrEmpty(); 71 | } 72 | 73 | [Fact(DisplayName = "Get valid users with filters")] 74 | [Trait("Integration", "User")] 75 | public async void DocumentExecuter_UserQuery_GetUsers_WithFilters() 76 | { 77 | var request = "query { users(pagination: { skip: 0, take: 10 }, filter: { name: {operation: \"c\", value: \"a\" } }) { id, name, projects { description } } }"; 78 | 79 | var result = await _Executer.ExecuteAsync(_ => 80 | { 81 | _.Schema = _Schema; 82 | _.Query = request; 83 | }); 84 | 85 | result.Errors.Should().BeNullOrEmpty(); 86 | } 87 | 88 | [Fact(DisplayName = "Get a valid user")] 89 | [Trait("Integration", "User")] 90 | public async void DocumentExecuter_UserQuery_GetUser() 91 | { 92 | var request = "query { user(id: \""+ MockHelper.Guids[1] +"\") { id, name } }"; 93 | 94 | var result = await _Executer.ExecuteAsync(_ => 95 | { 96 | _.Schema = _Schema; 97 | _.Query = request; 98 | }); 99 | 100 | result.Errors.Should().BeNullOrEmpty(); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /test/Graph.Tests/Integration/Query/TaskQueryTest.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Graph.Application.Graph.Query; 3 | using Graph.Infrastructure.Database.Query.TaskSchema; 4 | using GraphQL; 5 | using GraphQL.Types; 6 | using Moq; 7 | using System.Collections.Generic; 8 | using Xunit; 9 | using AutoMapper; 10 | using System.Linq; 11 | 12 | namespace Graph.Tests 13 | { 14 | public class TaskQueryTest 15 | { 16 | private readonly IDocumentExecuter _Executer; 17 | private readonly ISchema _Schema; 18 | 19 | public TaskQueryTest(IDocumentExecuter executor, TaskQuery query) 20 | { 21 | _Executer = executor; 22 | 23 | var schemaMock = new Mock().Object; 24 | schemaMock.Query = query; 25 | _Schema = schemaMock; 26 | } 27 | 28 | [Fact(DisplayName = "Invalid Task Query ( without pagination )")] 29 | [Trait("Integration", "Task")] 30 | public async void DocumentExecuter_TaskQuery_InvalidQuery_MissingPagination() 31 | { 32 | var request = "query { tasks() { id, description } }"; 33 | 34 | var result = await _Executer.ExecuteAsync(_ => 35 | { 36 | _.Schema = _Schema; 37 | _.Query = request; 38 | }); 39 | 40 | result.Errors.Should().NotBeEmpty(); 41 | } 42 | 43 | [Fact(DisplayName = "Invalid Task Query ( without taskion )")] 44 | [Trait("Integration", "Task")] 45 | public async void DocumentExecuter_TaskQuery_InvalidQuery_MissingTaskion() 46 | { 47 | var request = "query { tasks(pagination: { skip: 0, take: 10 }) { } }"; 48 | 49 | var result = await _Executer.ExecuteAsync(_ => 50 | { 51 | _.Schema = _Schema; 52 | _.Query = request; 53 | }); 54 | 55 | result.Errors.Should().NotBeEmpty(); 56 | } 57 | 58 | [Fact(DisplayName = "Get valid tasks")] 59 | [Trait("Integration", "Task")] 60 | public async void DocumentExecuter_TaskQuery_GetTasks() 61 | { 62 | var request = "query { tasks(pagination: { skip: 0, take: 10 }) { id, description } }"; 63 | 64 | var result = await _Executer.ExecuteAsync(_ => 65 | { 66 | _.Schema = _Schema; 67 | _.Query = request; 68 | }); 69 | 70 | result.Errors.Should().BeNullOrEmpty(); 71 | } 72 | 73 | [Fact(DisplayName = "Get valid tasks with filters")] 74 | [Trait("Integration", "Task")] 75 | public async void DocumentExecuter_TaskQuery_GetTasks_WithFilters() 76 | { 77 | var request = "query { tasks(pagination: { skip: 0, take: 10 }, filter: { description: {operation: \"c\", value: \"a\" } }) { id, description } }"; 78 | 79 | var result = await _Executer.ExecuteAsync(_ => 80 | { 81 | _.Schema = _Schema; 82 | _.Query = request; 83 | }); 84 | 85 | result.Errors.Should().BeNullOrEmpty(); 86 | } 87 | 88 | [Fact(DisplayName = "Get a valid task")] 89 | [Trait("Integration", "Task")] 90 | public async void DocumentExecuter_TaskQuery_GetTask() 91 | { 92 | var request = "query { task(id: \""+ MockHelper.Guids[5] + "\") { id, description } }"; 93 | 94 | var result = await _Executer.ExecuteAsync(_ => 95 | { 96 | _.Schema = _Schema; 97 | _.Query = request; 98 | }); 99 | 100 | result.Errors.Should().BeNullOrEmpty(); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/ServiceBus/MassTransitSB.cs: -------------------------------------------------------------------------------- 1 | using MassTransit; 2 | using MassTransit.AzureServiceBusTransport; 3 | using Microsoft.Extensions.Options; 4 | using Microsoft.ServiceBus; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace Graph.Infrastructure.ServiceBus 11 | { 12 | public class MassTransitSB : IServiceBus 13 | { 14 | private readonly IBusControl _Bus; 15 | private readonly ISubscribe _Subscriber; 16 | 17 | public MassTransitSB(IOptions configuration, ISubscribe subscribe) 18 | { 19 | _Subscriber = subscribe; 20 | _Bus = GetBusInstance(configuration); 21 | _Bus.StartAsync().Wait(); 22 | } 23 | 24 | private IBusControl GetBusInstance(IOptions configuration) 25 | { 26 | var config = configuration.Value; 27 | 28 | IBusControl bus = null; 29 | 30 | switch (config.Transport) 31 | { 32 | case BusTransport.RABBITMQ: 33 | bus = Bus.Factory.CreateUsingRabbitMq(cfg => 34 | { 35 | var host = cfg.Host(new Uri(config.Url), c => 36 | { 37 | c.Username(config.Credentials.Username); 38 | c.Password(config.Credentials.Password); 39 | }); 40 | 41 | cfg.ReceiveEndpoint(host, config.Queue, e => 42 | { 43 | e.Handler(ctx => _Subscriber.HandleMessage(ctx.Message)); 44 | 45 | EndpointConvention.Map(e.InputAddress); 46 | }); 47 | 48 | }); 49 | break; 50 | case BusTransport.AZURE: 51 | bus = Bus.Factory.CreateUsingAzureServiceBus(cfg => 52 | { 53 | var host = cfg.Host(config.Url, c => 54 | { 55 | c.SharedAccessSignature(s => 56 | { 57 | s.KeyName = config.Credentials.KeyName; 58 | s.SharedAccessKey = config.Credentials.SharedAccessKey; 59 | s.TokenTimeToLive = TimeSpan.FromDays(config.Credentials.TokenTimeToLive); 60 | s.TokenScope = TokenScope.Namespace; 61 | }); 62 | }); 63 | 64 | cfg.ReceiveEndpoint(host, config.Queue, e => 65 | { 66 | e.Handler(ctx => _Subscriber.HandleMessage(ctx.Message)); 67 | 68 | EndpointConvention.Map(e.InputAddress); 69 | }); 70 | }); 71 | break; 72 | default: 73 | bus = Bus.Factory.CreateUsingInMemory(cfg => 74 | { 75 | cfg.ReceiveEndpoint("memory.queue", ep => 76 | { 77 | ep.Handler(ctx => _Subscriber.HandleMessage(ctx.Message)); 78 | 79 | EndpointConvention.Map(ep.InputAddress); 80 | }); 81 | }); 82 | break; 83 | } 84 | 85 | return bus; 86 | } 87 | 88 | public Task SendMessage(Message message) 89 | { 90 | return _Bus.Send(message); 91 | } 92 | 93 | 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /test/Graph.Tests/Integration/Query/ProjectQueryTest.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Graph.Application.Graph.Query; 3 | using Graph.Infrastructure.Database.Query.ProjectSchema; 4 | using GraphQL; 5 | using GraphQL.Types; 6 | using Moq; 7 | using System.Collections.Generic; 8 | using Xunit; 9 | using AutoMapper; 10 | using System.Linq; 11 | 12 | namespace Graph.Tests 13 | { 14 | public class ProjectQueryTest 15 | { 16 | private readonly IDocumentExecuter _Executer; 17 | private readonly ISchema _Schema; 18 | 19 | public ProjectQueryTest(IDocumentExecuter executor, ProjectQuery query) 20 | { 21 | _Executer = executor; 22 | 23 | var schemaMock = new Mock().Object; 24 | schemaMock.Query = query; 25 | _Schema = schemaMock; 26 | } 27 | 28 | [Fact(DisplayName = "Invalid Project Query ( without pagination )")] 29 | [Trait("Integration", "Project")] 30 | public async void DocumentExecuter_ProjectQuery_InvalidQuery_MissingPagination() 31 | { 32 | var request = "query { projects() { id, description } }"; 33 | 34 | var result = await _Executer.ExecuteAsync(_ => 35 | { 36 | _.Schema = _Schema; 37 | _.Query = request; 38 | }); 39 | 40 | result.Errors.Should().NotBeEmpty(); 41 | } 42 | 43 | [Fact(DisplayName = "Invalid Project Query ( without projection )")] 44 | [Trait("Integration", "Project")] 45 | public async void DocumentExecuter_ProjectQuery_InvalidQuery_MissingProjection() 46 | { 47 | var request = "query { projects(pagination: { skip: 0, take: 10 }) { } }"; 48 | 49 | var result = await _Executer.ExecuteAsync(_ => 50 | { 51 | _.Schema = _Schema; 52 | _.Query = request; 53 | }); 54 | 55 | result.Errors.Should().NotBeEmpty(); 56 | } 57 | 58 | [Fact(DisplayName = "Get valid projects")] 59 | [Trait("Integration", "Project")] 60 | public async void DocumentExecuter_ProjectQuery_GetProjects() 61 | { 62 | var request = "query { projects(pagination: { skip: 0, take: 10 }) { id, description } }"; 63 | 64 | var result = await _Executer.ExecuteAsync(_ => 65 | { 66 | _.Schema = _Schema; 67 | _.Query = request; 68 | }); 69 | 70 | result.Errors.Should().BeNullOrEmpty(); 71 | } 72 | 73 | [Fact(DisplayName = "Get valid projects with filters")] 74 | [Trait("Integration", "Project")] 75 | public async void DocumentExecuter_ProjectQuery_GetProjects_WithFilters() 76 | { 77 | var request = "query { projects(pagination: { skip: 0, take: 10 }, filter: { description: {operation: \"c\", value: \"a\" } }) { id, description } }"; 78 | 79 | var result = await _Executer.ExecuteAsync(_ => 80 | { 81 | _.Schema = _Schema; 82 | _.Query = request; 83 | }); 84 | 85 | result.Errors.Should().BeNullOrEmpty(); 86 | } 87 | 88 | [Fact(DisplayName = "Get a valid project")] 89 | [Trait("Integration", "Project")] 90 | public async void DocumentExecuter_ProjectQuery_GetProject() 91 | { 92 | var request = "query { project(id: \""+ MockHelper.Guids[3] + "\") { id, description } }"; 93 | 94 | var result = await _Executer.ExecuteAsync(_ => 95 | { 96 | _.Schema = _Schema; 97 | _.Query = request; 98 | }); 99 | 100 | result.Errors.Should().BeNullOrEmpty(); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Graph.API/Graph/GraphQLMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using GraphQL; 9 | using GraphQL.Http; 10 | using GraphQL.Instrumentation; 11 | using GraphQL.Types; 12 | using GraphQL.Validation; 13 | using Microsoft.AspNetCore.Http; 14 | using Newtonsoft.Json; 15 | 16 | namespace Graph.API 17 | { 18 | public class GraphQLMiddleware 19 | { 20 | private readonly RequestDelegate _next; 21 | private readonly GraphQLSettings _settings; 22 | private readonly IDocumentExecuter _executer; 23 | 24 | private static readonly JsonArrayPool _ArrayPool = new JsonArrayPool(ArrayPool.Shared); 25 | 26 | public GraphQLMiddleware( 27 | RequestDelegate next, 28 | GraphQLSettings settings, 29 | IDocumentExecuter executer 30 | ) 31 | { 32 | _next = next; 33 | _settings = settings; 34 | _executer = executer; 35 | } 36 | 37 | public async Task Invoke(HttpContext context, ISchema schema) 38 | { 39 | if (!IsGraphQLRequest(context)) 40 | { 41 | await _next(context); 42 | return; 43 | } 44 | 45 | await ExecuteAsync(context, schema); 46 | } 47 | 48 | private bool IsGraphQLRequest(HttpContext context) 49 | { 50 | return context.Request.Path.StartsWithSegments(_settings.Path) 51 | && string.Equals(context.Request.Method, "POST", StringComparison.OrdinalIgnoreCase); 52 | } 53 | 54 | private async Task ExecuteAsync(HttpContext context, ISchema schema) 55 | { 56 | var request = await Deserialize(context.Request.Body); 57 | 58 | var result = await _executer.ExecuteAsync(_ => 59 | { 60 | _.Schema = schema; 61 | _.Query = request?.Query; 62 | _.OperationName = request?.OperationName; 63 | _.Inputs = request?.Variables.ToInputs(); 64 | _.UserContext = _settings.BuildUserContext?.Invoke(context); 65 | _.ValidationRules = DocumentValidator.CoreRules().Concat(new [] { new InputValidationRule() }); 66 | _.EnableMetrics = _settings.EnableMetrics; 67 | if (_settings.EnableMetrics) 68 | { 69 | _.FieldMiddleware.Use(); 70 | } 71 | }); 72 | 73 | await WriteResponseAsync(context, result); 74 | } 75 | 76 | private async Task WriteResponseAsync(HttpContext context, ExecutionResult result) 77 | { 78 | context.Response.ContentType = "application/json"; 79 | context.Response.StatusCode = result.Errors?.Any() == true ? (int)HttpStatusCode.BadRequest : (int)HttpStatusCode.OK; 80 | 81 | await WriteAsync(context.Response.Body, result); 82 | } 83 | 84 | private async static Task Deserialize(Stream s) 85 | { 86 | using (var reader = new StreamReader(s)) 87 | { 88 | var content = await reader.ReadToEndAsync(); 89 | return JsonConvert.DeserializeObject(content); 90 | } 91 | } 92 | 93 | private async static Task WriteAsync(Stream stream, ExecutionResult value) 94 | { 95 | using (var writer = new HttpResponseStreamWriter(stream, new UTF8Encoding(false))) 96 | using (var jsonWriter = new JsonTextWriter(writer) 97 | { 98 | ArrayPool = _ArrayPool, 99 | CloseOutput = false, 100 | AutoCompleteOnClose = false, 101 | }) 102 | { 103 | await jsonWriter.WriteRawValueAsync(JsonConvert.SerializeObject(value.GetValue(), Formatting.Indented)); 104 | await jsonWriter.FlushAsync().ConfigureAwait(false); 105 | } 106 | } 107 | 108 | 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Graph.Domain/Model/Project.cs: -------------------------------------------------------------------------------- 1 | using Graph.CrossCutting.Exceptions; 2 | using Graph.CrossCutting.Extensions; 3 | using Graph.CrossCutting.Interfaces; 4 | using Graph.Domain.Model; 5 | using Graph.Domain.Validator; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Text; 10 | 11 | namespace Graph.Domain 12 | { 13 | public class Project : IDomain 14 | { 15 | public Project(string description, string longDescription) 16 | { 17 | Id = Guid.NewGuid(); 18 | Description = description; 19 | LongDescription = longDescription; 20 | Users = new List(); 21 | Tasks = new List(); 22 | State = DomainState.NEW; 23 | } 24 | 25 | public Project(Guid id, string description, string longDescription, ICollection users, ICollection tasks) 26 | { 27 | Id = id; 28 | Description = description; 29 | LongDescription = longDescription; 30 | Users = users; 31 | Tasks = tasks; 32 | State = DomainState.FROM_DB; 33 | } 34 | 35 | public Guid Id { get; private set; } 36 | public string Description { get; private set; } 37 | public string LongDescription { get; private set; } 38 | public ICollection Users { get; private set; } 39 | public ICollection Tasks { get; private set; } 40 | public DomainState State { get; private set; } 41 | 42 | public void AddUser(User user) 43 | { 44 | user.Validate(); 45 | 46 | if (Users.Any(i => i.Id == user.Id)) throw new ValidationException("PROJUSER-01"); 47 | 48 | user.SetStateForRelation(true); 49 | 50 | user.AddProject(this); 51 | Users.Add(user); 52 | } 53 | 54 | public void RemoveUser(User user) 55 | { 56 | user.Validate(); 57 | 58 | if (!Users.Any(i => i.Id == user.Id)) throw new ValidationException("PROJUSER-02"); 59 | 60 | user.SetStateForRelation(false); 61 | 62 | user.RemoveProject(this); 63 | Users.Update(user); 64 | 65 | 66 | } 67 | 68 | public void AddTask(Task task) 69 | { 70 | task.Validate(); 71 | 72 | if (Tasks.Any(i => i.Id == task.Id)) throw new ValidationException("PROJTASK-01"); 73 | if (!Users.Any(i => i.Id == task.Assignee.Id) || !Users.Any(i => i.Id == task.Reporter.Id)) throw new ValidationException("PROJTASK-02"); 74 | 75 | Tasks.Add(task); 76 | } 77 | 78 | public void UpdateTask(Task task) 79 | { 80 | task.Validate(); 81 | 82 | var selectedTask = Tasks.FirstOrDefault(i => i.Id == task.Id); 83 | 84 | if (selectedTask.IsNull()) throw new ValidationException("PROJTASK-01"); 85 | if (!Users.Any(i => i.Id == task.Assignee.Id) || !Users.Any(i => i.Id == task.Reporter.Id)) 86 | throw new ValidationException("PROJTASK-02"); 87 | 88 | Tasks.Update(task); 89 | } 90 | 91 | public void RemoveTask(Task task) 92 | { 93 | task.Validate(); 94 | 95 | if (!Tasks.Any(i => i.Id == task.Id)) throw new ValidationException("PROJTASK-01"); 96 | 97 | Tasks.Remove(task); 98 | } 99 | 100 | public void SetDescription(string description, string longDescription) 101 | { 102 | this.Description = description; 103 | this.LongDescription = longDescription; 104 | 105 | this.Validate(); 106 | } 107 | 108 | public void Validate() 109 | { 110 | var validator = new ProjectValidator(); 111 | 112 | var validationResult = validator.Validate(this); 113 | 114 | if (!validationResult.IsValid) throw new ValidationException(string.Join(";", validationResult.Errors.Select(i => i.ErrorCode))); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Graph.Application/Graph/Task/Mutation/TaskMutation.cs: -------------------------------------------------------------------------------- 1 | using Graph.Application.Commands.Project; 2 | using Graph.Application.Commands.Task; 3 | using Graph.Application.Graph.Common; 4 | using Graph.Application.Graph.Task.Types.Input; 5 | using Graph.Application.Graph.Types.Input; 6 | using GraphQL.Types; 7 | using MediatR; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Text; 12 | 13 | namespace Graph.Application.Graph.Mutation 14 | { 15 | public class TaskMutation : ObjectGraphType 16 | { 17 | public TaskMutation(IServiceProvider serviceProvider) 18 | { 19 | Field( 20 | "addTask", 21 | arguments: new QueryArguments( 22 | new QueryArgument> { Name = "data" } 23 | ), 24 | resolve: context => 25 | { 26 | var command = context.GetArgument("data"); 27 | 28 | using (var scope = serviceProvider.CreateScope()) 29 | { 30 | var mediator = scope.ServiceProvider.GetRequiredService(); 31 | return mediator.Send(command).Result; 32 | } 33 | }); 34 | 35 | Field( 36 | "updateTaskInfo", 37 | arguments: new QueryArguments( 38 | new QueryArgument> { Name = "data" } 39 | ), 40 | resolve: context => 41 | { 42 | var command = context.GetArgument("data"); 43 | 44 | using (var scope = serviceProvider.CreateScope()) 45 | { 46 | var mediator = scope.ServiceProvider.GetRequiredService(); 47 | return mediator.Send(command).Result; 48 | } 49 | }); 50 | 51 | Field( 52 | "changeAssignee", 53 | arguments: new QueryArguments( 54 | new QueryArgument> { Name = "data" } 55 | ), 56 | resolve: context => 57 | { 58 | var command = context.GetArgument("data"); 59 | 60 | using (var scope = serviceProvider.CreateScope()) 61 | { 62 | var mediator = scope.ServiceProvider.GetRequiredService(); 63 | return mediator.Send(command).Result; 64 | } 65 | }); 66 | 67 | Field( 68 | "updateDeadline", 69 | arguments: new QueryArguments( 70 | new QueryArgument> { Name = "data" } 71 | ), 72 | resolve: context => 73 | { 74 | var command = context.GetArgument("data"); 75 | 76 | using (var scope = serviceProvider.CreateScope()) 77 | { 78 | var mediator = scope.ServiceProvider.GetRequiredService(); 79 | return mediator.Send(command).Result; 80 | } 81 | }); 82 | 83 | Field( 84 | "updateTaskStatus", 85 | arguments: new QueryArguments( 86 | new QueryArgument> { Name = "data" } 87 | ), 88 | resolve: context => 89 | { 90 | var command = context.GetArgument("data"); 91 | 92 | using (var scope = serviceProvider.CreateScope()) 93 | { 94 | var mediator = scope.ServiceProvider.GetRequiredService(); 95 | return mediator.Send(command).Result; 96 | } 97 | }); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /test/Graph.Tests/Integration/Mutation/TaskMutationTest.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Graph.Application.Graph.Mutation; 3 | using GraphQL; 4 | using GraphQL.Types; 5 | using Moq; 6 | using System; 7 | using Xunit; 8 | 9 | namespace Graph.Tests 10 | { 11 | public class TaskMutationTest 12 | { 13 | private readonly IDocumentExecuter _Executer; 14 | private readonly ISchema _Schema; 15 | 16 | public TaskMutationTest(IDocumentExecuter executor, TaskMutation mutation) 17 | { 18 | _Executer = executor; 19 | 20 | var schemaMock = new Mock().Object; 21 | schemaMock.Mutation = mutation; 22 | _Schema = schemaMock; 23 | } 24 | 25 | [Fact(DisplayName = "Add a valid task")] 26 | [Trait("Integration", "Task")] 27 | public async void DocumentExecuter_TaskMutation_AddTask_ValidTask() 28 | { 29 | var dataJson = "{ assigneeId: \"" + MockHelper.Guids[2] + "\", " + 30 | "reporterId: \"" + MockHelper.Guids[1] + "\", " + 31 | "projectId : \"" + MockHelper.Guids[4] + "\"," + 32 | "description: \"Task\"," + 33 | "deadLine: \""+ DateTime.Now.AddDays(5).Date.ToString("yyyy-MM-dd") +"\", " + 34 | "longDescription: \"Fix bug #42343\" }"; 35 | 36 | var request = "mutation { addTask(data: " + dataJson + " ) { result } }"; 37 | 38 | var result = await _Executer.ExecuteAsync(_ => 39 | { 40 | _.Schema = _Schema; 41 | _.Query = request; 42 | }); 43 | 44 | result.Errors.Should().BeNullOrEmpty(); 45 | } 46 | 47 | [Fact(DisplayName = "Edit a valid task")] 48 | [Trait("Integration", "Task")] 49 | public async void DocumentExecuter_TaskMutation_EditTask_ValidTask() 50 | { 51 | var dataJson = "{ id: \"" + MockHelper.Guids[6] + "\", description: \"Task\", longDescription: \"Fix bug #432163\" }"; 52 | 53 | var request = "mutation { updateTaskInfo(data: " + dataJson + " ) { result } }"; 54 | 55 | var result = await _Executer.ExecuteAsync(_ => 56 | { 57 | _.Schema = _Schema; 58 | _.Query = request; 59 | }); 60 | 61 | result.Errors.Should().BeNullOrEmpty(); 62 | } 63 | 64 | [Fact(DisplayName = "Change assignee of a valid task")] 65 | [Trait("Integration", "Task")] 66 | public async void DocumentExecuter_TaskMutation_ChangeAssignee_ValidTask() 67 | { 68 | var dataJson = "{ id: \"" + MockHelper.Guids[6] + "\", newAssigneeId: \"" + MockHelper.Guids[2] + "\" }"; 69 | 70 | var request = "mutation { changeAssignee(data: " + dataJson + " ) { result } }"; 71 | 72 | var result = await _Executer.ExecuteAsync(_ => 73 | { 74 | _.Schema = _Schema; 75 | _.Query = request; 76 | }); 77 | 78 | result.Errors.Should().BeNullOrEmpty(); 79 | } 80 | 81 | [Fact(DisplayName = "Update status of a valid task")] 82 | [Trait("Integration", "Task")] 83 | public async void DocumentExecuter_TaskMutation_UpdateStatus_ValidTask() 84 | { 85 | var dataJson = "{ id: \"" + MockHelper.Guids[6] + "\", status: DONE }"; 86 | 87 | var request = "mutation { updateTaskStatus(data: " + dataJson + " ) { result } }"; 88 | 89 | var result = await _Executer.ExecuteAsync(_ => 90 | { 91 | _.Schema = _Schema; 92 | _.Query = request; 93 | }); 94 | 95 | result.Errors.Should().BeNullOrEmpty(); 96 | } 97 | 98 | [Fact(DisplayName = "Update deadline of a valid task")] 99 | [Trait("Integration", "Task")] 100 | public async void DocumentExecuter_TaskMutation_UpdateDeadline_ValidTask() 101 | { 102 | var dataJson = "{ id: \"" + MockHelper.Guids[6] + "\", deadline: \"" + DateTime.Now.AddDays(5).Date.ToString("yyyy-MM-dd") + "\"}"; 103 | 104 | var request = "mutation { updateDeadline(data: " + dataJson + " ) { result } }"; 105 | 106 | var result = await _Executer.ExecuteAsync(_ => 107 | { 108 | _.Schema = _Schema; 109 | _.Query = request; 110 | }); 111 | 112 | result.Errors.Should().BeNullOrEmpty(); 113 | } 114 | 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /GraphAPI.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29411.108 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Graph.API", "src\Graph.API\Graph.API.csproj", "{715FC768-FF1C-4CA9-8774-AA331A73C1F5}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Graph.Application", "src\Graph.Application\Graph.Application.csproj", "{CF070433-F00D-4F06-A562-1943CB9E274B}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Graph.Infrastructure", "src\Graph.Infrastructure\Graph.Infrastructure.csproj", "{F84F5300-38EE-4579-BD36-3BB653510C3A}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Graph.CrossCutting", "src\Graph.CrossCutting\Graph.CrossCutting.csproj", "{8869DA2C-5DA5-447E-ADF0-5B2B556A35CD}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Graph.Domain", "src\Graph.Domain\Graph.Domain.csproj", "{D55CC4ED-BB1B-41EB-A82D-1BC03195AFDF}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Graph.CrossCutting.Ioc", "src\Graph.CrossCutting.Ioc\Graph.CrossCutting.Ioc.csproj", "{4C81FD0E-DEDD-48DF-A10F-849952568FFD}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Graph.Domain.Service", "src\Graph.Domain.Service\Graph.Domain.Service.csproj", "{72333B90-4ACA-4F4E-B500-ACF8AA7990E7}" 19 | EndProject 20 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Graph.Tests", "test\Graph.Tests\Graph.Tests.csproj", "{2B49722C-450D-4D6A-9699-D4F2E454EC3A}" 21 | EndProject 22 | Global 23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 24 | Debug|Any CPU = Debug|Any CPU 25 | Release|Any CPU = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {715FC768-FF1C-4CA9-8774-AA331A73C1F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {715FC768-FF1C-4CA9-8774-AA331A73C1F5}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {715FC768-FF1C-4CA9-8774-AA331A73C1F5}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {715FC768-FF1C-4CA9-8774-AA331A73C1F5}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {CF070433-F00D-4F06-A562-1943CB9E274B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {CF070433-F00D-4F06-A562-1943CB9E274B}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {CF070433-F00D-4F06-A562-1943CB9E274B}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {CF070433-F00D-4F06-A562-1943CB9E274B}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {F84F5300-38EE-4579-BD36-3BB653510C3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {F84F5300-38EE-4579-BD36-3BB653510C3A}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {F84F5300-38EE-4579-BD36-3BB653510C3A}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {F84F5300-38EE-4579-BD36-3BB653510C3A}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {8869DA2C-5DA5-447E-ADF0-5B2B556A35CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {8869DA2C-5DA5-447E-ADF0-5B2B556A35CD}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {8869DA2C-5DA5-447E-ADF0-5B2B556A35CD}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {8869DA2C-5DA5-447E-ADF0-5B2B556A35CD}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {D55CC4ED-BB1B-41EB-A82D-1BC03195AFDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {D55CC4ED-BB1B-41EB-A82D-1BC03195AFDF}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {D55CC4ED-BB1B-41EB-A82D-1BC03195AFDF}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {D55CC4ED-BB1B-41EB-A82D-1BC03195AFDF}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {4C81FD0E-DEDD-48DF-A10F-849952568FFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {4C81FD0E-DEDD-48DF-A10F-849952568FFD}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {4C81FD0E-DEDD-48DF-A10F-849952568FFD}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {4C81FD0E-DEDD-48DF-A10F-849952568FFD}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {72333B90-4ACA-4F4E-B500-ACF8AA7990E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {72333B90-4ACA-4F4E-B500-ACF8AA7990E7}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {72333B90-4ACA-4F4E-B500-ACF8AA7990E7}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {72333B90-4ACA-4F4E-B500-ACF8AA7990E7}.Release|Any CPU.Build.0 = Release|Any CPU 56 | {2B49722C-450D-4D6A-9699-D4F2E454EC3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 57 | {2B49722C-450D-4D6A-9699-D4F2E454EC3A}.Debug|Any CPU.Build.0 = Debug|Any CPU 58 | {2B49722C-450D-4D6A-9699-D4F2E454EC3A}.Release|Any CPU.ActiveCfg = Release|Any CPU 59 | {2B49722C-450D-4D6A-9699-D4F2E454EC3A}.Release|Any CPU.Build.0 = Release|Any CPU 60 | EndGlobalSection 61 | GlobalSection(SolutionProperties) = preSolution 62 | HideSolutionNode = FALSE 63 | EndGlobalSection 64 | GlobalSection(ExtensibilityGlobals) = postSolution 65 | SolutionGuid = {2E1E3BE2-A205-4002-8840-10027B6BDA38} 66 | EndGlobalSection 67 | EndGlobal 68 | -------------------------------------------------------------------------------- /src/Graph.Application/MessageHandler/BusMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using Graph.Infrastructure.Database.Query.Manager; 2 | using Graph.Infrastructure.Database.Query.ProjectSchema; 3 | using Graph.Infrastructure.Database.Query.UserSchema; 4 | using TaskSchema = Graph.Infrastructure.Database.Query.TaskSchema; 5 | using Graph.Infrastructure.ServiceBus; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Threading.Tasks; 10 | using System.Linq; 11 | using System.Reflection; 12 | using Graph.CrossCutting.Extensions; 13 | 14 | namespace Graph.Application.MessageHandler 15 | { 16 | public class BusMessageHandler : ISubscribe 17 | { 18 | private readonly IServiceProvider _ServiceProvider; 19 | 20 | public BusMessageHandler(IServiceProvider serviceProvider) 21 | { 22 | _ServiceProvider = serviceProvider; 23 | } 24 | 25 | public async Task HandleMessage(Message message) 26 | { 27 | using(var scope = _ServiceProvider.CreateScope()) 28 | { 29 | var command = this.GetType().GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).FirstOrDefault(i => i.Name.ToLower() == message.MessageType.ToLower()); 30 | 31 | if (!command.IsNull()) 32 | { 33 | await (Task)command.Invoke(this, new object[] { message, scope }); 34 | } 35 | else 36 | { 37 | throw new MethodAccessException(); 38 | } 39 | } 40 | } 41 | 42 | #region User 43 | 44 | private async Task AddUser(Message message, IServiceScope scope) 45 | { 46 | var user = Newtonsoft.Json.JsonConvert.DeserializeObject(message.MessageData); 47 | user.Projects = new List(); 48 | 49 | var manager = scope.ServiceProvider.GetRequiredService>(); 50 | 51 | await manager.Index(user); 52 | } 53 | 54 | private async Task UpdateUser(Message message, IServiceScope scope) 55 | { 56 | var user = Newtonsoft.Json.JsonConvert.DeserializeObject(message.MessageData); 57 | 58 | var manager = scope.ServiceProvider.GetRequiredService>(); 59 | 60 | await manager.Remove(Guid.Parse(user.Id)); 61 | await manager.Index(user); 62 | } 63 | 64 | #endregion 65 | 66 | #region Project 67 | 68 | private async Task AddProject(Message message, IServiceScope scope) 69 | { 70 | var project = Newtonsoft.Json.JsonConvert.DeserializeObject(message.MessageData); 71 | 72 | var manager = scope.ServiceProvider.GetRequiredService>(); 73 | 74 | await manager.Index(project); 75 | } 76 | private async Task UpdateUserProject(Message message, IServiceScope scope) 77 | { 78 | var messageData = Newtonsoft.Json.JsonConvert.DeserializeObject(message.MessageData); 79 | 80 | var projectManager = scope.ServiceProvider.GetRequiredService>(); 81 | var userManager = scope.ServiceProvider.GetRequiredService>(); 82 | 83 | await projectManager.Remove(Guid.Parse(messageData.Project.Id)); 84 | await projectManager.Index(messageData.Project); 85 | 86 | await userManager.Remove(Guid.Parse(messageData.User.Id)); 87 | await userManager.Index(messageData.User); 88 | } 89 | 90 | private async Task AddUpdateTaskProject(Message message, IServiceScope scope) 91 | { 92 | var messageData = Newtonsoft.Json.JsonConvert.DeserializeObject(message.MessageData); 93 | 94 | var projectManager = scope.ServiceProvider.GetRequiredService>(); 95 | var taskManager = scope.ServiceProvider.GetRequiredService>(); 96 | 97 | await projectManager.Remove(Guid.Parse(messageData.Project.Id)); 98 | await projectManager.Index(messageData.Project); 99 | 100 | await taskManager.Remove(Guid.Parse(messageData.Task.Id)); 101 | await taskManager.Index(messageData.Task); 102 | } 103 | 104 | #endregion 105 | 106 | #region Task 107 | 108 | private async Task UpdateTask(Message message, IServiceScope scope) 109 | { 110 | var messageData = Newtonsoft.Json.JsonConvert.DeserializeObject(message.MessageData); 111 | 112 | var taskManager = scope.ServiceProvider.GetRequiredService>(); 113 | 114 | await taskManager.Remove(Guid.Parse(messageData.Id)); 115 | await taskManager.Index(messageData); 116 | } 117 | 118 | #endregion 119 | 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Query/InMemoryManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Graph.CrossCutting.Extensions.GraphQL; 5 | using Graph.CrossCutting.Interfaces; 6 | using System.Linq; 7 | using System.Linq.Dynamic.Core; 8 | using Graph.CrossCutting.Extensions; 9 | using Newtonsoft.Json; 10 | using System.Text; 11 | 12 | namespace Graph.Infrastructure.Database.Query 13 | { 14 | public class InMemoryManager : IManager where T : class, IQueryModel 15 | { 16 | private ICollection _Data; 17 | 18 | public InMemoryManager() 19 | { 20 | _Data = new HashSet(); 21 | } 22 | 23 | public InMemoryManager(ICollection data) 24 | { 25 | _Data = data; 26 | } 27 | 28 | public Task> Get(string[] fields, IDictionary filters, string order, int skip, int take) 29 | { 30 | var query = _Data.AsQueryable() 31 | .Where(GetFilter(filters)) 32 | .OrderBy(order ?? "id asc") 33 | .Skip(skip) 34 | .Take(take) 35 | .Select(GetProjection(fields)); 36 | 37 | var json = JsonConvert.SerializeObject(query); 38 | var result = JsonConvert.DeserializeObject>(json); 39 | 40 | return Task.FromResult(result); 41 | } 42 | 43 | public Task GetById(Guid id, string[] fields) 44 | { 45 | var query = _Data.AsQueryable() 46 | .Where(i => i.Id == id.ToString()) 47 | .Skip(0).Take(1) 48 | .Select(GetProjection(fields)) 49 | .FirstOrDefault(); 50 | 51 | var json = JsonConvert.SerializeObject(query); 52 | var result = JsonConvert.DeserializeObject(json); 53 | 54 | return Task.FromResult(result); 55 | } 56 | 57 | public Task Index(T entry) 58 | { 59 | _Data.Add(entry); 60 | 61 | return Task.FromResult(true); 62 | } 63 | 64 | public Task Remove(Guid entryId) 65 | { 66 | var original = _Data.FirstOrDefault(i => i.Id == entryId.ToString()); 67 | _Data.Remove(original); 68 | 69 | return Task.FromResult(true); 70 | } 71 | 72 | private string GetProjection(string[] fields) 73 | { 74 | var projection = new List(); 75 | var grouppedFields = fields.GroupBy(i => i.Split('.')[0]) 76 | .ToDictionary( 77 | k => k.Key, 78 | v => v.Select(i => i.Split('.').ElementAtOrDefault(1)) 79 | ); 80 | 81 | foreach (var grouppedField in grouppedFields) 82 | { 83 | if (grouppedField.Value.ListIsNullOrEmpty()) projection.Add(grouppedField.Key); 84 | else 85 | { 86 | var innerFields = string.Join(",", grouppedField.Value); 87 | 88 | if (grouppedField.Key.EndsWith("s")) 89 | projection.Add(string.Format("{0}.Select(new ({1})) as {0}", grouppedField.Key, innerFields)); 90 | else 91 | projection.Add(string.Format("new ({0}) as {0}", innerFields)); 92 | } 93 | } 94 | 95 | var inerProjection = string.Join(",", projection); 96 | 97 | return string.Format("new({0})", inerProjection); 98 | } 99 | 100 | private string GetFilter(IDictionary filters) 101 | { 102 | if (filters.ListIsNullOrEmpty()) return "1 == 1"; 103 | 104 | var filterDefinition = new List(); 105 | 106 | foreach (var filter in filters) 107 | { 108 | filterDefinition.Add(GetFilterType(filter.Key, filter.Value)); 109 | } 110 | 111 | return string.Join(" && ", filterDefinition); 112 | } 113 | 114 | private string GetFilterType(string field, GraphFilter filter) 115 | { 116 | switch (filter.Operation) 117 | { 118 | case "e": 119 | return string.Format("{0} == \"{1}\"", field, filter.Value); 120 | case "c": 121 | return string.Format("{0}.Contains(\"{1}\")", field, filter.Value); 122 | case "g": 123 | return string.Format("{0} < {1}", field, filter.Value); 124 | case "ge": 125 | return string.Format("{0} <= {1}", field, filter.Value); 126 | case "l": 127 | return string.Format("{0} > {1}", field, filter.Value); 128 | case "le": 129 | return string.Format("{0} >= {1}", field, filter.Value); 130 | case "ne": 131 | return string.Format("{0} != \"{1}\"", field, filter.Value); 132 | default: 133 | return string.Empty; 134 | } 135 | } 136 | 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /test/Graph.Tests/CommandHandler/UserCommandHandlerTest.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Graph.Domain.Service.CommandHandler; 3 | using Graph.Domain.Service.Mappings; 4 | using Graph.Infrastructure.Database.Command; 5 | using Microsoft.EntityFrameworkCore; 6 | using System; 7 | using System.Linq; 8 | using Graph.Infrastructure.Database; 9 | using Microsoft.Extensions.Options; 10 | using Moq; 11 | using Graph.Infrastructure.Database.Command.Interfaces; 12 | using Graph.Infrastructure.ServiceBus; 13 | using Graph.Infrastructure.Database.Command.Repository; 14 | using Xunit; 15 | using Graph.Application.Commands.User; 16 | using MediatR; 17 | using Graph.Infrastructure.Database.Command.Model; 18 | using FluentAssertions; 19 | using System.Collections.Generic; 20 | using System.Diagnostics.CodeAnalysis; 21 | using Graph.Infrastructure.Database.Repository.Interfaces; 22 | using Graph.CrossCutting.Interfaces; 23 | using Graph.Tests.Comparers; 24 | using Graph.CrossCutting.Exceptions; 25 | 26 | namespace Graph.Tests 27 | { 28 | public class UserCommandHandlerTest 29 | { 30 | private readonly Guid GatesId; 31 | private readonly Guid MarkId; 32 | private readonly Guid WindowsProjectId; 33 | private readonly Guid FacebookId; 34 | private readonly Guid FacebookEmployeeId; 35 | 36 | public UserCommandHandlerTest() 37 | { 38 | GatesId = Guid.NewGuid(); 39 | MarkId = Guid.NewGuid(); 40 | WindowsProjectId = Guid.NewGuid(); 41 | FacebookId = Guid.NewGuid(); 42 | FacebookEmployeeId = Guid.NewGuid(); 43 | } 44 | 45 | [Theory(DisplayName = "Add valid users")] 46 | [Trait("Command", "User")] 47 | [InlineData("Steve Jobs", "steve@jobs.com")] 48 | [InlineData("Elon Musk", "elon@musk.com")] 49 | [InlineData("Warren Buffett", "warren@buffett.com")] 50 | public void UserCommandHandler_Handle_AddUserCommand_AddValidUsers(string name, string email) 51 | { 52 | IRequestHandler handler = GetCommandHandlerInstance(); 53 | var commandData = new AddUserCommand() { Email = email, Name = name }; 54 | 55 | var handlerCall = handler.Handle(commandData, default); 56 | 57 | handlerCall.Result.Should().BeTrue(); 58 | } 59 | 60 | [Theory(DisplayName = "Add invalid users")] 61 | [Trait("Command", "User")] 62 | [InlineData("Steve Jobs", "steve_jobs.com")] 63 | [InlineData("Elon Musk", "elon@musk")] 64 | [InlineData("", "warren@buffett.com")] 65 | [InlineData("", null)] 66 | [InlineData(null, null)] 67 | [InlineData(null, "")] 68 | public void UserCommandHandler_Handle_AddUserCommand_AddInvalidUsers(string name, string email) 69 | { 70 | IRequestHandler handler = GetCommandHandlerInstance(); 71 | var commandData = new AddUserCommand() { Email = email, Name = name }; 72 | 73 | Action handle = () => handler.Handle(commandData, default).Wait(); 74 | 75 | handle.Should().Throw(); 76 | } 77 | 78 | [Fact(DisplayName = "Add an existing User")] 79 | [Trait("Command", "User")] 80 | public void UserCommandHandler_Handle_AddUserCommand_AddExistingUser() 81 | { 82 | IRequestHandler handler = GetCommandHandlerInstance(); 83 | var commandData = new AddUserCommand() { Email = "bill@gates.com", Name = "Bill Gates" }; 84 | 85 | Action action = () => handler.Handle(commandData, default).Wait(); 86 | 87 | action.Should().Throw(); 88 | } 89 | 90 | [Fact(DisplayName = "Edit an existing User, with a valid email")] 91 | [Trait("Command", "User")] 92 | public void UserCommandHandler_Handle_UpdateUserInfoCommand_EditExistingUser_ValidEmail() 93 | { 94 | IRequestHandler handler = GetCommandHandlerInstance(); 95 | var commandData = new UpdateUserInfoCommand() { Id = GatesId, Email = "gates@microsoft.com", Name = "Bill Gates" }; 96 | 97 | var result = handler.Handle(commandData, default).Result; 98 | 99 | result.Should().BeTrue(); 100 | } 101 | 102 | [Fact(DisplayName = "Edit an existing User, with an invalid email")] 103 | [Trait("Command", "User")] 104 | public void UserCommandHandler_Handle_UpdateUserInfoCommand_EditExistingUser_InvalidEmail() 105 | { 106 | IRequestHandler handler = GetCommandHandlerInstance(); 107 | var commandData = new UpdateUserInfoCommand() { Id = MarkId, Email = "bill@gates.com", Name = "Mark Zuckerberg" }; 108 | 109 | Action action = () => handler.Handle(commandData, default).Wait(); 110 | 111 | action.Should().Throw(); 112 | } 113 | 114 | 115 | 116 | private UserCommandHandler GetCommandHandlerInstance() 117 | { 118 | var mapper = MockHelper.GetMapper(); 119 | var serviceBus = MockHelper.GetServiceBus(); 120 | var unitOfWork = MockHelper.GetUnitOfWork(); 121 | var userRepository = MockHelper.GetUserRepository(new UserComparer(), new[] { GatesId, MarkId, FacebookEmployeeId, WindowsProjectId, FacebookId }); 122 | 123 | return new UserCommandHandler(unitOfWork, serviceBus, userRepository, mapper); 124 | } 125 | 126 | } 127 | 128 | 129 | 130 | 131 | } 132 | 133 | 134 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Query/MongoManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | using System.Threading.Tasks; 5 | using Graph.CrossCutting.Extensions.GraphQL; 6 | using Microsoft.Extensions.Options; 7 | using MongoDB.Driver; 8 | using Graph.CrossCutting.Exceptions; 9 | using MongoDB.Bson; 10 | using System.Linq; 11 | using Graph.CrossCutting.Interfaces; 12 | 13 | namespace Graph.Infrastructure.Database.Query 14 | { 15 | public class MongoManager : IManager where T : class, IQueryModel 16 | { 17 | protected readonly IMongoCollection _Collection; 18 | protected const string MONGO_ID = "_id"; 19 | protected const string ENTITY_ID = "id"; 20 | protected const string DATABASE = "graph"; 21 | 22 | public MongoManager(IOptions options) 23 | { 24 | var client = new MongoClient(options.Value.ReadDatabase); 25 | var database = client.GetDatabase(DATABASE); 26 | _Collection = database.GetCollection(typeof(T).Name.ToLower()); 27 | } 28 | 29 | public virtual async Task> Get(string[] fields, IDictionary filters, string order, int skip = 0, int take = 10) 30 | { 31 | var filter = GetFilter(filters); 32 | var projection = GetProjection(fields); 33 | var sort = GetSort(order); 34 | 35 | var finder = await _Collection.Find(filter) 36 | .Sort(sort) 37 | .Skip(skip) 38 | .Limit(take) 39 | .Project(projection) 40 | .ToListAsync(); 41 | 42 | var json = ReplaceIdentification(finder.ToJson()); 43 | 44 | return JsonConvert.DeserializeObject>(json); 45 | } 46 | 47 | 48 | 49 | public virtual async Task GetById(Guid id, string[] fields) 50 | { 51 | var filter = Builders.Filter.Eq(MONGO_ID, id.ToString()); 52 | var projection = GetProjection(fields); 53 | 54 | try 55 | { 56 | var finder = await _Collection.Find(filter).Project(projection).FirstAsync(); 57 | 58 | var json = ReplaceIdentification(finder.ToJson()); 59 | 60 | return JsonConvert.DeserializeObject(json); 61 | } 62 | catch 63 | { 64 | return default; 65 | } 66 | 67 | } 68 | 69 | public virtual async Task Index(T entry) 70 | { 71 | await _Collection.InsertOneAsync(entry); 72 | 73 | return true; 74 | } 75 | 76 | public virtual async Task Remove(Guid entryId) 77 | { 78 | var filter = Builders.Filter.Eq(MONGO_ID, entryId.ToString()); 79 | 80 | await _Collection.DeleteOneAsync(filter); 81 | 82 | return true; 83 | } 84 | 85 | private ProjectionDefinition GetProjection(string[] fields) 86 | { 87 | var projection = Builders.Projection.Include(MONGO_ID); 88 | 89 | fields = fields.Select(i => i.EndsWith(ENTITY_ID) ? ReplaceIdentification(i, true) : i).ToArray(); 90 | 91 | foreach (var field in fields) 92 | { 93 | projection = projection.Include(field); 94 | } 95 | 96 | return projection; 97 | } 98 | 99 | private FilterDefinition GetFilter(IDictionary filters) 100 | { 101 | var builder = Builders.Filter; 102 | var filterDefinition = builder.Empty; 103 | 104 | foreach(var filter in filters) 105 | { 106 | filterDefinition &= GetFilterType(builder, filter.Key, filter.Value); 107 | } 108 | 109 | return filterDefinition; 110 | } 111 | 112 | private FilterDefinition GetFilterType(FilterDefinitionBuilder builder ,string field, GraphFilter filter) 113 | { 114 | if (field.Equals(ENTITY_ID)) ReplaceIdentification(field, true); 115 | 116 | switch(filter.Operation) 117 | { 118 | case "e": 119 | return builder.Eq(field, filter.Value); 120 | case "c": 121 | return builder.Regex(field, new BsonRegularExpression($".*{filter.Value}.*","i")); 122 | case "g": 123 | return builder.Gt(field, filter.Value); 124 | case "ge": 125 | return builder.Gte(field, filter.Value); 126 | case "l": 127 | return builder.Lt(field, filter.Value); 128 | case "le": 129 | return builder.Lte(field, filter.Value); 130 | case "ne": 131 | return builder.Ne(field, filter.Value); 132 | default: 133 | return builder.Empty; 134 | } 135 | } 136 | 137 | private SortDefinition GetSort(string order) 138 | { 139 | if(string.IsNullOrEmpty(order)) return Builders.Sort.Ascending(MONGO_ID); 140 | 141 | var sortTypes = new[] { "asc", "desc" }; 142 | var pairFieldSortType = order.Split(' '); 143 | 144 | if (pairFieldSortType.Count() != 2) throw new QueryArgumentException("QA-01"); 145 | 146 | var field = this.ReplaceIdentification(pairFieldSortType.ElementAtOrDefault(0).ToLower(), true); 147 | var sortType = pairFieldSortType.ElementAtOrDefault(1).ToLower(); 148 | 149 | if (typeof(T).GetField(field) != null) throw new QueryArgumentException("QA-02"); 150 | if (!sortTypes.Contains(sortType)) throw new QueryArgumentException("QA-03"); 151 | 152 | if (sortType == "asc") return Builders.Sort.Ascending(field); 153 | else return Builders.Sort.Descending(field); 154 | } 155 | 156 | protected string ReplaceIdentification(string text, bool reverse = false) 157 | { 158 | if (reverse) return text.Replace(ENTITY_ID, MONGO_ID); 159 | else return text.Replace(MONGO_ID, ENTITY_ID); 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Database/Query/ElasticSearchManager.cs: -------------------------------------------------------------------------------- 1 | using Graph.CrossCutting.Extensions.GraphQL; 2 | using Graph.CrossCutting.Interfaces; 3 | using Microsoft.Extensions.Options; 4 | using Nest; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | 10 | namespace Graph.Infrastructure.Database.Query 11 | { 12 | public class ElasticSearchManager : IManager where T : class, IQueryModel 13 | { 14 | protected readonly IElasticClient _Client; 15 | protected readonly string _Index; 16 | public ElasticSearchManager(IOptions options) 17 | { 18 | _Index = typeof(T).Name.ToLower(); 19 | var node = new Uri(options.Value.ReadDatabase); 20 | var settings = new ConnectionSettings(node); 21 | _Client = new ElasticClient(settings); 22 | } 23 | 24 | public async Task> Get(string[] fields, 25 | IDictionary filters, 26 | string order, 27 | int skip, 28 | int take) 29 | { 30 | var request = new SearchRequest(_Index) 31 | { 32 | From = skip, 33 | Size = take, 34 | Sort = GetSort(order), 35 | Query = GetQueryContainer(filters), 36 | Source = GetProjection(fields) 37 | }; 38 | 39 | var response = await _Client.SearchAsync(request); 40 | 41 | return response.Documents.AsEnumerable(); 42 | } 43 | 44 | public async Task GetById(Guid id, string[] fields) 45 | { 46 | var request = new SearchRequest(_Index) 47 | { 48 | Query = new TermQuery() { Field = "_id", Value = id.ToString(), IsStrict = true }, 49 | Source = GetProjection(fields) 50 | }; 51 | 52 | var response = await _Client.SearchAsync(request); 53 | 54 | return response.Documents.FirstOrDefault(); 55 | } 56 | 57 | public async Task Index(T entry) 58 | { 59 | await _Client.IndexAsync(entry, idx => idx.Index(_Index)); 60 | 61 | return true; 62 | } 63 | 64 | public async Task Remove(Guid entryId) 65 | { 66 | var request = new DeleteByQueryRequest(_Index) 67 | { 68 | Query = new TermQuery() { Field = "_id", Value = entryId.ToString(), IsStrict = true } 69 | }; 70 | 71 | var response = await _Client.DeleteByQueryAsync(request); 72 | 73 | return response.Took > 0; 74 | } 75 | 76 | private Union GetProjection(string[] fields) 77 | { 78 | var projection = new SourceFilterDescriptor(); 79 | 80 | projection.Includes(i => i.Fields(fields)); 81 | 82 | return projection; 83 | } 84 | 85 | private IList GetSort(string sort) 86 | { 87 | var list = new List(); 88 | if (string.IsNullOrEmpty(sort)) return null; 89 | 90 | var sortArray = sort.Split(' '); 91 | 92 | var sortField = new FieldSort() 93 | { 94 | Field = $"{sortArray[0]}.keyword", 95 | Order = (sortArray[1] == "asc") ? SortOrder.Ascending : SortOrder.Descending, 96 | UnmappedType = FieldType.Text 97 | }; 98 | 99 | list.Add(sortField); 100 | 101 | return list; 102 | } 103 | 104 | private QueryContainer GetQueryContainer(IDictionary filters) 105 | { 106 | if (filters.Count == 0) return new MatchAllQuery(); 107 | 108 | var container = new QueryContainer(); 109 | 110 | foreach (var filter in filters) 111 | { 112 | container = container && GetQueryType(filter.Key, filter.Value); 113 | } 114 | 115 | return container; 116 | } 117 | 118 | private QueryContainer GetQueryType(string field, GraphFilter graphFilter) 119 | { 120 | switch (graphFilter.Operation) 121 | { 122 | case "c": 123 | return new WildcardQuery() 124 | { 125 | Field = field, 126 | Value = $"*{graphFilter.Value.ToString().ToLower()}*" 127 | }; 128 | case "e": 129 | return new MatchQuery() 130 | { 131 | Field = $"{field}.keyword", 132 | Query = graphFilter.Value.ToString(), 133 | }; 134 | case "g": 135 | return new NumericRangeQuery() 136 | { 137 | Field = field, 138 | GreaterThan = (double)graphFilter.Value 139 | }; 140 | case "ge": 141 | return new NumericRangeQuery() 142 | { 143 | Field = field, 144 | GreaterThanOrEqualTo = (double)graphFilter.Value 145 | }; 146 | case "l": 147 | return new NumericRangeQuery() 148 | { 149 | Field = field, 150 | LessThan = (double)graphFilter.Value 151 | }; 152 | case "le": 153 | return new NumericRangeQuery() 154 | { 155 | Field = field, 156 | LessThanOrEqualTo = (double)graphFilter.Value 157 | }; 158 | case "ne": 159 | var newFilter = graphFilter; 160 | newFilter.Operation = "e"; 161 | 162 | return new BoolQuery() 163 | { 164 | MustNot = new QueryContainer[] { GetQueryType(field, newFilter) } 165 | }; 166 | default: 167 | throw new ArgumentException(); 168 | } 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/Graph.Domain.Service/CommandHandler/ProjectCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Command = Graph.Infrastructure.Database.Command.Model; 3 | using Query = Graph.Infrastructure.Database.Query.ProjectSchema; 4 | using UserQuery = Graph.Infrastructure.Database.Query.UserSchema; 5 | using TaskQuery = Graph.Infrastructure.Database.Query.TaskSchema; 6 | using Graph.Application.Commands; 7 | using Graph.Infrastructure.Database.Command.Interfaces; 8 | using MediatR; 9 | using Graph.CrossCutting.Extensions; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | using Graph.Infrastructure.ServiceBus; 13 | using Graph.Application.Commands.Project; 14 | using Graph.CrossCutting.Exceptions; 15 | 16 | namespace Graph.Domain.Service.CommandHandler 17 | { 18 | public class ProjectCommandHandler : IRequestHandler, 19 | IRequestHandler, 20 | IRequestHandler, 21 | IRequestHandler 22 | { 23 | private readonly IUnitOfWork _UnitOfWork; 24 | private readonly IServiceBus _Bus; 25 | private readonly IProjectRepository _ProjectRepository; 26 | private readonly IUserRepository _UserRepository; 27 | private readonly IMapper _Mapper; 28 | 29 | public ProjectCommandHandler(IUnitOfWork unitOfWork, 30 | IServiceBus bus, 31 | IProjectRepository projectRepository, 32 | IUserRepository userRepository, 33 | IMapper mapper) 34 | { 35 | _UnitOfWork = unitOfWork; 36 | _Bus = bus; 37 | _ProjectRepository = projectRepository; 38 | _UserRepository = userRepository; 39 | _Mapper = mapper; 40 | } 41 | 42 | public async Task Handle(AddProjectCommand request, CancellationToken cancellationToken) 43 | { 44 | var projectDomain = new Project(request.Description, request.LongDescription); 45 | projectDomain.Validate(); 46 | 47 | #region Persistence 48 | 49 | var project = projectDomain.ToModel(_Mapper); 50 | 51 | await _ProjectRepository.Add(project); 52 | await _UnitOfWork.Commit(); 53 | 54 | #endregion 55 | 56 | #region Bus 57 | 58 | var publishMessage = new Message(); 59 | publishMessage.MessageType = "AddProject"; 60 | publishMessage.SetData(projectDomain.ToQueryModel(_Mapper)); 61 | 62 | await _Bus.SendMessage(publishMessage); 63 | 64 | #endregion 65 | 66 | return true; 67 | } 68 | 69 | public async Task Handle(AddUserProjectCommand request, CancellationToken cancellationToken) 70 | { 71 | var userDomain = _UserRepository.GetById(request.UserId).Result.ToDomain(_Mapper); 72 | var projectDomain = _ProjectRepository.GetById(request.ProjectId).Result.ToDomain(_Mapper); 73 | 74 | projectDomain.Validate(); 75 | projectDomain.AddUser(userDomain); 76 | 77 | #region Persistence 78 | 79 | await _ProjectRepository.AddUser(projectDomain.ToModel(_Mapper)); 80 | await _UnitOfWork.Commit(); 81 | 82 | #endregion 83 | 84 | #region Bus 85 | 86 | var addUserProjectMessage = new Message(); 87 | addUserProjectMessage.MessageType = "UpdateUserProject"; 88 | addUserProjectMessage.SetData(new 89 | { 90 | Project = projectDomain.ToQueryModel(_Mapper), 91 | User = userDomain.ToQueryModel(_Mapper) 92 | }); 93 | 94 | await _Bus.SendMessage(addUserProjectMessage); 95 | 96 | #endregion 97 | 98 | return true; 99 | } 100 | 101 | public async Task Handle(RemoveUserProjectCommand request, CancellationToken cancellationToken) 102 | { 103 | var userDomain = _UserRepository.GetById(request.UserId).Result.ToDomain(_Mapper); 104 | var projectDomain = _ProjectRepository.GetById(request.ProjectId).Result.ToDomain(_Mapper); 105 | 106 | projectDomain.Validate(); 107 | projectDomain.RemoveUser(userDomain); 108 | 109 | #region Persistence 110 | 111 | await _ProjectRepository.RemoveUser(projectDomain.ToModel(_Mapper)); 112 | await _UnitOfWork.Commit(); 113 | 114 | #endregion 115 | 116 | #region Bus 117 | 118 | var addUserProjectMessage = new Message(); 119 | addUserProjectMessage.MessageType = "UpdateUserProject"; 120 | addUserProjectMessage.SetData(new 121 | { 122 | Project = projectDomain.ToQueryModel(_Mapper), 123 | User = userDomain.ToQueryModel(_Mapper) 124 | }); 125 | 126 | await _Bus.SendMessage(addUserProjectMessage); 127 | 128 | #endregion 129 | 130 | return true; 131 | } 132 | 133 | 134 | 135 | public async Task Handle(UpdateProjectInfoCommand request, CancellationToken cancellationToken) 136 | { 137 | var projectDomain = _ProjectRepository.GetById(request.Id).Result.ToDomain(_Mapper); 138 | projectDomain.Validate(); 139 | 140 | projectDomain.SetDescription(request.Description, request.LongDescription); 141 | 142 | #region Persistence 143 | 144 | await _ProjectRepository.Update(projectDomain.ToModel(_Mapper)); 145 | await _UnitOfWork.Commit(); 146 | 147 | #endregion 148 | 149 | #region Bus 150 | 151 | var publishMessage = new Message(); 152 | publishMessage.MessageType = "UpdateProject"; 153 | publishMessage.SetData(projectDomain.ToQueryModel(_Mapper)); 154 | 155 | await _Bus.SendMessage(publishMessage); 156 | 157 | #endregion 158 | 159 | return true; 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/Graph.Infrastructure/Migrations/20191125185945_Initial.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 4 | 5 | namespace Graph.Infrastructure.Migrations 6 | { 7 | public partial class Initial : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.CreateTable( 12 | name: "Projects", 13 | columns: table => new 14 | { 15 | Id = table.Column(nullable: false), 16 | Description = table.Column(nullable: true), 17 | LongDescription = table.Column(nullable: true) 18 | }, 19 | constraints: table => 20 | { 21 | table.PrimaryKey("PK_Projects", x => x.Id); 22 | }); 23 | 24 | migrationBuilder.CreateTable( 25 | name: "Statuses", 26 | columns: table => new 27 | { 28 | Id = table.Column(nullable: false) 29 | .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), 30 | Description = table.Column(nullable: true), 31 | LongDescription = table.Column(nullable: true) 32 | }, 33 | constraints: table => 34 | { 35 | table.PrimaryKey("PK_Statuses", x => x.Id); 36 | }); 37 | 38 | migrationBuilder.CreateTable( 39 | name: "Users", 40 | columns: table => new 41 | { 42 | Id = table.Column(nullable: false), 43 | Name = table.Column(maxLength: 100, nullable: false), 44 | Email = table.Column(maxLength: 100, nullable: false) 45 | }, 46 | constraints: table => 47 | { 48 | table.PrimaryKey("PK_Users", x => x.Id); 49 | }); 50 | 51 | migrationBuilder.CreateTable( 52 | name: "Tasks", 53 | columns: table => new 54 | { 55 | Id = table.Column(nullable: false), 56 | Description = table.Column(nullable: true), 57 | LongDescription = table.Column(nullable: true), 58 | CreatedDate = table.Column(nullable: false), 59 | DeadLine = table.Column(nullable: false), 60 | ProjectId = table.Column(nullable: false), 61 | AssigneeId = table.Column(nullable: false), 62 | ReporterId = table.Column(nullable: false), 63 | StatusId = table.Column(nullable: false) 64 | }, 65 | constraints: table => 66 | { 67 | table.PrimaryKey("PK_Tasks", x => x.Id); 68 | table.ForeignKey( 69 | name: "FK_Tasks_Users_AssigneeId", 70 | column: x => x.AssigneeId, 71 | principalTable: "Users", 72 | principalColumn: "Id", 73 | onDelete: ReferentialAction.Cascade); 74 | table.ForeignKey( 75 | name: "FK_Tasks_Projects_ProjectId", 76 | column: x => x.ProjectId, 77 | principalTable: "Projects", 78 | principalColumn: "Id", 79 | onDelete: ReferentialAction.Cascade); 80 | table.ForeignKey( 81 | name: "FK_Tasks_Users_ReporterId", 82 | column: x => x.ReporterId, 83 | principalTable: "Users", 84 | principalColumn: "Id", 85 | onDelete: ReferentialAction.Cascade); 86 | table.ForeignKey( 87 | name: "FK_Tasks_Statuses_StatusId", 88 | column: x => x.StatusId, 89 | principalTable: "Statuses", 90 | principalColumn: "Id", 91 | onDelete: ReferentialAction.Cascade); 92 | }); 93 | 94 | migrationBuilder.CreateTable( 95 | name: "UserProjects", 96 | columns: table => new 97 | { 98 | Projectid = table.Column(nullable: false), 99 | UserId = table.Column(nullable: false) 100 | }, 101 | constraints: table => 102 | { 103 | table.PrimaryKey("PK_UserProjects", x => new { x.Projectid, x.UserId }); 104 | table.ForeignKey( 105 | name: "FK_UserProjects_Projects_Projectid", 106 | column: x => x.Projectid, 107 | principalTable: "Projects", 108 | principalColumn: "Id", 109 | onDelete: ReferentialAction.Cascade); 110 | table.ForeignKey( 111 | name: "FK_UserProjects_Users_UserId", 112 | column: x => x.UserId, 113 | principalTable: "Users", 114 | principalColumn: "Id", 115 | onDelete: ReferentialAction.Cascade); 116 | }); 117 | 118 | migrationBuilder.InsertData( 119 | table: "Statuses", 120 | columns: new[] { "Id", "Description", "LongDescription" }, 121 | values: new object[,] 122 | { 123 | { 1, "BACKLOG", null }, 124 | { 2, "IN_PROGRESS", null }, 125 | { 3, "TESTING", null }, 126 | { 4, "DONE", null } 127 | }); 128 | 129 | migrationBuilder.CreateIndex( 130 | name: "IX_Tasks_AssigneeId", 131 | table: "Tasks", 132 | column: "AssigneeId"); 133 | 134 | migrationBuilder.CreateIndex( 135 | name: "IX_Tasks_ProjectId", 136 | table: "Tasks", 137 | column: "ProjectId"); 138 | 139 | migrationBuilder.CreateIndex( 140 | name: "IX_Tasks_ReporterId", 141 | table: "Tasks", 142 | column: "ReporterId"); 143 | 144 | migrationBuilder.CreateIndex( 145 | name: "IX_Tasks_StatusId", 146 | table: "Tasks", 147 | column: "StatusId"); 148 | 149 | migrationBuilder.CreateIndex( 150 | name: "IX_UserProjects_UserId", 151 | table: "UserProjects", 152 | column: "UserId"); 153 | 154 | migrationBuilder.CreateIndex( 155 | name: "IX_Users_Email", 156 | table: "Users", 157 | column: "Email", 158 | unique: true); 159 | } 160 | 161 | protected override void Down(MigrationBuilder migrationBuilder) 162 | { 163 | migrationBuilder.DropTable( 164 | name: "Tasks"); 165 | 166 | migrationBuilder.DropTable( 167 | name: "UserProjects"); 168 | 169 | migrationBuilder.DropTable( 170 | name: "Statuses"); 171 | 172 | migrationBuilder.DropTable( 173 | name: "Projects"); 174 | 175 | migrationBuilder.DropTable( 176 | name: "Users"); 177 | } 178 | } 179 | } 180 | --------------------------------------------------------------------------------