Skip to content

Conversation

@Sergio0694
Copy link
Member

Closes #301

CommunityToolkit.AppServices

A library and accompanying source generator to make working with AppService much easier 🚀

Simply define an interface:

[AppService("MyAppService")]
public interface IMyAppService
{
    Task<int> SumNumbersAsync(int x, int y);
}

Then implement it in your desktop extension:

public sealed partial class MyAppService : IMyAppService
{
    public async Task<int> SumNumbersAsync(int x, int y)
    {
        return x + y;
    }
}

That's it! The source generator will automatically create for you:

  • The base desktop extension infrastructure to receive commands, invoke them and send back results
  • The whole UWP service to represent the other connection endpoint

That means, the host UWP can literally just do this:

int result = await App.Services.GetRequiredService<IMyAppService>().SumNumbersAsync(40, 2); // 42

This enables a fundamentally easier, less error prone and much less verbose way to interact with AppService APIs 🚀

We developed this library for the Microsoft Store, which uses it for all the desktop extension functionaly, and would like to open source it and add it to the Windows Community Toolkit, so that it can benefit all developers using UWP and WinUI 3. This can be used both to support UWP applications needing a desktop extension component to escape the sandbox (like the Microsoft Store is doing), as well as both UWP and WInUI 3 apps using AppService to connect to other applications registered as extensions.

Additional features

The library comes bundled with:

  • Full set of APIs to manually create hosts and services
  • Source generator generating all the endpoints manually
  • Comprehensive Roslyn analyzers to emit errors/warnings for invalid uses
  • Support for IProgress<T> values across processes
  • Support for CancellationToken values across processes
  • Support for custom serializers to be able to also marshal arbitrary types

API breakdown

namespace CommunityToolkit.AppServices;

[AttributeUsage(AttributeTargets.Interface, AllowMultiple = false, Inherited = false)]
public sealed class AppServiceAttribute : Attribute
{
    public AppServiceAttribute(string appServiceName);

    public string AppServiceName { get; }
}

