Files
pos-system/services/ads-manager-service-net/SERVICE_DOCS.md
Ho Ngoc Hai f3779c4ebe docs: add SERVICE_DOCS.md for all 24 microservices from per-service code audit
Each SERVICE_DOCS.md documents: Overview, API Endpoints, Commands, Queries,
Domain Model, Database Schema, Integration Events, Dependencies, Configuration.
Generated by 23 parallel audit agents reading actual source code.

Key corrections from audit:
- inventory-service: 12 commands/6 queries (was listed as scaffold)
- promotion-service: 12 commands/10 queries (was listed as 0)
- mission-service: 4 commands/7 queries (was listed as 0)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 17:54:53 +07:00

26 KiB

AdsManagerService - Service Documentation

1. Overview

Purpose: Manages advertising campaigns, ad sets, and individual ads for the GoodGo platform. Follows a 3-tier ads structure: Campaign > Ad Set > Ad, similar to Meta/Facebook Ads architecture. Includes admin endpoints for ad review/moderation and reporting.

Port: 5011 (Development, via launchSettings.json), 8080 (Docker/Production)

Database: PostgreSQL - ads_manager_service (Neon cloud in appsettings.json)

Connection String Key: ConnectionStrings:DefaultConnection or DATABASE_URL environment variable

Framework: .NET 10.0, C# 14

Architecture: Clean Architecture (API / Domain / Infrastructure) + CQRS via MediatR

Health Endpoints:

  • GET /health - Full health check (includes PostgreSQL)
  • GET /health/live - Liveness probe (app is running)
  • GET /health/ready - Readiness probe (dependencies ready)

Auto-migration: EF Core migrations are applied automatically on startup.


2. API Endpoints

2.1 Campaigns Controller

Route prefix: api/v1/ads-manager/campaigns

Method Route Action Description Response
POST / CreateCampaign Create a new campaign 201 Created (Guid)
GET / ListCampaigns List campaigns with filtering and pagination 200 OK (ListCampaignsResult)
GET /{id} GetCampaignById Get campaign by ID 200 OK (CampaignDto) / 404
PUT /{id} UpdateCampaign Update campaign name and description 204 No Content / 404
POST /{id}/activate ActivateCampaign Activate a draft/paused campaign 204 No Content / 404
POST /{id}/pause PauseCampaign Pause an active campaign 204 No Content / 404
DELETE /{id} DeleteCampaign Archive a campaign (soft delete) 204 No Content / 404

ListCampaigns Query Parameters:

  • advertiserId (Guid?) - Filter by advertiser
  • status (string?) - Filter by status (draft, active, paused, completed, archived)
  • objective (string?) - Filter by objective (awareness, traffic, conversion, etc.)
  • searchTerm (string?) - Search by campaign name
  • page (int, default: 1) - Page number
  • pageSize (int, default: 20) - Page size

2.2 Ad Sets Controller

Route prefix: api/v1/ads-manager/adsets

Method Route Action Description Response
POST / CreateAdSet Create a new ad set 201 Created (Guid)
GET /{id} GetAdSetById Get ad set by ID 200 OK (AdSetDto) / 404

2.3 Ads Controller

Route prefix: api/v1/ads-manager/ads

Method Route Action Description Response
POST / CreateAd Create a new ad 201 Created (Guid)
GET /{id} GetAdById Get ad by ID 200 OK (AdDto) / 404
POST /{id}/submit SubmitAdForReview Submit ad for review 204 No Content / 404

2.4 Audiences Controller

Route prefix: api/v1/ads-manager/audiences

Method Route Action Description Response
GET / ListAudiences List audiences for an advertiser 200 OK (List<AudienceDto>)
GET /{id} GetAudienceById Get audience by ID 200 OK (AudienceDto) / 404

ListAudiences Query Parameters:

  • advertiserId (Guid) - Required. Filter by advertiser.

Note: The Audiences controller defines its own queries and DTOs inline (ListAudiencesQuery, GetAudienceByIdQuery, AudienceDto) but there are NO handler implementations for these queries in the codebase. These endpoints will fail at runtime.

