Skip to content

Service Model in ConnectSoft ConnectSoft Templates

Purpose & Overview

The Service Model represents the API boundary layer in the ConnectSoft ConnectSoft Templates, providing transport-agnostic Data Transfer Objects (DTOs) and service contracts that define how clients interact with the microservice. It serves as the contract layer between external clients and the application domain, maintaining strict separation of concerns and enabling multiple service protocols (REST, gRPC, CoreWCF, GraphQL, Azure Functions).

Key Principles

The service model follows these core principles:

  • API Boundary Isolation: Service models are completely separate from domain models, preventing internal structure leakage
  • Transport Agnostic: DTOs can be used across multiple service protocols (REST, gRPC, SOAP, GraphQL)
  • Validation: DataAnnotations provide structural validation at the API boundary
  • Mapping: AutoMapper transforms between ServiceModel DTOs and DomainModel Input/Output models
  • Versioning Support: Service contracts can evolve independently while maintaining backward compatibility
  • Contract-First: Service interfaces and DTOs define the API contract before implementation

Service Model Philosophy

The Service Model is the public face of ConnectSoft templates—it defines what clients see and how they interact with the application. By keeping service models separate from domain models, we maintain flexibility to evolve APIs independently, support multiple protocols, and prevent internal implementation details from leaking to clients.

Architecture Overview

Service Model Position in Clean Architecture

External Clients
    ├── REST Client
    ├── gRPC Client
    ├── SOAP Client
    └── GraphQL Client
Service Model Layer (ServiceModel)
    ├── Request DTOs
    ├── Response DTOs
    ├── Service Interfaces
    └── DTOs (Domain Entity Representations)
    ↓ (AutoMapper)
Domain Model Layer (DomainModel)
    ├── Input Models
    ├── Output Models
    └── Processors/Retrievers
Domain Layer (EntityModel)
    └── Aggregates/Entities

Project Structure

ConnectSoft.{TemplateName}.ServiceModel/
├── Request DTOs/
│   ├── CreateAggregateRootRequest.cs
│   ├── GetAggregateRootDetailsRequest.cs
│   ├── DeleteAggregateRootRequest.cs
│   └── FeatureAUseCaseARequest.cs
├── Response DTOs/
│   ├── CreateAggregateRootResponse.cs
│   ├── GetAggregateRootDetailsResponse.cs
│   └── FeatureAUseCaseAResponse.cs
├── DTOs/
│   └── AggregateRootDto.cs
├── Service Interfaces/
│   ├── IAggregateRootQueryService.cs
│   ├── IAggregateRootProcessService.cs
│   └── IFeatureAService.cs
└── Constants/
    └── ServiceModelConstants.cs

Service Model Integration Points

Layer Component Relationship
API Controllers REST/gRPC/CoreWCF/GraphQL Consume ServiceModel DTOs
AutoMapper Mapping Profiles Maps ServiceModel ↔ DomainModel
DomainModel Input/Output Models Target of ServiceModel mappings
Validation DataAnnotations Validates ServiceModel DTOs

Request DTOs

Purpose

Request DTOs represent the data that clients send to the microservice. They are validated at the API boundary using DataAnnotations and mapped to DomainModel Input models using AutoMapper.

Request DTO Structure

// CreateAggregateRootRequest.cs
namespace ConnectSoft.Template.ServiceModel
{
    using System;
    using System.ComponentModel.DataAnnotations;
    using System.ServiceModel;
    using ConnectSoft.Extensions.DataAnnotations;

    /// <summary>
    /// Create AggregateRoot message contract.
    /// </summary>
    [MessageContract(
        IsWrapped = true,
        WrapperNamespace = ServiceModelConstants.BaseNamespace + ServiceModelConstants.MessagesNamespace)]
    public class CreateAggregateRootRequest
    {
        /// <summary>
        /// Gets or sets a AggregateRoot identifier.
        /// </summary>
        /// <example><code>D93CBE8A-0E08-40E2-AA56-4B78378AD691</code></example>
        [MessageBodyMember(
            Namespace = ServiceModelConstants.BaseNamespace + ServiceModelConstants.MessagesNamespace, 
            Name = nameof(ObjectId))]
        [Required]
        [NotDefault]
        required public Guid ObjectId { get; set; }
    }
}

