Esempio n. 1
0
        private CompareExchangeResult <string> PutEmailReservation(string email, string userId, long index = 0)
        {
            var key = Conventions.CompareExchangeKeyFor(email);
            var newEmailReservation = new PutCompareExchangeValueOperation <string>(key, userId, index);

            return(docStore.Operations.Send(newEmailReservation));
        }
Esempio n. 2
0
        private Task <CompareExchangeResult <string> > CreateUserKeyReservationAsync(string email, string id)
        {
            var compareExchangeKey    = GetCompareExchangeKeyFromEmailOrUserName(email);
            var reserveEmailOperation = new PutCompareExchangeValueOperation <string>(compareExchangeKey, id, 0);

            return(DbSession.Advanced.DocumentStore.Operations.SendAsync(reserveEmailOperation));
        }
Esempio n. 3
0
        /// <summary>
        /// Create a new email reservation with the given id value
        /// </summary>
        /// <param name="email"></param>
        /// <param name="id"></param>
        /// <returns></returns>
        protected virtual Task <CompareExchangeResult <string> > CreateEmailReservationAsync(string email, string id)
        {
            var compareExchangeKey    = Conventions.CompareExchangeKeyFor(email);
            var reserveEmailOperation = new PutCompareExchangeValueOperation <string>(compareExchangeKey, id, 0);

            return(DbSession.Advanced.DocumentStore.Operations.SendAsync(reserveEmailOperation));
        }
        public IActionResult Run(RunParams runParams)
        {
            string cmpXchgKey   = runParams.CmpXchgKey ?? "*****@*****.**";
            string cmpXchgValue = runParams.CmpXchgValue ?? "employee/1-A";

            string result = null;

            #region Demo
            #region Step_1
            var putCmpXchgOperation =
                new PutCompareExchangeValueOperation <string>(cmpXchgKey, cmpXchgValue, 0);

            CompareExchangeResult <string> putCmpXchgResult =
                DocumentStoreHolder.Store.Operations.Send(putCmpXchgOperation);
            #endregion

            #region Step_2
            var success    = putCmpXchgResult.Successful;
            var putValue   = putCmpXchgResult.Value;
            var putVersion = putCmpXchgResult.Index;

            if (success == false)
            {
                result = "Key already exists";
            }
            #endregion

            #region Step_3
            var getCmpXchgOperation =
                new GetCompareExchangeValueOperation <string>(cmpXchgKey);

            CompareExchangeValue <string> getCmpXchgResult =
                DocumentStoreHolder.Store.Operations.Send(getCmpXchgOperation);
            #endregion

            #region Step_4
            var key                 = getCmpXchgResult.Key;
            var currentValue        = getCmpXchgResult.Value;
            var currentValueVersion = getCmpXchgResult.Index;
            var currentMetadata     = getCmpXchgResult.Metadata;
            #endregion
            #endregion

            result = result ?? $"Created a new Compare-Exchange Key: {key}, Value: {currentValue}, Value Version: {currentValueVersion}";
            return(Ok(result));
        }
Esempio n. 5
0
        /// <summary>
        /// Migrates the data model from a previous version of the RavenDB.Identity framework to the v6 model.
        /// This is necessary if you stored users using an RavenDB.Identity version 5 or ealier.
        /// </summary>
        /// <returns></returns>
        public static void MigrateToV6(IDocumentStore docStore)
        {
            var collectionName = docStore.Conventions.FindCollectionName(typeof(IdentityUserByUserName));

            // Step 1: find all the old IdentityByUserName objects.
            var emails = new List <(string userId, string email)>(1000);

            using (var dbSession = docStore.OpenSession())
            {
#pragma warning disable CS0618 // Type or member is obsolete
                var stream = dbSession.Advanced.Stream <IdentityUserByUserName>($"{collectionName}/");
#pragma warning restore CS0618 // Type or member is obsolete
                while (stream.MoveNext())
                {
                    var doc = stream.Current.Document;
                    emails.Add((userId: doc.UserId, email: doc.UserName));
                }
            }

            // Step 2: store each email as a cluster-wide compare/exchange value.
            foreach (var(userId, email) in emails)
            {
                var compareExchangeKey = GetCompareExchangeKeyFromEmail(email);
                var storeOperation     = new PutCompareExchangeValueOperation <string>(compareExchangeKey, userId, 0);
                var storeResult        = docStore.Operations.Send(storeOperation);
                if (!storeResult.Successful)
                {
                    var exception = new Exception($"Unable to migrate to RavenDB.Identity V6. An error occurred while storing the compare/exchange value. Before running this {nameof(MigrateToV6)} again, please delete all compare/exchange values in Raven that begin with {emailReservationKeyPrefix}.");
                    exception.Data.Add("compareExchangeKey", compareExchangeKey);
                    exception.Data.Add("compareExchangeValue", userId);
                    throw exception;
                }
            }

            // Step 3: remove all IdentityUserByUserName objects.
            var operation = docStore
                            .Operations
                            .Send(new DeleteByQueryOperation(new Client.Documents.Queries.IndexQuery
            {
                Query = $"from {collectionName}"
            }));
            operation.WaitForCompletion();
        }
