# 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](/paramore-brighter-documentation/v10-migration/nullablereferencetypes.md)

### 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](/paramore-brighter-documentation/under-the-hood/reactorandproactor.md)

### 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](/paramore-brighter-documentation/brighter-request-handlers-and-middleware-pipelines/policyretryandcircuitbreaker.md)

### 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](/paramore-brighter-documentation/using-an-external-bus/cloudeventssupport.md)

### 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](/paramore-brighter-documentation/using-an-external-bus/messagemappers.md)

### 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](/paramore-brighter-documentation/health-checks-and-observability/telemetry.md)

### 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](/paramore-brighter-documentation/brighter-configuration/inmemoryoptions.md)

## 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](/paramore-brighter-documentation/v10-migration/nullablereferencetypes.md)

### 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! 🚀


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://brightercommand.gitbook.io/paramore-brighter-documentation/v10-migration/v10migrationguide.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