Key Characteristics: - Validation Attributes: Uses [Required], [NotDefault], and other DataAnnotations - Message Contracts: Decorated with [MessageContract] for SOAP/CoreWCF support - Documentation: XML comments for Swagger/OpenAPI generation - Transport Agnostic: Can be used in REST, gRPC, SOAP, and other protocols

Query Request Example

// GetAggregateRootDetailsRequest.cs
namespace ConnectSoft.Template.ServiceModel
{
    using System;
    using System.ComponentModel.DataAnnotations;
    using System.ServiceModel;
    using ConnectSoft.Extensions.DataAnnotations;

    /// <summary>
    /// Get AggregateRoot message contract.
    /// </summary>
    [MessageContract(
        IsWrapped = true,
        WrapperNamespace = ServiceModelConstants.BaseNamespace + ServiceModelConstants.MessagesNamespace)]
    public class GetAggregateRootDetailsRequest
    {
        /// <summary>
        /// Gets or sets a AggregateRoot identifier.
        /// </summary>
        /// <example><code>D93CBE8A-0E08-40E2-AA56-4B78378AD691</code></example>
        [MessageBodyMember(
            Namespace = ServiceModelConstants.BaseNamespace + ServiceModelConstants.MessagesNamespace, 
            Name = nameof(ObjectId))]
        [Required]
        [NotDefault]
        required public Guid ObjectId { get; set; }
    }
}

Request DTO Best Practices

  1. Use Descriptive Names

    // ✅ GOOD - Clear intent
    CreateAggregateRootRequest
    
    // ❌ BAD - Ambiguous
    CreateRequest
    

  2. Include Validation Attributes

    // ✅ GOOD - Validated at boundary
    [Required]
    [NotDefault]
    required public Guid ObjectId { get; set; }
    

  3. Provide XML Documentation

    // ✅ GOOD - Self-documenting
    /// <summary>
    /// Gets or sets a AggregateRoot identifier.
    /// </summary>
    /// <example><code>D93CBE8A-0E08-40E2-AA56-4B78378AD691</code></example>
    public Guid ObjectId { get; set; }
    

  4. Use Appropriate Data Types

    // ✅ GOOD - Strong typing
    required public Guid ObjectId { get; set; }
    required public DateTimeOffset CreatedOn { get; set; }
    
    // ❌ BAD - Weak typing
    public string ObjectId { get; set; }  // Should be Guid
    

Response DTOs

Purpose

Response DTOs represent the data that the microservice returns to clients. They wrap domain entities (via DTOs) and provide a stable API contract.

Response DTO Structure

// CreateAggregateRootResponse.cs
namespace ConnectSoft.Template.ServiceModel
{
    using System.ServiceModel;

    /// <summary>
    /// Create AggregateRoot response message contract.
    /// </summary>
    [MessageContract(
        IsWrapped = true,
        WrapperNamespace = ServiceModelConstants.BaseNamespace + ServiceModelConstants.MessagesNamespace)]
    public class CreateAggregateRootResponse
    {
        /// <summary>
        /// Gets or sets a created AggregateRoot data transfer object.
        /// </summary>
        [MessageBodyMember(
            Namespace = ServiceModelConstants.BaseNamespace + ServiceModelConstants.MessagesNamespace, 
            Name = nameof(CreatedAggregateRoot))]
        required public AggregateRootDto CreatedAggregateRoot { get; set; }
    }
}

Key Characteristics: - Wraps DTOs: Contains domain entity DTOs, not domain entities directly - Message Contracts: Decorated for SOAP/CoreWCF support - Stable Contract: API contract remains stable even if domain model changes - Multiple Protocols: Can be serialized for REST (JSON), gRPC (protobuf), SOAP (XML)

Query Response Example

// GetAggregateRootDetailsResponse.cs
namespace ConnectSoft.Template.ServiceModel
{
    using System.ServiceModel;

