I instrumented opentelemetry for two types of Azure Functions: HttpTrigger and TimerTrigger.
The former works fine thanks to .AddAspNetCoreInstrumentation() middleware. During its call over HTTP it creates a parent trace which is later shared with other HTTP calls within the function's code and I have a nice nested trace where all function's activities are under their parent.
However with the TimeTrigger Azure Function I cannot achieve that probably because .AddAspNetCoreInstrumentation() middleware doesn't kick in when TimeTrigger function is executed thus no parent trace is created and all HTTP calls within the function's code behave independently and cannot be grouped under one trace creating a mess.
Here are Grafana screenshots to visualize the problem.
Expected correct result of HttpTrigger function:
Individual non-grouped traces of TimeTrigger function:
Here is the code I have.
Startup:
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using OpenTelemetry;
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
[assembly: FunctionsStartup(typeof(FunctionsOpenTelemetry.Startup))]
namespace FunctionsOpenTelemetry
{
public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
// OpenTelemetry Resource to be associated with traces
var openTelemetryResourceBuilder = ResourceBuilder.CreateDefault().AddService("Reporting-Serivce");
// Enable Tracing with OpenTelemetry
var openTelemetryTracerProvider = Sdk.CreateTracerProviderBuilder()
.SetResourceBuilder(openTelemetryResourceBuilder)
.SetSampler(new AlwaysOnSampler())
.AddHttpClientInstrumentation(g => g.Filter = context => !context.RequestUri.ToString().Contains("127.0.0.1"))
.AddAspNetCoreInstrumentation()
.AddConsoleExporter()
.AddJaegerExporter()
.Build();
builder.Services.AddSingleton(openTelemetryTracerProvider);
}
}
}
Functions code:
using System;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System.Net.Http;
namespace FunctionsOpenTelemetry
{
public static class Function1
{
[FunctionName("Function1")]
public static async Task Run1([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req, ILogger log)
{
DoTenHttpRequests();
}
[FunctionName("Function2")]
public static async Task Run2([TimerTrigger("0 */5 * * * *")] TimerInfo myTimer, ILogger log)
{
DoTenHttpRequests();
}
private static void DoTenHttpRequests()
{
for (var i = 0; i < 10; i++)
{
var client = new HttpClient();
client.BaseAddress = new Uri("https://www.google.com");
client.GetAsync("/").GetAwaiter().GetResult();
}
}
}
}
.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.1.0" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="4.0.1" />
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.2.0-rc2" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.0.0-rc9" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.0.0-rc9" />
<PackageReference Include="OpenTelemetry.Exporter.Jaeger" Version="1.2.0-rc2" />
</ItemGroup>
<ItemGroup>
<None Update="host.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="local.settings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
</ItemGroup>
</Project>
Summarizing my question: how can I instrument TimeTrigger Azure Functions so they create an initial parent trace and share it within their code like HttpTrigger function does?
Here is my own answer after a couple of days of investigation and conversations on GitHub.
As of Jan 2023 there is no built in .NET middleware to support automatic instrumentation of TimerTrigger Azure Functions which was confirmed at project's GitHub page. The workaround will be a manual instrumentation which requires adding code in each function creating a parent span.
Below is a working example I ended up with.
Startup.cs:
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using OpenTelemetry;
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
[assembly: FunctionsStartup(typeof(FunctionsOpenTelemetry.Startup))]
namespace FunctionsOpenTelemetry
{
public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
// OpenTelemetry Resource to be associated with traces
var openTelemetryResourceBuilder = ResourceBuilder.CreateDefault().AddService("POC-DotNet-Instrumentation");
// Enable Tracing with OpenTelemetry
var openTelemetryTracerProvider = Sdk.CreateTracerProviderBuilder()
.SetResourceBuilder(openTelemetryResourceBuilder)
.SetSampler(new AlwaysOnSampler())
.AddSource("TimerTriggerSource")
.AddHttpClientInstrumentation(g => g.Filter = context => !context.RequestUri.ToString().Contains("127.0.0.1"))
.AddAspNetCoreInstrumentation()
.AddConsoleExporter()
.AddJaegerExporter()
.Build();
builder.Services.AddSingleton(openTelemetryTracerProvider);
}
}
}
Functions code:
using System;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System.Net.Http;
using OpenTelemetry.Trace;
using System.Diagnostics;
namespace FunctionsOpenTelemetry
{
public class Function1
{
private static readonly ActivitySource activitySource = new("TimerTriggerSource");
[FunctionName("Function1")]
public async Task Run1([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req, ILogger log)
{
DoTenHttpRequests();
}
[FunctionName("Function2")]
public async Task Run2([TimerTrigger("0 */1 * * * *")] TimerInfo myTimer, ILogger log)
{
using var activity = activitySource.StartActivity("/timertrigger/Function2");
try
{
DoTenHttpRequests();
}
catch (Exception ex)
{
activity.SetStatus(Status.Error.WithDescription(ex.Message));
throw;
}
}
private static void DoTenHttpRequests()
{
for (var i = 0; i < 10; i++)
{
var client = new HttpClient();
client.BaseAddress = new Uri("https://www.google.com");
client.GetAsync("/").GetAwaiter().GetResult();
}
}
}
}
Related
I have been working for a few days on a little C# micro service project.
About the project:
Project uses a SwaggerAPI
Project uses Entity Framework
Project version: .NET 6.0
Project uses MVC architecture
My csproj file:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<Description>IO.Swagger</Description>
<Copyright>IO.Swagger</Copyright>
<TargetFramework>net6.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PreserveCompilationContext>true</PreserveCompilationContext>
<AssemblyName>IO.Swagger</AssemblyName>
<PackageId>IO.Swagger</PackageId>
<IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNet.Mvc" Version="5.2.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.8" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.2.3" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="6.2.3" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.2.3" />
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.2.3" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.4" />
</ItemGroup>
</Project>
My StartUp.cs:
using System;
using System.IO;
using System.Web.Mvc;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
using IO.Swagger.Filters;
using IO.Swagger.Models;
using IO.Swagger.Controllers;
using Microsoft.AspNetCore.Mvc.Formatters;
using IO.Swagger.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Cors.Infrastructure;
namespace IO.Swagger
{
public class Startup
{
private readonly IWebHostEnvironment _hostingEnv;
private IConfiguration Configuration { get; }
public Startup(IWebHostEnvironment env, IConfiguration configuration)
{
_hostingEnv = env;
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
// Add MVC API Endpoints
services.AddControllers().AddNewtonsoftJson();
services.AddControllers();
services.AddHttpClient();
services.AddMvcCore(options =>
{
options.RequireHttpsPermanent = true;
options.RespectBrowserAcceptHeader = true;
});
services.AddSwaggerGen();
services.AddCors();
services.AddDbContext<TaskContext>(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
});
services.AddControllersWithViews().AddNewtonsoftJson();
// services.AddRazorPages();
services.AddMvc(options =>
options.EnableEndpointRouting = false
);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if(env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(ep =>
{
ep.MapControllerRoute(
name: "DefaultApi",
pattern: "{controller}/{action}/{id?}",
defaults: new {controller = "DefaultApi", action = "Index"}
);
ep.MapControllers();
});
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("openapi.yaml", "My first API");
});
app.UseMvc();
}
}
}
My Controller Class:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Collections;
using System.ComponentModel.DataAnnotations;
using Swashbuckle.AspNetCore.Annotations;
using Swashbuckle.AspNetCore.SwaggerGen;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using IO.Swagger.Attributes;
using IO.Swagger.Data;
using IO.Swagger.Models;
namespace IO.Swagger.Controllers
{
[Route("{controller}/{action}/{id}")]
[ApiController]
public class DefaultApiController : ControllerBase
{
private readonly TaskContext _context;
public DefaultApiController(TaskContext taskContext)
{
this._context = taskContext;
}
/*
public ActionResult Index(string id)
{
return View();
}
*/
[HttpGet]
[Route("/v1/tasks")]
[ValidateModelState]
[SwaggerOperation("TasksGet")]
[SwaggerResponse(statusCode: 200, type: typeof(List<GetTask>), description: "A JSON array of tasks")]
public async Task<IActionResult> TasksGet()
{
var tasks = await _context.Tasks.ToListAsync();
return Ok(tasks);
}
[HttpPost("post")]
[Route("/v1/tasks")]
[ValidateModelState]
[SwaggerOperation("TasksPost")]
[SwaggerResponse(statusCode: 201, type: typeof(GetTask), description: "Successfully created", contentTypes: "application/json")]
public async Task<IActionResult> TasksPost([FromBody]PostTask body)
{
IO.Swagger.Models.Task task = createTask(body);
_context.Add(task);
await _context.SaveChangesAsync();
return Ok(task);
}
[HttpDelete]
[Route("/v1/tasks/{uuid}")]
[ActionName("TasksUuidDelete")]
[ValidateModelState]
[SwaggerOperation("TasksUuidDelete")]
[SwaggerResponse(statusCode: 200, type: typeof(GetTask), description: "Delete Task")]
public virtual async Task<IActionResult> TasksUuidDelete([FromRoute][Required]string uuid)
{
uuid = "{" + uuid + "}";
var taskToDelete = new IO.Swagger.Models.Task();
var allTasks = await _context.Tasks.ToArrayAsync();
foreach (var task in allTasks)
{
if(task.Uuid.Equals(uuid))
{
taskToDelete = task;
}
}
if(taskToDelete != null)
{
_context.Remove(taskToDelete);
await _context.SaveChangesAsync();
return Ok(taskToDelete);
} else
{
return NotFound();
}
}
}
[HttpGet]
[Route("/v1/tasks/{uuid}")]
[ActionName("TasksUuidGet")]
[ValidateModelState]
[SwaggerOperation("TasksUuidGet")]
[SwaggerResponse(statusCode: 200, type: typeof(GetTask), description: "Get Task")]
public async Task<IActionResult> TasksUuidGet([FromRoute][Required]string uuid)
{
uuid = "{" + uuid + "}";
var taskToFind = new IO.Swagger.Models.Task();
var allTasks = await _context.Tasks.ToArrayAsync();
foreach (var task in allTasks)
{
if(task.Uuid.Equals(uuid))
{
taskToFind = task;
}
}
if (taskToFind == null)
{
return NotFound();
}
await _context.SaveChangesAsync();
return Ok(taskToFind);
}
[HttpPut]
[Route("/v1/tasks/{uuid}")]
[ActionName("TasksUuidPut")]
[ValidateModelState]
[SwaggerOperation("TasksUuidPut")]
[SwaggerResponse(statusCode: 200, type: typeof(PostTask), description: "Replace Task")]
public async Task<IActionResult> TasksUuidPut([FromBody]PostTask body, [FromRoute][Required]string uuid)
{
uuid = "{" + uuid + "}";
var taskToUpdate = new IO.Swagger.Models.Task();
var allTasks = await _context.Tasks.ToArrayAsync();
foreach (var task in allTasks)
{
if(task.Uuid.Equals(uuid))
{
taskToUpdate = task;
taskToUpdate.Title = body.Title;
taskToUpdate.Description = body.Description;
}
}
await _context.SaveChangesAsync();
if (taskToUpdate != null)
{
return Ok(taskToUpdate);
} else
{
return NotFound();
}
}
private IO.Swagger.Models.Task createTask(PostTask body)
{
IO.Swagger.Models.Task taskForList = new IO.Swagger.Models.Task();
taskForList.Title = body.Title;
taskForList.Description = body.Description;
taskForList.Uuid = System.Guid.NewGuid().ToString("B");
return taskForList;
}
}
}
My problem is when I try to execute the HttpPut, HttpDelete methods with the cURL command, I get the following error:
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
Request starting HTTP/1.1 'PUT' https://localhost:5001/v1/tasks/c18556a8-635f-4fd7-8aea-13ff7a9c42d1 application/json 74
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
Executing endpoint '405 HTTP Method Not Supported'
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
Executed endpoint '405 HTTP Method Not Supported'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
Request finished HTTP/1.1 'PUT' https://localhost:5001/v1/tasks/c18556a8-635f-4fd7-8aea-13ff7a9c42d1 application/json 74 - 405 0 - 8.4272ms
But when I add to the HttpAttributes an ID like in HttpPost it works fine, but I can't put an ID to two or more HttpAttributes with the same URL because then I get an multiple methods error. I guess the problem is that HttpPut, HttpGet and HttpDelete have the same routing URL.
I read a lot about the routing but can't fix the problem.
What I have tried:
Use one method for HttpGet, HttpPut and HttpDelete and use a switch-case for the different implementations
Use an ID for all HttpAttributes
Different configurations with app.UseEndpoints
Tried to use different controller classes for the HttpActions
Nothing really helps.
My question is: how can I map different HttpActions with the same route URL?
I'm not sure if this is the solution to your issue but .NET 6.0+ does not automatically generate the startup.cs file anymore.
Good day!
I am trying to add Google Authentication to Firestore project.
Used various bundles VS2017, VS2019, .net Core 2.2, 3.1, .net 5.0.
The whole project works fine until any mention of Google.Apis.Auth.OAuth2.Mvc.Controllers (or AspNetCore3) is added.
Once a simple mention is added in the Startup.cs
app.UseEndpoints (...);
an exception is thrown
System.TypeLoadException: "Could not load type 'System.Web.HttpContextBase' from assembly 'System.Web, Version = 4.0.0.0, Culture = neutral, PublicKeyToken = b03f5f7f11d50a3a'."
Dependencies used:
<PackageReference Include="Firebase.Auth" Version="1.0.0" />
<PackageReference Include="FirebaseDatabase.net" Version="4.0.6" />
<PackageReference Include="Google.Apis.Auth.AspNetCore3" Version="1.55.0" />
<PackageReference Include="Google.Cloud.Firestore" Version="2.4.0" />
<PackageReference Include="Microsoft.AspNet.Mvc" Version="5.2.7" />
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.10" />
<PackageReference Include="Microsoft.AspNetCore.Session" Version="2.2.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="5.0.2" />
I tried adding Google.Apis, Google.Apis.Auth, Google.Apis.Auth.Mvc, Google.Apis.Core, result was the same.
Small code with this exception:
AuthCallbackController.cs
using System.Web.Mvc;
namespace Google.Apis.Auth.OAuth2.Mvc.Controllers
{
public class AuthCallbackController : Controller
{
public ActionResult Index()
{
return Redirect("/");
}
}
}
Program.cs
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
namespace ToDo2019Help
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace ToDo2019Help
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
}
}
}
There is more code in the final project, but the same exception is still thrown on app.UseEndpoints. i'm sorry for bad english. Thank you for any help!
EDIT
If replaced in AuthCallbackController.cs
using System.Web.Mvc;
with line
using Microsoft.AspNetCore.Mvc;
this exception is no longer thrown,
but GetUserId require System.Web.Mvc.Controller and controller.Session is
error "CS7069. Reference The reference to the type "HttpSessionStateBase" requires its definition in "System.Web", but it could not be found"
AppFlowMetadata.cs
using Google.Apis.Auth.OAuth2;
using Google.Apis.Auth.OAuth2.Flows;
public class AppFlowMetadata : Google.Apis.Auth.OAuth2.Mvc.FlowMetadata
{
private static readonly IAuthorizationCodeFlow flow =
new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = "PPPPPPPP",
ClientSecret = "PPPPPPPPP"
},
Scopes = new[] { "email", "profile" },
});
public override string GetUserId(System.Web.Mvc.Controller controller)
{
var user = controller.Session["user"];
if (user == null)
{
user = System.Guid.NewGuid();
controller.Session["user"] = user;
}
return user.ToString();
throw new System.NotImplementedException();
}
public override string AuthCallback
{
get
{
return #"/AuthCallback/IndexAsync";
}
}
public override IAuthorizationCodeFlow Flow
{
get { return flow; }
}
}
My mistake was using Google.Apis.Auth.OAuth2.Flows in a .Net Core project. You need to use the Google.Apis.Auth.AspNetCore3 library. An example of how to use it
I am attempting to put my connection string in the local settings .json file in an Azur e Function (v3) for entity framework core.
I am getting an errors saying.
ystem.Private.CoreLib: Exception while executing function: Function1.
Microsoft.EntityFrameworkCore: No database provider has been
configured for this DbContext. A provider can be configured by
overriding the DbContext.OnConfiguring method or by using AddDbContext
on the application service provider. If AddDbContext is used, then
also ensure that your DbContext type accepts a
DbContextOptions object in its constructor and passes it to
the base constructor for DbContext.
So I have removed the connection string in OnCofiguration on the context
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
#warning
// optionsBuilder.UseSqlServer("Server=tcp:xxxxx.database.windows.net,1433;Initial Catalog=CatsDB;Persist Security Info=False;User ID=!;Password=!;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;");
}
}
I am using Dependency Injection in a startup.cs class :
[assembly: FunctionsStartup(typeof(Startup))]
namespace Shizzle
{
class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddDbContext<CatsDBContext>(
options => options.UseSqlServer(ConfigurationManager.ConnectionStrings["SqlConnectionString"].ConnectionString));
}
}
And finally I am storing the connection string in local.settings.json
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet"
},
"ConnectionStrings": {
"SqlConnectionString": "Server=tcp:xxxx.database.windows.net,1433;Initial Catalog=CatsDB;Persist Security Info=False;User ID=!;Password=0!;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;"
}
}
and this is the DBContext :
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
namespace Shizzle.Models
{
public partial class CatsDBContext : DbContext
{
public CatsDBContext()
{
}
public CatsDBContext(DbContextOptions<CatsDBContext> options)
: base(options)
{
}
Any ideas would be amazing...
Thanks
The startup.cs class should be
[assembly: FunctionsStartup(typeof(Startup))]
namespace Shizzle
{
class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddDbContext<CatsDBContext>(
options => options.UseSqlServer(Configuration.GetConnectionString("SqlConnectionString")));
}
}
This worked for me, but I had to use an ancient EF Core version to be compatible with the current in-process Functions app version (see issue here).
.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AzureFunctionsVersion>v3</AzureFunctionsVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="2.2.6" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.13" />
</ItemGroup>
<ItemGroup>
<None Update="host.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="local.settings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
</ItemGroup>
</Project>
And the .cs file:
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
[assembly: FunctionsStartup(typeof(FunctionApp5.Startup))]
namespace FunctionApp5
{
class Startup : FunctionsStartup
{
public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
{
FunctionsHostBuilderContext context = builder.GetContext();
var settingsFile = Path.Combine(context.ApplicationRootPath, "local.settingss.json");
builder.ConfigurationBuilder
.AddJsonFile(settingsFile, optional: true, reloadOnChange: false);
// .AddJsonFile(Path.Combine(context.ApplicationRootPath, $"appsettings.{context.EnvironmentName}.json"), optional: true, reloadOnChange: false)
// .AddEnvironmentVariables();
}
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddDbContext<CatsDBContext>((sp,options) =>
{
var config = sp.GetRequiredService<IConfiguration>();
var constr = config.GetConnectionString("SqlConnectionString");
options.UseSqlServer(constr);
});
}
}
public partial class CatsDBContext : DbContext
{
public CatsDBContext()
{
}
public CatsDBContext(DbContextOptions<CatsDBContext> options)
: base(options)
{
}
}
public class Function1
{
CatsDBContext db;
public Function1(CatsDBContext db)
{
this.db = db;
}
[FunctionName("Function1")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
ILogger log)
{
var constr = db.Database.GetDbConnection().ConnectionString;
log.LogInformation("C# HTTP trigger function processed a request.");
string name = req.Query["name"];
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);
name = name ?? data?.name;
string responseMessage = string.IsNullOrEmpty(name)
? "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response."
: $"Hello, {name}. This HTTP triggered function executed successfully.";
return new OkObjectResult(responseMessage);
}
}
}
I can find tutorials to add it to MVC, and even from 2014, an article explaining how to add it to .NET 4.7 windows service.
However with a .NET Core 3.1 windows service, I just cannot figure out how to do it.
Most tutorials seem to revolve around a Startup.cs file which does not exist in a windows service. This is the latest tutorial I could find from Microsoft but it uses a Web App rather than a windows service.
The windows service runs using this code:
var builder = new HostBuilder() .ConfigureServices((hostContext, services) => { services.AddHostedService<MyWindowsService>(); });
I assume that SignalR needs to be set up around here.
I found some evidence you can do WebApp.Start("http://localhost:8080"); but this is OWIN. The example I found (an older version of the one above) then has a Startup class but there's no indication how this class is called. It takes an IAppBuilder and there's a method on it to add SignalR. However IAppBuilder does not appear to be .NET Core, nor could I find any SignalR methods of any kind.
I wonder if anyone could point me in the right direction?
SignalR Server-side requires a server that receives web requests, Kestrel or IIS normally. So you need a web app, you can still add hosted services to your webapp, there is even an example showing a web app with SignalR and a hosted service: https://learn.microsoft.com/aspnet/core/signalr/background-services?view=aspnetcore-5.0
For people wanting to develop a self-hosted ASPNETCore SignalR hub running in a Windows service, here's my barebones code. (Disclaimer: I'm new to ASPNET Core, and I don't know whether this approach would be approved by more knowledgeable folk.) The magic is in the ".UseStartup();" call.
Create a new service project using the VS 2019 "Worker Service" C# template.
Edit the service's .csproj file and insert the lines:
<ItemGroup>
<FrameworkReference Include="Microsoft.aspNetCore.App" />
</ItemGroup>
Create a Startup class:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Text;
namespace My.SignalRCore.Service
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSignalR();
services.AddHostedService<Worker>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting(); // pre-requisite for app.UseEndpoints()
app.UseEndpoints(endpoints =>
{
string url = $"/ServerHub";
endpoints.MapHub<MyHub>(url);
});
}
}
}
Create a MyHub : Hub class:
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace My.SignalRCore.Service
{
public class MyHub : Hub
{
public ILogger<Worker> _logger = null;
public MyHub(ILogger<Worker> logger)
{
_logger = logger;
//_logger.LogInformation($"{DateTimeOffset.Now} MyHub.Constructor()");
}
public async Task ProcessClientMessage(string user, string message)
{
// process an incoming message from a connected client
_logger.LogInformation($"{DateTime.Now.ToString("hh:mm:ss.fff")} MyHub.ProcessClientMessage({user}, {message})");
}
}
}
Amend the Program class to use a "UseStartup" call:
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using System;
namespace My.SignalRCore.Service
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
webBuilder.UseUrls("http://*:12457");
});
}
}
Add a hub reference (if needed) in the Worker class:
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace My.SignalRCore.Service
{
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly IHubContext<MyHub> _signalRHub;
public Worker(ILogger<Worker> logger, IHubContext<MyHub> signalRHub)
{
_logger = logger;
_signalRHub = signalRHub;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(15000, stoppingToken);
_logger.LogInformation($"{DateTime.Now.ToString("hh:mm:ss.fff")} Sending ping to all clients");
await _signalRHub.Clients.All.SendAsync("ReceiveMessage", "Server", "ping");
}
}
}
}
That's it for the server code. I've not yet installed it as a service, but it works as a console app.
On a non-dev machine, you might need to install the APSNET CORE 3.1 runtime, it's available here:
https://dotnet.microsoft.com/download/dotnet/3.1
For the client:
Install nuget package: Microsoft.AspNetCore.SignalR.Client
Create a client class along the lines of (note: the reconnect code here isn't working):
using Microsoft.AspNetCore.SignalR.Client;
using System;
using System.Threading.Tasks;
namespace My.SignalRCoreClientLib
{
public class SignalRCoreClientLib
{
public EventHandler<string> MessageEvent;
private HubConnection _connection;
public async Task Connect(string serverIp, int port)
{
if (_connection == null)
{
_connection = new HubConnectionBuilder()
.WithUrl($"http://{serverIp}:{port}/ServerHub")
.Build();
_connection.Closed += async (error) =>
{
await Task.Delay(new Random().Next(0, 5) * 1000);
await _connection.StartAsync();
};
_connection.On<string, string>("ReceiveMessage", (user, message) =>
{
string fullMessage = $"{user}: {message}";
MessageEvent?.Invoke(this, fullMessage);
});
}
try
{
await _connection.StartAsync();
}
catch (Exception ex)
{
MessageEvent?.Invoke(this, $"{ex.Message}; base Exception: {ex.GetBaseException().Message}");
await Task.Delay(new Random().Next(0, 5) * 1000);
await Connect(serverIp, port);
}
}
public async Task SendMessage(string user, string message)
{
try
{
await _connection.InvokeAsync("ProcessClientMessage", user, message);
}
catch (Exception ex)
{
MessageEvent?.Invoke(this, ex.Message);
}
}
}
}
That's it. Hope this is helpful.
I'm using Azure Cosmos DB in asp net core 3.1 app with Microsoft.EntityFrameworkCore.Cosmos package. In my AppContext class on OnModelCreating method i've give every dbset ToContainer() method to define the name of container otherwise it'll give AppContext name to container. Everything is working fine but it still create one container AppContext in CosmosDb, how can i fix this? Please help. Below is my code.
AppContext Class
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace CosmosApp.Context
{
public class AppContext : IdentityDbContext<AppUser>
{
public DbSet<Teacher> Teachers { get; set; }
public DbSet<Student> Students { get; set; }
public AppContext(DbContextOptions options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<IdentityUser>().ToContainer("Users");
modelBuilder.Entity<AppUser>().ToContainer("Users");
modelBuilder.Entity<IdentityUserRole<string>>().ToContainer("UserRoles");
modelBuilder.Entity<IdentityUserLogin<string>>().ToContainer("UserLogins");
modelBuilder.Entity<IdentityUserClaim<string>>().ToContainer("UserClaims");
modelBuilder.Entity<IdentityRole>().ToContainer("Roles");
modelBuilder.Entity<IdentityUserToken<string>>().ToContainer("UserTokens");
modelBuilder.Entity<Teacher>().ToContainer("Teachers");
modelBuilder.Entity<Student>().ToContainer("Students");
modelBuilder.Entity<Teacher>().HasNoDiscriminator();
modelBuilder.Entity<Student>().HasNoDiscriminator();
}
}
}
AppUser Class
using Microsoft.AspNetCore.Identity;
namespace CosmosApp.Entities
{
public class AppUser : IdentityUser
{
public string DisplayName { get; set; }
}
}
Startup Class
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<AppContext>(options =>
{
options.UseCosmos(
"https://localhost:8081/",
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
"TestDb");
});
services.AddControllers();
services.AddIdentity<AppUser, IdentityRole>(options => { })
.AddEntityFrameworkStores<AppContext>()
.AddDefaultTokenProviders();
services.AddAuthentication();
}
Program Class
using System;
using CosmosApp.Context;
using CosmosApp.Entities;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using AppContext = CosmosApp.Context.AppContext;
namespace CosmosApp
{
public class Program
{
public static void Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<AppContext>();
var userManager = services.GetRequiredService<UserManager<AppUser>>();
var roleManager = services.GetRequiredService<RoleManager<IdentityRole>>();
context.Database.EnsureCreated();
Seed.SeedData(context, userManager, roleManager).Wait();
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occured during migration");
}
}
host.Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
Packages Info
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Cosmos" Version="3.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
Now you can see it was creating unknown conatiner AppContext
Use HasDefaultContainer for the first Entity and ToContainer on subsuquent Entities.
protected override void OnModelCreating( ModelBuilder builder ) {
builder.HasDefaultContainer("Users");
builder.Entity<User>().ToContainer("Users");
builder.Entity<Teacher>().ToContainer("Teachers");
builder.Entity<Student>().ToContainer("Students");
}