├── Parking.Api ├── Queries │ ├── GetAllParkingInfoQuery.cs │ ├── GetRandomAvailablePlace.cs │ ├── GetTotalAvailablePlacesQuery.cs │ ├── GetParkingInfoQuery.cs │ └── Handlers │ │ └── ParkingQueryHandler.cs ├── Commands │ ├── CloseParkingCommand.cs │ ├── OpenParkingCommand.cs │ ├── CreateParkingCommand.cs │ ├── LeaveParkingPlaceCommand.cs │ ├── TakeParkingPlaceCommand.cs │ └── Handlers │ │ └── ParkingCommandHandler.cs ├── Responses │ ├── ParkingPlaceInfo.cs │ └── ParkingInfo.cs ├── Requests │ └── CreateParkingRequest.cs ├── Models │ ├── Parking.cs │ ├── Command.cs │ ├── ParkingPlace.cs │ └── ParkingContext.cs ├── appsettings.json ├── appsettings.Development.json ├── Services │ ├── AuthenticationService.cs │ └── CommandStoreService.cs ├── Migrations │ ├── 20190706102405_AddParkingFK.cs │ ├── 20190706102057_RemoveUser.cs │ ├── ParkingContextModelSnapshot.cs │ ├── 20190706102057_RemoveUser.Designer.cs │ ├── 20190706102405_AddParkingFK.Designer.cs │ ├── 20190706101240_InitialMigration.Designer.cs │ └── 20190706101240_InitialMigration.cs ├── Parking.Api.csproj ├── Program.cs ├── Properties │ └── launchSettings.json ├── Startup.cs └── Controllers │ └── ParkingController.cs ├── readme.md └── .gitignore /Parking.Api/Queries/GetAllParkingInfoQuery.cs: -------------------------------------------------------------------------------- 1 | namespace Parking.Api.Queries 2 | { 3 | public class GetAllParkingInfoQuery 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Parking.Api/Queries/GetRandomAvailablePlace.cs: -------------------------------------------------------------------------------- 1 | namespace Parking.Api.Queries 2 | { 3 | public class GetRandomAvailablePlace 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Parking.Api/Queries/GetTotalAvailablePlacesQuery.cs: -------------------------------------------------------------------------------- 1 | namespace Parking.Api.Queries 2 | { 3 | public class GetTotalAvailablePlacesQuery 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Parking.Api/Commands/CloseParkingCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Parking.Api.Commands 2 | { 3 | public class CloseParkingCommand 4 | { 5 | public string ParkingName { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Parking.Api/Commands/OpenParkingCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Parking.Api.Commands 2 | { 3 | public class OpenParkingCommand 4 | { 5 | public string ParkingName { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Parking.Api/Queries/GetParkingInfoQuery.cs: -------------------------------------------------------------------------------- 1 | namespace Parking.Api.Queries 2 | { 3 | public class GetParkingInfoQuery 4 | { 5 | public string ParkingName { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Parking.Api/Responses/ParkingPlaceInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Parking.Api.Responses 2 | { 3 | public class ParkingPlaceInfo 4 | { 5 | public string ParkingName { get; set; } 6 | public int Number { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Parking.Api/Commands/CreateParkingCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Parking.Api.Commands 2 | { 3 | public class CreateParkingCommand 4 | { 5 | public string ParkingName { get; set; } 6 | public int Capacity { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Parking.Api/Requests/CreateParkingRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Parking.Api.Requests 2 | { 3 | public class CreateParkingRequest 4 | { 5 | public string ParkingName { get; set; } 6 | public int Capacity { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Parking.Api/Commands/LeaveParkingPlaceCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Parking.Api.Commands 2 | { 3 | public class LeaveParkingPlaceCommand 4 | { 5 | public string ParkingName { get; set; } 6 | public int PlaceNumber { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Parking.Api/Commands/TakeParkingPlaceCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Parking.Api.Commands 2 | { 3 | public class TakeParkingPlaceCommand 4 | { 5 | public string ParkingName { get; set; } 6 | public int PlaceNumber { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Parking.Api/Models/Parking.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Parking.Api.Models 4 | { 5 | public class Parking 6 | { 7 | public string Name { get; set; } 8 | public bool IsOpened { get; set; } 9 | 10 | public List Places { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Parking.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning" 5 | } 6 | }, 7 | "AllowedHosts": "*", 8 | "ConnectionStrings": { 9 | "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=Parking;Trusted_Connection=True;MultipleActiveResultSets=True" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Parking.Api/Responses/ParkingInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Parking.Api.Responses 2 | { 3 | public class ParkingInfo 4 | { 5 | public string Name { get; set; } 6 | public bool IsOpened { get; set; } 7 | public int MaximumPlaces { get; set; } 8 | public int AvailablePlaces { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Parking.Api/Models/Command.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Parking.Api.Models 4 | { 5 | public class Command 6 | { 7 | public long Id { get; set; } 8 | public string Type { get; set; } 9 | public string Data { get; set; } 10 | public DateTime CreatedAt { get; set; } 11 | public string UserId { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Parking.Api/Models/ParkingPlace.cs: -------------------------------------------------------------------------------- 1 | namespace Parking.Api.Models 2 | { 3 | public class ParkingPlace 4 | { 5 | public string ParkingName { get; set; } 6 | public int Number { get; set; } 7 | 8 | public bool IsFree { get; set; } 9 | public string UserId { get; set; } 10 | 11 | public Parking Parking { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Parking.Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | }, 9 | "ConnectionStrings": { 10 | "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=Parking;Trusted_Connection=True;MultipleActiveResultSets=True" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Parking.Api/Services/AuthenticationService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Parking.Api.Services 4 | { 5 | public class AuthenticationService 6 | { 7 | private readonly string _userId; 8 | 9 | public AuthenticationService() 10 | { 11 | _userId = Guid.NewGuid().ToString(); 12 | } 13 | 14 | public string GetUserId() => _userId; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Parking.Api/Migrations/20190706102405_AddParkingFK.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | namespace Parking.Api.Migrations 4 | { 5 | public partial class AddParkingFK : Migration 6 | { 7 | protected override void Up(MigrationBuilder migrationBuilder) 8 | { 9 | 10 | } 11 | 12 | protected override void Down(MigrationBuilder migrationBuilder) 13 | { 14 | 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Parking.Api/Parking.Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.2 5 | InProcess 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Parking.Api/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace Parking.Api 12 | { 13 | public class Program 14 | { 15 | public static void Main(string[] args) 16 | { 17 | CreateWebHostBuilder(args).Build().Run(); 18 | } 19 | 20 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 21 | WebHost.CreateDefaultBuilder(args) 22 | .UseStartup(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Parking.Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:63466", 8 | "sslPort": 44321 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "swagger", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "Parking.Api": { 21 | "commandName": "Project", 22 | "launchBrowser": true, 23 | "launchUrl": "swagger", 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /Parking.Api/Models/ParkingContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace Parking.Api.Models 4 | { 5 | public class ParkingContext : DbContext 6 | { 7 | public ParkingContext(DbContextOptions options) 8 | : base(options) 9 | { } 10 | 11 | public DbSet Parking { get; set; } 12 | public DbSet ParkingPlaces { get; set; } 13 | public DbSet CommandStore { get; set; } 14 | 15 | protected override void OnModelCreating(ModelBuilder modelBuilder) 16 | { 17 | modelBuilder.Entity() 18 | .HasKey(p => p.Name); 19 | modelBuilder.Entity() 20 | .HasMany(p => p.Places) 21 | .WithOne(p => p.Parking) 22 | .HasForeignKey(p => p.ParkingName) 23 | .IsRequired(); 24 | 25 | modelBuilder.Entity() 26 | .HasKey(p => new { p.ParkingName, p.Number }); 27 | 28 | modelBuilder.Entity() 29 | .HasKey(c => c.Id); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Parking.Api/Services/CommandStoreService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Newtonsoft.Json; 3 | using Parking.Api.Models; 4 | using System; 5 | 6 | namespace Parking.Api.Services 7 | { 8 | public class CommandStoreService 9 | { 10 | private readonly DbContext _dbContext; 11 | private readonly AuthenticationService _authenticationService; 12 | 13 | public CommandStoreService( 14 | DbContext dbContext, 15 | AuthenticationService authenticationService 16 | ) 17 | { 18 | _dbContext = dbContext; 19 | _authenticationService = authenticationService; 20 | } 21 | 22 | public void Push(object command) 23 | { 24 | _dbContext.Set().Add( 25 | new Command 26 | { 27 | Type = command.GetType().Name, 28 | Data = JsonConvert.SerializeObject(command), 29 | CreatedAt = DateTime.Now, 30 | UserId = _authenticationService.GetUserId() 31 | } 32 | ); 33 | _dbContext.SaveChanges(); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Parking.Api/Migrations/20190706102057_RemoveUser.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | namespace Parking.Api.Migrations 4 | { 5 | public partial class RemoveUser : Migration 6 | { 7 | protected override void Up(MigrationBuilder migrationBuilder) 8 | { 9 | migrationBuilder.DropForeignKey( 10 | name: "FK_ParkingPlaces_Users_UserId", 11 | table: "ParkingPlaces"); 12 | 13 | migrationBuilder.DropTable( 14 | name: "Users"); 15 | 16 | migrationBuilder.DropIndex( 17 | name: "IX_ParkingPlaces_UserId", 18 | table: "ParkingPlaces"); 19 | 20 | migrationBuilder.AlterColumn( 21 | name: "UserId", 22 | table: "ParkingPlaces", 23 | nullable: true, 24 | oldClrType: typeof(string), 25 | oldNullable: true); 26 | } 27 | 28 | protected override void Down(MigrationBuilder migrationBuilder) 29 | { 30 | migrationBuilder.AlterColumn( 31 | name: "UserId", 32 | table: "ParkingPlaces", 33 | nullable: true, 34 | oldClrType: typeof(string), 35 | oldNullable: true); 36 | 37 | migrationBuilder.CreateTable( 38 | name: "Users", 39 | columns: table => new 40 | { 41 | Id = table.Column(nullable: false) 42 | }, 43 | constraints: table => 44 | { 45 | table.PrimaryKey("PK_Users", x => x.Id); 46 | }); 47 | 48 | migrationBuilder.CreateIndex( 49 | name: "IX_ParkingPlaces_UserId", 50 | table: "ParkingPlaces", 51 | column: "UserId"); 52 | 53 | migrationBuilder.AddForeignKey( 54 | name: "FK_ParkingPlaces_Users_UserId", 55 | table: "ParkingPlaces", 56 | column: "UserId", 57 | principalTable: "Users", 58 | principalColumn: "Id", 59 | onDelete: ReferentialAction.Restrict); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Parking.Api/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Parking.Api.Commands.Handlers; 8 | using Parking.Api.Models; 9 | using Parking.Api.Queries.Handlers; 10 | using Parking.Api.Services; 11 | using Swashbuckle.AspNetCore.Swagger; 12 | using AuthenticationService = Parking.Api.Services.AuthenticationService; 13 | 14 | namespace Parking.Api 15 | { 16 | public class Startup 17 | { 18 | public Startup(IConfiguration configuration) 19 | { 20 | Configuration = configuration; 21 | } 22 | 23 | public IConfiguration Configuration { get; } 24 | 25 | public void ConfigureServices(IServiceCollection services) 26 | { 27 | services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); 28 | 29 | services.AddSwaggerGen(c => 30 | { 31 | c.SwaggerDoc("v1", new Info { Title = "Parking API", Version = "v1" }); 32 | }); 33 | 34 | services.AddDbContextPool(options => 35 | { 36 | options.UseSqlServer( 37 | Configuration.GetConnectionString("DefaultConnection") 38 | ); 39 | }); 40 | 41 | services.AddScoped(); 42 | 43 | services.AddScoped(); 44 | services.AddScoped(); 45 | 46 | services.AddScoped(); 47 | } 48 | 49 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 50 | { 51 | if (env.IsDevelopment()) 52 | { 53 | app.UseDeveloperExceptionPage(); 54 | } 55 | else 56 | { 57 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 58 | app.UseHsts(); 59 | } 60 | 61 | app.UseSwagger(); 62 | app.UseSwaggerUI(c => 63 | { 64 | c.SwaggerEndpoint("/swagger/v1/swagger.json", "Parking API V1"); 65 | }); 66 | 67 | app.UseHttpsRedirection(); 68 | app.UseMvc(); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Parking.Api/Migrations/ParkingContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using Parking.Api.Models; 8 | 9 | namespace Parking.Api.Migrations 10 | { 11 | [DbContext(typeof(ParkingContext))] 12 | partial class ParkingContextModelSnapshot : ModelSnapshot 13 | { 14 | protected override void BuildModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder 18 | .HasAnnotation("ProductVersion", "2.2.3-servicing-35854") 19 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 20 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 21 | 22 | modelBuilder.Entity("Parking.Api.Models.Command", b => 23 | { 24 | b.Property("Id") 25 | .ValueGeneratedOnAdd() 26 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 27 | 28 | b.Property("CreatedAt"); 29 | 30 | b.Property("Data"); 31 | 32 | b.Property("Type"); 33 | 34 | b.Property("UserId"); 35 | 36 | b.HasKey("Id"); 37 | 38 | b.ToTable("CommandStore"); 39 | }); 40 | 41 | modelBuilder.Entity("Parking.Api.Models.Parking", b => 42 | { 43 | b.Property("Name") 44 | .ValueGeneratedOnAdd(); 45 | 46 | b.Property("IsOpened"); 47 | 48 | b.HasKey("Name"); 49 | 50 | b.ToTable("Parking"); 51 | }); 52 | 53 | modelBuilder.Entity("Parking.Api.Models.ParkingPlace", b => 54 | { 55 | b.Property("ParkingName"); 56 | 57 | b.Property("Number"); 58 | 59 | b.Property("IsFree"); 60 | 61 | b.Property("UserId"); 62 | 63 | b.HasKey("ParkingName", "Number"); 64 | 65 | b.ToTable("ParkingPlaces"); 66 | }); 67 | 68 | modelBuilder.Entity("Parking.Api.Models.ParkingPlace", b => 69 | { 70 | b.HasOne("Parking.Api.Models.Parking", "Parking") 71 | .WithMany("Places") 72 | .HasForeignKey("ParkingName") 73 | .OnDelete(DeleteBehavior.Cascade); 74 | }); 75 | #pragma warning restore 612, 618 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Parking.Api/Migrations/20190706102057_RemoveUser.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | using Parking.Api.Models; 9 | 10 | namespace Parking.Api.Migrations 11 | { 12 | [DbContext(typeof(ParkingContext))] 13 | [Migration("20190706102057_RemoveUser")] 14 | partial class RemoveUser 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "2.2.3-servicing-35854") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 23 | 24 | modelBuilder.Entity("Parking.Api.Models.Command", b => 25 | { 26 | b.Property("Id") 27 | .ValueGeneratedOnAdd() 28 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 29 | 30 | b.Property("CreatedAt"); 31 | 32 | b.Property("Data"); 33 | 34 | b.Property("Type"); 35 | 36 | b.Property("UserId"); 37 | 38 | b.HasKey("Id"); 39 | 40 | b.ToTable("CommandStore"); 41 | }); 42 | 43 | modelBuilder.Entity("Parking.Api.Models.Parking", b => 44 | { 45 | b.Property("Name") 46 | .ValueGeneratedOnAdd(); 47 | 48 | b.Property("IsOpened"); 49 | 50 | b.HasKey("Name"); 51 | 52 | b.ToTable("Parking"); 53 | }); 54 | 55 | modelBuilder.Entity("Parking.Api.Models.ParkingPlace", b => 56 | { 57 | b.Property("ParkingName"); 58 | 59 | b.Property("Number"); 60 | 61 | b.Property("IsFree"); 62 | 63 | b.Property("UserId"); 64 | 65 | b.HasKey("ParkingName", "Number"); 66 | 67 | b.ToTable("ParkingPlaces"); 68 | }); 69 | 70 | modelBuilder.Entity("Parking.Api.Models.ParkingPlace", b => 71 | { 72 | b.HasOne("Parking.Api.Models.Parking") 73 | .WithMany("Places") 74 | .HasForeignKey("ParkingName") 75 | .OnDelete(DeleteBehavior.Cascade); 76 | }); 77 | #pragma warning restore 612, 618 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Parking.Api/Migrations/20190706102405_AddParkingFK.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | using Parking.Api.Models; 9 | 10 | namespace Parking.Api.Migrations 11 | { 12 | [DbContext(typeof(ParkingContext))] 13 | [Migration("20190706102405_AddParkingFK")] 14 | partial class AddParkingFK 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "2.2.3-servicing-35854") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 23 | 24 | modelBuilder.Entity("Parking.Api.Models.Command", b => 25 | { 26 | b.Property("Id") 27 | .ValueGeneratedOnAdd() 28 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 29 | 30 | b.Property("CreatedAt"); 31 | 32 | b.Property("Data"); 33 | 34 | b.Property("Type"); 35 | 36 | b.Property("UserId"); 37 | 38 | b.HasKey("Id"); 39 | 40 | b.ToTable("CommandStore"); 41 | }); 42 | 43 | modelBuilder.Entity("Parking.Api.Models.Parking", b => 44 | { 45 | b.Property("Name") 46 | .ValueGeneratedOnAdd(); 47 | 48 | b.Property("IsOpened"); 49 | 50 | b.HasKey("Name"); 51 | 52 | b.ToTable("Parking"); 53 | }); 54 | 55 | modelBuilder.Entity("Parking.Api.Models.ParkingPlace", b => 56 | { 57 | b.Property("ParkingName"); 58 | 59 | b.Property("Number"); 60 | 61 | b.Property("IsFree"); 62 | 63 | b.Property("UserId"); 64 | 65 | b.HasKey("ParkingName", "Number"); 66 | 67 | b.ToTable("ParkingPlaces"); 68 | }); 69 | 70 | modelBuilder.Entity("Parking.Api.Models.ParkingPlace", b => 71 | { 72 | b.HasOne("Parking.Api.Models.Parking", "Parking") 73 | .WithMany("Places") 74 | .HasForeignKey("ParkingName") 75 | .OnDelete(DeleteBehavior.Cascade); 76 | }); 77 | #pragma warning restore 612, 618 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Parking.Api/Queries/Handlers/ParkingQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Parking.Api.Models; 3 | using Parking.Api.Responses; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace Parking.Api.Queries.Handlers 9 | { 10 | public class ParkingQueryHandler 11 | { 12 | private readonly DbContext _dbContext; 13 | 14 | public ParkingQueryHandler(DbContext dbContext) 15 | { 16 | _dbContext = dbContext; 17 | } 18 | 19 | public IEnumerable Handle(GetAllParkingInfoQuery _) 20 | { 21 | var parkings = _dbContext.Set() 22 | .Include(p => p.Places) 23 | .ToList(); 24 | 25 | return parkings.Select(p => 26 | { 27 | return new ParkingInfo 28 | { 29 | Name = p.Name, 30 | IsOpened = p.IsOpened, 31 | MaximumPlaces = p.Places.Count, 32 | AvailablePlaces = 33 | p.IsOpened 34 | ? p.Places.Where(pp => pp.IsFree).Count() 35 | : 0 36 | }; 37 | }); 38 | } 39 | 40 | public ParkingInfo Handle(GetParkingInfoQuery query) 41 | { 42 | var parking = _dbContext.Set() 43 | .Include(p => p.Places) 44 | .FirstOrDefault(p => p.Name == query.ParkingName); 45 | 46 | if (parking == null) 47 | { 48 | throw new Exception($"Cannot find parking '{query.ParkingName}'."); 49 | } 50 | 51 | return new ParkingInfo 52 | { 53 | Name = parking.Name, 54 | IsOpened = parking.IsOpened, 55 | MaximumPlaces = parking.Places.Count, 56 | AvailablePlaces = 57 | parking.IsOpened 58 | ? parking.Places.Where(pp => pp.IsFree).Count() 59 | : 0 60 | }; 61 | } 62 | 63 | public ParkingPlaceInfo Handle(GetRandomAvailablePlace _) 64 | { 65 | var random = new Random(); 66 | 67 | var parkingPlace = _dbContext.Set() 68 | .Include(p => p.Parking) 69 | .Where(p => p.Parking.IsOpened && p.IsFree) 70 | .OrderBy(p => random.Next()) 71 | .FirstOrDefault(); 72 | 73 | return new ParkingPlaceInfo 74 | { 75 | ParkingName = parkingPlace.ParkingName, 76 | Number = parkingPlace.Number 77 | }; 78 | } 79 | 80 | public int Handle(GetTotalAvailablePlacesQuery _) 81 | { 82 | return _dbContext.Set() 83 | .Where(p => p.Parking.IsOpened && p.IsFree) 84 | .Count(); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Parking.Api/Migrations/20190706101240_InitialMigration.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | using Parking.Api.Models; 9 | 10 | namespace Parking.Api.Migrations 11 | { 12 | [DbContext(typeof(ParkingContext))] 13 | [Migration("20190706101240_InitialMigration")] 14 | partial class InitialMigration 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "2.2.3-servicing-35854") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 23 | 24 | modelBuilder.Entity("Parking.Api.Models.Command", b => 25 | { 26 | b.Property("Id") 27 | .ValueGeneratedOnAdd() 28 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 29 | 30 | b.Property("CreatedAt"); 31 | 32 | b.Property("Data"); 33 | 34 | b.Property("Type"); 35 | 36 | b.Property("UserId"); 37 | 38 | b.HasKey("Id"); 39 | 40 | b.ToTable("CommandStore"); 41 | }); 42 | 43 | modelBuilder.Entity("Parking.Api.Models.Parking", b => 44 | { 45 | b.Property("Name") 46 | .ValueGeneratedOnAdd(); 47 | 48 | b.Property("IsOpened"); 49 | 50 | b.HasKey("Name"); 51 | 52 | b.ToTable("Parking"); 53 | }); 54 | 55 | modelBuilder.Entity("Parking.Api.Models.ParkingPlace", b => 56 | { 57 | b.Property("ParkingName"); 58 | 59 | b.Property("Number"); 60 | 61 | b.Property("IsFree"); 62 | 63 | b.Property("UserId"); 64 | 65 | b.HasKey("ParkingName", "Number"); 66 | 67 | b.HasIndex("UserId"); 68 | 69 | b.ToTable("ParkingPlaces"); 70 | }); 71 | 72 | modelBuilder.Entity("Parking.Api.Models.User", b => 73 | { 74 | b.Property("Id") 75 | .ValueGeneratedOnAdd(); 76 | 77 | b.HasKey("Id"); 78 | 79 | b.ToTable("Users"); 80 | }); 81 | 82 | modelBuilder.Entity("Parking.Api.Models.ParkingPlace", b => 83 | { 84 | b.HasOne("Parking.Api.Models.Parking") 85 | .WithMany("Places") 86 | .HasForeignKey("ParkingName") 87 | .OnDelete(DeleteBehavior.Cascade); 88 | 89 | b.HasOne("Parking.Api.Models.User") 90 | .WithMany() 91 | .HasForeignKey("UserId"); 92 | }); 93 | #pragma warning restore 612, 618 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Parking.Api/Controllers/ParkingController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Converto; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Parking.Api.Commands; 5 | using Parking.Api.Commands.Handlers; 6 | using Parking.Api.Queries; 7 | using Parking.Api.Queries.Handlers; 8 | using Parking.Api.Requests; 9 | using Parking.Api.Responses; 10 | 11 | namespace Parking.Api.Controllers 12 | { 13 | [Route("api/[controller]")] 14 | [ApiController] 15 | public class ParkingController : ControllerBase 16 | { 17 | private readonly ParkingCommandHandler _commandHandler; 18 | private readonly ParkingQueryHandler _queryHandler; 19 | 20 | public ParkingController( 21 | ParkingCommandHandler commandHandler, 22 | ParkingQueryHandler queryHandler 23 | ) 24 | { 25 | _commandHandler = commandHandler; 26 | _queryHandler = queryHandler; 27 | } 28 | 29 | [HttpGet("availablePlaces/count")] 30 | public int GetTotalAvailablePlaces() 31 | { 32 | var query = new GetTotalAvailablePlacesQuery(); 33 | return _queryHandler.Handle(query); 34 | } 35 | 36 | [HttpGet("availablePlaces/random")] 37 | public ParkingPlaceInfo GetRandomAvailablePlace() 38 | { 39 | var query = new GetRandomAvailablePlace(); 40 | return _queryHandler.Handle(query); 41 | } 42 | 43 | [HttpGet] 44 | public IEnumerable GetAllParkingInfos() 45 | { 46 | var query = new GetAllParkingInfoQuery(); 47 | return _queryHandler.Handle(query); 48 | } 49 | 50 | [HttpGet("{parkingName}")] 51 | public ParkingInfo GetParkingInfo(string parkingName) 52 | { 53 | var query = new GetParkingInfoQuery { ParkingName = parkingName }; 54 | return _queryHandler.Handle(query); 55 | } 56 | 57 | [HttpPost] 58 | public void CreateParking([FromBody] CreateParkingRequest request) 59 | { 60 | var command = request.ConvertTo(); 61 | _commandHandler.Handle(command); 62 | } 63 | 64 | [HttpPost("{parkingName}/open")] 65 | public void OpenParking(string parkingName) 66 | { 67 | var command = new OpenParkingCommand { ParkingName = parkingName }; 68 | _commandHandler.Handle(command); 69 | } 70 | 71 | [HttpPost("{parkingName}/close")] 72 | public void CloseParking(string parkingName) 73 | { 74 | var command = new CloseParkingCommand { ParkingName = parkingName }; 75 | _commandHandler.Handle(command); 76 | } 77 | 78 | [HttpPost("{parkingName}/{placeNumber}/take")] 79 | public void TakeParkingPlace(string parkingName, int placeNumber) 80 | { 81 | var command = new TakeParkingPlaceCommand 82 | { 83 | ParkingName = parkingName, 84 | PlaceNumber = placeNumber 85 | }; 86 | _commandHandler.Handle(command); 87 | } 88 | 89 | [HttpPost("{parkingName}/{placeNumber}/leave")] 90 | public void LeaveParkingPlace(string parkingName, int placeNumber) 91 | { 92 | var command = new LeaveParkingPlaceCommand 93 | { 94 | ParkingName = parkingName, 95 | PlaceNumber = placeNumber 96 | }; 97 | _commandHandler.Handle(command); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Parking.Api/Migrations/20190706101240_InitialMigration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Metadata; 3 | using Microsoft.EntityFrameworkCore.Migrations; 4 | 5 | namespace Parking.Api.Migrations 6 | { 7 | public partial class InitialMigration : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.CreateTable( 12 | name: "CommandStore", 13 | columns: table => new 14 | { 15 | Id = table.Column(nullable: false) 16 | .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), 17 | Type = table.Column(nullable: true), 18 | Data = table.Column(nullable: true), 19 | CreatedAt = table.Column(nullable: false), 20 | UserId = table.Column(nullable: true) 21 | }, 22 | constraints: table => 23 | { 24 | table.PrimaryKey("PK_CommandStore", x => x.Id); 25 | }); 26 | 27 | migrationBuilder.CreateTable( 28 | name: "Parking", 29 | columns: table => new 30 | { 31 | Name = table.Column(nullable: false), 32 | IsOpened = table.Column(nullable: false) 33 | }, 34 | constraints: table => 35 | { 36 | table.PrimaryKey("PK_Parking", x => x.Name); 37 | }); 38 | 39 | migrationBuilder.CreateTable( 40 | name: "Users", 41 | columns: table => new 42 | { 43 | Id = table.Column(nullable: false) 44 | }, 45 | constraints: table => 46 | { 47 | table.PrimaryKey("PK_Users", x => x.Id); 48 | }); 49 | 50 | migrationBuilder.CreateTable( 51 | name: "ParkingPlaces", 52 | columns: table => new 53 | { 54 | ParkingName = table.Column(nullable: false), 55 | Number = table.Column(nullable: false), 56 | IsFree = table.Column(nullable: false), 57 | UserId = table.Column(nullable: true) 58 | }, 59 | constraints: table => 60 | { 61 | table.PrimaryKey("PK_ParkingPlaces", x => new { x.ParkingName, x.Number }); 62 | table.ForeignKey( 63 | name: "FK_ParkingPlaces_Parking_ParkingName", 64 | column: x => x.ParkingName, 65 | principalTable: "Parking", 66 | principalColumn: "Name", 67 | onDelete: ReferentialAction.Cascade); 68 | table.ForeignKey( 69 | name: "FK_ParkingPlaces_Users_UserId", 70 | column: x => x.UserId, 71 | principalTable: "Users", 72 | principalColumn: "Id", 73 | onDelete: ReferentialAction.Restrict); 74 | }); 75 | 76 | migrationBuilder.CreateIndex( 77 | name: "IX_ParkingPlaces_UserId", 78 | table: "ParkingPlaces", 79 | column: "UserId"); 80 | } 81 | 82 | protected override void Down(MigrationBuilder migrationBuilder) 83 | { 84 | migrationBuilder.DropTable( 85 | name: "CommandStore"); 86 | 87 | migrationBuilder.DropTable( 88 | name: "ParkingPlaces"); 89 | 90 | migrationBuilder.DropTable( 91 | name: "Parking"); 92 | 93 | migrationBuilder.DropTable( 94 | name: "Users"); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # A naive introduction to CQRS in C# 2 | 3 | ### A quick intro 4 | 5 | CQRS for Command and Query Responsibility Segregation is a pattern used to separate the logic between commands and queries. 6 | 7 | Well, if you are used to create HTTP web API, here is the translation: 8 | 9 | * Queries = GET methods 10 | * Commands = POST/PUT/DELETE methods 11 | 12 | ![CQRS pattern described by Martin Fowler](https://martinfowler.com/bliki/images/cqrs/cqrs.png) 13 | 14 | So why CQRS? Of course, you have the Single Responsibility Principle by design and so you get the ability to design a loosely coupled architecture which has multiple benefits: 15 | 16 | * A clear Read model with a list of queries and domain objects you can use 17 | * An isolation of each command inside a Write model 18 | * A simple definition of each query and command 19 | * Logging queries and commands 20 | * Being ready to optimize the Read model or the Command model at any time (considering the underlying database) 21 | * And also writing new queries/commands without breaking previous changes 22 | 23 | ### The naive example 24 | 25 | In this example, we will create a Parking system using .NET Core and a single SQL Server database. 26 | 27 | If you want to see the code in details, please check the following repository: https://github.com/Odonno/cqrs-dotnet-core 28 | 29 | #### A command 30 | 31 | First, create a list of commands that will be used in your system. 32 | 33 | ```cs 34 | public class OpenParkingCommand 35 | { 36 | public string ParkingName { get; set; } 37 | } 38 | ``` 39 | 40 | Now, when you receive the action to open a parking, you set the command and handle it inside your own system, which can looks like this: 41 | 42 | ```cs 43 | public void Handle(OpenParkingCommand command) 44 | { 45 | var parking = _dbContext.Set() 46 | .FirstOrDefault(p => p.Name == command.ParkingName); 47 | 48 | if (parking == null) 49 | { 50 | throw new Exception($"Cannot find parking '{command.ParkingName}'."); 51 | } 52 | if (parking.IsOpened) 53 | { 54 | throw new Exception($"Parking '{command.ParkingName}' is already opened."); 55 | } 56 | 57 | parking.IsOpened = true; 58 | _dbContext.SaveChanges(); 59 | 60 | _commandStoreService.Push(command); 61 | } 62 | ``` 63 | 64 | #### A query 65 | 66 | Same for queries, list all queries that will be used in your system. 67 | 68 | ```cs 69 | public class GetParkingInfoQuery 70 | { 71 | public string ParkingName { get; set; } 72 | } 73 | ``` 74 | 75 | And handle those queries inside a query handler: 76 | 77 | ```cs 78 | public ParkingInfo Handle(GetParkingInfoQuery query) 79 | { 80 | var parking = _dbContext.Set() 81 | .Include(p => p.Places) 82 | .FirstOrDefault(p => p.Name == query.ParkingName); 83 | 84 | if (parking == null) 85 | { 86 | throw new Exception($"Cannot find parking '{query.ParkingName}'."); 87 | } 88 | 89 | return new ParkingInfo 90 | { 91 | Name = parking.Name, 92 | IsOpened = parking.IsOpened, 93 | MaximumPlaces = parking.Places.Count, 94 | AvailablePlaces = 95 | parking.IsOpened 96 | ? parking.Places.Where(pp => pp.IsFree).Count() 97 | : 0 98 | }; 99 | } 100 | ``` 101 | 102 | ### Beyond CQRS 103 | 104 | #### GraphQL 105 | 106 | If you are familiar with GraphQL, you may know that it implements CQRS by design: 107 | 108 | * Query = Read Model 109 | * Mutation = Write Model 110 | 111 | #### Command Sourcing 112 | 113 | Once you have a working CQRS architecture, you can persist every command executed in your application inside a database called a Command Store. 114 | 115 | A Command Store is pretty much a logging system where you can retrieve every change made in your system. 116 | 117 | Because it is like a logging system, you can design your Command Store as a `push only` database/collection. And following our example, it can look like this: 118 | 119 | ```cs 120 | public void Push(object command) 121 | { 122 | _dbContext.Set().Add( 123 | new Command 124 | { 125 | Type = command.GetType().Name, 126 | Data = JsonConvert.SerializeObject(command), 127 | CreatedAt = DateTime.Now, 128 | UserId = _authenticationService.GetUserId() 129 | } 130 | ); 131 | _dbContext.SaveChanges(); 132 | } 133 | ``` 134 | 135 | #### Event Sourcing 136 | 137 | Event Sourcing is a much more complex pattern designed to create a system around events where: 138 | 139 | * Events are the single source of truth (Write Model) 140 | * Data and services are the result of these events (Read Model) 141 | 142 | I will not explain this pattern here but it has a strong relationship with CQRS in a way it separates the events (Write Model) from the queries (Read Model). -------------------------------------------------------------------------------- /Parking.Api/Commands/Handlers/ParkingCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Parking.Api.Models; 3 | using Parking.Api.Services; 4 | using System; 5 | using System.Linq; 6 | 7 | namespace Parking.Api.Commands.Handlers 8 | { 9 | public class ParkingCommandHandler 10 | { 11 | private readonly DbContext _dbContext; 12 | private readonly CommandStoreService _commandStoreService; 13 | private readonly AuthenticationService _authenticationService; 14 | 15 | public ParkingCommandHandler( 16 | DbContext dbContext, 17 | CommandStoreService commandStoreService, 18 | AuthenticationService authenticationService 19 | ) 20 | { 21 | _dbContext = dbContext; 22 | _commandStoreService = commandStoreService; 23 | _authenticationService = authenticationService; 24 | } 25 | 26 | public void Handle(CloseParkingCommand command) 27 | { 28 | var parking = _dbContext.Set() 29 | .FirstOrDefault(p => p.Name == command.ParkingName); 30 | 31 | if (parking == null) 32 | { 33 | throw new Exception($"Cannot find parking '{command.ParkingName}'."); 34 | } 35 | if (!parking.IsOpened) 36 | { 37 | throw new Exception($"Parking '{command.ParkingName}' is already closed."); 38 | } 39 | 40 | parking.IsOpened = false; 41 | _dbContext.SaveChanges(); 42 | 43 | _commandStoreService.Push(command); 44 | } 45 | 46 | public void Handle(CreateParkingCommand command) 47 | { 48 | var places = Enumerable.Range(1, command.Capacity) 49 | .Select(n => 50 | { 51 | return new ParkingPlace 52 | { 53 | ParkingName = command.ParkingName, 54 | Number = n, 55 | IsFree = true 56 | }; 57 | }) 58 | .ToList(); 59 | 60 | var parking = new Models.Parking 61 | { 62 | Name = command.ParkingName, 63 | IsOpened = true, 64 | Places = places 65 | }; 66 | 67 | _dbContext.Add(parking); 68 | _dbContext.SaveChanges(); 69 | 70 | _commandStoreService.Push(command); 71 | } 72 | 73 | public void Handle(LeaveParkingPlaceCommand command) 74 | { 75 | var parking = _dbContext.Set() 76 | .FirstOrDefault(p => p.Name == command.ParkingName); 77 | 78 | if (parking == null) 79 | { 80 | throw new Exception($"Cannot find parking '{command.ParkingName}'."); 81 | } 82 | if (!parking.IsOpened) 83 | { 84 | throw new Exception($"The parking '{command.ParkingName}' is closed."); 85 | } 86 | 87 | var parkingPlace = _dbContext.Set() 88 | .FirstOrDefault(p => p.ParkingName == command.ParkingName && p.Number == command.PlaceNumber); 89 | 90 | if (parkingPlace == null) 91 | { 92 | throw new Exception($"Cannot find place #{command.PlaceNumber} in the parking '{command.ParkingName}'."); 93 | } 94 | if (parkingPlace.IsFree) 95 | { 96 | throw new Exception($"Parking place #{command.PlaceNumber} is still free."); 97 | } 98 | 99 | parkingPlace.IsFree = true; 100 | parkingPlace.UserId = null; 101 | _dbContext.SaveChanges(); 102 | 103 | _commandStoreService.Push(command); 104 | } 105 | 106 | public void Handle(OpenParkingCommand command) 107 | { 108 | var parking = _dbContext.Set() 109 | .FirstOrDefault(p => p.Name == command.ParkingName); 110 | 111 | if (parking == null) 112 | { 113 | throw new Exception($"Cannot find parking '{command.ParkingName}'."); 114 | } 115 | if (parking.IsOpened) 116 | { 117 | throw new Exception($"Parking '{command.ParkingName}' is already opened."); 118 | } 119 | 120 | parking.IsOpened = true; 121 | _dbContext.SaveChanges(); 122 | 123 | _commandStoreService.Push(command); 124 | } 125 | 126 | public void Handle(TakeParkingPlaceCommand command) 127 | { 128 | var parking = _dbContext.Set() 129 | .FirstOrDefault(p => p.Name == command.ParkingName); 130 | 131 | if (parking == null) 132 | { 133 | throw new Exception($"Cannot find parking '{command.ParkingName}'."); 134 | } 135 | if (!parking.IsOpened) 136 | { 137 | throw new Exception($"The parking '{command.ParkingName}' is closed."); 138 | } 139 | 140 | var parkingPlace = _dbContext.Set() 141 | .FirstOrDefault(p => p.ParkingName == command.ParkingName && p.Number == command.PlaceNumber); 142 | 143 | if (parkingPlace == null) 144 | { 145 | throw new Exception($"Cannot find place #{command.PlaceNumber} in the parking '{command.ParkingName}'."); 146 | } 147 | if (!parkingPlace.IsFree) 148 | { 149 | throw new Exception($"Parking place #{command.PlaceNumber} is already taken."); 150 | } 151 | 152 | parkingPlace.IsFree = false; 153 | parkingPlace.UserId = _authenticationService.GetUserId(); 154 | _dbContext.SaveChanges(); 155 | 156 | _commandStoreService.Push(command); 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/visualstudio 2 | # Edit at https://www.gitignore.io/?templates=visualstudio 3 | 4 | ### VisualStudio ### 5 | ## Ignore Visual Studio temporary files, build results, and 6 | ## files generated by popular Visual Studio add-ons. 7 | ## 8 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 9 | 10 | # User-specific files 11 | *.rsuser 12 | *.suo 13 | *.user 14 | *.userosscache 15 | *.sln.docstates 16 | 17 | # User-specific files (MonoDevelop/Xamarin Studio) 18 | *.userprefs 19 | 20 | # Mono auto generated files 21 | mono_crash.* 22 | 23 | # Build results 24 | [Dd]ebug/ 25 | [Dd]ebugPublic/ 26 | [Rr]elease/ 27 | [Rr]eleases/ 28 | x64/ 29 | x86/ 30 | [Aa][Rr][Mm]/ 31 | [Aa][Rr][Mm]64/ 32 | bld/ 33 | [Bb]in/ 34 | [Oo]bj/ 35 | [Ll]og/ 36 | 37 | # Visual Studio 2015/2017 cache/options directory 38 | .vs/ 39 | # Uncomment if you have tasks that create the project's static files in wwwroot 40 | #wwwroot/ 41 | 42 | # Visual Studio 2017 auto generated files 43 | Generated\ Files/ 44 | 45 | # MSTest test Results 46 | [Tt]est[Rr]esult*/ 47 | [Bb]uild[Ll]og.* 48 | 49 | # NUnit 50 | *.VisualState.xml 51 | TestResult.xml 52 | nunit-*.xml 53 | 54 | # Build Results of an ATL Project 55 | [Dd]ebugPS/ 56 | [Rr]eleasePS/ 57 | dlldata.c 58 | 59 | # Benchmark Results 60 | BenchmarkDotNet.Artifacts/ 61 | 62 | # .NET Core 63 | project.lock.json 64 | project.fragment.lock.json 65 | artifacts/ 66 | 67 | # StyleCop 68 | StyleCopReport.xml 69 | 70 | # Files built by Visual Studio 71 | *_i.c 72 | *_p.c 73 | *_h.h 74 | *.ilk 75 | *.meta 76 | *.obj 77 | *.iobj 78 | *.pch 79 | *.pdb 80 | *.ipdb 81 | *.pgc 82 | *.pgd 83 | *.rsp 84 | *.sbr 85 | *.tlb 86 | *.tli 87 | *.tlh 88 | *.tmp 89 | *.tmp_proj 90 | *_wpftmp.csproj 91 | *.log 92 | *.vspscc 93 | *.vssscc 94 | .builds 95 | *.pidb 96 | *.svclog 97 | *.scc 98 | 99 | # Chutzpah Test files 100 | _Chutzpah* 101 | 102 | # Visual C++ cache files 103 | ipch/ 104 | *.aps 105 | *.ncb 106 | *.opendb 107 | *.opensdf 108 | *.sdf 109 | *.cachefile 110 | *.VC.db 111 | *.VC.VC.opendb 112 | 113 | # Visual Studio profiler 114 | *.psess 115 | *.vsp 116 | *.vspx 117 | *.sap 118 | 119 | # Visual Studio Trace Files 120 | *.e2e 121 | 122 | # TFS 2012 Local Workspace 123 | $tf/ 124 | 125 | # Guidance Automation Toolkit 126 | *.gpState 127 | 128 | # ReSharper is a .NET coding add-in 129 | _ReSharper*/ 130 | *.[Rr]e[Ss]harper 131 | *.DotSettings.user 132 | 133 | # JustCode is a .NET coding add-in 134 | .JustCode 135 | 136 | # TeamCity is a build add-in 137 | _TeamCity* 138 | 139 | # DotCover is a Code Coverage Tool 140 | *.dotCover 141 | 142 | # AxoCover is a Code Coverage Tool 143 | .axoCover/* 144 | !.axoCover/settings.json 145 | 146 | # Visual Studio code coverage results 147 | *.coverage 148 | *.coveragexml 149 | 150 | # NCrunch 151 | _NCrunch_* 152 | .*crunch*.local.xml 153 | nCrunchTemp_* 154 | 155 | # MightyMoose 156 | *.mm.* 157 | AutoTest.Net/ 158 | 159 | # Web workbench (sass) 160 | .sass-cache/ 161 | 162 | # Installshield output folder 163 | [Ee]xpress/ 164 | 165 | # DocProject is a documentation generator add-in 166 | DocProject/buildhelp/ 167 | DocProject/Help/*.HxT 168 | DocProject/Help/*.HxC 169 | DocProject/Help/*.hhc 170 | DocProject/Help/*.hhk 171 | DocProject/Help/*.hhp 172 | DocProject/Help/Html2 173 | DocProject/Help/html 174 | 175 | # Click-Once directory 176 | publish/ 177 | 178 | # Publish Web Output 179 | *.[Pp]ublish.xml 180 | *.azurePubxml 181 | # Note: Comment the next line if you want to checkin your web deploy settings, 182 | # but database connection strings (with potential passwords) will be unencrypted 183 | *.pubxml 184 | *.publishproj 185 | 186 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 187 | # checkin your Azure Web App publish settings, but sensitive information contained 188 | # in these scripts will be unencrypted 189 | PublishScripts/ 190 | 191 | # NuGet Packages 192 | *.nupkg 193 | # NuGet Symbol Packages 194 | *.snupkg 195 | # The packages folder can be ignored because of Package Restore 196 | **/[Pp]ackages/* 197 | # except build/, which is used as an MSBuild target. 198 | !**/[Pp]ackages/build/ 199 | # Uncomment if necessary however generally it will be regenerated when needed 200 | #!**/[Pp]ackages/repositories.config 201 | # NuGet v3's project.json files produces more ignorable files 202 | *.nuget.props 203 | *.nuget.targets 204 | 205 | # Microsoft Azure Build Output 206 | csx/ 207 | *.build.csdef 208 | 209 | # Microsoft Azure Emulator 210 | ecf/ 211 | rcf/ 212 | 213 | # Windows Store app package directories and files 214 | AppPackages/ 215 | BundleArtifacts/ 216 | Package.StoreAssociation.xml 217 | _pkginfo.txt 218 | *.appx 219 | *.appxbundle 220 | *.appxupload 221 | 222 | # Visual Studio cache files 223 | # files ending in .cache can be ignored 224 | *.[Cc]ache 225 | # but keep track of directories ending in .cache 226 | !?*.[Cc]ache/ 227 | 228 | # Others 229 | ClientBin/ 230 | ~$* 231 | *~ 232 | *.dbmdl 233 | *.dbproj.schemaview 234 | *.jfm 235 | *.pfx 236 | *.publishsettings 237 | orleans.codegen.cs 238 | 239 | # Including strong name files can present a security risk 240 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 241 | #*.snk 242 | 243 | # Since there are multiple workflows, uncomment next line to ignore bower_components 244 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 245 | #bower_components/ 246 | 247 | # RIA/Silverlight projects 248 | Generated_Code/ 249 | 250 | # Backup & report files from converting an old project file 251 | # to a newer Visual Studio version. Backup files are not needed, 252 | # because we have git ;-) 253 | _UpgradeReport_Files/ 254 | Backup*/ 255 | UpgradeLog*.XML 256 | UpgradeLog*.htm 257 | ServiceFabricBackup/ 258 | *.rptproj.bak 259 | 260 | # SQL Server files 261 | *.mdf 262 | *.ldf 263 | *.ndf 264 | 265 | # Business Intelligence projects 266 | *.rdl.data 267 | *.bim.layout 268 | *.bim_*.settings 269 | *.rptproj.rsuser 270 | *- [Bb]ackup.rdl 271 | *- [Bb]ackup ([0-9]).rdl 272 | *- [Bb]ackup ([0-9][0-9]).rdl 273 | 274 | # Microsoft Fakes 275 | FakesAssemblies/ 276 | 277 | # GhostDoc plugin setting file 278 | *.GhostDoc.xml 279 | 280 | # Node.js Tools for Visual Studio 281 | .ntvs_analysis.dat 282 | node_modules/ 283 | 284 | # Visual Studio 6 build log 285 | *.plg 286 | 287 | # Visual Studio 6 workspace options file 288 | *.opt 289 | 290 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 291 | *.vbw 292 | 293 | # Visual Studio LightSwitch build output 294 | **/*.HTMLClient/GeneratedArtifacts 295 | **/*.DesktopClient/GeneratedArtifacts 296 | **/*.DesktopClient/ModelManifest.xml 297 | **/*.Server/GeneratedArtifacts 298 | **/*.Server/ModelManifest.xml 299 | _Pvt_Extensions 300 | 301 | # Paket dependency manager 302 | .paket/paket.exe 303 | paket-files/ 304 | 305 | # FAKE - F# Make 306 | .fake/ 307 | 308 | # CodeRush personal settings 309 | .cr/personal 310 | 311 | # Python Tools for Visual Studio (PTVS) 312 | __pycache__/ 313 | *.pyc 314 | 315 | # Cake - Uncomment if you are using it 316 | # tools/** 317 | # !tools/packages.config 318 | 319 | # Tabs Studio 320 | *.tss 321 | 322 | # Telerik's JustMock configuration file 323 | *.jmconfig 324 | 325 | # BizTalk build output 326 | *.btp.cs 327 | *.btm.cs 328 | *.odx.cs 329 | *.xsd.cs 330 | 331 | # OpenCover UI analysis results 332 | OpenCover/ 333 | 334 | # Azure Stream Analytics local run output 335 | ASALocalRun/ 336 | 337 | # MSBuild Binary and Structured Log 338 | *.binlog 339 | 340 | # NVidia Nsight GPU debugger configuration file 341 | *.nvuser 342 | 343 | # MFractors (Xamarin productivity tool) working folder 344 | .mfractor/ 345 | 346 | # Local History for Visual Studio 347 | .localhistory/ 348 | 349 | # BeatPulse healthcheck temp database 350 | healthchecksdb 351 | 352 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 353 | MigrationBackup/ 354 | 355 | # End of https://www.gitignore.io/api/visualstudio 356 | --------------------------------------------------------------------------------