Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Local execution of CQRS #630

Merged
merged 18 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b76abcb
Preliminary implementation of local execution of CQRS (commands only)
jakubfijalkowski Jan 6, 2024
4a20992
Do not re-use the `MapRemoteCqrs` pipeline config for local exec
jakubfijalkowski Jan 6, 2024
8cc75a4
Unify "normal" execution with "local" execution internals as much as …
jakubfijalkowski Jan 27, 2024
81e7be2
Begin implementing fully local `HttpContext`
jakubfijalkowski Jan 31, 2024
327f9f3
Switch to `null` objects in local call context
jakubfijalkowski Feb 1, 2024
926dc53
Test the dummy context classes
jakubfijalkowski Feb 7, 2024
176ad8e
Fix wrong assumed defaults in tests
jakubfijalkowski Feb 7, 2024
bed2704
Add `LocalCallContext` tests
jakubfijalkowski Feb 7, 2024
5801e70
Rewrite `MiddlewareBasedLocalCommandExecutorTests` to a version witho…
jakubfijalkowski Feb 7, 2024
dfdb8f8
Add local query executor
jakubfijalkowski Feb 7, 2024
67f499c
Add integration test that runs on top on test WebHost
jakubfijalkowski Feb 7, 2024
d3fd9a7
Add local opertions executor
jakubfijalkowski Feb 7, 2024
3297b27
Add keyed local executors support
jakubfijalkowski Feb 7, 2024
f581a82
Test the `ServiceProviderRegistrationExtensions`
jakubfijalkowski Feb 7, 2024
6f71a49
Test middlewares with local execution
jakubfijalkowski Feb 7, 2024
59da577
Restore the previously-removed `CQRSObjectsRegistrationSource` regist…
jakubfijalkowski Feb 7, 2024
124155f
Decode known status code for local execution
jakubfijalkowski Feb 7, 2024
925267d
Seal classes that can be sealed
jakubfijalkowski Feb 8, 2024
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: 0 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ dotnet_style_predefined_type_for_member_access = true
dotnet_style_readonly_field = true : suggestion

dotnet_style_require_accessibility_modifiers = always : warning
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async : warning

dotnet_style_object_initializer = true : suggestion
dotnet_style_collection_initializer = true : suggestion
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;

namespace LeanCode.CQRS.AspNetCore.Local.Context;

