microsoft / PowerPlatform-DataverseServiceClient

Code Replica for Microsoft.PowerPlatform.Dataverse.Client and supporting nuget packages.
MIT License
281 stars 51 forks source link

services.AddScoped<IServiceContext, ServiceContext>() seems to cache queries #51

Closed UM001 closed 4 years ago

UM001 commented 4 years ago

I have a T4 generated ServiceContext with Interface IServiceContext holding limited amount of entities. I use _serviceContext.Set to query data.

If I add those as singleton, everything is extremely fast, but data is 'cached'. That is changing in D365CE is not reflected in the entitity until I restart application. If scoped is used, data is retrieved (not 'cached') but application is much slower.. ..performance is becoming issue. What do I wrong?

` [System.CodeDom.Compiler.GeneratedCodeAttribute("CrmSvcUtil", "9.1.0.54")] public partial class ServiceContext : Microsoft.Xrm.Sdk.Client.OrganizationServiceContext {

    /// <summary>
    /// Constructor.
    /// </summary>
    public ServiceContext(Microsoft.Xrm.Sdk.IOrganizationService service) : 
            base(service)
    {
    }`

` services.AddCdsServiceClient( options => Configuration.Bind("CdsServiceClient", options) );

        services.AddScoped<IServiceContext, ServiceContext>();`

vs

` services.AddCdsServiceClient( options => Configuration.Bind("CdsServiceClient", options) );

        services.AddSingleton<IServiceContext, ServiceContext>();`

`public static class CdsServiceCollectionExtensions { ///

/// Include a CdsServiceClient as a singleton service within the Service Collection /// Optional include transient services for IOrganizationService /// and OrganizationServiceContext /// /// /// public static void AddCdsServiceClient(this IServiceCollection services, Action configureOptions) { CdsServiceClientOptions cdsServiceClientOptions = new CdsServiceClientOptions(); configureOptions(cdsServiceClientOptions);

        services.AddSingleton(sp =>
            new CdsServiceClientWrapper(
                cdsServiceClientOptions,
                sp.GetRequiredService<ILogger<CdsServiceClientWrapper>>(),
                cdsServiceClientOptions.TraceLevel)
            );

        services.AddTransient<IOrganizationService, CdsServiceClient>(sp =>
            sp.GetService<CdsServiceClientWrapper>()._cdsServiceClient.Clone());

        if (cdsServiceClientOptions.IncludeOrganizationServiceContext)
        {
            services.AddTransient(sp =>
                new OrganizationServiceContext(sp.GetService<IOrganizationService>()));
        }
    }
}`
MattB-msft commented 4 years ago

Can you generate the ServiceContext class with the CrmServiceUtil instead and see fi that varies your behavior? https://docs.microsoft.com/en-us/powerapps/developer/common-data-service/org-service/generate-early-bound-classes

UM001 commented 4 years ago

I have them generated with this tool (version 9.1.0.54). If I change code below from transient to scoped, the performance is better. The last is not required either. On every webrequest the constructor of CdsClient is executed. The idea is from this post: https://colinvermander.com/2020/04/02/tracing-cdsserviceclient-in-net-core-and-asp-net-core/ as all calls could be traced (not working yet, work in progress) and I want no httpclient socket exhausting.

`services.AddScoped<IOrganizationService, CdsServiceClient>(sp => sp.GetService()._cdsServiceClient.Clone());

        if (cdsServiceClientOptions.IncludeOrganizationServiceContext)
        {
            services.**AddScoped**(sp =>
                new OrganizationServiceContext(sp.GetService<IOrganizationService>()));
        }`
MattB-msft commented 4 years ago

@UM001 I am unclear as to what your trying to do here and what your issue is... from my read... you have created a OrganizationServiceContext ,

It sounds like you mixing OrganizationServiceContext queries with IOrganization Actions?

The existing behavior of the OrganizationServiceContext keeps track of changes that are made though it and attempts to Optimize those calls using the underlying ServiceContext logic.. however expects the author to manage the change and merge behavior using the MergeOptions enum.