    /// <summary>
    /// Get AggregateRoot details response message contract.
    /// </summary>
    [MessageContract(
        IsWrapped = true,
        WrapperNamespace = ServiceModelConstants.BaseNamespace + ServiceModelConstants.MessagesNamespace)]
    public class GetAggregateRootDetailsResponse
    {
        /// <summary>
        /// Gets or sets a found AggregateRoot data transfer object.
        /// </summary>
        [MessageBodyMember(
            Namespace = ServiceModelConstants.BaseNamespace + ServiceModelConstants.MessagesNamespace, 
            Name = nameof(FoundAggregateRoot))]
        required public AggregateRootDto FoundAggregateRoot { get; set; }
    }
}

Domain Entity DTOs

Purpose

Domain Entity DTOs represent domain entities in a format suitable for API consumption. They expose only the necessary fields and hide internal domain structure.

DTO Structure

// AggregateRootDto.cs
namespace ConnectSoft.Template.ServiceModel
{
    using System;
    using System.Runtime.Serialization;

    /// <summary>
    /// AggregateRoot data transfer object/contract.
    /// </summary>
    [DataContract(
        Namespace = ServiceModelConstants.BaseNamespace + ServiceModelConstants.DataContractsNamespace,
        Name = nameof(AggregateRootDto))]
    public class AggregateRootDto
    {
        /// <summary>
        /// Gets or sets a AggregateRoot's id.
        /// </summary>
        [DataMember(Order = 1)]
        public Guid ObjectId { get; set; }
    }
}

Key Characteristics: - DataContract: Decorated with [DataContract] and [DataMember] for serialization - Ordered Members: [DataMember(Order = n)] ensures consistent serialization - Minimal Exposure: Only exposes fields needed by clients - Versioning Support: Can evolve independently from domain entities

DTO Best Practices

  1. Expose Only Necessary Fields

    // ✅ GOOD - Only public API fields
    [DataMember(Order = 1)]
    public Guid ObjectId { get; set; }
    
    [DataMember(Order = 2)]
    public string Name { get; set; }
    
    // ❌ BAD - Internal fields exposed
    public string InternalSecretKey { get; set; }  // Never!
    

  2. Use Ordered DataMembers

    // ✅ GOOD - Explicit ordering
    [DataMember(Order = 1)]
    public Guid ObjectId { get; set; }
    
    [DataMember(Order = 2)]
    public string Name { get; set; }
    

  3. Provide XML Documentation

    // ✅ GOOD - Documented properties
    /// <summary>
    /// Gets or sets a AggregateRoot's id.
    /// </summary>
    public Guid ObjectId { get; set; }
    

Service Interfaces

Purpose

Service interfaces define the contract for service operations, enabling protocol-agnostic service definitions that can be implemented by REST controllers, gRPC services, CoreWCF services, or GraphQL resolvers.

Query Service Interface

// IAggregateRootQueryService.cs
namespace ConnectSoft.Template.ServiceModel
{
    using System.Threading.Tasks;

    /// <summary>
    /// AggregateRoot query service contract.
    /// </summary>
    public interface IAggregateRootQueryService
    {
        /// <summary>
        /// Gets AggregateRoot details.
        /// </summary>
        Task<GetAggregateRootDetailsResponse> GetAggregateRootDetails(
            GetAggregateRootDetailsRequest request);
    }
}

Process Service Interface

// IAggregateRootProcessService.cs
namespace ConnectSoft.Template.ServiceModel
{
    using System.Threading.Tasks;

    /// <summary>
    /// AggregateRoot process service contract.
    /// </summary>
    public interface IAggregateRootProcessService
    {
        /// <summary>
        /// Process, create and store a AggregateRoots.
        /// </summary>
        Task<CreateAggregateRootResponse> CreateAggregateRoot(
            CreateAggregateRootRequest request);

        /// <summary>
        /// Delete a given AggregateRoot.
        /// </summary>
        Task<DeleteAggregateRootResponse> DeleteAggregateRoot(
            DeleteAggregateRootRequest request);
    }
}

Benefits: - Protocol Independence: Same interface can be implemented for REST, gRPC, SOAP - Contract Definition: Clear contract for service operations - Testability: Easy to mock for testing - CQRS Alignment: Separates query and command operations

AutoMapper Integration