internal class LocalCallContext : HttpContext, IDisposable
{
private const int DefaultFeatureCollectionSize = 6; // 4 internal, 2 external (set in local executors)

private readonly FeatureCollection features;

private readonly ItemsFeature itemsFeature;
private readonly LocalCallServiceProvidersFeature serviceProvidersFeature;
private readonly LocalCallLifetimeFeature callLifetimeFeature;

public override IFeatureCollection Features => features;

public override ClaimsPrincipal User { get; set; }
public override string TraceIdentifier { get; set; }
public override HttpRequest Request { get; }
public override HttpResponse Response { get; }

public override IDictionary<object, object?> Items
{
get => itemsFeature.Items;
set => itemsFeature.Items = value;
}

public override IServiceProvider RequestServices
{
get => serviceProvidersFeature.RequestServices;
set => serviceProvidersFeature.RequestServices = value;
}

public override CancellationToken RequestAborted
{
get => callLifetimeFeature.RequestAborted;
set => callLifetimeFeature.RequestAborted = value;
}

public CancellationToken CallAborted => callLifetimeFeature.CallAborted;

public override ConnectionInfo Connection => NullConnectionInfo.Empty;
public override WebSocketManager WebSockets => NullWebSocketManager.Empty;
public override ISession Session
{
get => NullSession.Empty;
set { }
}

public LocalCallContext(
IServiceProvider requestServices,
ClaimsPrincipal user,
string? activityIdentifier,
IHeaderDictionary? headers,
CancellationToken cancellationToken
)
{
features = new(DefaultFeatureCollectionSize);

User = user;
TraceIdentifier = activityIdentifier ?? "";
Request = new LocalHttpRequest(this, headers);
Response = new NullHttpResponse(this);

itemsFeature = new();
serviceProvidersFeature = new(requestServices);
callLifetimeFeature = new(cancellationToken);

features.Set<IItemsFeature>(itemsFeature);
features.Set<IServiceProvidersFeature>(serviceProvidersFeature);
features.Set<IHttpRequestLifetimeFeature>(callLifetimeFeature);
features.Set<IEndpointFeature>(NullEndpointFeature.Empty);
}

public override void Abort() => callLifetimeFeature.Abort();

public void Dispose() => callLifetimeFeature.Dispose();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Http.Features;

namespace LeanCode.CQRS.AspNetCore.Local.Context;

internal class LocalCallLifetimeFeature : IHttpRequestLifetimeFeature, IDisposable
{
private readonly CancellationTokenSource source;
private CancellationToken requestAborted;

public CancellationToken RequestAborted
{
get => requestAborted;
set => requestAborted = value;
}

public CancellationToken CallAborted => source.Token;

public LocalCallLifetimeFeature(CancellationToken cancellationToken)
{
source = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
requestAborted = source.Token;
}

public void Abort() => source.Cancel();

public void Dispose() => source.Dispose();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Http.Features;

namespace LeanCode.CQRS.AspNetCore.Local.Context;

internal class LocalCallServiceProvidersFeature : IServiceProvidersFeature
{
public IServiceProvider RequestServices { get; set; }

public LocalCallServiceProvidersFeature(IServiceProvider requestServices)
{
RequestServices = requestServices;
}
}
104 changes: 104 additions & 0 deletions src/CQRS/LeanCode.CQRS.AspNetCore/Local/Context/LocalHttpRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using Microsoft.AspNetCore.Http;

namespace LeanCode.CQRS.AspNetCore.Local.Context;

internal class LocalHttpRequest : HttpRequest
{
public override HttpContext HttpContext { get; }
public override IHeaderDictionary Headers { get; }

public override bool HasFormContentType => false;

public override string Method
{
get => "";
set { }
}

public override string Scheme
{
get => "";
set { }
}

public override bool IsHttps
{
get => false;
set { }
}

public override HostString Host
{
get => default;
set { }
}

public override PathString PathBase
{
get => PathString.Empty;
set { }
}

public override PathString Path
{
get => PathString.Empty;
set { }
}

public override QueryString QueryString
{
get => QueryString.Empty;
set { }
}

public override IQueryCollection Query
{
get => QueryCollection.Empty;
set { }
}

public override string Protocol
{
get => "";
set { }
}

public override IRequestCookieCollection Cookies
{
get => NullRequestCookieCollection.Empty;
set { }

Check warning on line 69 in src/CQRS/LeanCode.CQRS.AspNetCore/Local/Context/LocalHttpRequest.cs

View check run for this annotation

Codecov / codecov/patch

src/CQRS/LeanCode.CQRS.AspNetCore/Local/Context/LocalHttpRequest.cs#L68-L69

Added lines #L68 - L69 were not covered by tests
}

public override long? ContentLength
{
get => null;
set { }
}

public override string? ContentType
{
get => null;
set { }
}

public override Stream Body
{
get => Stream.Null;
set { }
}

public override IFormCollection Form
{
get => FormCollection.Empty;
set { }
}

public LocalHttpRequest(HttpContext httpContext, IHeaderDictionary? headers)
{
HttpContext = httpContext;
Headers = headers ?? new HeaderDictionary();
}

public override Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken = default) =>
Task.FromResult<IFormCollection>(FormCollection.Empty);

Check warning on line 103 in src/CQRS/LeanCode.CQRS.AspNetCore/Local/Context/LocalHttpRequest.cs

View check run for this annotation

Codecov / codecov/patch

src/CQRS/LeanCode.CQRS.AspNetCore/Local/Context/LocalHttpRequest.cs#L103

Added line #L103 was not covered by tests
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System.Net;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Http;

namespace LeanCode.CQRS.AspNetCore.Local.Context;

internal class NullConnectionInfo : ConnectionInfo
{
public static readonly NullConnectionInfo Empty = new();

public override string Id
{
get => "";
set { }
}

public override IPAddress? RemoteIpAddress
{
get => null;
set { }
}

public override int RemotePort
{
get => 0;
set { }
}

public override IPAddress? LocalIpAddress
{
get => null;
set { }
}

public override int LocalPort
{
get => 0;
set { }
}

public override X509Certificate2? ClientCertificate
{
get => null;
set { }
}

private NullConnectionInfo() { }

public override Task<X509Certificate2?> GetClientCertificateAsync(CancellationToken cancellationToken = default) =>
Task.FromResult<X509Certificate2?>(null);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;

namespace LeanCode.CQRS.AspNetCore.Local.Context;

internal class NullEndpointFeature : IEndpointFeature
{
public static readonly NullEndpointFeature Empty = new();

public Endpoint? Endpoint
{
get => null;
set { }
}

private NullEndpointFeature() { }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System.Collections;
using System.Collections.ObjectModel;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;

namespace LeanCode.CQRS.AspNetCore.Local.Context;

internal class NullHeaderDictionary : IHeaderDictionary
{
public static readonly NullHeaderDictionary Empty = new();

public StringValues this[string key]
{
get => StringValues.Empty;
set { }
}

public long? ContentLength
{
get => null;
set { }
}

public ICollection<string> Keys { get; } = new ReadOnlyCollection<string>([ ]);

public ICollection<StringValues> Values { get; } = new ReadOnlyCollection<StringValues>([ ]);

public int Count => 0;

public bool IsReadOnly => true;

private NullHeaderDictionary() { }

public void Add(string key, StringValues value) { }

public void Add(KeyValuePair<string, StringValues> item) { }

public void Clear() { }

public bool Contains(KeyValuePair<string, StringValues> item) => false;

public bool ContainsKey(string key) => false;

public void CopyTo(KeyValuePair<string, StringValues>[] array, int arrayIndex) { }

public bool Remove(string key) => false;

public bool Remove(KeyValuePair<string, StringValues> item) => false;

public bool TryGetValue(string key, out StringValues value)
{
value = StringValues.Empty;
return false;
}

public IEnumerator<KeyValuePair<string, StringValues>> GetEnumerator() =>
Enumerable.Empty<KeyValuePair<string, StringValues>>().GetEnumerator();

Check warning on line 57 in src/CQRS/LeanCode.CQRS.AspNetCore/Local/Context/NullHeaderDictionary.cs

View check run for this annotation

Codecov / codecov/patch

src/CQRS/LeanCode.CQRS.AspNetCore/Local/Context/NullHeaderDictionary.cs#L57

Added line #L57 was not covered by tests

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

Check warning on line 59 in src/CQRS/LeanCode.CQRS.AspNetCore/Local/Context/NullHeaderDictionary.cs

View check run for this annotation

Codecov / codecov/patch

src/CQRS/LeanCode.CQRS.AspNetCore/Local/Context/NullHeaderDictionary.cs#L59

Added line #L59 was not covered by tests
}
Loading
Loading