For example: given this code where cli is a CdsServiceClient.

            CrmServiceContext ctx = new CrmServiceContext(cli);
            var recs0 = from acctQ in ctx.AccountSet
                        where acctQ.Id == Guid.Parse("1ece5cd4-ae56-e711-abaa-00155d701c02")
                        select acctQ;
            Trace.WriteLine(recs0.FirstOrDefault().PrimaryContactId?.Id); // report what is there now.

            var accountUpdateTarget = recs0.FirstOrDefault();
            accountUpdateTarget.PrimaryContactId = new EntityReference("contact", Guid.Parse("a7bf9a01-b056-e711-abaa-00155d701c02"));
            ctx.UpdateObject(accountUpdateTarget);
            ctx.SaveChanges(); // Update with new ER. 

           var recs1 = from acctQ in ctx.AccountSet
                       where acctQ.Id == Guid.Parse("1ece5cd4-ae56-e711-abaa-00155d701c02")
                       select acctQ;

            Trace.WriteLine(recs1.FirstOrDefault().PrimaryContactId?.Id);

This snippet will use a OrganizationServiceContext to look at the value of PrimaryContactId on the target account and report it's ID. Then it will update it to a new value ( Abbie Gardiner from he sample data in this case ) and requery it.

The requery will now report Abbie's ID vs whatever was there to begin with. this is because the ctx.UpdateObject marked the record as "dirty" and the ctx.SaveChanges committed the changes and cleared the record..

Now, if you do this:

            CrmServiceContext ctx = new CrmServiceContext(cli);
            var recs0 = from acctQ in ctx.AccountSet
                        where acctQ.Id == Guid.Parse("1ece5cd4-ae56-e711-abaa-00155d701c02")
                        select acctQ;
            Trace.WriteLine(recs0.FirstOrDefault().PrimaryContactId?.Id); // report current value. 

            // Update Account. 
            Account UpdateAccountPrimaryContact = new Account()
            {
                Id = Guid.Parse("1ece5cd4-ae56-e711-abaa-00155d701c02"),
                PrimaryContactId = new EntityReference("contact", Guid.Parse("a7bf9a01-b056-e711-abaa-00155d701c02"));
            };
            cli.Update(UpdateAccountPrimaryContact);

            var recs1 = from acctQ in ctx.AccountSet
                       where acctQ.Id == Guid.Parse("1ece5cd4-ae56-e711-abaa-00155d701c02")
                       select acctQ;

            Trace.WriteLine(recs1.FirstOrDefault().PrimaryContactId?.Id);

This will report the current value of the PrimaryContactId, then update it using .Update and requery... the difference this time will be the initial value reported for PrimaryContactId will be passed back because, as far as the the OrganizationServiceContext is concerned , nothing change and it already had that data.

To get the new "right value" back you would need to add ctx.ClearChanges(); after the .update command to force the OrganizationServiceContext to flush its local cache and reread.

Now, if you do not want to do that, you can also set the OrganizationServiceContext.MergeOption to NoTracking or OverwriteChanges. which should be used with caution. See : https://docs.microsoft.com/en-us/dotnet/api/microsoft.xrm.sdk.client.mergeoption?view=dynamics-general-ce-9 for more info.

UM001 commented 4 years ago

Thank you for your clear explanation. I experienced a very slow application so went looking into performance improvements. I saw that for every transaction the cds client is created. So I changed 'OrganizationServiceContext ' to singleton first:

` services.AddSingleton<IServiceContext, ServiceContext>();``

Great speed improvements. However, that works like a 'cache' as it injects the data to all other transactions during on the web server. So I changed that one to AddScoped. I also changed AddTransient to AddScoped for this one:

services.AddScoped<IOrganizationService, CdsServiceClient>(sp => sp.GetService()._cdsServiceClient.Clone());

It seems that constructing a cdsserviceclient is very resource intensive. I do not know exactly why. My servicecontext for D365FO is 40MB of dll. I do not exactly how to investigate that. I did improve performance with addscoped, which most likely will go wrong if in one web request multiple queries/ changes are run over the organizationservicecontext.

MattB-msft commented 4 years ago

Its that large because you created a strongly type class made of the full possibility of Dynamics. if you need to be dynamic, then you should use the late bound concepts of the system... if you want strongly typed object, you should scope that to only the objects you actually need.
Externally, there is a tool available in XrmToolbox ( https://www.xrmtoolbox.com/ ) that you can use that called the early bound generator.

The classes generated by this tool are fully compatible with the CdsServiceClient.