Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make CosmosClient singleton and add a public way to access it. #16051

Merged
merged 1 commit into from
Jun 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/EFCore.Cosmos/Extensions/CosmosDatabaseFacadeExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using JetBrains.Annotations;
using Microsoft.Azure.Cosmos;
using Microsoft.EntityFrameworkCore.Cosmos.Internal;
using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Utilities;
using Microsoft.Extensions.DependencyInjection;

// ReSharper disable once CheckNamespace
namespace Microsoft.EntityFrameworkCore
{
/// <summary>
/// Extension methods for the <see cref="DatabaseFacade" /> returned from <see cref="DbContext.Database" />
/// that can be used only with the Cosmos provider.
/// </summary>
public static class CosmosDatabaseFacadeExtensions
{
/// <summary>
/// Gets the underlying <see cref="CosmosClient" /> for this <see cref="DbContext" />.
/// </summary>
/// <param name="databaseFacade"> The <see cref="DatabaseFacade" /> for the context. </param>
/// <returns> The <see cref="CosmosClient" /> </returns>
public static CosmosClient GetCosmosClient([NotNull] this DatabaseFacade databaseFacade)
=> GetService<SingletonCosmosClientWrapper>(databaseFacade).Client;

private static TService GetService<TService>(IInfrastructure<IServiceProvider> databaseFacade)
{
Check.NotNull(databaseFacade, nameof(databaseFacade));

var service = databaseFacade.Instance.GetService<TService>();
if (service == null)
{
throw new InvalidOperationException(CosmosStrings.CosmosNotInUse);
}

return service;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,11 @@ public static IServiceCollection AddEntityFrameworkCosmos([NotNull] this IServic
.TryAdd<IQueryableMethodTranslatingExpressionVisitorFactory, CosmosQueryableMethodTranslatingExpressionVisitorFactory>()
.TryAdd<IShapedQueryCompilingExpressionVisitorFactory, CosmosShapedQueryCompilingExpressionVisitorFactory>()

.TryAdd<ISingletonOptions, ICosmosSingletonOptions>(p => p.GetService<ICosmosSingletonOptions>())
.TryAddProviderSpecificServices(
b => b
.TryAddSingleton<ICosmosSingletonOptions, CosmosSingletonOptions>()
.TryAddSingleton<SingletonCosmosClientWrapper, SingletonCosmosClientWrapper>()
.TryAddScoped<CosmosClientWrapper, CosmosClientWrapper>()
.TryAddScoped<ISqlGeneratorFactory, CosmosSqlGeneratorFactory>()
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Utilities;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.EntityFrameworkCore.Cosmos.Infrastructure.Internal
Expand All @@ -15,10 +17,11 @@ public class CosmosDbOptionsExtension : IDbContextOptionsExtension
{
private string _serviceEndPoint;
private string _authKeyOrResourceToken;
private string _region;
private string _databaseName;
private Func<ExecutionStrategyDependencies, IExecutionStrategy> _executionStrategyFactory;
private string _logFragment;
private string _region;
private long? _serviceProviderHash;

public CosmosDbOptionsExtension()
{
Expand Down Expand Up @@ -108,18 +111,43 @@ public bool ApplyServices(IServiceCollection services)
return true;
}

public long GetServiceProviderHashCode()
/// <summary>
/// Returns a hash code created from any options that would cause a new <see cref="IServiceProvider" />
/// to be needed.
/// </summary>
/// <returns> A hash over options that require a new service provider when changed. </returns>
public virtual long GetServiceProviderHashCode()
{
return 0;
if (_serviceProviderHash == null)
{
var hashCode = _serviceEndPoint.GetHashCode();
hashCode = (hashCode * 397) ^ _authKeyOrResourceToken.GetHashCode();
hashCode = (hashCode * 397) ^ (_region?.GetHashCode() ?? 0);

_serviceProviderHash = hashCode;
}

return _serviceProviderHash.Value;
}

public void Validate(IDbContextOptions options)
/// <summary>
/// Populates a dictionary of information that may change between uses of the
/// extension such that it can be compared to a previous configuration for
/// this option and differences can be logged. The dictionary key prefix
/// <c>"Cosmos:"</c> is used.
/// </summary>
/// <param name="debugInfo"> The dictionary to populate. </param>
public virtual void PopulateDebugInfo(IDictionary<string, string> debugInfo)
{
Check.NotNull(debugInfo, nameof(debugInfo));

debugInfo["Cosmos:" + nameof(ServiceEndPoint)] = _serviceEndPoint.GetHashCode().ToString(CultureInfo.InvariantCulture);
debugInfo["Cosmos:" + nameof(AuthKeyOrResourceToken)] = _authKeyOrResourceToken.GetHashCode().ToString(CultureInfo.InvariantCulture);
debugInfo["Cosmos:" + nameof(CosmosDbContextOptionsBuilder.Region)] = (_region?.GetHashCode() ?? 0).ToString(CultureInfo.InvariantCulture);
}

public virtual void PopulateDebugInfo(IDictionary<string, string> debugInfo)
public void Validate(IDbContextOptions options)
{
debugInfo["Cosmos"] = "1";
}

public string LogFragment
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.EntityFrameworkCore.Cosmos.Infrastructure.Internal
{
/// <summary>
/// <para>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </para>
/// <para>
/// The service lifetime is <see cref="ServiceLifetime.Singleton"/>. This means a single instance
/// is used by many <see cref="DbContext"/> instances. The implementation must be thread-safe.
/// This service cannot depend on services registered as <see cref="ServiceLifetime.Scoped"/>.
/// </para>
/// </summary>
public class CosmosSingletonOptions : ICosmosSingletonOptions
{
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public string ServiceEndPoint { get; private set; }

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public string AuthKeyOrResourceToken { get; private set; }

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public string Region { get; private set; }

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual void Initialize(IDbContextOptions options)
{
var cosmosOptions = options.FindExtension<CosmosDbOptionsExtension>();
if (cosmosOptions != null)
{
ServiceEndPoint = cosmosOptions.ServiceEndPoint;
AuthKeyOrResourceToken = cosmosOptions.AuthKeyOrResourceToken;
Region = cosmosOptions.Region;
}
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual void Validate(IDbContextOptions options)
{
var inMemoryOptions = options.FindExtension<CosmosDbOptionsExtension>();

if (inMemoryOptions != null
&& (ServiceEndPoint != inMemoryOptions.ServiceEndPoint
|| AuthKeyOrResourceToken != inMemoryOptions.AuthKeyOrResourceToken
|| Region != inMemoryOptions.Region))
{
throw new InvalidOperationException(
CoreStrings.SingletonOptionChanged(
nameof(CosmosDbContextOptionsExtensions.UseCosmos),
nameof(DbContextOptionsBuilder.UseInternalServiceProvider)));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.EntityFrameworkCore.Cosmos.Infrastructure.Internal
{
/// <summary>
/// <para>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </para>
/// <para>
/// The service lifetime is <see cref="ServiceLifetime.Singleton"/> and multiple registrations
/// are allowed. This means a single instance of each service is used by many <see cref="DbContext"/>
/// instances. The implementation must be thread-safe.
/// This service cannot depend on services registered as <see cref="ServiceLifetime.Scoped"/>.
/// </para>
/// </summary>
public interface ICosmosSingletonOptions : ISingletonOptions
{
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
string ServiceEndPoint { get; }

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
string AuthKeyOrResourceToken { get; }

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
string Region { get; }
}
}
6 changes: 6 additions & 0 deletions src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/EFCore.Cosmos/Properties/CosmosStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="CosmosNotInUse" xml:space="preserve">
<value>Cosmos-specific methods can only be used when the context is using the Cosmos provider.</value>
</data>
<data name="OrphanedNestedDocument" xml:space="preserve">
<value>The entity of type '{entityType}' is mapped as a part of the document mapped to '{missingEntityType}', but there is no tracked entity of this type with the corresponding key value. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the key values.</value>
</data>
Expand Down
54 changes: 22 additions & 32 deletions src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,65 +15,57 @@
using Microsoft.EntityFrameworkCore.Cosmos.Infrastructure.Internal;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal
{
public class CosmosClientWrapper : IDisposable
/// <summary>
/// <para>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </para>
/// <para>
/// The service lifetime is <see cref="ServiceLifetime.Scoped"/>. This means that each
/// <see cref="DbContext"/> instance will use its own instance of this service.
/// The implementation may depend on other services registered with any lifetime.
/// The implementation does not need to be thread-safe.
/// </para>
/// </summary>
public class CosmosClientWrapper
{
public static readonly JsonSerializer Serializer = new JsonSerializer();

private readonly SingletonCosmosClientWrapper _singletonWrapper;
private readonly string _databaseId;
private readonly string _endPoint;
private readonly string _authKey;
private CosmosClient _client;
private readonly IExecutionStrategyFactory _executionStrategyFactory;
private readonly IDiagnosticsLogger<DbLoggerCategory.Database.Command> _commandLogger;

private static readonly string _userAgent = " Microsoft.EntityFrameworkCore.Cosmos/" + ProductInfo.GetVersion();
public static readonly JsonSerializer Serializer = new JsonSerializer();
private readonly string _region;

static CosmosClientWrapper()
{
Serializer.Converters.Add(new ByteArrayConverter());
Serializer.DateFormatHandling = DateFormatHandling.IsoDateFormat;
}

public CosmosClientWrapper(
[NotNull] SingletonCosmosClientWrapper singletonWrapper,
[NotNull] IDbContextOptions dbContextOptions,
[NotNull] IExecutionStrategyFactory executionStrategyFactory,
[NotNull] IDiagnosticsLogger<DbLoggerCategory.Database.Command> commandLogger)
{
var options = dbContextOptions.FindExtension<CosmosDbOptionsExtension>();

_singletonWrapper = singletonWrapper;
_databaseId = options.DatabaseName;
_endPoint = options.ServiceEndPoint;
_authKey = options.AuthKeyOrResourceToken;
_region = options.Region;
_executionStrategyFactory = executionStrategyFactory;
_commandLogger = commandLogger;
}

private CosmosClient Client =>
_client ??= new CosmosClient(BuildCosmosConfiguration());

private CosmosClientOptions BuildCosmosConfiguration()
{
var configuration = new CosmosClientOptions(_endPoint, _authKey)
{
ApplicationName = _userAgent,
ConnectionMode = ConnectionMode.Direct,
};

if (_region != null)
{
configuration.ApplicationRegion = _region;
}

return configuration;
}
private CosmosClient Client => _singletonWrapper.Client;

public bool CreateDatabaseIfNotExists()
=> _executionStrategyFactory.Create().Execute(
Expand Down Expand Up @@ -528,7 +520,5 @@ public ValueTask DisposeAsync()
}
}
}

public void Dispose() => _client?.Dispose();
}
}
Loading