Nullable Reference Types
V10 enables nullable reference types across all Brighter projects, providing improved type safety and helping prevent null reference exceptions.
Understanding Nullable Reference Types
Non-Nullable by Default
In C# with nullable reference types enabled, reference types are non-nullable by default:
// This string cannot be null
string name = "John";
// Compiler warning: Cannot convert null to non-nullable reference type
string name = null; // ⚠️ CS8600Nullable Types
Use ? to explicitly mark types as nullable:
// This string can be null
string? optionalName = null; // ✅ OK
// Must check for null before using
if (optionalName != null)
{
Console.WriteLine(optionalName.Length);
}Impact on Brighter Code
Commands and Events
Required properties should be non-nullable:
public class CreateOrderCommand : Command
{
public Guid OrderId { get; }
public string CustomerName { get; } // Non-nullable: required
public decimal TotalAmount { get; }
public string? Notes { get; } // Nullable: optional
public CreateOrderCommand(
Guid orderId,
string customerName,
decimal totalAmount,
string? notes = null)
: base(Guid.NewGuid())
{
OrderId = orderId;
CustomerName = customerName;
TotalAmount = totalAmount;
Notes = notes;
}
}Handlers
Handler dependencies should declare nullability appropriately:
public class CreateOrderCommandHandler : RequestHandler<CreateOrderCommand>
{
private readonly IOrderRepository _orderRepository;
private readonly ILogger<CreateOrderCommandHandler> _logger;
public CreateOrderCommandHandler(
IOrderRepository orderRepository,
ILogger<CreateOrderCommandHandler> logger)
{
// Dependencies are non-nullable - null check not needed
_orderRepository = orderRepository;
_logger = logger;
}
public override CreateOrderCommand Handle(CreateOrderCommand command)
{
// command parameter is non-nullable
var order = new Order
{
Id = command.OrderId,
CustomerName = command.CustomerName,
TotalAmount = command.TotalAmount,
Notes = command.Notes // Can be null
};
_orderRepository.Add(order);
return base.Handle(command);
}
}Message Mappers
Message mappers should handle nullable message properties:
public class CreateOrderCommandMessageMapper : IAmAMessageMapper<CreateOrderCommand>
{
public Message MapToMessage(CreateOrderCommand request, string? topic = null)
{
var header = new MessageHeader(
messageId: request.Id,
topic: topic ?? "orders.create",
messageType: MessageType.MT_COMMAND);
var body = new MessageBody(JsonSerializer.Serialize(request));
return new Message(header, body);
}
public CreateOrderCommand MapToRequest(Message message)
{
// Deserialize may return null
var dto = JsonSerializer.Deserialize<CreateOrderDto>(message.Body.Value);
// Handle potential null
if (dto == null)
throw new ArgumentException("Failed to deserialize message body", nameof(message));
return new CreateOrderCommand(
orderId: dto.OrderId,
customerName: dto.CustomerName ?? throw new ArgumentException("CustomerName is required"),
totalAmount: dto.TotalAmount,
notes: dto.Notes // Nullable
);
}
}V10 Changes
BREAKING CHANGE: Nullable reference types are now enabled across all Brighter projects. You may need to update your code to address compiler warnings.
What Changed
All Brighter projects now have
<Nullable>enable</Nullable>in their project filesRequired properties are now non-nullable by default
Optional properties are explicitly marked as nullable with
?Compiler warnings (CS8600-CS8629) will appear for potential null reference issues
Benefits
Compile-Time Safety: Catch potential null reference issues before runtime
Clear Intent: Explicitly declare which properties can be null
Better Documentation: API signatures clearly show nullability expectations
Reduced Runtime Errors: Fewer
NullReferenceExceptionerrors in production
Migration Guide
Step 1: Enable Nullable Reference Types
If you haven't already, enable nullable reference types in your project:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>Step 2: Address Compiler Warnings
After enabling nullable reference types, you'll see compiler warnings. Address them systematically:
CS8600: Converting null literal or possible null value to non-nullable type
Problem:
string name = null; // ⚠️ CS8600Solutions:
Option 1: Make the type nullable:
string? name = null; // ✅Option 2: Provide a non-null value:
string name = "default"; // ✅CS8601: Possible null reference assignment
Problem:
public class CreateOrderCommand : Command
{
public string CustomerName { get; set; }
}
var command = new CreateOrderCommand();
// CustomerName is null but declared as non-nullableSolutions:
Option 1: Make property nullable if it can be null:
public string? CustomerName { get; set; }Option 2: Initialize with a default value:
public string CustomerName { get; set; } = string.Empty;Option 3: Use required keyword (C# 11+):
public required string CustomerName { get; set; }
// Must be initialized:
var command = new CreateOrderCommand
{
CustomerName = "John" // Required
};CS8602: Dereference of a possibly null reference
Problem:
string? name = GetName();
int length = name.Length; // ⚠️ CS8602: name might be nullSolutions:
Option 1: Null check:
if (name != null)
{
int length = name.Length; // ✅
}Option 2: Null-conditional operator:
int? length = name?.Length; // ✅ Returns null if name is nullOption 3: Null-coalescing operator:
string safeName = name ?? "default";
int length = safeName.Length; // ✅Option 4: Null-forgiving operator (use cautiously):
int length = name!.Length; // ⚠️ Asserts name is not null (throws at runtime if wrong)CS8603: Possible null reference return
Problem:
public string GetName()
{
return null; // ⚠️ CS8603
}Solutions:
Option 1: Make return type nullable:
public string? GetName()
{
return null; // ✅
}Option 2: Return a non-null value:
public string GetName()
{
return "default"; // ✅
}CS8618: Non-nullable field must contain a non-null value when exiting constructor
Problem:
public class Order
{
public string CustomerName { get; set; } // ⚠️ CS8618
}Solutions:
Option 1: Initialize in constructor:
public class Order
{
public string CustomerName { get; set; }
public Order(string customerName)
{
CustomerName = customerName; // ✅
}
}Option 2: Initialize with default value:
public class Order
{
public string CustomerName { get; set; } = string.Empty; // ✅
}Option 3: Make nullable if appropriate:
public class Order
{
public string? CustomerName { get; set; } // ✅
}Option 4: Use required keyword (C# 11+):
public class Order
{
public required string CustomerName { get; set; } // ✅
}Step 3: Update Handler Code
Review your handlers for null safety:
public class ProcessOrderHandler : RequestHandler<ProcessOrderCommand>
{
private readonly IOrderService _orderService;
public ProcessOrderHandler(IOrderService orderService)
{
// V10: Add null check for dependencies
_orderService = orderService ?? throw new ArgumentNullException(nameof(orderService));
}
public override ProcessOrderCommand Handle(ProcessOrderCommand command)
{
// V10: command is non-nullable, but properties might be
if (string.IsNullOrEmpty(command.OrderId))
throw new ArgumentException("OrderId is required", nameof(command));
_orderService.ProcessOrder(command.OrderId);
return base.Handle(command);
}
}Step 4: Update Message Mappers
Ensure message mappers handle deserialization nullability:
public class OrderEventMessageMapper : IAmAMessageMapper<OrderCreatedEvent>
{
public OrderCreatedEvent MapToRequest(Message message)
{
// Deserialization can return null
var dto = JsonSerializer.Deserialize<OrderDto>(message.Body.Value);
// V10: Handle null explicitly
if (dto == null)
throw new InvalidOperationException("Failed to deserialize message");
// Validate required properties
if (string.IsNullOrEmpty(dto.OrderId))
throw new ArgumentException("OrderId is required");
return new OrderCreatedEvent(
orderId: dto.OrderId,
customerName: dto.CustomerName ?? "Unknown", // Handle nullable
createdAt: dto.CreatedAt
);
}
}Best Practices
1. Use Non-Nullable for Required Properties
Make your intent clear by using non-nullable types for properties that should never be null:
public class CreateUserCommand : Command
{
public string Email { get; } // Required - non-nullable
public string FirstName { get; } // Required - non-nullable
public string LastName { get; } // Required - non-nullable
public string? MiddleName { get; } // Optional - nullable
public CreateUserCommand(
string email,
string firstName,
string lastName,
string? middleName = null)
: base(Guid.NewGuid())
{
Email = email ?? throw new ArgumentNullException(nameof(email));
FirstName = firstName ?? throw new ArgumentNullException(nameof(firstName));
LastName = lastName ?? throw new ArgumentNullException(nameof(lastName));
MiddleName = middleName;
}
}2. Validate at Boundaries
Validate nullability at system boundaries (message mappers, API controllers):
public class CreateUserCommandMapper : IAmAMessageMapper<CreateUserCommand>
{
public CreateUserCommand MapToRequest(Message message)
{
var dto = JsonSerializer.Deserialize<CreateUserDto>(message.Body.Value);
// Validate at boundary
if (dto == null)
throw new InvalidOperationException("Message body is empty");
if (string.IsNullOrEmpty(dto.Email))
throw new ArgumentException("Email is required");
if (string.IsNullOrEmpty(dto.FirstName))
throw new ArgumentException("FirstName is required");
if (string.IsNullOrEmpty(dto.LastName))
throw new ArgumentException("LastName is required");
return new CreateUserCommand(
email: dto.Email,
firstName: dto.FirstName,
lastName: dto.LastName,
middleName: dto.MiddleName
);
}
}3. Use Null-Coalescing for Defaults
Use ?? operator to provide sensible defaults:
public class OrderQuery
{
public int PageSize { get; }
public int PageNumber { get; }
public OrderQuery(int? pageSize = null, int? pageNumber = null)
{
// Provide defaults for nullable parameters
PageSize = pageSize ?? 10;
PageNumber = pageNumber ?? 1;
}
}4. Document Nullability in XML Comments
Make nullability expectations explicit in documentation:
/// <summary>
/// Creates a new order.
/// </summary>
/// <param name="customerId">The customer ID (required, non-null)</param>
/// <param name="notes">Optional notes (can be null)</param>
public class CreateOrderCommand : Command
{
public Guid CustomerId { get; }
public string? Notes { get; }
}5. Avoid Null-Forgiving Operator Unless Certain
Use the null-forgiving operator (!) sparingly:
// ⚠️ Avoid unless you're certain
var name = user.Name!; // Asserts Name is not null
// ✅ Better: Check explicitly
if (user.Name == null)
throw new InvalidOperationException("User name is required");
var name = user.Name;6. Use Pattern Matching for Null Checks
Modern C# pattern matching provides concise null checks:
// Traditional
if (command.Notes != null)
{
ProcessNotes(command.Notes);
}
// Pattern matching
if (command.Notes is { } notes)
{
ProcessNotes(notes);
}
// Null-coalescing
var notes = command.Notes ?? "No notes provided";7. Consider Required Members (C# 11+)
Use required keyword for properties that must be initialized:
public class OrderDto
{
public required string OrderId { get; init; }
public required string CustomerId { get; init; }
public decimal Amount { get; init; }
public string? Notes { get; init; }
}
// Must initialize required properties
var dto = new OrderDto
{
OrderId = "123", // Required
CustomerId = "456", // Required
Amount = 100.0m,
Notes = null // Optional
};Common Patterns in Brighter
Command with Validation
public class UpdateProductCommand : Command
{
public Guid ProductId { get; }
public string Name { get; }
public decimal? Price { get; } // Nullable: only update if provided
public UpdateProductCommand(
Guid productId,
string name,
decimal? price = null)
: base(Guid.NewGuid())
{
ProductId = productId;
Name = name ?? throw new ArgumentNullException(nameof(name));
Price = price;
}
}Event with Optional Properties
public class OrderCompletedEvent : Event
{
public Guid OrderId { get; }
public DateTime CompletedAt { get; }
public string? CompletedBy { get; } // Nullable: may be system-generated
public OrderCompletedEvent(
Guid orderId,
DateTime completedAt,
string? completedBy = null)
: base(Guid.NewGuid())
{
OrderId = orderId;
CompletedAt = completedAt;
CompletedBy = completedBy;
}
}Handler with Optional Dependencies
public class SendEmailHandler : RequestHandler<SendEmailCommand>
{
private readonly IEmailService _emailService;
private readonly ILogger<SendEmailHandler>? _logger; // Optional
public SendEmailHandler(
IEmailService emailService,
ILogger<SendEmailHandler>? logger = null)
{
_emailService = emailService ?? throw new ArgumentNullException(nameof(emailService));
_logger = logger; // Can be null
}
public override SendEmailCommand Handle(SendEmailCommand command)
{
_logger?.LogInformation("Sending email to {Recipient}", command.Recipient);
_emailService.SendEmail(command.Recipient, command.Subject, command.Body);
return base.Handle(command);
}
}Troubleshooting
Warning: Treat as Errors
Some teams treat warnings as errors. If you're seeing build failures:
<PropertyGroup>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>You must address all nullable warnings before the build succeeds.
Suppressing Warnings (Not Recommended)
If you must temporarily suppress warnings (not recommended for production code):
#pragma warning disable CS8600
string name = null; // Warning suppressed
#pragma warning restore CS8600Gradual Migration
For large codebases, consider enabling nullable reference types gradually:
<!-- Start with warnings only -->
<PropertyGroup>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>Then address warnings incrementally and eventually enable TreatWarningsAsErrors.
Additional Resources
Last updated
Was this helpful?
