2023-03-22 15.0.0-beta of the .NET Client Library

Hello,

I just released the beta of the .NET Client Library / SDK.

Streamlined developer experience

The goal was to streamline the developer experience. I have removed a lot of old code and made the handling of the app names more consistent. Because a lot of the code was auto generated you had to define the app name in the options and then again in the actual requests. For example:

var clientManager =
    new SquidexClientManager(
        new SquidexOptions
        {
            AppName = "...",
            ClientId = "...",
            ClientSecret = "...",
            Url = "https://cloud.squidex.io"
        });

var assetsClient = clientManager.CreateAssetsClient();

var assets = await assetsClient .GetAssetsAsync("my-app");

but for contents it was not needed, because the client was coded manually:

var client = clientManager.CreateContentsClient<BlogPost, BlogPostData>("posts");

var content = await client.CreateAsync(...);

I have adjusted the templates of the code generator and the app name is now set automatically.

In addition to that, the client manager has been renamed client and it storing references to the actual clients now. So you don’t have to do that.

This means that the example above looks like this now:

var client =
    new SquidexClient(
        new SquidexOptions
        {
            AppName = "...",
            ClientId = "...",
            ClientSecret = "...",
            Url = "https://cloud.squidex.io"
        });

// It is a property now.
var assets = await client.Assets.GetAssetsAsync();

Multiple Clients

If you deal with multiple apps you have to work with multiple clients now.

BUT, you can also register multiple clients in the service locator and use a new interface to provide the correct client:

var provider =
	new ServiceCollection()
		.AddSquidexClient(null)
		.Configure<SquidexServiceOptions>(options =>
		{
			options.AppName = "app";
			options.ClientId = "id";
			options.ClientSecret = "secret";
			options.Url = "https://custom1.squidex.io";
		})
		.Configure<SquidexServiceOptions>("named", options =>
		{
			options.AppName = "app";
			options.ClientId = "id";
			options.ClientSecret = "secret";
			options.Url = "https://custom2.squidex.io";
		})
		.BuildServiceProvider()
		.GetRequiredService<ISquidexClientProvider>();

var client1 = provider.Get();
var client2 = provider.Get("named");

The normal client (without the name) is registered as well, so you can resolve ISquidexClient or ISquidexClientProvider.

It is still in beta to get your feedback.

More Clients

I am also working together with the guys from fern (https://www.buildwithfern.com/) to get SDKs out for more programming language. I hope we can release Node (Typescript) and Java versions soon and and have a setup for more programming languages.

2 Likes

So aside from the rename of clientManager to client existing code for contents, shared and other clients like the following remain the same or are they also now properties? If I get time today I will try it out and see for myself.

var rulesClient = clientManager.CreateExtendableRulesClient();
builder.Services.AddSingleton(rulesClient);

var schemasClient = clientManager.CreateSchemasClient();
builder.Services.AddSingleton(schemasClient);

var someContentsClient = clientManager.CreateContentsClient<SomeContent, SomeContentData>("some-contents");
builder.Services.AddSingleton(someContentsClient);

var dynamicContentsClient = clientManager.CreateSharedDynamicContentsClient();
builder.Services.AddSingleton(dynamicContentsClient);

Every client that has no parameter is a property now.

e.g.

client.Schemas
client.ExtendableRules
client.SharedDynamicContents

If you have parameters it is a method, but thr result is cached.

var c1 = client.Contents<SomeContent, SomeContentData>("some-contents");
var c2 = client.Contents<SomeContent, SomeContentData>("some-contents");

ReferenceEquals(c1, c2) == true;

So you can also write directly:

client.client.Contents<SomeContent, SomeContentData>("some-contents")
    .CreateAsync(...);

Of course it is not the fastest way,

Of course you can also add the clients to the service locator, but I would leverage lazy initialization:

builder.Services.AddSingleton(c => c.GetRequiredService<ISquidexClient>().Schemas);
1 Like

From quick looks it seems i cannot (should not) use IHttpClientProvider directly on SquidexOptions. I have it just because in early days it was required to skip https validation.

It seems like currently i do not need custom IHttpClientProvider anymore as HTTPS validation can be set directly on SquidexOptions. During this i found I have commented code with option settings Proxy on HttpClient. As it was useful during development. I then can use Fiddler to actually see every sent raw requests / response. Did you think it could be added to default SquidexOptions?

Hi,

The http client provider is still accessible. But it was a big mess with the different ways to initialize a client. So it has been simplified. To make it somehow backwards compatible, there is a setting called IgnoreSelfSignedCertificates on the options.

But anyway, I recommend to use the integration for IServiceCollection where you have even more configurations. It is described in the docs: https://docs.squidex.io/02-documentation/software-development-kits/.net-standard/version-v15-and-later

1 Like

Thanks. Base on your docs it looks great

1 Like

Thanks for the documentation around this, I am just going through the process of updating the client library and it’s been very useful. I just wanted to clarify the best way of utilising the Lazy Initialization with DI. I’ve been through three stages so thought I would list them here in case anyone else finds it useful or has feedback.

First iteration was to wire up each client as a singleton in Program.cs as that was most similar to what we had before and required no changes to constructors or any other code:

    private readonly ISchemasClient schemasClient;
    private readonly IAppsClient appsClient;
    private readonly IContentsClient<<OurSchema, OurSchemaData>> ourSchemaContentsClient;

    public SquidexInitializer(
        ISchemasClient schemasClient,
        IAppsClient appsClient,
        IContentsClient<<OurSchema, OurSchemaData>> ourSchemaContentsClient
        )
    {
        this.schemasClient = schemasClient;
        this.appsClient = appsClient;
        this.ourSchemaContentsClient = ourSchemaContentsClient;
    }

    public async Task InitialiseSquidex()
    {
        var schema = await schemasClient.GetSchemaAsync("our-schema");
        var workflows = await appsClient.GetWorkflowsAsync();
        var schemaContents = await ourSchemaContentsClient.GetAllAsync();
    }

The second iteration was again driven by being cautious about the amount of code being changed, with ISquidexClient now being injected but the constructor assigning clients to fields so that call sites did not need to be amended:

    private readonly ISchemasClient schemasClient;
    private readonly IAppsClient appsClient;
    private readonly IContentsClient<<OurSchema, OurSchemaData>> ourSchemaContentsClient;

    public SquidexInitializer(ISquidexClient squidexClient)
    {
        schemasClient = squidexClient.Schemas;
        appsClient = squidexClient.Apps;
        ourSchemaContentsClient = squidexClient.Contents<OurSchema, OurSchemaData>("our-schema");
    }

    public async Task InitialiseSquidex()
    {
        var schema = await schemasClient.GetSchemaAsync("our-schema");
        var workflows = await appsClient.GetWorkflowsAsync();
        var schemaContents = await ourSchemaContentsClient.GetAllAsync();
    }

However I do not think that is taking full advantage of the lazy initialisation, so now I am instead aiming to use it at the call sites:

    private readonly ISquidexClient squidexClient;

    public SquidexInitializer(ISquidexClient squidexClient)
    {
        this.squidexClient = squidexClient;
    }

    public async Task InitialiseSquidex()
    {
        var schema = await squidexClient.Schemas.GetSchemaAsync("our-schema");
        var workflows = await squidexClient.Apps.GetWorkflowsAsync();
        var schemaContents = await squidexClient.Contents<<OurSchema, OurSchemaData>>("our-schema").GetAllAsync();
    }

Based on the documentation and what was said here the latter is the preferred approach?

I think the main difference is about testing. If you inject concrete clients you do not have to mock the ISquidexClient. But usually performance does not matter here.

1 Like