Unit Testing IServiceCollection Registration - c#

I'm trying to figure out the easiest way to test my Service Registrations method for my framework. I'm creating dynamic services my registration looks like so:
var messageHubConfig = new DynamicHubServiceConfiguration<Message, MessageDTO>();
messageHubConfig.SetDynamicHubOptions<AstootContext>(async (context, dto) =>
{
return await context.ConversationSubscriptions
.Where(x => x.ConversationId == dto.ConversationId
&& x.IsSubscribed)
.Distinct()
.Select(x => x.User.UniqueIdentifier)
.ToListAsync();
});
messageHubConfig.RequiresDynamicValidator = false;
messageHubConfig.EventMapping.AddCreateEvent(async (sp, obj, dto) =>
{
var conversationService = sp.GetService<IRestEzService<Conversation, ConversationDTO>>();
var conversationDTO = await conversationService.Get(new object[] { dto.ConversationId });
var hubTaskQueue = sp.GetService<IHubServiceTaskQueue>();
hubTaskQueue.QueueDynamicCreate(conversationDTO);
}).When(async (sp, dto) => {
var context = sp.GetService<AstootContext>();
return await context.Conversations.Where(x => x.Id == dto.ConversationId).Where(x => x.Messages.Count == 1).AnyAsync();
});
//Registers service with a hub
restConfiguration.RegisterRestService(typeof(IMessageDTOService),
typeof(MessageDTOService),
messageHubConfig);
Inside of my Register Rest Service Method I have a lot of different services Getting registered e.g:
services.AddTransient(restServiceType, (IServiceProvider serviceProvider) =>
{
var restService = (IRestEzService<TEntity, TDTO>)
ActivatorUtilities.CreateInstance(serviceProvider, restServiceImplementationType);
serviceOption.EventMapping?.Register(serviceProvider, restService);
return restService;
});
How can I be assure that my factory configuration is being registered properly, How can I create a Service Collection for testing?

Create a ServiceCollection,
var services = new ServiceCollection();
call your registration function and then assert that your restServiceType was added.
Next build a provider from the service collection, resolve the restServiceType
var provider = services.BuildServiceProvider();
var restService = provider.GetRequiredService(restServiceType);
and assert that it is created as desired.
The GetRequiredService extension method will throw an exception if the service is unable to resolve the target type.
Now that is based solely on what is currently being shown in your example as I am unaware of any other dependencies.

Based on #Nkosi's answer, a quick test that all Servives are wired up and in a particular order:
// Arrange
var services = new ServiceCollection();
// Act
var provider = services.AddBaseServices(); // Whatever service you have...
// Assert
Assert.AreEqual(27, provider.Count);
// Run this code once and copy the output into this test...
for(int i = 0; i < provider.Count; i++)
{
System.Diagnostics.Debug.WriteLine($"Assert.AreEqual(\"{provider[i].ServiceType.Name}\", provider[{i}].ServiceType.Name);");
}

Related

How to use other Singleton from within builder.Services.Configure() in C#

