I am working on an Azure IoT Edge container and we are using Serilog. Microsoft recommends outputting the log text to the console window in the format shown in this article: https://learn.microsoft.com/en-us/azure/iot-edge/how-to-retrieve-iot-edge-logs?view=iotedge-2020-11. It would output a line like this to the console:
<6> 2022-06-15 13:05:44.790 +00:00 [INF] - Received direct method call - ping
My question is - how can I display the level twice? The second is easy ({level:u3}), but how can I convert the level to a syslog number at the beginning of the line? The number reference is at the beginning of the article above. I thought about a text formatter, but I need the level formatted two different ways and was not sure that would work.
Thoughts?
I got it to work with a TextFormatter
internal class ConsoleTextFormatter : ITextFormatter
{
public void Format(LogEvent logEvent, TextWriter output)
{
var sb = new StringBuilder();
sb.AppendLine($"<{GetSysLogLevel(logEvent.Level)}> {logEvent.Timestamp:O} [{GetThreeLetterLevel(logEvent.Level)}] - {logEvent.MessageTemplate}");
if (logEvent.Exception != null)
sb.AppendLine(logEvent.Exception.ToString());
foreach (var property in logEvent.Properties.Where(p => p.Key != "SourceContext"))
{
sb.AppendLine($"{property.Key}={property.Value}");
}
output.Write(sb.ToString());
}
// these are in a static class that I cannot access (https://github.com/serilog/serilog/blob/dev/src/Serilog/Formatting/Display/LevelOutputFormat.cs),
// so I have hard coded them here
private string GetThreeLetterLevel(LogEventLevel level)
{
return level switch
{
LogEventLevel.Debug => "DBG",
LogEventLevel.Error => "ERR",
LogEventLevel.Fatal => "FTL",
LogEventLevel.Information => "INF",
LogEventLevel.Verbose => "VRB",
LogEventLevel.Warning => "WRN",
_ => throw new NotSupportedException("Unknown log level")
};
}
private string GetSysLogLevel(LogEventLevel level)
{
return level switch
{
LogEventLevel.Debug => "7",
LogEventLevel.Error => "3",
LogEventLevel.Fatal => "2", // critical
LogEventLevel.Information => "6",
LogEventLevel.Verbose => "7", // debug also
LogEventLevel.Warning => "4",
_ => throw new NotSupportedException("Unknown log level")
};
}
Related
I am trying to get the hang of swagger in minimal API. I have the following code:
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(setup => setup.SwaggerDoc("v1", new OpenApiInfo()
{
Description = "An api that will change your life for ever",
Title = "Alert Api",
Version = "v1",
Contact = new OpenApiContact()
{
Name = "Grundfos",
Url = new Uri("https://grundfos.com")
}
}));
WebApplication app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
//Map endpoints
app.MapGet("/alerts", async () => Results.Ok());
app.MapGet("/profiles", async () => Results.Ok());
this gives a swagger UI looking like this:
My question is: How do you sort the endpoints to be under a headline called "alerts" and "profiles"?
I think what you are after is something called Tags in swagger.
You can add WithTags at the end of your mapping, like so:
//Map endpoints
app.MapGet("/alerts", async () => Results.Ok()).WithTags("Alerts");
app.MapGet("/profiles", async () => Results.Ok()).WithTags("Profiles");
The result looks like this:
Alternatively you can also take another approach by configuring the AddSwaggerGen method.
In there you can take the first segment of the URL endpoint and use that as the tag name.
For example, the endpoints alerts and alerts/delete will both be placed in a section called alerts.
builder.Services.AddSwaggerGen(c =>
{
c.TagActionsBy(d =>
{
var rootSegment = d.RelativePath?
.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries)
.FirstOrDefault() ?? "Home";
return new List<string> { rootSegment! };
});
})
//Map endpoints without 'WithTags`
app.MapGet("/alerts", async () => Results.Ok());
app.MapGet("/alerts/delete", async () => Results.Ok());
app.MapGet("/profiles", async () => Results.Ok());
If you have a bunch of related endpoints and you want to group them in one tag SomethingEndpoints, you can do it by creating a static class named SomethingEndpoints and defining an extension method MapSomethingEndpoints() as follows.
Notes:
The automatically added tags are only available when passing method Job.
Passing (int y) => Job(y) or (int z) => z does not. You need to attach WithTags() manually in this case.
I don't know whether it is a feature by design or a bug!
SomethingEndpoints.cs
static class SomethingEndpoints
{
public static void MapSomethingEndpoints(this IEndpointRouteBuilder routes)
{
var group = routes.MapGroup("/api/v1/job");
group.MapGet("/{x:int}", Job);// no need WithTags!
group.MapGet("/{y:int}", (int y) => Job(y)).WithTags(nameof(SomethingEndpoints));
group.MapGet("/{z:int}", (int z) => z).WithTags(nameof(SomethingEndpoints));
}
static int Job(int x) => x;
}
Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.MapSomethingEndpoints();
app.Run();
documentation is very sparce and all i tried results in the deserializer injected but normal odata url's not working anymore.
https://github.com/OData/WebApi/issues/158 has solutions buut for 5.6.
The final relevant comment is:
#dbenzhuser - In that commit, look at ODataFormatterTests.cs for how
inject a custom deserializer/deserializer provider. You can still use
a custom DeserializerProvider but it's injected through DI instead of
injecting it through ODataMediaTypeFormatters.
which is quite meaningless. I tried the code there, but it breaks, as I said, the URL's.
Right now my Odata setup is simple:
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddOData();
\UnitTest\Microsoft.AspNet.OData.Test.Shared\Formatter\ODataFormatterTests.cs
has examples to inject them (like in lines 379-383)
config.MapODataServiceRoute("IgnoredRouteName", null, builder =>
builder.AddService(Microsoft.OData.ServiceLifetime.Singleton, sp => ODataTestUtil.GetEdmModel())
.AddService<ODataSerializerProvider>(ServiceLifetime.Singleton, sp => new CustomSerializerProvider())
.AddService<IEnumerable<IODataRoutingConvention>>(ServiceLifetime.Singleton, sp =>
ODataRoutingConventions.CreateDefaultWithAttributeRouting("IgnoredRouteName", config)));
but I seem unable to get this working without removing the core odata routing.
Anyone an idea how to use that for the current version without breaking the base functionality?
There are three steps if you want to maintain the base functionality:
Your DeserializerProvider implementation should default to the base implementation for all scenarios that your custom Deserializer can't manage. In the following example the custom deserializer only operates on Resources and not Sets:
public class EntityTypeDeserializerProvider : DefaultODataDeserializerProvider
{
private readonly DataContractDeserializer _dataContractDeserializer;
public EntityTypeDeserializerProvider(IServiceProvider rootContainer)
: base(rootContainer)
{
_dataContractDeserializer = new DataContractDeserializer(this);
}
public override ODataEdmTypeDeserializer GetEdmTypeDeserializer(IEdmTypeReference edmType)
{
if(edmType.Definition.TypeKind == EdmTypeKind.Complex || edmType.Definition.TypeKind == EdmTypeKind.Entity)
return _dataContractDeserializer;
else
return base.GetEdmTypeDeserializer(edmType);
}
}
As with the provider your custom _Deserializer should call through to the base implementation for everything that you do not need to customize. In the following implementation we are only trying to enforce the Order of the properties that are deserialized as well as calling the DataContract OnDeserializing and OnDeserialized methods, the rest of the deserialization is unaffected:
/// <summary>
/// OData serializer that oberys the DataMember Order Attribute and OnDeserializing and OnDeserialized attributes on the resource definition
/// </summary>
public class DataContractDeserializer : ODataResourceDeserializer
{
public DataContractDeserializer(ODataDeserializerProvider provider)
: base(provider) { }
public override object CreateResourceInstance(IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext)
{
var resource = base.CreateResourceInstance(structuredType, readContext);
var type = resource.GetType();
if(type.GetCustomAttributesData().Any(x => x.AttributeType == typeof(DataContractAttribute)))
{
// manually call OnDeserializing
var init = type.GetMethods(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic).FirstOrDefault(x => x.GetCustomAttributesData().Any(a => a.AttributeType == typeof(OnDeserializingAttribute)));
if(init != null)
{
init.Invoke(resource, new object[] { new StreamingContext(StreamingContextStates.Remoting, readContext ) });
}
}
return resource;
}
public override object ReadResource(ODataResourceWrapper resourceWrapper, IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext)
{
var resource = base.ReadResource(resourceWrapper, structuredType, readContext);
var type = resource.GetType();
if (type.GetCustomAttributesData().Any(x => x.AttributeType == typeof(DataContractAttribute)))
{
// manually call OnDeserialized
var final = type.GetMethods(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic).FirstOrDefault(x => x.GetCustomAttributesData().Any(a => a.AttributeType == typeof(OnDeserializedAttribute)));
if (final != null)
{
final.Invoke(resource, new object[] { new StreamingContext(StreamingContextStates.Remoting, readContext) });
}
}
return resource;
}
public override void ApplyStructuralProperties(object resource, ODataResourceWrapper resourceWrapper, IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext)
{
var type = resource.GetType();
var memberDescriptors = type.GetProperties().Where(x => x.HasAttribute<DataMemberAttribute>());
if (memberDescriptors.Any())
{
var orderedProperties = from p in resourceWrapper.Resource.Properties
let clsProperty = memberDescriptors.FirstOrDefault(m => m.Name == p.Name)
let memberAtt = (DataMemberAttribute)(clsProperty?.GetCustomAttributes(true).FirstOrDefault(a => a.GetType() == typeof(DataMemberAttribute)))
orderby (memberAtt?.Order).GetValueOrDefault()
select p;
foreach (var property in orderedProperties)
{
ApplyStructuralProperty(resource, property, structuredType, readContext);
}
}
else
base.ApplyStructuralProperties(resource, resourceWrapper, structuredType, readContext);
}
}
Finally, You need to replace the default DeserializerProvider registration with your own, the following is an example of an overload to MapODataServiceRoute that registers the DeserializerProvider from the previous 2 examples.
I have commented out an example of registering a specific SerializerProvider
private static ODataRoute MapODataServiceRoute(this HttpConfiguration configuration, string routeName,
string routePrefix, IEdmModel model, ODataBatchHandler batchHandler = null, ODataUriResolver uriResolver = null, IList<IODataRoutingConvention> routingConventions = null)
{
return configuration.MapODataServiceRoute(routeName, routePrefix, builder =>
builder
.AddService(ServiceLifetime.Singleton, sp => model)
//.AddService<ODataSerializerProvider>(ServiceLifetime.Singleton, sp => new DefaultODataSerializerProvider(sp))
.AddService<ODataDeserializerProvider>(ServiceLifetime.Singleton, sp => new EntityTypeDeserializerProvider(sp))
.AddService(ServiceLifetime.Singleton, sp => batchHandler ?? new DefaultODataBatchHandler(GlobalConfiguration.DefaultServer))
.AddService(ServiceLifetime.Singleton, sp => uriResolver ?? new ODataUriResolver())
.AddService<IEnumerable<IODataRoutingConvention>>(ServiceLifetime.Singleton, sp =>
routingConventions ??
ODataRoutingConventions.CreateDefaultWithAttributeRouting(routeName, configuration)
)
);
}
Hi I have a business logic layer that returns selectlistitems to a controller, so that will then pass to the view to populate select lists.
I have this method that works:
public IEnumerable<SelectListItem> GetDevices
{
get
{
using (IDeviceData repository = _dataFactory.Create())
{
return repository.DeviceTypes.ToList()
.Where(dt => dt.ParentId == 10 )
.Select(dt =>
new SelectListItem
{
Text = (dt.Name).Trim(),
Value = dt.Id.ToString()
});
}
}
}
And this that doesn't:
public IEnumerable<SelectListItem> GetGroups(int deviceTypeId)
{
using (IDeviceData repository = _dataFactory.Create())
{
return repository.DeviceTypeConfigurationParameterGroupMaps.ToList()
.Where(cm => cm.DeviceTypeId == deviceTypeId)
.Join(repository.ConfigurationParameterGroups, cm => cm.ConfigurationParameterGroupId, cg => cg.Id, (cm, cg) => new { cm, cg })
.Select(cg =>
new SelectListItem
{
Text = (cg.cg.Name).Trim(),
Value = cg.cg.Id.ToString()
});
}
}
The obvious difference is the join between two tables, the error I receieve is:
Results View = The type '<>f__AnonymousType0<p,d>' exists in both 'System.Web.dll' and 'EntityFramework.dll'
This is receieved when trying to expand the results whiel debugging. Any advice would eb welcome as I'm not overly familiar with LINQ
Figured it out:
public IEnumerable<SelectListItem> GetGroupsForDevice(int deviceTypeId)
{
using (IDeviceData repository = _dataFactory.Create())
{
return repository.DeviceTypeConfigurationParameterGroupMaps
.Where(cm => cm.DeviceTypeId == deviceTypeId)
.Join(repository.ConfigurationParameterGroups, cm => cm.ConfigurationParameterGroupId, cg => cg.Id, (cm, cg) => cg )
.ToList()
.Select(cg =>
new SelectListItem
{
Text = (cg.Name).Trim(),
Value = cg.Id.ToString()
}).ToList() ;
}
}
I needed to add ToList() after the join, and then again after converting to SelectlistItem. I also didnt need th create the new anonymous type - Thanks to joanna above for that.
This is the answer but not a good explanation, if anyone wants to pad it out a little please feel free!
I am using mvc4. On one of my page, it has Kendo Grid. I want to show 5 rows per page. I have no problem doing it using pure javascript, however, If I am using mvc helper. I got lost, couldn't find any samples online.
here's my javascript code.
<script language="javascript" type="text/javascript">
$(document).ready(function () {
$("#grid").kendoGrid({
dataSource: {
type: "json",
serverPaging: true,
pageSize: 5,
transport: { read: { url: "Products/GetAll", dataType: "json"} },
schema: { data: "Products", total: "TotalCount" }
},
height: 400,
pageable: true,
columns: [
{ field: "ProductId", title: "ProductId" },
{ field: "ProductType", title: "ProductType" },
{ field: "Name", title: "Name" },
{ field: "Created", title: "Created" }
],
dataBound: function () {
this.expandRow(this.tbody.find("tr.k-master-row").first());
}
});
});
now if i am using mvc helper
#(Html.Kendo().Grid(Model)
.Name("Grid") //please help me to finish the rest
Update:
Adding the action code.
[HttpPost]
public ActionResult GetAll([DataSourceRequest]DataSourceRequest request, int id)
{
var products= ProductService.GetAll(id);
return Json(products.ToDataSourceResult(request));
}
GetAll method in the service layer:
public IQueryable<Product> GetAll(int id)
{
var products= ProductRepository.Get(p => p.Id== id && p.IsActive == true, null, "ProductionYear")
.OrderBy(o => o.Name); //.ToList();
return Product.Select(p => new ProductVM()
{
Name = p.Name,
ProductionYear= p.ProductionYear.Year.ToString()
Id = p.Id
}).AsQueryable();
}
now, if I run the app, i will get following error:
"LINQ to Entities does not recognize the method 'System.String ToString()' method, and this method cannot be translated into a store expression."}
in the GetAll method, I comment out the "ToList()". If I use ToList, everything works. but I will query all the rows back instead just those rows I needed for that page.
You can set the PageSize inside the DataSource method. So you will need something like:
#(Html.Kendo().Grid(Model)
.Name("Grid")
.DataSource(dataSource => dataSource.Ajax()
.PageSize(5)
.Read(c => c.Action("GetAll", "Products")
.Model(s => s.Id(m => m.Id)))
.Columns(columns =>
{
columns.Bound(m => m.ProductId).Title("ProductId");
//other colums
})
.Events(g => g.DataBound("somefunction"))
.Pageable(true))
You can find more info the KendoUI Grid's Asp.NET MVC wrappers documentation.
If you are not using Kendo's ASP.NET MVC wrappers and are using the Kendo JavaScript objects directly and you are trying to do server side paging, then you need to create your datasource as follows:
var dataSource = {
"type":"aspnetmvc-ajax",
"serverSorting": true,
"serverPaging": true,
"page": 1,
"pageSize": 8,
"schema": {
"data":"items",
"total":"count",
"errors":"errors"
}
};
And your Read controller method will look something like:
public ActionResult List_Read([DataSourceRequest]DataSourceRequest request) {
try {
var model = null /* prepare your model object here contain one page of grid data */;
// using Json.NET here to serialize the model to string matching the
// schema expected by the data source
var content = JsonConvert.SerializeObject(new { count = model.Count, items = model.Items }, Formatting.None);
return Content(content, "application/json");
}
catch (Exception exception) {
// log exception if you need to
var content = JsonConvert.SerializeObject(new { errors = exception.Message.ToString() }, Formatting.None);
return Content(content, "application/json");
}
}
type, serverSorting and serverPaging are important to make sure paging and sorting happens server-side. type must be aspnetmvc-ajax, otherwise the query data will not be recognized by the model binder that has been specified by the [DataSourceRequest] attribute in the Read method. You cannot omit that attribute unless you want to write your own custom modelbinder to parse the query data sent by the kendo dataSource, which does not conform to the modelbinding conventions used by the DefaultModelBinder in ASP.NET MVC.
The other answer will work if you are using Kendo UI for ASP.NET MVC. If you don't you need to implement paging yourself. The Kendo DataSource will send pageSize and current page when making a request. You can use that to do the paging. It also sends "take" and "skip" which makes things even easier (hint Linq).
The error
"LINQ to Entities does not recognize the method 'System.String ToString()' method, and this method cannot be translated into a store expression."}
is self explanatory, to gat around this, you should do something like
return Product.**AsEnumerable**.Select(p => new ProductVM()
{
Name = p.Name,
ProductionYear= p.ProductionYear.Year.ToString()
Id = p.Id });
using AsEnumerable brings all records from the db, so best to use after any filtering
products.where(x => x.active = true).AsEnumerable
rather then
products.AsEnumerable.where(x => x.active = true)
Download kendo examples, then unzip and follow
<your directory>\Kendo UI for ASP.NET MVC Q1 2013\wrappers\aspnetmvc\Examples\Areas\razor\Views\web\grid\
for the view index
and
<your directory>\Kendo UI for ASP.NET MVC Q1 2013\wrappers\aspnetmvc\Examples\Controllers
for IndexController
in your view you might also want .ServerOperation(true) as bellow to avoid
Error during serialization or deserialization using the JSON JavaScriptSerializer. The length of the string exceeds the value set on the maxJsonLength property.
If you have big data to fetch
#(Html.Kendo().Grid(<yourmodel>)
.Name("Grid")
.Columns(columns =>
...
})
.Filterable()
.Pageable()
.DataSource(dataSource => dataSource
.Ajax()
.PageSize(8)
.ServerOperation(true) )
.Model(model =>
{
model.Id(p => p.Id);
...
})
)
)
also in controller in ActionResult Products_Read consider
DataSourceResult result = GetProducts().ToDataSourceResult(request, o => new { Id = o.Id, ... } );
return Json(result , JsonRequestBehavior.AllowGet);
I've not used the MvcContrib for unit testing before and I'm having a bit of trouble running some of the tests.
I have the following test method:
[TestMethod]
public void Create_GET_Route_Maps_To_Action()
{
RouteData getRouteData = "~/Interface/Pages/Create".WithMethod(HttpVerbs.Get);
getRouteData.DataTokens.Add("Popup", "true");
getRouteData.DataTokens.Add("WebDirectoryId", "99");
getRouteData.DataTokens.Add("LocaleId", "88");
getRouteData.DataTokens.Add("LayoutId", "77");
getRouteData.ShouldMapTo<PagesController>(c => c.Create(true, 99, 88, 77));
}
Which matches to the following method in my Controller
[HttpGet]
[Popup]
public ViewResult Create(bool? popup, int? webDirectoryId, int? localeId, int? layoutId)
{
PageCreateViewModel pageCreateViewModel = new PageCreateViewModel
{
WebDirectories = GetChildDirectories(pageService.GetAllDirectories().Where(d => d.IsActive).Where(d => d.ParentId == null), ""),
Layouts = Mapper.Map<List<SelectListItem>>(pageService.GetAllLayouts().OrderBy(l => l.Filename)),
Locales = localizationService.GetAllLocales().Where(l => l.IsActive).OrderBy(l => l.LocaleName).Select(l => new SelectListItem { Text = string.Format("{0} ({1})", l.LocaleName, l.IETFLanguageTag), Value = l.LocaleId.ToString() })
};
return View(pageCreateViewModel);
}
I get the following error and I'm at a loss to figure out why.
MvcContrib.TestHelper.AssertionException: Value for parameter 'popup' did not match: expected 'True' but was ''; no value found in the route context action parameter named 'popup' - does your matching route contain a token called 'popup'?
The token names are case sensitive and should match the names of your action parameters and you need to use the Values collection instead of the DataTokens:
So because your action looks like this:
Create(bool? popup, int? webDirectoryId, int? localeId, int? layoutId)
You need to use the same lower-case token names and Values collection:
getRouteData.Values.Add("popup", "true");
getRouteData.Values.Add("webDirectoryId", "99");
getRouteData.Values.Add("localeId", "88");
getRouteData.Values.Add("layoutId", "77");