# V10 Migration Guide

## Overview

Brighter V10 introduces significant improvements and new features while maintaining a clear migration path from V9. This guide provides step-by-step instructions for upgrading your application to V10, addressing breaking changes, and adopting new features.

**Key Changes in V10**:

* Cloud Events support
* OpenTelemetry Semantic Conventions
* Default Message Mappers
* Dynamic Message Deserialization
* Nullable Reference Types (breaking)
* Simplified Configuration (breaking)
* Reactor and Proactor terminology (breaking)
* Polly Resilience Pipeline v8 (breaking)
* Request Context enhancements
* InMemory options for testing
* Transport improvements (PostgreSQL, RabbitMQ, Kafka, AWS)
* Request ID is now an `Id` type

**Migration Effort**: Most applications can be migrated in **1-4 hours**, depending on complexity.

## Before You Start

### Prerequisites

1. **Backup your code**: Commit all changes to version control
2. **Review the release notes**: Read [Release Notes](https://github.com/BrighterCommand/Brighter/releases) for V10
3. **Update test suite**: Ensure your tests are passing on V9
4. **Check dependencies**: Review third-party package compatibility

### Recommended Migration Approach

1. **Upgrade in a feature branch**: Don't upgrade directly in main/master
2. **Address breaking changes first**: Fix compilation errors before adopting new features
3. **Test thoroughly**: Run your full test suite after each step
4. **Deploy to staging**: Validate in a non-production environment
5. **Monitor production**: Watch for issues after deployment

## Step 1: Update Package References

### Update NuGet Packages

Update all Brighter packages to V10:

```bash
# Core packages
dotnet add package Paramore.Brighter --version 10.0.0
dotnet add package Paramore.Brighter.Extensions.DependencyInjection --version 10.0.0

# Transport packages (update as needed)
dotnet add package Paramore.Brighter.MessagingGateway.RMQ --version 10.0.0
dotnet add package Paramore.Brighter.MessagingGateway.Kafka --version 10.0.0
dotnet add package Paramore.Brighter.MessagingGateway.AWSSQS --version 10.0.0

# Outbox/Inbox packages (update as needed)
dotnet add package Paramore.Brighter.Outbox.MsSql --version 10.0.0
dotnet add package Paramore.Brighter.Inbox.MsSql --version 10.0.0
```

### Check for Package Conflicts

```bash
# List all packages and check for conflicts
dotnet list package
```

## Step 2: Address Breaking Changes

### 1. Nullable Reference Types

**Breaking Change**: Nullable reference types are now enabled across all Brighter projects.

**Migration Steps**:

1. **Enable nullable reference types** in your project (if not already enabled):

```xml
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>
```

2. **Address compiler warnings** in Commands, Events, and Handlers:

**Before (V9)**:

```csharp
public class CreatePersonCommand : Command
{
    public string Name { get; set; }  // Warning: Non-nullable property
    public string Email { get; set; }  // Warning: Non-nullable property
}
```

**After (V10)**:

```csharp
public class CreatePersonCommand : Command
{
    public required string Name { get; set; }  // Required property
    public required string Email { get; set; }  // Required property

    // Or with constructor
    public CreatePersonCommand(Guid id, string name, string email) : base(id)
    {
        Name = name;
        Email = email;
    }
}
```

3. **Update Message Mappers** to handle nullable warnings:

```csharp
public class PersonCreatedMapper : IAmAMessageMapper<PersonCreated>
{
    public Message MapToMessage(PersonCreated request)
    {
        // Validate non-null properties
        ArgumentNullException.ThrowIfNull(request.Name);

        var header = new MessageHeader(
            messageId: request.Id,
            topic: new RoutingKey("PersonCreated"),
            messageType: MessageType.MT_EVENT
        );

        var body = new MessageBody(JsonSerializer.Serialize(request));
        return new Message(header, body);
    }
}
```

**See also**: [Nullable Reference Types Documentation](https://brightercommand.gitbook.io/paramore-brighter-documentation/v10-migration/nullablereferencetypes)

### 2. Simplified Configuration

**Breaking Change**: Builder methods renamed for clarity.

**Before (V9)**:

```csharp
services.AddBrighter()
    .UseExternalBus(new RmqProducerRegistryFactory(...).Create())
    .AddServiceActivator(options =>
    {
        options.Subscriptions = subscriptions;
        options.ChannelFactory = new ChannelFactory(...);
    });
```

**After (V10)**:

```csharp
services.AddBrighter()
    .AddProducers(options =>
    {
        options.ProducerRegistry = new RmqProducerRegistryFactory(...).Create();
    })
    .AddConsumers(options =>
    {
        options.Subscriptions = subscriptions;
        options.ChannelFactory = new ChannelFactory(...);
    });
```

**Migration Steps**:

1. Replace `UseExternalBus` with `AddProducers`
2. Replace `AddServiceActivator` with `AddConsumers`
3. Update property names: `ProducerRegistry` instead of passing directly

### 3. Reactor and Proactor Terminology

**Breaking Change**: The `runAsync` flag on Subscription has been renamed to `MessagePumpType`.

**Before (V9)**:

```csharp
var subscription = new Subscription<MyEvent>(
    new SubscriptionName("my-subscription"),
    new ChannelName("my-channel"),
    new RoutingKey("my.routing.key"),
    runAsync: true  // Old parameter
);
```

**After (V10)**:

```csharp
var subscription = new Subscription<MyEvent>(
    new SubscriptionName("my-subscription"),
    new ChannelName("my-channel"),
    new RoutingKey("my.routing.key"),
    messagePumpType: MessagePumpType.Proactor  // New parameter
);
```

**Migration Table**:

| V9 Parameter      | V10 Parameter                               | Description                    |
| ----------------- | ------------------------------------------- | ------------------------------ |
| `runAsync: false` | `messagePumpType: MessagePumpType.Reactor`  | Synchronous, blocking I/O      |
| `runAsync: true`  | `messagePumpType: MessagePumpType.Proactor` | Asynchronous, non-blocking I/O |

**See also**: [Reactor and Proactor Documentation](https://brightercommand.gitbook.io/paramore-brighter-documentation/under-the-hood/reactorandproactor)

### 4. Polly Resilience Pipeline

**Breaking Change**: `TimeoutPolicyAttribute` is obsolete. Use `UseResiliencePipeline` attribute.

**Before (V9)**:

```csharp
public class MyHandler : RequestHandlerAsync<MyCommand>
{
    [TimeoutPolicy(milliseconds: 5000, step: 1)]
    public override async Task<MyCommand> HandleAsync(
        MyCommand command,
        CancellationToken cancellationToken = default)
    {
        // Handler logic
        return await base.HandleAsync(command, cancellationToken);
    }
}
```

**After (V10)**:

1. **Define a Resilience Pipeline**:

```csharp
var resiliencePipelineRegistry = new ResiliencePipelineRegistry<string>();
resiliencePipelineRegistry.TryAddBuilder<ResiliencePropertyKey<RequestContext>>(
    "MyPipeline",
    (builder, context) =>
    {
        builder.AddTimeout(TimeSpan.FromSeconds(5));
        builder.AddRetry(new RetryStrategyOptions
        {
            MaxRetryAttempts = 3,
            Delay = TimeSpan.FromMilliseconds(100)
        });
    });
```

2. **Use the new attribute**:

```csharp
public class MyHandler : RequestHandlerAsync<MyCommand>
{
    [UseResiliencePipeline(policy: "MyPipeline", step: 1)]
    public override async Task<MyCommand> HandleAsync(
        MyCommand command,
        CancellationToken cancellationToken = default)
    {
        // Handler logic
        return await base.HandleAsync(command, cancellationToken);
    }
}
```

3. **Register the pipeline** with Brighter:

```csharp
services.AddBrighter(options =>
{
    options.PolicyRegistry = resiliencePipelineRegistry;
});
```

**See also**: [Policy Retry and Circuit Breaker Documentation](https://brightercommand.gitbook.io/paramore-brighter-documentation/brighter-request-handlers-and-middleware-pipelines/policyretryandcircuitbreaker)

### 5. Request Context Interface Changes

**Breaking Change**: `IRequestContext` interface has new properties.

**New Properties**:

* `PartitionKey`: Set message partition keys dynamically
* `CustomHeaders`: Add custom headers via request context
* `ResilienceContext`: Integration with Polly resilience pipeline
* `OriginatingMessage`: Access the original message (for consumers)

**Migration**: Most code should not be affected unless you implement `IRequestContext` directly.

**If you implement `IRequestContext`** (rare):

```csharp
public class MyRequestContext : IRequestContext
{
    public Guid Id { get; set; }
    public ISpan Span { get; set; }
    public Dictionary<string, object> Bag { get; set; }

    // V10: Add new properties
    public string? PartitionKey { get; set; }
    public Dictionary<string, string> CustomHeaders { get; set; } = new();
    public ResilienceContext? ResilienceContext { get; set; }
    public Message? OriginatingMessage { get; set; }
}
```

**Using new properties**:

```csharp
public class MyHandler : RequestHandlerAsync<MyCommand>
{
    public override async Task<MyCommand> HandleAsync(
        MyCommand command,
        CancellationToken cancellationToken = default)
    {
        // Set partition key for message routing
        Context.PartitionKey = command.TenantId;

        // Add custom headers
        Context.CustomHeaders["X-Correlation-Id"] = command.CorrelationId;

        // Access originating message (for consumers)
        if (Context.OriginatingMessage != null)
        {
            var receivedTimestamp = Context.OriginatingMessage.Header.TimeStamp;
        }

        return await base.HandleAsync(command, cancellationToken);
    }
}
```

### 6. Request Id and CorrelationId type change

In V9 the `Request` type used a `Guid` to represent the identity of a `Command` or `Event`. In V10, as part of a move away from primitives, we have changed this to be a type `Id`. An `Id` can be constructed from a `string` using its `ToString()` method. If you have used a `Guid` then you will need to turn the `Guid` into a `string`.

Within `IRequest` both `Id` and `CorrelationId` both use the type `Id` in V10.

If you were using a `Guid` to create a random identity, you can just use `Id.Random()` instead which has the same behavior.

Before:

```csharp

class MyCommand() : Command(Guid.NewGuid())
{
    public string Value { get; set; } = string.Empty;
}

```

After:

```csharp

class MyCommand() : Command(Id.Random())
{
    public string Value { get; set; } = string.Empty;
}

```

## Step 3: Adopt New Features (Optional)

### 1. Cloud Events Support

V10 adds full Cloud Events specification support.

**Benefits**:

* Standardized event metadata
* Better interoperability with other systems
* Rich event context information

**Adoption Steps**:

1. **Update Publication** to include Cloud Events properties:

```csharp
new RmqPublication
{
    Topic = new RoutingKey("PersonCreated"),
    CloudEventsType = new CloudEventsType("io.paramore.person.created"),
    Source = new Uri("https://api.example.com/persons"),
    Subject = "person/created",
    MakeChannels = OnMissingChannel.Create
}
```

2. **Use Cloud Events headers** in your mapper (optional):

```csharp
public class PersonCreatedMapper : IAmAMessageMapper<PersonCreated>
{
    public Message MapToMessage(PersonCreated request, Publication publication)
    {
        var header = new MessageHeader(
            messageId: request.Id,
            topic: publication.Topic,
            messageType: MessageType.MT_EVENT,
            type: publication.CloudEventsType,  // Use Cloud Events type
            source: publication.Source,
            subject: publication.Subject
        );

        var body = new MessageBody(JsonSerializer.Serialize(request));
        return new Message(header, body);
    }
}
```

**See also**: [Cloud Events Documentation](https://brightercommand.gitbook.io/paramore-brighter-documentation/using-an-external-bus/cloudeventssupport)

### 2. Default Message Mappers

V10 allows you to omit message mappers for simple JSON serialization.

**Benefits**:

* Less boilerplate code
* Faster development
* Still supports custom mappers for complex scenarios

**Before (V9) - Required Mapper**:

```csharp
public class PersonCreatedMapper : IAmAMessageMapper<PersonCreated>
{
    public Message MapToMessage(PersonCreated request)
    {
        var header = new MessageHeader(
            messageId: request.Id,
            topic: new RoutingKey("PersonCreated"),
            messageType: MessageType.MT_EVENT
        );

        var body = new MessageBody(JsonSerializer.Serialize(request));
        return new Message(header, body);
    }

    public PersonCreated MapToRequest(Message message)
    {
        return JsonSerializer.Deserialize<PersonCreated>(message.Body.Value)
            ?? throw new InvalidOperationException("Failed to deserialize");
    }
}

// Register mapper
messageMapperRegistry.Register<PersonCreated, PersonCreatedMapper>();
```

**After (V10) - No Mapper Needed**:

```csharp
// No mapper registration needed for simple JSON serialization!
// Brighter uses JsonMessageMapper<T> by default

// Just define your message
public class PersonCreated : Event
{
    public string Name { get; set; }
    public string Email { get; set; }
}

// Publish directly
await commandProcessor.PublishAsync(new PersonCreated
{
    Name = "Alice",
    Email = "alice@example.com"
});
```

**When to use custom mappers**:

* Complex transformations
* Non-JSON formats (XML, Protobuf, etc.)
* Transform pipelines (encryption, compression, claim check)
* Custom header mapping

**See also**: [Message Mappers Documentation](https://brightercommand.gitbook.io/paramore-brighter-documentation/using-an-external-bus/messagemappers)

### 3. Dynamic Message Deserialization

V10 supports multiple message types on the same channel.

**Benefits**:

* Content-based routing
* Flexible message processing
* Better use of infrastructure

**Example**:

```csharp
new KafkaSubscription(
    new SubscriptionName("task-state-subscription"),
    channelName: new ChannelName("task.state"),
    routingKey: new RoutingKey("task.update"),
    getRequestType: message => message switch
    {
        var m when m.Header.Type == new CloudEventsType("io.paramore.task.created")
            => typeof(TaskCreated),
        var m when m.Header.Type == new CloudEventsType("io.paramore.task.updated")
            => typeof(TaskUpdated),
        var m when m.Header.Type == new CloudEventsType("io.paramore.task.deleted")
            => typeof(TaskDeleted),
        _ => throw new ArgumentException(
            $"No type mapping found for message with type {message.Header.Type}",
            nameof(message))
    },
    groupId: "task-consumer-group",
    messagePumpType: MessagePumpType.Proactor
);
```

**See also**: [Dynamic Deserialization Documentation](https://github.com/BrighterCommand/Docs/blob/master/contents/DynamicDeserialization.md)

### 4. OpenTelemetry Semantic Conventions

V10 adopts OpenTelemetry Semantic Conventions for messaging.

**Impact**: Trace spans will have different names and attributes than V9.

**Benefits**:

* Standard messaging conventions
* Better integration with APM tools
* Consistent telemetry across systems

**Migration**:

1. **Update dashboards and alerts** to use new span names
2. **Review trace queries** for compatibility
3. **Test observability** in staging environment

**V9 Span Names**:

* `Paramore.Brighter.CommandProcessor.Send`
* `Paramore.Brighter.MessagePump.Receive`

**V10 Span Names** (OTel Semantic Conventions):

* `messaging.send`
* `messaging.receive`
* `messaging.process`

**See also**: [Telemetry Documentation](https://brightercommand.gitbook.io/paramore-brighter-documentation/health-checks-and-observability/telemetry)

### 5. InMemory Options for Testing

V10 provides comprehensive InMemory implementations for testing.

**Benefits**:

* Fast test execution
* No external dependencies
* Easy CI/CD integration

**Example**:

```csharp
// Test setup with InMemory components
var internalBus = new InternalBus();

services.AddBrighter(options =>
{
    options.HandlerLifetime = ServiceLifetime.Scoped;
})
.AddProducers(options =>
{
    var publication = new Publication { Topic = new RoutingKey("TestTopic") };
    options.ProducerRegistry = new InMemoryProducerRegistryFactory(
        internalBus,
        new[] { publication },
        InstrumentationOptions.All
    ).Create();
    options.Outbox = new InMemoryOutbox(TimeProvider.System);
})
.AddConsumers(options =>
{
    options.Inbox = new InboxConfiguration(
        new InMemoryInbox(TimeProvider.System),
        InboxConfiguration.NoActionOnExists
    );
    options.Subscriptions = subscriptions;
    options.ChannelFactory = new InMemoryChannelFactory(internalBus, TimeProvider.System);
})
.UseScheduler(new InMemorySchedulerFactory())
.AutoFromAssemblies();
```

**See also**: [InMemory Options Documentation](https://brightercommand.gitbook.io/paramore-brighter-documentation/brighter-configuration/inmemoryoptions)

## Step 4: Test Your Migration

### Unit Tests

1. **Run existing unit tests**:

```bash
dotnet test
```

2. **Address test failures**:
   * Update mocks for `IRequestContext` new properties
   * Update assertions for OpenTelemetry span names
   * Fix nullable reference warnings

### Integration Tests

1. **Test with InMemory components** (fast):

```csharp
[Fact]
public async Task Should_Process_Message_With_V10_Components()
{
    // Arrange
    var internalBus = new InternalBus();
    var serviceProvider = BuildServiceProvider(internalBus);
    var commandProcessor = serviceProvider.GetRequiredService<IAmACommandProcessor>();

    // Act
    await commandProcessor.PublishAsync(new PersonCreated
    {
        Name = "Alice",
        Email = "alice@example.com"
    });

    // Assert
    var messages = internalBus.Stream(new RoutingKey("PersonCreated"));
    Assert.NotEmpty(messages);
}
```

2. **Test with real transports** (slower, more complete):

```bash
# Start dependencies (Docker Compose)
docker-compose up -d rabbitmq postgres

# Run integration tests
dotnet test --filter Category=Integration
```

### Performance Testing

Compare V9 and V10 performance:

```csharp
[Benchmark]
public async Task V10_MessageProcessing()
{
    for (int i = 0; i < 1000; i++)
    {
        await _commandProcessor.SendAsync(new TestCommand { Value = i });
    }
}
```

Expected: V10 should have similar or better performance due to optimizations.

## Step 5: Deploy to Staging

### Pre-Deployment Checklist

* [ ] All tests passing
* [ ] Code review completed
* [ ] Documentation updated
* [ ] Release notes reviewed
* [ ] Rollback plan prepared

### Deployment Steps

1. **Deploy to staging environment**
2. **Run smoke tests**
3. **Monitor metrics**:
   * Message processing latency
   * Error rates
   * Resource usage (CPU, memory)
4. **Check logs** for warnings or errors
5. **Validate telemetry** (traces, metrics)

### Monitoring

Watch for:

* Increased error rates
* Performance degradation
* OpenTelemetry trace issues
* Null reference exceptions

## Step 6: Deploy to Production

### Production Deployment

1. **Deploy during low-traffic period**
2. **Use blue-green or canary deployment** if possible
3. **Monitor closely** for first 24 hours
4. **Have rollback plan ready**

### Post-Deployment

* Review logs for issues
* Check application metrics
* Validate message processing
* Monitor OpenTelemetry traces

## Common Migration Issues

### Issue 1: Nullable Reference Warnings

**Symptom**: CS8618, CS8600, CS8602 warnings

**Solution**: See [Nullable Reference Types Documentation](https://brightercommand.gitbook.io/paramore-brighter-documentation/v10-migration/nullablereferencetypes)

### Issue 2: Method Not Found

**Symptom**: `UseExternalBus` method not found

**Solution**: Replace with `AddProducers`

### Issue 3: Property Not Found on Subscription

**Symptom**: `runAsync` property does not exist

**Solution**: Use `messagePumpType: MessagePumpType.Proactor` or `MessagePumpType.Reactor`

### Issue 4: TimeoutPolicy Not Working

**Symptom**: `TimeoutPolicyAttribute` marked as obsolete

**Solution**: Migrate to `UseResiliencePipeline` with Polly v8

### Issue 5: Telemetry Spans Changed

**Symptom**: Dashboard queries not finding spans

**Solution**: Update queries to use OpenTelemetry Semantic Convention names

## Rollback Plan

If you need to roll back to V9:

1. **Revert package versions**:

```bash
dotnet add package Paramore.Brighter --version 9.x.x
```

2. **Revert code changes**:

```bash
git revert <commit-hash>
```

3. **Redeploy V9 version**
4. **Investigate issues** before attempting V10 migration again

## Getting Help

### Resources

* [Brighter Documentation](https://brightercommand.github.io/Brighter/)
* [GitHub Issues](https://github.com/BrighterCommand/Brighter/issues)
* [Release Notes](https://github.com/BrighterCommand/Brighter/releases)
* [Stack Overflow](https://stackoverflow.com/questions/tagged/brighter)

### Reporting Issues

If you encounter issues during migration:

1. **Check existing issues**: Search [GitHub Issues](https://github.com/BrighterCommand/Brighter/issues)
2. **Create a new issue** with:
   * V9 and V10 versions
   * Minimal reproduction example
   * Error messages and stack traces
   * Environment details (.NET version, OS, transport)

## Summary

Migrating to Brighter V10 involves:

1. **Update packages** to V10
2. **Address breaking changes**:
   * Nullable reference types
   * Simplified configuration API
   * Reactor/Proactor terminology
   * Polly Resilience Pipeline
3. **Adopt new features** (optional):
   * Cloud Events
   * Default message mappers
   * Dynamic deserialization
   * OpenTelemetry conventions
   * InMemory testing options
4. **Test thoroughly**
5. **Deploy to staging**, then production
6. **Monitor** and address any issues

Most migrations can be completed in **1-4 hours**. The breaking changes are straightforward, and V10 provides significant improvements in features, performance, and developer experience.

Good luck with your migration! 🚀
