diff --git a/Emergence.api/Controllers/TenantController.cs b/Emergence.api/Controllers/TenantController.cs index acc3435..74409d3 100644 --- a/Emergence.api/Controllers/TenantController.cs +++ b/Emergence.api/Controllers/TenantController.cs @@ -1,7 +1,8 @@ -using Microsoft.AspNetCore.Mvc; +using Emergence.api.Models; using Emergence.models; using Emergence.services.Interface; using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; using System.Text.Json.Serialization; namespace Emergence.api.Controllers; @@ -17,7 +18,7 @@ public class TenantController : ControllerBase } [HttpGet(Name = "GetTenants")] - public async Task>, NotFound>> Get() + public async Task>, NotFound>> GetTenants() { var tenants = await _tenantService.GetAllAsync(); if (tenants is null) @@ -26,4 +27,32 @@ public class TenantController : ControllerBase } return TypedResults.Ok(tenants); } + + [HttpGet("{id}", Name = "GetTenantById")] + public async Task, NotFound, BadRequest>> GetTenantById(Guid id) + { + var tenant = await _tenantService.GetByIdAsync(id); + if (tenant is null) + { + return TypedResults.NotFound(); + } + return TypedResults.Ok(tenant); + } + + [HttpPost(Name = "CreateTenant")] + public async Task, Conflict, BadRequest, UnprocessableEntity>> Create(TenantModel tenant) + { + if (tenant == null || !ModelState.IsValid) + return TypedResults.BadRequest(new ValidationProblemResult(ModelState)); + + if (tenant.Id.HasValue && tenant.Id.Value != Guid.Empty && _tenantService.GetByIdAsync(tenant.Id.Value).Result != null) + return TypedResults.Conflict(new ErrorDetailResults($"Tenant already exists", $"Tenant with the id: {tenant.Id} already exists")); + + var newTenant = await _tenantService.CreateAsync(tenant); + if (newTenant is null) + { + return TypedResults.UnprocessableEntity(new ErrorDetailResults($"Tenant failed to create", $"The new tenant failed to create. Please try again.")); + } + return TypedResults.Created($"/tenant/{newTenant.Id}", newTenant); + } } diff --git a/Emergence.api/Emergence.api.http b/Emergence.api/Emergence.api.http index d188f07..7031683 100644 --- a/Emergence.api/Emergence.api.http +++ b/Emergence.api/Emergence.api.http @@ -3,5 +3,9 @@ GET {{Emergence.api_HostAddress}}/tenant/ Accept: application/json +### +GET {{Emergence.api_HostAddress}}/tenant/cc641668-862d-4101-8581-14574d58fd7f/ +Accept: application/json + ### GET {{Emergence.api_HostAddress}}/openapi/v1.json diff --git a/Emergence.api/Models/ErrorDetailResults.cs b/Emergence.api/Models/ErrorDetailResults.cs new file mode 100644 index 0000000..15cdafa --- /dev/null +++ b/Emergence.api/Models/ErrorDetailResults.cs @@ -0,0 +1,13 @@ +namespace Emergence.api.Models; + +public class ErrorDetailResults +{ + public string Title { get; set; } + public string Description { get; set; } + + public ErrorDetailResults(string Title, string Description) + { + this.Title = Title; + this.Description = Description; + } +} diff --git a/Emergence.api/Models/ValidationProblemResult.cs b/Emergence.api/Models/ValidationProblemResult.cs new file mode 100644 index 0000000..05a41ec --- /dev/null +++ b/Emergence.api/Models/ValidationProblemResult.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Emergence.api.Models; + +public class ValidationProblemResult +{ + public ModelStateDictionary ModelState { get; set; } + public ValidationProblemResult(ModelStateDictionary modelState) + { + ModelState = modelState; + } +} diff --git a/Emergence.api/Notes.txt b/Emergence.api/Notes.txt new file mode 100644 index 0000000..30c6558 --- /dev/null +++ b/Emergence.api/Notes.txt @@ -0,0 +1,5 @@ +To add a migration +Add-Migration -Project Emergence.data -Startup Emergence.data + +To update the database +Update-Database -Project Emergence.data -Startup Emergence.data \ No newline at end of file diff --git a/Emergence.api/Program.cs b/Emergence.api/Program.cs index f480467..13f0c1d 100644 --- a/Emergence.api/Program.cs +++ b/Emergence.api/Program.cs @@ -4,7 +4,7 @@ using Scalar.AspNetCore; var builder = WebApplication.CreateBuilder(args); // Add services to the container. -builder.Services.AddEmergenceServices(); +builder.Services.AddEmergenceServices(builder.Configuration); builder.Services.AddControllers(); // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi diff --git a/Emergence.api/appsettings.json b/Emergence.api/appsettings.json index 10f68b8..1fa67a7 100644 --- a/Emergence.api/appsettings.json +++ b/Emergence.api/appsettings.json @@ -1,4 +1,7 @@ { + "ConnectionStrings": { + "AdminConnection": "Data Source=10.1.2.240;Database=Emergence;Integrated Security=false;User Id=emergence;Password=Bed7432Rank!;Connect Timeout=30;Encrypt=False;TrustServerCertificate=True;ApplicationIntent=ReadWrite;MultiSubnetFailover=False" + }, "Logging": { "LogLevel": { "Default": "Information", diff --git a/Emergence.data/Contexts/AdminDbContext.cs b/Emergence.data/Contexts/AdminDbContext.cs new file mode 100644 index 0000000..c5f61c3 --- /dev/null +++ b/Emergence.data/Contexts/AdminDbContext.cs @@ -0,0 +1,33 @@ +using Emergence.data.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; + + +namespace Emergence.data.Contexts +{ + public class AdminDbContext : DbContext + { + public AdminDbContext() : base() + { + + } + + public AdminDbContext(DbContextOptions options) : base(options) { + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseSeeding((context, _) => + { + context.SaveChanges(); + }); + } + + public DbSet Tenant { get; set; } + } +} diff --git a/Emergence.data/Emergence.data.csproj b/Emergence.data/Emergence.data.csproj new file mode 100644 index 0000000..4b76f60 --- /dev/null +++ b/Emergence.data/Emergence.data.csproj @@ -0,0 +1,24 @@ + + + + net10.0 + enable + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + diff --git a/Emergence.data/Extensions/ServiceBuilderExtension.cs b/Emergence.data/Extensions/ServiceBuilderExtension.cs new file mode 100644 index 0000000..bd02847 --- /dev/null +++ b/Emergence.data/Extensions/ServiceBuilderExtension.cs @@ -0,0 +1,19 @@ +using Emergence.data.Contexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + + +namespace Emergence.data.Extensions; + +public static class ServiceBuilderExtension +{ + public static IServiceCollection AddDataServices(this IServiceCollection services, IConfigurationManager configuration) + { + var connectionString = configuration.GetConnectionString("AdminConnection") ?? throw new InvalidOperationException("Connection string 'AdminConnection' not found."); + services.AddDbContext(options => + options.UseSqlServer(connectionString, x => x.MigrationsAssembly(typeof(AdminDbContext).Assembly.FullName))); + + return services; + } +} diff --git a/Emergence.data/Factories/AdminDbContextFactory.cs b/Emergence.data/Factories/AdminDbContextFactory.cs new file mode 100644 index 0000000..1c09208 --- /dev/null +++ b/Emergence.data/Factories/AdminDbContextFactory.cs @@ -0,0 +1,39 @@ +using Emergence.data.Contexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Json; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Emergence.data.Factories; + +public class AdminDbContextFactory : IDesignTimeDbContextFactory +{ + private readonly IConfiguration _configuration; + + public AdminDbContextFactory() + { + + } + + public AdminDbContextFactory(IConfiguration configuration) + { + _configuration = configuration; + } + public AdminDbContext CreateDbContext(string[] args) + { + var dir = new DirectoryInfo(Environment.CurrentDirectory); + // throw new Exception($"{dir.Parent}\\Emergence.api\\appsettings.json"); + ConfigurationManager config = new ConfigurationManager(); + config.AddJsonFile($"{dir.Parent}\\Emergence.api\\appsettings.json"); + var t = config.GetConnectionString("AdminConnection"); + + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseSqlServer(t); + + return new AdminDbContext(optionsBuilder.Options); + } + +} diff --git a/Emergence.data/Migrations/20260401053644_test.Designer.cs b/Emergence.data/Migrations/20260401053644_test.Designer.cs new file mode 100644 index 0000000..c81b04d --- /dev/null +++ b/Emergence.data/Migrations/20260401053644_test.Designer.cs @@ -0,0 +1,52 @@ +// +using System; +using Emergence.data.Contexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Emergence.data.Migrations +{ + [DbContext(typeof(AdminDbContext))] + [Migration("20260401053644_test")] + partial class test + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Emergence.data.Models.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CompanyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsInactive") + .HasColumnType("bit"); + + b.Property("TenantCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Tenant"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Emergence.data/Migrations/20260401053644_test.cs b/Emergence.data/Migrations/20260401053644_test.cs new file mode 100644 index 0000000..b5cab42 --- /dev/null +++ b/Emergence.data/Migrations/20260401053644_test.cs @@ -0,0 +1,36 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Emergence.data.Migrations +{ + /// + public partial class test : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Tenant", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + TenantCode = table.Column(type: "nvarchar(max)", nullable: false), + CompanyName = table.Column(type: "nvarchar(max)", nullable: false), + IsInactive = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Tenant", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Tenant"); + } + } +} diff --git a/Emergence.data/Migrations/AdminDbContextModelSnapshot.cs b/Emergence.data/Migrations/AdminDbContextModelSnapshot.cs new file mode 100644 index 0000000..429ce96 --- /dev/null +++ b/Emergence.data/Migrations/AdminDbContextModelSnapshot.cs @@ -0,0 +1,49 @@ +// +using System; +using Emergence.data.Contexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Emergence.data.Migrations +{ + [DbContext(typeof(AdminDbContext))] + partial class AdminDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Emergence.data.Models.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CompanyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsInactive") + .HasColumnType("bit"); + + b.Property("TenantCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Tenant"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Emergence.data/Models/Tenant.cs b/Emergence.data/Models/Tenant.cs new file mode 100644 index 0000000..ab3f423 --- /dev/null +++ b/Emergence.data/Models/Tenant.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Text; + +namespace Emergence.data.Models +{ + public class Tenant + { + [Key] + public Guid Id { get; set; } + + [Required] + public required string TenantCode { get; set; } + + [Required] + public required string CompanyName { get; set; } + + [Required] + public required bool IsInactive { get; set; } + + } +} diff --git a/Emergence.models/TenantModel.cs b/Emergence.models/TenantModel.cs index e8ef930..6775d32 100644 --- a/Emergence.models/TenantModel.cs +++ b/Emergence.models/TenantModel.cs @@ -6,7 +6,7 @@ namespace Emergence.models; public class TenantModel { - public Guid Id { get; set; } + public Guid? Id { get; set; } public string TenantCode { get; set; } public string CompanyName { get; set; } public bool IsInactive { get; set; } diff --git a/Emergence.services/Emergence.services.csproj b/Emergence.services/Emergence.services.csproj index ede46b0..eebb77a 100644 --- a/Emergence.services/Emergence.services.csproj +++ b/Emergence.services/Emergence.services.csproj @@ -11,6 +11,7 @@ + diff --git a/Emergence.services/Extensions/ServiceBuilderExtension.cs b/Emergence.services/Extensions/ServiceBuilderExtension.cs index 2bab7eb..81bda90 100644 --- a/Emergence.services/Extensions/ServiceBuilderExtension.cs +++ b/Emergence.services/Extensions/ServiceBuilderExtension.cs @@ -1,19 +1,19 @@ -using Emergence.services.Interface; +using Emergence.data.Extensions; +using Emergence.services.Interface; using Emergence.services.Services; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using System; -using System.Collections.Generic; -using System.Text; +using Microsoft.IdentityModel.Protocols; -namespace Emergence.services.Extensions +namespace Emergence.services.Extensions; + +public static class ServiceBuilderExtension { - public static class ServiceBuilderExtension + public static IServiceCollection AddEmergenceServices(this IServiceCollection services, IConfigurationManager configuration) { - public static IServiceCollection AddEmergenceServices(this IServiceCollection services) - { - services = services.AddTransient(); + services.AddDataServices(configuration); + services = services.AddTransient(); - return services; - } + return services; } } diff --git a/Emergence.services/Interface/IService.cs b/Emergence.services/Interfaces/IService.cs similarity index 85% rename from Emergence.services/Interface/IService.cs rename to Emergence.services/Interfaces/IService.cs index dc08a5a..4f88076 100644 --- a/Emergence.services/Interface/IService.cs +++ b/Emergence.services/Interfaces/IService.cs @@ -8,5 +8,7 @@ namespace Emergence.services.Interface { public Task> GetAllAsync(); public Task GetByIdAsync(I id); + + public Task CreateAsync(T tenant); } } diff --git a/Emergence.services/Interface/ITenantService.cs b/Emergence.services/Interfaces/ITenantService.cs similarity index 100% rename from Emergence.services/Interface/ITenantService.cs rename to Emergence.services/Interfaces/ITenantService.cs diff --git a/Emergence.services/Services/TenantService.cs b/Emergence.services/Services/TenantService.cs index a56adc6..161c70b 100644 --- a/Emergence.services/Services/TenantService.cs +++ b/Emergence.services/Services/TenantService.cs @@ -1,29 +1,73 @@ -using Emergence.models; +using Emergence.data.Contexts; +using Emergence.data.Models; +using Emergence.models; using Emergence.services.Interface; using System; using System.Collections.Generic; +using System.Data.Common; using System.Text; namespace Emergence.services.Services; +public static class TenantExtension +{ + public static TenantModel ConvertToTenantModel(this Tenant tenant) + { + TenantModel model = new TenantModel + { + Id = tenant.Id, + TenantCode = tenant.TenantCode, + CompanyName = tenant.CompanyName, + IsInactive = tenant.IsInactive, + }; + return model; + } + public static Tenant ConvertToTenant(this TenantModel tenant) + { + Tenant model = new Tenant + { + Id = tenant.Id.HasValue ? tenant.Id.Value : Guid.NewGuid(), + TenantCode = tenant.TenantCode, + CompanyName = tenant.CompanyName, + IsInactive = tenant.IsInactive, + }; + return model; + } +} public class TenantService : ITenantService { - private List testList = [ - new TenantModel {Id = Guid.NewGuid(), TenantCode = "FJP", CompanyName = "Fred J Potter", IsInactive = false }, - new TenantModel {Id = Guid.NewGuid(), TenantCode = "JAZZ", CompanyName = "Jazzbond", IsInactive = false }, - new TenantModel {Id = Guid.NewGuid(), TenantCode = "TDT", CompanyName = "The Digital Tailor", IsInactive = false }, - new TenantModel {Id = Guid.NewGuid(), TenantCode = "HBAS", CompanyName = "Hicks Building and Asbestos Services", IsInactive = false }, - ]; + private readonly AdminDbContext _dbContext; + public TenantService(AdminDbContext dbContext) + { + _dbContext = dbContext; + } public async Task> GetAllAsync() { - return testList; + return _dbContext.Tenant.Select(s => s.ConvertToTenantModel()); } public async Task GetByIdAsync(Guid id) { -#pragma warning disable CS8603 // Possible null reference return. - return testList.FirstOrDefault(f => f.Id == id); -#pragma warning restore CS8603 // Possible null reference return. + var entity = _dbContext.Tenant.Where(f => f.Id == id)?.Select(s => s.ConvertToTenantModel()).FirstOrDefault(); + return entity; + } + + public async Task CreateAsync(TenantModel model) + { + if (model.Id == Guid.Empty || model.Id == null) + model.Id = Guid.NewGuid(); + + Tenant newTenant = model.ConvertToTenant(); + try + { + _dbContext.Tenant.Add(newTenant); + await _dbContext.SaveChangesAsync(); + return model; + } + catch (DbException ex) + { + return null; + } } } diff --git a/Emergence.slnx b/Emergence.slnx index cce215e..bb6db19 100644 --- a/Emergence.slnx +++ b/Emergence.slnx @@ -1,5 +1,6 @@ +