I'm building a .NET 7 MVC app that uses Azure AD for Authentication but calls out to another API to add additional claims to the Identity.
This worked great when I defined the Claim Transformation statically, but I'd like to register the Claim Transformation as a singleton instead so that it can manage its own token lifetime to the API.
This is what the code looked like to add the claims when the transformation was static:
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));
builder.Services.Configure<MicrosoftIdentityOptions>(
OpenIdConnectDefaults.AuthenticationScheme, opt =>
{
opt.Events.OnTokenValidated = async context =>
{
if (context.Principal != null)
{
context.Principal = await ClaimsAPI.TransformAsync(context.Principal);
}
};
});
This works, but the Claim Transformation class can't store a bearer jwt, and would need to get a fresh one every time, wasting a ton of resources.
this is the closest I've come to getting it to work as a singleton, but it causes plenty of issues
builder.Services.AddSingleton<ICLaimsAPI, ClaimsAPI>();
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));
builder.Services.Configure<MicrosoftIdentityOptions>(
OpenIdConnectDefaults.AuthenticationScheme, opt =>
{
opt.Events.OnTokenValidated = async context =>
{
if (context.Principal != null)
{
context.Principal = await builder.Services.BuildServiceProvider()
.GetRequiredService<IClaimsAPI>()
.TransformAsync(context.Principal);
}
};
});
This generates a seperate copy of each singleton, which doesn't really work for obvious reasons.
How can I inject my service so that it adds the claims correctly?
EDIT: Solved!
I had to do some slight tweaks to #Acegambit's code. here is my working solution for postierity, just in case someone in the future needs to solve a similar problem.
builder.Services.AddSingleton<IClaimsAPI, ClaimsAPI>();
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));
builder.Services.AddOptions().AddSingleton<IConfigureOptions<MicrosoftIdentityOptions>>(provider =>
{
var ClaimsAPI = provider.GetRequiredService<IClaimsAPI>();
return new ConfigureNamedOptions<MicrosoftIdentityOptions>(OpenIdConnectDefaults.AuthenticationScheme, opt =>
{
opt.Events.OnTokenValidated = async context =>
{
if (context.Principal != null)
{
context.Principal = await ClaimsAPI.TransformAsync(context.Principal);
}
};
});
});
This took a little digging into the IServiceCollection extension methods. Looking at the implementation of Configure<TOptions> it really doesn't do a whole lot other than call .AddOptions() and register a singleton of type IConfigureOptions so I think you can pull out that code and do it yourself like so:
builder.Services.AddSingleton<IClaimsAPI, ClaimsAPI>();
builder.Services.AddOptions();
builder.Services.AddSingleton<IConfigureOptions<MicrosoftIdentityOptions>>(provider =>
{
var claimsApi = provider.GetRequiredService<IClaimsAPI>();
return new ConfigureNamedOptions<MicrosoftIdentityOptions>(string.Empty, options =>
{
// TODO: insert your logic to set the context.Principle here
// using the claimsApi that should resolve from the provider above
});
});
There's already an answer but I figure it would be good to show how options has evolved to make this scenario a bit more terse:
builder.Services.AddOptions<MicrosoftIdentityOptions>(OpenIdConnectDefaults.AuthenticationScheme)
.Configure<IClaimsAPI>((options, claimsApi) =>
{
options.Events = new()
{
OnTokenValidated = context =>
{
context.Principal = claimsApi.Transform(context.Principal);
return Task.CompletedTask;
}
};
});

How to configure json Name Case policy while using MinimalApi

I'm trying to get result from my minimal API who configured in endpoints of my MVC web application
my Get action configured like this :
endpoints.MapGet(
"HO-CFDZU4/api/Currency/Get",
[PermissionAuthorize(PermissionName.ReadCurrencyDictionary)]
async ([FromServicesAttribute] CurrencyService curency) =>
{
var result = await DataSourceLoader.LoadAsync(curency.Get(), new DataSourceLoadOptions());
return Results.Ok(result);
});
As result i get response with object where property names changed to lowercase, and its not suit for me.
I want to get exactly same name in same case like i return form action.
To get similar effect in MVC i used this code :
services
.AddMvc()
.AddFluentValidation(x => x.RegisterValidatorsFromAssembly(AppDomain.CurrentDomain.GetAssemblies().Where(x => x.FullName.Contains("ApplicationCore")).Single()))
.AddMvcLocalization()
.AddMvcOptions(options =>{})
.AddRazorRuntimeCompilation()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = null;
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
});
Which setup property naming policy for Json while using action in controllers, and i dont know how to setup same policy for minimalApi.
What Ive tried is to set [JsonPropertyName(name)] And it working good but we have lot of classes and i looking for more global solution.
I also tried configure JsonOptions globally like this:
services.Configure<JsonOptions>(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = null;
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
});
But it do nothing
Use JsonOptions from Microsoft.AspNetCore.Http.Json namespace (docs):
services.Configure<JsonOptions>(options =>
{
options.SerializerOptions.PropertyNamingPolicy = null;
options.SerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
});
UPD
If your application uses both Minimal APIs endpoints and MVC ones, then you try to configure options from both namespaces:
services.Configure<Microsoft.AspNetCore.Http.Json.JsonOptions>(options =>
{
options.SerializerOptions.PropertyNamingPolicy = null;
options.SerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
});
services.Configure<Microsoft.AspNetCore.Mvc.JsonOptions>(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = null;
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
});

Set AutoDelete after when setting receive endpoint after bus has started

