六边形架构中动态加载适配器:单主机集成WCF与Owin适配器
Great question! Implementing a single console host that dynamically loads both your WCF SOAP and OWIN REST adapters aligns perfectly with Hexagonal Architecture’s goal of decoupling core logic from external interfaces. Let’s walk through a practical approach step by step.
1. Clarify Port Naming & Define the Primary Port
First, let’s get the terminology right: your "UI port" is actually a Primary (Driving) Port—it’s the interface through which external systems (like UI clients, APIs) drive your core user management logic. This port should be completely technology-agnostic, so it doesn’t depend on WCF, OWIN, or any other adapter-specific details.
Here’s how you can define the port interface in your core bounded context project:
// MyDDD.UserManagement.Core/IPrimary/UserManagementPort.cs namespace MyDDD.UserManagement.Core.PrimaryPorts; public interface IUserManagementPort { Guid CreateNewUser(string username, string email, string passwordHash); void BlockUser(Guid userId); bool CheckIfUserExists(string usernameOrEmail); }
Implement this interface in your core service (the "application layer" of your Hexagonal Architecture):
// MyDDD.UserManagement.Core/Services/UserManagementService.cs namespace MyDDD.UserManagement.Core.Services; public class UserManagementService : IUserManagementPort { // Inject repositories (secondary ports) here as needed private readonly IUserRepository _userRepository; public UserManagementService(IUserRepository userRepository) { _userRepository = userRepository; } public Guid CreateNewUser(string username, string email, string passwordHash) { // Core business logic here var user = new User(username, email, passwordHash); _userRepository.Add(user); return user.Id; } // Implement other methods... }
2. Build Adapter Class Libraries (WCF SOAP & OWIN REST)
Each adapter should be a separate class library that depends only on your core bounded context (specifically, the primary port interface). Each adapter will implement the primary port and expose it via its technology (WCF/OWIN), plus provide a host interface to start/stop itself.
Define a Common Adapter Host Interface
Create a shared project (or add it to your core project) with an interface for adapter hosts:
// MyDDD.UserManagement.Adapters.Shared/IAdapterHost.cs namespace MyDDD.UserManagement.Adapters.Shared; public interface IAdapterHost { Task StartAsync(CancellationToken cancellationToken = default); Task StopAsync(CancellationToken cancellationToken = default); }
WCF SOAP Adapter
Create a class library MyDDD.UserManagement.Adapters.Soap:
- Implement the WCF service that wraps your primary port:
// MyDDD.UserManagement.Adapters.Soap/Services/UserManagementSoapService.cs namespace MyDDD.UserManagement.Adapters.Soap.Services; [ServiceContract] public class UserManagementSoapService { private readonly IUserManagementPort _userManagementPort; public UserManagementSoapService(IUserManagementPort userManagementPort) { _userManagementPort = userManagementPort; } [OperationContract] public Guid CreateNewUser(string username, string email, string passwordHash) { return _userManagementPort.CreateNewUser(username, email, passwordHash); } // Expose other operations as OperationContracts... }
- Implement the adapter host to start/stop the WCF service:
// MyDDD.UserManagement.Adapters.Soap/Host/SoapAdapterHost.cs namespace MyDDD.UserManagement.Adapters.Soap.Host; public class SoapAdapterHost : IAdapterHost { private ServiceHost? _serviceHost; private readonly IUserManagementPort _userManagementPort; private readonly string _serviceAddress; public SoapAdapterHost(IUserManagementPort userManagementPort, IConfiguration configuration) { _userManagementPort = userManagementPort; _serviceAddress = configuration["Adapters:Soap:Address"] ?? "http://localhost:8080/UserManagementSoap"; } public Task StartAsync(CancellationToken cancellationToken = default) { _serviceHost = new ServiceHost(typeof(UserManagementSoapService)); var endpoint = _serviceHost.AddServiceEndpoint( typeof(UserManagementSoapService), new BasicHttpBinding(), _serviceAddress); _serviceHost.Open(); Console.WriteLine($"WCF SOAP adapter started at {_serviceAddress}"); return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken = default) { _serviceHost?.Close(); Console.WriteLine("WCF SOAP adapter stopped"); return Task.CompletedTask; } }
OWIN REST Adapter
Create a class library MyDDD.UserManagement.Adapters.Rest:
- Use OWIN + Web API to expose REST endpoints that call the primary port:
// MyDDD.UserManagement.Adapters.Rest/Controllers/UserManagementController.cs namespace MyDDD.UserManagement.Adapters.Rest.Controllers; public class UserManagementController : ApiController { private readonly IUserManagementPort _userManagementPort; public UserManagementController(IUserManagementPort userManagementPort) { _userManagementPort = userManagementPort; } [HttpPost] [Route("api/users")] public IHttpActionResult CreateUser([FromBody] CreateUserRequest request) { var userId = _userManagementPort.CreateNewUser(request.Username, request.Email, request.PasswordHash); return Ok(new { UserId = userId }); } // Implement other endpoints... } public class CreateUserRequest { public string Username { get; set; } = string.Empty; public string Email { get; set; } = string.Empty; public string PasswordHash { get; set; } = string.Empty; }
- Implement the adapter host to start/stop the OWIN web host:
// MyDDD.UserManagement.Adapters.Rest/Host/RestAdapterHost.cs namespace MyDDD.UserManagement.Adapters.Rest.Host; public class RestAdapterHost : IAdapterHost { private IDisposable? _webApp; private readonly string _restAddress; public RestAdapterHost(IConfiguration configuration) { _restAddress = configuration["Adapters:Rest:Address"] ?? "http://localhost:8081/api"; } public Task StartAsync(CancellationToken cancellationToken = default) { _webApp = WebApp.Start<Startup>(_restAddress); Console.WriteLine($"OWIN REST adapter started at {_restAddress}"); return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken = default) { _webApp?.Dispose(); Console.WriteLine("OWIN REST adapter stopped"); return Task.CompletedTask; } } // Startup class for OWIN public class Startup { public void Configuration(IAppBuilder app) { var config = new HttpConfiguration(); config.MapHttpAttributeRoutes(); config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html")); app.UseWebApi(config); } }
3. Build the Single Console Host
Your console application will act as the "shell" that loads the core logic and dynamically discovers/loads adapters. We’ll use dependency injection to wire everything together.
Step 1: Set Up Dependency Injection
Use Microsoft’s built-in DI (or another container like Autofac) to register core services and adapter hosts.
// MyDDD.UserManagement.Host/Program.cs using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Configuration; using MyDDD.UserManagement.Adapters.Shared; using MyDDD.UserManagement.Core.PrimaryPorts; using MyDDD.UserManagement.Core.Services; using System.Reflection; var configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json", optional: false) .Build(); var services = new ServiceCollection(); // Register core services services.AddScoped<IUserManagementPort, UserManagementService>(); // Register secondary ports (repositories) here as needed // services.AddScoped<IUserRepository, SqlUserRepository>(); // Dynamically register adapter hosts from assemblies var adapterAssemblies = Directory.GetFiles(AppContext.BaseDirectory, "MyDDD.UserManagement.Adapters.*.dll") .Select(Assembly.LoadFrom); foreach (var assembly in adapterAssemblies) { var adapterHostTypes = assembly.GetTypes() .Where(t => typeof(IAdapterHost).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract); foreach (var type in adapterHostTypes) { services.AddScoped(typeof(IAdapterHost), type); } } var serviceProvider = services.BuildServiceProvider();
Step 2: Start All Adapters
Retrieve all registered IAdapterHost instances and start them:
var adapterHosts = serviceProvider.GetServices<IAdapterHost>(); var startTasks = adapterHosts.Select(h => h.StartAsync()).ToList(); await Task.WhenAll(startTasks); Console.WriteLine("All adapters started. Press Ctrl+C to stop.");
Step 3: Handle Shutdown Gracefully
Listen for console exit signals to stop all adapters:
var cancellationTokenSource = new CancellationTokenSource(); Console.CancelKeyPress += (sender, args) => { args.Cancel = true; cancellationTokenSource.Cancel(); }; try { await Task.Delay(Timeout.Infinite, cancellationTokenSource.Token); } catch (TaskCanceledException) { var stopTasks = adapterHosts.Select(h => h.StopAsync()).ToList(); await Task.WhenAll(stopTasks); Console.WriteLine("All adapters stopped. Exiting..."); }
4. Key Configuration (appsettings.json)
Use a configuration file to set adapter addresses without hardcoding:
{ "Adapters": { "Soap": { "Address": "http://localhost:8080/UserManagementSoap" }, "Rest": { "Address": "http://localhost:8081/api" } } }
5. Best Practices
- Keep Adapters Light: Adapters should only handle technology-specific concerns (SOAP serialization, REST routing) and delegate all business logic to the primary port.
- Isolate Adapter Dependencies: Each adapter should only depend on the core bounded context and its own technology stack (WCF/OWIN).
- Add Logging: Integrate a logging framework (like Serilog or Microsoft.Extensions.Logging) into adapters and the host to track activity and debug issues.
- Health Checks: Add health check endpoints to each adapter (e.g., a
/healthendpoint in the REST adapter) to monitor their status.
内容的提问来源于stack exchange,提问作者DmitriBodiu

