Query Facts

Querying facts gets a bit trickier. You start by writing a specification in LINQ.

Facts OfType

Every specification has to start with one or two known facts. For example, suppose you wanted to get all sites created by a specific user. You could write that using query syntax.

var sitesByUser = Given<User>.Match((user, facts) =>
    from site in facts.OfType<Site>()
    where site.creator == user
    select site
);

You could also write that using method syntax.

var sitesByUser = Given<User>.Match((user, facts) =>
    facts.OfType<Site>().Where(site => site.creator == user)
);

Or even use shorthand.

var sitesByUser = Given<User>.Match((user, facts) =>
    facts.OfType<Site>(site => site.creator == user)
);

No matter how you write it, this specification matches all facts of type Site related to the given user.

Facts Any

Now suppose that you wanted to filter out deleted sites. If someone had created a SiteDeleted fact, then we want to exclude it from the results. We can do this by checking that there are not any related facts:

var sitesByUser = Given<User>.Match((user, facts) =>
    from site in facts.OfType<Site>()
    where site.creator == user &&
        !facts.Any<SiteDeleted>(deleted => deleted.site == site)
    select site
);

You can't actually delete a fact. This is how you simulate deletion.

Sometimes users delete things accidentally. It happens. To let them restore a deleted fact, nest another "not any" clause.

var sitesByUser = Given<User>.Match((user, facts) =>
    from site in facts.OfType<Site>()
    where site.creator == user &&
        !facts.Any<SiteDeleted>(deleted => deleted.site == site &&
            !facts.Any<SiteRestored>(restored => restored.deleted == deleted)
        )
    select site
);

What if you want to delete it again? Do you need to nest it one level deeper?

No. Just create a new SiteDeleted.

Here's the model that makes this work:

[FactType("Blog.Site")]
public record Site(User creator, DateTime createdAt) { }

[FactType("Blog.Site.Deleted")]
public record SiteDeleted(Site site, DateTime deletedAt) { }

[FactType("Blog.Site.Restored")]
public record SiteRestored(SiteDeleted deleted) { }

SelectMany

You can extend a query to select more facts. The way you do that depends on which syntax you are using. If you favor the query syntax, then add another from clause.

var postsInAllSites = Given<User>.Match((user, facts) =>
    from site in facts.OfType<Site>()
    where site.creator == user &&
        !facts.Any<SiteDeleted>(deleted => deleted.site == site &&
            !facts.Any<SiteRestored>(restored => restored.deleted == deleted)
        )
    from post in facts.OfType<Post>()
    where post.site == site &&
        !facts.Any<PostDeleted>(deleted => deleted.post == post &&
            !facts.Any<PostRestored>(restored => restored.deleted == deleted)
        )
    select post

If instead you prefer the method syntax, then use SelectMany:

var postsInAllSites = Given<User>.Match((user, facts) =>
    facts.OfType<Site>()
        .Where(site => site.creator == user &&
            !facts.Any<SiteDeleted>(deleted => deleted.site == site &&
                !facts.Any<SiteRestored>(restored => restored.deleted == deleted)))
        .SelectMany(site => facts.OfType<Post>()
            .Where(post => post.site == site &&
                !facts.Any<PostDeleted>(deleted => deleted.post == post &&
                    !facts.Any<PostRestored>(restored => restored.deleted == deleted)))));

JinagaClient Hash

You can select more detailed information than the facts themselves. Do this with the select keyword or Select method. You will typically select an anonymous object containing fields of the fact. You can use the hash of the fact as an ID in that object.

var sitesByUser = Given<User>.Match((user, facts) =>
    from site in facts.OfType<Site>()
    where site.creator == user &&
        !facts.Any<SiteDeleted>(deleted => deleted.site == site &&
            !facts.Any<SiteRestored>(restored => restored.deleted == deleted)
        )
    select new
    {
        id = jinagaClient.Hash(site),
        createdAt = site.createdAt
    }
);

Facts Observable

If you are projecting results into a view model, you will want sub-selects to be observable. That will let you set up a call back when a child projection changes. Call facts.Observable with a sub query to get back an IObservableCollection.

var sitesByUser = Given<User>.Match((user, facts) =>
    from site in facts.OfType<Site>()
    where site.creator == user &&
        !facts.Any<SiteDeleted>(deleted => deleted.site == site &&
            !facts.Any<SiteRestored>(restored => restored.deleted == deleted)
        )
    select new
    {
        site,
        names = facts.Observable(
            from name in facts.OfType<SiteName>()
            where name.site == site &&
                !facts.Any<SiteName>(next => next.prior.Contains(name))
            select name.value
        ),
        domains = facts.Observable(
            from domain in facts.OfType<SiteDomain>()
            where domain.site == site &&
                !facts.Any<SiteDomain>(next => next.prior.Contains(domain))
            select domain.value
        )
    }
);

Continue With

Create a View Model

Jinaga is a product of Jinaga LLC.

Michael L Perry, President