I'm using masstransit/rabbitmq in net core 3.1. I have a dispatcher service which will send messages to worker services when they are available. Each worker service has a rabbitmq queue in front which is created when the service starts. I want to make sure that when the worker service stops, then the queue(and exchange) needs to be deleted. I have been able to get it to work when I set flag AutoDelete in the configuration (Program.cs):
services.AddMassTransit(x =>
{
x.AddBus(provider => Bus.Factory.CreateUsingRabbitMq(config =>
{
config.Host(settings.RabbitMq.Host, settings.RabbitMq.Port,
settings.RabbitMq.VirtualHost, h =>
{
h.Username(settings.RabbitMq.Username);
h.Password(settings.RabbitMq.Password);
});
var queueName = AssembleQueueName(settings);
var sp = services.BuildServiceProvider();
config.ReceiveEndpoint(queueName,
e =>
{
e.Consumer(() => new MessageConsumer());
e.AutoDelete = true;
});
}));
});
Unfortunately this does not work for me because I need have the ServiceProvider in my consumer class so therefore I'm doing the following instead (Worker.cs):
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var queueName = _settings.RabbitMq.ServicePrefixQueueName + "-" + _settings.ServiceId;
var messageHandler = _busControl.ConnectReceiveEndpoint(queueName, x =>
{
x.Consumer<MessageConsumer>(_serviceProvider);
});
await messageHandler.Ready;
_workerWitness.IsWorkerReady = true;
}
But here I don't know how to set the AutoDelete flag. Is it even possible?
If you follow the documentation, on configuring consumers with a container, you would see that you can configure your consumers so that they are resolved from the container as shown below (your code, updated to be correct):
services.AddMassTransit(x =>
{
x.AddConsumer<MessageConsumer>();
x.UsingRabbitMq((context, config) =>
{
config.Host(settings.RabbitMq.Host, settings.RabbitMq.Port,
settings.RabbitMq.VirtualHost, h =>
{
h.Username(settings.RabbitMq.Username);
h.Password(settings.RabbitMq.Password);
});
var queueName = AssembleQueueName(settings);
config.ReceiveEndpoint(queueName, e =>
{
e.AutoDelete = true;
e.ConfigureConsumer<MessageConsumer>(context);
});
}));
});

Passing requesting service object as null in .NET Core

My ConfigureService method in startup.cs class
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers().AddNewtonsoftJson();
services.AddSingleton<IConfiguration>(this.Configuration);
// Load settings
var settings = new BotSettings();
Configuration.Bind(settings);
services.AddDbContext<BotDbContext>(options => options.UseSqlServer(settings.ConnectionString));
// Create the credential provider to be used with the Bot Framework Adapter.
services.AddSingleton<ICredentialProvider, ConfigurationCredentialProvider>();
services.AddSingleton<BotAdapter>(sp => (BotFrameworkHttpAdapter)sp.GetService<IBotFrameworkHttpAdapter>());
// Register AuthConfiguration to enable custom claim validation for skills.
services.AddSingleton(sp => new AuthenticationConfiguration { ClaimsValidator = new AllowedCallersClaimsValidator(settings.SkillConfiguration) });
// register components.
ComponentRegistration.Add(new DialogsComponentRegistration());
ComponentRegistration.Add(new DeclarativeComponentRegistration());
ComponentRegistration.Add(new AdaptiveComponentRegistration());
ComponentRegistration.Add(new LanguageGenerationComponentRegistration());
ComponentRegistration.Add(new QnAMakerComponentRegistration());
ComponentRegistration.Add(new LuisComponentRegistration());
// register Handoff
ConfigureHandOff(services, settings);
// This is for custom action component registration.
ComponentRegistration.Add(new CustomActionComponentRegistration());
// Register the skills client and skills request handler.
services.AddSingleton<SkillConversationIdFactoryBase, SkillConversationIdFactory>();
services.AddHttpClient<BotFrameworkClient, SkillHttpClient>();
services.AddSingleton<ChannelServiceHandler, SkillHandler>();
services.AddApplicationInsightsTelemetry(settings?.ApplicationInsights?.InstrumentationKey ?? string.Empty);
services.AddSingleton<ITelemetryInitializer, OperationCorrelationTelemetryInitializer>();
services.AddSingleton<ITelemetryInitializer, TelemetryBotIdInitializer>();
services.AddSingleton<IBotTelemetryClient, BotTelemetryClient>();
services.AddSingleton<TelemetryLoggerMiddleware>(sp =>
{
var telemetryClient = sp.GetService<IBotTelemetryClient>();
return new TelemetryLoggerMiddleware(telemetryClient, logPersonalInformation: settings?.Telemetry?.LogPersonalInformation ?? false);
});
services.AddSingleton<TelemetryInitializerMiddleware>(sp =>
{
var httpContextAccessor = sp.GetService<IHttpContextAccessor>();
var telemetryLoggerMiddleware = sp.GetService<TelemetryLoggerMiddleware>();
return new TelemetryInitializerMiddleware(httpContextAccessor, telemetryLoggerMiddleware, settings?.Telemetry?.LogActivities ?? false);
});
var storage = ConfigureStorage(settings);
services.AddSingleton(storage);
var userState = new UserState(storage);
var conversationState = new ConversationState(storage);
services.AddSingleton(userState);
services.AddSingleton(conversationState);
//Configure bot loading path
var botDir = settings.Bot;
var resourceExplorer = new ResourceExplorer().AddFolder(botDir);
var defaultLocale = Configuration.GetValue<string>("defaultLanguage") ?? "en-us";
var rootDialog = GetRootDialog(botDir);
services.AddSingleton(resourceExplorer);
resourceExplorer.RegisterType<OnQnAMatch>("Microsoft.OnQnAMatch");
services.AddSingleton<IBotFrameworkHttpAdapter, BotFrameworkHttpAdapter>(s =>
GetBotAdapter(storage, settings, userState, conversationState, s));
var removeRecipientMention = settings?.Feature?.RemoveRecipientMention ?? false;
//Adding Required Services
services.AddTransient(typeof(IRepository<>), typeof(Repository<>));
services.AddTransient<IUserService, UserService>();
services.AddTransient<ICommunicationService, CommunicationService>();
services.AddTransient<IMessageService, MessageService>();
services.AddSingleton<IBot>(s =>
new ComposerBot(
s.GetService<IUserService>(),
s.GetService<ConversationState>(),
s.GetService<UserState>(),
s.GetService<MessageRouter>(),
s.GetService<MessageRouterResultHandler>()));
}
However when I am trying to access UserService Object it passing null object in ComposerBot.cs class? What could be the reason?
public ComposerBot(
IUserService userService,
ConversationState conversationState,
UserState userState,
MessageRouter messageRouter,
MessageRouterResultHandler messageRouterResultHandler)
{
this.userService = userService; **showing NULL**
this.conversationState = conversationState;
this.userState = userState;
this.dialogState = conversationState.CreateProperty<DialogState>("DialogState");
this.messageRouter = messageRouter;
this.messageRouterResultHandler = messageRouterResultHandler;
}
I think you are running into this issue:
https://github.com/dotnet/aspnetcore/issues/28684.
this is related:
https://github.com/dotnet/aspnetcore/issues/17442
A tempory solution at least for me was to inject the service in a razor page to get the user there and pass the user to the service.
note: this should be a comment but i dont have enough repuation to comment.

