Skip to content

Commit b44c1c5

Browse files
committed
progress
1 parent 95735a7 commit b44c1c5

File tree

125 files changed

+6970
-474
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

125 files changed

+6970
-474
lines changed

Foundatio.CommandQuery.slnx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<Solution>
22
<Folder Name="/Build/">
3+
<File Path=".github/workflows/build.yml" />
34
<File Path="Directory.Build.props" />
45
<File Path="README.md" />
56
</Folder>
@@ -10,6 +11,8 @@
1011
</Folder>
1112
<Project Path="../Foundatio.Mediator/src/Foundatio.Mediator.Abstractions/Foundatio.Mediator.Abstractions.csproj" />
1213
<Project Path="../Foundatio.Mediator/src/Foundatio.Mediator/Foundatio.Mediator.csproj" />
14+
<Project Path="src/Foundatio.CommandQuery.Dispatcher/Foundatio.CommandQuery.Dispatcher.csproj" Id="817ec0dc-4421-48b7-b60e-1ec6ac3f3622" />
15+
<Project Path="src/Foundatio.CommandQuery.Endpoints/Foundatio.CommandQuery.Endpoints.csproj" Id="012fcc75-7bfd-48c4-8712-98f8ea057d07" />
1316
<Project Path="src/Foundatio.CommandQuery.EntityFramework/Foundatio.CommandQuery.EntityFramework.csproj" Id="94dc262e-6e49-4ad4-9767-7c25a0c4065e" />
1417
<Project Path="src/Foundatio.CommandQuery.MongoDB/Foundatio.CommandQuery.MongoDB.csproj" Id="7252b936-1787-43f3-867c-0f2727abff4a" />
1518
<Project Path="src/Foundatio.CommandQuery/Foundatio.CommandQuery.csproj" Id="1e67f0dc-5ce8-4d5e-a61e-3f92b7aeb512" />
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Foundatio.CommandQuery.Dispatcher;
4+
5+
/// <summary>
6+
/// Represents a request to be dispatched, including its type and payload.
7+
/// </summary>
8+
[JsonConverter(typeof(DispatchRequestConverter))]
9+
public class DispatchRequest
10+
{
11+
/// <summary>
12+
/// Gets or sets the assembly-qualified type name of the request payload.
13+
/// </summary>
14+
[JsonPropertyName("type")]
15+
public string Type { get; set; } = null!;
16+
17+
/// <summary>
18+
/// Gets or sets the request payload to be dispatched.
19+
/// </summary>
20+
[JsonPropertyName("request")]
21+
public object Request { get; set; } = null!;
22+
23+
/// <summary>
24+
/// Creates a <see cref="DispatchRequest"/> for the specified request object.
25+
/// </summary>
26+
/// <param name="request">The request object to dispatch.</param>
27+
/// <returns>A new <see cref="DispatchRequest"/> containing the request and its type.</returns>
28+
/// <exception cref="ArgumentNullException">Thrown if <paramref name="request"/> is null.</exception>
29+
public static DispatchRequest Create(object request)
30+
{
31+
ArgumentNullException.ThrowIfNull(request);
32+
33+
return new()
34+
{
35+
Type = request.GetType().AssemblyQualifiedName!,
36+
Request = request
37+
};
38+
}
39+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
4+
namespace Foundatio.CommandQuery.Dispatcher;
5+
6+
public class DispatchRequestConverter : JsonConverter<DispatchRequest>
7+
{
8+
private static readonly JsonEncodedText TypeProperty = JsonEncodedText.Encode("type");
9+
private static readonly JsonEncodedText RequestProperty = JsonEncodedText.Encode("request");
10+
11+
public override DispatchRequest? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
12+
{
13+
if (reader.TokenType != JsonTokenType.StartObject)
14+
throw new JsonException("JsonReader expected start object token type");
15+
16+
reader.Read();
17+
if (reader.TokenType != JsonTokenType.PropertyName)
18+
throw new JsonException("JsonReader expected property name token type");
19+
20+
if (!reader.ValueTextEquals(TypeProperty.EncodedUtf8Bytes))
21+
throw new JsonException("JsonReader expected 'type' property name");
22+
23+
reader.Read();
24+
if (reader.TokenType != JsonTokenType.String)
25+
throw new JsonException("JsonReader expected string token type");
26+
27+
var requestType = reader.GetString();
28+
if (requestType == null)
29+
throw new JsonException("JsonReader expected non null string value");
30+
31+
var type = Type.GetType(requestType);
32+
if (type is null)
33+
throw new JsonException($"JsonReader could not resolve type {requestType}");
34+
35+
reader.Read();
36+
if (reader.TokenType != JsonTokenType.PropertyName)
37+
throw new JsonException("JsonReader expected property name token type");
38+
39+
if (!reader.ValueTextEquals(RequestProperty.EncodedUtf8Bytes))
40+
throw new JsonException("JsonReader expected 'request' property name");
41+
42+
var instance = JsonSerializer.Deserialize(ref reader, type, options);
43+
if (instance is null)
44+
throw new JsonException($"JsonReader could not deserialize type {requestType}");
45+
46+
if (reader.TokenType != JsonTokenType.EndObject)
47+
throw new JsonException("Unexpected end when reading JSON.");
48+
49+
return new DispatchRequest
50+
{
51+
Type = requestType,
52+
Request = instance
53+
};
54+
}
55+
56+
public override void Write(Utf8JsonWriter writer, DispatchRequest value, JsonSerializerOptions options)
57+
{
58+
59+
ArgumentNullException.ThrowIfNull(value);
60+
61+
if (value.Request is null)
62+
throw new ArgumentNullException(nameof(value), "Request property cannot be null.");
63+
64+
if (string.IsNullOrEmpty(value.Type))
65+
{
66+
value.Type = value.Request?.GetType().AssemblyQualifiedName
67+
?? throw new ArgumentException("Type property cannot be null or empty.", nameof(value));
68+
}
69+
70+
writer.WriteStartObject();
71+
writer.WriteString(TypeProperty, value.Type);
72+
73+
writer.WritePropertyName(RequestProperty);
74+
JsonSerializer.Serialize(writer, value.Request, value.Request.GetType(), options);
75+
76+
writer.WriteEndObject();
77+
}
78+
}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
using System.Security.Claims;
2+
3+
using Foundatio.CommandQuery.Commands;
4+
using Foundatio.CommandQuery.Definitions;
5+
using Foundatio.CommandQuery.Queries;
6+
7+
namespace Foundatio.CommandQuery.Dispatcher;
8+
9+
/// <summary>
10+
/// Provides a data service for dispatching common data requests to a data store.
11+
/// </summary>
12+
/// <remarks>
13+
/// This service acts as an abstraction for sending queries and commands to a dispatcher, enabling operations
14+
/// such as retrieving, creating, updating, and deleting entities in a consistent manner.
15+
/// </remarks>
16+
public class DispatcherDataService : IDispatcherDataService
17+
{
18+
/// <summary>
19+
/// Initializes a new instance of the <see cref="DispatcherDataService"/> class.
20+
/// </summary>
21+
/// <param name="dispatcher">The dispatcher used to send requests.</param>
22+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="dispatcher"/> is <see langword="null"/>.</exception>
23+
public DispatcherDataService(IDispatcher dispatcher)
24+
{
25+
ArgumentNullException.ThrowIfNull(dispatcher);
26+
27+
Dispatcher = dispatcher;
28+
}
29+
30+
/// <inheritdoc/>
31+
public IDispatcher Dispatcher { get; }
32+
33+
/// <inheritdoc/>
34+
public async ValueTask<TModel?> Get<TKey, TModel>(
35+
TKey id,
36+
TimeSpan? cacheTime = null,
37+
CancellationToken cancellationToken = default)
38+
where TModel : class
39+
{
40+
var user = await GetUser(cancellationToken).ConfigureAwait(false);
41+
42+
var command = new GetEntity<TKey, TModel>(user, id);
43+
command.Cache(cacheTime);
44+
45+
return await Dispatcher
46+
.Send<TModel>(command, cancellationToken)
47+
.ConfigureAwait(false);
48+
}
49+
50+
/// <inheritdoc/>
51+
public async ValueTask<IReadOnlyList<TModel>> Get<TKey, TModel>(
52+
IEnumerable<TKey> ids,
53+
TimeSpan? cacheTime = null,
54+
CancellationToken cancellationToken = default)
55+
where TModel : class
56+
{
57+
var user = await GetUser(cancellationToken).ConfigureAwait(false);
58+
59+
var command = new GetEntities<TKey, TModel>(user, [.. ids]);
60+
command.Cache(cacheTime);
61+
62+
var result = await Dispatcher
63+
.Send<IReadOnlyList<TModel>>(command, cancellationToken)
64+
.ConfigureAwait(false);
65+
66+
return result ?? [];
67+
}
68+
69+
/// <inheritdoc/>
70+
public async ValueTask<IReadOnlyList<TModel>> All<TModel>(
71+
string? sortField = null,
72+
TimeSpan? cacheTime = null,
73+
CancellationToken cancellationToken = default)
74+
where TModel : class
75+
{
76+
var sort = QuerySort.Parse(sortField);
77+
78+
var query = new QueryDefinition { Sorts = [sort] };
79+
80+
var user = await GetUser(cancellationToken).ConfigureAwait(false);
81+
82+
var command = new QueryEntities<TModel>(user, query);
83+
command.Cache(cacheTime);
84+
85+
var result = await Dispatcher
86+
.Send<QueryResult<TModel>>(command, cancellationToken)
87+
.ConfigureAwait(false);
88+
89+
return result?.Data ?? [];
90+
}
91+
92+
/// <inheritdoc/>
93+
public async ValueTask<QueryResult<TModel>> Query<TModel>(
94+
QueryDefinition? query = null,
95+
CancellationToken cancellationToken = default)
96+
where TModel : class
97+
{
98+
var user = await GetUser(cancellationToken).ConfigureAwait(false);
99+
100+
var command = new QueryEntities<TModel>(user, query);
101+
102+
var result = await Dispatcher
103+
.Send<QueryResult<TModel>>(command, cancellationToken)
104+
.ConfigureAwait(false);
105+
106+
return result ?? new QueryResult<TModel>();
107+
}
108+
109+
/// <inheritdoc/>
110+
public async ValueTask<QueryResult<TModel>> Search<TModel>(
111+
string searchText,
112+
QueryDefinition? query = null,
113+
CancellationToken cancellationToken = default)
114+
where TModel : class, ISupportSearch
115+
{
116+
query ??= new QueryDefinition();
117+
118+
var searchFilter = QueryBuilder.Search(TModel.SearchFields(), searchText);
119+
var sort = new QuerySort { Name = TModel.SortField() };
120+
121+
query.Filter = QueryBuilder.Group(query?.Filter, searchFilter);
122+
123+
return await Query<TModel>(query, cancellationToken);
124+
}
125+
126+
/// <inheritdoc/>
127+
public async ValueTask<TReadModel?> Save<TKey, TUpdateModel, TReadModel>(
128+
TKey id,
129+
TUpdateModel updateModel,
130+
CancellationToken cancellationToken = default)
131+
where TReadModel : class
132+
where TUpdateModel : class
133+
{
134+
var user = await GetUser(cancellationToken).ConfigureAwait(false);
135+
136+
var command = new UpdateEntity<TKey, TUpdateModel, TReadModel>(user, id, updateModel, true);
137+
138+
return await Dispatcher
139+
.Send<TReadModel>(command, cancellationToken)
140+
.ConfigureAwait(false);
141+
}
142+
143+
/// <inheritdoc/>
144+
public async ValueTask<TReadModel?> Create<TCreateModel, TReadModel>(
145+
TCreateModel createModel,
146+
CancellationToken cancellationToken = default)
147+
where TReadModel : class
148+
where TCreateModel : class
149+
{
150+
var user = await GetUser(cancellationToken).ConfigureAwait(false);
151+
152+
var command = new CreateEntity<TCreateModel, TReadModel>(user, createModel);
153+
154+
return await Dispatcher
155+
.Send<TReadModel>(command, cancellationToken)
156+
.ConfigureAwait(false);
157+
}
158+
159+
/// <inheritdoc/>
160+
public async ValueTask<TReadModel?> Update<TKey, TUpdateModel, TReadModel>(
161+
TKey id,
162+
TUpdateModel updateModel,
163+
CancellationToken cancellationToken = default)
164+
where TReadModel : class
165+
where TUpdateModel : class
166+
{
167+
var user = await GetUser(cancellationToken).ConfigureAwait(false);
168+
169+
var command = new UpdateEntity<TKey, TUpdateModel, TReadModel>(user, id, updateModel);
170+
171+
return await Dispatcher
172+
.Send<TReadModel>(command, cancellationToken)
173+
.ConfigureAwait(false);
174+
}
175+
176+
/// <inheritdoc/>
177+
public async ValueTask<TReadModel?> Delete<TKey, TReadModel>(
178+
TKey id,
179+
CancellationToken cancellationToken = default)
180+
where TReadModel : class
181+
{
182+
var user = await GetUser(cancellationToken).ConfigureAwait(false);
183+
var command = new DeleteEntity<TKey, TReadModel>(user, id);
184+
185+
return await Dispatcher
186+
.Send<TReadModel>(command, cancellationToken)
187+
.ConfigureAwait(false);
188+
}
189+
190+
/// <inheritdoc/>
191+
public virtual ValueTask<ClaimsPrincipal?> GetUser(CancellationToken cancellationToken = default)
192+
{
193+
return ValueTask.FromResult<ClaimsPrincipal?>(default);
194+
}
195+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
namespace Foundatio.CommandQuery.Dispatcher;
2+
3+
/// <summary>
4+
/// Options for the dispatcher.
5+
/// </summary>
6+
public class DispatcherOptions
7+
{
8+
/// <summary>
9+
/// The prefix for the feature routes.
10+
/// </summary>
11+
public string FeaturePrefix { get; set; } = "/api";
12+
13+
/// <summary>
14+
/// The prefix for the dispatcher routes.
15+
/// </summary>
16+
public string DispatcherPrefix { get; set; } = "/dispatcher";
17+
18+
/// <summary>
19+
/// The route for the send method.
20+
/// </summary>
21+
public string SendRoute { get; set; } = "/send";
22+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.9" />
11+
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.9" />
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<ProjectReference Include="..\Foundatio.CommandQuery\Foundatio.CommandQuery.csproj" />
16+
</ItemGroup>
17+
18+
</Project>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using Foundatio.Mediator;
2+
3+
namespace Foundatio.CommandQuery.Dispatcher;
4+
5+
/// <summary>
6+
/// An <see langword="interface"/> to represent a dispatcher for sending request messages.
7+
/// </summary>
8+
/// <remarks>
9+
/// Dispatcher is an abstraction over the <see cref="IMediator"/> pattern, allowing for sending of requests over
10+
/// HTTP for remote scenarios and directly to <see cref="IMediator"/> for server side scenarios. Use this abstraction
11+
/// when using the Blazor Interactive Auto rendering mode.
12+
/// </remarks>
13+
public interface IDispatcher
14+
{
15+
/// <summary>
16+
/// Sends a request to the message dispatcher.
17+
/// </summary>
18+
/// <typeparam name="TResponse"> The type of response from the dispatcher</typeparam>
19+
/// <param name="request">The request being sent</param>
20+
/// <param name="cancellationToken">Cancellation token</param>
21+
/// <returns>Awaitable task returning the <typeparamref name="TResponse"/></returns>
22+
ValueTask<TResponse?> Send<TResponse>(
23+
object request,
24+
CancellationToken cancellationToken = default);
25+
}

0 commit comments

Comments
 (0)