Purpose

AutoMapper provides automatic mapping between ServiceModel DTOs and DomainModel Input/Output models, eliminating boilerplate mapping code and ensuring consistent transformations.

Mapping Profile

// MicroserviceServiceModelMappingProfile.cs
namespace ConnectSoft.Template.ApplicationModel.Mappings
{
    using AutoMapper;
    using ConnectSoft.Template.DomainModel;
    using ConnectSoft.Template.EntityModel;
    using ConnectSoft.Template.ServiceModel;

    /// <summary>
    /// Provides a named configuration for ConnectSoft.Template (service model) automapper maps.
    /// </summary>
    public class MicroserviceServiceModelMappingProfile : Profile
    {
        public MicroserviceServiceModelMappingProfile()
        {
            // Domain Entity → DTO
            this.CreateMap<IAggregateRoot, AggregateRootDto>();

            // Request → Input
            this.CreateMap<GetAggregateRootDetailsRequest, GetAggregateRootDetailsInput>();
            this.CreateMap<CreateAggregateRootRequest, CreateAggregateRootInput>();
            this.CreateMap<DeleteAggregateRootRequest, DeleteAggregateRootInput>();

            // Output → Response
            this.CreateMap<FeatureAUseCaseAOutput, FeatureAUseCaseAResponse>();
        }
    }
}

Mapping Registration

// AutoMapperExtensions.cs
internal static void AddAutoMapper(this IServiceCollection services)
{
    var mapperConfig = new MapperConfiguration(mc =>
    {
#if UseGrpc || UseRestApi || UseCoreWCF || UseGraphQL
        mc.AddProfile<MicroserviceServiceModelMappingProfile>();
#endif
    });

    IMapper mapper = mapperConfig.CreateMapper();
    services.AddSingleton(mapper);
}

Usage in Controllers

// REST API Controller
[HttpPost("AggregateRoots/")]
public async Task<CreateAggregateRootResponse> CreateAggregateRoot(
    [FromBody] CreateAggregateRootRequest request,
    CancellationToken token = default)
{
    // Map Request → Domain Input
    var input = this.mapper.Map<CreateAggregateRootInput>(request);

    // Execute domain service
    var entity = await this.processor.CreateAggregateRoot(input, token);

    // Map Domain Entity → DTO
    var dto = this.mapper.Map<AggregateRootDto>(entity);

    // Return Response
    return new CreateAggregateRootResponse
    {
        CreatedAggregateRoot = dto
    };
}

Mapping Validation

Mapping configurations are validated via unit tests:

// MicroserviceServiceModelMappingProfileUnitTests.cs
[TestMethod]
public void ValidateMicroserviceServiceModelMappingProfileMappings()
{
    var config = new MapperConfiguration(cfg =>
    {
        cfg.AddProfile<MicroserviceServiceModelMappingProfile>();
    });

    config.AssertConfigurationIsValid();
}

Validation

DataAnnotations Validation

ServiceModel DTOs use DataAnnotations for structural validation:

public class CreateAggregateRootRequest
{
    [Required]
    [NotDefault]
    required public Guid ObjectId { get; set; }
}

Validation in REST API

REST API automatically validates request DTOs during model binding:

[HttpPost("AggregateRoots/")]
public async Task<IActionResult> Create(
    [FromBody] CreateAggregateRootRequest request)
{
    // Request is automatically validated before this method executes
    // If invalid, ASP.NET Core returns 400 Bad Request
    // ...
}

Manual Validation

For scenarios outside ASP.NET Core model binding (gRPC, background jobs), use validation helpers:

// ServiceModelInputValidationHelper.cs
public static class ServiceModelInputValidationHelper
{
    public static List<ValidationResult> Validate(object instance)
    {
        var context = new ValidationContext(instance);
        var results = new List<ValidationResult>();

        Validator.TryValidateObject(
            instance, 
            context, 
            results, 
            validateAllProperties: true);

        return results;
    }
}

Usage:

var request = new CreateAggregateRootRequest { ObjectId = Guid.Empty };
var validationErrors = ServiceModelInputValidationHelper.Validate(request);