IRabbitMqHost autofac registration problem

Need to resolve IRabbitMqHost for adding handler to bus after bus started.
Steps to Reproduce
Register "IRabbitMqHost" in masstransit configuration
Try to resolve "IRabbitMqHost" in autofac
Then "Exception of type 'Autofac.Core.Registration.ComponentNotRegisteredException' was thrown"
builder.Register(context =>
{
var bus = Bus.Factory.CreateUsingRabbitMq(opt =>
{
var result = new List<string>();
Configuration.GetSection("RabbitMq:HostNames").Bind(result);
var host = opt.Host(result[0], Configuration.GetValue<string>("RabbitMq:VirtualHost"), h =>
{
h.Username(Configuration.GetValue<string>("RabbitMq:Username"));
h.Password(Configuration.GetValue<string>("RabbitMq:Password"));
});
builder.Register<IRabbitMqHost>(a => host);
});
return bus;
}).As<IBus>()
.As<IBusControl>();
builder.Build().Resolve<IRabbitMqHost>()
Expected Behavior
Need to resolve IRabbitMqHost which configured in IBus configuration, because need to add handler after bus started.
Need _rabbitMqHost.ConnectReceiveEndpoint()...
Actual Behavior
container.Resolve throws exception belove.
image in https://github.com/MassTransit/MassTransit/issues/1470
When registered to bus in autofac gave delegate function which create bus instance and registers host. But this delegate not invoked yet. After build the containerbuilder and tried to resolve IBus, then it invoke delegate and registered IRabbitMqHost but not built container. Then when i try to solve IRabbitMqHost it not find registered component because added container not built.
this code worked for me;
builder.Register(context =>
{
var bus = Bus.Factory.CreateUsingRabbitMq(opt =>
{
var result = new List<string>();
Configuration.GetSection("RabbitMq:HostNames").Bind(result);
var host = opt.Host(result[0], Configuration.GetValue<string>("RabbitMq:VirtualHost"), h =>
{
h.Username(Configuration.GetValue<string>("RabbitMq:Username"));
h.Password(Configuration.GetValue<string>("RabbitMq:Password"));
});
ContainerBuilder b = new ContainerBuilder();
b.Register<IRabbitMqHost>(a => host).SingleInstance();
b.Update(ApplicationContainer);
});
return bus;
}).As<IBus>()
.As<IBusControl>()
.SingleInstance();
``

Categories

Resources