DatabaseDtoGenerator.SourceGenerator
0.1.4
dotnet add package DatabaseDtoGenerator.SourceGenerator --version 0.1.4
NuGet\Install-Package DatabaseDtoGenerator.SourceGenerator -Version 0.1.4
<PackageReference Include="DatabaseDtoGenerator.SourceGenerator" Version="0.1.4" />
<PackageVersion Include="DatabaseDtoGenerator.SourceGenerator" Version="0.1.4" />
<PackageReference Include="DatabaseDtoGenerator.SourceGenerator" />
paket add DatabaseDtoGenerator.SourceGenerator --version 0.1.4
#r "nuget: DatabaseDtoGenerator.SourceGenerator, 0.1.4"
#:package DatabaseDtoGenerator.SourceGenerator@0.1.4
#addin nuget:?package=DatabaseDtoGenerator.SourceGenerator&version=0.1.4
#tool nuget:?package=DatabaseDtoGenerator.SourceGenerator&version=0.1.4
DatabaseDtoGenerator
A Roslyn incremental source generator that produces CRUD DTOs, repositories, services, paged list queries, and DI registration from annotated EF Core entities.
What gets generated
For an entity like Product, the generator can emit:
ProductCreateDtoProductReadDtoProductUpdateDtoProductDeleteDtoProductListQueryIProductRepository,ProductRepositoryBase<TDbContext>,ProductRepositoryIProductService,ProductServiceBase<TRepository>,ProductServiceGeneratedCrudServiceCollectionExtensions.AddGeneratedCrudServices()
Basic usage
Annotate an entity with GenerateCrud, mark DTO fields with DtoField, and optionally add operation-level role metadata with DtoRole.
using DatabaseDtoGenerator.Abstractions;
using Microsoft.EntityFrameworkCore;
namespace MyApp.Models;
public sealed class AppDbContext : DbContext
{
}
[GenerateCrud(typeof(AppDbContext))]
[DtoRole(DtoOperation.Create, "Admin")]
[DtoRole(DtoOperation.Read, "Admin", "User")]
public class Product
{
[DtoField(DtoOperation.Read)]
public int Id { get; set; }
[DtoField(DtoOperation.Create | DtoOperation.Read | DtoOperation.Update)]
public string Name { get; set; } = string.Empty;
[DtoField(DtoOperation.Create | DtoOperation.Read | DtoOperation.Update)]
public decimal Price { get; set; }
}
Accumulation DTOs
In addition to CRUD DTOs, you can define named accumulation DTOs directly on model properties with AccumulationDtoField.
- names are unique within a model
- referenced virtual model properties must explicitly set
NestedDtoName - there is no default nested accumulation DTO selection
[GenerateCrud(typeof(AppDbContext))]
public class Category
{
[DtoField(DtoOperation.Read)]
[AccumulationDtoField("Tiny")]
[AccumulationDtoField("Basic")]
public int Id { get; set; }
[AccumulationDtoField("Basic")]
public string Name { get; set; } = string.Empty;
}
[GenerateCrud(typeof(AppDbContext))]
public class Product
{
[DtoField(DtoOperation.Read)]
[AccumulationDtoField("Summary")]
public int Id { get; set; }
[AccumulationDtoField("Summary")]
public string Name { get; set; } = string.Empty;
[AccumulationDtoField("Summary", NestedDtoName = "Basic")]
public virtual Category? Category { get; set; }
[AccumulationDtoField("Summary", NestedDtoName = "Tiny")]
public virtual List<Category> RelatedCategories { get; set; } = new();
}
This generates additional types such as:
CategoryTinyAccumulationDtoCategoryBasicAccumulationDtoProductSummaryAccumulationDto
and the nested collection keeps the list shape:
public System.Collections.Generic.List<CategoryTinyAccumulationDto> RelatedCategories { get; set; } = default!;
Generated services also expose accumulation DTO operations and mapping hooks, for example:
Task<PagedResult<ProductSummaryAccumulationDto>> ListSummaryAsync(ProductListQuery query, CancellationToken cancellationToken = default);
Task<ProductSummaryAccumulationDto?> GetSummaryByIdAsync(int id, CancellationToken cancellationToken = default);
and protected mapping methods such as:
protected virtual ProductSummaryAccumulationDto MapToSummaryAccumulationDto(Product entity)
protected virtual void MapEntityToSummaryAccumulationDto(Product entity, ProductSummaryAccumulationDto dto)
Dependency injection
The generator also emits a registration helper:
using DatabaseDtoGenerator.Generated;
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase("app-db"));
builder.Services.AddGeneratedCrudServices();
Generated CRUD surface
Generated services expose:
Task<PagedResult<ProductReadDto>> ListAsync(ProductListQuery query, CancellationToken cancellationToken = default);
Task<ProductReadDto?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
Task<ProductReadDto?> CreateAsync(ProductCreateDto dto, CancellationToken cancellationToken = default);
Task<ProductReadDto?> UpdateAsync(int id, ProductUpdateDto dto, CancellationToken cancellationToken = default);
Task DeleteAsync(int id, CancellationToken cancellationToken = default);
Generated repositories expose matching persistence methods over entities.
Pagination and list queries
Generated repositories and services include list methods with optional paging.
By default, paging is disabled and all matching items are returned:
var result = await productService.ListAsync(new ProductListQuery());
Set a positive PageSize to enable paging:
var result = await productService.ListAsync(new ProductListQuery
{
PageNumber = 1,
PageSize = 20
});
The abstractions package provides:
PagedRequestPagedResult<T>
PagedRequest defaults:
PageNumber = 1PageSize = 0(unpaged; returns all items)
Generated repositories normalize invalid paging values and cap positive page sizes at 200.
The generated repository base class also exposes extensibility hooks for list behavior:
BuildListQuery()for the default ordered list queryApplyListQuery(...)for custom filtering or sortingNormalizePagedQuery(...)for paging normalization
The generated service base class exposes:
BeforeListAsync(...)AfterListAsync(...)CreatePagedReadResult(...)
Customizing list behavior
You can override repository hooks to add filtering or sorting:
public sealed class ProductRepository : ProductRepositoryBase<AppDbContext>
{
public ProductRepository(AppDbContext dbContext) : base(dbContext)
{
}
protected override IQueryable<Product> ApplyListQuery(IQueryable<Product> query, ProductListQuery listQuery)
=> query.Where(product => product.Price > 0);
}
You can also override service hooks to adjust the paged DTO result.
Naming and output options
GenerateCrudAttribute supports configuration for generated names:
[GenerateCrud(
typeof(AppDbContext),
OutputNamespace = "MyApp.Generated",
DtoSuffix = "Model",
ServiceSuffix = "CrudService",
RepositorySuffix = "Store")]
public class Product
{
[DtoField(DtoOperation.Read)]
public int Id { get; set; }
}
This generates types like:
ProductCreateModelProductReadModelIProductStoreProductStoreBase<TDbContext>IProductCrudServiceProductCrudService
Validation rules
The generator validates configuration values and reports diagnostics when needed:
OutputNamespacemust be a valid C# namespaceDtoSuffix,ServiceSuffix, andRepositorySuffixmay contain only letters, digits, and underscores
When an invalid value is provided, the generator reports a diagnostic and falls back to the default naming behavior.
Diagnostics
Current diagnostics include:
DDG001missingDbContexttypeDDG002missing key propertyDDG003missing readable DTO fieldsDDG004invalidDtoFieldconfigurationDDG005invalidDtoRoleconfigurationDDG006invalidOutputNamespaceDDG007invalid generated type suffixDDG008invalidAccumulationDtoFieldconfigurationDDG009invalid accumulation DTO reference
Current scope and limitations
Current support focuses on:
- single-entity CRUD generation
- operation-level authorization roles
- paged list generation
- repository/service extensibility through virtual hooks
Not generated yet:
- HTTP controllers or endpoints
- attribute-driven filtering/sorting metadata on list queries
- property-level authorization rules
End-to-end example
1. Define the entity
using DatabaseDtoGenerator.Abstractions;
using Microsoft.EntityFrameworkCore;
namespace MyApp.Models;
public sealed class AppDbContext : DbContext
{
public DbSet<Product> Products => Set<Product>();
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
}
[GenerateCrud(typeof(AppDbContext))]
[DtoRole(DtoOperation.Create, "Admin")]
[DtoRole(DtoOperation.Read, "Admin", "User")]
public class Product
{
[DtoField(DtoOperation.Read)]
public int Id { get; set; }
[DtoField(DtoOperation.Create | DtoOperation.Read | DtoOperation.Update)]
public string Name { get; set; } = string.Empty;
[DtoField(DtoOperation.Create | DtoOperation.Read | DtoOperation.Update)]
public decimal Price { get; set; }
}
2. Register the generated services
using DatabaseDtoGenerator.Generated;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase("app-db"));
builder.Services.AddGeneratedCrudServices();
3. Consume the generated service
public sealed class ProductFacade
{
private readonly IProductService _productService;
public ProductFacade(IProductService productService)
{
_productService = productService;
}
public async Task<ProductReadDto?> CreateAsync(CancellationToken cancellationToken = default)
{
return await _productService.CreateAsync(new ProductCreateDto
{
Name = "Ball",
Price = 9.99m
}, cancellationToken);
}
public async Task<PagedResult<ProductReadDto>> ListAsync(CancellationToken cancellationToken = default)
{
return await _productService.ListAsync(new ProductListQuery
{
PageNumber = 1,
PageSize = 20
}, cancellationToken);
}
}
4. Extend the generated repository or service when needed
public sealed class CustomProductRepository : ProductRepositoryBase<AppDbContext>
{
public CustomProductRepository(AppDbContext dbContext) : base(dbContext)
{
}
protected override IQueryable<Product> ApplyListQuery(IQueryable<Product> query, ProductListQuery listQuery)
=> query.Where(product => product.Price > 0).OrderBy(product => product.Name);
}
Testing
The repository contains:
- integration tests against the example app
- source-generator tests for diagnostics and emitted output
- snapshot-style tests for generated CRUD and registration source
Publishing
Package version
Package versioning is centralized in:
Library/Directory.Build.props
Update VersionPrefix before creating a release tag.
Versioning follows semantic versioning:
majorfor breaking changesminorfor new backward-compatible featurespatchfor backward-compatible fixes
Build packages locally
dotnet pack Library/src/DatabaseDtoGenerator.Abstractions/DatabaseDtoGenerator.Abstractions.csproj -c Release -o Library/artifacts
dotnet pack Library/src/DatabaseDtoGenerator.SourceGenerator/DatabaseDtoGenerator.SourceGenerator.csproj -c Release -o Library/artifacts
This produces:
DatabaseDtoGenerator.Abstractions.<version>.nupkgDatabaseDtoGenerator.Abstractions.<version>.snupkgDatabaseDtoGenerator.SourceGenerator.<version>.nupkg
Publish with GitLab CI
For a step-by-step release checklist, see Library/RELEASE.md.
The repository includes .gitlab-ci.yml with these stages:
testpackpublish
Publishing to NuGet requires a GitLab CI/CD variable:
NUGET_API_KEY
Recommended release flow:
- update
VersionPrefixinLibrary/Directory.Build.props - commit and push changes
- create and push a tag such as
v0.1.4 - GitLab automatically runs
test,pack, andpublish
The publish job pushes both .nupkg and .snupkg files to https://api.nuget.org/v3/index.json.
Install from NuGet
In a consuming project, reference both packages:
<ItemGroup>
<PackageReference Include="DatabaseDtoGenerator.Abstractions" Version="0.1.4" />
<PackageReference Include="DatabaseDtoGenerator.SourceGenerator" Version="0.1.4" PrivateAssets="all" />
</ItemGroup>
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.