2.5 Admin Ads Controller

Route prefix: api/v1/admin/ads-manager/ads

Method Route Action Description Response
GET /pending ListPendingAds List ads pending review 200 OK (List<AdDto>)
POST /{id}/approve ApproveAd Approve an ad 204 No Content / 404
POST /{id}/reject RejectAd Reject an ad with reason 204 No Content / 404

ListPendingAds Query Parameters:

  • page (int, default: 1)
  • pageSize (int, default: 20)

RejectAd Request Body: { "reason": "string" }

2.6 Admin Campaigns Controller

Route prefix: api/v1/admin/ads-manager/campaigns

Method Route Action Description Response
GET / ListAllCampaigns List all campaigns across all advertisers 200 OK (ListCampaignsResult)
GET /stats GetCampaignStats Get aggregate campaign statistics 200 OK (CampaignStatsDto)

ListAllCampaigns Query Parameters:

  • status (string?)
  • page (int, default: 1)
  • pageSize (int, default: 50)

2.7 Admin Reports Controller

Route prefix: api/v1/admin/ads-manager/reports

Method Route Action Description Response
GET /top-advertisers GetTopAdvertisers Get top advertisers by spend 200 OK (List<TopAdvertiserDto>)
GET /revenue GetRevenueAnalytics Get revenue analytics 200 OK (RevenueAnalyticsDto)

GetTopAdvertisers Query Parameters:

  • limit (int, default: 10)

GetRevenueAnalytics Query Parameters:

  • startDate (DateTime?)
  • endDate (DateTime?)

3. Commands

CreateCampaignCommand

  • Returns: Guid (new campaign ID)
  • Parameters: AdvertiserId (Guid), Name (string), Description (string?), Objective (string: "awareness"/"traffic"/"conversion"/"app_installs"/"video_views"/"lead_generation"), BudgetType (string: "daily"/"lifetime"), BudgetAmount (decimal), Currency (string, default "VND"), StartDate (DateTime?), EndDate (DateTime?)
  • Behavior: Creates a Campaign aggregate with Draft status, sets schedule if dates provided, raises CampaignCreatedDomainEvent.

UpdateCampaignCommand

  • Returns: bool
  • Parameters: CampaignId (Guid), Name (string), Description (string?)
  • Behavior: Updates campaign name and description. Rejects if campaign is archived.

ActivateCampaignCommand

  • Returns: bool
  • Parameters: CampaignId (Guid)
  • Behavior: Transitions campaign from Draft/Paused to Active. Raises CampaignActivatedDomainEvent.

PauseCampaignCommand

  • Returns: bool
  • Parameters: CampaignId (Guid)
  • Behavior: Transitions campaign from Active to Paused. Raises CampaignStatusChangedDomainEvent. Uses injected IUnitOfWork (not repository UnitOfWork).

DeleteCampaignCommand

  • Returns: bool
  • Parameters: CampaignId (Guid)
  • Behavior: Soft-deletes by archiving the campaign. Cannot archive active campaigns (must pause first). Raises CampaignStatusChangedDomainEvent.

CreateAdSetCommand

  • Returns: Guid (new ad set ID)
  • Parameters: CampaignId (Guid), Name (string), DailyBudget (decimal), BidType (string: "cpc"/"cpm"/"ocpm"/"automatic", default "cpc"), BidAmount (decimal?), MinAge (int?), MaxAge (int?), Genders (string?), Locations (string?), Interests (string?)
  • Behavior: Creates an AdSet with targeting and bid strategy, in Draft status.

CreateAdCommand

  • Returns: Guid (new ad ID)
  • Parameters: AdSetId (Guid), Name (string), Format (string: "single_image"/"single_video"/"carousel"/"collection"/"stories", default "single_image"), Headline (string?), PrimaryText (string?), CallToAction (string?), DestinationUrl (string?), CreativeUrl (string?)
  • Behavior: Creates an Ad in Draft status with ReviewStatus=NotSubmitted.

