diff --git a/libraries/Bot.Builder.Community.Storage.EntityFramework/Bot.Builder.Community.Storage.EntityFramework.csproj b/libraries/Bot.Builder.Community.Storage.EntityFramework/Bot.Builder.Community.Storage.EntityFramework.csproj index 07dd6cb3..011d07f7 100644 --- a/libraries/Bot.Builder.Community.Storage.EntityFramework/Bot.Builder.Community.Storage.EntityFramework.csproj +++ b/libraries/Bot.Builder.Community.Storage.EntityFramework/Bot.Builder.Community.Storage.EntityFramework.csproj @@ -1,24 +1,16 @@  - - - netstandard2.0 - Entity Framework based storage for bots created using Microsoft Bot Builder SDK. - Bot Builder Community - Bot Builder Community - https://github.com/BotBuilderCommunity/botbuilder-community-dotnet/blob/master/LICENSE - https://github.com/BotBuilderCommunity/botbuilder-community-dotnet/tree/master/libraries/Bot.Builder.Community.Storage.EntityFramework - 1.0.0 - bot framework, bot builder, azure bot service, storage - 1.0.0 - 1.0.0 - + + net6.0 + enable + enable + - - - - - - + + + + + + diff --git a/libraries/Bot.Builder.Community.Storage.EntityFramework/BotDataContext.cs b/libraries/Bot.Builder.Community.Storage.EntityFramework/BotDataContext.cs index 2ab19059..9cd622d9 100644 --- a/libraries/Bot.Builder.Community.Storage.EntityFramework/BotDataContext.cs +++ b/libraries/Bot.Builder.Community.Storage.EntityFramework/BotDataContext.cs @@ -1,62 +1,64 @@ -using System; +using Bot.Builder.Community.Storage.EntityFramework; using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; -namespace Bot.Builder.Community.Storage.EntityFramework +namespace Bot.Builder.Community.Storage.EntityFramework; + +public class BotDataContext : DbContext { + private string? _connectionString; + /// - /// DbContext for BotDataEntitys + /// Constructor for BotDataContext receiving connectionString /// - public class BotDataContext : DbContext + /// Connection string to use when configuring the options during + public BotDataContext(string connectionString) + : base() { - private string _connectionString; - - /// - /// Constructor for BotDataContext receiving connectionString - /// - /// Connection string to use when configuring the options during - public BotDataContext(string connectionString) - : base() + if (string.IsNullOrEmpty(connectionString)) { - if (string.IsNullOrEmpty(connectionString)) - { - throw new ArgumentNullException(nameof(connectionString)); - } - - _connectionString = connectionString; + throw new ArgumentNullException(nameof(connectionString)); } - /// - /// Constructor for BotDataContext receiving DBContextOptions - /// - /// Options to use for configuration. - public BotDataContext(DbContextOptions options) - : base(options) - { } - - /// - /// BotDataEntity records - /// - public virtual DbSet BotDataEntity { get; set; } + _connectionString = connectionString; + } - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - if (!optionsBuilder.IsConfigured) - { - optionsBuilder.UseSqlServer(_connectionString); - } + /// + /// Constructor for BotDataContext receiving DBContextOptions + /// + /// Options to use for configuration. + public BotDataContext(DbContextOptions options) + : base(options) + { } - base.OnConfiguring(optionsBuilder); - } + /// + /// BotDataEntity records + /// + public DbSet? BotDataEntity { get; set; } - protected override void OnModelCreating(ModelBuilder builder) + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + if (!optionsBuilder.IsConfigured) { - builder.Entity(entity => - { - entity.ToTable(nameof(BotDataEntity)); - entity.HasIndex(e => e.RealId); - entity.HasKey(e => e.Id); - }); + optionsBuilder.UseSqlServer(_connectionString!); } + base.OnConfiguring(optionsBuilder); } + + protected override void OnModelCreating(ModelBuilder builder) + { + builder.Entity(entity => + { + entity.ToTable(nameof(BotDataEntity)); + entity.HasIndex(e => e.RealId); + entity.HasKey(e => e.Id); + }); + } + + } diff --git a/libraries/Bot.Builder.Community.Storage.EntityFramework/BotDataEntity.cs b/libraries/Bot.Builder.Community.Storage.EntityFramework/BotDataEntity.cs index 57119afa..c1eb2532 100644 --- a/libraries/Bot.Builder.Community.Storage.EntityFramework/BotDataEntity.cs +++ b/libraries/Bot.Builder.Community.Storage.EntityFramework/BotDataEntity.cs @@ -1,62 +1,62 @@ -using System; -using System.ComponentModel.DataAnnotations; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; -namespace Bot.Builder.Community.Storage.EntityFramework +namespace Bot.Builder.Community.Storage.EntityFramework; + +[Table("BotDataEntity")] +public class BotDataEntity { /// - /// BotDataEntity representing one bot data record. + /// Constructor for BotDataEntity /// - [Table("BotDataEntity")] - public class BotDataEntity + /// + /// Sets Timestamp to DateTimeOffset.UtfcNow + /// + public BotDataEntity() { - /// - /// Constructor for BotDataEntity - /// - /// - /// Sets Timestamp to DateTimeOffset.UtfcNow - /// - public BotDataEntity() - { - Timestamp = DateTimeOffset.UtcNow; - } - - /// - /// Gets or sets the auto-generated Id/Key. - /// - /// - /// The database generated Id/Key. - /// - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; set; } + Timestamp = DateTimeOffset.UtcNow; + } - /// - /// Gets or sets the un-sanitized Id/Key. - /// - /// - /// The un-sanitized Id/Key. - /// - [MaxLength(1024)] - public string RealId { get; set; } + /// + /// Gets or sets the auto-generated Id/Key. + /// + /// + /// The database generated Id/Key. + /// + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } - /// - /// Gets or sets the persisted object's state. - /// - /// - /// The persisted object's state. - /// - [Column(TypeName = "nvarchar(MAX)")] - public string Document { get; set; } + /// + /// Gets or sets the un-sanitized Id/Key. + /// + /// + /// The un-sanitized Id/Key. + /// + [MaxLength(1024)] + public string? RealId { get; set; } - /// - /// Gets or sets the current timestamp. - /// - /// - /// The current timestamp. - /// - [Required] - [Timestamp] - public DateTimeOffset Timestamp { get; set; } - } + /// + /// Gets or sets the persisted object's state. + /// + /// + /// The persisted object's state. + /// + [Column(TypeName = "nvarchar(MAX)")] + public string? Document { get; set; } + /// + /// Gets or sets the current timestamp. + /// + /// + /// The current timestamp. + /// + [Required] + [Timestamp] + public DateTimeOffset? Timestamp { get; set; } } diff --git a/libraries/Bot.Builder.Community.Storage.EntityFramework/EntityFrameworkStorage.cs b/libraries/Bot.Builder.Community.Storage.EntityFramework/EntityFrameworkStorage.cs index 3642a978..53ef33d2 100644 --- a/libraries/Bot.Builder.Community.Storage.EntityFramework/EntityFrameworkStorage.cs +++ b/libraries/Bot.Builder.Community.Storage.EntityFramework/EntityFrameworkStorage.cs @@ -1,214 +1,217 @@ -using System; +using Microsoft.Bot.Builder; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json; +using System; using System.Collections.Generic; using System.Linq; -using System.Threading; +using System.Text; using System.Threading.Tasks; -using Microsoft.Bot.Builder; -using Microsoft.EntityFrameworkCore; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using System.Transactions; +using Bot.Builder.Community.Storage.EntityFramework; + +namespace Bot.Builder.Community.Storage.EntityFramework; -namespace Bot.Builder.Community.Storage.EntityFramework +public class EntityFrameworkStorage : IStorage { + private static readonly JsonSerializer _jsonSerializer = JsonSerializer.Create(new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }); + + private bool _checkedConnection; + private readonly EntityFrameworkStorageOptions? _storageOptions; + /// - /// Implements an EntityFramework based storage provider for a bot. + /// Initializes a new instance of the class. + /// using the provided connection string. /// - public class EntityFrameworkStorage : IStorage + /// Entity Framework database connection string. + public EntityFrameworkStorage(string connectionString) + : this(new EntityFrameworkStorageOptions() { ConnectionString = connectionString }) { - private static readonly JsonSerializer _jsonSerializer = JsonSerializer.Create(new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }); - - private bool _checkedConnection; - private readonly EntityFrameworkStorageOptions _storageOptions; - - /// - /// Initializes a new instance of the class. - /// using the provided connection string. - /// - /// Entity Framework database connection string. - public EntityFrameworkStorage(string connectionString) - : this(new EntityFrameworkStorageOptions() { ConnectionString = connectionString }) + if (string.IsNullOrEmpty(connectionString)) { - if (string.IsNullOrEmpty(connectionString)) - { - throw new ArgumentNullException(nameof(connectionString)); - } + throw new ArgumentNullException(nameof(connectionString)); + } + } + /// + /// Initializes a new instance of the class. + /// + /// Entity Framework options class. + public EntityFrameworkStorage(EntityFrameworkStorageOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); } - /// - /// Initializes a new instance of the class. - /// - /// Entity Framework options class. - public EntityFrameworkStorage(EntityFrameworkStorageOptions options) + + if (string.IsNullOrEmpty(options.ConnectionString)) { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } + throw new ArgumentNullException(nameof(options.ConnectionString)); + } - if (string.IsNullOrEmpty(options.ConnectionString)) - { - throw new ArgumentNullException(nameof(options.ConnectionString)); - } + _storageOptions = options; + } - _storageOptions = options; + /// + /// Get a BotDataContext will by default use the connection string provided during EntityFrameworkStorage construction. + /// + public BotDataContext GetBotDataContext => new(_storageOptions!.ConnectionString!); + + /// + /// Deletes storage items from storage. + /// + /// keys of the objects to remove from the store. + /// A cancellation token that can be used by other objects + /// or threads to receive notice of cancellation. + /// A task that represents the work queued to execute. + /// + /// + public async Task DeleteAsync(string[] keys, CancellationToken cancellationToken) + { + if (keys == null) + { + throw new ArgumentNullException(nameof(keys)); } - /// - /// Get a BotDataContext will by default use the connection string provided during EntityFrameworkStorage construction. - /// - public virtual BotDataContext GetBotDataContext => new BotDataContext(_storageOptions.ConnectionString); - - /// - /// Deletes storage items from storage. - /// - /// keys of the objects to remove from the store. - /// A cancellation token that can be used by other objects - /// or threads to receive notice of cancellation. - /// A task that represents the work queued to execute. - /// - /// - public async Task DeleteAsync(string[] keys, CancellationToken cancellationToken) + if (keys.Length == 0) { - if (keys == null) - { - throw new ArgumentNullException(nameof(keys)); - } + return; + } - if (keys.Length == 0) - { - return; - } + // Ensure Initialization has been run + await EnsureConnection().ConfigureAwait(false); - // Ensure Initialization has been run - await EnsureConnection().ConfigureAwait(false); + using (var context = GetBotDataContext) + { + context.RemoveRange(context.BotDataEntity!.Where(item => keys.Contains(item.RealId))); + await context.SaveChangesAsync(); + } + } - using (var context = GetBotDataContext) - { - context.RemoveRange(context.BotDataEntity.Where(item => keys.Contains(item.RealId))); - await context.SaveChangesAsync(); - } + /// + /// Reads storage items from storage. + /// + /// keys of the objects to read from the store. + /// A cancellation token that can be used by other objects + /// or threads to receive notice of cancellation. + /// A task that represents the work queued to execute. + /// If the activities are successfully sent, the task result contains + /// the items read, indexed by key. + /// + /// + public async Task> ReadAsync(string[] keys, CancellationToken cancellationToken) + { + if (keys == null) + { + throw new ArgumentNullException(nameof(keys)); } - /// - /// Reads storage items from storage. - /// - /// keys of the objects to read from the store. - /// A cancellation token that can be used by other objects - /// or threads to receive notice of cancellation. - /// A task that represents the work queued to execute. - /// If the activities are successfully sent, the task result contains - /// the items read, indexed by key. - /// - /// - public async Task> ReadAsync(string[] keys, CancellationToken cancellationToken) + if (keys.Length == 0) { - if (keys == null) - { - throw new ArgumentNullException(nameof(keys)); - } + // No keys passed in, no result to return. + return new Dictionary(); + } - if (keys.Length == 0) - { - // No keys passed in, no result to return. - return new Dictionary(); - } + // Ensure we have checked for possible connection issues + await EnsureConnection().ConfigureAwait(false); - // Ensure we have checked for possible connection issues - await EnsureConnection().ConfigureAwait(false); + var storeItems = new Dictionary(keys.Length); - var storeItems = new Dictionary(keys.Length); + using (var database = GetBotDataContext) + { + var query = (from item in database.BotDataEntity + where keys.Any(k => k == item.RealId) + select new { item.RealId, item.Document }); - using (var database = GetBotDataContext) + foreach (var item in query) { - var query = (from item in database.BotDataEntity - where keys.Any(k => k == item.RealId) - select new { item.RealId, item.Document }); - - foreach (var item in query) - { - var jObject = JObject.Parse(item.Document).ToObject(typeof(object), _jsonSerializer); - storeItems.Add(item.RealId, jObject); - } - - return storeItems; + var jObject = JObject.Parse(item.Document).ToObject(typeof(object), _jsonSerializer); + storeItems.Add(item.RealId, jObject!); } + + return storeItems; } + } - /// - /// Writes storage items to storage. - /// - /// The items to write to storage, indexed by key. - /// A cancellation token that can be used by other objects - /// or threads to receive notice of cancellation. - /// A task that represents the work queued to execute. - /// - /// - public async Task WriteAsync(IDictionary changes, CancellationToken cancellationToken) + /// + /// Writes storage items to storage. + /// + /// The items to write to storage, indexed by key. + /// A cancellation token that can be used by other objects + /// or threads to receive notice of cancellation. + /// A task that represents the work queued to execute. + /// + /// + public async Task WriteAsync(IDictionary changes, CancellationToken cancellationToken) + { + if (changes == null) { - if (changes == null) - { - throw new ArgumentNullException(nameof(changes)); - } + throw new ArgumentNullException(nameof(changes)); + } - if (changes.Count == 0) - { - return; - } + if (changes.Count == 0) + { + return; + } - // Ensure Initialization has been run - await EnsureConnection().ConfigureAwait(false); - - using (var context = GetBotDataContext) + // Ensure Initialization has been run + await EnsureConnection().ConfigureAwait(false); + + using (var context = GetBotDataContext) + { + // Begin a transaction using the isolation level provided in Storage Options + var transaction = context.Database.BeginTransaction(); + + +#pragma warning disable CS8714 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint. + var existingItems = context.BotDataEntity!.Where(item => changes.Keys.Contains(item.RealId)).ToDictionary(d => (d as BotDataEntity).RealId); +#pragma warning restore CS8714 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint. + + foreach (var change in changes) { - // Begin a transaction using the isolation level provided in Storage Options - var transaction = context.Database.BeginTransaction(_storageOptions.TransactionIsolationLevel); - - var existingItems = context.BotDataEntity.Where(item => changes.Keys.Contains(item.RealId)).ToDictionary(d => (d as BotDataEntity).RealId); + var json = JObject.FromObject(change.Value, _jsonSerializer); + var existingItem = existingItems.FirstOrDefault(i => i.Key == change.Key).Value; - foreach (var change in changes) + if (existingItem != null) { - var json = JObject.FromObject(change.Value, _jsonSerializer); - var existingItem = existingItems.FirstOrDefault(i => i.Key == change.Key).Value; - - if (existingItem != null) - { - existingItem.Document = json.ToString(Formatting.None); - existingItem.Timestamp = DateTimeOffset.UtcNow; - } - else + existingItem.Document = json.ToString(Formatting.None); + existingItem.Timestamp = DateTimeOffset.UtcNow; + } + else + { + var newItem = new BotDataEntity { - var newItem = new BotDataEntity - { - RealId = change.Key, - Document = json.ToString(Formatting.None), - Timestamp = DateTimeOffset.UtcNow, - }; - await context.BotDataEntity.AddAsync(newItem); - } + RealId = change.Key, + Document = json.ToString(Formatting.None), + Timestamp = DateTimeOffset.UtcNow, + }; + await context.BotDataEntity!.AddAsync(newItem); } - - context.SaveChanges(); - transaction.Commit(); } + + context.SaveChanges(); + transaction.Commit(); } + } - /// - /// Ensures a database connection is possible. - /// - /// - /// Will throw ArgumentException if the database cannot be created to. - /// - private async Task EnsureConnection() + /// + /// Ensures a database connection is possible. + /// + /// + /// Will throw ArgumentException if the database cannot be created to. + /// + private async Task EnsureConnection() + { + // In the steady-state case, we'll already have verified the database is setup. + if (!_checkedConnection) { - // In the steady-state case, we'll already have verified the database is setup. - if (!_checkedConnection) + using (var context = GetBotDataContext) { - using (var context = GetBotDataContext) - { - if (!await context.Database.CanConnectAsync()) - throw new ArgumentException("The sql database defined in the connection has not been created. See https://github.com/BotBuilderCommunity/botbuilder-community-dotnet/tree/master/libraries/Bot.Builder.Community.Storage.EntityFramework"); - } - _checkedConnection = true; + if (!await context.Database.CanConnectAsync()) + throw new ArgumentException("The sql database defined in the connection has not been created. See https://github.com/BotBuilderCommunity/botbuilder-community-dotnet/tree/master/libraries/Bot.Builder.Community.Storage.EntityFramework"); } + _checkedConnection = true; } } + + + } diff --git a/libraries/Bot.Builder.Community.Storage.EntityFramework/EntityFrameworkStorageOptions.cs b/libraries/Bot.Builder.Community.Storage.EntityFramework/EntityFrameworkStorageOptions.cs index bd0d4215..18839d51 100644 --- a/libraries/Bot.Builder.Community.Storage.EntityFramework/EntityFrameworkStorageOptions.cs +++ b/libraries/Bot.Builder.Community.Storage.EntityFramework/EntityFrameworkStorageOptions.cs @@ -1,23 +1,25 @@ -using System.Data; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; -namespace Bot.Builder.Community.Storage.EntityFramework +namespace Bot.Builder.Community.Storage.EntityFramework; + +public class EntityFrameworkStorageOptions { /// - /// Entity Framework Storage Options. + /// Gets or sets the connection string to use while creating BotDataContext. + /// + public string? ConnectionString { get; set; } + + /// + /// Gets or sets the transaction isolation level to use during write operations. /// - public class EntityFrameworkStorageOptions - { - /// - /// Gets or sets the connection string to use while creating BotDataContext. - /// - public string ConnectionString { get; set; } + /// + /// Default IsolationLevel.ReadCommitted + /// + public IsolationLevel TransactionIsolationLevel => IsolationLevel.ReadCommitted; - /// - /// Gets or sets the transaction isolation level to use during write operations. - /// - /// - /// Default IsolationLevel.ReadCommitted - /// - public IsolationLevel TransactionIsolationLevel => IsolationLevel.ReadCommitted; - } } diff --git a/libraries/Bot.Builder.Community.Storage.EntityFramework/EntityFrameworkTranscriptStore.cs b/libraries/Bot.Builder.Community.Storage.EntityFramework/EntityFrameworkTranscriptStore.cs index e422f62e..25a7d50e 100644 --- a/libraries/Bot.Builder.Community.Storage.EntityFramework/EntityFrameworkTranscriptStore.cs +++ b/libraries/Bot.Builder.Community.Storage.EntityFramework/EntityFrameworkTranscriptStore.cs @@ -1,226 +1,226 @@ -using System; -using System.Linq; -using System.Threading.Tasks; +using Bot.Builder.Community.Storage.EntityFramework; using Microsoft.Bot.Builder; using Microsoft.Bot.Schema; using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bot.Builder.Community.Storage.EntityFramework; -namespace Bot.Builder.Community.Storage.EntityFramework +public class EntityFrameworkTranscriptStore : ITranscriptStore { + private static readonly JsonSerializer _jsonSerializer = JsonSerializer.Create(new JsonSerializerSettings() + { + NullValueHandling = NullValueHandling.Ignore, + Formatting = Formatting.Indented, + }); + + private TranscriptStoreOptions _options; + + /// + /// Initializes a new instance of the class. + /// + /// Connection string to connect to Sql Server Storage. + public EntityFrameworkTranscriptStore(string connectionString) + : this(new TranscriptStoreOptions() { ConnectionString = connectionString }) + { + if (string.IsNullOrEmpty(connectionString)) + { + throw new ArgumentNullException(nameof(connectionString)); + } + } + /// - /// The Entity Framework transcript store stores transcripts in Sql Server. + /// Initializes a new instance of the class. /// - /// - /// Each activity is stored as json in the Activity field. - /// - public class EntityFrameworkTranscriptStore : ITranscriptStore + /// Options to use for the Transcript Store + public EntityFrameworkTranscriptStore(TranscriptStoreOptions options) { - private static readonly JsonSerializer _jsonSerializer = JsonSerializer.Create(new JsonSerializerSettings() + if (options == null) { - NullValueHandling = NullValueHandling.Ignore, - Formatting = Formatting.Indented, - }); - - private TranscriptStoreOptions _options; - - /// - /// Initializes a new instance of the class. - /// - /// Connection string to connect to Sql Server Storage. - public EntityFrameworkTranscriptStore(string connectionString) - :this(new TranscriptStoreOptions() { ConnectionString = connectionString}) + throw new ArgumentNullException(nameof(options)); + } + if (string.IsNullOrEmpty(options.ConnectionString)) { - if (string.IsNullOrEmpty(connectionString)) - { - throw new ArgumentNullException(nameof(connectionString)); - } + throw new ArgumentNullException(nameof(options.ConnectionString) + " cannot be empty."); } + _options = options; + } - /// - /// Initializes a new instance of the class. - /// - /// Options to use for the Transcript Store - public EntityFrameworkTranscriptStore(TranscriptStoreOptions options) + /// + /// Get a TranscriptContext will by default use the connection string provided during EntityFrameworkTranscriptStore construction. + /// + public virtual TranscriptContext GetTranscriptContext => new TranscriptContext(_options.ConnectionString!); + + /// + /// Log an activity to the transcript. + /// + /// Activity being logged. + /// A A task that represents the work queued to execute. + public async Task LogActivityAsync(IActivity activity) + { + BotAssert.ActivityNotNull(activity); + + using (var context = GetTranscriptContext) { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - if (string.IsNullOrEmpty(options.ConnectionString)) + var transcript = new TranscriptEntity() { - throw new ArgumentNullException(nameof(options.ConnectionString) + " cannot be empty."); - } - _options = options; + Channel = activity.ChannelId, + Conversation = activity.Conversation.Id, + Activity = JsonConvert.SerializeObject(activity) + }; + await context.Transcript!.AddAsync(transcript); + await context.SaveChangesAsync(); } + } - /// - /// Get a TranscriptContext will by default use the connection string provided during EntityFrameworkTranscriptStore construction. - /// - public virtual TranscriptContext GetTranscriptContext => new TranscriptContext(_options.ConnectionString); - - /// - /// Log an activity to the transcript. - /// - /// Activity being logged. - /// A A task that represents the work queued to execute. - public async Task LogActivityAsync(IActivity activity) + /// + /// Get activities for a conversation (Aka the transcript). + /// + /// Channel Id. + /// Conversation Id. + /// Continuatuation token to page through results. (Id of last record returned) + /// Earliest time to include. + /// PagedResult of activities. + public Task> GetTranscriptActivitiesAsync(string channelId, string conversationId, string? continuationToken = null, DateTimeOffset startDate = default(DateTimeOffset)) + { + if (string.IsNullOrEmpty(channelId)) { - BotAssert.ActivityNotNull(activity); + throw new ArgumentNullException($"missing {nameof(channelId)}"); + } - using (var context = GetTranscriptContext) - { - var transcript = new TranscriptEntity() - { - Channel = activity.ChannelId, - Conversation = activity.Conversation.Id, - Activity = JsonConvert.SerializeObject(activity) - }; - await context.Transcript.AddAsync(transcript); - await context.SaveChangesAsync(); - } + if (string.IsNullOrEmpty(conversationId)) + { + throw new ArgumentNullException($"missing {nameof(conversationId)}"); } - /// - /// Get activities for a conversation (Aka the transcript). - /// - /// Channel Id. - /// Conversation Id. - /// Continuatuation token to page through results. (Id of last record returned) - /// Earliest time to include. - /// PagedResult of activities. - public Task> GetTranscriptActivitiesAsync(string channelId, string conversationId, string continuationToken = null, DateTimeOffset startDate = default(DateTimeOffset)) + int continuationId = 0; + if (!string.IsNullOrEmpty(continuationToken)) { - if (string.IsNullOrEmpty(channelId)) + if (!int.TryParse(continuationToken, out continuationId)) { - throw new ArgumentNullException($"missing {nameof(channelId)}"); + throw new ArgumentException(nameof(continuationToken) + " must be an integer"); } + } + + var pagedResult = new PagedResult(); - if (string.IsNullOrEmpty(conversationId)) + using (var context = GetTranscriptContext) + { + var query = context.Transcript!.Where(t => t.Channel == channelId && t.Conversation == conversationId); + // Filter on startDate, if present + if (startDate != default(DateTimeOffset)) { - throw new ArgumentNullException($"missing {nameof(conversationId)}"); + query = query.Where(t => t.Timestamp >= startDate); } - int continuationId = 0; + // Filter on continuationToken if present if (!string.IsNullOrEmpty(continuationToken)) { - if (!int.TryParse(continuationToken, out continuationId)) - { - throw new ArgumentException(nameof(continuationToken) + " must be an integer"); - } + query = query.Where(t => t.Id > continuationId); } - var pagedResult = new PagedResult(); + var finalItems = query.OrderBy(i => i.Id).Take(_options.PageSize).Select(i => new { i.Id, i.Activity }).ToArray(); + // Take only PageSize, and convert to Activities +#pragma warning disable CS8619 // Nullability of reference types in value doesn't match target type. + pagedResult.Items = finalItems.Select(i => JsonConvert.DeserializeObject(i.Activity!)).ToArray(); +#pragma warning restore CS8619 // Nullability of reference types in value doesn't match target type. - using (var context = GetTranscriptContext) + if (pagedResult.Items.Length == _options.PageSize) { - var query = context.Transcript.Where(t => t.Channel == channelId && t.Conversation == conversationId); - // Filter on startDate, if present - if (startDate != default(DateTimeOffset)) - { - query = query.Where(t => t.Timestamp >= startDate); - } - - // Filter on continuationToken if present - if (!string.IsNullOrEmpty(continuationToken)) - { - query = query.Where(t => t.Id > continuationId); - } - - var finalItems = query.OrderBy(i => i.Id).Take(_options.PageSize).Select(i => new { i.Id, i.Activity }).ToArray(); - // Take only PageSize, and convert to Activities - pagedResult.Items = finalItems.Select(i => JsonConvert.DeserializeObject(i.Activity)).ToArray(); - - if (pagedResult.Items.Length == _options.PageSize) - { - pagedResult.ContinuationToken = finalItems.Last().Id.ToString(); - } + pagedResult.ContinuationToken = finalItems.Last().Id.ToString(); } + } - return Task.FromResult(pagedResult); + return Task.FromResult(pagedResult); + } + + /// + /// List conversations in the channelId. + /// + /// Channel Id. + /// Continuatuation token to page through results. + /// A A task that represents the work queued to execute. + public Task> ListTranscriptsAsync(string channelId, string? continuationToken = default) + { + if (string.IsNullOrEmpty(channelId)) + { + throw new ArgumentNullException($"missing {nameof(channelId)}"); } - /// - /// List conversations in the channelId. - /// - /// Channel Id. - /// Continuatuation token to page through results. - /// A A task that represents the work queued to execute. - public Task> ListTranscriptsAsync(string channelId, string continuationToken = null) + DateTimeOffset continuationDate = default(DateTimeOffset); + if (!string.IsNullOrEmpty(continuationToken)) { - if (string.IsNullOrEmpty(channelId)) + if (!DateTimeOffset.TryParse(continuationToken, out continuationDate)) { - throw new ArgumentNullException($"missing {nameof(channelId)}"); + throw new ArgumentException(nameof(continuationToken) + " must be an DateTimeOffset"); } + } - DateTimeOffset continuationDate = default(DateTimeOffset); + var pagedResult = new PagedResult(); + + using (var context = GetTranscriptContext) + { + var query = context.Transcript!.Where(t => t.Channel == channelId); + + // Get all conversation.ids with thier min Timestamps + var items = (from p in query + group p by p.Conversation into grp + let timestamp = grp.Min(p => p.Timestamp) + let conversationId = grp.Key + + from p in grp + where p.Conversation == conversationId && p.Timestamp == timestamp + select new { p.Conversation, p.Timestamp }); + + // Filter on continuationToken if present if (!string.IsNullOrEmpty(continuationToken)) { - if (!DateTimeOffset.TryParse(continuationToken, out continuationDate)) - { - throw new ArgumentException(nameof(continuationToken) + " must be an DateTimeOffset"); - } + // TODO: what if two activities have the same timestamp??? is that possible??? + items = items.Where(i => i.Timestamp > continuationDate); } - - var pagedResult = new PagedResult(); - using (var context = GetTranscriptContext) + // Take only PageSize, and convert to Transcript Info + var finalItems = items.OrderBy(i => i.Timestamp).Take(_options.PageSize); + pagedResult.Items = finalItems.Select(i => new TranscriptInfo() { ChannelId = channelId, Id = i.Conversation, Created = (DateTimeOffset)i.Timestamp! }).ToArray(); + + // Set ContinuationToken to last date + if (pagedResult.Items.Length == _options.PageSize) { - var query = context.Transcript.Where(t => t.Channel == channelId); - - // Get all conversation.ids with thier min Timestamps - var items = (from p in query - group p by p.Conversation into grp - let timestamp = grp.Min(p => p.Timestamp) - let conversationId = grp.Key - - from p in grp - where p.Conversation == conversationId && p.Timestamp == timestamp - select new { p.Conversation, p.Timestamp }); - - // Filter on continuationToken if present - if (!string.IsNullOrEmpty(continuationToken)) - { - // TODO: what if two activities have the same timestamp??? is that possible??? - items = items.Where(i => i.Timestamp > continuationDate); - } - - // Take only PageSize, and convert to Transcript Info - var finalItems = items.OrderBy(i => i.Timestamp).Take(_options.PageSize); - pagedResult.Items = finalItems.Select(i => new TranscriptInfo() { ChannelId = channelId, Id = i.Conversation, Created = i.Timestamp }).ToArray(); - - // Set ContinuationToken to last date - if (pagedResult.Items.Length == _options.PageSize) - { - pagedResult.ContinuationToken = finalItems.OrderByDescending(i => i.Timestamp).First().Timestamp.ToString(); - } + pagedResult.ContinuationToken = finalItems.OrderByDescending(i => i.Timestamp).First().Timestamp.ToString(); } - - return Task.FromResult(pagedResult); } - /// - /// Delete a specific conversation and all of it's activities. - /// - /// Channel Id where conversation took place. - /// Id of the conversation to delete. - /// A A task that represents the work queued to execute. - public async Task DeleteTranscriptAsync(string channelId, string conversationId) + return Task.FromResult(pagedResult); + } + + /// + /// Delete a specific conversation and all of it's activities. + /// + /// Channel Id where conversation took place. + /// Id of the conversation to delete. + /// A A task that represents the work queued to execute. + public async Task DeleteTranscriptAsync(string channelId, string conversationId) + { + if (string.IsNullOrEmpty(channelId)) { - if (string.IsNullOrEmpty(channelId)) - { - throw new ArgumentNullException($"{nameof(channelId)} should not be null"); - } + throw new ArgumentNullException($"{nameof(channelId)} should not be null"); + } - if (string.IsNullOrEmpty(conversationId)) - { - throw new ArgumentNullException($"{nameof(conversationId)} should not be null"); - } + if (string.IsNullOrEmpty(conversationId)) + { + throw new ArgumentNullException($"{nameof(conversationId)} should not be null"); + } - using (var context = GetTranscriptContext) - { - context.RemoveRange(context.Transcript.Where(item => item.Conversation == conversationId && item.Channel == channelId)); - await context.SaveChangesAsync(); - } + using (var context = GetTranscriptContext) + { + context.RemoveRange(context.Transcript!.Where(item => item.Conversation == conversationId && item.Channel == channelId)); + await context.SaveChangesAsync(); } } + + } diff --git a/libraries/Bot.Builder.Community.Storage.EntityFramework/TranscriptContext.cs b/libraries/Bot.Builder.Community.Storage.EntityFramework/TranscriptContext.cs index 6635b181..500a62f0 100644 --- a/libraries/Bot.Builder.Community.Storage.EntityFramework/TranscriptContext.cs +++ b/libraries/Bot.Builder.Community.Storage.EntityFramework/TranscriptContext.cs @@ -1,56 +1,59 @@ -using System; +using Bot.Builder.Community.Storage.EntityFramework; using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; -namespace Bot.Builder.Community.Storage.EntityFramework +namespace Bot.Builder.Community.Storage.EntityFramework; + +public class TranscriptContext : DbContext { + private string _connectionString; + /// - /// DbContext for TranscriptEntitys + /// Constructor for TranscriptContext receiving connectionString /// - public class TranscriptContext : DbContext + /// Connection string to use when configuring the options during + public TranscriptContext(string connectionString) + : base() { - private string _connectionString; - - /// - /// Constructor for TranscriptContext receiving connectionString - /// - /// Connection string to use when configuring the options during - public TranscriptContext(string connectionString) - : base() + if (string.IsNullOrEmpty(connectionString)) { - if (string.IsNullOrEmpty(connectionString)) - { - throw new ArgumentNullException(nameof(connectionString)); - } - - _connectionString = connectionString; + throw new ArgumentNullException(nameof(connectionString)); } - /// - /// TranscriptEntity records - /// - public virtual DbSet Transcript { get; set; } - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - if (!optionsBuilder.IsConfigured) - { - optionsBuilder.UseSqlServer(_connectionString); - } + _connectionString = connectionString; + } - base.OnConfiguring(optionsBuilder); - } + /// + /// TranscriptEntity records + /// + public DbSet? Transcript { get; set; } - protected override void OnModelCreating(ModelBuilder builder) + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + if (!optionsBuilder.IsConfigured) { - builder.Entity(entity => - { - entity.ToTable(nameof(TranscriptEntity)); - entity.HasIndex(e => e.Conversation); - entity.HasIndex(e => e.Channel); - entity.HasIndex(e => new { e.Channel, e.Conversation }); - entity.HasKey(e => e.Id); - }); + optionsBuilder.UseSqlServer(_connectionString); } + base.OnConfiguring(optionsBuilder); + } + + protected override void OnModelCreating(ModelBuilder builder) + { + builder.Entity(entity => + { + entity.ToTable(nameof(TranscriptEntity)); + entity.HasIndex(e => e.Conversation); + entity.HasIndex(e => e.Channel); + entity.HasIndex(e => new { e.Channel, e.Conversation }); + entity.HasKey(e => e.Id); + }); } + + + } diff --git a/libraries/Bot.Builder.Community.Storage.EntityFramework/TranscriptEntity.cs b/libraries/Bot.Builder.Community.Storage.EntityFramework/TranscriptEntity.cs index 80382384..92e5e50c 100644 --- a/libraries/Bot.Builder.Community.Storage.EntityFramework/TranscriptEntity.cs +++ b/libraries/Bot.Builder.Community.Storage.EntityFramework/TranscriptEntity.cs @@ -1,55 +1,56 @@ -using System; -using System.ComponentModel.DataAnnotations; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; -namespace Bot.Builder.Community.Storage.EntityFramework +namespace Bot.Builder.Community.Storage.EntityFramework; + +[Table("TranscriptEntity")] +public class TranscriptEntity { /// - /// TranscriptEntity representing one Bot Activity record. + /// Constructor for TranscriptEntity /// - [Table("TranscriptEntity")] - public class TranscriptEntity + /// + /// Sets Timestamp to DateTimeOffset.UtfcNow + /// + public TranscriptEntity() { - /// - /// Constructor for TranscriptEntity - /// - /// - /// Sets Timestamp to DateTimeOffset.UtfcNow - /// - public TranscriptEntity() - { - Timestamp = DateTimeOffset.UtcNow; - } + Timestamp = DateTimeOffset.UtcNow; + } - /// - /// Gets or sets the auto-generated Id/Key. - /// - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; set; } + /// + /// Gets or sets the auto-generated Id/Key. + /// + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } - /// - /// Gets or sets the Channel this activity occurred on. - /// - [MaxLength(256)] - public string Channel { get; set; } + /// + /// Gets or sets the Channel this activity occurred on. + /// + [MaxLength(256)] + public string? Channel { get; set; } - /// - /// Gets or sets the Conversation id this activity occurred on. - /// - [MaxLength(1024)] - public string Conversation { get; set; } + /// + /// Gets or sets the Conversation id this activity occurred on. + /// + [MaxLength(1024)] + public string? Conversation { get; set; } - /// - /// Gets or sets the persisted Activity as a string. - /// - [Column(TypeName = "nvarchar(MAX)")] - public string Activity { get; set; } + /// + /// Gets or sets the persisted Activity as a string. + /// + [Column(TypeName = "nvarchar(MAX)")] + public string? Activity { get; set; } - /// - /// Gets or sets the current timestamp. - /// - [Required] - [Timestamp] - public DateTimeOffset Timestamp { get; set; } - } -} + /// + /// Gets or sets the current timestamp. + /// + [Required] + [Timestamp] + public DateTimeOffset? Timestamp { get; set; } +} \ No newline at end of file diff --git a/libraries/Bot.Builder.Community.Storage.EntityFramework/TranscriptStoreOptions.cs b/libraries/Bot.Builder.Community.Storage.EntityFramework/TranscriptStoreOptions.cs index 33c7df79..80e4279a 100644 --- a/libraries/Bot.Builder.Community.Storage.EntityFramework/TranscriptStoreOptions.cs +++ b/libraries/Bot.Builder.Community.Storage.EntityFramework/TranscriptStoreOptions.cs @@ -1,21 +1,26 @@ -namespace Bot.Builder.Community.Storage.EntityFramework +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bot.Builder.Community.Storage.EntityFramework; + + +public class TranscriptStoreOptions { + + /// + /// Gets or sets the connection string to use while creating TranscriptContext. + /// + public string? ConnectionString { get; set; } + /// - /// Entity Framework Transcript Options. + /// Gets or sets the total records to return from the store per page. /// - public class TranscriptStoreOptions - { - /// - /// Gets or sets the connection string to use while creating TranscriptContext. - /// - public string ConnectionString { get; set; } + /// + /// Default 20 + /// + public int PageSize => 20; - /// - /// Gets or sets the total records to return from the store per page. - /// - /// - /// Default 20 - /// - public int PageSize => 20; - } }