[AttributeUsage(AttributeTargets.ReturnValue | AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
public sealed class ValueSetSerializerAttribute : Attribute
{
    public ValueSetSerializerAttribute(Type valueSetSerializerType);

    public Type ValueSetSerializerType { get; }
}

public interface IValueSetSerializer<T>
{
    [return: NotNullIfNotNull("value")]
    ValueSet? Serialize(T? value);
    
    [return: NotNullIfNotNull("valueSet")]
    T? Deserialize(ValueSet? valueSet);
}

public enum AppServiceStatus
{
    Ok,
    Error,
    ActionNotFound,
    NoResponse,
    InvalidRequest,
    InvalidResponse,
    MismatchedResponseType,
    SerializationError,
    CantSend,
    AppServiceNotCompatible,
    Timeout,
    Canceled,
    CantStart
}

public sealed class AppServiceException : Exception
{
    public AppServiceStatus Status { get; }
}

public abstract class AppServiceHost
{
    protected AppServiceHost(string appServiceName);

    public bool OnBackgroundActivated(BackgroundActivatedEventArgs args);

    protected AppServiceRequest CreateAppServiceRequest([CallerMemberName] string? requestName = null);

    protected sealed class AppServiceRequest
    {
        public AppServiceRequest(AppServiceHost host, string requestName);

        public AppServiceRequest WithParameter<T>(T parameter, [CallerArgumentExpression("parameter")] string? parameterName = null);
        public AppServiceRequest WithParameter<TSerializer, TParameter>(TSerializer serializer, TParameter? parameter, [CallerArgumentExpression("parameter")] string? parameterName = null) where TSerializer : IValueSetSerializer<TParameter>;
        public AppServiceRequest WithProgress<T>(IProgress<T> progress);
        public AppServiceRequest WithProgress<TSerializer, TResult>(TSerializer serializer, IProgress<TResult?> progress) where TSerializer : IValueSetSerializer<TResult>;
        public AppServiceRequest WithCancellationToken(CancellationToken cancellationToken);

        public Task SendAndWaitForResultAsync();
        public Task<T> SendAndWaitForResultAsync<T>();
        public Task<TResult?> SendAndWaitForResultAsync<TSerializer, TResult>(TSerializer serializer) where TSerializer : IValueSetSerializer<TResult>;
        public Task SendAndWaitForResultAsync(TimeSpan timeout);
        public Task<T> SendAndWaitForResultAsync<T>(TimeSpan timeout);
        public Task<TResult?> SendAndWaitForResultAsync<TSerializer, TResult>(TSerializer serializer, TimeSpan timeout) where TSerializer : IValueSetSerializer<TResult>;
    }
}

public abstract class AppServiceComponent : IDisposable
{
    public event EventHandler? ConnectionFailed;
    public event EventHandler? ConnectionClosed;

    protected AppServiceComponent(string appServiceName);

    public async Task InitializeAppService();

    public void Dispose();

    protected void RegisterEndpoint(Action endpoint, [CallerArgumentExpression("endpoint")] string? endpointName = null);
    protected void RegisterEndpoint<T>(Func<T> endpoint, [CallerArgumentExpression("endpoint")] string? endpointName = null);
    protected void RegisterEndpoint<TSerializer, TResult>(TSerializer serializer, Func<TResult?> endpoint, [CallerArgumentExpression("endpoint")] string? endpointName = null) where TSerializer : IValueSetSerializer<TResult>;
    protected void RegisterEndpoint(Action<AppServiceParameters> endpoint, [CallerArgumentExpression("endpoint")] string? endpointName = null);
    protected void RegisterEndpoint<T>(Func<AppServiceParameters, T> endpoint, [CallerArgumentExpression("endpoint")] string? endpointName = null);
    protected void RegisterEndpoint<TSerializer, TResult>(TSerializer serializer, Func<AppServiceParameters, TResult?> endpoint, [CallerArgumentExpression("endpoint")] string? endpointName = null) where TSerializer : IValueSetSerializer<TResult>;
    protected void RegisterEndpoint(Func<Task> endpoint, [CallerArgumentExpression("endpoint")] string? endpointName = null);
    protected void RegisterEndpoint<T>(Func<Task<T>> endpoint, [CallerArgumentExpression("endpoint")] string? endpointName = null);
    protected void RegisterEndpoint<TSerializer, TResult>(TSerializer serializer, Func<Task<TResult?>> endpoint, [CallerArgumentExpression("endpoint")] string? endpointName = null) where TSerializer : IValueSetSerializer<TResult>; 
    protected void RegisterEndpoint(Func<AppServiceParameters, Task> endpoint, [CallerArgumentExpression("endpoint")] string? endpointName = null);    
    protected void RegisterEndpoint<T>(Func<AppServiceParameters, Task<T>> endpoint, [CallerArgumentExpression("endpoint")] string? endpointName = null);    
    protected void RegisterEndpoint<TSerializer, TResult>(TSerializer serializer, Func<AppServiceParameters, Task<TResult?>> endpoint, [CallerArgumentExpression("endpoint")] string? endpointName = null) where TSerializer : IValueSetSerializer<TResult>;

    protected readonly struct AppServiceParameters
    {
        public AppServiceParameters(AppServiceComponent component, AppServiceConnection connection, ValueSet valueSet);

        public void GetParameter<T>(out T parameter, [CallerArgumentExpression("parameter")] string? parameterName = null);
        public void GetParameter<TSerializer, TParameter>(TSerializer serializer, out TParameter? parameter, [CallerArgumentExpression("parameter")] string? parameterName = null) where TSerializer: IValueSetSerializer<TParameter>;
        
        public void GetProgress<T>(out IProgress<T> progress);
        public void GetProgress<TSerializer, TResult>(TSerializer serializer, out IProgress<TResult?> progress) where TSerializer : IValueSetSerializer<TResult>;            
        
        public void GetCancellationToken(out CancellationToken cancellationToken);
    }
}

@Sergio0694 Sergio0694 added experiment 🧪 Used to track issues that are experiments (or their linked discussions) extensions ⚡ labels May 31, 2023
@Sergio0694
Copy link
Member Author

@Youssef1313 new generator in town! 😄
Feel free to have a quick look just in case you're curious!

@Sergio0694 Sergio0694 requested a review from Arlodotexe May 31, 2023 17:36
@Sergio0694
Copy link
Member Author

@Arlodotexe I think your infra is injecting extra dependencies into packages, look at this:

I'm not WinUI nor the Immutable package anywhere, and they shouldn't be there. Could you take a look? 🙂

@Sergio0694
Copy link
Member Author

@Arlodotexe can't build the project anymore, I get this:

This SDK is super old and it doesn't even show up as an option in the VS installer anymore.
Can we bump it to 18362? I mean without bumping the minimum SDK for projects, just the tooling.

@Arlodotexe
Copy link
Member

@Arlodotexe can't build the project anymore, I get this:

This SDK is super old and it doesn't even show up as an option in the VS installer anymore. Can we bump it to 18362? I mean without bumping the minimum SDK for projects, just the tooling.

Can you supply more info? Which project is emitting this error exactly? What do you mean by "just the tooling"?

@Sergio0694
Copy link
Member Author

I'm getting this when building CommunityToolkit.AppServices:

1>C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\amd64\Microsoft.Common.CurrentVersion.targets(2623,5): error MSB3774: Could not find SDK "WindowsDesktop, Version=10.0.17763.0".
1>Done building project "CommunityToolkit.AppServices.csproj" -- FAILED.

@Arlodotexe
Copy link
Member

Seems that removing WinUI may have caused this, but our TargetPlatformVersion and TargetPlatformMinVersion match the minimum requirements. Doesn't seem like it bumped them for us, not sure what happened 🤔

image

Since 17763 is the min version for the entire toolkit, for now I recommend installing this SDK to build AppServices. Investigation to continue in another ticket.

@Arlodotexe Arlodotexe merged commit a0f91e1 into main Jun 1, 2023
@Sergio0694 Sergio0694 deleted the user/sergiopedri/app-services branch June 1, 2023 17:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

experiment 🧪 Used to track issues that are experiments (or their linked discussions) extensions ⚡

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants