Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 146 additions & 10 deletions src/Services/Customer/Customer.API/Controllers/CustomerController.cs
Original file line number Diff line number Diff line change
@@ -1,29 +1,165 @@
using Customer.API.ViewModels;
using Customer.Infrastructure.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using CustomerEntity = Customer.Domain.Entities.Customer;
using GenderEnum = Customer.Domain.Entities.Gender;

namespace Customer.API.Controllers;

[ApiController]
[Route("api/[controller]")]
[Authorize]
[Produces("application/json")]
public class CustomerController : ControllerBase
Comment on lines 11 to 14

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Controller drops [Route] attribute, diverging from every other service's pattern

All other controllers in the repo (Identity, Order, Product, Notification) use [Route("api/[controller]")] with relative action routes. This controller removes the [Route] attribute and uses absolute routes with leading / (e.g. [HttpGet("/")]). The comments explain this is intentional for gateway prefix stripping, but it creates an inconsistency across services that could confuse contributors. The Notification service (src/Services/Notification/Notification.API/Controllers/NotificationController.cs:9) keeps its controller-level [Route], suggesting no repo-wide decision to move to root routes.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional and required by the pinned routing convention for this service: the gateway matches /api/customers/{**catch-all} and strips the prefix via PathRemovePrefix, so the controller must sit at the service root with absolute routes ([HttpGet("/")], [HttpGet("/{id:int}")], etc.). Using [Route("api/[controller]")] here would mean the service expects /api/customers/... while the gateway forwards /..., breaking routing. The inline comments document this so contributors understand the divergence is deliberate.