if (validationErrors.Any())
{
    throw new ValidationException("Request validation failed.", validationErrors);
}

See Validation for detailed validation documentation.

Multi-Protocol Support

REST API

Request/Response DTOs work seamlessly with REST API:

[HttpPost("AggregateRoots/")]
public async Task<CreateAggregateRootResponse> Create(
    [FromBody] CreateAggregateRootRequest request)
{
    // REST-specific handling
    // ...
}

gRPC

ServiceModel DTOs can be mapped to gRPC proto messages or used directly:

// gRPC Service Implementation
public override async Task<CreateAggregateRootResponse> CreateAggregateRoot(
    CreateAggregateRootRequest request,
    ServerCallContext context)
{
    // Map gRPC request to ServiceModel request
    var serviceModelRequest = MapToServiceModel(request);

    // Process using domain services
    // ...

    // Map ServiceModel response to gRPC response
    return MapToGrpcResponse(serviceModelResponse);
}

CoreWCF (SOAP)

ServiceModel DTOs are decorated with [MessageContract] for SOAP support:

[MessageContract(
    IsWrapped = true,
    WrapperNamespace = ServiceModelConstants.BaseNamespace + ServiceModelConstants.MessagesNamespace)]
public class CreateAggregateRootRequest
{
    [MessageBodyMember(
        Namespace = ServiceModelConstants.BaseNamespace + ServiceModelConstants.MessagesNamespace, 
        Name = nameof(ObjectId))]
    required public Guid ObjectId { get; set; }
}

GraphQL

ServiceModel DTOs can be used as GraphQL types:

// GraphQL Type Definition
public class AggregateRootType : ObjectGraphType<AggregateRootDto>
{
    public AggregateRootType()
    {
        Field(x => x.ObjectId);
        // ...
    }
}

Azure Functions

ServiceModel DTOs work with Azure Functions:

[FunctionName("CreateAggregateRoot")]
public async Task<HttpResponseData> CreateAggregateRoot(
    [HttpTrigger(AuthorizationLevel.Function, "post", Route = "aggregates")] 
    HttpRequestData req)
{
    var request = await req.ReadFromJsonAsync<CreateAggregateRootRequest>();
    // ...
}

Constants and Namespaces

ServiceModelConstants

// ServiceModelConstants.cs
namespace ConnectSoft.Template.ServiceModel
{
    public static class ServiceModelConstants
    {
        public const string BaseNamespace = "http://connectsoft.com/microservicetemplate/";
        public const string MessagesNamespace = "messages";
        public const string DataContractsNamespace = "datacontracts";
    }
}

Purpose: - Namespace Management: Centralized namespace definitions - SOAP Support: Required for WSDL generation - Versioning: Namespaces can include version information

Complete Request-Response Flow

Creating an Aggregate

flowchart TD
    Client[Client] --> Request[CreateAggregateRootRequest]
    Request --> Controller[REST/gRPC/CoreWCF Controller]
    Controller --> Validate[Validate Request]
    Validate --> MapToInput[Map Request → Domain Input]
    MapToInput --> Processor[Processor]
    Processor --> Entity[Domain Entity]
    Entity --> MapToDto[Map Entity → DTO]
    MapToDto --> Response[CreateAggregateRootResponse]
    Response --> Client
Hold "Alt" / "Option" to enable pan & zoom

Flow Steps

  1. Client sends Request DTO (e.g., CreateAggregateRootRequest)
  2. Controller receives Request (REST/gRPC/CoreWCF/GraphQL)
  3. Request is validated (DataAnnotations, FluentValidation)
  4. Request mapped to Domain Input (CreateAggregateRootInput)
  5. Domain service executes (Processor/Retriever)
  6. Domain Entity returned (IAggregateRoot)
  7. Entity mapped to DTO (AggregateRootDto)
  8. Response DTO created (CreateAggregateRootResponse)
  9. Response returned to client

Best Practices