SubmitAdForReviewCommand

  • Returns: bool
  • Parameters: AdId (Guid)
  • Behavior: Transitions ad review status from NotSubmitted/Rejected to PendingReview.

ApproveAdCommand (defined in AdminAdsController)

  • Returns: bool
  • Parameters: AdId (Guid)
  • Behavior: Transitions ad review status from PendingReview to Approved.

RejectAdCommand (defined in AdminAdsController)

  • Returns: bool
  • Parameters: AdId (Guid), Reason (string)
  • Behavior: Transitions ad review status from PendingReview to Rejected.

4. Queries

GetCampaignByIdQuery

  • Parameters: CampaignId (Guid)
  • Returns: CampaignDto? (Id, AdvertiserId, Name, Description, Status, Objective, BudgetType, BudgetAmount, Currency, TotalSpend, StartDate, EndDate, CreatedAt, UpdatedAt)
  • Handler: Fetches from ICampaignRepository.

ListCampaignsQuery

  • Parameters: AdvertiserId (Guid?), Status (string?), Objective (string?), SearchTerm (string?), Page (int), PageSize (int)
  • Returns: ListCampaignsResult (Items: List<CampaignDto>, TotalCount, Page, PageSize, TotalPages)
  • Handler: Queries AdsManagerServiceContext directly with filters, pagination, ordered by CreatedAt DESC.

GetAdByIdQuery

  • Parameters: AdId (Guid)
  • Returns: AdDto? (Id, AdSetId, Name, Format, Status, ReviewStatus, Headline, PrimaryText, Description, CallToAction, DestinationUrl, CreativeUrl, CreatedAt, UpdatedAt)
  • Handler: Queries AdsManagerServiceContext directly.

GetAdSetByIdQuery

  • Parameters: AdSetId (Guid)
  • Returns: AdSetDto? (Id, CampaignId, Name, Status, DailyBudget, BidType, BidAmount, StartDate, EndDate, Targeting: TargetingDto, CreatedAt, UpdatedAt)
  • Handler: Queries AdsManagerServiceContext directly. Splits targeting comma-separated strings into lists.

GetCampaignStatsQuery

  • Parameters: None
  • Returns: CampaignStatsDto (TotalCampaigns, ActiveCampaigns, PausedCampaigns, DraftCampaigns, CompletedCampaigns, TotalSpend, TotalBudget)
  • Handler: Loads ALL campaigns into memory and counts in-memory. Warning: potential performance issue with large datasets.

ListPendingAdsQuery (defined in AdminAdsController)

  • Parameters: Page (int), PageSize (int)
  • Returns: List<AdDto>
  • Handler: Filters ads where ReviewStatus.Name == "Pending", ordered by CreatedAt ASC.

GetTopAdvertisersQuery (defined in AdminReportsController)

  • Parameters: Limit (int, default: 10)
  • Returns: List<TopAdvertiserDto> (AdvertiserId, TotalCampaigns, TotalSpend, ActiveCampaigns)
  • Handler: Groups campaigns by AdvertiserId, ordered by TotalSpend DESC.

GetRevenueAnalyticsQuery (defined in AdminReportsController)

  • Parameters: StartDate (DateTime?), EndDate (DateTime?)
  • Returns: RevenueAnalyticsDto (TotalRevenue, AverageRevenuePerCampaign, TotalCampaigns, RevenueByObjective: Dictionary)
  • Handler: Loads campaigns filtered by date range, groups revenue by objective.

ListAudiencesQuery (defined in AudiencesController)

  • Parameters: AdvertiserId (Guid)
  • Returns: List<AudienceDto>
  • Handler: NOT IMPLEMENTED - no handler exists in the codebase.

GetAudienceByIdQuery (defined in AudiencesController)

  • Parameters: AudienceId (Guid)
  • Returns: AudienceDto?
  • Handler: NOT IMPLEMENTED - no handler exists in the codebase.

5. Domain Model

Aggregates

