Migration Guides
v10.0.3 → v10.1.0
This release is additive: no field, type, value, or constructor signature changed. Four IBAN constants were added to BaseErrorCode and BaseErrorMessage so the new IBAN value object can report failures with the same vocabulary as the other value objects.
What's new
| Field | BaseErrorCode | BaseErrorMessage |
|---|---|---|
IbanRequired | REQUIRED_IBAN | "The 'IBAN' field is required." |
IbanLength | IBAN_LENGTH | "'IBAN' length does not match the expected length for its country." |
IbanNotValid | NOT_VALID_IBAN | "'IBAN' format is invalid." |
IbanCheckDigits | IBAN_CHECK_DIGITS | "'IBAN' check digits are invalid." |
Do I need to change anything?
No. Upgrade the package; existing code compiles and behaves exactly as before. The new fields are static readonly strings, like every other field since v10.0.4.
Use them when you validate IBANs:
using RA.Utilities.Core.Constants;
using RA.Utilities.Core.Exceptions;
if (string.IsNullOrWhiteSpace(request.Iban))
{
throw new BadRequestException(new ValidationError(BaseErrorMessage.IbanRequired)
{
PropertyName = "IBAN",
ErrorCode = BaseErrorCode.IbanRequired
});
}
IbanLength is not raised by the value object
RA.Utilities.Core.ValueObjects.IBAN validates the IBAN structure (13–34 characters) and the ISO 7064 mod-97 check digits. It does not consult a per-country length table, so a value that is structurally valid but the wrong length for its country (for example, 20 characters for DE, which expects 22) passes. Use IbanLength in your own country-registry validation; see Validate an IBAN and pick the right error code.
This release contains two breaking changes. The values are unchanged in both cases; what changed is how the compiler and the JSON serializer treat them.
ResponseTypewas converted from arecordback to a plainenum.- Every constant field was migrated from
consttostatic readonly, and the containing classes are no longerstatic.
Breaking Change 1: ResponseType converted from record back to enum
In v10.0.4, ResponseType was changed from the type-safe enum pattern record (introduced in v10.0.2) back to a standard C# enum. The set of values is now closed, but everyday usage is simpler.
Why this change?
- A plain
enumsupports constant patterns in switch expressions,Enum.TryParse, and all other standard enum APIs - The built-in catalog already covers the standard HTTP outcome categories
- The underlying values (
Success = 0…GatewayTimeout = 17) are stable and explicit
The old record carried a built-in ResponseTypeJsonConverter that always wrote strings. The new enum has no built-in converter, so System.Text.Json writes numbers ("responseType": 8) by default. If your clients expect "NotFound", register JsonStringEnumConverter — see Serialization.
What changed
| Aspect | v10.0.3 (record) | v10.0.4 (enum) |
|---|---|---|
| Type | record ResponseType | enum ResponseType |
| Values | public static readonly fields | Enum members (Success = 0 … GatewayTimeout = 17) |
| String value | responseType.Value | responseType.ToString() |
Implicit conversion to string | ✅ Yes | ❌ No — use ToString() |
| Custom values | ✅ Via inheritance | ❌ Not supported — use the closest member |
| Switch expressions | ⚠️ Property patterns only ({ Value: "NotFound" } =>) | ✅ Constant patterns (ResponseType.NotFound =>) |
| JSON write (default options) | Plain strings via ResponseTypeJsonConverter | Numbers (NotFound → 8) |
JSON write with JsonStringEnumConverter | Plain strings | Plain strings ("NotFound") |
| JSON read (default options) | Exact (ordinal) match; empty/whitespace → Error | Numbers only; a string throws JsonException |
JSON read with JsonStringEnumConverter | Exact (ordinal) match | Case-insensitive names; unknown values throw |
default | null (reference type) | ResponseType.Success (value 0) |
Migration Steps
1. Replace .Value with ToString()
Before (v10.0.3):
string type = responseType.Value;
After (v10.0.4):
string type = responseType.ToString();
2. Replace construction and parsing
Before (v10.0.3):
ResponseType type = new("NotFound");
After (v10.0.4):
ResponseType type = Enum.Parse<ResponseType>("NotFound", ignoreCase: true);
// or, for untrusted input:
if (Enum.TryParse(value, ignoreCase: true, out ResponseType type) && Enum.IsDefined(type)) { }
3. Simplify switch expressions
Before (v10.0.3):
var label = responseType switch
{
{ Value: "NotFound" } => "Not Found",
{ Value: "Conflict" } => "Conflict",
_ => "Unknown"
};
After (v10.0.4):
var label = responseType switch
{
ResponseType.NotFound => "Not Found",
ResponseType.Conflict => "Conflict",
_ => "Unknown"
};
4. Remove custom derived response types
Custom types inheriting from ResponseType no longer compile. Replace them with the closest built-in member and carry domain specifics in the error code and message:
Before (v10.0.3):
public record PaymentRequiredResponseType : ResponseType
{
private PaymentRequiredResponseType(string value) : base(value) { }
public static readonly PaymentRequiredResponseType Instance = new("PaymentRequired");
}
throw new RaBaseException(402, PaymentRequiredResponseType.Instance, "Premium subscription required.");
After (v10.0.4 – v10.1.0):
// v10.0.4 – v10.1.0 — no dedicated member yet
throw new RaBaseException(402, ResponseType.Error, "Premium subscription required.");
From v10.1.1, ResponseType has a dedicated PaymentRequired member, so new code can use the typed values:
throw new RaBaseException(
BaseResponseCode.PaymentRequired,
ResponseType.PaymentRequired,
BaseResponseMessages.PaymentRequired
);
5. Decide how ResponseType is serialized
The numeric wire format is the default. To keep the names your clients saw in v10.0.3, register JsonStringEnumConverter on the serializer used by your API:
using System.Text.Json.Serialization;
// Minimal APIs and Results.Json (used by RA.Utilities.Api)
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
// MVC controllers
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
With the converter, ResponseType.NotFound is written as "NotFound"; without it, it is written as 8. Verify the contract your clients rely on:
{
"responseCode": 404,
"responseType": "NotFound",
"responseMessage": "Product with value '99' not found."
}
Behavior of JsonStringEnumConverter differs from the old converter: names are matched case-insensitively, but unknown names (and null for a non-nullable enum) throw JsonException instead of falling back to Error. Validate input before deserializing if you need tolerant behavior.
6. Check default handling
default(ResponseType) is now Success (0) rather than null. If a model uses ResponseType as an optional property, decide whether Success is a safe default or whether the property should be ResponseType?.
Breaking Change 2: Constant fields migrated from const to static readonly
Every field in BaseResponseCode, BaseResponseMessages, BaseErrorCode, BaseErrorMessage, and HeaderParameters is now public static readonly instead of public const. The containing types changed from static class to partial class (with CA1052 suppressed).
Why this change?
constvalues are embedded into every consumer assembly at compile time, so correcting a value requires recompiling every consumer.static readonlyvalues are resolved from the package assembly at runtime. A corrected value ships in a package update and takes effect without recompiling consumers.- The classes can now be split across files inside the package using
partial.
partial only permits splitting a type's definition inside the assembly that declares it. You cannot add a partial part to BaseResponseCode from your own project; the compiled package type cannot be extended across assemblies.
What changed
| Aspect | v10.0.3 | v10.0.4 |
|---|---|---|
| Declaration | public const string EmailRequired = "REQUIRED_EMAIL"; | public static readonly string EmailRequired = "REQUIRED_EMAIL"; |
| Containing type | static class | partial class (non-static) |
| Constant-required contexts | ✅ Allowed | ❌ Compiler error (CS0133, CS1736, CS0150, CS0182) |
| Value inlining | Inlined into consumer IL | Read from the package assembly at runtime |
| Changing a value | Requires recompiling consumers | Requires only updating the package |
| Instantiation | Not possible | Possible (new BaseResponseCode()), but pointless |
| Binary compatibility | — | ✅ Existing compiled consumers keep working |
Ordinary reads (BaseResponseCode.NotFound) | ✅ | ✅ Unchanged |
Migration Steps
1. Change const declarations that reference these fields
const requires a compile-time constant (CS0133). Use static readonly instead:
Before:
private const string NotFoundMessage = BaseResponseMessages.NotFound;
After:
private static readonly string NotFoundMessage = BaseResponseMessages.NotFound;
2. Replace default parameter values
Default parameter values must be compile-time constants (CS1736). Use a nullable sentinel plus a coalesce, or add an overload:
Before:
public IActionResult Fail(string message = BaseResponseMessages.BadRequest)
{
return BadRequest(message);
}
After:
public IActionResult Fail(string? message = null)
{
return BadRequest(message ?? BaseResponseMessages.BadRequest);
}
3. Replace case labels
case labels must be compile-time constants (CS0150). Use if/else, or switch on the literal value when that is clearer:
Before:
switch (error.ErrorCode)
{
case BaseErrorCode.EmailRequired:
ShowEmailError();
break;
}
After:
if (error.ErrorCode == BaseErrorCode.EmailRequired)
{
ShowEmailError();
}
4. Replace attribute arguments
Attribute arguments must be compile-time constants (CS0182). Define a local const in your own project (duplicating the literal) or inline the literal in the attribute:
Before:
[DefaultValue(BaseResponseMessages.NotFound)]
public string? Message { get; set; }
After:
private const string NotFoundMessage = "The requested resource was not found.";
[DefaultValue(NotFoundMessage)]
public string? Message { get; set; }
5. Rebuild and re-test
Call sites that only read the fields — by far the most common usage — require no source changes. A rebuild is still required to compile against the new package, and any JSON tests that asserted string output should be reviewed against Breaking Change 1.
v10.0.1 → v10.0.2
Breaking Change: ResponseType converted from enum to record
In v10.0.2, ResponseType was changed from a traditional C# enum to a type-safe enum pattern using a record. This enables consuming projects to define custom response types via inheritance, but it requires code changes.
Why this change?
- Traditional
enumtypes cannot be extended by consuming projects - The
recordpattern allows custom response types (e.g.,PaymentRequired,RateLimited) while retaining strong typing - JSON serialization produced plain strings (e.g.,
"NotFound") via the built-inResponseTypeJsonConverter
What changed
| Aspect | v10.0.1 (enum) | v10.0.2 (record) |
|---|---|---|
| Type | enum ResponseType | record ResponseType |
| Members | Success = 0, NotFound = 3, etc. | public static readonly ResponseType Success = new("Success") |
| Removed members | - | Database, Unknown |
| New members | - | GatewayTimeout |
| JSON | JsonStringEnumConverter | ResponseTypeJsonConverter |
| Extensible | ❌ No | ✅ Yes, via inheritance |
| Switch expressions | ✅ Yes (enum constant patterns) | ⚠️ Partial (use property patterns: { Value: "NotFound" } =>) |
ToString() | Returns enum name | Returns Value string |
Migration Steps
1. Replace enum-specific patterns
Before (v10.0.1):
// Switch expression
var label = responseType switch
{
ResponseType.NotFound => "Not Found",
ResponseType.Conflict => "Conflict",
_ => "Unknown"
};
// Enum parsing
var type = Enum.Parse<ResponseType>("NotFound");
// Default value
ResponseType defaultType = default; // Was ResponseType.Success (0)
After (v10.0.2):
// Use property pattern matching (switch still works on the record itself)
var label = responseType switch
{
{ Value: "NotFound" } => "Not Found",
{ Value: "Conflict" } => "Conflict",
_ => "Unknown"
};
// Or switch on the Value string directly
var label = responseType.Value switch
{
"NotFound" => "Not Found",
"Conflict" => "Conflict",
_ => "Unknown"
};
// Use the static fields directly; parse via the JSON converter or constructor
ResponseType type = new("NotFound");
// No default value — always use an explicit static field
ResponseType defaultType = ResponseType.Error;
2. Remove references to removed members
Database and Unknown were removed. Replace them:
// Before
ResponseType.Database // Removed
// After
ResponseType.Error // Use Error for general failures
// Before
ResponseType.Unknown // Removed
// After
ResponseType.Error // Use Error instead
3. Update RaBaseException usage
If you use RA.Utilities.Core.Exceptions v10.0.4+ (which depends on this version), the RaBaseException constructor now accepts ResponseType directly:
// Before (v10.0.3)
throw new RaBaseException("NotFound", "Product not found.", 404);
// After (v10.0.4)
throw new RaBaseException(404, ResponseType.NotFound, "Product not found.");
4. Serialization is unchanged
JSON output is identical. Custom response types also serialize as plain strings:
{
"responseCode": 404,
"responseType": "NotFound",
"responseMessage": "Product with value '99' not found."
}
No changes needed to API clients.
Full Changelog
See the package release notes for a complete version history.