I've read whole documentation, tutorial [1], and spend several hours in sources, but I still do not understand how to configure loadbalancer, especially if I don't use magic annotations.
I have following configuration:
#Configuration
public class AppConfig {
public static final String SERVICE_ID = "service";
#Primary
#Bean
public ServiceInstanceListSupplier serviceInstanceListSupplier() {
return ServiceInstanceListSuppliers.from(SERVICE_ID,
new DefaultServiceInstance(SERVICE_ID + "1", SERVICE_ID, "localhost", 8886, false),
new DefaultServiceInstance(SERVICE_ID + "2", SERVICE_ID, "localhost", 8887, false));
}
#Bean
public LoadBalancerClientFactory loadBalancerClientFactory() {
return new LoadBalancerClientFactory();
}
#Bean
public ReactorLoadBalancerExchangeFilterFunction loadBalancerExchangeFilterFunction(LoadBalancerProperties properties) {
return new ReactorLoadBalancerExchangeFilterFunction(loadBalancerClientFactory(), properties);
}
}
and use bean loadBalancerExchangeFilterFunction as:
WebClient.builder()
.baseUrl("http://service/test-consumer")
.filter(lbFunction)
.build();
and it works. The problem is, that it works regardless of what hostname I use. So if I replace hostname "service" with whatever work I like, I will be still sending data to localhost:8886 or localhost:8887.
Can someone explain what is the role of serviceId and how this is paired to collection of DefaultServiceInstance?
(I want to understand the internals, what are the key components, their purpose and their interplay. I'm not primarily looking for magic annotation, but that one actually explained would be also great. Debugging it is really hard, I have several A4 with class diagrams and it still makes no sense at all).
Question: Is there a misconfiguration? What is the purpose of serviceId? It seems that none, as webclient using ReactorLoadBalancerExchangeFilterFunction will create roundrobin loadbalancer over configured ServiceInstances regardless of what is actual hostname used in given webclient.
Question2: how could I create 2 loadbalanced services and control to which service (not node) will request go? Do I need 2 separate webclients or some url pattern(like using serviceId in place of hostname) will do? If I need 2 webclients, how is pairing to DefaultServiceInstance done?
[1] https://spring.io/guides/gs/spring-cloud-loadbalancer/
EDIT:
after suggested update, the configuration looks like:
#Configuration
public class AppConfig {
#Bean
public ServiceInstanceListSupplier instanceSupplier(ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder()
.withDiscoveryClient()
.withHealthChecks()
.build(context);
}
#Bean
public LoadBalancerClientFactory loadBalancerClientFactory() {
return new LoadBalancerClientFactory();
}
#Bean
public ReactorLoadBalancerExchangeFilterFunction loadBalancerExchangeFilterFunction(LoadBalancerProperties properties) {
return new ReactorLoadBalancerExchangeFilterFunction(loadBalancerClientFactory(), properties);
}
}
application.properties contains:
spring.cloud.discovery.client.simple.instances.complicated[0].uri=http://localhost:8886
spring.cloud.discovery.client.simple.instances.complicated[1].uri=http://localhost:8887
webclient call to URL: http://localhost:8888/test-consumer (ie. hostname not matching serviceID) produces:
o.s.c.l.core.RoundRobinLoadBalancer : No servers available for service: localhost
eactorLoadBalancerExchangeFilterFunction : LoadBalancer does not contain an instance for the service localhost
webclient call to URL: http://complicated/test-consumer (ie. hostname matching serviceID) produces:
o.s.c.l.core.RoundRobinLoadBalancer : No servers available for service: complicated
eactorLoadBalancerExchangeFilterFunction : LoadBalancer does not contain an instance for the service complicated
The reason for this is that this.serviceId = environment.getProperty(PROPERTY_NAME); in DiscoveryClientServiceInstanceListSupplier(ReactiveDiscoveryClient,Environment) evaluates as null, thus even though I'm looking for some serviceId, delegate.getInstance is called with null, so no ServiceInstances are found. IF I removed #Bean instanceSupplier, and hope for autoconfiguration do it somehow magically, the this.serviceId = environment.getProperty(PROPERTY_NAME); is somehow magically set, serviceId is propagated correctly, and it works. For calls which leads elsewhere than configured serviceId, it fails saying, that this serviceId is not know, instead of making call.
SO it does mean, that if I configure loadbalancer, I cannot call anything else but (auto)configured services???
The LoadBalancer config should not be in a #Configuration-annotated class; instead, it should be a class passed for config via #LoadBalancerClient or #LoadBalancerClients annotation, as described here.
Also, the only bean you need to instantiate is the ServiceInstanceListSupplier (if you add spring-cloud-starter-loadbalancer, LoadBalancerClientFactory, and ReactorLoadBalancerExchangeFilterFunction will be instantiated by the starter).
So your LoadBalancer configuration class will look like so (without #Configuration):
public class AppConfig {
#Bean
public ServiceInstanceListSupplier instanceSupplier(ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder()
.withDiscoveryClient()
.withHealthChecks()
.build(context);
}
}
and your actual #Configuration class (for example, where you configure other webflux-related beans), will have the following annotation: #LoadBalancerClients(defaultConfiguration = AppConfig.class).
Then, if you enable health-checks in the complicated instances, it should work without any problems.
Finally, able to resolve this issue with the below Configuration. not sure why it works only with Non blocking approach and when we pass the new RestTemplate
#Bean
public ServiceInstanceListSupplier instanceSupplier(ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder()
.withDiscoveryClient()
.withBlockingHealthChecks(new RestTemplate())//this change
.build(context);
}
Related
I am aware that we can force FeignClient to use OkHttp instead of Ribbon by providing the url Ex. #FeignClient(url="serviceId", name="serviceId")
I want the OkHttpClient to be used even when just the name is provided. Ex. #FeignClient(name="serviceId")
As per the spring cloud documentation "if Ribbon is enabled it is a LoadBalancerFeignClient, otherwise the default feign client is used."
How can I disable ribbon so that the default feign client will be used.
None of the solutions on the internet worked for me.
Simply setting an absolute url in the url portion resulted in loadbalancing exceptions
// this resulted in java.lang.RuntimeException: com.netflix.client.ClientException: Load balancer does not have available server for client: localhost
#Lazy
#Configuration
#Import(FeignClientsConfiguration.class)
public class MyConfig {
#LocalServerPort
private int port;
#Bean
public MyClient myClient(final Decoder decoder, final Encoder encoder, final Client client) {
return Feign.builder().client(client)
.encoder(encoder)
.decoder(decoder)
.target(MyClient.class, "http://localhost:" + localServerPort);
}
}
setting spring.cloud.loadbalancing.ribbon.enabled=false resulted in application context problems. Additional settings needs to be disabled for this to work. I did not probe further
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'eurekaLoadBalancerClientConfiguration': Invocation of init method failed; nested exception is java.lang.NullPointerException
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:160)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:416)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1788)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:595)
...
...
My working solution
Finally, after inspecting the source code in org.springframework.cloud.openfeign.ribbon.DefaultFeignLoadBalancedConfiguration, I came up with this solution
#Lazy // required for #LocalServerPort to work in a #Configuration/#TestConfiguration
#TestConfiguration
#Import(FeignClientsConfiguration.class)
public class MyConfig {
#LocalServerPort
private int port;
#Bean
public MyClient myClient(Decoder decoder, Encoder encoder, Client client, Contract contract) {
return Feign.builder().client(client)
.encoder(encoder)
.decoder(decoder)
.contract(contract)
.target(MyClient.class, "http://localhost:" + localServerPort);
}
// provide a default `FeignClient` so that Spring will not automatically create their LoadBalancingFeignClient
#Bean
public Client feignClient(SpringClientFactory clientFactory) {
return new Client.Default(null, null);
}
}
I had the same question but my setup is a bit different and I did not get it working in my case (using spring-cloud-starter-openfeign with spring mvc style annotations).
FYI: I needed a custom client with an SSLSocketFactory and ended up just creating the bean for the client and keeping the url on #FeignClient
#Bean
public Client myClient() {
return new Client.Default(getSSLSocketFactory(), new NoopHostnameVerifier());
}
However, we do have projects using spring-cloud-starter-feign where the URL is not provided on the annotation. Not sure if the config below is complete (I did not set it up) but it might point you in the right direction...
dependencies
compile("org.springframework.cloud:spring-cloud-starter-feign") {
exclude group: 'org.springframework.cloud', module: 'spring-cloud-starter-ribbon'
exclude group: 'org.springframework.cloud', module: 'spring-cloud-starter-archaius'
}
config
#Configuration
#Import(FeignClientsConfiguration.class) // org.springframework.cloud.netflix.feign.FeignClientsConfiguration
public class MyConfig {
#Value("${client.url}")
private String url;
#Bean
public MyClient myClient(final Decoder decoder, final Encoder encoder, final Client client) {
return Feign.builder().client(client)
.encoder(encoder)
.decoder(decoder)
.target(MyClient.class, url);
}
}
It has nothing to do with Ribbon.
Check this:
feign:
httpclient:
enabled: false
This will disable the spring cloud autoconfigured httpclient, and will search a #Bean named httpClient in the context. So provide the definition of #Bean in a #Configuration class and that's all.
Check class FeignAutoConfiguration in spring cloud feign.
https://cloud.spring.io/spring-cloud-netflix/multi/multi_spring-cloud-feign.html
I'm trying to achieve to connect to two different MongoDBs with Spring (1.5.2. --> we included Spring in an internal Framework therefore it is not the latest version yet) and this already works partially but not fully. More precisely I found a strange behavior which I will describe below after showing my setup.
So this is what I done so far:
Project structure
backend
config
domain
customer
internal
repository
customer
internal
service
In configI have my Mongoconfigurations.
I created one base class which extends AbstractMongoConfiguration. This class holds fields for database, host etc. which are filled with the properties from a application.yml. It also holds a couple of methods for creating MongoClient and SimpleMongoDbFactory.
Furthermore there are two custom configuration classes. For each MongoDB one config. Both extend the base class.
Here is how they are coded:
Primary Connection
#Primary
#EntityScan(basePackages = "backend.domain.customer")
#Configuration
#EnableMongoRepositories(
basePackages = {"backend.repository.customer"},
mongoTemplateRef = "customerDataMongoTemplate")
#ConfigurationProperties(prefix = "customer.mongodb")
public class CustomerDataMongoConnection extends BaseMongoConfig{
public static final String TEMPLATE_NAME = "customerDataMongoTemplate";
#Override
#Bean(name = CustomerDataMongoConnection.TEMPLATE_NAME)
public MongoTemplate mongoTemplate() {
MongoClient client = getMongoClient(getAddress(),
getCredentials());
SimpleMongoDbFactory factory = getSimpleMongoDbFactory(client,
getDatabaseName());
return new MongoTemplate(factory);
}
}
The second configuration class looks pretty similar. Here it is:
#EntityScan(basePackages = "backend.domain.internal")
#Configuration
#EnableMongoRepositories(
basePackages = {"backend.repository.internal"}
mongoTemplateRef = InternalDataMongoConnection.TEMPLATE_NAME
)
#ConfigurationProperties(prefix = "internal.mongodb")
public class InternalDataMongoConnection extends BaseMongoConfig{
public static final String TEMPLATE_NAME = "internalDataMongoTemplate";
#Override
#Bean(name = InternalDataMongoConnection.TEMPLATE_NAME)
public MongoTemplate mongoTemplate() {
MongoClient client = getMongoClient(getAddress(), getCredentials());
SimpleMongoDbFactory factory = getSimpleMongoDbFactory(client,
getDatabaseName());
return new MongoTemplate(factory);
}
}
As you can see, I use EnableMongoRepositoriesto define which repository should use which connection.
My repositories are defined just like it is described in the Spring documentation.
However, here is one example which is located in package backend.repository.customer:
public interface ContactHistoryRepository extends MongoRepository<ContactHistoryEntity, String> {
public ContactHistoryEntity findById(String id);
}
The problem is that my backend always only uses the primary connection with this setup. Interestingly, when I remove the beanname for the MongoTemplate (just #Bean) the backend then uses the secondary connection (InternalMongoDataConnection). This is true for all defined repositories.
My question is, how can I achieve that my backend really take care of both connections? Probably I missed to set another parameter/configuration?
Since this is a pretty extensive post I apologise if I forgot something to mention. Please ask for missing information in the comments.
I found the answer.
In my package structure there was a empty configuration class (of my colleague) with the annotation #Configurationand #EnableMongoRepositories. This triggered the automatic wiring process of Stpring Data and therefore led to the problems I reported above.
I simply deleted the class and now it works as it should!
I'm testing an upgrade of my Spring Cloud DataFlow services from Spring Cloud Dalston.SR4/Spring Boot 1.5.9 to Spring Cloud Edgware/Spring Boot 1.5.9. Some of my services extend source (or sink) components from the app starters. I've found this does not work with Spring Cloud Edgware.
For example, I have overridden org.springframework.cloud.stream.app.rabbit.source.RabbitSourceConfiguration and bound my app to my overridden version. This has previously worked with Spring Cloud versions going back almost a year.
With Edgware, I get the following (whether the app is run standalone or within dataflow):
***************************
APPLICATION FAILED TO START
***************************
Description:
Field channels in org.springframework.cloud.stream.app.rabbit.source.RabbitSourceConfiguration required a bean of type 'org.springframework.cloud.stream.messaging.Source' that could not be found.
Action:
Consider defining a bean of type 'org.springframework.cloud.stream.messaging.Source' in your configuration.
I get the same behaviour with the 1.3.0.RELEASE and 1.2.0.RELEASE of spring-cloud-starter-stream-rabbit.
I override RabbitSourceConfiguration so I can set a header mapper on the AmqpInboundChannelAdapter, and also to perform a connectivity test prior to starting up the container.
My subclass is bound to the Spring Boot application with #EnableBinding(HeaderMapperRabbitSourceConfiguration.class). A cutdown version of my subclass is:
public class HeaderMapperRabbitSourceConfiguration extends RabbitSourceConfiguration {
public HeaderMapperRabbitSourceConfiguration(final MyHealthCheck healthCheck,
final MyAppConfig config) {
// ...
}
#Bean
#Override
public AmqpInboundChannelAdapter adapter() {
final AmqpInboundChannelAdapter adapter = super.adapter();
adapter.setHeaderMapper(new NotificationHeaderMapper(config));
return adapter;
}
#Bean
#Override
public SimpleMessageListenerContainer container() {
if (config.performConnectivityCheckOnStartup()) {
if (LOGGER.isInfoEnabled()) {
LOGGER.info("Attempting connectivity with ...");
}
final Health health = healthCheck.health();
if (health.getStatus() == Status.DOWN) {
LOGGER.error("Unable to connect .....");
throw new UnableToLoginException("Unable to connect ...");
} else if (LOGGER.isInfoEnabled()) {
LOGGER.info("Connectivity established with ...");
}
}
return super.container();
}
}
You really should never do stuff like healthCheck.health(); within a #Bean definition. The application context is not yet fully baked or started; it may, or may not, work depending on the order that beans are created.
If you want to prevent the app from starting, add a bean that implements SmartLifecycle, put the bean in a late phase (high value) so it's started after everything else. Then put your code in start(). autStartup must be true.
In this case, it's being run before the stream infrastructure has created the channel.
Some ordering might have changed from the earlier release but, in any case, performing activity like this in a #Bean definition is dangerous.
You just happened to be lucky before.
EDIT
I just noticed your #EnableBinding is wrong; it should be Source.class. I can't see how that would ever have worked - that's what creates the bean for the channels field of type Source.
This works fine for me after updating stream and the binder to 1.3.0.RELEASE...
#Configuration
public class MySource extends RabbitSourceConfiguration {
#Bean
#Override
public AmqpInboundChannelAdapter adapter() {
AmqpInboundChannelAdapter adapter = super.adapter();
adapter.setHeaderMapper(new MyMapper());
return adapter;
}
}
and
#SpringBootApplication
#EnableBinding(Source.class)
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
If that doesn't work, please edit the question to show your POM.
I've reached a bit of a brick-wall with my current project.
I have three normalised databases, one of which I want to dynamically connect to; these are:
Accounts: For secure account information, spanning clients
Configuration: For managing our clients
Client: Which will be atomic for each of our clients & hold all of their information
I need to use data stored in the "Configuration" database to modify the ConnectionString that will be used to connect to the "Client" database, but this is the bit I'm getting stuck on.
So far I've generated the entities from the databases into a project by hooking up EntityFrameWorkCore Tools and using the "Scaffold-DbContext" command & can do simple look-ups to make sure that the databases are being connected to okay.
Now I'm trying to register the databases by adding them to the ServiceCollection, I have them added in the StartUp class as follows:
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.Configure<MvcOptions>(options =>
{
options.Filters.Add(new RequireHttpsAttribute());
});
services.AddDbContext<Accounts>( options =>
options.UseSqlServer(Configuration.GetConnectionString("Accounts"))
);
services.AddDbContext<Support>(options =>
options.UseSqlServer(Configuration.GetConnectionString("Configuration"))
);
// Erm?
SelectClientDatabase(services);
}
Obviously the next stage is to dip into the "Configuration" database, so I've been trying to keep that contained in "SelectClientDatabase()", which just takes the IServiceCollection as a parameter and is for all intents and purposes empty for now. Over the last few days I've found some excellent write-ups on EFC and I'm currently exploring a CustomConfigurationProvider as a possible route, but I must admit I'm a little lost on starting out in ASP.Net Core.
Is it possible to hook into the freshly added DbContext within the ConfigureServices method? Or can/must I add this database to the service collection at a later point?
Thanks!
Edit 1:
I just found this post, which mentions that a DbContext cannot be used within OnConfiguring as it's still being configured; which makes a lot of sense. I'm now wondering if I can push all three DbContexts into a custom middleware to encapsulate, configure and make the connections available; something new to research.
Edit 2:
I've found another post, describing how to "Inject DbContext when database name is only know when the controller action is called" which looks like a promising starting point; however this is for an older version of ASP.Net Core, according to https://learn.microsoft.com "DbContextFactory" has been renamed so I'm now working to update the example given into a possible solution.
So, I've finally worked it all out. I gave up on the factory idea as I'm not comfortable enough with asp.net-core-2.0 to spend time working it out & I'm rushing head-long into a deadline so the faster options are now the better ones and I can always find time to refactor the code later (lol).
My appsettings.json file currently just contains the following (the relevant bit of appsettings.Developments.json is identical):
{
"ConnectionStrings" : {
"Accounts": "Server=testserver;Database=Accounts;Trusted_Connection=True;",
"Client": "Server=testserver;Database={CLIENT_DB};Trusted_Connection=True;",
"Configuration": "Server=testserver;Database=Configuration;Trusted_Connection=True;"
},
"Logging": {
"IncludeScopes": false,
"Debug": {
"LogLevel": {
"Default": "Warning"
}
},
"Console": {
"LogLevel": {
"Default": "Warning"
}
}
}
}
I've opted to configure the two static databases in the ConfigureServices method of StartUp, these should be configured and ready to use by the time the application gets around to having to do anything. The code there is nice & clean.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.Configure<MvcOptions>(options =>
{
//options.Filters.Add(new RequireHttpsAttribute());
});
services.AddDbContext<AccountsContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("Accounts"))
);
services.AddDbContext<ConfigContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("Configuration"))
);
services.AddSingleton(
Configuration.GetSection("ConnectionStrings").Get<ConnectionStrings>()
);
}
It turns out that one can be spoilt for choice in how to go about accessing configuration options set in the appsettings.json, I'm currently trying to work out how I've managed to get it to switch to the release version instead of the development one. I can't think what I've done to toggle that...
To get the placeholder config setting I'm using a singleton to hold the string value. This is just dipping into the "ConnectionStrings" group and stuffing that Json into the "ClientConnection" object (detailed below).
services.AddSingleton(
Configuration.GetSection("ConnectionStrings").Get<ClientConnection>()
);
Which populates the following structure (that I've just bunged off in its own file):
[DataContract(Name = "ConnectionStrings")]
public class ClientConnection
{
[DataMember]
public string Client { get; set; }
}
I only want this holding the connection string for the dynamically assigned database, so it's not too jazzy. The "Client" DataMember is what is selecting the correct key in the Json, if I wanted a different named node in the Json I'd rename it to "Accounts", for instance.
Another couple of options I tested, before settling on the Singleton option, are:
services.Configure<ConnectionStrings>(Configuration.GetSection("ConnectionStrings"));
and
var derp = Configuration.GetSection("ConnectionStrings:Client");
Which I discounted, but it's worth knowing other options (they'll probably be useful for loading other configuration options later).
I'm not keen on the way the Controller dependencies work in ASP.Net Core 2, I was hoping I'd be able to hide them in a BaseController so they wouldn't have to be specified in every single Controller I knock out, but I've not found a way to do this yes. The dependencies needed in the Controllers are passed in the constructor, these weirded me out for a while because they're auto-magically injected.
My BaseController is set up as follows:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.EntityFrameworkCore.Internal;
using ServiceLayer.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
namespace ServiceLayer.Controllers
{
public class BaseController : Controller
{
private readonly ClientConnection connectionStrings;
private readonly AccountsContext accountsContext;
private readonly ConfigurationContext configContext;
public ClientTemplateContext clientContext;
private DbContextServices DbContextServices { get; set; }
public BaseController(AccountsContext accounts, ConfigContext config, ClientConnection connection) : base()
{
accountsContext = accounts;
configContext = config;
connectionStrings = connection;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
base.OnActionExecuting(context);
}
}
}
The code for selecting the database then goes in the "OnActionExecuting()" method; this proved to be a bit of a pain as well, trying to ensure that the dbcontext was set up properly, in the end I settled on:
using System;
using System.Collections.Generic;
using System.Linq;
namespace ServiceLayer.Controllers
{
public class BaseController : Controller
{
private readonly ClientConnection connectionStrings;
private readonly AccountsContext accountsContext;
private readonly ConfigurationContext configContext;
public ClientTemplateContext clientContext;
private DbContextServices DbContextServices { get; set; }
public BaseController(AccountsContext accounts, ConfigurationContext config, ClientConnection connection) : base()
{
accountsContext = accounts;
configContext= config;
connectionStrings = connection;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
// Temporary selection identifier for the company
Guid cack = Guid.Parse("827F79C5-821B-4819-ABB8-819CBD76372F");
var dataSource = (from c in configContext.Clients
where c.Cack == cack
join ds in configContext.DataStorage on c.CompanyId equals ds.CompanyId
select ds.Name).FirstOrDefault();
// Proto-connection string
var cs = connectionStrings.Client;
if (!string.IsNullOrEmpty(cs) && !string.IsNullOrEmpty(dataSource))
{
// Populated ConnectionString
cs = cs.Replace("{CLIENT_DB}", dataSource);
clientContext = new ClientTemplateContext().Initialise(cs);
}
base.OnActionExecuting(context);
}
}
}
new ClientTemplateContext().Initialise() is a bit messy but I'll clean it up when I refactor everything else. "ClientTemplateContext" is the entity-framework-core generated class that ties together all the entities it generated, I've added the following code to that class (I did try putting it in a separate file but couldn't get that working, so it's staying in there for the moment)...
public ClientTemplateContext() {}
private ClientTemplateContext(DbContextOptions options) : base(options) {}
public ClientTemplateContext Initialise(string connectionString)
{
return new ClientTemplateContext().CreateDbContext(new[] { connectionString });
}
public ClientTemplateContext CreateDbContext(string[] args)
{
if (args == null && !args.Any())
{
//Log error.
return null;
}
var optionsBuilder = new DbContextOptionsBuilder<ClientTemplateContext>();
optionsBuilder.UseSqlServer(args[0]);
return new ClientTemplateContext(optionsBuilder.Options);
}
I also included using Microsoft.EntityFrameworkCore.Design; and added the IDesignTimeDbContextFactory<ClientTemplateContext> interface to the class. So it looks like this:
public partial class ClientTemplateContext : DbContext, IDesignTimeDbContextFactory<ClientTemplateContext>
This is where the CreateDbContext(string[] args) comes from & it allows us to create a new instance of a derived context at design-time.
Finally, the code for my test controller is as follows:
using Microsoft.AspNetCore.Mvc;
using ServiceLayer.Entities;
using System.Collections.Generic;
using System.Linq;
namespace ServiceLayer.Controllers
{
[Route("api/[controller]")]
public class ValuesController : BaseController
{
public ValuesController(
AccountsContext accounts,
ConfigurationContext config,
ClientConnection connection
) : base(accounts, config, connection) {}
// GET api/values
[HttpGet]
public IEnumerable<string> Get()
{
var herp = (from c in clientContext.Usage
select c).FirstOrDefault();
return new string[] {
herp.TimeStamp.ToString(),
herp.Request,
herp.Payload
};
}
}
}
This successfully yields data from the database dynamically selected from the DataSource table within the Configuration database!
["01/01/2017 00:00:00","derp","derp"]
If anyone can suggest improvements to my solution I'd love to see them, my solution is mashed together as it stands & I want to refactor it as soon as I feel I'm competent enough to do so.
What I try to achieve is a routing for example:
http://zuul-host:8080/v1/foo/hello to my service foo-v1, resource hello
I'm trying out the regexmapper example described at http://cloud.spring.io/spring-cloud-netflix/spring-cloud-netflix.html
My problem is that I see that a service called foo-v1 gets mapped to /v1/foo in the PatternServiceRouteMapper but then I'm not able to call that route. It's also no visible at /mappings. Do I have to activate that route somewhere?
Setup
Foo Service
application.properties
server.port=9092
spring.application.name=foo-v1
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/
eureka.instance.healthcheck.enable=true
Zuul
My configuration class Routings.java. I added some sysout log output for the service mapping and I get foo-v1 -> v1/foo in the log. Therefore this mapping should be active.
#Configuration
public class Routings {
#Bean
public PatternServiceRouteMapper serviceRouteMapper() {
return new PatternServiceRouteMapper(
"(?<name>^.+)-(?<version>v.+$)",
"${version}/${name}") {
#Override
public String apply(final String serviceId) {
String route = super.apply(serviceId);
System.out.println(serviceId + " -> " +route);
return route;
}
};
}
}
My ZuulApplication.java
#SpringBootApplication
#EnableZuulProxy
#ComponentScan
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class, args);
}
#RefreshScope
#ConfigurationProperties("zuul")
public ZuulProperties zuulProperties() {
return new ZuulProperties();
}
}
Ok, found the solution.
Remove ignoredServices: '*' from the zuul config.
This happens if you work through the examples. They start with explicitly configured routes and ignore dynamic routings. It's in the documentation but made no sense to me at that point :-)
To skip having a service automatically added, set zuul.ignored-services to a list of service id patterns.
When using the regexmapper we start using services that get added automatically and that's the feature we disabled with ignoredServices: '*'