{
private readonly CustomerDbContext _dbContext;
private readonly ILogger<CustomerController> _logger;

public CustomerController(ILogger<CustomerController> logger)
public CustomerController(CustomerDbContext dbContext, ILogger<CustomerController> logger)
{
_dbContext = dbContext;
_logger = logger;
}

[HttpGet]
public IActionResult GetAll()
// External: GET /api/customers -> service GET /
[HttpGet("/")]
[ProducesResponseType(typeof(IEnumerable<CustomerVM>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<ActionResult<IEnumerable<CustomerVM>>> GetAll()
{
// TODO: Implement — migrate logic from monolith's CustomerController
return Ok(new { service = "Customer", status = "scaffold" });
var customers = await _dbContext.Customers
.OrderBy(c => c.Name)
.ToListAsync();

return Ok(customers.Select(ToViewModel));
}

// External: GET /api/customers/{id} -> service GET /{id}
[HttpGet("/{id:int}")]
[ProducesResponseType(typeof(CustomerVM), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<ActionResult<CustomerVM>> GetById(int id)
{
var customer = await _dbContext.Customers.FindAsync(id);
if (customer == null)
return NotFound();

return Ok(ToViewModel(customer));
}

// External: POST /api/customers -> service POST /
[HttpPost("/")]
[ProducesResponseType(typeof(CustomerVM), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<ActionResult<CustomerVM>> Create([FromBody] CustomerVM model)
{
if (string.IsNullOrWhiteSpace(model.Name))
ModelState.AddModelError(nameof(model.Name), "Customer name cannot be empty");
if (string.IsNullOrWhiteSpace(model.Email))
ModelState.AddModelError(nameof(model.Email), "Email cannot be empty");
if (string.IsNullOrWhiteSpace(model.Gender))
ModelState.AddModelError(nameof(model.Gender), "Gender cannot be empty");
else if (!Enum.TryParse<GenderEnum>(model.Gender, ignoreCase: true, out var parsedGender) || !Enum.IsDefined(parsedGender))
ModelState.AddModelError(nameof(model.Gender), "Invalid gender value");
if (!ModelState.IsValid)
return BadRequest(ModelState);
Comment on lines +63 to +68

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Gender validation allows 'None' as a valid input despite requiring a non-empty value

The validation at src/Services/Customer/Customer.API/Controllers/CustomerController.cs:63-66 rejects null/whitespace Gender but accepts "None" (case-insensitive) since it's a defined enum value (Gender.None at src/Services/Customer/Customer.Domain/Entities/Gender.cs:5). This means a user can explicitly set gender to the 'unspecified' sentinel value, which may defeat the purpose of the non-empty check. Whether this is intentional depends on product requirements — if None is meant only as a default/unset marker, it should be excluded from valid inputs.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional — keeping None as an accepted value. None is a first-class defined member of the Gender enum carried over verbatim from the monolith (None/Female/Male), and it's the domain's "unspecified" value rather than an invalid one. The not-empty + IsDefined checks exist to reject blank/garbage/out-of-range input, not to forbid None. Excluding None would diverge from the source-of-truth domain model.


var customer = new CustomerEntity
{
Name = model.Name!,
Email = model.Email!,
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
PhoneNumber = model.PhoneNumber,
Address = model.Address,
City = model.City,
Gender = ParseGender(model.Gender),
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
CreatedBy = CurrentUser(),
UpdatedBy = CurrentUser(),
CreatedDate = DateTime.UtcNow,
UpdatedDate = DateTime.UtcNow
};

_dbContext.Customers.Add(customer);
await _dbContext.SaveChangesAsync();
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

var vm = ToViewModel(customer);
return CreatedAtAction(nameof(GetById), new { id = customer.Id }, vm);

@devin-ai-integration devin-ai-integration Bot Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 CreatedAtAction Location header will be incorrect for gateway clients

The CreatedAtAction(nameof(GetById), new { id = customer.Id }, vm) call at line 82 generates a Location header pointing to the internal service URL (e.g., /5). Clients calling through the API gateway expect /api/customers/5. YARP does not automatically rewrite response Location headers. This means the 201 Created response's Location header is technically incorrect for external consumers. Most REST clients use the response body rather than the Location header, so impact is low, but it violates HTTP 201 semantics for gateway-routed requests.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — this is intentional given the decoupling constraints. The service is deliberately prefix-agnostic (the gateway owns /api/customers and strips it via PathRemovePrefix), so the service cannot know its external base path without coupling it back to the gateway. The Location header will be the service-relative /{id}; the 201 body carries the full resource (including id), which is what clients consume here. Leaving as-is to preserve the database-per-service / prefix-independence design rather than hard-coding the external prefix.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Created-resource Location URL points to the wrong path, giving clients a 404 when they follow it

The Location header in the "customer created" response is built from a root-level route (CreatedAtAction(nameof(GetById), ...) at src/Services/Customer/Customer.API/Controllers/CustomerController.cs:86) without the gateway path prefix, so clients following that URL get a 404.

Impact: Any REST client that follows the standard Location header from a 201 Created response will receive a 404 from the API gateway.

Root cause: gateway prefix stripped on inbound but never restored on outbound URLs

The YARP API gateway matches requests at /api/customers/{**catch-all} and applies PathRemovePrefix: /api/customers (src/ApiGateway/appsettings.json:19), so the Customer service sees requests at /. The GetById action has an absolute route template /{id:int} (CustomerController.cs:39), so CreatedAtAction generates a Location path of /{id} (e.g. /5) rather than /api/customers/5.

The gateway has no response transform to rewrite the Location header, and the service does not use UseForwardedHeaders or set a PathBase. Consequently, the Location header in the 201 response points to a URL like http://gateway:5000/5, which matches no gateway route and returns 404.

Compare with the Notification service (src/Services/Notification/Notification.API/Controllers/NotificationController.cs:9) which uses [Route("api/[controller]")] and relative routes, generating self-consistent URLs (though that service has its own singular/plural mismatch).

Prompt for agents
The CreatedAtAction call generates a Location URL using the service's internal route (/{id}), but the YARP gateway strips the /api/customers prefix from inbound requests and does not add it back to outbound response headers. This means the Location header points to an unreachable path (e.g. /5 instead of /api/customers/5).

There are several approaches to fix this:

1. Add UseForwardedHeaders middleware in Program.cs and configure the gateway to pass X-Forwarded-Prefix (or X-Forwarded-PathBase). Then ASP.NET Core will set PathBase correctly and URL generation will include the prefix.

2. Add a YARP response transform (ResponseHeaderRoutePrefix or a custom transform) to rewrite Location headers in responses.

3. Re-add [Route("api/customers")] to the CustomerController (matching the gateway prefix pattern) and remove the leading / from all action route templates, so that URLs are generated with the full /api/customers/{id} path. This would mean the gateway's PathRemovePrefix transform is no longer needed for this service (or should be removed).

Option 3 is simplest and most consistent with other services in the repo (e.g. NotificationController uses [Route("api/[controller]")]).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged. The routing shape here (absolute root routes + gateway PathRemovePrefix) is a pinned system convention for this service, so option 3 (re-adding [Route("api/customers")]) is out of scope — it would reintroduce the prefix the gateway is configured to strip. The service is intentionally prefix-agnostic and cannot synthesize the external base path without coupling back to the gateway. The Location header is therefore service-relative (/{id}); the 201 body carries the full resource (including id), which is what clients consume. If we later want a correct external Location, the right place is a gateway-side response transform (or X-Forwarded-Prefix + UsePathBase), not a change to this service's routing.

}

// External: PUT /api/customers/{id} -> service PUT /{id}
[HttpPut("/{id:int}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> Update(int id, [FromBody] CustomerVM model)
{
var customer = await _dbContext.Customers.FindAsync(id);
if (customer == null)
return NotFound();

if (string.IsNullOrWhiteSpace(model.Name))
ModelState.AddModelError(nameof(model.Name), "Customer name cannot be empty");
if (string.IsNullOrWhiteSpace(model.Email))
ModelState.AddModelError(nameof(model.Email), "Email cannot be empty");
if (string.IsNullOrWhiteSpace(model.Gender))
ModelState.AddModelError(nameof(model.Gender), "Gender cannot be empty");
else if (!Enum.TryParse<GenderEnum>(model.Gender, ignoreCase: true, out var parsedGender) || !Enum.IsDefined(parsedGender))
ModelState.AddModelError(nameof(model.Gender), "Invalid gender value");
if (!ModelState.IsValid)
return BadRequest(ModelState);

customer.Name = model.Name!;
customer.Email = model.Email!;
customer.PhoneNumber = model.PhoneNumber;
customer.Address = model.Address;
customer.City = model.City;
customer.Gender = ParseGender(model.Gender);
customer.UpdatedBy = CurrentUser();
customer.UpdatedDate = DateTime.UtcNow;

await _dbContext.SaveChangesAsync();
return NoContent();
}

// External: DELETE /api/customers/{id} -> service DELETE /{id}
[HttpDelete("/{id:int}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> Delete(int id)
{
var customer = await _dbContext.Customers.FindAsync(id);
if (customer == null)
return NotFound();

_dbContext.Customers.Remove(customer);
await _dbContext.SaveChangesAsync();
return NoContent();
}

[HttpGet("{id}")]
public IActionResult GetById(int id)
private static CustomerVM ToViewModel(CustomerEntity c) => new()
{
Id = c.Id,
Name = c.Name,
Email = c.Email,
PhoneNumber = c.PhoneNumber,
Address = c.Address,
City = c.City,
Gender = c.Gender.ToString()
};

private static GenderEnum ParseGender(string? value) =>
Enum.TryParse<GenderEnum>(value, ignoreCase: true, out var gender) ? gender : GenderEnum.None;

// CreatedBy/UpdatedBy column is varchar(40); truncate the JWT identity to fit.
private string? CurrentUser()
{
// TODO: Implement — migrate logic from monolith
return Ok(new { service = "Customer", id });
var name = User.Identity?.Name;
if (string.IsNullOrEmpty(name))
return null;
return name.Length > 40 ? name[..40] : name;
}
}
3 changes: 1 addition & 2 deletions src/Services/Customer/Customer.API/Customer.API.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@
<ItemGroup>
<ProjectReference Include="..\Customer.Domain\Customer.Domain.csproj" />
<ProjectReference Include="..\Customer.Infrastructure\Customer.Infrastructure.csproj" />
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
<ProjectReference Include="..\..\Shared\Shared.Infrastructure\Shared.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.*" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.*" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.*" />
</ItemGroup>
Expand Down
2 changes: 0 additions & 2 deletions src/Services/Customer/Customer.API/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ WORKDIR /src
COPY ["Services/Customer/Customer.API/Customer.API.csproj", "Services/Customer/Customer.API/"]
COPY ["Services/Customer/Customer.Domain/Customer.Domain.csproj", "Services/Customer/Customer.Domain/"]
COPY ["Services/Customer/Customer.Infrastructure/Customer.Infrastructure.csproj", "Services/Customer/Customer.Infrastructure/"]
COPY ["Shared/Shared.Contracts/Shared.Contracts.csproj", "Shared/Shared.Contracts/"]
COPY ["Shared/Shared.Infrastructure/Shared.Infrastructure.csproj", "Shared/Shared.Infrastructure/"]
RUN dotnet restore "Services/Customer/Customer.API/Customer.API.csproj"
COPY . .
WORKDIR "/src/Services/Customer/Customer.API"
Expand Down
71 changes: 70 additions & 1 deletion src/Services/Customer/Customer.API/Program.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,93 @@
using System.Text;
using Customer.Infrastructure.Data;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddSwaggerGen(options =>
{
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Name = "Authorization",
Type = SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header,
Description = "Enter the JWT bearer token issued by the Identity service."
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" }
},
Array.Empty<string>()
}
});
});
builder.Services.AddHealthChecks();

builder.Services.AddDbContext<CustomerDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));

var jwtSection = builder.Configuration.GetSection("Jwt");
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.MapInboundClaims = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtSection["Issuer"],
ValidAudience = jwtSection["Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSection["Key"]!)),
NameClaimType = "name",
RoleClaimType = "role"
};
});
builder.Services.AddAuthorization();

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<CustomerDbContext>();
var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("Startup");

const int maxAttempts = 12;
for (var attempt = 1; ; attempt++)
{
try
{
db.Database.EnsureCreated();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 EnsureCreated used instead of EF Core migrations for schema management

The startup code at Program.cs:71 uses db.Database.EnsureCreated() which creates the database and schema if they don't exist, but will NOT update an existing database when the schema changes (e.g., new columns or entities are added). For a service that's expected to evolve, this means schema changes will silently not be applied, requiring manual intervention or a switch to db.Database.Migrate(). This may be acceptable for initial scaffolding but should be tracked for future work.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged and intentional for this PR's scope. EnsureCreated() is an explicitly-accepted option for bootstrapping this service's schema (the alternative, an EF migration applied at startup via Migrate(), is the preferred long-term approach). Switching to migrations means generating and committing a migrations assembly, which is broader than this carve-out. Tracking it as follow-up: replace EnsureCreated() with Database.Migrate() + an initial migration once the schema starts evolving.

break;
}
catch (Exception ex) when (attempt < maxAttempts)
{
logger.LogWarning(ex, "Database not ready (attempt {Attempt}/{Max}); retrying in 5s...", attempt, maxAttempts);
Thread.Sleep(TimeSpan.FromSeconds(5));
}
}
}
Comment on lines +67 to +80

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Thread.Sleep blocks the startup thread but is acceptable in this context

The database retry loop at src/Services/Customer/Customer.API/Program.cs:77 uses Thread.Sleep(TimeSpan.FromSeconds(5)) rather than await Task.Delay(...). Since this runs during application startup (before the app handles requests) and is in a synchronous top-level context, blocking the thread is acceptable. However, in containerized environments with health check probes on tight timeouts, a 60-second (12 × 5s) maximum block before the app starts could cause orchestrators (e.g., Kubernetes) to kill the pod. The /healthz endpoint won't respond until after this loop completes.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed it's acceptable here — it runs once at startup in the synchronous top-level context before the app serves traffic. The bound is 12 × 5s only in the pathological case where Postgres never becomes reachable; in practice it connects on the first retry (compose depends_on brings Postgres up first). Keeping Thread.Sleep for simplicity rather than introducing an async startup path. If we later add k8s liveness/readiness probes with tight timeouts, this loop would move behind a readiness gate.


if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();
app.MapHealthChecks("/healthz");

Expand Down
12 changes: 12 additions & 0 deletions src/Services/Customer/Customer.API/ViewModels/CustomerVM.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Customer.API.ViewModels;

public class CustomerVM
{
public int Id { get; set; }
public string? Name { get; set; }
public string? Email { get; set; }
public string? PhoneNumber { get; set; }
public string? Address { get; set; }
public string? City { get; set; }
public string? Gender { get; set; }
}
5 changes: 5 additions & 0 deletions src/Services/Customer/Customer.API/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,10 @@
},
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Database=customerdb;Username=postgres;Password=postgres"
},
"Jwt": {
"Key": "quickapp-dev-only-signing-key-change-me-please-32+chars",
"Issuer": "quickapp-identity",
"Audience": "quickapp"
}
}
Empty file.
18 changes: 18 additions & 0 deletions src/Services/Customer/Customer.Domain/Entities/BaseEntity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;

