Exemplo n.º 1
0
        /// <summary>
        /// Sets a property in the server transaction using the specified name and value.
        /// </summary>
        /// <typeparam name="TProperty">The type of the property.</typeparam>
        /// <param name="transaction">The server transaction.</param>
        /// <param name="name">The property name.</param>
        /// <param name="value">The property value.</param>
        /// <returns>The server transaction, so that calls can be easily chained.</returns>
        public static OpenIddictServerTransaction SetProperty <TProperty>(
            this OpenIddictServerTransaction transaction,
            string name, TProperty?value) where TProperty : class
        {
            if (transaction == null)
            {
                throw new ArgumentNullException(nameof(transaction));
            }

            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentException(SR.GetResourceString(SR.ID1105), nameof(name));
            }

            if (value == null)
            {
                transaction.Properties.Remove(name);
            }

            else
            {
                transaction.Properties[name] = value;
            }

            return(transaction);
        }
Exemplo n.º 2
0
    /// <summary>
    /// Initializes a new OpenIddict message.
    /// </summary>
    /// <param name="parameters">The message parameters.</param>
    /// <remarks>Parameters with a null or empty key are always ignored.</remarks>
    public OpenIddictMessage(JsonElement parameters)
    {
        if (parameters.ValueKind != JsonValueKind.Object)
        {
            throw new ArgumentException(SR.GetResourceString(SR.ID0189), nameof(parameters));
        }

        foreach (var parameter in parameters.EnumerateObject())
        {
            // Ignore parameters whose name is null or empty.
            if (string.IsNullOrEmpty(parameter.Name))
            {
                continue;
            }

            // While generally discouraged, JSON objects can contain multiple properties with
            // the same name. In this case, the last occurrence replaces the previous ones.
            if (HasParameter(parameter.Name))
            {
                RemoveParameter(parameter.Name);
            }

            AddParameter(parameter.Name, parameter.Value);
        }
    }
