diff --git a/Source/Rendering.Cratis/Authorization/ReadModelAuthorization.cs b/Source/Rendering.Cratis/Authorization/ReadModelAuthorization.cs
new file mode 100644
index 0000000..43e789c
--- /dev/null
+++ b/Source/Rendering.Cratis/Authorization/ReadModelAuthorization.cs
@@ -0,0 +1,74 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Cratis.Screenplay.Syntax;
+using Cratis.Stage.Rendering.Cratis.Naming;
+
+namespace Cratis.Stage.Rendering.Cratis.Authorization;
+
+///
+/// Renders the authorization attribute guarding one read model's rendered read surface, from the queries that
+/// read that read model.
+///
+///
+///
+/// A slice declares as many read models as its behavior needs, and a query names the one it reads with its
+/// return type. Authorization has to follow that attribution: a query returning OverdueInvoices says
+/// nothing about who may read InvoiceSummary, so the union guarding a read model is taken over its own
+/// queries and no others.
+///
+///
+/// The attribution is a correctness concern rather than a tidy one, because getting it wrong fails in the
+/// permissive direction. collapses a union to AllowAnonymous
+/// as soon as one member is unguarded — correctly, for queries that genuinely read the same model — so drawing
+/// the union from every query in the slice let a single unguarded query on an unrelated read model publish a
+/// guarded one to everyone.
+///
+///
+/// When the slice declares queries but none of them return this read model, the document has stated who may
+/// read the slice's other models and nothing about this one. Reading that silence as anonymous would take
+/// permission from a document that never granted it, so it falls back to requiring an authenticated caller and
+/// reports it — the same fallback used for every other requirement no attribute expresses faithfully. A slice
+/// declaring no query at all says nothing about reading anything, and keeps the AllowAnonymous that
+/// absence has always rendered as.
+///
+///
+public static class ReadModelAuthorization
+{
+ ///
+ /// Renders the authorization attribute for a read model, from the queries that return it.
+ ///
+ /// The rendered read model's C# type name.
+ /// Every the slice declares, across all of its read models.
+ /// The the policies are resolved against.
+ /// Collects anything that could not be rendered faithfully.
+ /// The attribute content, without the surrounding brackets.
+ public static string Render(
+ string readModel, IEnumerable queries, ApplicationSet applicationSet, ICollection diagnostics)
+ {
+ var declared = queries.ToArray();
+ var own = declared.Where(query => Reads(query, readModel)).ToArray();
+ var subject = $"Read model '{readModel}'";
+
+ if (own.Length == 0 && declared.Length > 0)
+ {
+ diagnostics.Add(
+ $"{subject} is returned by none of the {declared.Length} query declaration(s) in its slice — the " +
+ "document states who may read the other read models and nothing about this one, so its rendered " +
+ "read surface requires an authenticated caller rather than being left open to everyone.");
+ return "Authorize";
+ }
+
+ return AuthorizationRenderer.Render(own.Select(query => query.Authorize), applicationSet, subject, diagnostics);
+ }
+
+ ///
+ /// Whether a query reads the given read model. The return type names it, whether the query answers with one
+ /// instance or a collection of them.
+ ///
+ /// The to attribute.
+ /// The rendered read model's C# type name.
+ /// True when the query reads that read model.
+ static bool Reads(QuerySyntax query, string readModel) =>
+ Identifiers.ToPascalCase(query.ReturnType.Name).Equals(readModel, StringComparison.Ordinal);
+}
diff --git a/Source/Rendering.Cratis/Renderers/StateViewSliceRenderer.cs b/Source/Rendering.Cratis/Renderers/StateViewSliceRenderer.cs
index 3f2f70b..d8083ab 100644
--- a/Source/Rendering.Cratis/Renderers/StateViewSliceRenderer.cs
+++ b/Source/Rendering.Cratis/Renderers/StateViewSliceRenderer.cs
@@ -21,9 +21,10 @@ namespace Cratis.Stage.Rendering.Cratis.Renderers;
///
/// The slice's declared query blocks are not rendered — the read model gets a fixed all/by-id pair
/// instead — so the authorization those queries declare would have nowhere to land and the read surface would be
-/// open to everyone. The read model therefore carries the union of what its declared queries permit: every
-/// caller the document lets read this model through some query can still read it, and nobody else can. That the
-/// declared queries themselves are missing is reported separately.
+/// open to everyone. The read model therefore carries the union of what the queries that read it
+/// permit: every caller the document lets read this model through some query can still read it, and nobody else
+/// can. Which queries those are is 's job. That the declared queries
+/// themselves are missing is reported separately.
///
public class StateViewSliceRenderer : ISliceRenderer
{
@@ -43,10 +44,10 @@ public RenderedFile Render(LocatedSlice slice, ApplicationSet applicationSet, st
var referenced = new List(EventRenderer.ReferencedNames(slice.Slice.Events));
- // A slice may declare several projections. Only the first is rendered — the read model carries a fixed
- // all/by-id pair with no way to say which projection a query belongs to, so rendering the rest would
- // guard each of them with the same union of every query's authorization. The ones left out are reported
- // by UnrenderedConstructs rather than dropped in silence.
+ // A slice may declare several projections. Only the first is rendered; the ones left out are reported by
+ // UnrenderedConstructs rather than dropped in silence. Authorization is no longer what holds this back —
+ // a query names the read model it reads with its return type, so each read model's guard is attributable
+ // and the ones that are dropped no longer widen the one that survives.
if (slice.Slice.Projections.FirstOrDefault() is { } projection)
{
RenderReadModel(builder, projection, slice.Slice.Queries, applicationSet, referenced, diagnostics);
@@ -106,8 +107,7 @@ static void RenderReadModel(
ReportUnrenderedBlocks(builder, projection, typeName, diagnostics);
- var authorization = AuthorizationRenderer.Render(
- queries.Select(query => query.Authorize), applicationSet, $"Read model '{typeName}'", diagnostics);
+ var authorization = ReadModelAuthorization.Render(typeName, queries, applicationSet, diagnostics);
var parameters = string.Join(", ", properties.Select(property => RenderParameter(property, keyProperty)));
builder.Attribute("ReadModel").Attribute(authorization).OpenBlock($"public record {typeName}({parameters})");
diff --git a/Source/Rendering.Cratis/Renderers/UnrenderedConstructs.cs b/Source/Rendering.Cratis/Renderers/UnrenderedConstructs.cs
index 4a129ab..260b3d3 100644
--- a/Source/Rendering.Cratis/Renderers/UnrenderedConstructs.cs
+++ b/Source/Rendering.Cratis/Renderers/UnrenderedConstructs.cs
@@ -63,7 +63,7 @@ public static void Report(CSharpCodeBuilder builder, SliceSyntax slice, Rendered
yield return (
slice.Queries.Count(),
"query",
- "the read model carries the fixed all/by-id pair instead, guarded by the union of the declared queries' authorization.");
+ "the read model carries the fixed all/by-id pair instead, guarded by the union of the authorization declared by the queries that read it.");
yield return (
slice.Queries.Count(query => query.Performer is not null),
"query performer",
diff --git a/Source/Rendering.Cratis/for_ReadModelAuthorization/given/an_application_with_policies.cs b/Source/Rendering.Cratis/for_ReadModelAuthorization/given/an_application_with_policies.cs
new file mode 100644
index 0000000..eb38ab2
--- /dev/null
+++ b/Source/Rendering.Cratis/for_ReadModelAuthorization/given/an_application_with_policies.cs
@@ -0,0 +1,74 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Cratis.Screenplay.Diagnostics;
+using Cratis.Screenplay.Syntax;
+using Cratis.Specifications;
+using Cratis.Stage.Rendering.Cratis.Authorization;
+
+namespace Cratis.Stage.Rendering.Cratis.for_ReadModelAuthorization.given;
+
+///
+/// An application declaring the one role policy these specs authorize against, and the helpers for hanging
+/// query declarations off the read models a slice declares.
+///
+public class an_application_with_policies : Specification
+{
+ protected ApplicationSet _applicationSet = null!;
+ protected List _diagnostics = null!;
+
+ void Establish()
+ {
+ var application = new ApplicationSyntax(
+ [],
+ [],
+ [new PolicySyntax("Accountant", new RoleConditionSyntax("Accountant", SourceLocation.Start), null, SourceLocation.Start)],
+ [],
+ SourceLocation.Start);
+
+ _applicationSet = new ApplicationSet([application]);
+ _diagnostics = [];
+ }
+
+ ///
+ /// Builds a query answering with one instance of a read model, guarded by the named policies — or by nothing
+ /// at all when none are named.
+ ///
+ /// The name of the query.
+ /// The read model the query reads, which its return type names.
+ /// The policies any one of which may read it; none leaves the query unguarded.
+ /// The .
+ protected static QuerySyntax Query(string name, string readModel, params string[] policies) =>
+ Build(name, readModel, isCollection: false, policies);
+
+ ///
+ /// Builds a query answering with a collection of a read model, which reads it just as a single instance does.
+ ///
+ /// The name of the query.
+ /// The read model the query reads, which its return type names.
+ /// The policies any one of which may read it; none leaves the query unguarded.
+ /// The .
+ protected static QuerySyntax QueryForMany(string name, string readModel, params string[] policies) =>
+ Build(name, readModel, isCollection: true, policies);
+
+ static QuerySyntax Build(string name, string readModel, bool isCollection, string[] policies) =>
+ new(
+ name,
+ new TypeRefSyntax(readModel, isCollection, false, SourceLocation.Start),
+ null,
+ [],
+ Authorize(policies),
+ SourceLocation.Start);
+
+ static AuthorizeSyntax? Authorize(string[] policies) =>
+ policies.Length == 0
+ ? null
+ : new AuthorizeSyntax(
+ policies
+ .Select(policy => (PolicyRequirementSyntax)new PolicyReferenceSyntax(policy, SourceLocation.Start))
+ .Aggregate((left, right) => new LogicalPolicyRequirementSyntax(left, LogicalOperator.Or, right, SourceLocation.Start)),
+ SourceLocation.Start);
+
+ protected string Render(string readModel, params QuerySyntax[] queries) =>
+ ReadModelAuthorization.Render(readModel, queries, _applicationSet, _diagnostics);
+}
diff --git a/Source/Rendering.Cratis/for_ReadModelAuthorization/when_rendering_a_read_models_authorization/and_another_read_model_has_an_unguarded_query.cs b/Source/Rendering.Cratis/for_ReadModelAuthorization/when_rendering_a_read_models_authorization/and_another_read_model_has_an_unguarded_query.cs
new file mode 100644
index 0000000..94c7899
--- /dev/null
+++ b/Source/Rendering.Cratis/for_ReadModelAuthorization/when_rendering_a_read_models_authorization/and_another_read_model_has_an_unguarded_query.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Cratis.Specifications;
+using Cratis.Stage.Rendering.Cratis.for_ReadModelAuthorization.given;
+using Xunit;
+
+namespace Cratis.Stage.Rendering.Cratis.for_ReadModelAuthorization.when_rendering_a_read_models_authorization;
+
+public class and_another_read_model_has_an_unguarded_query : an_application_with_policies
+{
+ string _attribute = null!;
+
+ void Because() => _attribute = Render(
+ "InvoiceSummary",
+ Query("GetInvoiceSummary", "InvoiceSummary", "Accountant"),
+ QueryForMany("GetOverdueInvoices", "OverdueInvoices"));
+
+ [Fact] void should_keep_the_guard_its_own_query_declares() => _attribute.ShouldEqual("Roles(\"Accountant\")");
+}
diff --git a/Source/Rendering.Cratis/for_ReadModelAuthorization/when_rendering_a_read_models_authorization/and_its_query_answers_with_a_collection.cs b/Source/Rendering.Cratis/for_ReadModelAuthorization/when_rendering_a_read_models_authorization/and_its_query_answers_with_a_collection.cs
new file mode 100644
index 0000000..8ed4588
--- /dev/null
+++ b/Source/Rendering.Cratis/for_ReadModelAuthorization/when_rendering_a_read_models_authorization/and_its_query_answers_with_a_collection.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Cratis.Specifications;
+using Cratis.Stage.Rendering.Cratis.for_ReadModelAuthorization.given;
+using Xunit;
+
+namespace Cratis.Stage.Rendering.Cratis.for_ReadModelAuthorization.when_rendering_a_read_models_authorization;
+
+public class and_its_query_answers_with_a_collection : an_application_with_policies
+{
+ string _attribute = null!;
+
+ void Because() => _attribute = Render("InvoiceList", QueryForMany("ListInvoices", "InvoiceList", "Accountant"));
+
+ [Fact] void should_attribute_it_to_the_read_model_the_return_type_names() => _attribute.ShouldEqual("Roles(\"Accountant\")");
+}
diff --git a/Source/Rendering.Cratis/for_ReadModelAuthorization/when_rendering_a_read_models_authorization/and_no_query_returns_it.cs b/Source/Rendering.Cratis/for_ReadModelAuthorization/when_rendering_a_read_models_authorization/and_no_query_returns_it.cs
new file mode 100644
index 0000000..27d5750
--- /dev/null
+++ b/Source/Rendering.Cratis/for_ReadModelAuthorization/when_rendering_a_read_models_authorization/and_no_query_returns_it.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Cratis.Specifications;
+using Cratis.Stage.Rendering.Cratis.for_ReadModelAuthorization.given;
+using Xunit;
+
+namespace Cratis.Stage.Rendering.Cratis.for_ReadModelAuthorization.when_rendering_a_read_models_authorization;
+
+public class and_no_query_returns_it : an_application_with_policies
+{
+ string _attribute = null!;
+
+ void Because() => _attribute = Render("InvoiceSummary", QueryForMany("GetOverdueInvoices", "OverdueInvoices", "Accountant"));
+
+ [Fact] void should_require_an_authenticated_caller() => _attribute.ShouldEqual("Authorize");
+ [Fact] void should_report_that_nothing_states_who_may_read_it() =>
+ _diagnostics.ShouldContain(
+ "Read model 'InvoiceSummary' is returned by none of the 1 query declaration(s) in its slice — the document " +
+ "states who may read the other read models and nothing about this one, so its rendered read surface " +
+ "requires an authenticated caller rather than being left open to everyone.");
+}
diff --git a/Source/Rendering.Cratis/for_ReadModelAuthorization/when_rendering_a_read_models_authorization/and_one_of_its_own_queries_is_unguarded.cs b/Source/Rendering.Cratis/for_ReadModelAuthorization/when_rendering_a_read_models_authorization/and_one_of_its_own_queries_is_unguarded.cs
new file mode 100644
index 0000000..dbb056a
--- /dev/null
+++ b/Source/Rendering.Cratis/for_ReadModelAuthorization/when_rendering_a_read_models_authorization/and_one_of_its_own_queries_is_unguarded.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Cratis.Specifications;
+using Cratis.Stage.Rendering.Cratis.for_ReadModelAuthorization.given;
+using Xunit;
+
+namespace Cratis.Stage.Rendering.Cratis.for_ReadModelAuthorization.when_rendering_a_read_models_authorization;
+
+///
+/// The collapse to anonymous is correct for queries that genuinely read the same read model — the document
+/// lets anyone read it through the unguarded one, so the rendered pair standing in for both must too.
+///
+public class and_one_of_its_own_queries_is_unguarded : an_application_with_policies
+{
+ string _attribute = null!;
+
+ void Because() => _attribute = Render(
+ "InvoiceSummary",
+ Query("GetInvoiceSummary", "InvoiceSummary", "Accountant"),
+ Query("PeekInvoiceSummary", "InvoiceSummary"));
+
+ [Fact] void should_follow_the_query_that_asks_for_nothing() => _attribute.ShouldEqual("AllowAnonymous");
+}
diff --git a/Source/Rendering.Cratis/for_ReadModelAuthorization/when_rendering_a_read_models_authorization/and_the_slice_declares_no_queries.cs b/Source/Rendering.Cratis/for_ReadModelAuthorization/when_rendering_a_read_models_authorization/and_the_slice_declares_no_queries.cs
new file mode 100644
index 0000000..7d715fe
--- /dev/null
+++ b/Source/Rendering.Cratis/for_ReadModelAuthorization/when_rendering_a_read_models_authorization/and_the_slice_declares_no_queries.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Cratis.Specifications;
+using Cratis.Stage.Rendering.Cratis.for_ReadModelAuthorization.given;
+using Xunit;
+
+namespace Cratis.Stage.Rendering.Cratis.for_ReadModelAuthorization.when_rendering_a_read_models_authorization;
+
+///
+/// A slice declaring no query says nothing about reading anything, which is a different silence from one that
+/// states read authorization for its other read models and none for this one.
+///
+public class and_the_slice_declares_no_queries : an_application_with_policies
+{
+ string _attribute = null!;
+
+ void Because() => _attribute = Render("InvoiceSummary");
+
+ [Fact] void should_state_the_absence_as_anonymous() => _attribute.ShouldEqual("AllowAnonymous");
+ [Fact] void should_report_nothing() => _diagnostics.ShouldBeEmpty();
+}
diff --git a/Source/Rendering.Cratis/for_StateViewSliceRenderer/given/a_slice_with_two_read_models.cs b/Source/Rendering.Cratis/for_StateViewSliceRenderer/given/a_slice_with_two_read_models.cs
new file mode 100644
index 0000000..d6963e9
--- /dev/null
+++ b/Source/Rendering.Cratis/for_StateViewSliceRenderer/given/a_slice_with_two_read_models.cs
@@ -0,0 +1,90 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Cratis.Screenplay.Diagnostics;
+using Cratis.Screenplay.Syntax;
+using Cratis.Screenplay.Syntax.Projections;
+using Cratis.Specifications;
+
+namespace Cratis.Stage.Rendering.Cratis.for_StateViewSliceRenderer.given;
+
+///
+/// A dashboard slice shaped like the one in the invoicing sample: two projections, and a query for each of the
+/// read models they build. The rendered read model's own query is guarded by a role; the other read model's
+/// query is not guarded at all.
+///
+public class a_slice_with_two_read_models : Specification
+{
+ protected ApplicationSet _applicationSet = null!;
+ protected LocatedSlice _dashboardSlice = null!;
+
+ void Establish()
+ {
+ var invoiceRegistered = Event("InvoiceRegistered");
+ var invoiceMarkedOverdue = Event("InvoiceMarkedOverdue");
+
+ var summaryProjection = Projection("InvoiceSummary", "InvoiceRegistered");
+ var overdueProjection = Projection("OverdueInvoices", "InvoiceMarkedOverdue");
+
+ var summaryQuery = Query("GetInvoiceSummary", "InvoiceSummary", isCollection: false, policy: "Accountant");
+ var overdueQuery = Query("GetOverdueInvoices", "OverdueInvoices", isCollection: true, policy: null);
+
+ var slice = new SliceSyntax(
+ SliceType.StateView,
+ "InvoiceDashboard",
+ [invoiceRegistered, invoiceMarkedOverdue],
+ [],
+ [summaryQuery, overdueQuery],
+ [summaryProjection, overdueProjection],
+ [],
+ [],
+ [],
+ [],
+ [],
+ SourceLocation.Start);
+
+ var feature = new FeatureSyntax("Invoices", [], [slice], SourceLocation.Start);
+ var module = new ModuleSyntax("Billing", [], [feature], SourceLocation.Start);
+ var application = new ApplicationSyntax(
+ [],
+ [],
+ [new PolicySyntax("Accountant", new RoleConditionSyntax("Accountant", SourceLocation.Start), null, SourceLocation.Start)],
+ [module],
+ SourceLocation.Start);
+
+ _applicationSet = new ApplicationSet([application]);
+ _dashboardSlice = _applicationSet.Slices.Single();
+ }
+
+ static EventSyntax Event(string name) =>
+ new(
+ name,
+ [new PropertySyntax("invoiceNumber", new TypeRefSyntax("String", false, false, SourceLocation.Start), SourceLocation.Start)],
+ SourceLocation.Start);
+
+ static ProjectionSyntax Projection(string readModel, string @event) =>
+ new(
+ readModel,
+ readModel,
+ null,
+ AutoMapMode.Enabled,
+ new ExpressionKeySyntax(new PathExpressionSyntax("invoiceNumber", SourceLocation.Start), SourceLocation.Start),
+ [
+ new FromSyntax(
+ [new EventSpecSyntax(@event, null, SourceLocation.Start)],
+ null,
+ null,
+ [new SetMappingSyntax("invoiceNumber", new PathExpressionSyntax("invoiceNumber", SourceLocation.Start), SourceLocation.Start)],
+ SourceLocation.Start)
+ ],
+ SourceLocation.Start);
+
+ static QuerySyntax Query(string name, string readModel, bool isCollection, string? policy) =>
+ new(
+ name,
+ new TypeRefSyntax(readModel, isCollection, false, SourceLocation.Start),
+ null,
+ [],
+ policy is null ? null : new AuthorizeSyntax(new PolicyReferenceSyntax(policy, SourceLocation.Start), SourceLocation.Start),
+ SourceLocation.Start);
+}
diff --git a/Source/Rendering.Cratis/for_StateViewSliceRenderer/when_another_read_model_has_an_unguarded_query.cs b/Source/Rendering.Cratis/for_StateViewSliceRenderer/when_another_read_model_has_an_unguarded_query.cs
new file mode 100644
index 0000000..7207a52
--- /dev/null
+++ b/Source/Rendering.Cratis/for_StateViewSliceRenderer/when_another_read_model_has_an_unguarded_query.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Cratis.Specifications;
+using Cratis.Stage.Rendering.Cratis.CodeGeneration;
+using Cratis.Stage.Rendering.Cratis.for_StateViewSliceRenderer.given;
+using Cratis.Stage.Rendering.Cratis.Renderers;
+using Xunit;
+
+namespace Cratis.Stage.Rendering.Cratis.for_StateViewSliceRenderer;
+
+///
+/// Only the slice's first projection is rendered, so the dropped one's query used to be part of the union
+/// guarding the read model that survived — and an unguarded query anywhere in the slice published it to
+/// everyone. A query guards the read model its return type names and no other.
+///
+public class when_another_read_model_has_an_unguarded_query : a_slice_with_two_read_models
+{
+ RenderedFile _file = null!;
+ StateViewSliceRenderer _renderer = null!;
+
+ void Establish() => _renderer = new StateViewSliceRenderer();
+
+ void Because() => _file = _renderer.Render(_dashboardSlice, _applicationSet, "CratisApp");
+
+ [Fact] void should_render_the_first_projections_read_model() => _file.Content.ShouldContain("public record InvoiceSummary(");
+ [Fact] void should_keep_it_guarded_by_its_own_querys_policy() => _file.Content.ShouldContain("[Roles(\"Accountant\")]");
+ [Fact] void should_not_publish_it_to_everyone() => _file.Content.Contains("[AllowAnonymous]").ShouldBeFalse();
+}
diff --git a/Source/Rendering.Cratis/for_UnrenderedConstructs/when_reporting_what_a_slice_declares.cs b/Source/Rendering.Cratis/for_UnrenderedConstructs/when_reporting_what_a_slice_declares.cs
index 79e2560..18325f9 100644
--- a/Source/Rendering.Cratis/for_UnrenderedConstructs/when_reporting_what_a_slice_declares.cs
+++ b/Source/Rendering.Cratis/for_UnrenderedConstructs/when_reporting_what_a_slice_declares.cs
@@ -41,7 +41,7 @@ [Fact] void should_report_the_reactor() =>
[Fact] void should_report_the_queries() =>
_diagnostics.ShouldContain(
"Slice 'Summary' declares 1 query declaration(s) with no rendered equivalent — the read model carries the fixed " +
- "all/by-id pair instead, guarded by the union of the declared queries' authorization.");
+ "all/by-id pair instead, guarded by the union of the authorization declared by the queries that read it.");
[Fact] void should_report_the_query_performer() =>
_diagnostics.ShouldContain(
"Slice 'Summary' declares 1 query performer declaration(s) with no rendered equivalent — the query logic is not rendered.");