diff --git a/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs b/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs index 8e4c788ad04..dd2cc1cdb8f 100644 --- a/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs @@ -340,11 +340,14 @@ public static IQueryable SqlQueryRaw( Check.NotNull(parameters, nameof(parameters)); var facadeDependencies = GetFacadeDependencies(databaseFacade); - - return facadeDependencies.QueryProvider - .CreateQuery( - new SqlQueryRootExpression( - facadeDependencies.QueryProvider, typeof(TResult), sql, Expression.Constant(parameters))); + var queryProvider = facadeDependencies.QueryProvider; + var argumentsExpression = Expression.Constant(parameters); + + return queryProvider.CreateQuery( + facadeDependencies.TypeMappingSource.FindMapping(typeof(TResult)) != null + ? new SqlQueryRootExpression(queryProvider, typeof(TResult), sql, argumentsExpression) + : new FromSqlQueryRootExpression( + queryProvider, facadeDependencies.AdHocMapper.GetOrAddEntityType(typeof(TResult)), sql, argumentsExpression)); } /// @@ -380,17 +383,7 @@ public static IQueryable SqlQueryRaw( public static IQueryable SqlQuery( this DatabaseFacade databaseFacade, [NotParameterized] FormattableString sql) - { - Check.NotNull(sql, nameof(sql)); - Check.NotNull(sql.Format, nameof(sql.Format)); - - var facadeDependencies = GetFacadeDependencies(databaseFacade); - - return facadeDependencies.QueryProvider - .CreateQuery( - new SqlQueryRootExpression( - facadeDependencies.QueryProvider, typeof(TResult), sql.Format, Expression.Constant(sql.GetArguments()))); - } + => SqlQueryRaw(databaseFacade, sql.Format, sql.GetArguments()!); /// /// Executes the given SQL against the database and returns the number of rows affected. @@ -654,7 +647,7 @@ public static DbConnection GetDbConnection(this DatabaseFacade databaseFacade) /// /// If , then EF will take ownership of the connection and will /// dispose it in the same way it would dispose a connection created by EF. If , then the caller still - /// owns the connection and is responsible for its disposal. The default value is . + /// owns the connection and is responsible for its disposal. The default value is . /// public static void SetDbConnection(this DatabaseFacade databaseFacade, DbConnection? connection, bool contextOwnsConnection = false) => GetFacadeDependencies(databaseFacade).RelationalConnection.SetDbConnection(connection, contextOwnsConnection); diff --git a/src/EFCore.Relational/Storage/Internal/RelationalDatabaseFacadeDependencies.cs b/src/EFCore.Relational/Storage/Internal/RelationalDatabaseFacadeDependencies.cs index b24964ee677..fe5fa34ee6d 100644 --- a/src/EFCore.Relational/Storage/Internal/RelationalDatabaseFacadeDependencies.cs +++ b/src/EFCore.Relational/Storage/Internal/RelationalDatabaseFacadeDependencies.cs @@ -28,7 +28,9 @@ public RelationalDatabaseFacadeDependencies( IRelationalConnection relationalConnection, IRawSqlCommandBuilder rawSqlCommandBuilder, ICoreSingletonOptions coreOptions, - IAsyncQueryProvider queryProvider) + IAsyncQueryProvider queryProvider, + IAdHocMapper adHocMapper, + IRelationalTypeMappingSource relationalTypeMappingSource) { TransactionManager = transactionManager; DatabaseCreator = databaseCreator; @@ -41,6 +43,8 @@ public RelationalDatabaseFacadeDependencies( RawSqlCommandBuilder = rawSqlCommandBuilder; CoreOptions = coreOptions; QueryProvider = queryProvider; + AdHocMapper = adHocMapper; + TypeMappingSource = relationalTypeMappingSource; } /// @@ -49,7 +53,7 @@ public RelationalDatabaseFacadeDependencies( /// 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. /// - public virtual IDbContextTransactionManager TransactionManager { get; init; } + public virtual IDbContextTransactionManager TransactionManager { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -57,7 +61,7 @@ public RelationalDatabaseFacadeDependencies( /// 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. /// - public virtual IDatabaseCreator DatabaseCreator { get; init; } + public virtual IDatabaseCreator DatabaseCreator { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -65,7 +69,7 @@ public RelationalDatabaseFacadeDependencies( /// 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. /// - public virtual IExecutionStrategy ExecutionStrategy { get; init; } + public virtual IExecutionStrategy ExecutionStrategy { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -73,7 +77,7 @@ public RelationalDatabaseFacadeDependencies( /// 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. /// - public virtual IExecutionStrategyFactory ExecutionStrategyFactory { get; init; } + public virtual IExecutionStrategyFactory ExecutionStrategyFactory { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -81,7 +85,7 @@ public RelationalDatabaseFacadeDependencies( /// 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. /// - public virtual IEnumerable DatabaseProviders { get; init; } + public virtual IEnumerable DatabaseProviders { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -89,7 +93,7 @@ public RelationalDatabaseFacadeDependencies( /// 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. /// - public virtual IRelationalCommandDiagnosticsLogger CommandLogger { get; init; } + public virtual IRelationalCommandDiagnosticsLogger CommandLogger { get; } IDiagnosticsLogger IDatabaseFacadeDependencies.CommandLogger => CommandLogger; @@ -100,7 +104,7 @@ public RelationalDatabaseFacadeDependencies( /// 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. /// - public virtual IConcurrencyDetector ConcurrencyDetector { get; init; } + public virtual IConcurrencyDetector ConcurrencyDetector { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -108,7 +112,7 @@ public RelationalDatabaseFacadeDependencies( /// 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. /// - public virtual IRelationalConnection RelationalConnection { get; init; } + public virtual IRelationalConnection RelationalConnection { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -116,7 +120,7 @@ public RelationalDatabaseFacadeDependencies( /// 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. /// - public virtual IRawSqlCommandBuilder RawSqlCommandBuilder { get; init; } + public virtual IRawSqlCommandBuilder RawSqlCommandBuilder { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -124,7 +128,7 @@ public RelationalDatabaseFacadeDependencies( /// 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. /// - public virtual ICoreSingletonOptions CoreOptions { get; init; } + public virtual ICoreSingletonOptions CoreOptions { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -132,5 +136,21 @@ public RelationalDatabaseFacadeDependencies( /// 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. /// - public virtual IAsyncQueryProvider QueryProvider { get; init; } + public virtual IAsyncQueryProvider QueryProvider { get; } + + /// + /// 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. + /// + public virtual IAdHocMapper AdHocMapper { get; } + + /// + /// 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. + /// + public virtual ITypeMappingSource TypeMappingSource { get; } } diff --git a/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs b/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs index daaf29a6c2a..067d3d3aa75 100644 --- a/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs +++ b/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs @@ -122,6 +122,7 @@ public static readonly IDictionary CoreServices { typeof(IQueryTranslationPostprocessorFactory), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IShapedQueryCompilingExpressionVisitorFactory), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IDbContextLogger), new ServiceCharacteristics(ServiceLifetime.Scoped) }, + { typeof(IAdHocMapper), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(ILazyLoader), new ServiceCharacteristics(ServiceLifetime.Transient) }, { typeof(IParameterBindingFactory), new ServiceCharacteristics(ServiceLifetime.Singleton, multipleRegistrations: true) }, { typeof(ITypeMappingSourcePlugin), new ServiceCharacteristics(ServiceLifetime.Singleton, multipleRegistrations: true) }, @@ -301,6 +302,7 @@ public virtual EntityFrameworkServicesBuilder TryAddCoreServices() TryAdd(); TryAdd(); TryAdd(); + TryAdd(); TryAdd( p => p.GetService()?.FindExtension()?.DbContextLogger @@ -426,7 +428,7 @@ public virtual EntityFrameworkServicesBuilder TryAdd /// This builder, such that further calls can be chained. public virtual EntityFrameworkServicesBuilder TryAdd ( - Func factory) + Func factory) where TService : class where TImplementation : class, TService => TryAdd(typeof(TService), typeof(TImplementation), factory); diff --git a/src/EFCore/Infrastructure/ModelValidator.cs b/src/EFCore/Infrastructure/ModelValidator.cs index 57e6cc52d6c..0c31c05dd13 100644 --- a/src/EFCore/Infrastructure/ModelValidator.cs +++ b/src/EFCore/Infrastructure/ModelValidator.cs @@ -237,8 +237,11 @@ protected virtual void ValidatePropertyMapping( } throw new InvalidOperationException( - CoreStrings.NavigationNotAdded( - entityType.DisplayName(), clrProperty.Name, propertyType.ShortDisplayName())); + Equals(model.FindAnnotation(CoreAnnotationNames.AdHocModel)?.Value, true) + ? CoreStrings.NavigationNotAddedAdHoc( + entityType.DisplayName(), clrProperty.Name, propertyType.ShortDisplayName()) + : CoreStrings.NavigationNotAdded( + entityType.DisplayName(), clrProperty.Name, propertyType.ShortDisplayName())); } // ReSharper restore CheckForReferenceEqualityInstead.3 @@ -254,8 +257,11 @@ protected virtual void ValidatePropertyMapping( else { throw new InvalidOperationException( - CoreStrings.PropertyNotAdded( - entityType.DisplayName(), clrProperty.Name, propertyType.ShortDisplayName())); + Equals(model.FindAnnotation(CoreAnnotationNames.AdHocModel)?.Value, true) + ? CoreStrings.PropertyNotAddedAdHoc( + entityType.DisplayName(), clrProperty.Name, propertyType.ShortDisplayName()) + : CoreStrings.PropertyNotAdded( + entityType.DisplayName(), clrProperty.Name, propertyType.ShortDisplayName())); } } } diff --git a/src/EFCore/Internal/DbContextDependencies.cs b/src/EFCore/Internal/DbContextDependencies.cs index 46dd5994727..e3cf9768c6e 100644 --- a/src/EFCore/Internal/DbContextDependencies.cs +++ b/src/EFCore/Internal/DbContextDependencies.cs @@ -59,7 +59,7 @@ public DbContextDependencies( /// 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. /// - public IDbSetSource SetSource { get; init; } + public IDbSetSource SetSource { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -67,7 +67,7 @@ public DbContextDependencies( /// 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. /// - public IEntityFinderFactory EntityFinderFactory { get; init; } + public IEntityFinderFactory EntityFinderFactory { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -75,7 +75,7 @@ public DbContextDependencies( /// 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. /// - public IAsyncQueryProvider QueryProvider { get; init; } + public IAsyncQueryProvider QueryProvider { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -83,7 +83,7 @@ public DbContextDependencies( /// 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. /// - public IStateManager StateManager { get; init; } + public IStateManager StateManager { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -91,7 +91,7 @@ public DbContextDependencies( /// 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. /// - public IChangeDetector ChangeDetector { get; init; } + public IChangeDetector ChangeDetector { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -99,7 +99,7 @@ public DbContextDependencies( /// 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. /// - public IEntityGraphAttacher EntityGraphAttacher { get; init; } + public IEntityGraphAttacher EntityGraphAttacher { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -107,7 +107,7 @@ public DbContextDependencies( /// 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. /// - public IExceptionDetector ExceptionDetector { get; init; } + public IExceptionDetector ExceptionDetector { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -115,7 +115,7 @@ public DbContextDependencies( /// 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. /// - public IDiagnosticsLogger UpdateLogger { get; init; } + public IDiagnosticsLogger UpdateLogger { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -123,5 +123,5 @@ public DbContextDependencies( /// 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. /// - public IDiagnosticsLogger InfrastructureLogger { get; init; } + public IDiagnosticsLogger InfrastructureLogger { get; } } diff --git a/src/EFCore/Metadata/Conventions/AdHocModelInitializedConvention.cs b/src/EFCore/Metadata/Conventions/AdHocModelInitializedConvention.cs new file mode 100644 index 00000000000..87ba4d77566 --- /dev/null +++ b/src/EFCore/Metadata/Conventions/AdHocModelInitializedConvention.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; + +/// +/// Puts an attribute on the model indicating it is being used to build ad-hoc types. +/// +public class AdHocModelInitializedConvention : IModelInitializedConvention +{ + /// + /// Puts an attribute on the model indicating it is being used to build ad-hoc types. + /// + /// The builder for the model. + /// Additional information associated with convention execution. + public virtual void ProcessModelInitialized(IConventionModelBuilder modelBuilder, IConventionContext context) + => modelBuilder.HasAnnotation(CoreAnnotationNames.AdHocModel, true); +} diff --git a/src/EFCore/Metadata/IAdHocMapper.cs b/src/EFCore/Metadata/IAdHocMapper.cs new file mode 100644 index 00000000000..3f8256368b7 --- /dev/null +++ b/src/EFCore/Metadata/IAdHocMapper.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Metadata; + +/// +/// Creates ad-hoc mappings of CLR types to entity types after the model has been built. +/// +public interface IAdHocMapper +{ + /// + /// Gets the ad-hoc entity type mapped for the given CLR type, or creates the mapping and returns it if it does not exist. + /// + /// The type for which the entity type will be returned. + /// The ad-hoc entity type. + RuntimeEntityType GetOrAddEntityType(Type clrType); +} diff --git a/src/EFCore/Metadata/IReadOnlyModel.cs b/src/EFCore/Metadata/IReadOnlyModel.cs index 2621039a6f2..5da07813999 100644 --- a/src/EFCore/Metadata/IReadOnlyModel.cs +++ b/src/EFCore/Metadata/IReadOnlyModel.cs @@ -73,6 +73,15 @@ public interface IReadOnlyModel : IReadOnlyAnnotatable /// All entity types defined in the model. IEnumerable GetEntityTypes(); + /// + /// Gets all ad-hoc entity types defined in the model. + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// All entity types defined in the model. + IEnumerable GetAdHocEntityTypes(); + /// /// Gets the entity type with the given name. Returns if no entity type with the given name is found /// or the given CLR type is being used by shared type entity type @@ -231,6 +240,16 @@ string ToDebugString(MetadataDebugStringOptions options = MetadataDebugStringOpt builder.AppendLine().Append(entityType.ToDebugString(options, indent + 2)); } + var adHocEntityTypes = GetAdHocEntityTypes().ToList(); + if (adHocEntityTypes.Count > 0) + { + builder.AppendLine().Append(indentString + " ").Append("Ad-hoc entity types:"); + foreach (var entityType in adHocEntityTypes) + { + builder.AppendLine().Append(entityType.ToDebugString(options, indent + 4)); + } + } + if ((options & MetadataDebugStringOptions.IncludeAnnotations) != 0) { builder.Append(AnnotationsToDebugString(indent)); diff --git a/src/EFCore/Metadata/Internal/AdHocMapper.cs b/src/EFCore/Metadata/Internal/AdHocMapper.cs new file mode 100644 index 00000000000..235a6957cbe --- /dev/null +++ b/src/EFCore/Metadata/Internal/AdHocMapper.cs @@ -0,0 +1,94 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Metadata.Internal; + +/// +/// 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. +/// +public class AdHocMapper : IAdHocMapper +{ + private readonly IModel _model; + private readonly ModelCreationDependencies _modelCreationDependencies; + private ConventionSet? _conventionSet; + + /// + /// 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. + /// + public AdHocMapper( + IModel model, + ModelCreationDependencies modelCreationDependencies) + { + _model = model; + _modelCreationDependencies = modelCreationDependencies; + } + + private ConventionSet ConventionSet + { + get + { + if (_conventionSet == null) + { + _conventionSet = _modelCreationDependencies.ConventionSetBuilder.CreateConventionSet(); + _conventionSet.Remove(typeof(DbSetFindingConvention)); + _conventionSet.Remove(typeof(RelationshipDiscoveryConvention)); + _conventionSet.Remove(typeof(KeyDiscoveryConvention)); + _conventionSet.Remove(typeof(CascadeDeleteConvention)); + _conventionSet.Remove(typeof(ChangeTrackingStrategyConvention)); + _conventionSet.Remove(typeof(DeleteBehaviorAttributeConvention)); + _conventionSet.Remove(typeof(ForeignKeyAttributeConvention)); + _conventionSet.Remove(typeof(ForeignKeyIndexConvention)); + _conventionSet.Remove(typeof(ForeignKeyPropertyDiscoveryConvention)); + _conventionSet.Remove(typeof(IndexAttributeConvention)); + _conventionSet.Remove(typeof(KeyAttributeConvention)); + _conventionSet.Remove(typeof(KeylessEntityTypeAttributeConvention)); + _conventionSet.Remove(typeof(ManyToManyJoinEntityTypeConvention)); + _conventionSet.Remove(typeof(RequiredNavigationAttributeConvention)); + _conventionSet.Remove(typeof(NavigationBackingFieldAttributeConvention)); + _conventionSet.Remove(typeof(InversePropertyAttributeConvention)); + _conventionSet.Remove(typeof(NavigationEagerLoadingConvention)); + _conventionSet.Remove(typeof(NonNullableNavigationConvention)); + _conventionSet.Remove(typeof(NotMappedEntityTypeAttributeConvention)); + _conventionSet.Remove(typeof(OwnedEntityTypeAttributeConvention)); + _conventionSet.Remove(typeof(QueryFilterRewritingConvention)); + _conventionSet.Remove(typeof(ServicePropertyDiscoveryConvention)); + _conventionSet.Remove(typeof(ValueGenerationConvention)); + _conventionSet.Remove(typeof(BaseTypeDiscoveryConvention)); + _conventionSet.Remove(typeof(DiscriminatorConvention)); + _conventionSet.Add(new AdHocModelInitializedConvention()); + } + + return _conventionSet; + } + } + + /// + /// 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. + /// + public virtual RuntimeEntityType GetOrAddEntityType(Type clrType) + { + Check.DebugAssert(_model is RuntimeModel, "Ad-hoc entity types can only be used at runtime."); + + return ((RuntimeModel)_model).FindAdHocEntityType(clrType) ?? AddEntityType(clrType); + } + + private RuntimeEntityType AddEntityType(Type clrType) + { + var modelBuilder = new ModelBuilder(ConventionSet, _modelCreationDependencies.ModelDependencies); + modelBuilder.Entity(clrType).HasNoKey(); + var finalizedModel = modelBuilder.FinalizeModel(); + var runtimeModel = _modelCreationDependencies.ModelRuntimeInitializer.Initialize( + finalizedModel, designTime: false, _modelCreationDependencies.ValidationLogger); + + return ((RuntimeModel)_model).GetOrAddAdHocEntityType((RuntimeEntityType)runtimeModel.FindEntityType(clrType)!); + } +} diff --git a/src/EFCore/Metadata/Internal/CoreAnnotationNames.cs b/src/EFCore/Metadata/Internal/CoreAnnotationNames.cs index 8b399f698cb..dc238cab330 100644 --- a/src/EFCore/Metadata/Internal/CoreAnnotationNames.cs +++ b/src/EFCore/Metadata/Internal/CoreAnnotationNames.cs @@ -300,6 +300,14 @@ public static class CoreAnnotationNames /// public const string FullChangeTrackingNotificationsRequired = "ModelValidator.FullChangeTrackingNotificationsRequired"; + /// + /// 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. + /// + public const string AdHocModel = "AdHocModel"; + /// /// 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 @@ -345,6 +353,7 @@ public static class CoreAnnotationNames AmbiguousNavigations, AmbiguousField, DuplicateServiceProperties, - FullChangeTrackingNotificationsRequired + FullChangeTrackingNotificationsRequired, + AdHocModel }; } diff --git a/src/EFCore/Metadata/Internal/Model.cs b/src/EFCore/Metadata/Internal/Model.cs index e05b93e0236..8e0c11ba2ce 100644 --- a/src/EFCore/Metadata/Internal/Model.cs +++ b/src/EFCore/Metadata/Internal/Model.cs @@ -1235,6 +1235,16 @@ void IMutableModel.SetChangeTrackingStrategy(ChangeTrackingStrategy? changeTrack IEnumerable IReadOnlyModel.GetEntityTypes() => GetEntityTypes(); + /// + /// 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. + /// + [DebuggerStepThrough] + IEnumerable IReadOnlyModel.GetAdHocEntityTypes() + => Enumerable.Empty(); + /// /// 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 diff --git a/src/EFCore/Metadata/RuntimeEntityType.cs b/src/EFCore/Metadata/RuntimeEntityType.cs index fe19389f203..d155b6591da 100644 --- a/src/EFCore/Metadata/RuntimeEntityType.cs +++ b/src/EFCore/Metadata/RuntimeEntityType.cs @@ -43,8 +43,10 @@ private readonly SortedDictionary _triggers private RuntimeKey? _primaryKey; private readonly bool _hasSharedClrType; + [DynamicallyAccessedMembers(IEntityType.DynamicallyAccessedMemberTypes)] private readonly Type _clrType; + private readonly RuntimeEntityType? _baseType; private readonly SortedSet _directlyDerivedTypes = new(EntityTypeFullNameComparer.Instance); private readonly ChangeTrackingStrategy _changeTrackingStrategy; @@ -113,7 +115,14 @@ public RuntimeEntityType( /// /// Gets the model that this type belongs to. /// - public virtual RuntimeModel Model { [DebuggerStepThrough] get; } + public virtual RuntimeModel Model { [DebuggerStepThrough] get; private set; } + + /// + /// Re-parents the this entity type to the given model. + /// + /// The new parent model. + public virtual void Reparent(RuntimeModel model) + => Model = model; private IEnumerable GetDerivedTypes() { @@ -378,7 +387,8 @@ public virtual RuntimeNavigation AddNavigation( bool eagerLoaded = false, bool lazyLoadingEnabled = true) { - var navigation = new RuntimeNavigation(name, clrType, propertyInfo, fieldInfo, foreignKey, propertyAccessMode, eagerLoaded, lazyLoadingEnabled); + var navigation = new RuntimeNavigation( + name, clrType, propertyInfo, fieldInfo, foreignKey, propertyAccessMode, eagerLoaded, lazyLoadingEnabled); _navigations.Add(name, navigation); diff --git a/src/EFCore/Metadata/RuntimeModel.cs b/src/EFCore/Metadata/RuntimeModel.cs index 9ca509cecb1..bcb6599d7f4 100644 --- a/src/EFCore/Metadata/RuntimeModel.cs +++ b/src/EFCore/Metadata/RuntimeModel.cs @@ -38,6 +38,7 @@ public class RuntimeModel : AnnotatableBase, IRuntimeModel private readonly ConcurrentDictionary _indexerPropertyInfoMap = new(); private readonly ConcurrentDictionary _clrTypeNameMap = new(); + private readonly ConcurrentDictionary _adHocEntityTypes = new(); /// /// Sets a value indicating whether should be called. @@ -102,6 +103,16 @@ public virtual RuntimeEntityType AddEntityType( return entityType; } + /// + /// Adds an ad-hoc entity type to the model. + /// + /// The entity type. + public virtual RuntimeEntityType GetOrAddAdHocEntityType(RuntimeEntityType entityType) + { + entityType.Reparent(this); + return _adHocEntityTypes.GetOrAdd(((IReadOnlyTypeBase)entityType).ClrType, entityType); + } + /// /// Gets the entity type with the given name. Returns if no entity type with the given name is found /// or the given CLR type is being used by shared type entity type @@ -114,6 +125,17 @@ public virtual RuntimeEntityType AddEntityType( ? entityType : null; + /// + /// Gets the entity type with the given name. Returns if no entity type with the given name has been + /// mapped as an ad-hoc type. + /// + /// The CLR type of the entity type to find. + /// The entity type, or if none is found. + public virtual RuntimeEntityType? FindAdHocEntityType(Type clrType) + => _adHocEntityTypes.TryGetValue(clrType, out var entityType) + ? entityType + : null; + private RuntimeEntityType? FindEntityType(Type type) => FindEntityType(GetDisplayName(type)); @@ -273,6 +295,11 @@ bool IModel.IsIndexerMethod(MethodInfo methodInfo) IEnumerable IReadOnlyModel.GetEntityTypes() => _entityTypes.Values; + /// + [DebuggerStepThrough] + IEnumerable IReadOnlyModel.GetAdHocEntityTypes() + => _adHocEntityTypes.Values; + /// [DebuggerStepThrough] IEnumerable IModel.GetEntityTypes() diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index a0190a970c7..5e977aed92b 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -1709,6 +1709,14 @@ public static string NavigationNotAdded(object? entityType, object? navigation, GetString("NavigationNotAdded", nameof(entityType), nameof(navigation), nameof(propertyType)), entityType, navigation, propertyType); + /// + /// The property '{entityType}.{navigation}' of type '{propertyType}' appears to be a navigation to another entity type. Navigations are not supported when using 'SqlQuery". Either include this type in the model and use 'FromSql' for the query, or ignore this property using the '[NotMapped]' attribute. + /// + public static string NavigationNotAddedAdHoc(object? entityType, object? navigation, object? propertyType) + => string.Format( + GetString("NavigationNotAddedAdHoc", nameof(entityType), nameof(navigation), nameof(propertyType)), + entityType, navigation, propertyType); + /// /// The navigation '{navigation}' cannot be added to the entity type '{entityType}' because its CLR type '{clrType}' does not match the expected CLR type '{targetType}'. /// @@ -2165,6 +2173,14 @@ public static string PropertyNotAdded(object? entityType, object? property, obje GetString("PropertyNotAdded", nameof(entityType), nameof(property), nameof(propertyType)), entityType, property, propertyType); + /// + /// The property '{entityType}.{property}' could not be mapped because it is of type '{propertyType}', which is not a supported primitive type or a valid entity type. The property can be ignored using the '[NotMapped]' attribute. + /// + public static string PropertyNotAddedAdHoc(object? entityType, object? property, object? propertyType) + => string.Format( + GetString("PropertyNotAddedAdHoc", nameof(entityType), nameof(property), nameof(propertyType)), + entityType, property, propertyType); + /// /// The property '{1_entityType}.{0_property}' could not be found. Ensure that the property exists and has been included in the model. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 1df900824cb..f3b9dc892cb 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1067,6 +1067,9 @@ Unable to determine the relationship represented by navigation '{entityType}.{navigation}' of type '{propertyType}'. Either manually configure the relationship, or ignore this property using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'. + + The property '{entityType}.{navigation}' of type '{propertyType}' appears to be a navigation to another entity type. Navigations are not supported when using 'SqlQuery". Either include this type in the model and use 'FromSql' for the query, or ignore this property using the '[NotMapped]' attribute. + The navigation '{navigation}' cannot be added to the entity type '{entityType}' because its CLR type '{clrType}' does not match the expected CLR type '{targetType}'. @@ -1244,6 +1247,9 @@ The property '{entityType}.{property}' could not be mapped because it is of type '{propertyType}', which is not a supported primitive type or a valid entity type. Either explicitly map this property, or ignore it using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'. + + The property '{entityType}.{property}' could not be mapped because it is of type '{propertyType}', which is not a supported primitive type or a valid entity type. The property can be ignored using the '[NotMapped]' attribute. + The property '{1_entityType}.{0_property}' could not be found. Ensure that the property exists and has been included in the model. diff --git a/src/EFCore/Storage/IDatabaseFacadeDependencies.cs b/src/EFCore/Storage/IDatabaseFacadeDependencies.cs index a29318d7bd9..fa3fb92f077 100644 --- a/src/EFCore/Storage/IDatabaseFacadeDependencies.cs +++ b/src/EFCore/Storage/IDatabaseFacadeDependencies.cs @@ -70,4 +70,14 @@ public interface IDatabaseFacadeDependencies /// The async query provider. /// IAsyncQueryProvider QueryProvider { get; } + + /// + /// The ad-hoc type mapper. + /// + IAdHocMapper AdHocMapper { get; } + + /// + /// The . + /// + ITypeMappingSource TypeMappingSource { get; } } diff --git a/src/EFCore/Storage/Internal/DatabaseFacadeDependencies.cs b/src/EFCore/Storage/Internal/DatabaseFacadeDependencies.cs index f59c8e84489..ba60fc00f26 100644 --- a/src/EFCore/Storage/Internal/DatabaseFacadeDependencies.cs +++ b/src/EFCore/Storage/Internal/DatabaseFacadeDependencies.cs @@ -26,7 +26,9 @@ public DatabaseFacadeDependencies( IDiagnosticsLogger commandLogger, IConcurrencyDetector concurrencyDetector, ICoreSingletonOptions coreOptions, - IAsyncQueryProvider queryProvider) + IAsyncQueryProvider queryProvider, + IAdHocMapper adHocMapper, + ITypeMappingSource typeMappingSource) { TransactionManager = transactionManager; DatabaseCreator = databaseCreator; @@ -37,6 +39,8 @@ public DatabaseFacadeDependencies( ConcurrencyDetector = concurrencyDetector; CoreOptions = coreOptions; QueryProvider = queryProvider; + AdHocMapper = adHocMapper; + TypeMappingSource = typeMappingSource; } /// @@ -45,7 +49,7 @@ public DatabaseFacadeDependencies( /// 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. /// - public virtual IDbContextTransactionManager TransactionManager { get; init; } + public virtual IDbContextTransactionManager TransactionManager { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -53,7 +57,7 @@ public DatabaseFacadeDependencies( /// 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. /// - public virtual IDatabaseCreator DatabaseCreator { get; init; } + public virtual IDatabaseCreator DatabaseCreator { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -61,7 +65,7 @@ public DatabaseFacadeDependencies( /// 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. /// - public virtual IExecutionStrategy ExecutionStrategy { get; init; } + public virtual IExecutionStrategy ExecutionStrategy { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -69,7 +73,7 @@ public DatabaseFacadeDependencies( /// 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. /// - public virtual IExecutionStrategyFactory ExecutionStrategyFactory { get; init; } + public virtual IExecutionStrategyFactory ExecutionStrategyFactory { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -77,7 +81,7 @@ public DatabaseFacadeDependencies( /// 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. /// - public virtual IEnumerable DatabaseProviders { get; init; } + public virtual IEnumerable DatabaseProviders { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -85,7 +89,7 @@ public DatabaseFacadeDependencies( /// 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. /// - public virtual IDiagnosticsLogger CommandLogger { get; init; } + public virtual IDiagnosticsLogger CommandLogger { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -93,7 +97,7 @@ public DatabaseFacadeDependencies( /// 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. /// - public virtual IConcurrencyDetector ConcurrencyDetector { get; init; } + public virtual IConcurrencyDetector ConcurrencyDetector { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -101,7 +105,7 @@ public DatabaseFacadeDependencies( /// 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. /// - public virtual ICoreSingletonOptions CoreOptions { get; init; } + public virtual ICoreSingletonOptions CoreOptions { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -109,5 +113,21 @@ public DatabaseFacadeDependencies( /// 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. /// - public virtual IAsyncQueryProvider QueryProvider { get; init; } + public virtual IAsyncQueryProvider QueryProvider { get; } + + /// + /// 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. + /// + public virtual IAdHocMapper AdHocMapper { get; } + + /// + /// 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. + /// + public virtual ITypeMappingSource TypeMappingSource { get; } } diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs index ebcb88ac907..c149a056798 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs @@ -43,6 +43,7 @@ public void Test_new_annotations_handled_for_entity_types() CoreAnnotationNames.EagerLoaded, CoreAnnotationNames.LazyLoadingEnabled, CoreAnnotationNames.DuplicateServiceProperties, + CoreAnnotationNames.AdHocModel, RelationalAnnotationNames.ColumnName, RelationalAnnotationNames.ColumnOrder, RelationalAnnotationNames.ColumnType, @@ -212,6 +213,7 @@ public void Test_new_annotations_handled_for_properties() CoreAnnotationNames.NavigationCandidates, CoreAnnotationNames.AmbiguousNavigations, CoreAnnotationNames.DuplicateServiceProperties, + CoreAnnotationNames.AdHocModel, RelationalAnnotationNames.TableName, RelationalAnnotationNames.IsTableExcludedFromMigrations, RelationalAnnotationNames.ViewName, diff --git a/test/EFCore.Relational.Specification.Tests/Query/SqlQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/SqlQueryTestBase.cs new file mode 100644 index 00000000000..912c7db0701 --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/Query/SqlQueryTestBase.cs @@ -0,0 +1,1318 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.TestModels.Northwind; + +// ReSharper disable FormatStringProblem +// ReSharper disable InconsistentNaming +// ReSharper disable ConvertToConstant.Local +// ReSharper disable AccessToDisposedClosure +namespace Microsoft.EntityFrameworkCore.Query; + +public abstract class SqlQueryTestBase : QueryTestBase + where TFixture : NorthwindQueryRelationalFixture, new() +{ + // ReSharper disable once StaticMemberInGenericType + private static readonly string _eol = Environment.NewLine; + + protected SqlQueryTestBase(TFixture fixture) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Bad_data_error_handling_invalid_cast_key(bool async) + { + using var context = CreateContext(); + var query = context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString( + @"SELECT [ProductName] AS [ProductID], [ProductID] AS [ProductName], [SupplierID], [UnitPrice], [UnitsInStock], [Discontinued] + FROM [Products]")); + + Assert.Equal( + CoreStrings.ErrorMaterializingPropertyInvalidCast("UnmappedProduct", "ProductID", typeof(int), typeof(string)), + (async + ? await Assert.ThrowsAsync(() => query.ToListAsync()) + : Assert.Throws(() => query.ToList())).Message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Bad_data_error_handling_invalid_cast(bool async) + { + using var context = CreateContext(); + var query = context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString( + @"SELECT [ProductID], [SupplierID] AS [UnitPrice], [ProductName], [SupplierID], [UnitsInStock], [Discontinued] + FROM [Products]")); + + Assert.Equal( + CoreStrings.ErrorMaterializingPropertyInvalidCast("UnmappedProduct", "UnitPrice", typeof(decimal?), typeof(int)), + (async + ? await Assert.ThrowsAsync(() => query.ToListAsync()) + : Assert.Throws(() => query.ToList())).Message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Bad_data_error_handling_invalid_cast_projection(bool async) + { + using var context = CreateContext(); + var query = context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString( + @"SELECT [ProductID], [SupplierID] AS [UnitPrice], [ProductName], [UnitsInStock], [Discontinued] + FROM [Products]")) + .Select(p => p.UnitPrice); + + Assert.Equal( + RelationalStrings.ErrorMaterializingValueInvalidCast(typeof(decimal?), typeof(int)), + (async + ? await Assert.ThrowsAsync(() => query.ToListAsync()) + : Assert.Throws(() => query.ToList())).Message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Bad_data_error_handling_invalid_cast_no_tracking(bool async) + { + using var context = CreateContext(); + var query = context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString( + @"SELECT [ProductName] AS [ProductID], [ProductID] AS [ProductName], [SupplierID], [UnitPrice], [UnitsInStock], [Discontinued] + FROM [Products]")).AsNoTracking(); + + Assert.Equal( + CoreStrings.ErrorMaterializingPropertyInvalidCast("UnmappedProduct", "ProductID", typeof(int), typeof(string)), + (async + ? await Assert.ThrowsAsync(() => query.ToListAsync()) + : Assert.Throws(() => query.ToList())).Message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Bad_data_error_handling_null(bool async) + { + using var context = CreateContext(); + var query = context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString( + @"SELECT [ProductID], [ProductName], [SupplierID], [UnitPrice], [UnitsInStock], NULL AS [Discontinued] + FROM [Products]")); + + Assert.Equal( + RelationalStrings.ErrorMaterializingPropertyNullReference("UnmappedProduct", "Discontinued", typeof(bool)), + (async + ? await Assert.ThrowsAsync(() => query.ToListAsync()) + : Assert.Throws(() => query.ToList())).Message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Bad_data_error_handling_null_projection(bool async) + { + using var context = CreateContext(); + var query = context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString( + @"SELECT [ProductID], [ProductName], [SupplierID], [UnitPrice], [UnitsInStock], NULL AS [Discontinued] + FROM [Products]")) + .Select(p => p.Discontinued); + + Assert.Equal( + RelationalStrings.ErrorMaterializingValueNullReference(typeof(bool)), + (async + ? await Assert.ThrowsAsync(() => query.ToListAsync()) + : Assert.Throws(() => query.ToList())).Message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Bad_data_error_handling_null_no_tracking(bool async) + { + using var context = CreateContext(); + var query = context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString( + @"SELECT [ProductID], [ProductName], [SupplierID], [UnitPrice], [UnitsInStock], NULL AS [Discontinued] + FROM [Products]")).AsNoTracking(); + + Assert.Equal( + RelationalStrings.ErrorMaterializingPropertyNullReference("UnmappedProduct", "Discontinued", typeof(bool)), + (async + ? await Assert.ThrowsAsync(() => query.ToListAsync()) + : Assert.Throws(() => query.ToList())).Message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task SqlQueryRaw_queryable_simple(bool async) + => AssertQuery( + async, + _ => Fixture.CreateContext().Database.SqlQueryRaw + (NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [ContactName] LIKE '%z%'")), + ss => ss.Set().Where(x => x.ContactName.Contains("z")).Select(e => UnmappedCustomer.FromCustomer(e)), + elementSorter: e => e.CustomerID, + elementAsserter: AssertUnmappedCustomers, + entryCount: 0); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task SqlQueryRaw_queryable_simple_columns_out_of_order(bool async) + => AssertQuery( + async, + _ => Fixture.CreateContext().Database.SqlQueryRaw( + NormalizeDelimitersInRawString( + "SELECT [Region], [PostalCode], [Phone], [Fax], [CustomerID], [Country], [ContactTitle], [ContactName], [CompanyName], [City], [Address] FROM [Customers]")), + ss => ss.Set().Select(e => UnmappedCustomer.FromCustomer(e)), + elementSorter: e => e.CustomerID, + elementAsserter: AssertUnmappedCustomers, + entryCount: 0); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task SqlQueryRaw_queryable_simple_columns_out_of_order_and_extra_columns(bool async) + => AssertQuery( + async, + _ => Fixture.CreateContext().Database.SqlQueryRaw( + NormalizeDelimitersInRawString( + "SELECT [Region], [PostalCode], [PostalCode] AS [Foo], [Phone], [Fax], [CustomerID], [Country], [ContactTitle], [ContactName], [CompanyName], [City], [Address] FROM [Customers]")), + ss => ss.Set().Select(e => UnmappedCustomer.FromCustomer(e)), + elementSorter: e => e.CustomerID, + elementAsserter: AssertUnmappedCustomers, + entryCount: 0); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SqlQueryRaw_queryable_simple_columns_out_of_order_and_not_enough_columns_throws(bool async) + { + using var context = CreateContext(); + var query = context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString( + "SELECT [PostalCode], [Phone], [Fax], [CustomerID], [Country], [ContactTitle], [ContactName], [CompanyName], [City], [Address] FROM [Customers]")); + + Assert.Equal( + RelationalStrings.FromSqlMissingColumn("Region"), + (async + ? await Assert.ThrowsAsync(() => query.ToListAsync()) + : Assert.Throws(() => query.ToList())).Message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task SqlQueryRaw_queryable_composed(bool async) + => AssertQuery( + async, + _ => Fixture.CreateContext().Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers]")) + .Where(c => c.ContactName.Contains("z")), + ss => ss.Set().Where(c => c.ContactName.Contains("z")).Select(e => UnmappedCustomer.FromCustomer(e)), + elementSorter: e => e.CustomerID, + elementAsserter: AssertUnmappedCustomers, + entryCount: 0); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task SqlQueryRaw_queryable_composed_after_removing_whitespaces(bool async) + => AssertQuery( + async, + _ => Fixture.CreateContext().Database.SqlQueryRaw( + NormalizeDelimitersInRawString( + _eol + " " + _eol + _eol + _eol + "SELECT" + _eol + "* FROM [Customers]")) + .Where(c => c.ContactName.Contains("z")), + ss => ss.Set().Where(c => c.ContactName.Contains("z")).Select(e => UnmappedCustomer.FromCustomer(e)), + elementSorter: e => e.CustomerID, + elementAsserter: AssertUnmappedCustomers, + entryCount: 0); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SqlQueryRaw_queryable_composed_compiled(bool async) + { + if (async) + { + var query = EF.CompileAsyncQuery( + (NorthwindContext context) => context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers]")) + .Where(c => c.ContactName.Contains("z"))); + + using (var context = CreateContext()) + { + var actual = await query(context).ToListAsync(); + + Assert.Equal(14, actual.Count); + } + } + else + { + var query = EF.CompileQuery( + (NorthwindContext context) => context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers]")) + .Where(c => c.ContactName.Contains("z"))); + + using (var context = CreateContext()) + { + var actual = query(context).ToArray(); + + Assert.Equal(14, actual.Length); + } + } + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SqlQueryRaw_queryable_composed_compiled_with_parameter(bool async) + { + if (async) + { + var query = EF.CompileAsyncQuery( + (NorthwindContext context) => context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [CustomerID] = {0}"), "CONSH") + .Where(c => c.ContactName.Contains("z"))); + + using (var context = CreateContext()) + { + var actual = await query(context).ToListAsync(); + + Assert.Single(actual); + } + } + else + { + var query = EF.CompileQuery( + (NorthwindContext context) => context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [CustomerID] = {0}"), "CONSH") + .Where(c => c.ContactName.Contains("z"))); + + using (var context = CreateContext()) + { + var actual = query(context).ToArray(); + + Assert.Single(actual); + } + } + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SqlQueryRaw_queryable_composed_compiled_with_DbParameter(bool async) + { + if (async) + { + var query = EF.CompileAsyncQuery( + (NorthwindContext context) => context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [CustomerID] = @customer"), + CreateDbParameter("customer", "CONSH")) + .Where(c => c.ContactName.Contains("z"))); + + using (var context = CreateContext()) + { + var actual = await query(context).ToListAsync(); + + Assert.Single(actual); + } + } + else + { + var query = EF.CompileQuery( + (NorthwindContext context) => context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [CustomerID] = @customer"), + CreateDbParameter("customer", "CONSH")) + .Where(c => c.ContactName.Contains("z"))); + + using (var context = CreateContext()) + { + var actual = query(context).ToArray(); + + Assert.Single(actual); + } + } + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SqlQueryRaw_queryable_composed_compiled_with_nameless_DbParameter(bool async) + { + if (async) + { + var query = EF.CompileAsyncQuery( + (NorthwindContext context) => context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [CustomerID] = {0}"), + CreateDbParameter(null, "CONSH")) + .Where(c => c.ContactName.Contains("z"))); + + using (var context = CreateContext()) + { + var actual = await query(context).ToListAsync(); + + Assert.Single(actual); + } + } + else + { + var query = EF.CompileQuery( + (NorthwindContext context) => context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [CustomerID] = {0}"), + CreateDbParameter(null, "CONSH")) + .Where(c => c.ContactName.Contains("z"))); + + using (var context = CreateContext()) + { + var actual = query(context).ToArray(); + + Assert.Single(actual); + } + } + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task SqlQueryRaw_composed_contains(bool async) + { + var context = Fixture.CreateContext(); + return AssertQuery( + async, + ss => from c in context.Database.SqlQueryRaw(NormalizeDelimitersInRawString("SELECT * FROM [Customers]")) + where context.Database.SqlQueryRaw(NormalizeDelimitersInRawString("SELECT * FROM [Orders]")) + .Select(o => o.CustomerID) + .Contains(c.CustomerID) + select c, + ss => from c in ss.Set() + where ss.Set() + .Select(o => o.CustomerID) + .Contains(c.CustomerID) + select UnmappedCustomer.FromCustomer(c), + elementSorter: e => e.CustomerID, + elementAsserter: AssertUnmappedCustomers, + entryCount: 0); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task SqlQueryRaw_queryable_multiple_composed(bool async) + { + var context = Fixture.CreateContext(); + + return AssertQuery( + async, + _ => from c in context.Database + .SqlQueryRaw(NormalizeDelimitersInRawString("SELECT * FROM [Customers]")) + from o in context.Database + .SqlQueryRaw(NormalizeDelimitersInRawString("SELECT * FROM [Orders]")) + where c.CustomerID == o.CustomerID + select new { c, o }, + ss => from c in ss.Set() + from o in ss.Set() + where c.CustomerID == o.CustomerID + select new { c = UnmappedCustomer.FromCustomer(c), o = UnmappedOrder.FromOrder(o) }, + elementSorter: e => (e.c.CustomerID, e.o.OrderID), + elementAsserter: (l, r) => + { + AssertUnmappedCustomers(l.c, r.c); + AssertUnmappedOrders(l.o, r.o); + }, + entryCount: 0); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task SqlQueryRaw_queryable_multiple_composed_with_closure_parameters(bool async) + { + var startDate = new DateTime(1997, 1, 1); + var endDate = new DateTime(1998, 1, 1); + var context = Fixture.CreateContext(); + + return AssertQuery( + async, + _ => from c in context.Database.SqlQueryRaw(NormalizeDelimitersInRawString("SELECT * FROM [Customers]")) + from o in context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Orders] WHERE [OrderDate] BETWEEN {0} AND {1}"), + startDate, + endDate) + where c.CustomerID == o.CustomerID + select new { c, o }, + ss => from c in ss.Set() + from o in ss.Set().Where(x => x.OrderDate >= startDate && x.OrderDate <= endDate) + where c.CustomerID == o.CustomerID + select new { c = UnmappedCustomer.FromCustomer(c), o = UnmappedOrder.FromOrder(o) }, + elementSorter: e => (e.c.CustomerID, e.o.OrderID), + elementAsserter: (l, r) => + { + AssertUnmappedCustomers(l.c, r.c); + AssertUnmappedOrders(l.o, r.o); + }, + entryCount: 0); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SqlQueryRaw_queryable_multiple_composed_with_parameters_and_closure_parameters(bool async) + { + var city = "London"; + var startDate = new DateTime(1997, 1, 1); + var endDate = new DateTime(1998, 1, 1); + var context = Fixture.CreateContext(); + + await AssertQuery( + async, + _ => from c in context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [City] = {0}"), city) + from o in context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Orders] WHERE [OrderDate] BETWEEN {0} AND {1}"), + startDate, + endDate) + where c.CustomerID == o.CustomerID + select new { c, o }, + ss => from c in ss.Set().Where(x => x.City == city) + from o in ss.Set().Where(x => x.OrderDate >= startDate && x.OrderDate <= endDate) + where c.CustomerID == o.CustomerID + select new { c = UnmappedCustomer.FromCustomer(c), o = UnmappedOrder.FromOrder(o) }, + elementSorter: e => (e.c.CustomerID, e.o.OrderID), + elementAsserter: (l, r) => + { + AssertUnmappedCustomers(l.c, r.c); + AssertUnmappedOrders(l.o, r.o); + }, + entryCount: 0); + + city = "Berlin"; + startDate = new DateTime(1998, 4, 1); + endDate = new DateTime(1998, 5, 1); + + await AssertQuery( + async, + _ => from c in context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [City] = {0}"), city) + from o in context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Orders] WHERE [OrderDate] BETWEEN {0} AND {1}"), + startDate, + endDate) + where c.CustomerID == o.CustomerID + select new { c, o }, + ss => from c in ss.Set().Where(x => x.City == city) + from o in ss.Set().Where(x => x.OrderDate >= startDate && x.OrderDate <= endDate) + where c.CustomerID == o.CustomerID + select new { c = UnmappedCustomer.FromCustomer(c), o = UnmappedOrder.FromOrder(o) }, + elementSorter: e => (e.c.CustomerID, e.o.OrderID), + elementAsserter: (l, r) => + { + AssertUnmappedCustomers(l.c, r.c); + AssertUnmappedOrders(l.o, r.o); + }, + entryCount: 0); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task SqlQueryRaw_queryable_multiple_line_query(bool async) + => AssertQuery( + async, + _ => Fixture.CreateContext().Database.SqlQueryRaw( + NormalizeDelimitersInRawString( + @"SELECT * +FROM [Customers] +WHERE [City] = 'London'")), + ss => ss.Set().Where(x => x.City == "London").Select(e => UnmappedCustomer.FromCustomer(e)), + elementSorter: e => e.CustomerID, + elementAsserter: AssertUnmappedCustomers, + entryCount: 0); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task SqlQueryRaw_queryable_composed_multiple_line_query(bool async) + => AssertQuery( + async, + _ => Fixture.CreateContext().Database.SqlQueryRaw( + NormalizeDelimitersInRawString( + @"SELECT * +FROM [Customers]")) + .Where(c => c.City == "London"), + ss => ss.Set().Where(x => x.City == "London").Select(e => UnmappedCustomer.FromCustomer(e)), + elementSorter: e => e.CustomerID, + elementAsserter: AssertUnmappedCustomers, + entryCount: 0); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task SqlQueryRaw_queryable_with_parameters(bool async) + { + var city = "London"; + var contactTitle = "Sales Representative"; + + return AssertQuery( + async, + _ => Fixture.CreateContext().Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [City] = {0} AND [ContactTitle] = {1}"), city, + contactTitle), + ss => ss.Set().Where(x => x.City == city && x.ContactTitle == contactTitle) + .Select(e => UnmappedCustomer.FromCustomer(e)), + elementSorter: e => e.CustomerID, + elementAsserter: AssertUnmappedCustomers, + entryCount: 0); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task SqlQueryRaw_queryable_with_parameters_inline(bool async) + => AssertQuery( + async, + _ => Fixture.CreateContext().Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [City] = {0} AND [ContactTitle] = {1}"), "London", + "Sales Representative"), + ss => ss.Set().Where(x => x.City == "London" && x.ContactTitle == "Sales Representative") + .Select(e => UnmappedCustomer.FromCustomer(e)), + elementSorter: e => e.CustomerID, + elementAsserter: AssertUnmappedCustomers, + entryCount: 0); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task SqlQuery_queryable_with_parameters_interpolated(bool async) + { + var city = "London"; + var contactTitle = "Sales Representative"; + + return AssertQuery( + async, + _ => Fixture.CreateContext().Database.SqlQuery( + NormalizeDelimitersInInterpolatedString( + $"SELECT * FROM [Customers] WHERE [City] = {city} AND [ContactTitle] = {contactTitle}")), + ss => ss.Set().Where(x => x.City == city && x.ContactTitle == contactTitle) + .Select(e => UnmappedCustomer.FromCustomer(e)), + elementSorter: e => e.CustomerID, + elementAsserter: AssertUnmappedCustomers, + entryCount: 0); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task SqlQuery_queryable_with_parameters_inline_interpolated(bool async) + => AssertQuery( + async, + _ => Fixture.CreateContext().Database.SqlQuery( + NormalizeDelimitersInInterpolatedString( + $"SELECT * FROM [Customers] WHERE [City] = {"London"} AND [ContactTitle] = {"Sales Representative"}")), + ss => ss.Set().Where(x => x.City == "London" && x.ContactTitle == "Sales Representative") + .Select(e => UnmappedCustomer.FromCustomer(e)), + elementSorter: e => e.CustomerID, + elementAsserter: AssertUnmappedCustomers, + entryCount: 0); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SqlQuery_queryable_multiple_composed_with_parameters_and_closure_parameters_interpolated( + bool async) + { + var city = "London"; + var startDate = new DateTime(1997, 1, 1); + var endDate = new DateTime(1998, 1, 1); + var context = Fixture.CreateContext(); + + await AssertQuery( + async, + _ => from c in context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [City] = {0}"), city) + from o in context.Database.SqlQuery( + NormalizeDelimitersInInterpolatedString( + $"SELECT * FROM [Orders] WHERE [OrderDate] BETWEEN {startDate} AND {endDate}")) + where c.CustomerID == o.CustomerID + select new { c, o }, + ss => from c in ss.Set().Where(x => x.City == city) + from o in ss.Set().Where(x => x.OrderDate >= startDate && x.OrderDate <= endDate) + where c.CustomerID == o.CustomerID + select new { c = UnmappedCustomer.FromCustomer(c), o = UnmappedOrder.FromOrder(o) }, + elementSorter: e => (e.c.CustomerID, e.o.OrderID), + elementAsserter: (l, r) => + { + AssertUnmappedCustomers(l.c, r.c); + AssertUnmappedOrders(l.o, r.o); + }, + entryCount: 0); + + city = "Berlin"; + startDate = new DateTime(1998, 4, 1); + endDate = new DateTime(1998, 5, 1); + + await AssertQuery( + async, + _ => from c in context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [City] = {0}"), city) + from o in context.Database.SqlQuery( + NormalizeDelimitersInInterpolatedString( + $"SELECT * FROM [Orders] WHERE [OrderDate] BETWEEN {startDate} AND {endDate}")) + where c.CustomerID == o.CustomerID + select new { c, o }, + ss => from c in ss.Set().Where(x => x.City == city) + from o in ss.Set().Where(x => x.OrderDate >= startDate && x.OrderDate <= endDate) + where c.CustomerID == o.CustomerID + select new { c = UnmappedCustomer.FromCustomer(c), o = UnmappedOrder.FromOrder(o) }, + elementSorter: e => (e.c.CustomerID, e.o.OrderID), + elementAsserter: (l, r) => + { + AssertUnmappedCustomers(l.c, r.c); + AssertUnmappedOrders(l.o, r.o); + }, + entryCount: 0); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task SqlQueryRaw_queryable_with_null_parameter(bool async) + { + uint? reportsTo = null; + + return AssertQuery( + async, + _ => Fixture.CreateContext().Database.SqlQueryRaw( + NormalizeDelimitersInRawString( + // ReSharper disable once ExpressionIsAlwaysNull + "SELECT * FROM [Employees] WHERE [ReportsTo] = {0} OR ([ReportsTo] IS NULL AND {0} IS NULL)"), reportsTo), + ss => ss.Set().Where(x => x.ReportsTo == reportsTo).Select(e => UnmappedEmployee.FromEmployee(e)), + elementSorter: e => e.EmployeeID, + elementAsserter: AssertUnmappedEmployees, + entryCount: 0); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SqlQueryRaw_queryable_with_parameters_and_closure(bool async) + { + var city = "London"; + var contactTitle = "Sales Representative"; + + using var context = CreateContext(); + var query = context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [City] = {0}"), city) + .Where(c => c.ContactTitle == contactTitle); + var queryString = query.ToQueryString(); + + var actual = async + ? await query.ToArrayAsync() + : query.ToArray(); + + Assert.Equal(3, actual.Length); + Assert.True(actual.All(c => c.City == "London")); + Assert.True(actual.All(c => c.ContactTitle == "Sales Representative")); + + return queryString; + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SqlQueryRaw_queryable_simple_cache_key_includes_query_string(bool async) + { + await AssertQuery( + async, + _ => Fixture.CreateContext().Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [City] = 'London'")), + ss => ss.Set().Where(x => x.City == "London").Select(e => UnmappedCustomer.FromCustomer(e)), + elementSorter: e => e.CustomerID, + elementAsserter: AssertUnmappedCustomers, + entryCount: 0); + + await AssertQuery( + async, + _ => Fixture.CreateContext().Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [City] = 'Seattle'")), + ss => ss.Set().Where(x => x.City == "Seattle").Select(e => UnmappedCustomer.FromCustomer(e)), + elementSorter: e => e.CustomerID, + elementAsserter: AssertUnmappedCustomers, + entryCount: 0); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SqlQueryRaw_queryable_with_parameters_cache_key_includes_parameters(bool async) + { + var city = "London"; + var contactTitle = "Sales Representative"; + var sql = "SELECT * FROM [Customers] WHERE [City] = {0} AND [ContactTitle] = {1}"; + + await AssertQuery( + async, + _ => Fixture.CreateContext().Database.SqlQueryRaw(NormalizeDelimitersInRawString(sql), city, contactTitle), + ss => ss.Set().Where(x => x.City == city && x.ContactTitle == contactTitle) + .Select(e => UnmappedCustomer.FromCustomer(e)), + elementSorter: e => e.CustomerID, + elementAsserter: AssertUnmappedCustomers, + entryCount: 0); + + city = "Madrid"; + contactTitle = "Accounting Manager"; + + await AssertQuery( + async, + _ => Fixture.CreateContext().Database.SqlQueryRaw(NormalizeDelimitersInRawString(sql), city, contactTitle), + ss => ss.Set().Where(x => x.City == city && x.ContactTitle == contactTitle) + .Select(e => UnmappedCustomer.FromCustomer(e)), + elementSorter: e => e.CustomerID, + elementAsserter: AssertUnmappedCustomers, + entryCount: 0); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task SqlQueryRaw_queryable_simple_as_no_tracking_not_composed(bool async) + => AssertQuery( + async, + _ => Fixture.CreateContext().Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers]")) + .AsNoTracking(), + ss => ss.Set().Select(e => UnmappedCustomer.FromCustomer(e)), + elementSorter: e => e.CustomerID, + elementAsserter: AssertUnmappedCustomers, + entryCount: 0); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SqlQueryRaw_queryable_simple_projection_composed(bool async) + { + using var context = CreateContext(); + var boolMapping = (RelationalTypeMapping)context.GetService().FindMapping(typeof(bool)); + var boolLiteral = boolMapping.GenerateSqlLiteral(true); + + await AssertQuery( + async, + _ => Fixture.CreateContext().Database.SqlQueryRaw( + NormalizeDelimitersInRawString( + @"SELECT * +FROM [Products] +WHERE [Discontinued] <> " + + boolLiteral + + @" +AND (([UnitsInStock] + [UnitsOnOrder]) < [ReorderLevel])")) + .Select(p => p.ProductName), + ss => ss.Set() + .Where(x => x.Discontinued != true && (x.UnitsInStock + x.UnitsOnOrder) < x.ReorderLevel) + .Select(x => x.ProductName)); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SqlQueryRaw_annotations_do_not_affect_successive_calls(bool async) + { + using var context = CreateContext(); + var query = context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [ContactName] LIKE '%z%'")); + + var actual = async + ? await query.ToArrayAsync() + : query.ToArray(); + + Assert.Equal(14, actual.Length); + + query = context.Database.SqlQueryRaw("SELECT * FROM [Customers]"); + actual = async + ? await query.ToArrayAsync() + : query.ToArray(); + + Assert.Equal(91, actual.Length); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task SqlQueryRaw_composed_with_nullable_predicate(bool async) + => AssertQuery( + async, + _ => Fixture.CreateContext().Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers]")) + .Where(c => c.ContactName == c.CompanyName), + ss => ss.Set().Where(c => c.ContactName == c.CompanyName).Select(e => UnmappedCustomer.FromCustomer(e)), + elementSorter: e => e.CustomerID, + elementAsserter: AssertUnmappedCustomers, + entryCount: 0); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SqlQueryRaw_with_dbParameter(bool async) + { + var parameter = CreateDbParameter("@city", "London"); + + await AssertQuery( + async, + _ => Fixture.CreateContext().Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [City] = @city"), parameter), + ss => ss.Set().Where(x => x.City == "London").Select(e => UnmappedCustomer.FromCustomer(e)), + elementSorter: e => e.CustomerID, + elementAsserter: AssertUnmappedCustomers, + entryCount: 0); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SqlQueryRaw_with_dbParameter_without_name_prefix(bool async) + { + var parameter = CreateDbParameter("city", "London"); + + await AssertQuery( + async, + _ => Fixture.CreateContext().Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [City] = @city"), parameter), + ss => ss.Set().Where(x => x.City == "London").Select(e => UnmappedCustomer.FromCustomer(e)), + elementSorter: e => e.CustomerID, + elementAsserter: AssertUnmappedCustomers, + entryCount: 0); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SqlQueryRaw_with_dbParameter_mixed(bool async) + { + var city = "London"; + var title = "Sales Representative"; + + var titleParameter = CreateDbParameter("@title", title); + + await AssertQuery( + async, + _ => Fixture.CreateContext().Database.SqlQueryRaw( + NormalizeDelimitersInRawString( + "SELECT * FROM [Customers] WHERE [City] = {0} AND [ContactTitle] = @title"), city, titleParameter), + ss => ss.Set().Where(x => x.City == city && x.ContactTitle == title).Select(e => UnmappedCustomer.FromCustomer(e)), + elementSorter: e => e.CustomerID, + elementAsserter: AssertUnmappedCustomers, + entryCount: 0); + + var cityParameter = CreateDbParameter("@city", city); + + await AssertQuery( + async, + _ => Fixture.CreateContext().Database.SqlQueryRaw( + NormalizeDelimitersInRawString( + "SELECT * FROM [Customers] WHERE [City] = @city AND [ContactTitle] = {1}"), cityParameter, title), + ss => ss.Set().Where(x => x.City == city && x.ContactTitle == title).Select(e => UnmappedCustomer.FromCustomer(e)), + elementSorter: e => e.CustomerID, + elementAsserter: AssertUnmappedCustomers, + entryCount: 0); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SqlQueryRaw_with_db_parameters_called_multiple_times(bool async) + { + using var context = CreateContext(); + var parameter = CreateDbParameter("@id", "ALFKI"); + + var query = context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [CustomerID] = @id"), parameter); + + // ReSharper disable PossibleMultipleEnumeration + var result1 = async + ? await query.ToArrayAsync() + : query.ToArray(); + + Assert.Single(result1); + + var result2 = async + ? await query.ToArrayAsync() + : query.ToArray(); + // ReSharper restore PossibleMultipleEnumeration + + Assert.Single(result2); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SqlQuery_with_inlined_db_parameter(bool async) + { + var parameter = CreateDbParameter("@somename", "ALFKI"); + + await AssertQuery( + async, + _ => Fixture.CreateContext().Database.SqlQuery( + NormalizeDelimitersInInterpolatedString($"SELECT * FROM [Customers] WHERE [CustomerID] = {parameter}")), + ss => ss.Set().Where(x => x.CustomerID == "ALFKI").Select(e => UnmappedCustomer.FromCustomer(e)), + elementSorter: e => e.CustomerID, + elementAsserter: AssertUnmappedCustomers, + entryCount: 0); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SqlQuery_with_inlined_db_parameter_without_name_prefix(bool async) + { + var parameter = CreateDbParameter("somename", "ALFKI"); + + await AssertQuery( + async, + _ => Fixture.CreateContext().Database.SqlQuery( + NormalizeDelimitersInInterpolatedString($"SELECT * FROM [Customers] WHERE [CustomerID] = {parameter}")), + ss => ss.Set().Where(x => x.CustomerID == "ALFKI").Select(e => UnmappedCustomer.FromCustomer(e)), + elementSorter: e => e.CustomerID, + elementAsserter: AssertUnmappedCustomers, + entryCount: 0); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SqlQuery_parameterization_issue_12213(bool async) + { + using var context = CreateContext(); + var min = 10300; + var max = 10400; + + var query1 = context.Database.SqlQuery( + NormalizeDelimitersInInterpolatedString($"SELECT * FROM [Orders] WHERE [OrderID] >= {min}")) + .Select(i => i.OrderID); + + var actual1 = async + ? await query1.ToArrayAsync() + : query1.ToArray(); + + var query2 = context.Database.SqlQueryRaw("SELECT * FROM [Orders]") + .Where(o => o.OrderID <= max && query1.Contains(o.OrderID)) + .Select(o => o.OrderID); + + var actual2 = async + ? await query2.ToArrayAsync() + : query2.ToArray(); + + var query3 = context.Database.SqlQueryRaw("SELECT * FROM [Orders]") + .Where( + o => o.OrderID <= max + && context.Database.SqlQuery( + NormalizeDelimitersInInterpolatedString($"SELECT * FROM [Orders] WHERE [OrderID] >= {min}")) + .Select(i => i.OrderID) + .Contains(o.OrderID)) + .Select(o => o.OrderID); + + var actual3 = async + ? await query3.ToArrayAsync() + : query3.ToArray(); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SqlQueryRaw_does_not_parameterize_interpolated_string(bool async) + { + var tableName = "Orders"; + var max = 10250; + + await AssertQuery( + async, + _ => Fixture.CreateContext().Database.SqlQueryRaw( + NormalizeDelimitersInRawString($"SELECT * FROM [{tableName}] WHERE [OrderID] < {{0}}"), max), + ss => ss.Set().Where(x => x.OrderID < max).Select(e => UnmappedOrder.FromOrder(e)), + elementSorter: e => e.CustomerID, + elementAsserter: AssertUnmappedOrders, + entryCount: 0); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task SqlQueryRaw_with_set_operation(bool async) + { + var context = Fixture.CreateContext(); + + return AssertQuery( + async, + _ => context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [City] = 'London'")) + .Concat( + context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [City] = 'Berlin'"))), + ss => ss.Set().Where(x => x.City == "London") + .Concat(ss.Set().Where(x => x.City == "Berlin")).Select(e => UnmappedCustomer.FromCustomer(e)), + elementSorter: e => e.CustomerID, + elementAsserter: AssertUnmappedCustomers, + entryCount: 0); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Line_endings_after_Select(bool async) + => AssertQuery( + async, + _ => Fixture.CreateContext().Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT" + Environment.NewLine + "* FROM [Customers]")) + .Where(e => e.City == "Seattle"), + ss => ss.Set().Where(x => x.City == "Seattle").Select(e => UnmappedCustomer.FromCustomer(e)), + elementSorter: e => e.CustomerID, + elementAsserter: AssertUnmappedCustomers, + entryCount: 0); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task SqlQueryRaw_queryable_simple_projection_not_composed(bool async) + => AssertQuery( + async, + _ => Fixture.CreateContext().Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers]")) + .Select(c => new { c.CustomerID, c.City }) + .AsNoTracking(), + ss => ss.Set().Select(c => new { c.CustomerID, c.City }), + elementSorter: e => e.CustomerID, + elementAsserter: (e, a) => + { + Assert.Equal(e.CustomerID, a.CustomerID); + Assert.Equal(e.City, a.City); + }); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task SqlQueryRaw_in_subquery_with_dbParameter(bool async) + { + var context = Fixture.CreateContext(); + + return AssertQuery( + async, + _ => context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Orders]")).Where( + o => context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString(@"SELECT * FROM [Customers] WHERE [City] = @city"), + // ReSharper disable once FormatStringProblem + CreateDbParameter("@city", "London")) + .Select(c => c.CustomerID) + .Contains(o.CustomerID)), + ss => ss.Set().Select(e => UnmappedOrder.FromOrder(e)).Where( + o => ss.Set().Select(e => UnmappedCustomer.FromCustomer(e)).Where(x => x.City == "London") + .Select(c => c.CustomerID) + .Contains(o.CustomerID)), + elementSorter: e => e.OrderID, + elementAsserter: AssertUnmappedOrders, + entryCount: 0); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task SqlQueryRaw_in_subquery_with_positional_dbParameter_without_name(bool async) + { + var context = Fixture.CreateContext(); + + return AssertQuery( + async, + _ => context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Orders]")).Where( + o => context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString(@"SELECT * FROM [Customers] WHERE [City] = {0}"), + // ReSharper disable once FormatStringProblem + CreateDbParameter(null, "London")) + .Select(c => c.CustomerID) + .Contains(o.CustomerID)), + ss => ss.Set().Select(e => UnmappedOrder.FromOrder(e)).Where( + o => ss.Set().Select(e => UnmappedCustomer.FromCustomer(e)).Where(x => x.City == "London") + .Select(c => c.CustomerID) + .Contains(o.CustomerID)), + elementSorter: e => e.OrderID, + elementAsserter: AssertUnmappedOrders, + entryCount: 0); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task SqlQueryRaw_in_subquery_with_positional_dbParameter_with_name(bool async) + { + var context = Fixture.CreateContext(); + + return AssertQuery( + async, + _ => context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Orders]")).Where( + o => context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString(@"SELECT * FROM [Customers] WHERE [City] = {0}"), + // ReSharper disable once FormatStringProblem + CreateDbParameter("@city", "London")) + .Select(c => c.CustomerID) + .Contains(o.CustomerID)), + ss => ss.Set().Select(e => UnmappedOrder.FromOrder(e)).Where( + o => ss.Set().Select(e => UnmappedCustomer.FromCustomer(e)).Where(x => x.City == "London") + .Select(c => c.CustomerID) + .Contains(o.CustomerID)), + elementSorter: e => e.OrderID, + elementAsserter: AssertUnmappedOrders, + entryCount: 0); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SqlQueryRaw_with_dbParameter_mixed_in_subquery(bool async) + { + const string city = "London"; + const string title = "Sales Representative"; + var context = Fixture.CreateContext(); + + await AssertQuery( + async, + _ => context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Orders]")).Where( + o => context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString(@"SELECT * FROM [Customers] WHERE [City] = {0} AND [ContactTitle] = @title"), + city, + // ReSharper disable once FormatStringProblem + CreateDbParameter("@title", title)) + .Select(c => c.CustomerID) + .Contains(o.CustomerID)), + ss => ss.Set().Select(e => UnmappedOrder.FromOrder(e)).Where( + o => ss.Set().Select(e => UnmappedCustomer.FromCustomer(e)).Where(x => x.City == city && x.ContactTitle == title) + .Select(c => c.CustomerID) + .Contains(o.CustomerID)), + elementSorter: e => e.OrderID, + elementAsserter: AssertUnmappedOrders, + entryCount: 0); + + await AssertQuery( + async, + _ => context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Orders]")).Where( + o => context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString(@"SELECT * FROM [Customers] WHERE [City] = @city AND [ContactTitle] = {1}"), + // ReSharper disable once FormatStringProblem + CreateDbParameter("@city", city), + title) + .Select(c => c.CustomerID) + .Contains(o.CustomerID)), + ss => ss.Set().Select(e => UnmappedOrder.FromOrder(e)).Where( + o => ss.Set().Select(e => UnmappedCustomer.FromCustomer(e)).Where(x => x.City == city && x.ContactTitle == title) + .Select(c => c.CustomerID) + .Contains(o.CustomerID)), + elementSorter: e => e.OrderID, + elementAsserter: AssertUnmappedOrders, + entryCount: 0); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task SqlQueryRaw_composed_with_common_table_expression(bool async) + => AssertQuery( + async, + _ => Fixture.CreateContext().Database.SqlQueryRaw( + NormalizeDelimitersInRawString( + @"WITH [Customers2] AS ( + SELECT * FROM [Customers] +) +SELECT * FROM [Customers2]")) + .Where(c => c.ContactName.Contains("z")), + ss => ss.Set().Where(c => c.ContactName.Contains("z")).Select(e => UnmappedCustomer.FromCustomer(e)), + elementSorter: e => e.CustomerID, + elementAsserter: AssertUnmappedCustomers, + entryCount: 0); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Multiple_occurrences_of_SqlQuery_with_db_parameter_adds_parameter_only_once(bool async) + { + using var context = CreateContext(); + var city = "Seattle"; + var qqlQuery = context.Database.SqlQueryRaw( + NormalizeDelimitersInRawString(@"SELECT * FROM [Customers] WHERE [City] = {0}"), + CreateDbParameter("city", city)); + + var query = qqlQuery.Intersect(qqlQuery); + + var actual = async + ? await query.ToArrayAsync() + : query.ToArray(); + + Assert.Single(actual); + } + + [ConditionalFact] + public virtual void Ad_hoc_type_with_reference_navigation_throws() + { + using var context = CreateContext(); + + Assert.Equal( + CoreStrings.NavigationNotAddedAdHoc("Post", "Blog", "Blog"), + Assert.Throws( + () => context.Database.SqlQueryRaw(NormalizeDelimitersInRawString(@"SELECT * FROM [Posts]"))).Message); + } + + [ConditionalFact] + public virtual void Ad_hoc_type_with_unmapped_property_throws() + { + using var context = CreateContext(); + + Assert.Equal( + CoreStrings.PropertyNotAddedAdHoc("Person", "Contact", "ContactInfo"), + Assert.Throws( + () => context.Database.SqlQueryRaw(NormalizeDelimitersInRawString(@"SELECT * FROM [People]"))).Message); + } + + protected class Blog + { + public int Id { get; set; } + public List Posts { get; set; } + } + + protected class Post + { + public int Id { get; set; } + public int BlogId { get; set; } + public Blog Blog { get; set; } + } + + protected class Person + { + public int Id { get; set; } + public string Name { get; set; } + public ContactInfo Contact { get; set; } + } + + protected readonly struct ContactInfo + { + public string Address { get; init; } + public string Phone { get; init; } + } + + private static void AssertUnmappedCustomers(UnmappedCustomer l, UnmappedCustomer r) + { + Assert.Equal(l.CustomerID, r.CustomerID); + Assert.Equal(l.CompanyName, r.CompanyName); + Assert.Equal(l.ContactName, r.ContactName); + Assert.Equal(l.ContactTitle, r.ContactTitle); + Assert.Equal(l.City, r.City); + Assert.Equal(l.Region, r.Region); + Assert.Equal(l.Zip, r.Zip); + Assert.Equal(l.Country, r.Country); + Assert.Equal(l.Phone, r.Phone); + Assert.Equal(l.Fax, r.Fax); + } + + private static void AssertUnmappedOrders(UnmappedOrder l, UnmappedOrder r) + { + Assert.Equal(l.OrderID, r.OrderID); + Assert.Equal(l.CustomerID, r.CustomerID); + Assert.Equal(l.EmployeeID, r.EmployeeID); + Assert.Equal(l.OrderDate, r.OrderDate); + Assert.Equal(l.RequiredDate, r.RequiredDate); + Assert.Equal(l.ShippedDate, r.ShippedDate); + Assert.Equal(l.ShipVia, r.ShipVia); + Assert.Equal(l.Freight, r.Freight); + Assert.Equal(l.ShipName, r.ShipName); + Assert.Equal(l.ShipAddress, r.ShipAddress); + Assert.Equal(l.ShipRegion, r.ShipRegion); + Assert.Equal(l.ShipPostalCode, r.ShipPostalCode); + Assert.Equal(l.ShipCountry, r.ShipCountry); + } + + private static void AssertUnmappedEmployees(UnmappedEmployee l, UnmappedEmployee r) + { + Assert.Equal(l.EmployeeID, r.EmployeeID); + Assert.Equal(l.LastName, r.LastName); + Assert.Equal(l.FirstName, r.FirstName); + Assert.Equal(l.Title, r.Title); + Assert.Equal(l.TitleOfCourtesy, r.TitleOfCourtesy); + Assert.Equal(l.BirthDate, r.BirthDate); + Assert.Equal(l.HireDate, r.HireDate); + Assert.Equal(l.Address, r.Address); + Assert.Equal(l.City, r.City); + Assert.Equal(l.Region, r.Region); + Assert.Equal(l.PostalCode, r.PostalCode); + Assert.Equal(l.Country, r.Country); + Assert.Equal(l.HomePhone, r.HomePhone); + Assert.Equal(l.Extension, r.Extension); + Assert.Equal(l.Photo, r.Photo); + Assert.Equal(l.Notes, r.Notes); + Assert.Equal(l.ReportsTo, r.ReportsTo); + Assert.Equal(l.PhotoPath, r.PhotoPath); + } + + protected string NormalizeDelimitersInRawString(string sql) + => Fixture.TestStore.NormalizeDelimitersInRawString(sql); + + protected FormattableString NormalizeDelimitersInInterpolatedString(FormattableString sql) + => Fixture.TestStore.NormalizeDelimitersInInterpolatedString(sql); + + protected abstract DbParameter CreateDbParameter(string name, object value); + + protected NorthwindContext CreateContext() + => Fixture.CreateContext(); +} diff --git a/test/EFCore.Specification.Tests/TestModels/Northwind/UnmappedCustomer.cs b/test/EFCore.Specification.Tests/TestModels/Northwind/UnmappedCustomer.cs new file mode 100644 index 00000000000..c71cfa403d1 --- /dev/null +++ b/test/EFCore.Specification.Tests/TestModels/Northwind/UnmappedCustomer.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Microsoft.EntityFrameworkCore.TestModels.Northwind; + +[Table("Customers")] +public class UnmappedCustomer +{ + public UnmappedCustomer(string customerID) + { + CustomerID = customerID; + } + + [MaxLength(5)] + public string CustomerID { get; init; } + + [MaxLength(40)] + [Required] + public string? CompanyName { get; set; } + + [MaxLength(30)] + [Required] + public string? ContactName { get; set; } + + [MaxLength(30)] + public string? ContactTitle { get; set; } + + [MaxLength(60)] + public string? Address { get; set; } + + [MaxLength(15)] + public string? City { get; set; } + + [MaxLength(15)] + public string? Region { get; set; } + + [MaxLength(10)] + [Column("PostalCode")] + public string? Zip { get; set; } + + [MaxLength(15)] + public string? Country { get; set; } + + [MaxLength(24)] + public string? Phone { get; set; } + + [MaxLength(24)] + public string? Fax { get; set; } + + public bool IsLondon + => City == "London"; + + // Unmapped collection navigations are ignored for keyless entity types + public virtual List? Orders { get; set; } + + public static UnmappedCustomer FromCustomer(Customer customer) + => new(customer.CustomerID) + { + CompanyName = customer.CompanyName, + ContactName = customer.ContactName, + ContactTitle = customer.ContactTitle, + Address = customer.Address, + City = customer.City, + Region = customer.Region, + Zip = customer.PostalCode, + Country = customer.Country, + Phone = customer.Phone, + Fax = customer.Fax + }; +} diff --git a/test/EFCore.Specification.Tests/TestModels/Northwind/UnmappedEmployee.cs b/test/EFCore.Specification.Tests/TestModels/Northwind/UnmappedEmployee.cs new file mode 100644 index 00000000000..49b3d3d3008 --- /dev/null +++ b/test/EFCore.Specification.Tests/TestModels/Northwind/UnmappedEmployee.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.EntityFrameworkCore.TestModels.Northwind; + +public class UnmappedEmployee +{ + public int EmployeeID { get; set; } + + [MaxLength(20)] + [Required] + public string LastName { get; set; } + + [MaxLength(10)] + [Required] + public string FirstName { get; set; } + + [MaxLength(30)] + public string Title { get; set; } + + [MaxLength(25)] + public string TitleOfCourtesy { get; set; } + + public DateTime? BirthDate { get; set; } + public DateTime? HireDate { get; set; } + + [MaxLength(60)] + public string Address { get; set; } + + [MaxLength(15)] + public string City { get; set; } + + [MaxLength(15)] + public string Region { get; set; } + + [MaxLength(10)] + public string PostalCode { get; set; } + + [MaxLength(15)] + public string Country { get; set; } + + [MaxLength(24)] + public string HomePhone { get; set; } + + [MaxLength(4)] + public string Extension { get; set; } + + public byte[] Photo { get; set; } + public string Notes { get; set; } + public int? ReportsTo { get; set; } + + [MaxLength(255)] + public string PhotoPath { get; set; } + + public static UnmappedEmployee FromEmployee(Employee employee) + => new() + { + EmployeeID = (int)employee.EmployeeID, + LastName = employee.LastName, + FirstName = employee.FirstName, + Title = employee.Title, + TitleOfCourtesy = employee.TitleOfCourtesy, + BirthDate = employee.BirthDate, + HireDate = employee.HireDate, + Address = employee.Address, + City = employee.City, + Region = employee.Region, + PostalCode = employee.PostalCode, + Country = employee.Country, + HomePhone = employee.HomePhone, + Extension = employee.Extension, + Photo = employee.Photo, + Notes = employee.Notes, + ReportsTo = (int?)employee.ReportsTo, + PhotoPath = employee.PhotoPath, + }; +} diff --git a/test/EFCore.Specification.Tests/TestModels/Northwind/UnmappedOrder.cs b/test/EFCore.Specification.Tests/TestModels/Northwind/UnmappedOrder.cs new file mode 100644 index 00000000000..047afa99800 --- /dev/null +++ b/test/EFCore.Specification.Tests/TestModels/Northwind/UnmappedOrder.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Microsoft.EntityFrameworkCore.TestModels.Northwind; + +[Table("Orders")] +public class UnmappedOrder +{ + public int OrderID { get; set; } + + [MaxLength(5)] + public string CustomerID { get; set; } + + public int? EmployeeID { get; set; } + public DateTime? OrderDate { get; set; } + public DateTime? RequiredDate { get; set; } + public DateTime? ShippedDate { get; set; } + public int? ShipVia { get; set; } + + [Column(TypeName = "decimal(18,3")] + public decimal? Freight { get; set; } + + [MaxLength(40)] + public string ShipName { get; set; } + + [MaxLength(60)] + public string ShipAddress { get; set; } + + [MaxLength(15)] + public string ShipCity { get; set; } + + [MaxLength(15)] + public string ShipRegion { get; set; } + + [MaxLength(10)] + public string ShipPostalCode { get; set; } + + [MaxLength(15)] + public string ShipCountry { get; set; } + + public static UnmappedOrder FromOrder(Order order) + => new() + { + OrderID = order.OrderID, + CustomerID = order.CustomerID, + EmployeeID = (int)order.EmployeeID, + OrderDate = order.OrderDate, + RequiredDate = order.RequiredDate, + ShippedDate = order.ShippedDate, + ShipVia = order.ShipVia, + Freight = order.Freight, + ShipName = order.ShipName, + ShipAddress = order.ShipAddress, + ShipCity = order.ShipCity, + ShipRegion = order.ShipRegion, + ShipPostalCode = order.ShipPostalCode, + ShipCountry = order.ShipCountry, + }; +} diff --git a/test/EFCore.Specification.Tests/TestModels/Northwind/UnmappedProduct.cs b/test/EFCore.Specification.Tests/TestModels/Northwind/UnmappedProduct.cs new file mode 100644 index 00000000000..7ab004ed7b2 --- /dev/null +++ b/test/EFCore.Specification.Tests/TestModels/Northwind/UnmappedProduct.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations.Schema; + +namespace Microsoft.EntityFrameworkCore.TestModels.Northwind; + +public class UnmappedProduct +{ + public int ProductID { get; set; } + public string ProductName { get; set; } + public int? SupplierID { get; set; } + + [NotMapped] + public int? CategoryID { get; set; } + + [Column(TypeName = "decimal(18,3")] + public decimal? UnitPrice { get; set; } + + public short UnitsInStock { get; set; } + public bool Discontinued { get; set; } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/SqlQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/SqlQuerySqlServerTest.cs new file mode 100644 index 00000000000..3cec18d3e5a --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/SqlQuerySqlServerTest.cs @@ -0,0 +1,777 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Data.SqlClient; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class SqlQuerySqlServerTest : SqlQueryTestBase> +{ + public SqlQuerySqlServerTest(NorthwindQuerySqlServerFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task SqlQueryRaw_queryable_simple(bool async) + { + await base.SqlQueryRaw_queryable_simple(async); + + AssertSql( + """ +SELECT * FROM "Customers" WHERE "ContactName" LIKE '%z%' +"""); + } + + public override async Task SqlQueryRaw_queryable_simple_columns_out_of_order(bool async) + { + await base.SqlQueryRaw_queryable_simple_columns_out_of_order(async); + + AssertSql( + """ +SELECT "Region", "PostalCode", "Phone", "Fax", "CustomerID", "Country", "ContactTitle", "ContactName", "CompanyName", "City", "Address" FROM "Customers" +"""); + } + + public override async Task SqlQueryRaw_queryable_simple_columns_out_of_order_and_extra_columns(bool async) + { + await base.SqlQueryRaw_queryable_simple_columns_out_of_order_and_extra_columns(async); + + AssertSql( + """ +SELECT "Region", "PostalCode", "PostalCode" AS "Foo", "Phone", "Fax", "CustomerID", "Country", "ContactTitle", "ContactName", "CompanyName", "City", "Address" FROM "Customers" +"""); + } + + public override async Task SqlQueryRaw_queryable_composed(bool async) + { + await base.SqlQueryRaw_queryable_composed(async); + + AssertSql( + """ +SELECT [m].[Address], [m].[City], [m].[CompanyName], [m].[ContactName], [m].[ContactTitle], [m].[Country], [m].[CustomerID], [m].[Fax], [m].[Phone], [m].[Region], [m].[PostalCode] +FROM ( + SELECT * FROM "Customers" +) AS [m] +WHERE [m].[ContactName] LIKE N'%z%' +"""); + } + + public override async Task SqlQueryRaw_queryable_composed_after_removing_whitespaces(bool async) + { + await base.SqlQueryRaw_queryable_composed_after_removing_whitespaces(async); + + AssertSql( +""" +SELECT [m].[Address], [m].[City], [m].[CompanyName], [m].[ContactName], [m].[ContactTitle], [m].[Country], [m].[CustomerID], [m].[Fax], [m].[Phone], [m].[Region], [m].[PostalCode] +FROM ( + + + + + SELECT + * FROM "Customers" +) AS [m] +WHERE [m].[ContactName] LIKE N'%z%' +"""); + } + + public override async Task SqlQueryRaw_queryable_composed_compiled(bool async) + { + await base.SqlQueryRaw_queryable_composed_compiled(async); + + AssertSql( + """ +SELECT [m].[Address], [m].[City], [m].[CompanyName], [m].[ContactName], [m].[ContactTitle], [m].[Country], [m].[CustomerID], [m].[Fax], [m].[Phone], [m].[Region], [m].[PostalCode] +FROM ( + SELECT * FROM "Customers" +) AS [m] +WHERE [m].[ContactName] LIKE N'%z%' +"""); + } + + public override async Task SqlQueryRaw_queryable_composed_compiled_with_DbParameter(bool async) + { + await base.SqlQueryRaw_queryable_composed_compiled_with_DbParameter(async); + + AssertSql( + """ +customer='CONSH' (Nullable = false) (Size = 5) + +SELECT [m].[Address], [m].[City], [m].[CompanyName], [m].[ContactName], [m].[ContactTitle], [m].[Country], [m].[CustomerID], [m].[Fax], [m].[Phone], [m].[Region], [m].[PostalCode] +FROM ( + SELECT * FROM "Customers" WHERE "CustomerID" = @customer +) AS [m] +WHERE [m].[ContactName] LIKE N'%z%' +"""); + } + + public override async Task SqlQueryRaw_queryable_composed_compiled_with_nameless_DbParameter(bool async) + { + await base.SqlQueryRaw_queryable_composed_compiled_with_nameless_DbParameter(async); + + AssertSql( + """ +p0='CONSH' (Nullable = false) (Size = 5) + +SELECT [m].[Address], [m].[City], [m].[CompanyName], [m].[ContactName], [m].[ContactTitle], [m].[Country], [m].[CustomerID], [m].[Fax], [m].[Phone], [m].[Region], [m].[PostalCode] +FROM ( + SELECT * FROM "Customers" WHERE "CustomerID" = @p0 +) AS [m] +WHERE [m].[ContactName] LIKE N'%z%' +"""); + } + + public override async Task SqlQueryRaw_queryable_composed_compiled_with_parameter(bool async) + { + await base.SqlQueryRaw_queryable_composed_compiled_with_parameter(async); + + AssertSql( + """ +SELECT [m].[Address], [m].[City], [m].[CompanyName], [m].[ContactName], [m].[ContactTitle], [m].[Country], [m].[CustomerID], [m].[Fax], [m].[Phone], [m].[Region], [m].[PostalCode] +FROM ( + SELECT * FROM "Customers" WHERE "CustomerID" = N'CONSH' +) AS [m] +WHERE [m].[ContactName] LIKE N'%z%' +"""); + } + + public override async Task SqlQueryRaw_composed_contains(bool async) + { + await base.SqlQueryRaw_composed_contains(async); + + AssertSql( + """ +SELECT [m].[Address], [m].[City], [m].[CompanyName], [m].[ContactName], [m].[ContactTitle], [m].[Country], [m].[CustomerID], [m].[Fax], [m].[Phone], [m].[Region], [m].[PostalCode] +FROM ( + SELECT * FROM "Customers" +) AS [m] +WHERE EXISTS ( + SELECT 1 + FROM ( + SELECT * FROM "Orders" + ) AS [m0] + WHERE [m0].[CustomerID] = [m].[CustomerID]) +"""); + } + + public override async Task SqlQueryRaw_queryable_multiple_composed(bool async) + { + await base.SqlQueryRaw_queryable_multiple_composed(async); + + AssertSql( + """ +SELECT [m].[Address], [m].[City], [m].[CompanyName], [m].[ContactName], [m].[ContactTitle], [m].[Country], [m].[CustomerID], [m].[Fax], [m].[Phone], [m].[Region], [m].[PostalCode], [m0].[CustomerID], [m0].[EmployeeID], [m0].[Freight], [m0].[OrderDate], [m0].[OrderID], [m0].[RequiredDate], [m0].[ShipAddress], [m0].[ShipCity], [m0].[ShipCountry], [m0].[ShipName], [m0].[ShipPostalCode], [m0].[ShipRegion], [m0].[ShipVia], [m0].[ShippedDate] +FROM ( + SELECT * FROM "Customers" +) AS [m] +CROSS JOIN ( + SELECT * FROM "Orders" +) AS [m0] +WHERE [m].[CustomerID] = [m0].[CustomerID] +"""); + } + + public override async Task SqlQueryRaw_queryable_multiple_composed_with_closure_parameters(bool async) + { + await base.SqlQueryRaw_queryable_multiple_composed_with_closure_parameters(async); + + AssertSql( + """ +p0='1997-01-01T00:00:00.0000000' +p1='1998-01-01T00:00:00.0000000' + +SELECT [m].[Address], [m].[City], [m].[CompanyName], [m].[ContactName], [m].[ContactTitle], [m].[Country], [m].[CustomerID], [m].[Fax], [m].[Phone], [m].[Region], [m].[PostalCode], [m0].[CustomerID], [m0].[EmployeeID], [m0].[Freight], [m0].[OrderDate], [m0].[OrderID], [m0].[RequiredDate], [m0].[ShipAddress], [m0].[ShipCity], [m0].[ShipCountry], [m0].[ShipName], [m0].[ShipPostalCode], [m0].[ShipRegion], [m0].[ShipVia], [m0].[ShippedDate] +FROM ( + SELECT * FROM "Customers" +) AS [m] +CROSS JOIN ( + SELECT * FROM "Orders" WHERE "OrderDate" BETWEEN @p0 AND @p1 +) AS [m0] +WHERE [m].[CustomerID] = [m0].[CustomerID] +"""); + } + + public override async Task SqlQueryRaw_queryable_multiple_composed_with_parameters_and_closure_parameters(bool async) + { + await base.SqlQueryRaw_queryable_multiple_composed_with_parameters_and_closure_parameters(async); + + AssertSql( + """ +p0='London' (Size = 4000) +p1='1997-01-01T00:00:00.0000000' +p2='1998-01-01T00:00:00.0000000' + +SELECT [m].[Address], [m].[City], [m].[CompanyName], [m].[ContactName], [m].[ContactTitle], [m].[Country], [m].[CustomerID], [m].[Fax], [m].[Phone], [m].[Region], [m].[PostalCode], [m0].[CustomerID], [m0].[EmployeeID], [m0].[Freight], [m0].[OrderDate], [m0].[OrderID], [m0].[RequiredDate], [m0].[ShipAddress], [m0].[ShipCity], [m0].[ShipCountry], [m0].[ShipName], [m0].[ShipPostalCode], [m0].[ShipRegion], [m0].[ShipVia], [m0].[ShippedDate] +FROM ( + SELECT * FROM "Customers" WHERE "City" = @p0 +) AS [m] +CROSS JOIN ( + SELECT * FROM "Orders" WHERE "OrderDate" BETWEEN @p1 AND @p2 +) AS [m0] +WHERE [m].[CustomerID] = [m0].[CustomerID] +""", + // + """ +p0='Berlin' (Size = 4000) +p1='1998-04-01T00:00:00.0000000' +p2='1998-05-01T00:00:00.0000000' + +SELECT [m].[Address], [m].[City], [m].[CompanyName], [m].[ContactName], [m].[ContactTitle], [m].[Country], [m].[CustomerID], [m].[Fax], [m].[Phone], [m].[Region], [m].[PostalCode], [m0].[CustomerID], [m0].[EmployeeID], [m0].[Freight], [m0].[OrderDate], [m0].[OrderID], [m0].[RequiredDate], [m0].[ShipAddress], [m0].[ShipCity], [m0].[ShipCountry], [m0].[ShipName], [m0].[ShipPostalCode], [m0].[ShipRegion], [m0].[ShipVia], [m0].[ShippedDate] +FROM ( + SELECT * FROM "Customers" WHERE "City" = @p0 +) AS [m] +CROSS JOIN ( + SELECT * FROM "Orders" WHERE "OrderDate" BETWEEN @p1 AND @p2 +) AS [m0] +WHERE [m].[CustomerID] = [m0].[CustomerID] +"""); + } + + public override async Task SqlQueryRaw_queryable_multiple_line_query(bool async) + { + await base.SqlQueryRaw_queryable_multiple_line_query(async); + + AssertSql( + """ +SELECT * +FROM "Customers" +WHERE "City" = 'London' +"""); + } + + public override async Task SqlQueryRaw_queryable_composed_multiple_line_query(bool async) + { + await base.SqlQueryRaw_queryable_composed_multiple_line_query(async); + + AssertSql( + """ +SELECT [m].[Address], [m].[City], [m].[CompanyName], [m].[ContactName], [m].[ContactTitle], [m].[Country], [m].[CustomerID], [m].[Fax], [m].[Phone], [m].[Region], [m].[PostalCode] +FROM ( + SELECT * + FROM "Customers" +) AS [m] +WHERE [m].[City] = N'London' +"""); + } + + public override async Task SqlQueryRaw_queryable_with_parameters(bool async) + { + await base.SqlQueryRaw_queryable_with_parameters(async); + + AssertSql( + """ +p0='London' (Size = 4000) +p1='Sales Representative' (Size = 4000) + +SELECT * FROM "Customers" WHERE "City" = @p0 AND "ContactTitle" = @p1 +"""); + } + + public override async Task SqlQueryRaw_queryable_with_parameters_inline(bool async) + { + await base.SqlQueryRaw_queryable_with_parameters_inline(async); + + AssertSql( + """ +p0='London' (Size = 4000) +p1='Sales Representative' (Size = 4000) + +SELECT * FROM "Customers" WHERE "City" = @p0 AND "ContactTitle" = @p1 +"""); + } + + public override async Task SqlQuery_queryable_with_parameters_interpolated(bool async) + { + await base.SqlQuery_queryable_with_parameters_interpolated(async); + + AssertSql( + """ +p0='London' (Size = 4000) +p1='Sales Representative' (Size = 4000) + +SELECT * FROM "Customers" WHERE "City" = @p0 AND "ContactTitle" = @p1 +"""); + } + + public override async Task SqlQuery_queryable_with_parameters_inline_interpolated(bool async) + { + await base.SqlQuery_queryable_with_parameters_inline_interpolated(async); + + AssertSql( + """ +p0='London' (Size = 4000) +p1='Sales Representative' (Size = 4000) + +SELECT * FROM "Customers" WHERE "City" = @p0 AND "ContactTitle" = @p1 +"""); + } + + public override async Task SqlQuery_queryable_multiple_composed_with_parameters_and_closure_parameters_interpolated( + bool async) + { + await base.SqlQuery_queryable_multiple_composed_with_parameters_and_closure_parameters_interpolated(async); + + AssertSql( + """ +p0='London' (Size = 4000) +p1='1997-01-01T00:00:00.0000000' +p2='1998-01-01T00:00:00.0000000' + +SELECT [m].[Address], [m].[City], [m].[CompanyName], [m].[ContactName], [m].[ContactTitle], [m].[Country], [m].[CustomerID], [m].[Fax], [m].[Phone], [m].[Region], [m].[PostalCode], [m0].[CustomerID], [m0].[EmployeeID], [m0].[Freight], [m0].[OrderDate], [m0].[OrderID], [m0].[RequiredDate], [m0].[ShipAddress], [m0].[ShipCity], [m0].[ShipCountry], [m0].[ShipName], [m0].[ShipPostalCode], [m0].[ShipRegion], [m0].[ShipVia], [m0].[ShippedDate] +FROM ( + SELECT * FROM "Customers" WHERE "City" = @p0 +) AS [m] +CROSS JOIN ( + SELECT * FROM "Orders" WHERE "OrderDate" BETWEEN @p1 AND @p2 +) AS [m0] +WHERE [m].[CustomerID] = [m0].[CustomerID] +""", + // + """ +p0='Berlin' (Size = 4000) +p1='1998-04-01T00:00:00.0000000' +p2='1998-05-01T00:00:00.0000000' + +SELECT [m].[Address], [m].[City], [m].[CompanyName], [m].[ContactName], [m].[ContactTitle], [m].[Country], [m].[CustomerID], [m].[Fax], [m].[Phone], [m].[Region], [m].[PostalCode], [m0].[CustomerID], [m0].[EmployeeID], [m0].[Freight], [m0].[OrderDate], [m0].[OrderID], [m0].[RequiredDate], [m0].[ShipAddress], [m0].[ShipCity], [m0].[ShipCountry], [m0].[ShipName], [m0].[ShipPostalCode], [m0].[ShipRegion], [m0].[ShipVia], [m0].[ShippedDate] +FROM ( + SELECT * FROM "Customers" WHERE "City" = @p0 +) AS [m] +CROSS JOIN ( + SELECT * FROM "Orders" WHERE "OrderDate" BETWEEN @p1 AND @p2 +) AS [m0] +WHERE [m].[CustomerID] = [m0].[CustomerID] +"""); + } + + public override async Task SqlQueryRaw_queryable_with_null_parameter(bool async) + { + await base.SqlQueryRaw_queryable_with_null_parameter(async); + + AssertSql( + """ +p0=NULL (Nullable = false) + +SELECT * FROM "Employees" WHERE "ReportsTo" = @p0 OR ("ReportsTo" IS NULL AND @p0 IS NULL) +"""); + } + + public override async Task SqlQueryRaw_queryable_with_parameters_and_closure(bool async) + { + var queryString = await base.SqlQueryRaw_queryable_with_parameters_and_closure(async); + + AssertSql( + """ +p0='London' (Size = 4000) +@__contactTitle_1='Sales Representative' (Size = 30) + +SELECT [m].[Address], [m].[City], [m].[CompanyName], [m].[ContactName], [m].[ContactTitle], [m].[Country], [m].[CustomerID], [m].[Fax], [m].[Phone], [m].[Region], [m].[PostalCode] +FROM ( + SELECT * FROM "Customers" WHERE "City" = @p0 +) AS [m] +WHERE [m].[ContactTitle] = @__contactTitle_1 +"""); + + return null; + } + + public override async Task SqlQueryRaw_queryable_simple_cache_key_includes_query_string(bool async) + { + await base.SqlQueryRaw_queryable_simple_cache_key_includes_query_string(async); + + AssertSql( + """ +SELECT * FROM "Customers" WHERE "City" = 'London' +""", + // + """ +SELECT * FROM "Customers" WHERE "City" = 'Seattle' +"""); + } + + public override async Task SqlQueryRaw_queryable_with_parameters_cache_key_includes_parameters(bool async) + { + await base.SqlQueryRaw_queryable_with_parameters_cache_key_includes_parameters(async); + + AssertSql( + """ +p0='London' (Size = 4000) +p1='Sales Representative' (Size = 4000) + +SELECT * FROM "Customers" WHERE "City" = @p0 AND "ContactTitle" = @p1 +""", + // + """ +p0='Madrid' (Size = 4000) +p1='Accounting Manager' (Size = 4000) + +SELECT * FROM "Customers" WHERE "City" = @p0 AND "ContactTitle" = @p1 +"""); + } + + public override async Task SqlQueryRaw_queryable_simple_as_no_tracking_not_composed(bool async) + { + await base.SqlQueryRaw_queryable_simple_as_no_tracking_not_composed(async); + + AssertSql( + """ +SELECT * FROM "Customers" +"""); + } + + public override async Task SqlQueryRaw_queryable_simple_projection_composed(bool async) + { + await base.SqlQueryRaw_queryable_simple_projection_composed(async); + + AssertSql( + """ +SELECT [m].[ProductName] +FROM ( + SELECT * + FROM "Products" + WHERE "Discontinued" <> CAST(1 AS bit) + AND (("UnitsInStock" + "UnitsOnOrder") < "ReorderLevel") +) AS [m] +"""); + } + + public override async Task SqlQueryRaw_annotations_do_not_affect_successive_calls(bool async) + { + await base.SqlQueryRaw_annotations_do_not_affect_successive_calls(async); + + AssertSql( + """ +SELECT * FROM "Customers" WHERE "ContactName" LIKE '%z%' +""", + // + """ +SELECT * FROM [Customers] +"""); + } + + public override async Task SqlQueryRaw_composed_with_nullable_predicate(bool async) + { + await base.SqlQueryRaw_composed_with_nullable_predicate(async); + + AssertSql( +""" +SELECT [m].[Address], [m].[City], [m].[CompanyName], [m].[ContactName], [m].[ContactTitle], [m].[Country], [m].[CustomerID], [m].[Fax], [m].[Phone], [m].[Region], [m].[PostalCode] +FROM ( + SELECT * FROM "Customers" +) AS [m] +WHERE [m].[ContactName] = [m].[CompanyName] +"""); + } + + public override async Task SqlQueryRaw_with_dbParameter(bool async) + { + await base.SqlQueryRaw_with_dbParameter(async); + + AssertSql( + """ +@city='London' (Nullable = false) (Size = 6) + +SELECT * FROM "Customers" WHERE "City" = @city +"""); + } + + public override async Task SqlQueryRaw_with_dbParameter_without_name_prefix(bool async) + { + await base.SqlQueryRaw_with_dbParameter_without_name_prefix(async); + AssertSql( + """ +city='London' (Nullable = false) (Size = 6) + +SELECT * FROM "Customers" WHERE "City" = @city +"""); + } + + public override async Task SqlQueryRaw_with_dbParameter_mixed(bool async) + { + await base.SqlQueryRaw_with_dbParameter_mixed(async); + + AssertSql( + """ +p0='London' (Size = 4000) +@title='Sales Representative' (Nullable = false) (Size = 20) + +SELECT * FROM "Customers" WHERE "City" = @p0 AND "ContactTitle" = @title +""", + // + """ +@city='London' (Nullable = false) (Size = 6) +p1='Sales Representative' (Size = 4000) + +SELECT * FROM "Customers" WHERE "City" = @city AND "ContactTitle" = @p1 +"""); + } + + public override async Task SqlQueryRaw_with_db_parameters_called_multiple_times(bool async) + { + await base.SqlQueryRaw_with_db_parameters_called_multiple_times(async); + + AssertSql( + """ +@id='ALFKI' (Nullable = false) (Size = 5) + +SELECT * FROM "Customers" WHERE "CustomerID" = @id +""", + // + """ +@id='ALFKI' (Nullable = false) (Size = 5) + +SELECT * FROM "Customers" WHERE "CustomerID" = @id +"""); + } + + public override async Task SqlQuery_with_inlined_db_parameter(bool async) + { + await base.SqlQuery_with_inlined_db_parameter(async); + + AssertSql( + """ +@somename='ALFKI' (Nullable = false) (Size = 5) + +SELECT * FROM "Customers" WHERE "CustomerID" = @somename +"""); + } + + public override async Task SqlQuery_with_inlined_db_parameter_without_name_prefix(bool async) + { + await base.SqlQuery_with_inlined_db_parameter_without_name_prefix(async); + + AssertSql( + """ +somename='ALFKI' (Nullable = false) (Size = 5) + +SELECT * FROM "Customers" WHERE "CustomerID" = @somename +"""); + } + + public override async Task SqlQuery_parameterization_issue_12213(bool async) + { + await base.SqlQuery_parameterization_issue_12213(async); + + AssertSql( + """ +p0='10300' + +SELECT [m].[OrderID] +FROM ( + SELECT * FROM "Orders" WHERE "OrderID" >= @p0 +) AS [m] +""", + // + """ +@__max_1='10400' +p0='10300' + +SELECT [m].[OrderID] +FROM ( + SELECT * FROM [Orders] +) AS [m] +WHERE [m].[OrderID] <= @__max_1 AND EXISTS ( + SELECT 1 + FROM ( + SELECT * FROM "Orders" WHERE "OrderID" >= @p0 + ) AS [m0] + WHERE [m0].[OrderID] = [m].[OrderID]) +""", + // + """ +@__max_1='10400' +p0='10300' + +SELECT [m].[OrderID] +FROM ( + SELECT * FROM [Orders] +) AS [m] +WHERE [m].[OrderID] <= @__max_1 AND EXISTS ( + SELECT 1 + FROM ( + SELECT * FROM "Orders" WHERE "OrderID" >= @p0 + ) AS [m0] + WHERE [m0].[OrderID] = [m].[OrderID]) +"""); + } + + public override async Task SqlQueryRaw_does_not_parameterize_interpolated_string(bool async) + { + await base.SqlQueryRaw_does_not_parameterize_interpolated_string(async); + + AssertSql( + """ +p0='10250' + +SELECT * FROM "Orders" WHERE "OrderID" < @p0 +"""); + } + + public override async Task SqlQueryRaw_with_set_operation(bool async) + { + await base.SqlQueryRaw_with_set_operation(async); + + AssertSql( + """ +SELECT [m].[Address], [m].[City], [m].[CompanyName], [m].[ContactName], [m].[ContactTitle], [m].[Country], [m].[CustomerID], [m].[Fax], [m].[Phone], [m].[Region], [m].[PostalCode] +FROM ( + SELECT * FROM "Customers" WHERE "City" = 'London' +) AS [m] +UNION ALL +SELECT [m0].[Address], [m0].[City], [m0].[CompanyName], [m0].[ContactName], [m0].[ContactTitle], [m0].[Country], [m0].[CustomerID], [m0].[Fax], [m0].[Phone], [m0].[Region], [m0].[PostalCode] +FROM ( + SELECT * FROM "Customers" WHERE "City" = 'Berlin' +) AS [m0] +"""); + } + + public override async Task Line_endings_after_Select(bool async) + { + await base.Line_endings_after_Select(async); + + AssertSql( + """ +SELECT [m].[Address], [m].[City], [m].[CompanyName], [m].[ContactName], [m].[ContactTitle], [m].[Country], [m].[CustomerID], [m].[Fax], [m].[Phone], [m].[Region], [m].[PostalCode] +FROM ( + SELECT + * FROM "Customers" +) AS [m] +WHERE [m].[City] = N'Seattle' +"""); + } + + public override async Task SqlQueryRaw_in_subquery_with_dbParameter(bool async) + { + await base.SqlQueryRaw_in_subquery_with_dbParameter(async); + + AssertSql( + """ +@city='London' (Nullable = false) (Size = 6) + +SELECT [m].[CustomerID], [m].[EmployeeID], [m].[Freight], [m].[OrderDate], [m].[OrderID], [m].[RequiredDate], [m].[ShipAddress], [m].[ShipCity], [m].[ShipCountry], [m].[ShipName], [m].[ShipPostalCode], [m].[ShipRegion], [m].[ShipVia], [m].[ShippedDate] +FROM ( + SELECT * FROM "Orders" +) AS [m] +WHERE EXISTS ( + SELECT 1 + FROM ( + SELECT * FROM "Customers" WHERE "City" = @city + ) AS [m0] + WHERE [m0].[CustomerID] = [m].[CustomerID]) +"""); + } + + public override async Task SqlQueryRaw_in_subquery_with_positional_dbParameter_without_name(bool async) + { + await base.SqlQueryRaw_in_subquery_with_positional_dbParameter_without_name(async); + + AssertSql( + """ +p0='London' (Nullable = false) (Size = 6) + +SELECT [m].[CustomerID], [m].[EmployeeID], [m].[Freight], [m].[OrderDate], [m].[OrderID], [m].[RequiredDate], [m].[ShipAddress], [m].[ShipCity], [m].[ShipCountry], [m].[ShipName], [m].[ShipPostalCode], [m].[ShipRegion], [m].[ShipVia], [m].[ShippedDate] +FROM ( + SELECT * FROM "Orders" +) AS [m] +WHERE EXISTS ( + SELECT 1 + FROM ( + SELECT * FROM "Customers" WHERE "City" = @p0 + ) AS [m0] + WHERE [m0].[CustomerID] = [m].[CustomerID]) +"""); + } + + public override async Task SqlQueryRaw_in_subquery_with_positional_dbParameter_with_name(bool async) + { + await base.SqlQueryRaw_in_subquery_with_positional_dbParameter_with_name(async); + + AssertSql( + """ +@city='London' (Nullable = false) (Size = 6) + +SELECT [m].[CustomerID], [m].[EmployeeID], [m].[Freight], [m].[OrderDate], [m].[OrderID], [m].[RequiredDate], [m].[ShipAddress], [m].[ShipCity], [m].[ShipCountry], [m].[ShipName], [m].[ShipPostalCode], [m].[ShipRegion], [m].[ShipVia], [m].[ShippedDate] +FROM ( + SELECT * FROM "Orders" +) AS [m] +WHERE EXISTS ( + SELECT 1 + FROM ( + SELECT * FROM "Customers" WHERE "City" = @city + ) AS [m0] + WHERE [m0].[CustomerID] = [m].[CustomerID]) +"""); + } + + public override async Task SqlQueryRaw_with_dbParameter_mixed_in_subquery(bool async) + { + await base.SqlQueryRaw_with_dbParameter_mixed_in_subquery(async); + + AssertSql( + """ +p0='London' (Size = 4000) +@title='Sales Representative' (Nullable = false) (Size = 20) + +SELECT [m].[CustomerID], [m].[EmployeeID], [m].[Freight], [m].[OrderDate], [m].[OrderID], [m].[RequiredDate], [m].[ShipAddress], [m].[ShipCity], [m].[ShipCountry], [m].[ShipName], [m].[ShipPostalCode], [m].[ShipRegion], [m].[ShipVia], [m].[ShippedDate] +FROM ( + SELECT * FROM "Orders" +) AS [m] +WHERE EXISTS ( + SELECT 1 + FROM ( + SELECT * FROM "Customers" WHERE "City" = @p0 AND "ContactTitle" = @title + ) AS [m0] + WHERE [m0].[CustomerID] = [m].[CustomerID]) +""", + // + """ +@city='London' (Nullable = false) (Size = 6) +p1='Sales Representative' (Size = 4000) + +SELECT [m].[CustomerID], [m].[EmployeeID], [m].[Freight], [m].[OrderDate], [m].[OrderID], [m].[RequiredDate], [m].[ShipAddress], [m].[ShipCity], [m].[ShipCountry], [m].[ShipName], [m].[ShipPostalCode], [m].[ShipRegion], [m].[ShipVia], [m].[ShippedDate] +FROM ( + SELECT * FROM "Orders" +) AS [m] +WHERE EXISTS ( + SELECT 1 + FROM ( + SELECT * FROM "Customers" WHERE "City" = @city AND "ContactTitle" = @p1 + ) AS [m0] + WHERE [m0].[CustomerID] = [m].[CustomerID]) +"""); + } + + public override async Task Multiple_occurrences_of_SqlQuery_with_db_parameter_adds_parameter_only_once(bool async) + { + await base.Multiple_occurrences_of_SqlQuery_with_db_parameter_adds_parameter_only_once(async); + + AssertSql( + """ +city='Seattle' (Nullable = false) (Size = 7) + +SELECT [m].[Address], [m].[City], [m].[CompanyName], [m].[ContactName], [m].[ContactTitle], [m].[Country], [m].[CustomerID], [m].[Fax], [m].[Phone], [m].[Region], [m].[PostalCode] +FROM ( + SELECT * FROM "Customers" WHERE "City" = @city +) AS [m] +INTERSECT +SELECT [m0].[Address], [m0].[City], [m0].[CompanyName], [m0].[ContactName], [m0].[ContactTitle], [m0].[Country], [m0].[CustomerID], [m0].[Fax], [m0].[Phone], [m0].[Region], [m0].[PostalCode] +FROM ( + SELECT * FROM "Customers" WHERE "City" = @city +) AS [m0] +"""); + } + + public override async Task SqlQueryRaw_composed_with_common_table_expression(bool async) + { + var exception = + await Assert.ThrowsAsync(() => base.SqlQueryRaw_composed_with_common_table_expression(async)); + + Assert.Equal(RelationalStrings.FromSqlNonComposable, exception.Message); + } + + protected override DbParameter CreateDbParameter(string name, object value) + => new SqlParameter { ParameterName = name, Value = value }; + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/SqlQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/SqlQuerySqliteTest.cs new file mode 100644 index 00000000000..5470ded8278 --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/Query/SqlQuerySqliteTest.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Data.Sqlite; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class SqlQuerySqliteTest : SqlQueryTestBase> +{ + public SqlQuerySqliteTest(NorthwindQuerySqliteFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task SqlQueryRaw_queryable_composed(bool async) + { + await base.SqlQueryRaw_queryable_composed(async); + + AssertSql( + """ +SELECT "m"."Address", "m"."City", "m"."CompanyName", "m"."ContactName", "m"."ContactTitle", "m"."Country", "m"."CustomerID", "m"."Fax", "m"."Phone", "m"."Region", "m"."PostalCode" +FROM ( + SELECT * FROM "Customers" +) AS "m" +WHERE 'z' = '' OR instr("m"."ContactName", 'z') > 0 +"""); + } + + public override async Task SqlQueryRaw_queryable_with_parameters_and_closure(bool async) + { + var queryString = await base.SqlQueryRaw_queryable_with_parameters_and_closure(async); + + Assert.Equal( + @".param set p0 'London' +.param set @__contactTitle_1 'Sales Representative' + +SELECT ""m"".""Address"", ""m"".""City"", ""m"".""CompanyName"", ""m"".""ContactName"", ""m"".""ContactTitle"", ""m"".""Country"", ""m"".""CustomerID"", ""m"".""Fax"", ""m"".""Phone"", ""m"".""Region"", ""m"".""PostalCode"" +FROM ( + SELECT * FROM ""Customers"" WHERE ""City"" = @p0 +) AS ""m"" +WHERE ""m"".""ContactTitle"" = @__contactTitle_1", queryString, ignoreLineEndingDifferences: true); + + return queryString; + } + + public override Task Bad_data_error_handling_invalid_cast_key(bool async) + // Not supported on SQLite + => Task.CompletedTask; + + public override Task Bad_data_error_handling_invalid_cast(bool async) + // Not supported on SQLite + => Task.CompletedTask; + + public override Task Bad_data_error_handling_invalid_cast_projection(bool async) + // Not supported on SQLite + => Task.CompletedTask; + + public override Task Bad_data_error_handling_invalid_cast_no_tracking(bool async) + // Not supported on SQLite + => Task.CompletedTask; + + public override async Task SqlQueryRaw_composed_with_common_table_expression(bool async) + { + await base.SqlQueryRaw_composed_with_common_table_expression(async); + + AssertSql( + """ +SELECT "m"."Address", "m"."City", "m"."CompanyName", "m"."ContactName", "m"."ContactTitle", "m"."Country", "m"."CustomerID", "m"."Fax", "m"."Phone", "m"."Region", "m"."PostalCode" +FROM ( + WITH "Customers2" AS ( + SELECT * FROM "Customers" + ) + SELECT * FROM "Customers2" +) AS "m" +WHERE 'z' = '' OR instr("m"."ContactName", 'z') > 0 +"""); + } + + protected override DbParameter CreateDbParameter(string name, object value) + => new SqliteParameter { ParameterName = name, Value = value }; + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.Tests/ApiConsistencyTest.cs b/test/EFCore.Tests/ApiConsistencyTest.cs index 1ac67317e30..9066331b399 100644 --- a/test/EFCore.Tests/ApiConsistencyTest.cs +++ b/test/EFCore.Tests/ApiConsistencyTest.cs @@ -145,6 +145,7 @@ protected override void Initialize() typeof(IConventionAnnotatable).GetMethod(nameof(IConventionAnnotatable.SetOrRemoveAnnotation)), typeof(IConventionModelBuilder).GetMethod(nameof(IConventionModelBuilder.HasNoEntityType)), typeof(IReadOnlyEntityType).GetMethod(nameof(IReadOnlyEntityType.GetConcreteDerivedTypesInclusive)), + typeof(IReadOnlyModel).GetMethod(nameof(IReadOnlyModel.GetAdHocEntityTypes)), typeof(IMutableEntityType).GetMethod(nameof(IMutableEntityType.AddData)), typeof(IReadOnlyNavigationBase).GetMethod("get_DeclaringEntityType"), typeof(IReadOnlyNavigationBase).GetMethod("get_TargetEntityType"),