Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ const config: UserConfig<DefaultTheme.Config> = {
{
text: 'Querying Documents', link: '/documents/querying/', collapsed: true, items: [
{ text: 'Loading Documents by Id', link: '/documents/querying/byid' },
{ text: 'Checking Document Existence', link: '/documents/querying/check-exists' },
{ text: 'Querying Documents with Linq', link: '/documents/querying/linq/' },
{ text: 'Supported Linq Operators', link: '/documents/querying/linq/operators' },
{ text: 'Querying within Child Collections', link: '/documents/querying/linq/child-collections' },
Expand Down
35 changes: 35 additions & 0 deletions docs/documents/querying/check-exists.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Checking Document Existence

Sometimes you only need to know whether a document with a given id exists in the database, without actually loading and deserializing the full document. Marten provides the `CheckExistsAsync` API for this purpose, which issues a lightweight `SELECT EXISTS(...)` query against PostgreSQL. This avoids the overhead of JSON deserialization and object materialization, making it significantly more efficient than loading the document just to check if it's there.

## Usage

`CheckExistsAsync<T>` is available on `IQuerySession` (and therefore also on `IDocumentSession`). It supports all identity types: `Guid`, `int`, `long`, `string`, and strongly-typed identifiers.

<!-- snippet: sample_check_exists_usage -->
<!-- endSnippet -->

## Supported Identity Types

| Id Type | Supported |
|---------|-----------|
| `Guid` | Yes |
| `int` | Yes |
| `long` | Yes |
| `string` | Yes |
| `object` | Yes (for dynamic id types) |
| Strong-typed ids (Vogen, record structs, etc.) | Yes (via `object` overload) |

## Batched Queries

`CheckExists<T>` is also available as part of [batched queries](/documents/querying/batched-queries), allowing you to check existence of multiple documents in a single round-trip to the database:

<!-- snippet: sample_check_exists_batch_usage -->
<!-- endSnippet -->

## Behavior Notes

- Returns `true` if the document exists, `false` otherwise.
- Respects soft-delete filters: if a document type uses soft deletes, a soft-deleted document will return `false`.
- Respects multi-tenancy: the check is scoped to the current session's tenant.
- Does **not** load the document into the identity map or trigger any deserialization.
55 changes: 52 additions & 3 deletions docs/documents/querying/linq/operators.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,10 +217,59 @@ public void using_take_and_skip(IDocumentSession session)

TODO -- link to the paging support

## Grouping Operators
## GroupBy()

Sorry, but Marten does not yet support `GroupBy()`. You can track [this GitHub issue](https://github.com/JasperFx/marten/issues/569) to follow
any future work on this Linq operator.
Marten supports the `GroupBy()` LINQ operator for grouping documents by one or more keys and computing aggregate values. GroupBy translates to SQL `GROUP BY` with aggregate functions like `COUNT`, `SUM`, `MIN`, `MAX`, and `AVG`.

### Simple Key with Aggregates

<!-- snippet: sample_group_by_simple_key_with_count -->
<!-- endSnippet -->

### Composite Key

You can group by multiple properties using an anonymous type:

```csharp
var results = await session.Query<Target>()
.GroupBy(x => new { x.Color, x.String })
.Select(g => new { Color = g.Key.Color, Text = g.Key.String, Count = g.Count() })
.ToListAsync();
```

### Where Before GroupBy

Filter documents before grouping with a standard `Where()` clause:

```csharp
var results = await session.Query<Target>()
.Where(x => x.Number > 20)
.GroupBy(x => x.Color)
.Select(g => new { Color = g.Key, Count = g.Count() })
.ToListAsync();
```

### HAVING (Where After GroupBy)

Filter groups with a `Where()` clause after `GroupBy()` -- this translates to SQL `HAVING`:

```csharp
var results = await session.Query<Target>()
.GroupBy(x => x.Color)
.Where(g => g.Count() > 1)
.Select(g => new { Color = g.Key, Count = g.Count() })
.ToListAsync();
```

### Supported Aggregates

The following aggregate methods are supported within GroupBy projections:

- `g.Count()` / `g.LongCount()` -- `COUNT(*)`
- `g.Sum(x => x.Property)` -- `SUM(property)`
- `g.Min(x => x.Property)` -- `MIN(property)`
- `g.Max(x => x.Property)` -- `MAX(property)`
- `g.Average(x => x.Property)` -- `AVG(property)`

## Distinct()

Expand Down
11 changes: 11 additions & 0 deletions docs/events/dcb.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,17 @@ catch (DcbConcurrencyException ex)
The consistency check only detects events that match the **same tag query**. Events appended to unrelated tags or streams will not cause a violation.
:::

## Checking Event Existence

If you only need to know whether any events matching a tag query exist -- without loading or deserializing them -- use `EventsExistAsync`. This is a lightweight `SELECT EXISTS(...)` query that avoids the overhead of fetching and materializing event data:

<!-- snippet: sample_marten_dcb_events_exist_async -->
<!-- endSnippet -->

This is useful for guard clauses and validation logic in DCB workflows where you need to check preconditions before appending new events.

`EventsExistAsync` is also available in batch queries via `batch.Events.EventsExist(query)`.

## How It Works

### Storage
Expand Down
195 changes: 195 additions & 0 deletions src/DocumentDbTests/Reading/check_document_exists.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
using System;
using System.Threading.Tasks;
using Marten;
using Marten.Testing.Documents;
using Marten.Testing.Harness;
using Shouldly;
using Xunit;

namespace DocumentDbTests.Reading;

public class check_document_exists: IntegrationContext
{
public check_document_exists(DefaultStoreFixture fixture): base(fixture)
{
}

[Fact]
public async Task check_exists_by_guid_id_hit()
{
var doc = new GuidDoc { Id = Guid.NewGuid() };
theSession.Store(doc);
await theSession.SaveChangesAsync();

var exists = await theSession.CheckExistsAsync<GuidDoc>(doc.Id);
exists.ShouldBeTrue();
}

[Fact]
public async Task check_exists_by_guid_id_miss()
{
var exists = await theSession.CheckExistsAsync<GuidDoc>(Guid.NewGuid());
exists.ShouldBeFalse();
}

[Fact]
public async Task check_exists_by_int_id_hit()
{
var doc = new IntDoc { Id = 42 };
theSession.Store(doc);
await theSession.SaveChangesAsync();

var exists = await theSession.CheckExistsAsync<IntDoc>(42);
exists.ShouldBeTrue();
}

[Fact]
public async Task check_exists_by_int_id_miss()
{
var exists = await theSession.CheckExistsAsync<IntDoc>(999999);
exists.ShouldBeFalse();
}

[Fact]
public async Task check_exists_by_long_id_hit()
{
var doc = new LongDoc { Id = 200L };
theSession.Store(doc);
await theSession.SaveChangesAsync();

var exists = await theSession.CheckExistsAsync<LongDoc>(200L);
exists.ShouldBeTrue();
}

[Fact]
public async Task check_exists_by_long_id_miss()
{
var exists = await theSession.CheckExistsAsync<LongDoc>(999999L);
exists.ShouldBeFalse();
}

[Fact]
public async Task check_exists_by_string_id_hit()
{
var doc = new StringDoc { Id = "test-doc" };
theSession.Store(doc);
await theSession.SaveChangesAsync();

var exists = await theSession.CheckExistsAsync<StringDoc>("test-doc");
exists.ShouldBeTrue();
}

[Fact]
public async Task check_exists_by_string_id_miss()
{
var exists = await theSession.CheckExistsAsync<StringDoc>("nonexistent");
exists.ShouldBeFalse();
}

#region sample_check_exists_usage

[Fact]
public async Task check_exists_by_object_id()
{
var doc = new GuidDoc { Id = Guid.NewGuid() };
theSession.Store(doc);
await theSession.SaveChangesAsync();

// Use the object overload for dynamic id types
var exists = await theSession.CheckExistsAsync<GuidDoc>((object)doc.Id);
exists.ShouldBeTrue();
}

#endregion
}

public class check_document_exists_in_batch: IntegrationContext
{
public check_document_exists_in_batch(DefaultStoreFixture fixture): base(fixture)
{
}

#region sample_check_exists_batch_usage

[Fact]
public async Task check_exists_in_batch_by_guid_id()
{
var doc = new GuidDoc { Id = Guid.NewGuid() };
theSession.Store(doc);
await theSession.SaveChangesAsync();

var batch = theSession.CreateBatchQuery();
var existsHit = batch.CheckExists<GuidDoc>(doc.Id);
var existsMiss = batch.CheckExists<GuidDoc>(Guid.NewGuid());
await batch.Execute();

(await existsHit).ShouldBeTrue();
(await existsMiss).ShouldBeFalse();
}

#endregion

[Fact]
public async Task check_exists_in_batch_by_int_id()
{
var doc = new IntDoc { Id = 77 };
theSession.Store(doc);
await theSession.SaveChangesAsync();

var batch = theSession.CreateBatchQuery();
var existsHit = batch.CheckExists<IntDoc>(77);
var existsMiss = batch.CheckExists<IntDoc>(888888);
await batch.Execute();

(await existsHit).ShouldBeTrue();
(await existsMiss).ShouldBeFalse();
}

[Fact]
public async Task check_exists_in_batch_by_long_id()
{
var doc = new LongDoc { Id = 300L };
theSession.Store(doc);
await theSession.SaveChangesAsync();

var batch = theSession.CreateBatchQuery();
var existsHit = batch.CheckExists<LongDoc>(300L);
var existsMiss = batch.CheckExists<LongDoc>(999999L);
await batch.Execute();

(await existsHit).ShouldBeTrue();
(await existsMiss).ShouldBeFalse();
}

[Fact]
public async Task check_exists_in_batch_by_string_id()
{
var doc = new StringDoc { Id = "batch-test" };
theSession.Store(doc);
await theSession.SaveChangesAsync();

var batch = theSession.CreateBatchQuery();
var existsHit = batch.CheckExists<StringDoc>("batch-test");
var existsMiss = batch.CheckExists<StringDoc>("nope");
await batch.Execute();

(await existsHit).ShouldBeTrue();
(await existsMiss).ShouldBeFalse();
}

[Fact]
public async Task check_exists_in_batch_by_object_id()
{
var doc = new GuidDoc { Id = Guid.NewGuid() };
theSession.Store(doc);
await theSession.SaveChangesAsync();

var batch = theSession.CreateBatchQuery();
var existsHit = batch.CheckExists<GuidDoc>((object)doc.Id);
var existsMiss = batch.CheckExists<GuidDoc>((object)Guid.NewGuid());
await batch.Execute();

(await existsHit).ShouldBeTrue();
(await existsMiss).ShouldBeFalse();
}
}
Loading
Loading