scott-mcdonald / JsonApiFramework

JsonApiFramework is a fast, extensible, and portable .NET framework for the reading and writing of JSON API documents. Currently working on ApiFramework 1.0 which is a new framework that supports the many enhancements documented in the 2.0 milestone of this project while being media type agnostic but will support media types like {json:api} and GraphQL for serialization/deserialization purposes.
Other
96 stars 11 forks source link

Add the persistence of service models, for possible caching and other useful purposes... #6

Open scott-mcdonald opened 8 years ago

scott-mcdonald commented 8 years ago

Add caching of service models as they are static. Before building a new service model by user configuration and conventions, check if the service model is in cache and use it if it is. Otherwise build service model and cache it for future cache hits.

yurii-pelekh commented 2 months ago

Hi @scott-mcdonald. If this functionality is not yet implemented, could you please give some hints on how (and where) it should be implemented? I think I'm ready to implement it and contribute to the repository because we use this library in our project and face memory leaks (our DocumentContext contains a lot of configurations). Thanks.

scott-mcdonald commented 2 months ago

@yurii-pelekh So the name of this issue is misleading now that I think about this; what I meant was the persistence of the service models. This would be useful for persisting the computation cost of creating a service model, persisting the service model as a file, and subsequent "executions" would look for the file and if it exists, load the service model and use it immediately without rebuilding it. That is not that hard, but not simple either and to date have not added this feature...

Therefore I am going to change the name of this issue respectively...

But I don't think that is what you asking, It sounds like you are stating over time in some process you are creating multiple JsonApiFramework DocumentContext objects and they are leaking memory?

I do find that hard to believe if this is what you are stating. Typically whenever I am building a json:api document with the JsonApiFramework I following the same "pattern" in an abstract sense:

using var documentContext = Create DocumentContext here...

Build document with Fluent API or some higher level framework... The JsonApiFramework DocumentContext is automatically disposed at the end of the block because of the using var and because DocumentContext implements IDisposable interface for proper clean up...

You will have to give me more information, specifically on how you determined DocumentContext is causing memory leaks?

yurii-pelekh commented 2 months ago

Hi @scott-mcdonald! Thanks for the reply.

Here is an example of my DocumentContext:

public class OrderManagementDocumentContext : DocumentContext
{
    protected override void OnServiceModelCreating(IServiceModelBuilder serviceModelBuilder)
    {
        serviceModelBuilder.Configurations.Add(new OrderTypeConfiguration());
        serviceModelBuilder.Configurations.Add(new OrderConfiguration());
        serviceModelBuilder.Configurations.Add(new NumberRangeConfiguration());
        // And hundreds of other configurations.
    }
}

As you have noticed, my DocumentContext contains hundreds of Configuration objects. For all subsequent HTTP requests, all those Configuration objects are created from scratch. I noticed that there are a lot of memory allocations during request handling, and the root cause is in the DocumentContext. Because of that, GC (garbage collection) executes more often than expected.

So my idea was not to create each Configuration object for each new subsequent request but to make them static so that the same objects are used in the future (thus reducing memory allocations and GC time).

Could you please tell me, what may be wrong with my implementation of DocumentContext (I guess we should split it into multiple smaller ones or so), and whether it is possible to make Configurations static so they are not allocated for each new request? Thank you.

yurii-pelekh commented 2 months ago

Well, actually it should be easy, something like this:

public class OrderManagementDocumentContext : DocumentContext
{
    private static readonly OrderTypeConfiguration OrderTypeConfiguration = new();
    private static readonly OrderConfiguration OrderConfiguration = new();
    private static readonly NumberRangeConfiguration NumberRangeConfiguration = new();

    protected override void OnServiceModelCreating(IServiceModelBuilder serviceModelBuilder)
    {
        serviceModelBuilder.Configurations.Add(OrderTypeConfiguration);
        serviceModelBuilder.Configurations.Add(OrderConfiguration);
        serviceModelBuilder.Configurations.Add(NumberRangeConfiguration);
        // And hundreds of other configurations.
    }
}
scott-mcdonald commented 2 months ago

@yurii-pelekh

Now I have clarity on exactly what you are talking about. Yes adding the configurations directly in the OnModelCreating for a large number of configurations probably would not scale...

First some groundwork: JsonApiFramework represents a framework for just serialization/deserialization of json:api documents. I have built higher-level frameworks on top of JsonApiFramework, FYI...

I personally use Microsoft DI from the Microsoft.Extensions.DependencyInjection nuget package. You may use DI or new but just wanted to mention the DI part.

I will go ahead and give you at a high level how I abstract away the configuration of a service model and the creation of JsonApiFramework document context objects so it scales efficiently in some higher level frameworks that I have created. Note the following is incomplete and I'll leave it as an exercise if you want to even use it or go with something simpler like you have above. There is no correct answer, just sharing from one peer to another...

First I have the following abstractions:

/// <summary>Represents a registry of JSON API framework components needed when working
/// with the JSON API framework.</summary>
public interface IApiJsonApiFrameworkRegistry
{
    #region Methods
    /// <summary>Gets the JSON API framework service model, i.e. the API schema.</summary>
    IServiceModel GetServiceModel();

    /// <summary>Gets the JSON API framework options for creating a JSON API document context
    /// which is used for serializing/deserializing {json:api} documents from/to POCO objects.</summary>
    IDocumentContextOptions GetDocumentContextOptions();
    #endregion
}
/// <summary>Represents a factory to create a JSON API framework service model.</summary>
public interface IApiServiceModelFactory
{
    #region Factory Methods
    /// <summary>Creates a JSON API framework service model.</summary>
    IServiceModel CreateServiceModel();
    #endregion
}