Do's

  1. Separate Service Models from Domain Models

    // ✅ GOOD - ServiceModel separate from DomainModel
    CreateAggregateRootRequest  CreateAggregateRootInput
    
    // ❌ BAD - Domain model exposed directly
    public async Task<IAggregateRoot> Create(IAggregateRoot entity)
    

  2. Use AutoMapper for Transformations

    // ✅ GOOD - AutoMapper handles mapping
    var input = mapper.Map<CreateAggregateRootInput>(request);
    
    // ❌ BAD - Manual mapping boilerplate
    var input = new CreateAggregateRootInput
    {
        ObjectId = request.ObjectId,
        // ... many more properties
    };
    

  3. Validate at API Boundary

    // ✅ GOOD - Validation attributes
    [Required]
    [NotDefault]
    required public Guid ObjectId { get; set; }
    

  4. Document DTOs

    // ✅ GOOD - XML documentation
    /// <summary>
    /// Gets or sets a AggregateRoot identifier.
    /// </summary>
    public Guid ObjectId { get; set; }
    

  5. Use Appropriate Data Types

    // ✅ GOOD - Strong typing
    required public Guid ObjectId { get; set; }
    required public DateTimeOffset CreatedOn { get; set; }
    
    // ❌ BAD - Weak typing
    public string ObjectId { get; set; }  // Should be Guid
    

Don'ts

  1. Don't Expose Domain Entities Directly

    // ❌ BAD - Domain entity exposed
    [HttpGet]
    public async Task<IAggregateRoot> Get(Guid id)
    
    // ✅ GOOD - DTO exposed
    [HttpGet]
    public async Task<AggregateRootDto> Get(Guid id)
    

  2. Don't Put Business Logic in DTOs

    // ❌ BAD - Business logic in DTO
    public class CreateRequest
    {
        public void Validate() { /* business rules */ }  // Never!
    }
    
    // ✅ GOOD - DTO is pure data
    public class CreateRequest
    {
        public Guid ObjectId { get; set; }
    }
    

  3. Don't Skip Validation

    // ❌ BAD - No validation
    public class CreateRequest
    {
        public Guid ObjectId { get; set; }  // No validation!
    }
    
    // ✅ GOOD - Validated
    public class CreateRequest
    {
        [Required]
        [NotDefault]
        required public Guid ObjectId { get; set; }
    }
    

  4. Don't Include Internal Fields

    // ❌ BAD - Internal details exposed
    public class AggregateRootDto
    {
        public Guid ObjectId { get; set; }
        public string InternalSecretKey { get; set; }  // Never!
        public int InternalVersion { get; set; }  // Never!
    }
    
    // ✅ GOOD - Only public API fields
    public class AggregateRootDto
    {
        public Guid ObjectId { get; set; }
        public string Name { get; set; }
    }
    

  5. Don't Mix Service Models with Domain Models

    // ❌ BAD - Domain model in ServiceModel project
    // ServiceModel/CreateRequest.cs
    using ConnectSoft.Template.EntityModel;  // Never!
    
    // ✅ GOOD - ServiceModel only references DomainModel interfaces
    // ServiceModel/CreateRequest.cs
    // No domain model references
    

Testing

Unit Testing DTOs

Test DTO structure and validation:

[TestMethod]
public void CreateRequest_WithValidData_IsValid()
{
    // Arrange
    var request = new CreateAggregateRootRequest
    {
        ObjectId = Guid.NewGuid()
    };

    // Act
    var validationErrors = ServiceModelInputValidationHelper.Validate(request);

    // Assert
    Assert.IsTrue(validationErrors.Count == 0);
}

[TestMethod]
public void CreateRequest_WithEmptyGuid_IsInvalid()
{
    // Arrange
    var request = new CreateAggregateRootRequest
    {
        ObjectId = Guid.Empty
    };

    // Act
    var validationErrors = ServiceModelInputValidationHelper.Validate(request);

    // Assert
    Assert.IsTrue(validationErrors.Any(e => e.MemberNames.Contains("ObjectId")));
}

Testing AutoMapper Mappings

Validate mapping configurations:

[TestMethod]
public void ValidateMicroserviceServiceModelMappingProfileMappings()
{
    var config = new MapperConfiguration(cfg =>
    {
        cfg.AddProfile<MicroserviceServiceModelMappingProfile>();
    });

    config.AssertConfigurationIsValid();
}

