Skip to content

Commit

Permalink
Add documentation readme to all projects
Browse files Browse the repository at this point in the history
  • Loading branch information
kzu committed Jul 28, 2022
1 parent 60488d7 commit a7d7580
Show file tree
Hide file tree
Showing 4 changed files with 335 additions and 0 deletions.
1 change: 1 addition & 0 deletions Merq.sln
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.github\workflows\build.yml = .github\workflows\build.yml
src\Directory.props = src\Directory.props
.github\workflows\publish.yml = .github\workflows\publish.yml
readme.md = readme.md
EndProjectSection
Expand Down
20 changes: 20 additions & 0 deletions src/Merq.Core/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
A simple default implementation of the message bus that has no external
dependencies.

This implementation of `IMessageBus` relies on two other components: an
`EventStream` (which provides the eventing side of the bus) and a
`CommandBus` (which provides the commands side):

```csharp
var commands = new CommandBus(/* optionally command handlers */);

commands.Register<MyCommand>();
commands.Register(new MyOtherCommand(/* deps */));
// other registrations...
var events = new EventBus(/* optionally pass IObservable<T> producers */);

var bus = new MessageBus(commands, events);
```

For usage and authoring of commands and events, see [Merq](https://nuget.org/packages/Merq) readme.
147 changes: 147 additions & 0 deletions src/Merq.VisualStudio/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
This package provides a MEF-ready customization of the `Merq`
default implementation, which makes it trivial to consume from
a an application that uses [Microsoft.VisualStudio.Composition](https://nuget.org/packages/Microsoft.VisualStudio.Composition)
to load MEF-based components.

This is built-in Visual Studio, and you can take a dependency
on `Merq` from your project by simply declaring it as a prerequisite
for your extension via the manifest:

```xml
<PackageManifest Version="2.0.0" ...>
<Prerequisites>
<Prerequisite Id="Microsoft.VisualStudio.Component.Merq" Version="[17.0,)" DisplayName="Common Xamarin internal tools" />
</Prerequisites>
</PackageManifest>
```

With that in place, you can get access to the `IMessageBus` by simply
importing it in your MEF components:

```csharp
[Export]
[PartCreationPolicy(CreationPolicy.Shared)]
class MyComponent
{
readonly IMessageBus bus;

[ImportingConstructor]
public MyComponent(IMessageBus bus)
{
this.bus = bus;

bus.Observe<OnDidOpenSolution>().Subscribe(OnSolutionOpened);
}

void OnSolutionOpened(OnDidOpenSolution e)
{
// do something, perhaps execute some command?
bus.Execute(new MyCommand("Hello World"));

// perhaps raise further events?
bus.Notify(new MyOtherEvent());
}
}
```

To export command handlers to VS, you must export them with the relevant interface
they implement, such as:

```csharp
public record OpenSolution(string Path) : IAsyncCommand;

[Export(typeof(IAsyncCommandHandler<OpenSolution>))]
public class OpenSolutionHandler : IAsyncCommandHandler<OpenSolution>
{
public bool CanExecute(OpenSolution command)
=> !string.IsNullOrEmpty(command.Path) && File.Exists(command.Path);

public Task ExecuteAsync(OpenSolution command, CancellationToken cancellation)
{
// switch to main thread
// invoke relevant VS API
}
}
```

Events can be notified directly on the bus, as shown in the first example,
or can be produced externally. For example, the producer of `OnDidOpenSolution`
would look like the following:

```csharp
public record OnDidOpenSolution(string Path);

[Export(typeof(IObservable<OnDidOpenSolution>))]
public class OnDidOpenSolutionObservable : IObservable<OnDidOpenSolution>, IVsSolutionEvents
{
readonly JoinableTaskContext jtc;
readonly Subject<OnDidOpenSolution> subject = new();
readonly AsyncLazy<IVsSolution?> lazySolution;

[ImportingConstructor]
public OnDidOpenSolutionObservable(
[Import(typeof(SVsServiceProvider))] IServiceProvider servideProvider,
JoinableTaskContext joinableTaskContext)
{
jtc = joinableTaskContext;

lazySolution = new AsyncLazy<IVsSolution?>(async () =>
{
await jtc.Factory.SwitchToMainThreadAsync();
var solution = servideProvider.GetService<SVsSolution, IVsSolution>();
solution.AdviseSolutionEvents(this, out var _);
return solution;
}, jtc.Factory);
}

public IDisposable Subscribe(IObserver<OnDidOpenSolution> observer)
=> subject.Subscribe(observer);

int IVsSolutionEvents.OnAfterOpenSolution(object pUnkReserved, int fNewSolution)
{
var path = jtc.Factory.Run(async () =>
{
if (await lazySolution.GetValueAsync() is IVsSolution solution &&
ErrorHandler.Succeeded(solution.GetSolutionInfo(out var _, out var slnFile, out var _)) &&
!string.IsNullOrEmpty(slnFile))
{
return slnFile;
}

return null;
});

if (path is string)
{
subject.OnNext(new OnDidOpenSolution(path));
}

return VSConstants.S_OK;
}

// rest of IVsSolutionEvents members
}
```

The implementation above is just an example, but wouldn't be too far from a real one
using the IVs* APIs. It's worth remembering how simple this is to consume though:

```csharp
[ImportingConstructor]
public MyComponent(IMessageBus bus)
{
bus.Observe<OnDidOpenSolution>().Subscribe(OnSolutionOpened);
}

void OnSolutionOpened(OnDidOpenSolution e)
{
// ...
}
```

The benefit of the external producer implementing `IObservable<T>` itself is that
it won't be instantiated at all unless someone called `Observe<T>`, which minimizes
the startup and ongoing cost of having this extensibility mechanism built-in.

If you are [hosting VS MEF](https://github.com/microsoft/vs-mef/blob/main/doc/hosting.md)
in your app, the same concepts apply, so it should be a familiar experience.
167 changes: 167 additions & 0 deletions src/Merq/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
> **Mercury:** messenger of the Roman gods
> *Mercury* > *Merq-ry* > **Merq**

**Merq** brings the [Message Bus](https://docs.microsoft.com/en-us/previous-versions/msp-n-p/ff647328(v=pandp.10)) pattern together with
a [command-oriented interface](https://www.martinfowler.com/bliki/CommandOrientedInterface.html) to in-process application architecture.

These patterns are well established in microservices and service oriented
architectures, but their benefits can be applied to apps too, especially
extensible ones where multiple teams can contribute extensions which
are composed at run-time.

The resulting improved decoupling between components makes it easier to evolve
them independently, while improving discoverability of available commands and
events. You can see this approach applied in the real world in
[VSCode commands](https://code.visualstudio.com/api/extension-guides/command)
and various events such as [window events](https://code.visualstudio.com/api/references/vscode-api#window). Clearly, in the case of VSCode, everything
is in-process, but the benefits of a clean and predictable API are pretty
obvious.

*Merq* provides the same capabilities for .NET apps.

## Events

Events can be any type, there is no restriction or interfaces you must implement.
Nowadays, [C# record types](https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/types/records) are a perfect fit for event data types.
An example event could be a one-liner such as:

```csharp
public record ItemShipped(string Id, DateTimeOffset Date);
```

The events-based API surface on the message bus is simple enough:

```csharp
public interface IMessageBus
{
void Notify<TEvent>(TEvent e);
IObservable<TEvent> Observe<TEvent>();
}
```

By relying on `IObservable<TEvent>`, *Merq* integrates seamlessly with
more powerful event-driven handling via [System.Reactive](http://nuget.org/packages/system.reactive) or the more lightweight [RxFree](https://www.nuget.org/packages/RxFree). Subscribing to events with either of those packages is trivial:

```csharp
IDisposable subscription;

// constructor may use DI to get the dependency
public CustomerViewModel(IMessageBus bus)
{
subscription = bus.Observe<ItemShipped>().Subscribe(OnItemShipped);
}

void OnItemShipped(ItemShipped e) => // Refresh item status
public void Dispose() => subscription.Dispose();
```


## Commands

Commands can also be any type, and C# records make for concise definitions:

```csharp
record CancelOrder(string OrderId) : IAsyncCommand;
```

Unlike events, command messages need to signal the invocation style they require
for execution:

```csharp
// perhaps a method invoked when a user
// clicks/taps a Cancel button next to an order
async Task OnCancel(string orderId)
{
await bus.ExecuteAsync(new CancelOrder(orderId), CancellationToken.None);
// refresh UI for new state.
}
```

An example of a synchronous command could be:

```csharp
record SignOut() : ICommand;

void OnSignOut() => bus.Execute(new SignOut());

// or alternatively, for void commands that have no additional data:
void OnSignOut() => bus.Execute<SignOut>();
```

There are also `ICommand<TResult>` and `IAsyncCommand<TResult>` to signal
that the execution yields a result.

While these marker interfaces on the command messages might seem unnecessary,
they are actually quite important. They solve a key problem that execution
abstractions face: whether a command execution is synchronous or asynchronous
(as well as void or value-returning) should *not* be abstracted since otherwise
you can end up in a common anti-pattern (i.e. [async guidelines for ASP.NET](https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/master/AsyncGuidance.md)), known as [sync over async](https://devblogs.microsoft.com/pfxteam/should-i-expose-synchronous-wrappers-for-asynchronous-methods/) and
[async over sync](https://devblogs.microsoft.com/pfxteam/should-i-expose-asynchronous-wrappers-for-synchronous-methods/).

The marker interfaces on the command messages drive the compiler to only allow
the right invocation style on the message bus, as defined by the command author:

```csharp
public interface IMessageBus
{
// sync void
void Execute(ICommand command);
// sync value-returning
TResult? Execute<TResult>(ICommand<TResult> command);
// async void
Task ExecuteAsync(IAsyncCommand command, CancellationToken cancellation);
// async value-returning
Task<TResult> ExecuteAsync<TResult>(IAsyncCommand<TResult> command, CancellationToken cancellation);
}
```

For example, to create a value-returning async command that retrieves some
value, you would have:

```csharp
record FindDocuments(string Filter) : IAsyncCommand<IEnumerable<string>>;

class FindDocumentsHandler : IAsyncCommandHandler<FindDocument, IEnumerable<string>>
{
public bool CanExecute(FindDocument command) => !string.IsNullOrEmpty(command.Filter);

public Task<IEnumerable<string>> ExecuteAsync(FindDocument command, CancellationToken cancellation)
=> // evaluate command.Filter across all documents and return matches
}
```

In order to execute such command, the only execute method the compiler will allow
is:

```csharp
IEnumerable<string> files = await bus.ExecuteAsync(new FindDocuments("*.json"));
```

If the consumer tries to use `Execute`, the compiler will complain that the
command does not implement `ICommand<TResult>`, which is the synchronous version
of the marker interface. Likewise, mistakes cannot be made when implementing the
handler, since the handler interfaces define constraints on what the commands must
implement:

```csharp
// sync
public interface ICommandHandler<in TCommand> : ... where TCommand : ICommand;
public interface ICommandHandler<in TCommand, out TResult> : ... where TCommand : ICommand<TResult>;

// async
public interface IAsyncCommandHandler<in TCommand> : ... where TCommand : IAsyncCommand;
public interface IAsyncCommandHandler<in TCommand, TResult> : ... where TCommand : IAsyncCommand<TResult>
```

This design choice also makes it impossible to end up executing a command
implementation improperly.

In addition to execution, the `IMessageBus` also provides a mechanism to determine
if a command has a registered handler at all via the `CanHandle<T>` method as well
as a validation mechanism via `CanExecute<T>`, as shown above in the `FindDocumentsHandler` example.

Commands can notify new events, and event observers/subscribers can in turn
execute commands.

0 comments on commit a7d7580

Please sign in to comment.