Start and Restart Windows (UWP/WinUI) Applications on Windows Startup

A while ago Windows introduced a little-known feature that allows applications to automatically restart when Windows is restarted. However, rather than just look at this feature, which Windows app developer get for free, in this post we’re going to look at different options for starting and restarting a Windows application when Windows starts/restarts. Launch on … Continue reading “Start and Restart Windows (UWP/WinUI) Applications on Windows Startup”

A while ago Windows introduced a little-known feature that allows applications to automatically restart when Windows is restarted. However, rather than just look at this feature, which Windows app developer get for free, in this post we’re going to look at different options for starting and restarting a Windows application when Windows starts/restarts.

Launch on Windows Startup

The first thing we’re going to look at is how to configure a Windows (UWP/WinUI) application to automatically start when Windows starts. This is done by adding a StartupTask to the manifest of the application (i.e. the package.appxmanifest file).

<?xml version="1.0" encoding="utf-8"?>
<Package
  xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
  xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
  xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
  xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5"
  IgnorableNamespaces="uap mp">
	...
	<Applications>
		<Application Id="App"
		  Executable="$targetnametoken$.exe"
		  EntryPoint="LaunchOnWindowsStart.App">
			...
			<Extensions>
				<uap5:Extension Category="windows.startupTask">
					<uap5:StartupTask
					  TaskId="LaunchOnStartupTaskId"
					  DisplayName="My Launchable App" />
				</uap5:Extension>
			</Extensions>
		</Application>
	</Applications>
	...
</Package>

The points worth noting here are:

  • An additional namespace needs to be included for the StartupTask. In this case the namespace has been imported with a prefix of uap5.
  • TaskId – This is a string that you’ll use within the app in order to access the StartUpTask.
  • DisplayName – This is the text that will appear in the list of Windows startup tasks where a user can manually enable/disable startup tasks.

Including the StartupTask extension in the manifest file simply registers a startup task for the application with Windows. By default, the startup task will be disabled but can be enabled by the user either directly using the Windows startup task list, or when prompted by the application. Let’s look at these two options.

Toggling Startup Task Via Settings

Windows currently provides two ways to access the list of registered startup tasks. The first is via the Startup tab of Task Manager (Press Ctrl+Shift+Esc to launch Task Manager).

The other option is the Startup page within the Settings app.

In either location the user can toggle any startup task between Enabled and Disabled (or On/Off in the case of the settings app).

Just to clarify, if you add the StartupTask extension into the package.appxmanifest, a startup task for your application will appear in this list, showing the DisplayName and the publisher. The startup task will be disabled by default. From this list, the user can enable/disable your startup task. If the user enables the startup task for your application, it will launch the next time Windows starts up (or restarts).

Toggling Startup Task via the Application

A more common scenario is that you’ll want to provide an interface within the application itself for the user to toggle the behaviour when Windows starts. For example Spotify provides an option under Settings to customise the behaviour of the application when the user logs into the computer (i.e. Windows startup).

The process for toggling the state (i.e. enabled or disabled) of the startup task is to first, get a reference to the startup task, and then to either request the startup task be enabled, or to disable the task. Let’s see this in code.

In our application we’re going to have a very simple ToggleButton called LaunchOnStartupToggle and for the purpose of this post we’re going to manually set the IsChecked state (Please use data binding to a view model when implementing this in an actual app!!). When the application launches and navigates to the MainPage, we’re going to retrieve a reference to the startup task and update the IsChecked state on the ToggleButton based on the state of the startup task.

protected override async void OnNavigatedTo(NavigationEventArgs e)
{
    base.OnNavigatedTo(e);
    var startup = await StartupTask.GetAsync("LaunchOnStartupTaskId");
    UpdateToggleState(startup.State);
}
private void UpdateToggleState(StartupTaskState state)
{
    LaunchOnStartupToggle.IsEnabled = true;
    switch (state)
    {
        case StartupTaskState.Enabled:
            LaunchOnStartupToggle.IsChecked = true;
            break;
        case StartupTaskState.Disabled:
        case StartupTaskState.DisabledByUser:
            LaunchOnStartupToggle.IsChecked = false;
            break;
        default:
            LaunchOnStartupToggle.IsEnabled = false;
            break;
    }
}

Note that we’re also adjusting the IsEnabled state of the ToggleButton as there are some states where the computer policy will prevent the user overriding the state of the startup task.

Now, we need to handle when the ToggleButton changes state. For this, we’re simply going to handle the Click event on the ToggleButton (and yes, alternatively we could have handled the Checked and Unchecked events). A reference to the startup task can be retrieved using the StartupTask.GetAsync method, passing in the TaskId used in the package.appxmanifest.

private async void ToggleClick(object sender, RoutedEventArgs e)
{
    await ToggleLaunchOnStartup(LaunchOnStartupToggle.IsChecked??false);
}
private async Task ToggleLaunchOnStartup(bool enable)
{
    var startup = await StartupTask.GetAsync("LaunchOnStartupTaskId");
    switch (startup.State)
    {
        case StartupTaskState.Enabled when !enable:
            startup.Disable();
            break;
        case StartupTaskState.Disabled when enable:
            var updatedState = await startup.RequestEnableAsync();
            UpdateToggleState(updatedState);
            break;
        case StartupTaskState.DisabledByUser when enable:
            await new MessageDialog("Unable to change state of startup task via the application - enable via Startup tab on Task Manager (Ctrl+Shift+Esc)").ShowAsync();
            break;
        default:
            await new MessageDialog("Unable to change state of startup task").ShowAsync();
            break;
    }
}

To enable the startup task requires a call to RequestEnableAsync on the startup task reference. This will display a prompt for the user to choose whether to Enable or Disable the startup task for the app – note that the DisplayName set on the StartupTask in the package.appxmanifest is used in this dialog.

One important thing to note about this dialog – if the user opts to Disable the startup task, the state is changed to DisabledByUser and cannot be Enabled from within the application – calling RequestEnableAsync again will do nothing. Instead, the user should be directed to the startup tasks list in Settings or Task Manager.

Disabling the startup task from within the application is done by calling Disable on the startup task reference. Since the startup task has been disabled by the application, it can be enabled again by calling RequestEnableAsync again and allowing the user to select the Enable option.

Automatic Restart on Windows Restart

Most application don’t necessarily want to register a startup task and have the application launch every time Windows starts. However, what is convenient is that if the application is running when Windows has to restart, the application should be launched again (and ideally the application should be able to resume where the user left off). A lot of desktop applications already do this but this option wasn’t available to Windows (UWP/WinUI) applications until relatively recently.

Under Sign-in options in the Settings app, there is an option to Restart apps. This option is disabled by default, presumably because it’s being progressively rolled out to avoid disrupting users too much.

When the Restart apps option is switched on, any Windows applications that are running when Windows restarts, or if the user sings out and back in, will get relaunched.

As a Windows application developer you don’t need to do anything in order to take advantage of this. You don’t need to include a StartupTask (as previously described). The StartupTask is only required if you want your application to be launched every time Windows is started (irrespective of whether the application was running prior to Windows being restarted).

NOT WORKING!!!!

Ok, so by now if you’ve attempted to add a StartupTask, or played with the Restart apps option in Settings, you may well be getting frustrated because your application fails to launch – the splashscreen appears but then the application fails to launch.

This is because the default new project template you get when creating a new Windows (UWP/WinUI) application is significantly broken. Whilst it appears to include the basics required to run an application (i.e. the Launch event), it doesn’t include the code necessary to handle application activation. Application activation, triggered by things like a StartupTask, takes an alternative code path in a Windows application and does not call the OnLaunched method in the App class (why it doesn’t, I’ve never quite understood, since the application is indeed being launched *shrugs*).

Luckily the fix for this is relatively straight forward. You can either copy the code from the OnLaunched method override into an OnActivated method override, or you can move the OnLaunched logic into a separate method, HandleActivation, that can be called by both OnLaunched and OnActivated methods.

protected override void OnLaunched(LaunchActivatedEventArgs e)
{
    HandleActivation(e);
}
protected override void OnActivated(IActivatedEventArgs args)
{
    base.OnActivated(args);
    HandleActivation(args);
}
private void HandleActivation(IActivatedEventArgs e) {
    Frame rootFrame = Window.Current.Content as Frame;

    // Do not repeat app initialization when the Window already has content,
    // just ensure that the window is active
    if (rootFrame == null)
    {
        // Create a Frame to act as the navigation context and navigate to the first page
        rootFrame = new Frame();

        rootFrame.NavigationFailed += OnNavigationFailed;

        if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
        {
            //TODO: Load state from previously suspended application
        }

        // Place the frame in the current Window
        Window.Current.Content = rootFrame;
    }

    var launch = e as LaunchActivatedEventArgs;
    if (!(launch?.PrelaunchActivated??false))
    {
        if (rootFrame.Content == null)
        {
            // When the navigation stack isn't restored navigate to the first page,
            // configuring the new page by passing required information as a navigation
            // parameter
            rootFrame.Navigate(typeof(MainPage), launch?.Arguments);
        }
        // Ensure the current window is active
        Window.Current.Activate();
    }
}

Hopefully in this post you’ve seen how easily your application can register as a StartupTask. Don’t forget to handle the OnActivated method in your application.

Testing Cosmos DB in Azure DevOps Pipeline

As part of writing code to read and write data to Azure Cosmos DB I created a bunch of test cases. However, I didn’t want to have my test cases reading and writing from an actual Cosmos DB instance. For this, the Cosmos DB Emulator is perfect. That is, until you come to want to … Continue reading “Testing Cosmos DB in Azure DevOps Pipeline”

As part of writing code to read and write data to Azure Cosmos DB I created a bunch of test cases. However, I didn’t want to have my test cases reading and writing from an actual Cosmos DB instance. For this, the Cosmos DB Emulator is perfect. That is, until you come to want to run the test cases as part of your Azure DevOps Pipeline.

Don’t Do This

Ok, so there is a preview of a Cosmos DB Emulator extension for Azure DevOps that you can install and then invoke by following the instructions, which cumulates in adding the following yaml to your pipeline.

- task: azure-cosmosdb.emulato[email protected]2
  displayName: 'Run Azure Cosmos DB Emulator'

Unfortunately this doesn’t work with the latest windows host agent, giving the following error:

Error response from daemon: hcsshim::CreateComputeSystem 658b0f0e635e4c5bbdf4c5b3d5a8823da5d3b5183b7a7a10fe5386977cdccb5d: The container operating system does not match the host operating system.

This has also been documented, and let unresolved, on this GitHub issue.

Do This Instead

As someone has pointed out in the GitHub issue talking about the issue using the extension, the resolution is actually quite simple. On the latest Windows host agents, the Azure Cosmos DB Emulator is already preinstalled, it just needs to be started. Simply add the following to your yaml build pipeline.

- task: [email protected]2
  displayName: 'Starting Cosmos Emulator'
  inputs:
    targetType: 'inline'
    workingDirectory: $(Pipeline.Workspace)
    script: |
        Write-Host "Starting CosmosDB Emulator"
        Import-Module "C:/Program Files/Azure Cosmos DB Emulator/PSModules/Microsoft.Azure.CosmosDB.Emulator"
        Start-CosmosDbEmulator

And now you can access the Cosmos DB Emulator from your test cases. In my case I made sure to include the Endpoint and AuthKey (which is a static predefined key) in my .runsettings file.

<?xml version="1.0" encoding="utf-8"?>
<RunSettings>
	<TestRunParameters>
		<Parameter name="CosmosDBEmulatorEndpoint" value="https://localhost:8081" />
		<Parameter name="CosmosDBEmulatorAuthKey" value="C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==" />
	</TestRunParameters>
</RunSettings>

Persisting Cloud Events to Cosmos DB in Azure

If you start down the path of implementing Event Sourcing, you’ll most likely come across https://cloudevents.io/ which has the tag line “A specification for describing event data in a common way”. This project seems to be well supported (take a look at the contributors list) and has language projections for a number of different languages. … Continue reading “Persisting Cloud Events to Cosmos DB in Azure”

If you start down the path of implementing Event Sourcing, you’ll most likely come across https://cloudevents.io/ which has the tag line “A specification for describing event data in a common way”. This project seems to be well supported (take a look at the contributors list) and has language projections for a number of different languages. It also has some out of the box support for working with various event systems, such as Azure Event Grid. In this post we’re going to look at how you can simply save and retrieve a CloudEvent (the implementation of the Cloud Events spec) to Azure Cosmos DB.

Getting Started

We’ll start with a basic ASP.NET Core Web API project. Given the .NET 5 release is just around the corner, we’re going to pick the .NET 5.0 (Preview) option. We’re also going to enable the OpenAPI support – this is awesome as it not only gives us OpenAPI (aka next gen of Swagger) documentation for our API, it also gives us a neat test page that we’ll be using to send CloudEvents to our API.

We’re going to remove the default WeatherForecastController and the associated WeatherForecast class. Then we’re going to add a new controller, CloudEventsController, which has a single method, UploadCloudEvent, which will be invoked when a POST is made to the /api/cloudevents endpoint.

[ApiController]
[Route("[controller]")]
public class CloudEventsController : ControllerBase
{
    private readonly ILogger<CloudEventsController> _logger;

    public CloudEventsController(ILogger<CloudEventsController> logger)
    {
        _logger = logger;
    }

    [HttpPost]
    public async Task<ActionResult<string>> UploadCloudEvent(
        [FromBody] CloudEvent cloudEvent)
    {
        return Ok("TBD");
    }
}

At this point if we attempt to build the project, we’ll get a build error because the CloudEvent class isn’t recognised. Luckily Visual Studio can help us with this by recommending an appropriate NuGet Package.

We can build and run the project at this point and attempt to send a CloudEvent, as JSON, using the test UI.

Unfortunately what we get back is an error:

System.NotSupportedException: Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported. Type 'CloudNative.CloudEvents.CloudEvent'. Path: $ | LineNumber: 0 | BytePositionInLine: 1. ---> System.NotSupportedException: Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported. Type 'CloudNative.CloudEvents.CloudEvent'.

Luckily the C# implementation for CloudEvents also has an ASP.NET Core support package, CloudNative.CloudEvents.AspNetCore. This package includes a Json formatter, CloudEventJsonInputFormatter, which can be registered in the ConfigureServices in order to deserialise CloudEvent objects from Json.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers(opts =>
    {
        opts.InputFormatters.Insert(0,new CloudEventJsonInputFormatter());
    });

    services.AddControllers();
    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new OpenApiInfo { Title = "CloudEventsSample", Version = "v1" });
    });
}

At this point we’re pretty close to being able to send CloudEvents to our API. However, if we attempt to POST a CloudEvent, we’ll get the following error

{ "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "traceId": "00-67ef18df9db33e49bb9aeb0b0c717e32-87b5edd8ba995c4d-00", "errors": { "DataContentType.Name": [ "The Name field is required." ] } }

For those who’ve worked with the ASP.NET model validation, this type of error would look fairly familiar. Essentially, it’s failing because we’re not setting the Name property on the DataContentType element. What’s confusing is that neither the CloudEvent class, nor any of it’s dependent types, have been attributed with any validation attributes (eg Required).

In .NET 5, the default validation has changed, with all properties required by default. This is a careless change and one that’s going to cause in-ordinate amount of frustration both on new projects and existing project that are upgrading. Unfortunately, the ship has sailed on this one, so the best we can do is to code around it. Luckily, there’s a simple fix – add the following line to your ConfigureServices method.

services.AddControllers(options => options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true);

At this point you can run the project and use the test interface to submit a CloudEvent as Json. Everything appears to work and the test interface returns the string “TBD” as the code ssuggets. However, if you set a break point in the UploadCloudEvent method and inspect the cloudEvent object, you’ll see that none of the properties have the correct value.

Turns out that if you use the default options in the test UI, most of the properties of the CloudEvent are not set. However, if you select application/cloudevents+json from the content type dropdown, the CloudEvent object is correctly deserialised.

As you can see the CloudEvents has a number of standard properties, and then a nested Data property – this is where your application specific data will reside. You’ll also notice that there’s an Extensions property, which we’ll come back to later but is useful for capturing additional metadata about the event.

Saving To Azure Cosmos DB

Every application has their own requirements for how to process and store events. In our case, we opted to save CloudEvents to an Azure Cosmos DB. In this post we’re not providing advice as to whether this is appropriate for your application, I’m simply going to walk through how to save and retrieve CloudEvent objects to/from Azure Cosmos DB.

I’m not going to walk through the process of setting up Cosmos DB, since there’s some great tutorials in the documentation for the product. What we are going to do is to define a couple of constants that we’ll need.

private string EndpointUrl = "https://yourcosmosdb-westus.documents.azure.com:443/";
private string AuthorizationKey = "Ut5NuthyYUBXlL0bxBY.........";
private string DatabaseId = "cloudeventssample";
private string ContainerId = "cloudevents";
private string PartitionKeyPath = "/type";

The EndpointUrl and AuthorizationKey come from your instance of CosmosDB. The DatabaseId, ContainerId and PartitionKeyPath are all specific to your application. Not however, that the PartitionKeyPath has to match to a property on the CloudEvent object. For the moment we’re going to use the Type property on the CloudEvent. However, we’ll see later how we can use an extension to define a partitionkey property that is more suited for this purpose.

We can then add code to the UploadCloudEvent to save the CloudEvent to a Cosmos DB container.

[HttpPost]
public async Task<ActionResult<string>> UploadCloudEvent(
    [FromBody] CloudEvent cloudEvent)
{
    var cosmosClient = new CosmosClient(EndpointUrl, AuthorizationKey);
    var databaseReq = await cosmosClient.CreateDatabaseIfNotExistsAsync(DatabaseId);
    Debug.WriteLine("Created Database: {0}", databaseReq.Database.Id);
    var containerReq = await cosmosClient.GetDatabase(DatabaseId).CreateContainerIfNotExistsAsync(ContainerId, PartitionKeyPath);
    Debug.WriteLine("Created Container: {0}", containerReq.Container.Id);

    var container = containerReq.Container;

    var response = await container.CreateItemAsync(cloudEvent);

    return Ok($"Created {response.StatusCode}");
}

As you can probably have predicted by now, this fails – nothing worth doing is easy! This time it fails stating:

Microsoft.Azure.Cosmos.CosmosException : Response status code does not indicate success: BadRequest (400); Substatus: 0; ActivityId: 70c79590-fdf2-4bf6-80a1-cd4af23774c5; Reason: (Message: {"Errors":["The input content is invalid because the required properties - 'id; ' - are missing"]} ActivityId: 70c79590-fdf2-4bf6-80a1-cd4af23774c5, Request URI: /apps/548fba61-21b5-4fac-a478-8b7b5a1b3640/services/81adfd47-798e-4614-9c70-64cacf31b2e7/partitions/c9dabeda-0b5b-4ab3-8156-d4c63069599d/replicas/132485574048358760p/, RequestStats: Please see CosmosDiagnostics, SDK: Windows/10.0.19042 cosmos-netstandard-sdk/3.14.0);

This is weird, considering the CloudEvent class does have an Id property and the instance passed into the UploadCloudEvent method does have a non-null string value for Id. Which leads us to an issue with the serialization. The default Json serialization would be to change the Id property to id. However, it would appear that the Cosmos DB client doesn’t realise this, so is looking for an “id” property on the original entity (which is different from the Id property that exists).

The fix for this is to change the serialization options for the CosmosClient, as follows:

var options = new CosmosClientOptions
{
    SerializerOptions = new CosmosSerializationOptions { PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase }
};
var cosmosClient = new CosmosClient(EndpointUrl, AuthorizationKey, options);

Then, just when you thought there can’t possibly be anything more to saving a CloudEvent to CosmosDB, think again. Attempting to upload a CloudEvent now generates the following error:

Newtonsoft.Json.JsonSerializationException: Unable to find a constructor to use for type CloudNative.CloudEvents.CloudEvent. A class should either have a default constructor, one constructor with arguments or a constructor marked with the JsonConstructor attribute. Path 'dataContentType', line 1, position 19.

I’m sure there are other ways to resolve this issue but I went the route of handling the serialization and deserialization of the CloudEvent myself. The CloudEvents SDK already has helper methods that can do this but relies on reading from and writing to a stream. Luckily, the CosmosClient also has overloads that can take a stream for the contents of the item being created.

The following CreateCloudEventAsync method follows a similar structure to the CreateItemAsync method, accepting the same parameters. However, the implementation involves using a MemoryStream to serialize the CloudEvent before calling the CreateItemStreamAsync method on the Container. This uses the JsonEventFormatter, that’s part of the CloudEvents SDK.

public static class ContainerHelpers
{
    public static async Task<ResponseMessage> CreateCloudEventAsync(
        this Container container, CloudEvent item, 
        ItemRequestOptions requestOptions = null, 
        CancellationToken cancellationToken = default)
    {
        var formatter = new JsonEventFormatter();
        var bytes = formatter.EncodeStructuredEvent(item, out _);
        using (var ms = new MemoryStream(bytes))
        {
            var createResponse = await container.CreateItemStreamAsync(ms, new PartitionKey(item.Type), requestOptions, cancellationToken);
            return createResponse;
        }
    }
}

That completes the process of saving a CloudEvent to CosmosDB.

Reading CloudEvents from CosmosDB

Unfortunately, for the same reason that we weren’t able to use the CreateItemAsync method to save a CloudEvent, we’re also prevented from using the ReadItemAsync method. Instead we have to use the ReadItemStreamAsync method and then again use the JsonEventFormatter to deserialize the CloudEvent from the returned stream.

We’ll add the ReadCloudEventAsync extension method to the ContainerHelpers class shown earlier.

public static async Task<CloudEvent> ReadCloudEventAsync(
    this Container container, 
    string id, string partitionKey, 
    ItemRequestOptions requestOptions = null, 
    CancellationToken cancellationToken = default)
{
    var responseMessage = await container.ReadItemStreamAsync(id, new PartitionKey(partitionKey), requestOptions, cancellationToken);
    if (responseMessage.StatusCode == HttpStatusCode.NotFound) throw new CosmosException("Missing", responseMessage.StatusCode, 0, null, 0);
    var formatter = new JsonEventFormatter();
    var cloudEvent = await formatter.DecodeStructuredEventAsync(responseMessage.Content, null);
    return cloudEvent;
}

At the beginning of the UploadCloudEvent method, we’ll call the ReadCloudEventAsync method to see if the CloudEvent already exists, before attempting to create it only if it doesn’t exist.

try
{
    var ce = await container.ReadCloudEventAsync(cloudEvent.Id, cloudEvent.Type);

    return Ok($"Item already exists {ce.Id}");
}
catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
    var response = await container.CreateCloudEventAsync(cloudEvent);
    return Ok($"Created {response.StatusCode}");
}

CloudEvent Extensions

Earlier I mentioned that we were using the Type property on the CloudEvent class as the partition key. For a bunch of reasons I don’t think this is a good idea. Luckily, the CloudEvents SDK supports extending the CloudEvent class, not through inheritance (which always causes issues with serialization) but through extensions which can be added to the CloudEvent. In fact, there are a number of extensions already provided by the SDK. One of which is a PartitioningExtension, which makes it possible to add a partitionKey property to the CloudEvent JSON object, for example:

{
"specversion" : "1.0",
"type" : "com.github.pull.create",
"source" : "https://github.com/cloudevents/spec/pull",
"subject" : "123",
"id" : "A234-1234-1234",
"time" : "2018-04-05T17:31:00Z",
"partitionKey" : "defect-123",
"datacontenttype" : "text/xml",
"data" : ""
}

To get this to work in our Web API project, there are a couple of things we need to do:

PartitionKeyPath

We’ll need to change the PartitionKeyPath to be /partitionKey instead of /type. Unfortunately this means you’ll need to recreate the container (or just create a new one).

private string ContainerId = "paritionedcloudevents";
private string PartitionKeyPath = "/partitionKey";

PartitionKey for Reading and Writing

Instead of passing cloudEvent.Type into the ReadCloudEventAsync method, we now need to extract the value of the ParitioningExtension.

var ce = await container.ReadCloudEventAsync(cloudEvent.Id, cloudEvent.Extension<PartitioningExtension>().PartitioningKeyValue);

Also, in the CreateCloudEventAsync method, we need to change how the PartitionKey is created

var createResponse = await container.CreateItemStreamAsync(ms, new PartitionKey(item.Extension<PartitioningExtension>().PartitioningKeyValue), requestOptions, cancellationToken);

Use PartitioningExtension in CloudEventJsonInputFormatter

The one place where it’s not easy to add in the PartitioningExtension is with the CloudEventJsonInputFormatter, which is used to deserialize the CloudEvent that’s being sent to the /api/cloudevents endpoing. Currently, if the posted JSON includes the partitionKey property, it will be ignored and no extensions will be added to the generated CloudEvent object.

Unfortunately the CloudEventJsonInputFormatter that comes as part of the CloudEvents SDK provides no mechanism to include extensions as part of the deserialization process. Luckily, this is also quite easy to fix.

We’ll start by taking a copy of the CloudEventJsonInputFormatter class and add it to our project. To avoid issues with naming conflicts, we’ll rename our class to CloudEventWithExtensionsJsonInputFormatter.

Then we need to add support for instantiating the necessary extensions as part of the ReadRequestBodyAsync method. Here we’re simply going to use a function callback as this seemed an easy way to generate the extensions each time the method is called – Do NOT simply create a single instance of the extension, otherwise all CloudEvent objects will end up with the same extension. The changed lines are marked in bold.

public class CloudEventWithExtensionsJsonInputFormatter : TextInputFormatter
{
    private Func<ICloudEventExtension[]> cloudExtensionsFactory;
    public CloudEventWithExtensionsJsonInputFormatter(Func<ICloudEventExtension[]> extensionsFactory)
    {
        cloudExtensionsFactory = extensionsFactory;

        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json"));
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/cloudevents+json"));
        SupportedEncodings.Add(Encoding.UTF8);
        SupportedEncodings.Add(Encoding.Unicode);
    }
    public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }
        if (encoding == null)
        {
            throw new ArgumentNullException(nameof(encoding));
        }
        var request = context.HttpContext.Request;
        try
        {
            var cloudEvent = await request.ReadCloudEventAsync(cloudExtensionsFactory?.Invoke());
            return await InputFormatterResult.SuccessAsync(cloudEvent);
        }
        catch (Exception ex)
        {
            return await InputFormatterResult.FailureAsync();
        }
    }
    protected override bool CanReadType(Type type)
    {
        if (type == typeof(CloudEvent))
        {
            return base.CanReadType(type);
        }
        return false;
    }
}

Now we just need to update our Startup to register the CloudEventWithExtensionsJsonInputFormatter class, with the appropriate callback that returns a PartitioningExtension instance.

opts.InputFormatters.Insert(0,new CloudEventWithExtensionsJsonInputFormatter(()=>new[] { new PartitioningExtension() }));

After all that, we’re good to go – When we POST a CloudEvent with the partitionKey property we can see that it’s included in the PartitioningExtension.

Hopefully this has given you enough information to work with CloudEvents and CosmosDB.

If there’s anything you get stuck on, or to provide feedback, please don’t hesitate to leave a comment.