One of the major reasons to use the new alpha release of the CDS SDK is that you can use it with the latest version of ASP.NET Core 3.1. Super cool, yes, so how do you add it to your ASP.NET Core project through dependency injection, and how do you add it to ensure that it is the most efficient where each request isn’t blocking each other. But only to the limits of the SDK throttling 😊. Let’s explore how you can start an ASP.NET Core project from the ground up and include the CdsServiceClient.
TL;DR
Add CdsServiceClient through dependency injection as a transient registration using CdsServiceClient.Clone()
to ensure efficient non-blocking multiple requests on an ASP.NET Core 3.1 application.
services.AddCdsServiceClient(options =>
Configuration.Bind("CdsServiceClient", options));
https://github.com/CdsWeb-app/CdsWeb
The details…
Understanding Dependency Injection and Service Registration Lifetime with ASP.NET
If you have worked with the modern version of ASP.NET Core, be it 1.0 all the way to the latest of 3.1 they all support dependency injection (DI) design pattern and actually pushes it as the preferred and documented approach. Effectively with DI you are registering a class/service with the ASP.NET application, and making it available to all parts of the application. So ideally you want to register CdsServiceClient
or IOrganizationService
so that its accessible in your controllers or other classes in the application. Registration of the class/service is done within the Startup.cs of the application so that when the ASP.NET site starts that class is available everywhere else.
A key part of that registration is the service lifetime. You are either registering it as a singleton (created once for the entire application), scoped (created for each request), or transient (created for each request for each container). Read more about service lifetimes to gain a better understanding, and a demonstration of how the lifetimes effect a registration. So how should the CdsServiceClient
be registered you probably ask. The options are really Singleton and Transient. You might be thinking Singleton as you want one database connection for your application. However 1 connection with an application that can handle multiple requests at once will block each other. So you then maybe want to select Transient making every request make a new connection. However then there is a lot of overhead on every request. So what do you do?
Multiple Instances of CdsServiceClient
Efficiently…Clone()
There is a method on CdsServiceClient
and on the old CrmServiceClient
that is here to help you create copies efficiently…Clone()
. What is clone you ask, well it clones the CdsServiceClient
, and makes a new copy of it. It actually duplicates the token, the web proxy client and a bunch of things that would already exist and saves a lot of the startup work that occurs when you call the CdsServiceClient constructors. This is great because it gives you another instance of CdsServiceClient without the overhead of actually creating one from scratch.
var cdsClient1 = new CdsServiceClient("<connection string>");
var cdsClient2 = cdsClient1.Clone();
Applying CdsServiceClient.Clone()
and ASP.NET Service Registration Together
So how do you now on every request in every container clone an existing CdsServiceClient
and have that as a service registration? Ideally we have a Singleton CdsServiceClient
sitting at the application level that is available to be constantly cloned so that we can add the cloned CdsServiceClient
as a Transient lifetime for service registration. But if we have the CdsServiceClient
class registered as both Singleton and Transient, when we try to get the CdsServiceClient
through dependency injection in our classes it won’t know which to get.
To handle this we can create a little wrapper class and make CdsServiceClient a property of that class, then register the wrapper class as a Singleton.
public class CdsServiceClientWrapper
{
public readonly CdsServiceClient CdsServiceClient;
public CdsServiceClientWrapper(string connectionString)
{
CdsServiceClient = new CdsServiceClient(connectionString);
}
}
Then register that class as a Singleton during startup.
services.AddSingleton(sp =>
new CdsServiceClientWrapper("<connection string>"));
To get our cloned version that will be used in all our other containers through dependency injection add a clone of the wrapper CdsServiceClient
property using the IOrganizationService
as the service using the CdsServiceClient
implementation.
services.AddTransient<IOrganizationService, CdsServiceClient>(sp =>
sp.GetService<CdsServiceClientWrapper>().CdsServiceClient.Clone());
This then allows us in any class, like an API controller you can have IOrganizationService
as a constructor parameter on that class so you can utilize it throughout that class. As well you can cast IOrganizationService into CdsServiceClient. Here is an example of a simple controller using the account entity.
[ApiController]
[Route("[controller]")]
public class AccountController : ControllerBase
{
private readonly ILogger<AccountController> _logger;
private readonly IOrganizationService _orgService;
private readonly CdsServiceClient _cdsServiceClient;
public AccountController(ILogger<AccountController> logger, IOrganizationService orgService)
{
_logger = logger;
_orgService = orgService;
_cdsServiceClient = (CdsServiceClient)orgService;
}
[HttpGet]
public IEnumerable<AttributeMetadata> Get()
{
var response = _cdsServiceClient.GetAllAttributesForEntity("account");
return response;
}
[HttpGet("{id}")]
public string Get(Guid id)
{
var response = _orgService.Retrieve("account", id, new ColumnSet(true));
return response.GetAttributeValue<string>("fullname");
}
}
Make it simple with an extension method
When you are adding to ConfigureServices in Startup.cs you often will see quick one line methods that do a bunch of work. That is done with an extension method. We can wrap all of the functionality within here into an AddCdsServiceClient extension method as well that makes it easy to add to any application as well as get the options like the connection string or including IOrganizationService or OrganizationServiceContext as registrations too.
Our extension method could look something like this:
public static void AddCdsServiceClient(this IServiceCollection services, Action<CdsServiceClientOptions> configureOptions)
{
CdsServiceClientOptions cdsServiceClientOptions = new CdsServiceClientOptions();
configureOptions(cdsServiceClientOptions);
services.AddSingleton(sp =>
new CdsServiceClientWrapper(cdsServiceClientOptions.ConnectionString));
services.AddTransient<IOrganizationService, CdsServiceClient>(sp =>
sp.GetService<CdsServiceClientWrapper>().CdsServiceClient.Clone());
if (cdsServiceClientOptions.IncludeOrganizationServiceContext)
{
services.AddTransient(sp =>
new OrganizationServiceContext(sp.GetService<IOrganizationService>()));
}
}
Added here is also optional the OrganizationServiceContext
as a service registration if you prefer to use LINQ based queries.
To support the options there is an options class to support pulling configuration from the appsettings.json.
public class CdsServiceClientOptions
{
/// <summary>
/// <see cref="CdsServiceClient"/> constructors for connection string
/// </summary>
public string ConnectionString { get; set; }
/// <summary>
/// Parameter to allow for transient OrganizationServiceContext service based on Clone
/// of singleton CdsServiceClient.
/// </summary>
/// <see cref="OrganizationServiceContext"/>
public bool IncludeOrganizationServiceContext {get; set;}
}
Then within the Startup.cs it simply becomes:
services.AddCdsServiceClient(options => Configuration.Bind("CdsServiceClient", options));
In appsettings.json are the values of the options:
"CdsServiceClient": {
"ConnectionString": "",
"IncludeOrganizationServiceContext": false
}
Starting fresh then start with all this and more included in CdsWeb an ASP.NET Core 3.1 project available in the following Git repo:
I was just setting up an API today on a trial instance but have failed to get the connection to work.
Wondering what you recommend for connection string type. I have had no success with “Office365”
“AuthType=Office365;Url=https://##.crm3.dynamics.com;Username=##@##.onmicrosoft.com;Password=****;”
In XRM toolbox same connection string connects.
Within CdsServiceClientWrapper after CdsServiceClient the client attributes are all blank or default values.
Notable attributes.
ActiveAuthenticationType: InvalidConnection
LastCdsExcheption and LastCdsError are both empty.
LikeLike
With .net core you can only use client secret or certificate. User/pass is not supported.
LikeLike
Hi Colin,
Firstly, thank you for article, can you share any example of client secret authentication steps. I try to CdsServiceClient but didn’t make successful. I research to secret token steps but could’t find yet. By the way, have you a any onpremise organizastion connection methods?
LikeLike
Hi, if you want an example of the client secret it is the same as CrmServiceClient and just about constructing a connection string with the client secret and id.
var client = new CdsServiceClient(“AuthType=ClientSecret;url=https://contosotest.crm.dynamics.com;ClientId={AppId};ClientSecret={ClientSecret}”)
On prem is not available with the new SDK currently.
LikeLike