Campaign Aggregate (CampaignAggregate/)

  • Campaign (Entity, IAggregateRoot)

    • Fields: _name, _description, _advertiserId, _status (CampaignStatus), _objective (CampaignObjective), _budget (CampaignBudget), _startDate, _endDate, _createdAt, _updatedAt, TotalSpend
    • Public IDs: StatusId, ObjectiveId
    • Behavior methods: Update(), SetSchedule(), SetBudget(), Activate(), Pause(), Complete(), Archive(), RecordSpend()
    • State machine: Draft -> Active <-> Paused -> Completed; Draft/Paused/Completed -> Archived (Active cannot be directly archived)
    • Domain events: CampaignCreatedDomainEvent (on create), CampaignActivatedDomainEvent (on activate), CampaignStatusChangedDomainEvent (on pause/complete/archive)
  • CampaignStatus (Enumeration)

    • Values: Draft (1), Active (2), Paused (3), Completed (4), Archived (5)
    • Names stored as lowercase: "draft", "active", "paused", "completed", "archived"
  • CampaignObjective (Enumeration)

    • Values: Awareness (1, "awareness"), Traffic (2, "traffic"), Conversion (3, "conversion"), AppInstalls (4, "app_installs"), VideoViews (5, "video_views"), LeadGeneration (6, "lead_generation")
  • CampaignBudget (ValueObject)

    • Properties: Type (BudgetType enum), Amount (decimal), Currency (string, default "VND")
    • Factory methods: Daily(), Lifetime()
  • BudgetType (enum): Daily (1), Lifetime (2)

  • ICampaignRepository: Add(), Update(), GetByIdAsync(), GetByAdvertiserIdAsync(), GetActiveAsync()

AdSet Aggregate (AdSetAggregate/)

  • AdSet (Entity, IAggregateRoot)

    • Fields: _name, _campaignId, _status (AdSetStatus), _targeting (Targeting), _bidStrategy (BidStrategy), _dailyBudget, _startDate, _endDate, _createdAt, _updatedAt
    • Public ID: StatusId
    • Behavior methods: Update(), SetTargeting(), SetBidStrategy(), Activate(), Pause()
    • State machine: Draft -> Active <-> Paused
  • AdSetStatus (Enumeration): Draft (1), Active (2), Paused (3), Archived (4)

  • Targeting (ValueObject)

    • Properties: MinAge (int?), MaxAge (int?), Genders (string?), Locations (string?), Interests (string?), CustomAudienceIds (string?), LookalikeAudienceIds (string?)
    • All list-type fields stored as comma-separated strings.
    • Factory methods: Broad(), Demographic()
  • BidStrategy (ValueObject)

    • Properties: Type (BidType enum), BidAmount (decimal?), TargetCost (decimal?)
    • Factory methods: CPC(), CPM(), OCPM(), Automatic()
  • BidType (enum): CPC (1), CPM (2), OCPM (3), TargetCost (4), Automatic (5)

  • IAdSetRepository: Add(), Update(), GetByIdAsync(), GetByCampaignIdAsync()

Ad Aggregate (AdAggregate/)

  • Ad (Entity, IAggregateRoot)

    • Fields: _name, _adSetId, _format (AdFormat), _status (AdStatus), _reviewStatus (AdReviewStatus), _headline, _primaryText, _description, _callToAction, _destinationUrl, _creativeUrl, _createdAt, _updatedAt
    • Public IDs: FormatId, StatusId, ReviewStatusId
    • Behavior methods: Update(), SetCreative(), SubmitForReview(), Approve(), Reject(), Activate(), Pause()
    • Review state machine: NotSubmitted -> PendingReview -> Approved/Rejected; Approved resets to PendingReview if content updated
    • Status state machine: Draft -> Active <-> Paused (activation requires Approved review status)
  • AdFormat (Enumeration): SingleImage (1, "single_image"), SingleVideo (2, "single_video"), Carousel (3, "carousel"), Collection (4, "collection"), Stories (5, "stories")

  • AdStatus (Enumeration): Draft (1), Active (2), Paused (3), Archived (4)

  • AdReviewStatus (Enumeration): NotSubmitted (1, "not_submitted"), PendingReview (2, "pending_review"), Approved (3, "approved"), Rejected (4, "rejected")

  • IAdRepository: Add(), Update(), GetByIdAsync()