Some simple implementations....

internal class ApiJsonApiFrameworkRegistry : IApiJsonApiFrameworkRegistry
{
    #region Properties
    private IServiceModel ApiServiceModel { get; }
    private IDocumentContextOptions ApiDocumentContextOptions { get; }
    #endregion

    #region Constructors
    public ApiJsonApiFrameworkRegistry(IApiServiceModelFactory apiServiceModelFactory)
    {
        var apiServiceModel = apiServiceModelFactory.CreateServiceModel();
        this.ApiServiceModel = apiServiceModel;

        var apiDocumentContextOptions = CreateDocumentContextOptions(apiServiceModel);
        this.ApiDocumentContextOptions = apiDocumentContextOptions;
    }
    #endregion

    #region IApiJsonApiFrameworkRegistry Implementation
    public IServiceModel GetServiceModel()
    {
        return this.ApiServiceModel;
    }

    public IDocumentContextOptions GetDocumentContextOptions()
    {
        return this.ApiDocumentContextOptions;
    }
    #endregion

    #region Methods
    private static DocumentContextOptions<DocumentContext> CreateDocumentContextOptions(
        IServiceModel apiServiceModel)
    {
        var apiOptions = new DocumentContextOptions<DocumentContext>();
        var apiOptionsBuilder = new DocumentContextOptionsBuilder(apiOptions);

        apiOptionsBuilder.UseServiceModel(apiServiceModel);

        // Use apiOptionsBuilder to do higher level things here if needed...

        return apiOptions;
    }
    #endregion
}
internal class ApiServiceModelFactory(IEnumerable<IComplexTypeBuilder> apiComplexTypeBuilders,
                                      IEnumerable<IResourceTypeBuilder> apiResourceTypeBuilders)
    : IApiServiceModelFactory
{
    #region Properties
    private IEnumerable<IComplexTypeBuilder> ApiComplexTypeBuilders { get; } = apiComplexTypeBuilders;
    private IEnumerable<IResourceTypeBuilder> ApiResourceTypeBuilders { get; } = apiResourceTypeBuilders;
    #endregion

    #region IApiServiceModelFactory Implementation
    public IServiceModel CreateServiceModel()
    {
        var apiServiceModelBuilder = new ServiceModelBuilder();

        // Add complex/resource type configurations to the service model builder.
        AddComplexTypeConfigurations(apiServiceModelBuilder);
        AddResourceTypeConfigurations(apiServiceModelBuilder);

        // Create service model based on conventions and configurations.
        var apiConventions = CreateConventions();
        var apiServiceModel = apiServiceModelBuilder.Create(apiConventions);

        return apiServiceModel;
    }
    #endregion

    #region Implementation Methods
    private void AddComplexTypeConfigurations(IServiceModelBuilder apiServiceModelBuilder)
    {
        foreach (var apiComplexTypeBuilder in this.ApiComplexTypeBuilders)
        {
            apiServiceModelBuilder.Configurations.Add(apiComplexTypeBuilder);
        }
    }

    private void AddResourceTypeConfigurations(IServiceModelBuilder apiServiceModelBuilder)
    {
        foreach (var apiResourceTypeBuilder in this.ApiResourceTypeBuilders)
        {
            apiServiceModelBuilder.Configurations.Add(apiResourceTypeBuilder);
        }
    }

    private static IConventions CreateConventions()
    {
        var apiConventionsBuilder = new ConventionsBuilder();

        // Use JSON API standard member naming convention for JSON API resource attributes.
        // For example, FirstName in POCO becomes "firstName" as a JSON API attribute.
        apiConventionsBuilder.ApiAttributeNamingConventions()
                             .AddCamelCaseNamingConvention();

        // Use JSON API standard member naming and singular conventions of the POCO type
        // name as the JSON API type name.
        // For example, Article POCO type becomes "article" as the JSON API type.
        apiConventionsBuilder.ApiTypeNamingConventions()
                             .AddSingularNamingConvention()
                             .AddCamelCaseNamingConvention();

        // Discover all public properties as JSON API resource attributes.
        // For example, FirstName property in POCO becomes an attribute of a JSON API resource.
        apiConventionsBuilder.ResourceTypeConventions()
                             .AddPropertyDiscoveryConvention();

        apiConventionsBuilder.ComplexTypeConventions()
                             .AddPropertyDiscoveryConvention();

        var conventions = apiConventionsBuilder.Create();
        return conventions;
    }
    #endregion
}

From here I use DI, to create singletons for IJsonApiFrameworkRegistry -> JsonApiFrameworkRegistry and ISerivceModelFactory -> ServiceModelFactory respectively... I also use DI to inject the resource/complex type configurations as singletons into the service model factory...

Now in application code where you need to build json:api documents, I do something along the lines of...

    public static DocumentContext CreateJsonApiDocumentContext(IJsonApiFrameworkRegistry jsonApiFrameworkRegistry)
    {
        var jsonApiDocumentContextOptions = jsonApiFrameworkRegistry.GetDocumentContextOptions();
        var jsonApiDocumentContext = new DocumentContext(jsonApiDocumentContextOptions);
        return jsonApiDocumentContext;
    }

So this is roughly what I am currently doing, this could also be improved, maybe... Use this as you see fit but note I left some parts out to keep it as simple as possible...

Good luck... Scott

yurii-pelekh commented 2 months ago

@scott-mcdonald, thank you very much for this detailed answer! I appreciate it. I'll follow the tips you mentioned, and it should help. Thanks!