[TestMethod]
public void MapRequestToInput_MapsCorrectly()
{
    // Arrange
    var request = new CreateAggregateRootRequest
    {
        ObjectId = Guid.NewGuid()
    };

    // Act
    var input = mapper.Map<CreateAggregateRootInput>(request);

    // Assert
    Assert.AreEqual(request.ObjectId, input.ObjectId);
}

Integration Testing

Test complete request-response flow:

[TestMethod]
public async Task CreateAggregate_ReturnsResponse()
{
    // Arrange
    var request = new CreateAggregateRootRequest
    {
        ObjectId = Guid.NewGuid()
    };

    // Act
    var response = await client.PostAsJsonAsync("/api/aggregates", request)
        .Result.Content.ReadFromJsonAsync<CreateAggregateRootResponse>();

    // Assert
    Assert.IsNotNull(response);
    Assert.IsNotNull(response.CreatedAggregateRoot);
    Assert.AreEqual(request.ObjectId, response.CreatedAggregateRoot.ObjectId);
}

Versioning

DTO Versioning Strategies

ServiceModel DTOs can evolve independently:

  1. Additive Changes: Add new optional properties (backward compatible)

    // Version 1
    public class CreateRequest
    {
        public Guid ObjectId { get; set; }
    }
    
    // Version 2 (backward compatible)
    public class CreateRequest
    {
        public Guid ObjectId { get; set; }
        public string? Description { get; set; }  // New optional field
    }
    

  2. Namespace Versioning: Use different namespaces for major versions

    // v1
    Namespace = "http://connectsoft.com/microservicetemplate/v1/messages"
    
    // v2
    Namespace = "http://connectsoft.com/microservicetemplate/v2/messages"
    

  3. Separate DTOs: Create new DTOs for breaking changes

    // v1
    CreateAggregateRootRequest
    
    // v2 (breaking change)
    CreateAggregateRootV2Request
    

Troubleshooting

Issue: Mapping Fails

Symptom: AutoMapperMappingException or missing properties after mapping.

Solutions: 1. Verify mapping profile includes the mapping configuration 2. Check property names match between source and destination 3. Ensure AutoMapper is registered in DI container 4. Validate mapping configuration in unit tests

Issue: Validation Not Working

Symptom: Invalid requests pass through without validation.

Solutions: 1. Verify DataAnnotations are present on DTO properties 2. Check validators are registered via services.AddFluentValidation() 3. Ensure model binding is working correctly 4. Test validation manually using ServiceModelInputValidationHelper

Issue: DTO Not Serializing

Symptom: DTO properties missing in JSON/XML response.

Solutions: 1. Check [DataMember] attributes are present (for SOAP) 2. Verify property accessors are public 3. Ensure JSON serializer configuration is correct 4. Check for serialization attributes ([JsonIgnore], etc.)

Summary

The Service Model in the ConnectSoft ConnectSoft Templates provides:

  • API Boundary Isolation: Clean separation between API contracts and domain models
  • Multi-Protocol Support: DTOs work across REST, gRPC, SOAP, GraphQL, Azure Functions
  • Validation: DataAnnotations provide structural validation at API boundary
  • AutoMapper Integration: Automatic mapping between ServiceModel and DomainModel
  • Contract Definition: Service interfaces define clear API contracts
  • Versioning Support: DTOs can evolve independently while maintaining compatibility
  • Testability: DTOs and mappings are easily testable

By following these patterns, teams can:

  • Maintain Clean Architecture: Service models stay separate from domain models
  • Support Multiple Protocols: Same DTOs work across different service types
  • Evolve APIs Safely: Versioning strategies enable API evolution
  • Validate at Boundaries: Validation prevents invalid data from entering the system
  • Build Consistent APIs: Standardized patterns across all service types

The Service Model is the public face of the microservice—defining clear contracts, enabling multiple protocols, and maintaining strict boundaries between external clients and internal domain logic.

  • REST API: REST API implementation patterns
  • gRPC: gRPC service implementation
  • GraphQL: GraphQL schema and resolvers
  • CoreWCF: CoreWCF SOAP services
  • API Versioning: API versioning strategies and implementation patterns
  • Validation: Input validation and sanitization