Audience Aggregate (AudienceAggregate/)

  • CustomAudience (Entity, IAggregateRoot)

    • Fields: _name, _advertiserId, _source (AudienceSource), _size, _createdAt, _updatedAt
    • Public ID: SourceId
    • Behavior methods: UpdateSize()
  • LookalikeAudience (Entity, IAggregateRoot)

    • Fields: _name, _advertiserId, _sourceAudienceId, _similarityPercentage (1-10), _location, _size, _createdAt
    • Behavior methods: UpdateSize()
  • AudienceSource (Enumeration): CustomerList (1, "customer_list"), WebsiteVisitors (2, "website_visitors"), AppUsers (3, "app_users"), EngagementCustomers (4, "engagement_customers")

Note: No repository interface or implementation exists for Audience aggregates. No EF configuration exists for Audience entities. The migration creates basic CustomAudiences and LookalikeAudiences tables with minimal columns (only Id and SourceId for CustomAudience, only Id for LookalikeAudience) -- these are incomplete.

SeedWork (Base Classes)

  • Entity: Base class with Guid Id, DomainEvents collection, equality by ID
  • IAggregateRoot: Marker interface
  • IRepository<T>: Generic interface with IUnitOfWork UnitOfWork
  • IUnitOfWork: SaveChangesAsync(), SaveEntitiesAsync()
  • ValueObject: Equality by component values
  • Enumeration: Type-safe enum with int Id, string Name, parsing methods

Exceptions

  • DomainException: Base exception for domain errors
  • AdsDomainException: Ads-specific domain exception (extends Exception, not DomainException)

Domain Events

  • CampaignCreatedDomainEvent: Contains Campaign reference. Raised when a campaign is created.
  • CampaignActivatedDomainEvent: Contains CampaignId, PreviousStatus, NewStatus. Raised when a campaign is activated.
  • CampaignStatusChangedDomainEvent: Contains CampaignId, PreviousStatus, NewStatus. Raised on pause, complete, or archive.

Note: No domain event handlers are implemented. Events are dispatched by DbContext but not consumed.


6. Database Schema

Database: ads_manager_service (PostgreSQL)

Migration: 20260117171043_InitialCreate

Table: campaigns

Column Type Nullable Constraints
id uuid NOT NULL PK
name varchar(255) NOT NULL
description varchar(1000) NULL
advertiser_id uuid NOT NULL
status_id integer NOT NULL
objective_id integer NOT NULL
budget_type integer NOT NULL Owned (CampaignBudget)
budget_amount numeric(18,2) NOT NULL Owned (CampaignBudget)
currency varchar(10) NOT NULL Owned (CampaignBudget)
start_date timestamp with time zone NULL
end_date timestamp with time zone NULL
total_spend numeric(18,2) NOT NULL Default: 0
created_at timestamp with time zone NOT NULL
updated_at timestamp with time zone NULL

Indexes:

  • idx_campaigns_advertiser_id on advertiser_id
  • idx_campaigns_status_id on status_id
  • idx_campaigns_created_at on created_at

Table: ad_sets

Column Type Nullable Constraints
id uuid NOT NULL PK
name varchar(255) NOT NULL
campaign_id uuid NOT NULL
status_id integer NOT NULL
target_min_age integer NULL Owned (Targeting)
target_max_age integer NULL Owned (Targeting)
target_genders varchar(50) NULL Owned (Targeting)
target_locations varchar(500) NULL Owned (Targeting)
target_interests varchar(500) NULL Owned (Targeting)
custom_audience_ids varchar(500) NULL Owned (Targeting)
lookalike_audience_ids varchar(500) NULL Owned (Targeting)
bid_type integer NOT NULL Owned (BidStrategy)
bid_amount numeric(18,6) NULL Owned (BidStrategy)
target_cost numeric(18,6) NULL Owned (BidStrategy)
daily_budget numeric(18,2) NOT NULL
start_date timestamp with time zone NULL
end_date timestamp with time zone NULL
created_at timestamp with time zone NOT NULL
updated_at timestamp with time zone NULL