namespace Customer.Domain.Entities;

public class BaseEntity
{
public int Id { get; set; }

[MaxLength(40)]
public string? CreatedBy { get; set; }

[MaxLength(40)]
public string? UpdatedBy { get; set; }

public DateTime UpdatedDate { get; set; }

public DateTime CreatedDate { get; set; }
}
11 changes: 11 additions & 0 deletions src/Services/Customer/Customer.Domain/Entities/Customer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Customer.Domain.Entities;

public class Customer : BaseEntity
{
public required string Name { get; set; }
public required string Email { get; set; }
public string? PhoneNumber { get; set; }
public string? Address { get; set; }
public string? City { get; set; }
public Gender Gender { get; set; }
}
8 changes: 8 additions & 0 deletions src/Services/Customer/Customer.Domain/Entities/Gender.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Customer.Domain.Entities;

public enum Gender
{
None,
Female,
Male
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore;
using CustomerEntity = Customer.Domain.Entities.Customer;

namespace Customer.Infrastructure.Data;

Expand All @@ -8,9 +9,17 @@ public CustomerDbContext(DbContextOptions<CustomerDbContext> options) : base(opt
{
}

public DbSet<CustomerEntity> Customers => Set<CustomerEntity>();

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// TODO: Configure entity mappings migrated from monolith

modelBuilder.Entity<CustomerEntity>().Property(c => c.Name).IsRequired().HasMaxLength(100);
modelBuilder.Entity<CustomerEntity>().HasIndex(c => c.Name);
modelBuilder.Entity<CustomerEntity>().Property(c => c.Email).HasMaxLength(100);
modelBuilder.Entity<CustomerEntity>().Property(c => c.PhoneNumber).IsUnicode(false).HasMaxLength(30);
modelBuilder.Entity<CustomerEntity>().Property(c => c.City).HasMaxLength(50);
modelBuilder.Entity<CustomerEntity>().ToTable("AppCustomers");
}
}
Loading