Skip to content

Troubleshooting

Practical problem/solution guide for Pragmatic.Patch. Each section covers a common issue, the likely causes, and the fix.


Your patch type fails to compile with diagnostic PRAG1900.

Add the partial keyword:

// Wrong
[GeneratePatch<Guest>]
public record UpdateGuestPatch;
// Right
[GeneratePatch<Guest>]
public partial record UpdateGuestPatch;

The source generator emits properties, methods, and a JSON converter into a partial class. Without partial, the compiler cannot merge the generated code with your declaration.


PRAG1901: Entity Type Could Not Be Resolved

Section titled “PRAG1901: Entity Type Could Not Be Resolved”

The generator reports that the entity type in [GeneratePatch<TEntity>] could not be resolved.

  1. Is the entity type accessible? The entity class must be in a referenced assembly or the same project. Check that the project reference exists in your .csproj.

  2. Is the entity type a class? [GeneratePatch] requires a class. Records, structs, and interfaces are not supported as entity types.

  3. Is the source generator analyzer referenced? Verify the Pragmatic.SourceGenerator is referenced with OutputItemType="Analyzer":

    <ProjectReference Include="..\Pragmatic.SourceGenerator\Pragmatic.SourceGenerator.csproj"
    OutputItemType="Analyzer"
    ReferenceOutputAssembly="false" />

The generator warns that the entity has no properties suitable for patching.

  1. Does the entity have writable properties? The generator looks for properties with either a public setter or a Set*() method:

    // No settable properties -- PRAG1902
    public class Immutable { public string Name { get; } }
    // Has settable properties -- works
    public class Mutable { public string Name { get; set; } }
    // Also works -- Set* method
    public class Encapsulated
    {
    public string Name { get; private set; }
    internal void SetName(string value) => Name = value;
    }
  2. Are all properties in the excluded list? If the entity only has Id, auditing, and soft delete properties, all of them are excluded and nothing is left to patch.


All Optional Properties Are Undefined After Deserialization

Section titled “All Optional Properties Are Undefined After Deserialization”

You send a PATCH request with values, but all Optional<T> properties remain Undefined in the handler.

  1. Is the patch type SG-generated? Only types annotated with [GeneratePatch<TEntity>] get a generated JSON converter. If you created a manual DTO with Optional<T> properties, register the fallback converter:

    builder.Services.ConfigureHttpJsonOptions(options =>
    options.SerializerOptions.Converters.Add(new OptionalConverterFactory()));
  2. Is Content-Type set to application/json? Without the correct content type, ASP.NET Core may not invoke the JSON deserializer.

  3. Are property names matching? The generated converter respects PropertyNamingPolicy. If your API uses camelCase but you send PascalCase (or vice versa), the converter handles both. But check for typos in property names.

  4. Is the request body valid JSON? Malformed JSON may fail silently. Check the response status code — a 400 indicates a deserialization error.


You call patch.ApplyTo(entity) but no properties change on the entity.

  1. Did the client send any fields? Check patch.ModifiedProperties — if it is empty, the JSON body was {} or all fields were absent.

  2. Are the property names correct in the JSON? If the JSON uses "first_name" but the property is FirstName, the converter won’t match (unless you have a custom naming policy configured).

  3. Is the entity the correct type? ApplyTo() is strongly typed. Verify you are passing the entity type specified in [GeneratePatch<TEntity>].

  4. Are the properties excluded? Check if the properties you expect to patch are in the excluded list (Id, auditing, soft delete, collections, entity references). See Concepts: Excluded Properties.


Null Sent in JSON But Property Stays Undefined

Section titled “Null Sent in JSON But Property Stays Undefined”

You send { "price": null } for a decimal property, but Price remains Undefined instead of Null.

decimal is a non-nullable value type. It cannot represent null, so the converter treats null as “not applicable” and leaves the property as Undefined.

If you need to support null values for a property, make the entity property nullable:

public class Product
{
// Non-nullable: null in JSON becomes Undefined
public decimal Price { get; set; }
// Nullable: null in JSON becomes Optional<decimal?>.Null
public decimal? DiscountRate { get; set; }
}

For decimal Price, the only way to set it is to send an actual value: { "price": 0 } produces Optional.Of(0m).


ModifiedProperties Contains Unexpected Entries

Section titled “ModifiedProperties Contains Unexpected Entries”

ModifiedProperties includes a field that the client did not send.

  1. Check the raw JSON. The generated converter tracks keys by their presence in the JSON object, not by their value. Even { "name": null } adds "Name" to ModifiedProperties.

  2. Check for default serialization behavior. Some JSON serializers include properties with default values. Ensure the client is configured to omit undefined fields.

  3. Check the naming policy. The converter normalizes property names through the configured PropertyNamingPolicy. Both "name" and "Name" resolve to the same property.


IntelliSense does not show the generated properties or ApplyTo() method.

  1. Restart the IDE. Source generators sometimes require a restart for the language server to pick up new outputs.

  2. Build the project. Run dotnet build to ensure the generator executes. Check the build output for errors.

  3. Check the analyzer reference. Ensure the Pragmatic.SourceGenerator is referenced correctly:

    <ProjectReference Include="..\Pragmatic.SourceGenerator\Pragmatic.SourceGenerator.csproj"
    OutputItemType="Analyzer"
    ReferenceOutputAssembly="false" />
  4. Check generated files. Look in obj/Debug/net10.0/generated/ for the generated .g.cs files. They should include {TypeName}.Patch.g.cs and {TypeName}.JsonConverter.g.cs.


Can I use Optional<T> outside of generated patch DTOs?

Section titled “Can I use Optional<T> outside of generated patch DTOs?”

Yes. Optional<T> is a public struct in the Pragmatic.Patch namespace. You can use it in any DTO, but you must register the OptionalConverterFactory for JSON serialization:

builder.Services.ConfigureHttpJsonOptions(options =>
options.SerializerOptions.Converters.Add(new OptionalConverterFactory()));

Note: The factory uses MakeGenericType and is not AOT-safe. For NativeAOT, register typed converters directly.

Can I customize which properties are excluded?

Section titled “Can I customize which properties are excluded?”

Not currently. The exclusion list is hardcoded in the generator (identity, auditing, soft delete, concurrency, navigation, entity references). If you need to exclude additional properties, use a separate entity type that omits them, or apply the patch selectively:

if (patch.SensitiveField.HasValue)
return new ValidationError("SensitiveField cannot be patched");
patch.ApplyTo(entity);

Yes. Both partial record and partial class are supported. Records are recommended because they provide value equality and with expressions out of the box.

How does this work with Pragmatic.Endpoints?

Section titled “How does this work with Pragmatic.Endpoints?”

Declare the patch as a [FromBody] property on the endpoint:

[Endpoint(HttpVerb.Patch, "/api/products/{productId}")]
public partial class PatchProductEndpoint : Endpoint<ProductDto>
{
[FromRoute] public required Guid ProductId { get; init; }
[FromBody] public required PatchProduct Patch { get; init; }
public override async Task<Result<ProductDto, NotFoundError>> HandleAsync(CancellationToken ct)
{
var product = await _products.GetByIdAsync(ProductId, ct);
if (product is null) return new NotFoundError("Product", ProductId.ToString());
Patch.ApplyTo(product);
await _uow.SaveChangesAsync(ct);
return ProductDto.FromEntity(product);
}
}

  1. Check the concepts guide for architecture overview
  2. Check the getting started guide for setup instructions
  3. Check the tri-state semantics guide for Optional<T> details
  4. Open an issue on the Pragmatic.Design repository with:
    • Your entity class definition
    • The [GeneratePatch] declaration
    • The JSON body you are sending
    • The expected vs actual behavior
    • .NET version