diff --git a/src/BackgroundTasks/uBeac.Core.BackgroundTasks.Logging.MongoDB/FilterBuilderExtensions.cs b/src/BackgroundTasks/uBeac.Core.BackgroundTasks.Logging.MongoDB/FilterBuilderExtensions.cs new file mode 100644 index 0000000..1bcc01e --- /dev/null +++ b/src/BackgroundTasks/uBeac.Core.BackgroundTasks.Logging.MongoDB/FilterBuilderExtensions.cs @@ -0,0 +1,11 @@ +using MongoDB.Bson; +using System.Text.RegularExpressions; +using System.Linq.Expressions; + +namespace MongoDB.Driver; + +internal static class FilterBuilderExtensions +{ + public static FilterDefinition Contains(this FilterDefinitionBuilder builder, Expression> expression, string value) + => builder.Regex(expression, new BsonRegularExpression(new Regex($".*{value}.*", RegexOptions.IgnoreCase))); +} diff --git a/src/BackgroundTasks/uBeac.Core.BackgroundTasks.Logging.MongoDB/Repository.cs b/src/BackgroundTasks/uBeac.Core.BackgroundTasks.Logging.MongoDB/Repository.cs new file mode 100644 index 0000000..107f561 --- /dev/null +++ b/src/BackgroundTasks/uBeac.Core.BackgroundTasks.Logging.MongoDB/Repository.cs @@ -0,0 +1,37 @@ +using MongoDB.Driver; +using uBeac.Repositories.History; +using uBeac.Repositories.MongoDB; + +namespace uBeac.BackgroundTasks.Logging.MongoDB; + +public class MongoBackgroundTaskLogRepository : MongoEntityRepository, IBackgroundTaskLogRepository + where TEntity : BackgroundTaskLog + where TContext : IMongoDBContext +{ + public MongoBackgroundTaskLogRepository(TContext mongoDbContext, IApplicationContext applicationContext, IHistoryManager history) : base(mongoDbContext, applicationContext, history) + { + } + + public async Task> Search(BackgroundTaskSearchRequest request, CancellationToken cancellationToken = default) + { + var builder = Builders.Filter; + + var filter = builder.Empty; + + if (request.FromDate.HasValue) filter &= builder.Gte(x => x.StartDate, request.FromDate); + if (request.ToDate.HasValue) filter &= builder.Lte(x => x.StartDate, request.ToDate); + + if (!string.IsNullOrWhiteSpace(request.Term)) filter &= builder.Contains(x => x.Option.Name, request.Term) | + builder.Contains(x => x.Option.Type, request.Term); + + return (await Collection.FindAsync(filter, null, cancellationToken)).ToList(cancellationToken); + } +} + +public class MongoBackgroundTaskLogRepository : MongoBackgroundTaskLogRepository, IBackgroundTaskLogRepository + where TContext : IMongoDBContext +{ + public MongoBackgroundTaskLogRepository(TContext mongoDbContext, IApplicationContext applicationContext, IHistoryManager history) : base(mongoDbContext, applicationContext, history) + { + } +} diff --git a/src/BackgroundTasks/uBeac.Core.BackgroundTasks.Logging.MongoDB/ServiceCollectionExtensions.cs b/src/BackgroundTasks/uBeac.Core.BackgroundTasks.Logging.MongoDB/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..c44dc07 --- /dev/null +++ b/src/BackgroundTasks/uBeac.Core.BackgroundTasks.Logging.MongoDB/ServiceCollectionExtensions.cs @@ -0,0 +1,15 @@ +using uBeac.BackgroundTasks.Logging.MongoDB; +using uBeac.Repositories.MongoDB; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddMongoBackgroundTaskLogging(this IServiceCollection services) + where TContext : IMongoDBContext + { + services.AddBackgroundTaskLogging>(); + + return services; + } +} diff --git a/src/BackgroundTasks/uBeac.Core.BackgroundTasks.Logging.MongoDB/uBeac.Core.BackgroundTasks.Logging.MongoDB.csproj b/src/BackgroundTasks/uBeac.Core.BackgroundTasks.Logging.MongoDB/uBeac.Core.BackgroundTasks.Logging.MongoDB.csproj new file mode 100644 index 0000000..859f862 --- /dev/null +++ b/src/BackgroundTasks/uBeac.Core.BackgroundTasks.Logging.MongoDB/uBeac.Core.BackgroundTasks.Logging.MongoDB.csproj @@ -0,0 +1,25 @@ + + + + uBeac.BackgroundTasks.Logging.MongoDB + uBeac + ubeac-logo.jpg + https://github.com/ubeac/ubeac-api + net6.0 + enable + disable + + + + + + + + + + True + \ + + + + diff --git a/src/BackgroundTasks/uBeac.Core.BackgroundTasks.Logging/Entity.cs b/src/BackgroundTasks/uBeac.Core.BackgroundTasks.Logging/Entity.cs new file mode 100644 index 0000000..bd7e141 --- /dev/null +++ b/src/BackgroundTasks/uBeac.Core.BackgroundTasks.Logging/Entity.cs @@ -0,0 +1,22 @@ +namespace uBeac.BackgroundTasks.Logging; + +public class BackgroundTaskLog : Entity +{ + public RecurringBackgroundTaskOption Option { get; set; } + + public List Descriptions { get; set; } + + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + + public BackgroundTaskStatus Status { get; set; } + + public int Succeed { get; set; } + public int Failure { get; set; } +} + +public enum BackgroundTaskStatus +{ + Idle = 1, + Running = 2 +} diff --git a/src/BackgroundTasks/uBeac.Core.BackgroundTasks.Logging/Models/BackgroundTaskSearchRequest.cs b/src/BackgroundTasks/uBeac.Core.BackgroundTasks.Logging/Models/BackgroundTaskSearchRequest.cs new file mode 100644 index 0000000..242b486 --- /dev/null +++ b/src/BackgroundTasks/uBeac.Core.BackgroundTasks.Logging/Models/BackgroundTaskSearchRequest.cs @@ -0,0 +1,9 @@ +namespace uBeac.BackgroundTasks.Logging; + +public class BackgroundTaskSearchRequest +{ + public DateTime? FromDate { get; set; } + public DateTime? ToDate { get; set; } + + public string Term { get; set; } +} diff --git a/src/BackgroundTasks/uBeac.Core.BackgroundTasks.Logging/Repository.cs b/src/BackgroundTasks/uBeac.Core.BackgroundTasks.Logging/Repository.cs new file mode 100644 index 0000000..a024c45 --- /dev/null +++ b/src/BackgroundTasks/uBeac.Core.BackgroundTasks.Logging/Repository.cs @@ -0,0 +1,13 @@ +using uBeac.Repositories; + +namespace uBeac.BackgroundTasks.Logging; + +public interface IBackgroundTaskLogRepository : IEntityRepository + where TEntity : BackgroundTaskLog +{ + Task> Search(BackgroundTaskSearchRequest request, CancellationToken cancellationToken = default); +} + +public interface IBackgroundTaskLogRepository : IBackgroundTaskLogRepository +{ +} diff --git a/src/BackgroundTasks/uBeac.Core.BackgroundTasks.Logging/Service.cs b/src/BackgroundTasks/uBeac.Core.BackgroundTasks.Logging/Service.cs new file mode 100644 index 0000000..c4ceb17 --- /dev/null +++ b/src/BackgroundTasks/uBeac.Core.BackgroundTasks.Logging/Service.cs @@ -0,0 +1,72 @@ +using uBeac.Services; + +namespace uBeac.BackgroundTasks.Logging; + +public interface IBackgroundTaskLogService : IEntityService + where TEntity : BackgroundTaskLog +{ + Task> Search(BackgroundTaskSearchRequest request, CancellationToken cancellationToken = default); + + Task Update(TEntity log, string description, bool? success = null, CancellationToken cancellationToken = default); +} + +public interface IBackgroundTaskLogService : IBackgroundTaskLogService +{ + Task Create(RecurringBackgroundTaskOption option, CancellationToken cancellationToken = default); +} + +public class BackgroundTaskLogService : EntityService, IBackgroundTaskLogService + where TEntity : BackgroundTaskLog +{ + private readonly IBackgroundTaskLogRepository _repository; + + public BackgroundTaskLogService(IBackgroundTaskLogRepository repository) : base(repository) + { + _repository = repository; + } + + public async Task> Search(BackgroundTaskSearchRequest request, CancellationToken cancellationToken = default) + { + var result = await _repository.Search(request, cancellationToken); + + return result.ToListResult(); + } + + public async Task Update(TEntity log, string description, bool? success = null, CancellationToken cancellationToken = default) + { + if (success.HasValue && success.Value) + { + log.Succeed += 1; + } + else if (success.HasValue) + { + log.Failure += 1; + } + + log.Descriptions.Add($"{description} at {DateTime.Now}.\n"); + + await Task.Factory.StartNew(async () => await Update(log, cancellationToken), cancellationToken); + } +} + +public class BackgroundTaskLogService : BackgroundTaskLogService, IBackgroundTaskLogService +{ + public BackgroundTaskLogService(IBackgroundTaskLogRepository repository) : base(repository) + { + } + + public async Task Create(RecurringBackgroundTaskOption option, CancellationToken cancellationToken = default) + { + var log = new BackgroundTaskLog + { + Descriptions = new List { "Background Task is started!" }, + StartDate = DateTime.Now, + Status = BackgroundTaskStatus.Running, + Option = option + }; + + await Task.Factory.StartNew(async () => await Create(log, cancellationToken), cancellationToken); + + return log; + } +} diff --git a/src/BackgroundTasks/uBeac.Core.BackgroundTasks.Logging/ServiceCollectionExtensions.cs b/src/BackgroundTasks/uBeac.Core.BackgroundTasks.Logging/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..e6fb4cb --- /dev/null +++ b/src/BackgroundTasks/uBeac.Core.BackgroundTasks.Logging/ServiceCollectionExtensions.cs @@ -0,0 +1,36 @@ +using uBeac.BackgroundTasks.Logging; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddBackgroundTaskLogging(this IServiceCollection services) + where TEntity : BackgroundTaskLog + where TRepository : class, IBackgroundTaskLogRepository + where TService : class, IBackgroundTaskLogService + { + services.AddScoped, TRepository>(); + services.AddScoped, TService>(); + + return services; + } + + public static IServiceCollection AddBackgroundTaskLogging(this IServiceCollection services) + where TRepository : class, IBackgroundTaskLogRepository + where TService : class, IBackgroundTaskLogService + { + services.AddScoped(); + services.AddScoped(); + + return services; + } + + public static IServiceCollection AddBackgroundTaskLogging(this IServiceCollection services) + where TRepository : class, IBackgroundTaskLogRepository + { + services.AddScoped(); + services.AddScoped(); + + return services; + } +} diff --git a/src/BackgroundTasks/uBeac.Core.BackgroundTasks.Logging/uBeac.Core.BackgroundTasks.Logging.csproj b/src/BackgroundTasks/uBeac.Core.BackgroundTasks.Logging/uBeac.Core.BackgroundTasks.Logging.csproj new file mode 100644 index 0000000..a0572f2 --- /dev/null +++ b/src/BackgroundTasks/uBeac.Core.BackgroundTasks.Logging/uBeac.Core.BackgroundTasks.Logging.csproj @@ -0,0 +1,25 @@ + + + + uBeac.BackgroundTasks.Logging + uBeac + ubeac-logo.jpg + https://github.com/ubeac/ubeac-api + net6.0 + enable + disable + + + + + + + + + + True + \ + + + + diff --git a/src/BackgroundTasks/uBeac.Core.BackgroundTasks/BackgroundTaskManager.cs b/src/BackgroundTasks/uBeac.Core.BackgroundTasks/BackgroundTaskManager.cs new file mode 100644 index 0000000..0086ca0 --- /dev/null +++ b/src/BackgroundTasks/uBeac.Core.BackgroundTasks/BackgroundTaskManager.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.Hosting; + +namespace uBeac.BackgroundTasks; + +// The default behavior of the BackgroundService is that StartAsync calls ExecuteAsync. +// It's a default, the StartAsync is virtual so you could override it. +// If you create a subclass of BackgroundService, you must implement ExecuteAsync +// (because it's abstract). That should do your work. +// https://stackoverflow.com/questions/60356396/difference-between-executeasync-and-startasync-methods-in-backgroundservice-net#:~:text=The%20default%20behavior%20of%20the,so%20you%20could%20override%20it.&text=If%20you%20create%20a%20subclass,That%20should%20do%20your%20work. +public class BackgroundTaskManager : BackgroundService +{ + private readonly IEnumerable _backgroundTasks; + + public BackgroundTaskManager(IEnumerable backgroundTasks) + { + _backgroundTasks = backgroundTasks; + } + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + foreach (var backgroundTask in _backgroundTasks) + { + await backgroundTask.Start(cancellationToken); + } + } + + public override async Task StopAsync(CancellationToken cancellationToken) + { + foreach (var backgroundTask in _backgroundTasks) + { + await backgroundTask.Stop(cancellationToken); + } + await base.StopAsync(cancellationToken); + } + + public override void Dispose() + { + foreach (var backgroundTask in _backgroundTasks) + { + backgroundTask.Dispose(); + } + base.Dispose(); + } +} diff --git a/src/BackgroundTasks/uBeac.Core.BackgroundTasks/RecurringBackgroundTask.cs b/src/BackgroundTasks/uBeac.Core.BackgroundTasks/RecurringBackgroundTask.cs new file mode 100644 index 0000000..3f24fa0 --- /dev/null +++ b/src/BackgroundTasks/uBeac.Core.BackgroundTasks/RecurringBackgroundTask.cs @@ -0,0 +1,78 @@ +namespace uBeac.BackgroundTasks; + +public interface IRecurringBackgroundTask : IDisposable +{ + public Task Process(CancellationToken cancellationToken = default); + public Task Start(CancellationToken cancellationToken = default); + public Task Stop(CancellationToken cancellationToken = default); +} + +public abstract class RecurringBackgroundTask : IRecurringBackgroundTask +{ + private Timer _timer = null; + + public RecurringBackgroundTaskOption Option { get; } + + protected RecurringBackgroundTask(RecurringBackgroundTaskOption option) + { + Option = option; + } + + public virtual Task Start(CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + DisableTimer(); + else + EnableTimer(); + + return Task.CompletedTask; + } + + public virtual Task Stop(CancellationToken cancellationToken = default) + { + DisableTimer(); + return Task.CompletedTask; + } + + public abstract Task Process(CancellationToken cancellationToken = default); + + public virtual void Dispose() + { + _timer?.Dispose(); + } + + private void TimerElapsed(object state) + { + Process().Wait(); + } + + private void EnableTimer() + { + var durationToStart = TimeSpan.Zero; + var recurringDuration = Timeout.InfiniteTimeSpan; + + if (Option.Start.HasValue) + { + var startTime = DateTime.Today + Option.Start.Value; + + if (startTime < DateTime.Now && Option.Recurring.HasValue) + { + while (startTime < DateTime.Now) + startTime += Option.Recurring.Value; + } + + durationToStart = (DateTime.Now - startTime).Duration(); + + } + + if (Option.Recurring.HasValue) + recurringDuration = Option.Recurring.Value; + + _timer = new Timer(TimerElapsed, null, durationToStart, recurringDuration); + } + + private void DisableTimer() + { + _timer?.Change(Timeout.Infinite, 0); + } +} diff --git a/src/BackgroundTasks/uBeac.Core.BackgroundTasks/RecurringBackgroundTaskOptions.cs b/src/BackgroundTasks/uBeac.Core.BackgroundTasks/RecurringBackgroundTaskOptions.cs new file mode 100644 index 0000000..ed5ca30 --- /dev/null +++ b/src/BackgroundTasks/uBeac.Core.BackgroundTasks/RecurringBackgroundTaskOptions.cs @@ -0,0 +1,9 @@ +namespace uBeac.BackgroundTasks; + +public class RecurringBackgroundTaskOption +{ + public string Name { get; set; } + public TimeSpan? Start { get; set; } + public TimeSpan? Recurring { get; set; } + public string Type { get; set; } +} diff --git a/src/BackgroundTasks/uBeac.Core.BackgroundTasks/ServiceCollectionExtensions.cs b/src/BackgroundTasks/uBeac.Core.BackgroundTasks/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..d1ab7cf --- /dev/null +++ b/src/BackgroundTasks/uBeac.Core.BackgroundTasks/ServiceCollectionExtensions.cs @@ -0,0 +1,74 @@ +using Microsoft.Extensions.Configuration; +using uBeac.BackgroundTasks; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddBackgroundTasks(this IServiceCollection services, IConfiguration config, string sectionName = "BackgroundTasks") + { + var options = config.GetSection(sectionName).Get>(); + + var serviceDescriptors = new List>(); + + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + + foreach (var option in options) + { + var serviceTypes = assemblies.Select(x => x).Select(x => x.GetType(option.Type)).Where(x => typeof(IRecurringBackgroundTask).IsAssignableFrom(x)).ToList(); + + if (serviceTypes is null || serviceTypes.Count == 0) + throw new Exception(option.Type + " (RecurringBackgroundService) is configured, but does not exist!"); + + if (serviceTypes.Count > 1) + throw new Exception(option.Type + " (RecurringBackgroundService) is configured, but the type name is not unique in all assemblies!"); + + var serviceType = serviceTypes[0]; + + if (!typeof(IRecurringBackgroundTask).IsAssignableFrom(serviceType)) + throw new Exception(option.Type + " (RecurringBackgroundService) is configured, but it is not inherited from RecurringBackgroundService class!"); + + serviceDescriptors.Add(new KeyValuePair(option, serviceType)); + + } + + services.AddSingleton(serviceProvider => + { + var registeredTasks = new List(); + + foreach (var serviceDescriptor in serviceDescriptors) + { + var ctorParams = new List(); + var serviceType = serviceDescriptor.Value; + + var ctors = serviceType.GetConstructors(); + var ctor = ctors[0]; + var ctorParamTypes = ctor.GetParameters(); + + foreach (var param in ctorParamTypes) + { + if (param.ParameterType == typeof(RecurringBackgroundTaskOption)) + { + ctorParams.Add(serviceDescriptor.Key); + continue; + } + + ctorParams.Add(serviceProvider.GetRequiredService(param.ParameterType)); + } + + var serviceInstance = Activator.CreateInstance(serviceType, ctorParams.ToArray()); + + if (serviceInstance == null) + throw new Exception("Unable to create instance of (RecurringBackgroundService): " + serviceType.FullName); + + registeredTasks.Add((IRecurringBackgroundTask)serviceInstance); + } + + return registeredTasks.AsEnumerable(); + }); + + services.AddHostedService(); + + return services; + } +} diff --git a/src/BackgroundTasks/uBeac.Core.BackgroundTasks/uBeac.Core.BackgroundTasks.csproj b/src/BackgroundTasks/uBeac.Core.BackgroundTasks/uBeac.Core.BackgroundTasks.csproj new file mode 100644 index 0000000..f9b11a8 --- /dev/null +++ b/src/BackgroundTasks/uBeac.Core.BackgroundTasks/uBeac.Core.BackgroundTasks.csproj @@ -0,0 +1,29 @@ + + + + uBeac.BackgroundTasks + uBeac + ubeac-logo.jpg + https://github.com/ubeac/ubeac-api + net6.0 + enable + disable + + + + + + + + + + + + + + True + \ + + + + diff --git a/src/uBeac.Core.sln b/src/uBeac.Core.sln index 661b9b2..fdf25c9 100644 --- a/src/uBeac.Core.sln +++ b/src/uBeac.Core.sln @@ -67,6 +67,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "uBeac.Core.Identity.Jwt", " EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{6D83110C-1361-45E1-A638-6BC37F0B968F}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BackgroundTasks", "BackgroundTasks", "{F1E05E96-7182-4991-A055-8F48CB40020E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "uBeac.Core.BackgroundTasks", "BackgroundTasks\uBeac.Core.BackgroundTasks\uBeac.Core.BackgroundTasks.csproj", "{87C6730C-9A5B-446B-BEA4-451F46C372CC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "uBeac.Core.BackgroundTasks.Logging.MongoDB", "BackgroundTasks\uBeac.Core.BackgroundTasks.Logging.MongoDB\uBeac.Core.BackgroundTasks.Logging.MongoDB.csproj", "{005C69E3-6F5F-47B7-89DE-2AFD3C604174}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "uBeac.Core.BackgroundTasks.Logging", "BackgroundTasks\uBeac.Core.BackgroundTasks.Logging\uBeac.Core.BackgroundTasks.Logging.csproj", "{B08C861A-9C6B-4E65-987A-E39E296A7841}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -169,6 +177,18 @@ Global {6BD1B9F9-5A0F-4A43-A657-F8B31896EDE0}.Debug|Any CPU.Build.0 = Debug|Any CPU {6BD1B9F9-5A0F-4A43-A657-F8B31896EDE0}.Release|Any CPU.ActiveCfg = Release|Any CPU {6BD1B9F9-5A0F-4A43-A657-F8B31896EDE0}.Release|Any CPU.Build.0 = Release|Any CPU + {87C6730C-9A5B-446B-BEA4-451F46C372CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {87C6730C-9A5B-446B-BEA4-451F46C372CC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {87C6730C-9A5B-446B-BEA4-451F46C372CC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {87C6730C-9A5B-446B-BEA4-451F46C372CC}.Release|Any CPU.Build.0 = Release|Any CPU + {005C69E3-6F5F-47B7-89DE-2AFD3C604174}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {005C69E3-6F5F-47B7-89DE-2AFD3C604174}.Debug|Any CPU.Build.0 = Debug|Any CPU + {005C69E3-6F5F-47B7-89DE-2AFD3C604174}.Release|Any CPU.ActiveCfg = Release|Any CPU + {005C69E3-6F5F-47B7-89DE-2AFD3C604174}.Release|Any CPU.Build.0 = Release|Any CPU + {B08C861A-9C6B-4E65-987A-E39E296A7841}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B08C861A-9C6B-4E65-987A-E39E296A7841}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B08C861A-9C6B-4E65-987A-E39E296A7841}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B08C861A-9C6B-4E65-987A-E39E296A7841}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -198,6 +218,9 @@ Global {9D7D2782-2067-4B55-990B-ABF03939C1B3} = {EA4A3C91-FB93-49E8-8DF9-8CE86C150F71} {FAAE32CA-48F3-4F26-BF22-39DDDDF57770} = {6D83110C-1361-45E1-A638-6BC37F0B968F} {6BD1B9F9-5A0F-4A43-A657-F8B31896EDE0} = {EA4A3C91-FB93-49E8-8DF9-8CE86C150F71} + {87C6730C-9A5B-446B-BEA4-451F46C372CC} = {F1E05E96-7182-4991-A055-8F48CB40020E} + {005C69E3-6F5F-47B7-89DE-2AFD3C604174} = {F1E05E96-7182-4991-A055-8F48CB40020E} + {B08C861A-9C6B-4E65-987A-E39E296A7841} = {F1E05E96-7182-4991-A055-8F48CB40020E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {247F9BF0-18BB-4A57-BB3E-2870DDAEF0D1}