Indexes:

  • idx_ad_sets_campaign_id on campaign_id
  • idx_ad_sets_status_id on status_id

Note: No foreign key to campaigns table is defined.

Table: ads

Column Type Nullable Constraints
id uuid NOT NULL PK
name varchar(255) NOT NULL
ad_set_id uuid NOT NULL
format_id integer NOT NULL
status_id integer NOT NULL
review_status_id integer NOT NULL
headline varchar(255) NULL
primary_text varchar(1000) NULL
description varchar(500) NULL
call_to_action varchar(50) NULL
destination_url varchar(2048) NULL
creative_url varchar(2048) NULL
created_at timestamp with time zone NOT NULL
updated_at timestamp with time zone NULL

Indexes:

  • idx_ads_ad_set_id on ad_set_id
  • idx_ads_status_id on status_id
  • idx_ads_review_status_id on review_status_id

Note: No foreign key to ad_sets table is defined.

Table: CustomAudiences (incomplete)

Column Type Nullable
Id uuid NOT NULL (PK)
SourceId integer NOT NULL

Note: No EF configuration exists for this table. Migration only creates minimal columns. Most domain entity fields (Name, AdvertiserId, Size, CreatedAt, UpdatedAt) are not mapped.

Table: LookalikeAudiences (incomplete)

Column Type Nullable
Id uuid NOT NULL (PK)

Note: No EF configuration exists. Only Id column in migration. All other domain fields unmapped.


7. Integration Events

Published Domain Events (dispatched via MediatR before SaveChanges):

  • CampaignCreatedDomainEvent - When a campaign is created
  • CampaignActivatedDomainEvent - When a campaign is activated
  • CampaignStatusChangedDomainEvent - When campaign status changes (pause, complete, archive)

Cross-service Integration Events: None implemented. No RabbitMQ integration. No event publishers or consumers for cross-service communication.

Idempotency: IRequestManager / RequestManager infrastructure exists for duplicate request detection, but it is not currently used by any command handler.


8. Dependencies

NuGet Packages

API Layer (AdsManagerService.API.csproj):

  • MediatR 12.4.1
  • FluentValidation 11.11.0
  • FluentValidation.DependencyInjectionExtensions 11.11.0
  • Microsoft.EntityFrameworkCore.Design 10.0.2
  • Swashbuckle.AspNetCore 7.2.0
  • Asp.Versioning.Mvc 8.1.0
  • Asp.Versioning.Mvc.ApiExplorer 8.1.0
  • AspNetCore.HealthChecks.NpgSql 8.0.2
  • AspNetCore.HealthChecks.Redis 8.0.1
  • Hellang.Middleware.ProblemDetails 6.5.1
  • Serilog.AspNetCore 8.0.3
  • Serilog.Sinks.Console 6.0.0
  • Serilog.Sinks.Seq 8.0.0

Domain Layer (AdsManagerService.Domain.csproj):

  • MediatR.Contracts 2.0.1

Infrastructure Layer (AdsManagerService.Infrastructure.csproj):

  • Microsoft.EntityFrameworkCore 10.0.0
  • Npgsql.EntityFrameworkCore.PostgreSQL 10.0.0
  • Microsoft.EntityFrameworkCore.Tools 10.0.0
  • MediatR 12.4.1
  • Dapper 2.1.35
  • Microsoft.Extensions.Http.Polly 9.0.0
  • Polly 8.5.0
  • StackExchange.Redis 2.8.16

Test Projects:

  • xunit 2.9.2
  • FluentAssertions 6.12.2
  • Moq 4.20.72 (UnitTests only)
  • Microsoft.AspNetCore.Mvc.Testing 10.0.0 (FunctionalTests)
  • Microsoft.EntityFrameworkCore.InMemory 10.0.0 (FunctionalTests)
  • Testcontainers.PostgreSql 4.1.0 (FunctionalTests)
  • coverlet.collector 6.0.2

