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¶
-
Use Descriptive Names
-
Include Validation Attributes
-
Provide XML Documentation
-
Use Appropriate Data Types
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¶
-
Expose Only Necessary Fields
-
Use Ordered DataMembers
-
Provide XML Documentation
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
Flow Steps¶
- Client sends Request DTO (e.g.,
CreateAggregateRootRequest) - Controller receives Request (REST/gRPC/CoreWCF/GraphQL)
- Request is validated (DataAnnotations, FluentValidation)
- Request mapped to Domain Input (
CreateAggregateRootInput) - Domain service executes (Processor/Retriever)
- Domain Entity returned (
IAggregateRoot) - Entity mapped to DTO (
AggregateRootDto) - Response DTO created (
CreateAggregateRootResponse) - Response returned to client
Best Practices¶
Do's¶
-
Separate Service Models from Domain Models
-
Use AutoMapper for Transformations
-
Validate at API Boundary
-
Document DTOs
-
Use Appropriate Data Types
Don'ts¶
-
Don't Expose Domain Entities Directly
-
Don't Put Business Logic in DTOs
-
Don't Skip Validation
-
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; } } -
Don't Mix Service Models with Domain Models
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:
-
Additive Changes: Add new optional properties (backward compatible)
-
Namespace Versioning: Use different namespaces for major versions
-
Separate DTOs: Create new DTOs for breaking changes
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.
Related Documentation¶
- 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