diff --git a/README.md b/README.md index d7f6c70..0e61c7a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,51 @@ # Simpleverse.Repository ![build](https://github.com/lukaferlez/Simpleverse.Repository/workflows/build/badge.svg) + +A lightweight repository pattern built on top of [Dapper](https://github.com/DapperLib/Dapper), +providing generic query/list/add/update/upsert/delete operations for SQL Server, plus bulk +operations, application locks, and change tracking. + +## Packages + +- **Simpleverse.Repository** – database-agnostic abstractions (`IEntity`, `IQueryList`, `IAdd`, + `IUpdate`, `IUpsert`, `IReplace`, `IDelete`, etc.). +- **Simpleverse.Repository.Db** – SQL Server / Dapper implementation of those abstractions. + +## Install + +``` +dotnet add package Simpleverse.Repository.Db +``` + +## Quick start + +```csharp +using Microsoft.Data.SqlClient; +using Simpleverse.Repository.Db; +using Simpleverse.Repository.Db.Entity; +using Simpleverse.Repository.Db.SqlServer; + +// 1. Configure a repository +var repository = new SqlRepository(() => new SqlConnection(connectionString)); + +// 2. Map a model +[Dapper.Contrib.Extensions.Table("[Identity]")] +public class Identity +{ + [Dapper.Contrib.Extensions.Key] + public int Id { get; set; } + public string Name { get; set; } +} + +// 3. Query it – declaring a dedicated entity class isn't necessary for a quick +// start. The base Entity.Filter maps "virtual" model properties automatically, +// so the model itself can be used as the filter type. +var entity = new Entity(repository, new Table("I")); +var results = await entity.ListAsync(filter => filter.Name = "John"); +``` + +## Documentation + +See [docs/USAGE.md](docs/USAGE.md) for a full walkthrough covering entity mapping, +`TypeMeta` configuration, and common usage scenarios such as `ListAsync`, filtering, and +CRUD operations. diff --git a/docs/USAGE.md b/docs/USAGE.md new file mode 100644 index 0000000..399f1f6 --- /dev/null +++ b/docs/USAGE.md @@ -0,0 +1,222 @@ +# Usage Guide + +This guide covers the core concepts of `Simpleverse.Repository` / `Simpleverse.Repository.Db` +and walks through common scenarios such as entity mapping, configuring `TypeMeta`, and +querying data with `ListAsync`. + +## Packages + +- **Simpleverse.Repository** – Base, database-agnostic abstractions (`IEntity`, `IQueryList`, + `IAdd`, `IUpdate`, `IUpsert`, `IReplace`, `IDelete`, etc.). +- **Simpleverse.Repository.Db** – SQL Server / Dapper implementation of those abstractions, + including `DbRepository`, `SqlRepository`, `Table`, `TypeMeta`, `QueryBuilder`, bulk + operations (Insert/Update/Delete/Upsert/Merge) and application locks. + +Install via NuGet: + +``` +dotnet add package Simpleverse.Repository.Db +``` + +## Core concepts + +- Repositories are agnostic to the underlying datasource, so the same pattern can be used + whether the datasource is a DB, an API, a file system, etc. That is why the project is + split into two packages – the goal is a single interface consumers use without needing to + understand the underlying mechanics. +- Everything is type-safe and checkable at compile time – no strings or mappings involved, + so errors are caught early. +- Implementation details are fully up to the entity itself, so consumers can write their own + entity implementing the interface, backed by Entity Framework, direct SQL, stored + procedures, etc. + +### Entity mapping + +Models are plain POCOs mapped with [Dapper.Contrib](https://github.com/DapperLib/Dapper.Contrib) +attributes: + +```csharp +using Dapper.Contrib.Extensions; + +[Table("[Identity]")] +public class Identity +{ + [Key] + public virtual int Id { get; set; } + + public virtual string Name { get; set; } + + public virtual string From { get; set; } +} +``` + +Supported attributes: + +- `[Table("name")]` – overrides the table name (schema qualified names are supported, + e.g. `[Table("dbo.[Identity]")]`). +- `[Key]` – identity/auto-generated primary key. +- `[ExplicitKey]` – primary key supplied by the caller (not database generated). +- `[Computed]` – column is computed by the database and never written to. +- `[Write(false)]` – excludes the property from INSERT/UPDATE statements. +- `Simpleverse.Repository.Entity.ImmutableAttribute` – property can be inserted but is + never updated afterwards. + +### TypeMeta + +`Simpleverse.Repository.Db.Meta.TypeMeta` reflects over a model type once and caches the +result, exposing the property groups used to build SQL (`Properties`, `PropertiesKey`, +`PropertiesComputed`, `PropertiesExceptKeyAndComputed`, etc.) as well as the resolved +`TableName`. + +```csharp +using Simpleverse.Repository.Db.Meta; + +var meta = TypeMeta.Get(); + +meta.TableName; // "[Identity]" +meta.PropertiesKey; // [Id] +meta.PropertiesExceptKeyAndComputed; // [Name, From] +``` + +`TypeMeta` is looked up automatically whenever a `Table` is created, so in most cases you +never need to call `TypeMeta.Get()` yourself: + +```csharp +using Simpleverse.Repository.Db; + +var identityTable = new Table("I"); // "I" is the SQL alias used in generated queries +``` + +### Repository / connection setup + +`DbRepository` wraps a `Func` factory so that a fresh connection is opened per +operation: + +```csharp +using Simpleverse.Repository.Db; +using Simpleverse.Repository.Db.SqlServer; +using Microsoft.Data.SqlClient; + +var repository = new SqlRepository(() => new SqlConnection(connectionString)); +``` + +### Defining an Entity + +`Entity` (or the 4-generic overload with a separate update model) +implements list/get/exists/add/update/upsert/delete against a `Table`. + +For a quick start, declaring a dedicated entity class is not necessary – the simplest form +is to instantiate `Entity` directly: + +```csharp +using Simpleverse.Repository.Db; +using Simpleverse.Repository.Db.Entity; + +var entity = new Entity(repository, new Table("I")); +``` + +A dedicated class such as `IdentityEntity` below is purely a convenience once type names +start getting long (e.g. `Entity` once a +custom filter is introduced), or once behaviour needs to be extended beyond what the base +`Entity` class offers: + +```csharp +using Simpleverse.Repository.Db; +using Simpleverse.Repository.Db.Entity; + +public class IdentityEntity : Entity +{ + public IdentityEntity(DbRepository repository) + : base(repository, new Table("I")) + { + } +} +``` + +A separate `TFilter` class with a `Filter` override is only needed when you want to +change or expand filtering beyond the basic columns (e.g. custom joins or filters that +don't map 1:1 to a model property): + +```csharp +using Simpleverse.Repository.Db; +using Simpleverse.Repository.Db.Entity; + +public class IdentityQueryFilter +{ + public string Name { get; set; } +} + +public class IdentityEntity : Entity +{ + public IdentityEntity(DbRepository repository) + : base(repository, new Table("I")) + { + } + + protected override void Filter(QueryBuilder builder, IdentityQueryFilter filter) + { + builder.Where(x => x.Name, filter.Name); + base.Filter(builder, filter); + } +} +``` + +## Common scenarios + +### ListAsync + +`ListAsync` returns all rows matching an (optional) filter. It supports both action-based +setup and pre-built filter/options instances, and lets you project onto a different type +(including tuples for joined queries): + +```csharp +var entity = new IdentityEntity(repository); + +// list all +IEnumerable all = await entity.ListAsync(); + +// filter with an inline setup action +IEnumerable byName = await entity.ListAsync( + filterSetup: filter => filter.Name = "John" +); + +// project to a different shape +IEnumerable names = await entity.ListAsync( + filterSetup: filter => filter.Name = "John" +); + +// project a joined query onto a tuple +IEnumerable<(Identity identity, ExplicitKey explicitKey)> joined = + await entity.ListAsync<(Identity identity, ExplicitKey explicitKey)>(); +``` + +Cancellation is supported everywhere via the trailing `CancellationToken` parameter. + +### GetAsync / ExistsAsync + +```csharp +var byId = await entity.GetAsync(1); // by primary key +var byFilter = await entity.GetAsync(filter => filter.Name = "John"); +var exists = await entity.ExistsAsync(filter => filter.Name = "John"); +``` + +### Add / Update / Upsert / Delete + +```csharp +await entity.AddAsync(new Identity { Name = "Jane" }); + +await entity.UpdateAsync( + filter => filter.Name = "Jane", + options => options.Set(x => x.From, "referral") +); + +await entity.UpsertAsync(new Identity { Name = "Jane" }); + +await entity.DeleteAsync(filter => filter.Name = "Jane"); +``` + +## Further reading + +The test suite under `src/Simpleverse.Repository.Db.Test` contains runnable, end-to-end +examples for every operation (bulk insert/update/delete, merge/upsert, application locks, +change tracking, and more) and is a good reference for advanced scenarios not covered here.