├── Sample ├── Sample.Domain │ ├── GlobalUsing.cs │ ├── Repositories │ │ └── IRepository.cs │ ├── Sample.Domain.csproj │ ├── Entities │ │ ├── Event │ │ │ ├── Ticket.cs │ │ │ └── Event.cs │ │ ├── Users │ │ │ ├── EventManager.cs │ │ │ └── ParticipationUser.cs │ │ └── Base │ │ │ ├── Entity.cs │ │ │ └── AggregateRoot.cs │ └── Errors │ │ └── Erros.cs ├── Sample.Application │ ├── GlobalUsings.cs │ ├── EventManagers │ │ └── Commands │ │ │ ├── Responses │ │ │ └── AuthorizedEventManagerResponse.cs │ │ │ ├── LoginEventManagerCommand.cs │ │ │ └── RegisterEventManagerCommand.cs │ ├── ParticipationUsers │ │ └── Commands │ │ │ ├── Responses │ │ │ └── AuthorizedParticipationUserResponse.cs │ │ │ ├── LoginParticipationUserCommand.cs │ │ │ └── RegisterParticipationUserCommand.cs │ ├── Core │ │ ├── Abstractions │ │ │ ├── Data │ │ │ │ └── ISampleDbContext.cs │ │ │ ├── IHttpResolver.cs │ │ │ ├── Security │ │ │ │ ├── IJwtBearerGenerator.cs │ │ │ │ └── IPasswordHasher.cs │ │ │ ├── IEmailService.cs │ │ │ └── IFakeTranslate.cs │ │ └── Shared │ │ │ ├── QueryObjects │ │ │ └── UserFilters.cs │ │ │ └── Handlers │ │ │ └── BaseSecurityHandler.cs │ ├── AssemblyReference.cs │ ├── Tickets │ │ ├── Queries │ │ │ ├── Responses │ │ │ │ └── GetAllTicketResponse.cs │ │ │ ├── QueryObjects │ │ │ │ └── TicktsResponseSelects.cs │ │ │ └── GetAllMyTicketQuery.cs │ │ └── Commands │ │ │ ├── CancelTicketCommand.cs │ │ │ └── BookTicketCommand.cs │ ├── Events │ │ ├── Queries │ │ │ ├── GetAllEventQuery.cs │ │ │ ├── GetAllAvailableEventQuery.cs │ │ │ ├── Responses │ │ │ │ ├── GetAllEventResponse.cs │ │ │ │ └── GetAllAvailableEventResponse.cs │ │ │ ├── QueryObjects │ │ │ │ └── EventResponseSelects.cs │ │ │ └── Handlers │ │ │ │ └── EventQueryHandler.cs │ │ └── Commands │ │ │ ├── AddEventCommand.cs │ │ │ └── UpdateEventCommand.cs │ ├── DependencyInjection.cs │ └── Sample.Application.csproj ├── Sample.SharedKernel │ ├── Enums │ │ └── Any.cs │ ├── Contracts │ │ ├── Requests │ │ │ └── PagingRequest.cs │ │ └── Email │ │ │ └── SendEmailRequest.cs │ ├── LanguageKeys.cs │ ├── ConstValues.cs │ └── Sample.SharedKernel.csproj ├── Sample.Infrastructure │ ├── Security │ │ ├── Policies │ │ │ ├── Operator.cs │ │ │ ├── UserType │ │ │ │ ├── UserTypeRequirement.cs │ │ │ │ ├── HasUserTypesAttribute.cs │ │ │ │ └── UserTypeHandler.cs │ │ │ └── SampleAuthorizationPolicyProvider.cs │ │ ├── Jwt │ │ │ ├── Options │ │ │ │ └── JwtOptions.cs │ │ │ └── JwtBearerGenerator.cs │ │ ├── DependencyInjection │ │ │ └── SecurityServiceCollectionExtensions.cs │ │ └── Password │ │ │ └── PasswordHasher.cs │ ├── AssemblyReference.cs │ ├── HttpResolver │ │ ├── Exceptions │ │ │ └── InvalidHeaderException.cs │ │ └── HttpResolverImp.cs │ ├── Persistence │ │ ├── Repositories │ │ │ └── Repository.cs │ │ ├── EntitiesConfigurations │ │ │ ├── EventManagerConfiguration.cs │ │ │ ├── ParticipationUserConfiguration.cs │ │ │ └── EventConfiguration.cs │ │ ├── DataSeed │ │ │ └── DataSeed.cs │ │ ├── Context │ │ │ └── SampleDbContext.cs │ │ └── Migrations │ │ │ ├── 20240129195639_Initial.cs │ │ │ ├── SampleDbContextModelSnapshot.cs │ │ │ └── 20240129195639_Initial.Designer.cs │ ├── Email │ │ ├── Settings │ │ │ └── MailSettings.cs │ │ └── EmailService.cs │ ├── FakeTranslate │ │ └── FakeTranslateImp.cs │ ├── Sample.Infrastructure.csproj │ └── DependencyInjection.cs └── Sample.API │ ├── wwwroot │ └── 2024-1 │ │ ├── 17c8d8cc9eab4a43bda85dfd0a9535f4.png │ │ ├── 3fee00dbf654497faec9a5da9a27f9b7.png │ │ └── 81298471d4964b9499e54d5cdcbf4994.png │ ├── appsettings.json │ ├── ApiGroupAttribute.cs │ ├── ApiGroupNames.cs │ ├── appsettings.Development.json │ ├── Controllers │ ├── FilesController.cs │ ├── ZTestController.cs │ ├── EventManagersController.cs │ ├── ParticipationUsersController.cs │ ├── TicketsController.cs │ └── EventsController.cs │ ├── Core │ ├── LanguageKey │ │ └── LanguageKeySwaggerFilter.cs │ └── ActionsFilters │ │ └── ForceDefaultLanguage.cs │ ├── Properties │ └── launchSettings.json │ ├── Sample.API.csproj │ └── Program.cs ├── icon.png ├── Neptunee.Kernel ├── Entities │ ├── INeptuneeEntity.cs │ ├── INeptuneeEntity`1.cs │ ├── INeptuneeCreatableEntity.cs │ ├── INeptuneeDeletableEntity.cs │ ├── INeptuneeUpdatableEntity.cs │ ├── INeptuneeAuditableEntity.cs │ ├── INeptuneeAuditableEntity`1.cs │ ├── INeptuneeAggregateRoot.cs │ └── NeptuneeEnumeration.cs ├── DomainEvents │ ├── INeptuneeDomainEvent.cs │ ├── Handler │ │ └── INeptuneeDomainEventHandler.cs │ ├── Dispatcher │ │ ├── INeptuneeDomainEventDispatcher.cs │ │ └── NeptuneeDomainEventDispatcher.cs │ └── DependencyInjection │ │ └── NeptuneeDomainEventsServiceCollectionExtensions.cs ├── Clock │ ├── INeptuneeClock.cs │ ├── NeptuneeClockImp.cs │ └── DependencyInjection │ │ └── NeptuneeClockServiceCollectionExtensions.cs ├── Extensions │ ├── NeptuneeEnumerableExtensions.cs │ ├── NeptuneeFile.cs │ ├── NeptuneeExceptionExtension.cs │ └── NeptuneeQueryableExtensions.cs ├── LICENSE.md └── Neptunee.Kernel.csproj ├── Neptunee.Handlers ├── Requests │ ├── INeptuneeRequest.cs │ └── INeptuneeRequestHandler.cs ├── ServiceFactoryResolver │ └── NeptuneeServiceFactory.cs ├── RequestDispatcher │ ├── INeptuneeRequestDispatcher.cs │ └── NeptuneeRequestDispatcher.cs ├── DependencyInjection │ └── NeptuneeHandlersServiceCollectionExtensions.cs ├── LICENSE.md └── Neptunee.Handlers.csproj ├── Neptunee ├── DependencyInjection │ └── NeptuneeBaseCleanArchitectureCollectionExtensions.cs ├── IO │ └── Files │ │ ├── DependencyInjection │ │ └── NeptuneeFileServiceCollectionExtensions.cs │ │ ├── Options │ │ └── NeptuneeFileOptions.cs │ │ ├── NeptuneeFileService.cs │ │ └── INeptuneeFileService.cs ├── ExceptionFilter │ ├── DependencyInjection │ │ └── NeptuneeExceptionFilterServiceCollectionExtensions.cs │ └── NeptuneeExceptionHandlerFilter.cs ├── AppBuilder │ └── ExceptionHandlerMiddleware │ │ └── NeptuneeExceptionHandlerAppBuilderExtensions.cs ├── ExceptionHandler │ └── NeptuneeExceptionHandler.cs ├── LICENSE.md └── Neptunee.csproj ├── Neptunee.EntityFrameworkCore ├── Repository │ ├── Read │ │ ├── INeptuneeReadRepository.cs │ │ └── NeptuneeReadRepository.cs │ ├── DependencyInjection │ │ └── NeptuneeRepositoryServiceCollectionExtensions.cs │ ├── INeptuneeRepository.cs │ └── NeptuneeRepository.cs ├── Interfaces │ └── INeptuneeDbContext.cs ├── Extensions │ ├── NeptuneeQueryableExtensions.cs │ ├── NeptuneeModelBuilderExtensions.cs │ ├── NeptuneeDbContextExtensions.cs │ └── NeptuneeAppBuilderExtensions.cs ├── Specification │ ├── INeptuneeSpecification.cs │ ├── NeptuneeSpecificationEvaluator.cs │ └── NeptuneeSpecification.cs ├── LICENSE.md ├── Neptunee.EntityFrameworkCore.csproj └── Interceptors │ ├── PublishNeptuneeDomainEventsInterceptor.cs │ └── UpdateAuditableNeptuneeEntitiesInterceptor.cs ├── LICENSE.md ├── README.md ├── Neptunee.sln └── .gitignore /Sample/Sample.Domain/GlobalUsing.cs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Sample/Sample.Application/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HusseinnHM/Neptunee/HEAD/icon.png -------------------------------------------------------------------------------- /Sample/Sample.SharedKernel/Enums/Any.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.SharedKernel.Enums; 2 | 3 | public enum Any 4 | { 5 | 6 | } -------------------------------------------------------------------------------- /Neptunee.Kernel/Entities/INeptuneeEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Neptunee.Entities; 2 | 3 | public interface INeptuneeEntity 4 | { 5 | } 6 | 7 | -------------------------------------------------------------------------------- /Neptunee.Kernel/DomainEvents/INeptuneeDomainEvent.cs: -------------------------------------------------------------------------------- 1 | namespace Neptunee.DomainEvents; 2 | 3 | public interface INeptuneeDomainEvent 4 | { 5 | } -------------------------------------------------------------------------------- /Sample/Sample.Infrastructure/Security/Policies/Operator.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.Infrastructure.Security.Policies; 2 | 3 | public enum Operator 4 | { 5 | And = 1, 6 | Or = 2 7 | } -------------------------------------------------------------------------------- /Neptunee.Kernel/Entities/INeptuneeEntity`1.cs: -------------------------------------------------------------------------------- 1 | namespace Neptunee.Entities; 2 | 3 | public interface INeptuneeEntity : INeptuneeEntity 4 | { 5 | public TKey Id { get; set; } 6 | } -------------------------------------------------------------------------------- /Sample/Sample.API/wwwroot/2024-1/17c8d8cc9eab4a43bda85dfd0a9535f4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HusseinnHM/Neptunee/HEAD/Sample/Sample.API/wwwroot/2024-1/17c8d8cc9eab4a43bda85dfd0a9535f4.png -------------------------------------------------------------------------------- /Sample/Sample.API/wwwroot/2024-1/3fee00dbf654497faec9a5da9a27f9b7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HusseinnHM/Neptunee/HEAD/Sample/Sample.API/wwwroot/2024-1/3fee00dbf654497faec9a5da9a27f9b7.png -------------------------------------------------------------------------------- /Sample/Sample.API/wwwroot/2024-1/81298471d4964b9499e54d5cdcbf4994.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HusseinnHM/Neptunee/HEAD/Sample/Sample.API/wwwroot/2024-1/81298471d4964b9499e54d5cdcbf4994.png -------------------------------------------------------------------------------- /Neptunee.Kernel/Clock/INeptuneeClock.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Neptunee.Clock; 3 | 4 | public interface INeptuneeClock 5 | { 6 | DateTimeOffset UtcNow { get; } 7 | DateTimeOffset Now { get; } 8 | 9 | } -------------------------------------------------------------------------------- /Neptunee.Kernel/Entities/INeptuneeCreatableEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Neptunee.Entities; 2 | 3 | public interface INeptuneeCreatableEntity : INeptuneeEntity 4 | { 5 | public DateTimeOffset UtcDateCreated { get; set; } 6 | } -------------------------------------------------------------------------------- /Neptunee.Kernel/Entities/INeptuneeDeletableEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Neptunee.Entities; 2 | 3 | public interface INeptuneeDeletableEntity : INeptuneeEntity 4 | { 5 | public DateTimeOffset? UtcDateDeleted { get; set; } 6 | } -------------------------------------------------------------------------------- /Neptunee.Kernel/Entities/INeptuneeUpdatableEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Neptunee.Entities; 2 | 3 | public interface INeptuneeUpdatableEntity : INeptuneeEntity 4 | { 5 | public DateTimeOffset? UtcDateUpdated { get; set; } 6 | } -------------------------------------------------------------------------------- /Sample/Sample.API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /Sample/Sample.Application/EventManagers/Commands/Responses/AuthorizedEventManagerResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.Application.EventManagers.Commands.Responses; 2 | 3 | public record AuthorizedEventManagerResponse(string Token); -------------------------------------------------------------------------------- /Sample/Sample.Domain/Repositories/IRepository.cs: -------------------------------------------------------------------------------- 1 | using Neptunee.EntityFrameworkCore.Repository; 2 | 3 | namespace Sample.Domain.Repositories; 4 | 5 | public interface IRepository : INeptuneeRepository 6 | { 7 | 8 | } -------------------------------------------------------------------------------- /Neptunee.Kernel/Entities/INeptuneeAuditableEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Neptunee.Entities; 2 | 3 | public interface INeptuneeAuditableEntity : INeptuneeDeletableEntity, INeptuneeCreatableEntity, 4 | INeptuneeUpdatableEntity 5 | { 6 | } -------------------------------------------------------------------------------- /Neptunee.Kernel/Entities/INeptuneeAuditableEntity`1.cs: -------------------------------------------------------------------------------- 1 | namespace Neptunee.Entities; 2 | 3 | public interface INeptuneeAuditableEntity : INeptuneeEntity,INeptuneeAuditableEntity where TKey : struct, IEquatable 4 | { 5 | } -------------------------------------------------------------------------------- /Sample/Sample.Application/ParticipationUsers/Commands/Responses/AuthorizedParticipationUserResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.Application.ParticipationUsers.Commands.Responses; 2 | 3 | public record AuthorizedParticipationUserResponse(string Token); -------------------------------------------------------------------------------- /Sample/Sample.Application/Core/Abstractions/Data/ISampleDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace Sample.Application.Core.Abstractions.Data; 4 | 5 | public interface ISampleDbContext : INeptuneeDbContext 6 | { 7 | 8 | } -------------------------------------------------------------------------------- /Sample/Sample.Application/AssemblyReference.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace Sample.Application; 4 | 5 | public static class AssemblyReference 6 | { 7 | public static readonly Assembly Assembly = typeof(AssemblyReference).Assembly; 8 | } -------------------------------------------------------------------------------- /Sample/Sample.Application/Core/Abstractions/IHttpResolver.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.Application.Core.Abstractions; 2 | 3 | public interface IHttpResolver 4 | { 5 | Guid CurrentUserId(); 6 | string LanguageKey(); 7 | bool IsDefaultLanguageKey(); 8 | } -------------------------------------------------------------------------------- /Sample/Sample.Infrastructure/AssemblyReference.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace Sample.Infrastructure; 4 | 5 | public static class AssemblyReference 6 | { 7 | public static readonly Assembly Assembly = typeof(AssemblyReference).Assembly; 8 | } -------------------------------------------------------------------------------- /Neptunee.Kernel/Clock/NeptuneeClockImp.cs: -------------------------------------------------------------------------------- 1 | namespace Neptunee.Clock; 2 | 3 | public class NeptuneeClockImp : INeptuneeClock 4 | { 5 | public DateTimeOffset UtcNow { get; } = DateTimeOffset.UtcNow; 6 | public DateTimeOffset Now { get; } = DateTimeOffset.Now; 7 | } -------------------------------------------------------------------------------- /Sample/Sample.Application/Core/Abstractions/Security/IJwtBearerGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | 3 | namespace Sample.Application.Core.Abstractions.Security; 4 | 5 | public interface IJwtBearerGenerator 6 | { 7 | string Generate(List claims); 8 | 9 | } -------------------------------------------------------------------------------- /Sample/Sample.Application/Core/Abstractions/IEmailService.cs: -------------------------------------------------------------------------------- 1 | 2 | using Sample.SharedKernel.Contracts.Email; 3 | 4 | namespace Sample.Application.Core.Abstractions; 5 | 6 | public interface IEmailService 7 | { 8 | Task SendEmailAsync(SendEmailRequest sendEmailRequest); 9 | } -------------------------------------------------------------------------------- /Sample/Sample.Application/Core/Abstractions/IFakeTranslate.cs: -------------------------------------------------------------------------------- 1 | using Neptunee.EntityFrameworkCore.MultiLanguage.Types; 2 | 3 | namespace Sample.Application.Core.Abstractions; 4 | 5 | public interface IFakeTranslate 6 | { 7 | void Translate(params MultiLanguageProperty[] props); 8 | } -------------------------------------------------------------------------------- /Sample/Sample.Infrastructure/Security/Jwt/Options/JwtOptions.cs: -------------------------------------------------------------------------------- 1 | namespace EventManagement.Infrastructure.Jwt.Options; 2 | 3 | public class JwtOptions 4 | { 5 | public const string Jwt = "jwt"; 6 | 7 | public string Key { get; set; } 8 | public TimeSpan Expire { get; set; } 9 | } -------------------------------------------------------------------------------- /Neptunee.Kernel/Entities/INeptuneeAggregateRoot.cs: -------------------------------------------------------------------------------- 1 | using Neptunee.DomainEvents; 2 | 3 | namespace Neptunee.Entities; 4 | 5 | public interface INeptuneeAggregateRoot : INeptuneeEntity 6 | { 7 | IReadOnlyCollection DomainEvents { get; } 8 | 9 | void Clear(); 10 | } -------------------------------------------------------------------------------- /Sample/Sample.API/ApiGroupAttribute.cs: -------------------------------------------------------------------------------- 1 | using Neptunee.Swagger.Attributes; 2 | 3 | namespace Sample.API; 4 | 5 | public class ApiGroupAttribute : NeptuneeApiGroupAttribute 6 | { 7 | public ApiGroupAttribute(params ApiGroupNames[] param) : base(param) 8 | { 9 | } 10 | } -------------------------------------------------------------------------------- /Sample/Sample.Application/Core/Abstractions/Security/IPasswordHasher.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.Application.Core.Abstractions.Security; 2 | 3 | public interface IPasswordHasher 4 | { 5 | string HashPassword(string password); 6 | bool VerifyHashedPassword(string hashedPassword, string providedPassword); 7 | } -------------------------------------------------------------------------------- /Sample/Sample.Application/Tickets/Queries/Responses/GetAllTicketResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.Application.Tickets.Queries.Responses; 2 | 3 | public class GetAllTicketResponse 4 | { 5 | public Guid Id { get; init; } 6 | public Guid EventId { get; init; } 7 | public string EventName { get; init; } 8 | 9 | } -------------------------------------------------------------------------------- /Neptunee.Handlers/Requests/INeptuneeRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Neptunee.Handlers.Requests; 2 | 3 | public interface INeptuneeRequest 4 | { 5 | } 6 | 7 | public interface INeptuneeRequest : INeptuneeRequest 8 | { 9 | } 10 | 11 | public struct Void 12 | { 13 | public static Void Value = new(); 14 | } -------------------------------------------------------------------------------- /Neptunee.Kernel/DomainEvents/Handler/INeptuneeDomainEventHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Neptunee.DomainEvents.Handler; 2 | 3 | public interface INeptuneeDomainEventHandler where TDomainEvent : INeptuneeDomainEvent 4 | { 5 | Task HandleAsync(TDomainEvent domainEvent, CancellationToken cancellationToken = default); 6 | } -------------------------------------------------------------------------------- /Sample/Sample.Infrastructure/HttpResolver/Exceptions/InvalidHeaderException.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.Infrastructure.HttpResolver.Exceptions; 2 | 3 | public class InvalidHeaderException : Exception 4 | { 5 | public InvalidHeaderException(string headerKey) : base($"Invalid {headerKey} header") 6 | { 7 | 8 | } 9 | } -------------------------------------------------------------------------------- /Sample/Sample.SharedKernel/Contracts/Requests/PagingRequest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace Sample.SharedKernel.Contracts.Requests; 4 | 5 | public record PagingRequest 6 | { 7 | [DefaultValue(1)] public int PageIndex { get; set; } = 1; 8 | [DefaultValue(10)] public int PageSize { get; set; } = 10; 9 | } -------------------------------------------------------------------------------- /Neptunee.Kernel/DomainEvents/Dispatcher/INeptuneeDomainEventDispatcher.cs: -------------------------------------------------------------------------------- 1 | namespace Neptunee.DomainEvents.Dispatcher; 2 | 3 | public interface INeptuneeDomainEventDispatcher 4 | { 5 | Task PublishAsync(TDomainEvent domainEvent, CancellationToken cancellationToken = default) 6 | where TDomainEvent : INeptuneeDomainEvent; 7 | 8 | } -------------------------------------------------------------------------------- /Neptunee.Kernel/Extensions/NeptuneeEnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace System.Linq; 2 | 3 | public static class NeptuneeEnumerableExtensions 4 | { 5 | public static void ForEach(this IEnumerable source, Action action) 6 | { 7 | foreach (var e in source) 8 | { 9 | action(e); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Neptunee/DependencyInjection/NeptuneeBaseCleanArchitectureCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace Neptunee.DependencyInjection; 4 | 5 | public static class NeptuneeCollectionExtensions 6 | { 7 | public static IServiceCollection AddNeptunee(this IServiceCollection services) => 8 | services.AddNeptuneeClock(); 9 | } -------------------------------------------------------------------------------- /Sample/Sample.API/ApiGroupNames.cs: -------------------------------------------------------------------------------- 1 | using Neptunee.Swagger.Attributes; 2 | 3 | namespace Sample.API; 4 | 5 | public enum ApiGroupNames 6 | { 7 | [NeptuneeDocInfoGenerator(title: "All")] 8 | All, 9 | [NeptuneeDocInfoGenerator(title: "V1", version: "v1")] 10 | V1, 11 | 12 | [NeptuneeDocInfoGenerator(title: "V2", version: "v2")] 13 | V2, 14 | } -------------------------------------------------------------------------------- /Neptunee.Handlers/ServiceFactoryResolver/NeptuneeServiceFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Neptunee.Handlers.ServiceFactoryResolver; 2 | 3 | public delegate object NeptuneeServiceFactory(Type serviceType); 4 | 5 | internal static class NeptuneeServiceFactoryExtensions 6 | { 7 | internal static T Resolve(this NeptuneeServiceFactory factory) 8 | => (T)factory(typeof(T)); 9 | } -------------------------------------------------------------------------------- /Neptunee.Handlers/RequestDispatcher/INeptuneeRequestDispatcher.cs: -------------------------------------------------------------------------------- 1 | using Neptunee.Handlers.Requests; 2 | 3 | namespace Neptunee.Handlers.RequestDispatcher; 4 | 5 | public interface INeptuneeRequestDispatcher 6 | { 7 | Task SendAsync(TRequest request,CancellationToken cancellationToken = default) where TRequest : class, INeptuneeRequest; 8 | } -------------------------------------------------------------------------------- /Neptunee.EntityFrameworkCore/Repository/Read/INeptuneeReadRepository.cs: -------------------------------------------------------------------------------- 1 | using Neptunee.Entities; 2 | 3 | namespace Neptunee.EntityFrameworkCore.Repository; 4 | 5 | public interface INeptuneeReadRepository 6 | { 7 | IQueryable Query() where TEntity : class, INeptuneeEntity; 8 | IQueryable TrackingQuery() where TEntity : class, INeptuneeEntity; 9 | } -------------------------------------------------------------------------------- /Neptunee.EntityFrameworkCore/Interfaces/INeptuneeDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Infrastructure; 2 | 3 | namespace Microsoft.EntityFrameworkCore; 4 | 5 | public interface INeptuneeDbContext 6 | { 7 | DatabaseFacade Database { get; } 8 | DbSet Set() where TEntity : class; 9 | Task SaveChangesAsync(CancellationToken cancellationToken = default); 10 | } -------------------------------------------------------------------------------- /Sample/Sample.Application/Events/Queries/GetAllEventQuery.cs: -------------------------------------------------------------------------------- 1 | using Neptunee.Handlers.Requests; 2 | using Neptunee.OperationResponse; 3 | using Sample.Application.Events.Queries.Responses; 4 | using Sample.SharedKernel.Contracts.Requests; 5 | 6 | namespace Sample.Application.Events.Queries; 7 | 8 | public record GetAllEventQuery : PagingRequest, INeptuneeRequest>>; 9 | 10 | -------------------------------------------------------------------------------- /Sample/Sample.Application/DependencyInjection.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Neptunee.DependencyInjection; 3 | 4 | namespace Sample.Application; 5 | 6 | public static class DependencyInjection 7 | { 8 | public static IServiceCollection AddApplication(this IServiceCollection services) 9 | { 10 | return services.AddNeptuneeRequestHandlers(AssemblyReference.Assembly); 11 | } 12 | } -------------------------------------------------------------------------------- /Sample/Sample.Application/Core/Shared/QueryObjects/UserFilters.cs: -------------------------------------------------------------------------------- 1 | using Sample.Domain.Entities; 2 | 3 | namespace Sample.Application.Core.Shared.QueryObjects; 4 | 5 | public static class UserFilters 6 | { 7 | public static IQueryable FindBy(this IQueryable query, string email) where TUser : class, IEntityHasEmail 8 | { 9 | return query.Where(u => u.Email.ToUpper() == email.ToUpper()); 10 | } 11 | } -------------------------------------------------------------------------------- /Sample/Sample.Application/Events/Queries/GetAllAvailableEventQuery.cs: -------------------------------------------------------------------------------- 1 | using Neptunee.Handlers.Requests; 2 | using Neptunee.OperationResponse; 3 | using Sample.Application.Events.Queries.Responses; 4 | using Sample.SharedKernel.Contracts.Requests; 5 | 6 | namespace Sample.Application.Events.Queries; 7 | 8 | public record GetAllAvailableEventQuery : PagingRequest, INeptuneeRequest>>; 9 | 10 | -------------------------------------------------------------------------------- /Sample/Sample.API/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "ConnectionStrings": { 9 | "DefaultConnection": "Host=localhost;Port=5432;Username=postgres;Password=0000;Database=NeptuneeSampleDBa;" 10 | }, 11 | "Jwt": { 12 | "Key": "xxxMj$098BMW81aCSgoDaAAfvJL1ESA!@$$!", 13 | "Expire": "1.00:00:00", 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sample/Sample.Infrastructure/Persistence/Repositories/Repository.cs: -------------------------------------------------------------------------------- 1 | using Neptunee.EntityFrameworkCore.Repository; 2 | using Sample.Application.Core.Abstractions.Data; 3 | using Sample.Domain.Repositories; 4 | 5 | namespace Sample.Infrastructure.Persistence.Repositories; 6 | 7 | public class Repository : NeptuneeRepository, IRepository 8 | { 9 | public Repository(ISampleDbContext context) : base(context) 10 | { 11 | } 12 | } -------------------------------------------------------------------------------- /Neptunee.Kernel/Clock/DependencyInjection/NeptuneeClockServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Neptunee.Clock; 3 | 4 | namespace Neptunee.DependencyInjection; 5 | 6 | public static class NeptuneeClockServiceCollectionExtensions 7 | { 8 | public static IServiceCollection AddNeptuneeClock(this IServiceCollection services) => 9 | services.AddScoped(); 10 | 11 | } -------------------------------------------------------------------------------- /Sample/Sample.SharedKernel/LanguageKeys.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.SharedKernel; 2 | 3 | public static class LanguageKeys 4 | { 5 | public const string Default = "en"; 6 | public static readonly string[] Other = { "ar" }; 7 | 8 | 9 | public static bool Validate(string language) 10 | { 11 | return Default.Equals(language, StringComparison.OrdinalIgnoreCase) || 12 | Other.Contains(language, StringComparer.OrdinalIgnoreCase); 13 | } 14 | } -------------------------------------------------------------------------------- /Sample/Sample.Infrastructure/Email/Settings/MailSettings.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.Infrastructure.Email.Settings; 2 | 3 | public sealed class MailSettings 4 | { 5 | public const string SettingsKey = "Mail"; 6 | 7 | public string SenderDisplayName { get; set; } 8 | 9 | public string SenderEmail { get; set; } 10 | 11 | public string SmtpPassword { get; set; } 12 | 13 | public string SmtpServer { get; set; } 14 | 15 | public int SmtpPort { get; set; } 16 | } -------------------------------------------------------------------------------- /Sample/Sample.SharedKernel/Contracts/Email/SendEmailRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.SharedKernel.Contracts.Email; 2 | 3 | public sealed class SendEmailRequest 4 | { 5 | public SendEmailRequest(string emailTo, string subject, string body) 6 | { 7 | EmailTo = emailTo; 8 | Subject = subject; 9 | Body = body; 10 | } 11 | 12 | public string EmailTo { get; } 13 | 14 | public string Subject { get; } 15 | 16 | public string Body { get; } 17 | } -------------------------------------------------------------------------------- /Neptunee.Handlers/Requests/INeptuneeRequestHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Neptunee.Handlers.Requests; 2 | 3 | public interface INeptuneeRequestHandler where TRequest : class, INeptuneeRequest 4 | { 5 | Task HandleAsync(TRequest request, CancellationToken cancellationToken = default); 6 | } 7 | 8 | public interface INeptuneeRequestHandler : INeptuneeRequestHandler where TRequest : class, INeptuneeRequest 9 | { 10 | } 11 | 12 | -------------------------------------------------------------------------------- /Sample/Sample.Domain/Sample.Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Neptunee.EntityFrameworkCore/Extensions/NeptuneeQueryableExtensions.cs: -------------------------------------------------------------------------------- 1 | 2 | using Neptunee.Entities; 3 | using Neptunee.EntityFrameworkCore.Specification; 4 | 5 | namespace System.Linq; 6 | 7 | public static class NeptuneeQueryableExtensions 8 | { 9 | public static IQueryable Apply(this IQueryable query, 10 | INeptuneeSpecification specification) where TEntity : class, INeptuneeEntity => 11 | NeptuneeSpecificationEvaluator.GetQuery(query, specification); 12 | } -------------------------------------------------------------------------------- /Sample/Sample.Infrastructure/Persistence/EntitiesConfigurations/EventManagerConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | using Sample.Domain.Entities; 4 | 5 | namespace Sample.Infrastructure.Persistence.EntitiesConfigurations; 6 | 7 | public class EventManagerConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.HasIndex(e => e.Email).IsUnique(); 12 | } 13 | } -------------------------------------------------------------------------------- /Sample/Sample.Application/Events/Queries/Responses/GetAllEventResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.Application.Events.Queries.Responses; 2 | 3 | public record GetAllEventResponse 4 | { 5 | public Guid Id { get; init; } 6 | public string Name { get; init; } 7 | public string Description { get; init; } 8 | public string Location { get; init; } 9 | public DateTime StartDate { get; init; } 10 | public DateTime EndDate { get; init; } 11 | public int AvailableTickets { get; init; } 12 | public int BookedTickets { get; init; } 13 | } -------------------------------------------------------------------------------- /Sample/Sample.Infrastructure/Persistence/EntitiesConfigurations/ParticipationUserConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | using Sample.Domain.Entities; 4 | 5 | namespace Sample.Infrastructure.Persistence.EntitiesConfigurations; 6 | 7 | public class ParticipationUserConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.HasIndex(e => e.Email).IsUnique(); 12 | } 13 | } -------------------------------------------------------------------------------- /Sample/Sample.Application/Events/Queries/Responses/GetAllAvailableEventResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.Application.Events.Queries.Responses; 2 | 3 | public record GetAllAvailableEventResponse 4 | { 5 | public Guid Id { get; init; } 6 | public string Name { get; init; } 7 | public string Description { get; init; } 8 | public string Location { get; init; } 9 | public DateTime StartDate { get; init; } 10 | public DateTime EndDate { get; init; } 11 | public int AvailableTickets { get; init; } 12 | public bool AlreadyBook { get; init; } 13 | } -------------------------------------------------------------------------------- /Sample/Sample.API/Controllers/FilesController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Neptunee.IO.Files; 3 | 4 | namespace Sample.API.Controllers; 5 | 6 | [ApiController] 7 | [Route("api/[controller]/[action]")] 8 | public class FilesController : ControllerBase 9 | { 10 | public record UploadRequest(IFormFile File); 11 | [HttpPost] 12 | public async Task Upload([FromForm] UploadRequest request, [FromServices] INeptuneeFileService fileService) 13 | { 14 | return Ok(await fileService.UploadAsync(request.File)); 15 | } 16 | } -------------------------------------------------------------------------------- /Sample/Sample.SharedKernel/ConstValues.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.SharedKernel; 2 | 3 | public abstract class ConstValues 4 | { 5 | public abstract class HeaderKeys 6 | { 7 | public const string Language = "x-language"; 8 | } 9 | 10 | public abstract class ClaimTypes 11 | { 12 | public const string UserType = "userType"; 13 | } 14 | 15 | public abstract class UserTypes 16 | { 17 | public const string EventManager = nameof(EventManager); 18 | public const string ParticipationUser = nameof(ParticipationUser); 19 | } 20 | } -------------------------------------------------------------------------------- /Sample/Sample.Infrastructure/Persistence/EntitiesConfigurations/EventConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | using Sample.Domain.Entities; 4 | 5 | namespace Sample.Infrastructure.Persistence.EntitiesConfigurations; 6 | 7 | public class EventConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.HasIndex(e => e.StartDate); 12 | builder.Property(e => e.ConcurrencyStamp).IsConcurrencyToken(); 13 | } 14 | } -------------------------------------------------------------------------------- /Neptunee/IO/Files/DependencyInjection/NeptuneeFileServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Neptunee.IO.Files; 3 | using Neptunee.IO.Files.Options; 4 | 5 | namespace Neptunee.DependencyInjection; 6 | 7 | public static class NeptuneeFileServiceCollectionExtensions 8 | { 9 | public static IServiceCollection AddNeptuneeFileService(this IServiceCollection service, Action option) 10 | { 11 | service.Configure(option).AddTransient(); 12 | return service; 13 | } 14 | } -------------------------------------------------------------------------------- /Neptunee/ExceptionFilter/DependencyInjection/NeptuneeExceptionFilterServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Neptunee.Filters; 4 | 5 | namespace Neptunee.DependencyInjection; 6 | 7 | public static class NeptuneeExceptionFilterServiceCollectionExtensions 8 | { 9 | public static IServiceCollection AddNeptuneeExceptionHandlerFilter(this IServiceCollection services) 10 | { 11 | services.Configure(o => o.Filters.Add()); 12 | return services; 13 | } 14 | } -------------------------------------------------------------------------------- /Sample/Sample.Infrastructure/FakeTranslate/FakeTranslateImp.cs: -------------------------------------------------------------------------------- 1 | using Neptunee.EntityFrameworkCore.MultiLanguage; 2 | using Neptunee.EntityFrameworkCore.MultiLanguage.Types; 3 | using Sample.Application.Core.Abstractions; 4 | using Sample.SharedKernel; 5 | 6 | namespace Sample.Infrastructure.FakeTranslate; 7 | 8 | public class FakeTranslateImp : IFakeTranslate 9 | { 10 | public void Translate(params MultiLanguageProperty[] props) 11 | { 12 | foreach (var s in LanguageKeys.Other) 13 | { 14 | foreach (var prop in props) 15 | { 16 | prop.Upsert(s, $"{prop.GetFirst()} in {s}"); 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Sample/Sample.Domain/Entities/Event/Ticket.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.Domain.Entities; 2 | 3 | public class Ticket : Entity 4 | { 5 | private Ticket() 6 | { 7 | } 8 | 9 | public Ticket(Guid eventId, Guid participationUserId) 10 | { 11 | EventId = eventId; 12 | ParticipationUserId = participationUserId; 13 | DateCreated = DateTime.Now; 14 | } 15 | 16 | 17 | public Guid ParticipationUserId { get; private set; } 18 | public ParticipationUser ParticipationUser { get; private set; } 19 | 20 | public Guid EventId { get; private set; } 21 | public Event Event { get; private set; } 22 | 23 | public DateTime DateCreated { get; set; } 24 | } -------------------------------------------------------------------------------- /Sample/Sample.Application/Tickets/Queries/QueryObjects/TicktsResponseSelects.cs: -------------------------------------------------------------------------------- 1 | using Neptunee.EntityFrameworkCore.MultiLanguage; 2 | using Sample.Application.Tickets.Queries.Responses; 3 | using Sample.Domain.Entities; 4 | 5 | namespace Sample.Application.Tickets.Queries.QueryObjects; 6 | 7 | public static class TicketResponseSelects 8 | { 9 | public static IQueryable GetMyTicketsResponse(this IQueryable query, string languageKey) 10 | { 11 | return query.Select(t => new GetAllTicketResponse 12 | { 13 | Id = t.Id, 14 | EventId = t.EventId, 15 | EventName = t.Event.Name.GetIn(languageKey) 16 | }); 17 | } 18 | } -------------------------------------------------------------------------------- /Sample/Sample.Domain/Entities/Users/EventManager.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.Domain.Entities; 2 | 3 | public class EventManager : Entity,IEntityHasEmail, IEntityHasPassword 4 | { 5 | private EventManager() 6 | { 7 | } 8 | public EventManager(string name, string email,string passwordHash) 9 | { 10 | Name = name; 11 | Email = email; 12 | PasswordHash = passwordHash; 13 | } 14 | public string Name { get; private set; } 15 | public string PasswordHash { get; private set; } 16 | public string Email { get; private set; } 17 | 18 | private readonly List _createdEvents = new(); 19 | public IReadOnlyCollection CreatedEvents => _createdEvents.AsReadOnly(); 20 | } -------------------------------------------------------------------------------- /Sample/Sample.Domain/Entities/Users/ParticipationUser.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.Domain.Entities; 2 | 3 | public class ParticipationUser : Entity, IEntityHasEmail, IEntityHasPassword 4 | { 5 | private ParticipationUser() 6 | { 7 | } 8 | 9 | public ParticipationUser(string name, string email, string passwordHash) 10 | { 11 | Name = name; 12 | Email = email; 13 | PasswordHash = passwordHash; 14 | } 15 | 16 | public string Name { get; private set; } 17 | public string Email { get; private set; } 18 | public string PasswordHash { get; private set; } 19 | 20 | private readonly List _tickets = new(); 21 | public IReadOnlyCollection Tickets => _tickets.AsReadOnly(); 22 | } -------------------------------------------------------------------------------- /Neptunee.EntityFrameworkCore/Repository/DependencyInjection/NeptuneeRepositoryServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Neptunee.EntityFrameworkCore.Repository; 4 | 5 | namespace Neptunee.DependencyInjection; 6 | 7 | public static class NeptuneeRepositoryServiceCollectionExtensions 8 | { 9 | public static IServiceCollection AddNeptuneeRepositories(this IServiceCollection services, params Assembly[] assemblies) => 10 | services.Scan(s => s.FromAssemblies(assemblies) 11 | .AddClasses(c => c.AssignableToAny(typeof(INeptuneeRepository), typeof(INeptuneeReadRepository))) 12 | .AsImplementedInterfaces() 13 | .WithScopedLifetime()); 14 | } -------------------------------------------------------------------------------- /Sample/Sample.Application/Sample.Application.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Neptunee.EntityFrameworkCore/Repository/INeptuneeRepository.cs: -------------------------------------------------------------------------------- 1 | using Neptunee.Entities; 2 | 3 | namespace Neptunee.EntityFrameworkCore.Repository; 4 | 5 | public interface INeptuneeRepository : INeptuneeReadRepository 6 | { 7 | void Add(TEntity entity) where TEntity : class, INeptuneeEntity; 8 | void Update(TEntity entity) where TEntity : class, INeptuneeEntity; 9 | void Remove(TEntity entity) where TEntity : class, INeptuneeEntity; 10 | void SoftDelete(TEntity entity) where TEntity : class, INeptuneeDeletableEntity; 11 | void SoftDeleteRange(IEnumerable entities) where TEntity : class, INeptuneeDeletableEntity; 12 | Task SaveChangesAsync(CancellationToken cancellationToken = default); 13 | } -------------------------------------------------------------------------------- /Neptunee.Kernel/Extensions/NeptuneeFile.cs: -------------------------------------------------------------------------------- 1 | namespace Neptunee.Extensions; 2 | 3 | public static class NeptuneeFile 4 | { 5 | public static string GuidAndDateWithExtensionName(string fileName) 6 | { 7 | return $"{Guid.NewGuid():N}_{DateTimeOffset.UtcNow.DateTime.ToString("yyyy-MM-dd-hh-mm-ss").Replace("-", string.Empty)}{Path.GetExtension(fileName)}"; 8 | } 9 | 10 | public static string GuidWithExtensionName(string fileName) 11 | { 12 | return $"{Guid.NewGuid():N}{Path.GetExtension(fileName)}"; 13 | } 14 | 15 | public static string DateWithExtensionName(string fileName) 16 | { 17 | return $"{DateTimeOffset.UtcNow.DateTime.ToString("yyyy-MM-dd-hh-mm-ss").Replace("-", string.Empty)}{Path.GetExtension(fileName)}"; 18 | } 19 | } -------------------------------------------------------------------------------- /Sample/Sample.Domain/Entities/Base/Entity.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | using Neptunee.Entities; 3 | 4 | namespace Sample.Domain.Entities; 5 | 6 | 7 | public class Entity : INeptuneeAuditableEntity 8 | { 9 | public Entity() 10 | { 11 | Id = Guid.NewGuid(); 12 | } 13 | [DatabaseGenerated(DatabaseGeneratedOption.None)] 14 | public Guid Id { get; set; } 15 | public DateTimeOffset? UtcDateDeleted { get; set; } 16 | public DateTimeOffset UtcDateCreated { get; set; } 17 | public DateTimeOffset? UtcDateUpdated { get; set; } 18 | } 19 | 20 | public interface IEntityHasEmail 21 | { 22 | public string Email { get; } 23 | }public interface IEntityHasPassword 24 | { 25 | public string PasswordHash { get; } 26 | } -------------------------------------------------------------------------------- /Sample/Sample.SharedKernel/Sample.SharedKernel.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Sample/Sample.Infrastructure/Security/Policies/UserType/UserTypeRequirement.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Sample.SharedKernel; 3 | 4 | namespace Sample.Infrastructure.Security.Policies.UserType; 5 | 6 | public class UserTypeRequirement : IAuthorizationRequirement 7 | { 8 | public static string ClaimType => ConstValues.ClaimTypes.UserType; 9 | 10 | public Operator Operator { get; } 11 | 12 | public string[] UserTypes { get; } 13 | 14 | public UserTypeRequirement(Operator @operator, string[] userTypes) 15 | { 16 | if (userTypes.Length == 0) 17 | throw new ArgumentException("At least one UserType is required.", nameof(userTypes)); 18 | 19 | Operator = @operator; 20 | UserTypes = userTypes; 21 | } 22 | } -------------------------------------------------------------------------------- /Neptunee.EntityFrameworkCore/Repository/Read/NeptuneeReadRepository.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Neptunee.Entities; 3 | 4 | namespace Neptunee.EntityFrameworkCore.Repository; 5 | 6 | public class NeptuneeReadRepository : INeptuneeReadRepository 7 | where TIContext : INeptuneeDbContext 8 | 9 | { 10 | protected readonly TIContext Context; 11 | 12 | public NeptuneeReadRepository(TIContext context) 13 | { 14 | Context = context; 15 | } 16 | 17 | public virtual IQueryable Query() where TEntity : class, INeptuneeEntity 18 | { 19 | return Context.Query(); 20 | } 21 | 22 | public virtual IQueryable TrackingQuery() where TEntity : class, INeptuneeEntity 23 | { 24 | return Context.TrackingQuery(); 25 | } 26 | 27 | 28 | 29 | 30 | } -------------------------------------------------------------------------------- /Sample/Sample.API/Core/LanguageKey/LanguageKeySwaggerFilter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.OpenApi.Any; 2 | using Microsoft.OpenApi.Models; 3 | using Sample.SharedKernel; 4 | using Swashbuckle.AspNetCore.SwaggerGen; 5 | 6 | namespace Sample.API.Core.LanguageKey; 7 | 8 | public class LanguageKeySwaggerFilter : IOperationFilter 9 | { 10 | public void Apply(OpenApiOperation operation, OperationFilterContext context) 11 | { 12 | operation.Parameters ??= new List(); 13 | operation.Parameters.Add(new OpenApiParameter() 14 | { 15 | Name = ConstValues.HeaderKeys.Language, 16 | In = ParameterLocation.Header, 17 | Required = false, 18 | AllowEmptyValue = false, 19 | Schema = new OpenApiSchema() { Type = "string" }, 20 | Example = new OpenApiString("en"), 21 | }); 22 | } 23 | } -------------------------------------------------------------------------------- /Neptunee.Handlers/RequestDispatcher/NeptuneeRequestDispatcher.cs: -------------------------------------------------------------------------------- 1 | using Neptunee.Handlers.Requests; 2 | using Neptunee.Handlers.ServiceFactoryResolver; 3 | 4 | namespace Neptunee.Handlers.RequestDispatcher; 5 | 6 | public sealed class NeptuneeRequestDispatcher : INeptuneeRequestDispatcher 7 | { 8 | private readonly NeptuneeServiceFactory _serviceFactory; 9 | 10 | public NeptuneeRequestDispatcher(NeptuneeServiceFactory serviceFactory) 11 | { 12 | _serviceFactory = serviceFactory; 13 | } 14 | 15 | 16 | public async Task SendAsync(TRequest request,CancellationToken cancellationToken = default) where TRequest : class, INeptuneeRequest 17 | { 18 | var handler = _serviceFactory.Resolve>(); 19 | return await handler.HandleAsync(request, cancellationToken); 20 | } 21 | } -------------------------------------------------------------------------------- /Neptunee.EntityFrameworkCore/Extensions/NeptuneeModelBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Neptunee.Entities; 2 | 3 | namespace Microsoft.EntityFrameworkCore.Metadata; 4 | 5 | public static class NeptuneeModelBuilderExtensions 6 | { 7 | public static void SetPrimaryKeyValueGenerated(this ModelBuilder builder, ValueGenerated valueGenerated) 8 | { 9 | foreach (var entityType in builder.GetNeptuneeEntityTypes()) 10 | { 11 | foreach (var property in entityType.GetProperties().Where(p => p.IsPrimaryKey())) 12 | { 13 | property.ValueGenerated = valueGenerated; 14 | } 15 | } 16 | } 17 | 18 | public static IEnumerable GetNeptuneeEntityTypes(this ModelBuilder builder) 19 | { 20 | return builder.Model.GetEntityTypes().Where(e => e.ClrType.IsAssignableTo(typeof(INeptuneeEntity))); 21 | } 22 | 23 | 24 | } -------------------------------------------------------------------------------- /Neptunee/AppBuilder/ExceptionHandlerMiddleware/NeptuneeExceptionHandlerAppBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Diagnostics; 3 | using Microsoft.AspNetCore.Http; 4 | using Neptunee.Extensions; 5 | 6 | namespace Neptunee.AppBuilder.ExceptionHandlerMiddleware; 7 | 8 | public static class NeptuneeExceptionHandlerAppBuilderExtensions 9 | { 10 | public static IApplicationBuilder UseNeptuneeExceptionHandler(this IApplicationBuilder builder) 11 | { 12 | return builder.UseExceptionHandler(cfg => 13 | { 14 | cfg.Run(async context => 15 | { 16 | var exceptionHandlerFeature = context.Features.Get()!; 17 | context.Response.StatusCode = StatusCodes.Status500InternalServerError; 18 | await context.Response.WriteAsJsonAsync(exceptionHandlerFeature.Error.ProblemDetails()); 19 | }); 20 | }); 21 | } 22 | } -------------------------------------------------------------------------------- /Sample/Sample.API/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:54237", 8 | "sslPort": 0 9 | } 10 | }, 11 | "profiles": { 12 | "Sample.API": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "https://localhost:7123;http://localhost:5021", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "IIS Express": { 23 | "commandName": "IISExpress", 24 | "launchBrowser": true, 25 | "launchUrl": "swagger", 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sample/Sample.API/Core/ActionsFilters/ForceDefaultLanguage.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.Filters; 2 | using Neptunee.OperationResponse; 3 | using Sample.Application.Core.Abstractions; 4 | using Sample.SharedKernel; 5 | 6 | namespace Sample.API.Core.ActionsFilters; 7 | 8 | public class ForceDefaultLanguage : IActionFilter 9 | { 10 | private readonly IHttpResolver _httpResolver; 11 | 12 | public ForceDefaultLanguage(IHttpResolver httpResolver) 13 | { 14 | _httpResolver = httpResolver; 15 | } 16 | 17 | public void OnActionExecuting(ActionExecutingContext context) 18 | { 19 | if (!_httpResolver.IsDefaultLanguageKey()) 20 | { 21 | context.Result = Operation.BadRequest().Error($"{ConstValues.HeaderKeys.Language} header value must be : {LanguageKeys.Default} (default)").ToIActionResult(); 22 | } 23 | } 24 | 25 | public void OnActionExecuted(ActionExecutedContext context) 26 | { 27 | 28 | } 29 | } -------------------------------------------------------------------------------- /Neptunee.EntityFrameworkCore/Specification/INeptuneeSpecification.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using Neptunee.Entities; 3 | 4 | namespace Neptunee.EntityFrameworkCore.Specification; 5 | 6 | public interface INeptuneeSpecification where TEntity : class, INeptuneeEntity 7 | { 8 | public List>> Filters { get; } 9 | public List>> Includes { get; } 10 | public List IncludeStrings { get; } 11 | public Expression>? OrderBy { get; } 12 | public Expression>? OrderByDescending { get; } 13 | public Expression>? ThenOrderBy { get; } 14 | public Expression>? ThenOrderByDescending { get; } 15 | public Expression>? GroupBy { get; } 16 | 17 | public int? Take { get; } 18 | public int? Skip { get; } 19 | public bool Tracking { get; } 20 | public bool SplitQuery { get; } 21 | } -------------------------------------------------------------------------------- /Sample/Sample.API/Sample.API.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | all 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Neptunee/ExceptionHandler/NeptuneeExceptionHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Diagnostics; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.Extensions.Logging; 4 | using Neptunee.Extensions; 5 | 6 | namespace Neptunee.ExceptionHandler; 7 | 8 | #if NET8_0 9 | public class NeptuneeExceptionHandler : IExceptionHandler 10 | { 11 | private readonly ILogger _logger; 12 | 13 | public NeptuneeExceptionHandler(ILogger logger) 14 | { 15 | _logger = logger; 16 | } 17 | 18 | public async ValueTask TryHandleAsync( 19 | HttpContext httpContext, 20 | Exception exception, 21 | CancellationToken cancellationToken) 22 | { 23 | _logger.LogError(exception, "An unhandled exception has occurred while executing the request."); 24 | httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; 25 | await httpContext.Response.WriteAsJsonAsync(exception.ProblemDetails(), cancellationToken); 26 | return true; 27 | } 28 | } 29 | #endif -------------------------------------------------------------------------------- /Sample/Sample.Domain/Entities/Base/AggregateRoot.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | using Neptunee.DomainEvents; 3 | using Neptunee.Entities; 4 | 5 | namespace Sample.Domain.Entities; 6 | 7 | public class AggregateRoot : Entity, INeptuneeAggregateRoot 8 | { 9 | public AggregateRoot() 10 | { 11 | Id = Guid.NewGuid(); 12 | } 13 | 14 | [DatabaseGenerated(DatabaseGeneratedOption.None)] 15 | public Guid Id { get; set; } 16 | 17 | public DateTimeOffset? UtcDateDeleted { get; set; } 18 | public DateTimeOffset UtcDateCreated { get; set; } 19 | public DateTimeOffset? UtcDateUpdated { get; set; } 20 | 21 | private readonly List _domainEvents = new(); 22 | 23 | public IReadOnlyCollection DomainEvents => _domainEvents.AsReadOnly(); 24 | 25 | public void Clear() 26 | { 27 | _domainEvents.Clear(); 28 | } 29 | 30 | protected void AddDomainEvent(INeptuneeDomainEvent domainEvent) 31 | { 32 | _domainEvents.Add(domainEvent); 33 | } 34 | } -------------------------------------------------------------------------------- /Neptunee.Handlers/DependencyInjection/NeptuneeHandlersServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Neptunee.Handlers.RequestDispatcher; 4 | using Neptunee.Handlers.Requests; 5 | using Neptunee.Handlers.ServiceFactoryResolver; 6 | 7 | namespace Neptunee.DependencyInjection; 8 | 9 | public static class NeptuneeHandlersServiceCollectionExtensions 10 | { 11 | public static IServiceCollection AddNeptuneeRequestDispatcher(this IServiceCollection services) => 12 | services.AddTransient(p => p.GetRequiredService) 13 | .AddTransient(); 14 | 15 | 16 | public static IServiceCollection AddNeptuneeRequestHandlers(this IServiceCollection services, params Assembly[] assemblies) => 17 | services.Scan(s => s.FromAssemblies(assemblies) 18 | .AddClasses(c => c.AssignableToAny(typeof(INeptuneeRequestHandler<,>))) 19 | .AsImplementedInterfaces() 20 | .WithTransientLifetime()); 21 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Hussein Haj Mohammed 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Neptunee/ExceptionFilter/NeptuneeExceptionHandlerFilter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.AspNetCore.Mvc.Filters; 3 | using Microsoft.Extensions.Logging; 4 | using Neptunee.Extensions; 5 | 6 | namespace Neptunee.Filters; 7 | 8 | public class NeptuneeExceptionHandlerFilter : IAsyncExceptionFilter 9 | { 10 | private readonly ILogger _logger; 11 | 12 | public NeptuneeExceptionHandlerFilter(ILogger logger) 13 | { 14 | _logger = logger; 15 | } 16 | 17 | 18 | public async Task OnExceptionAsync(ExceptionContext context) 19 | { 20 | if (!context.ExceptionHandled) 21 | { 22 | _logger.LogError(context.Exception, "An unhandled exception has occurred while executing the request."); 23 | context.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; 24 | await context.HttpContext.Response.WriteAsJsonAsync(context.Exception.ProblemDetails()); 25 | context.ExceptionHandled = true; 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /Neptunee/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Hussein Haj Mohammed 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Neptunee.Handlers/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Hussein Haj Mohammed 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Neptunee.Kernel/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Hussein Haj Mohammed 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Sample/Sample.Infrastructure/Security/Policies/SampleAuthorizationPolicyProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.Extensions.Options; 3 | using Sample.Infrastructure.Security.Policies.UserType; 4 | 5 | namespace Sample.Infrastructure.Security.Policies; 6 | 7 | public class SampleAuthorizationPolicyProvider(IOptions options) : DefaultAuthorizationPolicyProvider(options) 8 | { 9 | public override async Task GetPolicyAsync(string policyName) 10 | { 11 | if (policyName.StartsWith(HasUserTypesAttribute.PolicyPrefix, StringComparison.OrdinalIgnoreCase)) 12 | { 13 | var @operator = HasUserTypesAttribute.GetOperatorFromPolicy(policyName); 14 | var userTypes = HasUserTypesAttribute.GetUserTypesFromPolicy(policyName); 15 | 16 | var requirement = new UserTypeRequirement(@operator, userTypes); 17 | 18 | return new AuthorizationPolicyBuilder().AddRequirements(requirement).Build(); 19 | } 20 | 21 | return await base.GetPolicyAsync(policyName); 22 | } 23 | } -------------------------------------------------------------------------------- /Neptunee.EntityFrameworkCore/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Hussein Haj Mohammed 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Sample/Sample.Infrastructure/Persistence/DataSeed/DataSeed.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Sample.Application.Core.Abstractions.Security; 4 | using Sample.Domain.Entities; 5 | using Sample.Infrastructure.Persistence.Context; 6 | 7 | namespace Sample.Infrastructure.Persistence.DataSeed; 8 | 9 | public class DataSeed 10 | { 11 | public static async Task Seed(SampleDbContext context, IServiceProvider serviceProvider) 12 | { 13 | await SeedUsers(context, serviceProvider.GetRequiredService()); 14 | } 15 | 16 | private static async Task SeedUsers(SampleDbContext context, IPasswordHasher passwordHasher) 17 | { 18 | if (context.ParticipationUsers.Any()) 19 | { 20 | return; 21 | } 22 | 23 | context.ParticipationUsers.Add(new ParticipationUser("Seed participation", "seed@test.com", passwordHasher.HashPassword("1234"))); 24 | context.EventManagers.Add(new EventManager("Seed EventManager", "seed@test.com", passwordHasher.HashPassword("1234"))); 25 | 26 | await context.SaveChangesAsync(); 27 | } 28 | } -------------------------------------------------------------------------------- /Sample/Sample.Infrastructure/Security/Jwt/JwtBearerGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.IdentityModel.Tokens.Jwt; 2 | using System.Security.Claims; 3 | using System.Text; 4 | using EventManagement.Infrastructure.Jwt.Options; 5 | using Microsoft.Extensions.Options; 6 | using Microsoft.IdentityModel.Tokens; 7 | using Sample.Application.Core.Abstractions.Security; 8 | 9 | namespace Sample.Infrastructure.Security.Jwt; 10 | 11 | public class JwtBearerGenerator : IJwtBearerGenerator 12 | { 13 | private readonly JwtOptions _jwtOptions; 14 | 15 | public JwtBearerGenerator(IOptions options) 16 | { 17 | _jwtOptions = options.Value; 18 | } 19 | 20 | 21 | public string Generate(List claims) 22 | { 23 | var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOptions.Key)); 24 | var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature); 25 | var token = new JwtSecurityToken( 26 | claims: claims, 27 | expires: DateTime.Now.Add(_jwtOptions.Expire), 28 | signingCredentials: credentials); 29 | 30 | return new JwtSecurityTokenHandler().WriteToken(token); 31 | } 32 | } -------------------------------------------------------------------------------- /Sample/Sample.Domain/Errors/Erros.cs: -------------------------------------------------------------------------------- 1 | using Neptunee.OperationResponse; 2 | 3 | namespace Sample.Domain; 4 | 5 | public static class Errors 6 | { 7 | public static class Events 8 | { 9 | public static Error AvailableTickets(int minimum) => new($"AvailableTickets cannot be less than {minimum}"); 10 | public static readonly Error StartDateCannotBeInPast = new("StartDate cannot be in past"); 11 | public static readonly Error EndDateCannotBeBeforeStatDate = new("EndDate cannot be before statDate"); 12 | public static readonly Error ChangesHappened = new("Event data are changed by another user after you got the original value"); 13 | } 14 | 15 | public static class Tickets 16 | { 17 | public static readonly Error AlreadyBooked = new("Already Booked"); 18 | public static readonly Error FullBooking = new("Full Booking"); 19 | } 20 | 21 | public static class Users 22 | { 23 | public static readonly Error EmailNotFound = new("Email not fount"); 24 | public static readonly Error EmailAlreadyUsed = new("Email already used"); 25 | public static readonly Error WrongEmailOrPassword = new("Wrong email or password "); 26 | } 27 | } -------------------------------------------------------------------------------- /Sample/Sample.Infrastructure/Security/Policies/UserType/HasUserTypesAttribute.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | 3 | namespace Sample.Infrastructure.Security.Policies.UserType; 4 | 5 | public class HasUserTypesAttribute : AuthorizeAttribute 6 | { 7 | internal const string PolicyPrefix = "HasUserTypes_"; 8 | private const string Separator = "_"; 9 | 10 | 11 | public HasUserTypesAttribute(Operator @operator, params string[] userTypes) 12 | { 13 | Policy = $"{PolicyPrefix}{(int)@operator}{Separator}{string.Join(Separator, userTypes)}"; 14 | } 15 | 16 | 17 | public HasUserTypesAttribute(string userType) 18 | { 19 | Policy = $"{PolicyPrefix}{(int)Operator.And}{Separator}{userType}"; 20 | } 21 | 22 | public static Operator GetOperatorFromPolicy(string policyName) 23 | { 24 | var @operator = int.Parse(policyName.AsSpan(PolicyPrefix.Length, 1)); 25 | return (Operator)@operator; 26 | } 27 | 28 | public static string[] GetUserTypesFromPolicy(string policyName) 29 | { 30 | return policyName.Substring(PolicyPrefix.Length + 2) 31 | .Split(new[] {Separator}, StringSplitOptions.RemoveEmptyEntries); 32 | } 33 | } -------------------------------------------------------------------------------- /Neptunee.Kernel/DomainEvents/Dispatcher/NeptuneeDomainEventDispatcher.cs: -------------------------------------------------------------------------------- 1 | using Neptunee.DomainEvents; 2 | using Neptunee.DomainEvents.Dispatcher; 3 | using Neptunee.DomainEvents.Handler; 4 | 5 | namespace Neptunee.Dispatchers.DomainEventDispatcher; 6 | 7 | public delegate object NeptuneeServiceFactory(Type serviceType); 8 | public sealed class NeptuneeDomainEventDispatcher : INeptuneeDomainEventDispatcher 9 | { 10 | private readonly NeptuneeServiceFactory _serviceFactory; 11 | 12 | public NeptuneeDomainEventDispatcher(NeptuneeServiceFactory serviceFactory) 13 | { 14 | _serviceFactory = serviceFactory; 15 | } 16 | 17 | 18 | public async Task PublishAsync(TDomainEvent domainEvent, CancellationToken cancellationToken = default) 19 | where TDomainEvent : INeptuneeDomainEvent 20 | { 21 | var domainEventHandler = _serviceFactory(typeof(INeptuneeDomainEventHandler<>).MakeGenericType(domainEvent.GetType())); 22 | await (Task)domainEventHandler 23 | .GetType() 24 | .GetMethod(nameof(INeptuneeDomainEventHandler.HandleAsync))! 25 | .Invoke(domainEventHandler, new object[] { domainEvent, cancellationToken })!; 26 | } 27 | } -------------------------------------------------------------------------------- /Sample/Sample.Infrastructure/Persistence/Context/SampleDbContext.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Microsoft.EntityFrameworkCore; 3 | using Neptunee.EntityFrameworkCore.MultiLanguage.Extensions; 4 | using Sample.Application.Core.Abstractions.Data; 5 | using Sample.Domain.Entities; 6 | 7 | namespace Sample.Infrastructure.Persistence.Context; 8 | 9 | public sealed class SampleDbContext : DbContext, ISampleDbContext 10 | { 11 | public SampleDbContext(DbContextOptions options) : base(options) 12 | { 13 | } 14 | 15 | public DbSet Events => Set(); 16 | public DbSet Tickets => Set(); 17 | public DbSet EventManagers => Set(); 18 | public DbSet ParticipationUsers => Set(); 19 | 20 | 21 | protected override void OnModelCreating(ModelBuilder builder) 22 | { 23 | base.OnModelCreating(builder); 24 | 25 | builder.ConfigureMultiLanguage(Database); 26 | 27 | builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly(), t => t.GetInterfaces() 28 | .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEntityTypeConfiguration<>))); 29 | } 30 | } -------------------------------------------------------------------------------- /Sample/Sample.Infrastructure/Sample.Infrastructure.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | all 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Sample/Sample.API/Controllers/ZTestController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.EntityFrameworkCore; 3 | using Neptunee.Handlers.RequestDispatcher; 4 | using Sample.Infrastructure.Persistence.Context; 5 | using Sample.Infrastructure.Persistence.DataSeed; 6 | 7 | namespace Sample.API.Controllers; 8 | 9 | [ApiController] 10 | [Route("api/[controller]/[action]")] 11 | public sealed class ZTestController : ControllerBase 12 | { 13 | private readonly SampleDbContext _context; 14 | private readonly IServiceProvider _serviceProvider; 15 | 16 | public ZTestController(INeptuneeRequestDispatcher dispatcher, SampleDbContext context, IServiceProvider serviceProvider) 17 | { 18 | _context = context; 19 | _serviceProvider = serviceProvider; 20 | } 21 | 22 | [HttpGet] 23 | public async Task DeleteDb(bool deleteIfNotDev = false) 24 | { 25 | await _context.Database.EnsureDeletedAsync(); 26 | await _context.Database.MigrateAsync(); 27 | await DataSeed.Seed(_context, _serviceProvider); 28 | return Ok("Done"); 29 | } 30 | 31 | [HttpGet] 32 | public IActionResult TestThrowException() 33 | { 34 | throw new ApplicationException("This test"); 35 | } 36 | } -------------------------------------------------------------------------------- /Sample/Sample.Application/EventManagers/Commands/LoginEventManagerCommand.cs: -------------------------------------------------------------------------------- 1 | using Neptunee.Handlers.Requests; 2 | using Neptunee.OperationResponse; 3 | using Sample.Application.Core.Abstractions.Security; 4 | using Sample.Application.Core.Shared.Handlers; 5 | using Sample.Application.EventManagers.Commands.Responses; 6 | using Sample.Domain.Entities; 7 | using Sample.Domain.Repositories; 8 | 9 | namespace Sample.Application.EventManagers.Commands; 10 | 11 | public record LoginEventManagerCommand(string Email, string Password) : INeptuneeRequest>; 12 | 13 | internal class LoginEventManagerCommandHandler : BaseSecurityHandler> 14 | { 15 | public LoginEventManagerCommandHandler(IPasswordHasher passwordHasher, IRepository repository, IJwtBearerGenerator jwtBearerGenerator) : base(passwordHasher, repository, jwtBearerGenerator) 16 | { 17 | } 18 | 19 | public override async Task> HandleAsync(LoginEventManagerCommand request, CancellationToken cancellationToken = default) 20 | { 21 | var result = await Login(request.Email, request.Password, cancellationToken); 22 | return result.IsFailure ? result : new AuthorizedEventManagerResponse(result); 23 | } 24 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Neptunee 2 | 3 | [![](https://img.shields.io/nuget/v/Neptunee?label=Neptunee)](https://www.nuget.org/packages/Neptunee) ![](https://img.shields.io/nuget/dt/Neptunee) 4 |
5 | [![](https://img.shields.io/nuget/v/Neptunee.Handlers?label=Neptunee.Handlers)](https://www.nuget.org/packages/Neptunee.Handlers) ![](https://img.shields.io/nuget/dt/Neptunee.Handlers) 6 |
7 | [![](https://img.shields.io/nuget/v/Neptunee.EntityFrameworkCore?label=Neptunee.EntityFrameworkCore)](https://www.nuget.org/packages/Neptunee.EntityFrameworkCore) ![](https://img.shields.io/nuget/dt/Neptunee.EntityFrameworkCore) 8 | 9 | **Domain-Driven Design (DDD)** & **Clean Architecture** with common **design patterns**. 10 |

11 | icon 12 | 13 | ## Highlights 14 | 15 | - [x] .NET 6.0 or Higher 16 | - [x] Domain-Driven Design & Clean Architecture Principles 17 | - [x] Entity Framework Core as DB Abstraction 18 | - [x] Repository & Specification Patterns 19 | - [x] Global Exceptions Handling (ExceptionFilter or Middleware) 20 | - [x] SoftDelete 21 | - [x] Audit Logging 22 | - [x] File Storage Service 23 | - [x] CQRS Without MediatR Dependencies 24 | - [x] DomainEvents Without MediatR Dependencies 25 | - [ ] And Much More 26 | 27 | 28 | ## Additional 29 | 30 | * [ ] [Sample](https://github.com/HusseinnHM/Neptunee/blob/master/Sample) 31 | -------------------------------------------------------------------------------- /Neptunee.Kernel/Extensions/NeptuneeExceptionExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace Neptunee.Extensions; 6 | 7 | public static class ExceptionExtensions 8 | { 9 | public static string FullMessage(this Exception exception) 10 | { 11 | var fullMessage = new StringBuilder(); 12 | return Recursive(exception); 13 | 14 | string Recursive(Exception ex) 15 | { 16 | fullMessage.Append(Environment.NewLine + ex + Environment.NewLine + ex.Message); 17 | if (ex.InnerException is null) return fullMessage.ToString(); 18 | return Recursive(ex.InnerException); 19 | } 20 | } 21 | 22 | public static ProblemDetails ProblemDetails(this Exception exception) 23 | { 24 | var problemDetails = new ProblemDetails 25 | { 26 | Status = StatusCodes.Status500InternalServerError, 27 | Title = "Server error", 28 | Detail = exception.Message, 29 | Type = exception.GetType().Name, 30 | }; 31 | problemDetails.Extensions.Add("FullDetail", exception.FullMessage()); 32 | problemDetails.Extensions.Add(nameof(exception.Source), exception.Source); 33 | problemDetails.Extensions.Add(nameof(exception.Data), exception.Data); 34 | return problemDetails; 35 | } 36 | } -------------------------------------------------------------------------------- /Sample/Sample.Infrastructure/HttpResolver/HttpResolverImp.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using Microsoft.AspNetCore.Http; 3 | using Sample.Application.Core.Abstractions; 4 | using Sample.Infrastructure.HttpResolver.Exceptions; 5 | using Sample.SharedKernel; 6 | 7 | namespace Sample.Infrastructure.HttpResolver; 8 | 9 | public class HttpResolverImp : IHttpResolver 10 | { 11 | private readonly IHttpContextAccessor _httpContextAccessor; 12 | 13 | 14 | public HttpResolverImp(IHttpContextAccessor httpContextAccessor) 15 | { 16 | _httpContextAccessor = httpContextAccessor; 17 | } 18 | 19 | public Guid CurrentUserId() 20 | { 21 | return Guid.TryParse(_httpContextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier), out var id) 22 | ? id 23 | : Guid.Empty; 24 | } 25 | 26 | public string LanguageKey() 27 | { 28 | var languageKey = _httpContextAccessor.HttpContext?.Request.Headers[ConstValues.HeaderKeys.Language].ToString() ?? string.Empty; 29 | 30 | return LanguageKeys.Validate(languageKey) || true 31 | ? languageKey 32 | : throw new InvalidHeaderException(ConstValues.HeaderKeys.Language); 33 | } 34 | 35 | public bool IsDefaultLanguageKey() 36 | { 37 | return LanguageKeys.Default.Equals(LanguageKey(), StringComparison.OrdinalIgnoreCase); 38 | } 39 | } -------------------------------------------------------------------------------- /Sample/Sample.Application/ParticipationUsers/Commands/LoginParticipationUserCommand.cs: -------------------------------------------------------------------------------- 1 | using Neptunee.Handlers.Requests; 2 | using Neptunee.OperationResponse; 3 | using Sample.Application.Core.Abstractions.Security; 4 | using Sample.Application.Core.Shared.Handlers; 5 | using Sample.Application.ParticipationUsers.Commands.Responses; 6 | using Sample.Domain.Entities; 7 | using Sample.Domain.Repositories; 8 | 9 | namespace Sample.Application.ParticipationUsers.Commands; 10 | 11 | public record LoginParticipationUserCommand(string Email, string Password) : INeptuneeRequest>; 12 | 13 | internal class LoginParticipationUserCommandHandler : BaseSecurityHandler> 14 | { 15 | public LoginParticipationUserCommandHandler(IPasswordHasher passwordHasher, IRepository repository, IJwtBearerGenerator jwtBearerGenerator) : base(passwordHasher, repository, jwtBearerGenerator) 16 | { 17 | } 18 | 19 | public override async Task> HandleAsync(LoginParticipationUserCommand request, CancellationToken cancellationToken = default) 20 | { 21 | var result = await Login(request.Email, request.Password, cancellationToken); 22 | return result.IsFailure ? result : new AuthorizedParticipationUserResponse(result); 23 | } 24 | } -------------------------------------------------------------------------------- /Neptunee.Kernel/DomainEvents/DependencyInjection/NeptuneeDomainEventsServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Neptunee.Dispatchers.DomainEventDispatcher; 4 | using Neptunee.DomainEvents.Dispatcher; 5 | using Neptunee.DomainEvents.Handler; 6 | 7 | namespace Neptunee.DependencyInjection; 8 | 9 | public static class NeptuneeDomainEventsServiceCollectionExtensions 10 | { 11 | public static IServiceCollection AddNeptuneeDomainEvents(this IServiceCollection services, params Assembly[] assemblies) => 12 | services 13 | .AddNeptuneeDomainEventsDispatcher() 14 | .AddNeptuneeDomainEventsHandlers(assemblies); 15 | 16 | public static IServiceCollection AddNeptuneeDomainEventsHandlers(this IServiceCollection services, params Assembly[] assemblies) => 17 | services 18 | .Scan(s => s.FromAssemblies(assemblies) 19 | .AddClasses(c => c.AssignableToAny(typeof(INeptuneeDomainEventHandler<>))) 20 | .AsImplementedInterfaces() 21 | .WithTransientLifetime()); 22 | 23 | internal static IServiceCollection AddNeptuneeDomainEventsDispatcher(this IServiceCollection services) => 24 | services 25 | .AddTransient(p => p.GetRequiredService) 26 | .AddTransient(); 27 | 28 | 29 | } -------------------------------------------------------------------------------- /Neptunee/IO/Files/Options/NeptuneeFileOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Neptunee.Extensions; 3 | 4 | namespace Neptunee.IO.Files.Options; 5 | 6 | public class NeptuneeFileOptions 7 | { 8 | internal string BasePath { get; private set; } = string.Empty; 9 | 10 | internal Func FileNameProvider { get; private set; } 11 | = file => NeptuneeFile.GuidWithExtensionName(file.FileName); 12 | 13 | internal Func DirUploadPathProvider { get; private set; } 14 | = (basePath, _) => 15 | { 16 | var dir = $"{DateTimeOffset.UtcNow.Year}-{DateTimeOffset.UtcNow.Month}"; 17 | var phyDir = Path.Combine(basePath, dir); 18 | if (!Directory.Exists(phyDir)) 19 | { 20 | Directory.CreateDirectory(phyDir); 21 | } 22 | 23 | return dir; 24 | }; 25 | 26 | public NeptuneeFileOptions SetBasePath(string path) 27 | { 28 | BasePath = path; 29 | return this; 30 | } 31 | 32 | public NeptuneeFileOptions SetFileNameProvider(Func fileNameProvider) 33 | { 34 | FileNameProvider = fileNameProvider; 35 | return this; 36 | } 37 | 38 | public NeptuneeFileOptions SetDirUploadPathProvider(Func dirUploadPathProvider) 39 | { 40 | DirUploadPathProvider = dirUploadPathProvider; 41 | return this; 42 | } 43 | } -------------------------------------------------------------------------------- /Sample/Sample.Application/EventManagers/Commands/RegisterEventManagerCommand.cs: -------------------------------------------------------------------------------- 1 | using Neptunee.Handlers.Requests; 2 | using Neptunee.OperationResponse; 3 | using Sample.Application.Core.Abstractions.Security; 4 | using Sample.Application.Core.Shared.Handlers; 5 | using Sample.Application.EventManagers.Commands.Responses; 6 | using Sample.Domain.Entities; 7 | using Sample.Domain.Repositories; 8 | 9 | namespace Sample.Application.EventManagers.Commands; 10 | 11 | public record RegisterEventManagerCommand(string Name, string Email, string Password) : INeptuneeRequest>; 12 | 13 | internal class RegisterEventManagerCommandHandler : BaseSecurityHandler> 14 | { 15 | public RegisterEventManagerCommandHandler(IPasswordHasher passwordHasher, IRepository repository, IJwtBearerGenerator jwtBearerGenerator) : base(passwordHasher, repository, jwtBearerGenerator) 16 | { 17 | } 18 | 19 | public override async Task> HandleAsync(RegisterEventManagerCommand request, CancellationToken cancellationToken = default) 20 | { 21 | var result = await Register(request.Email, 22 | () => new EventManager(request.Name, request.Email, PasswordHasher.HashPassword(request.Password)), 23 | cancellationToken); 24 | return result.IsFailure ? result : new AuthorizedEventManagerResponse(result); 25 | } 26 | } -------------------------------------------------------------------------------- /Sample/Sample.Infrastructure/Security/Policies/UserType/UserTypeHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | 3 | namespace Sample.Infrastructure.Security.Policies.UserType; 4 | 5 | public class UserTypeHandler : AuthorizationHandler 6 | { 7 | protected override Task HandleRequirementAsync( 8 | AuthorizationHandlerContext context, UserTypeRequirement requirement) 9 | { 10 | if (requirement.Operator == Operator.And) 11 | { 12 | foreach (var userType in requirement.UserTypes) 13 | { 14 | if (!context.User.HasClaim(c => c.Type == UserTypeRequirement.ClaimType && c.Value.Equals(userType,StringComparison.OrdinalIgnoreCase))) 15 | { 16 | context.Fail(); 17 | return Task.CompletedTask; 18 | } 19 | } 20 | 21 | context.Succeed(requirement); 22 | return Task.CompletedTask; 23 | } 24 | 25 | foreach (var userType in requirement.UserTypes) 26 | { 27 | if (context.User.HasClaim(c => c.Type == UserTypeRequirement.ClaimType && c.Value.Equals(userType,StringComparison.OrdinalIgnoreCase))) 28 | { 29 | context.Succeed(requirement); 30 | return Task.CompletedTask; 31 | } 32 | } 33 | 34 | context.Fail(); 35 | return Task.CompletedTask; 36 | } 37 | } -------------------------------------------------------------------------------- /Sample/Sample.API/Controllers/EventManagersController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Neptunee.Handlers.Requests; 4 | using Neptunee.OperationResponse; 5 | using Sample.Application.EventManagers.Commands; 6 | using Sample.Application.EventManagers.Commands.Responses; 7 | using Swashbuckle.AspNetCore.Annotations; 8 | 9 | namespace Sample.API.Controllers; 10 | 11 | [ApiController] 12 | [Route("api/[controller]/[action]")] 13 | public sealed class EventManagersController : ControllerBase 14 | { 15 | [HttpPost] 16 | [AllowAnonymous] 17 | [SwaggerResponse(StatusCodes.Status200OK, type: typeof(AuthorizedEventManagerResponse))] 18 | public async Task Login( 19 | [FromServices] INeptuneeRequestHandler> handler, 20 | [FromBody] LoginEventManagerCommand command, 21 | CancellationToken ct) 22 | => await handler.HandleAsync(command, ct).ToIActionResultAsync(); 23 | 24 | [HttpPost] 25 | [AllowAnonymous] 26 | [SwaggerResponse(StatusCodes.Status200OK, type: typeof(AuthorizedEventManagerResponse))] 27 | public async Task Register( 28 | [FromServices] INeptuneeRequestHandler> handler, 29 | [FromBody] RegisterEventManagerCommand command, 30 | CancellationToken ct) 31 | => await handler.HandleAsync(command, ct).ToIActionResultAsync(); 32 | } -------------------------------------------------------------------------------- /Sample/Sample.Application/ParticipationUsers/Commands/RegisterParticipationUserCommand.cs: -------------------------------------------------------------------------------- 1 | using Neptunee.Handlers.Requests; 2 | using Neptunee.OperationResponse; 3 | using Sample.Application.Core.Abstractions.Security; 4 | using Sample.Application.Core.Shared.Handlers; 5 | using Sample.Application.ParticipationUsers.Commands.Responses; 6 | using Sample.Domain.Entities; 7 | using Sample.Domain.Repositories; 8 | 9 | namespace Sample.Application.ParticipationUsers.Commands; 10 | 11 | public record RegisterParticipationUserCommand(string Name, string Email, string Password) : INeptuneeRequest>; 12 | 13 | internal class RegisterParticipationUserCommandHandler : BaseSecurityHandler> 14 | { 15 | public RegisterParticipationUserCommandHandler(IPasswordHasher passwordHasher, IRepository repository, IJwtBearerGenerator jwtBearerGenerator) : base(passwordHasher, repository, jwtBearerGenerator) 16 | { 17 | } 18 | 19 | public override async Task> HandleAsync(RegisterParticipationUserCommand request, CancellationToken cancellationToken = default) 20 | { 21 | var result = await Register(request.Email, 22 | () => new ParticipationUser(request.Name, request.Email, PasswordHasher.HashPassword(request.Password)), 23 | cancellationToken); 24 | return result.IsFailure ? result : new AuthorizedParticipationUserResponse(result); 25 | } 26 | } -------------------------------------------------------------------------------- /Neptunee.Handlers/Neptunee.Handlers.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | enable 5 | enable 6 | CS0693 7 | $(NoWarn);1591 8 | true 9 | default 10 | true 11 | HusseinnHM 12 | MIT 13 | https://github.com/HusseinnHM/Neptunee 14 | icon.png 15 | https://github.com/HusseinnHM/Neptunee 16 | dotnet, csharp, web, api, cqrs, mediator, CleanArchitecture, DDD 17 | Slim CQRS & Mediator 18 | net6.0;net7.0;net8.0 19 | 1.0.1 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /Neptunee.EntityFrameworkCore/Repository/NeptuneeRepository.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Neptunee.Entities; 3 | 4 | namespace Neptunee.EntityFrameworkCore.Repository; 5 | 6 | public class NeptuneeRepository : NeptuneeReadRepository, INeptuneeRepository 7 | where TIContext : INeptuneeDbContext 8 | { 9 | public NeptuneeRepository(TIContext context) : base(context) 10 | { 11 | } 12 | 13 | 14 | public virtual void Add(TEntity entity) where TEntity : class, INeptuneeEntity 15 | { 16 | Context.Set().Add(entity); 17 | } 18 | 19 | public virtual void Update(TEntity entity) where TEntity : class, INeptuneeEntity 20 | { 21 | Context.Set().Update(entity); 22 | } 23 | 24 | public virtual void Remove(TEntity entity) where TEntity : class, INeptuneeEntity 25 | { 26 | Context.Set().Remove(entity); 27 | } 28 | 29 | public virtual void SoftDelete(TEntity entity) where TEntity : class, INeptuneeDeletableEntity 30 | { 31 | Context.Set().SoftDelete(entity); 32 | } 33 | 34 | public virtual void SoftDeleteRange(IEnumerable entities) 35 | where TEntity : class, INeptuneeDeletableEntity 36 | { 37 | Context.Set().SoftDeleteRange(entities); 38 | } 39 | 40 | 41 | public async Task SaveChangesAsync(CancellationToken cancellationToken = default) 42 | { 43 | return await Context.SaveChangesAsync(cancellationToken); 44 | } 45 | } -------------------------------------------------------------------------------- /Neptunee/Neptunee.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | enable 5 | enable 6 | CS0693 7 | $(NoWarn);1591 8 | true 9 | default 10 | true 11 | HusseinnHM 12 | MIT 13 | https://github.com/HusseinnHM/Neptunee 14 | icon.png 15 | https://github.com/HusseinnHM/Neptunee 16 | dotnet, csharp, web, api, CleanArchitecture, DDD 17 | Domain-Driven Design (DDD) & Clean Architecture with common design patterns. 18 | net6.0;net7.0;net8.0 19 | 1.0.1 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /Sample/Sample.API/Controllers/ParticipationUsersController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Neptunee.Handlers.Requests; 4 | using Neptunee.OperationResponse; 5 | using Sample.Application.ParticipationUsers.Commands; 6 | using Sample.Application.ParticipationUsers.Commands.Responses; 7 | using Sample.Application.ParticipationUsers.Commands; 8 | using Swashbuckle.AspNetCore.Annotations; 9 | 10 | namespace Sample.API.Controllers; 11 | 12 | public sealed class ParticipationUsersController : ControllerBase 13 | { 14 | [HttpPost] 15 | [AllowAnonymous] 16 | [SwaggerResponse(StatusCodes.Status200OK, type: typeof(AuthorizedParticipationUserResponse))] 17 | public async Task Login( 18 | [FromServices] INeptuneeRequestHandler> handler, 19 | [FromBody] LoginParticipationUserCommand command, 20 | CancellationToken ct) 21 | => await handler.HandleAsync(command, ct).ToIActionResultAsync(); 22 | 23 | [HttpPost] 24 | [AllowAnonymous] 25 | [SwaggerResponse(StatusCodes.Status200OK, type: typeof(AuthorizedParticipationUserResponse))] 26 | public async Task Register( 27 | [FromServices] INeptuneeRequestHandler> handler, 28 | [FromBody] RegisterParticipationUserCommand command, 29 | CancellationToken ct) 30 | => await handler.HandleAsync(command, ct).ToIActionResultAsync(); 31 | } -------------------------------------------------------------------------------- /Sample/Sample.API/Program.cs: -------------------------------------------------------------------------------- 1 | using Neptunee.AppBuilder.ExceptionHandlerMiddleware; 2 | using Neptunee.DependencyInjection; 3 | using Neptunee.EntityFrameworkCore.Extensions; 4 | using Neptunee.OperationResponse.DependencyInjection; 5 | using Neptunee.Swagger; 6 | using Neptunee.Swagger.DependencyInjection; 7 | using Sample.API; 8 | using Sample.API.Core.LanguageKey; 9 | using Sample.Application; 10 | using Sample.Infrastructure; 11 | using Sample.Infrastructure.Persistence.Context; 12 | using Sample.Infrastructure.Persistence.DataSeed; 13 | 14 | 15 | var builder = WebApplication.CreateBuilder(args); 16 | builder.Services.AddControllers(); 17 | 18 | builder.Services 19 | .AddOperationSerializerOptions() 20 | .AddNeptuneeRequestDispatcher() 21 | .AddNeptuneeSwagger(o => 22 | { 23 | o.AddJwtBearerSecurityScheme(); 24 | o.SwaggerDocs(); 25 | o.GroupNamesDocInclusion(ApiGroupNames.All.ToString()); 26 | o.OperationFilter(); 27 | }) 28 | .AddNeptuneeFileService(o => o.SetBasePath(builder.Environment.WebRootPath)) 29 | //.AddNeptuneeExceptionHandlerFilter() 30 | .AddApplication() 31 | .AddInfrastructure(builder.Configuration, builder.Environment) 32 | .AddNeptunee(); 33 | 34 | var app = builder.Build(); 35 | 36 | app 37 | .UseNeptuneeExceptionHandler() 38 | .UseNeptuneeSwagger(o => o.AddEndpoints().SetDocExpansion()) 39 | .UseAuthorization(); 40 | 41 | app.MapControllers(); 42 | 43 | await app.MigrationAsync(DataSeed.Seed); 44 | 45 | app.Run(); -------------------------------------------------------------------------------- /Neptunee.Kernel/Neptunee.Kernel.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Neptunee 5 | enable 6 | enable 7 | CS0693 8 | $(NoWarn);1591 9 | true 10 | default 11 | true 12 | HusseinnHM 13 | MIT 14 | https://github.com/HusseinnHM/Neptunee 15 | icon.png 16 | https://github.com/HusseinnHM/Neptunee 17 | dotnet, csharp, web, api, CleanArchitecture, DDD 18 | Domain-Driven Design (DDD) & Clean Architecture with common design patterns. 19 | net6.0;net7.0;net8.0 20 | 1.0.1 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /Sample/Sample.Application/Events/Queries/QueryObjects/EventResponseSelects.cs: -------------------------------------------------------------------------------- 1 | using Neptunee.EntityFrameworkCore.MultiLanguage; 2 | using Sample.Application.Events.Queries.Responses; 3 | using Sample.Domain.Entities; 4 | 5 | namespace Sample.Application.Events.Queries.QueryObjects; 6 | 7 | public static class EventResponseSelects 8 | { 9 | public static IQueryable GetAllEventResponse(this IQueryable query, string languageKey) 10 | => query.Select(e => new GetAllEventResponse() 11 | { 12 | Id =e.Id, 13 | Name = e.Name.GetIn(languageKey), 14 | Description = e.Description.GetIn(languageKey), 15 | Location = e.Location.GetIn(languageKey), 16 | StartDate = e.StartDate, 17 | EndDate = e.EndDate, 18 | AvailableTickets = e.AvailableTickets, 19 | BookedTickets = e.Tickets.Count 20 | }); 21 | 22 | public static IQueryable GetAllAvailableEventResponse(this IQueryable query, string languageKey, Guid currentUserId) 23 | => query.Select(e => new GetAllAvailableEventResponse 24 | { 25 | Id = e.Id, 26 | Name = e.Name.GetIn(languageKey), 27 | Description = e.Description.GetIn(languageKey), 28 | Location = e.Location.GetIn(languageKey), 29 | StartDate = e.StartDate, 30 | EndDate = e.EndDate, 31 | AvailableTickets = e.AvailableTickets - e.Tickets.Count, 32 | AlreadyBook = e.Tickets.Any(ep => ep.ParticipationUserId == currentUserId) 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /Sample/Sample.Infrastructure/Email/EmailService.cs: -------------------------------------------------------------------------------- 1 | using MailKit.Net.Smtp; 2 | using MailKit.Security; 3 | using Microsoft.Extensions.Options; 4 | using MimeKit; 5 | using MimeKit.Text; 6 | using Sample.Application.Core.Abstractions; 7 | using Sample.Infrastructure.Email.Settings; 8 | using Sample.SharedKernel.Contracts.Email; 9 | 10 | namespace Sample.Infrastructure.Email; 11 | 12 | internal sealed class EmailService : IEmailService 13 | { 14 | private readonly MailSettings _mailSettings; 15 | 16 | public EmailService(IOptions maiLSettingsOptions) 17 | { 18 | _mailSettings = maiLSettingsOptions.Value; 19 | } 20 | 21 | 22 | public async Task SendEmailAsync(SendEmailRequest sendEmailRequest) 23 | { 24 | var email = new MimeMessage 25 | { 26 | From = 27 | { 28 | new MailboxAddress(_mailSettings.SenderDisplayName, _mailSettings.SenderEmail) 29 | }, 30 | To = 31 | { 32 | MailboxAddress.Parse(sendEmailRequest.EmailTo) 33 | }, 34 | Subject = sendEmailRequest.Subject, 35 | Body = new TextPart(TextFormat.Text) 36 | { 37 | Text = sendEmailRequest.Body 38 | } 39 | }; 40 | using var smtpClient = new SmtpClient(); 41 | await smtpClient.ConnectAsync(_mailSettings.SmtpServer, _mailSettings.SmtpPort, SecureSocketOptions.StartTls); 42 | await smtpClient.AuthenticateAsync(_mailSettings.SenderEmail, _mailSettings.SmtpPassword); 43 | await smtpClient.SendAsync(email); 44 | await smtpClient.DisconnectAsync(true); 45 | } 46 | } -------------------------------------------------------------------------------- /Sample/Sample.Application/Tickets/Commands/CancelTicketCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using Microsoft.EntityFrameworkCore; 3 | using Neptunee.Clock; 4 | using Neptunee.Handlers.Requests; 5 | using Neptunee.OperationResponse; 6 | using Sample.Application.Core.Abstractions; 7 | using Sample.Domain.Entities; 8 | using Sample.Domain.Repositories; 9 | 10 | namespace Sample.Application.Tickets.Commands; 11 | 12 | public record CancelTicketCommand(Guid Id) : INeptuneeRequest>; 13 | 14 | internal class CancelTicketCommandHandler : INeptuneeRequestHandler> 15 | { 16 | private readonly IRepository _repository; 17 | private readonly IHttpResolver _httpResolver; 18 | 19 | private readonly INeptuneeClock _clock; 20 | 21 | public CancelTicketCommandHandler(IRepository repository, IHttpResolver httpResolver, INeptuneeClock clock) 22 | { 23 | _repository = repository; 24 | _httpResolver = httpResolver; 25 | _clock = clock; 26 | } 27 | 28 | public async Task> HandleAsync(CancelTicketCommand request, CancellationToken cancellationToken = default) 29 | { 30 | var operation = Operation.Unknown(); 31 | var currentUserId = _httpResolver.CurrentUserId(); 32 | var deletedRows = await _repository 33 | .TrackingQuery() 34 | .Where(t => t.Id == request.Id && t.ParticipationUserId == currentUserId) 35 | .ExecuteUpdateAsync(e => e.SetProperty(f => f.UtcDateDeleted, _ => _clock.UtcNow), cancellationToken: cancellationToken); 36 | 37 | return deletedRows == 0 38 | ? operation.SetStatusCode(HttpStatusCode.NotFound) 39 | : operation.SetStatusCode(HttpStatusCode.OK); 40 | } 41 | } -------------------------------------------------------------------------------- /Neptunee.EntityFrameworkCore/Neptunee.EntityFrameworkCore.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | enable 5 | enable 6 | CS0693 7 | $(NoWarn);1591 8 | true 9 | default 10 | true 11 | HusseinnHM 12 | MIT 13 | https://github.com/HusseinnHM/Neptunee 14 | icon.png 15 | https://github.com/HusseinnHM/Neptunee 16 | EFCore, EntityFrameworkCore, dotnet, csharp, web, api, CleanArchitecture, DDD 17 | Assist EntityFrameworkCore. 18 | net6.0;net7.0;net8.0 19 | 1.0.1 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Sample/Sample.Application/Tickets/Commands/BookTicketCommand.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Neptunee.Handlers.Requests; 3 | using Neptunee.OperationResponse; 4 | using Sample.Application.Core.Abstractions; 5 | using Sample.Domain.Entities; 6 | using Sample.Domain.Repositories; 7 | using NoResponse = Neptunee.OperationResponse.NoResponse; 8 | 9 | namespace Sample.Application.Tickets.Commands; 10 | 11 | public record BookTicketCommand(Guid EventId) : INeptuneeRequest>; 12 | 13 | internal class BookTicketCommandHandler : INeptuneeRequestHandler> 14 | { 15 | private readonly IRepository _repository; 16 | private readonly IHttpResolver _httpResolver; 17 | 18 | public BookTicketCommandHandler(IRepository repository, IHttpResolver httpResolver) 19 | { 20 | _repository = repository; 21 | _httpResolver = httpResolver; 22 | } 23 | 24 | public async Task> HandleAsync(BookTicketCommand request, CancellationToken cancellationToken = default) 25 | { 26 | var eventEntity = await _repository 27 | .TrackingQuery() 28 | .Where(request.EventId) 29 | .Include(e => e.Tickets) 30 | .FirstAsync(cancellationToken: cancellationToken); 31 | var result = eventEntity.Book(_httpResolver.CurrentUserId()); 32 | if (result.IsFailure) 33 | { 34 | return result; 35 | } 36 | 37 | // await Task.Delay(10000); to testing Concurrency 38 | try 39 | { 40 | await _repository.SaveChangesAsync(cancellationToken); 41 | } 42 | catch (DbUpdateConcurrencyException) 43 | { 44 | return await HandleAsync(request); 45 | } 46 | 47 | return Operation.Ok(); 48 | } 49 | 50 | 51 | } -------------------------------------------------------------------------------- /Sample/Sample.Application/Tickets/Queries/GetAllMyTicketQuery.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Neptunee.Handlers.Requests; 3 | using Neptunee.OperationResponse; 4 | using Sample.Application.Core.Abstractions; 5 | using Sample.Application.Tickets.Queries.QueryObjects; 6 | using Sample.Application.Tickets.Queries.Responses; 7 | using Sample.Domain.Entities; 8 | using Sample.Domain.Repositories; 9 | using Sample.SharedKernel.Contracts.Requests; 10 | 11 | namespace Sample.Application.Tickets.Queries; 12 | 13 | public record GetAllTicketQuery : PagingRequest, INeptuneeRequest>>; 14 | 15 | internal class GetAllTicketQueryHandler : INeptuneeRequestHandler>> 16 | { 17 | private readonly IRepository _repository; 18 | private readonly IHttpResolver _httpResolver; 19 | private INeptuneeRequestHandler>> _neptuneeRequestHandlerImplementation; 20 | 21 | public GetAllTicketQueryHandler(IRepository repository, IHttpResolver httpResolver) 22 | { 23 | _repository = repository; 24 | _httpResolver = httpResolver; 25 | } 26 | 27 | public async Task>> HandleAsync(GetAllTicketQuery request, CancellationToken cancellationToken = default) 28 | { 29 | var currentUserId = _httpResolver.CurrentUserId(); 30 | return await _repository 31 | .Query() 32 | .FilterIgnoreSoftDeleted() 33 | .Where(t => t.ParticipationUserId == currentUserId) 34 | .Paging(request.PageIndex,request.PageSize) 35 | .OrderBy(t => t.Event.StartDate <= DateTime.Now) 36 | .GetMyTicketsResponse(_httpResolver.LanguageKey()) 37 | .ToListAsync(cancellationToken: cancellationToken); 38 | } 39 | } -------------------------------------------------------------------------------- /Neptunee.EntityFrameworkCore/Interceptors/PublishNeptuneeDomainEventsInterceptor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Diagnostics; 3 | using Neptunee.DomainEvents.Dispatcher; 4 | using Neptunee.Entities; 5 | 6 | namespace Neptunee.EntityFrameworkCore.Interceptors; 7 | 8 | public class PublishNeptuneeDomainEventsInterceptor : SaveChangesInterceptor where TKey : struct, IEquatable 9 | { 10 | private readonly INeptuneeDomainEventDispatcher _domainEventDispatcher; 11 | 12 | public PublishNeptuneeDomainEventsInterceptor(INeptuneeDomainEventDispatcher domainEventDispatcher) 13 | { 14 | _domainEventDispatcher = domainEventDispatcher; 15 | } 16 | 17 | public override async ValueTask> SavingChangesAsync(DbContextEventData eventData, InterceptionResult result, CancellationToken cancellationToken = new CancellationToken()) 18 | { 19 | var dbContext = eventData.Context; 20 | if (dbContext is null) 21 | { 22 | return await base.SavingChangesAsync(eventData, result, cancellationToken); 23 | } 24 | 25 | 26 | await DispatchDomainEventsAsync(dbContext, cancellationToken); 27 | return await base.SavingChangesAsync(eventData, result, cancellationToken); 28 | } 29 | 30 | 31 | private async Task DispatchDomainEventsAsync(DbContext dbContext, CancellationToken cancellationToken) 32 | { 33 | var events = new List(); 34 | foreach (var aggregateRoot in dbContext 35 | .ChangeTracker 36 | .Entries() 37 | .Where(a => a.Entity.DomainEvents.Count != 0) 38 | .Select(a => a.Entity)) 39 | { 40 | events.AddRange(aggregateRoot 41 | .DomainEvents 42 | .Select(e => _domainEventDispatcher.PublishAsync(e, cancellationToken))); 43 | aggregateRoot.Clear(); 44 | } 45 | await Task.WhenAll(events); 46 | } 47 | } -------------------------------------------------------------------------------- /Neptunee.Kernel/Entities/NeptuneeEnumeration.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace Neptunee.Entities; 4 | 5 | public abstract record NeptuneeEnumeration(int Value, string Name) where TEnum : NeptuneeEnumeration 6 | { 7 | private static readonly Lazy> EnumerationsDictionary = new(() => GetAllEnumerationOptions().ToDictionary(item => item.Value)); 8 | 9 | protected NeptuneeEnumeration() : this(default, string.Empty) 10 | { 11 | } 12 | 13 | public static IReadOnlyCollection List => EnumerationsDictionary.Value.Values.ToList(); 14 | 15 | public int Value { get; private set; } = Value; 16 | 17 | public string Name { get; private set; } = Name; 18 | 19 | public static TEnum FromValue(int value) => EnumerationsDictionary.Value.TryGetValue(value, out var enumeration) 20 | ? enumeration! 21 | : default!; 22 | 23 | public static bool ContainsValue(int value) => EnumerationsDictionary.Value.ContainsKey(value); 24 | 25 | 26 | private static IEnumerable GetAllEnumerationOptions() 27 | { 28 | Type enumType = typeof(TEnum); 29 | 30 | IEnumerable enumerationTypes = Assembly 31 | .GetAssembly(enumType)! 32 | .GetTypes() 33 | .Where(type => enumType.IsAssignableFrom(type)); 34 | 35 | var enumerations = new List(); 36 | 37 | foreach (Type enumerationType in enumerationTypes) 38 | { 39 | List enumerationTypeOptions = GetFieldsOfType(enumerationType); 40 | 41 | enumerations.AddRange(enumerationTypeOptions); 42 | } 43 | 44 | return enumerations; 45 | } 46 | 47 | 48 | private static List GetFieldsOfType(Type type) => 49 | type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy) 50 | .Where(fieldInfo => type.IsAssignableFrom(fieldInfo.FieldType)) 51 | .Select(fieldInfo => (TFieldType)fieldInfo.GetValue(null)!) 52 | .ToList(); 53 | } -------------------------------------------------------------------------------- /Sample/Sample.API/Controllers/TicketsController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Neptunee.Handlers.Requests; 3 | using Neptunee.OperationResponse; 4 | using Sample.Application.Events.Queries.Responses; 5 | using Sample.Application.Tickets.Commands; 6 | using Sample.Application.Tickets.Queries; 7 | using Sample.Application.Tickets.Queries.Responses; 8 | using Sample.Infrastructure.Security.Policies.UserType; 9 | using Sample.SharedKernel; 10 | using Swashbuckle.AspNetCore.Annotations; 11 | 12 | namespace Sample.API.Controllers; 13 | 14 | [ApiController] 15 | [Route("api/[controller]/[action]")] 16 | public class TicketsController : ControllerBase 17 | { 18 | [HttpGet] 19 | [HasUserTypes(ConstValues.UserTypes.ParticipationUser)] 20 | [SwaggerResponse(StatusCodes.Status200OK, type: typeof(List))] 21 | public async Task GetAll( 22 | [FromServices] INeptuneeRequestHandler>> handler, 23 | [FromQuery] GetAllTicketQuery query, 24 | CancellationToken ct) 25 | => await handler.HandleAsync(query, ct).ToIActionResultAsync(); 26 | 27 | [HttpPost] 28 | [HasUserTypes(ConstValues.UserTypes.ParticipationUser)] 29 | [SwaggerResponse(StatusCodes.Status200OK, type: typeof(Operation))] 30 | public async Task Book( 31 | [FromServices] INeptuneeRequestHandler> handler, 32 | [FromBody] BookTicketCommand command, 33 | CancellationToken ct) 34 | => await handler.HandleAsync(command, ct).ToIActionResultAsync(); 35 | 36 | [HttpPost] 37 | [HasUserTypes(ConstValues.UserTypes.ParticipationUser)] 38 | [SwaggerResponse(StatusCodes.Status200OK, type: typeof(Operation))] 39 | public async Task Cancel( 40 | [FromServices] INeptuneeRequestHandler> handler, 41 | [FromBody] CancelTicketCommand command, 42 | CancellationToken ct) 43 | => await handler.HandleAsync(command, ct).ToIActionResultAsync(); 44 | } -------------------------------------------------------------------------------- /Sample/Sample.Application/Events/Queries/Handlers/EventQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Neptunee.Handlers.Requests; 3 | using Neptunee.OperationResponse; 4 | using Sample.Application.Core.Abstractions; 5 | using Sample.Application.Events.Queries.QueryObjects; 6 | using Sample.Application.Events.Queries.Responses; 7 | using Sample.Domain.Entities; 8 | using Sample.Domain.Repositories; 9 | 10 | namespace Sample.Application.Events.Queries.Handlers; 11 | 12 | public class EventQueryHandler : 13 | INeptuneeRequestHandler>>, 14 | INeptuneeRequestHandler>> 15 | { 16 | private readonly IHttpResolver _httpResolver; 17 | private readonly IRepository _repository; 18 | 19 | public EventQueryHandler(IRepository repository, IHttpResolver httpResolver) 20 | { 21 | _repository = repository; 22 | _httpResolver = httpResolver; 23 | } 24 | 25 | public async Task>> HandleAsync(GetAllEventQuery request, CancellationToken cancellationToken) 26 | => await _repository 27 | .Query() 28 | .FilterIgnoreSoftDeleted() 29 | .Where(e => e.EventManagerId == _httpResolver.CurrentUserId()) 30 | .OrderBy(e => e.StartDate <= DateTime.UtcNow) 31 | .Paging(request.PageIndex, request.PageSize) 32 | .GetAllEventResponse(_httpResolver.LanguageKey()) 33 | .ToListAsync(cancellationToken: cancellationToken); 34 | 35 | public async Task>> HandleAsync(GetAllAvailableEventQuery request, CancellationToken cancellationToken = default) 36 | { 37 | return await _repository.Query() 38 | .FilterIgnoreSoftDeleted() 39 | .Paging(request.PageIndex, request.PageSize) 40 | .OrderBy(e => e.StartDate) 41 | .GetAllAvailableEventResponse(_httpResolver.LanguageKey(), _httpResolver.CurrentUserId()) 42 | .ToListAsync(cancellationToken: cancellationToken); 43 | } 44 | } -------------------------------------------------------------------------------- /Sample/Sample.Application/Events/Commands/AddEventCommand.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Neptunee.Handlers.Requests; 3 | using Neptunee.OperationResponse; 4 | using Sample.Application.Core.Abstractions; 5 | using Sample.Application.Events.Queries.QueryObjects; 6 | using Sample.Application.Events.Queries.Responses; 7 | using Sample.Domain.Entities; 8 | using Sample.Domain.Repositories; 9 | 10 | namespace Sample.Application.Events.Commands; 11 | 12 | public record AddEventCommand( 13 | string Name, 14 | string Description, 15 | string Location, 16 | DateTime StartDate, 17 | DateTime EndDate, 18 | int AvailableTickets) : INeptuneeRequest>; 19 | 20 | internal class AddEventCommandHandler : INeptuneeRequestHandler> 21 | { 22 | private readonly IRepository _repository; 23 | private readonly IHttpResolver _httpResolver; 24 | private readonly IFakeTranslate _fakeTranslate; 25 | 26 | public AddEventCommandHandler(IRepository repository, IHttpResolver httpResolver, IFakeTranslate fakeTranslate) 27 | { 28 | _repository = repository; 29 | _httpResolver = httpResolver; 30 | _fakeTranslate = fakeTranslate; 31 | } 32 | 33 | public async Task> HandleAsync(AddEventCommand request, CancellationToken cancellationToken = default) 34 | { 35 | var eventEntity = new Event(request.Name, 36 | request.Description, 37 | request.Location, 38 | request.StartDate, 39 | request.EndDate, 40 | request.AvailableTickets, 41 | _httpResolver.CurrentUserId(), 42 | _httpResolver.LanguageKey()); 43 | 44 | _fakeTranslate.Translate(eventEntity.Name, eventEntity.Description, eventEntity.Location); 45 | _repository.Add(eventEntity); 46 | await _repository.SaveChangesAsync(cancellationToken); 47 | 48 | return await _repository.Query() 49 | .Where(eventEntity.Id) 50 | .GetAllEventResponse(_httpResolver.LanguageKey()) 51 | .FirstAsync(cancellationToken: cancellationToken); 52 | } 53 | } -------------------------------------------------------------------------------- /Neptunee.EntityFrameworkCore/Extensions/NeptuneeDbContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.ChangeTracking; 2 | using Microsoft.EntityFrameworkCore.Infrastructure; 3 | using Neptunee.Clock; 4 | using Neptunee.Entities; 5 | 6 | namespace Microsoft.EntityFrameworkCore; 7 | 8 | public static class NeptuneeDbContextExtensions 9 | { 10 | public static IQueryable TrackingQuery(this INeptuneeDbContext dbContext) where TEntity : class, INeptuneeEntity 11 | { 12 | return dbContext.Set(); 13 | } 14 | 15 | public static IQueryable TrackingQuery(this DbContext dbContext) where TEntity : class, INeptuneeEntity 16 | { 17 | return dbContext.Set(); 18 | } 19 | 20 | public static IQueryable Query(this INeptuneeDbContext dbContext) where TEntity : class, INeptuneeEntity 21 | { 22 | return dbContext.TrackingQuery().AsNoTracking(); 23 | } 24 | 25 | public static IQueryable Query(this DbContext dbContext) where TEntity : class, INeptuneeEntity 26 | { 27 | return dbContext.TrackingQuery().AsNoTracking(); 28 | } 29 | 30 | public static EntityEntry Add(this DbContext dbContext,TEntity entity) where TEntity : class, INeptuneeCreatableEntity 31 | { 32 | entity.UtcDateCreated = dbContext.GetService().UtcNow; 33 | return dbContext.Add(entity); 34 | } 35 | public static EntityEntry Add(this DbSet dbSet,TEntity entity) where TEntity : class, INeptuneeCreatableEntity 36 | { 37 | entity.UtcDateCreated = dbSet.GetService().UtcNow; 38 | return dbSet.Add(entity); 39 | } 40 | 41 | 42 | 43 | public static void SoftDelete(this DbSet dbSet, TEntity entity) where TEntity : class, INeptuneeDeletableEntity 44 | { 45 | entity.UtcDateDeleted = dbSet.GetService().UtcNow; 46 | } 47 | 48 | public static void SoftDeleteRange(this DbSet dbSet, IEnumerable entities) where TEntity : class, INeptuneeDeletableEntity 49 | { 50 | foreach (var entity in entities) 51 | { 52 | dbSet.SoftDelete(entity); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /Neptunee.EntityFrameworkCore/Interceptors/UpdateAuditableNeptuneeEntitiesInterceptor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Diagnostics; 3 | using Neptunee.Clock; 4 | using Neptunee.Entities; 5 | 6 | namespace Neptunee.EntityFrameworkCore.Interceptors; 7 | 8 | public class UpdateAuditableNeptuneeEntitiesInterceptor : SaveChangesInterceptor where TKey : struct, IEquatable 9 | { 10 | private readonly INeptuneeClock _clock; 11 | 12 | public UpdateAuditableNeptuneeEntitiesInterceptor(INeptuneeClock clock) 13 | { 14 | _clock = clock; 15 | } 16 | 17 | public override async ValueTask> SavingChangesAsync(DbContextEventData eventData, InterceptionResult result, CancellationToken cancellationToken = new CancellationToken()) 18 | { 19 | var dbContext = eventData.Context; 20 | if (dbContext is null) 21 | { 22 | return await base.SavingChangesAsync(eventData, result, cancellationToken); 23 | } 24 | 25 | UpdateAuditableEntities(dbContext, _clock.UtcNow); 26 | return await base.SavingChangesAsync(eventData, result, cancellationToken); 27 | } 28 | 29 | private void UpdateAuditableEntities(DbContext dbContext, DateTimeOffset dateTimeNow) 30 | { 31 | foreach (var entityEntry in dbContext.ChangeTracker.Entries()) 32 | { 33 | switch (entityEntry) 34 | { 35 | case { State: EntityState.Added, Entity: INeptuneeCreatableEntity }: 36 | entityEntry.Property(nameof(INeptuneeCreatableEntity.UtcDateCreated)).CurrentValue = dateTimeNow; 37 | break; 38 | case { State: EntityState.Modified, Entity: INeptuneeDeletableEntity } 39 | when entityEntry.Property(nameof(INeptuneeDeletableEntity.UtcDateDeleted)).CurrentValue is not null: 40 | entityEntry.Property(nameof(INeptuneeDeletableEntity.UtcDateDeleted)).CurrentValue = dateTimeNow; 41 | break; 42 | case { State: EntityState.Modified, Entity: INeptuneeUpdatableEntity }: 43 | entityEntry.Property(nameof(INeptuneeUpdatableEntity.UtcDateUpdated)).CurrentValue = dateTimeNow; 44 | break; 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /Sample/Sample.Infrastructure/DependencyInjection.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using Neptunee.Clock; 7 | using Neptunee.DependencyInjection; 8 | using Neptunee.DomainEvents.Dispatcher; 9 | using Neptunee.EntityFrameworkCore.Interceptors; 10 | using Neptunee.EntityFrameworkCore.MultiLanguage.DependencyInjection; 11 | using Sample.Application.Core.Abstractions; 12 | using Sample.Application.Core.Abstractions.Data; 13 | using Sample.Infrastructure.Email; 14 | using Sample.Infrastructure.Email.Settings; 15 | using Sample.Infrastructure.FakeTranslate; 16 | using Sample.Infrastructure.HttpResolver; 17 | using Sample.Infrastructure.Persistence.Context; 18 | using Sample.Infrastructure.Security.DependencyInjection; 19 | 20 | namespace Sample.Infrastructure; 21 | 22 | public static class DependencyInjection 23 | { 24 | public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration,IWebHostEnvironment environment) 25 | { 26 | 27 | return services 28 | .AddDbContext((sp,o) => 29 | { 30 | o.UseNpgsql(configuration.GetConnectionString("DefaultConnection")); 31 | if (!environment.IsProduction()) 32 | { 33 | o.EnableSensitiveDataLogging(); 34 | } 35 | 36 | o.AddInterceptors(new PublishNeptuneeDomainEventsInterceptor(sp.GetRequiredService())); 37 | o.AddInterceptors(new UpdateAuditableNeptuneeEntitiesInterceptor(sp.GetRequiredService())); 38 | }) 39 | .AddMultiLanguage() 40 | .AddNeptuneeRepositories(AssemblyReference.Assembly) 41 | .AddNeptuneeDomainEvents(Application.AssemblyReference.Assembly) 42 | .AddHttpContextAccessor() 43 | .Configure(configuration.GetSection(MailSettings.SettingsKey)) 44 | .AddScoped() 45 | .AddTransient() 46 | .AddTransient() 47 | .AddSecurity(configuration); 48 | } 49 | } -------------------------------------------------------------------------------- /Neptunee.EntityFrameworkCore/Specification/NeptuneeSpecificationEvaluator.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Neptunee.Entities; 3 | 4 | namespace Neptunee.EntityFrameworkCore.Specification; 5 | 6 | internal class NeptuneeSpecificationEvaluator where TEntity : class, INeptuneeEntity 7 | { 8 | public static async Task> GetList(IQueryable query, INeptuneeSpecification specification) 9 | { 10 | return await GetQuery(query, specification).ToListAsync(); 11 | } 12 | 13 | public static IQueryable GetQuery(IQueryable query, INeptuneeSpecification specification) 14 | { 15 | query = specification.Filters.Aggregate(query, (queryable, predicate) => queryable.Where(predicate)); 16 | 17 | query = specification.Includes.Aggregate(query, (queryable, include) => queryable.Include(include)); 18 | 19 | query = specification.IncludeStrings.Aggregate(query, (queryable, include) => queryable.Include(include)); 20 | 21 | query = specification switch 22 | { 23 | { OrderBy: not null, ThenOrderBy: not null } => query.OrderBy(specification.OrderBy).ThenBy(specification.ThenOrderBy), 24 | { OrderBy: not null, ThenOrderByDescending: not null } => query.OrderBy(specification.OrderBy).ThenByDescending(specification.ThenOrderByDescending), 25 | { OrderBy: not null } => query.OrderBy(specification.OrderBy), 26 | { OrderByDescending: not null, ThenOrderBy: not null } => query.OrderByDescending(specification.OrderByDescending).ThenBy(specification.ThenOrderBy), 27 | { OrderByDescending: not null, ThenOrderByDescending: not null } => query.OrderByDescending(specification.OrderByDescending).ThenByDescending(specification.ThenOrderByDescending), 28 | { OrderByDescending: not null } => query.OrderByDescending(specification.OrderByDescending), 29 | _ => query 30 | }; 31 | 32 | 33 | if (specification.GroupBy != null) 34 | { 35 | query = query.GroupBy(specification.GroupBy).SelectMany(x => x); 36 | } 37 | 38 | if (specification.Skip is not (null or 0)) 39 | { 40 | query = query.Skip(specification.Skip.Value); 41 | } 42 | 43 | if (specification.Take is not (null or 0)) 44 | { 45 | query = query.Take(specification.Take.Value); 46 | } 47 | 48 | if (specification.SplitQuery) 49 | { 50 | query = query.AsSplitQuery(); 51 | } 52 | 53 | return specification.Tracking ? query.AsTracking() : query.AsNoTracking(); 54 | } 55 | } -------------------------------------------------------------------------------- /Sample/Sample.Infrastructure/Security/DependencyInjection/SecurityServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.IdentityModel.Tokens.Jwt; 2 | using System.Text; 3 | using EventManagement.Infrastructure.Jwt.Options; 4 | using Microsoft.AspNetCore.Authentication.JwtBearer; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.IdentityModel.Tokens; 9 | using Sample.Application.Core.Abstractions.Security; 10 | using Sample.Infrastructure.Security.Jwt; 11 | using Sample.Infrastructure.Security.Password; 12 | using Sample.Infrastructure.Security.Policies; 13 | using Sample.Infrastructure.Security.Policies.UserType; 14 | 15 | namespace Sample.Infrastructure.Security.DependencyInjection; 16 | 17 | public static class SecurityServiceCollectionExtensions 18 | { 19 | internal static IServiceCollection AddSecurity(this IServiceCollection services, IConfiguration configuration) 20 | { 21 | services.Configure(options => configuration.GetSection(JwtOptions.Jwt).Bind(options)); 22 | var jwtOptions = configuration.GetSection(JwtOptions.Jwt).Get()!; 23 | JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); 24 | 25 | return services.AddAuthentication(options => 26 | { 27 | options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 28 | options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; 29 | options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 30 | }).AddJwtBearer(options => 31 | { 32 | options.SaveToken = true; 33 | options.TokenValidationParameters = new TokenValidationParameters 34 | { 35 | ValidateIssuer = false, 36 | ValidateAudience = false, 37 | ValidateLifetime = true, 38 | ValidateIssuerSigningKey = true, 39 | RequireExpirationTime = true, 40 | IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.Key)) 41 | }; 42 | }) 43 | .Services 44 | .AddAuthorization(authorizationOptions => 45 | { 46 | authorizationOptions.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme) 47 | .RequireAuthenticatedUser() 48 | .Build(); 49 | }) 50 | .AddSingleton() 51 | .AddSingleton() 52 | .AddTransient() 53 | .AddTransient(); 54 | } 55 | } -------------------------------------------------------------------------------- /Sample/Sample.Domain/Entities/Event/Event.cs: -------------------------------------------------------------------------------- 1 | 2 | using Neptunee.EntityFrameworkCore.MultiLanguage.Types; 3 | using Neptunee.OperationResponse; 4 | 5 | namespace Sample.Domain.Entities; 6 | 7 | public class Event : Entity 8 | { 9 | private Event() 10 | { 11 | } 12 | 13 | public Event(string name, string description, string location, DateTime startDate, DateTime endDate, int availableTickets, Guid creatorId,string defaultLanguageKey) 14 | { 15 | Name = new(defaultLanguageKey,name); 16 | Description = new(defaultLanguageKey,description); 17 | Location = new(defaultLanguageKey,location); 18 | StartDate = startDate; 19 | EndDate = endDate; 20 | AvailableTickets = availableTickets; 21 | EventManagerId = creatorId; 22 | ConcurrencyStamp = Guid.NewGuid(); 23 | } 24 | 25 | public MultiLanguageProperty Name { get; private set; } 26 | public MultiLanguageProperty Description { get; private set; } 27 | public MultiLanguageProperty Location { get; private set; } 28 | public DateTime StartDate { get; private set; } 29 | public DateTime EndDate { get; private set; } 30 | public int AvailableTickets { get; private set; } 31 | 32 | public Guid EventManagerId { get; private set; } 33 | public EventManager EventManager { get; set; } 34 | 35 | public Guid ConcurrencyStamp { get; private set; } 36 | 37 | private readonly List _tickets = new(); 38 | public IReadOnlyCollection Tickets => _tickets.AsReadOnly(); 39 | 40 | public Result Modify(string name, string description, string location, DateTime startDate, DateTime endDate, int availableTickets, string languageKey) 41 | { 42 | if (_tickets.Count > availableTickets) 43 | { 44 | return Result.BadRequest(Errors.Events.AvailableTickets(_tickets.Count)); 45 | } 46 | 47 | if (AvailableTickets != availableTickets) 48 | { 49 | ConcurrencyStamp = Guid.NewGuid(); 50 | } 51 | 52 | Name.Upsert(languageKey, name); 53 | Description.Upsert(languageKey, description); 54 | Location.Upsert(languageKey, location); 55 | StartDate = startDate; 56 | EndDate = endDate; 57 | AvailableTickets = availableTickets; 58 | return Result.Ok(); 59 | } 60 | 61 | public Result Book(Guid participationUserId) 62 | { 63 | if (_tickets.Any(t => t.ParticipationUserId == participationUserId)) 64 | { 65 | return Result.BadRequest(Errors.Tickets.AlreadyBooked); 66 | } 67 | 68 | if (_tickets.Count == AvailableTickets) 69 | { 70 | return Result.BadRequest(Errors.Tickets.FullBooking,"Sorry"); 71 | } 72 | 73 | ConcurrencyStamp = Guid.NewGuid(); 74 | _tickets.Add(new Ticket(Id, participationUserId)); 75 | return Result.Ok(); 76 | } 77 | } -------------------------------------------------------------------------------- /Neptunee.Kernel/Extensions/NeptuneeQueryableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using Neptunee.Entities; 3 | 4 | namespace System.Linq; 5 | 6 | public static class NeptuneeQueryableExtensions 7 | { 8 | public static IQueryable WhereIf( 9 | this IQueryable query, 10 | bool condition, 11 | Expression> predicate) => 12 | condition ? query.Where(predicate) : query; 13 | 14 | public static IQueryable OrderByDescendingIf( 15 | this IQueryable query, 16 | bool condition, 17 | Expression> keySelector) => 18 | condition ? query.OrderByDescending(keySelector) : query; 19 | 20 | public static IQueryable OrderByIf( 21 | this IQueryable query, 22 | bool condition, 23 | Expression> keySelector, 24 | OrderByOption orderByOption = OrderByOption.Increasing) => 25 | condition ? query.OrderBy(keySelector, orderByOption) : query; 26 | 27 | public static IQueryable Include(this IQueryable queryable, params string[] includeProps) => 28 | includeProps.Aggregate(queryable, (query, includeProp) => query.Include(includeProp)); 29 | 30 | public static IQueryable Where(this IQueryable query, TKey id) where TEntity : class, INeptuneeEntity where TKey : struct, IEquatable 31 | => query.Where(entity => entity.Id.Equals(id)); 32 | 33 | public static IQueryable Where(this IQueryable query, IEnumerable ids) where TEntity : class, INeptuneeEntity where TKey : struct, IEquatable 34 | => query.Where(entity => ids.Contains(entity.Id)); 35 | 36 | public static IQueryable FilterIgnoreSoftDeleted(this IQueryable query) where TEntity : class, INeptuneeDeletableEntity 37 | => query.Where(entity => !entity.UtcDateDeleted.HasValue); 38 | 39 | public static IQueryable Paging(this IQueryable query, int pageIndex, int size) 40 | { 41 | if (pageIndex <= 0) 42 | { 43 | throw new ArgumentOutOfRangeException(nameof(pageIndex), "must greater than 0"); 44 | } 45 | 46 | return query.Skip((pageIndex - 1) * size).Take(size); 47 | } 48 | 49 | public static IQueryable OrderBy(this IQueryable query, 50 | Expression> keySelector, 51 | OrderByOption orderByOption = OrderByOption.Increasing) => 52 | orderByOption switch 53 | { 54 | OrderByOption.Increasing => query.OrderBy(keySelector), 55 | OrderByOption.Descending => query.OrderByDescending(keySelector), 56 | _ => throw new ArgumentOutOfRangeException(nameof(orderByOption), orderByOption, null) 57 | }; 58 | } 59 | 60 | public enum OrderByOption 61 | { 62 | Increasing, 63 | Descending 64 | } -------------------------------------------------------------------------------- /Sample/Sample.API/Controllers/EventsController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Neptunee.Handlers.Requests; 3 | using Neptunee.OperationResponse; 4 | using Sample.API.Core.ActionsFilters; 5 | using Sample.Application.Events.Commands; 6 | using Sample.Application.Events.Queries; 7 | using Sample.Application.Events.Queries.Responses; 8 | using Sample.Infrastructure.Security.Policies.UserType; 9 | using Sample.SharedKernel; 10 | using Swashbuckle.AspNetCore.Annotations; 11 | 12 | namespace Sample.API.Controllers; 13 | 14 | [ApiController] 15 | [Route("api/[controller]/[action]")] 16 | public class EventsController : ControllerBase 17 | { 18 | [HttpGet] 19 | [HasUserTypes(ConstValues.UserTypes.EventManager)] 20 | [SwaggerResponse(StatusCodes.Status200OK, type: typeof(List))] 21 | public async Task GetAll( 22 | [FromServices] INeptuneeRequestHandler>> handler, 23 | [FromQuery] GetAllEventQuery query, 24 | CancellationToken ct) 25 | => await handler.HandleAsync(query,ct).ToIActionResultAsync(); 26 | 27 | [HttpGet] 28 | [HasUserTypes(ConstValues.UserTypes.ParticipationUser)] 29 | [SwaggerResponse(StatusCodes.Status200OK, type: typeof(List))] 30 | public async Task GetAvailable( 31 | [FromServices] INeptuneeRequestHandler>> handler, 32 | [FromQuery] GetAllAvailableEventQuery query, 33 | CancellationToken ct) 34 | => await handler.HandleAsync(query, ct).ToIActionResultAsync(); 35 | 36 | [HttpPost] 37 | [HasUserTypes(ConstValues.UserTypes.EventManager)] 38 | [SwaggerResponse(StatusCodes.Status200OK, type: typeof(GetAllEventResponse))] 39 | [TypeFilter(typeof(ForceDefaultLanguage))] 40 | public async Task Add( 41 | [FromServices] INeptuneeRequestHandler> handler, 42 | [FromBody] AddEventCommand command, 43 | CancellationToken ct) 44 | => await handler.HandleAsync(command, ct).ToIActionResultAsync(); 45 | 46 | [HttpPost] 47 | [HasUserTypes(ConstValues.UserTypes.EventManager)] 48 | [SwaggerResponse(StatusCodes.Status200OK, type: typeof(GetAllEventResponse))] 49 | public async Task Update( 50 | [FromServices] INeptuneeRequestHandler> handler, 51 | [FromBody] UpdateEventCommand command, 52 | CancellationToken ct) 53 | => await handler.HandleAsync(command, ct).ToIActionResultAsync(); 54 | 55 | // [HttpDelete] 56 | // [HasUserTypes(ConstValues.UserTypes.EventManager)] 57 | // public async Task Delete( 58 | // [FromServices] INeptuneeRequestHandler> handler, 59 | // [FromBody] DeleteEventCommand command) 60 | // => await handler.HandleAsync(command).ToIActionResultAsync(); 61 | } -------------------------------------------------------------------------------- /Sample/Sample.Application/Core/Shared/Handlers/BaseSecurityHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Security.Claims; 3 | using Microsoft.EntityFrameworkCore; 4 | using Neptunee.Entities; 5 | using Neptunee.Handlers.Requests; 6 | using Neptunee.OperationResponse; 7 | using Sample.Application.Core.Abstractions.Security; 8 | using Sample.Application.Core.Shared.QueryObjects; 9 | using Sample.Domain; 10 | using Sample.Domain.Entities; 11 | using Sample.Domain.Repositories; 12 | using Sample.SharedKernel; 13 | 14 | namespace Sample.Application.Core.Shared.Handlers; 15 | 16 | internal abstract class BaseSecurityHandler : INeptuneeRequestHandler where TRequest : class, INeptuneeRequest 17 | { 18 | protected readonly IPasswordHasher PasswordHasher; 19 | protected readonly IRepository Repository; 20 | protected readonly IJwtBearerGenerator JwtBearerGenerator; 21 | 22 | public BaseSecurityHandler(IPasswordHasher passwordHasher, IRepository repository, IJwtBearerGenerator jwtBearerGenerator) 23 | { 24 | PasswordHasher = passwordHasher; 25 | Repository = repository; 26 | JwtBearerGenerator = jwtBearerGenerator; 27 | } 28 | 29 | public abstract Task HandleAsync(TRequest request, CancellationToken cancellationToken = default); 30 | 31 | protected async Task> Login(string email, string password, CancellationToken cancellationToken) where TUser : class, IEntityHasEmail, IEntityHasPassword, INeptuneeEntity 32 | { 33 | var user = await Repository 34 | .Query() 35 | .FindBy(email) 36 | .FirstOrDefaultAsync(cancellationToken: cancellationToken); 37 | 38 | if (user is null) 39 | { 40 | return Result.With(HttpStatusCode.NotFound, Errors.Users.EmailNotFound).To(); 41 | } 42 | 43 | if (!PasswordHasher.VerifyHashedPassword(user.PasswordHash, password)) 44 | { 45 | return Result.With(HttpStatusCode.NotFound, Errors.Users.WrongEmailOrPassword).To(); 46 | } 47 | 48 | 49 | return Success(user); 50 | } 51 | 52 | protected async Task> Register(string email, Func userFunc, CancellationToken cancellationToken) where TUser : class, IEntityHasEmail, IEntityHasPassword, INeptuneeEntity 53 | { 54 | if (await Repository.Query().FindBy(email).AnyAsync(cancellationToken: cancellationToken)) 55 | { 56 | return Result.With(HttpStatusCode.BadRequest, Errors.Users.EmailAlreadyUsed).To(); 57 | } 58 | 59 | var user = userFunc(); 60 | Repository.Add(user); 61 | await Repository.SaveChangesAsync(cancellationToken); 62 | return Success(user); 63 | } 64 | 65 | private string Success(TUser user) where TUser : INeptuneeEntity 66 | => JwtBearerGenerator.Generate(new List() 67 | { 68 | new(ClaimTypes.NameIdentifier, user.Id.ToString()), 69 | new(ConstValues.ClaimTypes.UserType, typeof(TUser).Name), 70 | // other claim like expire, email ...etc 71 | }); 72 | } -------------------------------------------------------------------------------- /Sample/Sample.Application/Events/Commands/UpdateEventCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using Microsoft.EntityFrameworkCore; 3 | using Neptunee.Handlers.Requests; 4 | using Neptunee.OperationResponse; 5 | using Sample.Application.Core.Abstractions; 6 | using Sample.Application.Events.Queries.QueryObjects; 7 | using Sample.Application.Events.Queries.Responses; 8 | using Sample.Domain; 9 | using Sample.Domain.Entities; 10 | using Sample.Domain.Repositories; 11 | 12 | namespace Sample.Application.Events.Commands; 13 | 14 | public record UpdateEventCommand( 15 | Guid Id, 16 | string Name, 17 | string Description, 18 | string Location, 19 | DateTime StartDate, 20 | DateTime EndDate, 21 | int AvailableTickets) : INeptuneeRequest>; 22 | 23 | internal class UpdateEventCommandHandler : INeptuneeRequestHandler> 24 | { 25 | private readonly IRepository _repository; 26 | private readonly IHttpResolver _httpResolver; 27 | 28 | public UpdateEventCommandHandler(IRepository repository, IHttpResolver httpResolver) 29 | { 30 | _repository = repository; 31 | _httpResolver = httpResolver; 32 | } 33 | 34 | public async Task> HandleAsync(UpdateEventCommand command, CancellationToken cancellationToken = default) 35 | { 36 | var operation = Operation.Unknown(); 37 | 38 | operation.OrFailureIf(command.StartDate < DateTime.Now, Errors.Events.StartDateCannotBeInPast); 39 | operation.OrFailureIf(command.EndDate < command.StartDate, Errors.Events.EndDateCannotBeBeforeStatDate); 40 | // other validations checks .. 41 | 42 | if (operation.IsFailure) 43 | { 44 | return operation; 45 | } 46 | 47 | var eventEntity = await _repository.TrackingQuery().Where(e => e.Id == command.Id).Include(e => e.Tickets).FirstOrDefaultAsync(cancellationToken: cancellationToken); 48 | if (eventEntity is null) 49 | { 50 | return operation.SetStatusCode(HttpStatusCode.NotFound); 51 | } 52 | 53 | if (eventEntity.EventManagerId != _httpResolver.CurrentUserId()) 54 | { 55 | return operation.SetStatusCode(HttpStatusCode.Forbidden); 56 | } 57 | 58 | var result = eventEntity.Modify(command.Name, 59 | command.Description, 60 | command.Location, 61 | command.StartDate, 62 | command.EndDate, 63 | command.AvailableTickets, 64 | _httpResolver.LanguageKey()); 65 | if (result.IsFailure) 66 | { 67 | return result; 68 | } 69 | 70 | // await Task.Delay(10000); to testing Concurrency 71 | try 72 | { 73 | await _repository.SaveChangesAsync(cancellationToken); 74 | operation.SetResponse(await _repository.Query().Where(eventEntity.Id).GetAllEventResponse(_httpResolver.LanguageKey()).FirstAsync(cancellationToken: cancellationToken)); 75 | } 76 | catch (DbUpdateConcurrencyException) 77 | { 78 | operation.Error(Errors.Events.ChangesHappened); 79 | } 80 | 81 | return operation; 82 | } 83 | } -------------------------------------------------------------------------------- /Neptunee.EntityFrameworkCore/Specification/NeptuneeSpecification.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using Neptunee.Entities; 3 | 4 | namespace Neptunee.EntityFrameworkCore.Specification; 5 | 6 | public abstract class NeptuneeSpecification : INeptuneeSpecification where TEntity : class, INeptuneeEntity 7 | { 8 | protected NeptuneeSpecification(Expression> filter) : this() 9 | { 10 | Filters.Add(filter); 11 | } 12 | 13 | protected NeptuneeSpecification() 14 | { 15 | Filters = new(); 16 | } 17 | 18 | public List>> Filters { get; private set; } 19 | public Expression>? OrderByDescending { get; private set; } 20 | public Expression>? OrderBy { get; private set; } 21 | public Expression>? ThenOrderByDescending { get; private set; } 22 | public Expression>? ThenOrderBy { get; private set; } 23 | public Expression>? GroupBy { get; private set; } 24 | 25 | public List>> Includes { get; } = new(); 26 | public List IncludeStrings { get; } = new(); 27 | public int? Take { get; private set; } 28 | public int? Skip { get; private set; } 29 | public bool Tracking { get; private set; } = false; 30 | public bool SplitQuery { get; private set; } = false; 31 | 32 | protected virtual void AddFilter(Expression> filterExpression) 33 | { 34 | Filters.Add(filterExpression); 35 | } 36 | 37 | protected virtual void AddFilterIf(bool condition, Expression> filterExpression) 38 | { 39 | if (condition) 40 | { 41 | AddFilter(filterExpression); 42 | } 43 | } 44 | 45 | protected virtual void AddInclude(Expression> includeExpression) 46 | { 47 | Includes.Add(includeExpression); 48 | } 49 | 50 | protected virtual void AddInclude(string includeString) 51 | { 52 | IncludeStrings.Add(includeString); 53 | } 54 | 55 | protected virtual void ApplyPaging(int pageIndex, int size) 56 | { 57 | if (pageIndex <= 0) 58 | { 59 | throw new ArgumentOutOfRangeException(nameof(pageIndex), "must greater than 0"); 60 | } 61 | 62 | Skip = (pageIndex - 1) * size; 63 | Take = size; 64 | } 65 | 66 | protected virtual void ApplyTake(int take) 67 | { 68 | Take = take; 69 | } 70 | 71 | protected virtual void ApplySkip(int skip) 72 | { 73 | Skip = skip; 74 | } 75 | 76 | protected virtual void ApplyOrderBy(Expression> orderByExpression) 77 | { 78 | OrderBy = orderByExpression; 79 | } 80 | 81 | protected virtual void ApplyOrderByDescending(Expression> orderByDescendingExpression) 82 | { 83 | OrderByDescending = orderByDescendingExpression; 84 | } 85 | 86 | protected virtual void ApplyThenOrderBy(Expression> orderByExpression) 87 | { 88 | ThenOrderBy = orderByExpression; 89 | } 90 | 91 | protected virtual void ApplyThenOrderByDescending(Expression> orderByDescendingExpression) 92 | { 93 | ThenOrderByDescending = orderByDescendingExpression; 94 | } 95 | 96 | protected virtual void ApplyGroupBy(Expression> groupByExpression) 97 | { 98 | GroupBy = groupByExpression; 99 | } 100 | 101 | protected virtual void AsTracking() 102 | { 103 | Tracking = true; 104 | } 105 | 106 | protected virtual void AsSplitQuery() 107 | { 108 | SplitQuery = true; 109 | } 110 | } -------------------------------------------------------------------------------- /Neptunee.EntityFrameworkCore/Extensions/NeptuneeAppBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Neptunee.EntityFrameworkCore.Extensions; 7 | 8 | public static class NeptuneeAppBuilderExtensions 9 | { 10 | public static async Task MigrationAsync(this IApplicationBuilder builder) where TContext : DbContext 11 | { 12 | var serviceScopeFactory = builder.ServiceScopeFactory(); 13 | using var serviceScope = serviceScopeFactory.CreateScope(); 14 | var logger = serviceScope.ServiceProvider.GetRequiredService>(); 15 | var context = serviceScope.ServiceProvider.GetRequiredService(); 16 | 17 | await context.Database.MigrateAsync(); 18 | logger.LogInformation("Migrate is done"); 19 | 20 | 21 | return builder; 22 | } 23 | 24 | public static async Task MigrationAsync(this IApplicationBuilder builder, 25 | Func seed) where TContext : DbContext 26 | { 27 | var serviceScopeFactory = builder.ServiceScopeFactory(); 28 | using var serviceScope = serviceScopeFactory.CreateScope(); 29 | var logger = serviceScope.ServiceProvider.GetRequiredService>(); 30 | var context = serviceScope.ServiceProvider.GetRequiredService(); 31 | 32 | await context.Database.MigrateAsync(); 33 | logger.LogInformation("Migrate is done"); 34 | 35 | await seed(context, serviceScope.ServiceProvider); 36 | logger.LogInformation("Seed is done"); 37 | 38 | 39 | return builder; 40 | } 41 | 42 | public static async Task MigrationAsync(this IApplicationBuilder builder, 43 | Action seed) where TContext : DbContext 44 | { 45 | var serviceScopeFactory = builder.ServiceScopeFactory(); 46 | using var serviceScope = serviceScopeFactory.CreateScope(); 47 | var logger = serviceScope.ServiceProvider.GetRequiredService>(); 48 | var context = serviceScope.ServiceProvider.GetRequiredService(); 49 | 50 | await context.Database.MigrateAsync(); 51 | logger.LogInformation("Migrate is done"); 52 | 53 | seed(context, serviceScope.ServiceProvider); 54 | logger.LogInformation("Seed is done"); 55 | 56 | return builder; 57 | } 58 | 59 | public static async Task SeedAsync(this IApplicationBuilder builder, 60 | Func seed) where TContext : DbContext 61 | { 62 | var serviceScopeFactory = builder.ServiceScopeFactory(); 63 | using var serviceScope = serviceScopeFactory.CreateScope(); 64 | var logger = serviceScope.ServiceProvider.GetRequiredService>(); 65 | var context = serviceScope.ServiceProvider.GetRequiredService(); 66 | 67 | await seed(context, serviceScope.ServiceProvider); 68 | logger.LogInformation("Seed is done"); 69 | 70 | return builder; 71 | } 72 | 73 | public static IApplicationBuilder Seed(this IApplicationBuilder builder, 74 | Action seed) where TContext : DbContext 75 | { 76 | var serviceScopeFactory = builder.ServiceScopeFactory(); 77 | using var serviceScope = serviceScopeFactory.CreateScope(); 78 | var logger = serviceScope.ServiceProvider.GetRequiredService>(); 79 | var context = serviceScope.ServiceProvider.GetRequiredService(); 80 | 81 | seed(context, serviceScope.ServiceProvider); 82 | logger.LogInformation("Seed is done"); 83 | 84 | return builder; 85 | } 86 | 87 | 88 | private static IServiceScopeFactory ServiceScopeFactory(this IApplicationBuilder builder) => 89 | builder 90 | .ApplicationServices 91 | .GetRequiredService(); 92 | } -------------------------------------------------------------------------------- /Sample/Sample.Infrastructure/Security/Password/PasswordHasher.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | using Microsoft.AspNetCore.Cryptography.KeyDerivation; 3 | using Sample.Application.Core.Abstractions.Security; 4 | 5 | namespace Sample.Infrastructure.Security.Password; 6 | 7 | public class PasswordHasher : IPasswordHasher 8 | { 9 | const int Iterations = 100_000; 10 | const int SaltSize = 128 / 8; 11 | 12 | private static readonly RandomNumberGenerator Rng = RandomNumberGenerator.Create(); 13 | 14 | public string HashPassword(string password) 15 | { 16 | var hash = HashPassword(password, 17 | Rng, 18 | prf: KeyDerivationPrf.HMACSHA512, 19 | iterCount: Iterations, 20 | saltSize: SaltSize, 21 | numBytesRequested: 256 / 8); 22 | 23 | return Convert.ToBase64String(hash); 24 | } 25 | 26 | public bool VerifyHashedPassword(string hashedPassword, string providedPassword) 27 | { 28 | var decodedHashedPassword = Convert.FromBase64String(hashedPassword); 29 | return VerifyHashedPasswordV3(decodedHashedPassword, providedPassword); 30 | } 31 | 32 | private static byte[] HashPassword(string password, RandomNumberGenerator rng, KeyDerivationPrf prf, int iterCount, int saltSize, int numBytesRequested) 33 | { 34 | var salt = new byte[saltSize]; 35 | rng.GetBytes(salt); 36 | var subKey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, numBytesRequested); 37 | 38 | var output = new byte[13 + salt.Length + subKey.Length]; 39 | WriteNetworkByteOrder(output, 1, (uint)prf); 40 | WriteNetworkByteOrder(output, 5, (uint)iterCount); 41 | WriteNetworkByteOrder(output, 9, (uint)saltSize); 42 | Buffer.BlockCopy(salt, 0, output, 13, salt.Length); 43 | Buffer.BlockCopy(subKey, 0, output, 13 + saltSize, subKey.Length); 44 | return output; 45 | } 46 | 47 | 48 | private static bool VerifyHashedPasswordV3(byte[] hashedPassword, string password) 49 | { 50 | try 51 | { 52 | // Read header information 53 | var prf = (KeyDerivationPrf)ReadNetworkByteOrder(hashedPassword, 1); 54 | var iterCount = (int)ReadNetworkByteOrder(hashedPassword, 5); 55 | var saltLength = (int)ReadNetworkByteOrder(hashedPassword, 9); 56 | 57 | // Read the salt: must be >= 128 bits 58 | if (saltLength < 128 / 8) 59 | { 60 | return false; 61 | } 62 | 63 | var salt = new byte[saltLength]; 64 | Buffer.BlockCopy(hashedPassword, 13, salt, 0, salt.Length); 65 | 66 | // Read the subKey (the rest of the payload): must be >= 128 bits 67 | var subKeyLength = hashedPassword.Length - 13 - salt.Length; 68 | if (subKeyLength < 128 / 8) 69 | { 70 | return false; 71 | } 72 | 73 | var expectedSubKey = new byte[subKeyLength]; 74 | Buffer.BlockCopy(hashedPassword, 13 + salt.Length, expectedSubKey, 0, expectedSubKey.Length); 75 | 76 | // Hash the incoming password and verify it 77 | var actualSubKey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, subKeyLength); 78 | 79 | return CryptographicOperations.FixedTimeEquals(actualSubKey, expectedSubKey); 80 | } 81 | catch 82 | { 83 | // This should never occur except in the case of a malformed payload, where 84 | // we might go off the end of the array. Regardless, a malformed payload 85 | // implies verification failed. 86 | return false; 87 | } 88 | } 89 | 90 | private static void WriteNetworkByteOrder(byte[] buffer, int offset, uint value) 91 | { 92 | buffer[offset + 0] = (byte)(value >> 24); 93 | buffer[offset + 1] = (byte)(value >> 16); 94 | buffer[offset + 2] = (byte)(value >> 8); 95 | buffer[offset + 3] = (byte)(value >> 0); 96 | } 97 | 98 | 99 | 100 | private static uint ReadNetworkByteOrder(byte[] buffer, int offset) 101 | { 102 | return ((uint)buffer[offset + 0] << 24) 103 | | ((uint)buffer[offset + 1] << 16) 104 | | ((uint)buffer[offset + 2] << 8) 105 | | buffer[offset + 3]; 106 | } 107 | } -------------------------------------------------------------------------------- /Neptunee/IO/Files/NeptuneeFileService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.Extensions.Options; 3 | using Neptunee.IO.Files.Options; 4 | 5 | namespace Neptunee.IO.Files; 6 | 7 | public class NeptuneeFileService : INeptuneeFileService 8 | { 9 | private readonly NeptuneeFileOptions _options; 10 | 11 | public NeptuneeFileService(IOptions options) 12 | { 13 | _options = options.Value; 14 | } 15 | 16 | public virtual async Task UploadAsync(IFormFile? file, string uploadPath) 17 | { 18 | if (file is null) 19 | { 20 | return null; 21 | } 22 | 23 | await PhysicalUpload(file, uploadPath); 24 | return uploadPath; 25 | } 26 | 27 | public virtual async Task UploadAsync(IFormFile? file, Func uploadPath) 28 | { 29 | if (file is null) 30 | { 31 | return null; 32 | } 33 | 34 | return await UploadAsync(file, uploadPath(file)); 35 | } 36 | 37 | public virtual async Task UploadAsync(IFormFile? file) 38 | { 39 | if (file is null) 40 | { 41 | return null; 42 | } 43 | 44 | var path = Path.Combine(_options.DirUploadPathProvider(_options.BasePath, file), _options.FileNameProvider(file)); 45 | await PhysicalUpload(file, path); 46 | return path; 47 | } 48 | 49 | public virtual async Task> UploadAsync(List files, string dirPath, Func fileName) 50 | { 51 | var list = new List(); 52 | foreach (var file in files) 53 | { 54 | list.Add(await UploadAsync(file, Path.Combine(dirPath, fileName(file)))); 55 | } 56 | 57 | return list!; 58 | } 59 | 60 | public virtual async Task> UploadAsync(List files) 61 | { 62 | return await UploadAsync(files, _options.DirUploadPathProvider(_options.BasePath, null!), _options.FileNameProvider); 63 | } 64 | 65 | public virtual async Task ModifyAsync(string? original, bool forceDelete, Func> upload) 66 | { 67 | var newPath = await upload(); 68 | 69 | if (forceDelete || newPath is not null) 70 | { 71 | Delete(original); 72 | original = null; 73 | } 74 | 75 | return newPath ?? original; 76 | } 77 | 78 | public virtual async Task ModifyAsync(List original, IEnumerable deletes, Task> uploads) 79 | { 80 | foreach (var deletePath in deletes) 81 | { 82 | Delete(deletePath); 83 | original.Remove(deletePath); 84 | } 85 | 86 | original.AddRange(await uploads); 87 | } 88 | 89 | 90 | public virtual void Delete(string? path) 91 | { 92 | if (path == null) 93 | { 94 | return; 95 | } 96 | 97 | PhysicalDelete(path); 98 | } 99 | 100 | public virtual void Delete(List path) 101 | { 102 | foreach (var p in path) 103 | { 104 | Delete(p); 105 | } 106 | } 107 | 108 | public virtual string CreateDirectoryIfNotExists(string path) 109 | { 110 | var phyPath = Path.Combine(_options.BasePath, path); 111 | if (Directory.Exists(phyPath)) 112 | { 113 | return path; 114 | } 115 | 116 | Directory.CreateDirectory(phyPath); 117 | return path; 118 | } 119 | 120 | public virtual void DeleteDirectoryIfExists(string path) 121 | { 122 | var phyPath = Path.Combine(_options.BasePath, path); 123 | if (Directory.Exists(phyPath)) 124 | { 125 | Directory.Delete(phyPath); 126 | } 127 | } 128 | 129 | protected virtual async Task PhysicalUpload(IFormFile file, string path) 130 | { 131 | var fs = new FileStream(Path.Combine(_options.BasePath, path), FileMode.Create); 132 | await file.CopyToAsync(fs); 133 | await fs.DisposeAsync(); 134 | fs.Close(); 135 | } 136 | 137 | protected virtual void PhysicalDelete(string path) 138 | { 139 | var phyPath = Path.Combine(_options.BasePath, path); 140 | if (File.Exists(phyPath)) 141 | { 142 | File.Delete(phyPath); 143 | } 144 | } 145 | } -------------------------------------------------------------------------------- /Neptunee/IO/Files/INeptuneeFileService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Neptunee.IO.Files.Options; 3 | 4 | namespace Neptunee.IO.Files; 5 | 6 | ///

7 | /// Represents a file service for uploading, modifying, and deleting files. 8 | /// 9 | public interface INeptuneeFileService 10 | { 11 | /// 12 | /// Uploads a file asynchronously to the specified virtual upload path if the file is not null. 13 | /// 14 | /// The file to upload. 15 | /// The virtual path where the file should be uploaded. 16 | /// The virtual path if the file is uploaded; otherwise, null. 17 | Task UploadAsync(IFormFile? file, string uploadPath); 18 | 19 | /// 20 | /// Uploads a file asynchronously to the specified virtual upload path generated by the provided function if the file is not null. 21 | /// 22 | /// The file to upload. 23 | /// A function that generates the upload path for the file. 24 | /// The virtual path if the file is uploaded; otherwise, null. 25 | Task UploadAsync(IFormFile? file, Func uploadPath); 26 | 27 | /// 28 | /// Uploads a file asynchronously to the specified virtual upload path generated by the provided functions in if the file is not null. 29 | /// 30 | /// The file to upload. 31 | /// The virtual path if the file is uploaded; otherwise, null. 32 | Task UploadAsync(IFormFile? file); 33 | 34 | /// 35 | /// Uploads a list of files asynchronously to the specified virtual directory path with custom file names. 36 | /// 37 | /// The list of files to upload. 38 | /// The virtual directory path where the files should be uploaded. 39 | /// A function that generates the file name for each file. 40 | /// A list of virtual paths for the uploaded files. 41 | Task> UploadAsync(List files, string dirPath, Func fileName); 42 | 43 | /// 44 | /// Modifies a file by uploading a new file if not null and deleting the original file if necessary. 45 | /// 46 | /// The virtual original file path. 47 | /// Force delete original file even the is null. 48 | /// The new virtual path if the file is uploaded; otherwise, null. 49 | /// The virtual path if the new file is uploaded or original doesn't deleted; otherwise, null. 50 | Task ModifyAsync(string? original, bool forceDelete, Func> upload); 51 | 52 | /// 53 | /// Modifies a file by uploading a new files and deleting the original files if necessary. 54 | /// 55 | /// The list of virtual original file paths. 56 | /// The list of virtual file paths to delete. 57 | /// A list of virtual upload paths for the new files. 58 | /// A list of virtual paths for the modified files. 59 | Task ModifyAsync(List original, IEnumerable deletes, Task> uploads); 60 | 61 | /// 62 | /// Deletes a file from the specified virtual path. 63 | /// 64 | /// The virtual path of the file to delete. 65 | void Delete(string? path); 66 | 67 | /// 68 | /// Deletes multiple files from the specified virtual paths. 69 | /// 70 | /// The list of virtual file paths to delete. 71 | void Delete(List paths); 72 | 73 | /// 74 | /// Creates a directory if it does not already exist. 75 | /// 76 | /// The virtual directory path to create. 77 | /// The virtual created directory path. 78 | string CreateDirectoryIfNotExists(string path); 79 | 80 | /// 81 | /// Deletes a directory if it exists. 82 | /// 83 | /// The virtual path of the directory to delete. 84 | void DeleteDirectoryIfExists(string path); 85 | } -------------------------------------------------------------------------------- /Neptunee.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neptunee", "Neptunee\Neptunee.csproj", "{5EED45E6-E121-4E7E-BBE7-73561A72B31C}" 4 | EndProject 5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sample", "Sample", "{8C76003B-F315-487A-92DD-AF5C7D47355A}" 6 | EndProject 7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.Domain", "Sample\Sample.Domain\Sample.Domain.csproj", "{831F23B0-85B1-462C-BC6E-2FDFF556F356}" 8 | EndProject 9 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.Application", "Sample\Sample.Application\Sample.Application.csproj", "{E86865DB-7EDC-4F6C-AB6A-B1850398974C}" 10 | EndProject 11 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.Infrastructure", "Sample\Sample.Infrastructure\Sample.Infrastructure.csproj", "{0B730179-2353-4EDB-BCD3-703FDBA457E0}" 12 | EndProject 13 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.API", "Sample\Sample.API\Sample.API.csproj", "{0F00E1DB-904E-4EA1-9546-23CC7275965B}" 14 | EndProject 15 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{94A801AE-8B1D-4CDA-8E80-2236FD98B33A}" 16 | ProjectSection(SolutionItems) = preProject 17 | README.md = README.md 18 | .gitignore = .gitignore 19 | EndProjectSection 20 | EndProject 21 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neptunee.Kernel", "Neptunee.Kernel\Neptunee.Kernel.csproj", "{0D1AE2D2-CD9A-4624-8724-6F5D743BD1ED}" 22 | EndProject 23 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neptunee.EntityFrameworkCore", "Neptunee.EntityFrameworkCore\Neptunee.EntityFrameworkCore.csproj", "{D30A46AE-20B1-4F30-B877-7D4A5F4F8AB3}" 24 | EndProject 25 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neptunee.Handlers", "Neptunee.Handlers\Neptunee.Handlers.csproj", "{777B51F9-3341-4206-AE47-EC936FE45601}" 26 | EndProject 27 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.SharedKernel", "Sample\Sample.SharedKernel\Sample.SharedKernel.csproj", "{E639214F-BE0C-4642-AB29-D05BC97EF643}" 28 | EndProject 29 | Global 30 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 31 | Debug|Any CPU = Debug|Any CPU 32 | Release|Any CPU = Release|Any CPU 33 | EndGlobalSection 34 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 35 | {5EED45E6-E121-4E7E-BBE7-73561A72B31C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {5EED45E6-E121-4E7E-BBE7-73561A72B31C}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {5EED45E6-E121-4E7E-BBE7-73561A72B31C}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {5EED45E6-E121-4E7E-BBE7-73561A72B31C}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {831F23B0-85B1-462C-BC6E-2FDFF556F356}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {831F23B0-85B1-462C-BC6E-2FDFF556F356}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {831F23B0-85B1-462C-BC6E-2FDFF556F356}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {831F23B0-85B1-462C-BC6E-2FDFF556F356}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {E86865DB-7EDC-4F6C-AB6A-B1850398974C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 44 | {E86865DB-7EDC-4F6C-AB6A-B1850398974C}.Debug|Any CPU.Build.0 = Debug|Any CPU 45 | {E86865DB-7EDC-4F6C-AB6A-B1850398974C}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {E86865DB-7EDC-4F6C-AB6A-B1850398974C}.Release|Any CPU.Build.0 = Release|Any CPU 47 | {0B730179-2353-4EDB-BCD3-703FDBA457E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 48 | {0B730179-2353-4EDB-BCD3-703FDBA457E0}.Debug|Any CPU.Build.0 = Debug|Any CPU 49 | {0B730179-2353-4EDB-BCD3-703FDBA457E0}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {0B730179-2353-4EDB-BCD3-703FDBA457E0}.Release|Any CPU.Build.0 = Release|Any CPU 51 | {0F00E1DB-904E-4EA1-9546-23CC7275965B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 52 | {0F00E1DB-904E-4EA1-9546-23CC7275965B}.Debug|Any CPU.Build.0 = Debug|Any CPU 53 | {0F00E1DB-904E-4EA1-9546-23CC7275965B}.Release|Any CPU.ActiveCfg = Release|Any CPU 54 | {0F00E1DB-904E-4EA1-9546-23CC7275965B}.Release|Any CPU.Build.0 = Release|Any CPU 55 | {0D1AE2D2-CD9A-4624-8724-6F5D743BD1ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 56 | {0D1AE2D2-CD9A-4624-8724-6F5D743BD1ED}.Debug|Any CPU.Build.0 = Debug|Any CPU 57 | {0D1AE2D2-CD9A-4624-8724-6F5D743BD1ED}.Release|Any CPU.ActiveCfg = Release|Any CPU 58 | {0D1AE2D2-CD9A-4624-8724-6F5D743BD1ED}.Release|Any CPU.Build.0 = Release|Any CPU 59 | {D30A46AE-20B1-4F30-B877-7D4A5F4F8AB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 60 | {D30A46AE-20B1-4F30-B877-7D4A5F4F8AB3}.Debug|Any CPU.Build.0 = Debug|Any CPU 61 | {D30A46AE-20B1-4F30-B877-7D4A5F4F8AB3}.Release|Any CPU.ActiveCfg = Release|Any CPU 62 | {D30A46AE-20B1-4F30-B877-7D4A5F4F8AB3}.Release|Any CPU.Build.0 = Release|Any CPU 63 | {777B51F9-3341-4206-AE47-EC936FE45601}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 64 | {777B51F9-3341-4206-AE47-EC936FE45601}.Debug|Any CPU.Build.0 = Debug|Any CPU 65 | {777B51F9-3341-4206-AE47-EC936FE45601}.Release|Any CPU.ActiveCfg = Release|Any CPU 66 | {777B51F9-3341-4206-AE47-EC936FE45601}.Release|Any CPU.Build.0 = Release|Any CPU 67 | {E639214F-BE0C-4642-AB29-D05BC97EF643}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 68 | {E639214F-BE0C-4642-AB29-D05BC97EF643}.Debug|Any CPU.Build.0 = Debug|Any CPU 69 | {E639214F-BE0C-4642-AB29-D05BC97EF643}.Release|Any CPU.ActiveCfg = Release|Any CPU 70 | {E639214F-BE0C-4642-AB29-D05BC97EF643}.Release|Any CPU.Build.0 = Release|Any CPU 71 | EndGlobalSection 72 | GlobalSection(NestedProjects) = preSolution 73 | {831F23B0-85B1-462C-BC6E-2FDFF556F356} = {8C76003B-F315-487A-92DD-AF5C7D47355A} 74 | {E86865DB-7EDC-4F6C-AB6A-B1850398974C} = {8C76003B-F315-487A-92DD-AF5C7D47355A} 75 | {0B730179-2353-4EDB-BCD3-703FDBA457E0} = {8C76003B-F315-487A-92DD-AF5C7D47355A} 76 | {0F00E1DB-904E-4EA1-9546-23CC7275965B} = {8C76003B-F315-487A-92DD-AF5C7D47355A} 77 | {E639214F-BE0C-4642-AB29-D05BC97EF643} = {8C76003B-F315-487A-92DD-AF5C7D47355A} 78 | EndGlobalSection 79 | EndGlobal 80 | -------------------------------------------------------------------------------- /Sample/Sample.Infrastructure/Persistence/Migrations/20240129195639_Initial.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | using Neptunee.EntityFrameworkCore.MultiLanguage.Types; 4 | 5 | #nullable disable 6 | 7 | namespace Sample.Infrastructure.Persistence.Migrations 8 | { 9 | /// 10 | public partial class Initial : Migration 11 | { 12 | /// 13 | protected override void Up(MigrationBuilder migrationBuilder) 14 | { 15 | migrationBuilder.CreateTable( 16 | name: "EventManagers", 17 | columns: table => new 18 | { 19 | Id = table.Column(type: "uuid", nullable: false), 20 | Name = table.Column(type: "text", nullable: false), 21 | PasswordHash = table.Column(type: "text", nullable: false), 22 | Email = table.Column(type: "text", nullable: false), 23 | UtcDateDeleted = table.Column(type: "timestamp with time zone", nullable: true), 24 | UtcDateCreated = table.Column(type: "timestamp with time zone", nullable: false), 25 | UtcDateUpdated = table.Column(type: "timestamp with time zone", nullable: true) 26 | }, 27 | constraints: table => 28 | { 29 | table.PrimaryKey("PK_EventManagers", x => x.Id); 30 | }); 31 | 32 | migrationBuilder.CreateTable( 33 | name: "ParticipationUsers", 34 | columns: table => new 35 | { 36 | Id = table.Column(type: "uuid", nullable: false), 37 | Name = table.Column(type: "text", nullable: false), 38 | Email = table.Column(type: "text", nullable: false), 39 | PasswordHash = table.Column(type: "text", nullable: false), 40 | UtcDateDeleted = table.Column(type: "timestamp with time zone", nullable: true), 41 | UtcDateCreated = table.Column(type: "timestamp with time zone", nullable: false), 42 | UtcDateUpdated = table.Column(type: "timestamp with time zone", nullable: true) 43 | }, 44 | constraints: table => 45 | { 46 | table.PrimaryKey("PK_ParticipationUsers", x => x.Id); 47 | }); 48 | 49 | migrationBuilder.CreateTable( 50 | name: "Events", 51 | columns: table => new 52 | { 53 | Id = table.Column(type: "uuid", nullable: false), 54 | Name = table.Column(type: "jsonb", nullable: false), 55 | Description = table.Column(type: "jsonb", nullable: false), 56 | Location = table.Column(type: "jsonb", nullable: false), 57 | StartDate = table.Column(type: "timestamp with time zone", nullable: false), 58 | EndDate = table.Column(type: "timestamp with time zone", nullable: false), 59 | AvailableTickets = table.Column(type: "integer", nullable: false), 60 | EventManagerId = table.Column(type: "uuid", nullable: false), 61 | ConcurrencyStamp = table.Column(type: "uuid", nullable: false), 62 | UtcDateDeleted = table.Column(type: "timestamp with time zone", nullable: true), 63 | UtcDateCreated = table.Column(type: "timestamp with time zone", nullable: false), 64 | UtcDateUpdated = table.Column(type: "timestamp with time zone", nullable: true) 65 | }, 66 | constraints: table => 67 | { 68 | table.PrimaryKey("PK_Events", x => x.Id); 69 | table.ForeignKey( 70 | name: "FK_Events_EventManagers_EventManagerId", 71 | column: x => x.EventManagerId, 72 | principalTable: "EventManagers", 73 | principalColumn: "Id", 74 | onDelete: ReferentialAction.Cascade); 75 | }); 76 | 77 | migrationBuilder.CreateTable( 78 | name: "Tickets", 79 | columns: table => new 80 | { 81 | Id = table.Column(type: "uuid", nullable: false), 82 | ParticipationUserId = table.Column(type: "uuid", nullable: false), 83 | EventId = table.Column(type: "uuid", nullable: false), 84 | DateCreated = table.Column(type: "timestamp with time zone", nullable: false), 85 | UtcDateDeleted = table.Column(type: "timestamp with time zone", nullable: true), 86 | UtcDateCreated = table.Column(type: "timestamp with time zone", nullable: false), 87 | UtcDateUpdated = table.Column(type: "timestamp with time zone", nullable: true) 88 | }, 89 | constraints: table => 90 | { 91 | table.PrimaryKey("PK_Tickets", x => x.Id); 92 | table.ForeignKey( 93 | name: "FK_Tickets_Events_EventId", 94 | column: x => x.EventId, 95 | principalTable: "Events", 96 | principalColumn: "Id", 97 | onDelete: ReferentialAction.Cascade); 98 | table.ForeignKey( 99 | name: "FK_Tickets_ParticipationUsers_ParticipationUserId", 100 | column: x => x.ParticipationUserId, 101 | principalTable: "ParticipationUsers", 102 | principalColumn: "Id", 103 | onDelete: ReferentialAction.Cascade); 104 | }); 105 | 106 | migrationBuilder.CreateIndex( 107 | name: "IX_EventManagers_Email", 108 | table: "EventManagers", 109 | column: "Email", 110 | unique: true); 111 | 112 | migrationBuilder.CreateIndex( 113 | name: "IX_Events_EventManagerId", 114 | table: "Events", 115 | column: "EventManagerId"); 116 | 117 | migrationBuilder.CreateIndex( 118 | name: "IX_Events_StartDate", 119 | table: "Events", 120 | column: "StartDate"); 121 | 122 | migrationBuilder.CreateIndex( 123 | name: "IX_ParticipationUsers_Email", 124 | table: "ParticipationUsers", 125 | column: "Email", 126 | unique: true); 127 | 128 | migrationBuilder.CreateIndex( 129 | name: "IX_Tickets_EventId", 130 | table: "Tickets", 131 | column: "EventId"); 132 | 133 | migrationBuilder.CreateIndex( 134 | name: "IX_Tickets_ParticipationUserId", 135 | table: "Tickets", 136 | column: "ParticipationUserId"); 137 | } 138 | 139 | /// 140 | protected override void Down(MigrationBuilder migrationBuilder) 141 | { 142 | migrationBuilder.DropTable( 143 | name: "Tickets"); 144 | 145 | migrationBuilder.DropTable( 146 | name: "Events"); 147 | 148 | migrationBuilder.DropTable( 149 | name: "ParticipationUsers"); 150 | 151 | migrationBuilder.DropTable( 152 | name: "EventManagers"); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Sample/Sample.Infrastructure/Persistence/Migrations/SampleDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 6 | using Neptunee.EntityFrameworkCore.MultiLanguage.Types; 7 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 8 | using Sample.Infrastructure.Persistence.Context; 9 | 10 | #nullable disable 11 | 12 | namespace Sample.Infrastructure.Persistence.Migrations 13 | { 14 | [DbContext(typeof(SampleDbContext))] 15 | partial class SampleDbContextModelSnapshot : ModelSnapshot 16 | { 17 | protected override void BuildModel(ModelBuilder modelBuilder) 18 | { 19 | #pragma warning disable 612, 618 20 | modelBuilder 21 | .HasAnnotation("ProductVersion", "8.0.1") 22 | .HasAnnotation("Relational:MaxIdentifierLength", 63); 23 | 24 | NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); 25 | 26 | modelBuilder.Entity("Sample.Domain.Entities.Event", b => 27 | { 28 | b.Property("Id") 29 | .HasColumnType("uuid"); 30 | 31 | b.Property("AvailableTickets") 32 | .HasColumnType("integer"); 33 | 34 | b.Property("ConcurrencyStamp") 35 | .IsConcurrencyToken() 36 | .HasColumnType("uuid"); 37 | 38 | b.Property("Description") 39 | .IsRequired() 40 | .HasColumnType("jsonb"); 41 | 42 | b.Property("EndDate") 43 | .HasColumnType("timestamp with time zone"); 44 | 45 | b.Property("EventManagerId") 46 | .HasColumnType("uuid"); 47 | 48 | b.Property("Location") 49 | .IsRequired() 50 | .HasColumnType("jsonb"); 51 | 52 | b.Property("Name") 53 | .IsRequired() 54 | .HasColumnType("jsonb"); 55 | 56 | b.Property("StartDate") 57 | .HasColumnType("timestamp with time zone"); 58 | 59 | b.Property("UtcDateCreated") 60 | .HasColumnType("timestamp with time zone"); 61 | 62 | b.Property("UtcDateDeleted") 63 | .HasColumnType("timestamp with time zone"); 64 | 65 | b.Property("UtcDateUpdated") 66 | .HasColumnType("timestamp with time zone"); 67 | 68 | b.HasKey("Id"); 69 | 70 | b.HasIndex("EventManagerId"); 71 | 72 | b.HasIndex("StartDate"); 73 | 74 | b.ToTable("Events"); 75 | }); 76 | 77 | modelBuilder.Entity("Sample.Domain.Entities.EventManager", b => 78 | { 79 | b.Property("Id") 80 | .HasColumnType("uuid"); 81 | 82 | b.Property("Email") 83 | .IsRequired() 84 | .HasColumnType("text"); 85 | 86 | b.Property("Name") 87 | .IsRequired() 88 | .HasColumnType("text"); 89 | 90 | b.Property("PasswordHash") 91 | .IsRequired() 92 | .HasColumnType("text"); 93 | 94 | b.Property("UtcDateCreated") 95 | .HasColumnType("timestamp with time zone"); 96 | 97 | b.Property("UtcDateDeleted") 98 | .HasColumnType("timestamp with time zone"); 99 | 100 | b.Property("UtcDateUpdated") 101 | .HasColumnType("timestamp with time zone"); 102 | 103 | b.HasKey("Id"); 104 | 105 | b.HasIndex("Email") 106 | .IsUnique(); 107 | 108 | b.ToTable("EventManagers"); 109 | }); 110 | 111 | modelBuilder.Entity("Sample.Domain.Entities.ParticipationUser", b => 112 | { 113 | b.Property("Id") 114 | .HasColumnType("uuid"); 115 | 116 | b.Property("Email") 117 | .IsRequired() 118 | .HasColumnType("text"); 119 | 120 | b.Property("Name") 121 | .IsRequired() 122 | .HasColumnType("text"); 123 | 124 | b.Property("PasswordHash") 125 | .IsRequired() 126 | .HasColumnType("text"); 127 | 128 | b.Property("UtcDateCreated") 129 | .HasColumnType("timestamp with time zone"); 130 | 131 | b.Property("UtcDateDeleted") 132 | .HasColumnType("timestamp with time zone"); 133 | 134 | b.Property("UtcDateUpdated") 135 | .HasColumnType("timestamp with time zone"); 136 | 137 | b.HasKey("Id"); 138 | 139 | b.HasIndex("Email") 140 | .IsUnique(); 141 | 142 | b.ToTable("ParticipationUsers"); 143 | }); 144 | 145 | modelBuilder.Entity("Sample.Domain.Entities.Ticket", b => 146 | { 147 | b.Property("Id") 148 | .HasColumnType("uuid"); 149 | 150 | b.Property("DateCreated") 151 | .HasColumnType("timestamp with time zone"); 152 | 153 | b.Property("EventId") 154 | .HasColumnType("uuid"); 155 | 156 | b.Property("ParticipationUserId") 157 | .HasColumnType("uuid"); 158 | 159 | b.Property("UtcDateCreated") 160 | .HasColumnType("timestamp with time zone"); 161 | 162 | b.Property("UtcDateDeleted") 163 | .HasColumnType("timestamp with time zone"); 164 | 165 | b.Property("UtcDateUpdated") 166 | .HasColumnType("timestamp with time zone"); 167 | 168 | b.HasKey("Id"); 169 | 170 | b.HasIndex("EventId"); 171 | 172 | b.HasIndex("ParticipationUserId"); 173 | 174 | b.ToTable("Tickets"); 175 | }); 176 | 177 | modelBuilder.Entity("Sample.Domain.Entities.Event", b => 178 | { 179 | b.HasOne("Sample.Domain.Entities.EventManager", "EventManager") 180 | .WithMany("CreatedEvents") 181 | .HasForeignKey("EventManagerId") 182 | .OnDelete(DeleteBehavior.Cascade) 183 | .IsRequired(); 184 | 185 | b.Navigation("EventManager"); 186 | }); 187 | 188 | modelBuilder.Entity("Sample.Domain.Entities.Ticket", b => 189 | { 190 | b.HasOne("Sample.Domain.Entities.Event", "Event") 191 | .WithMany("Tickets") 192 | .HasForeignKey("EventId") 193 | .OnDelete(DeleteBehavior.Cascade) 194 | .IsRequired(); 195 | 196 | b.HasOne("Sample.Domain.Entities.ParticipationUser", "ParticipationUser") 197 | .WithMany("Tickets") 198 | .HasForeignKey("ParticipationUserId") 199 | .OnDelete(DeleteBehavior.Cascade) 200 | .IsRequired(); 201 | 202 | b.Navigation("Event"); 203 | 204 | b.Navigation("ParticipationUser"); 205 | }); 206 | 207 | modelBuilder.Entity("Sample.Domain.Entities.Event", b => 208 | { 209 | b.Navigation("Tickets"); 210 | }); 211 | 212 | modelBuilder.Entity("Sample.Domain.Entities.EventManager", b => 213 | { 214 | b.Navigation("CreatedEvents"); 215 | }); 216 | 217 | modelBuilder.Entity("Sample.Domain.Entities.ParticipationUser", b => 218 | { 219 | b.Navigation("Tickets"); 220 | }); 221 | #pragma warning restore 612, 618 222 | } 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /Sample/Sample.Infrastructure/Persistence/Migrations/20240129195639_Initial.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using Neptunee.EntityFrameworkCore.MultiLanguage.Types; 8 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 9 | using Sample.Infrastructure.Persistence.Context; 10 | 11 | #nullable disable 12 | 13 | namespace Sample.Infrastructure.Persistence.Migrations 14 | { 15 | [DbContext(typeof(SampleDbContext))] 16 | [Migration("20240129195639_Initial")] 17 | partial class Initial 18 | { 19 | /// 20 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 21 | { 22 | #pragma warning disable 612, 618 23 | modelBuilder 24 | .HasAnnotation("ProductVersion", "8.0.1") 25 | .HasAnnotation("Relational:MaxIdentifierLength", 63); 26 | 27 | NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); 28 | 29 | modelBuilder.Entity("Sample.Domain.Entities.Event", b => 30 | { 31 | b.Property("Id") 32 | .HasColumnType("uuid"); 33 | 34 | b.Property("AvailableTickets") 35 | .HasColumnType("integer"); 36 | 37 | b.Property("ConcurrencyStamp") 38 | .IsConcurrencyToken() 39 | .HasColumnType("uuid"); 40 | 41 | b.Property("Description") 42 | .IsRequired() 43 | .HasColumnType("jsonb"); 44 | 45 | b.Property("EndDate") 46 | .HasColumnType("timestamp with time zone"); 47 | 48 | b.Property("EventManagerId") 49 | .HasColumnType("uuid"); 50 | 51 | b.Property("Location") 52 | .IsRequired() 53 | .HasColumnType("jsonb"); 54 | 55 | b.Property("Name") 56 | .IsRequired() 57 | .HasColumnType("jsonb"); 58 | 59 | b.Property("StartDate") 60 | .HasColumnType("timestamp with time zone"); 61 | 62 | b.Property("UtcDateCreated") 63 | .HasColumnType("timestamp with time zone"); 64 | 65 | b.Property("UtcDateDeleted") 66 | .HasColumnType("timestamp with time zone"); 67 | 68 | b.Property("UtcDateUpdated") 69 | .HasColumnType("timestamp with time zone"); 70 | 71 | b.HasKey("Id"); 72 | 73 | b.HasIndex("EventManagerId"); 74 | 75 | b.HasIndex("StartDate"); 76 | 77 | b.ToTable("Events"); 78 | }); 79 | 80 | modelBuilder.Entity("Sample.Domain.Entities.EventManager", b => 81 | { 82 | b.Property("Id") 83 | .HasColumnType("uuid"); 84 | 85 | b.Property("Email") 86 | .IsRequired() 87 | .HasColumnType("text"); 88 | 89 | b.Property("Name") 90 | .IsRequired() 91 | .HasColumnType("text"); 92 | 93 | b.Property("PasswordHash") 94 | .IsRequired() 95 | .HasColumnType("text"); 96 | 97 | b.Property("UtcDateCreated") 98 | .HasColumnType("timestamp with time zone"); 99 | 100 | b.Property("UtcDateDeleted") 101 | .HasColumnType("timestamp with time zone"); 102 | 103 | b.Property("UtcDateUpdated") 104 | .HasColumnType("timestamp with time zone"); 105 | 106 | b.HasKey("Id"); 107 | 108 | b.HasIndex("Email") 109 | .IsUnique(); 110 | 111 | b.ToTable("EventManagers"); 112 | }); 113 | 114 | modelBuilder.Entity("Sample.Domain.Entities.ParticipationUser", b => 115 | { 116 | b.Property("Id") 117 | .HasColumnType("uuid"); 118 | 119 | b.Property("Email") 120 | .IsRequired() 121 | .HasColumnType("text"); 122 | 123 | b.Property("Name") 124 | .IsRequired() 125 | .HasColumnType("text"); 126 | 127 | b.Property("PasswordHash") 128 | .IsRequired() 129 | .HasColumnType("text"); 130 | 131 | b.Property("UtcDateCreated") 132 | .HasColumnType("timestamp with time zone"); 133 | 134 | b.Property("UtcDateDeleted") 135 | .HasColumnType("timestamp with time zone"); 136 | 137 | b.Property("UtcDateUpdated") 138 | .HasColumnType("timestamp with time zone"); 139 | 140 | b.HasKey("Id"); 141 | 142 | b.HasIndex("Email") 143 | .IsUnique(); 144 | 145 | b.ToTable("ParticipationUsers"); 146 | }); 147 | 148 | modelBuilder.Entity("Sample.Domain.Entities.Ticket", b => 149 | { 150 | b.Property("Id") 151 | .HasColumnType("uuid"); 152 | 153 | b.Property("DateCreated") 154 | .HasColumnType("timestamp with time zone"); 155 | 156 | b.Property("EventId") 157 | .HasColumnType("uuid"); 158 | 159 | b.Property("ParticipationUserId") 160 | .HasColumnType("uuid"); 161 | 162 | b.Property("UtcDateCreated") 163 | .HasColumnType("timestamp with time zone"); 164 | 165 | b.Property("UtcDateDeleted") 166 | .HasColumnType("timestamp with time zone"); 167 | 168 | b.Property("UtcDateUpdated") 169 | .HasColumnType("timestamp with time zone"); 170 | 171 | b.HasKey("Id"); 172 | 173 | b.HasIndex("EventId"); 174 | 175 | b.HasIndex("ParticipationUserId"); 176 | 177 | b.ToTable("Tickets"); 178 | }); 179 | 180 | modelBuilder.Entity("Sample.Domain.Entities.Event", b => 181 | { 182 | b.HasOne("Sample.Domain.Entities.EventManager", "EventManager") 183 | .WithMany("CreatedEvents") 184 | .HasForeignKey("EventManagerId") 185 | .OnDelete(DeleteBehavior.Cascade) 186 | .IsRequired(); 187 | 188 | b.Navigation("EventManager"); 189 | }); 190 | 191 | modelBuilder.Entity("Sample.Domain.Entities.Ticket", b => 192 | { 193 | b.HasOne("Sample.Domain.Entities.Event", "Event") 194 | .WithMany("Tickets") 195 | .HasForeignKey("EventId") 196 | .OnDelete(DeleteBehavior.Cascade) 197 | .IsRequired(); 198 | 199 | b.HasOne("Sample.Domain.Entities.ParticipationUser", "ParticipationUser") 200 | .WithMany("Tickets") 201 | .HasForeignKey("ParticipationUserId") 202 | .OnDelete(DeleteBehavior.Cascade) 203 | .IsRequired(); 204 | 205 | b.Navigation("Event"); 206 | 207 | b.Navigation("ParticipationUser"); 208 | }); 209 | 210 | modelBuilder.Entity("Sample.Domain.Entities.Event", b => 211 | { 212 | b.Navigation("Tickets"); 213 | }); 214 | 215 | modelBuilder.Entity("Sample.Domain.Entities.EventManager", b => 216 | { 217 | b.Navigation("CreatedEvents"); 218 | }); 219 | 220 | modelBuilder.Entity("Sample.Domain.Entities.ParticipationUser", b => 221 | { 222 | b.Navigation("Tickets"); 223 | }); 224 | #pragma warning restore 612, 618 225 | } 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### MonoDevelop template 2 | #User Specific 3 | *.userprefs 4 | *.usertasks 5 | 6 | #Mono Project Files 7 | *.pidb 8 | *.resources 9 | test-results/ 10 | 11 | ### VisualStudioCode template 12 | .vscode/* 13 | !.vscode/settings.json 14 | !.vscode/tasks.json 15 | !.vscode/launch.json 16 | !.vscode/extensions.json 17 | *.code-workspace 18 | 19 | # Local History for Visual Studio Code 20 | .history/ 21 | 22 | ### JetBrains template 23 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 24 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 25 | 26 | # User-specific stuff 27 | .idea/**/workspace.xml 28 | .idea/**/tasks.xml 29 | .idea/**/usage.statistics.xml 30 | .idea/**/dictionaries 31 | .idea/**/shelf 32 | 33 | # Generated files 34 | .idea/**/contentModel.xml 35 | 36 | # Sensitive or high-churn files 37 | .idea/**/dataSources/ 38 | .idea/**/dataSources.ids 39 | .idea/**/dataSources.local.xml 40 | .idea/**/sqlDataSources.xml 41 | .idea/**/dynamic.xml 42 | .idea/**/uiDesigner.xml 43 | .idea/**/dbnavigator.xml 44 | 45 | # Gradle 46 | .idea/**/gradle.xml 47 | .idea/**/libraries 48 | 49 | # Gradle and Maven with auto-import 50 | # When using Gradle or Maven with auto-import, you should exclude module files, 51 | # since they will be recreated, and may cause churn. Uncomment if using 52 | # auto-import. 53 | # .idea/artifacts 54 | # .idea/compiler.xml 55 | # .idea/jarRepositories.xml 56 | # .idea/modules.xml 57 | # .idea/*.iml 58 | # .idea/modules 59 | # *.iml 60 | # *.ipr 61 | 62 | # CMake 63 | cmake-build-*/ 64 | 65 | # Mongo Explorer plugin 66 | .idea/**/mongoSettings.xml 67 | 68 | # File-based project format 69 | *.iws 70 | 71 | # IntelliJ 72 | out/ 73 | 74 | # mpeltonen/sbt-idea plugin 75 | .idea_modules/ 76 | 77 | # JIRA plugin 78 | atlassian-ide-plugin.xml 79 | 80 | # Cursive Clojure plugin 81 | .idea/replstate.xml 82 | 83 | # Crashlytics plugin (for Android Studio and IntelliJ) 84 | com_crashlytics_export_strings.xml 85 | crashlytics.properties 86 | crashlytics-build.properties 87 | fabric.properties 88 | 89 | # Editor-based Rest Client 90 | .idea/httpRequests 91 | 92 | # Android studio 3.1+ serialized cache file 93 | .idea/caches/build_file_checksums.ser 94 | 95 | ### Linux template 96 | *~ 97 | 98 | # temporary files which can be created if a process still has a handle open of a deleted file 99 | .fuse_hidden* 100 | 101 | # KDE directory preferences 102 | .directory 103 | 104 | # Linux trash folder which might appear on any partition or disk 105 | .Trash-* 106 | 107 | # .nfs files are created when an open file is removed but is still being accessed 108 | .nfs* 109 | 110 | ### VisualStudio template 111 | ## Ignore Visual Studio temporary files, build results, and 112 | ## files generated by popular Visual Studio add-ons. 113 | ## 114 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 115 | 116 | # User-specific files 117 | *.rsuser 118 | *.suo 119 | *.user 120 | *.userosscache 121 | *.sln.docstates 122 | 123 | # User-specific files (MonoDevelop/Xamarin Studio) 124 | 125 | # Mono auto generated files 126 | mono_crash.* 127 | 128 | # Build results 129 | [Dd]ebug/ 130 | [Dd]ebugPublic/ 131 | [Rr]elease/ 132 | [Rr]eleases/ 133 | x64/ 134 | x86/ 135 | [Ww][Ii][Nn]32/ 136 | [Aa][Rr][Mm]/ 137 | [Aa][Rr][Mm]64/ 138 | bld/ 139 | [Bb]in/ 140 | [Oo]bj/ 141 | [Ll]og/ 142 | [Ll]ogs/ 143 | 144 | # Visual Studio 2015/2017 cache/options directory 145 | .vs/ 146 | # Uncomment if you have tasks that create the project's static files in wwwroot 147 | #wwwroot/ 148 | 149 | # Visual Studio 2017 auto generated files 150 | Generated\ Files/ 151 | 152 | # MSTest test Results 153 | [Tt]est[Rr]esult*/ 154 | [Bb]uild[Ll]og.* 155 | 156 | # NUnit 157 | *.VisualState.xml 158 | TestResult.xml 159 | nunit-*.xml 160 | 161 | # Build Results of an ATL Project 162 | [Dd]ebugPS/ 163 | [Rr]eleasePS/ 164 | dlldata.c 165 | 166 | # Benchmark Results 167 | BenchmarkDotNet.Artifacts/ 168 | 169 | # .NET Core 170 | project.lock.json 171 | project.fragment.lock.json 172 | artifacts/ 173 | 174 | # ASP.NET Scaffolding 175 | ScaffoldingReadMe.txt 176 | 177 | # StyleCop 178 | StyleCopReport.xml 179 | 180 | # Files built by Visual Studio 181 | *_i.c 182 | *_p.c 183 | *_h.h 184 | *.ilk 185 | *.meta 186 | *.obj 187 | *.iobj 188 | *.pch 189 | *.pdb 190 | *.ipdb 191 | *.pgc 192 | *.pgd 193 | *.rsp 194 | *.sbr 195 | *.tlb 196 | *.tli 197 | *.tlh 198 | *.tmp 199 | *.tmp_proj 200 | *_wpftmp.csproj 201 | *.log 202 | *.vspscc 203 | *.vssscc 204 | .builds 205 | *.svclog 206 | *.scc 207 | 208 | # Chutzpah Test files 209 | _Chutzpah* 210 | 211 | # Visual C++ cache files 212 | ipch/ 213 | *.aps 214 | *.ncb 215 | *.opendb 216 | *.opensdf 217 | *.sdf 218 | *.cachefile 219 | *.VC.db 220 | *.VC.VC.opendb 221 | 222 | # Visual Studio profiler 223 | *.psess 224 | *.vsp 225 | *.vspx 226 | *.sap 227 | 228 | # Visual Studio Trace Files 229 | *.e2e 230 | 231 | # TFS 2012 Local Workspace 232 | $tf/ 233 | 234 | # Guidance Automation Toolkit 235 | *.gpState 236 | 237 | # ReSharper is a .NET coding add-in 238 | _ReSharper*/ 239 | *.[Rr]e[Ss]harper 240 | *.DotSettings.user 241 | 242 | # TeamCity is a build add-in 243 | _TeamCity* 244 | 245 | # DotCover is a Code Coverage Tool 246 | *.dotCover 247 | 248 | # AxoCover is a Code Coverage Tool 249 | .axoCover/* 250 | !.axoCover/settings.json 251 | 252 | # Coverlet is a free, cross platform Code Coverage Tool 253 | coverage*.json 254 | coverage*.xml 255 | coverage*.info 256 | 257 | # Visual Studio code coverage results 258 | *.coverage 259 | *.coveragexml 260 | 261 | # NCrunch 262 | _NCrunch_* 263 | .*crunch*.local.xml 264 | nCrunchTemp_* 265 | 266 | # MightyMoose 267 | *.mm.* 268 | AutoTest.Net/ 269 | 270 | # Web workbench (sass) 271 | .sass-cache/ 272 | 273 | # Installshield output folder 274 | [Ee]xpress/ 275 | 276 | # DocProject is a documentation generator add-in 277 | DocProject/buildhelp/ 278 | DocProject/Help/*.HxT 279 | DocProject/Help/*.HxC 280 | DocProject/Help/*.hhc 281 | DocProject/Help/*.hhk 282 | DocProject/Help/*.hhp 283 | DocProject/Help/Html2 284 | DocProject/Help/html 285 | 286 | # Click-Once directory 287 | publish/ 288 | 289 | # Publish Web Output 290 | *.[Pp]ublish.xml 291 | *.azurePubxml 292 | # Note: Comment the next line if you want to checkin your web deploy settings, 293 | # but database connection strings (with potential passwords) will be unencrypted 294 | *.pubxml 295 | *.publishproj 296 | 297 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 298 | # checkin your Azure Web App publish settings, but sensitive information contained 299 | # in these scripts will be unencrypted 300 | PublishScripts/ 301 | 302 | # NuGet Packages 303 | *.nupkg 304 | # NuGet Symbol Packages 305 | *.snupkg 306 | # The packages folder can be ignored because of Package Restore 307 | **/[Pp]ackages/* 308 | # except build/, which is used as an MSBuild target. 309 | !**/[Pp]ackages/build/ 310 | # Uncomment if necessary however generally it will be regenerated when needed 311 | #!**/[Pp]ackages/repositories.config 312 | # NuGet v3's project.json files produces more ignorable files 313 | *.nuget.props 314 | *.nuget.targets 315 | 316 | # Microsoft Azure Build Output 317 | csx/ 318 | *.build.csdef 319 | 320 | # Microsoft Azure Emulator 321 | ecf/ 322 | rcf/ 323 | 324 | # Windows Store app package directories and files 325 | AppPackages/ 326 | BundleArtifacts/ 327 | Package.StoreAssociation.xml 328 | _pkginfo.txt 329 | *.appx 330 | *.appxbundle 331 | *.appxupload 332 | 333 | # Visual Studio cache files 334 | # files ending in .cache can be ignored 335 | *.[Cc]ache 336 | # but keep track of directories ending in .cache 337 | !?*.[Cc]ache/ 338 | 339 | # Others 340 | ClientBin/ 341 | ~$* 342 | *.dbmdl 343 | *.dbproj.schemaview 344 | *.jfm 345 | *.pfx 346 | *.publishsettings 347 | orleans.codegen.cs 348 | 349 | # Including strong name files can present a security risk 350 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 351 | #*.snk 352 | 353 | # Since there are multiple workflows, uncomment next line to ignore bower_components 354 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 355 | #bower_components/ 356 | 357 | # RIA/Silverlight projects 358 | Generated_Code/ 359 | 360 | # Backup & report files from converting an old project file 361 | # to a newer Visual Studio version. Backup files are not needed, 362 | # because we have git ;-) 363 | _UpgradeReport_Files/ 364 | Backup*/ 365 | UpgradeLog*.XML 366 | UpgradeLog*.htm 367 | ServiceFabricBackup/ 368 | *.rptproj.bak 369 | 370 | # SQL Server files 371 | *.mdf 372 | *.ldf 373 | *.ndf 374 | 375 | # Business Intelligence projects 376 | *.rdl.data 377 | *.bim.layout 378 | *.bim_*.settings 379 | *.rptproj.rsuser 380 | *- [Bb]ackup.rdl 381 | *- [Bb]ackup ([0-9]).rdl 382 | *- [Bb]ackup ([0-9][0-9]).rdl 383 | 384 | # Microsoft Fakes 385 | FakesAssemblies/ 386 | 387 | # GhostDoc plugin setting file 388 | *.GhostDoc.xml 389 | 390 | # Node.js Tools for Visual Studio 391 | .ntvs_analysis.dat 392 | node_modules/ 393 | 394 | # Visual Studio 6 build log 395 | *.plg 396 | 397 | # Visual Studio 6 workspace options file 398 | *.opt 399 | 400 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 401 | *.vbw 402 | 403 | # Visual Studio LightSwitch build output 404 | **/*.HTMLClient/GeneratedArtifacts 405 | **/*.DesktopClient/GeneratedArtifacts 406 | **/*.DesktopClient/ModelManifest.xml 407 | **/*.Server/GeneratedArtifacts 408 | **/*.Server/ModelManifest.xml 409 | _Pvt_Extensions 410 | 411 | # Paket dependency manager 412 | .paket/paket.exe 413 | paket-files/ 414 | 415 | # FAKE - F# Make 416 | .fake/ 417 | 418 | # CodeRush personal settings 419 | .cr/personal 420 | 421 | # Python Tools for Visual Studio (PTVS) 422 | __pycache__/ 423 | *.pyc 424 | 425 | # Cake - Uncomment if you are using it 426 | # tools/** 427 | # !tools/packages.config 428 | 429 | # Tabs Studio 430 | *.tss 431 | 432 | # Telerik's JustMock configuration file 433 | *.jmconfig 434 | 435 | # BizTalk build output 436 | *.btp.cs 437 | *.btm.cs 438 | *.odx.cs 439 | *.xsd.cs 440 | 441 | # OpenCover UI analysis results 442 | OpenCover/ 443 | 444 | # Azure Stream Analytics local run output 445 | ASALocalRun/ 446 | 447 | # MSBuild Binary and Structured Log 448 | *.binlog 449 | 450 | # NVidia Nsight GPU debugger configuration file 451 | *.nvuser 452 | 453 | # MFractors (Xamarin productivity tool) working folder 454 | .mfractor/ 455 | 456 | # Local History for Visual Studio 457 | .localhistory/ 458 | 459 | # BeatPulse healthcheck temp database 460 | healthchecksdb 461 | 462 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 463 | MigrationBackup/ 464 | 465 | # Ionide (cross platform F# VS Code tools) working folder 466 | .ionide/ 467 | 468 | # Fody - auto-generated XML schema 469 | FodyWeavers.xsd 470 | 471 | ### Windows template 472 | # Windows thumbnail cache files 473 | Thumbs.db 474 | Thumbs.db:encryptable 475 | ehthumbs.db 476 | ehthumbs_vista.db 477 | 478 | # Dump file 479 | *.stackdump 480 | 481 | # Folder config file 482 | [Dd]esktop.ini 483 | 484 | # Recycle Bin used on file shares 485 | $RECYCLE.BIN/ 486 | 487 | # Windows Installer files 488 | *.cab 489 | *.msi 490 | *.msix 491 | *.msm 492 | *.msp 493 | 494 | # Windows shortcuts 495 | *.lnk 496 | 497 | ### macOS template 498 | # General 499 | .DS_Store 500 | .AppleDouble 501 | .LSOverride 502 | 503 | # Icon must end with two \r 504 | Icon 505 | 506 | # Thumbnails 507 | ._* 508 | 509 | # Files that might appear in the root of a volume 510 | .DocumentRevisions-V100 511 | .fseventsd 512 | .Spotlight-V100 513 | .TemporaryItems 514 | .Trashes 515 | .VolumeIcon.icns 516 | .com.apple.timemachine.donotpresent 517 | 518 | # Directories potentially created on remote AFP share 519 | .AppleDB 520 | .AppleDesktop 521 | Network Trash Folder 522 | Temporary Items 523 | .apdisk 524 | 525 | ### Redis template 526 | # Ignore redis binary dump (dump.rdb) files 527 | 528 | *.rdb 529 | 530 | /.idea/ 531 | --------------------------------------------------------------------------------