External Service Dependencies

  • PostgreSQL: Primary database (Neon cloud or local)
  • Redis: Connection string configured but not actively used in code (health check registered)

Note: No authentication/authorization middleware is configured. No JWT validation. All endpoints are unauthenticated.


9. Configuration

appsettings.json

{
  "ConnectionStrings": {
    "DefaultConnection": "<neon-postgresql-connection-string>"
  },
  "Redis": {
    "ConnectionString": "localhost:6379"
  },
  "Jwt": {
    "Secret": "your-super-secret-key-min-32-characters",
    "Issuer": "goodgo-platform",
    "Audience": "goodgo-services",
    "AccessTokenExpiryMinutes": 15,
    "RefreshTokenExpiryDays": 7
  },
  "Serilog": { ... }
}

Note: JWT settings are present in configuration but JWT authentication middleware is NOT registered in Program.cs. These settings are unused.

Environment Variables (.env.example)

Variable Description Default
ASPNETCORE_ENVIRONMENT Runtime environment Development
DATABASE_URL PostgreSQL connection string (fallback) -
REDIS_URL Redis connection localhost:6379
JWT_SECRET JWT signing key -
JWT_ISSUER JWT issuer goodgo-platform
JWT_AUDIENCE JWT audience goodgo-services
API_PORT API port 5000
LOG_LEVEL Minimum log level Information
SEQ_URL Seq logging endpoint http://localhost:5341

Docker Configuration

  • Base image: mcr.microsoft.com/dotnet/aspnet:10.0
  • Build image: mcr.microsoft.com/dotnet/sdk:10.0
  • Port: 8080
  • User: dotnetuser (UID 1001, GID 1001) - non-root
  • Health check: curl -f http://localhost:8080/health/live (30s interval, 3 retries)

MediatR Pipeline Behaviors (registered in order)

  1. LoggingBehavior - Logs request name and elapsed time (with Stopwatch)
  2. ValidatorBehavior - Runs FluentValidation validators (throws ValidationException on failure)
  3. TransactionBehavior - Wraps commands in a database transaction (skips queries based on type name ending in "Query")

Database Connection Resolution Order

  1. ConnectionStrings:DefaultConnection from appsettings
  2. DATABASE_URL environment variable
  3. Throws InvalidOperationException if neither is configured

Npgsql Resilience

  • Retry on failure: max 5 retries, max 30s delay

10. Known Issues and Gaps

  1. No Authentication: No JWT/Bearer auth middleware registered. All endpoints are publicly accessible.
  2. Audience handlers missing: ListAudiencesQuery and GetAudienceByIdQuery have no handler implementations. Endpoints will throw at runtime.
  3. Audience tables incomplete: CustomAudiences and LookalikeAudiences tables in migration have minimal columns. No EF configurations exist.
  4. No foreign keys: Campaign -> AdSet -> Ad relationships have no FK constraints in the database.
  5. No FluentValidation validators: No validator classes exist for any command.
  6. No domain event handlers: Domain events are dispatched but no handlers consume them.
  7. No cross-service events: No RabbitMQ/message broker integration.
  8. Idempotency unused: IRequestManager is registered but never called by any handler.
  9. Dapper unused: Dapper is referenced but no raw SQL queries exist.
  10. Redis unused: Redis package included and health check registered, but no caching logic implemented.
  11. Unit tests empty: The UnitTests project has no test files.
  12. Functional tests minimal: Only 2 tests (list campaigns returns 200, health check returns 200).
  13. GetCampaignStatsQuery: Loads ALL campaigns into memory before counting -- no SQL-level aggregation.
  14. AdsDomainException: Extends Exception directly, not DomainException. Inconsistent hierarchy.
  15. ListPendingAdsQuery handler: Filters by ReviewStatus.Name == "Pending" but the actual enum name is "pending_review" -- potential mismatch depending on EF query translation.