Esempio n. 6
0
        /// <summary>
        /// Update an existing reservation to point to a new UserId
        /// </summary>
        /// <param name="email"></param>
        /// <param name="id"></param>
        /// <returns></returns>
        protected virtual async Task <CompareExchangeResult <string> > UpdateEmailReservationAsync(string email, string id)
        {
            var key   = Conventions.CompareExchangeKeyFor(email);
            var store = DbSession.Advanced.DocumentStore;

            var readResult = await store.Operations.SendAsync(new GetCompareExchangeValueOperation <string>(key));

            if (readResult == null)
            {
                logger.LogError("Failed to get current index for {EmailReservation} to update it to {ReservedFor}", key, id);
                return(new CompareExchangeResult <string>()
                {
                    Successful = false
                });
            }

            var updateEmailUserIdOperation = new PutCompareExchangeValueOperation <string>(key, id, readResult.Index);

            return(await store.Operations.SendAsync(updateEmailUserIdOperation));
        }
Esempio n. 7
0
        /// <summary>
        /// Runs the migration. This can take several minutes if you have thousands of users.
        /// IMPORTANT: backup your database before running this migration; data loss is possible.
        /// </summary>
        /// <returns></returns>
        public void Migrate()
        {
            // This is done in 3 steps:
            // 1. Find all (now obsolete) IdentityByUserName objects. (Formerly, we used these objects to ensure uniqueness.)
            // 2. Create a cmpXchg value for user's email address.
            // 3. Delete the IdentityByUserName collection.

#pragma warning disable CS0618 // Type or member is obsolete
            // Step 1: find all the old IdentityByUserName objects
            var collectionName = docStore.Conventions.GetCollectionName(typeof(IdentityUserByUserName));
            var identityUsers  = this.Stream <IdentityUserByUserName>();
            var emails         = identityUsers.Select(u => (id: u.UserId, email: u.UserName));
#pragma warning restore CS0618 // Type or member is obsolete

            // Step 2: store each email as a compare/exchange value.
            foreach (var(id, email) in emails)
            {
                var compareExchangeKey = Conventions.CompareExchangeKeyFor(email);
                var storeOperation     = new PutCompareExchangeValueOperation <string>(compareExchangeKey, id, 0);
                var storeResult        = docStore.Operations.Send(storeOperation);
                if (!storeResult.Successful)
                {
                    var exception = new Exception($"Unable to migrate to RavenDB.Identity V6. An error occurred while storing the compare/exchange value. Before running {nameof(CompareExchangeUniqueness)} again, please delete all compare/exchange values in Raven that begin with {Conventions.EmailReservationKeyPrefix}.");
                    exception.Data.Add("compareExchangeKey", compareExchangeKey);
                    exception.Data.Add("compareExchangeValue", id);
                    throw exception;
                }
            }

            // Step 3: remove all IdentityUserByUserName objects.
            var operation = docStore
                            .Operations
                            .Send(new DeleteByQueryOperation(new Client.Documents.Queries.IndexQuery
            {
                Query = $"from {collectionName}"
            }));
            operation.WaitForCompletion();
        }
Esempio n. 8
0
        /// <inheritdoc />
        public async Task <IdentityResult> CreateAsync(TUser user, CancellationToken cancellationToken)
        {
            ThrowIfNullDisposedCancelled(user, cancellationToken);

            // Make sure we have a valid email address.
            if (string.IsNullOrWhiteSpace(user.Email))
            {
                throw new ArgumentException("The user's email address can't be null or empty.", nameof(user));
            }

            if (string.IsNullOrEmpty(user.Id))
            {
                var conventions = DbSession.Advanced.DocumentStore.Conventions;
                var entityName  = conventions.GetCollectionName(typeof(TUser));
                var separator   = conventions.IdentityPartsSeparator;
                var id          = $"{entityName}{separator}{user.Email}";
                user.Id = id;
            }

            cancellationToken.ThrowIfCancellationRequested();

            // See if the email address is already taken.
            // We do this using Raven's compare/exchange functionality, which works cluster-wide.
            // https://ravendb.net/docs/article-page/4.1/csharp/client-api/operations/compare-exchange/overview#creating-a-key
            //
            // Try to reserve a new user email
            // Note: This operation takes place outside of the session transaction it is a cluster-wide reservation.
            var compareExchangeKey    = GetCompareExchangeKeyFromEmail(user.Email);
            var reserveEmailOperation = new PutCompareExchangeValueOperation <string>(compareExchangeKey, user.Id, 0);
            var reserveEmailResult    = await DbSession.Advanced.DocumentStore.Operations.SendAsync(reserveEmailOperation);

            if (!reserveEmailResult.Successful)
            {
                return(IdentityResult.Failed(new[]
                {
                    new IdentityError
                    {
                        Code = "DuplicateEmail",
                        Description = $"The email address {user.Email} is already taken."
                    }
                }));
            }

            // This model allows us to lookup a user by name in order to get the id
            await DbSession.StoreAsync(user, cancellationToken);

            // Because this a a cluster-wide operation due to compare/exchange tokens,
            // we need to save changes here; if we can't store the user,
            // we need to roll back the email reservation.
            try
            {
                await DbSession.SaveChangesAsync();
            }
            catch (Exception)
            {
                // The compare/exchange email reservation is cluster-wide, outside of the session scope.
                // We need to manually roll it back.
                await this.DeleteUserEmailReservation(user.Email);

                throw;
            }

            return(IdentityResult.Success);
        }