Exemplo n.º 3
0
        /// <summary>
        /// Resolves the <see cref="IServiceProvider"/> instance from the OWIN context
        /// and creates a new instance of the <see cref="OpenIddictValidationOwinMiddleware"/> class,
        /// which is used to register <see cref="OpenIddictValidationOwinHandler"/> in the pipeline.
        /// </summary>
        /// <param name="context">The <see cref="IOwinContext"/>.</param>
        /// <returns>
        /// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
        /// </returns>
        public override Task Invoke(IOwinContext context)
        {
            if (context is null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            var provider = context.Get <IServiceProvider>(typeof(IServiceProvider).FullName);

            if (provider is null)
            {
                throw new InvalidOperationException(SR.GetResourceString(SR.ID0168));
            }

            // Note: the Microsoft.Extensions.DependencyInjection container doesn't support resolving services
            // with arbitrary parameters, which prevents the validation OWIN middleware from being resolved directly
            // from the DI container, as the next middleware in the pipeline cannot be specified as a parameter.
            // To work around this limitation, the validation OWIN middleware is manually instantiated and invoked.
            var middleware = new OpenIddictValidationOwinMiddleware(
                next: Next,
                options: GetRequiredService <IOptionsMonitor <OpenIddictValidationOwinOptions> >(provider),
                dispatcher: GetRequiredService <IOpenIddictValidationDispatcher>(provider),
                factory: GetRequiredService <IOpenIddictValidationFactory>(provider));

            return(middleware.Invoke(context));
Exemplo n.º 4
0
                /// <inheritdoc/>
                public async ValueTask HandleAsync(PrepareIntrospectionRequestContext context)
                {
                    if (context is null)
                    {
                        throw new ArgumentNullException(nameof(context));
                    }

                    Debug.Assert(context.Request is not null, SR.GetResourceString(SR.ID4008));

                    // This handler only applies to System.Net.Http requests. If the HTTP request cannot be resolved,
                    // this may indicate that the request was incorrectly processed by another client stack.
                    var request = context.Transaction.GetHttpRequestMessage();

                    if (request is null)
                    {
                        throw new InvalidOperationException(SR.GetResourceString(SR.ID0173));
                    }

                    // If no client identifier was attached to the request, skip the following logic.
                    if (string.IsNullOrEmpty(context.Request.ClientId))
                    {
                        return;
                    }

                    var configuration = await context.Options.ConfigurationManager.GetConfigurationAsync(default) ??
        /// <inheritdoc/>
        public void Configure(QuartzOptions options)
        {
            if (options is null)
            {
                throw new ArgumentNullException(nameof(options));
            }

            options.AddJob <OpenIddictQuartzJob>(builder =>
            {
                builder.StoreDurably()
                .WithIdentity(OpenIddictQuartzJob.Identity)
                .WithDescription(SR.GetResourceString(SR.ID8000));
            });

            options.AddTrigger(builder =>
            {
                // Note: this trigger uses a quite long interval (1 hour), which means it may be potentially
                // never reached if the application is shut down or recycled. As such, this trigger is set up
                // to fire 2 minutes after the application starts to ensure it's executed at least once.
                builder.ForJob(OpenIddictQuartzJob.Identity)
                .WithSimpleSchedule(options => options.WithIntervalInHours(1).RepeatForever())
                .WithDescription(SR.GetResourceString(SR.ID8001))
                .StartAt(DateBuilder.FutureDate(2, IntervalUnit.Minute));
            });
        }
Exemplo n.º 6
0
        public void SetParameter_ThrowsAnExceptionForNullOrEmptyName(string name)
        {
            // Arrange
            var message = new OpenIddictMessage();

            // Act and assert
            var exception = Assert.Throws <ArgumentException>(() => message.SetParameter(name, null));

            Assert.Equal("name", exception.ParamName);
            Assert.StartsWith(SR.GetResourceString(SR.ID0190), exception.Message);
        }
Exemplo n.º 7
0
        public void Constructor_ThrowsAnExceptionForInvalidJsonElement()
        {
            // Arrange, act and assert
            var exception = Assert.Throws <ArgumentException>(delegate
            {
                return(new OpenIddictMessage(JsonSerializer.Deserialize <JsonElement>("[0,1,2,3]")));
            });

            Assert.Equal("parameters", exception.ParamName);
            Assert.StartsWith(SR.GetResourceString(SR.ID0189), exception.Message);
        }
Exemplo n.º 8
0
    public void GetUnnamedParameter_ThrowsAnExceptionForNegativeIndex()
    {
        // Arrange
        var parameter = new OpenIddictParameter();

        // Act
        var exception = Assert.Throws <ArgumentOutOfRangeException>(() => parameter.GetUnnamedParameter(-1));

        // Assert
        Assert.Equal("index", exception.ParamName);
        Assert.StartsWith(SR.GetResourceString(SR.ID0193), exception.Message);
    }
        public void SetMaximumRefireCount_ThrowsAnExceptionForNegativeCount()
        {
            // Arrange
            var services = CreateServices();
            var builder  = CreateBuilder(services);

            // Act and assert
            var exception = Assert.Throws <ArgumentOutOfRangeException>(() => builder.SetMaximumRefireCount(-1));

            Assert.Equal("count", exception.ParamName);
            Assert.StartsWith(SR.GetResourceString(SR.ID1278), exception.Message);
        }
        public void SetMinimumTokenLifespan_ThrowsAnExceptionForNegativeLifespan()
        {
            // Arrange
            var services = CreateServices();
            var builder  = CreateBuilder(services);

            // Act and assert
            var exception = Assert.Throws <ArgumentOutOfRangeException>(() => builder.SetMinimumTokenLifespan(TimeSpan.FromSeconds(-1)));

            Assert.Equal("lifespan", exception.ParamName);
            Assert.StartsWith(SR.GetResourceString(SR.ID1279), exception.Message);
        }
Exemplo n.º 11
0
        /// <summary>
        /// Ensures that the authentication configuration is in a consistent and valid state.
        /// </summary>
        /// <param name="name">The name of the options instance to configure, if applicable.</param>
        /// <param name="options">The options instance to initialize.</param>
        public void PostConfigure(string name, AuthenticationOptions options)
        {
            if (options == null)
            {
                throw new ArgumentNullException(nameof(options));
            }

            if (!TryValidate(options.SchemeMap, options.DefaultSignInScheme) ||
                !TryValidate(options.SchemeMap, options.DefaultSignOutScheme))
            {
                throw new InvalidOperationException(SR.GetResourceString(SR.ID1164));
            }
        public void PostConfigure(string name, OpenIddictServerOwinOptions options)
        {
            if (options is null)
            {
                throw new ArgumentNullException(nameof(options));
            }

            if (options.AuthenticationMode == AuthenticationMode.Active)
            {
                throw new InvalidOperationException(SR.GetResourceString(SR.ID0119));
            }
        }
Exemplo n.º 13
0
    public void TryGetNamedParameter_ThrowsAnExceptionForNullOrEmptyName(string name)
    {
        // Arrange
        var parameter = new OpenIddictParameter();

        // Act
        var exception = Assert.Throws <ArgumentException>(() => parameter.TryGetNamedParameter(name, out _));

        // Assert
        Assert.Equal("name", exception.ParamName);
        Assert.StartsWith(SR.GetResourceString(SR.ID0192), exception.Message);
    }
Exemplo n.º 14
0
    public void SetTokensCollectionName_ThrowsAnExceptionForNullOrEmptyCollectionName(string name)
    {
        // Arrange
        var services = CreateServices();
        var builder  = CreateBuilder(services);

        // Act and assert
        var exception = Assert.Throws <ArgumentException>(() => builder.SetTokensCollectionName(name));

        Assert.Equal("name", exception.ParamName);
        Assert.StartsWith(SR.GetResourceString(SR.ID0261), exception.Message);
    }
Exemplo n.º 15
0
        /// <summary>
        /// Deserializes an <see cref="OpenIddictMessage"/> instance.
        /// </summary>
        /// <param name="reader">The JSON reader.</param>
        /// <param name="typeToConvert">The type of the deserialized instance.</param>
        /// <param name="options">The JSON serializer options.</param>
        /// <returns>The deserialized <see cref="OpenIddictMessage"/> instance.</returns>
        public override OpenIddictMessage Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (typeToConvert is null)
            {
                throw new ArgumentNullException(nameof(typeToConvert));
            }

            using var document = JsonDocument.ParseValue(ref reader);

            return(typeToConvert == typeof(OpenIddictMessage)  ? new OpenIddictMessage(document.RootElement.Clone()) :
                   typeToConvert == typeof(OpenIddictRequest)  ? new OpenIddictRequest(document.RootElement.Clone()) :
                   typeToConvert == typeof(OpenIddictResponse) ? new OpenIddictResponse(document.RootElement.Clone()) :
                   throw new ArgumentException(SR.GetResourceString(SR.ID0176), nameof(typeToConvert)));
        }
Exemplo n.º 16
0
        public void Constructor_ThrowsAnExceptionForNullOrEmptyParameterNames(string name)
        {
            // Arrange, act and assert
            var exception = Assert.Throws <ArgumentException>(delegate
            {
                return(new OpenIddictMessage(new[]
                {
                    new KeyValuePair <string, OpenIddictParameter>(name, "Fabrikam")
                }));
            });

            Assert.Equal("name", exception.ParamName);
            Assert.StartsWith(SR.GetResourceString(SR.ID0190), exception.Message);
        }
Exemplo n.º 17
0
        /// <summary>
        /// Finds all the base types that matches the specified generic type definition.
        /// </summary>
        /// <param name="type">The type to introspect.</param>
        /// <param name="definition">The generic type definition.</param>
        /// <returns>A <see cref="Type"/> instance if the base type was found, <c>null</c> otherwise.</returns>
        public static IEnumerable <Type> FindGenericBaseTypes(Type type, Type definition)
        {
            if (type is null)
            {
                throw new ArgumentNullException(nameof(type));
            }

            if (definition is null)
            {
                throw new ArgumentNullException(nameof(definition));
            }

            if (!definition.IsGenericTypeDefinition)
            {
                throw new ArgumentException(SR.GetResourceString(SR.ID0263), nameof(definition));
            }

            if (definition.IsInterface)
            {
                foreach (var contract in type.GetInterfaces())
                {
                    if (!contract.IsGenericType && !contract.IsConstructedGenericType)
                    {
                        continue;
                    }

                    if (contract.GetGenericTypeDefinition() == definition)
                    {
                        yield return(contract);
                    }
                }
            }

            else
            {
                for (var candidate = type; candidate is not null; candidate = candidate.BaseType)
                {
                    if (!candidate.IsGenericType && !candidate.IsConstructedGenericType)
                    {
                        continue;
                    }

                    if (candidate.GetGenericTypeDefinition() == definition)
                    {
                        yield return(candidate);
                    }
                }
            }
        }
Exemplo n.º 18
0
        public void Constructor_ThrowsAnExceptionForDuplicateParameters()
        {
            // Arrange, act and assert
            var exception = Assert.Throws <ArgumentException>(delegate
            {
                return(new OpenIddictMessage(new[]
                {
                    new KeyValuePair <string, OpenIddictParameter>("parameter", "Fabrikam"),
                    new KeyValuePair <string, OpenIddictParameter>("parameter", "Contoso")
                }));
            });

            Assert.Equal("name", exception.ParamName);
            Assert.StartsWith(SR.GetResourceString(SR.ID0191), exception.Message);
        }
Exemplo n.º 19
0
        public void UseDbContext_ThrowsAnExceptionForInvalidType()
        {
            // Arrange
            var services = CreateServices();
            var builder  = CreateBuilder(services);

            // Act and assert
            var exception = Assert.Throws <ArgumentException>(delegate
            {
                return(builder.UseDbContext(typeof(object)));
            });

            Assert.Equal("type", exception.ParamName);
            Assert.StartsWith(SR.GetResourceString(SR.ID1231), exception.Message);
        }
        public void Read_ThrowsAnExceptionForUnsupportedType(Type type)
        {
            // Arrange
            var converter = new OpenIddictConverter();

            // Act and assert
            var exception = Assert.Throws <ArgumentException>(delegate
            {
                var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(@"{""name"":""value""}"));
                return(converter.Read(ref reader, type, options: null));
            });

            Assert.StartsWith(SR.GetResourceString(SR.ID0176), exception.Message);
            Assert.Equal("typeToConvert", exception.ParamName);
        }
        /// <summary>
        /// Registers the OpenIddict server Quartz.NET integration in the DI container.
        /// </summary>
        /// <param name="builder">The services builder used by OpenIddict to register new services.</param>
        /// <remarks>This extension can be safely called multiple times.</remarks>
        /// <returns>The <see cref="OpenIddictServerQuartzBuilder"/>.</returns>
        public static OpenIddictServerQuartzBuilder UseQuartz(this OpenIddictServerBuilder builder)
        {
            if (builder == null)
            {
                throw new ArgumentNullException(nameof(builder));
            }

            // Warning: the AddQuartz() method is deliberately not used as it's not idempotent.
            // Calling it at this point may override user-defined services (e.g Quartz DI support).

            builder.Services.TryAddTransient <OpenIddictServerQuartzJob>();

            // To ensure this method can be safely called multiple times, the job details
            // of the OpenIddict server job are only added if no existing IJobDetail instance
            // pointing to OpenIddictServerQuartzJob was already registered in the DI container.
            if (!builder.Services.Any(descriptor => descriptor.ServiceType == typeof(IJobDetail) &&
                                      descriptor.ImplementationInstance is IJobDetail job &&
                                      job.Key.Equals(OpenIddictServerQuartzJob.Identity)))
            {
                builder.Services.AddSingleton(
                    JobBuilder.Create <OpenIddictServerQuartzJob>()
                    .StoreDurably()
                    .WithIdentity(OpenIddictServerQuartzJob.Identity)
                    .WithDescription(SR.GetResourceString(SR.ID9000))
                    .Build());
            }

            // To ensure this method can be safely called multiple times, the trigger details
            // of the OpenIddict server job are only added if no existing ITrigger instance
            // pointing to OpenIddictServerQuartzJob was already registered in the DI container.
            if (!builder.Services.Any(descriptor => descriptor.ServiceType == typeof(ITrigger) &&
                                      descriptor.ImplementationInstance is ITrigger trigger &&
                                      trigger.JobKey.Equals(OpenIddictServerQuartzJob.Identity)))
            {
                // Note: this trigger uses a quite long interval (1 hour), which means it may be
                // potentially never reached if the application is shut down or recycled. As such,
                // this trigger is set up to fire immediately to ensure it's executed at least once.
                builder.Services.AddSingleton(
                    TriggerBuilder.Create()
                    .ForJob(OpenIddictServerQuartzJob.Identity)
                    .WithSimpleSchedule(options => options.WithIntervalInHours(1).RepeatForever())
                    .WithDescription(SR.GetResourceString(SR.ID9001))
                    .StartNow()
                    .Build());
            }

            return(new OpenIddictServerQuartzBuilder(builder.Services));
        }
Exemplo n.º 22
0
            /// <summary>
            /// Adds the type of a handler filter to the filters list.
            /// </summary>
            /// <param name="type">The event handler filter type.</param>
            /// <returns>The builder instance, so that calls can be easily chained.</returns>
            public Builder <TContext> AddFilter(Type type)
            {
                if (type is null)
                {
                    throw new ArgumentNullException(nameof(type));
                }

                if (!typeof(IOpenIddictServerHandlerFilter <>).MakeGenericType(typeof(TContext)).IsAssignableFrom(type))
                {
                    throw new InvalidOperationException(SR.GetResourceString(SR.ID0104));
                }

                _filters.Add(type);

                return(this);
            }
        /// <summary>
        /// Registers the OpenIddict server handler in the global authentication options.
        /// </summary>
        /// <param name="options">The options instance to initialize.</param>
        public void Configure(AuthenticationOptions options)
        {
            if (options is null)
            {
                throw new ArgumentNullException(nameof(options));
            }

            // If a handler was already registered and the type doesn't correspond to the OpenIddict handler, throw an exception.
            if (options.SchemeMap.TryGetValue(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, out var builder) &&
                builder.HandlerType != typeof(OpenIddictServerAspNetCoreHandler))
            {
                throw new InvalidOperationException(SR.GetResourceString(SR.ID0108));
            }

            options.AddScheme <OpenIddictServerAspNetCoreHandler>(
                OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, displayName: null);
        }
Exemplo n.º 24
0
            /// <summary>
            /// Sets the service descriptor.
            /// </summary>
            /// <param name="descriptor">The service descriptor.</param>
            /// <returns>The builder instance, so that calls can be easily chained.</returns>
            public Builder <TContext> SetServiceDescriptor(ServiceDescriptor descriptor)
            {
                if (descriptor is null)
                {
                    throw new ArgumentNullException(nameof(descriptor));
                }

                var type = descriptor.ServiceType;

                if (!typeof(IOpenIddictServerHandler <>).MakeGenericType(typeof(TContext)).IsAssignableFrom(type))
                {
                    throw new InvalidOperationException(SR.GetResourceString(SR.ID0104));
                }

                _descriptor = descriptor;

                return(this);
            }
                /// <inheritdoc/>
                public ValueTask HandleAsync(HandleConfigurationResponseContext context)
                {
                    if (context is null)
                    {
                        throw new ArgumentNullException(nameof(context));
                    }

                    // The issuer returned in the discovery document must exactly match the URL used to access it.
                    // See https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationValidation.
                    var issuer = (string?)context.Response[Metadata.Issuer];

                    if (string.IsNullOrEmpty(issuer))
                    {
                        context.Reject(
                            error: Errors.ServerError,
                            description: SR.GetResourceString(SR.ID2096));

                        return(default);
Exemplo n.º 26
0
            static string?GetKeyIdentifier(SecurityKey key)
            {
                // When no key identifier can be retrieved from the security keys, a value is automatically
                // inferred from the hexadecimal representation of the certificate thumbprint (SHA-1)
                // when the key is bound to a X.509 certificate or from the public part of the signing key.

                if (key is X509SecurityKey x509SecurityKey)
                {
                    return(x509SecurityKey.Certificate.Thumbprint);
                }

                if (key is RsaSecurityKey rsaSecurityKey)
                {
                    // Note: if the RSA parameters are not attached to the signing key,
                    // extract them by calling ExportParameters on the RSA instance.
                    var parameters = rsaSecurityKey.Parameters;
                    if (parameters.Modulus is null)
                    {
                        parameters = rsaSecurityKey.Rsa.ExportParameters(includePrivateParameters: false);

                        Debug.Assert(parameters.Modulus is not null, SR.GetResourceString(SR.ID4003));
                    }

                    // Only use the 40 first chars of the base64url-encoded modulus.
                    var identifier = Base64UrlEncoder.Encode(parameters.Modulus);
                    return(identifier.Substring(0, Math.Min(identifier.Length, 40)).ToUpperInvariant());
                }

#if SUPPORTS_ECDSA
                if (key is ECDsaSecurityKey ecsdaSecurityKey)
                {
                    // Extract the ECDSA parameters from the signing credentials.
                    var parameters = ecsdaSecurityKey.ECDsa.ExportParameters(includePrivateParameters: false);

                    Debug.Assert(parameters.Q.X is not null, SR.GetResourceString(SR.ID4004));

                    // Only use the 40 first chars of the base64url-encoded X coordinate.
                    var identifier = Base64UrlEncoder.Encode(parameters.Q.X);
                    return(identifier.Substring(0, Math.Min(identifier.Length, 40)).ToUpperInvariant());
                }
#endif

                return(null);
            }
Exemplo n.º 27
0
                /// <inheritdoc/>
                public async ValueTask HandleAsync(PrepareIntrospectionRequestContext context)
                {
                    if (context == null)
                    {
                        throw new ArgumentNullException(nameof(context));
                    }

                    Debug.Assert(context.Request != null, SR.GetResourceString(SR.ID5008));

                    // This handler only applies to System.Net.Http requests. If the HTTP request cannot be resolved,
                    // this may indicate that the request was incorrectly processed by another client stack.
                    var request = context.Transaction.GetHttpRequestMessage();

                    if (request == null)
                    {
                        throw new InvalidOperationException(SR.GetResourceString(SR.ID1172));
                    }

                    var configuration = await context.Options.ConfigurationManager.GetConfigurationAsync(default) ??
Exemplo n.º 28
0
            /// <inheritdoc/>
            public ValueTask HandleAsync(ApplyLogoutResponseContext context)
            {
                if (context is null)
                {
                    throw new ArgumentNullException(nameof(context));
                }

                // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved,
                // this may indicate that the request was incorrectly processed by another server stack.
                var response = context.Transaction.GetHttpRequest()?.HttpContext.Response;

                if (response is null)
                {
                    throw new InvalidOperationException(SR.GetResourceString(SR.ID0114));
                }

                if (string.IsNullOrEmpty(context.PostLogoutRedirectUri))
                {
                    return(default);
Exemplo n.º 29
0
            /// <summary>
            /// Imports the properties set on the specified descriptor.
            /// </summary>
            /// <param name="descriptor">The existing descriptor properties are copied from.</param>
            /// <remarks>All the properties previously set on this instance are automatically replaced.</remarks>
            /// <returns>The builder instance, so that calls can be easily chained.</returns>
            public Builder <TContext> Import(OpenIddictServerHandlerDescriptor descriptor)
            {
                if (descriptor is null)
                {
                    throw new ArgumentNullException(nameof(descriptor));
                }

                if (descriptor.ContextType != typeof(TContext))
                {
                    throw new InvalidOperationException(SR.GetResourceString(SR.ID0284));
                }

                _descriptor = descriptor.ServiceDescriptor;
                _filters.Clear();
                _filters.AddRange(descriptor.FilterTypes);
                _order = descriptor.Order;
                _type  = descriptor.Type;

                return(this);
            }
Exemplo n.º 30
0
        /// <summary>
        /// Retrieves a property value from the server transaction using the specified name.
        /// </summary>
        /// <typeparam name="TProperty">The type of the property.</typeparam>
        /// <param name="transaction">The server transaction.</param>
        /// <param name="name">The property name.</param>
        /// <returns>The property value or <c>null</c> if it couldn't be found.</returns>
        public static TProperty?GetProperty <TProperty>(
            this OpenIddictServerTransaction transaction, string name) where TProperty : class
        {
            if (transaction == null)
            {
                throw new ArgumentNullException(nameof(transaction));
            }

            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentException(SR.GetResourceString(SR.ID1105), nameof(name));
            }

            if (transaction.Properties.TryGetValue(name, out var property) && property is TProperty result)
            {
                return(result);
            }

            return(null);
        }